tenant_partition 0.1.1 → 0.1.3

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,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Namespace principal de la gema.
3
4
  module TenantPartition
5
+ # Clase contenedora de la configuración global de la gema.
4
6
  class Configuration
5
- # @return [Symbol, nil] Columna de base de datos (ej: :isp_id)
7
+ # @return [Symbol, nil] Columna de base de datos usada para particionar (ej: :isp_id).
6
8
  attr_accessor :partition_key
7
9
 
8
- # @return [String, nil] Nombre del Header HTTP personalizado (ej: 'X-Tenant-ID')
10
+ # @return [String, nil] Nombre del Header HTTP para identificación (ej: 'X-Tenant-ID').
9
11
  attr_accessor :header_name
10
12
 
11
13
  def initialize
@@ -13,21 +15,27 @@ module TenantPartition
13
15
  @header_name = nil
14
16
  end
15
17
 
18
+ # Valida si la configuración mínima requerida está presente.
19
+ # @return [Boolean] true si es válida.
16
20
  def valid?
17
21
  !partition_key.nil?
18
22
  end
19
23
  end
20
24
 
21
25
  class << self
26
+ # @return [TenantPartition::Configuration] La instancia actual de configuración.
22
27
  attr_accessor :configuration
23
28
 
29
+ # Punto de entrada para configurar la gema.
30
+ # @yieldparam [TenantPartition::Configuration] config
31
+ # @raise [TenantPartition::Error] Si la configuración resultante es inválida.
24
32
  def configure
25
33
  self.configuration ||= Configuration.new
26
34
  yield(configuration)
27
35
 
28
- unless configuration.valid?
29
- raise TenantPartition::Error, "Debe configurar un 'partition_key' en el inicializador de TenantPartition."
30
- end
36
+ return if configuration.valid?
37
+
38
+ raise TenantPartition::Error, "Debe configurar un 'partition_key' en el inicializador de TenantPartition."
31
39
  end
32
40
  end
33
41
  end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TenantPartition
4
+ # Módulo Mixin que agrega capacidades de mantenimiento (Auditoría y Limpieza)
5
+ # a la fachada principal.
6
+ module Maintenance
7
+ # Audita todas las tablas DEFAULT del sistema buscando registros huérfanos.
8
+ # Un registro huérfano es aquel que cayó en la tabla default porque su partición no existía.
9
+ #
10
+ # @return [Hash{String => Integer}] Reporte con nombre del modelo y cantidad de huérfanos.
11
+ def audit
12
+ ensure_models_loaded!
13
+ log_info "AUDIT", "Iniciando auditoría de tablas DEFAULT..."
14
+
15
+ partitionable_models.each_with_object({}) do |model, report|
16
+ count = count_default_rows(model)
17
+ report[model.name] = count
18
+ log_audit_result(model, count)
19
+ end
20
+ end
21
+
22
+ # Ejecuta el proceso de limpieza global.
23
+ # Identifica registros huérfanos y los mueve a sus particiones correspondientes.
24
+ #
25
+ # @return [void]
26
+ def cleanup!
27
+ ensure_models_loaded!
28
+ key = configuration.partition_key
29
+ log_info "CLEANUP", "Iniciando proceso de limpieza global..."
30
+
31
+ partitionable_models.each do |model|
32
+ process_cleanup_for_model(model, key)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ # Registra el resultado de una auditoría en el log.
39
+ def log_audit_result(model, count)
40
+ if count.positive?
41
+ log_warn "ALERTA", "#{model.name}: #{count} registros huérfanos encontrados."
42
+ else
43
+ log_info "OK", "#{model.name}: Tabla default limpia."
44
+ end
45
+ end
46
+
47
+ # Orquesta la limpieza para un modelo específico.
48
+ def process_cleanup_for_model(model, key)
49
+ orphan_ids = fetch_orphan_ids(model, key)
50
+ return log_info("OK", "#{model.name}: Sin datos huérfanos.") if orphan_ids.empty?
51
+
52
+ log_warn "FIX", "#{model.name}: Procesando #{orphan_ids.count} tenants con datos huérfanos."
53
+ orphan_ids.each { |id| move_orphans(model, id) }
54
+ end
55
+
56
+ # Mueve los huérfanos de un tenant específico.
57
+ def move_orphans(model, id)
58
+ partition = model.find(id)
59
+ if partition
60
+ moved = partition.populate_from_default
61
+ log_info "MOVE", " -> ID #{id}: #{moved} registros recuperados."
62
+ else
63
+ log_error "ERROR", " -> ID #{id}: La partición física no existe. Cree el tenant primero."
64
+ end
65
+ end
66
+
67
+ # Cuenta las filas en la tabla default de un modelo.
68
+ def count_default_rows(model)
69
+ model.connection.select_value("SELECT count(*) FROM #{model.default_table}").to_i
70
+ rescue ActiveRecord::StatementInvalid
71
+ 0
72
+ end
73
+
74
+ # Obtiene los IDs únicos de los registros atrapados en la tabla default.
75
+ def fetch_orphan_ids(model, key)
76
+ sql = "SELECT DISTINCT #{key} FROM #{model.default_table} WHERE #{key} IS NOT NULL"
77
+ model.connection.execute(sql).map { |r| r[key.to_s] }
78
+ rescue ActiveRecord::StatementInvalid
79
+ []
80
+ end
81
+ end
82
+ end
@@ -1,48 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TenantPartition
4
- # Clase encargada de validar que el entorno y la configuración sean aptos
5
- # para el funcionamiento de TenantPartition.
4
+ # Clase interna encargada de validar prerequisitos del entorno y base de datos.
6
5
  class SafetyGuard
