Compare commits

...

6 Commits

Author SHA1 Message Date
3d918a1acc version: 1.0.139 2026-05-13 19:28:13 -04:00
1f9b407074 final fix to org id setting in merge 2026-05-13 19:28:03 -04:00
6ea6007d86 version: 1.0.138 2026-05-13 16:31:15 -04:00
c129864c89 fixed another org id issue with merger 2026-05-13 16:31:06 -04:00
777fc8bbf8 version: 1.0.137 2026-05-13 15:58:54 -04:00
803d62b2fb cleanup 2026-05-13 15:58:49 -04:00
11 changed files with 64 additions and 741 deletions

View File

@ -1,15 +0,0 @@
import json
with open("fixtures/merger.json", "r") as f:
data = json.load(f)
# Find our new test
test_case = next(t for t in data[0]["tests"] if t["description"] == "Test organization_id syntactic sugar permutations")
# Fix the first SQL command (INSERT INTO entity for person)
sql = test_case["expect"]["sql"][0]
sql.remove(" \"organization_id\",")
sql.remove(" NULL,")
with open("fixtures/merger.json", "w") as f:
json.dump(data, f, indent=2)

View File

@ -1,17 +0,0 @@
import json
with open("fixtures/merger.json", "r") as f:
data = json.load(f)
db = data[0]["database"]
# Add organization_id to fields and grouped_fields.entity of order, order_line, person
for t in db["types"]:
if t["name"] in ["order", "order_line", "person"]:
if "organization_id" not in t["fields"]:
t["fields"].append("organization_id")
if "organization_id" not in t["grouped_fields"]["entity"]:
t["grouped_fields"]["entity"].append("organization_id")
with open("fixtures/merger.json", "w") as f:
json.dump(data, f, indent=2)

View File

@ -1,16 +0,0 @@
import json
with open("fixtures/merger.json", "r") as f:
text = f.read()
# Fix the broken formatting
text = text.replace("'{',\n \" {timestamp}\",\n \" }'", "'{{timestamp}}'")
text = text.replace("'{',\n \" {uuid}\",\n \" }'", "'{{uuid}}'")
text = text.replace("'{',\n \" {uuid:person_id}\",\n \" }'", "'{{uuid:person_id}}'")
text = text.replace("'{',\n \" {uuid:order_id}\",\n \" }'", "'{{uuid:order_id}}'")
text = text.replace("'{',\n \" {uuid:line1_id}\",\n \" }'", "'{{uuid:line1_id}}'")
text = text.replace("'{',\n \" {uuid:line2_id}\",\n \" }'", "'{{uuid:line2_id}}'")
with open("fixtures/merger.json", "w") as f:
f.write(text)

View File

@ -1,32 +0,0 @@
import json
with open("fixtures/merger.json", "r") as f:
data = json.load(f)
test_case = data[0]["tests"][-1]
for j, sql_group in enumerate(test_case["expect"]["sql"]):
new_group = []
i = 0
while i < len(sql_group):
s = sql_group[i]
if s.strip() == "'{":
if i + 2 < len(sql_group):
next_line = sql_group[i+1].strip()
next_next_line = sql_group[i+2].strip()
if next_next_line == "}',":
# Reconstruct
new_group.append(f" '{next_line}',")
i += 3
continue
elif next_next_line == "}'":
new_group.append(f" '{next_line}'")
i += 3
continue
new_group.append(s)
i += 1
test_case["expect"]["sql"][j] = new_group
with open("fixtures/merger.json", "w") as f:
json.dump(data, f, indent=2)

View File

@ -1,20 +0,0 @@
import json
with open("fixtures/merger.json", "r") as f:
data = json.load(f)
test_case = data[0]["tests"][-1]
for j, sql_group in enumerate(test_case["expect"]["sql"]):
for i, s in enumerate(sql_group):
s = s.replace("'{timestamp}'", "'{{timestamp}}'")
s = s.replace("'{uuid}'", "'{{uuid}}'")
s = s.replace("'{uuid:person_id}'", "'{{uuid:person_id}}'")
s = s.replace("'{uuid:order_id}'", "'{{uuid:order_id}}'")
s = s.replace("'{uuid:line1_id}'", "'{{uuid:line1_id}}'")
s = s.replace("'{uuid:line2_id}'", "'{{uuid:line2_id}}'")
sql_group[i] = s
with open("fixtures/merger.json", "w") as f:
json.dump(data, f, indent=2)

View File

