activerecord-nuodb-adapter 1.0.0.rc.1
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/.buildpath +5 -0
- data/.gitignore +3 -0
- data/AUTHORS +4 -0
- data/COPYING +25 -0
- data/Gemfile +9 -0
- data/History.txt +4 -0
- data/LICENSE +26 -0
- data/Manifest.txt +11 -0
- data/README.rdoc +69 -0
- data/Rakefile +134 -0
- data/activerecord-nuodb-adapter.gemspec +25 -0
- data/lib/active_record/connection_adapters/nuodb/version.rb +35 -0
- data/lib/active_record/connection_adapters/nuodb_adapter.rb +778 -0
- data/lib/activerecord-nuodb-adapter.rb +29 -0
- data/lib/arel/visitors/nuodb.rb +55 -0
- data/test/test_simple.rb +181 -0
- metadata +130 -0
@@ -0,0 +1,778 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2012, NuoDB, Inc.
|
3
|
+
# All rights reserved.
|
4
|
+
#
|
5
|
+
# Redistribution and use in source and binary forms, with or without
|
6
|
+
# modification, are permitted provided that the following conditions are met:
|
7
|
+
#
|
8
|
+
# * Redistributions of source code must retain the above copyright
|
9
|
+
# notice, this list of conditions and the following disclaimer.
|
10
|
+
# * Redistributions in binary form must reproduce the above copyright
|
11
|
+
# notice, this list of conditions and the following disclaimer in the
|
12
|
+
# documentation and/or other materials provided with the distribution.
|
13
|
+
# * Neither the name of NuoDB, Inc. nor the names of its contributors may
|
14
|
+
# be used to endorse or promote products derived from this software
|
15
|
+
# without specific prior written permission.
|
16
|
+
#
|
17
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
18
|
+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
19
|
+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
20
|
+
# DISCLAIMED. IN NO EVENT SHALL NUODB, INC. BE LIABLE FOR ANY DIRECT, INDIRECT,
|
21
|
+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
22
|
+
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
23
|
+
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
24
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
25
|
+
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
26
|
+
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
27
|
+
#
|
28
|
+
|
29
|
+
require 'arel/visitors/nuodb'
|
30
|
+
require 'active_record'
|
31
|
+
require 'active_record/base'
|
32
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
33
|
+
require 'active_record/connection_adapters/statement_pool'
|
34
|
+
require 'active_record/connection_adapters/nuodb/version'
|
35
|
+
require 'arel/visitors/bind_visitor'
|
36
|
+
require 'active_support/core_ext/hash/keys'
|
37
|
+
|
38
|
+
require 'nuodb'
|
39
|
+
|
40
|
+
module ActiveRecord
|
41
|
+
|
42
|
+
class Base
|
43
|
+
|
44
|
+
def self.nuodb_connection(config) #:nodoc:
|
45
|
+
config.symbolize_keys!
|
46
|
+
unless config[:database]
|
47
|
+
raise ArgumentError, "No database file specified. Missing argument: database"
|
48
|
+
end
|
49
|
+
# supply configuration defaults
|
50
|
+
config.reverse_merge! :host => 'localhost'
|
51
|
+
ConnectionAdapters::NuoDBAdapter.new nil, logger, nil, config
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
class LostConnection < WrappedDatabaseException
|
57
|
+
end
|
58
|
+
|
59
|
+
module ConnectionAdapters
|
60
|
+
|
61
|
+
class NuoDBColumn < Column
|
62
|
+
|
63
|
+
def initialize(name, default, sql_type = nil, null = true, options = {})
|
64
|
+
@options = options.symbolize_keys
|
65
|
+
super(name, default, sql_type, null)
|
66
|
+
@primary = @options[:is_identity] || @options[:is_primary]
|
67
|
+
end
|
68
|
+
|
69
|
+
class << self
|
70
|
+
|
71
|
+
def string_to_binary(value)
|
72
|
+
"0x#{value.unpack("H*")[0]}"
|
73
|
+
end
|
74
|
+
|
75
|
+
def binary_to_string(value)
|
76
|
+
value =~ /[^[:xdigit:]]/ ? value : [value].pack('H*')
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
def is_identity?
|
82
|
+
@options[:is_identity]
|
83
|
+
end
|
84
|
+
|
85
|
+
def is_primary?
|
86
|
+
@options[:is_primary]
|
87
|
+
end
|
88
|
+
|
89
|
+
def is_utf8?
|
90
|
+
!!(@sql_type =~ /nvarchar|ntext|nchar/i)
|
91
|
+
end
|
92
|
+
|
93
|
+
def is_integer?
|
94
|
+
!!(@sql_type =~ /int/i)
|
95
|
+
end
|
96
|
+
|
97
|
+
def is_real?
|
98
|
+
!!(@sql_type =~ /real/i)
|
99
|
+
end
|
100
|
+
|
101
|
+
def sql_type_for_statement
|
102
|
+
if is_integer? || is_real?
|
103
|
+
sql_type.sub(/\((\d+)?\)/, '')
|
104
|
+
else
|
105
|
+
sql_type
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def default_function
|
110
|
+
@options[:default_function]
|
111
|
+
end
|
112
|
+
|
113
|
+
def table_name
|
114
|
+
@options[:table_name]
|
115
|
+
end
|
116
|
+
|
117
|
+
def table_klass
|
118
|
+
@table_klass ||= begin
|
119
|
+
table_name.classify.constantize
|
120
|
+
rescue StandardError, NameError, LoadError
|
121
|
+
nil
|
122
|
+
end
|
123
|
+
(@table_klass && @table_klass < ActiveRecord::Base) ? @table_klass : nil
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def extract_limit(sql_type)
|
129
|
+
case sql_type
|
130
|
+
when /^smallint/i
|
131
|
+
2
|
132
|
+
when /^int/i
|
133
|
+
4
|
134
|
+
when /^bigint/i
|
135
|
+
8
|
136
|
+
when /\(max\)/, /decimal/, /numeric/
|
137
|
+
nil
|
138
|
+
else
|
139
|
+
super
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def simplified_type(field_type)
|
144
|
+
case field_type
|
145
|
+
when /real/i then
|
146
|
+
:float
|
147
|
+
when /money/i then
|
148
|
+
:decimal
|
149
|
+
when /image/i then
|
150
|
+
:binary
|
151
|
+
when /bit/i then
|
152
|
+
:boolean
|
153
|
+
when /uniqueidentifier/i then
|
154
|
+
:string
|
155
|
+
when /datetime/i then
|
156
|
+
:datetime
|
157
|
+
when /varchar\(max\)/ then
|
158
|
+
:text
|
159
|
+
when /timestamp/ then
|
160
|
+
:binary
|
161
|
+
else
|
162
|
+
super
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
end #class NuoDBColumn
|
167
|
+
|
168
|
+
class NuoDBAdapter < AbstractAdapter
|
169
|
+
|
170
|
+
class StatementPool < ConnectionAdapters::StatementPool
|
171
|
+
|
172
|
+
attr_reader :max, :connection
|
173
|
+
|
174
|
+
def initialize(connection, max)
|
175
|
+
super
|
176
|
+
@cache = Hash.new { |h, pid| h[pid] = {} }
|
177
|
+
end
|
178
|
+
|
179
|
+
def each(&block)
|
180
|
+
cache.each(&block)
|
181
|
+
end
|
182
|
+
|
183
|
+
def key?(key)
|
184
|
+
cache.key?(key)
|
185
|
+
end
|
186
|
+
|
187
|
+
def [](key)
|
188
|
+
cache[key]
|
189
|
+
end
|
190
|
+
|
191
|
+
def []=(sql, key)
|
192
|
+
while max <= cache.size
|
193
|
+
dealloc cache.shift.last[:stmt]
|
194
|
+
end
|
195
|
+
cache[sql] = key
|
196
|
+
end
|
197
|
+
|
198
|
+
def length
|
199
|
+
cache.length
|
200
|
+
end
|
201
|
+
|
202
|
+
def delete(key)
|
203
|
+
dealloc cache[key][:stmt]
|
204
|
+
cache.delete(key)
|
205
|
+
end
|
206
|
+
|
207
|
+
def clear
|
208
|
+
cache.each_value do |hash|
|
209
|
+
dealloc hash[:stmt]
|
210
|
+
end
|
211
|
+
cache.clear
|
212
|
+
end
|
213
|
+
|
214
|
+
private
|
215
|
+
|
216
|
+
def cache
|
217
|
+
@cache[Process.pid]
|
218
|
+
end
|
219
|
+
|
220
|
+
def dealloc(stmt)
|
221
|
+
stmt.close if connection.ping
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def process_id
|
226
|
+
Process.pid
|
227
|
+
end
|
228
|
+
|
229
|
+
attr_accessor :config, :statements
|
230
|
+
|
231
|
+
class BindSubstitution < Arel::Visitors::NuoDB
|
232
|
+
include Arel::Visitors::BindVisitor
|
233
|
+
end
|
234
|
+
|
235
|
+
def initialize(connection, logger, pool, config)
|
236
|
+
super(connection, logger, pool)
|
237
|
+
@visitor = Arel::Visitors::NuoDB.new self
|
238
|
+
@config = config.clone
|
239
|
+
# prefer to run with prepared statements unless otherwise specified
|
240
|
+
if @config.fetch(:prepared_statements) { true }
|
241
|
+
@visitor = Arel::Visitors::NuoDB.new self
|
242
|
+
else
|
243
|
+
@visitor = BindSubstitution.new self
|
244
|
+
end
|
245
|
+
connect!
|
246
|
+
end
|
247
|
+
|
248
|
+
# ABSTRACT ADAPTER #######################################
|
249
|
+
|
250
|
+
# ADAPTER NAME ===========================================
|
251
|
+
|
252
|
+
def adapter_name
|
253
|
+
'NuoDB'
|
254
|
+
end
|
255
|
+
|
256
|
+
# FEATURES ===============================================
|
257
|
+
|
258
|
+
def supports_migrations?
|
259
|
+
true
|
260
|
+
end
|
261
|
+
|
262
|
+
def supports_primary_key?
|
263
|
+
true
|
264
|
+
end
|
265
|
+
|
266
|
+
def supports_count_distinct?
|
267
|
+
true
|
268
|
+
end
|
269
|
+
|
270
|
+
def supports_ddl_transactions?
|
271
|
+
true
|
272
|
+
end
|
273
|
+
|
274
|
+
def supports_bulk_alter?
|
275
|
+
false
|
276
|
+
end
|
277
|
+
|
278
|
+
def supports_savepoints?
|
279
|
+
true
|
280
|
+
end
|
281
|
+
|
282
|
+
def supports_index_sort_order?
|
283
|
+
true
|
284
|
+
end
|
285
|
+
|
286
|
+
def supports_partial_index?
|
287
|
+
false
|
288
|
+
end
|
289
|
+
|
290
|
+
def supports_explain?
|
291
|
+
false
|
292
|
+
end
|
293
|
+
|
294
|
+
# CONNECTION MANAGEMENT ==================================
|
295
|
+
|
296
|
+
def reconnect!
|
297
|
+
disconnect!
|
298
|
+
connect!
|
299
|
+
super
|
300
|
+
end
|
301
|
+
|
302
|
+
def connect!
|
303
|
+
# todo add native method for new and initialize where init takes a hash
|
304
|
+
@connection = ::NuoDB::Connection.createSqlConnection(
|
305
|
+
"#{config[:database]}@#{config[:host]}", config[:schema],
|
306
|
+
config[:username], config[:password])
|
307
|
+
@statements = StatementPool.new(@connection, @config.fetch(:statement_limit) { 1000 })
|
308
|
+
@quoted_column_names, @quoted_table_names = {}, {}
|
309
|
+
end
|
310
|
+
|
311
|
+
def disconnect!
|
312
|
+
super
|
313
|
+
clear_cache!
|
314
|
+
raw_connection.disconnect rescue nil
|
315
|
+
end
|
316
|
+
|
317
|
+
def reset!
|
318
|
+
reconnect!
|
319
|
+
end
|
320
|
+
|
321
|
+
def clear_cache!
|
322
|
+
@statements.clear
|
323
|
+
@statements = nil
|
324
|
+
end
|
325
|
+
|
326
|
+
# SAVEPOINT SUPPORT ======================================
|
327
|
+
|
328
|
+
def create_savepoint
|
329
|
+
execute("SAVEPOINT #{current_savepoint_name}")
|
330
|
+
end
|
331
|
+
|
332
|
+
def rollback_to_savepoint
|
333
|
+
execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
|
334
|
+
end
|
335
|
+
|
336
|
+
def release_savepoint
|
337
|
+
execute("RELEASE SAVEPOINT #{current_savepoint_name}")
|
338
|
+
end
|
339
|
+
|
340
|
+
# EXCEPTION TRANSLATION ==================================
|
341
|
+
|
342
|
+
protected
|
343
|
+
|
344
|
+
LOST_CONNECTION_MESSAGES = [/remote connection closed/i].freeze
|
345
|
+
|
346
|
+
def lost_connection_messages
|
347
|
+
LOST_CONNECTION_MESSAGES
|
348
|
+
end
|
349
|
+
|
350
|
+
CONNECTION_NOT_ESTABLISHED_MESSAGES = [/can't find broker for database/i, /no .* nodes are available for database/i]
|
351
|
+
|
352
|
+
def connection_not_established_messages
|
353
|
+
CONNECTION_NOT_ESTABLISHED_MESSAGES
|
354
|
+
end
|
355
|
+
|
356
|
+
def translate_exception(exception, message)
|
357
|
+
case message
|
358
|
+
when /duplicate value in unique index/i
|
359
|
+
RecordNotUnique.new(message, exception)
|
360
|
+
when /too few values specified in the value list/i
|
361
|
+
# defaults to StatementInvalid, so we are okay, but just to be explicit...
|
362
|
+
super
|
363
|
+
when *lost_connection_messages
|
364
|
+
LostConnection.new(message, exception)
|
365
|
+
when *connection_not_established_messages
|
366
|
+
ConnectionNotEstablished.new(message)
|
367
|
+
else
|
368
|
+
super
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
# SCHEMA DEFINITIONS #####################################
|
373
|
+
|
374
|
+
public
|
375
|
+
|
376
|
+
def primary_key(table_name)
|
377
|
+
# n.b. active record does not support composite primary keys!
|
378
|
+
row = exec_query(<<-eosql, 'SCHEMA', [config[:schema], table_name.to_s]).rows.first
|
379
|
+
SELECT
|
380
|
+
indexfields.field as field_name
|
381
|
+
FROM
|
382
|
+
system.indexfields AS indexfields
|
383
|
+
WHERE
|
384
|
+
indexfields.schema = ? AND
|
385
|
+
indexfields.tablename = ?
|
386
|
+
eosql
|
387
|
+
row && row.first
|
388
|
+
end
|
389
|
+
|
390
|
+
def version
|
391
|
+
self.class::VERSION
|
392
|
+
end
|
393
|
+
|
394
|
+
# SCHEMA STATEMENTS ######################################
|
395
|
+
|
396
|
+
public
|
397
|
+
|
398
|
+
def table_exists?(table_name)
|
399
|
+
return false unless table_name
|
400
|
+
|
401
|
+
table_name = table_name.to_s.downcase
|
402
|
+
schema, table = table_name.split('.', 2)
|
403
|
+
|
404
|
+
unless table
|
405
|
+
table = schema
|
406
|
+
schema = nil
|
407
|
+
end
|
408
|
+
|
409
|
+
tables('SCHEMA', schema).include? table
|
410
|
+
end
|
411
|
+
|
412
|
+
def tables(name = 'SCHEMA', schema = nil)
|
413
|
+
result = exec_query(<<-eosql, name, [schema || config[:schema]])
|
414
|
+
SELECT
|
415
|
+
tablename
|
416
|
+
FROM
|
417
|
+
system.tables
|
418
|
+
WHERE
|
419
|
+
schema = ?
|
420
|
+
eosql
|
421
|
+
result.inject([]) do |tables, row|
|
422
|
+
row.symbolize_keys!
|
423
|
+
tables << row[:tablename].downcase
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
# Returns an array of indexes for the given table. Skip primary keys.
|
428
|
+
def indexes(table_name, name = nil)
|
429
|
+
|
430
|
+
# the following query returns something like this:
|
431
|
+
#
|
432
|
+
# INDEXNAME TABLENAME NON_UNIQUE FIELD LENGTH
|
433
|
+
# ---------------------- --------- ---------- --------- ------
|
434
|
+
# COMPANIES..PRIMARY_KEY COMPANIES 0 ID 4
|
435
|
+
# COMPANY_INDEX COMPANIES 1 FIRM_ID 4
|
436
|
+
# COMPANY_INDEX COMPANIES 1 TYPE 255
|
437
|
+
# COMPANY_INDEX COMPANIES 1 RATING 4
|
438
|
+
# COMPANY_INDEX COMPANIES 1 RUBY_TYPE 255
|
439
|
+
|
440
|
+
result = exec_query(<<-eosql, 'SCHEMA', [config[:schema], table_name.to_s])
|
441
|
+
SELECT
|
442
|
+
indexes.indexname as index_name,
|
443
|
+
indexes.tablename as table_name,
|
444
|
+
CASE indexes.indextype WHEN 2 THEN 1 ELSE 0 END AS non_unique,
|
445
|
+
indexfields.field as field_name,
|
446
|
+
fields.length as field_length
|
447
|
+
FROM
|
448
|
+
system.indexes AS indexes, system.indexfields AS indexfields, system.fields AS fields
|
449
|
+
WHERE
|
450
|
+
indexes.schema = ? AND
|
451
|
+
indexes.tablename = ? AND
|
452
|
+
indexes.indexname = indexfields.indexname AND
|
453
|
+
indexfields.field = fields.field AND
|
454
|
+
indexfields.schema = fields.schema AND
|
455
|
+
indexfields.tablename = fields.tablename
|
456
|
+
eosql
|
457
|
+
indexes = []
|
458
|
+
current_index = nil
|
459
|
+
result.map do |row|
|
460
|
+
row.symbolize_keys!
|
461
|
+
index_name = row[:index_name]
|
462
|
+
if current_index != index_name
|
463
|
+
next if !!(/PRIMARY/ =~ index_name)
|
464
|
+
current_index = index_name
|
465
|
+
table_name = row[:table_name]
|
466
|
+
is_unique = row[:non_unique].to_i == 1
|
467
|
+
indexes << IndexDefinition.new(table_name, index_name, is_unique, [], [], [])
|
468
|
+
end
|
469
|
+
indexes.last.columns << row[:field_name] unless row[:field_name].nil?
|
470
|
+
indexes.last.lengths << row[:field_length] unless row[:field_length].nil?
|
471
|
+
end
|
472
|
+
indexes
|
473
|
+
end
|
474
|
+
|
475
|
+
def columns(table_name, name = nil)
|
476
|
+
sql = 'select field,datatype,length from system.fields where schema = ? and tablename = ?'
|
477
|
+
cache = statements[sql] ||= {
|
478
|
+
:stmt => raw_connection.prepare(sql)
|
479
|
+
}
|
480
|
+
stmt = cache[:stmt]
|
481
|
+
|
482
|
+
schema_name, table_name = split_table_name table_name
|
483
|
+
stmt.setString 1, schema_name
|
484
|
+
stmt.setString 2, table_name
|
485
|
+
|
486
|
+
rset = stmt.executeQuery
|
487
|
+
cols = []
|
488
|
+
while rset.next
|
489
|
+
name = rset.getString(1).downcase
|
490
|
+
# todo this was unimplemented, fix this mess
|
491
|
+
default = nil
|
492
|
+
sql_type = nil # TODO should come from query
|
493
|
+
null = true # TODO should come from query
|
494
|
+
cols << Column.new(name, default, sql_type, null)
|
495
|
+
end
|
496
|
+
cols
|
497
|
+
end
|
498
|
+
|
499
|
+
# todo implement the remaining schema statements methods: rename columns, tables, etc...
|
500
|
+
# todo, and these methods have to clear the cache!!!
|
501
|
+
|
502
|
+
public
|
503
|
+
|
504
|
+
def native_database_types
|
505
|
+
NATIVE_DATABASE_TYPES
|
506
|
+
end
|
507
|
+
|
508
|
+
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
|
509
|
+
case type.to_s
|
510
|
+
when 'integer'
|
511
|
+
return 'integer' unless limit
|
512
|
+
case limit
|
513
|
+
when 1, 2
|
514
|
+
'smallint'
|
515
|
+
when 3, 4
|
516
|
+
'integer'
|
517
|
+
when 5..8
|
518
|
+
'bigint'
|
519
|
+
else
|
520
|
+
raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
|
521
|
+
end
|
522
|
+
else
|
523
|
+
super
|
524
|
+
end
|
525
|
+
end
|
526
|
+
|
527
|
+
private
|
528
|
+
|
529
|
+
NATIVE_DATABASE_TYPES = {
|
530
|
+
:primary_key => 'int not null generated by default as identity primary key',
|
531
|
+
:string => {:name => 'varchar', :limit => 255},
|
532
|
+
:text => {:name => 'varchar', :limit => 255},
|
533
|
+
:integer => {:name => 'integer'},
|
534
|
+
:float => {:name => 'float', :limit => 8},
|
535
|
+
:decimal => {:name => 'decimal'},
|
536
|
+
:datetime => {:name => 'datetime'},
|
537
|
+
:timestamp => {:name => 'datetime'},
|
538
|
+
:time => {:name => 'time'},
|
539
|
+
:date => {:name => 'date'},
|
540
|
+
:binary => {:name => 'binary'},
|
541
|
+
:boolean => {:name => 'boolean'},
|
542
|
+
:char => {:name => 'char'},
|
543
|
+
:varchar_max => {:name => 'varchar(max)'},
|
544
|
+
:nchar => {:name => 'nchar'},
|
545
|
+
:nvarchar => {:name => 'nvarchar', :limit => 255},
|
546
|
+
:nvarchar_max => {:name => 'nvarchar(max)'},
|
547
|
+
:ntext => {:name => 'ntext', :limit => 255},
|
548
|
+
:ss_timestamp => {:name => 'timestamp'}
|
549
|
+
}
|
550
|
+
|
551
|
+
def split_table_name(table)
|
552
|
+
name_parts = table.split '.'
|
553
|
+
case name_parts.length
|
554
|
+
when 1
|
555
|
+
schema_name = raw_connection.getSchema
|
556
|
+
table_name = name_parts[0]
|
557
|
+
when 2
|
558
|
+
schema_name = name_parts[0]
|
559
|
+
table_name = name_parts[1]
|
560
|
+
else
|
561
|
+
raise "Invalid table name: #{table}"
|
562
|
+
end
|
563
|
+
[schema_name, table_name]
|
564
|
+
end
|
565
|
+
|
566
|
+
# QUOTING ################################################
|
567
|
+
|
568
|
+
public
|
569
|
+
|
570
|
+
def quote_column_name(name)
|
571
|
+
@quoted_column_names[name] ||= "`#{name.to_s.gsub('`', '``')}`"
|
572
|
+
end
|
573
|
+
|
574
|
+
def quote_table_name(name)
|
575
|
+
@quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
|
576
|
+
end
|
577
|
+
|
578
|
+
def quoted_true
|
579
|
+
"'true'"
|
580
|
+
end
|
581
|
+
|
582
|
+
def quoted_false
|
583
|
+
"'false'"
|
584
|
+
end
|
585
|
+
|
586
|
+
# DATABASE STATEMENTS ####################################
|
587
|
+
|
588
|
+
public
|
589
|
+
|
590
|
+
def outside_transaction?
|
591
|
+
false
|
592
|
+
end
|
593
|
+
|
594
|
+
def supports_statement_cache?
|
595
|
+
true
|
596
|
+
end
|
597
|
+
|
598
|
+
# Begins the transaction (and turns off auto-committing).
|
599
|
+
def begin_db_transaction()
|
600
|
+
log('begin transaction', nil) {
|
601
|
+
raw_connection.autocommit = false if raw_connection.autocommit?
|
602
|
+
}
|
603
|
+
end
|
604
|
+
|
605
|
+
# Commits the transaction (and turns on auto-committing).
|
606
|
+
def commit_db_transaction()
|
607
|
+
log('commit transaction', nil) {
|
608
|
+
raw_connection.autocommit = true unless raw_connection.autocommit?
|
609
|
+
raw_connection.commit
|
610
|
+
}
|
611
|
+
end
|
612
|
+
|
613
|
+
# Rolls back the transaction (and turns on auto-committing). Must be
|
614
|
+
# done if the transaction block raises an exception or returns false.
|
615
|
+
def rollback_db_transaction()
|
616
|
+
log('rollback transaction', nil) {
|
617
|
+
raw_connection.autocommit = true unless raw_connection.autocommit?
|
618
|
+
raw_connection.rollback
|
619
|
+
}
|
620
|
+
end
|
621
|
+
|
622
|
+
def execute(sql, name = 'SQL')
|
623
|
+
log(sql, name) do
|
624
|
+
cache = statements[process_id] ||= {
|
625
|
+
:stmt => raw_connection.createStatement
|
626
|
+
}
|
627
|
+
stmt = cache[:stmt]
|
628
|
+
stmt.execute(sql)
|
629
|
+
end
|
630
|
+
end
|
631
|
+
|
632
|
+
def exec_query(sql, name = 'SQL', binds = [])
|
633
|
+
log(sql, name, binds) do
|
634
|
+
if binds.empty?
|
635
|
+
|
636
|
+
cache = statements[process_id] ||= {
|
637
|
+
:stmt => raw_connection.createStatement
|
638
|
+
}
|
639
|
+
stmt = cache[:stmt]
|
640
|
+
|
641
|
+
stmt.execute(sql)
|
642
|
+
genkeys = stmt.getGeneratedKeys
|
643
|
+
row = genkeys ? next_row(genkeys) : nil
|
644
|
+
@last_inserted_id = row ? row[0] : nil
|
645
|
+
result = stmt.getResultSet
|
646
|
+
if result
|
647
|
+
names = column_names result
|
648
|
+
rows = all_rows result
|
649
|
+
ActiveRecord::Result.new(names, rows)
|
650
|
+
else
|
651
|
+
nil
|
652
|
+
end
|
653
|
+
|
654
|
+
else
|
655
|
+
|
656
|
+
cache = statements[sql] ||= {
|
657
|
+
:stmt => raw_connection.prepare(sql)
|
658
|
+
}
|
659
|
+
stmt = cache[:stmt]
|
660
|
+
binds.to_enum.with_index(1).each { |bind, column|
|
661
|
+
value = bind[1]
|
662
|
+
case value
|
663
|
+
when String
|
664
|
+
stmt.setString column, value
|
665
|
+
when Integer
|
666
|
+
stmt.setInteger column, value
|
667
|
+
when Fixnum
|
668
|
+
stmt.setInteger column, value
|
669
|
+
when Float
|
670
|
+
stmt.setDouble column, value
|
671
|
+
when TrueClass
|
672
|
+
stmt.setBoolean column, true
|
673
|
+
when FalseClass
|
674
|
+
stmt.setBoolean column, false
|
675
|
+
when Time
|
676
|
+
stmt.setTime column, value
|
677
|
+
else
|
678
|
+
raise "don't know how to bind #{value.class} to parameter #{column}"
|
679
|
+
end
|
680
|
+
}
|
681
|
+
|
682
|
+
stmt.execute
|
683
|
+
|
684
|
+
genkeys = stmt.getGeneratedKeys
|
685
|
+
row = genkeys ? next_row(genkeys) : nil
|
686
|
+
@last_inserted_id = row ? row[0] : nil
|
687
|
+
|
688
|
+
result = stmt.getResultSet
|
689
|
+
|
690
|
+
if result
|
691
|
+
names = column_names result
|
692
|
+
rows = all_rows result
|
693
|
+
ActiveRecord::Result.new(names, rows)
|
694
|
+
else
|
695
|
+
nil
|
696
|
+
end
|
697
|
+
|
698
|
+
end
|
699
|
+
end
|
700
|
+
end
|
701
|
+
|
702
|
+
def last_inserted_id(result)
|
703
|
+
@last_inserted_id
|
704
|
+
end
|
705
|
+
|
706
|
+
protected
|
707
|
+
|
708
|
+
def select(sql, name = nil, binds = [])
|
709
|
+
exec_query(sql, name, binds).to_a
|
710
|
+
end
|
711
|
+
|
712
|
+
protected
|
713
|
+
|
714
|
+
def select_rows(sql, name = nil)
|
715
|
+
rows = exec_query(sql, name).rows
|
716
|
+
end
|
717
|
+
|
718
|
+
private
|
719
|
+
|
720
|
+
def column_names (result)
|
721
|
+
return [] if result.nil?
|
722
|
+
names = []
|
723
|
+
meta = result.getMetaData
|
724
|
+
count = meta.getColumnCount
|
725
|
+
(1..count).each { |i|
|
726
|
+
names << meta.getColumnName(i).downcase
|
727
|
+
}
|
728
|
+
names
|
729
|
+
end
|
730
|
+
|
731
|
+
def all_rows(result)
|
732
|
+
rows = []
|
733
|
+
while (row = next_row(result)) != nil
|
734
|
+
rows << row
|
735
|
+
end
|
736
|
+
rows
|
737
|
+
end
|
738
|
+
|
739
|
+
def next_row(result)
|
740
|
+
return nil if result.nil?
|
741
|
+
if result.next
|
742
|
+
meta = result.getMetaData
|
743
|
+
count = meta.getColumnCount
|
744
|
+
row = []
|
745
|
+
(1..count).each { |i|
|
746
|
+
type = meta.getType(i)
|
747
|
+
case type
|
748
|
+
when :SQL_INTEGER
|
749
|
+
row << result.getInteger(i)
|
750
|
+
when :SQL_DOUBLE
|
751
|
+
row << result.getDouble(i)
|
752
|
+
when :SQL_STRING
|
753
|
+
row << result.getString(i)
|
754
|
+
when :SQL_DATE
|
755
|
+
row << result.getDate(i)
|
756
|
+
when :SQL_TIME
|
757
|
+
row << result.getTime(i)
|
758
|
+
when :SQL_TIMESTAMP
|
759
|
+
row << result.getTimestamp(i)
|
760
|
+
when :SQL_CHAR
|
761
|
+
row << result.getChar(i)
|
762
|
+
when :SQL_BOOLEAN
|
763
|
+
row << result.getBoolean(i)
|
764
|
+
else
|
765
|
+
raise "unknown type #{type} for column #{i}"
|
766
|
+
end
|
767
|
+
}
|
768
|
+
row
|
769
|
+
else
|
770
|
+
nil
|
771
|
+
end
|
772
|
+
end
|
773
|
+
|
774
|
+
end
|
775
|
+
|
776
|
+
end
|
777
|
+
|
778
|
+
end
|