7
- # Versión mínima de PostgreSQL soportada para particionamiento nativo estable.
6
+ # Versión mínima de PostgreSQL requerida.
8
7
  MIN_POSTGRES_VERSION = 13.0
9
8
 
10
- # Ejecuta todas las validaciones de seguridad.
11
- # @raise [TenantPartition::Error] Si alguna validación falla.
12
- # @return [void]
13
- def self.validate!
14
- check_configuration!
15
- return unless defined?(Rails) && Rails.env.to_s != 'test'
9
+ class << self
10
+ # Ejecuta todas las validaciones de seguridad.
11
+ # @raise [TenantPartition::Error] Si alguna validación falla.
12
+ def validate!
13
+ check_configuration!
14
+ return unless defined?(Rails) && Rails.env.to_s != "test"
16
15
 
17
- check_database_adapter!
18
- check_postgres_version!
19
- end
16
+ check_database_adapter!
17
+ check_postgres_version!
18
+ end
20
19
 
21
- private
20
+ private
21
+
22
+ def check_configuration!
23
+ return unless TenantPartition.configuration.partition_key.nil?
22
24
 
23
- # Verifica que la clave de partición esté presente.
24
- def self.check_configuration!
25
- if TenantPartition.configuration.partition_key.nil?
26
25
  raise TenantPartition::Error, "Falta configuración: 'partition_key' no ha sido definido."
27
26
  end
28
- end
29
27
 
30
- # Asegura que se esté utilizando PostgreSQL.
31
- def self.check_database_adapter!
32
- adapter = ActiveRecord::Base.connection_db_config.adapter
33
- unless adapter == "postgresql"
34
- raise TenantPartition::Error, "Adaptador incompatible: TenantPartition solo soporta PostgreSQL (usando: #{adapter})."
28
+ def check_database_adapter!
29
+ adapter = ActiveRecord::Base.connection_db_config.adapter
30
+ return if adapter == "postgresql"
31
+
32
+ raise TenantPartition::Error, "Adaptador incompatible: TenantPartition solo soporta " \
33
+ "PostgreSQL (usando: #{adapter})."
35
34
  end
36
- end
37
35
 
