Skip to content

feat!: port the safe-byte-cutoff algorithm from nanoid v6 #22 - #26

Merged
nik-sta merged 5 commits into
mainfrom
feat/22-safe-byte-cutoff
Jul 20, 2026
Merged

feat!: port the safe-byte-cutoff algorithm from nanoid v6 #22#26
nik-sta merged 5 commits into
mainfrom
feat/22-safe-byte-cutoff

Conversation

@nik-sta

@nik-sta nik-sta commented Jul 13, 2026

Copy link
Copy Markdown
Member

Summary

Replaces the pre-v6 power-of-two mask rejection with the modulo-cutoff rejection that ai/nanoid introduced in v6: random bytes below 256 - (256 % alphabetLength) are accepted and mapped with a modulo, higher bytes are rejected to avoid modulo bias.

  • Far fewer rejected bytes for non-power-of-two alphabets: a 33-symbol alphabet accepts 231 of 256 byte values instead of 33 of 64, measured 29% faster on PostgreSQL 17 (100k ids: 0.91s vs 1.28s)
  • Single-symbol alphabets work now; nanoid(5, 'a') previously crashed with "cannot take logarithm of zero"
  • The step formula accounts for the rejection rate itself, so the default additional bytes factor of 1.6 fits every alphabet; the README factor section is rewritten accordingly

Breaking change

The third parameter of nanoid_optimized() changes its meaning from a bitmask to the byte cutoff: pass 256 - (256 % length(alphabet)) instead of (2^n)-1, e.g. 256 instead of 63 for the default 64-symbol alphabet. The nanoid_optimized() calls in the regression tests are updated accordingly.

Verification

  • Unit tests: single-symbol alphabet, 33-symbol alphabet with every symbol reachable
  • Uniformity: chi-square 30.1 (33 symbols, df=32) and 50.5 (64 symbols, df=63) over 200k+ generated characters
  • Full unit and regression suites pass on PostgreSQL 17, including nanoid(102401) and the Introducing functions COST for more parallel operations? #16 parallel scenarios

Closes #22

Summary by CodeRabbit

  • Improvements

    • Improved NanoID generation for custom alphabets (including non–power-of-two sizes) using cutoff-based rejection to avoid modulo bias.
    • Made the NanoID migration apply atomically for more reliable upgrades.
  • Documentation

    • Updated “Upgrading” guidance with a clearer dependent-default rollback procedure.
    • Revised guidance and examples for the additional-bytes factor and updated nanoid_optimized parameter documentation (maskcutoff).
  • Tests

    • Added unit tests for single-character alphabets and broader symbol coverage.
    • Expanded upgrade-path regression tests and updated optimized-generation parameters.

@nik-sta nik-sta added enhancement New feature or request breaking-change Introduces a backwards-incompatible change performance Improves speed or resource usage upstream-parity Tracks feature parity with the original ai/nanoid library labels Jul 13, 2026
@nik-sta
nik-sta force-pushed the feat/22-safe-byte-cutoff branch from 539a0d5 to 802804e Compare July 13, 2026 20:08
@nik-sta
nik-sta changed the base branch from main to chore/19-existing-column-default-docs July 13, 2026 20:08
Base automatically changed from chore/19-existing-column-default-docs to main July 13, 2026 20:33
Replace the pre-v6 power-of-two mask rejection with the modulo-cutoff
rejection that ai/nanoid introduced in v6. Random bytes below
256 - (256 % alphabetLength) are accepted and mapped to a symbol with a
modulo; higher bytes are rejected to avoid modulo bias.

Benefits:
- Far fewer rejected bytes for non-power-of-two alphabets. A 33-symbol
  alphabet now accepts 231 of 256 byte values instead of 33 of 64,
  measured 29% faster on PostgreSQL 17 (100k IDs: 0.91s vs 1.28s).
- Single-symbol alphabets work now. Previously nanoid(5, 'a') failed
  with "cannot take logarithm of zero".
