From 697d15ec69162b5cd1927aef65edd604dc492612 Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Tue, 30 Sep 2025 23:38:04 +0300 Subject: [PATCH 01/17] refactor root files --- .editorconfig | 14 +++- .gitattributes | 147 ++++++++++++++++++++++----------------- .gitignore | 44 ++++-------- Directory.Build.props | 74 ++++++++++---------- Directory.Packages.props | 26 +++++++ GitVersion.yaml | 8 +++ LICENSE.md | 2 +- NoSQLite.sln | 76 -------------------- NoSQLite.slnx | 11 +++ global.json | 5 +- version.json | 19 ----- 11 files changed, 193 insertions(+), 233 deletions(-) create mode 100644 Directory.Packages.props create mode 100644 GitVersion.yaml delete mode 100644 NoSQLite.sln create mode 100644 NoSQLite.slnx delete mode 100644 version.json diff --git a/.editorconfig b/.editorconfig index 59e13c5..b107b2a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -24,5 +24,17 @@ indent_size = 2 [*.json] indent_size = 2 +# C# settings [*.cs] -csharp_style_namespace_declarations = file_scoped:warning +csharp_style_namespace_declarations = file_scoped:error +csharp_new_line_before_open_brace = types,methods,properties,anonymous_methods,control_blocks,anonymous_types,object_collection_array_initializers,lambdas + +# Verify settings +[*.{received,verified}.{json,txt,xml}] +charset = "utf-8-bom" +end_of_line = lf +indent_size = unset +indent_style = unset +insert_final_newline = false +tab_width = unset +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.gitattributes b/.gitattributes index b1060b7..ec10f3b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,66 +1,3 @@ -# Auto detect text files and perform LF normalization -* text=auto - -*.cs text diff=csharp -*.cshtml text diff=html -*.csx text diff=csharp -*.sln text eol=crlf -*.csproj text eol=crlf - -############################################################################### -# Set default behavior to automatically normalize line endings. -############################################################################### -* text=auto - -############################################################################### -# Set the merge driver for project and solution files -# -# Merging from the command prompt will add diff markers to the files if there -# are conflicts (Merging from VS is not affected by the settings below, in VS -# the diff markers are never inserted). Diff markers may cause the following -# file extensions to fail to load in VS. An alternative would be to treat -# these files as binary and thus will always conflict and require user -# intervention with every merge. To do so, just comment the entries below and -# uncomment the group further below -############################################################################### - -*.sln text eol=crlf -*.csproj text eol=crlf -*.vbproj text eol=crlf -*.vcxproj text eol=crlf -*.vcproj text eol=crlf -*.dbproj text eol=crlf -*.fsproj text eol=crlf -*.lsproj text eol=crlf -*.wixproj text eol=crlf -*.modelproj text eol=crlf -*.sqlproj text eol=crlf -*.wwaproj text eol=crlf - -*.xproj text eol=crlf -*.props text eol=crlf -*.filters text eol=crlf -*.vcxitems text eol=crlf - - -#*.sln merge=binary -#*.csproj merge=binary -#*.vbproj merge=binary -#*.vcxproj merge=binary -#*.vcproj merge=binary -#*.dbproj merge=binary -#*.fsproj merge=binary -#*.lsproj merge=binary -#*.wixproj merge=binary -#*.modelproj merge=binary -#*.sqlproj merge=binary -#*.wwaproj merge=binary - -#*.xproj merge=binary -#*.props merge=binary -#*.filters merge=binary -#*.vcxitems merge=binary - # Common settings that generally should always be used with your language specific settings # Auto detect text files and perform LF normalization @@ -144,3 +81,87 @@ .gitattributes export-ignore .gitignore export-ignore .gitkeep export-ignore + +# Auto detect text files and perform LF normalization +* text=auto + +*.cs text diff=csharp +*.cshtml text diff=html +*.csx text diff=csharp +*.sln text eol=crlf +*.csproj text eol=crlf + +# Apply override to all files in the directory +*.md linguist-detectable + +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just comment the entries below and +# uncomment the group further below +############################################################################### + +*.sln text eol=crlf +*.csproj text eol=crlf +*.vbproj text eol=crlf +*.vcxproj text eol=crlf +*.vcproj text eol=crlf +*.dbproj text eol=crlf +*.fsproj text eol=crlf +*.lsproj text eol=crlf +*.wixproj text eol=crlf +*.modelproj text eol=crlf +*.sqlproj text eol=crlf +*.wwaproj text eol=crlf + +*.xproj text eol=crlf +*.props text eol=crlf +*.filters text eol=crlf +*.vcxitems text eol=crlf + + +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +#*.xproj merge=binary +#*.props merge=binary +#*.filters merge=binary +#*.vcxitems merge=binary + +# Basic .gitattributes for a PowerShell repo. + +# Source files +# ============ +*.ps1 text eol=crlf +*.ps1x text eol=crlf +*.psm1 text eol=crlf +*.psd1 text eol=crlf +*.ps1xml text eol=crlf +*.pssc text eol=crlf +*.psrc text eol=crlf +*.cdxml text eol=crlf + +# Verify +*.verified.txt text eol=lf working-tree-encoding=UTF-8 +*.verified.xml text eol=lf working-tree-encoding=UTF-8 +*.verified.json text eol=lf working-tree-encoding=UTF-8 \ No newline at end of file diff --git a/.gitignore b/.gitignore index d79dc33..2093343 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## -## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.rsuser @@ -49,6 +49,10 @@ Generated\ Files/ TestResult.xml nunit-*.xml +# Verify +*.received.* +*.received/ + # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ @@ -206,6 +210,9 @@ PublishScripts/ *.nuget.props *.nuget.targets +# Nuget personal access tokens and Credentials +# nuget.config + # Microsoft Azure Build Output csx/ *.build.csdef @@ -294,17 +301,6 @@ node_modules/ # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw -# Visual Studio 6 auto-generated project file (contains which files were open etc.) -*.vbp - -# Visual Studio 6 workspace and project file (working project files containing files to include in project) -*.dsw -*.dsp - -# Visual Studio 6 technical files -*.ncb -*.aps - # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts @@ -361,9 +357,6 @@ ASALocalRun/ # Local History for Visual Studio .localhistory/ -# Visual Studio History (VSHistory) files -.vshistory/ - # BeatPulse healthcheck temp database healthchecksdb @@ -395,6 +388,7 @@ FodyWeavers.xsd *.msp # JetBrains Rider +.idea/ *.sln.iml .vscode/* @@ -402,24 +396,10 @@ FodyWeavers.xsd !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json -!.vscode/*.code-snippets +*.code-workspace # Local History for Visual Studio Code .history/ -# Built Visual Studio Code Extensions -*.vsix - -# Igonre db related files eg: sqlite. -*.db -*.db-shm -*.db-wal -*.db3 -*.db3-shm -*.db3-wal -*.sqlite -*.sqlite-shm -*.sqlite-wal -*.sqlite3 -*.sqlite3-shm -*.sqlite3-wal +# Custom +*.min.* \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index 30bb69b..794ecc0 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,55 +1,55 @@ + - 11 + latest enable true + true Debug - IDE0130;CA1416 + $(NoWarn);CS8509;IDE0039;IDE0130;IDE0290;IDE0060;RZ10012;IDE0052;BL0007;NU5128 + $(WarningsAsErrors);RZ2012; - - $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), 'Directory.Build.props')) - $(SourceDir)\artifacts - $(ArtifactsDir)\$(MSBuildProjectName)\$(Configuration) + $([System.IO.Path]::Combine( + $(MSBuildThisFileDirectory), + "artifacts", + $([MSBuild]::MakeRelative($(MSBuildThisFileDirectory),$(MSBuildProjectDirectory)).Replace("$(MSBuildProjectName)", '').TrimEnd('/')) + )) + true + true + false + false + - $(Artifacts)\bin - $(Artifacts)\obj - $(SourceDir)nuget - - $(BaseOutputPath) + + + Panagiotis Athanasiou + Copyright (c) $([System.DateTimeOffset]::UtcNow.ToString("yyyy")) $(Authors) + https://github.com/panoukos41/NoSQLite + git + MIT + A thin wrapper above sqlite to use it as a nosql database. + sqlite + - Panos Athanasiou - Copyright (c) 2022 Panos Athanasiou - MIT - https://github.com/panoukos41/NoSQLite - - A thin wrapper above sqlite to use it as a nosql database. - $(DefaultPackageDescription) - panoukos41 - sqlite - $(PackageProjectUrl) + $(RepositoryUrl) + $(RepositoryLicense) + $(RepositoryDescription) + $(RepositoryTags) $(RepositoryUrl)/releases - git + README.md - true - - true - - - - $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + true + true + snupkg - - - + + + + - - - - - diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..36fb8fd --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,26 @@ + + + + true + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GitVersion.yaml b/GitVersion.yaml new file mode 100644 index 0000000..6e3b19e --- /dev/null +++ b/GitVersion.yaml @@ -0,0 +1,8 @@ +workflow: GitHubFlow/v1 +branches: + main: + label: beta + increment: Patch + feature: + label: alpha + increment: Minor diff --git a/LICENSE.md b/LICENSE.md index 0a60099..d5364a5 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,7 +1,7 @@ MIT License -Copyright (c) 2021 Panos Athanasiou +Copyright (c) 2025 Panagiotis Athanasiou Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/NoSQLite.sln b/NoSQLite.sln deleted file mode 100644 index d390a33..0000000 --- a/NoSQLite.sln +++ /dev/null @@ -1,76 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.4.32912.340 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1CB9024C-F693-466A-A960-806B871E6DFD}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "root", "root", "{4CB6779A-2488-4EF5-8FB3-1EB90B63A311}" - ProjectSection(SolutionItems) = preProject - .gitattributes = .gitattributes - .gitignore = .gitignore - Directory.Build.props = Directory.Build.props - global.json = global.json - LICENSE.md = LICENSE.md - README.md = README.md - version.json = version.json - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NoSQLite", "src\NoSQLite\NoSQLite.csproj", "{4CB52AF4-51A8-45D9-8BFD-F921189063A0}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{C3F14F03-2367-47B9-BF89-4D028C29074A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp", "samples\ConsoleApp\ConsoleApp.csproj", "{053B5452-DBE9-421A-80AC-E415F11CBAEC}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ci", "ci", "{7C0251EF-9751-4494-ADF5-B6E226FFEFBC}" - ProjectSection(SolutionItems) = preProject - ci\build.ps1 = ci\build.ps1 - ci\pack.ps1 = ci\pack.ps1 - ci\test.ps1 = ci\test.ps1 - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{0743F8BC-C025-42F2-943C-08BB38399EDF}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{A450CC69-06DB-4506-B76A-83BF556D959F}" - ProjectSection(SolutionItems) = preProject - .github\workflows\release.yaml = .github\workflows\release.yaml - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{581BD499-7F2B-4255-B8E6-95F442B9C187}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NoSQLite.Test", "test\NoSQLite.Test\NoSQLite.Test.csproj", "{DD8B54C8-499F-4A46-BBDD-8F641940BF85}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {4CB52AF4-51A8-45D9-8BFD-F921189063A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4CB52AF4-51A8-45D9-8BFD-F921189063A0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4CB52AF4-51A8-45D9-8BFD-F921189063A0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4CB52AF4-51A8-45D9-8BFD-F921189063A0}.Release|Any CPU.Build.0 = Release|Any CPU - {053B5452-DBE9-421A-80AC-E415F11CBAEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {053B5452-DBE9-421A-80AC-E415F11CBAEC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {053B5452-DBE9-421A-80AC-E415F11CBAEC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {053B5452-DBE9-421A-80AC-E415F11CBAEC}.Release|Any CPU.Build.0 = Release|Any CPU - {DD8B54C8-499F-4A46-BBDD-8F641940BF85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DD8B54C8-499F-4A46-BBDD-8F641940BF85}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DD8B54C8-499F-4A46-BBDD-8F641940BF85}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DD8B54C8-499F-4A46-BBDD-8F641940BF85}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {4CB52AF4-51A8-45D9-8BFD-F921189063A0} = {1CB9024C-F693-466A-A960-806B871E6DFD} - {053B5452-DBE9-421A-80AC-E415F11CBAEC} = {C3F14F03-2367-47B9-BF89-4D028C29074A} - {7C0251EF-9751-4494-ADF5-B6E226FFEFBC} = {4CB6779A-2488-4EF5-8FB3-1EB90B63A311} - {0743F8BC-C025-42F2-943C-08BB38399EDF} = {4CB6779A-2488-4EF5-8FB3-1EB90B63A311} - {A450CC69-06DB-4506-B76A-83BF556D959F} = {0743F8BC-C025-42F2-943C-08BB38399EDF} - {DD8B54C8-499F-4A46-BBDD-8F641940BF85} = {581BD499-7F2B-4255-B8E6-95F442B9C187} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {600A3307-F265-4E32-B09A-73C7FC05B2D1} - EndGlobalSection -EndGlobal diff --git a/NoSQLite.slnx b/NoSQLite.slnx new file mode 100644 index 0000000..a3f9209 --- /dev/null +++ b/NoSQLite.slnx @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/global.json b/global.json index 4925416..93dd0dd 100644 --- a/global.json +++ b/global.json @@ -1,9 +1,6 @@ { - "msbuild-sdks": { - "MSBuild.Sdk.Extras": "3.0.44" - }, "sdk": { - "version": "7.0.0", + "version": "10.0.0", "rollForward": "latestMinor", "allowPrerelease": true } diff --git a/version.json b/version.json deleted file mode 100644 index 0d07bfd..0000000 --- a/version.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.3-beta.{height}", - "pathFilters": [ - ":/src" - ], - "publicReleaseRefSpec": [ - "^refs/heads/main$" - ], - "nugetPackageVersion": { - "semVer": 2 - }, - "cloudBuild": { - "setVersionVariables": true, - "buildNumber": { - "enabled": false - } - } -} \ No newline at end of file From ef5aa7175d9df64f9b499c119c043a618148d507 Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Tue, 30 Sep 2025 23:38:55 +0300 Subject: [PATCH 02/17] Refactor NoSQLite: Multi-targeting and feature updates Updated NoSQLite to support .NET 8.0, 9.0, and 10.0. Replaced `SQLitePCLRaw.bundle_e_sqlite3` with `SQLitePCLRaw.core`. Enhanced thread safety by switching `tables` to `ConcurrentDictionary`. Added new table management methods (`TableExists`, `DropTable`, `DropAndCreateTable`, `Clear`) and improved resource disposal patterns. Introduced JSON deserialization methods (`ColumnDeserializeDocument`, `ColumnDeserializeElement`) and improved index management with pattern matching. Refined method parameters, improved error handling, and cleaned up codebase with global usings and modern C# syntax. Fixed SQL syntax issues and updated method names for consistency. --- src/NoSQLite/NoSQLite.csproj | 7 +- src/NoSQLite/NoSQLiteConnection.cs | 135 ++++++++++++++++++++--------- src/NoSQLite/NoSQLiteTable.cs | 40 +++++---- src/NoSQLite/SQLiteStmt.cs | 66 ++++++++++---- src/NoSQLite/SQLiteTransaction.cs | 6 +- src/NoSQLite/Utilities.cs | 17 ++-- src/NoSQLite/_usings.cs | 2 + 7 files changed, 182 insertions(+), 91 deletions(-) create mode 100644 src/NoSQLite/_usings.cs diff --git a/src/NoSQLite/NoSQLite.csproj b/src/NoSQLite/NoSQLite.csproj index 528bc28..e61c852 100644 --- a/src/NoSQLite/NoSQLite.csproj +++ b/src/NoSQLite/NoSQLite.csproj @@ -1,13 +1,12 @@  - net6.0 - P41.NoSQLite - NoSQLite + net8.0;net9.0;net10.0 + True - + diff --git a/src/NoSQLite/NoSQLiteConnection.cs b/src/NoSQLite/NoSQLiteConnection.cs index c85912d..a2d458d 100644 --- a/src/NoSQLite/NoSQLiteConnection.cs +++ b/src/NoSQLite/NoSQLiteConnection.cs @@ -1,57 +1,73 @@ -using SQLitePCL; -using System.Buffers; +using System.Buffers; +using System.Collections.Concurrent; using System.Text.Json; namespace NoSQLite; -using static SQLitePCL.raw; - /// -/// +/// Represents a connection to a NoSQLite database, providing methods to manage tables and documents. /// [Preserve(AllMembers = true)] public sealed class NoSQLiteConnection : IDisposable { - static NoSQLiteConnection() - { - Batteries.Init(); - } + private readonly bool disposeDb; + + /// + /// The collection of tables managed by this connection. + /// + internal readonly ConcurrentDictionary tables; - internal readonly Dictionary tables; + /// + /// The underlying SQLite database handle. + /// internal readonly sqlite3 db; + + /// + /// Indicates whether the connection is open. + /// internal bool open = true; /// - /// Initialize a new instance of class. + /// Initializes a new instance of the class. /// - /// The path pointing to the database file or the path it will create the file at. - /// The options that will be used to serialize/deserialize the json objects. - public NoSQLiteConnection(string databasePath, JsonSerializerOptions? jsonOptions = null) + /// The path to the database file, or the path where the file will be created. + /// The options used to serialize/deserialize JSON objects, or null for defaults. + /// Thrown if is null. + public NoSQLiteConnection(string databasePath, JsonSerializerOptions? jsonOptions = null, bool wal = true) { ArgumentNullException.ThrowIfNull(databasePath, nameof(databasePath)); + disposeDb = true; var result = sqlite3_open(databasePath, out db); db.CheckResult(result, $"Could not open or create database file: {Path}"); - SetJournalMode(); + if (wal) + { + SetJournalMode(); + } Version = sqlite3_libversion().utf8_to_string(); Name = System.IO.Path.GetFileName(databasePath); Path = databasePath; JsonOptions = jsonOptions; - tables = new(); + tables = []; } - private void SetJournalMode() => + /// + /// Sets the SQLite journal mode to Write-Ahead Logging (WAL). + /// + public void SetJournalMode() + { sqlite3_exec(db, "PRAGMA journal_mode=WAL;"); + } /// - /// All the tables that belong to this Connection. + /// Gets or sets the JSON serializer options used to serialize/deserialize the documents. /// - public IReadOnlyCollection Tables => tables.Values; + public JsonSerializerOptions? JsonOptions { get; set; } /// - /// The database name. + /// Gets the database name. /// public string Name { get; } @@ -66,29 +82,47 @@ private void SetJournalMode() => public string Version { get; } /// - /// Gets or Sets the JSON serializer options used to serialzie/deserialize the documents. + /// Gets all the tables that belong to this connection. /// - public JsonSerializerOptions? JsonOptions { get; set; } + public IReadOnlyCollection Tables => [.. tables.Values]; /// - /// todo: Summary + /// Gets a table with the specified name, or the default "documents" table if is null. + /// If the table does not exist in the connection's cache, a new is created. /// - /// The name of the table to create/use or null to use the default documents table. - /// + /// The name of the table to create or use, or null to use the default "documents" table. + /// The instance for the specified table. public NoSQLiteTable GetTable(string? table = null) { const string docs = "documents"; - return tables.TryGetValue(table ?? docs, out var t) ? t : new(table ?? docs, this); + return tables.GetOrAdd(table ?? docs, static (table, connection) => new(table, connection), this); + } + + /// + /// Checks whether a table with the specified name exists in the database. + /// + /// The name of the table to check. + /// true if the table exists; otherwise, false. + public bool TableExists(string table) + { + using var stmt = new SQLiteStmt(db, $""" + SELECT count(*) FROM "sqlite_master" + WHERE type='table' AND name='{table}'; + """); + + stmt.Step(); + return stmt.ColumnInt(0) != 0; } /// - /// Create a document table if it does not exist with the specified name. + /// Creates a document table with the specified name if it does not already exist. /// - /// + /// The name of the table to create. + /// Thrown if the table cannot be created. public void CreateTable(string table) { var result = sqlite3_exec(db, $""" - CREATE TABLE IF NOT EXISTS '{table}' ( + CREATE TABLE IF NOT EXISTS "{table}" ( "id" TEXT NOT NULL UNIQUE, "json" TEXT NOT NULL, PRIMARY KEY("id") @@ -99,25 +133,41 @@ PRIMARY KEY("id") } /// - /// Deletes all rows from a table. + /// Drops a table with the specified name if it exists. This will delete all indexes, views, etc. /// - public void Clear(string table) + /// The name of the table to drop. + /// Thrown if the table cannot be dropped. + public void DropTable(string table) { - sqlite3_exec(db, $"""DELETE FROM "{table}";"""); + var result = sqlite3_exec(db, $""" + DROP TABLE IF EXISTS "{table}"; + """); + + db.CheckResult(result, $"Could not drop '{table}' database table"); } /// - /// Drops the table and then create it again. This will delete all indexes views etc. + /// Drops the table and then creates it again. This will delete all indexes, views, etc. /// + /// The name of the table to drop and recreate. /// See for more info. - public void DropAndCreate(string table) + public void DropAndCreateTable(string table) { - sqlite3_exec(db, $"""DROP TABLE IF EXISTS "{table}";"""); + DropTable(table); CreateTable(table); } /// - /// Execute wal_checkpoint. + /// Deletes all rows from a table. + /// + /// The name of the table to clear. + public void Clear(string table) + { + sqlite3_exec(db, $"""DELETE FROM "{table}";"""); + } + + /// + /// Executes a write-ahead log (WAL) checkpoint for the database. /// /// See for more info. public void Checkpoint() @@ -125,9 +175,11 @@ public void Checkpoint() sqlite3_wal_checkpoint(db, Name); } - /// + /// + /// Releases all resources used by the and closes the underlying database connection. + /// /// - /// This will close and dispose the underlying database connection. + /// This will close and dispose the underlying database connection and all associated tables. /// public void Dispose() { @@ -143,7 +195,10 @@ public void Dispose() ArrayPool.Shared.Return(buffer, true); - sqlite3_close_v2(db); - db.Dispose(); + if (disposeDb) + { + sqlite3_close_v2(db); + db.Dispose(); + } } } diff --git a/src/NoSQLite/NoSQLiteTable.cs b/src/NoSQLite/NoSQLiteTable.cs index d1b5f61..5f5edf4 100644 --- a/src/NoSQLite/NoSQLiteTable.cs +++ b/src/NoSQLite/NoSQLiteTable.cs @@ -1,5 +1,4 @@ -using SQLitePCL; -using System.Text.Json; +using System.Text.Json; namespace NoSQLite; @@ -25,7 +24,6 @@ internal NoSQLiteTable(string table, NoSQLiteConnection connection) db = connection.db; connection.CreateTable(table); - connection.tables.Add(table, this); #region Lazy statment initialization @@ -92,6 +90,7 @@ DELETE FROM "{Table}" disposables.Add(stmt); return stmt; }); + #endregion } @@ -107,7 +106,7 @@ DELETE FROM "{Table}" public string Table { get; } /// - /// Gets or Sets the JSON serializer options used to serialzie/deserialize the documents. + /// Gets or Sets the JSON serializer options used to serialize/deserialize the documents. /// public JsonSerializerOptions? JsonOptions { get; set; } @@ -120,12 +119,16 @@ DELETE FROM "{Table}" /// Drops the table and then Creates it again. This will delete all indexes views etc. /// /// See for more info. - public void DropAndCreate() => Connection.DropAndCreate(Table); + public void DropAndCreate() => Connection.DropAndCreateTable(Table); #region Basic private readonly Lazy countStmt; + /// + /// Gets the number of rows in the table. + /// + /// The number of rows in the table as an . public int Count() { var stmt = countStmt.Value; @@ -139,6 +142,10 @@ public int Count() } } + /// + /// Gets the number of rows in the table as a . + /// + /// The number of rows in the table as a . public long LongCount() { var stmt = countStmt.Value; @@ -155,7 +162,7 @@ public long LongCount() private readonly Lazy existsStmt; /// - /// Check wheter a document exists or not. + /// Check whether a document exists or not. /// /// The id to search for. /// True when the id exists otherwise false. @@ -180,7 +187,7 @@ public IEnumerable All() var stmt = allStmt.Value; lock (allStmt) { - start: + start: var result = stmt.Step(); if (result is not SQLITE_ROW) @@ -200,7 +207,7 @@ public IEnumerable AllBytes() var stmt = allStmt.Value; lock (allStmt) { - start: + start: var result = stmt.Step(); if (result is not SQLITE_ROW) @@ -426,7 +433,7 @@ public void Remove(string id) /// Deletes the specified ids from the database. /// /// The ids to delete. - public void RemoveMany(IEnumerable ids) + public void RemoveMany(params IEnumerable ids) { using var transaction = new SQLiteTransaction(db); foreach (var id in ids) @@ -455,7 +462,7 @@ SELECT count(*) FROM sqlite_master stmt.Step(); var value = stmt.ColumnInt(0); - return value != 0; + return value is not 0; } /// @@ -472,10 +479,10 @@ public bool CreateIndex(string indexName, string parameter) { string sql = $""" CREATE INDEX "{Table}_{indexName}" - ON "{Table}"(json_extract("json", '$.{parameter}')); + ON "{Table}" (json_extract(json, '$.{parameter}')); """; - return sqlite3_exec(db, sql) == SQLITE_OK; + return sqlite3_exec(db, sql) is SQLITE_OK; } /// @@ -501,7 +508,7 @@ public bool DeleteIndex(string indexName) DROP INDEX "{Table}_{indexName}" """; - return sqlite3_exec(db, sql) == SQLITE_OK; + return sqlite3_exec(db, sql) is SQLITE_OK; } #endregion @@ -521,10 +528,13 @@ DROP INDEX "{Table}_{indexName}" /// public void Dispose() { - Connection.tables.Remove(Table); + Connection.tables.Remove(Table, out _); if (disposables.Count <= 0) return; - foreach (var d in disposables) d.Dispose(); + var toDispose = new IDisposable[disposables.Count]; + disposables.CopyTo(toDispose); disposables.Clear(); + + foreach (var d in toDispose) d.Dispose(); } } diff --git a/src/NoSQLite/SQLiteStmt.cs b/src/NoSQLite/SQLiteStmt.cs index 97c6b9f..f4d6710 100644 --- a/src/NoSQLite/SQLiteStmt.cs +++ b/src/NoSQLite/SQLiteStmt.cs @@ -1,10 +1,7 @@ -using SQLitePCL; -using System.Text.Json; +using System.Text.Json; namespace NoSQLite; -using static SQLitePCL.raw; - internal sealed class SQLiteStmt : IDisposable { private readonly sqlite3 db; @@ -24,7 +21,7 @@ public SQLiteStmt(sqlite3 db, string sql) #region Bind /// - /// Bind text to a paramter. + /// Bind text to a parameter. /// /// Index starts from 1. /// The parameter index to bind to, starting from 1. @@ -32,7 +29,7 @@ public SQLiteStmt(sqlite3 db, string sql) public void BindText(int index, string value) => sqlite3_bind_text(stmt, index, value); /// - /// Bind text to a paramter. + /// Bind text to a parameter. /// /// Index starts from 1. /// The parameter index to bind to, starting from 1. @@ -68,7 +65,7 @@ public SQLiteStmt(sqlite3 db, string sql) public string ColumnText(int index) => sqlite3_column_text(stmt, index).utf8_to_string(); /// - /// Get the value of a text/blobl column as a series of . + /// Get the value of a text/blob column as a series of . /// /// Index starts from 0. /// The column to read the value from, starting from 0. @@ -76,15 +73,16 @@ public SQLiteStmt(sqlite3 db, string sql) public ReadOnlySpan ColumnBlob(int index) => sqlite3_column_blob(stmt, index); /// - /// Get the value of a text column deserialized to . + /// Get the value of a text/blob column and deserialize it to the specified type . /// - /// Index starts from 0. - /// The type to deserialize from. + /// The type to deserialize the column value to. /// The column to read the value from, starting from 0. - /// The serializer options to use. - /// An instance of or null. - /// - /// + /// The serializer options to use, or null for default options. + /// + /// The column value deserialized as , or null if the value is null. + /// + /// Thrown if the JSON is invalid. + /// Thrown if the type is not supported. public T? ColumnDeserialize(int index, JsonSerializerOptions? jsonOptions = null) { var bytes = ColumnBlob(index); @@ -92,6 +90,42 @@ public SQLiteStmt(sqlite3 db, string sql) return value; } + /// + /// Get the value of a text/blob column as a . + /// + /// Index starts from 0. + /// The column to read the value from, starting from 0. + /// The serializer options to use, or null for default options. + /// + /// The column value deserialized as a , or null if the value is null. + /// + /// Thrown if the JSON is invalid. + /// Thrown if the type is not supported. + public JsonDocument? ColumnDeserializeDocument(int index, JsonSerializerOptions? jsonOptions = null) + { + var bytes = ColumnBlob(index); + var value = JsonSerializer.Deserialize(bytes, jsonOptions); + return value; + } + + /// + /// Get the value of a text/blob column as a . + /// + /// Index starts from 0. + /// The column to read the value from, starting from 0. + /// The serializer options to use, or null for default options. + /// + /// The column value deserialized as a , or null if the value is null. + /// + /// Thrown if the JSON is invalid. + /// Thrown if the type is not supported. + public JsonElement? ColumnDeserializeElement(int index, JsonSerializerOptions? jsonOptions = null) + { + var bytes = ColumnBlob(index); + var value = JsonSerializer.Deserialize(bytes, jsonOptions); + return value; + } + #endregion /// @@ -109,13 +143,13 @@ public SQLiteStmt(sqlite3 db, string sql) public int Reset() => sqlite3_reset(stmt); /// - /// Get the error message the database has currently emmited. + /// Get the error message the database has currently emitted. /// /// The error message from the database. public string Error() => sqlite3_errmsg(db).utf8_to_string(); /// - /// Finilize this statement. + /// Finalize this statement. /// public void Dispose() { diff --git a/src/NoSQLite/SQLiteTransaction.cs b/src/NoSQLite/SQLiteTransaction.cs index b3b7926..7fef055 100644 --- a/src/NoSQLite/SQLiteTransaction.cs +++ b/src/NoSQLite/SQLiteTransaction.cs @@ -1,8 +1,4 @@ -using SQLitePCL; - -namespace NoSQLite; - -using static SQLitePCL.raw; +namespace NoSQLite; internal ref struct SQLiteTransaction { diff --git a/src/NoSQLite/Utilities.cs b/src/NoSQLite/Utilities.cs index 08bc29d..07f5e37 100644 --- a/src/NoSQLite/Utilities.cs +++ b/src/NoSQLite/Utilities.cs @@ -1,5 +1,4 @@ -using SQLitePCL; -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; namespace NoSQLite; @@ -25,7 +24,6 @@ public static void CheckResult(this sqlite3 db, int result, [InterpolatedStringH throw new NoSQLiteException($"{message.ToString()}. SQLite info, code: {result}, message: {sqlite3_errmsg(db).utf8_to_string()}"); } } - } [InterpolatedStringHandler] @@ -34,22 +32,20 @@ internal readonly ref struct SQLiteCodeInterpolation private readonly DefaultInterpolatedStringHandler _innerHandler; /// - /// Inidicates that the code is invalid and an exception - /// should be throwen with the message that has been created. + /// Indicates that the result code is invalid and an exception + /// should be thrown with the message that has been created. /// public bool ShouldThrow { get; } public SQLiteCodeInterpolation( int literalLength, int formattedCount, - int code, + int result, out bool shouldAppend) { // Here we determine which sqlite codes are OK. - // This is used in conjuction with the . - if (code == SQLITE_DONE || - code == SQLITE_OK || - code == SQLITE_ROW) + // This is used in conjunction with the "NoSQLiteException". + if (result is SQLITE_DONE or SQLITE_OK or SQLITE_ROW) { shouldAppend = false; ShouldThrow = false; @@ -70,7 +66,6 @@ public SQLiteCodeInterpolation( public string ToStringAndClear() => _innerHandler.ToStringAndClear(); } - [InterpolatedStringHandler] internal readonly ref struct ConditionInterpolation { diff --git a/src/NoSQLite/_usings.cs b/src/NoSQLite/_usings.cs new file mode 100644 index 0000000..870bd32 --- /dev/null +++ b/src/NoSQLite/_usings.cs @@ -0,0 +1,2 @@ +global using SQLitePCL; +global using static SQLitePCL.raw; From 741d5521d76aca5be4bba5f8a89f2708c723ef3f Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Tue, 30 Sep 2025 23:39:14 +0300 Subject: [PATCH 03/17] Refactor test infrastructure and migrate to TUnit Refactored the test setup by consolidating initialization logic into `_setup.cs`, replacing `ModuleInitializer`, `TestBase`, and `TestFixture` classes. Transitioned from `FluentAssertions` and `xUnit` to `TUnit` for assertions and test framework. Updated all tests to use asynchronous methods and `TUnit` assertions. Renamed data models (`Person`, `Animal`, etc.) to `TestPerson`, `TestAnimal`, etc., and updated references accordingly. Added new test classes (`Operations`, `Queries`, `Transactions`, `Parallelism`) to expand test coverage. Changed the target framework to `.NET 10.0` and streamlined dependencies in `NoSQLite.Test.csproj`. Improved maintainability and consistency across the codebase. --- .../Abstractions/ModuleInitializer.cs | 12 -- test/NoSQLite.Test/Abstractions/TestBase.cs | 25 ---- .../NoSQLite.Test/Abstractions/TestFixture.cs | 37 ------ test/NoSQLite.Test/CRUD.cs | 74 +++++++----- .../Data/{Faction.cs => TestAnimal.cs} | 2 +- .../Data/{Animal.cs => TestFaction.cs} | 2 +- .../Data/{Person.cs => TestPerson.cs} | 4 +- .../Data/{Vehicle.cs => TestVehicle.cs} | 2 +- test/NoSQLite.Test/Indexing.cs | 38 +++---- test/NoSQLite.Test/NoSQLite.Test.csproj | 10 +- test/NoSQLite.Test/Operations.cs | 107 ++++++++++++++++++ test/NoSQLite.Test/Parallelism.cs | 11 ++ test/NoSQLite.Test/Queries.cs | 8 ++ test/NoSQLite.Test/Transactions.cs | 11 ++ test/NoSQLite.Test/_setup.cs | 60 ++++++++++ test/NoSQLite.Test/_usings.cs | 6 +- 16 files changed, 268 insertions(+), 141 deletions(-) delete mode 100644 test/NoSQLite.Test/Abstractions/ModuleInitializer.cs delete mode 100644 test/NoSQLite.Test/Abstractions/TestBase.cs delete mode 100644 test/NoSQLite.Test/Abstractions/TestFixture.cs rename test/NoSQLite.Test/Data/{Faction.cs => TestAnimal.cs} (82%) rename test/NoSQLite.Test/Data/{Animal.cs => TestFaction.cs} (82%) rename test/NoSQLite.Test/Data/{Person.cs => TestPerson.cs} (87%) rename test/NoSQLite.Test/Data/{Vehicle.cs => TestVehicle.cs} (53%) create mode 100644 test/NoSQLite.Test/Operations.cs create mode 100644 test/NoSQLite.Test/Parallelism.cs create mode 100644 test/NoSQLite.Test/Queries.cs create mode 100644 test/NoSQLite.Test/Transactions.cs create mode 100644 test/NoSQLite.Test/_setup.cs diff --git a/test/NoSQLite.Test/Abstractions/ModuleInitializer.cs b/test/NoSQLite.Test/Abstractions/ModuleInitializer.cs deleted file mode 100644 index a7281bf..0000000 --- a/test/NoSQLite.Test/Abstractions/ModuleInitializer.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace NoSQLite.Test.Collections; - -public static class ModuleInitializer -{ - [ModuleInitializer] - public static void Initialize() - { - Bogus.Randomizer.Seed = new Random(420690001); - } -} diff --git a/test/NoSQLite.Test/Abstractions/TestBase.cs b/test/NoSQLite.Test/Abstractions/TestBase.cs deleted file mode 100644 index 5c58085..0000000 --- a/test/NoSQLite.Test/Abstractions/TestBase.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace NoSQLite.Test.Abstractions; - -public abstract class TestBase : IClassFixture> -{ - protected TestFixture Fixture { get; } - - public TestBase(TestFixture fixture) - { - Fixture = fixture; - } - - public NoSQLiteTable GetTable([CallerMemberName] string? caller = null) - { - return Fixture.Connection.GetTable($"{caller}_{Guid.NewGuid()}"); - } - - public NoSQLiteTable GetTable(IDictionary initPairs, [CallerMemberName] string? caller = null) - { - var table = GetTable(caller); - table.InsertMany(initPairs); - return table; - } -} diff --git a/test/NoSQLite.Test/Abstractions/TestFixture.cs b/test/NoSQLite.Test/Abstractions/TestFixture.cs deleted file mode 100644 index d605e8c..0000000 --- a/test/NoSQLite.Test/Abstractions/TestFixture.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace NoSQLite.Test.Abstractions; - -public sealed class TestFixture : IAsyncLifetime -{ - public string Path { get; private set; } = null!; - - public NoSQLiteConnection Connection { get; private set; } = null!; - - public bool Delete { get; set; } = true; - - public Task InitializeAsync() - { - Path = System.IO.Path.Combine(Environment.CurrentDirectory, $"{typeof(TTestClass).Name}.sqlite3"); - - if (File.Exists(Path)) - { - File.Delete(Path); - } - - Connection = new NoSQLiteConnection(Path); - - Assert.True(File.Exists(Connection.Path)); - return Task.CompletedTask; - } - - public Task DisposeAsync() - { - Connection.Dispose(); - Assert.Equal(0, Connection.Tables.Count); - - if (Delete) - { - File.Delete(Path); - } - return Task.CompletedTask; - } -} diff --git a/test/NoSQLite.Test/CRUD.cs b/test/NoSQLite.Test/CRUD.cs index d445023..3752ffb 100644 --- a/test/NoSQLite.Test/CRUD.cs +++ b/test/NoSQLite.Test/CRUD.cs @@ -2,25 +2,25 @@ namespace NoSQLite.Test; -public class CRUD : TestBase +public sealed class CRUD : TestBase { - private readonly Dictionary seed; + private readonly Dictionary seed; - public CRUD(TestFixture fixture) : base(fixture) + public CRUD() { seed = new PersonFaker().Generate(10).ToDictionary(x => x.Email); } - [Fact] - public void Insert() + [Test] + public async Task Insert() { var table = GetTable(); table.Insert("0", seed.First()); } - [Fact] - public void InsertMany() + [Test] + public async Task InsertMany() { var table = GetTable(); var peopleBytes = seed.ToDictionary(x => x.Key, pair => JsonSerializer.SerializeToUtf8Bytes(pair.Value)); @@ -29,76 +29,88 @@ public void InsertMany() table.InsertBytesMany(peopleBytes); } - [Fact] - public void Count() + [Test] + public async Task Count() { var table = GetTable(seed); var count = table.Count(); var longCount = table.LongCount(); - count.Should().Be(seed.Count); - longCount.Should().Be(seed.Count); + await That(count).IsEqualTo(seed.Count); + await That(longCount).IsEqualTo(seed.Count); } - [Fact] - public void All() + [Test] + public async Task All() { var table = GetTable(seed); var people = table.All(); var peopleBytes = table.AllBytes(); - people.Should().BeEquivalentTo(seed.Values); - peopleBytes.Select(p => JsonSerializer.Deserialize(p)).Should().BeEquivalentTo(seed.Values); + await That(people).IsEquivalentTo(seed.Values); + await That(peopleBytes.Select(p => JsonSerializer.Deserialize(p))).IsEquivalentTo(seed.Values); } - [Fact] - public void Exists() + [Test] + public async Task Exists() { var table = GetTable(seed); var person = seed.First(); var exists = table.Exists(person.Key); - exists.Should().BeTrue(); + await That(exists).IsTrue(); } - [Fact] - public void Find() + [Test] + public async Task Find() { var table = GetTable(seed); var pair = seed.First(); var person = pair.Value; - var found = table.Find(pair.Key); + var found = table.Find(pair.Key); var foundBytes = table.FindBytes(pair.Key); - found.Should().Be(person); - JsonSerializer.Deserialize(foundBytes).Should().Be(person); + await That(found).IsEqualTo(person); + await That(JsonSerializer.Deserialize(foundBytes)).IsEqualTo(person); } - [Fact] - public void FindMany() + [Test] + public async Task FindMany() { var table = GetTable(seed); var people = table.FindMany(seed.Keys); var peopleBytes = table.FindBytesMany(seed.Keys); - people.Should().BeEquivalentTo(seed.Values); - peopleBytes.Select(p => JsonSerializer.Deserialize(p)).Should().BeEquivalentTo(seed.Values); + await That(people).IsEquivalentTo(seed.Values); + await That(peopleBytes.Select(p => JsonSerializer.Deserialize(p))).IsEquivalentTo(seed.Values); } - [Fact] - public void FindPairs() + [Test] + public async Task FindPairs() { var table = GetTable(seed); var people = table.FindPairs(seed.Keys); var peopleBytes = table.FindBytesPairs(seed.Keys); - people.Should().BeEquivalentTo(seed); - peopleBytes.ToDictionary(p => p.Key, p => JsonSerializer.Deserialize(p.Value)).Should().BeEquivalentTo(seed); + await That(people).IsEquivalentTo(seed); + await That(peopleBytes.ToDictionary(p => p.Key, p => JsonSerializer.Deserialize(p.Value))).IsEquivalentTo(seed); + } + + [Test] + public async Task Clear() + { + var table = GetTable(seed); + + await That(table.Count()).IsNotEqualTo(0); + + table.Clear(); + + await That(table.Count()).IsNotEqualTo(0); } } diff --git a/test/NoSQLite.Test/Data/Faction.cs b/test/NoSQLite.Test/Data/TestAnimal.cs similarity index 82% rename from test/NoSQLite.Test/Data/Faction.cs rename to test/NoSQLite.Test/Data/TestAnimal.cs index 12eba24..4c0fa55 100644 --- a/test/NoSQLite.Test/Data/Faction.cs +++ b/test/NoSQLite.Test/Data/TestAnimal.cs @@ -6,6 +6,6 @@ namespace NoSQLite.Test.Data; -public sealed record Faction +public sealed record TestAnimal { } diff --git a/test/NoSQLite.Test/Data/Animal.cs b/test/NoSQLite.Test/Data/TestFaction.cs similarity index 82% rename from test/NoSQLite.Test/Data/Animal.cs rename to test/NoSQLite.Test/Data/TestFaction.cs index 96481e1..d2bafa2 100644 --- a/test/NoSQLite.Test/Data/Animal.cs +++ b/test/NoSQLite.Test/Data/TestFaction.cs @@ -6,6 +6,6 @@ namespace NoSQLite.Test.Data; -public sealed record Animal +public sealed record TestFaction { } diff --git a/test/NoSQLite.Test/Data/Person.cs b/test/NoSQLite.Test/Data/TestPerson.cs similarity index 87% rename from test/NoSQLite.Test/Data/Person.cs rename to test/NoSQLite.Test/Data/TestPerson.cs index ddd9a6c..7edb1bd 100644 --- a/test/NoSQLite.Test/Data/Person.cs +++ b/test/NoSQLite.Test/Data/TestPerson.cs @@ -2,7 +2,7 @@ namespace NoSQLite.Test.Data; -public sealed record Person +public sealed record TestPerson { public string Email { get; set; } @@ -15,7 +15,7 @@ public sealed record Person public DateOnly Birthdate { get; set; } } -public sealed class PersonFaker : Faker +public sealed class PersonFaker : Faker { public PersonFaker() { diff --git a/test/NoSQLite.Test/Data/Vehicle.cs b/test/NoSQLite.Test/Data/TestVehicle.cs similarity index 53% rename from test/NoSQLite.Test/Data/Vehicle.cs rename to test/NoSQLite.Test/Data/TestVehicle.cs index 526a374..77be285 100644 --- a/test/NoSQLite.Test/Data/Vehicle.cs +++ b/test/NoSQLite.Test/Data/TestVehicle.cs @@ -1,5 +1,5 @@ namespace NoSQLite.Test.Data; -public sealed record Vehicle +public sealed record TestVehicle { } diff --git a/test/NoSQLite.Test/Indexing.cs b/test/NoSQLite.Test/Indexing.cs index a5f2461..25e41f7 100644 --- a/test/NoSQLite.Test/Indexing.cs +++ b/test/NoSQLite.Test/Indexing.cs @@ -2,59 +2,55 @@ public sealed class Indexing : TestBase { - public Indexing(TestFixture fixture) : base(fixture) - { - } - - [Fact] - public void Exists() + [Test] + public async Task Exists() { var table = GetTable(); var create = table.CreateIndex("test", "email"); - create.Should().BeTrue(); + await That(create).IsTrue(); var exists = table.IndexExists("test"); var notExists = table.IndexExists("test_1"); - exists.Should().BeTrue(); - notExists.Should().BeFalse(); + await That(exists).IsTrue(); + await That(notExists).IsFalse(); } - [Fact] - public void Create() + [Test] + public async Task Create() { var table = GetTable(); var notExists = table.CreateIndex("test", "email"); var exists = table.CreateIndex("test", "email"); - notExists.Should().BeTrue(); - exists.Should().BeFalse(); + await That(exists).IsTrue(); + await That(notExists).IsFalse(); } - [Fact] - public void Recreate() + [Test] + public async Task Recreate() { var table = GetTable(); table.RecreateIndex("test", "email"); - table.IndexExists("test").Should().BeTrue(); + await That(table.IndexExists("test")).IsTrue(); } - [Fact] - public void Delete() + [Test] + public async Task Delete() { var table = GetTable(); var create = table.CreateIndex("test", "email"); - create.Should().BeTrue(); + await That(create).IsTrue(); var exists = table.DeleteIndex("test"); var notExists = table.DeleteIndex("test"); - exists.Should().BeTrue(); - notExists.Should().BeFalse(); + await That(exists).IsTrue(); + await That(notExists).IsFalse(); } } diff --git a/test/NoSQLite.Test/NoSQLite.Test.csproj b/test/NoSQLite.Test/NoSQLite.Test.csproj index 2d31bd1..0ec9d6f 100644 --- a/test/NoSQLite.Test/NoSQLite.Test.csproj +++ b/test/NoSQLite.Test/NoSQLite.Test.csproj @@ -1,17 +1,13 @@  - net7.0 + net10.0 false - - - - - - + + diff --git a/test/NoSQLite.Test/Operations.cs b/test/NoSQLite.Test/Operations.cs new file mode 100644 index 0000000..97caf39 --- /dev/null +++ b/test/NoSQLite.Test/Operations.cs @@ -0,0 +1,107 @@ +namespace NoSQLite.Test; + +public sealed class Operations : TestBase +{ + [Test] + public async Task CreateTable() + { + var table = nameof(CreateTable); + + await That(Connection.TableExists(table)).IsFalse(); + + Connection.CreateTable(table); + + await That(Connection.TableExists(table)).IsTrue(); + } + + [Test] + public async Task DropTable() + { + var table = nameof(DropTable); + + await That(Connection.TableExists(table)).IsFalse(); + + Connection.CreateTable(table); + + await That(Connection.TableExists(table)).IsTrue(); + + Connection.DropTable(table); + + await That(Connection.TableExists(table)).IsFalse(); + } + + [Test] + public async Task DropAndCreateTable() + { + var table = nameof(DropAndCreateTable); + + await That(Connection.TableExists(table)).IsFalse(); + + Connection.CreateTable(table); + + await That(Connection.TableExists(table)).IsTrue(); + + var dbTable = Connection.GetTable(table); + dbTable.Insert("0", new PersonFaker().Generate()); + await That(dbTable.Count()).IsEqualTo(1); + dbTable.Dispose(); + dbTable = null; + + Connection.DropAndCreateTable(table); + await That(Connection.TableExists(table)).IsTrue(); + + dbTable = Connection.GetTable(table); + await That(dbTable.Count()).IsEqualTo(0); + } + + [Test] + public async Task Checkpoint() + { + var dir = Path.Combine(Environment.CurrentDirectory, $"{nameof(Operations)}_{nameof(Checkpoint)}_dir"); + var path = Path.Combine(dir, $"db.sqlite3"); + + if (Directory.Exists(dir)) Directory.Delete(dir, true); + Directory.CreateDirectory(dir); + + var connection = new NoSQLiteConnection(path); + + await That(Directory.GetFiles(dir).Length).IsEqualTo(1); + + var table = connection.GetTable(); + table.Insert("0", new PersonFaker().Generate()); + + await That(Directory.GetFiles(dir).Length).IsEqualTo(3); + + connection.Checkpoint(); + + await That(Directory.GetFiles(dir).Length).IsEqualTo(1); + + connection.Dispose(); + Directory.Delete(dir, true); + } + + [Test] + public async Task Dispose_Test() + { + var dir = Path.Combine(Environment.CurrentDirectory, $"{nameof(Operations)}_Dispose_dir"); + var path = Path.Combine(dir, $"db.sqlite3"); + + if (Directory.Exists(dir)) Directory.Delete(dir, true); + Directory.CreateDirectory(dir); + + var connection = new NoSQLiteConnection(path); + + await That(Directory.GetFiles(dir).Length).IsEqualTo(1); + + var table = connection.GetTable(); + table.Insert("0", new PersonFaker().Generate()); + + await That(Directory.GetFiles(dir).Length).IsEqualTo(3); + + connection.Dispose(); + + await That(Directory.GetFiles(dir).Length).IsEqualTo(1); + + Directory.Delete(dir, true); + } +} diff --git a/test/NoSQLite.Test/Parallelism.cs b/test/NoSQLite.Test/Parallelism.cs new file mode 100644 index 0000000..7182563 --- /dev/null +++ b/test/NoSQLite.Test/Parallelism.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NoSQLite.Test; + +internal class Parallelism +{ +} diff --git a/test/NoSQLite.Test/Queries.cs b/test/NoSQLite.Test/Queries.cs new file mode 100644 index 0000000..e8b1e10 --- /dev/null +++ b/test/NoSQLite.Test/Queries.cs @@ -0,0 +1,8 @@ +namespace NoSQLite.Test; + +public sealed class Queries : TestBase +{ + public Queries() + { + } +} diff --git a/test/NoSQLite.Test/Transactions.cs b/test/NoSQLite.Test/Transactions.cs new file mode 100644 index 0000000..8d3329b --- /dev/null +++ b/test/NoSQLite.Test/Transactions.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NoSQLite.Test; + +internal class Transactions +{ +} diff --git a/test/NoSQLite.Test/_setup.cs b/test/NoSQLite.Test/_setup.cs new file mode 100644 index 0000000..70099ce --- /dev/null +++ b/test/NoSQLite.Test/_setup.cs @@ -0,0 +1,60 @@ +using System.Runtime.CompilerServices; + +namespace NoSQLite.Test; + +public static class ModuleInitializer +{ + [ModuleInitializer] + public static void Initialize() + { + Randomizer.Seed = new Random(420690001); + } +} + +public abstract class TestBase +{ + public static string DbPath { get; private set; } = null!; + + public static NoSQLiteConnection Connection { get; private set; } = null!; + + public static bool Delete { get; set; } = true; + + [Before(Class)] + public static async Task BeforeAsync() + { + DbPath = Path.Combine(Environment.CurrentDirectory, $"{typeof(TSelf).Name}.sqlite3"); + + if (File.Exists(DbPath)) + { + File.Delete(DbPath); + } + + Connection = new NoSQLiteConnection(DbPath); + + await That(File.Exists(Connection.Path)).IsTrue(); + } + + public static NoSQLiteTable GetTable([CallerMemberName] string? caller = null) + { + return Connection.GetTable($"{caller}_{Guid.NewGuid()}"); + } + + public static NoSQLiteTable GetTable(IDictionary initPairs, [CallerMemberName] string? caller = null) + { + var table = GetTable(caller); + table.InsertMany(initPairs); + return table; + } + + [After(Assembly)] + public static async Task AfterAsync() + { + Connection.Dispose(); + await That(Connection.Tables.Count).IsEqualTo(0); + + if (Delete) + { + File.Delete(DbPath); + } + } +} diff --git a/test/NoSQLite.Test/_usings.cs b/test/NoSQLite.Test/_usings.cs index 9667401..8444d68 100644 --- a/test/NoSQLite.Test/_usings.cs +++ b/test/NoSQLite.Test/_usings.cs @@ -1,4 +1,4 @@ -global using FluentAssertions; -global using NoSQLite.Test.Abstractions; global using NoSQLite.Test.Data; -global using Xunit; +global using Bogus; +global using TUnit; +global using static TUnit.Assertions.Assert; From a1ad33f2c3e12d38f88b408f7d448c92f3c54f4e Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Tue, 30 Sep 2025 23:47:06 +0300 Subject: [PATCH 04/17] Simplify NoSQLite API and update project structure Removed CI/CD pipeline and updated project to .NET 9.0. Simplified `Insert` and `InsertMany` methods by removing the `id` parameter. Updated database schema to use `JSONB` column type and added a new index. Reorganized and re-added several files, including benchmarking, CRUD, and extension utilities. Added `Invoke-Process.psm1` for PowerShell command handling. Updated SQLite database files to reflect schema changes. --- .github/Invoke-Process.psm1 | 18 ++++++ {ci => .github}/build.ps1 | 0 {ci => .github}/pack.ps1 | 0 {ci => .github}/test.ps1 | 0 .github/workflows/release.yaml | 56 ------------------ {samples => sample}/ConsoleApp/Benchy.cs | 0 {samples => sample}/ConsoleApp/CRUD.cs | 0 .../ConsoleApp/ConsoleApp.csproj | 7 +-- {samples => sample}/ConsoleApp/Data/Person.cs | 0 .../ConsoleApp/Extensions/RangeExtensions.cs | 0 {samples => sample}/ConsoleApp/INDEX.cs | 0 {samples => sample}/ConsoleApp/Program.cs | 0 .../ConsoleApp/Properties/launchSettings.json | 0 sample/ConsoleApp/console.sqlite3 | Bin 0 -> 16384 bytes sample/ConsoleApp/console.sqlite3-shm | Bin 0 -> 32768 bytes sample/ConsoleApp/console.sqlite3-wal | 0 src/NoSQLite/NoSQLiteConnection.cs | 4 +- src/NoSQLite/NoSQLiteTable.cs | 25 ++++---- test/NoSQLite.Test/Operations.cs | 11 +++- 19 files changed, 41 insertions(+), 80 deletions(-) create mode 100644 .github/Invoke-Process.psm1 rename {ci => .github}/build.ps1 (100%) rename {ci => .github}/pack.ps1 (100%) rename {ci => .github}/test.ps1 (100%) delete mode 100644 .github/workflows/release.yaml rename {samples => sample}/ConsoleApp/Benchy.cs (100%) rename {samples => sample}/ConsoleApp/CRUD.cs (100%) rename {samples => sample}/ConsoleApp/ConsoleApp.csproj (51%) rename {samples => sample}/ConsoleApp/Data/Person.cs (100%) rename {samples => sample}/ConsoleApp/Extensions/RangeExtensions.cs (100%) rename {samples => sample}/ConsoleApp/INDEX.cs (100%) rename {samples => sample}/ConsoleApp/Program.cs (100%) rename {samples => sample}/ConsoleApp/Properties/launchSettings.json (100%) create mode 100644 sample/ConsoleApp/console.sqlite3 create mode 100644 sample/ConsoleApp/console.sqlite3-shm create mode 100644 sample/ConsoleApp/console.sqlite3-wal diff --git a/.github/Invoke-Process.psm1 b/.github/Invoke-Process.psm1 new file mode 100644 index 0000000..ab406a7 --- /dev/null +++ b/.github/Invoke-Process.psm1 @@ -0,0 +1,18 @@ +function Invoke-Process($command) { + if ($command -is [string[]]) { + } + elseif ($command -is [string]) { + $command = $command -split " " + } + else { + throw "Invalid command object. Can only accept string[] or string"; + } + + $filePath = $command[0] + $argumentList = $command[1..($command.Length - 1)]; + $process = Start-Process -FilePath $filePath -ArgumentList $argumentList -NoNewWindow -Wait + Write-Host $process + if ($null -ne $process.ExitCode || $process.ExitCode -ne 0) { + exit $process.ExitCode + } +} \ No newline at end of file diff --git a/ci/build.ps1 b/.github/build.ps1 similarity index 100% rename from ci/build.ps1 rename to .github/build.ps1 diff --git a/ci/pack.ps1 b/.github/pack.ps1 similarity index 100% rename from ci/pack.ps1 rename to .github/pack.ps1 diff --git a/ci/test.ps1 b/.github/test.ps1 similarity index 100% rename from ci/test.ps1 rename to .github/test.ps1 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml deleted file mode 100644 index 357b3ff..0000000 --- a/.github/workflows/release.yaml +++ /dev/null @@ -1,56 +0,0 @@ -name: Release - -env: - DOTNET_NOLOGO: true - -on: - workflow_dispatch: - push: - branches: [main] - paths: - - "src/**/*.cs" - - "src/**/*.csproj" - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - with: { fetch-depth: 0 } - - - name: Setup .NET - uses: actions/setup-dotnet@v2 - with: { dotnet-version: 7.0.x, include-prerelease: true } - - - name: Setup NerdBank.GitVersioning - uses: dotnet/nbgv@master - id: nbgv - with: { setAllVars: true } - - - name: Build - shell: pwsh - run: ./ci/build.ps1 - - - name: Test - shell: pwsh - run: ./ci/test.ps1 - - - name: Pack - shell: pwsh - run: ./ci/pack.ps1 - - - name: Publish on NuGet - env: - SOURCE_URL: https://api.nuget.org/v3/index.json - NUGET_AUTH_TOKEN: ${{ secrets.NUGET_API_KEY }} - run: | - dotnet nuget push ./nuget/*.nupkg --skip-duplicate -s ${{ env.SOURCE_URL }} -k ${{ env.NUGET_AUTH_TOKEN }} - - - name: Create Github Release - uses: actions/create-release@latest - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: v${{ env.NBGV_SemVer2 }} - release_name: ${{ env.NBGV_SemVer2 }} diff --git a/samples/ConsoleApp/Benchy.cs b/sample/ConsoleApp/Benchy.cs similarity index 100% rename from samples/ConsoleApp/Benchy.cs rename to sample/ConsoleApp/Benchy.cs diff --git a/samples/ConsoleApp/CRUD.cs b/sample/ConsoleApp/CRUD.cs similarity index 100% rename from samples/ConsoleApp/CRUD.cs rename to sample/ConsoleApp/CRUD.cs diff --git a/samples/ConsoleApp/ConsoleApp.csproj b/sample/ConsoleApp/ConsoleApp.csproj similarity index 51% rename from samples/ConsoleApp/ConsoleApp.csproj rename to sample/ConsoleApp/ConsoleApp.csproj index 33f31d5..c866ae4 100644 --- a/samples/ConsoleApp/ConsoleApp.csproj +++ b/sample/ConsoleApp/ConsoleApp.csproj @@ -1,15 +1,12 @@  + net9.0 Exe - net6.0 - CS1591;NU1603; - false - false - + diff --git a/samples/ConsoleApp/Data/Person.cs b/sample/ConsoleApp/Data/Person.cs similarity index 100% rename from samples/ConsoleApp/Data/Person.cs rename to sample/ConsoleApp/Data/Person.cs diff --git a/samples/ConsoleApp/Extensions/RangeExtensions.cs b/sample/ConsoleApp/Extensions/RangeExtensions.cs similarity index 100% rename from samples/ConsoleApp/Extensions/RangeExtensions.cs rename to sample/ConsoleApp/Extensions/RangeExtensions.cs diff --git a/samples/ConsoleApp/INDEX.cs b/sample/ConsoleApp/INDEX.cs similarity index 100% rename from samples/ConsoleApp/INDEX.cs rename to sample/ConsoleApp/INDEX.cs diff --git a/samples/ConsoleApp/Program.cs b/sample/ConsoleApp/Program.cs similarity index 100% rename from samples/ConsoleApp/Program.cs rename to sample/ConsoleApp/Program.cs diff --git a/samples/ConsoleApp/Properties/launchSettings.json b/sample/ConsoleApp/Properties/launchSettings.json similarity index 100% rename from samples/ConsoleApp/Properties/launchSettings.json rename to sample/ConsoleApp/Properties/launchSettings.json diff --git a/sample/ConsoleApp/console.sqlite3 b/sample/ConsoleApp/console.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..749bb426f2ec35a66a6029d83bae724215b12dc5 GIT binary patch literal 16384 zcmeI&O>fgM7zc2tNy~^7%w_6!s;?5`FE9`){AaO!)!R|uF7}Cy@{*PqYdE%G!*OM(j-|Ymc&kjXAaZ}bLH%XyD z9xz4-(Pc5RqRG0LwPm#kmAfo-vOdx*`hyhc8lh|S(~Xap4Z(W|KmY;|fB*y_009U< z00Izzz~2ILQyJZ_X!GSD^!&G;7)>XBm?p!}o%qA^Ipa~!Y4sh}?ml+*SoME>tCqRb zW%--haUw!?cYB*vZ?CHvtM&PccCB2I3%*a?S7Sf#nl}pBD*e{Oj&n8@t63(K!2^%0 zT1DU4>$C2TEQ3ym4Z7{!fz!yksg<*i=elL@pY_^LTfKeu#M!T@DeIP5w+p4RQ_;xc zS0``Aaz{UOr>R&x5AzQk+Ids$?U$6MC5uj*gnp(UWQBkL1Rwwb2tWV=5P$##AOHaf zK;Ts1Mafv5S0i6UWB=eCSEmDfi${JeH|{o{HU3H6yO?TTOl|VnOQ&eixn<|$kHt~Q z8$6lD*%}i!^nygDJUDAbs FindBytesMany(IEnumerable ids, bool throwIfNo private readonly Lazy insertStmt; - public void Insert(string id, T obj) + public void Insert(T obj) { - InsertBytes(id, JsonSerializer.SerializeToUtf8Bytes(obj, JsonOptions)); + InsertBytes(JsonSerializer.SerializeToUtf8Bytes(obj, JsonOptions)); } - public void InsertMany(IDictionary keyValuePairs) + public void InsertMany(IEnumerable keyValuePairs) { using var transaction = new SQLiteTransaction(db); - foreach (var (id, obj) in keyValuePairs) + foreach (var obj in keyValuePairs) { - Insert(id, obj); + Insert(obj); } } - public void InsertBytes(string id, byte[] obj) + public void InsertBytes(byte[] obj) { var stmt = insertStmt.Value; lock (insertStmt) { - stmt.BindText(1, id); - stmt.BindText(2, obj); + stmt.BindText(1, obj); var result = stmt.Step(); - db.CheckResult(result, $"Could not Insert ({id})"); + db.CheckResult(result, $"Could not Insert"); stmt.Reset(); } } - public void InsertBytesMany(IDictionary keyValuePairs) + public void InsertBytesMany(IEnumerable keyValuePairs) { using var transaction = new SQLiteTransaction(db); - foreach (var (id, obj) in keyValuePairs) + foreach (var obj in keyValuePairs) { - InsertBytes(id, obj); + InsertBytes(obj); } } diff --git a/test/NoSQLite.Test/Operations.cs b/test/NoSQLite.Test/Operations.cs index 97caf39..7dd8f1a 100644 --- a/test/NoSQLite.Test/Operations.cs +++ b/test/NoSQLite.Test/Operations.cs @@ -12,6 +12,11 @@ public async Task CreateTable() Connection.CreateTable(table); await That(Connection.TableExists(table)).IsTrue(); + + var t = Connection.GetTable(table); + + t.CreateIndex("name", "name"); + t.Insert(new { id = "1", name = 10 }); } [Test] @@ -42,7 +47,7 @@ public async Task DropAndCreateTable() await That(Connection.TableExists(table)).IsTrue(); var dbTable = Connection.GetTable(table); - dbTable.Insert("0", new PersonFaker().Generate()); + dbTable.Insert(new PersonFaker().Generate()); await That(dbTable.Count()).IsEqualTo(1); dbTable.Dispose(); dbTable = null; @@ -68,7 +73,7 @@ public async Task Checkpoint() await That(Directory.GetFiles(dir).Length).IsEqualTo(1); var table = connection.GetTable(); - table.Insert("0", new PersonFaker().Generate()); + table.Insert(new PersonFaker().Generate()); await That(Directory.GetFiles(dir).Length).IsEqualTo(3); @@ -94,7 +99,7 @@ public async Task Dispose_Test() await That(Directory.GetFiles(dir).Length).IsEqualTo(1); var table = connection.GetTable(); - table.Insert("0", new PersonFaker().Generate()); + table.Insert(new PersonFaker().Generate()); await That(Directory.GetFiles(dir).Length).IsEqualTo(3); From 58a2b35104ed66a4311e51c4d8da3d68d9356896 Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Sat, 18 Oct 2025 01:19:57 +0300 Subject: [PATCH 05/17] Fully refactor Connection and Tables - The connection has been refactor to use a provided sqlite3 db managed by the user. Some extension methods on sqlite3 have been added to keep the old logic for a user. - The tables and it's SQLiteStmt have been reweritten to support fewer methods but with the correct schematics this library aims to achieve. - Tests reweritten using TUnit and focused only on the simple cases for now. --- Directory.Packages.props | 14 +- src/NoSQLite/NoSQLite.csproj | 4 + src/NoSQLite/NoSQLiteConnection.cs | 120 ++--- src/NoSQLite/NoSQLiteException.cs | 45 +- src/NoSQLite/NoSQLiteTable.cs | 554 ++++++------------------ src/NoSQLite/SQLiteStmt.cs | 289 +++++++++--- src/NoSQLite/Utilities.cs | 116 ++++- src/NoSQLite/sqlite3_mixins.cs | 68 +++ test/NoSQLite.Test/CRUD.cs | 116 ----- test/NoSQLite.Test/Connection.cs | 23 + test/NoSQLite.Test/Data/TestPerson.cs | 11 +- test/NoSQLite.Test/Indexing.cs | 56 --- test/NoSQLite.Test/NoSQLite.Test.csproj | 1 + test/NoSQLite.Test/Operations.cs | 112 ----- test/NoSQLite.Test/Queries.cs | 8 - test/NoSQLite.Test/Table.cs | 81 ++++ test/NoSQLite.Test/_setup.cs | 51 ++- 17 files changed, 755 insertions(+), 914 deletions(-) create mode 100644 src/NoSQLite/sqlite3_mixins.cs delete mode 100644 test/NoSQLite.Test/CRUD.cs create mode 100644 test/NoSQLite.Test/Connection.cs delete mode 100644 test/NoSQLite.Test/Indexing.cs delete mode 100644 test/NoSQLite.Test/Operations.cs delete mode 100644 test/NoSQLite.Test/Queries.cs create mode 100644 test/NoSQLite.Test/Table.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 36fb8fd..2225120 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,21 +6,21 @@ - - - + + + - - + + - + - \ No newline at end of file + diff --git a/src/NoSQLite/NoSQLite.csproj b/src/NoSQLite/NoSQLite.csproj index e61c852..73b6dcd 100644 --- a/src/NoSQLite/NoSQLite.csproj +++ b/src/NoSQLite/NoSQLite.csproj @@ -5,6 +5,10 @@ True + + + + diff --git a/src/NoSQLite/NoSQLiteConnection.cs b/src/NoSQLite/NoSQLiteConnection.cs index 23b4159..9fb237d 100644 --- a/src/NoSQLite/NoSQLiteConnection.cs +++ b/src/NoSQLite/NoSQLiteConnection.cs @@ -8,9 +8,9 @@ namespace NoSQLite; /// Represents a connection to a NoSQLite database, providing methods to manage tables and documents. /// [Preserve(AllMembers = true)] -public sealed class NoSQLiteConnection : IDisposable +public sealed partial class NoSQLiteConnection : IDisposable { - private readonly bool disposeDb; + private readonly Lazy tableExistsStmt; /// /// The collection of tables managed by this connection. @@ -30,51 +30,25 @@ public sealed class NoSQLiteConnection : IDisposable /// /// Initializes a new instance of the class. /// - /// The path to the database file, or the path where the file will be created. + /// The sqlite3 database. /// The options used to serialize/deserialize JSON objects, or null for defaults. - /// Thrown if is null. - public NoSQLiteConnection(string databasePath, JsonSerializerOptions? jsonOptions = null, bool wal = true) + public NoSQLiteConnection(sqlite3 db, JsonSerializerOptions? jsonOptions = null) { - ArgumentNullException.ThrowIfNull(databasePath, nameof(databasePath)); - disposeDb = true; - - var result = sqlite3_open(databasePath, out db); - db.CheckResult(result, $"Could not open or create database file: {Path}"); - - if (wal) - { - SetJournalMode(); - } - + this.db = db; + tables = []; Version = sqlite3_libversion().utf8_to_string(); - Name = System.IO.Path.GetFileName(databasePath); - Path = databasePath; JsonOptions = jsonOptions; - tables = []; - } - /// - /// Sets the SQLite journal mode to Write-Ahead Logging (WAL). - /// - public void SetJournalMode() - { - sqlite3_exec(db, "PRAGMA journal_mode=WAL;"); + tableExistsStmt = new(() => new SQLiteStmt(db, """ + SELECT name FROM "sqlite_master" + WHERE type='table' AND name = ?; + """u8)); } /// - /// Gets or sets the JSON serializer options used to serialize/deserialize the documents. + /// Gets the JSON serializer options used to serialize/deserialize the documents. /// - public JsonSerializerOptions? JsonOptions { get; set; } - - /// - /// Gets the database name. - /// - public string Name { get; } - - /// - /// Gets the database path used by this connection. - /// - public string Path { get; } + public JsonSerializerOptions? JsonOptions { get; } /// /// Gets the SQLite library version. @@ -84,18 +58,17 @@ public void SetJournalMode() /// /// Gets all the tables that belong to this connection. /// - public IReadOnlyCollection Tables => [.. tables.Values]; + public IEnumerable Tables => tables.Values; /// /// Gets a table with the specified name, or the default "documents" table if is null. /// If the table does not exist in the connection's cache, a new is created. /// - /// The name of the table to create or use, or null to use the default "documents" table. + /// The name of the table to use and create if it does not exist. /// The instance for the specified table. - public NoSQLiteTable GetTable(string? table = null) + public NoSQLiteTable GetTable(string table) { - const string docs = "documents"; - return tables.GetOrAdd(table ?? docs, static (table, connection) => new(table, connection), this); + return tables.GetOrAdd(table, static (table, connection) => new(table, connection), this); } /// @@ -105,13 +78,8 @@ public NoSQLiteTable GetTable(string? table = null) /// true if the table exists; otherwise, false. public bool TableExists(string table) { - using var stmt = new SQLiteStmt(db, $""" - SELECT count(*) FROM "sqlite_master" - WHERE type='table' AND name='{table}'; - """); - - stmt.Step(); - return stmt.ColumnInt(0) != 0; + var stmt = tableExistsStmt.Value; + return stmt.Execute(b => b.Text(1, table), static r => r.Result is SQLITE_ROW, shouldThrow: false); } /// @@ -121,56 +89,25 @@ SELECT count(*) FROM "sqlite_master" /// Thrown if the table cannot be created. public void CreateTable(string table) { - var result = sqlite3_exec(db, $""" + using var stmt = new SQLiteStmt(db, $""" CREATE TABLE IF NOT EXISTS "{table}" ( - "json" JSONB NOT NULL + "documents" JSON NOT NULL ); """); - - db.CheckResult(result, $"Could not create '{table}' database table"); + stmt.Execute(b => b.Text(1, table)); } /// /// Drops a table with the specified name if it exists. This will delete all indexes, views, etc. /// /// The name of the table to drop. - /// Thrown if the table cannot be dropped. + /// Thrown if the table cannot be dropped. public void DropTable(string table) { - var result = sqlite3_exec(db, $""" + using var stmt = new SQLiteStmt(db, $""" DROP TABLE IF EXISTS "{table}"; - """); - - db.CheckResult(result, $"Could not drop '{table}' database table"); - } - - /// - /// Drops the table and then creates it again. This will delete all indexes, views, etc. - /// - /// The name of the table to drop and recreate. - /// See for more info. - public void DropAndCreateTable(string table) - { - DropTable(table); - CreateTable(table); - } - - /// - /// Deletes all rows from a table. - /// - /// The name of the table to clear. - public void Clear(string table) - { - sqlite3_exec(db, $"""DELETE FROM "{table}";"""); - } - - /// - /// Executes a write-ahead log (WAL) checkpoint for the database. - /// - /// See for more info. - public void Checkpoint() - { - sqlite3_wal_checkpoint(db, Name); + """); + stmt.Execute(b => b.Text(1, table)); } /// @@ -188,15 +125,12 @@ public void Dispose() var length = tables.Count; var buffer = ArrayPool.Shared.Rent(length); tables.Values.CopyTo(buffer, 0); + tables.Clear(); for (int i = 0; i < length; i++) buffer[i].Dispose(); ArrayPool.Shared.Return(buffer, true); - if (disposeDb) - { - sqlite3_close_v2(db); - db.Dispose(); - } + if (tableExistsStmt.IsValueCreated) tableExistsStmt.Value.Dispose(); } } diff --git a/src/NoSQLite/NoSQLiteException.cs b/src/NoSQLite/NoSQLiteException.cs index 0c3799e..770f98c 100644 --- a/src/NoSQLite/NoSQLiteException.cs +++ b/src/NoSQLite/NoSQLiteException.cs @@ -1,14 +1,55 @@ -namespace NoSQLite; +using System.Runtime.CompilerServices; -/// +namespace NoSQLite; + +/// +/// Represents errors that occur during NoSQLite database operations. +/// public sealed class NoSQLiteException : Exception { + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. internal NoSQLiteException(string message) : base(message) { } + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. internal NoSQLiteException(string message, Exception inner) : base(message, inner) { } + + /// + /// Throws a with the specified message if the condition is true. + /// + /// The condition to evaluate. If true, the exception is thrown. + /// The interpolated message to include in the exception if thrown. + /// Thrown when is true. + internal static void If(bool condition, [InterpolatedStringHandlerArgument("condition")] ref ConditionInterpolation message) + { + if (condition) + { + throw new NoSQLiteException(message.ToString()); + } + } + + /// + /// Throws a with the specified message if the condition is true. + /// + /// The condition to evaluate. If true, the exception is thrown. + /// The interpolated message to include in the exception if thrown. + /// Thrown when is true. + internal static void KeyNotFound(bool condition, [InterpolatedStringHandlerArgument("condition")] ref ConditionInterpolation message) + { + if (condition) + { + throw new KeyNotFoundException(message.ToString()); + } + } } diff --git a/src/NoSQLite/NoSQLiteTable.cs b/src/NoSQLite/NoSQLiteTable.cs index d7d3102..1ea247d 100644 --- a/src/NoSQLite/NoSQLiteTable.cs +++ b/src/NoSQLite/NoSQLiteTable.cs @@ -1,12 +1,8 @@ -using System.Text.Json; +using System.Linq.Expressions; +using System.Text.Json; namespace NoSQLite; -using static SQLitePCL.raw; - -/// -/// todo: Summary -/// [Preserve(AllMembers = true)] public sealed class NoSQLiteTable : IDisposable { @@ -15,515 +11,207 @@ public sealed class NoSQLiteTable : IDisposable internal NoSQLiteTable(string table, NoSQLiteConnection connection) { - ArgumentNullException.ThrowIfNull(connection, nameof(connection)); - ArgumentNullException.ThrowIfNull(table, nameof(table)); + ArgumentNullException.ThrowIfNull(connection); + ArgumentException.ThrowIfNullOrEmpty(table); Table = table; Connection = connection; - JsonOptions = connection.JsonOptions; db = connection.db; connection.CreateTable(table); - - #region Lazy statment initialization - - countStmt = new(() => - { - var stmt = new SQLiteStmt(db, $""" - SELECT count(*) FROM "{Table}"; - """); - - disposables.Add(stmt); - return stmt; - }); - - existsStmt = new(() => - { - var stmt = new SQLiteStmt(db, $""" - SELECT count(*) FROM "{Table}" - WHERE id is ?; - """); - - disposables.Add(stmt); - return stmt; - }); - - allStmt = new(() => - { - var stmt = new SQLiteStmt(db, $""" - SELECT json FROM "{Table}"; - """); - disposables.Add(stmt); - return stmt; - }); - - findStmt = new(() => - { - var stmt = new SQLiteStmt(db, $""" - SELECT json - FROM "{Table}" - WHERE id is ?; - """); - - disposables.Add(stmt); - return stmt; - }); - - insertStmt = new(() => - { - var stmt = new SQLiteStmt(db, $""" - REPLACE INTO "{Table}" - VALUES (json(?)); - """); - - disposables.Add(stmt); - return stmt; - }); - - removeStmt = new(() => - { - var stmt = new SQLiteStmt(db, $""" - DELETE FROM "{Table}" - WHERE id is ?; - """); - - disposables.Add(stmt); - return stmt; - }); - - #endregion } - /// - /// The connection that created and manages this table. - /// - /// If the connection is disposed this will also be disposed. public NoSQLiteConnection Connection { get; } - /// - /// The name of the table this connection will use. - /// public string Table { get; } - /// - /// Gets or Sets the JSON serializer options used to serialize/deserialize the documents. - /// - public JsonSerializerOptions? JsonOptions { get; set; } - - /// - /// Deletes all rows from a table. - /// - public void Clear() => Connection.Clear(Table); + public JsonSerializerOptions? JsonOptions => Connection.JsonOptions; - /// - /// Drops the table and then Creates it again. This will delete all indexes views etc. - /// - /// See for more info. - public void DropAndCreate() => Connection.DropAndCreateTable(Table); + private SQLiteStmt CountStmt => field ??= new(db, disposables: disposables, sql: $""" + SELECT count(*) FROM "{Table}" + """); - #region Basic - - private readonly Lazy countStmt; - - /// - /// Gets the number of rows in the table. - /// - /// The number of rows in the table as an . public int Count() { - var stmt = countStmt.Value; - lock (countStmt) - { - stmt.Step(); - - var count = stmt.ColumnInt(0); - stmt.Reset(); - return count; - } + return CountStmt.Execute(null, static r => r.Int(0)); } - /// - /// Gets the number of rows in the table as a . - /// - /// The number of rows in the table as a . + private SQLiteStmt LongCountStmt => field ??= new(db, disposables: disposables, sql: $""" + SELECT count(*) FROM "{Table}" + """); + public long LongCount() { - var stmt = countStmt.Value; - lock (countStmt) - { - stmt.Step(); - - var count = stmt.ColumnLong(0); - stmt.Reset(); - return count; - } + var table = Table; + return LongCountStmt.Execute(null, static r => r.Long(0)); } - private readonly Lazy existsStmt; + private SQLiteStmt AllStmt => field ??= new(db, disposables: disposables, sql: $""" + SELECT "documents" FROM "{Table}" + """); - /// - /// Check whether a document exists or not. - /// - /// The id to search for. - /// True when the id exists otherwise false. - public bool Exists(string id) + public T[] All() { - var stmt = existsStmt.Value; - lock (existsStmt) - { - stmt.BindText(1, id); - stmt.Step(); - - var value = stmt.ColumnInt(0); - stmt.Reset(); - return value != 0; - } + var jsonOptions = JsonOptions; + return AllStmt.ExecuteMany(null, r => r.Deserialize(0, jsonOptions)!); } - private readonly Lazy allStmt; + private SQLiteStmt ClearStmt => field ??= new(db, disposables: disposables, sql: $""" + DELETE FROM "{Table}" + """); - public IEnumerable All() + public void Clear() { - var stmt = allStmt.Value; - lock (allStmt) - { - start: - var result = stmt.Step(); - - if (result is not SQLITE_ROW) - { - stmt.Reset(); - yield break; - } - - var value = stmt.ColumnDeserialize(0, JsonOptions); - yield return value!; - goto start; - } + ClearStmt.Execute(null); } - public IEnumerable AllBytes() - { - var stmt = allStmt.Value; - lock (allStmt) - { - start: - var result = stmt.Step(); - - if (result is not SQLITE_ROW) - { - stmt.Reset(); - yield break; - } - - var bytes = stmt.ColumnBlob(0).ToArray(); - yield return bytes; - goto start; - } - } - - #endregion - - #region Find - - private readonly Lazy findStmt; + private SQLiteStmt ExistsStmt => field ??= new(db, disposables: disposables, sql: $""" + SELECT count(*) FROM "{Table}" + WHERE "documents"->('$.' || ?) = ?; + """); - /// - /// Get an object for the provided id or null. - /// - /// The type the object will deserialize to. - /// The id to search for. - /// An instance of or null. - public T? Find(string id) + public bool Exists(Expression> selector, TKey key) { - var stmt = findStmt.Value; - lock (findStmt) - { - stmt.BindText(1, id); - var result = stmt.Step(); - - if (result is not SQLITE_ROW) - { - stmt.Reset(); - return default; - } - - var value = stmt.ColumnDeserialize(0, JsonOptions); - stmt.Reset(); - return value; - } - } - - /// - /// Get an object for each one of the provided ids. - /// - /// The type the objects will deserialize to. - /// The ids to search for. - /// True to ignore missing ids - /// A list of objects. - /// When is true and a key is not found. - public IEnumerable FindMany(IEnumerable ids, bool throwIfNotFound = true) - { - foreach (var id in ids) - { - var doc = Find(id); + var propertyPath = selector.GetPropertyPath(); + var jsonKey = JsonSerializer.SerializeToUtf8Bytes(key, JsonOptions); - if (doc is null) + return ExistsStmt.Execute( + b => { - Throw.KeyNotFound(throwIfNotFound, $"Could not locate the Id '{id}'"); - continue; - } - yield return doc; - } + b.Text(1, propertyPath); + b.Text(2, jsonKey); + }, + static b => b.Int(0) is not 0 + ); } - /// - /// Get Id - pairs. - /// If an object doesn't exist it will have a null value. - /// - /// The type the objects will deserialize to. - /// The ids to search for. - /// An enumerable of Id - object pairs. - /// Duplicate keys are ignored. - public IDictionary FindPairs(IEnumerable ids) - { - var dictionary = new Dictionary(); - foreach (var id in ids) - { - if (dictionary.ContainsKey(id)) continue; - - dictionary.Add(id, Find(id)); - } - return dictionary; - } + private SQLiteStmt FindStmt => field ??= new(db, disposables: disposables, sql: $""" + SELECT "documents" + FROM "{Table}" + WHERE "documents"->('$.' || ?) = ? + """); - /// - /// Get a byte array for the provided id or null. - /// - /// The id to search for. - /// A byte array or null. - public byte[]? FindBytes(string id) + public T Find(Expression> selector, TKey key) { - var stmt = findStmt.Value; - lock (findStmt) - { - stmt.BindText(1, id); - var result = stmt.Step(); + var propertyPath = selector.GetPropertyPath(); + var jsonKey = JsonSerializer.SerializeToUtf8Bytes(key, JsonOptions); - if (result is not SQLITE_ROW) + return FindStmt.Execute( + b => { - stmt.Reset(); - return default; - } - - var bytes = stmt.ColumnBlob(0).ToArray(); - stmt.Reset(); - return bytes; - } + b.Text(1, propertyPath); + b.Text(2, jsonKey); + }, + r => r.Deserialize(0, JsonOptions)! + ); } - /// - /// Get a byte array for each one of the provided ids. - /// - /// The ids to search for. - /// True to ignore missing ids - /// A list of byte arrays. - /// When is true and a key is not found. - public IEnumerable FindBytesMany(IEnumerable ids, bool throwIfNotFound = true) + private SQLiteStmt FindPropertyStmt => field ??= new(db, disposables: disposables, sql: $""" + SELECT "documents"->('$.' || ?) + FROM "{Table}" + WHERE "documents"->('$.' || ?) = ? + """); + + public TProperty FindProperty(Expression> keySelector, Expression> propertySelector, TKey key) { - foreach (var id in ids) - { - var bytes = FindBytes(id); + var keyPropertyPath = keySelector.GetPropertyPath(); + var propertyPath = propertySelector.GetPropertyPath(); + var jsonKey = JsonSerializer.SerializeToUtf8Bytes(key, JsonOptions); - if (bytes is null) + return FindPropertyStmt.Execute( + b => { - Throw.KeyNotFound(throwIfNotFound, $"Could not locate the Id '{id}'"); - continue; - } - yield return bytes; - } - } - - /// - /// Get Id - byte array pairs. - /// If an object doesn't exist it will have a null value - /// for the corresponding Id in the dictionary. - /// - /// The ids to search for. - /// A dictionary of Id - object pairs. - /// Duplicate keys are just put on the key again. - public IDictionary FindBytesPairs(IEnumerable ids) - { - var pairs = new Dictionary(); - foreach (var id in ids) - { - var bytes = FindBytes(id); - pairs[id] = bytes; - } - return pairs; + b.Text(1, propertyPath); + b.Text(2, keyPropertyPath); + b.Text(3, jsonKey); + }, + r => r.Deserialize(0, JsonOptions)! + ); } - #endregion - - #region Insert - - private readonly Lazy insertStmt; + private SQLiteStmt InsertStmt => field ??= new(db, disposables: disposables, sql: $""" + INSERT INTO "{Table}"("documents") VALUES (?) + """); public void Insert(T obj) { - InsertBytes(JsonSerializer.SerializeToUtf8Bytes(obj, JsonOptions)); - } + var document = JsonSerializer.SerializeToUtf8Bytes(obj, JsonOptions); - public void InsertMany(IEnumerable keyValuePairs) - { - using var transaction = new SQLiteTransaction(db); - foreach (var obj in keyValuePairs) - { - Insert(obj); - } + InsertStmt.Execute(b => b.Blob(1, document)); } - public void InsertBytes(byte[] obj) + public void Update(T document, Expression> selector) { - var stmt = insertStmt.Value; + using var stmt = new SQLiteStmt(db, $""" + UPDATE "{Table}" + SET "documents" = ? + WHERE "documents"->('$.' || ?) = ?; + """); - lock (insertStmt) - { - stmt.BindText(1, obj); - var result = stmt.Step(); - - db.CheckResult(result, $"Could not Insert"); - stmt.Reset(); - } - } + var propertyPath = selector.GetPropertyPath(); + var key = selector.Compile().Invoke(document); + var jsonDocument = JsonSerializer.SerializeToUtf8Bytes(document, JsonOptions); + var jsonKey = JsonSerializer.SerializeToUtf8Bytes(key, JsonOptions); - public void InsertBytesMany(IEnumerable keyValuePairs) - { - using var transaction = new SQLiteTransaction(db); - foreach (var obj in keyValuePairs) + stmt.Execute(b => { - InsertBytes(obj); - } + b.Blob(1, jsonDocument); + b.Text(2, propertyPath); + b.Text(3, jsonKey); + }); } - #endregion - - #region Remove - - private readonly Lazy removeStmt; - - /// - /// Deletes the specified id from the database. - /// - /// The id to delete. - public void Remove(string id) + public void Remove(Expression> selector, TKey key) { - var stmt = removeStmt.Value; + using var stmt = new SQLiteStmt(db, $""" + DELETE FROM "{Table}" + WHERE "documents"->('$.' || ?) = ?; + """); - lock (removeStmt) - { - stmt.BindText(1, id); - stmt.Step(); - stmt.Reset(); - } - } + var propertyPath = selector.GetPropertyPath(); + var jsonKey = JsonSerializer.SerializeToUtf8Bytes(key, JsonOptions); - /// - /// Deletes the specified ids from the database. - /// - /// The ids to delete. - public void RemoveMany(params IEnumerable ids) - { - using var transaction = new SQLiteTransaction(db); - foreach (var id in ids) + stmt.Execute(b => { - Remove(id); - } + b.Text(1, propertyPath); + b.Text(2, jsonKey); + }); } - #endregion - - #region Indexing - - /// - /// Check whether an index exists or not. - /// - /// The index to search for. - /// True when the index exists. public bool IndexExists(string indexName) { - string sql = $""" - SELECT count(*) FROM sqlite_master - WHERE type='index' and name="{Table}_{indexName}"; - """; + using var stmt = new SQLiteStmt(db, """ + SELECT name FROM "sqlite_master" + WHERE type='index' AND name = ?; + """u8); - using var stmt = new SQLiteStmt(db, sql); - stmt.Step(); + var index = $"{Table}_{indexName}"; - var value = stmt.ColumnInt(0); - return value is not 0; + return stmt.Execute(b => b.Text(1, index), static r => r.Result is SQLITE_ROW, shouldThrow: false); } - /// - /// Create an index on a json parameter using the json_extract(json, '$.param') opperator. - /// Parameter can include nested json values eg: assets.house.location - /// - /// The name of the index. If this name exists the index won't be created. - /// The json parameter to create the index for. - /// - /// Parameter names are case sensitive.
- /// Index name on sqlite will always be created as _ - ///
- public bool CreateIndex(string indexName, string parameter) + public void CreateIndex(Expression> selector, string indexName) { - string sql = $""" - CREATE INDEX "{Table}_{indexName}" - ON "{Table}" (json_extract(json, '$.{parameter}')); - """; + var propertyPath = selector.GetPropertyPath(); - return sqlite3_exec(db, sql) is SQLITE_OK; - } + // can't use parameter (?) here so we have to create a new statement every time. + using var stmt = new SQLiteStmt(db, $""" + CREATE INDEX IF NOT EXISTS "{Table}_{indexName}" + ON "{Table}" ("documents"->'$.{propertyPath}') + """); - /// - /// Combination of and - /// - /// The name of the index. - /// The json parameter to update the index for. - /// Index on sqlite is always _ - public void RecreateIndex(string indexName, string parameter) - { - DeleteIndex(indexName); - CreateIndex(indexName, parameter); + stmt.Execute(null); } - /// - /// Delete an index if it exists. - /// - /// The name of the index. - /// Index on sqlite is always _ public bool DeleteIndex(string indexName) { - string sql = $""" + using var stmt = new SQLiteStmt(db, $""" DROP INDEX "{Table}_{indexName}" - """; + """); - return sqlite3_exec(db, sql) is SQLITE_OK; + return stmt.Execute(null, static r => true); } - #endregion - - #region Query - - // todo: Implement query capabilities. - - #endregion - - #region View - - // todo: Implement view usage. - - #endregion - /// public void Dispose() { diff --git a/src/NoSQLite/SQLiteStmt.cs b/src/NoSQLite/SQLiteStmt.cs index f4d6710..1ddbee6 100644 --- a/src/NoSQLite/SQLiteStmt.cs +++ b/src/NoSQLite/SQLiteStmt.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Collections; +using System.Text.Json; namespace NoSQLite; @@ -7,18 +8,193 @@ internal sealed class SQLiteStmt : IDisposable private readonly sqlite3 db; private readonly sqlite3_stmt stmt; +#if NET9_0_OR_GREATER + private readonly Lock locker = new(); +#else + private readonly object locker = new(); +#endif + + /// + /// Initializes a new instance of the class using a SQL string. + /// + /// The SQLite database connection to use for this statement. + /// The SQL statement to prepare and execute as a string. + /// An optional list to which this statement will be added for disposal management. + public SQLiteStmt(sqlite3 db, string sql, List? disposables = null) + { + this.db = db; + sqlite3_prepare_v2(db, sql, out stmt); + disposables?.Add(this); + } + /// - /// Initialize a new . + /// Initializes a new instance of the class using a SQL byte span. /// - /// The database that runs the statements. - /// The sql to execute. - public SQLiteStmt(sqlite3 db, string sql) + /// The SQLite database connection to use for this statement. + /// The SQL statement to prepare and execute as a . + /// An optional list to which this statement will be added for disposal management. + public SQLiteStmt(sqlite3 db, ReadOnlySpan sql, List? disposables = null) { this.db = db; sqlite3_prepare_v2(db, sql, out stmt); + disposables?.Add(this); + } + + /// + /// Executes the SQL statement by stepping through it. + /// + /// An optional delegate that binds parameters to the statement before execution. + /// Indicates whether exceptions should be thrown on errors. + /// The statement is locked during execution to ensure thread safety. The statement is reset after execution. + public void Execute(SQLiteWriterFunc? bind, bool shouldThrow = true) + { + lock (locker) + { + using var step = new SQLiteStep(db, stmt, bind, static read => null, shouldThrow); + } + } + + /// + /// Executes the SQL statement and returns a result using the provided reader function. + /// + /// The type of the result returned by the reader function. + /// An optional delegate that binds parameters to the statement before execution. + /// A delegate that processes the and returns a result of type . + /// Indicates whether exceptions should be thrown on errors. + /// The result produced by the delegate after executing the statement. + /// The statement is locked during execution to ensure thread safety. The statement is reset after execution. + public TResult Execute(SQLiteWriterFunc? bind, SQLiteReaderFunc read, bool shouldThrow = true) + { + lock (locker) + { + using var step = new SQLiteStep(db, stmt, bind, read, shouldThrow); + return step.Result; + } + } + + /// + /// Executes the SQL statement and returns an array of results using the provided reader function. + /// + /// The type of the result returned by the reader function. + /// An optional delegate that binds parameters to the statement before execution. + /// A delegate that processes the and returns a result of type . + /// Indicates whether exceptions should be thrown on errors. + /// + /// An array of results produced by the delegate for each row returned by the statement. + /// + /// + /// The statement is locked during execution to ensure thread safety. The statement is reset after execution. + /// + public TResult[] ExecuteMany(SQLiteWriterFunc? bind, SQLiteReaderFunc read, bool shouldThrow = true) + { + lock (locker) + { + using var steps = new SQLiteSteps(db, stmt, bind, read, shouldThrow); + var results = new List(); + while (steps.MoveNext()) + { + results.Add(steps.Current); + } + return [.. results]; + } + } + + /// + /// Finalize this statement. + /// + public void Dispose() + { + sqlite3_finalize(stmt); + } + + private readonly ref struct SQLiteStep : IDisposable + { + private readonly sqlite3_stmt stmt; + + public TResult Result { get; } + + public SQLiteStep(sqlite3 db, sqlite3_stmt stmt, SQLiteWriterFunc? bind, SQLiteReaderFunc read, bool shouldThrow) + { + this.stmt = stmt; + bind?.Invoke(new(stmt)); + var result = shouldThrow ? sqlite3_step(stmt).CheckResult(db, $"") : sqlite3_step(stmt); + Result = read(new(stmt, result)); + if (bind is { }) + { + sqlite3_clear_bindings(stmt); + } + } + + public void Dispose() + { + sqlite3_reset(stmt); + } + } + + private ref struct SQLiteSteps : IEnumerator, IDisposable + { + private readonly sqlite3 db; + private readonly sqlite3_stmt stmt; + private readonly SQLiteReaderFunc read; + private readonly bool shouldThrow; + + public TResult Current { get; private set; } = default!; + + public SQLiteSteps(sqlite3 db, sqlite3_stmt stmt, SQLiteWriterFunc? bind, SQLiteReaderFunc read, bool shouldThrow) + { + this.db = db; + this.stmt = stmt; + this.read = read; + this.shouldThrow = shouldThrow; + bind?.Invoke(new(stmt)); + } + + public bool MoveNext() + { + var result = shouldThrow ? sqlite3_step(stmt).CheckResult(db, $"") : sqlite3_step(stmt); + if (result is not SQLITE_ROW) + { + return false; + } + Current = read(new(stmt, result)); + return true; + } + + public readonly void Reset() + { + throw new NotSupportedException(); + } + + public readonly void Dispose() + { + sqlite3_reset(stmt); + sqlite3_clear_bindings(stmt); + } + + readonly object IEnumerator.Current => Current!; + } +} + +#if NET9_0_OR_GREATER +internal readonly ref struct SQLiteParameterBinder +#else +internal readonly struct SQLiteParameterBinder +#endif +{ + private readonly sqlite3_stmt stmt; + + public SQLiteParameterBinder(sqlite3_stmt stmt) + { + this.stmt = stmt; } - #region Bind + /// + /// Binds a blob value to the specified parameter index in the statement. + /// + /// Index starts from 1. + /// The parameter index to bind to, starting from 1. + /// The blob value to bind as a . + public void Blob(int index, ReadOnlySpan value) => sqlite3_bind_blob(stmt, index, value); /// /// Bind text to a parameter. @@ -26,7 +202,7 @@ public SQLiteStmt(sqlite3 db, string sql) /// Index starts from 1. /// The parameter index to bind to, starting from 1. /// The value to bind. - public void BindText(int index, string value) => sqlite3_bind_text(stmt, index, value); + public void Text(int index, string value) => sqlite3_bind_text(stmt, index, value); /// /// Bind text to a parameter. @@ -34,11 +210,48 @@ public SQLiteStmt(sqlite3 db, string sql) /// Index starts from 1. /// The parameter index to bind to, starting from 1. /// The value to bind. - public void BindText(int index, ReadOnlySpan value) => sqlite3_bind_text(stmt, index, value); + public void Text(int index, ReadOnlySpan value) => sqlite3_bind_text(stmt, index, value); - #endregion + /// + /// Bind an integer value to a parameter. + /// + /// Index starts from 1. + /// The parameter index to bind to, starting from 1. + /// The integer value to bind. + public void Int(int index, int value) => sqlite3_bind_int(stmt, index, value); + + /// + /// Clear all bound parameters for the current statement. + /// + /// + /// This method clears all parameter bindings, allowing the statement to be reused with new values. + /// + public void Clear() => sqlite3_clear_bindings(stmt); +} + +#if NET9_0_OR_GREATER +internal delegate void SQLiteWriterFunc(TWriter writer) where TWriter : allows ref struct; +#else +internal delegate void SQLiteWriterFunc(TWriter writer); +#endif + +#if NET9_0_OR_GREATER +internal readonly ref struct SQLiteResultReader +#else +internal readonly struct SQLiteResultReader +#endif +{ + private readonly sqlite3_stmt stmt; - #region Column + public int Result { get; } + + public SQLiteResultReader(sqlite3_stmt stmt, int result) + { + this.stmt = stmt; + Result = result; + } + + public int Count() => sqlite3_column_count(stmt); /// /// Get the value of a number column as . @@ -46,7 +259,7 @@ public SQLiteStmt(sqlite3 db, string sql) /// Index starts from 0. /// The column to read the value from, starting from 0. /// The column value as . - public int ColumnInt(int index) => sqlite3_column_int(stmt, index); + public int Int(int index) => sqlite3_column_int(stmt, index); /// /// Get the value of a number column as . @@ -54,7 +267,7 @@ public SQLiteStmt(sqlite3 db, string sql) /// Index starts from 0. /// The column to read the value from, starting from 0. /// The column value as . - public long ColumnLong(int index) => sqlite3_column_int64(stmt, index); + public long Long(int index) => sqlite3_column_int64(stmt, index); /// /// Get the value of a text column as . @@ -62,7 +275,7 @@ public SQLiteStmt(sqlite3 db, string sql) /// Index starts from 0. /// The column to read the value from, starting from 0. /// The column value as . - public string ColumnText(int index) => sqlite3_column_text(stmt, index).utf8_to_string(); + public string Text(int index) => sqlite3_column_text(stmt, index).utf8_to_string(); /// /// Get the value of a text/blob column as a series of . @@ -70,7 +283,7 @@ public SQLiteStmt(sqlite3 db, string sql) /// Index starts from 0. /// The column to read the value from, starting from 0. /// The column value as a series of . - public ReadOnlySpan ColumnBlob(int index) => sqlite3_column_blob(stmt, index); + public ReadOnlySpan Blob(int index) => sqlite3_column_blob(stmt, index); /// /// Get the value of a text/blob column and deserialize it to the specified type . @@ -83,9 +296,9 @@ public SQLiteStmt(sqlite3 db, string sql) /// /// Thrown if the JSON is invalid. /// Thrown if the type is not supported. - public T? ColumnDeserialize(int index, JsonSerializerOptions? jsonOptions = null) + public T? Deserialize(int index, JsonSerializerOptions? jsonOptions = null) { - var bytes = ColumnBlob(index); + var bytes = Blob(index); var value = JsonSerializer.Deserialize(bytes, jsonOptions); return value; } @@ -101,9 +314,9 @@ public SQLiteStmt(sqlite3 db, string sql) /// /// Thrown if the JSON is invalid. /// Thrown if the type is not supported. - public JsonDocument? ColumnDeserializeDocument(int index, JsonSerializerOptions? jsonOptions = null) + public JsonDocument? DeserializeDocument(int index, JsonSerializerOptions? jsonOptions = null) { - var bytes = ColumnBlob(index); + var bytes = Blob(index); var value = JsonSerializer.Deserialize(bytes, jsonOptions); return value; } @@ -119,40 +332,16 @@ public SQLiteStmt(sqlite3 db, string sql) /// /// Thrown if the JSON is invalid. /// Thrown if the type is not supported. - public JsonElement? ColumnDeserializeElement(int index, JsonSerializerOptions? jsonOptions = null) + public JsonElement? DeserializeElement(int index, JsonSerializerOptions? jsonOptions = null) { - var bytes = ColumnBlob(index); + var bytes = Blob(index); var value = JsonSerializer.Deserialize(bytes, jsonOptions); return value; } - - #endregion - - /// - /// Execute the SQL statement. Depending on the SQL this - /// can be executed multiple times returning different results - /// for different sql statements. - /// - public int Step() => sqlite3_step(stmt); - - /// - /// Reset this back to the start - /// as if it was just created. - /// - /// This will lose all progress before reset was called. - public int Reset() => sqlite3_reset(stmt); - - /// - /// Get the error message the database has currently emitted. - /// - /// The error message from the database. - public string Error() => sqlite3_errmsg(db).utf8_to_string(); - - /// - /// Finalize this statement. - /// - public void Dispose() - { - sqlite3_finalize(stmt); - } } + +#if NET9_0_OR_GREATER +internal delegate TResult SQLiteReaderFunc(TReader reader) where TReader : allows ref struct; +#else +internal delegate TResult SQLiteReaderFunc(TReader reader); +#endif diff --git a/src/NoSQLite/Utilities.cs b/src/NoSQLite/Utilities.cs index 07f5e37..b44ccad 100644 --- a/src/NoSQLite/Utilities.cs +++ b/src/NoSQLite/Utilities.cs @@ -1,28 +1,122 @@ -using System.Runtime.CompilerServices; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; namespace NoSQLite; -using static SQLitePCL.raw; - -internal static class Throw +/// +/// Provides extension methods for SQLite result checking. +/// +internal static class Extensions { - public static void KeyNotFound(bool condition, [InterpolatedStringHandlerArgument("condition")] ref ConditionInterpolation message) + /// + /// Checks the SQLite result code and throws a if the result indicates an error. + /// + /// The SQLite database handle. + /// The SQLite result code to check. + /// The interpolated message to include in the exception if thrown. + /// The original if no error is detected. + /// Thrown when the result code indicates an error. + public static int CheckResult(this sqlite3 db, int result, [InterpolatedStringHandlerArgument("result")] ref SQLiteCodeInterpolation message) { - if (condition) + if (message.ShouldThrow) { - throw new KeyNotFoundException(message.ToString()); + throw new NoSQLiteException($"{message.ToString()}. SQLite info, code: {result}, message: {sqlite3_errmsg(db).utf8_to_string()}"); } + return result; } -} -internal static class Extensions -{ - public static void CheckResult(this sqlite3 db, int result, [InterpolatedStringHandlerArgument("result")] ref SQLiteCodeInterpolation message) + /// + /// Checks the SQLite result code and throws a if the result indicates an error. + /// + /// The SQLite result code to check. + /// The SQLite database handle. + /// The interpolated message to include in the exception if thrown. + /// The original if no error is detected. + /// Thrown when the result code indicates an error. + public static int CheckResult(this int result, sqlite3 db, [InterpolatedStringHandlerArgument("result")] ref SQLiteCodeInterpolation message) { if (message.ShouldThrow) { throw new NoSQLiteException($"{message.ToString()}. SQLite info, code: {result}, message: {sqlite3_errmsg(db).utf8_to_string()}"); } + return result; + } + + /// + /// Checks the SQLite result code and throws a if the result indicates an error. + /// + /// The SQLite result code to check. + /// The instance associated with the operation. + /// The interpolated message to include in the exception if thrown. + /// The original if no error is detected. + /// Thrown when the result code indicates an error. + public static int CheckResult(this int result, NoSQLiteConnection connection, [InterpolatedStringHandlerArgument("result")] ref SQLiteCodeInterpolation message) + { + if (message.ShouldThrow) + { + throw new NoSQLiteException($"{message.ToString()}. SQLite info, code: {result}, message: {sqlite3_errmsg(connection.db).utf8_to_string()}"); + } + return result; + } + + /// + /// Extracts the property name from a lambda expression representing a property accessor. + /// + /// The type containing the property. + /// The type of the property. + /// An expression representing a property accessor, e.g., x => x.Property. + /// The name of the property accessed in the expression. + /// Thrown when the expression does not represent a property access. + public static string GetPropertyName(this Expression> expression) + { + if (expression.Body is MemberExpression memberExpression) + { + return memberExpression.Member.Name; + } + + if (expression.Body is UnaryExpression unaryExpression && unaryExpression.Operand is MemberExpression operand) + { + return operand.Member.Name; + } + + throw new ArgumentException("Invalid expression. Expected a property access expression.", nameof(expression)); + } + + /// + /// Extracts the full property path from a lambda expression representing a property accessor. + /// + /// The type containing the property. + /// The type of the property. + /// An expression representing a property accessor, e.g., x => x.Nested.Property. + /// The full path of the property accessed in the expression, e.g., "Nested.Property". + /// Thrown when the expression does not represent a property access. + public static string GetPropertyPath(this Expression> expression) + { + static string BuildPath(Expression? expr) + { + if (expr is MemberExpression memberExpression) + { + var parentPath = BuildPath(memberExpression.Expression); + return string.IsNullOrEmpty(parentPath) + ? memberExpression.Member.Name + : $"{parentPath}.{memberExpression.Member.Name}"; + } + + if (expr is UnaryExpression unaryExpression) + { + return BuildPath(unaryExpression.Operand); + } + + return string.Empty; + } + + var path = BuildPath(expression.Body); + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentException("Invalid expression. Expected a property access expression.", nameof(expression)); + } + + return path; } } diff --git a/src/NoSQLite/sqlite3_mixins.cs b/src/NoSQLite/sqlite3_mixins.cs new file mode 100644 index 0000000..ba7d7ba --- /dev/null +++ b/src/NoSQLite/sqlite3_mixins.cs @@ -0,0 +1,68 @@ +using System.Diagnostics.CodeAnalysis; + +namespace NoSQLite; + +/// +/// Provides extension methods for the sqlite3 type to simplify database creation and disposal. +/// +[SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Matching the lib name.")] +public static class sqlite3_mixins +{ + extension(sqlite3) + { + /// + /// Creates or opens an SQLite database at the specified path. + /// Optionally enables Write-Ahead Logging (WAL) mode. + /// + /// The file path to the SQLite database. + /// If , enables WAL journal mode. Default is . + /// An instance of representing the opened database. + /// Thrown if is or empty. + /// Thrown if the database could not be opened. + public static sqlite3 Create(string databasePath, bool useWal = true) + { + ArgumentException.ThrowIfNullOrEmpty(databasePath); + + var result = sqlite3_open(databasePath, out var db); + db.CheckResult(result, $"Could not open or create database file: {databasePath}"); + + if (useWal) + { + sqlite3_exec(db, "PRAGMA journal_mode=WAL;"); + } + return db; + } + } + + extension(sqlite3 db) + { + /// + /// Executes a write-ahead log (WAL) checkpoint for the database. + /// + /// See for more info. + public void Checkpoint(string name) + { + sqlite3_wal_checkpoint(db, name); + } + + /// + /// Executes a write-ahead log (WAL) checkpoint for the database. + /// + /// See for more info. + public (int LogSize, int FramesCheckPointed) CheckpointV2(string name, int eMode) + { + sqlite3_wal_checkpoint_v2(db, name, eMode, out var logSize, out var framesCheckPointed); + return (logSize, framesCheckPointed); + } + + /// + /// Closes and disposes the specified sqlite3 database connection. + /// + /// The sqlite3 database connection to dispose. + public void CloseAndDispose() + { + sqlite3_close_v2(db); + db.Dispose(); + } + } +} diff --git a/test/NoSQLite.Test/CRUD.cs b/test/NoSQLite.Test/CRUD.cs deleted file mode 100644 index 3752ffb..0000000 --- a/test/NoSQLite.Test/CRUD.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System.Text.Json; - -namespace NoSQLite.Test; - -public sealed class CRUD : TestBase -{ - private readonly Dictionary seed; - - public CRUD() - { - seed = new PersonFaker().Generate(10).ToDictionary(x => x.Email); - } - - [Test] - public async Task Insert() - { - var table = GetTable(); - - table.Insert("0", seed.First()); - } - - [Test] - public async Task InsertMany() - { - var table = GetTable(); - var peopleBytes = seed.ToDictionary(x => x.Key, pair => JsonSerializer.SerializeToUtf8Bytes(pair.Value)); - - table.InsertMany(seed); - table.InsertBytesMany(peopleBytes); - } - - [Test] - public async Task Count() - { - var table = GetTable(seed); - - var count = table.Count(); - var longCount = table.LongCount(); - - await That(count).IsEqualTo(seed.Count); - await That(longCount).IsEqualTo(seed.Count); - } - - [Test] - public async Task All() - { - var table = GetTable(seed); - - var people = table.All(); - var peopleBytes = table.AllBytes(); - - await That(people).IsEquivalentTo(seed.Values); - await That(peopleBytes.Select(p => JsonSerializer.Deserialize(p))).IsEquivalentTo(seed.Values); - } - - [Test] - public async Task Exists() - { - var table = GetTable(seed); - var person = seed.First(); - - var exists = table.Exists(person.Key); - - await That(exists).IsTrue(); - } - - [Test] - public async Task Find() - { - var table = GetTable(seed); - var pair = seed.First(); - var person = pair.Value; - - var found = table.Find(pair.Key); - var foundBytes = table.FindBytes(pair.Key); - - await That(found).IsEqualTo(person); - await That(JsonSerializer.Deserialize(foundBytes)).IsEqualTo(person); - } - - [Test] - public async Task FindMany() - { - var table = GetTable(seed); - - var people = table.FindMany(seed.Keys); - var peopleBytes = table.FindBytesMany(seed.Keys); - - await That(people).IsEquivalentTo(seed.Values); - await That(peopleBytes.Select(p => JsonSerializer.Deserialize(p))).IsEquivalentTo(seed.Values); - } - - [Test] - public async Task FindPairs() - { - var table = GetTable(seed); - - var people = table.FindPairs(seed.Keys); - var peopleBytes = table.FindBytesPairs(seed.Keys); - - await That(people).IsEquivalentTo(seed); - await That(peopleBytes.ToDictionary(p => p.Key, p => JsonSerializer.Deserialize(p.Value))).IsEquivalentTo(seed); - } - - [Test] - public async Task Clear() - { - var table = GetTable(seed); - - await That(table.Count()).IsNotEqualTo(0); - - table.Clear(); - - await That(table.Count()).IsNotEqualTo(0); - } -} diff --git a/test/NoSQLite.Test/Connection.cs b/test/NoSQLite.Test/Connection.cs new file mode 100644 index 0000000..727815a --- /dev/null +++ b/test/NoSQLite.Test/Connection.cs @@ -0,0 +1,23 @@ +namespace NoSQLite.Test; + +public sealed class Connection : TestBase +{ + [Test] + [Arguments("MyDocuments")] + [Arguments("123 My Documents")] + [Arguments("!@# -- __ -- // --")] + [Arguments(" ")] + [Arguments("")] + public async Task Create_And_Drop_Table(string tableName) + { + await That(Connection.TableExists(tableName)).IsFalse(); + + Connection.CreateTable(tableName); + + await That(Connection.TableExists(tableName)).IsTrue(); + + Connection.DropTable(tableName); + + await That(Connection.TableExists(tableName)).IsFalse(); + } +} diff --git a/test/NoSQLite.Test/Data/TestPerson.cs b/test/NoSQLite.Test/Data/TestPerson.cs index 7edb1bd..7fe7a81 100644 --- a/test/NoSQLite.Test/Data/TestPerson.cs +++ b/test/NoSQLite.Test/Data/TestPerson.cs @@ -4,13 +4,15 @@ namespace NoSQLite.Test.Data; public sealed record TestPerson { - public string Email { get; set; } + public int Id { get; set; } - public string Name { get; set; } + public string Email { get; set; } = string.Empty; - public string Surname { get; set; } + public string Name { get; set; } = string.Empty; - public string Phone { get; set; } + public string Surname { get; set; } = string.Empty; + + public string Phone { get; set; } = string.Empty; public DateOnly Birthdate { get; set; } } @@ -19,6 +21,7 @@ public sealed class PersonFaker : Faker { public PersonFaker() { + RuleFor(x => x.Id, f => f.IndexFaker); RuleFor(x => x.Email, f => f.Person.Email); RuleFor(x => x.Name, f => f.Person.FirstName); RuleFor(x => x.Surname, f => f.Person.LastName); diff --git a/test/NoSQLite.Test/Indexing.cs b/test/NoSQLite.Test/Indexing.cs deleted file mode 100644 index 25e41f7..0000000 --- a/test/NoSQLite.Test/Indexing.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace NoSQLite.Test; - -public sealed class Indexing : TestBase -{ - [Test] - public async Task Exists() - { - var table = GetTable(); - - var create = table.CreateIndex("test", "email"); - await That(create).IsTrue(); - - var exists = table.IndexExists("test"); - var notExists = table.IndexExists("test_1"); - - await That(exists).IsTrue(); - await That(notExists).IsFalse(); - } - - [Test] - public async Task Create() - { - var table = GetTable(); - - var notExists = table.CreateIndex("test", "email"); - var exists = table.CreateIndex("test", "email"); - - await That(exists).IsTrue(); - await That(notExists).IsFalse(); - } - - [Test] - public async Task Recreate() - { - var table = GetTable(); - - table.RecreateIndex("test", "email"); - - await That(table.IndexExists("test")).IsTrue(); - } - - [Test] - public async Task Delete() - { - var table = GetTable(); - - var create = table.CreateIndex("test", "email"); - await That(create).IsTrue(); - - var exists = table.DeleteIndex("test"); - var notExists = table.DeleteIndex("test"); - - await That(exists).IsTrue(); - await That(notExists).IsFalse(); - } -} diff --git a/test/NoSQLite.Test/NoSQLite.Test.csproj b/test/NoSQLite.Test/NoSQLite.Test.csproj index 0ec9d6f..3b16a5a 100644 --- a/test/NoSQLite.Test/NoSQLite.Test.csproj +++ b/test/NoSQLite.Test/NoSQLite.Test.csproj @@ -7,6 +7,7 @@ + diff --git a/test/NoSQLite.Test/Operations.cs b/test/NoSQLite.Test/Operations.cs deleted file mode 100644 index 7dd8f1a..0000000 --- a/test/NoSQLite.Test/Operations.cs +++ /dev/null @@ -1,112 +0,0 @@ -namespace NoSQLite.Test; - -public sealed class Operations : TestBase -{ - [Test] - public async Task CreateTable() - { - var table = nameof(CreateTable); - - await That(Connection.TableExists(table)).IsFalse(); - - Connection.CreateTable(table); - - await That(Connection.TableExists(table)).IsTrue(); - - var t = Connection.GetTable(table); - - t.CreateIndex("name", "name"); - t.Insert(new { id = "1", name = 10 }); - } - - [Test] - public async Task DropTable() - { - var table = nameof(DropTable); - - await That(Connection.TableExists(table)).IsFalse(); - - Connection.CreateTable(table); - - await That(Connection.TableExists(table)).IsTrue(); - - Connection.DropTable(table); - - await That(Connection.TableExists(table)).IsFalse(); - } - - [Test] - public async Task DropAndCreateTable() - { - var table = nameof(DropAndCreateTable); - - await That(Connection.TableExists(table)).IsFalse(); - - Connection.CreateTable(table); - - await That(Connection.TableExists(table)).IsTrue(); - - var dbTable = Connection.GetTable(table); - dbTable.Insert(new PersonFaker().Generate()); - await That(dbTable.Count()).IsEqualTo(1); - dbTable.Dispose(); - dbTable = null; - - Connection.DropAndCreateTable(table); - await That(Connection.TableExists(table)).IsTrue(); - - dbTable = Connection.GetTable(table); - await That(dbTable.Count()).IsEqualTo(0); - } - - [Test] - public async Task Checkpoint() - { - var dir = Path.Combine(Environment.CurrentDirectory, $"{nameof(Operations)}_{nameof(Checkpoint)}_dir"); - var path = Path.Combine(dir, $"db.sqlite3"); - - if (Directory.Exists(dir)) Directory.Delete(dir, true); - Directory.CreateDirectory(dir); - - var connection = new NoSQLiteConnection(path); - - await That(Directory.GetFiles(dir).Length).IsEqualTo(1); - - var table = connection.GetTable(); - table.Insert(new PersonFaker().Generate()); - - await That(Directory.GetFiles(dir).Length).IsEqualTo(3); - - connection.Checkpoint(); - - await That(Directory.GetFiles(dir).Length).IsEqualTo(1); - - connection.Dispose(); - Directory.Delete(dir, true); - } - - [Test] - public async Task Dispose_Test() - { - var dir = Path.Combine(Environment.CurrentDirectory, $"{nameof(Operations)}_Dispose_dir"); - var path = Path.Combine(dir, $"db.sqlite3"); - - if (Directory.Exists(dir)) Directory.Delete(dir, true); - Directory.CreateDirectory(dir); - - var connection = new NoSQLiteConnection(path); - - await That(Directory.GetFiles(dir).Length).IsEqualTo(1); - - var table = connection.GetTable(); - table.Insert(new PersonFaker().Generate()); - - await That(Directory.GetFiles(dir).Length).IsEqualTo(3); - - connection.Dispose(); - - await That(Directory.GetFiles(dir).Length).IsEqualTo(1); - - Directory.Delete(dir, true); - } -} diff --git a/test/NoSQLite.Test/Queries.cs b/test/NoSQLite.Test/Queries.cs deleted file mode 100644 index e8b1e10..0000000 --- a/test/NoSQLite.Test/Queries.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NoSQLite.Test; - -public sealed class Queries : TestBase -{ - public Queries() - { - } -} diff --git a/test/NoSQLite.Test/Table.cs b/test/NoSQLite.Test/Table.cs new file mode 100644 index 0000000..958a355 --- /dev/null +++ b/test/NoSQLite.Test/Table.cs @@ -0,0 +1,81 @@ +namespace NoSQLite.Test; + +public sealed class Table : TestBase +{ + [Test] + public async Task CRUD() + { + var people = new PersonFaker().Generate(10); + var table = Connection.GetTable("crud"); + + // Insert + foreach (var person in people) + { + table.Insert(person); + } + + // Count, LongCount, All + await That(table.Count()).IsEqualTo(10); + await That(table.LongCount()).IsEqualTo(10); + await That(table.All().Length).IsEqualTo(10); + + // Exists + var id = 5; + await That(table.Exists(p => p.Id, id)).IsTrue(); + + // Find + var person5 = await That(table.Find(p => p.Id, id)).IsNotNull(); + + // Update + person5.Name = "test"; + table.Update(person5, p => p.Id); + + // Select (todo) + await That(table.FindProperty(p => p.Id, p => p.Name, id)).IsEqualTo("test"); + + // Remove, (Assert) Exists, Count, LongCount, All + table.Remove(p => p.Id, id); + await That(table.Exists(p => p.Id, id)).IsFalse(); + await That(table.Count()).IsEqualTo(9); + await That(table.LongCount()).IsEqualTo(9); + await That(table.All().Length).IsEqualTo(9); + + // Clear, (Assert) Count, LongCount, All + table.Clear(); + await That(table.Count()).IsEqualTo(0); + await That(table.LongCount()).IsEqualTo(0); + await That(table.All().Length).IsEqualTo(0); + } + + [Test] + [Arguments("MyIndex")] + [Arguments("123 My Index")] + [Arguments("!@# -- __ -- // --")] + [Arguments(" ")] + [Arguments("")] + public async Task Index(string indexName) + { + var table = Connection.GetTable("indexes"); + + await That(table.IndexExists(indexName)).IsFalse(); + + table.CreateIndex(p => p.Id, indexName); + + await That(table.IndexExists(indexName)).IsTrue(); + + // test plan index + using var planStmt = new SQLiteStmt(Db, """ + EXPLAIN QUERY PLAN + SELECT * + FROM "indexes" + WHERE "documents"->'$.Id' = '10'; + """); + + var result = planStmt.Execute(null, r => r.Text(3)); + await That(result).IsEqualTo($"SEARCH indexes USING INDEX indexes_{indexName} (=?)"); + + table.DeleteIndex(indexName); + + await That(table.IndexExists(indexName)).IsFalse(); + } +} diff --git a/test/NoSQLite.Test/_setup.cs b/test/NoSQLite.Test/_setup.cs index 70099ce..bd113d2 100644 --- a/test/NoSQLite.Test/_setup.cs +++ b/test/NoSQLite.Test/_setup.cs @@ -1,4 +1,5 @@ -using System.Runtime.CompilerServices; +using SQLitePCL; +using System.Runtime.CompilerServices; namespace NoSQLite.Test; @@ -11,46 +12,52 @@ public static void Initialize() } } -public abstract class TestBase +public abstract class TestBase { - public static string DbPath { get; private set; } = null!; + protected string DbPath { get; private set; } = null!; - public static NoSQLiteConnection Connection { get; private set; } = null!; + protected sqlite3 Db { get; private set; } = null!; - public static bool Delete { get; set; } = true; + protected NoSQLiteConnection Connection { get; private set; } = null!; - [Before(Class)] - public static async Task BeforeAsync() + private bool Delete { get; } = true; + + [Before(HookType.Test)] + public async Task BeforeAsync() { - DbPath = Path.Combine(Environment.CurrentDirectory, $"{typeof(TSelf).Name}.sqlite3"); + var dir = Path.Combine(Environment.CurrentDirectory, "databases"); + var now = TimeProvider.System.GetTimestamp(); + DbPath = Path.Combine(dir, $"{GetType().Name}_{now}.sqlite3"); - if (File.Exists(DbPath)) + if (!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + if (Delete && File.Exists(DbPath)) { File.Delete(DbPath); } - Connection = new NoSQLiteConnection(DbPath); + Batteries_V2.Init(); + Db = sqlite3.Create(DbPath, useWal: true); + Connection = new NoSQLiteConnection(Db); - await That(File.Exists(Connection.Path)).IsTrue(); - } - - public static NoSQLiteTable GetTable([CallerMemberName] string? caller = null) - { - return Connection.GetTable($"{caller}_{Guid.NewGuid()}"); + await That(File.Exists(DbPath)).IsTrue(); } - public static NoSQLiteTable GetTable(IDictionary initPairs, [CallerMemberName] string? caller = null) + public NoSQLiteTable GetTable([CallerMemberName] string? caller = null) { - var table = GetTable(caller); - table.InsertMany(initPairs); - return table; + return Connection.GetTable($"{caller}"); } - [After(Assembly)] - public static async Task AfterAsync() + [After(HookType.Test)] + public async Task AfterAsync() { Connection.Dispose(); + Db.CloseAndDispose(); await That(Connection.Tables.Count).IsEqualTo(0); + await That(File.Exists($"{DbPath}-shm")).IsFalse(); + await That(File.Exists($"{DbPath}-wal")).IsFalse(); if (Delete) { From 6278c7a40d2eb2987db61a0eaf2ff20420fc972a Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Sat, 18 Oct 2025 01:44:02 +0300 Subject: [PATCH 06/17] add: unique index --- src/NoSQLite/NoSQLiteTable.cs | 4 +-- test/NoSQLite.Test/Table.cs | 55 ++++++++++++++++++++++++++++++++--- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/NoSQLite/NoSQLiteTable.cs b/src/NoSQLite/NoSQLiteTable.cs index 1ea247d..4845109 100644 --- a/src/NoSQLite/NoSQLiteTable.cs +++ b/src/NoSQLite/NoSQLiteTable.cs @@ -190,13 +190,13 @@ SELECT name FROM "sqlite_master" return stmt.Execute(b => b.Text(1, index), static r => r.Result is SQLITE_ROW, shouldThrow: false); } - public void CreateIndex(Expression> selector, string indexName) + public void CreateIndex(Expression> selector, string indexName, bool unique = false) { var propertyPath = selector.GetPropertyPath(); // can't use parameter (?) here so we have to create a new statement every time. using var stmt = new SQLiteStmt(db, $""" - CREATE INDEX IF NOT EXISTS "{Table}_{indexName}" + CREATE{(unique ? " UNIQUE" : "")} INDEX IF NOT EXISTS "{Table}_{indexName}" ON "{Table}" ("documents"->'$.{propertyPath}') """); diff --git a/test/NoSQLite.Test/Table.cs b/test/NoSQLite.Test/Table.cs index 958a355..ec10575 100644 --- a/test/NoSQLite.Test/Table.cs +++ b/test/NoSQLite.Test/Table.cs @@ -55,7 +55,8 @@ public async Task CRUD() [Arguments("")] public async Task Index(string indexName) { - var table = Connection.GetTable("indexes"); + var tableName = "index"; + var table = Connection.GetTable(tableName); await That(table.IndexExists(indexName)).IsFalse(); @@ -64,18 +65,64 @@ public async Task Index(string indexName) await That(table.IndexExists(indexName)).IsTrue(); // test plan index - using var planStmt = new SQLiteStmt(Db, """ + using var planStmt = new SQLiteStmt(Db, $""" EXPLAIN QUERY PLAN SELECT * - FROM "indexes" + FROM "{tableName}" WHERE "documents"->'$.Id' = '10'; """); var result = planStmt.Execute(null, r => r.Text(3)); - await That(result).IsEqualTo($"SEARCH indexes USING INDEX indexes_{indexName} (=?)"); + await That(result).IsEqualTo($"SEARCH {tableName} USING INDEX {tableName}_{indexName} (=?)"); table.DeleteIndex(indexName); await That(table.IndexExists(indexName)).IsFalse(); } + + [Test] + public async Task Index_Unique() + { + var indexName = "id"; + var tableName = "unique"; + var table = Connection.GetTable(tableName); + + await That(table.IndexExists(indexName)).IsFalse(); + + table.CreateIndex(p => p.Id, indexName, unique: true); + + await That(table.IndexExists(indexName)).IsTrue(); + + // test plan index + using var planStmt = new SQLiteStmt(Db, $""" + EXPLAIN QUERY PLAN + SELECT * + FROM "{tableName}" + WHERE "documents"->'$.Id' = '10'; + """); + + var result = planStmt.Execute(null, r => r.Text(3)); + await That(result).IsEqualTo($"SEARCH {tableName} USING INDEX {tableName}_{indexName} (=?)"); + + // insert two times + var personFaker = new PersonFaker(); + var person = personFaker.Generate(); + await That(() => table.Insert(person)).ThrowsNothing(); + + // second time throws + await That(() => table.Insert(person)).Throws(); + + // count remains only one person + await That(table.Count()).IsEqualTo(1); + + // delete index + table.DeleteIndex(indexName); + await That(table.IndexExists(indexName)).IsFalse(); + + // insert doesnt throw + await That(() => table.Insert(person)).ThrowsNothing(); + + // count goes up to two people + await That(table.Count()).IsEqualTo(2); + } } From b0c6f287057371b760772b21806d322d17ad1a2a Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Sun, 19 Oct 2025 15:52:25 +0300 Subject: [PATCH 07/17] refactor: SQLiteStmt and add new methods - include JsonSerialzierOptions - improve write/read using the JsonSerializerOptions - add table Insert, Replace, Set --- src/NoSQLite/NoSQLiteConnection.cs | 9 +- src/NoSQLite/NoSQLiteException.cs | 24 ++-- src/NoSQLite/NoSQLiteTable.cs | 167 ++++++++++++++++++-------- src/NoSQLite/SQLiteStmt.cs | 92 +++++++++----- src/NoSQLite/Utilities.cs | 11 +- src/NoSQLite/_usings.cs | 2 + test/NoSQLite.Test/Data/TestPerson.cs | 10 +- test/NoSQLite.Test/Parallelism.cs | 11 -- test/NoSQLite.Test/Table.cs | 68 +++++++++-- test/NoSQLite.Test/_setup.cs | 6 +- test/NoSQLite.Test/_usings.cs | 2 + 11 files changed, 277 insertions(+), 125 deletions(-) delete mode 100644 test/NoSQLite.Test/Parallelism.cs diff --git a/src/NoSQLite/NoSQLiteConnection.cs b/src/NoSQLite/NoSQLiteConnection.cs index 9fb237d..0428572 100644 --- a/src/NoSQLite/NoSQLiteConnection.cs +++ b/src/NoSQLite/NoSQLiteConnection.cs @@ -1,4 +1,5 @@ -using System.Buffers; +using System; +using System.Buffers; using System.Collections.Concurrent; using System.Text.Json; @@ -39,7 +40,7 @@ public NoSQLiteConnection(sqlite3 db, JsonSerializerOptions? jsonOptions = null) Version = sqlite3_libversion().utf8_to_string(); JsonOptions = jsonOptions; - tableExistsStmt = new(() => new SQLiteStmt(db, """ + tableExistsStmt = new(() => new SQLiteStmt(this.db, JsonOptions, """ SELECT name FROM "sqlite_master" WHERE type='table' AND name = ?; """u8)); @@ -89,7 +90,7 @@ public bool TableExists(string table) /// Thrown if the table cannot be created. public void CreateTable(string table) { - using var stmt = new SQLiteStmt(db, $""" + using var stmt = new SQLiteStmt(db, JsonOptions, $""" CREATE TABLE IF NOT EXISTS "{table}" ( "documents" JSON NOT NULL ); @@ -104,7 +105,7 @@ public void CreateTable(string table) /// Thrown if the table cannot be dropped. public void DropTable(string table) { - using var stmt = new SQLiteStmt(db, $""" + using var stmt = new SQLiteStmt(db, JsonOptions, $""" DROP TABLE IF EXISTS "{table}"; """); stmt.Execute(b => b.Text(1, table)); diff --git a/src/NoSQLite/NoSQLiteException.cs b/src/NoSQLite/NoSQLiteException.cs index 770f98c..e3595b1 100644 --- a/src/NoSQLite/NoSQLiteException.cs +++ b/src/NoSQLite/NoSQLiteException.cs @@ -1,4 +1,5 @@ -using System.Runtime.CompilerServices; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; namespace NoSQLite; @@ -20,8 +21,7 @@ internal NoSQLiteException(string message) : base(message) /// /// The message that describes the error. /// The exception that is the cause of the current exception. - internal NoSQLiteException(string message, Exception inner) - : base(message, inner) + internal NoSQLiteException(string message, Exception inner) : base(message, inner) { } @@ -40,16 +40,20 @@ internal static void If(bool condition, [InterpolatedStringHandlerArgument("cond } /// - /// Throws a with the specified message if the condition is true. + /// Throws a with a message indicating that a document for the specified key was not found, + /// if the provided is null. /// - /// The condition to evaluate. If true, the exception is thrown. - /// The interpolated message to include in the exception if thrown. - /// Thrown when is true. - internal static void KeyNotFound(bool condition, [InterpolatedStringHandlerArgument("condition")] ref ConditionInterpolation message) + /// The type of the item being checked. + /// The type of the key used to identify the item. + /// The item to check for existence. If null, the exception is thrown. + /// The key associated with the item. + /// Thrown when is null. + internal static void KeyNotFound([NotNull] T? item, TKey key) { - if (condition) + if (item is null) { - throw new KeyNotFoundException(message.ToString()); + var msg = $"Could not find document for key {key}"; + throw new NoSQLiteException(msg, new KeyNotFoundException(msg)); } } } diff --git a/src/NoSQLite/NoSQLiteTable.cs b/src/NoSQLite/NoSQLiteTable.cs index 4845109..98ac165 100644 --- a/src/NoSQLite/NoSQLiteTable.cs +++ b/src/NoSQLite/NoSQLiteTable.cs @@ -1,12 +1,9 @@ -using System.Linq.Expressions; -using System.Text.Json; - -namespace NoSQLite; +namespace NoSQLite; [Preserve(AllMembers = true)] public sealed class NoSQLiteTable : IDisposable { - private readonly List disposables = new(6); + private readonly List disposables = []; private readonly sqlite3 db; internal NoSQLiteTable(string table, NoSQLiteConnection connection) @@ -27,7 +24,11 @@ internal NoSQLiteTable(string table, NoSQLiteConnection connection) public JsonSerializerOptions? JsonOptions => Connection.JsonOptions; - private SQLiteStmt CountStmt => field ??= new(db, disposables: disposables, sql: $""" + internal SQLiteStmt NewStmt(string sql) => new(db, JsonOptions, sql, disposables); + + internal SQLiteStmt NewStmt(ReadOnlySpan sql) => new(db, JsonOptions, sql, disposables); + + private SQLiteStmt CountStmt => field ??= NewStmt($""" SELECT count(*) FROM "{Table}" """); @@ -36,27 +37,25 @@ public int Count() return CountStmt.Execute(null, static r => r.Int(0)); } - private SQLiteStmt LongCountStmt => field ??= new(db, disposables: disposables, sql: $""" + private SQLiteStmt LongCountStmt => field ??= NewStmt($""" SELECT count(*) FROM "{Table}" """); public long LongCount() { - var table = Table; return LongCountStmt.Execute(null, static r => r.Long(0)); } - private SQLiteStmt AllStmt => field ??= new(db, disposables: disposables, sql: $""" + private SQLiteStmt AllStmt => field ??= NewStmt($""" SELECT "documents" FROM "{Table}" """); public T[] All() { - var jsonOptions = JsonOptions; - return AllStmt.ExecuteMany(null, r => r.Deserialize(0, jsonOptions)!); + return AllStmt.ExecuteMany(null, static r => r.Deserialize(0)!); } - private SQLiteStmt ClearStmt => field ??= new(db, disposables: disposables, sql: $""" + private SQLiteStmt ClearStmt => field ??= NewStmt($""" DELETE FROM "{Table}" """); @@ -65,14 +64,14 @@ public void Clear() ClearStmt.Execute(null); } - private SQLiteStmt ExistsStmt => field ??= new(db, disposables: disposables, sql: $""" + private SQLiteStmt ExistsStmt => field ??= NewStmt($""" SELECT count(*) FROM "{Table}" WHERE "documents"->('$.' || ?) = ?; """); public bool Exists(Expression> selector, TKey key) { - var propertyPath = selector.GetPropertyPath(); + var propertyPath = selector.GetPropertyPath(JsonOptions); var jsonKey = JsonSerializer.SerializeToUtf8Bytes(key, JsonOptions); return ExistsStmt.Execute( @@ -85,7 +84,7 @@ public bool Exists(Expression> selector, TKey key) ); } - private SQLiteStmt FindStmt => field ??= new(db, disposables: disposables, sql: $""" + private SQLiteStmt FindStmt => field ??= NewStmt($""" SELECT "documents" FROM "{Table}" WHERE "documents"->('$.' || ?) = ? @@ -93,29 +92,32 @@ public bool Exists(Expression> selector, TKey key) public T Find(Expression> selector, TKey key) { - var propertyPath = selector.GetPropertyPath(); + var propertyPath = selector.GetPropertyPath(JsonOptions); var jsonKey = JsonSerializer.SerializeToUtf8Bytes(key, JsonOptions); - return FindStmt.Execute( + var found = FindStmt.Execute( b => { b.Text(1, propertyPath); b.Text(2, jsonKey); }, - r => r.Deserialize(0, JsonOptions)! + static r => r.Deserialize(0) ); + + NoSQLiteException.KeyNotFound(found, key); + return found; } - private SQLiteStmt FindPropertyStmt => field ??= new(db, disposables: disposables, sql: $""" + private SQLiteStmt FindPropertyStmt => field ??= NewStmt($""" SELECT "documents"->('$.' || ?) FROM "{Table}" WHERE "documents"->('$.' || ?) = ? """); - public TProperty FindProperty(Expression> keySelector, Expression> propertySelector, TKey key) + public TProperty? FindProperty(Expression> keySelector, Expression> propertySelector, TKey key) { - var keyPropertyPath = keySelector.GetPropertyPath(); - var propertyPath = propertySelector.GetPropertyPath(); + var keyPropertyPath = keySelector.GetPropertyPath(JsonOptions); + var propertyPath = propertySelector.GetPropertyPath(JsonOptions); var jsonKey = JsonSerializer.SerializeToUtf8Bytes(key, JsonOptions); return FindPropertyStmt.Execute( @@ -125,62 +127,125 @@ public TProperty FindProperty(Expression> keyS b.Text(2, keyPropertyPath); b.Text(3, jsonKey); }, - r => r.Deserialize(0, JsonOptions)! + static r => r.Deserialize(0) ); } - private SQLiteStmt InsertStmt => field ??= new(db, disposables: disposables, sql: $""" - INSERT INTO "{Table}"("documents") VALUES (?) + private SQLiteStmt AddStmt => field ??= NewStmt($""" + INSERT INTO "{Table}"("documents") VALUES (json(?)) """); - public void Insert(T obj) + public void Add(T obj) { var document = JsonSerializer.SerializeToUtf8Bytes(obj, JsonOptions); - InsertStmt.Execute(b => b.Blob(1, document)); + AddStmt.Execute(b => b.Blob(1, document)); } + private SQLiteStmt UpdateStmt => field ??= NewStmt($""" + UPDATE "{Table}" + SET "documents" = json(?) + WHERE "documents"->('$.' || ?) = ?; + """); + public void Update(T document, Expression> selector) { - using var stmt = new SQLiteStmt(db, $""" - UPDATE "{Table}" - SET "documents" = ? - WHERE "documents"->('$.' || ?) = ?; - """); - - var propertyPath = selector.GetPropertyPath(); + var propertyPath = selector.GetPropertyPath(JsonOptions); var key = selector.Compile().Invoke(document); - var jsonDocument = JsonSerializer.SerializeToUtf8Bytes(document, JsonOptions); - var jsonKey = JsonSerializer.SerializeToUtf8Bytes(key, JsonOptions); - stmt.Execute(b => + UpdateStmt.Execute(b => { - b.Blob(1, jsonDocument); + b.JsonBlob(1, document); b.Text(2, propertyPath); - b.Text(3, jsonKey); + b.JsonText(3, key); }); } + private SQLiteStmt RemoveStmt => field ??= NewStmt($""" + DELETE FROM "{Table}" + WHERE "documents"->('$.' || ?) = ?; + """); + public void Remove(Expression> selector, TKey key) { - using var stmt = new SQLiteStmt(db, $""" - DELETE FROM "{Table}" - WHERE "documents"->('$.' || ?) = ?; - """); + var propertyPath = selector.GetPropertyPath(JsonOptions); - var propertyPath = selector.GetPropertyPath(); - var jsonKey = JsonSerializer.SerializeToUtf8Bytes(key, JsonOptions); + RemoveStmt.Execute(b => + { + b.Text(1, propertyPath); + b.JsonText(2, key); + }); + } + + // https://sqlite.org/json1.html#jins + private SQLiteStmt InsertStmt => field ??= NewStmt($""" + UPDATE "{Table}" + SET "documents" = json_insert("documents", ('$.' || ?), json(?)) + WHERE "documents"->('$.' || ?) = ? + """); + + public void Insert(Expression> keySelector, Expression> propertySelector, TKey key, TProperty? value) + { + var keyPropertyPath = keySelector.GetPropertyPath(JsonOptions); + var propertyPath = propertySelector.GetPropertyPath(JsonOptions); + + // cant know if it actaully inserted or not + InsertStmt.Execute(b => + { + b.Text(1, propertyPath); + b.JsonBlob(2, value); + b.Text(3, keyPropertyPath); + b.JsonText(4, key); + }); + } + + // https://sqlite.org/json1.html#jins + private SQLiteStmt ReplaceStmt => field ??= NewStmt($""" + UPDATE "{Table}" + SET "documents" = json_replace("documents", ('$.' || ?), json(?)) + WHERE "documents"->('$.' || ?) = ? + """); + + public void Replace(Expression> keySelector, Expression> propertySelector, TKey key, TProperty? value) + { + var keyPropertyPath = keySelector.GetPropertyPath(JsonOptions); + var propertyPath = propertySelector.GetPropertyPath(JsonOptions); + + // cant know if it actaully replaced or not + ReplaceStmt.Execute(b => + { + b.Text(1, propertyPath); + b.JsonBlob(2, value); + b.Text(3, keyPropertyPath); + b.JsonText(4, key); + }); + } + + // https://sqlite.org/json1.html#jins + private SQLiteStmt SetStmt => field ??= NewStmt($""" + UPDATE "{Table}" + SET "documents" = json_set("documents", ('$.' || ?), json(?)) + WHERE "documents"->('$.' || ?) = ? + """); + + public void Set(Expression> keySelector, Expression> propertySelector, TKey key, TProperty? value) + { + var keyPropertyPath = keySelector.GetPropertyPath(JsonOptions); + var propertyPath = propertySelector.GetPropertyPath(JsonOptions); - stmt.Execute(b => + // will set no matter what so failure will throw + SetStmt.Execute(b => { b.Text(1, propertyPath); - b.Text(2, jsonKey); + b.JsonBlob(2, value); + b.Text(3, keyPropertyPath); + b.JsonText(4, key); }); } public bool IndexExists(string indexName) { - using var stmt = new SQLiteStmt(db, """ + using var stmt = new SQLiteStmt(db, JsonOptions, """ SELECT name FROM "sqlite_master" WHERE type='index' AND name = ?; """u8); @@ -192,10 +257,10 @@ SELECT name FROM "sqlite_master" public void CreateIndex(Expression> selector, string indexName, bool unique = false) { - var propertyPath = selector.GetPropertyPath(); + var propertyPath = selector.GetPropertyPath(JsonOptions); // can't use parameter (?) here so we have to create a new statement every time. - using var stmt = new SQLiteStmt(db, $""" + using var stmt = new SQLiteStmt(db, JsonOptions, $""" CREATE{(unique ? " UNIQUE" : "")} INDEX IF NOT EXISTS "{Table}_{indexName}" ON "{Table}" ("documents"->'$.{propertyPath}') """); @@ -205,7 +270,7 @@ public void CreateIndex(Expression> selector, string inde public bool DeleteIndex(string indexName) { - using var stmt = new SQLiteStmt(db, $""" + using var stmt = new SQLiteStmt(db, JsonOptions, $""" DROP INDEX "{Table}_{indexName}" """); diff --git a/src/NoSQLite/SQLiteStmt.cs b/src/NoSQLite/SQLiteStmt.cs index 1ddbee6..9f4c60f 100644 --- a/src/NoSQLite/SQLiteStmt.cs +++ b/src/NoSQLite/SQLiteStmt.cs @@ -5,37 +5,44 @@ namespace NoSQLite; internal sealed class SQLiteStmt : IDisposable { - private readonly sqlite3 db; - private readonly sqlite3_stmt stmt; - #if NET9_0_OR_GREATER private readonly Lock locker = new(); #else private readonly object locker = new(); #endif + private readonly sqlite3 db; + private readonly sqlite3_stmt stmt; + private readonly JsonSerializerOptions? jsonOptions; + /// /// Initializes a new instance of the class using a SQL string. + /// Prepares the SQL statement for execution and optionally adds this instance to a disposables list. /// /// The SQLite database connection to use for this statement. + /// Optional JSON serializer options for deserialization operations. /// The SQL statement to prepare and execute as a string. /// An optional list to which this statement will be added for disposal management. - public SQLiteStmt(sqlite3 db, string sql, List? disposables = null) + public SQLiteStmt(sqlite3 db, JsonSerializerOptions? jsonOptions, string sql, List? disposables = null) { this.db = db; + this.jsonOptions = jsonOptions; sqlite3_prepare_v2(db, sql, out stmt); disposables?.Add(this); } /// /// Initializes a new instance of the class using a SQL byte span. + /// Prepares the SQL statement for execution and optionally adds this instance to a disposables list. /// /// The SQLite database connection to use for this statement. + /// Optional JSON serializer options for deserialization operations. /// The SQL statement to prepare and execute as a . /// An optional list to which this statement will be added for disposal management. - public SQLiteStmt(sqlite3 db, ReadOnlySpan sql, List? disposables = null) + public SQLiteStmt(sqlite3 db, JsonSerializerOptions? jsonOptions, ReadOnlySpan sql, List? disposables = null) { this.db = db; + this.jsonOptions = jsonOptions; sqlite3_prepare_v2(db, sql, out stmt); disposables?.Add(this); } @@ -50,7 +57,7 @@ public void Execute(SQLiteWriterFunc? bind, bool shouldTh { lock (locker) { - using var step = new SQLiteStep(db, stmt, bind, static read => null, shouldThrow); + using var step = new SQLiteStep(this, bind, static read => null, shouldThrow); } } @@ -67,7 +74,7 @@ public TResult Execute(SQLiteWriterFunc? bind, S { lock (locker) { - using var step = new SQLiteStep(db, stmt, bind, read, shouldThrow); + using var step = new SQLiteStep(this, bind, read, shouldThrow); return step.Result; } } @@ -89,7 +96,7 @@ public TResult[] ExecuteMany(SQLiteWriterFunc? b { lock (locker) { - using var steps = new SQLiteSteps(db, stmt, bind, read, shouldThrow); + using var steps = new SQLiteSteps(this, bind, read, shouldThrow); var results = new List(); while (steps.MoveNext()) { @@ -107,17 +114,23 @@ public void Dispose() sqlite3_finalize(stmt); } + public static implicit operator sqlite3(SQLiteStmt stmt) => stmt.db; + + public static implicit operator sqlite3_stmt(SQLiteStmt stmt) => stmt.stmt; + + public static implicit operator JsonSerializerOptions(SQLiteStmt stmt) => stmt.jsonOptions; + private readonly ref struct SQLiteStep : IDisposable { - private readonly sqlite3_stmt stmt; + private readonly SQLiteStmt stmt; public TResult Result { get; } - public SQLiteStep(sqlite3 db, sqlite3_stmt stmt, SQLiteWriterFunc? bind, SQLiteReaderFunc read, bool shouldThrow) + public SQLiteStep(SQLiteStmt stmt, SQLiteWriterFunc? bind, SQLiteReaderFunc read, bool shouldThrow) { this.stmt = stmt; bind?.Invoke(new(stmt)); - var result = shouldThrow ? sqlite3_step(stmt).CheckResult(db, $"") : sqlite3_step(stmt); + var result = shouldThrow ? sqlite3_step(stmt).CheckResult(stmt.db, $"") : sqlite3_step(stmt); Result = read(new(stmt, result)); if (bind is { }) { @@ -133,16 +146,14 @@ public void Dispose() private ref struct SQLiteSteps : IEnumerator, IDisposable { - private readonly sqlite3 db; - private readonly sqlite3_stmt stmt; + private readonly SQLiteStmt stmt; private readonly SQLiteReaderFunc read; private readonly bool shouldThrow; public TResult Current { get; private set; } = default!; - public SQLiteSteps(sqlite3 db, sqlite3_stmt stmt, SQLiteWriterFunc? bind, SQLiteReaderFunc read, bool shouldThrow) + public SQLiteSteps(SQLiteStmt stmt, SQLiteWriterFunc? bind, SQLiteReaderFunc read, bool shouldThrow) { - this.db = db; this.stmt = stmt; this.read = read; this.shouldThrow = shouldThrow; @@ -151,7 +162,7 @@ public SQLiteSteps(sqlite3 db, sqlite3_stmt stmt, SQLiteWriterFuncThe value to bind. public void Text(int index, ReadOnlySpan value) => sqlite3_bind_text(stmt, index, value); + /// + /// Bind an object to a parameter as json using . + /// + /// The type of the value. + /// Index starts from 1. + /// The parameter index to bind to, starting from 1. + /// The value to bind. + public void JsonBlob(int index, T value) + { + var json = JsonSerializer.SerializeToUtf8Bytes(value, stmt); + Blob(index, json); + } + + /// + /// Bind an object to a parameter as json using >. + /// + /// The type of the value. + /// Index starts from 1. + /// The parameter index to bind to, starting from 1. + /// The value to bind. + public void JsonText(int index, T value) + { + var json = JsonSerializer.SerializeToUtf8Bytes(value, stmt); + Text(index, json); + } + /// /// Bind an integer value to a parameter. /// @@ -241,11 +278,11 @@ internal readonly ref struct SQLiteResultReader internal readonly struct SQLiteResultReader #endif { - private readonly sqlite3_stmt stmt; + private readonly SQLiteStmt stmt; public int Result { get; } - public SQLiteResultReader(sqlite3_stmt stmt, int result) + public SQLiteResultReader(SQLiteStmt stmt, int result) { this.stmt = stmt; Result = result; @@ -290,16 +327,15 @@ public SQLiteResultReader(sqlite3_stmt stmt, int result) /// /// The type to deserialize the column value to. /// The column to read the value from, starting from 0. - /// The serializer options to use, or null for default options. /// /// The column value deserialized as , or null if the value is null. /// /// Thrown if the JSON is invalid. /// Thrown if the type is not supported. - public T? Deserialize(int index, JsonSerializerOptions? jsonOptions = null) + public T? Deserialize(int index) { var bytes = Blob(index); - var value = JsonSerializer.Deserialize(bytes, jsonOptions); + var value = bytes.IsEmpty ? default : JsonSerializer.Deserialize(bytes, stmt); return value; } @@ -308,16 +344,15 @@ public SQLiteResultReader(sqlite3_stmt stmt, int result) /// /// Index starts from 0. /// The column to read the value from, starting from 0. - /// The serializer options to use, or null for default options. /// /// The column value deserialized as a , or null if the value is null. /// /// Thrown if the JSON is invalid. /// Thrown if the type is not supported. - public JsonDocument? DeserializeDocument(int index, JsonSerializerOptions? jsonOptions = null) + public JsonDocument? DeserializeDocument(int index) { var bytes = Blob(index); - var value = JsonSerializer.Deserialize(bytes, jsonOptions); + var value = bytes.IsEmpty ? default : JsonSerializer.Deserialize(bytes, stmt); return value; } @@ -326,16 +361,15 @@ public SQLiteResultReader(sqlite3_stmt stmt, int result) /// /// Index starts from 0. /// The column to read the value from, starting from 0. - /// The serializer options to use, or null for default options. /// /// The column value deserialized as a , or null if the value is null. /// /// Thrown if the JSON is invalid. /// Thrown if the type is not supported. - public JsonElement? DeserializeElement(int index, JsonSerializerOptions? jsonOptions = null) + public JsonElement? DeserializeElement(int index) { var bytes = Blob(index); - var value = JsonSerializer.Deserialize(bytes, jsonOptions); + var value = bytes.IsEmpty ? default : JsonSerializer.Deserialize(bytes, stmt); return value; } } diff --git a/src/NoSQLite/Utilities.cs b/src/NoSQLite/Utilities.cs index b44ccad..c7309c2 100644 --- a/src/NoSQLite/Utilities.cs +++ b/src/NoSQLite/Utilities.cs @@ -1,5 +1,6 @@ using System.Linq.Expressions; using System.Runtime.CompilerServices; +using System.Text.Json; namespace NoSQLite; @@ -67,16 +68,16 @@ public static int CheckResult(this int result, NoSQLiteConnection connection, [I /// An expression representing a property accessor, e.g., x => x.Property. /// The name of the property accessed in the expression. /// Thrown when the expression does not represent a property access. - public static string GetPropertyName(this Expression> expression) + public static string GetPropertyName(this Expression> expression, JsonSerializerOptions? jsonOptions) { if (expression.Body is MemberExpression memberExpression) { - return memberExpression.Member.Name; + return jsonOptions?.PropertyNamingPolicy?.ConvertName(memberExpression.Member.Name) ?? memberExpression.Member.Name; } if (expression.Body is UnaryExpression unaryExpression && unaryExpression.Operand is MemberExpression operand) { - return operand.Member.Name; + return jsonOptions?.PropertyNamingPolicy?.ConvertName(operand.Member.Name) ?? operand.Member.Name; } throw new ArgumentException("Invalid expression. Expected a property access expression.", nameof(expression)); @@ -90,7 +91,7 @@ public static string GetPropertyName(this Expression> exp /// An expression representing a property accessor, e.g., x => x.Nested.Property. /// The full path of the property accessed in the expression, e.g., "Nested.Property". /// Thrown when the expression does not represent a property access. - public static string GetPropertyPath(this Expression> expression) + public static string GetPropertyPath(this Expression> expression, JsonSerializerOptions? jsonOptions) { static string BuildPath(Expression? expr) { @@ -116,7 +117,7 @@ static string BuildPath(Expression? expr) throw new ArgumentException("Invalid expression. Expected a property access expression.", nameof(expression)); } - return path; + return jsonOptions?.PropertyNamingPolicy?.ConvertName(path) ?? path; } } diff --git a/src/NoSQLite/_usings.cs b/src/NoSQLite/_usings.cs index 870bd32..3789e8a 100644 --- a/src/NoSQLite/_usings.cs +++ b/src/NoSQLite/_usings.cs @@ -1,2 +1,4 @@ global using SQLitePCL; +global using System.Text.Json; +global using System.Linq.Expressions; global using static SQLitePCL.raw; diff --git a/test/NoSQLite.Test/Data/TestPerson.cs b/test/NoSQLite.Test/Data/TestPerson.cs index 7fe7a81..28e9c3e 100644 --- a/test/NoSQLite.Test/Data/TestPerson.cs +++ b/test/NoSQLite.Test/Data/TestPerson.cs @@ -1,6 +1,4 @@ -using Bogus; - -namespace NoSQLite.Test.Data; +namespace NoSQLite.Test.Data; public sealed record TestPerson { @@ -15,6 +13,12 @@ public sealed record TestPerson public string Phone { get; set; } = string.Empty; public DateOnly Birthdate { get; set; } + + public bool Sane { get; set; } = true; + + public string? Nonce { get; set; } + + public string? Nonce2 { get; set; } } public sealed class PersonFaker : Faker diff --git a/test/NoSQLite.Test/Parallelism.cs b/test/NoSQLite.Test/Parallelism.cs deleted file mode 100644 index 7182563..0000000 --- a/test/NoSQLite.Test/Parallelism.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace NoSQLite.Test; - -internal class Parallelism -{ -} diff --git a/test/NoSQLite.Test/Table.cs b/test/NoSQLite.Test/Table.cs index ec10575..b275722 100644 --- a/test/NoSQLite.Test/Table.cs +++ b/test/NoSQLite.Test/Table.cs @@ -1,7 +1,22 @@ namespace NoSQLite.Test; +[MethodDataSource(nameof(Arguments))] public sealed class Table : TestBase { + public static IEnumerable> Arguments() => + [ + () => null, + () => JsonSerializerOptions.Default, + () => new(JsonSerializerOptions.Web) { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }, + ]; + + protected override JsonSerializerOptions? JsonOptions { get; } + + public Table(JsonSerializerOptions? jsonOptions) + { + JsonOptions = jsonOptions; + } + [Test] public async Task CRUD() { @@ -11,7 +26,7 @@ public async Task CRUD() // Insert foreach (var person in people) { - table.Insert(person); + table.Add(person); } // Count, LongCount, All @@ -22,17 +37,48 @@ public async Task CRUD() // Exists var id = 5; await That(table.Exists(p => p.Id, id)).IsTrue(); + await That(table.Exists(p => p.Id, 15)).IsFalse(); // Find + await That(() => table.Find(p => p.Id, 15)).Throws().WithInnerException().And.IsTypeOf(); var person5 = await That(table.Find(p => p.Id, id)).IsNotNull(); // Update - person5.Name = "test"; + person5!.Name = "test"; table.Update(person5, p => p.Id); - // Select (todo) + // Select await That(table.FindProperty(p => p.Id, p => p.Name, id)).IsEqualTo("test"); + // Insert/Replace/Set + // Check conditions for where the value doesnt exist works only when options are set to not writting null + if (Connection.JsonOptions?.DefaultIgnoreCondition is JsonIgnoreCondition.WhenWritingNull) + { + // should not replace because it doesnt exist + table.Replace(p => p.Id, p => p.Nonce, id, "r"); + await That(table.FindProperty(p => p.Id, p => p.Nonce, id)).IsEqualTo(null); + + // should insert because it doesnt exist + table.Insert(p => p.Id, p => p.Nonce, id, "inserted"); + await That(table.FindProperty(p => p.Id, p => p.Nonce, id)).IsEqualTo("inserted"); + + // should replace beacuse it exists + table.Replace(p => p.Id, p => p.Nonce, id, "replaced"); + await That(table.FindProperty(p => p.Id, p => p.Nonce, id)).IsEqualTo("replaced"); + + // should set the value even if it deosnt exist + table.Set(p => p.Id, p => p.Nonce2, id, "aaaaa"); + await That(table.FindProperty(p => p.Id, p => p.Nonce2, id)).IsEqualTo("aaaaa"); + } + + // Replace + table.Replace(p => p.Id, p => p.Sane, id, false); + await That(table.FindProperty(p => p.Id, p => p.Sane, id)).IsFalse(); + + // Set (should set regardles of existances or not) + table.Set(p => p.Id, p => p.Name, id, "test from set"); + await That(table.FindProperty(p => p.Id, p => p.Name, id)).IsEqualTo("test from set"); + // Remove, (Assert) Exists, Count, LongCount, All table.Remove(p => p.Id, id); await That(table.Exists(p => p.Id, id)).IsFalse(); @@ -57,6 +103,7 @@ public async Task Index(string indexName) { var tableName = "index"; var table = Connection.GetTable(tableName); + var propertyPath = Extensions.GetPropertyPath(p => p.Id, Connection.JsonOptions); await That(table.IndexExists(indexName)).IsFalse(); @@ -65,11 +112,11 @@ public async Task Index(string indexName) await That(table.IndexExists(indexName)).IsTrue(); // test plan index - using var planStmt = new SQLiteStmt(Db, $""" + using var planStmt = table.NewStmt($""" EXPLAIN QUERY PLAN SELECT * FROM "{tableName}" - WHERE "documents"->'$.Id' = '10'; + WHERE "documents"->'$.{propertyPath}' = '10'; """); var result = planStmt.Execute(null, r => r.Text(3)); @@ -86,6 +133,7 @@ public async Task Index_Unique() var indexName = "id"; var tableName = "unique"; var table = Connection.GetTable(tableName); + var propertyPath = Extensions.GetPropertyPath(p => p.Id, Connection.JsonOptions); await That(table.IndexExists(indexName)).IsFalse(); @@ -94,11 +142,11 @@ public async Task Index_Unique() await That(table.IndexExists(indexName)).IsTrue(); // test plan index - using var planStmt = new SQLiteStmt(Db, $""" + using var planStmt = table.NewStmt($""" EXPLAIN QUERY PLAN SELECT * FROM "{tableName}" - WHERE "documents"->'$.Id' = '10'; + WHERE "documents"->'$.{propertyPath}' = '10'; """); var result = planStmt.Execute(null, r => r.Text(3)); @@ -107,10 +155,10 @@ EXPLAIN QUERY PLAN // insert two times var personFaker = new PersonFaker(); var person = personFaker.Generate(); - await That(() => table.Insert(person)).ThrowsNothing(); + await That(() => table.Add(person)).ThrowsNothing(); // second time throws - await That(() => table.Insert(person)).Throws(); + await That(() => table.Add(person)).Throws(); // count remains only one person await That(table.Count()).IsEqualTo(1); @@ -120,7 +168,7 @@ EXPLAIN QUERY PLAN await That(table.IndexExists(indexName)).IsFalse(); // insert doesnt throw - await That(() => table.Insert(person)).ThrowsNothing(); + await That(() => table.Add(person)).ThrowsNothing(); // count goes up to two people await That(table.Count()).IsEqualTo(2); diff --git a/test/NoSQLite.Test/_setup.cs b/test/NoSQLite.Test/_setup.cs index bd113d2..5c7d379 100644 --- a/test/NoSQLite.Test/_setup.cs +++ b/test/NoSQLite.Test/_setup.cs @@ -20,7 +20,9 @@ public abstract class TestBase protected NoSQLiteConnection Connection { get; private set; } = null!; - private bool Delete { get; } = true; + protected virtual JsonSerializerOptions? JsonOptions { get; } + + private bool Delete { get; } = false; [Before(HookType.Test)] public async Task BeforeAsync() @@ -40,7 +42,7 @@ public async Task BeforeAsync() Batteries_V2.Init(); Db = sqlite3.Create(DbPath, useWal: true); - Connection = new NoSQLiteConnection(Db); + Connection = new NoSQLiteConnection(Db, JsonOptions); await That(File.Exists(DbPath)).IsTrue(); } diff --git a/test/NoSQLite.Test/_usings.cs b/test/NoSQLite.Test/_usings.cs index 8444d68..3d97e9c 100644 --- a/test/NoSQLite.Test/_usings.cs +++ b/test/NoSQLite.Test/_usings.cs @@ -1,3 +1,5 @@ +global using System.Text.Json; +global using System.Text.Json.Serialization; global using NoSQLite.Test.Data; global using Bogus; global using TUnit; From e6632abdca96d6edc28e488789eed4b50690bfc8 Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Sun, 19 Oct 2025 16:21:34 +0300 Subject: [PATCH 08/17] add: Summaries Rename Remove to Delete --- src/NoSQLite/NoSQLiteConnection.cs | 4 +- src/NoSQLite/NoSQLiteTable.cs | 140 ++++++++++++++++++++++++++++- src/NoSQLite/SQLiteStmt.cs | 57 ++++++++++-- src/NoSQLite/Utilities.cs | 4 +- src/NoSQLite/_usings.cs | 1 + test/NoSQLite.Test/Table.cs | 4 +- 6 files changed, 193 insertions(+), 17 deletions(-) diff --git a/src/NoSQLite/NoSQLiteConnection.cs b/src/NoSQLite/NoSQLiteConnection.cs index 0428572..77aea95 100644 --- a/src/NoSQLite/NoSQLiteConnection.cs +++ b/src/NoSQLite/NoSQLiteConnection.cs @@ -1,7 +1,5 @@ -using System; -using System.Buffers; +using System.Buffers; using System.Collections.Concurrent; -using System.Text.Json; namespace NoSQLite; diff --git a/src/NoSQLite/NoSQLiteTable.cs b/src/NoSQLite/NoSQLiteTable.cs index 98ac165..2d11855 100644 --- a/src/NoSQLite/NoSQLiteTable.cs +++ b/src/NoSQLite/NoSQLiteTable.cs @@ -1,5 +1,8 @@ namespace NoSQLite; +/// +/// Represents a table in a NoSQLite database, providing methods to manage documents and indexes. +/// [Preserve(AllMembers = true)] public sealed class NoSQLiteTable : IDisposable { @@ -18,10 +21,19 @@ internal NoSQLiteTable(string table, NoSQLiteConnection connection) connection.CreateTable(table); } + /// + /// Gets the associated with this table. + /// public NoSQLiteConnection Connection { get; } + /// + /// Gets the name of the table. + /// public string Table { get; } + /// + /// Gets the used for JSON serialization and deserialization. + /// public JsonSerializerOptions? JsonOptions => Connection.JsonOptions; internal SQLiteStmt NewStmt(string sql) => new(db, JsonOptions, sql, disposables); @@ -32,6 +44,10 @@ internal NoSQLiteTable(string table, NoSQLiteConnection connection) SELECT count(*) FROM "{Table}" """); + /// + /// Gets the number of documents in the table. + /// + /// The count of documents as an . public int Count() { return CountStmt.Execute(null, static r => r.Int(0)); @@ -41,6 +57,10 @@ public int Count() SELECT count(*) FROM "{Table}" """); + /// + /// Gets the number of documents in the table as a . + /// + /// The count of documents as a . public long LongCount() { return LongCountStmt.Execute(null, static r => r.Long(0)); @@ -50,6 +70,11 @@ public long LongCount() SELECT "documents" FROM "{Table}" """); + /// + /// Gets all documents in the table as an array of type . + /// + /// The type to deserialize each document to. + /// An array of all documents in the table. public T[] All() { return AllStmt.ExecuteMany(null, static r => r.Deserialize(0)!); @@ -59,6 +84,9 @@ public T[] All() DELETE FROM "{Table}" """); + /// + /// Removes all documents from the table. + /// public void Clear() { ClearStmt.Execute(null); @@ -69,6 +97,14 @@ SELECT count(*) FROM "{Table}" WHERE "documents"->('$.' || ?) = ?; """); + /// + /// Determines whether a document with the specified key exists in the table. + /// + /// The document type. + /// The key type. + /// An expression selecting the key property. + /// The key value to search for. + /// if a document with the specified key exists; otherwise, . public bool Exists(Expression> selector, TKey key) { var propertyPath = selector.GetPropertyPath(JsonOptions); @@ -90,6 +126,15 @@ public bool Exists(Expression> selector, TKey key) WHERE "documents"->('$.' || ?) = ? """); + /// + /// Finds and returns a document by key. + /// + /// The document type. + /// The key type. + /// An expression selecting the key property. + /// The key value to search for. + /// The document matching the specified key. + /// Thrown if the key is not found. public T Find(Expression> selector, TKey key) { var propertyPath = selector.GetPropertyPath(JsonOptions); @@ -114,6 +159,16 @@ public T Find(Expression> selector, TKey key) WHERE "documents"->('$.' || ?) = ? """); + /// + /// Finds and returns a property value from a document by key. + /// + /// The document type. + /// The key type. + /// The property type. + /// An expression selecting the key property. + /// An expression selecting the property to retrieve. + /// The key value to search for. + /// The property value, or null if not found. public TProperty? FindProperty(Expression> keySelector, Expression> propertySelector, TKey key) { var keyPropertyPath = keySelector.GetPropertyPath(JsonOptions); @@ -135,6 +190,11 @@ public T Find(Expression> selector, TKey key) INSERT INTO "{Table}"("documents") VALUES (json(?)) """); + /// + /// Adds a new document to the table. + /// + /// The document type. + /// The document to add. public void Add(T obj) { var document = JsonSerializer.SerializeToUtf8Bytes(obj, JsonOptions); @@ -148,6 +208,13 @@ public void Add(T obj) WHERE "documents"->('$.' || ?) = ?; """); + /// + /// Updates an existing document in the table. + /// + /// The document type. + /// The key type. + /// The updated document. + /// An expression selecting the key property. public void Update(T document, Expression> selector) { var propertyPath = selector.GetPropertyPath(JsonOptions); @@ -161,16 +228,23 @@ public void Update(T document, Expression> selector) }); } - private SQLiteStmt RemoveStmt => field ??= NewStmt($""" + private SQLiteStmt DeleteStmt => field ??= NewStmt($""" DELETE FROM "{Table}" WHERE "documents"->('$.' || ?) = ?; """); - public void Remove(Expression> selector, TKey key) + /// + /// Deletes a document from the table by key. + /// + /// The document type. + /// The key type. + /// An expression selecting the key property. + /// The key value of the document to remove. + public void Delete(Expression> selector, TKey key) { var propertyPath = selector.GetPropertyPath(JsonOptions); - RemoveStmt.Execute(b => + DeleteStmt.Execute(b => { b.Text(1, propertyPath); b.JsonText(2, key); @@ -184,6 +258,20 @@ public void Remove(Expression> selector, TKey key) WHERE "documents"->('$.' || ?) = ? """); + /// + /// Inserts a property value into a document by key. + /// + /// + /// Overwrite if already exists? NO (including values, only the key matters).
+ /// Create if does not exist? YES + ///
+ /// The document type. + /// The key type. + /// The property type. + /// An expression selecting the key property. + /// An expression selecting the property to insert. + /// The key value of the document. + /// The property value to insert. public void Insert(Expression> keySelector, Expression> propertySelector, TKey key, TProperty? value) { var keyPropertyPath = keySelector.GetPropertyPath(JsonOptions); @@ -206,6 +294,20 @@ public void Insert(Expression> keySelector, Ex WHERE "documents"->('$.' || ?) = ? """); + /// + /// Replaces a property value in a document by key. + /// + /// + /// Overwrite if already exists? YES ( values won't remove the key).
+ /// Create if does not exist? NO + ///
+ /// The document type. + /// The key type. + /// The property type. + /// An expression selecting the key property. + /// An expression selecting the property to replace. + /// The key value of the document. + /// The property value to replace. public void Replace(Expression> keySelector, Expression> propertySelector, TKey key, TProperty? value) { var keyPropertyPath = keySelector.GetPropertyPath(JsonOptions); @@ -228,6 +330,20 @@ public void Replace(Expression> keySelector, E WHERE "documents"->('$.' || ?) = ? """); + /// + /// Sets a property value in a document by key. + /// + /// + /// Overwrite if already exists? YES
+ /// Create if does not exist? YES + ///
+ /// The document type. + /// The key type. + /// The property type. + /// An expression selecting the key property. + /// An expression selecting the property to set. + /// The key value of the document. + /// The property value to set. public void Set(Expression> keySelector, Expression> propertySelector, TKey key, TProperty? value) { var keyPropertyPath = keySelector.GetPropertyPath(JsonOptions); @@ -243,6 +359,11 @@ public void Set(Expression> keySelector, Expre }); } + /// + /// Determines whether an index with the specified name exists for this table. + /// + /// The name of the index to check. + /// if the index exists; otherwise, . public bool IndexExists(string indexName) { using var stmt = new SQLiteStmt(db, JsonOptions, """ @@ -255,6 +376,14 @@ SELECT name FROM "sqlite_master" return stmt.Execute(b => b.Text(1, index), static r => r.Result is SQLITE_ROW, shouldThrow: false); } + /// + /// Creates an index on the specified property of the documents in the table. + /// + /// The document type. + /// The key type. + /// An expression selecting the property to index. + /// The name of the index to create. + /// Whether the index should enforce uniqueness. public void CreateIndex(Expression> selector, string indexName, bool unique = false) { var propertyPath = selector.GetPropertyPath(JsonOptions); @@ -268,6 +397,11 @@ public void CreateIndex(Expression> selector, string inde stmt.Execute(null); } + /// + /// Deletes an index with the specified name from this table. + /// + /// The name of the index to delete. + /// if the index was deleted; otherwise, . public bool DeleteIndex(string indexName) { using var stmt = new SQLiteStmt(db, JsonOptions, $""" diff --git a/src/NoSQLite/SQLiteStmt.cs b/src/NoSQLite/SQLiteStmt.cs index 9f4c60f..247769c 100644 --- a/src/NoSQLite/SQLiteStmt.cs +++ b/src/NoSQLite/SQLiteStmt.cs @@ -1,8 +1,9 @@ -using System.Collections; -using System.Text.Json; - -namespace NoSQLite; +namespace NoSQLite; +/// +/// Represents a prepared SQLite statement, providing methods for execution with parameter binding and result retrieval. +/// +[Preserve(AllMembers = true)] internal sealed class SQLiteStmt : IDisposable { #if NET9_0_OR_GREATER @@ -107,25 +108,54 @@ public TResult[] ExecuteMany(SQLiteWriterFunc? b } /// - /// Finalize this statement. + /// Finalizes this statement and releases associated resources. /// public void Dispose() { sqlite3_finalize(stmt); } + /// + /// Implicitly converts a to its underlying database connection. + /// + /// The instance. public static implicit operator sqlite3(SQLiteStmt stmt) => stmt.db; + /// + /// Implicitly converts a to its underlying statement handle. + /// + /// The instance. public static implicit operator sqlite3_stmt(SQLiteStmt stmt) => stmt.stmt; - public static implicit operator JsonSerializerOptions(SQLiteStmt stmt) => stmt.jsonOptions; + /// + /// Implicitly converts a to its associated . + /// + /// The instance. + public static implicit operator JsonSerializerOptions?(SQLiteStmt stmt) => stmt.jsonOptions; + /// + /// Represents a single step execution of a SQLite statement, providing result retrieval and automatic reset. + /// + /// The type of the result returned by the reader function. private readonly ref struct SQLiteStep : IDisposable { + /// + /// The parent instance. + /// private readonly SQLiteStmt stmt; + /// + /// Gets the result produced by the reader function after statement execution. + /// public TResult Result { get; } + /// + /// Initializes a new instance of the struct. + /// + /// The parent instance. + /// An optional delegate that binds parameters to the statement before execution. + /// A delegate that processes the and returns a result of type . + /// Indicates whether exceptions should be thrown on errors. public SQLiteStep(SQLiteStmt stmt, SQLiteWriterFunc? bind, SQLiteReaderFunc read, bool shouldThrow) { this.stmt = stmt; @@ -138,12 +168,19 @@ public SQLiteStep(SQLiteStmt stmt, SQLiteWriterFunc? bind } } + /// + /// Resets the statement after execution. + /// public void Dispose() { sqlite3_reset(stmt); } } + /// + /// Enumerates multiple result rows from an SQLite statement, providing result retrieval and automatic reset. + /// + /// The type of the result returned by the reader function. private ref struct SQLiteSteps : IEnumerator, IDisposable { private readonly SQLiteStmt stmt; @@ -152,6 +189,13 @@ public void Dispose() public TResult Current { get; private set; } = default!; + /// + /// Initializes a new instance of the struct. + /// + /// The parent instance. + /// An optional delegate that binds parameters to the statement before execution. + /// A delegate that processes the and returns a result of type . + /// Indicates whether exceptions should be thrown on errors. public SQLiteSteps(SQLiteStmt stmt, SQLiteWriterFunc? bind, SQLiteReaderFunc read, bool shouldThrow) { this.stmt = stmt; @@ -182,6 +226,7 @@ public readonly void Dispose() sqlite3_clear_bindings(stmt); } + /// readonly object IEnumerator.Current => Current!; } } diff --git a/src/NoSQLite/Utilities.cs b/src/NoSQLite/Utilities.cs index c7309c2..1dc9aad 100644 --- a/src/NoSQLite/Utilities.cs +++ b/src/NoSQLite/Utilities.cs @@ -1,6 +1,4 @@ -using System.Linq.Expressions; -using System.Runtime.CompilerServices; -using System.Text.Json; +using System.Runtime.CompilerServices; namespace NoSQLite; diff --git a/src/NoSQLite/_usings.cs b/src/NoSQLite/_usings.cs index 3789e8a..537a873 100644 --- a/src/NoSQLite/_usings.cs +++ b/src/NoSQLite/_usings.cs @@ -1,4 +1,5 @@ global using SQLitePCL; +global using System.Collections; global using System.Text.Json; global using System.Linq.Expressions; global using static SQLitePCL.raw; diff --git a/test/NoSQLite.Test/Table.cs b/test/NoSQLite.Test/Table.cs index b275722..140a94b 100644 --- a/test/NoSQLite.Test/Table.cs +++ b/test/NoSQLite.Test/Table.cs @@ -79,8 +79,8 @@ public async Task CRUD() table.Set(p => p.Id, p => p.Name, id, "test from set"); await That(table.FindProperty(p => p.Id, p => p.Name, id)).IsEqualTo("test from set"); - // Remove, (Assert) Exists, Count, LongCount, All - table.Remove(p => p.Id, id); + // Delete, (Assert) Exists, Count, LongCount, All + table.Delete(p => p.Id, id); await That(table.Exists(p => p.Id, id)).IsFalse(); await That(table.Count()).IsEqualTo(9); await That(table.LongCount()).IsEqualTo(9); From 561b4740ca915ca9b905c77b2b712de1633aa298 Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Sun, 19 Oct 2025 16:23:58 +0300 Subject: [PATCH 09/17] delete: sample --- NoSQLite.slnx | 3 - sample/ConsoleApp/Benchy.cs | 57 --------------- sample/ConsoleApp/CRUD.cs | 69 ------------------ sample/ConsoleApp/ConsoleApp.csproj | 16 ---- sample/ConsoleApp/Data/Person.cs | 10 --- .../ConsoleApp/Extensions/RangeExtensions.cs | 36 --------- sample/ConsoleApp/INDEX.cs | 23 ------ sample/ConsoleApp/Program.cs | 46 ------------ .../ConsoleApp/Properties/launchSettings.json | 9 --- sample/ConsoleApp/console.sqlite3 | Bin 16384 -> 0 bytes sample/ConsoleApp/console.sqlite3-shm | Bin 32768 -> 0 bytes sample/ConsoleApp/console.sqlite3-wal | 0 12 files changed, 269 deletions(-) delete mode 100644 sample/ConsoleApp/Benchy.cs delete mode 100644 sample/ConsoleApp/CRUD.cs delete mode 100644 sample/ConsoleApp/ConsoleApp.csproj delete mode 100644 sample/ConsoleApp/Data/Person.cs delete mode 100644 sample/ConsoleApp/Extensions/RangeExtensions.cs delete mode 100644 sample/ConsoleApp/INDEX.cs delete mode 100644 sample/ConsoleApp/Program.cs delete mode 100644 sample/ConsoleApp/Properties/launchSettings.json delete mode 100644 sample/ConsoleApp/console.sqlite3 delete mode 100644 sample/ConsoleApp/console.sqlite3-shm delete mode 100644 sample/ConsoleApp/console.sqlite3-wal diff --git a/NoSQLite.slnx b/NoSQLite.slnx index a3f9209..75ca580 100644 --- a/NoSQLite.slnx +++ b/NoSQLite.slnx @@ -1,7 +1,4 @@ - - - diff --git a/sample/ConsoleApp/Benchy.cs b/sample/ConsoleApp/Benchy.cs deleted file mode 100644 index 738a887..0000000 --- a/sample/ConsoleApp/Benchy.cs +++ /dev/null @@ -1,57 +0,0 @@ -using BenchmarkDotNet.Attributes; -using NoSQLite; - -namespace ConsoleApp; - -[MemoryDiagnoser(true)] -public class Benchy -{ - public NoSQLiteConnection Connection { get; set; } = null!; - public NoSQLiteTable WriteTable { get; set; } = null!; - public NoSQLiteTable ReadTable { get; set; } = null!; - - public Person Person { get; set; } = null!; - public string[] PeopleIds { get; set; } = null!; - public Dictionary People { get; set; } = null!; - - [GlobalSetup] - public void Setup() - { - Connection = new(Path.Combine(Environment.CurrentDirectory, "benchmark.sqlite3")); - WriteTable = Connection.GetTable("Write"); - ReadTable = Connection.GetTable("Read"); - - Person = new Person - { - Name = "singe_write", - Description = "A" - }; - - PeopleIds = (0..100).Select(static num => num.ToString()).ToArray(); - - People = PeopleIds.Select(num => new Person - { - Name = num.ToString(), - Description = "Yay", - Surname = "no" - }) - .ToDictionary(static x => x.Name); - - ReadTable.InsertMany(People); - } - - [GlobalCleanup] - public void Cleanup() => Connection.Dispose(); - - [Benchmark] - public Person Read_Singe() => ReadTable.Find("0")!; - - [Benchmark] - public List Read_Many() => ReadTable.FindMany(PeopleIds).ToList(); - - [Benchmark] - public void Write_Singe() => WriteTable.Insert(Person.Name, Person); - - [Benchmark] - public void Write_Many() => WriteTable.InsertMany(People); -} diff --git a/sample/ConsoleApp/CRUD.cs b/sample/ConsoleApp/CRUD.cs deleted file mode 100644 index 59334ee..0000000 --- a/sample/ConsoleApp/CRUD.cs +++ /dev/null @@ -1,69 +0,0 @@ -using NoSQLite; - -namespace ConsoleApp; - -public static class CRUD -{ - public static void Objects(NoSQLiteConnection connection) - { - var db = connection.GetTable(); - // objects - var panos = new Person - { - Name = "panos", - Surname = "athanasiou" - }; - - var john = new Person - { - Name = "john", - Surname = "mandis", - Description = "good friendo!" - }; - - db.Insert("0", panos); - db.Insert("1", john); - - db.InsertMany(new Dictionary - { - ["0"] = panos, - ["1"] = john - }); - - var exists = db.Exists("0"); - - var panos2 = db.Find("0"); - var john2 = db.Find("1"); - - var people = db.FindMany(new[] { "0", "1" }).ToArray(); - - db.Remove("0"); - db.RemoveMany(new[] { "0", "1" }); - - db.Insert("0", panos); - db.Insert("1", john); - } - - public static void Lists(NoSQLiteConnection connection) - { - var db = connection.GetTable(); - - var people = new[] - { - new Person{ Name = "person1", }, - new Person{ Name = "person2", }, - new Person{ Name = "person3", }, - new Person{ Name = "person4", }, - }; - - db.Insert("people", people); - - var exists = db.Exists("people"); - - var people2 = db.Find("people"); - - db.Remove("people"); - - db.Insert("people", people); - } -} diff --git a/sample/ConsoleApp/ConsoleApp.csproj b/sample/ConsoleApp/ConsoleApp.csproj deleted file mode 100644 index c866ae4..0000000 --- a/sample/ConsoleApp/ConsoleApp.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - net9.0 - Exe - - - - - - - - - - - diff --git a/sample/ConsoleApp/Data/Person.cs b/sample/ConsoleApp/Data/Person.cs deleted file mode 100644 index 9dba6f5..0000000 --- a/sample/ConsoleApp/Data/Person.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ConsoleApp.Data; - -public class Person -{ - public string Name { get; set; } - - public string Surname { get; set; } - - public string? Description { get; set; } -} diff --git a/sample/ConsoleApp/Extensions/RangeExtensions.cs b/sample/ConsoleApp/Extensions/RangeExtensions.cs deleted file mode 100644 index 602f5dd..0000000 --- a/sample/ConsoleApp/Extensions/RangeExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace System; - -public static class RangeExtensions -{ - public static RangeEnumerator GetEnumerator(this Range range) => new(range); - - public static IEnumerable Select(this Range range, Func selector) - { - foreach (var num in range) - { - yield return selector(num); - } - } -} - -public struct RangeEnumerator -{ - private int _current; - private readonly int _end; - - public RangeEnumerator(Range range) - { - if (range.End.IsFromEnd) throw new NotSupportedException("From end ranges not supported"); - - _current = range.Start.Value - 1; - _end = range.End.Value; - } - - public int Current => _current; - - public bool MoveNext() - { - _current += 1; - return _current <= _end; - } -} diff --git a/sample/ConsoleApp/INDEX.cs b/sample/ConsoleApp/INDEX.cs deleted file mode 100644 index b12b052..0000000 --- a/sample/ConsoleApp/INDEX.cs +++ /dev/null @@ -1,23 +0,0 @@ -using NoSQLite; - -namespace ConsoleApp; - -public static class INDEX -{ - public static void Execute(NoSQLiteConnection connection) - { - var db = connection.GetTable(); - - var indexName = "name_index"; - - var exists = db.IndexExists(indexName); - - db.CreateIndex(indexName, "name"); - - exists = db.IndexExists(indexName); - - db.DeleteIndex(indexName); - - exists = db.IndexExists(indexName); - } -} diff --git a/sample/ConsoleApp/Program.cs b/sample/ConsoleApp/Program.cs deleted file mode 100644 index 49b51f3..0000000 --- a/sample/ConsoleApp/Program.cs +++ /dev/null @@ -1,46 +0,0 @@ -global using ConsoleApp.Data; -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Running; -using ConsoleApp; -using NoSQLite; -using System.Diagnostics; -using System.Text.Json; -using System.Text.Json.Serialization; - - -//BenchmarkRunner.Run(); - -//return; - -var stopwatch = Stopwatch.StartNew(); - -var connection = new NoSQLiteConnection( - Path.Combine(Environment.CurrentDirectory, "console.sqlite3"), - new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - }); - -stopwatch.Stop(); -Console.WriteLine($"Open elapsed time: {stopwatch.ElapsedMilliseconds}"); - -Console.WriteLine($""" - Path: {connection.Path} - Version: {connection.Version} - """); - -stopwatch.Restart(); - -CRUD.Objects(connection); -CRUD.Lists(connection); -INDEX.Execute(connection); - -stopwatch.Stop(); -Console.WriteLine($"Operation time: {stopwatch.ElapsedMilliseconds}"); - -stopwatch.Restart(); -connection.Dispose(); - -stopwatch.Stop(); -Console.WriteLine($"Close elapsed time: {stopwatch.ElapsedMilliseconds}"); diff --git a/sample/ConsoleApp/Properties/launchSettings.json b/sample/ConsoleApp/Properties/launchSettings.json deleted file mode 100644 index 521dd32..0000000 --- a/sample/ConsoleApp/Properties/launchSettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "profiles": { - "ConsoleApp": { - "commandName": "Project", - "commandLineArgs": "", - "workingDirectory": "." - } - } -} \ No newline at end of file diff --git a/sample/ConsoleApp/console.sqlite3 b/sample/ConsoleApp/console.sqlite3 deleted file mode 100644 index 749bb426f2ec35a66a6029d83bae724215b12dc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeI&O>fgM7zc2tNy~^7%w_6!s;?5`FE9`){AaO!)!R|uF7}Cy@{*PqYdE%G!*OM(j-|Ymc&kjXAaZ}bLH%XyD z9xz4-(Pc5RqRG0LwPm#kmAfo-vOdx*`hyhc8lh|S(~Xap4Z(W|KmY;|fB*y_009U< z00Izzz~2ILQyJZ_X!GSD^!&G;7)>XBm?p!}o%qA^Ipa~!Y4sh}?ml+*SoME>tCqRb zW%--haUw!?cYB*vZ?CHvtM&PccCB2I3%*a?S7Sf#nl}pBD*e{Oj&n8@t63(K!2^%0 zT1DU4>$C2TEQ3ym4Z7{!fz!yksg<*i=elL@pY_^LTfKeu#M!T@DeIP5w+p4RQ_;xc zS0``Aaz{UOr>R&x5AzQk+Ids$?U$6MC5uj*gnp(UWQBkL1Rwwb2tWV=5P$##AOHaf zK;Ts1Mafv5S0i6UWB=eCSEmDfi${JeH|{o{HU3H6yO?TTOl|VnOQ&eixn<|$kHt~Q z8$6lD*%}i!^nygDJUDAbs Date: Sun, 19 Oct 2025 20:58:33 +0300 Subject: [PATCH 10/17] refactor: Dispose methods --- src/NoSQLite/NoSQLiteConnection.cs | 21 +++++++++++++-------- src/NoSQLite/NoSQLiteTable.cs | 28 +++++++++++++++++++++------- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/NoSQLite/NoSQLiteConnection.cs b/src/NoSQLite/NoSQLiteConnection.cs index 77aea95..d03d9b1 100644 --- a/src/NoSQLite/NoSQLiteConnection.cs +++ b/src/NoSQLite/NoSQLiteConnection.cs @@ -110,26 +110,31 @@ public void DropTable(string table) } /// - /// Releases all resources used by the and closes the underlying database connection. + /// Releases all resources used by the . /// /// - /// This will close and dispose the underlying database connection and all associated tables. + /// Disposes all managed instances and prepared statements. /// public void Dispose() { if (!open) return; open = false; - var length = tables.Count; var buffer = ArrayPool.Shared.Rent(length); - tables.Values.CopyTo(buffer, 0); - tables.Clear(); + try + { + tables.Values.CopyTo(buffer, 0); + tables.Clear(); - for (int i = 0; i < length; i++) buffer[i].Dispose(); + for (int i = 0; i < length; i++) buffer[i].Dispose(); - ArrayPool.Shared.Return(buffer, true); + if (tableExistsStmt.IsValueCreated) tableExistsStmt.Value.Dispose(); + } - if (tableExistsStmt.IsValueCreated) tableExistsStmt.Value.Dispose(); + finally + { + ArrayPool.Shared.Return(buffer, true); + } } } diff --git a/src/NoSQLite/NoSQLiteTable.cs b/src/NoSQLite/NoSQLiteTable.cs index 2d11855..84e4d34 100644 --- a/src/NoSQLite/NoSQLiteTable.cs +++ b/src/NoSQLite/NoSQLiteTable.cs @@ -1,4 +1,6 @@ -namespace NoSQLite; +using System.Buffers; + +namespace NoSQLite; /// /// Represents a table in a NoSQLite database, providing methods to manage documents and indexes. @@ -411,16 +413,28 @@ DROP INDEX "{Table}_{indexName}" return stmt.Execute(null, static r => true); } - /// + /// + /// Releases all resources used by the . + /// + /// + /// Disposes all prepared statements. + /// public void Dispose() { - Connection.tables.Remove(Table, out _); if (disposables.Count <= 0) return; - var toDispose = new IDisposable[disposables.Count]; - disposables.CopyTo(toDispose); - disposables.Clear(); + var length = disposables.Count; + var buffer = ArrayPool.Shared.Rent(length); + try + { + disposables.CopyTo(buffer, 0); + disposables.Clear(); - foreach (var d in toDispose) d.Dispose(); + for (int i = 0; i < length; i++) buffer[i].Dispose(); + } + finally + { + ArrayPool.Shared.Return(buffer, true); + } } } From 364edd65671648a55abd63f276ff5799c8a22445 Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Sun, 19 Oct 2025 21:12:58 +0300 Subject: [PATCH 11/17] fix: Table dispose method --- src/NoSQLite/NoSQLiteTable.cs | 22 +++++++++++----------- src/NoSQLite/SQLiteStmt.cs | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/NoSQLite/NoSQLiteTable.cs b/src/NoSQLite/NoSQLiteTable.cs index 84e4d34..283fe7e 100644 --- a/src/NoSQLite/NoSQLiteTable.cs +++ b/src/NoSQLite/NoSQLiteTable.cs @@ -6,9 +6,9 @@ namespace NoSQLite; /// Represents a table in a NoSQLite database, providing methods to manage documents and indexes. /// [Preserve(AllMembers = true)] -public sealed class NoSQLiteTable : IDisposable +public sealed class NoSQLiteTable { - private readonly List disposables = []; + private readonly List statements = []; private readonly sqlite3 db; internal NoSQLiteTable(string table, NoSQLiteConnection connection) @@ -38,9 +38,9 @@ internal NoSQLiteTable(string table, NoSQLiteConnection connection) /// public JsonSerializerOptions? JsonOptions => Connection.JsonOptions; - internal SQLiteStmt NewStmt(string sql) => new(db, JsonOptions, sql, disposables); + internal SQLiteStmt NewStmt(string sql) => new(db, JsonOptions, sql, statements); - internal SQLiteStmt NewStmt(ReadOnlySpan sql) => new(db, JsonOptions, sql, disposables); + internal SQLiteStmt NewStmt(ReadOnlySpan sql) => new(db, JsonOptions, sql, statements); private SQLiteStmt CountStmt => field ??= NewStmt($""" SELECT count(*) FROM "{Table}" @@ -419,22 +419,22 @@ DROP INDEX "{Table}_{indexName}" /// /// Disposes all prepared statements. /// - public void Dispose() + internal void Dispose() { - if (disposables.Count <= 0) return; + if (statements.Count <= 0) return; - var length = disposables.Count; - var buffer = ArrayPool.Shared.Rent(length); + var length = statements.Count; + var buffer = ArrayPool.Shared.Rent(length); try { - disposables.CopyTo(buffer, 0); - disposables.Clear(); + statements.CopyTo(buffer, 0); + statements.Clear(); for (int i = 0; i < length; i++) buffer[i].Dispose(); } finally { - ArrayPool.Shared.Return(buffer, true); + ArrayPool.Shared.Return(buffer, true); } } } diff --git a/src/NoSQLite/SQLiteStmt.cs b/src/NoSQLite/SQLiteStmt.cs index 247769c..cfec3cf 100644 --- a/src/NoSQLite/SQLiteStmt.cs +++ b/src/NoSQLite/SQLiteStmt.cs @@ -24,7 +24,7 @@ internal sealed class SQLiteStmt : IDisposable /// Optional JSON serializer options for deserialization operations. /// The SQL statement to prepare and execute as a string. /// An optional list to which this statement will be added for disposal management. - public SQLiteStmt(sqlite3 db, JsonSerializerOptions? jsonOptions, string sql, List? disposables = null) + public SQLiteStmt(sqlite3 db, JsonSerializerOptions? jsonOptions, string sql, List? disposables = null) { this.db = db; this.jsonOptions = jsonOptions; @@ -40,7 +40,7 @@ public SQLiteStmt(sqlite3 db, JsonSerializerOptions? jsonOptions, string sql, Li /// Optional JSON serializer options for deserialization operations. /// The SQL statement to prepare and execute as a . /// An optional list to which this statement will be added for disposal management. - public SQLiteStmt(sqlite3 db, JsonSerializerOptions? jsonOptions, ReadOnlySpan sql, List? disposables = null) + public SQLiteStmt(sqlite3 db, JsonSerializerOptions? jsonOptions, ReadOnlySpan sql, List? disposables = null) { this.db = db; this.jsonOptions = jsonOptions; From 4f47d732cbc35ab3d27e6b14a5e1aa0dc30082a9 Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Sun, 19 Oct 2025 22:02:26 +0300 Subject: [PATCH 12/17] refactor: test setup --- test/NoSQLite.Test/Table.cs | 5 +---- test/NoSQLite.Test/_setup.cs | 9 +++++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/test/NoSQLite.Test/Table.cs b/test/NoSQLite.Test/Table.cs index 140a94b..541d716 100644 --- a/test/NoSQLite.Test/Table.cs +++ b/test/NoSQLite.Test/Table.cs @@ -10,11 +10,8 @@ public sealed class Table : TestBase () => new(JsonSerializerOptions.Web) { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }, ]; - protected override JsonSerializerOptions? JsonOptions { get; } - - public Table(JsonSerializerOptions? jsonOptions) + public Table(JsonSerializerOptions? jsonOptions) : base(jsonOptions) { - JsonOptions = jsonOptions; } [Test] diff --git a/test/NoSQLite.Test/_setup.cs b/test/NoSQLite.Test/_setup.cs index 5c7d379..98a6a54 100644 --- a/test/NoSQLite.Test/_setup.cs +++ b/test/NoSQLite.Test/_setup.cs @@ -20,9 +20,14 @@ public abstract class TestBase protected NoSQLiteConnection Connection { get; private set; } = null!; - protected virtual JsonSerializerOptions? JsonOptions { get; } + protected JsonSerializerOptions? JsonOptions { get; } - private bool Delete { get; } = false; + private bool Delete { get; } = true; + + protected TestBase(JsonSerializerOptions? jsonOptions = null) + { + JsonOptions = jsonOptions; + } [Before(HookType.Test)] public async Task BeforeAsync() From 04f91a6e025249b0a9cae3630136973d4b6fc2dd Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Sun, 19 Oct 2025 22:07:10 +0300 Subject: [PATCH 13/17] update: README.md --- README.md | 124 ++++++++++++++++++++++++++---------------------------- 1 file changed, 59 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 262e249..a8667a6 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,75 @@ -## NoSQLite: NoSQL on top of SQLite +# NoSQLite [![Release](https://github.com/panoukos41/NoSQLite/actions/workflows/release.yaml/badge.svg)](https://github.com/panoukos41/NoSQLite/actions/workflows/release.yaml) [![NuGet](https://buildstats.info/nuget/P41.NoSQLite?includePreReleases=true)](https://www.nuget.org/packages/P41.NoSQLite) [![MIT License](https://img.shields.io/apm/l/atomic-design-ui.svg?)](https://github.com/panoukos41/NoSQLite/blob/main/LICENSE.md) -A thin wrapper above sqlite using the [`JSON1`](https://www.sqlite.org/json1.html) apis turn it into a [`NOSQL`](https://en.wikipedia.org/wiki/NoSQL) database. +A C# library to use SQLite as a NoSQL database. This library aims to be simple low level methods that you can use to create your own data access layers. -This library references and uses [`SQLitePCLRaw.bundle_e_sqlite3`](https://www.nuget.org/packages/SQLitePCLRaw.bundle_e_sqlite3) version `2.1.2` and later witch ensures that the [`JSON1 APIS`](https://www.sqlite.org/json1.html) are present. +> [!NOTE] +> The library is built using [`SQLitePCL.raw`](https://github.com/ericsink/SQLitePCL.raw). -The library executes `Batteries.Init();` for you when a connection is first initialized. +> [!IMPORTANT] +> To use the library you must ensure you are using an SQLite that contains the [`JSON1`](https://www.sqlite.org/json1.html) extension. The JSON functions and operators are built into SQLite by default, as of SQLite version 3.38.0 (2022-02-22) ## Getting Started -Create an instance of `NoSQLiteConnection`. -```csharp -var connection = new NoSQLiteConnection( - "path to database file", // Required - json_options) // Optional JsonSerializerOptions -``` - -The connection configures the `PRAGMA journal_mode` to be [`WAL`](https://www.sqlite.org/wal.html) - -The connection manages an `sqlite3` object when initialized. *You should always dispose it when you are done so that the databases flashes `WAL` files* but you can also ignore it ¯\ (ツ)/¯. - -## Tables - -You get a table using `connection.GetTable()` you can optionaly provide a table name or leave it as it is to get the default `documents` table. -> Tables are created if they do not exist. - -Tables are managed by their connections so you don't have to worry about disposing. If you request a table multiple times *(eg: the same name)* you will always get the same `Instance`. - -## Document Management - -Example class that will be used below: -```csharp -public class Person -{ - public string Name { get; set; } - public string Surname { get; set; } - public string? Description { get; set; } -} -``` -```csharp -var connection = new NoSQLiteConnection("path to database file", "json options or null"); -``` -```csharp -var docs = connection.GetTable(); -``` - -### Create/Update documents. - -Creating or Updating a document happens from the same `Insert` method, keep in mind that this always replaces the document with the new one. -```csharp -var panos = new Person -{ - Name = "panoukos", - Surname = "41", - Description = "C# dev" -} - -docs.Insert("panos", panos); // If it exists it is now replaced/updated. -``` - -### Get documents. -```csharp -var doc = docs.Get("panos"); // Get the document or null. -``` - -#### Delete documents. -```csharp -docs.Remove("panos"); // Will remove the document. -docs.Remove("panos"); // Will still succeed even if the document doesn't exist. -``` +Install some version of [`SQLitePCL.raw`](https://github.com/ericsink/SQLitePCL.raw) to create your `sqlite3` object. + +> [!TIP] +> You can look at the [test project](./test/NoSQLite.Test/) for more pragmatic usage. The project used `SQLitePCLRaw.bundle_e_sqlite3`. + +### Connection + +Using your `sqlite3` db create a new instances of the `NoSQLiteConnection` and optionally pass a `JsonSerializerOptions` object. + +> [!CAUTION] +> Once you use the connection be very careful on the consequences of switching your `JsonSerializerOptions` object. + +> [!Note] +> Disposing `NoSQLiteConnection will not close your `sqlite3` db or do anything with it. It will just cleanup after itself and it's associated tables and be ready to be discarded. + +### Tables + +You get a table using `connection.GetTable({TableName})` + +> [!NOTE] +> Tables are created if they do not exist. If you request a table multiple times you will always get the same table instance. + +### Document Management + +At the table level the following methods are supported: +| Method | Description | +|- |- | +| Count/CountLong | Returns the number of rows in the table. | +| All | Returns all rows in the table. Deserialized to `T` | +| Clear | Clears the table. | +| Exists | Check if a document exists. | +| Find | Returns the document if it exists or throws. | +| Add | Adds a document. | +| Update | Updates a document (replace). | +| Delete | Deletes a document. | +| IndexExists | Check if an index exists. | +| CreateIndex | Creates an index if it does not exists using `"{TableName}_{IndexName}"` (can also set unique flag). | +| DeleteIndex | Deletes an index if it does exist. | + +At the document level the following methods are supported: +| Method | Description | +|- |- | +| FindProperty | Finds a document by key and returns a property value. | +| Insert | Inserts a property value into a document by key. Overwrite `NO`, Create `YES`. | +| Replace | Replaces a property value in a document by key. Overwrite `YES`, Create `NO`. | +| Set | Sets a property value in a document by key. Overwrite `YES`, Create `YES`. | + +### Examples + +- For connection creation look at the [TestBase Before and After methods of the _setup.cs file](./test/NoSQLite.Test/_setup.cs). +- For CRUD examples look at the [CRUD method of the Table.cs file](./test/NoSQLite.Test/Table.cs). +- For INDEX examples look at the [Index and Index_Unique methods of the Table.cs file](./test/NoSQLite.Test/Table.cs). ## Build -To build this project [.NET 7](https://dotnet.microsoft.com/en-us/download/dotnet/7.0) is needed. +To build this project [.NET 10](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) is needed. ## Contribute From bbb730004d0567ff32e93595259b049401c90c73 Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Sun, 19 Oct 2025 22:13:01 +0300 Subject: [PATCH 14/17] fix: README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a8667a6..38aca6a 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ A C# library to use SQLite as a NoSQL database. This library aims to be simple l Install some version of [`SQLitePCL.raw`](https://github.com/ericsink/SQLitePCL.raw) to create your `sqlite3` object. > [!TIP] -> You can look at the [test project](./test/NoSQLite.Test/) for more pragmatic usage. The project used `SQLitePCLRaw.bundle_e_sqlite3`. +> You can look at the [test project](./test/NoSQLite.Test/) for more pragmatic usage. The test project uses [SQLitePCLRaw.bundle_e_sqlite3](https://www.nuget.org/packages/SQLitePCLRaw.bundle_e_sqlite3) nuget pacakge to use sqlite. ### Connection @@ -27,7 +27,7 @@ Using your `sqlite3` db create a new instances of the `NoSQLiteConnection` and o > Once you use the connection be very careful on the consequences of switching your `JsonSerializerOptions` object. > [!Note] -> Disposing `NoSQLiteConnection will not close your `sqlite3` db or do anything with it. It will just cleanup after itself and it's associated tables and be ready to be discarded. +> Disposing `NoSQLiteConnection` will not close your `sqlite3` db or do anything with it. It will just cleanup it's associated table and statement instances. ### Tables @@ -36,8 +36,6 @@ You get a table using `connection.GetTable({TableName})` > [!NOTE] > Tables are created if they do not exist. If you request a table multiple times you will always get the same table instance. -### Document Management - At the table level the following methods are supported: | Method | Description | |- |- | @@ -53,6 +51,8 @@ At the table level the following methods are supported: | CreateIndex | Creates an index if it does not exists using `"{TableName}_{IndexName}"` (can also set unique flag). | | DeleteIndex | Deletes an index if it does exist. | +### Documents + At the document level the following methods are supported: | Method | Description | |- |- | From ef5b4a1f350a5215dae40f13b6aa081ecf7fd79c Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Sun, 19 Oct 2025 22:51:06 +0300 Subject: [PATCH 15/17] refactor: Tests to run on all tfms --- test/NoSQLite.Test/NoSQLite.Test.csproj | 2 +- test/NoSQLite.Test/Table.cs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test/NoSQLite.Test/NoSQLite.Test.csproj b/test/NoSQLite.Test/NoSQLite.Test.csproj index 3b16a5a..82733f3 100644 --- a/test/NoSQLite.Test/NoSQLite.Test.csproj +++ b/test/NoSQLite.Test/NoSQLite.Test.csproj @@ -1,7 +1,7 @@  - net10.0 + net8.0;net9.0;net10.0 false diff --git a/test/NoSQLite.Test/Table.cs b/test/NoSQLite.Test/Table.cs index 541d716..12d5ff2 100644 --- a/test/NoSQLite.Test/Table.cs +++ b/test/NoSQLite.Test/Table.cs @@ -7,7 +7,11 @@ public sealed class Table : TestBase [ () => null, () => JsonSerializerOptions.Default, +#if NET9_0_OR_GREATER () => new(JsonSerializerOptions.Web) { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }, +#else + () => new(JsonSerializerOptions.Default) { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }, +#endif ]; public Table(JsonSerializerOptions? jsonOptions) : base(jsonOptions) From b6626d5ae2b0b24c87c6ead7ded9e1ad2d426e7f Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Sun, 19 Oct 2025 22:51:33 +0300 Subject: [PATCH 16/17] refactor: global.json to use new MTP runner --- global.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/global.json b/global.json index 93dd0dd..e2031ed 100644 --- a/global.json +++ b/global.json @@ -3,5 +3,8 @@ "version": "10.0.0", "rollForward": "latestMinor", "allowPrerelease": true + }, + "test": { + "runner": "Microsoft.Testing.Platform" } } \ No newline at end of file From cfb16a2c824c428e54e75fe60df941c42f1f4164 Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Sun, 19 Oct 2025 22:59:45 +0300 Subject: [PATCH 17/17] add: workflows and update readme --- .github/Invoke-Process.psm1 | 18 ------------ .github/build.ps1 | 18 ------------ .github/pack.ps1 | 17 ------------ .github/test.ps1 | 18 ------------ .github/workflows/_.yaml | 51 ++++++++++++++++++++++++++++++++++ .github/workflows/build.yaml | 18 ++++++++++++ .github/workflows/publish.yaml | 19 +++++++++++++ README.md | 18 +++++++----- 8 files changed, 99 insertions(+), 78 deletions(-) delete mode 100644 .github/Invoke-Process.psm1 delete mode 100644 .github/build.ps1 delete mode 100644 .github/pack.ps1 delete mode 100644 .github/test.ps1 create mode 100644 .github/workflows/_.yaml create mode 100644 .github/workflows/build.yaml create mode 100644 .github/workflows/publish.yaml diff --git a/.github/Invoke-Process.psm1 b/.github/Invoke-Process.psm1 deleted file mode 100644 index ab406a7..0000000 --- a/.github/Invoke-Process.psm1 +++ /dev/null @@ -1,18 +0,0 @@ -function Invoke-Process($command) { - if ($command -is [string[]]) { - } - elseif ($command -is [string]) { - $command = $command -split " " - } - else { - throw "Invalid command object. Can only accept string[] or string"; - } - - $filePath = $command[0] - $argumentList = $command[1..($command.Length - 1)]; - $process = Start-Process -FilePath $filePath -ArgumentList $argumentList -NoNewWindow -Wait - Write-Host $process - if ($null -ne $process.ExitCode || $process.ExitCode -ne 0) { - exit $process.ExitCode - } -} \ No newline at end of file diff --git a/.github/build.ps1 b/.github/build.ps1 deleted file mode 100644 index b1e7115..0000000 --- a/.github/build.ps1 +++ /dev/null @@ -1,18 +0,0 @@ -[CmdletBinding()] -param ( - [Parameter(Position = 1)] - [ValidateSet("Debug", "Release")] - [string] $configuration = "Release" -) - -$projects = @( - "./src/NoSQLite" - "./test/NoSQLite.Test" -) - -foreach ($project in $projects) { - dotnet ` - build $project ` - -c $configuration ` - -v minimal -} diff --git a/.github/pack.ps1 b/.github/pack.ps1 deleted file mode 100644 index 494d04d..0000000 --- a/.github/pack.ps1 +++ /dev/null @@ -1,17 +0,0 @@ -[CmdletBinding()] - -$projects = @( - "./src/NoSQLite" -) - -$output = "./nuget" - -foreach ($project in $projects) { - dotnet ` - pack $project ` - -c `Release ` - --no-restore ` - --no-build ` - -v minimal ` - -o $output -} diff --git a/.github/test.ps1 b/.github/test.ps1 deleted file mode 100644 index e13e2ea..0000000 --- a/.github/test.ps1 +++ /dev/null @@ -1,18 +0,0 @@ -[CmdletBinding()] -param ( - [Parameter(Position = 0)] - [ValidateSet("Debug", "Release")] - [string] $configuration = "Release" -) - -$projects = @( - "./test/NoSQLite.Test" -) - -foreach ($project in $projects) { - dotnet ` - test $project ` - -c $configuration ` - --no-restore ` - --no-build -} diff --git a/.github/workflows/_.yaml b/.github/workflows/_.yaml new file mode 100644 index 0000000..966da86 --- /dev/null +++ b/.github/workflows/_.yaml @@ -0,0 +1,51 @@ +name: CI/CD +env: { DOTNET_NOLOGO: true } +on: + workflow_call: + inputs: + test: { type: boolean, default: false, description: Run tests. } + publish: { type: boolean, default: false, description: Publish to nuget. Will run all tests too. } + secrets: + nuget-key: +jobs: + pipeline: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Read .NET Version + shell: pwsh + id: dotnet-version + run: | + $version = (Get-Content .\global.json -Raw | ConvertFrom-Json).sdk.version.TrimEnd('0') + 'x' + "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: { dotnet-version: "${{ steps.dotnet-version.outputs.version }}" } + + - name: Build + working-directory: src + shell: pwsh + run: dotnet build NoSQLite -c Release + + - name: Test + if: inputs.test || inputs.publish + working-directory: test + run: dotnet test --project NoSQLite.Test -c Release + + - name: Pack + if: inputs.publish + working-directory: src + run: dotnet pack NoSQLite --no-restore --no-build + + - name: Push + if: inputs.publish + working-directory: artifacts/src/package/release + env: + SOURCE_URL: https://api.nuget.org/v3/index.json + NUGET_AUTH_TOKEN: ${{ secrets.nuget-key }} + run: dotnet nuget push *.nupkg --skip-duplicate -s ${{ env.SOURCE_URL }} -k ${{ env.NUGET_AUTH_TOKEN }} diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..ace39a3 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,18 @@ +name: build +env: { DOTNET_NOLOGO: true } +on: + pull_request: + branches: + - main + paths: + - src/** + - test/** + types: + - opened + - ready_for_review + - review_requested +jobs: + pipeline: + uses: panoukos41/NoSQLite/.github/workflows/_.yaml@main + with: + test: true \ No newline at end of file diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..9210e25 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,19 @@ +name: publish +env: { DOTNET_NOLOGO: true } +on: + workflow_dispatch: + push: + branches: + - main + tags: + - v[0-9]+.[0-9]+.[0-9]+ # Only matches vX.X.X where X is a number + paths: + - src/** + - test/** +jobs: + pipeline: + uses: panoukos41/NoSQLite/.github/workflows/_.yaml@main + with: + publish: true + secrets: + nuget-key: ${{ secrets.NUGET_API_KEY }} diff --git a/README.md b/README.md index 38aca6a..a3955ff 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,20 @@ # NoSQLite -[![Release](https://github.com/panoukos41/NoSQLite/actions/workflows/release.yaml/badge.svg)](https://github.com/panoukos41/NoSQLite/actions/workflows/release.yaml) -[![NuGet](https://buildstats.info/nuget/P41.NoSQLite?includePreReleases=true)](https://www.nuget.org/packages/P41.NoSQLite) -[![MIT License](https://img.shields.io/apm/l/atomic-design-ui.svg?)](https://github.com/panoukos41/NoSQLite/blob/main/LICENSE.md) +[![Build Action](https://github.com/panoukos41/NoSQLite/actions/workflows/build.yaml/badge.svg)](https://github.com/panoukos41/NoSQLite/actions/workflows/build.yaml) +[![Publish Action](https://github.com/panoukos41/NoSQLite/actions/workflows/publish.yaml/badge.svg)](https://github.com/panoukos41/NoSQLite/actions/workflows/publish.yaml) +[![Downloads](https://img.shields.io/nuget/dt/P41.NoSQLite.contracts?style=flat)](https://www.nuget.org/packages/P41.NoSQLite/) -A C# library to use SQLite as a NoSQL database. This library aims to be simple low level methods that you can use to create your own data access layers. +[![License](https://img.shields.io/github/license/panoukos41/NoSQLite?style=flat)](./LICENSE) +[![.NET 8](https://img.shields.io/badge/.NET%208-%23512bd4?style=flat)](https://dotnet.microsoft.com) +[![.NET 9](https://img.shields.io/badge/.NET%209-%23512bd4?style=flat)](https://dotnet.microsoft.com) +[![.NET 10](https://img.shields.io/badge/.NET%2010-%23512bd4?style=flat)](https://dotnet.microsoft.com) -> [!NOTE] -> The library is built using [`SQLitePCL.raw`](https://github.com/ericsink/SQLitePCL.raw). +A C# library built using [`SQLitePCL.raw`](https://github.com/ericsink/SQLitePCL.raw) to use [SQLite](https://sqlite.org) as a [NoSQL](https://en.wikipedia.org/wiki/NoSQL) database. + +The library aims to provide simple low level methods that are used to create your own data access layers. For now the library uses a single `connection` class which creates/uses `tables` that have a single column called `documents`. In reality it should work for other tables with more columns as long as they contain one column called `documents` but no tests have been made or run for this use case. > [!IMPORTANT] -> To use the library you must ensure you are using an SQLite that contains the [`JSON1`](https://www.sqlite.org/json1.html) extension. The JSON functions and operators are built into SQLite by default, as of SQLite version 3.38.0 (2022-02-22) +> To use the library you must ensure you are using an SQLite that contains the [`JSON1`](https://www.sqlite.org/json1.html) extension. As of version 3.38.0 (2022-02-22) the JSON functions and operators are built into SQLite by default. ## Getting Started