[Feature Proposal] <De>serialize PostgreSQL composite types into Ruby hashes

Hi all,

Currently there is no special support for PostgreSQL composite types. They are mapped to normal text columns like “(Paris,Champs-Élysées)”.

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

2 Likes

This is the proposed code in my fork of the project https://github.com/nepalez/rails/commit/335807b531b060049e4265c2046ef881b336b81b

2 Likes

Following the recommendation of @Envek, I added new setting:

config.active_record.pg_composite_type_cast = :hash

If it is set, the behavior is changed. By default the current serialization into string is kept unchanged.

1 Like

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.

1 Like

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

1 Like

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