Class: Tina4::Migration

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

Constant Summary collapse

TRACKING_TABLE =
"tina4_migration"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(db, migrations_dir: nil) ⇒ Migration

Returns a new instance of Migration.



10
11
12
13
14
# File 'lib/tina4/migration.rb', line 10

def initialize(db, migrations_dir: nil)
  @db = db
  @migrations_dir = migrations_dir || resolve_migrations_dir
  ensure_tracking_table
end

Instance Attribute Details

#dbObject (readonly)

Returns the value of attribute db.



8
9
10
# File 'lib/tina4/migration.rb', line 8

def db
  @db
end

#migrations_dirObject (readonly)

Returns the value of attribute migrations_dir.



8
9
10
# File 'lib/tina4/migration.rb', line 8

def migrations_dir
  @migrations_dir
end

Class Method Details

.create_migration(description, migrations_dir: "migrations", kind: "sql") ⇒ Object

Create a migration file — static helper for parity with Python/Node.

Parameters:

  • description (String)

    Human-readable migration name

  • migrations_dir (String) (defaults to: "migrations")

    Directory for migration files (default: 'migrations')

  • kind (String) (defaults to: "sql")

    File kind: 'sql' or 'ruby' (default: 'sql')



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

def self.create_migration(description, migrations_dir: "migrations", kind: "sql")
  new(nil, migrations_dir: migrations_dir).create(description, kind)
end

Instance Method Details

#create(description, kind = "sql") ⇒ Object

Create a new migration file

kind="ruby" — creates timestamp_description.rb with MigrationBase subclass (default) kind="sql" — creates timestamp_description.sql + .down.sql kind="python" — alias for "ruby" (class-based scaffold for cross-framework parity)



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/tina4/migration.rb', line 68

def create(description, kind = "sql")
  FileUtils.mkdir_p(@migrations_dir)
  timestamp = Time.now.strftime("%Y%m%d%H%M%S")
  created_at = Time.now.utc.strftime("%Y-%m-%d %H:%M:%S UTC")
  safe_name = description.gsub(/[^a-z0-9]+/i, "_").downcase.gsub(/^_|_$/, "")

  # MEASURED 2026-08-06: the accepted kind differed in every framework -
  # python "python", php "php", ruby "ruby" OR "python", node "class" - and
  # NONE validated it, so create_migration(..., kind="python") produced a
  # code migration in Python and Ruby and a SILENT .sql file in PHP and
  # Node. "code" is now the canonical spelling in all four; each keeps its
  # own language name as a legacy alias; anything else raises.
  # Ruby also accepted "python", which was a copy-paste from the master
  # and is dropped: a Ruby project never wants a .py migration.
  kind = (kind || "sql").to_s.strip.downcase
  unless %w[sql code ruby].include?(kind)
    raise ArgumentError,
          "Unknown migration kind #{kind.inspect}. Use 'sql' (default) or " \
          "'code' (alias: 'ruby'). An unrecognised kind used to produce a " \
          ".sql file silently, which is why this now raises."
  end

  if %w[code ruby].include?(kind)
    filename = "#{timestamp}_#{safe_name}.rb"
    filepath = File.join(@migrations_dir, filename)

    File.write(filepath, <<~RUBY)
      # frozen_string_literal: true
      # Migration: #{description}
      # Created: #{created_at}

      class #{classify(description)} < Tina4::MigrationBase
        def up(db = nil)
          # db.execute("CREATE TABLE ...")
        end

        def down(db = nil)
          # db.execute("DROP TABLE IF EXISTS ...")
        end
      end
    RUBY

    Tina4::Log.info("Created migration: #{filename}")
    return filepath
  end

  # Default: SQL
  up_filename = "#{timestamp}_#{safe_name}.sql"
  down_filename = "#{timestamp}_#{safe_name}.down.sql"
  up_path = File.join(@migrations_dir, up_filename)
  down_path = File.join(@migrations_dir, down_filename)

  File.write(up_path, "-- Migration: #{description}\n-- Created: #{created_at}\n\n")
  File.write(down_path, "-- Rollback: #{description}\n-- Created: #{created_at}\n\n")

  Tina4::Log.info("Created migration: #{up_filename}")
  up_path
end

#get_appliedObject

Get list of applied migration records (public alias for completed_migrations)



152
153
154
# File 'lib/tina4/migration.rb', line 152

def get_applied
  completed_migrations
end

#get_applied_migrationsObject

Alias for get_applied — parity with PHP/Node



157
158
159
# File 'lib/tina4/migration.rb', line 157

def get_applied_migrations
  get_applied
end

#get_filesObject

Get all migration files on disk, excluding .down files



167
168
169
170
171
# File 'lib/tina4/migration.rb', line 167

def get_files
  migration_files = Dir.glob(File.join(@migrations_dir, "*.sql")).reject { |f| f.end_with?(".down.sql") }
  migration_files += Dir.glob(File.join(@migrations_dir, "*.rb"))
  migration_files.map { |f| File.basename(f) }.sort
end

#get_pendingObject

Get list of pending migration filenames (public alias for pending_migrations)



162
163
164
# File 'lib/tina4/migration.rb', line 162

def get_pending
  pending_migrations.map { |f| File.basename(f) }
end

#migrateObject Also known as: run

Run all pending migrations



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/tina4/migration.rb', line 17

def migrate
  pending = pending_migrations
  if pending.empty?
    Tina4::Log.info("No pending migrations")
    return []
  end

  batch = next_batch_number
  results = []
  pending.each do |file|
    result = run_migration(file, batch)
    results << result
    # Stop on failure
    break if result[:status] == "failed"
  end
  results
end

#record_migration(name, batch, passed: 1) ⇒ Object

Insert a record into the migration tracking table.

Parameters:

  • name (String)

    Migration filename (e.g. "20240101000000_create_users.sql")

  • batch (Integer)

    Batch number this migration belongs to

  • passed (Integer) (defaults to: 1)

    1 if successful (default), 0 if failed



132
133
134
# File 'lib/tina4/migration.rb', line 132

def record_migration(name, batch, passed: 1)
  _record_migration(name, batch, passed: passed)
end

#remove_migration_record(name) ⇒ Object

Delete a migration record from the tracking table by filename.

Parameters:

  • name (String)

    Migration filename to remove



139
140
141
# File 'lib/tina4/migration.rb', line 139

def remove_migration_record(name)
  _remove_migration_record(name)
end

#rollback(steps = 1) ⇒ Object

Rollback last batch (or N steps)



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/tina4/migration.rb', line 38

def rollback(steps = 1)
  completed = completed_migrations_with_batch
  return [] if completed.empty?

  # Get the last N unique batches
  batches = completed.map { |m| m[:batch] }.uniq.sort.reverse
  batches_to_rollback = batches.first(steps)

  results = []
  completed.select { |m| batches_to_rollback.include?(m[:batch]) }
           .sort_by { |m| -m[:id] }
           .each do |migration|
    result = rollback_migration(migration[:migration_name])
    results << result
  end
  results
end

#statusObject



56
57
58
59
60
61
# File 'lib/tina4/migration.rb', line 56

def status
  {
    completed: completed_migrations,
    pending: pending_migrations.map { |f| File.basename(f) }
  }
end