Skip to content

Commit e2a4fd7

Browse files
authored
Issue 149 named foreign keys and views (#332)
* Handle invalid DDL errors for silent false * Support named foreign keys and view statements * Document named foreign keys and view statements * Keep issue 149 changes unreleased
1 parent d546768 commit e2a4fd7

9 files changed

Lines changed: 457 additions & 278 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ The format is based on Keep a Changelog 1.0.0, and this project adheres to Seman
1111
- None.
1212

1313
### Fixed
14+
- MySQL-style `ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY constraint_name (...) REFERENCES ...` statements now parse correctly instead of failing on the duplicated foreign key name. `ALTER TABLE ... DROP FOREIGN KEY ...` is also supported, and simple `DROP VIEW` / `CREATE VIEW ... AS ...` statements are now recognized in parser output. https://github.com/xnuinside/simple-ddl-parser/issues/149
1415
- HQL primitive generic array types like `array<string>` now parse without failing on the closing `>` token. https://github.com/xnuinside/simple-ddl-parser/issues/192
1516
- `TRUNCATE TABLE schema.table` statements now return the affected table in parser output instead of being skipped. https://github.com/xnuinside/simple-ddl-parser/issues/190
1617

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ In output you will have names like 'dbo' and 'TO_Requests', not '[dbo]' and '[TO
385385

386386
- STATEMENTS: PRIMARY KEY, CHECK, FOREIGN KEY in table definitions (in create table();)
387387

388-
- ALTER TABLE STATEMENTS: ADD CHECK (with CONSTRAINT), ADD FOREIGN KEY (with CONSTRAINT), ADD UNIQUE, ADD DEFAULT FOR, ALTER TABLE ONLY, ALTER TABLE IF EXISTS; ALTER .. PRIMARY KEY; ALTER .. USING INDEX TABLESPACE; ALTER .. ADD; ALTER .. MODIFY; ALTER .. ALTER COLUMN; multiple ADD / DROP / MODIFY operations in one statement; etc
388+
- ALTER TABLE STATEMENTS: ADD CHECK (with CONSTRAINT), ADD FOREIGN KEY (with CONSTRAINT), MySQL-style `ADD CONSTRAINT ... FOREIGN KEY constraint_name (...)`, DROP FOREIGN KEY, ADD UNIQUE, ADD DEFAULT FOR, ALTER TABLE ONLY, ALTER TABLE IF EXISTS; ALTER .. PRIMARY KEY; ALTER .. USING INDEX TABLESPACE; ALTER .. ADD; ALTER .. MODIFY; ALTER .. ALTER COLUMN; multiple ADD / DROP / MODIFY operations in one statement; etc
389389

390390
- PARTITION BY statement
391391

@@ -409,6 +409,8 @@ In output you will have names like 'dbo' and 'TO_Requests', not '[dbo]' and '[TO
409409

410410
- CREATE DATABASE [IF NOT EXISTS] + Properties parsing
411411

412+
- DROP VIEW and simple CREATE VIEW ... AS ... statements
413+
412414
### SparkSQL Dialect statements
413415

414416
- USING
@@ -584,7 +586,9 @@ The format is based on Keep a Changelog 1.0.0, and this project adheres to Seman
584586
- None.
585587

586588
### Fixed
587-
- None.
589+
- MySQL-style `ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY constraint_name (...) REFERENCES ...` statements now parse correctly instead of failing on the duplicated foreign key name. `ALTER TABLE ... DROP FOREIGN KEY ...` is also supported, and simple `DROP VIEW` / `CREATE VIEW ... AS ...` statements are now recognized in parser output. https://github.com/xnuinside/simple-ddl-parser/issues/149
590+
- HQL primitive generic array types like `array<string>` now parse without failing on the closing `>` token. https://github.com/xnuinside/simple-ddl-parser/issues/192
591+
- `TRUNCATE TABLE schema.table` statements now return the affected table in parser output instead of being skipped. https://github.com/xnuinside/simple-ddl-parser/issues/190
588592

589593
## [1.12.0] - 2026-03-27
590594
### Added

simple_ddl_parser/dialects/sql.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,7 @@ class AlterTable:
878878
def p_expression_alter(self, p: List) -> None:
879879
"""expr : alter_foreign ref
880880
| alter_drop_column
881+
| alter_drop_foreign
881882
| alter_check
882883
| alter_unique
883884
| alter_default
@@ -915,6 +916,16 @@ def p_alter_drop_column(self, p: List) -> None:
915916
p[0]["columns_to_drop"] = []
916917
p[0]["columns_to_drop"].append(p_list[-1])
917918

919+
def p_alter_drop_foreign(self, p: List) -> None:
920+
"""alter_drop_foreign : alt_table DROP FOREIGN KEY id
921+
| alter_drop_foreign COMMA DROP FOREIGN KEY id
922+
"""
923+
p[0] = p[1]
924+
p_list = list(p)
925+
if not p[0].get("foreign_keys_to_drop"):
926+
p[0]["foreign_keys_to_drop"] = []
927+
p[0]["foreign_keys_to_drop"].append(p_list[-1])
928+
918929
def p_alter_rename_column(self, p: List) -> None:
919930
"""alter_rename_column : alt_table RENAME COLUMN id id id"""
920931
p[0] = p[1]
@@ -2055,9 +2066,10 @@ def p_index_pid(self, p: List) -> None:
20552066
def p_foreign(self, p):
20562067
# todo: need to redone id lists
20572068
"""foreign : FOREIGN KEY LP pid RP
2069+
| FOREIGN KEY id LP pid RP
20582070
| FOREIGN KEY"""
20592071
p_list = remove_par(list(p))
2060-
if len(p_list) == 4:
2072+
if isinstance(p_list[-1], list):
20612073
columns = p_list[-1]
20622074
p[0] = columns
20632075

simple_ddl_parser/output/base_data.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,10 @@ def append_statement_information_to_table(self, statement: Dict) -> None:
262262
self.alter_rename_columns(statement)
263263
elif "columns_to_drop" in statement:
264264
self.alter_drop_columns(statement)
265+
elif "foreign_keys_to_drop" in statement:
266+
if not self.alter.get("foreign_keys_to_drop"):
267+
self.alter["foreign_keys_to_drop"] = []
268+
self.alter["foreign_keys_to_drop"].extend(statement["foreign_keys_to_drop"])
265269
elif "columns_to_modify" in statement:
266270
self.alter_modify_columns(statement)
267271
elif "check" in statement:

simple_ddl_parser/output/core.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ def group_by_type_result(self) -> None:
102102
"sequences": [],
103103
"domains": [],
104104
"schemas": [],
105+
"views": [],
106+
"drop_views": [],
105107
"ddl_properties": [],
106108
"comments": [],
107109
"comment_on": [],
@@ -112,6 +114,8 @@ def group_by_type_result(self) -> None:
112114
"type_name": "types",
113115
"domain_name": "domains",
114116
"schema_name": "schemas",
117+
"view_name": "views",
118+
"drop_view_name": "drop_views",
115119
"tablespace_name": "tablespaces",
116120
"database_name": "databases",
117121
"value": "ddl_properties",
@@ -134,6 +138,10 @@ def group_by_type_result(self) -> None:
134138
del result_as_dict["comments"]
135139
if not result_as_dict["comment_on"]:
136140
del result_as_dict["comment_on"]
141+
if not result_as_dict["views"]:
142+
del result_as_dict["views"]
143+
if not result_as_dict["drop_views"]:
144+
del result_as_dict["drop_views"]
137145

138146
self.final_result = result_as_dict
139147

simple_ddl_parser/parser.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,28 @@
3939
flags=re.IGNORECASE | re.DOTALL | re.VERBOSE,
4040
)
4141

42+
CREATE_VIEW_RE = re.compile(
43+
r"""
44+
^\s*CREATE\s+
45+
(?P<replace>OR\s+REPLACE\s+)?
46+
VIEW\s+
47+
(?P<target>[^\s(]+)
48+
\s+AS\s+
49+
(?P<definition>.+?)
50+
\s*$
51+
""",
52+
flags=re.IGNORECASE | re.DOTALL | re.VERBOSE,
53+
)
54+
55+
DROP_VIEW_RE = re.compile(
56+
r"""
57+
^\s*DROP\s+VIEW\s+
58+
(?P<target>[^\s;]+)
59+
\s*$
60+
""",
61+
flags=re.IGNORECASE | re.DOTALL | re.VERBOSE,
62+
)
63+
4264

4365
def set_logging_config(
4466
log_level: Union[str, int], log_file: Optional[str] = None
@@ -427,6 +449,27 @@ def parse_create_table_as_select_statement(self, statement: str) -> Optional[Dic
427449
}
428450
}
429451

452+
def parse_create_view_statement(self, statement: str) -> Optional[Dict]:
453+
match = CREATE_VIEW_RE.match(statement)
454+
if not match:
455+
return None
456+
457+
schema, view_name = self.split_table_identifier(match.group("target"))
458+
return {
459+
"schema": schema,
460+
"view_name": view_name,
461+
"definition": match.group("definition").strip(),
462+
"replace": bool(match.group("replace")),
463+
}
464+
465+
def parse_drop_view_statement(self, statement: str) -> Optional[Dict]:
466+
match = DROP_VIEW_RE.match(statement)
467+
if not match:
468+
return None
469+
470+
schema, view_name = self.split_table_identifier(match.group("target"))
471+
return {"schema": schema, "drop_view_name": view_name}
472+
430473
@staticmethod
431474
def clone_create_table_as_select_columns(
432475
source_table: Dict, select_columns: Union[str, List[Dict[str, str]]]
@@ -585,6 +628,14 @@ def parse_statement(self) -> None:
585628
if create_table_as_select_statement:
586629
self.tables.append(create_table_as_select_statement)
587630
return
631+
create_view_statement = self.parse_create_view_statement(self.statement)
632+
if create_view_statement:
633+
self.tables.append(create_view_statement)
634+
return
635+
drop_view_statement = self.parse_drop_view_statement(self.statement)
636+
if drop_view_statement:
637+
self.tables.append(drop_view_statement)
638+
return
588639
_parse_result = yacc.parse(self.statement)
589640
if _parse_result:
590641
if (

simple_ddl_parser/parsetab.py

Lines changed: 279 additions & 275 deletions
Large diffs are not rendered by default.

tests/non_statement_tests/test_common.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,74 @@ def test_silent_false_flag():
3535
assert "Unknown statement" in e.value[1]
3636

3737

38+
def test_silent_false_mysql_named_foreign_key_in_alter():
39+
ddl = """
40+
CREATE TABLE parent (
41+
id int PRIMARY KEY
42+
);
43+
44+
CREATE TABLE child (
45+
parent_id int
46+
);
47+
48+
ALTER TABLE child ADD CONSTRAINT fk_child_parent FOREIGN KEY fk_child_parent (parent_id) REFERENCES parent (id);
49+
"""
50+
51+
result = DDLParser(ddl, silent=False).run(output_mode="mysql")
52+
53+
assert result[1]["table_name"] == "child"
54+
assert result[1]["alter"]["columns"] == [
55+
{
56+
"name": "parent_id",
57+
"constraint_name": "fk_child_parent",
58+
"references": {
59+
"column": "id",
60+
"table": "parent",
61+
"schema": None,
62+
"on_update": None,
63+
"on_delete": None,
64+
"deferrable_initially": None,
65+
},
66+
}
67+
]
68+
69+
70+
def test_create_and_drop_view_statements():
71+
ddl = """
72+
DROP VIEW reporting.user_ip_address_view;
73+
CREATE VIEW reporting.user_ip_address_view AS SELECT id, name FROM users;
74+
"""
75+
76+
result = DDLParser(ddl, silent=False).run(group_by_type=True)
77+
78+
assert result["drop_views"] == [
79+
{"schema": "reporting", "drop_view_name": "user_ip_address_view"}
80+
]
81+
assert result["views"] == [
82+
{
83+
"schema": "reporting",
84+
"view_name": "user_ip_address_view",
85+
"definition": "SELECT id , name FROM users",
86+
"replace": False,
87+
}
88+
]
89+
90+
91+
def test_alter_drop_foreign_key_statement():
92+
ddl = """
93+
CREATE TABLE child (
94+
parent_id int
95+
);
96+
97+
ALTER TABLE child DROP FOREIGN KEY fk_child_parent;
98+
"""
99+
100+
result = DDLParser(ddl, silent=False).run(output_mode="mysql")
101+
102+
assert result[0]["table_name"] == "child"
103+
assert result[0]["alter"]["foreign_keys_to_drop"] == ["fk_child_parent"]
104+
105+
38106
def test_flag_normalize_names():
39107
ddl = (
40108
ddl

tests/test_read_from_file.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,33 @@ def test_parse_from_file_one_table():
8484
)
8585

8686

87+
def test_parse_from_file_mysql_named_foreign_key_with_silent_false(tmp_path):
88+
ddl_file = tmp_path / "named_fk.sql"
89+
ddl_file.write_text(
90+
"""
91+
CREATE TABLE parent (
92+
id int PRIMARY KEY
93+
);
94+
95+
CREATE TABLE child (
96+
parent_id int
97+
);
98+
99+
ALTER TABLE child ADD CONSTRAINT fk_child_parent FOREIGN KEY fk_child_parent (parent_id) REFERENCES parent (id);
100+
""",
101+
encoding="utf-8",
102+
)
103+
104+
result = parse_from_file(
105+
str(ddl_file),
106+
parser_settings={"silent": False},
107+
output_mode="mysql",
108+
)
109+
110+
assert result[1]["table_name"] == "child"
111+
assert result[1]["alter"]["columns"][0]["constraint_name"] == "fk_child_parent"
112+
113+
87114
def test_parse_from_file_two_statements():
88115
expected = [
89116
{

0 commit comments

Comments
 (0)