force_schema 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/.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
|