wineskins 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ scratch
2
+ *.sqlite3
3
+ *.log
4
+ *.mdw
5
+ *.mdb
6
+ *.ldb
7
+
8
+ *.gem
9
+ test/old
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2012 Eric Gjertsen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to
5
+ deal in the Software without restriction, including without limitation the
6
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
+ sell copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,235 @@
1
+ # Wineskins
2
+
3
+ A Ruby database transfer utility built on [Sequel](http://sequel.rubyforge.org/).
4
+
5
+ Sometimes your old wine needs to be poured into new skins too.
6
+
7
+ ## Basic usage
8
+
9
+ From the command line, the utility runs __transfer instructions__ for a specified
10
+ __source__ and __destination__ database. By default, it looks for these instructions
11
+ in the file `./transfer.rb`. (The syntax of these instructions will be
12
+ described in a moment.) So the easiest way to execute the utility is:
13
+
14
+ wskins some-db://source/url some-other-db://dest/url
15
+
16
+ This will run the instructions in ./transfer.rb to transfer the schema and/or
17
+ data from the specified source database (as a URL recognized by
18
+ `Sequel.connect`), to the specified destination database.
19
+
20
+ You can specify another transfer instructions file via `--config`:
21
+
22
+ wskins --config path/to/transfer.rb some-db://source/url some-other-db://dest/url
23
+
24
+ If your databases can't be opened easily via a URL, instead you can manually set
25
+ the constants `SOURCE_DB` and `DEST_DB` to your Sequel databases within a ruby
26
+ script, and require that script from the command line like this:
27
+
28
+ wskins --require path/to/db/setup.rb
29
+
30
+ (This is necessary for instance if you are using ADO adapters.)
31
+
32
+ Run `wskins --help` to see complete usage options.
33
+
34
+ ## How to install
35
+
36
+ [Get Ruby](http://www.ruby-lang.org/en/downloads/) if you don't have it.
37
+
38
+ Then
39
+
40
+ gem install wineskins
41
+
42
+ This will also install the sequel gem. If Sequel needs native drivers for your
43
+ database(s), install them separately. Consult the
44
+ [Sequel docs](http://sequel.rubyforge.org/) for more info.
45
+
46
+ ## Transfer instructions syntax
47
+
48
+ ### A very simple case:
49
+
50
+ tables :students, :classes, :enrollments
51
+
52
+ This will copy the tables, indexes, and foreign key constraints, and then insert
53
+ all the records, for the three listed tables.
54
+
55
+ ### To rename tables:
56
+
57
+ tables :students, [:classes, :courses], :enrollments
58
+
59
+ The `classes` table in the source will be renamed `courses` in the destination.
60
+ All foreign keys referencing `classes` (in e.g. the `enrollments` table) will be
61
+ changed accordingly.
62
+
63
+ If you are copying records, be careful in the order you list the tables -- this
64
+ is the order that the records will be inserted. If you have foreign key
65
+ constraints between two tables, you must list the target (primary key) table
66
+ first to ensure that the keys exist before inserting the foreign key table
67
+ records.
68
+
69
+ So in this example, the `:students` and `:classes` tables are transferred _before_
70
+ the `:enrollments` table, which presumably has foreign keys to `:students` and
71
+ `:classes`.
72
+
73
+ ### To rename fields:
74
+
75
+ table :classes, :rename => {:class_id => :id}
76
+
77
+ Primary keys, indexes, foreign keys will be changed accordingly in the
78
+ destination database.
79
+
80
+ ### Want to copy the schema, but not import data yet?
81
+
82
+ tables :students, :classes, :enrollments, :schema_only => true
83
+
84
+ ### Have the schema in place, just need to import data?
85
+
86
+ tables :students, :classes, :enrollments, :records_only => true
87
+
88
+ You have finer-grained control as well:
89
+
90
+ table :students, :create_tables => true,
91
+ :create_indexes => false,
92
+ :create_fk_constraints => false,
93
+ :insert_records => true
94
+
95
+ ### Adjusting column definitions
96
+
97
+ Sometimes you need to manually adjust column types or other options in the
98
+ destination: for example due to different conventions between databases. You can
99
+ pass through column definitions to Sequel's schema generator, and they will be
100
+ used _instead of_ the source database table:
101
+
102
+ table :classes do
103
+ column :slots, :integer, :null => false, :default => 25
104
+ end
105
+
106
+ Note that in this example, all of the column definitions _except `slots`_ will
107
+ be copied from the source table, while `slots` will be defined as specified.
108
+
109
+ ### Excluding and including columns
110
+
111
+ (_Note: not yet implemented._)
112
+ You can also exclude specific columns entirely, or include only specified columns:
113
+
114
+ table :enrollments, :exclude => [:final_grade, :status]
115
+ table :students, :include => [:id, :name, :grad_year]
116
+
117
+ Although it's nearly as easy to do this manually in a hook (see below).
118
+
119
+ ### Limiting the imported data
120
+
121
+ (_Note: not yet implemented._)
122
+ It's also possible to specify a filter on the source records that get imported.
123
+
124
+ table :students do
125
+ insert_records :grad_year => (2010..2012)
126
+ end
127
+
128
+ Filters can be anything that Sequel accepts as arguments to `Dataset#filter`.
129
+
130
+ ### Generating a transcript
131
+
132
+ If you just want a script for generating the schema later, and don't actually
133
+ want to make database changes, do something like this:
134
+
135
+ transcript 'path/to/transfer.sql'
136
+ tables :schema_only => true
137
+
138
+ and include the `--dry-run` option on the command line.
139
+
140
+ ### Hooks for manual futzing
141
+
142
+ Wineskins executes a given transfer in four stages:
143
+
144
+ 1. All the tables are created (`:create_table`)
145
+ 2. All the indexes are created via `alter_table` (`:create_indexes`)
146
+ 3. All the foreign key constraints are created via `alter_table` (`:create_fk_constraints`)
147
+ 4. The records are inserted into each table from the source database (`:insert_records`)
148
+
149
+ Each of these stages has a `before_*` and `after_*` hook where you can stick
150
+ whatever custom steps you need using Sequel's incredibly wide toolset, and there
151
+ are also general `before` and `after` hooks that run before and after the entire
152
+ transfer. You can define as many of these as you want at each hook. For
153
+ instance (to turn off foreign key constraints before inserting records):
154
+
155
+ before_insert_records do
156
+ dest.pragma_set 'foreign_keys', 'off'
157
+ end
158
+
159
+ Or to take the example above of excluding columns, you could do this manually
160
+ in a callback like:
161
+
162
+ after_create_tables do
163
+ dest[:enrollments].alter_table do
164
+ drop_column :final_grade
165
+ drop_column :status
166
+ end
167
+ end
168
+
169
+ Within callbacks, the source database is referenced via `source`, the
170
+ destination database via `dest`.
171
+
172
+ ### A note on the syntax
173
+
174
+ In the examples above I've used both a 'hash-options' style and a block syntax.
175
+ Either can be used interchangably and even in combination if you want (although
176
+ it's ugly looking). The options set in the block always override the options
177
+ hash. Also, note that custom `column` definitions must be done within a block.
178
+
179
+ ## Motivations
180
+
181
+ This tool aims to simplify transferring data between databases, and is designed
182
+ around the canonical case where the destination database is completely empty,
183
+ and you want to set up everything the way it is in the source and then import
184
+ the data. Of course, many other scenarios are possible, but the point is that
185
+ the only things you should need to specify are either (1) differences from this
186
+ scenario, or (2) differences between database adapters that Sequel cannot
187
+ handle automatically.
188
+
189
+ Note that accordingly, if schema or records already exist in your destination,
190
+ you are responsible for dealing with this in whatever way makes sense for your
191
+ scenario. No tables, indexes, constraints, or records are automatically deleted
192
+ in the destination.
193
+
194
+ So, you might want to wipe out and replace what exists (via `drop_table`,
195
+ `dest[:table].delete`, etc. in callbacks); or you might want to keep what
196
+ exists (omitting changes via `:schema_only`, `:records_only` options, etc.); or
197
+ you might want to alter what exists (via custom `alter_table`,
198
+ `dest[:table].filter(some_filter).delete`, etc. in callbacks).
199
+
200
+ The principle is that _as much as possible, the source database should determine
201
+ the schema of the destination database_, thus minimizing manually-entered (and
202
+ possibly incorrect) schema definition code. Also it helps avoid, for simple but
203
+ typical cases, the great pain and knashing of the teeth involved in massaging
204
+ the source data into the right format for for importing.
205
+
206
+ ## Alternatives / Similar projects
207
+
208
+ - Sequel's [schema dumper extension](http://sequel.rubyforge.org/rdoc-plugins/files/lib/sequel/extensions/schema_dumper_rb.html) lets you dump and load schema using Sequel's migration DSL.
209
+ - [DbCopier](https://github.com/santosh79/db-copier), apparently unmaintained?
210
+ - [Linkage](https://github.com/coupler/linkage) mimics joins between tables in
211
+ different databases.
212
+
213
+ ## Please help
214
+
215
+ This is a young young project, don't expect it will work out of the box without
216
+ some futzing. It's only been formally tested on Sqlite to Sqlite transfers, and
217
+ ad-hoc tested on a 'real' MS Access to Sqlite transfer.
218
+
219
+ If you start using it and run into weird shit, at the very least let me know
220
+ about it. Better still if you send some informed guesses as to what's going on.
221
+ Pull requests are awesome and going the extra mile and all that... but before
222
+ you go to the trouble, unless it's a really minor fix, let me know about the
223
+ issue, I might be able to save you some time and we can have a conversation
224
+ about it you know?
225
+
226
+ There's a TODO list in the project root if you want to see where I'm thinking
227
+ of heading, comments welcome.
228
+
229
+
230
+ ## Requirements
231
+
232
+ - ruby >= 1.8.7
233
+ - sequel ~> 3.0 (note >= 3.39 needed for MS Access source databases)
234
+ - progressbar (optional)
235
+
@@ -0,0 +1,7 @@
1
+ require 'rake'
2
+
3
+ task(:test) do
4
+ require './test/suite.rb'
5
+ end
6
+
7
+ task :default => :test
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path('../lib/wineskins_cli', File.dirname(__FILE__))
4
+
5
+ Wineskins::Runner.new(ARGV).run
@@ -0,0 +1,269 @@
1
+ require 'sequel'
2
+
3
+ # Modification of ADO/Access adapter to get schema info
4
+ # incorporated into Sequel itself v3.39.0
5
+ # require File.expand_path('sequel_ext/adapters/shared/access', File.dirname(__FILE__))
6
+
7
+ require File.expand_path('wineskins/version', File.dirname(__FILE__))
8
+ require File.expand_path('wineskins/utils', File.dirname(__FILE__))
9
+ require File.expand_path('wineskins/transcript', File.dirname(__FILE__))
10
+ require File.expand_path('wineskins/schema_methods', File.dirname(__FILE__))
11
+ require File.expand_path('wineskins/record_methods', File.dirname(__FILE__))
12
+
13
+ module Wineskins
14
+
15
+ def self.transfer(source, dest, opts={}, &block)
16
+ Transfer.new(source, dest, &block).run(opts)
17
+ end
18
+
19
+ class Transfer
20
+ include SchemaMethods
21
+ include RecordMethods
22
+
23
+ attr_accessor :source, :dest
24
+ attr_reader :table_defs, :progressbar
25
+ attr_reader :before_hooks, :after_hooks
26
+
27
+ def initialize(source, dest, &block)
28
+ self.source = source
29
+ self.dest = dest
30
+ @table_defs = []
31
+ @before_hooks = Hash.new{|h,k|h[k]=[]}
32
+ @after_hooks = Hash.new{|h,k|h[k]=[]}
33
+ self.define(&block) if block_given?
34
+ end
35
+
36
+ def define(&block)
37
+ instance_eval(&block)
38
+ self
39
+ end
40
+
41
+ def run(opts={})
42
+ rollback = (opts[:dryrun] ? :always : nil)
43
+ dest.transaction(:rollback => rollback) do
44
+ trigger_before_hooks
45
+ trigger_before_hooks :create_tables
46
+ create_tables!
47
+ trigger_after_hooks :create_tables
48
+ trigger_before_hooks :create_indexes
49
+ create_indexes!
50
+ trigger_after_hooks :create_indexes
51
+ trigger_before_hooks :create_fk_constraints
52
+ create_fk_constraints!
53
+ trigger_after_hooks :create_fk_constraints
54
+ trigger_before_hooks :insert_records
55
+ insert_records!
56
+ trigger_after_hooks :insert_records
57
+ trigger_after_hooks
58
+ end
59
+ end
60
+
61
+ def transcript(file=nil)
62
+ self.dest.loggers << Transcript.new(file)
63
+ end
64
+
65
+ def tables(*args)
66
+ opts = (Hash === args.last ? args.pop : {})
67
+ tbls = (args.empty? ? self.source.tables : args)
68
+ tbls.each do |tbl| table(tbl, opts) end
69
+ end
70
+
71
+ def table(name, opts={}, &block)
72
+ @table_defs << Table.new(name, opts, &block)
73
+ end
74
+
75
+ def before(event=nil, &cb)
76
+ before_hooks[event] << cb
77
+ end
78
+
79
+ def after(event=nil, &cb)
80
+ after_hooks[event] << cb
81
+ end
82
+
83
+ [:before,
84
+ :after
85
+ ].product([
86
+ :create_tables,
87
+ :create_indexes,
88
+ :create_fk_constraints,
89
+ :insert_records
90
+ ]).each do |(hook, event)|
91
+ define_method("#{hook}_#{event}") do |&block|
92
+ send hook, event, &block
93
+ end
94
+ end
95
+
96
+ def set_progressbar(title, total)
97
+ require 'progressbar'
98
+ @progressbar = ProgressBar.new(title, total)
99
+ rescue LoadError
100
+ @progressbar = nil
101
+ end
102
+
103
+ private
104
+
105
+ def create_tables!
106
+ @table_defs.select {|t| t.create_table?}.each do |table|
107
+ transfer_table table
108
+ end
109
+ end
110
+
111
+ def create_indexes!
112
+ @table_defs.select {|t| t.create_indexes?}.each do |table|
113
+ transfer_indexes table
114
+ end
115
+ end
116
+
117
+ def create_fk_constraints!
118
+ @table_defs.select {|t| t.create_fk_constraints?}.each do |table|
119
+ transfer_fk_constraints table, table_rename
120
+ end
121
+ end
122
+
123
+ def insert_records!
124
+ @table_defs.select {|t| t.insert_records?}.each do |table|
125
+ transfer_records table
126
+ end
127
+ end
128
+
129
+ # used in create_fk_constraints
130
+ def table_rename
131
+ @table_defs.inject({}) do |memo, table|
132
+ memo[table.source_name] = table.dest_name
133
+ memo
134
+ end
135
+ end
136
+
137
+ def trigger_before_hooks(event=nil)
138
+ before_hooks[event].each do |cb|
139
+ cb.call
140
+ end
141
+ end
142
+
143
+ def trigger_after_hooks(event=nil)
144
+ after_hooks[event].each do |cb|
145
+ cb.call
146
+ end
147
+ end
148
+
149
+ end
150
+
151
+ # data structure for table transfer definition
152
+ class Table
153
+
154
+ attr_accessor :source_name, :dest_name, :dest_columns
155
+ attr_accessor :include,
156
+ :exclude,
157
+ :rename,
158
+ :create_table,
159
+ :create_indexes,
160
+ :create_fk_constraints,
161
+ :insert_records
162
+
163
+ def initialize(name, opts={}, &block)
164
+ self.source_name, self.dest_name = Array(name)
165
+ self.dest_name ||= self.source_name
166
+ self.dest_columns = {}
167
+ Builder.new(self, default_opts.merge(opts), &block)
168
+ end
169
+
170
+ def create_table?
171
+ !!create_table
172
+ end
173
+
174
+ def create_indexes?
175
+ !!create_indexes
176
+ end
177
+
178
+ def create_fk_constraints?
179
+ !!create_fk_constraints
180
+ end
181
+
182
+ def insert_records?
183
+ !!insert_records
184
+ end
185
+
186
+ # todo: handle Proc or Regex === rename
187
+ def rename_map(cols)
188
+ col_map = cols.inject({}) {|m,c| m[c]=c;m}
189
+ col_map.merge(rename)
190
+ end
191
+
192
+ def default_opts
193
+ { include: nil,
194
+ exclude: [],
195
+ rename: {},
196
+ create_table: true,
197
+ create_indexes: true,
198
+ create_fk_constraints: true,
199
+ insert_records: true
200
+ }
201
+ end
202
+
203
+ class Builder
204
+
205
+ def initialize(target, opts={}, &block)
206
+ @target = target
207
+ set_options opts
208
+ instance_eval(&block) if block_given?
209
+ end
210
+
211
+ def include(flds)
212
+ @target.include = flds
213
+ end
214
+
215
+ def exclude(flds)
216
+ @target.exclude = flds
217
+ end
218
+
219
+ def rename(fldmap=nil, &block)
220
+ @target.rename = fldmap || block
221
+ end
222
+
223
+ def column(name, *args)
224
+ @target.dest_columns[name] = args
225
+ end
226
+
227
+ def create_table(bool=true)
228
+ @target.create_table = bool
229
+ end
230
+
231
+ def create_indexes(bool=true)
232
+ @target.create_indexes = bool
233
+ end
234
+
235
+ def create_fk_constraints(bool=true)
236
+ @target.create_fk_constraints = bool
237
+ end
238
+
239
+ def insert_records(bool=true)
240
+ @target.insert_records = bool
241
+ end
242
+
243
+ def schema_only
244
+ create_table; create_indexes; create_fk_constraints
245
+ insert_records false
246
+ end
247
+
248
+ def records_only
249
+ create_table(false); create_indexes(false); create_fk_constraints(false)
250
+ insert_records
251
+ end
252
+
253
+ def set_options(opts)
254
+ [:include, :exclude, :rename,
255
+ :create_table, :create_indexes, :create_fk_constraints, :insert_records,
256
+ ].each do |opt|
257
+ self.send(opt, opts[opt]) if opts[opt]
258
+ end
259
+ [:schema_only, :records_only].each do |opt|
260
+ self.send(opt) if opts[opt]
261
+ end
262
+ end
263
+
264
+ end
265
+
266
+ end
267
+
268
+ end
269
+