activerecord-cubrid2-adapter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/LICENSE +21 -0
  4. data/README.md +63 -0
  5. data/Rakefile +11 -0
  6. data/VERSION +1 -0
  7. data/activerecord-cubrid2-adapter.gemspec +26 -0
  8. data/lib/active_record/connection_adapters/abstract_cubrid2_adapter.rb +750 -0
  9. data/lib/active_record/connection_adapters/cubrid2/column.rb +28 -0
  10. data/lib/active_record/connection_adapters/cubrid2/database_statements.rb +192 -0
  11. data/lib/active_record/connection_adapters/cubrid2/explain_pretty_printer.rb +71 -0
  12. data/lib/active_record/connection_adapters/cubrid2/quoting.rb +118 -0
  13. data/lib/active_record/connection_adapters/cubrid2/schema_creation.rb +98 -0
  14. data/lib/active_record/connection_adapters/cubrid2/schema_definitions.rb +81 -0
  15. data/lib/active_record/connection_adapters/cubrid2/schema_dumper.rb +31 -0
  16. data/lib/active_record/connection_adapters/cubrid2/schema_statements.rb +276 -0
  17. data/lib/active_record/connection_adapters/cubrid2/type_metadata.rb +31 -0
  18. data/lib/active_record/connection_adapters/cubrid2/version.rb +7 -0
  19. data/lib/active_record/connection_adapters/cubrid2_adapter.rb +169 -0
  20. data/lib/activerecord-cubrid2-adapter.rb +4 -0
  21. data/lib/arel/visitors/cubrid.rb +67 -0
  22. data/lib/cubrid2/client.rb +93 -0
  23. data/lib/cubrid2/console.rb +5 -0
  24. data/lib/cubrid2/error.rb +86 -0
  25. data/lib/cubrid2/field.rb +3 -0
  26. data/lib/cubrid2/result.rb +7 -0
  27. data/lib/cubrid2/statement.rb +11 -0
  28. data/lib/cubrid2/version.rb +3 -0
  29. data/lib/cubrid2.rb +76 -0
  30. data/tests/Gemfile +10 -0
  31. data/tests/test_activerecord.rb +109 -0
  32. 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