Class: Familia::Migration::Base Abstract

Inherits:
Object
  • Object
show all
Defined in:
lib/familia/migration/base.rb

Overview

This class is abstract.

Subclass and implement #migration_needed? and #migrate

Base class for Familia data migrations providing common infrastructure for idempotent data transformations and configuration updates.

Unlike traditional database migrations, these migrations:

  • Don't track execution state in a migrations table
  • Use #migration_needed? to detect if changes are required
  • Support both dry-run and actual execution modes
  • Provide built-in statistics tracking and logging

Subclassing Requirements

Subclasses must implement these methods:

Subclasses may override:

  • #prepare - Initialize and validate migration parameters
  • #down - Rollback logic for reversible migrations

Usage Patterns

For simple data migrations, extend Base directly:

class ConfigurationMigration < Familia::Migration::Base self.migration_id = '20260131_120000_config_update'

def migration_needed?
  !redis.exists('config:new_feature_flag')
end

def migrate
  for_realsies_this_time? do
    redis.set('config:new_feature_flag', 'true')
  end
  track_stat(:settings_updated)
end

end

For record-by-record processing, use Model. For bulk updates with Redis pipelining, use Pipeline.

CLI Usage

ConfigurationMigration.cli_run # Dry run (preview) ConfigurationMigration.cli_run(['--run']) # Actual execution

Direct Known Subclasses

Model

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Base

Initialize new migration instance with default state



150
151
152
153
# File 'lib/familia/migration/base.rb', line 150

def initialize(options = {})
  @options = options
  @stats   = Hash.new(0)  # Auto-incrementing counter for tracking migration stats
end

Class Attribute Details

.dependenciesArray<String>

List of migration IDs that must run before this one

Returns:

  • (Array<String>)


68
69
70
# File 'lib/familia/migration/base.rb', line 68

def dependencies
  @dependencies
end

.descriptionString

Human-readable description of what this migration does

Returns:

  • (String)


64
65
66
# File 'lib/familia/migration/base.rb', line 64

def description
  @description
end

.migration_idString

Unique identifier for this migration

Returns:

  • (String)

    format: timestamp_snake_case_name



60
61
62
# File 'lib/familia/migration/base.rb', line 60

def migration_id
  @migration_id
end

Instance Attribute Details

#optionsHash

CLI options passed to migration, typically { run: true/false }

Returns:

  • (Hash)

    the options hash



143
144
145
# File 'lib/familia/migration/base.rb', line 143

def options
  @options
end

#statsHash (readonly)

Migration statistics for tracking operations performed

Returns:

  • (Hash)

    auto-incrementing counters for named statistics



147
148
149
# File 'lib/familia/migration/base.rb', line 147

def stats
  @stats
end

Class Method Details

.check_onlyInteger

Check-only mode for programmatic use

Returns exit code indicating whether migration is needed. Does not perform any migration work.

Returns:

  • (Integer)

    0 if no migration needed, 1 if migration needed



116
117
118
119
120
# File 'lib/familia/migration/base.rb', line 116

def check_only
  migration = new
  migration.prepare
  migration.migration_needed? ? 1 : 0
end

.cli_run(argv = ARGV) ⇒ Integer

CLI entry point for migration execution

Handles command-line argument parsing and returns appropriate exit codes. This is the recommended entry point for migration scripts.

Examples:

In migration script

if __FILE__ == $0
  exit(MyMigration.cli_run)
end

Parameters:

  • argv (Array<String>) (defaults to: ARGV)

    command-line arguments (default: ARGV)

Returns:

  • (Integer)

    exit code (0 = success, 1 = error/action required)



99
100
101
102
103
104
105
106
107
108
# File 'lib/familia/migration/base.rb', line 99

def cli_run(argv = ARGV)
  if argv.include?('--check')
    check_only
  else
    result = run(run: argv.include?('--run'))
    # nil (not needed) and true (success) both return 0
    # only false (failure) returns 1
    result == false ? 1 : 0
  end
