get-objects-lib 0.1.0__tar.gz
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-0.1.0/PKG-INFO +231 -0
- get_objects_lib-0.1.0/README.md +216 -0
- get_objects_lib-0.1.0/get_objects_lib/__init__.py +12 -0
- get_objects_lib-0.1.0/get_objects_lib/dialect/__init__.py +3 -0
- get_objects_lib-0.1.0/get_objects_lib/dialect/dialect.py +11 -0
- get_objects_lib-0.1.0/get_objects_lib/dialect/expressions.py +116 -0
- get_objects_lib-0.1.0/get_objects_lib/dialect/generator.py +231 -0
- get_objects_lib-0.1.0/get_objects_lib/dialect/parser/__init__.py +3 -0
- get_objects_lib-0.1.0/get_objects_lib/dialect/parser/base.py +278 -0
- get_objects_lib-0.1.0/get_objects_lib/dialect/parser/non_semicolon.py +182 -0
- get_objects_lib-0.1.0/get_objects_lib/dialect/parser/sql_server.py +512 -0
- get_objects_lib-0.1.0/get_objects_lib/dialect/text_utils.py +22 -0
- get_objects_lib-0.1.0/get_objects_lib/dialect/tokenizer.py +51 -0
- get_objects_lib-0.1.0/get_objects_lib/objects/__init__.py +3 -0
- get_objects_lib-0.1.0/get_objects_lib/objects/dependencies.py +162 -0
- get_objects_lib-0.1.0/get_objects_lib/objects/header.py +160 -0
- get_objects_lib-0.1.0/get_objects_lib/objects/standardize.py +72 -0
- get_objects_lib-0.1.0/pyproject.toml +30 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: get-objects-lib
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Extrae objetos referenciados en scripts T-SQL
|
|
5
|
+
Keywords: sql,tsql,sqlglot
|
|
6
|
+
Author: Ivan-Rosales
|
|
7
|
+
Author-email: 61328655+Ivan-Rosales@users.noreply.github.com
|
|
8
|
+
Requires-Python: >=3.14
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Dist: sqlglot (>=30.18.0,<31.0.0)
|
|
12
|
+
Project-URL: Repository, https://gitlabv2.intelisis-solutions.com:8008/TICS/Interfaces/utilidades/get_objects_lib
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# get-objects-lib
|
|
16
|
+
|
|
17
|
+
Parser y generador de **T-SQL (SQL Server)** construido sobre [sqlglot](https://github.com/tobymao/sqlglot),
|
|
18
|
+
pensado para scripts reales: sin `;` entre sentencias, con `GO`, bloques `BEGIN ... END`,
|
|
19
|
+
procedimientos, funciones, vistas y triggers. Encima del dialecto hay un paquete (`objects`) para
|
|
20
|
+
analizar objetos:
|
|
21
|
+
|
|
22
|
+
- **`get_dependencies`**: qué procedimientos, funciones y tablas/vistas usa un query u objeto.
|
|
23
|
+
- **`standardize_object`**: convierte un `CREATE` / `ALTER` / `CREATE OR ALTER` a una forma estándar
|
|
24
|
+
que se puede ejecutar siempre (crea un objeto mínimo si no existe y luego aplica el `ALTER`).
|
|
25
|
+
|
|
26
|
+
## Instalación
|
|
27
|
+
|
|
28
|
+
Requiere Python 3.14 y [Poetry](https://python-poetry.org/).
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
poetry install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Los ejemplos y las pruebas se corren desde la raíz del proyecto.
|
|
35
|
+
|
|
36
|
+
## Uso
|
|
37
|
+
|
|
38
|
+
### Dialecto: parsear y generar T-SQL
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import sqlglot
|
|
42
|
+
from get_objects_lib import SqlServer
|
|
43
|
+
|
|
44
|
+
sql = """
|
|
45
|
+
IF @Modo = 1
|
|
46
|
+
EXEC dbo.spProcesar @ID
|
|
47
|
+
ELSE
|
|
48
|
+
BEGIN
|
|
49
|
+
UPDATE Venta SET Estatus = 'X' WHERE ID = @ID
|
|
50
|
+
PRINT 'actualizado'
|
|
51
|
+
END
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
for statement in sqlglot.parse(sql, read=SqlServer):
|
|
55
|
+
print(repr(statement)) # árbol de expresiones
|
|
56
|
+
print(statement.sql(dialect=SqlServer)) # de vuelta a T-SQL
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`sqlglot.parse` devuelve una sentencia por elemento. Un `GO` aparece como su propia sentencia
|
|
60
|
+
(`BatchSeparator`).
|
|
61
|
+
|
|
62
|
+
### Dependencias de un objeto o query
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from get_objects_lib import get_dependencies
|
|
66
|
+
|
|
67
|
+
for ref in get_dependencies(sql):
|
|
68
|
+
print(ref.kind, ref) # p.ej. "procedure dbo.spProcesar", "table_or_view Venta"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Cada resultado es un `ObjectRef(kind, name, schema, catalog, server)`:
|
|
72
|
+
|
|
73
|
+
| `kind` | Qué detecta |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `procedure` | `EXEC proc`, `EXEC @r = proc`, `INSERT ... EXEC proc`, `sp_executesql` |
|
|
76
|
+
| `function` | `dbo.fn(...)` en cualquier expresión, funciones de tabla en `FROM` y `CROSS/OUTER APPLY` |
|
|
77
|
+
| `table_or_view` | tablas y vistas (sin catálogo no se pueden distinguir), incluida la tabla de un trigger |
|
|
78
|
+
|
|
79
|
+
- Salen en orden de aparición y sin repetidos. Sin esquema se asume `dbo`, así que `SendHttp` y
|
|
80
|
+
`dbo.SendHttp` cuentan una sola vez.
|
|
81
|
+
- Se ignoran: tablas temporales (`#t`, `##t`), variables de tabla (`@t`), CTEs, alias,
|
|
82
|
+
`inserted`/`deleted` de los triggers, métodos XML (`T.c.value(...)`), funciones de tabla del
|
|
83
|
+
sistema (`STRING_SPLIT`, `OPENJSON`, ...) y el propio objeto que se está definiendo.
|
|
84
|
+
- Si el script define un objeto, se analiza todo su lote (hasta el `GO`), como hace SQL Server.
|
|
85
|
+
- Soporta nombres de hasta cuatro partes (`servidor.base.esquema.objeto`).
|
|
86
|
+
|
|
87
|
+
### Forma estándar de un objeto
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from get_objects_lib import standardize_object
|
|
91
|
+
|
|
92
|
+
print(standardize_object(sql)) # SET ... OFF por defecto
|
|
93
|
+
print(standardize_object(sql, ansi_nulls=True, quoted_identifier=True))
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Para un script como:
|
|
97
|
+
|
|
98
|
+
```sql
|
|
99
|
+
SET ANSI_NULLS ON;
|
|
100
|
+
GO
|
|
101
|
+
-- Comentario de encabezado
|
|
102
|
+
CREATE PROCEDURE [dbo].[spEjemplo] @ID INT
|
|
103
|
+
AS
|
|
104
|
+
BEGIN
|
|
105
|
+
SELECT * FROM Venta WHERE ID = @ID
|
|
106
|
+
END
|
|
107
|
+
GO
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
genera:
|
|
111
|
+
|
|
112
|
+
```sql
|
|
113
|
+
SET ANSI_NULLS OFF;
|
|
114
|
+
GO
|
|
115
|
+
SET QUOTED_IDENTIFIER OFF;
|
|
116
|
+
GO
|
|
117
|
+
IF OBJECT_ID(N'dbo.spEjemplo', N'P') IS NULL
|
|
118
|
+
EXEC (N'CREATE PROCEDURE dbo.spEjemplo AS RETURN 0;');
|
|
119
|
+
GO
|
|
120
|
+
ALTER PROCEDURE [dbo].[spEjemplo] @ID INT
|
|
121
|
+
AS
|
|
122
|
+
BEGIN
|
|
123
|
+
SELECT * FROM Venta WHERE ID = @ID
|
|
124
|
+
END
|
|
125
|
+
GO
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- Acepta `CREATE`, `ALTER` y `CREATE OR ALTER` de procedimientos, funciones, vistas y triggers.
|
|
129
|
+
- **El cuerpo se copia tal cual** del texto original (comentarios y formato incluidos); lo que va
|
|
130
|
+
antes del `CREATE`/`ALTER` y los lotes posteriores (p.ej. un `GRANT` tras el `GO`) se descartan.
|
|
131
|
+
- El `CREATE` mínimo es del mismo tipo que el objeto real, porque `ALTER` no puede cambiarlo:
|
|
132
|
+
|
|
133
|
+
| Tipo (`OBJECT_ID`) | Objeto mínimo |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `P` procedimiento | `CREATE PROCEDURE x AS RETURN 0;` |
|
|
136
|
+
| `V` vista | `CREATE VIEW x AS SELECT 1 AS Stub;` |
|
|
137
|
+
| `FN` función escalar | `CREATE FUNCTION x() RETURNS INT AS BEGIN RETURN NULL END;` |
|
|
138
|
+
| `IF` función de tabla en línea | `CREATE FUNCTION x() RETURNS TABLE AS RETURN SELECT 1 AS Stub;` |
|
|
139
|
+
| `TF` función de tabla de varias sentencias | `CREATE FUNCTION x() RETURNS @Stub TABLE (Stub INT) AS BEGIN RETURN END;` |
|
|
140
|
+
| `TR` trigger | `CREATE TRIGGER x ON tabla <momento> <eventos> AS RETURN;` (misma tabla, momento y eventos) |
|
|
141
|
+
|
|
142
|
+
- El nombre va sin corchetes salvo que los necesite (espacios, palabras reservadas: `dbo.[Order]`).
|
|
143
|
+
- Respeta el fin de línea del script (CRLF en archivos de SSMS).
|
|
144
|
+
- Lanza `ValueError` si el script no define un objeto, o si es un trigger DDL (`ON DATABASE`).
|
|
145
|
+
|
|
146
|
+
## Qué cubre el dialecto
|
|
147
|
+
|
|
148
|
+
Además de lo que ya soporta el T-SQL de sqlglot, el dialecto `SqlServer` maneja:
|
|
149
|
+
|
|
150
|
+
- **Separación de sentencias sin `;`** y lotes con `GO` / `GO n` (solo en su línea; no dentro de
|
|
151
|
+
strings o comentarios).
|
|
152
|
+
- **Control de flujo**: `IF` / `ELSE IF` / `ELSE` con y sin `BEGIN`, condiciones con subconsultas
|
|
153
|
+
(`IF (SELECT COUNT(*) ...) > 0`), `WHILE`, `BREAK`, `CONTINUE`, `RETURN [expr]`, `GOTO` y
|
|
154
|
+
etiquetas, bloques `BEGIN ... END` sueltos, `BEGIN TRY ... END CATCH`, `WAITFOR`.
|
|
155
|
+
- **Objetos**: `CREATE` / `ALTER` / `CREATE OR ALTER` de procedimientos (con o sin paréntesis en
|
|
156
|
+
los parámetros, `OUTPUT`/`OUT`), funciones escalares y de tabla, vistas y triggers; opciones
|
|
157
|
+
`WITH RECOMPILE`, `SCHEMABINDING`, `EXECUTE AS`, `RETURNS NULL ON NULL INPUT`, etc.
|
|
158
|
+
- **Sentencias**: `EXEC` con parámetros con nombre, valor de retorno, SQL dinámico (`EXEC (...)`,
|
|
159
|
+
`EXEC (...) AT servidor`), `INSERT ... EXEC`, `INSERT ... SELECT`, `DEFAULT VALUES`,
|
|
160
|
+
`DELETE/UPDATE TOP (n)`, `MERGE`, CTEs, cursores (`DECLARE ... CURSOR`, `OPEN`, `FETCH`, `CLOSE`,
|
|
161
|
+
`DEALLOCATE`), transacciones (`BEGIN TRAN`, `COMMIT`, `ROLLBACK`, `SAVE TRAN`), `PRINT`,
|
|
162
|
+
`RAISERROR`, `THROW`, asignaciones compuestas (`+=`, `-=`, ...), `SET TRANSACTION ISOLATION LEVEL`.
|
|
163
|
+
- **Tablas**: pistas con y sin `WITH` (`FROM Venta (NOLOCK)`), índices en línea en `CREATE TABLE`
|
|
164
|
+
y `DECLARE @t TABLE`.
|
|
165
|
+
- **Comandos no modelados** (`DBCC`, `BULK INSERT`, `UPDATE STATISTICS`, `ENABLE/DISABLE TRIGGER`,
|
|
166
|
+
`EXECUTE AS`, `REVERT`, `CHECKPOINT`, ...) se conservan como `Command` de su sentencia.
|
|
167
|
+
- **Generación fiel a T-SQL**: no reemplaza funciones (`SYSDATETIME`, `SUSER_SNAME`, `DATENAME`,
|
|
168
|
+
`DATEDIFF`, ...), conserva el nombre de las funciones de usuario y escribe `INT`.
|
|
169
|
+
|
|
170
|
+
### Expresiones propias
|
|
171
|
+
|
|
172
|
+
Definidas en [`get_objects_lib/dialect/expressions.py`](get_objects_lib/dialect/expressions.py):
|
|
173
|
+
`AlterObject`, `BatchSeparator`, `BeginEnd`, `CompoundAssignment`, `CursorStatement`, `DeclareCursor`,
|
|
174
|
+
`ExecParameter`, `ExecuteAt`, `FetchCursor`, `Goto`, `InlineIndex`, `Label`, `LoopControl`, `Print`,
|
|
175
|
+
`RaiseError`, `ReturnStatement`, `SaveTransaction`, `Throw`, `Top`, `TriggerSpec`, `TryCatch`, `WaitFor`.
|
|
176
|
+
|
|
177
|
+
## Estructura
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
get_objects_lib/ paquete instalable; expone SqlServer, get_dependencies,
|
|
181
|
+
│ standardize_object, ObjectRef, ObjectHeader, find_object_header
|
|
182
|
+
├── dialect/
|
|
183
|
+
│ ├── dialect.py dialecto SqlServer (une tokenizer, parser y generador)
|
|
184
|
+
│ ├── tokenizer.py GO/PRINT/FETCH sin tragarse el script, +=, nombres de variables
|
|
185
|
+
│ ├── expressions.py expresiones propias de T-SQL
|
|
186
|
+
│ ├── generator.py generacion de T-SQL
|
|
187
|
+
│ ├── text_utils.py
|
|
188
|
+
│ └── parser/
|
|
189
|
+
│ ├── non_semicolon.py separa el script en sentencias (chunks) sin depender de ;
|
|
190
|
+
│ ├── base.py reglas de inicio de sentencia y de bloques
|
|
191
|
+
│ └── sql_server.py parseo de cada construccion de T-SQL
|
|
192
|
+
└── objects/
|
|
193
|
+
├── header.py encabezado del objeto (CREATE/ALTER, tipo, nombre, lote)
|
|
194
|
+
├── dependencies.py get_dependencies
|
|
195
|
+
└── standardize.py standardize_object
|
|
196
|
+
tests/ pruebas; ver tests/README.md
|
|
197
|
+
├── golden.py utilidades y generacion de resultados esperados
|
|
198
|
+
├── test_*.py
|
|
199
|
+
└── fixtures/
|
|
200
|
+
├── dialect/ scripts del parser/generador + expected/
|
|
201
|
+
└── objects/ scripts de objetos + expected/
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Pruebas
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
poetry run python -m unittest
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Son pruebas *golden*: cada script de `tests/fixtures/` se compara con sus resultados esperados en
|
|
211
|
+
`expected/` (árbol, SQL generado, dependencias y forma estándar), y además todos los scripts deben
|
|
212
|
+
cumplir propiedades generales (el SQL generado es estable, el árbol es el mismo con CRLF, unidos con
|
|
213
|
+
`GO` conservan sus sentencias). Los casos se descubren solos.
|
|
214
|
+
|
|
215
|
+
Para agregar un caso: crea el script en `tests/fixtures/dialect/` o `tests/fixtures/objects/`, genera
|
|
216
|
+
sus resultados esperados con `poetry run python -m tests.golden <script>.sql`, **revísalos a mano** y
|
|
217
|
+
agrégalo al catálogo de [`tests/README.md`](tests/README.md), que describe qué cubre cada caso.
|
|
218
|
+
|
|
219
|
+
## Limitaciones y decisiones de diseño
|
|
220
|
+
|
|
221
|
+
- **Cuerpo de un procedimiento sin `BEGIN`**: en SQL Server llega hasta el `GO`; el parser toma una
|
|
222
|
+
sola sentencia (o hasta el `END` si tiene `BEGIN`). `get_dependencies` sí analiza todo el lote.
|
|
223
|
+
- **Un objeto por script**: si un script define varios, `standardize_object` y `get_dependencies`
|
|
224
|
+
toman el primero.
|
|
225
|
+
- **SQL dinámico**: lo que se ejecuta con `EXEC (@sql)` o `sp_executesql` no se analiza.
|
|
226
|
+
- **Comandos no modelados** (`Command`): su contenido no se analiza para dependencias (p.ej. la
|
|
227
|
+
tabla de un `BULK INSERT`).
|
|
228
|
+
- **Normalizaciones del generador** (equivalentes en T-SQL): agrega `AS` en los alias, escribe
|
|
229
|
+
`WITH (NOLOCK)` aunque el original no tenga `WITH`, `OUT` como `OUTPUT`, `IS NOT NULL` como
|
|
230
|
+
`NOT ... IS NULL`, y agrega alias a las columnas de tablas derivadas y CTEs (`SELECT ID AS ID`).
|
|
231
|
+
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# get-objects-lib
|
|
2
|
+
|
|
3
|
+
Parser y generador de **T-SQL (SQL Server)** construido sobre [sqlglot](https://github.com/tobymao/sqlglot),
|
|
4
|
+
pensado para scripts reales: sin `;` entre sentencias, con `GO`, bloques `BEGIN ... END`,
|
|
5
|
+
procedimientos, funciones, vistas y triggers. Encima del dialecto hay un paquete (`objects`) para
|
|
6
|
+
analizar objetos:
|
|
7
|
+
|
|
8
|
+
- **`get_dependencies`**: qué procedimientos, funciones y tablas/vistas usa un query u objeto.
|
|
9
|
+
- **`standardize_object`**: convierte un `CREATE` / `ALTER` / `CREATE OR ALTER` a una forma estándar
|
|
10
|
+
que se puede ejecutar siempre (crea un objeto mínimo si no existe y luego aplica el `ALTER`).
|
|
11
|
+
|
|
12
|
+
## Instalación
|
|
13
|
+
|
|
14
|
+
Requiere Python 3.14 y [Poetry](https://python-poetry.org/).
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
poetry install
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Los ejemplos y las pruebas se corren desde la raíz del proyecto.
|
|
21
|
+
|
|
22
|
+
## Uso
|
|
23
|
+
|
|
24
|
+
### Dialecto: parsear y generar T-SQL
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import sqlglot
|
|
28
|
+
from get_objects_lib import SqlServer
|
|
29
|
+
|
|
30
|
+
sql = """
|
|
31
|
+
IF @Modo = 1
|
|
32
|
+
EXEC dbo.spProcesar @ID
|
|
33
|
+
ELSE
|
|
34
|
+
BEGIN
|
|
35
|
+
UPDATE Venta SET Estatus = 'X' WHERE ID = @ID
|
|
36
|
+
PRINT 'actualizado'
|
|
37
|
+
END
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
for statement in sqlglot.parse(sql, read=SqlServer):
|
|
41
|
+
print(repr(statement)) # árbol de expresiones
|
|
42
|
+
print(statement.sql(dialect=SqlServer)) # de vuelta a T-SQL
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`sqlglot.parse` devuelve una sentencia por elemento. Un `GO` aparece como su propia sentencia
|
|
46
|
+
(`BatchSeparator`).
|
|
47
|
+
|
|
48
|
+
### Dependencias de un objeto o query
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from get_objects_lib import get_dependencies
|
|
52
|
+
|
|
53
|
+
for ref in get_dependencies(sql):
|
|
54
|
+
print(ref.kind, ref) # p.ej. "procedure dbo.spProcesar", "table_or_view Venta"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Cada resultado es un `ObjectRef(kind, name, schema, catalog, server)`:
|
|
58
|
+
|
|
59
|
+
| `kind` | Qué detecta |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `procedure` | `EXEC proc`, `EXEC @r = proc`, `INSERT ... EXEC proc`, `sp_executesql` |
|
|
62
|
+
| `function` | `dbo.fn(...)` en cualquier expresión, funciones de tabla en `FROM` y `CROSS/OUTER APPLY` |
|
|
63
|
+
| `table_or_view` | tablas y vistas (sin catálogo no se pueden distinguir), incluida la tabla de un trigger |
|
|
64
|
+
|
|
65
|
+
- Salen en orden de aparición y sin repetidos. Sin esquema se asume `dbo`, así que `SendHttp` y
|
|
66
|
+
`dbo.SendHttp` cuentan una sola vez.
|
|
67
|
+
- Se ignoran: tablas temporales (`#t`, `##t`), variables de tabla (`@t`), CTEs, alias,
|
|
68
|
+
`inserted`/`deleted` de los triggers, métodos XML (`T.c.value(...)`), funciones de tabla del
|
|
69
|
+
sistema (`STRING_SPLIT`, `OPENJSON`, ...) y el propio objeto que se está definiendo.
|
|
70
|
+
- Si el script define un objeto, se analiza todo su lote (hasta el `GO`), como hace SQL Server.
|
|
71
|
+
- Soporta nombres de hasta cuatro partes (`servidor.base.esquema.objeto`).
|
|
72
|
+
|
|
73
|
+
### Forma estándar de un objeto
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from get_objects_lib import standardize_object
|
|
77
|
+
|
|
78
|
+
print(standardize_object(sql)) # SET ... OFF por defecto
|
|
79
|
+
print(standardize_object(sql, ansi_nulls=True, quoted_identifier=True))
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Para un script como:
|
|
83
|
+
|
|
84
|
+
```sql
|
|
85
|
+
SET ANSI_NULLS ON;
|
|
86
|
+
GO
|
|
87
|
+
-- Comentario de encabezado
|
|
88
|
+
CREATE PROCEDURE [dbo].[spEjemplo] @ID INT
|
|
89
|
+
AS
|
|
90
|
+
BEGIN
|
|
91
|
+
SELECT * FROM Venta WHERE ID = @ID
|
|
92
|
+
END
|
|
93
|
+
GO
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
genera:
|
|
97
|
+
|
|
98
|
+
```sql
|
|
99
|
+
SET ANSI_NULLS OFF;
|
|
100
|
+
GO
|
|
101
|
+
SET QUOTED_IDENTIFIER OFF;
|
|
102
|
+
GO
|
|
103
|
+
IF OBJECT_ID(N'dbo.spEjemplo', N'P') IS NULL
|
|
104
|
+
EXEC (N'CREATE PROCEDURE dbo.spEjemplo AS RETURN 0;');
|
|
105
|
+
GO
|
|
106
|
+
ALTER PROCEDURE [dbo].[spEjemplo] @ID INT
|
|
107
|
+
AS
|
|
108
|
+
BEGIN
|
|
109
|
+
SELECT * FROM Venta WHERE ID = @ID
|
|
110
|
+
END
|
|
111
|
+
GO
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
- Acepta `CREATE`, `ALTER` y `CREATE OR ALTER` de procedimientos, funciones, vistas y triggers.
|
|
115
|
+
- **El cuerpo se copia tal cual** del texto original (comentarios y formato incluidos); lo que va
|
|
116
|
+
antes del `CREATE`/`ALTER` y los lotes posteriores (p.ej. un `GRANT` tras el `GO`) se descartan.
|
|
117
|
+
- El `CREATE` mínimo es del mismo tipo que el objeto real, porque `ALTER` no puede cambiarlo:
|
|
118
|
+
|
|
119
|
+
| Tipo (`OBJECT_ID`) | Objeto mínimo |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `P` procedimiento | `CREATE PROCEDURE x AS RETURN 0;` |
|
|
122
|
+
| `V` vista | `CREATE VIEW x AS SELECT 1 AS Stub;` |
|
|
123
|
+
| `FN` función escalar | `CREATE FUNCTION x() RETURNS INT AS BEGIN RETURN NULL END;` |
|
|
124
|
+
| `IF` función de tabla en línea | `CREATE FUNCTION x() RETURNS TABLE AS RETURN SELECT 1 AS Stub;` |
|
|
125
|
+
| `TF` función de tabla de varias sentencias | `CREATE FUNCTION x() RETURNS @Stub TABLE (Stub INT) AS BEGIN RETURN END;` |
|
|
126
|
+
| `TR` trigger | `CREATE TRIGGER x ON tabla <momento> <eventos> AS RETURN;` (misma tabla, momento y eventos) |
|
|
127
|
+
|
|
128
|
+
- El nombre va sin corchetes salvo que los necesite (espacios, palabras reservadas: `dbo.[Order]`).
|
|
129
|
+
- Respeta el fin de línea del script (CRLF en archivos de SSMS).
|
|
130
|
+
- Lanza `ValueError` si el script no define un objeto, o si es un trigger DDL (`ON DATABASE`).
|
|
131
|
+
|
|
132
|
+
## Qué cubre el dialecto
|
|
133
|
+
|
|
134
|
+
Además de lo que ya soporta el T-SQL de sqlglot, el dialecto `SqlServer` maneja:
|
|
135
|
+
|
|
136
|
+
- **Separación de sentencias sin `;`** y lotes con `GO` / `GO n` (solo en su línea; no dentro de
|
|
137
|
+
strings o comentarios).
|
|
138
|
+
- **Control de flujo**: `IF` / `ELSE IF` / `ELSE` con y sin `BEGIN`, condiciones con subconsultas
|
|
139
|
+
(`IF (SELECT COUNT(*) ...) > 0`), `WHILE`, `BREAK`, `CONTINUE`, `RETURN [expr]`, `GOTO` y
|
|
140
|
+
etiquetas, bloques `BEGIN ... END` sueltos, `BEGIN TRY ... END CATCH`, `WAITFOR`.
|
|
141
|
+
- **Objetos**: `CREATE` / `ALTER` / `CREATE OR ALTER` de procedimientos (con o sin paréntesis en
|
|
142
|
+
los parámetros, `OUTPUT`/`OUT`), funciones escalares y de tabla, vistas y triggers; opciones
|
|
143
|
+
`WITH RECOMPILE`, `SCHEMABINDING`, `EXECUTE AS`, `RETURNS NULL ON NULL INPUT`, etc.
|
|
144
|
+
- **Sentencias**: `EXEC` con parámetros con nombre, valor de retorno, SQL dinámico (`EXEC (...)`,
|
|
145
|
+
`EXEC (...) AT servidor`), `INSERT ... EXEC`, `INSERT ... SELECT`, `DEFAULT VALUES`,
|
|
146
|
+
`DELETE/UPDATE TOP (n)`, `MERGE`, CTEs, cursores (`DECLARE ... CURSOR`, `OPEN`, `FETCH`, `CLOSE`,
|
|
147
|
+
`DEALLOCATE`), transacciones (`BEGIN TRAN`, `COMMIT`, `ROLLBACK`, `SAVE TRAN`), `PRINT`,
|
|
148
|
+
`RAISERROR`, `THROW`, asignaciones compuestas (`+=`, `-=`, ...), `SET TRANSACTION ISOLATION LEVEL`.
|
|
149
|
+
- **Tablas**: pistas con y sin `WITH` (`FROM Venta (NOLOCK)`), índices en línea en `CREATE TABLE`
|
|
150
|
+
y `DECLARE @t TABLE`.
|
|
151
|
+
- **Comandos no modelados** (`DBCC`, `BULK INSERT`, `UPDATE STATISTICS`, `ENABLE/DISABLE TRIGGER`,
|
|
152
|
+
`EXECUTE AS`, `REVERT`, `CHECKPOINT`, ...) se conservan como `Command` de su sentencia.
|
|
153
|
+
- **Generación fiel a T-SQL**: no reemplaza funciones (`SYSDATETIME`, `SUSER_SNAME`, `DATENAME`,
|
|
154
|
+
`DATEDIFF`, ...), conserva el nombre de las funciones de usuario y escribe `INT`.
|
|
155
|
+
|
|
156
|
+
### Expresiones propias
|
|
157
|
+
|
|
158
|
+
Definidas en [`get_objects_lib/dialect/expressions.py`](get_objects_lib/dialect/expressions.py):
|
|
159
|
+
`AlterObject`, `BatchSeparator`, `BeginEnd`, `CompoundAssignment`, `CursorStatement`, `DeclareCursor`,
|
|
160
|
+
`ExecParameter`, `ExecuteAt`, `FetchCursor`, `Goto`, `InlineIndex`, `Label`, `LoopControl`, `Print`,
|
|
161
|
+
`RaiseError`, `ReturnStatement`, `SaveTransaction`, `Throw`, `Top`, `TriggerSpec`, `TryCatch`, `WaitFor`.
|
|
162
|
+
|
|
163
|
+
## Estructura
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
get_objects_lib/ paquete instalable; expone SqlServer, get_dependencies,
|
|
167
|
+
│ standardize_object, ObjectRef, ObjectHeader, find_object_header
|
|
168
|
+
├── dialect/
|
|
169
|
+
│ ├── dialect.py dialecto SqlServer (une tokenizer, parser y generador)
|
|
170
|
+
│ ├── tokenizer.py GO/PRINT/FETCH sin tragarse el script, +=, nombres de variables
|
|
171
|
+
│ ├── expressions.py expresiones propias de T-SQL
|
|
172
|
+
│ ├── generator.py generacion de T-SQL
|
|
173
|
+
│ ├── text_utils.py
|
|
174
|
+
│ └── parser/
|
|
175
|
+
│ ├── non_semicolon.py separa el script en sentencias (chunks) sin depender de ;
|
|
176
|
+
│ ├── base.py reglas de inicio de sentencia y de bloques
|
|
177
|
+
│ └── sql_server.py parseo de cada construccion de T-SQL
|
|
178
|
+
└── objects/
|
|
179
|
+
├── header.py encabezado del objeto (CREATE/ALTER, tipo, nombre, lote)
|
|
180
|
+
├── dependencies.py get_dependencies
|
|
181
|
+
└── standardize.py standardize_object
|
|
182
|
+
tests/ pruebas; ver tests/README.md
|
|
183
|
+
├── golden.py utilidades y generacion de resultados esperados
|
|
184
|
+
├── test_*.py
|
|
185
|
+
└── fixtures/
|
|
186
|
+
├── dialect/ scripts del parser/generador + expected/
|
|
187
|
+
└── objects/ scripts de objetos + expected/
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Pruebas
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
poetry run python -m unittest
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Son pruebas *golden*: cada script de `tests/fixtures/` se compara con sus resultados esperados en
|
|
197
|
+
`expected/` (árbol, SQL generado, dependencias y forma estándar), y además todos los scripts deben
|
|
198
|
+
cumplir propiedades generales (el SQL generado es estable, el árbol es el mismo con CRLF, unidos con
|
|
199
|
+
`GO` conservan sus sentencias). Los casos se descubren solos.
|
|
200
|
+
|
|
201
|
+
Para agregar un caso: crea el script en `tests/fixtures/dialect/` o `tests/fixtures/objects/`, genera
|
|
202
|
+
sus resultados esperados con `poetry run python -m tests.golden <script>.sql`, **revísalos a mano** y
|
|
203
|
+
agrégalo al catálogo de [`tests/README.md`](tests/README.md), que describe qué cubre cada caso.
|
|
204
|
+
|
|
205
|
+
## Limitaciones y decisiones de diseño
|
|
206
|
+
|
|
207
|
+
- **Cuerpo de un procedimiento sin `BEGIN`**: en SQL Server llega hasta el `GO`; el parser toma una
|
|
208
|
+
sola sentencia (o hasta el `END` si tiene `BEGIN`). `get_dependencies` sí analiza todo el lote.
|
|
209
|
+
- **Un objeto por script**: si un script define varios, `standardize_object` y `get_dependencies`
|
|
210
|
+
toman el primero.
|
|
211
|
+
- **SQL dinámico**: lo que se ejecuta con `EXEC (@sql)` o `sp_executesql` no se analiza.
|
|
212
|
+
- **Comandos no modelados** (`Command`): su contenido no se analiza para dependencias (p.ej. la
|
|
213
|
+
tabla de un `BULK INSERT`).
|
|
214
|
+
- **Normalizaciones del generador** (equivalentes en T-SQL): agrega `AS` en los alias, escribe
|
|
215
|
+
`WITH (NOLOCK)` aunque el original no tenga `WITH`, `OUT` como `OUTPUT`, `IS NOT NULL` como
|
|
216
|
+
`NOT ... IS NULL`, y agrega alias a las columnas de tablas derivadas y CTEs (`SELECT ID AS ID`).
|
|
@@ -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."""
|