Skip to content
Browse files

First commit.

  • Loading branch information...
0 parents commit a84097e3be4ea6cd60cd5856c9e4407a85f149f9 @DAddYE DAddYE committed Sep 5, 2011
4 .gitignore
@@ -0,0 +1,4 @@
+*.gem
+.bundle
+Gemfile.lock
+pkg/*
6 Gemfile
@@ -0,0 +1,6 @@
+source "http://rubygems.org"
+
+# Specify your gem's dependencies in mini_record.gemspec
+gem "minitest"
+gem "sqlite3"
+gemspec
111 README.md
@@ -0,0 +1,111 @@
+MiniRecord is micro extension for our dear ActiveRecord.
+With it you can add the ability to create columns outside the schema, directly
+in your **model** in a similar way that you just know in others projects
+like DataMapper or MongoMapper.
+
+My inspiration come from this handy project: https://github.com/pjhyett/auto_migrations
+
+## Features
+
+* Define columns/properties inside your model
+* Perform migrations automatically
+* Auto upgrade your schema, so if you know what you are doing you don't lost your existing data!
+* Add, Remove, Change Columns; Add, Remove, Change indexes
+
+## Instructions
+
+What you need is to move/remove `db/migrations` and `db/schema.rb`.
+It's no more necessary and avoid conflicts.
+
+Add to your gemfile
+
+gem 'mini_record'
+
+That's all!
+
+## Examples
+
+Remember that inside properties you can use all migrations methods,
+see [documentation](http://api.rubyonrails.org/classes/ActiveRecord/Migration.html)
+
+``` rb
+class Person < ActiveRecord::Base
+ properties do |t|
+ t.string :name
+ t.integer :address_id
+ end
+ belongs_to :address
+end
+
+class Address < ActiveRecord::Base
+ properties, :id => true do |t| # id => true is not really necessary but as
+ t.string :city # in +create_table+ you have here the same options
+ t.string :state
+ t.integer :number
+ end
+end
+```
+
+Once you bootstrap your app, missing columns and tables will be created on the fly.
+
+### Adding a new column
+
+Super easy, open your model and just add it:
+
+``` rb
+class Person < ActiveRecord::Base
+ properties do |t|
+ t.string :name
+ t.string :surname # <<- this
+ t.integer :address_id
+ end
+ belongs_to :address
+end
+```
+
+So now when you start your webserver you can see an `ALTER TABLE` statement, this mean that your existing
+records are happy and safe.
+
+### Removing a column
+
+It's exactly the same, but the column will be _really_ deleted without affect other columns.
+
+### Changing a column
+
+It's not possible for us know when/what column you have renamed, but we can know if you changed the `type` so
+if you change `t.string :name` to `t.text :name` we are be able to perform an `ALTER TABLE`
+
+### Drop unused tables/indexes
+
+You can do it by hand but if yours are lazy like mine you can simply invoke:
+
+``` rb
+ActiveRecord::Base.drop_unused_tables
+ActiveRecord::Base.drop_unused_indexes
+```
+
+# Warning
+
+This software is not tested in a production project, now is only heavy development and if you can
+pleas fork it, find bug add a spec and then come back with a pull request. Thanks!
+
+
+## Author
+
+DAddYE, you can follow me on twitter [@daddye](http://twitter.com/daddye) or take a look at my site [daddye.it](http://www.daddye.it)
+
+## Copyright
+
+Copyright (C) 2011 Davide D'Agostino - [@daddye](http://twitter.com/daddye)
+
+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 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.
12 Rakefile
@@ -0,0 +1,12 @@
+require "bundler/gem_tasks"
+require "rake"
+require "rake/testtask"
+
+Rake::TestTask.new(:test) do |test|
+ test.libs << 'spec'
+ test.test_files = Dir['spec/**/*_spec.rb']
+ test.verbose = true
+end
+
+task :default => :test
+task :spec => :test
5 lib/mini_record.rb
@@ -0,0 +1,5 @@
+require 'rubygems' unless defined?(Gem)
+require 'active_record'
+require 'mini_record/properties'
+
+ActiveRecord::Base.send(:include, MiniRecord::Properties)
108 lib/mini_record/auto_migrations.rb
@@ -0,0 +1,108 @@
+require 'active_support/core_ext/class/attribute_accessors'
+
+module MiniRecord
+
+ module AutoMigrations
+ def self.included(base)
+ base.extend ClassMethods
+ class << base
+ cattr_accessor :tables_in_schema, :indexes_in_schema
+ self.tables_in_schema, self.indexes_in_schema = [], []
+ end
+ end
+
+ module ClassMethods
+ def auto_create_table(table_name, options, &block)
+
+ (self.tables_in_schema ||= []) << table_name
+
+ # Table doesn't exist, create it
+ unless connection.tables.include?(table_name)
+ return connection.create_table(table_name, options, &block)
+ end
+
+ # Grab database columns
+ fields_in_db = connection.columns(table_name).inject({}) do |hash, column|
+ hash[column.name] = column
+ hash
+ end
+
+ # Grab schema columns (lifted from active_record/connection_adapters/abstract/schema_statements.rb)
+ table_definition = ActiveRecord::ConnectionAdapters::TableDefinition.new(connection)
+ primary_key = options[:primary_key] || "id"
+ table_definition.primary_key(primary_key) unless options[:id] == false
+
+ # Return the table definition
+ yield table_definition
+
+ # Grab new schema
+ fields_in_schema = table_definition.columns.inject({}) do |hash, column|
+ hash[column.name.to_s] = column
+ hash
+ end
+
+ # Remove fields from db no longer in schema
+ (fields_in_db.keys - fields_in_schema.keys & fields_in_db.keys).each do |field|
+ column = fields_in_db[field]
+ connection.remove_column table_name, column.name
+ end
+
+ # Add fields to db new to schema
+ (fields_in_schema.keys - fields_in_db.keys).each do |field|
+ column = fields_in_schema[field]
+ options = {:limit => column.limit, :precision => column.precision, :scale => column.scale}
+ options[:default] = column.default if !column.default.nil?
+ options[:null] = column.null if !column.null.nil?
+ connection.add_column table_name, column.name, column.type.to_sym, options
+ end
+
+ # Change attributes of existent columns
+ (fields_in_schema.keys & fields_in_db.keys).each do |field|
+ if field != primary_key #ActiveRecord::Base.get_primary_key(table_name)
+ changed = false # flag
+ new_type = fields_in_schema[field].type.to_sym
+ new_attr = {}
+
+ # First, check if the field type changed
+ if fields_in_schema[field].type.to_sym != fields_in_db[field].type.to_sym
+ changed = true
+ end
+
+ # Special catch for precision/scale, since *both* must be specified together
+ # Always include them in the attr struct, but they'll only get applied if changed = true
+ new_attr[:precision] = fields_in_schema[field][:precision]
+ new_attr[:scale] = fields_in_schema[field][:scale]
+
+ # Next, iterate through our extended attributes, looking for any differences
+ # This catches stuff like :null, :precision, etc
+ fields_in_schema[field].each_pair do |att,value|
+ next if att == :type or att == :base or att == :name # special cases
+ if !value.nil? && value != fields_in_db[field].send(att)
+ new_attr[att] = value
+ changed = true
+ end
+ end
+
+ # Change the column if applicable
+ connection.change_column table_name, field, new_type, new_attr if changed
+ end
+ end
+ end
+
+ def drop_unused_tables
+ (connection.tables - tables_in_schema - %w(schema_info schema_migrations)).each do |table|
+ connection.drop_table table
+ end
+ end
+
+ def drop_unused_indexes
+ tables_in_schema.each do |table_name|
+ indexes_in_db = connection.indexes(table_name).map(&:name)
+ (indexes_in_db - indexes_in_schema & indexes_in_db).each do |index_name|
+ connection.remove_index table_name, :name => index_name
+ end
+ end
+ end
+ end # ClassMethods
+ end # Migrations
+end # ActiveKey
18 lib/mini_record/properties.rb
@@ -0,0 +1,18 @@
+require 'mini_record/auto_migrations'
+
+module MiniRecord
+ module Properties
+ def self.included(base)
+ base.extend(ClassMethods)
+ base.send(:include, MiniRecord::AutoMigrations)
+ end
+
+ module ClassMethods
+ def properties(options={}, &block)
+ auto_create_table(table_name, options, &block)
+ reset_column_information
+ end
+ alias :keys :properties
+ end
+ end # Properties
+end # MiniRecord
3 lib/mini_record/version.rb
@@ -0,0 +1,3 @@
+module MiniRecord
+ VERSION = "0.0.1"
+end
24 mini_record.gemspec
@@ -0,0 +1,24 @@
+# -*- encoding: utf-8 -*-
+$:.push File.expand_path("../lib", __FILE__)
+require "mini_record/version"
+
+Gem::Specification.new do |s|
+ s.name = "mini_record"
+ s.version = MiniRecord::VERSION
+ s.authors = ["Davide D'Agostino"]
+ s.email = ["d.dagostino@lipsiasoft.com"]
+ s.homepage = ""
+ s.summary = %q{TODO: Write a gem summary}
+ s.description = %q{TODO: Write a gem description}
+
+ s.rubyforge_project = "mini_record"
+
+ s.files = `git ls-files`.split("\n")
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+ s.require_paths = ["lib"]
+
+ # specify any dependencies here; for example:
+ # s.add_development_dependency "rspec"
+ s.add_dependency "activerecord", "~>3.1.0"
+end
309 rakefile.compiled.rbc
@@ -0,0 +1,309 @@
+!RBIX
+16846133056282117387
+x
+M
+1
+n
+n
+x
+10
+__script__
+i
+86
+5
+7
+0
+64
+47
+49
+1
+1
+15
+5
+7
+2
+64
+47
+49
+1
+1
+15
+5
+7
+3
+64
+47
+49
+1
+1
+15
+45
+4
+5
+43
+6
+7
+7
+56
+8
+50
+9
+1
+15
+5
+44
+43
+10
+79
+49
+11
+1
+13
+7
+12
+7
+7
+49
+13
+2
+15
+47
+49
+14
+1
+15
+5
+44
+43
+10
+79
+49
+11
+1
+13
+7
+15
+7
+7
+49
+13
+2
+15
+47
+49
+14
+1
+15
+2
+11
+I
+5
+I
+0
+I
+0
+I
+0
+n
+p
+16
+s
+17
+bundler/gem_tasks
+x
+7
+require
+s
+4
+rake
+s
+13
+rake/testtask
+x
+4
+Rake
+n
+x
+8
+TestTask
+x
+4
+test
+M
+1
+p
+2
+x
+9
+for_block
+t
+n
+x
+9
+__block__
+i
+46
+57
+19
+0
+15
+20
+0
+49
+0
+0
+7
+1
+64
+49
+2
+1
+15
+20
+0
+45
+3
+4
+7
+5
+64
+49
+6
+1
+13
+18
+2
+49
+7
+1
+15
+15
+20
+0
+2
+13
+18
+2
+49
+8
+1
+15
+11
+I
+5
+I
+1
+I
+1
+I
+1
+n
+p
+9
+x
+4
+libs
+s
+4
+spec
+x
+2
+<<
+x
+3
+Dir
+n
+s
+17
+spec/**/*_spec.rb
+x
+2
+[]
+x
+11
+test_files=
+x
+8
+verbose=
+p
+9
+I
+0
+I
+5
+I
+4
+I
+6
+I
+10
+I
+7
+I
+23
+I
+8
+I
+2e
+x
+42
+/Developer/src/Extras/mini_record/rakefile
+p
+1
+x
+4
+test
+x
+3
+new
+x
+4
+Hash
+x
+16
+new_from_literal
+x
+7
+default
+x
+3
+[]=
+x
+4
+task
+x
+4
+spec
+p
+13
+I
+0
+I
+1
+I
+9
+I
+2
+I
+12
+I
+3
+I
+1b
+I
+5
+I
+28
+I
+b
+I
+3e
+I
+c
+I
+56
+x
+42
+/Developer/src/Extras/mini_record/rakefile
+p
+0
21 spec/fake_model.rb
@@ -0,0 +1,21 @@
+require 'logger'
+
+ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
+# ActiveRecord::Base.connection.tables.each { |t| ActiveRecord::Base.connection.drop_table(t) }
+# ActiveRecord::Base.logger = Logger.new($stdout)
+
+class Person < ActiveRecord::Base
+ properties do |p|
+ p.string :name
+ end
+
+ # Testing purpose
+ def self.db_columns
+ connection.columns(table_name).map(&:name)
+ end
+
+ def self.schema_columns
+ table_definition = ActiveRecord::ConnectionAdapters::TableDefinition.new(connection)
+ table_definition.columns.map(&:name)
+ end
+end
57 spec/mini_record_spec.rb
@@ -0,0 +1,57 @@
+require File.expand_path('../spec_helper.rb', __FILE__)
+require File.expand_path('../fake_model.rb', __FILE__)
+
+describe MiniRecord do
+
+ it 'works correctly' do
+ # For unknown reason separate specs doesn't works
+ Person.table_name.must_equal 'people'
+ Person.db_columns.must_equal %w[id name]
+ Person.column_names.must_equal Person.db_columns
+ person = Person.create(:name => 'foo')
+ person.name.must_equal 'foo'
+ proc { person.surname }.must_raise NoMethodError
+
+ # Add a column without lost data
+ Person.class_eval do
+ properties do |p|
+ p.string :name
+ p.string :surname
+ end
+ end
+ Person.count.must_equal 1
+ person = Person.last
+ person.name.must_equal 'foo'
+ person.surname.must_be_nil
+ person.update_attribute(:surname, 'bar')
+ Person.db_columns.must_equal %w[id name surname]
+ Person.column_names.must_equal Person.db_columns
+
+ # Remove a column without lost data
+ Person.class_eval do
+ properties do |p|
+ p.string :name
+ end
+ end
+ person = Person.last
+ person.name.must_equal 'foo'
+ proc { person.surname }.must_raise NoMethodError
+ Person.db_columns.must_equal %w[id name]
+ Person.column_names.must_equal Person.db_columns
+
+ # Change column without lost data
+ Person.class_eval do
+ properties do |p|
+ p.text :name
+ end
+ end
+ person = Person.last
+ person.name.must_equal 'foo'
+ end
+
+ it 'should remove no tables, since Person is still defined' do
+ ActiveRecord::Base.drop_unused_tables
+ ActiveRecord::Base.connection.tables.must_equal %w[people]
+ Person.count.must_equal 1
+ end
+end
4 spec/spec_helper.rb
@@ -0,0 +1,4 @@
+require 'rubygems' unless defined?(Gem)
+require 'bundler/setup'
+require 'mini_record'
+require 'minitest/autorun'

0 comments on commit a84097e

Please sign in to comment.
Something went wrong with that request. Please try again.