sqlleaf 0.0.1__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.
- sqlleaf/__init__.py +4 -0
- sqlleaf/dialects/__init__.py +0 -0
- sqlleaf/dialects/plpgsql/__init__.py +0 -0
- sqlleaf/dialects/plpgsql/p_classes.py +275 -0
- sqlleaf/dialects/plpgsql/p_dialect.py +148 -0
- sqlleaf/dialects/plpgsql/p_generator.py +538 -0
- sqlleaf/dialects/plpgsql/p_parser.py +1244 -0
- sqlleaf/dialects/plpgsql/p_tokenizer.py +41 -0
- sqlleaf/exception.py +67 -0
- sqlleaf/lineage.py +326 -0
- sqlleaf/mappings.py +376 -0
- sqlleaf/models/__init__.py +0 -0
- sqlleaf/models/context.py +153 -0
- sqlleaf/models/hook.py +9 -0
- sqlleaf/models/node/__init__.py +27 -0
- sqlleaf/models/node/base.py +148 -0
- sqlleaf/models/node/column.py +345 -0
- sqlleaf/models/node/dynamodb.py +39 -0
- sqlleaf/models/node/function.py +33 -0
- sqlleaf/models/node/interval.py +21 -0
- sqlleaf/models/node/json.py +57 -0
- sqlleaf/models/node/literal.py +17 -0
- sqlleaf/models/node/null.py +14 -0
- sqlleaf/models/node/pivot.py +34 -0
- sqlleaf/models/node/program.py +27 -0
- sqlleaf/models/node/sequence.py +18 -0
- sqlleaf/models/node/star.py +11 -0
- sqlleaf/models/node/stream.py +14 -0
- sqlleaf/models/node/user_defined_function.py +33 -0
- sqlleaf/models/node/var.py +7 -0
- sqlleaf/models/node/variable.py +8 -0
- sqlleaf/models/node/window.py +22 -0
- sqlleaf/models/query/__init__.py +33 -0
- sqlleaf/models/query/base.py +248 -0
- sqlleaf/models/query/call.py +111 -0
- sqlleaf/models/query/copy.py +144 -0
- sqlleaf/models/query/ctas.py +61 -0
- sqlleaf/models/query/database.py +48 -0
- sqlleaf/models/query/delete.py +26 -0
- sqlleaf/models/query/execute.py +66 -0
- sqlleaf/models/query/holder.py +49 -0
- sqlleaf/models/query/insert.py +69 -0
- sqlleaf/models/query/merge.py +29 -0
- sqlleaf/models/query/multitable_insert.py +42 -0
- sqlleaf/models/query/prepare.py +61 -0
- sqlleaf/models/query/procedure.py +72 -0
- sqlleaf/models/query/put.py +38 -0
- sqlleaf/models/query/replace.py +32 -0
- sqlleaf/models/query/schema.py +47 -0
- sqlleaf/models/query/select.py +27 -0
- sqlleaf/models/query/sequence.py +33 -0
- sqlleaf/models/query/set.py +74 -0
- sqlleaf/models/query/stage.py +78 -0
- sqlleaf/models/query/table.py +220 -0
- sqlleaf/models/query/trigger.py +42 -0
- sqlleaf/models/query/type.py +42 -0
- sqlleaf/models/query/unload.py +64 -0
- sqlleaf/models/query/update.py +45 -0
- sqlleaf/models/query/user_defined_function.py +297 -0
- sqlleaf/models/query/values.py +20 -0
- sqlleaf/models/query/view.py +41 -0
- sqlleaf/path.py +194 -0
- sqlleaf/processors/__init__.py +0 -0
- sqlleaf/processors/collector/__init__.py +0 -0
- sqlleaf/processors/collector/collector.py +827 -0
- sqlleaf/processors/collector/substitute.py +396 -0
- sqlleaf/processors/generator/__init__.py +0 -0
- sqlleaf/processors/generator/dialects/__init__.py +5 -0
- sqlleaf/processors/generator/dialects/athena.py +45 -0
- sqlleaf/processors/generator/dialects/base.py +572 -0
- sqlleaf/processors/generator/dialects/mysql.py +19 -0
- sqlleaf/processors/generator/dialects/postgres.py +182 -0
- sqlleaf/processors/generator/dialects/redshift.py +192 -0
- sqlleaf/processors/generator/dialects/snowflake.py +90 -0
- sqlleaf/processors/generator/generator.py +495 -0
- sqlleaf/processors/transformer/__init__.py +31 -0
- sqlleaf/processors/transformer/base.py +899 -0
- sqlleaf/processors/transformer/call.py +9 -0
- sqlleaf/processors/transformer/copy.py +63 -0
- sqlleaf/processors/transformer/ctas.py +39 -0
- sqlleaf/processors/transformer/delete.py +14 -0
- sqlleaf/processors/transformer/execute.py +9 -0
- sqlleaf/processors/transformer/expressions/__init__.py +11 -0
- sqlleaf/processors/transformer/expressions/functional_notation.py +22 -0
- sqlleaf/processors/transformer/expressions/row.py +451 -0
- sqlleaf/processors/transformer/expressions/values.py +318 -0
- sqlleaf/processors/transformer/insert.py +125 -0
- sqlleaf/processors/transformer/merge.py +14 -0
- sqlleaf/processors/transformer/multitable_insert.py +14 -0
- sqlleaf/processors/transformer/replace.py +23 -0
- sqlleaf/processors/transformer/transformer.py +141 -0
- sqlleaf/processors/transformer/udf.py +235 -0
- sqlleaf/processors/transformer/unload.py +30 -0
- sqlleaf/processors/transformer/update.py +174 -0
- sqlleaf/processors/transformer/values.py +10 -0
- sqlleaf/settings/__init__.py +19 -0
- sqlleaf/settings/athena.py +23 -0
- sqlleaf/settings/base.py +37 -0
- sqlleaf/settings/postgres.py +52 -0
- sqlleaf/system_tables/load.py +0 -0
- sqlleaf/typing.py +93 -0
- sqlleaf/util/__init__.py +34 -0
- sqlleaf/util/expression.py +282 -0
- sqlleaf/util/graph.py +116 -0
- sqlleaf/util/helpers.py +42 -0
- sqlleaf/util/iterators.py +37 -0
- sqlleaf-0.0.1.dist-info/METADATA +46 -0
- sqlleaf-0.0.1.dist-info/RECORD +111 -0
- sqlleaf-0.0.1.dist-info/WHEEL +5 -0
- sqlleaf-0.0.1.dist-info/licenses/LICENSE +21 -0
- sqlleaf-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1244 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import typing as t
|
|
3
|
+
|
|
4
|
+
from sqlglot.parsers.postgres import PostgresParser
|
|
5
|
+
from sqlglot import exp
|
|
6
|
+
|
|
7
|
+
from sqlleaf.dialects.plpgsql.p_classes import *
|
|
8
|
+
from sqlleaf.dialects.plpgsql.p_tokenizer import TokenType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PlPgSQLParser(PostgresParser):
|
|
12
|
+
STATEMENT_PARSERS = {
|
|
13
|
+
**PostgresParser.STATEMENT_PARSERS,
|
|
14
|
+
TokenType.BEGIN: lambda self: self._parse_plpgsql_block(),
|
|
15
|
+
TokenType.DECLARE: lambda self: self._parse_plpgsql_block(),
|
|
16
|
+
TokenType.UPDATE: lambda self: self._parse_pgupdate(),
|
|
17
|
+
TokenType.DELETE: lambda self: self._parse_pgdelete(),
|
|
18
|
+
TokenType.CASE: lambda self: self._parse_pgcase(),
|
|
19
|
+
TokenType.RETURN: lambda self: self._parse_pgreturn(),
|
|
20
|
+
TokenType.WHILE: lambda self: self._parse_pgwhile(),
|
|
21
|
+
TokenType.IF: lambda self: self._parse_pgif(),
|
|
22
|
+
TokenType.LOOP: lambda self: self._parse_pgloop(),
|
|
23
|
+
TokenType.FOR: lambda self: self._parse_pgfor(),
|
|
24
|
+
TokenType.FOREACH: lambda self: self._parse_pgforeach(),
|
|
25
|
+
TokenType.EXIT: lambda self: self._parse_pgexit(),
|
|
26
|
+
TokenType.CONTINUE: lambda self: self._parse_pgcontinue(),
|
|
27
|
+
TokenType.RAISE: lambda self: self._parse_pgraise(),
|
|
28
|
+
TokenType.ASSERT: lambda self: self._parse_pgassert(),
|
|
29
|
+
TokenType.OPEN: lambda self: self._parse_pgopen(),
|
|
30
|
+
TokenType.PERFORM: lambda self: self._parse_pgperform(),
|
|
31
|
+
TokenType.FETCH: lambda self: self._parse_pgfetch(),
|
|
32
|
+
TokenType.MOVE: lambda self: self._parse_pgmove(),
|
|
33
|
+
TokenType.CLOSE: lambda self: self._parse_pgclose(),
|
|
34
|
+
TokenType.EXECUTE: lambda self: self._parse_pgexecute(),
|
|
35
|
+
TokenType.GET: lambda self: self._parse_pggetdiagnostics(),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Intercept bare FOUND wherever an expression is allowed and parse it into PGFound
|
|
39
|
+
NO_PAREN_FUNCTION_PARSERS = {
|
|
40
|
+
**PostgresParser.NO_PAREN_FUNCTION_PARSERS,
|
|
41
|
+
"FOUND": lambda self: self.expression(PGFound()),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Class-level constant tables (hoisted from per-call definitions)
|
|
45
|
+
_NAMED_PAIR_OPS: dict[TokenType, type[exp.Expression]] = {
|
|
46
|
+
TokenType.COLON_EQ: exp.PropertyEQ,
|
|
47
|
+
TokenType.EQ: exp.EQ,
|
|
48
|
+
TokenType.FARROW: exp.Kwarg,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_RAISE_LEVELS = frozenset({
|
|
52
|
+
"DEBUG",
|
|
53
|
+
"LOG",
|
|
54
|
+
"INFO",
|
|
55
|
+
"NOTICE",
|
|
56
|
+
"WARNING",
|
|
57
|
+
"EXCEPTION",
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
_SIMPLE_FETCH_DIRS = {
|
|
61
|
+
"NEXT": PGNext,
|
|
62
|
+
"PRIOR": PGPrior,
|
|
63
|
+
"FIRST": PGFirst,
|
|
64
|
+
"LAST": PGLast,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
def _match_expect(self, token_type: TokenType, *, advance: bool = True) -> None:
|
|
68
|
+
if not self._match(token_type, advance=advance):
|
|
69
|
+
self.raise_error(f"Expected token: {token_type.name}")
|
|
70
|
+
|
|
71
|
+
def _match_text_expect(self, text: str, *, advance: bool = True) -> None:
|
|
72
|
+
if not self._match_texts((text,), advance=advance):
|
|
73
|
+
self.raise_error(f"Expected token: {text.upper()}")
|
|
74
|
+
|
|
75
|
+
def _parse_statement(self) -> exp.Expression | None:
|
|
76
|
+
if not self._match(TokenType.LABEL_BEGIN):
|
|
77
|
+
return super()._parse_statement()
|
|
78
|
+
|
|
79
|
+
label = self._parse_id_var(any_token=True)
|
|
80
|
+
if label is None:
|
|
81
|
+
self.raise_error("Expected label identifier after <<")
|
|
82
|
+
|
|
83
|
+
self._match_expect(TokenType.LABEL_END)
|
|
84
|
+
|
|
85
|
+
statement = super()._parse_statement()
|
|
86
|
+
if statement is None or not isinstance(statement, (PGBlock, PGLoop, PGForIn, PGForEach, exp.WhileBlock)):
|
|
87
|
+
self.raise_error("Label can only prefix BEGIN/DECLARE blocks and LOOP/FOR/FOREACH/WHILE statements")
|
|
88
|
+
|
|
89
|
+
statement.set("label", label)
|
|
90
|
+
return statement
|
|
91
|
+
|
|
92
|
+
def _parse_pggetdiagnostics(self) -> PGGetDiagnostics:
|
|
93
|
+
current = False
|
|
94
|
+
stacked = False
|
|
95
|
+
if self._match_texts(("CURRENT",)):
|
|
96
|
+
current = True
|
|
97
|
+
elif self._match_texts(("STACKED",)):
|
|
98
|
+
stacked = True
|
|
99
|
+
|
|
100
|
+
self._match_text_expect("DIAGNOSTICS")
|
|
101
|
+
|
|
102
|
+
pairs: list[exp.Expression] = []
|
|
103
|
+
|
|
104
|
+
def parse_item() -> exp.Expression:
|
|
105
|
+
# Items are represented as identifier-like expressions
|
|
106
|
+
ident = self._parse_id_var(any_token=True)
|
|
107
|
+
if ident is None:
|
|
108
|
+
self.raise_error("Expected diagnostics item after operator")
|
|
109
|
+
return ident
|
|
110
|
+
|
|
111
|
+
# Parse one-or-more pairs: <var> {:=|=} <item> [, ...]
|
|
112
|
+
while True:
|
|
113
|
+
var = self._parse_id_var(any_token=True)
|
|
114
|
+
if var is None:
|
|
115
|
+
self.raise_error(
|
|
116
|
+
"Expected variable after DIAGNOSTICS"
|
|
117
|
+
if not pairs
|
|
118
|
+
else "Expected variable after comma in GET DIAGNOSTICS"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
pair = self._parse_named_pair(
|
|
122
|
+
var,
|
|
123
|
+
allowed_ops={TokenType.EQ, TokenType.COLON_EQ},
|
|
124
|
+
rhs_parser=parse_item,
|
|
125
|
+
)
|
|
126
|
+
if pair is None:
|
|
127
|
+
self.raise_error("Expected assignment operator (= or :=) after variable")
|
|
128
|
+
pairs.append(pair)
|
|
129
|
+
|
|
130
|
+
if not self._match(TokenType.COMMA):
|
|
131
|
+
break
|
|
132
|
+
|
|
133
|
+
return self.expression(PGGetDiagnostics(current=current, stacked=stacked, expressions=pairs))
|
|
134
|
+
|
|
135
|
+
def _parse_named_pair(
|
|
136
|
+
self,
|
|
137
|
+
name_expr: exp.Expression,
|
|
138
|
+
allowed_ops: set[TokenType],
|
|
139
|
+
rhs_parser: t.Callable[[], exp.Expression] | None = None,
|
|
140
|
+
) -> exp.PropertyEQ | exp.EQ | exp.Kwarg | None:
|
|
141
|
+
"""
|
|
142
|
+
Parse a named-argument style pair following a name expression.
|
|
143
|
+
"""
|
|
144
|
+
rhsp = rhs_parser or self._parse_expression
|
|
145
|
+
|
|
146
|
+
# Filter to the allowed operators for this context
|
|
147
|
+
candidates = tuple(t for t in self._NAMED_PAIR_OPS.keys() if t in allowed_ops)
|
|
148
|
+
if not candidates:
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
if not self._match_set(candidates, advance=False):
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
# Consume the operator and build the corresponding expression
|
|
155
|
+
op = self._curr.token_type
|
|
156
|
+
self._advance()
|
|
157
|
+
klass = self._NAMED_PAIR_OPS[op]
|
|
158
|
+
return self.expression(klass(this=name_expr, expression=rhsp()))
|
|
159
|
+
|
|
160
|
+
def _parse_statement_body(
|
|
161
|
+
self,
|
|
162
|
+
*,
|
|
163
|
+
stop_texts: tuple[str, ...] = (),
|
|
164
|
+
stop_tokens: tuple[TokenType, ...] = (),
|
|
165
|
+
error_msg: str = "Invalid expression / Unexpected token",
|
|
166
|
+
strict: bool = True,
|
|
167
|
+
parse_one: t.Callable[[], exp.Expression | None] | None = None,
|
|
168
|
+
) -> list[exp.Expression]:
|
|
169
|
+
"""
|
|
170
|
+
Parse a sequence of ';'-delimited statements until a terminator.
|
|
171
|
+
"""
|
|
172
|
+
out: list[exp.Expression] = []
|
|
173
|
+
parse = parse_one or self._parse_statement
|
|
174
|
+
|
|
175
|
+
# Normalize stop_texts to uppercase for lexeme comparison
|
|
176
|
+
stop_texts_upper = tuple(s.upper() for s in stop_texts)
|
|
177
|
+
|
|
178
|
+
while True:
|
|
179
|
+
# If we've consumed the current chunk, move to the next one
|
|
180
|
+
if self._index >= self._tokens_size:
|
|
181
|
+
self._advance_chunk()
|
|
182
|
+
|
|
183
|
+
# No more tokens
|
|
184
|
+
if not self._curr:
|
|
185
|
+
break
|
|
186
|
+
|
|
187
|
+
# Stop on a token-type terminator
|
|
188
|
+
if stop_tokens and self._match_set(stop_tokens, advance=False):
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
# Stop on a lexeme (identifier-like) terminator
|
|
192
|
+
if stop_texts_upper and self._curr.text.upper() in stop_texts_upper:
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
# Parse one unit from this chunk
|
|
196
|
+
node = parse()
|
|
197
|
+
if node is not None:
|
|
198
|
+
out.append(node)
|
|
199
|
+
|
|
200
|
+
if strict:
|
|
201
|
+
# After parsing a unit, there should be no leftover tokens in the chunk
|
|
202
|
+
if self._index < self._tokens_size:
|
|
203
|
+
self.raise_error(error_msg)
|
|
204
|
+
self.check_errors()
|
|
205
|
+
|
|
206
|
+
self._advance_chunk()
|
|
207
|
+
|
|
208
|
+
return out
|
|
209
|
+
|
|
210
|
+
def _parse_plpgsql_block(self) -> PGBlock:
|
|
211
|
+
declare = None
|
|
212
|
+
|
|
213
|
+
entry = self._prev.token_type if self._prev else None
|
|
214
|
+
if entry == TokenType.DECLARE:
|
|
215
|
+
declare = self._parse_pldeclare()
|
|
216
|
+
self._match_expect(TokenType.BEGIN)
|
|
217
|
+
elif entry != TokenType.BEGIN:
|
|
218
|
+
self.raise_error("Expected BEGIN or DECLARE to start PL/pgSQL block")
|
|
219
|
+
|
|
220
|
+
expressions = self._parse_statement_body(
|
|
221
|
+
stop_texts=("EXCEPTION",),
|
|
222
|
+
stop_tokens=(TokenType.END,),
|
|
223
|
+
error_msg="Invalid expression / Unexpected token",
|
|
224
|
+
strict=True,
|
|
225
|
+
parse_one=self._parse_block_unit,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
exception = None
|
|
229
|
+
# If EXCEPTION section follows, parse it now (helper didn't consume it)
|
|
230
|
+
if self._match_texts("EXCEPTION"):
|
|
231
|
+
exception = self._parse_pgexception()
|
|
232
|
+
|
|
233
|
+
# Require END to close the block
|
|
234
|
+
self._match_expect(TokenType.END)
|
|
235
|
+
|
|
236
|
+
# Optional trailing label and statement terminator
|
|
237
|
+
label = self._parse_id_var(any_token=True)
|
|
238
|
+
self._match(TokenType.SEMICOLON)
|
|
239
|
+
|
|
240
|
+
return self.expression(
|
|
241
|
+
PGBlock(expressions=expressions, declare=declare, exception=exception, begin=True, label=label)
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def _parse_pg_assignment(self) -> exp.PropertyEQ | None:
|
|
245
|
+
"""Parse a simple PL/pgSQL assignment statement inside a block.
|
|
246
|
+
|
|
247
|
+
Pattern: <identifier> := <expression>
|
|
248
|
+
|
|
249
|
+
Notes:
|
|
250
|
+
- We do NOT accept '=' as assignment. If we detect '<ident> =', raise a ParseError.
|
|
251
|
+
- Parsing is local to the current chunk (terminated by ';').
|
|
252
|
+
"""
|
|
253
|
+
index = self._index
|
|
254
|
+
|
|
255
|
+
# LHS must be an identifier-like variable
|
|
256
|
+
lhs = self._parse_id_var()
|
|
257
|
+
if not lhs:
|
|
258
|
+
self._retreat(index)
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
if self._match(TokenType.EQ, advance=False):
|
|
262
|
+
self.raise_error("Use := for assignment in PL/pgSQL blocks")
|
|
263
|
+
|
|
264
|
+
pair = self._parse_named_pair(
|
|
265
|
+
lhs,
|
|
266
|
+
allowed_ops={TokenType.COLON_EQ},
|
|
267
|
+
rhs_parser=self._parse_expression,
|
|
268
|
+
)
|
|
269
|
+
if pair is None:
|
|
270
|
+
self._retreat(index)
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
return pair
|
|
274
|
+
|
|
275
|
+
def _parse_pgexception(self) -> exp.Expression:
|
|
276
|
+
whens: list[exp.Expression] = []
|
|
277
|
+
|
|
278
|
+
# Parse one or more WHEN clauses
|
|
279
|
+
while self._match(TokenType.WHEN, advance=False):
|
|
280
|
+
whens.append(self._parse_pgwhen())
|
|
281
|
+
|
|
282
|
+
if not whens:
|
|
283
|
+
self.raise_error("Invalid expression / Unexpected token")
|
|
284
|
+
|
|
285
|
+
return self.expression(PGException(whens=whens))
|
|
286
|
+
|
|
287
|
+
def _parse_pgwhen(self) -> exp.Expression:
|
|
288
|
+
self._match_expect(TokenType.WHEN)
|
|
289
|
+
|
|
290
|
+
conditions: list[exp.Expression] = []
|
|
291
|
+
|
|
292
|
+
# First condition (identifier or SQLSTATE 'xxxxx')
|
|
293
|
+
cond = self._parse_pgwhen_condition()
|
|
294
|
+
if cond is not None:
|
|
295
|
+
conditions.append(cond)
|
|
296
|
+
|
|
297
|
+
# Parse one or more condition identifiers separated by OR; AND is not allowed
|
|
298
|
+
while self._match(TokenType.OR):
|
|
299
|
+
more = self._parse_pgwhen_condition()
|
|
300
|
+
if more is None:
|
|
301
|
+
self.raise_error("Expected condition after OR")
|
|
302
|
+
conditions.append(more)
|
|
303
|
+
|
|
304
|
+
if not conditions:
|
|
305
|
+
self.raise_error("WHEN requires at least one condition")
|
|
306
|
+
|
|
307
|
+
self._match_expect(TokenType.THEN)
|
|
308
|
+
|
|
309
|
+
# Parse one or more statements until next WHEN or END
|
|
310
|
+
thens: list[exp.Expression] = self._parse_statement_body(
|
|
311
|
+
stop_texts=("WHEN",),
|
|
312
|
+
stop_tokens=(TokenType.END,),
|
|
313
|
+
error_msg="Invalid expression in WHEN body / Unexpected token",
|
|
314
|
+
strict=True,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
if not thens:
|
|
318
|
+
self.raise_error("WHEN body requires at least one statement")
|
|
319
|
+
|
|
320
|
+
# Combine multiple conditions into a single OR expression like exp.Case does
|
|
321
|
+
condition_expr = conditions[0]
|
|
322
|
+
if len(conditions) > 1:
|
|
323
|
+
condition_expr = exp.or_(*conditions)
|
|
324
|
+
|
|
325
|
+
return self.expression(PGWhen(condition=condition_expr, then=thens))
|
|
326
|
+
|
|
327
|
+
def _parse_pgwhen_condition(self) -> exp.Expression | None:
|
|
328
|
+
if self._match_texts(("OTHERS",)):
|
|
329
|
+
return self.expression(PGOthers())
|
|
330
|
+
|
|
331
|
+
if self._match_texts(("SQLSTATE",)):
|
|
332
|
+
return self._parse_sqlstate()
|
|
333
|
+
|
|
334
|
+
return self._parse_id_var()
|
|
335
|
+
|
|
336
|
+
def _parse_sqlstate(self) -> PGSqlState | None:
|
|
337
|
+
"""Consume a quoted literal after SQLSTATE."""
|
|
338
|
+
if self._match(TokenType.STRING, advance=False):
|
|
339
|
+
lit = self._parse_primary()
|
|
340
|
+
if isinstance(lit, exp.Literal) and lit.is_string:
|
|
341
|
+
return self.expression(PGSqlState(this=lit))
|
|
342
|
+
self.raise_error("SQLSTATE must be followed by a quoted literal")
|
|
343
|
+
|
|
344
|
+
def _parse_pgif(self) -> PGIf:
|
|
345
|
+
# First branch starts immediately after IF
|
|
346
|
+
branches: list[exp.Expression] = [self._parse_pgif_branch()]
|
|
347
|
+
|
|
348
|
+
# Zero or more ELSIF / ELSEIF branches
|
|
349
|
+
while self._match_texts(("ELSIF", "ELSEIF")):
|
|
350
|
+
branches.append(self._parse_pgif_branch())
|
|
351
|
+
|
|
352
|
+
# Optional ELSE branch
|
|
353
|
+
default: list[exp.Expression] | None = None
|
|
354
|
+
if self._match(TokenType.ELSE):
|
|
355
|
+
default = self._parse_pgif_body()
|
|
356
|
+
|
|
357
|
+
self._match_expect(TokenType.END)
|
|
358
|
+
self._match_expect(TokenType.IF)
|
|
359
|
+
|
|
360
|
+
return self.expression(PGIf(ifs=branches, default=default))
|
|
361
|
+
|
|
362
|
+
def _parse_pgcase(self) -> PGCase:
|
|
363
|
+
search_expr: exp.Expression | None = None
|
|
364
|
+
if not self._match(TokenType.WHEN, advance=False):
|
|
365
|
+
search_expr = self._parse_expression()
|
|
366
|
+
|
|
367
|
+
branches: list[exp.Expression] = []
|
|
368
|
+
while self._match(TokenType.WHEN, advance=False):
|
|
369
|
+
branches.append(self._parse_pgcasewhen(search_expr))
|
|
370
|
+
|
|
371
|
+
if not branches:
|
|
372
|
+
self.raise_error("CASE requires at least one WHEN branch")
|
|
373
|
+
|
|
374
|
+
default: list[exp.Expression] | None = None
|
|
375
|
+
if self._match(TokenType.ELSE):
|
|
376
|
+
default = self._parse_statement_body(
|
|
377
|
+
stop_tokens=(TokenType.END,),
|
|
378
|
+
error_msg="Invalid expression in CASE ELSE body / Unexpected token",
|
|
379
|
+
strict=True,
|
|
380
|
+
parse_one=self._parse_block_unit,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
self._match_expect(TokenType.END)
|
|
384
|
+
self._match_expect(TokenType.CASE)
|
|
385
|
+
|
|
386
|
+
return self.expression(PGCase(this=search_expr, ifs=branches, default=default))
|
|
387
|
+
|
|
388
|
+
def _parse_pgcasewhen(self, search_expr: exp.Expression | None) -> PGWhen:
|
|
389
|
+
self._match_expect(TokenType.WHEN)
|
|
390
|
+
|
|
391
|
+
if search_expr is None:
|
|
392
|
+
condition = self._parse_expression()
|
|
393
|
+
self._match_expect(TokenType.THEN)
|
|
394
|
+
else:
|
|
395
|
+
if self._match(TokenType.THEN, advance=False):
|
|
396
|
+
self.raise_error("CASE search WHEN requires at least one expression before THEN")
|
|
397
|
+
|
|
398
|
+
comparisons = [exp.EQ(this=search_expr.copy(), expression=self._parse_expression())]
|
|
399
|
+
while self._match(TokenType.COMMA):
|
|
400
|
+
comparisons.append(exp.EQ(this=search_expr.copy(), expression=self._parse_expression()))
|
|
401
|
+
|
|
402
|
+
self._match_expect(TokenType.THEN)
|
|
403
|
+
condition = comparisons[0] if len(comparisons) == 1 else exp.or_(*comparisons)
|
|
404
|
+
|
|
405
|
+
then_body = self._parse_statement_body(
|
|
406
|
+
stop_tokens=(TokenType.ELSE, TokenType.END),
|
|
407
|
+
stop_texts=("WHEN",),
|
|
408
|
+
error_msg="Invalid expression in CASE WHEN body / Unexpected token",
|
|
409
|
+
strict=True,
|
|
410
|
+
parse_one=self._parse_block_unit,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
if not then_body:
|
|
414
|
+
self.raise_error("CASE WHEN body requires at least one statement")
|
|
415
|
+
|
|
416
|
+
return self.expression(PGWhen(condition=condition, then=then_body))
|
|
417
|
+
|
|
418
|
+
def _parse_pgif_branch(self) -> PGIfBranch:
|
|
419
|
+
# Parse <condition> THEN <statements>
|
|
420
|
+
condition = self._parse_expression()
|
|
421
|
+
self._match_expect(TokenType.THEN)
|
|
422
|
+
|
|
423
|
+
then_body = self._parse_pgif_body()
|
|
424
|
+
if not then_body:
|
|
425
|
+
self.raise_error("IF branch requires at least one statement")
|
|
426
|
+
|
|
427
|
+
return self.expression(PGIfBranch(condition=condition, then=then_body))
|
|
428
|
+
|
|
429
|
+
def _parse_pgif_body(self) -> list[exp.Expression]:
|
|
430
|
+
return self._parse_statement_body(
|
|
431
|
+
stop_texts=("ELSIF", "ELSEIF"),
|
|
432
|
+
stop_tokens=(TokenType.ELSE, TokenType.END),
|
|
433
|
+
error_msg="Invalid expression in IF body / Unexpected token",
|
|
434
|
+
strict=True,
|
|
435
|
+
parse_one=self._parse_block_unit,
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
def _parse_block_unit(self) -> exp.Expression | None:
|
|
439
|
+
assignment = self._parse_pg_assignment()
|
|
440
|
+
if assignment is not None:
|
|
441
|
+
return assignment
|
|
442
|
+
return self._parse_statement()
|
|
443
|
+
|
|
444
|
+
def _parse_pldeclare(self) -> exp.Declare:
|
|
445
|
+
items: list[exp.Expression] = self._parse_statement_body(
|
|
446
|
+
stop_texts=("BEGIN",),
|
|
447
|
+
stop_tokens=(),
|
|
448
|
+
error_msg="Invalid DECLARE item / Unexpected token",
|
|
449
|
+
strict=True,
|
|
450
|
+
parse_one=self._parse_pldeclareitem,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
return self.expression(exp.Declare(expressions=items))
|
|
454
|
+
|
|
455
|
+
def _parse_pldeclareitem(self) -> PGDeclareItem | None:
|
|
456
|
+
ident = self._parse_id_var()
|
|
457
|
+
if not ident:
|
|
458
|
+
return None
|
|
459
|
+
|
|
460
|
+
# Optional CONSTANT modifier
|
|
461
|
+
is_constant = self._match_texts(("CONSTANT",))
|
|
462
|
+
|
|
463
|
+
# Optional ALIAS FOR $n variant: must come before type parsing
|
|
464
|
+
if self._match_text_seq("ALIAS", "FOR"):
|
|
465
|
+
# Parse a positional parameter like $1
|
|
466
|
+
param_expr = self._parse_identifier()
|
|
467
|
+
return self.expression(
|
|
468
|
+
PGDeclareItem(
|
|
469
|
+
this=ident,
|
|
470
|
+
alias_for=True,
|
|
471
|
+
expression=param_expr,
|
|
472
|
+
value=param_expr,
|
|
473
|
+
constant=is_constant,
|
|
474
|
+
)
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# Detect cursor declaration before type parsing:
|
|
478
|
+
index = self._index
|
|
479
|
+
scroll: bool | None = None
|
|
480
|
+
if self._match_texts(("NO",)):
|
|
481
|
+
if self._match_texts(("SCROLL",)):
|
|
482
|
+
scroll = False
|
|
483
|
+
else:
|
|
484
|
+
# Not a valid cursor prelude, rollback
|
|
485
|
+
self._retreat(index)
|
|
486
|
+
elif self._match_texts(("SCROLL",)):
|
|
487
|
+
scroll = True
|
|
488
|
+
|
|
489
|
+
# If SCROLL/NO SCROLL matched, require CURSOR next; else try bare CURSOR
|
|
490
|
+
if scroll is not None:
|
|
491
|
+
if self._match_texts(("CURSOR",)):
|
|
492
|
+
return self._parse_pl_declare_cursor(ident, is_constant, scroll)
|
|
493
|
+
self._retreat(index)
|
|
494
|
+
elif self._match_texts(("CURSOR",)):
|
|
495
|
+
return self._parse_pl_declare_cursor(ident, is_constant, None)
|
|
496
|
+
|
|
497
|
+
type_expr = self._parse_pl_type_or_typeref()
|
|
498
|
+
|
|
499
|
+
# Optional COLLATE clause after type
|
|
500
|
+
collate_expr = None
|
|
501
|
+
if self._match(TokenType.COLLATE):
|
|
502
|
+
# Collation can be an identifier or quoted identifier
|
|
503
|
+
collate_expr = self._parse_id_var()
|
|
504
|
+
|
|
505
|
+
# Optional NOT NULL constraint (sits after COLLATE and before DEFAULT)
|
|
506
|
+
not_null = False
|
|
507
|
+
if self._match_text_seq("NOT", "NULL"):
|
|
508
|
+
not_null = True
|
|
509
|
+
|
|
510
|
+
# Optional initialization with DEFAULT or assignment (:= or =)
|
|
511
|
+
init_expr = None
|
|
512
|
+
is_default = False
|
|
513
|
+
assign_op: str | None = None
|
|
514
|
+
if self._match(TokenType.DEFAULT):
|
|
515
|
+
is_default = True
|
|
516
|
+
init_expr = self._parse_bitwise()
|
|
517
|
+
elif self._match(TokenType.COLON_EQ):
|
|
518
|
+
assign_op = ":="
|
|
519
|
+
init_expr = self._parse_bitwise()
|
|
520
|
+
elif self._match(TokenType.EQ):
|
|
521
|
+
assign_op = "="
|
|
522
|
+
init_expr = self._parse_bitwise()
|
|
523
|
+
|
|
524
|
+
return self.expression(
|
|
525
|
+
PGDeclareItem(
|
|
526
|
+
this=ident,
|
|
527
|
+
kind=type_expr,
|
|
528
|
+
collate=collate_expr,
|
|
529
|
+
not_null=not_null,
|
|
530
|
+
expression=init_expr,
|
|
531
|
+
value=init_expr,
|
|
532
|
+
default=is_default,
|
|
533
|
+
assign=assign_op,
|
|
534
|
+
constant=is_constant,
|
|
535
|
+
)
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
def _parse_pl_type_or_typeref(self) -> exp.Expression | None:
|
|
539
|
+
index = self._index
|
|
540
|
+
|
|
541
|
+
this = self._parse_id_var()
|
|
542
|
+
ref = None
|
|
543
|
+
|
|
544
|
+
if this is not None:
|
|
545
|
+
table = None
|
|
546
|
+
if self._match(TokenType.DOT):
|
|
547
|
+
column = self._parse_id_var()
|
|
548
|
+
if column is None:
|
|
549
|
+
self._retreat(index)
|
|
550
|
+
return self._parse_type() or self._parse_types()
|
|
551
|
+
table = this
|
|
552
|
+
this = column
|
|
553
|
+
|
|
554
|
+
ref = self.expression(exp.Column(this=this, table=table))
|
|
555
|
+
|
|
556
|
+
if ref is not None and self._match(TokenType.MOD):
|
|
557
|
+
rowtype = False
|
|
558
|
+
if self._match(TokenType.TYPE):
|
|
559
|
+
rowtype = False
|
|
560
|
+
elif self._match_text_seq("ROWTYPE"):
|
|
561
|
+
rowtype = True
|
|
562
|
+
else:
|
|
563
|
+
self._retreat(index)
|
|
564
|
+
return self._parse_type() or self._parse_types()
|
|
565
|
+
|
|
566
|
+
array = False
|
|
567
|
+
if self._match(TokenType.L_BRACKET):
|
|
568
|
+
self._match_expect(TokenType.R_BRACKET)
|
|
569
|
+
array = True
|
|
570
|
+
return self.expression(PGTypeRef(this=ref, array=array, rowtype=rowtype))
|
|
571
|
+
|
|
572
|
+
self._retreat(index)
|
|
573
|
+
return self._parse_types() or self._parse_type()
|
|
574
|
+
|
|
575
|
+
def _parse_pl_declare_cursor(
|
|
576
|
+
self, ident: exp.Expression, is_constant: bool, scroll: bool | None
|
|
577
|
+
) -> PGDeclareItem:
|
|
578
|
+
# Optional argument declaration list: ( name type [, ...] )
|
|
579
|
+
cursor_args: list[exp.Expression] | None = None
|
|
580
|
+
if self._match(TokenType.L_PAREN):
|
|
581
|
+
cursor_args = self._parse_csv(self._parse_pl_cursor_arg)
|
|
582
|
+
self._match_expect(TokenType.R_PAREN)
|
|
583
|
+
|
|
584
|
+
# Required FOR <query>
|
|
585
|
+
self._match_expect(TokenType.FOR)
|
|
586
|
+
|
|
587
|
+
query = self._parse_statement()
|
|
588
|
+
if query is None:
|
|
589
|
+
self.raise_error("Expected query after CURSOR ... FOR")
|
|
590
|
+
|
|
591
|
+
return self.expression(
|
|
592
|
+
PGDeclareItem(
|
|
593
|
+
this=ident,
|
|
594
|
+
cursor=True,
|
|
595
|
+
scroll=scroll,
|
|
596
|
+
cursor_args=cursor_args,
|
|
597
|
+
query=query,
|
|
598
|
+
constant=is_constant,
|
|
599
|
+
)
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
def _parse_pl_cursor_arg(self) -> PGCursorArg:
|
|
603
|
+
name = self._parse_id_var()
|
|
604
|
+
arg_type = self._parse_type() or self._parse_types()
|
|
605
|
+
return self.expression(PGCursorArg(this=name, kind=arg_type))
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def _parse_returning(self) -> exp.Returning | None:
|
|
609
|
+
# Override base to support INTO [STRICT] <target>[, ...]
|
|
610
|
+
if not self._match(TokenType.RETURNING):
|
|
611
|
+
return None
|
|
612
|
+
|
|
613
|
+
expressions = self._parse_csv(self._parse_expression)
|
|
614
|
+
|
|
615
|
+
into = None
|
|
616
|
+
if self._match(TokenType.INTO):
|
|
617
|
+
strict = self._match_texts(("STRICT",))
|
|
618
|
+
# Parse one or more identifier-like targets (variables)
|
|
619
|
+
targets = self._parse_csv(lambda: self._parse_id_var(any_token=True))
|
|
620
|
+
if not targets:
|
|
621
|
+
self.raise_error("Expected target list after INTO")
|
|
622
|
+
|
|
623
|
+
into = self.expression(
|
|
624
|
+
PGInto(
|
|
625
|
+
this=targets[0],
|
|
626
|
+
expressions=targets if len(targets) > 1 else None,
|
|
627
|
+
strict=strict,
|
|
628
|
+
)
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
return self.expression(PGReturning(expressions=expressions, into=into))
|
|
632
|
+
|
|
633
|
+
def _parse_into(self) -> exp.Into | None:
|
|
634
|
+
if not self._match(TokenType.INTO):
|
|
635
|
+
return None
|
|
636
|
+
|
|
637
|
+
strict = self._match_texts(("STRICT",))
|
|
638
|
+
temp = self._match(TokenType.TEMPORARY)
|
|
639
|
+
unlogged = self._match_text_seq("UNLOGGED")
|
|
640
|
+
self._match(TokenType.TABLE)
|
|
641
|
+
|
|
642
|
+
targets = self._parse_csv(lambda: self._parse_table(schema=True))
|
|
643
|
+
if not targets:
|
|
644
|
+
self.raise_error("Expected target list after INTO")
|
|
645
|
+
|
|
646
|
+
return self.expression(
|
|
647
|
+
PGInto(
|
|
648
|
+
this=targets[0],
|
|
649
|
+
expressions=targets if len(targets) > 1 else None,
|
|
650
|
+
temporary=temp,
|
|
651
|
+
unlogged=unlogged,
|
|
652
|
+
strict=strict,
|
|
653
|
+
)
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
def _extract_trailing_where_current_of(self) -> tuple[int, exp.Identifier] | None:
|
|
657
|
+
statement_index = self._index
|
|
658
|
+
where_index = -1
|
|
659
|
+
|
|
660
|
+
for index in range(statement_index, self._tokens_size - 3):
|
|
661
|
+
token = self._tokens[index]
|
|
662
|
+
if token.text.upper() != "WHERE":
|
|
663
|
+
continue
|
|
664
|
+
|
|
665
|
+
if (
|
|
666
|
+
self._tokens[index + 1].text.upper() == "CURRENT"
|
|
667
|
+
and self._tokens[index + 2].text.upper() == "OF"
|
|
668
|
+
):
|
|
669
|
+
where_index = index
|
|
670
|
+
|
|
671
|
+
if where_index < 0:
|
|
672
|
+
return None
|
|
673
|
+
|
|
674
|
+
cursor_token = self._tokens[where_index + 3]
|
|
675
|
+
if cursor_token.token_type not in {TokenType.IDENTIFIER, TokenType.VAR, TokenType.STRING}:
|
|
676
|
+
self.raise_error("Expected cursor name after WHERE CURRENT OF", token=cursor_token)
|
|
677
|
+
|
|
678
|
+
cursor = exp.to_identifier(
|
|
679
|
+
cursor_token.text,
|
|
680
|
+
quoted=cursor_token.token_type in {TokenType.IDENTIFIER, TokenType.STRING},
|
|
681
|
+
)
|
|
682
|
+
cursor.update_positions(cursor_token)
|
|
683
|
+
|
|
684
|
+
for trailing_token in self._tokens[where_index + 4 : self._tokens_size]:
|
|
685
|
+
if trailing_token.token_type not in {TokenType.SEMICOLON, TokenType.SENTINEL}:
|
|
686
|
+
self.raise_error("Invalid expression after WHERE CURRENT OF", token=trailing_token)
|
|
687
|
+
|
|
688
|
+
return where_index, cursor
|
|
689
|
+
|
|
690
|
+
def _parse_pg_dml_with_current_of(self, parse: t.Callable[[], exp.Expression]) -> tuple[exp.Expression, exp.Identifier | None]:
|
|
691
|
+
current_of_info = self._extract_trailing_where_current_of()
|
|
692
|
+
original_tokens_size = self._tokens_size
|
|
693
|
+
|
|
694
|
+
if current_of_info:
|
|
695
|
+
self._tokens_size = current_of_info[0]
|
|
696
|
+
|
|
697
|
+
statement = parse()
|
|
698
|
+
self._tokens_size = original_tokens_size
|
|
699
|
+
|
|
700
|
+
if current_of_info:
|
|
701
|
+
self._retreat(original_tokens_size)
|
|
702
|
+
|
|
703
|
+
return statement, current_of_info and current_of_info[1]
|
|
704
|
+
|
|
705
|
+
def _parse_pgupdate(self) -> PGUpdate:
|
|
706
|
+
update, current_of = self._parse_pg_dml_with_current_of(super()._parse_update)
|
|
707
|
+
return self.expression(PGUpdate(**update.args, current_of=current_of))
|
|
708
|
+
|
|
709
|
+
def _parse_pgdelete(self) -> PGDelete:
|
|
710
|
+
delete, current_of = self._parse_pg_dml_with_current_of(super()._parse_delete)
|
|
711
|
+
return self.expression(PGDelete(**delete.args, current_of=current_of))
|
|
712
|
+
|
|
713
|
+
def _parse_pgwhile(self) -> exp.WhileBlock:
|
|
714
|
+
# Parse condition expression
|
|
715
|
+
cond = self._parse_expression()
|
|
716
|
+
|
|
717
|
+
# Guard against accidental aliasing like: <cond> AS LOOP
|
|
718
|
+
if isinstance(cond, exp.Alias):
|
|
719
|
+
alias = cond.args.get("alias")
|
|
720
|
+
if isinstance(alias, exp.Identifier) and alias.name.upper() == "LOOP":
|
|
721
|
+
cond = cond.this
|
|
722
|
+
else:
|
|
723
|
+
self._match_expect(TokenType.LOOP)
|
|
724
|
+
else:
|
|
725
|
+
self._match_expect(TokenType.LOOP)
|
|
726
|
+
|
|
727
|
+
# Parse loop body and optional trailing label using shared helpers
|
|
728
|
+
body = self._parse_loop_body()
|
|
729
|
+
label = self._parse_loop_label()
|
|
730
|
+
|
|
731
|
+
return self.expression(exp.WhileBlock(this=cond, body=body, label=label))
|
|
732
|
+
|
|
733
|
+
def _parse_pgperform(self) -> PGPerform:
|
|
734
|
+
if self._match(TokenType.SENTINEL, advance=False):
|
|
735
|
+
self.raise_error("Expected query body after PERFORM")
|
|
736
|
+
|
|
737
|
+
perform_index = self._index - 1
|
|
738
|
+
perform_token = self._tokens[perform_index]
|
|
739
|
+
original_type = perform_token.token_type
|
|
740
|
+
|
|
741
|
+
perform_token.token_type = TokenType.SELECT
|
|
742
|
+
try:
|
|
743
|
+
self._retreat(perform_index)
|
|
744
|
+
query = self._parse_select()
|
|
745
|
+
finally:
|
|
746
|
+
perform_token.token_type = original_type
|
|
747
|
+
|
|
748
|
+
if not isinstance(query, exp.Select):
|
|
749
|
+
self.raise_error("Expected query body after PERFORM")
|
|
750
|
+
|
|
751
|
+
return self.expression(PGPerform(**query.args))
|
|
752
|
+
|
|
753
|
+
def _parse_pgfor(self) -> PGForIn:
|
|
754
|
+
# Parse loop target identifier/variable
|
|
755
|
+
target = self._parse_id_var()
|
|
756
|
+
|
|
757
|
+
self._match_expect(TokenType.IN)
|
|
758
|
+
reverse = self._match(TokenType.REVERSE)
|
|
759
|
+
|
|
760
|
+
# Try range form: <start> .. <end> [BY <step>] LOOP
|
|
761
|
+
start_index = self._index
|
|
762
|
+
start_expr = self._parse_bitwise()
|
|
763
|
+
if start_expr is not None and self._match(TokenType.DDOT):
|
|
764
|
+
end_expr = self._parse_bitwise()
|
|
765
|
+
step_expr = self._parse_bitwise() if self._match(TokenType.BY) else None
|
|
766
|
+
self._match_expect(TokenType.LOOP)
|
|
767
|
+
body = self._parse_loop_body()
|
|
768
|
+
return self.expression(
|
|
769
|
+
PGForIn(
|
|
770
|
+
this=target,
|
|
771
|
+
reverse=reverse or None,
|
|
772
|
+
start=start_expr,
|
|
773
|
+
end=end_expr,
|
|
774
|
+
step=step_expr,
|
|
775
|
+
expressions=body,
|
|
776
|
+
label=self._parse_loop_label(),
|
|
777
|
+
)
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
if reverse:
|
|
781
|
+
self.raise_error("Expected '..' range after REVERSE in FOR header")
|
|
782
|
+
|
|
783
|
+
# Not a range: rewind and parse the query/cursor form
|
|
784
|
+
self._retreat(start_index)
|
|
785
|
+
return self._parse_pgfor_query(target)
|
|
786
|
+
|
|
787
|
+
def _parse_pgfor_query(self, target: exp.Expression) -> PGForIn:
|
|
788
|
+
# If the header begins with EXECUTE, parse via _parse_pgexecute and
|
|
789
|
+
# reject INTO / INTO STRICT in this context, mirroring
|
|
790
|
+
# RETURN QUERY EXECUTE semantics.
|
|
791
|
+
if self._match(TokenType.EXECUTE):
|
|
792
|
+
query = self._parse_pgexecute()
|
|
793
|
+
if query.args.get("expressions") or query.args.get("strict"):
|
|
794
|
+
self.raise_error("INTO is not allowed in FOR IN EXECUTE")
|
|
795
|
+
else:
|
|
796
|
+
start_index = self._index
|
|
797
|
+
query = None
|
|
798
|
+
|
|
799
|
+
cursor = self._parse_id_var()
|
|
800
|
+
if cursor is not None:
|
|
801
|
+
args = None
|
|
802
|
+
if self._match(TokenType.L_PAREN, advance=False):
|
|
803
|
+
args = self._parse_cursor_call_args()
|
|
804
|
+
|
|
805
|
+
# Cursor call only if the header ends here (next token is LOOP)
|
|
806
|
+
if self._match(TokenType.LOOP, advance=False):
|
|
807
|
+
query = self.expression(PGCursorCall(this=cursor, expressions=args))
|
|
808
|
+
|
|
809
|
+
if query is None:
|
|
810
|
+
self._retreat(start_index)
|
|
811
|
+
query = self._parse_statement()
|
|
812
|
+
|
|
813
|
+
self._match_expect(TokenType.LOOP)
|
|
814
|
+
body = self._parse_loop_body()
|
|
815
|
+
return self.expression(
|
|
816
|
+
PGForIn(
|
|
817
|
+
this=target,
|
|
818
|
+
query=query,
|
|
819
|
+
expressions=body,
|
|
820
|
+
label=self._parse_loop_label(),
|
|
821
|
+
)
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
def _parse_pgforeach(self) -> PGForEach:
|
|
825
|
+
# Loop target variable
|
|
826
|
+
target = self._parse_id_var()
|
|
827
|
+
|
|
828
|
+
slice_expr = None
|
|
829
|
+
if self._match_texts(("SLICE",)):
|
|
830
|
+
slice_expr = self._parse_number()
|
|
831
|
+
if slice_expr is None:
|
|
832
|
+
self.raise_error("Expected a number after SLICE")
|
|
833
|
+
|
|
834
|
+
self._match_expect(TokenType.IN)
|
|
835
|
+
self._match_expect(TokenType.ARRAY)
|
|
836
|
+
|
|
837
|
+
# The array expression stops at the LOOP token naturally.
|
|
838
|
+
array_expr = self._parse_bitwise()
|
|
839
|
+
self._match_expect(TokenType.LOOP)
|
|
840
|
+
body = self._parse_loop_body()
|
|
841
|
+
return self.expression(
|
|
842
|
+
PGForEach(
|
|
843
|
+
this=target,
|
|
844
|
+
slice=slice_expr,
|
|
845
|
+
expression=array_expr,
|
|
846
|
+
expressions=body,
|
|
847
|
+
label=self._parse_loop_label(),
|
|
848
|
+
)
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
def _parse_loop_body(self) -> list[exp.Expression]:
|
|
852
|
+
body = self._parse_statement_body(stop_tokens=(TokenType.END,), strict=False)
|
|
853
|
+
self._match_expect(TokenType.END)
|
|
854
|
+
self._match_expect(TokenType.LOOP)
|
|
855
|
+
return body or []
|
|
856
|
+
|
|
857
|
+
def _parse_loop_label(self) -> exp.Expression | None:
|
|
858
|
+
label = self._parse_id_var(any_token=True)
|
|
859
|
+
self._match(TokenType.SEMICOLON)
|
|
860
|
+
return label
|
|
861
|
+
|
|
862
|
+
def _parse_pgassert(self) -> PGAssert:
|
|
863
|
+
condition = self._parse_expression()
|
|
864
|
+
|
|
865
|
+
message = None
|
|
866
|
+
if self._match(TokenType.COMMA):
|
|
867
|
+
message = self._parse_expression()
|
|
868
|
+
|
|
869
|
+
return self.expression(PGAssert(condition=condition, message=message))
|
|
870
|
+
|
|
871
|
+
def _parse_pgraise(self) -> PGRaise:
|
|
872
|
+
level = None
|
|
873
|
+
message: exp.Expression | None = None
|
|
874
|
+
fmt_args: list[exp.Expression] | None = None
|
|
875
|
+
condition = None
|
|
876
|
+
sqlstate: exp.Expression | None = None
|
|
877
|
+
using_args: list[exp.Expression] | None = None
|
|
878
|
+
|
|
879
|
+
# Optional level
|
|
880
|
+
if self._curr is not None and self._curr.text.upper() in self._RAISE_LEVELS:
|
|
881
|
+
# Treat as identifier to keep minimal AST
|
|
882
|
+
token = self._curr
|
|
883
|
+
self._advance()
|
|
884
|
+
ident = exp.to_identifier(token.text)
|
|
885
|
+
ident.update_positions(token)
|
|
886
|
+
level = ident
|
|
887
|
+
|
|
888
|
+
# Helper to parse USING options into PropertyEQ/EQ entries
|
|
889
|
+
def _parse_using_list() -> list[exp.Expression]:
|
|
890
|
+
# Current token should be USING (not yet consumed)
|
|
891
|
+
self._match_expect(TokenType.USING)
|
|
892
|
+
opts: list[exp.Expression] = []
|
|
893
|
+
# Parse CSV of name {:=|=} expr
|
|
894
|
+
first = True
|
|
895
|
+
while True:
|
|
896
|
+
# Name is a simple identifier or variable token
|
|
897
|
+
name = self._parse_id_var(any_token=True)
|
|
898
|
+
if not name:
|
|
899
|
+
if first:
|
|
900
|
+
self.raise_error("Expected option name after USING")
|
|
901
|
+
break
|
|
902
|
+
|
|
903
|
+
pair = self._parse_named_pair(
|
|
904
|
+
name,
|
|
905
|
+
allowed_ops={TokenType.COLON_EQ, TokenType.EQ},
|
|
906
|
+
rhs_parser=self._parse_expression,
|
|
907
|
+
)
|
|
908
|
+
if pair is None:
|
|
909
|
+
self.raise_error("Expected ':=' or '=' in USING option")
|
|
910
|
+
opts.append(pair)
|
|
911
|
+
|
|
912
|
+
first = False
|
|
913
|
+
if not self._match(TokenType.COMMA):
|
|
914
|
+
break
|
|
915
|
+
|
|
916
|
+
return opts
|
|
917
|
+
|
|
918
|
+
# Branch on next token for main payload
|
|
919
|
+
if self._curr is None or self._index >= self._tokens_size:
|
|
920
|
+
# Bare re-raise
|
|
921
|
+
pass
|
|
922
|
+
elif self._match_texts("SQLSTATE"):
|
|
923
|
+
sqlstate = self._parse_sqlstate()
|
|
924
|
+
elif self._match(TokenType.STRING, advance=False):
|
|
925
|
+
# Message/format expression (typically a string)
|
|
926
|
+
message = self._parse_expression()
|
|
927
|
+
# Optional CSV of expressions
|
|
928
|
+
args: list[exp.Expression] = []
|
|
929
|
+
while self._match(TokenType.COMMA):
|
|
930
|
+
args.append(self._parse_expression())
|
|
931
|
+
if args:
|
|
932
|
+
fmt_args = args
|
|
933
|
+
elif self._match(TokenType.USING, advance=False):
|
|
934
|
+
# USING-only form
|
|
935
|
+
using_args = _parse_using_list()
|
|
936
|
+
else:
|
|
937
|
+
# Condition name (identifier)
|
|
938
|
+
condition = self._parse_id_var(any_token=True)
|
|
939
|
+
|
|
940
|
+
# Optional USING after message/condition/sqlstate
|
|
941
|
+
if using_args is None and self._match(TokenType.USING, advance=False):
|
|
942
|
+
using_args = _parse_using_list()
|
|
943
|
+
|
|
944
|
+
return self.expression(
|
|
945
|
+
PGRaise(
|
|
946
|
+
level=level,
|
|
947
|
+
message=message,
|
|
948
|
+
expressions=fmt_args,
|
|
949
|
+
condition=condition,
|
|
950
|
+
sqlstate=sqlstate,
|
|
951
|
+
using=using_args,
|
|
952
|
+
)
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
def _parse_pgreturn(self) -> PGReturn:
|
|
956
|
+
# Support RETURN QUERY <query>; and RETURN QUERY EXECUTE ... [USING ...];
|
|
957
|
+
if self._match_texts("QUERY"):
|
|
958
|
+
# Fast-path for dynamic EXECUTE variant
|
|
959
|
+
if self._match(TokenType.EXECUTE):
|
|
960
|
+
exec_node = self._parse_pgexecute()
|
|
961
|
+
|
|
962
|
+
# Prohibit INTO / INTO STRICT in RETURN QUERY EXECUTE form
|
|
963
|
+
if exec_node.args.get("expressions") or exec_node.args.get("strict"):
|
|
964
|
+
self.raise_error("INTO is not allowed in RETURN QUERY EXECUTE")
|
|
965
|
+
|
|
966
|
+
return self.expression(PGReturn(this=exec_node, query=True))
|
|
967
|
+
|
|
968
|
+
# Otherwise parse a full query-producing statement (SELECT/WITH/VALUES/INSERT ... RETURNING)
|
|
969
|
+
query = self._parse_statement()
|
|
970
|
+
if query is None:
|
|
971
|
+
self.raise_error("Expected query after RETURN QUERY")
|
|
972
|
+
return self.expression(PGReturn(this=query, query=True))
|
|
973
|
+
|
|
974
|
+
# Support RETURN NEXT <expression>;
|
|
975
|
+
if self._match_texts("NEXT"):
|
|
976
|
+
# NEXT must be followed by an expression on the same chunk
|
|
977
|
+
if self._curr is None or self._index >= self._tokens_size:
|
|
978
|
+
self.raise_error("Expected expression after RETURN NEXT")
|
|
979
|
+
expr = self._parse_expression()
|
|
980
|
+
return self.expression(PGReturn(this=expr, next=True))
|
|
981
|
+
|
|
982
|
+
# Otherwise, optional expression on the same chunk before semicolon.
|
|
983
|
+
# If end of chunk reached immediately, it's a bare RETURN.
|
|
984
|
+
expr: exp.Expression | None = None
|
|
985
|
+
if self._curr is not None and self._index < self._tokens_size:
|
|
986
|
+
expr = self._parse_expression()
|
|
987
|
+
|
|
988
|
+
return self.expression(PGReturn(this=expr))
|
|
989
|
+
|
|
990
|
+
def _parse_pgloop(self) -> PGLoop:
|
|
991
|
+
# Parse body statements until END using the shared helper
|
|
992
|
+
body: list[exp.Expression] = self._parse_statement_body(
|
|
993
|
+
stop_tokens=(TokenType.END,),
|
|
994
|
+
error_msg="Invalid expression inside LOOP / Unexpected token",
|
|
995
|
+
strict=True,
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
# Consume END and then require trailing LOOP
|
|
999
|
+
self._match_expect(TokenType.END)
|
|
1000
|
+
self._match_expect(TokenType.LOOP)
|
|
1001
|
+
# Optional trailing label and statement terminator
|
|
1002
|
+
label = self._parse_loop_label()
|
|
1003
|
+
|
|
1004
|
+
return self.expression(PGLoop(body=body, end_label=label))
|
|
1005
|
+
|
|
1006
|
+
def _parse_pgexit_or_continue(self, *, token_type: TokenType, expr_cls: type[exp.Expression]):
|
|
1007
|
+
"""Parse EXIT/CONTINUE constructs which share the same grammar.
|
|
1008
|
+
|
|
1009
|
+
Syntax: <KEYWORD> [label] [WHEN <expr>]
|
|
1010
|
+
"""
|
|
1011
|
+
label = None
|
|
1012
|
+
condition = None
|
|
1013
|
+
|
|
1014
|
+
# Optional label unless the next token starts a WHEN clause
|
|
1015
|
+
if self._curr is not None and self._curr.text.upper() != "WHEN":
|
|
1016
|
+
label = self._parse_id_var()
|
|
1017
|
+
|
|
1018
|
+
# Optional WHEN <expression>
|
|
1019
|
+
if self._match(TokenType.WHEN):
|
|
1020
|
+
condition = self._parse_expression()
|
|
1021
|
+
|
|
1022
|
+
# Build node. For EXIT, use the PL/pgSQL subclass (PGExit) which extends
|
|
1023
|
+
# the shared exp.Leave with an optional WHEN clause.
|
|
1024
|
+
return self.expression(expr_cls(this=label, when=condition))
|
|
1025
|
+
|
|
1026
|
+
def _parse_pgexit(self) -> PGExit:
|
|
1027
|
+
return self._parse_pgexit_or_continue(token_type=TokenType.EXIT, expr_cls=PGExit)
|
|
1028
|
+
|
|
1029
|
+
def _parse_pgcontinue(self) -> PGContinue:
|
|
1030
|
+
return self._parse_pgexit_or_continue(token_type=TokenType.CONTINUE, expr_cls=PGContinue)
|
|
1031
|
+
|
|
1032
|
+
def _parse_cursor_call_args(self) -> list[exp.Expression]:
|
|
1033
|
+
"""Parse a parenthesized cursor argument list and consume the closing ')'."""
|
|
1034
|
+
self._advance()
|
|
1035
|
+
|
|
1036
|
+
args: list[exp.Expression] = []
|
|
1037
|
+
if not self._match(TokenType.R_PAREN, advance=False):
|
|
1038
|
+
while True:
|
|
1039
|
+
first = self._parse_expression()
|
|
1040
|
+
pair = self._parse_named_pair(
|
|
1041
|
+
first,
|
|
1042
|
+
allowed_ops={TokenType.COLON_EQ, TokenType.FARROW},
|
|
1043
|
+
rhs_parser=self._parse_expression,
|
|
1044
|
+
)
|
|
1045
|
+
args.append(pair or first)
|
|
1046
|
+
|
|
1047
|
+
if not self._match(TokenType.COMMA):
|
|
1048
|
+
break
|
|
1049
|
+
if self._match(TokenType.R_PAREN, advance=False):
|
|
1050
|
+
self.raise_error("Trailing comma is not allowed in cursor argument list")
|
|
1051
|
+
|
|
1052
|
+
self._match_expect(TokenType.R_PAREN)
|
|
1053
|
+
|
|
1054
|
+
return args
|
|
1055
|
+
|
|
1056
|
+
def _parse_pgopen(self) -> PGOpenCursor:
|
|
1057
|
+
# Cursor variable identifier
|
|
1058
|
+
cursor = self._parse_id_var()
|
|
1059
|
+
|
|
1060
|
+
# Bound cursor with arguments: OPEN c(<args>) where args can be positional,
|
|
1061
|
+
# name := value (PropertyEQ), or name => value (Kwarg)
|
|
1062
|
+
if self._match(TokenType.L_PAREN, advance=False):
|
|
1063
|
+
# Parse cursor call arguments directly (positional, name := value, name => value)
|
|
1064
|
+
args = self._parse_cursor_call_args()
|
|
1065
|
+
return self.expression(PGOpenCursor(this=cursor, expressions=args))
|
|
1066
|
+
|
|
1067
|
+
# Optional [[NO] SCROLL]
|
|
1068
|
+
scroll: bool | None = None
|
|
1069
|
+
if self._match_texts("NO"):
|
|
1070
|
+
self._match_text_expect("SCROLL")
|
|
1071
|
+
scroll = False
|
|
1072
|
+
elif self._match_texts("SCROLL"):
|
|
1073
|
+
scroll = True
|
|
1074
|
+
|
|
1075
|
+
# If there's no FOR and no SCROLL/NO SCROLL, treat as bound cursor with zero args: OPEN c;
|
|
1076
|
+
if not self._match(TokenType.FOR):
|
|
1077
|
+
if scroll is None:
|
|
1078
|
+
return self.expression(PGOpenCursor(this=cursor))
|
|
1079
|
+
# If SCROLL/NO SCROLL was provided, FOR is required
|
|
1080
|
+
self._match_expect(TokenType.FOR)
|
|
1081
|
+
|
|
1082
|
+
if self._match(TokenType.EXECUTE):
|
|
1083
|
+
query = self._parse_pgexecute()
|
|
1084
|
+
if query.args["expressions"]:
|
|
1085
|
+
self.raise_error("INTO is not allowed in OPEN FOR EXECUTE")
|
|
1086
|
+
else:
|
|
1087
|
+
query = self._parse_statement()
|
|
1088
|
+
if query is None:
|
|
1089
|
+
self.raise_error("Expected query after OPEN ... FOR")
|
|
1090
|
+
|
|
1091
|
+
return self.expression(PGOpenCursor(this=cursor, expression=query, scroll=scroll))
|
|
1092
|
+
|
|
1093
|
+
def _parse_pgfetch(self) -> PGFetch:
|
|
1094
|
+
# Parse optional direction, optional preposition (required when direction given), and cursor
|
|
1095
|
+
direction, preposition, cursor = self._parse_pg_direction_and_cursor(after_kw="FETCH")
|
|
1096
|
+
|
|
1097
|
+
# INTO keyword
|
|
1098
|
+
self._match_expect(TokenType.INTO)
|
|
1099
|
+
|
|
1100
|
+
# One or more targets separated by commas on the same chunk
|
|
1101
|
+
targets = self._parse_csv(self._parse_expression)
|
|
1102
|
+
|
|
1103
|
+
if not targets:
|
|
1104
|
+
self.raise_error("Expected target list after INTO in FETCH")
|
|
1105
|
+
|
|
1106
|
+
return self.expression(
|
|
1107
|
+
PGFetch(
|
|
1108
|
+
this=cursor,
|
|
1109
|
+
expressions=targets,
|
|
1110
|
+
direction=direction,
|
|
1111
|
+
preposition=preposition,
|
|
1112
|
+
)
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
def _parse_pgmove(self) -> "PGMove":
|
|
1116
|
+
# Parse optional direction, optional preposition (required when direction given), and cursor
|
|
1117
|
+
direction, preposition, cursor = self._parse_pg_direction_and_cursor(after_kw="MOVE")
|
|
1118
|
+
|
|
1119
|
+
return self.expression(
|
|
1120
|
+
PGMove(
|
|
1121
|
+
this=cursor,
|
|
1122
|
+
direction=direction,
|
|
1123
|
+
preposition=preposition,
|
|
1124
|
+
)
|
|
1125
|
+
)
|
|
1126
|
+
|
|
1127
|
+
def _parse_pgclose(self) -> "PGClose":
|
|
1128
|
+
cursor = self._parse_id_var()
|
|
1129
|
+
return self.expression(PGClose(this=cursor))
|
|
1130
|
+
|
|
1131
|
+
def _parse_pgexecute(self) -> PGExecute:
|
|
1132
|
+
# Command-string: any scalar expression (literal, '||' concat, function call).
|
|
1133
|
+
# _parse_expression naturally stops at INTO / USING (non-alias tokens).
|
|
1134
|
+
command = self._parse_expression()
|
|
1135
|
+
if command is None:
|
|
1136
|
+
self.raise_error("Expected command string after EXECUTE")
|
|
1137
|
+
|
|
1138
|
+
targets: list[exp.Expression] | None = None
|
|
1139
|
+
strict = False
|
|
1140
|
+
using: list[exp.Expression] | None = None
|
|
1141
|
+
|
|
1142
|
+
# Optional INTO [STRICT] target [, ...]
|
|
1143
|
+
if self._match(TokenType.INTO):
|
|
1144
|
+
# STRICT is tokenized as VAR, so match by text
|
|
1145
|
+
strict = self._match_texts(("STRICT",))
|
|
1146
|
+
|
|
1147
|
+
# One or more targets separated by commas
|
|
1148
|
+
targets = self._parse_csv(self._parse_expression)
|
|
1149
|
+
if not targets:
|
|
1150
|
+
self.raise_error("Expected target list after INTO in EXECUTE")
|
|
1151
|
+
|
|
1152
|
+
# Optional USING expression [, ...]
|
|
1153
|
+
if self._match(TokenType.USING):
|
|
1154
|
+
# Require at least one expression and disallow trailing commas
|
|
1155
|
+
first = self._parse_expression()
|
|
1156
|
+
if first is None:
|
|
1157
|
+
self.raise_error("Expected expression list after USING in EXECUTE")
|
|
1158
|
+
using = [first]
|
|
1159
|
+
while self._match(TokenType.COMMA):
|
|
1160
|
+
nxt = self._parse_expression()
|
|
1161
|
+
if nxt is None:
|
|
1162
|
+
self.raise_error("Expected expression after comma in USING list")
|
|
1163
|
+
using.append(nxt)
|
|
1164
|
+
|
|
1165
|
+
return self.expression(
|
|
1166
|
+
PGExecute(
|
|
1167
|
+
this=command,
|
|
1168
|
+
expressions=targets,
|
|
1169
|
+
strict=strict,
|
|
1170
|
+
using=using or [],
|
|
1171
|
+
)
|
|
1172
|
+
)
|
|
1173
|
+
|
|
1174
|
+
def _parse_pg_direction_and_cursor(
|
|
1175
|
+
self, *, after_kw: str
|
|
1176
|
+
) -> tuple[exp.Expression | None, str | None, exp.Expression]:
|
|
1177
|
+
"""
|
|
1178
|
+
Parse optional direction and required preposition (if direction present), then cursor name.
|
|
1179
|
+
"""
|
|
1180
|
+
direction: exp.Expression | None = None
|
|
1181
|
+
preposition: str | None = None
|
|
1182
|
+
|
|
1183
|
+
def _parse_required_preposition(msg: str) -> str:
|
|
1184
|
+
if self._match_texts(("FROM", "IN")):
|
|
1185
|
+
return (self._prev.text or "").upper()
|
|
1186
|
+
self.raise_error(msg)
|
|
1187
|
+
return ""
|
|
1188
|
+
|
|
1189
|
+
# Handle FORWARD/BACKWARD with optional count/ALL
|
|
1190
|
+
if self._match_texts(("FORWARD", "BACKWARD")):
|
|
1191
|
+
which = (self._prev.text or "").upper()
|
|
1192
|
+
cls = PGForward if which == "FORWARD" else PGBackward
|
|
1193
|
+
# Optional ALL or count after FORWARD/BACKWARD
|
|
1194
|
+
if self._match_texts("ALL"):
|
|
1195
|
+
direction = self.expression(cls(all=True))
|
|
1196
|
+
else:
|
|
1197
|
+
# A number
|
|
1198
|
+
if self._match_set((TokenType.NUMBER, TokenType.DASH), advance=False):
|
|
1199
|
+
count_expr = self._parse_bitwise()
|
|
1200
|
+
direction = self.expression(cls(this=count_expr))
|
|
1201
|
+
else:
|
|
1202
|
+
direction = self.expression(cls())
|
|
1203
|
+
|
|
1204
|
+
preposition = _parse_required_preposition(
|
|
1205
|
+
f"Expected FROM or IN after {after_kw} direction"
|
|
1206
|
+
)
|
|
1207
|
+
|
|
1208
|
+
elif self._match_texts(tuple(self._SIMPLE_FETCH_DIRS.keys())):
|
|
1209
|
+
kw = (self._prev.text or "").upper()
|
|
1210
|
+
direction = self.expression(self._SIMPLE_FETCH_DIRS[kw]())
|
|
1211
|
+
preposition = _parse_required_preposition(
|
|
1212
|
+
f"Expected FROM or IN after {after_kw} direction"
|
|
1213
|
+
)
|
|
1214
|
+
elif self._match_texts(("ABSOLUTE", "RELATIVE")):
|
|
1215
|
+
kind = (self._prev.text or "").upper()
|
|
1216
|
+
count_expr = self._parse_bitwise()
|
|
1217
|
+
direction = (
|
|
1218
|
+
self.expression(PGAbsolute(this=count_expr))
|
|
1219
|
+
if kind == "ABSOLUTE"
|
|
1220
|
+
else self.expression(PGRelative(this=count_expr))
|
|
1221
|
+
)
|
|
1222
|
+
preposition = _parse_required_preposition(
|
|
1223
|
+
f"Expected FROM or IN after {after_kw} ABSOLUTE/RELATIVE count"
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1226
|
+
else:
|
|
1227
|
+
# Standalone ALL or standalone count (equivalent to FORWARD ALL / FORWARD count)
|
|
1228
|
+
if self._match_texts("ALL"):
|
|
1229
|
+
direction = self.expression(PGAll())
|
|
1230
|
+
preposition = _parse_required_preposition(
|
|
1231
|
+
f"Expected FROM or IN after {after_kw} ALL"
|
|
1232
|
+
)
|
|
1233
|
+
elif self._match_set((TokenType.NUMBER, TokenType.DASH), advance=False):
|
|
1234
|
+
count_expr = self._parse_bitwise()
|
|
1235
|
+
direction = count_expr
|
|
1236
|
+
preposition = _parse_required_preposition(
|
|
1237
|
+
f"Expected FROM or IN after {after_kw} count"
|
|
1238
|
+
)
|
|
1239
|
+
else:
|
|
1240
|
+
if self._match_texts(("FROM", "IN")):
|
|
1241
|
+
preposition = (self._prev.text or "").upper()
|
|
1242
|
+
|
|
1243
|
+
cursor = self._parse_id_var()
|
|
1244
|
+
return direction, preposition, cursor
|