sequel_core 1.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 (57) hide show
  1. data/CHANGELOG +1003 -0
  2. data/COPYING +18 -0
  3. data/README +81 -0
  4. data/Rakefile +176 -0
  5. data/bin/sequel +41 -0
  6. data/lib/sequel_core.rb +59 -0
  7. data/lib/sequel_core/adapters/adapter_skeleton.rb +68 -0
  8. data/lib/sequel_core/adapters/ado.rb +100 -0
  9. data/lib/sequel_core/adapters/db2.rb +158 -0
  10. data/lib/sequel_core/adapters/dbi.rb +126 -0
  11. data/lib/sequel_core/adapters/informix.rb +87 -0
  12. data/lib/sequel_core/adapters/jdbc.rb +108 -0
  13. data/lib/sequel_core/adapters/mysql.rb +269 -0
  14. data/lib/sequel_core/adapters/odbc.rb +145 -0
  15. data/lib/sequel_core/adapters/odbc_mssql.rb +93 -0
  16. data/lib/sequel_core/adapters/openbase.rb +90 -0
  17. data/lib/sequel_core/adapters/oracle.rb +99 -0
  18. data/lib/sequel_core/adapters/postgres.rb +519 -0
  19. data/lib/sequel_core/adapters/sqlite.rb +192 -0
  20. data/lib/sequel_core/array_keys.rb +296 -0
  21. data/lib/sequel_core/connection_pool.rb +152 -0
  22. data/lib/sequel_core/core_ext.rb +59 -0
  23. data/lib/sequel_core/core_sql.rb +191 -0
  24. data/lib/sequel_core/database.rb +433 -0
  25. data/lib/sequel_core/dataset.rb +409 -0
  26. data/lib/sequel_core/dataset/convenience.rb +321 -0
  27. data/lib/sequel_core/dataset/sequelizer.rb +354 -0
  28. data/lib/sequel_core/dataset/sql.rb +586 -0
  29. data/lib/sequel_core/exceptions.rb +45 -0
  30. data/lib/sequel_core/migration.rb +191 -0
  31. data/lib/sequel_core/model.rb +8 -0
  32. data/lib/sequel_core/pretty_table.rb +73 -0
  33. data/lib/sequel_core/schema.rb +8 -0
  34. data/lib/sequel_core/schema/schema_generator.rb +131 -0
  35. data/lib/sequel_core/schema/schema_sql.rb +131 -0
  36. data/lib/sequel_core/worker.rb +58 -0
  37. data/spec/adapters/informix_spec.rb +139 -0
  38. data/spec/adapters/mysql_spec.rb +330 -0
  39. data/spec/adapters/oracle_spec.rb +130 -0
  40. data/spec/adapters/postgres_spec.rb +189 -0
  41. data/spec/adapters/sqlite_spec.rb +345 -0
  42. data/spec/array_keys_spec.rb +679 -0
  43. data/spec/connection_pool_spec.rb +356 -0
  44. data/spec/core_ext_spec.rb +67 -0
  45. data/spec/core_sql_spec.rb +301 -0
  46. data/spec/database_spec.rb +812 -0
  47. data/spec/dataset_spec.rb +2381 -0
  48. data/spec/migration_spec.rb +261 -0
  49. data/spec/pretty_table_spec.rb +66 -0
  50. data/spec/rcov.opts +4 -0
  51. data/spec/schema_generator_spec.rb +86 -0
  52. data/spec/schema_spec.rb +230 -0
  53. data/spec/sequelizer_spec.rb +448 -0
  54. data/spec/spec.opts +5 -0
  55. data/spec/spec_helper.rb +44 -0
  56. data/spec/worker_spec.rb +96 -0
  57. metadata +162 -0
