Summary
This is a follow-up to the
WHERE col IN (...) regression reported against V1.9.5.
That issue was tentatively marked as fixed when SharpCoreDb 1.9.6 shipped
on 2026-08, but a verification probe run against the published 1.9.6 package
on 2026-08-29 shows the regression is still present in every form that
matters for real applications.
The parser still consumes the IN keyword without applying any filter for
multi-value lists. The single-element form WHERE col IN (@p0) happens to
return the correct count, but that form is functionally identical to
WHERE col = @p0 — it does not indicate a fix.
Reproduction (single file, .NET 10, standalone)
// dotnet new console -n ProbeIn196 --framework net10.0
// cd ProbeIn196 && dotnet add package SharpCoreDB.EntityFrameworkCore --version 1.9.6
// then paste the code below into Program.cs and run `dotnet run -c Release`
using System;
using System.IO;
using Microsoft.Extensions.DependencyInjection;
using SharpCoreDB.EntityFrameworkCore.Storage;
var dbPath = Path.Combine(Path.GetTempPath(), "probe-in-196.scdb");
if (File.Exists(dbPath)) File.Delete(dbPath);
var sp = new ServiceCollection()
.AddSingleton($"DataSource={dbPath};Password=test;")
.AddSingleton<SharpCoreDB.DatabaseFactory>()
.BuildServiceProvider();
using var conn = new SharpCoreDBConnection(sp, $"DataSource={dbPath};Password=test;");
conn.Open();
void Exec(string sql, params (string name, object value)[] ps)
{
using var c = conn.CreateCommand();
c.CommandText = sql;
foreach (var (n, v) in ps) { var p = c.CreateParameter(); p.ParameterName = n; p.Value = v; c.Parameters.Add(p); }
c.ExecuteNonQuery();
}
void Query(string label, string expected, string sql, params (string name, object value)[] ps)
{
using var c = conn.CreateCommand();
c.CommandText = sql;
foreach (var (n, v) in ps) { var p = c.CreateParameter(); p.ParameterName = n; p.Value = v; c.Parameters.Add(p); }
using var r = c.ExecuteReader();
int rows = 0; while (r.Read()) rows++;
Console.WriteLine($"{label,-60} rows={rows} (expected {expected})");
}
Exec("""
CREATE TABLE kg_nodes_test (
id TEXT PRIMARY KEY,
node_type TEXT NOT NULL,
external_id TEXT NOT NULL
)
""");
foreach (var node in new[] { ("A", "WorkItem", "WI-1"), ("B", "WorkItem", "WI-2"), ("C", "Person", "P-1") })
Exec("INSERT INTO kg_nodes_test (id, node_type, external_id) VALUES (@id, @nt, @ei)",
("@id", node.Item1), ("@nt", node.Item2), ("@ei", node.Item3));
Query("control -> WHERE node_type = @p0", "2", "SELECT id FROM kg_nodes_test WHERE node_type = @p0", ("@p0", "WorkItem"));
Query("expected 2 -> WHERE node_type IN (@p0)", "2", "SELECT id FROM kg_nodes_test WHERE node_type IN (@p0)", ("@p0", "WorkItem"));
Query("expected 2 -> WHERE node_type IN (@p0, @p1)", "2", "SELECT id FROM kg_nodes_test WHERE node_type IN (@p0, @p1)", ("@p0", "WorkItem"), ("@p1", "Person"));
Query("expected 2 -> WHERE node_type IN (VALUES (@p0))", "2", "SELECT id FROM kg_nodes_test WHERE node_type IN (VALUES (@p0))", ("@p0", "WorkItem"));
Query("expected 2 -> WHERE node_type IN (VALUES (@p0), (@p1))", "2", "SELECT id FROM kg_nodes_test WHERE node_type IN (VALUES (@p0), (@p1))", ("@p0", "WorkItem"), ("@p1", "Person"));
Query("expected 1 -> WHERE (node_type, external_id) IN (VALUES (@nt, @ei))", "1", "SELECT id FROM kg_nodes_test WHERE (node_type, external_id) IN (VALUES (@nt, @ei))", ("@nt", "WorkItem"), ("@ei", "WI-1"));
using (var c = conn.CreateCommand())
{
c.CommandText = "DELETE FROM kg_nodes_test WHERE node_type = @p0";
var p = c.CreateParameter(); p.ParameterName = "@p0"; p.Value = "WorkItem"; c.Parameters.Add(p);
var affected = c.ExecuteNonQuery();
Console.WriteLine($"DELETE 2 rows by node_type='WorkItem' affected={affected} (expected 2)");
}
Expected output (matches 1.9.3 / 1.9.4)
control -> WHERE node_type = @p0 rows=2 (expected 2)
expected 2 -> WHERE node_type IN (@p0) rows=2 (expected 2)
expected 2 -> WHERE node_type IN (@p0, @p1) rows=2 (expected 2)
expected 2 -> WHERE node_type IN (VALUES (@p0)) rows=2 (expected 2)
expected 2 -> WHERE node_type IN (VALUES (@p0), (@p1)) rows=2 (expected 2)
expected 1 -> WHERE (node_type, external_id) IN (VALUES (@nt, @ei)) rows=1 (expected 1)
DELETE 2 rows by node_type='WorkItem' affected=2 (expected 2)
Actual output (1.9.6 — verified 2026-08-29)
control -> WHERE node_type = @p0 rows=2 (expected 2)
expected 2 -> WHERE node_type IN (@p0) rows=2 (expected 2)
expected 2 -> WHERE node_type IN (@p0, @p1) rows=3 (expected 2) ← REGRESSION
expected 2 -> WHERE node_type IN (VALUES (@p0)) rows=0 (expected 2) ← REGRESSION (new failure mode)
expected 2 -> WHERE node_type IN (VALUES (@p0), (@p1)) rows=0 (expected 2) ← REGRESSION (new failure mode)
expected 1 -> WHERE (node_type, external_id) IN (VALUES (@nt, @ei)) rows=0 (expected 1) ← REGRESSION (new failure mode)
DELETE 2 rows by node_type='WorkItem' affected=1 (expected 2) ← REGRESSION
Comparison vs. 1.9.5
| Form |
1.9.5 |
1.9.6 |
Verdict |
WHERE col = @p0 |
✅ 2 |
✅ 2 |
unchanged |
WHERE col IN (@p0) (1 param) |
❌ 3 (all rows) |
✅ 2 |
looks fixed but is functionally = |
WHERE col IN (@p0, @p1) (2 params) |
❌ 3 |
❌ 3 (all rows) |
still broken |
WHERE col IN (VALUES (@p0)) |
❌ 3 |
❌ 0 |
regressed differently — now returns 0 instead of all rows |
WHERE col IN (VALUES (@p0), (@p1)) |
❌ 3 |
❌ 0 |
regressed differently |
(col1, col2) IN (VALUES (@nt, @ei)) (tuple) |
❌ 3 |
❌ 0 |
regressed differently |
WHERE col = @p0 OR col = @p1 |
❌ 0 |
❌ 0 |
unchanged (still 0) |
DELETE ... WHERE ... ExecuteNonQuery affected count |
❌ always 1 |
❌ always 1 |
unchanged |
In other words, 1.9.6 made the IN (VALUES (@p0)) family of forms worse —
they now return 0 rows instead of all rows. The IN (@p0, @p1) family still
returns all rows. The behavior is inconsistent across the multi-value IN
shapes.
Impact
Forge (a .NET 10 / C# 14 application with a Knowledge Graph store backed by
SharpCoreDB) cannot use any batched IN lookup — every multi-value batch
becomes an O(N) per-row round trip, with hundreds of single-column =
queries per call for typical workloads. Our test suite currently passes
110 / 119 tests on 1.9.6 (identical to 1.9.5); the 9 unimprovable cases are
either blocked by this regression or by the persistent OR /
ExecuteNonQuery issues that were already filed in the original report.
We are blocked from:
- Reverting the per-row SELECT workarounds in
SharpCoreDbKnowledgeGraphStore.
- Implementing a join-based
RemoveIncidentEdges optimization that needs
WHERE source IN (...) OR target IN (...) to filter.
- Implementing a
GraphSearch query API that uses tuple-IN to match
composite node keys.
Suggested fix direction
- The 1.9.6 parser seems to have a partial fix in place for
IN (@p0) (single-element, equivalent to =). It's likely that the
IN-handling path was rewritten to consume the comma-separated list as
if it were a sequence of assignments (e.g., @p0, @p1, …) and the
single-element case happens to leave one of those assignments intact. The
multi-element case is then either silently skipping the predicate
(IN (@p0, @p1)) or stopping early when it encounters the second
VALUES row (IN (VALUES (@p0), (@p1))).
- Add a regression test that round-trips
IN queries with parameterized
lists of size 1, 2, 5, and 10, both literal and VALUES-shaped, and
asserts the row count matches the filter.
- While in the same area, please also address the persistent
OR and
ExecuteNonQuery DELETE-affected-count issues from V1.9.4 — they remain
in V1.9.6 unchanged.
Environment
SharpCoreDB 1.9.6 (NuGet, released 2026-08)
SharpCoreDB.EntityFrameworkCore 1.9.6
SharpCoreDB.Graph 1.9.6 and SharpCoreDB.Graph.Advanced 1.9.6
- .NET 10 / C# 14, x64, Windows
- Verified on a clean standalone probe. Behavior
reproducible with the snippet above in ~10 seconds.
Summary
This is a follow-up to the
WHERE col IN (...)regression reported against V1.9.5.That issue was tentatively marked as fixed when SharpCoreDb 1.9.6 shipped
on 2026-08, but a verification probe run against the published 1.9.6 package
on 2026-08-29 shows the regression is still present in every form that
matters for real applications.
The parser still consumes the
INkeyword without applying any filter formulti-value lists. The single-element form
WHERE col IN (@p0)happens toreturn the correct count, but that form is functionally identical to
WHERE col = @p0— it does not indicate a fix.Reproduction (single file, .NET 10, standalone)
Expected output (matches 1.9.3 / 1.9.4)
Actual output (1.9.6 — verified 2026-08-29)
Comparison vs. 1.9.5
WHERE col = @p0WHERE col IN (@p0)(1 param)=WHERE col IN (@p0, @p1)(2 params)WHERE col IN (VALUES (@p0))WHERE col IN (VALUES (@p0), (@p1))(col1, col2) IN (VALUES (@nt, @ei))(tuple)WHERE col = @p0 OR col = @p1DELETE ... WHERE ...ExecuteNonQueryaffected countIn other words, 1.9.6 made the
IN (VALUES (@p0))family of forms worse —they now return 0 rows instead of all rows. The
IN (@p0, @p1)family stillreturns all rows. The behavior is inconsistent across the multi-value
INshapes.
Impact
Forge (a .NET 10 / C# 14 application with a Knowledge Graph store backed by
SharpCoreDB) cannot use any batched
INlookup — every multi-value batchbecomes an
O(N)per-row round trip, with hundreds of single-column=queries per call for typical workloads. Our test suite currently passes
110 / 119 tests on 1.9.6 (identical to 1.9.5); the 9 unimprovable cases are
either blocked by this regression or by the persistent
OR/ExecuteNonQueryissues that were already filed in the original report.We are blocked from:
SharpCoreDbKnowledgeGraphStore.RemoveIncidentEdgesoptimization that needsWHERE source IN (...) OR target IN (...)to filter.GraphSearchquery API that uses tuple-INto matchcomposite node keys.
Suggested fix direction
IN (@p0)(single-element, equivalent to=). It's likely that theIN-handling path was rewritten to consume the comma-separated list asif it were a sequence of assignments (e.g.,
@p0,@p1, …) and thesingle-element case happens to leave one of those assignments intact. The
multi-element case is then either silently skipping the predicate
(
IN (@p0, @p1)) or stopping early when it encounters the secondVALUESrow (IN (VALUES (@p0), (@p1))).INqueries with parameterizedlists of size 1, 2, 5, and 10, both literal and
VALUES-shaped, andasserts the row count matches the filter.
ORandExecuteNonQueryDELETE-affected-count issues from V1.9.4 — they remainin V1.9.6 unchanged.
Environment
SharpCoreDB1.9.6 (NuGet, released 2026-08)SharpCoreDB.EntityFrameworkCore1.9.6SharpCoreDB.Graph1.9.6 andSharpCoreDB.Graph.Advanced1.9.6reproducible with the snippet above in ~10 seconds.