Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: ADO.NET database configuration
description: Find Orleans ADO.NET schema scripts and provider invariants.
ms.date: 08/02/2026
ms.date: 10/02/2026
ms.topic: reference
---

Expand Down Expand Up @@ -44,6 +44,9 @@ Orleans keeps its ADO.NET schema scripts beside each provider's source. Run the
- [Oracle](https://github.com/dotnet/orleans/blob/main/src/AdoNet/Orleans.Persistence.AdoNet/Oracle-Persistence.sql)
- [SQLite](https://github.com/dotnet/orleans/blob/main/src/AdoNet/Orleans.Persistence.AdoNet/Sqlite-Persistence.sql)

The SQLite scripts are idempotent. Reapplying them refreshes persistence queries
while preserving grain state.

## Reminders

- [SQL Server](https://github.com/dotnet/orleans/blob/main/src/AdoNet/Orleans.Reminders.AdoNet/SQLServer-Reminders.sql)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,3 @@
<InternalsVisibleTo Include="Orleans.AdoNet.Tests" />
</ItemGroup>
</Project>

6 changes: 5 additions & 1 deletion src/AdoNet/Orleans.Persistence.AdoNet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,11 @@ Before using the ADO.NET provider, you need to set up the necessary database tab
- [MySQL Scripts](https://github.com/dotnet/orleans/tree/main/src/AdoNet/Orleans.Persistence.AdoNet/MySQL-Persistence.sql)
- [PostgreSQL Scripts](https://github.com/dotnet/orleans/tree/main/src/AdoNet/Orleans.Persistence.AdoNet/PostgreSQL-Persistence.sql)
- [Oracle Scripts](https://github.com/dotnet/orleans/tree/main/src/AdoNet/Orleans.Persistence.AdoNet/Oracle-Persistence.sql)
- [SQLite Scripts](https://github.com/dotnet/orleans/tree/main/src/AdoNet/Orleans.Persistence.AdoNet/Sqlite-Persistence.sql)
- [SQLite Main Script](https://github.com/dotnet/orleans/blob/main/src/AdoNet/Shared/Sqlite-Main.sql)
- [SQLite Persistence Script](https://github.com/dotnet/orleans/blob/main/src/AdoNet/Orleans.Persistence.AdoNet/Sqlite-Persistence.sql)

The SQLite scripts are idempotent: reapplying them refreshes the persistence
queries while preserving grain state.

## Documentation
For more comprehensive documentation, please refer to:
Expand Down
99 changes: 60 additions & 39 deletions src/AdoNet/Orleans.Persistence.AdoNet/Sqlite-Persistence.sql
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
--
-- 7. In the storage operations queries the columns need to be in the exact same order
-- since the storage table operations support optionally streaming.
CREATE TABLE OrleansStorage
CREATE TABLE IF NOT EXISTS OrleansStorage
(
-- These are for the book keeping. Orleans calculates
-- these hashes (see RelationalStorageProvide implementation),
Expand Down Expand Up @@ -73,56 +73,75 @@ CREATE TABLE OrleansStorage
-- rows down to [0, n] relevant ones, n being the number of collided value pairs.
);

CREATE INDEX IX_OrleansStorage ON OrleansStorage(GrainIdHash, GrainTypeHash);
CREATE INDEX IF NOT EXISTS IX_OrleansStorage ON OrleansStorage(GrainIdHash, GrainTypeHash);


-- Updates an existing grain state with optimistic concurrency control or inserts it if it does not exist.
INSERT INTO OrleansQuery (QueryKey, QueryText) VALUES
('WriteToStorageKey', '
BEGIN TRANSACTION;

CREATE TEMP TABLE IF NOT EXISTS OrleansStorageWriteState
-- TEMP objects survive pooled connection reuse. Bump the suffix on both object names whenever the trigger body changes.
CREATE TEMP TABLE IF NOT EXISTS OrleansStorageWriteRequest_10_4
(
TotalChangesBefore INT NOT NULL
GrainIdHash INT NOT NULL,
GrainIdN0 BIGINT NOT NULL,
GrainIdN1 BIGINT NOT NULL,
GrainTypeHash INT NOT NULL,
GrainTypeString NVARCHAR(512) NOT NULL,
GrainIdExtensionString NVARCHAR(512) NULL,
ServiceId NVARCHAR(150) NOT NULL,
PayloadBinary BLOB NULL,
GrainStateVersion INT NULL,
Applied INT NOT NULL DEFAULT 0
);
DELETE FROM OrleansStorageWriteState;
INSERT INTO OrleansStorageWriteState (TotalChangesBefore) VALUES (total_changes() + 1);

UPDATE OrleansStorage
SET
PayloadBinary = @PayloadBinary,
ModifiedOn = datetime(''now''),
Version = Version + 1
WHERE
GrainIdHash = @GrainIdHash AND GrainTypeHash = @GrainTypeHash
AND GrainIdN0 = @GrainIdN0 AND GrainIdN1 = @GrainIdN1
AND GrainTypeString = @GrainTypeString
AND (GrainIdExtensionString = @GrainIdExtensionString OR (GrainIdExtensionString IS NULL AND @GrainIdExtensionString IS NULL))
AND ServiceId = @ServiceId
AND Version = @GrainStateVersion;
CREATE TEMP TRIGGER IF NOT EXISTS OrleansStorageWriteApply_10_4
AFTER INSERT ON OrleansStorageWriteRequest_10_4
BEGIN
UPDATE OrleansStorage
SET
PayloadBinary = NEW.PayloadBinary,
ModifiedOn = datetime(''now''),
Version = Version + 1
WHERE
GrainIdHash = NEW.GrainIdHash AND GrainTypeHash = NEW.GrainTypeHash
AND GrainIdN0 = NEW.GrainIdN0 AND GrainIdN1 = NEW.GrainIdN1
AND GrainTypeString = NEW.GrainTypeString
AND (GrainIdExtensionString = NEW.GrainIdExtensionString OR (GrainIdExtensionString IS NULL AND NEW.GrainIdExtensionString IS NULL))
AND ServiceId = NEW.ServiceId
AND Version = NEW.GrainStateVersion;

INSERT INTO OrleansStorage (GrainIdHash, GrainIdN0, GrainIdN1, GrainTypeHash, GrainTypeString, GrainIdExtensionString, ServiceId, PayloadBinary, ModifiedOn, Version)
SELECT @GrainIdHash, @GrainIdN0, @GrainIdN1, @GrainTypeHash, @GrainTypeString, @GrainIdExtensionString, @ServiceId, @PayloadBinary, datetime(''now''), 1
WHERE changes() = 0
AND @GrainStateVersion IS NULL
AND NOT EXISTS (
SELECT 1 FROM OrleansStorage
WHERE GrainIdHash = @GrainIdHash AND GrainTypeHash = @GrainTypeHash
AND GrainIdN0 = @GrainIdN0 AND GrainIdN1 = @GrainIdN1
AND GrainTypeString = @GrainTypeString
AND (GrainIdExtensionString = @GrainIdExtensionString OR (GrainIdExtensionString IS NULL AND @GrainIdExtensionString IS NULL))
AND ServiceId = @ServiceId
);
UPDATE OrleansStorageWriteRequest_10_4 SET Applied = changes() WHERE rowid = NEW.rowid;

INSERT INTO OrleansStorage (GrainIdHash, GrainIdN0, GrainIdN1, GrainTypeHash, GrainTypeString, GrainIdExtensionString, ServiceId, PayloadBinary, ModifiedOn, Version)
SELECT NEW.GrainIdHash, NEW.GrainIdN0, NEW.GrainIdN1, NEW.GrainTypeHash, NEW.GrainTypeString, NEW.GrainIdExtensionString, NEW.ServiceId, NEW.PayloadBinary, datetime(''now''), 1
WHERE NEW.GrainStateVersion IS NULL
AND NOT EXISTS (
SELECT 1 FROM OrleansStorage
WHERE GrainIdHash = NEW.GrainIdHash AND GrainTypeHash = NEW.GrainTypeHash
AND GrainIdN0 = NEW.GrainIdN0 AND GrainIdN1 = NEW.GrainIdN1
AND GrainTypeString = NEW.GrainTypeString
AND (GrainIdExtensionString = NEW.GrainIdExtensionString OR (GrainIdExtensionString IS NULL AND NEW.GrainIdExtensionString IS NULL))
AND ServiceId = NEW.ServiceId
);

UPDATE OrleansStorageWriteRequest_10_4 SET Applied = Applied + changes(), PayloadBinary = NULL WHERE rowid = NEW.rowid;
END;

DELETE FROM OrleansStorageWriteRequest_10_4;

INSERT INTO OrleansStorageWriteRequest_10_4 (GrainIdHash, GrainIdN0, GrainIdN1, GrainTypeHash, GrainTypeString, GrainIdExtensionString, ServiceId, PayloadBinary, GrainStateVersion)
VALUES (@GrainIdHash, @GrainIdN0, @GrainIdN1, @GrainTypeHash, @GrainTypeString, @GrainIdExtensionString, @ServiceId, @PayloadBinary, @GrainStateVersion);

SELECT CASE WHEN @GrainStateVersion IS NULL THEN 1 ELSE @GrainStateVersion + 1 END AS NewGrainStateVersion
WHERE total_changes() > (SELECT TotalChangesBefore FROM OrleansStorageWriteState LIMIT 1);
FROM OrleansStorageWriteRequest_10_4
WHERE Applied > 0;

SELECT @GrainStateVersion AS NewGrainStateVersion
WHERE total_changes() = (SELECT TotalChangesBefore FROM OrleansStorageWriteState LIMIT 1)
FROM OrleansStorageWriteRequest_10_4
WHERE Applied = 0
AND @GrainStateVersion IS NOT NULL;

COMMIT;
');
')
ON CONFLICT(QueryKey) DO UPDATE SET QueryText = excluded.QueryText;

-- Retrieves the binary payload and the current version of a specific grain state.
INSERT INTO OrleansQuery (QueryKey, QueryText) VALUES
Expand All @@ -139,7 +158,8 @@ INSERT INTO OrleansQuery (QueryKey, QueryText) VALUES
AND (GrainIdExtensionString = @GrainIdExtensionString OR (GrainIdExtensionString IS NULL AND @GrainIdExtensionString IS NULL))
AND ServiceId = @ServiceId
LIMIT 1;
');
')
ON CONFLICT(QueryKey) DO UPDATE SET QueryText = excluded.QueryText;

-- Clears the grain state by setting the payload to null and incrementing the version for consistency.
INSERT INTO OrleansQuery (QueryKey, QueryText) VALUES
Expand All @@ -163,4 +183,5 @@ INSERT INTO OrleansQuery (QueryKey, QueryText) VALUES
SELECT @GrainStateVersion AS NewGrainStateVersion
WHERE changes() = 0
AND @GrainStateVersion IS NOT NULL;
');
')
ON CONFLICT(QueryKey) DO UPDATE SET QueryText = excluded.QueryText;
2 changes: 1 addition & 1 deletion src/AdoNet/Shared/Sqlite-Main.sql
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Implementation notes:

-- These settings improves throughput of the database by reducing locking by better separating readers from writers.
-- SQL Server 2012 and newer can refer to itself as CURRENT. Older ones need a workaround.
CREATE TABLE OrleansQuery
CREATE TABLE IF NOT EXISTS OrleansQuery
(
QueryKey TEXT PRIMARY KEY,
QueryText TEXT NOT NULL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,147 @@ public sealed class SqlitePersistenceQueryTests : IDisposable
public SqlitePersistenceQueryTests()
{
_connection.Open();
InitializeDatabase(_connection);
}

[Fact]
public void FailedWriteDoesNotPoisonPooledConnection()
{
var databasePath = Path.Combine(Path.GetTempPath(), $"orleans-sqlite-write-failure-{Guid.NewGuid():N}.db");
var connectionString = new SqliteConnectionStringBuilder
{
DataSource = databasePath,
DefaultTimeout = 1,
Pooling = true
}.ToString();
var independentConnectionString = new SqliteConnectionStringBuilder
{
DataSource = databasePath,
DefaultTimeout = 1,
Pooling = false
}.ToString();

try
{
using (var connection = new SqliteConnection(connectionString))
{
connection.Open();
InitializeDatabase(connection);
Assert.Equal([1], ExecuteQuery(connection, "WriteToStorageKey", null));
}

using (var blocker = new SqliteConnection(independentConnectionString))
{
blocker.Open();
using (var command = blocker.CreateCommand())
{
command.CommandText = "BEGIN IMMEDIATE;";
command.ExecuteNonQuery();
}

using (var connection = new SqliteConnection(connectionString))
{
connection.Open();
var exception = Assert.Throws<SqliteException>(() => ExecuteQuery(connection, "WriteToStorageKey", 1));
Assert.Equal(5, exception.SqliteErrorCode);
using var begin = connection.CreateCommand();
begin.CommandText = "BEGIN; ROLLBACK;";
begin.ExecuteNonQuery();
}

using var rollback = blocker.CreateCommand();
rollback.CommandText = "ROLLBACK;";
rollback.ExecuteNonQuery();
}

using (var connection = new SqliteConnection(connectionString))
{
connection.Open();
using (var begin = connection.CreateCommand())
{
begin.CommandText = "BEGIN; ROLLBACK;";
begin.ExecuteNonQuery();
}

Assert.Equal([2], ExecuteQuery(connection, "ClearStorageKey", 1));
}

AssertStoredState(independentConnectionString, 2, null);

using (var connection = new SqliteConnection(connectionString))
{
connection.Open();
Assert.Equal([3], ExecuteQuery(connection, "WriteToStorageKey", 2));
}

AssertStoredState(independentConnectionString, 3, "updated");
}
finally
{
using var connection = new SqliteConnection(connectionString);
SqliteConnection.ClearPool(connection);
File.Delete(databasePath);
}
}

[Fact]
public void ConflictAfterSuccessfulWritePreservesVersionProgression()
{
Assert.Equal([1], ExecuteQuery("WriteToStorageKey", null));
Assert.Equal([2], ExecuteQuery("WriteToStorageKey", 1));
Assert.Equal([99], ExecuteQuery("WriteToStorageKey", 99));
Assert.Equal([3], ExecuteQuery("WriteToStorageKey", 2));
Assert.Equal([99], ExecuteQuery("WriteToStorageKey", 99));
Assert.Equal([(3, "updated")], ReadRows());
}

[Fact]
public void ReapplyingScriptsInstallsCurrentQueriesAndPreservesState()
{
var expected = new Dictionary<string, string>
{
["WriteToStorageKey"] = GetQuery("WriteToStorageKey"),
["ReadFromStorageKey"] = GetQuery("ReadFromStorageKey"),
["ClearStorageKey"] = GetQuery("ClearStorageKey")
};
InsertRow(7);

using (var command = _connection.CreateCommand())
{
command.CommandText = """
UPDATE OrleansQuery
SET QueryText = 'outdated'
WHERE QueryKey IN ('WriteToStorageKey', 'ReadFromStorageKey', 'ClearStorageKey');
""";
Assert.Equal(3, command.ExecuteNonQuery());
}

InitializeDatabase(_connection);

Assert.Equal(expected["WriteToStorageKey"], GetQuery("WriteToStorageKey"));
Assert.Equal(expected["ReadFromStorageKey"], GetQuery("ReadFromStorageKey"));
Assert.Equal(expected["ClearStorageKey"], GetQuery("ClearStorageKey"));
Assert.Equal([(7, "original")], ReadRows());
}

private static void AssertStoredState(string connectionString, int version, string? payload)
{
using var connection = new SqliteConnection(connectionString);
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = "SELECT Version, CAST(PayloadBinary AS TEXT) FROM OrleansStorage";
using var reader = command.ExecuteReader();
Assert.True(reader.Read());
Assert.Equal(version, reader.GetInt32(0));
Assert.Equal(payload, reader.IsDBNull(1) ? null : reader.GetString(1));
Assert.False(reader.Read());
}

private static void InitializeDatabase(SqliteConnection connection)
{
foreach (var script in new[] { "Sqlite-Main.sql", "Sqlite-Persistence.sql" })
{
using var command = _connection.CreateCommand();
using var command = connection.CreateCommand();
command.CommandText = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, script));
command.ExecuteNonQuery();
}
Expand Down Expand Up @@ -142,13 +280,12 @@ public void HashCollisionPreservesOtherIdentity(string queryKey, string identity
}

private List<int> ExecuteQuery(string queryKey, int? version, string? extension = null)
{
using var lookup = _connection.CreateCommand();
lookup.CommandText = "SELECT QueryText FROM OrleansQuery WHERE QueryKey = @QueryKey";
lookup.Parameters.AddWithValue("QueryKey", queryKey);
=> ExecuteQuery(_connection, queryKey, version, extension);

using var command = _connection.CreateCommand();
command.CommandText = Assert.IsType<string>(lookup.ExecuteScalar());
private static List<int> ExecuteQuery(SqliteConnection connection, string queryKey, int? version, string? extension = null)
{
using var command = connection.CreateCommand();
command.CommandText = GetQuery(connection, queryKey);
AddParameters(command, version, extension);

using var reader = command.ExecuteReader();
Expand All @@ -167,6 +304,16 @@ private List<int> ExecuteQuery(string queryKey, int? version, string? extension
return versions;
}

private string GetQuery(string queryKey) => GetQuery(_connection, queryKey);

private static string GetQuery(SqliteConnection connection, string queryKey)
{
using var command = connection.CreateCommand();
command.CommandText = "SELECT QueryText FROM OrleansQuery WHERE QueryKey = @QueryKey";
command.Parameters.AddWithValue("QueryKey", queryKey);
return Assert.IsType<string>(command.ExecuteScalar());
}

private void InsertRow(int? version, string? extension = null, string? differentIdentityColumn = null)
{
using var command = _connection.CreateCommand();
Expand Down
Loading