Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
03d88c9
feat(native): add OutWit.Database.Native AOT C ABI exports.
KarataevDmitry Jun 6, 2026
0e69566
fix(parser): allow ORDER BY/LIMIT in nested subqueries.
KarataevDmitry Jun 6, 2026
3a4c137
feat(native): SQL interop exports and index metadata JSON context.
KarataevDmitry Jun 11, 2026
c9f2c05
ci(parser): vendor ANTLR tool jar to fix flaky ANT02 builds.
KarataevDmitry Jun 12, 2026
a382c4a
fix(parser): set AntlrToolJar to vendored jar path for Linux CI.
KarataevDmitry Jun 12, 2026
6ee99de
fix(core): drain LsmMemTableFlusher queue before worker shutdown.
KarataevDmitry Jun 12, 2026
ed8b339
fix(parser): wire AntlrToolJar via ItemDefinitionGroup for CI.
KarataevDmitry Jun 12, 2026
a79268c
fix(core): drain LsmParallelCompactor queue before worker shutdown.
KarataevDmitry Jun 12, 2026
c8a61b1
fix(ado): normalize Windows paths in Database property on Linux.
KarataevDmitry Jun 12, 2026
fc6eb96
fix(core): drain LsmParallelWriter channel on shutdown.
KarataevDmitry Jun 12, 2026
d2042a2
fix(parser): apply AntlrToolJar defaults before grammar Include.
KarataevDmitry Jun 12, 2026
a933113
test(perf): relax dual-index scaling threshold for CI.
KarataevDmitry Jun 12, 2026
b3f579e
fix(parser): add SQLite $name parameter placeholders.
KarataevDmitry Jun 12, 2026
0c60bc7
fix(engine): preserve @/:/$ prefixes when binding parameters.
KarataevDmitry Jun 12, 2026
c51db85
test: cover SQLite $name parameters in parser, engine, and ADO.
KarataevDmitry Jun 12, 2026
0df9b79
docs(WitSQL): document $name named parameters.
KarataevDmitry Jun 12, 2026
0fb4cc6
test(perf): restore scaling thresholds and mark benchmarks as Perform…
KarataevDmitry Jun 12, 2026
af1d824
ci: exclude Performance category from PR test gate
KarataevDmitry Jun 12, 2026
05b8d0d
merge pr3 (CI stabilization + LSM shutdown + ORDER BY/LIMIT subquerie…
dmitrat Jun 13, 2026
c7f0362
merge pr2 ($name SQLite parameters)
dmitrat Jun 13, 2026
077bfd6
chore: defer NativeAOT C-ABI layer to its own PR
dmitrat Jun 13, 2026
12a988f
fix(core): drain LsmParallelWriter before cancel on shutdown (durabil…
dmitrat Jun 13, 2026
4c35690
fix(engine): resolve named placeholders symmetrically across prefix s…
dmitrat Jun 13, 2026
e2cdaf6
chore: bump Core/Parser/Database/AdoNet/EntityFramework to 1.1.0
dmitrat Jun 13, 2026
6dcdd19
test(perf): mark remaining performance fixtures as [Category("Perform…
dmitrat Jun 13, 2026
66fe2a4
fix(parser): pin vendored ANTLR jar via inline item metadata (determi…
dmitrat Jun 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,17 @@ jobs:
restore-keys: |
nuget-${{ runner.os }}-

- name: Verify vendored ANTLR tool JAR
run: test -f build/antlr/antlr4-4.13.1-complete.jar

- name: Restore
run: dotnet restore OutWit.slnx

- name: Build
run: dotnet build OutWit.slnx --configuration Release --no-restore

- name: Test
run: dotnet test OutWit.slnx --configuration Release --no-build --verbosity normal --logger "trx;LogFileName=test-results.trx" --results-directory TestResults
run: dotnet test OutWit.slnx --configuration Release --no-build --verbosity normal --filter "Category!=Performance" --logger "trx;LogFileName=test-results.trx" --results-directory TestResults

- name: Upload Test Results
if: always()
Expand Down
1 change: 1 addition & 0 deletions Docs/WitSQL.md
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,7 @@ WitSQL supports named and positional parameters:
-- Named parameters
SELECT * FROM Users WHERE Id = @UserId;
SELECT * FROM Users WHERE Name = :name;
SELECT * FROM Users WHERE MigrationId = $id;

-- Positional parameters
SELECT * FROM Users WHERE Id = ?;
Expand Down
111 changes: 111 additions & 0 deletions Sources/Core/OutWit.Database.Core.Tests/LSM/LsmParallelWriterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,117 @@ public void DisposedWriterThrowsTest()

#endregion

#region Shutdown Durability Tests

// These lock in the drain-before-cancel contract: every buffer submitted to the
// merge channel must be durably written through to the store on shutdown, and
// awaited writes must complete successfully rather than be faulted/dropped by a
// premature cancellation. A large maxPendingBuffers guarantees every submit is
// accepted (never silently rejected by a full bounded channel), so the assertions
// are deterministic: anything missing means the shutdown path lost queued work.

[Test]
public void DisposeDrainsAllQueuedBuffersTest()
{
var dir = Path.Combine(m_testDir, "dispose_drain_sync");
var options = new LsmOptions { EnableWal = false, MemTableSizeLimit = 1024 * 1024 };

using var store = new StoreLsm(dir, options);

const int count = 200;
var writer = new LsmParallelWriter(store, maxPendingBuffers: count * 2);

// Queue one buffer per key; do NOT wait for the background merge to catch up.
for (int i = 0; i < count; i++)
{
writer.Put(ToBytes($"k{i:D5}"), ToBytes($"v{i:D5}"));
writer.FlushCurrentBuffer();
}

// Dispose must drain the whole queue before returning.
writer.Dispose();

for (int i = 0; i < count; i++)
Assert.That(store.Get(ToBytes($"k{i:D5}")), Is.EqualTo(ToBytes($"v{i:D5}")), $"Missing k{i:D5} after Dispose");
}

[Test]
public async Task DisposeAsyncDrainsAllQueuedBuffersTest()
{
var dir = Path.Combine(m_testDir, "dispose_drain_async");
var options = new LsmOptions { EnableWal = false, MemTableSizeLimit = 1024 * 1024 };

using var store = new StoreLsm(dir, options);

const int count = 200;
var writer = new LsmParallelWriter(store, maxPendingBuffers: count * 2);

for (int i = 0; i < count; i++)
{
writer.Put(ToBytes($"k{i:D5}"), ToBytes($"v{i:D5}"));
writer.FlushCurrentBuffer();
}

await writer.DisposeAsync();

for (int i = 0; i < count; i++)
Assert.That(store.Get(ToBytes($"k{i:D5}")), Is.EqualTo(ToBytes($"v{i:D5}")), $"Missing k{i:D5} after DisposeAsync");
}

[Test]
public async Task DisposeCompletesAwaitedWritesSuccessfullyTest()
{
var dir = Path.Combine(m_testDir, "dispose_awaited");
var options = new LsmOptions { EnableWal = false, MemTableSizeLimit = 1024 * 1024 };

using var store = new StoreLsm(dir, options);

const int count = 50;
var writer = new LsmParallelWriter(store, maxPendingBuffers: count * 2);

// Issue awaited writes but capture the tasks WITHOUT awaiting them yet, so they
// are still in flight when shutdown begins. WriteAsync completes synchronously
// on the un-full channel, so all buffers + completions are enqueued before the
// state machines yield at 'await completion.Task'.
var flushTasks = new List<Task>();
for (int i = 0; i < count; i++)
{
writer.Put(ToBytes($"k{i:D5}"), ToBytes($"v{i:D5}"));
flushTasks.Add(writer.FlushCurrentBufferAsync());
}

await writer.DisposeAsync();

// Every awaited write must resolve successfully - not faulted or cancelled.
await Task.WhenAll(flushTasks);
Assert.That(flushTasks.All(t => t.IsCompletedSuccessfully), Is.True, "An awaited write did not complete successfully across shutdown");

for (int i = 0; i < count; i++)
Assert.That(store.Get(ToBytes($"k{i:D5}")), Is.EqualTo(ToBytes($"v{i:D5}")), $"Missing k{i:D5} after awaited write + Dispose");
}

[Test]
public void DisposeCompletesPromptlyWithoutHangingTest()
{
var dir = Path.Combine(m_testDir, "dispose_no_hang");
var options = new LsmOptions { EnableWal = false, MemTableSizeLimit = 1024 * 1024 };

using var store = new StoreLsm(dir, options);

var writer = new LsmParallelWriter(store, maxPendingBuffers: 500);
for (int i = 0; i < 100; i++)
{
writer.Put(ToBytes($"k{i:D5}"), ToBytes($"v{i:D5}"));
writer.FlushCurrentBuffer();
}

// The drain join is bounded; Dispose must return well within it.
var disposeTask = Task.Run(() => writer.Dispose());
Assert.That(disposeTask.Wait(TimeSpan.FromSeconds(10)), Is.True, "Dispose did not complete within the bounded drain window");
}

#endregion

#region Integration Tests

[Test]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using System.Text.Json.Serialization;

namespace OutWit.Database.Core.Indexes;

[JsonSerializable(typeof(IndexMetadata))]
[JsonSerializable(typeof(List<string>))]
internal sealed partial class IndexMetadataJsonContext : JsonSerializerContext;
16 changes: 8 additions & 8 deletions Sources/Core/OutWit.Database.Core/Indexes/IndexMetadataStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public void SaveIndex(string name, bool isUnique)

var metadata = new IndexMetadata { Name = name, IsUnique = isUnique };
var key = CreateKey(name);
var value = JsonSerializer.SerializeToUtf8Bytes(metadata);
var value = JsonSerializer.SerializeToUtf8Bytes(metadata, IndexMetadataJsonContext.Default.IndexMetadata);

m_store.Put(key, value);

Expand Down Expand Up @@ -84,7 +84,7 @@ public void SaveIndex(string name, bool isUnique)
if (value == null)
return null;

return JsonSerializer.Deserialize<IndexMetadata>(value);
return JsonSerializer.Deserialize(value, IndexMetadataJsonContext.Default.IndexMetadata);
}

/// <summary>
Expand Down Expand Up @@ -150,7 +150,7 @@ public async ValueTask SaveIndexAsync(string name, bool isUnique, CancellationTo

var metadata = new IndexMetadata { Name = name, IsUnique = isUnique };
var key = CreateKey(name);
var value = JsonSerializer.SerializeToUtf8Bytes(metadata);
var value = JsonSerializer.SerializeToUtf8Bytes(metadata, IndexMetadataJsonContext.Default.IndexMetadata);

await m_store.PutAsync(key, value, cancellationToken).ConfigureAwait(false);

Expand All @@ -177,7 +177,7 @@ public async ValueTask SaveIndexAsync(string name, bool isUnique, CancellationTo
if (value == null)
return null;

return JsonSerializer.Deserialize<IndexMetadata>(value);
return JsonSerializer.Deserialize(value, IndexMetadataJsonContext.Default.IndexMetadata);
}

/// <summary>
Expand Down Expand Up @@ -244,7 +244,7 @@ private List<string> LoadCatalog()

try
{
return JsonSerializer.Deserialize<List<string>>(value) ?? [];
return JsonSerializer.Deserialize(value, IndexMetadataJsonContext.Default.ListString) ?? [];
}
catch
{
Expand All @@ -254,7 +254,7 @@ private List<string> LoadCatalog()

private void SaveCatalog(List<string> catalog)
{
var value = JsonSerializer.SerializeToUtf8Bytes(catalog);
var value = JsonSerializer.SerializeToUtf8Bytes(catalog, IndexMetadataJsonContext.Default.ListString);
m_store.Put(CATALOG_KEY, value);
}

Expand All @@ -271,7 +271,7 @@ private async ValueTask<List<string>> LoadCatalogAsync(CancellationToken cancell

try
{
return JsonSerializer.Deserialize<List<string>>(value) ?? [];
return JsonSerializer.Deserialize(value, IndexMetadataJsonContext.Default.ListString) ?? [];
}
catch
{
Expand All @@ -281,7 +281,7 @@ private async ValueTask<List<string>> LoadCatalogAsync(CancellationToken cancell

private async ValueTask SaveCatalogAsync(List<string> catalog, CancellationToken cancellationToken = default)
{
var value = JsonSerializer.SerializeToUtf8Bytes(catalog);
var value = JsonSerializer.SerializeToUtf8Bytes(catalog, IndexMetadataJsonContext.Default.ListString);
await m_store.PutAsync(CATALOG_KEY, value, cancellationToken).ConfigureAwait(false);
}

Expand Down
6 changes: 2 additions & 4 deletions Sources/Core/OutWit.Database.Core/LSM/LsmMemTableFlusher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,9 @@ public void Dispose()
m_disposed = true;

m_flushChannel.Writer.Complete();
m_cts.Cancel();

Task.WaitAll(m_flushTasks, TimeSpan.FromSeconds(10));

m_cts.Cancel();
m_cts.Dispose();
}

Expand All @@ -287,10 +286,9 @@ public async ValueTask DisposeAsync()
m_disposed = true;

m_flushChannel.Writer.Complete();
await m_cts.CancelAsync();

await Task.WhenAll(m_flushTasks).WaitAsync(TimeSpan.FromSeconds(10));

await m_cts.CancelAsync();
m_cts.Dispose();
}

Expand Down
6 changes: 2 additions & 4 deletions Sources/Core/OutWit.Database.Core/LSM/LsmParallelCompactor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -272,10 +272,9 @@ public void Dispose()
m_disposed = true;

m_jobChannel.Writer.Complete();
m_cts.Cancel();

Task.WaitAll(m_workerTasks, TimeSpan.FromSeconds(30));

m_cts.Cancel();
m_cts.Dispose();
}

Expand All @@ -289,10 +288,9 @@ public async ValueTask DisposeAsync()
m_disposed = true;

m_jobChannel.Writer.Complete();
await m_cts.CancelAsync();

await Task.WhenAll(m_workerTasks).WaitAsync(TimeSpan.FromSeconds(30));

await m_cts.CancelAsync();
m_cts.Dispose();
}

Expand Down
Loading
Loading