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,162 @@
1
+ from dataclasses import dataclass
2
+
3
+ import sqlglot
4
+ from sqlglot import exp
5
+
6
+ from get_objects_lib.dialect import SqlServer
7
+ from get_objects_lib.dialect.expressions import ExecParameter
8
+ from get_objects_lib.objects.header import find_object_header, quote_name
9
+
10
+ PROCEDURE = 'procedure'
11
+ FUNCTION = 'function'
12
+ # Sin catalogo no se puede distinguir una tabla de una vista
13
+ TABLE_OR_VIEW = 'table_or_view'
14
+
15
+ # Tablas virtuales de un trigger
16
+ TRIGGER_PSEUDO_TABLES = {'inserted', 'deleted'}
17
+
18
+ # Funciones de tabla del sistema que se parsean igual que una de usuario
19
+ BUILTIN_TABLE_FUNCTIONS = {
20
+ 'STRING_SPLIT', 'OPENJSON', 'OPENXML', 'OPENQUERY', 'OPENROWSET', 'OPENDATASOURCE',
21
+ 'GENERATE_SERIES', 'CHANGETABLE', 'CONTAINSTABLE', 'FREETEXTTABLE',
22
+ }
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class ObjectRef:
27
+ kind: str
28
+ name: str
29
+ schema: str | None = None
30
+ catalog: str | None = None
31
+ server: str | None = None # servidor vinculado: servidor.base.esquema.objeto
32
+
33
+ def __str__(self) -> str:
34
+ return '.'.join(quote_name(p) for p in (self.server, self.catalog, self.schema, self.name) if p)
35
+
36
+ @property
37
+ def _key(self) -> tuple:
38
+ # SQL Server no distingue mayusculas; sin esquema se asume dbo
39
+ return (
40
+ self.kind,
41
+ (self.server or '').lower(),
42
+ (self.catalog or '').lower(),
43
+ (self.schema or 'dbo').lower(),
44
+ self.name.lower(),
45
+ )
46
+
47
+
48
+ def get_dependencies(sql: str) -> list[ObjectRef]:
49
+ """
50
+ Procedimientos, funciones y tablas/vistas que usa un query o la
51
+ definicion de un objeto, en orden de aparicion y sin repetidos.
52
+
53
+ No ve lo que se ejecuta con SQL dinamico (EXEC(@sql), sp_executesql).
54
+ Ignora tablas temporales, variables de tabla, CTEs, alias, inserted /
55
+ deleted de los triggers y el propio objeto que se esta definiendo. En un
56
+ trigger, la tabla a la que pertenece cuenta como dependencia.
57
+ """
58
+ header = find_object_header(sql)
59
+ if header:
60
+ statements = header.statements
61
+ own = ObjectRef(PROCEDURE, header.name, header.schema)._key[1:]
62
+ else:
63
+ statements = sqlglot.parse(sql, read=SqlServer)
64
+ own = None
65
+
66
+ refs: dict[tuple, ObjectRef] = {}
67
+ for statement in statements:
68
+ if statement is None:
69
+ continue
70
+
71
+ aliases = {a.name.lower() for a in statement.find_all(exp.TableAlias) if a.name}
72
+ ctes = {c.alias_or_name.lower() for c in statement.find_all(exp.CTE)}
73
+
74
+ for node in statement.walk(bfs=False):
75
+ ref = _reference(node, aliases, ctes)
76
+ if ref and ref._key[1:] != own:
77
+ refs.setdefault(ref._key, ref)
78
+
79
+ return list(refs.values())
80
+
81
+
82
+ def _reference(node: exp.Expr, aliases: set[str], ctes: set[str]) -> ObjectRef | None:
83
+ # EXEC dbo.sp ... / EXEC sp_executesql ...
84
+ if isinstance(node, exp.Execute):
85
+ target = node.this
86
+ if isinstance(target, exp.Table) and _is_name(target.this):
87
+ return _from_table(PROCEDURE, target)
88
+ return None
89
+
90
+ # dbo.fn(...) en cualquier expresion, incluido CROSS/OUTER APPLY
91
+ if isinstance(node, exp.Dot) and isinstance(node.expression, exp.Anonymous):
92
+ parts = _identifier_parts(node.this)
93
+ if not parts or any(p.lower() in aliases for p in parts):
94
+ # T.c.value(...), x.nodes(...): metodos XML sobre una columna o alias
95
+ return None
96
+ schema, catalog = parts[-1], (parts[-2] if len(parts) > 1 else None)
97
+ return ObjectRef(FUNCTION, node.expression.name, schema, catalog)
98
+
99
+ # fn(...) sin esquema en un APPLY
100
+ if isinstance(node, exp.Lateral) and isinstance(node.this, exp.Anonymous):
101
+ return _function_without_schema(node.this)
102
+
103
+ if not isinstance(node, exp.Table):
104
+ return None
105
+
106
+ # FROM dbo.fn(...) / FROM fn(...)
107
+ if isinstance(node.this, exp.Anonymous):
108
+ if isinstance(node.args.get('db'), ExecParameter):
109
+ return None # @xml.nodes(...)
110
+ if node.db:
111
+ return _from_table(FUNCTION, node, name=node.this.name)
112
+ return _function_without_schema(node.this)
113
+
114
+ if not _is_name(node.this) or node.this.args.get('temporary'):
115
+ return None # @tabla, #temporal
116
+ if node.find_ancestor(exp.Execute) or _is_create_target(node):
117
+ return None
118
+ if not node.db and (node.name.lower() in ctes or node.name.lower() in aliases):
119
+ return None # CTE, o UPDATE v ... FROM Venta v
120
+ if not node.db and node.name.lower() in TRIGGER_PSEUDO_TABLES:
121
+ return None # inserted / deleted de un trigger
122
+
123
+ return _from_table(TABLE_OR_VIEW, node)
124
+
125
+
126
+ def _is_name(node: exp.Expr) -> bool:
127
+ """Identificador, o esquema.objeto de un nombre de cuatro partes."""
128
+ if isinstance(node, exp.Dot):
129
+ return isinstance(node.this, exp.Identifier) and isinstance(node.expression, exp.Identifier)
130
+ return isinstance(node, exp.Identifier)
131
+
132
+
133
+ def _from_table(kind: str, table: exp.Table, name: str | None = None) -> ObjectRef:
134
+ if isinstance(table.this, exp.Dot):
135
+ # servidor.base.esquema.objeto: Table(this=Dot(esquema, objeto), db=base, catalog=servidor)
136
+ return ObjectRef(
137
+ kind, table.this.expression.name, table.this.this.name, table.db or None, table.catalog or None,
138
+ )
139
+ return ObjectRef(kind, name or table.name, table.db or None, table.catalog or None)
140
+
141
+
142
+ def _function_without_schema(func: exp.Anonymous) -> ObjectRef | None:
143
+ if func.name.upper() in BUILTIN_TABLE_FUNCTIONS:
144
+ return None
145
+ return ObjectRef(FUNCTION, func.name)
146
+
147
+
148
+ def _identifier_parts(node: exp.Expr) -> list[str] | None:
149
+ """dbo -> ['dbo']; Otra.dbo -> ['Otra', 'dbo']; cualquier otra cosa -> None."""
150
+ if isinstance(node, exp.Identifier):
151
+ return [node.name]
152
+ if isinstance(node, exp.Dot):
153
+ left, right = _identifier_parts(node.this), _identifier_parts(node.expression)
154
+ return left + right if left and right else None
155
+ return None
156
+
157
+
158
+ def _is_create_target(table: exp.Table) -> bool:
159
+ parent = table.parent
160
+ if isinstance(parent, (exp.UserDefinedFunction, exp.StoredProcedure)):
161
+ parent = parent.parent
162
+ return isinstance(parent, exp.Create) and parent.this in (table, table.parent)
@@ -0,0 +1,160 @@
1
+ import re
2
+ from dataclasses import dataclass
3
+
4
+ import sqlglot
5
+ from sqlglot import exp, TokenType
6
+
7
+ from get_objects_lib.dialect import SqlServer
8
+ from get_objects_lib.dialect.parser.base import BaseParser
9
+
10
+ OBJECT_TOKENS = (TokenType.PROCEDURE, TokenType.FUNCTION, TokenType.VIEW, TokenType.TRIGGER)
11
+
12
+ # Codigos de tipo de sys.objects / OBJECT_ID
13
+ PROCEDURE = 'P'
14
+ VIEW = 'V'
15
+ SCALAR_FUNCTION = 'FN'
16
+ INLINE_FUNCTION = 'IF'
17
+ TABLE_FUNCTION = 'TF'
18
+ TRIGGER = 'TR'
19
+
20
+ KIND_KEYWORDS = {
21
+ PROCEDURE: 'PROCEDURE',
22
+ VIEW: 'VIEW',
23
+ SCALAR_FUNCTION: 'FUNCTION',
24
+ INLINE_FUNCTION: 'FUNCTION',
25
+ TABLE_FUNCTION: 'FUNCTION',
26
+ TRIGGER: 'TRIGGER',
27
+ }
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class ObjectHeader:
32
+ """CREATE/ALTER de un procedimiento, funcion, vista o trigger dentro de un script."""
33
+ type_code: str
34
+ name: str
35
+ schema: str | None
36
+ # Posiciones en el texto original
37
+ start: int # inicio del CREATE/ALTER
38
+ body_start: int # justo despues de PROCEDURE/FUNCTION/VIEW
39
+ end: int # antes del GO que cierra el lote (o fin del texto)
40
+ statement: exp.Create
41
+ # Todas las sentencias del lote: en T-SQL el cuerpo llega hasta el GO
42
+ statements: tuple[exp.Expr, ...] = ()
43
+
44
+ @property
45
+ def keyword(self) -> str:
46
+ return KIND_KEYWORDS[self.type_code]
47
+
48
+ @property
49
+ def full_name(self) -> str:
50
+ return '.'.join(quote_name(p) for p in (self.schema, self.name) if p)
51
+
52
+
53
+ # Palabras reservadas de T-SQL (docs de SQL Server, "Reserved Keywords")
54
+ RESERVED_KEYWORDS = set('''
55
+ ADD ALL ALTER AND ANY AS ASC AUTHORIZATION BACKUP BEGIN BETWEEN BREAK BROWSE BULK BY CASCADE CASE
56
+ CHECK CHECKPOINT CLOSE CLUSTERED COALESCE COLLATE COLUMN COMMIT COMPUTE CONSTRAINT CONTAINS
57
+ CONTAINSTABLE CONTINUE CONVERT CREATE CROSS CURRENT CURRENT_DATE CURRENT_TIME CURRENT_TIMESTAMP
58
+ CURRENT_USER CURSOR DATABASE DBCC DEALLOCATE DECLARE DEFAULT DELETE DENY DESC DISK DISTINCT
59
+ DISTRIBUTED DOUBLE DROP DUMP ELSE END ERRLVL ESCAPE EXCEPT EXEC EXECUTE EXISTS EXIT EXTERNAL FETCH
60
+ FILE FILLFACTOR FOR FOREIGN FREETEXT FREETEXTTABLE FROM FULL FUNCTION GOTO GRANT GROUP HAVING
61
+ HOLDLOCK IDENTITY IDENTITY_INSERT IDENTITYCOL IF IN INDEX INNER INSERT INTERSECT INTO IS JOIN KEY
62
+ KILL LEFT LIKE LINENO LOAD MERGE NATIONAL NOCHECK NONCLUSTERED NOT NULL NULLIF OF OFF OFFSETS ON
63
+ OPEN OPENDATASOURCE OPENQUERY OPENROWSET OPENXML OPTION OR ORDER OUTER OVER PERCENT PIVOT PLAN
64
+ PRECISION PRIMARY PRINT PROC PROCEDURE PUBLIC RAISERROR READ READTEXT RECONFIGURE REFERENCES
65
+ REPLICATION RESTORE RESTRICT RETURN REVERT REVOKE RIGHT ROLLBACK ROWCOUNT ROWGUIDCOL RULE SAVE
66
+ SCHEMA SECURITYAUDIT SELECT SEMANTICKEYPHRASETABLE SEMANTICSIMILARITYDETAILSTABLE
67
+ SEMANTICSIMILARITYTABLE SESSION_USER SET SETUSER SHUTDOWN SOME STATISTICS SYSTEM_USER TABLE
68
+ TABLESAMPLE TEXTSIZE THEN TO TOP TRAN TRANSACTION TRIGGER TRUNCATE TRY_CONVERT TSEQUAL UNION UNIQUE
69
+ UNPIVOT UPDATE UPDATETEXT USE USER VALUES VARYING VIEW WAITFOR WHEN WHERE WHILE WITH WITHIN
70
+ WRITETEXT
71
+ '''.split())
72
+
73
+
74
+ def quote_name(part: str) -> str:
75
+ """Corchetes solo si el nombre no es un identificador simple o es palabra reservada."""
76
+ if re.fullmatch(r'[A-Za-z_][A-Za-z0-9_@$#]*', part) and part.upper() not in RESERVED_KEYWORDS:
77
+ return part
78
+ return f"[{part.replace(']', ']]')}]"
79
+
80
+
81
+ def split_batches(sql: str) -> list[tuple[int, int]]:
82
+ """
83
+ (inicio, fin) de cada lote del script, sin los GO. Se usan los tokens (no
84
+ el texto) para no cortar en un GO dentro de un string o un comentario.
85
+ """
86
+ tokens = SqlServer().tokenize(sql)
87
+ batches, start, i = [], 0, 0
88
+ while i < len(tokens):
89
+ length = BaseParser._batch_separator_length(tokens, i)
90
+ if length:
91
+ batches.append((start, tokens[i].start))
92
+ start = tokens[i + length - 1].end + 1
93
+ i += length
94
+ else:
95
+ i += 1
96
+ batches.append((start, len(sql)))
97
+ return batches
98
+
99
+
100
+ def find_object_header(sql: str) -> ObjectHeader | None:
101
+ for start, end in split_batches(sql):
102
+ header = _find_in_batch(sql, start, end)
103
+ if header:
104
+ return header
105
+ return None
106
+
107
+
108
+ def _find_in_batch(sql: str, batch_start: int, batch_end: int) -> ObjectHeader | None:
109
+ tokens = SqlServer().tokenize(sql[batch_start:batch_end])
110
+
111
+ for i, token in enumerate(tokens):
112
+ is_create = token.token_type == TokenType.CREATE
113
+ is_alter = (
114
+ token.token_type == TokenType.ALTER
115
+ and not (i > 0 and tokens[i - 1].token_type == TokenType.OR)
116
+ )
117
+ if not (is_create or is_alter):
118
+ continue
119
+
120
+ j = i + 1
121
+ # CREATE OR ALTER
122
+ if is_create and j + 1 < len(tokens) and tokens[j].token_type == TokenType.OR:
123
+ j += 2
124
+ if j >= len(tokens) or tokens[j].token_type not in OBJECT_TOKENS:
125
+ continue
126
+
127
+ start = batch_start + token.start
128
+ statements = [s for s in sqlglot.parse(sql[start:batch_end], read=SqlServer) if s]
129
+ statement = statements[0]
130
+ if not isinstance(statement, exp.Create):
131
+ return None
132
+
133
+ table = statement.this if isinstance(statement.this, exp.Table) else statement.this.this
134
+ return ObjectHeader(
135
+ type_code=_type_code(statement),
136
+ name=table.name,
137
+ schema=table.db or None,
138
+ start=start,
139
+ body_start=batch_start + tokens[j].end + 1,
140
+ end=batch_end,
141
+ statement=statement,
142
+ statements=tuple(statements),
143
+ )
144
+
145
+ return None
146
+
147
+
148
+ def _type_code(statement: exp.Create) -> str:
149
+ kind = statement.kind
150
+ if kind in ('PROCEDURE', 'PROC'):
151
+ return PROCEDURE
152
+ if kind == 'VIEW':
153
+ return VIEW
154
+ if kind == 'TRIGGER':
155
+ return TRIGGER
156
+
157
+ returns = statement.find(exp.ReturnsProperty)
158
+ if not returns or not returns.args.get('is_table'):
159
+ return SCALAR_FUNCTION
160
+ return TABLE_FUNCTION if returns.args.get('table') else INLINE_FUNCTION
@@ -0,0 +1,72 @@
1
+ from sqlglot import exp
2
+
3
+ from get_objects_lib.dialect.expressions import TriggerSpec
4
+ from get_objects_lib.objects.header import (
5
+ INLINE_FUNCTION, PROCEDURE, SCALAR_FUNCTION, TABLE_FUNCTION, TRIGGER, VIEW, find_object_header, quote_name,
6
+ )
7
+
8
+ # CREATE minimo de cada tipo. Debe ser del mismo tipo que el objeto real:
9
+ # ALTER FUNCTION no puede cambiar una funcion escalar a una de tabla, ni una
10
+ # de tabla en linea a una de varias sentencias.
11
+ STUBS = {
12
+ PROCEDURE: 'CREATE PROCEDURE {name} AS RETURN 0;',
13
+ VIEW: 'CREATE VIEW {name} AS SELECT 1 AS Stub;',
14
+ SCALAR_FUNCTION: 'CREATE FUNCTION {name}() RETURNS INT AS BEGIN RETURN NULL END;',
15
+ INLINE_FUNCTION: 'CREATE FUNCTION {name}() RETURNS TABLE AS RETURN SELECT 1 AS Stub;',
16
+ TABLE_FUNCTION: 'CREATE FUNCTION {name}() RETURNS @Stub TABLE (Stub INT) AS BEGIN RETURN END;',
17
+ # Misma tabla, momento y eventos: un trigger AFTER no se puede crear sobre
18
+ # una vista ni un INSTEAD OF repetido sobre la misma accion
19
+ TRIGGER: 'CREATE TRIGGER {name} ON {table} {timing} {events} AS RETURN;',
20
+ }
21
+
22
+
23
+ def standardize_object(sql: str, ansi_nulls: bool = False, quoted_identifier: bool = False) -> str:
24
+ """
25
+ Convierte el CREATE / ALTER / CREATE OR ALTER de un procedimiento, funcion
26
+ o vista a la forma estandar: crea un objeto minimo si no existe y despues
27
+ aplica el ALTER con la definicion real.
28
+
29
+ El cuerpo se copia tal cual del texto original (sin regenerarlo); lo que
30
+ haya antes del CREATE/ALTER (comentarios, SET, GO) se descarta.
31
+ """
32
+ header = find_object_header(sql)
33
+ if header is None:
34
+ raise ValueError('No se encontro un CREATE/ALTER de PROCEDURE, FUNCTION, VIEW o TRIGGER')
35
+
36
+ name = header.full_name
37
+ stub = STUBS[header.type_code].format(name=name, **_trigger_parts(header))
38
+ body = sql[header.body_start:header.end].rstrip()
39
+ # Mismo fin de linea que el script original (CRLF en archivos de SSMS)
40
+ newline = '\r\n' if '\r\n' in sql else '\n'
41
+
42
+ return newline.join([
43
+ f'SET ANSI_NULLS {"ON" if ansi_nulls else "OFF"};',
44
+ 'GO',
45
+ f'SET QUOTED_IDENTIFIER {"ON" if quoted_identifier else "OFF"};',
46
+ 'GO',
47
+ f"IF OBJECT_ID(N'{_escape(name)}', N'{header.type_code}') IS NULL",
48
+ f" EXEC (N'{_escape(stub)}');",
49
+ 'GO',
50
+ f'ALTER {header.keyword}{body}',
51
+ 'GO',
52
+ '',
53
+ ])
54
+
55
+
56
+ def _trigger_parts(header) -> dict[str, str]:
57
+ if header.type_code != TRIGGER:
58
+ return {}
59
+ spec = header.statement.find(TriggerSpec)
60
+ table = spec.args['table']
61
+ if not isinstance(table, exp.Table):
62
+ # ON DATABASE / ON ALL SERVER: no estan en sys.objects (OBJECT_ID)
63
+ raise ValueError('Los triggers DDL (ON DATABASE / ALL SERVER) no estan soportados')
64
+ return {
65
+ 'table': '.'.join(quote_name(p) for p in (table.catalog, table.db, table.name) if p),
66
+ 'timing': spec.args['timing'],
67
+ 'events': ', '.join(e.name for e in spec.args['events']),
68
+ }
69
+
70
+
71
+ def _escape(text: str) -> str:
72
+ return text.replace("'", "''")
@@ -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,18 @@
1
+ get_objects_lib/__init__.py,sha256=KjA6O95xtpO2bxmip7zmQq1WemcMBk4sEEvlA1qVixY,407
2
+ get_objects_lib/dialect/__init__.py,sha256=icwyd8tPZh_aapJoVyKcFAriIU624Bvx1BQHOdIApLE,82
3
+ get_objects_lib/dialect/dialect.py,sha256=c6a3ABNzWImM6emwhg_QK4lek5eKPQdd7GSTes6OZhg,375
4
+ get_objects_lib/dialect/expressions.py,sha256=w1A_ygewfG8mW-tae8CJYyvdp8sNzcviPJs_ClXdXKM,3645
5
+ get_objects_lib/dialect/generator.py,sha256=ZecR-6REKWxmuItDNFSixX-2lW02NDNpK10buz5UVNc,11897
6
+ get_objects_lib/dialect/parser/__init__.py,sha256=DNYPQkU45SXYSqf_-3MqNiRh17mlSmqe0prkdRh2Jtk,104
7
+ get_objects_lib/dialect/parser/base.py,sha256=iGVJN-hWXecxjX9bSaBrwBJvftcg_tr5I0jRjK-7eF0,11535
8
+ get_objects_lib/dialect/parser/non_semicolon.py,sha256=jvEkuNbyyk6WBjaKesXxNbVHh_vgYwhmvC4Xei9zYQ0,7677
9
+ get_objects_lib/dialect/parser/sql_server.py,sha256=oUr1mEE75NgRfPNKAXxf_1lePF0f133s7nq_o_DE1sA,23863
10
+ get_objects_lib/dialect/text_utils.py,sha256=bzwC9fctYgE0NGOAgJ5INjjCNzizfDejlAiQ0yG3YlM,624
11
+ get_objects_lib/dialect/tokenizer.py,sha256=xNkNRSoKJQ4v1HugzoUEIvhvCb323wiaeSsP1ZjCO-4,2265
12
+ get_objects_lib/objects/__init__.py,sha256=WPasqsK1SbjGTGBmveqj2FaJjoa5NW99CF0WzMzPY4g,223
13
+ get_objects_lib/objects/dependencies.py,sha256=B4EY-G9KMTgLS6LmN7kAht0DmCKkuHBsXbh5QaxJ09c,6363
14
+ get_objects_lib/objects/header.py,sha256=QALNGQrwDE82VAkPvgR1WWvGACtX2KJBv5rqEQFSVF4,6076
15
+ get_objects_lib/objects/standardize.py,sha256=cMan6pcTVunyvkC7PP3dcfD5jnQp8SHQcGmU0Imyit8,3112
16
+ get_objects_lib-0.1.0.dist-info/METADATA,sha256=bCwMFnQcJ1pFY9IN9P3DWEg9e5pnyPKIl5pDZS_95X4,10320
17
+ get_objects_lib-0.1.0.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
18
+ get_objects_lib-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.1.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any