Move definition queries to common lib

This commit is contained in:
Daniel J. Summers 2023-12-23 23:23:55 -05:00
parent bad95888bf
commit 8b242d0fa3
4 changed files with 110 additions and 191 deletions

View File

@ -119,6 +119,35 @@ module Query =
let whereById paramName = let whereById paramName =
whereByField (Configuration.idField ()) EQ paramName whereByField (Configuration.idField ()) EQ paramName
/// Queries to define tables and indexes
module Definition =
/// SQL statement to create a document table
let ensureTableFor name dataType =
$"CREATE TABLE IF NOT EXISTS %s{name} (data %s{dataType} NOT NULL)"
/// Split a schema and table name
let private splitSchemaAndTable (tableName: string) =
let parts = tableName.Split '.'
if Array.length parts = 1 then "", tableName else parts[0], parts[1]
/// SQL statement to create an index on one or more fields in a JSON document
let ensureIndexOn tableName indexName (fields: string seq) =
let _, tbl = splitSchemaAndTable tableName
let jsonFields =
fields
|> Seq.map (fun it ->
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]}"
$"(data ->> '{fieldName}'){direction}")
|> String.concat ", "
$"CREATE INDEX IF NOT EXISTS idx_{tbl}_%s{indexName} ON {tableName} ({jsonFields})"
/// SQL statement to create a key index for a document table
let ensureKey tableName =
(ensureIndexOn tableName "key" [ Configuration.idField () ]).Replace("INDEX", "UNIQUE INDEX")
/// Query to insert a document /// Query to insert a document
[<CompiledName "Insert">] [<CompiledName "Insert">]
let insert tableName = let insert tableName =

View File

@ -25,41 +25,23 @@ module Configuration =
| None -> invalidOp "Please provide a connection string before attempting data access" | None -> invalidOp "Please provide a connection string before attempting data access"
[<RequireQualifiedAccess>]
module Query =
/// Data definition
module Definition =
/// SQL statement to create a document table
let ensureTable name =
Query.Definition.ensureTableFor name "TEXT"
/// Execute a non-query command /// Execute a non-query command
let internal write (cmd: SqliteCommand) = backgroundTask { let internal write (cmd: SqliteCommand) = backgroundTask {
let! _ = cmd.ExecuteNonQueryAsync() let! _ = cmd.ExecuteNonQueryAsync()
() ()
} }
/// Data definition
[<RequireQualifiedAccess>]
module Definition =
/// SQL statement to create a document table
let createTable name =
$"CREATE TABLE IF NOT EXISTS %s{name} (data TEXT NOT NULL)"
/// SQL statement to create a key index for a document table
let createKey name =
$"CREATE UNIQUE INDEX IF NOT EXISTS idx_%s{name}_key ON {name} ((data ->> '{Configuration.idField ()}'))"
/// Definitions that take a SqliteConnection as their last parameter
module WithConn =
/// Create a document table
let ensureTable name (conn: SqliteConnection) = backgroundTask {
use cmd = conn.CreateCommand()
cmd.CommandText <- createTable name
do! write cmd
cmd.CommandText <- createKey name
do! write cmd
}
/// Create a document table
let ensureTable name =
use conn = Configuration.dbConn ()
WithConn.ensureTable name conn
/// Add a parameter to a SQLite command, ignoring the return value (can still be accessed on cmd via indexing) /// Add a parameter to a SQLite command, ignoring the return value (can still be accessed on cmd via indexing)
let addParam (cmd: SqliteCommand) name (value: obj) = let addParam (cmd: SqliteCommand) name (value: obj) =
@ -118,6 +100,26 @@ open System.Threading.Tasks
/// Versions of queries that accept a SqliteConnection as the last parameter /// Versions of queries that accept a SqliteConnection as the last parameter
module WithConn = module WithConn =
/// Functions to create tables and indexes
[<RequireQualifiedAccess>]
module Definition =
/// Create a document table
let ensureTable name (conn: SqliteConnection) = backgroundTask {
use cmd = conn.CreateCommand()
cmd.CommandText <- Query.Definition.ensureTable name
do! write cmd
cmd.CommandText <- Query.Definition.ensureKey name
do! write cmd
}
/// Create an index on a document table
let ensureIndex tableName indexName fields (conn: SqliteConnection) = backgroundTask {
use cmd = conn.CreateCommand()
cmd.CommandText <- Query.Definition.ensureIndexOn tableName indexName fields
do! write cmd
}
/// Insert a new document /// Insert a new document
let insert<'TDoc> tableName (document: 'TDoc) conn = let insert<'TDoc> tableName (document: 'TDoc) conn =
executeNonQuery (Query.insert tableName) document conn executeNonQuery (Query.insert tableName) document conn
@ -280,6 +282,15 @@ module WithConn =
return if isFound then mapFunc rdr else Unchecked.defaultof<'T> return if isFound then mapFunc rdr else Unchecked.defaultof<'T>
} }
/// Functions to create tables and indexes
[<RequireQualifiedAccess>]
module Definition =
/// Create a document table
let ensureTable name =
use conn = Configuration.dbConn ()
WithConn.Definition.ensureTable name conn
/// Insert a new document /// Insert a new document
let insert<'TDoc> tableName (document: 'TDoc) = let insert<'TDoc> tableName (document: 'TDoc) =
use conn = Configuration.dbConn () use conn = Configuration.dbConn ()
@ -411,7 +422,11 @@ module Extensions =
/// Create a document table /// Create a document table
member conn.ensureTable name = member conn.ensureTable name =
Definition.WithConn.ensureTable name conn WithConn.Definition.ensureTable name conn
/// Create an index on a document table
member conn.ensureIndex tableName indexName fields =
WithConn.Definition.ensureIndex tableName indexName fields conn
/// Insert a new document /// Insert a new document
member conn.insert<'TDoc> tableName (document: 'TDoc) = member conn.insert<'TDoc> tableName (document: 'TDoc) =

