activerecord-jdbc-alt-adapter 72.0.0.alpha1-java → 72.0.0.rc1-java

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9368e28d99e49de4164150e2e006ce8991be7d7aab58e6773936866f86dbbdd0
4
- data.tar.gz: '03983c9218db6dd3a9ab278b0364ded1d9f5c315853b7ed6b543a05cca8c6c6c'
3
+ metadata.gz: 0f7f249466042cd80343c394b99718ff78c5754a2105f06eaf87672f25d2d54e
4
+ data.tar.gz: cc61ff60b8f74d3fa100156f59c4cdd198865e87d9be401bf833a2e0ed623291
5
5
  SHA512:
6
- metadata.gz: 450ddae41c05f2519c53ff0ad17de0bfd3cfefa1a471775a9d27946843f2d685301ff12cbbcdd4f434f275ee4f067798fc500ff49a2f2ec79f2339dfede6fc8c
7
- data.tar.gz: aa9aa18ad21ead09d7bab3180032c8fa71ffee37fbb43dbba6a52ac3b436da34010e229e10cf1d6eec9edb2a803addcd0585a0b8ac78bdba05281f77dc7f5924
6
+ metadata.gz: 25c0368cbbeecad8536caa0ffadff34c28aacb0ff4a66f2085016224322645add68206976dbcef42475bdc39c1ffd47da761eacd919292ae15e6711c44935317
7
+ data.tar.gz: 9beed2af43699a9349a9284c252fd97db3c0110510892ac6937b5ace94df6be02269fb7360caa653fb864a4326d6582e958bea1afe4c69d8adaabec96af2a671
data/README.md CHANGED
@@ -175,7 +175,7 @@ adapters are available:
175
175
 
176
176
  ```yml
177
177
  development:
178
- adapter: mysql2 # or mysql
178
+ adapter: mysql2
179
179
  database: blog_development
180
180
  username: blog
181
181
  password: 1234
@@ -199,7 +199,7 @@ or preferably using the *properties:* syntax:
199
199
 
200
200
  ```yml
201
201
  production:
202
- adapter: mysql
202
+ adapter: mysql2
203
203
  username: blog
204
204
  password: blog
205
205
  url: "jdbc:mysql://localhost:3306/blog?profileSQL=true"
@@ -60,7 +60,16 @@ module ArJdbc
60
60
  # this version of log() automatically fills type_casted_binds from binds if necessary
61
61
  def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name = nil, async: false)
62
62
  if binds.any? && (type_casted_binds.nil? || type_casted_binds.empty?)
63
- type_casted_binds = ->{ binds.map(&:value_for_database) } # extract_raw_bind_values
63
+ type_casted_binds = lambda {
64
+ # extract_raw_bind_values
65
+ binds.map do |bind|
66
+ if bind.respond_to?(:value_for_database)
67
+ bind.value_for_database
68
+ else
69
+ bind
70
+ end
71
+ end
72
+ }
64
73
  end
65
74
  super
66
75
  end
@@ -26,7 +26,9 @@ module ArJdbc
26
26
  def begin_db_transaction
27
27
  log('BEGIN', 'TRANSACTION') do
28
28
  with_raw_connection(allow_retry: true, materialize_transactions: false) do |conn|
29
- conn.begin
29
+ result = conn.begin
30
+ verified!
31
+ result
30
32
  end
31
33
  end
32
34
  end
@@ -5,7 +5,6 @@ require 'active_record/connection_adapters/abstract_adapter'
5
5
 
6
6
  require 'arjdbc/version'
7
7
  require 'arjdbc/jdbc/java'
8
- require 'arjdbc/jdbc/base_ext'
9
8
  require 'arjdbc/jdbc/error'
10
9
  require 'arjdbc/jdbc/connection_methods'
11
10
  require 'arjdbc/jdbc/column'
Binary file
@@ -54,6 +54,7 @@ module ActiveRecord
54
54
  include MSSQL::ExplainSupport
55
55
  include MSSQL::DatabaseLimits
56
56
 
57
+ # Latin1-General, case-sensitive, accent-sensitive, kanatype-insensitive, width-sensitive
57
58
  @cs_equality_operator = 'COLLATE Latin1_General_CS_AS_WS'
58
59
 
59
60
  class << self
@@ -265,29 +266,25 @@ module ActiveRecord
265
266
 
266
267
  alias_method :current_schema=, :default_schema=
267
268
 
268
- # FIXME: This needs to be fixed when we implement the collation per
269
- # column basis. At the moment we only use the global database collation
270
- def default_uniqueness_comparison(attribute, value) # :nodoc:
269
+ # FIXME: This needs to be fixed when we implement the collation per column
270
+ def case_sensitive_comparison(attribute, value)
271
271
  column = column_for_attribute(attribute)
272
272
 
273
- if [:string, :text].include?(column.type) && collation && !collation.match(/_CS/) && !value.nil?
274
- # NOTE: there is a deprecation warning here in the mysql adapter
275
- # no sure if it's required.
273
+ case_sensitive = collation && collation.match(/_CS/)
274
+
275
+ if %i[string text].include?(column.type) && !case_sensitive && !value.nil?
276
276
  attribute.eq(Arel::Nodes::Bin.new(value))
277
277
  else
278
278
  super
279
279
  end
280
280
  end
281
281
 
282
- def case_sensitive_comparison(attribute, value)
283
- column = column_for_attribute(attribute)
282
+ def can_perform_case_insensitive_comparison_for?(column)
283
+ case_sensitive = collation && collation.match(/_CS/)
284
284
 
285
- if [:string, :text].include?(column.type) && collation && !collation.match(/_CS/) && !value.nil?
286
- attribute.eq(Arel::Nodes::Bin.new(value))
287
- else
288
- super
289
- end
285
+ %i[string text].include?(column.type) && !case_sensitive
290
286
  end
287
+ private :can_perform_case_insensitive_comparison_for?
291
288
 
292
289
  def configure_connection
293
290
  # Here goes initial settings per connection