38
- # Verifica la versión de PostgreSQL mediante una consulta directa al motor.
39
- def self.check_postgres_version!
40
- version = ActiveRecord::Base.connection.select_value("SHOW server_version").to_f
41
- if version < MIN_POSTGRES_VERSION
42
- raise TenantPartition::Error, "Versión de PostgreSQL insuficiente: Se requiere v#{MIN_POSTGRES_VERSION}+ (detectada: #{version})."
36
+ def check_postgres_version!
37
+ version = ActiveRecord::Base.connection.select_value("SHOW server_version").to_f
38
+ return if version >= MIN_POSTGRES_VERSION
39
+
40
+ raise TenantPartition::Error, "Versión de PostgreSQL insuficiente: Se requiere " \
41
+ "v#{MIN_POSTGRES_VERSION}+ (detectada: #{version})."
42
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotEstablished
43
+ # Si no hay conexión aún, se posterga la validación hasta el primer uso real
43
44
  end
44
- rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotEstablished
45
- # Si no hay conexión aún, se posterga la validación
46
45
  end
47
46
  end
48
47
  end
@@ -2,65 +2,65 @@
2
2
 
3
3
  module TenantPartition
4
4
  module Schema
5
- # Proporciona métodos adicionales para las migraciones de ActiveRecord
6
- # enfocados en la automatización del particionamiento nativo de PostgreSQL.
5
+ # Módulo que extiende ActiveRecord::Migration con DSL para particionamiento.
7
6
  module Statements
8
- # Crea una tabla padre particionada por lista y genera automáticamente
9
- # su partición por defecto (DEFAULT).
7
+ # Crea una tabla particionada por lista y su tabla DEFAULT asociada.
8
+ # Configura automáticamente Primary Keys Compuestas para compatibilidad con Postgres.
10
9
  #
11
- # CORRECCIÓN IMPORTANTE: Configura una Primary Key Compuesta (id + partition_key)
12
- # para cumplir con los requisitos de unicidad de PostgreSQL en tablas particionadas.
13
- #
14
- # @param table_name [Symbol, String] El nombre de la tabla a crear.
15
- # @param options [Hash] Opciones estándar + :partition_key opcional.
16
- # @yield [t] Bloque para definir las columnas de la tabla.
10
+ # @param table_name [Symbol] Nombre de la tabla.
11
+ # @param options [Hash] Opciones de migración estándar.
12
+ # @option options [Symbol] :partition_key Clave opcional para sobreescribir la global.
13
+ # @yield [t] Bloque de definición de tabla (ActiveRecord::ConnectionAdapters::TableDefinition).
17
14
  def create_partitioned_table(table_name, **options, &block)
18
- # Prioridad: 1. Opción pasada al método, 2. Configuración global
19
15
  key = options.delete(:partition_key) || TenantPartition.configuration&.partition_key
16
+ raise TenantPartition::Error, "Falta 'partition_key'." unless key
17
+
18
+ configure_pk_and_options(options, key)
20
19
 
21
- unless key
22
- raise TenantPartition::Error, "Debe configurar 'partition_key' globalmente o pasarlo como opción."
20
+ create_table(table_name, **options) do |t|
21
+ setup_partitioning(t, key, block)
23
22
  end
24
23
 
25
- # --- LÓGICA DE PRIMARY KEY COMPUESTA ---
26
- # 1. Desactivamos la creación automática del ID simple, ya que Postgres fallaría.
27
- options[:id] = false
24
+ create_default_partition(table_name)
25
+ end
28
26
 
29
- # 2. Definimos explícitamente que la PK está formada por el ID y la CLAVE DE PARTICIÓN.
30
- # Esto genera en SQL: PRIMARY KEY (id, isp_id)
31
- options[:primary_key] = [:id, key]
27
+ # Elimina una tabla particionada en cascada.
28
+ # @param table_name [Symbol] Nombre de la tabla.
29
+ def drop_partitioned_table(table_name)
30
+ drop_table(table_name, cascade: true)
31
+ end
32
32
 
33
- # 3. Inyectamos la estrategia de particionamiento
34
- options[:options] = "PARTITION BY LIST (#{key})"
33
+ private
35
34
 
