Extender tai-sql¶
Todo punto de extensión es una clase abstracta: se implementa el contrato y se registra. Nunca hay que tocar el núcleo con casos especiales.
| ABC | Para qué |
|---|---|
BaseGenerator |
Un generador nuevo: recibe el schema analizado y produce lo que sea |
DatabaseDriver |
Un motor nuevo: todo su SQL dialectal y sus capacidades |
TypeDescriptor |
Un tipo de columna nuevo |
BaseEncoder |
Un proveedor de embeddings nuevo |
Un generador propio¶
El caso habitual: quieres producir algo más a partir del schema —documentación, tipos de TypeScript, un esquema OpenAPI, fixtures—.
from pathlib import Path
from tai_sql.generators import BaseGenerator
class MarkdownGenerator(BaseGenerator):
"""Escribe un fichero Markdown con el diccionario de datos del schema."""
def generate(self) -> str:
lineas = [f'# {self.schema.name}', '']
for tabla in self.schema.tables.values():
lineas.append(f'## `{tabla.tablename}`')
lineas.append(tabla.description)
for columna in tabla.columns.values():
lineas.append(f'- `{columna.name}` ({columna.type}) — {columna.description}')
lineas.append('')
contenido = '\n'.join(lineas)
for directorio in self.config.output_dirs:
(Path(directorio) / f'{self.schema.name}.md').write_text(contenido, encoding='utf-8')
return contenido
Y se declara como cualquier otro:
generate() es el único método abstracto. Los directorios de salida se crean solos.
Qué tienes dentro¶
El schema entra por inyección, no leyendo estado global:
| Atributo | Qué es |
|---|---|
self.schema |
El SchemaModel ya analizado |
self.provider |
La conexión declarada en el datasource() |
self.config.output_dirs |
La lista de directorios de salida |
El SchemaModel es inmutable y sus referencias son por nombre:
self.schema.name # 'public'
self.schema.syntax # 'v1' | 'v2'
self.schema.operative_fields # bool
self.schema.tables # Mapping[str, TableModel] ← por nombre de clase
self.schema.views # Mapping[str, ViewModel]
self.schema.enums # Mapping[str, EnumModel]
self.schema.entity('Usuario') # tabla o vista, por nombre
De una tabla:
tabla.name # 'Usuario' — el nombre de la clase
tabla.tablename # 'usuario' — el nombre físico
tabla.description
tabla.columns # Mapping[str, ColumnModel]
tabla.relations # Mapping[str, RelationModel]
tabla.foreign_keys
tabla.unique_constraints
tabla.indexes
tabla.triggers # Mapping[str, tuple[TriggerModel, ...]] por evento
tabla.calculated_columns
tabla.self_reference
De una columna:
columna.name
columna.type # 'str', 'bigint', 'datetime'…
columna.nullable
columna.constraints.primary_key
columna.constraints.unique
columna.constraints.default
columna.is_foreign_key
columna.description
columna.encrypt
columna.is_calculated
columna.vector # VectorModel | None
Emite desde plantillas, no desde cadenas
Es la convención del framework: el código generado sale de plantillas Jinja2 externas, nunca de strings embebidos en Python. Si tu generador produce código, te ahorrará el mismo dolor.
Si generas código que se despliega, no importes tai_sql en él
Es el principio 3. Lo que el output necesite en runtime se define inline en el output.
Un tipo propio¶
from tai_sql import TypeDescriptor, register_type
class IPAddressType(TypeDescriptor):
name = 'ipaddress'
python_type = str
pydantic_type = 'str'
def to_sqlalchemy(self, driver: str):
...
def filter_fields(self) -> list[str]:
return ['exact', 'in']
register_type(IPAddressType())
filter_fields() decide qué filtros genera el cliente para las columnas de ese tipo:
'exact', 'in', 'range', 'like' o 'none'.
Un encoder propio¶
Recuerda que un encoder es configuración: no debe cargar ninguna librería de ML al importar el schema. Quien codifica de verdad es el cliente generado.
Un driver propio¶
Implementar DatabaseDriver y registrarlo. Es el punto de extensión más grande —tipos, DDL,
quoting, upsert, consultas de catálogo y capacidades—, y la forma de comprobar que está completo
es la suite de tests de drivers, que está parametrizada sobre el registro.
Si vas por ahí, lo razonable es abrir un issue antes: el contrato es amplio y conviene acordar el alcance.