@@ -485,6 +482,8 @@ module ActiveRecord
485
482
  ConnectionNotEstablished.new(exception)
486
483
  when /(cannot insert duplicate key .* with unique index) | (violation of unique key constraint)/i
487
484
  RecordNotUnique.new(message, sql: sql, binds: binds)
485
+ when /Violation of PRIMARY KEY constraint .* Cannot insert duplicate key in object .* The duplicate key value is/i
486
+ RecordNotUnique.new(message, sql: sql, binds: binds)
488
487
  when /Lock request time out period exceeded/i
489
488
  LockTimeout.new(message, sql: sql, binds: binds)
490
489
  when /The .* statement conflicted with the FOREIGN KEY constraint/
@@ -9,17 +9,26 @@ module ActiveRecord
9
9
  def initialize(name, raw_default, sql_type_metadata = nil, null = true, table_name = nil, default_function = nil, collation = nil, comment: nil)
10
10
  @table_name = table_name
11
11
 
12
- default = extract_default(raw_default)
12
+ default_val, default_fun = extract_default(raw_default)
13
13
 
14
- super(name, default, sql_type_metadata, null, default_function, collation: collation, comment: comment)
14
+ super(name, default_val, sql_type_metadata, null, default_fun, collation: collation, comment: comment)
15
15
  end
16
16
 
17
17
  def extract_default(value)
18
- # return nil if default does not match the patterns to avoid
19
- # any unexpected errors.
20
- return unless value =~ /^\(N?'(.*)'\)$/m || value =~ /^\(\(?(.*?)\)?\)$/
21
-
22
- unquote_string(Regexp.last_match[1])
18
+ return [nil, nil] unless value
19
+
20
+ case value
21
+ when /\A\(N?'(.*)'\)\Z/m
22
+ [unquote_string(Regexp.last_match[1]), nil]
23
+ when /\A\(\((.*)\)\)\Z/
24
+ [unquote_string(Regexp.last_match[1]), nil]
25
+ when /\A\((\w+\(\))\)\Z/
26
+ [nil, unquote_string(Regexp.last_match[1])]
27
+ else
28
+ # return nil if default does not match the patterns to avoid
29
+ # any unexpected errors.
30
+ [nil, nil]
31
+ end
23
32
  end
24
33
 
25
34
  def unquote_string(string)
@@ -31,6 +31,8 @@ module ActiveRecord
31
31
 
32
32
  def write_query?(sql) # :nodoc:
33
33
  !READ_QUERY.match?(sql)
34
+ rescue ArgumentError # Invalid encoding
35
+ !READ_QUERY.match?(sql.b)
34
36
  end
35
37
 
36
38
  # Internal method to test different isolation levels supported by this
@@ -176,6 +178,9 @@ module ActiveRecord
176
178
  log(sql, name, binds) do
177
179
  with_raw_connection do |conn|
178
180
  result = conditional_indentity_insert(sql) do
181
+ # DEPRECATION WARNING: to_time will always preserve the timezone offset of the receiver in Rails 8.0.
182
+ # To opt in to the new behavior, set `ActiveSupport.to_time_preserves_timezone = true`.
183
+ # (called from block in execute_insert_pk
179
184
  conn.execute_insert_pk(sql, binds, pk)
180
185
  end
181
186
  verified!
@@ -38,6 +38,22 @@ module ActiveRecord
38
38
  end
39
39
  end
40
40
 
41
+ def visit_CreateIndexDefinition(o)
42
+ index = o.index
43
+
44
+ sql = []
45
+ sql << "IF NOT EXISTS (SELECT name FROM sysindexes WHERE name = '#{o.index.name}')" if o.if_not_exists
46
+ sql << "CREATE"
47
+ sql << "UNIQUE" if index.unique
48
+ sql << index.type.upcase if index.type
49
+ sql << "INDEX"
50
+ sql << "#{quote_column_name(index.name)} ON #{quote_table_name(index.table)}"
51
+ sql << "(#{quoted_columns(index)})"
52
+ sql << "WHERE #{index.where}" if index.where
53
+
54
+ sql.join(" ")
55
+ end
56
+
41
57
  def add_column_options!(sql, options)
42
58
  sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options)
43
59
 
@@ -80,8 +80,50 @@ module ActiveRecord
80
80
  valid_raw_connection.primary_keys(table_name)
81
81
  end
82
82
 
83
+ def build_change_column_definition(table_name, column_name, type, **options) # :nodoc:
84
+ td = create_table_definition(table_name)
85
+ cd = td.new_column_definition(column_name, type, **options)
86
+ ChangeColumnDefinition.new(cd, column_name)
87
+ end
88
+
89
+ def build_change_column_default_definition(table_name, column_name, default_or_changes) # :nodoc:
90
+ column = column_for(table_name, column_name)
91
+ return unless column
92
+
93
+ default = extract_new_default_value(default_or_changes)
94
+ ChangeColumnDefaultDefinition.new(column, default)
95
+ end
96
+
83
97
  def foreign_keys(table_name)
84
- valid_raw_connection.foreign_keys(table_name)
98
+ # valid_raw_connection.foreign_keys(table_name)
99
+ fk_info = execute_procedure(:sp_fkeys, nil, nil, nil, table_name, nil)
100
+
101
+ grouped_fk = fk_info.group_by { |row| row["FK_NAME"] }.values.each { |group| group.sort_by! { |row| row["KEY_SEQ"] } }
102
+ grouped_fk.map do |group|
103
+ row = group.first
104
+ options = {
105
+ name: row["FK_NAME"],
106
+ on_update: extract_foreign_key_action("update", row["FK_NAME"]),
107
+ on_delete: extract_foreign_key_action("delete", row["FK_NAME"])
108
+ }
109
+
110
+ if group.one?
111
+ options[:column] = row["FKCOLUMN_NAME"]
112
+ options[:primary_key] = row["PKCOLUMN_NAME"]
113
+ else
114
+ options[:column] = group.map { |row| row["FKCOLUMN_NAME"] }
115
+ options[:primary_key] = group.map { |row| row["PKCOLUMN_NAME"] }
116
+ end
117
+
118
+ ForeignKeyDefinition.new(table_name, row["PKTABLE_NAME"], options)
119
+ end
120
+ end
121
+
122
+ def extract_foreign_key_action(action, fk_name)
123
+ case select_value("SELECT #{action}_referential_action_desc FROM sys.foreign_keys WHERE name = '#{fk_name}'")
124
+ when "CASCADE" then :cascade
125
+ when "SET_NULL" then :nullify
126
+ end
85
127
  end
