active_record_inline_schema 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.travis.yml +10 -0
- data/Gemfile +10 -0
- data/README.md +65 -0
- data/Rakefile +21 -0
- data/active_record_inline_schema.gemspec +25 -0
- data/lib/active_record_inline_schema.rb +5 -0
- data/lib/active_record_inline_schema/auto_schema.rb +229 -0
- data/lib/active_record_inline_schema/version.rb +3 -0
- data/spec/models.rb +93 -0
- data/spec/mysql_spec.rb +31 -0
- data/spec/postgresql_spec.rb +25 -0
- data/spec/shared_examples.rb +240 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/sqlite3_spec.rb +10 -0
- metadata +83 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# ActiveRecordInlineSchema
|
2
|
+
|
3
|
+
Define table structure (columns and indexes) inside your ActiveRecord models like you can do in migrations. Also similar to DataMapper inline schema syntax.
|
4
|
+
|
5
|
+
Specify columns like you would with ActiveRecord migrations and then run .auto_upgrade! Based on the mini_record gem from Davide D'Agostino, it adds fewer aliases, doesn't create timestamps and relationship columns automatically.
|
6
|
+
|
7
|
+
## Production use
|
8
|
+
|
9
|
+
Over 2 years in [Brighter Planet's environmental impact API](http://impact.brighterplanet.com) and [reference data service](http://data.brighterplanet.com).
|
10
|
+
|
11
|
+
Lots and lots of use in the [`earth` library](https://github.com/brighterplanet/earth).
|
12
|
+
|
13
|
+
## Examples
|
14
|
+
|
15
|
+
class Breed < ActiveRecord::Base
|
16
|
+
col :species_name
|
17
|
+
col :weight, :type => :float
|
18
|
+
col :weight_units
|
19
|
+
end
|
20
|
+
Breed.auto_upgrade!
|
21
|
+
|
22
|
+
class Airport < ActiveRecord::Base
|
23
|
+
self.primary_key = "iata_code"
|
24
|
+
belongs_to :country, :foreign_key => 'country_iso_3166_code', :primary_key => 'iso_3166_code'
|
25
|
+
col :iata_code
|
26
|
+
col :name
|
27
|
+
col :city
|
28
|
+
col :country_name
|
29
|
+
col :country_iso_3166_code
|
30
|
+
col :latitude, :type => :float
|
31
|
+
col :longitude, :type => :float
|
32
|
+
end
|
33
|
+
Airport.auto_upgrade!
|
34
|
+
|
35
|
+
class ApiResponse < ActiveRecord::Base
|
36
|
+
col :raw_body, :type => 'varbinary(16384)'
|
37
|
+
end
|
38
|
+
ApiResponse.auto_upgrade!
|
39
|
+
|
40
|
+
## Credits
|
41
|
+
|
42
|
+
Massive thanks to DAddYE, who you follow on twitter [@daddye](http://twitter.com/daddye) and look at his site at [daddye.it](http://www.daddye.it)
|
43
|
+
|
44
|
+
## TODO
|
45
|
+
|
46
|
+
* merge back into mini_record? they make some choices (automatically creating relationships, mixing in lots of aliases, etc.) that I don't need
|
47
|
+
* make the documentation as good as mini_record
|
48
|
+
|
49
|
+
## Copyright
|
50
|
+
|
51
|
+
Copyright 2012 Seamus Abshere
|
52
|
+
|
53
|
+
Adapted from [mini_record](https://github.com/DAddYE/mini_record), which is copyright 2011 Davide D'Agostino
|
54
|
+
|
55
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
56
|
+
associated documentation files (the “Software”), to deal in the Software without restriction, including without
|
57
|
+
limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
58
|
+
and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
59
|
+
|
60
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
61
|
+
|
62
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
63
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM,
|
64
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
65
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rake"
|
3
|
+
require "rake/testtask"
|
4
|
+
|
5
|
+
Rake::TestTask.new(:test) do |test|
|
6
|
+
test.libs << 'spec'
|
7
|
+
test.test_files = Dir['spec/**/*_spec.rb']
|
8
|
+
test.verbose = true
|
9
|
+
end
|
10
|
+
|
11
|
+
task :test_each_db_adapter do
|
12
|
+
%w{ mysql sqlite3 postgresql }.each do |db_adapter|
|
13
|
+
puts
|
14
|
+
puts "#{'*'*10} Running #{db_adapter} tests"
|
15
|
+
puts
|
16
|
+
puts `bundle exec rake test DB_ADAPTER=#{db_adapter} TEST=spec/#{db_adapter}_spec.rb`
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
task :default => :test_each_db_adapter
|
21
|
+
task :spec => :test_each_db_adapter
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "active_record_inline_schema/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "active_record_inline_schema"
|
7
|
+
s.version = ActiveRecordInlineSchema::VERSION
|
8
|
+
s.authors = ["Seamus Abshere", "Davide D'Agostino"]
|
9
|
+
s.email = ["seamus@abshere.net", "d.dagostino@lipsiasoft.com"]
|
10
|
+
s.homepage = "https://github.com/seamusabshere/active_record_inline_schema"
|
11
|
+
s.summary = %q{Define table structure (columns and indexes) inside your ActiveRecord models like you can do in migrations. Also similar to DataMapper inline schema syntax.}
|
12
|
+
s.description = %q{Specify columns like you would with ActiveRecord migrations and then run .auto_upgrade! Based on the mini_record gem from Davide D'Agostino, it adds fewer aliases, doesn't create timestamps and relationship columns automatically.}
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
|
19
|
+
s.add_runtime_dependency "activerecord", ">=3"
|
20
|
+
# s.add_runtime_dependency "activerecord", "3.0.12"
|
21
|
+
# s.add_runtime_dependency "activerecord", "3.1.3"
|
22
|
+
# s.add_runtime_dependency "activerecord", "3.2.2"
|
23
|
+
|
24
|
+
# dev dependencies appear to be in the Gemfile
|
25
|
+
end
|
@@ -0,0 +1,229 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
module ActiveRecordInlineSchema
|
3
|
+
module AutoSchema
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
def table_definition
|
11
|
+
return superclass.table_definition unless superclass == ActiveRecord::Base
|
12
|
+
|
13
|
+
@_table_definition ||= begin
|
14
|
+
ActiveRecord::ConnectionAdapters::TableDefinition.new(connection)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def indexes
|
19
|
+
return superclass.indexes unless superclass == ActiveRecord::Base
|
20
|
+
|
21
|
+
@_indexes ||= {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def col(*args)
|
25
|
+
return unless connection?
|
26
|
+
|
27
|
+
options = args.extract_options!
|
28
|
+
type = options.delete(:as) || options.delete(:type) || :string
|
29
|
+
args.each do |column_name|
|
30
|
+
if table_definition.respond_to?(type)
|
31
|
+
table_definition.send(type, column_name, options)
|
32
|
+
else
|
33
|
+
table_definition.column(column_name, type, options)
|
34
|
+
end
|
35
|
+
column_name = table_definition.columns[-1].name
|
36
|
+
case index_name = options.delete(:index)
|
37
|
+
when Hash
|
38
|
+
add_index(options.delete(:column) || column_name, index_name)
|
39
|
+
when TrueClass
|
40
|
+
add_index(column_name)
|
41
|
+
when String, Symbol, Array
|
42
|
+
add_index(index_name)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def reset_table_definition!
|
48
|
+
@_table_definition = nil
|
49
|
+
end
|
50
|
+
|
51
|
+
def schema
|
52
|
+
reset_table_definition!
|
53
|
+
yield table_definition
|
54
|
+
table_definition
|
55
|
+
end
|
56
|
+
|
57
|
+
def add_index(column_name, options={})
|
58
|
+
index_name = shorten_index_name connection.index_name(table_name, :column => column_name)
|
59
|
+
indexes[index_name] = options.merge(:column => column_name, :name => index_name)
|
60
|
+
index_name
|
61
|
+
end
|
62
|
+
|
63
|
+
def connection?
|
64
|
+
!!connection
|
65
|
+
rescue Exception => e
|
66
|
+
puts "\e[31m%s\e[0m" % e.message.strip
|
67
|
+
false
|
68
|
+
end
|
69
|
+
|
70
|
+
def shorten_index_name(name)
|
71
|
+
if name.length < connection.index_name_length
|
72
|
+
name
|
73
|
+
else
|
74
|
+
name[0..(connection.index_name_length-11)] + ::Zlib.crc32(name).to_s
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def sqlite?
|
79
|
+
connection.adapter_name =~ /sqlite/i
|
80
|
+
end
|
81
|
+
|
82
|
+
def mysql?
|
83
|
+
connection.adapter_name =~ /mysql/i
|
84
|
+
end
|
85
|
+
|
86
|
+
def postgresql?
|
87
|
+
connection.adapter_name =~ /postgresql/i
|
88
|
+
end
|
89
|
+
|
90
|
+
def auto_upgrade!(create_table_options = '')
|
91
|
+
return unless connection?
|
92
|
+
|
93
|
+
# normally activerecord's mysql adapter does this
|
94
|
+
if mysql?
|
95
|
+
create_table_options ||= 'ENGINE=InnoDB'
|
96
|
+
end
|
97
|
+
|
98
|
+
non_standard_primary_key = if (primary_key_column = table_definition.columns.detect { |column| column.name.to_s == primary_key.to_s })
|
99
|
+
primary_key_column.type != :primary_key
|
100
|
+
end
|
101
|
+
|
102
|
+
unless non_standard_primary_key
|
103
|
+
table_definition.column :id, :primary_key
|
104
|
+
end
|
105
|
+
|
106
|
+
# Table doesn't exist, create it
|
107
|
+
unless connection.table_exists? table_name
|
108
|
+
|
109
|
+
# avoid using connection.create_table because in 3.0.x it ignores table_definition
|
110
|
+
# and it also is too eager about adding a primary key column
|
111
|
+
create_sql = "CREATE TABLE #{quoted_table_name} (#{table_definition.to_sql}) #{create_table_options}"
|
112
|
+
|
113
|
+
if sqlite?
|
114
|
+
connection.execute create_sql
|
115
|
+
if non_standard_primary_key
|
116
|
+
add_index primary_key, :unique => true
|
117
|
+
end
|
118
|
+
elsif postgresql?
|
119
|
+
connection.execute create_sql
|
120
|
+
if non_standard_primary_key
|
121
|
+
# can't use add_index method because it won't let you do "PRIMARY KEY"
|
122
|
+
connection.execute "ALTER TABLE #{quoted_table_name} ADD PRIMARY KEY (#{quoted_primary_key})"
|
123
|
+
end
|
124
|
+
elsif mysql?
|
125
|
+
if non_standard_primary_key
|
126
|
+
# only string keys are supported
|
127
|
+
create_sql.sub! %r{#{connection.quote_column_name(primary_key)} varchar\(255\)([^,\)]*)}, "#{connection.quote_column_name(primary_key)} varchar(255)\\1 PRIMARY KEY"
|
128
|
+
create_sql.sub! 'DEFAULT NULLPRIMARY KEY', 'PRIMARY KEY'
|
129
|
+
end
|
130
|
+
connection.execute create_sql
|
131
|
+
end
|
132
|
+
|
133
|
+
if connection.respond_to?(:schema_cache)
|
134
|
+
connection.schema_cache.clear!
|
135
|
+
end
|
136
|
+
reset_column_information
|
137
|
+
end
|
138
|
+
|
139
|
+
# Add to schema inheritance column if necessary
|
140
|
+
if descendants.present? && !table_definition.columns.any? { |column| column.name.to_s == inheritance_column.to_s }
|
141
|
+
table_definition.column inheritance_column, :string
|
142
|
+
end
|
143
|
+
|
144
|
+
# Grab database columns
|
145
|
+
fields_in_db = connection.columns(table_name).inject({}) do |hash, column|
|
146
|
+
hash[column.name] = column
|
147
|
+
hash
|
148
|
+
end
|
149
|
+
|
150
|
+
# Grab new schema
|
151
|
+
fields_in_schema = table_definition.columns.inject({}) do |hash, column|
|
152
|
+
hash[column.name.to_s] = column
|
153
|
+
hash
|
154
|
+
end
|
155
|
+
|
156
|
+
# Remove fields from db no longer in schema
|
157
|
+
(fields_in_db.keys - fields_in_schema.keys & fields_in_db.keys).each do |field|
|
158
|
+
column = fields_in_db[field]
|
159
|
+
connection.remove_column table_name, column.name
|
160
|
+
end
|
161
|
+
|
162
|
+
# Add fields to db new to schema
|
163
|
+
(fields_in_schema.keys - fields_in_db.keys).each do |field|
|
164
|
+
column = fields_in_schema[field]
|
165
|
+
options = {:limit => column.limit, :precision => column.precision, :scale => column.scale}
|
166
|
+
options[:default] = column.default if !column.default.nil?
|
167
|
+
options[:null] = column.null if !column.null.nil?
|
168
|
+
connection.add_column table_name, column.name, column.type.to_sym, options
|
169
|
+
end
|
170
|
+
|
171
|
+
# Change attributes of existent columns
|
172
|
+
(fields_in_schema.keys & fields_in_db.keys).each do |field|
|
173
|
+
if field != primary_key #ActiveRecord::Base.get_primary_key(table_name)
|
174
|
+
changed = false # flag
|
175
|
+
new_type = fields_in_schema[field].type.to_sym
|
176
|
+
new_attr = {}
|
177
|
+
|
178
|
+
# First, check if the field type changed
|
179
|
+
if (fields_in_schema[field].type.to_sym != fields_in_db[field].type.to_sym) and (fields_in_schema[field].type.to_sym != fields_in_db[field].sql_type.to_sym)
|
180
|
+
# $stderr.puts "A(#{field}) - #{fields_in_schema[field].type.to_sym}"
|
181
|
+
# $stderr.puts "B(#{field}) - #{fields_in_db[field].type.to_sym} - #{fields_in_db[field].sql_type.to_sym}"
|
182
|
+
changed = true
|
183
|
+
end
|
184
|
+
|
185
|
+
# Special catch for precision/scale, since *both* must be specified together
|
186
|
+
# Always include them in the attr struct, but they'll only get applied if changed = true
|
187
|
+
new_attr[:precision] = fields_in_schema[field][:precision]
|
188
|
+
new_attr[:scale] = fields_in_schema[field][:scale]
|
189
|
+
|
190
|
+
# Next, iterate through our extended attributes, looking for any differences
|
191
|
+
# This catches stuff like :null, :precision, etc
|
192
|
+
fields_in_schema[field].each_pair do |att,value|
|
193
|
+
next if att == :type or att == :base or att == :name # special cases
|
194
|
+
if !value.nil? && value != fields_in_db[field].send(att)
|
195
|
+
# $stderr.puts "C(#{att}) - #{value.inspect}"
|
196
|
+
# $stderr.puts "D(#{att}) - #{fields_in_db[field].send(att).inspect}"
|
197
|
+
new_attr[att] = value
|
198
|
+
changed = true
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Change the column if applicable
|
203
|
+
connection.change_column table_name, field, new_type, new_attr if changed
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# Remove old index
|
208
|
+
indexes_in_db = connection.indexes(table_name).map(&:name)
|
209
|
+
(indexes_in_db - indexes.keys).each do |name|
|
210
|
+
connection.remove_index(table_name, :name => name)
|
211
|
+
end
|
212
|
+
|
213
|
+
# Add indexes
|
214
|
+
indexes.each do |name, options|
|
215
|
+
options = options.dup
|
216
|
+
unless connection.indexes(table_name).detect { |i| i.name == name }
|
217
|
+
connection.add_index(table_name, options.delete(:column), options)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
# Reload column information
|
222
|
+
if connection.respond_to?(:schema_cache)
|
223
|
+
connection.schema_cache.clear!
|
224
|
+
end
|
225
|
+
reset_column_information
|
226
|
+
end
|
227
|
+
end # ClassMethods
|
228
|
+
end # AutoSchema
|
229
|
+
end # ActiveRecordInlineSchema
|
data/spec/models.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
# be sure to set up activerecord before you require this helper
|
2
|
+
|
3
|
+
class Person < ActiveRecord::Base
|
4
|
+
include SpecHelper
|
5
|
+
schema do |s|
|
6
|
+
s.string :name
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class Post < ActiveRecord::Base
|
11
|
+
include SpecHelper
|
12
|
+
|
13
|
+
col :title
|
14
|
+
col :body
|
15
|
+
col :category, :as => :references
|
16
|
+
belongs_to :category
|
17
|
+
end
|
18
|
+
|
19
|
+
class Category < ActiveRecord::Base
|
20
|
+
include SpecHelper
|
21
|
+
|
22
|
+
col :title
|
23
|
+
has_many :posts
|
24
|
+
end
|
25
|
+
|
26
|
+
class Animal < ActiveRecord::Base
|
27
|
+
include SpecHelper
|
28
|
+
|
29
|
+
col :name, :index => true
|
30
|
+
add_index :id
|
31
|
+
end
|
32
|
+
|
33
|
+
class Pet < ActiveRecord::Base
|
34
|
+
include SpecHelper
|
35
|
+
|
36
|
+
col :name, :index => true
|
37
|
+
end
|
38
|
+
class Dog < Pet; end
|
39
|
+
class Cat < Pet; end
|
40
|
+
|
41
|
+
class Vegetable < ActiveRecord::Base
|
42
|
+
include SpecHelper
|
43
|
+
|
44
|
+
self.primary_key = 'latin_name'
|
45
|
+
|
46
|
+
col :latin_name
|
47
|
+
col :common_name
|
48
|
+
end
|
49
|
+
|
50
|
+
class Gender < ActiveRecord::Base
|
51
|
+
include SpecHelper
|
52
|
+
|
53
|
+
self.primary_key = 'name'
|
54
|
+
|
55
|
+
col :name
|
56
|
+
end
|
57
|
+
|
58
|
+
class User < ActiveRecord::Base
|
59
|
+
include SpecHelper
|
60
|
+
self.inheritance_column = 'role' # messed up in 3.2.2
|
61
|
+
col :name
|
62
|
+
col :surname
|
63
|
+
col :role
|
64
|
+
end
|
65
|
+
class Administrator < User; end
|
66
|
+
class Customer < User; end
|
67
|
+
|
68
|
+
class Fake < ActiveRecord::Base
|
69
|
+
include SpecHelper
|
70
|
+
col :name, :surname
|
71
|
+
col :category, :group, :as => :references
|
72
|
+
end
|
73
|
+
|
74
|
+
class AutomobileMakeModelYearVariant < ActiveRecord::Base
|
75
|
+
include SpecHelper
|
76
|
+
col :make_model_year_name
|
77
|
+
add_index :make_model_year_name
|
78
|
+
end
|
79
|
+
|
80
|
+
case ENV['DB_ADAPTER']
|
81
|
+
when 'mysql'
|
82
|
+
class CustomMysql < ActiveRecord::Base
|
83
|
+
include SpecHelper
|
84
|
+
col :varb, :type => 'varbinary(255)'
|
85
|
+
col :varc, :type => 'varchar(255)'
|
86
|
+
end
|
87
|
+
when 'postgresql'
|
88
|
+
class CustomPostgresql < ActiveRecord::Base
|
89
|
+
include SpecHelper
|
90
|
+
col :inet, :type => 'inet'
|
91
|
+
col :bytea, :type => 'bytea'
|
92
|
+
end
|
93
|
+
end
|
data/spec/mysql_spec.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require File.expand_path('../spec_helper.rb', __FILE__)
|
2
|
+
|
3
|
+
bin = ENV['TEST_MYSQL_BIN'] || 'mysql'
|
4
|
+
username = ENV['TEST_MYSQL_USERNAME'] || 'root'
|
5
|
+
password = ENV['TEST_MYSQL_PASSWORD'] || 'password'
|
6
|
+
database = ENV['TEST_MYSQL_DATABASE'] || 'test_active_record_inline_schema'
|
7
|
+
cmd = "#{bin} -u #{username} -p#{password}"
|
8
|
+
|
9
|
+
`#{cmd} -e 'show databases'`
|
10
|
+
unless $?.success?
|
11
|
+
$stderr.puts "Skipping mysql tests because `#{cmd}` doesn't work"
|
12
|
+
exit 0
|
13
|
+
end
|
14
|
+
|
15
|
+
system %{#{cmd} -e "drop database #{database}"}
|
16
|
+
system %{#{cmd} -e "create database #{database}"}
|
17
|
+
|
18
|
+
ActiveRecord::Base.establish_connection(
|
19
|
+
'adapter' => 'mysql',
|
20
|
+
'encoding' => 'utf8',
|
21
|
+
'database' => database,
|
22
|
+
'username' => username,
|
23
|
+
'password' => password
|
24
|
+
)
|
25
|
+
|
26
|
+
# require 'logger'
|
27
|
+
# ActiveRecord::Base.logger = ActiveSupport::BufferedLogger.new($stdout)
|
28
|
+
|
29
|
+
require File.expand_path('../models.rb', __FILE__)
|
30
|
+
|
31
|
+
require File.expand_path('../shared_examples.rb', __FILE__)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.expand_path('../spec_helper.rb', __FILE__)
|
2
|
+
|
3
|
+
createdb_bin = ENV['TEST_CREATEDB_BIN'] || 'createdb'
|
4
|
+
dropdb_bin = ENV['TEST_DROPDB_BIN'] || 'dropdb'
|
5
|
+
username = ENV['TEST_POSTGRES_USERNAME'] || `whoami`.chomp
|
6
|
+
# password = ENV['TEST_POSTGRES_PASSWORD'] || 'password'
|
7
|
+
database = ENV['TEST_POSTGRES_DATABASE'] || 'test_active_record_inline_schema'
|
8
|
+
|
9
|
+
system %{#{dropdb_bin} #{database}}
|
10
|
+
system %{#{createdb_bin} #{database}}
|
11
|
+
|
12
|
+
ActiveRecord::Base.establish_connection(
|
13
|
+
'adapter' => 'postgresql',
|
14
|
+
'encoding' => 'utf8',
|
15
|
+
'database' => database,
|
16
|
+
'username' => username,
|
17
|
+
# 'password' => password
|
18
|
+
)
|
19
|
+
|
20
|
+
# require 'logger'
|
21
|
+
# ActiveRecord::Base.logger = ActiveSupport::BufferedLogger.new($stdout)
|
22
|
+
|
23
|
+
require File.expand_path('../models.rb', __FILE__)
|
24
|
+
|
25
|
+
require File.expand_path('../shared_examples.rb', __FILE__)
|
@@ -0,0 +1,240 @@
|
|
1
|
+
describe ActiveRecordInlineSchema do
|
2
|
+
before do
|
3
|
+
ActiveRecord::Base.descendants.each do |active_record|
|
4
|
+
ActiveRecord::Base.connection.drop_table active_record.table_name rescue nil
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'has #schema inside model' do
|
9
|
+
# For unknown reason separate specs doesn't works
|
10
|
+
ActiveRecord::Base.connection.table_exists?(Person.table_name).must_equal false
|
11
|
+
Person.auto_upgrade!
|
12
|
+
Person.table_name.must_equal 'people'
|
13
|
+
Person.db_columns.sort.must_equal %w[id name]
|
14
|
+
Person.column_names.sort.must_equal Person.db_columns
|
15
|
+
Person.column_names.sort.must_equal Person.schema_columns
|
16
|
+
person = Person.create(:name => 'foo')
|
17
|
+
person.name.must_equal 'foo'
|
18
|
+
proc { person.surname }.must_raise NoMethodError
|
19
|
+
|
20
|
+
# Add a column without lost data
|
21
|
+
Person.class_eval do
|
22
|
+
schema do |p|
|
23
|
+
p.string :name
|
24
|
+
p.string :surname
|
25
|
+
end
|
26
|
+
end
|
27
|
+
Person.auto_upgrade!
|
28
|
+
Person.count.must_equal 1
|
29
|
+
person = Person.last
|
30
|
+
person.name.must_equal 'foo'
|
31
|
+
person.surname.must_be_nil
|
32
|
+
person.update_attribute(:surname, 'bar')
|
33
|
+
Person.db_columns.sort.must_equal %w[id name surname]
|
34
|
+
Person.column_names.sort.must_equal Person.db_columns
|
35
|
+
|
36
|
+
# Remove a column without lost data
|
37
|
+
Person.class_eval do
|
38
|
+
schema do |p|
|
39
|
+
p.string :name
|
40
|
+
end
|
41
|
+
end
|
42
|
+
Person.auto_upgrade!
|
43
|
+
person = Person.last
|
44
|
+
person.name.must_equal 'foo'
|
45
|
+
proc { person.surname }.must_raise NoMethodError
|
46
|
+
Person.db_columns.sort.must_equal %w[id name]
|
47
|
+
Person.column_names.sort.must_equal Person.db_columns
|
48
|
+
Person.column_names.sort.must_equal Person.schema_columns
|
49
|
+
|
50
|
+
# Change column without lost data
|
51
|
+
Person.class_eval do
|
52
|
+
schema do |p|
|
53
|
+
p.text :name
|
54
|
+
end
|
55
|
+
end
|
56
|
+
person = Person.last
|
57
|
+
person.name.must_equal 'foo'
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'has #key,col,property,attribute inside model' do
|
61
|
+
ActiveRecord::Base.connection.table_exists?(Post.table_name).must_equal false
|
62
|
+
ActiveRecord::Base.connection.table_exists?(Category.table_name).must_equal false
|
63
|
+
Post.auto_upgrade!; Category.auto_upgrade!
|
64
|
+
Post.column_names.sort.must_equal Post.db_columns
|
65
|
+
Category.column_names.sort.must_equal Category.schema_columns
|
66
|
+
|
67
|
+
# Check default properties
|
68
|
+
category = Category.create(:title => 'category')
|
69
|
+
post = Post.create(:title => 'foo', :body => 'bar', :category_id => category.id)
|
70
|
+
post = Post.first
|
71
|
+
post.title.must_equal 'foo'
|
72
|
+
post.body.must_equal 'bar'
|
73
|
+
post.category.must_equal category
|
74
|
+
|
75
|
+
|
76
|
+
# Remove a column
|
77
|
+
Post.reset_table_definition!
|
78
|
+
Post.class_eval do
|
79
|
+
col :name
|
80
|
+
col :category, :as => :references
|
81
|
+
end
|
82
|
+
Post.auto_upgrade!
|
83
|
+
post = Post.first
|
84
|
+
post.name.must_be_nil
|
85
|
+
post.category.must_equal category
|
86
|
+
post.wont_respond_to :title
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'has indexes inside model' do
|
90
|
+
# Check indexes
|
91
|
+
Animal.auto_upgrade!
|
92
|
+
Animal.db_indexes.size.must_be :>, 0
|
93
|
+
Animal.db_indexes.must_equal Animal.indexes.keys.sort
|
94
|
+
|
95
|
+
indexes_was = Animal.db_indexes
|
96
|
+
|
97
|
+
# Remove an index
|
98
|
+
Animal.indexes.delete(indexes_was.pop)
|
99
|
+
Animal.auto_upgrade!
|
100
|
+
Animal.indexes.keys.sort.must_equal indexes_was
|
101
|
+
Animal.db_indexes.must_equal indexes_was
|
102
|
+
|
103
|
+
# Add a new index
|
104
|
+
Animal.class_eval do
|
105
|
+
col :category, :as => :references, :index => true
|
106
|
+
end
|
107
|
+
Animal.auto_upgrade!
|
108
|
+
Animal.db_columns.must_include "category_id"
|
109
|
+
Animal.db_indexes.must_equal((indexes_was << "index_animals_on_category_id").sort)
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'works with STI' do
|
113
|
+
Pet.auto_upgrade!
|
114
|
+
Pet.reset_column_information
|
115
|
+
Pet.db_columns.must_include "type"
|
116
|
+
Dog.auto_upgrade!
|
117
|
+
Pet.db_columns.must_include "type"
|
118
|
+
|
119
|
+
# Now, let's we know if STI is working
|
120
|
+
Pet.create(:name => "foo")
|
121
|
+
Dog.create(:name => "bar")
|
122
|
+
Dog.count.must_equal 1
|
123
|
+
Dog.first.name.must_equal "bar"
|
124
|
+
Pet.count.must_equal 2
|
125
|
+
Pet.all.map(&:name).must_equal ["foo", "bar"]
|
126
|
+
|
127
|
+
# Check that this doesn't break things
|
128
|
+
Cat.auto_upgrade!
|
129
|
+
Dog.first.name.must_equal "bar"
|
130
|
+
|
131
|
+
# What's happen if we change schema?
|
132
|
+
Dog.table_definition.must_equal Pet.table_definition
|
133
|
+
Dog.indexes.must_equal Pet.indexes
|
134
|
+
Dog.class_eval do
|
135
|
+
col :bau
|
136
|
+
end
|
137
|
+
Dog.auto_upgrade!
|
138
|
+
Pet.db_columns.must_include "bau"
|
139
|
+
Dog.new.must_respond_to :bau
|
140
|
+
Cat.new.must_respond_to :bau
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'works with custom inheritance column' do
|
144
|
+
User.auto_upgrade!
|
145
|
+
Administrator.create(:name => "Davide", :surname => "D'Agostino")
|
146
|
+
Customer.create(:name => "Foo", :surname => "Bar")
|
147
|
+
Administrator.count.must_equal 1
|
148
|
+
Administrator.first.name.must_equal "Davide"
|
149
|
+
Customer.count.must_equal 1
|
150
|
+
Customer.first.name.must_equal "Foo"
|
151
|
+
User.count.must_equal 2
|
152
|
+
User.first.role.must_equal "Administrator"
|
153
|
+
User.last.role.must_equal "Customer"
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'allow multiple columns definitions' do
|
157
|
+
Fake.auto_upgrade!
|
158
|
+
Fake.create(:name => 'foo', :surname => 'bar', :category_id => 1, :group_id => 2)
|
159
|
+
fake = Fake.first
|
160
|
+
fake.name.must_equal 'foo'
|
161
|
+
fake.surname.must_equal 'bar'
|
162
|
+
fake.category_id.must_equal 1
|
163
|
+
fake.group_id.must_equal 2
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'allows non-integer primary keys' do
|
167
|
+
Vegetable.auto_upgrade!
|
168
|
+
Vegetable.primary_key.must_equal 'latin_name'
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'properly creates primary key columns so that ActiveRecord uses them' do
|
172
|
+
Vegetable.auto_upgrade!
|
173
|
+
Vegetable.delete_all
|
174
|
+
n = 'roobus roobious'
|
175
|
+
v = Vegetable.new; v.latin_name = n; v.save!
|
176
|
+
Vegetable.find(n).must_equal v
|
177
|
+
end
|
178
|
+
|
179
|
+
it 'automatically shortens long index names' do
|
180
|
+
AutomobileMakeModelYearVariant.auto_upgrade!
|
181
|
+
AutomobileMakeModelYearVariant.db_indexes.first.start_with?('index_automobile_make_model_ye').must_equal true
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'properly creates primary key columns that are unique' do
|
185
|
+
Vegetable.auto_upgrade!
|
186
|
+
Vegetable.delete_all
|
187
|
+
n = 'roobus roobious'
|
188
|
+
v = Vegetable.new; v.latin_name = n; v.save!
|
189
|
+
if sqlite?
|
190
|
+
flunk # segfaults
|
191
|
+
# lambda { v = Vegetable.new; v.latin_name = n; v.save! }.must_raise(SQLite3::ConstraintException)
|
192
|
+
else
|
193
|
+
lambda { v = Vegetable.new; v.latin_name = n; v.save! }.must_raise(ActiveRecord::RecordNotUnique)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'properly creates tables with one column, a string primary key' do
|
198
|
+
Gender.auto_upgrade!
|
199
|
+
Gender.column_names.must_equal ['name']
|
200
|
+
end
|
201
|
+
|
202
|
+
it 'is idempotent' do
|
203
|
+
ActiveRecord::Base.descendants.each do |active_record|
|
204
|
+
active_record.auto_upgrade!
|
205
|
+
active_record.reset_column_information
|
206
|
+
before = [ active_record.db_columns, active_record.db_indexes ]
|
207
|
+
active_record.auto_upgrade!
|
208
|
+
active_record.reset_column_information
|
209
|
+
[ active_record.db_columns, active_record.db_indexes ].must_equal before
|
210
|
+
active_record.auto_upgrade!
|
211
|
+
active_record.reset_column_information
|
212
|
+
active_record.auto_upgrade!
|
213
|
+
active_record.reset_column_information
|
214
|
+
active_record.auto_upgrade!
|
215
|
+
active_record.reset_column_information
|
216
|
+
[ active_record.db_columns, active_record.db_indexes ].must_equal before
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
case ENV['DB_ADAPTER']
|
221
|
+
when 'mysql'
|
222
|
+
it "takes custom types" do
|
223
|
+
CustomMysql.auto_upgrade!
|
224
|
+
CustomMysql.columns_hash['varb'].sql_type.must_equal 'varbinary(255)'
|
225
|
+
CustomMysql.columns_hash['varc'].sql_type.must_equal 'varchar(255)'
|
226
|
+
end
|
227
|
+
when 'postgresql'
|
228
|
+
it "takes custom types" do
|
229
|
+
CustomPostgresql.auto_upgrade!
|
230
|
+
CustomPostgresql.columns_hash['inet'].sql_type.must_equal 'inet'
|
231
|
+
CustomPostgresql.columns_hash['bytea'].sql_type.must_equal 'bytea'
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
private
|
236
|
+
|
237
|
+
def sqlite?
|
238
|
+
ActiveRecord::Base.connection.adapter_name =~ /sqlite/i
|
239
|
+
end
|
240
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rubygems' unless defined?(Gem)
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'active_record_inline_schema'
|
4
|
+
require 'minitest/autorun'
|
5
|
+
|
6
|
+
# require 'logger'
|
7
|
+
# ActiveRecord::Base.logger = Logger.new($stderr)
|
8
|
+
# ActiveRecord::Base.logger.level = Logger::DEBUG
|
9
|
+
|
10
|
+
module SpecHelper
|
11
|
+
def self.included(base)
|
12
|
+
base.extend(ClassMethods)
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
def db_columns
|
17
|
+
connection.columns(table_name).map(&:name).sort
|
18
|
+
end
|
19
|
+
|
20
|
+
def db_indexes
|
21
|
+
connection.indexes(table_name).map(&:name).sort
|
22
|
+
end
|
23
|
+
|
24
|
+
def schema_columns
|
25
|
+
table_definition.columns.map { |c| c.name.to_s }.sort
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require File.expand_path('../spec_helper.rb', __FILE__)
|
2
|
+
|
3
|
+
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
|
4
|
+
|
5
|
+
# require 'logger'
|
6
|
+
# ActiveRecord::Base.logger = ActiveSupport::BufferedLogger.new($stdout)
|
7
|
+
|
8
|
+
require File.expand_path('../models.rb', __FILE__)
|
9
|
+
|
10
|
+
require File.expand_path('../shared_examples.rb', __FILE__)
|
metadata
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: active_record_inline_schema
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Seamus Abshere
|
9
|
+
- Davide D'Agostino
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-03-26 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: activerecord
|
17
|
+
requirement: &2166623420 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '3'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *2166623420
|
26
|
+
description: Specify columns like you would with ActiveRecord migrations and then
|
27
|
+
run .auto_upgrade! Based on the mini_record gem from Davide D'Agostino, it adds
|
28
|
+
fewer aliases, doesn't create timestamps and relationship columns automatically.
|
29
|
+
email:
|
30
|
+
- seamus@abshere.net
|
31
|
+
- d.dagostino@lipsiasoft.com
|
32
|
+
executables: []
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files: []
|
35
|
+
files:
|
36
|
+
- .gitignore
|
37
|
+
- .travis.yml
|
38
|
+
- Gemfile
|
39
|
+
- README.md
|
40
|
+
- Rakefile
|
41
|
+
- active_record_inline_schema.gemspec
|
42
|
+
- lib/active_record_inline_schema.rb
|
43
|
+
- lib/active_record_inline_schema/auto_schema.rb
|
44
|
+
- lib/active_record_inline_schema/version.rb
|
45
|
+
- spec/models.rb
|
46
|
+
- spec/mysql_spec.rb
|
47
|
+
- spec/postgresql_spec.rb
|
48
|
+
- spec/shared_examples.rb
|
49
|
+
- spec/spec_helper.rb
|
50
|
+
- spec/sqlite3_spec.rb
|
51
|
+
homepage: https://github.com/seamusabshere/active_record_inline_schema
|
52
|
+
licenses: []
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options: []
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
requirements: []
|
70
|
+
rubyforge_project:
|
71
|
+
rubygems_version: 1.8.15
|
72
|
+
signing_key:
|
73
|
+
specification_version: 3
|
74
|
+
summary: Define table structure (columns and indexes) inside your ActiveRecord models
|
75
|
+
like you can do in migrations. Also similar to DataMapper inline schema syntax.
|
76
|
+
test_files:
|
77
|
+
- spec/models.rb
|
78
|
+
- spec/mysql_spec.rb
|
79
|
+
- spec/postgresql_spec.rb
|
80
|
+
- spec/shared_examples.rb
|
81
|
+
- spec/spec_helper.rb
|
82
|
+
- spec/sqlite3_spec.rb
|
83
|
+
has_rdoc:
|