My proposal is to implement serialization of PostgreSQL composite types into hashes, which is more natural way of representing those types (defined as structures with uniquely named attributes):
# db/migrate/20140207133952_create_contacts.rb
execute <<-SQL
CREATE TYPE full_address AS
(
city VARCHAR(90),
street VARCHAR(90)
);
SQL
create_table :contacts do |t|
t.column :address, :full_address
end
# app/models/contact.rb
class Contact < ApplicationRecord
end
# Usage
# Instead of "(Paris,Champs-Élysées)"
Contact.create address: { city: "Paris", street: "Champs-Élysées" }
Contact.first.address # => { city: "Paris", street: "Champs-Élysées" }
I identified a set of steps to make this possible:
looking for attributes definitions in the pg_attribute catalog for attrelid corresponding to typrelid of the pg_type row
adding recursion to SQL query that extracts type definitions – to load all nested types (including nested composites) into TypeMap at once
fixing a way composite types are identified by the OID::TypeMapInitializer
adding an OID::Composite < Type::Value class to represent a composite type (now all composite types falls back to default strings)
Definitely, this change would be backward-incompatible because it breaks current expectations about the type’n’structure of model attribute values.
I can prepare the PR for the feature, and I’d love to hear your feedback about it
One of the reasons why the attribute APIs exist is to make things like this possible without having to change the framework. So, unless this use case is a lot common I’d say that you would be better off implementing a custom type in your application and using in your model like this:
# app/models/contact.rb
class Contact < ApplicationRecord
attribute :address, MyCustomType.new
end
You already have the implementation of your type in the PR.
This type can be even shared in a gem if more people find it useful.
@rafaelfranca you’re right about the possibility of customizing types on the application side. We can do it now via either attributes API, or by registering a custom subclass of Type::Value.
But exactly the same argument could be applied to any other PostgreSQL type (array, domain, even to integer or float). We could parse the default string at the application level, but this would be a huge overwork. Instead, we defined better defaults for every single case to help a developer with a knowledge extracted from the database.
When a custom type was created in the database, we already defined its structure, and this structure can be extracted in a form of hash. For some cases like (100,”USD”) the conversion to { amount: 100, currency: “USD” } could be enough without any application-specific extension. For the cases you need value object, the attribute API is a solution, but you would start from pre-parsed hashes instead of raw strings.
UPDATE: I recognized that I should justify why Hash is better default for composite type (processed via attributes API) than a raw string. This is because in a string (Moscow,Solyanka) the important information about meaning of its parts is lost, while in the hash { city: "Moscow", street: "Solyanka" } it can be properly preserved. A developer can use this additional information when building a value object.
I have to say, I personally never understood why this wasn’t added when support for other psql specific types was added. defaulting to something usable in 90% of cases is supposed to be what rails is about: making it so the majority of users don’t need to add additional extensions to work with the structure of the database