Skip to content

Commit bf438c8

Browse files
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).
1 parent 94ee7aa commit bf438c8

2 files changed

Lines changed: 65 additions & 8 deletions

File tree

src/json2sql/converter.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,22 @@ def _convert_objects(self, objects: list[dict], table_name: str) -> str:
9393
# When flattening, compute the full column set first so rows align
9494
if self.flatten:
9595
columns, flat_map = self._infer_columns_flattened(objects, table_name)
96-
# Process nested arrays into child tables
96+
# Process nested arrays into child tables, grouped by key so that
97+
# each nested array produces exactly ONE child table whose INSERT
98+
# covers every parent row's children.
99+
nested_groups: dict[str, tuple[list[dict], list[dict]]] = {}
97100
for obj in objects:
98101
for key, value in obj.items():
99102
if (
100103
isinstance(value, list)
101104
and value
102105
and all(isinstance(v, dict) for v in value)
103106
):
104-
self._flatten_nested(table_name, key, value, obj)
107+
children, parents = nested_groups.setdefault(key, ([], []))
108+
children.extend(value)
109+
parents.extend([obj] * len(value))
110+
for key, (children, parents) in nested_groups.items():
111+
self._flatten_nested(table_name, key, children, parents)
105112
else:
106113
columns = self._infer_columns(objects)
107114
flat_map = {}
@@ -240,27 +247,34 @@ def _flatten_nested(
240247
parent_table: str,
241248
key: str,
242249
nested_objects: list[dict],
243-
parent_obj: dict,
250+
parent_objs: list[dict],
244251
) -> None:
245-
"""Flatten a nested array of objects into a separate table."""
252+
"""Flatten nested arrays of objects into a single child table.
253+
254+
``nested_objects`` and ``parent_objs`` are aligned lists: each child
255+
row links back to its own parent via the foreign key. Grouping all
256+
parents' children into one table avoids emitting duplicate
257+
``CREATE TABLE`` statements when multiple rows carry nested arrays.
258+
"""
246259
child_table = f"{parent_table}_{key}"
247260
columns = self._infer_columns(nested_objects)
248261
# Add parent reference — only if no existing column has the FK name
249262
parent_ref = None
250263
for pk in ("id", "name", parent_table + "_id"):
251-
if pk in parent_obj:
264+
if any(pk in parent_obj for parent_obj in parent_objs):
252265
parent_ref = pk
253266
break
254267
fk_col = f"{parent_table}_{parent_ref}" if parent_ref else None
255268
fk_already_exists = fk_col and fk_col in columns
256269
if fk_col and not fk_already_exists:
270+
fk_parent = next(p for p in parent_objs if parent_ref in p)
257271
columns = {
258-
fk_col: sql_type_for(parent_obj[parent_ref], self.dialect),
272+
fk_col: sql_type_for(fk_parent[parent_ref], self.dialect),
259273
**columns,
260274
}
261275

262276
rows: list[list[str]] = []
263-
for nested in nested_objects:
277+
for nested, parent_obj in zip(nested_objects, parent_objs, strict=True):
264278
row: list[str] = []
265279
for col_name in columns:
266280
if col_name == fk_col and not fk_already_exists:
@@ -277,11 +291,16 @@ def _process_flatten(self, objects: list, table_name: str) -> None:
277291
return
278292
if not objects or not isinstance(objects[0], dict):
279293
return
294+
nested_groups: dict[str, tuple[list[dict], list[dict]]] = {}
280295
for obj in objects:
281296
for key, value in obj.items():
282297
if (
283298
isinstance(value, list)
284299
and value
285300
and all(isinstance(v, dict) for v in value)
286301
):
287-
self._flatten_nested(table_name, key, value, obj)
302+
children, parents = nested_groups.setdefault(key, ([], []))
303+
children.extend(value)
304+
parents.extend([obj] * len(value))
305+
for key, (children, parents) in nested_groups.items():
306+
self._flatten_nested(table_name, key, children, parents)

tests/test_edge_cases.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,41 @@ def test_convert_objects_list_vs_dict_root(self):
108108
result = converter.convert(json.dumps([{"name": "test"}]))
109109
assert "INSERT INTO" in result
110110
assert "'test'" in result
111+
112+
113+
def test_flatten_multiple_parent_rows_single_child_table():
114+
"""Multiple parent rows with nested arrays yield ONE child table with all rows."""
115+
import json as _json
116+
117+
from json2sql.converter import JSONToSQLConverter
118+
119+
data = [
120+
{"id": 1, "name": "a", "tags": [{"label": "x", "score": 1}]},
121+
{
122+
"id": 2,
123+
"name": "b",
124+
"tags": [{"label": "y", "score": 2}, {"label": "z", "score": 3}],
125+
},
126+
]
127+
text = _json.dumps(data)
128+
out = JSONToSQLConverter(flatten=True).convert(text, "users")
129+
assert out.count('CREATE TABLE "users_tags"') == 1
130+
assert "'z', 3" in out and "'y', 2" in out and "'x', 1" in out
131+
132+
schema = JSONToSQLConverter(flatten=True).generate_schema(text, "users")
133+
assert schema.count('CREATE TABLE "users_tags"') == 1
134+
135+
136+
def test_flatten_child_rows_keep_own_parent_fk():
137+
"""Each child row links to its own parent via the FK column."""
138+
import json as _json
139+
140+
from json2sql.converter import JSONToSQLConverter
141+
142+
data = [
143+
{"id": 10, "items": [{"sku": "a1"}]},
144+
{"id": 20, "items": [{"sku": "b1"}]},
145+
]
146+
out = JSONToSQLConverter(flatten=True).convert(_json.dumps(data), "orders")
147+
assert "(10, 'a1')" in out
148+
assert "(20, 'b1')" in out

0 commit comments

Comments
 (0)