get-objects-lib 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.
- get_objects_lib/__init__.py +12 -0
- get_objects_lib/dialect/__init__.py +3 -0
- get_objects_lib/dialect/dialect.py +11 -0
- get_objects_lib/dialect/expressions.py +116 -0
- get_objects_lib/dialect/generator.py +231 -0
- get_objects_lib/dialect/parser/__init__.py +3 -0
- get_objects_lib/dialect/parser/base.py +278 -0
- get_objects_lib/dialect/parser/non_semicolon.py +182 -0
- get_objects_lib/dialect/parser/sql_server.py +512 -0
- get_objects_lib/dialect/text_utils.py +22 -0
- get_objects_lib/dialect/tokenizer.py +51 -0
- get_objects_lib/objects/__init__.py +3 -0
- get_objects_lib/objects/dependencies.py +162 -0
- get_objects_lib/objects/header.py +160 -0
- get_objects_lib/objects/standardize.py +72 -0
- get_objects_lib-0.1.0.dist-info/METADATA +231 -0
- get_objects_lib-0.1.0.dist-info/RECORD +18 -0
- get_objects_lib-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
from sqlglot import exp, TokenType
|
|
4
|
+
from sqlglot.parser import Parser
|
|
5
|
+
|
|
6
|
+
from get_objects_lib.dialect.expressions import (
|
|
7
|
+
AlterObject, BatchSeparator, BeginEnd, CursorStatement, DeclareCursor, ExecParameter, FetchCursor, Goto, Label, Top, TryCatch, WaitFor, CompoundAssignment, TriggerSpec, ExecuteAt, InlineIndex, LoopControl, Print, RaiseError, ReturnStatement,
|
|
8
|
+
SaveTransaction, Throw,
|
|
9
|
+
)
|
|
10
|
+
from get_objects_lib.dialect.parser.non_semicolon import NonSemiColonParser
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# sqlglot normaliza estas funciones para traducir entre dialectos y al generar
|
|
14
|
+
# T-SQL las cambia: SYSDATETIME() -> GETDATE() (menos precision), SUSER_SNAME()
|
|
15
|
+
# -> CURRENT_USER() (otro dato), DATENAME -> FORMAT, DATEDIFF/EOMONTH con CAST
|
|
16
|
+
# agregados. Se dejan como funciones genericas para conservarlas tal cual.
|
|
17
|
+
PRESERVED_FUNCTIONS = (
|
|
18
|
+
"DATEDIFF", "DATEDIFF_BIG", "DATENAME", "EOMONTH", "SYSDATETIME", "SUSER_SNAME", "SUSER_NAME", "SPACE",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SqlServerParser(NonSemiColonParser):
|
|
23
|
+
# sqlglot escribe UNCOMITTED (con una M) y no tiene SNAPSHOT
|
|
24
|
+
TRANSACTION_CHARACTERISTICS = {
|
|
25
|
+
**NonSemiColonParser.TRANSACTION_CHARACTERISTICS,
|
|
26
|
+
"ISOLATION": (
|
|
27
|
+
("LEVEL", "READ", "UNCOMMITTED"), ("LEVEL", "READ", "COMMITTED"), ("LEVEL", "REPEATABLE", "READ"),
|
|
28
|
+
("LEVEL", "SNAPSHOT"), ("LEVEL", "SERIALIZABLE"),
|
|
29
|
+
),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
FUNCTIONS = {k: v for k, v in NonSemiColonParser.FUNCTIONS.items() if k not in PRESERVED_FUNCTIONS}
|
|
33
|
+
|
|
34
|
+
ALTER_AS_CREATE = (TokenType.PROCEDURE, TokenType.FUNCTION, TokenType.VIEW, TokenType.TRIGGER)
|
|
35
|
+
|
|
36
|
+
# UPDATE(columna) dentro de un trigger
|
|
37
|
+
FUNC_TOKENS = {*NonSemiColonParser.FUNC_TOKENS, TokenType.UPDATE}
|
|
38
|
+
|
|
39
|
+
def _parse_alter(self) -> exp.Expr:
|
|
40
|
+
# sqlglot solo sabe alterar tablas; con PROCEDURE/FUNCTION cae a
|
|
41
|
+
# Command. Su sintaxis es la misma que la del CREATE, asi que
|
|
42
|
+
# reutilizamos _parse_create (self._prev sigue siendo el ALTER).
|
|
43
|
+
if not self._match_set(self.ALTER_AS_CREATE, advance=False):
|
|
44
|
+
return super()._parse_alter()
|
|
45
|
+
|
|
46
|
+
create = self._parse_create()
|
|
47
|
+
if not isinstance(create, exp.Create):
|
|
48
|
+
return create
|
|
49
|
+
|
|
50
|
+
return self.expression(AlterObject(**create.args))
|
|
51
|
+
|
|
52
|
+
def _parse_create(self) -> exp.Create | exp.Command:
|
|
53
|
+
# CREATE [OR ALTER] TRIGGER: sqlglot no soporta la sintaxis de T-SQL
|
|
54
|
+
index = self._index
|
|
55
|
+
replace = self._match_pair(TokenType.OR, TokenType.ALTER)
|
|
56
|
+
if self._match(TokenType.TRIGGER):
|
|
57
|
+
return self._parse_trigger(replace)
|
|
58
|
+
self._retreat(index)
|
|
59
|
+
|
|
60
|
+
create = super()._parse_create()
|
|
61
|
+
if not isinstance(create, exp.Create):
|
|
62
|
+
return create
|
|
63
|
+
|
|
64
|
+
# Si el cuerpo BEGIN ... END empieza con RETURN, sqlglot envuelve el
|
|
65
|
+
# bloque completo en un Return; el Return real ya esta dentro del bloque.
|
|
66
|
+
body = create.args.get("expression")
|
|
67
|
+
if isinstance(body, exp.Return) and isinstance(body.this, exp.Block):
|
|
68
|
+
create.set("expression", body.this)
|
|
69
|
+
|
|
70
|
+
# Con los parametros entre parentesis sqlglot arma un
|
|
71
|
+
# UserDefinedFunction aunque sea un PROCEDURE.
|
|
72
|
+
if create.kind in ("PROCEDURE", "PROC") and isinstance(create.this, exp.UserDefinedFunction):
|
|
73
|
+
create.set("this", exp.StoredProcedure(**create.this.args))
|
|
74
|
+
|
|
75
|
+
return create
|
|
76
|
+
|
|
77
|
+
def _parse_field_def(self) -> exp.Expr | None:
|
|
78
|
+
# INDEX nombre (...) en CREATE TABLE / DECLARE @t TABLE: sqlglot lo
|
|
79
|
+
# tomaba como una columna llamada INDEX
|
|
80
|
+
if self._match(TokenType.INDEX):
|
|
81
|
+
return self._parse_inline_index()
|
|
82
|
+
return super()._parse_field_def()
|
|
83
|
+
|
|
84
|
+
def _parse_inline_index(self) -> InlineIndex:
|
|
85
|
+
this = self._parse_id_var(any_token=False)
|
|
86
|
+
unique = self._match(TokenType.UNIQUE)
|
|
87
|
+
clustered = self._match_texts(('CLUSTERED', 'NONCLUSTERED')) and self._prev.text.upper()
|
|
88
|
+
expressions = self._parse_wrapped_csv(self._parse_ordered)
|
|
89
|
+
include = self._parse_wrapped_csv(self._parse_id_var) if self._match_text_seq('INCLUDE') else None
|
|
90
|
+
where = self._parse_where()
|
|
91
|
+
options = self._parse_wrapped_csv(self._parse_expression) if self._match(TokenType.WITH) else None
|
|
92
|
+
return self.expression(InlineIndex(
|
|
93
|
+
this=this, unique=unique, clustered=clustered or None, expressions=expressions,
|
|
94
|
+
include=include, where=where, options=options,
|
|
95
|
+
))
|
|
96
|
+
|
|
97
|
+
def _parse_trigger(self, replace: bool) -> exp.Create:
|
|
98
|
+
this = self._parse_table_parts()
|
|
99
|
+
self._match(TokenType.ON)
|
|
100
|
+
if self._match_text_seq('ALL', 'SERVER'):
|
|
101
|
+
table = exp.var('ALL SERVER')
|
|
102
|
+
elif self._match(TokenType.DATABASE):
|
|
103
|
+
table = exp.var('DATABASE')
|
|
104
|
+
else:
|
|
105
|
+
table = self._parse_table_parts()
|
|
106
|
+
|
|
107
|
+
options = self._parse_csv(self._parse_procedure_option) if self._match(TokenType.WITH) else None
|
|
108
|
+
|
|
109
|
+
if self._match_text_seq('INSTEAD', 'OF'):
|
|
110
|
+
timing = 'INSTEAD OF'
|
|
111
|
+
else:
|
|
112
|
+
self._match_texts(('FOR', 'AFTER'))
|
|
113
|
+
timing = self._prev.text.upper()
|
|
114
|
+
|
|
115
|
+
events = self._parse_csv(lambda: self._parse_var(any_token=True))
|
|
116
|
+
not_for_replication = self._match_text_seq('NOT', 'FOR', 'REPLICATION')
|
|
117
|
+
|
|
118
|
+
self._match(TokenType.ALIAS)
|
|
119
|
+
begin = self._match(TokenType.BEGIN)
|
|
120
|
+
body = self._parse_block()
|
|
121
|
+
|
|
122
|
+
spec = TriggerSpec(
|
|
123
|
+
table=table, options=options or None, timing=timing, events=events,
|
|
124
|
+
not_for_replication=not_for_replication,
|
|
125
|
+
)
|
|
126
|
+
return self.expression(exp.Create(
|
|
127
|
+
this=this, kind='TRIGGER', replace=replace, expression=body, begin=begin,
|
|
128
|
+
properties=exp.Properties(expressions=[spec]),
|
|
129
|
+
))
|
|
130
|
+
|
|
131
|
+
def _parse_user_defined_function_expression(self) -> exp.Expr | None:
|
|
132
|
+
# En T-SQL el BEGIN ... END de una funcion es un bloque de sentencias,
|
|
133
|
+
# igual que el de un procedimiento. sqlglot ya consumio el BEGIN y,
|
|
134
|
+
# si la primera sentencia es RETURN, tambien ese RETURN.
|
|
135
|
+
prev_index = self._index - 2
|
|
136
|
+
if (
|
|
137
|
+
self._prev.text.upper() == "RETURN"
|
|
138
|
+
and prev_index >= 0
|
|
139
|
+
and self._tokens[prev_index].token_type == TokenType.BEGIN
|
|
140
|
+
):
|
|
141
|
+
self._retreat(self._index - 1)
|
|
142
|
+
|
|
143
|
+
if self._prev.token_type == TokenType.BEGIN:
|
|
144
|
+
return self._parse_block()
|
|
145
|
+
|
|
146
|
+
return super()._parse_user_defined_function_expression()
|
|
147
|
+
|
|
148
|
+
def _parse_statement(self) -> exp.Expr | None:
|
|
149
|
+
# Sentencias que no modelamos: la sentencia completa como Command
|
|
150
|
+
if self._curr and (
|
|
151
|
+
self._curr.token_type == TokenType.COMMAND
|
|
152
|
+
or self._is_generic_command(self._curr, self._next)
|
|
153
|
+
or (self._curr.token_type == TokenType.EXECUTE and self._next and self._next.token_type == TokenType.ALIAS)
|
|
154
|
+
):
|
|
155
|
+
self._advance()
|
|
156
|
+
return self._parse_known_command(self._prev)
|
|
157
|
+
|
|
158
|
+
# El separador de lotes ya viene aislado en su propio chunk
|
|
159
|
+
if self._curr and self._batch_separator_length(self._tokens, self._index):
|
|
160
|
+
self._advance()
|
|
161
|
+
count = self._parse_number() if self._curr else None
|
|
162
|
+
return self.expression(BatchSeparator(this=count))
|
|
163
|
+
|
|
164
|
+
if self._curr and self._curr.token_type == TokenType.DECLARE and self._is_word(self._peek(2), 'CURSOR'):
|
|
165
|
+
return self._parse_declare_cursor()
|
|
166
|
+
|
|
167
|
+
if self._curr and self._curr.token_type == TokenType.FETCH:
|
|
168
|
+
return self._parse_fetch_cursor()
|
|
169
|
+
|
|
170
|
+
if self._is_word(self._curr, 'OPEN', 'CLOSE', 'DEALLOCATE'):
|
|
171
|
+
kind = self._curr.text.upper()
|
|
172
|
+
self._advance()
|
|
173
|
+
global_ = self._match_text_seq('GLOBAL')
|
|
174
|
+
return self.expression(CursorStatement(kind=kind, this=self._parse_cursor_name(), global_=global_))
|
|
175
|
+
|
|
176
|
+
# BEGIN TRY ... END TRY BEGIN CATCH ... END CATCH
|
|
177
|
+
if self._is_word(self._curr, 'BEGIN') and self._is_word(self._next, 'TRY'):
|
|
178
|
+
return self._parse_try_catch()
|
|
179
|
+
|
|
180
|
+
# BEGIN ... END suelto (BEGIN TRAN lo resuelve STATEMENT_PARSERS)
|
|
181
|
+
if self._curr and self._is_begin(self._curr, self._next):
|
|
182
|
+
self._advance()
|
|
183
|
+
return self.expression(BeginEnd(this=self._parse_block()))
|
|
184
|
+
|
|
185
|
+
keyword = self._curr.text.upper() if self._curr and self._curr.token_type == TokenType.VAR else None
|
|
186
|
+
|
|
187
|
+
if self._curr and self._is_label(self._curr, self._prev, self._next):
|
|
188
|
+
self._advance(2) # nombre y ":"
|
|
189
|
+
return self.expression(Label(this=exp.to_identifier(self._tokens[self._index - 2].text)))
|
|
190
|
+
|
|
191
|
+
if keyword == "GOTO":
|
|
192
|
+
self._advance()
|
|
193
|
+
return self.expression(Goto(this=self._parse_id_var(any_token=True)))
|
|
194
|
+
|
|
195
|
+
if keyword == "WAITFOR" and self._is_word(self._next, "DELAY", "TIME"):
|
|
196
|
+
self._advance(2)
|
|
197
|
+
return self.expression(WaitFor(kind=self._prev.text.upper(), this=self._parse_expression()))
|
|
198
|
+
|
|
199
|
+
if keyword == "PRINT":
|
|
200
|
+
self._advance()
|
|
201
|
+
return self.expression(Print(this=self._parse_expression()))
|
|
202
|
+
|
|
203
|
+
if keyword == "RAISERROR":
|
|
204
|
+
self._advance()
|
|
205
|
+
expressions = self._parse_wrapped_csv(self._parse_expression)
|
|
206
|
+
options = self._parse_csv(self._parse_var) if self._match(TokenType.WITH) else None
|
|
207
|
+
return self.expression(RaiseError(expressions=expressions, options=options))
|
|
208
|
+
|
|
209
|
+
if keyword == "THROW":
|
|
210
|
+
self._advance()
|
|
211
|
+
return self.expression(Throw(expressions=self._parse_csv(self._parse_expression) if self._curr else None))
|
|
212
|
+
|
|
213
|
+
if keyword == "SAVE" and self._next and self._next.text.upper() in ("TRAN", "TRANSACTION"):
|
|
214
|
+
self._advance(2)
|
|
215
|
+
return self.expression(SaveTransaction(this=self._parse_id_var() or self._parse_primary_or_var()))
|
|
216
|
+
|
|
217
|
+
if keyword in ("BREAK", "CONTINUE"):
|
|
218
|
+
self._advance()
|
|
219
|
+
return self.expression(LoopControl(this=self._prev.text.upper()))
|
|
220
|
+
|
|
221
|
+
# RETURN [expresion]: el chunk termina donde termina la sentencia,
|
|
222
|
+
# asi que lo que quede en el es su valor.
|
|
223
|
+
if self._curr and self._curr.token_type == TokenType.VAR and self._curr.text.upper() == "RETURN":
|
|
224
|
+
self._advance()
|
|
225
|
+
this = self._parse_expression() if self._curr else None
|
|
226
|
+
return self.expression(ReturnStatement(this=this))
|
|
227
|
+
|
|
228
|
+
return super()._parse_statement()
|
|
229
|
+
|
|
230
|
+
def _peek(self, offset: int):
|
|
231
|
+
index = self._index + offset
|
|
232
|
+
return self._tokens[index] if index < len(self._tokens) else None
|
|
233
|
+
|
|
234
|
+
def _parse_cursor_name(self) -> exp.Expr | None:
|
|
235
|
+
if self._match(TokenType.PARAMETER):
|
|
236
|
+
return self._parse_parameter()
|
|
237
|
+
return self._parse_id_var(any_token=True)
|
|
238
|
+
|
|
239
|
+
def _parse_declare_cursor(self) -> DeclareCursor:
|
|
240
|
+
self._advance() # DECLARE
|
|
241
|
+
this = self._parse_id_var(any_token=True)
|
|
242
|
+
self._match_text_seq('CURSOR')
|
|
243
|
+
|
|
244
|
+
options = []
|
|
245
|
+
while self._curr and self._curr.token_type != TokenType.FOR:
|
|
246
|
+
options.append(self._parse_var(any_token=True))
|
|
247
|
+
self._match(TokenType.FOR)
|
|
248
|
+
query = self._parse_select()
|
|
249
|
+
|
|
250
|
+
for_update = read_only = False
|
|
251
|
+
columns = None
|
|
252
|
+
if self._match(TokenType.FOR):
|
|
253
|
+
if self._match_text_seq('READ', 'ONLY'):
|
|
254
|
+
read_only = True
|
|
255
|
+
elif self._match(TokenType.UPDATE):
|
|
256
|
+
for_update = True
|
|
257
|
+
if self._match_text_seq('OF'):
|
|
258
|
+
columns = self._parse_csv(self._parse_column)
|
|
259
|
+
|
|
260
|
+
return self.expression(DeclareCursor(
|
|
261
|
+
this=this, options=options or None, expression=query,
|
|
262
|
+
for_update=for_update, columns=columns, read_only=read_only,
|
|
263
|
+
))
|
|
264
|
+
|
|
265
|
+
def _parse_fetch_cursor(self) -> FetchCursor:
|
|
266
|
+
self._advance() # FETCH
|
|
267
|
+
direction = count = None
|
|
268
|
+
if self._match_texts(('NEXT', 'PRIOR', 'FIRST', 'LAST')):
|
|
269
|
+
direction = self._prev.text.upper()
|
|
270
|
+
elif self._match_texts(('ABSOLUTE', 'RELATIVE')):
|
|
271
|
+
direction = self._prev.text.upper()
|
|
272
|
+
count = self._parse_term()
|
|
273
|
+
self._match(TokenType.FROM)
|
|
274
|
+
|
|
275
|
+
global_ = self._match_text_seq('GLOBAL')
|
|
276
|
+
this = self._parse_cursor_name()
|
|
277
|
+
into = self._parse_csv(self._parse_primary_or_var) if self._match(TokenType.INTO) else None
|
|
278
|
+
|
|
279
|
+
return self.expression(FetchCursor(this=this, direction=direction, count=count, global_=global_, into=into))
|
|
280
|
+
|
|
281
|
+
def _parse_known_command(self, start) -> exp.Command:
|
|
282
|
+
"""
|
|
283
|
+
Como _parse_as_command de sqlglot, pero sin su aviso de "unsupported
|
|
284
|
+
syntax": estas sentencias se dejan como Command a proposito.
|
|
285
|
+
"""
|
|
286
|
+
while self._curr:
|
|
287
|
+
self._advance()
|
|
288
|
+
text = self._find_sql(start, self._prev)
|
|
289
|
+
return exp.Command(this=text[:len(start.text)], expression=text[len(start.text):])
|
|
290
|
+
|
|
291
|
+
def _parse_try_catch(self) -> TryCatch:
|
|
292
|
+
self._advance(2) # BEGIN TRY
|
|
293
|
+
try_block = self._parse_block()
|
|
294
|
+
self._match_text_seq('TRY')
|
|
295
|
+
|
|
296
|
+
# BEGIN CATCH viene en el siguiente chunk
|
|
297
|
+
if not self._curr and self._chunk_index < len(self._chunks):
|
|
298
|
+
self._advance_chunk()
|
|
299
|
+
if not self._match_text_seq('BEGIN', 'CATCH'):
|
|
300
|
+
self.raise_error('Expected BEGIN CATCH after END TRY')
|
|
301
|
+
catch_block = self._parse_block()
|
|
302
|
+
self._match_text_seq('CATCH')
|
|
303
|
+
|
|
304
|
+
return self.expression(TryCatch(this=try_block, catch=catch_block))
|
|
305
|
+
|
|
306
|
+
# Opciones de PROCEDURE / FUNCTION / VIEW que sqlglot no conoce
|
|
307
|
+
EXTRA_OBJECT_OPTIONS = (("RETURNS", "NULL", "ON", "NULL", "INPUT"), ("CALLED", "ON", "NULL", "INPUT"), ("VIEW_METADATA",))
|
|
308
|
+
|
|
309
|
+
def _parse_with_property(self) -> exp.Expr | None | list[exp.Expr]:
|
|
310
|
+
# sqlglot revisa los atributos de vista (SCHEMABINDING, ENCRYPTION) antes
|
|
311
|
+
# que la lista de opciones: con "WITH SCHEMABINDING, EXECUTE AS ..." se
|
|
312
|
+
# queda solo con la primera y deja la coma.
|
|
313
|
+
is_list = self._next is not None and self._next.token_type == TokenType.COMMA
|
|
314
|
+
if is_list or self._is_word(self._curr, "RETURNS", "CALLED", "VIEW_METADATA"):
|
|
315
|
+
return self.expression(exp.WithProcedureOptions(expressions=self._parse_csv(self._parse_procedure_option)))
|
|
316
|
+
return super()._parse_with_property()
|
|
317
|
+
|
|
318
|
+
def _parse_procedure_option(self) -> exp.Expr | None:
|
|
319
|
+
for words in self.EXTRA_OBJECT_OPTIONS:
|
|
320
|
+
if self._match_text_seq(*words):
|
|
321
|
+
return exp.var(" ".join(words))
|
|
322
|
+
return super()._parse_procedure_option()
|
|
323
|
+
|
|
324
|
+
def _match_compound_assignment(self) -> str | None:
|
|
325
|
+
"""Token "+=", "-=", ... (el tokenizer los une); devuelve el operador."""
|
|
326
|
+
if self._curr and self._curr.token_type == TokenType.EQ and self._curr.text != "=":
|
|
327
|
+
self._advance()
|
|
328
|
+
return self._prev.text[:-1]
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
def _parse_equality(self) -> exp.Expr | None:
|
|
332
|
+
# UPDATE ... SET col += 1 / SELECT @a += 1
|
|
333
|
+
this = self._parse_comparison()
|
|
334
|
+
op = self._match_compound_assignment()
|
|
335
|
+
if op:
|
|
336
|
+
return self.expression(CompoundAssignment(this=this, expression=self._parse_comparison(), op=op))
|
|
337
|
+
|
|
338
|
+
while self._match_set(self.EQUALITY):
|
|
339
|
+
comments = self._prev_comments
|
|
340
|
+
this = self.expression(
|
|
341
|
+
self.EQUALITY[self._prev.token_type](this=this, expression=self._parse_comparison()),
|
|
342
|
+
comments=comments,
|
|
343
|
+
)
|
|
344
|
+
return this
|
|
345
|
+
|
|
346
|
+
def _parse_set_item_assignment(self, kind: str | None = None) -> exp.Expr | None:
|
|
347
|
+
# SET @a += 1
|
|
348
|
+
index = self._index
|
|
349
|
+
left = self._parse_primary() or self._parse_column()
|
|
350
|
+
op = left and self._match_compound_assignment()
|
|
351
|
+
if op:
|
|
352
|
+
value = self.expression(CompoundAssignment(this=left, expression=self._parse_assignment(), op=op))
|
|
353
|
+
return self.expression(exp.SetItem(this=value, kind=kind))
|
|
354
|
+
self._retreat(index)
|
|
355
|
+
return super()._parse_set_item_assignment(kind)
|
|
356
|
+
|
|
357
|
+
def _parse_condition(self) -> exp.Expr | None:
|
|
358
|
+
# sqlglot espera "(expresion)" completa: falla con IF (SELECT ...) > 0
|
|
359
|
+
# y con IF (@a > 0) AND @b = 1. Una expresion normal cubre ambos.
|
|
360
|
+
return self._parse_expression()
|
|
361
|
+
|
|
362
|
+
# Pistas de tabla que T-SQL acepta sin WITH: FROM Venta (NOLOCK)
|
|
363
|
+
TABLE_HINT_WORDS = {
|
|
364
|
+
"NOLOCK", "READUNCOMMITTED", "READCOMMITTED", "READCOMMITTEDLOCK", "REPEATABLEREAD", "SERIALIZABLE",
|
|
365
|
+
"SNAPSHOT", "UPDLOCK", "XLOCK", "TABLOCK", "TABLOCKX", "PAGLOCK", "ROWLOCK", "NOWAIT", "READPAST",
|
|
366
|
+
"HOLDLOCK", "NOEXPAND", "FORCESEEK", "FORCESCAN", "KEEPIDENTITY", "KEEPDEFAULTS",
|
|
367
|
+
"IGNORE_CONSTRAINTS", "IGNORE_TRIGGERS",
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
def _is_hint_list(self, index: int) -> bool:
|
|
371
|
+
"""tokens[index] abre "(hint[, hint...])" solo con pistas de tabla."""
|
|
372
|
+
tokens = self._tokens
|
|
373
|
+
if index >= len(tokens) or tokens[index].token_type != TokenType.L_PAREN:
|
|
374
|
+
return False
|
|
375
|
+
index += 1
|
|
376
|
+
while index + 1 < len(tokens) and tokens[index].text.upper() in self.TABLE_HINT_WORDS:
|
|
377
|
+
if tokens[index + 1].token_type == TokenType.R_PAREN:
|
|
378
|
+
return True
|
|
379
|
+
if tokens[index + 1].token_type != TokenType.COMMA:
|
|
380
|
+
return False
|
|
381
|
+
index += 2
|
|
382
|
+
return False
|
|
383
|
+
|
|
384
|
+
def _parse_table_part(self, schema: bool = False) -> exp.Expr | None:
|
|
385
|
+
# "Venta (NOLOCK)" no es la llamada a una funcion Venta
|
|
386
|
+
if self._curr and self._is_hint_list(self._index + 1):
|
|
387
|
+
return self._parse_id_var(any_token=False)
|
|
388
|
+
return super()._parse_table_part(schema)
|
|
389
|
+
|
|
390
|
+
def _parse_table_alias(self, alias_tokens=None) -> exp.TableAlias | None:
|
|
391
|
+
# "Venta (NOLOCK)" sin alias: no es un alias con lista de columnas
|
|
392
|
+
if self._is_hint_list(self._index):
|
|
393
|
+
return None
|
|
394
|
+
|
|
395
|
+
# "Venta v (NOLOCK)": el parentesis no es la lista de columnas del alias
|
|
396
|
+
offset = 2 if self._curr and self._curr.token_type == TokenType.ALIAS else 1
|
|
397
|
+
if self._curr and self._is_hint_list(self._index + offset):
|
|
398
|
+
self._match(TokenType.ALIAS)
|
|
399
|
+
alias = self._parse_id_var(any_token=False, tokens=alias_tokens or self.TABLE_ALIAS_TOKENS)
|
|
400
|
+
if alias:
|
|
401
|
+
return self.expression(exp.TableAlias(this=alias))
|
|
402
|
+
return super()._parse_table_alias(alias_tokens)
|
|
403
|
+
|
|
404
|
+
def _parse_table_hints(self) -> list[exp.Expr] | None:
|
|
405
|
+
if self._is_hint_list(self._index):
|
|
406
|
+
self._advance() # (
|
|
407
|
+
hint = self.expression(exp.WithTableHint(expressions=self._parse_csv(lambda: self._parse_var(any_token=True))))
|
|
408
|
+
self._match_r_paren()
|
|
409
|
+
return [hint]
|
|
410
|
+
return super()._parse_table_hints()
|
|
411
|
+
|
|
412
|
+
def _parse_alias(self, this: exp.Expr | None, explicit: bool = False) -> exp.Expr | None:
|
|
413
|
+
# "IF @ID IS NULL RETURN" / "IF ... EXEC ...": una palabra que abre la
|
|
414
|
+
# siguiente sentencia no es un alias implicito de la expresion anterior.
|
|
415
|
+
if self._curr and self._is_open_token(self._curr):
|
|
416
|
+
return this
|
|
417
|
+
|
|
418
|
+
return super()._parse_alias(this, explicit)
|
|
419
|
+
|
|
420
|
+
def _parse_top(self) -> Top | None:
|
|
421
|
+
if not self._match(TokenType.TOP):
|
|
422
|
+
return None
|
|
423
|
+
this = self._parse_wrapped(self._parse_expression)
|
|
424
|
+
return self.expression(Top(this=this, percent=self._match(TokenType.PERCENT)))
|
|
425
|
+
|
|
426
|
+
def _parse_delete(self) -> exp.Delete:
|
|
427
|
+
# DELETE TOP (n): sqlglot toma "TOP (n)" como una tabla con alias
|
|
428
|
+
top = self._parse_top()
|
|
429
|
+
delete = super()._parse_delete()
|
|
430
|
+
if top:
|
|
431
|
+
delete.set("limit", top)
|
|
432
|
+
return delete
|
|
433
|
+
|
|
434
|
+
def _parse_update(self) -> exp.Update:
|
|
435
|
+
top = self._parse_top()
|
|
436
|
+
update = super()._parse_update()
|
|
437
|
+
if top:
|
|
438
|
+
update.set("limit", top)
|
|
439
|
+
return update
|
|
440
|
+
|
|
441
|
+
def _parse_insert(self) -> exp.Insert:
|
|
442
|
+
# INSERT ... EXEC: sqlglot no lo soporta; el EXEC es el origen de los datos
|
|
443
|
+
insert = super()._parse_insert()
|
|
444
|
+
if isinstance(insert, exp.Insert) and not insert.expression and self._match(TokenType.EXECUTE):
|
|
445
|
+
insert.set("expression", self._parse_execute())
|
|
446
|
+
return insert
|
|
447
|
+
|
|
448
|
+
def _parse_execute(self) -> exp.Execute:
|
|
449
|
+
# EXEC (<expresion>) es SQL dinamico: un string, N'...', una variable
|
|
450
|
+
# o una concatenacion. sqlglot lo intenta leer como nombre de tabla.
|
|
451
|
+
if self._match(TokenType.L_PAREN, advance=False):
|
|
452
|
+
# EXEC ('sql', @p1, ...) AT servidor: con servidor vinculado puede
|
|
453
|
+
# llevar parametros dentro del parentesis
|
|
454
|
+
expressions = self._parse_wrapped_csv(self._parse_expression)
|
|
455
|
+
this = exp.Paren(this=expressions[0]) if len(expressions) == 1 else exp.Tuple(expressions=expressions)
|
|
456
|
+
if self._match_text_seq('AT'):
|
|
457
|
+
return self.expression(ExecuteAt(this=this, at=self._parse_id_var(any_token=True)))
|
|
458
|
+
return self.expression(exp.Execute(this=this))
|
|
459
|
+
|
|
460
|
+
return super()._parse_execute()
|
|
461
|
+
|
|
462
|
+
def _parse_parameter(self) -> exp.Parameter:
|
|
463
|
+
this = self._parse_identifier() or self._parse_primary_or_var()
|
|
464
|
+
# El tokenizer marca el nombre como IDENTIFIER (ver SqlServerTokenizer)
|
|
465
|
+
if isinstance(this, exp.Identifier):
|
|
466
|
+
this = exp.var(this.name)
|
|
467
|
+
# OUTPUT se tokeniza como RETURNING; OUT es su forma corta
|
|
468
|
+
output = self._match(TokenType.RETURNING) or self._match_text_seq("OUT")
|
|
469
|
+
|
|
470
|
+
return self.expression(ExecParameter(this=this, output=output))
|
|
471
|
+
|
|
472
|
+
def _parse_batch_statements(
|
|
473
|
+
self,
|
|
474
|
+
parse_method: t.Callable[[Parser], exp.Expr | None],
|
|
475
|
+
sep_first_statement: bool = True,
|
|
476
|
+
top_level: bool = False,
|
|
477
|
+
) -> list[exp.Expr | None]:
|
|
478
|
+
# Un bloque IF/WHILE "sin BEGIN" (bodyless) solo se distingue de un
|
|
479
|
+
# cuerpo BEGIN...END real por el token que se consumio justo antes de
|
|
480
|
+
# entrar aqui. Si quien nos llamo (p.ej. _parse_create) ya hizo match
|
|
481
|
+
# de su propio BEGIN antes de invocar _parse_block, self._prev es ese
|
|
482
|
+
# BEGIN, y NO debemos tomar el atajo de una sola sentencia: es un
|
|
483
|
+
# bloque real de varias sentencias, solo que su BEGIN ya fue
|
|
484
|
+
# consumido por el llamador.
|
|
485
|
+
prev_was_begin = self._block_already_opened()
|
|
486
|
+
|
|
487
|
+
if (
|
|
488
|
+
not top_level
|
|
489
|
+
and not prev_was_begin
|
|
490
|
+
and self._is_open_token(self._curr, self._prev, self._next)
|
|
491
|
+
):
|
|
492
|
+
result = [parse_method(self)]
|
|
493
|
+
|
|
494
|
+
# Una sentencia bodyless nunca avanza mas alla de su propio chunk.
|
|
495
|
+
# Miramos el siguiente chunk para decidir que hacer con el antes
|
|
496
|
+
# de devolver el control a quien nos llamo.
|
|
497
|
+
if (
|
|
498
|
+
not self._curr
|
|
499
|
+
and self._chunk_index < len(self._chunks)
|
|
500
|
+
and self._chunks[self._chunk_index]
|
|
501
|
+
):
|
|
502
|
+
next_chunk = self._chunks[self._chunk_index]
|
|
503
|
+
first_token = next_chunk[0]
|
|
504
|
+
|
|
505
|
+
if first_token.token_type == TokenType.ELSE:
|
|
506
|
+
# Pertenece al _parse_if que nos envuelve - lo cargamos
|
|
507
|
+
# para que su propio self._match(TokenType.ELSE) lo vea.
|
|
508
|
+
self._advance_chunk()
|
|
509
|
+
|
|
510
|
+
return result
|
|
511
|
+
|
|
512
|
+
return super()._parse_batch_statements(parse_method, sep_first_statement, top_level=top_level)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class StrUtilMixin:
|
|
2
|
+
@staticmethod
|
|
3
|
+
def _clean_spaces(text):
|
|
4
|
+
text = ' '.join(text.split())
|
|
5
|
+
|
|
6
|
+
for r in ('( ', ' )', '[ ', ' ]'):
|
|
7
|
+
text = text.replace(r, r.strip())
|
|
8
|
+
|
|
9
|
+
return text
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def _clean_lines(text: str):
|
|
13
|
+
return '\n'.join([l for l in text.splitlines() if l])
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def _flat(text: str):
|
|
17
|
+
text = text.replace('\n', ' ')
|
|
18
|
+
return StrUtilMixin._clean_spaces(text)
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def _set_tab(text: str, index: int = 0):
|
|
22
|
+
return '\n'.join([f'{index * '\t'}{l}' for l in text.splitlines()])
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from sqlglot import TokenType
|
|
2
|
+
from sqlglot.tokens import Token
|
|
3
|
+
from sqlglot.dialects.tsql import TSQL
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SqlServerTokenizer(TSQL.Tokenizer):
|
|
7
|
+
# Un token de COMMANDS al inicio del script (o tras ";") se traga el resto
|
|
8
|
+
# como un solo string. El parser maneja FETCH y los COMMAND (UPDATE
|
|
9
|
+
# STATISTICS, ...) dentro de su propia sentencia.
|
|
10
|
+
COMMANDS = TSQL.Tokenizer.COMMANDS - {TokenType.END, TokenType.FETCH, TokenType.COMMAND}
|
|
11
|
+
|
|
12
|
+
# Como COMMAND, GO y PRINT se tragan el resto del script despues de un
|
|
13
|
+
# ";" (o al inicio). Los dejamos como VAR y el parser los reconoce.
|
|
14
|
+
KEYWORDS = {k: v for k, v in TSQL.Tokenizer.KEYWORDS.items() if k not in ("GO", "PRINT")}
|
|
15
|
+
|
|
16
|
+
# Operador pegado a "=": asignacion compuesta (SET @i += 1)
|
|
17
|
+
COMPOUND_OPERATORS = {
|
|
18
|
+
TokenType.PLUS, TokenType.DASH, TokenType.STAR, TokenType.SLASH,
|
|
19
|
+
TokenType.MOD, TokenType.AMP, TokenType.PIPE, TokenType.CARET,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
def tokenize(self, sql: str) -> list[Token]:
|
|
23
|
+
# Une "+" "=" en un solo token EQ con texto "+=" (lo mismo para -= *= ...)
|
|
24
|
+
tokens = []
|
|
25
|
+
for token in super().tokenize(sql):
|
|
26
|
+
prev = tokens[-1] if tokens else None
|
|
27
|
+
if (
|
|
28
|
+
token.token_type == TokenType.EQ
|
|
29
|
+
and prev is not None
|
|
30
|
+
and prev.token_type in self.COMPOUND_OPERATORS
|
|
31
|
+
and prev.end + 1 == token.start
|
|
32
|
+
):
|
|
33
|
+
tokens[-1] = Token(
|
|
34
|
+
TokenType.EQ, prev.text + token.text, prev.line, prev.col, prev.start, token.end, prev.comments,
|
|
35
|
+
)
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
# Nombre de variable (@End, @If, @Begin...): IDENTIFIER para que no
|
|
39
|
+
# se confunda con la palabra clave; el parser lo convierte en Var.
|
|
40
|
+
if (
|
|
41
|
+
prev is not None
|
|
42
|
+
and prev.token_type == TokenType.PARAMETER
|
|
43
|
+
and prev.end + 1 == token.start
|
|
44
|
+
and token.token_type != TokenType.PARAMETER
|
|
45
|
+
):
|
|
46
|
+
token = Token(
|
|
47
|
+
TokenType.IDENTIFIER, token.text, token.line, token.col, token.start, token.end, token.comments,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
tokens.append(token)
|
|
51
|
+
return tokens
|