86
128
 
87
129
  def charset
@@ -84,15 +84,6 @@ module ActiveRecord
84
84
  @connection_parameters = conn_params
85
85
  end
86
86
 
87
- def self.database_exists?(config)
88
- conn = ActiveRecord::Base.mysql2_connection(config)
89
- conn && conn.really_valid?
90
- rescue ActiveRecord::NoDatabaseError
91
- false
92
- ensure
93
- conn.disconnect! if conn
94
- end
95
-
96
87
  def supports_json?
97
88
  !mariadb? && database_version >= '5.7.8'
98
89
  end
@@ -250,7 +241,7 @@ module ActiveRecord
250
241
 
251
242
  # e.g. "5.7.20-0ubuntu0.16.04.1"
252
243
  def full_version
253
- schema_cache.database_version.full_version_string
244
+ database_version.full_version_string
254
245
  end
255
246
 
256
247
  def get_full_version
@@ -7,7 +7,9 @@ module ArJdbc
7
7
 
8
8
  load_jdbc_driver
9
9
 
10
- config[:driver] ||= database_driver_name
10
+ # don't set driver if it's explicitly set to false
11
+ # allow Java's service discovery mechanism (with connector/j 8.0)
12
+ config[:driver] ||= database_driver_name if config[:driver] != false
11
13
 
12
14
  host = (config[:host] ||= "localhost")
13
15
  port = (config[:port] ||= 3306)
@@ -40,7 +42,7 @@ module ArJdbc
40
42
  def build_properties(config)
41
43
  properties = config[:properties] || {}
42
44
 
43
- properties["zeroDateTimeBehavior"] ||= "CONVERT_TO_NULL"
45
+ properties["zeroDateTimeBehavior"] ||= default_zero_date_time_behavior(config[:driver])
44
46
 
45
47
  properties["jdbcCompliantTruncation"] ||= false
46
48
 
@@ -88,6 +90,14 @@ module ArJdbc
88
90
  properties
89
91
  end
90
92
 
93
+ def default_zero_date_time_behavior(driver)
94
+ return "CONVERT_TO_NULL" if driver == false
95
+
96
+ return "CONVERT_TO_NULL" if driver.start_with?("com.mysql.cj.")
97
+
98
+ "convertToNull"
99
+ end
100
+
91
101
  # See https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-charsets.html
92
102
  # to charset-name (characterEncoding=...)
93
103
  def convert_mysql_encoding(config)
@@ -345,7 +345,7 @@ module ArJdbc
345
345
  type.typname AS name,
346
346
  type.OID AS oid,
347
347
  n.nspname AS schema,
348
- string_agg(enum.enumlabel, ',' ORDER BY enum.enumsortorder) AS value
348
+ array_agg(enum.enumlabel ORDER BY enum.enumsortorder) AS value
349
349
  FROM pg_enum AS enum
350
350
  JOIN pg_type AS type ON (type.oid = enum.enumtypid)
351
351
  JOIN pg_namespace n ON type.typnamespace = n.oid
@@ -842,6 +842,15 @@ module ActiveRecord::ConnectionAdapters
842
842
  # setting, you should immediately run <tt>bin/rails db:migrate</tt> to update the types in your schema.rb.
843
843
  class_attribute :datetime_type, default: :timestamp
844
844
 
845
+ ##
846
+ # :singleton-method:
847
+ # Toggles automatic decoding of date columns.
848
+ #
849
+ # ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.select_value("select '2024-01-01'::date").class #=> String
850
+ # ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.decode_dates = true
851
+ # ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.select_value("select '2024-01-01'::date").class #=> Date
852
+ class_attribute :decode_dates, default: false
853
+
845
854
  # Try to use as much of the built in postgres logic as possible
846
855
  # maybe someday we can extend the actual adapter
847
856
  include ActiveRecord::ConnectionAdapters::PostgreSQL::ReferentialIntegrity
@@ -855,9 +864,12 @@ module ActiveRecord::ConnectionAdapters
855
864
  include ArJdbc::Abstract::DatabaseStatements
856
865
  include ArJdbc::Abstract::StatementCache
857
866
  include ArJdbc::Abstract::TransactionSupport
858
- include ArJdbc::PostgreSQL
859
867
  include ArJdbc::PostgreSQLConfig
860
868
 
869
+ # NOTE: after AR refactor quote_column_name became class and instance method
870
+ include ArJdbc::PostgreSQL
871
+ extend ArJdbc::PostgreSQL
872
+
861
873
  require 'arjdbc/postgresql/oid_types'
862
874
  include ::ArJdbc::PostgreSQL::OIDTypes
863
875
  include ::ArJdbc::PostgreSQL::DatabaseStatements
@@ -18,6 +18,7 @@ require "active_record/connection_adapters/sqlite3/schema_statements"
18
18
  require "active_support/core_ext/class/attribute"
19
19
  require "arjdbc/sqlite3/column"
20
20
  require "arjdbc/sqlite3/adapter_hash_config"
21
+ require "arjdbc/sqlite3/pragmas"
21
22
 
22
23
  require "arjdbc/abstract/relation_query_attribute_monkey_patch"
23
24
 
@@ -59,6 +60,7 @@ module ArJdbc
59
60
  # DIFFERENCE: Some common constant names to reduce differences in rest of this module from AR5 version
60
61
  ConnectionAdapters = ::ActiveRecord::ConnectionAdapters
61
62
  IndexDefinition = ::ActiveRecord::ConnectionAdapters::IndexDefinition
63
+ ForeignKeyDefinition = ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition
62
64
  Quoting = ::ActiveRecord::ConnectionAdapters::SQLite3::Quoting
63
65
  RecordNotUnique = ::ActiveRecord::RecordNotUnique
64
66
  SchemaCreation = ConnectionAdapters::SQLite3::SchemaCreation
@@ -79,6 +81,15 @@ module ArJdbc
79
81
  json: { name: "json" },
80
82
  }
81
83
 
84
+ DEFAULT_PRAGMAS = {
85
+ "foreign_keys" => true,
86
+ "journal_mode" => :wal,
87
+ "synchronous" => :normal,
88
+ "mmap_size" => 134217728, # 128 megabytes
89
+ "journal_size_limit" => 67108864, # 64 megabytes
90
+ "cache_size" => 2000
91
+ }
92
+
82
93
  class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
83
94
  private
84
95
  def dealloc(stmt)
@@ -154,8 +165,23 @@ module ArJdbc
154
165
  !@memory_database
155
166
  end
156
167
 
168
+ def supports_virtual_columns?
169
+ database_version >= "3.31.0"
170
+ end
171
+
172
+ def connected?
173
+ !(@raw_connection.nil? || @raw_connection.closed?)
174
+ end
175
+
157
176
  def active?
158
- @raw_connection && !@raw_connection.closed?
177
+ if connected?
178
+ @lock.synchronize do
179
+ if @raw_connection&.active?
180
+ verified!
181
+ true
182
+ end
183
+ end
184
+ end || false
159
185
  end
160
186
 
161
187
  def return_value_after_insert?(column) # :nodoc:
@@ -167,10 +193,11 @@ module ArJdbc
167
193
  # Disconnects from the database if already connected. Otherwise, this
168
194
  # method does nothing.
169
195
  def disconnect!
170
- super
171
-
172
- @raw_connection&.close rescue nil
173
- @raw_connection = nil
196
+ @lock.synchronize do
197
+ super
198
+ @raw_connection&.close rescue nil
199
+ @raw_connection = nil
200
+ end
174
201
  end
175
202
 
176
203
  def supports_index_sort_order?
@@ -235,7 +262,6 @@ module ArJdbc
235
262
  internal_exec_query "DROP INDEX #{quote_column_name(index_name)}"
236
263
  end
237
264
 
238
-
239
265
  # Renames a table.
240
266
  #
241
267
  # Example:
@@ -324,15 +350,31 @@ module ArJdbc
324
350
  end
325
351
  alias :add_belongs_to :add_reference
326
352
 
353
+ FK_REGEX = /.*FOREIGN KEY\s+\("([^"]+)"\)\s+REFERENCES\s+"(\w+)"\s+\("(\w+)"\)/
354
+ DEFERRABLE_REGEX = /DEFERRABLE INITIALLY (\w+)/
327
355
  def foreign_keys(table_name)
328
356
  # SQLite returns 1 row for each column of composite foreign keys.
329
357
  fk_info = internal_exec_query("PRAGMA foreign_key_list(#{quote(table_name)})", "SCHEMA")
358
+ # Deferred or immediate foreign keys can only be seen in the CREATE TABLE sql
359
+ fk_defs = table_structure_sql(table_name)
360
+ .select do |column_string|
361
+ column_string.start_with?("CONSTRAINT") &&
362
+ column_string.include?("FOREIGN KEY")
363
+ end
364
+ .to_h do |fk_string|
365
+ _, from, table, to = fk_string.match(FK_REGEX).to_a
366
+ _, mode = fk_string.match(DEFERRABLE_REGEX).to_a
367
+ deferred = mode&.downcase&.to_sym || false
368
+ [[table, from, to], deferred]
369
+ end
370
+
330
371
  grouped_fk = fk_info.group_by { |row| row["id"] }.values.each { |group| group.sort_by! { |row| row["seq"] } }
331
372
  grouped_fk.map do |group|
332
373
  row = group.first
333
374
  options = {
334
375
  on_delete: extract_foreign_key_action(row["on_delete"]),
335
- on_update: extract_foreign_key_action(row["on_update"])
376
+ on_update: extract_foreign_key_action(row["on_update"]),
377
+ deferrable: fk_defs[[row["table"], row["from"], row["to"]]]
336
378
  }
337
379
 
338
380
  if group.one?
@@ -342,8 +384,7 @@ module ArJdbc
342
384
  options[:column] = group.map { |row| row["from"] }
343
385
  options[:primary_key] = group.map { |row| row["to"] }
344
386
  end
345
- # DIFFERENCE: FQN
346
- ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(table_name, row["table"], options)
387
+ ForeignKeyDefinition.new(table_name, row["table"], options)
347
388
  end
348
389
  end
349
390
 
@@ -390,7 +431,14 @@ module ArJdbc
390
431
 
391
432
  type_metadata = fetch_type_metadata(field["type"])
392
433
  default_value = extract_value_from_default(default)
393
- default_function = extract_default_function(default_value, default)
434
+ generated_type = extract_generated_type(field)
435
+
436
+ if generated_type.present?
437
+ default_function = default
438
+ else
439
+ default_function = extract_default_function(default_value, default)
440
+ end
441
+
394
442
  rowid = is_column_the_rowid?(field, definitions)
395
443
 
396
444
  ActiveRecord::ConnectionAdapters::SQLite3Column.new(
@@ -401,7 +449,8 @@ module ArJdbc
401
449
  default_function,
402
450
  collation: field["collation"],
403
451
  auto_increment: field["auto_increment"],
404
- rowid: rowid
452
+ rowid: rowid,
453
+ generated_type: generated_type
405
454
  )
406
455
  end
407
456
 
@@ -413,7 +462,12 @@ module ArJdbc
413
462
  end
414
463
 
415
464
  def table_structure(table_name)
416
- structure = internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA")
465
+ structure = if supports_virtual_columns?
466
+ internal_exec_query("PRAGMA table_xinfo(#{quote_table_name(table_name)})", "SCHEMA")
467
+ else
468
+ internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA")
469
+ end
470
+
417
471
  raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
418
472
  table_structure_with_collation(table_name, structure)
419
473
  end
@@ -453,8 +507,9 @@ module ArJdbc
453
507
  # See: https://www.sqlite.org/lang_altertable.html
454
508
  # SQLite has an additional restriction on the ALTER TABLE statement
455
509
  def invalid_alter_table_type?(type, options)
456
- type.to_sym == :primary_key || options[:primary_key] ||
457
- options[:null] == false && options[:default].nil?
510
+ type == :primary_key || options[:primary_key] ||
511
+ options[:null] == false && options[:default].nil? ||
512
+ (type == :virtual && options[:stored])
458
513
  end
459
514
 
460
515
  def alter_table(
@@ -510,12 +565,6 @@ module ArJdbc
510
565
  options[:rename][column.name.to_sym] ||
511
566
  column.name) : column.name
512
567
 
513
- if column.has_default?
514
- type = lookup_cast_type_from_column(column)
515
- default = type.deserialize(column.default)
516
- default = -> { column.default_function } if default.nil?
517
- end
518
-
519
568
  column_options = {
520
569
  limit: column.limit,
521
570
  precision: column.precision,
@@ -525,19 +574,31 @@ module ArJdbc
525
574
  primary_key: column_name == from_primary_key
526
575
  }
527
576
 
528
- unless column.auto_increment?
529
- column_options[:default] = default
577
+ if column.virtual?
578
+ column_options[:as] = column.default_function
579
+ column_options[:stored] = column.virtual_stored?
580
+ column_options[:type] = column.type
581
+ elsif column.has_default?
582
+ type = lookup_cast_type_from_column(column)
583
+ default = type.deserialize(column.default)
584
+ default = -> { column.default_function } if default.nil?
585
+
586
+ unless column.auto_increment?
587
+ column_options[:default] = default
588
+ end
530
589
  end
531
590
 
532
- column_type = column.bigint? ? :bigint : column.type
591
+ column_type = column.virtual? ? :virtual : (column.bigint? ? :bigint : column.type)
533
592
  @definition.column(column_name, column_type, **column_options)
534
593
  end
535
594
 
536
595
  yield @definition if block_given?
537
596
  end
538
597
  copy_table_indexes(from, to, options[:rename] || {})
598
+
599
+ columns_to_copy = @definition.columns.reject { |col| col.options.key?(:as) }.map(&:name)
539
600
  copy_table_contents(from, to,
540
- @definition.columns.map(&:name),
601
+ columns_to_copy,
541
602
  options[:rename] || {})
542
603
  end
543
604
 
@@ -611,32 +672,22 @@ module ArJdbc
611
672
 
612
673
  COLLATE_REGEX = /.*\"(\w+)\".*collate\s+\"(\w+)\".*/i.freeze
613
674
  PRIMARY_KEY_AUTOINCREMENT_REGEX = /.*\"(\w+)\".+PRIMARY KEY AUTOINCREMENT/i
675
+ GENERATED_ALWAYS_AS_REGEX = /.*"(\w+)".+GENERATED ALWAYS AS \((.+)\) (?:STORED|VIRTUAL)/i
614
676
 
615
677
  def table_structure_with_collation(table_name, basic_structure)
616
678
  collation_hash = {}
617
679
  auto_increments = {}
618
- sql = <<~SQL
619
- SELECT sql FROM
620
- (SELECT * FROM sqlite_master UNION ALL
621
- SELECT * FROM sqlite_temp_master)
622
- WHERE type = 'table' AND name = #{quote(table_name)}
623
- SQL
624
-
625
- # Result will have following sample string
626
- # CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
627
- # "password_digest" varchar COLLATE "NOCASE");
628
- result = query_value(sql, "SCHEMA")
680
+ generated_columns = {}
629
681
 
630
- if result
631
- # Splitting with left parentheses and discarding the first part will return all
632
- # columns separated with comma(,).
633
- columns_string = result.split("(", 2).last
682
+ column_strings = table_structure_sql(table_name, basic_structure.map { |column| column["name"] })
634
683
 
635
- columns_string.split(",").each do |column_string|
684
+ if column_strings.any?
685
+ column_strings.each do |column_string|
636
686
  # This regex will match the column name and collation type and will save
637
687
  # the value in $1 and $2 respectively.
638
688
  collation_hash[$1] = $2 if COLLATE_REGEX =~ column_string
639
689
  auto_increments[$1] = true if PRIMARY_KEY_AUTOINCREMENT_REGEX =~ column_string
690
+ generated_columns[$1] = $2 if GENERATED_ALWAYS_AS_REGEX =~ column_string
640
691
  end
641
692
 
642
693
  basic_structure.map do |column|
@@ -650,6 +701,10 @@ module ArJdbc
650
701
  column["auto_increment"] = true
651
702
  end
652
703
 
704
+ if generated_columns.has_key?(column_name)
705
+ column["dflt_value"] = generated_columns[column_name]
706
+ end
707
+
653
708
  column
654
709
  end
655
710
  else
@@ -657,6 +712,50 @@ module ArJdbc
657
712
  end
658
713
  end
659
714
 
715
+ UNQUOTED_OPEN_PARENS_REGEX = /\((?![^'"]*['"][^'"]*$)/
716
+ FINAL_CLOSE_PARENS_REGEX = /\);*\z/
717
+
718
+ def table_structure_sql(table_name, column_names = nil)
719
+ unless column_names
720
+ column_info = table_info(table_name)
721
+ column_names = column_info.map { |column| column["name"] }
722
+ end
723
+
724
+ sql = <<~SQL
725
+ SELECT sql FROM
726
+ (SELECT * FROM sqlite_master UNION ALL
727
+ SELECT * FROM sqlite_temp_master)
728
+ WHERE type = 'table' AND name = #{quote(table_name)}
729
+ SQL
730
+
731
+ # Result will have following sample string
732
+ # CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
733
+ # "password_digest" varchar COLLATE "NOCASE",
734
+ # "o_id" integer,
735
+ # CONSTRAINT "fk_rails_78146ddd2e" FOREIGN KEY ("o_id") REFERENCES "os" ("id"));
736
+ result = query_value(sql, "SCHEMA")
737
+
738
+ return [] unless result
739
+
740
+ # Splitting with left parentheses and discarding the first part will return all
741
+ # columns separated with comma(,).
742
+ result.partition(UNQUOTED_OPEN_PARENS_REGEX)
743
+ .last
744
+ .sub(FINAL_CLOSE_PARENS_REGEX, "")
745
+ # column definitions can have a comma in them, so split on commas followed
746
+ # by a space and a column name in quotes or followed by the keyword CONSTRAINT
747
+ .split(/,(?=\s(?:CONSTRAINT|"(?:#{Regexp.union(column_names).source})"))/i)
748
+ .map(&:strip)
749
+ end
750
+
751
+ def table_info(table_name)
752
+ if supports_virtual_columns?
753
+ internal_exec_query("PRAGMA table_xinfo(#{quote_table_name(table_name)})", "SCHEMA")
754
+ else
755
+ internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA")
756
+ end
757
+ end
758
+
660
759
  def arel_visitor
661
760
  Arel::Visitors::SQLite.new(self)
662
761
  end
@@ -686,29 +785,17 @@ module ArJdbc
686
785
  end
687
786
  end
688
787
 
689
- # Enforce foreign key constraints
690
- # https://www.sqlite.org/pragma.html#pragma_foreign_keys
691
- # https://www.sqlite.org/foreignkeys.html
692
- raw_execute("PRAGMA foreign_keys = ON", "SCHEMA")
693
- unless @memory_database
694
- # Journal mode WAL allows for greater concurrency (many readers + one writer)
695
- # https://www.sqlite.org/pragma.html#pragma_journal_mode
696
- raw_execute("PRAGMA journal_mode = WAL", "SCHEMA")
697
- # Set more relaxed level of database durability
698
- # 2 = "FULL" (sync on every write), 1 = "NORMAL" (sync every 1000 written pages) and 0 = "NONE"
699
- # https://www.sqlite.org/pragma.html#pragma_synchronous
700
- raw_execute("PRAGMA synchronous = NORMAL", "SCHEMA")
701
- # Set the global memory map so all processes can share some data
702
- # https://www.sqlite.org/pragma.html#pragma_mmap_size
703
- # https://www.sqlite.org/mmap.html
704
- raw_execute("PRAGMA mmap_size = #{128.megabytes}", "SCHEMA")
705
- end
706
- # Impose a limit on the WAL file to prevent unlimited growth
707
- # https://www.sqlite.org/pragma.html#pragma_journal_size_limit
708
- raw_execute("PRAGMA journal_size_limit = #{64.megabytes}", "SCHEMA")
709
- # Set the local connection cache to 2000 pages
710
- # https://www.sqlite.org/pragma.html#pragma_cache_size
711
- raw_execute("PRAGMA cache_size = 2000", "SCHEMA")
788
+ super
789
+
790
+ pragmas = @config.fetch(:pragmas, {}).stringify_keys
791
+ DEFAULT_PRAGMAS.merge(pragmas).each do |pragma, value|
792
+ if ::SQLite3::Pragmas.respond_to?(pragma)
793
+ stmt = ::SQLite3::Pragmas.public_send(pragma, value)
794
+ raw_execute(stmt, "SCHEMA")
795
+ else
796
+ warn "Unknown SQLite pragma: #{pragma}"
797
+ end
798
+ end
712
799
  end
713
800
  end
714
801
  # DIFFERENCE: A registration here is moved down to concrete class so we are not registering part of an adapter.
@@ -745,6 +832,12 @@ module ActiveRecord::ConnectionAdapters
745
832
  end
746
833
  end
747
834
 
835
+ # NOTE: include these modules before all then override some methods with the
836
+ # ones defined in ArJdbc::SQLite3 java part and ArJdbc::Abstract
837
+ include ::ActiveRecord::ConnectionAdapters::SQLite3::Quoting
838
+ include ::ActiveRecord::ConnectionAdapters::SQLite3::SchemaStatements
839
+ include ::ActiveRecord::ConnectionAdapters::SQLite3::DatabaseStatements
840
+
748
841
  include ArJdbc::Abstract::Core
749
842
  include ArJdbc::SQLite3
750
843
  include ArJdbc::SQLite3Config
@@ -754,9 +847,6 @@ module ActiveRecord::ConnectionAdapters
754
847
  include ArJdbc::Abstract::StatementCache
755
848
  include ArJdbc::Abstract::TransactionSupport
756
849
 
757
- include ::ActiveRecord::ConnectionAdapters::SQLite3::Quoting
758
- include ::ActiveRecord::ConnectionAdapters::SQLite3::SchemaStatements
759
- include ::ActiveRecord::ConnectionAdapters::SQLite3::DatabaseStatements
760
850
 
761
851
  ##
762
852
  # :singleton-method:
@@ -771,6 +861,14 @@ module ActiveRecord::ConnectionAdapters
771
861
  def initialize(...)
772
862
  super
773
863
 
864
+ @memory_database = false
865
+ case @config[:database].to_s
866
+ when ""
867
+ raise ArgumentError, "No database file specified. Missing argument: database"
868
+ when ":memory:"
869
+ @memory_database = true
870
+ end
871
+
774
872
  # assign arjdbc extra connection params
775
873
  conn_params = build_connection_config(@config.compact)
776
874
 
@@ -4,12 +4,14 @@ module ActiveRecord::ConnectionAdapters
4
4
  class SQLite3Column < JdbcColumn
5
5
 
6
6
  attr_reader :rowid
7
-
8
- def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation: nil, comment: nil, auto_increment: nil, rowid: false, **)
7
+
8
+ def initialize(*, auto_increment: nil, rowid: false, generated_type: nil, **)
9
9
  super
