From a1559ad29ed12bb7721fa04607846bcb05db3754 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 26 Dec 2023 22:54:09 -0500 Subject: [PATCH] Add Postgres C# unit tests --- src/Postgres/Library.fs | 18 +++ src/Tests.CSharp/PostgresCSharpTests.cs | 180 ++++++++++++++++++++++++ src/Tests.CSharp/SqliteCSharpTests.cs | 1 + src/Tests/Program.fs | 1 + 4 files changed, 200 insertions(+) create mode 100644 src/Tests.CSharp/PostgresCSharpTests.cs diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index 4652ce6..e189f83 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -92,10 +92,12 @@ module Query = $"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 = $"data @> %s{paramName}" /// Create a WHERE clause fragment to implement a @? (JSON Path match) condition + [] let whereJsonPathMatches paramName = $"data @? %s{paramName}::jsonpath" @@ -103,10 +105,12 @@ module Query = module Count = /// Query to count matching documents using a JSON containment query (@>) + [] let byContains tableName = $"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereDataContains "@criteria"}""" /// Query to count matching documents using a JSON Path match (@?) + [] let byJsonPath tableName = $"""SELECT COUNT(*) AS it FROM %s{tableName} WHERE {whereJsonPathMatches "@path"}""" @@ -114,10 +118,12 @@ module Query = module Exists = /// Query to determine if documents exist using a JSON containment query (@>) + [] let byContains tableName = $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereDataContains "@criteria"}) AS it""" /// Query to determine if documents exist using a JSON Path match (@?) + [] let byJsonPath tableName = $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereJsonPathMatches "@path"}) AS it""" @@ -125,10 +131,12 @@ module Query = module Find = /// Query to retrieve documents using a JSON containment query (@>) + [] let byContains tableName = $"""{Query.selectFromTable tableName} WHERE {whereDataContains "@criteria"}""" /// Query to retrieve documents using a JSON Path match (@?) + [] let byJsonPath tableName = $"""{Query.selectFromTable tableName} WHERE {whereJsonPathMatches "@path"}""" @@ -136,18 +144,22 @@ module Query = module Update = /// Query to update a document + [] let partialById tableName = $"""UPDATE %s{tableName} SET data = data || @data WHERE {Query.whereById "@id"}""" /// Query to update a document + [] let partialByField tableName fieldName op = $"""UPDATE %s{tableName} SET data = data || @data WHERE {Query.whereByField fieldName op "@field"}""" /// Query to update partial documents matching a JSON containment query (@>) + [] let partialByContains tableName = $"""UPDATE %s{tableName} SET data = data || @data WHERE {whereDataContains "@criteria"}""" /// Query to update partial documents matching a JSON containment query (@>) + [] let partialByJsonPath tableName = $"""UPDATE %s{tableName} SET data = data || @data WHERE {whereJsonPathMatches "@path"}""" @@ -155,10 +167,12 @@ module Query = module Delete = /// Query to delete documents using a JSON containment query (@>) + [] let byContains tableName = $"""DELETE FROM %s{tableName} WHERE {whereDataContains "@criteria"}""" /// Query to delete documents using a JSON Path match (@?) + [] let byJsonPath tableName = $"""DELETE FROM %s{tableName} WHERE {whereJsonPathMatches "@path"}""" @@ -168,18 +182,22 @@ module Query = module Results = /// Create a domain item from a document, specifying the field in which the document is found + [] let fromDocument<'T> field (row: RowReader) : 'T = Configuration.serializer().Deserialize<'T>(row.string field) /// Create a domain item from a document + [] let fromData<'T> row : 'T = fromDocument "data" row /// Extract a count from the column "it" + [] let toCount (row: RowReader) = row.int "it" /// Extract a true/false value from the column "it" + [] let toExists (row: RowReader) = row.bool "it" diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs new file mode 100644 index 0000000..34a623c --- /dev/null +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -0,0 +1,180 @@ +using Expecto.CSharp; +using Expecto; +using BitBadger.Documents.Postgres; +using Npgsql.FSharp; + +namespace BitBadger.Documents.Tests.CSharp; + +using static Runner; + +/// +/// C# tests for the PostgreSQL implementation of BitBadger.Documents +/// +public class PostgresCSharpTests +{ + public static Test Unit = + TestList("Unit", new[] + { + TestList("Parameters", new[] + { + TestCase("Id succeeds", () => { + Expect.equal(Parameters.Id(88).Item1, "@id", "ID parameter not constructed correctly"); + }), + TestCase("Json succeeds", () => + { + Expect.equal(Parameters.Json("@test", new { Something = "good" }).Item1, "@test", + "JSON parameter not constructed correctly"); + }), + TestCase("Field succeeds", () => + { + Expect.equal(Parameters.Field(242).Item1, "@field", "Field parameter not constructed correctly"); + }), + TestCase("None succeeds", () => { + Expect.isEmpty(Parameters.None, "The no-params sequence should be empty"); + }) + }), + TestList("Query", new[] + { + TestList("Definition", new[] + { + TestCase("EnsureTable succeeds", () => + { + Expect.equal(Postgres.Query.Definition.EnsureTable(PostgresDb.TableName), + $"CREATE TABLE IF NOT EXISTS {PostgresDb.TableName} (data JSONB NOT NULL)", + "CREATE TABLE statement not constructed correctly"); + }), + TestCase("EnsureJsonIndex succeeds for full index", () => + { + Expect.equal(Postgres.Query.Definition.EnsureJsonIndex("schema.tbl", DocumentIndex.Full), + "CREATE INDEX IF NOT EXISTS idx_tbl_document ON schema.tbl USING GIN (data)", + "CREATE INDEX statement not constructed correctly"); + }), + TestCase("EnsureJsonIndex succeeds for JSONB Path Ops index", () => + { + Expect.equal( + Postgres.Query.Definition.EnsureJsonIndex(PostgresDb.TableName, DocumentIndex.Optimized), + string.Format( + "CREATE INDEX IF NOT EXISTS idx_{0}_document ON {0} USING GIN (data jsonb_path_ops)", + PostgresDb.TableName), + "CREATE INDEX statement not constructed correctly"); + }) + }), + TestCase("WhereDataContains succeeds", () => + { + Expect.equal(Postgres.Query.WhereDataContains("@test"), "data @> @test", + "WHERE clause not correct"); + }), + TestCase("WhereJsonPathMatches succeeds", () => + { + Expect.equal(Postgres.Query.WhereJsonPathMatches("@path"), "data @? @path::jsonpath", + "WHERE clause not correct"); + }), + TestList("Count", new[] + { + TestCase("ByContains succeeds", () => + { + Expect.equal(Postgres.Query.Count.ByContains(PostgresDb.TableName), + $"SELECT COUNT(*) AS it FROM {PostgresDb.TableName} WHERE data @> @criteria", + "JSON containment count query not correct"); + }), + TestCase("ByJsonPath succeeds", () => + { + Expect.equal(Postgres.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", new[] + { + TestCase("ByContains succeeds", () => + { + Expect.equal(Postgres.Query.Exists.ByContains(PostgresDb.TableName), + $"SELECT EXISTS (SELECT 1 FROM {PostgresDb.TableName} WHERE data @> @criteria) AS it", + "JSON containment exists query not correct"); + }), + TestCase("byJsonPath succeeds", () => + { + Expect.equal(Postgres.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", new[] + { + TestCase("byContains succeeds", () => + { + Expect.equal(Postgres.Query.Find.ByContains(PostgresDb.TableName), + $"SELECT data FROM {PostgresDb.TableName} WHERE data @> @criteria", + "SELECT by JSON containment query not correct"); + }), + TestCase("byJsonPath succeeds", () => + { + Expect.equal(Postgres.Query.Find.ByJsonPath(PostgresDb.TableName), + $"SELECT data FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", + "SELECT by JSON Path match query not correct"); + }) + }), + TestList("Update", new[] + { + TestCase("partialById succeeds", () => + { + Expect.equal(Postgres.Query.Update.PartialById(PostgresDb.TableName), + $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Id' = @id", + "UPDATE partial by ID statement not correct"); + }), + TestCase("partialByField succeeds", () => + { + Expect.equal(Postgres.Query.Update.PartialByField(PostgresDb.TableName, "Snail", Op.LT), + $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data ->> 'Snail' < @field", + "UPDATE partial by ID statement not correct"); + }), + TestCase("partialByContains succeeds", () => + { + Expect.equal(Postgres.Query.Update.PartialByContains(PostgresDb.TableName), + $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @> @criteria", + "UPDATE partial by JSON containment statement not correct"); + }), + TestCase("partialByJsonPath succeeds", () => + { + Expect.equal(Postgres.Query.Update.PartialByJsonPath(PostgresDb.TableName), + $"UPDATE {PostgresDb.TableName} SET data = data || @data WHERE data @? @path::jsonpath", + "UPDATE partial by JSON Path statement not correct"); + }) + }), + TestList("Delete", new[] + { + TestCase("byContains succeeds", () => + { + Expect.equal(Postgres.Query.Delete.ByContains(PostgresDb.TableName), + $"DELETE FROM {PostgresDb.TableName} WHERE data @> @criteria", + "DELETE by JSON containment query not correct"); + }), + TestCase("byJsonPath succeeds", () => + { + Expect.equal(Postgres.Query.Delete.ByJsonPath(PostgresDb.TableName), + $"DELETE FROM {PostgresDb.TableName} WHERE data @? @path::jsonpath", + "DELETE by JSON Path match query not correct"); + }) + }) + }) + }); + + private static readonly List TestDocuments = new() + { + new() { Id = "one", Value = "FIRST!", NumValue = 0 }, + new() { Id = "two", Value = "another", NumValue = 10, Sub = new() { Foo = "green", Bar = "blue" } }, + new() { Id = "three", Value = "", NumValue = 4 }, + new() { Id = "four", Value = "purple", NumValue = 17, Sub = new() { Foo = "green", Bar = "red" } }, + new() { Id = "five", Value = "purple", NumValue = 18 } + }; + + internal static async Task LoadDocs() + { + foreach (var doc in TestDocuments) await Document.Insert(SqliteDb.TableName, doc); + } + + /// + /// All Postgres C# tests + /// + public static Test All = TestList("Postgres.C#", new[] { Unit }); +} diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index e314ec2..39ed2e9 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -16,6 +16,7 @@ public static class SqliteCSharpTests /// /// Unit tests for the SQLite library /// + [Tests] public static Test Unit = TestList("Unit", new[] { diff --git a/src/Tests/Program.fs b/src/Tests/Program.fs index 2f16be6..30fdeef 100644 --- a/src/Tests/Program.fs +++ b/src/Tests/Program.fs @@ -7,6 +7,7 @@ let allTests = [ CommonTests.all CommonCSharpTests.Unit PostgresTests.all + PostgresCSharpTests.All SqliteTests.all testSequenced SqliteExtensionTests.integrationTests SqliteCSharpTests.All