v3 RC1 #1
@ -1,7 +1,5 @@
|
||||
namespace BitBadger.Documents.Postgres
|
||||
|
||||
open Npgsql
|
||||
|
||||
/// The type of index to generate for the document
|
||||
[<Struct>]
|
||||
type DocumentIndex =
|
||||
@ -11,11 +9,13 @@ type DocumentIndex =
|
||||
| Optimized
|
||||
|
||||
|
||||
open Npgsql
|
||||
|
||||
/// Configuration for document handling
|
||||
module Configuration =
|
||||
|
||||
/// 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)
|
||||
let useDataSource source =
|
||||
@ -38,10 +38,8 @@ module private Helpers =
|
||||
let internal fromDataSource () =
|
||||
Configuration.dataSource () |> Sql.fromDataSource
|
||||
|
||||
open System.Threading.Tasks
|
||||
|
||||
/// 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
|
||||
()
|
||||
}
|
||||
@ -49,50 +47,36 @@ module private Helpers =
|
||||
|
||||
open BitBadger.Documents
|
||||
|
||||
/// Data definition
|
||||
[<RequireQualifiedAccess>]
|
||||
module Definition =
|
||||
|
||||
/// SQL statement to create a document table
|
||||
let createTable name =
|
||||
$"CREATE TABLE IF NOT EXISTS %s{name} (data JSONB NOT NULL)"
|
||||
/// Functions for creating parameters
|
||||
[<AutoOpen>]
|
||||
module Parameters =
|
||||
|
||||
/// SQL statement to create a key index for a document table
|
||||
let createKey (name : string) =
|
||||
let tableName = name.Split(".") |> Array.last
|
||||
$"CREATE UNIQUE INDEX IF NOT EXISTS idx_{tableName}_key ON {name} ((data ->> '{Configuration.idField ()}'))"
|
||||
|
||||
/// SQL statement to create an index on documents in the specified table
|
||||
let createIndex (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})"
|
||||
|
||||
/// Definitions that take SqlProps as their last parameter
|
||||
module WithProps =
|
||||
|
||||
/// Create a document table
|
||||
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 ID parameter (name "@id", key will be treated as a string)
|
||||
[<CompiledName "Id">]
|
||||
let idParam (key: 'TKey) =
|
||||
"@id", Sql.string (string key)
|
||||
|
||||
/// 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 ())
|
||||
/// 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>
|
||||
|
||||
|
||||
/// Query construction functions
|
||||
[<RequireQualifiedAccess>]
|
||||
module Query =
|
||||
|
||||
/// Table and index definition queries
|
||||
module Definition =
|
||||
|
||||
/// 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
|
||||
[<CompiledName "EnsureJsonIndex">]
|
||||
let ensureJsonIndex (name : string) idxType =
|
||||
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 INDEX IF NOT EXISTS idx_{tableName}_document ON {name} USING GIN (data{extraOps})"
|
||||
|
||||
/// Create a WHERE clause fragment to implement a @> (JSON contains) condition
|
||||
let whereDataContains paramName =
|
||||
@ -115,23 +99,6 @@ module Query =
|
||||
let whereJsonPathMatches paramName =
|
||||
$"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
|
||||
module Count =
|
||||
|
||||
@ -196,31 +163,6 @@ module Query =
|
||||
$"""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
|
||||
[<AutoOpen>]
|
||||
module Results =
|
||||
@ -251,28 +193,28 @@ module WithProps =
|
||||
|
||||
/// Execute a query that returns a list of results
|
||||
[<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.parameters parameters
|
||||
|> Sql.executeAsync mapFunc
|
||||
|
||||
/// Execute a query that returns a list of results
|
||||
let List<'T>(query, parameters, mapFunc: System.Func<RowReader, 'T>, sqlProps) = backgroundTask {
|
||||
let! results = list query (List.ofSeq parameters) mapFunc.Invoke sqlProps
|
||||
let List<'TDoc>(query, parameters, mapFunc: System.Func<RowReader, 'TDoc>, sqlProps) = backgroundTask {
|
||||
let! results = list<'TDoc> query (List.ofSeq parameters) mapFunc.Invoke sqlProps
|
||||
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">]
|
||||
let single<'T> query parameters mapFunc sqlProps = backgroundTask {
|
||||
let! results = list<'T> query parameters mapFunc sqlProps
|
||||
let single<'TDoc> query parameters mapFunc sqlProps = backgroundTask {
|
||||
let! results = list<'TDoc> 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<RowReader, 'T>, sqlProps) = backgroundTask {
|
||||
let! result = single<'T> query (FSharp.Collections.List.ofSeq parameters) mapFunc.Invoke sqlProps
|
||||
/// 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<RowReader, 'TDoc>, sqlProps) = backgroundTask {
|
||||
let! result = single<'TDoc> query (FSharp.Collections.List.ofSeq parameters) mapFunc.Invoke sqlProps
|
||||
return Option.toObj result
|
||||
}
|
||||
|
||||
@ -295,21 +237,25 @@ module WithProps =
|
||||
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
|
||||
|
||||
/// Execute a non-query statement to manipulate a document
|
||||
let private executeNonQuery query (document: 'T) sqlProps =
|
||||
sqlProps
|
||||
|> Sql.query query
|
||||
|> Sql.parameters [ "@data", Query.jsonbDocParam document ]
|
||||
|> Sql.executeNonQueryAsync
|
||||
|> ignoreTask
|
||||
/// Table and index definition commands
|
||||
module Definition =
|
||||
|
||||
/// Create a document table
|
||||
[<CompiledName "EnsureTable">]
|
||||
let ensureTable name sqlProps = backgroundTask {
|
||||
do! Custom.nonQuery (Query.Definition.ensureTable name) [] sqlProps
|
||||
do! Custom.nonQuery (Query.Definition.ensureKey name) [] sqlProps
|
||||
}
|
||||
|
||||
/// Execute a non-query statement to manipulate a document with an ID specified
|
||||
let private executeNonQueryWithId query docId (document: 'T) sqlProps =
|
||||
sqlProps
|
||||
|> Sql.query query
|
||||
|> Sql.parameters (Query.docParameters docId document)
|
||||
|> 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
|
||||
[<AutoOpen>]
|
||||
@ -575,6 +521,26 @@ module Custom =
|
||||
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
|
||||
[<AutoOpen>]
|
||||
module Document =
|
||||
|
@ -134,8 +134,8 @@ public static class PostgresDb
|
||||
|
||||
var sqlProps = Sql.connect(database.ConnectionString);
|
||||
|
||||
Sql.executeNonQuery(Sql.query(Definition.createTable(TableName), sqlProps));
|
||||
Sql.executeNonQuery(Sql.query(Definition.createKey(TableName), sqlProps));
|
||||
Sql.executeNonQuery(Sql.query(Postgres.Query.Definition.EnsureTable(TableName), sqlProps));
|
||||
Sql.executeNonQuery(Sql.query(Query.Definition.EnsureKey(TableName), sqlProps));
|
||||
|
||||
Postgres.Configuration.useDataSource(MkDataSource(database.ConnectionString));
|
||||
|
||||
|
@ -8,105 +8,121 @@ open BitBadger.Documents.Tests
|
||||
/// Tests which do not hit the database
|
||||
let unitTests =
|
||||
testList "Unit" [
|
||||
testList "Definition" [
|
||||
test "createTable succeeds" {
|
||||
Expect.equal (Definition.createTable PostgresDb.TableName)
|
||||
$"CREATE TABLE IF NOT EXISTS {PostgresDb.TableName} (data JSONB NOT NULL)"
|
||||
"CREATE TABLE statement not constructed correctly"
|
||||
testList "Parameters" [
|
||||
test "idParam succeeds" {
|
||||
Expect.equal (idParam 88) ("@id", Sql.string "88") "ID parameter not constructed correctly"
|
||||
}
|
||||
test "createKey succeeds" {
|
||||
Expect.equal (Definition.createKey PostgresDb.TableName)
|
||||
$"CREATE UNIQUE INDEX IF NOT EXISTS idx_{PostgresDb.TableName}_key ON {PostgresDb.TableName} ((data ->> 'Id'))"
|
||||
"CREATE INDEX for key statement not constructed correctly"
|
||||
test "jsonParam succeeds" {
|
||||
Expect.equal
|
||||
(jsonParam "@test" {| Something = "good" |})
|
||||
("@test", Sql.jsonb """{"Something":"good"}""")
|
||||
"JSON parameter not constructed correctly"
|
||||
}
|
||||
test "createIndex succeeds for full index" {
|
||||
Expect.equal (Definition.createIndex "schema.tbl" Full)
|
||||
"CREATE INDEX IF NOT EXISTS idx_tbl ON schema.tbl USING GIN (data)"
|
||||
"CREATE INDEX statement not constructed correctly"
|
||||
test "fieldParam succeeds" {
|
||||
let it = fieldParam 242
|
||||
Expect.equal (fst it) "@field" "Field parameter name not correct"
|
||||
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" {
|
||||
Expect.equal (Definition.createIndex PostgresDb.TableName Optimized)
|
||||
$"CREATE INDEX IF NOT EXISTS idx_{PostgresDb.TableName} ON {PostgresDb.TableName} USING GIN (data jsonb_path_ops)"
|
||||
"CREATE INDEX statement not constructed correctly"
|
||||
test "noParams succeeds" {
|
||||
Expect.isEmpty noParams "The no-params sequence should be empty"
|
||||
}
|
||||
]
|
||||
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" {
|
||||
Expect.equal (Query.whereDataContains "@test") "data @> @test" "WHERE clause not correct"
|
||||
}
|
||||
test "whereJsonPathMatches succeeds" {
|
||||
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" [
|
||||
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"
|
||||
"JSON containment count query not correct"
|
||||
}
|
||||
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"
|
||||
"JSON Path match count query not correct"
|
||||
}
|
||||
]
|
||||
testList "Exists" [
|
||||
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"
|
||||
"JSON containment exists query not correct"
|
||||
}
|
||||
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"
|
||||
"JSON Path match existence query not correct"
|
||||
}
|
||||
]
|
||||
testList "Find" [
|
||||
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 by JSON containment query not correct"
|
||||
}
|
||||
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 by JSON Path match query not correct"
|
||||
}
|
||||
]
|
||||
testList "Update" [
|
||||
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 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" {
|
||||
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 partial by JSON containment statement not correct"
|
||||
}
|
||||
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 partial by JSON Path statement not correct"
|
||||
}
|
||||
@ -126,7 +142,6 @@ let unitTests =
|
||||
]
|
||||
]
|
||||
|
||||
open Npgsql.FSharp
|
||||
open ThrowawayDb.Postgres
|
||||
open Types
|
||||
|
||||
@ -163,17 +178,82 @@ let integrationTests =
|
||||
"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" [
|
||||
testTask "ensureTable succeeds" {
|
||||
use db = PostgresDb.BuildDb()
|
||||
let tableExists () =
|
||||
Sql.connect db.ConnectionString
|
||||
|> Sql.query "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it"
|
||||
|> Sql.executeRowAsync (fun row -> row.bool "it")
|
||||
Custom.scalar "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ensured') AS it" [] toExists
|
||||
let keyExists () =
|
||||
Sql.connect db.ConnectionString
|
||||
|> Sql.query "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it"
|
||||
|> Sql.executeRowAsync (fun row -> row.bool "it")
|
||||
Custom.scalar
|
||||
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_key') AS it" [] toExists
|
||||
|
||||
let! exists = tableExists ()
|
||||
let! alsoExists = keyExists ()
|
||||
@ -186,22 +266,38 @@ let integrationTests =
|
||||
Expect.isTrue exists' "The table should now exist"
|
||||
Expect.isTrue alsoExists' "The key index should now exist"
|
||||
}
|
||||
testTask "ensureIndex succeeds" {
|
||||
testTask "ensureJsonIndex succeeds" {
|
||||
use db = PostgresDb.BuildDb()
|
||||
let indexExists () =
|
||||
Sql.connect db.ConnectionString
|
||||
|> Sql.query "SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured') AS it"
|
||||
|> Sql.executeRowAsync (fun row -> row.bool "it")
|
||||
Custom.scalar
|
||||
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_ensured_document') AS it"
|
||||
[]
|
||||
toExists
|
||||
|
||||
let! exists = indexExists ()
|
||||
Expect.isFalse exists "The index should not exist already"
|
||||
|
||||
do! Definition.ensureTable "ensured"
|
||||
do! Definition.ensureIndex "ensured" Optimized
|
||||
do! Definition.ensureTable "ensured"
|
||||
do! Definition.ensureJsonIndex "ensured" Optimized
|
||||
let! exists' = indexExists ()
|
||||
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
|
||||
}
|
||||
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" [
|
||||
testTask "succeeds" {
|
||||
@ -217,8 +313,11 @@ let integrationTests =
|
||||
testTask "fails for duplicate key" {
|
||||
use db = PostgresDb.BuildDb()
|
||||
do! insert PostgresDb.TableName { emptyDoc with Id = "test" }
|
||||
Expect.throws (fun () ->
|
||||
insert PostgresDb.TableName {emptyDoc with Id = "test" } |> Async.AwaitTask |> Async.RunSynchronously)
|
||||
Expect.throws
|
||||
(fun () ->
|
||||
insert PostgresDb.TableName {emptyDoc with Id = "test" }
|
||||
|> Async.AwaitTask
|
||||
|> Async.RunSynchronously)
|
||||
"An exception should have been raised for duplicate document ID insert"
|
||||
}
|
||||
]
|
||||
@ -257,6 +356,13 @@ let integrationTests =
|
||||
let! theCount = Count.all PostgresDb.TableName
|
||||
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" {
|
||||
use db = PostgresDb.BuildDb()
|
||||
do! loadDocs ()
|
||||
@ -289,6 +395,22 @@ let integrationTests =
|
||||
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" [
|
||||
testTask "succeeds when documents exist" {
|
||||
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
|
||||
|
||||
|
||||
let all = testList "FSharp.Documents" [ unitTests; integrationTests ]
|
||||
let all = testList "Postgres" [ unitTests; integrationTests ]
|
||||
|
Loading…
Reference in New Issue
Block a user