v3 RC1 #1

Merged
danieljsummers merged 25 commits from merge-projects into main 2024-01-06 20:51:49 +00:00
3 changed files with 263 additions and 243 deletions
Showing only changes of commit f3014acc2a - Show all commits

View File

@ -1,7 +1,5 @@
namespace BitBadger.Documents.Postgres namespace BitBadger.Documents.Postgres
open Npgsql
/// The type of index to generate for the document /// The type of index to generate for the document
[<Struct>] [<Struct>]
type DocumentIndex = type DocumentIndex =
@ -11,11 +9,13 @@ type DocumentIndex =
| Optimized | Optimized
open Npgsql
/// Configuration for document handling /// Configuration for document handling
module Configuration = module Configuration =
/// The data source to use for query execution /// The data source to use for query execution
let mutable private dataSourceValue : Npgsql.NpgsqlDataSource option = None let mutable private dataSourceValue : NpgsqlDataSource option = None
/// Register a data source to use for query execution (disposes the current one if it exists) /// Register a data source to use for query execution (disposes the current one if it exists)
let useDataSource source = let useDataSource source =
@ -38,10 +38,8 @@ module private Helpers =
let internal fromDataSource () = let internal fromDataSource () =
Configuration.dataSource () |> Sql.fromDataSource Configuration.dataSource () |> Sql.fromDataSource
open System.Threading.Tasks
/// Execute a task and ignore the result /// Execute a task and ignore the result
let internal ignoreTask<'T> (it : Task<'T>) = backgroundTask { let internal ignoreTask<'T> (it : System.Threading.Tasks.Task<'T>) = backgroundTask {
let! _ = it let! _ = it
() ()
} }
@ -49,50 +47,36 @@ module private Helpers =
open BitBadger.Documents open BitBadger.Documents
/// Data definition /// Functions for creating parameters
[<RequireQualifiedAccess>] [<AutoOpen>]
module Definition = module Parameters =
/// SQL statement to create a document table /// Create an ID parameter (name "@id", key will be treated as a string)
let createTable name = [<CompiledName "Id">]
$"CREATE TABLE IF NOT EXISTS %s{name} (data JSONB NOT NULL)" let idParam (key: 'TKey) =
"@id", Sql.string (string key)
/// SQL statement to create a key index for a document table /// Create a parameter with a JSON value
let createKey (name : string) = [<CompiledName "Json">]
let tableName = name.Split(".") |> Array.last let jsonParam (name: string) (it: 'TJson) =
$"CREATE UNIQUE INDEX IF NOT EXISTS idx_{tableName}_key ON {name} ((data ->> '{Configuration.idField ()}'))" name, Sql.jsonb (Configuration.serializer().Serialize it)
/// SQL statement to create an index on documents in the specified table /// Create a JSON field parameter (name "@field")
let createIndex (name : string) idxType = [<CompiledName "Field">]
let extraOps = match idxType with Full -> "" | Optimized -> " jsonb_path_ops" let fieldParam (value: obj) =
let tableName = name.Split(".") |> Array.last "@field", Sql.parameter (NpgsqlParameter("@field", value))
$"CREATE INDEX IF NOT EXISTS idx_{tableName} ON {name} USING GIN (data{extraOps})"
/// Definitions that take SqlProps as their last parameter /// An empty parameter sequence
module WithProps = [<CompiledName "None">]
let noParams =
/// Create a document table Seq.empty<string * SqlValue>
let ensureTable name sqlProps = backgroundTask {
do! sqlProps |> Sql.query (createTable name) |> Sql.executeNonQueryAsync |> ignoreTask
do! sqlProps |> Sql.query (createKey name) |> Sql.executeNonQueryAsync |> ignoreTask
}
/// Create an index on documents in the specified table
let ensureIndex name idxType sqlProps =
sqlProps |> Sql.query (createIndex name idxType) |> Sql.executeNonQueryAsync |> ignoreTask
/// Create a document table
let ensureTable name =
WithProps.ensureTable name (fromDataSource ())
let ensureIndex name idxType =
WithProps.ensureIndex name idxType (fromDataSource ())
/// Query construction functions /// Query construction functions
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module Query = module Query =
/// Table and index definition queries
module Definition = module Definition =
/// SQL statement to create a document table /// SQL statement to create a document table
@ -105,7 +89,7 @@ module Query =
let ensureJsonIndex (name: string) idxType = let ensureJsonIndex (name: string) idxType =
let extraOps = match idxType with Full -> "" | Optimized -> " jsonb_path_ops" let extraOps = match idxType with Full -> "" | Optimized -> " jsonb_path_ops"
let tableName = name.Split '.' |> Array.last let tableName = name.Split '.' |> Array.last
$"CREATE INDEX IF NOT EXISTS idx_{tableName} ON {name} USING GIN (data{extraOps})" $"CREATE INDEX IF NOT EXISTS idx_{tableName}_document ON {name} USING GIN (data{extraOps})"
/// Create a WHERE clause fragment to implement a @> (JSON contains) condition /// Create a WHERE clause fragment to implement a @> (JSON contains) condition
let whereDataContains paramName = let whereDataContains paramName =
@ -115,23 +99,6 @@ module Query =
let whereJsonPathMatches paramName = let whereJsonPathMatches paramName =
$"data @? %s{paramName}::jsonpath" $"data @? %s{paramName}::jsonpath"
/// Create a JSONB document parameter
let jsonbDocParam (it: obj) =
Sql.jsonb (Configuration.serializer().Serialize it)
/// Create ID and data parameters for a query
let docParameters<'T> docId (doc: 'T) =
[ "@id", Sql.string docId; "@data", jsonbDocParam doc ]
/// Query to insert a document
let insert tableName =
$"INSERT INTO %s{tableName} VALUES (@data)"
/// Query to save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
let save tableName =
sprintf "INSERT INTO %s VALUES (@data) ON CONFLICT ((data ->> '%s')) DO UPDATE SET data = EXCLUDED.data"
tableName (Configuration.idField ())
/// Queries for counting documents /// Queries for counting documents
module Count = module Count =
@ -196,31 +163,6 @@ module Query =
$"""DELETE FROM %s{tableName} WHERE {whereJsonPathMatches "@path"}""" $"""DELETE FROM %s{tableName} WHERE {whereJsonPathMatches "@path"}"""
/// Functions for creating parameters
[<AutoOpen>]
module Parameters =
/// Create an ID parameter (name "@id", key will be treated as a string)
[<CompiledName "Id">]
let idParam (key: 'TKey) =
"@id", Sql.string (string key)
/// Create a parameter with a JSON value
[<CompiledName "Json">]
let jsonParam (name: string) (it: 'TJson) =
name, Sql.jsonb (Configuration.serializer().Serialize it)
/// Create a JSON field parameter (name "@field")
[<CompiledName "Field">]
let fieldParam (value: obj) =
"@field", Sql.parameter (NpgsqlParameter("@field", value))
/// An empty parameter sequence
[<CompiledName "None">]
let noParams =
Seq.empty<string * SqlValue>
/// Functions for dealing with results /// Functions for dealing with results
[<AutoOpen>] [<AutoOpen>]
module Results = module Results =
@ -251,28 +193,28 @@ module WithProps =
/// Execute a query that returns a list of results /// Execute a query that returns a list of results
[<CompiledName "FSharpList">] [<CompiledName "FSharpList">]
let list<'T> query parameters (mapFunc: RowReader -> 'T) sqlProps = let list<'TDoc> query parameters (mapFunc: RowReader -> 'TDoc) sqlProps =
Sql.query query sqlProps Sql.query query sqlProps
|> Sql.parameters parameters |> Sql.parameters parameters
|> Sql.executeAsync mapFunc |> Sql.executeAsync mapFunc
/// Execute a query that returns a list of results /// Execute a query that returns a list of results
let List<'T>(query, parameters, mapFunc: System.Func<RowReader, 'T>, sqlProps) = backgroundTask { let List<'TDoc>(query, parameters, mapFunc: System.Func<RowReader, 'TDoc>, sqlProps) = backgroundTask {
let! results = list query (List.ofSeq parameters) mapFunc.Invoke sqlProps let! results = list<'TDoc> query (List.ofSeq parameters) mapFunc.Invoke sqlProps
return ResizeArray results return ResizeArray results
} }
/// Execute a query that returns one or no results (returns None if not found) /// Execute a query that returns one or no results; returns None if not found
[<CompiledName "FSharpSingle">] [<CompiledName "FSharpSingle">]
let single<'T> query parameters mapFunc sqlProps = backgroundTask { let single<'TDoc> query parameters mapFunc sqlProps = backgroundTask {
let! results = list<'T> query parameters mapFunc sqlProps let! results = list<'TDoc> query parameters mapFunc sqlProps
return FSharp.Collections.List.tryHead results return FSharp.Collections.List.tryHead results
} }
/// Execute a query that returns one or no results (returns null if not found) /// Execute a query that returns one or no results; returns null if not found
let Single<'T when 'T: null>( let Single<'TDoc when 'TDoc: null>(
query, parameters, mapFunc: System.Func<RowReader, 'T>, sqlProps) = backgroundTask { query, parameters, mapFunc: System.Func<RowReader, 'TDoc>, sqlProps) = backgroundTask {
let! result = single<'T> query (FSharp.Collections.List.ofSeq parameters) mapFunc.Invoke sqlProps let! result = single<'TDoc> query (FSharp.Collections.List.ofSeq parameters) mapFunc.Invoke sqlProps
return Option.toObj result return Option.toObj result
} }
@ -295,21 +237,25 @@ module WithProps =
let Scalar<'T when 'T: struct>(query, parameters, mapFunc: System.Func<RowReader, 'T>, sqlProps) = let Scalar<'T when 'T: struct>(query, parameters, mapFunc: System.Func<RowReader, 'T>, sqlProps) =
scalar<'T> query (FSharp.Collections.List.ofSeq parameters) mapFunc.Invoke sqlProps scalar<'T> query (FSharp.Collections.List.ofSeq parameters) mapFunc.Invoke sqlProps
/// Execute a non-query statement to manipulate a document /// Table and index definition commands
let private executeNonQuery query (document: 'T) sqlProps = module Definition =
sqlProps
|> Sql.query query
|> Sql.parameters [ "@data", Query.jsonbDocParam document ]
|> Sql.executeNonQueryAsync
|> ignoreTask
/// Execute a non-query statement to manipulate a document with an ID specified /// Create a document table
let private executeNonQueryWithId query docId (document: 'T) sqlProps = [<CompiledName "EnsureTable">]
sqlProps let ensureTable name sqlProps = backgroundTask {
|> Sql.query query do! Custom.nonQuery (Query.Definition.ensureTable name) [] sqlProps
|> Sql.parameters (Query.docParameters docId document) do! Custom.nonQuery (Query.Definition.ensureKey name) [] sqlProps
|> Sql.executeNonQueryAsync }
|> ignoreTask
/// Create an index on documents in the specified table
[<CompiledName "EnsureJsonIndex">]
let ensureJsonIndex name idxType sqlProps =
Custom.nonQuery (Query.Definition.ensureJsonIndex name idxType) [] sqlProps
/// Create an index on field(s) within documents in the specified table
[<CompiledName "EnsureFieldIndex">]
let ensureFieldIndex tableName indexName fields sqlProps =
Custom.nonQuery (Query.Definition.ensureIndexOn tableName indexName fields) [] sqlProps
/// Commands to add documents /// Commands to add documents
[<AutoOpen>] [<AutoOpen>]
@ -575,6 +521,26 @@ module Custom =
WithProps.Custom.Scalar<'T>(query, parameters, mapFunc, fromDataSource ()) WithProps.Custom.Scalar<'T>(query, parameters, mapFunc, fromDataSource ())
/// Table and index definition commands
[<RequireQualifiedAccess>]
module Definition =
/// Create a document table
[<CompiledName "EnsureTable">]
let ensureTable name =
WithProps.Definition.ensureTable name (fromDataSource ())
/// Create an index on documents in the specified table
[<CompiledName "EnsureJsonIndex">]
let ensureJsonIndex name idxType =
WithProps.Definition.ensureJsonIndex name idxType (fromDataSource ())
/// Create an index on field(s) within documents in the specified table
[<CompiledName "EnsureFieldIndex">]
let ensureFieldIndex tableName indexName fields =
WithProps.Definition.ensureFieldIndex tableName indexName fields (fromDataSource ())
/// Document writing functions /// Document writing functions
[<AutoOpen>] [<AutoOpen>]
module Document = module Document =

View File

@ -134,8 +134,8 @@ public static class PostgresDb
var sqlProps = Sql.connect(database.ConnectionString); var sqlProps = Sql.connect(database.ConnectionString);
Sql.executeNonQuery(Sql.query(Definition.createTable(TableName), sqlProps)); Sql.executeNonQuery(Sql.query(Postgres.Query.Definition.EnsureTable(TableName), sqlProps));
Sql.executeNonQuery(Sql.query(Definition.createKey(TableName), sqlProps)); Sql.executeNonQuery(Sql.query(Query.Definition.EnsureKey(TableName), sqlProps));
Postgres.Configuration.useDataSource(MkDataSource(database.ConnectionString)); Postgres.Configuration.useDataSource(MkDataSource(database.ConnectionString));

View File

@ -8,105 +8,121 @@ open BitBadger.Documents.Tests
/// Tests which do not hit the database /// Tests which do not hit the database
let unitTests = let unitTests =
testList "Unit" [ testList "Unit" [
testList "Definition" [ testList "Parameters" [
test "createTable succeeds" { test "idParam succeeds" {
Expect.equal (Definition.createTable PostgresDb.TableName) Expect.equal (idParam 88) ("@id", Sql.string "88") "ID parameter not constructed correctly"
$"CREATE TABLE IF NOT EXISTS {PostgresDb.TableName} (data JSONB NOT NULL)"
"CREATE TABLE statement not constructed correctly"
} }
test "createKey succeeds" { test "jsonParam succeeds" {
Expect.equal (Definition.createKey PostgresDb.TableName) Expect.equal
$"CREATE UNIQUE INDEX IF NOT EXISTS idx_{PostgresDb.TableName}_key ON {PostgresDb.TableName} ((data ->> 'Id'))" (jsonParam "@test" {| Something = "good" |})
"CREATE INDEX for key statement not constructed correctly" ("@test", Sql.jsonb """{"Something":"good"}""")
"JSON parameter not constructed correctly"
} }
test "createIndex succeeds for full index" { test "fieldParam succeeds" {
Expect.equal (Definition.createIndex "schema.tbl" Full) let it = fieldParam 242
"CREATE INDEX IF NOT EXISTS idx_tbl ON schema.tbl USING GIN (data)" Expect.equal (fst it) "@field" "Field parameter name not correct"
"CREATE INDEX statement not constructed correctly" match snd it with
| SqlValue.Parameter value ->
Expect.equal value.ParameterName "@field" "Parameter name not correct"
Expect.equal value.Value 242 "Parameter value not correct"
| _ -> Expect.isTrue false "The parameter was not a Parameter type"
} }
test "createIndex succeeds for JSONB Path Ops index" { test "noParams succeeds" {
Expect.equal (Definition.createIndex PostgresDb.TableName Optimized) Expect.isEmpty noParams "The no-params sequence should be empty"
$"CREATE INDEX IF NOT EXISTS idx_{PostgresDb.TableName} ON {PostgresDb.TableName} USING GIN (data jsonb_path_ops)"
"CREATE INDEX statement not constructed correctly"
} }
] ]
testList "Query" [ testList "Query" [
testList "Definition" [
test "ensureTable succeeds" {
Expect.equal
(Query.Definition.ensureTable PostgresDb.TableName)
$"CREATE TABLE IF NOT EXISTS {PostgresDb.TableName} (data JSONB NOT NULL)"
"CREATE TABLE statement not constructed correctly"
}
test "ensureJsonIndex succeeds for full index" {
Expect.equal
(Query.Definition.ensureJsonIndex "schema.tbl" Full)
"CREATE INDEX IF NOT EXISTS idx_tbl_document ON schema.tbl USING GIN (data)"
"CREATE INDEX statement not constructed correctly"
}
test "ensureJsonIndex succeeds for JSONB Path Ops index" {
Expect.equal
(Query.Definition.ensureJsonIndex PostgresDb.TableName Optimized)
(sprintf "CREATE INDEX IF NOT EXISTS idx_%s_document ON %s USING GIN (data jsonb_path_ops)"
PostgresDb.TableName PostgresDb.TableName)
"CREATE INDEX statement not constructed correctly"
}
]
test "whereDataContains succeeds" { test "whereDataContains succeeds" {
Expect.equal (Query.whereDataContains "@test") "data @> @test" "WHERE clause not correct" Expect.equal (Query.whereDataContains "@test") "data @> @test" "WHERE clause not correct"
} }
test "whereJsonPathMatches succeeds" { test "whereJsonPathMatches succeeds" {
Expect.equal (Query.whereJsonPathMatches "@path") "data @? @path::jsonpath" "WHERE clause not correct" Expect.equal (Query.whereJsonPathMatches "@path") "data @? @path::jsonpath" "WHERE clause not correct"
} }
test "jsonbDocParam succeeds" {
Expect.equal (Query.jsonbDocParam {| Hello = "There" |}) (Sql.jsonb "{\"Hello\":\"There\"}")
"JSONB document not serialized correctly"
}
test "docParameters succeeds" {
let parameters = Query.docParameters "abc123" {| Testing = 456 |}
let expected = [
"@id", Sql.string "abc123"
"@data", Sql.jsonb "{\"Testing\":456}"
]
Expect.equal parameters expected "There should have been 2 parameters, one string and one JSONB"
}
test "insert succeeds" {
Expect.equal (Query.insert PostgresDb.TableName) $"INSERT INTO {PostgresDb.TableName} VALUES (@data)"
"INSERT statement not correct"
}
test "save succeeds" {
Expect.equal (Query.save PostgresDb.TableName)
$"INSERT INTO {PostgresDb.TableName} VALUES (@data) ON CONFLICT ((data ->> 'Id')) DO UPDATE SET data = EXCLUDED.data"
"INSERT ON CONFLICT UPDATE statement not correct"
}
testList "Count" [ testList "Count" [
test "byContains succeeds" { test "byContains succeeds" {
Expect.equal (Query.Count.byContains PostgresDb.TableName) Expect.equal
(Query.Count.byContains PostgresDb.TableName)
$"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data @> @criteria" $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data @> @criteria"
"JSON containment count query not correct" "JSON containment count query not correct"
} }
test "byJsonPath succeeds" { test "byJsonPath succeeds" {
Expect.equal (Query.Count.byJsonPath PostgresDb.TableName) Expect.equal
(Query.Count.byJsonPath PostgresDb.TableName)
$"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath" $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath"
"JSON Path match count query not correct" "JSON Path match count query not correct"
} }
] ]
testList "Exists" [ testList "Exists" [
test "byContains succeeds" { test "byContains succeeds" {
Expect.equal (Query.Exists.byContains PostgresDb.TableName) Expect.equal
(Query.Exists.byContains PostgresDb.TableName)
$"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data @> @criteria) AS it" $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data @> @criteria) AS it"
"JSON containment exists query not correct" "JSON containment exists query not correct"
} }
test "byJsonPath succeeds" { test "byJsonPath succeeds" {
Expect.equal (Query.Exists.byJsonPath PostgresDb.TableName) Expect.equal
(Query.Exists.byJsonPath PostgresDb.TableName)
$"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath) AS it" $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath) AS it"
"JSON Path match existence query not correct" "JSON Path match existence query not correct"
} }
] ]
testList "Find" [ testList "Find" [
test "byContains succeeds" { test "byContains succeeds" {
Expect.equal (Query.Find.byContains PostgresDb.TableName) Expect.equal
(Query.Find.byContains PostgresDb.TableName)
$"SELECT data FROM {PostgresDb.TableName} WHERE data @> @criteria" $"SELECT data FROM {PostgresDb.TableName} WHERE data @> @criteria"
"SELECT by JSON containment query not correct" "SELECT by JSON containment query not correct"
} }
test "byJsonPath succeeds" { test "byJsonPath succeeds" {
Expect.equal (Query.Find.byJsonPath PostgresDb.TableName) Expect.equal
(Query.Find.byJsonPath PostgresDb.TableName)
$"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath" $"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath"
"SELECT by JSON Path match query not correct" "SELECT by JSON Path match query not correct"
} }
] ]
testList "Update" [ testList "Update" [
test "partialById succeeds" { test "partialById succeeds" {
Expect.equal (Query.Update.partialById PostgresDb.TableName) Expect.equal
(Query.Update.partialById PostgresDb.TableName)
$"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Id' = @id" $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Id' = @id"
"UPDATE partial by ID statement not correct" "UPDATE partial by ID statement not correct"
} }
test "partialByField succeeds" {
Expect.equal
(Query.Update.partialByField PostgresDb.TableName "Snail" LT)
$"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Snail' < @field"
"UPDATE partial by ID statement not correct"
}
test "partialByContains succeeds" { test "partialByContains succeeds" {
Expect.equal (Query.Update.partialByContains PostgresDb.TableName) Expect.equal
(Query.Update.partialByContains PostgresDb.TableName)
$"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @> @criteria" $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @> @criteria"
"UPDATE partial by JSON containment statement not correct" "UPDATE partial by JSON containment statement not correct"
} }
test "partialByJsonPath succeeds" { test "partialByJsonPath succeeds" {
Expect.equal (Query.Update.partialByJsonPath PostgresDb.TableName) Expect.equal
(Query.Update.partialByJsonPath PostgresDb.TableName)
$"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @? @path::jsonpath" $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @? @path::jsonpath"
"UPDATE partial by JSON Path statement not correct" "UPDATE partial by JSON Path statement not correct"
} }
@ -126,7 +142,6 @@ let unitTests =
] ]
] ]
open Npgsql.FSharp
open ThrowawayDb.Postgres open ThrowawayDb.Postgres
open Types open Types
@ -163,17 +178,82 @@ let integrationTests =
"Data source should have been the same" "Data source should have been the same"
} }
] ]
testList "Custom" [
testList "list" [
testTask "succeeds when data is found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! docs = Custom.list (Query.selectFromTable PostgresDb.TableName) [] fromData<JsonDocument>
Expect.hasCountOf docs 5u isTrue "There should have been 5 documents returned"
}
testTask "succeeds when data is not found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! docs =
Custom.list $"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath"
[ "@path", Sql.string "$.NumValue ? (@ > 100)" ] fromData<JsonDocument>
Expect.isEmpty docs "There should have been no documents returned"
}
]
testList "single" [
testTask "succeeds when a row is found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! doc =
Custom.single $"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id"
[ "@id", Sql.string "one"] fromData<JsonDocument>
Expect.isSome doc "There should have been a document returned"
Expect.equal doc.Value.Id "one" "The incorrect document was returned"
}
testTask "succeeds when a row is not found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! doc =
Custom.single $"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id"
[ "@id", Sql.string "eighty" ] fromData<JsonDocument>
Expect.isNone doc "There should not have been a document returned"
}
]
testList "nonQuery" [
testTask "succeeds when operating on data" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
do! Custom.nonQuery $"DELETE FROM {PostgresDb.TableName}" []
let! remaining = Count.all PostgresDb.TableName
Expect.equal remaining 0 "There should be no documents remaining in the table"
}
testTask "succeeds when no data matches where clause" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
do! Custom.nonQuery $"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath"
[ "@path", Sql.string "$.NumValue ? (@ > 100)" ]
let! remaining = Count.all PostgresDb.TableName
Expect.equal remaining 5 "There should be 5 documents remaining in the table"
}
]
testTask "scalar succeeds" {
use db = PostgresDb.BuildDb()
let! nbr = Custom.scalar $"SELECT 5 AS test_value" [] (fun row -> row.int "test_value")
Expect.equal nbr 5 "The query should have returned the number 5"
}
]
testList "Definition" [ testList "Definition" [
testTask "ensureTable succeeds" { testTask "ensureTable succeeds" {
use db = PostgresDb.BuildDb() use db = PostgresDb.BuildDb()
let tableExists () = let tableExists () =
Sql.connect db.ConnectionString Custom.scalar "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it" [] toExists
|> Sql.query "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it"
|> Sql.executeRowAsync (fun row -> row.bool "it")
let keyExists () = let keyExists () =
Sql.connect db.ConnectionString Custom.scalar
|> Sql.query "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it" "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it" [] toExists
|> Sql.executeRowAsync (fun row -> row.bool "it")
let! exists = tableExists () let! exists = tableExists ()
let! alsoExists = keyExists () let! alsoExists = keyExists ()
@ -186,22 +266,38 @@ let integrationTests =
Expect.isTrue exists' "The table should now exist" Expect.isTrue exists' "The table should now exist"
Expect.isTrue alsoExists' "The key index should now exist" Expect.isTrue alsoExists' "The key index should now exist"
} }
testTask "ensureIndex succeeds" { testTask "ensureJsonIndex succeeds" {
use db = PostgresDb.BuildDb() use db = PostgresDb.BuildDb()
let indexExists () = let indexExists () =
Sql.connect db.ConnectionString Custom.scalar
|> Sql.query "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured') AS it" "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_document') AS it"
|> Sql.executeRowAsync (fun row -> row.bool "it") []
toExists
let! exists = indexExists () let! exists = indexExists ()
Expect.isFalse exists "The index should not exist already" Expect.isFalse exists "The index should not exist already"
do! Definition.ensureTable "ensured" do! Definition.ensureTable "ensured"
do! Definition.ensureIndex "ensured" Optimized do! Definition.ensureJsonIndex "ensured" Optimized
let! exists' = indexExists () let! exists' = indexExists ()
Expect.isTrue exists' "The index should now exist" Expect.isTrue exists' "The index should now exist"
// TODO: check for GIN(jsonp_path_ops), write test for "full" index that checks for their absence // TODO: check for GIN(jsonp_path_ops), write test for "full" index that checks for their absence
} }
testTask "ensureFieldIndex succeeds" {
use db = PostgresDb.BuildDb()
let indexExists () =
Custom.scalar
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_test') AS it" [] toExists
let! exists = indexExists ()
Expect.isFalse exists "The index should not exist already"
do! Definition.ensureTable "ensured"
do! Definition.ensureFieldIndex "ensured" "test" [ "Id"; "Category" ]
let! exists' = indexExists ()
Expect.isTrue exists' "The index should now exist"
// TODO: check for field definition
}
] ]
testList "insert" [ testList "insert" [
testTask "succeeds" { testTask "succeeds" {
@ -217,8 +313,11 @@ let integrationTests =
testTask "fails for duplicate key" { testTask "fails for duplicate key" {
use db = PostgresDb.BuildDb() use db = PostgresDb.BuildDb()
do! insert PostgresDb.TableName { emptyDoc with Id = "test" } do! insert PostgresDb.TableName { emptyDoc with Id = "test" }
Expect.throws (fun () -> Expect.throws
insert PostgresDb.TableName {emptyDoc with Id = "test" } |> Async.AwaitTask |> Async.RunSynchronously) (fun () ->
insert PostgresDb.TableName {emptyDoc with Id = "test" }
|> Async.AwaitTask
|> Async.RunSynchronously)
"An exception should have been raised for duplicate document ID insert" "An exception should have been raised for duplicate document ID insert"
} }
] ]
@ -257,6 +356,13 @@ let integrationTests =
let! theCount = Count.all PostgresDb.TableName let! theCount = Count.all PostgresDb.TableName
Expect.equal theCount 5 "There should have been 5 matching documents" Expect.equal theCount 5 "There should have been 5 matching documents"
} }
testTask "byField succeeds" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! theCount = Count.byField PostgresDb.TableName "Value" EQ "purple"
Expect.equal theCount 2 "There should have been 2 matching documents"
}
testTask "byContains succeeds" { testTask "byContains succeeds" {
use db = PostgresDb.BuildDb() use db = PostgresDb.BuildDb()
do! loadDocs () do! loadDocs ()
@ -289,6 +395,22 @@ let integrationTests =
Expect.isFalse exists "There should not have been an existing document" Expect.isFalse exists "There should not have been an existing document"
} }
] ]
testList "byField" [
testTask "succeeds when documents exist" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! exists = Exists.byField PostgresDb.TableName "Sub" EX ""
Expect.isTrue exists "There should have been existing documents"
}
testTask "succeeds when documents do not exist" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! exists = Exists.byField PostgresDb.TableName "NumValue" EQ "six"
Expect.isFalse exists "There should not have been existing documents"
}
]
testList "byContains" [ testList "byContains" [
testTask "succeeds when documents exist" { testTask "succeeds when documents exist" {
use db = PostgresDb.BuildDb() use db = PostgresDb.BuildDb()
@ -605,76 +727,8 @@ let integrationTests =
} }
] ]
] ]
testList "Custom" [
testList "single" [
testTask "succeeds when a row is found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! doc =
Custom.single $"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id"
[ "@id", Sql.string "one"] fromData<JsonDocument>
Expect.isSome doc "There should have been a document returned"
Expect.equal doc.Value.Id "one" "The incorrect document was returned"
}
testTask "succeeds when a row is not found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! doc =
Custom.single $"SELECT data FROM {PostgresDb.TableName} WHERE data ->> 'Id' = @id"
[ "@id", Sql.string "eighty" ] fromData<JsonDocument>
Expect.isNone doc "There should not have been a document returned"
}
]
testList "list" [
testTask "succeeds when data is found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! docs = Custom.list (Query.selectFromTable PostgresDb.TableName) [] fromData<JsonDocument>
Expect.hasCountOf docs 5u isTrue "There should have been 5 documents returned"
}
testTask "succeeds when data is not found" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
let! docs =
Custom.list $"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath"
[ "@path", Sql.string "$.NumValue ? (@ > 100)" ] fromData<JsonDocument>
Expect.isEmpty docs "There should have been no documents returned"
}
]
testList "nonQuery" [
testTask "succeeds when operating on data" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
do! Custom.nonQuery $"DELETE FROM {PostgresDb.TableName}" []
let! remaining = Count.all PostgresDb.TableName
Expect.equal remaining 0 "There should be no documents remaining in the table"
}
testTask "succeeds when no data matches where clause" {
use db = PostgresDb.BuildDb()
do! loadDocs ()
do! Custom.nonQuery $"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath"
[ "@path", Sql.string "$.NumValue ? (@ > 100)" ]
let! remaining = Count.all PostgresDb.TableName
Expect.equal remaining 5 "There should be 5 documents remaining in the table"
}
]
testTask "scalar succeeds" {
use db = PostgresDb.BuildDb()
let! nbr = Custom.scalar $"SELECT 5 AS test_value" [] (fun row -> row.int "test_value")
Expect.equal nbr 5 "The query should have returned the number 5"
}
]
] ]
|> testSequenced |> testSequenced
let all = testList "FSharp.Documents" [ unitTests; integrationTests ] let all = testList "Postgres" [ unitTests; integrationTests ]