From ec47484c6d4ad63ae89c5c44a71b82f535bed3ed Mon Sep 17 00:00:00 2001 From: kpiljoong Date: Wed, 20 May 2026 01:44:02 +0900 Subject: [PATCH 1/3] docs: clarify SQL storage lengths and binary types --- README.md | 6 ++++-- spec/0001-spec.md | 13 +++++-------- spec/README.md | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 6d10058..1c5d735 100644 --- a/README.md +++ b/README.md @@ -138,8 +138,10 @@ random60: 0x03699c13c10128b5 **Global lookup by public ID** GSI: `GSI1PK = PUBLICID#{orderlyid}`, `GSI1SK = CONST` -**Postgres / MySQL** -- Store both `id_text VARCHAR(64) UNIQUE` and `id_bin BINARY(20)` +**SQL storage** +- Store the text form as `id_text VARCHAR(69) UNIQUE` +- Use `VARCHAR(64)` only if checksum suffixes are never stored +- Store the 20-byte binary body as `id_bin BINARY(20)` on MySQL/Aurora or `id_bin BYTEA` on PostgreSQL - Index `id_bin` for range scans - Use `id_text` for APIs/logs diff --git a/spec/0001-spec.md b/spec/0001-spec.md index 8c23a47..aefdd65 100644 --- a/spec/0001-spec.md +++ b/spec/0001-spec.md @@ -54,13 +54,10 @@ OrderlyID provides typed, time-sortable, globally unique identifiers optimized f ## 6. Checksum -1. `now_ms = system_clock_ms()`. If privacy enabled, quantize. -2. If another ID in the same ms: increment 12-bit seq; wrap allowed. -3. Draw 60 bits from CSPRNG. -4. Pack fields into 160-bit body, encode to 32 Base32 chars. -5. Emit `"_"`; append checksum if configured. - -*Ordering semantics*: within same prefix, lexicographic sort ≈ `(time, flags, tenant, seq, shard, random)`. +- Domain: HRP-expanded `"_"` plus payload symbols. +- Algorithm: Bech32 polymod, truncated to 20 bits and encoded as 4 Crockford Base32 characters. +- Verification: if present, parsers MUST validate the checksum and reject mismatches. +- False-accept probability: approximately 1 in 1,048,576. ## 7. Generation @@ -96,7 +93,7 @@ OrderlyID provides typed, time-sortable, globally unique identifiers optimized f ## 10. Interop & Tests -- **SQL**: store `id_text VARCHAR(64)` and `id_bin BINARY(20)`; index `id_bin` for range scans. +- **SQL**: store the text form as `id_text VARCHAR(69) UNIQUE`; use `VARCHAR(64)` only if checksum suffixes are never stored. Store the 20-byte binary body as `id_bin BINARY(20)` on MySQL/Aurora or `id_bin BYTEA` on PostgreSQL; index `id_bin` for range scans. - **DynamoDB**: use as sort key; shard global feeds via virtual partitions. - **APIs/logs**: emit typed ID with checksum externally; redact tail if sensitive. - **Conformance**: implementations claiming v1 MUST: diff --git a/spec/README.md b/spec/README.md index 69fb674..d645b09 100644 --- a/spec/README.md +++ b/spec/README.md @@ -94,7 +94,7 @@ checksum: 9xgg ## Interop & storage guidance -- **SQL**: store both `id_text VARCHAR(64) UNIQUE` and `id_bin BINARY(20)`; index `id_bin` for range scans. +- **SQL**: store the text form as `id_text VARCHAR(69) UNIQUE`; use `VARCHAR(64)` only if checksum suffixes are never stored. Store the 20-byte binary body as `id_bin BINARY(20)` on MySQL/Aurora or `id_bin BYTEA` on PostgreSQL; index `id_bin` for range scans. - **DynamoDB**: use ID as sort key; to avoid hot partitions, hash into virtual shards. - **APIs & logs**: prefer the typed public ID; redact tail chars if sensitive. From 206d95bc3b418a6af0433e62bb99904c77340099 Mon Sep 17 00:00:00 2001 From: kpiljoong Date: Wed, 20 May 2026 01:47:09 +0900 Subject: [PATCH 2/3] test: make checksum tamper assertion deterministic --- orderlyid_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/orderlyid_test.go b/orderlyid_test.go index 4db6307..74e292a 100644 --- a/orderlyid_test.go +++ b/orderlyid_test.go @@ -54,7 +54,11 @@ func TestChecksumRoundTrip(t *testing.T) { t.Fatalf("parse with checksum failed: %v", err) } // Tamper last char - bad := id[:len(id)-1] + "0" + repl := byte('0') + if id[len(id)-1] == repl { + repl = '1' + } + bad := id[:len(id)-1] + string(repl) if _, err := Parse(bad); err == nil { t.Fatalf("expected checksum mismatch") } else if !errors.Is(err, ErrInvalidChecksum) { From 5161984b3a52bbfc34cadca4bc1463a8032b64cc Mon Sep 17 00:00:00 2001 From: kpiljoong Date: Wed, 20 May 2026 01:52:27 +0900 Subject: [PATCH 3/3] docs: clarify checksum and generation sections --- spec/0001-spec.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/spec/0001-spec.md b/spec/0001-spec.md index aefdd65..6bc21ea 100644 --- a/spec/0001-spec.md +++ b/spec/0001-spec.md @@ -58,16 +58,18 @@ OrderlyID provides typed, time-sortable, globally unique identifiers optimized f - Algorithm: Bech32 polymod, truncated to 20 bits and encoded as 4 Crockford Base32 characters. - Verification: if present, parsers MUST validate the checksum and reject mismatches. - False-accept probability: approximately 1 in 1,048,576. +- The checksum is an integrity check for transcription errors, not cryptographic authentication. ## 7. Generation -1. `now_ms = system_clock_ms()`. If privacy enabled, quantize. -2. If another ID in the same ms: increment 12-bit seq; wrap allowed. -3. Draw 60 bits from CSPRNG. -4. Pack fields into 160-bit body, encode to 32 Base32 chars. -5. Emit `"_"`; append checksum if configured. +1. `now_ms = system_clock_ms()`. If privacy bucketing is enabled, quantize to the configured bucket size. +2. If another ID is generated in the same millisecond, increment the 12-bit `seq`; wrap is allowed. +3. Draw 60 bits from a CSPRNG. +4. Pack fields into the 160-bit body in big-endian order. +5. Encode the body into exactly 32 Crockford Base32 characters. +6. Emit `"_"`; append `"-"` if checksum is configured. -*Ordering semantics*: within same prefix, lexicographic sort ≈ `(time, flags, tenant, seq, shard, random)`. +*Ordering semantics*: within the same prefix, lexicographic sort ≈ `(time, flags, tenant, seq, shard, random)`. ---