mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Add collation support for string and text columns in SQLite3
This commit is contained in:
parent
902360b77f
commit
3932912a59
6 changed files with 139 additions and 9 deletions
|
@ -1,3 +1,18 @@
|
||||||
|
* SQLite: `:collation` support for string and text columns.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
create_table :foo do |t|
|
||||||
|
t.string :string_nocase, collation: 'NOCASE'
|
||||||
|
t.text :text_rtrim, collation: 'RTRIM'
|
||||||
|
end
|
||||||
|
|
||||||
|
add_column :foo, :title, :string, collation: 'RTRIM'
|
||||||
|
|
||||||
|
change_column :foo, :title, :string, collation: 'NOCASE'
|
||||||
|
|
||||||
|
*Akshay Vishnoi*
|
||||||
|
|
||||||
* Allow the use of symbols or strings to specify enum values in test
|
* Allow the use of symbols or strings to specify enum values in test
|
||||||
fixtures:
|
fixtures:
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
module ActiveRecord
|
||||||
|
module ConnectionAdapters
|
||||||
|
module SQLite3
|
||||||
|
class SchemaCreation < AbstractAdapter::SchemaCreation
|
||||||
|
private
|
||||||
|
def add_column_options!(sql, options)
|
||||||
|
if options[:collation]
|
||||||
|
sql << " COLLATE \"#{options[:collation]}\""
|
||||||
|
end
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,5 +1,6 @@
|
||||||
require 'active_record/connection_adapters/abstract_adapter'
|
require 'active_record/connection_adapters/abstract_adapter'
|
||||||
require 'active_record/connection_adapters/statement_pool'
|
require 'active_record/connection_adapters/statement_pool'
|
||||||
|
require 'active_record/connection_adapters/sqlite3/schema_creation'
|
||||||
|
|
||||||
gem 'sqlite3', '~> 1.3.6'
|
gem 'sqlite3', '~> 1.3.6'
|
||||||
require 'sqlite3'
|
require 'sqlite3'
|
||||||
|
@ -84,6 +85,10 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def schema_creation # :nodoc:
|
||||||
|
SQLite3::SchemaCreation.new self
|
||||||
|
end
|
||||||
|
|
||||||
def initialize(connection, logger, connection_options, config)
|
def initialize(connection, logger, connection_options, config)
|
||||||
super(connection, logger)
|
super(connection, logger)
|
||||||
|
|
||||||
|
@ -344,9 +349,10 @@ module ActiveRecord
|
||||||
field["dflt_value"] = $1.gsub('""', '"')
|
field["dflt_value"] = $1.gsub('""', '"')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
collation = field['collation']
|
||||||
sql_type = field['type']
|
sql_type = field['type']
|
||||||
type_metadata = fetch_type_metadata(sql_type)
|
type_metadata = fetch_type_metadata(sql_type)
|
||||||
new_column(field['name'], field['dflt_value'], type_metadata, field['notnull'].to_i == 0)
|
new_column(field['name'], field['dflt_value'], type_metadata, field['notnull'].to_i == 0, nil, collation)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -441,6 +447,7 @@ module ActiveRecord
|
||||||
self.null = options[:null] if options.include?(:null)
|
self.null = options[:null] if options.include?(:null)
|
||||||
self.precision = options[:precision] if options.include?(:precision)
|
self.precision = options[:precision] if options.include?(:precision)
|
||||||
self.scale = options[:scale] if options.include?(:scale)
|
self.scale = options[:scale] if options.include?(:scale)
|
||||||
|
self.collation = options[:collation] if options.include?(:collation)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -454,9 +461,9 @@ module ActiveRecord
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def table_structure(table_name)
|
def table_structure(table_name)
|
||||||
structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash
|
structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA')
|
||||||
raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
|
raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
|
||||||
structure
|
table_structure_with_collation(table_name, structure)
|
||||||
end
|
end
|
||||||
|
|
||||||
def alter_table(table_name, options = {}) #:nodoc:
|
def alter_table(table_name, options = {}) #:nodoc:
|
||||||
|
@ -491,7 +498,7 @@ module ActiveRecord
|
||||||
@definition.column(column_name, column.type,
|
@definition.column(column_name, column.type,
|
||||||
:limit => column.limit, :default => column.default,
|
:limit => column.limit, :default => column.default,
|
||||||
:precision => column.precision, :scale => column.scale,
|
:precision => column.precision, :scale => column.scale,
|
||||||
:null => column.null)
|
:null => column.null, collation: column.collation)
|
||||||
end
|
end
|
||||||
yield @definition if block_given?
|
yield @definition if block_given?
|
||||||
end
|
end
|
||||||
|
@ -553,6 +560,46 @@ module ActiveRecord
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
COLLATE_REGEX = /.*\"(\w+)\".*collate\s+\"(\w+)\".*/i.freeze
|
||||||
|
|
||||||
|
def table_structure_with_collation(table_name, basic_structure)
|
||||||
|
collation_hash = {}
|
||||||
|
sql = "SELECT sql FROM
|
||||||
|
(SELECT * FROM sqlite_master UNION ALL
|
||||||
|
SELECT * FROM sqlite_temp_master)
|
||||||
|
WHERE type='table' and name='#{ table_name }' \;"
|
||||||
|
|
||||||
|
# Result will have following sample string
|
||||||
|
# CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
# "password_digest" varchar COLLATE "NOCASE");
|
||||||
|
result = exec_query(sql, 'SCHEMA').first
|
||||||
|
|
||||||
|
if result
|
||||||
|
# Splitting with left parantheses and picking up last will return all
|
||||||
|
# columns separated with comma(,).
|
||||||
|
columns_string = result["sql"].split('(').last
|
||||||
|
|
||||||
|
columns_string.split(',').each do |column_string|
|
||||||
|
# This regex will match the column name and collation type and will save
|
||||||
|
# the value in $1 and $2 respectively.
|
||||||
|
collation_hash[$1] = $2 if (COLLATE_REGEX =~ column_string)
|
||||||
|
end
|
||||||
|
|
||||||
|
basic_structure.map! do |column|
|
||||||
|
column_name = column['name']
|
||||||
|
|
||||||
|
if collation_hash.has_key? column_name
|
||||||
|
column['collation'] = collation_hash[column_name]
|
||||||
|
end
|
||||||
|
|
||||||
|
column
|
||||||
|
end
|
||||||
|
else
|
||||||
|
basic_structure.to_hash
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
53
activerecord/test/cases/adapters/sqlite3/collation_test.rb
Normal file
53
activerecord/test/cases/adapters/sqlite3/collation_test.rb
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
require "cases/helper"
|
||||||
|
require 'support/schema_dumping_helper'
|
||||||
|
|
||||||
|
class SQLite3CollationTest < ActiveRecord::TestCase
|
||||||
|
include SchemaDumpingHelper
|
||||||
|
|
||||||
|
def setup
|
||||||
|
@connection = ActiveRecord::Base.connection
|
||||||
|
@connection.create_table :collation_table_sqlite3, force: true do |t|
|
||||||
|
t.string :string_nocase, collation: 'NOCASE'
|
||||||
|
t.text :text_rtrim, collation: 'RTRIM'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def teardown
|
||||||
|
@connection.drop_table :collation_table_sqlite3, if_exists: true
|
||||||
|
end
|
||||||
|
|
||||||
|
test "string column with collation" do
|
||||||
|
column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == 'string_nocase' }
|
||||||
|
assert_equal :string, column.type
|
||||||
|
assert_equal 'NOCASE', column.collation
|
||||||
|
end
|
||||||
|
|
||||||
|
test "text column with collation" do
|
||||||
|
column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == 'text_rtrim' }
|
||||||
|
assert_equal :text, column.type
|
||||||
|
assert_equal 'RTRIM', column.collation
|
||||||
|
end
|
||||||
|
|
||||||
|
test "add column with collation" do
|
||||||
|
@connection.add_column :collation_table_sqlite3, :title, :string, collation: 'RTRIM'
|
||||||
|
|
||||||
|
column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == 'title' }
|
||||||
|
assert_equal :string, column.type
|
||||||
|
assert_equal 'RTRIM', column.collation
|
||||||
|
end
|
||||||
|
|
||||||
|
test "change column with collation" do
|
||||||
|
@connection.add_column :collation_table_sqlite3, :description, :string
|
||||||
|
@connection.change_column :collation_table_sqlite3, :description, :text, collation: 'RTRIM'
|
||||||
|
|
||||||
|
column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == 'description' }
|
||||||
|
assert_equal :text, column.type
|
||||||
|
assert_equal 'RTRIM', column.collation
|
||||||
|
end
|
||||||
|
|
||||||
|
test "schema dump includes collation" do
|
||||||
|
output = dump_table_schema("collation_table_sqlite3")
|
||||||
|
assert_match %r{t.string\s+"string_nocase",\s+collation: "NOCASE"$}, output
|
||||||
|
assert_match %r{t.text\s+"text_rtrim",\s+collation: "RTRIM"$}, output
|
||||||
|
end
|
||||||
|
end
|
|
@ -421,14 +421,14 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_statement_closed
|
def test_statement_closed
|
||||||
db = SQLite3::Database.new(ActiveRecord::Base.
|
db = ::SQLite3::Database.new(ActiveRecord::Base.
|
||||||
configurations['arunit']['database'])
|
configurations['arunit']['database'])
|
||||||
statement = SQLite3::Statement.new(db,
|
statement = ::SQLite3::Statement.new(db,
|
||||||
'CREATE TABLE statement_test (number integer not null)')
|
'CREATE TABLE statement_test (number integer not null)')
|
||||||
statement.stubs(:step).raises(SQLite3::BusyException, 'busy')
|
statement.stubs(:step).raises(::SQLite3::BusyException, 'busy')
|
||||||
statement.stubs(:columns).once.returns([])
|
statement.stubs(:columns).once.returns([])
|
||||||
statement.expects(:close).once
|
statement.expects(:close).once
|
||||||
SQLite3::Statement.stubs(:new).returns(statement)
|
::SQLite3::Statement.stubs(:new).returns(statement)
|
||||||
|
|
||||||
assert_raises ActiveRecord::StatementInvalid do
|
assert_raises ActiveRecord::StatementInvalid do
|
||||||
@conn.exec_query 'select * from statement_test'
|
@conn.exec_query 'select * from statement_test'
|
||||||
|
|
|
@ -81,7 +81,7 @@ module ActiveRecord
|
||||||
oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im]
|
oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im]
|
||||||
mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /]
|
mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /]
|
||||||
postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
|
postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
|
||||||
sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im]
|
sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im, /^\s*SELECT sql\b.*\bFROM sqlite_master/im]
|
||||||
|
|
||||||
[oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql|
|
[oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql|
|
||||||
ignored_sql.concat db_ignored_sql
|
ignored_sql.concat db_ignored_sql
|
||||||
|
|
Loading…
Reference in a new issue