tenant_partition 0.2.1 → 0.3.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e675f290b7cbe1ef13c1d6db7ec36894a92c169c82ae42112a1b0979c220f9f1
4
- data.tar.gz: 4470f638d2f110c48d9f22a1b8a685f2c598dd01292cb867617a45782d7def11
3
+ metadata.gz: 968c88d57e55597859aceca92e0404d0430d8277f8dd84ebb20d25d0fb1b011f
4
+ data.tar.gz: 46fc3262411b92a3083373710b6cffac249201910ff8344a619610ed88b0cfd3
5
5
  SHA512:
6
- metadata.gz: 87115b8fc726b431102c6ea6d53103f71370d643a3fc543504ceb94df1d718eedd4041092a7f2384ecb98e9825447ba88281a32035ee244081b04579bda041c6
7
- data.tar.gz: 3d535affbac9ea2101aaf1d8ecd415f379fb0e8cfa0d1a826b5d02e01567bd1a90f87ae01dd9be53744d8fe975e467a1362ac24f773e417b17d4635ec6b4d0e6
6
+ metadata.gz: 1aa3eb1a9a6ae2596b69458aebf7b8b77c985aa7087cac89ef679adefd59b0636835fba9e8ea9ee544f0d4065a18be6887d00a281236ad8bc50c82b03d58c9a1
7
+ data.tar.gz: e335c13f9df1178a53296599f99593e5fe3489e83403229a46f1e53fa41b572558ea1ca66947602c2e392dcb86188630a2c95d16473a861a1f0e26e5f4ee047a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2026-03-09
4
+ ### Added (Zero-Downtime Migrations)
5
+ - **Migraciones Online:** Nueva suite de herramientas para migrar tablas masivas en producción sin tiempo de inactividad (Zero-Downtime).
6
+ - **Schema Statements:** Se agregaron los helpers `create_partition_sync_trigger`, `remove_partition_sync_trigger` y `swap_partitioned_tables` para manejar el Live Sync mediante Triggers de base de datos y Cutover atómico.
7
+ - **Backfill Engine (`TenantPartition::Migrator`):** Nueva clase para mover millones de registros en segundo plano. Incluye resolución automática de conflictos (`UPSERT`) para convivir con la replicación en vivo, y soporte nativo de paginación por fechas (`created_at`) para tablas que usan UUIDs.
8
+ - **Generador de Rails:** Nuevo comando `rails g tenant_partition:online_migration` que genera automáticamente las migraciones de 5 fases necesarias para migrar una tabla de forma segura.
9
+ - **Rake Tasks:** Nueva tarea `rake tenant_partition:backfill_data` para ejecutar el copiado de datos fácilmente desde la consola o background jobs.
10
+
11
+ ### Changed
12
+ - **Railtie:** Se actualizó la carga de tareas Rake para incluir dinámicamente todos los archivos `.rake` del directorio `tasks/`, permitiendo que coexistan las tareas de mantenimiento y migración.
13
+
3
14
  ## [0.2.1] - 2026-02-13
4
15
  ### Added
5
16
  - **Mejora en Migraciones:** El helper `create_partitioned_table` ahora acepta la opción `id_type: :uuid` o `id_type: :bigint` (por defecto). Esto permite usar particionamiento con IDs enteros seriales estándar o UUIDs según la necesidad del proyecto.
data/README.md CHANGED
@@ -1,24 +1,25 @@
1
- # TenantPartition
1
+ # TenantPartition 🏢
2
2
 
3
- **TenantPartition** es una solución robusta y "Rails-native" para implementar **Particionamiento Declarativo (List Partitioning)** de PostgreSQL en aplicaciones Ruby on Rails.
3
+ **TenantPartition** es un framework de infraestructura "Rails-native" diseñado para implementar **Particionamiento Declarativo (List Partitioning)** de PostgreSQL en aplicaciones Ruby on Rails (7.1+).
4
4
 
