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;