mtpk-postgres 0.1.2__tar.gz → 0.1.4__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.2
3
+ Version: 0.1.4
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
@@ -54,6 +54,26 @@ pip install -e .
54
54
 
55
55
  ## Historial
56
56
 
57
+ - **0.1.4**: corrige la causa de fondo del bug de la 0.1.3, que solo tapaba el síntoma en la columna generada
58
+ en sí. Había dos problemas relacionados: (1) `comparar_generar_alter` comparaba el tipo de columna por su
59
+ nombre "en crudo" (`col.tipo`), así que un modelo que usa alias como `INT`, `TINYINT` o `DATETIME` (en vez de
60
+ `INTEGER`/`SMALLINT`/`TIMESTAMP`, que es como se reconstruyen siempre desde Postgres) nunca coincidía con la
61
+ realidad y proponía un `ALTER COLUMN ... TYPE` en cada ejecución aunque el tipo no hubiera cambiado — inofensivo
62
+ la mayoría de las veces (Postgres permite "cambiar" una columna a su mismo tipo), pero rechazado de plano si otra
63
+ columna `GENERATED` depende de la columna "alterada"; ahora se compara por el tipo canónico
64
+ (`Columna._tipo_base()`). (2) `ManagerDB.aplicar_cambios()` — la ruta que se usa en la primera sincronización de
65
+ una base de datos nueva — creaba todas las tablas del modelo y a continuación las volvía a comparar todas,
66
+ incluidas las que acababa de crear en la misma llamada; una tabla recién creada coincide por definición con el
67
+ modelo que la creó, así que ahora se excluye de esa comparación (que solo tiene sentido sobre tablas
68
+ preexistentes). Esta segunda corrección evita esta clase entera de falsos positivos, no solo el caso ya visto.
69
+ - **0.1.3**: corrige un bug bloqueante en `comparar_generar_alter` — al crear una tabla nueva con una columna
70
+ `GENERATED ALWAYS AS (...) STORED` (p.ej. una columna de búsqueda `tsvector` calculada a partir de otra), la
71
+ siguiente pasada de comparación la trataba como una columna normal (la introspección no reconstruye la expresión
72
+ de generación) y proponía un `ALTER COLUMN` sobre ella justo después de crearla — algo que Postgres puede
73
+ rechazar cuando otras columnas dependen de la generada. Como `aplicar_cambios()` ejecuta todo en una única
74
+ transacción, ese fallo deshacía también la creación de las demás tablas. Ahora las columnas `generado` nunca se
75
+ comparan ni se alteran una vez existen (para cambiar su expresión hay que borrarlas y recrearlas a mano); de paso,
76
+ `Columna.to_sql()` ya respeta `not_null=True` en columnas generadas (antes se ignoraba).
57
77
  - **0.1.2**: soporte nativo para columnas `VECTOR(n)` (pgvector) e índices con método (`USING gin/hnsw/...`,
58
78
  `opclass`, `WITH (...)`) en `Index`. La introspección (`obtener_columnas_postgres`) ahora reconoce las columnas
59
79
  `vector` por su `udt_name` y reconstruye su dimensión leyendo el catálogo (`pg_attribute`/`format_type`); sin esto,
@@ -34,6 +34,26 @@ pip install -e .
34
34
 
35
35
  ## Historial
36
36
 
