activerecord-cubrid2-adapter 0.0.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.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/LICENSE +21 -0
- data/README.md +63 -0
- data/Rakefile +11 -0
- data/VERSION +1 -0
- data/activerecord-cubrid2-adapter.gemspec +26 -0
- data/lib/active_record/connection_adapters/abstract_cubrid2_adapter.rb +750 -0
- data/lib/active_record/connection_adapters/cubrid2/column.rb +28 -0
- data/lib/active_record/connection_adapters/cubrid2/database_statements.rb +192 -0
- data/lib/active_record/connection_adapters/cubrid2/explain_pretty_printer.rb +71 -0
- data/lib/active_record/connection_adapters/cubrid2/quoting.rb +118 -0
- data/lib/active_record/connection_adapters/cubrid2/schema_creation.rb +98 -0
- data/lib/active_record/connection_adapters/cubrid2/schema_definitions.rb +81 -0
- data/lib/active_record/connection_adapters/cubrid2/schema_dumper.rb +31 -0
- data/lib/active_record/connection_adapters/cubrid2/schema_statements.rb +276 -0
- data/lib/active_record/connection_adapters/cubrid2/type_metadata.rb +31 -0
- data/lib/active_record/connection_adapters/cubrid2/version.rb +7 -0
- data/lib/active_record/connection_adapters/cubrid2_adapter.rb +169 -0
- data/lib/activerecord-cubrid2-adapter.rb +4 -0
- data/lib/arel/visitors/cubrid.rb +67 -0
- data/lib/cubrid2/client.rb +93 -0
- data/lib/cubrid2/console.rb +5 -0
- data/lib/cubrid2/error.rb +86 -0
- data/lib/cubrid2/field.rb +3 -0
- data/lib/cubrid2/result.rb +7 -0
- data/lib/cubrid2/statement.rb +11 -0
- data/lib/cubrid2/version.rb +3 -0
- data/lib/cubrid2.rb +76 -0
- data/tests/Gemfile +10 -0
- data/tests/test_activerecord.rb +109 -0
- metadata +102 -0
@@ -0,0 +1,750 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
4
|
+
require 'active_record/connection_adapters/statement_pool'
|
5
|
+
require 'active_record/connection_adapters/cubrid2/column'
|
6
|
+
require 'active_record/connection_adapters/cubrid2/explain_pretty_printer'
|
7
|
+
require 'active_record/connection_adapters/cubrid2/quoting'
|
8
|
+
require 'active_record/connection_adapters/cubrid2/schema_creation'
|
9
|
+
require 'active_record/connection_adapters/cubrid2/schema_definitions'
|
10
|
+
require 'active_record/connection_adapters/cubrid2/schema_dumper'
|
11
|
+
require 'active_record/connection_adapters/cubrid2/schema_statements'
|
12
|
+
require 'active_record/connection_adapters/cubrid2/type_metadata'
|
13
|
+
require 'active_record/connection_adapters/cubrid2/version'
|
14
|
+
require 'arel/visitors/cubrid'
|
15
|
+
|
16
|
+
module ActiveRecord
|
17
|
+
module ConnectionAdapters
|
18
|
+
class AbstractCubrid2Adapter < AbstractAdapter
|
19
|
+
include Cubrid2::Quoting
|
20
|
+
include Cubrid2::SchemaStatements
|
21
|
+
|
22
|
+
##
|
23
|
+
# :singleton-method:
|
24
|
+
# By default, the CubridAdapter will consider all columns of type <tt>tinyint(1)</tt>
|
25
|
+
# as boolean. If you wish to disable this emulation you can add the following line
|
26
|
+
# to your application.rb file:
|
27
|
+
#
|
28
|
+
# ActiveRecord::ConnectionAdapters::CubridAdapter.emulate_booleans = false
|
29
|
+
class_attribute :emulate_booleans, default: true
|
30
|
+
|
31
|
+
NATIVE_DATABASE_TYPES = {
|
32
|
+
primary_key: 'bigint auto_increment PRIMARY KEY',
|
33
|
+
string: { name: 'varchar', limit: 255 }, # 1_073_741_823
|
34
|
+
text: { name: 'text' },
|
35
|
+
integer: { name: 'int', limit: 4 },
|
36
|
+
float: { name: 'float', limit: 24 },
|
37
|
+
decimal: { name: 'decimal' },
|
38
|
+
datetime: { name: 'datetime' },
|
39
|
+
timestamp: { name: 'timestamp' },
|
40
|
+
time: { name: 'time' },
|
41
|
+
date: { name: 'date' },
|
42
|
+
binary: { name: 'blob' },
|
43
|
+
blob: { name: 'blob' },
|
44
|
+
boolean: { name: 'smallint' },
|
45
|
+
json: { name: 'json' }
|
46
|
+
}
|
47
|
+
|
48
|
+
class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
|
49
|
+
private
|
50
|
+
|
51
|
+
def dealloc(stmt)
|
52
|
+
stmt.close
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def initialize(connection, logger, _connection_options, config)
|
57
|
+
super(connection, logger, config)
|
58
|
+
end
|
59
|
+
|
60
|
+
def get_database_version # :nodoc:
|
61
|
+
full_version_string = get_full_version
|
62
|
+
version_string = version_string(full_version_string)
|
63
|
+
Version.new(version_string, full_version_string)
|
64
|
+
end
|
65
|
+
|
66
|
+
def supports_bulk_alter?
|
67
|
+
true
|
68
|
+
end
|
69
|
+
|
70
|
+
def supports_index_sort_order?
|
71
|
+
false
|
72
|
+
end
|
73
|
+
|
74
|
+
def supports_expression_index?
|
75
|
+
false
|
76
|
+
end
|
77
|
+
|
78
|
+
def supports_transaction_isolation?
|
79
|
+
true
|
80
|
+
end
|
81
|
+
|
82
|
+
def supports_explain?
|
83
|
+
true
|
84
|
+
end
|
85
|
+
|
86
|
+
def supports_indexes_in_create?
|
87
|
+
true
|
88
|
+
end
|
89
|
+
|
90
|
+
def supports_foreign_keys?
|
91
|
+
true
|
92
|
+
end
|
93
|
+
|
94
|
+
def supports_views?
|
95
|
+
true
|
96
|
+
end
|
97
|
+
|
98
|
+
def supports_datetime_with_precision?
|
99
|
+
false
|
100
|
+
end
|
101
|
+
|
102
|
+
def supports_virtual_columns?
|
103
|
+
true
|
104
|
+
end
|
105
|
+
|
106
|
+
def supports_optimizer_hints?
|
107
|
+
false
|
108
|
+
end
|
109
|
+
|
110
|
+
def supports_common_table_expressions?
|
111
|
+
database_version >= '10.2'
|
112
|
+
end
|
113
|
+
|
114
|
+
def supports_advisory_locks?
|
115
|
+
false
|
116
|
+
end
|
117
|
+
|
118
|
+
def supports_insert_on_duplicate_skip?
|
119
|
+
true
|
120
|
+
end
|
121
|
+
|
122
|
+
def supports_insert_on_duplicate_update?
|
123
|
+
true
|
124
|
+
end
|
125
|
+
|
126
|
+
def supports_rename_column?
|
127
|
+
true
|
128
|
+
end
|
129
|
+
|
130
|
+
# In cubrid: locking is done automatically
|
131
|
+
# See: https://www.cubrid.org/manual/en/11.2/sql/transaction.html#id13
|
132
|
+
def get_advisory_lock(_lock_name, _timeout = 0) # :nodoc:
|
133
|
+
# query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1
|
134
|
+
true
|
135
|
+
end
|
136
|
+
|
137
|
+
def release_advisory_lock(_lock_name) # :nodoc:
|
138
|
+
# query_value("SELECT RELEASE_LOCK(#{quote(lock_name.to_s)})") == 1
|
139
|
+
true
|
140
|
+
end
|
141
|
+
|
142
|
+
def native_database_types
|
143
|
+
NATIVE_DATABASE_TYPES
|
144
|
+
end
|
145
|
+
|
146
|
+
def index_algorithms
|
147
|
+
{ }
|
148
|
+
end
|
149
|
+
|
150
|
+
# HELPER METHODS ===========================================
|
151
|
+
|
152
|
+
# The two drivers have slightly different ways of yielding hashes of results, so
|
153
|
+
# this method must be implemented to provide a uniform interface.
|
154
|
+
def each_hash(result) # :nodoc:
|
155
|
+
raise NotImplementedError
|
156
|
+
end
|
157
|
+
|
158
|
+
# Must return the Cubrid error number from the exception, if the exception has an
|
159
|
+
# error number.
|
160
|
+
def error_number(exception) # :nodoc:
|
161
|
+
raise NotImplementedError
|
162
|
+
end
|
163
|
+
|
164
|
+
# REFERENTIAL INTEGRITY ====================================
|
165
|
+
|
166
|
+
# def disable_referential_integrity # :nodoc:
|
167
|
+
# old = query_value('SELECT @@FOREIGN_KEY_CHECKS')
|
168
|
+
|
169
|
+
# begin
|
170
|
+
# update('SET FOREIGN_KEY_CHECKS = 0')
|
171
|
+
# yield
|
172
|
+
# ensure
|
173
|
+
# update("SET FOREIGN_KEY_CHECKS = #{old}")
|
174
|
+
# end
|
175
|
+
# end
|
176
|
+
|
177
|
+
# CONNECTION MANAGEMENT ====================================
|
178
|
+
|
179
|
+
def clear_cache! # :nodoc:
|
180
|
+
reload_type_map
|
181
|
+
super
|
182
|
+
end
|
183
|
+
|
184
|
+
#--
|
185
|
+
# DATABASE STATEMENTS ======================================
|
186
|
+
#++
|
187
|
+
|
188
|
+
def explain(arel, binds = [])
|
189
|
+
sql = "EXPLAIN #{to_sql(arel, binds)}"
|
190
|
+
start = Concurrent.monotonic_time
|
191
|
+
result = exec_query(sql, 'EXPLAIN', binds)
|
192
|
+
elapsed = Concurrent.monotonic_time - start
|
193
|
+
|
194
|
+
Cubrid2::ExplainPrettyPrinter.new.pp(result, elapsed)
|
195
|
+
end
|
196
|
+
|
197
|
+
# Executes the SQL statement in the context of this connection.
|
198
|
+
def execute(sql, name = nil)
|
199
|
+
materialize_transactions
|
200
|
+
|
201
|
+
stmt = nil
|
202
|
+
|
203
|
+
log(sql, name) do
|
204
|
+
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
|
205
|
+
stmt = @connection.query(sql)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
stmt
|
210
|
+
end
|
211
|
+
|
212
|
+
# CubridAdapter doesn't have to free a result after using it, but we use this method
|
213
|
+
# to write stuff in an abstract way without concerning ourselves about whether it
|
214
|
+
# needs to be explicitly freed or not.
|
215
|
+
def execute_and_free(sql, name = nil) # :nodoc:
|
216
|
+
yield execute(sql, name)
|
217
|
+
end
|
218
|
+
|
219
|
+
def begin_db_transaction
|
220
|
+
# NOTE: no begin statement in cubrid
|
221
|
+
# execute "BEGIN"
|
222
|
+
end
|
223
|
+
|
224
|
+
def begin_isolated_db_transaction(isolation)
|
225
|
+
execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}"
|
226
|
+
begin_db_transaction
|
227
|
+
end
|
228
|
+
|
229
|
+
def commit_db_transaction # :nodoc:
|
230
|
+
execute 'COMMIT'
|
231
|
+
end
|
232
|
+
|
233
|
+
def exec_rollback_db_transaction # :nodoc:
|
234
|
+
execute 'ROLLBACK'
|
235
|
+
end
|
236
|
+
|
237
|
+
def empty_insert_statement_value(_primary_key = nil)
|
238
|
+
'VALUES ()'
|
239
|
+
end
|
240
|
+
|
241
|
+
# SCHEMA STATEMENTS ========================================
|
242
|
+
|
243
|
+
# Drops the database: not supported now
|
244
|
+
def recreate_database(_name, _options = {})
|
245
|
+
raise 'In Cubrid create/drop database not supported'
|
246
|
+
end
|
247
|
+
|
248
|
+
# Create a new Cubrid database: not supported now
|
249
|
+
def create_database(_name, _options = {})
|
250
|
+
raise 'In Cubrid create/drop database not supported'
|
251
|
+
end
|
252
|
+
|
253
|
+
# Drops a Cubrid database: not supported now
|
254
|
+
def drop_database(_name) # :nodoc:
|
255
|
+
raise 'In Cubrid create/drop database not supported'
|
256
|
+
end
|
257
|
+
|
258
|
+
def current_database
|
259
|
+
query_value('SELECT database()', 'SCHEMA')
|
260
|
+
end
|
261
|
+
|
262
|
+
# Returns the database character set.
|
263
|
+
def charset
|
264
|
+
# check character set:
|
265
|
+
# See: https://www.cubrid.com/qna/3802763
|
266
|
+
@charset ||= query_value("select charset('ABC')", 'SCHEMA')
|
267
|
+
end
|
268
|
+
|
269
|
+
# Returns the database collation strategy.
|
270
|
+
def collation
|
271
|
+
# check collation set:
|
272
|
+
# See: https://www.cubrid.com/qna/3802763
|
273
|
+
@collation ||= query_value("select collation('ABC')", 'SCHEMA')
|
274
|
+
end
|
275
|
+
|
276
|
+
def table_comment(table_name) # :nodoc:
|
277
|
+
raise 'table comment not supported' unless supports_comments?
|
278
|
+
|
279
|
+
query_value(<<~SQL, 'SCHEMA').presence
|
280
|
+
SELECT comment
|
281
|
+
FROM db_class
|
282
|
+
WHERE owner_name = 'PUBLIC'
|
283
|
+
AND class_type = 'CLASS'
|
284
|
+
AND is_system_class = 'NO'
|
285
|
+
AND class_name = #{quote_table_name(table_name)}
|
286
|
+
SQL
|
287
|
+
end
|
288
|
+
|
289
|
+
def change_table_comment(table_name, comment_or_changes) # :nodoc:
|
290
|
+
comment = extract_new_comment_value(comment_or_changes)
|
291
|
+
comment = '' if comment.nil?
|
292
|
+
execute("ALTER TABLE #{quote_table_name(table_name)} COMMENT=#{quote(comment)}")
|
293
|
+
end
|
294
|
+
|
295
|
+
# Renames a table.
|
296
|
+
#
|
297
|
+
# Example:
|
298
|
+
# rename_table('octopuses', 'octopi')
|
299
|
+
def rename_table(table_name, new_name)
|
300
|
+
execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
|
301
|
+
rename_table_indexes(table_name, new_name)
|
302
|
+
end
|
303
|
+
|
304
|
+
# Drops a table from the database.
|
305
|
+
#
|
306
|
+
# [<tt>:force</tt>]
|
307
|
+
# Set to +:cascade+ to drop dependent objects as well.
|
308
|
+
# Defaults to false.
|
309
|
+
# [<tt>:if_exists</tt>]
|
310
|
+
# Set to +true+ to only drop the table if it exists.
|
311
|
+
# Defaults to false.
|
312
|
+
# [<tt>:temporary</tt>]
|
313
|
+
# Set to +true+ to drop temporary table.
|
314
|
+
# Defaults to false.
|
315
|
+
#
|
316
|
+
# Although this command ignores most +options+ and the block if one is given,
|
317
|
+
# it can be helpful to provide these in a migration's +change+ method so it can be reverted.
|
318
|
+
# In that case, +options+ and the block will be used by create_table.
|
319
|
+
def drop_table(table_name, options = {})
|
320
|
+
if_exists = (options[:if_exists] ? 'IF EXISTS' : '')
|
321
|
+
cascade = (options[:force] == :cascade ? 'CASCADE CONSTRAINTS' : '')
|
322
|
+
execute "DROP TABLE #{if_exists} #{quote_table_name(table_name)} #{cascade}"
|
323
|
+
end
|
324
|
+
|
325
|
+
def rename_index(table_name, old_name, new_name)
|
326
|
+
if supports_rename_index?
|
327
|
+
validate_index_length!(table_name, new_name)
|
328
|
+
|
329
|
+
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME INDEX #{quote_table_name(old_name)} TO #{quote_table_name(new_name)}"
|
330
|
+
else
|
331
|
+
super
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
def change_column_default(table_name, column_name, default_or_changes) # :nodoc:
|
336
|
+
default = extract_new_default_value(default_or_changes)
|
337
|
+
change_column table_name, column_name, nil, default: default
|
338
|
+
end
|
339
|
+
|
340
|
+
def change_column_null(table_name, column_name, null, default = nil) # :nodoc:
|
341
|
+
unless null || default.nil?
|
342
|
+
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
|
343
|
+
end
|
344
|
+
|
345
|
+
change_column table_name, column_name, nil, null: null
|
346
|
+
end
|
347
|
+
|
348
|
+
def change_column_comment(table_name, column_name, comment_or_changes) # :nodoc:
|
349
|
+
comment = extract_new_comment_value(comment_or_changes)
|
350
|
+
change_column table_name, column_name, nil, comment: comment
|
351
|
+
end
|
352
|
+
|
353
|
+
def change_column(table_name, column_name, type, options = {}) # :nodoc:
|
354
|
+
execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_for_alter(table_name, column_name, type, **options)}")
|
355
|
+
end
|
356
|
+
|
357
|
+
def rename_column(table_name, column_name, new_column_name) # :nodoc:
|
358
|
+
execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_for_alter(table_name, column_name, new_column_name)}")
|
359
|
+
rename_column_indexes(table_name, column_name, new_column_name)
|
360
|
+
end
|
361
|
+
|
362
|
+
def add_index(table_name, column_name, options = {}) # :nodoc:
|
363
|
+
index, algorithm, if_not_exists = add_index_options(table_name, column_name, **options)
|
364
|
+
|
365
|
+
return if if_not_exists && index_exists?(table_name, column_name, name: index.name)
|
366
|
+
|
367
|
+
create_index = CreateIndexDefinition.new(index, algorithm)
|
368
|
+
execute schema_creation.accept(create_index)
|
369
|
+
end
|
370
|
+
|
371
|
+
def add_sql_comment!(sql, comment) # :nodoc:
|
372
|
+
return sql unless supports_comments?
|
373
|
+
|
374
|
+
sql << " COMMENT #{quote(comment)}" if comment.present?
|
375
|
+
sql
|
376
|
+
end
|
377
|
+
|
378
|
+
def foreign_keys(table_name)
|
379
|
+
raise ArgumentError unless table_name.present?
|
380
|
+
|
381
|
+
# In Cubrid, we cannot know referencing table that foreign key indicates from the system catalog.
|
382
|
+
# See: https://www.cubrid.com/qna/3822484
|
383
|
+
# So we use the raw sql generated by 'SHOW CREATE TABLE ...'
|
384
|
+
|
385
|
+
tableinfo = create_table_info(table_name)
|
386
|
+
lines = tableinfo.gsub('CONSTRAINT', "\nCONSTRAINT").split('CONSTRAINT')
|
387
|
+
|
388
|
+
fkeys = []
|
389
|
+
lines.each do |line|
|
390
|
+
fk_matches = line.match(/(.*) FOREIGN KEY (.*)/)
|
391
|
+
next if fk_matches.nil?
|
392
|
+
|
393
|
+
name = _strip_key_str(fk_matches[1])
|
394
|
+
detail_match = fk_matches[2].match(/(.*) REFERENCES (.*) ON DELETE (.*) ON UPDATE (.*)\s*/)
|
395
|
+
|
396
|
+
column = _strip_key_str(detail_match[1])
|
397
|
+
to_table_match = detail_match[2]&.match(/(.*)\s+\((.*)\)/)
|
398
|
+
|
399
|
+
to_table = _strip_key_str(to_table_match[1])
|
400
|
+
primary_key = _strip_key_str(to_table_match[2])
|
401
|
+
|
402
|
+
options = {
|
403
|
+
name: name,
|
404
|
+
column: column,
|
405
|
+
primary_key: primary_key
|
406
|
+
}
|
407
|
+
|
408
|
+
options[:on_update] = extract_foreign_key_action(_strip_left_str(detail_match[3]))
|
409
|
+
options[:on_delete] = extract_foreign_key_action(_strip_left_str(detail_match[4]))
|
410
|
+
|
411
|
+
fkeys << ForeignKeyDefinition.new(table_name, to_table, options)
|
412
|
+
end
|
413
|
+
|
414
|
+
fkeys
|
415
|
+
end
|
416
|
+
|
417
|
+
def _strip_key_str(str)
|
418
|
+
str.gsub(/[\[\]]/, '')
|
419
|
+
.gsub(/[()]/, '')
|
420
|
+
.gsub(/^\s+/, '').gsub(/\s+$/, '')
|
421
|
+
end
|
422
|
+
|
423
|
+
def _strip_left_str(str)
|
424
|
+
str.gsub(/([;,)].*)$/, '')
|
425
|
+
end
|
426
|
+
|
427
|
+
def table_options(table_name) # :nodoc:
|
428
|
+
table_options = {}
|
429
|
+
|
430
|
+
tableinfo = create_table_info(table_name)
|
431
|
+
|
432
|
+
# strip create_definitions and partition_options
|
433
|
+
# Be aware that `create_table_info` might not include any table options due to `NO_TABLE_OPTIONS` sql mode.
|
434
|
+
raw_table_options = tableinfo.sub(/\A.*\n\) ?/m, '').sub(%r{\n/\*!.*\*/\n\z}m, '').strip
|
435
|
+
|
436
|
+
table_options[:options] = raw_table_options unless raw_table_options.blank?
|
437
|
+
|
438
|
+
# strip COMMENT
|
439
|
+
table_options[:comment] = table_comment(table_name) if raw_table_options.sub!(/ COMMENT='.+'/, '')
|
440
|
+
|
441
|
+
table_options
|
442
|
+
end
|
443
|
+
|
444
|
+
# SHOW VARIABLES LIKE 'name'
|
445
|
+
def show_variable(_name)
|
446
|
+
raise 'Not supported'
|
447
|
+
end
|
448
|
+
|
449
|
+
def primary_keys(table_name) # :nodoc:
|
450
|
+
raise ArgumentError unless table_name.present?
|
451
|
+
|
452
|
+
prikeys = []
|
453
|
+
column_definitions(table_name).each do |col|
|
454
|
+
prikeys << col[:Field] if col[:Key] == 'PRI'
|
455
|
+
end
|
456
|
+
prikeys
|
457
|
+
end
|
458
|
+
|
459
|
+
def default_uniqueness_comparison(attribute, value, klass) # :nodoc:
|
460
|
+
column = column_for_attribute(attribute)
|
461
|
+
|
462
|
+
if column.collation && !column.case_sensitive? && !value.nil?
|
463
|
+
ActiveSupport::Deprecation.warn(<<~MSG.squish)
|
464
|
+
Uniqueness validator will no longer enforce case sensitive comparison in Rails 6.1.
|
465
|
+
To continue case sensitive comparison on the :#{attribute.name} attribute in #{klass} model,
|
466
|
+
pass `case_sensitive: true` option explicitly to the uniqueness validator.
|
467
|
+
MSG
|
468
|
+
attribute.eq(Arel::Nodes::Bin.new(value))
|
469
|
+
else
|
470
|
+
super
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
474
|
+
def case_sensitive_comparison(attribute, value) # :nodoc:
|
475
|
+
column = column_for_attribute(attribute)
|
476
|
+
|
477
|
+
if column.collation && !column.case_sensitive?
|
478
|
+
attribute.eq(Arel::Nodes::Bin.new(value))
|
479
|
+
else
|
480
|
+
super
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
def can_perform_case_insensitive_comparison_for?(column)
|
485
|
+
column.case_sensitive?
|
486
|
+
end
|
487
|
+
private :can_perform_case_insensitive_comparison_for?
|
488
|
+
|
489
|
+
def columns_for_distinct(columns, orders) # :nodoc:
|
490
|
+
order_columns = orders.reject(&:blank?).map do |s|
|
491
|
+
# Convert Arel node to string
|
492
|
+
s = visitor.compile(s) unless s.is_a?(String)
|
493
|
+
# Remove any ASC/DESC modifiers
|
494
|
+
s.gsub(/\s+(?:ASC|DESC)\b/i, '')
|
495
|
+
end.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
|
496
|
+
|
497
|
+
(order_columns << super).join(', ')
|
498
|
+
end
|
499
|
+
|
500
|
+
# def strict_mode?
|
501
|
+
# self.class.type_cast_config_to_boolean(@config.fetch(:strict, true))
|
502
|
+
# end
|
503
|
+
|
504
|
+
def default_index_type?(index) # :nodoc:
|
505
|
+
index.using == :btree || super
|
506
|
+
end
|
507
|
+
|
508
|
+
def build_insert_sql(insert) # :nodoc:
|
509
|
+
sql = +"INSERT #{insert.into} #{insert.values_list}"
|
510
|
+
|
511
|
+
if insert.skip_duplicates?
|
512
|
+
no_op_column = quote_column_name(insert.keys.first)
|
513
|
+
sql << " ON DUPLICATE KEY UPDATE #{no_op_column}=#{no_op_column}"
|
514
|
+
elsif insert.update_duplicates?
|
515
|
+
sql << ' ON DUPLICATE KEY UPDATE '
|
516
|
+
sql << insert.updatable_columns.map { |column| "#{column}=VALUES(#{column})" }.join(',')
|
517
|
+
end
|
518
|
+
|
519
|
+
sql
|
520
|
+
end
|
521
|
+
|
522
|
+
def check_version # :nodoc:
|
523
|
+
if database_version < '9.0'
|
524
|
+
raise "Your version of Cubrid (#{database_version}) is too old. Active Record supports Cubrid >= 9.0."
|
525
|
+
end
|
526
|
+
end
|
527
|
+
|
528
|
+
private
|
529
|
+
|
530
|
+
def initialize_type_map(m = type_map)
|
531
|
+
super
|
532
|
+
|
533
|
+
register_class_with_limit m, /char/i, CubridString
|
534
|
+
|
535
|
+
m.register_type(/blob/i, Type::Binary.new(limit: 2**30 - 1))
|
536
|
+
m.register_type(/clob/i, Type::Binary.new(limit: 2**30 - 1))
|
537
|
+
m.register_type(/^float/i, Type::Float.new(limit: 24))
|
538
|
+
m.register_type(/^double/i, Type::Float.new(limit: 53))
|
539
|
+
|
540
|
+
register_integer_type m, /^bigint/i, limit: 8
|
541
|
+
register_integer_type m, /^int/i, limit: 4
|
542
|
+
register_integer_type m, /^smallint/i, limit: 2
|
543
|
+
|
544
|
+
m.register_type(/^smallint\(1\)/i, Type::Boolean.new) if emulate_booleans
|
545
|
+
m.alias_type(/year/i, 'integer')
|
546
|
+
m.alias_type(/bit/i, 'binary')
|
547
|
+
|
548
|
+
m.register_type(/enum/i) do |sql_type|
|
549
|
+
limit = sql_type[/^enum\s*\((.+)\)/i, 1]
|
550
|
+
.split(',').map { |enum| enum.strip.length - 2 }.max
|
551
|
+
CubridString.new(limit: limit)
|
552
|
+
# String.new(limit: limit)
|
553
|
+
end
|
554
|
+
|
555
|
+
m.register_type(/^set/i) do |sql_type|
|
556
|
+
limit = sql_type[/^set\s*\((.+)\)/i, 1]
|
557
|
+
.split(',').map { |set| set.strip.length - 1 }.sum - 1
|
558
|
+
CubridString.new(limit: limit)
|
559
|
+
# String.new(limit: limit)
|
560
|
+
end
|
561
|
+
end
|
562
|
+
|
563
|
+
def register_integer_type(mapping, key, **options)
|
564
|
+
mapping.register_type(key) do |_sql_type|
|
565
|
+
# if /\bunsigned\b/.match?(sql_type)
|
566
|
+
# Type::UnsignedInteger.new(**options)
|
567
|
+
# else
|
568
|
+
Type::Integer.new(**options)
|
569
|
+
# end
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
def extract_precision(sql_type)
|
574
|
+
if /\A(?:date)?time(?:stamp)?\b/.match?(sql_type)
|
575
|
+
super || 0
|
576
|
+
else
|
577
|
+
super
|
578
|
+
end
|
579
|
+
end
|
580
|
+
|
581
|
+
# See https://www.cubrid.com/tutorial/3793681
|
582
|
+
# ER_FILSORT_ABORT = 1028
|
583
|
+
ER_DUP_ENTRY = 212
|
584
|
+
ER_NOT_NULL_VIOLATION = 631
|
585
|
+
# ER_NO_REFERENCED_ROW = 1216
|
586
|
+
# ER_ROW_IS_REFERENCED = 1217
|
587
|
+
ER_DO_NOT_HAVE_DEFAULT = 1364
|
588
|
+
# ER_ROW_IS_REFERENCED_2 = 1451
|
589
|
+
# ER_NO_REFERENCED_ROW_2 = 1452
|
590
|
+
ER_DATA_TOO_LONG = 781, 531
|
591
|
+
ER_OUT_OF_RANGE = 935
|
592
|
+
ER_LOCK_DEADLOCK = [72..76]
|
593
|
+
ER_CANNOT_ADD_FOREIGN = [920, 921, 922, 927]
|
594
|
+
ER_CANNOT_CREATE_TABLE = 65,
|
595
|
+
# ER_LOCK_WAIT_TIMEOUT = 1205
|
596
|
+
ER_QUERY_INTERRUPTED = 790
|
597
|
+
# ER_QUERY_TIMEOUT = 3024
|
598
|
+
ER_FK_INCOMPATIBLE_COLUMNS = [918, 923]
|
599
|
+
|
600
|
+
def translate_exception(exception, message:, sql:, binds:)
|
601
|
+
case error_number(exception)
|
602
|
+
when ER_DUP_ENTRY
|
603
|
+
RecordNotUnique.new(message, sql: sql, binds: binds)
|
604
|
+
# when ER_NO_REFERENCED_ROW, ER_ROW_IS_REFERENCED, ER_ROW_IS_REFERENCED_2, ER_NO_REFERENCED_ROW_2
|
605
|
+
# InvalidForeignKey.new(message, sql: sql, binds: binds)
|
606
|
+
when ER_CANNOT_ADD_FOREIGN, ER_FK_INCOMPATIBLE_COLUMNS
|
607
|
+
mismatched_foreign_key(message, sql: sql, binds: binds)
|
608
|
+
when ER_CANNOT_CREATE_TABLE
|
609
|
+
super
|
610
|
+
when ER_DATA_TOO_LONG
|
611
|
+
ValueTooLong.new(message, sql: sql, binds: binds)
|
612
|
+
when ER_OUT_OF_RANGE
|
613
|
+
RangeError.new(message, sql: sql, binds: binds)
|
614
|
+
when ER_NOT_NULL_VIOLATION, ER_DO_NOT_HAVE_DEFAULT
|
615
|
+
NotNullViolation.new(message, sql: sql, binds: binds)
|
616
|
+
when ER_LOCK_DEADLOCK
|
617
|
+
Deadlocked.new(message, sql: sql, binds: binds)
|
618
|
+
# when ER_LOCK_WAIT_TIMEOUT
|
619
|
+
# LockWaitTimeout.new(message, sql: sql, binds: binds)
|
620
|
+
# when ER_QUERY_TIMEOUT #, ER_FILSORT_ABORT
|
621
|
+
# StatementTimeout.new(message, sql: sql, binds: binds)
|
622
|
+
when ER_QUERY_INTERRUPTED
|
623
|
+
QueryCanceled.new(message, sql: sql, binds: binds)
|
624
|
+
else
|
625
|
+
super
|
626
|
+
end
|
627
|
+
end
|
628
|
+
|
629
|
+
def change_column_for_alter(table_name, column_name, type, options = {})
|
630
|
+
column = column_for(table_name, column_name)
|
631
|
+
type ||= column.sql_type
|
632
|
+
|
633
|
+
options[:default] = column.default unless options.key?(:default)
|
634
|
+
options[:null] = column.null unless options.key?(:null)
|
635
|
+
options[:comment] = column.comment unless options.key?(:comment)
|
636
|
+
|
637
|
+
td = create_table_definition(table_name)
|
638
|
+
cd = td.new_column_definition(column.name, type, **options)
|
639
|
+
schema_creation.accept(ChangeColumnDefinition.new(cd, column.name))
|
640
|
+
end
|
641
|
+
|
642
|
+
def rename_column_for_alter(table_name, column_name, new_column_name)
|
643
|
+
return rename_column_sql(table_name, column_name, new_column_name) if supports_rename_column?
|
644
|
+
|
645
|
+
column = column_for(table_name, column_name)
|
646
|
+
options = {
|
647
|
+
default: column.default,
|
648
|
+
null: column.null,
|
649
|
+
auto_increment: column.auto_increment?,
|
650
|
+
comment: column.comment
|
651
|
+
}
|
652
|
+
|
653
|
+
current_type = exec_query("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE #{quote(column_name)}", "SCHEMA").first["Type"]
|
654
|
+
td = create_table_definition(table_name)
|
655
|
+
cd = td.new_column_definition(new_column_name, current_type, **options)
|
656
|
+
schema_creation.accept(ChangeColumnDefinition.new(cd, column.name))
|
657
|
+
end
|
658
|
+
|
659
|
+
def add_index_for_alter(table_name, column_name, **options)
|
660
|
+
index, algorithm, _ = add_index_options(table_name, column_name, **options)
|
661
|
+
algorithm = ", #{algorithm}" if algorithm
|
662
|
+
|
663
|
+
"ADD #{schema_creation.accept(index)}#{algorithm}"
|
664
|
+
end
|
665
|
+
|
666
|
+
def remove_index_for_alter(table_name, column_name = nil, **options)
|
667
|
+
index_name = index_name_for_remove(table_name, column_name, options)
|
668
|
+
"DROP INDEX #{quote_column_name(index_name)}"
|
669
|
+
end
|
670
|
+
|
671
|
+
def supports_rename_index?
|
672
|
+
# https://www.cubrid.org/manual/en/10.0/sql/schema/index_stmt.html#alter-index
|
673
|
+
database_version >= '10.0'
|
674
|
+
end
|
675
|
+
|
676
|
+
def configure_connection; end
|
677
|
+
|
678
|
+
def column_definitions(table_name) # :nodoc:
|
679
|
+
execute_and_free("EXPLAIN #{quote_table_name(table_name)}", 'SCHEMA') do |result|
|
680
|
+
each_hash(result)
|
681
|
+
end
|
682
|
+
end
|
683
|
+
|
684
|
+
def create_table_info(table_name) # :nodoc:
|
685
|
+
res = exec_query("SHOW CREATE TABLE #{quote_table_name(table_name)}", 'SCHEMA')
|
686
|
+
res.first['CREATE TABLE']
|
687
|
+
end
|
688
|
+
|
689
|
+
def arel_visitor
|
690
|
+
Arel::Visitors::Cubrid.new(self)
|
691
|
+
end
|
692
|
+
|
693
|
+
def build_statement_pool
|
694
|
+
StatementPool.new(self.class.type_cast_config_to_integer(@config[:statement_limit]))
|
695
|
+
end
|
696
|
+
|
697
|
+
def mismatched_foreign_key(message, sql:, binds:)
|
698
|
+
q1 = '[`"\[]'
|
699
|
+
q2 = '[`"\]]'
|
700
|
+
match = /
|
701
|
+
(?:CREATE|ALTER)\s+TABLE\s*(?:#{q1}?\w+#{q2}?\.)?#{q1}?(?<table>\w+)#{q2}?.+?
|
702
|
+
FOREIGN\s+KEY\s*\(#{q1}?(?<foreign_key>\w+)#{q2}?\)\s*
|
703
|
+
REFERENCES\s*(#{q1}?(?<target_table>\w+)#{q2}?)\s*\(#{q1}?(?<primary_key>\w+)#{q2}?\)
|
704
|
+
/xmi.match(sql)
|
705
|
+
|
706
|
+
options = {
|
707
|
+
message: message,
|
708
|
+
sql: sql,
|
709
|
+
binds: binds
|
710
|
+
}
|
711
|
+
|
712
|
+
if match
|
713
|
+
options[:table] = match[:table]
|
714
|
+
options[:foreign_key] = match[:foreign_key]
|
715
|
+
options[:target_table] = match[:target_table]
|
716
|
+
options[:primary_key] = match[:primary_key]
|
717
|
+
options[:primary_key_column] = column_for(match[:target_table], match[:primary_key])
|
718
|
+
end
|
719
|
+
|
720
|
+
MismatchedForeignKey.new(**options)
|
721
|
+
end
|
722
|
+
|
723
|
+
def version_string(full_version_string)
|
724
|
+
full_version_string.match(/^(?:5\.5\.5-)?(\d+\.\d+\.\d+.\d+)/)[1]
|
725
|
+
end
|
726
|
+
|
727
|
+
class CubridString < Type::String # :nodoc:
|
728
|
+
def serialize(value)
|
729
|
+
case value
|
730
|
+
when true then '1'
|
731
|
+
when false then '0'
|
732
|
+
else super
|
733
|
+
end
|
734
|
+
end
|
735
|
+
|
736
|
+
private
|
737
|
+
|
738
|
+
def cast_value(value)
|
739
|
+
case value
|
740
|
+
when true then '1'
|
741
|
+
when false then '0'
|
742
|
+
else super
|
743
|
+
end
|
744
|
+
end
|
745
|
+
end
|
746
|
+
|
747
|
+
ActiveRecord::Type.register(:string, CubridString, adapter: :cubrid)
|
748
|
+
end
|
749
|
+
end
|
750
|
+
end
|