Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support "if" and pattern match in where condition #89

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,25 @@ select {
} |> conn.SelectAsync<Person>
```

You can use `if` expression in `where` condition, but condition must be of type `bool`:

```F#
select {
for p in personTable do
where (if usePositionFilter then p.Position > 5 else true)
} |> conn.SelectAsync<Person>
```

You can use pattern match expression in `where` condition:

```F#
select {
for p in personTable do
where (match positionFilter with | Some x -> p.Position > x | None -> true)
} |> conn.SelectAsync<Person>
```
(Only usage of `Option`, `Result` and simple custom DU was tested, its possible you will encounter some issues with more complex DU)

NOTE: Do not use the forward pipe `|>` operator in your query expressions because it's not implemented, so don't do it (unless you like exceptions)!

To use LIKE operator in `where` condition, use `like`:
Expand Down
2 changes: 2 additions & 0 deletions src/Dapper.FSharp/MSSQL/Domain.fs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ type Where =
| Binary of Where * BinaryOperation * Where
| Unary of UnaryOperation * Where
| Expr of string
| True
| False
static member (+) (a, b) = Binary(a, And, b)
static member (*) (a, b) = Binary(a, Or, b)
static member (!!) a = Unary (Not, a)
Expand Down
2 changes: 2 additions & 0 deletions src/Dapper.FSharp/MSSQL/Evaluator.fs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ let rec evalWhere (meta:WhereAnalyzer.FieldWhereMetadata list) (w:Where) =
match w with
| Empty -> ""
| Expr expr -> expr
| True -> "1=1"
| False -> "1=0"
| Column (field, comp) ->
let fieldMeta = meta |> List.find (fun x -> x.Key = (field,comp))
let withField op = sprintf "%s %s @%s" (inBrackets fieldMeta.Name) op fieldMeta.ParameterName
Expand Down
111 changes: 94 additions & 17 deletions src/Dapper.FSharp/MSSQL/LinqExpressionVisitors.fs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,12 @@ module SqlPatterns =
| ExpressionType.LessThanOrEqual -> Some (exp :?> BinaryExpression)
| _ -> None

let isOptionType (t: Type) =
let (|IfElse|_|) (exp: Expression) =
match exp.NodeType with
| ExpressionType.Conditional -> Some (exp :?> ConditionalExpression)
| _ -> None

let isOptionType (t: Type) =
t.IsGenericType && t.GetGenericTypeDefinition() = typedefof<Option<_>>

/// A property member, a property wrapped in 'Some', or an option 'Value'.
Expand Down Expand Up @@ -151,7 +156,23 @@ module SqlPatterns =
| MemberTypes.Field -> (m.Member :?> FieldInfo).GetValue(parentObject) |> Some
| MemberTypes.Property -> (m.Member :?> PropertyInfo).GetValue(parentObject) |> Some
| _ -> notImplMsg(sprintf "Unable to unwrap where value for '%s'" m.Member.Name)
| Member m when m.Expression.NodeType = ExpressionType.MemberAccess ->
// Get value from DU case
| Member m when m.Expression.NodeType = ExpressionType.TypeAs ->
match (m.Expression :?> UnaryExpression).Operand with
| Constant c when Reflection.FSharpType.IsUnion c.Type ->
let values = Reflection.FSharpValue.GetUnionFields(c.Value, c.Type) |> snd
match m.Member.Name with
| "Item"
| "Item1" -> Some values.[0]
| "Item2" -> Some values.[1]
| "Item3" -> Some values.[2]
| "Item4" -> Some values.[3]
| "Item5" -> Some values.[4]
| "Item6" -> Some values.[5]
| "Item7" -> Some values.[6]
| _ -> None
| _ -> None
| Member m when m.Expression.NodeType = ExpressionType.MemberAccess ->
// Extract constant value from nested object/properties
notImplMsg "Nested property value extraction is not supported in 'where' statements. Try manually unwrapping and passing in the value."
| Constant c -> Some c.Value
Expand All @@ -166,6 +187,28 @@ module SqlPatterns =
Some null
| _ -> None

let (|ValueOrParameterSubstitute|_|) (sub: Map<string, Expression>) (exp: Expression) =
match exp with
| Value x -> Some x
| Parameter p ->
match sub |> Map.tryFind p.Name with
| Some (Value v) -> Some v
| _ -> None
| _ -> None

let (|GetTagCondition|_|) (exp: Expression) =
match exp.NodeType with
| ExpressionType.Equal ->
let b = exp :?> BinaryExpression
match b.Left with
| MethodCall m when m.Method.Name = "GetTag" ->
match m.Arguments[0], b.Right with
| Constant c, Constant right ->
if (m.Method.Invoke(null, [| c.Value |]) :?> int) = (right.Value :?> int) then Some true else Some false
| _ -> None
| _ -> None
| _ -> None

let getColumnComparison (expType: ExpressionType, value: obj) =
match expType with
| ExpressionType.Equal when (isNull value) -> IsNull
Expand All @@ -188,6 +231,16 @@ let getComparison (expType: ExpressionType) =
| ExpressionType.LessThanOrEqual -> "<="
| _ -> notImplMsg "Unsupported comparison type"

let getComparisonOp (expType: ExpressionType) =
match expType with
| ExpressionType.Equal -> (=)
| ExpressionType.NotEqual -> (<>)
| ExpressionType.GreaterThan -> (>)
| ExpressionType.GreaterThanOrEqual -> (>=)
| ExpressionType.LessThan -> (<)
| ExpressionType.LessThanOrEqual -> (<=)
| _ -> notImplMsg "Unsupported comparison type"

let rec unwrapListExpr (lstValues: obj list, lstExp: MethodCallExpression) =
if lstExp.Arguments.Count > 0 then
match lstExp.Arguments.[0] with
Expand All @@ -197,13 +250,21 @@ let rec unwrapListExpr (lstValues: obj list, lstExp: MethodCallExpression) =
lstValues

let visitWhere<'T> (filter: Expression<Func<'T, bool>>) (qualifyColumn: MemberInfo -> string) =
let rec visit (exp: Expression) : Where =
let rec visitSubParam (sub: Map<string, Expression>) (exp: Expression) : Where =
let visit = visitSubParam sub
match exp with
| Constant c when c.Value = true ->
True
| Constant c when c.Value = false ->
False
| Lambda x -> visit x.Body
| Not x ->
let operand = visit x.Operand
Unary (Not, operand)
| MethodCall m when m.Method.Name = "Invoke" ->
match Seq.tryHead m.Arguments, m.Object with
| Some x, Lambda l -> visitSubParam (sub |> Map.add l.Parameters[0].Name x) m.Object
| _ ->
// Handle tuples
visit m.Object
| MethodCall m when List.contains m.Method.Name [ "isIn"; "isNotIn" ] ->
Expand All @@ -218,7 +279,7 @@ let visitWhere<'T> (filter: Expression<Func<'T, bool>>) (qualifyColumn: MemberIn
| _ -> notImpl()
| MethodCall m when List.contains m.Method.Name [ "like"; "notLike" ] ->
match m.Arguments.[0], m.Arguments.[1] with
| Property p, Value value ->
| Property p, ValueOrParameterSubstitute sub value ->
let pattern = string value
match m.Method.Name with
| "like" -> Column ((qualifyColumn p), (Like pattern))
Expand All @@ -231,37 +292,53 @@ let visitWhere<'T> (filter: Expression<Func<'T, bool>>) (qualifyColumn: MemberIn
then Column (qualifyColumn p, ColumnComparison.IsNull)
else Column (qualifyColumn p, ColumnComparison.IsNotNull)
| _ -> notImpl()
// support pattern match
| GetTagCondition b -> if b then True else False
| BinaryAnd x ->
let lt = visit x.Left
let rt = visit x.Right
Binary (lt, And, rt)
| BinaryOr x ->
let lt = visit x.Left
let rt = visit x.Right
Binary (lt, Or, rt)
match visit x.Left with
| False -> False // short circuit, because `if c then x else false` is converted to c && x
| lt ->
let rt = visit x.Right
Binary (lt, And, rt)
| BinaryOr x ->
match visit x.Left with
| True -> True // short circuit, because `if c then true else x` is converted to c || x
| lt ->
let rt = visit x.Right
Binary (lt, Or, rt)
| BinaryCompare x ->
match x.Left, x.Right with
match x.Left, x.Right with
| Property p1, Property p2 ->
// Handle col to col comparisons
let lt = qualifyColumn p1
let cp = getComparison exp.NodeType
let rt = qualifyColumn p2
Expr (sprintf "%s %s %s" lt cp rt)
| Property p, Value value
| Value value, Property p ->
| Property p, ValueOrParameterSubstitute sub value
| ValueOrParameterSubstitute sub value, Property p ->
// Handle column to value comparisons
let columnComparison = getColumnComparison(exp.NodeType, value)
Column (qualifyColumn p, columnComparison)
| Value v1, Value v2 ->
| ValueOrParameterSubstitute sub v1, ValueOrParameterSubstitute sub v2 when (v1 :? int) && (v2 :? int) -> if (getComparisonOp exp.NodeType) (v1 :?> int) (v2 :?> int) then True else False
| ValueOrParameterSubstitute sub v1, ValueOrParameterSubstitute sub v2 when (v1 :? bool) && (v2 :? bool) -> if (getComparisonOp exp.NodeType) (v1 :?> bool) (v2 :?> bool) then True else False
| ValueOrParameterSubstitute sub v1, ValueOrParameterSubstitute sub v2 ->
// Not implemented because I didn't want to embed logic to properly format strings, dates, etc.
// This can be easily added later if it is implemented in Dapper.FSharp.
notImplMsg("Value to value comparisons are not currently supported. Ex: where (1 = 1)")
notImplMsg("Value to value comparisons are currently supported only for int and bool. Ex: where (1 = 1)")
| _ ->
notImpl()
| IfElse x ->
match visit x.Test with
| True ->
visit x.IfTrue
| False ->
visit x.IfFalse
| _ -> notImplMsg "Only boolean constant as condition is supported."

| _ ->
notImpl()

visit (filter :> Expression)
visitSubParam Map.empty (filter :> Expression)

/// Returns a list of one or more fully qualified column names: ["{schema}.{table}.{column}"]
let visitGroupBy<'T, 'Prop> (propertySelector: Expression<Func<'T, 'Prop>>) (qualifyColumn: MemberInfo -> string) =
Expand Down
4 changes: 3 additions & 1 deletion src/Dapper.FSharp/MSSQL/WhereAnalyzer.fs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ let normalizeParamName (s:string) = s.Replace(".","_")

let rec getWhereMetadata (meta:FieldWhereMetadata list) (w:Where) =
match w with
| Empty -> meta
| Empty
| True
| False
| Expr _ -> meta
| Column (field, comp) ->
let parName =
Expand Down
2 changes: 2 additions & 0 deletions src/Dapper.FSharp/MySQL/Domain.fs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ type Where =
| Binary of Where * BinaryOperation * Where
| Unary of UnaryOperation * Where
| Expr of string
| True
| False
static member (+) (a, b) = Binary(a, And, b)
static member (*) (a, b) = Binary(a, Or, b)
static member (!!) a = Unary (Not, a)
Expand Down
2 changes: 2 additions & 0 deletions src/Dapper.FSharp/MySQL/Evaluator.fs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ let evalOrderDirection = function
let rec evalWhere (meta:WhereAnalyzer.FieldWhereMetadata list) (w:Where) =
match w with
| Empty -> ""
| True -> "TRUE"
| False -> "FALSE"
| Expr expr -> expr
| Column (field, comp) ->
let fieldMeta = meta |> List.find (fun x -> x.Key = (field,comp))
Expand Down
Loading