36
- # Crear la tabla padre
37
- create_table(table_name, **options) do |t|
38
- # A. Como desactivamos id: false, debemos crear la columna ID manualmente.
39
- # Usamos gen_random_uuid() para que sea autogenerado.
40
- t.uuid :id, null: false, default: -> { "gen_random_uuid()" }
35
+ # Configura las columnas y definiciones dentro del bloque create_table.
36
+ def setup_partitioning(table, key, block)
37
+ table.uuid :id, null: false, default: -> { "gen_random_uuid()" }
38
+ block.call(table)
39
+ ensure_partition_column(table, key)
40
+ end
41
41
 
42
- # B. Ejecutamos las definiciones del usuario
43
- block.call(t)
42
+ # Prepara las opciones de PK compuesta y tipo de partición.
43
+ def configure_pk_and_options(options, key)
44
+ options[:id] = false
45
+ options[:primary_key] = [:id, key]
46
+ options[:options] = "PARTITION BY LIST (#{key})"
47
+ end
44
48
 
45
- # C. Inyección automática del atributo de partición si el usuario no lo definió.
46
- # Nota: Si el usuario ya puso `t.uuid :isp_id` en su migración, esta línea la salta.
47
- unless t.columns.any? { |c| c.name == key.to_s }
48
- t.column key, :string, null: false
49
- end
50
- end
49
+ # Asegura que la columna de partición exista si el usuario olvidó definirla.
50
+ def ensure_partition_column(table, key)
51
+ return if table.columns.any? { |c| c.name == key.to_s }
51
52
 
52
- # Crear la partición DEFAULT
53
+ table.column key, :string, null: false
54
+ end
55
+
56
+ # Crea la tabla _default para capturar datos sin partición asignada.
57
+ def create_default_partition(table_name)
53
58
  default_name = "#{table_name}_default"
54
59
  execute <<-SQL.squish
55
60
  CREATE TABLE IF NOT EXISTS #{default_name}
56
61
  PARTITION OF #{table_name} DEFAULT;
57
62
  SQL
58
63
  end
59
-
60
- # Elimina una tabla particionada y su tabla por defecto asociada.
61
- def drop_partitioned_table(table_name)
62
- drop_table(table_name, cascade: true)
63
- end
64
64
  end
65
65
  end
66
66
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TenantPartition
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
@@ -4,211 +4,113 @@ require "active_record"
4
4
  require "active_model"
5
5
  require "active_support/all"
6
6
 
7
- # Carga de la versión y componentes internos
8
7
  require_relative "tenant_partition/version"
9
8
  require_relative "tenant_partition/configuration"
10
9
  require_relative "tenant_partition/safety_guard"
11
10
  require_relative "tenant_partition/base"
11
+ require_relative "tenant_partition/maintenance"
12
12
  require_relative "tenant_partition/concerns/partitioned"
13
13
  require_relative "tenant_partition/concerns/controller"
14
14
 
15
- # Integración con Ruby on Rails
16
15
  require_relative "tenant_partition/railtie" if defined?(Rails)
17
16
 
18
- # TenantPartition es el punto de entrada principal para la gestión de particionamiento
19
- # en PostgreSQL dentro de aplicaciones Rails.
20
- #
21
- # Actúa como una **Fachada** que centraliza:
22
- # 1. Configuración de la gema.
23
- # 2. Orquestación del ciclo de vida de los tenants (Crear/Borrar particiones).
24
- # 3. Mantenimiento y limpieza de datos huérfanos.
17
+ # Fachada principal para la gestión y orquestación de particionamiento en PostgreSQL.
18
+ # Centraliza la configuración, creación de particiones y mantenimiento.
25
19
  module TenantPartition
26
- # Error base para todas las excepciones de la gema.
27
20
  class Error < StandardError; end
28
21
 
22
+ # Incorpora las funcionalidades de Auditoría y Limpieza.
23
+ extend Maintenance
24
+
29
25
  class << self
30
- # @return [TenantPartition::Configuration] El objeto de configuración global.
26
+ # @return [TenantPartition::Configuration] Objeto de configuración global.
31
27
  attr_accessor :configuration
