26 Commits

Author SHA1 Message Date
88f11a854f Add compat namespace functions
- Update package release notes
2024-08-17 14:08:46 -04:00
e2948ea6a1 Add ordered* tests for SQLite
- Ignore "n:" prefix for SQLite fields
- Remove byFIeld functions
2024-08-17 12:38:14 -04:00
d993f71788 Reorg SQLite tests 2024-08-16 22:54:27 -04:00
fba8ec7bc5 Finish ordered tests for PostgreSQL
- Rename tests, remove tested obsolete byField funcs
2024-08-16 22:04:23 -04:00
8e33299e22 WIP on ordered tests, test reorg 2024-08-15 22:32:25 -04:00
e07844570a Fix numeric field rendering in order by 2024-08-15 19:16:00 -04:00
bac3bd2ef0 Add *Ordered functions
- WIP on PostgreSQL numeric ordering
2024-08-14 22:13:46 -04:00
edd4d006da First cut of field order by function 2024-08-13 09:03:05 -04:00
96f5e1515d Remove byField functions 2024-08-12 09:07:25 -04:00
d382699a17 Merge branch 'wip-1' of https://git.bitbadger.solutions/bit-badger/BitBadger.Documents into wip-1 2024-08-11 17:06:07 -04:00
0c308c5f33 Add ToString for FieldMatch; add tests 2024-08-11 17:05:58 -04:00
77a6aaa583 Ignore IDE settings 2024-08-11 12:10:07 -04:00
d9d37f110d Add tests for PG parameter types
- Alter whereById to expect a parameter
2024-08-10 19:51:13 -04:00
e0fb793ec9 WIP on Postgres type derivation 2024-08-10 15:07:59 -04:00
74e5b77edb Complete common migration, WIP on tests 2024-08-10 09:56:59 -04:00
98bc83ac17 Pull count, exists, update to common 2024-08-09 22:55:12 -04:00
35755df99a WIP on pulling code up to Common 2024-08-09 20:15:08 -04:00
b1c3991e11 WIP on supporting numeric fields 2024-08-09 18:42:10 -04:00
85750e19f2 Add Postgres Query byFields tests 2024-08-08 22:36:48 -04:00
d8f64417e5 Add byFields to SQLite implementation
- Add fieldNameParams/FieldNames to Postgres
(syncs names between both implementations)
2024-08-08 19:45:25 -04:00
202fca272e Prefer seq over list for parameters
- This greatly reduces the duplicated functions needed
between F# and C#; F# lists satisfy the sequence reqs
2024-08-08 17:32:44 -04:00
8a15bdce2e Add byFields to PostgreSQL impl
- Swap howMatched and fields throughout
- Existing tests pass; still need new ones for byFields
2024-08-07 23:23:28 -04:00
d131eda56e Remove internal calls to deprecated funcs 2024-08-07 20:20:10 -04:00
e2232e91bb Add whereByFields code / tests
- Add wrapper class for unnamed field parameters
- Support table qualifiers by field
- Support dot access to document fields/sub-fields
2024-08-07 16:39:15 -04:00
e96c449324 WIP on PG Query.whereByFIelds 2024-08-07 08:30:28 -04:00
433302d995 WIP on field enhancements 2024-08-04 18:59:32 -04:00
16 changed files with 807 additions and 1589 deletions

3
.gitignore vendored
View File