@@ -0,0 +1,93 @@
1
+ if !Sequel.const_defined?('ODBC')
2
+ require File.join(File.dirname(__FILE__), 'odbc')
3
+ end
4
+
5
+ module Sequel
6
+ module ODBC
7
+ module MSSQL
8
+ class Database < ODBC::Database
9
+ set_adapter_scheme :odbc_mssql
10
+
11
+ def dataset(opts = nil)
12
+ MSSQL::Dataset.new(self, opts)
13
+ end
14
+ end
15
+
16
+ class Dataset < ODBC::Dataset
17
+ # Allows you to do .nolock on a query
18
+ def nolock
19
+ clone_merge(:with => "(NOLOCK)")
20
+ end
21
+
22
+ # Formats a SELECT statement using the given options and the dataset
23
+ # options.
24
+ def select_sql(opts = nil)
25
+ opts = opts ? @opts.merge(opts) : @opts
26
+
27
+ if sql = opts[:sql]
28
+ return sql
29
+ end
30
+
31
+ # ADD TOP to SELECT string for LIMITS
32
+ if limit = opts[:limit]
33
+ top = "TOP #{limit} "
34
+ raise Error, "Offset not supported" if opts[:offset]
35
+ end
36
+
37
+ columns = opts[:select]
38
+ select_columns = columns ? column_list(columns) : WILDCARD
39
+
40
+ if distinct = opts[:distinct]
41
+ distinct_clause = distinct.empty? ? "DISTINCT" : "DISTINCT ON (#{column_list(distinct)})"
42
+ sql = "SELECT #{top}#{distinct_clause} #{select_columns}"
43
+ else
44
+ sql = "SELECT #{top}#{select_columns}"
45
+ end
46
+
47
+ if opts[:from]
48
+ sql << " FROM #{source_list(opts[:from])}"
49
+ end
50
+
51
+ # ADD WITH to SELECT string for NOLOCK
52
+ if with = opts[:with]
53
+ sql << " WITH #{with}"
54
+ end
55
+
56
+ if join = opts[:join]
57
+ sql << join
58
+ end
59
+
60
+ if where = opts[:where]
61
+ sql << " WHERE #{where}"
62
+ end
63
+
64
+ if group = opts[:group]
65
+ sql << " GROUP BY #{column_list(group)}"
66
+ end
67
+
68
+ if order = opts[:order]
69
+ sql << " ORDER BY #{column_list(order)}"
70
+ end
71
+
72
+ if having = opts[:having]
73
+ sql << " HAVING #{having}"
74
+ end
75
+
76
+ if union = opts[:union]
77
+ sql << (opts[:union_all] ? \
78
+ " UNION ALL #{union.sql}" : " UNION #{union.sql}")
79
+ elsif intersect = opts[:intersect]
80
+ sql << (opts[:intersect_all] ? \
81
+ " INTERSECT ALL #{intersect.sql}" : " INTERSECT #{intersect.sql}")
82
+ elsif except = opts[:except]
83
+ sql << (opts[:except_all] ? \
84
+ " EXCEPT ALL #{except.sql}" : " EXCEPT #{except.sql}")
85
+ end
86
+
87
+ sql
88
+ end
89
+ alias_method :sql, :select_sql
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,90 @@
1
+ require 'openbase'
2
+
3
+ module Sequel
4
+ module OpenBase
5
+ class Database < Sequel::Database
6
+ set_adapter_scheme :openbase
7
+
8
+ def connect
9
+ OpenBase.new(
10
+ opts[:database],
11
+ opts[:host] || 'localhost',
12
+ opts[:user],
13
+ opts[:password]
14
+ )
15
+ end
16
+
17
+ def disconnect
18
+ # would this work?
19
+ @pool.disconnect {|c| c.disconnect}
20
+ end
21
+
22
+ def dataset(opts = nil)
23
+ OpenBase::Dataset.new(self, opts)
24
+ end
25
+
26
+ def execute(sql)
27
+ @logger.info(sql) if @logger
28
+ @pool.hold {|conn| conn.execute(sql)}
29
+ end
30
+
31
+ alias_method :do, :execute
32
+ end
33
+
34
+ class Dataset < Sequel::Dataset
35
+ def literal(v)
36
+ case v
37
+ when Time
38
+ literal(v.iso8601)
39
+ else
40
+ super
41
+ end
42
+ end
43
+
44
+ def fetch_rows(sql, &block)
45
+ @db.synchronize do
46
+ result = @db.execute sql
47
+ begin
48
+ @columns = result.column_infos.map {|c| c.name.to_sym}
49
+ result.each do |r|
50
+ row = {}
51
+ r.each_with_index {|v, i| row[@columns[i]] = v}
52
+ yield row
53
+ end
54
+ ensure
55
+ # result.close
56
+ end
57
+ end
58
+ self
59
+ end
60
+
61
+ def array_tuples_fetch_rows(sql, &block)
62
+ @db.synchronize do
63
+ result = @db.execute sql
64
+ begin
65
+ @columns = result.column_infos.map {|c| c.name.to_sym}
66
+ result.each do |r|
67
+ r.keys = @columns
68
+ yield r
69
+ end
70
+ ensure
71
+ # cursor.close
72
+ end
73
+ end
74
+ self
75
+ end
76
+
77
+ def insert(*values)
78
+ @db.do insert_sql(*values)
79
+ end
80
+
81
+ def update(*args, &block)
82
+ @db.do update_sql(*args, &block)
83
+ end
84
+
85
+ def delete(opts = nil)
86
+ @db.do delete_sql(opts)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,99 @@
1
+ require 'oci8'
2
+
3
+ module Sequel
4
+ module Oracle
5
+ class Database < Sequel::Database
6
+ set_adapter_scheme :oracle
7
+
8
+ # AUTO_INCREMENT = 'IDENTITY(1,1)'.freeze
9
+ #
10
+ # def auto_increment_sql
11
+ # AUTO_INCREMENT
12
+ # end
13
+
14
+ def connect
15
+ if @opts[:database]
16
+ dbname = @opts[:host] ? \
17
+ "//#{@opts[:host]}/#{@opts[:database]}" : @opts[:database]
18
+ else
19
+ dbname = @opts[:host]
20
+ end
21
+ conn = OCI8.new(@opts[:user], @opts[:password], dbname, @opts[:privilege])
22
+ conn.autocommit = true
23
+ conn.non_blocking = true
24
+ conn
25
+ end
26
+
27
+ def disconnect
28
+ @pool.disconnect {|c| c.logoff}
29
+ end
30
+
31
+ def dataset(opts = nil)
32
+ Oracle::Dataset.new(self, opts)
33
+ end
34
+
35
+ def execute(sql)
36
+ @logger.info(sql) if @logger
37
+ @pool.hold {|conn| conn.exec(sql)}
38
+ end
39
+
40
+ alias_method :do, :execute
41
+ end
42
+
43
+ class Dataset < Sequel::Dataset
44
+ def literal(v)
45
+ case v
46
+ when Time
47
+ literal(v.iso8601)
48
+ else
49
+ super
50
+ end
51
+ end
52
+
53
+ def fetch_rows(sql, &block)
54
+ @db.synchronize do
55
+ cursor = @db.execute sql
56
+ begin
57
+ @columns = cursor.get_col_names.map {|c| c.to_sym}
58
+ while r = cursor.fetch
59
+ row = {}
60
+ r.each_with_index {|v, i| row[@columns[i]] = v}
61
+ yield row
62
+ end
63
+ ensure
64
+ cursor.close
65
+ end
66
+ end
67
+ self
68
+ end
69
+
70
+ def array_tuples_fetch_rows(sql, &block)
71
+ @db.synchronize do
72
+ cursor = @db.execute sql
73
+ begin
74
+ @columns = cursor.get_col_names.map {|c| c.to_sym}
75
+ while r = cursor.fetch
76
+ r.keys = columns
77
+ yield r
78
+ end
79
+ ensure
80
+ cursor.close
81
+ end
82
+ end
83
+ self
84
+ end
85
+
86
+ def insert(*values)
87
+ @db.do insert_sql(*values)
88
+ end
89
+
90
+ def update(*args, &block)
91
+ @db.do update_sql(*args, &block)
92
+ end
93
+
94
+ def delete(opts = nil)
95
+ @db.do delete_sql(opts)
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,519 @@
1
+ require 'postgres'
2
+
3
+ class PGconn
4
+ # the pure-ruby postgres adapter does not have a quote method.
5
+ TRUE = 't'.freeze
6
+ FALSE = 'f'.freeze
7
+ NULL = 'NULL'.freeze
8
+
9
+ unless methods.include?('quote')
10
+ def self.quote(obj)
11
+ case obj
12
+ when true
13
+ TRUE
14
+ when false
15
+ FALSE
16
+ when nil
17
+ NULL
18
+ when String
19
+ "'#{obj}'"
20
+ else
21
+ obj.to_s
22
+ end
23
+ end
24
+ end
25
+
26
+ class << self
27
+ # The postgres gem's string quoting doesn't render string literals properly, which this fixes.
28
+ #
29
+ # "a basic string" #=> 'a basic string'
30
+ # "this\or that" #=> E'this\\or that'
31
+ #
32
+ # See <http://www.postgresql.org/docs/8.2/static/sql-syntax-lexical.html> for details.
33
+ def quote_with_proper_escaping(s)
34
+ value = quote_without_proper_escaping(s)
35
+ value = "E#{value}" if value =~ /\\/
36
+ return value
37
+ end
38
+ alias_method :quote_without_proper_escaping, :quote
39
+ alias_method :quote, :quote_with_proper_escaping
40
+ end
41
+
42
+ def connected?
43
+ status == PGconn::CONNECTION_OK
44
+ end
45
+
46
+ def execute(sql)
47
+ begin
48
+ async_exec(sql)
49
+ rescue PGError => e
50
+ unless connected?
51
+ reset
52
+ async_exec(sql)
53
+ else
54
+ raise e
55
+ end
56
+ end
57
+ end
58
+
59
+ attr_accessor :transaction_in_progress
60
+
61
+ SELECT_CURRVAL = "SELECT currval('%s')".freeze
62
+
63
+ def last_insert_id(table)
64
+ @table_sequences ||= {}
65
+ if !@table_sequences.include?(table)
66
+ pkey_and_seq = pkey_and_sequence(table)
67
+ if pkey_and_seq
68
+ @table_sequences[table] = pkey_and_seq[1]
69
+ end
70
+ end
71
+ if seq = @table_sequences[table]
72
+ r = async_query(SELECT_CURRVAL % seq)
73
+ return r[0][0].to_i unless r.nil? || r.empty?
74
+ end
75
+ nil # primary key sequence not found
76
+ end
77
+
78
+ # Shamelessly appropriated from ActiveRecord's Postgresql adapter.
79
+
80
+ SELECT_PK_AND_SERIAL_SEQUENCE = <<-end_sql
81
+ SELECT attr.attname, name.nspname, seq.relname
82
+ FROM pg_class seq, pg_attribute attr, pg_depend dep,
83
+ pg_namespace name, pg_constraint cons
84
+ WHERE seq.oid = dep.objid
85
+ AND seq.relnamespace = name.oid
86
+ AND seq.relkind = 'S'
87
+ AND attr.attrelid = dep.refobjid
88
+ AND attr.attnum = dep.refobjsubid
89
+ AND attr.attrelid = cons.conrelid
90
+ AND attr.attnum = cons.conkey[1]
91
+ AND cons.contype = 'p'
92
+ AND dep.refobjid = '%s'::regclass
93
+ end_sql
94
+
95
+ SELECT_PK_AND_CUSTOM_SEQUENCE = <<-end_sql
96
+ SELECT attr.attname, name.nspname, split_part(def.adsrc, '''', 2)
97
+ FROM pg_class t
98
+ JOIN pg_namespace name ON (t.relnamespace = name.oid)
99
+ JOIN pg_attribute attr ON (t.oid = attrelid)
100
+ JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum)
101
+ JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
102
+ WHERE t.oid = '%s'::regclass
103
+ AND cons.contype = 'p'
104
+ AND def.adsrc ~* 'nextval'
105
+ end_sql
106
+
107
+ SELECT_PK = <<-end_sql
108
+ SELECT pg_attribute.attname
109
+ FROM pg_class, pg_attribute, pg_index
110
+ WHERE pg_class.oid = pg_attribute.attrelid AND
111
+ pg_class.oid = pg_index.indrelid AND
112
+ pg_index.indkey[0] = pg_attribute.attnum AND
113
+ pg_index.indisprimary = 't' AND
114
+ pg_class.relname = '%s'
115
+ end_sql
116
+
117
+ def pkey_and_sequence(table)
118
+ r = async_query(SELECT_PK_AND_SERIAL_SEQUENCE % table)
119
+ return [r[0].first, r[0].last] unless r.nil? or r.empty?
120
+
121
+ r = async_query(SELECT_PK_AND_CUSTOM_SEQUENCE % table)
122
+ return [r[0].first, r[0].last] unless r.nil? or r.empty?
123
+ rescue
124
+ nil
125
+ end
126
+
127
+ def primary_key(table)
128
+ r = async_query(SELECT_PK % table)
129
+ pkey = r[0].first unless r.nil? or r.empty?
130
+ return pkey.to_sym if pkey
131
+ rescue
132
+ nil
133
+ end
134
+ end
135
+
136
+ class String
137
+ POSTGRES_BOOL_TRUE = 't'.freeze
138
+ POSTGRES_BOOL_FALSE = 'f'.freeze
139
+
140
+ def postgres_to_bool
141
+ if self == POSTGRES_BOOL_TRUE
142
+ true
143
+ elsif self == POSTGRES_BOOL_FALSE
144
+ false
145
+ else
146
+ nil
147
+ end
148
+ end
149
+ end
150
+
151
+ module Sequel
152
+ module Postgres
153
+ PG_TYPES = {
154
+ 16 => :postgres_to_bool,
155
+ 20 => :to_i,
156
+ 21 => :to_i,
157
+ 22 => :to_i,
158
+ 23 => :to_i,
159
+ 700 => :to_f,
160
+ 701 => :to_f,
161
+ 1114 => :to_time
162
+ }
163
+
164
+ if PGconn.respond_to?(:translate_results=)
165
+ PGconn.translate_results = true
166
+ AUTO_TRANSLATE = true
167
+ else
168
+ AUTO_TRANSLATE = false
169
+ end
170
+
171
+ class Database < Sequel::Database
172
+ set_adapter_scheme :postgres
173
+
174
+ def connect
175
+ conn = PGconn.connect(
176
+ @opts[:host] || 'localhost',
177
+ @opts[:port] || 5432,
178
+ '', '',
179
+ @opts[:database],
180
+ @opts[:user],
181
+ @opts[:password]
182
+ )
183
+ if encoding = @opts[:encoding] || @opts[:charset]
184
+ conn.set_client_encoding(encoding)
185
+ end
186
+ conn
187
+ end
188
+
189
+ def disconnect
190
+ @pool.disconnect {|c| c.close}
191
+ end
192
+
193
+ def dataset(opts = nil)
194
+ Postgres::Dataset.new(self, opts)
195
+ end
196
+
197
+ RELATION_QUERY = {:from => [:pg_class], :select => [:relname]}.freeze
198
+ RELATION_FILTER = "(relkind = 'r') AND (relname !~ '^pg|sql')".freeze
199
+ SYSTEM_TABLE_REGEXP = /^pg|sql/.freeze
200
+
201
+ def tables
202
+ dataset(RELATION_QUERY).filter(RELATION_FILTER).map {|r| r[:relname].to_sym}
203
+ end
204
+
205
+ def locks
206
+ dataset.from("pg_class, pg_locks").
207
+ select("pg_class.relname, pg_locks.*").
208
+ filter("pg_class.relfilenode=pg_locks.relation")
209
+ end
210
+
211
+ def execute(sql)
212
+ @logger.info(sql) if @logger
213
+ @pool.hold {|conn| conn.execute(sql)}
214
+ rescue => e
215
+ @logger.error(e.message) if @logger
216
+ raise e
217
+ end
218
+
219
+ def execute_and_forget(sql)
220
+ @logger.info(sql) if @logger
221
+ @pool.hold {|conn| conn.execute(sql).clear}
222
+ rescue => e
223
+ @logger.error(e.message) if @logger
224
+ raise e
225
+ end
226
+
227
+ def primary_key_for_table(conn, table)
228
+ @primary_keys ||= {}
229
+ @primary_keys[table] ||= conn.primary_key(table)
230
+ end
231
+
232
+ RE_CURRVAL_ERROR = /currval of sequence "(.*)" is not yet defined in this session/.freeze
233
+
234
+ def insert_result(conn, table, values)
235
+ begin
236
+ result = conn.last_insert_id(table)
237
+ return result if result
238
+ rescue PGError => e
239
+ # An error could occur if the inserted values include a primary key
240
+ # value, while the primary key is serial.
241
+ if e.message =~ RE_CURRVAL_ERROR
242
+ raise Error, "Could not return primary key value for the inserted record. Are you specifying a primary key value for a serial primary key?"
243
+ else
244
+ raise e
245
+ end
246
+ end
247
+
248
+ case values
249
+ when Hash
250
+ values[primary_key_for_table(conn, table)]
251
+ when Array
252
+ values.first
253
+ else
254
+ nil
255
+ end
256
+ end
257
+
258
+ def execute_insert(sql, table, values)
259
+ @logger.info(sql) if @logger
260
+ @pool.hold do |conn|
261
+ conn.execute(sql).clear
262
+ insert_result(conn, table, values)
263
+ end
264
+ rescue => e
265
+ @logger.error(e.message) if @logger
266
+ raise e
267
+ end
268
+
269
+ def synchronize(&block)
270
+ @pool.hold(&block)
271
+ end
272
+
273
+ SQL_BEGIN = 'BEGIN'.freeze
274
+ SQL_COMMIT = 'COMMIT'.freeze
275
+ SQL_ROLLBACK = 'ROLLBACK'.freeze
276
+
277
+ def transaction
278
+ @pool.hold do |conn|
279
+ if conn.transaction_in_progress
280
+ yield conn
281
+ else
282
+ @logger.info(SQL_BEGIN) if @logger
283
+ conn.async_exec(SQL_BEGIN)
284
+ begin
285
+ conn.transaction_in_progress = true
286
+ result = yield
287
+ begin
288
+ @logger.info(SQL_COMMIT) if @logger
289
+ conn.async_exec(SQL_COMMIT)
290
+ rescue => e
291
+ @logger.error(e.message) if @logger
292
+ raise e
293
+ end
294
+ result
295
+ rescue => e
296
+ @logger.info(SQL_ROLLBACK) if @logger
297
+ conn.async_exec(SQL_ROLLBACK) rescue nil
298
+ raise e unless Error::Rollback === e
299
+ ensure
300
+ conn.transaction_in_progress = nil
301
+ end
302
+ end
303
+ end
304
+ end
305
+
306
+ def serial_primary_key_options
307
+ {:primary_key => true, :type => :serial}
308
+ end
309
+
310
+ def drop_table_sql(name)
311
+ "DROP TABLE #{name} CASCADE"
312
+ end
313
+ end
314
+
315
+ class Dataset < Sequel::Dataset
316
+ def literal(v)
317
+ case v
318
+ when LiteralString
319
+ v
320
+ when String, Fixnum, Float, TrueClass, FalseClass
321
+ PGconn.quote(v)
322
+ else
323
+ super
324
+ end
325
+ end
326
+
327
+ def match_expr(l, r)
328
+ case r
329
+ when Regexp
330
+ r.casefold? ? \
331
+ "(#{literal(l)} ~* #{literal(r.source)})" :
332
+ "(#{literal(l)} ~ #{literal(r.source)})"
333
+ else
334
+ super
335
+ end
336
+ end
337
+
338
+ FOR_UPDATE = ' FOR UPDATE'.freeze
339
+ FOR_SHARE = ' FOR SHARE'.freeze
340
+
341
+ def select_sql(opts = nil)
342
+ row_lock_mode = opts ? opts[:lock] : @opts[:lock]
343
+ sql = super
344
+ case row_lock_mode
345
+ when :update
346
+ sql << FOR_UPDATE
347
+ when :share
348
+ sql << FOR_SHARE
349
+ end
350
+ sql
351
+ end
352
+
353
+ def for_update
354
+ clone_merge(:lock => :update)
355
+ end
356
+
357
+ def for_share
358
+ clone_merge(:lock => :share)
359
+ end
360
+
361
+ EXPLAIN = 'EXPLAIN '.freeze
362
+ EXPLAIN_ANALYZE = 'EXPLAIN ANALYZE '.freeze
363
+ QUERY_PLAN = 'QUERY PLAN'.to_sym
364
+
365
+ def explain(opts = nil)
366
+ analysis = []
367
+ fetch_rows(EXPLAIN + select_sql(opts)) do |r|
368
+ analysis << r[QUERY_PLAN]
369
+ end
370
+ analysis.join("\r\n")
371
+ end
372
+
373
+ def analyze(opts = nil)
374
+ analysis = []
375
+ fetch_rows(EXPLAIN_ANALYZE + select_sql(opts)) do |r|
376
+ analysis << r[QUERY_PLAN]
377
+ end
378
+ analysis.join("\r\n")
379
+ end
380
+
381
+ LOCK = 'LOCK TABLE %s IN %s MODE'.freeze
382
+
383
+ ACCESS_SHARE = 'ACCESS SHARE'.freeze
384
+ ROW_SHARE = 'ROW SHARE'.freeze
385
+ ROW_EXCLUSIVE = 'ROW EXCLUSIVE'.freeze
386
+ SHARE_UPDATE_EXCLUSIVE = 'SHARE UPDATE EXCLUSIVE'.freeze
387
+ SHARE = 'SHARE'.freeze
388
+ SHARE_ROW_EXCLUSIVE = 'SHARE ROW EXCLUSIVE'.freeze
389
+ EXCLUSIVE = 'EXCLUSIVE'.freeze
390
+ ACCESS_EXCLUSIVE = 'ACCESS EXCLUSIVE'.freeze
391
+
392
+ # Locks the table with the specified mode.
393
+ def lock(mode, &block)
394
+ sql = LOCK % [@opts[:from], mode]
395
+ @db.synchronize do
396
+ if block # perform locking inside a transaction and yield to block
397
+ @db.transaction {@db.execute_and_forget(sql); yield}
398
+ else
399
+ @db.execute_and_forget(sql) # lock without a transaction
400
+ self
401
+ end
402
+ end
403
+ end
404
+
405
+ def insert(*values)
406
+ @db.execute_insert(insert_sql(*values), @opts[:from],
407
+ values.size == 1 ? values.first : values)
408
+ end
409
+
410
+ def update(*args, &block)
411
+ @db.synchronize do
412
+ result = @db.execute(update_sql(*args, &block))
413
+ begin
414
+ affected = result.cmdtuples
415
+ ensure
416
+ result.clear
417
+ end
418
+ affected
419
+ end
420
+ end
421
+
422
+ def delete(opts = nil)
423
+ @db.synchronize do
424
+ result = @db.execute(delete_sql(opts))
425
+ begin
426
+ affected = result.cmdtuples
427
+ ensure
428
+ result.clear
429
+ end
430
+ affected
431
+ end
432
+ end
433
+
434
+ def fetch_rows(sql, &block)
435
+ @db.synchronize do
436
+ result = @db.execute(sql)
437
+ begin
438
+ conv = row_converter(result)
439
+ result.each {|r| yield conv[r]}
440
+ ensure
441
+ result.clear
442
+ end
443
+ end
444
+ end
445
+
446
+ @@converters_mutex = Mutex.new
447
+ @@converters = {}
448
+
449
+ def row_converter(result)
450
+ @columns = []; translators = []
451
+ result.fields.each_with_index do |f, idx|
452
+ @columns << f.to_sym
453
+ translators << PG_TYPES[result.type(idx)]
454
+ end
455
+
456
+ # create result signature and memoize the converter
457
+ sig = [@columns, translators].hash
458
+ @@converters_mutex.synchronize do
459
+ @@converters[sig] ||= compile_converter(@columns, translators)
460
+ end
461
+ end
462
+
463
+ def compile_converter(columns, translators)
464
+ used_columns = []
465
+ kvs = []
466
+ columns.each_with_index do |column, idx|
467
+ next if used_columns.include?(column)
468
+ used_columns << column
469
+
470
+ if !AUTO_TRANSLATE and translator = translators[idx]
471
+ kvs << ":\"#{column}\" => ((t = r[#{idx}]) ? t.#{translator} : nil)"
472
+ else
473
+ kvs << ":\"#{column}\" => r[#{idx}]"
474
+ end
475
+ end
476
+ eval("lambda {|r| {#{kvs.join(COMMA_SEPARATOR)}}}")
477
+ end
478
+
479
+ def array_tuples_fetch_rows(sql, &block)
480
+ @db.synchronize do
481
+ result = @db.execute(sql)
482
+ begin
483
+ conv = array_tuples_row_converter(result)
484
+ result.each {|r| yield conv[r]}
485
+ ensure
486
+ result.clear
487
+ end
488
+ end
489
+ end
490
+
491
+ @@array_tuples_converters_mutex = Mutex.new
492
+ @@array_tuples_converters = {}
493
+
494
+ def array_tuples_row_converter(result)
495
+ @columns = []; translators = []
496
+ result.fields.each_with_index do |f, idx|
497
+ @columns << f.to_sym
498
+ translators << PG_TYPES[result.type(idx)]
499
+ end
500
+
501
+ # create result signature and memoize the converter
502
+ sig = [@columns, translators].hash
503
+ @@array_tuples_converters_mutex.synchronize do
504
+ @@array_tuples_converters[sig] ||= array_tuples_compile_converter(@columns, translators)
505
+ end
506
+ end
507
+
508
+ def array_tuples_compile_converter(columns, translators)
509
+ tr = []
510
+ columns.each_with_index do |column, idx|
511
+ if !AUTO_TRANSLATE and t = translators[idx]
512
+ tr << "if (v = r[#{idx}]); r[#{idx}] = v.#{t}; end"
513
+ end
514
+ end
515
+ eval("lambda {|r| r.keys = columns; #{tr.join(';')}; r}")
516
+ end
517
+ end
518
+ end
519
+ end