5
- A diferencia de otras soluciones que dependen de esquemas (schemas) o hackeos a la conexión de base de datos, `tenant_partition` utiliza características nativas de PostgreSQL para dividir tablas gigantes en tablas físicas más pequeñas por tenant (Cliente, ISP, Organización, etc.), manteniendo la experiencia de desarrollo de ActiveRecord estándar.
5
+ A diferencia de otras soluciones multi-tenant que dependen de múltiples esquemas (schemas) o hackeos a nivel de consultas (row-level filtering), `tenant_partition` utiliza características nativas del motor de PostgreSQL para dividir físicamente tablas gigantes en tablas más pequeñas y ultrarrápidas por tenant (Cliente, ISP, Organización, etc.). Todo esto, manteniendo intacta la experiencia de desarrollo estándar de ActiveRecord.
6
6
 
7
7
  ## 🚀 Características Principales
8
8
 
9
- * **API Simple (Opt-in):** Usa `partition_table` en tus modelos para activar la magia.
10
- * **Migraciones Inteligentes:** Helper `create_partitioned_table` que maneja la complejidad de Postgres automáticamente.
11
- * **Soporte Nativo CPK:** Compatible con **Composite Primary Keys** de Rails 7.1+.
12
- * **Sin Magic Strings:** Usa métodos explícitos (`create_partition`, `drop_partition`).
13
- * **Gestión de Datos Huérfanos:** Herramientas para mover datos de la tabla "Default" a su partición correcta automáticamente.
14
- * **Safety Guards:** Protección contra borrados accidentales en Producción.
15
- * **Tasks de Mantenimiento:** Rake tasks integradas para auditoría y limpieza.
9
+ * **API Simple y Opt-in:** Activa el particionamiento en tus modelos simplemente agregando la macro `partition_table`.
10
+ * **Zero-Downtime Migrations (2 Fases):** Herramientas de nivel empresarial para migrar tablas masivas en producción sin detener el servicio, con barreras anti-errores para despliegues en CI/CD.
11
+ * **Introspección Dinámica (Safe Deploys):** El código de tus modelos detecta automáticamente si la tabla en la base de datos ya fue particionada, permitiendo desplegar tu código *antes* de finalizar las migraciones de infraestructura.
12
+ * **Smart Hashing para UUIDs:** Evasión automática del límite de 63 caracteres de PostgreSQL en el nombrado de tablas al usar UUIDs largos como claves de partición.
13
+ * **Sincronización en Tiempo Real (Live Sync):** Triggers de base de datos automatizados con resolución de conflictos (`UPSERT`) integrada.
14
+ * **Backfill Engine con Auto-Aprovisionamiento JIT:** Motor de copiado en segundo plano que crea automáticamente las particiones hijas al vuelo (Just-In-Time) a medida que descubre nuevos tenants.
15
+ * **Soporte Nativo CPK:** Totalmente compatible con **Composite Primary Keys** de Rails 7.1+.
16
+ * **Gestión de Datos Huérfanos:** Auditoría y auto-reparación de registros que caen en la partición `_default`.
16
17
 
17
18
  ---
18
19
 
19
20
  ## 📦 Instalación
20
21
 
21
- Agrega esto a tu `Gemfile`:
22
+ Agrega la gema a tu `Gemfile`:
22
23
 
23
24
  ```ruby
24
25
  gem 'tenant_partition'
@@ -30,191 +31,218 @@ Y ejecuta:
30
31
  bundle install
31
32
  ```
32
33
 
34
+ **Requisitos mínimos:** Ruby 3.2+, Rails 7.1+ y PostgreSQL 13+ (Optimizado para PG 17). *(Nota: La gema incluye "magia" retrocompatible para operar en Rails 6.0+ bajo su propio riesgo).*
35
+
33
36
  ---
34
37
 
35
38
  ## ⚙️ Configuración Inicial
36
39
 
37
- ### 1. Inicializador
38
- Crea un archivo para definir tu clave de partición global (por ejemplo, `:isp_id`, `:account_id`, `:tenant_id`).
40
+ **1. Configuración Global**
41
+ Crea un archivo de inicialización para definir tu clave de partición base (por ejemplo, `:isp_id`, `:account_id` o `:tenant_id`).
39
42
 
40
43
  ```ruby
41
44
  # config/initializers/tenant_partition.rb
42
-
43
45
  TenantPartition.configure do |config|
44
- # Esta es la columna que actuará como discriminador global
46
+ # Columna que actuará como discriminador principal en toda la base de datos
45
47
  config.partition_key = :isp_id
46
48
  end
47
49
  ```
48
50
 
