diff --git a/src/Common/Library.fs b/src/Common/Library.fs index bf6f9bd..03308b7 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -19,6 +19,10 @@ type Op = | NE /// Between (BETWEEN) | BT + /// In (IN) + | IN + // Array Contains/Exists (PostgreSQL: |? SQLite: EXISTS / json_each / IN) + //| AEX /// Exists (IS NOT NULL) | EX /// Does Not Exist (IS NULL) @@ -33,6 +37,7 @@ type Op = | LE -> "<=" | NE -> "<>" | BT -> "BETWEEN" + | IN -> "IN" | EX -> "IS NOT NULL" | NEX -> "IS NULL" @@ -89,6 +94,10 @@ type Field = { static member BT name (min: obj) (max: obj) = { Name = name; Op = BT; Value = [ min; max ]; ParameterName = None; Qualifier = None } + /// Create an IN field criterion + static member IN name (values: obj seq) = + { Name = name; Op = IN; Value = values; ParameterName = None; Qualifier = None } + /// Create an exists (IS NOT NULL) field criterion static member EX name = { Name = name; Op = EX; Value = obj (); ParameterName = None; Qualifier = None } diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index 4095549..fd37426 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -96,6 +96,13 @@ module Parameters = parameterFor (List.head values) (fun v -> Sql.parameter (NpgsqlParameter($"{p}min", v)))) yield ($"{p}max", parameterFor (List.last values) (fun v -> Sql.parameter (NpgsqlParameter($"{p}max", v)))) + | IN -> + let p = name.Derive it.ParameterName + yield! + it.Value :?> obj seq + |> Seq.mapi (fun idx v -> + let paramName = $"{p}_{idx}" + paramName, Sql.parameter (NpgsqlParameter(paramName, v))) | _ -> let p = name.Derive it.ParameterName yield (p, parameterFor it.Value (fun v -> Sql.parameter (NpgsqlParameter(p, v)))) }) @@ -138,6 +145,10 @@ module Query = let param, value = match it.Op with | BT -> $"{p}min AND {p}max", (it.Value :?> obj list)[0] + | IN -> + let values = it.Value :?> obj seq + let paramNames = values |> Seq.mapi (fun idx _ -> $"{p}_{idx}") |> String.concat ", " + $"({paramNames})", defaultArg (Seq.tryHead values) (obj ()) | _ -> p, it.Value if isNumeric value then $"({it.Path PostgreSQL})::numeric {it.Op} {param}" diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index d34d3f1..4458ccb 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -42,6 +42,10 @@ module Query = | BT -> let p = name.Derive it.ParameterName $"{it.Path SQLite} {it.Op} {p}min AND {p}max" + | IN -> + let p = name.Derive it.ParameterName + let paramNames = it.Value :?> obj seq |> Seq.mapi (fun idx _ -> $"{p}_{idx}") |> String.concat ", " + $"{it.Path SQLite} {it.Op} ({paramNames})" | _ -> $"{it.Path SQLite} {it.Op} {name.Derive it.ParameterName}") |> String.concat $" {howMatched} " @@ -110,6 +114,9 @@ module Parameters = let values = it.Value :?> obj list yield SqliteParameter($"{p}min", List.head values) yield SqliteParameter($"{p}max", List.last values) + | IN -> + let p = name.Derive it.ParameterName + yield! it.Value :?> obj seq |> Seq.mapi (fun idx v -> SqliteParameter($"{p}_{idx}", v)) | _ -> yield SqliteParameter(name.Derive it.ParameterName, it.Value) }) |> Seq.collect id |> Seq.append parameters diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index 497e712..fd83d72 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -177,6 +177,18 @@ let queryTests = testList "Query" [ "(data->>'aField')::numeric BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max" "WHERE clause not correct" } + test "succeeds for a field with an IN operator with alphanumeric values" { + Expect.equal + (Query.whereByFields All [ Field.IN "this" [ "a"; "b"; "c" ] ]) + "data->>'this' IN (@field0_0, @field0_1, @field0_2)" + "WHERE clause not correct" + } + test "succeeds for a field with an IN operator with numeric values" { + Expect.equal + (Query.whereByFields All [ Field.IN "this" [ 7; 14; 21 ] ]) + "(data->>'this')::numeric IN (@field0_0, @field0_1, @field0_2)" + "WHERE clause not correct" + } ] testList "whereById" [ test "succeeds for numeric ID" { diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index 1a2e71d..1cf4aa8 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -51,6 +51,12 @@ let queryTests = testList "Query" [ "data->>'aField' BETWEEN @field0min AND @field0max AND data->>'anotherField' BETWEEN @field1min AND @field1max" "WHERE clause not correct" } + test "succeeds for a field with an IN operator" { + Expect.equal + (Query.whereByFields All [ Field.IN "this" [ "a"; "b"; "c" ] ]) + "data->>'this' IN (@field0_0, @field0_1, @field0_2)" + "WHERE clause not correct" + } ] test "whereById succeeds" { Expect.equal (Query.whereById "@id") "data->>'Id' = @id" "WHERE clause not correct"