Identifier strategy
Preserving gc_lo_id
gc_lo.id is the incrementing integer stored in the
32-bit MySQL column. gc_lo_id is the synced value stored
on MongoDB documents. At cutover, writes move from MySQL-first to
MongoDB-first, and MongoDB writes do not sync back to MySQL. The
compatibility requirement is that consumers still using
gc_lo_id must continue to receive a positive integer.
gc_lo.id, 32-bit incrementing INT
gc_lo_id, copied from gc_lo.id
gc_lo.id to MongoDB.
gc_lo_id.
Option 1
ULID-derived integer for gc_lo_id
For records created after cutover, MongoDB can generate a ULID and
convert it to its 128-bit positive integer form before sending it as
gc_lo_id. This satisfies the “positive integer” shape,
but it materially changes the integer size that consumers see.
How it works
A ULID is one 128-bit number
The text form is 26 Crockford Base32 characters. Decoding those characters from left to right gives the decimal integer.
ULID layout
- Timestamp
- 1783485022616 ms 2026-07-08T04:30:22.616Z
- Randomness
- 1170567883218641823505623
Integer composition
That produces the 128-bit decimal integer. Generated integers are printed from a BigInt, so they use canonical decimal form with no leading zeroes.
Base32 decode loop
Tradeoffs
ULID tradeoffs
A ULID-derived integer keeps gc_lo_id numeric, but
the number is much larger than the old MySQL 32-bit sequence.
Consumers must treat it as a precise 128-bit value or string.
gc_lo.id.
gc_lo_id travels as a
decimal string or a true arbitrary precision integer.
Option 2
Snowflake ID as gc_lo_id
For records created after cutover, MongoDB can generate a
Snowflake-style positive integer and send it as gc_lo_id.
This keeps the consumer-facing value numeric and much smaller than a
ULID-derived integer, while avoiding writes back to MySQL.
How it works
A Snowflake ID is a packed integer
A common Snowflake layout packs a timestamp, worker identifier, and per-millisecond sequence into one positive 64-bit integer. The exact bit layout can vary by implementation.
Integer composition
With the common 41/10/12 layout, each worker can issue up to 4,096 IDs per millisecond before it has to wait for the next millisecond.
Fit with gc_lo_id
- Before cutover
gc_lo_idis copied from MySQLgc_lo.id- After cutover
gc_lo_idis generated in the MongoDB write path
In SQL terms this would need BIGINT, not 32-bit
INT. For MongoDB and APIs, be explicit about client
serialization because JavaScript numbers may still be unsafe.
Tradeoffs
Snowflake ID tradeoffs
Snowflake IDs are smaller than ULID integers and remain numeric, but they introduce generator coordination and clock assumptions in the MongoDB write path.
BIGINT-sized storage.
Consumers assuming old 32-bit gc_lo.id limits
still need to be checked.
Option 3
gc_sequence bumping as gc_lo_id
For records created after cutover, the MongoDB storage service can
call an internal LO ID allocation endpoint backed by the existing
MySQL gc_sequence bumping logic. The returned positive
integer is then written to MongoDB as gc_lo_id.
How it works
Reuse the existing LO sequence allocator
The current PHP path calls LoRepository::bump(),
which delegates to DB::bump($this->go1, DB::LO).
For LO IDs, that sequence type is lo.
GET_LOCK("bump:lo", 30)
gc_sequence.id for type = 'lo'
(nextId, 'lo') and return nextId
gc_lo_id
Current bump logic
gc_sequence stores one row per allocated ID. It is
not a single mutable counter row, so consumed IDs remain consumed.
Fit with gc_lo_id
- Before cutover
gc_lo.idcomes from the same bump path- After cutover
- MongoDB storage service calls the bump endpoint directly
This is the closest continuation of the existing positive integer contract because the value profile remains the current monotonically increasing LO sequence.
Tradeoffs
gc_sequence bumping tradeoffs
This option best preserves legacy consumer expectations, but it keeps the MongoDB write path dependent on the existing MySQL sequence allocator.
gc_lo.id.
gc_sequence table or an equivalent service.
bump:lo, so this
is suitable for controlled internal usage, not high-volume ID
generation without further work.
GET_LOCK()
succeeded before running the callback. Fix that before exposing
this as a service dependency.