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,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