37
+ - **0.1.4**: corrige la causa de fondo del bug de la 0.1.3, que solo tapaba el síntoma en la columna generada
38
+ en sí. Había dos problemas relacionados: (1) `comparar_generar_alter` comparaba el tipo de columna por su
39
+ nombre "en crudo" (`col.tipo`), así que un modelo que usa alias como `INT`, `TINYINT` o `DATETIME` (en vez de
40
+ `INTEGER`/`SMALLINT`/`TIMESTAMP`, que es como se reconstruyen siempre desde Postgres) nunca coincidía con la
41
+ realidad y proponía un `ALTER COLUMN ... TYPE` en cada ejecución aunque el tipo no hubiera cambiado — inofensivo
42
+ la mayoría de las veces (Postgres permite "cambiar" una columna a su mismo tipo), pero rechazado de plano si otra
43
+ columna `GENERATED` depende de la columna "alterada"; ahora se compara por el tipo canónico
44
+ (`Columna._tipo_base()`). (2) `ManagerDB.aplicar_cambios()` — la ruta que se usa en la primera sincronización de
45
+ una base de datos nueva — creaba todas las tablas del modelo y a continuación las volvía a comparar todas,
46
+ incluidas las que acababa de crear en la misma llamada; una tabla recién creada coincide por definición con el
47
+ modelo que la creó, así que ahora se excluye de esa comparación (que solo tiene sentido sobre tablas
48
+ preexistentes). Esta segunda corrección evita esta clase entera de falsos positivos, no solo el caso ya visto.
49
+ - **0.1.3**: corrige un bug bloqueante en `comparar_generar_alter` — al crear una tabla nueva con una columna
50
+ `GENERATED ALWAYS AS (...) STORED` (p.ej. una columna de búsqueda `tsvector` calculada a partir de otra), la
51
+ siguiente pasada de comparación la trataba como una columna normal (la introspección no reconstruye la expresión
52
+ de generación) y proponía un `ALTER COLUMN` sobre ella justo después de crearla — algo que Postgres puede
53
+ rechazar cuando otras columnas dependen de la generada. Como `aplicar_cambios()` ejecuta todo en una única
54
+ transacción, ese fallo deshacía también la creación de las demás tablas. Ahora las columnas `generado` nunca se
55
+ comparan ni se alteran una vez existen (para cambiar su expresión hay que borrarlas y recrearlas a mano); de paso,
56
+ `Columna.to_sql()` ya respeta `not_null=True` en columnas generadas (antes se ignoraba).
37
57
  - **0.1.2**: soporte nativo para columnas `VECTOR(n)` (pgvector) e índices con método (`USING gin/hnsw/...`,
38
58
  `opclass`, `WITH (...)`) en `Index`. La introspección (`obtener_columnas_postgres`) ahora reconoce las columnas
39
59
  `vector` por su `udt_name` y reconstruye su dimensión leyendo el catálogo (`pg_attribute`/`format_type`); sin esto,
@@ -1,4 +1,4 @@
1
- __version__ = "0.1.2"
1
+ __version__ = "0.1.4"
2
2
  __author__ = "jjandres"
3
3
 
4
4
  from .interface import *
@@ -129,14 +129,25 @@ class Columna:
129
129
  }
130
130
  _SIN_SOPORTE = {"GEOMETRY", "POINT", "LINESTRING", "POLYGON"}
131
131
 
132
- def _tipo_sql(self) -> str:
133
- """Traduce self.tipo (+ longitud/precision/escala) al tipo SQL de Postgres."""
132
+ def _tipo_base(self) -> str:
133
+ """
134
+ Nombre canónico del tipo en Postgres (sin longitud/precisión), p.ej. "INT" y "INTEGER"
135
+ devuelven ambos "INTEGER". Se usa tanto para generar SQL como para comparar el tipo del
136
+ modelo contra el tipo reconstruido desde la base de datos (obtener_columnas_postgres ya
137
+ siempre devuelve el nombre canónico) — comparar por el nombre "en crudo" haría que un
138
+ modelo que usa alias tipo "INT"/"TINYINT"/"DATETIME" nunca coincidiera con la realidad y
139
+ disparara un ALTER COLUMN de tipo en cada ejecución, aunque el tipo real no haya cambiado.
140
+ """
134
141
  tipo_upper = self.tipo.upper()
135
142
  if tipo_upper in self._SIN_SOPORTE:
136
143
  raise NotImplementedError(
137
144
  f"El tipo '{tipo_upper}' requiere la extensión PostGIS y no está soportado."
138
145
  )
139
- base = self._MAPA_TIPOS.get(tipo_upper, tipo_upper)
146
+ return self._MAPA_TIPOS.get(tipo_upper, tipo_upper)
147
+
148
+ def _tipo_sql(self) -> str:
149
+ """Traduce self.tipo (+ longitud/precision/escala) al tipo SQL de Postgres."""
150
+ base = self._tipo_base()
140
151
 
