activerecord-sqlanywhere-adapter 0.1.2 → 0.1.3
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/CHANGELOG +27 -23
- data/LICENSE +23 -23
- data/README +126 -125
- data/lib/active_record/connection_adapters/sqlanywhere_adapter.rb +572 -570
- data/test/connection.rb +25 -25
- metadata +62 -53
@@ -1,570 +1,572 @@
|
|
1
|
-
#====================================================
|
2
|
-
#
|
3
|
-
# Copyright 2008-
|
4
|
-
#
|
5
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
-
# you may not use this file except in compliance with the License.
|
7
|
-
# You may obtain a copy of the License at
|
8
|
-
#
|
9
|
-
#
|
10
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
-
#
|
12
|
-
# Unless required by applicable law or agreed to in writing, software
|
13
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
-
#
|
16
|
-
# See the License for the specific language governing permissions and
|
17
|
-
# limitations under the License.
|
18
|
-
#
|
19
|
-
# While not a requirement of the license, if you do modify this file, we
|
20
|
-
# would appreciate hearing about it. Please email sqlany_interfaces@sybase.com
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#====================================================
|
24
|
-
|
25
|
-
require 'active_record/connection_adapters/abstract_adapter'
|
26
|
-
|
27
|
-
# Singleton class to hold a valid instance of the SQLAnywhereInterface across all connections
|
28
|
-
class SA
|
29
|
-
include Singleton
|
30
|
-
attr_accessor :api
|
31
|
-
|
32
|
-
def initialize
|
33
|
-
require_library_or_gem 'sqlanywhere' unless defined? SQLAnywhere
|
34
|
-
@api = SQLAnywhere::SQLAnywhereInterface.new()
|
35
|
-
raise LoadError, "Could not load SQLAnywhere DBCAPI library" if SQLAnywhere::API.sqlany_initialize_interface(@api) == 0
|
36
|
-
raise LoadError, "Could not initialize SQLAnywhere DBCAPI library" if @api.sqlany_init() == 0
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
module ActiveRecord
|
41
|
-
class Base
|
42
|
-
DEFAULT_CONFIG = { :username => 'dba', :password => 'sql' }
|
43
|
-
# Main connection function to SQL Anywhere
|
44
|
-
# Connection Adapter takes four parameters:
|
45
|
-
# * :database (required, no default). Corresponds to "DatabaseName=" in connection string
|
46
|
-
# * :server (optional, defaults to :databse). Corresponds to "ServerName=" in connection string
|
47
|
-
# * :username (optional, default to 'dba')
|
48
|
-
# * :password (optional, deafult to 'sql')
|
49
|
-
# * :
|
50
|
-
# * :
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
connection_string
|
60
|
-
connection_string += "
|
61
|
-
connection_string += "
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
return :
|
77
|
-
return :
|
78
|
-
return :
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
when /^
|
87
|
-
when /^
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
#
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
@
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
#
|
154
|
-
#
|
155
|
-
#
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
:
|
161
|
-
:
|
162
|
-
:
|
163
|
-
:
|
164
|
-
:
|
165
|
-
:
|
166
|
-
:
|
167
|
-
:
|
168
|
-
:
|
169
|
-
:
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
#
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
#
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
#
|
209
|
-
#
|
210
|
-
#
|
211
|
-
#
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
order = order.
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
raise ActiveRecord::
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
max_cols
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
@affected_rows =
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
SA.instance.api.
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
#
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
SA.instance.api.
|
299
|
-
|
300
|
-
SA.instance.api.
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
max_cols
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
when
|
350
|
-
column_type_sql = '
|
351
|
-
when
|
352
|
-
column_type_sql = '
|
353
|
-
when
|
354
|
-
column_type_sql = '
|
355
|
-
|
356
|
-
column_type_sql = '
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
index.
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
add_column_sql
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
#
|
451
|
-
#
|
452
|
-
#
|
453
|
-
#
|
454
|
-
#
|
455
|
-
#
|
456
|
-
#
|
457
|
-
#
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
nesting_level = nesting_level
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
return
|
501
|
-
|
502
|
-
|
503
|
-
final_sql
|
504
|
-
final_sql << "#{select_components[0][
|
505
|
-
|
506
|
-
|
507
|
-
return
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
#
|
514
|
-
#
|
515
|
-
#
|
516
|
-
#
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
SA.instance.api.sqlany_execute_immediate(@connection, "
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
end
|
570
|
-
|
1
|
+
#====================================================
|
2
|
+
#
|
3
|
+
# Copyright 2008-2010 iAnywhere Solutions, Inc.
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
#
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
#
|
19
|
+
# While not a requirement of the license, if you do modify this file, we
|
20
|
+
# would appreciate hearing about it. Please email sqlany_interfaces@sybase.com
|
21
|
+
#
|
22
|
+
#
|
23
|
+
#====================================================
|
24
|
+
|
25
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
26
|
+
|
27
|
+
# Singleton class to hold a valid instance of the SQLAnywhereInterface across all connections
|
28
|
+
class SA
|
29
|
+
include Singleton
|
30
|
+
attr_accessor :api
|
31
|
+
|
32
|
+
def initialize
|
33
|
+
require_library_or_gem 'sqlanywhere' unless defined? SQLAnywhere
|
34
|
+
@api = SQLAnywhere::SQLAnywhereInterface.new()
|
35
|
+
raise LoadError, "Could not load SQLAnywhere DBCAPI library" if SQLAnywhere::API.sqlany_initialize_interface(@api) == 0
|
36
|
+
raise LoadError, "Could not initialize SQLAnywhere DBCAPI library" if @api.sqlany_init() == 0
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module ActiveRecord
|
41
|
+
class Base
|
42
|
+
DEFAULT_CONFIG = { :username => 'dba', :password => 'sql' }
|
43
|
+
# Main connection function to SQL Anywhere
|
44
|
+
# Connection Adapter takes four parameters:
|
45
|
+
# * :database (required, no default). Corresponds to "DatabaseName=" in connection string
|
46
|
+
# * :server (optional, defaults to :databse). Corresponds to "ServerName=" in connection string
|
47
|
+
# * :username (optional, default to 'dba')
|
48
|
+
# * :password (optional, deafult to 'sql')
|
49
|
+
# * :encoding (optional, defaults to charset of OS)
|
50
|
+
# * :commlinks (optional). Corresponds to "CommLinks=" in connection string
|
51
|
+
# * :connection_name (optional). Corresponds to "ConnectionName=" in connection string
|
52
|
+
|
53
|
+
def self.sqlanywhere_connection(config)
|
54
|
+
|
55
|
+
config = DEFAULT_CONFIG.merge(config)
|
56
|
+
|
57
|
+
raise ArgumentError, "No database name was given. Please add a :database option." unless config.has_key?(:database)
|
58
|
+
|
59
|
+
connection_string = "ServerName=#{(config[:server] || config[:database])};DatabaseName=#{config[:database]};UserID=#{config[:username]};Password=#{config[:password]};"
|
60
|
+
connection_string += "CommLinks=#{config[:commlinks]};" unless config[:commlinks].nil?
|
61
|
+
connection_string += "ConnectionName=#{config[:connection_name]};" unless config[:connection_name].nil?
|
62
|
+
connection_string += "CharSet=#{config[:encoding]};" unless config[:encoding].nil?
|
63
|
+
connection_string += "Idle=0" # Prevent the server from disconnecting us if we're idle for >240mins (by default)
|
64
|
+
|
65
|
+
db = SA.instance.api.sqlany_new_connection()
|
66
|
+
|
67
|
+
ConnectionAdapters::SQLAnywhereAdapter.new(db, logger, connection_string)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
module ConnectionAdapters
|
72
|
+
class SQLAnywhereColumn < Column
|
73
|
+
private
|
74
|
+
# Overridden to handle SQL Anywhere integer, varchar, binary, and timestamp types
|
75
|
+
def simplified_type(field_type)
|
76
|
+
return :boolean if field_type =~ /tinyint/i
|
77
|
+
return :string if field_type =~ /varchar/i
|
78
|
+
return :binary if field_type =~ /long binary/i
|
79
|
+
return :datetime if field_type =~ /timestamp/i
|
80
|
+
return :integer if field_type =~ /smallint|bigint/i
|
81
|
+
super
|
82
|
+
end
|
83
|
+
|
84
|
+
def extract_limit(sql_type)
|
85
|
+
case sql_type
|
86
|
+
when /^tinyint/i: 1
|
87
|
+
when /^smallint/i: 2
|
88
|
+
when /^integer/i: 4
|
89
|
+
when /^bigint/i: 8
|
90
|
+
else super
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
protected
|
95
|
+
# Handles the encoding of a binary object into SQL Anywhere
|
96
|
+
# SQL Anywhere requires that binary values be encoded as \xHH, where HH is a hexadecimal number
|
97
|
+
# This function encodes the binary string in this format
|
98
|
+
def self.string_to_binary(value)
|
99
|
+
"\\x" + value.unpack("H*")[0].scan(/../).join("\\x")
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.binary_to_string(value)
|
103
|
+
value.gsub(/\\x[0-9]{2}/) { |byte| byte[2..3].hex }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class SQLAnywhereAdapter < AbstractAdapter
|
108
|
+
def initialize( connection, logger = nil, connection_string = "") #:nodoc:
|
109
|
+
super(connection, logger)
|
110
|
+
@auto_commit = true
|
111
|
+
@affected_rows = 0
|
112
|
+
@connection_string = connection_string
|
113
|
+
connect!
|
114
|
+
end
|
115
|
+
|
116
|
+
def adapter_name #:nodoc:
|
117
|
+
'SQLAnywhere'
|
118
|
+
end
|
119
|
+
|
120
|
+
def supports_migrations? #:nodoc:
|
121
|
+
true
|
122
|
+
end
|
123
|
+
|
124
|
+
def requires_reloading?
|
125
|
+
true
|
126
|
+
end
|
127
|
+
|
128
|
+
def active?
|
129
|
+
# The liveness variable is used a low-cost "no-op" to test liveness
|
130
|
+
SA.instance.api.sqlany_execute_immediate(@connection, "SET liveness = 1") == 1
|
131
|
+
rescue
|
132
|
+
false
|
133
|
+
end
|
134
|
+
|
135
|
+
def disconnect!
|
136
|
+
result = SA.instance.api.sqlany_disconnect( @connection )
|
137
|
+
super
|
138
|
+
end
|
139
|
+
|
140
|
+
def reconnect!
|
141
|
+
disconnect!
|
142
|
+
connect!
|
143
|
+
end
|
144
|
+
|
145
|
+
def supports_count_distinct? #:nodoc:
|
146
|
+
true
|
147
|
+
end
|
148
|
+
|
149
|
+
def supports_autoincrement? #:nodoc:
|
150
|
+
true
|
151
|
+
end
|
152
|
+
|
153
|
+
# Maps native ActiveRecord/Ruby types into SQLAnywhere types
|
154
|
+
# TINYINTs are treated as the default boolean value
|
155
|
+
# ActiveRecord allows NULLs in boolean columns, and the SQL Anywhere BIT type does not
|
156
|
+
# As a result, TINYINT must be used. All TINYINT columns will be assumed to be boolean and
|
157
|
+
# should not be used as single-byte integer columns. This restriction is similar to other ActiveRecord database drivers
|
158
|
+
def native_database_types #:nodoc:
|
159
|
+
{
|
160
|
+
:primary_key => 'INTEGER PRIMARY KEY DEFAULT AUTOINCREMENT NOT NULL',
|
161
|
+
:string => { :name => "varchar", :limit => 255 },
|
162
|
+
:text => { :name => "long varchar" },
|
163
|
+
:integer => { :name => "integer" },
|
164
|
+
:float => { :name => "float" },
|
165
|
+
:decimal => { :name => "decimal" },
|
166
|
+
:datetime => { :name => "datetime" },
|
167
|
+
:timestamp => { :name => "datetime" },
|
168
|
+
:time => { :name => "time" },
|
169
|
+
:date => { :name => "date" },
|
170
|
+
:binary => { :name => "long binary" },
|
171
|
+
:boolean => { :name => "tinyint"}
|
172
|
+
}
|
173
|
+
end
|
174
|
+
|
175
|
+
# QUOTING ==================================================
|
176
|
+
|
177
|
+
# Applies quotations around column names in generated queries
|
178
|
+
def quote_column_name(name) #:nodoc:
|
179
|
+
%Q("#{name}")
|
180
|
+
end
|
181
|
+
|
182
|
+
# Handles special quoting of binary columns. Binary columns will be treated as strings inside of ActiveRecord.
|
183
|
+
# ActiveRecord requires that any strings it inserts into databases must escape the backslash (\).
|
184
|
+
# Since in the binary case, the (\x) is significant to SQL Anywhere, it cannot be escaped.
|
185
|
+
def quote(value, column = nil)
|
186
|
+
case value
|
187
|
+
when String, ActiveSupport::Multibyte::Chars
|
188
|
+
value_S = value.to_s
|
189
|
+
if column && column.type == :binary && column.class.respond_to?(:string_to_binary)
|
190
|
+
"#{quoted_string_prefix}'#{column.class.string_to_binary(value_S)}'"
|
191
|
+
else
|
192
|
+
super(value, column)
|
193
|
+
end
|
194
|
+
else
|
195
|
+
super(value, column)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def quoted_true
|
200
|
+
'1'
|
201
|
+
end
|
202
|
+
|
203
|
+
def quoted_false
|
204
|
+
'0'
|
205
|
+
end
|
206
|
+
|
207
|
+
|
208
|
+
# SQL Anywhere, in accordance with the SQL Standard, does not allow a column to appear in the ORDER BY list
|
209
|
+
# that is not also in the SELECT with when obtaining DISTINCT rows beacuse the actual semantics of this query
|
210
|
+
# are unclear. The following functions create a query that mimics the way that SQLite and MySQL handle this query.
|
211
|
+
#
|
212
|
+
# This function (distinct) is based on the Oracle ActiveRecord driver created by Graham Jenkins (2005)
|
213
|
+
# (http://svn.rubyonrails.org/rails/adapters/oracle/lib/active_record/connection_adapters/oracle_adapter.rb)
|
214
|
+
def distinct(columns, order_by)
|
215
|
+
return "DISTINCT #{columns}" if order_by.blank?
|
216
|
+
order_columns = order_by.split(',').map { |s| s.strip }.reject(&:blank?)
|
217
|
+
order_columns = order_columns.zip((0...order_columns.size).to_a).map do |c, i|
|
218
|
+
"FIRST_VALUE(#{c.split.first}) OVER (PARTITION BY #{columns} ORDER BY #{c}) AS alias_#{i}__"
|
219
|
+
end
|
220
|
+
sql = "DISTINCT #{columns}, "
|
221
|
+
sql << order_columns * ", "
|
222
|
+
end
|
223
|
+
|
224
|
+
# This function (add_order_by_for_association_limiting) is based on the Oracle ActiveRecord driver created by Graham Jenkins (2005)
|
225
|
+
# (http://svn.rubyonrails.org/rails/adapters/oracle/lib/active_record/connection_adapters/oracle_adapter.rb)
|
226
|
+
def add_order_by_for_association_limiting!(sql, options)
|
227
|
+
return sql if options[:order].blank?
|
228
|
+
|
229
|
+
order = options[:order].split(',').collect { |s| s.strip }.reject(&:blank?)
|
230
|
+
order.map! {|s| $1 if s =~ / (.*)/}
|
231
|
+
order = order.zip((0...order.size).to_a).map { |s,i| "alias_#{i}__ #{s}" }.join(', ')
|
232
|
+
|
233
|
+
sql << " ORDER BY #{order}"
|
234
|
+
end
|
235
|
+
|
236
|
+
# The database execution function
|
237
|
+
def execute(sql, name = nil) #:nodoc:
|
238
|
+
return if sql.nil?
|
239
|
+
sql = modify_limit_offset(sql)
|
240
|
+
|
241
|
+
# ActiveRecord allows a query to return TOP 0. SQL Anywhere requires that the TOP value is a positive integer.
|
242
|
+
return Array.new() if sql =~ /TOP 0/i
|
243
|
+
|
244
|
+
# Executes the query, iterates through the results, and builds an array of hashes.
|
245
|
+
rs = SA.instance.api.sqlany_execute_direct(@connection, sql)
|
246
|
+
if rs.nil?
|
247
|
+
error = SA.instance.api.sqlany_error(@connection)
|
248
|
+
case error[0].to_i
|
249
|
+
when -143
|
250
|
+
if sql =~ /^SELECT/i then
|
251
|
+
raise ActiveRecord::StatementInvalid.new("#{error}:#{sql}")
|
252
|
+
else
|
253
|
+
raise ActiveRecord::ActiveRecordError.new("#{error}:#{sql}")
|
254
|
+
end
|
255
|
+
else
|
256
|
+
raise ActiveRecord::StatementInvalid.new("#{error}:#{sql}")
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
record = []
|
261
|
+
if( SA.instance.api.sqlany_num_cols(rs) > 0 )
|
262
|
+
while SA.instance.api.sqlany_fetch_next(rs) == 1
|
263
|
+
max_cols = SA.instance.api.sqlany_num_cols(rs)
|
264
|
+
result = Hash.new()
|
265
|
+
max_cols.times do |cols|
|
266
|
+
result[SA.instance.api.sqlany_get_column_info(rs, cols)[2]] = SA.instance.api.sqlany_get_column(rs, cols)[1]
|
267
|
+
end
|
268
|
+
record << result
|
269
|
+
end
|
270
|
+
@affected_rows = 0
|
271
|
+
else
|
272
|
+
@affected_rows = SA.instance.api.sqlany_affected_rows(rs)
|
273
|
+
end
|
274
|
+
SA.instance.api.sqlany_free_stmt(rs)
|
275
|
+
|
276
|
+
SA.instance.api.sqlany_commit(@connection) if @auto_commit
|
277
|
+
return record
|
278
|
+
end
|
279
|
+
|
280
|
+
# The database update function.
|
281
|
+
def update_sql(sql, name = nil)
|
282
|
+
execute( sql, name )
|
283
|
+
return @affected_rows
|
284
|
+
end
|
285
|
+
|
286
|
+
# The database delete function.
|
287
|
+
def delete_sql(sql, name = nil) #:nodoc:
|
288
|
+
execute( sql, name )
|
289
|
+
return @affected_rows
|
290
|
+
end
|
291
|
+
|
292
|
+
# The database insert function.
|
293
|
+
# ActiveRecord requires that insert_sql returns the primary key of the row just inserted. In most cases, this can be accomplished
|
294
|
+
# by immediatly querying the @@identity property. If the @@identity property is 0, then passed id_value is used
|
295
|
+
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
|
296
|
+
execute(sql, name)
|
297
|
+
|
298
|
+
identity = SA.instance.api.sqlany_execute_direct(@connection, 'SELECT @@identity')
|
299
|
+
raise ActiveRecord::StatementInvalid.new("#{SA.instance.api.sqlany_error(@connection)}:#{sql}") if identity.nil?
|
300
|
+
SA.instance.api.sqlany_fetch_next(identity)
|
301
|
+
retval = SA.instance.api.sqlany_get_column(identity, 0)[1]
|
302
|
+
SA.instance.api.sqlany_free_stmt(identity)
|
303
|
+
|
304
|
+
retval = id_value if retval == 0
|
305
|
+
return retval
|
306
|
+
end
|
307
|
+
|
308
|
+
# Returns a query as an array of arrays
|
309
|
+
def select_rows(sql, name = nil)
|
310
|
+
rs = SA.instance.api.sqlany_execute_direct(@connection, sql)
|
311
|
+
raise ActiveRecord::StatementInvalid.new("#{SA.instance.api.sqlany_error(@connection)}:#{sql}") if rs.nil?
|
312
|
+
record = []
|
313
|
+
while SA.instance.api.sqlany_fetch_next(rs) == 1
|
314
|
+
max_cols = SA.instance.api.sqlany_num_cols(rs)
|
315
|
+
result = Array.new(max_cols)
|
316
|
+
max_cols.times do |cols|
|
317
|
+
result[cols] = SA.instance.api.sqlany_get_column(rs, cols)[1]
|
318
|
+
end
|
319
|
+
record << result
|
320
|
+
end
|
321
|
+
SA.instance.api.sqlany_free_stmt(rs)
|
322
|
+
return record
|
323
|
+
end
|
324
|
+
|
325
|
+
def begin_db_transaction #:nodoc:
|
326
|
+
@auto_commit = false;
|
327
|
+
end
|
328
|
+
|
329
|
+
def commit_db_transaction #:nodoc:
|
330
|
+
SA.instance.api.sqlany_commit(@connection)
|
331
|
+
@auto_commit = true;
|
332
|
+
end
|
333
|
+
|
334
|
+
def rollback_db_transaction #:nodoc:
|
335
|
+
SA.instance.api.sqlany_rollback(@connection)
|
336
|
+
@auto_commit = true;
|
337
|
+
end
|
338
|
+
|
339
|
+
def add_lock!(sql, options) #:nodoc:
|
340
|
+
sql
|
341
|
+
end
|
342
|
+
|
343
|
+
# SQL Anywhere does not support sizing of integers based on the sytax INTEGER(size). Integer sizes
|
344
|
+
# must be captured when generating the SQL and replaced with the appropriate size.
|
345
|
+
def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
|
346
|
+
if native = native_database_types[type]
|
347
|
+
if type == :integer
|
348
|
+
case limit
|
349
|
+
when 1
|
350
|
+
column_type_sql = 'tinyint'
|
351
|
+
when 2
|
352
|
+
column_type_sql = 'smallint'
|
353
|
+
when 3..4
|
354
|
+
column_type_sql = 'integer'
|
355
|
+
when 5..8
|
356
|
+
column_type_sql = 'bigint'
|
357
|
+
else
|
358
|
+
column_type_sql = 'integer'
|
359
|
+
end
|
360
|
+
column_type_sql
|
361
|
+
else
|
362
|
+
super(type, limit, precision, scale)
|
363
|
+
end
|
364
|
+
else
|
365
|
+
super(type, limit, precision, scale)
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
# Do not return SYS-owned or DBO-owned tables
|
370
|
+
def tables(name = nil) #:nodoc:
|
371
|
+
sql = "SELECT table_name FROM SYS.SYSTABLE WHERE creator NOT IN (0,3)"
|
372
|
+
select(sql, name).map { |row| row["table_name"] }
|
373
|
+
end
|
374
|
+
|
375
|
+
def columns(table_name, name = nil) #:nodoc:
|
376
|
+
table_structure(table_name).map do |field|
|
377
|
+
field['default'] = field['default'][1..-2] if (!field['default'].nil? and field['default'][0].chr == "'")
|
378
|
+
SQLAnywhereColumn.new(field['name'], field['default'], field['domain'], (field['nulls'] == 1))
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
def indexes(table_name, name = nil) #:nodoc:
|
383
|
+
sql = "SELECT DISTINCT index_name, \"unique\" FROM SYS.SYSTABLE INNER JOIN SYS.SYSIDXCOL ON SYS.SYSTABLE.table_id = SYS.SYSIDXCOL.table_id INNER JOIN SYS.SYSIDX ON SYS.SYSTABLE.table_id = SYS.SYSIDX.table_id AND SYS.SYSIDXCOL.index_id = SYS.SYSIDX.index_id WHERE table_name = '#{table_name}' AND index_category > 2"
|
384
|
+
select(sql, name).map do |row|
|
385
|
+
index = IndexDefinition.new(table_name, row['index_name'])
|
386
|
+
index.unique = row['unique'] == 1
|
387
|
+
sql = "SELECT column_name FROM SYS.SYSIDX INNER JOIN SYS.SYSIDXCOL ON SYS.SYSIDXCOL.table_id = SYS.SYSIDX.table_id AND SYS.SYSIDXCOL.index_id = SYS.SYSIDX.index_id INNER JOIN SYS.SYSCOLUMN ON SYS.SYSCOLUMN.table_id = SYS.SYSIDXCOL.table_id AND SYS.SYSCOLUMN.column_id = SYS.SYSIDXCOL.column_id WHERE index_name = '#{row['index_name']}'"
|
388
|
+
index.columns = select(sql).map { |col| col['column_name'] }
|
389
|
+
index
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
def primary_key(table_name) #:nodoc:
|
394
|
+
sql = "SELECT SYS.SYSTABCOL.column_name FROM (SYS.SYSTABLE JOIN SYS.SYSTABCOL) LEFT OUTER JOIN (SYS.SYSIDXCOL JOIN SYS.SYSIDX) WHERE table_name = '#{table_name}' AND SYS.SYSIDXCOL.sequence = 0"
|
395
|
+
rs = select(sql)
|
396
|
+
if !rs.nil? and !rs[0].nil?
|
397
|
+
rs[0]['column_name']
|
398
|
+
else
|
399
|
+
nil
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
def remove_index(table_name, options={}) #:nodoc:
|
404
|
+
execute "DROP INDEX #{table_name}.#{quote_column_name(index_name(table_name, options))}"
|
405
|
+
end
|
406
|
+
|
407
|
+
def rename_table(name, new_name)
|
408
|
+
execute "ALTER TABLE #{quote_table_name(name)} RENAME #{quote_table_name(new_name)}"
|
409
|
+
end
|
410
|
+
|
411
|
+
def remove_column(table_name, column_name) #:nodoc:
|
412
|
+
execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}"
|
413
|
+
end
|
414
|
+
|
415
|
+
def change_column_default(table_name, column_name, default) #:nodoc:
|
416
|
+
execute "ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} DEFAULT #{quote(default)}"
|
417
|
+
end
|
418
|
+
|
419
|
+
def change_column_null(table_name, column_name, null, default = nil)
|
420
|
+
unless null || default.nil?
|
421
|
+
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
|
422
|
+
end
|
423
|
+
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? '' : 'NOT'} NULL")
|
424
|
+
end
|
425
|
+
|
426
|
+
def change_column(table_name, column_name, type, options = {}) #:nodoc:
|
427
|
+
add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
428
|
+
add_column_options!(add_column_sql, options)
|
429
|
+
add_column_sql << ' NULL' if options[:null]
|
430
|
+
execute(add_column_sql)
|
431
|
+
end
|
432
|
+
|
433
|
+
def rename_column(table_name, column_name, new_column_name) #:nodoc:
|
434
|
+
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
|
435
|
+
end
|
436
|
+
|
437
|
+
def remove_column(table_name, column_name)
|
438
|
+
sql = "SELECT \"index_name\" FROM SYS.SYSTAB join SYS.SYSTABCOL join SYS.SYSIDXCOL join SYS.SYSIDX WHERE \"column_name\" = '#{column_name}' AND \"table_name\" = '#{table_name}'"
|
439
|
+
select(sql, nil).map do |row|
|
440
|
+
execute "DROP INDEX \"#{table_name}\".\"#{row['index_name']}\""
|
441
|
+
end
|
442
|
+
execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}"
|
443
|
+
end
|
444
|
+
|
445
|
+
protected
|
446
|
+
def select(sql, name = nil) #:nodoc:
|
447
|
+
return execute(sql, name)
|
448
|
+
end
|
449
|
+
|
450
|
+
# ActiveRecord uses the OFFSET/LIMIT keywords at the end of query to limit the number of items in the result set.
|
451
|
+
# This syntax is NOT supported by SQL Anywhere. In previous versions of this adapter this adapter simply
|
452
|
+
# overrode the add_limit_offset function and added the appropriate TOP/START AT keywords to the start of the query.
|
453
|
+
# However, this will not work for cases where add_limit_offset is being used in a subquery since add_limit_offset
|
454
|
+
# is called with the WHERE clause.
|
455
|
+
#
|
456
|
+
# As a result, the following function must be called before every SELECT statement against the database. It
|
457
|
+
# recursivly walks through all subqueries in the SQL statment and replaces the instances of OFFSET/LIMIT with the
|
458
|
+
# corresponding TOP/START AT. It was my intent to do the entire thing using regular expressions, but it would seem
|
459
|
+
# that it is not possible given that it must count levels of nested brackets.
|
460
|
+
def modify_limit_offset(sql)
|
461
|
+
modified_sql = ""
|
462
|
+
subquery_sql = ""
|
463
|
+
in_single_quote = false
|
464
|
+
in_double_quote = false
|
465
|
+
nesting_level = 0
|
466
|
+
if sql =~ /(OFFSET|LIMIT)/xmi then
|
467
|
+
if sql =~ /\(/ then
|
468
|
+
sql.split(//).each_with_index do |x, i|
|
469
|
+
case x[0]
|
470
|
+
when 40 # left brace - (
|
471
|
+
modified_sql << x if nesting_level == 0
|
472
|
+
subquery_sql << x if nesting_level > 0
|
473
|
+
nesting_level = nesting_level + 1 unless in_double_quote || in_single_quote
|
474
|
+
when 41 # right brace - )
|
475
|
+
nesting_level = nesting_level - 1 unless in_double_quote || in_single_quote
|
476
|
+
if nesting_level == 0 and !in_double_quote and !in_single_quote then
|
477
|
+
modified_sql << modify_limit_offset(subquery_sql)
|
478
|
+
subquery_sql = ""
|
479
|
+
end
|
480
|
+
modified_sql << x if nesting_level == 0
|
481
|
+
subquery_sql << x if nesting_level > 0
|
482
|
+
when 39 # single quote - '
|
483
|
+
in_single_quote = in_single_quote ^ true unless in_double_quote
|
484
|
+
modified_sql << x if nesting_level == 0
|
485
|
+
subquery_sql << x if nesting_level > 0
|
486
|
+
when 34 # double quote - "
|
487
|
+
in_double_quote = in_double_quote ^ true unless in_single_quote
|
488
|
+
modified_sql << x if nesting_level == 0
|
489
|
+
subquery_sql << x if nesting_level > 0
|
490
|
+
else
|
491
|
+
modified_sql << x if nesting_level == 0
|
492
|
+
subquery_sql << x if nesting_level > 0
|
493
|
+
end
|
494
|
+
raise ActiveRecord::StatementInvalid.new("Braces do not match: #{sql}") if nesting_level < 0
|
495
|
+
end
|
496
|
+
else
|
497
|
+
modified_sql = sql
|
498
|
+
end
|
499
|
+
raise ActiveRecord::StatementInvalid.new("Quotes do not match: #{sql}") if in_double_quote or in_single_quote
|
500
|
+
return "" if modified_sql.nil?
|
501
|
+
select_components = modified_sql.scan(/\ASELECT\s+(DISTINCT)?(.*?)(?:\s+LIMIT\s+(.*?))?(?:\s+OFFSET\s+(.*?))?\Z/xmi)
|
502
|
+
return modified_sql if select_components[0].nil?
|
503
|
+
final_sql = "SELECT #{select_components[0][0]} "
|
504
|
+
final_sql << "TOP #{select_components[0][2]} " unless select_components[0][2].nil?
|
505
|
+
final_sql << "START AT #{(select_components[0][3].to_i + 1).to_s} " unless select_components[0][3].nil?
|
506
|
+
final_sql << "#{select_components[0][1]}"
|
507
|
+
return final_sql
|
508
|
+
else
|
509
|
+
return sql
|
510
|
+
end
|
511
|
+
end
|
512
|
+
|
513
|
+
# Queries the structure of a table including the columns names, defaults, type, and nullability
|
514
|
+
# ActiveRecord uses the type to parse scale and precision information out of the types. As a result,
|
515
|
+
# chars, varchars, binary, nchars, nvarchars must all be returned in the form <i>type</i>(<i>width</i>)
|
516
|
+
# numeric and decimal must be returned in the form <i>type</i>(<i>width</i>, <i>scale</i>)
|
517
|
+
# Nullability is returned as 0 (no nulls allowed) or 1 (nulls allowed)
|
518
|
+
# Alos, ActiveRecord expects an autoincrement column to have default value of NULL
|
519
|
+
|
520
|
+
def table_structure(table_name)
|
521
|
+
sql = <<-SQL
|
522
|
+
SELECT SYS.SYSCOLUMN.column_name AS name,
|
523
|
+
NULLIF(SYS.SYSCOLUMN."default", 'autoincrement') AS "default",
|
524
|
+
IF SYS.SYSCOLUMN.domain_id IN (7,8,9,11,33,34,35,3,27) THEN
|
525
|
+
IF SYS.SYSCOLUMN.domain_id IN (3,27) THEN
|
526
|
+
SYS.SYSDOMAIN.domain_name || '(' || SYS.SYSCOLUMN.width || ',' || SYS.SYSCOLUMN.scale || ')'
|
527
|
+
ELSE
|
528
|
+
SYS.SYSDOMAIN.domain_name || '(' || SYS.SYSCOLUMN.width || ')'
|
529
|
+
ENDIF
|
530
|
+
ELSE
|
531
|
+
SYS.SYSDOMAIN.domain_name
|
532
|
+
ENDIF AS domain,
|
533
|
+
IF SYS.SYSCOLUMN.nulls = 'Y' THEN 1 ELSE 0 ENDIF AS nulls
|
534
|
+
FROM
|
535
|
+
SYS.SYSCOLUMN
|
536
|
+
INNER JOIN SYS.SYSTABLE ON SYS.SYSCOLUMN.table_id = SYS.SYSTABLE.table_id
|
537
|
+
INNER JOIN SYS.SYSDOMAIN ON SYS.SYSCOLUMN.domain_id = SYS.SYSDOMAIN.domain_id
|
538
|
+
WHERE
|
539
|
+
table_name = '#{table_name}'
|
540
|
+
SQL
|
541
|
+
returning structure = select(sql) do
|
542
|
+
raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if false
|
543
|
+
end
|
544
|
+
end
|
545
|
+
|
546
|
+
# Required to prevent DEFAULT NULL being added to primary keys
|
547
|
+
def options_include_default?(options)
|
548
|
+
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
|
549
|
+
end
|
550
|
+
|
551
|
+
private
|
552
|
+
|
553
|
+
def connect!
|
554
|
+
result = SA.instance.api.sqlany_connect(@connection, @connection_string)
|
555
|
+
if result == 1 then
|
556
|
+
set_connection_options
|
557
|
+
else
|
558
|
+
error = SA.instance.api.sqlany_error(@connection)
|
559
|
+
raise ActiveRecord::ActiveRecordError.new("#{error}: Cannot Establish Connection")
|
560
|
+
end
|
561
|
+
end
|
562
|
+
|
563
|
+
def set_connection_options
|
564
|
+
SA.instance.api.sqlany_execute_immediate(@connection, "SET TEMPORARY OPTION non_keywords = 'LOGIN'") rescue nil
|
565
|
+
SA.instance.api.sqlany_execute_immediate(@connection, "SET TEMPORARY OPTION timestamp_format = 'YYYY-MM-DD HH:NN:SS'") rescue nil
|
566
|
+
# The liveness variable is used a low-cost "no-op" to test liveness
|
567
|
+
SA.instance.api.sqlany_execute_immediate(@connection, "CREATE VARIABLE liveness INT") rescue nil
|
568
|
+
end
|
569
|
+
end
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|