From 8d3385b70bfeded2935107d0087b92b6a4d0ef17 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 26 Dec 2023 12:54:37 -0500 Subject: [PATCH] Integrate PostgreSQL with common lib - Add "byField" functions to Postgres impl --- src/Common/Library.fs | 12 - src/Postgres/Library.fs | 635 +++++++++++++++++--------- src/Sqlite/Library.fs | 15 + src/Tests.CSharp/CommonCSharpTests.cs | 113 ++--- src/Tests.CSharp/SqliteCSharpTests.cs | 23 + src/Tests/CommonTests.fs | 12 - src/Tests/PostgresTests.fs | 57 +-- src/Tests/Program.fs | 1 + src/Tests/SqliteTests.fs | 22 + 9 files changed, 549 insertions(+), 341 deletions(-) diff --git a/src/Common/Library.fs b/src/Common/Library.fs index 7123a52..0c85cce 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -210,18 +210,6 @@ module Query = let full tableName = $"""UPDATE %s{tableName} SET data = @data WHERE {whereById "@id"}""" - /// Query to update a partial document by its ID - [] - let partialById tableName = - $"""UPDATE %s{tableName} SET data = json_patch(data, json(@data)) WHERE {whereById "@id"}""" - - /// Query to update a partial document via a comparison on a JSON field - [] - let partialByField tableName fieldName op = - sprintf - "UPDATE %s SET data = json_patch(data, json(@data)) WHERE %s" - tableName (whereByField fieldName op "@field") - /// Queries to delete documents module Delete = diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index ba7c111..18ba007 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -1,5 +1,7 @@ namespace BitBadger.Documents.Postgres +open Npgsql + /// The type of index to generate for the document [] type DocumentIndex = @@ -91,14 +93,20 @@ module Definition = [] module Query = - /// Create a SELECT clause to retrieve the document data from the given table - let selectFromTable tableName = - $"SELECT data FROM %s{tableName}" - - /// Create a WHERE clause fragment to implement an ID-based query - let whereById paramName = - $"data ->> '{Configuration.idField ()}' = %s{paramName}" - + module Definition = + + /// SQL statement to create a document table + [] + let ensureTable name = + Query.Definition.ensureTableFor name "JSONB" + + /// SQL statement to create an index on JSON documents in the specified table + [] + let ensureJsonIndex (name : string) idxType = + let extraOps = match idxType with Full -> "" | Optimized -> " jsonb_path_ops" + let tableName = name.Split '.' |> Array.last + $"CREATE INDEX IF NOT EXISTS idx_{tableName} ON {name} USING GIN (data{extraOps})" + /// Create a WHERE clause fragment to implement a @> (JSON contains) condition let whereDataContains paramName = $"data @> %s{paramName}" @@ -127,10 +135,6 @@ module Query = /// Queries for counting documents module Count = - /// Query to count all documents in a table - let all tableName = - $"SELECT COUNT(*) AS it FROM %s{tableName}" - /// Query to count matching documents using a JSON containment query (@>) let byContains tableName = $"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereDataContains "@criteria"}""" @@ -142,10 +146,6 @@ module Query = /// Queries for determining document existence module Exists = - /// Query to determine if a document exists for the given ID - let byId tableName = - $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereById "@id"}) AS it""" - /// Query to determine if documents exist using a JSON containment query (@>) let byContains tableName = $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereDataContains "@criteria"}) AS it""" @@ -157,28 +157,24 @@ module Query = /// Queries for retrieving documents module Find = - /// Query to retrieve a document by its ID - let byId tableName = - $"""{selectFromTable tableName} WHERE {whereById "@id"}""" - /// Query to retrieve documents using a JSON containment query (@>) let byContains tableName = - $"""{selectFromTable tableName} WHERE {whereDataContains "@criteria"}""" + $"""{Query.selectFromTable tableName} WHERE {whereDataContains "@criteria"}""" /// Query to retrieve documents using a JSON Path match (@?) let byJsonPath tableName = - $"""{selectFromTable tableName} WHERE {whereJsonPathMatches "@path"}""" + $"""{Query.selectFromTable tableName} WHERE {whereJsonPathMatches "@path"}""" /// Queries to update documents module Update = - /// Query to update a document - let full tableName = - $"""UPDATE %s{tableName} SET data = @data WHERE {whereById "@id"}""" - /// Query to update a document let partialById tableName = - $"""UPDATE %s{tableName} SET data = data || @data WHERE {whereById "@id"}""" + $"""UPDATE %s{tableName} SET data = data || @data WHERE {Query.whereById "@id"}""" + + /// Query to update a document + let partialByField tableName fieldName op = + $"""UPDATE %s{tableName} SET data = data || @data WHERE {Query.whereByField fieldName op "@field"}""" /// Query to update partial documents matching a JSON containment query (@>) let partialByContains tableName = @@ -191,10 +187,6 @@ module Query = /// Queries to delete documents module Delete = - /// Query to delete a document by its ID - let byId tableName = - $"""DELETE FROM %s{tableName} WHERE {whereById "@id"}""" - /// Query to delete documents using a JSON containment query (@>) let byContains tableName = $"""DELETE FROM %s{tableName} WHERE {whereDataContains "@criteria"}""" @@ -204,6 +196,31 @@ module Query = $"""DELETE FROM %s{tableName} WHERE {whereJsonPathMatches "@path"}""" +/// Functions for creating parameters +[] +module Parameters = + + /// Create an ID parameter (name "@id", key will be treated as a string) + [] + let idParam (key: 'TKey) = + "@id", Sql.string (string key) + + /// Create a parameter with a JSON value + [] + let jsonParam (name: string) (it: 'TJson) = + name, Sql.jsonb (Configuration.serializer().Serialize it) + + /// Create a JSON field parameter (name "@field") + [] + let fieldParam (value: obj) = + "@field", Sql.parameter (NpgsqlParameter("@field", value)) + + /// An empty parameter sequence + [] + let noParams = + Seq.empty + + /// Functions for dealing with results [] module Results = @@ -215,11 +232,69 @@ module Results = /// Create a domain item from a document let fromData<'T> row : 'T = fromDocument "data" row + + /// Extract a count from the column "it" + let toCount (row: RowReader) = + row.int "it" + + /// Extract a true/false value from the column "it" + let toExists (row: RowReader) = + row.bool "it" /// Versions of queries that accept SqlProps as the last parameter module WithProps = + /// Commands to execute custom SQL queries + [] + module Custom = + + /// Execute a query that returns a list of results + [] + let list<'T> query parameters (mapFunc: RowReader -> 'T) sqlProps = + Sql.query query sqlProps + |> Sql.parameters parameters + |> Sql.executeAsync mapFunc + + /// Execute a query that returns a list of results + let List<'T>(query, parameters, mapFunc: System.Func, sqlProps) = backgroundTask { + let! results = list query (List.ofSeq parameters) mapFunc.Invoke sqlProps + return ResizeArray results + } + + /// Execute a query that returns one or no results (returns None if not found) + [] + let single<'T> query parameters mapFunc sqlProps = backgroundTask { + let! results = list<'T> query parameters mapFunc sqlProps + return FSharp.Collections.List.tryHead results + } + + /// Execute a query that returns one or no results (returns null if not found) + let Single<'T when 'T: null>( + query, parameters, mapFunc: System.Func, sqlProps) = backgroundTask { + let! result = single<'T> query (FSharp.Collections.List.ofSeq parameters) mapFunc.Invoke sqlProps + return Option.toObj result + } + + /// Execute a query that returns no results + [] + let nonQuery query parameters sqlProps = + Sql.query query sqlProps + |> Sql.parameters (FSharp.Collections.List.ofSeq parameters) + |> Sql.executeNonQueryAsync + |> ignoreTask + + /// Execute a query that returns a scalar value + [] + let scalar<'T when 'T: struct> query parameters (mapFunc: RowReader -> 'T) sqlProps = + Sql.query query sqlProps + |> Sql.parameters parameters + |> Sql.executeRowAsync mapFunc + + /// Execute a query that returns a scalar value + let Scalar<'T when 'T: struct>(query, parameters, mapFunc: System.Func, sqlProps) = + scalar<'T> query (FSharp.Collections.List.ofSeq parameters) mapFunc.Invoke sqlProps + /// Execute a non-query statement to manipulate a document let private executeNonQuery query (document: 'T) sqlProps = sqlProps @@ -236,208 +311,283 @@ module WithProps = |> Sql.executeNonQueryAsync |> ignoreTask - /// Insert a new document - let insert<'T> tableName (document: 'T) sqlProps = - executeNonQuery (Query.insert tableName) document sqlProps + /// Commands to add documents + [] + module Document = + + /// Insert a new document + [] + let insert<'TDoc> tableName (document: 'TDoc) sqlProps = + Custom.nonQuery (Query.insert tableName) [ jsonParam "@data" document ] sqlProps - /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") - let save<'T> tableName (document: 'T) sqlProps = - executeNonQuery (Query.save tableName) document sqlProps + /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") + [] + let save<'TDoc> tableName (document: 'TDoc) sqlProps = + Custom.nonQuery (Query.save tableName) [ jsonParam "@data" document ] sqlProps /// Commands to count documents [] module Count = /// Count all documents in a table + [] let all tableName sqlProps = - sqlProps - |> Sql.query (Query.Count.all tableName) - |> Sql.executeRowAsync (fun row -> row.int "it") + Custom.scalar (Query.Count.all tableName) [] toCount sqlProps + + /// Count matching documents using a JSON field comparison (->> =) + [] + let byField tableName fieldName op (value: obj) sqlProps = + Custom.scalar (Query.Count.byField tableName fieldName op) [ fieldParam value ] toCount sqlProps /// Count matching documents using a JSON containment query (@>) - let byContains tableName (criteria: obj) sqlProps = - sqlProps - |> Sql.query (Query.Count.byContains tableName) - |> Sql.parameters [ "@criteria", Query.jsonbDocParam criteria ] - |> Sql.executeRowAsync (fun row -> row.int "it") + [] + let byContains tableName (criteria: 'TContains) sqlProps = + Custom.scalar (Query.Count.byContains tableName) [ jsonParam "@criteria" criteria ] toCount sqlProps /// Count matching documents using a JSON Path match query (@?) + [] let byJsonPath tableName jsonPath sqlProps = - sqlProps - |> Sql.query (Query.Count.byJsonPath tableName) - |> Sql.parameters [ "@path", Sql.string jsonPath ] - |> Sql.executeRowAsync (fun row -> row.int "it") + Custom.scalar (Query.Count.byJsonPath tableName) [ "@path", Sql.string jsonPath ] toCount sqlProps /// Commands to determine if documents exist [] module Exists = /// Determine if a document exists for the given ID - let byId tableName docId sqlProps = - sqlProps - |> Sql.query (Query.Exists.byId tableName) - |> Sql.parameters [ "@id", Sql.string docId ] - |> Sql.executeRowAsync (fun row -> row.bool "it") + [] + let byId tableName (docId: 'TKey) sqlProps = + Custom.scalar (Query.Exists.byId tableName) [ idParam docId ] toExists sqlProps + /// Determine if a document exists using a JSON field comparison (->> =) + [] + let byField tableName fieldName op (value: obj) sqlProps = + Custom.scalar (Query.Exists.byField tableName fieldName op) [ fieldParam value ] toExists sqlProps + /// Determine if a document exists using a JSON containment query (@>) - let byContains tableName (criteria: obj) sqlProps = - sqlProps - |> Sql.query (Query.Exists.byContains tableName) - |> Sql.parameters [ "@criteria", Query.jsonbDocParam criteria ] - |> Sql.executeRowAsync (fun row -> row.bool "it") + [] + let byContains tableName (criteria: 'TContains) sqlProps = + Custom.scalar (Query.Exists.byContains tableName) [ jsonParam "@criteria" criteria ] toExists sqlProps /// Determine if a document exists using a JSON Path match query (@?) + [] let byJsonPath tableName jsonPath sqlProps = - sqlProps - |> Sql.query (Query.Exists.byJsonPath tableName) - |> Sql.parameters [ "@path", Sql.string jsonPath ] - |> Sql.executeRowAsync (fun row -> row.bool "it") + Custom.scalar (Query.Exists.byJsonPath tableName) [ "@path", Sql.string jsonPath ] toExists sqlProps /// Commands to determine if documents exist [] module Find = /// Retrieve all documents in the given table - let all<'T> tableName sqlProps = - sqlProps - |> Sql.query (Query.selectFromTable tableName) - |> Sql.executeAsync fromData<'T> + [] + let all<'TDoc> tableName sqlProps = + Custom.list<'TDoc> (Query.selectFromTable tableName) [] fromData<'TDoc> sqlProps - /// Retrieve a document by its ID - let byId<'T> tableName docId sqlProps = backgroundTask { - let! results = - sqlProps - |> Sql.query (Query.Find.byId tableName) - |> Sql.parameters [ "@id", Sql.string docId ] - |> Sql.executeAsync fromData<'T> - return List.tryHead results - } + /// Retrieve all documents in the given table + let All<'TDoc>(tableName, sqlProps) = + Custom.List<'TDoc>(Query.selectFromTable tableName, [], fromData<'TDoc>, sqlProps) - /// Execute a JSON containment query (@>) - let byContains<'T> tableName (criteria: obj) sqlProps = - sqlProps - |> Sql.query (Query.Find.byContains tableName) - |> Sql.parameters [ "@criteria", Query.jsonbDocParam criteria ] - |> Sql.executeAsync fromData<'T> + /// Retrieve a document by its ID (returns None if not found) + [] + let byId<'TKey, 'TDoc> tableName (docId: 'TKey) sqlProps = + Custom.single (Query.Find.byId tableName) [ idParam docId ] fromData<'TDoc> sqlProps - /// Execute a JSON Path match query (@?) - let byJsonPath<'T> tableName jsonPath sqlProps = - sqlProps - |> Sql.query (Query.Find.byJsonPath tableName) - |> Sql.parameters [ "@path", Sql.string jsonPath ] - |> Sql.executeAsync fromData<'T> + /// Retrieve a document by its ID (returns null if not found) + let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId: 'TKey, sqlProps) = + Custom.Single<'TDoc>(Query.Find.byId tableName, [ idParam docId ], fromData<'TDoc>, sqlProps) + + /// Retrieve documents matching a JSON field comparison (->> =) + [] + let byField<'TDoc> tableName fieldName op (value: obj) sqlProps = + Custom.list<'TDoc> (Query.Find.byField tableName fieldName op) [ fieldParam value ] fromData<'TDoc> sqlProps - /// Execute a JSON containment query (@>), returning only the first result - let firstByContains<'T> tableName (criteria: obj) sqlProps = backgroundTask { - let! results = byContains<'T> tableName criteria sqlProps - return List.tryHead results - } + /// Retrieve documents matching a JSON field comparison (->> =) + let ByField<'TDoc>(tableName, fieldName, op, value: obj, sqlProps) = + Custom.List<'TDoc>( + Query.Find.byField tableName fieldName op, [ fieldParam value ], fromData<'TDoc>, sqlProps) + + /// Retrieve documents matching a JSON containment query (@>) + [] + let byContains<'TDoc> tableName (criteria: obj) sqlProps = + Custom.list<'TDoc> + (Query.Find.byContains tableName) [ jsonParam "@criteria" criteria ] fromData<'TDoc> sqlProps - /// Execute a JSON Path match query (@?), returning only the first result - let firstByJsonPath<'T> tableName jsonPath sqlProps = backgroundTask { - let! results = byJsonPath<'T> tableName jsonPath sqlProps - return List.tryHead results - } + /// Retrieve documents matching a JSON containment query (@>) + let ByContains<'TDoc>(tableName, criteria: obj, sqlProps) = + Custom.List<'TDoc>( + Query.Find.byContains tableName, [ jsonParam "@criteria" criteria ], fromData<'TDoc>, sqlProps) + + /// Retrieve documents matching a JSON Path match query (@?) + [] + let byJsonPath<'TDoc> tableName jsonPath sqlProps = + Custom.list<'TDoc> + (Query.Find.byJsonPath tableName) [ "@path", Sql.string jsonPath ] fromData<'TDoc> sqlProps + + /// Retrieve documents matching a JSON Path match query (@?) + let ByJsonPath<'TDoc>(tableName, jsonPath, sqlProps) = + Custom.List<'TDoc>( + Query.Find.byJsonPath tableName, [ "@path", Sql.string jsonPath ], fromData<'TDoc>, sqlProps) + + /// Retrieve the first document matching a JSON field comparison (->> =); returns None if not found + [] + let firstByField<'TDoc> tableName fieldName op (value: obj) sqlProps = + Custom.single<'TDoc> + $"{Query.Find.byField tableName fieldName op} LIMIT 1" [ fieldParam value ] fromData<'TDoc> sqlProps + + /// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found + let FirstByField<'TDoc when 'TDoc: null>(tableName, fieldName, op, value: obj, sqlProps) = + Custom.Single<'TDoc>( + $"{Query.Find.byField tableName fieldName op} LIMIT 1", [ fieldParam value ], fromData<'TDoc>, sqlProps) + + /// Retrieve the first document matching a JSON containment query (@>); returns None if not found + [] + let firstByContains<'TDoc> tableName (criteria: obj) sqlProps = + Custom.single<'TDoc> + $"{Query.Find.byContains tableName} LIMIT 1" [ jsonParam "@criteria" criteria ] fromData<'TDoc> sqlProps + + /// Retrieve the first document matching a JSON containment query (@>); returns null if not found + let FirstByContains<'TDoc when 'TDoc: null>(tableName, criteria: obj, sqlProps) = + Custom.Single<'TDoc>( + $"{Query.Find.byContains tableName} LIMIT 1", + [ jsonParam "@criteria" criteria ], + fromData<'TDoc>, + sqlProps) + + /// Retrieve the first document matching a JSON Path match query (@?); returns None if not found + [] + let firstByJsonPath<'TDoc> tableName jsonPath sqlProps = + Custom.single<'TDoc> + $"{Query.Find.byJsonPath tableName} LIMIT 1" [ "@path", Sql.string jsonPath ] fromData<'TDoc> sqlProps + + /// Retrieve the first document matching a JSON Path match query (@?); returns null if not found + let FirstByJsonPath<'TDoc when 'TDoc: null>(tableName, jsonPath, sqlProps) = + Custom.Single<'TDoc>( + $"{Query.Find.byJsonPath tableName} LIMIT 1", + [ "@path", Sql.string jsonPath ], + fromData<'TDoc>, + sqlProps) /// Commands to update documents [] module Update = /// Update an entire document - let full<'T> tableName docId (document: 'T) sqlProps = - executeNonQueryWithId (Query.Update.full tableName) docId document sqlProps + [] + let full tableName (docId: 'TKey) (document: 'TDoc) sqlProps = + Custom.nonQuery (Query.Update.full tableName) [ idParam docId; jsonParam "@data" document ] sqlProps /// Update an entire document - let fullFunc<'T> tableName (idFunc: 'T -> string) (document: 'T) sqlProps = + [] + let fullFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) sqlProps = full tableName (idFunc document) document sqlProps + /// Update an entire document + let FullFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc, sqlProps) = + fullFunc tableName idFunc.Invoke document sqlProps + /// Update a partial document - let partialById tableName docId (partial: obj) sqlProps = - executeNonQueryWithId (Query.Update.partialById tableName) docId partial sqlProps + [] + let partialById tableName (docId: 'TKey) (partial: 'TPartial) sqlProps = + Custom.nonQuery (Query.Update.partialById tableName) [ idParam docId; jsonParam "@data" partial ] sqlProps + + /// Update partial documents using a JSON field comparison query in the WHERE clause (->> =) + [] + let partialByField tableName fieldName op (value: obj) (partial: 'TPartial) sqlProps = + Custom.nonQuery + (Query.Update.partialByField tableName fieldName op) + [ jsonParam "@data" partial; fieldParam value ] + sqlProps /// Update partial documents using a JSON containment query in the WHERE clause (@>) - let partialByContains tableName (criteria: obj) (partial: obj) sqlProps = - sqlProps - |> Sql.query (Query.Update.partialByContains tableName) - |> Sql.parameters [ "@data", Query.jsonbDocParam partial; "@criteria", Query.jsonbDocParam criteria ] - |> Sql.executeNonQueryAsync - |> ignoreTask + [] + let partialByContains tableName (criteria: 'TContains) (partial: 'TPartial) sqlProps = + Custom.nonQuery + (Query.Update.partialByContains tableName) + [ jsonParam "@data" partial; jsonParam "@criteria" criteria ] + sqlProps /// Update partial documents using a JSON Path match query in the WHERE clause (@?) - let partialByJsonPath tableName jsonPath (partial: obj) sqlProps = - sqlProps - |> Sql.query (Query.Update.partialByJsonPath tableName) - |> Sql.parameters [ "@data", Query.jsonbDocParam partial; "@path", Sql.string jsonPath ] - |> Sql.executeNonQueryAsync - |> ignoreTask + [] + let partialByJsonPath tableName jsonPath (partial: 'TPartial) sqlProps = + Custom.nonQuery + (Query.Update.partialByJsonPath tableName) + [ jsonParam "@data" partial; "@path", Sql.string jsonPath ] + sqlProps /// Commands to delete documents [] module Delete = /// Delete a document by its ID - let byId tableName docId sqlProps = - executeNonQueryWithId (Query.Delete.byId tableName) docId {||} sqlProps + [] + let byId tableName (docId: 'TKey) sqlProps = + Custom.nonQuery (Query.Delete.byId tableName) [ idParam docId ] sqlProps + /// Delete documents by matching a JSON field comparison query (->> =) + [] + let byField tableName fieldName op (value: obj) sqlProps = + Custom.nonQuery (Query.Delete.byField tableName fieldName op) [ fieldParam value ] sqlProps + /// Delete documents by matching a JSON contains query (@>) - let byContains tableName (criteria: obj) sqlProps = - sqlProps - |> Sql.query (Query.Delete.byContains tableName) - |> Sql.parameters [ "@criteria", Query.jsonbDocParam criteria ] - |> Sql.executeNonQueryAsync - |> ignoreTask + [] + let byContains tableName (criteria: 'TCriteria) sqlProps = + Custom.nonQuery (Query.Delete.byContains tableName) [ jsonParam "@criteria" criteria ] sqlProps /// Delete documents by matching a JSON Path match query (@?) + [] let byJsonPath tableName path sqlProps = - sqlProps - |> Sql.query (Query.Delete.byJsonPath tableName) - |> Sql.parameters [ "@path", Sql.string path ] - |> Sql.executeNonQueryAsync - |> ignoreTask + Custom.nonQuery (Query.Delete.byJsonPath tableName) [ "@path", Sql.string path ] sqlProps - /// Commands to execute custom SQL queries - [] - module Custom = + +/// Commands to execute custom SQL queries +[] +module Custom = - /// Execute a query that returns one or no results - let single<'T> query parameters (mapFunc: RowReader -> 'T) sqlProps = backgroundTask { - let! results = - Sql.query query sqlProps - |> Sql.parameters parameters - |> Sql.executeAsync mapFunc - return List.tryHead results - } + /// Execute a query that returns a list of results + [] + let list<'TDoc> query parameters (mapFunc: RowReader -> 'TDoc) = + WithProps.Custom.list<'TDoc> query parameters mapFunc (fromDataSource ()) - /// Execute a query that returns a list of results - let list<'T> query parameters (mapFunc: RowReader -> 'T) sqlProps = - Sql.query query sqlProps - |> Sql.parameters parameters - |> Sql.executeAsync mapFunc + /// Execute a query that returns a list of results + let List<'TDoc>(query, parameters, mapFunc: System.Func) = + WithProps.Custom.List<'TDoc>(query, parameters, mapFunc, fromDataSource ()) - /// Execute a query that returns no results - let nonQuery query parameters sqlProps = - Sql.query query sqlProps - |> Sql.parameters (List.ofSeq parameters) - |> Sql.executeNonQueryAsync - |> ignoreTask - - /// Execute a query that returns a scalar value - let scalar<'T when 'T : struct> query parameters (mapFunc: RowReader -> 'T) sqlProps = - Sql.query query sqlProps - |> Sql.parameters parameters - |> Sql.executeRowAsync mapFunc + /// Execute a query that returns one or no results; returns None if not found + [] + let single<'TDoc> query parameters (mapFunc: RowReader -> 'TDoc) = + WithProps.Custom.single<'TDoc> query parameters mapFunc (fromDataSource ()) + + /// Execute a query that returns one or no results; returns null if not found + let Single<'TDoc when 'TDoc: null>(query, parameters, mapFunc: System.Func) = + WithProps.Custom.Single<'TDoc>(query, parameters, mapFunc, fromDataSource ()) + + /// Execute a query that returns no results + [] + let nonQuery query parameters = + WithProps.Custom.nonQuery query parameters (fromDataSource ()) + + /// Execute a query that returns a scalar value + [] + let scalar<'T when 'T: struct> query parameters (mapFunc: RowReader -> 'T) = + WithProps.Custom.scalar query parameters mapFunc (fromDataSource ()) + + /// Execute a query that returns a scalar value + let Scalar<'T when 'T: struct>(query, parameters, mapFunc: System.Func) = + WithProps.Custom.Scalar<'T>(query, parameters, mapFunc, fromDataSource ()) /// Document writing functions [] module Document = + /// Insert a new document - let insert<'T> tableName (document: 'T) = - WithProps.insert tableName document (fromDataSource ()) + [] + let insert<'TDoc> tableName (document: 'TDoc) = + WithProps.Document.insert<'TDoc> tableName document (fromDataSource ()) /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") - let save<'T> tableName (document: 'T) = - WithProps.save<'T> tableName document (fromDataSource ()) + [] + let save<'TDoc> tableName (document: 'TDoc) = + WithProps.Document.save<'TDoc> tableName document (fromDataSource ()) /// Queries to count documents @@ -445,14 +595,22 @@ module Document = module Count = /// Count all documents in a table + [] let all tableName = WithProps.Count.all tableName (fromDataSource ()) + /// Count matching documents using a JSON field comparison query (->> =) + [] + let byField tableName fieldName op (value: obj) = + WithProps.Count.byField tableName fieldName op value (fromDataSource ()) + /// Count matching documents using a JSON containment query (@>) + [] let byContains tableName criteria = WithProps.Count.byContains tableName criteria (fromDataSource ()) /// Count matching documents using a JSON Path match query (@?) + [] let byJsonPath tableName jsonPath = WithProps.Count.byJsonPath tableName jsonPath (fromDataSource ()) @@ -462,14 +620,22 @@ module Count = module Exists = /// Determine if a document exists for the given ID + [] let byId tableName docId = WithProps.Exists.byId tableName docId (fromDataSource ()) - /// Determine if a document exists using a JSON containment query (@>) + /// Determine if documents exist using a JSON field comparison query (->> =) + [] + let byField tableName fieldName op (value: obj) = + WithProps.Exists.byField tableName fieldName op value (fromDataSource ()) + + /// Determine if documents exist using a JSON containment query (@>) + [] let byContains tableName criteria = WithProps.Exists.byContains tableName criteria (fromDataSource ()) - /// Determine if a document exists using a JSON Path match query (@?) + /// Determine if documents exist using a JSON Path match query (@?) + [] let byJsonPath tableName jsonPath = WithProps.Exists.byJsonPath tableName jsonPath (fromDataSource ()) @@ -479,28 +645,76 @@ module Exists = module Find = /// Retrieve all documents in the given table - let all<'T> tableName = - WithProps.Find.all<'T> tableName (fromDataSource ()) + [] + let all<'TDoc> tableName = + WithProps.Find.all<'TDoc> tableName (fromDataSource ()) - /// Retrieve a document by its ID - let byId<'T> tableName docId = - WithProps.Find.byId<'T> tableName docId (fromDataSource ()) + /// Retrieve all documents in the given table + let All<'TDoc> tableName = + WithProps.Find.All<'TDoc>(tableName, fromDataSource ()) - /// Execute a JSON containment query (@>) - let byContains<'T> tableName criteria = - WithProps.Find.byContains<'T> tableName criteria (fromDataSource ()) + /// Retrieve a document by its ID; returns None if not found + [] + let byId<'TKey, 'TDoc> tableName docId = + WithProps.Find.byId<'TKey, 'TDoc> tableName docId (fromDataSource ()) - /// Execute a JSON Path match query (@?) - let byJsonPath<'T> tableName jsonPath = - WithProps.Find.byJsonPath<'T> tableName jsonPath (fromDataSource ()) + /// Retrieve a document by its ID; returns null if not found + let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId: 'TKey) = + WithProps.Find.ById<'TKey, 'TDoc>(tableName, docId, fromDataSource ()) + + /// Retrieve documents matching a JSON field comparison query (->> =) + [] + let byField<'TDoc> tableName fieldName op (value: obj) = + WithProps.Find.byField<'TDoc> tableName fieldName op value (fromDataSource ()) - /// Execute a JSON containment query (@>), returning only the first result - let firstByContains<'T> tableName (criteria: obj) = - WithProps.Find.firstByContains<'T> tableName criteria (fromDataSource ()) + /// Retrieve documents matching a JSON field comparison query (->> =) + let ByField<'TDoc>(tableName, fieldName, op, value: obj) = + WithProps.Find.ByField<'TDoc>(tableName, fieldName, op, value, fromDataSource ()) + + /// Retrieve documents matching a JSON containment query (@>) + [] + let byContains<'TDoc> tableName (criteria: obj) = + WithProps.Find.byContains<'TDoc> tableName criteria (fromDataSource ()) - /// Execute a JSON Path match query (@?), returning only the first result - let firstByJsonPath<'T> tableName jsonPath = - WithProps.Find.firstByJsonPath<'T> tableName jsonPath (fromDataSource ()) + /// Retrieve documents matching a JSON containment query (@>) + let ByContains<'TDoc>(tableName, criteria: obj) = + WithProps.Find.ByContains<'TDoc>(tableName, criteria, fromDataSource ()) + + /// Retrieve documents matching a JSON Path match query (@?) + [] + let byJsonPath<'TDoc> tableName jsonPath = + WithProps.Find.byJsonPath<'TDoc> tableName jsonPath (fromDataSource ()) + + /// Retrieve documents matching a JSON Path match query (@?) + let ByJsonPath<'TDoc>(tableName, jsonPath) = + WithProps.Find.ByJsonPath<'TDoc>(tableName, jsonPath, fromDataSource ()) + + /// Retrieve the first document matching a JSON field comparison query (->> =); returns None if not found + [] + let firstByField<'TDoc> tableName fieldName op (value: obj) = + WithProps.Find.firstByField<'TDoc> tableName fieldName op value (fromDataSource ()) + + /// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found + let FirstByField<'TDoc when 'TDoc: null>(tableName, fieldName, op, value: obj) = + WithProps.Find.FirstByField<'TDoc>(tableName, fieldName, op, value, fromDataSource ()) + + /// Retrieve the first document matching a JSON containment query (@>); returns None if not found + [] + let firstByContains<'TDoc> tableName (criteria: obj) = + WithProps.Find.firstByContains<'TDoc> tableName criteria (fromDataSource ()) + + /// Retrieve the first document matching a JSON containment query (@>); returns null if not found + let FirstByContains<'TDoc when 'TDoc: null>(tableName, criteria: obj) = + WithProps.Find.FirstByContains<'TDoc>(tableName, criteria, fromDataSource ()) + + /// Retrieve the first document matching a JSON Path match query (@?); returns None if not found + [] + let firstByJsonPath<'TDoc> tableName jsonPath = + WithProps.Find.firstByJsonPath<'TDoc> tableName jsonPath (fromDataSource ()) + + /// Retrieve the first document matching a JSON Path match query (@?); returns null if not found + let FirstByJsonPath<'TDoc when 'TDoc: null>(tableName, jsonPath) = + WithProps.Find.FirstByJsonPath<'TDoc>(tableName, jsonPath, fromDataSource ()) /// Commands to update documents @@ -508,23 +722,37 @@ module Find = module Update = /// Update a full document - let full<'T> tableName docId (document: 'T) = - WithProps.Update.full<'T> tableName docId document (fromDataSource ()) + [] + let full tableName (docId: 'TKey) (document: 'TDoc) = + WithProps.Update.full tableName docId document (fromDataSource ()) /// Update a full document - let fullFunc<'T> tableName idFunc (document: 'T) = - WithProps.Update.fullFunc<'T> tableName idFunc document (fromDataSource ()) + [] + let fullFunc tableName (idFunc: 'TDoc -> 'TKey) (document: 'TDoc) = + WithProps.Update.fullFunc tableName idFunc document (fromDataSource ()) + + /// Update a full document + let FullFunc(tableName, idFunc: System.Func<'TDoc, 'TKey>, document: 'TDoc) = + WithProps.Update.FullFunc(tableName, idFunc, document, fromDataSource ()) /// Update a partial document - let partialById tableName docId (partial: obj) = + [] + let partialById tableName (docId: 'TKey) (partial: 'TPartial) = WithProps.Update.partialById tableName docId partial (fromDataSource ()) + /// Update partial documents using a JSON field comparison query in the WHERE clause (->> =) + [] + let partialByField tableName fieldName op (value: obj) (partial: 'TPartial) = + WithProps.Update.partialByField tableName fieldName op value partial (fromDataSource ()) + /// Update partial documents using a JSON containment query in the WHERE clause (@>) - let partialByContains tableName (criteria: obj) (partial: obj) = + [] + let partialByContains tableName (criteria: 'TCriteria) (partial: 'TPartial) = WithProps.Update.partialByContains tableName criteria partial (fromDataSource ()) /// Update partial documents using a JSON Path match query in the WHERE clause (@?) - let partialByJsonPath tableName jsonPath (partial: obj) = + [] + let partialByJsonPath tableName jsonPath (partial: 'TPartial) = WithProps.Update.partialByJsonPath tableName jsonPath partial (fromDataSource ()) @@ -533,34 +761,21 @@ module Update = module Delete = /// Delete a document by its ID - let byId tableName docId = + [] + let byId tableName (docId: 'TKey) = WithProps.Delete.byId tableName docId (fromDataSource ()) - /// Delete documents by matching a JSON contains query (@>) - let byContains tableName (criteria: obj) = + /// Delete documents by matching a JSON field comparison query (->> =) + [] + let byField tableName fieldName op (value: obj) = + WithProps.Delete.byField tableName fieldName op value (fromDataSource ()) + + /// Delete documents by matching a JSON containment query (@>) + [] + let byContains tableName (criteria: 'TContains) = WithProps.Delete.byContains tableName criteria (fromDataSource ()) /// Delete documents by matching a JSON Path match query (@?) + [] let byJsonPath tableName path = WithProps.Delete.byJsonPath tableName path (fromDataSource ()) - - -/// Commands to execute custom SQL queries -[] -module Custom = - - /// Execute a query that returns one or no results - let single<'T> query parameters (mapFunc: RowReader -> 'T) = - WithProps.Custom.single query parameters mapFunc (fromDataSource ()) - - /// Execute a query that returns a list of results - let list<'T> query parameters (mapFunc: RowReader -> 'T) = - WithProps.Custom.list query parameters mapFunc (fromDataSource ()) - - /// Execute a query that returns no results - let nonQuery query parameters = - WithProps.Custom.nonQuery query parameters (fromDataSource ()) - - /// Execute a query that returns a scalar value - let scalar<'T when 'T: struct> query parameters (mapFunc: RowReader -> 'T) = - WithProps.Custom.scalar query parameters mapFunc (fromDataSource ()) diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index 7f372db..21e82f2 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -38,6 +38,21 @@ module Query = [] let ensureTable name = Query.Definition.ensureTableFor name "TEXT" + + /// Document update queries + module Update = + + /// Query to update a partial document by its ID + [] + let partialById tableName = + $"""UPDATE %s{tableName} SET data = json_patch(data, json(@data)) WHERE {Query.whereById "@id"}""" + + /// Query to update a partial document via a comparison on a JSON field + [] + let partialByField tableName fieldName op = + sprintf + "UPDATE %s SET data = json_patch(data, json(@data)) WHERE %s" + tableName (Query.whereByField fieldName op "@field") /// Parameter handling helpers diff --git a/src/Tests.CSharp/CommonCSharpTests.cs b/src/Tests.CSharp/CommonCSharpTests.cs index a90cb8f..4d755a5 100644 --- a/src/Tests.CSharp/CommonCSharpTests.cs +++ b/src/Tests.CSharp/CommonCSharpTests.cs @@ -1,9 +1,10 @@ -namespace BitBadger.Documents.Tests.CSharp; - -using Documents; using Expecto.CSharp; using Expecto; +namespace BitBadger.Documents.Tests.CSharp; + +using static Runner; + /// /// A test serializer that returns known values /// @@ -21,13 +22,14 @@ public static class CommonCSharpTests /// /// Unit tests /// - [Tests] public static Test Unit = - Runner.TestList("Common.C# Unit", new[] + [Tests] + public static Test Unit = + TestList("Common.C# Unit", new[] { - Runner.TestSequenced( - Runner.TestList("Configuration", new[] + TestSequenced( + TestList("Configuration", new[] { - Runner.TestCase("UseSerializer succeeds", () => + TestCase("UseSerializer succeeds", () => { try { @@ -49,12 +51,12 @@ public static class CommonCSharpTests Configuration.UseSerializer(DocumentSerializer.Default); } }), - Runner.TestCase("Serializer returns configured serializer", () => + TestCase("Serializer returns configured serializer", () => { Expect.isTrue(ReferenceEquals(DocumentSerializer.Default, Configuration.Serializer()), "Serializer should have been the same"); }), - Runner.TestCase("UseIdField / IdField succeeds", () => + TestCase("UseIdField / IdField succeeds", () => { try { @@ -69,89 +71,89 @@ public static class CommonCSharpTests } }) })), - Runner.TestList("Op", new[] + TestList("Op", new[] { - Runner.TestCase("EQ succeeds", () => + TestCase("EQ succeeds", () => { Expect.equal(Op.EQ.ToString(), "=", "The equals operator was not correct"); }), - Runner.TestCase("GT succeeds", () => + TestCase("GT succeeds", () => { Expect.equal(Op.GT.ToString(), ">", "The greater than operator was not correct"); }), - Runner.TestCase("GE succeeds", () => + TestCase("GE succeeds", () => { Expect.equal(Op.GE.ToString(), ">=", "The greater than or equal to operator was not correct"); }), - Runner.TestCase("LT succeeds", () => + TestCase("LT succeeds", () => { Expect.equal(Op.LT.ToString(), "<", "The less than operator was not correct"); }), - Runner.TestCase("LE succeeds", () => + TestCase("LE succeeds", () => { Expect.equal(Op.LE.ToString(), "<=", "The less than or equal to operator was not correct"); }), - Runner.TestCase("NE succeeds", () => + TestCase("NE succeeds", () => { Expect.equal(Op.NE.ToString(), "<>", "The not equal to operator was not correct"); }), - Runner.TestCase("EX succeeds", () => + TestCase("EX succeeds", () => { Expect.equal(Op.EX.ToString(), "IS NOT NULL", "The \"exists\" operator was not correct"); }), - Runner.TestCase("NEX succeeds", () => + TestCase("NEX succeeds", () => { Expect.equal(Op.NEX.ToString(), "IS NULL", "The \"not exists\" operator was not correct"); }) }), - Runner.TestList("Query", new[] + TestList("Query", new[] { - Runner.TestCase("SelectFromTable succeeds", () => + TestCase("SelectFromTable succeeds", () => { Expect.equal(Query.SelectFromTable("test.table"), "SELECT data FROM test.table", "SELECT statement not correct"); }), - Runner.TestCase("WhereById succeeds", () => + TestCase("WhereById succeeds", () => { Expect.equal(Query.WhereById("@id"), "data ->> 'Id' = @id", "WHERE clause not correct"); }), - Runner.TestList("WhereByField", new[] + TestList("WhereByField", new[] { - Runner.TestCase("succeeds when a logical operator is passed", () => + TestCase("succeeds when a logical operator is passed", () => { Expect.equal(Query.WhereByField("theField", Op.GT, "@test"), "data ->> 'theField' > @test", "WHERE clause not correct"); }), - Runner.TestCase("succeeds when an existence operator is passed", () => + TestCase("succeeds when an existence operator is passed", () => { Expect.equal(Query.WhereByField("thatField", Op.NEX, ""), "data ->> 'thatField' IS NULL", "WHERE clause not correct"); }) }), - Runner.TestList("Definition", new[] + TestList("Definition", new[] { - Runner.TestCase("EnsureTableFor succeeds", () => + TestCase("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"); }), - Runner.TestList("EnsureKey", new[] + TestList("EnsureKey", new[] { - Runner.TestCase("succeeds when a schema is present", () => + TestCase("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"); }), - Runner.TestCase("succeeds when a schema is not present", () => + TestCase("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"); }) }), - Runner.TestCase("EnsureIndexOn succeeds for multiple fields and directions", () => + TestCase("EnsureIndexOn succeeds for multiple fields and directions", () => { Expect.equal( Query.Definition.EnsureIndexOn("test.table", "gibberish", @@ -161,87 +163,72 @@ public static class CommonCSharpTests "CREATE INDEX for multiple field statement incorrect"); }) }), - Runner.TestCase("Insert succeeds", () => + TestCase("Insert succeeds", () => { Expect.equal(Query.Insert("tbl"), "INSERT INTO tbl VALUES (@data)", "INSERT statement not correct"); }), - Runner.TestCase("Save succeeds", () => + TestCase("Save succeeds", () => { Expect.equal(Query.Save("tbl"), $"INSERT INTO tbl VALUES (@data) ON CONFLICT ((data ->> 'Id')) DO UPDATE SET data = EXCLUDED.data", "INSERT ON CONFLICT UPDATE statement not correct"); }), - Runner.TestList("Count", new[] + TestList("Count", new[] { - Runner.TestCase("All succeeds", () => + TestCase("All succeeds", () => { Expect.equal(Query.Count.All("tbl"), "SELECT COUNT(*) AS it FROM tbl", "Count query not correct"); }), - Runner.TestCase("ByField succeeds", () => + TestCase("ByField succeeds", () => { Expect.equal(Query.Count.ByField("tbl", "thatField", Op.EQ), "SELECT COUNT(*) AS it FROM tbl WHERE data ->> 'thatField' = @field", "JSON field text comparison count query not correct"); }) }), - Runner.TestList("Exists", new[] + TestList("Exists", new[] { - Runner.TestCase("ById succeeds", () => + TestCase("ById succeeds", () => { Expect.equal(Query.Exists.ById("tbl"), "SELECT EXISTS (SELECT 1 FROM tbl WHERE data ->> 'Id' = @id) AS it", "ID existence query not correct"); }), - Runner.TestCase("ByField succeeds", () => + TestCase("ByField succeeds", () => { Expect.equal(Query.Exists.ByField("tbl", "Test", Op.LT), "SELECT EXISTS (SELECT 1 FROM tbl WHERE data ->> 'Test' < @field) AS it", "JSON field text comparison exists query not correct"); }) }), - Runner.TestList("Find", new[] + TestList("Find", new[] { - Runner.TestCase("ById succeeds", () => + TestCase("ById succeeds", () => { Expect.equal(Query.Find.ById("tbl"), "SELECT data FROM tbl WHERE data ->> 'Id' = @id", "SELECT by ID query not correct"); }), - Runner.TestCase("ByField succeeds", () => + TestCase("ByField succeeds", () => { Expect.equal(Query.Find.ByField("tbl", "Golf", Op.GE), "SELECT data FROM tbl WHERE data ->> 'Golf' >= @field", "SELECT by JSON comparison query not correct"); }) }), - Runner.TestList("Update", new[] + TestCase("Update.Full succeeds", () => { - Runner.TestCase("Full succeeds", () => - { - Expect.equal(Query.Update.Full("tbl"), "UPDATE tbl SET data = @data WHERE data ->> 'Id' = @id", - "UPDATE full statement not correct"); - }), - Runner.TestCase("PartialById succeeds", () => - { - Expect.equal(Query.Update.PartialById("tbl"), - "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Id' = @id", - "UPDATE partial by ID statement not correct"); - }), - Runner.TestCase("PartialByField succeeds", () => - { - Expect.equal(Query.Update.PartialByField("tbl", "Part", Op.NE), - "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Part' <> @field", - "UPDATE partial by JSON comparison query not correct"); - }) + Expect.equal(Query.Update.Full("tbl"), "UPDATE tbl SET data = @data WHERE data ->> 'Id' = @id", + "UPDATE full statement not correct"); }), - Runner.TestList("Delete", new[] + TestList("Delete", new[] { - Runner.TestCase("ById succeeds", () => + TestCase("ById succeeds", () => { Expect.equal(Query.Delete.ById("tbl"), "DELETE FROM tbl WHERE data ->> 'Id' = @id", "DELETE by ID query not correct"); }), - Runner.TestCase("ByField succeeds", () => + TestCase("ByField succeeds", () => { Expect.equal(Query.Delete.ByField("tbl", "gone", Op.NEX), "DELETE FROM tbl WHERE data ->> 'gone' IS NULL", diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index f1c35c6..e314ec2 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -19,6 +19,29 @@ public static class SqliteCSharpTests public static Test Unit = TestList("Unit", new[] { + TestList("Query", new[] + { + TestCase("Definition.EnsureTable succeeds", () => + { + Expect.equal(Sqlite.Query.Definition.EnsureTable("tbl"), + "CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)", "CREATE TABLE statement not correct"); + }), + TestList("Update", new[] + { + TestCase("PartialById succeeds", () => + { + Expect.equal(Sqlite.Query.Update.PartialById("tbl"), + "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Id' = @id", + "UPDATE partial by ID statement not correct"); + }), + TestCase("PartialByField succeeds", () => + { + Expect.equal(Sqlite.Query.Update.PartialByField("tbl", "Part", Op.NE), + "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Part' <> @field", + "UPDATE partial by JSON comparison query not correct"); + }) + }), + }), TestList("Parameters", new[] { TestCase("Id succeeds", () => diff --git a/src/Tests/CommonTests.fs b/src/Tests/CommonTests.fs index 5dab352..a0ce0d7 100644 --- a/src/Tests/CommonTests.fs +++ b/src/Tests/CommonTests.fs @@ -141,18 +141,6 @@ let all = $"UPDATE {tbl} SET data = @data WHERE data ->> 'Id' = @id" "UPDATE full statement not correct" } - test "partialById succeeds" { - Expect.equal - (Query.Update.partialById tbl) - $"UPDATE {tbl} 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 tbl "Part" NE) - $"UPDATE {tbl} SET data = json_patch(data, json(@data)) WHERE data ->> 'Part' <> @field" - "UPDATE partial by JSON comparison query not correct" - } ] testList "Delete" [ test "byId succeeds" { diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index 0a7f558..afce064 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -1,6 +1,7 @@ module PostgresTests open Expecto +open BitBadger.Documents open BitBadger.Documents.Postgres open BitBadger.Documents.Tests @@ -30,13 +31,6 @@ let unitTests = } ] testList "Query" [ - test "selectFromTable succeeds" { - Expect.equal (Query.selectFromTable PostgresDb.TableName) $"SELECT data FROM {PostgresDb.TableName}" - "SELECT statement not correct" - } - test "whereById succeeds" { - Expect.equal (Query.whereById "@id") "data ->> 'Id' = @id" "WHERE clause not correct" - } test "whereDataContains succeeds" { Expect.equal (Query.whereDataContains "@test") "data @> @test" "WHERE clause not correct" } @@ -65,10 +59,6 @@ let unitTests = "INSERT ON CONFLICT UPDATE statement not correct" } testList "Count" [ - test "all succeeds" { - Expect.equal (Query.Count.all PostgresDb.TableName) $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName}" - "Count query not correct" - } test "byContains succeeds" { Expect.equal (Query.Count.byContains PostgresDb.TableName) $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data @> @criteria" @@ -81,11 +71,6 @@ let unitTests = } ] testList "Exists" [ - test "byId succeeds" { - Expect.equal (Query.Exists.byId PostgresDb.TableName) - $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id) AS it" - "ID existence query not correct" - } test "byContains succeeds" { Expect.equal (Query.Exists.byContains PostgresDb.TableName) $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data @> @criteria) AS it" @@ -98,10 +83,6 @@ let unitTests = } ] testList "Find" [ - test "byId succeeds" { - Expect.equal (Query.Find.byId PostgresDb.TableName) - $"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id" "SELECT by ID query not correct" - } test "byContains succeeds" { Expect.equal (Query.Find.byContains PostgresDb.TableName) $"SELECT data FROM {PostgresDb.TableName} WHERE data @> @criteria" @@ -114,11 +95,6 @@ let unitTests = } ] testList "Update" [ - test "full succeeds" { - Expect.equal (Query.Update.full PostgresDb.TableName) - $"UPDATE {PostgresDb.TableName} SET data = @data WHERE data ->> 'Id' = @id" - "UPDATE full statement not correct" - } test "partialById succeeds" { Expect.equal (Query.Update.partialById PostgresDb.TableName) $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Id' = @id" @@ -136,10 +112,6 @@ let unitTests = } ] testList "Delete" [ - test "byId succeeds" { - Expect.equal (Query.Delete.byId PostgresDb.TableName) $"DELETE FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id" - "DELETE by ID query not correct" - } test "byContains succeeds" { Expect.equal (Query.Delete.byContains PostgresDb.TableName) $"DELETE FROM {PostgresDb.TableName} WHERE data @> @criteria" @@ -266,14 +238,14 @@ let integrationTests = let testDoc = { emptyDoc with Id = "test"; Sub = Some { Foo = "a"; Bar = "b" } } do! insert PostgresDb.TableName testDoc - let! before = Find.byId PostgresDb.TableName "test" - if Option.isNone before then Expect.isTrue false "There should have been a document returned" + let! before = Find.byId PostgresDb.TableName "test" + Expect.isSome before "There should have been a document returned" Expect.equal before.Value testDoc "The document is not correct" let upd8Doc = { testDoc with Sub = Some { Foo = "c"; Bar = "d" } } do! save PostgresDb.TableName upd8Doc - let! after = Find.byId PostgresDb.TableName "test" - if Option.isNone after then Expect.isTrue false "There should have been a document returned post-update" + let! after = Find.byId PostgresDb.TableName "test" + Expect.isSome after "There should have been a document returned post-update" Expect.equal after.Value upd8Doc "The updated document is not correct" } ] @@ -378,7 +350,7 @@ let integrationTests = use db = PostgresDb.BuildDb() do! loadDocs () - let! doc = Find.byId PostgresDb.TableName "two" + let! doc = Find.byId PostgresDb.TableName "two" Expect.isTrue (Option.isSome doc) "There should have been a document returned" Expect.equal doc.Value.Id "two" "The incorrect document was returned" } @@ -386,7 +358,7 @@ let integrationTests = use db = PostgresDb.BuildDb() do! loadDocs () - let! doc = Find.byId PostgresDb.TableName "three hundred eighty-seven" + let! doc = Find.byId PostgresDb.TableName "three hundred eighty-seven" Expect.isFalse (Option.isSome doc) "There should not have been a document returned" } ] @@ -481,9 +453,8 @@ let integrationTests = let testDoc = { emptyDoc with Id = "one"; Sub = Some { Foo = "blue"; Bar = "red" } } do! Update.full PostgresDb.TableName "one" testDoc - let! after = Find.byId PostgresDb.TableName "one" - if Option.isNone after then - Expect.isTrue false "There should have been a document returned post-update" + let! after = Find.byId PostgresDb.TableName "one" + Expect.isSome after "There should have been a document returned post-update" Expect.equal after.Value testDoc "The updated document is not correct" } testTask "succeeds when no document is updated" { @@ -504,9 +475,8 @@ let integrationTests = do! Update.fullFunc PostgresDb.TableName (_.Id) { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } - let! after = Find.byId PostgresDb.TableName "one" - if Option.isNone after then - Expect.isTrue false "There should have been a document returned post-update" + let! after = Find.byId PostgresDb.TableName "one" + Expect.isSome after "There should have been a document returned post-update" Expect.equal after.Value { Id = "one"; Value = "le un"; NumValue = 1; Sub = None } "The updated document is not correct" } @@ -526,9 +496,8 @@ let integrationTests = do! loadDocs () do! Update.partialById PostgresDb.TableName "one" {| NumValue = 44 |} - let! after = Find.byId PostgresDb.TableName "one" - if Option.isNone after then - Expect.isTrue false "There should have been a document returned post-update" + let! after = Find.byId PostgresDb.TableName "one" + Expect.isSome after "There should have been a document returned post-update" Expect.equal after.Value.NumValue 44 "The updated document is not correct" } testTask "succeeds when no document is updated" { diff --git a/src/Tests/Program.fs b/src/Tests/Program.fs index 3d185f5..2f16be6 100644 --- a/src/Tests/Program.fs +++ b/src/Tests/Program.fs @@ -6,6 +6,7 @@ let allTests = "BitBadger.Documents" [ CommonTests.all CommonCSharpTests.Unit + PostgresTests.all SqliteTests.all testSequenced SqliteExtensionTests.integrationTests SqliteCSharpTests.All diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index a632a35..819be57 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -10,6 +10,28 @@ open Types /// Unit tests for the SQLite library let unitTests = testList "Unit" [ + testList "Query" [ + test "Definition.ensureTable succeeds" { + Expect.equal + (Query.Definition.ensureTable "tbl") + "CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)" + "CREATE TABLE statement not correct" + } + testList "Update" [ + test "partialById succeeds" { + Expect.equal + (Query.Update.partialById "tbl") + "UPDATE tbl 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 "tbl" "Part" NE) + "UPDATE tbl SET data = json_patch(data, json(@data)) WHERE data ->> 'Part' <> @field" + "UPDATE partial by JSON comparison query not correct" + } + ] + ] testList "Parameters" [ test "idParam succeeds" { let theParam = idParam 7