Metadata-Version: 2.3
Name: get-objects-lib
Version: 1.0.2
Summary: Extrae objetos referenciados en scripts T-SQL
Keywords: sql,tsql,sqlglot
Author: Ivan-Rosales
Author-email: 61328655+Ivan-Rosales@users.noreply.github.com
Requires-Python: >=3.12,<4.0
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Dist: sqlglot (>=30.18.0,<31.0.0)
Project-URL: Repository, https://gitlabv2.intelisis-solutions.com:8008/TICS/Interfaces/utilidades/get_objects_lib
Description-Content-Type: text/markdown

# get-objects-lib

Parser y generador de **T-SQL (SQL Server)** construido sobre [sqlglot](https://github.com/tobymao/sqlglot),
pensado para scripts reales: sin `;` entre sentencias, con `GO`, bloques `BEGIN ... END`,
procedimientos, funciones, vistas y triggers. Encima del dialecto hay un paquete (`objects`) para
analizar objetos:

- **`get_dependencies`**: qué procedimientos, funciones y tablas/vistas usa un query u objeto.
- **`standardize_object`**: convierte un `CREATE` / `ALTER` / `CREATE OR ALTER` a una forma estándar
  que se puede ejecutar siempre (crea un objeto mínimo si no existe y luego aplica el `ALTER`).

## Instalación

Requiere Python 3.12 o superior y [Poetry](https://python-poetry.org/).

```bash
poetry install
```

Los ejemplos y las pruebas se corren desde la raíz del proyecto.

## Uso

### Dialecto: parsear y generar T-SQL

```python
import sqlglot
from get_objects_lib import SqlServer

sql = """
IF @Modo = 1
    EXEC dbo.spProcesar @ID
ELSE
BEGIN
    UPDATE Venta SET Estatus = 'X' WHERE ID = @ID
    PRINT 'actualizado'
END
"""

for statement in sqlglot.parse(sql, read=SqlServer):
    print(repr(statement))                 # árbol de expresiones
    print(statement.sql(dialect=SqlServer))  # de vuelta a T-SQL
```

`sqlglot.parse` devuelve una sentencia por elemento. Un `GO` aparece como su propia sentencia
(`BatchSeparator`).

### Dependencias de un objeto o query

```python
from get_objects_lib import get_dependencies

for ref in get_dependencies(sql):
    print(ref.kind, ref)   # p.ej. "procedure dbo.spProcesar", "table_or_view Venta"
```

Cada resultado es un `ObjectRef(kind, name, schema, catalog, server)`:

| `kind` | Qué detecta |
|---|---|
| `procedure` | `EXEC proc`, `EXEC @r = proc`, `INSERT ... EXEC proc`, `sp_executesql` |
| `function` | `dbo.fn(...)` en cualquier expresión, funciones de tabla en `FROM` y `CROSS/OUTER APPLY` |
| `table_or_view` | tablas y vistas (sin catálogo no se pueden distinguir), incluida la tabla de un trigger |

- Salen en orden de aparición y sin repetidos. Sin esquema se asume `dbo`, así que `SendHttp` y
  `dbo.SendHttp` cuentan una sola vez.
- Se ignoran: tablas temporales (`#t`, `##t`), variables de tabla (`@t`), CTEs, alias,
  `inserted`/`deleted` de los triggers, métodos XML (`T.c.value(...)`), funciones de tabla del
  sistema (`STRING_SPLIT`, `OPENJSON`, ...) y el propio objeto que se está definiendo.
- Si el script define un objeto, se analiza todo su lote (hasta el `GO`), como hace SQL Server.
- Soporta nombres de hasta cuatro partes (`servidor.base.esquema.objeto`).

### Forma estándar de un objeto

```python
from get_objects_lib import standardize_object

print(standardize_object(sql))                           # SET ... OFF por defecto
print(standardize_object(sql, ansi_nulls=True, quoted_identifier=True))
```

Para un script como:

```sql
SET ANSI_NULLS ON;
GO
-- Comentario de encabezado
CREATE PROCEDURE [dbo].[spEjemplo] @ID INT
AS
BEGIN
    SELECT * FROM Venta WHERE ID = @ID
END
GO
```

genera:

```sql
SET ANSI_NULLS OFF;
GO
SET QUOTED_IDENTIFIER OFF;
GO
IF OBJECT_ID(N'dbo.spEjemplo', N'P') IS NULL
    EXEC (N'CREATE PROCEDURE dbo.spEjemplo AS RETURN 0;');
GO
ALTER PROCEDURE [dbo].[spEjemplo] @ID INT
AS
BEGIN
    SELECT * FROM Venta WHERE ID = @ID
END
GO
```

- Acepta `CREATE`, `ALTER` y `CREATE OR ALTER` de procedimientos, funciones, vistas y triggers.
- **El cuerpo se copia tal cual** del texto original (comentarios y formato incluidos); lo que va
  antes del `CREATE`/`ALTER` y los lotes posteriores (p.ej. un `GRANT` tras el `GO`) se descartan.
- El `CREATE` mínimo es del mismo tipo que el objeto real, porque `ALTER` no puede cambiarlo:

  | Tipo (`OBJECT_ID`) | Objeto mínimo |
  |---|---|
  | `P` procedimiento | `CREATE PROCEDURE x AS RETURN 0;` |
  | `V` vista | `CREATE VIEW x AS SELECT 1 AS Stub;` |
  | `FN` función escalar | `CREATE FUNCTION x() RETURNS INT AS BEGIN RETURN NULL END;` |
  | `IF` función de tabla en línea | `CREATE FUNCTION x() RETURNS TABLE AS RETURN SELECT 1 AS Stub;` |
  | `TF` función de tabla de varias sentencias | `CREATE FUNCTION x() RETURNS @Stub TABLE (Stub INT) AS BEGIN RETURN END;` |
  | `TR` trigger | `CREATE TRIGGER x ON tabla <momento> <eventos> AS RETURN;` (misma tabla, momento y eventos) |

- El nombre va sin corchetes salvo que los necesite (espacios, palabras reservadas: `dbo.[Order]`).
- Respeta el fin de línea del script (CRLF en archivos de SSMS).
- Lanza `ValueError` si el script no define un objeto, o si es un trigger DDL (`ON DATABASE`).

## Qué cubre el dialecto

Además de lo que ya soporta el T-SQL de sqlglot, el dialecto `SqlServer` maneja:

- **Separación de sentencias sin `;`** y lotes con `GO` / `GO n` (solo en su línea; no dentro de
  strings o comentarios).
- **Control de flujo**: `IF` / `ELSE IF` / `ELSE` con y sin `BEGIN`, condiciones con subconsultas
  (`IF (SELECT COUNT(*) ...) > 0`), `WHILE`, `BREAK`, `CONTINUE`, `RETURN [expr]`, `GOTO` y
  etiquetas, bloques `BEGIN ... END` sueltos, `BEGIN TRY ... END CATCH`, `WAITFOR`.
- **Objetos**: `CREATE` / `ALTER` / `CREATE OR ALTER` de procedimientos (con o sin paréntesis en
  los parámetros, `OUTPUT`/`OUT`), funciones escalares y de tabla, vistas y triggers; opciones
  `WITH RECOMPILE`, `SCHEMABINDING`, `EXECUTE AS`, `RETURNS NULL ON NULL INPUT`, etc.
- **Sentencias**: `EXEC` con parámetros con nombre, valor de retorno, SQL dinámico (`EXEC (...)`,
  `EXEC (...) AT servidor`), `INSERT ... EXEC`, `INSERT ... SELECT`, `DEFAULT VALUES`,
  `DELETE/UPDATE TOP (n)`, `MERGE`, CTEs, cursores (`DECLARE ... CURSOR`, `OPEN`, `FETCH`, `CLOSE`,
  `DEALLOCATE`), transacciones (`BEGIN TRAN`, `COMMIT`, `ROLLBACK`, `SAVE TRAN`), `PRINT`,
  `RAISERROR`, `THROW`, asignaciones compuestas (`+=`, `-=`, ...), `SET TRANSACTION ISOLATION LEVEL`.
- **Tablas**: pistas con y sin `WITH` (`FROM Venta (NOLOCK)`), índices en línea en `CREATE TABLE`
  y `DECLARE @t TABLE`.
- **Comandos no modelados** (`DBCC`, `BULK INSERT`, `UPDATE STATISTICS`, `ENABLE/DISABLE TRIGGER`,
  `EXECUTE AS`, `REVERT`, `CHECKPOINT`, ...) se conservan como `Command` de su sentencia.
- **Generación fiel a T-SQL**: no reemplaza funciones (`SYSDATETIME`, `SUSER_SNAME`, `DATENAME`,
  `DATEDIFF`, ...), conserva el nombre de las funciones de usuario y escribe `INT`.

### Expresiones propias

Definidas en [`get_objects_lib/dialect/expressions.py`](get_objects_lib/dialect/expressions.py):
`AlterObject`, `BatchSeparator`, `BeginEnd`, `CompoundAssignment`, `CursorStatement`, `DeclareCursor`,
`ExecParameter`, `ExecuteAt`, `FetchCursor`, `Goto`, `InlineIndex`, `Label`, `LoopControl`, `Print`,
`RaiseError`, `ReturnStatement`, `SaveTransaction`, `Throw`, `Top`, `TriggerSpec`, `TryCatch`, `WaitFor`.

## Estructura

```
get_objects_lib/                 paquete instalable; expone SqlServer, get_dependencies,
│                                standardize_object, ObjectRef, ObjectHeader, find_object_header
├── dialect/
│   ├── dialect.py               dialecto SqlServer (une tokenizer, parser y generador)
│   ├── tokenizer.py             GO/PRINT/FETCH sin tragarse el script, +=, nombres de variables
│   ├── expressions.py           expresiones propias de T-SQL
│   ├── generator.py             generacion de T-SQL
│   ├── text_utils.py
│   └── parser/
│       ├── non_semicolon.py     separa el script en sentencias (chunks) sin depender de ;
│       ├── base.py              reglas de inicio de sentencia y de bloques
│       └── sql_server.py        parseo de cada construccion de T-SQL
└── objects/
    ├── header.py                encabezado del objeto (CREATE/ALTER, tipo, nombre, lote)
    ├── dependencies.py          get_dependencies
    └── standardize.py           standardize_object
tests/                           pruebas; ver tests/README.md
├── golden.py                    utilidades y generacion de resultados esperados
├── test_*.py
└── fixtures/
    ├── dialect/                 scripts del parser/generador + expected/
    └── objects/                 scripts de objetos + expected/
```

## Pruebas

```bash
poetry run python -m unittest
```

Son pruebas *golden*: cada script de `tests/fixtures/` se compara con sus resultados esperados en
`expected/` (árbol, SQL generado, dependencias y forma estándar), y además todos los scripts deben
cumplir propiedades generales (el SQL generado es estable, el árbol es el mismo con CRLF, unidos con
`GO` conservan sus sentencias). Los casos se descubren solos.

Para agregar un caso: crea el script en `tests/fixtures/dialect/` o `tests/fixtures/objects/`, genera
sus resultados esperados con `poetry run python -m tests.golden <script>.sql`, **revísalos a mano** y
agrégalo al catálogo de [`tests/README.md`](tests/README.md), que describe qué cubre cada caso.

## Limitaciones y decisiones de diseño

- **Cuerpo de un procedimiento sin `BEGIN`**: en SQL Server llega hasta el `GO`; el parser toma una
  sola sentencia (o hasta el `END` si tiene `BEGIN`). `get_dependencies` sí analiza todo el lote.
- **Un objeto por script**: si un script define varios, `standardize_object` y `get_dependencies`
  toman el primero.
- **SQL dinámico**: lo que se ejecuta con `EXEC (@sql)` o `sp_executesql` no se analiza.
- **Comandos no modelados** (`Command`): su contenido no se analiza para dependencias (p.ej. la
  tabla de un `BULK INSERT`).
- **Normalizaciones del generador** (equivalentes en T-SQL): agrega `AS` en los alias, escribe
  `WITH (NOLOCK)` aunque el original no tenga `WITH`, `OUT` como `OUTPUT`, `IS NOT NULL` como
  `NOT ... IS NULL`, y agrega alias a las columnas de tablas derivadas y CTEs (`SELECT ID AS ID`).

