mini_record 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +36 -18
- data/lib/mini_record/auto_schema.rb +91 -7
- data/lib/mini_record/version.rb +1 -1
- data/spec/mini_record_spec.rb +36 -9
- data/spec/models.rb +44 -0
- metadata +6 -7
- data/lib/mini_record/auto_migrations.rb +0 -108
- data/spec/fake_model.rb +0 -21
data/README.md
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
MiniRecord is micro extension for our `ActiveRecord` gem.
|
1
|
+
MiniRecord is a micro extension for our `ActiveRecord` gem.
|
2
2
|
With it you can add the ability to create columns outside the default schema, directly
|
3
3
|
in your **model** in a similar way that you just know in others projects
|
4
4
|
like DataMapper or MongoMapper.
|
5
5
|
|
6
|
-
My inspiration come from this handy [project](https://github.com/pjhyett/auto_migrations)
|
6
|
+
My inspiration come from this handy [project](https://github.com/pjhyett/auto_migrations).
|
7
7
|
|
8
8
|
## Features
|
9
9
|
|
@@ -38,14 +38,33 @@ class Person < ActiveRecord::Base
|
|
38
38
|
end
|
39
39
|
belongs_to :address
|
40
40
|
end
|
41
|
+
Person.auto_upgrade!
|
41
42
|
|
43
|
+
# you can use also this way
|
42
44
|
class Address < ActiveRecord::Base
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
end
|
45
|
+
key.string :city
|
46
|
+
key.string :state
|
47
|
+
key.integer :number
|
48
|
+
has_many :people
|
48
49
|
end
|
50
|
+
|
51
|
+
# or this
|
52
|
+
class Address < ActiveRecord::Base
|
53
|
+
col.string :city
|
54
|
+
col.string :state
|
55
|
+
col.integer :number
|
56
|
+
has_many :people
|
57
|
+
end
|
58
|
+
|
59
|
+
# or this
|
60
|
+
class Address < ActiveRecord::Base
|
61
|
+
property.string :city
|
62
|
+
property.string :state
|
63
|
+
property.integer :number
|
64
|
+
has_many :people
|
65
|
+
end
|
66
|
+
|
67
|
+
Address.auto_upgrade!
|
49
68
|
```
|
50
69
|
|
51
70
|
Once you bootstrap your **app**, missing columns and tables will be created on the fly.
|
@@ -63,6 +82,14 @@ class Person < ActiveRecord::Base
|
|
63
82
|
end
|
64
83
|
belongs_to :address
|
65
84
|
end
|
85
|
+
|
86
|
+
# or if you use others ways
|
87
|
+
class Person < ActiveRecord::Base
|
88
|
+
col.string :name
|
89
|
+
col.string :surname # <<- this
|
90
|
+
col.integer :address_id
|
91
|
+
belongs_to :address
|
92
|
+
end
|
66
93
|
```
|
67
94
|
|
68
95
|
So now when you start your **webserver** you can see an `ALTER TABLE` statement, this mean that your existing
|
@@ -72,21 +99,12 @@ records are happy and safe.
|
|
72
99
|
|
73
100
|
It's exactly the same, but the column will be _really_ deleted without affect other columns.
|
74
101
|
|
75
|
-
###
|
102
|
+
### Change columns
|
76
103
|
|
77
104
|
It's not possible for us know when/what column you have renamed, but we can know if you changed the `type` so
|
78
105
|
if you change `t.string :name` to `t.text :name` we are be able to perform an `ALTER TABLE`
|
79
106
|
|
80
|
-
|
81
|
-
|
82
|
-
You can do it by hand but if yours are lazy like mine you can simply invoke:
|
83
|
-
|
84
|
-
``` rb
|
85
|
-
ActiveRecord::Base.drop_unused_tables
|
86
|
-
ActiveRecord::Base.drop_unused_indexes
|
87
|
-
```
|
88
|
-
|
89
|
-
# Warning
|
107
|
+
## Warnings
|
90
108
|
|
91
109
|
This software is not yet tested in a production project, now is only heavy development and if you can
|
92
110
|
pleas fork it, find bug add a spec and then come back with a pull request. Thanks!
|
@@ -1,19 +1,103 @@
|
|
1
|
-
require 'mini_record/auto_migrations'
|
2
|
-
|
3
1
|
module MiniRecord
|
4
2
|
module AutoSchema
|
5
3
|
def self.included(base)
|
6
4
|
base.extend(ClassMethods)
|
7
|
-
base.send(:include, MiniRecord::AutoMigrations)
|
8
5
|
end
|
9
6
|
|
10
7
|
module ClassMethods
|
11
|
-
def
|
12
|
-
|
13
|
-
|
8
|
+
def table_definition
|
9
|
+
@_table_definition ||= begin
|
10
|
+
tb = ActiveRecord::ConnectionAdapters::TableDefinition.new(connection)
|
11
|
+
tb.primary_key(primary_key)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
alias :col :table_definition
|
15
|
+
alias :key :table_definition
|
16
|
+
alias :property :table_definition
|
17
|
+
|
18
|
+
def reset_table_definition!
|
19
|
+
@_table_definition = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def schema
|
23
|
+
reset_table_definition!
|
24
|
+
yield table_definition
|
14
25
|
end
|
15
26
|
alias :keys :schema
|
16
27
|
alias :properties :schema
|
17
|
-
|
28
|
+
|
29
|
+
def auto_upgrade!
|
30
|
+
# Table doesn't exist, create it
|
31
|
+
unless connection.tables.include?(table_name)
|
32
|
+
# TODO: Add to create_table options
|
33
|
+
class << connection; attr_accessor :table_definition; end unless connection.respond_to?(:table_definition=)
|
34
|
+
connection.table_definition = table_definition
|
35
|
+
connection.create_table(table_name)
|
36
|
+
connection.table_definition = ActiveRecord::ConnectionAdapters::TableDefinition.new(connection)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Grab database columns
|
40
|
+
fields_in_db = connection.columns(table_name).inject({}) do |hash, column|
|
41
|
+
hash[column.name] = column
|
42
|
+
hash
|
43
|
+
end
|
44
|
+
|
45
|
+
# Grab new schema
|
46
|
+
fields_in_schema = table_definition.columns.inject({}) do |hash, column|
|
47
|
+
hash[column.name.to_s] = column
|
48
|
+
hash
|
49
|
+
end
|
50
|
+
|
51
|
+
# Remove fields from db no longer in schema
|
52
|
+
(fields_in_db.keys - fields_in_schema.keys & fields_in_db.keys).each do |field|
|
53
|
+
column = fields_in_db[field]
|
54
|
+
connection.remove_column table_name, column.name
|
55
|
+
end
|
56
|
+
|
57
|
+
# Add fields to db new to schema
|
58
|
+
(fields_in_schema.keys - fields_in_db.keys).each do |field|
|
59
|
+
column = fields_in_schema[field]
|
60
|
+
options = {:limit => column.limit, :precision => column.precision, :scale => column.scale}
|
61
|
+
options[:default] = column.default if !column.default.nil?
|
62
|
+
options[:null] = column.null if !column.null.nil?
|
63
|
+
connection.add_column table_name, column.name, column.type.to_sym, options
|
64
|
+
end
|
65
|
+
|
66
|
+
# Change attributes of existent columns
|
67
|
+
(fields_in_schema.keys & fields_in_db.keys).each do |field|
|
68
|
+
if field != primary_key #ActiveRecord::Base.get_primary_key(table_name)
|
69
|
+
changed = false # flag
|
70
|
+
new_type = fields_in_schema[field].type.to_sym
|
71
|
+
new_attr = {}
|
72
|
+
|
73
|
+
# First, check if the field type changed
|
74
|
+
if fields_in_schema[field].type.to_sym != fields_in_db[field].type.to_sym
|
75
|
+
changed = true
|
76
|
+
end
|
77
|
+
|
78
|
+
# Special catch for precision/scale, since *both* must be specified together
|
79
|
+
# Always include them in the attr struct, but they'll only get applied if changed = true
|
80
|
+
new_attr[:precision] = fields_in_schema[field][:precision]
|
81
|
+
new_attr[:scale] = fields_in_schema[field][:scale]
|
82
|
+
|
83
|
+
# Next, iterate through our extended attributes, looking for any differences
|
84
|
+
# This catches stuff like :null, :precision, etc
|
85
|
+
fields_in_schema[field].each_pair do |att,value|
|
86
|
+
next if att == :type or att == :base or att == :name # special cases
|
87
|
+
if !value.nil? && value != fields_in_db[field].send(att)
|
88
|
+
new_attr[att] = value
|
89
|
+
changed = true
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Change the column if applicable
|
94
|
+
connection.change_column table_name, field, new_type, new_attr if changed
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Reload column information
|
99
|
+
reset_column_information
|
100
|
+
end
|
101
|
+
end # ClassMethods
|
18
102
|
end # AutoSchema
|
19
103
|
end # MiniRecord
|
data/lib/mini_record/version.rb
CHANGED
data/spec/mini_record_spec.rb
CHANGED
@@ -1,13 +1,16 @@
|
|
1
1
|
require File.expand_path('../spec_helper.rb', __FILE__)
|
2
|
-
require File.expand_path('../
|
2
|
+
require File.expand_path('../models.rb', __FILE__)
|
3
3
|
|
4
4
|
describe MiniRecord do
|
5
5
|
|
6
|
-
it '
|
6
|
+
it 'has #schema inside model' do
|
7
7
|
# For unknown reason separate specs doesn't works
|
8
|
+
ActiveRecord::Base.connection.table_exists?(Person.table_name).must_equal false
|
9
|
+
Person.auto_upgrade!
|
8
10
|
Person.table_name.must_equal 'people'
|
9
|
-
Person.db_columns.must_equal %w[id name]
|
11
|
+
Person.db_columns.sort.must_equal %w[id name]
|
10
12
|
Person.column_names.must_equal Person.db_columns
|
13
|
+
Person.column_names.must_equal Person.schema_columns
|
11
14
|
person = Person.create(:name => 'foo')
|
12
15
|
person.name.must_equal 'foo'
|
13
16
|
proc { person.surname }.must_raise NoMethodError
|
@@ -19,12 +22,13 @@ describe MiniRecord do
|
|
19
22
|
p.string :surname
|
20
23
|
end
|
21
24
|
end
|
25
|
+
Person.auto_upgrade!
|
22
26
|
Person.count.must_equal 1
|
23
27
|
person = Person.last
|
24
28
|
person.name.must_equal 'foo'
|
25
29
|
person.surname.must_be_nil
|
26
30
|
person.update_attribute(:surname, 'bar')
|
27
|
-
Person.db_columns.must_equal %w[id name surname]
|
31
|
+
Person.db_columns.sort.must_equal %w[id name surname]
|
28
32
|
Person.column_names.must_equal Person.db_columns
|
29
33
|
|
30
34
|
# Remove a column without lost data
|
@@ -33,11 +37,13 @@ describe MiniRecord do
|
|
33
37
|
p.string :name
|
34
38
|
end
|
35
39
|
end
|
40
|
+
Person.auto_upgrade!
|
36
41
|
person = Person.last
|
37
42
|
person.name.must_equal 'foo'
|
38
43
|
proc { person.surname }.must_raise NoMethodError
|
39
|
-
Person.db_columns.must_equal %w[id name]
|
44
|
+
Person.db_columns.sort.must_equal %w[id name]
|
40
45
|
Person.column_names.must_equal Person.db_columns
|
46
|
+
Person.column_names.must_equal Person.schema_columns
|
41
47
|
|
42
48
|
# Change column without lost data
|
43
49
|
Person.class_eval do
|
@@ -49,9 +55,30 @@ describe MiniRecord do
|
|
49
55
|
person.name.must_equal 'foo'
|
50
56
|
end
|
51
57
|
|
52
|
-
it '
|
53
|
-
ActiveRecord::Base.
|
54
|
-
ActiveRecord::Base.connection.
|
55
|
-
|
58
|
+
it 'has #key,col,property inside model' do
|
59
|
+
ActiveRecord::Base.connection.table_exists?(Post.table_name).must_equal false
|
60
|
+
ActiveRecord::Base.connection.table_exists?(Category.table_name).must_equal false
|
61
|
+
Post.auto_upgrade!; Category.auto_upgrade!
|
62
|
+
Post.column_names.must_equal Post.db_columns
|
63
|
+
Category.column_names.must_equal Category.schema_columns
|
64
|
+
|
65
|
+
# Check default properties
|
66
|
+
category = Category.create(:title => 'category')
|
67
|
+
post = Post.create(:title => 'foo', :body => 'bar', :category_id => category.id)
|
68
|
+
post = Post.first
|
69
|
+
post.title.must_equal 'foo'
|
70
|
+
post.body.must_equal 'bar'
|
71
|
+
post.category.must_equal category
|
72
|
+
|
73
|
+
# Remove a column
|
74
|
+
Post.reset_table_definition!
|
75
|
+
Post.class_eval do
|
76
|
+
key.string :name
|
77
|
+
key.references :category
|
78
|
+
end
|
79
|
+
Post.auto_upgrade!
|
80
|
+
post = Post.first
|
81
|
+
post.name.must_be_nil
|
82
|
+
post.category.must_equal category
|
56
83
|
end
|
57
84
|
end
|
data/spec/models.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
|
4
|
+
# ActiveRecord::Base.connection.tables.each { |t| ActiveRecord::Base.connection.drop_table(t) }
|
5
|
+
# ActiveRecord::Base.logger = Logger.new($stdout)
|
6
|
+
|
7
|
+
module SpecHelper
|
8
|
+
def self.included(base)
|
9
|
+
base.extend(ClassMethods)
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def db_columns
|
14
|
+
connection.columns(table_name).map(&:name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def schema_columns
|
18
|
+
table_definition.columns.map { |c| c.name.to_s }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Person < ActiveRecord::Base
|
24
|
+
include SpecHelper
|
25
|
+
schema do |s|
|
26
|
+
s.string :name
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Post < ActiveRecord::Base
|
31
|
+
include SpecHelper
|
32
|
+
|
33
|
+
key.string :title
|
34
|
+
key.string :body
|
35
|
+
key.references :category
|
36
|
+
belongs_to :category
|
37
|
+
end
|
38
|
+
|
39
|
+
class Category < ActiveRecord::Base
|
40
|
+
include SpecHelper
|
41
|
+
|
42
|
+
key.string :title
|
43
|
+
has_many :posts
|
44
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mini_record
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 27
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
- 0
|
9
8
|
- 1
|
10
|
-
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Davide D'Agostino
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-09-
|
18
|
+
date: 2011-09-06 00:00:00 +02:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -52,13 +52,12 @@ files:
|
|
52
52
|
- README.md
|
53
53
|
- Rakefile
|
54
54
|
- lib/mini_record.rb
|
55
|
-
- lib/mini_record/auto_migrations.rb
|
56
55
|
- lib/mini_record/auto_schema.rb
|
57
56
|
- lib/mini_record/version.rb
|
58
57
|
- mini_record.gemspec
|
59
58
|
- rakefile.compiled.rbc
|
60
|
-
- spec/fake_model.rb
|
61
59
|
- spec/mini_record_spec.rb
|
60
|
+
- spec/models.rb
|
62
61
|
- spec/spec_helper.rb
|
63
62
|
has_rdoc: true
|
64
63
|
homepage: https://github.com/DAddYE/mini_record
|
@@ -95,6 +94,6 @@ signing_key:
|
|
95
94
|
specification_version: 3
|
96
95
|
summary: MiniRecord is a micro gem that allow you to write schema inside your model as you can do in DataMapper.
|
97
96
|
test_files:
|
98
|
-
- spec/fake_model.rb
|
99
97
|
- spec/mini_record_spec.rb
|
98
|
+
- spec/models.rb
|
100
99
|
- spec/spec_helper.rb
|
@@ -1,108 +0,0 @@
|
|
1
|
-
require 'active_support/core_ext/class/attribute_accessors'
|
2
|
-
|
3
|
-
module MiniRecord
|
4
|
-
|
5
|
-
module AutoMigrations
|
6
|
-
def self.included(base)
|
7
|
-
base.extend ClassMethods
|
8
|
-
class << base
|
9
|
-
cattr_accessor :tables_in_schema, :indexes_in_schema
|
10
|
-
self.tables_in_schema, self.indexes_in_schema = [], []
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
module ClassMethods
|
15
|
-
def auto_create_table(table_name, options, &block)
|
16
|
-
|
17
|
-
(self.tables_in_schema ||= []) << table_name
|
18
|
-
|
19
|
-
# Table doesn't exist, create it
|
20
|
-
unless connection.tables.include?(table_name)
|
21
|
-
return connection.create_table(table_name, options, &block)
|
22
|
-
end
|
23
|
-
|
24
|
-
# Grab database columns
|
25
|
-
fields_in_db = connection.columns(table_name).inject({}) do |hash, column|
|
26
|
-
hash[column.name] = column
|
27
|
-
hash
|
28
|
-
end
|
29
|
-
|
30
|
-
# Grab schema columns (lifted from active_record/connection_adapters/abstract/schema_statements.rb)
|
31
|
-
table_definition = ActiveRecord::ConnectionAdapters::TableDefinition.new(connection)
|
32
|
-
primary_key = options[:primary_key] || "id"
|
33
|
-
table_definition.primary_key(primary_key) unless options[:id] == false
|
34
|
-
|
35
|
-
# Return the table definition
|
36
|
-
yield table_definition
|
37
|
-
|
38
|
-
# Grab new schema
|
39
|
-
fields_in_schema = table_definition.columns.inject({}) do |hash, column|
|
40
|
-
hash[column.name.to_s] = column
|
41
|
-
hash
|
42
|
-
end
|
43
|
-
|
44
|
-
# Remove fields from db no longer in schema
|
45
|
-
(fields_in_db.keys - fields_in_schema.keys & fields_in_db.keys).each do |field|
|
46
|
-
column = fields_in_db[field]
|
47
|
-
connection.remove_column table_name, column.name
|
48
|
-
end
|
49
|
-
|
50
|
-
# Add fields to db new to schema
|
51
|
-
(fields_in_schema.keys - fields_in_db.keys).each do |field|
|
52
|
-
column = fields_in_schema[field]
|
53
|
-
options = {:limit => column.limit, :precision => column.precision, :scale => column.scale}
|
54
|
-
options[:default] = column.default if !column.default.nil?
|
55
|
-
options[:null] = column.null if !column.null.nil?
|
56
|
-
connection.add_column table_name, column.name, column.type.to_sym, options
|
57
|
-
end
|
58
|
-
|
59
|
-
# Change attributes of existent columns
|
60
|
-
(fields_in_schema.keys & fields_in_db.keys).each do |field|
|
61
|
-
if field != primary_key #ActiveRecord::Base.get_primary_key(table_name)
|
62
|
-
changed = false # flag
|
63
|
-
new_type = fields_in_schema[field].type.to_sym
|
64
|
-
new_attr = {}
|
65
|
-
|
66
|
-
# First, check if the field type changed
|
67
|
-
if fields_in_schema[field].type.to_sym != fields_in_db[field].type.to_sym
|
68
|
-
changed = true
|
69
|
-
end
|
70
|
-
|
71
|
-
# Special catch for precision/scale, since *both* must be specified together
|
72
|
-
# Always include them in the attr struct, but they'll only get applied if changed = true
|
73
|
-
new_attr[:precision] = fields_in_schema[field][:precision]
|
74
|
-
new_attr[:scale] = fields_in_schema[field][:scale]
|
75
|
-
|
76
|
-
# Next, iterate through our extended attributes, looking for any differences
|
77
|
-
# This catches stuff like :null, :precision, etc
|
78
|
-
fields_in_schema[field].each_pair do |att,value|
|
79
|
-
next if att == :type or att == :base or att == :name # special cases
|
80
|
-
if !value.nil? && value != fields_in_db[field].send(att)
|
81
|
-
new_attr[att] = value
|
82
|
-
changed = true
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
# Change the column if applicable
|
87
|
-
connection.change_column table_name, field, new_type, new_attr if changed
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
def drop_unused_tables
|
93
|
-
(connection.tables - tables_in_schema - %w(schema_info schema_migrations)).each do |table|
|
94
|
-
connection.drop_table table
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
def drop_unused_indexes
|
99
|
-
tables_in_schema.each do |table_name|
|
100
|
-
indexes_in_db = connection.indexes(table_name).map(&:name)
|
101
|
-
(indexes_in_db - indexes_in_schema & indexes_in_db).each do |index_name|
|
102
|
-
connection.remove_index table_name, :name => index_name
|
103
|
-
end
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end # ClassMethods
|
107
|
-
end # Migrations
|
108
|
-
end # ActiveKey
|
data/spec/fake_model.rb
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
require 'logger'
|
2
|
-
|
3
|
-
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
|
4
|
-
# ActiveRecord::Base.connection.tables.each { |t| ActiveRecord::Base.connection.drop_table(t) }
|
5
|
-
# ActiveRecord::Base.logger = Logger.new($stdout)
|
6
|
-
|
7
|
-
class Person < ActiveRecord::Base
|
8
|
-
schema do |s|
|
9
|
-
s.string :name
|
10
|
-
end
|
11
|
-
|
12
|
-
# Testing purpose
|
13
|
-
def self.db_columns
|
14
|
-
connection.columns(table_name).map(&:name)
|
15
|
-
end
|
16
|
-
|
17
|
-
def self.schema_columns
|
18
|
-
table_definition = ActiveRecord::ConnectionAdapters::TableDefinition.new(connection)
|
19
|
-
table_definition.columns.map(&:name)
|
20
|
-
end
|
21
|
-
end
|