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