View File

@ -56,6 +56,36 @@ let all =
"WHERE clause not correct" "WHERE clause not correct"
} }
] ]
testList "Definition" [
test "ensureTableFor succeeds" {
Expect.equal
(Query.Definition.ensureTableFor "my.table" "JSONB")
"CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)"
"CREATE TABLE statement not constructed correctly"
}
testList "ensureKey" [
test "succeeds when a schema is present" {
Expect.equal
(Query.Definition.ensureKey "test.table")
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data ->> 'Id'))"
"CREATE INDEX for key statement with schema not constructed correctly"
}
test "succeeds when a schema is not present" {
Expect.equal
(Query.Definition.ensureKey "table")
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data ->> 'Id'))"
"CREATE INDEX for key statement without schema not constructed correctly"
}
]
test "ensureIndexOn succeeds for multiple fields and directions" {
Expect.equal
(Query.Definition.ensureIndexOn "test.table" "gibberish" [ "taco"; "guac DESC"; "salsa ASC" ])
([ "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table "
"((data ->> 'taco'), (data ->> 'guac') DESC, (data ->> 'salsa') ASC)" ]
|> String.concat "")
"CREATE INDEX for multiple field statement incorrect"
}
]
test "insert succeeds" { test "insert succeeds" {
Expect.equal (Query.insert tbl) $"INSERT INTO {tbl} VALUES (@data)" "INSERT statement not correct" Expect.equal (Query.insert tbl) $"INSERT INTO {tbl} VALUES (@data)" "INSERT statement not correct"
} }

View File

