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.
@@ -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 #