Skip to content

[Still open in 1.9.8] Multi-value WHERE IN (...) and OR predicates still return wrong results — 1.9.7 only fixed part of the regression #348

Description

@YBazanPro

Summary

The 1.9.7 release notes claimed the WHERE ... IN (...) regression
(introduced in 1.9.5) is "fully resolved":

IN / NOT IN are now evaluated correctly in every SQL shape, including
the forms reported against the 1.9.6 package: multi-value lists
IN (@p0, @p1), SQLite VALUES forms IN (VALUES (@p0), (@p1)),
composite-key tuple rows (a, b) IN (VALUES (…)), and OR-chained
predicates in single-file (.scdb) mode.

A fresh standalone probe run against the published 1.9.7 package shows that
only two of those four forms are actually fixed (single-row VALUES and
tuple-IN). The DELETE-affected-count fix is also confirmed working. But:

  • IN (@p0, @p1) (multi-value parameter list) — still returns all rows
  • IN (VALUES (@p0), (@p1)) (multi-row VALUES) — still returns all rows
  • OR-chained predicates — now return all rows (were 0 rows in 1.9.4-1.9.6)

The 1.9.8.0 release shipped with no SQL parser changes (only block-level
Brotli/GZip compression and a Unicode regression test), so all of the above
are still broken in 1.9.8. This report covers 1.9.8 specifically; the
probe output for 1.9.7 was identical and is included in the comparison
table below.

Reproduction (single file, .NET 10, standalone — no library internals referenced)

// dotnet new console -n ProbeIn198 --framework net10.0// cd ProbeIn198 && dotnet add package SharpCoreDB.EntityFrameworkCore --version 1.9.8// then paste the code below into Program.cs and run `dotnet run -c Release`usingSystem;usingSystem.IO;usingMicrosoft.Extensions.DependencyInjection;usingSharpCoreDB.EntityFrameworkCore.Storage;vardbPath=Path.Combine(Path.GetTempPath(),"probe-in-198.scdb");if(File.Exists(dbPath))File.Delete(dbPath);varsp=newServiceCollection().AddSingleton($"DataSource={dbPath};Password=test;").AddSingleton<SharpCoreDB.DatabaseFactory>().BuildServiceProvider();usingvarconn=newSharpCoreDBConnection(sp,$"DataSource={dbPath};Password=test;");conn.Open();voidExec(stringsql,params(stringname,objectvalue)[]ps){usingvarc=conn.CreateCommand();c.CommandText=sql;foreach(var(n,v)inps){varp=c.CreateParameter();p.ParameterName=n;p.Value=v;c.Parameters.Add(p);}c.ExecuteNonQuery();}voidQuery(stringlabel,stringexpected,stringsql,params(stringname,objectvalue)[]ps){usingvarc=conn.CreateCommand();c.CommandText=sql;foreach(var(n,v)inps){varp=c.CreateParameter();p.ParameterName=n;p.Value=v;c.Parameters.Add(p);}usingvarr=c.ExecuteReader();introws=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(varnodeinnew[]{("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 ('WorkItem', 'Person')","2","SELECT id FROM kg_nodes_test WHERE node_type IN ('WorkItem', '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"));Query("expected 2 -> WHERE node_type = @p0 OR node_type = @p1","2","SELECT id FROM kg_nodes_test WHERE node_type = @p0 OR node_type = @p1",("@p0","WorkItem"),("@p1","Person"));using(varc=conn.CreateCommand()){c.CommandText="DELETE FROM kg_nodes_test WHERE node_type = @p0";varp=c.CreateParameter();p.ParameterName="@p0";p.Value="WorkItem";c.Parameters.Add(p);varaffected=c.ExecuteNonQuery();Console.WriteLine($"DELETE 2 rows by node_type='WorkItem' affected={affected} (expected 2)");}

Expected output

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 ('WorkItem', 'Person') 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)
expected 2 -> WHERE node_type = @p0 OR node_type = @p1 rows=2 (expected 2)
DELETE 2 rows by node_type='WorkItem' affected=2 (expected 2)

Actual output (1.9.8.0 — verified 2026-08-30)

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) ← STILL BROKEN (multi-value param list)
expected 2 -> WHERE node_type IN ('WorkItem', 'Person') rows=3 (expected 2) ← STILL BROKEN (literal list)
expected 2 -> WHERE node_type IN (VALUES (@p0)) rows=2 (expected 2) ← fixed in 1.9.7
expected 2 -> WHERE node_type IN (VALUES (@p0), (@p1)) rows=3 (expected 2) ← STILL BROKEN (multi-row VALUES)
expected 1 -> WHERE (node_type, external_id) IN (VALUES (@nt, @ei)) rows=1 (expected 1) ← fixed in 1.9.7
expected 2 -> WHERE node_type = @p0 OR node_type = @p1 rows=3 (expected 2) ← STILL BROKEN (OR — now returns all rows)
DELETE 2 rows by node_type='WorkItem' affected=2 (expected 2) ← fixed in 1.9.7

