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.
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,276 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Cubrid2
6
+ module SchemaStatements # :nodoc:
7
+ # Returns an array of indexes for the given table.
8
+ def indexes(table_name)
9
+ indexes = []
10
+ current_index = nil
11
+ execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", 'SCHEMA') do |result|
12
+ each_hash(result) do |row|
13
+ if current_index != row[:Key_name]
14
+ next if row[:Key_name] == 'PRIMARY' # skip the primary key
15
+
16
+ current_index = row[:Key_name]
17
+
18
+ cubrid_index_type = row[:Index_type].downcase.to_sym
19
+
20
+ # currently only support btree
21
+ # https://www.cubrid.org/manual/en/11.2/sql/query/show.html?highlight=show%20index#show-index
22
+ index_using = cubrid_index_type
23
+ index_type = nil
24
+
25
+ indexes << [
26
+ row[:Table],
27
+ row[:Key_name],
28
+ row[:Non_unique].to_i == 0,
29
+ [],
30
+ { lengths: {},
31
+ orders: {},
32
+ type: index_type,
33
+ using: index_using,
34
+ comment: row[:Comment].presence,
35
+ null: row[:Null] == 'YES',
36
+ visible: row[:Visible] == 'YES' }
37
+ ]
38
+ end
39
+
40
+ if row[:Func]
41
+ expression = row[:Func]
42
+ expression = +"(#{expression})" unless expression.start_with?('(')
43
+ indexes.last[-2] << expression
44
+ indexes.last[-1][:expressions] ||= {}
45
+ indexes.last[-1][:expressions][expression] = expression
46
+ indexes.last[-1][:orders][expression] = :desc if row[:Collation] == 'D'
47
+ else
48
+ indexes.last[-2] << row[:Column_name]
49
+ indexes.last[-1][:lengths][row[:Column_name]] = row[:Sub_part].to_i if row[:Sub_part]
50
+ indexes.last[-1][:orders][row[:Column_name]] = :desc if row[:Collation] == 'D'
51
+ end
52
+ end
53
+ end
54
+
55
+ indexes.map do |index|
56
+ options = index.pop
57
+
58
+ if expressions = options.delete(:expressions)
59
+ orders = options.delete(:orders)
60
+ lengths = options.delete(:lengths)
61
+
62
+ columns = index[-1].map do |name|
63
+ [name.to_sym, expressions[name] || +quote_column_name(name)]
64
+ end.to_h
65
+
66
+ index[-1] = add_options_for_index_columns(
67
+ columns, order: orders, length: lengths
68
+ ).values.join(', ')
69
+ end
70
+
71
+ IndexDefinition.new(*index, **options)
72
+ end
73
+ end
74
+
75
+ def remove_column(table_name, column_name, type = nil, **options)
76
+ remove_foreign_key(table_name, column: column_name) if foreign_key_exists?(table_name, column: column_name)
77
+ super
78
+ end
79
+
80
+ def create_table(table_name, options: default_row_format, **)
81
+ super
82
+ end
83
+
84
+ def internal_string_options_for_primary_key
85
+ super.tap do |options|
86
+ if !row_format_dynamic_by_default? && charset =~ /^utf8/
87
+ options[:collation] = collation.sub(/\A[^_]+/, 'utf8')
88
+ end
89
+ end
90
+ end
91
+
92
+ def update_table_definition(table_name, base)
93
+ Cubrid2::Table.new(table_name, base)
94
+ end
95
+
96
+ def create_schema_dumper(options)
97
+ Cubrid2::SchemaDumper.create(self, options)
98
+ end
99
+
100
+ # Maps logical Rails types to Cubrid-specific data types.
101
+ def type_to_sql(type, limit: nil,
102
+ precision: nil, scale: nil,
103
+ size: limit_to_size(limit, type),
104
+ unsigned: nil, **)
105
+
106
+ case type.to_s
107
+ when 'integer'
108
+ integer_to_sql(limit)
109
+ when 'serial'
110
+ integer_to_sql(8) #bigint
111
+ when 'float', 'real', 'double', 'double precision'
112
+ float_to_sql(limit)
113
+ when 'text', 'string', 'varchar', 'char varing'
114
+ type_with_size_to_sql('string', size)
115
+ when 'char', 'character'
116
+ type_with_size_to_sql('char', size)
117
+ when 'blob', 'binary'
118
+ type_with_size_to_sql('blob', size)
119
+ when 'clob'
120
+ type_with_size_to_sql('clob', size)
121
+ when 'nchar', 'nchar varing'
122
+ raise 'Not supported from cubrid 9.0'
123
+ else
124
+ super
125
+ end
126
+ end
127
+
128
+ def table_alias_length
129
+ # https://www.cubrid.org/manual/en/9.1.0/sql/identifier.html#id2
130
+ 222
131
+ end
132
+
133
+ private
134
+
135
+ def row_format_dynamic_by_default?
136
+ false
137
+ end
138
+
139
+ def default_row_format
140
+ return if row_format_dynamic_by_default?
141
+
142
+ nil
143
+ end
144
+
145
+ def schema_creation
146
+ Cubrid2::SchemaCreation.new(self)
147
+ end
148
+
149
+ def create_table_definition(*args, **options)
150
+ Cubrid2::TableDefinition.new(self, *args, **options)
151
+ end
152
+
153
+ def new_column_from_field(_table_name, field)
154
+ type_metadata = fetch_type_metadata(field[:Type], field[:Extra])
155
+ default = field[:Default]
156
+ default_function = nil
157
+
158
+ if type_metadata.type == :datetime # && /\ACURRENT_TIMESTAMP(?:\([0-6]?\))?\z/i.match?(default)
159
+ default_function = default
160
+ default = nil
161
+ end
162
+
163
+ Cubrid2::Column.new(
164
+ field[:Field],
165
+ default,
166
+ type_metadata,
167
+ field[:Null] == 'YES',
168
+ default_function,
169
+ collation: field[:Collation],
170
+ comment: field[:Comment].presence,
171
+ extra: field[:Extra]
172
+ )
173
+ end
174
+
175
+ def fetch_type_metadata(sql_type, extra = '')
176
+ Cubrid2::TypeMetadata.new(super(sql_type), extra: extra)
177
+ end
178
+
179
+ def extract_foreign_key_action(specifier)
180
+ case specifier
181
+ when 'CASCADE' then :cascade
182
+ when 'SET NULL' then :nullify
183
+ when 'RESTRICT' then :restrict
184
+ end
185
+ end
186
+
187
+ def add_index_length(quoted_columns, **options)
188
+ lengths = options_for_index_columns(options[:length])
189
+ quoted_columns.each do |name, column|
190
+ column << "(#{lengths[name]})" if lengths[name].present?
191
+ end
192
+ end
193
+
194
+ def add_options_for_index_columns(quoted_columns, **options)
195
+ quoted_columns = add_index_length(quoted_columns, **options)
196
+ super
197
+ end
198
+
199
+ def data_source_sql(name = nil, type: nil)
200
+ scope = quoted_scope(name, type: type)
201
+ sql = +'SHOW TABLES '
202
+ sql << " LIKE #{scope[:name]}" if scope[:name]
203
+ sql
204
+ end
205
+
206
+ def quoted_scope(name = nil, type: nil)
207
+ schema, name = extract_schema_qualified_name(name)
208
+ scope = {}
209
+ scope[:schema] = schema ? quote(schema) : 'database()'
210
+ scope[:name] = quote(name) if name
211
+ scope[:type] = quote(type) if type
212
+ scope
213
+ end
214
+
215
+ def extract_schema_qualified_name(string)
216
+ return [] if string.nil?
217
+
218
+ q1 = '[`\"\[]'
219
+ q2 = '[`\"\]]'
220
+ schema, name = string.scan(/[^`"\[\].]+|#{q1}[^"]*#{q2}/)
221
+ if name.nil?
222
+ name = schema
223
+ schema = nil
224
+ end
225
+ [schema, name]
226
+ end
227
+
228
+ def type_with_size_to_sql(type, _size)
229
+ case type
230
+ when 'string'
231
+ 'varchar'
232
+ when 'char'
233
+ 'char'
234
+ when 'blob'
235
+ 'blob'
236
+ when 'clob'
237
+ 'clob'
238
+ end
239
+ end
240
+
241
+ def limit_to_size(limit, type)
242
+ case type.to_s
243
+ when 'text', 'blob', 'binary'
244
+ case limit
245
+ when 0..0xff then 'tiny'
246
+ when nil, 0x100..0xffff then nil
247
+ when 0x10000..0xffffff then 'medium'
248
+ when 0x1000000..0xffffffff then 'long'
249
+ else raise ArgumentError, "No #{type} type has byte size #{limit}"
250
+ end
251
+ end
252
+ end
253
+
254
+ def integer_to_sql(limit)
255
+ case limit
256
+ when 1 then 'smallint'
257
+ when 2 then 'smallint'
258
+ when 3 then 'int'
259
+ when nil, 4 then 'int'
260
+ when 5..8 then 'bigint'
261
+ when 9..16 then 'decimal'
262
+ else raise ArgumentError, "No integer type has byte size #{limit}. Use a decimal with scale 0 instead."
263
+ end
264
+ end
265
+
266
+ def float_to_sql(limit)
267
+ case limit
268
+ when nil, 1..4 then 'float'
269
+ when 5..8 then 'double'
270
+ else raise ArgumentError, "No float type has byte size #{limit}. Use a decimal with scale 0 instead."
271
+ end
272
+ end
273
+ end
274
+ end
275
+ end
276
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Cubrid2
6
+ class TypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc:
7
+ undef to_yaml if method_defined?(:to_yaml)
8
+
9
+ attr_reader :extra
10
+
11
+ def initialize(type_metadata, extra: '')
12
+ super(type_metadata)
13
+ @extra = extra
14
+ end
15
+
16
+ def ==(other)
17
+ other.is_a?(TypeMetadata) &&
18
+ __getobj__ == other.__getobj__ &&
19
+ extra == other.extra
20
+ end
21
+ alias eql? ==
22
+
23
+ def hash
24
+ TypeMetadata.hash ^
25
+ __getobj__.hash ^
26
+ extra.hash
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,7 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Cubrid2
4
+ VERSION = File.read(File.expand_path("../../../../VERSION", __dir__)).chomp
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record/connection_adapters/abstract_cubrid2_adapter'
4
+ require 'active_record/connection_adapters/cubrid2/database_statements'
5
+ require 'cubrid2'
6
+
7
+ module ActiveRecord
8
+ module ConnectionHandling # :nodoc:
9
+ ER_DATABASE_CONNECTION_ERROR = -1000
10
+
11
+ # Establishes a connection to the database that's used by all Active Record objects.
12
+ def cubrid2_connection(config)
13
+ config = config.symbolize_keys
14
+ config[:flags] ||= 0
15
+
16
+ client = Cubrid2::Client.new(config)
17
+ ConnectionAdapters::Cubrid2Adapter.new(client, logger, nil, config)
18
+ rescue Cubrid2::Error => e
19
+ if e.error_number == ER_DATABASE_CONNECTION_ERROR
20
+ raise ActiveRecord::NoDatabaseError
21
+ else
22
+ raise
23
+ end
24
+ end
25
+ end
26
+
27
+ module ConnectionAdapters
28
+ class Cubrid2Adapter < AbstractCubrid2Adapter
29
+ ADAPTER_NAME = 'Cubrid2'
30
+
31
+ include Cubrid2::DatabaseStatements
32
+
33
+ def initialize(connection, logger, connection_options, config)
34
+ superclass_config = config.reverse_merge(prepared_statements: false)
35
+ super(connection, logger, connection_options, superclass_config)
36
+ configure_connection
37
+ end
38
+
39
+ def adapter_name
40
+ ADAPTER_NAME
41
+ end
42
+
43
+ def self.database_exists?(config)
44
+ !!ActiveRecord::Base.cubrid_connection(config)
45
+ rescue ActiveRecord::NoDatabaseError
46
+ false
47
+ end
48
+
49
+ def supports_json?
50
+ database_version >= '10.2'
51
+ end
52
+
53
+ def supports_comments?
54
+ # https://www.cubrid.org/manual/en/10.0/release_note/r10_0.html#overview
55
+ database_version >= '10.0'
56
+ end
57
+
58
+ def supports_comments_in_create?
59
+ supports_comments?
60
+ end
61
+
62
+ def supports_savepoints?
63
+ true
64
+ end
65
+
66
+ def supports_lazy_transactions?
67
+ false
68
+ end
69
+
70
+ # HELPER METHODS ===========================================
71
+ def each_hash(result) # :nodoc:
72
+ stmt = result.is_a?(Array) ? result.first : result
73
+ if block_given?
74
+ if result && stmt
75
+ while row = stmt.fetch_hash
76
+ yield row.symbolize_keys
77
+ end
78
+ end
79
+ else
80
+ to_enum(:each_hash, stmt)
81
+ end
82
+ end
83
+
84
+ def error_number(exception)
85
+ exception.error_number if exception.respond_to?(:error_number)
86
+ end
87
+
88
+ #--
89
+ # QUOTING ==================================================
90
+ #++
91
+
92
+ def quote_string(string)
93
+ # escaping with backslash is only allowed when 'no_backslash_escapes' == 'yes' in cubrid config, default is yes.
94
+ # See: https://www.cubrid.org/manual/ko/11.2/sql/literal.html#id5
95
+ # "'#{string.gsub("'", "''")}'"
96
+ string
97
+ end
98
+
99
+ #--
100
+ # CONNECTION MANAGEMENT ====================================
101
+ #++
102
+
103
+ def active?
104
+ @connection.ping
105
+ end
106
+
107
+ def reconnect!
108
+ super
109
+ disconnect!
110
+ connect
111
+ end
112
+ alias reset! reconnect!
113
+
114
+ # Disconnects from the database if already connected.
115
+ # Otherwise, this method does nothing.
116
+ def disconnect!
117
+ super
118
+ @connection.close
119
+ end
120
+
121
+ def discard! # :nodoc:
122
+ super
123
+ #@connection.automatic_close = false
124
+ @connection = nil
125
+ end
126
+
127
+ def server_version
128
+ @connection.server_version
129
+ end
130
+
131
+ def ping
132
+ @connection.ping
133
+ end
134
+
135
+ def cubrid_connection
136
+ @connection
137
+ end
138
+
139
+ # 오류??
140
+ def auto_commit
141
+ @connection.auto_commit
142
+ end
143
+
144
+ def auto_commit=(flag)
145
+ @connection.auto_commit = flag
146
+ end
147
+
148
+ private
149
+
150
+ def connect
151
+ @connection = Cubrid2::Client.new(@config)
152
+ configure_connection
153
+ end
154
+
155
+ def configure_connection
156
+ @connection.query_options[:as] = :array
157
+ super
158
+ end
159
+
160
+ def full_version
161
+ schema_cache.database_version.full_version_string
162
+ end
163
+
164
+ def get_full_version
165
+ @connection.server_version
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,4 @@
1
+ # load local cubrid gem wrapper (ruby code)
2
+ require 'cubrid2'
3
+
4
+ require 'active_record/connection_adapters/cubrid2_adapter'
@@ -0,0 +1,67 @@
1
+ module Arel # :nodoc: all
2
+ module Visitors
3
+ class Cubrid < Arel::Visitors::ToSql
4
+ private
5
+
6
+ def visit_Arel_Nodes_Bin(o, collector)
7
+ collector << 'BINARY '
8
+ visit o.expr, collector
9
+ end
10
+
11
+ def visit_Arel_Nodes_UnqualifiedColumn(o, collector)
12
+ visit o.expr, collector
13
+ end
14
+
15
+ def visit_Arel_Nodes_SelectCore(o, collector)
16
+ o.froms ||= Arel.sql('DB_ROOT')
17
+ super
18
+ end
19
+
20
+ def visit_Arel_Nodes_Concat(o, collector)
21
+ collector << ' CONCAT('
22
+ visit o.left, collector
23
+ collector << ', '
24
+ visit o.right, collector
25
+ collector << ') '
26
+ collector
27
+ end
28
+
29
+ def visit_Arel_Nodes_IsNotDistinctFrom(o, collector)
30
+ collector = visit o.left, collector
31
+ collector << ' <=> '
32
+ visit o.right, collector
33
+ end
34
+
35
+ def visit_Arel_Nodes_IsDistinctFrom(o, collector)
36
+ collector << 'NOT '
37
+ visit_Arel_Nodes_IsNotDistinctFrom o, collector
38
+ end
39
+
40
+ def visit_Arel_Nodes_Regexp(o, collector)
41
+ infix_value o, collector, ' REGEXP '
42
+ end
43
+
44
+ def visit_Arel_Nodes_NotRegexp(o, collector)
45
+ infix_value o, collector, ' NOT REGEXP '
46
+ end
47
+
48
+ # no-op
49
+ def visit_Arel_Nodes_NullsFirst(o, collector)
50
+ visit o.expr, collector
51
+ end
52
+
53
+ # In the simple case, Cubrid allows us to place JOINs directly into the UPDATE
54
+ # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support
55
+ # these, we must use a subquery.
56
+ def prepare_update_statement(o)
57
+ if o.offset # || has_group_by_and_having?(o) ||
58
+ has_join_sources?(o) && has_limit_or_offset_or_orders?(o)
59
+ super
60
+ else
61
+ o
62
+ end
63
+ end
64
+ alias prepare_delete_statement prepare_update_statement
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,93 @@
1
+ module Cubrid2
2
+ class Client
3
+ delegate :new, :prepare, to: :@conn
4
+
5
+ attr_reader :query_options, :read_timeout, :conn
6
+
7
+ def self.default_query_options
8
+ @default_query_options ||= {
9
+ auto_commit: true
10
+ }
11
+ end
12
+
13
+ def initialize(opts = {})
14
+ raise Cubrid2::Error, 'Options parameter must be a Hash' unless opts.is_a? Hash
15
+
16
+ opts = Cubrid2::Util.key_hash_as_symbols(opts)
17
+ @read_timeout = nil
18
+ @query_options = self.class.default_query_options.dup
19
+ @query_options.merge! opts
20
+
21
+ %i[auto_commit].each do |key|
22
+ next unless opts.key?(key)
23
+
24
+ case key
25
+ when :auto_commit
26
+ send(:"#{key}=", !!opts[key]) # rubocop:disable Style/DoubleNegation
27
+ else
28
+ send(:"#{key}=", opts[key])
29
+ end
30
+ end
31
+
32
+ flags = 0
33
+
34
+ user = opts[:username] || opts[:user]
35
+ pass = opts[:password] || opts[:pass]
36
+ host = opts[:host] || opts[:hostname]
37
+ port = opts[:port]
38
+ database = opts[:database] || opts[:dbname] || opts[:db]
39
+
40
+ # Correct the data types before passing these values down to the C level
41
+ user = user.to_s unless user.nil?
42
+ pass = pass.to_s unless pass.nil?
43
+ host = host.to_s unless host.nil?
44
+ port = port.to_i unless port.nil?
45
+ database = database.to_s unless database.nil?
46
+
47
+ @conn = Cubrid.connect database, host, port, user, pass
48
+ end
49
+
50
+ def query(sql, options = {})
51
+ Thread.handle_interrupt(::Cubrid2::Util::TIMEOUT_ERROR_CLASS => :never) do
52
+ _query(sql, @query_options.merge(options))
53
+ end
54
+ end
55
+
56
+ def _query(sql, _options)
57
+ @conn.query(sql)
58
+ end
59
+
60
+ def query_info
61
+ info = query_info_string
62
+ return {} unless info
63
+
64
+ info_hash = {}
65
+ info.split.each_slice(2) { |s| info_hash[s[0].downcase.delete(':').to_sym] = s[1].to_i }
66
+ info_hash
67
+ end
68
+
69
+ def info
70
+ self.class.info
71
+ end
72
+
73
+ def ping
74
+ @conn.server_version.present?
75
+ end
76
+
77
+ def server_version
78
+ @conn.server_version
79
+ end
80
+
81
+ def close
82
+ @conn.close
83
+ end
84
+
85
+ def auto_commit
86
+ @conn.auto_commit
87
+ end
88
+
89
+ def auto_commit=(flag)
90
+ @conn.auto_commit = flag
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,5 @@
1
+ # Loaded by script/console. Land helpers here.
2
+
3
+ Pry.config.prompt = lambda do |context, *|
4
+ "[cubrid2] #{context}> "
5
+ end