fix: keep a schema-qualified call inside an index expression from reading as a relation

A quoted routine call qualified by a schema and sitting inside a CREATE INDEX
expression, ON "Foo" (public."f"(col)), walked its qualifier read-through back
across the opening paren to the ON that introduces the indexed table, so the call
was misread as a relation and dropped from the call set, leaving a rewrite in that
routine unscanned. A word now only introduces the name when nothing but whitespace
and qualifier dots lies between them, so a paren in that gap keeps ON (and any
relation-introducing keyword) from reaching across it and the call stays a call.
This commit is contained in:
mateo-berri 2026-08-24 16:04:38 -07:00
parent c9d1b94828
commit a527275d05
2 changed files with 21 additions and 7 deletions

View file

@ -121,6 +121,7 @@ LOOP_TARGET = re.compile(r"\bFOR(?:EACH)?\s+([A-Za-z_][A-Za-z0-9_]*)\s+IN\b", re
LOOP_HEADER = re.compile(r"\bFOR(?:EACH)?\b.*?\bLOOP\b", re.IGNORECASE | re.DOTALL)
WORD_OR_ASSIGN = re.compile(r"[A-Za-z_][A-Za-z0-9_]*|:=|(?<![<>!:=])=(?![=>])")
PRECEDING_WORD = re.compile(r"([A-Za-z_][A-Za-z0-9_]*)[^A-Za-z0-9_]*$")
QUALIFIER_GAP = re.compile(r"[\s.]*")
EXPLAIN_OPTIONS = re.compile(r"\bEXPLAIN\b(?:\s+(?:ANALYZE|ANALYSE|VERBOSE)\b)+", re.IGNORECASE)
DEFINES_A_ROUTINE = re.compile(
r"\bCREATE\b(?:\s+OR\s+REPLACE)?\s+(?:FUNCTION|PROCEDURE)\b", re.IGNORECASE
@ -589,16 +590,21 @@ def names_a_relation(before: str) -> bool:
touching or spaced as `public . "Foo"`, is the qualifier and the one before it decides. The
introducing word is settled before that, so a quoted schema, which blanks to spaces and leaves
`INTO` itself as the word ahead of the name however the dot is spaced, still reads as a relation,
while a genuine `SELECT public."f"()` reads through its qualifier to the `SELECT` and stays a call."""
while a genuine `SELECT public."f"()` reads through its qualifier to the `SELECT` and stays a call.
A word only introduces the name when nothing but whitespace and qualifier dots lies between them,
so a `(` in that gap keeps it from reaching across: a schema-qualified call inside a `CREATE INDEX`
expression, `ON "Foo" (public."f"(col))`, leaves `ON` behind the paren and the call stays a call."""
word = PRECEDING_WORD.search(before)
if word is None:
return False
keyword = word.group(1).upper()
if keyword in INTRODUCES_A_RELATION:
return True
if keyword == "ON":
return NAMES_AN_INDEX.search(before[before.rfind(";") + 1 :]) is not None
if "." in before[word.end(1) :]:
gap = before[word.end(1) :]
if QUALIFIER_GAP.fullmatch(gap):
keyword = word.group(1).upper()
if keyword in INTRODUCES_A_RELATION:
return True
if keyword == "ON":
return NAMES_AN_INDEX.search(before[before.rfind(";") + 1 :]) is not None
if "." in gap:
return names_a_relation(before[: word.start(1)])
return False

View file

@ -521,6 +521,14 @@ class TestStoredRoutines:
sql = self.DEFINITION + 'SELECT public . "backfill" ();\n'
assert _keywords(tmp_path, sql) == ("UPDATE",)
def test_a_schema_qualified_call_in_an_index_expression_still_counts(self, tmp_path):
sql = self.DEFINITION + 'CREATE INDEX ON "Bar" (public."backfill"("a"));\n'
assert _keywords(tmp_path, sql) == ("UPDATE",)
def test_a_spaced_schema_qualified_call_in_an_index_expression_still_counts(self, tmp_path):
sql = self.DEFINITION + 'CREATE INDEX ON "Bar" (public . "backfill" ("a"));\n'
assert _keywords(tmp_path, sql) == ("UPDATE",)
def test_a_trigger_wiring_the_function_up_counts_as_a_call(self, tmp_path):
sql = self.DEFINITION + 'CREATE TRIGGER t AFTER INSERT ON "Foo" EXECUTE FUNCTION backfill();\n'
assert _keywords(tmp_path, sql) == ("UPDATE",)