# 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_invalid_uuid_dont_match_to_nil
UUIDType.create!
assert_empty UUIDType.where(guid: "")
assert_empty UUIDType.where(guid: "foobar")
end
class DuckUUID
def initialize(uuid)
@uuid = uuid
end
def to_s
@uuid
end
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}",
# Support Object-Oriented UUIDs which respond to #to_s
DuckUUID.new("A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11"),
].each do |valid_uuid|
uuid = UUIDType.new guid: valid_uuid
assert_instance_of String, 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], ActiveRecord::Base.connection.schema_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
ActiveRecord::Base.connection.schema_migration.delete_all
end
uses_transaction :test_schema_dumper_for_uuid_primary_key_default_in_legacy_migration
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], ActiveRecord::Base.connection.schema_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
ActiveRecord::Base.connection.schema_migration.delete_all
end
uses_transaction :test_schema_dumper_for_uuid_primary_key_with_default_nil_in_legacy_migration
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