mini_record 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|