From 57860f167dbb36813148708ea97c936804d837a1 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 25 Aug 2026 09:00:00 +0200 Subject: [PATCH] feat(orm): add migrations with schema definitions and migrator --- lib/prism/orm/migration.rb | 60 ++++++++++++++++++++++++++++++++++++++ lib/prism/orm/migrator.rb | 60 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 lib/prism/orm/migration.rb create mode 100644 lib/prism/orm/migrator.rb diff --git a/lib/prism/orm/migration.rb b/lib/prism/orm/migration.rb new file mode 100644 index 0000000..a85669a --- /dev/null +++ b/lib/prism/orm/migration.rb @@ -0,0 +1,60 @@ +module Prism + module Orm + class Migration + class TableDefinition + attr_reader :columns + + def initialize + @columns = ['id'] + end + + def column(name, _type = :string) + @columns << name.to_s + end + + %i[string text integer float boolean datetime].each do |type| + define_method(type) { |name| column(name, type) } + end + + def timestamps + column(:created_at) + column(:updated_at) + end + end + + def initialize(store) + @store = store + end + + def up + raise NotImplementedError, "#{self.class} must implement #up" + end + + def down + raise NotImplementedError, "#{self.class} must implement #down" + end + + private + + attr_reader :store + + def create_table(name) + definition = TableDefinition.new + yield(definition) if block_given? + store.create_table(name, definition.columns) + end + + def drop_table(name) + store.drop_table(name) + end + + def add_column(table, column, _type = :string, default: nil) + store.add_column(table, column, default: default) + end + + def remove_column(table, column) + store.remove_column(table, column) + end + end + end +end diff --git a/lib/prism/orm/migrator.rb b/lib/prism/orm/migrator.rb new file mode 100644 index 0000000..7dd9d04 --- /dev/null +++ b/lib/prism/orm/migrator.rb @@ -0,0 +1,60 @@ +module Prism + module Orm + class Migrator + Entry = Struct.new(:version, :name, :path) + + def initialize(store, migrations_path) + @store = store + @migrations_path = migrations_path + end + + def entries + Dir.glob(File.join(@migrations_path, '*.rb')).sort.map do |path| + version, name = File.basename(path, '.rb').split('_', 2) + Entry.new(version, name, path) + end + end + + def pending + applied = @store.applied_migrations + entries.reject { |entry| applied.include?(entry.version) } + end + + def migrate! + applied = pending.map { |entry| apply(entry) } + applied.map(&:version) + end + + def rollback!(steps = 1) + versions = @store.applied_migrations.sort.last(steps).reverse + versions.map do |version| + entry = entries.find { |candidate| candidate.version == version } + next unless entry + revert(entry) + entry.version + end.compact + end + + private + + def apply(entry) + migration = load_migration(entry) + migration.up + @store.record_migration(entry.version) + entry + end + + def revert(entry) + migration = load_migration(entry) + migration.down + @store.forget_migration(entry.version) + end + + def load_migration(entry) + class_name = Inflector.camelize(entry.name) + load entry.path + Object.const_get(class_name).new(@store) + end + end + end +end