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
1 change: 1 addition & 0 deletions src/SharpCoreDB/Database/Core/Database.Core.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public partial class Database : IDatabase, IDisposable, IAsyncDisposable
private QueryPlanCache? planCache; // ✅ Lazy-initialized query plan cache
private SqlParser? _sharedSqlParser; // ✅ Reusable SqlParser for compiled queries
private IGraphRagProvider? _cachedGraphRagProvider; // v2: cached DI resolution (avoids per-call GetService)
private IVectorQueryOptimizer? _cachedVectorQueryOptimizer; // resolved once, same reason

/// <summary>
/// v2 (Native AOT readiness): metadata JSON options. <see cref="AotJsonSerializer.Options"/> resolves the
Expand Down
61 changes: 42 additions & 19 deletions src/SharpCoreDB/Database/Execution/Database.Execution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,47 @@ private SqlParser GetSharedSqlParser()
{
_sharedSqlParser = new SqlParser(tables, _dbPath, storage, isReadOnly, queryCache, config);
_sharedSqlParser.Database = this;
AttachOptionalProviders(_sharedSqlParser);
}

// v2: resolve the optional GRAPH_RAG provider once and cache it for the parser
// lifetime. Previously this DI lookup ran on every ExecuteSQL/ExecuteQuery call.
if (_cachedGraphRagProvider is null)
{
_cachedGraphRagProvider = _serviceProvider.GetService(typeof(IGraphRagProvider)) as IGraphRagProvider;
}
return _sharedSqlParser;
}

if (_cachedGraphRagProvider is not null)
{
_sharedSqlParser.SetGraphRagProvider(_cachedGraphRagProvider);
}
/// <summary>
/// Attaches the optional SQL extensions that live in separate assemblies and are therefore
/// resolved from the service provider instead of being referenced directly: the GRAPH_RAG
/// provider and the vector query optimizer.
/// <para>
/// The vector optimizer used to be attached nowhere at all. <c>SqlParser.VectorQueryOptimizer</c>
/// was an <c>internal static</c> property that no code inside this assembly ever set and no other
/// assembly could set (the type is internal), so both of its consumers were permanent no-ops:
/// <c>CREATE VECTOR INDEX</c> built no index, and the <c>ORDER BY vec_distance_*(...) LIMIT k</c>
/// planner branch was never entered. Resolving <see cref="IVectorQueryOptimizer"/> here — the same
/// way the GRAPH_RAG provider is resolved — is what makes <c>AddVectorSupport()</c> reach the
/// planner. Both lookups are cached on the database, so a parser rebuild costs nothing when the
/// extension is registered and resolves to null only once when it is not.
/// </para>
/// </summary>
/// <param name="parser">The parser instance to configure.</param>
private void AttachOptionalProviders(SqlParser parser)
{
if (_cachedGraphRagProvider is null)
{
_cachedGraphRagProvider = _serviceProvider.GetService(typeof(IGraphRagProvider)) as IGraphRagProvider;
}

return _sharedSqlParser;
if (_cachedGraphRagProvider is not null)
{
parser.SetGraphRagProvider(_cachedGraphRagProvider);
}

_cachedVectorQueryOptimizer ??=
_serviceProvider.GetService(typeof(IVectorQueryOptimizer)) as IVectorQueryOptimizer;

if (_cachedVectorQueryOptimizer is not null)
{
parser.SetVectorQueryOptimizer(_cachedVectorQueryOptimizer);
}
}


Expand Down Expand Up @@ -469,14 +495,11 @@ public List<Dictionary<string, object>> ExecuteCompiledQuery(DataStructures.Prep
return executor.Execute(stmt.CompiledPlan, parameters);
}

// ✅ Fallback: Lazy-initialize shared SqlParser (thread-safe via Interlocked)
if (_sharedSqlParser is null)
{
_sharedSqlParser = new SqlParser(tables, _dbPath, storage, isReadOnly, queryCache, config);
_sharedSqlParser.Database = this;
}

