Permalink
Browse files

initial commit

  • Loading branch information...
0 parents commit 6842d014433218f6398b31c0455a40e9dedcb3b5 @svenfuchs committed Aug 11, 2011
Showing with 451 additions and 0 deletions.
  1. +18 −0 Gemfile
  2. +59 −0 Gemfile.lock
  3. +21 −0 LICENSE
  4. +21 −0 README.md
  5. +10 −0 Rakefile
  6. +23 −0 data_migrations.gemspec
  7. +173 −0 lib/data_migrations.rb
  8. +3 −0 lib/data_migrations/version.rb
  9. +65 −0 test/data_migrations_test.rb
  10. +24 −0 test/database.yml.backup
  11. +34 −0 test/test_helper.rb
18 Gemfile
@@ -0,0 +1,18 @@
+source :rubygems
+
+gemspec
+
+group :development, :test do
+ platforms :mri_18 do
+ # required as linecache uses it but does not have it as a dep
+ gem "require_relative", "~> 1.0.1"
+ gem 'ruby-debug'
+ end
+
+ unless RUBY_VERSION == '1.9.3' && RUBY_PLATFORM !~ /darwin/
+ # will need to install ruby-debug19 manually for 1.9.3:
+ # gem install ruby-debug19 -- --with-ruby-include=$rvm_path/src/ruby-1.9.3-preview1
+ gem 'ruby-debug19', :platforms => :mri_19
+ end
+end
+
59 Gemfile.lock
@@ -0,0 +1,59 @@
+PATH
+ remote: .
+ specs:
+ data_migrations (0.0.1)
+ activerecord
+
+GEM
+ remote: http://rubygems.org/
+ specs:
+ activemodel (3.0.9)
+ activesupport (= 3.0.9)
+ builder (~> 2.1.2)
+ i18n (~> 0.5.0)
+ activerecord (3.0.9)
+ activemodel (= 3.0.9)
+ activesupport (= 3.0.9)
+ arel (~> 2.0.10)
+ tzinfo (~> 0.3.23)
+ activesupport (3.0.9)
+ archive-tar-minitar (0.5.2)
+ arel (2.0.10)
+ builder (2.1.2)
+ columnize (0.3.4)
+ i18n (0.5.0)
+ linecache (0.46)
+ rbx-require-relative (> 0.0.4)
+ linecache19 (0.5.12)
+ ruby_core_source (>= 0.1.4)
+ pg (0.11.0)
+ rbx-require-relative (0.0.5)
+ require_relative (1.0.2)
+ ruby-debug (0.10.4)
+ columnize (>= 0.1)
+ ruby-debug-base (~> 0.10.4.0)
+ ruby-debug-base (0.10.4)
+ linecache (>= 0.3)
+ ruby-debug-base19 (0.11.25)
+ columnize (>= 0.3.1)
+ linecache19 (>= 0.5.11)
+ ruby_core_source (>= 0.1.4)
+ ruby-debug19 (0.11.6)
+ columnize (>= 0.3.1)
+ linecache19 (>= 0.5.11)
+ ruby-debug-base19 (>= 0.11.19)
+ ruby_core_source (0.1.5)
+ archive-tar-minitar (>= 0.5.2)
+ test_declarative (0.0.5)
+ tzinfo (0.3.29)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ data_migrations!
+ pg
+ require_relative (~> 1.0.1)
+ ruby-debug
+ ruby-debug19
+ test_declarative
21 LICENSE
@@ -0,0 +1,21 @@
+MIT LICENSE
+
+Copyright (c) Sven Fuchs <svenfuchs@artweb-design.de>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
21 README.md
@@ -0,0 +1,21 @@
+# data\_migrations
+
+ class CreateRepositories < ActiveRecord::Migration
+ def self.up
+ create_table :tests do |t|
+ # ...
+ end
+
+ migrate_table :builds, :to => :tests |t|
+ t.where 'parent_id IS NULL'
+ t.move :number
+ t.copy :result, :to => :status
+ t.copy :commit, :result, :to => [:hash, :status]
+ t.copy :all, :except => :foo
+ t.exec 'UPDATE foo ...'
+ end
+ end
+
+ def self.down
+ end
+ end
10 Rakefile
@@ -0,0 +1,10 @@
+require 'rake'
+require 'rake/testtask'
+
+Rake::TestTask.new do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = false
+end
+
+task :default => :test
23 data_migrations.gemspec
@@ -0,0 +1,23 @@
+# encoding: utf-8
+
+$:.unshift File.expand_path('../lib', __FILE__)
+require 'data_migrations/version'
+
+Gem::Specification.new do |s|
+ s.name = "data_migrations"
+ s.version = DataMigrations::VERSION
+ s.authors = ["Sven Fuchs"]
+ s.email = "svenfuchs@artweb-design.de"
+ s.homepage = "https://github.com/svenfuchs/data_migrations"
+ s.summary = "[summary]"
+ s.description = "[description]"
+
+ s.files = Dir['{lib/**/*,[A-Z]*}']
+ s.platform = Gem::Platform::RUBY
+ s.require_path = 'lib'
+ s.rubyforge_project = '[none]'
+
+ s.add_dependency 'activerecord'
+ s.add_development_dependency 'test_declarative'
+ s.add_development_dependency 'pg'
+end
173 lib/data_migrations.rb
@@ -0,0 +1,173 @@
+require 'active_record'
+require 'active_support/core_ext/module/delegation'
+require 'active_support/core_ext/array/extract_options'
+
+module DataMigrations
+ class Definition
+ attr_reader :source, :target, :instructions
+
+ def initialize(source, options = {})
+ @source = Table.new(source)
+ @target = Table.new(options[:to])
+ @instructions = []
+
+ yield self
+ end
+
+ def condition(condition = nil)
+ condition ? @condition = condition : @condition
+ end
+ alias :where :condition
+
+ def move(*columns)
+ options = columns.extract_options!
+ instructions << Move.new(self, columns, options)
+ end
+
+ def copy(*columns)
+ options = columns.extract_options!
+ instructions << Copy.new(self, columns, options)
+ end
+
+ def exec(sql)
+ instructions << Exec.new(self, sql)
+ end
+ end
+
+ class Table
+ delegate :connection, :to => :'ActiveRecord::Base'
+
+ attr_reader :name
+
+ def initialize(name)
+ @name = name
+ end
+
+ def columns
+ @columns ||= connection.columns(name)
+ end
+
+ def column(name)
+ columns.detect { |column| column.name.to_s == name.to_s } || raise_column_not_found(name)
+ end
+
+ def quoted_name
+ connection.quote_table_name(name)
+ end
+
+ def raise_column_not_found(name)
+ raise "could not find column #{name} on #{self.name}"
+ end
+ end
+
+ class Column
+ attr_reader :table, :name, :alias
+
+ def initialize(table, name, alias_)
+ @table = table
+ @name = name
+ @alias = alias_
+ end
+
+ def aliased
+ self.alias ? "#{quote(name)} AS #{quote(self.alias)}" : quote(name)
+ end
+
+ def definition
+ [quote(self.alias || name), type].join(' ')
+ end
+
+ def type
+ column.sql_type
+ end
+
+ def column
+ @column ||= table.column(name)
+ end
+
+ def quoted_name
+ quote(name)
+ end
+
+ def quote(name)
+ ActiveRecord::Base.connection.quote_column_name(name)
+ end
+ end
+
+ class Instruction
+ delegate :connection, :to => :'ActiveRecord::Base'
+ delegate :condition, :to => :definition
+
+ attr_reader :definition
+
+ def initialize(definition)
+ @definition = definition
+ end
+
+ def execute
+ statements.each { |statement| connection.execute(statement) }
+ end
+
+ def source
+ definition.source
+ end
+
+ def target
+ definition.target
+ end
+
+ def columns=(columns)
+ @columns = columns.map { |column, alias_| Column.new(definition.source, column, alias_) }
+ end
+
+ def aliased_column_names
+ columns.map(&:aliased).join(', ')
+ end
+
+ def column_definitions
+ columns.map(&:definition).join(', ')
+ end
+ end
+
+ class Copy < Instruction
+ attr_reader :columns, :options
+
+ def initialize(definition, columns, options)
+ super(definition)
+ self.columns = columns.zip(Array(options[:to]))
+ end
+
+ def statements
+ [insert_statement]
+ end
+
+ def insert_statement
+ statement = "INSERT INTO #{target.quoted_name} SELECT #{aliased_column_names} FROM #{source.quoted_name} "
+ statement << "WHERE #{condition} " if condition
+ statement << "AS t(#{column_definitions})"
+ end
+ end
+
+ class Move < Copy
+ def statements
+ super + drop_statements
+ end
+
+ def drop_statements
+ columns.map do |column|
+ "ALTER TABLE #{source.quoted_name} DROP COLUMN #{column.quoted_name}"
+ end
+ end
+ end
+
+ def Runner
+ attr_reader :definition
+
+ def initialize(definition)
+ @definition = definition
+ end
+
+ def run!
+ end
+ end
+end
3 lib/data_migrations/version.rb
@@ -0,0 +1,3 @@
+module DataMigrations
+ VERSION = "0.0.1"
+end
65 test/data_migrations_test.rb
@@ -0,0 +1,65 @@
+require 'test_helper'
+
+class Test::Unit::TestCase
+ def define(&block)
+ DataMigrations::Definition.new(:builds, :to => :tasks, &block)
+ end
+
+ test 'copy: generates an INSERT INTO ... SELECT FROM statement (for a single column)' do
+ definition = define { |t| t.copy :commit }
+ statement = definition.instructions.first.statements.first
+ assert_equal 'INSERT INTO "tasks" SELECT "commit" FROM "builds" AS t("commit" character varying(255))', statement
+ end
+
+ test 'copy: generates an INSERT INTO ... SELECT FROM statement (for a multiple columns)' do
+ definition = define { |t| t.copy :parent_id, :commit, :status }
+ statement = definition.instructions.first.statements.first
+ assert_equal 'INSERT INTO "tasks" SELECT "parent_id", "commit", "status" FROM "builds" AS t("parent_id" integer, "commit" character varying(255), "status" integer)', statement
+ end
+
+ test 'copy: aliases column names if specified (for a single column)' do
+ definition = define { |t| t.copy :commit, :to => :hash }
+ statement = definition.instructions.first.statements.first
+ assert_equal 'INSERT INTO "tasks" SELECT "commit" AS "hash" FROM "builds" AS t("hash" character varying(255))', statement
+ end
+
+ test 'copy: aliases column names if specified (for a multiple columns)' do
+ definition = define { |t| t.copy :parent_id, :commit, :status, :to => [:owner_id, :hash, :result] }
+ statement = definition.instructions.first.statements.first
+ assert_equal 'INSERT INTO "tasks" SELECT "parent_id" AS "owner_id", "commit" AS "hash", "status" AS "result" FROM "builds" AS t("owner_id" integer, "hash" character varying(255), "result" integer)', statement
+ end
+
+ test 'copy: includes a condition if given' do
+ definition = define { |t| t.where 'status = 1'; t.copy :commit }
+ statement = definition.instructions.first.statements.first
+ assert_equal 'INSERT INTO "tasks" SELECT "commit" FROM "builds" WHERE status = 1 AS t("commit" character varying(255))', statement
+ end
+
+ test 'move: drops the columns after copying them (for a single column)' do
+ definition = define { |t| t.move :commit }
+ statement = definition.instructions.first.statements
+ assert_equal 'ALTER TABLE "builds" DROP COLUMN "commit"', statement[1]
+ end
+
+ test 'move: drops the columns after copying them (for a single column, with aliases)' do
+ definition = define { |t| t.move :commit, :to => :hash }
+ statement = definition.instructions.first.statements
+ assert_equal 'ALTER TABLE "builds" DROP COLUMN "commit"', statement[1]
+ end
+
+ test 'move: drops the columns after copying them (for multiple columns)' do
+ definition = define { |t| t.move :parent_id, :commit, :status }
+ statement = definition.instructions.first.statements
+ [:parent_id, :commit, :status].each_with_index do |column, ix|
+ assert_equal %(ALTER TABLE "builds" DROP COLUMN "#{column}"), statement[ix + 1]
+ end
+ end
+
+ test 'move: drops the columns after copying them (for multiple columns, with aliases)' do
+ definition = define { |t| t.move :parent_id, :commit, :status, :to => [:status, :result, :owner_id] }
+ statement = definition.instructions.first.statements
+ [:parent_id, :commit, :status].each_with_index do |column, ix|
+ assert_equal %(ALTER TABLE "builds" DROP COLUMN "#{column}"), statement[ix + 1]
+ end
+ end
+end
24 test/database.yml.backup
@@ -0,0 +1,24 @@
+defaults: &defaults
+ username: sns
+ password: sns
+ database: sns
+ encoding: utf8
+
+postgresql:
+ <<: *defaults
+ adapter: postgresql
+ host: 127.0.0.1
+ port: 5432
+ min_messages: notice
+
+mysql:
+ <<: *defaults
+ adapter: mysql2
+
+mysql2:
+ <<: *defaults
+ adapter: mysql2
+
+sqlite3:
+ adapter: sqlite3
+ database: ':memory:'
34 test/test_helper.rb
@@ -0,0 +1,34 @@
+require 'logger'
+require 'bundler/setup'
+require 'test/unit'
+require 'test_declarative'
+require 'data_migrations'
+
+log = '/tmp/data_migrations.log'
+FileUtils.touch(log) unless File.exists?(log)
+ActiveRecord::Base.logger = Logger.new(log)
+
+adapter = ENV['ADAPTER'] || 'postgresql'
+
+
+db_config = begin
+ db_configs = YAML.load_file(File.expand_path('../database.yml', __FILE__)).symbolize_keys
+ db_configs[adapter.to_sym].symbolize_keys
+rescue Errno::ENOENT => e
+ { :adapter => 'postgresql', :database => 'data_migrations_test' }
+end
+
+puts "Running tests against #{adapter}"
+ActiveRecord::Base.establish_connection(db_config)
+
+
+ActiveRecord::Schema.define(:version => 1) do
+ create_table :builds, :force => true do |t|
+ t.integer :parent_id
+ t.integer :status
+ t.string :commit
+ t.string :author_name
+ end
+end unless ActiveRecord::Base.connection.table_exists?(:builds)
+
+

0 comments on commit 6842d01

Please sign in to comment.