Skip to content

SqliteVec: support per-connection initialization callbacks - #81

Open
rossdonald wants to merge 1 commit into
CommunityToolkit:mainfrom
rossdonald:sqlitevec-connection-callback
Open

rossdonald wants to merge 1 commit into
CommunityToolkit:mainfrom
rossdonald:sqlitevec-connection-callback

Conversation

@rossdonald

Copy link
Copy Markdown
Contributor

Add an opt-in connection initialization callback to SqliteVectorStoreOptions and SqliteCollectionOptions. If defined, the connector invokes it right after it opens a connection and loads the vec0 extension allowing per-connection options to be set.

Fixes #73

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The store-level path runs the initializer before loading sqlite-vec, causing ListCollectionNamesAsync to fail with pooling disabled when the initializer calls vec_version().

1 open issue
What changed in this PR

Adds opt-in per-connection initialization to SqliteVec, enabling consumers to configure PRAGMAs and other connection settings as requested in #73.

Changes:

  • Adds initializer callbacks to store and collection options, including propagation to store-created collections.
  • Centralizes connection setup and disposal when initialization fails.
  • Adds callback coverage and bumps the provider version to 1.0.4-preview.
File Description
MEVD/​test/​SqliteVec.ConformanceTests/​SqliteConnectionInitializerTests.cs Tests invocation, propagation, extension ordering, and failure cleanup.
MEVD/​src/​SqliteVec/​SqliteVectorStoreOptions.cs Adds and documents the store-level callback.
MEVD/​src/​SqliteVec/​SqliteVectorStore.cs Propagates callbacks and uses shared connection setup.
MEVD/​src/​SqliteVec/​SqliteVec.csproj Updates the provider package version.
MEVD/​src/​SqliteVec/​SqliteConnectionFactory.cs Centralizes connection opening, initialization, and failure cleanup.
MEVD/​src/​SqliteVec/​SqliteCollectionOptions.cs Adds and documents the collection-level callback.
MEVD/​src/​SqliteVec/​SqliteCollection.cs Routes collection connections through the shared factory.

Review effort: Balanced

using var connection = await SqliteConnectionFactory.OpenAsync(
_connectionString,
_connectionInitializer,
loadVector: false,

@adamsitnik adamsitnik left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rossdonald big thanks for your contribution!

The changes look good, but I think we should rename the callback and make it async, so we don't call sync code from async method. PTAL at my comments.

Similar public API exposed by BCL: https://learn.microsoft.com/en-us/dotnet/api/system.net.http.socketshttphandler.connectcallback?view=net-10.0

/// <c>PRAGMA synchronous=NORMAL</c>, <c>PRAGMA cache_size</c> or <c>PRAGMA temp_store=MEMORY</c>, or for loading additional extensions.
/// </para>
/// <para>
/// When the collection is created from a <see cref="SqliteVectorStore"/>, the callback configured on the vector store options

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for documenting this important information 👍

connection.LoadVector();
}

initializer?.Invoke(connection);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The callback is synchronous. Are there any SqliteConnection properties that require calling async methods to configure? I just want to be 100% sure the callback does not need to return a Task/ValueTask in order to avoid calling async methods from sync callback.


public void Dispose()
{
File.Delete(_databasePath);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you for taking care of the cleanup 👍


using var setCommand = connection.CreateCommand();
setCommand.CommandText = "PRAGMA synchronous=NORMAL";
setCommand.ExecuteNonQuery();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is the typical usage, we are calling sync method from an async context. How about making the callback async and accepting a CancellationToken?

public Func<SqliteConnection, CancellationToken, ValueTask> ConnectionInitializer { get; set; }

/// thread-safe.
/// </para>
/// </remarks>
public Action<SqliteConnection>? ConnectionInitializer { get; set; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked for some "prior art" in ASP.NET and EF Core and BCL and found "ConfigureHttpClient", "ConfigureEndpointDefaults", "ConfigureDataSource" and I think that "ConfigureConnection" would be better.

Suggested change
public Action<SqliteConnection>? ConnectionInitializer { get; set; }
public Action<SqliteConnection>? ConfigureConnection { get; set; }

connection.LoadVector();
}

initializer?.Invoke(connection);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With pooling on (the Microsoft.Data.Sqlite default), this runs again every time a pooled physical connection is reused, and what it sets stays on that connection. The tests use Pooling=false, so they don't cover it. Two cases that fail on this branch with the default connection string and pass with Pooling=false:

  • an ATTACH DATABASE initializer fails on the second operation: SQLite Error 1: 'database aux is already in use';
  • PRAGMA query_only=ON on one collection leaks to another collection with the same connection string: SQLite Error 8: 'attempt to write a readonly database'.

Maybe document that the callback must be safe to run more than once on the same connection, and add a test with pooling on?

Repro tests
[Fact]
public async Task AttachInitializerWorksOnEveryOperationWithPooling()
{
    var options = new SqliteCollectionOptions
    {
        ConnectionInitializer = connection =>
        {
            using var command = connection.CreateCommand();
            command.CommandText = $"ATTACH DATABASE '{_auxPath}' AS aux";
            command.ExecuteNonQuery();
        }
    };

    var collection = new SqliteCollection<string, ProbeRecord>(ConnectionString, "AttachCollection", options);

    await collection.EnsureCollectionExistsAsync();
    await collection.UpsertAsync(new ProbeRecord { Key = "k1", Text = "hello", Vector = new([1f, 2f, 3f, 4f]) });
}

[Fact]
public async Task QueryOnlyFromOneCollectionDoesNotLeakToAnotherWithPooling()
{
    var readOnly = new SqliteCollection<string, ProbeRecord>(ConnectionString, "ReadOnlyCollection", new SqliteCollectionOptions
    {
        ConnectionInitializer = connection =>
        {
            using var command = connection.CreateCommand();
            command.CommandText = "PRAGMA query_only=ON";
            command.ExecuteNonQuery();
        }
    });

    var writable = new SqliteCollection<string, ProbeRecord>(ConnectionString, "WritableCollection");
    await writable.EnsureCollectionExistsAsync();

    Assert.False(await readOnly.CollectionExistsAsync());

    await writable.UpsertAsync(new ProbeRecord { Key = "k1", Text = "hello", Vector = new([1f, 2f, 3f, 4f]) });
}

ConnectionString is $"Data Source={_databasePath}" (pooling on), and _auxPath is a second temp file. ProbeRecord has a string key, a Text data property and a 4-dimension vector.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SqliteVec: Add method to allow setting pragmas per connection

4 participants