miguel 0.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1c7f6a7a9e366374d9d88b5c5dde5e3ca0e8e9fa
4
+ data.tar.gz: 99bc1f4cb8a9ab1bc6f84b8595f541d5226b6f0c
5
+ SHA512:
6
+ metadata.gz: 3e1d62b3872b4961a7df1276cf82ec89829b81d05560ce3d435d48f9a077bc60e46a919a4b21ae7131e24c01ceb521ebd176120525a01ff7f8d989be66335835
7
+ data.tar.gz: 483d87a4bd5c54251dce7255e99e54238ddcc6a729d820c881fe841153732854b55b697895b0749d3cd6a07533ea237a43c66abb40dd6b0b4678436f4324908e
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ *~
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # Rake makefile.
2
+
3
+ task :default => :test
4
+
5
+ desc 'Run tests'
6
+ task :test do
7
+ sh "bacon --automatic --quiet"
8
+ end
9
+
10
+ # EOF #
data/TODO ADDED
@@ -0,0 +1,2 @@
1
+ add tests
2
+ write docs
data/bin/miguel ADDED
@@ -0,0 +1,9 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ $:.unshift File.dirname( __FILE__ ) + '/../lib'
4
+
5
+ require 'miguel/command'
6
+
7
+ Miguel::Command.new.run( ARGV )
8
+
9
+ # EOF #
@@ -0,0 +1,242 @@
1
+ # Command line driver.
2
+
3
+ require 'miguel'
4
+ require 'optparse'
5
+
6
+ module Miguel
7
+
8
+ # Miguel command line interface.
9
+ class Command
10
+
11
+ attr_reader :env, :format, :loggers, :force, :quiet, :trace
12
+
13
+ # Initialize the command options.
14
+ def init
15
+ @env = nil
16
+ @format = nil
17
+ @loggers = []
18
+ @force = nil
19
+ @quiet = nil
20
+ @trace = nil
21
+ end
22
+
23
+ # Run the command.
24
+ def run( args )
25
+ args = args.dup
26
+ init
27
+ OptionParser.new( &method( :set_opts ) ).parse!( args )
28
+ execute( args )
29
+ exit 0
30
+ rescue Exception => e
31
+ raise if trace or e.is_a?( SystemExit )
32
+ $stderr.print "#{e.class}: " unless e.is_a?( RuntimeError )
33
+ $stderr.puts e.message
34
+ exit 1
35
+ end
36
+
37
+ private
38
+
39
+ # Set the command options.
40
+ def set_opts( opts )
41
+ opts.banner = "Miguel: The Database Migrator and Migration Generator for Sequel"
42
+ opts.define_head "Usage: miguel [options] <command> <db|schema> [db|schema]"
43
+ opts.separator ""
44
+ opts.separator "Examples:"
45
+ opts.separator " miguel show mysql://localhost/main"
46
+ opts.separator " miguel dump schema.rb"
47
+ opts.separator " miguel diff db.yml test.yml"
48
+ opts.separator " miguel apply db.yml schema.rb"
49
+ opts.separator ""
50
+ opts.separator "Commands:"
51
+ opts.separator " show <db|schema> Show schema of given database or schema file"
52
+ opts.separator " dump <db|schema> Dump migration which creates given schema"
53
+ opts.separator " down <db|schema> Dump migration which reverses given schema"
54
+ opts.separator " diff <db|schema> <db|schema> Dump migration needed to migrate to the second schema"
55
+ opts.separator " apply <db> <db|schema> Apply given schema to the database"
56
+ opts.separator " clear <db> Drop all tables in given database"
57
+ opts.separator ""
58
+ opts.separator "Options:"
59
+
60
+ opts.on_tail( '-h', '-?', '--help', 'Show this message' ) do
61
+ puts opts
62
+ exit
63
+ end
64
+
65
+ opts.on( '-e', '--env <env>', 'Use given environment config for database(s)' ) do |v|
66
+ @env = v
67
+ end
68
+
69
+ opts.on( '-E', '--echo', 'Echo SQL statements' ) do
70
+ require 'logger'
71
+ @loggers << Logger.new( $stdout )
72
+ end
73
+
74
+ opts.on( '-f', '--force', 'Force changes to be applied without confirmation' ) do
75
+ @force = true
76
+ end
77
+
78
+ opts.on( '-l', '--log <file>', 'Log SQL statements to given file' ) do |v|
79
+ require 'logger'
80
+ @loggers << Logger.new( v )
81
+ end
82
+
83
+ formats = [ :bare, :change, :full ]
84
+ opts.on( '-m', '--migration <format>', formats, "Select format for dumped migrations (#{formats.join(', ')})" ) do |v|
85
+ @format = v
86
+ end
87
+
88
+ opts.on( '-q', '--quiet', "Don't display the changes to be applied" ) do
89
+ @quiet = true
90
+ end
91
+
92
+ opts.on( '-t', '--trace', 'Show full backtrace if an exception is raised' ) do
93
+ @trace = true
94
+ end
95
+
96
+ opts.on_tail( '-v', '--version', 'Print version' ) do
97
+ puts "miguel #{Miguel::VERSION}"
98
+ exit
99
+ end
100
+ end
101
+
102
+ # Execute the command itself.
103
+ def execute( args )
104
+ command = args.shift or fail "Missing command, use -h to see usage."
105
+ case command
106
+ when 'show'
107
+ check_args( args, 1 )
108
+ schema = get_schema( args.shift )
109
+ print schema.dump
110
+ when 'dump'
111
+ check_args( args, 1 )
112
+ schema = get_schema( args.shift )
113
+ show_changes( Schema.new, schema )
114
+ when 'down'
115
+ check_args( args, 1 )
116
+ schema = get_schema( args.shift )
117
+ show_changes( schema, Schema.new )
118
+ when 'diff'
119
+ check_args( args, 2 )
120
+ old_schema = get_schema( args.shift )
121
+ new_schema = get_schema( args.shift )
122
+ show_changes( old_schema, new_schema )
123
+ when 'apply'
124
+ check_args( args, 2 )
125
+ db = get_db( args.shift )
126
+ schema = get_schema( args.shift )
127
+ apply_schema( db, schema )
128
+ when 'clear'
129
+ check_args( args, 1 )
130
+ db = get_db( args.shift )
131
+ apply_schema( db, Schema.new )
132
+ else
133
+ fail "Invalid command, use -h to see usage."
134
+ end
135
+ end
136
+
137
+ # Make sure the argument count is as expected.
138
+ def check_args( args, count )
139
+ fail "Not enough arguments present', use -h to see usage." if args.count < count
140
+ fail "Extra arguments present', use -h to see usage." if args.count > count
141
+ end
142
+
143
+ # Import schema from given database.
144
+ def import_schema( db )
145
+ Importer.new( db ).schema
146
+ end
147
+
148
+ # Get schema from given schema file or database.
149
+ def get_schema( name )
150
+ schema = if name.nil? or name.empty?
151
+ fail "Missing database or schema name."
152
+ elsif File.exist?( name ) and name =~ /\.rb\b/
153
+ Schema.load( name ) or fail "No schema loaded from file '#{name}'."
154
+ else
155
+ db = get_db( name )
156
+ import_schema( db )
157
+ end
158
+ schema
159
+ end
160
+
161
+ # Connect to given database.
162
+ def get_db( name )
163
+ db = if name.nil? or name.empty?
164
+ fail "Missing database name."
165
+ elsif File.exist?( name )
166
+ require 'yaml'
167
+ config = YAML.load_file( name )
168
+ env = self.env || "development"
169
+ config = config[ env ] || config[ env.to_sym ] || config
170
+ config.keys.each{ |k| config[ k.to_sym ] = config.delete( k ) }
171
+ Sequel.connect( config )
172
+ else
173
+ Sequel.connect( name )
174
+ end
175
+ db.loggers = loggers
176
+ db
177
+ end
178
+
179
+ # Show changes between the two schemas.
180
+ def show_changes( from, to )
181
+ m = Migrator.new
182
+ case format
183
+ when nil, :bare
184
+ print m.changes( from, to )
185
+ when :change
186
+ print m.change_migration( from, to )
187
+ when :full
188
+ print m.full_migration( from, to )
189
+ end
190
+ end
191
+
192
+ # Apply given schema to given database.
193
+ def apply_schema( db, schema )
194
+ from = import_schema( db )
195
+ changes = Migrator.new.changes( from, schema ).to_s
196
+
197
+ if changes.empty?
198
+ puts "No changes are necessary." unless quiet
199
+ return
200
+ end
201
+
202
+ unless quiet
203
+ puts "These changes will be applied to the database:"
204
+ print changes
205
+ end
206
+
207
+ unless force
208
+ fail "OK, aborting." unless confirm?
209
+ end
210
+
211
+ db.instance_eval( changes )
212
+
213
+ puts "OK, those changes were applied." unless quiet
214
+ end
215
+
216
+ # Ask the user for a confirmation.
217
+ def confirm?
218
+ loop do
219
+ print "Confirm (yes or no)? "
220
+
221
+ unless line = $stdin.gets
222
+ puts
223
+ puts "I take EOF as 'no'. Use --force if you want to skip the confirmation instead."
224
+ return
225
+ end
226
+
227
+ case line.chomp.downcase
228
+ when 'yes'
229
+ return true
230
+ when 'no'
231
+ return false
232
+ else
233
+ puts "Please answer 'yes' or 'no'."
234
+ end
235
+ end
236
+ end
237
+
238
+ end
239
+
240
+ end
241
+
242
+ # EOF #
@@ -0,0 +1,45 @@
1
+ # Simple dumper.
2
+
3
+ module Miguel
4
+
5
+ # Class for dumping indented code blocks.
6
+ class Dumper
7
+
8
+ # Create new dumper.
9
+ def initialize( out = [], step = 2 )
10
+ @out = out
11
+ @indent = 0
12
+ @step = step
13
+ end
14
+
15
+ # Get all output gathered so far as a string.
16
+ def text
17
+ @out.join
18
+ end
19
+
20
+ alias to_s text
21
+
22
+ # Append given line/block to the output.
23
+ #
24
+ # If block is given, it is automatically enclosed between do/end keywords
25
+ # and anything dumped within it is automatically indented.
26
+ def dump( line )
27
+ if block_given?
28
+ dump "#{line} do"
29
+ @indent += @step
30
+ yield
31
+ @indent -= @step
32
+ dump "end"
33
+ else
34
+ @out << "#{' ' * @indent}#{line}\n"
35
+ end
36
+ self
37
+ end
38
+
39
+ alias << dump
40
+
41
+ end
42
+
43
+ end
44
+
45
+ # EOF #
@@ -0,0 +1,232 @@
1
+ # Sequel schema import.
2
+
3
+ require 'miguel/schema'
4
+
5
+ module Miguel
6
+
7
+ # Class for importing database schema from Sequel database.
8
+ class Importer
9
+
10
+ # The database we operate upon.
11
+ attr_reader :db
12
+
13
+ # Create new instance for importing schema from given database.
14
+ def initialize( db )
15
+ @db = db
16
+ end
17
+
18
+ private
19
+
20
+ # Which characrets we convert when parsing enum values.
21
+ # Quite likely not exhaustive, but sufficient for our purposes.
22
+ ESCAPED_CHARS = {
23
+ "''" => "'",
24
+ "\\\\" => "\\",
25
+ "\\n" => "\n",
26
+ "\\t" => "\t",
27
+ }
28
+
29
+ # Regexp matching escaped sequences in enum values.
30
+ ESCAPED_CHARS_RE = /''|\\./
31
+
32
+ # Parse the element values for enum/set types.
33
+ def parse_elements( string )
34
+ string.scan(/'((?:[^']|'')*)'/).flatten.map do
35
+ |x| x.gsub( ESCAPED_CHARS_RE ){ |c| ESCAPED_CHARS[ c ] || c }
36
+ end
37
+ end
38
+
39
+ # Convert given database type to type and optional options used by our schema definitions.
40
+ # The ruby type provided serves as a hint of what Sequel's idea of the type is.
41
+ def revert_type_literal_internal( type, ruby_type )
42
+
43
+ return :boolean, :default_size => 1 if ruby_type == :boolean
44
+
45
+ case db.database_type
46
+ when :mysql
47
+ case type
48
+ when /\Aint\(\d+\)\z/
49
+ return :integer, :default_size => 11
50
+ when /\Aint\(\d+\) unsigned\z/
51
+ return :integer, :unsigned => true, :default_size => 10
52
+ when /\Abigint\(\d+\)\z/
53
+ return :bigint, :default_size => 20
54
+ when /\Adecimal\(\d+,\d+\)\z/
55
+ return :decimal, :default_size => [ 10, 0 ]
56
+ when /\A(enum|set)\((.*)\)\z/
57
+ return $1.to_sym, :elements => parse_elements( $2 )
58
+ end
59
+ end
60
+
61
+ case type
62
+ when /\Avarchar/
63
+ return :string, :default_size => 255
64
+ when /\Achar/
65
+ return :string, :fixed => true, :default_size => 255
66
+ when /\Atext\z/
67
+ return :string, :text => true
68
+ when /\A(\w+)\([\s\d,]+\)\z/
69
+ return $1.to_sym
70
+ when /\A\w+\z/
71
+ return type
72
+ end
73
+
74
+ ruby_type
75
+ end
76
+
77
+ # Convert given database type to type and optional options used by our schema definitions.
78
+ # The ruby type provided serves as a hint of what Sequel's idea of the type is.
79
+ def revert_type_literal( type, ruby_type )
80
+
81
+ case type
82
+ when /\(\s*(\d+)\s*\)/
83
+ size = $1.to_i
84
+ when /\(([\s\d,]+)\)/
85
+ size = $1.split( ',' ).map{ |x| x.to_i }
86
+ end
87
+
88
+ type, opts = revert_type_literal_internal( type, ruby_type )
89
+
90
+ opts ||= {}
91
+
92
+ default_size = opts.delete( :default_size )
93
+
94
+ if size and size != default_size
95
+ opts[ :size ] = size
96
+ end
97
+
98
+ [ type, opts ]
99
+ end
100
+
101
+ # Import indexes of given table.
102
+ def import_indexes( table )
103
+ # Foreign keys also automatically create indexes, which we must exclude when importing.
104
+ # But only if they look like indexes named by the automatic foreign key naming convention.
105
+ foreign_key_indexes = table.foreign_keys.map{ |x| x.columns if x.columns.size == 1 }.compact
106
+ for name, opts in db.indexes( table.name )
107
+ opts = opts.dup
108
+ columns = opts.delete( :columns )
109
+ next if ( ! opts[ :unique ] ) && foreign_key_indexes.include?( columns ) && name == columns.first
110
+ table.add_index( columns, opts )
111
+ end
112
+ end
113
+
114
+ # Import foreign keys of given table.
115
+ def import_foreign_keys( table )
116
+ for opts in db.foreign_key_list( table.name )
117
+ opts = opts.dup
118
+ name = opts.delete( :name )
119
+ columns = opts.delete( :columns )
120
+ table_name = opts.delete( :table )
121
+ table.add_foreign_key( columns, table_name, opts )
122
+ end
123
+ end
124
+
125
+ # Our custom mapping of some database defaults into the values we use in our schemas.
126
+ DEFAULT_CONSTANTS = {
127
+ "0000-00-00 00:00:00" => 0,
128
+ "CURRENT_TIMESTAMP" => Sequel.lit("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"),
129
+ }
130
+
131
+ # Options which are ignored for columns.
132
+ # These are usually just schema hints which the user normally doesn't specify.
133
+ IGNORED_OPTS = [ :max_length ]
134
+
135
+ # Import columns of given table.
136
+ def import_columns( table )
137
+ schema = db.schema( table.name )
138
+
139
+ # Get info about primary key columns.
140
+
141
+ primary_key_columns = schema.select{ |name, opts| opts[ :primary_key ] }
142
+
143
+ multi_primary_key = ( primary_key_columns.count > 1 )
144
+
145
+ # Import each column in sequence.
146
+
147
+ for name, opts in schema
148
+
149
+ opts = opts.dup
150
+
151
+ # Discard anything we don't need.
152
+
153
+ opts.delete_if{ |key, value| IGNORED_OPTS.include? key }
154
+
155
+ # Import type.
156
+
157
+ type = opts.delete( :type )
158
+ db_type = opts.delete( :db_type )
159
+
160
+ type, type_opts = revert_type_literal( db_type, type )
161
+ opts.merge!( type_opts ) if type_opts
162
+
163
+ # Import NULL option.
164
+
165
+ opts[ :null ] = opts.delete( :allow_null )
166
+
167
+ # Import default value.
168
+
169
+ default = opts.delete( :default )
170
+ ruby_default = opts.delete( :ruby_default )
171
+
172
+ default = DEFAULT_CONSTANTS[ default ] || ( ruby_default.nil? ? default : ruby_default )
173
+
174
+ unless default.nil?
175
+ opts[ :default ] = default
176
+ end
177
+
178
+ # Deal with primary keys, which is a bit obscure because of the auto-increment handling.
179
+
180
+ primary_key = opts.delete( :primary_key )
181
+ auto_increment = opts.delete( :auto_increment )
182
+
183
+ if primary_key && ! multi_primary_key
184
+ if auto_increment
185
+ table.add_column( :primary_key, name, opts.merge( :type => type ) )
186
+ next
187
+ end
188
+ opts[ :primary_key ] = primary_key
189
+ end
190
+
191
+ table.add_column( type, name, opts )
192
+ end
193
+
194
+ # Define multi-column primary key if necessary.
195
+ # Note that Sequel currently doesn't preserve the primary key order, so neither can we.
196
+
197
+ if multi_primary_key
198
+ table.add_column( :primary_key, primary_key_columns.map{ |name, opts| name } )
199
+ end
200
+ end
201
+
202
+ # Import all fields of given table.
203
+ def import_table( table )
204
+ # This must come first, so we can exclude foreign key indexes later.
205
+ import_foreign_keys( table )
206
+ import_indexes( table )
207
+ import_columns( table )
208
+ end
209
+
210
+ public
211
+
212
+ # Which tables we automatically ignore on import.
213
+ IGNORED_TABLES = [ :schema_info ]
214
+
215
+ # Import the database schema.
216
+ def schema
217
+ schema = Schema.new
218
+
219
+ for name in db.tables
220
+ next if IGNORED_TABLES.include? name
221
+ table = schema.add_table( name )
222
+ import_table( table )
223
+ end
224
+
225
+ schema
226
+ end
227
+
228
+ end
229
+
230
+ end
231
+
232
+ # EOF #