32
28
 
33
- # Configura la gema mediante un bloque e inicializa las validaciones de seguridad.
34
- #
29
+ # Bloque de configuración e inicialización.
35
30
  # @yieldparam [TenantPartition::Configuration] config
36
- # @return [void]
37
31
  def configure
38
32
  self.configuration ||= Configuration.new
39
33
  yield(configuration)
40
34
  SafetyGuard.validate!
41
35
  end
42
36
 
43
- # =========================================================================
44
- # GRUPO 1: ORQUESTACIÓN DE TENANTS (Ciclo de Vida)
45
- # =========================================================================
46
-
47
- # Crea las particiones físicas para un tenant específico en TODOS los modelos registrados.
48
- #
49
- # Este método es **idempotente**: verifica si la partición existe antes de intentar crearla,
50
- # evitando errores de SQL.
51
- #
52
- # @param partition_id [String, Integer] El ID del tenant (ej: el UUID del ISP).
53
- # @return [void]
37
+ # Crea las particiones físicas para un tenant en todos los modelos registrados.
38
+ # @param partition_id [String, Integer] Identificador del tenant.
54
39
  def create!(partition_id)
55
40
  ensure_models_loaded!
56
-
57
41
  log_info "CREATE", "Iniciando aprovisionamiento para ID: #{partition_id}"
58
42
 
59
43
  partitionable_models.each do |model|
60
- if model.exists?(partition_id)
61
- log_info "SKIP", "#{model.name}: La partición ya existe."
62
- else
63
- model.create(partition_id)
64
- log_info "OK", "#{model.name}: Partición creada exitosamente."
65
- end
44
+ process_creation(model, partition_id)
66
45
  end
67
46
  end
68
47
 
69
- # Elimina (DETACH + DROP) las particiones físicas de un tenant en TODOS los modelos.
70
- #
71
- # @warning Esta acción es destructiva e irreversible. Borra los datos físicos.
72
- #
73
- # @param partition_id [String, Integer] El ID del tenant a eliminar.
74
- # @return [void]
48
+ # Elimina irreversiblemente las particiones y datos de un tenant.
49
+ # @param partition_id [String, Integer] Identificador del tenant.
75
50
  def destroy!(partition_id)
76
51
  ensure_models_loaded!
77
-
78
52
  log_info "DESTROY", "Eliminando infraestructura para ID: #{partition_id}"
79
53
 
80
54
  partitionable_models.each do |model|
81
- if model.exists?(partition_id)
82
- partition = model.find(partition_id)
83
- if partition.destroy
84
- log_info "DROP", "#{model.name}: Partición eliminada."
85
- else
86
- log_error "FAIL", "#{model.name}: No se pudo eliminar la partición."
87
- end
88
- else
89
- log_info "SKIP", "#{model.name}: No existe partición para borrar."
90
- end
91
- end
92
- end
93
-
94
- # =========================================================================
95
- # GRUPO 2: MANTENIMIENTO Y OPS (Audit & Cleanup)
96
- # =========================================================================
97
-
98
- # Audita todas las tablas DEFAULT buscando registros "huérfanos".
99
- #
100
- # Un registro huérfano es aquel que se insertó en la tabla padre pero, al no existir
101
- # su partición correspondiente en ese momento, cayó en la tabla _default.
102
- #
103
- # @return [Hash{String => Integer}] Mapa con el nombre del modelo y la cantidad de registros huérfanos.
104
- # @example
105
- # TenantPartition.audit
106
- # # => { "Partition::Message" => 150, "Partition::Log" => 0 }
107
- def audit
108
- ensure_models_loaded!
109
- report = {}
110
-
111
- log_info "AUDIT", "Iniciando auditoría de tablas DEFAULT..."
112
-
113
- partitionable_models.each do |model|
114
- count = count_default_rows(model)
115
- report[model.name] = count
116
-
117
- if count > 0
118
- log_warn "ALERTA", "#{model.name}: #{count} registros huérfanos encontrados."
119
- else
120
- log_info "OK", "#{model.name}: Tabla default limpia."
121
- end
55
+ process_destruction(model, partition_id)
122
56
  end