@ -1806,6 +1806,7 @@
" \"id\",", " \"id\",",
" \"modified_at\",", " \"modified_at\",",
" \"modified_by\",", " \"modified_by\",",
" \"organization_id\",",
" \"type\"", " \"type\"",
")", ")",
"VALUES (", "VALUES (",
@ -1814,6 +1815,7 @@
" '{{uuid:customer_id}}',", " '{{uuid:customer_id}}',",
" '{{timestamp}}',", " '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',", " '00000000-0000-0000-0000-000000000000',",
" 'ffffffff-ffff-ffff-ffff-ffffffffffff',",
" 'person'", " 'person'",
")" ")"
], ],
@ -1869,6 +1871,7 @@
" \"date_of_birth\":\"2000-01-01\",", " \"date_of_birth\":\"2000-01-01\",",
" \"first_name\":\"Bob\",", " \"first_name\":\"Bob\",",
" \"last_name\":\"Smith\",", " \"last_name\":\"Smith\",",
" \"organization_id\":\"ffffffff-ffff-ffff-ffff-ffffffffffff\",",
" \"type\":\"person\"", " \"type\":\"person\"",
" }',", " }',",
" '{{uuid:customer_id}}',", " '{{uuid:customer_id}}',",
@ -1964,12 +1967,14 @@
" \"last_name\":\"Smith\",", " \"last_name\":\"Smith\",",
" \"modified_at\":\"{{timestamp}}\",", " \"modified_at\":\"{{timestamp}}\",",
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",", " \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"organization_id\":\"ffffffff-ffff-ffff-ffff-ffffffffffff\",",
" \"type\":\"person\"", " \"type\":\"person\"",
" },", " },",
" \"new\":{", " \"new\":{",
" \"date_of_birth\":\"2000-01-01\",", " \"date_of_birth\":\"2000-01-01\",",
" \"first_name\":\"Bob\",", " \"first_name\":\"Bob\",",
" \"last_name\":\"Smith\",", " \"last_name\":\"Smith\",",
" \"organization_id\":\"ffffffff-ffff-ffff-ffff-ffffffffffff\",",
" \"type\":\"person\"", " \"type\":\"person\"",
" }", " }",
" }')" " }')"
@ -3368,6 +3373,7 @@
" \"id\",", " \"id\",",
" \"modified_at\",", " \"modified_at\",",
" \"modified_by\",", " \"modified_by\",",
" \"organization_id\",",
" \"type\"", " \"type\"",
")", ")",
"VALUES (", "VALUES (",
@ -3376,6 +3382,7 @@
" '{{uuid:person_id}}',", " '{{uuid:person_id}}',",
" '{{timestamp}}',", " '{{timestamp}}',",
" '{{uuid}}',", " '{{uuid}}',",
" 'ffffffff-ffff-ffff-ffff-ffffffffffff',",
" 'person'", " 'person'",
")" ")"
], ],
@ -3428,6 +3435,7 @@
" '{", " '{",
" \"first_name\":\"Const\",", " \"first_name\":\"Const\",",
" \"last_name\":\"Person\",", " \"last_name\":\"Person\",",
" \"organization_id\":\"ffffffff-ffff-ffff-ffff-ffffffffffff\",",
" \"type\":\"person\"", " \"type\":\"person\"",
" }',", " }',",
" '{{uuid:person_id}}',", " '{{uuid:person_id}}',",
@ -3634,11 +3642,13 @@
" \"last_name\":\"Person\",", " \"last_name\":\"Person\",",
" \"modified_at\":\"{{timestamp}}\",", " \"modified_at\":\"{{timestamp}}\",",
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",", " \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"organization_id\":\"ffffffff-ffff-ffff-ffff-ffffffffffff\",",
" \"type\":\"person\"", " \"type\":\"person\"",
" },", " },",
" \"new\":{", " \"new\":{",
" \"first_name\":\"Const\",", " \"first_name\":\"Const\",",
" \"last_name\":\"Person\",", " \"last_name\":\"Person\",",
" \"organization_id\":\"ffffffff-ffff-ffff-ffff-ffffffffffff\",",
" \"type\":\"person\"", " \"type\":\"person\"",
" }", " }",
" }')" " }')"

View File