return _sharedSqlParser.ExecuteQuery(stmt.Plan, parameters);
// ✅ Fallback: the shared parser created by GetSharedSqlParser() (which also attaches the
// optional GRAPH_RAG provider and vector optimizer). This block used to build its own
// SqlParser, so a compiled query falling back here got a parser without either extension —
// the second of the two creation sites is exactly how an extension point loses its wiring.
return GetSharedSqlParser().ExecuteQuery(stmt.Plan, parameters);
}

/// <inheritdoc />
Expand Down
31 changes: 26 additions & 5 deletions src/SharpCoreDB/Services/SqlParser.Core.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,33 @@ internal SqlParser(Dictionary<string, ITable> tables, string dbPath, bool isRead
private List<Dictionary<string, object>> _pendingQueryResults = [];

/// <summary>
/// Optional vector query optimizer registered by the VectorSearch module.
/// When set, the query planner detects ORDER BY vec_distance_*() + LIMIT patterns
/// and routes them to a vector index instead of a full table scan.
/// Thread-safe: set once during Database initialization, read from any thread.
/// Optional vector query optimizer supplied by the VectorSearch module.
/// <para>
/// When set, the planner detects <c>ORDER BY vec_distance_*(col, vector) LIMIT k</c> and
/// <c>CREATE VECTOR INDEX</c> routes them to a vector index instead of a full table scan.
/// </para>
/// <para>
/// This is a per-parser instance member, not a static one. It used to be
/// <c>internal static</c>, which made the extension point unreachable in practice: an
/// <c>internal</c> static on an <c>internal</c> class cannot be assigned by another assembly
/// (SharpCoreDB.VectorSearch registers <see cref="IVectorQueryOptimizer"/> with the DI
/// container, it cannot see this type), and no code inside this assembly ever assigned it
/// either. Every call site was therefore a null-conditional no-op, in both directions:
/// <c>CREATE VECTOR INDEX</c> built nothing and <c>ORDER BY vec_distance_*</c> never reached
/// the optimizer. <see cref="Database"/> resolves the registered optimizer from its service
/// provider and attaches it here (see <c>Database.GetSharedSqlParser</c>).
/// </para>
/// </summary>
internal static IVectorQueryOptimizer? VectorQueryOptimizer { get; set; }
internal IVectorQueryOptimizer? VectorQueryOptimizer { get; private set; }

/// <summary>
/// Attaches the optional vector query optimizer for this parser instance.
/// Called by <see cref="Database"/> once, when the shared parser is created, with the
/// <see cref="IVectorQueryOptimizer"/> registered by <c>AddVectorSupport()</c> (if any).
/// </summary>
/// <param name="optimizer">The optimizer to use, or <see langword="null"/> for none.</param>
internal void SetVectorQueryOptimizer(IVectorQueryOptimizer? optimizer)
=> this.VectorQueryOptimizer = optimizer;

/// <inheritdoc />
public void Execute(string sql, IWAL? wal = null)
Expand Down
4 changes: 2 additions & 2 deletions src/SharpCoreDB/Services/SqlParser.DDL.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1157,7 +1157,7 @@ private void ExecuteCreateVectorIndex(string sql, string[] parts, IWAL? wal)

// Phase 5.4: Build live in-memory index if optimizer is registered
// Supports DISKANN via VectorIndexManager / DiskAnnIndex
SqlParser.VectorQueryOptimizer?.BuildIndex(table, tableName, columnName, indexType);
this.VectorQueryOptimizer?.BuildIndex(table, tableName, columnName, indexType);

wal?.Log(sql);
}
Expand Down Expand Up @@ -1191,7 +1191,7 @@ private void ExecuteDropVectorIndex(string sql, string[] parts, IWAL? wal)
table.RemoveMetadata($"vector_index:{colName}:sql");

// Phase 5.4: Drop live in-memory index if optimizer is registered
SqlParser.VectorQueryOptimizer?.DropIndex(tableName, colName);
this.VectorQueryOptimizer?.DropIndex(tableName, colName);

found = true;
break;
Expand Down
Loading
Loading