v3 RC1 #1
|
@ -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
|
||||||
|
@ -102,10 +86,10 @@ module Query =
|
||||||
|
|
||||||
/// SQL statement to create an index on JSON documents in the specified table
|
/// SQL statement to create an index on JSON documents in the specified table
|
||||||
[<CompiledName "EnsureJsonIndex">]
|
[<CompiledName "EnsureJsonIndex">]
|
||||||
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 =
|
||||||
|
|
|
@ -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));
|
||||||
|
|
||||||
|
|
|
@ -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 ]
|
||||||
|
|
Loading…
Reference in New Issue
Block a user