sql2sqlx 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sql2sqlx/__init__.py +83 -0
- sql2sqlx/__main__.py +14 -0
- sql2sqlx/cli.py +323 -0
- sql2sqlx/converter.py +1241 -0
- sql2sqlx/emitter.py +275 -0
- sql2sqlx/errors.py +93 -0
- sql2sqlx/keywords.py +169 -0
- sql2sqlx/lexer.py +494 -0
- sql2sqlx/model.py +487 -0
- sql2sqlx/parser.py +2193 -0
- sql2sqlx/py.typed +2 -0
- sql2sqlx/refs.py +902 -0
- sql2sqlx/splitter.py +360 -0
- sql2sqlx/version.py +15 -0
- sql2sqlx-0.1.0.dist-info/METADATA +733 -0
- sql2sqlx-0.1.0.dist-info/RECORD +20 -0
- sql2sqlx-0.1.0.dist-info/WHEEL +5 -0
- sql2sqlx-0.1.0.dist-info/entry_points.txt +2 -0
- sql2sqlx-0.1.0.dist-info/licenses/LICENSE +202 -0
- sql2sqlx-0.1.0.dist-info/top_level.txt +1 -0
sql2sqlx/emitter.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# Copyright (c) Soumyadip Sarkar.
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# This source code is licensed under the Apache-style license found in the
|
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
|
6
|
+
|
|
7
|
+
"""Render Dataform ``.sqlx`` files.
|
|
8
|
+
|
|
9
|
+
The emitter's contract is *character fidelity for untouched SQL*:
|
|
10
|
+
generated bodies are produced by applying span edits (reference rewrites,
|
|
11
|
+
select-list aliases, ``${self()}`` substitutions) to slices of the
|
|
12
|
+
**original** source text - tokens are never re-serialized, so the user's
|
|
13
|
+
formatting, casing and inline comments survive after input decoding.
|
|
14
|
+
|
|
15
|
+
Two SQLX-specific safety rules live here:
|
|
16
|
+
|
|
17
|
+
* Any literal ``${`` in a SQL string or quoted identifier is emitted through
|
|
18
|
+
a constant JavaScript placeholder. Dataform does not provide a backslash
|
|
19
|
+
escape for SQL placeholders, so ``\\${`` is insufficient. Replacement text
|
|
20
|
+
inserted *by* sql2sqlx (``${ref(...)}``, ``${self()}``) stays active.
|
|
21
|
+
* Operations bodies have exactly one trailing semicolon stripped:
|
|
22
|
+
Dataform executes the body as a BigQuery script, and a trailing empty
|
|
23
|
+
statement is at best noise.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import re
|
|
30
|
+
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
|
|
31
|
+
|
|
32
|
+
from sql2sqlx.lexer import BACKTICK, PARAM, STRING, Token
|
|
33
|
+
from sql2sqlx.model import TableName
|
|
34
|
+
|
|
35
|
+
#: Canonical top-level config key order (unknown keys follow, insertion-ordered).
|
|
36
|
+
_KEY_ORDER = (
|
|
37
|
+
"type",
|
|
38
|
+
"database",
|
|
39
|
+
"schema",
|
|
40
|
+
"name",
|
|
41
|
+
"materialized",
|
|
42
|
+
"protected",
|
|
43
|
+
"hasOutput",
|
|
44
|
+
"uniqueKey",
|
|
45
|
+
"description",
|
|
46
|
+
"columns",
|
|
47
|
+
"dependencies",
|
|
48
|
+
"tags",
|
|
49
|
+
"bigquery",
|
|
50
|
+
"disabled",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
#: Canonical key order inside the ``bigquery`` block.
|
|
54
|
+
_BQ_ORDER = (
|
|
55
|
+
"partitionBy",
|
|
56
|
+
"clusterBy",
|
|
57
|
+
"updatePartitionFilter",
|
|
58
|
+
"labels",
|
|
59
|
+
"partitionExpirationDays",
|
|
60
|
+
"requirePartitionFilter",
|
|
61
|
+
"additionalOptions",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
_JS_IDENT_RE = re.compile(r"^[A-Za-z_$][A-Za-z0-9_$]*$")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _json(value: str) -> str:
|
|
68
|
+
"""JSON-encode a string for embedding in JS (non-ASCII preserved)."""
|
|
69
|
+
return json.dumps(value, ensure_ascii=False)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def sqlx_constant(value: str) -> str:
|
|
73
|
+
"""Return a SQLX placeholder that evaluates to the constant ``value``."""
|
|
74
|
+
return "${" + _json(value) + "}"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def sqlx_escape_edits(tokens: Sequence[Token]) -> List[Tuple[int, int, str]]:
|
|
78
|
+
"""Build edits that preserve literal ``${`` through Dataform compile.
|
|
79
|
+
|
|
80
|
+
GoogleSQL only permits ``$`` in the relevant valid inputs inside string
|
|
81
|
+
literals or quoted identifiers. String occurrences can be replaced by a
|
|
82
|
+
constant placeholder for the two characters. A quoted identifier must be
|
|
83
|
+
replaced as a whole because a SQLX placeholder nested between BigQuery
|
|
84
|
+
backticks produces invalid generated JavaScript.
|
|
85
|
+
"""
|
|
86
|
+
edits: List[Tuple[int, int, str]] = []
|
|
87
|
+
for token in tokens:
|
|
88
|
+
if "${" not in token.text:
|
|
89
|
+
continue
|
|
90
|
+
if token.kind == BACKTICK or (token.kind == PARAM and token.text.startswith("@`")):
|
|
91
|
+
edits.append((token.start, token.end, sqlx_constant(token.text)))
|
|
92
|
+
continue
|
|
93
|
+
if token.kind != STRING:
|
|
94
|
+
continue
|
|
95
|
+
offset = 0
|
|
96
|
+
while True:
|
|
97
|
+
offset = token.text.find("${", offset)
|
|
98
|
+
if offset < 0:
|
|
99
|
+
break
|
|
100
|
+
start = token.start + offset
|
|
101
|
+
edits.append((start, start + 2, sqlx_constant("${")))
|
|
102
|
+
offset += 2
|
|
103
|
+
return edits
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _js_key(key: str) -> str:
|
|
107
|
+
"""Render an object key: bare when a valid JS identifier, else quoted."""
|
|
108
|
+
return key if _JS_IDENT_RE.match(key) else _json(key)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _js_value(value: Any, indent: int) -> str:
|
|
112
|
+
"""Render a Python value as JavaScript object-literal source.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
value: ``str``/``bool``/``int``/``float``/``list``/``dict`` (nested).
|
|
116
|
+
indent: Current indentation level (2 spaces per level).
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
JS source text for the value.
|
|
120
|
+
"""
|
|
121
|
+
pad = " " * indent
|
|
122
|
+
if isinstance(value, bool):
|
|
123
|
+
return "true" if value else "false"
|
|
124
|
+
if isinstance(value, int):
|
|
125
|
+
return str(value)
|
|
126
|
+
if isinstance(value, float):
|
|
127
|
+
return str(int(value)) if value.is_integer() else repr(value)
|
|
128
|
+
if isinstance(value, str):
|
|
129
|
+
return _json(value)
|
|
130
|
+
if isinstance(value, (list, tuple)):
|
|
131
|
+
inner = [_js_value(v, indent + 1) for v in value]
|
|
132
|
+
one_line = "[" + ", ".join(inner) + "]"
|
|
133
|
+
if len(one_line) <= 72 and "\n" not in one_line:
|
|
134
|
+
return one_line
|
|
135
|
+
ip = " " * (indent + 1)
|
|
136
|
+
return "[\n" + ",\n".join(ip + v for v in inner) + "\n" + pad + "]"
|
|
137
|
+
if isinstance(value, dict):
|
|
138
|
+
if not value:
|
|
139
|
+
return "{}"
|
|
140
|
+
ip = " " * (indent + 1)
|
|
141
|
+
lines = [f"{ip}{_js_key(k)}: {_js_value(v, indent + 1)}" for k, v in value.items()]
|
|
142
|
+
return "{\n" + ",\n".join(lines) + "\n" + pad + "}"
|
|
143
|
+
return _json(str(value)) # pragma: no cover - defensive
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def render_config(config: Dict[str, Any]) -> str:
|
|
147
|
+
"""Render a Dataform ``config { ... }`` block.
|
|
148
|
+
|
|
149
|
+
Keys are emitted in canonical order (:data:`_KEY_ORDER`, then any
|
|
150
|
+
remaining keys in insertion order); the nested ``bigquery`` object is
|
|
151
|
+
ordered by :data:`_BQ_ORDER`. Ordering is purely cosmetic but makes
|
|
152
|
+
output deterministic and diff-friendly.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
config: The config mapping.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
The complete ``config { ... }`` block, without a trailing newline.
|
|
159
|
+
"""
|
|
160
|
+
ordered: Dict[str, Any] = {}
|
|
161
|
+
for key in _KEY_ORDER:
|
|
162
|
+
if key in config:
|
|
163
|
+
ordered[key] = config[key]
|
|
164
|
+
for key, val in config.items():
|
|
165
|
+
if key not in ordered:
|
|
166
|
+
ordered[key] = val
|
|
167
|
+
bq = ordered.get("bigquery")
|
|
168
|
+
if isinstance(bq, dict):
|
|
169
|
+
obq: Dict[str, Any] = {k: bq[k] for k in _BQ_ORDER if k in bq}
|
|
170
|
+
for k, v in bq.items():
|
|
171
|
+
if k not in obq:
|
|
172
|
+
obq[k] = v
|
|
173
|
+
ordered["bigquery"] = obq
|
|
174
|
+
lines = [f" {_js_key(k)}: {_js_value(v, 1)}" for k, v in ordered.items()]
|
|
175
|
+
return "config {\n" + ",\n".join(lines) + "\n}"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def apply_edits_escaped(
|
|
179
|
+
text: str, start: int, end: int, edits: Iterable[Tuple[int, int, str]]
|
|
180
|
+
) -> str:
|
|
181
|
+
"""Slice ``text[start:end]`` and apply non-overlapping span edits.
|
|
182
|
+
|
|
183
|
+
Edits use *absolute* offsets into ``text``. Edits outside the slice or
|
|
184
|
+
overlapping an already-applied edit are skipped defensively. Literal SQLX
|
|
185
|
+
interpolation is handled by :func:`sqlx_escape_edits`; replacements such
|
|
186
|
+
as ``ref()`` and ``self()`` are inserted verbatim and remain active.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
text: Full original source text.
|
|
190
|
+
start: Slice start offset.
|
|
191
|
+
end: Slice end offset.
|
|
192
|
+
edits: ``(start, end, replacement)`` triples, any order.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
The edited, escaped slice.
|
|
196
|
+
"""
|
|
197
|
+
segments: List[str] = []
|
|
198
|
+
pos = start
|
|
199
|
+
for a, b, replacement in sorted(edits, key=lambda e: (e[0], e[1])):
|
|
200
|
+
if a < pos or a < start or b > end or b < a:
|
|
201
|
+
continue
|
|
202
|
+
segments.append(text[pos:a])
|
|
203
|
+
segments.append(replacement)
|
|
204
|
+
pos = b
|
|
205
|
+
segments.append(text[pos:end])
|
|
206
|
+
return "".join(segments)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def ref_expr(name: TableName) -> str:
|
|
210
|
+
"""Build the ``${ref(...)}`` expression for a produced table.
|
|
211
|
+
|
|
212
|
+
The reference carries exactly the qualification the *producer* was
|
|
213
|
+
declared with: ``${ref("name")}``, ``${ref("schema", "name")}``, or
|
|
214
|
+
the object form ``${ref({database: ..., schema: ..., name: ...})}``
|
|
215
|
+
when a project was explicit.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
name: The producer's original (pre-resolution) table name.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
The interpolation expression text.
|
|
222
|
+
"""
|
|
223
|
+
if name.project:
|
|
224
|
+
return "${ref({database: %s, schema: %s, name: %s})}" % (
|
|
225
|
+
_json(name.project),
|
|
226
|
+
_json(name.dataset or ""),
|
|
227
|
+
_json(name.table),
|
|
228
|
+
)
|
|
229
|
+
if name.dataset:
|
|
230
|
+
return "${ref(%s, %s)}" % (_json(name.dataset), _json(name.table))
|
|
231
|
+
return "${ref(%s)}" % _json(name.table)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def build_sqlx(
|
|
235
|
+
config: Dict[str, Any],
|
|
236
|
+
body: Optional[str] = None,
|
|
237
|
+
annotation: Optional[str] = None,
|
|
238
|
+
leading_comments: Optional[str] = None,
|
|
239
|
+
trailing_comments: Optional[str] = None,
|
|
240
|
+
) -> str:
|
|
241
|
+
"""Assemble a complete ``.sqlx`` file.
|
|
242
|
+
|
|
243
|
+
Layout: ``config`` block, blank line, then (in order) the provenance
|
|
244
|
+
annotation comment, the statement's original leading comments, and the
|
|
245
|
+
SQL body. One trailing semicolon is stripped from the body. When the
|
|
246
|
+
body is ``None`` (declarations) only the config block is emitted.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
config: The config mapping (see :func:`render_config`).
|
|
250
|
+
body: Edited SQL body, or ``None``.
|
|
251
|
+
annotation: Provenance comment line, or ``None``.
|
|
252
|
+
leading_comments: Comments that preceded the statement, or ``None``.
|
|
253
|
+
trailing_comments: Final file comments after the statement's
|
|
254
|
+
terminating semicolon, or ``None``.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
The full file contents, newline-terminated.
|
|
258
|
+
"""
|
|
259
|
+
head = render_config(config)
|
|
260
|
+
bits: List[str] = []
|
|
261
|
+
if annotation:
|
|
262
|
+
bits.append(annotation)
|
|
263
|
+
if leading_comments and leading_comments.strip():
|
|
264
|
+
bits.append(leading_comments.strip())
|
|
265
|
+
if body is not None:
|
|
266
|
+
cleaned = body.strip()
|
|
267
|
+
if cleaned.endswith(";"):
|
|
268
|
+
cleaned = cleaned[:-1].rstrip()
|
|
269
|
+
if cleaned:
|
|
270
|
+
bits.append(cleaned)
|
|
271
|
+
if trailing_comments and trailing_comments.strip():
|
|
272
|
+
bits.append(trailing_comments.strip())
|
|
273
|
+
if bits:
|
|
274
|
+
return head + "\n\n" + "\n".join(bits) + "\n"
|
|
275
|
+
return head + "\n"
|
sql2sqlx/errors.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Copyright (c) Soumyadip Sarkar.
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# This source code is licensed under the Apache-style license found in the
|
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
|
6
|
+
|
|
7
|
+
"""Exception hierarchy for :mod:`sql2sqlx`.
|
|
8
|
+
|
|
9
|
+
All exceptions raised deliberately by this library derive from
|
|
10
|
+
:class:`Sql2SqlxError`, so callers can catch a single base class.
|
|
11
|
+
Location-aware errors (lexing/splitting problems) carry a 1-based
|
|
12
|
+
``line`` and ``column`` pointing at the offending character in the
|
|
13
|
+
original SQL text.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Sql2SqlxError(Exception):
|
|
22
|
+
"""Base class for all errors raised by sql2sqlx."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LexError(Sql2SqlxError):
|
|
26
|
+
"""Raised when the SQL text cannot be tokenized.
|
|
27
|
+
|
|
28
|
+
Typical causes are unterminated string literals, unterminated
|
|
29
|
+
backtick-quoted identifiers, or unterminated ``/* ... */`` block
|
|
30
|
+
comments.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
message: Human-readable description of the problem.
|
|
34
|
+
line: 1-based line number of the offending character.
|
|
35
|
+
column: 1-based column number of the offending character.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, message: str, line: int, column: int) -> None:
|
|
39
|
+
"""Initialize the error.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
message: Human-readable description of the problem.
|
|
43
|
+
line: 1-based line number in the source text.
|
|
44
|
+
column: 1-based column number in the source text.
|
|
45
|
+
"""
|
|
46
|
+
super().__init__(f"{message} (line {line}, column {column})")
|
|
47
|
+
self.message = message
|
|
48
|
+
self.line = line
|
|
49
|
+
self.column = column
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SplitError(Sql2SqlxError):
|
|
53
|
+
"""Raised when statement splitting fails irrecoverably.
|
|
54
|
+
|
|
55
|
+
In practice the splitter is lenient (mismatched ``END`` markers are
|
|
56
|
+
tolerated), so this error is reserved for structural impossibilities
|
|
57
|
+
such as unbalanced parentheses at end of input.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, message: str, line: int = 0, column: int = 0) -> None:
|
|
61
|
+
"""Initialize the error.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
message: Human-readable description of the problem.
|
|
65
|
+
line: 1-based line number in the source text (0 if unknown).
|
|
66
|
+
column: 1-based column number in the source text (0 if unknown).
|
|
67
|
+
"""
|
|
68
|
+
loc = f" (line {line}, column {column})" if line else ""
|
|
69
|
+
super().__init__(f"{message}{loc}")
|
|
70
|
+
self.message = message
|
|
71
|
+
self.line = line
|
|
72
|
+
self.column = column
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ConversionError(Sql2SqlxError):
|
|
76
|
+
"""Raised when a statement or file cannot be converted at all.
|
|
77
|
+
|
|
78
|
+
Note that *unsupported-but-valid* SQL never raises: it falls back to
|
|
79
|
+
a Dataform ``operations`` action and a warning is recorded in the
|
|
80
|
+
:class:`~sql2sqlx.model.ConversionReport`. This exception is for
|
|
81
|
+
genuinely broken input (e.g. unreadable files, lexer failures).
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, message: str, path: Optional[str] = None) -> None:
|
|
85
|
+
"""Initialize the error.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
message: Human-readable description of the problem.
|
|
89
|
+
path: Path of the file being converted, if applicable.
|
|
90
|
+
"""
|
|
91
|
+
super().__init__(f"{path}: {message}" if path else message)
|
|
92
|
+
self.message = message
|
|
93
|
+
self.path = path
|
sql2sqlx/keywords.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Copyright (c) Soumyadip Sarkar.
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# This source code is licensed under the Apache-style license found in the
|
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
|
6
|
+
|
|
7
|
+
"""GoogleSQL reserved keywords and related token classification sets.
|
|
8
|
+
|
|
9
|
+
:data:`RESERVED` is the official GoogleSQL reserved-keyword list. It is
|
|
10
|
+
used to decide whether a bare identifier can be a table alias (reserved
|
|
11
|
+
words cannot be used as unquoted aliases), which keeps the reference
|
|
12
|
+
scanner from mistaking clause keywords for aliases.
|
|
13
|
+
|
|
14
|
+
:data:`FROM_CLAUSE_ENDERS` are keywords that terminate the table list of
|
|
15
|
+
a ``FROM`` clause at the same nesting depth; encountering one switches
|
|
16
|
+
the reference scanner out of "expecting a table path" mode.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import FrozenSet
|
|
22
|
+
|
|
23
|
+
#: Official GoogleSQL reserved keywords (case-insensitive).
|
|
24
|
+
RESERVED: FrozenSet[str] = frozenset(
|
|
25
|
+
{
|
|
26
|
+
"ALL",
|
|
27
|
+
"AND",
|
|
28
|
+
"ANY",
|
|
29
|
+
"ARRAY",
|
|
30
|
+
"AS",
|
|
31
|
+
"ASC",
|
|
32
|
+
"ASSERT_ROWS_MODIFIED",
|
|
33
|
+
"AT",
|
|
34
|
+
"BETWEEN",
|
|
35
|
+
"BY",
|
|
36
|
+
"CASE",
|
|
37
|
+
"CAST",
|
|
38
|
+
"COLLATE",
|
|
39
|
+
"CONTAINS",
|
|
40
|
+
"CREATE",
|
|
41
|
+
"CROSS",
|
|
42
|
+
"CUBE",
|
|
43
|
+
"CURRENT",
|
|
44
|
+
"DEFAULT",
|
|
45
|
+
"DEFINE",
|
|
46
|
+
"DESC",
|
|
47
|
+
"DISTINCT",
|
|
48
|
+
"ELSE",
|
|
49
|
+
"END",
|
|
50
|
+
"ENUM",
|
|
51
|
+
"ESCAPE",
|
|
52
|
+
"EXCEPT",
|
|
53
|
+
"EXCLUDE",
|
|
54
|
+
"EXISTS",
|
|
55
|
+
"EXTRACT",
|
|
56
|
+
"FALSE",
|
|
57
|
+
"FETCH",
|
|
58
|
+
"FOLLOWING",
|
|
59
|
+
"FOR",
|
|
60
|
+
"FROM",
|
|
61
|
+
"FULL",
|
|
62
|
+
"GRAPH_TABLE",
|
|
63
|
+
"GROUP",
|
|
64
|
+
"GROUPING",
|
|
65
|
+
"GROUPS",
|
|
66
|
+
"HASH",
|
|
67
|
+
"HAVING",
|
|
68
|
+
"IF",
|
|
69
|
+
"IGNORE",
|
|
70
|
+
"IN",
|
|
71
|
+
"INNER",
|
|
72
|
+
"INTERSECT",
|
|
73
|
+
"INTERVAL",
|
|
74
|
+
"INTO",
|
|
75
|
+
"IS",
|
|
76
|
+
"JOIN",
|
|
77
|
+
"LATERAL",
|
|
78
|
+
"LEFT",
|
|
79
|
+
"LIKE",
|
|
80
|
+
"LIMIT",
|
|
81
|
+
"LOOKUP",
|
|
82
|
+
"MERGE",
|
|
83
|
+
"NATURAL",
|
|
84
|
+
"NEW",
|
|
85
|
+
"NO",
|
|
86
|
+
"NOT",
|
|
87
|
+
"NULL",
|
|
88
|
+
"NULLS",
|
|
89
|
+
"OF",
|
|
90
|
+
"ON",
|
|
91
|
+
"OR",
|
|
92
|
+
"ORDER",
|
|
93
|
+
"OUTER",
|
|
94
|
+
"OVER",
|
|
95
|
+
"PARTITION",
|
|
96
|
+
"PRECEDING",
|
|
97
|
+
"PROTO",
|
|
98
|
+
"QUALIFY",
|
|
99
|
+
"RANGE",
|
|
100
|
+
"RECURSIVE",
|
|
101
|
+
"RESPECT",
|
|
102
|
+
"RIGHT",
|
|
103
|
+
"ROLLUP",
|
|
104
|
+
"ROWS",
|
|
105
|
+
"SELECT",
|
|
106
|
+
"SET",
|
|
107
|
+
"SOME",
|
|
108
|
+
"STRUCT",
|
|
109
|
+
"TABLESAMPLE",
|
|
110
|
+
"THEN",
|
|
111
|
+
"TO",
|
|
112
|
+
"TREAT",
|
|
113
|
+
"TRUE",
|
|
114
|
+
"UNBOUNDED",
|
|
115
|
+
"UNION",
|
|
116
|
+
"UNNEST",
|
|
117
|
+
"USING",
|
|
118
|
+
"WHEN",
|
|
119
|
+
"WHERE",
|
|
120
|
+
"WINDOW",
|
|
121
|
+
"WITH",
|
|
122
|
+
"WITHIN",
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
#: Keywords that end the table list of a FROM clause at the same depth.
|
|
127
|
+
FROM_CLAUSE_ENDERS: FrozenSet[str] = frozenset(
|
|
128
|
+
{
|
|
129
|
+
"WHERE",
|
|
130
|
+
"GROUP",
|
|
131
|
+
"HAVING",
|
|
132
|
+
"QUALIFY",
|
|
133
|
+
"WINDOW",
|
|
134
|
+
"ORDER",
|
|
135
|
+
"LIMIT",
|
|
136
|
+
"UNION",
|
|
137
|
+
"INTERSECT",
|
|
138
|
+
"EXCEPT",
|
|
139
|
+
"SET",
|
|
140
|
+
"WHEN",
|
|
141
|
+
"THEN",
|
|
142
|
+
"RETURNING",
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
#: Date/time part identifiers; presence at top level of a select-list item
|
|
147
|
+
#: makes implicit-alias detection ambiguous (``INTERVAL 1 DAY``), so items
|
|
148
|
+
#: containing INTERVAL trigger a safe fallback instead of a guess.
|
|
149
|
+
DATE_PARTS: FrozenSet[str] = frozenset(
|
|
150
|
+
{
|
|
151
|
+
"YEAR",
|
|
152
|
+
"QUARTER",
|
|
153
|
+
"MONTH",
|
|
154
|
+
"WEEK",
|
|
155
|
+
"ISOWEEK",
|
|
156
|
+
"DAY",
|
|
157
|
+
"DAYOFWEEK",
|
|
158
|
+
"DAYOFYEAR",
|
|
159
|
+
"HOUR",
|
|
160
|
+
"MINUTE",
|
|
161
|
+
"SECOND",
|
|
162
|
+
"MILLISECOND",
|
|
163
|
+
"MICROSECOND",
|
|
164
|
+
"ISOYEAR",
|
|
165
|
+
"DATE",
|
|
166
|
+
"TIME",
|
|
167
|
+
"DATETIME",
|
|
168
|
+
}
|
|
169
|
+
)
|