@@ -397,6 +397,3 @@ FodyWeavers.xsd
# JetBrains Rider
*.sln.iml
**/.idea
# Test run files
src/*-tests.txt

View File

@@ -1,7 +1,5 @@
namespace BitBadger.Documents
open System.Security.Cryptography
/// The types of logical operations available for JSON fields
[<Struct>]
type Op =
@@ -150,70 +148,6 @@ type ParameterName() =
currentIdx <- currentIdx + 1
$"@field{currentIdx}"
#if NET6_0
open System.Text
#endif
/// Automatically-generated document ID strategies
[<Struct>]
type AutoId =
/// No automatic IDs will be generated
| Disabled
/// Generate a MAX-plus-1 numeric value for documents
| Number
/// Generate a GUID for each document (as a lowercase, no-dashes, 32-character string)
| Guid
/// Generate a random string of hexadecimal characters for each document
| RandomString
with
/// Generate a GUID string
static member GenerateGuid () =
System.Guid.NewGuid().ToString "N"
/// Generate a string of random hexadecimal characters
static member GenerateRandomString (length: int) =
#if NET8_0_OR_GREATER
RandomNumberGenerator.GetHexString(length, lowercase = true)
#else
RandomNumberGenerator.GetBytes((length / 2) + 1)
|> 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<int8> then
let value = prop.GetValue document :?> int8
value = int8 0
elif prop.PropertyType = typeof<int16> then
let value = prop.GetValue document :?> int16
value = int16 0
elif prop.PropertyType = typeof<int> then
let value = prop.GetValue document :?> int
value = 0
elif prop.PropertyType = typeof<int64> 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<string> 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
type IDocumentSerializer =
@@ -266,7 +200,7 @@ module Configuration =
serializerValue
/// The serialized name of the ID field for documents
let mutable private idFieldValue = "Id"
let mutable idFieldValue = "Id"
/// Specify the name of the ID field for documents
[<CompiledName "UseIdField">]
@@ -278,32 +212,6 @@ module Configuration =
let idField () =
idFieldValue
/// The automatic ID strategy used by the library
let mutable private autoIdValue = Disabled
/// Specify the automatic ID generation strategy used by the library
[<CompiledName "UseAutoIdStrategy">]
let useAutoIdStrategy it =
autoIdValue <- it
/// Retrieve the currently configured automatic ID generation strategy
[<CompiledName "AutoIdStrategy">]
let autoIdStrategy () =
autoIdValue
/// The length of automatically generated random strings
let mutable private idStringLengthValue = 16
/// Specify the length of automatically generated random strings
[<CompiledName "UseIdStringLength">]
let useIdStringLength length =
idStringLengthValue <- length
/// Retrieve the currently configured length of automatically generated random strings
[<CompiledName "IdStringLength">]
let idStringLength () =
idStringLengthValue
/// Query construction functions
[<RequireQualifiedAccess>]
@@ -401,13 +309,10 @@ module Query =
{ it with Name = parts[0] }, Some $" {parts[1]}"
else it, None)
|> Seq.map (fun (field, direction) ->
if field.Name.StartsWith "n:" then
let f = { field with Name = field.Name[2..] }
match dialect with PostgreSQL -> $"({f.Path PostgreSQL})::numeric" | SQLite -> f.Path SQLite
elif field.Name.StartsWith "i:" then
let p = { field with Name = field.Name[2..] }.Path dialect
match dialect with PostgreSQL -> $"LOWER({p})" | SQLite -> $"{p} COLLATE NOCASE"
else field.Path dialect
match dialect, field.Name.StartsWith "n:" with
| PostgreSQL, true -> $"({ { field with Name = field.Name[2..] }.Path PostgreSQL})::numeric"
| SQLite, true -> { field with Name = field.Name[2..] }.Path SQLite
| _, _ -> field.Path dialect
|> function path -> path + defaultArg direction "")
|> String.concat ", "
|> function it -> $" ORDER BY {it}"

View File

@@ -6,8 +6,7 @@
<AssemblyVersion>4.0.0.0</AssemblyVersion>
<FileVersion>4.0.0.0</FileVersion>
<VersionPrefix>4.0.0</VersionPrefix>
<VersionSuffix>rc2</VersionSuffix>
<PackageReleaseNotes>From rc1: add case-insensitive ordering. From v3.1: Change ByField to ByFields; support dot-access to nested document fields; add Find*Ordered functions/methods; see project site for breaking changes and compatibility</PackageReleaseNotes>
<PackageReleaseNotes>Change ByField to ByFields; support dot-access to nested document fields; add Find*Ordered functions/methods; see project site for breaking changes and compatibility</PackageReleaseNotes>
<Authors>danieljsummers</Authors>
<Company>Bit Badger Solutions</Company>
<PackageReadmeFile>README.md</PackageReadmeFile>

View File

@@ -312,23 +312,7 @@ module WithProps =
/// Insert a new document
[<CompiledName "Insert">]
let insert<'TDoc> tableName (document: 'TDoc) 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
Custom.nonQuery (Query.insert tableName) [ jsonParam "@data" document ] sqlProps
/// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
[<CompiledName "Save">]

View File

@@ -35,11 +35,11 @@ module Extensions =
/// Insert a new document
member conn.insert<'TDoc> tableName (document: 'TDoc) =
WithConn.Document.insert<'TDoc> tableName document conn
WithConn.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.Document.save tableName document conn
WithConn.save tableName document conn
/// Count all documents in a table
member conn.countAll tableName =
@@ -159,12 +159,12 @@ type SqliteConnectionCSharpExtensions =
/// Insert a new document
[<Extension>]
static member inline Insert<'TDoc>(conn, tableName, document: 'TDoc) =
WithConn.Document.insert<'TDoc> tableName document conn
WithConn.insert<'TDoc> tableName document conn
/// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
[<Extension>]
static member inline Save<'TDoc>(conn, tableName, document: 'TDoc) =
WithConn.Document.save<'TDoc> tableName document conn
WithConn.save<'TDoc> tableName document conn
/// Count all documents in a table
[<Extension>]

View File

@@ -258,29 +258,10 @@ module WithConn =
let ensureFieldIndex tableName indexName fields conn =
Custom.nonQuery (Query.Definition.ensureIndexOn tableName indexName fields SQLite) [] conn
/// Commands to add documents
[<AutoOpen>]
module Document =
/// Insert a new document
[<CompiledName "Insert">]
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
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")
[<CompiledName "Save">]
@@ -566,13 +547,13 @@ module Document =
[<CompiledName "Insert">]
let insert<'TDoc> tableName (document: 'TDoc) =
use conn = Configuration.dbConn ()
WithConn.Document.insert tableName document conn
WithConn.insert tableName document conn
/// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
[<CompiledName "Save">]
let save<'TDoc> tableName (document: 'TDoc) =
use conn = Configuration.dbConn ()
WithConn.Document.save tableName document conn
WithConn.save tableName document conn
/// Commands to count documents

View File

@@ -22,9 +22,57 @@ internal class TestSerializer : IDocumentSerializer
public static class CommonCSharpTests
{
/// <summary>
/// Unit tests for the Op enum
/// Unit tests
/// </summary>
private static readonly Test OpTests = TestList("Op",
[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<object>("{\"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");
}
})
])),
TestList("Op",
[
TestCase("EQ succeeds", () =>
{
@@ -62,12 +110,8 @@ public static class CommonCSharpTests
{
Expect.equal(Op.NEX.ToString(), "IS NULL", "The \"not exists\" operator was not correct");
})
]);
/// <summary>
/// Unit tests for the Field class
/// </summary>
private static readonly Test FieldTests = TestList("Field",
]),
TestList("Field",
[
TestCase("EQ succeeds", () =>
{
@@ -201,7 +245,8 @@ public static class CommonCSharpTests
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");
Expect.equal("this.data->>'SomethingElse'", field.Path(Dialect.SQLite),
"The SQLite path is incorrect");
}),
TestCase("succeeds for a SQLite nested field with no qualifier", () =>
{
@@ -212,15 +257,12 @@ public static class CommonCSharpTests
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");
Expect.equal("bird.data->>'Nest'->>'Away'", field.Path(Dialect.SQLite),
"The SQLite path is incorrect");
})
])
]);
/// <summary>
/// Unit tests for the FieldMatch enum
/// </summary>
private static readonly Test FieldMatchTests = TestList("FieldMatch.ToString",
]),
TestList("FieldMatch.ToString",
[
TestCase("succeeds for Any", () =>
{
@@ -230,12 +272,8 @@ public static class CommonCSharpTests
{
Expect.equal(FieldMatch.All.ToString(), "AND", "SQL for All is incorrect");
})
]);
/// <summary>
/// Unit tests for the ParameterName class
/// </summary>
private static readonly Test ParameterNameTests = TestList("ParameterName.Derive",
]),
TestList("ParameterName.Derive",
[
TestCase("succeeds with existing name", () =>
{
@@ -256,229 +294,8 @@ public static class CommonCSharpTests
Expect.equal(name.Derive(FSharpOption<string>.None), "@field3",
"Counter should have advanced from previous call");
})
]);
/// <summary>
/// Unit tests for the AutoId enum
/// </summary>
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
}
})
])
]);
/// <summary>
/// Unit tests for the Configuration static class
/// </summary>
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<object>("{\"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);
}
})
]);
/// <summary>
/// Unit tests for the Query static class
/// </summary>
private static readonly Test QueryTests = TestList("Query",
]),
TestList("Query",
[
TestCase("StatementWhere succeeds", () =>
{
@@ -569,7 +386,8 @@ public static class CommonCSharpTests
[
TestCase("succeeds for no fields", () =>
{
Expect.equal(Query.OrderBy([], Dialect.PostgreSQL), "", "Order By should have been blank (PostgreSQL)");
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", () =>
@@ -589,7 +407,8 @@ public static class CommonCSharpTests
[
Field.Named("Nested.Test.Field DESC"), Field.Named("AnotherField"),
Field.Named("It DESC")
], Dialect.PostgreSQL),
],
Dialect.PostgreSQL),
" ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC",
"Order By not constructed correctly");
}),
@@ -600,7 +419,8 @@ public static class CommonCSharpTests
[
Field.Named("Nested.Test.Field DESC"), Field.Named("AnotherField"),
Field.Named("It DESC")
], Dialect.SQLite),
],
Dialect.SQLite),
" ORDER BY data->>'Nested'->>'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC",
"Order By not constructed correctly");
}),
@@ -613,34 +433,8 @@ public static class CommonCSharpTests
{
Expect.equal(Query.OrderBy([Field.Named("n:Test")], Dialect.SQLite), " ORDER BY data->>'Test'",
"Order By not constructed correctly for numeric field");
}),
TestCase("succeeds for PostgreSQL case-insensitive ordering", () =>
{
Expect.equal(Query.OrderBy([Field.Named("i:Test.Field DESC")], Dialect.PostgreSQL),
" ORDER BY LOWER(data#>>'{Test,Field}') DESC",
"Order By not constructed correctly for case-insensitive field");
}),
TestCase("succeeds for SQLite case-insensitive ordering", () =>
{
Expect.equal(Query.OrderBy([Field.Named("i:Test.Field ASC")], Dialect.SQLite),
" ORDER BY data->>'Test'->>'Field' COLLATE NOCASE ASC",
"Order By not constructed correctly for case-insensitive field");
})
])
]);
/// <summary>
/// Unit tests
/// </summary>
[Tests]
public static readonly Test Unit = TestList("Common.C# Unit",
[
OpTests,
FieldTests,
FieldMatchTests,
ParameterNameTests,
AutoIdTests,
QueryTests,
TestSequenced(ConfigurationTests)
])
]);
}

View File

@@ -170,11 +170,7 @@ 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");
})
])
]);
/// <summary>
@@ -314,6 +310,15 @@ public static class PostgresCSharpTests
})
]);
/// <summary>
/// Tests which do not hit the database
/// </summary>
private static readonly Test Unit = TestList("Unit",
[
ParametersTests,
QueryTests
]);
private static readonly List<JsonDocument> TestDocuments =
[
new() { Id = "one", Value = "FIRST!", NumValue = 0 },
@@ -543,83 +548,6 @@ 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<NumIdDocument>(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<JsonDocument>(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<JsonDocument>(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",

View File

@@ -325,86 +325,9 @@ 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<NumIdDocument>(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<JsonDocument>(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<JsonDocument>(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("Save",
TestList("Document.Save",
[
TestCase("succeeds when a document is inserted", async () =>
{
@@ -627,9 +550,8 @@ public static class SqliteCSharpTests
var docs = await Find.ByFieldsOrdered<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.GT("NumValue", 15)], [Field.Named("Id")]);
Expect.hasLength(docs, 2, "There should have been two documents returned");
Expect.equal(string.Join('|', docs.Select(x => x.Id)), "five|four",
"The documents were not sorted correctly");
"There should have been two documents returned");
}),
TestCase("succeeds when documents are not found", async () =>
{
@@ -638,31 +560,8 @@ public static class SqliteCSharpTests
var docs = await Find.ByFieldsOrdered<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.GT("NumValue", 15)], [Field.Named("Id DESC")]);
Expect.hasLength(docs, 2, "There should have been two documents returned");
Expect.equal(string.Join('|', docs.Select(x => x.Id)), "four|five",
"The documents were not sorted correctly");
}),
TestCase("succeeds when sorting case-sensitively", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var docs = await Find.ByFieldsOrdered<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.LE("NumValue", 10)], [Field.Named("Value")]);
Expect.hasLength(docs, 3, "There should have been three documents returned");
Expect.equal(string.Join('|', docs.Select(x => x.Id)), "three|one|two",
"The documents were not sorted correctly");
}),
TestCase("succeeds when sorting case-insensitively", async () =>
{
await using var db = await SqliteDb.BuildDb();
await LoadDocs();
var docs = await Find.ByFieldsOrdered<JsonDocument>(SqliteDb.TableName, FieldMatch.Any,
[Field.LE("NumValue", 10)], [Field.Named("i:Value")]);
Expect.hasLength(docs, 3, "There should have been three documents returned");
Expect.equal(string.Join('|', docs.Select(x => x.Id)), "three|two|one",
"The documents were not sorted correctly");
"There should have been two documents returned");
})
]),
TestList("FirstByFields",

View File

@@ -1,11 +1,5 @@
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; } = "";

View File

@@ -6,8 +6,8 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="Types.fs" />
<Compile Include="CommonTests.fs" />
<Compile Include="Types.fs" />
<Compile Include="PostgresTests.fs" />
<Compile Include="PostgresExtensionTests.fs" />
<Compile Include="SqliteTests.fs" />

View File

@@ -6,8 +6,10 @@ open Expecto
/// Test table name
let tbl = "test_table"
/// Unit tests for the Op DU
let opTests = testList "Op" [
/// Tests which do not hit the database
let all =
testList "Common" [
testList "Op" [
test "EQ succeeds" {
Expect.equal (string EQ) "=" "The equals operator was not correct"
}
@@ -35,10 +37,8 @@ let opTests = testList "Op" [
test "NEX succeeds" {
Expect.equal (string NEX) "IS NULL" """The "not exists" operator was not correct"""
}
]
/// Unit tests for the Field class
let fieldTests = testList "Field" [
]
testList "Field" [
test "EQ succeeds" {
let field = Field.EQ "Test" 14
Expect.equal field.Name "Test" "Field name incorrect"
@@ -111,10 +111,12 @@ let fieldTests = testList "Field" [
}
testList "NameToPath" [
test "succeeds for PostgreSQL and a simple name" {
Expect.equal "data->>'Simple'" (Field.NameToPath "Simple" PostgreSQL) "Path not constructed correctly"
Expect.equal
"data->>'Simple'" (Field.NameToPath "Simple" PostgreSQL) "Path not constructed correctly"
}
test "succeeds for SQLite and a simple name" {
Expect.equal "data->>'Simple'" (Field.NameToPath "Simple" SQLite) "Path not constructed correctly"
Expect.equal
"data->>'Simple'" (Field.NameToPath "Simple" SQLite) "Path not constructed correctly"
}
test "succeeds for PostgreSQL and a nested name" {
Expect.equal
@@ -146,7 +148,8 @@ let fieldTests = testList "Field" [
}
test "succeeds for a PostgreSQL single field with a qualifier" {
let field = { Field.LT "SomethingElse" 9 with Qualifier = Some "this" }
Expect.equal "this.data->>'SomethingElse'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect"
Expect.equal
"this.data->>'SomethingElse'" (field.Path PostgreSQL) "The PostgreSQL path is incorrect"
}
test "succeeds for a PostgreSQL nested field with no qualifier" {
let field = Field.EQ "My.Nested.Field" "howdy"
@@ -173,20 +176,16 @@ let fieldTests = testList "Field" [
Expect.equal "bird.data->>'Nest'->>'Away'" (field.Path SQLite) "The SQLite path is incorrect"
}
]
]
/// Unit tests for the FieldMatch DU
let fieldMatchTests = testList "FieldMatch.ToString" [
]
testList "FieldMatch.ToString" [
test "succeeds for Any" {
Expect.equal (string Any) "OR" "SQL for Any is incorrect"
}
test "succeeds for All" {
Expect.equal (string All) "AND" "SQL for All is incorrect"
}
]
/// Unit tests for the ParameterName class
let parameterNameTests = testList "ParameterName.Derive" [
]
testList "ParameterName.Derive" [
test "succeeds with existing name" {
let name = ParameterName()
Expect.equal (name.Derive(Some "@taco")) "@taco" "Name should have been @taco"
@@ -199,137 +198,8 @@ let parameterNameTests = testList "ParameterName.Derive" [
Expect.equal (name.Derive None) "@field2" "Counter should have advanced from previous call"
Expect.equal (name.Derive None) "@field3" "Counter should have advanced from previous call"
}
]
/// Unit tests for the AutoId DU
let autoIdTests = testList "AutoId" [
test "GenerateGuid succeeds" {
let 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"
}
test "GenerateRandomString succeeds" {
[ 6; 8; 12; 20; 32; 57; 64 ]
|> List.iter (fun length ->
let 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" [
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<System.InvalidOperationException>
(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<System.InvalidOperationException>
(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<System.InvalidOperationException>
(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<System.InvalidOperationException>
(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<obj> """{"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
let queryTests = testList "Query" [
testList "Query" [
test "statementWhere succeeds" {
Expect.equal (Query.statementWhere "x" "y") "x WHERE y" "Statements not combined correctly"
}
@@ -450,28 +320,6 @@ let queryTests = testList "Query" [
" ORDER BY data->>'Test'"
"Order By not constructed correctly for numeric field"
}
test "succeeds for PostgreSQL case-insensitive ordering" {
Expect.equal
(Query.orderBy [ Field.Named "i:Test.Field DESC" ] PostgreSQL)
" ORDER BY LOWER(data#>>'{Test,Field}') DESC"
"Order By not constructed correctly for case-insensitive field"
}
test "succeeds for SQLite case-insensitive ordering" {
Expect.equal
(Query.orderBy [ Field.Named "i:Test.Field ASC" ] SQLite)
" ORDER BY data->>'Test'->>'Field' COLLATE NOCASE ASC"
"Order By not constructed correctly for case-insensitive field"
}
]
]
/// Tests which do not hit the database
let all = testList "Common" [
opTests
fieldTests
fieldMatchTests
parameterNameTests
autoIdTests
queryTests
testSequenced configurationTests
]
]
]

View File

@@ -252,9 +252,18 @@ 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 testDocuments do do! insert PostgresDb.TableName doc
for doc in documents do do! insert PostgresDb.TableName doc
}
/// Integration tests for the Configuration module of the PostgreSQL library
@@ -426,68 +435,6 @@ 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<NumIdDocument> 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<JsonDocument> 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<JsonDocument> 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" {

View File

@@ -118,9 +118,18 @@ 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 testDocuments do do! insert SqliteDb.TableName doc
for doc in documents do do! insert SqliteDb.TableName doc
}
/// Integration tests for the Configuration module of the SQLite library
@@ -135,6 +144,32 @@ let configurationTests = testList "Configuration" [
finally
Configuration.useConnectionString "Data Source=:memory:"
}
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<obj> """{"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" {
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"
Configuration.useIdField "Id"
}
]
/// Integration tests for the Custom module of the SQLite library
@@ -274,68 +309,6 @@ 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<NumIdDocument> 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<JsonDocument> 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<JsonDocument> 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" {
@@ -527,7 +500,6 @@ let findTests = testList "Find" [
let! docs =
Find.byFieldsOrdered<JsonDocument>
SqliteDb.TableName Any [ Field.GT "NumValue" 15 ] [ Field.Named "Id" ]
Expect.hasLength docs 2 "There should have been two documents returned"
Expect.equal
(docs |> List.map _.Id |> String.concat "|") "five|four" "The documents were not ordered correctly"
}
@@ -538,32 +510,9 @@ let findTests = testList "Find" [
let! docs =
Find.byFieldsOrdered<JsonDocument>
SqliteDb.TableName Any [ Field.GT "NumValue" 15 ] [ Field.Named "Id DESC" ]
Expect.hasLength docs 2 "There should have been two documents returned"
Expect.equal
(docs |> List.map _.Id |> String.concat "|") "four|five" "The documents were not ordered correctly"
}
testTask "succeeds when sorting case-sensitively" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! docs =
Find.byFieldsOrdered<JsonDocument>
SqliteDb.TableName All [ Field.LE "NumValue" 10 ] [ Field.Named "Value" ]
Expect.hasLength docs 3 "There should have been three documents returned"
Expect.equal
(docs |> List.map _.Id |> String.concat "|") "three|one|two" "Documents not ordered correctly"
}
testTask "succeeds when sorting case-insensitively" {
use! db = SqliteDb.BuildDb()
do! loadDocs ()
let! docs =
Find.byFieldsOrdered<JsonDocument>
SqliteDb.TableName All [ Field.LE "NumValue" 10 ] [ Field.Named "i:Value" ]
Expect.hasLength docs 3 "There should have been three documents returned"
Expect.equal
(docs |> List.map _.Id |> String.concat "|") "three|two|one" "Documents not ordered correctly"
}
]
testList "firstByFields" [
testTask "succeeds when a document is found" {

View File

@@ -1,9 +1,5 @@
module Types
type NumIdDocument =
{ Key: int
Text: string }
type SubDocument =
{ Foo: string
Bar: string }

View File

@@ -1,16 +1,13 @@
#!/bin/bash
echo --- Package Common library
rm Common/bin/Release/BitBadger.Documents.Common.*.nupkg || true
dotnet pack Common/BitBadger.Documents.Common.fsproj -c Release
cp Common/bin/Release/BitBadger.Documents.Common.*.nupkg .
echo --- Package PostgreSQL library
rm Postgres/bin/Release/BitBadger.Documents.Postgres*.nupkg || true
dotnet pack Postgres/BitBadger.Documents.Postgres.fsproj -c Release
cp Postgres/bin/Release/BitBadger.Documents.Postgres.*.nupkg .
echo --- Package SQLite library
rm Sqlite/bin/Release/BitBadger.Documents.Sqlite*.nupkg || true
dotnet pack Sqlite/BitBadger.Documents.Sqlite.fsproj -c Release
cp Sqlite/bin/Release/BitBadger.Documents.Sqlite.*.nupkg .