wineskins 0.2.2

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,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
+