diff options
Diffstat (limited to 'activerecord/test/cases/adapters/postgresql')
44 files changed, 5915 insertions, 0 deletions
diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb new file mode 100644 index 0000000000..308ad1d854 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require "cases/helper" + +class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase + def setup + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do + def execute(sql, name = nil) sql end + end + end + + teardown do + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do + remove_method :execute + end + end + + def test_create_database_with_encoding + assert_equal %(CREATE DATABASE "matt" ENCODING = 'utf8'), create_database(:matt) + assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'latin1'), create_database(:aimonetti, encoding: :latin1) + assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'latin1'), create_database(:aimonetti, "encoding" => :latin1) + end + + def test_create_database_with_collation_and_ctype + assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'UTF8' LC_COLLATE = 'ja_JP.UTF8' LC_CTYPE = 'ja_JP.UTF8'), create_database(:aimonetti, encoding: :"UTF8", collation: :"ja_JP.UTF8", ctype: :"ja_JP.UTF8") + end + + def test_add_index + # add_index calls index_name_exists? which can't work since execute is stubbed + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_exists?) { |*| false } + + expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" ("last_name") WHERE state = 'active') + assert_equal expected, add_index(:people, :last_name, unique: true, where: "state = 'active'") + + expected = %(CREATE UNIQUE INDEX "index_people_on_lower_last_name" ON "people" (lower(last_name))) + assert_equal expected, add_index(:people, "lower(last_name)", unique: true) + + expected = %(CREATE UNIQUE INDEX "index_people_on_last_name_varchar_pattern_ops" ON "people" (last_name varchar_pattern_ops)) + assert_equal expected, add_index(:people, "last_name varchar_pattern_ops", unique: true) + + expected = %(CREATE INDEX CONCURRENTLY "index_people_on_last_name" ON "people" ("last_name")) + assert_equal expected, add_index(:people, :last_name, algorithm: :concurrently) + + expected = %(CREATE INDEX "index_people_on_last_name_and_first_name" ON "people" ("last_name" DESC, "first_name" ASC)) + assert_equal expected, add_index(:people, [:last_name, :first_name], order: { last_name: :desc, first_name: :asc }) + assert_equal expected, add_index(:people, ["last_name", :first_name], order: { last_name: :desc, "first_name" => :asc }) + + %w(gin gist hash btree).each do |type| + expected = %(CREATE INDEX "index_people_on_last_name" ON "people" USING #{type} ("last_name")) + assert_equal expected, add_index(:people, :last_name, using: type) + + expected = %(CREATE INDEX CONCURRENTLY "index_people_on_last_name" ON "people" USING #{type} ("last_name")) + assert_equal expected, add_index(:people, :last_name, using: type, algorithm: :concurrently) + + expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" USING #{type} ("last_name") WHERE state = 'active') + assert_equal expected, add_index(:people, :last_name, using: type, unique: true, where: "state = 'active'") + + expected = %(CREATE UNIQUE INDEX "index_people_on_lower_last_name" ON "people" USING #{type} (lower(last_name))) + assert_equal expected, add_index(:people, "lower(last_name)", using: type, unique: true) + end + + expected = %(CREATE INDEX "index_people_on_last_name" ON "people" USING gist ("last_name" bpchar_pattern_ops)) + assert_equal expected, add_index(:people, :last_name, using: :gist, opclass: { last_name: :bpchar_pattern_ops }) + + expected = %(CREATE INDEX "index_people_on_last_name_and_first_name" ON "people" ("last_name" DESC NULLS LAST, "first_name" ASC)) + assert_equal expected, add_index(:people, [:last_name, :first_name], order: { last_name: "DESC NULLS LAST", first_name: :asc }) + + expected = %(CREATE INDEX "index_people_on_last_name" ON "people" ("last_name" NULLS FIRST)) + assert_equal expected, add_index(:people, :last_name, order: "NULLS FIRST") + + assert_raise ArgumentError do + add_index(:people, :last_name, algorithm: :copy) + end + + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :remove_method, :index_name_exists? + end + + def test_remove_index + # remove_index calls index_name_for_remove which can't work since execute is stubbed + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_for_remove) do |*| + "index_people_on_last_name" + end + + expected = %(DROP INDEX CONCURRENTLY "index_people_on_last_name") + assert_equal expected, remove_index(:people, name: "index_people_on_last_name", algorithm: :concurrently) + + assert_raise ArgumentError do + add_index(:people, :last_name, algorithm: :copy) + end + + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :remove_method, :index_name_for_remove + end + + def test_remove_index_when_name_is_specified + expected = %(DROP INDEX CONCURRENTLY "index_people_on_last_name") + assert_equal expected, remove_index(:people, name: "index_people_on_last_name", algorithm: :concurrently) + end + + def test_remove_index_with_wrong_option + assert_raises ArgumentError do + remove_index(:people, coulmn: :last_name) + end + end + + private + def method_missing(method_symbol, *arguments) + ActiveRecord::Base.connection.send(method_symbol, *arguments) + end +end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb new file mode 100644 index 0000000000..58fa7532a2 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -0,0 +1,390 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/schema_dumping_helper" + +class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + include InTimeZone + + class PgArray < ActiveRecord::Base + self.table_name = "pg_arrays" + end + + def setup + @connection = ActiveRecord::Base.connection + + enable_extension!("hstore", @connection) + + @connection.transaction do + @connection.create_table("pg_arrays") do |t| + t.string "tags", array: true, limit: 255 + t.integer "ratings", array: true + t.datetime :datetimes, array: true + t.hstore :hstores, array: true + t.decimal :decimals, array: true, default: [], precision: 10, scale: 2 + t.timestamp :timestamps, array: true, default: [], precision: 6 + end + end + PgArray.reset_column_information + @column = PgArray.columns_hash["tags"] + @type = PgArray.type_for_attribute("tags") + end + + teardown do + @connection.drop_table "pg_arrays", if_exists: true + disable_extension!("hstore", @connection) + end + + def test_column + assert_equal :string, @column.type + assert_equal "character varying(255)", @column.sql_type + assert_predicate @column, :array? + assert_not_predicate @type, :binary? + + ratings_column = PgArray.columns_hash["ratings"] + assert_equal :integer, ratings_column.type + assert_predicate ratings_column, :array? + end + + def test_not_compatible_with_serialize_array + new_klass = Class.new(PgArray) do + serialize :tags, Array + end + assert_raises(ActiveRecord::AttributeMethods::Serialization::ColumnNotSerializableError) do + new_klass.new + end + end + + class MyTags + def initialize(tags); @tags = tags end + def to_a; @tags end + def self.load(tags); new(tags) end + def self.dump(object); object.to_a end + end + + def test_array_with_serialized_attributes + new_klass = Class.new(PgArray) do + serialize :tags, MyTags + end + + new_klass.create!(tags: MyTags.new(["one", "two"])) + record = new_klass.first + + assert_instance_of MyTags, record.tags + assert_equal ["one", "two"], record.tags.to_a + + record.tags = MyTags.new(["three", "four"]) + record.save! + + assert_equal ["three", "four"], record.reload.tags.to_a + end + + def test_default + @connection.add_column "pg_arrays", "score", :integer, array: true, default: [4, 4, 2] + PgArray.reset_column_information + + assert_equal([4, 4, 2], PgArray.column_defaults["score"]) + assert_equal([4, 4, 2], PgArray.new.score) + ensure + PgArray.reset_column_information + end + + def test_default_strings + @connection.add_column "pg_arrays", "names", :string, array: true, default: ["foo", "bar"] + PgArray.reset_column_information + + assert_equal(["foo", "bar"], PgArray.column_defaults["names"]) + assert_equal(["foo", "bar"], PgArray.new.names) + ensure + PgArray.reset_column_information + end + + def test_change_column_with_array + @connection.add_column :pg_arrays, :snippets, :string, array: true, default: [] + @connection.change_column :pg_arrays, :snippets, :text, array: true, default: [] + + PgArray.reset_column_information + column = PgArray.columns_hash["snippets"] + + assert_equal :text, column.type + assert_equal [], PgArray.column_defaults["snippets"] + assert_predicate column, :array? + end + + def test_change_column_cant_make_non_array_column_to_array + @connection.add_column :pg_arrays, :a_string, :string + assert_raises ActiveRecord::StatementInvalid do + @connection.transaction do + @connection.change_column :pg_arrays, :a_string, :string, array: true + end + end + end + + def test_change_column_default_with_array + @connection.change_column_default :pg_arrays, :tags, [] + + PgArray.reset_column_information + assert_equal [], PgArray.column_defaults["tags"] + end + + def test_type_cast_array + assert_equal(["1", "2", "3"], @type.deserialize("{1,2,3}")) + assert_equal([], @type.deserialize("{}")) + assert_equal([nil], @type.deserialize("{NULL}")) + end + + def test_type_cast_integers + x = PgArray.new(ratings: ["1", "2"]) + + assert_equal([1, 2], x.ratings) + + x.save! + x.reload + + assert_equal([1, 2], x.ratings) + end + + def test_schema_dump_with_shorthand + output = dump_table_schema "pg_arrays" + assert_match %r[t\.string\s+"tags",\s+limit: 255,\s+array: true], output + assert_match %r[t\.integer\s+"ratings",\s+array: true], output + assert_match %r[t\.decimal\s+"decimals",\s+precision: 10,\s+scale: 2,\s+default: \[\],\s+array: true], output + end + + def test_select_with_strings + @connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')" + x = PgArray.first + assert_equal(["1", "2", "3"], x.tags) + end + + def test_rewrite_with_strings + @connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')" + x = PgArray.first + x.tags = ["1", "2", "3", "4"] + x.save! + assert_equal ["1", "2", "3", "4"], x.reload.tags + end + + def test_select_with_integers + @connection.execute "insert into pg_arrays (ratings) VALUES ('{1,2,3}')" + x = PgArray.first + assert_equal([1, 2, 3], x.ratings) + end + + def test_rewrite_with_integers + @connection.execute "insert into pg_arrays (ratings) VALUES ('{1,2,3}')" + x = PgArray.first + x.ratings = [2, "3", 4] + x.save! + assert_equal [2, 3, 4], x.reload.ratings + end + + def test_multi_dimensional_with_strings + assert_cycle(:tags, [[["1"], ["2"]], [["2"], ["3"]]]) + end + + def test_with_empty_strings + assert_cycle(:tags, [ "1", "2", "", "4", "", "5" ]) + end + + def test_with_multi_dimensional_empty_strings + assert_cycle(:tags, [[["1", "2"], ["", "4"], ["", "5"]]]) + end + + def test_with_arbitrary_whitespace + assert_cycle(:tags, [[["1", "2"], [" ", "4"], [" ", "5"]]]) + end + + def test_multi_dimensional_with_integers + assert_cycle(:ratings, [[[1], [7]], [[8], [10]]]) + end + + def test_strings_with_quotes + assert_cycle(:tags, ["this has", 'some "s that need to be escaped"']) + end + + def test_strings_with_commas + assert_cycle(:tags, ["this,has", "many,values"]) + end + + def test_strings_with_array_delimiters + assert_cycle(:tags, ["{", "}"]) + end + + def test_strings_with_null_strings + assert_cycle(:tags, ["NULL", "NULL"]) + end + + def test_contains_nils + assert_cycle(:tags, ["1", nil, nil]) + end + + def test_insert_fixture + tag_values = ["val1", "val2", "val3_with_'_multiple_quote_'_chars"] + @connection.insert_fixture({ "tags" => tag_values }, "pg_arrays") + assert_equal(PgArray.last.tags, tag_values) + end + + def test_insert_fixtures + tag_values = ["val1", "val2", "val3_with_'_multiple_quote_'_chars"] + assert_deprecated do + @connection.insert_fixtures([{ "tags" => tag_values }], "pg_arrays") + end + assert_equal(PgArray.last.tags, tag_values) + end + + def test_attribute_for_inspect_for_array_field + record = PgArray.new { |a| a.ratings = (1..10).to_a } + assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]", record.attribute_for_inspect(:ratings)) + end + + def test_attribute_for_inspect_for_array_field_for_large_array + record = PgArray.new { |a| a.ratings = (1..11).to_a } + assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]", record.attribute_for_inspect(:ratings)) + end + + def test_escaping + unknown = 'foo\\",bar,baz,\\' + tags = ["hello_#{unknown}"] + ar = PgArray.create!(tags: tags) + ar.reload + assert_equal tags, ar.tags + end + + def test_string_quoting_rules_match_pg_behavior + tags = ["", "one{", "two}", %(three"), "four\\", "five ", "six\t", "seven\n", "eight,", "nine", "ten\r", "NULL"] + x = PgArray.create!(tags: tags) + x.reload + + assert_not_predicate x, :changed? + end + + def test_quoting_non_standard_delimiters + strings = ["hello,", "world;"] + oid = ActiveRecord::ConnectionAdapters::PostgreSQL::OID + comma_delim = oid::Array.new(ActiveRecord::Type::String.new, ",") + semicolon_delim = oid::Array.new(ActiveRecord::Type::String.new, ";") + conn = PgArray.connection + + assert_equal %({"hello,",world;}), conn.type_cast(comma_delim.serialize(strings)) + assert_equal %({hello,;"world;"}), conn.type_cast(semicolon_delim.serialize(strings)) + end + + def test_mutate_array + x = PgArray.create!(tags: %w(one two)) + + x.tags << "three" + x.save! + x.reload + + assert_equal %w(one two three), x.tags + assert_not_predicate x, :changed? + end + + def test_mutate_value_in_array + x = PgArray.create!(hstores: [{ a: "a" }, { b: "b" }]) + + x.hstores.first["a"] = "c" + x.save! + x.reload + + assert_equal [{ "a" => "c" }, { "b" => "b" }], x.hstores + assert_not_predicate x, :changed? + end + + def test_datetime_with_timezone_awareness + tz = "Pacific Time (US & Canada)" + + in_time_zone tz do + PgArray.reset_column_information + time_string = Time.current.to_s + time = Time.zone.parse(time_string) + + record = PgArray.new(datetimes: [time_string]) + assert_equal [time], record.datetimes + assert_equal ActiveSupport::TimeZone[tz], record.datetimes.first.time_zone + + record.save! + record.reload + + assert_equal [time], record.datetimes + assert_equal ActiveSupport::TimeZone[tz], record.datetimes.first.time_zone + end + end + + def test_assigning_non_array_value + record = PgArray.new(tags: "not-an-array") + assert_equal [], record.tags + assert_equal "not-an-array", record.tags_before_type_cast + assert record.save + assert_equal record.tags, record.reload.tags + end + + def test_assigning_empty_string + record = PgArray.new(tags: "") + assert_equal [], record.tags + assert_equal "", record.tags_before_type_cast + assert record.save + assert_equal record.tags, record.reload.tags + end + + def test_assigning_valid_pg_array_literal + record = PgArray.new(tags: "{1,2,3}") + assert_equal ["1", "2", "3"], record.tags + assert_equal "{1,2,3}", record.tags_before_type_cast + assert record.save + assert_equal record.tags, record.reload.tags + end + + def test_where_by_attribute_with_array + tags = ["black", "blue"] + record = PgArray.create!(tags: tags) + assert_equal record, PgArray.where(tags: tags).take + end + + def test_uniqueness_validation + klass = Class.new(PgArray) do + validates_uniqueness_of :tags + + def self.model_name; ActiveModel::Name.new(PgArray) end + end + e1 = klass.create("tags" => ["black", "blue"]) + assert e1.persisted?, "Saving e1" + + e2 = klass.create("tags" => ["black", "blue"]) + assert !e2.persisted?, "e2 shouldn't be valid" + assert e2.errors[:tags].any?, "Should have errors for tags" + assert_equal ["has already been taken"], e2.errors[:tags], "Should have uniqueness message for tags" + end + + def test_encoding_arrays_of_utf8_strings + arrays_of_utf8_strings = %w(nový ファイル) + assert_equal arrays_of_utf8_strings, @type.deserialize(@type.serialize(arrays_of_utf8_strings)) + assert_equal [arrays_of_utf8_strings], @type.deserialize(@type.serialize([arrays_of_utf8_strings])) + end + + def test_precision_is_respected_on_timestamp_columns + time = Time.now.change(usec: 123) + record = PgArray.create!(timestamps: [time]) + + assert_equal 123, record.timestamps.first.usec + record.reload + assert_equal 123, record.timestamps.first.usec + end + + private + def assert_cycle(field, array) + # test creation + x = PgArray.create!(field => array) + x.reload + assert_equal(array, x.public_send(field)) + + # test updating + x = PgArray.create!(field => []) + x.public_send("#{field}=", array) + x.save! + x.reload + assert_equal(array, x.public_send(field)) + end +end diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb new file mode 100644 index 0000000000..c8e728bbb6 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/connection_helper" +require "support/schema_dumping_helper" + +class PostgresqlBitStringTest < ActiveRecord::PostgreSQLTestCase + include ConnectionHelper + include SchemaDumpingHelper + + class PostgresqlBitString < ActiveRecord::Base; end + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table("postgresql_bit_strings", force: true) do |t| + t.bit :a_bit, default: "00000011", limit: 8 + t.bit_varying :a_bit_varying, default: "0011", limit: 4 + t.bit :another_bit + t.bit_varying :another_bit_varying + end + end + + def teardown + return unless @connection + @connection.drop_table "postgresql_bit_strings", if_exists: true + end + + def test_bit_string_column + column = PostgresqlBitString.columns_hash["a_bit"] + assert_equal :bit, column.type + assert_equal "bit(8)", column.sql_type + assert_not_predicate column, :array? + + type = PostgresqlBitString.type_for_attribute("a_bit") + assert_not_predicate type, :binary? + end + + def test_bit_string_varying_column + column = PostgresqlBitString.columns_hash["a_bit_varying"] + assert_equal :bit_varying, column.type + assert_equal "bit varying(4)", column.sql_type + assert_not_predicate column, :array? + + type = PostgresqlBitString.type_for_attribute("a_bit_varying") + assert_not_predicate type, :binary? + end + + def test_default + assert_equal "00000011", PostgresqlBitString.column_defaults["a_bit"] + assert_equal "00000011", PostgresqlBitString.new.a_bit + + assert_equal "0011", PostgresqlBitString.column_defaults["a_bit_varying"] + assert_equal "0011", PostgresqlBitString.new.a_bit_varying + end + + def test_schema_dumping + output = dump_table_schema("postgresql_bit_strings") + assert_match %r{t\.bit\s+"a_bit",\s+limit: 8,\s+default: "00000011"$}, output + assert_match %r{t\.bit_varying\s+"a_bit_varying",\s+limit: 4,\s+default: "0011"$}, output + end + + if ActiveRecord::Base.connection.prepared_statements + def test_assigning_invalid_hex_string_raises_exception + assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit: "FF" } + assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit_varying: "F" } + end + end + + def test_roundtrip + record = PostgresqlBitString.create!(a_bit: "00001010", a_bit_varying: "0101") + assert_equal "00001010", record.a_bit + assert_equal "0101", record.a_bit_varying + assert_nil record.another_bit + assert_nil record.another_bit_varying + + record.a_bit = "11111111" + record.a_bit_varying = "0xF" + record.save! + + assert record.reload + assert_equal "11111111", record.a_bit + assert_equal "1111", record.a_bit_varying + end +end diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb new file mode 100644 index 0000000000..64bb6906cd --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/schema_dumping_helper" + +class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + class ByteaDataType < ActiveRecord::Base + self.table_name = "bytea_data_type" + end + + def setup + @connection = ActiveRecord::Base.connection + begin + @connection.transaction do + @connection.create_table("bytea_data_type") do |t| + t.binary "payload" + t.binary "serialized" + end + end + end + @column = ByteaDataType.columns_hash["payload"] + @type = ByteaDataType.type_for_attribute("payload") + end + + teardown do + @connection.drop_table "bytea_data_type", if_exists: true + end + + def test_column + assert @column.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLColumn) + assert_equal :binary, @column.type + end + + def test_binary_columns_are_limitless_the_upper_limit_is_one_GB + assert_equal "bytea", @connection.type_to_sql(:binary, limit: 100_000) + assert_raise ActiveRecord::ActiveRecordError do + @connection.type_to_sql(:binary, limit: 4294967295) + end + end + + def test_type_cast_binary_converts_the_encoding + assert @column + + data = "\u001F\x8B" + assert_equal("UTF-8", data.encoding.name) + assert_equal("ASCII-8BIT", @type.deserialize(data).encoding.name) + end + + def test_type_cast_binary_value + data = "\u001F\x8B".dup.force_encoding("BINARY") + assert_equal(data, @type.deserialize(data)) + end + + def test_type_case_nil + assert_nil(@type.deserialize(nil)) + end + + def test_read_value + data = "\u001F" + @connection.execute "insert into bytea_data_type (payload) VALUES ('#{data}')" + record = ByteaDataType.first + assert_equal(data, record.payload) + record.delete + end + + def test_read_nil_value + @connection.execute "insert into bytea_data_type (payload) VALUES (null)" + record = ByteaDataType.first + assert_nil(record.payload) + record.delete + end + + def test_write_value + data = "\u001F" + record = ByteaDataType.create(payload: data) + assert_not_predicate record, :new_record? + assert_equal(data, record.payload) + end + + def test_via_to_sql + data = "'\u001F\\" + ByteaDataType.create(payload: data) + sql = ByteaDataType.where(payload: data).select(:payload).to_sql + result = @connection.query(sql) + assert_equal([[data]], result) + end + + def test_via_to_sql_with_complicating_connection + Thread.new do + other_conn = ActiveRecord::Base.connection + other_conn.execute("SET standard_conforming_strings = off") + other_conn.execute("SET escape_string_warning = off") + end.join + + test_via_to_sql + end + + def test_write_binary + data = File.read(File.join(__dir__, "..", "..", "..", "assets", "example.log")) + assert(data.size > 1) + record = ByteaDataType.create(payload: data) + assert_not_predicate record, :new_record? + assert_equal(data, record.payload) + assert_equal(data, ByteaDataType.where(id: record.id).first.payload) + end + + def test_write_nil + record = ByteaDataType.create(payload: nil) + assert_not_predicate record, :new_record? + assert_nil(record.payload) + assert_nil(ByteaDataType.where(id: record.id).first.payload) + end + + class Serializer + def load(str); str; end + def dump(str); str; end + end + + def test_serialize + klass = Class.new(ByteaDataType) { + serialize :serialized, Serializer.new + } + obj = klass.new + obj.serialized = "hello world" + obj.save! + obj.reload + assert_equal "hello world", obj.serialized + end + + def test_schema_dumping + output = dump_table_schema("bytea_data_type") + assert_match %r{t\.binary\s+"payload"$}, output + assert_match %r{t\.binary\s+"serialized"$}, output + end +end diff --git a/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb b/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb new file mode 100644 index 0000000000..305e033642 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "cases/helper" + +class PostgresqlCaseInsensitiveTest < ActiveRecord::PostgreSQLTestCase + class Default < ActiveRecord::Base; end + + def test_case_insensitiveness + connection = ActiveRecord::Base.connection + table = Default.arel_table + + column = Default.columns_hash["char1"] + comparison = connection.case_insensitive_comparison table, :char1, column, nil + assert_match(/lower/i, comparison.to_sql) + + column = Default.columns_hash["char2"] + comparison = connection.case_insensitive_comparison table, :char2, column, nil + assert_match(/lower/i, comparison.to_sql) + + column = Default.columns_hash["char3"] + comparison = connection.case_insensitive_comparison table, :char3, column, nil + assert_match(/lower/i, comparison.to_sql) + + column = Default.columns_hash["multiline_default"] + comparison = connection.case_insensitive_comparison table, :multiline_default, column, nil + assert_match(/lower/i, comparison.to_sql) + end +end diff --git a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb new file mode 100644 index 0000000000..6dba4f3e14 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "cases/helper" + +module ActiveRecord + class Migration + class PGChangeSchemaTest < ActiveRecord::PostgreSQLTestCase + attr_reader :connection + + def setup + super + @connection = ActiveRecord::Base.connection + connection.create_table(:strings) do |t| + t.string :somedate + end + end + + def teardown + connection.drop_table :strings + end + + def test_change_string_to_date + connection.change_column :strings, :somedate, :timestamp, using: 'CAST("somedate" AS timestamp)' + assert_equal :datetime, connection.columns(:strings).find { |c| c.name == "somedate" }.type + end + + def test_change_type_with_symbol + connection.change_column :strings, :somedate, :timestamp, cast_as: :timestamp + assert_equal :datetime, connection.columns(:strings).find { |c| c.name == "somedate" }.type + end + + def test_change_type_with_array + connection.change_column :strings, :somedate, :timestamp, array: true, cast_as: :timestamp + column = connection.columns(:strings).find { |c| c.name == "somedate" } + assert_equal :datetime, column.type + assert_predicate column, :array? + end + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/cidr_test.rb b/activerecord/test/cases/adapters/postgresql/cidr_test.rb new file mode 100644 index 0000000000..f20958fbd2 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/cidr_test.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "cases/helper" +require "ipaddr" + +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + class CidrTest < ActiveRecord::PostgreSQLTestCase + test "type casting IPAddr for database" do + type = OID::Cidr.new + ip = IPAddr.new("255.0.0.0/8") + ip2 = IPAddr.new("127.0.0.1") + + assert_equal "255.0.0.0/8", type.serialize(ip) + assert_equal "127.0.0.1/32", type.serialize(ip2) + end + + test "casting does nothing with non-IPAddr objects" do + type = OID::Cidr.new + + assert_equal "foo", type.serialize("foo") + end + end + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb new file mode 100644 index 0000000000..9eb0b7d99c --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/schema_dumping_helper" + +class PostgresqlCitextTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + class Citext < ActiveRecord::Base + self.table_name = "citexts" + end + + def setup + @connection = ActiveRecord::Base.connection + + enable_extension!("citext", @connection) + + @connection.create_table("citexts") do |t| + t.citext "cival" + end + end + + teardown do + @connection.drop_table "citexts", if_exists: true + disable_extension!("citext", @connection) + end + + def test_citext_enabled + assert @connection.extension_enabled?("citext") + end + + def test_column + column = Citext.columns_hash["cival"] + assert_equal :citext, column.type + assert_equal "citext", column.sql_type + assert_not_predicate column, :array? + + type = Citext.type_for_attribute("cival") + assert_not_predicate type, :binary? + end + + def test_change_table_supports_json + @connection.transaction do + @connection.change_table("citexts") do |t| + t.citext "username" + end + Citext.reset_column_information + column = Citext.columns_hash["username"] + assert_equal :citext, column.type + + raise ActiveRecord::Rollback # reset the schema change + end + ensure + Citext.reset_column_information + end + + def test_write + x = Citext.new(cival: "Some CI Text") + x.save! + citext = Citext.first + assert_equal "Some CI Text", citext.cival + + citext.cival = "Some NEW CI Text" + citext.save! + + assert_equal "Some NEW CI Text", citext.reload.cival + end + + def test_select_case_insensitive + @connection.execute "insert into citexts (cival) values('Cased Text')" + x = Citext.where(cival: "cased text").first + assert_equal "Cased Text", x.cival + end + + def test_schema_dump_with_shorthand + output = dump_table_schema("citexts") + assert_match %r[t\.citext "cival"], output + end +end 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..7468f4c4f8 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/collation_test.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/schema_dumping_helper" + +class PostgresqlCollationTest < ActiveRecord::PostgreSQLTestCase + 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/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb new file mode 100644 index 0000000000..b0ce2694a3 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/connection_helper" + +module PostgresqlCompositeBehavior + include ConnectionHelper + + class PostgresqlComposite < ActiveRecord::Base + self.table_name = "postgresql_composites" + end + + def setup + super + + @connection = ActiveRecord::Base.connection + @connection.transaction do + @connection.execute <<-SQL + CREATE TYPE full_address AS + ( + city VARCHAR(90), + street VARCHAR(90) + ); + SQL + @connection.create_table("postgresql_composites") do |t| + t.column :address, :full_address + end + end + end + + def teardown + super + + @connection.drop_table "postgresql_composites", if_exists: true + @connection.execute "DROP TYPE IF EXISTS full_address" + reset_connection + PostgresqlComposite.reset_column_information + end +end + +# Composites are mapped to `OID::Identity` by default. The user is informed by a warning like: +# "unknown OID 5653508: failed to recognize type of 'address'. It will be treated as String." +# To take full advantage of composite types, we suggest you register your own +OID::Type+. +# See PostgresqlCompositeWithCustomOIDTest +class PostgresqlCompositeTest < ActiveRecord::PostgreSQLTestCase + include PostgresqlCompositeBehavior + + def test_column + ensure_warning_is_issued + + column = PostgresqlComposite.columns_hash["address"] + assert_nil column.type + assert_equal "full_address", column.sql_type + assert_not_predicate column, :array? + + type = PostgresqlComposite.type_for_attribute("address") + assert_not_predicate type, :binary? + end + + def test_composite_mapping + ensure_warning_is_issued + + @connection.execute "INSERT INTO postgresql_composites VALUES (1, ROW('Paris', 'Champs-Élysées'));" + composite = PostgresqlComposite.first + assert_equal "(Paris,Champs-Élysées)", composite.address + + composite.address = "(Paris,Rue Basse)" + composite.save! + + assert_equal '(Paris,"Rue Basse")', composite.reload.address + end + + private + def ensure_warning_is_issued + warning = capture(:stderr) do + PostgresqlComposite.columns_hash + end + assert_match(/unknown OID \d+: failed to recognize type of 'address'\. It will be treated as String\./, warning) + end +end + +class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::PostgreSQLTestCase + include PostgresqlCompositeBehavior + + class FullAddressType < ActiveRecord::Type::Value + def type; :full_address end + + def deserialize(value) + if value =~ /\("?([^",]*)"?,"?([^",]*)"?\)/ + FullAddress.new($1, $2) + end + end + + def cast(value) + value + end + + def serialize(value) + return if value.nil? + "(#{value.city},#{value.street})" + end + end + + FullAddress = Struct.new(:city, :street) + + def setup + super + + @connection.send(:type_map).register_type "full_address", FullAddressType.new + end + + def test_column + column = PostgresqlComposite.columns_hash["address"] + assert_equal :full_address, column.type + assert_equal "full_address", column.sql_type + assert_not_predicate column, :array? + + type = PostgresqlComposite.type_for_attribute("address") + assert_not_predicate type, :binary? + end + + def test_composite_mapping + @connection.execute "INSERT INTO postgresql_composites VALUES (1, ROW('Paris', 'Champs-Élysées'));" + composite = PostgresqlComposite.first + assert_equal "Paris", composite.address.city + assert_equal "Champs-Élysées", composite.address.street + + composite.address = FullAddress.new("Paris", "Rue Basse") + composite.save! + + assert_equal "Paris", composite.reload.address.city + assert_equal "Rue Basse", composite.reload.address.street + end +end diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb new file mode 100644 index 0000000000..d1b3c434e1 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -0,0 +1,272 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/connection_helper" + +module ActiveRecord + class PostgresqlConnectionTest < ActiveRecord::PostgreSQLTestCase + include ConnectionHelper + + class NonExistentTable < ActiveRecord::Base + end + + fixtures :comments + + def setup + super + @subscriber = SQLSubscriber.new + @subscription = ActiveSupport::Notifications.subscribe("sql.active_record", @subscriber) + @connection = ActiveRecord::Base.connection + end + + def teardown + ActiveSupport::Notifications.unsubscribe(@subscription) + super + end + + def test_truncate + count = ActiveRecord::Base.connection.execute("select count(*) from comments").first["count"].to_i + assert_operator count, :>, 0 + ActiveRecord::Base.connection.truncate("comments") + count = ActiveRecord::Base.connection.execute("select count(*) from comments").first["count"].to_i + assert_equal 0, count + end + + def test_encoding + assert_queries(1) do + assert_not_nil @connection.encoding + end + end + + def test_collation + assert_queries(1) do + assert_not_nil @connection.collation + end + end + + def test_ctype + assert_queries(1) do + assert_not_nil @connection.ctype + end + end + + def test_default_client_min_messages + assert_equal "warning", @connection.client_min_messages + end + + # Ensure, we can set connection params using the example of Generic + # Query Optimizer (geqo). It is 'on' per default. + def test_connection_options + params = ActiveRecord::Base.connection_config.dup + params[:options] = "-c geqo=off" + NonExistentTable.establish_connection(params) + + # Verify the connection param has been applied. + expect = NonExistentTable.connection.query("show geqo").first.first + assert_equal "off", expect + end + + def test_reset + @connection.query("ROLLBACK") + @connection.query("SET geqo TO off") + + # Verify the setting has been applied. + expect = @connection.query("show geqo").first.first + assert_equal "off", expect + + @connection.reset! + + # Verify the setting has been cleared. + expect = @connection.query("show geqo").first.first + assert_equal "on", expect + end + + def test_reset_with_transaction + @connection.query("ROLLBACK") + @connection.query("SET geqo TO off") + + # Verify the setting has been applied. + expect = @connection.query("show geqo").first.first + assert_equal "off", expect + + @connection.query("BEGIN") + @connection.reset! + + # Verify the setting has been cleared. + expect = @connection.query("show geqo").first.first + assert_equal "on", expect + end + + def test_tables_logs_name + @connection.tables + assert_equal "SCHEMA", @subscriber.logged[0][1] + end + + def test_indexes_logs_name + @connection.indexes("items") + assert_equal "SCHEMA", @subscriber.logged[0][1] + end + + def test_table_exists_logs_name + @connection.table_exists?("items") + assert_equal "SCHEMA", @subscriber.logged[0][1] + end + + def test_table_alias_length_logs_name + @connection.instance_variable_set("@max_identifier_length", nil) + @connection.table_alias_length + assert_equal "SCHEMA", @subscriber.logged[0][1] + end + + def test_current_database_logs_name + @connection.current_database + assert_equal "SCHEMA", @subscriber.logged[0][1] + end + + def test_encoding_logs_name + @connection.encoding + assert_equal "SCHEMA", @subscriber.logged[0][1] + end + + def test_schema_names_logs_name + @connection.schema_names + assert_equal "SCHEMA", @subscriber.logged[0][1] + end + + if ActiveRecord::Base.connection.prepared_statements + def test_statement_key_is_logged + bind = Relation::QueryAttribute.new(nil, 1, Type::Value.new) + @connection.exec_query("SELECT $1::integer", "SQL", [bind], prepare: true) + name = @subscriber.payloads.last[:statement_name] + assert name + res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(1)") + plan = res.column_types["QUERY PLAN"].deserialize res.rows.first.first + assert_operator plan.length, :>, 0 + end + end + + # Must have PostgreSQL >= 9.2, or with_manual_interventions set to + # true for this test to run. + # + # When prompted, restart the PostgreSQL server with the + # "-m fast" option or kill the individual connection assuming + # you know the incantation to do that. + # To restart PostgreSQL 9.1 on OS X, installed via MacPorts, ... + # sudo su postgres -c "pg_ctl restart -D /opt/local/var/db/postgresql91/defaultdb/ -m fast" + def test_reconnection_after_actual_disconnection_with_verify + original_connection_pid = @connection.query("select pg_backend_pid()") + + # Sanity check. + assert_predicate @connection, :active? + + if @connection.send(:postgresql_version) >= 90200 + secondary_connection = ActiveRecord::Base.connection_pool.checkout + secondary_connection.query("select pg_terminate_backend(#{original_connection_pid.first.first})") + ActiveRecord::Base.connection_pool.checkin(secondary_connection) + elsif ARTest.config["with_manual_interventions"] + puts "Kill the connection now (e.g. by restarting the PostgreSQL " \ + 'server with the "-m fast" option) and then press enter.' + $stdin.gets + else + # We're not capable of terminating the backend ourselves, and + # we're not allowed to seek assistance; bail out without + # actually testing anything. + return + end + + @connection.verify! + + assert_predicate @connection, :active? + + # If we get no exception here, then either we re-connected successfully, or + # we never actually got disconnected. + new_connection_pid = @connection.query("select pg_backend_pid()") + + assert_not_equal original_connection_pid, new_connection_pid, + "umm -- looks like you didn't break the connection, because we're still " \ + "successfully querying with the same connection pid." + ensure + # Repair all fixture connections so other tests won't break. + @fixture_connections.each(&:verify!) + end + + def test_set_session_variable_true + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { debug_print_plan: true })) + set_true = ActiveRecord::Base.connection.exec_query "SHOW DEBUG_PRINT_PLAN" + assert_equal set_true.rows, [["on"]] + end + end + + def test_set_session_variable_false + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { debug_print_plan: false })) + set_false = ActiveRecord::Base.connection.exec_query "SHOW DEBUG_PRINT_PLAN" + assert_equal set_false.rows, [["off"]] + end + end + + def test_set_session_variable_nil + run_without_connection do |orig_connection| + # This should be a no-op that does not raise an error + ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { debug_print_plan: nil })) + end + end + + def test_set_session_variable_default + run_without_connection do |orig_connection| + # This should execute a query that does not raise an error + ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { debug_print_plan: :default })) + end + end + + def test_set_session_timezone + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { timezone: "America/New_York" })) + assert_equal "America/New_York", ActiveRecord::Base.connection.query_value("SHOW TIME ZONE") + end + end + + def test_get_and_release_advisory_lock + lock_id = 5295901941911233559 + list_advisory_locks = <<-SQL + SELECT locktype, + (classid::bigint << 32) | objid::bigint AS lock_id + FROM pg_locks + WHERE locktype = 'advisory' + SQL + + got_lock = @connection.get_advisory_lock(lock_id) + assert got_lock, "get_advisory_lock should have returned true but it didn't" + + advisory_lock = @connection.query(list_advisory_locks).find { |l| l[1] == lock_id } + assert advisory_lock, + "expected to find an advisory lock with lock_id #{lock_id} but there wasn't one" + + released_lock = @connection.release_advisory_lock(lock_id) + assert released_lock, "expected release_advisory_lock to return true but it didn't" + + advisory_locks = @connection.query(list_advisory_locks).select { |l| l[1] == lock_id } + assert_empty advisory_locks, + "expected to have released advisory lock with lock_id #{lock_id} but it was still held" + end + + def test_release_non_existent_advisory_lock + fake_lock_id = 2940075057017742022 + with_warning_suppression do + released_non_existent_lock = @connection.release_advisory_lock(fake_lock_id) + assert_equal released_non_existent_lock, false, + "expected release_advisory_lock to return false when there was no lock to release" + end + end + + private + + def with_warning_suppression + log_level = @connection.client_min_messages + @connection.client_min_messages = "error" + yield + @connection.client_min_messages = log_level + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb new file mode 100644 index 0000000000..b7535d5c9a --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/ddl_helper" + +class PostgresqlTime < ActiveRecord::Base +end + +class PostgresqlOid < ActiveRecord::Base +end + +class PostgresqlLtree < ActiveRecord::Base +end + +class PostgresqlDataTypeTest < ActiveRecord::PostgreSQLTestCase + self.use_transactional_tests = false + + def setup + @connection = ActiveRecord::Base.connection + + @connection.execute("INSERT INTO postgresql_times (id, time_interval, scaled_time_interval) VALUES (1, '1 year 2 days ago', '3 weeks ago')") + @first_time = PostgresqlTime.find(1) + + @connection.execute("INSERT INTO postgresql_oids (id, obj_id) VALUES (1, 1234)") + @first_oid = PostgresqlOid.find(1) + end + + teardown do + [PostgresqlTime, PostgresqlOid].each(&:delete_all) + end + + def test_data_type_of_time_types + assert_equal :interval, @first_time.column_for_attribute(:time_interval).type + assert_equal :interval, @first_time.column_for_attribute(:scaled_time_interval).type + end + + def test_data_type_of_oid_types + assert_equal :oid, @first_oid.column_for_attribute(:obj_id).type + end + + def test_time_values + assert_equal "-1 years -2 days", @first_time.time_interval + assert_equal "-21 days", @first_time.scaled_time_interval + end + + def test_oid_values + assert_equal 1234, @first_oid.obj_id + end + + def test_update_time + @first_time.time_interval = "2 years 3 minutes" + assert @first_time.save + assert @first_time.reload + assert_equal "2 years 00:03:00", @first_time.time_interval + end + + def test_update_oid + new_value = 567890 + @first_oid.obj_id = new_value + assert @first_oid.save + assert @first_oid.reload + assert_equal new_value, @first_oid.obj_id + end + + def test_text_columns_are_limitless_the_upper_limit_is_one_GB + assert_equal "text", @connection.type_to_sql(:text, limit: 100_000) + assert_raise ActiveRecord::ActiveRecordError do + @connection.type_to_sql(:text, limit: 4294967295) + end + end +end + +class PostgresqlInternalDataTypeTest < ActiveRecord::PostgreSQLTestCase + include DdlHelper + + setup do + @connection = ActiveRecord::Base.connection + end + + def test_name_column_type + with_example_table @connection, "ex", "data name" do + column = @connection.columns("ex").find { |col| col.name == "data" } + assert_equal :string, column.type + end + end + + def test_char_column_type + with_example_table @connection, "ex", 'data "char"' do + column = @connection.columns("ex").find { |col| col.name == "data" } + assert_equal :string, column.type + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/date_test.rb b/activerecord/test/cases/adapters/postgresql/date_test.rb new file mode 100644 index 0000000000..a86abac2be --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/date_test.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/topic" + +class PostgresqlDateTest < ActiveRecord::PostgreSQLTestCase + def test_load_infinity_and_beyond + topic = Topic.find_by_sql("SELECT 'infinity'::date AS last_read").first + assert topic.last_read.infinite?, "timestamp should be infinite" + assert_operator topic.last_read, :>, 0 + + topic = Topic.find_by_sql("SELECT '-infinity'::date AS last_read").first + assert topic.last_read.infinite?, "timestamp should be infinite" + assert_operator topic.last_read, :<, 0 + end + + def test_save_infinity_and_beyond + topic = Topic.create!(last_read: 1.0 / 0.0) + assert_equal(1.0 / 0.0, topic.last_read) + + topic = Topic.create!(last_read: -1.0 / 0.0) + assert_equal(-1.0 / 0.0, topic.last_read) + end + + def test_bc_date + date = Date.new(0) - 1.week + topic = Topic.create!(last_read: date) + assert_equal date, Topic.find(topic.id).last_read + end + + def test_bc_date_leap_year + date = Time.utc(-4, 2, 29).to_date + topic = Topic.create!(last_read: date) + assert_equal date, Topic.find(topic.id).last_read + end + + def test_bc_date_year_zero + date = Time.utc(0, 4, 7).to_date + topic = Topic.create!(last_read: date) + assert_equal date, Topic.find(topic.id).last_read + end +end diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb new file mode 100644 index 0000000000..eeaad94c27 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/connection_helper" + +class PostgresqlDomainTest < ActiveRecord::PostgreSQLTestCase + include ConnectionHelper + + class PostgresqlDomain < ActiveRecord::Base + self.table_name = "postgresql_domains" + end + + def setup + @connection = ActiveRecord::Base.connection + @connection.transaction do + @connection.execute "CREATE DOMAIN custom_money as numeric(8,2)" + @connection.create_table("postgresql_domains") do |t| + t.column :price, :custom_money + end + end + end + + teardown do + @connection.drop_table "postgresql_domains", if_exists: true + @connection.execute "DROP DOMAIN IF EXISTS custom_money" + reset_connection + end + + def test_column + column = PostgresqlDomain.columns_hash["price"] + assert_equal :decimal, column.type + assert_equal "custom_money", column.sql_type + assert_not_predicate column, :array? + + type = PostgresqlDomain.type_for_attribute("price") + assert_not_predicate type, :binary? + end + + def test_domain_acts_like_basetype + PostgresqlDomain.create price: "" + record = PostgresqlDomain.first + assert_nil record.price + + record.price = "34.15" + record.save! + + assert_equal BigDecimal("34.15"), record.reload.price + end +end diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb new file mode 100644 index 0000000000..6789ff63e7 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/connection_helper" + +class PostgresqlEnumTest < ActiveRecord::PostgreSQLTestCase + include ConnectionHelper + + class PostgresqlEnum < ActiveRecord::Base + self.table_name = "postgresql_enums" + end + + def setup + @connection = ActiveRecord::Base.connection + @connection.transaction do + @connection.execute <<-SQL + CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy'); + SQL + @connection.create_table("postgresql_enums") do |t| + t.column :current_mood, :mood + end + end + end + + teardown do + @connection.drop_table "postgresql_enums", if_exists: true + @connection.execute "DROP TYPE IF EXISTS mood" + reset_connection + end + + def test_column + column = PostgresqlEnum.columns_hash["current_mood"] + assert_equal :enum, column.type + assert_equal "mood", column.sql_type + assert_not_predicate column, :array? + + type = PostgresqlEnum.type_for_attribute("current_mood") + assert_not_predicate type, :binary? + end + + def test_enum_defaults + @connection.add_column "postgresql_enums", "good_mood", :mood, default: "happy" + PostgresqlEnum.reset_column_information + + assert_equal "happy", PostgresqlEnum.column_defaults["good_mood"] + assert_equal "happy", PostgresqlEnum.new.good_mood + ensure + PostgresqlEnum.reset_column_information + end + + def test_enum_mapping + @connection.execute "INSERT INTO postgresql_enums VALUES (1, 'sad');" + enum = PostgresqlEnum.first + assert_equal "sad", enum.current_mood + + enum.current_mood = "happy" + enum.save! + + assert_equal "happy", enum.reload.current_mood + end + + def test_invalid_enum_update + @connection.execute "INSERT INTO postgresql_enums VALUES (1, 'sad');" + enum = PostgresqlEnum.first + enum.current_mood = "angry" + + assert_raise ActiveRecord::StatementInvalid do + enum.save + end + end + + def test_no_oid_warning + @connection.execute "INSERT INTO postgresql_enums VALUES (1, 'sad');" + stderr_output = capture(:stderr) { PostgresqlEnum.first } + + assert_predicate stderr_output, :blank? + end + + def test_enum_type_cast + enum = PostgresqlEnum.new + enum.current_mood = :happy + + assert_equal "happy", enum.current_mood + end + + def test_assigning_enum_to_nil + model = PostgresqlEnum.new(current_mood: nil) + + assert_nil model.current_mood + assert model.save + assert_nil model.reload.current_mood + end +end diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb new file mode 100644 index 0000000000..be525383e9 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/author" +require "models/post" + +class PostgreSQLExplainTest < ActiveRecord::PostgreSQLTestCase + fixtures :authors + + def test_explain_for_one_query + explain = Author.where(id: 1).explain + assert_match %r(EXPLAIN for: SELECT "authors"\.\* FROM "authors" WHERE "authors"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain + assert_match %(QUERY PLAN), explain + end + + def test_explain_with_eager_loading + explain = Author.where(id: 1).includes(:posts).explain + assert_match %(QUERY PLAN), explain + assert_match %r(EXPLAIN for: SELECT "authors"\.\* FROM "authors" WHERE "authors"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain + assert_match %r(EXPLAIN for: SELECT "posts"\.\* FROM "posts" WHERE "posts"\."author_id" = (?:\$1 \[\["author_id", 1\]\]|1)), explain + end +end diff --git a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb new file mode 100644 index 0000000000..df97ab11e7 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require "cases/helper" + +class PostgresqlExtensionMigrationTest < ActiveRecord::PostgreSQLTestCase + self.use_transactional_tests = false + + class EnableHstore < ActiveRecord::Migration::Current + def change + enable_extension "hstore" + end + end + + class DisableHstore < ActiveRecord::Migration::Current + def change + disable_extension "hstore" + end + end + + def setup + super + + @connection = ActiveRecord::Base.connection + + @old_schema_migration_table_name = ActiveRecord::SchemaMigration.table_name + @old_table_name_prefix = ActiveRecord::Base.table_name_prefix + @old_table_name_suffix = ActiveRecord::Base.table_name_suffix + + ActiveRecord::Base.table_name_prefix = "p_" + ActiveRecord::Base.table_name_suffix = "_s" + ActiveRecord::SchemaMigration.delete_all rescue nil + ActiveRecord::SchemaMigration.table_name = "p_schema_migrations_s" + ActiveRecord::Migration.verbose = false + end + + def teardown + ActiveRecord::Base.table_name_prefix = @old_table_name_prefix + ActiveRecord::Base.table_name_suffix = @old_table_name_suffix + ActiveRecord::SchemaMigration.delete_all rescue nil + ActiveRecord::Migration.verbose = true + ActiveRecord::SchemaMigration.table_name = @old_schema_migration_table_name + + super + end + + def test_enable_extension_migration_ignores_prefix_and_suffix + @connection.disable_extension("hstore") + + migrations = [EnableHstore.new(nil, 1)] + ActiveRecord::Migrator.new(:up, migrations).migrate + assert @connection.extension_enabled?("hstore"), "extension hstore should be enabled" + end + + def test_disable_extension_migration_ignores_prefix_and_suffix + @connection.enable_extension("hstore") + + migrations = [DisableHstore.new(nil, 1)] + ActiveRecord::Migrator.new(:up, migrations).migrate + assert_not @connection.extension_enabled?("hstore"), "extension hstore should not be enabled" + end +end diff --git a/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb b/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb new file mode 100644 index 0000000000..4fa315ad23 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/professor" + +if ActiveRecord::Base.connection.supports_foreign_tables? + class ForeignTableTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + class ForeignProfessor < ActiveRecord::Base + self.table_name = "foreign_professors" + end + + class ForeignProfessorWithPk < ForeignProfessor + self.primary_key = "id" + end + + def setup + @professor = Professor.create(name: "Nicola") + + @connection = ActiveRecord::Base.connection + enable_extension!("postgres_fdw", @connection) + + foreign_db_config = ARTest.connection_config["arunit2"] + @connection.execute <<-SQL + CREATE SERVER foreign_server + FOREIGN DATA WRAPPER postgres_fdw + OPTIONS (dbname '#{foreign_db_config["database"]}') + SQL + + @connection.execute <<-SQL + CREATE USER MAPPING FOR CURRENT_USER + SERVER foreign_server + SQL + + @connection.execute <<-SQL + CREATE FOREIGN TABLE foreign_professors ( + id int, + name character varying NOT NULL + ) SERVER foreign_server OPTIONS ( + table_name 'professors' + ) + SQL + end + + def teardown + disable_extension!("postgres_fdw", @connection) + @connection.execute <<-SQL + DROP SERVER IF EXISTS foreign_server CASCADE + SQL + end + + def test_table_exists + table_name = ForeignProfessor.table_name + assert_not ActiveRecord::Base.connection.table_exists?(table_name) + end + + def test_foreign_tables_are_valid_data_sources + table_name = ForeignProfessor.table_name + assert @connection.data_source_exists?(table_name), "'#{table_name}' should be a data source" + end + + def test_foreign_tables + assert_equal ["foreign_professors"], @connection.foreign_tables + end + + def test_foreign_table_exists + assert @connection.foreign_table_exists?("foreign_professors") + assert @connection.foreign_table_exists?(:foreign_professors) + assert_not @connection.foreign_table_exists?("nonexistingtable") + assert_not @connection.foreign_table_exists?("'") + assert_not @connection.foreign_table_exists?(nil) + end + + def test_attribute_names + assert_equal ["id", "name"], ForeignProfessor.attribute_names + end + + def test_attributes + professor = ForeignProfessorWithPk.find(@professor.id) + assert_equal @professor.attributes, professor.attributes + end + + def test_does_not_have_a_primary_key + assert_nil ForeignProfessor.primary_key + end + + def test_insert_record + # Explicit `id` here to avoid complex configurations to implicitly work with remote table + ForeignProfessorWithPk.create!(id: 100, name: "Leonardo") + + professor = ForeignProfessorWithPk.last + assert_equal "Leonardo", professor.name + end + + def test_update_record + professor = ForeignProfessorWithPk.find(@professor.id) + professor.name = "Albert" + professor.save! + professor.reload + assert_equal "Albert", professor.name + end + + def test_delete_record + professor = ForeignProfessorWithPk.find(@professor.id) + assert_difference("ForeignProfessor.count", -1) { professor.destroy } + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb new file mode 100644 index 0000000000..95dee3bf44 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/schema_dumping_helper" + +class PostgresqlFullTextTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + class Tsvector < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table("tsvectors") do |t| + t.tsvector "text_vector" + end + end + + teardown do + @connection.drop_table "tsvectors", if_exists: true + end + + def test_tsvector_column + column = Tsvector.columns_hash["text_vector"] + assert_equal :tsvector, column.type + assert_equal "tsvector", column.sql_type + assert_not_predicate column, :array? + + type = Tsvector.type_for_attribute("text_vector") + assert_not_predicate type, :binary? + end + + def test_update_tsvector + Tsvector.create text_vector: "'text' 'vector'" + tsvector = Tsvector.first + assert_equal "'text' 'vector'", tsvector.text_vector + + tsvector.text_vector = "'new' 'text' 'vector'" + tsvector.save! + assert tsvector.reload + assert_equal "'new' 'text' 'vector'", tsvector.text_vector + end + + def test_schema_dump_with_shorthand + output = dump_table_schema("tsvectors") + assert_match %r{t\.tsvector "text_vector"}, output + end +end diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb new file mode 100644 index 0000000000..8c6f046553 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -0,0 +1,373 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/connection_helper" +require "support/schema_dumping_helper" + +class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase + include ConnectionHelper + include SchemaDumpingHelper + + class PostgresqlPoint < ActiveRecord::Base + attribute :x, :point + attribute :y, :point + attribute :z, :point + attribute :array_of_points, :point, array: true + attribute :legacy_x, :legacy_point + attribute :legacy_y, :legacy_point + attribute :legacy_z, :legacy_point + end + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table("postgresql_points") do |t| + t.point :x + t.point :y, default: [12.2, 13.3] + t.point :z, default: "(14.4,15.5)" + t.point :array_of_points, array: true + t.point :legacy_x + t.point :legacy_y, default: [12.2, 13.3] + t.point :legacy_z, default: "(14.4,15.5)" + end + end + + teardown do + @connection.drop_table "postgresql_points", if_exists: true + end + + def test_column + column = PostgresqlPoint.columns_hash["x"] + assert_equal :point, column.type + assert_equal "point", column.sql_type + assert_not_predicate column, :array? + + type = PostgresqlPoint.type_for_attribute("x") + assert_not_predicate type, :binary? + end + + def test_default + assert_equal ActiveRecord::Point.new(12.2, 13.3), PostgresqlPoint.column_defaults["y"] + assert_equal ActiveRecord::Point.new(12.2, 13.3), PostgresqlPoint.new.y + + assert_equal ActiveRecord::Point.new(14.4, 15.5), PostgresqlPoint.column_defaults["z"] + assert_equal ActiveRecord::Point.new(14.4, 15.5), PostgresqlPoint.new.z + end + + def test_schema_dumping + output = dump_table_schema("postgresql_points") + assert_match %r{t\.point\s+"x"$}, output + assert_match %r{t\.point\s+"y",\s+default: \[12\.2, 13\.3\]$}, output + assert_match %r{t\.point\s+"z",\s+default: \[14\.4, 15\.5\]$}, output + end + + def test_roundtrip + PostgresqlPoint.create! x: [10, 25.2] + record = PostgresqlPoint.first + assert_equal ActiveRecord::Point.new(10, 25.2), record.x + + record.x = ActiveRecord::Point.new(1.1, 2.2) + record.save! + assert record.reload + assert_equal ActiveRecord::Point.new(1.1, 2.2), record.x + end + + def test_mutation + p = PostgresqlPoint.create! x: ActiveRecord::Point.new(10, 20) + + p.x.y = 25 + p.save! + p.reload + + assert_equal ActiveRecord::Point.new(10.0, 25.0), p.x + assert_not_predicate p, :changed? + end + + def test_array_assignment + p = PostgresqlPoint.new(x: [1, 2]) + + assert_equal ActiveRecord::Point.new(1, 2), p.x + end + + def test_string_assignment + p = PostgresqlPoint.new(x: "(1, 2)") + + assert_equal ActiveRecord::Point.new(1, 2), p.x + end + + def test_empty_string_assignment + p = PostgresqlPoint.new(x: "") + assert_nil p.x + end + + def test_array_of_points_round_trip + expected_value = [ + ActiveRecord::Point.new(1, 2), + ActiveRecord::Point.new(2, 3), + ActiveRecord::Point.new(3, 4), + ] + p = PostgresqlPoint.new(array_of_points: expected_value) + + assert_equal expected_value, p.array_of_points + p.save! + p.reload + assert_equal expected_value, p.array_of_points + end + + def test_legacy_column + column = PostgresqlPoint.columns_hash["legacy_x"] + assert_equal :point, column.type + assert_equal "point", column.sql_type + assert_not_predicate column, :array? + + type = PostgresqlPoint.type_for_attribute("legacy_x") + assert_not_predicate type, :binary? + end + + def test_legacy_default + assert_equal [12.2, 13.3], PostgresqlPoint.column_defaults["legacy_y"] + assert_equal [12.2, 13.3], PostgresqlPoint.new.legacy_y + + assert_equal [14.4, 15.5], PostgresqlPoint.column_defaults["legacy_z"] + assert_equal [14.4, 15.5], PostgresqlPoint.new.legacy_z + end + + def test_legacy_schema_dumping + output = dump_table_schema("postgresql_points") + assert_match %r{t\.point\s+"legacy_x"$}, output + assert_match %r{t\.point\s+"legacy_y",\s+default: \[12\.2, 13\.3\]$}, output + assert_match %r{t\.point\s+"legacy_z",\s+default: \[14\.4, 15\.5\]$}, output + end + + def test_legacy_roundtrip + PostgresqlPoint.create! legacy_x: [10, 25.2] + record = PostgresqlPoint.first + assert_equal [10, 25.2], record.legacy_x + + record.legacy_x = [1.1, 2.2] + record.save! + assert record.reload + assert_equal [1.1, 2.2], record.legacy_x + end + + def test_legacy_mutation + p = PostgresqlPoint.create! legacy_x: [10, 20] + + p.legacy_x[1] = 25 + p.save! + p.reload + + assert_equal [10.0, 25.0], p.legacy_x + assert_not_predicate p, :changed? + end +end + +class PostgresqlGeometricTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + class PostgresqlGeometric < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table("postgresql_geometrics") do |t| + t.lseg :a_line_segment + t.box :a_box + t.path :a_path + t.polygon :a_polygon + t.circle :a_circle + end + end + + teardown do + @connection.drop_table "postgresql_geometrics", if_exists: true + end + + def test_geometric_types + g = PostgresqlGeometric.new( + a_line_segment: "(2.0, 3), (5.5, 7.0)", + a_box: "2.0, 3, 5.5, 7.0", + a_path: "[(2.0, 3), (5.5, 7.0), (8.5, 11.0)]", + a_polygon: "((2.0, 3), (5.5, 7.0), (8.5, 11.0))", + a_circle: "<(5.3, 10.4), 2>" + ) + + g.save! + + h = PostgresqlGeometric.find(g.id) + + assert_equal "[(2,3),(5.5,7)]", h.a_line_segment + assert_equal "(5.5,7),(2,3)", h.a_box # reordered to store upper right corner then bottom left corner + assert_equal "[(2,3),(5.5,7),(8.5,11)]", h.a_path + assert_equal "((2,3),(5.5,7),(8.5,11))", h.a_polygon + assert_equal "<(5.3,10.4),2>", h.a_circle + end + + def test_alternative_format + g = PostgresqlGeometric.new( + a_line_segment: "((2.0, 3), (5.5, 7.0))", + a_box: "(2.0, 3), (5.5, 7.0)", + a_path: "((2.0, 3), (5.5, 7.0), (8.5, 11.0))", + a_polygon: "2.0, 3, 5.5, 7.0, 8.5, 11.0", + a_circle: "((5.3, 10.4), 2)" + ) + + g.save! + + h = PostgresqlGeometric.find(g.id) + assert_equal "[(2,3),(5.5,7)]", h.a_line_segment + assert_equal "(5.5,7),(2,3)", h.a_box # reordered to store upper right corner then bottom left corner + assert_equal "((2,3),(5.5,7),(8.5,11))", h.a_path + assert_equal "((2,3),(5.5,7),(8.5,11))", h.a_polygon + assert_equal "<(5.3,10.4),2>", h.a_circle + end + + def test_geometric_function + PostgresqlGeometric.create! a_path: "[(2.0, 3), (5.5, 7.0), (8.5, 11.0)]" # [ ] is an open path + PostgresqlGeometric.create! a_path: "((2.0, 3), (5.5, 7.0), (8.5, 11.0))" # ( ) is a closed path + + objs = PostgresqlGeometric.find_by_sql "SELECT isopen(a_path) FROM postgresql_geometrics ORDER BY id ASC" + assert_equal [true, false], objs.map(&:isopen) + + objs = PostgresqlGeometric.find_by_sql "SELECT isclosed(a_path) FROM postgresql_geometrics ORDER BY id ASC" + assert_equal [false, true], objs.map(&:isclosed) + end + + def test_schema_dumping + output = dump_table_schema("postgresql_geometrics") + assert_match %r{t\.lseg\s+"a_line_segment"$}, output + assert_match %r{t\.box\s+"a_box"$}, output + assert_match %r{t\.path\s+"a_path"$}, output + assert_match %r{t\.polygon\s+"a_polygon"$}, output + assert_match %r{t\.circle\s+"a_circle"$}, output + end +end + +class PostgreSQLGeometricLineTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + class PostgresqlLine < ActiveRecord::Base; end + + setup do + unless ActiveRecord::Base.connection.send(:postgresql_version) >= 90400 + skip("line type is not fully implemented") + end + @connection = ActiveRecord::Base.connection + @connection.create_table("postgresql_lines") do |t| + t.line :a_line + end + end + + teardown do + if defined?(@connection) + @connection.drop_table "postgresql_lines", if_exists: true + end + end + + def test_geometric_line_type + g = PostgresqlLine.new( + a_line: "{2.0, 3, 5.5}" + ) + g.save! + + h = PostgresqlLine.find(g.id) + assert_equal "{2,3,5.5}", h.a_line + end + + def test_alternative_format_line_type + g = PostgresqlLine.new( + a_line: "(2.0, 3), (4.0, 6.0)" + ) + g.save! + + h = PostgresqlLine.find(g.id) + assert_equal "{1.5,-1,0}", h.a_line + end + + def test_schema_dumping_for_line_type + output = dump_table_schema("postgresql_lines") + assert_match %r{t\.line\s+"a_line"$}, output + end +end + +class PostgreSQLGeometricTypesTest < ActiveRecord::PostgreSQLTestCase + attr_reader :connection, :table_name + + def setup + super + @connection = ActiveRecord::Base.connection + @table_name = :testings + end + + def test_creating_column_with_point_type + connection.create_table(table_name) do |t| + t.point :foo_point + end + + assert_column_exists(:foo_point) + assert_type_correct(:foo_point, :point) + end + + def test_creating_column_with_line_type + connection.create_table(table_name) do |t| + t.line :foo_line + end + + assert_column_exists(:foo_line) + assert_type_correct(:foo_line, :line) + end + + def test_creating_column_with_lseg_type + connection.create_table(table_name) do |t| + t.lseg :foo_lseg + end + + assert_column_exists(:foo_lseg) + assert_type_correct(:foo_lseg, :lseg) + end + + def test_creating_column_with_box_type + connection.create_table(table_name) do |t| + t.box :foo_box + end + + assert_column_exists(:foo_box) + assert_type_correct(:foo_box, :box) + end + + def test_creating_column_with_path_type + connection.create_table(table_name) do |t| + t.path :foo_path + end + + assert_column_exists(:foo_path) + assert_type_correct(:foo_path, :path) + end + + def test_creating_column_with_polygon_type + connection.create_table(table_name) do |t| + t.polygon :foo_polygon + end + + assert_column_exists(:foo_polygon) + assert_type_correct(:foo_polygon, :polygon) + end + + def test_creating_column_with_circle_type + connection.create_table(table_name) do |t| + t.circle :foo_circle + end + + assert_column_exists(:foo_circle) + assert_type_correct(:foo_circle, :circle) + end + + private + + def assert_column_exists(column_name) + assert connection.column_exists?(table_name, column_name) + end + + def assert_type_correct(column_name, type) + column = connection.columns(table_name).find { |c| c.name == column_name.to_s } + assert_equal type, column.type + end +end diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb new file mode 100644 index 0000000000..4b061a9375 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -0,0 +1,378 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/schema_dumping_helper" + +class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + class Hstore < ActiveRecord::Base + self.table_name = "hstores" + + store_accessor :settings, :language, :timezone + end + + class FakeParameters + def to_unsafe_h + { "hi" => "hi" } + end + end + + def setup + @connection = ActiveRecord::Base.connection + + enable_extension!("hstore", @connection) + + @connection.transaction do + @connection.create_table("hstores") do |t| + t.hstore "tags", default: "" + t.hstore "payload", array: true + t.hstore "settings" + end + end + Hstore.reset_column_information + @column = Hstore.columns_hash["tags"] + @type = Hstore.type_for_attribute("tags") + end + + teardown do + @connection.drop_table "hstores", if_exists: true + disable_extension!("hstore", @connection) + end + + def test_hstore_included_in_extensions + assert_respond_to @connection, :extensions + assert_includes @connection.extensions, "hstore", "extension list should include hstore" + end + + def test_disable_enable_hstore + assert @connection.extension_enabled?("hstore") + @connection.disable_extension "hstore" + assert_not @connection.extension_enabled?("hstore") + @connection.enable_extension "hstore" + assert @connection.extension_enabled?("hstore") + ensure + # Restore column(s) dropped by `drop extension hstore cascade;` + load_schema + end + + def test_column + assert_equal :hstore, @column.type + assert_equal "hstore", @column.sql_type + assert_not_predicate @column, :array? + + assert_not_predicate @type, :binary? + end + + def test_default + @connection.add_column "hstores", "permissions", :hstore, default: '"users"=>"read", "articles"=>"write"' + Hstore.reset_column_information + + assert_equal({ "users" => "read", "articles" => "write" }, Hstore.column_defaults["permissions"]) + assert_equal({ "users" => "read", "articles" => "write" }, Hstore.new.permissions) + ensure + Hstore.reset_column_information + end + + def test_change_table_supports_hstore + @connection.transaction do + @connection.change_table("hstores") do |t| + t.hstore "users", default: "" + end + Hstore.reset_column_information + column = Hstore.columns_hash["users"] + assert_equal :hstore, column.type + + raise ActiveRecord::Rollback # reset the schema change + end + ensure + Hstore.reset_column_information + end + + def test_hstore_migration + hstore_migration = Class.new(ActiveRecord::Migration::Current) do + def change + change_table("hstores") do |t| + t.hstore :keys + end + end + end + + hstore_migration.new.suppress_messages do + hstore_migration.migrate(:up) + assert_includes @connection.columns(:hstores).map(&:name), "keys" + hstore_migration.migrate(:down) + assert_not_includes @connection.columns(:hstores).map(&:name), "keys" + end + end + + def test_cast_value_on_write + x = Hstore.new tags: { "bool" => true, "number" => 5 } + assert_equal({ "bool" => true, "number" => 5 }, x.tags_before_type_cast) + assert_equal({ "bool" => "true", "number" => "5" }, x.tags) + x.save + assert_equal({ "bool" => "true", "number" => "5" }, x.reload.tags) + end + + def test_type_cast_hstore + assert_equal({ "1" => "2" }, @type.deserialize("\"1\"=>\"2\"")) + assert_equal({}, @type.deserialize("")) + assert_equal({ "key" => nil }, @type.deserialize("key => NULL")) + assert_equal({ "c" => "}", '"a"' => 'b "a b' }, @type.deserialize(%q(c=>"}", "\"a\""=>"b \"a b"))) + end + + def test_with_store_accessors + x = Hstore.new(language: "fr", timezone: "GMT") + assert_equal "fr", x.language + assert_equal "GMT", x.timezone + + x.save! + x = Hstore.first + assert_equal "fr", x.language + assert_equal "GMT", x.timezone + + x.language = "de" + x.save! + + x = Hstore.first + assert_equal "de", x.language + assert_equal "GMT", x.timezone + end + + def test_duplication_with_store_accessors + x = Hstore.new(language: "fr", timezone: "GMT") + assert_equal "fr", x.language + assert_equal "GMT", x.timezone + + y = x.dup + assert_equal "fr", y.language + assert_equal "GMT", y.timezone + end + + def test_yaml_round_trip_with_store_accessors + x = Hstore.new(language: "fr", timezone: "GMT") + assert_equal "fr", x.language + assert_equal "GMT", x.timezone + + y = YAML.load(YAML.dump(x)) + assert_equal "fr", y.language + assert_equal "GMT", y.timezone + end + + def test_changes_in_place + hstore = Hstore.create!(settings: { "one" => "two" }) + hstore.settings["three"] = "four" + hstore.save! + hstore.reload + + assert_equal "four", hstore.settings["three"] + assert_not_predicate hstore, :changed? + end + + def test_dirty_from_user_equal + settings = { "alongkey" => "anything", "key" => "value" } + hstore = Hstore.create!(settings: settings) + + hstore.settings = { "key" => "value", "alongkey" => "anything" } + assert_equal settings, hstore.settings + assert_not_predicate hstore, :changed? + end + + def test_hstore_dirty_from_database_equal + settings = { "alongkey" => "anything", "key" => "value" } + hstore = Hstore.create!(settings: settings) + hstore.reload + + assert_equal settings, hstore.settings + hstore.settings = settings + assert_not_predicate hstore, :changed? + end + + def test_gen1 + assert_equal('" "=>""', @type.serialize(" " => "")) + end + + def test_gen2 + assert_equal('","=>""', @type.serialize("," => "")) + end + + def test_gen3 + assert_equal('"="=>""', @type.serialize("=" => "")) + end + + def test_gen4 + assert_equal('">"=>""', @type.serialize(">" => "")) + end + + def test_parse1 + assert_equal({ "a" => nil, "b" => nil, "c" => "NuLl", "null" => "c" }, @type.deserialize('a=>null,b=>NuLl,c=>"NuLl",null=>c')) + end + + def test_parse2 + assert_equal({ " " => " " }, @type.deserialize("\\ =>\\ ")) + end + + def test_parse3 + assert_equal({ "=" => ">" }, @type.deserialize("==>>")) + end + + def test_parse4 + assert_equal({ "=a" => "q=w" }, @type.deserialize('\=a=>q=w')) + end + + def test_parse5 + assert_equal({ "=a" => "q=w" }, @type.deserialize('"=a"=>q\=w')) + end + + def test_parse6 + assert_equal({ "\"a" => "q>w" }, @type.deserialize('"\"a"=>q>w')) + end + + def test_parse7 + assert_equal({ "\"a" => "q\"w" }, @type.deserialize('\"a=>q"w')) + end + + def test_rewrite + @connection.execute "insert into hstores (tags) VALUES ('1=>2')" + x = Hstore.first + x.tags = { '"a\'' => "b" } + assert x.save! + end + + def test_select + @connection.execute "insert into hstores (tags) VALUES ('1=>2')" + x = Hstore.first + assert_equal({ "1" => "2" }, x.tags) + end + + def test_array_cycle + assert_array_cycle([{ "AA" => "BB", "CC" => "DD" }, { "AA" => nil }]) + end + + def test_array_strings_with_quotes + assert_array_cycle([{ "this has" => 'some "s that need to be escaped"' }]) + end + + def test_array_strings_with_commas + assert_array_cycle([{ "this,has" => "many,values" }]) + end + + def test_array_strings_with_array_delimiters + assert_array_cycle(["{" => "}"]) + end + + def test_array_strings_with_null_strings + assert_array_cycle([{ "NULL" => "NULL" }]) + end + + def test_contains_nils + assert_array_cycle([{ "NULL" => nil }]) + end + + def test_select_multikey + @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')" + x = Hstore.first + assert_equal({ "1" => "2", "2" => "3" }, x.tags) + end + + def test_create + assert_cycle("a" => "b", "1" => "2") + end + + def test_nil + assert_cycle("a" => nil) + end + + def test_quotes + assert_cycle("a" => 'b"ar', '1"foo' => "2") + end + + def test_whitespace + assert_cycle("a b" => "b ar", '1"foo' => "2") + end + + def test_backslash + assert_cycle('a\\b' => 'b\\ar', '1"foo' => "2") + end + + def test_comma + assert_cycle("a, b" => "bar", '1"foo' => "2") + end + + def test_arrow + assert_cycle("a=>b" => "bar", '1"foo' => "2") + end + + def test_quoting_special_characters + assert_cycle("ca" => "cà", "ac" => "àc") + end + + def test_multiline + assert_cycle("a\nb" => "c\nd") + end + + class TagCollection + def initialize(hash); @hash = hash end + def to_hash; @hash end + def self.load(hash); new(hash) end + def self.dump(object); object.to_hash end + end + + class HstoreWithSerialize < Hstore + serialize :tags, TagCollection + end + + def test_hstore_with_serialized_attributes + HstoreWithSerialize.create! tags: TagCollection.new("one" => "two") + record = HstoreWithSerialize.first + assert_instance_of TagCollection, record.tags + assert_equal({ "one" => "two" }, record.tags.to_hash) + record.tags = TagCollection.new("three" => "four") + record.save! + assert_equal({ "three" => "four" }, HstoreWithSerialize.first.tags.to_hash) + end + + def test_clone_hstore_with_serialized_attributes + HstoreWithSerialize.create! tags: TagCollection.new("one" => "two") + record = HstoreWithSerialize.first + dupe = record.dup + assert_equal({ "one" => "two" }, dupe.tags.to_hash) + end + + def test_schema_dump_with_shorthand + output = dump_table_schema("hstores") + assert_match %r[t\.hstore "tags",\s+default: {}], output + end + + def test_supports_to_unsafe_h_values + assert_equal("\"hi\"=>\"hi\"", @type.serialize(FakeParameters.new)) + end + + private + def assert_array_cycle(array) + # test creation + x = Hstore.create!(payload: array) + x.reload + assert_equal(array, x.payload) + + # test updating + x = Hstore.create!(payload: []) + x.payload = array + x.save! + x.reload + assert_equal(array, x.payload) + end + + def assert_cycle(hash) + # test creation + x = Hstore.create!(tags: hash) + x.reload + assert_equal(hash, x.tags) + + # test updating + x = Hstore.create!(tags: {}) + x.tags = hash + x.save! + x.reload + assert_equal(hash, x.tags) + end +end diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb new file mode 100644 index 0000000000..5e56ce8427 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +require "cases/helper" + +class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase + include InTimeZone + + class PostgresqlInfinity < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:postgresql_infinities) do |t| + t.float :float + t.datetime :datetime + t.date :date + end + end + + teardown do + @connection.drop_table "postgresql_infinities", if_exists: true + end + + test "type casting infinity on a float column" do + record = PostgresqlInfinity.create!(float: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.float + end + + test "type casting string on a float column" do + record = PostgresqlInfinity.new(float: "Infinity") + assert_equal Float::INFINITY, record.float + record = PostgresqlInfinity.new(float: "-Infinity") + assert_equal(-Float::INFINITY, record.float) + record = PostgresqlInfinity.new(float: "NaN") + assert record.float.nan?, "Expected #{record.float} to be NaN" + end + + test "update_all with infinity on a float column" do + record = PostgresqlInfinity.create! + PostgresqlInfinity.update_all(float: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.float + end + + test "type casting infinity on a datetime column" do + record = PostgresqlInfinity.create!(datetime: "infinity") + record.reload + assert_equal Float::INFINITY, record.datetime + + record = PostgresqlInfinity.create!(datetime: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.datetime + end + + test "type casting infinity on a date column" do + record = PostgresqlInfinity.create!(date: "infinity") + record.reload + assert_equal Float::INFINITY, record.date + + record = PostgresqlInfinity.create!(date: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.date + end + + test "update_all with infinity on a datetime column" do + record = PostgresqlInfinity.create! + PostgresqlInfinity.update_all(datetime: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.datetime + end + + test "assigning 'infinity' on a datetime column with TZ aware attributes" do + begin + in_time_zone "Pacific Time (US & Canada)" do + record = PostgresqlInfinity.create!(datetime: "infinity") + assert_equal Float::INFINITY, record.datetime + assert_equal record.datetime, record.reload.datetime + end + ensure + # setting time_zone_aware_attributes causes the types to change. + # There is no way to do this automatically since it can be set on a superclass + PostgresqlInfinity.reset_column_information + end + end + + test "where clause with infinite range on a datetime column" do + record = PostgresqlInfinity.create!(datetime: Time.current) + + string = PostgresqlInfinity.where(datetime: "-infinity".."infinity") + assert_equal record, string.take + + infinity = PostgresqlInfinity.where(datetime: -::Float::INFINITY..::Float::INFINITY) + assert_equal record, infinity.take + + assert_equal infinity.to_sql, string.to_sql + end + + test "where clause with infinite range on a date column" do + record = PostgresqlInfinity.create!(date: Date.current) + + string = PostgresqlInfinity.where(date: "-infinity".."infinity") + assert_equal record, string.take + + infinity = PostgresqlInfinity.where(date: -::Float::INFINITY..::Float::INFINITY) + assert_equal record, infinity.take + + assert_equal infinity.to_sql, string.to_sql + end +end diff --git a/activerecord/test/cases/adapters/postgresql/integer_test.rb b/activerecord/test/cases/adapters/postgresql/integer_test.rb new file mode 100644 index 0000000000..3e45b057ff --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/integer_test.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "cases/helper" +require "active_support/core_ext/numeric/bytes" + +class PostgresqlIntegerTest < ActiveRecord::PostgreSQLTestCase + class PgInteger < ActiveRecord::Base + end + + def setup + @connection = ActiveRecord::Base.connection + + @connection.transaction do + @connection.create_table "pg_integers", force: true do |t| + t.integer :quota, limit: 8, default: 2.gigabytes + end + end + end + + teardown do + @connection.drop_table "pg_integers", if_exists: true + end + + test "schema properly respects bigint ranges" do + assert_equal 2.gigabytes, PgInteger.new.quota + end +end diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb new file mode 100644 index 0000000000..ee08841eb3 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require "cases/helper" +require "cases/json_shared_test_cases" + +module PostgresqlJSONSharedTestCases + include JSONSharedTestCases + + def setup + super + @connection.create_table("json_data_type") do |t| + t.public_send column_type, "payload", default: {} # t.json 'payload', default: {} + t.public_send column_type, "settings" # t.json 'settings' + t.public_send column_type, "objects", array: true # t.json 'objects', array: true + end + rescue ActiveRecord::StatementInvalid + skip "do not test on PostgreSQL without #{column_type} type." + end + + def test_default + @connection.add_column "json_data_type", "permissions", column_type, default: { "users": "read", "posts": ["read", "write"] } + klass.reset_column_information + + assert_equal({ "users" => "read", "posts" => ["read", "write"] }, klass.column_defaults["permissions"]) + assert_equal({ "users" => "read", "posts" => ["read", "write"] }, klass.new.permissions) + end + + def test_deserialize_with_array + x = klass.new(objects: ["foo" => "bar"]) + assert_equal ["foo" => "bar"], x.objects + x.save! + assert_equal ["foo" => "bar"], x.objects + x.reload + assert_equal ["foo" => "bar"], x.objects + end +end + +class PostgresqlJSONTest < ActiveRecord::PostgreSQLTestCase + include PostgresqlJSONSharedTestCases + + def column_type + :json + end +end + +class PostgresqlJSONBTest < ActiveRecord::PostgreSQLTestCase + include PostgresqlJSONSharedTestCases + + def column_type + :jsonb + end +end diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb new file mode 100644 index 0000000000..8349ee6ee2 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/schema_dumping_helper" + +class PostgresqlLtreeTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + class Ltree < ActiveRecord::Base + self.table_name = "ltrees" + end + + def setup + @connection = ActiveRecord::Base.connection + + enable_extension!("ltree", @connection) + + @connection.transaction do + @connection.create_table("ltrees") do |t| + t.ltree "path" + end + end + rescue ActiveRecord::StatementInvalid + skip "do not test on PG without ltree" + end + + teardown do + @connection.drop_table "ltrees", if_exists: true + end + + def test_column + column = Ltree.columns_hash["path"] + assert_equal :ltree, column.type + assert_equal "ltree", column.sql_type + assert_not_predicate column, :array? + + type = Ltree.type_for_attribute("path") + assert_not_predicate type, :binary? + end + + def test_write + ltree = Ltree.new(path: "1.2.3.4") + assert ltree.save! + end + + def test_select + @connection.execute "insert into ltrees (path) VALUES ('1.2.3')" + ltree = Ltree.first + assert_equal "1.2.3", ltree.path + end + + def test_schema_dump_with_shorthand + output = dump_table_schema("ltrees") + assert_match %r[t\.ltree "path"], output + end +end diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb new file mode 100644 index 0000000000..be3590e8dd --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/schema_dumping_helper" + +class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + class PostgresqlMoney < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.execute("set lc_monetary = 'C'") + @connection.create_table("postgresql_moneys", force: true) do |t| + t.money "wealth" + t.money "depth", default: "150.55" + end + end + + teardown do + @connection.drop_table "postgresql_moneys", if_exists: true + end + + def test_column + column = PostgresqlMoney.columns_hash["wealth"] + assert_equal :money, column.type + assert_equal "money", column.sql_type + assert_equal 2, column.scale + assert_not_predicate column, :array? + + type = PostgresqlMoney.type_for_attribute("wealth") + assert_not_predicate type, :binary? + end + + def test_default + assert_equal BigDecimal("150.55"), PostgresqlMoney.column_defaults["depth"] + assert_equal BigDecimal("150.55"), PostgresqlMoney.new.depth + end + + def test_money_values + @connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (1, '567.89'::money)") + @connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (2, '-567.89'::money)") + + first_money = PostgresqlMoney.find(1) + second_money = PostgresqlMoney.find(2) + assert_equal 567.89, first_money.wealth + assert_equal(-567.89, second_money.wealth) + end + + def test_money_type_cast + type = PostgresqlMoney.type_for_attribute("wealth") + assert_equal(12345678.12, type.cast("$12,345,678.12".dup)) + assert_equal(12345678.12, type.cast("$12.345.678,12".dup)) + assert_equal(-1.15, type.cast("-$1.15".dup)) + assert_equal(-2.25, type.cast("($2.25)".dup)) + end + + def test_schema_dumping + output = dump_table_schema("postgresql_moneys") + assert_match %r{t\.money\s+"wealth",\s+scale: 2$}, output + assert_match %r{t\.money\s+"depth",\s+scale: 2,\s+default: "150\.55"$}, output + end + + def test_create_and_update_money + money = PostgresqlMoney.create(wealth: "987.65".dup) + assert_equal 987.65, money.wealth + + new_value = BigDecimal("123.45") + money.wealth = new_value + money.save! + money.reload + assert_equal new_value, money.wealth + end + + def test_update_all_with_money_string + money = PostgresqlMoney.create! + PostgresqlMoney.update_all(wealth: "987.65") + money.reload + + assert_equal 987.65, money.wealth + end + + def test_update_all_with_money_big_decimal + money = PostgresqlMoney.create! + PostgresqlMoney.update_all(wealth: "123.45".to_d) + money.reload + + assert_equal 123.45, money.wealth + end + + def test_update_all_with_money_numeric + money = PostgresqlMoney.create! + PostgresqlMoney.update_all(wealth: 123.45) + money.reload + + assert_equal 123.45, money.wealth + end +end diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb new file mode 100644 index 0000000000..736570451b --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/network_test.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/schema_dumping_helper" + +class PostgresqlNetworkTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + class PostgresqlNetworkAddress < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table("postgresql_network_addresses", force: true) do |t| + t.inet "inet_address", default: "192.168.1.1" + t.cidr "cidr_address", default: "192.168.1.0/24" + t.macaddr "mac_address", default: "ff:ff:ff:ff:ff:ff" + end + end + + teardown do + @connection.drop_table "postgresql_network_addresses", if_exists: true + end + + def test_cidr_column + column = PostgresqlNetworkAddress.columns_hash["cidr_address"] + assert_equal :cidr, column.type + assert_equal "cidr", column.sql_type + assert_not_predicate column, :array? + + type = PostgresqlNetworkAddress.type_for_attribute("cidr_address") + assert_not_predicate type, :binary? + end + + def test_inet_column + column = PostgresqlNetworkAddress.columns_hash["inet_address"] + assert_equal :inet, column.type + assert_equal "inet", column.sql_type + assert_not_predicate column, :array? + + type = PostgresqlNetworkAddress.type_for_attribute("inet_address") + assert_not_predicate type, :binary? + end + + def test_macaddr_column + column = PostgresqlNetworkAddress.columns_hash["mac_address"] + assert_equal :macaddr, column.type + assert_equal "macaddr", column.sql_type + assert_not_predicate column, :array? + + type = PostgresqlNetworkAddress.type_for_attribute("mac_address") + assert_not_predicate type, :binary? + end + + def test_network_types + PostgresqlNetworkAddress.create(cidr_address: "192.168.0.0/24", + inet_address: "172.16.1.254/32", + mac_address: "01:23:45:67:89:0a") + + address = PostgresqlNetworkAddress.first + assert_equal IPAddr.new("192.168.0.0/24"), address.cidr_address + assert_equal IPAddr.new("172.16.1.254"), address.inet_address + assert_equal "01:23:45:67:89:0a", address.mac_address + + address.cidr_address = "10.1.2.3/32" + address.inet_address = "10.0.0.0/8" + address.mac_address = "bc:de:f0:12:34:56" + + address.save! + assert address.reload + assert_equal IPAddr.new("10.1.2.3/32"), address.cidr_address + assert_equal IPAddr.new("10.0.0.0/8"), address.inet_address + assert_equal "bc:de:f0:12:34:56", address.mac_address + end + + def test_invalid_network_address + invalid_address = PostgresqlNetworkAddress.new(cidr_address: "invalid addr", + inet_address: "invalid addr") + assert_nil invalid_address.cidr_address + assert_nil invalid_address.inet_address + assert_equal "invalid addr", invalid_address.cidr_address_before_type_cast + assert_equal "invalid addr", invalid_address.inet_address_before_type_cast + assert invalid_address.save + + invalid_address.reload + assert_nil invalid_address.cidr_address + assert_nil invalid_address.inet_address + assert_nil invalid_address.cidr_address_before_type_cast + assert_nil invalid_address.inet_address_before_type_cast + end + + def test_schema_dump_with_shorthand + output = dump_table_schema("postgresql_network_addresses") + assert_match %r{t\.inet\s+"inet_address",\s+default: "192\.168\.1\.1"}, output + assert_match %r{t\.cidr\s+"cidr_address",\s+default: "192\.168\.1\.0/24"}, output + assert_match %r{t\.macaddr\s+"mac_address",\s+default: "ff:ff:ff:ff:ff:ff"}, output + end +end diff --git a/activerecord/test/cases/adapters/postgresql/numbers_test.rb b/activerecord/test/cases/adapters/postgresql/numbers_test.rb new file mode 100644 index 0000000000..b53a12254d --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/numbers_test.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require "cases/helper" + +class PostgresqlNumberTest < ActiveRecord::PostgreSQLTestCase + class PostgresqlNumber < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table("postgresql_numbers", force: true) do |t| + t.column "single", "REAL" + t.column "double", "DOUBLE PRECISION" + end + end + + teardown do + @connection.drop_table "postgresql_numbers", if_exists: true + end + + def test_data_type + assert_equal :float, PostgresqlNumber.columns_hash["single"].type + assert_equal :float, PostgresqlNumber.columns_hash["double"].type + end + + def test_values + @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (1, 123.456, 123456.789)") + @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (2, '-Infinity', 'Infinity')") + @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (3, 123.456, 'NaN')") + + first, second, third = PostgresqlNumber.find(1, 2, 3) + + assert_equal 123.456, first.single + assert_equal 123456.789, first.double + assert_equal(-::Float::INFINITY, second.single) + assert_equal ::Float::INFINITY, second.double + assert third.double.nan?, "Expected #{third.double} to be NaN" + end + + def test_update + record = PostgresqlNumber.create! single: "123.456", double: "123456.789" + new_single = 789.012 + new_double = 789012.345 + record.single = new_single + record.double = new_double + record.save! + + record.reload + assert_equal new_single, record.single + assert_equal new_double, record.double + end +end diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb new file mode 100644 index 0000000000..1951230c8a --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -0,0 +1,390 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/ddl_helper" +require "support/connection_helper" + +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapterTest < ActiveRecord::PostgreSQLTestCase + self.use_transactional_tests = false + include DdlHelper + include ConnectionHelper + + def setup + @connection = ActiveRecord::Base.connection + end + + def test_bad_connection + assert_raise ActiveRecord::NoDatabaseError do + configuration = ActiveRecord::Base.configurations["arunit"].merge(database: "should_not_exist-cinco-dog-db") + connection = ActiveRecord::Base.postgresql_connection(configuration) + connection.exec_query("SELECT 1") + end + end + + def test_primary_key + with_example_table do + assert_equal "id", @connection.primary_key("ex") + end + end + + def test_primary_key_works_tables_containing_capital_letters + assert_equal "id", @connection.primary_key("CamelCase") + end + + def test_non_standard_primary_key + with_example_table "data character varying(255) primary key" do + assert_equal "data", @connection.primary_key("ex") + end + end + + def test_primary_key_returns_nil_for_no_pk + with_example_table "id integer" do + assert_nil @connection.primary_key("ex") + end + end + + def test_exec_insert_with_returning_disabled + connection = connection_without_insert_returning + result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], "id", "postgresql_partitioned_table_parent_id_seq") + expect = connection.query("select max(id) from postgresql_partitioned_table_parent").first.first + assert_equal expect.to_i, result.rows.first.first + end + + def test_exec_insert_with_returning_disabled_and_no_sequence_name_given + connection = connection_without_insert_returning + result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], "id") + expect = connection.query("select max(id) from postgresql_partitioned_table_parent").first.first + assert_equal expect.to_i, result.rows.first.first + end + + def test_exec_insert_default_values_with_returning_disabled_and_no_sequence_name_given + connection = connection_without_insert_returning + result = connection.exec_insert("insert into postgresql_partitioned_table_parent DEFAULT VALUES", nil, [], "id") + expect = connection.query("select max(id) from postgresql_partitioned_table_parent").first.first + assert_equal expect.to_i, result.rows.first.first + end + + def test_exec_insert_default_values_quoted_schema_with_returning_disabled_and_no_sequence_name_given + connection = connection_without_insert_returning + result = connection.exec_insert('insert into "public"."postgresql_partitioned_table_parent" DEFAULT VALUES', nil, [], "id") + expect = connection.query("select max(id) from postgresql_partitioned_table_parent").first.first + assert_equal expect.to_i, result.rows.first.first + end + + def test_serial_sequence + assert_equal "public.accounts_id_seq", + @connection.serial_sequence("accounts", "id") + + assert_raises(ActiveRecord::StatementInvalid) do + @connection.serial_sequence("zomg", "id") + end + end + + def test_default_sequence_name + assert_equal "public.accounts_id_seq", + @connection.default_sequence_name("accounts", "id") + + assert_equal "public.accounts_id_seq", + @connection.default_sequence_name("accounts") + end + + def test_default_sequence_name_bad_table + assert_equal "zomg_id_seq", + @connection.default_sequence_name("zomg", "id") + + assert_equal "zomg_id_seq", + @connection.default_sequence_name("zomg") + end + + def test_pk_and_sequence_for + with_example_table do + pk, seq = @connection.pk_and_sequence_for("ex") + assert_equal "id", pk + assert_equal @connection.default_sequence_name("ex", "id"), seq.to_s + end + end + + def test_pk_and_sequence_for_with_non_standard_primary_key + with_example_table "code serial primary key" do + pk, seq = @connection.pk_and_sequence_for("ex") + assert_equal "code", pk + assert_equal @connection.default_sequence_name("ex", "code"), seq.to_s + end + end + + def test_pk_and_sequence_for_returns_nil_if_no_seq + with_example_table "id integer primary key" do + assert_nil @connection.pk_and_sequence_for("ex") + end + end + + def test_pk_and_sequence_for_returns_nil_if_no_pk + with_example_table "id integer" do + assert_nil @connection.pk_and_sequence_for("ex") + end + end + + def test_pk_and_sequence_for_returns_nil_if_table_not_found + assert_nil @connection.pk_and_sequence_for("unobtainium") + end + + def test_pk_and_sequence_for_with_collision_pg_class_oid + @connection.exec_query("create table ex(id serial primary key)") + @connection.exec_query("create table ex2(id serial primary key)") + + correct_depend_record = [ + "'pg_class'::regclass", + "'ex_id_seq'::regclass", + "0", + "'pg_class'::regclass", + "'ex'::regclass", + "1", + "'a'" + ] + + collision_depend_record = [ + "'pg_attrdef'::regclass", + "'ex2_id_seq'::regclass", + "0", + "'pg_class'::regclass", + "'ex'::regclass", + "1", + "'a'" + ] + + @connection.exec_query( + "DELETE FROM pg_depend WHERE objid = 'ex_id_seq'::regclass AND refobjid = 'ex'::regclass AND deptype = 'a'" + ) + @connection.exec_query( + "INSERT INTO pg_depend VALUES(#{collision_depend_record.join(',')})" + ) + @connection.exec_query( + "INSERT INTO pg_depend VALUES(#{correct_depend_record.join(',')})" + ) + + seq = @connection.pk_and_sequence_for("ex").last + assert_equal PostgreSQL::Name.new("public", "ex_id_seq"), seq + + @connection.exec_query( + "DELETE FROM pg_depend WHERE objid = 'ex2_id_seq'::regclass AND refobjid = 'ex'::regclass AND deptype = 'a'" + ) + ensure + @connection.drop_table "ex", if_exists: true + @connection.drop_table "ex2", if_exists: true + end + + def test_table_alias_length + assert_nothing_raised do + @connection.table_alias_length + end + end + + def test_exec_no_binds + with_example_table do + result = @connection.exec_query("SELECT id, data FROM ex") + assert_equal 0, result.rows.length + assert_equal 2, result.columns.length + assert_equal %w{ id data }, result.columns + + string = @connection.quote("foo") + @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") + result = @connection.exec_query("SELECT id, data FROM ex") + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length + + assert_equal [[1, "foo"]], result.rows + end + end + + if ActiveRecord::Base.connection.prepared_statements + def test_exec_with_binds + with_example_table do + string = @connection.quote("foo") + @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") + + bind = Relation::QueryAttribute.new("id", 1, Type::Value.new) + result = @connection.exec_query("SELECT id, data FROM ex WHERE id = $1", nil, [bind]) + + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length + + assert_equal [[1, "foo"]], result.rows + end + end + + def test_exec_typecasts_bind_vals + with_example_table do + string = @connection.quote("foo") + @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") + + bind = Relation::QueryAttribute.new("id", "1-fuu", Type::Integer.new) + result = @connection.exec_query("SELECT id, data FROM ex WHERE id = $1", nil, [bind]) + + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length + + assert_equal [[1, "foo"]], result.rows + end + end + end + + def test_partial_index + with_example_table do + @connection.add_index "ex", %w{ id number }, name: "partial", where: "number > 100" + index = @connection.indexes("ex").find { |idx| idx.name == "partial" } + assert_equal "(number > 100)", index.where + end + end + + def test_expression_index + with_example_table do + @connection.add_index "ex", "mod(id, 10), abs(number)", name: "expression" + index = @connection.indexes("ex").find { |idx| idx.name == "expression" } + assert_equal "mod(id, 10), abs(number)", index.columns + end + end + + def test_index_with_opclass + with_example_table do + @connection.add_index "ex", "data", opclass: "varchar_pattern_ops" + index = @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data" } + assert_equal ["data"], index.columns + + @connection.remove_index "ex", "data" + assert_not @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data" } + end + end + + def test_columns_for_distinct_zero_orders + assert_equal "posts.id", + @connection.columns_for_distinct("posts.id", []) + end + + def test_columns_for_distinct_one_order + assert_equal "posts.id, posts.created_at AS alias_0", + @connection.columns_for_distinct("posts.id", ["posts.created_at desc"]) + end + + def test_columns_for_distinct_few_orders + assert_equal "posts.id, posts.created_at AS alias_0, posts.position AS alias_1", + @connection.columns_for_distinct("posts.id", ["posts.created_at desc", "posts.position asc"]) + end + + def test_columns_for_distinct_with_case + assert_equal( + "posts.id, CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0", + @connection.columns_for_distinct("posts.id", + ["CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END"]) + ) + end + + def test_columns_for_distinct_blank_not_nil_orders + assert_equal "posts.id, posts.created_at AS alias_0", + @connection.columns_for_distinct("posts.id", ["posts.created_at desc", "", " "]) + end + + def test_columns_for_distinct_with_arel_order + order = Object.new + def order.to_sql + "posts.created_at desc" + end + assert_equal "posts.id, posts.created_at AS alias_0", + @connection.columns_for_distinct("posts.id", [order]) + end + + def test_columns_for_distinct_with_nulls + assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls first"]) + assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls last"]) + end + + def test_columns_for_distinct_without_order_specifiers + assert_equal "posts.title, posts.updater_id AS alias_0", + @connection.columns_for_distinct("posts.title", ["posts.updater_id"]) + + assert_equal "posts.title, posts.updater_id AS alias_0", + @connection.columns_for_distinct("posts.title", ["posts.updater_id nulls last"]) + + assert_equal "posts.title, posts.updater_id AS alias_0", + @connection.columns_for_distinct("posts.title", ["posts.updater_id nulls first"]) + end + + def test_raise_error_when_cannot_translate_exception + assert_raise TypeError do + @connection.send(:log, nil) { @connection.execute(nil) } + end + end + + def test_reload_type_map_for_newly_defined_types + @connection.execute "CREATE TYPE feeling AS ENUM ('good', 'bad')" + result = @connection.select_all "SELECT 'good'::feeling" + assert_instance_of(PostgreSQLAdapter::OID::Enum, + result.column_types["feeling"]) + ensure + @connection.execute "DROP TYPE IF EXISTS feeling" + reset_connection + end + + def test_only_reload_type_map_once_for_every_unrecognized_type + reset_connection + connection = ActiveRecord::Base.connection + + silence_warnings do + assert_queries 2, ignore_none: true do + connection.select_all "select 'pg_catalog.pg_class'::regclass" + end + assert_queries 1, ignore_none: true do + connection.select_all "select 'pg_catalog.pg_class'::regclass" + end + assert_queries 2, ignore_none: true do + connection.select_all "SELECT NULL::anyarray" + end + end + ensure + reset_connection + end + + def test_only_warn_on_first_encounter_of_unrecognized_oid + reset_connection + connection = ActiveRecord::Base.connection + + warning = capture(:stderr) { + connection.select_all "select 'pg_catalog.pg_class'::regclass" + connection.select_all "select 'pg_catalog.pg_class'::regclass" + connection.select_all "select 'pg_catalog.pg_class'::regclass" + } + assert_match(/\Aunknown OID \d+: failed to recognize type of 'regclass'\. It will be treated as String\.\n\z/, warning) + ensure + reset_connection + end + + def test_unparsed_defaults_are_at_least_set_when_saving + with_example_table "id SERIAL PRIMARY KEY, number INTEGER NOT NULL DEFAULT (4 + 4) * 2 / 4" do + number_klass = Class.new(ActiveRecord::Base) do + self.table_name = "ex" + end + column = number_klass.columns_hash["number"] + assert_nil column.default + assert_nil column.default_function + + first_number = number_klass.new + assert_nil first_number.number + + first_number.save! + assert_equal 4, first_number.reload.number + end + end + + private + + def with_example_table(definition = "id serial primary key, number integer, data character varying(255)", &block) + super(@connection, "ex", definition, &block) + end + + def connection_without_insert_returning + ActiveRecord::Base.postgresql_connection(ActiveRecord::Base.configurations["arunit"].merge(insert_returning: false)) + end + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/prepared_statements_disabled_test.rb b/activerecord/test/cases/adapters/postgresql/prepared_statements_disabled_test.rb new file mode 100644 index 0000000000..f7478b50c3 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/prepared_statements_disabled_test.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/computer" +require "models/developer" + +class PreparedStatementsDisabledTest < ActiveRecord::PostgreSQLTestCase + fixtures :developers + + def setup + @conn = ActiveRecord::Base.establish_connection :arunit_without_prepared_statements + end + + def teardown + @conn.release_connection + ActiveRecord::Base.establish_connection :arunit + end + + def test_select_query_works_even_when_prepared_statements_are_disabled + assert_not Developer.connection.prepared_statements + + david = developers(:david) + + assert_equal david, Developer.where(name: "David").last # With Binds + assert_operator Developer.count, :>, 0 # Without Binds + end +end diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb new file mode 100644 index 0000000000..d50dc49276 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter + class QuotingTest < ActiveRecord::PostgreSQLTestCase + def setup + @conn = ActiveRecord::Base.connection + end + + def test_type_cast_true + assert_equal true, @conn.type_cast(true) + end + + def test_type_cast_false + assert_equal false, @conn.type_cast(false) + end + + def test_quote_float_nan + nan = 0.0 / 0 + assert_equal "'NaN'", @conn.quote(nan) + end + + def test_quote_float_infinity + infinity = 1.0 / 0 + assert_equal "'Infinity'", @conn.quote(infinity) + end + + def test_quote_range + range = "1,2]'; SELECT * FROM users; --".."a" + type = OID::Range.new(Type::Integer.new, :int8range) + assert_equal "'[1,0]'", @conn.quote(type.serialize(range)) + end + + def test_quote_bit_string + value = "'); SELECT * FROM users; /*\n01\n*/--" + type = OID::Bit.new + assert_nil @conn.quote(type.serialize(value)) + end + end + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb new file mode 100644 index 0000000000..261c24634e --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -0,0 +1,414 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/connection_helper" + +if ActiveRecord::Base.connection.respond_to?(:supports_ranges?) && ActiveRecord::Base.connection.supports_ranges? + class PostgresqlRange < ActiveRecord::Base + self.table_name = "postgresql_ranges" + self.time_zone_aware_types += [:tsrange, :tstzrange] + end + + class PostgresqlRangeTest < ActiveRecord::PostgreSQLTestCase + self.use_transactional_tests = false + include ConnectionHelper + include InTimeZone + + def setup + @connection = PostgresqlRange.connection + begin + @connection.transaction do + @connection.execute <<_SQL + CREATE TYPE floatrange AS RANGE ( + subtype = float8, + subtype_diff = float8mi + ); +_SQL + + @connection.create_table("postgresql_ranges") do |t| + t.daterange :date_range + t.numrange :num_range + t.tsrange :ts_range + t.tstzrange :tstz_range + t.int4range :int4_range + t.int8range :int8_range + end + + @connection.add_column "postgresql_ranges", "float_range", "floatrange" + end + PostgresqlRange.reset_column_information + rescue ActiveRecord::StatementInvalid + skip "do not test on PG without range" + end + + insert_range(id: 101, + date_range: "[''2012-01-02'', ''2012-01-04'']", + num_range: "[0.1, 0.2]", + ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'']", + tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']", + int4_range: "[1, 10]", + int8_range: "[10, 100]", + float_range: "[0.5, 0.7]") + + insert_range(id: 102, + date_range: "[''2012-01-02'', ''2012-01-04'')", + num_range: "[0.1, 0.2)", + ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'')", + tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')", + int4_range: "[1, 10)", + int8_range: "[10, 100)", + float_range: "[0.5, 0.7)") + + insert_range(id: 103, + date_range: "[''2012-01-02'',]", + num_range: "[0.1,]", + ts_range: "[''2010-01-01 14:30'',]", + tstz_range: "[''2010-01-01 14:30:00+05'',]", + int4_range: "[1,]", + int8_range: "[10,]", + float_range: "[0.5,]") + + insert_range(id: 104, + date_range: "[,]", + num_range: "[,]", + ts_range: "[,]", + tstz_range: "[,]", + int4_range: "[,]", + int8_range: "[,]", + float_range: "[,]") + + insert_range(id: 105, + date_range: "[''2012-01-02'', ''2012-01-02'')", + num_range: "[0.1, 0.1)", + ts_range: "[''2010-01-01 14:30'', ''2010-01-01 14:30'')", + tstz_range: "[''2010-01-01 14:30:00+05'', ''2010-01-01 06:30:00-03'')", + int4_range: "[1, 1)", + int8_range: "[10, 10)", + float_range: "[0.5, 0.5)") + + @new_range = PostgresqlRange.new + @first_range = PostgresqlRange.find(101) + @second_range = PostgresqlRange.find(102) + @third_range = PostgresqlRange.find(103) + @fourth_range = PostgresqlRange.find(104) + @empty_range = PostgresqlRange.find(105) + end + + teardown do + @connection.drop_table "postgresql_ranges", if_exists: true + @connection.execute "DROP TYPE IF EXISTS floatrange" + reset_connection + end + + def test_data_type_of_range_types + assert_equal :daterange, @first_range.column_for_attribute(:date_range).type + assert_equal :numrange, @first_range.column_for_attribute(:num_range).type + assert_equal :tsrange, @first_range.column_for_attribute(:ts_range).type + assert_equal :tstzrange, @first_range.column_for_attribute(:tstz_range).type + assert_equal :int4range, @first_range.column_for_attribute(:int4_range).type + assert_equal :int8range, @first_range.column_for_attribute(:int8_range).type + end + + def test_int4range_values + assert_equal 1...11, @first_range.int4_range + assert_equal 1...10, @second_range.int4_range + assert_equal 1...Float::INFINITY, @third_range.int4_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range) + assert_nil @empty_range.int4_range + end + + def test_int8range_values + assert_equal 10...101, @first_range.int8_range + assert_equal 10...100, @second_range.int8_range + assert_equal 10...Float::INFINITY, @third_range.int8_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range) + assert_nil @empty_range.int8_range + end + + def test_daterange_values + assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range + assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 4), @second_range.date_range + assert_equal Date.new(2012, 1, 2)...Float::INFINITY, @third_range.date_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range) + assert_nil @empty_range.date_range + end + + def test_numrange_values + assert_equal BigDecimal("0.1")..BigDecimal("0.2"), @first_range.num_range + assert_equal BigDecimal("0.1")...BigDecimal("0.2"), @second_range.num_range + assert_equal BigDecimal("0.1")...BigDecimal("Infinity"), @third_range.num_range + assert_equal BigDecimal("-Infinity")...BigDecimal("Infinity"), @fourth_range.num_range + assert_nil @empty_range.num_range + end + + def test_tsrange_values + tz = ::ActiveRecord::Base.default_timezone + assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range + assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range) + assert_nil @empty_range.ts_range + end + + def test_tstzrange_values + assert_equal Time.parse("2010-01-01 09:30:00 UTC")..Time.parse("2011-01-01 17:30:00 UTC"), @first_range.tstz_range + assert_equal Time.parse("2010-01-01 09:30:00 UTC")...Time.parse("2011-01-01 17:30:00 UTC"), @second_range.tstz_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range) + assert_nil @empty_range.tstz_range + end + + def test_custom_range_values + assert_equal 0.5..0.7, @first_range.float_range + assert_equal 0.5...0.7, @second_range.float_range + assert_equal 0.5...Float::INFINITY, @third_range.float_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.float_range) + assert_nil @empty_range.float_range + end + + def test_timezone_awareness_tzrange + tz = "Pacific Time (US & Canada)" + + in_time_zone tz do + PostgresqlRange.reset_column_information + time_string = Time.current.to_s + time = Time.zone.parse(time_string) + + record = PostgresqlRange.new(tstz_range: time_string..time_string) + assert_equal time..time, record.tstz_range + assert_equal ActiveSupport::TimeZone[tz], record.tstz_range.begin.time_zone + + record.save! + record.reload + + assert_equal time..time, record.tstz_range + assert_equal ActiveSupport::TimeZone[tz], record.tstz_range.begin.time_zone + end + end + + def test_create_tstzrange + tstzrange = Time.parse("2010-01-01 14:30:00 +0100")...Time.parse("2011-02-02 14:30:00 CDT") + round_trip(@new_range, :tstz_range, tstzrange) + assert_equal @new_range.tstz_range, tstzrange + assert_equal @new_range.tstz_range, Time.parse("2010-01-01 13:30:00 UTC")...Time.parse("2011-02-02 19:30:00 UTC") + end + + def test_update_tstzrange + assert_equal_round_trip(@first_range, :tstz_range, + Time.parse("2010-01-01 14:30:00 CDT")...Time.parse("2011-02-02 14:30:00 CET")) + assert_nil_round_trip(@first_range, :tstz_range, + Time.parse("2010-01-01 14:30:00 +0100")...Time.parse("2010-01-01 13:30:00 +0000")) + end + + def test_create_tsrange + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@new_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)) + end + + def test_update_tsrange + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)) + assert_nil_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0)) + end + + def test_timezone_awareness_tsrange + tz = "Pacific Time (US & Canada)" + + in_time_zone tz do + PostgresqlRange.reset_column_information + time_string = Time.current.to_s + time = Time.zone.parse(time_string) + + record = PostgresqlRange.new(ts_range: time_string..time_string) + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone + + record.save! + record.reload + + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone + end + end + + def test_create_tstzrange_preserve_usec + tstzrange = Time.parse("2010-01-01 14:30:00.670277 +0100")...Time.parse("2011-02-02 14:30:00.745125 CDT") + round_trip(@new_range, :tstz_range, tstzrange) + assert_equal @new_range.tstz_range, tstzrange + assert_equal @new_range.tstz_range, Time.parse("2010-01-01 13:30:00.670277 UTC")...Time.parse("2011-02-02 19:30:00.745125 UTC") + end + + def test_update_tstzrange_preserve_usec + assert_equal_round_trip(@first_range, :tstz_range, + Time.parse("2010-01-01 14:30:00.245124 CDT")...Time.parse("2011-02-02 14:30:00.451274 CET")) + assert_nil_round_trip(@first_range, :tstz_range, + Time.parse("2010-01-01 14:30:00.245124 +0100")...Time.parse("2010-01-01 13:30:00.245124 +0000")) + end + + def test_create_tsrange_preseve_usec + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@new_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0, 125435)...Time.send(tz, 2011, 2, 2, 14, 30, 0, 225435)) + end + + def test_update_tsrange_preserve_usec + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)...Time.send(tz, 2011, 2, 2, 14, 30, 0, 224242)) + assert_nil_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)...Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)) + end + + def test_timezone_awareness_tsrange_preserve_usec + tz = "Pacific Time (US & Canada)" + + in_time_zone tz do + PostgresqlRange.reset_column_information + time_string = "2017-09-26 07:30:59.132451 -0700" + time = Time.zone.parse(time_string) + assert time.usec > 0 + + record = PostgresqlRange.new(ts_range: time_string..time_string) + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone + assert_equal time.usec, record.ts_range.begin.usec + + record.save! + record.reload + + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone + assert_equal time.usec, record.ts_range.begin.usec + end + end + + def test_create_numrange + assert_equal_round_trip(@new_range, :num_range, + BigDecimal("0.5")...BigDecimal("1")) + end + + def test_update_numrange + assert_equal_round_trip(@first_range, :num_range, + BigDecimal("0.5")...BigDecimal("1")) + assert_nil_round_trip(@first_range, :num_range, + BigDecimal("0.5")...BigDecimal("0.5")) + end + + def test_create_daterange + assert_equal_round_trip(@new_range, :date_range, + Range.new(Date.new(2012, 1, 1), Date.new(2013, 1, 1), true)) + end + + def test_update_daterange + assert_equal_round_trip(@first_range, :date_range, + Date.new(2012, 2, 3)...Date.new(2012, 2, 10)) + assert_nil_round_trip(@first_range, :date_range, + Date.new(2012, 2, 3)...Date.new(2012, 2, 3)) + end + + def test_create_int4range + assert_equal_round_trip(@new_range, :int4_range, Range.new(3, 50, true)) + end + + def test_update_int4range + assert_equal_round_trip(@first_range, :int4_range, 6...10) + assert_nil_round_trip(@first_range, :int4_range, 3...3) + end + + def test_create_int8range + assert_equal_round_trip(@new_range, :int8_range, Range.new(30, 50, true)) + end + + def test_update_int8range + assert_equal_round_trip(@first_range, :int8_range, 60000...10000000) + assert_nil_round_trip(@first_range, :int8_range, 39999...39999) + end + + def test_exclude_beginning_for_subtypes_without_succ_method_is_not_supported + assert_raises(ArgumentError) { PostgresqlRange.create!(num_range: "(0.1, 0.2]") } + assert_raises(ArgumentError) { PostgresqlRange.create!(float_range: "(0.5, 0.7]") } + assert_raises(ArgumentError) { PostgresqlRange.create!(int4_range: "(1, 10]") } + assert_raises(ArgumentError) { PostgresqlRange.create!(int8_range: "(10, 100]") } + assert_raises(ArgumentError) { PostgresqlRange.create!(date_range: "(''2012-01-02'', ''2012-01-04'']") } + assert_raises(ArgumentError) { PostgresqlRange.create!(ts_range: "(''2010-01-01 14:30'', ''2011-01-01 14:30'']") } + assert_raises(ArgumentError) { PostgresqlRange.create!(tstz_range: "(''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']") } + end + + def test_where_by_attribute_with_range + range = 1..100 + record = PostgresqlRange.create!(int4_range: range) + assert_equal record, PostgresqlRange.where(int4_range: range).take + end + + def test_update_all_with_ranges + PostgresqlRange.create! + + PostgresqlRange.update_all(int8_range: 1..100) + + assert_equal 1...101, PostgresqlRange.first.int8_range + end + + def test_ranges_correctly_escape_input + range = "-1,2]'; DROP TABLE postgresql_ranges; --".."a" + PostgresqlRange.update_all(int8_range: range) + + assert_nothing_raised do + PostgresqlRange.first + end + end + + def test_infinity_values + PostgresqlRange.create!(int4_range: 1..Float::INFINITY, + int8_range: -Float::INFINITY..0, + float_range: -Float::INFINITY..Float::INFINITY) + + record = PostgresqlRange.first + + assert_equal(1...Float::INFINITY, record.int4_range) + assert_equal(-Float::INFINITY...1, record.int8_range) + assert_equal(-Float::INFINITY...Float::INFINITY, record.float_range) + end + + private + def assert_equal_round_trip(range, attribute, value) + round_trip(range, attribute, value) + assert_equal value, range.public_send(attribute) + end + + def assert_nil_round_trip(range, attribute, value) + round_trip(range, attribute, value) + assert_nil range.public_send(attribute) + end + + def round_trip(range, attribute, value) + range.public_send "#{attribute}=", value + assert range.save + assert range.reload + end + + def insert_range(values) + @connection.execute <<-SQL + INSERT INTO postgresql_ranges ( + id, + date_range, + num_range, + ts_range, + tstz_range, + int4_range, + int8_range, + float_range + ) VALUES ( + #{values[:id]}, + '#{values[:date_range]}', + '#{values[:num_range]}', + '#{values[:ts_range]}', + '#{values[:tstz_range]}', + '#{values[:int4_range]}', + '#{values[:int8_range]}', + '#{values[:float_range]}' + ) + SQL + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb new file mode 100644 index 0000000000..0bcc214c24 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/connection_helper" + +class PostgreSQLReferentialIntegrityTest < ActiveRecord::PostgreSQLTestCase + self.use_transactional_tests = false + + include ConnectionHelper + + IS_REFERENTIAL_INTEGRITY_SQL = lambda do |sql| + sql.match(/DISABLE TRIGGER ALL/) || sql.match(/ENABLE TRIGGER ALL/) + end + + module MissingSuperuserPrivileges + def execute(sql) + if IS_REFERENTIAL_INTEGRITY_SQL.call(sql) + super "BROKEN;" rescue nil # put transaction in broken state + raise ActiveRecord::StatementInvalid, "PG::InsufficientPrivilege" + else + super + end + end + end + + module ProgrammerMistake + def execute(sql) + if IS_REFERENTIAL_INTEGRITY_SQL.call(sql) + raise ArgumentError, "something is not right." + else + super + end + end + end + + def setup + @connection = ActiveRecord::Base.connection + end + + def teardown + reset_connection + if ActiveRecord::Base.connection.is_a?(MissingSuperuserPrivileges) + raise "MissingSuperuserPrivileges patch was not removed" + end + end + + def test_should_reraise_invalid_foreign_key_exception_and_show_warning + @connection.extend MissingSuperuserPrivileges + + warning = capture(:stderr) do + e = assert_raises(ActiveRecord::InvalidForeignKey) do + @connection.disable_referential_integrity do + raise ActiveRecord::InvalidForeignKey, "Should be re-raised" + end + end + assert_equal "Should be re-raised", e.message + end + assert_match (/WARNING: Rails was not able to disable referential integrity/), warning + assert_match (/cause: PG::InsufficientPrivilege/), warning + end + + def test_does_not_print_warning_if_no_invalid_foreign_key_exception_was_raised + @connection.extend MissingSuperuserPrivileges + + warning = capture(:stderr) do + e = assert_raises(ActiveRecord::StatementInvalid) do + @connection.disable_referential_integrity do + raise ActiveRecord::StatementInvalid, "Should be re-raised" + end + end + assert_equal "Should be re-raised", e.message + end + assert warning.blank?, "expected no warnings but got:\n#{warning}" + end + + def test_does_not_break_transactions + @connection.extend MissingSuperuserPrivileges + + @connection.transaction do + @connection.disable_referential_integrity do + assert_transaction_is_not_broken + end + assert_transaction_is_not_broken + end + end + + def test_does_not_break_nested_transactions + @connection.extend MissingSuperuserPrivileges + + @connection.transaction do + @connection.transaction(requires_new: true) do + @connection.disable_referential_integrity do + assert_transaction_is_not_broken + end + end + assert_transaction_is_not_broken + end + end + + def test_only_catch_active_record_errors_others_bubble_up + @connection.extend ProgrammerMistake + + assert_raises ArgumentError do + @connection.disable_referential_integrity {} + end + end + + private + + def assert_transaction_is_not_broken + assert_equal 1, @connection.select_value("SELECT 1") + end +end diff --git a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb new file mode 100644 index 0000000000..100d247113 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require "cases/helper" + +class PostgresqlRenameTableTest < ActiveRecord::PostgreSQLTestCase + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table :before_rename, force: true + end + + def teardown + @connection.drop_table "before_rename", if_exists: true + @connection.drop_table "after_rename", if_exists: true + end + + test "renaming a table also renames the primary key index" do + # sanity check + assert_equal 1, num_indices_named("before_rename_pkey") + assert_equal 0, num_indices_named("after_rename_pkey") + + @connection.rename_table :before_rename, :after_rename + + assert_equal 0, num_indices_named("before_rename_pkey") + assert_equal 1, num_indices_named("after_rename_pkey") + end + + private + + def num_indices_named(name) + @connection.execute(<<-SQL).values.length + SELECT 1 FROM "pg_index" + JOIN "pg_class" ON "pg_index"."indexrelid" = "pg_class"."oid" + WHERE "pg_class"."relname" = '#{name}' + SQL + end +end diff --git a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb new file mode 100644 index 0000000000..fcb0aec81b --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +require "cases/helper" + +class SchemaThing < ActiveRecord::Base +end + +class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase + self.use_transactional_tests = false + + TABLE_NAME = "schema_things" + COLUMNS = [ + "id serial primary key", + "name character varying(50)" + ] + USERS = ["rails_pg_schema_user1", "rails_pg_schema_user2"] + + def setup + @connection = ActiveRecord::Base.connection + @connection.execute "SET search_path TO '$user',public" + set_session_auth + USERS.each do |u| + @connection.execute "CREATE USER #{u}" rescue nil + @connection.execute "CREATE SCHEMA AUTHORIZATION #{u}" rescue nil + set_session_auth u + @connection.execute "CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})" + @connection.execute "INSERT INTO #{TABLE_NAME} (name) VALUES ('#{u}')" + set_session_auth + end + end + + teardown do + set_session_auth + @connection.execute "RESET search_path" + USERS.each do |u| + @connection.drop_schema u + @connection.execute "DROP USER #{u}" + end + end + + def test_schema_invisible + assert_raise(ActiveRecord::StatementInvalid) do + set_session_auth + @connection.execute "SELECT * FROM #{TABLE_NAME}" + end + end + + def test_session_auth= + assert_raise(ActiveRecord::StatementInvalid) do + @connection.session_auth = "DEFAULT" + @connection.execute "SELECT * FROM #{TABLE_NAME}" + end + end + + def test_setting_auth_clears_stmt_cache + assert_nothing_raised do + set_session_auth + USERS.each do |u| + set_session_auth u + assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = 1") + set_session_auth + end + end + end + + if ActiveRecord::Base.connection.prepared_statements + def test_auth_with_bind + assert_nothing_raised do + set_session_auth + USERS.each do |u| + @connection.clear_cache! + set_session_auth u + assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = $1", "SQL", [bind_param(1)]) + set_session_auth + end + end + end + end + + def test_sequence_schema_caching + assert_nothing_raised do + USERS.each do |u| + set_session_auth u + st = SchemaThing.new name: "TEST1" + st.save! + st = SchemaThing.new id: 5, name: "TEST2" + st.save! + set_session_auth + end + end + end + + def test_tables_in_current_schemas + assert_not_includes @connection.tables, TABLE_NAME + USERS.each do |u| + set_session_auth u + assert_includes @connection.tables, TABLE_NAME + set_session_auth + end + end + + private + def set_session_auth(auth = nil) + @connection.session_auth = auth || "default" + end + + def bind_param(value) + ActiveRecord::Relation::QueryAttribute.new(nil, value, ActiveRecord::Type::Value.new) + end +end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb new file mode 100644 index 0000000000..7ad03c194f --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -0,0 +1,652 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/default" +require "support/schema_dumping_helper" + +module PGSchemaHelper + def with_schema_search_path(schema_search_path) + @connection.schema_search_path = schema_search_path + @connection.schema_cache.clear! + yield if block_given? + ensure + @connection.schema_search_path = "'$user', public" + @connection.schema_cache.clear! + end +end + +class SchemaTest < ActiveRecord::PostgreSQLTestCase + include PGSchemaHelper + self.use_transactional_tests = false + + SCHEMA_NAME = "test_schema" + SCHEMA2_NAME = "test_schema2" + TABLE_NAME = "things" + CAPITALIZED_TABLE_NAME = "Things" + INDEX_A_NAME = "a_index_things_on_name" + INDEX_B_NAME = "b_index_things_on_different_columns_in_each_schema" + INDEX_C_NAME = "c_index_full_text_search" + INDEX_D_NAME = "d_index_things_on_description_desc" + INDEX_E_NAME = "e_index_things_on_name_vector" + INDEX_A_COLUMN = "name" + INDEX_B_COLUMN_S1 = "email" + INDEX_B_COLUMN_S2 = "moment" + INDEX_C_COLUMN = "(to_tsvector('english', coalesce(things.name, '')))" + INDEX_D_COLUMN = "description" + INDEX_E_COLUMN = "name_vector" + COLUMNS = [ + "id integer", + "name character varying(50)", + "email character varying(50)", + "description character varying(100)", + "name_vector tsvector", + "moment timestamp without time zone default now()" + ] + PK_TABLE_NAME = "table_with_pk" + UNMATCHED_SEQUENCE_NAME = "unmatched_primary_key_default_value_seq" + UNMATCHED_PK_TABLE_NAME = "table_with_unmatched_sequence_for_pk" + + class Thing1 < ActiveRecord::Base + self.table_name = "test_schema.things" + end + + class Thing2 < ActiveRecord::Base + self.table_name = "test_schema2.things" + end + + class Thing3 < ActiveRecord::Base + self.table_name = 'test_schema."things.table"' + end + + class Thing4 < ActiveRecord::Base + self.table_name = 'test_schema."Things"' + end + + class Thing5 < ActiveRecord::Base + self.table_name = "things" + end + + class Song < ActiveRecord::Base + self.table_name = "music.songs" + has_and_belongs_to_many :albums + end + + class Album < ActiveRecord::Base + self.table_name = "music.albums" + has_and_belongs_to_many :songs + end + + def setup + @connection = ActiveRecord::Base.connection + @connection.execute "CREATE SCHEMA #{SCHEMA_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})" + @connection.execute "CREATE TABLE #{SCHEMA_NAME}.\"#{TABLE_NAME}.table\" (#{COLUMNS.join(',')})" + @connection.execute "CREATE TABLE #{SCHEMA_NAME}.\"#{CAPITALIZED_TABLE_NAME}\" (#{COLUMNS.join(',')})" + @connection.execute "CREATE SCHEMA #{SCHEMA2_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})" + @connection.execute "CREATE INDEX #{INDEX_A_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING btree (#{INDEX_A_COLUMN});" + @connection.execute "CREATE INDEX #{INDEX_A_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING btree (#{INDEX_A_COLUMN});" + @connection.execute "CREATE INDEX #{INDEX_B_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING btree (#{INDEX_B_COLUMN_S1});" + @connection.execute "CREATE INDEX #{INDEX_B_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING btree (#{INDEX_B_COLUMN_S2});" + @connection.execute "CREATE INDEX #{INDEX_C_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING gin (#{INDEX_C_COLUMN});" + @connection.execute "CREATE INDEX #{INDEX_C_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING gin (#{INDEX_C_COLUMN});" + @connection.execute "CREATE INDEX #{INDEX_D_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING btree (#{INDEX_D_COLUMN} DESC);" + @connection.execute "CREATE INDEX #{INDEX_D_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING btree (#{INDEX_D_COLUMN} DESC);" + @connection.execute "CREATE INDEX #{INDEX_E_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING gin (#{INDEX_E_COLUMN});" + @connection.execute "CREATE INDEX #{INDEX_E_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING gin (#{INDEX_E_COLUMN});" + @connection.execute "CREATE TABLE #{SCHEMA_NAME}.#{PK_TABLE_NAME} (id serial primary key)" + @connection.execute "CREATE TABLE #{SCHEMA2_NAME}.#{PK_TABLE_NAME} (id serial primary key)" + @connection.execute "CREATE SEQUENCE #{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}" + @connection.execute "CREATE TABLE #{SCHEMA_NAME}.#{UNMATCHED_PK_TABLE_NAME} (id integer NOT NULL DEFAULT nextval('#{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}'::regclass), CONSTRAINT unmatched_pkey PRIMARY KEY (id))" + end + + teardown do + @connection.drop_schema SCHEMA2_NAME, if_exists: true + @connection.drop_schema SCHEMA_NAME, if_exists: true + end + + def test_schema_names + assert_equal ["public", "test_schema", "test_schema2"], @connection.schema_names + end + + def test_create_schema + begin + @connection.create_schema "test_schema3" + assert @connection.schema_names.include? "test_schema3" + ensure + @connection.drop_schema "test_schema3" + end + end + + def test_raise_create_schema_with_existing_schema + begin + @connection.create_schema "test_schema3" + assert_raises(ActiveRecord::StatementInvalid) do + @connection.create_schema "test_schema3" + end + ensure + @connection.drop_schema "test_schema3" + end + end + + def test_drop_schema + begin + @connection.create_schema "test_schema3" + ensure + @connection.drop_schema "test_schema3" + end + assert_not_includes @connection.schema_names, "test_schema3" + end + + def test_drop_schema_if_exists + @connection.create_schema "some_schema" + assert_includes @connection.schema_names, "some_schema" + @connection.drop_schema "some_schema", if_exists: true + assert_not_includes @connection.schema_names, "some_schema" + end + + def test_habtm_table_name_with_schema + ActiveRecord::Base.connection.drop_schema "music", if_exists: true + ActiveRecord::Base.connection.create_schema "music" + ActiveRecord::Base.connection.execute <<-SQL + CREATE TABLE music.albums (id serial primary key); + CREATE TABLE music.songs (id serial primary key); + CREATE TABLE music.albums_songs (album_id integer, song_id integer); + SQL + + song = Song.create + Album.create + assert_equal song, Song.includes(:albums).references(:albums).first + ensure + ActiveRecord::Base.connection.drop_schema "music", if_exists: true + end + + def test_drop_schema_with_nonexisting_schema + assert_raises(ActiveRecord::StatementInvalid) do + @connection.drop_schema "idontexist" + end + + assert_nothing_raised do + @connection.drop_schema "idontexist", if_exists: true + end + end + + def test_raise_wrapped_exception_on_bad_prepare + assert_raises(ActiveRecord::StatementInvalid) do + @connection.exec_query "select * from developers where id = ?", "sql", [bind_param(1)] + end + end + + if ActiveRecord::Base.connection.prepared_statements + def test_schema_change_with_prepared_stmt + altered = false + @connection.exec_query "select * from developers where id = $1", "sql", [bind_param(1)] + @connection.exec_query "alter table developers add column zomg int", "sql", [] + altered = true + @connection.exec_query "select * from developers where id = $1", "sql", [bind_param(1)] + ensure + # We are not using DROP COLUMN IF EXISTS because that syntax is only + # supported by pg 9.X + @connection.exec_query("alter table developers drop column zomg", "sql", []) if altered + end + end + + def test_data_source_exists? + [Thing1, Thing2, Thing3, Thing4].each do |klass| + name = klass.table_name + assert @connection.data_source_exists?(name), "'#{name}' data_source should exist" + end + end + + def test_data_source_exists_when_on_schema_search_path + with_schema_search_path(SCHEMA_NAME) do + assert(@connection.data_source_exists?(TABLE_NAME), "data_source should exist and be found") + end + end + + def test_data_source_exists_when_not_on_schema_search_path + with_schema_search_path("PUBLIC") do + assert(!@connection.data_source_exists?(TABLE_NAME), "data_source exists but should not be found") + end + end + + def test_data_source_exists_wrong_schema + assert(!@connection.data_source_exists?("foo.things"), "data_source should not exist") + end + + def test_data_source_exists_quoted_names + [ %("#{SCHEMA_NAME}"."#{TABLE_NAME}"), %(#{SCHEMA_NAME}."#{TABLE_NAME}"), %(#{SCHEMA_NAME}."#{TABLE_NAME}")].each do |given| + assert(@connection.data_source_exists?(given), "data_source should exist when specified as #{given}") + end + with_schema_search_path(SCHEMA_NAME) do + given = %("#{TABLE_NAME}") + assert(@connection.data_source_exists?(given), "data_source should exist when specified as #{given}") + end + end + + def test_data_source_exists_quoted_table + with_schema_search_path(SCHEMA_NAME) do + assert(@connection.data_source_exists?('"things.table"'), "data_source should exist") + end + end + + def test_with_schema_prefixed_table_name + assert_nothing_raised do + assert_equal COLUMNS, columns("#{SCHEMA_NAME}.#{TABLE_NAME}") + end + end + + def test_with_schema_prefixed_capitalized_table_name + assert_nothing_raised do + assert_equal COLUMNS, columns("#{SCHEMA_NAME}.#{CAPITALIZED_TABLE_NAME}") + end + end + + def test_with_schema_search_path + assert_nothing_raised do + with_schema_search_path(SCHEMA_NAME) do + assert_equal COLUMNS, columns(TABLE_NAME) + end + end + end + + def test_proper_encoding_of_table_name + assert_equal '"table_name"', @connection.quote_table_name("table_name") + assert_equal '"table.name"', @connection.quote_table_name('"table.name"') + assert_equal '"schema_name"."table_name"', @connection.quote_table_name("schema_name.table_name") + assert_equal '"schema_name"."table.name"', @connection.quote_table_name('schema_name."table.name"') + assert_equal '"schema.name"."table_name"', @connection.quote_table_name('"schema.name".table_name') + assert_equal '"schema.name"."table.name"', @connection.quote_table_name('"schema.name"."table.name"') + end + + def test_classes_with_qualified_schema_name + assert_equal 0, Thing1.count + assert_equal 0, Thing2.count + assert_equal 0, Thing3.count + assert_equal 0, Thing4.count + + Thing1.create(id: 1, name: "thing1", email: "thing1@localhost", moment: Time.now) + assert_equal 1, Thing1.count + assert_equal 0, Thing2.count + assert_equal 0, Thing3.count + assert_equal 0, Thing4.count + + Thing2.create(id: 1, name: "thing1", email: "thing1@localhost", moment: Time.now) + assert_equal 1, Thing1.count + assert_equal 1, Thing2.count + assert_equal 0, Thing3.count + assert_equal 0, Thing4.count + + Thing3.create(id: 1, name: "thing1", email: "thing1@localhost", moment: Time.now) + assert_equal 1, Thing1.count + assert_equal 1, Thing2.count + assert_equal 1, Thing3.count + assert_equal 0, Thing4.count + + Thing4.create(id: 1, name: "thing1", email: "thing1@localhost", moment: Time.now) + assert_equal 1, Thing1.count + assert_equal 1, Thing2.count + assert_equal 1, Thing3.count + assert_equal 1, Thing4.count + end + + def test_raise_on_unquoted_schema_name + assert_raises(ActiveRecord::StatementInvalid) do + with_schema_search_path "$user,public" + end + end + + def test_without_schema_search_path + assert_raises(ActiveRecord::StatementInvalid) { columns(TABLE_NAME) } + end + + def test_ignore_nil_schema_search_path + assert_nothing_raised { with_schema_search_path nil } + end + + def test_index_name_exists + with_schema_search_path(SCHEMA_NAME) do + assert @connection.index_name_exists?(TABLE_NAME, INDEX_A_NAME) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_B_NAME) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_C_NAME) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_D_NAME) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME) + assert_not @connection.index_name_exists?(TABLE_NAME, "missing_index") + end + end + + def test_dump_indexes_for_schema_one + do_dump_index_tests_for_schema(SCHEMA_NAME, INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN, INDEX_E_COLUMN) + end + + def test_dump_indexes_for_schema_two + do_dump_index_tests_for_schema(SCHEMA2_NAME, INDEX_A_COLUMN, INDEX_B_COLUMN_S2, INDEX_D_COLUMN, INDEX_E_COLUMN) + end + + def test_dump_indexes_for_schema_multiple_schemas_in_search_path + do_dump_index_tests_for_schema("public, #{SCHEMA_NAME}", INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN, INDEX_E_COLUMN) + end + + def test_dump_indexes_for_table_with_scheme_specified_in_name + indexes = @connection.indexes("#{SCHEMA_NAME}.#{TABLE_NAME}") + assert_equal 5, indexes.size + end + + def test_with_uppercase_index_name + @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" + + with_schema_search_path SCHEMA_NAME do + assert_nothing_raised { @connection.remove_index "things", name: "things_Index" } + end + end + + def test_remove_index_when_schema_specified + @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" + assert_nothing_raised { @connection.remove_index "things", name: "#{SCHEMA_NAME}.things_Index" } + + @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" + assert_nothing_raised { @connection.remove_index "#{SCHEMA_NAME}.things", name: "things_Index" } + + @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" + assert_nothing_raised { @connection.remove_index "#{SCHEMA_NAME}.things", name: "#{SCHEMA_NAME}.things_Index" } + + @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" + assert_raises(ArgumentError) { @connection.remove_index "#{SCHEMA2_NAME}.things", name: "#{SCHEMA_NAME}.things_Index" } + end + + def test_primary_key_with_schema_specified + [ + %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}"), + %(#{SCHEMA_NAME}."#{PK_TABLE_NAME}"), + %(#{SCHEMA_NAME}.#{PK_TABLE_NAME}) + ].each do |given| + assert_equal "id", @connection.primary_key(given), "primary key should be found when table referenced as #{given}" + end + end + + def test_primary_key_assuming_schema_search_path + with_schema_search_path("#{SCHEMA_NAME}, #{SCHEMA2_NAME}") do + assert_equal "id", @connection.primary_key(PK_TABLE_NAME), "primary key should be found" + end + end + + def test_pk_and_sequence_for_with_schema_specified + pg_name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name + [ + %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}"), + %("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}") + ].each do |given| + pk, seq = @connection.pk_and_sequence_for(given) + assert_equal "id", pk, "primary key should be found when table referenced as #{given}" + assert_equal pg_name.new(SCHEMA_NAME, "#{PK_TABLE_NAME}_id_seq"), seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}") + assert_equal pg_name.new(SCHEMA_NAME, UNMATCHED_SEQUENCE_NAME), seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}") + end + end + + def test_current_schema + { + %('$user',public) => "public", + SCHEMA_NAME => SCHEMA_NAME, + %(#{SCHEMA2_NAME},#{SCHEMA_NAME},public) => SCHEMA2_NAME, + %(public,#{SCHEMA2_NAME},#{SCHEMA_NAME}) => "public" + }.each do |given, expect| + with_schema_search_path(given) { assert_equal expect, @connection.current_schema } + end + end + + def test_prepared_statements_with_multiple_schemas + [SCHEMA_NAME, SCHEMA2_NAME].each do |schema_name| + with_schema_search_path schema_name do + Thing5.create(id: 1, name: "thing inside #{SCHEMA_NAME}", email: "thing1@localhost", moment: Time.now) + end + end + + [SCHEMA_NAME, SCHEMA2_NAME].each do |schema_name| + with_schema_search_path schema_name do + assert_equal 1, Thing5.count + end + end + end + + def test_schema_exists? + { + "public" => true, + SCHEMA_NAME => true, + SCHEMA2_NAME => true, + "darkside" => false + }.each do |given, expect| + assert_equal expect, @connection.schema_exists?(given) + end + end + + def test_reset_pk_sequence + sequence_name = "#{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}" + @connection.execute "SELECT setval('#{sequence_name}', 123)" + assert_equal 124, @connection.select_value("SELECT nextval('#{sequence_name}')") + @connection.reset_pk_sequence!("#{SCHEMA_NAME}.#{UNMATCHED_PK_TABLE_NAME}") + assert_equal 1, @connection.select_value("SELECT nextval('#{sequence_name}')") + end + + def test_set_pk_sequence + table_name = "#{SCHEMA_NAME}.#{PK_TABLE_NAME}" + _, sequence_name = @connection.pk_and_sequence_for table_name + @connection.set_pk_sequence! table_name, 123 + assert_equal 124, @connection.select_value("SELECT nextval('#{sequence_name}')") + @connection.reset_pk_sequence! table_name + end + + private + def columns(table_name) + @connection.send(:column_definitions, table_name).map do |name, type, default| + "#{name} #{type}" + (default ? " default #{default}" : "") + end + end + + def do_dump_index_tests_for_schema(this_schema_name, first_index_column_name, second_index_column_name, third_index_column_name, fourth_index_column_name) + with_schema_search_path(this_schema_name) do + indexes = @connection.indexes(TABLE_NAME).sort_by(&:name) + assert_equal 5, indexes.size + + index_a, index_b, index_c, index_d, index_e = indexes + + do_dump_index_assertions_for_one_index(index_a, INDEX_A_NAME, first_index_column_name) + do_dump_index_assertions_for_one_index(index_b, INDEX_B_NAME, second_index_column_name) + do_dump_index_assertions_for_one_index(index_d, INDEX_D_NAME, third_index_column_name) + do_dump_index_assertions_for_one_index(index_e, INDEX_E_NAME, fourth_index_column_name) + + assert_equal :btree, index_a.using + assert_equal :btree, index_b.using + assert_equal :gin, index_c.using + assert_equal :btree, index_d.using + assert_equal :gin, index_e.using + + assert_equal :desc, index_d.orders + end + end + + def do_dump_index_assertions_for_one_index(this_index, this_index_name, this_index_column) + assert_equal TABLE_NAME, this_index.table + assert_equal 1, this_index.columns.size + assert_equal this_index_column, this_index.columns[0] + assert_equal this_index_name, this_index.name + end + + def bind_param(value) + ActiveRecord::Relation::QueryAttribute.new(nil, value, ActiveRecord::Type::Value.new) + end +end + +class SchemaForeignKeyTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + setup do + @connection = ActiveRecord::Base.connection + end + + def test_dump_foreign_key_targeting_different_schema + @connection.create_schema "my_schema" + @connection.create_table "my_schema.trains" do |t| + t.string :name + end + @connection.create_table "wagons" do |t| + t.integer :train_id + end + @connection.add_foreign_key "wagons", "my_schema.trains", column: "train_id" + output = dump_table_schema "wagons" + assert_match %r{\s+add_foreign_key "wagons", "my_schema\.trains", column: "train_id"$}, output + ensure + @connection.drop_table "wagons", if_exists: true + @connection.drop_table "my_schema.trains", if_exists: true + @connection.drop_schema "my_schema", if_exists: true + end +end + +class SchemaIndexOpclassTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table "trains" do |t| + t.string :name + t.text :description + end + end + + teardown do + @connection.drop_table "trains", if_exists: true + end + + def test_string_opclass_is_dumped + @connection.execute "CREATE INDEX trains_name_and_description ON trains USING btree(name text_pattern_ops, description text_pattern_ops)" + + output = dump_table_schema "trains" + + assert_match(/opclass: :text_pattern_ops/, output) + end + + def test_non_default_opclass_is_dumped + @connection.execute "CREATE INDEX trains_name_and_description ON trains USING btree(name, description text_pattern_ops)" + + output = dump_table_schema "trains" + + assert_match(/opclass: \{ description: :text_pattern_ops \}/, output) + end +end + +class SchemaIndexNullsOrderTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table "trains" do |t| + t.string :name + t.text :description + end + end + + teardown do + @connection.drop_table "trains", if_exists: true + end + + def test_nulls_order_is_dumped + @connection.execute "CREATE INDEX trains_name_and_description ON trains USING btree(name NULLS FIRST, description)" + output = dump_table_schema "trains" + assert_match(/order: \{ name: "NULLS FIRST" \}/, output) + end + + def test_non_default_order_with_nulls_is_dumped + @connection.execute "CREATE INDEX trains_name_and_desc ON trains USING btree(name DESC NULLS LAST, description)" + output = dump_table_schema "trains" + assert_match(/order: \{ name: "DESC NULLS LAST" \}/, output) + end +end + +class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCase + setup do + @connection = ActiveRecord::Base.connection + @connection.drop_schema "schema_1", if_exists: true + @connection.execute "CREATE SCHEMA schema_1" + @connection.execute "CREATE DOMAIN schema_1.text AS text" + @connection.execute "CREATE DOMAIN schema_1.varchar AS varchar" + @connection.execute "CREATE DOMAIN schema_1.bpchar AS bpchar" + + @old_search_path = @connection.schema_search_path + @connection.schema_search_path = "schema_1, pg_catalog" + @connection.create_table "defaults" do |t| + t.text "text_col", default: "some value" + t.string "string_col", default: "some value" + t.decimal "decimal_col", default: "3.14159265358979323846" + end + Default.reset_column_information + end + + teardown do + @connection.schema_search_path = @old_search_path + @connection.drop_schema "schema_1", if_exists: true + Default.reset_column_information + end + + def test_text_defaults_in_new_schema_when_overriding_domain + assert_equal "some value", Default.new.text_col, "Default of text column was not correctly parsed" + end + + def test_string_defaults_in_new_schema_when_overriding_domain + assert_equal "some value", Default.new.string_col, "Default of string column was not correctly parsed" + end + + def test_decimal_defaults_in_new_schema_when_overriding_domain + assert_equal BigDecimal("3.14159265358979323846"), Default.new.decimal_col, "Default of decimal column was not correctly parsed" + end + + def test_bpchar_defaults_in_new_schema_when_overriding_domain + @connection.execute "ALTER TABLE defaults ADD bpchar_col bpchar DEFAULT 'some value'" + Default.reset_column_information + assert_equal "some value", Default.new.bpchar_col, "Default of bpchar column was not correctly parsed" + end + + def test_text_defaults_after_updating_column_default + @connection.execute "ALTER TABLE defaults ALTER COLUMN text_col SET DEFAULT 'some text'::schema_1.text" + assert_equal "some text", Default.new.text_col, "Default of text column was not correctly parsed after updating default using '::text' since postgreSQL will add parens to the default in db" + end + + def test_default_containing_quote_and_colons + @connection.execute "ALTER TABLE defaults ALTER COLUMN string_col SET DEFAULT 'foo''::bar'" + assert_equal "foo'::bar", Default.new.string_col + end +end + +class SchemaWithDotsTest < ActiveRecord::PostgreSQLTestCase + include PGSchemaHelper + self.use_transactional_tests = false + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_schema "my.schema" + end + + teardown do + @connection.drop_schema "my.schema", if_exists: true + end + + test "rename_table" do + with_schema_search_path('"my.schema"') do + @connection.create_table :posts + @connection.rename_table :posts, :articles + assert_equal ["articles"], @connection.tables + end + end + + test "Active Record basics" do + with_schema_search_path('"my.schema"') do + @connection.create_table :articles do |t| + t.string :title + end + article_class = Class.new(ActiveRecord::Base) do + self.table_name = '"my.schema".articles' + end + + article_class.create!(title: "zOMG, welcome to my blorgh!") + welcome_article = article_class.last + assert_equal "zOMG, welcome to my blorgh!", welcome_article.title + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/serial_test.rb b/activerecord/test/cases/adapters/postgresql/serial_test.rb new file mode 100644 index 0000000000..83ea86be6d --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/serial_test.rb @@ -0,0 +1,156 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/schema_dumping_helper" + +class PostgresqlSerialTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + class PostgresqlSerial < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table "postgresql_serials", force: true do |t| + t.serial :seq + t.integer :serials_id, default: -> { "nextval('postgresql_serials_id_seq')" } + end + end + + teardown do + @connection.drop_table "postgresql_serials", if_exists: true + end + + def test_serial_column + column = PostgresqlSerial.columns_hash["seq"] + assert_equal :integer, column.type + assert_equal "integer", column.sql_type + assert_predicate column, :serial? + end + + def test_not_serial_column + column = PostgresqlSerial.columns_hash["serials_id"] + assert_equal :integer, column.type + assert_equal "integer", column.sql_type + assert_not_predicate column, :serial? + end + + def test_schema_dump_with_shorthand + output = dump_table_schema "postgresql_serials" + assert_match %r{t\.serial\s+"seq",\s+null: false$}, output + end + + def test_schema_dump_with_not_serial + output = dump_table_schema "postgresql_serials" + assert_match %r{t\.integer\s+"serials_id",\s+default: -> \{ "nextval\('postgresql_serials_id_seq'::regclass\)" \}$}, output + end +end + +class PostgresqlBigSerialTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + class PostgresqlBigSerial < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table "postgresql_big_serials", force: true do |t| + t.bigserial :seq + t.bigint :serials_id, default: -> { "nextval('postgresql_big_serials_id_seq')" } + end + end + + teardown do + @connection.drop_table "postgresql_big_serials", if_exists: true + end + + def test_bigserial_column + column = PostgresqlBigSerial.columns_hash["seq"] + assert_equal :integer, column.type + assert_equal "bigint", column.sql_type + assert_predicate column, :serial? + end + + def test_not_bigserial_column + column = PostgresqlBigSerial.columns_hash["serials_id"] + assert_equal :integer, column.type + assert_equal "bigint", column.sql_type + assert_not_predicate column, :serial? + end + + def test_schema_dump_with_shorthand + output = dump_table_schema "postgresql_big_serials" + assert_match %r{t\.bigserial\s+"seq",\s+null: false$}, output + end + + def test_schema_dump_with_not_bigserial + output = dump_table_schema "postgresql_big_serials" + assert_match %r{t\.bigint\s+"serials_id",\s+default: -> \{ "nextval\('postgresql_big_serials_id_seq'::regclass\)" \}$}, output + end +end + +module SequenceNameDetectionTestCases + class CollidedSequenceNameTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table :foo_bar, force: true do |t| + t.serial :baz_id + end + @connection.create_table :foo, force: true do |t| + t.serial :bar_id + t.bigserial :bar_baz_id + end + end + + def teardown + @connection.drop_table :foo_bar, if_exists: true + @connection.drop_table :foo, if_exists: true + end + + def test_serial_columns + columns = @connection.columns(:foo) + columns.each do |column| + assert_equal :integer, column.type + assert_predicate column, :serial? + end + end + + def test_schema_dump_with_collided_sequence_name + output = dump_table_schema "foo" + assert_match %r{t\.serial\s+"bar_id",\s+null: false$}, output + assert_match %r{t\.bigserial\s+"bar_baz_id",\s+null: false$}, output + end + end + + class LongerSequenceNameDetectionTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + def setup + @table_name = "long_table_name_to_test_sequence_name_detection_for_serial_cols" + @connection = ActiveRecord::Base.connection + @connection.create_table @table_name, force: true do |t| + t.serial :seq + t.bigserial :bigseq + end + end + + def teardown + @connection.drop_table @table_name, if_exists: true + end + + def test_serial_columns + columns = @connection.columns(@table_name) + columns.each do |column| + assert_equal :integer, column.type + assert_predicate column, :serial? + end + end + + def test_schema_dump_with_long_table_name + output = dump_table_schema @table_name + assert_match %r{create_table "#{@table_name}", force: :cascade}, output + assert_match %r{t\.serial\s+"seq",\s+null: false$}, output + assert_match %r{t\.bigserial\s+"bigseq",\s+null: false$}, output + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb new file mode 100644 index 0000000000..fef4b02b04 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + class InactivePgConnection + def query(*args) + raise PG::Error + end + + def status + PG::CONNECTION_BAD + end + end + + class StatementPoolTest < ActiveRecord::PostgreSQLTestCase + if Process.respond_to?(:fork) + def test_cache_is_per_pid + cache = StatementPool.new nil, 10 + cache["foo"] = "bar" + assert_equal "bar", cache["foo"] + + pid = fork { + lookup = cache["foo"] + exit!(!lookup) + } + + Process.waitpid pid + assert $?.success?, "process should exit successfully" + end + end + + def test_dealloc_does_not_raise_on_inactive_connection + cache = StatementPool.new InactivePgConnection.new, 10 + cache["foo"] = "bar" + assert_nothing_raised { cache.clear } + end + end + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb new file mode 100644 index 0000000000..b7f213efc8 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/developer" +require "models/topic" + +class PostgresqlTimestampTest < ActiveRecord::PostgreSQLTestCase + class PostgresqlTimestampWithZone < ActiveRecord::Base; end + + self.use_transactional_tests = false + + setup do + @connection = ActiveRecord::Base.connection + @connection.execute("INSERT INTO postgresql_timestamp_with_zones (id, time) VALUES (1, '2010-01-01 10:00:00-1')") + end + + teardown do + PostgresqlTimestampWithZone.delete_all + end + + def test_timestamp_with_zone_values_with_rails_time_zone_support + with_timezone_config default: :utc, aware_attributes: true do + @connection.reconnect! + + timestamp = PostgresqlTimestampWithZone.find(1) + assert_equal Time.utc(2010, 1, 1, 11, 0, 0), timestamp.time + assert_instance_of Time, timestamp.time + end + ensure + @connection.reconnect! + end + + def test_timestamp_with_zone_values_without_rails_time_zone_support + with_timezone_config default: :local, aware_attributes: false do + @connection.reconnect! + # make sure to use a non-UTC time zone + @connection.execute("SET time zone 'America/Jamaica'", "SCHEMA") + + timestamp = PostgresqlTimestampWithZone.find(1) + assert_equal Time.utc(2010, 1, 1, 11, 0, 0), timestamp.time + assert_instance_of Time, timestamp.time + end + ensure + @connection.reconnect! + end +end + +class PostgresqlTimestampFixtureTest < ActiveRecord::PostgreSQLTestCase + fixtures :topics + + def test_group_by_date + keys = Topic.group("date_trunc('month', created_at)").count.keys + assert_operator keys.length, :>, 0 + keys.each { |k| assert_kind_of Time, k } + end + + def test_load_infinity_and_beyond + d = Developer.find_by_sql("select 'infinity'::timestamp as updated_at") + assert d.first.updated_at.infinite?, "timestamp should be infinite" + + d = Developer.find_by_sql("select '-infinity'::timestamp as updated_at") + time = d.first.updated_at + assert time.infinite?, "timestamp should be infinite" + assert_operator time, :<, 0 + end + + def test_save_infinity_and_beyond + d = Developer.create!(name: "aaron", updated_at: 1.0 / 0.0) + assert_equal(1.0 / 0.0, d.updated_at) + + d = Developer.create!(name: "aaron", updated_at: -1.0 / 0.0) + assert_equal(-1.0 / 0.0, d.updated_at) + end + + def test_bc_timestamp + date = Date.new(0) - 1.week + Developer.create!(name: "aaron", updated_at: date) + assert_equal date, Developer.find_by_name("aaron").updated_at + end + + def test_bc_timestamp_leap_year + date = Time.utc(-4, 2, 29) + Developer.create!(name: "taihou", updated_at: date) + assert_equal date, Developer.find_by_name("taihou").updated_at + end + + def test_bc_timestamp_year_zero + date = Time.utc(0, 4, 7) + Developer.create!(name: "yahagi", updated_at: date) + assert_equal date, Developer.find_by_name("yahagi").updated_at + end +end diff --git a/activerecord/test/cases/adapters/postgresql/transaction_test.rb b/activerecord/test/cases/adapters/postgresql/transaction_test.rb new file mode 100644 index 0000000000..8e952a9408 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/transaction_test.rb @@ -0,0 +1,190 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/connection_helper" +require "concurrent/atomic/cyclic_barrier" + +module ActiveRecord + class PostgresqlTransactionTest < ActiveRecord::PostgreSQLTestCase + self.use_transactional_tests = false + + class Sample < ActiveRecord::Base + self.table_name = "samples" + end + + setup do + @abort, Thread.abort_on_exception = Thread.abort_on_exception, false + Thread.report_on_exception, @original_report_on_exception = false, Thread.report_on_exception if Thread.respond_to?(:report_on_exception) + + @connection = ActiveRecord::Base.connection + + @connection.transaction do + @connection.drop_table "samples", if_exists: true + @connection.create_table("samples") do |t| + t.integer "value" + end + end + + Sample.reset_column_information + end + + teardown do + @connection.drop_table "samples", if_exists: true + + Thread.abort_on_exception = @abort + Thread.report_on_exception = @original_report_on_exception if Thread.respond_to?(:report_on_exception) + end + + test "raises SerializationFailure when a serialization failure occurs" do + assert_raises(ActiveRecord::SerializationFailure) do + before = Concurrent::CyclicBarrier.new(2) + after = Concurrent::CyclicBarrier.new(2) + + thread = Thread.new do + with_warning_suppression do + Sample.transaction isolation: :serializable do + before.wait + Sample.create value: Sample.sum(:value) + after.wait + end + end + end + + begin + with_warning_suppression do + Sample.transaction isolation: :serializable do + before.wait + Sample.create value: Sample.sum(:value) + after.wait + end + end + ensure + thread.join + end + end + end + + test "raises Deadlocked when a deadlock is encountered" do + with_warning_suppression do + assert_raises(ActiveRecord::Deadlocked) do + barrier = Concurrent::CyclicBarrier.new(2) + + s1 = Sample.create value: 1 + s2 = Sample.create value: 2 + + thread = Thread.new do + Sample.transaction do + s1.lock! + barrier.wait + s2.update value: 1 + end + end + + begin + Sample.transaction do + s2.lock! + barrier.wait + s1.update value: 2 + end + ensure + thread.join + end + end + end + end + + test "raises LockWaitTimeout when lock wait timeout exceeded" do + skip unless ActiveRecord::Base.connection.postgresql_version >= 90300 + assert_raises(ActiveRecord::LockWaitTimeout) do + s = Sample.create!(value: 1) + latch1 = Concurrent::CountDownLatch.new + latch2 = Concurrent::CountDownLatch.new + + thread = Thread.new do + Sample.transaction do + Sample.lock.find(s.id) + latch1.count_down + latch2.wait + end + end + + begin + Sample.transaction do + latch1.wait + Sample.connection.execute("SET lock_timeout = 1") + Sample.lock.find(s.id) + end + ensure + Sample.connection.execute("SET lock_timeout = DEFAULT") + latch2.count_down + thread.join + end + end + end + + test "raises QueryCanceled when statement timeout exceeded" do + assert_raises(ActiveRecord::QueryCanceled) do + s = Sample.create!(value: 1) + latch1 = Concurrent::CountDownLatch.new + latch2 = Concurrent::CountDownLatch.new + + thread = Thread.new do + Sample.transaction do + Sample.lock.find(s.id) + latch1.count_down + latch2.wait + end + end + + begin + Sample.transaction do + latch1.wait + Sample.connection.execute("SET statement_timeout = 1") + Sample.lock.find(s.id) + end + ensure + Sample.connection.execute("SET statement_timeout = DEFAULT") + latch2.count_down + thread.join + end + end + end + + test "raises QueryCanceled when canceling statement due to user request" do + assert_raises(ActiveRecord::QueryCanceled) do + s = Sample.create!(value: 1) + latch = Concurrent::CountDownLatch.new + + thread = Thread.new do + Sample.transaction do + Sample.lock.find(s.id) + latch.count_down + sleep(0.5) + conn = Sample.connection + pid = conn.query_value("SELECT pid FROM pg_stat_activity WHERE query LIKE '% FOR UPDATE'") + conn.execute("SELECT pg_cancel_backend(#{pid})") + end + end + + begin + Sample.transaction do + latch.wait + Sample.lock.find(s.id) + end + ensure + thread.join + end + end + end + + private + + def with_warning_suppression + log_level = ActiveRecord::Base.connection.client_min_messages + ActiveRecord::Base.connection.client_min_messages = "error" + yield + ensure + ActiveRecord::Base.connection.client_min_messages = log_level + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb new file mode 100644 index 0000000000..8212ed4263 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "cases/helper" + +class PostgresqlTypeLookupTest < ActiveRecord::PostgreSQLTestCase + setup do + @connection = ActiveRecord::Base.connection + end + + test "array delimiters are looked up correctly" do + box_array = @connection.send(:type_map).lookup(1020) + int_array = @connection.send(:type_map).lookup(1007) + + assert_equal ";", box_array.delimiter + assert_equal ",", int_array.delimiter + end + + test "array types correctly respect registration of subtypes" do + int_array = @connection.send(:type_map).lookup(1007, -1, "integer[]") + bigint_array = @connection.send(:type_map).lookup(1016, -1, "bigint[]") + big_array = [123456789123456789] + + assert_raises(ActiveModel::RangeError) { int_array.serialize(big_array) } + assert_equal "{123456789123456789}", @connection.type_cast(bigint_array.serialize(big_array)) + end + + test "range types correctly respect registration of subtypes" do + int_range = @connection.send(:type_map).lookup(3904, -1, "int4range") + bigint_range = @connection.send(:type_map).lookup(3926, -1, "int8range") + big_range = 0..123456789123456789 + + assert_raises(ActiveModel::RangeError) { int_range.serialize(big_range) } + assert_equal "[0,123456789123456789]", @connection.type_cast(bigint_range.serialize(big_range)) + end +end diff --git a/activerecord/test/cases/adapters/postgresql/utils_test.rb b/activerecord/test/cases/adapters/postgresql/utils_test.rb new file mode 100644 index 0000000000..c91884f384 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/utils_test.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require "cases/helper" +require "active_record/connection_adapters/postgresql/utils" + +class PostgreSQLUtilsTest < ActiveRecord::PostgreSQLTestCase + Name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name + include ActiveRecord::ConnectionAdapters::PostgreSQL::Utils + + def test_extract_schema_qualified_name + { + %(table_name) => [nil, "table_name"], + %("table.name") => [nil, "table.name"], + %(schema.table_name) => %w{schema table_name}, + %("schema".table_name) => %w{schema table_name}, + %(schema."table_name") => %w{schema table_name}, + %("schema"."table_name") => %w{schema table_name}, + %("even spaces".table) => ["even spaces", "table"], + %(schema."table.name") => ["schema", "table.name"] + }.each do |given, expect| + assert_equal Name.new(*expect), extract_schema_qualified_name(given) + end + end +end + +class PostgreSQLNameTest < ActiveRecord::PostgreSQLTestCase + Name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name + + test "represents itself as schema.name" do + obj = Name.new("public", "articles") + assert_equal "public.articles", obj.to_s + end + + test "without schema, represents itself as name only" do + obj = Name.new(nil, "articles") + assert_equal "articles", obj.to_s + end + + test "quoted returns a string representation usable in a query" do + assert_equal %("articles"), Name.new(nil, "articles").quoted + assert_equal %("public"."articles"), Name.new("public", "articles").quoted + end + + test "prevents double quoting" do + name = Name.new('"quoted_schema"', '"quoted_table"') + assert_equal "quoted_schema.quoted_table", name.to_s + assert_equal %("quoted_schema"."quoted_table"), name.quoted + end + + test "equality based on state" do + assert_equal Name.new("access", "users"), Name.new("access", "users") + assert_equal Name.new(nil, "users"), Name.new(nil, "users") + assert_not_equal Name.new(nil, "users"), Name.new("access", "users") + assert_not_equal Name.new("access", "users"), Name.new("public", "users") + assert_not_equal Name.new("public", "users"), Name.new("public", "articles") + end + + test "can be used as hash key" do + hash = { Name.new("schema", "article_seq") => "success" } + assert_equal "success", hash[Name.new("schema", "article_seq")] + assert_nil hash[Name.new("schema", "articles")] + assert_nil hash[Name.new("public", "article_seq")] + end +end diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb new file mode 100644 index 0000000000..71d07e2f4c --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -0,0 +1,383 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/schema_dumping_helper" + +module PostgresqlUUIDHelper + def connection + @connection ||= ActiveRecord::Base.connection + end + + def drop_table(name) + connection.drop_table name, if_exists: true + end + + def uuid_function + connection.supports_pgcrypto_uuid? ? "gen_random_uuid()" : "uuid_generate_v4()" + end + + def uuid_default + connection.supports_pgcrypto_uuid? ? {} : { default: uuid_function } + end +end + +class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase + include PostgresqlUUIDHelper + include SchemaDumpingHelper + + class UUIDType < ActiveRecord::Base + self.table_name = "uuid_data_type" + end + + setup do + enable_extension!("uuid-ossp", connection) + enable_extension!("pgcrypto", connection) if connection.supports_pgcrypto_uuid? + + connection.create_table "uuid_data_type" do |t| + t.uuid "guid" + end + end + + teardown do + drop_table "uuid_data_type" + end + + if ActiveRecord::Base.connection.respond_to?(:supports_pgcrypto_uuid?) && + ActiveRecord::Base.connection.supports_pgcrypto_uuid? + def test_uuid_column_default + connection.add_column :uuid_data_type, :thingy, :uuid, null: false, default: "gen_random_uuid()" + UUIDType.reset_column_information + column = UUIDType.columns_hash["thingy"] + assert_equal "gen_random_uuid()", column.default_function + end + end + + def test_change_column_default + connection.add_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v1()" + UUIDType.reset_column_information + column = UUIDType.columns_hash["thingy"] + assert_equal "uuid_generate_v1()", column.default_function + + connection.change_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v4()" + UUIDType.reset_column_information + column = UUIDType.columns_hash["thingy"] + assert_equal "uuid_generate_v4()", column.default_function + ensure + UUIDType.reset_column_information + end + + def test_add_column_with_null_true_and_default_nil + connection.add_column :uuid_data_type, :thingy, :uuid, null: true, default: nil + + UUIDType.reset_column_information + column = UUIDType.columns_hash["thingy"] + + assert column.null + assert_nil column.default + end + + def test_add_column_with_default_array + connection.add_column :uuid_data_type, :thingy, :uuid, array: true, default: [] + + UUIDType.reset_column_information + column = UUIDType.columns_hash["thingy"] + + assert_predicate column, :array? + assert_equal "{}", column.default + + schema = dump_table_schema "uuid_data_type" + assert_match %r{t\.uuid "thingy", default: \[\], array: true$}, schema + end + + def test_data_type_of_uuid_types + column = UUIDType.columns_hash["guid"] + assert_equal :uuid, column.type + assert_equal "uuid", column.sql_type + assert_not_predicate column, :array? + + type = UUIDType.type_for_attribute("guid") + assert_not_predicate type, :binary? + end + + def test_treat_blank_uuid_as_nil + UUIDType.create! guid: "" + assert_nil(UUIDType.last.guid) + end + + def test_treat_invalid_uuid_as_nil + uuid = UUIDType.create! guid: "foobar" + assert_nil(uuid.guid) + end + + def test_invalid_uuid_dont_modify_before_type_cast + uuid = UUIDType.new guid: "foobar" + assert_equal "foobar", uuid.guid_before_type_cast + end + + def test_acceptable_uuid_regex + # Valid uuids + ["A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11", + "{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}", + "a0eebc999c0b4ef8bb6d6bb9bd380a11", + "a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11", + "{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}", + # The following is not a valid RFC 4122 UUID, but PG doesn't seem to care, + # so we shouldn't block it either. (Pay attention to "fb6d" – the "f" here + # is invalid – it must be one of 8, 9, A, B, a, b according to the spec.) + "{a0eebc99-9c0b-4ef8-fb6d-6bb9bd380a11}", + ].each do |valid_uuid| + uuid = UUIDType.new guid: valid_uuid + assert_not_nil uuid.guid + end + + # Invalid uuids + [["A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11"], + Hash.new, + 0, + 0.0, + true, + "Z0000C99-9C0B-4EF8-BB6D-6BB9BD380A11", + "a0eebc999r0b4ef8ab6d6bb9bd380a11", + "a0ee-bc99------4ef8-bb6d-6bb9-bd38-0a11", + "{a0eebc99-bb6d6bb9-bd380a11}", + "{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11", + "a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}"].each do |invalid_uuid| + uuid = UUIDType.new guid: invalid_uuid + assert_nil uuid.guid + end + end + + def test_uuid_formats + ["A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11", + "{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}", + "a0eebc999c0b4ef8bb6d6bb9bd380a11", + "a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11", + "{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}"].each do |valid_uuid| + UUIDType.create(guid: valid_uuid) + uuid = UUIDType.last + assert_equal "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", uuid.guid + end + end + + def test_schema_dump_with_shorthand + output = dump_table_schema "uuid_data_type" + assert_match %r{t\.uuid "guid"}, output + end + + def test_uniqueness_validation_ignores_uuid + klass = Class.new(ActiveRecord::Base) do + self.table_name = "uuid_data_type" + validates :guid, uniqueness: { case_sensitive: false } + + def self.name + "UUIDType" + end + end + + record = klass.create!(guid: "a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11") + duplicate = klass.new(guid: record.guid) + + assert record.guid.present? # Ensure we actually are testing a UUID + assert_not_predicate duplicate, :valid? + end +end + +class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase + include PostgresqlUUIDHelper + include SchemaDumpingHelper + + class UUID < ActiveRecord::Base + self.table_name = "pg_uuids" + end + + setup do + connection.create_table("pg_uuids", id: :uuid, default: "uuid_generate_v1()") do |t| + t.string "name" + t.uuid "other_uuid", default: "uuid_generate_v4()" + end + + # Create custom PostgreSQL function to generate UUIDs + # to test dumping tables which columns have defaults with custom functions + connection.execute <<-SQL + CREATE OR REPLACE FUNCTION my_uuid_generator() RETURNS uuid + AS $$ SELECT * FROM #{uuid_function} $$ + LANGUAGE SQL VOLATILE; + SQL + + # Create such a table with custom function as default value generator + connection.create_table("pg_uuids_2", id: :uuid, default: "my_uuid_generator()") do |t| + t.string "name" + t.uuid "other_uuid_2", default: "my_uuid_generator()" + end + + connection.create_table("pg_uuids_3", id: :uuid, **uuid_default) do |t| + t.string "name" + end + end + + teardown do + drop_table "pg_uuids" + drop_table "pg_uuids_2" + drop_table "pg_uuids_3" + connection.execute "DROP FUNCTION IF EXISTS my_uuid_generator();" + end + + def test_id_is_uuid + assert_equal :uuid, UUID.columns_hash["id"].type + assert UUID.primary_key + end + + def test_id_has_a_default + u = UUID.create + assert_not_nil u.id + end + + def test_auto_create_uuid + u = UUID.create + u.reload + assert_not_nil u.other_uuid + end + + def test_pk_and_sequence_for_uuid_primary_key + pk, seq = connection.pk_and_sequence_for("pg_uuids") + assert_equal "id", pk + assert_nil seq + end + + def test_schema_dumper_for_uuid_primary_key + schema = dump_table_schema "pg_uuids" + assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: -> { "uuid_generate_v1\(\)" }/, schema) + assert_match(/t\.uuid "other_uuid", default: -> { "uuid_generate_v4\(\)" }/, schema) + end + + def test_schema_dumper_for_uuid_primary_key_with_custom_default + schema = dump_table_schema "pg_uuids_2" + assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: -> { "my_uuid_generator\(\)" }/, schema) + assert_match(/t\.uuid "other_uuid_2", default: -> { "my_uuid_generator\(\)" }/, schema) + end + + def test_schema_dumper_for_uuid_primary_key_default + schema = dump_table_schema "pg_uuids_3" + if connection.supports_pgcrypto_uuid? + assert_match(/\bcreate_table "pg_uuids_3", id: :uuid, default: -> { "gen_random_uuid\(\)" }/, schema) + else + assert_match(/\bcreate_table "pg_uuids_3", id: :uuid, default: -> { "uuid_generate_v4\(\)" }/, schema) + end + end + + def test_schema_dumper_for_uuid_primary_key_default_in_legacy_migration + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + + migration = Class.new(ActiveRecord::Migration[5.0]) do + def version; 101 end + def migrate(x) + create_table("pg_uuids_4", id: :uuid) + end + end.new + ActiveRecord::Migrator.new(:up, [migration]).migrate + + schema = dump_table_schema "pg_uuids_4" + assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: -> { "uuid_generate_v4\(\)" }/, schema) + ensure + drop_table "pg_uuids_4" + ActiveRecord::Migration.verbose = @verbose_was + end +end + +class PostgresqlUUIDTestNilDefault < ActiveRecord::PostgreSQLTestCase + include PostgresqlUUIDHelper + include SchemaDumpingHelper + + setup do + connection.create_table("pg_uuids", id: false) do |t| + t.primary_key :id, :uuid, default: nil + t.string "name" + end + end + + teardown do + drop_table "pg_uuids" + end + + def test_id_allows_default_override_via_nil + col_desc = connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default + FROM pg_attribute a + LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum + WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first + assert_nil col_desc["default"] + end + + def test_schema_dumper_for_uuid_primary_key_with_default_override_via_nil + schema = dump_table_schema "pg_uuids" + assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: nil/, schema) + end + + def test_schema_dumper_for_uuid_primary_key_with_default_nil_in_legacy_migration + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + + migration = Class.new(ActiveRecord::Migration[5.0]) do + def version; 101 end + def migrate(x) + create_table("pg_uuids_4", id: :uuid, default: nil) + end + end.new + ActiveRecord::Migrator.new(:up, [migration]).migrate + + schema = dump_table_schema "pg_uuids_4" + assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: nil/, schema) + ensure + drop_table "pg_uuids_4" + ActiveRecord::Migration.verbose = @verbose_was + end +end + +class PostgresqlUUIDTestInverseOf < ActiveRecord::PostgreSQLTestCase + include PostgresqlUUIDHelper + + class UuidPost < ActiveRecord::Base + self.table_name = "pg_uuid_posts" + has_many :uuid_comments, inverse_of: :uuid_post + end + + class UuidComment < ActiveRecord::Base + self.table_name = "pg_uuid_comments" + belongs_to :uuid_post + end + + setup do + connection.transaction do + connection.create_table("pg_uuid_posts", id: :uuid, **uuid_default) do |t| + t.string "title" + end + connection.create_table("pg_uuid_comments", id: :uuid, **uuid_default) do |t| + t.references :uuid_post, type: :uuid + t.string "content" + end + end + end + + teardown do + drop_table "pg_uuid_comments" + drop_table "pg_uuid_posts" + end + + def test_collection_association_with_uuid + post = UuidPost.create! + comment = post.uuid_comments.create! + assert post.uuid_comments.find(comment.id) + end + + def test_find_with_uuid + UuidPost.create! + assert_raise ActiveRecord::RecordNotFound do + UuidPost.find(123456) + end + end + + def test_find_by_with_uuid + UuidPost.create! + assert_nil UuidPost.find_by(id: 789) + end +end diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb new file mode 100644 index 0000000000..71ead6f7f3 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/schema_dumping_helper" + +class PostgresqlXMLTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + class XmlDataType < ActiveRecord::Base + self.table_name = "xml_data_type" + end + + def setup + @connection = ActiveRecord::Base.connection + begin + @connection.transaction do + @connection.create_table("xml_data_type") do |t| + t.xml "payload" + end + end + rescue ActiveRecord::StatementInvalid + skip "do not test on PG without xml" + end + @column = XmlDataType.columns_hash["payload"] + end + + teardown do + @connection.drop_table "xml_data_type", if_exists: true + end + + def test_column + assert_equal :xml, @column.type + end + + def test_null_xml + @connection.execute "insert into xml_data_type (payload) VALUES(null)" + assert_nil XmlDataType.first.payload + end + + def test_round_trip + data = XmlDataType.new(payload: "<foo>bar</foo>") + assert_equal "<foo>bar</foo>", data.payload + data.save! + assert_equal "<foo>bar</foo>", data.reload.payload + end + + def test_update_all + data = XmlDataType.create! + XmlDataType.update_all(payload: "<bar>baz</bar>") + assert_equal "<bar>baz</bar>", data.reload.payload + end + + def test_schema_dump_with_shorthand + output = dump_table_schema("xml_data_type") + assert_match %r{t\.xml "payload"}, output + end +end |