Full version comparison

Form1.9.41.9.51.9.61.9.71.9.8
WHERE col = @p0✅ 2✅ 2✅ 2✅ 2✅ 2
WHERE col IN ('a', 'b') (literal list)✅ 2❌ 3❌ 3❌ 3❌ 3
WHERE col IN (@p0) (single param)✅ 2❌ 3✅ 2✅ 2✅ 2
WHERE col IN (@p0, @p1) (multi-value param list)✅ 2❌ 3❌ 3❌ 3❌ 3
WHERE col IN (VALUES (@p0)) (1-row VALUES)✅ 2❌ 3❌ 0✅ 2✅ 2
WHERE col IN (VALUES (@p0), (@p1)) (2-row VALUES)✅ 2❌ 3❌ 0❌ 3❌ 3
(col1, col2) IN (VALUES (@nt, @ei)) (tuple)✅ 1❌ 3❌ 0✅ 1✅ 1
WHERE col = @p0 OR col = @p1❌ 0❌ 0❌ 0❌ 3❌ 3
WHERE (col = @p0 OR col = @p1) (parenthesised OR)❌ 0❌ 0❌ 0❌ 0❌ 0
DELETE ... WHERE ...ExecuteNonQuery affected count❌ 1❌ 1❌ 1✅ 2✅ 2

1.9.8 is functionally identical to 1.9.7 for every form the probe
exercises. The four unfixed forms from 1.9.7 are still broken:

  1. IN ('a', 'b') (literal list) — returns all rows.
  2. IN (@p0, @p1) (multi-value parameter list) — returns all rows.
  3. IN (VALUES (@p0), (@p1)) (multi-row VALUES) — returns all rows.
  4. OR predicates — return all rows (was 0 in 1.9.4–1.9.6).

Suggested fix direction

  1. The 1.9.7 release notes describe the fix as rewriting
    SharpCoreDB.Services.SqlParser.SqlInPredicate to parse
    top-level-comma lists, strip SQLite VALUES, and support tuple-column /
    tuple-row forms. The probe shows the single-row VALUES case works but
    the multi-row case does not — the new parser is likely consuming only
    the first row and dropping the rest. The same hypothesis explains
    IN (@p0, @p1): the multi-value parameter list form was claimed as
    fixed in 1.9.7 but the probe shows it still returns all rows.
  2. The 1.9.7 OR parser was reportedly fixed to "split on top-level OR
    and AND", but the probe shows OR now returns all rows instead of 0.
    The new parser is likely producing a tautology (e.g.,
    WHERE col = @p0 OR TRUE) when it encounters the second operand.
  3. The literal-list form IN ('a', 'b') is also still broken — this form
    was never mentioned in the 1.9.7 release notes but is the simplest
    case of the bug and may share the same root cause as the parameterized
    list.
  4. Please add a regression test that round-trips every form in the
    comparison table above (literal list, parameterized list of size
    1/2/5/10, VALUES of size 1/2/5/10, tuple-IN, OR, parenthesised OR,
    AND+OR) and asserts the row count matches the filter for each one.

Environment

  • SharpCoreDB1.9.8.0 (NuGet, released 2026-08-30, commit a1d4b6d)
  • SharpCoreDB.EntityFrameworkCore1.9.8
  • .NET 10 / C# 14, x64, Windows
  • Verified on a clean standalone probe (no application code). Behavior
    reproducible with the snippet above in ~10 seconds.

Cross-references:

  • The 1.9.5/1.9.6/1.9.7 chain of verification probes against the same
    kg_nodes_test table scenario became permanent regression coverage in
    the SharpCoreDB test suite (per the 1.9.7 release notes acknowledgements).
  • The OR and ExecuteNonQuery DELETE-affected-count issues are
    long-standing. The DELETE count is fixed in 1.9.7; the OR issue is
    still open and in fact got worse in 1.9.7 (returns all rows instead of 0).
  • The affected parser paths are in SharpCoreDB.Services.SqlParser
    (SqlInPredicate for IN, EvaluateCondition for OR/AND).

Activity

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

Metadata

Metadata

Labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions