mtpk-postgres 0.1.1__tar.gz → 0.1.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mtpk_postgres
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Librería para sincronización estructural de bases de datos Postgres con modelos Python (adaptación de mtpk_mariadb)
5
5
  Author-email: José Jesús Andrés Zambrana <jjandres@multiplika.es>
6
6
  License: MIT
@@ -45,9 +45,20 @@ pip install -e .
45
45
  reportan como `ENUM` (Postgres no permite distinguirlos después del hecho).
46
46
  - A diferencia de MariaDB, el DDL en Postgres es transaccional: los métodos de `ManagerDB` confirman (`COMMIT`)
47
47
  explícitamente tras crear tablas, triggers y procedimientos.
48
+ - Columnas `VECTOR(n)` (extensión [pgvector](https://github.com/pgvector/pgvector), hay que crearla antes con
49
+ `CREATE EXTENSION IF NOT EXISTS vector;`): `Columna(tipo="VECTOR", longitud=n)`. Es una extensión propia de este
50
+ paquete, no existe en `mtpk-mariadb`.
51
+ - Índices con método (`GIN`, `GIST`, `HNSW`/`IVFFlat` de pgvector, etc.): `Index(columnas=[...], metodo="hnsw",
52
+ opclass="vector_cosine_ops", with_opciones="m = 16, ef_construction = 64")`. `metodo` es incompatible con `unico`
53
+ (Postgres no permite índices UNIQUE con esos métodos).
48
54
 
49
55
  ## Historial
50
56
 
57
+ - **0.1.2**: soporte nativo para columnas `VECTOR(n)` (pgvector) e índices con método (`USING gin/hnsw/...`,
58
+ `opclass`, `WITH (...)`) en `Index`. La introspección (`obtener_columnas_postgres`) ahora reconoce las columnas
59
+ `vector` por su `udt_name` y reconstruye su dimensión leyendo el catálogo (`pg_attribute`/`format_type`); sin esto,
60
+ `data_type` llegaba como `'USER-DEFINED'` y `comparar_generar_alter` proponía un `ALTER COLUMN` en cada ejecución
61
+ aunque la columna no hubiera cambiado.
51
62
  - **0.1.1**: corrige un bug bloqueante — `Tabla` no enlazaba `indice.tabla`/`fk.tabla`/`columna.tabla` cuando
52
63
  `columnas`/`indices`/`foreign_keys` se pasaban directamente por el constructor (solo lo hacían `add_index()` y
53
64
  compañía). Como `Index.to_sql()` necesita esa referencia para generar el `CREATE INDEX`, cualquier tabla definida
@@ -25,9 +25,20 @@ pip install -e .
25
25
  reportan como `ENUM` (Postgres no permite distinguirlos después del hecho).
26
26
  - A diferencia de MariaDB, el DDL en Postgres es transaccional: los métodos de `ManagerDB` confirman (`COMMIT`)
27
27
  explícitamente tras crear tablas, triggers y procedimientos.
28
+ - Columnas `VECTOR(n)` (extensión [pgvector](https://github.com/pgvector/pgvector), hay que crearla antes con
29
+ `CREATE EXTENSION IF NOT EXISTS vector;`): `Columna(tipo="VECTOR", longitud=n)`. Es una extensión propia de este
30
+ paquete, no existe en `mtpk-mariadb`.
31
+ - Índices con método (`GIN`, `GIST`, `HNSW`/`IVFFlat` de pgvector, etc.): `Index(columnas=[...], metodo="hnsw",
32
+ opclass="vector_cosine_ops", with_opciones="m = 16, ef_construction = 64")`. `metodo` es incompatible con `unico`
33
+ (Postgres no permite índices UNIQUE con esos métodos).
28
34
 
29
35
  ## Historial
30
36
 
37
+ - **0.1.2**: soporte nativo para columnas `VECTOR(n)` (pgvector) e índices con método (`USING gin/hnsw/...`,
38
+ `opclass`, `WITH (...)`) en `Index`. La introspección (`obtener_columnas_postgres`) ahora reconoce las columnas
39
+ `vector` por su `udt_name` y reconstruye su dimensión leyendo el catálogo (`pg_attribute`/`format_type`); sin esto,
40
+ `data_type` llegaba como `'USER-DEFINED'` y `comparar_generar_alter` proponía un `ALTER COLUMN` en cada ejecución
41
+ aunque la columna no hubiera cambiado.
31
42
  - **0.1.1**: corrige un bug bloqueante — `Tabla` no enlazaba `indice.tabla`/`fk.tabla`/`columna.tabla` cuando
32
43
  `columnas`/`indices`/`foreign_keys` se pasaban directamente por el constructor (solo lo hacían `add_index()` y
33
44
  compañía). Como `Index.to_sql()` necesita esa referencia para generar el `CREATE INDEX`, cualquier tabla definida
@@ -1,4 +1,4 @@
1
- __version__ = "0.1.1"
1
+ __version__ = "0.1.2"
2
2
  __author__ = "jjandres"
3
3
 
4
4
  from .interface import *
@@ -62,12 +62,14 @@ class Columna:
62
62
  tipo: Literal[
63
63
  "TINYINT", "SMALLINT", "MEDIUMINT", "INT", "INTEGER", "BIGINT", "DECIMAL", "NUMERIC", "FLOAT", "DOUBLE",
64
64
  "CHAR", "VARCHAR", "TEXT", "TINYTEXT", "MEDIUMTEXT", "LONGTEXT", "BINARY", "VARBINARY", "BLOB", "TINYBLOB", "MEDIUMBLOB", "LONGBLOB",
65
- "DATE", "TIME", "YEAR", "DATETIME", "TIMESTAMP", "ENUM", "SET", "BOOLEAN", "GEOMETRY", "POINT", "LINESTRING", "POLYGON"
65
+ "DATE", "TIME", "YEAR", "DATETIME", "TIMESTAMP", "ENUM", "SET", "BOOLEAN", "GEOMETRY", "POINT", "LINESTRING", "POLYGON",
66
+ "VECTOR"
66
67
  ]
67
- """Tipo de dato del modelo (mismo vocabulario que mtpk-mariadb, traducido a Postgres en to_sql())"""
68
+ """Tipo de dato del modelo (mismo vocabulario que mtpk-mariadb, traducido a Postgres en to_sql());
69
+ VECTOR es una extensión propia de este paquete (no existe en mtpk-mariadb) para columnas `pgvector`."""
68
70
 
69
71
  longitud: Optional[int] = None
70
- """Longitud para tipos como VARCHAR(n), CHAR(n)."""
72
+ """Longitud para tipos como VARCHAR(n), CHAR(n). Para VECTOR, es el número de dimensiones: VECTOR(n)."""
71
73
 
72
74
  precision: Optional[int] = None
73
75
  """Precisión para DECIMAL(p, s), etc."""
@@ -123,6 +125,7 @@ class Columna:
123
125
  "YEAR": "SMALLINT",
124
126
  "BOOLEAN": "BOOLEAN",
125
127
  "ENUM": "TEXT", "SET": "TEXT",
128
+ "VECTOR": "VECTOR",
126
129
  }
127
130
  _SIN_SOPORTE = {"GEOMETRY", "POINT", "LINESTRING", "POLYGON"}
128
131
 
@@ -135,6 +138,13 @@ class Columna:
135
138
  )
136
139
  base = self._MAPA_TIPOS.get(tipo_upper, tipo_upper)
137
140
 
141
+ if base == "VECTOR":
142
+ if self.longitud is None:
143
+ raise ValueError(
144
+ f"La columna '{self.nombre}' es VECTOR pero no tiene 'longitud' (número de dimensiones)."
145
+ )
146
+ # Requiere la extensión pgvector: CREATE EXTENSION IF NOT EXISTS vector;
147
+ return f"{base}({self.longitud})"
138
148
  if base in {"VARCHAR", "CHAR"}:
139
149
  return f"{base}({self.longitud})"
140
150
  if base in {"DECIMAL", "NUMERIC"}:
@@ -230,12 +240,20 @@ class Index:
230
240
  - Atributos:
231
241
  - `columnas` (List[str]): Lista de nombres de columnas incluidas en el índice. Pueden ser una o varias (índice compuesto).
232
242
  - `nombre` (str, opcional): Nombre personalizado del índice. Si no se indica, se generará automáticamente combinando los nombres de las columnas.
233
- - `unico` (bool): Indica si el índice es único (`UNIQUE`). Por defecto es `False`.
243
+ - `unico` (bool): Indica si el índice es único (`UNIQUE`). Por defecto es `False`. Incompatible con `metodo`.
244
+ - `metodo` (str, opcional): Método de acceso del índice (`USING <metodo>`), p.ej. `"gin"`, `"gist"` o `"hnsw"`/`"ivfflat"`
245
+ (estos dos últimos requieren la extensión `pgvector`). Si no se indica, se usa el btree normal de Postgres.
246
+ - `opclass` (str, opcional): Clase de operador a aplicar a cada columna del índice, p.ej. `"vector_cosine_ops"`
247
+ para un HNSW/IVFFlat sobre una columna `VECTOR`, o `"gin_trgm_ops"` para búsqueda por trigramas.
248
+ - `with_opciones` (str, opcional): Contenido crudo de la cláusula `WITH (...)`, p.ej. `"m = 16, ef_construction = 64"` (HNSW).
234
249
  - `tabla` (Tabla, opcional): Referencia a la tabla que contiene este índice. Se asigna automáticamente al añadir el índice a la tabla.
235
250
  """
236
251
  columnas: List[str]
237
252
  nombre: Optional[str] = None
238
253
  unico: bool = False
254
+ metodo: Optional[str] = None
255
+ opclass: Optional[str] = None
256
+ with_opciones: Optional[str] = None
239
257
  tabla: Optional["Tabla"] = field(default=None, repr=False)
240
258
 
241
259
  def to_sql(self) -> str:
@@ -243,8 +261,25 @@ class Index:
243
261
  raise ValueError(
244
262
  f"El índice sobre {self.columnas} no está asociado a ninguna tabla; no se puede generar el CREATE INDEX."
245
263
  )
246
- cols = ", ".join(f'"{c}"' for c in self.columnas)
264
+ if self.metodo and self.unico:
265
+ raise ValueError(
266
+ f"El índice sobre {self.columnas} no puede ser UNIQUE y usar 'metodo' ({self.metodo}) a la vez "
267
+ "(GIN/HNSW/IVFFlat no admiten UNIQUE en Postgres)."
268
+ )
269
+
247
270
  nombre = self.nombre or f"{'_'.join(self.columnas)}_{'uk' if self.unico else 'idx'}"
271
+
272
+ if self.metodo:
273
+ cols = ", ".join(
274
+ f'"{c}" {self.opclass}' if self.opclass else f'"{c}"'
275
+ for c in self.columnas
276
+ )
277
+ sql = f'CREATE INDEX IF NOT EXISTS "{nombre}" ON "{self.tabla.nombre}" USING {self.metodo} ({cols})'
278
+ if self.with_opciones:
279
+ sql += f" WITH ({self.with_opciones})"
280
+ return sql
281
+
282
+ cols = ", ".join(f'"{c}"' for c in self.columnas)
248
283
  tipo_sql = "UNIQUE INDEX" if self.unico else "INDEX"
249
284
  return f'CREATE {tipo_sql} IF NOT EXISTS "{nombre}" ON "{self.tabla.nombre}" ({cols})'
250
285
 
@@ -464,7 +499,7 @@ class Tabla:
464
499
  cursor.execute(
465
500
  """
466
501
  SELECT
467
- c.column_name, c.data_type, c.character_maximum_length,
502
+ c.column_name, c.data_type, c.udt_name, c.character_maximum_length,
468
503
  c.numeric_precision, c.numeric_scale, c.is_nullable,
469
504
  c.column_default, c.is_identity,
470
505
  COALESCE(pk.es_pk, false) AS es_pk,
@@ -496,21 +531,33 @@ class Tabla:
496
531
  filas = cursor.fetchall()
497
532
 
498
533
  checks_por_columna = self._obtener_opciones_check(conexion)
534
+ dimensiones_vector = self._obtener_dimensiones_vector(conexion)
499
535
 
500
536
  for fila in filas:
501
- tipo_base = self._PG_A_MODELO.get(fila["data_type"], fila["data_type"].upper())
502
- enum_opciones = None
503
-
504
- if tipo_base == "TEXT" and fila["column_name"] in checks_por_columna:
505
- # Postgres no distingue ENUM de SET tras reconstruir desde el CHECK: ambos
506
- # se guardan como TEXT + CHECK (ver Columna.to_sql), así que se asume ENUM.
507
- tipo_base = "ENUM"
508
- enum_opciones = checks_por_columna[fila["column_name"]]
537
+ longitud = fila["character_maximum_length"]
538
+
539
+ if fila["udt_name"] == "vector":
540
+ # information_schema no expone el nº de dimensiones de un `vector(n)` (tipo de
541
+ # la extensión pgvector): data_type viene como 'USER-DEFINED' y
542
+ # character_maximum_length es NULL. Sin este caso especial, tipo_base quedaría
543
+ # como "USER-DEFINED" y nunca igualaría al "VECTOR" del modelo, provocando un
544
+ # ALTER COLUMN espurio en cada ejecución.
545
+ tipo_base = "VECTOR"
546
+ longitud = dimensiones_vector.get(fila["column_name"])
547
+ enum_opciones = None
548
+ else:
549
+ tipo_base = self._PG_A_MODELO.get(fila["data_type"], fila["data_type"].upper())
550
+ enum_opciones = None
551
+ if tipo_base == "TEXT" and fila["column_name"] in checks_por_columna:
552
+ # Postgres no distingue ENUM de SET tras reconstruir desde el CHECK: ambos
553
+ # se guardan como TEXT + CHECK (ver Columna.to_sql), así que se asume ENUM.
554
+ tipo_base = "ENUM"
555
+ enum_opciones = checks_por_columna[fila["column_name"]]
509
556
 
510
557
  col = Columna(
511
558
  nombre=fila["column_name"],
512
559
  tipo=tipo_base,
513
- longitud=fila["character_maximum_length"],
560
+ longitud=longitud,
514
561
  precision=fila["numeric_precision"],
515
562
  escala=fila["numeric_scale"],
516
563
  not_null=(fila["is_nullable"] == "NO"),
@@ -524,6 +571,33 @@ class Tabla:
524
571
 
525
572
  return columnas
526
573
 
574
+ def _obtener_dimensiones_vector(self, conexion) -> Dict[str, int]:
575
+ """
576
+ Devuelve {columna: dimensiones} para las columnas `vector(n)` (extensión pgvector) de
577
+ esta tabla, leyendo el modificador de tipo real desde los catálogos (atttypmod vía
578
+ format_type), ya que information_schema no lo expone para tipos definidos por extensión.
579
+ """
580
+ resultado: Dict[str, int] = {}
581
+ with conexion.cursor(row_factory=dict_row) as cursor:
582
+ cursor.execute(
583
+ """
584
+ SELECT a.attname AS columna,
585
+ pg_catalog.format_type(a.atttypid, a.atttypmod) AS tipo_completo
586
+ FROM pg_attribute a
587
+ JOIN pg_class t ON t.oid = a.attrelid
588
+ JOIN pg_namespace n ON n.oid = t.relnamespace
589
+ JOIN pg_type ty ON ty.oid = a.atttypid
590
+ WHERE t.relname = %s AND n.nspname = 'public'
591
+ AND ty.typname = 'vector' AND a.attnum > 0 AND NOT a.attisdropped
592
+ """,
593
+ (self.nombre,)
594
+ )
595
+ for fila in cursor.fetchall():
596
+ m = re.match(r"vector\((\d+)\)", fila["tipo_completo"] or "", re.IGNORECASE)
597
+ if m:
598
+ resultado[fila["columna"]] = int(m.group(1))
599
+ return resultado
600
+
527
601
  def _obtener_opciones_check(self, conexion) -> Dict[str, List[str]]:
528
602
  """
529
603
  Reconstruye, de forma best-effort, las opciones de las columnas ENUM/SET (TEXT + CHECK)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mtpk_postgres
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Librería para sincronización estructural de bases de datos Postgres con modelos Python (adaptación de mtpk_mariadb)
5
5
  Author-email: José Jesús Andrés Zambrana <jjandres@multiplika.es>
6
6
  License: MIT
@@ -45,9 +45,20 @@ pip install -e .
45
45
  reportan como `ENUM` (Postgres no permite distinguirlos después del hecho).
46
46
  - A diferencia de MariaDB, el DDL en Postgres es transaccional: los métodos de `ManagerDB` confirman (`COMMIT`)
47
47
  explícitamente tras crear tablas, triggers y procedimientos.
48
+ - Columnas `VECTOR(n)` (extensión [pgvector](https://github.com/pgvector/pgvector), hay que crearla antes con
49
+ `CREATE EXTENSION IF NOT EXISTS vector;`): `Columna(tipo="VECTOR", longitud=n)`. Es una extensión propia de este
50
+ paquete, no existe en `mtpk-mariadb`.
51
+ - Índices con método (`GIN`, `GIST`, `HNSW`/`IVFFlat` de pgvector, etc.): `Index(columnas=[...], metodo="hnsw",
52
+ opclass="vector_cosine_ops", with_opciones="m = 16, ef_construction = 64")`. `metodo` es incompatible con `unico`
53
+ (Postgres no permite índices UNIQUE con esos métodos).
48
54
 
49
55
  ## Historial
50
56
 
57
+ - **0.1.2**: soporte nativo para columnas `VECTOR(n)` (pgvector) e índices con método (`USING gin/hnsw/...`,
58
+ `opclass`, `WITH (...)`) en `Index`. La introspección (`obtener_columnas_postgres`) ahora reconoce las columnas
59
+ `vector` por su `udt_name` y reconstruye su dimensión leyendo el catálogo (`pg_attribute`/`format_type`); sin esto,
60
+ `data_type` llegaba como `'USER-DEFINED'` y `comparar_generar_alter` proponía un `ALTER COLUMN` en cada ejecución
61
+ aunque la columna no hubiera cambiado.
51
62
  - **0.1.1**: corrige un bug bloqueante — `Tabla` no enlazaba `indice.tabla`/`fk.tabla`/`columna.tabla` cuando
52
63
  `columnas`/`indices`/`foreign_keys` se pasaban directamente por el constructor (solo lo hacían `add_index()` y
53
64
  compañía). Como `Index.to_sql()` necesita esa referencia para generar el `CREATE INDEX`, cualquier tabla definida
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mtpk_postgres"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "Librería para sincronización estructural de bases de datos Postgres con modelos Python (adaptación de mtpk_mariadb)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
File without changes
File without changes