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
23 changes: 15 additions & 8 deletions datasette/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1245,10 +1245,18 @@ class StartupError(Exception):
pass


_single_line_comment_re = re.compile(r"--.*")
_multi_line_comment_re = re.compile(r"/\*.*?\*/", re.DOTALL)
_single_quote_re = re.compile(r"'(?:''|[^'])*'")
_double_quote_re = re.compile(r'"(?:\"\"|[^"])*"')
# Comments and string literals, matched in a single pass so that whichever
# construct starts first "wins" - this ensures a comment marker inside a string
# literal (or a quote inside a comment) does not confuse the parameter scan.
_comments_and_strings_re = re.compile(
r"""
--[^\n]* # single line comment
| /\*.*?\*/ # multi line comment
| '(?:''|[^'])*' # single quoted string ('' escapes a quote)
| "(?:""|[^"])*" # double quoted identifier ("" escapes a quote)
""",
re.DOTALL | re.VERBOSE,
)
_named_param_re = re.compile(r":(\w+)")


Expand All @@ -1259,10 +1267,9 @@ def named_parameters(sql: str) -> List[str]:

e.g. for ``select * from foo where id=:id`` this would return ``["id"]``
"""
sql = _single_line_comment_re.sub("", sql)
sql = _multi_line_comment_re.sub("", sql)
sql = _single_quote_re.sub("", sql)
sql = _double_quote_re.sub("", sql)
# Strip comments and string literals first so that any ":name" sequences
# inside them are not mistaken for named parameters
sql = _comments_and_strings_re.sub("", sql)
# Extract parameters from what is left
return _named_param_re.findall(sql)

Expand Down
10 changes: 10 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,16 @@ def test_parse_metadata(content, expected):
("select 1 + :one + :two", ["one", "two"]),
("select 'bob' || '0:00' || :cat", ["cat"]),
("select this is invalid :one, :two, :three", ["one", "two", "three"]),
# A string literal containing a comment marker should not hide
# parameters that come after it
("select * from t where note = '-- TODO' and id = :id", ["id"]),
("select '--' || :y", ["y"]),
("select * from t where note = '/* x */' and id = :id", ["id"]),
# Parameters that live inside a comment should be ignored
("select :x -- and :ignored", ["x"]),
("select :x /* and :ignored */ from t", ["x"]),
# Parameters inside a string literal should be ignored
("select ':ignored' || :real", ["real"]),
),
)
@pytest.mark.parametrize("use_async_version", (False, True))
Expand Down
Loading