end

.inherited(subclass) ⇒ Object

Auto-registration hook called when a subclass is defined. Registers the migration with Familia::Migration.migrations.

Parameters:

  • subclass (Class)

    The inheriting class



74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/familia/migration/base.rb', line 74

def inherited(subclass)
  super
  subclass.dependencies ||= []

  # Only register named classes (skip anonymous classes)
  # Use respond_to? to handle load-order edge cases where migrations
  # array may not yet be defined (e.g., Model class loading during require)
  return if subclass.name.nil?
  return unless Familia::Migration.respond_to?(:migrations)

  Familia::Migration.migrations << subclass
end

.run(options = {}) ⇒ Boolean?

Main entry point for migration execution

Orchestrates the full migration process including preparation, conditional execution based on #migration_needed?, and cleanup.

Parameters:

  • options (Hash) (defaults to: {})

    CLI options, typically { run: true/false }

Returns:

  • (Boolean, nil)

    true if migration completed successfully, nil if not needed, false if failed



130
131
132
133
134
135
136
137
138
# File 'lib/familia/migration/base.rb', line 130

def run(options = {})
  migration         = new
  migration.options = options
  migration.prepare

  return migration.handle_migration_not_needed unless migration.migration_needed?

  migration.migrate
end

Instance Method Details

#actual_run?Boolean

Check if migration is running in actual execution mode

Returns:

  • (Boolean)

    true if changes will be applied



216
217
218
# File 'lib/familia/migration/base.rb', line 216

def actual_run?
  options[:run]
end

#dbclientRedis (protected) Also known as: redis

Access to database client

Provides a database connection for migrations that need to access data outside of Familia models.

Returns:

  • (Redis)

    configured Redis connection



439
440
441
# File 'lib/familia/migration/base.rb', line 439

def dbclient
  @dbclient ||= Familia.dbclient
end

#debug(message = nil) ⇒ void

This method returns an undefined value.

Log debug message

Parameters:

  • message (String) (defaults to: nil)

    message to log



291
292
293
# File 'lib/familia/migration/base.rb', line 291

def debug(message = nil)
  Familia.logger.debug { message } if message
end

#downObject

Optional rollback logic. Override in subclass to support reversible migrations.



195
196
197
# File 'lib/familia/migration/base.rb', line 195

def down
  # Override in subclass for rollback support
end

#dry_run?Boolean

Check if migration is running in dry-run mode

Returns:

  • (Boolean)

    true if no changes should be made



210
211
212
# File 'lib/familia/migration/base.rb', line 210

def dry_run?
  !options[:run]
end

#dry_run_only? { ... } ⇒ Boolean

Execute block only in dry run mode

Use this for dry-run specific logging or validation.

Yields:

  • Block to execute if in dry run mode

Returns:

  • (Boolean)

    true if block was executed, false if skipped



248
249
250
251
252
253
# File 'lib/familia/migration/base.rb', line 248

def dry_run_only?
  return false unless dry_run?

  yield if block_given?
  true
end

#error(message = nil) ⇒ void

This method returns an undefined value.

Log error message

Parameters:

  • message (String) (defaults to: nil)

    message to log



305
306
307
# File 'lib/familia/migration/base.rb', line 305

def error(message = nil)
  Familia.logger.error { message } if message
end

#for_realsies_this_time? { ... } ⇒ Boolean

Execute block only in actual run mode

Use this to wrap code that makes actual changes to the system. In dry-run mode, the block will not be executed.

Yields:

  • Block to execute if in actual run mode

Returns:

  • (Boolean)

    true if block was executed, false if skipped



235
236
237
238
239
240
# File 'lib/familia/migration/base.rb', line 235

def for_realsies_this_time?
  return false unless actual_run?

  yield if block_given?
  true
end

#handle_migration_not_needednil

Handle case where migration is not needed