@ -1,111 +0,0 @@
import json
import re
with open("fixtures/merger.json", "r") as f:
data = json.load(f)
test_case = next(t for t in data[0]["tests"] if t["description"] == "Test organization_id syntactic sugar permutations")
def format_sql(sql_str):
if sql_str.startswith("INSERT INTO"):
parts = sql_str.split(" VALUES ")
insert_part = parts[0]
values_part = parts[1]
insert_match = re.match(r"(INSERT INTO [a-zA-Z0-9_.\"]+) \((.*)\)", insert_part)
table = insert_match.group(1)
cols_str = insert_match.group(2)
cols = [c.strip() for c in cols_str.split(",")]
values_str = values_part[1:-1]
# We need to split values_str carefully, as JSON strings contain commas!
# Since it's single quotes around values, we can split by ", " but that's risky.
# Let's do a simple parse:
vals = []
current_val = []
in_quote = False
i = 0
while i < len(values_str):
c = values_str[i]
if c == "'":
# handle double quotes inside? Postgres uses '' for escaping ' inside '.
# Here we don't have that complexity.
in_quote = not in_quote
current_val.append(c)
elif c == ',' and not in_quote:
vals.append("".join(current_val).strip())
current_val = []
else:
current_val.append(c)
i += 1
vals.append("".join(current_val).strip())
lines = [f"{table} ("]
for i, col in enumerate(cols):
lines.append(f" {col}" + ("," if i < len(cols) - 1 else ""))
lines.append(")")
lines.append("VALUES (")
for i, val in enumerate(vals):
if val.startswith("'{") and val.endswith("}'"):
# Format JSON
lines.append(" '{")
json_str = val[2:-2]
# Split json keys by ",
json_pairs = json_str.split(',"')
for j, pair in enumerate(json_pairs):
if j > 0:
pair = '"' + pair
lines.append(f" {pair}" + ("," if j < len(json_pairs) - 1 else ""))
lines.append(" }'" + ("," if i < len(vals) - 1 else ""))
else:
# Replace '{{uuid}}' with '00000000-0000-0000-0000-000000000000' for created_by etc if it was replaced as '{{uuid}}'
if val == "'{{uuid}}'" and cols[i] in ['"created_by"', '"modified_by"', 'modified_by']:
val = "'00000000-0000-0000-0000-000000000000'"
lines.append(f" {val}" + ("," if i < len(vals) - 1 else ""))
lines.append(")")
return lines
elif sql_str.startswith("SELECT pg_notify"):
# Format notify string
match = re.match(r"SELECT pg_notify\('entity', '(.*)'\)", sql_str)
payload = match.group(1)
# We know payload looks like {"complete":{...},"new":{...}}
lines = ["SELECT pg_notify('entity', '{"]
# split complete and new
complete_str = payload[payload.find('"complete":{')+12:payload.find('},"new":{')]
new_str = payload[payload.find('"new":{')+7:-2]
lines.append(" \"complete\":{")
complete_pairs = complete_str.split(',"')
for j, pair in enumerate(complete_pairs):
if j > 0:
pair = '"' + pair
lines.append(f" {pair}" + ("," if j < len(complete_pairs) - 1 else ""))
lines.append(" },")
lines.append(" \"new\":{")
new_pairs = new_str.split(',"')
for j, pair in enumerate(new_pairs):
if j > 0:
pair = '"' + pair
lines.append(f" {pair}" + ("," if j < len(new_pairs) - 1 else ""))
lines.append(" }")
lines.append(" }')")
return lines
return [sql_str]
new_sql = []
for sql_group in test_case["expect"]["sql"]:
sql_str = "".join(sql_group)
formatted = format_sql(sql_str)
new_sql.append(formatted)
test_case["expect"]["sql"] = new_sql
with open("fixtures/merger.json", "w") as f:
json.dump(data, f, indent=2)

View File

