miguel 0.1.0.pre1
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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/Rakefile +10 -0
- data/TODO +2 -0
- data/bin/miguel +9 -0
- data/lib/miguel/command.rb +242 -0
- data/lib/miguel/dumper.rb +45 -0
- data/lib/miguel/importer.rb +232 -0
- data/lib/miguel/migrator.rb +242 -0
- data/lib/miguel/schema.rb +585 -0
- data/lib/miguel/version.rb +10 -0
- data/lib/miguel.rb +3 -0
- data/miguel.gemspec +26 -0
- data/test/test_dumper.rb +90 -0
- metadata +87 -0
@@ -0,0 +1,585 @@
|
|
1
|
+
# Schema class.
|
2
|
+
|
3
|
+
require 'sequel'
|
4
|
+
|
5
|
+
require 'miguel/dumper'
|
6
|
+
|
7
|
+
module Miguel
|
8
|
+
|
9
|
+
# Class for defining database schema.
|
10
|
+
class Schema
|
11
|
+
|
12
|
+
# Module for pretty printing of names, types, and especially options.
|
13
|
+
module Output
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def out_value( value )
|
18
|
+
case value
|
19
|
+
when Hash
|
20
|
+
"{" << ( value.map{ |k,v| "#{out_value( k )} => #{out_value( v )}" }.join( ', ' ) ) << "}"
|
21
|
+
when Array
|
22
|
+
"[" << ( value.map{ |v| out_value( v ) }.join( ', ' ) ) << "]"
|
23
|
+
when Sequel::LiteralString
|
24
|
+
"Sequel.lit(#{value.to_s.inspect})"
|
25
|
+
else
|
26
|
+
value.inspect
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def out_hash( value, prefix = ', ' )
|
31
|
+
return "" if value.empty?
|
32
|
+
prefix.dup << value.map{ |k,v| "#{out_value( k )} => #{out_value( v )}" }.join( ', ' )
|
33
|
+
end
|
34
|
+
|
35
|
+
public
|
36
|
+
|
37
|
+
def out_opts( prefix = ', ' )
|
38
|
+
out_hash( opts, prefix )
|
39
|
+
end
|
40
|
+
|
41
|
+
def out_canonic_opts( prefix = ', ' )
|
42
|
+
out_hash( canonic_opts, prefix )
|
43
|
+
end
|
44
|
+
|
45
|
+
def out_name
|
46
|
+
name.inspect
|
47
|
+
end
|
48
|
+
|
49
|
+
def out_type
|
50
|
+
type.to_s =~ /\A[A-Z]/ ? type.to_s : type.inspect
|
51
|
+
end
|
52
|
+
|
53
|
+
def out_columns
|
54
|
+
columns.inspect
|
55
|
+
end
|
56
|
+
|
57
|
+
def out_table_name
|
58
|
+
table_name.inspect
|
59
|
+
end
|
60
|
+
|
61
|
+
def out_default
|
62
|
+
out_value(default)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Class representing single database column.
|
67
|
+
class Column
|
68
|
+
|
69
|
+
include Output
|
70
|
+
|
71
|
+
# Column type, name and options.
|
72
|
+
attr_reader :type, :name, :opts
|
73
|
+
|
74
|
+
# Create new column with given type and name.
|
75
|
+
def initialize( type, name, opts = {} )
|
76
|
+
@type = type
|
77
|
+
@name = name
|
78
|
+
@opts = opts
|
79
|
+
end
|
80
|
+
|
81
|
+
# Get the column default.
|
82
|
+
def default
|
83
|
+
d = opts[ :default ]
|
84
|
+
d = type_default if d.nil? && ! allow_null
|
85
|
+
d
|
86
|
+
end
|
87
|
+
|
88
|
+
# Get default default for column type.
|
89
|
+
def type_default
|
90
|
+
case canonic_type
|
91
|
+
when :string
|
92
|
+
""
|
93
|
+
when :boolean
|
94
|
+
false
|
95
|
+
when :enum, :set
|
96
|
+
[ *opts[ :elements ], "" ].first
|
97
|
+
else
|
98
|
+
0
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Check whether the column allow NULL values.
|
103
|
+
def allow_null
|
104
|
+
allow = opts[ :null ]
|
105
|
+
allow.nil? || allow
|
106
|
+
end
|
107
|
+
|
108
|
+
# Options which are not relevant to type specification.
|
109
|
+
NON_TYPE_OPTS = [ :null, :default ]
|
110
|
+
|
111
|
+
# Get opts relevant to the column type only (excludes :null and :default).
|
112
|
+
def type_opts
|
113
|
+
opts.reject{ |key, value| NON_TYPE_OPTS.include? key }
|
114
|
+
end
|
115
|
+
|
116
|
+
# Canonic names of some builtin ruby types.
|
117
|
+
CANONIC_TYPES = {
|
118
|
+
:fixnum => :integer,
|
119
|
+
:bignum => :bigint,
|
120
|
+
:bigdecimal => :decimal,
|
121
|
+
:numeric => :decimal,
|
122
|
+
:float => :double,
|
123
|
+
:file => :blob,
|
124
|
+
:trueclass => :boolean,
|
125
|
+
:falseclass => :boolean,
|
126
|
+
}
|
127
|
+
|
128
|
+
# Get the canonic type name, for type comparison.
|
129
|
+
def canonic_type
|
130
|
+
t = type.to_s.downcase.to_sym
|
131
|
+
CANONIC_TYPES[ t ] || t
|
132
|
+
end
|
133
|
+
|
134
|
+
# Default options implied for certain types.
|
135
|
+
DEFAULT_OPTS = {
|
136
|
+
:string => { :size => 255 },
|
137
|
+
:bigint => { :size => 20 },
|
138
|
+
:decimal => { :size => [ 10, 0 ] },
|
139
|
+
:integer => { :unsigned => false },
|
140
|
+
}
|
141
|
+
|
142
|
+
# Options which are ignored for columns.
|
143
|
+
# These usually relate to the associated foreign key constraints, not the column itself.
|
144
|
+
IGNORED_OPTS = [ :key ]
|
145
|
+
|
146
|
+
# Get the column options in a canonic way.
|
147
|
+
def canonic_opts
|
148
|
+
return {} if type == :primary_key && name.is_a?( Array )
|
149
|
+
o = { :type => canonic_type, :default => default }
|
150
|
+
o.merge!( DEFAULT_OPTS[ canonic_type ] || {} )
|
151
|
+
o.merge!( opts )
|
152
|
+
o.delete_if{ |key, value| IGNORED_OPTS.include? key }
|
153
|
+
end
|
154
|
+
|
155
|
+
# Compare one column with another one.
|
156
|
+
def == other
|
157
|
+
other.is_a?( Column ) &&
|
158
|
+
name == other.name &&
|
159
|
+
canonic_type == other.canonic_type &&
|
160
|
+
canonic_opts == other.canonic_opts
|
161
|
+
end
|
162
|
+
|
163
|
+
# Dump column definition.
|
164
|
+
def dump( out )
|
165
|
+
out << "#{type} #{out_name}#{out_opts}"
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
# Class representing database index.
|
171
|
+
class Index
|
172
|
+
|
173
|
+
include Output
|
174
|
+
|
175
|
+
# Index column(s) and options.
|
176
|
+
attr_reader :columns, :opts
|
177
|
+
|
178
|
+
# Create new index for given column(s).
|
179
|
+
def initialize( columns, opts = {} )
|
180
|
+
@columns = [ *columns ]
|
181
|
+
@opts = opts
|
182
|
+
end
|
183
|
+
|
184
|
+
# Options we ignore when comparing.
|
185
|
+
IGNORED_OPTS = [ :null ]
|
186
|
+
|
187
|
+
# Get the index options, in a canonic way.
|
188
|
+
def canonic_opts
|
189
|
+
o = { :unique => false }
|
190
|
+
o.merge!( opts )
|
191
|
+
o.delete_if{ |key, value| IGNORED_OPTS.include? key }
|
192
|
+
end
|
193
|
+
|
194
|
+
# Compare one index with another one.
|
195
|
+
def == other
|
196
|
+
other.is_a?( Index ) &&
|
197
|
+
columns == other.columns &&
|
198
|
+
canonic_opts == other.canonic_opts
|
199
|
+
end
|
200
|
+
|
201
|
+
# Dump index definition.
|
202
|
+
def dump( out )
|
203
|
+
out << "index #{out_columns}#{out_opts}"
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
207
|
+
|
208
|
+
# Class representing foreign key constraint.
|
209
|
+
class ForeignKey
|
210
|
+
|
211
|
+
include Output
|
212
|
+
|
213
|
+
# Key's column(s), the target table name and options.
|
214
|
+
attr_reader :columns, :table_name, :opts
|
215
|
+
|
216
|
+
# Create new foreign key for given columns referring to given table.
|
217
|
+
def initialize( columns, table_name, opts = {} )
|
218
|
+
@columns = [ *columns ]
|
219
|
+
@table_name = table_name
|
220
|
+
@opts = opts
|
221
|
+
if key = opts[ :key ]
|
222
|
+
opts[ :key ] = [ *key ]
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# Options we ignore when comparing.
|
227
|
+
# These are usually tied to the underlying column, not constraint.
|
228
|
+
IGNORED_OPTS = [ :null, :unsigned, :type ]
|
229
|
+
|
230
|
+
# Get the foreign key options, in a canonic way.
|
231
|
+
def canonic_opts
|
232
|
+
opts.reject{ |key, value| IGNORED_OPTS.include? key }
|
233
|
+
end
|
234
|
+
|
235
|
+
# Compare one foreign key with another one.
|
236
|
+
def == other
|
237
|
+
other.is_a?( ForeignKey ) &&
|
238
|
+
columns == other.columns &&
|
239
|
+
table_name == other.table_name &&
|
240
|
+
canonic_opts == other.canonic_opts
|
241
|
+
end
|
242
|
+
|
243
|
+
# Dump foreign key definition.
|
244
|
+
def dump( out )
|
245
|
+
out << "foreign_key #{out_columns}, #{out_table_name}#{out_opts}"
|
246
|
+
end
|
247
|
+
|
248
|
+
end
|
249
|
+
|
250
|
+
# Class representing database table.
|
251
|
+
class Table
|
252
|
+
|
253
|
+
include Output
|
254
|
+
|
255
|
+
# Helper class used to evaluate the +add_table+ block.
|
256
|
+
# Also implements the timestamping helper.
|
257
|
+
class Context
|
258
|
+
|
259
|
+
# Create new context for given table.
|
260
|
+
def initialize( table )
|
261
|
+
@table = table
|
262
|
+
end
|
263
|
+
|
264
|
+
# Send anything unrecognized as new definition to our table.
|
265
|
+
def method_missing( name, *args )
|
266
|
+
@table.add_definition( name, *args )
|
267
|
+
end
|
268
|
+
|
269
|
+
# The +method_missing+ doesn't take care of constant like methods (like String :name),
|
270
|
+
# so those have to be defined explicitly for each such supported type.
|
271
|
+
for type in Sequel::Schema::Generator::GENERIC_TYPES
|
272
|
+
class_eval( "def #{type}(*args) ; @table.add_definition(:#{type},*args) ; end", __FILE__, __LINE__ )
|
273
|
+
end
|
274
|
+
|
275
|
+
# Create the default timestamp fields.
|
276
|
+
def timestamps
|
277
|
+
# Unfortunately, MySQL allows only either automatic create timestamp
|
278
|
+
# (DEFAULT CURRENT_TIMESTAMP) or automatic update timestamp (DEFAULT
|
279
|
+
# CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP), but not both - one
|
280
|
+
# has to be updated manually anyway. So we choose to have the update timestamp
|
281
|
+
# automatically updated, and let the create one to be set manually.
|
282
|
+
# Also, Sequel doesn't currently honor :on_update for column definitions,
|
283
|
+
# so we have to use default literal to make it work. Sigh.
|
284
|
+
timestamp :create_time, :null => false, :default => 0
|
285
|
+
timestamp :update_time, :null => false, :default => Sequel.lit( 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP' )
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
# Schema to which this table belongs.
|
290
|
+
attr_reader :schema
|
291
|
+
|
292
|
+
# Name of the table.
|
293
|
+
attr_reader :name
|
294
|
+
|
295
|
+
# List of table indices and foreign keys.
|
296
|
+
attr_reader :indexes, :foreign_keys
|
297
|
+
|
298
|
+
# Create new table with given name belonging to given schema.
|
299
|
+
def initialize( schema, name )
|
300
|
+
@schema = schema
|
301
|
+
@name = name
|
302
|
+
@columns = {}
|
303
|
+
@indexes = []
|
304
|
+
@foreign_keys = []
|
305
|
+
end
|
306
|
+
|
307
|
+
# Get all columns.
|
308
|
+
def columns
|
309
|
+
@columns.values
|
310
|
+
end
|
311
|
+
|
312
|
+
# Get names of all table columns.
|
313
|
+
def column_names
|
314
|
+
@columns.keys
|
315
|
+
end
|
316
|
+
|
317
|
+
# Get given named columns.
|
318
|
+
def named_columns( names )
|
319
|
+
@columns.values_at( *names )
|
320
|
+
end
|
321
|
+
|
322
|
+
# Add column definition.
|
323
|
+
def add_column( type, name, *args )
|
324
|
+
fail( ArgumentError, "column #{name} in table #{self.name} is already defined" ) if @columns[ name ]
|
325
|
+
@columns[ name ] = Column.new( type, name, *args )
|
326
|
+
end
|
327
|
+
|
328
|
+
# Add index definition.
|
329
|
+
def add_index( columns, *args )
|
330
|
+
@indexes << Index.new( columns, *args )
|
331
|
+
end
|
332
|
+
|
333
|
+
# Add foreign key definition.
|
334
|
+
def add_foreign_key( columns, table_name, *args )
|
335
|
+
add_column( :integer, columns, *args ) unless columns.is_a? Array
|
336
|
+
@foreign_keys << ForeignKey.new( columns, table_name, *args )
|
337
|
+
end
|
338
|
+
|
339
|
+
# Add definition of column, index or foreign key.
|
340
|
+
def add_definition( name, *args )
|
341
|
+
name, *args = schema.apply_defaults( self.name, name, *args )
|
342
|
+
case name
|
343
|
+
when :index
|
344
|
+
add_index( *args )
|
345
|
+
when :foreign_key
|
346
|
+
add_foreign_key( *args )
|
347
|
+
else
|
348
|
+
add_column( name, *args )
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
# Define table using the provided block.
|
353
|
+
def define( &block )
|
354
|
+
fail( ArgumentError, "missing table definition block" ) unless block
|
355
|
+
Context.new( self ).instance_eval( &block )
|
356
|
+
self
|
357
|
+
end
|
358
|
+
|
359
|
+
# Dump table definition to given output.
|
360
|
+
def dump( out = Dumper.new )
|
361
|
+
out.dump "table #{out_name}" do
|
362
|
+
for column in columns
|
363
|
+
column.dump( out )
|
364
|
+
end
|
365
|
+
for index in indexes
|
366
|
+
index.dump( out )
|
367
|
+
end
|
368
|
+
for foreign_key in foreign_keys
|
369
|
+
foreign_key.dump( out )
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
end
|
375
|
+
|
376
|
+
# Create new schema.
|
377
|
+
def initialize
|
378
|
+
@tables = {}
|
379
|
+
@aliases = {}
|
380
|
+
@defaults = {}
|
381
|
+
@callbacks = {}
|
382
|
+
end
|
383
|
+
|
384
|
+
# Get all tables.
|
385
|
+
def tables
|
386
|
+
@tables.values
|
387
|
+
end
|
388
|
+
|
389
|
+
# Get names of all tables.
|
390
|
+
def table_names
|
391
|
+
@tables.keys
|
392
|
+
end
|
393
|
+
|
394
|
+
# Get tables with given names.
|
395
|
+
def named_tables( names )
|
396
|
+
@tables.values_at( *names )
|
397
|
+
end
|
398
|
+
|
399
|
+
# Add table with given name, optionally defined with provided block.
|
400
|
+
def add_table( name, &block )
|
401
|
+
name = name.to_sym
|
402
|
+
fail( ArgumentError, "table #{name} is already defined" ) if @tables[ name ]
|
403
|
+
@tables[ name ] = table = Table.new( self, name )
|
404
|
+
table.define( &block ) if block
|
405
|
+
table
|
406
|
+
end
|
407
|
+
alias table add_table
|
408
|
+
|
409
|
+
# Helper for creating join tables conveniently.
|
410
|
+
# It is equivalent to the following:
|
411
|
+
# add_table name do
|
412
|
+
# foreign_key id_left, table_left
|
413
|
+
# foreign_key id_right, table_right
|
414
|
+
# primary_key [id_left, id_right]
|
415
|
+
# unique [id_right, id_left]
|
416
|
+
# end
|
417
|
+
# In case a block is provided, it is used to further extend the table defined.
|
418
|
+
def add_join_table( id_left, table_left, id_right, table_right, name = nil, &block )
|
419
|
+
name ||= [ table_left, table_right ].sort.join( '_' )
|
420
|
+
add_table name do
|
421
|
+
foreign_key id_left, table_left
|
422
|
+
foreign_key id_right, table_right
|
423
|
+
primary_key [ id_left, id_right ]
|
424
|
+
unique [ id_right, id_left ]
|
425
|
+
instance_eval &block if block
|
426
|
+
end
|
427
|
+
end
|
428
|
+
alias join_table add_join_table
|
429
|
+
|
430
|
+
# Set default options for given statement used in +add_table+ blocks.
|
431
|
+
# It uses the following arguments:
|
432
|
+
# +name+:: The name of the statement, like +:primary_key+ or +:String+.
|
433
|
+
# The special name +:global+ may be used to set default options for any statement.
|
434
|
+
# +alias+:: Optional real statement to use instead of +name+, like +:String+ instead of +:Text+.
|
435
|
+
# +args+:: Hash containing the default options for +name+.
|
436
|
+
# +block+:: Optional block which may further modify the options.
|
437
|
+
#
|
438
|
+
# If a block is provided, it is invoked with the following arguments:
|
439
|
+
# +opts+:: The trailing options passed to given statement, to be modified as necessary.
|
440
|
+
# +args+:: Any leading arguments passed to given statement, as readonly context.
|
441
|
+
# +table+:: The name of the currently defined table, as readonly context.
|
442
|
+
#
|
443
|
+
# The final options for each statement are created in the following
|
444
|
+
# order: +:global+ options, extended with +:null+ set to +true+ in case of ? syntax,
|
445
|
+
# merged with options for +name+ (without ?), modified by the optional +block+
|
446
|
+
# callback, and merged with the original options used with the statement.
|
447
|
+
#
|
448
|
+
# Also note that the defaults are applied in the instant the +table+ block is evaluated,
|
449
|
+
# so it is eventually possible (though not necessarily recommended) to change them in between.
|
450
|
+
def set_defaults( name, *args, &block )
|
451
|
+
@aliases[ name ] = args.shift if args.first.is_a? Symbol
|
452
|
+
@defaults[ name ] = args.pop if args.last.is_a? Hash
|
453
|
+
@callbacks[ name ] = block
|
454
|
+
fail( ArgumentError, "invalid defaults for #{name}" ) unless args.empty?
|
455
|
+
end
|
456
|
+
|
457
|
+
# Get default options for given statement.
|
458
|
+
def get_defaults( name )
|
459
|
+
@defaults[ name ] || {}
|
460
|
+
end
|
461
|
+
|
462
|
+
# Set standard defaults and aliases for often used types.
|
463
|
+
#
|
464
|
+
# The current set of defaults is as follows:
|
465
|
+
#
|
466
|
+
# :global, :null => false
|
467
|
+
# :primary_key, :type => :integer, :unsigned => true
|
468
|
+
# :foreign_key, :key => :id, :type => :integer, :unsigned => true
|
469
|
+
# :unique, :index, :unique => true
|
470
|
+
# :Bool, :TrueClass
|
471
|
+
# :True, :TrueClass, :default => true
|
472
|
+
# :False, :TrueClass, :default => false
|
473
|
+
# :Signed, :integer, :unsigned => false
|
474
|
+
# :Unsigned, :integer, :unsigned => true
|
475
|
+
# :Text, :String, :text => true
|
476
|
+
# :Time, :timestamp, :default => 0
|
477
|
+
# :Time?, :timestamp, :default => nil
|
478
|
+
def set_standard_defaults
|
479
|
+
|
480
|
+
# We set NOT NULL on everything by default, but note the ?
|
481
|
+
# syntax (like Text?) which declares the column as NULL.
|
482
|
+
# We also like our keys unsigned, so we make that a default, too.
|
483
|
+
# Unfortunately, :unsigned currently works only with :integer,
|
484
|
+
# not the default :Integer, and :integer can't be specified for compound keys,
|
485
|
+
# so we have to use the callback to set the type only at correct times.
|
486
|
+
|
487
|
+
set_defaults :global, :null => false
|
488
|
+
set_defaults :primary_key, :unsigned => true do |opts,args,table|
|
489
|
+
opts[ :type ] ||= :integer unless args.first.is_a? Array
|
490
|
+
end
|
491
|
+
set_defaults :foreign_key, :key => :id, :unsigned => true do |opts,args,table|
|
492
|
+
opts[ :type ] ||= :integer unless args.first.is_a? Array
|
493
|
+
end
|
494
|
+
|
495
|
+
# Save some typing for unique indexes.
|
496
|
+
|
497
|
+
set_defaults :unique, :index, :unique => true
|
498
|
+
|
499
|
+
# Type shortcuts we use frequently.
|
500
|
+
|
501
|
+
set_defaults :Bool, :TrueClass
|
502
|
+
set_defaults :True, :TrueClass, :default => true
|
503
|
+
set_defaults :False, :TrueClass, :default => false
|
504
|
+
|
505
|
+
set_defaults :Signed, :integer, :unsigned => false
|
506
|
+
set_defaults :Unsigned, :integer, :unsigned => true
|
507
|
+
|
508
|
+
set_defaults :Text, :String, :text => true
|
509
|
+
|
510
|
+
# We want times to be stored as 4 byte timestamps, however
|
511
|
+
# we have to be careful to turn off the MySQL autoupdate behavior.
|
512
|
+
# That's why we have to set defaults explicitly.
|
513
|
+
|
514
|
+
set_defaults :Time, :timestamp, :default => 0
|
515
|
+
set_defaults :Time?, :timestamp, :default => nil
|
516
|
+
|
517
|
+
self
|
518
|
+
end
|
519
|
+
|
520
|
+
# Apply default options to given +add_table+ block statement.
|
521
|
+
# See +set_defaults+ for detailed explanation.
|
522
|
+
def apply_defaults( table_name, name, *args )
|
523
|
+
opts = {}
|
524
|
+
opts.merge!( get_defaults( :global ) )
|
525
|
+
|
526
|
+
if name[ -1 ] == '?'
|
527
|
+
opts[ :null ] = true
|
528
|
+
original_name = name
|
529
|
+
name = name[ 0..-2 ].to_sym
|
530
|
+
end
|
531
|
+
|
532
|
+
opts.merge!( get_defaults( name ) )
|
533
|
+
opts.merge!( get_defaults( original_name ) ) if original_name
|
534
|
+
|
535
|
+
if callback = @callbacks[ name ]
|
536
|
+
callback.call( opts, args, table_name )
|
537
|
+
end
|
538
|
+
|
539
|
+
opts.merge!( args.pop ) if args.last.is_a? Hash
|
540
|
+
args << opts unless opts.empty?
|
541
|
+
|
542
|
+
[ ( @aliases[ name ] || name ), *args ]
|
543
|
+
end
|
544
|
+
|
545
|
+
# Dump table definition to given output.
|
546
|
+
def dump( out = Dumper.new )
|
547
|
+
for table in tables
|
548
|
+
table.dump( out )
|
549
|
+
end
|
550
|
+
out
|
551
|
+
end
|
552
|
+
|
553
|
+
# Define schema with the provided block.
|
554
|
+
def define( opts = {}, &block )
|
555
|
+
fail( ArgumentError, "missing schema block" ) unless block
|
556
|
+
set_standard_defaults unless opts[ :use_defaults ] == false
|
557
|
+
instance_eval &block
|
558
|
+
self
|
559
|
+
end
|
560
|
+
|
561
|
+
class << self
|
562
|
+
|
563
|
+
# The most recent schema defined by Schema.define.
|
564
|
+
attr_reader :schema
|
565
|
+
|
566
|
+
# Define schema with provided block.
|
567
|
+
def define( opts = {}, &block )
|
568
|
+
@schema = new.define( opts, &block )
|
569
|
+
end
|
570
|
+
|
571
|
+
# Load schema from given file.
|
572
|
+
def load( name )
|
573
|
+
@schema = nil
|
574
|
+
name = File.expand_path( name )
|
575
|
+
Kernel.load( name )
|
576
|
+
schema
|
577
|
+
end
|
578
|
+
|
579
|
+
end
|
580
|
+
|
581
|
+
end
|
582
|
+
|
583
|
+
end
|
584
|
+
|
585
|
+
# EOF #
|
data/lib/miguel.rb
ADDED
data/miguel.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# Gem specification.
|
2
|
+
|
3
|
+
require File.expand_path( '../lib/miguel/version', __FILE__ )
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'miguel'
|
7
|
+
s.version = Miguel::VERSION + '.pre1'
|
8
|
+
s.summary = 'Database migrator and migration generator for Sequel.'
|
9
|
+
s.description = <<EOT
|
10
|
+
This gem makes it easy to create and maintain an up-to-date database schema
|
11
|
+
and apply it to the database as needed by the means of standard Sequel migrations."
|
12
|
+
EOT
|
13
|
+
|
14
|
+
s.author = 'Patrik Rak'
|
15
|
+
s.email = 'patrik@raxoft.cz'
|
16
|
+
s.homepage = 'http://rubygems.org/gems/miguel'
|
17
|
+
s.license = 'MIT'
|
18
|
+
|
19
|
+
s.files = `git ls-files`.split( "\n" )
|
20
|
+
s.executables = `git ls-files -- bin/*`.split( "\n" ).map{ |f| File.basename( f ) }
|
21
|
+
|
22
|
+
s.add_runtime_dependency 'sequel', '~> 4.0'
|
23
|
+
s.add_development_dependency 'bacon', '~> 1.2'
|
24
|
+
end
|
25
|
+
|
26
|
+
# EOF #
|
data/test/test_dumper.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
# Test Dumper.
|
2
|
+
|
3
|
+
require 'miguel/dumper'
|
4
|
+
|
5
|
+
describe Miguel::Dumper do
|
6
|
+
|
7
|
+
should 'collect dumped lines' do
|
8
|
+
d = Miguel::Dumper.new
|
9
|
+
d.dump "a"
|
10
|
+
d << "b"
|
11
|
+
d.dump "c"
|
12
|
+
d << "d"
|
13
|
+
d.text.should == "a\nb\nc\nd\n"
|
14
|
+
end
|
15
|
+
|
16
|
+
should 'support nesting' do
|
17
|
+
d = Miguel::Dumper.new
|
18
|
+
d.dump "test" do
|
19
|
+
d.dump "row" do
|
20
|
+
d << "x"
|
21
|
+
d << "y"
|
22
|
+
end
|
23
|
+
d.dump "foo" do
|
24
|
+
d << "bar"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
d.text.should == <<EOT
|
28
|
+
test do
|
29
|
+
row do
|
30
|
+
x
|
31
|
+
y
|
32
|
+
end
|
33
|
+
foo do
|
34
|
+
bar
|
35
|
+
end
|
36
|
+
end
|
37
|
+
EOT
|
38
|
+
end
|
39
|
+
|
40
|
+
should 'support text interpolation' do
|
41
|
+
d = Miguel::Dumper.new
|
42
|
+
d << "abc"
|
43
|
+
d << "xyz"
|
44
|
+
d.text.should == d.to_s
|
45
|
+
d.text.should == "#{d}"
|
46
|
+
end
|
47
|
+
|
48
|
+
should 'accept nonstring arguments' do
|
49
|
+
d = Miguel::Dumper.new
|
50
|
+
d << 123
|
51
|
+
d << 0.5
|
52
|
+
d << :test
|
53
|
+
d.text.should == "123\n0.5\ntest\n"
|
54
|
+
end
|
55
|
+
|
56
|
+
should 'support chaining' do
|
57
|
+
d = Miguel::Dumper.new
|
58
|
+
d.dump( "x" ).should == d
|
59
|
+
( d << "y" << "z" ).should == d
|
60
|
+
d.text.should == "x\ny\nz\n"
|
61
|
+
end
|
62
|
+
|
63
|
+
should 'support custom buffer' do
|
64
|
+
a = []
|
65
|
+
d = Miguel::Dumper.new( a )
|
66
|
+
d << "xyz"
|
67
|
+
d << "abc"
|
68
|
+
a.should == [ "xyz\n", "abc\n" ]
|
69
|
+
d.text.should == "xyz\nabc\n"
|
70
|
+
end
|
71
|
+
|
72
|
+
should 'support configurable indentation' do
|
73
|
+
d = Miguel::Dumper.new( [], 4 )
|
74
|
+
d.dump "a" do
|
75
|
+
d.dump "b" do
|
76
|
+
d << "c"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
d.text.should == <<EOT
|
80
|
+
a do
|
81
|
+
b do
|
82
|
+
c
|
83
|
+
end
|
84
|
+
end
|
85
|
+
EOT
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
# EOF #
|