diff --git a/.github/workflows/csharp.yml b/.github/workflows/csharp.yml index cf693ae..9dd11a3 100644 --- a/.github/workflows/csharp.yml +++ b/.github/workflows/csharp.yml @@ -3,9 +3,9 @@ name: C# on: push: branches: [main] - paths: ['csharp/**', '.github/workflows/csharp.yml'] + paths: ['csharp/**', 'experiments/system_data_sqlite/**', '.github/workflows/csharp.yml'] pull_request: - paths: ['csharp/**', '.github/workflows/csharp.yml'] + paths: ['csharp/**', 'experiments/system_data_sqlite/**', '.github/workflows/csharp.yml'] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -31,3 +31,5 @@ jobs: global-json-file: csharp/global.json - run: dotnet build -c Release -warnaserror - run: dotnet test --project SQLiteVSDoublets.Tests -c Release --no-build + - name: Check SQLite provider reports + run: python ../experiments/system_data_sqlite/check_reports.py diff --git a/README.md b/README.md index 9530dbe..9a105c0 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ Rust ([doublets](https://crates.io/crates/doublets) and [rusqlite](https://crates.io/crates/rusqlite) with bundled SQLite) and C# ([Platform.Data.Doublets](https://www.nuget.org/packages/Platform.Data.Doublets), [Platform.Data.Doublets.Sequences](https://www.nuget.org/packages/Platform.Data.Doublets.Sequences) -and [Microsoft.Data.Sqlite](https://www.nuget.org/packages/Microsoft.Data.Sqlite)) +and the providers [Microsoft.Data.Sqlite](https://www.nuget.org/packages/Microsoft.Data.Sqlite) +and [System.Data.SQLite](https://www.nuget.org/packages/System.Data.SQLite)) run the same workloads on the same deterministic data, with 32 bit (`u32`/`uint`) and 64 bit (`u64`/`ulong`) ids: @@ -42,6 +43,16 @@ with index trees) and split (separate data and index arrays), each volatile with SQLite of the same durability: volatile with `SQLite Memory`, non-volatile with `SQLite File`. +C# also runs `SystemDataSQLite_Memory` and `SystemDataSQLite_File` with +System.Data.SQLite 2.0.4. `SQLite_Memory` and `SQLite_File` use +Microsoft.Data.Sqlite. Both providers use the same schemas, parameterized +commands, immediate transactions and native `e_sqlite3` library supplied by +Microsoft.Data.Sqlite, on Linux, macOS and Windows. The JSON reports record +each provider and engine version in `sqlite_providers`. The System.Data.SQLite +rows compare with Microsoft.Data.Sqlite of the same durability. Published +tables from older runs include only the providers measured in those runs; +the next Benchmarks run adds the new rows. + Every repetition runs on a fresh store in an empty directory, after discarded warm-up repetitions on up to 10,000 records that run for at least a second, so the .NET JIT has already optimized the code. Each operation is one @@ -76,6 +87,10 @@ cargo run --release --manifest-path rust/Cargo.toml -- \ links 64 100000 --output results/links-rust-64-100000.json dotnet run -c Release --project csharp/SQLiteVSDoublets -- \ objects 32 1000 --variants SQLite_File,Doublets_Split_NonVolatile_Cached +dotnet run -c Release --project csharp/SQLiteVSDoublets -- \ + links 64 1000 --repetitions 3 \ + --variants SQLite_Memory,SystemDataSQLite_Memory \ + --output results/links-csharp-64-1000.json # print the tables, add --readme README.md --charts docs/benchmarks to update python3 scripts/benchmark_report.py results ``` diff --git a/README.ru.md b/README.ru.md index 761f35d..8f431c3 100644 --- a/README.ru.md +++ b/README.ru.md @@ -18,7 +18,8 @@ Rust ([doublets](https://crates.io/crates/doublets) и [rusqlite](https://crates.io/crates/rusqlite) со встроенной SQLite) и C# ([Platform.Data.Doublets](https://www.nuget.org/packages/Platform.Data.Doublets), [Platform.Data.Doublets.Sequences](https://www.nuget.org/packages/Platform.Data.Doublets.Sequences) -и [Microsoft.Data.Sqlite](https://www.nuget.org/packages/Microsoft.Data.Sqlite)) +и провайдеры [Microsoft.Data.Sqlite](https://www.nuget.org/packages/Microsoft.Data.Sqlite) +и [System.Data.SQLite](https://www.nuget.org/packages/System.Data.SQLite)) выполняют одинаковую нагрузку на одинаковых детерминированных данных с 32-битными (`u32`/`uint`) и 64-битными (`u64`/`ulong`) идентификаторами: @@ -42,6 +43,16 @@ Rust ([doublets](https://crates.io/crates/doublets) и файлах). Дуплеты сравниваются с SQLite той же надёжности хранения: энергозависимые с `SQLite Memory`, энергонезависимые с `SQLite File`. +C# также проверяет `SystemDataSQLite_Memory` и `SystemDataSQLite_File` +с System.Data.SQLite 2.0.4. `SQLite_Memory` и `SQLite_File` используют +Microsoft.Data.Sqlite. Оба провайдера используют одинаковые схемы, +параметризованные команды, немедленные транзакции и нативную библиотеку +`e_sqlite3` из Microsoft.Data.Sqlite на Linux, macOS и Windows. JSON-отчёты +содержат версии каждого провайдера и движка в `sqlite_providers`. +Строки System.Data.SQLite сравниваются с Microsoft.Data.Sqlite той же +надёжности хранения. Опубликованные таблицы старых запусков содержат только +измеренные тогда провайдеры; следующий запуск Benchmarks добавит новые строки. + Каждый повтор выполняется на новом хранилище в пустой папке после отбрасываемых прогревочных повторов на не более чем 10 000 записей, которые длятся не меньше секунды, чтобы JIT .NET уже оптимизировал код. Каждая @@ -77,6 +88,10 @@ cargo run --release --manifest-path rust/Cargo.toml -- \ links 64 100000 --output results/links-rust-64-100000.json dotnet run -c Release --project csharp/SQLiteVSDoublets -- \ objects 32 1000 --variants SQLite_File,Doublets_Split_NonVolatile_Cached +dotnet run -c Release --project csharp/SQLiteVSDoublets -- \ + links 64 1000 --repetitions 3 \ + --variants SQLite_Memory,SystemDataSQLite_Memory \ + --output results/links-csharp-64-1000.json # печатает таблицы, для обновления этого файла добавьте # --readme README.ru.md --charts docs/benchmarks python3 scripts/benchmark_report.py results diff --git a/csharp/SQLiteVSDoublets.Tests/StoragesTests.cs b/csharp/SQLiteVSDoublets.Tests/StoragesTests.cs index 9316718..4a85878 100644 --- a/csharp/SQLiteVSDoublets.Tests/StoragesTests.cs +++ b/csharp/SQLiteVSDoublets.Tests/StoragesTests.cs @@ -31,6 +31,13 @@ public void ScatteredVisitsEveryIdOnce(ulong n) => public static TheoryData ObjectsVariants => [.. Harness.ObjectsVariants]; + [Theory, InlineData("SystemDataSQLite_Memory"), InlineData("SystemDataSQLite_File")] + public void SystemDataSQLiteVariantsAreRegistered(string variant) + { + Assert.Contains(variant, Harness.LinksVariants); + Assert.Contains(variant, Harness.ObjectsVariants); + } + [Theory, MemberData(nameof(LinksVariants))] public void EveryLinksVariantPassesTheLifecycle(string variant) { @@ -103,9 +110,31 @@ public void EveryObjectsStorageReturnsTheStoredPosts(bool cached) { RoundTrip(SQLiteBlogPosts.InMemory()); RoundTrip(SQLiteBlogPosts.InMemory()); + RoundTrip(SQLiteBlogPosts.InMemory(SQLiteProvider.SystemDataSQLite)); + RoundTrip(SQLiteBlogPosts.InMemory(SQLiteProvider.SystemDataSQLite)); RoundTrip(new DoubletsBlogPosts(Harness.United(new HeapResizableDirectMemory(), external: true), cached)); RoundTrip(new DoubletsBlogPosts(Harness.United(new HeapResizableDirectMemory(), external: true), cached)); RoundTrip(new DoubletsBlogPosts(Harness.Split(new HeapResizableDirectMemory(), new HeapResizableDirectMemory(), external: true), cached)); RoundTrip(new DoubletsBlogPosts(Harness.Split(new HeapResizableDirectMemory(), new HeapResizableDirectMemory(), external: true), cached)); } + + [Theory, InlineData(SQLiteProvider.MicrosoftDataSqlite), InlineData(SQLiteProvider.SystemDataSQLite)] + public void SQLiteCommandsCanBeReusedAfterRollback(SQLiteProvider provider) + { + using var links = SQLiteLinks.InMemory(provider); + Assert.Throws(() => links.Transaction(() => + { + links.Create(1, 2); + throw new InvalidOperationException("roll back the inserted link"); + })); + Assert.Equal(0UL, links.Count()); + Assert.Null(links.Search(1, 2)); + var id = links.Transaction(() => links.Create(3, 4)); + Assert.Equal(new Link(id, 3, 4), links.Get(id)); + Assert.Equal(1UL, links.Count()); + } + + [Fact] + public void BothSQLiteProvidersUseTheSameNativeEngine() => + Assert.Equal(SQLiteStorage.Version(SQLiteProvider.MicrosoftDataSqlite), SQLiteStorage.Version(SQLiteProvider.SystemDataSQLite)); } diff --git a/csharp/SQLiteVSDoublets/Harness.cs b/csharp/SQLiteVSDoublets/Harness.cs index 8d16019..98cc653 100644 --- a/csharp/SQLiteVSDoublets/Harness.cs +++ b/csharp/SQLiteVSDoublets/Harness.cs @@ -21,6 +21,8 @@ public static class Harness [ "SQLite_Memory", "SQLite_File", + "SystemDataSQLite_Memory", + "SystemDataSQLite_File", "Doublets_United_Volatile", "Doublets_United_NonVolatile", "Doublets_Split_Volatile", @@ -31,6 +33,8 @@ public static class Harness [ "SQLite_Memory", "SQLite_File", + "SystemDataSQLite_Memory", + "SystemDataSQLite_File", "Doublets_United_Volatile_Cached", "Doublets_United_Volatile_Uncached", "Doublets_United_NonVolatile_Cached", @@ -328,6 +332,8 @@ public static Measurement MeasureLinks(string variant, ulong n, int repetitio { "SQLite_Memory" => _ => SQLiteLinks.InMemory(), "SQLite_File" => dir => SQLiteLinks.Open(File(dir, "links.db")), + "SystemDataSQLite_Memory" => _ => SQLiteLinks.InMemory(SQLiteProvider.SystemDataSQLite), + "SystemDataSQLite_File" => dir => SQLiteLinks.Open(File(dir, "links.db"), SQLiteProvider.SystemDataSQLite), "Doublets_United_Volatile" => _ => new DoubletsLinks(United(new HeapResizableDirectMemory(), external: false)), "Doublets_United_NonVolatile" => dir => new DoubletsLinks(United(Mapped(dir, "links.links"), external: false)), "Doublets_Split_Volatile" => _ => new DoubletsLinks(Split(new HeapResizableDirectMemory(), new HeapResizableDirectMemory(), external: false)), @@ -345,6 +351,8 @@ public static Measurement MeasureObjects(string variant, ulong n, int repetit { "SQLite_Memory" => _ => SQLiteBlogPosts.InMemory(), "SQLite_File" => dir => SQLiteBlogPosts.Open(File(dir, "blog_posts.db")), + "SystemDataSQLite_Memory" => _ => SQLiteBlogPosts.InMemory(SQLiteProvider.SystemDataSQLite), + "SystemDataSQLite_File" => dir => SQLiteBlogPosts.Open(File(dir, "blog_posts.db"), SQLiteProvider.SystemDataSQLite), "Doublets_United_Volatile" => _ => new DoubletsBlogPosts(United(new HeapResizableDirectMemory(), external: true), cached), "Doublets_United_NonVolatile" => dir => new DoubletsBlogPosts(United(Mapped(dir, "links.links"), external: true), cached), "Doublets_Split_Volatile" => _ => new DoubletsBlogPosts(Split(new HeapResizableDirectMemory(), new HeapResizableDirectMemory(), external: true), cached), diff --git a/csharp/SQLiteVSDoublets/Links.cs b/csharp/SQLiteVSDoublets/Links.cs index ee76e5e..cf4f512 100644 --- a/csharp/SQLiteVSDoublets/Links.cs +++ b/csharp/SQLiteVSDoublets/Links.cs @@ -1,5 +1,6 @@ +using System.Data; +using System.Data.Common; using System.Numerics; -using Microsoft.Data.Sqlite; using Platform.Data; using Platform.Data.Doublets; @@ -24,28 +25,32 @@ public interface ILinksStorage : IDisposable where T : struct public sealed class SQLiteLinks : SQLiteStorage, ILinksStorage where T : struct, IBinaryInteger { - private static readonly (string, SqliteType) IdParameter = ("$id", SqliteType.Integer); - private static readonly (string, SqliteType) FromParameter = ("$from", SqliteType.Integer); - private static readonly (string, SqliteType) ToParameter = ("$to", SqliteType.Integer); + private static readonly (string, DbType) IdParameter = ("$id", DbType.Int64); + private static readonly (string, DbType) FromParameter = ("$from", DbType.Int64); + private static readonly (string, DbType) ToParameter = ("$to", DbType.Int64); - private readonly SqliteCommand _create; - private readonly SqliteCommand _update; - private readonly SqliteCommand _delete; - private readonly SqliteCommand _get; - private readonly SqliteCommand _search; - private readonly SqliteCommand _each; - private readonly SqliteCommand _eachWithFrom; - private readonly SqliteCommand _eachWithTo; + private readonly DbCommand _create; + private readonly DbCommand _update; + private readonly DbCommand _delete; + private readonly DbCommand _get; + private readonly DbCommand _search; + private readonly DbCommand _each; + private readonly DbCommand _eachWithFrom; + private readonly DbCommand _eachWithTo; - public static SQLiteLinks Open(string path) => new(path); + public static SQLiteLinks Open(string path) => Open(path, SQLiteProvider.MicrosoftDataSqlite); - public static SQLiteLinks InMemory() => new(":memory:"); + public static SQLiteLinks Open(string path, SQLiteProvider provider) => new(path, provider); - private SQLiteLinks(string path) : base(path, "links", """ + public static SQLiteLinks InMemory() => InMemory(SQLiteProvider.MicrosoftDataSqlite); + + public static SQLiteLinks InMemory(SQLiteProvider provider) => new(":memory:", provider); + + private SQLiteLinks(string path, SQLiteProvider provider) : base(path, "links", """ CREATE TABLE links (id INTEGER PRIMARY KEY, "from" INTEGER NOT NULL, "to" INTEGER NOT NULL); CREATE INDEX links_from_to ON links ("from", "to"); CREATE INDEX links_to_from ON links ("to", "from"); - """) + """, provider) { _create = Command("""INSERT INTO links ("from", "to") VALUES ($from, $to)""", FromParameter, ToParameter); _update = Command("""UPDATE links SET "from" = $from, "to" = $to WHERE id = $id""", IdParameter, FromParameter, ToParameter); @@ -59,7 +64,7 @@ private SQLiteLinks(string path) : base(path, "links", """ private static T Id(long sql) => T.CreateChecked(sql); - private static SqliteCommand With(SqliteCommand command, params ReadOnlySpan values) + private static DbCommand With(DbCommand command, params ReadOnlySpan values) { for (var i = 0; i < values.Length; i++) { @@ -92,7 +97,7 @@ public T Create(T from, T to) public void EachWithTo(T to, Action> visit) => Read(With(_eachWithTo, to), visit); - private static void Read(SqliteCommand command, Action> visit) + private static void Read(DbCommand command, Action> visit) { using var reader = command.ExecuteReader(); while (reader.Read()) diff --git a/csharp/SQLiteVSDoublets/Objects.cs b/csharp/SQLiteVSDoublets/Objects.cs index bd9045d..7d2d11d 100644 --- a/csharp/SQLiteVSDoublets/Objects.cs +++ b/csharp/SQLiteVSDoublets/Objects.cs @@ -1,5 +1,6 @@ +using System.Data; +using System.Data.Common; using System.Numerics; -using Microsoft.Data.Sqlite; using Platform.Collections.Stacks; using Platform.Converters; using Platform.Data; @@ -26,27 +27,31 @@ public interface IBlogPostsStorage : IDisposable where T : struct public sealed class SQLiteBlogPosts : SQLiteStorage, IBlogPostsStorage where T : struct, IBinaryInteger { - private readonly SqliteCommand _create; - private readonly SqliteCommand _get; - private readonly SqliteCommand _each; - private readonly SqliteCommand _delete; + private readonly DbCommand _create; + private readonly DbCommand _get; + private readonly DbCommand _each; + private readonly DbCommand _delete; - public static SQLiteBlogPosts Open(string path) => new(path); + public static SQLiteBlogPosts Open(string path) => Open(path, SQLiteProvider.MicrosoftDataSqlite); - public static SQLiteBlogPosts InMemory() => new(":memory:"); + public static SQLiteBlogPosts Open(string path, SQLiteProvider provider) => new(path, provider); - private SQLiteBlogPosts(string path) : base(path, "blog_posts", - "CREATE TABLE blog_posts (id INTEGER PRIMARY KEY, title TEXT NOT NULL, content TEXT NOT NULL, publication_date INTEGER NOT NULL)") + public static SQLiteBlogPosts InMemory() => InMemory(SQLiteProvider.MicrosoftDataSqlite); + + public static SQLiteBlogPosts InMemory(SQLiteProvider provider) => new(":memory:", provider); + + private SQLiteBlogPosts(string path, SQLiteProvider provider) : base(path, "blog_posts", + "CREATE TABLE blog_posts (id INTEGER PRIMARY KEY, title TEXT NOT NULL, content TEXT NOT NULL, publication_date INTEGER NOT NULL)", provider) { _create = Command( "INSERT INTO blog_posts (title, content, publication_date) VALUES ($title, $content, $publication_date)", - ("$title", SqliteType.Text), ("$content", SqliteType.Text), ("$publication_date", SqliteType.Integer)); - _get = Command("SELECT title, content, publication_date FROM blog_posts WHERE id = $id", ("$id", SqliteType.Integer)); + ("$title", DbType.String), ("$content", DbType.String), ("$publication_date", DbType.Int64)); + _get = Command("SELECT title, content, publication_date FROM blog_posts WHERE id = $id", ("$id", DbType.Int64)); _each = Command("SELECT title, content, publication_date, id FROM blog_posts"); - _delete = Command("DELETE FROM blog_posts WHERE id = $id", ("$id", SqliteType.Integer)); + _delete = Command("DELETE FROM blog_posts WHERE id = $id", ("$id", DbType.Int64)); } - private static BlogPost BlogPost(SqliteDataReader reader) => + private static BlogPost BlogPost(DbDataReader reader) => new(reader.GetString(0), reader.GetString(1), (ulong)reader.GetInt64(2)); public T Create(BlogPost post) diff --git a/csharp/SQLiteVSDoublets/Program.cs b/csharp/SQLiteVSDoublets/Program.cs index 2d022b2..d2a3254 100644 --- a/csharp/SQLiteVSDoublets/Program.cs +++ b/csharp/SQLiteVSDoublets/Program.cs @@ -7,7 +7,6 @@ using System.Text.Json; using System.Text.Json.Nodes; using Comparisons.SQLiteVSDoublets; -using Microsoft.Data.Sqlite; const ulong LinksWork = 3_000_000; const ulong ObjectsWork = 500_000; @@ -37,10 +36,6 @@ _ => throw new ArgumentException("category must be links or objects and bits must be 32 or 64"), }).ToList(); -using var connection = new SqliteConnection("Data Source=:memory:"); -connection.Open(); -using var version = connection.CreateCommand(); -version.CommandText = "SELECT sqlite_version()"; var report = new JsonObject { ["language"] = "C#", @@ -50,7 +45,20 @@ ["repetitions"] = repetitions, ["warm_up_size"] = Math.Min(size, Harness.WarmUpSize), ["warm_up_seconds"] = Harness.WarmUpTime.TotalSeconds, - ["sqlite_version"] = (string)version.ExecuteScalar()!, + ["sqlite_version"] = SQLiteStorage.Version(SQLiteProvider.MicrosoftDataSqlite), + ["sqlite_providers"] = new JsonObject + { + ["Microsoft.Data.Sqlite"] = new JsonObject + { + ["provider_version"] = typeof(Microsoft.Data.Sqlite.SqliteConnection).Assembly.GetName().Version!.ToString(), + ["sqlite_version"] = SQLiteStorage.Version(SQLiteProvider.MicrosoftDataSqlite), + }, + ["System.Data.SQLite"] = new JsonObject + { + ["provider_version"] = typeof(System.Data.SQLite.SQLiteConnection).Assembly.GetName().Version!.ToString(), + ["sqlite_version"] = SQLiteStorage.Version(SQLiteProvider.SystemDataSQLite), + }, + }, ["results"] = new JsonArray(measurements.Select(measurement => (JsonNode)measurement.ToJson()).ToArray()), }; var json = report.ToJsonString(new JsonSerializerOptions { WriteIndented = true }); diff --git a/csharp/SQLiteVSDoublets/SQLiteStorage.cs b/csharp/SQLiteVSDoublets/SQLiteStorage.cs index 05b4f20..791fdf3 100644 --- a/csharp/SQLiteVSDoublets/SQLiteStorage.cs +++ b/csharp/SQLiteVSDoublets/SQLiteStorage.cs @@ -1,39 +1,75 @@ +using System.Data; +using System.Data.Common; +using System.Data.SQLite; using Microsoft.Data.Sqlite; using SQLitePCL; namespace Comparisons.SQLiteVSDoublets; +public enum SQLiteProvider +{ + MicrosoftDataSqlite, + SystemDataSQLite, +} + /// A connection with reusable prepared commands that all join the current transaction. public abstract class SQLiteStorage : IDisposable { - private readonly SqliteConnection _connection; - private readonly List _commands = []; + private readonly DbConnection _connection; + private readonly List _commands = []; private readonly string _table; - protected SQLiteStorage(string path, string table, string schema) + protected SQLiteStorage(string path, string table, string schema, SQLiteProvider provider) { - _connection = new SqliteConnection($"Data Source={path};Pooling=False"); - _connection.Open(); + _connection = OpenConnection(path, provider); _table = table; using var create = _connection.CreateCommand(); create.CommandText = schema; create.ExecuteNonQuery(); } - protected SqliteCommand Command(string sql, params (string Name, SqliteType Type)[] parameters) + private static DbConnection OpenConnection(string path, SQLiteProvider provider) + { + DbConnection connection = provider switch + { + SQLiteProvider.MicrosoftDataSqlite => new SqliteConnection(new SqliteConnectionStringBuilder { DataSource = path, Pooling = false }.ToString()), + SQLiteProvider.SystemDataSQLite => new SQLiteConnection(new SQLiteConnectionStringBuilder { DataSource = path, Pooling = false }.ToString()), + _ => throw new ArgumentOutOfRangeException(nameof(provider)), + }; + connection.Open(); + return connection; + } + + public static string Version(SQLiteProvider provider) + { + using var connection = OpenConnection(":memory:", provider); + using var version = connection.CreateCommand(); + version.CommandText = "SELECT sqlite_version()"; + return (string)version.ExecuteScalar()!; + } + + protected DbCommand Command(string sql, params (string Name, DbType Type)[] parameters) { var command = _connection.CreateCommand(); command.CommandText = sql; foreach (var (name, type) in parameters) { - command.Parameters.Add(name, type); + var parameter = command.CreateParameter(); + parameter.ParameterName = name; + parameter.DbType = type; + command.Parameters.Add(parameter); } _commands.Add(command); return command; } /// Like RETURNING id, but without its cost (3× slower inserts, see experiments/sqlite_returning). - protected long LastInsertRowId => raw.sqlite3_last_insert_rowid(_connection.Handle); + protected long LastInsertRowId => _connection switch + { + SqliteConnection microsoft => raw.sqlite3_last_insert_rowid(microsoft.Handle), + SQLiteConnection system => system.LastInsertRowId, + _ => throw new InvalidOperationException("unknown SQLite provider"), + }; public ulong Count() { diff --git a/csharp/SQLiteVSDoublets/SQLiteVSDoublets.csproj b/csharp/SQLiteVSDoublets/SQLiteVSDoublets.csproj index fd447ae..7aa2830 100644 --- a/csharp/SQLiteVSDoublets/SQLiteVSDoublets.csproj +++ b/csharp/SQLiteVSDoublets/SQLiteVSDoublets.csproj @@ -13,6 +13,7 @@ + diff --git a/experiments/system_data_sqlite/Program.cs b/experiments/system_data_sqlite/Program.cs new file mode 100644 index 0000000..faaa5d0 --- /dev/null +++ b/experiments/system_data_sqlite/Program.cs @@ -0,0 +1,7 @@ +using System.Data.SQLite; + +using var connection = new SQLiteConnection("Data Source=:memory:;Pooling=False"); +connection.Open(); +using var command = connection.CreateCommand(); +command.CommandText = "SELECT sqlite_version()"; +Console.WriteLine($"System.Data.SQLite {typeof(SQLiteConnection).Assembly.GetName().Version}: SQLite {command.ExecuteScalar()}"); diff --git a/experiments/system_data_sqlite/README.md b/experiments/system_data_sqlite/README.md new file mode 100644 index 0000000..ada3239 --- /dev/null +++ b/experiments/system_data_sqlite/README.md @@ -0,0 +1,39 @@ + + +# System.Data.SQLite native library probe + +System.Data.SQLite 2.0.4 contains the managed provider and requires a native +`e_sqlite3` library, as described in the +[upstream build documentation](https://system.data.sqlite.org/home/doc/trunk/www/build.md). +Microsoft.Data.Sqlite 10.0.12 supplies this library for Linux, macOS and Windows. + +From the repository root, reproduce the missing native dependency in a +separate output directory: + +```bash +dotnet run --project experiments/system_data_sqlite -c Release \ + --artifacts-path /tmp/system-data-sqlite-missing-native \ + -p:IncludeNativeSQLite=false +``` + +The command fails with `DllNotFoundException` for `e_sqlite3`. Include the +native library with the default configuration: + +```bash +dotnet run --project experiments/system_data_sqlite -c Release +``` + +This opens one in-memory database and prints the provider and SQLite versions. +The benchmark project uses both providers with the same native library and +validates their results through the same lifecycle. + +After building the C# solution, check the executable's reports for both +categories, both id sizes and both storage types: + +```bash +python3 experiments/system_data_sqlite/check_reports.py +``` + +The check uses 1,000 records and three repetitions per variant, verifies the +reported provider/engine versions, and removes its temporary files. C# CI +runs it on all three operating systems. diff --git a/experiments/system_data_sqlite/check_reports.py b/experiments/system_data_sqlite/check_reports.py new file mode 100644 index 0000000..b562297 --- /dev/null +++ b/experiments/system_data_sqlite/check_reports.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""Check bounded C# provider benchmarks and their JSON reports on every supported OS.""" + +import json +import shutil +# Only this repository's fixed benchmark command is executed. +import subprocess # nosec B404 +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +BENCHMARK = ROOT / "csharp/SQLiteVSDoublets/bin/Release/net10.0/sqlite-vs-doublets.dll" +VARIANTS = ("SQLite_Memory", "SQLite_File", "SystemDataSQLite_Memory", "SystemDataSQLite_File") +OPERATIONS = { + "links": {"create", "query_all", "query_by_id", "query_by_from_to", "query_by_from", "query_by_to", "update", "delete"}, + "objects": {"create", "read_all", "read_by_id", "delete"}, +} + + +class ProviderReportsTests(unittest.TestCase): + """Validate the executable's provider selection, native loading and report output.""" + + def test_reports(self): + """Run both categories and id sizes with memory and file storage for each provider.""" + dotnet = shutil.which("dotnet") + self.assertIsNotNone(dotnet, "dotnet must be installed to run the benchmark") + with tempfile.TemporaryDirectory() as directory: + for category, operations in OPERATIONS.items(): + for bits in (32, 64): + with self.subTest(category=category, bits=bits): + output = Path(directory) / f"{category}-{bits}.json" + # Arguments are constants and owned temporary paths; no shell is used. + subprocess.run([ # nosec B603 + dotnet, str(BENCHMARK), category, str(bits), "1000", + "--repetitions", "3", "--variants", ",".join(VARIANTS), + "--directory", directory, "--output", str(output), + ], check=True) + report = json.loads(output.read_text(encoding="utf-8")) + self.assertEqual( + (report["language"], report["category"], report["bits"], report["size"], report["repetitions"]), + ("C#", category, bits, 1000, 3), + ) + providers = report["sqlite_providers"] + self.assertEqual(set(providers), {"Microsoft.Data.Sqlite", "System.Data.SQLite"}) + self.assertTrue(all(provider["provider_version"] for provider in providers.values())) + self.assertEqual({provider["sqlite_version"] for provider in providers.values()}, {report["sqlite_version"]}) + self.assertEqual([result["variant"] for result in report["results"]], list(VARIANTS)) + for result in report["results"]: + self.assertEqual(set(result["operations"]), operations) + self.assertTrue(all(len(operation["samples_ns"]) == 3 for operation in result["operations"].values())) + self.assertEqual(result["file_bytes"] is None, result["variant"].endswith("_Memory")) + if result["file_bytes"] is not None: + self.assertGreater(result["file_bytes"], 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/experiments/system_data_sqlite/system_data_sqlite.csproj b/experiments/system_data_sqlite/system_data_sqlite.csproj new file mode 100644 index 0000000..f0e637d --- /dev/null +++ b/experiments/system_data_sqlite/system_data_sqlite.csproj @@ -0,0 +1,13 @@ + + + Exe + net10.0 + enable + enable + true + + + + + + diff --git a/scripts/benchmark_report.py b/scripts/benchmark_report.py index 434acd3..7bdc188 100644 --- a/scripts/benchmark_report.py +++ b/scripts/benchmark_report.py @@ -131,9 +131,11 @@ def load(directory): def baseline(variant): - """Doublets are compared with SQLite of the same durability: volatile with memory, non-volatile with file.""" + """Compare with Microsoft.Data.Sqlite of the same durability, including the System.Data.SQLite provider.""" if variant.startswith("SQLite"): return None + if variant.startswith("SystemDataSQLite_"): + return variant.replace("SystemDataSQLite_", "SQLite_", 1) return "SQLite_File" if "_NonVolatile" in variant else "SQLite_Memory" diff --git a/scripts/test_benchmark_report.py b/scripts/test_benchmark_report.py index 080e304..311207d 100644 --- a/scripts/test_benchmark_report.py +++ b/scripts/test_benchmark_report.py @@ -74,6 +74,17 @@ def test_clear_differences(self): self.assertEqual(report.comparison(measurement(2500), measurement(1000), self.text), "2.5× slower") self.assertEqual(report.comparison(measurement(1), measurement(1000), self.text), "1000× faster") + def test_system_data_sqlite_is_compared_with_the_matching_provider_storage(self): + self.assertEqual(report.baseline("SystemDataSQLite_Memory"), "SQLite_Memory") + self.assertEqual(report.baseline("SystemDataSQLite_File"), "SQLite_File") + data = results("objects", { + "SQLite_Memory": 1000, "SQLite_File": 2000, + "SystemDataSQLite_Memory": 500, "SystemDataSQLite_File": 500, + }) + table = report.table(data, self.text).splitlines() + self.assertEqual(table[4].count("2× faster"), 4) + self.assertEqual(table[5].count("4× faster"), 4) + def test_overlapping_interquartile_ranges_are_not_a_difference(self): measured, reference = measurement(150, 90, 100, 150, 200, 300), measurement(190, 150, 180, 190, 210, 260) self.assertEqual(report.quartiles(measured), (100, 200))