Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions src/json2sql/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,22 @@ 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 (
isinstance(value, list)
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 = {}
Expand Down Expand Up @@ -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:
Expand All @@ -277,11 +291,16 @@ 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 (
isinstance(value, list)
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)
38 changes: 38 additions & 0 deletions tests/test_edge_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading