Skip to content

v3.0.0 - Safe-Byte-Cutoff Algorithm, Prefixed Ids & 256-Symbol Alphabets

Latest

Choose a tag to compare

@nik-sta nik-sta released this 20 Jul 18:51
Immutable release. Only release title and notes can be modified.
c304264

πŸš€ Version 3.0.0

πŸ’₯ Breaking Changes:

  • nanoid_optimized() third parameter: mask becomes cutoff: 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 be 256 - (256 % length(alphabet)), e.g. 256 instead of 63 for the default 64-symbol alphabet. Old mask values keep running without an error but silently produce wrong ids: with 63 and a 64-symbol alphabet, the last symbol of the alphabet can never appear in a generated id. Every direct nanoid_optimized() call must be updated; see the migration guide below.
  • Old nanoid() signatures are dropped on upgrade: nanoid() gains prefix text DEFAULT '' as its new last parameter, so the upgrade drops the previous nanoid(int, text, float) signature (and the 2022-era nanoid(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 with cannot take logarithm of zero.
  • Prefixed ids (#18): nanoid() takes an optional prefix that is prepended to the generated id for Stripe-style typed ids (usr_, ord_, ...), e.g. nanoid(prefix => 'usr_'). The prefix does not count towards size, so the total id length is length(prefix) + size; a NULL prefix 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 or NULL alphabet, or a NULL or 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.sql now 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.6 now 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