49
- ### 2. Habilitar en ApplicationRecord
50
- Incluye el concern en tu modelo base. **No te preocupes, esto no particiona nada por defecto**, solo habilita la posibilidad de usar la macro `partition_table` en tus modelos.
51
+ **2. Habilitar el DSL en ActiveRecord**
52
+ Inyecta el comportamiento base en tu aplicación. Esto **no** particiona tus modelos por defecto, solo les da la habilidad de entender la macro de la gema.
51
53
 
52
54
  ```ruby
53
55
  # app/models/application_record.rb
54
56
  class ApplicationRecord < ActiveRecord::Base
55
57
  primary_abstract_class
56
58
 
57
- # Habilita la herramienta (modo inactivo por defecto)
58
59
  include TenantPartition::Concerns::Partitioned
59
60
  end
60
61
  ```
61
62
 
62
63
  ---
63
64
 
64
- ## 🛠 Guía de Implementación
65
+ ## 🛠 Guía 1: Tablas Nuevas (Green Field)
65
66
 
66
- ### Paso 1: Migración de Base de Datos
67
+ Si estás desarrollando un feature desde cero, particionar una tabla nueva es sumamente directo.
67
68
 
68
- Olvídate del SQL manual. Usa el helper `create_partitioned_table` que hace todo el trabajo sucio por ti:
69
- 1. Crea la tabla padre con `PARTITION BY LIST`.
70
- 2. Configura la Primary Key Compuesta `[:id, :partition_key]`.
71
- 3. Crea automáticamente la partición `_default` para capturar datos no asignados.
72
-
73
- #### Opción A: Usando Enteros (BigInt) - Recomendado
69
+ ### 1. La Migración
70
+ Usa el helper `create_partitioned_table`. Este configura automáticamente la Primary Key Compuesta, la partición tipo `LIST` y la tabla `_default`.
74
71
 
75
72
  ```ruby
76
73
  class CreateConversations < ActiveRecord::Migration[7.1]
77
74
  def change
78
- # partition_key: usa el default de la config (:isp_id) si no se especifica.
79
- # id_type: :bigint por defecto.
80
- create_partitioned_table :conversations do |t|
75
+ # Soporta id_type: :bigint (por defecto) o :uuid
76
+ create_partitioned_table :conversations, id_type: :uuid do |t|
81
77
  t.string :subject
82
78
  t.text :body
83
79
  t.timestamps
84
-
85
- # Nota: No definas :id ni :isp_id aquí, el helper lo hace por ti.
80
+
81
+ # 🪄 Nota: NO definas explícitamente el :id ni el :isp_id aquí.
82
+ # La gema lo hace por ti automáticamente.
86
83
  end
87
84
  end
88
85
  end
89
86
  ```
90
87
 
91
- #### Opción B: Usando UUIDs
92
-
88
+ ### 2. El Modelo
93
89
  ```ruby
94
- class CreateConversations < ActiveRecord::Migration[7.1]
95
- def change
96
- enable_extension 'pgcrypto' # Necesario para gen_random_uuid()
97
-
98
- create_partitioned_table :conversations, id_type: :uuid do |t|
99
- t.string :subject
100
- t.timestamps
101
- end
102
- end
90
+ class Conversation < ApplicationRecord
91
+ # Activa la Composite Primary Key y los scopes de enrutamiento
92
+ partition_table
103
93
  end
104
94
  ```
105
95
 
106
- ### Paso 2: Configurar el Modelo
96
+ ---
97
+
98
+ ## 🔥 Guía 2: Migrar una Tabla Existente (Zero-Downtime)
99
+
100
+ Si tienes una tabla legacy con millones de registros y necesitas particionarla en producción sin causar tiempo de inactividad, utiliza nuestra suite de migración en dos fases.
101
+
102
+ *(⚠️ **Requisito:** La tabla original ya debe tener la columna de tu `partition_key` definida).*
103
+
104
+ ### Fase 1: Preparación, Código y Live Sync (Despliegue 1)
105
+ Genera la infraestructura inicial ejecutando:
106
+
107
+ ```bash
108
+ rails g tenant_partition:prepare versions isp_id
109
+ ```
107
110
 
108
- Usa la macro `partition_table` para activar la funcionalidad en el modelo correspondiente.
111
+ Abre la migración generada en `db/migrate/`. Gracias a la **introspección**, la gema clonará la estructura de tu tabla original y configurará los triggers de PostgreSQL (`INSERT/UPDATE/DELETE`) en un solo paso:
109
112
 
