From 8cb527d9b3ae9c16b45393af203e66f52b23a180 Mon Sep 17 00:00:00 2001 From: Adrian Ehrsam Date: Fri, 30 Oct 2026 22:11:50 +0000 Subject: [PATCH 1/2] Fix _split_sql splitting on semicolons inside /* block comments */ (#62); bump to 0.17.1 Co-Authored-By: Claude Sonnet 5.5 --- pgdevkit/migrate.py | 21 ++++++++++++++++++++- pyproject.toml | 2 +- tests/test_migrate.py | 21 +++++++++++++++++++++ uv.lock | 2 +- 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/pgdevkit/migrate.py b/pgdevkit/migrate.py index 2e67b94..6d71e06 100644 --- a/pgdevkit/migrate.py +++ b/pgdevkit/migrate.py @@ -78,12 +78,13 @@ 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, or $$...$$ blocks.""" stmts: list[str] = [] buf: list[str] = [] i = 0 in_string = False in_line_comment = False + block_depth = 0 # Postgres block comments nest dollar_tag: str | None = None while i < len(sql): @@ -92,6 +93,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: @@ -111,6 +125,11 @@ def _split_sql(sql: str) -> list[str]: 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: diff --git a/pyproject.toml b/pyproject.toml index 8414a42..7d0df5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/tests/test_migrate.py b/tests/test_migrate.py index 832d395..3fe96c9 100644 --- a/tests/test_migrate.py +++ b/tests/test_migrate.py @@ -50,6 +50,27 @@ 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 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) diff --git a/uv.lock b/uv.lock index ef76b18..c0dd63c 100644 --- a/uv.lock +++ b/uv.lock @@ -451,7 +451,7 @@ wheels = [ [[package]] name = "pgdevkit" -version = "0.17.0" +version = "0.17.1" source = { editable = "." } dependencies = [ { name = "docker" }, From df1277f88183d9c827cd990032f2e99e2af1dfed Mon Sep 17 00:00:00 2001 From: Adrian Ehrsam Date: Fri, 30 Oct 2026 22:13:44 +0000 Subject: [PATCH 2/2] Track double-quoted identifiers so /* inside one is not a comment (#62) Co-Authored-By: Claude Sonnet 5.5 --- pgdevkit/migrate.py | 10 +++++++++- tests/test_migrate.py | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/pgdevkit/migrate.py b/pgdevkit/migrate.py index 6d71e06..63242f4 100644 --- a/pgdevkit/migrate.py +++ b/pgdevkit/migrate.py @@ -78,11 +78,12 @@ 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 (`--` and nested `/* */`), '...' 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 @@ -122,6 +123,13 @@ 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) diff --git a/tests/test_migrate.py b/tests/test_migrate.py index 3fe96c9..8721d69 100644 --- a/tests/test_migrate.py +++ b/tests/test_migrate.py @@ -68,6 +68,8 @@ def test_split_sql_ignores_semicolon_in_multiline_block_comment(): 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"]