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,12 @@
|
|
|
1
|
+
"""Parser y generador de T-SQL sobre sqlglot, y analisis de objetos de SQL Server."""
|
|
2
|
+
from get_objects_lib.dialect import SqlServer
|
|
3
|
+
from get_objects_lib.objects import ObjectHeader, ObjectRef, find_object_header, get_dependencies, standardize_object
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
'SqlServer',
|
|
7
|
+
'ObjectHeader',
|
|
8
|
+
'ObjectRef',
|
|
9
|
+
'find_object_header',
|
|
10
|
+
'get_dependencies',
|
|
11
|
+
'standardize_object',
|
|
12
|
+
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from sqlglot.dialects.tsql import TSQL
|
|
2
|
+
|
|
3
|
+
from get_objects_lib.dialect.parser.sql_server import SqlServerParser
|
|
4
|
+
from get_objects_lib.dialect.tokenizer import SqlServerTokenizer
|
|
5
|
+
from get_objects_lib.dialect.generator import SqlServerGenerator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SqlServer(TSQL):
|
|
9
|
+
Tokenizer = SqlServerTokenizer
|
|
10
|
+
Parser = SqlServerParser
|
|
11
|
+
Generator = SqlServerGenerator
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from sqlglot import exp
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ExecParameter(exp.Parameter):
|
|
5
|
+
arg_types = {"this": True, "expression": False, "output": False}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ReturnStatement(exp.Return):
|
|
9
|
+
"""RETURN como sentencia; el valor es opcional (RETURN a secas)."""
|
|
10
|
+
arg_types = {"this": False}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BatchSeparator(exp.Expression):
|
|
14
|
+
"""GO [n]: separa lotes; this es el numero de repeticiones."""
|
|
15
|
+
arg_types = {"this": False}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LoopControl(exp.Expression):
|
|
19
|
+
"""BREAK / CONTINUE dentro de un WHILE; this es la palabra clave."""
|
|
20
|
+
arg_types = {"this": True}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Print(exp.Expression):
|
|
24
|
+
arg_types = {"this": True}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RaiseError(exp.Expression):
|
|
28
|
+
"""RAISERROR (mensaje, severidad, estado [, args]) [WITH opciones]"""
|
|
29
|
+
arg_types = {"expressions": True, "options": False}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Throw(exp.Expression):
|
|
33
|
+
"""THROW [numero, mensaje, estado]; sin argumentos relanza el error."""
|
|
34
|
+
arg_types = {"expressions": False}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SaveTransaction(exp.Expression):
|
|
38
|
+
"""SAVE TRAN[SACTION] nombre"""
|
|
39
|
+
arg_types = {"this": True}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BeginEnd(exp.Expression):
|
|
43
|
+
"""Bloque BEGIN ... END suelto (sin IF/WHILE/procedimiento que lo contenga)."""
|
|
44
|
+
arg_types = {"this": True}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TryCatch(exp.Expression):
|
|
48
|
+
"""BEGIN TRY ... END TRY BEGIN CATCH ... END CATCH"""
|
|
49
|
+
arg_types = {"this": True, "catch": True}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DeclareCursor(exp.Expression):
|
|
53
|
+
"""DECLARE nombre CURSOR [opciones] FOR select [FOR UPDATE [OF columnas] | FOR READ ONLY]"""
|
|
54
|
+
arg_types = {"this": True, "options": False, "expression": True, "for_update": False, "columns": False, "read_only": False}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CursorStatement(exp.Expression):
|
|
58
|
+
"""OPEN / CLOSE / DEALLOCATE [GLOBAL] cursor"""
|
|
59
|
+
arg_types = {"kind": True, "this": True, "global_": False}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class FetchCursor(exp.Expression):
|
|
63
|
+
"""FETCH [NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n] [FROM] cursor [INTO @variables]"""
|
|
64
|
+
arg_types = {"this": True, "direction": False, "count": False, "global_": False, "into": False}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Top(exp.Expression):
|
|
68
|
+
"""TOP (n) [PERCENT] de un DELETE / UPDATE; se guarda en su arg "limit"."""
|
|
69
|
+
arg_types = {"this": True, "percent": False}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Label(exp.Expression):
|
|
73
|
+
"""Etiqueta de GOTO: "Nombre:"."""
|
|
74
|
+
arg_types = {"this": True}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Goto(exp.Expression):
|
|
78
|
+
arg_types = {"this": True}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class WaitFor(exp.Expression):
|
|
82
|
+
"""WAITFOR DELAY | TIME expresion"""
|
|
83
|
+
arg_types = {"kind": True, "this": True}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class CompoundAssignment(exp.Expression):
|
|
87
|
+
"""@a += 1, col -= 2, ...; op es el operador sin el "=" (+, -, *, /, %, &, |, ^)."""
|
|
88
|
+
arg_types = {"this": True, "expression": True, "op": True}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TriggerSpec(exp.Expression):
|
|
92
|
+
"""
|
|
93
|
+
Encabezado de un trigger de T-SQL: ON tabla [WITH opciones]
|
|
94
|
+
{FOR | AFTER | INSTEAD OF} eventos [NOT FOR REPLICATION]
|
|
95
|
+
"""
|
|
96
|
+
arg_types = {"table": True, "options": False, "timing": True, "events": True, "not_for_replication": False}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ExecuteAt(exp.Execute):
|
|
100
|
+
"""EXEC (sql [, parametros]) AT servidor_vinculado"""
|
|
101
|
+
arg_types = {**exp.Execute.arg_types, "at": True}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class InlineIndex(exp.Expression):
|
|
105
|
+
"""
|
|
106
|
+
Indice en la definicion de una tabla: INDEX nombre [UNIQUE]
|
|
107
|
+
[CLUSTERED | NONCLUSTERED] (columnas) [INCLUDE (...)] [WHERE ...] [WITH (...)]
|
|
108
|
+
"""
|
|
109
|
+
arg_types = {
|
|
110
|
+
"this": True, "unique": False, "clustered": False, "expressions": True,
|
|
111
|
+
"include": False, "where": False, "options": False,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class AlterObject(exp.Create):
|
|
116
|
+
"""ALTER PROCEDURE/FUNCTION/VIEW/TRIGGER: misma estructura que su CREATE."""
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
from sqlglot import exp
|
|
2
|
+
from sqlglot.dialects.tsql import TSQL
|
|
3
|
+
|
|
4
|
+
from get_objects_lib.dialect.expressions import (
|
|
5
|
+
AlterObject, BatchSeparator, BeginEnd, CursorStatement, DeclareCursor, ExecParameter, FetchCursor, Goto, Label, Top, TryCatch, WaitFor, CompoundAssignment, TriggerSpec, ExecuteAt, InlineIndex, LoopControl, Print, RaiseError, ReturnStatement,
|
|
6
|
+
SaveTransaction, Throw,
|
|
7
|
+
)
|
|
8
|
+
from get_objects_lib.dialect.text_utils import StrUtilMixin
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SqlServerGenerator(TSQL.Generator, StrUtilMixin):
|
|
12
|
+
# T-SQL se escribe INT; sqlglot lo genera como INTEGER
|
|
13
|
+
TYPE_MAPPING = {**TSQL.Generator.TYPE_MAPPING, exp.DataType.Type.INT: "INT"}
|
|
14
|
+
|
|
15
|
+
TRANSFORMS = {
|
|
16
|
+
**TSQL.Generator.TRANSFORMS,
|
|
17
|
+
ExecParameter: lambda self, e: (
|
|
18
|
+
f'@{self.sql(e, "this")} {"OUTPUT" if e.args.get("output") else ""}'.strip()
|
|
19
|
+
),
|
|
20
|
+
AlterObject: lambda self, e: self.alterobject_sql(e),
|
|
21
|
+
ReturnStatement: lambda self, e: f'RETURN {self.sql(e, "this")}'.strip(),
|
|
22
|
+
BatchSeparator: lambda self, e: f'GO {self.sql(e, "this")}'.strip(),
|
|
23
|
+
LoopControl: lambda self, e: e.this,
|
|
24
|
+
Print: lambda self, e: f'PRINT {self.sql(e, "this")}',
|
|
25
|
+
RaiseError: lambda self, e: self.raiseerror_sql(e),
|
|
26
|
+
InlineIndex: lambda self, e: self.inlineindex_sql(e),
|
|
27
|
+
ExecuteAt: lambda self, e: f'{self.execute_sql(e)} AT {self.sql(e, "at")}',
|
|
28
|
+
BeginEnd: lambda self, e: self.beginend_sql(e),
|
|
29
|
+
TryCatch: lambda self, e: self.trycatch_sql(e),
|
|
30
|
+
Label: lambda self, e: f'{self.sql(e, "this")}:',
|
|
31
|
+
CompoundAssignment: lambda self, e: f'{self.sql(e, "this")} {e.args["op"]}= {self.sql(e, "expression")}',
|
|
32
|
+
Goto: lambda self, e: f'GOTO {self.sql(e, "this")}',
|
|
33
|
+
WaitFor: lambda self, e: f'WAITFOR {e.args["kind"]} {self.sql(e, "this")}',
|
|
34
|
+
DeclareCursor: lambda self, e: self.declarecursor_sql(e),
|
|
35
|
+
CursorStatement: lambda self, e: self.cursorstatement_sql(e),
|
|
36
|
+
FetchCursor: lambda self, e: self.fetchcursor_sql(e),
|
|
37
|
+
SaveTransaction: lambda self, e: f'SAVE TRANSACTION {self.sql(e, "this")}',
|
|
38
|
+
Throw: lambda self, e: f'THROW {self.expressions(e, flat=True)}'.strip(),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def _move_ctes_to_top_level(self, expression):
|
|
42
|
+
# sqlglot sube los CTE anidados de un CREATE al nivel superior (para
|
|
43
|
+
# CREATE TABLE ... AS SELECT). En el cuerpo BEGIN ... END de un
|
|
44
|
+
# procedimiento o funcion cada sentencia tiene su propio CTE.
|
|
45
|
+
if isinstance(expression, exp.Create) and isinstance(expression.expression, exp.Block):
|
|
46
|
+
return expression
|
|
47
|
+
return super()._move_ctes_to_top_level(expression)
|
|
48
|
+
|
|
49
|
+
def inlineindex_sql(self, expression: InlineIndex) -> str:
|
|
50
|
+
unique = " UNIQUE" if expression.args.get("unique") else ""
|
|
51
|
+
clustered = f" {expression.args['clustered']}" if expression.args.get("clustered") else ""
|
|
52
|
+
include = self.expressions(expression, key="include", flat=True)
|
|
53
|
+
include = f" INCLUDE ({include})" if include else ""
|
|
54
|
+
where = self.sql(expression, "where").strip()
|
|
55
|
+
where = f" {where}" if where else ""
|
|
56
|
+
options = self.expressions(expression, key="options", flat=True)
|
|
57
|
+
options = f" WITH ({options})" if options else ""
|
|
58
|
+
columns = self.expressions(expression, flat=True)
|
|
59
|
+
return f"INDEX {self.sql(expression, 'this')}{unique}{clustered} ({columns}){include}{where}{options}"
|
|
60
|
+
|
|
61
|
+
def insert_sql(self, expression: exp.Insert) -> str:
|
|
62
|
+
# sqlglot solo escribe DEFAULT VALUES cuando RETURNING va al final; en
|
|
63
|
+
# T-SQL el OUTPUT va antes y DEFAULT VALUES se perdia
|
|
64
|
+
sql = super().insert_sql(expression)
|
|
65
|
+
if expression.args.get("default") and not sql.rstrip().endswith("DEFAULT VALUES"):
|
|
66
|
+
sql = f"{sql.rstrip()} DEFAULT VALUES"
|
|
67
|
+
return sql
|
|
68
|
+
|
|
69
|
+
def anonymous_sql(self, expression: exp.Anonymous) -> str:
|
|
70
|
+
# sqlglot pasa a mayusculas las funciones que no conoce salvo las
|
|
71
|
+
# calificadas con Dot; dbo.fn(...) en un FROM (Table) o fn(...) en un
|
|
72
|
+
# APPLY tambien son funciones de usuario: se dejan como se escribieron.
|
|
73
|
+
return self.func(self.sql(expression, "this"), *expression.expressions, normalize=False)
|
|
74
|
+
|
|
75
|
+
def returnsproperty_sql(self, expression: exp.ReturnsProperty) -> str:
|
|
76
|
+
# TSQL usa str(table), que genera con el dialecto por defecto y no
|
|
77
|
+
# conoce ExecParameter (RETURNS @t TABLE (...)).
|
|
78
|
+
table = self.sql(expression, "table")
|
|
79
|
+
table = f"{table} " if table else ""
|
|
80
|
+
return f"RETURNS {table}{self.sql(expression, 'this')}"
|
|
81
|
+
|
|
82
|
+
def create_sql(self, expression: exp.Create) -> str:
|
|
83
|
+
if expression.kind == "TRIGGER":
|
|
84
|
+
return self._trigger_sql(expression)
|
|
85
|
+
return super().create_sql(expression)
|
|
86
|
+
|
|
87
|
+
def _trigger_sql(self, expression: exp.Create) -> str:
|
|
88
|
+
spec = expression.find(TriggerSpec)
|
|
89
|
+
replace = " OR ALTER" if expression.args.get("replace") else ""
|
|
90
|
+
options = self.expressions(spec, key="options", flat=True)
|
|
91
|
+
options = f" WITH {options}" if options else ""
|
|
92
|
+
events = self.expressions(spec, key="events", flat=True)
|
|
93
|
+
not_for_replication = " NOT FOR REPLICATION" if spec.args.get("not_for_replication") else ""
|
|
94
|
+
begin = " BEGIN" if expression.args.get("begin") else ""
|
|
95
|
+
return (
|
|
96
|
+
f"CREATE{replace} TRIGGER {self.sql(expression, 'this')} ON {self.sql(spec, 'table')}{options}"
|
|
97
|
+
f" {spec.args['timing']} {events}{not_for_replication} AS{begin}{self.sep()}{self.sql(expression, 'expression')}"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def alterobject_sql(self, expression: AlterObject) -> str:
|
|
101
|
+
sql = self.create_sql(expression)
|
|
102
|
+
return f"ALTER{sql[len('CREATE'):]}"
|
|
103
|
+
|
|
104
|
+
def select_sql(self, expression: exp.Select) -> str:
|
|
105
|
+
sql = super().select_sql(expression)
|
|
106
|
+
if len(sql) < 100: sql = self._flat(sql)
|
|
107
|
+
return sql
|
|
108
|
+
|
|
109
|
+
def exists_sql(self, expression: exp.Exists) -> str:
|
|
110
|
+
sql = super().exists_sql(expression)
|
|
111
|
+
if len(sql) < 100: sql = self._flat(sql)
|
|
112
|
+
return sql
|
|
113
|
+
|
|
114
|
+
def block_sql(self, expression: exp.Block) -> str:
|
|
115
|
+
lines = [self.sql(e) for e in expression.expressions]
|
|
116
|
+
return "\n".join(line for line in lines if line)
|
|
117
|
+
|
|
118
|
+
def _block_body_and_end(self, block: exp.Block | None) -> tuple[str, str]:
|
|
119
|
+
if not block: return "", ""
|
|
120
|
+
|
|
121
|
+
expressions = list(block.expressions)
|
|
122
|
+
end = ""
|
|
123
|
+
if expressions and isinstance(expressions[-1], exp.EndStatement):
|
|
124
|
+
end = self.sql(expressions.pop())
|
|
125
|
+
|
|
126
|
+
lines = [self.sql(e) for e in expressions]
|
|
127
|
+
body = "\n".join(line for line in lines if line)
|
|
128
|
+
return body, end
|
|
129
|
+
|
|
130
|
+
def ifblock_sql(self, expression: exp.IfBlock) -> str:
|
|
131
|
+
this = self.sql(expression, "this")
|
|
132
|
+
|
|
133
|
+
true_expressions = getattr(expression.args.get('true'), 'expressions', [])
|
|
134
|
+
true_body, true_end = self._block_body_and_end(expression.args.get('true'))
|
|
135
|
+
|
|
136
|
+
true_sql = self.indent(true_body, level=1)
|
|
137
|
+
if len(true_expressions) != 1:
|
|
138
|
+
true_sql = f"BEGIN\n{self.indent(true_body, level=1)}"
|
|
139
|
+
if true_end:
|
|
140
|
+
true_sql = f"{true_sql}\n{true_end}"
|
|
141
|
+
|
|
142
|
+
false_expressions = getattr(expression.args.get('false'), 'expressions', [])
|
|
143
|
+
false_body, false_end = self._block_body_and_end(expression.args.get('false'))
|
|
144
|
+
|
|
145
|
+
if false_body or false_end:
|
|
146
|
+
if len(false_expressions) == 1:
|
|
147
|
+
false_sql = f"\nELSE\n{self.indent(false_body, level=1)}"
|
|
148
|
+
else:
|
|
149
|
+
false_sql = f"\nELSE\nBEGIN\n{self.indent(false_body, level=1)}"
|
|
150
|
+
if false_end:
|
|
151
|
+
false_sql = f"{false_sql}\n{false_end}"
|
|
152
|
+
else:
|
|
153
|
+
false_sql = ""
|
|
154
|
+
|
|
155
|
+
return f"IF {this}\n{true_sql}{false_sql}"
|
|
156
|
+
|
|
157
|
+
def declarecursor_sql(self, expression: DeclareCursor) -> str:
|
|
158
|
+
options = self.expressions(expression, key="options", sep=" ")
|
|
159
|
+
options = f" {options}" if options else ""
|
|
160
|
+
sql = f"DECLARE {self.sql(expression, 'this')} CURSOR{options} FOR {self.sql(expression, 'expression')}"
|
|
161
|
+
if expression.args.get("read_only"):
|
|
162
|
+
return f"{sql} FOR READ ONLY"
|
|
163
|
+
if expression.args.get("for_update"):
|
|
164
|
+
columns = self.expressions(expression, key="columns", flat=True)
|
|
165
|
+
return f"{sql} FOR UPDATE OF {columns}" if columns else f"{sql} FOR UPDATE"
|
|
166
|
+
return sql
|
|
167
|
+
|
|
168
|
+
def cursorstatement_sql(self, expression: CursorStatement) -> str:
|
|
169
|
+
global_ = "GLOBAL " if expression.args.get("global_") else ""
|
|
170
|
+
return f"{expression.args['kind']} {global_}{self.sql(expression, 'this')}"
|
|
171
|
+
|
|
172
|
+
def fetchcursor_sql(self, expression: FetchCursor) -> str:
|
|
173
|
+
direction = expression.args.get("direction")
|
|
174
|
+
count = self.sql(expression, "count")
|
|
175
|
+
direction = f"{direction} {count} " if count else f"{direction} " if direction else ""
|
|
176
|
+
global_ = "GLOBAL " if expression.args.get("global_") else ""
|
|
177
|
+
into = self.expressions(expression, key="into", flat=True)
|
|
178
|
+
into = f" INTO {into}" if into else ""
|
|
179
|
+
return f"FETCH {direction}FROM {global_}{self.sql(expression, 'this')}{into}"
|
|
180
|
+
|
|
181
|
+
def _with_top(self, expression: exp.Delete | exp.Update, keyword: str, generate) -> str:
|
|
182
|
+
# T-SQL: DELETE TOP (n) ... / UPDATE TOP (n) ..., no LIMIT al final
|
|
183
|
+
top = expression.args.get("limit")
|
|
184
|
+
if not isinstance(top, Top):
|
|
185
|
+
return generate(expression)
|
|
186
|
+
expression = expression.copy()
|
|
187
|
+
expression.set("limit", None)
|
|
188
|
+
percent = " PERCENT" if top.args.get("percent") else ""
|
|
189
|
+
sql = generate(expression)
|
|
190
|
+
return f"{keyword} TOP ({self.sql(top, 'this')}){percent}{sql[len(keyword):]}"
|
|
191
|
+
|
|
192
|
+
def delete_sql(self, expression: exp.Delete) -> str:
|
|
193
|
+
return self._with_top(expression, "DELETE", super().delete_sql)
|
|
194
|
+
|
|
195
|
+
def update_sql(self, expression: exp.Update) -> str:
|
|
196
|
+
return self._with_top(expression, "UPDATE", super().update_sql)
|
|
197
|
+
|
|
198
|
+
def trycatch_sql(self, expression: TryCatch) -> str:
|
|
199
|
+
try_body, _ = self._block_body_and_end(expression.this)
|
|
200
|
+
catch_body, _ = self._block_body_and_end(expression.args.get("catch"))
|
|
201
|
+
return (
|
|
202
|
+
f"BEGIN TRY\n{self.indent(try_body, level=1)}\nEND TRY\n"
|
|
203
|
+
f"BEGIN CATCH\n{self.indent(catch_body, level=1)}\nEND CATCH"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def beginend_sql(self, expression: BeginEnd) -> str:
|
|
207
|
+
body, end = self._block_body_and_end(expression.this)
|
|
208
|
+
return f"BEGIN\n{self.indent(body, level=1)}\n{end}"
|
|
209
|
+
|
|
210
|
+
def raiseerror_sql(self, expression: RaiseError) -> str:
|
|
211
|
+
options = self.expressions(expression, key="options", flat=True)
|
|
212
|
+
options = f" WITH {options}" if options else ""
|
|
213
|
+
return f"RAISERROR({self.expressions(expression, flat=True)}){options}"
|
|
214
|
+
|
|
215
|
+
def whileblock_sql(self, expression: exp.WhileBlock) -> str:
|
|
216
|
+
# TSQL siempre escribe "WHILE c BEGIN cuerpo" sin END; igual que en
|
|
217
|
+
# ifblock_sql, BEGIN ... END solo si el cuerpo original lo tenia.
|
|
218
|
+
this = self.sql(expression, "this")
|
|
219
|
+
block = expression.args.get("body")
|
|
220
|
+
body, end = self._block_body_and_end(block)
|
|
221
|
+
if len(getattr(block, "expressions", [])) == 1:
|
|
222
|
+
return f"WHILE {this}\n{self.indent(body, level=1)}"
|
|
223
|
+
|
|
224
|
+
sql = f"WHILE {this}\nBEGIN\n{self.indent(body, level=1)}"
|
|
225
|
+
return f"{sql}\n{end}" if end else sql
|
|
226
|
+
|
|
227
|
+
def generate(self, expression: exp.Expr, copy: bool = True) -> str:
|
|
228
|
+
# Sin reemplazar ';' por saltos de linea: romperia los literales que
|
|
229
|
+
# lo contienen (EXEC (N'CREATE ... AS RETURN 0;')).
|
|
230
|
+
sql = super().generate(expression, copy)
|
|
231
|
+
return self._clean_lines(sql)
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
from typing import List, Callable
|
|
2
|
+
|
|
3
|
+
from sqlglot import exp, Parser
|
|
4
|
+
from sqlglot.dialects.tsql import TSQL
|
|
5
|
+
from sqlglot.tokens import Token, TokenType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseParser(TSQL.Parser):
|
|
9
|
+
OPEN_TOKENS = [
|
|
10
|
+
TokenType.SET,
|
|
11
|
+
TokenType.ELSE,
|
|
12
|
+
TokenType.DECLARE,
|
|
13
|
+
TokenType.EXECUTE,
|
|
14
|
+
TokenType.SELECT,
|
|
15
|
+
TokenType.UPDATE,
|
|
16
|
+
TokenType.DELETE,
|
|
17
|
+
TokenType.INSERT,
|
|
18
|
+
TokenType.CREATE,
|
|
19
|
+
TokenType.COMMIT,
|
|
20
|
+
TokenType.ROLLBACK,
|
|
21
|
+
TokenType.TRUNCATE,
|
|
22
|
+
TokenType.DROP,
|
|
23
|
+
TokenType.MERGE,
|
|
24
|
+
TokenType.USE,
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
OPEN_KEYWORDS = [
|
|
28
|
+
'RETURN',
|
|
29
|
+
'OPEN',
|
|
30
|
+
'IF',
|
|
31
|
+
'WHILE',
|
|
32
|
+
'BREAK',
|
|
33
|
+
'CONTINUE',
|
|
34
|
+
'PRINT',
|
|
35
|
+
'RAISERROR',
|
|
36
|
+
'THROW',
|
|
37
|
+
'SAVE',
|
|
38
|
+
'CLOSE',
|
|
39
|
+
'DEALLOCATE',
|
|
40
|
+
'GOTO',
|
|
41
|
+
'WAITFOR',
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
# Sentencias que sqlglot no modela; se parsean como Command. Son palabras
|
|
45
|
+
# reservadas: no pueden ser nombres sin corchetes.
|
|
46
|
+
GENERIC_COMMANDS = ('DBCC', 'BULK', 'REVERT', 'CHECKPOINT', 'KILL', 'RECONFIGURE', 'SHUTDOWN', 'DENY')
|
|
47
|
+
|
|
48
|
+
# DROP TABLE IF EXISTS: ese IF es parte del DROP, no una sentencia
|
|
49
|
+
OBJECT_KIND_TOKENS = (
|
|
50
|
+
TokenType.TABLE, TokenType.PROCEDURE, TokenType.VIEW, TokenType.FUNCTION, TokenType.INDEX,
|
|
51
|
+
TokenType.TRIGGER, TokenType.SCHEMA, TokenType.SEQUENCE, TokenType.DATABASE,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# BEGIN TRAN / BEGIN DISTRIBUTED TRANSACTION es una sentencia, no un bloque
|
|
55
|
+
TRANSACTION_WORDS = ('TRAN', 'TRANSACTION', 'DISTRIBUTED')
|
|
56
|
+
|
|
57
|
+
# ALTER solo abre sentencia si le sigue uno de estos objetos; asi no se
|
|
58
|
+
# parten "CREATE OR ALTER" ni "ALTER TABLE t ALTER COLUMN c".
|
|
59
|
+
ALTER_OBJECTS = [
|
|
60
|
+
TokenType.PROCEDURE,
|
|
61
|
+
TokenType.FUNCTION,
|
|
62
|
+
TokenType.VIEW,
|
|
63
|
+
TokenType.TABLE,
|
|
64
|
+
TokenType.TRIGGER,
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def _is_word(token: Token | None, *words: str) -> bool:
|
|
69
|
+
"""Palabra clave sin comillas: 'end' o [End] no cuentan como END."""
|
|
70
|
+
return (
|
|
71
|
+
token is not None
|
|
72
|
+
and token.token_type not in (TokenType.STRING, TokenType.NATIONAL_STRING, TokenType.IDENTIFIER)
|
|
73
|
+
and token.text.upper() in words
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def _is_begin(token: Token, _next: Token | None = None) -> bool:
|
|
78
|
+
if not BaseParser._is_word(token, 'BEGIN'):
|
|
79
|
+
return False
|
|
80
|
+
return not BaseParser._is_begin_transaction(token, _next)
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def _is_begin_transaction(token: Token, _next: Token | None) -> bool:
|
|
84
|
+
return BaseParser._is_word(token, 'BEGIN') and BaseParser._is_word(_next, *BaseParser.TRANSACTION_WORDS)
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def _is_end(token: Token) -> bool:
|
|
88
|
+
return BaseParser._is_word(token, 'END')
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def _batch_separator_length(tokens: List[Token], index: int) -> int:
|
|
92
|
+
"""
|
|
93
|
+
Tokens que ocupa un GO en tokens[index] (GO o GO n), o 0 si no lo es.
|
|
94
|
+
GO solo separa lotes cuando va solo en su linea.
|
|
95
|
+
"""
|
|
96
|
+
token = tokens[index]
|
|
97
|
+
if token.token_type != TokenType.VAR or token.text.upper() != 'GO':
|
|
98
|
+
return 0
|
|
99
|
+
if index > 0 and tokens[index - 1].line == token.line:
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
length = 1
|
|
103
|
+
_next = tokens[index + 1] if index + 1 < len(tokens) else None
|
|
104
|
+
if _next and _next.line == token.line and _next.token_type == TokenType.NUMBER:
|
|
105
|
+
length = 2
|
|
106
|
+
after = tokens[index + length] if index + length < len(tokens) else None
|
|
107
|
+
if after and after.line == token.line:
|
|
108
|
+
return 0
|
|
109
|
+
return length
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def _is_trigger_event(token: Token, _prev: Token | None) -> bool:
|
|
113
|
+
return (
|
|
114
|
+
token.token_type in (TokenType.INSERT, TokenType.UPDATE, TokenType.DELETE)
|
|
115
|
+
and _prev is not None
|
|
116
|
+
and (BaseParser._is_word(_prev, 'AFTER', 'OF') or _prev.token_type == TokenType.COMMA)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def _is_generic_command(token: Token, _next: Token | None) -> bool:
|
|
121
|
+
if token.token_type != TokenType.VAR:
|
|
122
|
+
return False
|
|
123
|
+
if token.text.upper() in BaseParser.GENERIC_COMMANDS:
|
|
124
|
+
return True
|
|
125
|
+
# ENABLE / DISABLE no son reservadas: solo con TRIGGER
|
|
126
|
+
return token.text.upper() in ('ENABLE', 'DISABLE') and _next is not None and _next.token_type == TokenType.TRIGGER
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def _is_label(token: Token, _prev: Token | None, _next: Token | None) -> bool:
|
|
130
|
+
"""Etiqueta de GOTO: "Nombre:" al inicio de su linea."""
|
|
131
|
+
return (
|
|
132
|
+
token.token_type == TokenType.VAR
|
|
133
|
+
and _next is not None and _next.token_type == TokenType.COLON and _next.line == token.line
|
|
134
|
+
and (_prev is None or _prev.line != token.line)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _starts_cte(tokens: List[Token], index: int) -> bool:
|
|
139
|
+
"""
|
|
140
|
+
WITH nombre AS ( ... o WITH nombre (columnas) AS ( ...: inicio de un CTE.
|
|
141
|
+
Descarta WITH (NOLOCK), WITH NOWAIT, WITH RECOMPILE AS, etc.
|
|
142
|
+
"""
|
|
143
|
+
def at(i):
|
|
144
|
+
return tokens[i] if i < len(tokens) else None
|
|
145
|
+
|
|
146
|
+
if tokens[index].token_type != TokenType.WITH:
|
|
147
|
+
return False
|
|
148
|
+
i = index + 1
|
|
149
|
+
name = at(i)
|
|
150
|
+
if name is None or name.token_type in (TokenType.L_PAREN, TokenType.STRING):
|
|
151
|
+
return False
|
|
152
|
+
i += 1
|
|
153
|
+
if at(i) is not None and at(i).token_type == TokenType.L_PAREN:
|
|
154
|
+
# lista de columnas
|
|
155
|
+
depth = 0
|
|
156
|
+
while at(i) is not None:
|
|
157
|
+
depth += {TokenType.L_PAREN: 1, TokenType.R_PAREN: -1}.get(at(i).token_type, 0)
|
|
158
|
+
i += 1
|
|
159
|
+
if depth == 0:
|
|
160
|
+
break
|
|
161
|
+
return (
|
|
162
|
+
at(i) is not None and at(i).token_type == TokenType.ALIAS
|
|
163
|
+
and at(i + 1) is not None and at(i + 1).token_type == TokenType.L_PAREN
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def _is_open_token(token: Token, _prev: Token | None = None, _next: Token | None = None) -> bool:
|
|
168
|
+
if (
|
|
169
|
+
token.token_type == TokenType.EXECUTE
|
|
170
|
+
and _prev is not None and _prev.token_type in (TokenType.WITH, TokenType.COMMA)
|
|
171
|
+
and _next is not None and _next.token_type == TokenType.ALIAS
|
|
172
|
+
):
|
|
173
|
+
return False # WITH [RECOMPILE,] EXECUTE AS ... de un procedimiento o funcion
|
|
174
|
+
if token.token_type == TokenType.INSERT and BaseParser._is_word(_prev, 'BULK'):
|
|
175
|
+
return False # BULK INSERT
|
|
176
|
+
if BaseParser._is_trigger_event(token, _prev):
|
|
177
|
+
return False # AFTER INSERT, UPDATE, DELETE de un trigger
|
|
178
|
+
if token.token_type == TokenType.UPDATE and _next is not None and _next.token_type == TokenType.L_PAREN:
|
|
179
|
+
return False # funcion UPDATE(columna) de un trigger
|
|
180
|
+
if token.token_type in BaseParser.OPEN_TOKENS or token.token_type == TokenType.COMMAND:
|
|
181
|
+
return True
|
|
182
|
+
if BaseParser._is_generic_command(token, _next):
|
|
183
|
+
return True
|
|
184
|
+
if token.token_type == TokenType.VAR and token.text.upper() == 'THROW':
|
|
185
|
+
# THROW no es reservada (puede ser una columna): solo abre sentencia
|
|
186
|
+
# sin argumentos o con THROW numero/@variable, ...
|
|
187
|
+
return _next is None or _next.line != token.line or _next.token_type in (TokenType.NUMBER, TokenType.PARAMETER)
|
|
188
|
+
if token.token_type == TokenType.VAR and token.text.upper() in BaseParser.OPEN_KEYWORDS:
|
|
189
|
+
return not (token.text.upper() == 'IF' and _prev is not None and _prev.token_type in BaseParser.OBJECT_KIND_TOKENS)
|
|
190
|
+
if BaseParser._is_begin_transaction(token, _next):
|
|
191
|
+
return True
|
|
192
|
+
if BaseParser._is_word(token, 'BEGIN') and BaseParser._is_word(_next, 'TRY'):
|
|
193
|
+
return True # BEGIN TRY: sentencia TRY/CATCH (p.ej. cuerpo de un IF sin BEGIN)
|
|
194
|
+
if BaseParser._is_label(token, _prev, _next):
|
|
195
|
+
return True
|
|
196
|
+
if token.token_type == TokenType.FETCH:
|
|
197
|
+
# FETCH de cursor, no el de ORDER BY ... OFFSET n ROWS FETCH NEXT
|
|
198
|
+
return not BaseParser._is_word(_prev, 'ROWS', 'ROW')
|
|
199
|
+
if token.token_type == TokenType.ALTER:
|
|
200
|
+
after_or = _prev is not None and _prev.token_type == TokenType.OR
|
|
201
|
+
return not after_or and _next is not None and _next.token_type in BaseParser.ALTER_OBJECTS
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
def _is_block_without_begin(self, raw_tokens: List[Token], index: int) -> bool:
|
|
205
|
+
tokens_to_check = []
|
|
206
|
+
|
|
207
|
+
# Profundidad, no booleano: en "(a IN (1, 2) UNION SELECT ...)" el
|
|
208
|
+
# primer ")" no cierra el parentesis exterior.
|
|
209
|
+
depth = 0
|
|
210
|
+
for i, token in enumerate(raw_tokens[index + 1:], start=index + 1):
|
|
211
|
+
if token.token_type == TokenType.L_PAREN:
|
|
212
|
+
depth += 1
|
|
213
|
+
|
|
214
|
+
_next = raw_tokens[i + 1] if i + 1 < len(raw_tokens) else None
|
|
215
|
+
if self._is_open_token(token, raw_tokens[i - 1], _next) and not depth:
|
|
216
|
+
break
|
|
217
|
+
|
|
218
|
+
if not depth:
|
|
219
|
+
tokens_to_check.append(token)
|
|
220
|
+
|
|
221
|
+
if token.token_type == TokenType.R_PAREN and depth:
|
|
222
|
+
depth -= 1
|
|
223
|
+
|
|
224
|
+
return not any(
|
|
225
|
+
self._is_begin(t, tokens_to_check[i + 1] if i + 1 < len(tokens_to_check) else None)
|
|
226
|
+
for i, t in enumerate(tokens_to_check)
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def _block_already_opened(self) -> bool:
|
|
230
|
+
"""El BEGIN (o BEGIN TRY / BEGIN CATCH) del bloque ya fue consumido."""
|
|
231
|
+
if self._prev and self._prev.token_type == TokenType.BEGIN:
|
|
232
|
+
return True
|
|
233
|
+
before = self._tokens[self._index - 2] if self._index >= 2 else None
|
|
234
|
+
return self._is_word(self._prev, 'TRY', 'CATCH') and self._is_word(before, 'BEGIN')
|
|
235
|
+
|
|
236
|
+
def _parse_batch_statements(
|
|
237
|
+
self,
|
|
238
|
+
parse_method: Callable[[Parser], exp.Expr | None],
|
|
239
|
+
sep_first_statement: bool = True,
|
|
240
|
+
top_level: bool = False,
|
|
241
|
+
) -> list[exp.Expr | None]:
|
|
242
|
+
expressions = []
|
|
243
|
+
|
|
244
|
+
if sep_first_statement:
|
|
245
|
+
# Si quien nos llama ya consumio el BEGIN del bloque (p.ej.
|
|
246
|
+
# _parse_create), un BEGIN aqui es un bloque anidado, no el nuestro.
|
|
247
|
+
if not self._block_already_opened():
|
|
248
|
+
self._match(TokenType.BEGIN)
|
|
249
|
+
# Si el chunk termina en el BEGIN no hay sentencia: no agregar None
|
|
250
|
+
first = parse_method(self)
|
|
251
|
+
if first is not None or top_level:
|
|
252
|
+
expressions.append(first)
|
|
253
|
+
|
|
254
|
+
chunks_length = len(self._chunks)
|
|
255
|
+
while self._chunk_index < chunks_length:
|
|
256
|
+
self._advance_chunk()
|
|
257
|
+
|
|
258
|
+
if not top_level and self._match(TokenType.ELSE, advance=False):
|
|
259
|
+
return expressions
|
|
260
|
+
|
|
261
|
+
if (
|
|
262
|
+
expressions
|
|
263
|
+
and self._curr
|
|
264
|
+
and self._curr.token_type == TokenType.END
|
|
265
|
+
and (not self._next or self._next.token_type == TokenType.ELSE or self._is_word(self._next, 'TRY', 'CATCH'))
|
|
266
|
+
):
|
|
267
|
+
self._advance()
|
|
268
|
+
expressions.append(exp.EndStatement())
|
|
269
|
+
return expressions
|
|
270
|
+
|
|
271
|
+
expressions.append(parse_method(self))
|
|
272
|
+
|
|
273
|
+
if self._index < self._tokens_size:
|
|
274
|
+
self.raise_error("Invalid expression / Unexpected token")
|
|
275
|
+
|
|
276
|
+
self.check_errors()
|
|
277
|
+
|
|
278
|
+
return expressions
|