110
113
  ```ruby
111
- # app/models/conversation.rb
112
- class Conversation < ApplicationRecord
113
- # ¡Esto es todo!
114
- # Automáticamente configura la Primary Key compuesta [:id, :isp_id]
115
- # y los scopes necesarios.
116
- partition_table
114
+ def up
115
+ create_partitioned_table_from(
116
+ :versions_partitioned, :versions, partition_key: :isp_id, sync_triggers: true
117
+ )
117
118
  end
118
119
  ```
119
120
 
120
- **¿Necesitas una key diferente para un solo modelo?**
121
+ **Activa la gema en tu modelo:** Agrega la macro a tu clase ActiveRecord.
121
122
  ```ruby
122
- class AuditLog < ApplicationRecord
123
- # Este modelo se particiona por año, ignorando la config global
124
- partition_table key: :year
123
+ class Version < ApplicationRecord
124
+ partition_table
125
125
  end
126
126
  ```
127
+ *(🪄 **Safe Deploy:** Gracias a la Introspección Dinámica, la gema sabe que la base de datos aún no ha finalizado la migración. El modelo seguirá comportándose como una tabla normal sin romper tu aplicación).*
127
128
 
128
- ### Paso 3: Crear y Eliminar Tenants
129
+ **👉 Ejecuta `rails db:migrate` y despliega a producción.** A partir de este milisegundo, la base de datos enviará todo dato "vivo" nuevo a tu nueva tabla sombra, y tu código estará listo para el futuro.
129
130
 
130
- Gestiona el ciclo de vida de las particiones utilizando los métodos de clase inyectados.
131
+ ### Fase 2: Backfill Histórico (Fase Manual)
132
+ Con la app corriendo, copia el historial pesado ejecutando esta tarea (idealmente en un entorno de background job o consola de ops):
131
133
 
132
- ```ruby
133
- # Crear partición física para el ISP con ID 100
134
- Conversation.create_partition(100)
135
- # => Crea la tabla "conversations_isp_100"
134
+ Para tablas con **IDs Enteros**:
135
+ ```bash
136
+ rake tenant_partition:backfill_data[PaperTrail::Version,versions_partitioned,id]
137
+ ```
138
+ Para tablas con **UUIDs** (Paginación segura por fecha):
139
+ ```bash
140
+ rake tenant_partition:backfill_data[PaperTrail::Version,versions_partitioned,created_at]
141
+ ```
142
+ *(🪄 **Auto-Aprovisionamiento JIT:** El `Migrator` creará las particiones físicas al vuelo utilizando "Smart Hashing" para evitar los límites de 63 caracteres de PostgreSQL. Además, resolverá conflictos usando `ON CONFLICT DO UPDATE` para no pisar los datos vivos).*
136
143
 
137
- # Verificar si existe
138
- Conversation.partition_table_exists?(100)
139
- # => true
144
+ ### Fase 3: Cutover Atómico (Despliegue 2)
145
+ Una vez que el Backfill termine al 100%, genera la migración de intercambio final:
140
146
 
141
- # Eliminar partición (CUIDADO: Borra datos)
142
- Conversation.drop_partition(100)
143
- # => Elimina "conversations_isp_100"
147
+ ```bash
148
+ rails g tenant_partition:cutover versions isp_id
144
149
  ```
145
150
 
146
- #### Automatización con Callbacks
147
- Es común crear las particiones automáticamente cuando nace un nuevo Tenant.
151
+ **👉 Ejecuta `rails db:migrate` y despliega a producción.** Una transacción atómica cruzará los nombres de las tablas y eliminará los triggers en 1 milisegundo.
152
+ En el instante en que los servidores se reinicien tras el deploy, tu modelo (`Version`) consultará a PostgreSQL, detectará que la tabla ahora sí es particionada, y activará automáticamente sus superpoderes (Composite Primary Keys y Partition Pruning). **¡Cero downtime logrado!**
153
+
154
+ ---
155
+
156
+ ## 🔍 Consultas y Rendimiento (Partition Pruning)
157
+
158
+ Para que PostgreSQL sea extremadamente rápido, debe aprovechar el **Partition Pruning** (poda de particiones), yendo directamente a la tabla hija en lugar de escanear toda la base de datos.
159
+
160
+ La gema inyecta automáticamente el scope `for_partition(valor)`.
148
161
 
162
+ ```ruby
163
+ # ❌ LENTO: Escaneará todas las particiones hijas
164
+ Conversation.where(status: 'active')
165
+
166
+ # ✅ ULTRARRÁPIDO: Va directamente a 'conversations_isp_123'
167
+ Conversation.for_partition(123).where(status: 'active')
168
+ ```
169
+
170
+ ---
171
+
172
+ ## 🏗 Orquestación del Ciclo de Vida de los Tenants
173
+
174
+ Debes crear la infraestructura física (la tabla hija) para cada Tenant a medida que nacen nuevos clientes en tu sistema.
175
+
176
+ **Integración directa en tus Modelos:**
149
177
  ```ruby
