From 2f23913cef0d3453c0896c7792ffeba9be45ad2e Mon Sep 17 00:00:00 2001 From: MPCoreDeveloper Date: Mon, 26 Oct 2026 21:13:02 +0100 Subject: [PATCH 1/2] fix(sql): one literal scanner for the statement path, so bound text round-trips `ExecuteSQL(sql, parameters)` renders every value back into the statement text (`ParameterBinder` 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. They did not: the VALUES scanner dropped the quote of a doubled pair 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, which writes source text and Windows paths as TEXT values, saw it as rows rejected with "Mismatched parentheses in VALUES clause" and - the worse half - as a value silently landing in the neighbouring column (`Invalid VECTOR literal '... iterations)'` on `embedding`). The rules are now SQL's and SQLite's, written 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 truncated any quoted value at its first space) and the wire protocol's `$n` -> `?` translation (hence `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 strip loop 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, where a trailing `''` used to become NULL. 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) failed 32 of 57 assertions before the change 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). Suites: 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` issue 7 gains a two-file runnable reproduction with the before/after console output, so the next consumer does not have to take the symptom on trust. --- docs/CHANGELOG.md | 4 + docs/sharpcoredb-known-issues.md | 232 +++++++++++++++- .../BinaryProtocolHandler.cs | 26 +- .../DataStructures/Table.Collation.cs | 16 +- .../DataStructures/Table.QueryHelpers.cs | 21 +- .../DataStructures/Table.Scanning.cs | 17 +- .../Database/Execution/Database.Batch.cs | 103 ++++--- src/SharpCoreDB/DatabaseExtensions.cs | 4 +- .../Optimizations/WhereClauseAnalyzer.cs | 4 +- src/SharpCoreDB/Properties/AssemblyInfo.cs | 4 + .../Services/Compilation/FastSqlLexer.cs | 18 +- .../Services/PreparedStatements.cs | 84 +++--- src/SharpCoreDB/Services/SqlInPredicate.cs | 30 ++- src/SharpCoreDB/Services/SqlLiteralScanner.cs | 195 ++++++++++++++ .../Services/SqlParser.DML.InsertConflict.cs | 4 +- src/SharpCoreDB/Services/SqlParser.DML.cs | 240 ++++++++--------- src/SharpCoreDB/Services/SqlParser.Helpers.cs | 15 +- .../SqlParser.PerformanceOptimizations.cs | 9 +- .../Services/SqlParserExtensions.cs | 2 +- src/SharpCoreDB/Services/TypeConverter.cs | 17 +- src/SharpCoreDB/SharpCoreDB.csproj | 3 + src/SharpCoreDB/SingleFileTable.cs | 10 +- .../Storage/Scdb/SingleFileDatabase.Batch.cs | 74 ++--- .../EncryptionCoverageTests.cs | 6 +- .../InsertValuesParsingTests.cs | 66 +++-- .../ParameterLiteralRoundTripTests.cs | 255 ++++++++++++++++++ .../WritePathProfilerTests.cs | 6 +- 27 files changed, 1099 insertions(+), 366 deletions(-) create mode 100644 src/SharpCoreDB/Services/SqlLiteralScanner.cs create mode 100644 tests/SharpCoreDB.Tests/ParameterLiteralRoundTripTests.cs diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e849ddcd..8a94ce0c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 > bucket had collected since the `v2.1.0` tag — the list below — **shipped in `[2.1.1]`**. New entries > start here. +### 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 > **What this release is.** A patch release with no API changes, cut from `v2.1.0..HEAD`. The SQL vector 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.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/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/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..2ece711d 100644 --- a/src/SharpCoreDB/SharpCoreDB.csproj +++ b/src/SharpCoreDB/SharpCoreDB.csproj @@ -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(); } From 1787c15ebad6efac11898277865f6fb9cb463fc6 Mon Sep 17 00:00:00 2001 From: MPCoreDeveloper Date: Mon, 26 Oct 2026 21:19:14 +0100 Subject: [PATCH 2/2] Release 2.1.2, closing the Unreleased bucket and shipping the bound-parameter fix 2.1.1 is published and nuget.org versions are immutable, so the next publish has to carry a version that does not exist yet - and the Unreleased bucket had collected the statement-path literal scanner, a data-correctness fix rather than a performance one: a bound `TEXT` parameter holding an apostrophe, a doubled quote or a backslash was re-scanned as SQL, so the value could land in the neighbouring column, or the statement was rejected with "Mismatched parentheses in VALUES clause". No API changes, so a patch is the honest cut. Version bumped in the 28 packable projects under src/ (`Version`, plus `FileVersion` on the core library), plus `SharpCoreDBVersion` in Directory.Packages.props, which is what the providers and the test projects consume from the published feed. `AssemblyVersion` stays 2.1.0.0 on purpose: a patch should not change the assembly identity of the core library. `PackageReleaseNotes` gained a 2.1.2 line in all 23 packages that declare one, chained in front of the 2.1.1 and 2.1.0 text, and the EF Core provider's `Description` no longer asserts "Compatible with SharpCoreDB 2.1.1". `docs/CHANGELOG.md` closes `[Unreleased]` into `[2.1.2] - 2026-10-26` with a note on what the release is, keeping the fix entry evidence-first. Documentation, because the last two cuts left reader-facing pages naming releases that are superseded: - `src/SharpCoreDB/NuGet.README.md` - the page nuget.org serves - gains a "What's New in 2.1.2" section (symptom, cause, fix, a code sample, the validation numbers), and its title, banner, install snippets and footer move to 2.1.2. The footer still read "Latest Version: 2.1.0". - The 16 per-package `NuGet.README.md` files carried `dotnet add package ... --version 2.1.1`, which is the one line a reader copies off a package page; all now name 2.1.2. - `README.md` (the repository front page) still announced "Current release: v2.1.0" - a release behind - so it gets the 2.1.2 banner, a "What's new in v2.1.2" section, the two-line release table, the quick start and all 30 package snippets at 2.1.2, with 2.1.1 added to "Previous releases". - `src/SharpCoreDB.Server/README.md` pinned `--version 2.1.0-RC.3`, a prerelease that predates 2.1.0, and `docs/Vectors/README.md` pinned 2.1.0; both now 2.1.2. - `docs/PROJECT_STATUS.md` becomes current again: 2.1.2 is the version, and the stale "2.1.0 is prepared on master and not yet tagged or pushed" bullet - false since 2026-10-10 - is replaced by what shipped. - The two historical records (`docs/2.1.0-RC.3_WHAT_CHANGED.md`, `docs/net11/PROMOTION_TO_MAIN.md`) keep their RC.3 snippets on purpose: they describe that cut. Verified: `dotnet msbuild -getProperty:Version` reports 2.1.2 for all 28 packable projects (2.1.2.0 file version, 2.1.0.0 assembly version on the core); `SharpCoreDB.CI.slnf` builds with 0 errors under `/p:UseLocalProjectReferences=true` (required here - 2.1.2 does not exist on nuget.org yet); suites core 2107 / VectorSearch 255 / EF Core 116 / linq2DB 24, all 0 failed. The silent failure this avoids is the one 2.1.1's note recorded: the publish jobs push with `--skip-duplicate`, so a tree still declaring 2.1.1 would have gone green and shipped nothing. --- Directory.Packages.props | 2 +- README.md | 127 +++++++++++++----- docs/CHANGELOG.md | 21 ++- docs/PROJECT_STATUS.md | 22 ++- docs/Vectors/README.md | 4 +- docs/analytics/README.md | 2 +- docs/cqrs/README.md | 2 +- docs/graphrag/README.md | 6 +- docs/manual/quickstart.md | 2 +- docs/scdb/PRODUCTION_GUIDE.md | 2 +- src/SharpCoreDB.Analytics/NuGet.README.md | 2 +- .../SharpCoreDB.Analytics.csproj | 4 +- .../SharpCoreDB.Aspire.Hosting.csproj | 4 +- src/SharpCoreDB.CQRS/NuGet.README.md | 2 +- src/SharpCoreDB.CQRS/SharpCoreDB.CQRS.csproj | 4 +- .../SharpCoreDB.Client.Protocol.csproj | 4 +- src/SharpCoreDB.Client/NuGet.README.md | 2 +- .../SharpCoreDB.Client.csproj | 2 +- src/SharpCoreDB.Data.Provider/NuGet.README.md | 2 +- .../SharpCoreDB.Data.Provider.csproj | 4 +- src/SharpCoreDB.Distributed/NuGet.README.md | 2 +- .../SharpCoreDB.Distributed.csproj | 4 +- .../NuGet.README.md | 2 +- .../SharpCoreDB.EntityFrameworkCore.csproj | 6 +- src/SharpCoreDB.EventSourcing/NuGet.README.md | 2 +- .../SharpCoreDB.EventSourcing.csproj | 4 +- src/SharpCoreDB.Extensions/NuGet.README.md | 2 +- .../SharpCoreDB.Extensions.csproj | 4 +- .../SharpCoreDB.Functional.Dapper.csproj | 4 +- ...reDB.Functional.EntityFrameworkCore.csproj | 4 +- .../SharpCoreDB.Functional.Linq2DB.csproj | 4 +- .../SharpCoreDB.Functional.csproj | 4 +- .../NuGet.README.md | 2 +- .../SharpCoreDB.Graph.Advanced.csproj | 2 +- src/SharpCoreDB.Graph/NuGet.README.md | 2 +- .../SharpCoreDB.Graph.csproj | 4 +- .../SharpCoreDB.HybridSearch.csproj | 4 +- .../SharpCoreDB.Identity.csproj | 2 +- src/SharpCoreDB.Projections/NuGet.README.md | 2 +- .../SharpCoreDB.Projections.csproj | 4 +- src/SharpCoreDB.Provider.Sync/NuGet.README.md | 2 +- .../SharpCoreDB.Provider.Sync.csproj | 2 +- .../NuGet.README.md | 2 +- .../SharpCoreDB.Provider.YesSql.csproj | 4 +- .../SharpCoreDB.Search.csproj | 4 +- src/SharpCoreDB.Serilog.Sinks/NuGet.README.md | 2 +- .../SharpCoreDB.Serilog.Sinks.csproj | 4 +- .../SharpCoreDB.Server.Core.csproj | 4 +- .../SharpCoreDB.Server.Protocol.csproj | 4 +- src/SharpCoreDB.Server/NuGet.README.md | 2 +- src/SharpCoreDB.Server/README.md | 4 +- .../SharpCoreDB.Server.csproj | 2 +- src/SharpCoreDB.VectorSearch/NuGet.README.md | 2 +- .../SharpCoreDB.VectorSearch.csproj | 4 +- src/SharpCoreDB/NuGet.README.md | 63 +++++++-- src/SharpCoreDB/SharpCoreDB.csproj | 6 +- 56 files changed, 258 insertions(+), 137 deletions(-) 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 8a94ce0c..266f14fa 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -7,9 +7,24 @@ 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 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/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/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/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/SharpCoreDB.csproj b/src/SharpCoreDB/SharpCoreDB.csproj index 2ece711d..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