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,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Cubrid2
6
+ class Column < ConnectionAdapters::Column # :nodoc:
7
+ delegate :extra, to: :sql_type_metadata, allow_nil: true
8
+
9
+ def unsigned?
10
+ false
11
+ end
12
+
13
+ def case_sensitive?
14
+ collation && !collation.end_with?('_ci')
15
+ end
16
+
17
+ def auto_increment?
18
+ !respond_to?(:extra) && extra == 'auto_increment'
19
+ end
20
+
21
+ def virtual?
22
+ # /\b(?:VIRTUAL|STORED|PERSISTENT)\b/.match?(extra)
23
+ false
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Cubrid2
6
+ module DatabaseStatements
7
+ # Returns an ActiveRecord::Result instance.
8
+ def select_all(*param1, **param2) # :nodoc:
9
+ if ExplainRegistry.collect? && prepared_statements
10
+ unprepared_statement { super }
11
+ else
12
+ super
13
+ end
14
+ end
15
+
16
+ def query(sql, name = nil) # :nodoc:
17
+ execute(sql, name)
18
+ end
19
+
20
+ def query_values(sql, name = nil) # :nodoc:
21
+ res = exec_query(sql, name)
22
+ res&.rows&.map(&:first)
23
+ end
24
+
25
+ def query_value(sql, name = nil) # :nodoc:
26
+ _single_value_from_rows(query(sql, name))
27
+ end
28
+
29
+ READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(
30
+ :begin, :commit, :explain, :select, :set, :show, :release, :savepoint, :rollback, :describe, :desc, :with
31
+ ) # :nodoc:
32
+ private_constant :READ_QUERY
33
+
34
+ def write_query?(sql) # :nodoc:
35
+ !READ_QUERY.match?(sql)
36
+ end
37
+
38
+ # Executes the SQL statement in the context of this connection.
39
+ def execute(sql, name = nil)
40
+ if preventing_writes? && write_query?(sql)
41
+ raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
42
+ end
43
+
44
+ # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
45
+ # made since we established the connection
46
+ @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
47
+ # @connection.query_options[:database_timezone] = ActiveRecord.default_timezone
48
+
49
+ super
50
+ end
51
+
52
+ def exec_query(sql, name = 'SQL', binds = [], prepare: false)
53
+ if without_prepared_statement?(binds)
54
+ execute_and_free(sql, name) do |result|
55
+ _build_stmt_result(result)
56
+ end
57
+ else
58
+ exec_stmt_and_free(sql, name, binds, cache_stmt: prepare) do |_, result|
59
+ _build_stmt_result(result)
60
+ end
61
+ end
62
+ end
63
+
64
+ def exec_delete(sql, name = nil, binds = [])
65
+ if without_prepared_statement?(binds)
66
+ @lock.synchronize do
67
+ execute_and_free(sql, name) { |stmt| stmt.affected_rows }
68
+ end
69
+ else
70
+ exec_stmt_and_free(sql, name, binds) { |stmt| stmt.affected_rows }
71
+ end
72
+ end
73
+ alias exec_update exec_delete
74
+
75
+ private
76
+
77
+ def is_conn_utf8?
78
+ charset == 'utf8' && collation =~ /^utf8/
79
+ end
80
+
81
+ def _build_stmt_result(stmt)
82
+ columns = stmt.column_info.map { |col| col['name'] }
83
+ rows = is_conn_utf8? ? _extract_rows_from_stmt__utf8(stmt) : _extract_rows_from_stmt__raw(stmt)
84
+ build_result(columns: columns, rows: rows)
85
+ end
86
+
87
+ def _extract_rows_from_stmt__raw(stmt, as_hash: false)
88
+ rows = []
89
+ if as_hash
90
+ while row = stmt.fetch_hash
91
+ rows << row
92
+ end
93
+ else
94
+ while row = stmt.fetch
95
+ rows << row
96
+ end
97
+ end
98
+ rows
99
+ end
100
+
101
+ def _extract_rows_from_stmt__utf8(stmt, as_hash: false)
102
+ rows = []
103
+ if as_hash
104
+ while row = stmt.fetch_hash
105
+ rows << row.map do |x|
106
+ [x[0], _as_utf8(x[1])]
107
+ end.to_h
108
+ end
109
+ else
110
+ while row = stmt.fetch
111
+ rows << row.map { |x| _as_utf8(x) }
112
+ end
113
+ end
114
+ rows
115
+ end
116
+
117
+ def _as_utf8(val)
118
+ if val.is_a?(String)
119
+ val.force_encoding('UTF-8')
120
+ else
121
+ val
122
+ end
123
+ end
124
+
125
+ def _single_value_from_rows(rows)
126
+ row = rows.fetch
127
+ row && row.first
128
+ end
129
+
130
+ def execute_batch(statements, name = nil)
131
+ combine_multi_statements(statements).each do |statement|
132
+ execute(statement, name)
133
+ end
134
+ end
135
+
136
+ def default_insert_value(column)
137
+ super unless column.auto_increment?
138
+ end
139
+
140
+ def last_inserted_id(_result)
141
+ stmt = query('SELECT LAST_INSERT_ID() as val_')
142
+ row = stmt.fetch
143
+ row[0]&.to_i
144
+ end
145
+
146
+ def supports_set_server_option?
147
+ @connection.respond_to?(:set_server_option)
148
+ end
149
+
150
+ def exec_stmt_and_free(sql, name, binds, cache_stmt: false)
151
+ if preventing_writes? && write_query?(sql)
152
+ raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
153
+ end
154
+
155
+ materialize_transactions
156
+
157
+ # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
158
+ # made since we established the connection
159
+ @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
160
+
161
+ type_casted_binds = type_casted_binds(binds)
162
+
163
+ log(sql, name, binds, type_casted_binds) do
164
+ stmt = if cache_stmt
165
+ @statements[sql] ||= @connection.prepare(sql)
166
+ else
167
+ @connection.prepare(sql)
168
+ end
169
+
170
+ begin
171
+ result = ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
172
+ stmt.execute(*type_casted_binds)
173
+ end
174
+ rescue Cubrid2::Error => e
175
+ if cache_stmt
176
+ @statements.delete(sql)
177
+ else
178
+ stmt.close
179
+ end
180
+ raise e
181
+ end
182
+
183
+ ret = yield stmt, result
184
+ result.free if result
185
+ stmt.close unless cache_stmt
186
+ ret
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Cubrid2
6
+ class ExplainPrettyPrinter # :nodoc:
7
+ # Pretty prints the result of an query that resembles Cubrid shell:
8
+ #
9
+ # Field Type Null Key Default Extra
10
+ # ====================================================================================================================================
11
+ # 'host_year' 'INTEGER' 'NO' 'PRI' NULL ''
12
+ # 'event_code' 'INTEGER' 'NO' 'MUL' NULL ''
13
+ # 'athlete_code' 'INTEGER' 'NO' 'MUL' NULL ''
14
+ # 'stadium_code' 'INTEGER' 'NO' '' NULL ''
15
+ # 'nation_code' 'CHAR(3)' 'YES' '' NULL ''
16
+ # 'medal' 'CHAR(1)' 'YES' '' NULL ''
17
+ # 'game_date' 'DATE' 'YES' '' NULL ''
18
+ #
19
+ # 7 rows selected. (0.010673 sec)
20
+ #
21
+ def pp(result, elapsed)
22
+ widths = compute_column_widths(result) + 4
23
+ separator = build_separator(widths)
24
+
25
+ pp = []
26
+
27
+ pp << build_cells(result.columns, widths)
28
+ pp << separator
29
+
30
+ result.rows.each do |row|
31
+ pp << build_cells(row, widths)
32
+ end
33
+
34
+ pp << build_footer(result.rows.length, elapsed)
35
+
36
+ pp.join("\n") + "\n"
37
+ end
38
+
39
+ private
40
+
41
+ def compute_column_widths(result)
42
+ [].tap do |widths|
43
+ result.columns.each_with_index do |column, i|
44
+ cells_in_column = [column] + result.rows.map { |r| r[i].nil? ? 'NULL' : r[i].to_s }
45
+ widths << cells_in_column.map(&:length).max
46
+ end
47
+ end
48
+ end
49
+
50
+ def build_separator(widths)
51
+ '=' * widths
52
+ end
53
+
54
+ def build_cells(items, widths)
55
+ cells = []
56
+ items.each_with_index do |item, i|
57
+ item = 'NULL' if item.nil?
58
+ justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust'
59
+ cells << item.to_s.send(justifier, widths[i])
60
+ end
61
+ ' ' + cells.join(' ') + ' '
62
+ end
63
+
64
+ def build_footer(nrows, elapsed)
65
+ rows_label = nrows == 1 ? 'row' : 'rows'
66
+ "#{nrows} #{rows_label} selected. (%.2f sec)" % elapsed
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Cubrid2
6
+ module Quoting # :nodoc:
7
+ # def quote_string(s)
8
+ # super
9
+ # end
10
+
11
+ def quote_column_name(name)
12
+ self.class.quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`"
13
+ end
14
+
15
+ def quote_table_name(name)
16
+ self.class.quoted_table_names[name] ||= super.gsub(".", "`.`").freeze
17
+ end
18
+
19
+ # def quote_table_name(name) # :nodoc:
20
+ # schema, name = extract_schema_qualified_name(name.to_s)
21
+
22
+ # self.class.quoted_table_names[name] ||= "`#{quote_string(schema)}`.`#{quote_string(name)}`".freeze
23
+ # end
24
+
25
+ # Quotes schema names for use in SQL queries.
26
+ def quote_schema_name(name)
27
+ quote_table_name(name)
28
+ end
29
+
30
+ def quote_table_name_for_assignment(_table, attr)
31
+ quote_column_name(attr)
32
+ end
33
+
34
+ # Quotes column names for use in SQL queries.
35
+ #def quote_column_name(name) # :nodoc:
36
+ #self.class.quoted_column_names[name] ||= quote(super).freeze
37
+ #pp "## quote_column_name: #{name}"
38
+ #self.class.quoted_column_names[name] ||= quote_string(name).freeze
39
+ #end
40
+
41
+ # def visit_Arel_Attributes_Attribute(o, collector)
42
+ # join_name = o.relation.table_alias || o.relation.name
43
+ # collector << quote_table_name(join_name) << "." << quote_column_name(o.name)
44
+ # end
45
+
46
+ def unquoted_true
47
+ 1
48
+ end
49
+
50
+ def unquoted_false
51
+ 0
52
+ end
53
+
54
+ def quoted_date(value)
55
+ if supports_datetime_with_precision?
56
+ super
57
+ else
58
+ super.sub(/\.\d{6}\z/, '')
59
+ end
60
+ end
61
+
62
+ def quoted_binary(value)
63
+ # https://www.cubrid.org/manual/ko/11.2/sql/literal.html#id5
64
+ "X'#{value.hex}'"
65
+ end
66
+
67
+ def column_name_matcher
68
+ COLUMN_NAME
69
+ end
70
+
71
+ def column_name_with_order_matcher
72
+ COLUMN_NAME_WITH_ORDER
73
+ end
74
+
75
+ COLUMN_NAME = /
76
+ \A
77
+ (
78
+ (?:
79
+ # `table_name`.`column_name` | function(one or no argument)
80
+ # "table_name"."column_name" | function(one or no argument)
81
+ # [table_name].[column_name] | function(one or no argument)
82
+ ((?:\w+\.|[`"\[]\w+[`"\]]\.)?(?:\w+|[`"\[]\w+[`"\]])) | \w+\((?:|\g<2>)\)
83
+ )
84
+ (?:\s+AS\s+(?:\w+|`\w+`))?
85
+ )
86
+ (?:\s*,\s*\g<1>)*
87
+ \z
88
+ /ix
89
+
90
+ COLUMN_NAME_WITH_ORDER = /
91
+ \A
92
+ (
93
+ (?:
94
+ # `table_name`.`column_name` | function(one or no argument)
95
+ # "table_name"."column_name" | function(one or no argument)
96
+ # [table_name].[column_name] | function(one or no argument)
97
+ ((?:\w+\.|[`"\[]\w+[`"\]]\.)?(?:\w+|[`"\[]\w+[`"\]])) | \w+\((?:|\g<2>)\)
98
+ )
99
+ (?:\s+ASC|\s+DESC)?
100
+ )
101
+ (?:\s*,\s*\g<1>)*
102
+ \z
103
+ /ix
104
+
105
+ private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER
106
+
107
+ private
108
+
109
+ def _type_cast(value)
110
+ case value
111
+ when Date, Time then value
112
+ else super
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Cubrid2
6
+ class SchemaCreation < ActiveRecord::ConnectionAdapters::SchemaCreation # :nodoc:
7
+ delegate :add_sql_comment!, to: :@conn, private: true
8
+
9
+ private
10
+
11
+ # def visit_DropForeignKey(name)
12
+ # "DROP FOREIGN KEY #{name}"
13
+ # end
14
+
15
+ def visit_DropCheckConstraint(name)
16
+ "DROP CONSTRAINT #{name}"
17
+ end
18
+
19
+ def visit_AddColumnDefinition(o)
20
+ add_column_position!(super, column_options(o.column))
21
+ end
22
+
23
+ def visit_ChangeColumnDefinition(o)
24
+ change_column_sql = +"CHANGE #{quote_column_name(o.name)} #{accept(o.column)}"
25
+ add_column_position!(change_column_sql, column_options(o.column))
26
+ end
27
+
28
+ def visit_CreateIndexDefinition(o)
29
+ sql = visit_IndexDefinition(o.index, true)
30
+ sql
31
+ end
32
+
33
+ def visit_IndexDefinition(o, create = false)
34
+ index_type = o.type&.to_s&.upcase || o.unique && "UNIQUE"
35
+
36
+ sql = create ? ["CREATE"] : []
37
+ sql << index_type if index_type
38
+ sql << "INDEX"
39
+ sql << quote_column_name(o.name)
40
+ #sql << "USING #{o.using}" if o.using
41
+ sql << "ON #{quote_table_name(o.table)}" if create
42
+ sql << "(#{quoted_columns(o)})"
43
+
44
+ add_sql_comment!(sql.join(" "), o.comment)
45
+ end
46
+
47
+ def add_table_options!(create_sql, options)
48
+ add_sql_comment!(super, options[:comment])
49
+ end
50
+
51
+ def add_column_options!(sql, options)
52
+ # In cubrid, default value of timestamp follows system parameter 'return_null_on_function_errors'
53
+ # if return_null_on_function_errors == 'no', timestamp null means error.
54
+ # https://www.cubrid.org/manual/en/10.1/sql/datatype.html#date-time-type
55
+ if /\Atimestamp\b/.match?(options[:column].sql_type) && !options[:primary_key] &&
56
+ !(options[:null] == false || options_include_default?(options))
57
+ sql << ' NULL'
58
+ end
59
+
60
+ if charset = options[:charset]
61
+ sql << " CHARSET #{charset}"
62
+ end
63
+
64
+ if collation = options[:collation]
65
+ sql << " COLLATE #{collation}"
66
+ end
67
+
68
+ if as = options[:as]
69
+ sql << " AS (#{as})"
70
+ end
71
+
72
+ add_sql_comment!(super, options[:comment])
73
+ end
74
+
75
+ def add_column_position!(sql, options)
76
+ if options[:first]
77
+ sql << ' FIRST'
78
+ elsif options[:after]
79
+ sql << " AFTER #{quote_column_name(options[:after])}"
80
+ end
81
+
82
+ sql
83
+ end
84
+
85
+ def index_in_create(table_name, column_name, options)
86
+ index_def, algorithm, if_not_exists = @conn.add_index_options(table_name, column_name, **options)
87
+ index_name = index_def.name
88
+ index_columns = index_def.columns.map { |x| quote_column_name(x) }.join(', ')
89
+ index_type = index_def.unique ? 'UNIQUE' : ''
90
+ comment = index_def.comment
91
+ add_sql_comment!(
92
+ +"#{index_type} INDEX #{quote_column_name(index_name)} (#{index_columns})", comment
93
+ )
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Cubrid2
6
+ module ColumnMethods
7
+ extend ActiveSupport::Concern
8
+ included do
9
+ define_column_methods :blob, :clob, :nchar
10
+
11
+ alias_method :char, :nchar
12
+ end
13
+ end
14
+
15
+ class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
16
+ include ColumnMethods
17
+
18
+ def new_column_definition(name, type, **options) # :nodoc:
19
+ case type
20
+ when :primary_key
21
+ type = :integer
22
+ options[:limit] ||= 8
23
+ options[:primary_key] = true
24
+ end
25
+
26
+ super
27
+ end
28
+
29
+ private
30
+
31
+ def aliased_types(_name, fallback)
32
+ fallback
33
+ end
34
+
35
+ def integer_like_primary_key_type(type, options)
36
+ options[:auto_increment] = true
37
+ type
38
+ end
39
+ end
40
+
41
+ class Table < ActiveRecord::ConnectionAdapters::Table
42
+ include ColumnMethods
43
+ end
44
+
45
+ class IndexDefinition < ActiveRecord::ConnectionAdapters::IndexDefinition
46
+ attr_reader :null, :visible
47
+
48
+ def initialize(table, name, unique = false, columns = [], **options)
49
+ options.tap do |o|
50
+ o[:lengths] ||= {}
51
+ o[:orders] ||= {}
52
+ o[:opclasses] ||= {}
53
+ end
54
+
55
+ # get rise to error
56
+ @visible = options.delete(:visible)
57
+ @null = options.delete(:null)
58
+
59
+ super(table, name, unique, columns, **options)
60
+ end
61
+
62
+ def column_options
63
+ super.tap { |o|
64
+ o[:null] = @null
65
+ o[:visible] = @visible
66
+ }
67
+ end
68
+
69
+ private
70
+
71
+ def concise_options(options)
72
+ if columns.size == options.size && options.values.uniq.size == 1
73
+ options.values.first
74
+ else
75
+ options
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Cubrid2
6
+ class SchemaDumper < ConnectionAdapters::SchemaDumper # :nodoc:
7
+ private
8
+
9
+ def prepare_column_options(column)
10
+ spec = super
11
+ spec[:auto_increment] = 'true' if column.auto_increment?
12
+ spec
13
+ end
14
+
15
+ def column_spec_for_primary_key(column)
16
+ spec = super
17
+ spec.delete(:auto_increment) if column.type == :integer && column.auto_increment?
18
+ spec
19
+ end
20
+
21
+ def default_primary_key?(column)
22
+ super && column.auto_increment?
23
+ end
24
+
25
+ def explicit_primary_key_default?(column)
26
+ column.type == :integer && !column.auto_increment?
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end