miguel 0.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 #
@@ -0,0 +1,10 @@
1
+ # Version number.
2
+
3
+ module Miguel
4
+ MAJOR = 0
5
+ MINOR = 1
6
+ PATCH = 0
7
+ VERSION = [ MAJOR, MINOR, PATCH ].join( '.' ).freeze
8
+ end
9
+
10
+ # EOF #
data/lib/miguel.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'miguel/version'
2
+ require 'miguel/migrator'
3
+ require 'miguel/importer'
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 #
@@ -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 #