tenant_partition 0.2.0 → 0.2.1
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 +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +93 -72
- data/lib/tenant_partition/schema/statements.rb +17 -4
- data/lib/tenant_partition/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e675f290b7cbe1ef13c1d6db7ec36894a92c169c82ae42112a1b0979c220f9f1
|
|
4
|
+
data.tar.gz: 4470f638d2f110c48d9f22a1b8a685f2c598dd01292cb867617a45782d7def11
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 87115b8fc726b431102c6ea6d53103f71370d643a3fc543504ceb94df1d718eedd4041092a7f2384ecb98e9825447ba88281a32035ee244081b04579bda041c6
|
|
7
|
+
data.tar.gz: 3d535affbac9ea2101aaf1d8ecd415f379fb0e8cfa0d1a826b5d02e01567bd1a90f87ae01dd9be53744d8fe975e467a1362ac24f773e417b17d4635ec6b4d0e6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.1] - 2026-02-13
|
|
4
|
+
### Added
|
|
5
|
+
- **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.
|
|
6
|
+
|
|
3
7
|
## [0.2.0] - 2026-02-13
|
|
4
8
|
### Changed (Breaking Changes)
|
|
5
9
|
- **Arquitectura:** Se eliminó la clase `TenantPartition::Base` y la necesidad de crear modelos de infraestructura en el namespace `Partition::`.
|
data/README.md
CHANGED
|
@@ -7,6 +7,7 @@ A diferencia de otras soluciones que dependen de esquemas (schemas) o hackeos a
|
|
|
7
7
|
## 🚀 Características Principales
|
|
8
8
|
|
|
9
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.
|
|
10
11
|
* **Soporte Nativo CPK:** Compatible con **Composite Primary Keys** de Rails 7.1+.
|
|
11
12
|
* **Sin Magic Strings:** Usa métodos explícitos (`create_partition`, `drop_partition`).
|
|
12
13
|
* **Gestión de Datos Huérfanos:** Herramientas para mover datos de la tabla "Default" a su partición correcta automáticamente.
|
|
@@ -33,7 +34,8 @@ bundle install
|
|
|
33
34
|
|
|
34
35
|
## ⚙️ Configuración Inicial
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
### 1. Inicializador
|
|
38
|
+
Crea un archivo para definir tu clave de partición global (por ejemplo, `:isp_id`, `:account_id`, `:tenant_id`).
|
|
37
39
|
|
|
38
40
|
```ruby
|
|
39
41
|
# config/initializers/tenant_partition.rb
|
|
@@ -44,105 +46,105 @@ TenantPartition.configure do |config|
|
|
|
44
46
|
end
|
|
45
47
|
```
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
## 🏗 Estrategias de Uso
|
|
50
|
-
|
|
51
|
-
La gema utiliza un patrón "Opt-In". Incluir el módulo no altera tus modelos hasta que lo activas explícitamente. Puedes elegir la estrategia que mejor se adapte a tu proyecto:
|
|
52
|
-
|
|
53
|
-
### Opción A: Global (Recomendado)
|
|
54
|
-
Incluye el concern en `ApplicationRecord`. Esto **NO** particiona tus tablas, solo habilita la posibilidad de usar la macro `partition_table` en el futuro. Es ideal para mantener el código limpio.
|
|
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.
|
|
55
51
|
|
|
56
52
|
```ruby
|
|
57
53
|
# app/models/application_record.rb
|
|
58
54
|
class ApplicationRecord < ActiveRecord::Base
|
|
59
55
|
primary_abstract_class
|
|
60
56
|
|
|
61
|
-
# Habilita la herramienta
|
|
62
|
-
include TenantPartition::Concerns::Partitioned
|
|
63
|
-
end
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### Opción B: Local (A la carta)
|
|
67
|
-
Si prefieres no tocar `ApplicationRecord` o estás en un sistema legacy, puedes incluir el concern solo en los modelos específicos.
|
|
68
|
-
|
|
69
|
-
```ruby
|
|
70
|
-
# app/models/conversation.rb
|
|
71
|
-
class Conversation < ApplicationRecord
|
|
57
|
+
# Habilita la herramienta (modo inactivo por defecto)
|
|
72
58
|
include TenantPartition::Concerns::Partitioned
|
|
73
|
-
partition_table # Activación inmediata
|
|
74
59
|
end
|
|
75
60
|
```
|
|
76
61
|
|
|
77
62
|
---
|
|
78
63
|
|
|
79
|
-
##
|
|
64
|
+
## 🛠 Guía de Implementación
|
|
80
65
|
|
|
81
|
-
|
|
66
|
+
### Paso 1: Migración de Base de Datos
|
|
82
67
|
|
|
83
|
-
|
|
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.
|
|
84
72
|
|
|
85
|
-
|
|
73
|
+
#### Opción A: Usando Enteros (BigInt) - Recomendado
|
|
86
74
|
|
|
87
75
|
```ruby
|
|
88
|
-
class
|
|
89
|
-
|
|
90
|
-
|
|
76
|
+
class CreateConversations < ActiveRecord::Migration[7.1]
|
|
77
|
+
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|
|
|
81
|
+
t.string :subject
|
|
82
|
+
t.text :body
|
|
83
|
+
t.timestamps
|
|
91
84
|
|
|
92
|
-
|
|
93
|
-
|
|
85
|
+
# Nota: No definas :id ni :isp_id aquí, el helper lo hace por ti.
|
|
86
|
+
end
|
|
87
|
+
end
|
|
94
88
|
end
|
|
95
89
|
```
|
|
96
90
|
|
|
97
|
-
|
|
98
|
-
1. Configura la **Primary Key Compuesta** (`[:id, :partition_key]`).
|
|
99
|
-
2. Registra el modelo en el sistema de mantenimiento de la gema.
|
|
100
|
-
3. Inyecta los métodos de gestión de infraestructura (ver abajo).
|
|
101
|
-
4. Agrega el scope `for_partition(value)`.
|
|
91
|
+
#### Opción B: Usando UUIDs
|
|
102
92
|
|
|
103
|
-
|
|
93
|
+
```ruby
|
|
94
|
+
class CreateConversations < ActiveRecord::Migration[7.1]
|
|
95
|
+
def change
|
|
96
|
+
enable_extension 'pgcrypto' # Necesario para gen_random_uuid()
|
|
104
97
|
|
|
105
|
-
|
|
98
|
+
create_partitioned_table :conversations, id_type: :uuid do |t|
|
|
99
|
+
t.string :subject
|
|
100
|
+
t.timestamps
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
```
|
|
106
105
|
|
|
107
|
-
|
|
108
|
-
| :--- | :--- | :--- |
|
|
109
|
-
| `create_partition(val)` | Crea la tabla física en Postgres (`CREATE TABLE ... PARTITION OF ...`). | `Conversation.create_partition(100)` |
|
|
110
|
-
| `drop_partition(val)` | Elimina la tabla física y sus datos (`DROP TABLE ...`). | `Conversation.drop_partition(100)` |
|
|
111
|
-
| `partition_table_exists?(val)` | Devuelve `true` si la tabla física existe en la BD. | `Conversation.partition_table_exists?(100)` |
|
|
112
|
-
| `partition_table_name(val)` | Devuelve el nombre real de la tabla hija. | `Conversation.partition_table_name(100)` <br> *=> "conversations_isp_100"* |
|
|
106
|
+
### Paso 2: Configurar el Modelo
|
|
113
107
|
|
|
114
|
-
|
|
108
|
+
Usa la macro `partition_table` para activar la funcionalidad en el modelo correspondiente.
|
|
115
109
|
|
|
116
|
-
|
|
110
|
+
```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
|
|
117
|
+
end
|
|
118
|
+
```
|
|
117
119
|
|
|
118
|
-
|
|
120
|
+
**¿Necesitas una key diferente para un solo modelo?**
|
|
121
|
+
```ruby
|
|
122
|
+
class AuditLog < ApplicationRecord
|
|
123
|
+
# Este modelo se particiona por año, ignorando la config global
|
|
124
|
+
partition_table key: :year
|
|
125
|
+
end
|
|
126
|
+
```
|
|
119
127
|
|
|
120
|
-
|
|
128
|
+
### Paso 3: Crear y Eliminar Tenants
|
|
121
129
|
|
|
122
|
-
|
|
123
|
-
class CreateConversations < ActiveRecord::Migration[7.1]
|
|
124
|
-
def up
|
|
125
|
-
# 1. Crear la tabla padre particionada (id: false es importante)
|
|
126
|
-
create_table :conversations, id: false, options: "PARTITION BY LIST (isp_id)" do |t|
|
|
127
|
-
t.bigserial :id, null: false
|
|
128
|
-
t.integer :isp_id, null: false # Tu partition key
|
|
130
|
+
Gestiona el ciclo de vida de las particiones utilizando los métodos de clase inyectados.
|
|
129
131
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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"
|
|
133
136
|
|
|
134
|
-
|
|
135
|
-
|
|
137
|
+
# Verificar si existe
|
|
138
|
+
Conversation.partition_table_exists?(100)
|
|
139
|
+
# => true
|
|
136
140
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
# ...
|
|
141
|
+
# Eliminar partición (CUIDADO: Borra datos)
|
|
142
|
+
Conversation.drop_partition(100)
|
|
143
|
+
# => Elimina "conversations_isp_100"
|
|
141
144
|
```
|
|
142
145
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
Es común automatizar la creación de particiones cuando se crea un nuevo Tenant (ej. un nuevo ISP o Cliente).
|
|
146
|
+
#### Automatización con Callbacks
|
|
147
|
+
Es común crear las particiones automáticamente cuando nace un nuevo Tenant.
|
|
146
148
|
|
|
147
149
|
```ruby
|
|
148
150
|
# app/models/isp.rb
|
|
@@ -150,7 +152,7 @@ class Isp < ApplicationRecord
|
|
|
150
152
|
after_create :provision_infrastructure
|
|
151
153
|
|
|
152
154
|
def provision_infrastructure
|
|
153
|
-
#
|
|
155
|
+
# Helper global que crea particiones en TODOS los modelos registrados
|
|
154
156
|
TenantPartition.create!(self.id)
|
|
155
157
|
end
|
|
156
158
|
end
|
|
@@ -160,10 +162,10 @@ end
|
|
|
160
162
|
|
|
161
163
|
## 🧹 Mantenimiento y Datos Huérfanos
|
|
162
164
|
|
|
163
|
-
Si insertas datos con un `isp_id` para el cual no has creado una partición
|
|
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.
|
|
164
166
|
|
|
165
167
|
### Auditoría
|
|
166
|
-
Verifica si tienes datos en las tablas default:
|
|
168
|
+
Verifica si tienes datos "mal ubicados" en las tablas default:
|
|
167
169
|
|
|
168
170
|
```bash
|
|
169
171
|
bundle exec rake tenant_partition:audit
|
|
@@ -172,7 +174,7 @@ bundle exec rake tenant_partition:audit
|
|
|
172
174
|
```
|
|
173
175
|
|
|
174
176
|
### Limpieza (Cleanup)
|
|
175
|
-
Crea las particiones faltantes y mueve los datos automáticamente:
|
|
177
|
+
Crea las particiones faltantes y mueve los datos automáticamente a su hogar correcto:
|
|
176
178
|
|
|
177
179
|
```bash
|
|
178
180
|
bundle exec rake tenant_partition:cleanup
|
|
@@ -184,9 +186,9 @@ bundle exec rake tenant_partition:cleanup
|
|
|
184
186
|
|
|
185
187
|
## 🛡️ Producción y Seguridad
|
|
186
188
|
|
|
187
|
-
La gema incluye un `SafetyGuard` que impide ejecutar comandos destructivos (`drop_partition`, `destroy!`) en entorno de producción
|
|
189
|
+
La gema incluye un `SafetyGuard` que impide ejecutar comandos destructivos (`drop_partition`, `destroy!`) en entorno de producción para evitar catástrofes.
|
|
188
190
|
|
|
189
|
-
|
|
191
|
+
Si realmente necesitas borrar un tenant en producción, debes autorizarlo explícitamente:
|
|
190
192
|
|
|
191
193
|
```bash
|
|
192
194
|
DISABLE_TENANT_PARTITION_GUARD=true bundle exec rake tenant_partition:destroy_tenant[123]
|
|
@@ -194,6 +196,25 @@ DISABLE_TENANT_PARTITION_GUARD=true bundle exec rake tenant_partition:destroy_te
|
|
|
194
196
|
|
|
195
197
|
---
|
|
196
198
|
|
|
199
|
+
## 📖 Referencia de API
|
|
200
|
+
|
|
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.
|
|
206
|
+
|
|
207
|
+
### Métodos de Instancia (Modelos)
|
|
208
|
+
* `partition_table(key: nil)`: Macro de activación.
|
|
209
|
+
|
|
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.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
197
218
|
## License
|
|
198
219
|
|
|
199
220
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -10,15 +10,18 @@ module TenantPartition
|
|
|
10
10
|
# @param table_name [Symbol] Nombre de la tabla.
|
|
11
11
|
# @param options [Hash] Opciones de migración estándar.
|
|
12
12
|
# @option options [Symbol] :partition_key Clave opcional para sobreescribir la global.
|
|
13
|
+
# @option options [Symbol] :id_type Tipo de ID (:uuid o :bigint). Default: :bigint.
|
|
13
14
|
# @yield [t] Bloque de definición de tabla (ActiveRecord::ConnectionAdapters::TableDefinition).
|
|
14
15
|
def create_partitioned_table(table_name, **options, &block)
|
|
15
16
|
key = options.delete(:partition_key) || TenantPartition.configuration&.partition_key
|
|
17
|
+
id_type = options.delete(:id_type) || :bigint # Default conservador (BigInt)
|
|
18
|
+
|
|
16
19
|
raise TenantPartition::Error, "Falta 'partition_key'." unless key
|
|
17
20
|
|
|
18
21
|
configure_pk_and_options(options, key)
|
|
19
22
|
|
|
20
23
|
create_table(table_name, **options) do |t|
|
|
21
|
-
setup_partitioning(t, key, block)
|
|
24
|
+
setup_partitioning(t, key, id_type, block)
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
create_default_partition(table_name)
|
|
@@ -33,9 +36,17 @@ module TenantPartition
|
|
|
33
36
|
private
|
|
34
37
|
|
|
35
38
|
# Configura las columnas y definiciones dentro del bloque create_table.
|
|
36
|
-
def setup_partitioning(table, key, block)
|
|
37
|
-
|
|
39
|
+
def setup_partitioning(table, key, id_type, block)
|
|
40
|
+
# Definimos la ID según la preferencia del usuario
|
|
41
|
+
if id_type == :uuid
|
|
42
|
+
table.uuid :id, null: false, default: -> { "gen_random_uuid()" }
|
|
43
|
+
else
|
|
44
|
+
table.bigserial :id, null: false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Ejecutamos el bloque del usuario (definición de columnas adicionales)
|
|
38
48
|
block.call(table)
|
|
49
|
+
|
|
39
50
|
ensure_partition_column(table, key)
|
|
40
51
|
end
|
|
41
52
|
|
|
@@ -50,7 +61,9 @@ module TenantPartition
|
|
|
50
61
|
def ensure_partition_column(table, key)
|
|
51
62
|
return if table.columns.any? { |c| c.name == key.to_s }
|
|
52
63
|
|
|
53
|
-
|
|
64
|
+
# Si no la definió, la creamos (asumiendo integer por defecto para claves foráneas típicas)
|
|
65
|
+
# Si el usuario quiere un UUID como partition key, debería definirlo explícitamente en el bloque.
|
|
66
|
+
table.integer key, null: false
|
|
54
67
|
end
|
|
55
68
|
|
|
56
69
|
# Crea la tabla _default para capturar datos sin partición asignada.
|