Rails currently creates PostgreSQL bigint primary keys using bigserial:
id bigserial primary key
I would like to propose first-class support for PostgreSQL identity columns as an alternative primary-key generation strategy:
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
Motivation
PostgreSQL has supported identity columns since PostgreSQL 10.
The PostgreSQL documentation describes serial / bigserial as a PostgreSQL-specific way to create an autoincrementing column, and points to the SQL-standard identity-column feature as the alternative:
bigserial is not a true PostgreSQL type. It is shorthand that creates a bigint column, a sequence, and a DEFAULT nextval(...).
For modern PostgreSQL schemas, the equivalent identity form expresses the generation behavior directly:
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
Rails already understands PostgreSQL identity columns at runtime and during schema introspection, but Rails migrations still default to:
NATIVE_DATABASE_TYPES = {
primary_key: "bigserial primary key",
# ...
}
See the current PostgreSQL adapter implementation:
The adapter also already reports support for identity columns via supports_identity_columns?.
So the missing piece is primarily on the DDL / migration-generation side: applications should be able to ask the PostgreSQL adapter to create normal Rails bigint primary keys using IDENTITY without resorting to raw SQL.
Proposed API
I think this should be configurable at the PostgreSQL adapter level, similarly to the existing datetime_type configuration.
Rails currently allows applications to do:
ActiveSupport.on_load(:active_record_postgresqladapter) do
self.datetime_type = :timestamptz
end
A similar API could be introduced for primary-key generation:
ActiveSupport.on_load(:active_record_postgresqladapter) do
self.primary_key_generation = :identity
end
The default could remain:
:serial
for backwards compatibility.
With the default Rails behavior:
create_table :users do |t|
t.string :name
end
Rails would continue to generate:
CREATE TABLE "users" (
"id" bigserial primary key,
"name" character varying
);
With:
ActiveSupport.on_load(:active_record_postgresqladapter) do
self.primary_key_generation = :identity
end
the same idiomatic migration would instead generate:
CREATE TABLE "users" (
"id" bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
"name" character varying
);
This keeps the Rails migration API unchanged while allowing the PostgreSQL adapter to choose the PostgreSQL representation.
Why primary_key_generation rather than primary_key_type
I initially considered an API such as:
self.primary_key_type = :identity
but serial versus identity is not really a difference in the underlying data type.
Both ultimately use a bigint column. What differs is the mechanism used to generate values.
For that reason, something like:
self.primary_key_generation = :serial
self.primary_key_generation = :identity
seems more precise.
It also avoids confusion with existing Rails concepts such as primary_key_type: :uuid, where the actual logical column type changes.
Why GENERATED BY DEFAULT
For Rails’ normal primary-key behavior, I think:
GENERATED BY DEFAULT AS IDENTITY
is a better fit than:
GENERATED ALWAYS AS IDENTITY
PostgreSQL documents that BY DEFAULT allows an explicitly supplied value to take precedence, while ALWAYS rejects an explicit value unless the statement uses OVERRIDING SYSTEM VALUE.
Rails applications and tooling can legitimately provide explicit IDs in cases such as fixtures, imports, data migrations, replication workflows, or restoring existing data.
BY DEFAULT therefore seems closer to the behavior Rails applications already expect from bigserial.
See:
Per-table override
It may also be useful to support an explicit per-table override.
For example:
create_table :users, id: :identity do |t|
# ...
end
or another API that fits Active Record’s existing type system.
The application-level adapter setting would define the normal default, while individual migrations could override it when necessary.
Conceptually:
PostgreSQL adapter default
:serial
Application configuration
primary_key_generation = :identity
Per-table migration override
id: :serial / id: :identity
The exact migration syntax is less important to me than having the adapter-level configuration, because applications that choose identity columns generally want that behavior consistently rather than repeating an option on every create_table.
Backwards compatibility
I see two possible approaches.
The conservative option is to keep bigserial as the Rails default indefinitely and make identity opt-in:
ActiveSupport.on_load(:active_record_postgresqladapter) do
self.primary_key_generation = :identity
end
Another option would be to make IDENTITY the default for a future migration version, while preserving bigserial behavior for older migration versions through Active Record’s migration compatibility layer.
I think the first step can simply be first-class, configurable support. Whether identity should later become the default can be considered separately.
Existing Rails support and related work
There has already been work in Active Record to correctly handle PostgreSQL identity columns.
Identity values not being returned
This issue demonstrated Active Record being used with both:
GENERATED BY DEFAULT AS IDENTITY
and:
GENERATED ALWAYS AS IDENTITY
Runtime support fix
rails/rails#49504 — Fix auto populating IDENTITY columns for PostgreSQL
This PR fixed Active Record’s handling of identity-generated primary keys and was merged.
Earlier discussion involving PostgreSQL identity
rails/rails#33194 — Migrations with auto_increment fail on default postgresql database
This older issue explicitly discussed PostgreSQL identity columns as the PostgreSQL equivalent for automatically generated IDs.
GENERATED ALWAYS / override behavior
rails/rails#36940 — PostgreSQL IDENTITY and OVERRIDE clause for Arel/AR INSERT
This issue discussed the interaction between GENERATED ALWAYS AS IDENTITY and explicit ID insertion.
Identity columns and composite primary keys
This is another example of Active Record already needing to understand identity-column semantics at runtime.
These issues and fixes show that identity columns are already a supported PostgreSQL schema concept in Active Record. The proposal here is to make creation of those columns first-class in the normal Rails migration path as well.
Questions
I would especially appreciate feedback on these points:
- Does an adapter-level setting such as
PostgreSQLAdapter.primary_key_generationfit Active Record’s configuration model? - Are
:serialand:identityreasonable values for that setting? - Should
GENERATED BY DEFAULT AS IDENTITYbe the identity behavior Rails generates by default? - Should there also be a per-table migration override?
- Should identity remain opt-in, or would it make sense to consider it as the default for a future migration compatibility version?
My main goal is not to change Rails’ logical default primary-key type away from bigint.
The goal is to allow PostgreSQL applications to use the modern SQL-standard identity mechanism for that bigint primary key through idiomatic Rails migrations:
bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
instead of requiring raw SQL or continuing to rely on the PostgreSQL-specific bigserial shorthand.