150
178
  # app/models/isp.rb
151
179
  class Isp < ApplicationRecord
152
180
  after_create :provision_infrastructure
181
+ after_destroy :destroy_infrastructure
182
+
183
+ private
153
184
 
154
185
  def provision_infrastructure
155
- # Helper global que crea particiones en TODOS los modelos registrados
186
+ # Crea las particiones en TODOS los modelos que tengan `partition_table`
156
187
  TenantPartition.create!(self.id)
157
188
  end
189
+
190
+ def destroy_infrastructure
191
+ TenantPartition.destroy!(self.id)
192
+ end
158
193
  end
159
194
  ```
160
195
 
161
- ---
196
+ **Generador de API:**
197
+ Si prefieres orquestar esto desde un microservicio externo, puedes generar un controlador pre-armado:
198
+ ```bash
199
+ rails g tenant_partition:api_controller System
200
+ # Creará: app/controllers/system/tenant_partitions_controller.rb
201
+ ```
162
202
 
163
- ## 🧹 Mantenimiento y Datos Huérfanos
203
+ ---
164
204
 
165
- Si insertas datos con un `isp_id` para el cual no has creado una partición, Postgres los guardará en la tabla `_default` que creamos en la migración. `TenantPartition` incluye herramientas para detectar y corregir esto.
205
+ ## 🧹 Mantenimiento: Datos Huérfanos
166
206
 
167
- ### Auditoría
168
- Verifica si tienes datos "mal ubicados" en las tablas default:
207
+ Si un registro es insertado antes de que su tenant sea aprovisionado, PostgreSQL lo enviará de forma segura a la tabla `_default`. Puedes auditar y corregir esto con tareas Rake:
169
208
 
209
+ **1. Auditoría (Encontrar datos perdidos):**
170
210
  ```bash
171
- bundle exec rake tenant_partition:audit
211
+ rake tenant_partition:audit
172
212
  # [TenantPartition] [AUDIT] Iniciando auditoría...
173
213
  # [TenantPartition] [ALERTA] Conversation: 450 registros huérfanos encontrados.
174
214
  ```
175
215
 
176
- ### Limpieza (Cleanup)
177
- Crea las particiones faltantes y mueve los datos automáticamente a su hogar correcto:
178
-
179
- ```bash
180
- bundle exec rake tenant_partition:cleanup
181
- # [TenantPartition] [FIX] Conversation: Procesando 2 tenants con datos huérfanos.
182
- # [TenantPartition] [MOVE] -> ID 101: 450 registros recuperados.
183
- ```
184
-
185
- ---
186
-
187
- ## 🛡️ Producción y Seguridad
188
-
189
- La gema incluye un `SafetyGuard` que impide ejecutar comandos destructivos (`drop_partition`, `destroy!`) en entorno de producción para evitar catástrofes.
190
-
191
- Si realmente necesitas borrar un tenant en producción, debes autorizarlo explícitamente:
192
-
216
+ **2. Limpieza (Mover a su lugar correcto):**
217
+ *(Asegúrate de haber aprovisionado el tenant primero)*
193
218
  ```bash