123
-
124
- report
125
57
  end
126
58
 
127
- # Realiza una limpieza global moviendo registros huérfanos a sus particiones correctas.
128
- #
129
- # 1. Identifica qué IDs de tenant tienen datos en las tablas DEFAULT.
130
- # 2. Verifica si ya existe la partición física para esos IDs.
131
- # 3. Mueve los datos atómicamente.
132
- #
133
- # @return [void]
134
- def cleanup!
59
+ # Verifica si existe infraestructura creada para un tenant.
60
+ # @param partition_id [String, Integer] Identificador del tenant.
61
+ # @return [Boolean] true si existe al menos una tabla particionada.
62
+ def exists?(partition_id)
135
63
  ensure_models_loaded!
136
- key = configuration.partition_key
137
-
138
- log_info "CLEANUP", "Iniciando proceso de limpieza global..."
139
-
140
- partitionable_models.each do |model|
141
- # Obtenemos los IDs únicos presentes en la tabla default
142
- orphan_ids = fetch_orphan_ids(model, key)
143
-
144
- if orphan_ids.empty?
145
- log_info "OK", "#{model.name}: Sin datos huérfanos."
146
- next
147
- end
148
-
149
- log_warn "FIX", "#{model.name}: Procesando #{orphan_ids.count} tenants con datos huérfanos."
150
-
151
- orphan_ids.each do |id|
152
- partition = model.find(id)
153
-
154
- if partition
155
- moved = partition.populate_from_default
156
- log_info "MOVE", " -> ID #{id}: #{moved} registros recuperados."
157
- else
158
- log_error "ERROR", " -> ID #{id}: La partición física no existe. Cree el tenant primero."
159
- end
160
- end
161
- end
162
- end
163
-
164
- private
165
-
166
- # Encuentra todas las clases cargadas que heredan de TenantPartition::Base.
167
- # @return [Array<Class>]
168
- def partitionable_models
169
- ObjectSpace.each_object(Class).select { |klass| klass < TenantPartition::Base }
170
- end
171
-
172
- # Cuenta registros en la tabla default de un modelo de infraestructura.
173
- # @return [Integer]
174
- def count_default_rows(model)
175
- model.connection.select_value("SELECT count(*) FROM #{model.default_table}").to_i
176
- rescue ActiveRecord::StatementInvalid
177
- 0
64
+ partitionable_models.any? { |model| model.exists?(partition_id) }
178
65
  end
179
66
 
180
- # Obtiene los IDs de partición distintos que existen en la tabla default.
181
- # @return [Array<String>]
182
- def fetch_orphan_ids(model, key)
183
- sql = "SELECT DISTINCT #{key} FROM #{model.default_table} WHERE #{key} IS NOT NULL"
184
- model.connection.execute(sql).map { |r| r[key.to_s] }
185
- rescue ActiveRecord::StatementInvalid
186
- []
187
- end
67
+ # --- Shared Helpers (Accesibles por Maintenance) ---
188
68
 
189
- # Asegura que Rails haya cargado los modelos de infraestructura.
190
- # Vital en modo Development donde la carga es perezosa (Lazy Loading).
69
+ # Fuerza la carga de los modelos de partición en entornos con Lazy Loading (Dev).
191
70
  def ensure_models_loaded!
192
71
  return unless defined?(Rails)
193
72
 
194
73
  partition_dir = Rails.root.join("app/models/partition")
195
74
  return unless Dir.exist?(partition_dir)
196
75
 
197
- # Forzamos la carga de todos los archivos en app/models/partition/
198
76
  Dir[partition_dir.join("**/*.rb")].each { |file| require_dependency file }
199
77
  end
200
78
 