Called automatically when #migration_needed? returns false. Provides standard messaging about migration state.

Returns:

  • (nil)


355
356
357
358
359
360
361
# File 'lib/familia/migration/base.rb', line 355

def handle_migration_not_needed
  info('')
  info('Migration needed? false.')
  info('')
  info('This usually means that the migration has already been applied.')
  nil
end

#header(message) ⇒ void

This method returns an undefined value.

Print formatted header with separator lines

Parameters:

  • message (String)

    header text to display



275
276
277
278
279
# File 'lib/familia/migration/base.rb', line 275

def header(message)
  info ''
  info separator
  info(message.upcase)
end

#info(message = nil) ⇒ void

This method returns an undefined value.

Log informational message

Parameters:

  • message (String) (defaults to: nil)

    message to log



284
285
286
# File 'lib/familia/migration/base.rb', line 284

def info(message = nil)
  Familia.logger.info { message } if message
end

#migrateBoolean

This method is abstract.

Subclasses must implement this method

Perform actual migration work

This is the core migration logic that subclasses must implement. Use #for_realsies_this_time? to wrap actual changes and #track_stat to record operations performed.

Returns:

  • (Boolean)

    true if migration succeeded

Raises:

  • (NotImplementedError)

    if not implemented by subclass



176
177
178
# File 'lib/familia/migration/base.rb', line 176

def migrate
  raise NotImplementedError, "#{self.class} must implement #migrate"
end

#migration_needed?Boolean

This method is abstract.

Subclasses must implement this method

Detect if migration needs to run

This method should implement idempotency logic by checking current system state and returning false if migration has already been applied or is not needed.

Returns:

  • (Boolean)

    true if migration should proceed

Raises:

  • (NotImplementedError)

    if not implemented by subclass



189
190
191
# File 'lib/familia/migration/base.rb', line 189

def migration_needed?
  raise NotImplementedError, "#{self.class} must implement #migration_needed?"
end

#preparevoid

This method returns an undefined value.

Hook for subclass initialization and validation

Override this method to:

  • Set instance variables needed by the migration
  • Validate prerequisites and configuration
  • Initialize connections or external dependencies


163
164
165
# File 'lib/familia/migration/base.rb', line 163

def prepare
  debug('Preparing migration - default implementation')
end

This method returns an undefined value.

Display migration summary with custom content block

Automatically adjusts header based on run mode and yields the current mode to the block for conditional content.

Parameters:

  • title (String, nil) (defaults to: nil)

    custom summary title

Yields:

  • (Symbol)

    :dry_run or :actual_run for conditional content



339
340
341
342
343
344
345
346
347
# File 'lib/familia/migration/base.rb', line 339

def print_summary(title = nil)
  if dry_run?
    header(title || 'DRY RUN SUMMARY')
    yield(:dry_run) if block_given?
  else
    header(title || 'ACTUAL RUN SUMMARY')
    yield(:actual_run) if block_given?
  end
end

#progress(current, total, message = 'Processing', step = 100) ⇒ void

This method returns an undefined value.

Progress indicator for long operations

Displays progress updates at specified intervals to avoid overwhelming the log output during bulk operations.

Parameters:

  • current (Integer)

    current item number

  • total (Integer)

    total items to process

  • message (String) (defaults to: 'Processing')

    operation description

  • step (Integer) (defaults to: 100)

    progress reporting frequency (default 100)



325
326
327
328
329
# File 'lib/familia/migration/base.rb', line 325

def progress(current, total, message = 'Processing', step = 100)
  return unless current % step == 0 || current == total

  info "#{message} #{current}/#{total}..."
end

#reversible?Boolean

Check if this migration has rollback support.

Returns:

  • (Boolean)

    true if down method is overridden



202
203
204
# File 'lib/familia/migration/base.rb', line 202

def reversible?
  method(:down).owner != Familia::Migration::Base
end

#run_mode_bannervoid

