From b2adeb1dc77352a7d047d9522952facdd130aecf Mon Sep 17 00:00:00 2001 From: Nick Wiltshire Date: Sat, 29 Aug 2026 13:58:55 -0600 Subject: [PATCH] Add encodeImmutable for generated columns Table-backed encode stays STABLE because it reads the blocklist. Reject negative and NULL numbers instead of hanging in toId. Co-authored-by: Cursor --- CHANGELOG.md | 8 +- README.md | 34 +++++++- src/install.sql | 90 +++++++++++++++---- src/tests/encoding.test.sql | 24 ++++++ src/tests/immutable.test.sql | 162 +++++++++++++++++++++++++++++++++++ 5 files changed, 300 insertions(+), 18 deletions(-) create mode 100644 src/tests/immutable.test.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 902fdfb..029f0eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,4 +4,10 @@ -Created install.sql with working but probably under tested working encode/decode ## V0.2 --Added spec tests & fixed any failures \ No newline at end of file +-Added spec tests & fixed any failures + +## V0.3 +-Marked pure helpers and decode as IMMUTABLE +-Kept table-backed encode STABLE (it reads sqids.blocklist) +-Added sqids.encodeImmutable for generated columns (compiled-in default blocklist, or a TEXT[] argument) +-Reject negative and NULL numbers instead of hanging in toId diff --git a/README.md b/README.md index efdcedd..c1466dd 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,17 @@ Not good for: > **Note** > ๐Ÿšง The `src/install.sql` file is idempotent but destructive. It will `DROP SCHEMA sqids` so be sure you aren't using a schema with that name! -The blocklist is stored in a table. If you need it to somehow be dynamic per-call, you can likely use transactions, but I have not tested it. +The blocklist is stored in a table. `sqids.encode` reads that table, so Postgres treats it as STABLE. If you need it to somehow be dynamic per-call, you can likely use transactions, but I have not tested it. + +`sqids.encodeImmutable` does not read `sqids.blocklist`, which is why it can be used in generated columns. With three arguments it applies the compiled-in default blocklist. Pass a `TEXT[]` as the fourth argument for a static custom list: + +```sql +select sqids.encodeImmutable(array[1, 2, 3], 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 0, ARRAY['foo']::text[]); +``` + +Table rows only affect `sqids.encode`. `encodeNumbers` with a blocklist argument is the post-shuffle core used by those wrappers โ€” call `encode` / `encodeImmutable` instead of using it in generated columns. + +Numbers must be non-negative. Negative values and NULL elements raise an exception (they used to hang the backend). ### Compatibility @@ -76,6 +86,27 @@ select sqids.encode(array[123, 456, 789], 12); --eVH6til6J03E select sqids.decode('eVH6til6J03E'); -- {123,456,789} ``` +### Generated columns + +Use `encodeImmutable` when the ID should be stored as a generated column. `encode` is not allowed there because it reads `sqids.blocklist`. + +```sql +CREATE TABLE orders ( + id integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + external_id text UNIQUE NOT NULL GENERATED ALWAYS AS ( + sqids.encodeImmutable(ARRAY[id]::BIGINT[], 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 5) + ) STORED +); +``` + +A custom static list is a constant array in the expression (not the table): + +```sql +external_id text GENERATED ALWAYS AS ( + sqids.encodeImmutable(ARRAY[id]::BIGINT[], 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 5, ARRAY['foo']::text[]) +) STORED +``` + ## ๐Ÿงช Testing Run the sql files in tests dir to install. @@ -87,6 +118,7 @@ select sqids.alphabet_test(); select sqids.blocklist_test(); select sqids.encoding_test(); select sqids.minlength_test(); +select sqids.immutable_test(); ``` ## ๐Ÿ“ License diff --git a/src/install.sql b/src/install.sql index c0c00e2..2ab84ed 100644 --- a/src/install.sql +++ b/src/install.sql @@ -31,7 +31,7 @@ BEGIN --raise notice 'Shuffled alphabet: %', array_to_string(chars, ''); RETURN array_to_string(chars, ''); END; -$$ LANGUAGE plpgsql; +$$ LANGUAGE plpgsql IMMUTABLE; CREATE OR REPLACE FUNCTION sqids.checkAlphabet(alphabet TEXT) RETURNS BOOLEAN AS $$ DECLARE @@ -58,14 +58,18 @@ BEGIN RETURN TRUE; END; -$$ LANGUAGE plpgsql; +$$ LANGUAGE plpgsql IMMUTABLE; -CREATE OR REPLACE FUNCTION sqids.isBlockedId(id TEXT) RETURNS BOOLEAN AS $$ +CREATE OR REPLACE FUNCTION sqids.isBlockedId(id TEXT, blocklist TEXT[]) RETURNS BOOLEAN AS $$ DECLARE lowercase_id TEXT := LOWER(id); word TEXT; BEGIN - FOR word IN SELECT str FROM sqids.blocklist LOOP + IF blocklist IS NULL THEN + RETURN FALSE; + END IF; + + FOREACH word IN ARRAY blocklist LOOP word := LOWER(word); IF LENGTH(word) <= LENGTH(lowercase_id) THEN IF LENGTH(lowercase_id) <= 3 OR LENGTH(word) <= 3 THEN @@ -84,7 +88,16 @@ BEGIN RETURN FALSE; END; -$$ LANGUAGE plpgsql; +$$ LANGUAGE plpgsql IMMUTABLE; + +CREATE OR REPLACE FUNCTION sqids.isBlockedId(id TEXT) RETURNS BOOLEAN AS $$ +DECLARE + words TEXT[]; +BEGIN + SELECT COALESCE(array_agg(str), ARRAY[]::TEXT[]) INTO words FROM sqids.blocklist; + RETURN sqids.isBlockedId(id, words); +END; +$$ LANGUAGE plpgsql STABLE; CREATE OR REPLACE FUNCTION sqids.toId(num BIGINT, alphabet TEXT) RETURNS TEXT AS $$ @@ -112,7 +125,7 @@ BEGIN --raise notice 'toId result: %', array_to_string(id, ''); RETURN array_to_string(id, ''); END; -$$ LANGUAGE plpgsql; +$$ LANGUAGE plpgsql IMMUTABLE; CREATE OR REPLACE FUNCTION sqids.toNumber(id TEXT, alphabet TEXT) RETURNS BIGINT AS $$ DECLARE @@ -139,9 +152,11 @@ BEGIN --raise notice 'tonumber result: %', result; RETURN result; END; -$$ LANGUAGE plpgsql; +$$ LANGUAGE plpgsql IMMUTABLE; -CREATE OR REPLACE FUNCTION sqids.encodeNumbers(numbers BIGINT[], alphabet TEXT, minLength INT, increment INT DEFAULT 0) RETURNS TEXT AS $$ +-- Internal: alphabet must already be shuffled (as encode / encodeImmutable do). +-- Prefer those wrappers; do not call this from generated columns. +CREATE OR REPLACE FUNCTION sqids.encodeNumbers(numbers BIGINT[], alphabet TEXT, minLength INT, increment INT, blocklist TEXT[]) RETURNS TEXT AS $$ DECLARE offset_var INT; arr_alphabet TEXT[]; @@ -164,6 +179,12 @@ BEGIN RETURN ''; END IF; + FOR i IN 1..array_length(numbers, 1) LOOP + IF numbers[i] IS NULL OR numbers[i] < 0 THEN + RAISE EXCEPTION 'Sqids: numbers must be non-negative integers, received %', numbers[i]; + END IF; + END LOOP; + arr_alphabet := regexp_split_to_array(alphabet, ''); --raise notice 'arr_alphabet: %', arr_alphabet; @@ -224,14 +245,23 @@ BEGIN END LOOP; END IF; - IF sqids.isBlockedId(id) THEN + IF sqids.isBlockedId(id, blocklist) THEN --raise notice 'Blocked ID: %', id; - id := sqids.encodeNumbers(numbers, original_alphabet, minLength, increment + 1); + id := sqids.encodeNumbers(numbers, original_alphabet, minLength, increment + 1, blocklist); END IF; RETURN id; END; -$$ LANGUAGE plpgsql; +$$ LANGUAGE plpgsql IMMUTABLE; + +CREATE OR REPLACE FUNCTION sqids.encodeNumbers(numbers BIGINT[], alphabet TEXT, minLength INT, increment INT DEFAULT 0) RETURNS TEXT AS $$ +DECLARE + words TEXT[]; +BEGIN + SELECT COALESCE(array_agg(str), ARRAY[]::TEXT[]) INTO words FROM sqids.blocklist; + RETURN sqids.encodeNumbers(numbers, alphabet, minLength, increment, words); +END; +$$ LANGUAGE plpgsql STABLE; CREATE OR REPLACE FUNCTION sqids.decode(id TEXT, alphabet TEXT DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') RETURNS BIGINT[] AS $$ @@ -301,7 +331,7 @@ BEGIN RETURN ret; END; -$$ LANGUAGE plpgsql; +$$ LANGUAGE plpgsql IMMUTABLE; CREATE OR REPLACE FUNCTION sqids.encode(numbers BIGINT[], alphabet TEXT DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', minLength INT DEFAULT 0) RETURNS TEXT AS $$ DECLARE @@ -313,20 +343,48 @@ BEGIN id := sqids.encodeNumbers(numbers, alphabet, minLength); RETURN id; END; -$$ LANGUAGE plpgsql; +$$ LANGUAGE plpgsql STABLE; CREATE OR REPLACE FUNCTION sqids.encode(numbers BIGINT[], minLength INT) RETURNS TEXT AS $$ BEGIN RETURN sqids.encode(numbers, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', minLength); END; -$$ LANGUAGE plpgsql; +$$ LANGUAGE plpgsql STABLE; + +CREATE OR REPLACE FUNCTION sqids.encodeImmutable(numbers BIGINT[], alphabet TEXT, minLength INT, blocklist TEXT[]) RETURNS TEXT AS $$ +DECLARE + id TEXT; +BEGIN + PERFORM sqids.checkAlphabet(alphabet); + alphabet := sqids.shuffle(alphabet); + + id := sqids.encodeNumbers(numbers, alphabet, minLength, 0, blocklist); + RETURN id; +END; +$$ LANGUAGE plpgsql IMMUTABLE; + +CREATE OR REPLACE FUNCTION sqids.encodeImmutable(numbers BIGINT[], alphabet TEXT DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', minLength INT DEFAULT 0) RETURNS TEXT AS $$ +BEGIN + RETURN sqids.encodeImmutable(numbers, alphabet, minLength, sqids.defaultBlocklistWords()); +END; +$$ LANGUAGE plpgsql IMMUTABLE; + +CREATE OR REPLACE FUNCTION sqids.encodeImmutable(numbers BIGINT[], minLength INT) RETURNS TEXT AS $$ +BEGIN + RETURN sqids.encodeImmutable(numbers, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', minLength); +END; +$$ LANGUAGE plpgsql IMMUTABLE; + +CREATE OR REPLACE FUNCTION sqids.defaultBlocklistWords() RETURNS TEXT[] AS $$ + SELECT ARRAY['0rgasm','1d10t','1d1ot','1di0t','1diot','1eccacu10','1eccacu1o','1eccacul0','1eccaculo','1mbec11e','1mbec1le','1mbeci1e','1mbecile','a11upat0','a11upato','a1lupat0','a1lupato','aand','ah01e','ah0le','aho1e','ahole','al1upat0','al1upato','allupat0','allupato','ana1','ana1e','anal','anale','anus','arrapat0','arrapato','arsch','arse','ass','b00b','b00be','b01ata','b0ceta','b0iata','b0ob','b0obe','b0sta','b1tch','b1te','b1tte','ba1atkar','balatkar','bastard0','bastardo','batt0na','battona','bitch','bite','bitte','bo0b','bo0be','bo1ata','boceta','boiata','boob','boobe','bosta','bran1age','bran1er','bran1ette','bran1eur','bran1euse','branlage','branler','branlette','branleur','branleuse','c0ck','c0g110ne','c0g11one','c0g1i0ne','c0g1ione','c0gl10ne','c0gl1one','c0gli0ne','c0glione','c0na','c0nnard','c0nnasse','c0nne','c0u111es','c0u11les','c0u1l1es','c0u1lles','c0ui11es','c0ui1les','c0uil1es','c0uilles','c11t','c11t0','c11to','c1it','c1it0','c1ito','cabr0n','cabra0','cabrao','cabron','caca','cacca','cacete','cagante','cagar','cagare','cagna','cara1h0','cara1ho','caracu10','caracu1o','caracul0','caraculo','caralh0','caralho','cazz0','cazz1mma','cazzata','cazzimma','cazzo','ch00t1a','ch00t1ya','ch00tia','ch00tiya','ch0d','ch0ot1a','ch0ot1ya','ch0otia','ch0otiya','ch1asse','ch1avata','ch1er','ch1ng0','ch1ngadaz0s','ch1ngadazos','ch1ngader1ta','ch1ngaderita','ch1ngar','ch1ngo','ch1ngues','ch1nk','chatte','chiasse','chiavata','chier','ching0','chingadaz0s','chingadazos','chingader1ta','chingaderita','chingar','chingo','chingues','chink','cho0t1a','cho0t1ya','cho0tia','cho0tiya','chod','choot1a','choot1ya','chootia','chootiya','cl1t','cl1t0','cl1to','clit','clit0','clito','cock','cog110ne','cog11one','cog1i0ne','cog1ione','cogl10ne','cogl1one','cogli0ne','coglione','cona','connard','connasse','conne','cou111es','cou11les','cou1l1es','cou1lles','coui11es','coui1les','couil1es','couilles','cracker','crap','cu10','cu1att0ne','cu1attone','cu1er0','cu1ero','cu1o','cul0','culatt0ne','culattone','culer0','culero','culo','cum','cunt','d11d0','d11do','d1ck','d1ld0','d1ldo','damn','de1ch','deich','depp','di1d0','di1do','dick','dild0','dildo','dyke','encu1e','encule','enema','enf01re','enf0ire','enfo1re','enfoire','estup1d0','estup1do','estupid0','estupido','etr0n','etron','f0da','f0der','f0ttere','f0tters1','f0ttersi','f0tze','f0utre','f1ca','f1cker','f1ga','fag','fica','ficker','figa','foda','foder','fottere','fotters1','fottersi','fotze','foutre','fr0c10','fr0c1o','fr0ci0','fr0cio','fr0sc10','fr0sc1o','fr0sci0','fr0scio','froc10','froc1o','froci0','frocio','frosc10','frosc1o','frosci0','froscio','fuck','g00','g0o','g0u1ne','g0uine','gandu','go0','goo','gou1ne','gouine','gr0gnasse','grognasse','haram1','harami','haramzade','hund1n','hundin','id10t','id1ot','idi0t','idiot','imbec11e','imbec1le','imbeci1e','imbecile','j1zz','jerk','jizz','k1ke','kam1ne','kamine','kike','leccacu10','leccacu1o','leccacul0','leccaculo','m1erda','m1gn0tta','m1gnotta','m1nch1a','m1nchia','m1st','mam0n','mamahuev0','mamahuevo','mamon','masturbat10n','masturbat1on','masturbate','masturbati0n','masturbation','merd0s0','merd0so','merda','merde','merdos0','merdoso','mierda','mign0tta','mignotta','minch1a','minchia','mist','musch1','muschi','n1gger','neger','negr0','negre','negro','nerch1a','nerchia','nigger','orgasm','p00p','p011a','p01la','p0l1a','p0lla','p0mp1n0','p0mp1no','p0mpin0','p0mpino','p0op','p0rca','p0rn','p0rra','p0uff1asse','p0uffiasse','p1p1','p1pi','p1r1a','p1rla','p1sc10','p1sc1o','p1sci0','p1scio','p1sser','pa11e','pa1le','pal1e','palle','pane1e1r0','pane1e1ro','pane1eir0','pane1eiro','panele1r0','panele1ro','paneleir0','paneleiro','patakha','pec0r1na','pec0rina','pecor1na','pecorina','pen1s','pendej0','pendejo','penis','pip1','pipi','pir1a','pirla','pisc10','pisc1o','pisci0','piscio','pisser','po0p','po11a','po1la','pol1a','polla','pomp1n0','pomp1no','pompin0','pompino','poop','porca','porn','porra','pouff1asse','pouffiasse','pr1ck','prick','pussy','put1za','puta','puta1n','putain','pute','putiza','puttana','queca','r0mp1ba11e','r0mp1ba1le','r0mp1bal1e','r0mp1balle','r0mpiba11e','r0mpiba1le','r0mpibal1e','r0mpiballe','rand1','randi','rape','recch10ne','recch1one','recchi0ne','recchione','retard','romp1ba11e','romp1ba1le','romp1bal1e','romp1balle','rompiba11e','rompiba1le','rompibal1e','rompiballe','ruff1an0','ruff1ano','ruffian0','ruffiano','s1ut','sa10pe','sa1aud','sa1ope','sacanagem','sal0pe','salaud','salope','saugnapf','sb0rr0ne','sb0rra','sb0rrone','sbattere','sbatters1','sbattersi','sborr0ne','sborra','sborrone','sc0pare','sc0pata','sch1ampe','sche1se','sche1sse','scheise','scheisse','schlampe','schwachs1nn1g','schwachs1nnig','schwachsinn1g','schwachsinnig','schwanz','scopare','scopata','sexy','sh1t','shit','slut','sp0mp1nare','sp0mpinare','spomp1nare','spompinare','str0nz0','str0nza','str0nzo','stronz0','stronza','stronzo','stup1d','stupid','succh1am1','succh1ami','succhiam1','succhiami','sucker','t0pa','tapette','test1c1e','test1cle','testic1e','testicle','tette','topa','tr01a','tr0ia','tr0mbare','tr1ng1er','tr1ngler','tring1er','tringler','tro1a','troia','trombare','turd','twat','vaffancu10','vaffancu1o','vaffancul0','vaffanculo','vag1na','vagina','verdammt','verga','w1chsen','wank','wichsen','x0ch0ta','x0chota','xana','xoch0ta','xochota','z0cc01a','z0cc0la','z0cco1a','z0ccola','z1z1','z1zi','ziz1','zizi','zocc01a','zocc0la','zocco1a','zoccola']::TEXT[]; +$$ LANGUAGE sql IMMUTABLE; CREATE OR REPLACE FUNCTION sqids.defaultBlocklist() RETURNS VOID AS $$ BEGIN --raise notice 'inserting default blocklist'; DELETE FROM sqids.blocklist; - INSERT INTO sqids.blocklist (str) VALUES -('0rgasm'),('1d10t'),('1d1ot'),('1di0t'),('1diot'),('1eccacu10'),('1eccacu1o'),('1eccacul0'),('1eccaculo'),('1mbec11e'),('1mbec1le'),('1mbeci1e'),('1mbecile'),('a11upat0'),('a11upato'),('a1lupat0'),('a1lupato'),('aand'),('ah01e'),('ah0le'),('aho1e'),('ahole'),('al1upat0'),('al1upato'),('allupat0'),('allupato'),('ana1'),('ana1e'),('anal'),('anale'),('anus'),('arrapat0'),('arrapato'),('arsch'),('arse'),('ass'),('b00b'),('b00be'),('b01ata'),('b0ceta'),('b0iata'),('b0ob'),('b0obe'),('b0sta'),('b1tch'),('b1te'),('b1tte'),('ba1atkar'),('balatkar'),('bastard0'),('bastardo'),('batt0na'),('battona'),('bitch'),('bite'),('bitte'),('bo0b'),('bo0be'),('bo1ata'),('boceta'),('boiata'),('boob'),('boobe'),('bosta'),('bran1age'),('bran1er'),('bran1ette'),('bran1eur'),('bran1euse'),('branlage'),('branler'),('branlette'),('branleur'),('branleuse'),('c0ck'),('c0g110ne'),('c0g11one'),('c0g1i0ne'),('c0g1ione'),('c0gl10ne'),('c0gl1one'),('c0gli0ne'),('c0glione'),('c0na'),('c0nnard'),('c0nnasse'),('c0nne'),('c0u111es'),('c0u11les'),('c0u1l1es'),('c0u1lles'),('c0ui11es'),('c0ui1les'),('c0uil1es'),('c0uilles'),('c11t'),('c11t0'),('c11to'),('c1it'),('c1it0'),('c1ito'),('cabr0n'),('cabra0'),('cabrao'),('cabron'),('caca'),('cacca'),('cacete'),('cagante'),('cagar'),('cagare'),('cagna'),('cara1h0'),('cara1ho'),('caracu10'),('caracu1o'),('caracul0'),('caraculo'),('caralh0'),('caralho'),('cazz0'),('cazz1mma'),('cazzata'),('cazzimma'),('cazzo'),('ch00t1a'),('ch00t1ya'),('ch00tia'),('ch00tiya'),('ch0d'),('ch0ot1a'),('ch0ot1ya'),('ch0otia'),('ch0otiya'),('ch1asse'),('ch1avata'),('ch1er'),('ch1ng0'),('ch1ngadaz0s'),('ch1ngadazos'),('ch1ngader1ta'),('ch1ngaderita'),('ch1ngar'),('ch1ngo'),('ch1ngues'),('ch1nk'),('chatte'),('chiasse'),('chiavata'),('chier'),('ching0'),('chingadaz0s'),('chingadazos'),('chingader1ta'),('chingaderita'),('chingar'),('chingo'),('chingues'),('chink'),('cho0t1a'),('cho0t1ya'),('cho0tia'),('cho0tiya'),('chod'),('choot1a'),('choot1ya'),('chootia'),('chootiya'),('cl1t'),('cl1t0'),('cl1to'),('clit'),('clit0'),('clito'),('cock'),('cog110ne'),('cog11one'),('cog1i0ne'),('cog1ione'),('cogl10ne'),('cogl1one'),('cogli0ne'),('coglione'),('cona'),('connard'),('connasse'),('conne'),('cou111es'),('cou11les'),('cou1l1es'),('cou1lles'),('coui11es'),('coui1les'),('couil1es'),('couilles'),('cracker'),('crap'),('cu10'),('cu1att0ne'),('cu1attone'),('cu1er0'),('cu1ero'),('cu1o'),('cul0'),('culatt0ne'),('culattone'),('culer0'),('culero'),('culo'),('cum'),('cunt'),('d11d0'),('d11do'),('d1ck'),('d1ld0'),('d1ldo'),('damn'),('de1ch'),('deich'),('depp'),('di1d0'),('di1do'),('dick'),('dild0'),('dildo'),('dyke'),('encu1e'),('encule'),('enema'),('enf01re'),('enf0ire'),('enfo1re'),('enfoire'),('estup1d0'),('estup1do'),('estupid0'),('estupido'),('etr0n'),('etron'),('f0da'),('f0der'),('f0ttere'),('f0tters1'),('f0ttersi'),('f0tze'),('f0utre'),('f1ca'),('f1cker'),('f1ga'),('fag'),('fica'),('ficker'),('figa'),('foda'),('foder'),('fottere'),('fotters1'),('fottersi'),('fotze'),('foutre'),('fr0c10'),('fr0c1o'),('fr0ci0'),('fr0cio'),('fr0sc10'),('fr0sc1o'),('fr0sci0'),('fr0scio'),('froc10'),('froc1o'),('froci0'),('frocio'),('frosc10'),('frosc1o'),('frosci0'),('froscio'),('fuck'),('g00'),('g0o'),('g0u1ne'),('g0uine'),('gandu'),('go0'),('goo'),('gou1ne'),('gouine'),('gr0gnasse'),('grognasse'),('haram1'),('harami'),('haramzade'),('hund1n'),('hundin'),('id10t'),('id1ot'),('idi0t'),('idiot'),('imbec11e'),('imbec1le'),('imbeci1e'),('imbecile'),('j1zz'),('jerk'),('jizz'),('k1ke'),('kam1ne'),('kamine'),('kike'),('leccacu10'),('leccacu1o'),('leccacul0'),('leccaculo'),('m1erda'),('m1gn0tta'),('m1gnotta'),('m1nch1a'),('m1nchia'),('m1st'),('mam0n'),('mamahuev0'),('mamahuevo'),('mamon'),('masturbat10n'),('masturbat1on'),('masturbate'),('masturbati0n'),('masturbation'),('merd0s0'),('merd0so'),('merda'),('merde'),('merdos0'),('merdoso'),('mierda'),('mign0tta'),('mignotta'),('minch1a'),('minchia'),('mist'),('musch1'),('muschi'),('n1gger'),('neger'),('negr0'),('negre'),('negro'),('nerch1a'),('nerchia'),('nigger'),('orgasm'),('p00p'),('p011a'),('p01la'),('p0l1a'),('p0lla'),('p0mp1n0'),('p0mp1no'),('p0mpin0'),('p0mpino'),('p0op'),('p0rca'),('p0rn'),('p0rra'),('p0uff1asse'),('p0uffiasse'),('p1p1'),('p1pi'),('p1r1a'),('p1rla'),('p1sc10'),('p1sc1o'),('p1sci0'),('p1scio'),('p1sser'),('pa11e'),('pa1le'),('pal1e'),('palle'),('pane1e1r0'),('pane1e1ro'),('pane1eir0'),('pane1eiro'),('panele1r0'),('panele1ro'),('paneleir0'),('paneleiro'),('patakha'),('pec0r1na'),('pec0rina'),('pecor1na'),('pecorina'),('pen1s'),('pendej0'),('pendejo'),('penis'),('pip1'),('pipi'),('pir1a'),('pirla'),('pisc10'),('pisc1o'),('pisci0'),('piscio'),('pisser'),('po0p'),('po11a'),('po1la'),('pol1a'),('polla'),('pomp1n0'),('pomp1no'),('pompin0'),('pompino'),('poop'),('porca'),('porn'),('porra'),('pouff1asse'),('pouffiasse'),('pr1ck'),('prick'),('pussy'),('put1za'),('puta'),('puta1n'),('putain'),('pute'),('putiza'),('puttana'),('queca'),('r0mp1ba11e'),('r0mp1ba1le'),('r0mp1bal1e'),('r0mp1balle'),('r0mpiba11e'),('r0mpiba1le'),('r0mpibal1e'),('r0mpiballe'),('rand1'),('randi'),('rape'),('recch10ne'),('recch1one'),('recchi0ne'),('recchione'),('retard'),('romp1ba11e'),('romp1ba1le'),('romp1bal1e'),('romp1balle'),('rompiba11e'),('rompiba1le'),('rompibal1e'),('rompiballe'),('ruff1an0'),('ruff1ano'),('ruffian0'),('ruffiano'),('s1ut'),('sa10pe'),('sa1aud'),('sa1ope'),('sacanagem'),('sal0pe'),('salaud'),('salope'),('saugnapf'),('sb0rr0ne'),('sb0rra'),('sb0rrone'),('sbattere'),('sbatters1'),('sbattersi'),('sborr0ne'),('sborra'),('sborrone'),('sc0pare'),('sc0pata'),('sch1ampe'),('sche1se'),('sche1sse'),('scheise'),('scheisse'),('schlampe'),('schwachs1nn1g'),('schwachs1nnig'),('schwachsinn1g'),('schwachsinnig'),('schwanz'),('scopare'),('scopata'),('sexy'),('sh1t'),('shit'),('slut'),('sp0mp1nare'),('sp0mpinare'),('spomp1nare'),('spompinare'),('str0nz0'),('str0nza'),('str0nzo'),('stronz0'),('stronza'),('stronzo'),('stup1d'),('stupid'),('succh1am1'),('succh1ami'),('succhiam1'),('succhiami'),('sucker'),('t0pa'),('tapette'),('test1c1e'),('test1cle'),('testic1e'),('testicle'),('tette'),('topa'),('tr01a'),('tr0ia'),('tr0mbare'),('tr1ng1er'),('tr1ngler'),('tring1er'),('tringler'),('tro1a'),('troia'),('trombare'),('turd'),('twat'),('vaffancu10'),('vaffancu1o'),('vaffancul0'),('vaffanculo'),('vag1na'),('vagina'),('verdammt'),('verga'),('w1chsen'),('wank'),('wichsen'),('x0ch0ta'),('x0chota'),('xana'),('xoch0ta'),('xochota'),('z0cc01a'),('z0cc0la'),('z0cco1a'),('z0ccola'),('z1z1'),('z1zi'),('ziz1'),('zizi'),('zocc01a'),('zocc0la'),('zocco1a'),('zoccola'); + INSERT INTO sqids.blocklist (str) + SELECT unnest(sqids.defaultBlocklistWords()); END; $$ LANGUAGE plpgsql; diff --git a/src/tests/encoding.test.sql b/src/tests/encoding.test.sql index 4a728f5..ca10306 100644 --- a/src/tests/encoding.test.sql +++ b/src/tests/encoding.test.sql @@ -143,5 +143,29 @@ BEGIN RAISE NOTICE ' FAILED'; END IF; + RAISE NOTICE 'negative numbers raise'; + BEGIN + PERFORM sqids.encode(array[1, 2, 4, -1]); + RAISE NOTICE ' FAILED'; + EXCEPTION WHEN others THEN + IF SQLERRM LIKE 'Sqids: numbers must be non-negative%' THEN + RAISE NOTICE ' PASSED'; + ELSE + RAISE NOTICE ' FAILED; %', SQLERRM; + END IF; + END; + + RAISE NOTICE 'NULL numbers raise'; + BEGIN + PERFORM sqids.encode(ARRAY[1, NULL]::BIGINT[]); + RAISE NOTICE ' FAILED'; + EXCEPTION WHEN others THEN + IF SQLERRM LIKE 'Sqids: numbers must be non-negative%' THEN + RAISE NOTICE ' PASSED'; + ELSE + RAISE NOTICE ' FAILED; %', SQLERRM; + END IF; + END; + END $$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/src/tests/immutable.test.sql b/src/tests/immutable.test.sql new file mode 100644 index 0000000..12d2c1e --- /dev/null +++ b/src/tests/immutable.test.sql @@ -0,0 +1,162 @@ +CREATE OR REPLACE FUNCTION sqids.assert_volatility(fn_name TEXT, identity_args TEXT, expected CHAR) RETURNS BOOLEAN AS $$ +DECLARE + actual CHAR; +BEGIN + SELECT p.provolatile INTO actual + FROM pg_proc p + JOIN pg_namespace n ON n.oid = p.pronamespace + WHERE n.nspname = 'sqids' + AND p.proname = fn_name + AND pg_get_function_identity_arguments(p.oid) = identity_args; + + IF actual IS NULL THEN + RAISE NOTICE ' FAILED; missing sqids.%(%)', fn_name, identity_args; + RETURN FALSE; + END IF; + + IF actual IS DISTINCT FROM expected THEN + RAISE NOTICE ' FAILED; %(%) is % expected %', fn_name, identity_args, actual, expected; + RETURN FALSE; + END IF; + + RETURN TRUE; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION sqids.immutable_test() RETURNS VOID AS $$ +DECLARE + got TEXT; + ext TEXT; + ok BOOLEAN := TRUE; +BEGIN + RAISE NOTICE 'sqids.immutable_test'; + PERFORM sqids.defaultBlocklist(); + + RAISE NOTICE 'function volatility'; + ok := sqids.assert_volatility('shuffle', 'alphabet text', 'i') + AND sqids.assert_volatility('checkalphabet', 'alphabet text', 'i') + AND sqids.assert_volatility('toid', 'num bigint, alphabet text', 'i') + AND sqids.assert_volatility('tonumber', 'id text, alphabet text', 'i') + AND sqids.assert_volatility('decode', 'id text, alphabet text', 'i') + AND sqids.assert_volatility('defaultblocklistwords', '', 'i') + AND sqids.assert_volatility('isblockedid', 'id text, blocklist text[]', 'i') + AND sqids.assert_volatility('encodenumbers', 'numbers bigint[], alphabet text, minlength integer, increment integer, blocklist text[]', 'i') + AND sqids.assert_volatility('encodeimmutable', 'numbers bigint[], alphabet text, minlength integer, blocklist text[]', 'i') + AND sqids.assert_volatility('encodeimmutable', 'numbers bigint[], alphabet text, minlength integer', 'i') + AND sqids.assert_volatility('encodeimmutable', 'numbers bigint[], minlength integer', 'i') + AND sqids.assert_volatility('isblockedid', 'id text', 's') + AND sqids.assert_volatility('encodenumbers', 'numbers bigint[], alphabet text, minlength integer, increment integer', 's') + AND sqids.assert_volatility('encode', 'numbers bigint[], alphabet text, minlength integer', 's') + AND sqids.assert_volatility('encode', 'numbers bigint[], minlength integer', 's') + AND sqids.assert_volatility('defaultblocklist', '', 'v'); + IF ok THEN + RAISE NOTICE ' PASSED'; + END IF; + + RAISE NOTICE 'encodeImmutable matches encode with the default blocklist'; + IF sqids.encodeImmutable(array[1, 2, 3]) = sqids.encode(array[1, 2, 3]) + AND sqids.encodeImmutable(array[4572721]) = sqids.encode(array[4572721]) THEN + RAISE NOTICE ' PASSED'; + ELSE + RAISE NOTICE ' FAILED'; + END IF; + + RAISE NOTICE 'encodeImmutable matches encode with a custom alphabet'; + IF sqids.encodeImmutable(array[123, 456, 789], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 12) + = sqids.encode(array[123, 456, 789], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 12) THEN + RAISE NOTICE ' PASSED'; + ELSE + RAISE NOTICE ' FAILED'; + END IF; + + RAISE NOTICE 'encodeImmutable ignores table blocklist changes'; + DELETE FROM sqids.blocklist; + IF sqids.encode(array[4572721]) = 'aho1e' + AND sqids.encodeImmutable(array[4572721]) = 'JExTR' THEN + RAISE NOTICE ' PASSED'; + ELSE + RAISE NOTICE ' FAILED; encode=% encodeImmutable=%', sqids.encode(array[4572721]), sqids.encodeImmutable(array[4572721]); + END IF; + PERFORM sqids.defaultBlocklist(); + + RAISE NOTICE 'encodeImmutable ignores extra table blocklist rows'; + INSERT INTO sqids.blocklist (str) VALUES ('86Rf07'); + IF sqids.encode(array[1, 2, 3]) <> '86Rf07' + AND sqids.encodeImmutable(array[1, 2, 3]) = '86Rf07' THEN + RAISE NOTICE ' PASSED'; + ELSE + RAISE NOTICE ' FAILED; encode=% encodeImmutable=%', sqids.encode(array[1, 2, 3]), sqids.encodeImmutable(array[1, 2, 3]); + END IF; + PERFORM sqids.defaultBlocklist(); + + RAISE NOTICE 'encodeImmutable with an explicit blocklist'; + got := sqids.encodeImmutable(array[1, 2, 3], 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 0, ARRAY['86Rf07']::TEXT[]); + IF got <> '86Rf07' AND sqids.decode(got) = array[1, 2, 3]::BIGINT[] THEN + RAISE NOTICE ' PASSED'; + ELSE + RAISE NOTICE ' FAILED; got=%', got; + END IF; + + RAISE NOTICE 'encodeImmutable is allowed in a generated column'; + DROP TABLE IF EXISTS sqids.immutable_gen_test; + CREATE TABLE sqids.immutable_gen_test ( + id integer PRIMARY KEY, + external_id text GENERATED ALWAYS AS ( + sqids.encodeImmutable(ARRAY[id]::BIGINT[], 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 5) + ) STORED, + short_id text GENERATED ALWAYS AS ( + sqids.encodeImmutable(ARRAY[id]::BIGINT[]) + ) STORED + ); + INSERT INTO sqids.immutable_gen_test(id) VALUES (1); + SELECT external_id INTO ext FROM sqids.immutable_gen_test WHERE id = 1; + got := sqids.encodeImmutable(ARRAY[1]::BIGINT[], 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 5); + IF ext = got AND LENGTH(ext) >= 5 THEN + RAISE NOTICE ' PASSED'; + ELSE + RAISE NOTICE ' FAILED; generated=% expected=%', ext, got; + END IF; + DROP TABLE sqids.immutable_gen_test; + + RAISE NOTICE 'encodeImmutable with a literal blocklist is allowed in a generated column'; + DROP TABLE IF EXISTS sqids.immutable_gen_custom_test; + CREATE TABLE sqids.immutable_gen_custom_test ( + id integer PRIMARY KEY, + external_id text GENERATED ALWAYS AS ( + sqids.encodeImmutable(ARRAY[id]::BIGINT[], 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 0, ARRAY['Uk']::TEXT[]) + ) STORED + ); + INSERT INTO sqids.immutable_gen_custom_test(id) VALUES (1); + SELECT external_id INTO ext FROM sqids.immutable_gen_custom_test WHERE id = 1; + IF ext <> 'Uk' AND sqids.decode(ext) = ARRAY[1]::BIGINT[] THEN + RAISE NOTICE ' PASSED'; + ELSE + RAISE NOTICE ' FAILED; generated=%', ext; + END IF; + DROP TABLE sqids.immutable_gen_custom_test; + + RAISE NOTICE 'table-backed encode is rejected in a generated column'; + BEGIN + CREATE TABLE sqids.volatile_gen_test ( + id integer PRIMARY KEY, + external_id text GENERATED ALWAYS AS (sqids.encode(ARRAY[id]::BIGINT[])) STORED + ); + RAISE NOTICE ' FAILED'; + DROP TABLE IF EXISTS sqids.volatile_gen_test; + EXCEPTION WHEN invalid_object_definition THEN + RAISE NOTICE ' PASSED'; + END; + + RAISE NOTICE 'encodeImmutable rejects negative numbers'; + BEGIN + PERFORM sqids.encodeImmutable(array[-1]); + RAISE NOTICE ' FAILED'; + EXCEPTION WHEN others THEN + IF SQLERRM LIKE 'Sqids: numbers must be non-negative%' THEN + RAISE NOTICE ' PASSED'; + ELSE + RAISE NOTICE ' FAILED; %', SQLERRM; + END IF; + END; +END +$$ LANGUAGE plpgsql;