Permalink
Browse files

use update/insert statements for copy commands

  • Loading branch information...
1 parent e6274b6 commit 078e18ddb123d2865b72e67cc883516b061d88a7 Sven Fuchs committed Aug 15, 2011
View
@@ -15,6 +15,7 @@
# t.copy :all, :except => :foo
# t.exec 'UPDATE foo ...'
+ # t.set :source, 'github'
end
end
@@ -30,3 +31,4 @@
drop_table :tests
end
end
+
View
@@ -8,13 +8,10 @@ module DataMigrations
autoload :Migration, 'data_migrations/migration'
autoload :Table, 'data_migrations/table'
- extend ActiveSupport::Concern
-
- module ClassMethods
- def migrate_table(name, options, &block)
- Migration.new(name, options, &block).run!
- end
+ def migrate_table(name, options, &block)
+ Migration.new(name, options, &block).run!
end
+ alias :migrate_data :migrate_table
- ActiveRecord::Migration.send(:include, self)
+ ActiveRecord::Migration.send(:extend, self)
end
@@ -2,16 +2,12 @@ module DataMigrations
class Column
attr_reader :table, :name, :alias
- def initialize(table, name, alias_)
+ def initialize(table, name, alias_ = nil)
@table = table
@name = name
@alias = alias_
end
- def aliased
- self.alias ? "#{quote(name)} AS #{quote(self.alias)}" : quote(name)
- end
-
def definition
[quoted_alias_name, type].join(' ')
end
@@ -21,19 +17,31 @@ def type
end
def column
- @column ||= table.column(name)
+ table.column(name)
+ end
+
+ def aliased_name
+ self.alias.present? ? "#{quote(name)} AS #{quote(self.alias)}" : quote(name)
end
def quoted_name
quote(name)
end
def quoted_alias_name
- quote(self.alias || name)
+ quote(self.alias.present? ? self.alias : name)
+ end
+
+ def quote_value(value)
+ table.connection.quote(value, column)
end
def quote(name)
- ActiveRecord::Base.connection.quote_column_name(name)
+ table.connection.quote_column_name(name)
+ end
+
+ def ==(other)
+ name == other.name
end
end
end
@@ -1,8 +1,9 @@
module DataMigrations
class Instruction
- autoload :Base, 'data_migrations/instruction/base'
- autoload :Copy, 'data_migrations/instruction/copy'
- autoload :Exec, 'data_migrations/instruction/exec'
- autoload :Move, 'data_migrations/instruction/move'
+ autoload :Base, 'data_migrations/instruction/base'
+ autoload :Copy, 'data_migrations/instruction/copy'
+ autoload :Exec, 'data_migrations/instruction/exec'
+ autoload :Remove, 'data_migrations/instruction/remove'
+ autoload :Set, 'data_migrations/instruction/set'
end
end
@@ -1,6 +1,8 @@
module DataMigrations
class Instruction
class Base
+ attr_accessor :columns, :options
+
delegate :connection, :to => :'ActiveRecord::Base'
delegate :condition, :to => :migration
@@ -10,6 +12,12 @@ def initialize(migration)
@migration = migration
end
+ def columns=(columns)
+ @columns = normalize_columns(columns).map do |column, alias_|
+ Column.new(migration.source, column, alias_)
+ end
+ end
+
def execute
statements.each do |statement|
puts "Executing: #{statement}"
@@ -26,6 +34,37 @@ def source
def target
migration.target
end
+
+ def alias_names
+ columns.map(&:quoted_alias_name).join(', ')
+ end
+
+ def alias_setters
+ columns.map { |column| "#{column.quoted_alias_name} = source.#{column.quoted_name}" }.join(', ')
+ end
+
+ def quoted_column_names
+ columns.map(&:quoted_name).join(', ')
+ end
+
+ def aliased_column_names
+ columns.map(&:aliased_name).join(', ')
+ end
+
+ def column_definitions
+ columns.map(&:definition).join(', ')
+ end
+
+ def normalize_columns(columns)
+ columns = columns.map(&:to_s)
+ columns = source.column_names - instructed_columns if columns.include?('all')
+ columns -= Array(options[:except]).map(&:to_s) if options[:except]
+ columns.zip(Array(options[:to]).map(&:to_s))
+ end
+
+ def instructed_columns
+ migration.instructions.map(&:columns).flatten.map(&:name)
+ end
end
end
end
@@ -1,51 +1,38 @@
module DataMigrations
class Instruction
class Copy < Base
- attr_reader :columns, :options
-
def initialize(migration, *columns)
super(migration)
- @options = columns.extract_options!
- @columns = normalize_columns(columns).map do |column, alias_|
- Column.new(migration.source, column, alias_)
- end
+ self.options = columns.extract_options!
+ options[:to] = [nil].concat(Array(options[:to]) || [])
+ self.columns = ['id'].concat(columns)
end
def statements
- [insert_statement]
+ [update_statement, insert_statement]
end
protected
- def insert_statement
- statement = "INSERT INTO #{target.quoted_name} (#{alias_names})"
- statement << " SELECT #{aliased_column_names} FROM #{source.quoted_name}"
- statement << " WHERE #{condition}" if condition
- # statement << "AS t(#{column_definitions})"
+ def update_statement
+ statement = "UPDATE #{target.quoted_name} SET #{alias_setters} FROM ("
+ statement << "SELECT #{quoted_column_names} FROM #{source.quoted_name} "
+ statement << "WHERE #{source.quoted_name}.id IN (SELECT id FROM #{target.quoted_name})"
+ statement << " AND #{condition}" if condition
+ statement << ") AS source WHERE #{target.quoted_name}.id = source.id"
statement
end
- def alias_names
- columns.map(&:quoted_alias_name).join(', ')
- end
-
- def aliased_column_names
- columns.map(&:aliased).join(', ')
- end
-
- def column_definitions
- columns.map(&:definition).join(', ')
- end
-
- def normalize_columns(columns)
- columns = columns.map(&:to_s)
- columns = source.column_names - instructed_columns if columns.include?('all')
- columns -= Array(options[:except]).map(&:to_s) if options[:except]
- columns.zip(Array(options.delete(:to)).map(&:to_s))
+ def insert_statement
+ statement = "INSERT INTO #{target.quoted_name} (#{alias_names}) "
+ statement << "SELECT #{aliased_column_names} FROM #{source.quoted_name} "
+ statement << "WHERE #{source.quoted_name}.id NOT IN (SELECT id FROM #{target.quoted_name})"
+ statement << " AND #{condition}" if condition
+ statement
end
def instructed_columns
- migration.instructions.map(&:columns).flatten.map(&:name)
+ super - ['id']
end
end
end
@@ -1,13 +1,19 @@
module DataMigrations
class Instruction
- class Move < Copy
+ class Remove < Base
+ def initialize(migration, *columns)
+ super(migration)
+ self.options = columns.extract_options!
+ self.columns = columns
+ end
+
def statements
- super + drop_statements
+ remove_statements
end
protected
- def drop_statements
+ def remove_statements
columns.map do |column|
"ALTER TABLE #{source.quoted_name} DROP COLUMN #{column.quoted_name}"
end
@@ -0,0 +1,23 @@
+module DataMigrations
+ class Instruction
+ class Set < Base
+ attr_reader :column, :value
+
+ def initialize(migration, column, value)
+ super(migration)
+ @column = Column.new(migration.target, column)
+ @value = value
+ end
+
+ def statements
+ [update_statement]
+ end
+
+ protected
+
+ def update_statement
+ "UPDATE #{target.quoted_name} SET #{column.quoted_name} = #{column.quote_value(value)}"
+ end
+ end
+ end
+end
@@ -19,12 +19,17 @@ def condition(condition = nil)
end
alias :where :condition
+ def copy(*args)
+ instructions << Instruction::Copy.new(self, *args)
+ end
+
def move(*args)
- instructions << Instruction::Move.new(self, *args)
+ copy(*args)
+ instructions << Instruction::Remove.new(self, *args)
end
- def copy(*args)
- instructions << Instruction::Copy.new(self, *args)
+ def set(*args)
+ instructions << Instruction::Set.new(self, *args)
end
def exec(*args)
@@ -0,0 +1,39 @@
+module DataMigrations
+ module Setup
+ def setup_data_migrations
+ unless @setup
+ install_upsert
+ @setup = true
+ end
+ end
+
+ def install_upsert
+ ActiveRecord::Base.connection.execute <<-sql
+ CREATE FUNCTION upsert (sql_update TEXT, sql_insert TEXT)
+ RETURNS VOID
+ LANGUAGE plpgsql
+ AS $$
+ BEGIN
+ LOOP
+ -- first try to update
+ EXECUTE sql_update;
+ -- check if the row is found
+ IF FOUND THEN
+ RETURN;
+ END IF;
+ -- not found so insert the row
+ BEGIN
+ EXECUTE sql_insert;
+ RETURN;
+ EXCEPTION WHEN unique_violation THEN
+ -- do nothing and loop
+ END;
+ END LOOP;
+ END;
+ $$;
+ sql
+ rescue ActiveRecord::StatementInvalid
+ # ignore duplicate installs
+ end
+ end
+end
Oops, something went wrong.

0 comments on commit 078e18d

Please sign in to comment.