201
- # Helpers de Logging
79
+ # Retorna todas las subclases de TenantPartition::Base cargadas en memoria.
80
+ # @return [Array<Class>] Lista de clases de modelos.
81
+ def partitionable_models
82
+ ObjectSpace.each_object(Class).select { |klass| klass < TenantPartition::Base }
83
+ end
84
+
85
+ # --- Logging ---
86
+
202
87
  def log_info(tag, msg) = logger&.info(format_log(tag, msg))
203
88
  def log_warn(tag, msg) = logger&.warn(format_log(tag, msg))
204
89
  def log_error(tag, msg) = logger&.error(format_log(tag, msg))
90
+ def format_log(tag, msg) = "[TenantPartition] [#{tag}] #{msg}"
91
+ def logger = (defined?(Rails) ? Rails.logger : Logger.new($stdout))
205
92
 
206
- def format_log(tag, msg)
207
- "[TenantPartition] [#{tag}] #{msg}"
93
+ private
94
+
95
+ def process_creation(model, partition_id)
96
+ if model.exists?(partition_id)
97
+ log_info "SKIP", "#{model.name}: La partición ya existe."
98
+ else
99
+ model.create(partition_id)
100
+ log_info "OK", "#{model.name}: Partición creada exitosamente."
101
+ end
208
102
  end
209
103
 
210
- def logger
211
- defined?(Rails) ? Rails.logger : Logger.new($stdout)
104
+ def process_destruction(model, partition_id)
105
+ if model.exists?(partition_id)
106
+ if model.find(partition_id).destroy
107
+ log_info "DROP", "#{model.name}: Partición eliminada."
108
+ else
109
+ log_error "FAIL", "#{model.name}: No se pudo eliminar la partición."
110
+ end
111
+ else
112
+ log_info "SKIP", "#{model.name}: No existe partición para borrar."
113
+ end
212
114
  end
213
115
  end
214
116
  end
metadata CHANGED
@@ -1,22 +1,25 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tenant_partition
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - gabriel
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-01-27 00:00:00.000000000 Z
11
+ date: 2026-02-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: activerecord
14
+ name: activemodel
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '7.1'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '9.0'
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -24,13 +27,19 @@ dependencies:
24
27
  - - ">="
25
28
  - !ruby/object:Gem::Version
26
29
  version: '7.1'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '9.0'
27
33
  - !ruby/object:Gem::Dependency
28
- name: activemodel
34
+ name: activerecord
29
35
  requirement: !ruby/object:Gem::Requirement
30
36
  requirements:
31
37
  - - ">="
32
38
  - !ruby/object:Gem::Version
33
39
  version: '7.1'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '9.0'
34
43
  type: :runtime
35
44
  prerelease: false
36
45
  version_requirements: !ruby/object:Gem::Requirement
@@ -38,6 +47,9 @@ dependencies:
38
47
  - - ">="
39
48
  - !ruby/object:Gem::Version
40
49
  version: '7.1'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '9.0'
41
53
  description: Framework de infraestructura para Rails 7.1+ que automatiza el particionamiento
42
54
  nativo (List Partitioning). Incluye soporte para Composite Primary Keys, orquestación
43
55
  de tenants y migraciones zero-downtime.
@@ -51,11 +63,16 @@ files:
51
63
  - LICENSE.txt
52
64
  - README.md
53
65
  - Rakefile
66
+ - lib/generators/tenant_partition/api_controller_generator.rb
67
+ - lib/generators/tenant_partition/templates/README
68
+ - lib/generators/tenant_partition/templates/tenant_partitions_controller.rb.erb
54
69
  - lib/tenant_partition.rb
55
70
  - lib/tenant_partition/base.rb
56
71
  - lib/tenant_partition/concerns/controller.rb
72
+ - lib/tenant_partition/concerns/data_mover.rb
57
73
  - lib/tenant_partition/concerns/partitioned.rb
58
74
  - lib/tenant_partition/configuration.rb
75
+ - lib/tenant_partition/maintenance.rb
59
76
  - lib/tenant_partition/railtie.rb
60
77
  - lib/tenant_partition/safety_guard.rb
61
78
  - lib/tenant_partition/schema/statements.rb