nbadw-util 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,3 @@
1
+ == nbadw
2
+
3
+ Put appropriate LICENSE for your project here.
data/README ADDED
@@ -0,0 +1,3 @@
1
+ == nbadw
2
+
3
+ You should document your project here.
@@ -0,0 +1,48 @@
1
+ # encoding: utf-8
2
+ require 'rubygems'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |s|
7
+ s.name = "nbadw-util"
8
+ s.summary = %Q{NB Aquatic Data Warehouse - Models and Database Utilities}
9
+ s.email = "casey.colin@gmail.com"
10
+ s.homepage = "http://github.com/colincasey/nbadw"
11
+ s.description = "Database models, migrations, and utilities for the New Brunswick Aquatic Data Warehouse"
12
+ s.authors = ["Colin Casey"]
13
+ s.add_dependency 'sequel', '>= 3.5.0'
14
+ s.rubygems_version = '1.3.1'
15
+ s.files = FileList['lib/**/*.rb'] + ['README.rdoc', 'LICENSE', 'VERSION.yml', 'Rakefile']
16
+ s.files.exclude('main.rb')
17
+ end
18
+ rescue LoadError => e
19
+ if e.message =~ /jeweler/
20
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
21
+ else
22
+ puts e.message + ' -- while loading jeweler.'
23
+ end
24
+ end
25
+
26
+ require 'rake/rdoctask'
27
+ Rake::RDocTask.new do |rdoc|
28
+ rdoc.rdoc_dir = 'rdoc'
29
+ rdoc.title = 'NB Aquatic Data Warehouse'
30
+ rdoc.options << '--line-numbers' << '--inline-source'
31
+ rdoc.rdoc_files.include('README*')
32
+ rdoc.rdoc_files.include('lib/**/*.rb')
33
+ end
34
+
35
+ begin
36
+ require 'rcov/rcovtask'
37
+ Rcov::RcovTask.new do |t|
38
+ t.libs << 'spec'
39
+ t.test_files = FileList['spec/*_spec.rb']
40
+ t.verbose = true
41
+ end
42
+ rescue LoadError
43
+ if RUBY_PLATFORM =~ /java/
44
+ puts "RCov is not available. In order to run rcov, you must: sudo gem install jruby-rcov"
45
+ else
46
+ puts "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
47
+ end
48
+ end
@@ -0,0 +1,311 @@
1
+ require 'sequel'
2
+ require 'sequel/extensions/schema_dumper'
3
+ require 'sequel/extensions/migration'
4
+ require 'nbadw/util/progress_bar'
5
+ require 'sequel/jdbc_access_adapter'
6
+
7
+ module NBADW
8
+ module Util
9
+ class CopyDatabaseTask
10
+ attr_reader :source, :destination, :page_size, :except
11
+
12
+ def initialize(src, dest, options = {})
13
+ @source = Sequel.connect(src, :single_threaded => true)
14
+ @destination = Sequel.connect(dest, :single_threaded => true)
15
+ @page_size = options[:page_size] || :unlimited
16
+ @verify_data = !!options[:verify_data]
17
+ @except = options[:except] || []
18
+ end
19
+
20
+ def self.start(src, dest, options = {})
21
+ print "Initializing copy operation"
22
+ task = new(src, dest, options)
23
+ begin
24
+ task.copy
25
+ rescue Exception => e
26
+ puts "...fail!!!"
27
+ puts "Reason: #{e.message}"
28
+ puts e.backtrace.join("\n")
29
+ end
30
+ end
31
+
32
+ def copy
33
+ puts "..."
34
+ puts "#{source.tables.length} tables, #{format_number(total_records(source))} records"
35
+ copy_schema
36
+ copy_data
37
+ copy_indexes
38
+ verify_data if verify_data?
39
+ puts "...copy completed"
40
+ end
41
+
42
+ def copy_schema
43
+ begin
44
+ run_callback :before_copy_schema
45
+
46
+ tables = source.tables
47
+ progress = ProgressBar.new("Schema copy", tables.length)
48
+
49
+ tables.each do |t|
50
+ next if except.include?(t.to_s)
51
+ args = { :table => t, :schema => source.dump_table_schema(t.to_sym, :indexes => false) }
52
+ run_callback :before_create_table, args
53
+ migration = "Class.new(Sequel::Migration) do \n def up \n #{args[:schema]} \n end \n end"
54
+ eval(migration).apply(destination, :up)
55
+ run_callback :after_create_table, args
56
+ progress.inc(1)
57
+ end
58
+
59
+ run_callback :after_copy_schema
60
+ ensure
61
+ progress.finish if progress
62
+ end
63
+ end
64
+
65
+ def copy_data
66
+ run_callback :before_copy_data
67
+
68
+ progress = ProgressBar.new("Data copy", source.tables.size)
69
+ begin
70
+ source.tables.each do |table_name|
71
+ next if except.include?(table_name.to_s)
72
+ src_table = source[table_name.to_sym]
73
+ dst_table = destination[table_name.to_sym]
74
+ args = { :table => table_name }
75
+ page_size == :unlimited ? copy_table_without_limit(src_table, dst_table, args) : copy_table_with_limit(src_table, dst_table, args)
76
+ progress.inc(1)
77
+ end
78
+ ensure
79
+ progress.finish
80
+ end
81
+
82
+ run_callback :after_copy_data
83
+ end
84
+
85
+ def copy_table_without_limit(src_table, dst_table, args = {})
86
+ src_table.each do |row|
87
+ args.merge!({ :row => row })
88
+ run_callback :before_copy_row, args
89
+ dst_table.insert(row)
90
+ run_callback :after_copy_row, args
91
+ end
92
+ end
93
+
94
+ def copy_table_with_limit(src_table, dst_table, args = {})
95
+ count = src_table.count
96
+ offset = 0
97
+ while(offset < count) do
98
+ rows = src_table.limit(page_size, offset).all
99
+ rows.each_with_index do |row, i|
100
+ args.merge!({ :row => row, :index => i, :offset => offset })
101
+ run_callback :before_copy_row, args
102
+ dst_table.insert(row)
103
+ run_callback :after_copy_row, args
104
+ end
105
+ offset += rows.size
106
+ end
107
+ end
108
+
109
+ def copy_indexes
110
+ begin
111
+ run_callback :before_copy_indexes
112
+
113
+ tables = source.tables
114
+ progress = ProgressBar.new("Index copy", tables.length)
115
+
116
+ tables.each do |t|
117
+ next if except.include?(t.to_s)
118
+ args = { :table => t, :indexes => source.send(:dump_table_indexes, t.to_sym, :add_index) }
119
+ run_callback :before_add_indexes, args
120
+ migration = "Class.new(Sequel::Migration) do \n def up \n #{args[:indexes]} \n end \n end"
121
+ eval(migration).apply(destination, :up)
122
+ run_callback :after_add_indexes, args
123
+ progress.inc(1)
124
+ end
125
+
126
+ run_callback :after_copy_indexes
127
+ ensure
128
+ progress.finish if progress
129
+ end
130
+ end
131
+
132
+ def verify_data
133
+ tables = source.tables
134
+ progress = ProgressBar.new("Verify data", tables.length)
135
+ begin
136
+ tables.each do |table_name|
137
+ next if except.include?(table_name.to_s)
138
+ src_table = source[table_name.to_sym]
139
+ dst_table = destination[table_name.to_sym]
140
+ page_size == :unlimited ? verify_table_without_limit(table_name, src_table, dst_table) : verify_table_with_limit(table_name, src_table, dst_table)
141
+ progress.inc(1)
142
+ end
143
+ ensure
144
+ progress.finish if progress
145
+ end
146
+ end
147
+
148
+ def verify_table_without_limit(table_name, src_table, dst_table)
149
+ src_table.each do |row|
150
+ row_found = dst_table.filter(row).first
151
+ raise "no matching row found in #{table_name} for #{row.inspect}" unless row_found
152
+ verify_row(table_name, row, row_found)
153
+ end
154
+ end
155
+
156
+ def verify_table_with_limit(table_name, src_table, dst_table)
157
+ count = src_table.count
158
+ offset = 0
159
+ while(offset < count) do
160
+ rows = src_table.limit(page_size, offset).all
161
+ rows.each do |row|
162
+ row_found = dst_table.filter(row).first
163
+ raise "no matching row found in #{table_name} for #{row.inspect}" unless row_found
164
+ verify_row(table_name, row, row_found)
165
+ end
166
+ offset += rows.length
167
+ end
168
+ end
169
+
170
+ def verify_row(table_name, row1, row2)
171
+ diff = {}
172
+ row1.each do |col, val|
173
+ eql = case val
174
+ when Time then (val - row1[col]).abs < 1 # time fields are sometimes off by very miniscule fractions
175
+ else val == row1[col]
176
+ end
177
+ diff[col] = "#{val}, #{row2[col]}" unless eql
178
+ end
179
+ raise "row does not match exactly - expected #{row1.inspect}, but was #{row2.inspect} - in table #{table_name}, diff #{diff.inspect}" unless diff.empty?
180
+ end
181
+
182
+ def verify_data?
183
+ @verify_data
184
+ end
185
+
186
+ def total_records(db)
187
+ db.tables.inject(0) { |total, table_name| total += db[table_name.to_sym].count }
188
+ end
189
+
190
+ def format_number(num)
191
+ num.to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1,")
192
+ end
193
+
194
+ # the following is a callback system that helps to handle slight
195
+ # differences when copying between database types
196
+ class << self
197
+ def callbacks
198
+ @callbacks ||= []
199
+ end
200
+
201
+ def before(callback, opts = {}, &block)
202
+ add_callback(:before, callback, opts, &block)
203
+ end
204
+
205
+ def after(callback, opts = {}, &block)
206
+ add_callback(:after, callback, opts, &block)
207
+ end
208
+
209
+ def add_callback(type, callback, opts, &block)
210
+ callback_config = {
211
+ :type => type,
212
+ :callback => callback,
213
+ :adapter => opts[:adapter] || :all,
214
+ :for => opts[:for],
215
+ :logic => block
216
+ }
217
+ callbacks << callback_config
218
+ end
219
+ end
220
+
221
+ # prevent MySQL from changing '0' values on insert since we'd like an exact copy
222
+ before :copy_schema, :adapter => :mysql, :for => :destination do |src, dst, args|
223
+ dst.run("SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO';")
224
+ end
225
+
226
+ # fix to catch schema dumps for PostgreSQL which set an invalid boolean default
227
+ before :create_table, :adapter => :postgres, :for => :destination do |src, dst, args|
228
+ schema = args[:schema]
229
+ schema = schema.split("\n").collect do |line|
230
+ if line.match(/TrueClass/)
231
+ line = line.sub(/:default=>(\d)/) { |match| ":default=>#{$1 == '0' ? 'true' : 'false'}" }
232
+ end
233
+ line
234
+ end.join("\n")
235
+ args[:schema] = schema
236
+ end
237
+
238
+ # this fixes the string as primary keys
239
+ before :create_table, :adapter => :access, :for => :source do |src, dst, args|
240
+ table = args[:table].to_s
241
+ pks = src.schema(args[:table]).collect do |col_schema|
242
+ col, opts = col_schema
243
+ opts[:primary_key] ? col_schema : nil
244
+ end.compact
245
+
246
+ if pks.size == 1 && pks[0][1][:type] == :string
247
+ col, opts = pks[0]
248
+ schema = args[:schema]
249
+ schema = schema.split("\n").collect do |line|
250
+ line = " String :#{col}, :size=>#{opts[:column_size]}, :null=>false" if line.match(/primary_key/)
251
+ line = " primary_key [:#{col}]\nend" if line.match(/^end/)
252
+ line
253
+ end.join("\n")
254
+ args[:schema] = schema
255
+ end
256
+ end
257
+
258
+ # When copying from access, convert all BigDecimal columns to Float or lose precision!
259
+ before :create_table, :adapter => :access, :for => :source do |src, dst, args|
260
+ args[:schema] = args[:schema].gsub(/BigDecimal/, 'Float')
261
+ end
262
+
263
+ STRING_TO_INT_FIXES = [
264
+ { :table => "auxuserdbselectedsites", :column => "aquaticsiteuseid" },
265
+ { :table => "auxuserdbselectedsiteuse", :column => "aquaticsiteuseid" },
266
+ { :table => "cdtranslation - dfo stock mating", :column => "mating code" },
267
+ { :table => "del-missing age class in tblfishmeasurement", :column => "fishsampleid" },
268
+ { :table => "del-missing age class in tblfishmeasurement-robin", :column => "fishsampleid" },
269
+ { :table => "selections", :column => "selectionid" },
270
+ { :table => "tblelectrofishingmethoddetail", :column => "aquaticactivitydetailid" },
271
+ { :table => "tbloldhabitatsurvey", :column => "habitatsurveyid" }
272
+ ]
273
+ # not sure what's up here...
274
+ before :create_table, :adapter => :postgres, :for => :destination do |src, dst, args|
275
+ table = args[:table].to_s.downcase
276
+ if fix = STRING_TO_INT_FIXES.detect { |fix| fix[:table] == table }
277
+ schema = args[:schema]
278
+ schema = schema.split("\n").collect do |line|
279
+ line = " Integer :\"#{fix[:column]}\"" if line.match(/#{fix[:column]}/)
280
+ line
281
+ end.join("\n")
282
+ args[:schema] = schema
283
+ end
284
+ end
285
+
286
+ # determines which callbacks to run (is this needlessly complex?)
287
+ def run_callback(full_callback, args = {})
288
+ full_callback.to_s.match(/(before|after)_(.*)/)
289
+ type, callback = $1.to_sym, $2.to_sym
290
+ CopyDatabaseTask.callbacks.each do |callback_config|
291
+ if callback_config[:type] == type && callback_config[:callback] == callback # callback matches
292
+ # which adapters should we check against?
293
+ adapters = [:all] # always check for all...
294
+ if callback_config[:for] == :destination # only destination?
295
+ adapters << destination.database_type.to_sym
296
+ elsif callback_config[:for] == :source # only source?
297
+ adapters << source.database_type.to_sym
298
+ else # or both?
299
+ adapters << destination.database_type.to_sym
300
+ adapters << source.database_type.to_sym
301
+ end
302
+ # if the adapter matches, run the callback
303
+ if adapters.include?(callback_config[:adapter])
304
+ callback_config[:logic].call(source, destination, args)
305
+ end
306
+ end
307
+ end
308
+ end
309
+ end # CopyDatabaseTask
310
+ end # Util
311
+ end # NBADW
@@ -0,0 +1,236 @@
1
+ #
2
+ # Ruby/ProgressBar - a text progress bar library
3
+ #
4
+ # Copyright (C) 2001-2005 Satoru Takabayashi <satoru@namazu.org>
5
+ # All rights reserved.
6
+ # This is free software with ABSOLUTELY NO WARRANTY.
7
+ #
8
+ # You can redistribute it and/or modify it under the terms
9
+ # of Ruby's license.
10
+ #
11
+
12
+ class ProgressBar
13
+ VERSION = "0.9"
14
+
15
+ def initialize (title, total, out = STDERR)
16
+ @title = title
17
+ @total = total
18
+ @out = out
19
+ @terminal_width = 80
20
+ @bar_mark = "="
21
+ @current = 0
22
+ @previous = 0
23
+ @finished_p = false
24
+ @start_time = Time.now
25
+ @previous_time = @start_time
26
+ @title_width = 14
27
+ @format = "%-#{@title_width}s %3d%% %s %s"
28
+ @format_arguments = [:title, :percentage, :bar, :stat]
29
+ clear
30
+ show
31
+ end
32
+ attr_reader :title
33
+ attr_reader :current
34
+ attr_reader :total
35
+ attr_accessor :start_time
36
+
37
+ private
38
+ def fmt_bar
39
+ bar_width = do_percentage * @terminal_width / 100
40
+ sprintf("|%s%s|",
41
+ @bar_mark * bar_width,
42
+ " " * (@terminal_width - bar_width))
43
+ end
44
+
45
+ def fmt_percentage
46
+ do_percentage
47
+ end
48
+
49
+ def fmt_stat
50
+ if @finished_p then elapsed else eta end
51
+ end
52
+
53
+ def fmt_stat_for_file_transfer
54
+ if @finished_p then
55
+ sprintf("%s %s %s", bytes, transfer_rate, elapsed)
56
+ else
57
+ sprintf("%s %s %s", bytes, transfer_rate, eta)
58
+ end
59
+ end
60
+
61
+ def fmt_title
62
+ @title[0,(@title_width - 1)] + ":"
63
+ end
64
+
65
+ def convert_bytes (bytes)
66
+ if bytes < 1024
67
+ sprintf("%6dB", bytes)
68
+ elsif bytes < 1024 * 1000 # 1000kb
69
+ sprintf("%5.1fKB", bytes.to_f / 1024)
70
+ elsif bytes < 1024 * 1024 * 1000 # 1000mb
71
+ sprintf("%5.1fMB", bytes.to_f / 1024 / 1024)
72
+ else
73
+ sprintf("%5.1fGB", bytes.to_f / 1024 / 1024 / 1024)
74
+ end
75
+ end
76
+
77
+ def transfer_rate
78
+ bytes_per_second = @current.to_f / (Time.now - @start_time)
79
+ sprintf("%s/s", convert_bytes(bytes_per_second))
80
+ end
81
+
82
+ def bytes
83
+ convert_bytes(@current)
84
+ end
85
+
86
+ def format_time (t)
87
+ t = t.to_i
88
+ sec = t % 60
89
+ min = (t / 60) % 60
90
+ hour = t / 3600
91
+ sprintf("%02d:%02d:%02d", hour, min, sec);
92
+ end
93
+
94
+ # ETA stands for Estimated Time of Arrival.
95
+ def eta
96
+ if @current == 0
97
+ "ETA: --:--:--"
98
+ else
99
+ elapsed = Time.now - @start_time
100
+ eta = elapsed * @total / @current - elapsed;
101
+ sprintf("ETA: %s", format_time(eta))
102
+ end
103
+ end
104
+
105
+ def elapsed
106
+ elapsed = Time.now - @start_time
107
+ sprintf("Time: %s", format_time(elapsed))
108
+ end
109
+
110
+ def eol
111
+ if @finished_p then "\n" else "\r" end
112
+ end
113
+
114
+ def do_percentage
115
+ if @total.zero?
116
+ 100
117
+ else
118
+ @current * 100 / @total
119
+ end
120
+ end
121
+
122
+ def get_width
123
+ # FIXME: I don't know how portable it is.
124
+ default_width = 80
125
+ begin
126
+ tiocgwinsz = 0x5413
127
+ data = [0, 0, 0, 0].pack("SSSS")
128
+ if @out.ioctl(tiocgwinsz, data) >= 0 then
129
+ rows, cols, xpixels, ypixels = data.unpack("SSSS")
130
+ if cols > 0 then cols else default_width end
131
+ else
132
+ default_width
133
+ end
134
+ rescue Exception
135
+ default_width
136
+ end
137
+ end
138
+
139
+ def show
140
+ arguments = @format_arguments.map {|method|
141
+ method = sprintf("fmt_%s", method)
142
+ send(method)
143
+ }
144
+ line = sprintf(@format, *arguments)
145
+
146
+ width = get_width
147
+ if line.length == width - 1
148
+ @out.print(line + eol)
149
+ @out.flush
150
+ elsif line.length >= width
151
+ @terminal_width = [@terminal_width - (line.length - width + 1), 0].max
152
+ if @terminal_width == 0 then @out.print(line + eol) else show end
153
+ else # line.length < width - 1
154
+ @terminal_width += width - line.length + 1
155
+ show
156
+ end
157
+ @previous_time = Time.now
158
+ end
159
+
160
+ def show_if_needed
161
+ if @total.zero?
162
+ cur_percentage = 100
163
+ prev_percentage = 0
164
+ else
165
+ cur_percentage = (@current * 100 / @total).to_i
166
+ prev_percentage = (@previous * 100 / @total).to_i
167
+ end
168
+
169
+ # Use "!=" instead of ">" to support negative changes
170
+ if cur_percentage != prev_percentage ||
171
+ Time.now - @previous_time >= 1 || @finished_p
172
+ show
173
+ end
174
+ end
175
+
176
+ public
177
+ def clear
178
+ @out.print "\r"
179
+ @out.print(" " * (get_width - 1))
180
+ @out.print "\r"
181
+ end
182
+
183
+ def finish
184
+ @current = @total
185
+ @finished_p = true
186
+ show
187
+ end
188
+
189
+ def finished?
190
+ @finished_p
191
+ end
192
+
193
+ def file_transfer_mode
194
+ @format_arguments = [:title, :percentage, :bar, :stat_for_file_transfer]
195
+ end
196
+
197
+ def format= (format)
198
+ @format = format
199
+ end
200
+
201
+ def format_arguments= (arguments)
202
+ @format_arguments = arguments
203
+ end
204
+
205
+ def halt
206
+ @finished_p = true
207
+ show
208
+ end
209
+
210
+ def inc (step = 1)
211
+ @current += step
212
+ @current = @total if @current > @total
213
+ show_if_needed
214
+ @previous = @current
215
+ end
216
+
217
+ def set (count)
218
+ if count < 0 || count > @total
219
+ raise "invalid count: #{count} (total: #{@total})"
220
+ end
221
+ @current = count
222
+ show_if_needed
223
+ @previous = @current
224
+ end
225
+
226
+ def inspect
227
+ "#<ProgressBar:#{@current}/#{@total}>"
228
+ end
229
+ end
230
+
231
+ class ReversedProgressBar < ProgressBar
232
+ def do_percentage
233
+ 100 - super
234
+ end
235
+ end
236
+
@@ -0,0 +1,44 @@
1
+ require 'sequel/adapters/shared/access'
2
+
3
+ module Sequel
4
+ module JDBC
5
+ class Database
6
+ # Alias the generic JDBC version so it can be called directly later
7
+ alias jdbc_schema_parse_table schema_parse_table
8
+ end
9
+
10
+ # Database and Dataset instance methods for MSSQL specific
11
+ # support via JDBC.
12
+ module Access
13
+ # Database instance methods for MSSQL databases accessed via JDBC.
14
+ module DatabaseMethods
15
+ PRIMARY_KEY_INDEX_RE = /\Apk__/i.freeze
16
+
17
+ include Sequel::Access::DatabaseMethods
18
+
19
+ # Return instance of Sequel::JDBC::MSSQL::Dataset with the given opts.
20
+ def dataset(opts=nil)
21
+ Sequel::JDBC::Access::Dataset.new(self, opts)
22
+ end
23
+
24
+ private
25
+
26
+ # Call the generic JDBC version instead of MSSQL version,
27
+ # since the JDBC version handles primary keys.
28
+ def schema_parse_table(table, opts={})
29
+ jdbc_schema_parse_table(table, opts)
30
+ end
31
+
32
+ # Primary key indexes appear to start with pk__ on MSSQL
33
+ def primary_key_index_re
34
+ PRIMARY_KEY_INDEX_RE
35
+ end
36
+ end
37
+
38
+ # Dataset class for MSSQL datasets accessed via JDBC.
39
+ class Dataset < JDBC::Dataset
40
+ include Sequel::Access::DatasetMethods
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,416 @@
1
+ module Sequel
2
+ module Access
3
+ module DatabaseMethods
4
+ AUTO_INCREMENT = 'COUNTER(1,1)'.freeze
5
+ SERVER_VERSION_RE = /^(\d+)\.(\d+)\.(\d+)/.freeze
6
+ SQL_BEGIN = "BEGIN TRANSACTION".freeze
7
+ SQL_COMMIT = "COMMIT TRANSACTION".freeze
8
+ SQL_ROLLBACK = "ROLLBACK TRANSACTION".freeze
9
+ SQL_ROLLBACK_TO_SAVEPOINT = 'ROLLBACK TRANSACTION autopoint_%d'.freeze
10
+ SQL_SAVEPOINT = 'SAVE TRANSACTION autopoint_%d'.freeze
11
+ TEMPORARY = "#".freeze
12
+
13
+ def database_type
14
+ :access
15
+ end
16
+
17
+ def supports_savepoints?
18
+ false
19
+ end
20
+
21
+ private
22
+
23
+ # MSSQL uses the IDENTITY(1,1) column for autoincrementing columns.
24
+ def auto_increment_sql
25
+ AUTO_INCREMENT
26
+ end
27
+
28
+ # MSSQL specific syntax for altering tables.
29
+ def alter_table_sql(table, op)
30
+ case op[:op]
31
+ when :add_column
32
+ "ALTER TABLE #{quote_schema_table(table)} ADD #{column_definition_sql(op)}"
33
+ when :rename_column
34
+ "SP_RENAME #{literal("#{quote_schema_table(table)}.#{quote_identifier(op[:name])}")}, #{literal(op[:new_name].to_s)}, 'COLUMN'"
35
+ when :set_column_type
36
+ "ALTER TABLE #{quote_schema_table(table)} ALTER COLUMN #{quote_identifier(op[:name])} #{type_literal(op)}"
37
+ when :set_column_null
38
+ sch = schema(table).find{|k,v| k.to_s == op[:name].to_s}.last
39
+ type = {:type=>sch[:db_type]}
40
+ type[:size] = sch[:max_chars] if sch[:max_chars]
41
+ "ALTER TABLE #{quote_schema_table(table)} ALTER COLUMN #{quote_identifier(op[:name])} #{type_literal(type)} #{'NOT ' unless op[:null]}NULL"
42
+ when :set_column_default
43
+ "ALTER TABLE #{quote_schema_table(table)} ADD CONSTRAINT #{quote_identifier("sequel_#{table}_#{op[:name]}_def")} DEFAULT #{literal(op[:default])} FOR #{quote_identifier(op[:name])}"
44
+ else
45
+ super(table, op)
46
+ end
47
+ end
48
+
49
+ # SQL to start a new savepoint
50
+ def begin_savepoint_sql(depth)
51
+ SQL_SAVEPOINT % depth
52
+ end
53
+
54
+ # SQL to BEGIN a transaction.
55
+ def begin_transaction_sql
56
+ SQL_BEGIN
57
+ end
58
+
59
+ # Commit the active transaction on the connection, does not commit/release
60
+ # savepoints.
61
+ def commit_transaction(conn)
62
+ log_connection_execute(conn, commit_transaction_sql) unless Thread.current[:sequel_transaction_depth] > 1
63
+ end
64
+
65
+ # SQL to COMMIT a transaction.
66
+ def commit_transaction_sql
67
+ SQL_COMMIT
68
+ end
69
+
70
+ # The SQL to drop an index for the table.
71
+ def drop_index_sql(table, op)
72
+ "DROP INDEX #{quote_identifier(op[:name] || default_index_name(table, op[:columns]))} ON #{quote_schema_table(table)}"
73
+ end
74
+
75
+ # Always quote identifiers in the metadata_dataset, so schema parsing works.
76
+ def metadata_dataset
77
+ ds = super
78
+ ds.quote_identifiers = true
79
+ ds
80
+ end
81
+
82
+ # SQL to rollback to a savepoint
83
+ def rollback_savepoint_sql(depth)
84
+ SQL_ROLLBACK_TO_SAVEPOINT % depth
85
+ end
86
+
87
+ # SQL to ROLLBACK a transaction.
88
+ def rollback_transaction_sql
89
+ SQL_ROLLBACK
90
+ end
91
+
92
+ # MSSQL uses the INFORMATION_SCHEMA to hold column information. This method does
93
+ # not support the parsing of primary key information.
94
+ def schema_parse_table(table_name, opts)
95
+ m = output_identifier_meth
96
+ m2 = input_identifier_meth
97
+ ds = metadata_dataset.from(:information_schema__tables___t).
98
+ join(:information_schema__columns___c, :table_catalog=>:table_catalog,
99
+ :table_schema => :table_schema, :table_name => :table_name).
100
+ select(:column_name___column, :data_type___db_type, :character_maximum_length___max_chars, :column_default___default, :is_nullable___allow_null).
101
+ filter(:c__table_name=>m2.call(table_name.to_s))
102
+ if schema = opts[:schema] || default_schema
103
+ ds.filter!(:table_schema=>schema)
104
+ end
105
+ ds.map do |row|
106
+ row[:allow_null] = row[:allow_null] == 'YES' ? true : false
107
+ row[:default] = nil if blank_object?(row[:default])
108
+ row[:type] = schema_column_type(row[:db_type])
109
+ [m.call(row.delete(:column)), row]
110
+ end
111
+ end
112
+
113
+ # SQL fragment for marking a table as temporary
114
+ def temporary_table_sql
115
+ TEMPORARY
116
+ end
117
+
118
+ # MSSQL has both datetime and timestamp classes, most people are going
119
+ # to want datetime
120
+ def type_literal_generic_datetime(column)
121
+ :datetime
122
+ end
123
+
124
+ # MSSQL has both datetime and timestamp classes, most people are going
125
+ # to want datetime
126
+ def type_literal_generic_time(column)
127
+ column[:only_time] ? :time : :datetime
128
+ end
129
+
130
+ # MSSQL doesn't have a true boolean class, so it uses bit
131
+ def type_literal_generic_trueclass(column)
132
+ :bit
133
+ end
134
+
135
+ # MSSQL uses image type for blobs
136
+ def type_literal_generic_file(column)
137
+ :image
138
+ end
139
+ end
140
+
141
+ module DatasetMethods
142
+ BOOL_TRUE = '1'.freeze
143
+ BOOL_FALSE = '0'.freeze
144
+ COMMA_SEPARATOR = ', '.freeze
145
+ DELETE_CLAUSE_METHODS = Dataset.clause_methods(:delete, %w'with from output from2 where')
146
+ INSERT_CLAUSE_METHODS = Dataset.clause_methods(:insert, %w'with into columns output values')
147
+ SELECT_CLAUSE_METHODS = Dataset.clause_methods(:select, %w'with limit distinct columns from table_options join where group order having compounds')
148
+ UPDATE_CLAUSE_METHODS = Dataset.clause_methods(:update, %w'with table set output from where')
149
+ WILDCARD = LiteralString.new('*').freeze
150
+ CONSTANT_MAP = {:CURRENT_DATE=>'CAST(CURRENT_TIMESTAMP AS DATE)'.freeze, :CURRENT_TIME=>'CAST(CURRENT_TIMESTAMP AS TIME)'.freeze}
151
+
152
+ # Split out from fetch rows to allow processing of JDBC result sets
153
+ # that don't come from issuing an SQL string.
154
+ def process_result_set(result)
155
+ # get column names
156
+ meta = result.getMetaData
157
+ cols = []
158
+ i = 0
159
+ meta.getColumnCount.times{cols << [output_identifier(meta.getColumnLabel(i+=1)), i]}
160
+ @columns = cols.map{|c| c.at(0)}
161
+ row = {}
162
+ blk = if @convert_types
163
+ lambda{ |n, i|
164
+ begin
165
+ row[n] = convert_type(result.getObject(i))
166
+ rescue
167
+ # XXX: this is because HXTT driver throws an error here
168
+ if n == :column_def && row[:type_name] == 'TIMESTAMP'
169
+ row[:column_def] = nil
170
+ end
171
+ end
172
+ }
173
+ else
174
+ lambda{|n, i| row[n] = result.getObject(i)}
175
+ end
176
+ # get rows
177
+ rsmd = result.get_meta_data
178
+ num_cols = rsmd.get_column_count
179
+
180
+ while result.next
181
+ row = {}
182
+ cols.each(&blk)
183
+ yield row
184
+ end
185
+ end
186
+
187
+ # MSSQL uses + for string concatenation
188
+ def complex_expression_sql(op, args)
189
+ case op
190
+ when :'||'
191
+ super(:+, args)
192
+ else
193
+ super(op, args)
194
+ end
195
+ end
196
+
197
+ # MSSQL doesn't support the SQL standard CURRENT_DATE or CURRENT_TIME
198
+ def constant_sql(constant)
199
+ CONSTANT_MAP[constant] || super
200
+ end
201
+
202
+ # When returning all rows, if an offset is used, delete the row_number column
203
+ # before yielding the row.
204
+ def fetch_rows(sql, &block)
205
+ @opts[:offset] ? super(sql) {|r| r.delete(:"recno()"); yield r} : super(sql, &block)
206
+ end
207
+
208
+ # MSSQL uses the CONTAINS keyword for full text search
209
+ def full_text_search(cols, terms, opts = {})
210
+ filter("CONTAINS (#{literal(cols)}, #{literal(terms)})")
211
+ end
212
+
213
+ # MSSQL uses a UNION ALL statement to insert multiple values at once.
214
+ def multi_insert_sql(columns, values)
215
+ [insert_sql(columns, LiteralString.new(values.map {|r| "SELECT #{expression_list(r)}" }.join(" UNION ALL ")))]
216
+ end
217
+
218
+ # Allows you to do .nolock on a query
219
+ def nolock
220
+ clone(:table_options => "(NOLOCK)")
221
+ end
222
+
223
+ # Include an OUTPUT clause in the eventual INSERT, UPDATE, or DELETE query.
224
+ #
225
+ # The first argument is the table to output into, and the second argument
226
+ # is either an Array of column values to select, or a Hash which maps output
227
+ # column names to selected values, in the style of #insert or #update.
228
+ #
229
+ # Output into a returned result set is not currently supported.
230
+ #
231
+ # Examples:
232
+ #
233
+ # dataset.output(:output_table, [:deleted__id, :deleted__name])
234
+ # dataset.output(:output_table, :id => :inserted__id, :name => :inserted__name)
235
+ def output(into, values)
236
+ output = {}
237
+ case values
238
+ when Hash:
239
+ output[:column_list], output[:select_list] = values.keys, values.values
240
+ when Array:
241
+ output[:select_list] = values
242
+ end
243
+ output[:into] = into
244
+ clone({:output => output})
245
+ end
246
+
247
+ # An output method that modifies the receiver.
248
+ def output!(into, values)
249
+ mutation_method(:output, into, values)
250
+ end
251
+
252
+ # MSSQL uses [] to quote identifiers
253
+ def quoted_identifier(name)
254
+ "[#{name}]"
255
+ end
256
+
257
+ # Pagination queries (i.e., limit with offset) are supported HXTT
258
+ # with the help of the recno() function which returns the
259
+ # row number of each record
260
+ def select_sql
261
+ return super unless offset = @opts[:offset]
262
+ if @opts[:select]
263
+ @opts[:select] << :recno.sql_function
264
+ else
265
+ @opts[:select] = [WILDCARD, :recno.sql_function]
266
+ end
267
+ s = unlimited.where("BETWEEN (recno(), #{@opts[:offset] + 1}, #{@opts[:limit] + @opts[:offset]})")
268
+ s.select_sql
269
+ end
270
+ # def select_sql
271
+ # return super unless offset = @opts[:offset]
272
+ # raise(Error, 'Access requires an order be provided if using an offset') unless order = @opts[:order]
273
+ #
274
+ # total_rows = unlimited.count
275
+ # if @opts[:limit] + @opts[:offset] > total_rows
276
+ # correction = @opts[:limit] + @opts[:offset] - total_rows
277
+ # @opts[:limit] = @opts[:limit] - correction
278
+ # end
279
+ #
280
+ # s0 = unlimited.limit(@opts[:limit] + @opts[:offset]).order(order)
281
+ # s1 = unlimited.from(s0.as('s1')).limit(@opts[:limit]).reverse_order(order)
282
+ # s2 = unlimited.from(s1.as('s2')).order(order)
283
+ # s2.select_sql
284
+ # end
285
+
286
+ # The version of the database server.
287
+ def server_version
288
+ db.server_version(@opts[:server])
289
+ end
290
+
291
+ # Microsoft SQL Server does not support INTERSECT or EXCEPT
292
+ def supports_intersect_except?
293
+ false
294
+ end
295
+
296
+ # MSSQL does not support IS TRUE
297
+ def supports_is_true?
298
+ false
299
+ end
300
+
301
+ # MSSQL 2005+ supports window functions
302
+ def supports_window_functions?
303
+ true
304
+ end
305
+
306
+ private
307
+
308
+ # MSSQL can modify joined datasets
309
+ def check_modification_allowed!
310
+ raise(InvalidOperation, "Grouped datasets cannot be modified") if opts[:group]
311
+ end
312
+
313
+ # MSSQL supports the OUTPUT clause for DELETE statements.
314
+ # It also allows prepending a WITH clause.
315
+ def delete_clause_methods
316
+ DELETE_CLAUSE_METHODS
317
+ end
318
+
319
+ # Handle the with clause for delete, insert, and update statements
320
+ # to be the same as the insert statement.
321
+ def delete_with_sql(sql)
322
+ select_with_sql(sql)
323
+ end
324
+ alias insert_with_sql delete_with_sql
325
+ alias update_with_sql delete_with_sql
326
+
327
+ # MSSQL raises an error if you try to provide more than 3 decimal places
328
+ # for a fractional timestamp. This probably doesn't work for smalldatetime
329
+ # fields.
330
+ def format_timestamp_usec(usec)
331
+ sprintf(".%03d", usec/1000)
332
+ end
333
+
334
+ # MSSQL supports FROM clauses in DELETE and UPDATE statements.
335
+ def from_sql(sql)
336
+ if (opts[:from].is_a?(Array) && opts[:from].size > 1) || opts[:join]
337
+ select_from_sql(sql)
338
+ select_join_sql(sql)
339
+ end
340
+ end
341
+ alias delete_from2_sql from_sql
342
+ alias update_from_sql from_sql
343
+
344
+ # MSSQL supports the OUTPUT clause for INSERT statements.
345
+ # It also allows prepending a WITH clause.
346
+ def insert_clause_methods
347
+ INSERT_CLAUSE_METHODS
348
+ end
349
+
350
+ # MSSQL uses a literal hexidecimal number for blob strings
351
+ def literal_blob(v)
352
+ blob = '0x'
353
+ v.each_byte{|x| blob << sprintf('%02x', x)}
354
+ blob
355
+ end
356
+
357
+ # Use unicode string syntax for all strings
358
+ def literal_string(v)
359
+ "N#{super}"
360
+ end
361
+
362
+ # Use 0 for false on MSSQL
363
+ def literal_false
364
+ BOOL_FALSE
365
+ end
366
+
367
+ # Use 1 for true on MSSQL
368
+ def literal_true
369
+ BOOL_TRUE
370
+ end
371
+
372
+ # The alias to use for the row_number column when emulating OFFSET
373
+ def row_number_column
374
+ :x_sequel_row_number_x
375
+ end
376
+
377
+ # MSSQL adds the limit before the columns
378
+ def select_clause_methods
379
+ SELECT_CLAUSE_METHODS
380
+ end
381
+
382
+ # MSSQL uses TOP for limit
383
+ def select_limit_sql(sql)
384
+ sql << " TOP #{@opts[:limit]}" if @opts[:limit]
385
+ end
386
+
387
+ # MSSQL uses the WITH statement to lock tables
388
+ def select_table_options_sql(sql)
389
+ sql << " WITH #{@opts[:table_options]}" if @opts[:table_options]
390
+ end
391
+
392
+ # SQL fragment for MSSQL's OUTPUT clause.
393
+ def output_sql(sql)
394
+ return unless output = @opts[:output]
395
+ sql << " OUTPUT #{column_list(output[:select_list])}"
396
+ if into = output[:into]
397
+ sql << " INTO #{table_ref(into)}"
398
+ if column_list = output[:column_list]
399
+ cl = []
400
+ column_list.each { |k, v| cl << literal(String === k ? k.to_sym : k) }
401
+ sql << " (#{cl.join(COMMA_SEPARATOR)})"
402
+ end
403
+ end
404
+ end
405
+ alias delete_output_sql output_sql
406
+ alias update_output_sql output_sql
407
+ alias insert_output_sql output_sql
408
+
409
+ # MSSQL supports the OUTPUT clause for UPDATE statements.
410
+ # It also allows prepending a WITH clause.
411
+ def update_clause_methods
412
+ UPDATE_CLAUSE_METHODS
413
+ end
414
+ end
415
+ end
416
+ end
@@ -0,0 +1,8 @@
1
+ # encoding: utf-8
2
+ require 'sequel/adapters/jdbc'
3
+
4
+ Sequel::JDBC::DATABASE_SETUP[:access] = proc do |db|
5
+ require 'sequel/adapters/jdbc/access'
6
+ db.extend(Sequel::JDBC::Access::DatabaseMethods)
7
+ com.hxtt.sql.access.AccessDriver
8
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nbadw-util
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Colin Casey
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-10-22 00:00:00 -03:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: sequel
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 3.5.0
24
+ version:
25
+ description: Database models, migrations, and utilities for the New Brunswick Aquatic Data Warehouse
26
+ email: casey.colin@gmail.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - LICENSE
33
+ - README
34
+ files:
35
+ - LICENSE
36
+ - Rakefile
37
+ - lib/nbadw/util/copy_database_task.rb
38
+ - lib/nbadw/util/progress_bar.rb
39
+ - lib/sequel/adapters/jdbc/access.rb
40
+ - lib/sequel/adapters/shared/access.rb
41
+ - lib/sequel/jdbc_access_adapter.rb
42
+ - README
43
+ has_rdoc: true
44
+ homepage: http://github.com/colincasey/nbadw
45
+ licenses: []
46
+
47
+ post_install_message:
48
+ rdoc_options:
49
+ - --charset=UTF-8
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ version:
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: "0"
63
+ version:
64
+ requirements: []
65
+
66
+ rubyforge_project:
67
+ rubygems_version: 1.3.5
68
+ signing_key:
69
+ specification_version: 3
70
+ summary: NB Aquatic Data Warehouse - Models and Database Utilities
71
+ test_files: []
72
+