diff --git a/Directory.Packages.props b/Directory.Packages.props index c9cedac6..6736eefa 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,7 +2,7 @@ true true - 2.1.1 + 2.1.2 diff --git a/README.md b/README.md index af2b84b8..2143081a 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,16 @@ Use it when you need: measured 2026-10-11)**, the **RC.3 sweep of 2,897 tests across 16 suites**, and **backward compatibility** -> **Current release: v2.1.0 (2026-10-10) — the .NET 11 / C# 15 line, on `master`.** **Published on +> **Current release: v2.1.2 (2026-10-26) — the .NET 11 / C# 15 line, on `master`.** A data-correctness +> patch with no API changes: a **bound string parameter holding an apostrophe, a doubled quote or a +> backslash was re-scanned as SQL**, so the value could land in the neighbouring column, and a value +> ending in a backslash could not be written at all. One scanner now owns SQL's and SQLite's escaping +> rules for every literal reader on the statement path. Validated by **core 2,107 tests / 0 failed, +> VectorSearch 255 / 0, EF Core 116 / 0, linq2DB 24 / 0**, and a two-file runnable reproduction ships in +> [`docs/sharpcoredb-known-issues.md`](docs/sharpcoredb-known-issues.md) §7. Full notes: +> [`docs/CHANGELOG.md`](docs/CHANGELOG.md) › `[2.1.2]`. + +> **Previous release — v2.1.0 (2026-10-10) — the .NET 11 / C# 15 line, on `master`.** **Published on > NuGet.org** (all 27 packable packages) from `master`, `net11.0` only. Highlights: the fixed-width inline capacity > ships at 24 bytes by default on both storage paths (worth **+34 %** on the batched multi-row INSERT > shape and **0,63× → 0,87×** on the tracked fair-PK INSERT ratio), the overflow arena appends a row's payloads in one call instead of one @@ -73,17 +82,17 @@ Use it when you need: ## Requirements and the two release lines -| | **v2.1.0 — this line (`master`)** | **v2.0.0.3 — parked line (`release/v2.0`)** | +| | **v2.1.2 — this line (`master`)** | **v2.0.0.3 — parked line (`release/v2.0`)** | |---|---|---| | Target framework | `net11.0` only (every project under `src`) | `net10.0` | | Language | C# 15 (`LangVersion=preview`, always on) | C# 14 | | SDK | .NET 11 RC, pinned in `global.json` (`11.0.100-rc.1.26425.128`, `allowPrerelease`, `rollForward: latestPatch`) | .NET 10 (`10.0.x`) | | Pipeline | `.github/workflows/ci.yml` | `.github/workflows/ci-net10.yml` | -| Packages | `2.1.0` — the 27 packable projects under `src` | `2.0.0.3` | +| Packages | `2.1.2` — the 27 packable projects under `src` | `2.0.0.3` | | Status | current release, receives new work | servicing only | - **Pick one line per solution.** The package references are line-specific: a `net10.0` project uses the - `2.0.0.x` packages, a `net11.0` project the `2.1.0` ones. Do not mix them in one solution. + `2.0.0.x` packages, a `net11.0` project the `2.1.2` ones. Do not mix them in one solution. - **The .NET 11 SDK is required to build** — the `global.json` pin is deliberate and `dotnet test` is not this repository's runner (Microsoft.Testing.Platform is; run the built test executable, or see [`docs/INDEX.md`](docs/INDEX.md)). @@ -115,7 +124,47 @@ Use it when you need: --- -## What's new in v2.1.0 (current release) +## What's new in v2.1.2 (current release) + +**A data-correctness patch with no API changes.** `ExecuteSQL(sql, parameters)` does not bind values at the +engine level: it renders each one back **into the statement text** and hands that text to the ordinary +parser, so the formatter and the statement scanners have to agree about escaping — and they did not. A +bound `TEXT` value holding an apostrophe, a doubled quote or a backslash was re-scanned as SQL: the value +could land in the **neighbouring column**, and a value ending in a backslash could not be written at all +(`Mismatched parentheses in VALUES clause`). Any consumer storing prose, source code or Windows paths +through a parameter was exposed, and both faces of the failure were silent. + +The rules now live once, in [`SqlLiteralScanner`](src/SharpCoreDB/Services/SqlLiteralScanner.cs), and every +literal reader on the statement path uses it: the `VALUES` and `SET` splitters, the matching-parenthesis +scan, all four parameter extractors, the batch `INSERT` parsers, `FastSqlLexer.ReadString`, the expression +tokenizer, the `WHERE` tokenizer (`EvaluateWhere` split on spaces and truncated *any* quoted value at its +first space) and the server's `$n` → `?` translation. A doubled quote is one quote and does not close the +literal; a backslash is data. `ParseValue` removes exactly one quote pair and collapses the doubling (a +parameter holding `'` used to store an empty string), a quoted `''` stores an empty string as SQLite does +instead of `NULL`, `UPDATE … SET col = @v` splits at the first `=` instead of every `=`, and a tuple that +cannot fit its statement throws before any row is written — naming the table, the tuple index, both counts +and the column list — instead of shifting values right. + +```csharp +// Before 2.1.2 the body landed in `Source` (or the INSERT threw "Mismatched parentheses"). +// After: both round-trip byte-for-byte, Windows paths and prose included. +db.ExecuteSQL("INSERT INTO Passages (Id, Source, Body) VALUES (@id, @src, @body)", + new Dictionary + { + ["@id"] = 7, + ["@src"] = @"C:\src\app\", + ["@body"] = @"It's O(n log n) — see docs\draft.pdf, 'quoted' too" + }); +``` + +Validated on this tree: **core 2,107 tests / 0 failed**, VectorSearch **255 / 0**, EF Core **116 / 0**, +linq2DB **24 / 0**, and `SharpCoreDB.CI.slnf` builds clean. A two-file runnable reproduction with the +before/after console output ships in [`docs/sharpcoredb-known-issues.md`](docs/sharpcoredb-known-issues.md) +§7, and the consumer that reported it — SharpAgent's code-evidence index, which writes source text and +Windows paths as `TEXT` — runs its **144 indexing tests green** against this build with **no code change**. +Full list: [`docs/CHANGELOG.md`](docs/CHANGELOG.md) › `[2.1.2]`. + +## What's new in v2.1.0 (previous release) The v1.x benchmark gap (point reads/updates/deletes **16–52x behind SQLite** because every update appended a new row) is closed as an engine behaviour. The numbers below are re-measured on @@ -235,7 +284,7 @@ Headline changes, grouped by area. Everything below is in the shipped `2.1.0` pa ### 1) Embedded mode ```bash -dotnet add package SharpCoreDB --version 2.1.0 +dotnet add package SharpCoreDB --version 2.1.2 ``` ```csharp @@ -272,8 +321,8 @@ gRPC endpoint: `https://localhost:5001` Install client/server packages: ```bash -dotnet add package SharpCoreDB.Server.Core --version 2.1.0 -dotnet add package SharpCoreDB.Client --version 2.1.0 +dotnet add package SharpCoreDB.Server.Core --version 2.1.2 +dotnet add package SharpCoreDB.Client --version 2.1.2 ``` > The server host is a standalone executable (`src/SharpCoreDB.Server`), distributed as the @@ -286,6 +335,10 @@ dotnet add package SharpCoreDB.Client --version 2.1.0 ## Previous releases +- **v2.1.1** — patch, no API changes: a bound `float[]` reaches a `VECTOR(n)` column as a vector, + `ORDER BY vec_distance_*() LIMIT k` reaches the vector index instead of scanning, `information_schema` + is answered by the engine itself (so REST and the embedded API see the catalog the PG handshake does), + and the 18 % coverage floor is measured and enforced in CI. - **v2.0.0.3** — the net10.0 / C# 14 stable line, parked on `release/v2.0` and built by `ci-net10.yml` (the complete package list is above). Engine foundation it carries: fixed-width record layout by default for new PK tables, in-place UPDATE/DELETE with commit-time tombstones, and the reopen data-integrity @@ -300,50 +353,50 @@ dotnet add package SharpCoreDB.Client --version 2.1.0 ## Available NuGet packages -### v2.1.0 — current release (`master`, net11.0 / C# 15 preview) +### v2.1.2 — current release (`master`, net11.0 / C# 15 preview) ```bash # Core -dotnet add package SharpCoreDB --version 2.1.0 +dotnet add package SharpCoreDB --version 2.1.2 # Server/client -dotnet add package SharpCoreDB.Server.Core --version 2.1.0 -dotnet add package SharpCoreDB.Server.Protocol --version 2.1.0 -dotnet add package SharpCoreDB.Client --version 2.1.0 -dotnet add package SharpCoreDB.Client.Protocol --version 2.1.0 -dotnet add package SharpCoreDB.Data.Provider --version 2.1.0 +dotnet add package SharpCoreDB.Server.Core --version 2.1.2 +dotnet add package SharpCoreDB.Server.Protocol --version 2.1.2 +dotnet add package SharpCoreDB.Client --version 2.1.2 +dotnet add package SharpCoreDB.Client.Protocol --version 2.1.2 +dotnet add package SharpCoreDB.Data.Provider --version 2.1.2 # Engines and extensions -dotnet add package SharpCoreDB.Analytics --version 2.1.0 -dotnet add package SharpCoreDB.VectorSearch --version 2.1.0 -dotnet add package SharpCoreDB.Search --version 2.1.0 -dotnet add package SharpCoreDB.HybridSearch --version 2.1.0 -dotnet add package SharpCoreDB.Graph --version 2.1.0 -dotnet add package SharpCoreDB.Graph.Advanced --version 2.1.0 -dotnet add package SharpCoreDB.Distributed --version 2.1.0 -dotnet add package SharpCoreDB.Provider.Sync --version 2.1.0 -dotnet add package SharpCoreDB.Provider.YesSql --version 2.1.0 -dotnet add package SharpCoreDB.EntityFrameworkCore --version 2.1.0 -dotnet add package SharpCoreDB.Extensions --version 2.1.0 -dotnet add package SharpCoreDB.Identity --version 2.1.0 -dotnet add package SharpCoreDB.Serilog.Sinks --version 2.1.0 +dotnet add package SharpCoreDB.Analytics --version 2.1.2 +dotnet add package SharpCoreDB.VectorSearch --version 2.1.2 +dotnet add package SharpCoreDB.Search --version 2.1.2 +dotnet add package SharpCoreDB.HybridSearch --version 2.1.2 +dotnet add package SharpCoreDB.Graph --version 2.1.2 +dotnet add package SharpCoreDB.Graph.Advanced --version 2.1.2 +dotnet add package SharpCoreDB.Distributed --version 2.1.2 +dotnet add package SharpCoreDB.Provider.Sync --version 2.1.2 +dotnet add package SharpCoreDB.Provider.YesSql --version 2.1.2 +dotnet add package SharpCoreDB.EntityFrameworkCore --version 2.1.2 +dotnet add package SharpCoreDB.Extensions --version 2.1.2 +dotnet add package SharpCoreDB.Identity --version 2.1.2 +dotnet add package SharpCoreDB.Serilog.Sinks --version 2.1.2 # Optional architecture packages -dotnet add package SharpCoreDB.EventSourcing --version 2.1.0 -dotnet add package SharpCoreDB.Projections --version 2.1.0 -dotnet add package SharpCoreDB.CQRS --version 2.1.0 +dotnet add package SharpCoreDB.EventSourcing --version 2.1.2 +dotnet add package SharpCoreDB.Projections --version 2.1.2 +dotnet add package SharpCoreDB.CQRS --version 2.1.2 # Optional functional adapters -dotnet add package SharpCoreDB.Functional --version 2.1.0 -dotnet add package SharpCoreDB.Functional.Dapper --version 2.1.0 -dotnet add package SharpCoreDB.Functional.EntityFrameworkCore --version 2.1.0 -dotnet add package SharpCoreDB.Functional.Linq2DB --version 2.1.0 +dotnet add package SharpCoreDB.Functional --version 2.1.2 +dotnet add package SharpCoreDB.Functional.Dapper --version 2.1.2 +dotnet add package SharpCoreDB.Functional.EntityFrameworkCore --version 2.1.2 +dotnet add package SharpCoreDB.Functional.Linq2DB --version 2.1.2 # Hosting and tooling -dotnet add package SharpCoreDB.Aspire.Hosting --version 2.1.0 +dotnet add package SharpCoreDB.Aspire.Hosting --version 2.1.2 ``` -> The 27 commands above are the complete packable set of this line. **`2.1.0` is a stable release**, so +> The 27 commands above are the complete packable set of this line. **`2.1.2` is a stable release**, so > NuGet selects it by default for a `net11.0` project; a `net10.0` project resolves to the `2.0.0.3` line > instead. Pin the version explicitly (as above) when you want to be certain, and tick *Include > prerelease* only if you are deliberately testing an RC build. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e849ddcd..266f14fa 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -7,9 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -> **Where this bucket ends.** `2.1.1` was cut from this line's `HEAD` on 2026-10-25, so every entry this -> bucket had collected since the `v2.1.0` tag — the list below — **shipped in `[2.1.1]`**. New entries -> start here. +> **Where this bucket ends.** `2.1.2` was cut from this line's `HEAD` on 2026-10-26, so the entry this +> bucket had collected since the `v2.1.1` tag — the statement-path literal scanner below — **shipped in +> `[2.1.2]`**. New entries start here. + +## [2.1.2] - 2026-10-26 + +> **What this release is.** A patch release with no API changes, cut from `v2.1.1..HEAD`, and a data-correctness +> one rather than a performance one. `ExecuteSQL(sql, parameters)` never binds: it renders every value back +> into the statement text and hands that text to the ordinary parser, so the formatter and the scanners have +> to agree about escaping — and they did not. A bound `TEXT` value holding an apostrophe, a doubled quote or a +> backslash was re-scanned as SQL, and the damage was silent: the value landed in the neighbouring column, or +> the statement was rejected with "Mismatched parentheses in VALUES clause". Any consumer that stores prose, +> source code or Windows paths through a parameter was exposed; SharpAgent's code-evidence index hit both +> faces of it. One scanner now owns the rules (`Services/SqlLiteralScanner.cs`), every literal reader on the +> statement path uses it, and a tuple that still cannot fit its statement fails loudly instead of shifting +> values. Verified on this tree: core **2107 / 0 failed**, VectorSearch **255 / 0**, EF Core **116 / 0**, +> linq2DB **24 / 0**, `SharpCoreDB.CI.slnf` builds clean, and the reproduction published in +> `docs/sharpcoredb-known-issues.md` §7 prints the fixed output when extracted from the document. + +### Fixed + +- **A bound string parameter was re-parsed as SQL text, so an apostrophe or a backslash in the value corrupted the row — it could land in the next column, or the statement failed with "Mismatched parentheses in VALUES clause".** `ExecuteSQL(sql, parameters)` renders every value back into the statement text (`ParameterBinder.FormatParameter` emits `'it''s'` for `it's`, SQL's doubled-quote convention) and hands the *rendered* statement to the ordinary parser, so the formatter and the scanner have to agree about escaping — and they did not: the VALUES scanner dropped the quote of a doubled pair (standard SQL doubling was not an escape) and let a backslash suppress a quote toggle, which also meant a value ending in a backslash could not be written at all. SharpAgent's code-evidence index hit it as rows rejected with "Mismatched parentheses in VALUES clause" and, worse, as a value silently landing in the neighbouring column (`Invalid VECTOR literal '… iterations)'` on `embedding`). The rules are now SQL's and SQLite's, defined once in `Services/SqlLiteralScanner.cs` and used by every literal reader on the statement path — the VALUES and `SET` splitters, the matching-parenthesis scan, all four parameter extractors, the batch INSERT parsers, `FastSqlLexer.ReadString`, the expression tokenizer, the `WHERE` tokenizer (`Table.Scanning.EvaluateWhere`, which split on spaces and therefore truncated *any* quoted value at its first space) and the wire protocol's `$n` → `?` translation (which needed `InternalsVisibleTo("SharpCoreDB.Server.Core")`): a doubled quote is one quote and does not close the literal, and a backslash is data. `ParseValue` now removes exactly **one** pair of quotes and collapses the doubling — the old `while`-strip removed every edge quote, so a parameter holding `'` stored an empty string, and `'it''s'` reached storage as `it''s`. `UPDATE … SET col = @v` splits at the first `=` instead of every `=` (a value containing `=` silently dropped that assignment), a quoted empty literal stores an empty string as SQLite does (a trailing `''` used to become `NULL`), and a tuple that still produces more values than the statement can consume throws **before any row is written**, naming the table, the tuple index, both counts and the column list, instead of shifting values into the next column. Verified red→green: `ParameterLiteralRoundTripTests` (74 cases — apostrophes, doubled quotes, backslash before a quote, trailing backslash, commas, brackets, braces, newlines, non-ASCII, `NULL`, the empty string, `'`, `''`, `\`, each through an interior and a trailing INSERT value, multi-row INSERT, positional `?`, `UPDATE … SET`, and a `WHERE val = @val` lookup) failed 32 of 57 assertions before the fix and passes 74/74 after; `InsertValuesParsingTests` was corrected to SQL semantics (`'it''s'` → `it's`, a trailing backslash is data, a trailing `''` is an empty string) and the suites are **core 2107 / 0 failed**, VectorSearch 255 / 0, EF Core 116 / 0, linq2DB 24 / 0 (`SharpCoreDB.CI.slnf` builds clean). Two latent test bugs surfaced on the way: `EncryptionCoverageTests` and `WritePathProfilerTests` interpolated `{i * 0.5}` straight into SQL, which renders `0,5` on a comma-decimal machine and is a four-value tuple — the old parser silently dropped the overflow value, the new arity check rejects it, so both now format invariantly. `docs/sharpcoredb-known-issues.md` §7 now carries a self-contained, runnable reproduction (two files, `dotnet run`) with the before/after console output, and the same cases are pinned by `ParameterLiteralRoundTripTests`. ## [2.1.1] - 2026-10-25 diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 3628477b..1dbf5ce7 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -1,16 +1,26 @@ # SharpCoreDB Project Status -**Version:** 2.1.0 (`master`, net11.0 / C# 15 preview) · 2.0.0.3 (net10.0 / C# 14 stable, parked on `release/v2.0`) -**Status:** ✅ v2.1.0 promoted to the main branch · ✅ v2.0 stable line parked on `release/v2.0` · ✅ Performance-first line -**Last Updated:** October 10, 2026 +**Version:** 2.1.2 (`master`, net11.0 / C# 15 preview) · 2.0.0.3 (net10.0 / C# 14 stable, parked on `release/v2.0`) +**Status:** ✅ v2.1.2 released on the main branch · ✅ v2.0 stable line parked on `release/v2.0` · ✅ Performance-first line +**Last Updated:** October 26, 2026 ## Current Status +The **current release is `2.1.2` (2026-10-26)** — a data-correctness patch with no API changes: a bound +`TEXT` parameter holding an apostrophe, a doubled quote or a backslash was re-scanned as SQL by the +statement path, so the value could land in the neighbouring column, or the statement was rejected with +"Mismatched parentheses in VALUES clause". One scanner (`Services/SqlLiteralScanner.cs`) now owns SQL's +and SQLite's escaping rules for every literal reader on the statement path. Validated on the `2.1.2` tree: +core **2,107 tests / 0 failed**, VectorSearch **255 / 0**, EF Core **116 / 0**, linq2DB **24 / 0**. Notes: +[`CHANGELOG.md`](CHANGELOG.md) › `[2.1.2]`; the runnable reproduction is in +[`sharpcoredb-known-issues.md`](sharpcoredb-known-issues.md) §7. + The **v2.1 line (`2.1.0`)** is validated and packaged, and was promoted to `master` on **2026-10-10** (see [`net11/PROMOTION_TO_MAIN.md`](net11/PROMOTION_TO_MAIN.md)): **2,897 tests across 16 suites — 0 failed, 0 errors, 16 skipped** (core 1,916), the write-path regression gate **passed**, and the full benchmark arm set (comparative, fair-PK, pure-default, PageBased, multi-row INSERT) measured in the product-default regime. Release notes: [`2.1.0-RC.3_WHAT_CHANGED.md`](2.1.0-RC.3_WHAT_CHANGED.md). +`2.1.1` followed on **2026-10-25** with the SQL vector fixes; `2.1.2` is the current patch. The **v2.0 stable packages (`2.0.0.3`)** are the net10.0 / C# 14 line, parked on `release/v2.0`, including: @@ -34,9 +44,9 @@ The **v2.0 stable packages (`2.0.0.3`)** are the net10.0 / C# 14 line, parked on - ✅ **The RC.3 line was published to NuGet.org (2026-09-17)** — all 27 packages at `2.1.0-RC.3` from `release/v2.1.0.0-RC.3` (`Manual NuGet Publish` run 35244818191); `SharpCoreDB.Search`, `SharpCoreDB.HybridSearch` and `SharpCoreDB.Aspire.Hosting` ship on this line for the first time -- ⏳ **2.1.0 is prepared on `master` and not yet tagged or pushed** — the owner's flip (`git tag -a v2.1.0` - + `git push`) is what publishes the 27 packages at `2.1.0`; version metadata, CI and the shipped - package documentation are already aligned for it (see [`net11/PROMOTION_TO_MAIN.md`](net11/PROMOTION_TO_MAIN.md)) +- ✅ **2.1.0 was published from `master`, and `2.1.1` followed as a patch (2026-10-25)** — the publish + trigger is the `push: branches: [master]` job in `.github/workflows/ci.yml`, which packs and pushes the + 27 packages; `2.1.2` is the current release. History: [`CHANGELOG.md`](CHANGELOG.md). - ✅ **Linux/macOS data-integrity defect fixed** — the AES-GCM cipher caches were shared across threads, which is only safe on Windows (dotnet/runtime#53320); they are thread-affine now, so concurrent encryption cannot corrupt a cipher's native state or repeat a nonce off Windows diff --git a/docs/Vectors/README.md b/docs/Vectors/README.md index a8ff5b02..ab17e28c 100644 --- a/docs/Vectors/README.md +++ b/docs/Vectors/README.md @@ -76,10 +76,10 @@ This is the foundation for: ```bash # Core database -dotnet add package SharpCoreDB --version 2.1.0 +dotnet add package SharpCoreDB --version 2.1.2 # Vector search extension -dotnet add package SharpCoreDB.VectorSearch --version 2.1.0 +dotnet add package SharpCoreDB.VectorSearch --version 2.1.2 ``` ### 2. Register Vector Search diff --git a/docs/analytics/README.md b/docs/analytics/README.md index fff63daa..90de6a47 100644 --- a/docs/analytics/README.md +++ b/docs/analytics/README.md @@ -32,7 +32,7 @@ The SharpCoreDB Analytics Engine provides high-performance data aggregation, win ### Installation ```bash -dotnet add package SharpCoreDB.Analytics --version 2.1.1 +dotnet add package SharpCoreDB.Analytics --version 2.1.2 ``` ### Basic Aggregation diff --git a/docs/cqrs/README.md b/docs/cqrs/README.md index c1bdf88f..ca4352b2 100644 --- a/docs/cqrs/README.md +++ b/docs/cqrs/README.md @@ -54,7 +54,7 @@ Non-goals: ## 2. Installation ```bash -dotnet add package SharpCoreDB.CQRS --version 2.1.1 +dotnet add package SharpCoreDB.CQRS --version 2.1.2 ``` --- diff --git a/docs/graphrag/README.md b/docs/graphrag/README.md index 3ececa68..54ff269a 100644 --- a/docs/graphrag/README.md +++ b/docs/graphrag/README.md @@ -49,9 +49,9 @@ For package-level mapping, see `../FEATURE_MATRIX.md`. ## Quick start ```bash -dotnet add package SharpCoreDB.Graph.Advanced --version 2.1.1 -dotnet add package SharpCoreDB.Graph --version 2.1.1 -dotnet add package SharpCoreDB.VectorSearch --version 2.1.1 +dotnet add package SharpCoreDB.Graph.Advanced --version 2.1.2 +dotnet add package SharpCoreDB.Graph --version 2.1.2 +dotnet add package SharpCoreDB.VectorSearch --version 2.1.2 ``` ```csharp diff --git a/docs/manual/quickstart.md b/docs/manual/quickstart.md index 8ea34ff8..103ece29 100644 --- a/docs/manual/quickstart.md +++ b/docs/manual/quickstart.md @@ -8,7 +8,7 @@ ## 1.1 Install ```bash -dotnet add package SharpCoreDB --version 2.1.1 +dotnet add package SharpCoreDB --version 2.1.2 ``` The engine targets **.NET 11** and **C# 15**. No native dependencies, no external processes — diff --git a/docs/scdb/PRODUCTION_GUIDE.md b/docs/scdb/PRODUCTION_GUIDE.md index fd4008f7..04ce3840 100644 --- a/docs/scdb/PRODUCTION_GUIDE.md +++ b/docs/scdb/PRODUCTION_GUIDE.md @@ -46,7 +46,7 @@ This guide covers deploying SharpCoreDB with the **SCDB (SharpCore DataBase)** s dotnet --version # Should show 11.0 or higher # NuGet Package -dotnet add package SharpCoreDB --version 2.1.1 +dotnet add package SharpCoreDB --version 2.1.2 ``` The 2.0.x servicing line on `release/v2.0` targets .NET 10 instead; match the runtime to the package diff --git a/docs/sharpcoredb-known-issues.md b/docs/sharpcoredb-known-issues.md index ce6e3cdd..16ea7fd4 100644 --- a/docs/sharpcoredb-known-issues.md +++ b/docs/sharpcoredb-known-issues.md @@ -192,6 +192,235 @@ error message. --- +## 7. ✅ FIXED — A bound string parameter is re-parsed as SQL text, so quotes and backslashes corrupt the row + +> ✅ **FIXED (2026-10-26, `master` @ 9461c74e + this change).** Found while building SharpAgent's code-evidence +> index, which writes source text, comments and Windows paths as `TEXT` values. Every reproduction below is now +> a test: `tests/SharpCoreDB.Tests/ParameterLiteralRoundTripTests.cs` (74 cases) failed 32 of 57 before the fix +> and the core suite is **2107 / 0 failed** after it. + +### Runnable reproduction + +Two files, no test framework, ~40 lines — the whole defect and the whole fix are visible in the console output. +Put them in `artifacts/repro/` (two levels below the repo root, so the `..\..\src` project reference is right) and +run `dotnet run -c Release`. + +`Repro.csproj` + +```xml + + + + Exe + net11.0 + enable + enable + + + + + + + + +``` + +`Program.cs` + +```csharp +using Microsoft.Extensions.DependencyInjection; +using SharpCoreDB; + +var services = new ServiceCollection(); +services.AddSharpCoreDB(); +var factory = services.BuildServiceProvider().GetRequiredService(); + +var dir = Path.Combine(Path.GetTempPath(), "scdb-issue7-" + Guid.NewGuid().ToString("N")); +Directory.CreateDirectory(dir); +var db = factory.Create(dir, "pw", isReadOnly: false, config: new DatabaseConfig { NoEncryptMode = true }); +db.ExecuteSQL("CREATE TABLE t (id INTEGER PRIMARY KEY, head TEXT, val TEXT, tail TEXT)"); + +// @val sits in the middle of the tuple, so a value that is split at its own comma moves into `tail`. +string[] middle = +[ + "it's fine, really", + @"a stray \' apostrophe, then a comma", + @"ends with a backslash \", + "plain text", +]; + +for (int i = 0; i < middle.Length; i++) +{ + Try(i + 1, "INSERT INTO t VALUES (@id, 'head', @val, 'tail')", middle[i]); +} + +// The empty string as the *last* value: the tuple ends in ''. +Try(99, "INSERT INTO t VALUES (@id, 'head', 'tail', @val)", string.Empty); + +void Try(int id, string sql, string value) +{ + Console.WriteLine($"@val = [{value}]"); + try + { + db.ExecuteSQL(sql, new Dictionary { ["id"] = id, ["val"] = value }); + var row = db.ExecuteQuery($"SELECT * FROM t WHERE id = {id}")[0]; + Console.WriteLine($" val={Show(row["val"])} tail={Show(row["tail"])}"); + } + catch (Exception ex) + { + Console.WriteLine($" {ex.GetType().Name}: {ex.Message}"); + } +} + +static string Show(object? value) => value is null or DBNull ? "" : $"[{value}]"; +``` + +**Before the fix** — `HEAD` `9461c74e`, the tree this report was filed against (output copied from that run; the +`←` notes are annotations, and `Show` prints `` for a null so an empty string and a null cannot be confused): + +```text +@val = [it's fine, really] + val=[its fine] tail=[really, tail] ← apostrophe gone, literal split at its comma +@val = [a stray \' apostrophe, then a comma] + InvalidOperationException: Mismatched parentheses in VALUES clause +@val = [ends with a backslash \] + InvalidOperationException: Mismatched parentheses in VALUES clause +@val = [plain text] + val=[plain text] tail=[tail] +@val = [] + val=[tail] tail= ← '' dropped: val overwritten, tail set to NULL +``` + +**After the fix** — same program, same machine, no other change: + +```text +@val = [it's fine, really] + val=[it's fine, really] tail=[tail] +@val = [a stray \' apostrophe, then a comma] + val=[a stray \' apostrophe, then a comma] tail=[tail] +@val = [ends with a backslash \] + val=[ends with a backslash \] tail=[tail] +@val = [plain text] + val=[plain text] tail=[tail] +@val = [] + val=[tail] tail=[] +``` + +Every line of that program is also a test case in `tests/SharpCoreDB.Tests/ParameterLiteralRoundTripTests.cs` +(74 cases), so the regression cannot come back silently. + +**Fix (what changed).** The formatter and the scanners now share **one** escaping convention, defined once in +`src/SharpCoreDB/Services/SqlLiteralScanner.cs` and used by every literal reader on the statement path: + +- **The rule is SQL's, and therefore SQLite's:** inside `'…'` a doubled quote is one quote of data and does not + close the literal, and a backslash is data. A backslash can never be an escape here — it is a legal last + character of a value (a Windows path), and a scanner that lets it suppress the closing quote cannot represent + that value in a statement at all. +- **Converted call sites:** `SqlParser.ParseInsertValues` (now purely structural — it returns each element as + written and `ParseValue` reads the literal), `SqlParser.FindMatchingCloseParen`, + `SqlInPredicate.SplitTopLevelComma`, all four `PreparedStatement` extractors, `Database.Batch` + (`ParseValues`, `ParseValuesToArray`, `ParseValueFast`), `SingleFileDatabase.Batch` (`ParseValueList`, + `TrimQuotes`), `FastSqlLexer.ReadString`, the `TypeConverter` tokenizer, the `WHERE` tokenizer + (`Table.Scanning.EvaluateWhere` — splitting on spaces truncated *any* quoted value at its first space), + `Table.QueryHelpers`, `Table.Collation`, `SqlInPredicate`'s IN/tuple value parsing, `SingleFileTable`, + `WhereClauseAnalyzer`, `SqlParser.PerformanceOptimizations`, `SqlParserExtensions`, `DatabaseExtensions`, and + the wire protocol's `$n` → `?` translation (`BinaryProtocolHandler`, which needed + `InternalsVisibleTo("SharpCoreDB.Server.Core")`). +- **`ParseValue` removes exactly one pair of quotes** and collapses the doubling. The old `while`-strip removed + every edge quote, so a parameter holding `'` stored an empty string; and it left `''` in place, so `'it''s'` + reached storage as `it''s`. +- **A quoted empty literal is a value.** `(1, '')` stores an empty string, as SQLite does; only a literally + absent element is dropped. A *trailing* `''` used to become `NULL`, which made an empty string impossible to + round-trip. +- **`UPDATE … SET col = @v` splits at the first `=`**, not at every `=`: a value containing `=` (a query string, + a source line) silently dropped the whole assignment before. +- **Safety net (recommendation 1):** a VALUES tuple that produces more values than the statement can consume now + throws **before any row is written**, naming the table, the tuple index, both counts and the column list, + instead of shifting values into the neighbouring column. +- **Two latent test bugs surfaced:** `EncryptionCoverageTests` and `WritePathProfilerTests` interpolated + `{i * 0.5}` straight into SQL, which renders `0,5` on a comma-decimal machine and is a **four**-value tuple. + The old parser silently dropped the overflow; the new arity check rejects it (SQLite rejects it too), so those + two tests now format invariantly. + +**Not changed, deliberately:** values are still rendered into the statement text for `ExecuteSQL(sql, params)`. +Recommendation 3 (build the row from the parameter objects and skip the text round trip) remains an +optimisation, not a correctness need now that the convention is shared and pinned. + +**Location:** `src/SharpCoreDB/Services/PreparedStatements.cs` (`ParameterBinder.FormatParameter`, lines 478-510), +`src/SharpCoreDB/Services/SqlParser.Core.cs:193` (`Execute(sql, parameters)` → `BindParameters`), and +`src/SharpCoreDB/Services/SqlParser.DML.cs` (`ParseMultiRowInsertValues` 584-628, `FindMatchingCloseParen` +633-661, `ParseInsertValues` 664-823). + +- `ExecuteSQL(sql, parameters)` renders every value back **into the statement text** + (`FormatParameter` returns `'it''s'` for `it's`, `[0.5, 0.25]` for a `float[]`, `NULL` for null) and then + hands the *rendered* statement to the ordinary parser. The value therefore becomes parser input, and + correctness depends on the formatter and the literal scanner agreeing exactly. +- They do not agree. The scanner toggles quote state on every `'` whose previously **appended** character is + not a backslash (`SqlParser.DML.cs:709-717`): standard SQL quote-doubling is not an escape, and there is no + `toggles == 3` branch, so after a doubled quote the scanner considers itself **outside** the literal for the + rest of it. + +**Reproduced** with `CREATE TABLE t (id INTEGER PRIMARY KEY, a TEXT, b TEXT)` and +`INSERT INTO t VALUES (1, @text, 'tail')`: + +| `@text` | What is stored | +|---|---| +| `it's fine, really` | `a = "its fine"`, `b = "really, tail"` — apostrophe dropped, literal split at the comma, **tail shifted into the next column** | +| `a stray \' apostrophe, then a comma` | `InvalidOperationException: Mismatched parentheses in VALUES clause` | +| `ends with a backslash \` | `InvalidOperationException: Mismatched parentheses in VALUES clause` | +| `the mask (see below, the reader) wins, and then it records iterations)` | correct — parens and commas alone are safe, which is why the defect hides | + +**Consequence:** a value may land in the **neighbouring column**, including a non-text one. SharpAgent saw it as +`Invalid VECTOR literal '… iterations)'` on `embedding`, and before that as rows rejected with "Mismatched +parentheses". Silent data corruption is the more dangerous half: nothing is thrown for the apostrophe case. + +**Why the scanners disagree.** There are four independent quote scanners, each with slightly different rules: + +- `SqlParser.ParseInsertValues` / `FindMatchingCloseParen` — backslash-aware, doubling-unaware. +- `SqlInPredicate.SplitTopLevelComma` (`SqlInPredicate.cs:408-456`, the one UPDATE's `SET` list uses) — + quote/paren/bracket-aware, doubling-unaware, backslash-unaware (`inString = c != quote`). +- `PreparedStatement.ExtractParameterPositions` / `ExtractNamedParameters` (`PreparedStatements.cs:101-194`) — + backslash-aware, doubling-unaware. +- `ParameterExtractor` — its own copy of the same idea. + +And `InsertValuesParsingTests` currently **pins the corruption as intended**: +`DoubledQuote_IsDroppedAndContentConcatenated_NotEscaped` asserts `'it''s'` → `its`, and +`BackslashEscapedQuote_IsKeptVerbatim_AndDoesNotToggle` asserts that a backslash suppresses the toggle. + +**Workaround in SharpAgent:** never send free-form text through `ExecuteSQL(sql, parameters)`. Writes go through +`ITable.Insert` / `UpdateByPrimaryKey` with native values (`src/SharpCoreAgent.Indexing/IndexSql.cs`), which +never renders a statement, so no escaping question arises. This is a consumer-side workaround, not a fix: the +same corruption is reachable from any `ExecuteSQL` call with a text parameter. + +**Recommendation (in this order):** ✅ 1, 2 and 4 are implemented (see *Fix* above). 3 remains an optional +optimisation. The original analysis is kept below as the record of what was wrong. + +1. **Assert the arity.** After `ParseMultiRowInsertValues`, compare `rowValues.Count` with the resolved column + list and throw naming the column, the literal index and the offset instead of letting a shifted value reach + `ParseValue`. Converts silent corruption into a loud, diagnosable error, with no hot-path cost. +2. **One scanner, one escaping convention.** Extract a single `SqlLiteralScanner` used by the VALUES splitter, + `FindMatchingCloseParen`, `SplitTopLevelComma` and both `Extract*` methods, implementing SQL's actual rules + (`''` inside a literal is one quote and does **not** close it; a backslash is data, not an escape), and make + `FormatParameter` emit exactly what that scanner reads back — `'` → `''`, and either escape `\` as `\\` or, + preferably, stop treating `\` as an escape anywhere. Then "any string round-trips byte-exact" becomes a + testable property rather than a coincidence. +3. **Stop rendering values into statements.** For simple single-table `INSERT … VALUES (…, @p, …)` / + `UPDATE … SET col = @p WHERE pk = @pk`, build the row dictionary from the parameter objects and call the + table API the SQL path already ends in (`Table.Insert`, `Table.InsertBatch`, `Table.UpdateByPrimaryKey`). + No text is produced, so nothing can be mis-escaped, and the statement avoids a format-then-parse round trip. + The engine already shape-gates a "simple select" plan, so the precedent exists. +4. **Replace the two pinning tests** with round-trip tests (`ExecuteSQL` with a parameter, read back, compare + byte-exact) over: apostrophes, doubled quotes, backslashes before quotes, trailing backslashes, braces, + parens, brackets, embedded commas, newlines and non-ASCII. Keep `InsertValuesParsingTests`' literal + `INSERT … VALUES` cases that assert genuine SQL behaviour, and correct `'it''s'` → `it's`. + +**Guardrail for step 2:** the multi-row VALUES scan is a measured hot path (comments at +`SqlParser.DML.cs:572-583` and `:436-450` record the numbers). Any rewrite must keep the slice fast path for +literals without doubling or backslashes, and re-run `--multirowinsert` plus the `WritePathProfiler` +attribution before merge. + +--- + ## Priority summary | # | Issue | Severity | Status in ServiceMq.SharpCoreDb | In SharpCoreDB 2.1.0 | @@ -201,4 +430,5 @@ error message. | 3 | Single-file no-op index | 🟠 Medium | Directory mode forced | **Fixed** — `SingleFileTable` keeps a PK hash index | | 4 | No read-after-write flush | 🟡 Low | Flush calls in provider | **Fixed** — `ExecuteQuery` flushes pending batch writes | | 5 | SQL validator `@` prefix | 🟡 Low | Keys without `@` prefix | **Fixed** — `NormalizeKey` strips `@` / `:` | -| 6 | INTEGER → Int32 | 🟡 Low | `BIGINT` columns | **Still the default** — opt in with `UseSqliteIntegerAffinity` | \ No newline at end of file +| 6 | INTEGER → Int32 | 🟡 Low | `BIGINT` columns | **Still the default** — opt in with `UseSqliteIntegerAffinity` | +| 7 | Bound text re-parsed as SQL | 🔴 High | Write through `ITable.Insert` instead of SQL text | **Fixed** — one `SqlLiteralScanner` (SQL/SQLite: a doubled quote is one quote, a backslash is data) behind every scanner; `ParseValue` strips one pair and un-collapses doubling; a tuple with too many values is rejected. `ParameterLiteralRoundTripTests` 74/74 | \ No newline at end of file diff --git a/src/SharpCoreDB.Analytics/NuGet.README.md b/src/SharpCoreDB.Analytics/NuGet.README.md index e63428aa..b113d2e2 100644 --- a/src/SharpCoreDB.Analytics/NuGet.README.md +++ b/src/SharpCoreDB.Analytics/NuGet.README.md @@ -53,7 +53,7 @@ Unlock enterprise-grade analytics with 100+ aggregate functions, window function ## 📦 Installation ```bash -dotnet add package SharpCoreDB.Analytics --version 2.1.1 +dotnet add package SharpCoreDB.Analytics --version 2.1.2 ``` **Requires:** SharpCoreDB v1.9.5+ diff --git a/src/SharpCoreDB.Analytics/SharpCoreDB.Analytics.csproj b/src/SharpCoreDB.Analytics/SharpCoreDB.Analytics.csproj index e85c6750..011495ba 100644 --- a/src/SharpCoreDB.Analytics/SharpCoreDB.Analytics.csproj +++ b/src/SharpCoreDB.Analytics/SharpCoreDB.Analytics.csproj @@ -4,7 +4,7 @@ net11.0 enable enable - 2.1.1 + 2.1.2 SharpCoreDB.Analytics @@ -13,7 +13,7 @@ SharpCoreDB.Analytics Analytics extensions for SharpCoreDB: aggregates, window functions, SIMD-accelerated analytics, and time-series helpers for .NET 11. Copyright (c) 2026 MPCoreDeveloper - v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md + v2.1.2 (2026-10-26): patch release with no API changes - a bound text parameter is no longer re-scanned as SQL text, so a value holding an apostrophe, a doubled quote, a backslash, a comma or a parenthesis now round-trips exactly instead of landing in the neighbouring column or failing with "Mismatched parentheses in VALUES clause"; full detail in docs/CHANGELOG.md. Previous: v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md analytics;aggregates;window-functions;statistics;timeseries;database;sharpcoredb;net11;csharp15 MIT https://github.com/MPCoreDeveloper/SharpCoreDB diff --git a/src/SharpCoreDB.Aspire.Hosting/SharpCoreDB.Aspire.Hosting.csproj b/src/SharpCoreDB.Aspire.Hosting/SharpCoreDB.Aspire.Hosting.csproj index cd41e596..46e67c02 100644 --- a/src/SharpCoreDB.Aspire.Hosting/SharpCoreDB.Aspire.Hosting.csproj +++ b/src/SharpCoreDB.Aspire.Hosting/SharpCoreDB.Aspire.Hosting.csproj @@ -16,7 +16,7 @@ SharpCoreDB.Aspire.Hosting - 2.1.1 + 2.1.2 .NET Aspire hosting integration for the SharpCoreDB network server @@ -33,7 +33,7 @@ SharpCoreDB.jpg true snupkg - v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): .NET Aspire hosting package for SharpCoreDB.Server - container image ghcr.io/mpcoredeveloper/sharpcoredb-server with HTTPS gRPC (5001) and HTTPS REST API (8443) endpoints. Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/server/ASPIRE_INTEGRATION.md + v2.1.2 (2026-10-26): patch release with no API changes - a bound text parameter is no longer re-scanned as SQL text, so a value holding an apostrophe, a doubled quote, a backslash, a comma or a parenthesis now round-trips exactly instead of landing in the neighbouring column or failing with "Mismatched parentheses in VALUES clause"; full detail in docs/CHANGELOG.md. Previous: v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): .NET Aspire hosting package for SharpCoreDB.Server - container image ghcr.io/mpcoredeveloper/sharpcoredb-server with HTTPS gRPC (5001) and HTTPS REST API (8443) endpoints. Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/server/ASPIRE_INTEGRATION.md diff --git a/src/SharpCoreDB.CQRS/NuGet.README.md b/src/SharpCoreDB.CQRS/NuGet.README.md index 9d1f4cbb..acf1156d 100644 --- a/src/SharpCoreDB.CQRS/NuGet.README.md +++ b/src/SharpCoreDB.CQRS/NuGet.README.md @@ -28,7 +28,7 @@ CQRS and outbox primitives for `SharpCoreDB`. ## Installation ```bash -dotnet add package SharpCoreDB.CQRS --version 2.1.1 +dotnet add package SharpCoreDB.CQRS --version 2.1.2 ``` ## Documentation diff --git a/src/SharpCoreDB.CQRS/SharpCoreDB.CQRS.csproj b/src/SharpCoreDB.CQRS/SharpCoreDB.CQRS.csproj index 92b1ae3d..ebd7f80a 100644 --- a/src/SharpCoreDB.CQRS/SharpCoreDB.CQRS.csproj +++ b/src/SharpCoreDB.CQRS/SharpCoreDB.CQRS.csproj @@ -11,7 +11,7 @@ $(NoWarn);1591;SA1101;SA1633;SA1309 SharpCoreDB.CQRS - 2.1.1 + 2.1.2 MPCoreDeveloper SharpCoreDB SharpCoreDB.CQRS @@ -24,7 +24,7 @@ git SharpCoreDB.jpg NuGet.README.md - v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md + v2.1.2 (2026-10-26): patch release with no API changes - a bound text parameter is no longer re-scanned as SQL text, so a value holding an apostrophe, a doubled quote, a backslash, a comma or a parenthesis now round-trips exactly instead of landing in the neighbouring column or failing with "Mismatched parentheses in VALUES clause"; full detail in docs/CHANGELOG.md. Previous: v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md diff --git a/src/SharpCoreDB.Client.Protocol/SharpCoreDB.Client.Protocol.csproj b/src/SharpCoreDB.Client.Protocol/SharpCoreDB.Client.Protocol.csproj index 2b483600..866e62f0 100644 --- a/src/SharpCoreDB.Client.Protocol/SharpCoreDB.Client.Protocol.csproj +++ b/src/SharpCoreDB.Client.Protocol/SharpCoreDB.Client.Protocol.csproj @@ -9,7 +9,7 @@ SharpCoreDB.Client.Protocol - 2.1.1 + 2.1.2 Client-side protocol implementation for SharpCoreDB (gRPC, binary) @@ -26,7 +26,7 @@ SharpCoreDB.jpg true snupkg - v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md + v2.1.2 (2026-10-26): patch release with no API changes - a bound text parameter is no longer re-scanned as SQL text, so a value holding an apostrophe, a doubled quote, a backslash, a comma or a parenthesis now round-trips exactly instead of landing in the neighbouring column or failing with "Mismatched parentheses in VALUES clause"; full detail in docs/CHANGELOG.md. Previous: v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md diff --git a/src/SharpCoreDB.Client/NuGet.README.md b/src/SharpCoreDB.Client/NuGet.README.md index 8be2f102..b5ab6c42 100644 --- a/src/SharpCoreDB.Client/NuGet.README.md +++ b/src/SharpCoreDB.Client/NuGet.README.md @@ -27,7 +27,7 @@ ## Installation ```bash -dotnet add package SharpCoreDB.Client --version 2.1.1 +dotnet add package SharpCoreDB.Client --version 2.1.2 ``` ## Documentation diff --git a/src/SharpCoreDB.Client/SharpCoreDB.Client.csproj b/src/SharpCoreDB.Client/SharpCoreDB.Client.csproj index 15cc0662..0c2dce07 100644 --- a/src/SharpCoreDB.Client/SharpCoreDB.Client.csproj +++ b/src/SharpCoreDB.Client/SharpCoreDB.Client.csproj @@ -9,7 +9,7 @@ SharpCoreDB.Client - 2.1.1 + 2.1.2 .NET client library for SharpCoreDB network server (ADO.NET-like API) diff --git a/src/SharpCoreDB.Data.Provider/NuGet.README.md b/src/SharpCoreDB.Data.Provider/NuGet.README.md index 76ce3ae4..98d8d328 100644 --- a/src/SharpCoreDB.Data.Provider/NuGet.README.md +++ b/src/SharpCoreDB.Data.Provider/NuGet.README.md @@ -57,7 +57,7 @@ while (reader.Read()) ## 📦 Installation ```bash -dotnet add package SharpCoreDB.Data.Provider --version 2.1.1 +dotnet add package SharpCoreDB.Data.Provider --version 2.1.2 ``` **Requires:** SharpCoreDB v1.9.5+ diff --git a/src/SharpCoreDB.Data.Provider/SharpCoreDB.Data.Provider.csproj b/src/SharpCoreDB.Data.Provider/SharpCoreDB.Data.Provider.csproj index 75998472..3e435a4f 100644 --- a/src/SharpCoreDB.Data.Provider/SharpCoreDB.Data.Provider.csproj +++ b/src/SharpCoreDB.Data.Provider/SharpCoreDB.Data.Provider.csproj @@ -16,7 +16,7 @@ https://github.com/MPCoreDeveloper/SharpCoreDB https://github.com/MPCoreDeveloper/SharpCoreDB ado.net;database;data-provider;sharpcoredb - 2.1.1 + 2.1.2 git NuGet.README.md SharpCoreDB.jpg @@ -24,7 +24,7 @@ snupkg SharpCoreDB.Data.Provider Data provider layer for SharpCoreDB on .NET 11. - v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md + v2.1.2 (2026-10-26): patch release with no API changes - a bound text parameter is no longer re-scanned as SQL text, so a value holding an apostrophe, a doubled quote, a backslash, a comma or a parenthesis now round-trips exactly instead of landing in the neighbouring column or failing with "Mismatched parentheses in VALUES clause"; full detail in docs/CHANGELOG.md. Previous: v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md false diff --git a/src/SharpCoreDB.Distributed/NuGet.README.md b/src/SharpCoreDB.Distributed/NuGet.README.md index 001fe3eb..2fc942a0 100644 --- a/src/SharpCoreDB.Distributed/NuGet.README.md +++ b/src/SharpCoreDB.Distributed/NuGet.README.md @@ -167,7 +167,7 @@ Multi-master replication, distributed transactions, and automatic sharding - Pha ## 📦 Installation ```bash -dotnet add package SharpCoreDB.Distributed --version 2.1.1 +dotnet add package SharpCoreDB.Distributed --version 2.1.2 ``` **Requires:** SharpCoreDB v1.9.5+ diff --git a/src/SharpCoreDB.Distributed/SharpCoreDB.Distributed.csproj b/src/SharpCoreDB.Distributed/SharpCoreDB.Distributed.csproj index d3e3d3d0..340ef6ec 100644 --- a/src/SharpCoreDB.Distributed/SharpCoreDB.Distributed.csproj +++ b/src/SharpCoreDB.Distributed/SharpCoreDB.Distributed.csproj @@ -15,7 +15,7 @@ SharpCoreDB.Distributed - 2.1.1 + 2.1.2 MPCoreDeveloper SharpCoreDB SharpCoreDB.Distributed @@ -28,7 +28,7 @@ git SharpCoreDB.jpg NuGet.README.md - v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md + v2.1.2 (2026-10-26): patch release with no API changes - a bound text parameter is no longer re-scanned as SQL text, so a value holding an apostrophe, a doubled quote, a backslash, a comma or a parenthesis now round-trips exactly instead of landing in the neighbouring column or failing with "Mismatched parentheses in VALUES clause"; full detail in docs/CHANGELOG.md. Previous: v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md diff --git a/src/SharpCoreDB.EntityFrameworkCore/NuGet.README.md b/src/SharpCoreDB.EntityFrameworkCore/NuGet.README.md index 3fb2117c..a380f776 100644 --- a/src/SharpCoreDB.EntityFrameworkCore/NuGet.README.md +++ b/src/SharpCoreDB.EntityFrameworkCore/NuGet.README.md @@ -67,7 +67,7 @@ var users = await context.Users.Where(u => u.IsActive).ToListAsync(); ## 📦 Installation ```bash -dotnet add package SharpCoreDB.EntityFrameworkCore --version 2.1.1 +dotnet add package SharpCoreDB.EntityFrameworkCore --version 2.1.2 ``` **Requires:** SharpCoreDB v1.9.5+, EntityFrameworkCore v8.0+ diff --git a/src/SharpCoreDB.EntityFrameworkCore/SharpCoreDB.EntityFrameworkCore.csproj b/src/SharpCoreDB.EntityFrameworkCore/SharpCoreDB.EntityFrameworkCore.csproj index 58c82450..921fa38d 100644 --- a/src/SharpCoreDB.EntityFrameworkCore/SharpCoreDB.EntityFrameworkCore.csproj +++ b/src/SharpCoreDB.EntityFrameworkCore/SharpCoreDB.EntityFrameworkCore.csproj @@ -8,13 +8,13 @@ SharpCoreDB.EntityFrameworkCore - 2.1.1 + 2.1.2 MPCoreDeveloper SharpCoreDB SharpCoreDB.EntityFrameworkCore - Entity Framework Core provider for SharpCoreDB encrypted database engine. Built for .NET 11 with C# 15. Supports Windows, Linux, macOS, Android, iOS, and IoT/embedded devices with platform-specific optimizations. Compatible with SharpCoreDB 2.1.1. + Entity Framework Core provider for SharpCoreDB encrypted database engine. Built for .NET 11 with C# 15. Supports Windows, Linux, macOS, Android, iOS, and IoT/embedded devices with platform-specific optimizations. Compatible with SharpCoreDB 2.1.2. Copyright (c) 2026 MPCoreDeveloper - v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md + v2.1.2 (2026-10-26): patch release with no API changes - a bound text parameter is no longer re-scanned as SQL text, so a value holding an apostrophe, a doubled quote, a backslash, a comma or a parenthesis now round-trips exactly instead of landing in the neighbouring column or failing with "Mismatched parentheses in VALUES clause"; full detail in docs/CHANGELOG.md. Previous: v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md sharpcoredb;entityframeworkcore;efcore;database;encryption;net11;csharp15;android;ios;mobile;iot;arm64;x64 MIT https://github.com/MPCoreDeveloper/SharpCoreDB diff --git a/src/SharpCoreDB.EventSourcing/NuGet.README.md b/src/SharpCoreDB.EventSourcing/NuGet.README.md index 60a869d2..c70e08d7 100644 --- a/src/SharpCoreDB.EventSourcing/NuGet.README.md +++ b/src/SharpCoreDB.EventSourcing/NuGet.README.md @@ -28,7 +28,7 @@ Event store primitives for `SharpCoreDB`. ## Installation ```bash -dotnet add package SharpCoreDB.EventSourcing --version 2.1.1 +dotnet add package SharpCoreDB.EventSourcing --version 2.1.2 ``` ## Documentation diff --git a/src/SharpCoreDB.EventSourcing/SharpCoreDB.EventSourcing.csproj b/src/SharpCoreDB.EventSourcing/SharpCoreDB.EventSourcing.csproj index a08a8293..6be3ed92 100644 --- a/src/SharpCoreDB.EventSourcing/SharpCoreDB.EventSourcing.csproj +++ b/src/SharpCoreDB.EventSourcing/SharpCoreDB.EventSourcing.csproj @@ -14,7 +14,7 @@ $(NoWarn);1591;SA1101;SA1633;SA1309 SharpCoreDB.EventSourcing - 2.1.1 + 2.1.2 MPCoreDeveloper SharpCoreDB SharpCoreDB.EventSourcing @@ -27,7 +27,7 @@ git SharpCoreDB.jpg NuGet.README.md - v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) with optimistic-concurrency conditional appends - IEventStore.TryAppendEventsAsync / TryAppendEventAsync with an expectedVersion, returning ConditionalAppendResult (Conflict carries the expected and actual stream lengths). This is the append-only ledger's compare-and-set primitive: a writer that lost the race is told so and nothing is written. + v2.1.2 (2026-10-26): patch release with no API changes - a bound text parameter is no longer re-scanned as SQL text, so a value holding an apostrophe, a doubled quote, a backslash, a comma or a parenthesis now round-trips exactly instead of landing in the neighbouring column or failing with "Mismatched parentheses in VALUES clause"; full detail in docs/CHANGELOG.md. Previous: v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) with optimistic-concurrency conditional appends - IEventStore.TryAppendEventsAsync / TryAppendEventAsync with an expectedVersion, returning ConditionalAppendResult (Conflict carries the expected and actual stream lengths). This is the append-only ledger's compare-and-set primitive: a writer that lost the race is told so and nothing is written. diff --git a/src/SharpCoreDB.Extensions/NuGet.README.md b/src/SharpCoreDB.Extensions/NuGet.README.md index 459de356..bfb88251 100644 --- a/src/SharpCoreDB.Extensions/NuGet.README.md +++ b/src/SharpCoreDB.Extensions/NuGet.README.md @@ -81,7 +81,7 @@ var userId = connection.QuerySingle( ## 📦 Installation ```bash -dotnet add package SharpCoreDB.Extensions --version 2.1.1 +dotnet add package SharpCoreDB.Extensions --version 2.1.2 ``` **Requires:** SharpCoreDB v1.9.5+ diff --git a/src/SharpCoreDB.Extensions/SharpCoreDB.Extensions.csproj b/src/SharpCoreDB.Extensions/SharpCoreDB.Extensions.csproj index f04347a2..b79bf528 100644 --- a/src/SharpCoreDB.Extensions/SharpCoreDB.Extensions.csproj +++ b/src/SharpCoreDB.Extensions/SharpCoreDB.Extensions.csproj @@ -8,13 +8,13 @@ SharpCoreDB.Extensions - 2.1.1 + 2.1.2 MPCoreDeveloper SharpCoreDB SharpCoreDB.Extensions Extensions for SharpCoreDB including Dapper integration and ASP.NET Core health checks. Built for .NET 11 with C# 15. Supports Windows, Linux, macOS, Android, iOS, and IoT/embedded devices with platform-specific optimizations. Copyright (c) 2026 MPCoreDeveloper - v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md + v2.1.2 (2026-10-26): patch release with no API changes - a bound text parameter is no longer re-scanned as SQL text, so a value holding an apostrophe, a doubled quote, a backslash, a comma or a parenthesis now round-trips exactly instead of landing in the neighbouring column or failing with "Mismatched parentheses in VALUES clause"; full detail in docs/CHANGELOG.md. Previous: v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md sharpcoredb;database;dapper;healthchecks;extensions;net11;csharp15;android;ios;mobile;iot;arm64;x64 MIT https://github.com/MPCoreDeveloper/SharpCoreDB diff --git a/src/SharpCoreDB.Functional.Dapper/SharpCoreDB.Functional.Dapper.csproj b/src/SharpCoreDB.Functional.Dapper/SharpCoreDB.Functional.Dapper.csproj index 6bb55a39..4329bf28 100644 --- a/src/SharpCoreDB.Functional.Dapper/SharpCoreDB.Functional.Dapper.csproj +++ b/src/SharpCoreDB.Functional.Dapper/SharpCoreDB.Functional.Dapper.csproj @@ -7,13 +7,13 @@ enable SharpCoreDB.Functional.Dapper - 2.1.1 + 2.1.2 MPCoreDeveloper SharpCoreDB SharpCoreDB.Functional.Dapper Dapper adapter for SharpCoreDB.Functional with zero-dependency Option/Fin APIs and fluent functional composition for .NET 11 and C# 15. Copyright (c) 2026 MPCoreDeveloper - v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md + v2.1.2 (2026-10-26): patch release with no API changes - a bound text parameter is no longer re-scanned as SQL text, so a value holding an apostrophe, a doubled quote, a backslash, a comma or a parenthesis now round-trips exactly instead of landing in the neighbouring column or failing with "Mismatched parentheses in VALUES clause"; full detail in docs/CHANGELOG.md. Previous: v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md sharpcoredb;functional;dapper;option;fin;net11;csharp15 MIT https://github.com/MPCoreDeveloper/SharpCoreDB diff --git a/src/SharpCoreDB.Functional.EntityFrameworkCore/SharpCoreDB.Functional.EntityFrameworkCore.csproj b/src/SharpCoreDB.Functional.EntityFrameworkCore/SharpCoreDB.Functional.EntityFrameworkCore.csproj index 221287e1..f058db7b 100644 --- a/src/SharpCoreDB.Functional.EntityFrameworkCore/SharpCoreDB.Functional.EntityFrameworkCore.csproj +++ b/src/SharpCoreDB.Functional.EntityFrameworkCore/SharpCoreDB.Functional.EntityFrameworkCore.csproj @@ -7,13 +7,13 @@ enable SharpCoreDB.Functional.EntityFrameworkCore - 2.1.1 + 2.1.2 MPCoreDeveloper SharpCoreDB SharpCoreDB.Functional.EntityFrameworkCore Entity Framework Core adapter for SharpCoreDB.Functional with zero-dependency Option/Fin APIs and fluent functional composition for .NET 11 and C# 15. Copyright (c) 2026 MPCoreDeveloper - v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md + v2.1.2 (2026-10-26): patch release with no API changes - a bound text parameter is no longer re-scanned as SQL text, so a value holding an apostrophe, a doubled quote, a backslash, a comma or a parenthesis now round-trips exactly instead of landing in the neighbouring column or failing with "Mismatched parentheses in VALUES clause"; full detail in docs/CHANGELOG.md. Previous: v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md sharpcoredb;functional;entityframeworkcore;efcore;option;fin;net11;csharp15 MIT https://github.com/MPCoreDeveloper/SharpCoreDB diff --git a/src/SharpCoreDB.Functional.Linq2DB/SharpCoreDB.Functional.Linq2DB.csproj b/src/SharpCoreDB.Functional.Linq2DB/SharpCoreDB.Functional.Linq2DB.csproj index b920ac20..eda09cd8 100644 --- a/src/SharpCoreDB.Functional.Linq2DB/SharpCoreDB.Functional.Linq2DB.csproj +++ b/src/SharpCoreDB.Functional.Linq2DB/SharpCoreDB.Functional.Linq2DB.csproj @@ -7,13 +7,13 @@ enable SharpCoreDB.Functional.Linq2DB - 2.1.1 + 2.1.2 MPCoreDeveloper SharpCoreDB SharpCoreDB.Functional.Linq2DB Production-ready linq2db adapter for SharpCoreDB.Functional. Zero-dependency Option/Fin/Seq APIs, compile-time LINQ safety, BulkCopy batching, full type mappings (ULID/GUID), and low-overhead queries. Perfect for AI/agentic and GraphRAG workloads on .NET 11 / C# 15. Copyright (c) 2026 MPCoreDeveloper - v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md + v2.1.2 (2026-10-26): patch release with no API changes - a bound text parameter is no longer re-scanned as SQL text, so a value holding an apostrophe, a doubled quote, a backslash, a comma or a parenthesis now round-trips exactly instead of landing in the neighbouring column or failing with "Mismatched parentheses in VALUES clause"; full detail in docs/CHANGELOG.md. Previous: v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md sharpcoredb;functional;linq2db;linq;option;fin;net11;csharp15;aot MIT https://github.com/MPCoreDeveloper/SharpCoreDB diff --git a/src/SharpCoreDB.Functional/SharpCoreDB.Functional.csproj b/src/SharpCoreDB.Functional/SharpCoreDB.Functional.csproj index d633a3bd..4970d5ea 100644 --- a/src/SharpCoreDB.Functional/SharpCoreDB.Functional.csproj +++ b/src/SharpCoreDB.Functional/SharpCoreDB.Functional.csproj @@ -7,13 +7,13 @@ enable SharpCoreDB.Functional - 2.1.1 + 2.1.2 MPCoreDeveloper SharpCoreDB SharpCoreDB.Functional Functional facade for SharpCoreDB with zero-dependency Option, Fin, Seq, and Unit types, fluent composition, and low-allocation wrappers for .NET 11 and C# 15. Copyright (c) 2026 MPCoreDeveloper - v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md + v2.1.2 (2026-10-26): patch release with no API changes - a bound text parameter is no longer re-scanned as SQL text, so a value holding an apostrophe, a doubled quote, a backslash, a comma or a parenthesis now round-trips exactly instead of landing in the neighbouring column or failing with "Mismatched parentheses in VALUES clause"; full detail in docs/CHANGELOG.md. Previous: v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md sharpcoredb;functional;option;result;fin;monad;net11;csharp15 MIT https://github.com/MPCoreDeveloper/SharpCoreDB diff --git a/src/SharpCoreDB.Graph.Advanced/NuGet.README.md b/src/SharpCoreDB.Graph.Advanced/NuGet.README.md index 54c59259..8db538cf 100644 --- a/src/SharpCoreDB.Graph.Advanced/NuGet.README.md +++ b/src/SharpCoreDB.Graph.Advanced/NuGet.README.md @@ -28,7 +28,7 @@ Advanced graph analytics and GraphRAG package for `SharpCoreDB`. ## Installation ```bash -dotnet add package SharpCoreDB.Graph.Advanced --version 2.1.1 +dotnet add package SharpCoreDB.Graph.Advanced --version 2.1.2 ``` ## Documentation diff --git a/src/SharpCoreDB.Graph.Advanced/SharpCoreDB.Graph.Advanced.csproj b/src/SharpCoreDB.Graph.Advanced/SharpCoreDB.Graph.Advanced.csproj index c0e7d07f..03813cf9 100644 --- a/src/SharpCoreDB.Graph.Advanced/SharpCoreDB.Graph.Advanced.csproj +++ b/src/SharpCoreDB.Graph.Advanced/SharpCoreDB.Graph.Advanced.csproj @@ -6,7 +6,7 @@ preview SharpCoreDB.Graph.Advanced SharpCoreDB.Graph.Advanced - 2.1.1 + 2.1.2 SharpCoreDB.Graph.Advanced MPCoreDeveloper MPCoreDeveloper diff --git a/src/SharpCoreDB.Graph/NuGet.README.md b/src/SharpCoreDB.Graph/NuGet.README.md index 303906b8..4414918e 100644 --- a/src/SharpCoreDB.Graph/NuGet.README.md +++ b/src/SharpCoreDB.Graph/NuGet.README.md @@ -53,7 +53,7 @@ A* pathfinding and graph algorithms **30-50% faster than alternatives** with pur ## 📦 Installation ```bash -dotnet add package SharpCoreDB.Graph --version 2.1.1 +dotnet add package SharpCoreDB.Graph --version 2.1.2 ``` **Requires:** SharpCoreDB v1.9.5+ diff --git a/src/SharpCoreDB.Graph/SharpCoreDB.Graph.csproj b/src/SharpCoreDB.Graph/SharpCoreDB.Graph.csproj index 2a6ca1ad..2537683d 100644 --- a/src/SharpCoreDB.Graph/SharpCoreDB.Graph.csproj +++ b/src/SharpCoreDB.Graph/SharpCoreDB.Graph.csproj @@ -13,7 +13,7 @@ $(NoWarn);1591;SA1101;SA1633;SA1309 SharpCoreDB.Graph - 2.1.1 + 2.1.2 MPCoreDeveloper SharpCoreDB SharpCoreDB.Graph @@ -26,7 +26,7 @@ git SharpCoreDB.jpg NuGet.README.md - v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md + v2.1.2 (2026-10-26): patch release with no API changes - a bound text parameter is no longer re-scanned as SQL text, so a value holding an apostrophe, a doubled quote, a backslash, a comma or a parenthesis now round-trips exactly instead of landing in the neighbouring column or failing with "Mismatched parentheses in VALUES clause"; full detail in docs/CHANGELOG.md. Previous: v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md diff --git a/src/SharpCoreDB.HybridSearch/SharpCoreDB.HybridSearch.csproj b/src/SharpCoreDB.HybridSearch/SharpCoreDB.HybridSearch.csproj index 24acf2cc..e06ccfe7 100644 --- a/src/SharpCoreDB.HybridSearch/SharpCoreDB.HybridSearch.csproj +++ b/src/SharpCoreDB.HybridSearch/SharpCoreDB.HybridSearch.csproj @@ -12,7 +12,7 @@ $(NoWarn);1591;SA1101;SA1633;SA1309 SharpCoreDB.HybridSearch - 2.1.1 + 2.1.2 MPCoreDeveloper SharpCoreDB SharpCoreDB.HybridSearch @@ -25,7 +25,7 @@ git SharpCoreDB.jpg NuGet.README.md - v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview). Fuses the BM25 lexical leg and the vector leg by RANK (never by raw score), with per-leg ranks carried on every hit. + v2.1.2 (2026-10-26): patch release with no API changes - a bound text parameter is no longer re-scanned as SQL text, so a value holding an apostrophe, a doubled quote, a backslash, a comma or a parenthesis now round-trips exactly instead of landing in the neighbouring column or failing with "Mismatched parentheses in VALUES clause"; full detail in docs/CHANGELOG.md. Previous: v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview). Fuses the BM25 lexical leg and the vector leg by RANK (never by raw score), with per-leg ranks carried on every hit. diff --git a/src/SharpCoreDB.Identity/SharpCoreDB.Identity.csproj b/src/SharpCoreDB.Identity/SharpCoreDB.Identity.csproj index d74512cf..517af018 100644 --- a/src/SharpCoreDB.Identity/SharpCoreDB.Identity.csproj +++ b/src/SharpCoreDB.Identity/SharpCoreDB.Identity.csproj @@ -8,7 +8,7 @@ true SharpCoreDB.Identity - 2.1.1 + 2.1.2 MPCoreDeveloper SharpCoreDB SharpCoreDB.Identity diff --git a/src/SharpCoreDB.Projections/NuGet.README.md b/src/SharpCoreDB.Projections/NuGet.README.md index 61a47923..c2bfcc02 100644 --- a/src/SharpCoreDB.Projections/NuGet.README.md +++ b/src/SharpCoreDB.Projections/NuGet.README.md @@ -28,7 +28,7 @@ Projection primitives for `SharpCoreDB.EventSourcing`. ## Installation ```bash -dotnet add package SharpCoreDB.Projections --version 2.1.1 +dotnet add package SharpCoreDB.Projections --version 2.1.2 ``` ## Documentation diff --git a/src/SharpCoreDB.Projections/SharpCoreDB.Projections.csproj b/src/SharpCoreDB.Projections/SharpCoreDB.Projections.csproj index 58ab88b5..ccfaacd7 100644 --- a/src/SharpCoreDB.Projections/SharpCoreDB.Projections.csproj +++ b/src/SharpCoreDB.Projections/SharpCoreDB.Projections.csproj @@ -11,7 +11,7 @@ $(NoWarn);1591;SA1101;SA1633;SA1309 SharpCoreDB.Projections - 2.1.1 + 2.1.2 MPCoreDeveloper SharpCoreDB SharpCoreDB.Projections @@ -24,7 +24,7 @@ git SharpCoreDB.jpg NuGet.README.md - v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md + v2.1.2 (2026-10-26): patch release with no API changes - a bound text parameter is no longer re-scanned as SQL text, so a value holding an apostrophe, a doubled quote, a backslash, a comma or a parenthesis now round-trips exactly instead of landing in the neighbouring column or failing with "Mismatched parentheses in VALUES clause"; full detail in docs/CHANGELOG.md. Previous: v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md diff --git a/src/SharpCoreDB.Provider.Sync/NuGet.README.md b/src/SharpCoreDB.Provider.Sync/NuGet.README.md index ec7fd884..d720326d 100644 --- a/src/SharpCoreDB.Provider.Sync/NuGet.README.md +++ b/src/SharpCoreDB.Provider.Sync/NuGet.README.md @@ -71,7 +71,7 @@ Console.WriteLine($"Synced {result.TotalChangesDownloaded} changes from cloud"); ## 📦 Installation ```bash -dotnet add package SharpCoreDB.Provider.Sync --version 2.1.1 +dotnet add package SharpCoreDB.Provider.Sync --version 2.1.2 ``` **Requires:** SharpCoreDB v1.9.5+, Dotmim.Sync.Core v1.3.0+ diff --git a/src/SharpCoreDB.Provider.Sync/SharpCoreDB.Provider.Sync.csproj b/src/SharpCoreDB.Provider.Sync/SharpCoreDB.Provider.Sync.csproj index 072454ac..2eb8f1f2 100644 --- a/src/SharpCoreDB.Provider.Sync/SharpCoreDB.Provider.Sync.csproj +++ b/src/SharpCoreDB.Provider.Sync/SharpCoreDB.Provider.Sync.csproj @@ -9,7 +9,7 @@ SharpCoreDB.Provider.Sync - 2.1.1 + 2.1.2 MPCoreDeveloper SharpCoreDB SharpCoreDB.Provider.Sync diff --git a/src/SharpCoreDB.Provider.YesSql/NuGet.README.md b/src/SharpCoreDB.Provider.YesSql/NuGet.README.md index fcabf070..39a155e4 100644 --- a/src/SharpCoreDB.Provider.YesSql/NuGet.README.md +++ b/src/SharpCoreDB.Provider.YesSql/NuGet.README.md @@ -36,7 +36,7 @@ YesSql ORM integration with SharpCoreDB's encryption and performance for documen ## 📦 Installation ```bash -dotnet add package SharpCoreDB.Provider.YesSql --version 2.1.1 +dotnet add package SharpCoreDB.Provider.YesSql --version 2.1.2 ``` **Requires:** SharpCoreDB v1.9.5+, YesSql.Core v5.4.7+ diff --git a/src/SharpCoreDB.Provider.YesSql/SharpCoreDB.Provider.YesSql.csproj b/src/SharpCoreDB.Provider.YesSql/SharpCoreDB.Provider.YesSql.csproj index cb49012e..a8d61a2d 100644 --- a/src/SharpCoreDB.Provider.YesSql/SharpCoreDB.Provider.YesSql.csproj +++ b/src/SharpCoreDB.Provider.YesSql/SharpCoreDB.Provider.YesSql.csproj @@ -8,13 +8,13 @@ SharpCoreDB.Provider.YesSql - 2.1.1 + 2.1.2 MPCoreDeveloper SharpCoreDB SharpCoreDB.Provider.YesSql YesSql provider for SharpCoreDB encrypted database engine. Built for .NET 11 with C# 15. Supports Windows, Linux, macOS, Android, iOS, and IoT/embedded devices with platform-specific optimizations. Copyright (c) 2026 MPCoreDeveloper - v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md + v2.1.2 (2026-10-26): patch release with no API changes - a bound text parameter is no longer re-scanned as SQL text, so a value holding an apostrophe, a doubled quote, a backslash, a comma or a parenthesis now round-trips exactly instead of landing in the neighbouring column or failing with "Mismatched parentheses in VALUES clause"; full detail in docs/CHANGELOG.md. Previous: v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md sharpcoredb;yessql;database;orm;net11;csharp15;android;ios;mobile;iot;arm64;x64 MIT https://github.com/MPCoreDeveloper/SharpCoreDB diff --git a/src/SharpCoreDB.Search/SharpCoreDB.Search.csproj b/src/SharpCoreDB.Search/SharpCoreDB.Search.csproj index e1318f2e..e39aad2f 100644 --- a/src/SharpCoreDB.Search/SharpCoreDB.Search.csproj +++ b/src/SharpCoreDB.Search/SharpCoreDB.Search.csproj @@ -12,7 +12,7 @@ $(NoWarn);1591;SA1101;SA1633;SA1309 SharpCoreDB.Search - 2.1.1 + 2.1.2 MPCoreDeveloper SharpCoreDB SharpCoreDB.Search @@ -25,7 +25,7 @@ git SharpCoreDB.jpg NuGet.README.md - v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview). Munarium-inspired lexical stack: a classifying tokenizer (numbers, sign absorption, hyphen compounds), PostgreSQL-16 English stop words, an English word-only stemmer and a BM25 inverted index with positions. + v2.1.2 (2026-10-26): patch release with no API changes - a bound text parameter is no longer re-scanned as SQL text, so a value holding an apostrophe, a doubled quote, a backslash, a comma or a parenthesis now round-trips exactly instead of landing in the neighbouring column or failing with "Mismatched parentheses in VALUES clause"; full detail in docs/CHANGELOG.md. Previous: v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview). Munarium-inspired lexical stack: a classifying tokenizer (numbers, sign absorption, hyphen compounds), PostgreSQL-16 English stop words, an English word-only stemmer and a BM25 inverted index with positions. diff --git a/src/SharpCoreDB.Serilog.Sinks/NuGet.README.md b/src/SharpCoreDB.Serilog.Sinks/NuGet.README.md index 6e811497..f994e3f0 100644 --- a/src/SharpCoreDB.Serilog.Sinks/NuGet.README.md +++ b/src/SharpCoreDB.Serilog.Sinks/NuGet.README.md @@ -53,7 +53,7 @@ Log.Error(ex, "An error occurred"); ## 📦 Installation ```bash -dotnet add package SharpCoreDB.Serilog.Sinks --version 2.1.1 +dotnet add package SharpCoreDB.Serilog.Sinks --version 2.1.2 ``` **Requires:** SharpCoreDB v1.9.5+, Serilog v2.13+ diff --git a/src/SharpCoreDB.Serilog.Sinks/SharpCoreDB.Serilog.Sinks.csproj b/src/SharpCoreDB.Serilog.Sinks/SharpCoreDB.Serilog.Sinks.csproj index 58e19c1d..3bff6085 100644 --- a/src/SharpCoreDB.Serilog.Sinks/SharpCoreDB.Serilog.Sinks.csproj +++ b/src/SharpCoreDB.Serilog.Sinks/SharpCoreDB.Serilog.Sinks.csproj @@ -7,7 +7,7 @@ SharpCoreDB.Serilog.Sinks - 2.1.1 + 2.1.2 MPCoreDeveloper SharpCoreDB SharpCoreDB.Serilog.Sinks @@ -24,7 +24,7 @@ false true snupkg - v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md + v2.1.2 (2026-10-26): patch release with no API changes - a bound text parameter is no longer re-scanned as SQL text, so a value holding an apostrophe, a doubled quote, a backslash, a comma or a parenthesis now round-trips exactly instead of landing in the neighbouring column or failing with "Mismatched parentheses in VALUES clause"; full detail in docs/CHANGELOG.md. Previous: v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md diff --git a/src/SharpCoreDB.Server.Core/BinaryProtocolHandler.cs b/src/SharpCoreDB.Server.Core/BinaryProtocolHandler.cs index 1d7b9122..9cb4ecc7 100644 --- a/src/SharpCoreDB.Server.Core/BinaryProtocolHandler.cs +++ b/src/SharpCoreDB.Server.Core/BinaryProtocolHandler.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using SharpCoreDB.Server.Core; +using SharpCoreDB.Services; using SharpCoreDB.Server.Core.Catalog; using SharpCoreDB.Server.Core.Observability; using SharpCoreDB.Server.Core.Security; @@ -1118,27 +1119,30 @@ private static string TranslateDollarParameters(string sql) } var sb = new StringBuilder(sql.Length); - bool inString = false; - char stringChar = '\0'; for (int i = 0; i < sql.Length; i++) { char c = sql[i]; - if ((c == '\'' || c == '"') && (i == 0 || sql[i - 1] != '\\')) + // A quoted literal is copied verbatim and never translated. SqlLiteralScanner is the single quote + // definition (a doubled quote is one quote and does not close the literal; a backslash is data), so a + // '$1' inside a value stays text here — the previous form exited the literal at the first quote it saw + // after a doubled one and rewrote it. + if (c is '\'' or '"') { - if (!inString) + int closing = SqlLiteralScanner.FindClosingQuote(sql, i, out _); + if (closing < 0) { - inString = true; - stringChar = c; - } - else if (c == stringChar) - { - inString = false; + sb.Append(sql, i, sql.Length - i); // unterminated literal: take the rest as written + break; } + + sb.Append(sql, i, closing - i + 1); + i = closing; + continue; } - if (!inString && c == '$' && i + 1 < sql.Length && char.IsDigit(sql[i + 1])) + if (c == '$' && i + 1 < sql.Length && char.IsDigit(sql[i + 1])) { // Skip '$' and all following digits; emit a single '?'. i++; diff --git a/src/SharpCoreDB.Server.Core/SharpCoreDB.Server.Core.csproj b/src/SharpCoreDB.Server.Core/SharpCoreDB.Server.Core.csproj index 0f55a5fa..7fd75ad9 100644 --- a/src/SharpCoreDB.Server.Core/SharpCoreDB.Server.Core.csproj +++ b/src/SharpCoreDB.Server.Core/SharpCoreDB.Server.Core.csproj @@ -9,7 +9,7 @@ SharpCoreDB.Server.Core - 2.1.1 + 2.1.2 Core server infrastructure for SharpCoreDB network server @@ -26,7 +26,7 @@ SharpCoreDB.jpg true snupkg - v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md + v2.1.2 (2026-10-26): patch release with no API changes - a bound text parameter is no longer re-scanned as SQL text, so a value holding an apostrophe, a doubled quote, a backslash, a comma or a parenthesis now round-trips exactly instead of landing in the neighbouring column or failing with "Mismatched parentheses in VALUES clause"; full detail in docs/CHANGELOG.md. Previous: v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md diff --git a/src/SharpCoreDB.Server.Protocol/SharpCoreDB.Server.Protocol.csproj b/src/SharpCoreDB.Server.Protocol/SharpCoreDB.Server.Protocol.csproj index 5e77c1b3..e8d4871c 100644 --- a/src/SharpCoreDB.Server.Protocol/SharpCoreDB.Server.Protocol.csproj +++ b/src/SharpCoreDB.Server.Protocol/SharpCoreDB.Server.Protocol.csproj @@ -9,7 +9,7 @@ SharpCoreDB.Server.Protocol - 2.1.1 + 2.1.2 Network protocol definitions for SharpCoreDB server (gRPC, binary, HTTP) @@ -26,7 +26,7 @@ SharpCoreDB.jpg true snupkg - v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md + v2.1.2 (2026-10-26): patch release with no API changes - a bound text parameter is no longer re-scanned as SQL text, so a value holding an apostrophe, a doubled quote, a backslash, a comma or a parenthesis now round-trips exactly instead of landing in the neighbouring column or failing with "Mismatched parentheses in VALUES clause"; full detail in docs/CHANGELOG.md. Previous: v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): net11.0-only (C# 15 preview) release line - 2,897 tests green across 16 suites; the fixed-width inline capacity ships at 16 by default on both storage paths (batched multi-row INSERT measured +12 to +19 percent); the overflow arena writes a row's payloads in one append instead of one file open per value; and in-place UPDATE/DELETE with deferred index maintenance put the fixed-width fair-PK layout ahead of SQLite on UPDATE (0.8x) and DELETE (0.5x). Full details: https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/CHANGELOG.md diff --git a/src/SharpCoreDB.Server/NuGet.README.md b/src/SharpCoreDB.Server/NuGet.README.md index c08c79b5..d4ca8640 100644 --- a/src/SharpCoreDB.Server/NuGet.README.md +++ b/src/SharpCoreDB.Server/NuGet.README.md @@ -35,7 +35,7 @@ Network database server package for `SharpCoreDB`. ## Installation ```bash -dotnet add package SharpCoreDB.Server.Core --version 2.1.1 +dotnet add package SharpCoreDB.Server.Core --version 2.1.2 ``` ## Documentation diff --git a/src/SharpCoreDB.Server/README.md b/src/SharpCoreDB.Server/README.md index b1515812..c4b22fb6 100644 --- a/src/SharpCoreDB.Server/README.md +++ b/src/SharpCoreDB.Server/README.md @@ -38,8 +38,8 @@ docker run -p 5001:5001 -p 8443:8443 ghcr.io/mpcoredeveloper/sharpcoredb-server: The packable server-side libraries are: ```bash -dotnet add package SharpCoreDB.Server.Core --version 2.1.0-RC.3 -dotnet add package SharpCoreDB.Server.Protocol --version 2.1.0-RC.3 +dotnet add package SharpCoreDB.Server.Core --version 2.1.2 +dotnet add package SharpCoreDB.Server.Protocol --version 2.1.2 ``` ## Documentation diff --git a/src/SharpCoreDB.Server/SharpCoreDB.Server.csproj b/src/SharpCoreDB.Server/SharpCoreDB.Server.csproj index 5ebecfd2..937420c0 100644 --- a/src/SharpCoreDB.Server/SharpCoreDB.Server.csproj +++ b/src/SharpCoreDB.Server/SharpCoreDB.Server.csproj @@ -19,7 +19,7 @@ sharpcoredb-server SharpCoreDB Network Server SharpCoreDB network database server with gRPC and HTTP support - 2.1.1 + 2.1.2 MPCoreDeveloper SharpCoreDB Copyright (c) 2026 MPCoreDeveloper diff --git a/src/SharpCoreDB.VectorSearch/NuGet.README.md b/src/SharpCoreDB.VectorSearch/NuGet.README.md index 5964c307..3acee588 100644 --- a/src/SharpCoreDB.VectorSearch/NuGet.README.md +++ b/src/SharpCoreDB.VectorSearch/NuGet.README.md @@ -54,7 +54,7 @@ Semantic search and similarity matching **50-100x faster than SQLite** using HNS ## 📦 Installation ```bash -dotnet add package SharpCoreDB.VectorSearch --version 2.1.1 +dotnet add package SharpCoreDB.VectorSearch --version 2.1.2 ``` **Requires:** SharpCoreDB v1.9.5+ diff --git a/src/SharpCoreDB.VectorSearch/SharpCoreDB.VectorSearch.csproj b/src/SharpCoreDB.VectorSearch/SharpCoreDB.VectorSearch.csproj index c2a2944f..a7af6e64 100644 --- a/src/SharpCoreDB.VectorSearch/SharpCoreDB.VectorSearch.csproj +++ b/src/SharpCoreDB.VectorSearch/SharpCoreDB.VectorSearch.csproj @@ -15,7 +15,7 @@ SharpCoreDB.VectorSearch - 2.1.1 + 2.1.2 MPCoreDeveloper SharpCoreDB SharpCoreDB.VectorSearch @@ -28,7 +28,7 @@ git SharpCoreDB.jpg NuGet.README.md - v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): Munarium-inspired native C# features - DiskAnn index type, content-verified artifacts (canonical manifest + SHA256 artifact_id + verification), hybrid fusion weights, C# 15 record-based BuildResult for strict error handling. All changes are pure managed .NET 11 / C# 15 on the release/v2.1.0.0 branch. See docs/Vectors/MUNARIUM_INSPIRATION_PLAN.md. + v2.1.2 (2026-10-26): patch release with no API changes - a bound text parameter is no longer re-scanned as SQL text, so a value holding an apostrophe, a doubled quote, a backslash, a comma or a parenthesis now round-trips exactly instead of landing in the neighbouring column or failing with "Mismatched parentheses in VALUES clause"; full detail in docs/CHANGELOG.md. Previous: v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): Munarium-inspired native C# features - DiskAnn index type, content-verified artifacts (canonical manifest + SHA256 artifact_id + verification), hybrid fusion weights, C# 15 record-based BuildResult for strict error handling. All changes are pure managed .NET 11 / C# 15 on the release/v2.1.0.0 branch. See docs/Vectors/MUNARIUM_INSPIRATION_PLAN.md. diff --git a/src/SharpCoreDB/DataStructures/Table.Collation.cs b/src/SharpCoreDB/DataStructures/Table.Collation.cs index 907595a2..2e985426 100644 --- a/src/SharpCoreDB/DataStructures/Table.Collation.cs +++ b/src/SharpCoreDB/DataStructures/Table.Collation.cs @@ -3,6 +3,7 @@ namespace SharpCoreDB.DataStructures; using System; using System.Collections.Generic; using System.Linq; +using SharpCoreDB.Services; /// /// ✅ Phase 5: Collation-aware query execution support for WHERE, ORDER BY, DISTINCT, and GROUP BY. @@ -91,11 +92,10 @@ private static bool EvaluateInOperator(string? rowValue, string? listStr, Collat if (string.IsNullOrEmpty(listStr)) return false; - // Parse comma-separated list (remove parentheses, spaces) - var list = listStr - .Trim('(', ')') - .Split(',') - .Select(s => s.Trim().Trim('\'', '"')) + // Parse comma-separated list (remove parentheses, spaces). SplitTopLevelComma keeps a comma inside a + // quoted item together, and Unquote reads the item with SQL's rules ('' is one quote). + var list = SqlInPredicate.SplitTopLevelComma(listStr.Trim('(', ')')) + .Select(s => SqlLiteralScanner.Unquote(s.Trim())) .ToList(); // Check if rowValue equals any item in the list @@ -375,10 +375,8 @@ private static bool EvaluateInOperatorWithLocale(string? rowValue, string? listS if (string.IsNullOrEmpty(listStr)) return false; - var list = listStr - .Trim('(', ')') - .Split(',') - .Select(s => s.Trim().Trim('\'', '"')) + var list = SqlInPredicate.SplitTopLevelComma(listStr.Trim('(', ')')) + .Select(s => SqlLiteralScanner.Unquote(s.Trim())) .ToList(); foreach (var item in list) diff --git a/src/SharpCoreDB/DataStructures/Table.QueryHelpers.cs b/src/SharpCoreDB/DataStructures/Table.QueryHelpers.cs index 8c34ce40..ba179230 100644 --- a/src/SharpCoreDB/DataStructures/Table.QueryHelpers.cs +++ b/src/SharpCoreDB/DataStructures/Table.QueryHelpers.cs @@ -8,6 +8,7 @@ namespace SharpCoreDB.DataStructures; using System; using System.Collections.Generic; using System.Linq; +using SharpCoreDB.Services; /// /// Query helper methods for Table - WHERE clause parsing, row deserialization, result ordering. @@ -53,7 +54,11 @@ private static bool TryParseSimpleWhereClause(string where, out string column, o } ReadOnlySpan colSpan = span[..eq].Trim().Trim("\"[]`".AsSpan()); - ReadOnlySpan valSpan = span[(eq + 1)..].Trim().Trim("'\"".AsSpan()); + + // The value stays as written: Unquote below removes exactly one pair of quotes and collapses SQL doubling + // (''), so a value holding an apostrophe resolves to the same key it was stored under. Trimming the quote + // characters here left the doubling in place and stripped every edge quote (known issue 7). + ReadOnlySpan valSpan = span[(eq + 1)..].Trim(); if (colSpan.IsEmpty || valSpan.IsEmpty) { @@ -70,7 +75,7 @@ private static bool TryParseSimpleWhereClause(string where, out string column, o } column = colSpan.ToString(); - value = valSpan.ToString(); + value = SqlLiteralScanner.Unquote(valSpan.ToString()); return true; } @@ -132,8 +137,8 @@ private static bool TryParseRangeWhereClause( if (parts.Length == 3) { column = parts[0]; - rangeStart = parts[1].Trim('\'', '"'); - rangeEnd = parts[2].Trim('\'', '"'); + rangeStart = SqlLiteralScanner.Unquote(parts[1].Trim()); + rangeEnd = SqlLiteralScanner.Unquote(parts[2].Trim()); return true; } } @@ -145,7 +150,7 @@ private static bool TryParseRangeWhereClause( if (parts.Length == 2) { column = parts[0].Trim(); - rangeStart = parts[1].Trim().Trim('\'', '"'); + rangeStart = SqlLiteralScanner.Unquote(parts[1].Trim()); rangeEnd = "9999999999"; // Large number for upper bound return true; } @@ -159,7 +164,7 @@ private static bool TryParseRangeWhereClause( if (parts.Length == 2) { column = parts[0].Trim(); - rangeStart = parts[1].Trim().Trim('\'', '"'); + rangeStart = SqlLiteralScanner.Unquote(parts[1].Trim()); rangeEnd = "9999999999"; return true; } @@ -173,7 +178,7 @@ private static bool TryParseRangeWhereClause( { column = parts[0].Trim(); rangeStart = "0"; // Minimum value - rangeEnd = parts[1].Trim().Trim('\'', '"'); + rangeEnd = SqlLiteralScanner.Unquote(parts[1].Trim()); return true; } } @@ -186,7 +191,7 @@ private static bool TryParseRangeWhereClause( { column = parts[0].Trim(); rangeStart = "0"; - rangeEnd = parts[1].Trim().Trim('\'', '"'); + rangeEnd = SqlLiteralScanner.Unquote(parts[1].Trim()); return true; } } diff --git a/src/SharpCoreDB/DataStructures/Table.Scanning.cs b/src/SharpCoreDB/DataStructures/Table.Scanning.cs index c487f6f2..41dd19f2 100644 --- a/src/SharpCoreDB/DataStructures/Table.Scanning.cs +++ b/src/SharpCoreDB/DataStructures/Table.Scanning.cs @@ -225,8 +225,11 @@ private bool EvaluateWhere(Dictionary row, string? where) // split-based evaluator treats a leading "(" as part of the column name). where = SqlInPredicate.StripOuterParentheses(where); - var parts = where.Split(' ', StringSplitOptions.RemoveEmptyEntries); - if (parts.Length < 3) return true; + // Tokenised on top-level whitespace (SqlLiteralScanner): a quoted value keeps its spaces and its doubled + // quotes, so 'it''s fine, really' is one token and compares as it''s→it's after Unquote. Splitting on ' ' + // truncated such a value at its first space and the row could never match (known issue 7's shape). + var parts = SqlLiteralScanner.SplitTopLevelWhitespace(where); + if (parts.Count < 3) return true; // ✅ Issue #348: split compound conditions on top-level AND/OR and evaluate each // operand recursively (via the full single-condition evaluator below). The previous @@ -266,7 +269,7 @@ private bool EvaluateWhere(Dictionary row, string? where) // SqlParser evaluator, which now implements SQL-correct semantics (NULL never matches // LIKE, %/_ patterns, BETWEEN as inclusive range). Previously these fell through the // switch's default → return true (no filtering) in this directory-mode scan path. - if (parts.Length >= 2 && + if (parts.Count >= 2 && (parts[1].Equals("LIKE", StringComparison.OrdinalIgnoreCase) || parts[1].Equals("NOT LIKE", StringComparison.OrdinalIgnoreCase) || parts[1].Equals("BETWEEN", StringComparison.OrdinalIgnoreCase))) @@ -287,7 +290,7 @@ private bool EvaluateWhere(Dictionary row, string? where) } // Handle IS NOT NULL (4 tokens: column IS NOT NULL) - if (parts.Length >= 4 + if (parts.Count >= 4 && parts[1].Equals("IS", StringComparison.OrdinalIgnoreCase) && parts[2].Equals("NOT", StringComparison.OrdinalIgnoreCase) && parts[3].Equals("NULL", StringComparison.OrdinalIgnoreCase)) @@ -303,17 +306,17 @@ private bool EvaluateWhere(Dictionary row, string? where) } var op = parts[1]; - var value = parts[2].Trim('"').Trim((char)39); + var value = SqlLiteralScanner.Unquote(parts[2]); // Handle multi-word operators: NOT REGEXP, NOT LIKE, NOT IN (4 tokens: col NOT OP val) - if (parts.Length >= 4 + if (parts.Count >= 4 && parts[1].Equals("NOT", StringComparison.OrdinalIgnoreCase) && (parts[2].Equals("REGEXP", StringComparison.OrdinalIgnoreCase) || parts[2].Equals("LIKE", StringComparison.OrdinalIgnoreCase) || parts[2].Equals("IN", StringComparison.OrdinalIgnoreCase))) { op = $"NOT {parts[2].ToUpperInvariant()}"; - value = parts[3].Trim('"').Trim((char)39); + value = SqlLiteralScanner.Unquote(parts[3]); } if (!row.TryGetValue(columnName, out var rowValue) || rowValue == null) diff --git a/src/SharpCoreDB/Database/Execution/Database.Batch.cs b/src/SharpCoreDB/Database/Execution/Database.Batch.cs index ecf832c2..0d15dc6e 100644 --- a/src/SharpCoreDB/Database/Execution/Database.Batch.cs +++ b/src/SharpCoreDB/Database/Execution/Database.Batch.cs @@ -111,37 +111,43 @@ public Dictionary ParseValues(ReadOnlySpan valuesClause) int valueStart = 0; int valueIndex = 0; - bool inQuotes = false; int parenDepth = 0; int bracketDepth = 0; // bracketed literals ([0.5, 0.25] — a VECTOR value) never split - + for (int i = 0; i < valuesClause.Length && valueIndex < Columns.Count; i++) { char c = valuesClause[i]; - - if (c == '\'' && (i == 0 || valuesClause[i - 1] != '\\')) + + // Skip a whole literal through SqlLiteralScanner (a doubled quote is one quote and does not close + // the literal; a backslash is data), so a comma inside a value cannot split the row. + if (c == '\'') { - inQuotes = !inQuotes; + int closing = SqlLiteralScanner.FindClosingQuote(valuesClause, i, out _); + if (closing < 0) + { + break; + } + + i = closing; + continue; } - else if (!inQuotes) - { - if (c == '(') parenDepth++; - else if (c == ')') parenDepth--; - else if (c == '[') bracketDepth++; - else if (c == ']' && bracketDepth > 0) bracketDepth--; - else if (c == ',' && parenDepth == 0 && bracketDepth == 0) - { - // ✅ FIX: Strict bounds check before Slice to prevent ArgumentOutOfRangeException - if (valueStart < i && valueIndex < ColumnTypes.Count) + + if (c == '(') parenDepth++; + else if (c == ')') parenDepth--; + else if (c == '[') bracketDepth++; + else if (c == ']' && bracketDepth > 0) bracketDepth--; + else if (c == ',' && parenDepth == 0 && bracketDepth == 0) { - var valueSpan = valuesClause.Slice(valueStart, i - valueStart).Trim(); - var value = ParseValueFast(valueSpan, ColumnTypes[valueIndex]); - row[Columns[valueIndex]] = value ?? DBNull.Value; - } - - valueStart = i + 1; - valueIndex++; - } + // ✅ FIX: Strict bounds check before Slice to prevent ArgumentOutOfRangeException + if (valueStart < i && valueIndex < ColumnTypes.Count) + { + var valueSpan = valuesClause.Slice(valueStart, i - valueStart).Trim(); + var value = ParseValueFast(valueSpan, ColumnTypes[valueIndex]); + row[Columns[valueIndex]] = value ?? DBNull.Value; + } + + valueStart = i + 1; + valueIndex++; } } @@ -178,26 +184,35 @@ public object[] ParseValuesToArray(ReadOnlySpan valuesClause) int valueStart = 0; int valueIndex = 0; - bool inQuotes = false; int parenDepth = 0; + int bracketDepth = 0; for (int i = 0; i < valuesClause.Length && valueIndex < Columns.Count; i++) { char c = valuesClause[i]; - if (IsQuoteChar(c, i, valuesClause)) - { - inQuotes = !inQuotes; - } - else if (!inQuotes) + // Whole literals are skipped (SqlLiteralScanner is the single quote definition), so a comma or a + // parenthesis inside a value cannot end it — and a bracketed VECTOR literal stays together. + if (c == '\'') { - if (c == '(') parenDepth++; - else if (c == ')') parenDepth--; - else if (c == ',' && parenDepth == 0) + int closing = SqlLiteralScanner.FindClosingQuote(valuesClause, i, out _); + if (closing < 0) { - CommitParsedValue(valuesClause, i, ref valueStart, valueIndex, values); - valueIndex++; + break; } + + i = closing; + continue; + } + + if (c == '(') parenDepth++; + else if (c == ')') parenDepth--; + else if (c == '[') bracketDepth++; + else if (c == ']' && bracketDepth > 0) bracketDepth--; + else if (c == ',' && parenDepth == 0 && bracketDepth == 0) + { + CommitParsedValue(valuesClause, i, ref valueStart, valueIndex, values); + valueIndex++; } } @@ -220,9 +235,6 @@ public object[] ParseValuesToArray(ReadOnlySpan valuesClause) return values; } - private static bool IsQuoteChar(char c, int i, ReadOnlySpan clause) - => c == '\'' && (i == 0 || clause[i - 1] != '\\'); - private void CommitParsedValue(ReadOnlySpan valuesClause, int commaIndex, ref int valueStart, int valueIndex, object[] values) { if (valueStart < commaIndex && valueIndex < ColumnTypes.Count) @@ -242,11 +254,18 @@ private void CommitParsedValue(ReadOnlySpan valuesClause, int commaIndex, { if (value.IsEmpty || value.Equals("NULL", StringComparison.OrdinalIgnoreCase)) return null; - - // Remove quotes if present + + // Remove one pair of quotes if present, and collapse SQL quote doubling ('it''s' → it's) — the same + // literal syntax SqlLiteralScanner and SqlParser.ParseValue read. + string? unescaped = null; if (value.Length >= 2 && value[0] == '\'' && value[^1] == '\'') { value = value[1..^1]; + if (value.IndexOf("''") >= 0) + { + unescaped = value.ToString().Replace("''", "'", StringComparison.Ordinal); + value = unescaped.AsSpan(); + } } return type switch @@ -1882,7 +1901,11 @@ private static List> RowsToDictionaryList(SharpCoreDB var valuesStart = rest.IndexOf("VALUES", StringComparison.OrdinalIgnoreCase) + "VALUES".Length; var valuesStr = rest[valuesStart..].Trim().TrimStart('(').TrimEnd(')'); - var values = SqlInPredicate.SplitTopLevelComma(valuesStr).Select(v => v.Trim().Trim('\'')).ToList(); + + // Keep the quotes: SqlParser.ParseValue reads literal syntax (one pair off, '' collapsed). Trimming + // them here stripped every edge quote and left the doubling in place, so a value ending in a quote + // lost it — known issue 7's shape. + var values = SqlInPredicate.SplitTopLevelComma(valuesStr).Select(v => v.Trim()).ToList(); Dictionary row = []; var table = tables[tableName]; diff --git a/src/SharpCoreDB/DatabaseExtensions.cs b/src/SharpCoreDB/DatabaseExtensions.cs index f51f87a0..ca1618b7 100644 --- a/src/SharpCoreDB/DatabaseExtensions.cs +++ b/src/SharpCoreDB/DatabaseExtensions.cs @@ -1533,7 +1533,7 @@ private bool EvaluateSingleCondition(Dictionary row, string cond if (valueStr.StartsWith('\'') && valueStr.EndsWith('\'')) { - valueStr = valueStr[1..^1]; + valueStr = SqlLiteralScanner.Unquote(valueStr); } if (rowValue is int intVal && int.TryParse(valueStr, out var intCompare)) @@ -1584,7 +1584,7 @@ private static object ParseValue(string valueStr) if ((valueStr.StartsWith('\'') && valueStr.EndsWith('\'')) || (valueStr.StartsWith('"') && valueStr.EndsWith('"'))) { - return valueStr[1..^1]; + return SqlLiteralScanner.Unquote(valueStr); } if (int.TryParse(valueStr, out var intVal)) diff --git a/src/SharpCoreDB/NuGet.README.md b/src/SharpCoreDB/NuGet.README.md index c8ca961a..56cfb63f 100644 --- a/src/SharpCoreDB/NuGet.README.md +++ b/src/SharpCoreDB/NuGet.README.md @@ -1,8 +1,8 @@ -# SharpCoreDB 2.1.1 — Performance-First Database Engine +# SharpCoreDB 2.1.2 — Performance-First Database Engine **High-Performance Embedded AND Networked Database for .NET 11 (C# 15 preview)** -> **This is the 2.1.x release line (current: 2.1.1)** — it targets **net11.0 only** with +> **This is the 2.1.x release line (current: 2.1.2)** — it targets **net11.0 only** with > **C# 15 preview** (`LangVersion=preview`). The net10.0 / C# 14 packages are the v2.0 stable > line, parked on `release/v2.0`. See `docs/net11/UPGRADE.md` for what the .NET 11 line adds. @@ -14,6 +14,49 @@ SharpCoreDB is a modern, encrypted, file-based database engine with SQL support, [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![SonarCloud Quality Gate](https://img.shields.io/sonar/quality_gate/MPCoreDeveloper_SharpCoreDB?server=https%3A%2F%2Fsonarcloud.io&logo=sonarcloud)](https://sonarcloud.io/dashboard?id=MPCoreDeveloper_SharpCoreDB) +## What's New in 2.1.2 + +**A data-correctness patch with no API changes** — a bound text parameter now reaches storage exactly as +you passed it: + +- **A bound string holding an apostrophe, a doubled quote or a backslash is no longer re-scanned as SQL.** + `ExecuteSQL(sql, parameters)` does not bind at the engine level: it renders each value into the + statement text and hands that text to the ordinary parser, so the formatter and the statement scanners + have to agree about escaping. They disagreed — the `VALUES` scanner dropped the quote of a doubled pair + and let a backslash suppress a quote toggle — and the damage was silent rather than an error: a passage + written through a parameter could land in the **neighbouring column**, and a value ending in a backslash + could not be written at all. One scanner (`Services/SqlLiteralScanner.cs`) now owns SQL's and SQLite's + rules and every literal reader on the statement path uses it: the `VALUES` / `SET` splitters, the + matching-parenthesis scan, all four parameter extractors, the batch `INSERT` parsers, + `FastSqlLexer.ReadString`, the expression tokenizer, the `WHERE` tokenizer — which used to split on + spaces and truncate *any* quoted value at its first space — and the server's `$n` → `?` translation. + + ```csharp + // 2.1.1: the value landed in the next column, or the statement threw + // "Mismatched parentheses in VALUES clause". + // 2.1.2: it round-trips byte-for-byte, and so does every Windows path. + db.ExecuteSQL("INSERT INTO Docs (Id, Body) VALUES (@id, @body)", new Dictionary + { + ["@id"] = 1, + ["@body"] = @"It's C:\src\app\, see docs\draft.pdf" + }); + ``` + +- **A parameter holding a single quote no longer stores an empty string.** `ParseValue` removes exactly + one quote pair and collapses the doubling; the old strip loop removed every edge quote, so a value of + `'` stored `""` and `'it''s'` reached storage as `it''s`. A quoted empty literal (`''`) now stores an + empty string, as SQLite does, where a trailing `''` used to become `NULL`. +- **`UPDATE … SET col = @v` splits at the first `=`**, not at every `=` — a value containing `=` used to + drop that assignment silently. A tuple that cannot fit its statement now throws before any row is + written, naming the table, the tuple index, both counts and the column list, instead of shifting + values right. +- **Verified on this tree:** core **2,107** tests / 0 failed, VectorSearch **255** / 0, EF Core **116** / + 0, linq2db **24** / 0; `SharpCoreDB.CI.slnf` builds clean. A two-file runnable reproduction with the + before/after console output ships in `docs/sharpcoredb-known-issues.md` §7, and the consumer that + reported it — SharpAgent's code-evidence index, which writes source text and Windows paths as `TEXT` — + runs its 144 indexing tests green against this build with no code change. +- Full list: `docs/CHANGELOG.md` › `[2.1.2]`. + ## What's New in 2.1.1 **A patch release with no API changes**, cut from the commits since the `v2.1.0` tag: @@ -250,16 +293,16 @@ db.Flush(); // Persist to disk ## 📦 Installation ```bash -dotnet add package SharpCoreDB --version 2.1.1 +dotnet add package SharpCoreDB --version 2.1.2 ``` **Optional companion packages:** ```bash -dotnet add package SharpCoreDB.Functional --version 2.1.1 -dotnet add package SharpCoreDB.Functional.Dapper --version 2.1.1 -dotnet add package SharpCoreDB.Functional.EntityFrameworkCore --version 2.1.1 -dotnet add package SharpCoreDB.Graph.Advanced --version 2.1.1 +dotnet add package SharpCoreDB.Functional --version 2.1.2 +dotnet add package SharpCoreDB.Functional.Dapper --version 2.1.2 +dotnet add package SharpCoreDB.Functional.EntityFrameworkCore --version 2.1.2 +dotnet add package SharpCoreDB.Graph.Advanced --version 2.1.2 ``` ## 🔄 Upgrading from v1.9 @@ -287,10 +330,10 @@ We welcome contributions! Check the repository for contribution guidelines. --- -**Latest Version:** 2.1.0 (October 2026) +**Latest Version:** 2.1.2 (October 2026) **Target:** .NET 11 / C# 15 (preview) -**Tests:** 2,897 across 16 suites (0 failed, 16 skipped) -**Status:** ✅ Released — 2.1.0 (net11.0-only performance line) +**Tests:** core suite 2,107 (0 failed) · VectorSearch 255 · EF Core 116 · linq2db 24 +**Status:** ✅ Released — 2.1.2 (net11.0-only performance line) **Versioning:** SharpCoreDB now uses 4-part versions (`n.n.n.n`). diff --git a/src/SharpCoreDB/Optimizations/WhereClauseAnalyzer.cs b/src/SharpCoreDB/Optimizations/WhereClauseAnalyzer.cs index 37c7027c..7f815951 100644 --- a/src/SharpCoreDB/Optimizations/WhereClauseAnalyzer.cs +++ b/src/SharpCoreDB/Optimizations/WhereClauseAnalyzer.cs @@ -111,8 +111,8 @@ public static bool TryParseSimpleNumericWhere( return false; // Not a numeric column } - // Remove quotes if present - valuePart = valuePart.Trim('\'', '"'); + // Remove quotes if present (and collapse SQL doubling: '' is one quote) + valuePart = SqlLiteralScanner.Unquote(valuePart.Trim()); // Parse operator SimdWhereFilter.ComparisonOp compOp; diff --git a/src/SharpCoreDB/Properties/AssemblyInfo.cs b/src/SharpCoreDB/Properties/AssemblyInfo.cs index fb67bb87..8ec9391b 100644 --- a/src/SharpCoreDB/Properties/AssemblyInfo.cs +++ b/src/SharpCoreDB/Properties/AssemblyInfo.cs @@ -8,3 +8,7 @@ // ✅ Phase 1: Allow test assemblies to access internal members for performance testing [assembly: InternalsVisibleTo("SharpCoreDB.Tests")] [assembly: InternalsVisibleTo("SharpCoreDB.Benchmarks")] + +// The wire protocol translates PostgreSQL-style '$1' markers to '?' before a statement is executed, so it has to +// use the same literal scanner as the parser (SqlLiteralScanner) rather than carry its own copy of the rules. +[assembly: InternalsVisibleTo("SharpCoreDB.Server.Core")] diff --git a/src/SharpCoreDB/Services/Compilation/FastSqlLexer.cs b/src/SharpCoreDB/Services/Compilation/FastSqlLexer.cs index 00eb6a8f..870aee31 100644 --- a/src/SharpCoreDB/Services/Compilation/FastSqlLexer.cs +++ b/src/SharpCoreDB/Services/Compilation/FastSqlLexer.cs @@ -160,16 +160,24 @@ private Token ReadString(char quote) var start = position; position++; // Skip opening quote - while (!IsAtEnd() && Current() != quote) + while (!IsAtEnd()) { - if (Current() == '\\' && Peek(1) == quote) + if (Current() != quote) { - position += 2; // Skip escaped quote + position++; + continue; } - else + + // A doubled quote is one quote of data and does not close the literal (SQL/SQLite). A backslash is + // data as well: it is a legal last character of a value (a Windows path), so it can never escape the + // closing quote — the old escape rule consumed past the literal's end for such a value. + if (Peek(1) == quote) { - position++; + position += 2; + continue; } + + break; } if (!IsAtEnd()) position++; // Skip closing quote diff --git a/src/SharpCoreDB/Services/PreparedStatements.cs b/src/SharpCoreDB/Services/PreparedStatements.cs index 8bd1e328..ca22eae9 100644 --- a/src/SharpCoreDB/Services/PreparedStatements.cs +++ b/src/SharpCoreDB/Services/PreparedStatements.cs @@ -98,32 +98,34 @@ public string BindParameters(Dictionary parameters) } /// Extracts positions of ? placeholders in SQL. + /// + /// Quoted literals are skipped as a whole through , the single quote + /// definition: a doubled quote is one quote and does not close the literal, and a backslash is data. The + /// previous form toggled on every quote whose predecessor was not a backslash, so 'it''s ?' was read + /// as "outside the literal" after the doubled quote and its ? counted as a placeholder. + /// private static int[] ExtractParameterPositions(string sql) { var positions = new List(); - bool inString = false; - char stringChar = '\0'; for (int i = 0; i < sql.Length; i++) { char c = sql[i]; - // Handle string literals - if ((c == '\'' || c == '"') && (i == 0 || sql[i - 1] != '\\')) + if (c is '\'' or '"') { - if (!inString) - { - inString = true; - stringChar = c; - } - else if (c == stringChar) + int closing = SqlLiteralScanner.FindClosingQuote(sql, i, out _); + if (closing < 0) { - inString = false; + break; // unterminated literal: nothing after it is a placeholder } + + i = closing; + continue; } // Count ? outside strings - if (!inString && c == '?') + if (c == '?') { positions.Add(i); } @@ -136,30 +138,26 @@ private static int[] ExtractParameterPositions(string sql) private static Dictionary? ExtractNamedParameters(string sql) { var parameters = new Dictionary(); - bool inString = false; - char stringChar = '\0'; for (int i = 0; i < sql.Length; i++) { char c = sql[i]; - // Handle string literals - if ((c == '\'' || c == '"') && (i == 0 || sql[i - 1] != '\\')) + // Skip a whole literal (see ExtractParameterPositions for the rule). + if (c is '\'' or '"') { - if (!inString) - { - inString = true; - stringChar = c; - } - else if (c == stringChar) + int closing = SqlLiteralScanner.FindClosingQuote(sql, i, out _); + if (closing < 0) { - inString = false; + break; } + + i = closing; continue; } // Extract @paramName outside strings - if (!inString && c == '@' && i + 1 < sql.Length && char.IsLetter(sql[i + 1])) + if (c == '@' && i + 1 < sql.Length && char.IsLetter(sql[i + 1])) { int nameStart = i + 1; int nameEnd = nameStart; @@ -176,7 +174,6 @@ private static int[] ExtractParameterPositions(string sql) } // Skip past the parameter name (nameEnd is already one past the last character) - // Note: loop increment will still happen, so we need to account for that i = nameEnd - 1; } } @@ -394,30 +391,26 @@ internal static string Bind(string sql, Dictionary parameters) internal static List<(string name, int pos)> ExtractNamedParameterOccurrences(string sql) { var occurrences = new List<(string name, int pos)>(); - bool inString = false; - char stringChar = '\0'; for (int i = 0; i < sql.Length; i++) { char c = sql[i]; - // Skip string literals ('...' or "..."). - if ((c == '\'' || c == '"') && (i == 0 || sql[i - 1] != '\\')) + // Skip the whole literal (SqlLiteralScanner is the single quote definition — see + // ExtractParameterPositions). + if (c is '\'' or '"') { - if (!inString) - { - inString = true; - stringChar = c; - } - else if (c == stringChar) + int closing = SqlLiteralScanner.FindClosingQuote(sql, i, out _); + if (closing < 0) { - inString = false; + break; } + i = closing; continue; } - if (inString || c != '@' || i + 1 >= sql.Length || !char.IsLetterOrDigit(sql[i + 1])) + if (c != '@' || i + 1 >= sql.Length || !char.IsLetterOrDigit(sql[i + 1])) { continue; } @@ -440,29 +433,24 @@ internal static string Bind(string sql, Dictionary parameters) internal static int[] ExtractPositionalParameterPositions(string sql) { var positions = new List(); - bool inString = false; - char stringChar = '\0'; for (int i = 0; i < sql.Length; i++) { char c = sql[i]; - if ((c == '\'' || c == '"') && (i == 0 || sql[i - 1] != '\\')) + if (c is '\'' or '"') { - if (!inString) - { - inString = true; - stringChar = c; - } - else if (c == stringChar) + int closing = SqlLiteralScanner.FindClosingQuote(sql, i, out _); + if (closing < 0) { - inString = false; + break; } + i = closing; continue; } - if (!inString && c == '?') + if (c == '?') { positions.Add(i); } diff --git a/src/SharpCoreDB/Services/SqlInPredicate.cs b/src/SharpCoreDB/Services/SqlInPredicate.cs index da860532..3269f817 100644 --- a/src/SharpCoreDB/Services/SqlInPredicate.cs +++ b/src/SharpCoreDB/Services/SqlInPredicate.cs @@ -335,7 +335,7 @@ private static string StripSingleValue(string item) item = item[1..^1]; } - return item.Trim().Trim('\'', '"'); + return SqlLiteralScanner.Unquote(item.Trim()); } /// @@ -382,13 +382,13 @@ private static List ParseValueRows(string listPart) { // Tuple row: (v1, v2) — split the inner list on top-level commas. rows.Add(SplitTopLevel(t[1..^1]) - .Select(v => v.Trim().Trim('\'', '"')) + .Select(v => SqlLiteralScanner.Unquote(v.Trim())) .Where(v => v.Length > 0) .ToArray()); } else { - rows.Add([t.Trim('\'', '"')]); + rows.Add([SqlLiteralScanner.Unquote(t)]); } } @@ -410,25 +410,29 @@ public static List SplitTopLevelComma(string text) var parts = new List(); int depth = 0; int bracketDepth = 0; - bool inString = false; - char quote = '\0'; int start = 0; for (int i = 0; i < text.Length; i++) { char c = text[i]; - if (inString) - { - inString = c != quote; // closing quote exits the string literal - continue; - } + // A quoted literal is skipped as a whole. SqlLiteralScanner is the single quote definition: a + // doubled quote is one quote and does not end the literal, and a backslash is data. The previous + // form exited at the first quote it met (`inString = c != quote`), so 'it''s, ok' split mid-value + // — the SET-list half of known issue 7. if (c is '\'' or '"') { - inString = true; - quote = c; + int closing = SqlLiteralScanner.FindClosingQuote(text, i, out _); + if (closing < 0) + { + break; // unterminated literal: the rest is one part, exactly as before + } + + i = closing; + continue; } - else if (c == '(') + + if (c == '(') { depth++; } diff --git a/src/SharpCoreDB/Services/SqlLiteralScanner.cs b/src/SharpCoreDB/Services/SqlLiteralScanner.cs new file mode 100644 index 00000000..720112c3 --- /dev/null +++ b/src/SharpCoreDB/Services/SqlLiteralScanner.cs @@ -0,0 +1,195 @@ +// +// Copyright (c) 2026 MPCoreDeveloper. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace SharpCoreDB.Services; + +using System; +using System.Collections.Generic; + +/// +/// The single definition of how SharpCoreDB reads a SQL string literal — used by every scanner on the +/// statement path (VALUES and SET splitters, matching-parenthesis scan, parameter extraction, the batch +/// INSERT parsers and the wire-protocol parameter translation). +/// +/// The rules are SQL's, and therefore SQLite's: inside '…' a doubled quote ('') is one quote and +/// does not close the literal, and a backslash is ordinary data. SharpCoreDB used to treat a backslash +/// as an escape and did not implement doubling at all, while +/// rendered strings with SQL doubling — so a bound value that +/// contained an apostrophe or ended in a backslash could not round-trip (known issue 7: the value was split at +/// its own comma and shifted into the next column, or the statement failed with "Mismatched parentheses"). +/// Formatter and scanner now share one convention, so "any string comes back byte-exact" holds. +/// +/// +/// A backslash can never be an escape here: it is a legal last character of a value (a Windows path), and a +/// scanner that lets it suppress the closing quote cannot represent that value in a statement at all. +/// +/// +internal static class SqlLiteralScanner +{ + /// + /// Finds the closing quote of the literal that begins at (which must be the quote + /// itself), honouring quote doubling. Returns the closing quote index, or -1 when the literal is + /// unterminated. reports whether the literal contains a doubled quote, so + /// callers that slice the content know they must collapse it. + /// + /// The statement text. + /// Index of the opening quote. + /// Set when the literal contains '' (or the doubled form of the quote in use). + /// The closing quote index, or -1. + internal static int FindClosingQuote(ReadOnlySpan text, int start, out bool hasDoubledQuote) + { + hasDoubledQuote = false; + char quote = text[start]; + + for (int i = start + 1; i < text.Length; i++) + { + if (text[i] != quote) + { + continue; + } + + // A doubled quote is one quote of data: it does not close the literal. + if (i + 1 < text.Length && text[i + 1] == quote) + { + hasDoubledQuote = true; + i++; + continue; + } + + return i; + } + + return -1; + } + + /// String overload of . + internal static int FindClosingQuote(string text, int start, out bool hasDoubledQuote) + => FindClosingQuote(text.AsSpan(), start, out hasDoubledQuote); + + /// + /// Returns the index just past the closing quote of the literal that begins at + /// , or -1 when it is unterminated. Callers that only need to know where a + /// literal ends (splitters, parenthesis matching, parameter extraction) use this. + /// + internal static int SkipLiteral(ReadOnlySpan text, int start) + { + int closing = FindClosingQuote(text, start, out _); + return closing < 0 ? -1 : closing + 1; + } + + /// + /// Splits a condition on whitespace that is not inside a quoted literal, a parenthesized group or a + /// bracketed literal, returning the tokens with their text unaltered. + /// + /// This is what a WHERE evaluator needs: splitting on spaces alone turns + /// name = 'it''s fine, really' into three tokens of which the value is 'it''s, so the row can + /// never match (and a bracketed vector literal [0.5, 0.25] is split at its comma's space). + /// + /// + /// The condition text. + /// The top-level tokens, in order. + internal static List SplitTopLevelWhitespace(string text) + { + var tokens = new List(); + int depth = 0; + int bracketDepth = 0; + int start = -1; + + for (int i = 0; i < text.Length; i++) + { + char c = text[i]; + + if (c is '\'' or '"') + { + if (start < 0) + { + start = i; + } + + int closing = FindClosingQuote(text, i, out _); + if (closing < 0) + { + i = text.Length - 1; // unterminated literal: take the rest as one token + continue; + } + + i = closing; + continue; + } + + if (char.IsWhiteSpace(c) && depth == 0 && bracketDepth == 0) + { + if (start >= 0) + { + tokens.Add(text[start..i]); + start = -1; + } + + continue; + } + + if (c == '(') + { + depth++; + } + else if (c == ')' && depth > 0) + { + depth--; + } + else if (c == '[') + { + bracketDepth++; + } + else if (c == ']' && bracketDepth > 0) + { + bracketDepth--; + } + + if (start < 0) + { + start = i; + } + } + + if (start >= 0) + { + tokens.Add(text[start..]); + } + + return tokens; + } + + /// + /// Removes exactly one pair of matching surrounding quotes and collapses the doubled quote inside it + /// ('it''s' → it's). Text without a matching pair is returned unchanged, so a value that is + /// not a literal is never altered. This replaces the earlier Trim('\'') / repeated-strip forms, + /// which removed every leading and trailing quote and left the doubling in place. + /// + /// A literal (with quotes) or a bare value. + /// The unquoted value. + internal static string Unquote(string value) + { + if (value.Length < 2) + { + return value; + } + + char quote = value[0]; + if (quote != '\'' && quote != '"') + { + return value; + } + + if (value[^1] != quote) + { + return value; + } + + var inner = value[1..^1]; + return quote == '\'' + ? inner.Replace("''", "'", StringComparison.Ordinal) + : inner.Replace("\"\"", "\"", StringComparison.Ordinal); + } +} diff --git a/src/SharpCoreDB/Services/SqlParser.DML.InsertConflict.cs b/src/SharpCoreDB/Services/SqlParser.DML.InsertConflict.cs index a8f20e67..41c11dbb 100644 --- a/src/SharpCoreDB/Services/SqlParser.DML.InsertConflict.cs +++ b/src/SharpCoreDB/Services/SqlParser.DML.InsertConflict.cs @@ -232,7 +232,9 @@ private static bool EvaluateDoUpdateWhere( { var leftCol = literalOpMatch.Groups[1].Value; var op = literalOpMatch.Groups[2].Value; - var rightRaw = literalOpMatch.Groups[3].Value.Trim().Trim('\''); + // Keep the raw literal: ParseValue removes one pair of quotes and collapses doubling. Trimming quotes + // here as well stripped a value that legitimately ends in a quote (known issue 7's shape). + var rightRaw = literalOpMatch.Groups[3].Value.Trim(); existingRow.TryGetValue(leftCol, out var leftVal); var colIdx = table.Columns.IndexOf(leftCol); var colType = colIdx >= 0 ? table.ColumnTypes[colIdx] : DataType.String; diff --git a/src/SharpCoreDB/Services/SqlParser.DML.cs b/src/SharpCoreDB/Services/SqlParser.DML.cs index 3a432609..cf5c72b4 100644 --- a/src/SharpCoreDB/Services/SqlParser.DML.cs +++ b/src/SharpCoreDB/Services/SqlParser.DML.cs @@ -383,6 +383,26 @@ private void ExecuteInsert(string sql, IWAL? wal) } } + // Known issue 7, safety net: a value that splits its literal at a comma used to land in the NEXT column + // (SharpAgent saw an embedding column receive "… iterations)") or fail later as an unrelated parse error. + // A tuple that produces more values than the statement can consume is malformed, so it is rejected here — + // before any row is written — naming the table, the tuple and both counts. Fewer values stays legal: + // that is the documented missing-value path, and the column falls to NULL. + int expectedValueCount = insertColumns?.Count ?? table.Columns.Count - (skipInternalRowId ? 1 : 0); + for (int rowIndex = 0; rowIndex < allRowValues.Count; rowIndex++) + { + var rowValues = allRowValues[rowIndex]; + if (rowValues.Count > expectedValueCount) + { + throw new InvalidOperationException( + $"Column count mismatch in INSERT INTO '{tableName}': VALUES tuple {rowIndex} produced " + + $"{rowValues.Count} values but {expectedValueCount} can be consumed " + + $"({string.Join(", ", insertColumns ?? table.Columns)}). A string literal that ends early " + + $"splits the tuple at its own comma; write an embedded quote as a doubled quote ('') " + + $"instead of escaping it with a backslash."); + } + } + // Build one row from one VALUES tuple. Shared by the per-row loop and the batched fast path below so // the two cannot drift apart on value parsing, column mapping or internal-_rowid skipping. Dictionary BuildRowFromValues(List rowValues) @@ -629,30 +649,43 @@ private static List> ParseMultiRowInsertValues(string valuesRest) /// /// Helper: Find matching closing parenthesis, respecting quoted strings. + /// + /// The quote rule is 's: a doubled quote is one quote and does not close the + /// literal, so a value such as 'it''s)' cannot end the tuple early. + /// /// private static int FindMatchingCloseParen(string str, int openParenIdx) { int depth = 0; - bool inQuotes = false; for (int i = openParenIdx; i < str.Length; i++) { char c = str[i]; - // Toggle quote state, respecting escape - if (c == '\'' && (i == 0 || str[i - 1] != '\\')) + // Skip a whole literal, quotes included. + if (c == '\'') { - inQuotes = !inQuotes; + int closing = SqlLiteralScanner.FindClosingQuote(str, i, out _); + if (closing < 0) + { + return -1; // unterminated literal — the tuple cannot be closed + } + + i = closing; + continue; } // Track parenthesis depth only outside quotes - if (!inQuotes) + if (c == '(') { - if (c == '(') depth++; - else if (c == ')') + depth++; + } + else if (c == ')') + { + depth--; + if (depth == 0) { - depth--; - if (depth == 0) return i; + return i; } } } @@ -661,165 +694,103 @@ private static int FindMatchingCloseParen(string str, int openParenIdx) } /// - /// ✅ UPDATED: Parses single row INSERT VALUES using modern Span-based approach. - /// Respects quoted strings and handles escaping correctly. + /// Splits one INSERT … VALUES tuple into its element texts. + /// + /// Structural only: each element is returned as written, quotes and all, and + /// is the single reader of literal syntax. The two therefore cannot + /// disagree about escaping, which is what known issue 7 was: the formatter doubled quotes while this + /// scanner had its own backslash-escape rule, so a bound text value was split at its own comma and shifted + /// into the neighbouring column. + /// + /// + /// The rules, pinned by InsertValuesParsingTests: + /// + /// a comma splits only outside a quoted literal and outside a bracketed literal + /// ([0.5, 0.25, 0.125] is one VECTOR value — docs/Vectors/TECHNICAL_SPEC.md §7). Quote + /// scanning is 's single definition: a doubled quote is one quote and does not + /// close the literal, and a backslash is data; + /// every element is Trim()ed; + /// an element is a value when it has any text, and a quoted element is a value even when its content is + /// empty — (1, '') stores an empty string, as SQLite does. Only a literally absent element is dropped, + /// so (1,) yields one value while (1, ) yields an empty one: a missing literal and an + /// empty-string literal are different values. + /// + /// /// private static List ParseInsertValues(ReadOnlySpan valuesStr) { List values = []; - // (2026-09-16, plan §9 priority 2 lever 1) Literals are sliced out of the statement instead of being - // appended to a StringBuilder one character at a time. Measured, that per-character build is the parse - // stage's dominant cost on the 1,000-row-statement shape (3.33 µs/row, ~1 KB/row allocated). - // - // The four rules reproduced here are pinned by InsertValuesParsingTests: - // 1. a quote toggles and is DROPPED, unless the previously *appended* character was a backslash — the - // check is against the accumulated content, not the source text, so a dropped quote does not update it; - // 2. a comma splits only outside quotes and outside a bracketed literal; - // 3. every literal is Trimmed; - // 4. a literal whose content is empty is not emitted at all, so `(1,)` yields one value while `(1, )` - // yields an empty one. - // - // Rule 2's bracketed-literal clause is what makes a VECTOR value expressible in VALUES at all: - // `[0.5, 0.25, 0.125, 0.0625]` is one literal, not four (docs/Vectors/TECHNICAL_SPEC.md §7). Without - // it the first fragment reached ParseVectorValue as "[0.5" and the statement failed as invalid base64. - // - // A literal is slice-expressible exactly when at most one quote pair toggles inside it. An interior - // toggling quote — `'it''s'` → `its` — is *dropped* by rule 1, which no contiguous slice can express, so - // such a literal falls back to the character loop this method used to run for everything. That loop is - // kept below as the reference; it is the slow path, not a second interpretation of the rules. int index = 0; int length = valuesStr.Length; while (index < length) { - int literalStart = index; - int contentStart = index; - int contentEnd = -1; - int toggles = 0; + int elementStart = index; int bracketDepth = 0; - bool inQuotes = false; - char lastAppended = '\0'; - bool terminatedByComma = false; + int literalCount = 0; + // One pass over the element: whole literals are skipped (a comma inside one is text), a bracketed + // literal stays in one piece, and the first top-level comma ends the element. while (index < length) { char c = valuesStr[index]; - if (c == '\'' && lastAppended != '\\') + if (c == '\'') { - toggles++; - if (toggles == 1) { contentStart = index + 1; inQuotes = true; } - else if (toggles == 2) { contentEnd = index; inQuotes = false; } + int closing = SqlLiteralScanner.FindClosingQuote(valuesStr, index, out _); + if (closing < 0) + { + // Unterminated literal: the rest of the tuple belongs to this element. The text is still + // passed on as written, so nothing is silently truncated here. + literalCount++; + index = length; + break; + } - index++; - continue; // a dropped quote must not become the last appended character + literalCount++; + index = closing + 1; + continue; } - if (!inQuotes) + if (c == '[') { - if (c == '[') - { - bracketDepth++; - } - else if (c == ']' && bracketDepth > 0) - { - bracketDepth--; - } + bracketDepth++; } - - if (c == ',' && !inQuotes && bracketDepth == 0) + else if (c == ']' && bracketDepth > 0) + { + bracketDepth--; + } + else if (c == ',' && bracketDepth == 0) { - terminatedByComma = true; break; } - lastAppended = c; index++; } - // Rule 4, corrected by the suites: an interior literal is emitted **unconditionally** at its comma — - // `('', 2)` stores an empty string — and only the *trailing* literal is suppressed when it has no text - // at all. So `(1,)` yields one value, `(1, )` yields an empty one, and `('')` as the last literal - // yields none while `('', 2)` yields an empty string. - // The flag is set where the comma is *seen*; deriving it afterwards from `index < length` was wrong, - // because a literal whose closing quote is the tuple's last character also ends with index == length. + bool terminatedByComma = index < length; + var element = valuesStr[elementStart..index]; - if (toggles == 0) - { - var literal = valuesStr[literalStart..index]; - if (literal.Length > 0 || terminatedByComma) - { - values.Add(literal.Trim().ToString()); - } - } - else if (toggles == 2 && contentEnd >= contentStart) + // A quoted element IS a value even when its content is empty: `(1, '')` stores an empty string in + // SQLite and must here too. Only a literally absent element is dropped, so `(1,)` yields one value + // while `(1, )` still yields an empty one — a missing literal and an empty-string literal are + // different values. (The previous rule suppressed a *trailing* `''` as well, which made a bound + // empty string silently NULL — known issue 7's second half; InsertValuesParsingTests pinned it.) + bool hasValue = literalCount > 0 || element.Length > 0; + + if (hasValue || terminatedByComma) { - var literal = valuesStr[contentStart..contentEnd]; - if (literal.Length > 0 || terminatedByComma) - { - values.Add(literal.Trim().ToString()); - } - } - else - { - var fallback = BuildLiteralByAppending(valuesStr[literalStart..index], terminatedByComma); - if (fallback is not null) - { - values.Add(fallback); - } + values.Add(element.Trim().ToString()); } - if (index < length) + if (terminatedByComma) { index++; // step over the separating comma } } return values; - - // The reference implementation, used only for literals a slice cannot express: one with an interior - // toggling quote, or an unterminated one. It is byte-for-byte the behaviour this method had before the - // fast path existed, including emitting nothing when the accumulated content is empty — plus the - // bracketed-literal clause of rule 2, so it cannot disagree with the fast path about a vector literal. - static string? BuildLiteralByAppending(ReadOnlySpan text, bool terminatedByComma) - { - var currentValue = new StringBuilder(); - bool inQuotes = false; - int bracketDepth = 0; - - foreach (char c in text) - { - if (c == '\'' && (currentValue.Length == 0 || currentValue[^1] != '\\')) - { - inQuotes = !inQuotes; - continue; - } - - if (!inQuotes) - { - if (c == '[') - { - bracketDepth++; - } - else if (c == ']' && bracketDepth > 0) - { - bracketDepth--; - } - } - - if (c == ',' && !inQuotes && bracketDepth == 0) - { - break; - } - - currentValue.Append(c); - } - - // Emitted whenever the literal ended at a comma, even with nothing accumulated (an empty string); - // suppressed only for an empty trailing literal. - return currentValue.Length == 0 && !terminatedByComma ? null : currentValue.ToString().Trim(); - } } /// @@ -2278,11 +2249,14 @@ private void ExecuteUpdate(string sql, IWAL? wal) var updates = new Dictionary(); foreach (var setClause in setClauses) { - var parts = setClause.Split('='); - if (parts.Length == 2) + // Split at the FIRST '=' only. A bound text value may contain '=' itself (a URL query string, a + // source line), and Split('=') produced three parts for those and silently dropped the assignment — + // the same class as known issue 7: the value was not the value. + int assign = setClause.IndexOf('='); + if (assign > 0) { - var colName = parts[0].Trim(); - var valueStr = parts[1].Trim(); + var colName = setClause[..assign].Trim(); + var valueStr = setClause[(assign + 1)..].Trim(); // Find column type var colIndex = table.Columns.IndexOf(colName); diff --git a/src/SharpCoreDB/Services/SqlParser.Helpers.cs b/src/SharpCoreDB/Services/SqlParser.Helpers.cs index c28ba43f..f3d4141a 100644 --- a/src/SharpCoreDB/Services/SqlParser.Helpers.cs +++ b/src/SharpCoreDB/Services/SqlParser.Helpers.cs @@ -162,17 +162,18 @@ private static List ParseWhereColumns(string where) /// public static object? ParseValue(string val, DataType type) { - if (val == "NULL") + // A bare NULL keyword is the null value; a quoted 'NULL' is the four-character string. The order matters, + // so the literal is unquoted only after this check. + if (val.Equals("NULL", StringComparison.OrdinalIgnoreCase)) { return null; } - while (val.Length >= 2 && - ((val.StartsWith('\'') && val.EndsWith('\'')) || - (val.StartsWith('"') && val.EndsWith('"')))) - { - val = val[1..^1]; - } + // Exactly one pair of quotes is removed and SQL quote doubling is collapsed ('it''s' → it's), through + // SqlLiteralScanner — the same definition every statement scanner uses. The previous form stripped + // repeatedly, so a value that ended in a quote lost it (a parameter holding `'` stored an empty string), + // and it never collapsed doubling, so 'it''s' reached storage as it''s. Both are known issue 7. + val = SqlLiteralScanner.Unquote(val); try { diff --git a/src/SharpCoreDB/Services/SqlParser.PerformanceOptimizations.cs b/src/SharpCoreDB/Services/SqlParser.PerformanceOptimizations.cs index 0207024c..91d93cf3 100644 --- a/src/SharpCoreDB/Services/SqlParser.PerformanceOptimizations.cs +++ b/src/SharpCoreDB/Services/SqlParser.PerformanceOptimizations.cs @@ -395,9 +395,8 @@ private static bool CompareIn(object? value, string valueStr) if (!valueStr.StartsWith("(") || !valueStr.EndsWith(")")) return false; - var items = valueStr[1..^1] // Remove parentheses - .Split(',') - .Select(s => s.Trim().Trim('\'', '"')) + var items = SqlInPredicate.SplitTopLevelComma(valueStr[1..^1]) // Remove parentheses + .Select(s => SqlLiteralScanner.Unquote(s.Trim())) .ToList(); return items.Contains(value.ToString() ?? string.Empty); @@ -411,8 +410,8 @@ private static bool CompareLike(object? value, string valueStr) if (value == null) return false; - // Remove quotes - valueStr = valueStr.Trim('\'', '"'); + // Remove quotes (and collapse SQL doubling) + valueStr = SqlLiteralScanner.Unquote(valueStr); // Simple LIKE: convert SQL wildcards to regex var pattern = "^" + System.Text.RegularExpressions.Regex.Escape(valueStr) diff --git a/src/SharpCoreDB/Services/SqlParserExtensions.cs b/src/SharpCoreDB/Services/SqlParserExtensions.cs index c23f89da..d4312de5 100644 --- a/src/SharpCoreDB/Services/SqlParserExtensions.cs +++ b/src/SharpCoreDB/Services/SqlParserExtensions.cs @@ -321,6 +321,6 @@ private static List ExtractValues(string sql) var valuesStr = remaining.Substring(startIdx + 1, endIdx - startIdx - 1); // Top-level split: a bracketed VECTOR literal ([0.5, 0.25]) is one value, not two. - return [.. Services.SqlInPredicate.SplitTopLevelComma(valuesStr).Select(v => v.Trim().Trim('\'') as object)]; + return [.. Services.SqlInPredicate.SplitTopLevelComma(valuesStr).Select(v => Services.SqlLiteralScanner.Unquote(v.Trim()) as object)]; } } diff --git a/src/SharpCoreDB/Services/TypeConverter.cs b/src/SharpCoreDB/Services/TypeConverter.cs index c92708ba..f38712ea 100644 --- a/src/SharpCoreDB/Services/TypeConverter.cs +++ b/src/SharpCoreDB/Services/TypeConverter.cs @@ -186,6 +186,15 @@ private static List TokenizeExpression(string expression) currentToken.Append(c); if (c == stringChar) { + // A doubled quote is one quote of data and does not close the literal (SQL/SQLite), so + // 'it''s' is one token rather than two. A backslash is data too — never an escape. + if (i + 1 < expression.Length && expression[i + 1] == stringChar) + { + currentToken.Append(expression[i + 1]); + i++; + continue; + } + inString = false; tokens.Add(currentToken.ToString()); currentToken.Clear(); @@ -395,13 +404,13 @@ private static bool EvaluateComparisonExpression(List tokens, ref int in } else if (token.StartsWith('\'') && token.EndsWith('\'')) { - // String literal - return token.Trim('\'', '"'); + // String literal: one pair of quotes off, and SQL doubling collapsed ('it''s' → it's). + return SqlLiteralScanner.Unquote(token); } else if (token.StartsWith('"') && token.EndsWith('"')) { - // String literal - return token.Trim('\'', '"'); + // Quoted identifier or string literal — same rule. + return SqlLiteralScanner.Unquote(token); } else if (int.TryParse(token, out var intValue)) { diff --git a/src/SharpCoreDB/SharpCoreDB.csproj b/src/SharpCoreDB/SharpCoreDB.csproj index 59603516..92095a05 100644 --- a/src/SharpCoreDB/SharpCoreDB.csproj +++ b/src/SharpCoreDB/SharpCoreDB.csproj @@ -37,15 +37,15 @@ SharpCoreDB - 2.1.1 + 2.1.2 2.1.0.0 - 2.1.1.0 + 2.1.2.0 MPCoreDeveloper SharpCoreDB SharpCoreDB A lightweight, encrypted, file-based database engine with SQL support, AES-256-GCM encryption, modern C# features, and production-ready performance for time-tracking, invoicing, and project management applications. Supports Windows, Linux, macOS, Android, iOS, and IoT/embedded devices. Copyright (c) 2026 MPCoreDeveloper - v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): .NET 11 / C# 15 preview release line — net11.0-only, native Zstandard compression (quality presets 1/5/19 + frame checksum), Runtime Async, an additive C# 15 API surface (SharpCoreDB.Net11), and the Munarium-inspired DiskANN index + content-verified index artifacts. The net10.0 / C# 14 line is the v2.0 stable packages, now parked on release/v2.0. See docs/net11/UPGRADE.md for details. + v2.1.2 (2026-10-26): patch release with no API changes - a bound text parameter is no longer re-scanned as SQL text, so a value holding an apostrophe, a doubled quote, a backslash, a comma or a parenthesis now round-trips exactly instead of landing in the neighbouring column or failing with "Mismatched parentheses in VALUES clause"; full detail in docs/CHANGELOG.md. Previous: v2.1.1 (2026-10-25): patch release with no API changes. A float[] parameter now reaches a VECTOR(n) column as a vector instead of the literal text "System.Single[]", and ORDER BY vec_distance_*() LIMIT k now reaches the vector index instead of a full scan. Previous: v2.1.0 (2026-10-10): .NET 11 / C# 15 preview release line — net11.0-only, native Zstandard compression (quality presets 1/5/19 + frame checksum), Runtime Async, an additive C# 15 API surface (SharpCoreDB.Net11), and the Munarium-inspired DiskANN index + content-verified index artifacts. The net10.0 / C# 14 line is the v2.0 stable packages, now parked on release/v2.0. See docs/net11/UPGRADE.md for details. database;embedded;encryption;sql;nosql;time-tracking;invoicing;net11;csharp15;zstd;performance;android;ios;mobile;iot;arm64;x64;distributed;sync MIT https://github.com/MPCoreDeveloper/SharpCoreDB @@ -161,6 +161,9 @@ + + diff --git a/src/SharpCoreDB/SingleFileTable.cs b/src/SharpCoreDB/SingleFileTable.cs index f0078b2d..e79234ae 100644 --- a/src/SharpCoreDB/SingleFileTable.cs +++ b/src/SharpCoreDB/SingleFileTable.cs @@ -1316,7 +1316,7 @@ private bool TryParsePkEquality(string condition, out string? value) if ((val.StartsWith('\'') && val.EndsWith('\'')) || (val.StartsWith('"') && val.EndsWith('"'))) { - val = val[1..^1]; + val = SqlLiteralScanner.Unquote(val); } // Normalize numeric literals to the row's canonical ToString() form so `pk = 05` @@ -1693,7 +1693,7 @@ private static string NormalizeColumnName(string columnName) if ((valueStr.StartsWith('\'') && valueStr.EndsWith('\'')) || (valueStr.StartsWith('"') && valueStr.EndsWith('"'))) { - valueStr = valueStr[1..^1]; + valueStr = SqlLiteralScanner.Unquote(valueStr); } if (valueStr.Length == 0) @@ -1825,8 +1825,8 @@ private static bool EvaluateSingleCondition(Dictionary row, stri if (betweenMatch.Success) { var col = NormalizeColumnName(betweenMatch.Groups[1].Value); - var lowStr = betweenMatch.Groups[2].Value.Trim('\'', '"'); - var highStr = betweenMatch.Groups[3].Value.Trim('\'', '"'); + var lowStr = SqlLiteralScanner.Unquote(betweenMatch.Groups[2].Value.Trim()); + var highStr = SqlLiteralScanner.Unquote(betweenMatch.Groups[3].Value.Trim()); if (!row.TryGetValue(col, out var rowVal) || rowVal is null or DBNull) { return false; @@ -1894,7 +1894,7 @@ private static bool EvaluateSingleCondition(Dictionary row, stri if ((valueStr.StartsWith('\'') && valueStr.EndsWith('\'')) || (valueStr.StartsWith('"') && valueStr.EndsWith('"'))) { - valueStr = valueStr[1..^1]; + valueStr = SqlLiteralScanner.Unquote(valueStr); } if (rowValue is int intVal && int.TryParse(valueStr, out var intCompare)) diff --git a/src/SharpCoreDB/Storage/Scdb/SingleFileDatabase.Batch.cs b/src/SharpCoreDB/Storage/Scdb/SingleFileDatabase.Batch.cs index 16c6671c..75dc3ade 100644 --- a/src/SharpCoreDB/Storage/Scdb/SingleFileDatabase.Batch.cs +++ b/src/SharpCoreDB/Storage/Scdb/SingleFileDatabase.Batch.cs @@ -372,7 +372,6 @@ private static List ParseValueList(string valuesStr) var span = valuesStr.AsSpan(); int start = 0; - bool inQuotes = false; int inParens = 0; // ✅ FIX: Use int for parenthesis depth int inBrackets = 0; // bracketed literals ([0.5, 0.25] — a VECTOR value) never split @@ -381,41 +380,47 @@ private static List ParseValueList(string valuesStr) { char c = span[i]; - // Check for quote (with escape handling) - if (c == '\'' && (i == 0 || span[i - 1] != '\\')) + // A quoted literal is skipped as a whole: SqlLiteralScanner is the single quote definition ('' is one + // quote and does not close the literal; a backslash is data), so a comma inside a value cannot split it. + if (c == '\'') { - inQuotes = !inQuotes; - } - else if (!inQuotes) - { - // Handle parenthesis depth - if (c == '(') - { - inParens++; - } - else if (c == ')') - { - inParens--; - } - else if (c == '[') + int closing = SqlLiteralScanner.FindClosingQuote(span, i, out _); + if (closing < 0) { - inBrackets++; + break; } - else if (c == ']' && inBrackets > 0) - { - inBrackets--; - } - // Check for value delimiter - else if (c == ',' && inParens == 0 && inBrackets == 0) + + i = closing; + continue; + } + + // Handle parenthesis depth + if (c == '(') + { + inParens++; + } + else if (c == ')') + { + inParens--; + } + else if (c == '[') + { + inBrackets++; + } + else if (c == ']' && inBrackets > 0) + { + inBrackets--; + } + // Check for value delimiter + else if (c == ',' && inParens == 0 && inBrackets == 0) + { + // Extract value using span (zero-copy until ToString) + var valueSpan = span[start..i].Trim(); + if (valueSpan.Length > 0) { - // Extract value using span (zero-copy until ToString) - var valueSpan = span[start..i].Trim(); - if (valueSpan.Length > 0) - { - values.Add(TrimQuotes(valueSpan)); - } - start = i + 1; + values.Add(TrimQuotes(valueSpan)); } + start = i + 1; } } @@ -441,7 +446,12 @@ private static string TrimQuotes(ReadOnlySpan value) // Check if both ends have quotes if (value.Length >= 2 && value[0] == '\'' && value[value.Length - 1] == '\'') { - return value[1..^1].ToString(); + var inner = value[1..^1]; + + // SQL quote doubling is one quote of data: 'it''s' → it's. + return inner.IndexOf("''") >= 0 + ? inner.ToString().Replace("''", "'", StringComparison.Ordinal) + : inner.ToString(); } // Check if only start has quote diff --git a/tests/SharpCoreDB.Tests/EncryptionCoverageTests.cs b/tests/SharpCoreDB.Tests/EncryptionCoverageTests.cs index 171b706a..8b70ce05 100644 --- a/tests/SharpCoreDB.Tests/EncryptionCoverageTests.cs +++ b/tests/SharpCoreDB.Tests/EncryptionCoverageTests.cs @@ -80,7 +80,11 @@ public void TableDataFiles_MarkerPresence_MatchesTheDocumentedPosture(Kind kind, db.ExecuteSQL("CREATE TABLE docs (id INTEGER PRIMARY KEY, name TEXT, score REAL)"); for (int i = 1; i <= 5; i++) { - db.ExecuteSQL($"INSERT INTO docs VALUES ({i}, '{Marker}_{i}', {i * 0.5})"); + // Invariant culture: {i * 0.5} formats as "0,5" on a Dutch machine, which is a four-value tuple because + // SQL's decimal literal separator is '.', as SQLite requires. The engine now rejects such a tuple + // (correctly) instead of silently dropping the overflow value. + var score = (i * 0.5).ToString("0.0", System.Globalization.CultureInfo.InvariantCulture); + db.ExecuteSQL($"INSERT INTO docs VALUES ({i}, '{Marker}_{i}', {score})"); } db.Flush(); diff --git a/tests/SharpCoreDB.Tests/InsertValuesParsingTests.cs b/tests/SharpCoreDB.Tests/InsertValuesParsingTests.cs index 7a13a35d..0ec6170b 100644 --- a/tests/SharpCoreDB.Tests/InsertValuesParsingTests.cs +++ b/tests/SharpCoreDB.Tests/InsertValuesParsingTests.cs @@ -15,25 +15,24 @@ namespace SharpCoreDB.Tests; /// /// Pins the observable behaviour of the multi-row INSERT … VALUES literal scanner -/// (SqlParser.ParseInsertValues, SqlParser.DML.cs:654) before it is rewritten to slice -/// literals out of the statement text instead of appending them to a StringBuilder one character at a -/// time (plan §9 priority 2, lever 1 — the measured 25 % of a multi-row pass). +/// (SqlParser.ParseInsertValues). /// -/// The rewrite is only safe if these four rules are reproduced exactly, and three of them are surprising enough -/// to be documented here rather than in prose: +/// The scanner is structural: it splits a tuple into element texts and hands each one, quotes and all, to +/// SqlParser.ParseValue. The rules below are what the suites pin: +/// /// -/// a single quote toggles quote state and is dropped — unless the previously appended -/// character was a backslash, a check against the scanner's accumulated content rather than the source text. -/// So 'it''s' becomes its (standard SQL quote-doubling is not an escape here) while -/// 'a\'b' keeps its backslash and its content. -/// a comma splits literals only outside quotes. +/// a single quote opens and closes a literal, and '' inside one is one quote of data that does +/// not close it — SQL and SQLite ('it''s' → it's). A backslash is data too: it is a legal last +/// character of a value, so it can never escape the closing quote. (Before known issue 7 was fixed the scanner +/// dropped the quote of a doubled pair and let a backslash suppress a toggle, which is why a bound value +/// containing either could not round-trip.) +/// a comma splits literals only outside quotes and outside a bracketed literal. /// every literal is Trim()ed. -/// a literal with no text at all is not emitted, so (1,) yields a single value — leaving the -/// second column to the missing-value path and therefore NULL — while (1, ) yields an empty -/// literal, which for a TEXT column stores an empty string. A missing literal and an empty-string literal are -/// different values. +/// a literal that is present is a value even when it is empty, so (1, '') stores an empty string, as +/// SQLite does; only a literally absent element is dropped, so (1,) leaves the column to the +/// missing-value path and therefore NULL, while (1, ) stores an empty string. A missing literal and +/// an empty-string literal are different values. /// -/// /// public sealed class InsertValuesParsingTests : IDisposable { @@ -87,30 +86,30 @@ public async Task QuotedComma_IsOneLiteral_NotATupleBreak() Assert.Equal("c", NameOf(db, 2)); } - // ── Rule 1: quote toggling, and the escape check against accumulated content ──────────────── + // ── Rule 1: a doubled quote is one quote; a backslash is data ─────────────────────────────── [Fact] - public async Task DoubledQuote_IsDroppedAndContentConcatenated_NotEscaped() + public async Task DoubledQuote_IsOneQuote_NotAnEscape() { await using var db = Open("doubled_quote"); - // 'it''s' → quote(dropped) i t quote(dropped) quote(dropped) s quote(dropped) → "its" + // 'it''s' → the doubled quote is data, so the literal is it's (SQL and SQLite behaviour). db.ExecuteSQL("INSERT INTO t VALUES (1, 'it''s'), (2, 'ok')"); - Assert.Equal("its", NameOf(db, 1)); + Assert.Equal("it's", NameOf(db, 1)); } [Fact] - public async Task BackslashEscapedQuote_IsKeptVerbatim_AndDoesNotToggle() + public async Task TrailingBackslash_DoesNotSwallowTheClosingQuote() { await using var db = Open("escaped_quote"); - // 'a\'b' → quote(dropped) a \ ' (the accumulated content ends in '\', so this is NOT a toggle — the - // quote is appended and stays) b quote(dropped) → "a\'b". The backslash is data, not an escape: it is - // stored verbatim, and so is the quote it "escaped". - db.ExecuteSQL("INSERT INTO t VALUES (1, 'a\\'b'), (2, 'ok')"); + // 'C:\temp\' → the backslash is data, so the final quote still closes the literal. A scanner that treats + // a backslash as an escape cannot represent this value at all — it read the closing quote as escaped and + // reported "Mismatched parentheses in VALUES clause". + db.ExecuteSQL("INSERT INTO t VALUES (1, 'C:\\temp\\'), (2, 'ok')"); - Assert.Equal("a\\'b", NameOf(db, 1)); + Assert.Equal("C:\\temp\\", NameOf(db, 1)); } // ── Rule 3: literals are trimmed ──────────────────────────────────────────────────────────── @@ -155,10 +154,6 @@ public async Task EmptyStringLiteral_InAnInteriorPosition_IsEmitted_AndStaysEmpt await using var db = Open("interior_empty"); db.ExecuteSQL("CREATE TABLE t3 (id INTEGER PRIMARY KEY, name TEXT, note INTEGER)"); - // Found by the suites when the scanner was rewritten (2026-09-16): a literal that is followed by a comma - // is emitted unconditionally, so '' stores an empty string — only a *trailing* literal is suppressed when - // it has no text. Inside `(1, '')` the empty literal IS the trailing one, so it is not emitted at all and - // the column falls to NULL; expressing the interior case needs a value after it. db.ExecuteSQL("INSERT INTO t3 VALUES (1, '', 42), (2, 'x', 43)"); var rows = db.ExecuteQuery("SELECT * FROM t3 WHERE id = 1"); @@ -166,6 +161,19 @@ public async Task EmptyStringLiteral_InAnInteriorPosition_IsEmitted_AndStaysEmpt Assert.Equal(42, Convert.ToInt32(rows[0]["note"])); } + [Fact] + public async Task EmptyStringLiteral_InTheTrailingPosition_IsStoredAsAnEmptyString() + { + await using var db = Open("trailing_empty"); + + // SQLite stores '' here, so SharpCoreDB must too. The old rule suppressed a trailing empty literal, which + // meant a *present* empty value became NULL — and, through a bound parameter, an empty string was not + // round-trippable at all (known issue 7). + db.ExecuteSQL("INSERT INTO t VALUES (1, ''), (2, 'ok')"); + + Assert.Equal(string.Empty, NameOf(db, 1)); + } + // ── Control: the single-tuple shape still parses ──────────────────────────────────────────── [Fact] diff --git a/tests/SharpCoreDB.Tests/ParameterLiteralRoundTripTests.cs b/tests/SharpCoreDB.Tests/ParameterLiteralRoundTripTests.cs new file mode 100644 index 00000000..16ffb218 --- /dev/null +++ b/tests/SharpCoreDB.Tests/ParameterLiteralRoundTripTests.cs @@ -0,0 +1,255 @@ +// +// Copyright (c) 2026 MPCoreDeveloper. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace SharpCoreDB.Tests; + +using Microsoft.Extensions.DependencyInjection; +using SharpCoreDB.Interfaces; +using SharpCoreDB.Services; +using System; +using System.Collections.Generic; +using System.IO; +using Xunit; + +/// +/// Known issue 7: a bound string parameter is rendered back into the statement text +/// (ParameterBinder.FormatParameter emits 'it''s' for it's) and then read back by the +/// SQL literal scanner, so formatter and scanner have to agree about escaping. +/// +/// They disagreed. The formatter doubled the single quote — the SQL and SQLite convention — while the VALUES +/// scanner treated a backslash as an escape and did not implement doubling. A parameter holding +/// it's fine, really therefore closed its literal at the doubled quote, split at the following comma and +/// pushed really into the next column; a value ending in a backslash or holding a backslash before a +/// quote threw Mismatched parentheses in VALUES clause. +/// +/// +/// These tests are the property that was missing: any string that goes in through a parameter comes back +/// byte-exact in its own column, and the neighbouring columns are untouched. They drive the public API only, so +/// they cover the whole path — binder, VALUES splitter, literal scanner and value conversion. +/// +/// +public sealed class ParameterLiteralRoundTripTests : IDisposable +{ + private readonly DatabaseFactory _factory; + private readonly string _dirPath; + private int _dbCounter; + + public ParameterLiteralRoundTripTests() + { + var services = new ServiceCollection(); + services.AddSharpCoreDB(); + var provider = services.BuildServiceProvider(); + _factory = provider.GetRequiredService(); + _dirPath = Path.Combine(Path.GetTempPath(), $"SCDB_ParameterRoundTrip_{Guid.NewGuid():N}"); + Directory.CreateDirectory(_dirPath); + } + + public void Dispose() + { + try { if (Directory.Exists(_dirPath)) Directory.Delete(_dirPath, true); } catch { } + } + + // ── Helpers ───────────────────────────────────────────────────────────────────────────────── + + private IDatabase Open() + { + var dir = Path.Combine(_dirPath, $"db{_dbCounter++}"); + Directory.CreateDirectory(dir); + var db = _factory.Create(dir, "pw", isReadOnly: false, config: new DatabaseConfig { NoEncryptMode = true }); + db.ExecuteSQL("CREATE TABLE t (id INTEGER PRIMARY KEY, head TEXT, val TEXT, tail TEXT)"); + return db; + } + + private static Dictionary P(params (string Name, object? Value)[] pairs) + { + var parameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var (name, value) in pairs) + { + parameters[name] = value; + } + + return parameters; + } + + private static Dictionary Read(IDatabase db, int id) + { + var rows = db.ExecuteQuery($"SELECT * FROM t WHERE id = {id}"); + Assert.Single(rows); + return rows[0]; + } + + /// + /// The strings that broke the old scanner: an apostrophe (doubled by the formatter), a backslash before a + /// quote, a trailing backslash, commas and brackets inside the value, plus plain text that must survive + /// unchanged. NULL as text and the empty string are included because both are values, not absences. + /// + public static TheoryData TextValues => new() + { + "it's fine, really", + "a stray \\' apostrophe, then a comma", + "ends with a backslash \\", + "the mask (see below, the reader) wins, and then it records iterations)", + "quoted at both ends: 'x'", + "doubled '' quote", + "brackets [0.5, 0.25] and braces {x}", + "line1\nline2\ttabbed", + "unicode: ünïcødé ✓ 日本語", + "NULL", + "null", + "; semicolon -- dash", + "key=value&other=2", + "", + "'", + "''", + "\\", + }; + + // ── The defect: a text parameter in the middle of a VALUES tuple ──────────────────────────── + + [Theory] + [MemberData(nameof(TextValues))] + public async Task ExecuteSQL_TextParameterBetweenLiterals_RoundTripsByteExact(string value) + { + await using var db = Open(); + + // head and tail are literals, so a value split at its own comma can only be detected as corruption of + // the neighbours — which is exactly how SharpAgent saw it (a value landing in `embedding`). + db.ExecuteSQL( + "INSERT INTO t VALUES (@id, 'head', @val, 'tail')", + P(("id", 1), ("val", value))); + + var row = Read(db, 1); + Assert.Equal("head", row["head"] as string); + Assert.Equal(value, row["val"] as string); + Assert.Equal("tail", row["tail"] as string); + } + + [Theory] + [MemberData(nameof(TextValues))] + public async Task ExecuteSQL_TextParameterAsLastLiteral_RoundTripsByteExact(string value) + { + await using var db = Open(); + + db.ExecuteSQL( + "INSERT INTO t VALUES (@id, 'head', 'val', @val)", + P(("id", 1), ("val", value))); + + var row = Read(db, 1); + Assert.Equal("head", row["head"] as string); + Assert.Equal("val", row["val"] as string); + Assert.Equal(value, row["tail"] as string); + } + + [Fact] + public async Task ExecuteSQL_MultiRowInsertWithTextParameters_RoundTripsEveryRow() + { + await using var db = Open(); + + db.ExecuteSQL( + "INSERT INTO t VALUES (@a, 'head', @av, 'tail'), (@b, 'head', @bv, 'tail')", + P(("a", 1), ("av", "it's the first, really"), + ("b", 2), ("bv", "and the second ends with a backslash \\"))); + + Assert.Equal("it's the first, really", Read(db, 1)["val"] as string); + Assert.Equal("and the second ends with a backslash \\", Read(db, 2)["val"] as string); + } + + [Fact] + public async Task ExecuteSQL_PositionalTextParameter_RoundTripsByteExact() + { + await using var db = Open(); + + db.ExecuteSQL( + "INSERT INTO t VALUES (?, 'head', ?, 'tail')", + P(("0", 1), ("1", "it's positional, and holds a comma"))); + + Assert.Equal("it's positional, and holds a comma", Read(db, 1)["val"] as string); + } + + // ── The same values through UPDATE (the SET splitter has its own scanner) ─────────────────── + + [Theory] + [MemberData(nameof(TextValues))] + public async Task ExecuteSQL_TextParameter_CanBeFoundByEqualToTheSameValue(string value) + { + await using var db = Open(); + + db.ExecuteSQL("INSERT INTO t VALUES (@id, 'head', @val, 'tail')", P(("id", 1), ("val", value))); + + var rows = db.ExecuteQuery("SELECT * FROM t WHERE val = @val", P(("val", value))); + + Assert.Single(rows); + Assert.Equal(value, rows[0]["val"] as string); + } + + [Theory] + [MemberData(nameof(TextValues))] + public async Task ExecuteSQL_TextParameterInUpdateSet_RoundTripsByteExact(string value) + { + await using var db = Open(); + + db.ExecuteSQL("INSERT INTO t VALUES (1, 'head', 'original', 'tail')"); + db.ExecuteSQL( + "UPDATE t SET val = @val WHERE id = @id", + P(("val", value), ("id", 1))); + + var row = Read(db, 1); + Assert.Equal(value, row["val"] as string); + Assert.Equal("head", row["head"] as string); + Assert.Equal("tail", row["tail"] as string); + } + + // ── Literal SQL (not parameters) follows the same convention ──────────────────────────────── + + [Fact] + public async Task ExecuteSQL_LiteralWithDoubledQuote_StoresOneQuote() + { + await using var db = Open(); + + // SQL/SQLite: '' inside a literal is one quote and does not close it. + db.ExecuteSQL("INSERT INTO t VALUES (1, 'head', 'it''s', 'tail')"); + + Assert.Equal("it's", Read(db, 1)["val"] as string); + } + + [Fact] + public async Task ExecuteSQL_LiteralEndingInBackslash_StoresTheBackslash() + { + await using var db = Open(); + + // SQLite: a backslash is data, so the closing quote still closes the literal. + db.ExecuteSQL("INSERT INTO t VALUES (1, 'head', 'C:\\temp\\', 'tail')"); + + Assert.Equal("C:\\temp\\", Read(db, 1)["val"] as string); + Assert.Equal("tail", Read(db, 1)["tail"] as string); + } + + [Fact] + public async Task ExecuteSQL_LiteralWithCommaAndDoubledQuote_KeepsTheTupleInOnePiece() + { + await using var db = Open(); + + db.ExecuteSQL("INSERT INTO t VALUES (1, 'head', 'it''s fine, really', 'tail')"); + + var row = Read(db, 1); + Assert.Equal("it's fine, really", row["val"] as string); + Assert.Equal("tail", row["tail"] as string); + } + + // ── Safety net: a tuple with too many values fails loudly instead of shifting columns ─────── + + [Fact] + public async Task ExecuteSQL_InsertTupleWithMoreValuesThanColumns_ThrowsNamingTheTable() + { + await using var db = Open(); + + var ex = Assert.Throws( + () => db.ExecuteSQL("INSERT INTO t VALUES (1, 'a', 'b', 'c', 'too many')")); + + Assert.Contains("'t'", ex.Message, StringComparison.Ordinal); + Assert.Contains("5", ex.Message, StringComparison.Ordinal); + Assert.Contains("4", ex.Message, StringComparison.Ordinal); + } +} diff --git a/tests/SharpCoreDB.Tests/WritePathProfilerTests.cs b/tests/SharpCoreDB.Tests/WritePathProfilerTests.cs index 2bba2e5a..df6649f0 100644 --- a/tests/SharpCoreDB.Tests/WritePathProfilerTests.cs +++ b/tests/SharpCoreDB.Tests/WritePathProfilerTests.cs @@ -73,11 +73,13 @@ public async Task Enabled_AttributesTimeToStages_OfARealWriteWorkload() db.ExecuteSQL("CREATE TABLE docs (id INTEGER PRIMARY KEY, name TEXT, score REAL)"); for (int i = 0; i < 50; i++) { - db.ExecuteSQL($"INSERT INTO docs VALUES ({i}, 'user{i}', {i * 0.5})"); + // Invariant culture: a Dutch machine renders {i * 0.5} as "0,5", which is two SQL values, not one. + var score = (i * 0.5).ToString("0.0", System.Globalization.CultureInfo.InvariantCulture); + db.ExecuteSQL($"INSERT INTO docs VALUES ({i}, 'user{i}', {score})"); } db.ExecuteBatchSQL([.. Enumerable.Range(0, 50).Select(i => - $"UPDATE docs SET score = {i * 1.5} WHERE id = {i}")]); + $"UPDATE docs SET score = {(i * 1.5).ToString("0.0", System.Globalization.CultureInfo.InvariantCulture)} WHERE id = {i}")]); db.Flush(); }