From 8a35b8eba4118c862f180495780c34e74774d72f Mon Sep 17 00:00:00 2001 From: Alex Groleau Date: Tue, 25 Aug 2026 13:11:10 -0400 Subject: [PATCH] fixed test issues with lookups --- fixtures/merger.json | 541 +++++++-------------------------- src/database/executors/mock.rs | 99 +++--- src/database/type.rs | 10 + src/merger/mod.rs | 28 +- 4 files changed, 199 insertions(+), 479 deletions(-) diff --git a/fixtures/merger.json b/fixtures/merger.json index 45b57ea..de6e18d 100644 --- a/fixtures/merger.json +++ b/fixtures/merger.json @@ -1363,65 +1363,18 @@ " AND t4.\"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}}'", "RETURNING to_jsonb(\"entity\".*)" ], [ - "INSERT INTO agreego.\"organization\" (", - " \"id\",", - " \"type\"", - ")", - "VALUES (", - " '{{uuid:generated_0}}',", - " 'person'", - ")", - "RETURNING to_jsonb(\"organization\".*)" - ], - [ - "INSERT INTO agreego.\"user\" (", - " \"id\",", - " \"type\"", - ")", - "VALUES (", - " '{{uuid:generated_0}}',", - " 'person'", - ")", - "RETURNING to_jsonb(\"user\".*)" - ], - [ - "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}}'", "RETURNING to_jsonb(\"person\".*)" ], [ @@ -1435,45 +1388,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\"", " }", "}'))" ] @@ -1527,65 +1475,18 @@ " AND t4.\"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}}'", "RETURNING to_jsonb(\"entity\".*)" ], [ - "INSERT INTO agreego.\"organization\" (", - " \"id\",", - " \"type\"", - ")", - "VALUES (", - " '{{uuid:data.id}}',", - " 'person'", - ")", - "RETURNING to_jsonb(\"organization\".*)" - ], - [ - "INSERT INTO agreego.\"user\" (", - " \"id\",", - " \"type\"", - ")", - "VALUES (", - " '{{uuid:data.id}}',", - " 'person'", - ")", - "RETURNING to_jsonb(\"user\".*)" - ], - [ - "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}}'", "RETURNING to_jsonb(\"person\".*)" ], [ @@ -1599,46 +1500,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}}\"", "}'))" ] ] @@ -1700,67 +1597,18 @@ " (t3.\"name\" = 'LookupName'))" ], [ - "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}}'", "RETURNING to_jsonb(\"entity\".*)" ], [ - "INSERT INTO agreego.\"organization\" (", - " \"id\",", - " \"name\",", - " \"type\"", - ")", - "VALUES (", - " '{{uuid:data.id}}',", - " 'LookupName',", - " 'person'", - ")", - "RETURNING to_jsonb(\"organization\".*)" - ], - [ - "INSERT INTO agreego.\"user\" (", - " \"id\",", - " \"type\"", - ")", - "VALUES (", - " '{{uuid:data.id}}',", - " 'person'", - ")", - "RETURNING to_jsonb(\"user\".*)" - ], - [ - "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}}'", "RETURNING to_jsonb(\"person\".*)" ], [ @@ -1774,49 +1622,43 @@ " \"modified_by\"", ")", "VALUES (", - " NULL,", " '{", - " \"name\": \"LookupName\",", - " \"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\",", " \"name\": \"LookupName\",", " \"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\": {", - " \"name\": \"LookupName\",", - " \"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}}\"", "}'))" ] ] @@ -1867,114 +1709,24 @@ " AND t4.\"date_of_birth\" = '{{timestamp}}'", " AND t4.\"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'", - ")", - "RETURNING to_jsonb(\"entity\".*)" - ], - [ - "INSERT INTO agreego.\"organization\" (", - " \"id\",", - " \"type\"", - ")", - "VALUES (", - " '{{uuid:data.id}}',", - " 'person'", - ")", - "RETURNING to_jsonb(\"organization\".*)" - ], - [ - "INSERT INTO agreego.\"user\" (", - " \"id\",", - " \"type\"", - ")", - "VALUES (", - " '{{uuid:data.id}}',", - " 'person'", - ")", - "RETURNING to_jsonb(\"user\".*)" - ], - [ - "INSERT INTO agreego.\"person\" (", - " \"date_of_birth\",", - " \"first_name\",", - " \"id\",", - " \"last_name\",", - " \"pronouns\",", - " \"type\"", - ")", - "VALUES (", - " '{{timestamp}}',", - " 'LookupFirst',", - " '{{uuid:data.id}}',", - " 'LookupLast',", - " 'they/them',", - " 'person'", - ")", - "RETURNING to_jsonb(\"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}}\"", "}'))" ] ] @@ -2011,59 +1763,19 @@ " 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}}'", "RETURNING to_jsonb(\"entity\".*)" ], [ - "INSERT INTO agreego.\"organization\" (", - " \"id\",", - " \"type\"", - ")", - "VALUES (", - " '{{uuid:mocks.0.id}}',", - " 'person'", - ")", - "RETURNING to_jsonb(\"organization\".*)" - ], - [ - "INSERT INTO agreego.\"user\" (", - " \"id\",", - " \"type\"", - ")", - "VALUES (", - " '{{uuid:mocks.0.id}}',", - " 'person'", - ")", - "RETURNING to_jsonb(\"user\".*)" - ], - [ - "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}}'", "RETURNING to_jsonb(\"person\".*)" ], [ @@ -2077,7 +1789,10 @@ " \"modified_by\"", ")", "VALUES (", - " NULL,", + " '{", + " \"first_name\": \"OldFirst\",", + " \"last_name\": \"OldLast\"", + " }',", " '{", " \"first_name\": \"NewFirst\",", " \"last_name\": \"NewLast\",", @@ -2085,21 +1800,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}}\"", " },", @@ -2107,6 +1820,10 @@ " \"first_name\": \"NewFirst\",", " \"last_name\": \"NewLast\",", " \"type\": \"person\"", + " },", + " \"old\": {", + " \"first_name\": \"OldFirst\",", + " \"last_name\": \"OldLast\"", " }", "}'))" ] @@ -3345,59 +3062,14 @@ " 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'", - ")", + "UPDATE agreego.\"entity\" SET", + " archived = true,", + " modified_at = '{{timestamp}}',", + " modified_by = '00000000-0000-0000-0000-000000000000'", + "WHERE", + " id = 'abc-archived'", "RETURNING to_jsonb(\"entity\".*)" ], - [ - "INSERT INTO agreego.\"organization\" (", - " \"id\",", - " \"type\"", - ")", - "VALUES (", - " 'abc-archived',", - " 'person'", - ")", - "RETURNING to_jsonb(\"organization\".*)" - ], - [ - "INSERT INTO agreego.\"user\" (", - " \"id\",", - " \"type\"", - ")", - "VALUES (", - " 'abc-archived',", - " 'person'", - ")", - "RETURNING to_jsonb(\"user\".*)" - ], - [ - "INSERT INTO agreego.\"person\" (", - " \"id\",", - " \"type\"", - ")", - "VALUES (", - " 'abc-archived',", - " 'person'", - ")", - "RETURNING to_jsonb(\"person\".*)" - ], [ "INSERT INTO agreego.change (", " \"old\",", @@ -3409,33 +3081,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", " }", "}'))" ] diff --git a/src/database/executors/mock.rs b/src/database/executors/mock.rs index f044ed8..207ae80 100644 --- a/src/database/executors/mock.rs +++ b/src/database/executors/mock.rs @@ -130,56 +130,69 @@ impl MockExecutor { #[cfg(test)] fn parse_and_match_mocks(sql: &str, mocks: &[Value]) -> Option> { let sql_upper = sql.to_uppercase(); - if !sql_upper.starts_with("SELECT") { + if !sql_upper.starts_with("SELECT") && !sql_upper.starts_with("(SELECT") { 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; - }; + let union_regex = Regex::new(r"(?i)\s+UNION\s+").ok()?; + let queries: 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; - } - } + let mock_obj = match mock.as_object() { + Some(obj) => obj, + None => continue, + }; - if where_clause.is_empty() { - matches.push(mock.clone()); + let mock_type = mock_obj.get("type").and_then(|v| v.as_str()).unwrap_or(""); + + let mut mock_matched = false; + + for query in &queries { + let q_upper = query.to_uppercase(); + + // Check if mock type matches the table or any joined tables in this query + let table_regex = Regex::new(r#"(?i)\s+(?:FROM|JOIN)\s+(?:[a-zA-Z_]\w*\.)?"?([a-zA-Z_]\w*)"?"#).ok()?; + let tables: Vec = table_regex + .captures_iter(query) + .filter_map(|c| c.get(1).map(|m| m.as_str().to_string())) + .collect(); + + if !mock_type.is_empty() && !tables.is_empty() && !tables.iter().any(|t| t == mock_type) { continue; } - let or_parts = or_regex.split(&where_clause); - let mut any_branch_matched = false; + // Extract WHERE clause + let mut where_clause = String::new(); + if let Some(where_idx) = q_upper.find(" WHERE ") { + let mut where_end = q_upper.find(" ORDER BY ").unwrap_or(q_upper.len()); + if let Some(limit_idx) = q_upper.find(" LIMIT ") { + if limit_idx < where_end { + where_end = limit_idx; + } + } + where_clause = query[where_idx + 7..where_end].trim_end_matches(')').to_string(); + } + if where_clause.is_empty() { + mock_matched = true; + break; + } + + let or_parts = or_regex.split(&where_clause); 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) { + let part = part.trim(); + if part.is_empty() { + continue; + } + if let Some(eq_idx) = part.find('=') { let left = part[..eq_idx] .trim() @@ -193,7 +206,7 @@ fn parse_and_match_mocks(sql: &str, mocks: &[Value]) -> Option> { 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(), + Some(Value::Null) | None => "null".to_string(), _ => "".to_string(), }; if mock_val_str != right { @@ -201,19 +214,21 @@ fn parse_and_match_mocks(sql: &str, mocks: &[Value]) -> Option> { break; } } else if part.to_uppercase().contains(" IS NULL") { - let left = part[..part.to_uppercase().find(" IS NULL").unwrap()] + let is_null_idx = part.to_uppercase().find(" IS NULL").unwrap(); + let left = part[..is_null_idx] .trim() .split('.') .last() .unwrap_or("") .trim_matches('"'); - let mock_val_str = match mock_obj.get(left) { - Some(Value::Null) => "null".to_string(), - _ => "".to_string(), + let is_null_val = match mock_obj.get(left) { + Some(Value::Null) | None => true, + Some(Value::String(s)) if s.is_empty() => true, + _ => false, }; - if mock_val_str != "null" { + if !is_null_val { branch_matches = false; break; } @@ -221,15 +236,19 @@ fn parse_and_match_mocks(sql: &str, mocks: &[Value]) -> Option> { } if branch_matches { - any_branch_matched = true; + mock_matched = true; break; } } - if any_branch_matched { - matches.push(mock.clone()); + if mock_matched { + break; } } + + if mock_matched { + matches.push(mock.clone()); + } } Some(matches) diff --git a/src/database/type.rs b/src/database/type.rs index 73ef571..0691446 100644 --- a/src/database/type.rs +++ b/src/database/type.rs @@ -5,6 +5,14 @@ use serde::{Deserialize, Serialize}; use std::sync::Arc; use serde_json::Value; +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Roles { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub read: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub write: Vec, +} + #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(default)] pub struct Type { @@ -38,6 +46,8 @@ pub struct Type { #[serde(default)] pub field_defaults: IndexMap, pub field_types: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub roles: Option, #[serde(default)] pub schemas: IndexMap>, } diff --git a/src/merger/mod.rs b/src/merger/mod.rs index 84a9bba..a9d9f7e 100644 --- a/src/merger/mod.rs +++ b/src/merger/mod.rs @@ -762,23 +762,33 @@ impl Merger { if let Some(parent_type) = self.db.types.get(parent_type_name) { if !parent_type.lookup_fields.is_empty() { let mut lookup_complete = true; + let mut has_provided_fields = false; for column in &parent_type.lookup_fields { + let is_nullable = parent_type.null_fields.contains(column); let val = entity_fields.get(column).or_else(|| { parent_type.field_defaults.get(column) }); match val { Some(Value::Null) | None => { - lookup_complete = false; - break; + if !is_nullable { + lookup_complete = false; + break; + } } Some(Value::String(s)) if s.is_empty() => { - lookup_complete = false; - break; + if !is_nullable { + lookup_complete = false; + break; + } + } + _ => { + if entity_fields.contains_key(column) { + has_provided_fields = true; + } } - _ => {} } } - if lookup_complete { + if lookup_complete && has_provided_fields { lookup_satisfied_keys.push((&parent_type.lookup_fields, parent_type)); } } @@ -831,7 +841,11 @@ impl Merger { .get(column) .or_else(|| parent_type.field_defaults.get(column)) .unwrap_or(&Value::Null); - lookup_predicates.push(format!("{}.\"{}\" = {}", t_alias, column, Self::quote_literal(val))); + if val.is_null() || val.as_str() == Some("") { + lookup_predicates.push(format!("{}.\"{}\" IS NULL", t_alias, column)); + } else { + lookup_predicates.push(format!("{}.\"{}\" = {}", t_alias, column, Self::quote_literal(val))); + } } where_parts.push(format!("({})", lookup_predicates.join(" AND "))); }