Skip to content

Commit 7a133a1

Browse files
committed
Sqlite, fix and enabled support for virtual columns
1 parent 659177c commit 7a133a1

File tree

2 files changed

+129
-38
lines changed

2 files changed

+129
-38
lines changed

lib/arjdbc/sqlite3/adapter.rb

Lines changed: 112 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ module SQLite3
6060
# DIFFERENCE: Some common constant names to reduce differences in rest of this module from AR5 version
6161
ConnectionAdapters = ::ActiveRecord::ConnectionAdapters
6262
IndexDefinition = ::ActiveRecord::ConnectionAdapters::IndexDefinition
63+
ForeignKeyDefinition = ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition
6364
Quoting = ::ActiveRecord::ConnectionAdapters::SQLite3::Quoting
6465
RecordNotUnique = ::ActiveRecord::RecordNotUnique
6566
SchemaCreation = ConnectionAdapters::SQLite3::SchemaCreation
@@ -164,6 +165,10 @@ def supports_concurrent_connections?
164165
!@memory_database
165166
end
166167

168+
def supports_virtual_columns?
169+
database_version >= "3.31.0"
170+
end
171+
167172
def connected?
168173
!(@raw_connection.nil? || @raw_connection.closed?)
169174
end
@@ -257,7 +262,6 @@ def remove_index(table_name, column_name = nil, **options) # :nodoc:
257262
internal_exec_query "DROP INDEX #{quote_column_name(index_name)}"
258263
end
259264

260-
261265
# Renames a table.
262266
#
263267
# Example:
@@ -346,15 +350,31 @@ def add_reference(table_name, ref_name, **options) # :nodoc:
346350
end
347351
alias :add_belongs_to :add_reference
348352

353+
FK_REGEX = /.*FOREIGN KEY\s+\("([^"]+)"\)\s+REFERENCES\s+"(\w+)"\s+\("(\w+)"\)/
354+
DEFERRABLE_REGEX = /DEFERRABLE INITIALLY (\w+)/
349355
def foreign_keys(table_name)
350356
# SQLite returns 1 row for each column of composite foreign keys.
351357
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+
352371
grouped_fk = fk_info.group_by { |row| row["id"] }.values.each { |group| group.sort_by! { |row| row["seq"] } }
353372
grouped_fk.map do |group|
354373
row = group.first
355374
options = {
356375
on_delete: extract_foreign_key_action(row["on_delete"]),
357-
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"]]]
358378
}
359379

360380
if group.one?
@@ -364,8 +384,7 @@ def foreign_keys(table_name)
364384
options[:column] = group.map { |row| row["from"] }
365385
options[:primary_key] = group.map { |row| row["to"] }
366386
end
367-
# DIFFERENCE: FQN
368-
::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(table_name, row["table"], options)
387+
ForeignKeyDefinition.new(table_name, row["table"], options)
369388
end
370389
end
371390

@@ -412,7 +431,14 @@ def new_column_from_field(table_name, field, definitions)
412431

413432
type_metadata = fetch_type_metadata(field["type"])
414433
default_value = extract_value_from_default(default)
415-
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+
416442
rowid = is_column_the_rowid?(field, definitions)
417443

418444
ActiveRecord::ConnectionAdapters::SQLite3Column.new(
@@ -423,7 +449,8 @@ def new_column_from_field(table_name, field, definitions)
423449
default_function,
424450
collation: field["collation"],
425451
auto_increment: field["auto_increment"],
426-
rowid: rowid
452+
rowid: rowid,
453+
generated_type: generated_type
427454
)
428455
end
429456

@@ -435,7 +462,12 @@ def bind_params_length
435462
end
436463

437464
def table_structure(table_name)
438-
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+
439471
raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
440472
table_structure_with_collation(table_name, structure)
441473
end
@@ -475,8 +507,9 @@ def has_default_function?(default_value, default)
475507
# See: https://www.sqlite.org/lang_altertable.html
476508
# SQLite has an additional restriction on the ALTER TABLE statement
477509
def invalid_alter_table_type?(type, options)
478-
type.to_sym == :primary_key || options[:primary_key] ||
479-
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])
480513
end
481514

482515
def alter_table(
@@ -532,12 +565,6 @@ def copy_table(from, to, options = {})
532565
options[:rename][column.name.to_sym] ||
533566
column.name) : column.name
534567

