rubyrep 1.0.0
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.
- data/History.txt +4 -0
- data/License.txt +20 -0
- data/Manifest.txt +137 -0
- data/README.txt +37 -0
- data/Rakefile +30 -0
- data/bin/rubyrep +8 -0
- data/config/hoe.rb +72 -0
- data/config/mysql_config.rb +25 -0
- data/config/postgres_config.rb +21 -0
- data/config/proxied_test_config.rb +14 -0
- data/config/redmine_config.rb +17 -0
- data/config/rep_config.rb +20 -0
- data/config/requirements.rb +32 -0
- data/config/test_config.rb +20 -0
- data/lib/rubyrep/base_runner.rb +195 -0
- data/lib/rubyrep/command_runner.rb +144 -0
- data/lib/rubyrep/committers/buffered_committer.rb +140 -0
- data/lib/rubyrep/committers/committers.rb +146 -0
- data/lib/rubyrep/configuration.rb +240 -0
- data/lib/rubyrep/connection_extenders/connection_extenders.rb +133 -0
- data/lib/rubyrep/connection_extenders/jdbc_extender.rb +284 -0
- data/lib/rubyrep/connection_extenders/mysql_extender.rb +168 -0
- data/lib/rubyrep/connection_extenders/postgresql_extender.rb +261 -0
- data/lib/rubyrep/database_proxy.rb +52 -0
- data/lib/rubyrep/direct_table_scan.rb +75 -0
- data/lib/rubyrep/generate_runner.rb +105 -0
- data/lib/rubyrep/initializer.rb +39 -0
- data/lib/rubyrep/logged_change.rb +326 -0
- data/lib/rubyrep/proxied_table_scan.rb +171 -0
- data/lib/rubyrep/proxy_block_cursor.rb +145 -0
- data/lib/rubyrep/proxy_connection.rb +318 -0
- data/lib/rubyrep/proxy_cursor.rb +44 -0
- data/lib/rubyrep/proxy_row_cursor.rb +43 -0
- data/lib/rubyrep/proxy_runner.rb +89 -0
- data/lib/rubyrep/replication_difference.rb +91 -0
- data/lib/rubyrep/replication_extenders/mysql_replication.rb +271 -0
- data/lib/rubyrep/replication_extenders/postgresql_replication.rb +204 -0
- data/lib/rubyrep/replication_extenders/replication_extenders.rb +26 -0
- data/lib/rubyrep/replication_helper.rb +104 -0
- data/lib/rubyrep/replication_initializer.rb +307 -0
- data/lib/rubyrep/replication_run.rb +48 -0
- data/lib/rubyrep/replication_runner.rb +138 -0
- data/lib/rubyrep/replicators/replicators.rb +37 -0
- data/lib/rubyrep/replicators/two_way_replicator.rb +334 -0
- data/lib/rubyrep/scan_progress_printers/progress_bar.rb +65 -0
- data/lib/rubyrep/scan_progress_printers/scan_progress_printers.rb +65 -0
- data/lib/rubyrep/scan_report_printers/scan_detail_reporter.rb +111 -0
- data/lib/rubyrep/scan_report_printers/scan_report_printers.rb +67 -0
- data/lib/rubyrep/scan_report_printers/scan_summary_reporter.rb +75 -0
- data/lib/rubyrep/scan_runner.rb +25 -0
- data/lib/rubyrep/session.rb +177 -0
- data/lib/rubyrep/sync_helper.rb +111 -0
- data/lib/rubyrep/sync_runner.rb +31 -0
- data/lib/rubyrep/syncers/syncers.rb +112 -0
- data/lib/rubyrep/syncers/two_way_syncer.rb +174 -0
- data/lib/rubyrep/table_scan.rb +54 -0
- data/lib/rubyrep/table_scan_helper.rb +38 -0
- data/lib/rubyrep/table_sorter.rb +70 -0
- data/lib/rubyrep/table_spec_resolver.rb +136 -0
- data/lib/rubyrep/table_sync.rb +68 -0
- data/lib/rubyrep/trigger_mode_switcher.rb +63 -0
- data/lib/rubyrep/type_casting_cursor.rb +31 -0
- data/lib/rubyrep/uninstall_runner.rb +92 -0
- data/lib/rubyrep/version.rb +9 -0
- data/lib/rubyrep.rb +68 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/script/txt2html +74 -0
- data/setup.rb +1585 -0
- data/sims/performance/big_rep_spec.rb +100 -0
- data/sims/performance/big_scan_spec.rb +57 -0
- data/sims/performance/big_sync_spec.rb +141 -0
- data/sims/performance/performance.rake +228 -0
- data/sims/sim_helper.rb +24 -0
- data/spec/base_runner_spec.rb +218 -0
- data/spec/buffered_committer_spec.rb +271 -0
- data/spec/command_runner_spec.rb +145 -0
- data/spec/committers_spec.rb +174 -0
- data/spec/configuration_spec.rb +198 -0
- data/spec/connection_extender_interface_spec.rb +138 -0
- data/spec/connection_extenders_registration_spec.rb +129 -0
- data/spec/database_proxy_spec.rb +48 -0
- data/spec/database_rake_spec.rb +40 -0
- data/spec/db_specific_connection_extenders_spec.rb +34 -0
- data/spec/db_specific_replication_extenders_spec.rb +38 -0
- data/spec/direct_table_scan_spec.rb +61 -0
- data/spec/generate_runner_spec.rb +84 -0
- data/spec/initializer_spec.rb +46 -0
- data/spec/logged_change_spec.rb +480 -0
- data/spec/postgresql_replication_spec.rb +48 -0
- data/spec/postgresql_support_spec.rb +57 -0
- data/spec/progress_bar_spec.rb +77 -0
- data/spec/proxied_table_scan_spec.rb +151 -0
- data/spec/proxy_block_cursor_spec.rb +197 -0
- data/spec/proxy_connection_spec.rb +399 -0
- data/spec/proxy_cursor_spec.rb +56 -0
- data/spec/proxy_row_cursor_spec.rb +66 -0
- data/spec/proxy_runner_spec.rb +70 -0
- data/spec/replication_difference_spec.rb +160 -0
- data/spec/replication_extender_interface_spec.rb +365 -0
- data/spec/replication_extenders_spec.rb +32 -0
- data/spec/replication_helper_spec.rb +121 -0
- data/spec/replication_initializer_spec.rb +477 -0
- data/spec/replication_run_spec.rb +166 -0
- data/spec/replication_runner_spec.rb +213 -0
- data/spec/replicators_spec.rb +31 -0
- data/spec/rubyrep_spec.rb +8 -0
- data/spec/scan_detail_reporter_spec.rb +119 -0
- data/spec/scan_progress_printers_spec.rb +68 -0
- data/spec/scan_report_printers_spec.rb +67 -0
- data/spec/scan_runner_spec.rb +50 -0
- data/spec/scan_summary_reporter_spec.rb +61 -0
- data/spec/session_spec.rb +212 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +295 -0
- data/spec/sync_helper_spec.rb +157 -0
- data/spec/sync_runner_spec.rb +78 -0
- data/spec/syncers_spec.rb +171 -0
- data/spec/table_scan_helper_spec.rb +29 -0
- data/spec/table_scan_spec.rb +49 -0
- data/spec/table_sorter_spec.rb +31 -0
- data/spec/table_spec_resolver_spec.rb +102 -0
- data/spec/table_sync_spec.rb +84 -0
- data/spec/trigger_mode_switcher_spec.rb +83 -0
- data/spec/two_way_replicator_spec.rb +551 -0
- data/spec/two_way_syncer_spec.rb +256 -0
- data/spec/type_casting_cursor_spec.rb +50 -0
- data/spec/uninstall_runner_spec.rb +86 -0
- data/tasks/database.rake +439 -0
- data/tasks/deployment.rake +29 -0
- data/tasks/environment.rake +9 -0
- data/tasks/java.rake +37 -0
- data/tasks/redmine_test.rake +47 -0
- data/tasks/rspec.rake +68 -0
- data/tasks/rubyrep.tailor +18 -0
- data/tasks/stats.rake +19 -0
- data/tasks/task_helper.rb +20 -0
- data.tar.gz.sig +0 -0
- metadata +243 -0
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
class ActiveRecord::ConnectionAdapters::Column
|
|
2
|
+
# Bug in ActiveRecord parsing of PostgreSQL timestamps with microseconds:
|
|
3
|
+
# Certain values are incorrectly rounded, thus ending up with timestamps
|
|
4
|
+
# that are off by one microsecond.
|
|
5
|
+
# This monkey patch fixes the problem.
|
|
6
|
+
def self.fast_string_to_time(string)
|
|
7
|
+
if string =~ Format::ISO_DATETIME
|
|
8
|
+
microsec = ($7.to_f * 1_000_000).round # used to be #to_i instead
|
|
9
|
+
new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module RR
|
|
15
|
+
|
|
16
|
+
# Connection extenders provide additional database specific functionality
|
|
17
|
+
# not coming in the ActiveRecord library.
|
|
18
|
+
# This module itself only provides functionality to register and retrieve
|
|
19
|
+
# such connection extenders.
|
|
20
|
+
module ConnectionExtenders
|
|
21
|
+
# Returns a Hash of currently registered connection extenders.
|
|
22
|
+
# (Empty Hash if no connection extenders were defined.)
|
|
23
|
+
def self.extenders
|
|
24
|
+
@extenders ||= {}
|
|
25
|
+
@extenders
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Registers one or multiple connection extender.
|
|
29
|
+
# extender is a Hash with
|
|
30
|
+
# key:: The adapter symbol as used by ActiveRecord::Connection Adapters, e. g. :postgresql
|
|
31
|
+
# value:: Name of the module implementing the connection extender
|
|
32
|
+
def self.register(extender)
|
|
33
|
+
@extenders ||= {}
|
|
34
|
+
@extenders.merge! extender
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Dummy ActiveRecord descendant only used to create database connections.
|
|
38
|
+
class DummyActiveRecord < ActiveRecord::Base
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Creates an ActiveRecord database connection according to the provided +config+ connection hash.
|
|
42
|
+
# Possible values of this parameter are described in ActiveRecord::Base#establish_connection.
|
|
43
|
+
# The database connection is extended with the correct ConnectionExtenders module.
|
|
44
|
+
#
|
|
45
|
+
# ActiveRecord only allows one database connection per class.
|
|
46
|
+
# (It disconnects the existing database connection if a new connection is established.)
|
|
47
|
+
# To go around this, we delete ActiveRecord's memory of the existing database connection
|
|
48
|
+
# as soon as it is created.
|
|
49
|
+
def self.db_connect_without_cache(config)
|
|
50
|
+
if RUBY_PLATFORM =~ /java/
|
|
51
|
+
adapter = config[:adapter]
|
|
52
|
+
|
|
53
|
+
# As recommended in the activerecord-jdbc-adapter use the jdbc versions
|
|
54
|
+
# of the Adapters. E. g. instead of "postgresql", "jdbcpostgresql".
|
|
55
|
+
adapter = 'jdbc' + adapter unless adapter =~ /^jdbc/
|
|
56
|
+
|
|
57
|
+
DummyActiveRecord.establish_connection(config.merge(:adapter => adapter))
|
|
58
|
+
else
|
|
59
|
+
DummyActiveRecord.establish_connection(config)
|
|
60
|
+
end
|
|
61
|
+
connection = DummyActiveRecord.connection
|
|
62
|
+
|
|
63
|
+
# Delete the database connection from ActiveRecords's 'memory'
|
|
64
|
+
ActiveRecord::Base.connection_handler.connection_pools.delete DummyActiveRecord.name
|
|
65
|
+
|
|
66
|
+
extender = ""
|
|
67
|
+
if RUBY_PLATFORM =~ /java/
|
|
68
|
+
extender = :jdbc
|
|
69
|
+
elsif ConnectionExtenders.extenders.include? config[:adapter].to_sym
|
|
70
|
+
extender = config[:adapter].to_sym
|
|
71
|
+
else
|
|
72
|
+
raise "No ConnectionExtender available for :#{config[:adapter]}"
|
|
73
|
+
end
|
|
74
|
+
connection_module = ConnectionExtenders.extenders[extender]
|
|
75
|
+
connection.extend connection_module
|
|
76
|
+
|
|
77
|
+
# Hack to get Postgres schema support under JRuby to par with the standard
|
|
78
|
+
# ruby version
|
|
79
|
+
if RUBY_PLATFORM =~ /java/ and config[:adapter].to_sym == :postgresql
|
|
80
|
+
connection.extend RR::ConnectionExtenders::JdbcPostgreSQLExtender
|
|
81
|
+
connection.initialize_search_path
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
replication_module = ReplicationExtenders.extenders[config[:adapter].to_sym]
|
|
85
|
+
connection.extend replication_module if replication_module
|
|
86
|
+
|
|
87
|
+
connection
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
@@use_cache = true
|
|
91
|
+
|
|
92
|
+
# Returns the current cache status (+true+ if caching is used; +false+ otherwise).
|
|
93
|
+
def self.use_cache?; @@use_cache; end
|
|
94
|
+
|
|
95
|
+
# Returns the connection cache hash.
|
|
96
|
+
def self.connection_cache; @@connection_cache; end
|
|
97
|
+
|
|
98
|
+
# Sets a new connection cache
|
|
99
|
+
def self.connection_cache=(cache)
|
|
100
|
+
@@connection_cache = cache
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Creates database connections by calling #db_connect_without_cache with the
|
|
104
|
+
# provided +config+ configuration hash.
|
|
105
|
+
# A new database connection is created only if no according cached connection
|
|
106
|
+
# is available.
|
|
107
|
+
def self.db_connect(config)
|
|
108
|
+
config_dump = Marshal.dump config.reject {|key, | [:proxy_host, :proxy_port].include? key}
|
|
109
|
+
config_checksum = Digest::SHA1.hexdigest(config_dump)
|
|
110
|
+
@@connection_cache ||= {}
|
|
111
|
+
cached_db_connection = connection_cache[config_checksum]
|
|
112
|
+
if use_cache? and cached_db_connection and cached_db_connection.active?
|
|
113
|
+
cached_db_connection
|
|
114
|
+
else
|
|
115
|
+
db_connection = db_connect_without_cache config
|
|
116
|
+
connection_cache[config_checksum] = db_connection if @@use_cache
|
|
117
|
+
db_connection
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# If status == true: enable the cache. If status == false: don' use cache
|
|
122
|
+
# Returns the old connection caching status
|
|
123
|
+
def self.use_db_connection_cache(status)
|
|
124
|
+
old_status, @@use_cache = @@use_cache, status
|
|
125
|
+
old_status
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Free up all cached connections
|
|
129
|
+
def self.clear_db_connection_cache
|
|
130
|
+
@@connection_cache = {}
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
require 'java'
|
|
2
|
+
|
|
3
|
+
module RR
|
|
4
|
+
module ConnectionExtenders
|
|
5
|
+
|
|
6
|
+
# Provides various JDBC specific functionality required by Rubyrep.
|
|
7
|
+
module JdbcSQLExtender
|
|
8
|
+
RR::ConnectionExtenders.register :jdbc => self
|
|
9
|
+
|
|
10
|
+
# A cursor to iterate over the records returned by select_cursor.
|
|
11
|
+
# Only one row is kept in memory at a time.
|
|
12
|
+
module JdbcResultSet
|
|
13
|
+
# Returns true if there are more rows to read.
|
|
14
|
+
def next?
|
|
15
|
+
if @next_status == nil
|
|
16
|
+
@next_status = self.next
|
|
17
|
+
end
|
|
18
|
+
@next_status
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Returns the row as a column => value hash and moves the cursor to the next row.
|
|
22
|
+
def next_row
|
|
23
|
+
raise("no more rows available") unless next?
|
|
24
|
+
@next_status = nil
|
|
25
|
+
|
|
26
|
+
unless @columns
|
|
27
|
+
meta_data = self.getMetaData
|
|
28
|
+
stores_upper = self.getStatement.getConnection.getMetaData.storesUpperCaseIdentifiers
|
|
29
|
+
column_count = meta_data.getColumnCount
|
|
30
|
+
@columns = Array.new(column_count)
|
|
31
|
+
@columns.each_index do |i|
|
|
32
|
+
column_name = meta_data.getColumnName(i+1)
|
|
33
|
+
if stores_upper and not column_name =~ /[a-z]/
|
|
34
|
+
column_name.downcase!
|
|
35
|
+
end
|
|
36
|
+
@columns[i] = {
|
|
37
|
+
:index => i+1,
|
|
38
|
+
:name => column_name,
|
|
39
|
+
:type => meta_data.getColumnType(i+1)
|
|
40
|
+
#:scale => meta_data.getScale(i+1)
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
row = {}
|
|
46
|
+
@columns.each_index do |i|
|
|
47
|
+
row[@columns[i][:name]] = jdbc_to_ruby(@columns[i])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
row
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Releases the databases resources hold by this cursor
|
|
54
|
+
def clear
|
|
55
|
+
@columns = nil
|
|
56
|
+
self.close
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
Types = java.sql.Types unless const_defined?(:Types)
|
|
60
|
+
|
|
61
|
+
# Converts the specified column of the current row to the proper ruby string
|
|
62
|
+
# column is a hash with the following elements:
|
|
63
|
+
# * :index: field number (starting with 1) of the result set field
|
|
64
|
+
# * :type: the java.sql.Type constant specifying the type of the result set field
|
|
65
|
+
def jdbc_to_ruby(column)
|
|
66
|
+
case column[:type]
|
|
67
|
+
when Types::BINARY, Types::BLOB, Types::LONGVARBINARY, Types::VARBINARY
|
|
68
|
+
is = self.getBinaryStream(column[:index])
|
|
69
|
+
if is == nil or self.wasNull
|
|
70
|
+
return nil
|
|
71
|
+
end
|
|
72
|
+
byte_list = org.jruby.util.ByteList.new(2048)
|
|
73
|
+
buffer = Java::byte[2048].new
|
|
74
|
+
while (n = is.read(buffer)) != -1
|
|
75
|
+
byte_list.append(buffer, 0, n)
|
|
76
|
+
end
|
|
77
|
+
is.close
|
|
78
|
+
return byte_list.toString
|
|
79
|
+
when Types::LONGVARCHAR, Types::CLOB
|
|
80
|
+
rss = self.getCharacterStream(column[:index])
|
|
81
|
+
if rss == nil or self.wasNull
|
|
82
|
+
return nil
|
|
83
|
+
end
|
|
84
|
+
str = java.lang.StringBuffer.new(2048)
|
|
85
|
+
cuf = Java::char[2048].new
|
|
86
|
+
while (n = rss.read(cuf)) != -1
|
|
87
|
+
str.append(cuf, 0, n)
|
|
88
|
+
end
|
|
89
|
+
rss.close
|
|
90
|
+
return str.toString
|
|
91
|
+
when Types::TIMESTAMP
|
|
92
|
+
time = self.getTimestamp(column[:index]);
|
|
93
|
+
if time == nil or self.wasNull
|
|
94
|
+
return nil
|
|
95
|
+
end
|
|
96
|
+
time_string = time.toString()
|
|
97
|
+
time_string = time_string.gsub(/ 00:00:00.0$/, '')
|
|
98
|
+
return time_string
|
|
99
|
+
else
|
|
100
|
+
value = self.getString(column[:index])
|
|
101
|
+
if value == nil or self.wasNull
|
|
102
|
+
return nil
|
|
103
|
+
end
|
|
104
|
+
return value
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
private :jdbc_to_ruby
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Monkey patch for activerecord-jdbc-adapter-0.7.2 as it doesn't set the
|
|
111
|
+
# +@active+ flag to false, thus ActiveRecord#active? incorrectly confirms
|
|
112
|
+
# the connection to still be active.
|
|
113
|
+
def disconnect!
|
|
114
|
+
super
|
|
115
|
+
@active = false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Executes the given sql query with the otional name written in the
|
|
119
|
+
# ActiveRecord log file.
|
|
120
|
+
# * +row_buffer_size+: not used.
|
|
121
|
+
# Returns the results as a Cursor object supporting
|
|
122
|
+
# * next? - returns true if there are more rows to read
|
|
123
|
+
# * next_row - returns the row as a column => value hash and moves the cursor to the next row
|
|
124
|
+
# * clear - clearing the cursor (making allocated memory available for GC)
|
|
125
|
+
def select_cursor(sql, row_buffer_size = 1000)
|
|
126
|
+
statement = @connection.connection.createStatement
|
|
127
|
+
statement.setFetchSize row_buffer_size
|
|
128
|
+
result_set = statement.executeQuery(sql)
|
|
129
|
+
result_set.send :extend, JdbcResultSet
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Returns an ordered list of primary key column names of the given table
|
|
133
|
+
def primary_key_names(table)
|
|
134
|
+
if not tables.include? table
|
|
135
|
+
raise "table '#{table}' does not exist"
|
|
136
|
+
end
|
|
137
|
+
columns = []
|
|
138
|
+
result_set = @connection.connection.getMetaData.getPrimaryKeys(nil, nil, table);
|
|
139
|
+
while result_set.next
|
|
140
|
+
column_name = result_set.getString("COLUMN_NAME")
|
|
141
|
+
key_seq = result_set.getShort("KEY_SEQ")
|
|
142
|
+
columns << {:column_name => column_name, :key_seq => key_seq}
|
|
143
|
+
end
|
|
144
|
+
columns.sort! {|a, b| a[:key_seq] <=> b[:key_seq]}
|
|
145
|
+
key_names = columns.map {|column| column[:column_name]}
|
|
146
|
+
key_names
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Returns for each given table, which other tables it references via
|
|
150
|
+
# foreign key constraints.
|
|
151
|
+
# * tables: an array of table names
|
|
152
|
+
# * returns: a hash with
|
|
153
|
+
# * key: name of the referencing table
|
|
154
|
+
# * value: an array of names of referenced tables
|
|
155
|
+
def referenced_tables(tables)
|
|
156
|
+
result = {}
|
|
157
|
+
tables.each do |table|
|
|
158
|
+
references_of_this_table = []
|
|
159
|
+
result_set = @connection.connection.getMetaData.getImportedKeys(nil, nil, table)
|
|
160
|
+
while result_set.next
|
|
161
|
+
referenced_table = result_set.getString("PKTABLE_NAME")
|
|
162
|
+
unless references_of_this_table.include? referenced_table
|
|
163
|
+
references_of_this_table << referenced_table
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
result[table] = references_of_this_table
|
|
167
|
+
end
|
|
168
|
+
result
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
require 'connection_extenders/postgresql_extender'
|
|
173
|
+
|
|
174
|
+
# Adds the correct query executioner functionality to the base class
|
|
175
|
+
class JdbcPostgreSQLFetcher < PostgreSQLFetcher
|
|
176
|
+
# Executes the given statements and returns the result set.
|
|
177
|
+
def execute(sql)
|
|
178
|
+
statement = connection.instance_variable_get(:@connection).connection.createStatement
|
|
179
|
+
execute_method = sql =~ /close/i ? :execute : :executeQuery
|
|
180
|
+
result_set = statement.send(execute_method, sql)
|
|
181
|
+
result_set.send :extend, RR::ConnectionExtenders::JdbcSQLExtender::JdbcResultSet
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# PostgreSQL specific functionality not provided by the standard JDBC
|
|
186
|
+
# connection extender:
|
|
187
|
+
# * Integration of memory efficient select_cursor.
|
|
188
|
+
# * Hack to get schema support for Postgres under JRuby on par with the
|
|
189
|
+
# standard ruby version.
|
|
190
|
+
module JdbcPostgreSQLExtender
|
|
191
|
+
|
|
192
|
+
# Executes the given sql query with the otional name written in the
|
|
193
|
+
# ActiveRecord log file.
|
|
194
|
+
#
|
|
195
|
+
# :+row_buffer_size+ controls how many records are ready into memory at a
|
|
196
|
+
# time. Implemented using the PostgeSQL "DECLARE CURSOR" and "FETCH" constructs.
|
|
197
|
+
# This is necessary as the postgresql driver always reads the
|
|
198
|
+
# complete resultset into memory.
|
|
199
|
+
#
|
|
200
|
+
# Returns the results as a Cursor object supporting
|
|
201
|
+
# * next? - returns true if there are more rows to read
|
|
202
|
+
# * next_row - returns the row as a column => value hash and moves the cursor to the next row
|
|
203
|
+
# * clear - clearing the cursor (making allocated memory available for GC)
|
|
204
|
+
def select_cursor(sql, row_buffer_size = 1000)
|
|
205
|
+
cursor_name = "RR_#{Time.now.to_i}#{rand(1_000_000)}"
|
|
206
|
+
|
|
207
|
+
statement = @connection.connection.createStatement
|
|
208
|
+
statement.execute("DECLARE #{cursor_name} NO SCROLL CURSOR WITH HOLD FOR " + sql)
|
|
209
|
+
JdbcPostgreSQLFetcher.new(self, cursor_name, row_buffer_size)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Returns the list of a table's column names, data types, and default values.
|
|
213
|
+
#
|
|
214
|
+
# The underlying query is roughly:
|
|
215
|
+
# SELECT column.name, column.type, default.value
|
|
216
|
+
# FROM column LEFT JOIN default
|
|
217
|
+
# ON column.table_id = default.table_id
|
|
218
|
+
# AND column.num = default.column_num
|
|
219
|
+
# WHERE column.table_id = get_table_id('table_name')
|
|
220
|
+
# AND column.num > 0
|
|
221
|
+
# AND NOT column.is_dropped
|
|
222
|
+
# ORDER BY column.num
|
|
223
|
+
#
|
|
224
|
+
# If the table name is not prefixed with a schema, the database will
|
|
225
|
+
# take the first match from the schema search path.
|
|
226
|
+
#
|
|
227
|
+
# Query implementation notes:
|
|
228
|
+
# - format_type includes the column size constraint, e.g. varchar(50)
|
|
229
|
+
# - ::regclass is a function that gives the id for a table name
|
|
230
|
+
def column_definitions(table_name) #:nodoc:
|
|
231
|
+
rows = select_all(<<-end_sql)
|
|
232
|
+
SELECT a.attname as name, format_type(a.atttypid, a.atttypmod) as type, d.adsrc as default, a.attnotnull as notnull
|
|
233
|
+
FROM pg_attribute a LEFT JOIN pg_attrdef d
|
|
234
|
+
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
|
|
235
|
+
WHERE a.attrelid = '#{table_name}'::regclass
|
|
236
|
+
AND a.attnum > 0 AND NOT a.attisdropped
|
|
237
|
+
ORDER BY a.attnum
|
|
238
|
+
end_sql
|
|
239
|
+
|
|
240
|
+
rows.map do |row|
|
|
241
|
+
[row['name'], row['type'], row['default'], row['notnull']]
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Returns the list of all column definitions for a table.
|
|
246
|
+
def columns(table_name, name = nil)
|
|
247
|
+
# Limit, precision, and scale are all handled by the superclass.
|
|
248
|
+
column_definitions(table_name).collect do |name, type, default, notnull|
|
|
249
|
+
ActiveRecord::ConnectionAdapters::PostgreSQLColumn.new(name, default, type, notnull == 'f')
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Sets the schema search path as per configuration parameters
|
|
254
|
+
def initialize_search_path
|
|
255
|
+
execute "SET search_path TO #{config[:schema_search_path]}" if config[:schema_search_path]
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Returns the active schema search path.
|
|
259
|
+
def schema_search_path
|
|
260
|
+
@schema_search_path ||= select_one('SHOW search_path')['search_path']
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Returns the list of all tables in the schema search path or a specified schema.
|
|
264
|
+
def tables(name = nil)
|
|
265
|
+
schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
|
|
266
|
+
select_all(<<-SQL, name).map { |row| row['tablename'] }
|
|
267
|
+
SELECT tablename
|
|
268
|
+
FROM pg_tables
|
|
269
|
+
WHERE schemaname IN (#{schemas})
|
|
270
|
+
SQL
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Converts the given Time object into the correctly formatted string
|
|
274
|
+
# representation.
|
|
275
|
+
#
|
|
276
|
+
# Monkeypatched as activerecord-jdbcpostgresql-adapter (at least in version
|
|
277
|
+
# 0.8.2) does otherwise "loose" the microseconds when writing Time values
|
|
278
|
+
# to the database.
|
|
279
|
+
def quoted_date(value)
|
|
280
|
+
"#{value.strftime("%Y-%m-%d %H:%M:%S")}#{value.respond_to?(:usec) ? ".#{value.usec.to_s.rjust(6, '0')}" : ""}"
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# A cursor to iterate over the records returned by select_cursor.
|
|
2
|
+
# Only one row is kept in memory at a time.
|
|
3
|
+
|
|
4
|
+
module MysqlResultExtender
|
|
5
|
+
# Returns true if there are more rows to read.
|
|
6
|
+
def next?
|
|
7
|
+
@current_row_num ||= 0
|
|
8
|
+
@num_rows ||= self.num_rows()
|
|
9
|
+
@current_row_num < @num_rows
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Returns the row as a column => value hash and moves the cursor to the next row.
|
|
13
|
+
def next_row
|
|
14
|
+
raise("no more rows available") unless next?
|
|
15
|
+
row = fetch_hash()
|
|
16
|
+
@current_row_num += 1
|
|
17
|
+
row
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Releases the database resources hold by this cursor
|
|
21
|
+
def clear
|
|
22
|
+
free
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
module RR
|
|
27
|
+
|
|
28
|
+
# Overwrites #select_cursor to allow fetching of MySQL results in chunks
|
|
29
|
+
class ProxyConnection
|
|
30
|
+
|
|
31
|
+
# Allow selecting of MySQL results in chunks.
|
|
32
|
+
# For full documentation of method interface refer to ProxyConnection#select_cursor.
|
|
33
|
+
def select_cursor_with_mysql_chunks(options)
|
|
34
|
+
if config[:adapter] != 'mysql' or !options.include?(:row_buffer_size) or options.include?(:query)
|
|
35
|
+
select_cursor_without_mysql_chunks options
|
|
36
|
+
else
|
|
37
|
+
ConnectionExtenders::MysqlFetcher.new(self, options)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
alias_method_chain :select_cursor, :mysql_chunks unless method_defined?(:select_cursor_without_mysql_chunks)
|
|
41
|
+
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
module ConnectionExtenders
|
|
45
|
+
|
|
46
|
+
# Fetches MySQL results in chunks
|
|
47
|
+
class MysqlFetcher
|
|
48
|
+
|
|
49
|
+
# The current database ProxyConnection
|
|
50
|
+
attr_accessor :connection
|
|
51
|
+
|
|
52
|
+
# hash of select options
|
|
53
|
+
attr_accessor :options
|
|
54
|
+
|
|
55
|
+
# column_name => value hash of the last returned row
|
|
56
|
+
attr_accessor :last_row
|
|
57
|
+
|
|
58
|
+
# Creates a new fetcher.
|
|
59
|
+
# * +connection+: the current database connection
|
|
60
|
+
# * +cursor_name+: name of the cursor from which to fetch
|
|
61
|
+
# * +row_buffer_size+: number of records to read at once
|
|
62
|
+
def initialize(connection, options)
|
|
63
|
+
self.connection = connection
|
|
64
|
+
self.options = options.clone
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Returns +true+ if there are more rows to read.
|
|
68
|
+
def next?
|
|
69
|
+
unless @current_result
|
|
70
|
+
if last_row
|
|
71
|
+
options.merge! :from => last_row, :exclude_starting_row => true
|
|
72
|
+
end
|
|
73
|
+
options[:query] =
|
|
74
|
+
connection.table_select_query(options[:table], options) +
|
|
75
|
+
" limit #{options[:row_buffer_size]}"
|
|
76
|
+
@current_result = connection.select_cursor_without_mysql_chunks(options)
|
|
77
|
+
end
|
|
78
|
+
@current_result.next?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Returns the row as a column => value hash and moves the cursor to the next row.
|
|
82
|
+
def next_row
|
|
83
|
+
raise("no more rows available") unless next?
|
|
84
|
+
self.last_row = @current_result.next_row
|
|
85
|
+
unless @current_result.next?
|
|
86
|
+
@current_result.clear
|
|
87
|
+
@current_result = nil
|
|
88
|
+
end
|
|
89
|
+
self.last_row
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Closes the cursor and frees up all ressources
|
|
93
|
+
def clear
|
|
94
|
+
if @current_result
|
|
95
|
+
@current_result.clear
|
|
96
|
+
@current_result = nil
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Provides various MySQL specific functionality required by Rubyrep.
|
|
102
|
+
module MysqlExtender
|
|
103
|
+
RR::ConnectionExtenders.register :mysql => self
|
|
104
|
+
|
|
105
|
+
# Executes the given sql query with the optional name written in the
|
|
106
|
+
# ActiveRecord log file.
|
|
107
|
+
# :+row_buffer_size+ is not currently used.
|
|
108
|
+
# Returns the results as a Cursor object supporting
|
|
109
|
+
# * next? - returns true if there are more rows to read
|
|
110
|
+
# * next_row - returns the row as a column => value hash and moves the cursor to the next row
|
|
111
|
+
# * clear - clearing the cursor (making allocated memory available for GC)
|
|
112
|
+
def select_cursor(sql, row_buffer_size = 1000)
|
|
113
|
+
result = execute sql
|
|
114
|
+
result.send :extend, MysqlResultExtender
|
|
115
|
+
result
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Returns an ordered list of primary key column names of the given table
|
|
119
|
+
def primary_key_names(table)
|
|
120
|
+
row = self.select_one(<<-end_sql)
|
|
121
|
+
select table_name from information_schema.tables
|
|
122
|
+
where table_schema = database() and table_name = '#{table}'
|
|
123
|
+
end_sql
|
|
124
|
+
if row.nil?
|
|
125
|
+
raise "table '#{table}' does not exist"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
rows = self.select_all(<<-end_sql)
|
|
129
|
+
select column_name from information_schema.key_column_usage
|
|
130
|
+
where table_schema = database() and table_name = '#{table}'
|
|
131
|
+
and constraint_name = 'PRIMARY'
|
|
132
|
+
order by ordinal_position
|
|
133
|
+
end_sql
|
|
134
|
+
|
|
135
|
+
columns = rows.map {|_row| _row['column_name']}
|
|
136
|
+
columns
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Returns for each given table, which other tables it references via
|
|
140
|
+
# foreign key constraints.
|
|
141
|
+
# * tables: an array of table names
|
|
142
|
+
# Returns: a hash with
|
|
143
|
+
# * key: name of the referencing table
|
|
144
|
+
# * value: an array of names of referenced tables
|
|
145
|
+
def referenced_tables(tables)
|
|
146
|
+
rows = self.select_all(<<-end_sql)
|
|
147
|
+
select distinct table_name as referencing_table, referenced_table_name as referenced_table
|
|
148
|
+
from information_schema.key_column_usage
|
|
149
|
+
where table_schema = database()
|
|
150
|
+
and table_name in ('#{tables.join("', '")}')
|
|
151
|
+
end_sql
|
|
152
|
+
result = {}
|
|
153
|
+
rows.each do |row|
|
|
154
|
+
unless result.include? row['referencing_table']
|
|
155
|
+
result[row['referencing_table']] = []
|
|
156
|
+
end
|
|
157
|
+
if row['referenced_table'] != nil
|
|
158
|
+
result[row['referencing_table']] << row['referenced_table']
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
tables.each do |table|
|
|
162
|
+
result[table] = [] unless result.include? table
|
|
163
|
+
end
|
|
164
|
+
result
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|