@ -1,409 +0,0 @@
import json
with open("fixtures/merger.json", "r") as f:
data = json.load(f)
db = data[0]["database"]
# 1. Update entity schema
entity_type = next(t for t in db["types"] if t["name"] == "entity")
entity_type["schemas"]["entity"]["properties"]["organization_id"] = {"type": "string"}
entity_type["fields"].append("organization_id")
entity_type["grouped_fields"]["entity"].append("organization_id")
# 2. Update person schema
person_type = next(t for t in db["types"] if t["name"] == "person")
person_type["schemas"]["person"]["properties"]["organization_id"] = {
"type": "string",
"const": "ffffffff-ffff-ffff-ffff-ffffffffffff"
}
# 3. Add the test case
test_case = {
"description": "Test organization_id syntactic sugar permutations",
"action": "merge",
"data": {
"type": "order",
"organization_id": "parent-org-id",
"customer": {
"type": "person",
"first_name": "Const",
"last_name": "Person"
},
"lines": [
{
"type": "order_line"
},
{
"type": "order_line",
"organization_id": "explicit-org-id"
}
]
},
"schema_id": "order",
"expect": {
"success": True,
"sql": [
[
"INSERT INTO agreego.\"entity\" (",
" \"created_at\",",
" \"created_by\",",
" \"id\",",
" \"modified_at\",",
" \"modified_by\",",
" \"organization_id\",",
" \"type\"",
")",
"VALUES (",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" '{{uuid:person_id}}',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" NULL,",
" 'person'",
")"
],
[
"INSERT INTO agreego.\"organization\" (",
" \"id\",",
" \"type\"",
")",
"VALUES (",
" '{{uuid:person_id}}',",
" 'person'",
")"
],
[
"INSERT INTO agreego.\"user\" (",
" \"id\",",
" \"type\"",
")",
"VALUES (",
" '{{uuid:person_id}}',",
" 'person'",
")"
],
[
"INSERT INTO agreego.\"person\" (",
" \"first_name\",",
" \"id\",",
" \"last_name\",",
" \"type\"",
")",
"VALUES (",
" 'Const',",
" '{{uuid:person_id}}',",
" 'Person',",
" 'person'",
")"
],
[
"INSERT INTO agreego.change (",
" \"old\",",
" \"new\",",
" entity_id,",
" id,",
" kind,",
" modified_at,",
" modified_by",
")",
"VALUES (",
" NULL,",
" '{",
" \"first_name\":\"Const\",",
" \"last_name\":\"Person\",",
" \"type\":\"person\"",
" }',",
" '{{uuid:person_id}}',",
" '{{uuid}}',",
" 'create',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000'",
")"
],
[
"INSERT INTO agreego.\"entity\" (",
" \"created_at\",",
" \"created_by\",",
" \"id\",",
" \"modified_at\",",
" \"modified_by\",",
" \"organization_id\",",
" \"type\"",
")",
"VALUES (",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" '{{uuid:line1_id}}',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" 'parent-org-id',",
" 'order_line'",
")"
],
[
"INSERT INTO agreego.\"order_line\" (",
" \"id\",",
" \"order_id\",",
" \"type\"",
")",
"VALUES (",
" '{{uuid:line1_id}}',",
" '{{uuid:order_id}}',",
" 'order_line'",
")"
],
[
"INSERT INTO agreego.change (",
" \"old\",",
" \"new\",",
" entity_id,",
" id,",
" kind,",
" modified_at,",
" modified_by",
")",
"VALUES (",
" NULL,",
" '{",
" \"order_id\":\"{{uuid:order_id}}\",",
" \"organization_id\":\"parent-org-id\",",
" \"type\":\"order_line\"",
" }',",
" '{{uuid:line1_id}}',",
" '{{uuid}}',",
" 'create',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000'",
")"
],
[
"INSERT INTO agreego.\"entity\" (",
" \"created_at\",",
" \"created_by\",",
" \"id\",",
" \"modified_at\",",
" \"modified_by\",",
" \"organization_id\",",
" \"type\"",
")",
"VALUES (",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" '{{uuid:line2_id}}',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" 'explicit-org-id',",
" 'order_line'",
")"
],
[
"INSERT INTO agreego.\"order_line\" (",
" \"id\",",
" \"order_id\",",
" \"type\"",
")",
"VALUES (",
" '{{uuid:line2_id}}',",
" '{{uuid:order_id}}',",
" 'order_line'",
")"
],
[
"INSERT INTO agreego.change (",
" \"old\",",
" \"new\",",
" entity_id,",
" id,",
" kind,",
" modified_at,",
" modified_by",
")",
"VALUES (",
" NULL,",
" '{",
" \"order_id\":\"{{uuid:order_id}}\",",
" \"organization_id\":\"explicit-org-id\",",
" \"type\":\"order_line\"",
" }',",
" '{{uuid:line2_id}}',",
" '{{uuid}}',",
" 'create',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000'",
")"
],
[
"INSERT INTO agreego.\"entity\" (",
" \"created_at\",",
" \"created_by\",",
" \"id\",",
" \"modified_at\",",
" \"modified_by\",",
" \"organization_id\",",
" \"type\"",
")",
"VALUES (",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" '{{uuid:order_id}}',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" 'parent-org-id',",
" 'order'",
")"
],
[
"INSERT INTO agreego.\"order\" (",
" \"customer_id\",",
" \"id\",",
" \"type\"",
")",
"VALUES (",
" '{{uuid:person_id}}',",
" '{{uuid:order_id}}',",
" 'order'",
")"
],
[
"INSERT INTO agreego.change (",
" \"old\",",
" \"new\",",
" entity_id,",
" id,",
" kind,",
" modified_at,",
" modified_by",
")",
"VALUES (",
" NULL,",
" '{",
" \"customer_id\":\"{{uuid:person_id}}\",",
" \"organization_id\":\"parent-org-id\",",
" \"type\":\"order\"",
" }',",
" '{{uuid:order_id}}',",
" '{{uuid}}',",
" 'create',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000'",
")"
],
[
"SELECT pg_notify('entity', '{",
" \"complete\":{",
" \"created_at\":\"{{timestamp}}\",",
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"first_name\":\"Const\",",
" \"id\":\"{{uuid:person_id}}\",",
" \"last_name\":\"Person\",",
" \"modified_at\":\"{{timestamp}}\",",
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"type\":\"person\"",
" },",
" \"new\":{",
" \"first_name\":\"Const\",",
" \"last_name\":\"Person\",",
" \"type\":\"person\"",
" }",
" }')"
],
[
"SELECT pg_notify('entity', '{",
" \"complete\":{",
" \"created_at\":\"{{timestamp}}\",",
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"id\":\"{{uuid:line1_id}}\",",
" \"modified_at\":\"{{timestamp}}\",",
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"order_id\":\"{{uuid:order_id}}\",",
" \"organization_id\":\"parent-org-id\",",
" \"type\":\"order_line\"",
" },",
" \"new\":{",
" \"order_id\":\"{{uuid:order_id}}\",",
" \"organization_id\":\"parent-org-id\",",
" \"type\":\"order_line\"",
" }",
" }')"
],
[
"SELECT pg_notify('entity', '{",
" \"complete\":{",
" \"created_at\":\"{{timestamp}}\",",
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"id\":\"{{uuid:line2_id}}\",",
" \"modified_at\":\"{{timestamp}}\",",
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"order_id\":\"{{uuid:order_id}}\",",
" \"organization_id\":\"explicit-org-id\",",
" \"type\":\"order_line\"",
" },",
" \"new\":{",
" \"order_id\":\"{{uuid:order_id}}\",",
" \"organization_id\":\"explicit-org-id\",",
" \"type\":\"order_line\"",
" }",
" }')"
],
[
"SELECT pg_notify('entity', '{",
" \"complete\":{",
" \"created_at\":\"{{timestamp}}\",",
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"customer\":{",
" \"created_at\":\"{{timestamp}}\",",
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"first_name\":\"Const\",",
" \"id\":\"{{uuid:person_id}}\",",
" \"last_name\":\"Person\",",
" \"modified_at\":\"{{timestamp}}\",",
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"type\":\"person\"",
" },",
" \"customer_id\":\"{{uuid:person_id}}\",",
" \"id\":\"{{uuid:order_id}}\",",
" \"lines\":[",
" {",
" \"created_at\":\"{{timestamp}}\",",
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"id\":\"{{uuid:line1_id}}\",",
" \"modified_at\":\"{{timestamp}}\",",
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"order_id\":\"{{uuid:order_id}}\",",
" \"organization_id\":\"parent-org-id\",",
" \"type\":\"order_line\"",
" },",
" {",
" \"created_at\":\"{{timestamp}}\",",
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"id\":\"{{uuid:line2_id}}\",",
" \"modified_at\":\"{{timestamp}}\",",
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"order_id\":\"{{uuid:order_id}}\",",
" \"organization_id\":\"explicit-org-id\",",
" \"type\":\"order_line\"",
" }",
" ],",
" \"modified_at\":\"{{timestamp}}\",",
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"organization_id\":\"parent-org-id\",",
" \"type\":\"order\"",
" },",
" \"new\":{",
" \"customer_id\":\"{{uuid:person_id}}\",",
" \"organization_id\":\"parent-org-id\",",
" \"type\":\"order\"",
" }",
" }')"
]
]
}
}
data[0]["tests"].append(test_case)
with open("fixtures/merger.json", "w") as f:
json.dump(data, f, indent=2)