- The step size formula accounts for the rejection rate itself, so the
  default additional bytes factor of 1.6 is a good fit for every
  alphabet. The README section on calculating a custom factor is
  replaced accordingly.

Uniformity verified with chi-square tests: 30.1 (33 symbols, df=32) and
50.5 (64 symbols, df=63) over 200k+ generated characters.

BREAKING CHANGE: the third parameter of nanoid_optimized() changed its
meaning from a bitmask to the byte cutoff. Callers must pass
256 - (256 % length(alphabet)) instead of (2^n)-1, e.g. 256 instead of
63 for the default 64-symbol alphabet. The nanoid_optimized() calls in
the regression tests are updated accordingly.
@nik-sta
nik-sta force-pushed the feat/22-safe-byte-cutoff branch from 802804e to 661c853 Compare July 13, 2026 20:35
@nik-sta

nik-sta commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 81441c40-8773-47a9-892a-f82d9ecb10aa

📥 Commits

Reviewing files that changed from the base of the PR and between bd6e3b1 and d170a0a.

📒 Files selected for processing (4)
  • README.md
  • dev/test/regression_tests.sql
  • dev/test/run_tests.sh
  • nanoid.sql
🚧 Files skipped from review as they are similar to previous changes (2)
  • dev/test/run_tests.sh
  • nanoid.sql

📝 Walkthrough

Walkthrough

nanoid() now uses cutoff-based rejection sampling and bounded step sizing, while nanoid_optimized() accepts a cutoff parameter. The migration is transactional, upgrade-path tests cover dependent defaults, and documentation reflects the updated API.

Changes

Safe byte cutoff algorithm and upgrade handling

Layer / File(s) Summary
Cutoff-based generation
nanoid.sql
nanoid() computes cutoff and bounded step values, and nanoid_optimized() rejects unsafe bytes before modulo-based alphabet mapping. Installation is wrapped in a transaction and removes the legacy overload.
Generation and upgrade validation
dev/test/unit_tests.sql, dev/test/regression_tests.sql, dev/test/run_tests.sh
Tests cover single-symbol and non-power-of-two alphabets, updated optimized calls, transactional upgrade rollback, idempotent reapplication, and dependent-default recovery.
Public usage documentation
README.md
Documentation describes transactional upgrades, cutoff semantics, additional-byte step sizing, updated examples, and parameter guidance.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant nanoid
  participant nanoid_optimized
  participant gen_random_bytes
  nanoid->>nanoid: Compute cutoff and step
  nanoid->>nanoid_optimized: Pass generation parameters
  nanoid_optimized->>gen_random_bytes: Request random bytes
  gen_random_bytes-->>nanoid_optimized: Return random bytes
  nanoid_optimized->>nanoid_optimized: Reject bytes at or above cutoff
  nanoid_optimized-->>nanoid: Return generated identifier
Loading

Possibly related issues

  • viascom/nanoid-mysql-mariadb#5 — Covers porting the same cutoff-based generation behavior, API changes, tests, and documentation to MySQL/MariaDB.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive Most acceptance criteria appear implemented, but the summary doesn't confirm the required chi-square uniformity verification. Add or reference chi-square test coverage proving uniform distribution for non-power-of-two alphabets.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: porting NanoID's safe-byte-cutoff algorithm.
