diff --git a/src/Common/Library.fs b/src/Common/Library.fs index 2e1a64d..428c859 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -154,7 +154,7 @@ type ParameterName() = open System.Text #endif -/// Automatically-generated document ID options +/// Automatically-generated document ID strategies [] type AutoId = /// No automatic IDs will be generated @@ -167,11 +167,11 @@ type AutoId = | RandomString with /// Generate a GUID string - static member GenerateGuid() = + static member GenerateGuid () = System.Guid.NewGuid().ToString "N" /// Generate a string of random hexadecimal characters - static member GenerateRandomString(length: int) = + static member GenerateRandomString (length: int) = #if NET8_0_OR_GREATER RandomNumberGenerator.GetHexString(length, lowercase = true) #else @@ -179,6 +179,40 @@ with |> Array.fold (fun (str: StringBuilder) byt -> str.Append(byt.ToString "x2")) (StringBuilder length) |> function it -> it.Length <- length; it.ToString() #endif + + /// Does the given document need an automatic ID generated? + static member NeedsAutoId<'T> strategy (document: 'T) idProp = + match strategy with + | Disabled -> false + | _ -> + let prop = document.GetType().GetProperty idProp + if isNull prop then invalidOp $"{idProp} not found in document" + else + match strategy with + | Number -> + if prop.PropertyType = typeof then + let value = prop.GetValue document :?> int8 + value = int8 0 + elif prop.PropertyType = typeof then + let value = prop.GetValue document :?> int16 + value = int16 0 + elif prop.PropertyType = typeof then + let value = prop.GetValue document :?> int + value = 0 + elif prop.PropertyType = typeof then + let value = prop.GetValue document :?> int64 + value = int64 0 + else invalidOp "Document ID was not a number; cannot auto-generate a Number ID" + | Guid | RandomString -> + if prop.PropertyType = typeof then + let value = + prop.GetValue document + |> Option.ofObj + |> Option.map (fun it -> it :?> string) + |> Option.defaultValue "" + value = "" + else invalidOp "Document ID was not a string; cannot auto-generate GUID or random string" + | Disabled -> false /// The required document serialization implementation diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index 54c4d77..4095549 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -312,7 +312,23 @@ module WithProps = /// Insert a new document [] let insert<'TDoc> tableName (document: 'TDoc) sqlProps = - Custom.nonQuery (Query.insert tableName) [ jsonParam "@data" document ] sqlProps + let query = + match Configuration.autoIdStrategy () with + | Disabled -> Query.insert tableName + | strategy -> + let idField = Configuration.idField () + let dataParam = + if AutoId.NeedsAutoId strategy document idField then + match strategy with + | Number -> + $"' || (SELECT COALESCE(MAX((data->>'{idField}')::numeric), 0) + 1 FROM {tableName}) || '" + | Guid -> $"\"{AutoId.GenerateGuid()}\"" + | RandomString -> $"\"{AutoId.GenerateRandomString(Configuration.idStringLength ())}\"" + | Disabled -> "@data" + |> function it -> $"""@data::jsonb || ('{{"{idField}":{it}}}')::jsonb""" + else "@data" + (Query.insert tableName).Replace("@data", dataParam) + Custom.nonQuery query [ jsonParam "@data" document ] sqlProps /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") [] diff --git a/src/Sqlite/Extensions.fs b/src/Sqlite/Extensions.fs index d8b5106..4a32dec 100644 --- a/src/Sqlite/Extensions.fs +++ b/src/Sqlite/Extensions.fs @@ -35,11 +35,11 @@ module Extensions = /// Insert a new document member conn.insert<'TDoc> tableName (document: 'TDoc) = - WithConn.insert<'TDoc> tableName document conn + WithConn.Document.insert<'TDoc> tableName document conn /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") member conn.save<'TDoc> tableName (document: 'TDoc) = - WithConn.save tableName document conn + WithConn.Document.save tableName document conn /// Count all documents in a table member conn.countAll tableName = @@ -159,12 +159,12 @@ type SqliteConnectionCSharpExtensions = /// Insert a new document [] static member inline Insert<'TDoc>(conn, tableName, document: 'TDoc) = - WithConn.insert<'TDoc> tableName document conn + WithConn.Document.insert<'TDoc> tableName document conn /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") [] static member inline Save<'TDoc>(conn, tableName, document: 'TDoc) = - WithConn.save<'TDoc> tableName document conn + WithConn.Document.save<'TDoc> tableName document conn /// Count all documents in a table [] diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index accbdb8..d34d3f1 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -258,15 +258,34 @@ module WithConn = let ensureFieldIndex tableName indexName fields conn = Custom.nonQuery (Query.Definition.ensureIndexOn tableName indexName fields SQLite) [] conn - /// Insert a new document - [] - let insert<'TDoc> tableName (document: 'TDoc) conn = - Custom.nonQuery (Query.insert tableName) [ jsonParam "@data" document ] conn - - /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") - [] - let save<'TDoc> tableName (document: 'TDoc) conn = - Custom.nonQuery (Query.save tableName) [ jsonParam "@data" document ] conn + /// Commands to add documents + [] + module Document = + + /// Insert a new document + [] + let insert<'TDoc> tableName (document: 'TDoc) conn = + let query = + match Configuration.autoIdStrategy () with + | Disabled -> Query.insert tableName + | strategy -> + let idField = Configuration.idField () + let dataParam = + if AutoId.NeedsAutoId strategy document idField then + match strategy with + | Number -> $"(SELECT coalesce(max(data->>'{idField}'), 0) + 1 FROM {tableName})" + | Guid -> $"'{AutoId.GenerateGuid()}'" + | RandomString -> $"'{AutoId.GenerateRandomString(Configuration.idStringLength ())}'" + | Disabled -> "@data" + |> function it -> $"json_set(@data, '$.{idField}', {it})" + else "@data" + (Query.insert tableName).Replace("@data", dataParam) + Custom.nonQuery query [ jsonParam "@data" document ] conn + + /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") + [] + let save<'TDoc> tableName (document: 'TDoc) conn = + Custom.nonQuery (Query.save tableName) [ jsonParam "@data" document ] conn /// Commands to count documents [] @@ -547,13 +566,13 @@ module Document = [] let insert<'TDoc> tableName (document: 'TDoc) = use conn = Configuration.dbConn () - WithConn.insert tableName document conn + WithConn.Document.insert tableName document conn /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") [] let save<'TDoc> tableName (document: 'TDoc) = use conn = Configuration.dbConn () - WithConn.save tableName document conn + WithConn.Document.save tableName document conn /// Commands to count documents diff --git a/src/Tests.CSharp/CommonCSharpTests.cs b/src/Tests.CSharp/CommonCSharpTests.cs index 0f07f8f..0a0ca67 100644 --- a/src/Tests.CSharp/CommonCSharpTests.cs +++ b/src/Tests.CSharp/CommonCSharpTests.cs @@ -21,469 +21,614 @@ internal class TestSerializer : IDocumentSerializer /// public static class CommonCSharpTests { + /// + /// Unit tests for the Op enum + /// + private static readonly Test OpTests = TestList("Op", + [ + TestCase("EQ succeeds", () => + { + Expect.equal(Op.EQ.ToString(), "=", "The equals operator was not correct"); + }), + TestCase("GT succeeds", () => + { + Expect.equal(Op.GT.ToString(), ">", "The greater than operator was not correct"); + }), + TestCase("GE succeeds", () => + { + Expect.equal(Op.GE.ToString(), ">=", "The greater than or equal to operator was not correct"); + }), + TestCase("LT succeeds", () => + { + Expect.equal(Op.LT.ToString(), "<", "The less than operator was not correct"); + }), + TestCase("LE succeeds", () => + { + Expect.equal(Op.LE.ToString(), "<=", "The less than or equal to operator was not correct"); + }), + TestCase("NE succeeds", () => + { + Expect.equal(Op.NE.ToString(), "<>", "The not equal to operator was not correct"); + }), + TestCase("BT succeeds", () => + { + Expect.equal(Op.BT.ToString(), "BETWEEN", "The \"between\" operator was not correct"); + }), + TestCase("EX succeeds", () => + { + Expect.equal(Op.EX.ToString(), "IS NOT NULL", "The \"exists\" operator was not correct"); + }), + TestCase("NEX succeeds", () => + { + Expect.equal(Op.NEX.ToString(), "IS NULL", "The \"not exists\" operator was not correct"); + }) + ]); + + /// + /// Unit tests for the Field class + /// + private static readonly Test FieldTests = TestList("Field", + [ + TestCase("EQ succeeds", () => + { + var field = Field.EQ("Test", 14); + Expect.equal(field.Name, "Test", "Field name incorrect"); + Expect.equal(field.Op, Op.EQ, "Operator incorrect"); + Expect.equal(field.Value, 14, "Value incorrect"); + }), + TestCase("GT succeeds", () => + { + var field = Field.GT("Great", "night"); + Expect.equal(field.Name, "Great", "Field name incorrect"); + Expect.equal(field.Op, Op.GT, "Operator incorrect"); + Expect.equal(field.Value, "night", "Value incorrect"); + }), + TestCase("GE succeeds", () => + { + var field = Field.GE("Nice", 88L); + Expect.equal(field.Name, "Nice", "Field name incorrect"); + Expect.equal(field.Op, Op.GE, "Operator incorrect"); + Expect.equal(field.Value, 88L, "Value incorrect"); + }), + TestCase("LT succeeds", () => + { + var field = Field.LT("Lesser", "seven"); + Expect.equal(field.Name, "Lesser", "Field name incorrect"); + Expect.equal(field.Op, Op.LT, "Operator incorrect"); + Expect.equal(field.Value, "seven", "Value incorrect"); + }), + TestCase("LE succeeds", () => + { + var field = Field.LE("Nobody", "KNOWS"); + Expect.equal(field.Name, "Nobody", "Field name incorrect"); + Expect.equal(field.Op, Op.LE, "Operator incorrect"); + Expect.equal(field.Value, "KNOWS", "Value incorrect"); + }), + TestCase("NE succeeds", () => + { + var field = Field.NE("Park", "here"); + Expect.equal(field.Name, "Park", "Field name incorrect"); + Expect.equal(field.Op, Op.NE, "Operator incorrect"); + Expect.equal(field.Value, "here", "Value incorrect"); + }), + TestCase("BT succeeds", () => + { + var field = Field.BT("Age", 18, 49); + Expect.equal(field.Name, "Age", "Field name incorrect"); + Expect.equal(field.Op, Op.BT, "Operator incorrect"); + Expect.equal(((FSharpList)field.Value).ToArray(), [18, 49], "Value incorrect"); + }), + TestCase("EX succeeds", () => + { + var field = Field.EX("Groovy"); + Expect.equal(field.Name, "Groovy", "Field name incorrect"); + Expect.equal(field.Op, Op.EX, "Operator incorrect"); + }), + TestCase("NEX succeeds", () => + { + var field = Field.NEX("Rad"); + Expect.equal(field.Name, "Rad", "Field name incorrect"); + Expect.equal(field.Op, Op.NEX, "Operator incorrect"); + }), + TestList("NameToPath", + [ + TestCase("succeeds for PostgreSQL and a simple name", () => + { + Expect.equal("data->>'Simple'", Field.NameToPath("Simple", Dialect.PostgreSQL), + "Path not constructed correctly"); + }), + TestCase("succeeds for SQLite and a simple name", () => + { + Expect.equal("data->>'Simple'", Field.NameToPath("Simple", Dialect.SQLite), + "Path not constructed correctly"); + }), + TestCase("succeeds for PostgreSQL and a nested name", () => + { + Expect.equal("data#>>'{A,Long,Path,to,the,Property}'", + Field.NameToPath("A.Long.Path.to.the.Property", Dialect.PostgreSQL), + "Path not constructed correctly"); + }), + TestCase("succeeds for SQLite and a nested name", () => + { + Expect.equal("data->>'A'->>'Long'->>'Path'->>'to'->>'the'->>'Property'", + Field.NameToPath("A.Long.Path.to.the.Property", Dialect.SQLite), + "Path not constructed correctly"); + }) + ]), + TestCase("WithParameterName succeeds", () => + { + var field = Field.EQ("Bob", "Tom").WithParameterName("@name"); + Expect.isSome(field.ParameterName, "The parameter name should have been filled"); + Expect.equal("@name", field.ParameterName.Value, "The parameter name is incorrect"); + }), + TestCase("WithQualifier succeeds", () => + { + var field = Field.EQ("Bill", "Matt").WithQualifier("joe"); + Expect.isSome(field.Qualifier, "The table qualifier should have been filled"); + Expect.equal("joe", field.Qualifier.Value, "The table qualifier is incorrect"); + }), + TestList("Path", + [ + TestCase("succeeds for a PostgreSQL single field with no qualifier", () => + { + var field = Field.GE("SomethingCool", 18); + Expect.equal("data->>'SomethingCool'", field.Path(Dialect.PostgreSQL), + "The PostgreSQL path is incorrect"); + }), + TestCase("succeeds for a PostgreSQL single field with a qualifier", () => + { + var field = Field.LT("SomethingElse", 9).WithQualifier("this"); + Expect.equal("this.data->>'SomethingElse'", field.Path(Dialect.PostgreSQL), + "The PostgreSQL path is incorrect"); + }), + TestCase("succeeds for a PostgreSQL nested field with no qualifier", () => + { + var field = Field.EQ("My.Nested.Field", "howdy"); + Expect.equal("data#>>'{My,Nested,Field}'", field.Path(Dialect.PostgreSQL), + "The PostgreSQL path is incorrect"); + }), + TestCase("succeeds for a PostgreSQL nested field with a qualifier", () => + { + var field = Field.EQ("Nest.Away", "doc").WithQualifier("bird"); + Expect.equal("bird.data#>>'{Nest,Away}'", field.Path(Dialect.PostgreSQL), + "The PostgreSQL path is incorrect"); + }), + TestCase("succeeds for a SQLite single field with no qualifier", () => + { + var field = Field.GE("SomethingCool", 18); + Expect.equal("data->>'SomethingCool'", field.Path(Dialect.SQLite), "The SQLite path is incorrect"); + }), + TestCase("succeeds for a SQLite single field with a qualifier", () => + { + var field = Field.LT("SomethingElse", 9).WithQualifier("this"); + Expect.equal("this.data->>'SomethingElse'", field.Path(Dialect.SQLite), "The SQLite path is incorrect"); + }), + TestCase("succeeds for a SQLite nested field with no qualifier", () => + { + var field = Field.EQ("My.Nested.Field", "howdy"); + Expect.equal("data->>'My'->>'Nested'->>'Field'", field.Path(Dialect.SQLite), + "The SQLite path is incorrect"); + }), + TestCase("succeeds for a SQLite nested field with a qualifier", () => + { + var field = Field.EQ("Nest.Away", "doc").WithQualifier("bird"); + Expect.equal("bird.data->>'Nest'->>'Away'", field.Path(Dialect.SQLite), "The SQLite path is incorrect"); + }) + ]) + ]); + + /// + /// Unit tests for the FieldMatch enum + /// + private static readonly Test FieldMatchTests = TestList("FieldMatch.ToString", + [ + TestCase("succeeds for Any", () => + { + Expect.equal(FieldMatch.Any.ToString(), "OR", "SQL for Any is incorrect"); + }), + TestCase("succeeds for All", () => + { + Expect.equal(FieldMatch.All.ToString(), "AND", "SQL for All is incorrect"); + }) + ]); + + /// + /// Unit tests for the ParameterName class + /// + private static readonly Test ParameterNameTests = TestList("ParameterName.Derive", + [ + TestCase("succeeds with existing name", () => + { + ParameterName name = new(); + Expect.equal(name.Derive(FSharpOption.Some("@taco")), "@taco", "Name should have been @taco"); + Expect.equal(name.Derive(FSharpOption.None), "@field0", + "Counter should not have advanced for named field"); + }), + TestCase("Derive succeeds with non-existent name", () => + { + ParameterName name = new(); + Expect.equal(name.Derive(FSharpOption.None), "@field0", + "Anonymous field name should have been returned"); + Expect.equal(name.Derive(FSharpOption.None), "@field1", + "Counter should have advanced from previous call"); + Expect.equal(name.Derive(FSharpOption.None), "@field2", + "Counter should have advanced from previous call"); + Expect.equal(name.Derive(FSharpOption.None), "@field3", + "Counter should have advanced from previous call"); + }) + ]); + + /// + /// Unit tests for the AutoId enum + /// + private static readonly Test AutoIdTests = TestList("AutoId", + [ + TestCase("GenerateGuid succeeds", () => + { + var autoId = AutoId.GenerateGuid(); + Expect.isNotNull(autoId, "The GUID auto-ID should not have been null"); + Expect.stringHasLength(autoId, 32, "The GUID auto-ID should have been 32 characters long"); + Expect.equal(autoId, autoId.ToLowerInvariant(), "The GUID auto-ID should have been lowercase"); + }), + TestCase("GenerateRandomString succeeds", () => + { + foreach (var length in (int[]) [6, 8, 12, 20, 32, 57, 64]) + { + var autoId = AutoId.GenerateRandomString(length); + Expect.isNotNull(autoId, $"Random string ({length}) should not have been null"); + Expect.stringHasLength(autoId, length, $"Random string should have been {length} characters long"); + Expect.equal(autoId, autoId.ToLowerInvariant(), + $"Random string ({length}) should have been lowercase"); + } + }), + TestList("NeedsAutoId", + [ + TestCase("succeeds when no auto ID is configured", () => + { + Expect.isFalse(AutoId.NeedsAutoId(AutoId.Disabled, new object(), "id"), + "Disabled auto-ID never needs an automatic ID"); + }), + TestCase("fails for any when the ID property is not found", () => + { + try + { + _ = AutoId.NeedsAutoId(AutoId.Number, new { Key = "" }, "Id"); + Expect.isTrue(false, "Non-existent ID property should have thrown an exception"); + } + catch (InvalidOperationException) + { + // pass + } + }), + TestCase("succeeds for byte when the ID is zero", () => + { + Expect.isTrue(AutoId.NeedsAutoId(AutoId.Number, new { Id = (sbyte)0 }, "Id"), + "Zero ID should have returned true"); + }), + TestCase("succeeds for byte when the ID is non-zero", () => + { + Expect.isFalse(AutoId.NeedsAutoId(AutoId.Number, new { Id = (sbyte)4 }, "Id"), + "Non-zero ID should have returned false"); + }), + TestCase("succeeds for short when the ID is zero", () => + { + Expect.isTrue(AutoId.NeedsAutoId(AutoId.Number, new { Id = (short)0 }, "Id"), + "Zero ID should have returned true"); + }), + TestCase("succeeds for short when the ID is non-zero", () => + { + Expect.isFalse(AutoId.NeedsAutoId(AutoId.Number, new { Id = (short)7 }, "Id"), + "Non-zero ID should have returned false"); + }), + TestCase("succeeds for int when the ID is zero", () => + { + Expect.isTrue(AutoId.NeedsAutoId(AutoId.Number, new { Id = 0 }, "Id"), + "Zero ID should have returned true"); + }), + TestCase("succeeds for int when the ID is non-zero", () => + { + Expect.isFalse(AutoId.NeedsAutoId(AutoId.Number, new { Id = 32 }, "Id"), + "Non-zero ID should have returned false"); + }), + TestCase("succeeds for long when the ID is zero", () => + { + Expect.isTrue(AutoId.NeedsAutoId(AutoId.Number, new { Id = 0L }, "Id"), + "Zero ID should have returned true"); + }), + TestCase("succeeds for long when the ID is non-zero", () => + { + Expect.isFalse(AutoId.NeedsAutoId(AutoId.Number, new { Id = 80L }, "Id"), + "Non-zero ID should have returned false"); + }), + TestCase("fails for number when the ID is not a number", () => + { + try + { + _ = AutoId.NeedsAutoId(AutoId.Number, new { Id = "" }, "Id"); + Expect.isTrue(false, "Numeric ID against a string should have thrown an exception"); + } + catch (InvalidOperationException) + { + // pass + } + }), + TestCase("succeeds for GUID when the ID is blank", () => + { + Expect.isTrue(AutoId.NeedsAutoId(AutoId.Guid, new { Id = "" }, "Id"), + "Blank ID should have returned true"); + }), + TestCase("succeeds for GUID when the ID is filled", () => + { + Expect.isFalse(AutoId.NeedsAutoId(AutoId.Guid, new { Id = "abc" }, "Id"), + "Filled ID should have returned false"); + }), + TestCase("fails for GUID when the ID is not a string", () => + { + try + { + _ = AutoId.NeedsAutoId(AutoId.Guid, new { Id = 8 }, "Id"); + Expect.isTrue(false, "String ID against a number should have thrown an exception"); + } + catch (InvalidOperationException) + { + // pass + } + }), + TestCase("succeeds for RandomString when the ID is blank", () => + { + Expect.isTrue(AutoId.NeedsAutoId(AutoId.RandomString, new { Id = "" }, "Id"), + "Blank ID should have returned true"); + }), + TestCase("succeeds for RandomString when the ID is filled", () => + { + Expect.isFalse(AutoId.NeedsAutoId(AutoId.RandomString, new { Id = "x" }, "Id"), + "Filled ID should have returned false"); + }), + TestCase("fails for RandomString when the ID is not a string", () => + { + try + { + _ = AutoId.NeedsAutoId(AutoId.RandomString, new { Id = 33 }, "Id"); + Expect.isTrue(false, "String ID against a number should have thrown an exception"); + } + catch (InvalidOperationException) + { + // pass + } + }) + ]) + ]); + + /// + /// Unit tests for the Configuration static class + /// + private static readonly Test ConfigurationTests = TestList("Configuration", + [ + TestCase("UseSerializer succeeds", () => + { + try + { + Configuration.UseSerializer(new TestSerializer()); + + var serialized = Configuration.Serializer().Serialize(new SubDocument + { + Foo = "howdy", + Bar = "bye" + }); + Expect.equal(serialized, "{\"Overridden\":true}", "Specified serializer was not used"); + + var deserialized = Configuration.Serializer() + .Deserialize("{\"Something\":\"here\"}"); + Expect.isNull(deserialized, "Specified serializer should have returned null"); + } + finally + { + Configuration.UseSerializer(DocumentSerializer.Default); + } + }), + TestCase("Serializer returns configured serializer", () => + { + Expect.isTrue(ReferenceEquals(DocumentSerializer.Default, Configuration.Serializer()), + "Serializer should have been the same"); + }), + TestCase("UseIdField / IdField succeeds", () => + { + try + { + Expect.equal(Configuration.IdField(), "Id", + "The default configured ID field was incorrect"); + Configuration.UseIdField("id"); + Expect.equal(Configuration.IdField(), "id", "UseIdField did not set the ID field"); + } + finally + { + Configuration.UseIdField("Id"); + } + }), + TestCase("UseAutoIdStrategy / AutoIdStrategy succeeds", () => + { + try + { + Expect.equal(Configuration.AutoIdStrategy(), AutoId.Disabled, + "The default auto-ID strategy was incorrect"); + Configuration.UseAutoIdStrategy(AutoId.Guid); + Expect.equal(Configuration.AutoIdStrategy(), AutoId.Guid, + "The auto-ID strategy was not set correctly"); + } + finally + { + Configuration.UseAutoIdStrategy(AutoId.Disabled); + } + }), + TestCase("UseIdStringLength / IdStringLength succeeds", () => + { + try + { + Expect.equal(Configuration.IdStringLength(), 16, "The default ID string length was incorrect"); + Configuration.UseIdStringLength(33); + Expect.equal(Configuration.IdStringLength(), 33, "The ID string length was not set correctly"); + } + finally + { + Configuration.UseIdStringLength(16); + } + }) + ]); + + /// + /// Unit tests for the Query static class + /// + private static readonly Test QueryTests = TestList("Query", + [ + TestCase("StatementWhere succeeds", () => + { + Expect.equal(Query.StatementWhere("q", "r"), "q WHERE r", "Statements not combined correctly"); + }), + TestList("Definition", + [ + TestCase("EnsureTableFor succeeds", () => + { + Expect.equal(Query.Definition.EnsureTableFor("my.table", "JSONB"), + "CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)", + "CREATE TABLE statement not constructed correctly"); + }), + TestList("EnsureKey", + [ + TestCase("succeeds when a schema is present", () => + { + Expect.equal(Query.Definition.EnsureKey("test.table", Dialect.SQLite), + "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'Id'))", + "CREATE INDEX for key statement with schema not constructed correctly"); + }), + TestCase("succeeds when a schema is not present", () => + { + Expect.equal(Query.Definition.EnsureKey("table", Dialect.PostgreSQL), + "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data->>'Id'))", + "CREATE INDEX for key statement without schema not constructed correctly"); + }) + ]), + TestList("EnsureIndexOn", + [ + TestCase("succeeds for multiple fields and directions", () => + { + Expect.equal( + Query.Definition.EnsureIndexOn("test.table", "gibberish", + ["taco", "guac DESC", "salsa ASC"], Dialect.SQLite), + "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table " + + "((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)", + "CREATE INDEX for multiple field statement incorrect"); + }), + TestCase("succeeds for nested PostgreSQL field", () => + { + Expect.equal( + Query.Definition.EnsureIndexOn("tbl", "nest", ["a.b.c"], Dialect.PostgreSQL), + "CREATE INDEX IF NOT EXISTS idx_tbl_nest ON tbl ((data#>>'{a,b,c}'))", + "CREATE INDEX for nested PostgreSQL field incorrect"); + }), + TestCase("succeeds for nested SQLite field", () => + { + Expect.equal( + Query.Definition.EnsureIndexOn("tbl", "nest", ["a.b.c"], Dialect.SQLite), + "CREATE INDEX IF NOT EXISTS idx_tbl_nest ON tbl ((data->>'a'->>'b'->>'c'))", + "CREATE INDEX for nested SQLite field incorrect"); + }) + ]) + ]), + TestCase("Insert succeeds", () => + { + Expect.equal(Query.Insert("tbl"), "INSERT INTO tbl VALUES (@data)", "INSERT statement not correct"); + }), + TestCase("Save succeeds", () => + { + Expect.equal(Query.Save("tbl"), + "INSERT INTO tbl VALUES (@data) ON CONFLICT ((data->>'Id')) DO UPDATE SET data = EXCLUDED.data", + "INSERT ON CONFLICT UPDATE statement not correct"); + }), + TestCase("Count succeeds", () => + { + Expect.equal(Query.Count("tbl"), "SELECT COUNT(*) AS it FROM tbl", "Count query not correct"); + }), + TestCase("Exists succeeds", () => + { + Expect.equal(Query.Exists("tbl", "chicken"), "SELECT EXISTS (SELECT 1 FROM tbl WHERE chicken) AS it", + "Exists query not correct"); + }), + TestCase("Find succeeds", () => + { + Expect.equal(Query.Find("test.table"), "SELECT data FROM test.table", "Find query not correct"); + }), + TestCase("Update succeeds", () => + { + Expect.equal(Query.Update("tbl"), "UPDATE tbl SET data = @data", "Update query not correct"); + }), + TestCase("Delete succeeds", () => + { + Expect.equal(Query.Delete("tbl"), "DELETE FROM tbl", "Delete query not correct"); + }), + TestList("OrderBy", + [ + TestCase("succeeds for no fields", () => + { + Expect.equal(Query.OrderBy([], Dialect.PostgreSQL), "", "Order By should have been blank (PostgreSQL)"); + Expect.equal(Query.OrderBy([], Dialect.SQLite), "", "Order By should have been blank (SQLite)"); + }), + TestCase("succeeds for PostgreSQL with one field and no direction", () => + { + Expect.equal(Query.OrderBy([Field.Named("TestField")], Dialect.PostgreSQL), + " ORDER BY data->>'TestField'", "Order By not constructed correctly"); + }), + TestCase("succeeds for SQLite with one field and no direction", () => + { + Expect.equal(Query.OrderBy([Field.Named("TestField")], Dialect.SQLite), + " ORDER BY data->>'TestField'", "Order By not constructed correctly"); + }), + TestCase("succeeds for PostgreSQL with multiple fields and direction", () => + { + Expect.equal( + Query.OrderBy( + [ + Field.Named("Nested.Test.Field DESC"), Field.Named("AnotherField"), + Field.Named("It DESC") + ], Dialect.PostgreSQL), + " ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC", + "Order By not constructed correctly"); + }), + TestCase("succeeds for SQLite with multiple fields and direction", () => + { + Expect.equal( + Query.OrderBy( + [ + Field.Named("Nested.Test.Field DESC"), Field.Named("AnotherField"), + Field.Named("It DESC") + ], Dialect.SQLite), + " ORDER BY data->>'Nested'->>'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC", + "Order By not constructed correctly"); + }), + TestCase("succeeds for PostgreSQL numeric fields", () => + { + Expect.equal(Query.OrderBy([Field.Named("n:Test")], Dialect.PostgreSQL), + " ORDER BY (data->>'Test')::numeric", "Order By not constructed correctly for numeric field"); + }), + TestCase("succeeds for SQLite numeric fields", () => + { + Expect.equal(Query.OrderBy([Field.Named("n:Test")], Dialect.SQLite), " ORDER BY data->>'Test'", + "Order By not constructed correctly for numeric field"); + }) + ]) + ]); + /// /// Unit tests /// [Tests] public static readonly Test Unit = TestList("Common.C# Unit", [ - TestSequenced( - TestList("Configuration", - [ - TestCase("UseSerializer succeeds", () => - { - try - { - Configuration.UseSerializer(new TestSerializer()); - - var serialized = Configuration.Serializer().Serialize(new SubDocument - { - Foo = "howdy", - Bar = "bye" - }); - Expect.equal(serialized, "{\"Overridden\":true}", "Specified serializer was not used"); - - var deserialized = Configuration.Serializer() - .Deserialize("{\"Something\":\"here\"}"); - Expect.isNull(deserialized, "Specified serializer should have returned null"); - } - finally - { - Configuration.UseSerializer(DocumentSerializer.Default); - } - }), - TestCase("Serializer returns configured serializer", () => - { - Expect.isTrue(ReferenceEquals(DocumentSerializer.Default, Configuration.Serializer()), - "Serializer should have been the same"); - }), - TestCase("UseIdField / IdField succeeds", () => - { - try - { - Expect.equal(Configuration.IdField(), "Id", - "The default configured ID field was incorrect"); - Configuration.UseIdField("id"); - Expect.equal(Configuration.IdField(), "id", "UseIdField did not set the ID field"); - } - finally - { - Configuration.UseIdField("Id"); - } - }), - TestCase("UseAutoIdStrategy / AutoIdStrategy succeeds", () => - { - try - { - Expect.equal(Configuration.AutoIdStrategy(), AutoId.Disabled, - "The default auto-ID strategy was incorrect"); - Configuration.UseAutoIdStrategy(AutoId.Guid); - Expect.equal(Configuration.AutoIdStrategy(), AutoId.Guid, - "The auto-ID strategy was not set correctly"); - } - finally - { - Configuration.UseAutoIdStrategy(AutoId.Disabled); - } - }), - TestCase("UseIdStringLength / IdStringLength succeeds", () => - { - try - { - Expect.equal(Configuration.IdStringLength(), 16, "The default ID string length was incorrect"); - Configuration.UseIdStringLength(33); - Expect.equal(Configuration.IdStringLength(), 33, "The ID string length was not set correctly"); - } - finally - { - Configuration.UseIdStringLength(16); - } - }) - ])), - TestList("Op", - [ - TestCase("EQ succeeds", () => - { - Expect.equal(Op.EQ.ToString(), "=", "The equals operator was not correct"); - }), - TestCase("GT succeeds", () => - { - Expect.equal(Op.GT.ToString(), ">", "The greater than operator was not correct"); - }), - TestCase("GE succeeds", () => - { - Expect.equal(Op.GE.ToString(), ">=", "The greater than or equal to operator was not correct"); - }), - TestCase("LT succeeds", () => - { - Expect.equal(Op.LT.ToString(), "<", "The less than operator was not correct"); - }), - TestCase("LE succeeds", () => - { - Expect.equal(Op.LE.ToString(), "<=", "The less than or equal to operator was not correct"); - }), - TestCase("NE succeeds", () => - { - Expect.equal(Op.NE.ToString(), "<>", "The not equal to operator was not correct"); - }), - TestCase("BT succeeds", () => - { - Expect.equal(Op.BT.ToString(), "BETWEEN", "The \"between\" operator was not correct"); - }), - TestCase("EX succeeds", () => - { - Expect.equal(Op.EX.ToString(), "IS NOT NULL", "The \"exists\" operator was not correct"); - }), - TestCase("NEX succeeds", () => - { - Expect.equal(Op.NEX.ToString(), "IS NULL", "The \"not exists\" operator was not correct"); - }) - ]), - TestList("Field", - [ - TestCase("EQ succeeds", () => - { - var field = Field.EQ("Test", 14); - Expect.equal(field.Name, "Test", "Field name incorrect"); - Expect.equal(field.Op, Op.EQ, "Operator incorrect"); - Expect.equal(field.Value, 14, "Value incorrect"); - }), - TestCase("GT succeeds", () => - { - var field = Field.GT("Great", "night"); - Expect.equal(field.Name, "Great", "Field name incorrect"); - Expect.equal(field.Op, Op.GT, "Operator incorrect"); - Expect.equal(field.Value, "night", "Value incorrect"); - }), - TestCase("GE succeeds", () => - { - var field = Field.GE("Nice", 88L); - Expect.equal(field.Name, "Nice", "Field name incorrect"); - Expect.equal(field.Op, Op.GE, "Operator incorrect"); - Expect.equal(field.Value, 88L, "Value incorrect"); - }), - TestCase("LT succeeds", () => - { - var field = Field.LT("Lesser", "seven"); - Expect.equal(field.Name, "Lesser", "Field name incorrect"); - Expect.equal(field.Op, Op.LT, "Operator incorrect"); - Expect.equal(field.Value, "seven", "Value incorrect"); - }), - TestCase("LE succeeds", () => - { - var field = Field.LE("Nobody", "KNOWS"); - Expect.equal(field.Name, "Nobody", "Field name incorrect"); - Expect.equal(field.Op, Op.LE, "Operator incorrect"); - Expect.equal(field.Value, "KNOWS", "Value incorrect"); - }), - TestCase("NE succeeds", () => - { - var field = Field.NE("Park", "here"); - Expect.equal(field.Name, "Park", "Field name incorrect"); - Expect.equal(field.Op, Op.NE, "Operator incorrect"); - Expect.equal(field.Value, "here", "Value incorrect"); - }), - TestCase("BT succeeds", () => - { - var field = Field.BT("Age", 18, 49); - Expect.equal(field.Name, "Age", "Field name incorrect"); - Expect.equal(field.Op, Op.BT, "Operator incorrect"); - Expect.equal(((FSharpList)field.Value).ToArray(), [18, 49], "Value incorrect"); - }), - TestCase("EX succeeds", () => - { - var field = Field.EX("Groovy"); - Expect.equal(field.Name, "Groovy", "Field name incorrect"); - Expect.equal(field.Op, Op.EX, "Operator incorrect"); - }), - TestCase("NEX succeeds", () => - { - var field = Field.NEX("Rad"); - Expect.equal(field.Name, "Rad", "Field name incorrect"); - Expect.equal(field.Op, Op.NEX, "Operator incorrect"); - }), - TestList("NameToPath", - [ - TestCase("succeeds for PostgreSQL and a simple name", () => - { - Expect.equal("data->>'Simple'", Field.NameToPath("Simple", Dialect.PostgreSQL), - "Path not constructed correctly"); - }), - TestCase("succeeds for SQLite and a simple name", () => - { - Expect.equal("data->>'Simple'", Field.NameToPath("Simple", Dialect.SQLite), - "Path not constructed correctly"); - }), - TestCase("succeeds for PostgreSQL and a nested name", () => - { - Expect.equal("data#>>'{A,Long,Path,to,the,Property}'", - Field.NameToPath("A.Long.Path.to.the.Property", Dialect.PostgreSQL), - "Path not constructed correctly"); - }), - TestCase("succeeds for SQLite and a nested name", () => - { - Expect.equal("data->>'A'->>'Long'->>'Path'->>'to'->>'the'->>'Property'", - Field.NameToPath("A.Long.Path.to.the.Property", Dialect.SQLite), - "Path not constructed correctly"); - }) - ]), - TestCase("WithParameterName succeeds", () => - { - var field = Field.EQ("Bob", "Tom").WithParameterName("@name"); - Expect.isSome(field.ParameterName, "The parameter name should have been filled"); - Expect.equal("@name", field.ParameterName.Value, "The parameter name is incorrect"); - }), - TestCase("WithQualifier succeeds", () => - { - var field = Field.EQ("Bill", "Matt").WithQualifier("joe"); - Expect.isSome(field.Qualifier, "The table qualifier should have been filled"); - Expect.equal("joe", field.Qualifier.Value, "The table qualifier is incorrect"); - }), - TestList("Path", - [ - TestCase("succeeds for a PostgreSQL single field with no qualifier", () => - { - var field = Field.GE("SomethingCool", 18); - Expect.equal("data->>'SomethingCool'", field.Path(Dialect.PostgreSQL), - "The PostgreSQL path is incorrect"); - }), - TestCase("succeeds for a PostgreSQL single field with a qualifier", () => - { - var field = Field.LT("SomethingElse", 9).WithQualifier("this"); - Expect.equal("this.data->>'SomethingElse'", field.Path(Dialect.PostgreSQL), - "The PostgreSQL path is incorrect"); - }), - TestCase("succeeds for a PostgreSQL nested field with no qualifier", () => - { - var field = Field.EQ("My.Nested.Field", "howdy"); - Expect.equal("data#>>'{My,Nested,Field}'", field.Path(Dialect.PostgreSQL), - "The PostgreSQL path is incorrect"); - }), - TestCase("succeeds for a PostgreSQL nested field with a qualifier", () => - { - var field = Field.EQ("Nest.Away", "doc").WithQualifier("bird"); - Expect.equal("bird.data#>>'{Nest,Away}'", field.Path(Dialect.PostgreSQL), - "The PostgreSQL path is incorrect"); - }), - TestCase("succeeds for a SQLite single field with no qualifier", () => - { - var field = Field.GE("SomethingCool", 18); - Expect.equal("data->>'SomethingCool'", field.Path(Dialect.SQLite), "The SQLite path is incorrect"); - }), - TestCase("succeeds for a SQLite single field with a qualifier", () => - { - var field = Field.LT("SomethingElse", 9).WithQualifier("this"); - Expect.equal("this.data->>'SomethingElse'", field.Path(Dialect.SQLite), - "The SQLite path is incorrect"); - }), - TestCase("succeeds for a SQLite nested field with no qualifier", () => - { - var field = Field.EQ("My.Nested.Field", "howdy"); - Expect.equal("data->>'My'->>'Nested'->>'Field'", field.Path(Dialect.SQLite), - "The SQLite path is incorrect"); - }), - TestCase("succeeds for a SQLite nested field with a qualifier", () => - { - var field = Field.EQ("Nest.Away", "doc").WithQualifier("bird"); - Expect.equal("bird.data->>'Nest'->>'Away'", field.Path(Dialect.SQLite), - "The SQLite path is incorrect"); - }) - ]) - ]), - TestList("FieldMatch.ToString", - [ - TestCase("succeeds for Any", () => - { - Expect.equal(FieldMatch.Any.ToString(), "OR", "SQL for Any is incorrect"); - }), - TestCase("succeeds for All", () => - { - Expect.equal(FieldMatch.All.ToString(), "AND", "SQL for All is incorrect"); - }) - ]), - TestList("ParameterName.Derive", - [ - TestCase("succeeds with existing name", () => - { - ParameterName name = new(); - Expect.equal(name.Derive(FSharpOption.Some("@taco")), "@taco", "Name should have been @taco"); - Expect.equal(name.Derive(FSharpOption.None), "@field0", - "Counter should not have advanced for named field"); - }), - TestCase("Derive succeeds with non-existent name", () => - { - ParameterName name = new(); - Expect.equal(name.Derive(FSharpOption.None), "@field0", - "Anonymous field name should have been returned"); - Expect.equal(name.Derive(FSharpOption.None), "@field1", - "Counter should have advanced from previous call"); - Expect.equal(name.Derive(FSharpOption.None), "@field2", - "Counter should have advanced from previous call"); - Expect.equal(name.Derive(FSharpOption.None), "@field3", - "Counter should have advanced from previous call"); - }) - ]), - TestList("AutoId", - [ - TestCase("GenerateGuid succeeds", () => - { - var autoId = AutoId.GenerateGuid(); - Expect.isNotNull(autoId, "The GUID auto-ID should not have been null"); - Expect.stringHasLength(autoId, 32, "The GUID auto-ID should have been 32 characters long"); - Expect.equal(autoId, autoId.ToLowerInvariant(), "The GUID auto-ID should have been lowercase"); - }), - TestCase("GenerateRandomString succeeds", () => - { - foreach (var length in (int[])[6, 8, 12, 20, 32, 57, 64]) - { - var autoId = AutoId.GenerateRandomString(length); - Expect.isNotNull(autoId, $"Random string ({length}) should not have been null"); - Expect.stringHasLength(autoId, length, $"Random string should have been {length} characters long"); - Expect.equal(autoId, autoId.ToLowerInvariant(), - $"Random string ({length}) should have been lowercase"); - } - }) - ]), - TestList("Query", - [ - TestCase("StatementWhere succeeds", () => - { - Expect.equal(Query.StatementWhere("q", "r"), "q WHERE r", "Statements not combined correctly"); - }), - TestList("Definition", - [ - TestCase("EnsureTableFor succeeds", () => - { - Expect.equal(Query.Definition.EnsureTableFor("my.table", "JSONB"), - "CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)", - "CREATE TABLE statement not constructed correctly"); - }), - TestList("EnsureKey", - [ - TestCase("succeeds when a schema is present", () => - { - Expect.equal(Query.Definition.EnsureKey("test.table", Dialect.SQLite), - "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'Id'))", - "CREATE INDEX for key statement with schema not constructed correctly"); - }), - TestCase("succeeds when a schema is not present", () => - { - Expect.equal(Query.Definition.EnsureKey("table", Dialect.PostgreSQL), - "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data->>'Id'))", - "CREATE INDEX for key statement without schema not constructed correctly"); - }) - ]), - TestList("EnsureIndexOn", - [ - TestCase("succeeds for multiple fields and directions", () => - { - Expect.equal( - Query.Definition.EnsureIndexOn("test.table", "gibberish", - ["taco", "guac DESC", "salsa ASC"], Dialect.SQLite), - "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table " - + "((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)", - "CREATE INDEX for multiple field statement incorrect"); - }), - TestCase("succeeds for nested PostgreSQL field", () => - { - Expect.equal( - Query.Definition.EnsureIndexOn("tbl", "nest", ["a.b.c"], Dialect.PostgreSQL), - "CREATE INDEX IF NOT EXISTS idx_tbl_nest ON tbl ((data#>>'{a,b,c}'))", - "CREATE INDEX for nested PostgreSQL field incorrect"); - }), - TestCase("succeeds for nested SQLite field", () => - { - Expect.equal( - Query.Definition.EnsureIndexOn("tbl", "nest", ["a.b.c"], Dialect.SQLite), - "CREATE INDEX IF NOT EXISTS idx_tbl_nest ON tbl ((data->>'a'->>'b'->>'c'))", - "CREATE INDEX for nested SQLite field incorrect"); - }) - ]) - ]), - TestCase("Insert succeeds", () => - { - Expect.equal(Query.Insert("tbl"), "INSERT INTO tbl VALUES (@data)", "INSERT statement not correct"); - }), - TestCase("Save succeeds", () => - { - Expect.equal(Query.Save("tbl"), - "INSERT INTO tbl VALUES (@data) ON CONFLICT ((data->>'Id')) DO UPDATE SET data = EXCLUDED.data", - "INSERT ON CONFLICT UPDATE statement not correct"); - }), - TestCase("Count succeeds", () => - { - Expect.equal(Query.Count("tbl"), "SELECT COUNT(*) AS it FROM tbl", "Count query not correct"); - }), - TestCase("Exists succeeds", () => - { - Expect.equal(Query.Exists("tbl", "chicken"), "SELECT EXISTS (SELECT 1 FROM tbl WHERE chicken) AS it", - "Exists query not correct"); - }), - TestCase("Find succeeds", () => - { - Expect.equal(Query.Find("test.table"), "SELECT data FROM test.table", "Find query not correct"); - }), - TestCase("Update succeeds", () => - { - Expect.equal(Query.Update("tbl"), "UPDATE tbl SET data = @data", "Update query not correct"); - }), - TestCase("Delete succeeds", () => - { - Expect.equal(Query.Delete("tbl"), "DELETE FROM tbl", "Delete query not correct"); - }), - TestList("OrderBy", - [ - TestCase("succeeds for no fields", () => - { - Expect.equal(Query.OrderBy([], Dialect.PostgreSQL), "", - "Order By should have been blank (PostgreSQL)"); - Expect.equal(Query.OrderBy([], Dialect.SQLite), "", "Order By should have been blank (SQLite)"); - }), - TestCase("succeeds for PostgreSQL with one field and no direction", () => - { - Expect.equal(Query.OrderBy([Field.Named("TestField")], Dialect.PostgreSQL), - " ORDER BY data->>'TestField'", "Order By not constructed correctly"); - }), - TestCase("succeeds for SQLite with one field and no direction", () => - { - Expect.equal(Query.OrderBy([Field.Named("TestField")], Dialect.SQLite), - " ORDER BY data->>'TestField'", "Order By not constructed correctly"); - }), - TestCase("succeeds for PostgreSQL with multiple fields and direction", () => - { - Expect.equal( - Query.OrderBy( - [ - Field.Named("Nested.Test.Field DESC"), Field.Named("AnotherField"), - Field.Named("It DESC") - ], - Dialect.PostgreSQL), - " ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC", - "Order By not constructed correctly"); - }), - TestCase("succeeds for SQLite with multiple fields and direction", () => - { - Expect.equal( - Query.OrderBy( - [ - Field.Named("Nested.Test.Field DESC"), Field.Named("AnotherField"), - Field.Named("It DESC") - ], - Dialect.SQLite), - " ORDER BY data->>'Nested'->>'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC", - "Order By not constructed correctly"); - }), - TestCase("succeeds for PostgreSQL numeric fields", () => - { - Expect.equal(Query.OrderBy([Field.Named("n:Test")], Dialect.PostgreSQL), - " ORDER BY (data->>'Test')::numeric", "Order By not constructed correctly for numeric field"); - }), - TestCase("succeeds for SQLite numeric fields", () => - { - Expect.equal(Query.OrderBy([Field.Named("n:Test")], Dialect.SQLite), " ORDER BY data->>'Test'", - "Order By not constructed correctly for numeric field"); - }) - ]) - ]) + OpTests, + FieldTests, + FieldMatchTests, + ParameterNameTests, + AutoIdTests, + QueryTests, + TestSequenced(ConfigurationTests) ]); } diff --git a/src/Tests.CSharp/PostgresCSharpTests.cs b/src/Tests.CSharp/PostgresCSharpTests.cs index 38324af..a224094 100644 --- a/src/Tests.CSharp/PostgresCSharpTests.cs +++ b/src/Tests.CSharp/PostgresCSharpTests.cs @@ -170,7 +170,11 @@ public static class PostgresCSharpTests Expect.isTrue(false, "The parameter was not a StringArray type"); } }) - ]) + ]), + TestCase("None succeeds", () => + { + Expect.isEmpty(Parameters.None, "The no-params sequence should be empty"); + }) ]); /// @@ -310,15 +314,6 @@ public static class PostgresCSharpTests }) ]); - /// - /// Tests which do not hit the database - /// - private static readonly Test Unit = TestList("Unit", - [ - ParametersTests, - QueryTests - ]); - private static readonly List TestDocuments = [ new() { Id = "one", Value = "FIRST!", NumValue = 0 }, @@ -548,6 +543,83 @@ public static class PostgresCSharpTests { // This is what should have happened } + }), + TestCase("succeeds when adding a numeric auto ID", async () => + { + try + { + Configuration.UseAutoIdStrategy(AutoId.Number); + Configuration.UseIdField("Key"); + await using var db = PostgresDb.BuildDb(); + var before = await Count.All(PostgresDb.TableName); + Expect.equal(before, 0, "There should be no documents in the table"); + + await Document.Insert(PostgresDb.TableName, new NumIdDocument { Text = "one" }); + await Document.Insert(PostgresDb.TableName, new NumIdDocument { Text = "two" }); + await Document.Insert(PostgresDb.TableName, new NumIdDocument { Key = 77, Text = "three" }); + await Document.Insert(PostgresDb.TableName, new NumIdDocument { Text = "four" }); + + var after = await Find.AllOrdered(PostgresDb.TableName, [Field.Named("n:Key")]); + Expect.hasLength(after, 4, "There should have been 4 documents returned"); + Expect.sequenceEqual(after.Select(x => x.Key), [1, 2, 77, 78], + "The IDs were not generated correctly"); + } + finally + { + Configuration.UseAutoIdStrategy(AutoId.Disabled); + Configuration.UseIdField("Id"); + } + }), + TestCase("succeeds when adding a GUID auto ID", async () => + { + try + { + Configuration.UseAutoIdStrategy(AutoId.Guid); + await using var db = PostgresDb.BuildDb(); + var before = await Count.All(PostgresDb.TableName); + Expect.equal(before, 0, "There should be no documents in the table"); + + await Document.Insert(PostgresDb.TableName, new JsonDocument { Value = "one" }); + await Document.Insert(PostgresDb.TableName, new JsonDocument { Value = "two" }); + await Document.Insert(PostgresDb.TableName, new JsonDocument { Id = "abc123", Value = "three" }); + await Document.Insert(PostgresDb.TableName, new JsonDocument { Value = "four" }); + + var after = await Find.All(PostgresDb.TableName); + Expect.hasLength(after, 4, "There should have been 4 documents returned"); + Expect.equal(after.Count(x => x.Id.Length == 32), 3, "Three of the IDs should have been GUIDs"); + Expect.equal(after.Count(x => x.Id == "abc123"), 1, "The provided ID should have been used as-is"); + } + finally + { + Configuration.UseAutoIdStrategy(AutoId.Disabled); + } + }), + TestCase("succeeds when adding a RandomString auto ID", async () => + { + try + { + Configuration.UseAutoIdStrategy(AutoId.RandomString); + Configuration.UseIdStringLength(44); + await using var db = PostgresDb.BuildDb(); + var before = await Count.All(PostgresDb.TableName); + Expect.equal(before, 0, "There should be no documents in the table"); + + await Document.Insert(PostgresDb.TableName, new JsonDocument { Value = "one" }); + await Document.Insert(PostgresDb.TableName, new JsonDocument { Value = "two" }); + await Document.Insert(PostgresDb.TableName, new JsonDocument { Id = "abc123", Value = "three" }); + await Document.Insert(PostgresDb.TableName, new JsonDocument { Value = "four" }); + + var after = await Find.All(PostgresDb.TableName); + Expect.hasLength(after, 4, "There should have been 4 documents returned"); + Expect.equal(after.Count(x => x.Id.Length == 44), 3, + "Three of the IDs should have been 44-character random strings"); + Expect.equal(after.Count(x => x.Id == "abc123"), 1, "The provided ID should have been used as-is"); + } + finally + { + Configuration.UseAutoIdStrategy(AutoId.Disabled); + Configuration.UseIdStringLength(16); + } }) ]), TestList("Save", diff --git a/src/Tests.CSharp/SqliteCSharpTests.cs b/src/Tests.CSharp/SqliteCSharpTests.cs index b453ff8..2ea73ca 100644 --- a/src/Tests.CSharp/SqliteCSharpTests.cs +++ b/src/Tests.CSharp/SqliteCSharpTests.cs @@ -325,9 +325,86 @@ public static class SqliteCSharpTests { // This is what is supposed to happen } + }), + TestCase("succeeds when adding a numeric auto ID", async () => + { + try + { + Configuration.UseAutoIdStrategy(AutoId.Number); + Configuration.UseIdField("Key"); + await using var db = await SqliteDb.BuildDb(); + var before = await Count.All(SqliteDb.TableName); + Expect.equal(before, 0L, "There should be no documents in the table"); + + await Document.Insert(SqliteDb.TableName, new NumIdDocument { Text = "one" }); + await Document.Insert(SqliteDb.TableName, new NumIdDocument { Text = "two" }); + await Document.Insert(SqliteDb.TableName, new NumIdDocument { Key = 77, Text = "three" }); + await Document.Insert(SqliteDb.TableName, new NumIdDocument { Text = "four" }); + + var after = await Find.AllOrdered(SqliteDb.TableName, [Field.Named("Key")]); + Expect.hasLength(after, 4, "There should have been 4 documents returned"); + Expect.sequenceEqual(after.Select(x => x.Key), [1, 2, 77, 78], + "The IDs were not generated correctly"); + } + finally + { + Configuration.UseAutoIdStrategy(AutoId.Disabled); + Configuration.UseIdField("Id"); + } + }), + TestCase("succeeds when adding a GUID auto ID", async () => + { + try + { + Configuration.UseAutoIdStrategy(AutoId.Guid); + await using var db = await SqliteDb.BuildDb(); + var before = await Count.All(SqliteDb.TableName); + Expect.equal(before, 0L, "There should be no documents in the table"); + + await Document.Insert(SqliteDb.TableName, new JsonDocument { Value = "one" }); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Value = "two" }); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "abc123", Value = "three" }); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Value = "four" }); + + var after = await Find.All(SqliteDb.TableName); + Expect.hasLength(after, 4, "There should have been 4 documents returned"); + Expect.equal(after.Count(x => x.Id.Length == 32), 3, "Three of the IDs should have been GUIDs"); + Expect.equal(after.Count(x => x.Id == "abc123"), 1, "The provided ID should have been used as-is"); + } + finally + { + Configuration.UseAutoIdStrategy(AutoId.Disabled); + } + }), + TestCase("succeeds when adding a RandomString auto ID", async () => + { + try + { + Configuration.UseAutoIdStrategy(AutoId.RandomString); + Configuration.UseIdStringLength(44); + await using var db = await SqliteDb.BuildDb(); + var before = await Count.All(SqliteDb.TableName); + Expect.equal(before, 0L, "There should be no documents in the table"); + + await Document.Insert(SqliteDb.TableName, new JsonDocument { Value = "one" }); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Value = "two" }); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Id = "abc123", Value = "three" }); + await Document.Insert(SqliteDb.TableName, new JsonDocument { Value = "four" }); + + var after = await Find.All(SqliteDb.TableName); + Expect.hasLength(after, 4, "There should have been 4 documents returned"); + Expect.equal(after.Count(x => x.Id.Length == 44), 3, + "Three of the IDs should have been 44-character random strings"); + Expect.equal(after.Count(x => x.Id == "abc123"), 1, "The provided ID should have been used as-is"); + } + finally + { + Configuration.UseAutoIdStrategy(AutoId.Disabled); + Configuration.UseIdStringLength(16); + } }) ]), - TestList("Document.Save", + TestList("Save", [ TestCase("succeeds when a document is inserted", async () => { diff --git a/src/Tests.CSharp/Types.cs b/src/Tests.CSharp/Types.cs index 5e7f972..ce63230 100644 --- a/src/Tests.CSharp/Types.cs +++ b/src/Tests.CSharp/Types.cs @@ -1,5 +1,11 @@ namespace BitBadger.Documents.Tests.CSharp; +public class NumIdDocument +{ + public int Key { get; set; } = 0; + public string Text { get; set; } = ""; +} + public class SubDocument { public string Foo { get; set; } = ""; diff --git a/src/Tests/BitBadger.Documents.Tests.fsproj b/src/Tests/BitBadger.Documents.Tests.fsproj index d976fb3..a362ea2 100644 --- a/src/Tests/BitBadger.Documents.Tests.fsproj +++ b/src/Tests/BitBadger.Documents.Tests.fsproj @@ -6,8 +6,8 @@ - + diff --git a/src/Tests/CommonTests.fs b/src/Tests/CommonTests.fs index 46b69c9..ad0da18 100644 --- a/src/Tests/CommonTests.fs +++ b/src/Tests/CommonTests.fs @@ -217,6 +217,115 @@ let autoIdTests = testList "AutoId" [ Expect.stringHasLength autoId length $"Random string should have been {length} characters long" Expect.equal autoId (autoId.ToLowerInvariant ()) $"Random string ({length}) should have been lowercase") } + testList "NeedsAutoId" [ + test "succeeds when no auto ID is configured" { + Expect.isFalse (AutoId.NeedsAutoId Disabled (obj ()) "id") "Disabled auto-ID never needs an automatic ID" + } + test "fails for any when the ID property is not found" { + Expect.throwsT + (fun () -> AutoId.NeedsAutoId Number {| Key = "" |} "Id" |> ignore) + "Non-existent ID property should have thrown an exception" + } + test "succeeds for byte when the ID is zero" { + Expect.isTrue (AutoId.NeedsAutoId Number {| Id = int8 0 |} "Id") "Zero ID should have returned true" + } + test "succeeds for byte when the ID is non-zero" { + Expect.isFalse (AutoId.NeedsAutoId Number {| Id = int8 4 |} "Id") "Non-zero ID should have returned false" + } + test "succeeds for short when the ID is zero" { + Expect.isTrue (AutoId.NeedsAutoId Number {| Id = int16 0 |} "Id") "Zero ID should have returned true" + } + test "succeeds for short when the ID is non-zero" { + Expect.isFalse (AutoId.NeedsAutoId Number {| Id = int16 7 |} "Id") "Non-zero ID should have returned false" + } + test "succeeds for int when the ID is zero" { + Expect.isTrue (AutoId.NeedsAutoId Number {| Id = 0 |} "Id") "Zero ID should have returned true" + } + test "succeeds for int when the ID is non-zero" { + Expect.isFalse (AutoId.NeedsAutoId Number {| Id = 32 |} "Id") "Non-zero ID should have returned false" + } + test "succeeds for long when the ID is zero" { + Expect.isTrue (AutoId.NeedsAutoId Number {| Id = 0L |} "Id") "Zero ID should have returned true" + } + test "succeeds for long when the ID is non-zero" { + Expect.isFalse (AutoId.NeedsAutoId Number {| Id = 80L |} "Id") "Non-zero ID should have returned false" + } + test "fails for number when the ID is not a number" { + Expect.throwsT + (fun () -> AutoId.NeedsAutoId Number {| Id = "" |} "Id" |> ignore) + "Numeric ID against a string should have thrown an exception" + } + test "succeeds for GUID when the ID is blank" { + Expect.isTrue (AutoId.NeedsAutoId Guid {| Id = "" |} "Id") "Blank ID should have returned true" + } + test "succeeds for GUID when the ID is filled" { + Expect.isFalse (AutoId.NeedsAutoId Guid {| Id = "abc" |} "Id") "Filled ID should have returned false" + } + test "fails for GUID when the ID is not a string" { + Expect.throwsT + (fun () -> AutoId.NeedsAutoId Guid {| Id = 8 |} "Id" |> ignore) + "String ID against a number should have thrown an exception" + } + test "succeeds for RandomString when the ID is blank" { + Expect.isTrue (AutoId.NeedsAutoId RandomString {| Id = "" |} "Id") "Blank ID should have returned true" + } + test "succeeds for RandomString when the ID is filled" { + Expect.isFalse (AutoId.NeedsAutoId RandomString {| Id = "x" |} "Id") "Filled ID should have returned false" + } + test "fails for RandomString when the ID is not a string" { + Expect.throwsT + (fun () -> AutoId.NeedsAutoId RandomString {| Id = 33 |} "Id" |> ignore) + "String ID against a number should have thrown an exception" + } + ] +] + +/// Unit tests for the Configuration module +let configurationTests = testList "Configuration" [ + test "useSerializer succeeds" { + try + Configuration.useSerializer + { new IDocumentSerializer with + member _.Serialize<'T>(it: 'T) : string = """{"Overridden":true}""" + member _.Deserialize<'T>(it: string) : 'T = Unchecked.defaultof<'T> + } + + let serialized = Configuration.serializer().Serialize {| Foo = "howdy"; Bar = "bye" |} + Expect.equal serialized """{"Overridden":true}""" "Specified serializer was not used" + + let deserialized = Configuration.serializer().Deserialize """{"Something":"here"}""" + Expect.isNull deserialized "Specified serializer should have returned null" + finally + Configuration.useSerializer DocumentSerializer.``default`` + } + test "serializer returns configured serializer" { + Expect.isTrue (obj.ReferenceEquals(DocumentSerializer.``default``, Configuration.serializer ())) + "Serializer should have been the same" + } + test "useIdField / idField succeeds" { + try + Expect.equal (Configuration.idField ()) "Id" "The default configured ID field was incorrect" + Configuration.useIdField "id" + Expect.equal (Configuration.idField ()) "id" "useIdField did not set the ID field" + finally + Configuration.useIdField "Id" + } + test "useAutoIdStrategy / autoIdStrategy succeeds" { + try + Expect.equal (Configuration.autoIdStrategy ()) Disabled "The default auto-ID strategy was incorrect" + Configuration.useAutoIdStrategy Guid + Expect.equal (Configuration.autoIdStrategy ()) Guid "The auto-ID strategy was not set correctly" + finally + Configuration.useAutoIdStrategy Disabled + } + test "useIdStringLength / idStringLength succeeds" { + try + Expect.equal (Configuration.idStringLength ()) 16 "The default ID string length was incorrect" + Configuration.useIdStringLength 33 + Expect.equal (Configuration.idStringLength ()) 33 "The ID string length was not set correctly" + finally + Configuration.useIdStringLength 16 + } ] /// Unit tests for the Query module @@ -344,54 +453,6 @@ let queryTests = testList "Query" [ ] ] -/// Unit tests for the Configuration module -let configurationTests = testList "Configuration" [ - test "useSerializer succeeds" { - try - Configuration.useSerializer - { new IDocumentSerializer with - member _.Serialize<'T>(it: 'T) : string = """{"Overridden":true}""" - member _.Deserialize<'T>(it: string) : 'T = Unchecked.defaultof<'T> - } - - let serialized = Configuration.serializer().Serialize {| Foo = "howdy"; Bar = "bye" |} - Expect.equal serialized """{"Overridden":true}""" "Specified serializer was not used" - - let deserialized = Configuration.serializer().Deserialize """{"Something":"here"}""" - Expect.isNull deserialized "Specified serializer should have returned null" - finally - Configuration.useSerializer DocumentSerializer.``default`` - } - test "serializer returns configured serializer" { - Expect.isTrue (obj.ReferenceEquals(DocumentSerializer.``default``, Configuration.serializer ())) - "Serializer should have been the same" - } - test "useIdField / idField succeeds" { - try - Expect.equal (Configuration.idField ()) "Id" "The default configured ID field was incorrect" - Configuration.useIdField "id" - Expect.equal (Configuration.idField ()) "id" "useIdField did not set the ID field" - finally - Configuration.useIdField "Id" - } - test "useAutoIdStrategy / autoIdStrategy succeeds" { - try - Expect.equal (Configuration.autoIdStrategy ()) Disabled "The default auto-ID strategy was incorrect" - Configuration.useAutoIdStrategy Guid - Expect.equal (Configuration.autoIdStrategy ()) Guid "The auto-ID strategy was not set correctly" - finally - Configuration.useAutoIdStrategy Disabled - } - test "useIdStringLength / idStringLength succeeds" { - try - Expect.equal (Configuration.idStringLength ()) 16 "The default ID string length was incorrect" - Configuration.useIdStringLength 33 - Expect.equal (Configuration.idStringLength ()) 33 "The ID string length was not set correctly" - finally - Configuration.useIdStringLength 16 - } -] - /// Tests which do not hit the database let all = testList "Common" [ opTests diff --git a/src/Tests/PostgresTests.fs b/src/Tests/PostgresTests.fs index ee31a27..497e712 100644 --- a/src/Tests/PostgresTests.fs +++ b/src/Tests/PostgresTests.fs @@ -252,18 +252,9 @@ let queryTests = testList "Query" [ open ThrowawayDb.Postgres open Types -/// Documents to use for integration tests -let documents = [ - { Id = "one"; Value = "FIRST!"; NumValue = 0; Sub = None } - { Id = "two"; Value = "another"; NumValue = 10; Sub = Some { Foo = "green"; Bar = "blue" } } - { Id = "three"; Value = ""; NumValue = 4; Sub = None } - { Id = "four"; Value = "purple"; NumValue = 17; Sub = Some { Foo = "green"; Bar = "red" } } - { Id = "five"; Value = "purple"; NumValue = 18; Sub = None } -] - /// Load the test documents into the database let loadDocs () = backgroundTask { - for doc in documents do do! insert PostgresDb.TableName doc + for doc in testDocuments do do! insert PostgresDb.TableName doc } /// Integration tests for the Configuration module of the PostgreSQL library @@ -435,6 +426,68 @@ let documentTests = testList "Document" [ |> Async.RunSynchronously) "An exception should have been raised for duplicate document ID insert" } + testTask "succeeds when adding a numeric auto ID" { + try + Configuration.useAutoIdStrategy Number + Configuration.useIdField "Key" + use db = PostgresDb.BuildDb() + let! before = Count.all PostgresDb.TableName + Expect.equal before 0 "There should be no documents in the table" + + do! insert PostgresDb.TableName { Key = 0; Text = "one" } + do! insert PostgresDb.TableName { Key = 0; Text = "two" } + do! insert PostgresDb.TableName { Key = 77; Text = "three" } + do! insert PostgresDb.TableName { Key = 0; Text = "four" } + + let! after = Find.allOrdered PostgresDb.TableName [ Field.Named "n:Key" ] + Expect.hasLength after 4 "There should have been 4 documents returned" + Expect.equal (after |> List.map _.Key) [ 1; 2; 77; 78 ] "The IDs were not generated correctly" + finally + Configuration.useAutoIdStrategy Disabled + Configuration.useIdField "Id" + } + testTask "succeeds when adding a GUID auto ID" { + try + Configuration.useAutoIdStrategy Guid + use db = PostgresDb.BuildDb() + let! before = Count.all PostgresDb.TableName + Expect.equal before 0 "There should be no documents in the table" + + do! insert PostgresDb.TableName { emptyDoc with Value = "one" } + do! insert PostgresDb.TableName { emptyDoc with Value = "two" } + do! insert PostgresDb.TableName { emptyDoc with Id = "abc123"; Value = "three" } + do! insert PostgresDb.TableName { emptyDoc with Value = "four" } + + let! after = Find.all PostgresDb.TableName + Expect.hasLength after 4 "There should have been 4 documents returned" + Expect.hasCountOf after 3u (fun doc -> doc.Id.Length = 32) "Three of the IDs should have been GUIDs" + Expect.hasCountOf after 1u (fun doc -> doc.Id = "abc123") "The provided ID should have been used as-is" + finally + Configuration.useAutoIdStrategy Disabled + } + testTask "succeeds when adding a RandomString auto ID" { + try + Configuration.useAutoIdStrategy RandomString + Configuration.useIdStringLength 44 + use db = PostgresDb.BuildDb() + let! before = Count.all PostgresDb.TableName + Expect.equal before 0 "There should be no documents in the table" + + do! insert PostgresDb.TableName { emptyDoc with Value = "one" } + do! insert PostgresDb.TableName { emptyDoc with Value = "two" } + do! insert PostgresDb.TableName { emptyDoc with Id = "abc123"; Value = "three" } + do! insert PostgresDb.TableName { emptyDoc with Value = "four" } + + let! after = Find.all PostgresDb.TableName + Expect.hasLength after 4 "There should have been 4 documents returned" + Expect.hasCountOf + after 3u (fun doc -> doc.Id.Length = 44) + "Three of the IDs should have been 44-character random strings" + Expect.hasCountOf after 1u (fun doc -> doc.Id = "abc123") "The provided ID should have been used as-is" + finally + Configuration.useAutoIdStrategy Disabled + Configuration.useIdStringLength 16 + } ] testList "save" [ testTask "succeeds when a document is inserted" { diff --git a/src/Tests/SqliteTests.fs b/src/Tests/SqliteTests.fs index 16b91eb..ebf0293 100644 --- a/src/Tests/SqliteTests.fs +++ b/src/Tests/SqliteTests.fs @@ -118,18 +118,9 @@ let parametersTests = testList "Parameters" [ (** INTEGRATION TESTS **) -/// Documents used for integration tests -let documents = [ - { Id = "one"; Value = "FIRST!"; NumValue = 0; Sub = None } - { Id = "two"; Value = "another"; NumValue = 10; Sub = Some { Foo = "green"; Bar = "blue" } } - { Id = "three"; Value = ""; NumValue = 4; Sub = None } - { Id = "four"; Value = "purple"; NumValue = 17; Sub = Some { Foo = "green"; Bar = "red" } } - { Id = "five"; Value = "purple"; NumValue = 18; Sub = None } -] - /// Load a table with the test documents let loadDocs () = backgroundTask { - for doc in documents do do! insert SqliteDb.TableName doc + for doc in testDocuments do do! insert SqliteDb.TableName doc } /// Integration tests for the Configuration module of the SQLite library @@ -283,6 +274,68 @@ let documentTests = testList "Document" [ insert SqliteDb.TableName {emptyDoc with Id = "test" } |> Async.AwaitTask |> Async.RunSynchronously) "An exception should have been raised for duplicate document ID insert" } + testTask "succeeds when adding a numeric auto ID" { + try + Configuration.useAutoIdStrategy Number + Configuration.useIdField "Key" + use! db = SqliteDb.BuildDb() + let! before = Count.all SqliteDb.TableName + Expect.equal before 0L "There should be no documents in the table" + + do! insert SqliteDb.TableName { Key = 0; Text = "one" } + do! insert SqliteDb.TableName { Key = 0; Text = "two" } + do! insert SqliteDb.TableName { Key = 77; Text = "three" } + do! insert SqliteDb.TableName { Key = 0; Text = "four" } + + let! after = Find.allOrdered SqliteDb.TableName [ Field.Named "Key" ] + Expect.hasLength after 4 "There should have been 4 documents returned" + Expect.equal (after |> List.map _.Key) [ 1; 2; 77; 78 ] "The IDs were not generated correctly" + finally + Configuration.useAutoIdStrategy Disabled + Configuration.useIdField "Id" + } + testTask "succeeds when adding a GUID auto ID" { + try + Configuration.useAutoIdStrategy Guid + use! db = SqliteDb.BuildDb() + let! before = Count.all SqliteDb.TableName + Expect.equal before 0L "There should be no documents in the table" + + do! insert SqliteDb.TableName { emptyDoc with Value = "one" } + do! insert SqliteDb.TableName { emptyDoc with Value = "two" } + do! insert SqliteDb.TableName { emptyDoc with Id = "abc123"; Value = "three" } + do! insert SqliteDb.TableName { emptyDoc with Value = "four" } + + let! after = Find.all SqliteDb.TableName + Expect.hasLength after 4 "There should have been 4 documents returned" + Expect.hasCountOf after 3u (fun doc -> doc.Id.Length = 32) "Three of the IDs should have been GUIDs" + Expect.hasCountOf after 1u (fun doc -> doc.Id = "abc123") "The provided ID should have been used as-is" + finally + Configuration.useAutoIdStrategy Disabled + } + testTask "succeeds when adding a RandomString auto ID" { + try + Configuration.useAutoIdStrategy RandomString + Configuration.useIdStringLength 44 + use! db = SqliteDb.BuildDb() + let! before = Count.all SqliteDb.TableName + Expect.equal before 0L "There should be no documents in the table" + + do! insert SqliteDb.TableName { emptyDoc with Value = "one" } + do! insert SqliteDb.TableName { emptyDoc with Value = "two" } + do! insert SqliteDb.TableName { emptyDoc with Id = "abc123"; Value = "three" } + do! insert SqliteDb.TableName { emptyDoc with Value = "four" } + + let! after = Find.all SqliteDb.TableName + Expect.hasLength after 4 "There should have been 4 documents returned" + Expect.hasCountOf + after 3u (fun doc -> doc.Id.Length = 44) + "Three of the IDs should have been 44-character random strings" + Expect.hasCountOf after 1u (fun doc -> doc.Id = "abc123") "The provided ID should have been used as-is" + finally + Configuration.useAutoIdStrategy Disabled + Configuration.useIdStringLength 16 + } ] testList "save" [ testTask "succeeds when a document is inserted" { diff --git a/src/Tests/Types.fs b/src/Tests/Types.fs index de2abca..0deb585 100644 --- a/src/Tests/Types.fs +++ b/src/Tests/Types.fs @@ -1,5 +1,9 @@ module Types +type NumIdDocument = + { Key: int + Text: string } + type SubDocument = { Foo: string Bar: string }