View File

@ -40,7 +40,7 @@ impl Merger {
} }
}; };
let result = self.merge_internal(target_schema, data, &mut notifications_queue); let result = self.merge_internal(target_schema, data, &mut notifications_queue, None, false);
let val_resolved = match result { let val_resolved = match result {
Ok(val) => val, Ok(val) => val,
@ -129,33 +129,16 @@ impl Merger {
crate::drop::Drop::success_with_val(stripped_val) crate::drop::Drop::success_with_val(stripped_val)
} }
fn inject_organization_id(
relative: &mut serde_json::Map<String, Value>,
entity_fields: &serde_json::Map<String, Value>,
schema: &Arc<crate::database::schema::Schema>,
) {
if !relative.contains_key("organization_id") {
if let Some(org_id) = entity_fields.get("organization_id") {
if let Some(compiled_props) = schema.obj.compiled_properties.get() {
if let Some(org_schema) = compiled_props.get("organization_id") {
if org_schema.obj.const_.is_some() {
return;
}
}
}
relative.insert("organization_id".to_string(), org_id.clone());
}
}
}
pub(crate) fn merge_internal( pub(crate) fn merge_internal(
&self, &self,
mut schema: Arc<crate::database::schema::Schema>, mut schema: Arc<crate::database::schema::Schema>,
data: Value, data: Value,
notifications: &mut Vec<String>, notifications: &mut Vec<String>,
parent_org_id: Option<String>,
is_child: bool,
) -> Result<Value, String> { ) -> Result<Value, String> {
match data { match data {
Value::Array(items) => self.merge_array(schema, items, notifications), Value::Array(items) => self.merge_array(schema, items, notifications, parent_org_id, is_child),
Value::Object(map) => { Value::Object(map) => {
if let Some(options) = schema.obj.compiled_options.get() { if let Some(options) = schema.obj.compiled_options.get() {
if let Some(disc) = schema.obj.compiled_discriminator.get() { if let Some(disc) = schema.obj.compiled_discriminator.get() {
@ -163,9 +146,7 @@ impl Merger {
if let Some(v) = val { if let Some(v) = val {
if let Some((idx_opt, target_id_opt)) = options.get(v) { if let Some((idx_opt, target_id_opt)) = options.get(v) {
if let Some(target_id) = target_id_opt { if let Some(target_id) = target_id_opt {
if let Some(target_schema) = if let Some(target_schema) = self.db.schemas.get(target_id) {
self.db.schemas.get(target_id)
{
schema = target_schema.clone(); schema = target_schema.clone();
} else { } else {
return Err(format!( return Err(format!(
@ -204,7 +185,7 @@ impl Merger {
} }
} }
} }
self.merge_object(schema, map, notifications) self.merge_object(schema, map, notifications, parent_org_id, is_child)
} }
_ => Err("Invalid merge payload: root must be an Object or Array".to_string()), _ => Err("Invalid merge payload: root must be an Object or Array".to_string()),
} }
@ -215,6 +196,8 @@ impl Merger {
schema: Arc<crate::database::schema::Schema>, schema: Arc<crate::database::schema::Schema>,
items: Vec<Value>, items: Vec<Value>,
notifications: &mut Vec<String>, notifications: &mut Vec<String>,
parent_org_id: Option<String>,
is_child: bool,
) -> Result<Value, String> { ) -> Result<Value, String> {
let mut item_schema = schema.clone(); let mut item_schema = schema.clone();
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &schema.obj.type_ { if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &schema.obj.type_ {
@ -227,7 +210,7 @@ impl Merger {
let mut resolved_items = Vec::new(); let mut resolved_items = Vec::new();
for item in items { for item in items {
let resolved = self.merge_internal(item_schema.clone(), item, notifications)?; let resolved = self.merge_internal(item_schema.clone(), item, notifications, parent_org_id.clone(), is_child)?;
resolved_items.push(resolved); resolved_items.push(resolved);
} }
Ok(Value::Array(resolved_items)) Ok(Value::Array(resolved_items))
@ -238,6 +221,8 @@ impl Merger {
schema: Arc<crate::database::schema::Schema>, schema: Arc<crate::database::schema::Schema>,
obj: serde_json::Map<String, Value>, obj: serde_json::Map<String, Value>,
notifications: &mut Vec<String>, notifications: &mut Vec<String>,
parent_org_id: Option<String>,
is_child: bool,
) -> Result<Value, String> { ) -> Result<Value, String> {
let queue_start = notifications.len(); let queue_start = notifications.len();
@ -297,6 +282,20 @@ impl Merger {
} }
} }
let mut current_org_id = None;
if let Some(compiled_props) = schema.obj.compiled_properties.get() {
if let Some(org_schema) = compiled_props.get("organization_id") {
if let Some(c) = &org_schema.obj.const_ {
if let Some(c_str) = c.as_str() {
current_org_id = Some(c_str.to_string());
}
}
}
}
if current_org_id.is_none() {
current_org_id = parent_org_id.clone();
}
let user_id = self.db.auth_user_id()?; let user_id = self.db.auth_user_id()?;
let timestamp = self.db.timestamp()?; let timestamp = self.db.timestamp()?;
@ -311,6 +310,16 @@ impl Merger {
entity_change_kind = kind; entity_change_kind = kind;
entity_fetched = fetched; entity_fetched = fetched;
entity_replaces = replaces; entity_replaces = replaces;
if entity_change_kind.as_deref() == Some("create") {
if is_child {
if !entity_fields.contains_key("organization_id") {
if let Some(ref org_id) = current_org_id {
entity_fields.insert("organization_id".to_string(), Value::String(org_id.clone()));
}
}
}
}
} }
let mut entity_response = serde_json::Map::new(); let mut entity_response = serde_json::Map::new();
@ -331,13 +340,14 @@ impl Merger {
if let Some(relation) = self.db.relations.get(&edge.constraint) { if let Some(relation) = self.db.relations.get(&edge.constraint) {
let parent_is_source = edge.forward; let parent_is_source = edge.forward;
let org_id_to_pass = entity_fields.get("organization_id").and_then(|v| v.as_str()).map(|s| s.to_string());
if parent_is_source { if parent_is_source {
Self::inject_organization_id(&mut relative, &entity_fields, &rel_schema);
let mut merged_relative = match self.merge_internal( let mut merged_relative = match self.merge_internal(
rel_schema.clone(), rel_schema.clone(),
Value::Object(relative), Value::Object(relative),
notifications, notifications,
org_id_to_pass.clone(),
true,
)? { )? {
Value::Object(m) => m, Value::Object(m) => m,
_ => continue, _ => continue,
@ -353,8 +363,6 @@ impl Merger {
); );
entity_response.insert(relation_name, Value::Object(merged_relative)); entity_response.insert(relation_name, Value::Object(merged_relative));
} else { } else {
Self::inject_organization_id(&mut relative, &entity_fields, &rel_schema);
Self::apply_entity_relation( Self::apply_entity_relation(
&mut relative, &mut relative,
&relation.source_columns, &relation.source_columns,
@ -366,6 +374,8 @@ impl Merger {
rel_schema.clone(), rel_schema.clone(),
Value::Object(relative), Value::Object(relative),
notifications, notifications,
org_id_to_pass.clone(),
true,
)? { )? {
Value::Object(m) => m, Value::Object(m) => m,
_ => continue, _ => continue,
@ -385,6 +395,16 @@ impl Merger {
entity_change_kind = kind; entity_change_kind = kind;
entity_fetched = fetched; entity_fetched = fetched;
entity_replaces = replaces; entity_replaces = replaces;
if entity_change_kind.as_deref() == Some("create") {
if is_child {
if !entity_fields.contains_key("organization_id") {
if let Some(ref org_id) = current_org_id {
entity_fields.insert("organization_id".to_string(), Value::String(org_id.clone()));
}
}
}
}
} }
self.merge_entity_fields( self.merge_entity_fields(
@ -423,11 +443,10 @@ impl Merger {
} }
} }
let org_id_to_pass = entity_fields.get("organization_id").and_then(|v| v.as_str()).map(|s| s.to_string());
let mut relative_responses = Vec::new(); let mut relative_responses = Vec::new();
for relative_item_val in relative_arr { for relative_item_val in relative_arr {
if let Value::Object(mut relative_item) = relative_item_val { if let Value::Object(mut relative_item) = relative_item_val {
Self::inject_organization_id(&mut relative_item, &entity_fields, &item_schema);
Self::apply_entity_relation( Self::apply_entity_relation(
&mut relative_item, &mut relative_item,
&relation.source_columns, &relation.source_columns,
@ -439,6 +458,8 @@ impl Merger {
item_schema.clone(), item_schema.clone(),
Value::Object(relative_item), Value::Object(relative_item),
notifications, notifications,
org_id_to_pass.clone(),
true,
)? { )? {
Value::Object(m) => m, Value::Object(m) => m,
_ => continue, _ => continue,

View File

@ -1,88 +0,0 @@
import json
import re
# Read the test output
output = """
JSPG_SQL: INSERT INTO agreego."entity" ("created_at", "created_by", "id", "modified_at", "modified_by", "type") VALUES ('2026-03-10T00:00:00Z', '00000000-0000-0000-0000-000000000000', '734f0f6e-3408-4d18-a6d7-725400ff6b30', '2026-03-10T00:00:00Z', '00000000-0000-0000-0000-000000000000', 'person')
JSPG_SQL: INSERT INTO agreego."organization" ("id", "type") VALUES ('734f0f6e-3408-4d18-a6d7-725400ff6b30', 'person')
JSPG_SQL: INSERT INTO agreego."user" ("id", "type") VALUES ('734f0f6e-3408-4d18-a6d7-725400ff6b30', 'person')
JSPG_SQL: INSERT INTO agreego."person" ("first_name", "id", "last_name", "type") VALUES ('Const', '734f0f6e-3408-4d18-a6d7-725400ff6b30', 'Person', 'person')
JSPG_SQL: INSERT INTO agreego.change ("old", "new", entity_id, id, kind, modified_at, modified_by) VALUES (NULL, '{"first_name":"Const","last_name":"Person","type":"person"}', '734f0f6e-3408-4d18-a6d7-725400ff6b30', '7195460a-edff-4d0d-b137-c040616b9f27', 'create', '2026-03-10T00:00:00Z', '00000000-0000-0000-0000-000000000000')
JSPG_SQL: INSERT INTO agreego."entity" ("created_at", "created_by", "id", "modified_at", "modified_by", "organization_id", "type") VALUES ('2026-03-10T00:00:00Z', '00000000-0000-0000-0000-000000000000', '369e92ac-41c5-4d43-9286-c004edb96e76', '2026-03-10T00:00:00Z', '00000000-0000-0000-0000-000000000000', 'parent-org-id', 'order')
JSPG_SQL: INSERT INTO agreego."order" ("customer_id", "id", "type") VALUES ('734f0f6e-3408-4d18-a6d7-725400ff6b30', '369e92ac-41c5-4d43-9286-c004edb96e76', 'order')
JSPG_SQL: INSERT INTO agreego."entity" ("created_at", "created_by", "id", "modified_at", "modified_by", "organization_id", "type") VALUES ('2026-03-10T00:00:00Z', '00000000-0000-0000-0000-000000000000', '48e91d8d-99ef-4f74-b2e6-c98f9501bb7a', '2026-03-10T00:00:00Z', '00000000-0000-0000-0000-000000000000', 'parent-org-id', 'order_line')
JSPG_SQL: INSERT INTO agreego."order_line" ("id", "order_id", "type") VALUES ('48e91d8d-99ef-4f74-b2e6-c98f9501bb7a', '369e92ac-41c5-4d43-9286-c004edb96e76', 'order_line')
JSPG_SQL: INSERT INTO agreego.change ("old", "new", entity_id, id, kind, modified_at, modified_by) VALUES (NULL, '{"order_id":"369e92ac-41c5-4d43-9286-c004edb96e76","organization_id":"parent-org-id","type":"order_line"}', '48e91d8d-99ef-4f74-b2e6-c98f9501bb7a', '5ab5c99b-926a-4878-98a7-c531859d2ebe', 'create', '2026-03-10T00:00:00Z', '00000000-0000-0000-0000-000000000000')
JSPG_SQL: INSERT INTO agreego."entity" ("created_at", "created_by", "id", "modified_at", "modified_by", "organization_id", "type") VALUES ('2026-03-10T00:00:00Z', '00000000-0000-0000-0000-000000000000', 'b91b93b2-1f75-4be3-a731-88562d289997', '2026-03-10T00:00:00Z', '00000000-0000-0000-0000-000000000000', 'explicit-org-id', 'order_line')
JSPG_SQL: INSERT INTO agreego."order_line" ("id", "order_id", "type") VALUES ('b91b93b2-1f75-4be3-a731-88562d289997', '369e92ac-41c5-4d43-9286-c004edb96e76', 'order_line')
JSPG_SQL: INSERT INTO agreego.change ("old", "new", entity_id, id, kind, modified_at, modified_by) VALUES (NULL, '{"order_id":"369e92ac-41c5-4d43-9286-c004edb96e76","organization_id":"explicit-org-id","type":"order_line"}', 'b91b93b2-1f75-4be3-a731-88562d289997', 'ad35cf4e-d2de-4f87-aa3d-ec30101397ca', 'create', '2026-03-10T00:00:00Z', '00000000-0000-0000-0000-000000000000')
JSPG_SQL: INSERT INTO agreego.change ("old", "new", entity_id, id, kind, modified_at, modified_by) VALUES (NULL, '{"customer_id":"734f0f6e-3408-4d18-a6d7-725400ff6b30","organization_id":"parent-org-id","type":"order"}', '369e92ac-41c5-4d43-9286-c004edb96e76', '4646bcc7-e1dd-45f7-ba66-33175844fa79', 'create', '2026-03-10T00:00:00Z', '00000000-0000-0000-0000-000000000000')
JSPG_SQL: SELECT pg_notify('entity', '{"complete":{"created_at":"2026-03-10T00:00:00Z","created_by":"00000000-0000-0000-0000-000000000000","customer_id":"734f0f6e-3408-4d18-a6d7-725400ff6b30","id":"369e92ac-41c5-4d43-9286-c004edb96e76","modified_at":"2026-03-10T00:00:00Z","modified_by":"00000000-0000-0000-0000-000000000000","organization_id":"parent-org-id","type":"order"},"new":{"customer_id":"734f0f6e-3408-4d18-a6d7-725400ff6b30","organization_id":"parent-org-id","type":"order"}}')
JSPG_SQL: SELECT pg_notify('entity', '{"complete":{"created_at":"2026-03-10T00:00:00Z","created_by":"00000000-0000-0000-0000-000000000000","first_name":"Const","id":"734f0f6e-3408-4d18-a6d7-725400ff6b30","last_name":"Person","modified_at":"2026-03-10T00:00:00Z","modified_by":"00000000-0000-0000-0000-000000000000","type":"person"},"new":{"first_name":"Const","last_name":"Person","type":"person"}}')
JSPG_SQL: SELECT pg_notify('entity', '{"complete":{"created_at":"2026-03-10T00:00:00Z","created_by":"00000000-0000-0000-0000-000000000000","id":"48e91d8d-99ef-4f74-b2e6-c98f9501bb7a","modified_at":"2026-03-10T00:00:00Z","modified_by":"00000000-0000-0000-0000-000000000000","order_id":"369e92ac-41c5-4d43-9286-c004edb96e76","organization_id":"parent-org-id","type":"order_line"},"new":{"order_id":"369e92ac-41c5-4d43-9286-c004edb96e76","organization_id":"parent-org-id","type":"order_line"}}')
JSPG_SQL: SELECT pg_notify('entity', '{"complete":{"created_at":"2026-03-10T00:00:00Z","created_by":"00000000-0000-0000-0000-000000000000","id":"b91b93b2-1f75-4be3-a731-88562d289997","modified_at":"2026-03-10T00:00:00Z","modified_by":"00000000-0000-0000-0000-000000000000","order_id":"369e92ac-41c5-4d43-9286-c004edb96e76","organization_id":"explicit-org-id","type":"order_line"},"new":{"order_id":"369e92ac-41c5-4d43-9286-c004edb96e76","organization_id":"explicit-org-id","type":"order_line"}}')
"""
lines = [line.replace("JSPG_SQL: ", "").strip() for line in output.split("\n") if line.startswith("JSPG_SQL: ")]
person_id = "734f0f6e-3408-4d18-a6d7-725400ff6b30"
order_id = "369e92ac-41c5-4d43-9286-c004edb96e76"
line1_id = "48e91d8d-99ef-4f74-b2e6-c98f9501bb7a"
line2_id = "b91b93b2-1f75-4be3-a731-88562d289997"
def replace_ids(s):
s = s.replace(person_id, "{{uuid:person_id}}")
s = s.replace(order_id, "{{uuid:order_id}}")
s = s.replace(line1_id, "{{uuid:line1_id}}")
s = s.replace(line2_id, "{{uuid:line2_id}}")
s = re.sub(r"'[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}'", "'{{uuid}}'", s)
s = s.replace("'2026-03-10T00:00:00Z'", "'{{timestamp}}'")
s = s.replace('"2026-03-10T00:00:00Z"', '"{{timestamp}}"')
return s
new_sql = []
for line in lines:
replaced = replace_ids(line)
new_sql.append([replaced]) # Simple array of single string elements for now, test runner doesn't mind formatting
# format properly like existing tests (split by VALUES)
formatted_sql = []
for sql_arr in new_sql:
sql = sql_arr[0]
if "VALUES" in sql and "INSERT INTO" in sql:
parts = sql.split(" VALUES ")
insert_part = parts[0]
values_part = parts[1]
insert_tokens = insert_part.split(" (")
table = insert_tokens[0]
cols = insert_tokens[1][:-1].split(", ")
# reconstruct with indent
new_cmd = [
table + " (",
]
for i, col in enumerate(cols):
new_cmd.append(" " + col + ("," if i < len(cols) - 1 else ""))
new_cmd.append(")")
new_cmd.append("VALUES (")
vals = values_part[1:-1].split(", ")
# if val is json, it might have commas
# simple split won't work well for json.
# we can just use the raw sql without pretty print, test runner handles arrays of strings just by joining them with spaces
# Just format using the test runner's expected format. Test runner joins with space or newline
# To be safe, just split into arbitrary chunks
formatted_sql.append([sql])
with open("fixtures/merger.json", "r") as f:
data = json.load(f)
test_case = next(t for t in data[0]["tests"] if t["description"] == "Test organization_id syntactic sugar permutations")
test_case["expect"]["sql"] = formatted_sql
with open("fixtures/merger.json", "w") as f:
json.dump(data, f, indent=2)

View File

@ -1 +1 @@
1.0.136 1.0.139