├── CHANGELOG.md ├── LICENSE ├── src ├── tests │ ├── alphabet.test.sql │ ├── minlength.test.sql │ ├── encoding.test.sql │ └── blocklist.test.sql └── install.sql └── README.md /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## V0.1 4 | -Created install.sql with working but probably under tested working encode/decode 5 | 6 | ## V0.2 7 | -Added spec tests & fixed any failures -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present Sqids maintainers. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/tests/alphabet.test.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION sqids.alphabet_test() RETURNS VOID AS $$ 2 | BEGIN 3 | RAISE NOTICE 'sqids.alphabet_test'; 4 | RAISE NOTICE 'simple encode'; 5 | IF sqids.encode(array[1, 2, 3], '0123456789abcdef') = '489158' THEN 6 | RAISE NOTICE ' PASSED'; 7 | ELSE 8 | RAISE NOTICE ' FAILED'; 9 | END IF; 10 | 11 | RAISE NOTICE 'simple decode'; 12 | IF sqids.decode('489158', '0123456789abcdef') = array[1, 2, 3]::BIGINT[] THEN 13 | RAISE NOTICE ' PASSED'; 14 | ELSE 15 | RAISE NOTICE ' FAILED'; 16 | END IF; 17 | 18 | RAISE NOTICE 'short alphabet encode/decode'; 19 | IF sqids.decode(sqids.encode(array[1, 2, 3], 'abc'), 'abc') = array[1, 2, 3]::BIGINT[] THEN 20 | RAISE NOTICE ' PASSED'; 21 | ELSE 22 | RAISE NOTICE ' FAILED'; 23 | END IF; 24 | 25 | RAISE NOTICE 'long alphabet encode/decode'; 26 | IF sqids.decode(sqids.encode(array[1, 2, 3], 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_+|{}[];:\''"/?.>,<`~'), 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_+|{}[];:\''"/?.>,<`~') = array[1, 2, 3]::BIGINT[] THEN 27 | RAISE NOTICE ' PASSED'; 28 | ELSE 29 | RAISE NOTICE ' FAILED'; 30 | END IF; 31 | 32 | RAISE NOTICE 'test multibyte characters'; 33 | BEGIN 34 | PERFORM sqids.checkAlphabet('ë1092'); 35 | RAISE NOTICE ' FAILED'; 36 | EXCEPTION WHEN OTHERS THEN 37 | RAISE NOTICE ' PASSED'; 38 | END; 39 | 40 | RAISE NOTICE 'repeating alphabet characters'; 41 | BEGIN 42 | PERFORM sqids.checkAlphabet('aabcdefg'); 43 | RAISE NOTICE ' FAILED'; 44 | EXCEPTION WHEN OTHERS THEN 45 | RAISE NOTICE ' PASSED'; 46 | END; 47 | 48 | RAISE NOTICE 'too short alphabet'; 49 | BEGIN 50 | PERFORM sqids.checkAlphabet('ab'); 51 | RAISE NOTICE ' FAILED'; 52 | EXCEPTION WHEN OTHERS THEN 53 | RAISE NOTICE ' PASSED'; 54 | END; 55 | END 56 | $$ LANGUAGE plpgsql; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Sqids PLpgSQL](https://sqids.org/plpgsql) 2 | 3 | [Sqids](https://sqids.org/plpgsql) (pronounced "squids") is a small library that lets you **generate unique IDs from numbers**. It's good for link shortening, fast & URL-safe ID generation and decoding back into numbers for quicker database lookups. 4 | 5 | Features: 6 | 7 | - **Encode multiple numbers** - generate short IDs from one or several non-negative numbers 8 | - **Quick decoding** - easily decode IDs back into numbers 9 | - **Unique IDs** - generate unique IDs by shuffling the alphabet once 10 | - **ID padding** - provide minimum length to make IDs more uniform 11 | - **URL safe** - auto-generated IDs do not contain common profanity 12 | - **Randomized output** - Sequential input provides nonconsecutive IDs 13 | - **Many implementations** - Support for [40+ programming languages](https://sqids.org/) 14 | 15 | ## 🧰 Use-cases 16 | 17 | Good for: 18 | 19 | - Generating IDs for public URLs (eg: link shortening) 20 | - Generating IDs for internal systems (eg: event tracking) 21 | - Decoding for quicker database lookups (eg: by primary keys) 22 | 23 | Not good for: 24 | 25 | - Sensitive data (this is not an encryption library) 26 | - User IDs (can be decoded revealing user count) 27 | 28 | ## 🚀 Getting started 29 | 30 | ### Important notes 31 | 32 | > **Note** 33 | > 🚧 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! 34 | 35 | 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. 36 | 37 | ### Compatibility 38 | 39 | Written & tested on Postgres 15.6. The functions used are pretty simple - it will likely work on 9+ (definitely not earlier). Be sure to install & run tests! 40 | 41 | ### Installation 42 | 43 | Simply run `src/install.sql` on your database. 44 | 45 | ## 👩‍💻 Examples 46 | 47 | After install, use encode & decode: 48 | 49 | encode takes an array of BIGINT, an alphabet, and an optional minLength. 50 | 51 | ```sql 52 | select sqids.encode(array[123, 456, 789], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 12); -- EBDQWDLPCTHG 53 | ``` 54 | 55 | decode requires the id and alphabet. It returns an array of BIGINT. 56 | 57 | ```sql 58 | select sqids.decode('EBDQWDLPCTHG', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'); -- {123,456,789} 59 | ``` 60 | 61 | ### With default alphabet & min length (0) 62 | 63 | ```sql 64 | select sqids.encode(array[123, 456, 789]); --eVH6til6J 65 | ``` 66 | 67 | ### With default alphabet & custom min length 68 | 69 | ```sql 70 | select sqids.encode(array[123, 456, 789], 12); --eVH6til6J03E 71 | ``` 72 | 73 | ### Decode with default alphabet 74 | 75 | ```sql 76 | select sqids.decode('eVH6til6J03E'); -- {123,456,789} 77 | ``` 78 | 79 | ## 🧪 Testing 80 | 81 | Run the sql files in tests dir to install. 82 | 83 | Then run: 84 | 85 | ```sql 86 | select sqids.alphabet_test(); 87 | select sqids.blocklist_test(); 88 | select sqids.encoding_test(); 89 | select sqids.minlength_test(); 90 | ``` 91 | 92 | ## 📝 License 93 | 94 | [MIT](LICENSE) -------------------------------------------------------------------------------- /src/tests/minlength.test.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION sqids.minlength_test() RETURNS void AS $$ 2 | DECLARE 3 | default_alphabet TEXT := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 4 | numbers BIGINT[]; 5 | id TEXT; 6 | map JSON; 7 | id_pair RECORD; 8 | rec_json json; 9 | i INT; 10 | BEGIN 11 | RAISE NOTICE 'tsids.minlength_test'; 12 | RAISE NOTICE 'simple'; 13 | numbers := array[1, 2, 3]; 14 | id := '86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTM'; 15 | IF sqids.encode(numbers, default_alphabet, LENGTH(default_alphabet)) = id THEN 16 | RAISE NOTICE ' PASSED'; 17 | ELSE 18 | RAISE NOTICE ' FAILED'; 19 | END IF; 20 | 21 | RAISE NOTICE 'incremental'; 22 | map := '{"6": "86Rf07", "7": "86Rf07x", "8": "86Rf07xd", "9": "86Rf07xd4", "10": "86Rf07xd4z", "11": "86Rf07xd4zB", "12": "86Rf07xd4zBm", "13": "86Rf07xd4zBmi", "62": "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTM", "63": "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMy", "64": "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMyf", "65": "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMyf1"}'::json; 23 | FOR id_pair IN SELECT * FROM json_each_text(map) LOOP 24 | i := id_pair.key::INT; 25 | id := id_pair.value; 26 | 27 | IF sqids.encode(array[1, 2, 3], default_alphabet, i) = id THEN 28 | RAISE NOTICE ' PASSED'; 29 | ELSE 30 | RAISE NOTICE ' FAILED'; 31 | END IF; 32 | END LOOP; 33 | 34 | RAISE NOTICE 'incremental numbers'; 35 | map := '{"SvIzsqYMyQwI3GWgJAe17URxX8V924Co0DaTZLtFjHriEn5bPhcSkfmvOslpBu": [0, 0], 36 | "n3qafPOLKdfHpuNw3M61r95svbeJGk7aAEgYn4WlSjXURmF8IDqZBy0CT2VxQc": [0, 1], 37 | "tryFJbWcFMiYPg8sASm51uIV93GXTnvRzyfLleh06CpodJD42B7OraKtkQNxUZ": [0, 2], 38 | "eg6ql0A3XmvPoCzMlB6DraNGcWSIy5VR8iYup2Qk4tjZFKe1hbwfgHdUTsnLqE": [0, 3], 39 | "rSCFlp0rB2inEljaRdxKt7FkIbODSf8wYgTsZM1HL9JzN35cyoqueUvVWCm4hX": [0, 4], 40 | "sR8xjC8WQkOwo74PnglH1YFdTI0eaf56RGVSitzbjuZ3shNUXBrqLxEJyAmKv2": [0, 5], 41 | "uY2MYFqCLpgx5XQcjdtZK286AwWV7IBGEfuS9yTmbJvkzoUPeYRHr4iDs3naN0": [0, 6], 42 | "74dID7X28VLQhBlnGmjZrec5wTA1fqpWtK4YkaoEIM9SRNiC3gUJH0OFvsPDdy": [0, 7], 43 | "30WXpesPhgKiEI5RHTY7xbB1GnytJvXOl2p0AcUjdF6waZDo9Qk8VLzMuWrqCS": [0, 8], 44 | "moxr3HqLAK0GsTND6jowfZz3SUx7cQ8aC54Pl1RbIvFXmEJuBMYVeW9yrdOtin": [0, 9]}'::json; 45 | FOR id_pair IN SELECT * FROM json_each(map) LOOP 46 | id := id_pair.key; 47 | numbers := ARRAY(SELECT value::integer FROM json_array_elements_text(id_pair.value) AS value); 48 | raise notice 'decode %', id; 49 | IF sqids.decode(id, default_alphabet) = numbers THEN 50 | RAISE NOTICE ' PASSED'; 51 | ELSE 52 | RAISE NOTICE ' FAILED'; 53 | END IF; 54 | 55 | raise notice 'encode %', numbers; 56 | IF sqids.encode(numbers, default_alphabet, LENGTH(default_alphabet)) = id THEN 57 | RAISE NOTICE ' PASSED'; 58 | ELSE 59 | RAISE NOTICE ' FAILED'; 60 | END IF; 61 | END LOOP; 62 | 63 | RAISE NOTICE 'min lengths'; 64 | map := '[[0], [0, 0, 0, 0, 0], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [100, 200, 300], [1000, 2000, 3000], [1000000], [9007199254740991]]'::json; 65 | FOREACH i IN ARRAY ARRAY[0, 1, 5, 10, LENGTH(default_alphabet)] LOOP 66 | FOR rec_json IN SELECT * FROM json_array_elements_text(map) LOOP 67 | numbers := ARRAY(SELECT value::BIGINT FROM json_array_elements_text(rec_json) AS value); 68 | 69 | RAISE NOTICE 'minLength: %, numbers: %', i, numbers; 70 | RAISE NOTICE 'expect.soft(sqids.encode(%)).toBe(%);', numbers, id; 71 | IF LENGTH(sqids.encode(numbers, default_alphabet, i)) >= i THEN 72 | RAISE NOTICE ' PASSED'; 73 | ELSE 74 | RAISE NOTICE ' FAILED'; 75 | END IF; 76 | 77 | IF sqids.decode(sqids.encode(numbers, default_alphabet, i), default_alphabet) = numbers THEN 78 | RAISE NOTICE ' PASSED'; 79 | ELSE 80 | RAISE NOTICE ' FAILED'; 81 | END IF; 82 | END LOOP; 83 | 84 | END LOOP; 85 | 86 | END 87 | $$ LANGUAGE plpgsql; -------------------------------------------------------------------------------- /src/tests/encoding.test.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION sqids.encoding_test() RETURNS VOID AS $$ 2 | DECLARE 3 | id_pairs json; 4 | id_pair record; 5 | id TEXT; 6 | numbers BIGINT[]; 7 | BEGIN 8 | RAISE NOTICE 'sqids.encoding_test'; 9 | RAISE NOTICE 'simple'; 10 | numbers := array[1, 2, 3]; 11 | id := '86Rf07'; 12 | 13 | RAISE NOTICE 'expect.soft(sqids.encode(%)).toBe(%);', numbers, id; 14 | IF sqids.encode(numbers) = id THEN 15 | RAISE NOTICE ' PASSED'; 16 | ELSE 17 | RAISE NOTICE ' FAILED'; 18 | END IF; 19 | 20 | RAISE NOTICE 'expect.soft(sqids.decode(%)).toEqual(%);', id, numbers; 21 | IF sqids.decode(id) = numbers THEN 22 | RAISE NOTICE ' PASSED'; 23 | ELSE 24 | RAISE NOTICE ' FAILED'; 25 | END IF; 26 | 27 | RAISE NOTICE 'different inputs'; 28 | numbers := array[0, 0, 0, 1, 2, 3, 100, 1000, 100000, 1000000, 9007199254740991]; 29 | 30 | RAISE NOTICE 'expect.soft(sqids.decode(sqids.encode(%))).toEqual(%);', numbers, numbers; 31 | IF sqids.decode(sqids.encode(numbers)) = numbers THEN 32 | RAISE NOTICE ' PASSED'; 33 | ELSE 34 | RAISE NOTICE ' FAILED'; 35 | END IF; 36 | 37 | RAISE NOTICE 'incremental numbers'; 38 | id_pairs := '{"bM": [0], "Uk": [1], "gb": [2], "Ef": [3], "Vq": [4], "uw": [5], "OI": [6], "AX": [7], "p6": [8], "nJ": [9]}'::json; 39 | 40 | FOR id_pair IN SELECT * FROM json_each(id_pairs) LOOP 41 | id := id_pair.key; 42 | numbers := ARRAY(SELECT value::integer FROM json_array_elements_text(id_pair.value) AS value); 43 | 44 | RAISE NOTICE 'Key: %, Value: %', id, numbers; 45 | RAISE NOTICE 'expect.soft(sqids.encode(%)).toBe(%);', numbers, id; 46 | IF sqids.encode(numbers) = id THEN 47 | RAISE NOTICE ' PASSED'; 48 | ELSE 49 | RAISE NOTICE ' FAILED'; 50 | END IF; 51 | 52 | RAISE NOTICE 'expect.soft(sqids.decode(%)).toEqual(%);', id, numbers; 53 | IF sqids.decode(id) = numbers THEN 54 | RAISE NOTICE ' PASSED'; 55 | ELSE 56 | RAISE NOTICE ' FAILED'; 57 | END IF; 58 | END LOOP; 59 | 60 | RAISE NOTICE 'incremental numbers, same index 0'; 61 | id_pairs := '{"SvIz": [0, 0], "n3qa": [0, 1], "tryF": [0, 2], "eg6q": [0, 3], "rSCF": [0, 4], "sR8x": [0, 5], "uY2M": [0, 6], "74dI": [0, 7], "30WX": [0, 8], "moxr": [0, 9]}'::json; 62 | 63 | FOR id_pair IN SELECT * FROM json_each(id_pairs) LOOP 64 | id := id_pair.key; 65 | numbers := ARRAY(SELECT value::integer FROM json_array_elements_text(id_pair.value) AS value); 66 | 67 | RAISE NOTICE 'Key: %, Value: %', id, numbers; 68 | RAISE NOTICE 'expect.soft(sqids.encode(%)).toBe(%);', numbers, id; 69 | IF sqids.encode(numbers) = id THEN 70 | RAISE NOTICE ' PASSED'; 71 | ELSE 72 | RAISE NOTICE ' FAILED'; 73 | END IF; 74 | 75 | RAISE NOTICE 'expect.soft(sqids.decode(%)).toEqual(%);', id, numbers; 76 | IF sqids.decode(id) = numbers THEN 77 | RAISE NOTICE ' PASSED'; 78 | ELSE 79 | RAISE NOTICE ' FAILED'; 80 | END IF; 81 | END LOOP; 82 | 83 | RAISE NOTICE 'incremental numbers, same index 1'; 84 | id_pairs := '{"SvIz": [0, 0], "nWqP": [1, 0], "tSyw": [2, 0], "eX68": [3, 0], "rxCY": [4, 0], "sV8a": [5, 0], "uf2K": [6, 0], "7Cdk": [7, 0], "3aWP": [8, 0], "m2xn": [9, 0]}'::json; 85 | 86 | FOR id_pair IN SELECT * FROM json_each(id_pairs) LOOP 87 | id := id_pair.key; 88 | numbers := ARRAY(SELECT value::integer FROM json_array_elements_text(id_pair.value) AS value); 89 | 90 | RAISE NOTICE 'Key: %, Value: %', id, numbers; 91 | RAISE NOTICE 'expect.soft(sqids.encode(%)).toBe(%);', numbers, id; 92 | IF sqids.encode(numbers) = id THEN 93 | RAISE NOTICE ' PASSED'; 94 | ELSE 95 | RAISE NOTICE ' FAILED'; 96 | END IF; 97 | 98 | RAISE NOTICE 'expect.soft(sqids.decode(%)).toEqual(%);', id, numbers; 99 | IF sqids.decode(id) = numbers THEN 100 | RAISE NOTICE ' PASSED'; 101 | ELSE 102 | RAISE NOTICE ' FAILED'; 103 | END IF; 104 | END LOOP; 105 | 106 | RAISE NOTICE 'multi input'; 107 | numbers := array[ 108 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 109 | 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 110 | 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 111 | 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 112 | 98, 99 113 | ]; 114 | 115 | RAISE NOTICE 'expect.soft(sqids.decode(sqids.encode(%))).toEqual(%);', numbers, numbers; 116 | IF sqids.decode(sqids.encode(numbers)) = numbers THEN 117 | RAISE NOTICE ' PASSED'; 118 | ELSE 119 | RAISE NOTICE ' FAILED'; 120 | END IF; 121 | 122 | RAISE NOTICE 'encoding no numbers'; 123 | RAISE NOTICE 'expect.soft(sqids.encode([])).toBe('''');'; 124 | IF sqids.encode(ARRAY[]::BIGINT[]) = '' THEN 125 | RAISE NOTICE ' PASSED'; 126 | ELSE 127 | RAISE NOTICE ' FAILED'; 128 | END IF; 129 | 130 | RAISE NOTICE 'decoding empty string'; 131 | RAISE NOTICE 'expect.soft(sqids.decode('')).toEqual([]);'; 132 | IF sqids.decode('') = ARRAY[]::BIGINT[] THEN 133 | RAISE NOTICE ' PASSED'; 134 | ELSE 135 | RAISE NOTICE ' FAILED'; 136 | END IF; 137 | 138 | RAISE NOTICE 'decoding an ID with an invalid character'; 139 | RAISE NOTICE 'expect.soft(sqids.decode(''*'')).toBe([]);'; 140 | IF sqids.decode('*') = ARRAY[]::BIGINT[] THEN 141 | RAISE NOTICE ' PASSED'; 142 | ELSE 143 | RAISE NOTICE ' FAILED'; 144 | END IF; 145 | 146 | END 147 | $$ LANGUAGE plpgsql; -------------------------------------------------------------------------------- /src/tests/blocklist.test.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION sqids.blocklist_test() RETURNS VOID AS $$ 2 | DECLARE 3 | i RECORD; 4 | BEGIN 5 | RAISE NOTICE 'sqids.blocklist_test'; 6 | RAISE NOTICE 'use default blocklist'; 7 | PERFORM sqids.defaultBlocklist(); 8 | 9 | RAISE NOTICE 'expect.soft(sqids.decode(''aho1e'')).toEqual([4572721]);'; 10 | IF sqids.decode('aho1e') = array[4572721]::BIGINT[] THEN 11 | RAISE NOTICE ' PASSED'; 12 | ELSE 13 | RAISE NOTICE ' FAILED'; 14 | END IF; 15 | 16 | RAISE NOTICE 'expect.soft(sqids.encode([4572721])).toBe(''JExTR'');'; 17 | IF sqids.encode(array[4572721]) = 'JExTR' THEN 18 | RAISE NOTICE ' PASSED'; 19 | ELSE 20 | RAISE NOTICE ' FAILED; got %', sqids.encode(array[4572721]); 21 | END IF; 22 | 23 | RAISE NOTICE 'if blocklist is empty, don''t use any blocklist'; 24 | DELETE FROM sqids.blocklist; 25 | 26 | RAISE NOTICE 'expect.soft(sqids.decode(''aho1e'')).toEqual([4572721]);'; 27 | IF sqids.decode('aho1e') = array[4572721]::BIGINT[] THEN 28 | RAISE NOTICE ' PASSED'; 29 | ELSE 30 | RAISE NOTICE ' FAILED'; 31 | END IF; 32 | 33 | RAISE NOTICE 'expect.soft(sqids.encode([4572721])).toBe(''aho1e'');'; 34 | IF sqids.encode(array[4572721]) = 'aho1e' THEN 35 | RAISE NOTICE ' PASSED'; 36 | ELSE 37 | RAISE NOTICE ' FAILED'; 38 | END IF; 39 | 40 | RAISE NOTICE 'if a non-empty blocklist param passed, use only that'; 41 | DELETE FROM sqids.blocklist; 42 | INSERT INTO sqids.blocklist (str) VALUES ('ArUO'); 43 | 44 | RAISE NOTICE 'expect.soft(sqids.decode(''aho1e'')).toEqual([4572721]);'; 45 | IF sqids.decode('aho1e') = array[4572721]::BIGINT[] THEN 46 | RAISE NOTICE ' PASSED'; 47 | ELSE 48 | RAISE NOTICE ' FAILED'; 49 | END IF; 50 | 51 | RAISE NOTICE 'expect.soft(sqids.encode([4572721])).toBe(''aho1e'');'; 52 | IF sqids.encode(array[4572721]) = 'aho1e' THEN 53 | RAISE NOTICE ' PASSED'; 54 | ELSE 55 | RAISE NOTICE ' FAILED'; 56 | END IF; 57 | 58 | RAISE NOTICE 'expect.soft(sqids.decode(''ArUO'')).toEqual([100000]);'; 59 | IF sqids.decode('ArUO') = array[100000]::BIGINT[] THEN 60 | RAISE NOTICE ' PASSED'; 61 | ELSE 62 | RAISE NOTICE ' FAILED'; 63 | END IF; 64 | 65 | RAISE NOTICE 'expect.soft(sqids.encode([100000])).toBe(''QyG4'');'; 66 | IF sqids.encode(array[100000]) = 'QyG4' THEN 67 | RAISE NOTICE ' PASSED'; 68 | ELSE 69 | RAISE NOTICE ' FAILED; got %', sqids.encode(array[100000]); 70 | END IF; 71 | 72 | RAISE NOTICE 'expect.soft(sqids.decode(''QyG4'')).toEqual([100000]);'; 73 | IF sqids.decode('QyG4') = array[100000]::BIGINT[] THEN 74 | RAISE NOTICE ' PASSED'; 75 | ELSE 76 | RAISE NOTICE ' FAILED'; 77 | END IF; 78 | 79 | RAISE NOTICE 'custom blocklist'; 80 | DELETE FROM sqids.blocklist; 81 | INSERT INTO sqids.blocklist (str) VALUES 82 | ('JSwXFaosAN'), --normal result of 1st encoding, let's block that word on purpose 83 | ('OCjV9JK64o'), --result of 2nd encoding 84 | ('rBHf'), --result of 3rd encoding is `4rBHfOiqd3`, let's block a substring 85 | ('79SM'), --result of 4th encoding is `dyhgw479SM`, let's block the postfix 86 | ('7tE6'); --result of 4th encoding is `7tE6jdAHLe`, let's block the prefix 87 | 88 | RAISE NOTICE 'expect.soft(sqids.encode([1000000, 2000000])).toBe(''1aYeB7bRUt'');'; 89 | IF sqids.encode(array[1000000, 2000000]) = '1aYeB7bRUt' THEN 90 | RAISE NOTICE ' PASSED'; 91 | ELSE 92 | RAISE NOTICE ' FAILED'; 93 | END IF; 94 | 95 | RAISE NOTICE 'expect.soft(sqids.decode(''1aYeB7bRUt'')).toEqual([1_000_000, 2_000_000]);'; 96 | IF sqids.decode('1aYeB7bRUt') = array[1000000, 2000000]::BIGINT[] THEN 97 | RAISE NOTICE ' PASSED'; 98 | ELSE 99 | RAISE NOTICE ' FAILED'; 100 | END IF; 101 | 102 | RAISE NOTICE 'decoding blocklist words should still work'; 103 | DELETE FROM sqids.blocklist; 104 | INSERT INTO sqids.blocklist (str) VALUES 105 | ('86Rf07'), 106 | ('se8ojk'), 107 | ('ARsz1p'), 108 | ('Q8AI49'), 109 | ('5sQRZO'); 110 | 111 | RAISE NOTICE 'expect.soft(sqids.decode(''86Rf07'')).toEqual([1, 2, 3]);'; 112 | IF sqids.decode('86Rf07') = array[1, 2, 3]::BIGINT[] THEN 113 | RAISE NOTICE ' PASSED'; 114 | ELSE 115 | RAISE NOTICE ' FAILED'; 116 | END IF; 117 | 118 | RAISE NOTICE 'expect.soft(sqids.decode(''se8ojk'')).toEqual([1, 2, 3]);'; 119 | IF sqids.decode('se8ojk') = array[1, 2, 3]::BIGINT[] THEN 120 | RAISE NOTICE ' PASSED'; 121 | ELSE 122 | RAISE NOTICE ' FAILED'; 123 | END IF; 124 | 125 | RAISE NOTICE 'expect.soft(sqids.decode(''ARsz1p'')).toEqual([1, 2, 3]);'; 126 | IF sqids.decode('ARsz1p') = array[1, 2, 3]::BIGINT[] THEN 127 | RAISE NOTICE ' PASSED'; 128 | ELSE 129 | RAISE NOTICE ' FAILED'; 130 | END IF; 131 | 132 | RAISE NOTICE 'expect.soft(sqids.decode(''Q8AI49'')).toEqual([1, 2, 3]);'; 133 | IF sqids.decode('Q8AI49') = array[1, 2, 3]::BIGINT[] THEN 134 | RAISE NOTICE ' PASSED'; 135 | ELSE 136 | RAISE NOTICE ' FAILED'; 137 | END IF; 138 | 139 | RAISE NOTICE 'expect.soft(sqids.decode(''5sQRZO'')).toEqual([1, 2, 3]);'; 140 | IF sqids.decode('5sQRZO') = array[1, 2, 3]::BIGINT[] THEN 141 | RAISE NOTICE ' PASSED'; 142 | ELSE 143 | RAISE NOTICE ' FAILED'; 144 | END IF; 145 | 146 | RAISE NOTICE 'match against a short blocklist word'; 147 | DELETE FROM sqids.blocklist; 148 | INSERT INTO sqids.blocklist (str) VALUES ('pnd'); 149 | 150 | RAISE NOTICE 'expect.soft(sqids.decode(sqids.encode([1000]))).toEqual([1000]);'; 151 | IF sqids.decode(sqids.encode(array[1000])) = array[1000]::BIGINT[] THEN 152 | RAISE NOTICE ' PASSED'; 153 | ELSE 154 | RAISE NOTICE ' FAILED'; 155 | END IF; 156 | 157 | RAISE NOTICE 'blocklist filtering in constructor'; 158 | DELETE FROM sqids.blocklist; 159 | INSERT INTO sqids.blocklist (str) VALUES ('sxnzkl'); --lowercase blocklist in only-uppercase alphabet 160 | 161 | RAISE NOTICE 'expect.soft(id).toEqual(''IBSHOZ''); // without blocklist, would''ve been "SXNZKL"'; 162 | IF sqids.encode(array[1, 2, 3], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') = 'IBSHOZ' THEN 163 | RAISE NOTICE ' PASSED'; 164 | ELSE 165 | RAISE NOTICE ' FAILED'; 166 | END IF; 167 | 168 | RAISE NOTICE 'expect.soft(numbers).toEqual([1, 2, 3]);'; 169 | IF sqids.decode('IBSHOZ', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') = array[1, 2, 3]::BIGINT[] THEN 170 | RAISE NOTICE ' PASSED'; 171 | ELSE 172 | RAISE NOTICE ' FAILED'; 173 | END IF; 174 | 175 | RAISE NOTICE 'max encoding attempts'; 176 | DELETE FROM sqids.blocklist; 177 | INSERT INTO sqids.blocklist (str) VALUES ('cab'), ('abc'), ('bca'); 178 | BEGIN 179 | PERFORM sqids.encode(array[0], 'abc', 3); 180 | RAISE NOTICE ' FAILED'; 181 | EXCEPTION WHEN OTHERS THEN 182 | RAISE NOTICE ' PASSED'; 183 | END; 184 | 185 | END 186 | $$ LANGUAGE plpgsql; -------------------------------------------------------------------------------- /src/install.sql: -------------------------------------------------------------------------------- 1 | DROP SCHEMA IF EXISTS sqids CASCADE; 2 | CREATE SCHEMA sqids; 3 | 4 | CREATE TABLE sqids.blocklist( 5 | str TEXT PRIMARY KEY 6 | ); 7 | 8 | CREATE OR REPLACE FUNCTION sqids.shuffle(alphabet TEXT) RETURNS TEXT AS $$ 9 | DECLARE 10 | chars TEXT[]; 11 | i BIGINT; 12 | j BIGINT; 13 | r BIGINT; 14 | temp TEXT; 15 | BEGIN 16 | chars := regexp_split_to_array(alphabet, ''); 17 | i := 0; 18 | j := array_length(chars, 1) - 1; 19 | 20 | WHILE j > 0 LOOP 21 | r := (i * j + ascii(chars[i + 1]) + ascii(chars[j + 1])) % array_length(chars, 1); 22 | 23 | temp := chars[i + 1]; 24 | chars[i + 1] := chars[r + 1]; 25 | chars[r + 1] := temp; 26 | 27 | i := i + 1; 28 | j := j - 1; 29 | END LOOP; 30 | 31 | --raise notice 'Shuffled alphabet: %', array_to_string(chars, ''); 32 | RETURN array_to_string(chars, ''); 33 | END; 34 | $$ LANGUAGE plpgsql; 35 | 36 | CREATE OR REPLACE FUNCTION sqids.checkAlphabet(alphabet TEXT) RETURNS BOOLEAN AS $$ 37 | DECLARE 38 | chars CHAR[]; 39 | unique_chars CHAR[]; 40 | BEGIN 41 | IF LENGTH(alphabet) < 3 THEN 42 | RAISE EXCEPTION 'Alphabet must have at least 3 characters'; 43 | END IF; 44 | 45 | -- IF POSITION(' ' IN alphabet) > 0 THEN 46 | -- RAISE EXCEPTION 'Alphabet must not contain spaces'; 47 | -- END IF; 48 | 49 | IF octet_length(alphabet) <> length(alphabet) THEN 50 | RAISE EXCEPTION 'Alphabet must not contain multibyte characters'; 51 | END IF; 52 | 53 | chars := regexp_split_to_array(alphabet, ''); 54 | unique_chars := ARRAY(SELECT DISTINCT unnest(chars)); 55 | IF array_length(chars, 1) <> array_length(unique_chars, 1) THEN 56 | RAISE EXCEPTION 'Alphabet must not contain duplicate characters'; 57 | END IF; 58 | 59 | RETURN TRUE; 60 | END; 61 | $$ LANGUAGE plpgsql; 62 | 63 | CREATE OR REPLACE FUNCTION sqids.isBlockedId(id TEXT) RETURNS BOOLEAN AS $$ 64 | DECLARE 65 | lowercase_id TEXT := LOWER(id); 66 | word TEXT; 67 | BEGIN 68 | FOR word IN SELECT str FROM sqids.blocklist LOOP 69 | word := LOWER(word); 70 | IF LENGTH(word) <= LENGTH(lowercase_id) THEN 71 | IF LENGTH(lowercase_id) <= 3 OR LENGTH(word) <= 3 THEN 72 | IF lowercase_id = word THEN 73 | RETURN TRUE; 74 | END IF; 75 | ELSIF POSITION('\d' IN word) > 0 THEN 76 | IF lowercase_id LIKE word || '%' OR lowercase_id LIKE '%' || word THEN 77 | RETURN TRUE; 78 | END IF; 79 | ELSIF POSITION(word IN lowercase_id) > 0 THEN 80 | RETURN TRUE; 81 | END IF; 82 | END IF; 83 | END LOOP; 84 | 85 | RETURN FALSE; 86 | END; 87 | $$ LANGUAGE plpgsql; 88 | 89 | 90 | CREATE OR REPLACE FUNCTION sqids.toId(num BIGINT, alphabet TEXT) RETURNS TEXT AS $$ 91 | DECLARE 92 | id TEXT[]; 93 | chars TEXT[]; 94 | result BIGINT; 95 | BEGIN 96 | id := ARRAY[]::TEXT[]; 97 | chars := regexp_split_to_array(alphabet, ''); 98 | result := num; 99 | 100 | --raise notice 'num passed to toId: %', num; 101 | --raise notice 'alphabet passed to toId: %', alphabet; 102 | 103 | LOOP 104 | id := array_prepend(chars[(result % array_length(chars, 1)) + 1], id); 105 | result := (result / array_length(chars, 1))::BIGINT; 106 | 107 | IF result = 0 THEN 108 | EXIT; 109 | END IF; 110 | END LOOP; 111 | 112 | --raise notice 'toId result: %', array_to_string(id, ''); 113 | RETURN array_to_string(id, ''); 114 | END; 115 | $$ LANGUAGE plpgsql; 116 | 117 | CREATE OR REPLACE FUNCTION sqids.toNumber(id TEXT, alphabet TEXT) RETURNS BIGINT AS $$ 118 | DECLARE 119 | chars TEXT[]; 120 | result BIGINT := 0; 121 | char TEXT; 122 | a BIGINT; 123 | v BIGINT; 124 | BEGIN 125 | --raise notice 'id: %', id; 126 | --raise notice 'alphabet: %', alphabet; 127 | chars := regexp_split_to_array(alphabet, ''); 128 | 129 | 130 | FOR i IN 1..length(id) LOOP 131 | char := substring(id from i for 1); 132 | --raise notice 'char: %', char; 133 | --raise notice 'result: %', result; 134 | --raise notice 'array_length(chars, 1): %', array_length(chars, 1); 135 | --raise notice 'array_position(chars, char): %', array_position(chars, char); 136 | result := result * array_length(chars, 1) + (array_position(chars, char) - 1); 137 | END LOOP; 138 | 139 | --raise notice 'tonumber result: %', result; 140 | RETURN result; 141 | END; 142 | $$ LANGUAGE plpgsql; 143 | 144 | CREATE OR REPLACE FUNCTION sqids.encodeNumbers(numbers BIGINT[], alphabet TEXT, minLength INT, increment INT DEFAULT 0) RETURNS TEXT AS $$ 145 | DECLARE 146 | offset_var INT; 147 | arr_alphabet TEXT[]; 148 | original_alphabet TEXT := alphabet; 149 | prefix TEXT; 150 | ret TEXT[]; 151 | a BIGINT; 152 | v BIGINT; 153 | i BIGINT; 154 | num BIGINT; 155 | id TEXT; 156 | m BIGINT; 157 | char INT; 158 | BEGIN 159 | IF increment > LENGTH(alphabet) THEN 160 | RAISE EXCEPTION 'Reached max attempts to re-generate the ID'; 161 | END IF; 162 | 163 | IF array_length(numbers, 1) is null THEN 164 | RETURN ''; 165 | END IF; 166 | 167 | arr_alphabet := regexp_split_to_array(alphabet, ''); 168 | 169 | --raise notice 'arr_alphabet: %', arr_alphabet; 170 | a := array_length(numbers, 1); 171 | --raise notice 'Offset start: %', a; 172 | FOR i IN 0..(array_length(numbers, 1) - 1) LOOP 173 | v := numbers[i + 1]; 174 | --raise notice 'avi: % % %', a, v, i; 175 | m := v % array_length(arr_alphabet, 1); 176 | --raise notice 'm: %', m; 177 | char := ascii(arr_alphabet[m + 1]); 178 | --raise notice 'char: %', char; 179 | a := char + i + a; 180 | --raise notice 'a: %', a; 181 | END LOOP; 182 | offset_var := a % array_length(arr_alphabet, 1); 183 | 184 | --raise notice 'Offset 1: %', offset_var; 185 | 186 | 187 | 188 | offset_var := (offset_var + increment) % array_length(arr_alphabet, 1); 189 | --raise notice 'Offset 2: %', offset_var; 190 | 191 | arr_alphabet := arr_alphabet[offset_var + 1:] || arr_alphabet[1:offset_var]; 192 | --raise notice 'Sliced Alphabet: %', arr_alphabet; 193 | prefix := arr_alphabet[1]; 194 | alphabet := array_to_string(arr_alphabet, ''); 195 | alphabet := reverse(alphabet); 196 | arr_alphabet := regexp_split_to_array(alphabet, ''); 197 | ret := ARRAY[prefix]; 198 | 199 | --raise notice 'Alphabet: %', alphabet; 200 | --raise notice 'Prefix: %', prefix; 201 | --raise notice 'Ret: %', ret; 202 | 203 | FOR i IN 1..array_length(numbers, 1) LOOP 204 | num := numbers[i]; 205 | --raise notice 'num: %', num; 206 | ret := array_append(ret, sqids.toId(num, substring(alphabet FROM 2 FOR LENGTH(alphabet) - 1))); 207 | 208 | --raise notice 'ret In Loop: %', ret; 209 | 210 | IF i < array_length(numbers, 1) THEN 211 | ret := array_append(ret, substring(alphabet FROM 1 FOR 1)); 212 | alphabet := sqids.shuffle(alphabet); 213 | END IF; 214 | END LOOP; 215 | 216 | id := array_to_string(ret, ''); 217 | 218 | IF LENGTH(id) < minLength THEN 219 | id := id || substring(alphabet FROM 1 FOR 1); 220 | 221 | WHILE minLength - LENGTH(id) > 0 LOOP 222 | alphabet := sqids.shuffle(alphabet); 223 | id := id || substring(alphabet FROM 1 FOR LEAST(minLength - LENGTH(id), LENGTH(alphabet))); 224 | END LOOP; 225 | END IF; 226 | 227 | IF sqids.isBlockedId(id) THEN 228 | --raise notice 'Blocked ID: %', id; 229 | id := sqids.encodeNumbers(numbers, original_alphabet, minLength, increment + 1); 230 | END IF; 231 | 232 | RETURN id; 233 | END; 234 | $$ LANGUAGE plpgsql; 235 | 236 | 237 | CREATE OR REPLACE FUNCTION sqids.decode(id TEXT, alphabet TEXT DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') RETURNS BIGINT[] AS $$ 238 | DECLARE 239 | ret BIGINT[]; 240 | prefix TEXT; 241 | offset_var INT; 242 | i BIGINT; 243 | c TEXT; 244 | separator TEXT; 245 | chunks TEXT[]; 246 | slicedId TEXT; 247 | num BIGINT; 248 | BEGIN 249 | IF id = '' THEN 250 | ret := array[]::BIGINT[]; 251 | RETURN ret; 252 | END IF; 253 | 254 | PERFORM sqids.checkAlphabet(alphabet); 255 | 256 | FOR i IN 1..LENGTH(id) LOOP 257 | c := substring(id FROM i FOR 1); 258 | IF POSITION(c IN alphabet) = 0 THEN 259 | ret := array[]::BIGINT[]; 260 | RETURN ret; 261 | END IF; 262 | END LOOP; 263 | 264 | alphabet := sqids.shuffle(alphabet); 265 | 266 | prefix := substring(id FROM 1 FOR 1); 267 | offset_var := POSITION(prefix IN alphabet) - 1; 268 | 269 | --raise notice 'prefix: %', prefix; 270 | --raise notice 'offset_var: %', offset_var; 271 | 272 | alphabet := substring(alphabet FROM offset_var + 1 FOR LENGTH(alphabet) - offset_var) || substring(alphabet FROM 1 FOR offset_var); 273 | 274 | alphabet := reverse(alphabet); 275 | --raise notice 'Alphabet: %', alphabet; 276 | slicedId := substring(id FROM 2); 277 | 278 | --raise notice 'Sliced ID: %', slicedId; 279 | 280 | WHILE LENGTH(slicedId) > 0 LOOP 281 | separator := substring(alphabet FROM 1 FOR 1); 282 | --raise notice 'Separator: %', separator; 283 | chunks := string_to_array(slicedId, separator); 284 | --raise notice 'Chunks: %', chunks; 285 | 286 | IF array_length(chunks, 1) > 0 THEN 287 | IF chunks[1] = '' THEN 288 | RETURN ret; 289 | END IF; 290 | 291 | num := sqids.toNumber(chunks[1], substring(alphabet FROM 2 FOR LENGTH(alphabet) - 1)); 292 | ret := array_append(ret, num); 293 | 294 | IF array_length(chunks, 1) > 1 THEN 295 | alphabet := sqids.shuffle(alphabet); 296 | END IF; 297 | END IF; 298 | 299 | slicedId := array_to_string(chunks[2:], separator); 300 | END LOOP; 301 | 302 | RETURN ret; 303 | END; 304 | $$ LANGUAGE plpgsql; 305 | 306 | CREATE OR REPLACE FUNCTION sqids.encode(numbers BIGINT[], alphabet TEXT DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', minLength INT DEFAULT 0) RETURNS TEXT AS $$ 307 | DECLARE 308 | id TEXT; 309 | BEGIN 310 | PERFORM sqids.checkAlphabet(alphabet); 311 | alphabet := sqids.shuffle(alphabet); 312 | 313 | id := sqids.encodeNumbers(numbers, alphabet, minLength); 314 | RETURN id; 315 | END; 316 | $$ LANGUAGE plpgsql; 317 | 318 | CREATE OR REPLACE FUNCTION sqids.encode(numbers BIGINT[], minLength INT) RETURNS TEXT AS $$ 319 | BEGIN 320 | RETURN sqids.encode(numbers, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', minLength); 321 | END; 322 | $$ LANGUAGE plpgsql; 323 | 324 | CREATE OR REPLACE FUNCTION sqids.defaultBlocklist() RETURNS VOID AS $$ 325 | BEGIN 326 | --raise notice 'inserting default blocklist'; 327 | DELETE FROM sqids.blocklist; 328 | INSERT INTO sqids.blocklist (str) VALUES 329 | ('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'); 330 | END; 331 | $$ LANGUAGE plpgsql; 332 | 333 | SELECT sqids.defaultBlocklist(); --------------------------------------------------------------------------------