Repository navigation
Expand file tree
/
Copy pathsql_names.py
More file actions
78 lines (67 loc) · 2.84 KB
/
Copy pathsql_names.py
File metadata and controls
78 lines (67 loc) · 2.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
"""PostgreSQL identifier normalization.
One normalizer serves both sides of SQL routine resolution: the definition
side (`CREATE FUNCTION App."MyFunc"`) and the string-call side
(`callSp('App."MyFunc"')`). If the two sides folded differently, a routine
and the call that names it would register under different keys and never
connect.
PostgreSQL's rules: an unquoted identifier folds to lowercase; a quoted one
keeps its case, may contain dots that are NOT qualifier separators, and
doubles its quotes to embed one (`"a""b"` names `a"b`).
"""
from __future__ import annotations
# A dot INSIDE a quoted identifier, encoded so the canonical key keeps it
# apart from a qualifier separator: "billing.v1".usp_total and
# billing."v1.usp_total" must not collide, and every downstream consumer
# (FQN joining, the registry trie, dotted-suffix lookup) splits on ".".
# ONE DOT LEADER renders as a dot to a human. Because PostgreSQL permits
# the leader characters themselves inside quoted identifiers, the encoding
# is a proper escape scheme (TWO DOT LEADER escapes itself and ONE DOT
# LEADER), so it stays injective on any input.
QUOTED_DOT = "․" # U+2024 ONE DOT LEADER: an encoded literal dot
_ESCAPE = "‥" # U+2025 TWO DOT LEADER: escapes itself and QUOTED_DOT
def _encode_quoted_segment(segment: str) -> str:
segment = segment.replace(_ESCAPE, _ESCAPE + _ESCAPE)
segment = segment.replace(QUOTED_DOT, _ESCAPE + QUOTED_DOT)
return segment.replace(".", QUOTED_DOT)
def normalize_sql_reference(reference: str) -> str:
"""`App."My.Func"` -> `app.My․Func`; `"a""b"` -> `a"b`; `MyFunc` -> `myfunc`.
Splits into segments on dots OUTSIDE quotes only, folds unquoted segments
to lowercase, and keeps quoted segments verbatim (quotes stripped, doubled
quotes unescaped, embedded dots encoded as QUOTED_DOT). Empty segments
vanish, so a malformed reference still yields its salvageable parts
rather than nothing.
"""
segments: list[str] = []
buf: list[str] = []
in_quotes = False
saw_quotes = False
i = 0
def flush() -> None:
nonlocal saw_quotes
if saw_quotes:
segment = _encode_quoted_segment("".join(buf))
else:
segment = "".join(buf).strip().lower()
if segment:
segments.append(segment)
buf.clear()
saw_quotes = False
while i < len(reference):
ch = reference[i]
if ch == '"':
if in_quotes and i + 1 < len(reference) and reference[i + 1] == '"':
buf.append('"')
i += 2
continue
in_quotes = not in_quotes
saw_quotes = True
i += 1
continue
if ch == "." and not in_quotes:
flush()
i += 1
continue
buf.append(ch)
i += 1
flush()
return ".".join(segments)