Skip to content
Merged
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
29 changes: 28 additions & 1 deletion pgdevkit/migrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,14 @@ def default_tracking_table(start: Path | None = None, dialect: str | Dialect = "


def _split_sql(sql: str) -> list[str]:
"""Split SQL on semicolons, ignoring those inside comments, '...' strings, or $$...$$ blocks."""
"""Split SQL on semicolons, ignoring those inside comments (`--` and nested `/* */`), '...' strings, "..." identifiers, or $$...$$ blocks."""
stmts: list[str] = []
buf: list[str] = []
i = 0
in_string = False
in_identifier = False
in_line_comment = False
block_depth = 0 # Postgres block comments nest
dollar_tag: str | None = None

while i < len(sql):
Expand All @@ -92,6 +94,19 @@ def _split_sql(sql: str) -> list[str]:
if c == "\n":
in_line_comment = False
buf.append(c)
elif block_depth:
two = sql[i:i + 2]
if two == "/*":
block_depth += 1
elif two == "*/":
block_depth -= 1
else:
buf.append(c)
i += 1
continue
buf.append(two)
i += 2
continue
elif dollar_tag is not None:
buf.append(c)
if c == "$" and sql[i:i + len(dollar_tag)] == dollar_tag:
Expand All @@ -108,9 +123,21 @@ def _split_sql(sql: str) -> list[str]:
elif c == "'":
in_string = False
buf.append(c)
elif in_identifier:
if c == '"':
in_identifier = False # an escaped "" just closes and immediately reopens
buf.append(c)
elif c == '"':
in_identifier = True
buf.append(c)
elif c == "-" and i + 1 < len(sql) and sql[i + 1] == "-":
in_line_comment = True
buf.append(c)
elif c == "/" and i + 1 < len(sql) and sql[i + 1] == "*":
block_depth = 1
buf.append("/*")
i += 2
continue
elif c == "$":
m = re.match(r"\$([A-Za-z0-9_]*)\$", sql[i:])
if m:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ packages = ["pgdevkit"]

[project]
name = "pgdevkit"
version = "0.17.0"
version = "0.17.1"
description = "A helper for developing with Postgres"
readme = "README.md"
requires-python = ">=3.14"
Expand Down
23 changes: 23 additions & 0 deletions tests/test_migrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,29 @@ def test_created_table_names_falls_back_to_regex_for_unparseable_statements():
assert _created_table_names(_split_sql(sql)) == ["app.dynamic_table"]


def test_split_sql_ignores_semicolon_in_block_comment():
sql = "create view v as select 1 /* wins; ordered by id */ as a;"
assert _split_sql(sql) == ["create view v as select 1 /* wins; ordered by id */ as a"]


def test_split_sql_ignores_semicolon_in_nested_block_comment():
sql = "select 1 /* outer; /* inner; */ still; comment */ as a; select 2;"
assert _split_sql(sql) == ["select 1 /* outer; /* inner; */ still; comment */ as a", "select 2"]


def test_split_sql_ignores_semicolon_in_multiline_block_comment():
sql = "select 1 /* first;\n-- not a line comment; ' quote\nlast; */ as a;\nselect 2;"
assert _split_sql(sql) == ["select 1 /* first;\n-- not a line comment; ' quote\nlast; */ as a", "select 2"]


def test_split_sql_block_comment_opener_in_string_or_dollar_quote_or_line_comment_is_not_a_comment():
assert _split_sql("select '/*'; select 2;") == ["select '/*'", "select 2"]
assert _split_sql("do $$ begin perform '/*'; end $$; select 2;") == ["do $$ begin perform '/*'; end $$", "select 2"]
assert _split_sql('select "/*"; select 2;') == ['select "/*"', "select 2"]
assert _split_sql('select "a""/*"; select 2;') == ['select "a""/*"', "select 2"]
assert _split_sql("select 1 -- /* open\n; select 2;") == ["select 1 -- /* open", "select 2"]


def test_strip_line_comments_preserves_string_literals_containing_dashes():
sql = "SELECT '--not-a-comment' AS x -- a real comment\nFROM t;"
stripped = strip_line_comments(sql)
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading