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