10
+
10
11
  @auto_increment = auto_increment
11
12
  @default = nil if default =~ /NULL/
12
13
  @rowid = rowid
14
+ @generated_type = generated_type
13
15
  end
14
16
 
15
17
  def self.string_to_binary(value)
@@ -39,6 +41,18 @@ module ActiveRecord::ConnectionAdapters
39
41
  auto_increment? || rowid
40
42
  end
41
43
 
44
+ def virtual?
45
+ !@generated_type.nil?
46
+ end
47
+
48
+ def virtual_stored?
49
+ virtual? && @generated_type == :stored
50
+ end
51
+
52
+ def has_default?
53
+ super && !virtual?
54
+ end
55
+
42
56
  def init_with(coder)
43
57
  @auto_increment = coder["auto_increment"]
44
58
  super
@@ -100,4 +114,4 @@ module ActiveRecord::ConnectionAdapters
100
114
  end
101
115
  end
102
116
  end
103
- end
117
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SQLite3
4
+ # defines methods to de generate pragma statements
5
+ module Pragmas
6
+ class << self
7
+ # The enumeration of valid synchronous modes.
8
+ SYNCHRONOUS_MODES = [["full", 2], ["normal", 1], ["off", 0]].freeze
9
+
10
+ # The enumeration of valid temp store modes.
11
+ TEMP_STORE_MODES = [["default", 0], ["file", 1], ["memory", 2]].freeze
12
+
13
+ # The enumeration of valid auto vacuum modes.
14
+ AUTO_VACUUM_MODES = [["none", 0], ["full", 1], ["incremental", 2]].freeze
15
+
16
+ # The list of valid journaling modes.
17
+ JOURNAL_MODES = [["delete"], ["truncate"], ["persist"], ["memory"], ["wal"], ["off"]].freeze
18
+
19
+ # The list of valid locking modes.
20
+ LOCKING_MODES = [["normal"], ["exclusive"]].freeze
21
+
22
+ # The list of valid encodings.
23
+ ENCODINGS = [["utf-8"], ["utf-16"], ["utf-16le"], ["utf-16be"]].freeze
24
+
25
+ # The list of valid WAL checkpoints.
26
+ WAL_CHECKPOINTS = [["passive"], ["full"], ["restart"], ["truncate"]].freeze
27
+
28
+ # Enforce foreign key constraints
29
+ # https://www.sqlite.org/pragma.html#pragma_foreign_keys
30
+ # https://www.sqlite.org/foreignkeys.html
31
+ def foreign_keys(value)
32
+ gen_boolean_pragma(:foreign_keys, value)
33
+ end
34
+
35
+ # Journal mode WAL allows for greater concurrency (many readers + one writer)
36
+ # https://www.sqlite.org/pragma.html#pragma_journal_mode
37
+ def journal_mode(value)
38
+ gen_enum_pragma(:journal_mode, value, JOURNAL_MODES)
39
+ end
40
+
41
+ # Set more relaxed level of database durability
42
+ # 2 = "FULL" (sync on every write), 1 = "NORMAL" (sync every 1000 written pages) and 0 = "NONE"
43
+ # https://www.sqlite.org/pragma.html#pragma_synchronous
44
+ def synchronous(value)
45
+ gen_enum_pragma(:synchronous, value, SYNCHRONOUS_MODES)
46
+ end
47
+
48
+ def temp_store(value)
49
+ gen_enum_pragma(:temp_store, value, TEMP_STORE_MODES)
50
+ end
51
+
52
+ # Set the global memory map so all processes can share some data
53
+ # https://www.sqlite.org/pragma.html#pragma_mmap_size
54
+ # https://www.sqlite.org/mmap.html
55
+ def mmap_size(value)
56
+ "PRAGMA mmap_size = #{value.to_i}"
57
+ end
58
+
59
+ # Impose a limit on the WAL file to prevent unlimited growth
60
+ # https://www.sqlite.org/pragma.html#pragma_journal_size_limit
61
+ def journal_size_limit(value)
62
+ "PRAGMA journal_size_limit = #{value.to_i}"
63
+ end
64
+
65
+ # Set the local connection cache to 2000 pages
66
+ # https://www.sqlite.org/pragma.html#pragma_cache_size
67
+ def cache_size(value)
68
+ "PRAGMA cache_size = #{value.to_i}"
69
+ end
70
+
71
+ private
72
+
73
+ def gen_boolean_pragma(name, mode)
74
+ case mode
75
+ when String
76
+ case mode.downcase
77
+ when "on", "yes", "true", "y", "t" then mode = "'ON'"
78
+ when "off", "no", "false", "n", "f" then mode = "'OFF'"
79
+ else
80
+ raise ActiveRecord::JDBCError, "unrecognized pragma parameter #{mode.inspect}"
81
+ end
82
+ when true, 1
83
+ mode = "ON"
84
+ when false, 0, nil
85
+ mode = "OFF"
86
+ else
87
+ raise ActiveRecord::JDBCError, "unrecognized pragma parameter #{mode.inspect}"
88
+ end
89
+
90
+ "PRAGMA #{name} = #{mode}"
91
+ end
92
+
93
+ def gen_enum_pragma(name, mode, enums)
94
+ match = enums.find { |p| p.find { |i| i.to_s.downcase == mode.to_s.downcase } }
95
+
96
+ unless match
97
+ # Unknown pragma value
98
+ raise ActiveRecord::JDBCError, "unrecognized #{name} #{mode.inspect}"
99
+ end
100
+
101
+ "PRAGMA #{name} = '#{match.first.upcase}'"
102
+ end
103
+ end
104
+ end
105
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ArJdbc
4
- VERSION = "72.0.0.alpha1"
4
+ VERSION = "72.0.0.rc1"
5
5
  end
data/lib/arjdbc.rb CHANGED
@@ -17,6 +17,15 @@ if defined?(JRUBY_VERSION)
17
17
  ActiveRecord::ConnectionAdapters.register(
18
18
  "sqlserver", "ActiveRecord::ConnectionAdapters::MSSQLAdapter", "active_record/connection_adapters/mssql_adapter"
19
19
  )
