Skip to content

Commit f35a846

Browse files
committed
test(integration): expand corpus — like, ne, isNull filter operators
Adds two new query scenarios so the corpus exercises more of the filter-op vocabulary both runners advertise. Plus one real bug surfaced by the new isNull scenario. - queries/filter-like-and-ne.yaml: Program titles via `like "Found%"` + status via `ne "PUBLISHED"`. Like pattern is case-aligned so the test doesn't depend on LIKE-vs-ILIKE wiring. - queries/filter-is-null.yaml: Week label `isNull: true` + `isNull: false`, verifying `IS NULL` translation (not `= NULL`, which is always false). Bug fix surfaced by the new scenario: - C# DbContextAdapter.BuildOp: `isNull` was checking `rawValue is true`, but YamlDotNet deserializes a YAML boolean into a `string` ("true" / "false") when the target is `object?`. The dispatch silently took the `Expression.Not(...)` branch — `isNull: true` returned the rows where label was NOT NULL. Fix: accept both `bool` and `string` shapes; throw on anything else so a future malformed scenario fails loud. Gates: TS integration 11/11, C# integration 11/11.
1 parent 20e78f8 commit f35a846

3 files changed

Lines changed: 72 additions & 2 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: filter-is-null
2+
description: |
3+
Filter Weeks with `isNull` on a nullable field. Verifies the runner
4+
translates `{ field: { isNull: true } }` to SQL `IS NULL` (not `= NULL`,
5+
which is always false in three-valued logic) and that `{ isNull: false }`
6+
surfaces the complementary row set.
7+
seed-data: |
8+
INSERT INTO "programs" ("id", "title", "priceCents", "status", "createdAt") VALUES
9+
(1, 'Foundations', 4999, 'PUBLISHED', '2026-05-01T10:00:00');
10+
INSERT INTO "weeks" ("id", "programId", "label") VALUES
11+
(1, 1, 'Week 1'),
12+
(2, 1, NULL),
13+
(3, 1, 'Week 3'),
14+
(4, 1, NULL);
15+
queries:
16+
- name: weeks-with-null-label
17+
op: list
18+
entity: Week
19+
filter: { label: { isNull: true } }
20+
sort: [{ field: id, dir: asc }]
21+
expect:
22+
- { id: "2", programId: "1", label: null }
23+
- { id: "4", programId: "1", label: null }
24+
25+
- name: weeks-with-non-null-label
26+
op: list
27+
entity: Week
28+
filter: { label: { isNull: false } }
29+
sort: [{ field: id, dir: asc }]
30+
expect:
31+
- { id: "1", programId: "1", label: "Week 1" }
32+
- { id: "3", programId: "1", label: "Week 3" }
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: filter-like-and-ne
2+
description: |
3+
Filter Programs with `like` (SQL pattern match) and `ne` (not-equal). Both
4+
ports must surface the same rows, in the same order. The `like` pattern is
5+
case-aligned to the seed data (no case-fold dependency) so the test passes
6+
whether a port wires LIKE or ILIKE under the hood.
7+
seed-data: |
8+
INSERT INTO "programs" ("id", "title", "priceCents", "status", "createdAt") VALUES
9+
(1, 'Foundations', 4999, 'PUBLISHED', '2026-05-01T10:00:00'),
10+
(2, 'Strength', 7999, 'PUBLISHED', '2026-05-02T10:00:00'),
11+
(3, 'Mobility', 2999, 'DRAFT', '2026-05-03T10:00:00'),
12+
(4, 'Archived', 1999, 'ARCHIVED', '2026-05-04T10:00:00');
13+
queries:
14+
- name: title-like-prefix
15+
op: list
16+
entity: Program
17+
filter: { title: { like: "Found%" } }
18+
sort: [{ field: id, dir: asc }]
19+
expect:
20+
- { id: "1", title: "Foundations", priceCents: "4999", status: "PUBLISHED", createdAt: "2026-05-01T10:00:00" }
21+
22+
- name: status-not-published
23+
op: list
24+
entity: Program
25+
filter: { status: { ne: "PUBLISHED" } }
26+
sort: [{ field: id, dir: asc }]
27+
expect:
28+
- { id: "3", title: "Mobility", priceCents: "2999", status: "DRAFT", createdAt: "2026-05-03T10:00:00" }
29+
- { id: "4", title: "Archived", priceCents: "1999", status: "ARCHIVED", createdAt: "2026-05-04T10:00:00" }

server/csharp/MetaObjects.IntegrationTests/Runner/DbContextAdapter.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,20 @@ private static IQueryable<T> ApplyFilter<T>(IQueryable<T> queryable, IReadOnlyDi
125125

126126
private static Expression BuildOp(Expression propExpr, string op, object? rawValue)
127127
{
128-
// null/IsNull operators don't need value coercion.
128+
// null/IsNull operators don't need value coercion (other than the bool flag).
129+
// YamlDotNet deserializes a YAML bool into a `string` when the target is
130+
// `object?`, so accept both shapes here.
129131
if (op == "isNull")
130132
{
131133
var isNullExpr = Expression.Equal(propExpr, Expression.Constant(null, propExpr.Type));
132-
return rawValue is true ? isNullExpr : Expression.Not(isNullExpr);
134+
var wantNull = rawValue switch
135+
{
136+
bool b => b,
137+
string s when bool.TryParse(s, out var parsed) => parsed,
138+
_ => throw new InvalidOperationException(
139+
$"filter op `isNull` requires a boolean value (got: {rawValue?.GetType().Name ?? "null"})"),
140+
};
141+
return wantNull ? isNullExpr : Expression.Not(isNullExpr);
133142
}
134143

135144
// `in` takes a list of values.

0 commit comments

Comments
 (0)