This method returns an undefined value.

Display run mode banner with appropriate warnings



222
223
224
225
226
# File 'lib/familia/migration/base.rb', line 222

def run_mode_banner
  header("Running in #{dry_run? ? 'DRY RUN' : 'ACTUAL RUN'} mode")
  info(dry_run? ? 'No changes will be made' : 'Changes WILL be applied to the database')
  info(separator)
end

#schema_validation_enabled?Boolean

Check if schema validation is enabled for this migration

Schema validation is enabled by default when SchemaRegistry is loaded. Use #skip_schema_validation! to disable for this migration instance.

Returns:

  • (Boolean)


417
418
419
# File 'lib/familia/migration/base.rb', line 417

def schema_validation_enabled?
  @schema_validation != false && Familia::SchemaRegistry.loaded?
end

#separatorString

Generate separator line for visual formatting

Returns:

  • (String)

    dash separator line



311
312
313
# File 'lib/familia/migration/base.rb', line 311

def separator
  '-' * 60
end

#skip_schema_validation!void

This method returns an undefined value.

Disable schema validation for this migration

Call this in #prepare or at any point before validation to skip all schema validation for this migration run.



427
428
429
# File 'lib/familia/migration/base.rb', line 427

def skip_schema_validation!
  @schema_validation = false
end

#track_stat(key, increment = 1) ⇒ nil

Increment named counter for migration statistics

Use this to track operations, errors, skipped records, etc. Statistics are automatically displayed in migration summaries.

Parameters:

  • key (Symbol)

    stat name to increment

  • increment (Integer) (defaults to: 1)

    amount to add (default 1)

Returns:

  • (nil)


265
266
267
268
# File 'lib/familia/migration/base.rb', line 265

def track_stat(key, increment = 1)
  @stats[key] += increment
  nil
end

#validate_schema(obj, context: nil) ⇒ Hash

Validate an object against its schema

Uses the SchemaRegistry to validate an object's data against its registered JSON schema. Returns validation results without raising exceptions.

Parameters:

  • obj (Object)

    object with to_h method

  • context (String, nil) (defaults to: nil)

    context for error messages (e.g., 'before transform')

Returns:

  • (Hash)

    { valid: Boolean, errors: Array }



374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
# File 'lib/familia/migration/base.rb', line 374

def validate_schema(obj, context: nil)
  return { valid: true, errors: [] } unless schema_validation_enabled?

  klass_name = obj.class.name
  data = obj.respond_to?(:to_h) ? obj.to_h : obj

  result = Familia::SchemaRegistry.validate(klass_name, data)

  unless result[:valid]
    context_msg = context ? " (#{context})" : ''
    warn "Schema validation failed for #{klass_name}#{context_msg}: #{result[:errors].size} error(s)"
    result[:errors].first(3).each do |e|
      debug "  - #{e['type'] || 'error'}: #{e['data_pointer'] || '/'}"
    end
  end

  result
end

#validate_schema!(obj, context: nil) ⇒ true

Validate an object or raise SchemaValidationError

Uses the SchemaRegistry to validate an object's data against its registered JSON schema. Raises an exception if validation fails.

Parameters:

  • obj (Object)

    object with to_h method

  • context (String, nil) (defaults to: nil)

    context for error messages

Returns:

  • (true)

    if valid

Raises:



402
403
404
405
406
407
408
409
# File 'lib/familia/migration/base.rb', line 402

def validate_schema!(obj, context: nil)
  result = validate_schema(obj, context: context)
  unless result[:valid]
    raise Familia::SchemaValidationError.new(result[:errors])
  end

  true
end

#warn(message = nil) ⇒ void

This method returns an undefined value.

Log warning message

Parameters:

  • message (String) (defaults to: nil)

    message to log



298
299
300
# File 'lib/familia/migration/base.rb', line 298

def warn(message = nil)
  Familia.logger.warn { message } if message
end