sequel_core 1.0

Sign up to get free protection for your applications and to get access to all the features.
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