From bf438c8e53af05900348ef9f28f391c5497a6f65 Mon Sep 17 00:00:00 2001 From: Jaixii Date: Sat, 22 Aug 2026 13:55:15 -0400 Subject: [PATCH] cowork-bot: dedupe nested-array child tables when multiple parent rows carry arrays Grouping children per key so convert()/generate_schema() emit exactly one CREATE TABLE per child table, with every child row linked to its own parent FK (previously duplicate CREATE TABLEs and dropped rows). --- src/json2sql/converter.py | 35 +++++++++++++++++++++++++++-------- tests/test_edge_cases.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/src/json2sql/converter.py b/src/json2sql/converter.py index a326f8c..c32d11b 100644 --- a/src/json2sql/converter.py +++ b/src/json2sql/converter.py @@ -93,7 +93,10 @@ def _convert_objects(self, objects: list[dict], table_name: str) -> str: # When flattening, compute the full column set first so rows align if self.flatten: columns, flat_map = self._infer_columns_flattened(objects, table_name) - # Process nested arrays into child tables + # Process nested arrays into child tables, grouped by key so that + # each nested array produces exactly ONE child table whose INSERT + # covers every parent row's children. + nested_groups: dict[str, tuple[list[dict], list[dict]]] = {} for obj in objects: for key, value in obj.items(): if ( @@ -101,7 +104,11 @@ def _convert_objects(self, objects: list[dict], table_name: str) -> str: and value and all(isinstance(v, dict) for v in value) ): - self._flatten_nested(table_name, key, value, obj) + children, parents = nested_groups.setdefault(key, ([], [])) + children.extend(value) + parents.extend([obj] * len(value)) + for key, (children, parents) in nested_groups.items(): + self._flatten_nested(table_name, key, children, parents) else: columns = self._infer_columns(objects) flat_map = {} @@ -240,27 +247,34 @@ def _flatten_nested( parent_table: str, key: str, nested_objects: list[dict], - parent_obj: dict, + parent_objs: list[dict], ) -> None: - """Flatten a nested array of objects into a separate table.""" + """Flatten nested arrays of objects into a single child table. + + ``nested_objects`` and ``parent_objs`` are aligned lists: each child + row links back to its own parent via the foreign key. Grouping all + parents' children into one table avoids emitting duplicate + ``CREATE TABLE`` statements when multiple rows carry nested arrays. + """ child_table = f"{parent_table}_{key}" columns = self._infer_columns(nested_objects) # Add parent reference — only if no existing column has the FK name parent_ref = None for pk in ("id", "name", parent_table + "_id"): - if pk in parent_obj: + if any(pk in parent_obj for parent_obj in parent_objs): parent_ref = pk break fk_col = f"{parent_table}_{parent_ref}" if parent_ref else None fk_already_exists = fk_col and fk_col in columns if fk_col and not fk_already_exists: + fk_parent = next(p for p in parent_objs if parent_ref in p) columns = { - fk_col: sql_type_for(parent_obj[parent_ref], self.dialect), + fk_col: sql_type_for(fk_parent[parent_ref], self.dialect), **columns, } rows: list[list[str]] = [] - for nested in nested_objects: + for nested, parent_obj in zip(nested_objects, parent_objs, strict=True): row: list[str] = [] for col_name in columns: if col_name == fk_col and not fk_already_exists: @@ -277,6 +291,7 @@ def _process_flatten(self, objects: list, table_name: str) -> None: return if not objects or not isinstance(objects[0], dict): return + nested_groups: dict[str, tuple[list[dict], list[dict]]] = {} for obj in objects: for key, value in obj.items(): if ( @@ -284,4 +299,8 @@ def _process_flatten(self, objects: list, table_name: str) -> None: and value and all(isinstance(v, dict) for v in value) ): - self._flatten_nested(table_name, key, value, obj) + children, parents = nested_groups.setdefault(key, ([], [])) + children.extend(value) + parents.extend([obj] * len(value)) + for key, (children, parents) in nested_groups.items(): + self._flatten_nested(table_name, key, children, parents) diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py index 5ac5b73..fa32f7e 100644 --- a/tests/test_edge_cases.py +++ b/tests/test_edge_cases.py @@ -108,3 +108,41 @@ def test_convert_objects_list_vs_dict_root(self): result = converter.convert(json.dumps([{"name": "test"}])) assert "INSERT INTO" in result assert "'test'" in result + + +def test_flatten_multiple_parent_rows_single_child_table(): + """Multiple parent rows with nested arrays yield ONE child table with all rows.""" + import json as _json + + from json2sql.converter import JSONToSQLConverter + + data = [ + {"id": 1, "name": "a", "tags": [{"label": "x", "score": 1}]}, + { + "id": 2, + "name": "b", + "tags": [{"label": "y", "score": 2}, {"label": "z", "score": 3}], + }, + ] + text = _json.dumps(data) + out = JSONToSQLConverter(flatten=True).convert(text, "users") + assert out.count('CREATE TABLE "users_tags"') == 1 + assert "'z', 3" in out and "'y', 2" in out and "'x', 1" in out + + schema = JSONToSQLConverter(flatten=True).generate_schema(text, "users") + assert schema.count('CREATE TABLE "users_tags"') == 1 + + +def test_flatten_child_rows_keep_own_parent_fk(): + """Each child row links to its own parent via the FK column.""" + import json as _json + + from json2sql.converter import JSONToSQLConverter + + data = [ + {"id": 10, "items": [{"sku": "a1"}]}, + {"id": 20, "items": [{"sku": "b1"}]}, + ] + out = JSONToSQLConverter(flatten=True).convert(_json.dumps(data), "orders") + assert "(10, 'a1')" in out + assert "(20, 'b1')" in out