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

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