20
+ ActiveRecord::ConnectionAdapters.register(
21
+ "sqlite3", "ActiveRecord::ConnectionAdapters::SQLite3Adapter", "arjdbc/sqlite3/adapter"
22
+ )
23
+ ActiveRecord::ConnectionAdapters.register(
24
+ "postgresql", "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter", "arjdbc/postgresql/adapter"
25
+ )
26
+ ActiveRecord::ConnectionAdapters.register(
27
+ "mysql2", "ActiveRecord::ConnectionAdapters::Mysql2Adapter", "arjdbc/mysql/adapter"
28
+ )
20
29
  end
21
30
  else
22
31
  warn "activerecord-jdbc-adapter is for use with JRuby only"
@@ -126,6 +126,7 @@ public class RubyJdbcConnection extends RubyObject {
126
126
  private IRubyObject adapter; // the AbstractAdapter instance we belong to
127
127
  private volatile boolean connected = true;
128
128
  private RubyClass attributeClass;
129
+ private RubyClass timeZoneClass;
129
130
 
130
131
  private boolean lazy = false; // final once set on initialize
131
132
  private boolean jndi; // final once set on initialize
@@ -135,6 +136,7 @@ public class RubyJdbcConnection extends RubyObject {
135
136
  protected RubyJdbcConnection(Ruby runtime, RubyClass metaClass) {
136
137
  super(runtime, metaClass);
137
138
  attributeClass = runtime.getModule("ActiveModel").getClass("Attribute");
139
+ timeZoneClass = runtime.getModule("ActiveSupport").getClass("TimeWithZone");
138
140
  }
139
141
 
140
142
  private static final ObjectAllocator ALLOCATOR = new ObjectAllocator() {
@@ -2441,6 +2443,9 @@ public class RubyJdbcConnection extends RubyObject {
2441
2443
  if (attributeClass.isInstance(attribute)) {
2442
2444
  type = jdbcTypeForAttribute(context, attribute);
2443
2445
  value = valueForDatabase(context, attribute);
2446
+ } else if (timeZoneClass.isInstance(attribute)) {
2447
+ type = jdbcTypeFor("timestamp");
2448
+ value = attribute;
2444
2449
  } else {
2445
2450
  type = jdbcTypeForPrimitiveAttribute(context, attribute);
2446
2451
  value = attribute;
@@ -473,7 +473,10 @@ public class SQLite3RubyJdbcConnection extends RubyJdbcConnection {
473
473
  // Assume we will only call this with an array.
474
474
  final RubyArray statements = (RubyArray) statementsArg;
475
475
  return withConnection(context, connection -> {
476
+ final Ruby runtime = context.runtime;
477
+
476
478
  Statement statement = null;
479
+
477
480
  try {
478
481
  statement = createStatement(context, connection);
479
482
 
@@ -481,8 +484,15 @@ public class SQLite3RubyJdbcConnection extends RubyJdbcConnection {
481
484
  for (int i = 0; i < length; i++) {
482
485
  statement.addBatch(sqlString(statements.eltOk(i)));
483
486
  }
484
- statement.executeBatch();
485
- return context.nil;
487
+
488
+ int[] rows = statement.executeBatch();
489
+
490
+ RubyArray rowsAffected = runtime.newArray();
491
+
492
+ for (int i = 0; i < rows.length; i++) {
493
+ rowsAffected.append(runtime.newFixnum(rows[i]));
494
+ }
495
+ return rowsAffected;
486
496
  } catch (final SQLException e) {
487
497
  // Generate list semicolon list of statements which should match AR error formatting more.
488
498
  debugErrorSQL(context, sqlString(statements.join(context, context.runtime.newString(";\n"))));
metadata CHANGED
@@ -1,22 +1,22 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-jdbc-alt-adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 72.0.0.alpha1
4
+ version: 72.0.0.rc1
5
5
  platform: java
6
6
  authors:
7
7
  - Nick Sieger, Ola Bini, Karol Bucek, Jesse Chavez, and JRuby contributors
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-01-28 00:00:00.000000000 Z
11
+ date: 2025-02-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
+ name: activerecord
14
15
  requirement: !ruby/object:Gem::Requirement
15
16
  requirements:
16
17
  - - "~>"
17
18
  - !ruby/object:Gem::Version
18
19
  version: 7.2.2
19
- name: activerecord
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
@@ -95,7 +95,6 @@ files:
95
95
  - lib/arjdbc/jdbc/adapter.rb
96
96
  - lib/arjdbc/jdbc/adapter_java.jar
97
97
  - lib/arjdbc/jdbc/adapter_require.rb
98
- - lib/arjdbc/jdbc/base_ext.rb
99
98
  - lib/arjdbc/jdbc/callbacks.rb
100
99
  - lib/arjdbc/jdbc/column.rb
101
100
  - lib/arjdbc/jdbc/connection.rb
@@ -157,6 +156,7 @@ files:
157
156
  - lib/arjdbc/sqlite3/adapter_hash_config.rb
158
157
  - lib/arjdbc/sqlite3/column.rb
159
158
  - lib/arjdbc/sqlite3/connection_methods.rb
159
+ - lib/arjdbc/sqlite3/pragmas.rb
160
160
  - lib/arjdbc/tasks.rb
161
161
  - lib/arjdbc/tasks/database_tasks.rb
162
162
  - lib/arjdbc/tasks/databases.rake
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveRecord
4
- class << Base
5
- m = Module.new do
6
- # Allow adapters to provide their own {#reset_column_information} method.
7
- # @note This only affects the current thread's connection.
8
- def reset_column_information # :nodoc:
9
- # invoke the adapter-specific reset_column_information method
10
- connection.reset_column_information if connection.respond_to?(:reset_column_information)
11
- super
12
- end
13
- end
14
-
15
- self.prepend(m)
16
- end
17
- end