194
- DISABLE_TENANT_PARTITION_GUARD=true bundle exec rake tenant_partition:destroy_tenant[123]
219
+ rake tenant_partition:cleanup
220
+ # [TenantPartition] [MOVE] -> ID 101: 450 registros recuperados hacia su partición.
195
221
  ```
196
222
 
197
223
  ---
198
224
 
199
- ## 📖 Referencia de API
225
+ ## 📖 Referencia Rápida de la API
200
226
 
201
- ### `TenantPartition` (Global)
202
- * `configure { ... }`: Configuración inicial.
203
- * `create!(id)`: Crea particiones para el ID dado en **todos** los modelos registrados.
204
- * `destroy!(id)`: Elimina particiones para el ID dado en **todos** los modelos.
205
- * `exists?(id)`: Devuelve `true` si existe infraestructura para ese ID.
227
+ **Módulo Global `TenantPartition`**
228
+ * `.create!(id)`: Aprovisionamiento de tablas para un tenant en toda la app.
229
+ * `.destroy!(id)`: Eliminación de tablas de un tenant (protegido en Prod).
230
+ * `.exists?(id)`: Verifica infraestructura.
206
231
 
207
- ### Métodos de Instancia (Modelos)
208
- * `partition_table(key: nil)`: Macro de activación.
232
+ **Macros de Modelo**
233
+ * `partition_table(key: nil)`: Activa particionamiento (opcional: sobreescribe clave).
234
+ * `for_partition(value)`: Scope de búsqueda optimizada (con auto-casteo de tipos).
235
+ * `create_partition(value)`, `drop_partition(value)`: Operaciones DDL manuales por modelo.
236
+ * `partitions`: Devuelve nombres de tablas físicas hijas (incluyendo `_default`).
237
+ * `partition_values`: Devuelve los IDs/valores de tenants que ya tienen partición.
209
238
 
210
- ### Métodos de Clase (Modelos)
211
- * `create_partition(value)`: Crea tabla física.
212
- * `drop_partition(value)`: Borra tabla física.
213
- * `partition_table_exists?(value)`: Verifica existencia.
214
- * `partition_table_name(value)`: Devuelve el nombre real de la tabla en Postgres.
239
+ **Helpers de Migraciones**
240
+ * `create_partitioned_table(table_name, **options)`
241
+ * `create_partitioned_table_from(target, source, sync_triggers: false, **options)`
242
+ * `swap_partitioned_tables(legacy, partitioned)`
215
243
 
216
244
  ---
217
245
 
218
- ## License
246
+ ## Licencia
219
247
 
220
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
248
+ Esta gema está disponible como código abierto bajo los términos de la [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module TenantPartition
7
+ module Generators
8
+ # Generador de la Fase 2 (Cutover) para migraciones Zero-Downtime.
9
+ #
10
+ # Este generador crea la migración final necesaria para completar el proceso de
11
+ # particionamiento en vivo. La migración generada se encarga de realizar un
12
+ # intercambio atómico (Swap) a nivel de PostgreSQL: renombra la tabla legacy a
13
+ # un nombre de respaldo, y activa la tabla particionada (sombra) con el nombre original,
14
+ # eliminando al mismo tiempo los triggers de sincronización temporal.
15
+ #
16
+ # @example Generar migración de cutover para la tabla 'versions'
17
+ # rails g tenant_partition:cutover versions isp_id
18
+ class CutoverGenerator < Rails::Generators::Base
19
+ include Rails::Generators::Migration
20
+
21
+ source_root File.expand_path("templates", __dir__)
22
+
23
+ # @!attribute [r] table_name
24
+ # @return [String] El nombre de la tabla legacy original.
25
+ argument :table_name, type: :string, banner: "nombre_de_la_tabla_actual"
26
+
27
+ # @!attribute [r] partition_key
28
+ # @return [String] El nombre de la columna utilizada como clave de partición.
29
+ argument :partition_key, type: :string, banner: "columna_partition_key"
30
+
31
+ desc "Genera la Fase 2 (Cutover): Intercambia las tablas de forma atómica y elimina triggers."
32
+
33
+ # Método requerido por Rails::Generators::Migration para la nomenclatura de archivos.
34
+ # Determina el siguiente número (timestamp) para el archivo de migración.
35
+ #
36
+ # @param dirname [String] El directorio donde se guardarán las migraciones.
37
+ # @return [String] El prefijo numérico para el archivo.
38
+ def self.next_migration_number(dirname)
39
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
40
+ end
41
+
42
+ # Crea el archivo de migración de cutover en la carpeta db/migrate.
43
+ # Utiliza la plantilla complete_online_migration.rb.erb.
44
+ #
45
+ # @return [void]
46
+ def create_cutover_migration
47
+ migration_template(
48
+ "complete_online_migration.rb.erb",
49
+ "db/migrate/complete_online_migration_for_#{table_name}.rb",
50
+ migration_version: migration_version
51
+ )
52
+ end
53
+
54
+ # Muestra en consola las instrucciones críticas y advertencias de seguridad
55
+ # una vez que el generador termina de ejecutarse exitosamente.
56
+ #
57
+ # @return [void]
58
+ # Muestra en consola las instrucciones críticas y advertencias de seguridad
59
+ # forzando la interpolación ERB para una mejor experiencia de usuario.
60
+ def show_readme
61
+ if behavior == :invoke
62
+ template_path = File.join(self.class.source_root, "CUTOVER_README")
63
+ parsed_readme = ERB.new(File.read(template_path)).result(binding)
64
+ puts "\n#{parsed_readme}\n"
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ # Obtiene la versión actual de ActiveRecord para inyectarla en la sintaxis de la migración.
71
+ #
72
+ # @return [String] Ejemplo: "[7.1]"
73
+ def migration_version
74
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
75
+ end
76
+
77
+ # Construye el nombre de la tabla destino (sombra) que ahora pasará a ser la principal.
78
+ #
79
+ # @return [String] Ejemplo: "versions_partitioned"
80
+ def target_table
81
+ "#{table_name}_partitioned"
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module TenantPartition
7
+ module Generators
8
+ # Generador de la Fase 1 (Preparación) para migraciones Zero-Downtime.
9
+ #
10
+ # Este generador crea la infraestructura inicial necesaria para comenzar a migrar
11
+ # una tabla legacy hacia una tabla particionada sin tiempo de inactividad.
12
+ # Específicamente, genera una migración que clona la estructura de la tabla
13
+ # original y establece los triggers de PostgreSQL para la sincronización en vivo (Live Sync).
14
+ #
15
+ # @example Generar migración de preparación para la tabla 'versions'
16
+ # rails g tenant_partition:prepare versions isp_id
17
+ class PrepareGenerator < Rails::Generators::Base
18
+ include Rails::Generators::Migration
19
+
20
+ source_root File.expand_path("templates", __dir__)
21
+
22
+ # @!attribute [r] table_name
23
+ # @return [String] El nombre de la tabla legacy que se desea migrar.
24
+ argument :table_name, type: :string, banner: "nombre_de_la_tabla_actual"
25
+
26
+ # @!attribute [r] partition_key
27
+ # @return [String] El nombre de la columna que actuará como clave de partición.
28
+ argument :partition_key, type: :string, banner: "columna_partition_key"
29
+
30
+ desc "Genera la Fase 1 (Preparación): Crea la tabla sombra y los triggers de Live Sync."
31
+
32
+ # Método requerido por Rails::Generators::Migration para la nomenclatura de archivos.
33
+ # Determina el siguiente número (timestamp) para el archivo de migración.
34
+ #
35
+ # @param dirname [String] El directorio donde se guardarán las migraciones.
36
+ # @return [String] El prefijo numérico para el archivo.
37
+ def self.next_migration_number(dirname)
38
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
39
+ end
40
+
41
+ # Crea el archivo de migración de preparación en la carpeta db/migrate.
42
+ # Utiliza la plantilla prepare_online_migration.rb.erb.
43
+ #
44
+ # @return [void]
45
+ def create_preparation_migration
46
+ migration_template(
47
+ "prepare_online_migration.rb.erb",
48
+ "db/migrate/prepare_online_migration_for_#{table_name}.rb",
49
+ migration_version: migration_version
50
+ )
51
+ end
52
+
53
+ # Muestra en consola las instrucciones de los siguientes pasos
54
+ # una vez que el generador termina de ejecutarse exitosamente.
55
+ #
56
+ # @return [void]
57
+ def show_readme
58
+ readme "PREPARE_README" if behavior == :invoke
59
+ end
60
+
61
+ private
62
+
63
+ # Obtiene la versión actual de ActiveRecord para inyectarla en la sintaxis de la migración.
64
+ #
65
+ # @return [String] Ejemplo: "[7.1]"
66
+ def migration_version
67
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
68
+ end
69
+
70
+ # Construye el nombre de la tabla destino (sombra) que recibirá los datos.
71
+ #
72
+ # @return [String] Ejemplo: "versions_partitioned"
73
+ def target_table
74
+ "#{table_name}_partitioned"
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,22 @@
1
+ ===============================================================================
2
+ 🚀 ¡Fase 2: Cutover Generado Exitosamente!
3
+
4
+ Has generado la migración final de intercambio atómico.
5
+
6
+ ⚠️ ¡ALTO! ANTES DE DESPLEGAR A PRODUCCIÓN ⚠️
7
+ Asegúrate de que el proceso de Backfill masivo haya terminado al 100%.
8
+ Puedes verificarlo reemplazando "Model" por tu clase real (ej. PaperTrail::Version)
9
+ y ejecutando esto en tu consola de Rails (`rails c`):
10
+
11
+ Model.count == Model.from('<%= table_name %>_partitioned').count
12
+
13
+ Si te devuelve `true`, los números coinciden:
14
+ 1. Sube este código y ejecuta la migración en Producción (Despliegue 2).
15
+ Esto hará el intercambio atómico de Postgres en 1 milisegundo.
16
+
17
+ 2. ¡Listo! En el próximo reinicio de los servidores, tu modelo
18
+ detectará el cambio en la base de datos y activará automáticamente
19
+ sus superpoderes gracias a la Introspección Dinámica.
20
+
21
+ ¡Felicidades por lograr una migración Zero-Downtime perfecta!
22
+ ===============================================================================
@@ -0,0 +1,12 @@
1
+ ===============================================================================
2
+ 🪄 ¡Fase 1: Preparación Online Generada!
3
+
4
+ 1. Revisa el archivo de migración generado en db/migrate/.
5
+ 2. Sube este código a tu repositorio y despliega en Producción (Despliegue 1).
6
+ 3. Con la nueva tabla sombra ya creada en BD, ejecuta el Backfill histórico:
7
+ `rake tenant_partition:backfill_data[MiModelo,mi_tabla_partitioned]`
8
+
9
+ Una vez que el copiado llegue al 100%, podrás generar el intercambio final
10
+ con el comando de Fase 2:
11
+ `rails g tenant_partition:cutover nombre_tabla partition_key`
12
+ ===============================================================================
@@ -0,0 +1,21 @@
1
+ class CompleteOnlineMigrationFor<%= table_name.camelize %> < ActiveRecord::Migration<%= migration_version %>
2
+ def up
3
+ # 3. Intercambio Atómico (Cutover)
4
+ swap_partitioned_tables(:<%= table_name %>, :<%= target_table %>)
5
+
6
+ # La tabla original quedó renombrada como '<%= table_name %>_legacy' como backup.
7
+ # Cuando estés seguro de que todo funciona, puedes crear otra migración para borrarla.
8
+ end
9
+
10
+ def down
11
+ # Revierte el swap de nombres
12
+ swap_partitioned_tables(:<%= target_table %>, :<%= table_name %>)
13
+
14
+ # Restaura los triggers por si hay que volver atrás
15
+ create_partition_sync_trigger(
16
+ :<%= table_name %>,
17
+ :<%= target_table %>,
18
+ :<%= partition_key %>
19
+ )
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ class PrepareOnlineMigrationFor<%= table_name.camelize %> < ActiveRecord::Migration<%= migration_version %>
2
+ def up
3
+ # 🪄 Magia de TenantPartition:
4
+ # Esto lee la tabla actual '<%= table_name %>', crea '<%= target_table %>'
5
+ # con exactamente las mismas columnas y automáticamente instala los triggers de sincronización.
6
+ create_partitioned_table_from(
7
+ :<%= target_table %>,
8
+ :<%= table_name %>,
9
+ partition_key: :<%= partition_key %>,
10
+ sync_triggers: true
11
+ # id_type: :uuid # Descomenta esto si tu tabla original usa UUIDs
12
+ ) do |t|
13
+ # Opcional: Agrega aquí índices para tu nueva tabla si los necesitas.
14
+ # Ejemplo: t.index [:mi_columna, :<%= partition_key %>]
15
+ end
16
+ end
17
+
18
+ def down
19
+ remove_partition_sync_trigger(:<%= table_name %>, :<%= target_table %>)
20
+ drop_partitioned_table(:<%= target_table %>)
21
+ end
22
+ end