Skip to content

Commit 6dba4dc

Browse files
cowork-bot: fix FK column detection in flatten mode
When flattening nested arrays, the FK column in the child table must match the parent table's primary key column name and type. Previously the code preferred 'name' over explicit ID fields like 'user_id' or 'users_id', causing a type mismatch (TEXT FK vs INTEGER PK). New priority order for parent reference key: 1. 'id' (generic primary key) 2. '{parent_table}_id' (table-specific, e.g., 'users_id') 3. Any key ending in '_id' found in parent objects (e.g., 'user_id') 4. 'name' (fallback only when no ID-like field exists) Added 12 regression tests covering all three dialects (Postgres, MySQL, SQLite).
1 parent bf438c8 commit 6dba4dc

8 files changed

Lines changed: 87 additions & 158 deletions

File tree

src/json2sql.egg-info/PKG-INFO

Lines changed: 0 additions & 129 deletions
This file was deleted.

src/json2sql.egg-info/SOURCES.txt

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/json2sql.egg-info/dependency_links.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/json2sql.egg-info/entry_points.txt

Lines changed: 0 additions & 2 deletions
This file was deleted.

src/json2sql.egg-info/requires.txt

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/json2sql.egg-info/top_level.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/json2sql/converter.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,9 +258,25 @@ def _flatten_nested(
258258
"""
259259
child_table = f"{parent_table}_{key}"
260260
columns = self._infer_columns(nested_objects)
261-
# Add parent reference — only if no existing column has the FK name
261+
# Add parent reference — only if no existing column has the FK name.
262+
# Prefer explicit ID fields over generic "name" to ensure the FK column
263+
# type matches the parent table's primary key type.
262264
parent_ref = None
263-
for pk in ("id", "name", parent_table + "_id"):
265+
# Priority order for parent reference key:
266+
# 1. "id" (generic primary key)
267+
# 2. "{parent_table}_id" (table-specific, e.g., "users_id")
268+
# 3. Any key ending in "_id" found in parent objects (e.g., "user_id")
269+
# 4. "name" (fallback only when no ID-like field exists)
270+
candidate_keys = ["id", f"{parent_table}_id"]
271+
# Add any *_id keys found in parent objects (excluding already listed)
272+
seen = set(candidate_keys)
273+
for obj in parent_objs:
274+
for k in obj:
275+
if k.endswith("_id") and k not in seen:
276+
candidate_keys.append(k)
277+
seen.add(k)
278+
candidate_keys.append("name")
279+
for pk in candidate_keys:
264280
if any(pk in parent_obj for parent_obj in parent_objs):
265281
parent_ref = pk
266282
break

tests/test_type_inference.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,72 @@ def test_convert_never_emits_empty_column_list():
131131
)
132132
assert "();" not in out
133133
assert "INSERT INTO" in out
134+
135+
136+
class TestFlattenFKDetection:
137+
"""Tests for correct FK column detection in flatten mode.
138+
139+
The FK column in a child table must match the parent table's primary key
140+
column name and type. Previously the code preferred "name" over explicit
141+
ID fields like "user_id" or "users_id", causing a type mismatch.
142+
"""
143+
144+
@pytest.mark.parametrize("dialect", [Dialect.POSTGRES, Dialect.MYSQL, Dialect.SQLITE])
145+
def test_flatten_prefers_id_over_name(self, dialect):
146+
"""When parent has both 'id' and 'name', 'id' should be used for FK."""
147+
conv = JSONToSQLConverter(dialect=dialect, flatten=True)
148+
data = json.dumps([
149+
{"id": 1, "name": "Alice", "tags": [{"label": "x"}]},
150+
{"id": 2, "name": "Bob", "tags": [{"label": "y"}]},
151+
])
152+
out = conv.convert(data, table_name="users")
153+
# FK column should be users_id (from parent's id), not users_name
154+
assert '"users_id"' in out or '`users_id`' in out
155+
assert '"users_name"' not in out and '`users_name`' not in out
156+
# Parent table should have id column
157+
assert '"id"' in out or '`id`' in out
158+
159+
@pytest.mark.parametrize("dialect", [Dialect.POSTGRES, Dialect.MYSQL, Dialect.SQLITE])
160+
def test_flatten_prefers_table_specific_id(self, dialect):
161+
"""When parent has '{table}_id' (e.g., users_id), it should be used."""
162+
conv = JSONToSQLConverter(dialect=dialect, flatten=True)
163+
data = json.dumps([
164+
{"users_id": 10, "name": "Alice", "tags": [{"label": "x"}]},
165+
{"users_id": 20, "name": "Bob", "tags": [{"label": "y"}]},
166+
])
167+
out = conv.convert(data, table_name="users")
168+
# FK column should be users_users_id (from parent's users_id)
169+
assert '"users_users_id"' in out or '`users_users_id`' in out
170+
assert '"users_name"' not in out and '`users_name`' not in out
171+
172+
@pytest.mark.parametrize("dialect", [Dialect.POSTGRES, Dialect.MYSQL, Dialect.SQLITE])
173+
def test_flatten_prefers_any_id_suffix(self, dialect):
174+
"""When parent has a singular '*_id' (e.g., user_id), it should be used over 'name'."""
175+
conv = JSONToSQLConverter(dialect=dialect, flatten=True)
176+
data = json.dumps([
177+
{"user_id": 100, "name": "Alice", "tags": [{"label": "x"}]},
178+
{"user_id": 200, "name": "Bob", "tags": [{"label": "y"}]},
179+
])
180+
out = conv.convert(data, table_name="users")
181+
# FK column should be users_user_id (from parent's user_id)
182+
assert '"users_user_id"' in out or '`users_user_id`' in out
183+
assert '"users_name"' not in out and '`users_name`' not in out
184+
# FK type should be numeric (matching parent's user_id type)
185+
if dialect == Dialect.MYSQL:
186+
assert "INT" in out
187+
else:
188+
assert "INTEGER" in out
189+
190+
@pytest.mark.parametrize("dialect", [Dialect.POSTGRES, Dialect.MYSQL, Dialect.SQLITE])
191+
def test_flatten_fallback_to_name_when_no_id(self, dialect):
192+
"""When parent has no ID-like field, 'name' is used as fallback."""
193+
conv = JSONToSQLConverter(dialect=dialect, flatten=True)
194+
data = json.dumps([
195+
{"name": "Alice", "tags": [{"label": "x"}]},
196+
{"name": "Bob", "tags": [{"label": "y"}]},
197+
])
198+
out = conv.convert(data, table_name="users")
199+
# FK column should be users_name (fallback)
200+
assert '"users_name"' in out or '`users_name`' in out
201+
# Parent table should have name column
202+
assert '"name"' in out or '`name`' in out

0 commit comments

Comments
 (0)