Out of Scope Changes check ✅ Passed The changes stay focused on the NanoID algorithm port, docs, and tests with no clear unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/22-safe-byte-cutoff

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@nanoid.sql`:
- Around line 83-86: Preserve backward compatibility for the existing
nanoid_optimized() four-argument API: do not reinterpret its third argument from
the legacy mask to cutoff. Keep the current cutoff-based implementation under a
new versioned function or add a deprecated wrapper that accepts the old mask
semantics and translates or delegates appropriately, while directing callers
toward the new API.

In `@README.md`:
- Around line 95-99: Update the README section describing nanoid() batching to
show the implemented step calculation as min(1024, ceil(additionalBytesFactor *
256 * size / cutoff)). State that once the 1024-byte cap is reached, increasing
additionalBytesFactor cannot further reduce follow-up batches.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2986b16f-bdb1-4b7a-a867-8481786f9f7d

📥 Commits

Reviewing files that changed from the base of the PR and between 131cc7a and 661c853.

📒 Files selected for processing (4)
  • README.md
  • dev/test/regression_tests.sql
  • dev/test/unit_tests.sql
  • nanoid.sql

Comment thread nanoid.sql
Comment thread README.md Outdated
nik-sta added 2 commits July 13, 2026 23:53
…#22

The whole script now runs in one transaction, so a DROP blocked by dependent
objects rolls back atomically instead of leaving a partially upgraded install
(an old nanoid_optimized() receiving a cutoff as mask would generate constant
ids). The current nanoid() signature is no longer dropped (CREATE OR REPLACE
upgrades it in place, also under dependent column defaults), the 2022-era
nanoid(int, text) signature is dropped to keep positional calls unambiguous,
the step cap moved before the int cast (least), and the test suite gained an
upgrade-path stage per PostgreSQL version.
@nik-sta

nik-sta commented Jul 14, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@nik-sta

nik-sta commented Jul 14, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


You're currently rate limited under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. Your next review will be available in 8 minutes.

@nik-sta

nik-sta commented Jul 14, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

nik-sta added a commit that referenced this pull request Jul 20, 2026
…matrix #30 (#31)

## Summary

Keeps the multi-version test suite honest about "latest minor of every
major" and extends it to the PostgreSQL 19 prerelease:

- `run_tests.sh` now pulls the image before each version run (native
first, amd64 fallback); when the pull fails (offline), it falls back to
the local cache with a note. A stale local cache had been silently
testing 14.20 and 16.13 while 14.23 and 16.14 were the released latest
minors (checked against the [PostgreSQL release
archive](https://www.postgresql.org/docs/release/) on 2026-07-14).
- The default matrix gains `19beta1` (PostgreSQL 19 Beta 1, released
2026-06-04, official `postgres:19beta1-alpine` image). The inline
comment says to bump the prerelease as new betas/RCs land and to replace
it with plain `19` at GA.
- The README test section documents both behaviors.

The documented PostgreSQL 9.6 minimum was re-verified as part of this
work and stays: installation on 9.5.25 fails with `syntax error at or
near "PARALLEL"` (the `PARALLEL` clause of `CREATE FUNCTION` exists
since 9.6), and the suite passes on 9.6.24.

## Verification

- `bash -n` passes
- `dev/test/run_tests.sh 9.6 14 19beta1` passes: 14 now reports server
14.23 (freshly pulled latest minor instead of the stale cached 14.20),
9.6 exercises the amd64 pull fallback, and the current `nanoid.sql`
passes the full unit and regression suites on 19beta1

## Note

Expect a small textual conflict with #26 in `run_tests.sh` and the
README test section (the upgrade-path test stage added there touches
adjacent lines); whichever lands second gets rebased as part of the
usual stack maintenance.

Closes #30

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Tests**
* Expanded PostgreSQL compatibility testing to include versions 9.6
through 19 prerelease.
* Tests now pull the latest minor release for each major version when
available.
* Added offline support by falling back to locally cached Docker images
when pulls fail.
* Updated documentation and test instructions to reflect the broader
test matrix.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
# Conflicts:
#	README.md
#	dev/test/run_tests.sh
#	nanoid.sql
@nik-sta
nik-sta merged commit 771e11f into main Jul 20, 2026
1 check passed
@nik-sta
nik-sta deleted the feat/22-safe-byte-cutoff branch July 20, 2026 18:30
@nik-sta
nik-sta restored the feat/22-safe-byte-cutoff branch July 20, 2026 18:31
@nik-sta
nik-sta deleted the feat/22-safe-byte-cutoff branch July 20, 2026 18:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change Introduces a backwards-incompatible change enhancement New feature or request performance Improves speed or resource usage upstream-parity Tracks feature parity with the original ai/nanoid library

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Port the v6 safe-byte-cutoff algorithm from ai/nanoid

1 participant