Add collation support for string and text columns in SQLite3

This commit is contained in:
Akshay Vishnoi 2015-05-24 11:54:02 +05:30
parent 902360b77f
commit 3932912a59
6 changed files with 139 additions and 9 deletions

View File

@ -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
fixtures:

View File

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

View File

@ -1,5 +1,6 @@
require 'active_record/connection_adapters/abstract_adapter'
require 'active_record/connection_adapters/statement_pool'
require 'active_record/connection_adapters/sqlite3/schema_creation'
gem 'sqlite3', '~> 1.3.6'
require 'sqlite3'
@ -84,6 +85,10 @@ module ActiveRecord
end
end
def schema_creation # :nodoc:
SQLite3::SchemaCreation.new self
end
def initialize(connection, logger, connection_options, config)
super(connection, logger)
@ -344,9 +349,10 @@ module ActiveRecord
field["dflt_value"] = $1.gsub('""', '"')
end
collation = field['collation']
sql_type = field['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
@ -441,6 +447,7 @@ module ActiveRecord
self.null = options[:null] if options.include?(:null)
self.precision = options[:precision] if options.include?(:precision)
self.scale = options[:scale] if options.include?(:scale)
self.collation = options[:collation] if options.include?(:collation)
end
end
end
@ -454,9 +461,9 @@ module ActiveRecord
protected
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?
structure
table_structure_with_collation(table_name, structure)
end
def alter_table(table_name, options = {}) #:nodoc:
@ -491,7 +498,7 @@ module ActiveRecord
@definition.column(column_name, column.type,
:limit => column.limit, :default => column.default,
:precision => column.precision, :scale => column.scale,
:null => column.null)
:null => column.null, collation: column.collation)
end
yield @definition if block_given?
end
@ -553,6 +560,46 @@ module ActiveRecord
super
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

View 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

View File

@ -421,14 +421,14 @@ module ActiveRecord
end
def test_statement_closed
db = SQLite3::Database.new(ActiveRecord::Base.
db = ::SQLite3::Database.new(ActiveRecord::Base.
configurations['arunit']['database'])
statement = SQLite3::Statement.new(db,
statement = ::SQLite3::Statement.new(db,
'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.expects(:close).once
SQLite3::Statement.stubs(:new).returns(statement)
::SQLite3::Statement.stubs(:new).returns(statement)
assert_raises ActiveRecord::StatementInvalid do
@conn.exec_query 'select * from statement_test'

View File

@ -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]
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]
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|
ignored_sql.concat db_ignored_sql