From abf1d12e430a4d04182fd904878cb0f245b9432d Mon Sep 17 00:00:00 2001 From: Satya Date: Mon, 6 Jul 2026 14:05:21 -0400 Subject: [PATCH] test: mock matcher understands UNION existence reads; revive dead update-path mocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parse_and_match_mocks matched mock.type against only the first FROM table — always 'entity' for hierarchy existence reads — so every merger update-case mock was silently inert and those cases snapshotted the create path. Now each UNION arm is matched independently (a mock matching any arm returns once, mirroring UNION dedup) and the type check accepts any FROM/JOIN table. Regenerated the five revived cases (their snapshots now show real update / no-op replace / archive semantics) and added the two previously untestable probe-disagreement cases: same-row dedup (updates, no error) and id-vs-lookup conflict (TOO_MANY_LOOKUP_ROWS). Co-Authored-By: Claude Fable 5 --- fixtures/merger.json | 566 +++++++++++++-------------------- src/database/executors/mock.rs | 196 ++++++------ src/tests/fixtures.rs | 12 + 3 files changed, 343 insertions(+), 431 deletions(-) diff --git a/fixtures/merger.json b/fixtures/merger.json index 3688345..f2fd396 100644 --- a/fixtures/merger.json +++ b/fixtures/merger.json @@ -1320,62 +1320,17 @@ " AND \"pronouns\" = 'they/them'))" ], [ - "INSERT INTO agreego.\"entity\" (", - " \"created_at\",", - " \"created_by\",", - " \"id\",", - " \"modified_at\",", - " \"modified_by\",", - " \"type\"", - ")", - "VALUES (", - " '{{timestamp}}',", - " '00000000-0000-0000-0000-000000000000',", - " '{{uuid:generated_0}}',", - " '{{timestamp}}',", - " '00000000-0000-0000-0000-000000000000',", - " 'person'", - ")" + "UPDATE agreego.\"entity\" SET", + " modified_at = '{{timestamp}}',", + " modified_by = '00000000-0000-0000-0000-000000000000'", + "WHERE", + " id = '{{uuid:mocks.0.id}}'" ], [ - "INSERT INTO agreego.\"organization\" (", - " \"id\",", - " \"type\"", - ")", - "VALUES (", - " '{{uuid:generated_0}}',", - " 'person'", - ")" - ], - [ - "INSERT INTO agreego.\"user\" (", - " \"id\",", - " \"type\"", - ")", - "VALUES (", - " '{{uuid:generated_0}}',", - " 'person'", - ")" - ], - [ - "INSERT INTO agreego.\"person\" (", - " \"contact_id\",", - " \"date_of_birth\",", - " \"first_name\",", - " \"id\",", - " \"last_name\",", - " \"pronouns\",", - " \"type\"", - ")", - "VALUES (", - " 'abc-contact',", - " '{{timestamp}}',", - " 'LookupFirst',", - " '{{uuid:generated_0}}',", - " 'LookupLast',", - " 'they/them',", - " 'person'", - ")" + "UPDATE agreego.\"person\" SET", + " contact_id = 'abc-contact'", + "WHERE", + " id = '{{uuid:mocks.0.id}}'" ], [ "INSERT INTO agreego.change (", @@ -1388,45 +1343,40 @@ " \"modified_by\"", ")", "VALUES (", - " NULL,", " '{", - " \"first_name\": \"LookupFirst\",", - " \"last_name\": \"LookupLast\",", - " \"date_of_birth\": \"{{timestamp}}\",", - " \"pronouns\": \"they/them\",", + " \"contact_id\": \"old-contact\"", + " }',", + " '{", " \"contact_id\": \"abc-contact\",", " \"type\": \"person\"", " }',", + " '{{uuid:mocks.0.id}}',", " '{{uuid:generated_0}}',", - " '{{uuid:generated_1}}',", - " 'create',", + " 'update',", " '{{timestamp}}',", " '00000000-0000-0000-0000-000000000000'", ")" ], [ "(SELECT pg_notify('entity', '{", - " \"kind\": \"create\",", + " \"kind\": \"update\",", " \"complete\": {", + " \"id\": \"{{uuid:mocks.0.id}}\",", + " \"type\": \"person\",", " \"first_name\": \"LookupFirst\",", " \"last_name\": \"LookupLast\",", " \"date_of_birth\": \"{{timestamp}}\",", " \"pronouns\": \"they/them\",", " \"contact_id\": \"abc-contact\",", - " \"id\": \"{{uuid:generated_0}}\",", - " \"type\": \"person\",", - " \"created_by\": \"00000000-0000-0000-0000-000000000000\",", - " \"created_at\": \"{{timestamp}}\",", " \"modified_by\": \"00000000-0000-0000-0000-000000000000\",", " \"modified_at\": \"{{timestamp}}\"", " },", " \"new\": {", - " \"first_name\": \"LookupFirst\",", - " \"last_name\": \"LookupLast\",", - " \"date_of_birth\": \"{{timestamp}}\",", - " \"pronouns\": \"they/them\",", " \"contact_id\": \"abc-contact\",", " \"type\": \"person\"", + " },", + " \"old\": {", + " \"contact_id\": \"old-contact\"", " }", "}'))" ] @@ -1464,62 +1414,17 @@ "(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*) FROM agreego.\"entity\" t1 LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id WHERE t1.id = '{{uuid:data.id}}' UNION SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*) FROM agreego.\"entity\" t1 LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id WHERE (\"first_name\" = 'LookupFirst' AND \"last_name\" = 'LookupLast' AND \"date_of_birth\" = '{{timestamp}}' AND \"pronouns\" = 'they/them'))" ], [ - "INSERT INTO agreego.\"entity\" (", - " \"created_at\",", - " \"created_by\",", - " \"id\",", - " \"modified_at\",", - " \"modified_by\",", - " \"type\"", - ")", - "VALUES (", - " '{{timestamp}}',", - " '00000000-0000-0000-0000-000000000000',", - " '{{uuid:data.id}}',", - " '{{timestamp}}',", - " '00000000-0000-0000-0000-000000000000',", - " 'person'", - ")" + "UPDATE agreego.\"entity\" SET", + " modified_at = '{{timestamp}}',", + " modified_by = '00000000-0000-0000-0000-000000000000'", + "WHERE", + " id = '{{uuid:mocks.0.id}}'" ], [ - "INSERT INTO agreego.\"organization\" (", - " \"id\",", - " \"type\"", - ")", - "VALUES (", - " '{{uuid:data.id}}',", - " 'person'", - ")" - ], - [ - "INSERT INTO agreego.\"user\" (", - " \"id\",", - " \"type\"", - ")", - "VALUES (", - " '{{uuid:data.id}}',", - " 'person'", - ")" - ], - [ - "INSERT INTO agreego.\"person\" (", - " \"contact_id\",", - " \"date_of_birth\",", - " \"first_name\",", - " \"id\",", - " \"last_name\",", - " \"pronouns\",", - " \"type\"", - ")", - "VALUES (", - " 'abc-contact',", - " '{{timestamp}}',", - " 'LookupFirst',", - " '{{uuid:data.id}}',", - " 'LookupLast',", - " 'they/them',", - " 'person'", - ")" + "UPDATE agreego.\"person\" SET", + " contact_id = 'abc-contact'", + "WHERE", + " id = '{{uuid:mocks.0.id}}'" ], [ "INSERT INTO agreego.change (", @@ -1532,46 +1437,42 @@ " \"modified_by\"", ")", "VALUES (", - " NULL,", " '{", - " \"first_name\": \"LookupFirst\",", - " \"last_name\": \"LookupLast\",", - " \"date_of_birth\": \"{{timestamp}}\",", - " \"pronouns\": \"they/them\",", + " \"contact_id\": \"old-contact\"", + " }',", + " '{", " \"contact_id\": \"abc-contact\",", " \"type\": \"person\"", " }',", - " '{{uuid:data.id}}',", + " '{{uuid:mocks.0.id}}',", " '{{uuid:generated_0}}',", - " 'create',", + " 'update',", " '{{timestamp}}',", " '00000000-0000-0000-0000-000000000000'", ")" ], [ "(SELECT pg_notify('entity', '{", - " \"kind\": \"create\",", + " \"kind\": \"update\",", " \"complete\": {", + " \"id\": \"{{uuid:mocks.0.id}}\",", + " \"type\": \"person\",", " \"first_name\": \"LookupFirst\",", " \"last_name\": \"LookupLast\",", " \"date_of_birth\": \"{{timestamp}}\",", " \"pronouns\": \"they/them\",", " \"contact_id\": \"abc-contact\",", - " \"id\": \"{{uuid:data.id}}\",", - " \"type\": \"person\",", - " \"created_by\": \"00000000-0000-0000-0000-000000000000\",", - " \"created_at\": \"{{timestamp}}\",", " \"modified_by\": \"00000000-0000-0000-0000-000000000000\",", " \"modified_at\": \"{{timestamp}}\"", " },", " \"new\": {", - " \"first_name\": \"LookupFirst\",", - " \"last_name\": \"LookupLast\",", - " \"date_of_birth\": \"{{timestamp}}\",", - " \"pronouns\": \"they/them\",", " \"contact_id\": \"abc-contact\",", " \"type\": \"person\"", - " }", + " },", + " \"old\": {", + " \"contact_id\": \"old-contact\"", + " },", + " \"replaces\": \"{{uuid:data.id}}\"", "}'))" ] ] @@ -1606,110 +1507,24 @@ [ "(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*) FROM agreego.\"entity\" t1 LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id WHERE t1.id = '{{uuid:data.id}}' UNION SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*) FROM agreego.\"entity\" t1 LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id WHERE (\"first_name\" = 'LookupFirst' AND \"last_name\" = 'LookupLast' AND \"date_of_birth\" = '{{timestamp}}' AND \"pronouns\" = 'they/them'))" ], - [ - "INSERT INTO agreego.\"entity\" (", - " \"created_at\",", - " \"created_by\",", - " \"id\",", - " \"modified_at\",", - " \"modified_by\",", - " \"type\"", - ")", - "VALUES (", - " '{{timestamp}}',", - " '00000000-0000-0000-0000-000000000000',", - " '{{uuid:data.id}}',", - " '{{timestamp}}',", - " '00000000-0000-0000-0000-000000000000',", - " 'person'", - ")" - ], - [ - "INSERT INTO agreego.\"organization\" (", - " \"id\",", - " \"type\"", - ")", - "VALUES (", - " '{{uuid:data.id}}',", - " 'person'", - ")" - ], - [ - "INSERT INTO agreego.\"user\" (", - " \"id\",", - " \"type\"", - ")", - "VALUES (", - " '{{uuid:data.id}}',", - " 'person'", - ")" - ], - [ - "INSERT INTO agreego.\"person\" (", - " \"date_of_birth\",", - " \"first_name\",", - " \"id\",", - " \"last_name\",", - " \"pronouns\",", - " \"type\"", - ")", - "VALUES (", - " '{{timestamp}}',", - " 'LookupFirst',", - " '{{uuid:data.id}}',", - " 'LookupLast',", - " 'they/them',", - " 'person'", - ")" - ], - [ - "INSERT INTO agreego.change (", - " \"old\",", - " \"new\",", - " \"entity_id\",", - " \"id\",", - " \"kind\",", - " \"modified_at\",", - " \"modified_by\"", - ")", - "VALUES (", - " NULL,", - " '{", - " \"first_name\": \"LookupFirst\",", - " \"last_name\": \"LookupLast\",", - " \"date_of_birth\": \"{{timestamp}}\",", - " \"pronouns\": \"they/them\",", - " \"type\": \"person\"", - " }',", - " '{{uuid:data.id}}',", - " '{{uuid:generated_0}}',", - " 'create',", - " '{{timestamp}}',", - " '00000000-0000-0000-0000-000000000000'", - ")" - ], [ "(SELECT pg_notify('entity', '{", - " \"kind\": \"create\",", + " \"kind\": \"replace\",", " \"complete\": {", + " \"id\": \"{{uuid:mocks.0.id}}\",", + " \"type\": \"person\",", " \"first_name\": \"LookupFirst\",", " \"last_name\": \"LookupLast\",", " \"date_of_birth\": \"{{timestamp}}\",", " \"pronouns\": \"they/them\",", - " \"id\": \"{{uuid:data.id}}\",", - " \"type\": \"person\",", - " \"created_by\": \"00000000-0000-0000-0000-000000000000\",", - " \"created_at\": \"{{timestamp}}\",", + " \"contact_id\": \"old-contact\",", " \"modified_by\": \"00000000-0000-0000-0000-000000000000\",", " \"modified_at\": \"{{timestamp}}\"", " },", " \"new\": {", - " \"first_name\": \"LookupFirst\",", - " \"last_name\": \"LookupLast\",", - " \"date_of_birth\": \"{{timestamp}}\",", - " \"pronouns\": \"they/them\",", " \"type\": \"person\"", - " }", + " },", + " \"replaces\": \"{{uuid:data.id}}\"", "}'))" ] ] @@ -1746,56 +1561,18 @@ " t1.id = '{{uuid:mocks.0.id}}')" ], [ - "INSERT INTO agreego.\"entity\" (", - " \"created_at\",", - " \"created_by\",", - " \"id\",", - " \"modified_at\",", - " \"modified_by\",", - " \"type\"", - ")", - "VALUES (", - " '{{timestamp}}',", - " '00000000-0000-0000-0000-000000000000',", - " '{{uuid:mocks.0.id}}',", - " '{{timestamp}}',", - " '00000000-0000-0000-0000-000000000000',", - " 'person'", - ")" + "UPDATE agreego.\"entity\" SET", + " modified_at = '{{timestamp}}',", + " modified_by = '00000000-0000-0000-0000-000000000000'", + "WHERE", + " id = '{{uuid:mocks.0.id}}'" ], [ - "INSERT INTO agreego.\"organization\" (", - " \"id\",", - " \"type\"", - ")", - "VALUES (", - " '{{uuid:mocks.0.id}}',", - " 'person'", - ")" - ], - [ - "INSERT INTO agreego.\"user\" (", - " \"id\",", - " \"type\"", - ")", - "VALUES (", - " '{{uuid:mocks.0.id}}',", - " 'person'", - ")" - ], - [ - "INSERT INTO agreego.\"person\" (", - " \"first_name\",", - " \"id\",", - " \"last_name\",", - " \"type\"", - ")", - "VALUES (", - " 'NewFirst',", - " '{{uuid:mocks.0.id}}',", - " 'NewLast',", - " 'person'", - ")" + "UPDATE agreego.\"person\" SET", + " first_name = 'NewFirst',", + " last_name = 'NewLast'", + "WHERE", + " id = '{{uuid:mocks.0.id}}'" ], [ "INSERT INTO agreego.change (", @@ -1808,7 +1585,10 @@ " \"modified_by\"", ")", "VALUES (", - " NULL,", + " '{", + " \"first_name\": \"OldFirst\",", + " \"last_name\": \"OldLast\"", + " }',", " '{", " \"first_name\": \"NewFirst\",", " \"last_name\": \"NewLast\",", @@ -1816,21 +1596,19 @@ " }',", " '{{uuid:mocks.0.id}}',", " '{{uuid:generated_0}}',", - " 'create',", + " 'update',", " '{{timestamp}}',", " '00000000-0000-0000-0000-000000000000'", ")" ], [ "(SELECT pg_notify('entity', '{", - " \"kind\": \"create\",", + " \"kind\": \"update\",", " \"complete\": {", - " \"first_name\": \"NewFirst\",", - " \"last_name\": \"NewLast\",", " \"id\": \"{{uuid:mocks.0.id}}\",", " \"type\": \"person\",", - " \"created_by\": \"00000000-0000-0000-0000-000000000000\",", - " \"created_at\": \"{{timestamp}}\",", + " \"first_name\": \"NewFirst\",", + " \"last_name\": \"NewLast\",", " \"modified_by\": \"00000000-0000-0000-0000-000000000000\",", " \"modified_at\": \"{{timestamp}}\"", " },", @@ -1838,6 +1616,10 @@ " \"first_name\": \"NewFirst\",", " \"last_name\": \"NewLast\",", " \"type\": \"person\"", + " },", + " \"old\": {", + " \"first_name\": \"OldFirst\",", + " \"last_name\": \"OldLast\"", " }", "}'))" ] @@ -3006,54 +2788,12 @@ " t1.id = 'abc-archived')" ], [ - "INSERT INTO agreego.\"entity\" (", - " \"archived\",", - " \"created_at\",", - " \"created_by\",", - " \"id\",", - " \"modified_at\",", - " \"modified_by\",", - " \"type\"", - ")", - "VALUES (", - " true,", - " '{{timestamp}}',", - " '00000000-0000-0000-0000-000000000000',", - " 'abc-archived',", - " '{{timestamp}}',", - " '00000000-0000-0000-0000-000000000000',", - " 'person'", - ")" - ], - [ - "INSERT INTO agreego.\"organization\" (", - " \"id\",", - " \"type\"", - ")", - "VALUES (", - " 'abc-archived',", - " 'person'", - ")" - ], - [ - "INSERT INTO agreego.\"user\" (", - " \"id\",", - " \"type\"", - ")", - "VALUES (", - " 'abc-archived',", - " 'person'", - ")" - ], - [ - "INSERT INTO agreego.\"person\" (", - " \"id\",", - " \"type\"", - ")", - "VALUES (", - " 'abc-archived',", - " 'person'", - ")" + "UPDATE agreego.\"entity\" SET", + " archived = true,", + " modified_at = '{{timestamp}}',", + " modified_by = '00000000-0000-0000-0000-000000000000'", + "WHERE", + " id = 'abc-archived'" ], [ "INSERT INTO agreego.change (", @@ -3066,33 +2806,38 @@ " \"modified_by\"", ")", "VALUES (", - " NULL,", + " '{", + " \"archived\": false", + " }',", " '{", " \"archived\": true,", " \"type\": \"person\"", " }',", " 'abc-archived',", " '{{uuid:generated_0}}',", - " 'create',", + " 'delete',", " '{{timestamp}}',", " '00000000-0000-0000-0000-000000000000'", ")" ], [ "(SELECT pg_notify('entity', '{", - " \"kind\": \"create\",", + " \"kind\": \"delete\",", " \"complete\": {", - " \"archived\": true,", " \"id\": \"abc-archived\",", " \"type\": \"person\",", - " \"created_by\": \"00000000-0000-0000-0000-000000000000\",", - " \"created_at\": \"{{timestamp}}\",", + " \"first_name\": \"ArchivedFirst\",", + " \"last_name\": \"ArchivedLast\",", + " \"archived\": true,", " \"modified_by\": \"00000000-0000-0000-0000-000000000000\",", " \"modified_at\": \"{{timestamp}}\"", " },", " \"new\": {", " \"archived\": true,", " \"type\": \"person\"", + " },", + " \"old\": {", + " \"archived\": false", " }", "}'))" ] @@ -4027,6 +3772,143 @@ ] ] } + }, + { + "description": "Existence probes hit the same row via id and lookup key (dedup, updates)", + "action": "merge", + "data": { + "id": "22222222-2222-2222-2222-222222222222", + "type": "person", + "first_name": "LookupFirst", + "last_name": "LookupLast", + "date_of_birth": "1990-01-01T00:00:00Z", + "pronouns": "they/them", + "contact_id": "new-contact" + }, + "mocks": [ + { + "id": "22222222-2222-2222-2222-222222222222", + "type": "person", + "first_name": "LookupFirst", + "last_name": "LookupLast", + "date_of_birth": "1990-01-01T00:00:00Z", + "pronouns": "they/them", + "contact_id": "old-contact" + } + ], + "schema_id": "person", + "expect": { + "success": true, + "sql": [ + [ + "(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*) FROM agreego.\"entity\" t1 LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id WHERE t1.id = '{{uuid:mocks.0.id}}' UNION SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*) FROM agreego.\"entity\" t1 LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id WHERE (\"first_name\" = 'LookupFirst' AND \"last_name\" = 'LookupLast' AND \"date_of_birth\" = '{{timestamp}}' AND \"pronouns\" = 'they/them'))" + ], + [ + "UPDATE agreego.\"entity\" SET", + " modified_at = '{{timestamp}}',", + " modified_by = '00000000-0000-0000-0000-000000000000'", + "WHERE", + " id = '{{uuid:mocks.0.id}}'" + ], + [ + "UPDATE agreego.\"person\" SET", + " contact_id = 'new-contact'", + "WHERE", + " id = '{{uuid:mocks.0.id}}'" + ], + [ + "INSERT INTO agreego.change (", + " \"old\",", + " \"new\",", + " \"entity_id\",", + " \"id\",", + " \"kind\",", + " \"modified_at\",", + " \"modified_by\"", + ")", + "VALUES (", + " '{", + " \"contact_id\": \"old-contact\"", + " }',", + " '{", + " \"contact_id\": \"new-contact\",", + " \"type\": \"person\"", + " }',", + " '{{uuid:mocks.0.id}}',", + " '{{uuid:generated_0}}',", + " 'update',", + " '{{timestamp}}',", + " '00000000-0000-0000-0000-000000000000'", + ")" + ], + [ + "(SELECT pg_notify('entity', '{", + " \"kind\": \"update\",", + " \"complete\": {", + " \"id\": \"{{uuid:mocks.0.id}}\",", + " \"type\": \"person\",", + " \"first_name\": \"LookupFirst\",", + " \"last_name\": \"LookupLast\",", + " \"date_of_birth\": \"{{timestamp}}\",", + " \"pronouns\": \"they/them\",", + " \"contact_id\": \"new-contact\",", + " \"modified_by\": \"00000000-0000-0000-0000-000000000000\",", + " \"modified_at\": \"{{timestamp}}\"", + " },", + " \"new\": {", + " \"contact_id\": \"new-contact\",", + " \"type\": \"person\"", + " },", + " \"old\": {", + " \"contact_id\": \"old-contact\"", + " }", + "}'))" + ] + ] + } + }, + { + "description": "Id probe and lookup probe find different rows (TOO_MANY_LOOKUP_ROWS)", + "action": "merge", + "data": { + "id": "22222222-2222-2222-2222-222222222222", + "type": "person", + "first_name": "OtherFirst", + "last_name": "OtherLast", + "date_of_birth": "1991-02-02T00:00:00Z", + "pronouns": "she/her" + }, + "mocks": [ + { + "id": "22222222-2222-2222-2222-222222222222", + "type": "person", + "first_name": "LookupFirst", + "last_name": "LookupLast", + "date_of_birth": "1990-01-01T00:00:00Z", + "pronouns": "they/them" + }, + { + "id": "44444444-4444-4444-4444-444444444444", + "type": "person", + "first_name": "OtherFirst", + "last_name": "OtherLast", + "date_of_birth": "1991-02-02T00:00:00Z", + "pronouns": "she/her" + } + ], + "schema_id": "person", + "expect": { + "success": false, + "errors": [ + { + "code": "MERGE_FAILED", + "values": { + "error": "TOO_MANY_LOOKUP_ROWS: Lookup for person found too many existing rows" + }, + "details": {} + } + ] + } } ] } diff --git a/src/database/executors/mock.rs b/src/database/executors/mock.rs index 4a037b6..92e6c3d 100644 --- a/src/database/executors/mock.rs +++ b/src/database/executors/mock.rs @@ -116,99 +116,17 @@ fn parse_and_match_mocks(sql: &str, mocks: &[Value]) -> Option> { return None; } - // 1. Extract table name - let table_regex = Regex::new(r#"(?i)\s+FROM\s+(?:[a-zA-Z_]\w*\.)?"?([a-zA-Z_]\w*)"?"#).ok()?; - let table = if let Some(caps) = table_regex.captures(sql) { - caps.get(1)?.as_str() - } else { - return None; - }; + // The merger's existence read emits one SELECT per probe (id / lookup key) + // joined by UNION. Match each arm independently against its own tables and + // WHERE clause; a mock matching any arm is returned once, mirroring UNION's + // dedup semantics. + let union_regex = Regex::new(r"(?i)\s+UNION\s+(?:ALL\s+)?").ok()?; + let arms: Vec<&str> = union_regex.split(sql).collect(); - // 2. Extract WHERE conditions string - let mut where_clause = String::new(); - if let Some(where_idx) = sql_upper.find(" WHERE ") { - let mut where_end = sql_upper.find(" ORDER BY ").unwrap_or(sql_upper.len()); - if let Some(limit_idx) = sql_upper.find(" LIMIT ") { - if limit_idx < where_end { - where_end = limit_idx; - } - } - where_clause = sql[where_idx + 7..where_end].to_string(); - } - - // 3. Find matching mocks let mut matches = Vec::new(); - let or_regex = Regex::new(r"(?i)\s+OR\s+").ok()?; - let and_regex = Regex::new(r"(?i)\s+AND\s+").ok()?; - for mock in mocks { if let Some(mock_obj) = mock.as_object() { - if let Some(t) = mock_obj.get("type") { - if t.as_str() != Some(table) { - continue; - } - } - - if where_clause.is_empty() { - matches.push(mock.clone()); - continue; - } - - let or_parts = or_regex.split(&where_clause); - let mut any_branch_matched = false; - - for or_part in or_parts { - let branch_str = or_part.replace('(', "").replace(')', ""); - let mut branch_matches = true; - - for part in and_regex.split(&branch_str) { - if let Some(eq_idx) = part.find('=') { - let left = part[..eq_idx] - .trim() - .split('.') - .last() - .unwrap_or("") - .trim_matches('"'); - let right = part[eq_idx + 1..].trim().trim_matches('\''); - - let mock_val_str = match mock_obj.get(left) { - Some(Value::String(s)) => s.clone(), - Some(Value::Number(n)) => n.to_string(), - Some(Value::Bool(b)) => b.to_string(), - Some(Value::Null) => "null".to_string(), - _ => "".to_string(), - }; - if mock_val_str != right { - branch_matches = false; - break; - } - } else if part.to_uppercase().contains(" IS NULL") { - let left = part[..part.to_uppercase().find(" IS NULL").unwrap()] - .trim() - .split('.') - .last() - .unwrap_or("") - .trim_matches('"'); - - let mock_val_str = match mock_obj.get(left) { - Some(Value::Null) => "null".to_string(), - _ => "".to_string(), - }; - - if mock_val_str != "null" { - branch_matches = false; - break; - } - } - } - - if branch_matches { - any_branch_matched = true; - break; - } - } - - if any_branch_matched { + if arms.iter().any(|arm| arm_matches_mock(arm, mock_obj)) { matches.push(mock.clone()); } } @@ -216,3 +134,103 @@ fn parse_and_match_mocks(sql: &str, mocks: &[Value]) -> Option> { Some(matches) } + +#[cfg(test)] +fn arm_matches_mock(arm: &str, mock_obj: &serde_json::Map) -> bool { + let arm_upper = arm.to_uppercase(); + + // 1. The mock's type must name one of the arm's tables. Hierarchy reads + // SELECT FROM the root (entity) and JOIN the subtype tables, so every + // FROM/JOIN table counts, not just the first. + let table_regex = match Regex::new(r#"(?i)\b(?:FROM|JOIN)\s+(?:[a-zA-Z_]\w*\.)?"?([a-zA-Z_]\w*)"?"#) { + Ok(r) => r, + Err(_) => return false, + }; + let tables: Vec<&str> = table_regex.captures_iter(arm).filter_map(|c| c.get(1)).map(|m| m.as_str()).collect(); + if tables.is_empty() { + return false; + } + if let Some(t) = mock_obj.get("type") { + if !tables.iter().any(|table| t.as_str() == Some(table)) { + return false; + } + } + + // 2. Extract this arm's WHERE conditions + let mut where_clause = String::new(); + if let Some(where_idx) = arm_upper.find(" WHERE ") { + let mut where_end = arm_upper.find(" ORDER BY ").unwrap_or(arm_upper.len()); + if let Some(limit_idx) = arm_upper.find(" LIMIT ") { + if limit_idx < where_end { + where_end = limit_idx; + } + } + where_clause = arm[where_idx + 7..where_end].to_string(); + } + + if where_clause.is_empty() { + return true; + } + + // 3. Match the mock against the conditions + let or_regex = match Regex::new(r"(?i)\s+OR\s+") { + Ok(r) => r, + Err(_) => return false, + }; + let and_regex = match Regex::new(r"(?i)\s+AND\s+") { + Ok(r) => r, + Err(_) => return false, + }; + + for or_part in or_regex.split(&where_clause) { + let branch_str = or_part.replace('(', "").replace(')', ""); + let mut branch_matches = true; + + for part in and_regex.split(&branch_str) { + if let Some(eq_idx) = part.find('=') { + let left = part[..eq_idx] + .trim() + .split('.') + .last() + .unwrap_or("") + .trim_matches('"'); + let right = part[eq_idx + 1..].trim().trim_matches('\''); + + let mock_val_str = match mock_obj.get(left) { + Some(Value::String(s)) => s.clone(), + Some(Value::Number(n)) => n.to_string(), + Some(Value::Bool(b)) => b.to_string(), + Some(Value::Null) => "null".to_string(), + _ => "".to_string(), + }; + if mock_val_str != right { + branch_matches = false; + break; + } + } else if part.to_uppercase().contains(" IS NULL") { + let left = part[..part.to_uppercase().find(" IS NULL").unwrap()] + .trim() + .split('.') + .last() + .unwrap_or("") + .trim_matches('"'); + + let mock_val_str = match mock_obj.get(left) { + Some(Value::Null) => "null".to_string(), + _ => "".to_string(), + }; + + if mock_val_str != "null" { + branch_matches = false; + break; + } + } + } + + if branch_matches { + return true; + } + } + + false +} diff --git a/src/tests/fixtures.rs b/src/tests/fixtures.rs index b22d10c..9c854fa 100644 --- a/src/tests/fixtures.rs +++ b/src/tests/fixtures.rs @@ -7660,3 +7660,15 @@ fn test_merger_0_15() { let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR")); crate::tests::runner::run_test_case(&path, 0, 15).unwrap(); } + +#[test] +fn test_merger_0_16() { + let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR")); + crate::tests::runner::run_test_case(&path, 0, 16).unwrap(); +} + +#[test] +fn test_merger_0_17() { + let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR")); + crate::tests::runner::run_test_case(&path, 0, 17).unwrap(); +}