@@ -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
0 commit comments