141
152
  if base == "VECTOR":
142
153
  if self.longitud is None:
@@ -166,6 +177,8 @@ class Columna:
166
177
 
167
178
  if self.generado:
168
179
  partes.append(f"{tipo_sql} GENERATED ALWAYS AS ({self.generado}) STORED")
180
+ if self.not_null:
181
+ partes.append("NOT NULL")
169
182
  else:
170
183
  partes.append(tipo_sql)
171
184
 
@@ -748,8 +761,14 @@ class Tabla:
748
761
  return (val_real, val_def) in equivalentes or (val_def, val_real) in equivalentes or val_real == val_def
749
762
 
750
763
  def tipos_iguales(col_real, col_def):
764
+ # Comparar por el tipo CANÓNICO (col._tipo_base()), no por el texto crudo de col.tipo:
765
+ # el modelo puede usar alias como "INT"/"TINYINT"/"DATETIME" que Postgres no distingue
766
+ # de "INTEGER"/"SMALLINT"/"TIMESTAMP" — comparando el texto crudo, esas columnas nunca
767
+ # coincidirían con la realidad y se propondría un ALTER COLUMN ... TYPE en cada
768
+ # ejecución aunque el tipo real no cambie (inofensivo casi siempre, pero Postgres lo
769
+ # rechaza si otra columna GENERATED depende de la columna "alterada").
751
770
  return (
752
- norm(col_real.tipo) == norm(col_def.tipo) and
771
+ col_real._tipo_base() == col_def._tipo_base() and
753
772
  col_real.longitud == col_def.longitud and
754
773
  col_real.precision == col_def.precision and
755
774
  (col_real.escala or None) == (col_def.escala or None)
@@ -771,6 +790,16 @@ class Tabla:
771
790
  col_real = reales.get(nombre)
772
791
  if not col_real:
773
792
  col_alteraciones.append(f"ADD COLUMN {col_def.to_sql()}")
793
+ elif col_def.generado:
794
+ # Las columnas GENERATED ALWAYS AS (...) STORED no se comparan ni se alteran
795
+ # automáticamente una vez existen: la introspección no reconstruye la expresión
796
+ # de generación (obtener_columnas_postgres no la lee), así que cualquier columna
797
+ # "normal" reconstruida a partir de una generada se compararía con datos
798
+ # incompletos (p.ej. sin el NOT NULL que sí lleva la generada) y dispararía un
799
+ # ALTER espurio — que además Postgres puede rechazar de plano si otras columnas
800
+ # dependen de ella. Si la expresión cambia, hay que borrar y recrear la columna
801
+ # a mano.
802
+ continue
774
803
  elif not iguales(col_real, col_def):
775
804
  if not tipos_iguales(col_real, col_def):
776
805
  tipo_sql = col_def._tipo_sql()
@@ -1195,6 +1224,21 @@ class ManagerDB(Database):
1195
1224
  )
