diff --git a/Sources/Core/OutWit.Database.Core.BouncyCastle/README.md b/Sources/Core/OutWit.Database.Core.BouncyCastle/README.md index 4a0a9f44..368c4ea6 100644 --- a/Sources/Core/OutWit.Database.Core.BouncyCastle/README.md +++ b/Sources/Core/OutWit.Database.Core.BouncyCastle/README.md @@ -9,7 +9,7 @@ This package provides an alternative encryption algorithm when AES-NI hardware a ## Installation ```xml - + ``` --- diff --git a/Sources/Core/OutWit.Database.Core.IndexedDb/README.md b/Sources/Core/OutWit.Database.Core.IndexedDb/README.md index 20b71023..5f7d72c2 100644 --- a/Sources/Core/OutWit.Database.Core.IndexedDb/README.md +++ b/Sources/Core/OutWit.Database.Core.IndexedDb/README.md @@ -15,7 +15,7 @@ This package allows WitDatabase to run entirely in the browser with data persist ## Installation ```xml - + ``` Add the JavaScript files to your `index.html`: diff --git a/Sources/Core/OutWit.Database.Core.Tests/AuditVerification/SecondaryIndexConcurrencyProbeTests.cs b/Sources/Core/OutWit.Database.Core.Tests/AuditVerification/SecondaryIndexConcurrencyProbeTests.cs index ebbd5647..41e50a4a 100644 --- a/Sources/Core/OutWit.Database.Core.Tests/AuditVerification/SecondaryIndexConcurrencyProbeTests.cs +++ b/Sources/Core/OutWit.Database.Core.Tests/AuditVerification/SecondaryIndexConcurrencyProbeTests.cs @@ -164,10 +164,26 @@ public void ControlTheParkedWriterAloneLosesNothingTest() /// silent change - it is the standing evidence for why the wrapper exists. /// /// + /// /// The shape of the damage varies and is deliberately not pinned: over ten runs the second writer /// threw ArgumentOutOfRangeException or IndexOutOfRangeException nine times, and /// once nothing threw at all and three entries were simply gone - two of them the FIRST writer's, /// already inserted and acknowledged. The exception is the lucky outcome. + /// + /// + /// It asserted one of those shapes anyway until 2026-08-15 - that the SECOND writer's key + /// was among the missing ones - which contradicted the paragraph above. It reddened CI on a branch + /// that touches no engine code and passed on a re-run of the same commit: on a loaded runner the + /// second writer did not finish inside the two seconds + /// waits, so it landed after the + /// release and its own entry survived - while 207 of the first writer's were lost. The + /// damage was real and larger than usual, and the case failed for having said in advance which + /// entry it would be. + /// + /// + /// So it asserts what it can measure on a machine whose scheduling it does not control - that two + /// writers in one leaf split damage the index - and REPORTS the shape rather than pinning it. + /// /// [Test] public void ProbeConcurrentAddOverABareIndexStoreTest() @@ -185,13 +201,20 @@ public void ProbeConcurrentAddOverABareIndexStoreTest() // PINS A DEFECT, NOT CORRECT BEHAVIOUR. A bare StoreBTree is what CreateBTreeIndexFactory // hands every secondary index, and it has no locking of any kind: the second writer walks // straight into the leaf the first one is halfway through splitting, snapshots it, and - // the two then rewrite it from two different snapshots. Invert both assertions when index - // stores are serialised - nothing may be lost and no writer may throw. + // the two then rewrite it from two different snapshots. Invert this when index stores are + // serialised - nothing may be lost and no writer may throw. Assert.That(outcome.Damage, Is.Not.EqualTo(Damage.None), "two writers were inside the same leaf split and nothing went wrong - re-measure " + "before believing it"); - Assert.That(outcome.MissingKeys, Does.Contain(SECOND_WRITER_KEY), - "the second writer's entry survived - the damage this probe pins has moved"); + + // WHOSE entries went is the weather; that acknowledged work was lost, or that a writer + // threw, is the finding. Both are already what Damage classifies, so this says out loud + // what the value has to have come from. + Assert.That(outcome.MissingKeys.Count > 0 + || outcome.FirstWriterError != null + || outcome.SecondWriterError != null, Is.True, + "nothing is missing and nobody threw, so Damage was classified from something this " + + "probe does not measure: " + outcome.Describe()); }); } diff --git a/Sources/Core/OutWit.Database.Core.Tests/ShippedReadmesTests.cs b/Sources/Core/OutWit.Database.Core.Tests/ShippedReadmesTests.cs new file mode 100644 index 00000000..54ceaa7a --- /dev/null +++ b/Sources/Core/OutWit.Database.Core.Tests/ShippedReadmesTests.cs @@ -0,0 +1,140 @@ +using System.Text.RegularExpressions; + +namespace OutWit.Database.Core.Tests; + +/// +/// The READMEs that ship inside the packages install the version that is being shipped. +/// +/// +/// +/// A README goes into the NuGet package, so it reaches people who never open the repository or the +/// site - and the first thing they copy out of it is the PackageReference. Eight of them across +/// five packages pinned 12.8.0 while every package was on 13.1.1: the number was written once, +/// per file, and nothing could notice it going stale. +/// +/// +/// The rule is over the whole surface - every README under Sources, every pinned +/// version in it - rather than over the one file somebody noticed, and it counts what it examined so +/// that "nothing left to find" cannot read like "the folder moved". +/// +/// +[TestFixture] +public class ShippedReadmesTests +{ + #region Constants + + /// An install snippet: <PackageReference Include="X" Version="Y" />. + private static readonly Regex PACKAGE_REFERENCE = + new(@"OutWit\.[\w.]+)""\s+Version=""(?[^""]+)""", + RegexOptions.Compiled); + + /// The version a project declares: <Version>13.1.1</Version>. + private static readonly Regex PROJECT_VERSION = + new(@"(?[^<]+)", RegexOptions.Compiled); + + #endregion + + #region Tests + + [Test] + public void EveryInstallSnippetNamesTheVersionThatShipsTest() + { + var versions = ProjectVersions(); + var stale = new List(); + var examined = 0; + + foreach (var readme in Readmes()) + { + var text = File.ReadAllText(readme); + + foreach (Match match in PACKAGE_REFERENCE.Matches(text)) + { + examined++; + + var package = match.Groups["package"].Value; + var pinned = match.Groups["version"].Value; + + if (!versions.TryGetValue(package, out var shipping)) + { + stale.Add($"{Path.GetFileName(Path.GetDirectoryName(readme))}/README.md installs " + + $"{package}, which is not a project in this repository"); + continue; + } + + if (pinned != shipping) + { + stale.Add($"{Path.GetFileName(Path.GetDirectoryName(readme))}/README.md installs " + + $"{package} {pinned}; the package is {shipping}"); + } + } + } + + Assert.Multiple(() => + { + // THE SURFACE. Eight snippets across five READMEs today, and a rule that read none of + // them would pass exactly as loudly as one that read all of them. + Assert.That(examined, Is.EqualTo(8), + "the shipped READMEs carry a different number of install snippets than this rule was " + + "measured against - check the new one, then change this number"); + + Assert.That(versions, Has.Count.GreaterThan(5), + "CONTROL: almost no project version was read, so nothing here is being compared"); + + Assert.That(stale, Is.Empty, + "these install a version that is not the one being shipped:" + + Environment.NewLine + string.Join(Environment.NewLine, stale)); + }); + } + + #endregion + + #region Tools + + /// Package name -> the version its project declares. + private static Dictionary ProjectVersions() + { + var versions = new Dictionary(StringComparer.Ordinal); + + foreach (var project in Directory.EnumerateFiles(SourcesFolder(), "OutWit.*.csproj", + SearchOption.AllDirectories)) + { + var name = Path.GetFileNameWithoutExtension(project); + + if (name.EndsWith(".Tests", StringComparison.Ordinal)) + continue; + + var match = PROJECT_VERSION.Match(File.ReadAllText(project)); + + if (match.Success) + versions[name] = match.Groups["version"].Value; + } + + return versions; + } + + private static IEnumerable Readmes() => + Directory.EnumerateFiles(SourcesFolder(), "README.md", SearchOption.AllDirectories) + .Where(path => !path.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", + StringComparison.Ordinal) + && !path.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", + StringComparison.Ordinal)); + + private static string SourcesFolder() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + + while (directory != null) + { + var candidate = Path.Combine(directory.FullName, "Sources"); + + if (Directory.Exists(candidate)) + return candidate; + + directory = directory.Parent; + } + + throw new DirectoryNotFoundException("the Sources folder was not found from " + AppContext.BaseDirectory); + } + + #endregion +} diff --git a/Sources/Core/OutWit.Database.Core/README.md b/Sources/Core/OutWit.Database.Core/README.md index dd35c159..1d22342b 100644 --- a/Sources/Core/OutWit.Database.Core/README.md +++ b/Sources/Core/OutWit.Database.Core/README.md @@ -19,7 +19,8 @@ OutWit.Database.Core is a production-ready embedded database engine designed for ### Key Features -- **Storage Engines**: B+Tree (read-optimized) and LSM-Tree (write-optimized) +- **Storage Engines**: B+Tree, the default and the right choice for almost everything, and LSM-Tree + for a narrow shape of workload - see [Choosing a storage engine](#choosing-a-storage-engine) - **MVCC**: Multi-Version Concurrency Control with snapshot isolation - **5 Isolation Levels**: ReadUncommitted, ReadCommitted, RepeatableRead, Serializable, Snapshot - **Row-Level Locking**: FOR UPDATE, FOR SHARE, NOWAIT, SKIP LOCKED @@ -35,17 +36,17 @@ OutWit.Database.Core is a production-ready embedded database engine designed for ## Installation ```xml - + ``` For ChaCha20-Poly1305 encryption: ```xml - + ``` For Blazor WebAssembly (IndexedDB storage): ```xml - + ``` --- @@ -178,8 +179,8 @@ var db = new WitDatabaseBuilder() // .WithStorage(customStorage) // Custom IStorage // Engine - .WithBTree() // B+Tree (read-optimized) - // .WithLsmTree() // LSM-Tree (write-optimized) + .WithBTree() // B+Tree - the default, and what to use unless measured otherwise + // .WithLsmTree() // LSM-Tree - see "Choosing a storage engine" // .WithLsmTree(opts => { ... }) // LSM with custom options // .WithStore(customStore) // Custom IKeyValueStore @@ -456,9 +457,32 @@ var db = new WitDatabaseBuilder() --- +## Choosing a storage engine + +**`WithBTree()` is the default and is the right choice for almost everything.** "LSM is +write-optimised" was the claim here until it was measured, and the measurement retired it. + +Measured 2026-08-11, 100,000 rows written in batches of 1,000 through SQL, microseconds per row: + +| | B+Tree | LSM | +|---|---|---| +| **MVCC on** - what you get by default | 36.8 | **771.9** | +| `MVCC=false` | 15.1-27.4 | 16.7 | + +**MVCC is on by default**, so a database that names the store and nothing else gets the first row: +the B+Tree pays roughly 1.5-2x for MVCC and the LSM pays about **50x**. It is a per-row cost rather +than a per-transaction one, so no batch size amortises it, and `SyncWrites` is not the expensive +property. + +With `MVCC=false` the two stores write at the same speed, and that is the comparison the old claim +described. `Docs/WitSQL.md` § 14.9 has the full numbers and the shape of workload where the LSM store +is the better answer. + +--- + ## Performance Tips -1. **Choose the right engine**: B+Tree for reads, LSM-Tree for writes +1. **Start with B+Tree**, and read *Choosing a storage engine* above before changing it 2. **Tune cache size**: More cache = fewer disk reads 3. **Use appropriate page size**: 4KB default, 8KB-16KB for large values 4. **Batch operations**: Use transactions for multiple writes @@ -474,7 +498,7 @@ WitDatabase can run entirely in the browser using IndexedDB as the storage backe ### Installation ```xml - + ``` Add JavaScript files to `index.html`: diff --git a/Sources/Engine/OutWit.Database.Parser/README.md b/Sources/Engine/OutWit.Database.Parser/README.md index 2cfff874..4998a77d 100644 --- a/Sources/Engine/OutWit.Database.Parser/README.md +++ b/Sources/Engine/OutWit.Database.Parser/README.md @@ -26,7 +26,7 @@ OutWit.Database.Parser is a high-performance SQL parser built on [ANTLR4](https: ## Installation ```xml - + ``` --- diff --git a/Sources/Engine/OutWit.Database.Tests/Expressions/ExpressionEvaluatorFunctionsTests.cs b/Sources/Engine/OutWit.Database.Tests/Expressions/ExpressionEvaluatorFunctionsTests.cs index aa421556..f3aaf50f 100644 --- a/Sources/Engine/OutWit.Database.Tests/Expressions/ExpressionEvaluatorFunctionsTests.cs +++ b/Sources/Engine/OutWit.Database.Tests/Expressions/ExpressionEvaluatorFunctionsTests.cs @@ -1,3 +1,4 @@ +using System.Reflection; using OutWit.Database.Expressions; using OutWit.Database.Parser.Expressions; using OutWit.Database.Parser.Schema.Types; @@ -808,6 +809,16 @@ public void EvaluateDatabaseTest() Assert.That(result.AsString(), Is.EqualTo("WitDB")); } + /// + /// VERSION() answers the version of the engine that is running. + /// + /// + /// It answered the literal "1.0.0" until 2026-08-15, and this case pinned it there while + /// the engine was on 13.1.1 - `SELECT VERSION()` being the obvious thing for a user to run. The + /// expectation is read from the ASSEMBLY here, independently of the property under test: a case + /// carrying "13.1.1" in its own text would be the same defect one layer out, stale at the + /// next release. + /// [Test] public void EvaluateVersionTest() { @@ -816,7 +827,20 @@ public void EvaluateVersionTest() var result = evaluator.Evaluate(func, CreateEmptyRow()); - Assert.That(result.AsString(), Is.EqualTo("1.0.0")); + var informational = typeof(ExpressionEvaluator).Assembly + .GetCustomAttribute()!.InformationalVersion; + + var expected = informational.Split('+')[0]; + + Assert.Multiple(() => + { + Assert.That(result.AsString(), Is.EqualTo(expected)); + + // CONTROL: the assembly is not itself answering 1.0.0, which would make the comparison + // above pass for the wrong reason. + Assert.That(expected, Is.Not.EqualTo("1.0.0"), + "the engine assembly reports 1.0.0, so this case cannot tell the fix from the defect"); + }); } [Test] diff --git a/Sources/Engine/OutWit.Database/Expressions/ExpressionEvaluator.Functions.cs b/Sources/Engine/OutWit.Database/Expressions/ExpressionEvaluator.Functions.cs index 8d56d339..bbfea8f6 100644 --- a/Sources/Engine/OutWit.Database/Expressions/ExpressionEvaluator.Functions.cs +++ b/Sources/Engine/OutWit.Database/Expressions/ExpressionEvaluator.Functions.cs @@ -1,4 +1,4 @@ -using OutWit.Database.Parser.Expressions; +using OutWit.Database.Parser.Expressions; using OutWit.Database.Sql; using OutWit.Database.Types; using OutWit.Database.Values; @@ -186,7 +186,7 @@ private WitSqlValue EvaluateFunction(WitSqlExpressionFunctionCall func, WitSqlRo // System Functions "DATABASE" => WitSqlValue.FromText("WitDB"), - "VERSION" => WitSqlValue.FromText("1.0.0"), + "VERSION" => WitSqlValue.FromText(WitDatabaseVersion.Text), // Metadata Functions "CHANGES" => WitSqlValue.FromInt(m_context.LastChangesCount), diff --git a/Sources/Engine/OutWit.Database/README.md b/Sources/Engine/OutWit.Database/README.md index 3474cfd8..cc97361c 100644 --- a/Sources/Engine/OutWit.Database/README.md +++ b/Sources/Engine/OutWit.Database/README.md @@ -28,7 +28,7 @@ OutWit.Database is the SQL execution engine built on top of OutWit.Database.Core ## Installation ```xml - + ``` --- diff --git a/Sources/Engine/OutWit.Database/WitDatabaseVersion.cs b/Sources/Engine/OutWit.Database/WitDatabaseVersion.cs new file mode 100644 index 00000000..3eabd6e4 --- /dev/null +++ b/Sources/Engine/OutWit.Database/WitDatabaseVersion.cs @@ -0,0 +1,95 @@ +using System.Globalization; +using System.Reflection; + +namespace OutWit.Database; + +/// +/// What version of this engine is running, read from the assembly rather than written down. +/// +/// +/// +/// Four places answered 1.0.0 until 2026-08-15 - SELECT VERSION(), +/// WitDbConnection.ServerVersion, and both version rows of +/// GetSchema("DataSourceInformation"), which is what tooling and ORMs read. The engine was on +/// 13.1.1. Each was a literal, so each went stale on its own and nothing could notice. +/// +/// +/// The value is the assembly's INFORMATIONAL version, which is what the csproj's +/// <Version> becomes, cut at the + the SDK appends the commit sha after. It is +/// read once: an assembly's version cannot change while it is loaded. +/// +/// +public static class WitDatabaseVersion +{ + #region Fields + + private static readonly Lazy s_text = new(ReadText); + + private static readonly Lazy s_normalized = new(() => Normalize(s_text.Value)); + + #endregion + + #region Functions + + /// + /// Reads the version off this assembly, falling back to the assembly version when there is no + /// informational one - which happens only in a build that sets neither. + /// + private static string ReadText() + { + var assembly = typeof(WitDatabaseVersion).Assembly; + + var informational = assembly + .GetCustomAttribute()?.InformationalVersion; + + if (!string.IsNullOrEmpty(informational)) + { + // "13.1.1+89ac429…" - the sha is noise in an answer a person or a tool reads. + var plus = informational.IndexOf('+'); + + return plus > 0 ? informational[..plus] : informational; + } + + return assembly.GetName().Version?.ToString(3) ?? "0.0.0"; + } + + /// + /// The same version in the shape ADO.NET's DataSourceProductVersionNormalized is compared + /// in: two digits of major, two of minor, four of build, so that a string comparison orders + /// versions correctly. + /// + /// + /// A pre-release suffix is dropped rather than encoded - 13.1.1-rc.1 normalises to + /// 13.01.0001. The field exists to be COMPARED, and there is no ordering of suffixes that + /// a consumer could rely on. + /// + private static string Normalize(string version) + { + var numeric = version.Split('-', '+')[0]; + var parts = numeric.Split('.'); + + var major = Part(parts, 0); + var minor = Part(parts, 1); + var build = Part(parts, 2); + + return string.Format(CultureInfo.InvariantCulture, "{0:00}.{1:00}.{2:0000}", major, minor, build); + } + + private static int Part(string[] parts, int index) => + parts.Length > index && int.TryParse(parts[index], NumberStyles.None, CultureInfo.InvariantCulture, + out var value) + ? value + : 0; + + #endregion + + #region Properties + + /// The engine's version as a person would write it: 13.1.1. + public static string Text => s_text.Value; + + /// The same, zero-padded for comparison: 13.01.0001. + public static string Normalized => s_normalized.Value; + + #endregion +} diff --git a/Sources/Providers/OutWit.Database.AdoNet.Tests/Schema/VersionComesFromTheAssemblyTests.cs b/Sources/Providers/OutWit.Database.AdoNet.Tests/Schema/VersionComesFromTheAssemblyTests.cs new file mode 100644 index 00000000..52d81c91 --- /dev/null +++ b/Sources/Providers/OutWit.Database.AdoNet.Tests/Schema/VersionComesFromTheAssemblyTests.cs @@ -0,0 +1,123 @@ +using System.Data; +using System.Reflection; +using OutWit.Database.Engine; + +namespace OutWit.Database.AdoNet.Tests.Schema; + +/// +/// Everything that answers "which version is this" answers the same thing, and it comes from the +/// assembly. +/// +/// +/// +/// Four places answered the literal 1.0.0 until 2026-08-15, while the engine was on +/// 13.1.1: SELECT VERSION(), ServerVersion, and both version rows of +/// GetSchema("DataSourceInformation") - which is what tooling and ORMs read to decide what a +/// database can do. Four literals, four independent ways to go stale. +/// +/// +/// The expectation is read from the assembly here, not written down. A case carrying +/// "13.1.1" in its own text would be the same defect one layer out: it would pass today and +/// have to be edited at every release, which is exactly how the four literals survived thirteen +/// major versions. +/// +/// +[TestFixture] +public class VersionComesFromTheAssemblyTests +{ + #region Constants + + /// + /// The engine assembly's own informational version, minus the commit sha the SDK appends. Read + /// through a type of the ENGINE, because that is what a "server version" describes here. + /// + private static string EngineVersion => + typeof(WitSqlEngine).Assembly + .GetCustomAttribute()! + .InformationalVersion + .Split('+')[0]; + + #endregion + + #region Tests + + [Test] + public void ServerVersionIsTheEnginesVersionTest() + { + using var connection = new WitDbConnection("Data Source=:memory:"); + + Assert.Multiple(() => + { + Assert.That(connection.ServerVersion, Is.EqualTo(EngineVersion)); + + // CONTROL: if the assembly itself said 1.0.0, the comparison above would pass while the + // defect was still there. + Assert.That(EngineVersion, Is.Not.EqualTo("1.0.0"), + "the engine assembly reports 1.0.0, so nothing here can tell the fix from the defect"); + }); + } + + [Test] + public void TheSchemaRowsToolingReadsCarryTheSameVersionTest() + { + using var connection = new WitDbConnection("Data Source=:memory:"); + connection.Open(); + + var information = connection.GetSchema("DataSourceInformation"); + var row = information.Rows[0]; + + var text = (string)row["DataSourceProductVersion"]; + var normalized = (string)row["DataSourceProductVersionNormalized"]; + + Assert.Multiple(() => + { + Assert.That(text, Is.EqualTo(EngineVersion)); + Assert.That(text, Is.EqualTo(connection.ServerVersion), + "the two surfaces must not be able to disagree"); + + // The normalised form exists to be COMPARED as a string, so its shape is the point: + // two digits of major, two of minor, four of build. + Assert.That(normalized, Does.Match(@"^\d{2}\.\d{2}\.\d{4}$")); + + var parts = EngineVersion.Split('-', '+')[0].Split('.'); + + Assert.That(normalized, Is.EqualTo( + $"{int.Parse(parts[0]):00}.{int.Parse(parts[1]):00}.{int.Parse(parts[2]):0000}"), + "and it is the same version, zero-padded - not a second answer"); + }); + } + + /// + /// The engine's own answer, through SQL, is the same one. This is the surface a user reaches + /// first, and it was the loudest of the four. + /// + [Test] + public void SelectVersionAnswersTheSameTest() + { + using var connection = new WitDbConnection("Data Source=:memory:"); + connection.Open(); + + using var command = connection.CreateCommand(); + command.CommandText = "SELECT VERSION()"; + + Assert.That(command.ExecuteScalar()?.ToString(), Is.EqualTo(EngineVersion)); + } + + /// + /// CONTROL: the normalisation is a function of the version rather than of the current one, so it + /// is measured on values that are not this build's. + /// + [Test] + public void TheNormalisedFormIsPaddedAndDropsAPreReleaseSuffixTest() + { + Assert.Multiple(() => + { + Assert.That(WitDatabaseVersion.Normalized, Does.Match(@"^\d{2}\.\d{2}\.\d{4}$")); + Assert.That(WitDatabaseVersion.Text, Is.Not.Empty); + Assert.That(WitDatabaseVersion.Text, Does.Not.Contain("+"), + "the commit sha is not part of an answer a person or a tool reads"); + }); + } + + #endregion +} diff --git a/Sources/Providers/OutWit.Database.AdoNet/Schema/SchemaProvider.cs b/Sources/Providers/OutWit.Database.AdoNet/Schema/SchemaProvider.cs index 599316b9..a4fcd504 100644 --- a/Sources/Providers/OutWit.Database.AdoNet/Schema/SchemaProvider.cs +++ b/Sources/Providers/OutWit.Database.AdoNet/Schema/SchemaProvider.cs @@ -1,4 +1,4 @@ -using System.Data; +using System.Data; using OutWit.Database.Engine; namespace OutWit.Database.AdoNet.Schema; @@ -170,8 +170,8 @@ private static DataTable GetDataSourceInformation() var row = table.NewRow(); row["CompositeIdentifierSeparatorPattern"] = @"\."; row["DataSourceProductName"] = "WitDatabase"; - row["DataSourceProductVersion"] = "1.0.0"; - row["DataSourceProductVersionNormalized"] = "01.00.0000"; + row["DataSourceProductVersion"] = WitDatabaseVersion.Text; + row["DataSourceProductVersionNormalized"] = WitDatabaseVersion.Normalized; row["GroupByBehavior"] = 1; // GroupByBehavior.Unrelated row["IdentifierPattern"] = @"(^\[\p{Lo}\p{Lu}\p{Ll}_@#][\p{Lo}\p{Lu}\p{Ll}\p{Nd}@$#_]*$)|(^\[([^\]\0]|\]\])+\]$)|(^\"".+\""$)"; row["IdentifierCase"] = 1; // IdentifierCase.Insensitive diff --git a/Sources/Providers/OutWit.Database.AdoNet/WitDbConnection.cs b/Sources/Providers/OutWit.Database.AdoNet/WitDbConnection.cs index ce35af6f..c943ee00 100644 --- a/Sources/Providers/OutWit.Database.AdoNet/WitDbConnection.cs +++ b/Sources/Providers/OutWit.Database.AdoNet/WitDbConnection.cs @@ -1,4 +1,4 @@ -using System.Data; +using System.Data; using System.Data.Common; using Transaction = System.Transactions.Transaction; using OutWit.Database.AdoNet.Engines; @@ -905,7 +905,11 @@ public override string DataSource } /// - public override string ServerVersion => "1.0.0"; + /// + /// The ENGINE's version, read from its assembly. It answered the literal 1.0.0 until + /// 2026-08-15, which is what every ADO.NET consumer was told while the engine was on 13.1.1. + /// + public override string ServerVersion => WitDatabaseVersion.Text; /// public override ConnectionState State => m_state;