535-
if column.has_default?
536-
type = lookup_cast_type_from_column(column)
537-
default = type.deserialize(column.default)
538-
default = -> { column.default_function } if default.nil?
539-
end
540-
541568
column_options = {
542569
limit: column.limit,
543570
precision: column.precision,
@@ -547,19 +574,31 @@ def copy_table(from, to, options = {})
547574
primary_key: column_name == from_primary_key
548575
}
549576

550-
unless column.auto_increment?
551-
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
552589
end
553590

554-
column_type = column.bigint? ? :bigint : column.type
591+
column_type = column.virtual? ? :virtual : (column.bigint? ? :bigint : column.type)
555592
@definition.column(column_name, column_type, **column_options)
556593
end
557594

558595
yield @definition if block_given?
559596
end
560597
copy_table_indexes(from, to, options[:rename] || {})
598+
599+
columns_to_copy = @definition.columns.reject { |col| col.options.key?(:as) }.map(&:name)
561600
copy_table_contents(from, to,
562-
@definition.columns.map(&:name),
601+
columns_to_copy,
563602
options[:rename] || {})
564603
end
565604

@@ -633,32 +672,22 @@ def translate_exception(exception, message:, sql:, binds:)
633672

634673
COLLATE_REGEX = /.*\"(\w+)\".*collate\s+\"(\w+)\".*/i.freeze
635674
PRIMARY_KEY_AUTOINCREMENT_REGEX = /.*\"(\w+)\".+PRIMARY KEY AUTOINCREMENT/i
675+
GENERATED_ALWAYS_AS_REGEX = /.*"(\w+)".+GENERATED ALWAYS AS \((.+)\) (?:STORED|VIRTUAL)/i
636676

637677
def table_structure_with_collation(table_name, basic_structure)
638678
collation_hash = {}
639679
auto_increments = {}
640-
sql = <<~SQL
641-
SELECT sql FROM
642-
(SELECT * FROM sqlite_master UNION ALL
643-
SELECT * FROM sqlite_temp_master)
644-
WHERE type = 'table' AND name = #{quote(table_name)}
645-
SQL
680+
generated_columns = {}
646681

647-
# Result will have following sample string
648-
# CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
649-
# "password_digest" varchar COLLATE "NOCASE");
650-
result = query_value(sql, "SCHEMA")
651-
652-
if result
653-
# Splitting with left parentheses and discarding the first part will return all
654-
# columns separated with comma(,).
655-
columns_string = result.split("(", 2).last
682+
column_strings = table_structure_sql(table_name, basic_structure.map { |column| column["name"] })
656683

657-
columns_string.split(",").each do |column_string|
684+
if column_strings.any?
685+
column_strings.each do |column_string|
658686
# This regex will match the column name and collation type and will save
659687
# the value in $1 and $2 respectively.
660688
collation_hash[$1] = $2 if COLLATE_REGEX =~ column_string
661689
auto_increments[$1] = true if PRIMARY_KEY_AUTOINCREMENT_REGEX =~ column_string
690+
generated_columns[$1] = $2 if GENERATED_ALWAYS_AS_REGEX =~ column_string
662691
end
663692

664693
basic_structure.map do |column|
@@ -672,13 +701,61 @@ def table_structure_with_collation(table_name, basic_structure)
672701
column["auto_increment"] = true
673702
end
674703

704+
if generated_columns.has_key?(column_name)
705+
column["dflt_value"] = generated_columns[column_name]
706+
end
707+
675708
column
676709
end
677710
else
678711
basic_structure.to_a
679712
end
680713
end
681714

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+
682759
def arel_visitor
683760
Arel::Visitors::SQLite.new(self)
684761
end

lib/arjdbc/sqlite3/column.rb

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ module ActiveRecord::ConnectionAdapters
44
class SQLite3Column < JdbcColumn
55

66
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, **)
99
super
10+
1011
@auto_increment = auto_increment
1112
@default = nil if default =~ /NULL/
1213
@rowid = rowid
14+
@generated_type = generated_type
1315
end
1416

1517
def self.string_to_binary(value)
@@ -39,6 +41,18 @@ def auto_incremented_by_db?
3941
auto_increment? || rowid
4042
end
4143

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+
4256
def init_with(coder)
4357
@auto_increment = coder["auto_increment"]
4458
super
@@ -100,4 +114,4 @@ def extract_scale(sql_type)
100114
end
101115
end
102116
end
103-
end
117+
end

0 commit comments

Comments
 (0)