π Version 3.0.0
π₯ Breaking Changes:
nanoid_optimized()third parameter:maskbecomescutoff: The byte-rejection scheme changed from the pre-v6 power-of-two mask to the modulo-cutoff rejection that ai/nanoid introduced in v6. The third argument is now the exclusive upper bound for accepted random bytes and should be256 - (256 % length(alphabet)), e.g.256instead of63for the default 64-symbol alphabet. Old mask values keep running without an error but silently produce wrong ids: with63and a 64-symbol alphabet, the last symbol of the alphabet can never appear in a generated id. Every directnanoid_optimized()call must be updated; see the migration guide below.- Old
nanoid()signatures are dropped on upgrade:nanoid()gainsprefix text DEFAULT ''as its new last parameter, so the upgrade drops the previousnanoid(int, text, float)signature (and the 2022-erananoid(int, text)) to keep positional calls unambiguous. Existing calls keep working unchanged; only database objects that depend on an old signature (for example column defaults) block the upgrade until you follow the migration guide.
π‘ New Features:
- Safe-byte-cutoff algorithm (#22): Random bytes below
256 - (256 % alphabetLength)are accepted and mapped with a modulo, higher bytes are rejected to avoid modulo bias. Far fewer bytes are rejected 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 (100'000 ids: 0.91s vs 1.28s). Single-symbol alphabets work now;nanoid(5, 'a')previously crashed withcannot take logarithm of zero. - Prefixed ids (#18):
nanoid()takes an optionalprefixthat is prepended to the generated id for Stripe-style typed ids (usr_,ord_, ...), e.g.nanoid(prefix => 'usr_'). The prefix does not count towardssize, so the total id length islength(prefix) + size; aNULLprefix behaves like an empty prefix. Idea by @abdirahmn1. - 256-symbol alphabets (#20): The maximum alphabet length was raised from 255 to 256 symbols. For a 256-symbol alphabet every random byte maps directly to a valid alphabet index; upstream ai/nanoid allows 256 as well.
π Fixes:
- Termination guards in
nanoid_optimized()(#32): A direct call with a non-positive size, an empty orNULLalphabet, or aNULLor non-positive cutoff or step used to loop forever, burning a connection and CPU until the backend was killed. These inputs now raise a clear exception up front; the per-byte hot path stays check-free.
β Changes:
- Atomic install/upgrade:
nanoid.sqlnow runs in a single transaction. Either the upgrade completes as a whole or the database keeps its previous, fully working installation. - Additional bytes factor: The step formula accounts for the rejection rate itself, so the default factor of
1.6now fits every alphabet; the README section was rewritten accordingly. - Test matrix: The latest minor of every image is pulled before each run and the PostgreSQL 19 prerelease was added; the suite now covers every major version from 9.6 through 18 plus 19beta1 (#30).
- README: Documented how to set
nanoid()as the default on an existing column (#19); updated the copyright year to 2026 and added missing license headers (#23).
β Verification:
- Full unit and regression suites pass on all supported majors (PostgreSQL 9.6 through 18 plus the 19 prerelease).
- Uniformity of the new algorithm verified with chi-square tests: 30.1 (33 symbols, df=32) and 50.5 (64 symbols, df=63) over 200'000+ generated characters.
π Migration Guide from nanoid() Version 2.1.1 to 3.0.0:
1. Find existing usages
Column defaults that reference the functions both block step 2 and may carry old mask values:
SELECT n.nspname AS schema,
c.relname AS "table",
a.attname AS "column",
pg_get_expr(d.adbin, d.adrelid) AS default_expression
FROM pg_attrdef d
JOIN pg_class c ON c.oid = d.adrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN pg_attribute a ON a.attrelid = d.adrelid AND a.attnum = d.adnum
WHERE pg_get_expr(d.adbin, d.adrelid) ILIKE '%nanoid%';Also search your application code and stored procedures for direct nanoid_optimized( calls.
2. Upgrade the functions
Run the new nanoid.sql. It runs in a single transaction: either the upgrade completes as a whole or the database keeps its previous, fully working installation. If a dependent object (for example a column default found in step 1) blocks one of the DROP FUNCTION statements, the whole script rolls back; then drop the defaults, run the script, and add them back:
ALTER TABLE mytable ALTER COLUMN id DROP DEFAULT;
-- run nanoid.sql
ALTER TABLE mytable ALTER COLUMN id SET DEFAULT nanoid();3. Update every direct nanoid_optimized() call
Replace the old mask with the new cutoff, 256 - (256 % length(alphabet)):
-- 2.x (mask, wrong on 3.0.0: the last alphabet symbol would never appear):
SELECT nanoid_optimized(10, '_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 63, 16);
-- 3.0.0 (cutoff):
SELECT nanoid_optimized(10, '_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 256, 16);Callers of nanoid() need no changes: the new prefix parameter defaults to ''.
4. Verify
SELECT nanoid(); -- 21 characters from the default alphabet
SELECT nanoid(prefix => 'usr_'); -- 25 characters starting with usr_
SELECT p.oid::regprocedure
FROM pg_proc p
WHERE p.proname IN ('nanoid', 'nanoid_optimized');
-- Expect exactly two rows:
-- nanoid(integer,text,double precision,text)
-- nanoid_optimized(integer,text,integer,integer)Note: When using prefixes, size your columns for length(prefix) + size, for example char(25) for the default size of 21 plus the 4-character prefix usr_.
Full Changelog: 2.1.1...3.0.0