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:
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user