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.
@@ -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
@@ -0,0 +1,3 @@
1
+ from get_objects_lib.objects.dependencies import ObjectRef, get_dependencies
2
+ from get_objects_lib.objects.header import ObjectHeader, find_object_header
3
+ from get_objects_lib.objects.standardize import standardize_object