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.
Files changed (140) hide show
  1. data/History.txt +4 -0
  2. data/License.txt +20 -0
  3. data/Manifest.txt +137 -0
  4. data/README.txt +37 -0
  5. data/Rakefile +30 -0
  6. data/bin/rubyrep +8 -0
  7. data/config/hoe.rb +72 -0
  8. data/config/mysql_config.rb +25 -0
  9. data/config/postgres_config.rb +21 -0
  10. data/config/proxied_test_config.rb +14 -0
  11. data/config/redmine_config.rb +17 -0
  12. data/config/rep_config.rb +20 -0
  13. data/config/requirements.rb +32 -0
  14. data/config/test_config.rb +20 -0
  15. data/lib/rubyrep/base_runner.rb +195 -0
  16. data/lib/rubyrep/command_runner.rb +144 -0
  17. data/lib/rubyrep/committers/buffered_committer.rb +140 -0
  18. data/lib/rubyrep/committers/committers.rb +146 -0
  19. data/lib/rubyrep/configuration.rb +240 -0
  20. data/lib/rubyrep/connection_extenders/connection_extenders.rb +133 -0
  21. data/lib/rubyrep/connection_extenders/jdbc_extender.rb +284 -0
  22. data/lib/rubyrep/connection_extenders/mysql_extender.rb +168 -0
  23. data/lib/rubyrep/connection_extenders/postgresql_extender.rb +261 -0
  24. data/lib/rubyrep/database_proxy.rb +52 -0
  25. data/lib/rubyrep/direct_table_scan.rb +75 -0
  26. data/lib/rubyrep/generate_runner.rb +105 -0
  27. data/lib/rubyrep/initializer.rb +39 -0
  28. data/lib/rubyrep/logged_change.rb +326 -0
  29. data/lib/rubyrep/proxied_table_scan.rb +171 -0
  30. data/lib/rubyrep/proxy_block_cursor.rb +145 -0
  31. data/lib/rubyrep/proxy_connection.rb +318 -0
  32. data/lib/rubyrep/proxy_cursor.rb +44 -0
  33. data/lib/rubyrep/proxy_row_cursor.rb +43 -0
  34. data/lib/rubyrep/proxy_runner.rb +89 -0
  35. data/lib/rubyrep/replication_difference.rb +91 -0
  36. data/lib/rubyrep/replication_extenders/mysql_replication.rb +271 -0
  37. data/lib/rubyrep/replication_extenders/postgresql_replication.rb +204 -0
  38. data/lib/rubyrep/replication_extenders/replication_extenders.rb +26 -0
  39. data/lib/rubyrep/replication_helper.rb +104 -0
  40. data/lib/rubyrep/replication_initializer.rb +307 -0
  41. data/lib/rubyrep/replication_run.rb +48 -0
  42. data/lib/rubyrep/replication_runner.rb +138 -0
  43. data/lib/rubyrep/replicators/replicators.rb +37 -0
  44. data/lib/rubyrep/replicators/two_way_replicator.rb +334 -0
  45. data/lib/rubyrep/scan_progress_printers/progress_bar.rb +65 -0
  46. data/lib/rubyrep/scan_progress_printers/scan_progress_printers.rb +65 -0
  47. data/lib/rubyrep/scan_report_printers/scan_detail_reporter.rb +111 -0
  48. data/lib/rubyrep/scan_report_printers/scan_report_printers.rb +67 -0
  49. data/lib/rubyrep/scan_report_printers/scan_summary_reporter.rb +75 -0
  50. data/lib/rubyrep/scan_runner.rb +25 -0
  51. data/lib/rubyrep/session.rb +177 -0
  52. data/lib/rubyrep/sync_helper.rb +111 -0
  53. data/lib/rubyrep/sync_runner.rb +31 -0
  54. data/lib/rubyrep/syncers/syncers.rb +112 -0
  55. data/lib/rubyrep/syncers/two_way_syncer.rb +174 -0
  56. data/lib/rubyrep/table_scan.rb +54 -0
  57. data/lib/rubyrep/table_scan_helper.rb +38 -0
  58. data/lib/rubyrep/table_sorter.rb +70 -0
  59. data/lib/rubyrep/table_spec_resolver.rb +136 -0
  60. data/lib/rubyrep/table_sync.rb +68 -0
  61. data/lib/rubyrep/trigger_mode_switcher.rb +63 -0
  62. data/lib/rubyrep/type_casting_cursor.rb +31 -0
  63. data/lib/rubyrep/uninstall_runner.rb +92 -0
  64. data/lib/rubyrep/version.rb +9 -0
  65. data/lib/rubyrep.rb +68 -0
  66. data/script/destroy +14 -0
  67. data/script/generate +14 -0
  68. data/script/txt2html +74 -0
  69. data/setup.rb +1585 -0
  70. data/sims/performance/big_rep_spec.rb +100 -0
  71. data/sims/performance/big_scan_spec.rb +57 -0
  72. data/sims/performance/big_sync_spec.rb +141 -0
  73. data/sims/performance/performance.rake +228 -0
  74. data/sims/sim_helper.rb +24 -0
  75. data/spec/base_runner_spec.rb +218 -0
  76. data/spec/buffered_committer_spec.rb +271 -0
  77. data/spec/command_runner_spec.rb +145 -0
  78. data/spec/committers_spec.rb +174 -0
  79. data/spec/configuration_spec.rb +198 -0
  80. data/spec/connection_extender_interface_spec.rb +138 -0
  81. data/spec/connection_extenders_registration_spec.rb +129 -0
  82. data/spec/database_proxy_spec.rb +48 -0
  83. data/spec/database_rake_spec.rb +40 -0
  84. data/spec/db_specific_connection_extenders_spec.rb +34 -0
  85. data/spec/db_specific_replication_extenders_spec.rb +38 -0
  86. data/spec/direct_table_scan_spec.rb +61 -0
  87. data/spec/generate_runner_spec.rb +84 -0
  88. data/spec/initializer_spec.rb +46 -0
  89. data/spec/logged_change_spec.rb +480 -0
  90. data/spec/postgresql_replication_spec.rb +48 -0
  91. data/spec/postgresql_support_spec.rb +57 -0
  92. data/spec/progress_bar_spec.rb +77 -0
  93. data/spec/proxied_table_scan_spec.rb +151 -0
  94. data/spec/proxy_block_cursor_spec.rb +197 -0
  95. data/spec/proxy_connection_spec.rb +399 -0
  96. data/spec/proxy_cursor_spec.rb +56 -0
  97. data/spec/proxy_row_cursor_spec.rb +66 -0
  98. data/spec/proxy_runner_spec.rb +70 -0
  99. data/spec/replication_difference_spec.rb +160 -0
  100. data/spec/replication_extender_interface_spec.rb +365 -0
  101. data/spec/replication_extenders_spec.rb +32 -0
  102. data/spec/replication_helper_spec.rb +121 -0
  103. data/spec/replication_initializer_spec.rb +477 -0
  104. data/spec/replication_run_spec.rb +166 -0
  105. data/spec/replication_runner_spec.rb +213 -0
  106. data/spec/replicators_spec.rb +31 -0
  107. data/spec/rubyrep_spec.rb +8 -0
  108. data/spec/scan_detail_reporter_spec.rb +119 -0
  109. data/spec/scan_progress_printers_spec.rb +68 -0
  110. data/spec/scan_report_printers_spec.rb +67 -0
  111. data/spec/scan_runner_spec.rb +50 -0
  112. data/spec/scan_summary_reporter_spec.rb +61 -0
  113. data/spec/session_spec.rb +212 -0
  114. data/spec/spec.opts +1 -0
  115. data/spec/spec_helper.rb +295 -0
  116. data/spec/sync_helper_spec.rb +157 -0
  117. data/spec/sync_runner_spec.rb +78 -0
  118. data/spec/syncers_spec.rb +171 -0
  119. data/spec/table_scan_helper_spec.rb +29 -0
  120. data/spec/table_scan_spec.rb +49 -0
  121. data/spec/table_sorter_spec.rb +31 -0
  122. data/spec/table_spec_resolver_spec.rb +102 -0
  123. data/spec/table_sync_spec.rb +84 -0
  124. data/spec/trigger_mode_switcher_spec.rb +83 -0
  125. data/spec/two_way_replicator_spec.rb +551 -0
  126. data/spec/two_way_syncer_spec.rb +256 -0
  127. data/spec/type_casting_cursor_spec.rb +50 -0
  128. data/spec/uninstall_runner_spec.rb +86 -0
  129. data/tasks/database.rake +439 -0
  130. data/tasks/deployment.rake +29 -0
  131. data/tasks/environment.rake +9 -0
  132. data/tasks/java.rake +37 -0
  133. data/tasks/redmine_test.rake +47 -0
  134. data/tasks/rspec.rake +68 -0
  135. data/tasks/rubyrep.tailor +18 -0
  136. data/tasks/stats.rake +19 -0
  137. data/tasks/task_helper.rb +20 -0
  138. data.tar.gz.sig +0 -0
  139. metadata +243 -0
  140. 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