RC4 changes (#7)

- Add `In` and `InArray` comparisons
- Replace `Op` with `Comparison` (internal API, but was public)
- Spell out comparisons in `Field` constructor functions

Reviewed-on: #7
This commit was merged in pull request #7.
This commit is contained in:
2024-09-17 02:33:57 +00:00
parent 3bc662c984
commit 168bf0cd14
19 changed files with 883 additions and 569 deletions

View File

@@ -2,39 +2,45 @@
open System.Security.Cryptography
/// The types of logical operations available for JSON fields
[<Struct>]
type Op =
/// The types of comparisons available for JSON fields
type Comparison =
/// Equals (=)
| EQ
| Equal of Value: obj
/// Greater Than (>)
| GT
| Greater of Value: obj
/// Greater Than or Equal To (>=)
| GE
| GreaterOrEqual of Value: obj
/// Less Than (<)
| LT
| Less of Value: obj
/// Less Than or Equal To (<=)
| LE
| LessOrEqual of Value: obj
/// Not Equal to (<>)
| NE
| NotEqual of Value: obj
/// Between (BETWEEN)
| BT
| Between of Min: obj * Max: obj
/// In (IN)
| In of Values: obj seq
/// In Array (PostgreSQL: |?, SQLite: EXISTS / json_each / IN)
| InArray of Table: string * Values: obj seq
/// Exists (IS NOT NULL)
| EX
| Exists
/// Does Not Exist (IS NULL)
| NEX
| NotExists
override this.ToString() =
/// Get the operator SQL for this comparison
member this.OpSql =
match this with
| EQ -> "="
| GT -> ">"
| GE -> ">="
| LT -> "<"
| LE -> "<="
| NE -> "<>"
| BT -> "BETWEEN"
| EX -> "IS NOT NULL"
| NEX -> "IS NULL"
| Equal _ -> "="
| Greater _ -> ">"
| GreaterOrEqual _ -> ">="
| Less _ -> "<"
| LessOrEqual _ -> "<="
| NotEqual _ -> "<>"
| Between _ -> "BETWEEN"
| In _ -> "IN"
| InArray _ -> "?|" // PostgreSQL only; SQL needs a subquery for this
| Exists -> "IS NOT NULL"
| NotExists -> "IS NULL"
/// The dialect in which a command should be rendered
@@ -43,73 +49,129 @@ type Dialect =
| PostgreSQL
| SQLite
/// The format in which an element of a JSON field should be extracted
[<Struct>]
type FieldFormat =
/// Use ->> or #>>; extracts a text (PostgreSQL) or SQL (SQLite) value
| AsSql
/// Use -> or #>; extracts a JSONB (PostgreSQL) or JSON (SQLite) value
| AsJson
/// Criteria for a field WHERE clause
type Field = {
/// The name of the field
Name: string
type Field =
{ /// The name of the field
Name: string
/// The operation by which the field will be compared
Op: Op
/// The comparison for the field
Comparison: Comparison
/// The value of the field
Value: obj
/// The name of the parameter for this field
ParameterName: string option
/// The name of the parameter for this field
ParameterName: string option
/// The table qualifier for this field
Qualifier: string option }
with
/// The table qualifier for this field
Qualifier: string option
} with
/// Create a comparison against a field
static member Where name comparison =
{ Name = name; Comparison = comparison; ParameterName = None; Qualifier = None }
/// Create an equals (=) field criterion
static member EQ name (value: obj) =
{ Name = name; Op = EQ; Value = value; ParameterName = None; Qualifier = None }
static member Equal name (value: obj) =
Field.Where name (Equal value)
/// Create an equals (=) field criterion (alias)
static member EQ name (value: obj) = Field.Equal name value
/// Create a greater than (>) field criterion
static member GT name (value: obj) =
{ Name = name; Op = GT; Value = value; ParameterName = None; Qualifier = None }
static member Greater name (value: obj) =
Field.Where name (Greater value)
/// Create a greater than (>) field criterion (alias)
static member GT name (value: obj) = Field.Greater name value
/// Create a greater than or equal to (>=) field criterion
static member GE name (value: obj) =
{ Name = name; Op = GE; Value = value; ParameterName = None; Qualifier = None }
static member GreaterOrEqual name (value: obj) =
Field.Where name (GreaterOrEqual value)
/// Create a greater than or equal to (>=) field criterion (alias)
static member GE name (value: obj) = Field.GreaterOrEqual name value
/// Create a less than (<) field criterion
static member LT name (value: obj) =
{ Name = name; Op = LT; Value = value; ParameterName = None; Qualifier = None }
static member Less name (value: obj) =
Field.Where name (Less value)
/// Create a less than (<) field criterion (alias)
static member LT name (value: obj) = Field.Less name value
/// Create a less than or equal to (<=) field criterion
static member LE name (value: obj) =
{ Name = name; Op = LE; Value = value; ParameterName = None; Qualifier = None }
static member LessOrEqual name (value: obj) =
Field.Where name (LessOrEqual value)
/// Create a less than or equal to (<=) field criterion (alias)
static member LE name (value: obj) = Field.LessOrEqual name value
/// Create a not equals (<>) field criterion
static member NE name (value: obj) =
{ Name = name; Op = NE; Value = value; ParameterName = None; Qualifier = None }
static member NotEqual name (value: obj) =
Field.Where name (NotEqual value)
/// Create a BETWEEN field criterion
static member BT name (min: obj) (max: obj) =
{ Name = name; Op = BT; Value = [ min; max ]; ParameterName = None; Qualifier = None }
/// Create a not equals (<>) field criterion (alias)
static member NE name (value: obj) = Field.NotEqual name value
/// Create a Between field criterion
static member Between name (min: obj) (max: obj) =
Field.Where name (Between(min, max))
/// Create a Between field criterion (alias)
static member BT name (min: obj) (max: obj) = Field.Between name min max
/// Create an In field criterion
static member In name (values: obj seq) =
Field.Where name (In values)
/// Create an In field criterion (alias)
static member IN name (values: obj seq) = Field.In name values
/// Create an InArray field criterion
static member InArray name tableName (values: obj seq) =
Field.Where name (InArray(tableName, values))
/// Create an exists (IS NOT NULL) field criterion
static member EX name =
{ Name = name; Op = EX; Value = obj (); ParameterName = None; Qualifier = None }
static member Exists name =
Field.Where name Exists
/// Create an exists (IS NOT NULL) field criterion (alias)
static member EX name = Field.Exists name
/// Create a not exists (IS NULL) field criterion
static member NEX name =
{ Name = name; Op = NEX; Value = obj (); ParameterName = None; Qualifier = None }
static member NotExists name =
Field.Where name NotExists
/// Create a not exists (IS NULL) field criterion (alias)
static member NEX name = Field.NotExists name
/// Transform a field name (a.b.c) to a path for the given SQL dialect
static member NameToPath (name: string) dialect =
static member NameToPath (name: string) dialect format =
let path =
if name.Contains '.' then
match dialect with
| PostgreSQL -> "#>>'{" + String.concat "," (name.Split '.') + "}'"
| SQLite -> "->>'" + String.concat "'->>'" (name.Split '.') + "'"
else $"->>'{name}'"
| PostgreSQL ->
(match format with AsJson -> "#>" | AsSql -> "#>>")
+ "'{" + String.concat "," (name.Split '.') + "}'"
| SQLite ->
let parts = name.Split '.'
let last = Array.last parts
let final = (match format with AsJson -> "'->'" | AsSql -> "'->>'") + $"{last}'"
"->'" + String.concat "'->'" (Array.truncate (Array.length parts - 1) parts) + final
else
match format with AsJson -> $"->'{name}'" | AsSql -> $"->>'{name}'"
$"data{path}"
/// Create a field with a given name, but no other properties filled (op will be EQ, value will be "")
static member Named name =
{ Name = name; Op = EQ; Value = ""; ParameterName = None; Qualifier = None }
Field.Where name (Equal "")
/// Specify the name of the parameter for this field
member this.WithParameterName name =
@@ -120,8 +182,9 @@ type Field = {
{ this with Qualifier = Some alias }
/// Get the qualified path to the field
member this.Path dialect =
(this.Qualifier |> Option.map (fun q -> $"{q}.") |> Option.defaultValue "") + Field.NameToPath this.Name dialect
member this.Path dialect format =
(this.Qualifier |> Option.map (fun q -> $"{q}.") |> Option.defaultValue "")
+ Field.NameToPath this.Name dialect format
/// How fields should be matched
@@ -337,7 +400,7 @@ module Query =
let parts = it.Split ' '
let fieldName = if Array.length parts = 1 then it else parts[0]
let direction = if Array.length parts < 2 then "" else $" {parts[1]}"
$"({Field.NameToPath fieldName dialect}){direction}")
$"({Field.NameToPath fieldName dialect AsSql}){direction}")
|> String.concat ", "
$"CREATE INDEX IF NOT EXISTS idx_{tbl}_%s{indexName} ON {tableName} ({jsonFields})"
@@ -403,11 +466,13 @@ module Query =
|> Seq.map (fun (field, direction) ->
if field.Name.StartsWith "n:" then
let f = { field with Name = field.Name[2..] }
match dialect with PostgreSQL -> $"({f.Path PostgreSQL})::numeric" | SQLite -> f.Path SQLite
match dialect with
| PostgreSQL -> $"({f.Path PostgreSQL AsSql})::numeric"
| SQLite -> f.Path SQLite AsSql
elif field.Name.StartsWith "i:" then
let p = { field with Name = field.Name[2..] }.Path dialect
let p = { field with Name = field.Name[2..] }.Path dialect AsSql
match dialect with PostgreSQL -> $"LOWER({p})" | SQLite -> $"{p} COLLATE NOCASE"
else field.Path dialect
else field.Path dialect AsSql
|> function path -> path + defaultArg direction "")
|> String.concat ", "
|> function it -> $" ORDER BY {it}"