diff options
Diffstat (limited to 'activerecord')
18 files changed, 150 insertions, 68 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index db1221dc75..83e67f9a09 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -2,6 +2,17 @@ *Ryuta Kamizono* +* PostgreSQL: `:collation` support for string and text columns. + + Example: + + create_table :foos do |t| + t.string :string_en, collation: 'en_US.UTF-8' + t.text :text_ja, collation: 'ja_JP.UTF-8' + end + + *Ryuta Kamizono* + * Make `unscope` aware of "less than" and "greater than" conditions. *TAKAHASHI Kazuaki* diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 91c7298983..2c7409b2dc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -123,6 +123,8 @@ module ActiveRecord 'f' end + # Quote date/time values for use in SQL input. Includes microseconds + # if the value is a Time responding to usec. def quoted_date(value) if value.acts_like?(:time) zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index f754df93b6..18d943f452 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -14,10 +14,6 @@ module ActiveRecord send m, o end - def visit_AddColumn(o) - "ADD #{accept(o)}" - end - delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, to: :@conn private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql @@ -25,7 +21,7 @@ module ActiveRecord def visit_AlterTable(o) sql = "ALTER TABLE #{quote_table_name(o.name)} " - sql << o.adds.map { |col| visit_AddColumn col }.join(' ') + sql << o.adds.map { |col| accept col }.join(' ') sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(' ') sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(' ') end @@ -37,6 +33,10 @@ module ActiveRecord column_sql end + def visit_AddColumnDefinition(o) + "ADD #{accept(o.column)}" + end + def visit_TableDefinition(o) create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE " create_sql << "#{quote_table_name(o.name)} " @@ -70,6 +70,7 @@ module ActiveRecord column_options[:after] = o.after column_options[:auto_increment] = o.auto_increment column_options[:primary_key] = o.primary_key + column_options[:collation] = o.collation column_options end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 4761024ad0..0ccf0c498b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -15,13 +15,16 @@ module ActiveRecord # are typically created by methods in TableDefinition, and added to the # +columns+ attribute of said TableDefinition object, in order to be used # for generating a number of table creation or table changing SQL statements. - class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :sql_type) #:nodoc: + class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type) #:nodoc: def primary_key? primary_key || type.to_sym == :primary_key end end + class AddColumnDefinition < Struct.new(:column) # :nodoc: + end + class ChangeColumnDefinition < Struct.new(:column, :name) #:nodoc: end @@ -227,7 +230,7 @@ module ActiveRecord # The +type+ parameter is normally one of the migrations native types, # which is one of the following: # <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>, - # <tt>:integer</tt>, <tt>:float</tt>, <tt>:decimal</tt>, + # <tt>:integer</tt>, <tt>:bigint</tt>, <tt>:float</tt>, <tt>:decimal</tt>, # <tt>:datetime</tt>, <tt>:time</tt>, <tt>:date</tt>, # <tt>:binary</tt>, <tt>:boolean</tt>. # @@ -434,6 +437,7 @@ module ActiveRecord column.after = options[:after] column.auto_increment = options[:auto_increment] column.primary_key = type == :primary_key || options[:primary_key] + column.collation = options[:collation] column end @@ -476,7 +480,7 @@ module ActiveRecord def add_column(name, type, options) name = name.to_s type = type.to_sym - @adds << @td.new_column_definition(name, type, options) + @adds << AddColumnDefinition.new(@td.new_column_definition(name, type, options)) end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index 999cb0ec5a..deb014ad46 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -35,12 +35,16 @@ module ActiveRecord default = schema_default(column) if column.has_default? spec[:default] = default unless default.nil? + if collation = schema_collation(column) + spec[:collation] = collation + end + spec end # Lists the valid migration options def migration_keys - [:name, :limit, :precision, :scale, :default, :null] + [:name, :limit, :precision, :scale, :default, :null, :collation] end private @@ -56,6 +60,10 @@ module ActiveRecord type.type_cast_for_schema(default) end end + + def schema_collation(column) + column.collation.inspect if column.collation + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index ae42e8ef8d..0705c22a8c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -387,8 +387,8 @@ module ActiveRecord end end - def new_column(name, default, sql_type_metadata = nil, null = true) - Column.new(name, default, sql_type_metadata, null) + def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) + Column.new(name, default, sql_type_metadata, null, default_function, collation) end def lookup_cast_type(sql_type) # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 75c81cadfa..cd51b60616 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -14,7 +14,7 @@ module ActiveRecord end class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition - attr_accessor :charset, :collation + attr_accessor :charset end class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition @@ -28,7 +28,6 @@ module ActiveRecord column.auto_increment = true end column.charset = options[:charset] - column.collation = options[:collation] column end @@ -44,10 +43,6 @@ module ActiveRecord end class SchemaCreation < AbstractAdapter::SchemaCreation - def visit_AddColumn(o) - add_column_position!(super, column_options(o)) - end - private def visit_DropForeignKey(name) @@ -67,6 +62,10 @@ module ActiveRecord create_sql end + def visit_AddColumnDefinition(o) + add_column_position!(super, column_options(o.column)) + end + def visit_ChangeColumnDefinition(o) change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}" add_column_position!(change_column_sql, column_options(o.column)) @@ -75,7 +74,6 @@ module ActiveRecord def column_options(o) column_options = super column_options[:charset] = o.charset - column_options[:collation] = o.collation column_options end @@ -128,20 +126,20 @@ module ActiveRecord spec = super spec.delete(:precision) if /time/ === column.sql_type && column.precision == 0 spec.delete(:limit) if :boolean === column.type + spec + end + + def schema_collation(column) if column.collation && table_name = column.instance_variable_get(:@table_name) @collation_cache ||= {} @collation_cache[table_name] ||= select_one("SHOW TABLE STATUS LIKE '#{table_name}'")["Collation"] - spec[:collation] = column.collation.inspect if column.collation != @collation_cache[table_name] + column.collation.inspect if column.collation != @collation_cache[table_name] end - spec - end - - def migration_keys - super + [:collation] end + private :schema_collation class Column < ConnectionAdapters::Column # :nodoc: - delegate :strict, :collation, :extra, to: :sql_type_metadata, allow_nil: true + delegate :strict, :extra, to: :sql_type_metadata, allow_nil: true def initialize(*) super @@ -195,12 +193,11 @@ module ActiveRecord end class MysqlTypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc: - attr_reader :collation, :extra, :strict + attr_reader :extra, :strict - def initialize(type_metadata, collation: "", extra: "", strict: false) + def initialize(type_metadata, extra: "", strict: false) super(type_metadata) @type_metadata = type_metadata - @collation = collation @extra = extra @strict = strict end @@ -218,7 +215,7 @@ module ActiveRecord protected def attributes_for_hash - [self.class, @type_metadata, collation, extra, strict] + [self.class, @type_metadata, extra, strict] end end @@ -342,8 +339,8 @@ module ActiveRecord raise NotImplementedError end - def new_column(field, default, sql_type_metadata = nil, null = true) # :nodoc: - Column.new(field, default, sql_type_metadata, null) + def new_column(field, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc: + Column.new(field, default, sql_type_metadata, null, default_function, collation) end # Must return the MySQL error number from the exception, if the exception has an @@ -566,8 +563,8 @@ module ActiveRecord each_hash(result).map do |field| field_name = set_field_encoding(field[:Field]) sql_type = field[:Type] - type_metadata = fetch_type_metadata(sql_type, field[:Collation], field[:Extra]) - new_column(field_name, field[:Default], type_metadata, field[:Null] == "YES") + type_metadata = fetch_type_metadata(sql_type, field[:Extra]) + new_column(field_name, field[:Default], type_metadata, field[:Null] == "YES", nil, field[:Collation]) end end end @@ -634,7 +631,7 @@ module ActiveRecord change_column table_name, column_name, column.sql_type, :default => default end - def change_column_null(table_name, column_name, null, default = nil) + def change_column_null(table_name, column_name, null, default = nil) #:nodoc: column = column_for(table_name, column_name) unless null || default.nil? @@ -836,8 +833,8 @@ module ActiveRecord end end - def fetch_type_metadata(sql_type, collation = "", extra = "") - MysqlTypeMetadata.new(super(sql_type), collation: collation, extra: extra, strict: strict_mode?) + def fetch_type_metadata(sql_type, extra = "") + MysqlTypeMetadata.new(super(sql_type), extra: extra, strict: strict_mode?) end # MySQL is too stupid to create a temporary table for use subquery, so we have @@ -892,7 +889,7 @@ module ActiveRecord def add_column_sql(table_name, column_name, type, options = {}) td = create_table_definition(table_name) cd = td.new_column_definition(column_name, type, options) - schema_creation.visit_AddColumn cd + schema_creation.accept(AddColumnDefinition.new(cd)) end def change_column_sql(table_name, column_name, type, options = {}) diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index f4dda5154e..4b95b0681d 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -12,7 +12,7 @@ module ActiveRecord ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ end - attr_reader :name, :null, :sql_type_metadata, :default, :default_function + attr_reader :name, :null, :sql_type_metadata, :default, :default_function, :collation delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true @@ -22,12 +22,13 @@ module ActiveRecord # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>. # +sql_type_metadata+ is various information about the type of the column # +null+ determines if this column allows +NULL+ values. - def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil) + def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) @name = name @sql_type_metadata = sql_type_metadata @null = null @default = default @default_function = default_function + @collation = collation @table_name = nil end @@ -60,7 +61,7 @@ module ActiveRecord protected def attributes_for_hash - [self.class, name, default, sql_type_metadata, null, default_function] + [self.class, name, default, sql_type_metadata, null, default_function, collation] end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 21631be25c..e97e82f056 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -1,6 +1,6 @@ require 'active_record/connection_adapters/abstract_mysql_adapter' -gem 'mysql2', '~> 0.3.13' +gem 'mysql2', '~> 0.3.18' require 'mysql2' module ActiveRecord diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index b7755c4593..f175730551 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -40,8 +40,7 @@ module ActiveRecord PGconn.quote_ident(name.to_s) end - # Quote date/time values for use in SQL input. Includes microseconds - # if the value is a Time responding to usec. + # Quote date/time values for use in SQL input. def quoted_date(value) #:nodoc: if value.year <= 0 bce_year = format("%04d", -value.year + 1) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 70fc1a6bd8..662c6b4d38 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -8,6 +8,13 @@ module ActiveRecord o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.array) super end + + def add_column_options!(sql, options) + if options[:collation] + sql << " COLLATE \"#{options[:collation]}\"" + end + super + end end module SchemaStatements @@ -159,18 +166,18 @@ module ActiveRecord # Returns the list of all column definitions for a table. def columns(table_name) # Limit, precision, and scale are all handled by the superclass. - column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod| + column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod, collation| oid = oid.to_i fmod = fmod.to_i type_metadata = fetch_type_metadata(column_name, type, oid, fmod) default_value = extract_value_from_default(default) default_function = extract_default_function(default_value, default) - new_column(column_name, default_value, type_metadata, !notnull, default_function) + new_column(column_name, default_value, type_metadata, !notnull, default_function, collation) end end - def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil) # :nodoc: - PostgreSQLColumn.new(name, default, sql_type_metadata, null, default_function) + def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc: + PostgreSQLColumn.new(name, default, sql_type_metadata, null, default_function, collation) end # Returns the current database name. @@ -403,13 +410,15 @@ module ActiveRecord super end - # Changes the column of a table. - def change_column(table_name, column_name, type, options = {}) + def change_column(table_name, column_name, type, options = {}) #:nodoc: clear_cache! quoted_table_name = quote_table_name(table_name) quoted_column_name = quote_column_name(column_name) sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale], options[:array]) sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}" + if options[:collation] + sql << " COLLATE \"#{options[:collation]}\"" + end if options[:using] sql << " USING #{options[:using]}" elsif options[:cast_as] @@ -438,7 +447,7 @@ module ActiveRecord end end - def change_column_null(table_name, column_name, null, default = nil) + def change_column_null(table_name, column_name, null, default = nil) #:nodoc: clear_cache! unless null || default.nil? column = column_for(table_name, column_name) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 332ac9d88c..7e15c2ab26 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -772,7 +772,9 @@ module ActiveRecord def column_definitions(table_name) # :nodoc: exec_query(<<-end_sql, 'SCHEMA').rows SELECT a.attname, format_type(a.atttypid, a.atttypmod), - pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod + pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod, + (SELECT c.collname FROM pg_collation c, pg_type t + WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation) FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 7e184dd510..3186769510 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -450,7 +450,7 @@ module ActiveRecord end end - def change_column_null(table_name, column_name, null, default = nil) + def change_column_null(table_name, column_name, null, default = nil) #:nodoc: unless null || default.nil? exec_query("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") end diff --git a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb index 340fc95503..345122b1ad 100644 --- a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb +++ b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb @@ -1,5 +1,4 @@ require "cases/helper" -require 'models/person' class MysqlCaseSensitivityTest < ActiveRecord::TestCase class CollationTest < ActiveRecord::Base diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb index 09bebf3071..ccf3d84a44 100644 --- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb +++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb @@ -1,5 +1,4 @@ require "cases/helper" -require 'models/person' class Mysql2CaseSensitivityTest < ActiveRecord::TestCase class CollationTest < ActiveRecord::Base diff --git a/activerecord/test/cases/adapters/postgresql/collation_test.rb b/activerecord/test/cases/adapters/postgresql/collation_test.rb new file mode 100644 index 0000000000..17ef5f304c --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/collation_test.rb @@ -0,0 +1,53 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class PostgresqlCollationTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table :postgresql_collations, force: true do |t| + t.string :string_c, collation: 'C' + t.text :text_posix, collation: 'POSIX' + end + end + + def teardown + @connection.drop_table :postgresql_collations, if_exists: true + end + + test "string column with collation" do + column = @connection.columns(:postgresql_collations).find { |c| c.name == 'string_c' } + assert_equal :string, column.type + assert_equal 'C', column.collation + end + + test "text column with collation" do + column = @connection.columns(:postgresql_collations).find { |c| c.name == 'text_posix' } + assert_equal :text, column.type + assert_equal 'POSIX', column.collation + end + + test "add column with collation" do + @connection.add_column :postgresql_collations, :title, :string, collation: 'C' + + column = @connection.columns(:postgresql_collations).find { |c| c.name == 'title' } + assert_equal :string, column.type + assert_equal 'C', column.collation + end + + test "change column with collation" do + @connection.add_column :postgresql_collations, :description, :string + @connection.change_column :postgresql_collations, :description, :text, collation: 'POSIX' + + column = @connection.columns(:postgresql_collations).find { |c| c.name == 'description' } + assert_equal :text, column.type + assert_equal 'POSIX', column.collation + end + + test "schema dump includes collation" do + output = dump_table_schema("postgresql_collations") + assert_match %r{t.string\s+"string_c",\s+collation: "C"$}, output + assert_match %r{t.text\s+"text_posix",\s+collation: "POSIX"$}, output + end +end diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index 008503bc24..872fa595b4 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,5 +1,16 @@ ActiveRecord::Schema.define do + enable_extension!('uuid-ossp', ActiveRecord::Base.connection) + + create_table :uuid_parents, id: :uuid, force: true do |t| + t.string :name + end + + create_table :uuid_children, id: :uuid, force: true do |t| + t.string :name + t.uuid :uuid_parent_id + end + %w(postgresql_times postgresql_oids defaults postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name| drop_table table_name, if_exists: true diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 7b42f8a4a5..66f8f1611d 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -6,20 +6,6 @@ ActiveRecord::Schema.define do end end - #put adapter specific setup here - case adapter_name - when "PostgreSQL" - enable_extension!('uuid-ossp', ActiveRecord::Base.connection) - create_table :uuid_parents, id: :uuid, force: true do |t| - t.string :name - end - create_table :uuid_children, id: :uuid, force: true do |t| - t.string :name - t.uuid :uuid_parent_id - end - end - - # ------------------------------------------------------------------- # # # # Please keep these create table statements in alphabetical order # |