1196
1225
  """)
1197
1226
 
1227
+ # Tablas que ya existían ANTES de esta llamada: solo tiene sentido comparar
1228
+ # (y potencialmente ALTERar) esas. Una tabla recién creada en esta misma llamada
1229
+ # coincide por definición con el modelo que la creó — compararla igual puede
1230
+ # disparar un ALTER falso-positivo (p.ej. no detecta que "INT" y "INTEGER" son el
1231
+ # mismo tipo) que además Postgres puede rechazar de plano si hay una columna
1232
+ # GENERATED que depende de la columna "alterada".
1233
+ tablas_preexistentes = set()
1234
+ for nombre_tabla in self.tablas:
1235
+ cursor.execute(
1236
+ "SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = %s",
1237
+ (nombre_tabla,)
1238
+ )
1239
+ if cursor.fetchone():
1240
+ tablas_preexistentes.add(nombre_tabla)
1241
+
1198
1242
  # --- Crear tablas nuevas (+ índices + comentarios) ---
1199
1243
  for nombre_tabla, tabla in self.tablas.items():
1200
1244
  sentencias_creacion = [tabla.to_sql()] + tabla.to_sql_indices() + tabla.to_sql_comentarios()
@@ -1206,8 +1250,10 @@ class ManagerDB(Database):
1206
1250
  if self.logger:
1207
1251
  self.logger.info(f"Tabla \"{nombre_tabla}\": ejecutado CREATE IF NOT EXISTS.")
1208
1252
 
1209
- # --- Aplicar ALTERs ---
1253
+ # --- Aplicar ALTERs (solo en tablas que ya existían antes de esta llamada) ---
1210
1254
  for nombre_tabla, tabla in self.tablas.items():
1255
+ if nombre_tabla not in tablas_preexistentes:
1256
+ continue
1211
1257
  alter_sqls = tabla.comparar_generar_alter(conexion, permitir_drop=permitir_drop)
1212
1258
  for alter_sql in alter_sqls:
1213
1259
  cursor.execute(alter_sql)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mtpk_postgres
3
- Version: 0.1.2
3
+ Version: 0.1.4
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
@@ -54,6 +54,26 @@ pip install -e .
54
54
 
55
55
  ## Historial
56
56
 
57
+ - **0.1.4**: corrige la causa de fondo del bug de la 0.1.3, que solo tapaba el síntoma en la columna generada
58
+ en sí. Había dos problemas relacionados: (1) `comparar_generar_alter` comparaba el tipo de columna por su
59
+ nombre "en crudo" (`col.tipo`), así que un modelo que usa alias como `INT`, `TINYINT` o `DATETIME` (en vez de
60
+ `INTEGER`/`SMALLINT`/`TIMESTAMP`, que es como se reconstruyen siempre desde Postgres) nunca coincidía con la
61
+ realidad y proponía un `ALTER COLUMN ... TYPE` en cada ejecución aunque el tipo no hubiera cambiado — inofensivo
62
+ la mayoría de las veces (Postgres permite "cambiar" una columna a su mismo tipo), pero rechazado de plano si otra
63
+ columna `GENERATED` depende de la columna "alterada"; ahora se compara por el tipo canónico
64
+ (`Columna._tipo_base()`). (2) `ManagerDB.aplicar_cambios()` — la ruta que se usa en la primera sincronización de
65
+ una base de datos nueva — creaba todas las tablas del modelo y a continuación las volvía a comparar todas,
66
+ incluidas las que acababa de crear en la misma llamada; una tabla recién creada coincide por definición con el
67
+ modelo que la creó, así que ahora se excluye de esa comparación (que solo tiene sentido sobre tablas
68
+ preexistentes). Esta segunda corrección evita esta clase entera de falsos positivos, no solo el caso ya visto.
69
+ - **0.1.3**: corrige un bug bloqueante en `comparar_generar_alter` — al crear una tabla nueva con una columna
70
+ `GENERATED ALWAYS AS (...) STORED` (p.ej. una columna de búsqueda `tsvector` calculada a partir de otra), la
71
+ siguiente pasada de comparación la trataba como una columna normal (la introspección no reconstruye la expresión
72
+ de generación) y proponía un `ALTER COLUMN` sobre ella justo después de crearla — algo que Postgres puede
73
+ rechazar cuando otras columnas dependen de la generada. Como `aplicar_cambios()` ejecuta todo en una única
74
+ transacción, ese fallo deshacía también la creación de las demás tablas. Ahora las columnas `generado` nunca se
75
+ comparan ni se alteran una vez existen (para cambiar su expresión hay que borrarlas y recrearlas a mano); de paso,
76
+ `Columna.to_sql()` ya respeta `not_null=True` en columnas generadas (antes se ignoraba).
57
77
  - **0.1.2**: soporte nativo para columnas `VECTOR(n)` (pgvector) e índices con método (`USING gin/hnsw/...`,
58
78
  `opclass`, `WITH (...)`) en `Index`. La introspección (`obtener_columnas_postgres`) ahora reconoce las columnas
59
79
  `vector` por su `udt_name` y reconstruye su dimensión leyendo el catálogo (`pg_attribute`/`format_type`); sin esto,
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mtpk_postgres"
7
- version = "0.1.2"
7
+ version = "0.1.4"
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