@ -49,169 +49,16 @@ module Db =
} }
/// A function that always returns true
let isTrue<'T> (_ : 'T) = true
open BitBadger.Documents open BitBadger.Documents
open Expecto open Expecto
open Microsoft.Data.Sqlite open Microsoft.Data.Sqlite
/// Tests which do not hit the database /// These tests each use a fresh copy of a SQLite database
let unitTests = let all =
testList "Unit" [
testList "Definition" [
test "createTable succeeds" {
Expect.equal (Definition.createTable Db.tableName)
$"CREATE TABLE IF NOT EXISTS {Db.tableName} (data TEXT NOT NULL)"
"CREATE TABLE statement not constructed correctly"
}
test "createKey succeeds" {
Expect.equal (Definition.createKey Db.tableName)
$"CREATE UNIQUE INDEX IF NOT EXISTS idx_{Db.tableName}_key ON {Db.tableName} ((data ->> 'Id'))"
"CREATE INDEX for key statement not constructed correctly"
}
]
testList "Op" [
test "EQ succeeds" {
Expect.equal (string EQ) "=" "The equals operator was not correct"
}
test "GT succeeds" {
Expect.equal (string GT) ">" "The greater than operator was not correct"
}
test "GE succeeds" {
Expect.equal (string GE) ">=" "The greater than or equal to operator was not correct"
}
test "LT succeeds" {
Expect.equal (string LT) "<" "The less than operator was not correct"
}
test "LE succeeds" {
Expect.equal (string LE) "<=" "The less than or equal to operator was not correct"
}
test "NE succeeds" {
Expect.equal (string NE) "<>" "The not equal to operator was not correct"
}
test "EX succeeds" {
Expect.equal (string EX) "IS NOT NULL" """The "exists" operator ws not correct"""
}
test "NEX succeeds" {
Expect.equal (string NEX) "IS NULL" """The "not exists" operator ws not correct"""
}
]
testList "Query" [
test "selectFromTable succeeds" {
Expect.equal
(Query.selectFromTable Db.tableName)
$"SELECT data FROM {Db.tableName}"
"SELECT statement not correct"
}
test "whereById succeeds" {
Expect.equal (Query.whereById "@id") "data ->> 'Id' = @id" "WHERE clause not correct"
}
testList "whereByField" [
test "succeeds when a logical operator is passed" {
Expect.equal
(Query.whereByField "theField" GT "@test")
"data ->> 'theField' > @test"
"WHERE clause not correct"
}
test "succeeds when an existence operator is passed" {
Expect.equal
(Query.whereByField "thatField" NEX "")
"data ->> 'thatField' IS NULL"
"WHERE clause not correct"
}
]
test "insert succeeds" {
Expect.equal
(Query.insert Db.tableName)
$"INSERT INTO {Db.tableName} VALUES (@data)"
"INSERT statement not correct"
}
test "save succeeds" {
Expect.equal
(Query.save Db.tableName)
$"INSERT INTO {Db.tableName} VALUES (@data) ON CONFLICT ((data ->> 'Id')) DO UPDATE SET data = EXCLUDED.data"
"INSERT ON CONFLICT UPDATE statement not correct"
}
testList "Count" [
test "all succeeds" {
Expect.equal
(Query.Count.all Db.tableName)
$"SELECT COUNT(*) AS it FROM {Db.tableName}"
"Count query not correct"
}
test "byField succeeds" {
Expect.equal
(Query.Count.byField Db.tableName "thatField" EQ)
$"SELECT COUNT(*) AS it FROM {Db.tableName} WHERE data ->> 'thatField' = @field"
"JSON field text comparison count query not correct"
}
]
testList "Exists" [
test "byId succeeds" {
Expect.equal
(Query.Exists.byId Db.tableName)
$"SELECT EXISTS (SELECT 1 FROM {Db.tableName} WHERE data ->> 'Id' = @id) AS it"
"ID existence query not correct"
}
test "byField succeeds" {
Expect.equal
(Query.Exists.byField Db.tableName "Test" LT)
$"SELECT EXISTS (SELECT 1 FROM {Db.tableName} WHERE data ->> 'Test' < @field) AS it"
"JSON field text comparison exists query not correct"
}
]
testList "Find" [
test "byId succeeds" {
Expect.equal
(Query.Find.byId Db.tableName)
$"SELECT data FROM {Db.tableName} WHERE data ->> 'Id' = @id"
"SELECT by ID query not correct"
}
test "byField succeeds" {
Expect.equal
(Query.Find.byField Db.tableName "Golf" GE)
$"SELECT data FROM {Db.tableName} WHERE data ->> 'Golf' >= @field"
"SELECT by JSON text comparison query not correct"
}
]
testList "Update" [
test "full succeeds" {
Expect.equal
(Query.Update.full Db.tableName)
$"UPDATE {Db.tableName} SET data = @data WHERE data ->> 'Id' = @id"
"UPDATE full statement not correct"
}
test "partialById succeeds" {
Expect.equal
(Query.Update.partialById Db.tableName)
$"UPDATE {Db.tableName} SET data = json_patch(data, json(@data)) WHERE data ->> 'Id' = @id"
"UPDATE partial by ID statement not correct"
}
test "partialByField succeeds" {
Expect.equal
(Query.Update.partialByField Db.tableName "Part" NE)
$"UPDATE {Db.tableName} SET data = json_patch(data, json(@data)) WHERE data ->> 'Part' <> @field"
"UPDATE partial by JSON containment statement not correct"
}
]
testList "Delete" [
test "byId succeeds" {
Expect.equal
(Query.Delete.byId Db.tableName)
$"DELETE FROM {Db.tableName} WHERE data ->> 'Id' = @id"
"DELETE by ID query not correct"
}
test "byField succeeds" {
Expect.equal
(Query.Delete.byField Db.tableName "gone" EQ)
$"DELETE FROM {Db.tableName} WHERE data ->> 'gone' = @field"
"DELETE by JSON containment query not correct"
}
]
]
]
let isTrue<'T> (_ : 'T) = true
let integrationTests =
let documents = [ let documents = [
{ Id = "one"; Value = "FIRST!"; NumValue = 0; Sub = None } { Id = "one"; Value = "FIRST!"; NumValue = 0; Sub = None }
{ Id = "two"; Value = "another"; NumValue = 10; Sub = Some { Foo = "green"; Bar = "blue" } } { Id = "two"; Value = "another"; NumValue = 10; Sub = Some { Foo = "green"; Bar = "blue" } }
@ -1116,5 +963,3 @@ let integrationTests =
} }
] ]
|> testSequenced |> testSequenced
let all = testList "Sqlite" [ unitTests; integrationTests ]