diff --git a/src/Common/BitBadger.Documents.Common.fsproj b/src/Common/BitBadger.Documents.Common.fsproj index 388732d..8129e69 100644 --- a/src/Common/BitBadger.Documents.Common.fsproj +++ b/src/Common/BitBadger.Documents.Common.fsproj @@ -13,7 +13,9 @@ - + + + diff --git a/src/Common/Library.fs b/src/Common/Library.fs index 83053be..f51e81a 100644 --- a/src/Common/Library.fs +++ b/src/Common/Library.fs @@ -4,26 +4,37 @@ open System.Security.Cryptography /// The types of comparisons available for JSON fields type Comparison = + /// Equals (=) | Equal of Value: obj + /// Greater Than (>) | Greater of Value: obj + /// Greater Than or Equal To (>=) | GreaterOrEqual of Value: obj + /// Less Than (<) | Less of Value: obj + /// Less Than or Equal To (<=) | LessOrEqual of Value: obj + /// Not Equal to (<>) | NotEqual of Value: obj + /// Between (BETWEEN) | Between of Min: obj * Max: obj + /// In (IN) | In of Values: obj seq + /// In Array (PostgreSQL: |?, SQLite: EXISTS / json_each / IN) | InArray of Table: string * Values: obj seq + /// Exists (IS NOT NULL) | Exists + /// Does Not Exist (IS NULL) | NotExists @@ -53,26 +64,29 @@ type Dialect = /// The format in which an element of a JSON field should be extracted [] type FieldFormat = + /// Use ->> or #>>; extracts a text (PostgreSQL) or SQL (SQLite) value | AsSql + /// Use -> or #>; extracts a JSONB (PostgreSQL) or JSON (SQLite) value | AsJson /// Criteria for a field WHERE clause -type Field = - { /// The name of the field - Name: string +type Field = { - /// The comparison for the field - Comparison: Comparison + /// The name of the field + Name: string - /// The name of the parameter for this field - ParameterName: string option + /// The comparison for the field + Comparison: Comparison - /// The table qualifier for this field - Qualifier: string option } -with + /// The name of the parameter for this field + ParameterName: string option + + /// The table qualifier for this field + Qualifier: string option +} with /// Create a comparison against a field static member Where name (comparison: Comparison) = @@ -190,8 +204,10 @@ with /// How fields should be matched [] type FieldMatch = + /// Any field matches (OR) | Any + /// All fields match (AND) | All @@ -202,6 +218,7 @@ type FieldMatch = /// Derive parameter names (each instance wraps a counter to uniquely name anonymous fields) type ParameterName() = + /// The counter for the next field value let mutable currentIdx = -1 @@ -213,35 +230,30 @@ type ParameterName() = currentIdx <- currentIdx + 1 $"@field{currentIdx}" -#if NET6_0 -open System.Text -#endif /// Automatically-generated document ID strategies [] 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 () = + 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 + static member GenerateRandomString(length: int) = 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 = diff --git a/src/Common/README.md b/src/Common/README.md index 7047424..c0107b8 100644 --- a/src/Common/README.md +++ b/src/Common/README.md @@ -7,6 +7,7 @@ This package provides common definitions and functionality for `BitBadger.Docume ## Features - Select, insert, update, save (upsert), delete, count, and check existence of documents, and create tables and indexes for these documents +- Automatically generate IDs for documents (numeric IDs, GUIDs, or random strings) - Addresses documents via ID and via comparison on any field (for PostgreSQL, also via equality on any property by using JSON containment, or via condition on any property using JSON Path queries) - Accesses documents as your domain models (POCOs) - Uses `Task`-based async for all data access functions diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 425a298..60cc6e9 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,22 +1,19 @@ - net6.0;net8.0 + net8.0;net9.0 embedded false 4.0.0.0 4.0.0.0 4.0.0 - rc5 From v3.1: (see project site for breaking changes and compatibility) - Change ByField to ByFields - Support dot-access to nested document fields - Add Find*Ordered functions/methods - -Release Candidate Changes: -- from v4-rc4: Field construction functions are now generic. -- from v4-rc3: Add In/InArray field comparisons, revamp internal comparison handling. -- from v4-rc2: preserve additional ORDER BY qualifiers. -- from v4-rc1: add case-insensitive ordering. +- Add case-insensitive ordering (as of rc2) +- Preserve additional ORDER BY qualifiers (as of rc3) +- Add In / InArray comparisons (as of rc4) +- Field construction functions are generic (as of rc5) danieljsummers Bit Badger Solutions README.md diff --git a/src/Postgres/BitBadger.Documents.Postgres.fsproj b/src/Postgres/BitBadger.Documents.Postgres.fsproj index 6773b53..79ba4a6 100644 --- a/src/Postgres/BitBadger.Documents.Postgres.fsproj +++ b/src/Postgres/BitBadger.Documents.Postgres.fsproj @@ -14,8 +14,9 @@ - - + + + diff --git a/src/Postgres/Compat.fs b/src/Postgres/Compat.fs index dae2bfa..349efe7 100644 --- a/src/Postgres/Compat.fs +++ b/src/Postgres/Compat.fs @@ -71,7 +71,7 @@ module WithProps = /// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found [] - let FirstByField<'TDoc when 'TDoc: null>(tableName, field, sqlProps) = + let FirstByField<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, field, sqlProps) = WithProps.Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field, sqlProps) [] @@ -144,7 +144,7 @@ module Find = /// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found [] - let FirstByField<'TDoc when 'TDoc: null>(tableName, field) = + let FirstByField<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, field) = Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field) @@ -248,7 +248,7 @@ type NpgsqlConnectionCSharpCompatExtensions = /// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found [] [] - static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, field) = + static member inline FindFirstByField<'TDoc when 'TDoc: null and 'TDoc: not struct>(conn, tableName, field) = WithProps.Find.FirstByFields<'TDoc>(tableName, Any, [ field ], Sql.existingConnection conn) /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) diff --git a/src/Postgres/Extensions.fs b/src/Postgres/Extensions.fs index 2b3b63a..17c7810 100644 --- a/src/Postgres/Extensions.fs +++ b/src/Postgres/Extensions.fs @@ -211,7 +211,7 @@ type NpgsqlConnectionCSharpExtensions = /// Execute a query that returns one or no results; returns None if not found [] - static member inline CustomSingle<'TDoc when 'TDoc: null>( + static member inline CustomSingle<'TDoc when 'TDoc: null and 'TDoc: not struct>( conn, query, parameters, mapFunc: System.Func) = WithProps.Custom.Single<'TDoc>(query, parameters, mapFunc, Sql.existingConnection conn) @@ -303,7 +303,7 @@ type NpgsqlConnectionCSharpExtensions = /// Retrieve a document by its ID; returns None if not found [] - static member inline FindById<'TKey, 'TDoc when 'TDoc: null>(conn, tableName, docId: 'TKey) = + static member inline FindById<'TKey, 'TDoc when 'TDoc: null and 'TDoc: not struct>(conn, tableName, docId: 'TKey) = WithProps.Find.ById<'TKey, 'TDoc>(tableName, docId, Sql.existingConnection conn) /// Retrieve documents matching a JSON field comparison query (->> =) @@ -339,38 +339,41 @@ type NpgsqlConnectionCSharpExtensions = /// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found [] - static member inline FindFirstByFields<'TDoc when 'TDoc: null>(conn, tableName, howMatched, fields) = + static member inline FindFirstByFields<'TDoc when 'TDoc: null and 'TDoc: not struct>( + conn, tableName, howMatched, fields) = WithProps.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, Sql.existingConnection conn) /// Retrieve the first document matching a JSON field comparison query (->> =) ordered by the given fields in the /// document; returns null if not found [] - static member inline FindFirstByFieldsOrdered<'TDoc when 'TDoc: null>( + static member inline FindFirstByFieldsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>( conn, tableName, howMatched, queryFields, orderFields) = WithProps.Find.FirstByFieldsOrdered<'TDoc>( tableName, howMatched, queryFields, orderFields, Sql.existingConnection conn) /// Retrieve the first document matching a JSON containment query (@>); returns None if not found [] - static member inline FindFirstByContains<'TDoc when 'TDoc: null>(conn, tableName, criteria: obj) = + static member inline FindFirstByContains<'TDoc when 'TDoc: null and 'TDoc: not struct>( + conn, tableName, criteria: obj) = WithProps.Find.FirstByContains<'TDoc>(tableName, criteria, Sql.existingConnection conn) /// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the document; /// returns None if not found [] - static member inline FindFirstByContainsOrdered<'TDoc when 'TDoc: null>( + static member inline FindFirstByContainsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>( conn, tableName, criteria: obj, orderFields) = WithProps.Find.FirstByContainsOrdered<'TDoc>(tableName, criteria, orderFields, Sql.existingConnection conn) /// Retrieve the first document matching a JSON Path match query (@?); returns None if not found [] - static member inline FindFirstByJsonPath<'TDoc when 'TDoc: null>(conn, tableName, jsonPath) = + static member inline FindFirstByJsonPath<'TDoc when 'TDoc: null and 'TDoc: not struct>(conn, tableName, jsonPath) = WithProps.Find.FirstByJsonPath<'TDoc>(tableName, jsonPath, Sql.existingConnection conn) /// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the document; /// returns None if not found [] - static member inline FindFirstByJsonPathOrdered<'TDoc when 'TDoc: null>(conn, tableName, jsonPath, orderFields) = + static member inline FindFirstByJsonPathOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>( + conn, tableName, jsonPath, orderFields) = WithProps.Find.FirstByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields, Sql.existingConnection conn) /// Update an entire document by its ID diff --git a/src/Postgres/Library.fs b/src/Postgres/Library.fs index 4b4b3c2..39b3882 100644 --- a/src/Postgres/Library.fs +++ b/src/Postgres/Library.fs @@ -3,8 +3,10 @@ /// The type of index to generate for the document [] type DocumentIndex = + /// A GIN index with standard operations (all operators supported) | Full + /// A GIN index with JSONPath operations (optimized for @>, @?, @@ operators) | Optimized @@ -36,6 +38,7 @@ open Npgsql.FSharp /// Helper functions [] module private Helpers = + /// Shorthand to retrieve the data source as SqlProps let internal fromDataSource () = Configuration.dataSource () |> Sql.fromDataSource @@ -272,7 +275,7 @@ module WithProps = } /// Execute a query that returns one or no results; returns null if not found - let Single<'TDoc when 'TDoc: null>( + let Single<'TDoc when 'TDoc: null and 'TDoc: not struct>( query, parameters, mapFunc: System.Func, sqlProps) = backgroundTask { let! result = single<'TDoc> query parameters mapFunc.Invoke sqlProps return Option.toObj result @@ -439,7 +442,7 @@ module WithProps = Custom.single (Query.byId (Query.find tableName) docId) [ idParam docId ] fromData<'TDoc> sqlProps /// Retrieve a document by its ID (returns null if not found) - let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId: 'TKey, sqlProps) = + let ById<'TKey, 'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, docId: 'TKey, sqlProps) = Custom.Single<'TDoc>( Query.byId (Query.find tableName) docId, [ idParam docId ], fromData<'TDoc>, sqlProps) @@ -549,7 +552,7 @@ module WithProps = sqlProps /// Retrieve the first document matching JSON field comparisons (->> =); returns null if not found - let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields, sqlProps) = + let FirstByFields<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, howMatched, fields, sqlProps) = Custom.Single<'TDoc>( $"{Query.byFields (Query.find tableName) howMatched fields} LIMIT 1", addFieldParams fields [], @@ -568,7 +571,8 @@ module WithProps = /// Retrieve the first document matching JSON field comparisons (->> =) ordered by the given fields in the /// document; returns null if not found - let FirstByFieldsOrdered<'TDoc when 'TDoc: null>(tableName, howMatched, queryFields, orderFields, sqlProps) = + let FirstByFieldsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>( + tableName, howMatched, queryFields, orderFields, sqlProps) = Custom.Single<'TDoc>( $"{Query.byFields (Query.find tableName) howMatched queryFields}{Query.orderBy orderFields PostgreSQL} LIMIT 1", addFieldParams queryFields [], @@ -585,7 +589,7 @@ module WithProps = sqlProps /// Retrieve the first document matching a JSON containment query (@>); returns null if not found - let FirstByContains<'TDoc when 'TDoc: null>(tableName, criteria: obj, sqlProps) = + let FirstByContains<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, criteria: obj, sqlProps) = Custom.Single<'TDoc>( $"{Query.byContains (Query.find tableName)} LIMIT 1", [ jsonParam "@criteria" criteria ], @@ -604,7 +608,8 @@ module WithProps = /// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the /// document; returns null if not found - let FirstByContainsOrdered<'TDoc when 'TDoc: null>(tableName, criteria: obj, orderFields, sqlProps) = + let FirstByContainsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>( + tableName, criteria: obj, orderFields, sqlProps) = Custom.Single<'TDoc>( $"{Query.byContains (Query.find tableName)}{Query.orderBy orderFields PostgreSQL} LIMIT 1", [ jsonParam "@criteria" criteria ], @@ -621,7 +626,7 @@ module WithProps = sqlProps /// Retrieve the first document matching a JSON Path match query (@?); returns null if not found - let FirstByJsonPath<'TDoc when 'TDoc: null>(tableName, jsonPath, sqlProps) = + let FirstByJsonPath<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, jsonPath, sqlProps) = Custom.Single<'TDoc>( $"{Query.byPathMatch (Query.find tableName)} LIMIT 1", [ "@path", Sql.string jsonPath ], @@ -640,7 +645,8 @@ module WithProps = /// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the /// document; returns null if not found - let FirstByJsonPathOrdered<'TDoc when 'TDoc: null>(tableName, jsonPath, orderFields, sqlProps) = + let FirstByJsonPathOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>( + tableName, jsonPath, orderFields, sqlProps) = Custom.Single<'TDoc>( $"{Query.byPathMatch (Query.find tableName)}{Query.orderBy orderFields PostgreSQL} LIMIT 1", [ "@path", Sql.string jsonPath ], @@ -779,7 +785,8 @@ module Custom = WithProps.Custom.single<'TDoc> query parameters mapFunc (fromDataSource ()) /// Execute a query that returns one or no results; returns null if not found - let Single<'TDoc when 'TDoc: null>(query, parameters, mapFunc: System.Func) = + let Single<'TDoc when 'TDoc: null and 'TDoc: not struct>( + query, parameters, mapFunc: System.Func) = WithProps.Custom.Single<'TDoc>(query, parameters, mapFunc, fromDataSource ()) /// Execute a query that returns no results @@ -910,7 +917,7 @@ module Find = WithProps.Find.byId<'TKey, 'TDoc> tableName docId (fromDataSource ()) /// Retrieve a document by its ID; returns null if not found - let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId: 'TKey) = + let ById<'TKey, 'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, docId: 'TKey) = WithProps.Find.ById<'TKey, 'TDoc>(tableName, docId, fromDataSource ()) /// Retrieve documents matching a JSON field comparison query (->> =) @@ -973,7 +980,7 @@ module Find = WithProps.Find.firstByFields<'TDoc> tableName howMatched fields (fromDataSource ()) /// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found - let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields) = + let FirstByFields<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, howMatched, fields) = WithProps.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, fromDataSource ()) /// Retrieve the first document matching a JSON field comparison query (->> =) ordered by the given fields in the @@ -984,7 +991,8 @@ module Find = /// Retrieve the first document matching a JSON field comparison query (->> =) ordered by the given fields in the /// document; returns null if not found - let FirstByFieldsOrdered<'TDoc when 'TDoc: null>(tableName, howMatched, queryFields, orderFields) = + let FirstByFieldsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>( + tableName, howMatched, queryFields, orderFields) = WithProps.Find.FirstByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, fromDataSource ()) /// Retrieve the first document matching a JSON containment query (@>); returns None if not found @@ -993,7 +1001,7 @@ module Find = WithProps.Find.firstByContains<'TDoc> tableName criteria (fromDataSource ()) /// Retrieve the first document matching a JSON containment query (@>); returns null if not found - let FirstByContains<'TDoc when 'TDoc: null>(tableName, criteria: obj) = + let FirstByContains<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, criteria: obj) = WithProps.Find.FirstByContains<'TDoc>(tableName, criteria, fromDataSource ()) /// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the document; @@ -1004,7 +1012,7 @@ module Find = /// Retrieve the first document matching a JSON containment query (@>) ordered by the given fields in the document; /// returns null if not found - let FirstByContainsOrdered<'TDoc when 'TDoc: null>(tableName, criteria: obj, orderFields) = + let FirstByContainsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, criteria: obj, orderFields) = WithProps.Find.FirstByContainsOrdered<'TDoc>(tableName, criteria, orderFields, fromDataSource ()) /// Retrieve the first document matching a JSON Path match query (@?); returns None if not found @@ -1013,7 +1021,7 @@ module Find = WithProps.Find.firstByJsonPath<'TDoc> tableName jsonPath (fromDataSource ()) /// Retrieve the first document matching a JSON Path match query (@?); returns null if not found - let FirstByJsonPath<'TDoc when 'TDoc: null>(tableName, jsonPath) = + let FirstByJsonPath<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, jsonPath) = WithProps.Find.FirstByJsonPath<'TDoc>(tableName, jsonPath, fromDataSource ()) /// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the document; @@ -1024,7 +1032,7 @@ module Find = /// Retrieve the first document matching a JSON Path match query (@?) ordered by the given fields in the document; /// returns null if not found - let FirstByJsonPathOrdered<'TDoc when 'TDoc: null>(tableName, jsonPath, orderFields) = + let FirstByJsonPathOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, jsonPath, orderFields) = WithProps.Find.FirstByJsonPathOrdered<'TDoc>(tableName, jsonPath, orderFields, fromDataSource ()) diff --git a/src/Postgres/README.md b/src/Postgres/README.md index baa1161..ff442c9 100644 --- a/src/Postgres/README.md +++ b/src/Postgres/README.md @@ -5,11 +5,16 @@ This package provides a lightweight document library backed by [PostgreSQL](http ## Features - Select, insert, update, save (upsert), delete, count, and check existence of documents, and create tables and indexes for these documents +- Automatically generate IDs for documents (numeric IDs, GUIDs, or random strings) - Address documents via ID, via comparison on any field, via equality on any property (using JSON containment, on a likely indexed field), or via condition on any property (using JSON Path queries) - Access documents as your domain models (POCOs) - Use `Task`-based async for all data access functions - Use building blocks for more complex queries +## Upgrading from v3 + +There is a breaking API change for `ByField` (C#) / `byField` (F#), along with a compatibility namespace that can mitigate the impact of these changes. See [the migration guide](https://bitbadger.solutions/open-source/relational-documents/upgrade-from-v3-to-v4.html) for full details. + ## Getting Started Once the package is installed, the library needs a data source. Construct an `NpgsqlDataSource` instance, and provide it to the library: diff --git a/src/Sqlite/BitBadger.Documents.Sqlite.fsproj b/src/Sqlite/BitBadger.Documents.Sqlite.fsproj index f51a328..5865390 100644 --- a/src/Sqlite/BitBadger.Documents.Sqlite.fsproj +++ b/src/Sqlite/BitBadger.Documents.Sqlite.fsproj @@ -14,8 +14,8 @@ - - + + diff --git a/src/Sqlite/Compat.fs b/src/Sqlite/Compat.fs index 7288470..7f982e4 100644 --- a/src/Sqlite/Compat.fs +++ b/src/Sqlite/Compat.fs @@ -71,7 +71,7 @@ module WithConn = /// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found [] - let FirstByField<'TDoc when 'TDoc: null>(tableName, field, conn) = + let FirstByField<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, field, conn) = WithConn.Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field, conn) [] @@ -144,7 +144,7 @@ module Find = /// Retrieve the first document matching a JSON field comparison (->> =); returns null if not found [] - let FirstByField<'TDoc when 'TDoc: null>(tableName, field) = + let FirstByField<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, field) = Find.FirstByFields<'TDoc>(tableName, Any, Seq.singleton field) @@ -247,7 +247,7 @@ type SqliteConnectionCSharpCompatExtensions = /// Retrieve the first document matching a JSON field comparison query (->> =); returns null if not found [] [] - static member inline FindFirstByField<'TDoc when 'TDoc: null>(conn, tableName, field) = + static member inline FindFirstByField<'TDoc when 'TDoc: null and 'TDoc: not struct>(conn, tableName, field) = WithConn.Find.FirstByFields<'TDoc>(tableName, Any, [ field ], conn) /// Patch documents using a JSON field comparison query in the WHERE clause (->> =) diff --git a/src/Sqlite/Extensions.fs b/src/Sqlite/Extensions.fs index 4a32dec..9f143dc 100644 --- a/src/Sqlite/Extensions.fs +++ b/src/Sqlite/Extensions.fs @@ -1,6 +1,5 @@ namespace BitBadger.Documents.Sqlite -open BitBadger.Documents open Microsoft.Data.Sqlite /// F# extensions for the SqliteConnection type @@ -131,7 +130,7 @@ type SqliteConnectionCSharpExtensions = /// Execute a query that returns one or no results [] - static member inline CustomSingle<'TDoc when 'TDoc: null>( + static member inline CustomSingle<'TDoc when 'TDoc: null and 'TDoc: not struct>( conn, query, parameters, mapFunc: System.Func) = WithConn.Custom.Single<'TDoc>(query, parameters, mapFunc, conn) @@ -198,7 +197,7 @@ type SqliteConnectionCSharpExtensions = /// Retrieve a document by its ID [] - static member inline FindById<'TKey, 'TDoc when 'TDoc: null>(conn, tableName, docId: 'TKey) = + static member inline FindById<'TKey, 'TDoc when 'TDoc: null and 'TDoc: not struct>(conn, tableName, docId: 'TKey) = WithConn.Find.ById<'TKey, 'TDoc>(tableName, docId, conn) /// Retrieve documents via a comparison on JSON fields @@ -213,13 +212,14 @@ type SqliteConnectionCSharpExtensions = /// Retrieve documents via a comparison on JSON fields, returning only the first result [] - static member inline FindFirstByFields<'TDoc when 'TDoc: null>(conn, tableName, howMatched, fields) = + static member inline FindFirstByFields<'TDoc when 'TDoc: null and 'TDoc: not struct>( + conn, tableName, howMatched, fields) = WithConn.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, conn) /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning only /// the first result [] - static member inline FindFirstByFieldsOrdered<'TDoc when 'TDoc: null>( + static member inline FindFirstByFieldsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>( conn, tableName, howMatched, queryFields, orderFields) = WithConn.Find.FirstByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn) @@ -262,9 +262,3 @@ type SqliteConnectionCSharpExtensions = [] static member inline DeleteByFields(conn, tableName, howMatched, fields) = WithConn.Delete.byFields tableName howMatched fields conn - - /// Delete documents by matching a comparison on a JSON field - [] - [] - static member inline DeleteByField(conn, tableName, field) = - conn.DeleteByFields(tableName, Any, [ field ]) diff --git a/src/Sqlite/Library.fs b/src/Sqlite/Library.fs index 28e8080..520f465 100644 --- a/src/Sqlite/Library.fs +++ b/src/Sqlite/Library.fs @@ -153,7 +153,7 @@ module Results = /// Create a domain item from a document, specifying the field in which the document is found [] let fromDocument<'TDoc> field (rdr: SqliteDataReader) : 'TDoc = - Configuration.serializer().Deserialize<'TDoc>(rdr.GetString(rdr.GetOrdinal(field))) + Configuration.serializer().Deserialize<'TDoc>(rdr.GetString(rdr.GetOrdinal field)) /// Create a domain item from a document [] @@ -221,7 +221,7 @@ module WithConn = } /// Execute a query that returns one or no results (returns null if not found) - let Single<'TDoc when 'TDoc: null>( + let Single<'TDoc when 'TDoc: null and 'TDoc: not struct>( query, parameters, mapFunc: System.Func, conn ) = backgroundTask { let! result = single<'TDoc> query parameters mapFunc.Invoke conn @@ -358,7 +358,7 @@ module WithConn = Custom.single<'TDoc> (Query.byId (Query.find tableName) docId) [ idParam docId ] fromData<'TDoc> conn /// Retrieve a document by its ID (returns null if not found) - let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId: 'TKey, conn) = + let ById<'TKey, 'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, docId: 'TKey, conn) = Custom.Single<'TDoc>(Query.byId (Query.find tableName) docId, [ idParam docId ], fromData<'TDoc>, conn) /// Retrieve documents via a comparison on JSON fields @@ -405,7 +405,7 @@ module WithConn = conn /// Retrieve documents via a comparison on JSON fields, returning only the first result - let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields, conn) = + let FirstByFields<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, howMatched, fields, conn) = Custom.Single( $"{Query.byFields (Query.find tableName) howMatched fields} LIMIT 1", addFieldParams fields [], @@ -424,7 +424,8 @@ module WithConn = /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning /// only the first result - let FirstByFieldsOrdered<'TDoc when 'TDoc: null>(tableName, howMatched, queryFields, orderFields, conn) = + let FirstByFieldsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>( + tableName, howMatched, queryFields, orderFields, conn) = Custom.Single( $"{Query.byFields (Query.find tableName) howMatched queryFields}{Query.orderBy orderFields SQLite} LIMIT 1", addFieldParams queryFields [], @@ -529,7 +530,8 @@ module Custom = WithConn.Custom.single<'TDoc> query parameters mapFunc conn /// Execute a query that returns one or no results (returns null if not found) - let Single<'TDoc when 'TDoc: null>(query, parameters, mapFunc: System.Func) = + let Single<'TDoc when 'TDoc: null and 'TDoc: not struct>( + query, parameters, mapFunc: System.Func) = use conn = Configuration.dbConn () WithConn.Custom.Single<'TDoc>(query, parameters, mapFunc, conn) @@ -652,7 +654,7 @@ module Find = WithConn.Find.byId<'TKey, 'TDoc> tableName docId conn /// Retrieve a document by its ID (returns null if not found) - let ById<'TKey, 'TDoc when 'TDoc: null>(tableName, docId) = + let ById<'TKey, 'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, docId) = use conn = Configuration.dbConn () WithConn.Find.ById<'TKey, 'TDoc>(tableName, docId, conn) @@ -685,7 +687,7 @@ module Find = WithConn.Find.firstByFields<'TDoc> tableName howMatched fields conn /// Retrieve documents via a comparison on JSON fields, returning only the first result - let FirstByFields<'TDoc when 'TDoc: null>(tableName, howMatched, fields) = + let FirstByFields<'TDoc when 'TDoc: null and 'TDoc: not struct>(tableName, howMatched, fields) = use conn = Configuration.dbConn () WithConn.Find.FirstByFields<'TDoc>(tableName, howMatched, fields, conn) @@ -698,7 +700,8 @@ module Find = /// Retrieve documents via a comparison on JSON fields ordered by the given fields in the document, returning only /// the first result - let FirstByFieldsOrdered<'TDoc when 'TDoc: null>(tableName, howMatched, queryFields, orderFields) = + let FirstByFieldsOrdered<'TDoc when 'TDoc: null and 'TDoc: not struct>( + tableName, howMatched, queryFields, orderFields) = use conn = Configuration.dbConn () WithConn.Find.FirstByFieldsOrdered<'TDoc>(tableName, howMatched, queryFields, orderFields, conn) diff --git a/src/Sqlite/README.md b/src/Sqlite/README.md index 6d069d3..fdd8a46 100644 --- a/src/Sqlite/README.md +++ b/src/Sqlite/README.md @@ -5,11 +5,16 @@ This package provides a lightweight document library backed by [SQLite](https:// ## Features - Select, insert, update, save (upsert), delete, count, and check existence of documents, and create tables and indexes for these documents +- Automatically generate IDs for documents (numeric IDs, GUIDs, or random strings) - Address documents via ID or via comparison on any field - Access documents as your domain models (POCOs) - Use `Task`-based async for all data access functions - Use building blocks for more complex queries +## Upgrading from v3 + +There is a breaking API change for `ByField` (C#) / `byField` (F#), along with a compatibility namespace that can mitigate the impact of these changes. See [the migration guide](https://bitbadger.solutions/open-source/relational-documents/upgrade-from-v3-to-v4.html) for full details. + ## Getting Started Once the package is installed, the library needs a connection string. Once it has been obtained / constructed, provide it to the library: @@ -72,28 +77,28 @@ Count customers in Atlanta: ```csharp // C#; parameters are table name, field, operator, and value -// Count.ByField type signature is Func> -var customerCount = await Count.ByField("customer", Field.Equal("City", "Atlanta")); +// Count.ByFields type signature is Func, Task> +var customerCount = await Count.ByFields("customer", FieldMatch.Any, [Field.Equal("City", "Atlanta")]); ``` ```fsharp // F# -// Count.byField type signature is string -> Field -> Task -let! customerCount = Count.byField "customer" (Field.Equal "City" "Atlanta") +// Count.byFields type signature is string -> FieldMatch -> Field seq -> Task +let! customerCount = Count.byFields "customer" Any [ Field.Equal "City" "Atlanta" ] ``` Delete customers in Chicago: _(no offense, Second City; just an example...)_ ```csharp // C#; parameters are same as above, except return is void -// Delete.ByField type signature is Func -await Delete.ByField("customer", Field.Equal("City", "Chicago")); +// Delete.ByFields type signature is Func, Task> +await Delete.ByFields("customer", FieldMatch.Any, [Field.Equal("City", "Chicago")]); ``` ```fsharp // F# -// Delete.byField type signature is string -> string -> Op -> obj -> Task -do! Delete.byField "customer" (Field.Equal "City" "Chicago") +// Delete.byFields type signature is string -> FieldMatch -> Field seq -> Task +do! Delete.byFields "customer" Any [ Field.Equal "City" "Chicago" ] ``` ## More Information diff --git a/src/Tests/BitBadger.Documents.Tests.fsproj b/src/Tests/BitBadger.Documents.Tests.fsproj index a362ea2..77929cc 100644 --- a/src/Tests/BitBadger.Documents.Tests.fsproj +++ b/src/Tests/BitBadger.Documents.Tests.fsproj @@ -17,7 +17,7 @@ - + diff --git a/src/test_all.sh b/src/test_all.sh index 65a2592..2149d3f 100755 --- a/src/test_all.sh +++ b/src/test_all.sh @@ -7,8 +7,8 @@ dotnet build BitBadger.Documents.sln --no-restore cd ./Tests || exit export BBDOX_PG_PORT=8301 -PG_VERSIONS=('12' '13' '14' '15' 'latest') -NET_VERSIONS=('6.0' '8.0') +PG_VERSIONS=('13' '14' '15' '16' 'latest') +NET_VERSIONS=('8.0' '9.0') for PG_VERSION in "${PG_VERSIONS[@]}" do