Skip to content

Commit 46ad541

Browse files
committed
Fix bug in parsing SQLite table definitions to handle commas in default function definitions
1 parent d2cf718 commit 46ad541

File tree

2 files changed

+37
-10
lines changed

2 files changed

+37
-10
lines changed

activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -472,11 +472,7 @@ def bind_params_length
472472
end
473473

474474
def table_structure(table_name)
475-
structure = if supports_virtual_columns?
476-
internal_exec_query("PRAGMA table_xinfo(#{quote_table_name(table_name)})", "SCHEMA")
477-
else
478-
internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA")
479-
end
475+
structure = table_info(table_name)
480476
raise ActiveRecord::StatementInvalid.new("Could not find table '#{table_name}'", connection_pool: @pool) if structure.empty?
481477
table_structure_with_collation(table_name, structure)
482478
end
@@ -679,7 +675,7 @@ def table_structure_with_collation(table_name, basic_structure)
679675
auto_increments = {}
680676
generated_columns = {}
681677

682-
column_strings = table_structure_sql(table_name)
678+
column_strings = table_structure_sql(table_name, basic_structure.map { |column| column["name"] })
683679

684680
if column_strings.any?
685681
column_strings.each do |column_string|
@@ -712,7 +708,15 @@ def table_structure_with_collation(table_name, basic_structure)
712708
end
713709
end
714710

715-
def table_structure_sql(table_name)
711+
UNQUOTED_OPEN_PARENS_REGEX = /\((?![^'"]*['"][^'"]*$)/
712+
FINAL_CLOSE_PARENS_REGEX = /\);*\z/
713+
714+
def table_structure_sql(table_name, column_names = nil)
715+
unless column_names
716+
column_info = table_info(table_name)
717+
column_names = column_info.map { |column| column["name"] }
718+
end
719+
716720
sql = <<~SQL
717721
SELECT sql FROM
718722
(SELECT * FROM sqlite_master UNION ALL
@@ -722,16 +726,30 @@ def table_structure_sql(table_name)
722726

723727
# Result will have following sample string
724728
# CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
725-
# "password_digest" varchar COLLATE "NOCASE");
729+
# "password_digest" varchar COLLATE "NOCASE",
730+
# "o_id" integer,
731+
# CONSTRAINT "fk_rails_78146ddd2e" FOREIGN KEY ("o_id") REFERENCES "os" ("id"));
726732
result = query_value(sql, "SCHEMA")
727733

728734
return [] unless result
729735

730736
# Splitting with left parentheses and discarding the first part will return all
731737
# columns separated with comma(,).
732-
columns_string = result.split("(", 2).last
738+
result.partition(UNQUOTED_OPEN_PARENS_REGEX)
739+
.last
740+
.sub(FINAL_CLOSE_PARENS_REGEX, "")
741+
# column definitions can have a comma in them, so split on commas followed
742+
# by a space and a column name in quotes or followed by the keyword CONSTRAINT
743+
.split(/,(?=\s(?:CONSTRAINT|"(?:#{Regexp.union(column_names).source})"))/i)
744+
.map(&:strip)
745+
end
733746

734-
columns_string.split(",").map(&:strip)
747+
def table_info(table_name)
748+
if supports_virtual_columns?
749+
internal_exec_query("PRAGMA table_xinfo(#{quote_table_name(table_name)})", "SCHEMA")
750+
else
751+
internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA")
752+
end
735753
end
736754

737755
def arel_visitor

activerecord/test/cases/adapters/sqlite3/virtual_column_test.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def setup
1717
t.virtual :upper_name, type: :string, as: "UPPER(name)", stored: true
1818
t.virtual :lower_name, type: :string, as: "LOWER(name)", stored: false
1919
t.virtual :octet_name, type: :integer, as: "LENGTH(name)"
20+
t.virtual :mutated_name, type: :string, as: "REPLACE(name, 'l', 'L')"
2021
t.integer :column1
2122
end
2223
VirtualColumn.create(name: "Rails", column1: 10)
@@ -58,6 +59,14 @@ def test_implicit_virtual_column
5859
assert_equal 5, VirtualColumn.take.octet_name
5960
end
6061

62+
def test_virtual_column_with_comma_in_definition
63+
column = VirtualColumn.columns_hash["mutated_name"]
64+
assert_predicate column, :virtual?
65+
assert_not_predicate column, :virtual_stored?
66+
assert_not_nil column.default_function
67+
assert_equal "RaiLs", VirtualColumn.take.mutated_name
68+
end
69+
6170
def test_change_table_with_stored_generated_column
6271
@connection.change_table :virtual_columns do |t|
6372
t.virtual :decr_column1, type: :integer, as: "column1 - 1", stored: true

0 commit comments

Comments
 (0)