force_schema 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/README.rdoc +44 -0
- data/Rakefile +26 -0
- data/force_schema.gemspec +29 -0
- data/lib/force_schema.rb +23 -0
- data/lib/force_schema/registry.rb +6 -0
- data/lib/force_schema/schema.rb +279 -0
- data/lib/force_schema/version.rb +3 -0
- data/test/helper.rb +14 -0
- data/test/support/car.rb +18 -0
- data/test/support/monk.rb +7 -0
- data/test/support/shared.rb +103 -0
- data/test/test_mysql.rb +16 -0
- data/test/test_sqlite3.rb +10 -0
- metadata +145 -0
data/Gemfile
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
=force_schema
|
2
|
+
|
3
|
+
class Car < ActiveRecord::Base
|
4
|
+
set_primary_key :name
|
5
|
+
force_schema do
|
6
|
+
string 'name' # Nissan Altima, will automatically be indexed because it's the primary key
|
7
|
+
string 'make_name' # Nissan
|
8
|
+
string 'model_name' # Altime
|
9
|
+
float 'fuel_efficiency_city'
|
10
|
+
string 'fuel_efficiency_city_units'
|
11
|
+
float 'fuel_efficiency_highway'
|
12
|
+
string 'fuel_efficiency_highway_units'
|
13
|
+
integer 'year'
|
14
|
+
datetime 'released_at'
|
15
|
+
date 'released_on'
|
16
|
+
index ['name', 'make_name']
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
== Warning: data loss
|
21
|
+
|
22
|
+
If you call <tt>Car.force_schema!</tt>, it may destroy data by adding, removing, or changing the type of columns. <b>No attempt is made to preserve data.</b>
|
23
|
+
|
24
|
+
== What it synchronizes
|
25
|
+
|
26
|
+
* adds missing columns
|
27
|
+
* adds missing indexes
|
28
|
+
* removes unrecognized columns
|
29
|
+
* removes unrecognized indexes
|
30
|
+
* changes the type of columns if it doesn't match what you asked for
|
31
|
+
* changes the columns indexed if they don't match what you asked for
|
32
|
+
* makes sure default value is synced up
|
33
|
+
|
34
|
+
== Supported databases
|
35
|
+
|
36
|
+
* MySQL is tested
|
37
|
+
* SQLite is tested
|
38
|
+
* Postgres is not supported
|
39
|
+
|
40
|
+
== History
|
41
|
+
|
42
|
+
Extracted from https://github.com/seamusabshere/data_miner. In production use at http://carbon.brighterplanet.com and http://data.brighterplanet.com.
|
43
|
+
|
44
|
+
Copyright 2011 Seamus Abshere
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
require 'rake'
|
5
|
+
require 'rake/testtask'
|
6
|
+
Rake::TestTask.new(:test) do |test|
|
7
|
+
test.libs << 'lib' << 'test'
|
8
|
+
test.pattern = 'test/**/test_*.rb'
|
9
|
+
test.verbose = true
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'rake/rdoctask'
|
13
|
+
Rake::RDocTask.new do |rdoc|
|
14
|
+
rdoc.rdoc_dir = 'rdoc'
|
15
|
+
rdoc.title = 'force_schema'
|
16
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
17
|
+
rdoc.rdoc_files.include('README*')
|
18
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
19
|
+
end
|
20
|
+
|
21
|
+
task :test_separately do
|
22
|
+
puts `bundle exec rake test TEST=test/test_mysql.rb`
|
23
|
+
puts `bundle exec rake test TEST=test/test_sqlite3.rb`
|
24
|
+
end
|
25
|
+
|
26
|
+
task :default => :test_separately
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "force_schema/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "force_schema"
|
7
|
+
s.version = ForceSchema::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Seamus Abshere"]
|
10
|
+
s.email = ["seamus@abshere.net"]
|
11
|
+
s.homepage = "https://github.com/seamusabshere/force_schema"
|
12
|
+
s.summary = %q{Sometimes you don't need to write up and down migrations, you just want a table (aka schema) to have a certain structure.}
|
13
|
+
s.description = %q{Declare a table structure like an ActiveRecord migration and run 'force_schema!' whenever you want. For when you don't need up and down migrations.}
|
14
|
+
|
15
|
+
s.rubyforge_project = "force_schema"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
s.add_development_dependency 'test-unit'
|
23
|
+
s.add_development_dependency 'mysql'
|
24
|
+
s.add_development_dependency 'sqlite3-ruby'
|
25
|
+
s.add_development_dependency 'rake'
|
26
|
+
s.add_dependency 'activerecord', '>=2.3.10'
|
27
|
+
s.add_dependency 'activesupport', '>=2.3.10'
|
28
|
+
s.add_dependency 'blockenspiel'
|
29
|
+
end
|
data/lib/force_schema.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_support/core_ext'
|
3
|
+
|
4
|
+
module ForceSchema
|
5
|
+
autoload :Registry, 'force_schema/registry'
|
6
|
+
autoload :Schema, 'force_schema/schema'
|
7
|
+
|
8
|
+
def force_schema!
|
9
|
+
enforced_schema.run
|
10
|
+
end
|
11
|
+
|
12
|
+
def force_schema(create_table_options = {}, &blk)
|
13
|
+
enforced_schema.create_table_options = create_table_options
|
14
|
+
::Blockenspiel.invoke blk, enforced_schema
|
15
|
+
enforced_schema
|
16
|
+
end
|
17
|
+
|
18
|
+
def enforced_schema
|
19
|
+
Registry.instance[name] ||= Schema.new self
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
::ActiveRecord::Base.extend ::ForceSchema
|
@@ -0,0 +1,279 @@
|
|
1
|
+
require 'blockenspiel'
|
2
|
+
require 'zlib'
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/core_ext'
|
5
|
+
module ForceSchema
|
6
|
+
class Schema
|
7
|
+
MAX_INDEX_NAME_LENGTH = 32 # mysql
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def suggest_index_name(active_record, columns, index_options = {})
|
11
|
+
return index_options[:name] if index_options.has_key? :name
|
12
|
+
default_name = 'index_' + ::Array.wrap(columns).join('_and_') #active_record.connection.index_name(active_record.table_name, index_options.merge(:column => columns))
|
13
|
+
if default_name.length < MAX_INDEX_NAME_LENGTH
|
14
|
+
default_name
|
15
|
+
else
|
16
|
+
default_name[0..MAX_INDEX_NAME_LENGTH-11] + ::Zlib.crc32(default_name).to_s
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
include ::Blockenspiel::DSL
|
22
|
+
|
23
|
+
attr_reader :active_record
|
24
|
+
attr_reader :create_table_options
|
25
|
+
|
26
|
+
def initialize(active_record)
|
27
|
+
@active_record = active_record
|
28
|
+
end
|
29
|
+
|
30
|
+
def create_table_options=(options = {})
|
31
|
+
@create_table_options = options.symbolize_keys
|
32
|
+
raise ":id => true is not allowed in create_table_options." if create_table_options[:id] === true
|
33
|
+
raise ":primary_key is not allowed in create_table_options. Use set_primary_key instead." if create_table_options.has_key?(:primary_key)
|
34
|
+
if create_table_options[:options].blank? and mysql?
|
35
|
+
create_table_options[:options] = 'ENGINE=INNODB CHARSET=UTF8 COLLATE=UTF8_GENERAL_CI'
|
36
|
+
end
|
37
|
+
create_table_options[:id] = false # always
|
38
|
+
end
|
39
|
+
|
40
|
+
# sabshere 1/25/11 lifted straight from activerecord-3.0.3/lib/active_record/connection_adapters/abstract/schema_definitions.rb
|
41
|
+
%w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
|
42
|
+
class_eval <<-EOV
|
43
|
+
def #{column_type}(*args) # def string(*args)
|
44
|
+
options = args.extract_options! # options = args.extract_options!
|
45
|
+
column_names = args # column_names = args
|
46
|
+
#
|
47
|
+
column_names.each { |name| ideal_table.column(name, '#{column_type}', options) } # column_names.each { |name| ideal_table.column(name, 'string', options) }
|
48
|
+
end # end
|
49
|
+
EOV
|
50
|
+
end
|
51
|
+
|
52
|
+
def index(columns, index_options = {})
|
53
|
+
index_options = index_options.symbolize_keys
|
54
|
+
columns = ::Array.wrap columns
|
55
|
+
index_name = Schema.suggest_index_name active_record, columns, index_options
|
56
|
+
index_unique = index_options.has_key?(:unique) ? index_options[:unique] : true
|
57
|
+
ideal_indexes.push ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, index_name, index_unique, columns)
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
|
61
|
+
def run
|
62
|
+
_create_table
|
63
|
+
_set_primary_key
|
64
|
+
_remove_columns
|
65
|
+
_add_columns
|
66
|
+
_remove_indexes
|
67
|
+
_add_indexes
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def log_debug(msg)
|
74
|
+
::ActiveRecord::Base.logger.try :debug, msg
|
75
|
+
end
|
76
|
+
|
77
|
+
def column(*args)
|
78
|
+
ideal_table.column(*args)
|
79
|
+
end
|
80
|
+
|
81
|
+
def ideal_primary_key_name
|
82
|
+
active_record.primary_key.to_s
|
83
|
+
end
|
84
|
+
|
85
|
+
def actual_primary_key_name
|
86
|
+
if sqlite? and pk = active_record.columns_hash.detect { |k, v| v.primary }
|
87
|
+
pk[1].name
|
88
|
+
elsif pk = connection.primary_key(table_name)
|
89
|
+
pk
|
90
|
+
end.to_s
|
91
|
+
end
|
92
|
+
|
93
|
+
def index_equivalent?(a, b)
|
94
|
+
return false unless a and b
|
95
|
+
%w{ name columns }.all? do |property|
|
96
|
+
a_property = ::Array.wrap(a.send(property)).map(&:to_s)
|
97
|
+
b_property = ::Array.wrap(b.send(property)).map(&:to_s)
|
98
|
+
log_debug "...comparing #{a_property.inspect} <-> #{b_property.inspect}"
|
99
|
+
a_property == b_property
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
COMPARABLE_OPTIONS = [ :default ] # other things vary rather unpredicatably by adapter
|
104
|
+
|
105
|
+
def column_equivalent?(a, b)
|
106
|
+
return false unless a and b
|
107
|
+
a_type = (a.type.to_s == 'primary_key') ? 'integer' : a.type.to_s
|
108
|
+
b_type = (b.type.to_s == 'primary_key') ? 'integer' : b.type.to_s
|
109
|
+
a_type == b_type and a.name.to_s == b.name.to_s and column_options(a).slice(COMPARABLE_OPTIONS) == column_options(b).slice(COMPARABLE_OPTIONS)
|
110
|
+
end
|
111
|
+
|
112
|
+
%w{ column index }.each do |i|
|
113
|
+
eval %{
|
114
|
+
def #{i}_needs_to_be_placed?(name)
|
115
|
+
actual = actual_#{i} name
|
116
|
+
return true unless actual
|
117
|
+
ideal = ideal_#{i} name
|
118
|
+
not #{i}_equivalent? actual, ideal
|
119
|
+
end
|
120
|
+
|
121
|
+
def #{i}_needs_to_be_removed?(name)
|
122
|
+
ideal_#{i}(name).nil?
|
123
|
+
end
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
127
|
+
def ideal_column(name)
|
128
|
+
ideal_table[name.to_s]
|
129
|
+
end
|
130
|
+
|
131
|
+
def actual_column(name)
|
132
|
+
active_record.columns_hash[name.to_s]
|
133
|
+
end
|
134
|
+
|
135
|
+
def ideal_index(name)
|
136
|
+
ideal_indexes.detect { |ideal| ideal.name == name.to_s }
|
137
|
+
end
|
138
|
+
|
139
|
+
def actual_index(name)
|
140
|
+
actual_indexes.detect { |actual| actual.name == name.to_s }
|
141
|
+
end
|
142
|
+
|
143
|
+
POSSIBLE_OPTIONS = [ :limit, :default, :null, :precision, :scale ]
|
144
|
+
|
145
|
+
def column_options(column)
|
146
|
+
POSSIBLE_OPTIONS.inject({}) do |memo, k|
|
147
|
+
v = column.send(k)
|
148
|
+
if v == false or v.present?
|
149
|
+
memo[k] = v
|
150
|
+
end
|
151
|
+
memo
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def place_column(name)
|
156
|
+
remove_column name if actual_column name
|
157
|
+
ideal = ideal_column name
|
158
|
+
options = column_options ideal
|
159
|
+
log_debug "PLACING COLUMN #{name}"
|
160
|
+
connection.add_column table_name, name, ideal.type.to_sym, options
|
161
|
+
active_record.reset_column_information
|
162
|
+
end
|
163
|
+
|
164
|
+
def remove_column(name)
|
165
|
+
log_debug "REMOVING COLUMN #{name}"
|
166
|
+
connection.remove_column table_name, name
|
167
|
+
active_record.reset_column_information
|
168
|
+
end
|
169
|
+
|
170
|
+
def place_index(name)
|
171
|
+
remove_index name if actual_index name
|
172
|
+
ideal = ideal_index name
|
173
|
+
log_debug "PLACING INDEX #{name}"
|
174
|
+
connection.add_index table_name, ideal.columns, :name => ideal.name
|
175
|
+
active_record.reset_column_information
|
176
|
+
end
|
177
|
+
|
178
|
+
def remove_index(name)
|
179
|
+
log_debug "REMOVING INDEX #{name}"
|
180
|
+
connection.remove_index table_name, :name => name
|
181
|
+
active_record.reset_column_information
|
182
|
+
end
|
183
|
+
|
184
|
+
# sabshere 1/25/11 what if there were multiple connections
|
185
|
+
# blockenspiel doesn't like to delegate this to #active_record
|
186
|
+
def connection
|
187
|
+
::ActiveRecord::Base.connection
|
188
|
+
end
|
189
|
+
|
190
|
+
def table_name
|
191
|
+
active_record.table_name
|
192
|
+
end
|
193
|
+
|
194
|
+
def ideal_table
|
195
|
+
@ideal_table ||= ::ActiveRecord::ConnectionAdapters::TableDefinition.new connection
|
196
|
+
end
|
197
|
+
|
198
|
+
def ideal_indexes
|
199
|
+
@ideal_indexes ||= []
|
200
|
+
end
|
201
|
+
|
202
|
+
def actual_indexes
|
203
|
+
connection.indexes table_name
|
204
|
+
end
|
205
|
+
|
206
|
+
def _create_table
|
207
|
+
if not active_record.table_exists?
|
208
|
+
log_debug "CREATING TABLE #{table_name} with #{create_table_options.inspect}"
|
209
|
+
connection.create_table table_name, create_table_options do |t|
|
210
|
+
t.integer 'force_schema_tmp'
|
211
|
+
end
|
212
|
+
active_record.reset_column_information
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def _set_primary_key
|
217
|
+
if ideal_primary_key_name == 'id' and not ideal_column('id')
|
218
|
+
log_debug "no special primary key set on #{table_name}, so using 'id'"
|
219
|
+
column 'id', :primary_key # needs to be a sym?
|
220
|
+
end
|
221
|
+
actual = actual_column actual_primary_key_name
|
222
|
+
ideal = ideal_column ideal_primary_key_name
|
223
|
+
if not column_equivalent? actual, ideal
|
224
|
+
log_debug "looks like #{table_name} has a bad (or missing) primary key"
|
225
|
+
if actual
|
226
|
+
log_debug "looks like primary key needs to change from #{actual_primary_key_name} to #{ideal_primary_key_name}, re-creating #{table_name} from scratch"
|
227
|
+
connection.drop_table table_name
|
228
|
+
active_record.reset_column_information
|
229
|
+
_create_table
|
230
|
+
end
|
231
|
+
place_column ideal_primary_key_name
|
232
|
+
unless ideal.type.to_s == 'primary_key'
|
233
|
+
log_debug "SETTING #{ideal_primary_key_name} AS PRIMARY KEY"
|
234
|
+
if sqlite?
|
235
|
+
special_sqlite_primary_key_index_name = "IDX_#{table_name}_#{ideal_primary_key_name}"
|
236
|
+
connection.execute "CREATE UNIQUE INDEX #{special_sqlite_primary_key_index_name} ON #{table_name} (#{ideal_primary_key_name} ASC)"
|
237
|
+
ideal_indexes.push ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, special_sqlite_primary_key_index_name, true, ideal_primary_key_name)
|
238
|
+
else
|
239
|
+
connection.execute "ALTER TABLE `#{table_name}` ADD PRIMARY KEY (`#{ideal_primary_key_name}`)"
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
active_record.reset_column_information
|
244
|
+
end
|
245
|
+
|
246
|
+
def sqlite?
|
247
|
+
connection.adapter_name.downcase.start_with? 'sqlite'
|
248
|
+
end
|
249
|
+
|
250
|
+
def mysql?
|
251
|
+
connection.adapter_name.downcase.start_with? 'mysql'
|
252
|
+
end
|
253
|
+
|
254
|
+
def _remove_columns
|
255
|
+
active_record.columns_hash.values.each do |actual|
|
256
|
+
remove_column actual.name if column_needs_to_be_removed? actual.name
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def _add_columns
|
261
|
+
ideal_table.columns.each do |ideal|
|
262
|
+
place_column ideal.name if column_needs_to_be_placed? ideal.name
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def _remove_indexes
|
267
|
+
actual_indexes.each do |actual|
|
268
|
+
remove_index actual.name if index_needs_to_be_removed? actual.name
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
def _add_indexes
|
273
|
+
ideal_indexes.each do |ideal|
|
274
|
+
next if ideal.name == ideal_primary_key_name # this should already have been taken care of
|
275
|
+
place_index ideal.name if index_needs_to_be_placed? ideal.name
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
Bundler.setup
|
4
|
+
require 'logger'
|
5
|
+
require 'test/unit'
|
6
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
7
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
8
|
+
require 'force_schema'
|
9
|
+
require 'support/shared'
|
10
|
+
class Test::Unit::TestCase
|
11
|
+
end
|
12
|
+
|
13
|
+
ActiveRecord::Base.logger = Logger.new($stderr)
|
14
|
+
ActiveRecord::Base.logger.level = Logger::DEBUG
|
data/test/support/car.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
class Car < ActiveRecord::Base
|
2
|
+
set_primary_key :name
|
3
|
+
|
4
|
+
force_schema do
|
5
|
+
string 'name' # Nissan Altima, will automatically be indexed because it's the primary key
|
6
|
+
string 'make_name' # Nissan
|
7
|
+
string 'model_name' # Altime
|
8
|
+
float 'fuel_efficiency_city'
|
9
|
+
string 'fuel_efficiency_city_units'
|
10
|
+
float 'fuel_efficiency_highway'
|
11
|
+
string 'fuel_efficiency_highway_units'
|
12
|
+
integer 'year'
|
13
|
+
datetime 'released_at'
|
14
|
+
date 'released_on'
|
15
|
+
boolean 'consumer_reports_best_buy', :default => false
|
16
|
+
index ['name', 'make_name']
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Shared
|
2
|
+
def setup
|
3
|
+
ActiveRecord::Base.connection.drop_table 'cars' rescue nil
|
4
|
+
Car.force_schema!
|
5
|
+
end
|
6
|
+
|
7
|
+
def test_001_add_columns_and_indexes_from_scratch
|
8
|
+
assert_equal :string, ct(Car, :name)
|
9
|
+
assert_equal :float, ct(Car, :fuel_efficiency_city)
|
10
|
+
assert_equal :integer, ct(Car, :year)
|
11
|
+
assert_equal :datetime, ct(Car, :released_at)
|
12
|
+
assert_equal :date, ct(Car, :released_on)
|
13
|
+
assert index?(Car, :index_name_and_make_name)
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_002_restore_removed_column
|
17
|
+
Car.connection.remove_column :cars, :year
|
18
|
+
assert_equal nil, ct(Car, :year)
|
19
|
+
Car.force_schema!
|
20
|
+
assert_equal :integer, ct(Car, :year)
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_003_fix_column_type
|
24
|
+
Car.connection.remove_column :cars, :year
|
25
|
+
Car.connection.add_column :cars, :year, :string
|
26
|
+
Car.reset_column_information
|
27
|
+
assert_equal :string, ct(Car, :year)
|
28
|
+
Car.force_schema!
|
29
|
+
assert_equal :integer, ct(Car, :year)
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_004_remove_unrecognized_column
|
33
|
+
Car.connection.add_column :cars, :foobar, :string
|
34
|
+
assert_equal :string, ct(Car, :foobar)
|
35
|
+
Car.force_schema!
|
36
|
+
assert_equal nil, ct(Car, :foobar)
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_005_restore_removed_index
|
40
|
+
Car.connection.remove_index :cars, :name => 'index_name_and_make_name'
|
41
|
+
Car.reset_column_information
|
42
|
+
assert !index?(Car, :index_name_and_make_name)
|
43
|
+
Car.force_schema!
|
44
|
+
assert index?(Car, :index_name_and_make_name)
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_006_restore_damaged_index
|
48
|
+
Car.connection.remove_column :cars, :make_name
|
49
|
+
Car.force_schema!
|
50
|
+
assert index?(Car, :index_name_and_make_name)
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_007_remove_unrecognized_index
|
54
|
+
Car.connection.add_index :cars, :year, :name => 'foobar'
|
55
|
+
Car.reset_column_information
|
56
|
+
assert index?(Car, :foobar)
|
57
|
+
Car.force_schema!
|
58
|
+
assert !index?(Car, :foobar)
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_008_primary_key_is_unique
|
62
|
+
a = Car.new
|
63
|
+
a.name = 'a'
|
64
|
+
a.save!
|
65
|
+
assert_raises(ActiveRecord::RecordNotUnique) do
|
66
|
+
a = Car.new
|
67
|
+
a.name = 'a'
|
68
|
+
a.save!
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_009_edge_case_one_column
|
73
|
+
ActiveRecord::Base.connection.drop_table 'monks' rescue nil
|
74
|
+
Monk.force_schema!
|
75
|
+
Monk.force_schema!
|
76
|
+
Monk.force_schema!
|
77
|
+
assert_equal :string, ct(Monk, :name)
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_010_default_false
|
81
|
+
a = Car.new
|
82
|
+
assert_equal false, a.consumer_reports_best_buy
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def ct(active_record, column_name)
|
88
|
+
if c = active_record.columns_hash[column_name.to_s]
|
89
|
+
c.type
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def index?(active_record, index_name)
|
94
|
+
active_record.reset_column_information
|
95
|
+
index_name = index_name.to_s
|
96
|
+
return true if index_name == active_record.primary_key.to_s
|
97
|
+
if ActiveRecord::VERSION::MAJOR < 3
|
98
|
+
active_record.connection.index_exists?(active_record.table_name, index_name, :not_supported_by_adapter)
|
99
|
+
else
|
100
|
+
active_record.connection.index_exists?(active_record.table_name, nil, :name => index_name)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
data/test/test_mysql.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
ActiveRecord::Base.establish_connection(
|
4
|
+
'adapter' => 'mysql',
|
5
|
+
'database' => 'test_force_schema',
|
6
|
+
'username' => 'root',
|
7
|
+
'password' => 'password',
|
8
|
+
'encoding' => 'utf8'
|
9
|
+
)
|
10
|
+
|
11
|
+
require 'support/car'
|
12
|
+
require 'support/monk'
|
13
|
+
|
14
|
+
class TestMysql < Test::Unit::TestCase
|
15
|
+
include Shared
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: force_schema
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Seamus Abshere
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-07-19 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: test-unit
|
16
|
+
requirement: &2154422840 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2154422840
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: mysql
|
27
|
+
requirement: &2154422420 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2154422420
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: sqlite3-ruby
|
38
|
+
requirement: &2154422000 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *2154422000
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rake
|
49
|
+
requirement: &2154421580 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *2154421580
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: activerecord
|
60
|
+
requirement: &2154421080 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 2.3.10
|
66
|
+
type: :runtime
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *2154421080
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: activesupport
|
71
|
+
requirement: &2154420580 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 2.3.10
|
77
|
+
type: :runtime
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *2154420580
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: blockenspiel
|
82
|
+
requirement: &2154420200 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
type: :runtime
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: *2154420200
|
91
|
+
description: Declare a table structure like an ActiveRecord migration and run 'force_schema!'
|
92
|
+
whenever you want. For when you don't need up and down migrations.
|
93
|
+
email:
|
94
|
+
- seamus@abshere.net
|
95
|
+
executables: []
|
96
|
+
extensions: []
|
97
|
+
extra_rdoc_files: []
|
98
|
+
files:
|
99
|
+
- .gitignore
|
100
|
+
- Gemfile
|
101
|
+
- README.rdoc
|
102
|
+
- Rakefile
|
103
|
+
- force_schema.gemspec
|
104
|
+
- lib/force_schema.rb
|
105
|
+
- lib/force_schema/registry.rb
|
106
|
+
- lib/force_schema/schema.rb
|
107
|
+
- lib/force_schema/version.rb
|
108
|
+
- test/helper.rb
|
109
|
+
- test/support/car.rb
|
110
|
+
- test/support/monk.rb
|
111
|
+
- test/support/shared.rb
|
112
|
+
- test/test_mysql.rb
|
113
|
+
- test/test_sqlite3.rb
|
114
|
+
homepage: https://github.com/seamusabshere/force_schema
|
115
|
+
licenses: []
|
116
|
+
post_install_message:
|
117
|
+
rdoc_options: []
|
118
|
+
require_paths:
|
119
|
+
- lib
|
120
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
|
+
none: false
|
128
|
+
requirements:
|
129
|
+
- - ! '>='
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
requirements: []
|
133
|
+
rubyforge_project: force_schema
|
134
|
+
rubygems_version: 1.8.5
|
135
|
+
signing_key:
|
136
|
+
specification_version: 3
|
137
|
+
summary: Sometimes you don't need to write up and down migrations, you just want a
|
138
|
+
table (aka schema) to have a certain structure.
|
139
|
+
test_files:
|
140
|
+
- test/helper.rb
|
141
|
+
- test/support/car.rb
|
142
|
+
- test/support/monk.rb
|
143
|
+
- test/support/shared.rb
|
144
|
+
- test/test_mysql.rb
|
145
|
+
- test/test_sqlite3.rb
|