Skip to content

Commit 335c624

Browse files
author
DevForge Engineer
committed
cowork-bot: surface silently-dropped SQL constraints and malformed type overrides as warnings
1 parent d9496e5 commit 335c624

3 files changed

Lines changed: 108 additions & 5 deletions

File tree

‎src/schemaforge/generators/json_schema_generator.py‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from __future__ import annotations
88

99
import json
10+
import warnings
1011
from typing import Any
1112

1213
from ..ir import Column, ColumnType, Schema, Table
@@ -130,7 +131,15 @@ def _column_to_prop(self, col: Column) -> dict[str, Any]:
130131
prop = _json.loads(overridden)
131132
return self._add_base_annotations(prop, col)
132133
except (json.JSONDecodeError, ValueError):
133-
pass
134+
# A malformed override is a silent-failure trap: the
135+
# generator would fall back to a plain type string with
136+
# no indication the JSON override was discarded.
137+
warnings.warn(
138+
f"json_schema generator: type override for column "
139+
f"'{col.name}' looks like JSON but failed to parse; "
140+
f"using it as a plain type instead",
141+
stacklevel=2,
142+
)
134143
prop["type"] = overridden
135144
return self._add_base_annotations(prop, col)
136145

‎src/schemaforge/parsers/sql_parser.py‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import contextlib
66
import re
7+
import warnings
78
from typing import Any
89

910
from ..ir import Column, ColumnType, EnumType, Index, Schema, Table
@@ -146,10 +147,17 @@ def _parse_create_table(self, stmt: str) -> Table | None:
146147
table.indexes.append(idx)
147148
elif upper.startswith("PRIMARY KEY"):
148149
pass # PK handled via column constraints
149-
elif upper.startswith("CONSTRAINT"):
150-
pass # Foreign keys, etc.
151-
elif upper.startswith("FOREIGN KEY") or upper.startswith("CHECK"):
152-
pass
150+
elif upper.startswith("CONSTRAINT") or upper.startswith(
151+
"FOREIGN KEY"
152+
) or upper.startswith("CHECK"):
153+
# Silent drops here are a correctness trap: schemas that differ
154+
# only in FK/CHECK constraints would compare as equivalent.
155+
# Surface the loss instead of swallowing it.
156+
warnings.warn(
157+
f"SQL parser: ignored unsupported table constraint in "
158+
f"'{table.name}': {defn[:60]}",
159+
stacklevel=3,
160+
)
153161
else:
154162
col = self._parse_column_def(defn)
155163
if col:

‎tests/test_silent_failures.py‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""Silent-failure regression tests: unsupported constructs must warn, not vanish."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
import warnings
7+
from schemaforge.generators.json_schema_generator import JSONSchemaGenerator
8+
from schemaforge.parsers.sql_parser import SQLParser
9+
from schemaforge.type_config import TypeConfig
10+
11+
12+
def test_sql_parser_warns_on_foreign_key_constraint() -> None:
13+
sql = """
14+
CREATE TABLE orders (
15+
id INTEGER PRIMARY KEY,
16+
customer_id INTEGER,
17+
FOREIGN KEY (customer_id) REFERENCES customers(id)
18+
);
19+
"""
20+
with warnings.catch_warnings(record=True) as caught:
21+
warnings.simplefilter("always")
22+
SQLParser().parse(sql)
23+
assert any("FOREIGN KEY" in str(w.message) for w in caught)
24+
25+
26+
def test_sql_parser_warns_on_named_check_constraint() -> None:
27+
sql = """
28+
CREATE TABLE products (
29+
price NUMERIC,
30+
CONSTRAINT positive_price CHECK (price >= 0)
31+
);
32+
"""
33+
with warnings.catch_warnings(record=True) as caught:
34+
warnings.simplefilter("always")
35+
SQLParser().parse(sql)
36+
assert any("CHECK" in str(w.message) for w in caught)
37+
38+
39+
def test_sql_parser_no_warning_for_plain_table() -> None:
40+
sql = """
41+
CREATE TABLE users (
42+
id INTEGER PRIMARY KEY,
43+
email VARCHAR(255) NOT NULL
44+
);
45+
"""
46+
with warnings.catch_warnings(record=True) as caught:
47+
warnings.simplefilter("always")
48+
SQLParser().parse(sql)
49+
assert not caught
50+
51+
52+
def test_json_schema_generator_warns_on_malformed_override() -> None:
53+
from schemaforge.ir import Column, ColumnType
54+
55+
class MalformedOverrideCfg(TypeConfig):
56+
"""Returns an override that looks like JSON but fails to parse."""
57+
58+
def get_override(self, col, fmt, type_args=None): # noqa: D102
59+
return "{broken json}"
60+
61+
col = Column(name="email", type=ColumnType.STRING, nullable=True)
62+
gen = JSONSchemaGenerator(type_config=MalformedOverrideCfg())
63+
with warnings.catch_warnings(record=True) as caught:
64+
warnings.simplefilter("always")
65+
gen._column_to_prop(col)
66+
assert any("failed to parse" in str(w.message) for w in caught)
67+
68+
69+
def _make_column(name: str):
70+
from schemaforge.ir import Column, ColumnType
71+
72+
return Column(name=name, type=ColumnType.STRING, nullable=True)
73+
74+
75+
@pytest.mark.parametrize(
76+
"sql",
77+
[
78+
"CREATE TABLE t (a INT, CONSTRAINT fk_a FOREIGN KEY (a) REFERENCES o(a));",
79+
"CREATE TABLE t (a INT, FOREIGN KEY (a) REFERENCES o(a));",
80+
],
81+
)
82+
def test_warning_includes_table_name(sql: str) -> None:
83+
with warnings.catch_warnings(record=True) as caught:
84+
warnings.simplefilter("always")
85+
SQLParser().parse(sql)
86+
assert any("'t'" in str(w.message) for w in caught)

0 commit comments

Comments
 (0)