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.

MySQL source today gc_lo.id, 32-bit incrementing INT
MongoDB synced field gc_lo_id, copied from gc_lo.id
Current write path Write to MySQL, then sync gc_lo.id to MongoDB.
Cutover write path Write to MongoDB, with no back-sync to MySQL.
Consumer contract Keep sending a positive integer as gc_lo_id.
Option 1 ULID-derived integer Generate a ULID, then send its 128-bit integer as gc_lo_id Option 2 Snowflake ID Generate a time-sortable positive integer as gc_lo_id Option 3 gc_sequence bumping Expose the current MySQL LO sequence allocator to MongoDB writes

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.

ULID Integer Converter

Convert between the 26-character ULID and its 128-bit integer.

Enter a ULID or integer to convert.

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.

48 bits timestamp in milliseconds
80 bits randomness
26 characters ULID text is fixed length.
First char: 0-7 The top Base32 digit is capped by the 128-bit range.
Up to 39 digits Decimal integers are not padded; valid values are 0 through 2^128 - 1.

ULID layout

01KWZZVGCR YZG9SCNCX09A176Q
Timestamp
1783485022616 ms 2026-07-08T04:30:22.616Z
Randomness
1170567883218641823505623

Integer composition

1783485022616 * 2^80 + 1170567883218641823505623

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

n = 0 for each character n = n * 32 + value(character)
0123 4567 89AB CDEF GHJK MNPQ RSTV WXYZ

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.

Positive integer, but huge A ULID integer can be up to 39 decimal digits. That is a very different value profile from old MySQL gc_lo.id.
Not 32-bit or 64-bit-safe A full ULID is 128 bits. Consumers using 32-bit or 64-bit integer types will not be able to hold every value.
JSON and JavaScript can round the integer JavaScript numbers are exact only through 2^53 - 1. A 128-bit ULID integer can be silently changed if parsed as a Number.
Decimal sorting needs padding The 26-character ULID string sorts chronologically as text. A decimal string only sorts correctly if it is fixed-width padded.
Index/storage shape changes MongoDB can store the value, but applications and downstream systems must agree whether 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.

1 sign
41 bits timestamp since custom epoch
10 bits worker
12 bits sequence

Integer composition

((timestampMs - epochMs) << 22) | (workerId << 12) | sequence

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_id is copied from MySQL gc_lo.id
After cutover
gc_lo_id is 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.

Best fit for positive integer consumers Compared with ULID integers, Snowflake IDs are much smaller and are commonly represented as positive decimal IDs.
Still not 32-bit A Snowflake ID belongs in BIGINT-sized storage. Consumers assuming old 32-bit gc_lo.id limits still need to be checked.
JavaScript still needs care Many Snowflake IDs exceed 2^53 - 1, so APIs should send them as strings when browser or JSON clients are involved.
Workers must be coordinated Each generator needs a unique worker ID. Collisions can happen if two writers use the same worker bits at the same time.
Clocks matter If a machine clock moves backward, the generator must wait, fail, or use a rollback strategy to avoid duplicate IDs.
Metadata is visible IDs reveal rough creation time and sometimes worker patterns, which may matter for public-facing identifiers.
Layout is not universal Snowflake is a pattern, not one strict standard. Epoch, worker bits, and sequence bits need to be documented and fixed.

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.

1 Storage service requests next LO ID
2 Allocator serializes with GET_LOCK("bump:lo", 30)
3 Read latest gc_sequence.id for type = 'lo'
4 Insert (nextId, 'lo') and return nextId
5 MongoDB document stores that value as gc_lo_id

Current bump logic

SELECT id FROM gc_sequence WHERE type = 'lo' ORDER BY id DESC LIMIT 1 nextId = lastId + 1 INSERT (nextId, 'lo')

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.id comes 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.

Best compatibility Consumers continue receiving a familiar positive integer from the same LO sequence style as MySQL gc_lo.id.
Still depends on MySQL Even after MongoDB owns writes, ID allocation still needs the MySQL-backed gc_sequence table or an equivalent service.
Single lock bottleneck LO allocations are serialized by bump:lo, so this is suitable for controlled internal usage, not high-volume ID generation without further work.
Non-idempotent retries Retrying an allocation can burn additional IDs. Gaps are already normal, but callers must not assume gapless IDs.
Lock handling should be hardened The current helper does not check whether GET_LOCK() succeeded before running the callback. Fix that before exposing this as a service dependency.
Expose only internally The endpoint should be staff/service-only, ideally restricted to the storage service at the mesh or network layer.