├── LICENSE ├── README.md └── src ├── 0_create_hashids_schema.sql ├── 1_create_hashids_constent_shuffle.sql ├── 2_create_hashids_setup_seps.sql ├── 3_create_hashids_setup_alphabet.sql ├── 4_create_hashids_hash.sql ├── 5_create_hashids_unhash.sql ├── 6_create_hashids_encode_list.sql ├── 7_create_hashids_encode.sql ├── 8_create_hashids_decode.sql └── all_hashids.sql /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Array Analytics 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # plpg_hashids 2 | A small set of TSQL functions to generate YouTube-like hashes from one or many numbers. 3 | Use hashids when you do not want to expose your database ids to the user. 4 | 5 | [http://www.hashids.org/](http://www.hashids.org/) 6 | 7 | This repository contains a port to plpgsql of the other projects found at hashids.org. 8 | The [TSQL](https://github.com/waynebloss/hashids-tsql), [Postgres](https://github.com/iCyberon/pg_hashids), [Javascript](https://github.com/ivanakimov/hashids.js) and [.NET](https://github.com/ullmark/hashids.net) versions of Hashids are the primary reference projects for this port, with the TSQL version being the initial version. 9 | 10 | Tested PostgreSQL versions : 9.6.X (Should work on older and newer versions, just not tested) 11 | 12 | **It was done using plpgsql becuase, Azure and AWS don't allow creating your own extensions when running on their platform, and pg_hashids isn't a supported extension.** 13 | 14 | ## What is it? 15 | 16 | hashids (Hash ID's) creates short, unique, decryptable hashes from unsigned integers. 17 | 18 | _(NOTE: This is **NOT** a true cryptographic hash, since it is reversible.)_ 19 | 20 | It was designed for websites to use in URL shortening, tracking stuff, or making pages private (or at least unguessable). 21 | 22 | This algorithm tries to satisfy the following requirements: 23 | 24 | 1. Hashes must be unique and decryptable. 25 | 2. They should be able to contain more than one integer (so you can use them in complex or clustered systems). 26 | 3. You should be able to specify minimum hash length. 27 | 4. Hashes should not contain basic English curse words (since they are meant to appear in public places - like the URL). 28 | 29 | Instead of showing items as `1`, `2`, or `3`, you could show them as `U6dc`, `u87U`, and `HMou`. 30 | You can choose to store these hashes in the database or encrypt + decrypt on the fly. 31 | 32 | All integers need to be greater than or equal to zero. 33 | 34 | See [hashids.org](http://www.hashids.org/) for more information on this technique. 35 | 36 | ## Usage 37 | 38 | Run the scripts in the order that they are in, in the `src` folder. Please note, that they are in their own schema (`hashids`), if you don't want that you will have to edit the scripts to remove the assumption of the `hashids` schema. 39 | 40 | #### Encoding 41 | Returns a hash using the default `alphabet` and empty `salt`. 42 | 43 | SELECT hashids.encode(1001); -- Result: jNl 44 | 45 | Returns a hash using the default `alphabet` and supplied `salt`. 46 | 47 | SELECT hashids.encode(1234567, 'This is my salt'); -- Result: Pdzxp 48 | 49 | Returns a hash using the default `alphabet`, `salt` and minimum hash length. 50 | 51 | SELECT hashids.encode(1234567, 'This is my salt', 10); -- Result: PlRPdzxpR7 52 | 53 | Returns a hash using the supplied `alphabet`, `salt` and minimum hash length. 54 | 55 | SELECT hashids.encode(1234567, 'This is my salt', 10, 'abcdefghijABCDxFGHIJ1234567890'); -- Result: 3GJ956J9B9 56 | 57 | Returns a hash for an array of numbers. 58 | 59 | SELECT hashids.encode_list(ARRAY[1,2,3]); -- Result: o2fXhV 60 | 61 | #### Decoding 62 | You can also decode previously generated hashes. Just use the same `salt`, otherwise you'll get wrong results. 63 | 64 | SELECT unnest(hashids.decode('PlRPdzxpR7', 'This is my salt', 10)); -- Result: 1234567 65 | 66 | Using a custom alphabet 67 | 68 | SELECT unnest(hashids.decode('3GJ956J9B9', 'This is my salt', 10, 'abcdefghijABCDxFGHIJ1234567890')); -- Result: 1234567 69 | 70 | # Note 71 | The code and scripts are provided as is. Array Analytics isn't responsible if anything bad happens. 72 | -------------------------------------------------------------------------------- /src/0_create_hashids_schema.sql: -------------------------------------------------------------------------------- 1 | -- our usage of these functions places all of them in a separate schema, with "hashids" being the default name 2 | -- change this or don't use it (but know that the other sql functions will not work out of the box, they are assuming the hashids schema). 3 | CREATE SCHEMA hashids; 4 | -------------------------------------------------------------------------------- /src/1_create_hashids_constent_shuffle.sql: -------------------------------------------------------------------------------- 1 | drop function if exists hashids.consistent_shuffle(text, text); 2 | 3 | CREATE OR REPLACE FUNCTION hashids.consistent_shuffle 4 | ( 5 | p_alphabet text, 6 | p_salt text 7 | ) 8 | RETURNS text AS 9 | $$ 10 | DECLARE p_alphabet ALIAS FOR $1; 11 | p_salt ALIAS FOR $2; 12 | v_ls int; 13 | v_i int; 14 | v_v int := 0; 15 | v_p int := 0; 16 | v_n int := 0; 17 | v_j int := 0; 18 | v_temp char(1); 19 | BEGIN 20 | 21 | -- Null or Whitespace? 22 | IF p_salt IS NULL OR length(LTRIM(RTRIM(p_salt))) = 0 THEN 23 | RETURN p_alphabet; 24 | END IF; 25 | 26 | v_ls := length(p_salt); 27 | v_i := length(p_alphabet) - 1; 28 | 29 | WHILE v_i > 0 LOOP 30 | 31 | v_v := v_v % v_ls; 32 | v_n := ascii(SUBSTRING(p_salt, v_v + 1, 1)); -- need some investigation to see if +1 here is because of 1 based arrays in sql ... this isn't in the reference JS or .net code. 33 | v_p := v_p + v_n; 34 | v_j := (v_n + v_v + v_p) % v_i; 35 | v_temp := SUBSTRING(p_alphabet, v_j + 1, 1); 36 | p_alphabet := 37 | SUBSTRING(p_alphabet, 1, v_j) || 38 | SUBSTRING(p_alphabet, v_i + 1, 1) || 39 | SUBSTRING(p_alphabet, v_j + 2, 255); 40 | p_alphabet := SUBSTRING(p_alphabet, 1, v_i) || v_temp || SUBSTRING(p_alphabet, v_i + 2, 255); 41 | v_i := v_i - 1; 42 | v_v := v_v + 1; 43 | 44 | END LOOP; -- WHILE 45 | 46 | RETURN p_alphabet; 47 | 48 | END; 49 | $$ 50 | LANGUAGE plpgsql IMMUTABLE 51 | COST 200; -------------------------------------------------------------------------------- /src/2_create_hashids_setup_seps.sql: -------------------------------------------------------------------------------- 1 | 2 | CREATE OR REPLACE FUNCTION hashids.clean_seps_from_alphabet( 3 | in p_seps text, 4 | in p_alphabet text 5 | ) 6 | RETURNS text AS 7 | $$ 8 | DECLARE 9 | p_seps ALIAS for $1; 10 | p_alphabet ALIAS for $2; 11 | v_split_seps text[] := regexp_split_to_array(p_seps, ''); 12 | v_split_alphabet text[] := regexp_split_to_array(p_alphabet, ''); 13 | v_i integer := 1; 14 | v_length integer := length(p_seps); 15 | v_ret text := ''; 16 | BEGIN 17 | -- had to add this function because doing this: 18 | -- p_seps := array_to_string(ARRAY(select chars.cha from (select unnest(regexp_split_to_array(p_seps, '')) as cha intersect select unnest(regexp_split_to_array(p_alphabet, '')) as cha ) as chars order by ascii(cha) desc), ''); 19 | -- doesn't preserve the order of the input 20 | 21 | for v_i in 1..v_length loop 22 | -- raise notice 'v_split_seps[%]: % == %', v_i, v_split_seps[v_i], v_split_seps[v_i] = any (v_split_alphabet); 23 | if (v_split_seps[v_i] = any (v_split_alphabet)) then 24 | v_ret = v_ret || v_split_seps[v_i]; 25 | end if; 26 | end loop; 27 | 28 | -- raise notice 'v_ret: %', v_ret; 29 | return v_ret; 30 | END; 31 | $$ 32 | LANGUAGE plpgsql IMMUTABLE 33 | COST 200; 34 | 35 | CREATE OR REPLACE FUNCTION hashids.clean_alphabet_from_seps( 36 | in p_seps text, 37 | in p_alphabet text 38 | ) 39 | RETURNS text AS 40 | $$ 41 | DECLARE 42 | p_seps ALIAS for $1; 43 | p_alphabet ALIAS for $2; 44 | v_split_seps text[] := regexp_split_to_array(p_seps, ''); 45 | v_split_alphabet text[] := regexp_split_to_array(p_alphabet, ''); 46 | v_i integer := 1; 47 | v_length integer := length(p_alphabet); 48 | v_ret text := ''; 49 | BEGIN 50 | -- had to add this function because doing this: 51 | -- p_alphabet := array_to_string(ARRAY( select chars.cha from (select unnest(regexp_split_to_array(p_alphabet, '')) as cha EXCEPT select unnest(regexp_split_to_array(p_seps, '')) as cha) as chars ), ''); 52 | -- doesn't preserve the order of the input 53 | 54 | for v_i in 1..v_length loop 55 | --raise notice 'v_split_alphabet[%]: % != %', v_i, v_split_alphabet[v_i], v_split_alphabet[v_i] <> all (v_split_seps); 56 | if (v_split_alphabet[v_i] <> all (v_split_seps)) then 57 | v_ret = v_ret || v_split_alphabet[v_i]; 58 | end if; 59 | end loop; 60 | 61 | -- raise notice 'v_ret: %', v_ret; 62 | return v_ret; 63 | END; 64 | $$ 65 | LANGUAGE plpgsql IMMUTABLE 66 | COST 200; 67 | 68 | CREATE OR REPLACE FUNCTION hashids.distinct_alphabet(in p_alphabet text) 69 | RETURNS text AS 70 | $$ 71 | DECLARE 72 | p_alphabet ALIAS for $1; 73 | v_split_alphabet text[] := regexp_split_to_array(p_alphabet, ''); 74 | v_i integer := 2; 75 | v_length integer := length(p_alphabet); 76 | v_ret_array text[]; 77 | BEGIN 78 | -- had to add this function because doing this: 79 | -- p_alphabet := string_agg(distinct chars.split_chars, '') from (select unnest(regexp_split_to_array(p_alphabet, '')) as split_chars) as chars; 80 | -- doesn't preserve the order of the input, which was causing issues 81 | if (v_length = 0) then 82 | RAISE EXCEPTION 'alphabet must contain at least 1 char' USING HINT = 'Please check your alphabet'; 83 | end if; 84 | v_ret_array := array_append(v_ret_array, v_split_alphabet[1]); 85 | 86 | -- starting at 2 because already appended 1 to it. 87 | for v_i in 2..v_length loop 88 | -- raise notice 'v_split_alphabet[%]: % != %', v_i, v_split_alphabet[v_i], v_split_alphabet[v_i] <> all (v_ret_array); 89 | 90 | if (v_split_alphabet[v_i] <> all (v_ret_array)) then 91 | v_ret_array := array_append(v_ret_array, v_split_alphabet[v_i]); 92 | end if; 93 | end loop; 94 | 95 | -- raise notice 'v_ret_array: %', array_to_string(v_ret_array, ''); 96 | return array_to_string(v_ret_array, ''); 97 | END; 98 | $$ 99 | LANGUAGE plpgsql IMMUTABLE 100 | COST 200; 101 | -------------------------------------------------------------------------------- /src/3_create_hashids_setup_alphabet.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION hashids.setup_alphabet( 2 | in p_salt text default '', 3 | inout p_alphabet text default 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890', 4 | out p_seps text, 5 | out p_guards text) 6 | AS 7 | $$ 8 | DECLARE 9 | p_salt ALIAS for $1; 10 | p_alphabet ALIAS for $2; 11 | p_seps ALIAS for $3; 12 | p_guards ALIAS for $4; 13 | v_sep_div float := 3.5; 14 | v_guard_div float := 12.0; 15 | v_guard_count integer; 16 | v_seps_length integer; 17 | v_seps_diff integer; 18 | BEGIN 19 | p_seps := 'cfhistuCFHISTU'; 20 | -- p_alphabet := string_agg(distinct chars.split_chars, '') from (select unnest(regexp_split_to_array(p_alphabet, '')) as split_chars) as chars; 21 | -- this also doesn't preserve the order of alphabet, but it doesn't appear to matter, never mind on that 22 | p_alphabet := hashids.distinct_alphabet(p_alphabet); 23 | 24 | 25 | if length(p_alphabet) < 16 then 26 | RAISE EXCEPTION 'alphabet must contain 16 unique characters, it is: %', length(p_alphabet) USING HINT = 'Please check your alphabet'; 27 | end if; 28 | 29 | -- seps should only contain character present in the passed alphabet 30 | -- p_seps := array_to_string(ARRAY(select chars.cha from (select unnest(regexp_split_to_array(p_seps, '')) as cha intersect select unnest(regexp_split_to_array(p_alphabet, '')) as cha ) as chars order by ascii(cha) desc), ''); 31 | -- this doesn't preserve the input order, which is bad 32 | p_seps := hashids.clean_seps_from_alphabet(p_seps, p_alphabet); 33 | 34 | -- alphabet should not contain seps. 35 | -- p_alphabet := array_to_string(ARRAY( select chars.cha from (select unnest(regexp_split_to_array(p_alphabet, '')) as cha EXCEPT select unnest(regexp_split_to_array(p_seps, '')) as cha) as chars ), ''); 36 | -- this also doesn't prevserve the order 37 | p_alphabet := hashids.clean_alphabet_from_seps(p_seps, p_alphabet); 38 | 39 | 40 | p_seps := hashids.consistent_shuffle(p_seps, p_salt); 41 | 42 | if (length(p_seps) = 0) or ((length(p_alphabet) / length(p_seps)) > v_sep_div) then 43 | v_seps_length := cast( ceil( length(p_alphabet)/v_sep_div ) as integer); 44 | if v_seps_length = 1 then 45 | v_seps_length := 2; 46 | end if; 47 | if v_seps_length > length(p_seps) then 48 | v_seps_diff := v_seps_length - length(p_seps); 49 | p_seps := p_seps || SUBSTRING(p_alphabet, 1, v_seps_diff); 50 | p_alphabet := SUBSTRING(p_alphabet, v_seps_diff + 1); 51 | else 52 | p_seps := SUBSTRING(p_seps, 1, v_seps_length + 1); 53 | end if; 54 | end if; 55 | 56 | p_alphabet := hashids.consistent_shuffle(p_alphabet, p_salt); 57 | 58 | v_guard_count := cast(ceil(length(p_alphabet) / v_guard_div ) as integer); 59 | 60 | if length(p_alphabet) < 3 then 61 | p_guards := SUBSTRING(p_seps, 1, v_guard_count); 62 | p_seps := SUBSTRING(p_seps, v_guard_count + 1); 63 | else 64 | p_guards := SUBSTRING(p_alphabet, 1, v_guard_count); 65 | p_alphabet := SUBSTRING(p_alphabet, v_guard_count + 1); 66 | end if; 67 | 68 | END; 69 | $$ 70 | LANGUAGE plpgsql IMMUTABLE 71 | COST 200; -------------------------------------------------------------------------------- /src/4_create_hashids_hash.sql: -------------------------------------------------------------------------------- 1 | drop function if exists hashids.hash(bigint, text, boolean); 2 | 3 | CREATE OR REPLACE FUNCTION hashids.hash( 4 | p_input bigint, 5 | p_alphabet text, 6 | p_zero_offset boolean DEFAULT true) 7 | RETURNS text AS 8 | $$ 9 | DECLARE 10 | p_input ALIAS for $1; 11 | p_alphabet ALIAS for $2; 12 | p_zero_offset integer := case when $3 = true then 1 else 0 end ; -- adding an offset so that this can work with values from a zero based array language 13 | v_hash varchar(255) := ''; 14 | v_alphabet_length integer := length($2); 15 | v_pos integer; 16 | BEGIN 17 | 18 | WHILE 1 = 1 LOOP 19 | v_pos := (p_input % v_alphabet_length) + p_zero_offset; -- have to add one, because SUBSTRING in SQL starts at 1 instead of 0 (like it does in other languages) 20 | --raise notice '% mod % == %', p_input, v_alphabet_length, v_pos; 21 | --raise notice 'SUBSTRING(%, %, 1): %', p_alphabet, v_pos, (SUBSTRING(p_alphabet, v_pos, 1)); 22 | --raise notice '% || % == %', SUBSTRING(p_alphabet, v_pos, 1), v_hash, SUBSTRING(p_alphabet, v_pos, 1) || v_hash; 23 | v_hash := SUBSTRING(p_alphabet, v_pos, 1) || v_hash; 24 | p_input := CAST((p_input / v_alphabet_length) as int); 25 | --raise notice 'p_input %', p_input; 26 | IF p_input <= 0 THEN 27 | EXIT; 28 | END IF; 29 | END LOOP; 30 | 31 | RETURN v_hash; 32 | END; 33 | $$ 34 | LANGUAGE plpgsql IMMUTABLE 35 | COST 250; 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/5_create_hashids_unhash.sql: -------------------------------------------------------------------------------- 1 | DROP FUNCTION if exists hashids.unhash(text, text, boolean); 2 | 3 | CREATE OR REPLACE FUNCTION hashids.unhash( 4 | p_input text, 5 | p_alphabet text, 6 | p_zero_offset boolean DEFAULT true) 7 | RETURNS bigint AS 8 | $$ 9 | DECLARE 10 | p_input ALIAS for $1; 11 | p_alphabet ALIAS for $2; 12 | p_zero_offset integer := case when $3 = true then 1 else 0 end ; -- adding an offset so that this can work with values from a zero based array language 13 | v_input_length integer := length($1); 14 | v_alphabet_length integer := length($2); 15 | v_ret bigint := 0; 16 | v_input_char char(1); 17 | v_pos integer; 18 | v_i integer := 1; 19 | BEGIN 20 | for v_i in 1..v_input_length loop 21 | v_input_char := SUBSTRING(p_input, (v_i), 1); 22 | v_pos := POSITION(v_input_char in p_alphabet) - p_zero_offset; -- have to remove one to interface with .net because it is a zero based index 23 | --raise notice '%[%] is % to position % in %', p_input, v_i, v_input_char, v_pos, p_alphabet; 24 | --raise notice ' % + (% * power(%, % - % - 1)) == %', v_ret, v_pos, v_alphabet_length, v_input_length, (v_i - 1), v_ret + (v_pos * power(v_alphabet_length, v_input_length - (v_i-1) - 1)); 25 | v_ret := v_ret + (v_pos * power(v_alphabet_length, v_input_length - (v_i-p_zero_offset) - 1)); 26 | end loop; 27 | 28 | RETURN v_ret; 29 | END; 30 | $$ 31 | LANGUAGE plpgsql IMMUTABLE 32 | COST 100; -------------------------------------------------------------------------------- /src/6_create_hashids_encode_list.sql: -------------------------------------------------------------------------------- 1 | 2 | drop function if exists hashids.encode_list(bigint[], text, integer, text, boolean); 3 | drop function if exists hashids.encode_list(bigint[], text, integer, text); 4 | drop function if exists hashids.encode_list(bigint[], text, integer); 5 | drop function if exists hashids.encode_list(bigint[], text); 6 | drop function if exists hashids.encode_list(bigint[]); 7 | 8 | 9 | CREATE OR REPLACE FUNCTION hashids.encode_list( 10 | in p_numbers bigint[], 11 | in p_salt text, -- DEFAULT '', 12 | in p_min_hash_length integer, -- integer default 0, 13 | in p_alphabet text, -- DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890', 14 | in p_zero_offset boolean DEFAULT true) 15 | RETURNS text AS 16 | $$ 17 | DECLARE 18 | p_numbers ALIAS for $1; 19 | p_salt ALIAS for $2; 20 | p_min_hash_length ALIAS for $3; 21 | p_alphabet ALIAS for $4; 22 | p_zero_offset integer := case when $5 = true then 1 else 0 end ; -- adding an offset so that this can work with values from a zero based array language 23 | v_seps text; 24 | v_guards text; 25 | 26 | -- Working Data 27 | v_alphabet text := p_alphabet; 28 | v_numbersHashInt int = 0; 29 | v_lottery char(1); 30 | v_buffer varchar(255); 31 | v_last varchar(255); 32 | v_ret varchar(255); 33 | v_sepsIndex int; 34 | v_lastId int; 35 | v_count int = array_length(p_numbers, 1); 36 | v_i int = 0; 37 | v_id int = 0; 38 | v_number int; 39 | v_guardIndex int; 40 | v_guard char(1); 41 | v_halfLength int; 42 | v_excess int; 43 | BEGIN 44 | 45 | select * from hashids.setup_alphabet(p_salt, p_alphabet) into v_alphabet, v_seps, v_guards; 46 | --raise notice 'v_seps: %', v_seps; 47 | --raise notice 'v_alphabet: %', v_alphabet; 48 | --raise notice 'v_guards: %', v_guards; 49 | 50 | -- Calculate numbersHashInt 51 | for v_lastId in 1..v_count LOOP 52 | v_numbersHashInt := v_numbersHashInt + (p_numbers[v_lastId] % ((v_lastId-p_zero_offset) + 100)); 53 | END LOOP; 54 | 55 | -- Choose lottery 56 | v_lottery := SUBSTRING(v_alphabet, (v_numbersHashInt % length(v_alphabet)) + 1, 1); -- is this a +1 because of sql 1 based index, need to double check to see if can be replaced with param. 57 | v_ret := v_lottery; 58 | 59 | -- Encode many 60 | v_i := 0; 61 | v_id := 0; 62 | for v_i in 1..v_count LOOP 63 | v_number := p_numbers[v_i]; 64 | raise notice '%[%]: % for %', p_numbers, v_i, v_number, v_count; 65 | 66 | v_buffer := v_lottery || p_salt || v_alphabet; 67 | v_alphabet := hashids.consistent_shuffle(v_alphabet, SUBSTRING(v_buffer, 1, length(v_alphabet))); 68 | v_last := hashids.hash(v_number, v_alphabet, cast(p_zero_offset as boolean)); 69 | v_ret := v_ret || v_last; 70 | --raise notice 'v_ret: %', v_ret; 71 | --raise notice '(v_i < v_count: % < % == %', v_i, v_count, (v_i < v_count); 72 | IF (v_i) < v_count THEN 73 | --raise notice 'v_sepsIndex: % mod (% + %) == %', v_number, ascii(SUBSTRING(v_last, 1, 1)), v_i, (v_number % (ascii(SUBSTRING(v_last, 1, 1)) + v_i)); 74 | v_sepsIndex := v_number % (ascii(SUBSTRING(v_last, 1, 1)) + (v_i-p_zero_offset)); -- since this is 1 base vs 0 based bringing the number back down so that the mod is the same for zero based records 75 | v_sepsIndex := v_sepsIndex % length(v_seps); 76 | v_ret := v_ret || SUBSTRING(v_seps, v_sepsIndex+1, 1); 77 | END IF; 78 | 79 | END LOOP; 80 | 81 | ---------------------------------------------------------------------------- 82 | -- Enforce minHashLength 83 | ---------------------------------------------------------------------------- 84 | IF length(v_ret) < p_min_hash_length THEN 85 | 86 | ------------------------------------------------------------------------ 87 | -- Add first 2 guard characters 88 | ------------------------------------------------------------------------ 89 | v_guardIndex := (v_numbersHashInt + ascii(SUBSTRING(v_ret, 1, 1))) % length(v_guards); 90 | v_guard := SUBSTRING(v_guards, v_guardIndex + 1, 1); 91 | --raise notice '% || % is %', v_guard, v_ret, v_guard || v_ret; 92 | v_ret := v_guard || v_ret; 93 | IF length(v_ret) < p_min_hash_length THEN 94 | v_guardIndex := (v_numbersHashInt + ascii(SUBSTRING(v_ret, 3, 1))) % length(v_guards); 95 | v_guard := SUBSTRING(v_guards, v_guardIndex + 1, 1); 96 | v_ret := v_ret || v_guard; 97 | END IF; 98 | ------------------------------------------------------------------------ 99 | -- Add the rest 100 | ------------------------------------------------------------------------ 101 | WHILE length(v_ret) < p_min_hash_length LOOP 102 | v_halfLength := COALESCE(v_halfLength, CAST((length(v_alphabet) / 2) as int)); 103 | v_alphabet := hashids.consistent_shuffle(v_alphabet, v_alphabet); 104 | v_ret := SUBSTRING(v_alphabet, v_halfLength + 1, 255) || v_ret || SUBSTRING(v_alphabet, 1, v_halfLength); 105 | v_excess := length(v_ret) - p_min_hash_length; 106 | IF v_excess > 0 THEN 107 | v_ret := SUBSTRING(v_ret, CAST((v_excess / 2) as int) + 1, p_min_hash_length); 108 | END IF; 109 | END LOOP; 110 | END IF; 111 | RETURN v_ret; 112 | END; 113 | $$ 114 | LANGUAGE plpgsql IMMUTABLE 115 | COST 350; 116 | 117 | 118 | CREATE OR REPLACE FUNCTION hashids.encode_list( in p_numbers bigint[] ) 119 | RETURNS text AS 120 | $$ 121 | -- Options Data - generated by hashids-tsql 122 | DECLARE 123 | p_numbers ALIAS for $1; 124 | p_salt text := ''; -- default 125 | p_min_hash_length integer := 0; -- default 126 | p_alphabet text := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; -- default 127 | p_zero_offset boolean := true ; -- adding an offset so that this can work with values from a zero based array language 128 | BEGIN 129 | RETURN hashids.encode_list(p_numbers, p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 130 | END; 131 | $$ 132 | LANGUAGE plpgsql IMMUTABLE 133 | COST 300; 134 | 135 | CREATE OR REPLACE FUNCTION hashids.encode_list( 136 | in p_numbers bigint[], 137 | in p_salt text ) 138 | RETURNS text AS 139 | $$ 140 | -- Options Data - generated by hashids-tsql 141 | DECLARE 142 | p_numbers ALIAS for $1; 143 | p_salt ALIAS for $2; -- default 144 | p_min_hash_length integer := 0; -- default 145 | p_alphabet text := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; -- default 146 | p_zero_offset boolean := true ; -- adding an offset so that this can work with values from a zero based array language 147 | BEGIN 148 | RETURN hashids.encode_list(p_numbers, p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 149 | END; 150 | $$ 151 | LANGUAGE plpgsql IMMUTABLE 152 | COST 300; 153 | 154 | CREATE OR REPLACE FUNCTION hashids.encode_list( 155 | in p_numbers bigint[], 156 | in p_salt text, 157 | in p_min_hash_length integer ) 158 | RETURNS text AS 159 | $$ 160 | -- Options Data - generated by hashids-tsql 161 | DECLARE 162 | p_numbers ALIAS for $1; 163 | p_salt ALIAS for $2; -- default 164 | p_min_hash_length ALIAS for $3; -- default 165 | p_alphabet text := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; -- default 166 | p_zero_offset boolean := true ; -- adding an offset so that this can work with values from a zero based array language 167 | BEGIN 168 | RETURN hashids.encode_list(p_numbers, p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 169 | END; 170 | $$ 171 | LANGUAGE plpgsql IMMUTABLE 172 | COST 300; 173 | 174 | 175 | -------------------------------------------------------------------------------- /src/7_create_hashids_encode.sql: -------------------------------------------------------------------------------- 1 | DROP FUNCTION if exists hashids.encode(bigint); 2 | DROP FUNCTION if exists hashids.encode(bigint, text); 3 | DROP FUNCTION if exists hashids.encode(bigint, text, integer); 4 | DROP FUNCTION if exists hashids.encode(bigint, text, integer, text); 5 | DROP FUNCTION if exists hashids.encode(bigint, text, integer, text, boolean); 6 | 7 | CREATE OR REPLACE FUNCTION hashids.encode(in p_number bigint) 8 | RETURNS text AS 9 | $$ 10 | DECLARE 11 | p_number ALIAS for $1; 12 | p_salt text := ''; -- default 13 | p_min_hash_length integer := 0; -- default 14 | p_alphabet text := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; -- default 15 | p_zero_offset boolean := true ; -- adding an offset so that this can work with values from a zero based array language 16 | BEGIN 17 | RETURN hashids.encode_list(ARRAY[p_number::bigint]::bigint[], p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 18 | END; 19 | $$ 20 | LANGUAGE plpgsql IMMUTABLE 21 | COST 300; 22 | 23 | 24 | CREATE OR REPLACE FUNCTION hashids.encode( 25 | in p_number bigint, 26 | in p_salt text) 27 | RETURNS text AS 28 | $$ 29 | DECLARE 30 | p_number ALIAS for $1; 31 | p_salt ALIAS for $2; 32 | p_min_hash_length integer := 0; -- default 33 | p_alphabet text := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; -- default 34 | p_zero_offset boolean := true ; -- adding an offset so that this can work with values from a zero based array language 35 | BEGIN 36 | RETURN hashids.encode_list(ARRAY[p_number::bigint]::bigint[], p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 37 | END; 38 | $$ 39 | LANGUAGE plpgsql IMMUTABLE 40 | COST 300; 41 | 42 | 43 | CREATE OR REPLACE FUNCTION hashids.encode( 44 | in p_number bigint, 45 | in p_salt text, 46 | in p_min_hash_length integer) 47 | RETURNS text AS 48 | $$ 49 | DECLARE 50 | p_number ALIAS for $1; 51 | p_salt ALIAS for $2; 52 | p_min_hash_length ALIAS for $3; -- default 53 | p_alphabet text := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; -- default 54 | p_zero_offset boolean := true ; -- adding an offset so that this can work with values from a zero based array language 55 | BEGIN 56 | RETURN hashids.encode_list(ARRAY[p_number::bigint]::bigint[], p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 57 | END; 58 | $$ 59 | LANGUAGE plpgsql IMMUTABLE 60 | COST 300; 61 | 62 | CREATE OR REPLACE FUNCTION hashids.encode( 63 | in p_number bigint, 64 | in p_salt text, 65 | in p_min_hash_length integer, 66 | in p_alphabet text) 67 | RETURNS text AS 68 | $$ 69 | DECLARE 70 | p_number ALIAS for $1; 71 | p_salt ALIAS for $2; 72 | p_min_hash_length ALIAS for $3; -- default 73 | p_alphabet ALIAS for $4; -- default 74 | p_zero_offset boolean := true ; -- adding an offset so that this can work with values from a zero based array language 75 | BEGIN 76 | RETURN hashids.encode_list(ARRAY[p_number::bigint]::bigint[], p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 77 | END; 78 | $$ 79 | LANGUAGE plpgsql IMMUTABLE 80 | COST 300; 81 | 82 | CREATE OR REPLACE FUNCTION hashids.encode( 83 | in p_number bigint, 84 | in p_salt text, 85 | in p_min_hash_length integer, 86 | in p_alphabet text, 87 | in p_zero_offset boolean) 88 | RETURNS text AS 89 | $$ 90 | DECLARE 91 | p_number ALIAS for $1; 92 | p_salt ALIAS for $2; 93 | p_min_hash_length ALIAS for $3; -- default 94 | p_alphabet ALIAS for $4; -- default 95 | p_zero_offset ALIAS for $5 ; -- adding an offset so that this can work with values from a zero based array language 96 | BEGIN 97 | RETURN hashids.encode_list(ARRAY[p_number::bigint]::bigint[], p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 98 | END; 99 | $$ 100 | LANGUAGE plpgsql IMMUTABLE 101 | COST 300; 102 | -------------------------------------------------------------------------------- /src/8_create_hashids_decode.sql: -------------------------------------------------------------------------------- 1 | 2 | DROP FUNCTION if exists hashids.decode(text, text, integer, text, boolean); 3 | DROP FUNCTION if exists hashids.decode(text, text, integer, text); 4 | DROP FUNCTION if exists hashids.decode(text, text, integer); 5 | DROP FUNCTION if exists hashids.decode(text, text); 6 | DROP FUNCTION if exists hashids.decode(text); 7 | 8 | CREATE OR REPLACE FUNCTION hashids.decode( 9 | in p_hash text, 10 | in p_salt text, 11 | in p_min_hash_length integer, 12 | in p_alphabet text, 13 | p_zero_offset boolean DEFAULT true) 14 | RETURNS bigint[] AS 15 | $$ 16 | DECLARE 17 | p_hash ALIAS for $1; 18 | p_salt ALIAS for $2; 19 | p_min_hash_length ALIAS for $3; 20 | p_alphabet ALIAS for $4; 21 | p_zero_offset ALIAS for $5; -- adding an offset so that this can work with values from a zero based array language 22 | 23 | v_seps text; 24 | v_guards text; 25 | v_alphabet text := p_alphabet; 26 | v_lottery char(1); 27 | 28 | v_hashBreakdown varchar(255); 29 | v_hashArray text[]; 30 | v_index integer := 1; 31 | v_j integer := 1; 32 | v_hashArrayLength integer; 33 | v_subHash varchar; 34 | v_buffer varchar(255); 35 | v_encodeCheck varchar(255); 36 | v_ret_temp bigint; 37 | v_ret bigint[]; 38 | BEGIN 39 | 40 | select * from hashids.setup_alphabet(p_salt, v_alphabet) into v_alphabet, v_seps, v_guards; 41 | --raise notice 'v_seps: %', v_seps; 42 | --raise notice 'v_alphabet: %', v_alphabet; 43 | --raise notice 'v_guards: %', v_guards; 44 | 45 | v_hashBreakdown := regexp_replace(p_hash, '[' || v_guards || ']', ' '); 46 | v_hashArray := regexp_split_to_array(p_hash, '[' || v_guards || ']'); 47 | 48 | -- take the guards and replace with space, 49 | -- split on space 50 | -- if length is 3 or 2, set index to 1 else start at zero 51 | 52 | -- if first index in idBreakDown isn't default 53 | 54 | if ((array_length(v_hashArray, 1) = 3) or (array_length(v_hashArray, 1) = 2)) then 55 | v_index := 2; -- in the example code (C# and js) it is 1 here, but postgresql arrays start at 1, so switching to 2 56 | END IF; 57 | --raise notice '%', v_hashArray; 58 | 59 | v_hashBreakdown := v_hashArray[v_index]; 60 | --raise notice 'v_hashArray[%] %', v_index, v_hashBreakdown; 61 | if (left(v_hashBreakdown, 1) <> '') IS NOT false then 62 | v_lottery := left(v_hashBreakdown, 1); 63 | --raise notice 'v_lottery %', v_lottery; 64 | --raise notice 'SUBSTRING(%, 2, % - 1) %', v_hashBreakdown, length(v_hashBreakdown), SUBSTRING(v_hashBreakdown, 2); 65 | 66 | v_hashBreakdown := SUBSTRING(v_hashBreakdown, 2); 67 | v_hashArray := regexp_split_to_array(v_hashBreakdown, '[' || v_seps || ']'); 68 | --raise notice 'v_hashArray % -- %', v_hashArray, array_length(v_hashArray, 1); 69 | v_hashArrayLength := array_length(v_hashArray, 1); 70 | for v_j in 1..v_hashArrayLength LOOP 71 | v_subHash := v_hashArray[v_j]; 72 | --raise notice 'v_subHash %', v_subHash; 73 | v_buffer := v_lottery || p_salt || v_alphabet; 74 | --raise notice 'v_buffer %', v_buffer; 75 | --raise notice 'v_alphabet: hashids.consistent_shuffle(%, %) == %', v_alphabet, SUBSTRING(v_buffer, 1, length(v_alphabet)), hashids.consistent_shuffle(v_alphabet, SUBSTRING(v_buffer, 1, length(v_alphabet))); 76 | v_alphabet := hashids.consistent_shuffle(v_alphabet, SUBSTRING(v_buffer, 1, length(v_alphabet))); 77 | v_ret_temp := hashids.unhash(v_subHash, v_alphabet, p_zero_offset); 78 | --raise notice 'v_ret_temp: %', v_ret_temp; 79 | v_ret := array_append(v_ret, v_ret_temp); 80 | END LOOP; 81 | v_encodeCheck := hashids.encode_list(v_ret, p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 82 | IF (v_encodeCheck <> p_hash) then 83 | raise notice 'hashids.encodeList(%): % <> %', v_ret, v_encodeCheck, p_hash; 84 | return ARRAY[]::bigint[]; 85 | end if; 86 | end if; 87 | 88 | RETURN v_ret; 89 | END; 90 | $$ 91 | LANGUAGE plpgsql IMMUTABLE 92 | COST 300; 93 | 94 | 95 | CREATE OR REPLACE FUNCTION hashids.decode( in p_hash text ) 96 | RETURNS bigint[] AS 97 | $$ 98 | DECLARE 99 | p_numbers ALIAS for $1; 100 | p_salt text := ''; -- default 101 | p_min_hash_length integer := 0; -- default 102 | p_alphabet text := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; -- default 103 | p_zero_offset boolean := true ; -- adding an offset so that this can work with values from a zero based array language 104 | BEGIN 105 | RETURN hashids.decode(p_hash, p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 106 | END; 107 | $$ 108 | LANGUAGE plpgsql IMMUTABLE 109 | COST 300; 110 | 111 | CREATE OR REPLACE FUNCTION hashids.decode( 112 | in p_hash text, 113 | in p_salt text) 114 | RETURNS text AS 115 | $$ 116 | DECLARE 117 | p_numbers ALIAS for $1; 118 | p_salt ALIAS for $2; -- default 119 | p_min_hash_length integer := 0; -- default 120 | p_alphabet text := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; -- default 121 | p_zero_offset boolean := true ; -- adding an offset so that this can work with values from a zero based array language 122 | BEGIN 123 | RETURN hashids.decode(p_hash, p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 124 | END; 125 | $$ 126 | LANGUAGE plpgsql IMMUTABLE 127 | COST 300; 128 | 129 | CREATE OR REPLACE FUNCTION hashids.decode( 130 | in p_hash text, 131 | in p_salt text, 132 | in p_min_hash_length integer) 133 | RETURNS bigint[] AS 134 | $$ 135 | DECLARE 136 | p_numbers ALIAS for $1; 137 | p_salt ALIAS for $2; -- default 138 | p_min_hash_length ALIAS for $3; -- default 139 | p_alphabet text := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; -- default 140 | p_zero_offset boolean := true ; -- adding an offset so that this can work with values from a zero based array language 141 | BEGIN 142 | RETURN hashids.decode(p_hash, p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 143 | END; 144 | $$ 145 | LANGUAGE plpgsql IMMUTABLE 146 | COST 300; 147 | 148 | CREATE OR REPLACE FUNCTION hashids.decode( 149 | in p_hash text, 150 | in p_salt text, 151 | in p_min_hash_length integer, 152 | in p_alphabet text) 153 | RETURNS bigint[] AS 154 | $$ 155 | DECLARE 156 | p_numbers ALIAS for $1; 157 | p_salt ALIAS for $2; -- default 158 | p_min_hash_length ALIAS for $3; -- default 159 | p_alphabet ALIAS for $4; -- default 160 | p_zero_offset boolean := true ; -- adding an offset so that this can work with values from a zero based array language 161 | BEGIN 162 | RETURN hashids.decode(p_hash, p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 163 | END; 164 | $$ 165 | LANGUAGE plpgsql IMMUTABLE 166 | COST 300; 167 | 168 | CREATE OR REPLACE FUNCTION hashids.decode( 169 | in p_hash text, 170 | in p_salt text, 171 | in p_min_hash_length integer, 172 | in p_alphabet text, 173 | in p_zero_offset boolean DEFAULT true) 174 | RETURNS bigint[] AS 175 | $$ 176 | DECLARE 177 | p_numbers ALIAS for $1; 178 | p_salt ALIAS for $2; -- default 179 | p_min_hash_length ALIAS for $3; -- default 180 | p_alphabet ALIAS for $4; -- default 181 | p_zero_offset ALIAS for $5 ; -- adding an offset so that this can work with values from a zero based array language 182 | BEGIN 183 | RETURN hashids.decode(p_hash, p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 184 | END; 185 | $$ 186 | LANGUAGE plpgsql IMMUTABLE 187 | COST 300; 188 | -------------------------------------------------------------------------------- /src/all_hashids.sql: -------------------------------------------------------------------------------- 1 | -- our usage of these functions places all of them in a separate schema, with "hashids" being the default name 2 | -- change this or don't use it (but know that the other sql functions will not work out of the box, they are assuming the hashids schema). 3 | CREATE SCHEMA hashids; 4 | 5 | -- drop all functions out of schema 6 | DROP FUNCTION if exists hashids.consistent_shuffle(text, text); 7 | DROP FUNCTION if exists hashids.clean_seps_from_alphabet(text, text); 8 | DROP FUNCTION if exists hashids.clean_alphabet_from_seps(text, text); 9 | DROP FUNCTION if exists hashids.distinct_alphabet(text); 10 | 11 | DROP FUNCTION if exists hashids.setup_alphabet(text, text, text, text); 12 | 13 | DROP FUNCTION if exists hashids.hash(bigint, text, boolean); 14 | 15 | DROP FUNCTION if exists hashids.unhash(text, text, boolean); 16 | 17 | DROP FUNCTION if exists hashids.encode_list(bigint[], text, integer, text, boolean); 18 | DROP FUNCTION if exists hashids.encode_list(bigint[], text, integer, text); 19 | DROP FUNCTION if exists hashids.encode_list(bigint[], text, integer); 20 | DROP FUNCTION if exists hashids.encode_list(bigint[], text); 21 | DROP FUNCTION if exists hashids.encode_list(bigint[]); 22 | 23 | DROP FUNCTION if exists hashids.encode(bigint); 24 | DROP FUNCTION if exists hashids.encode(bigint, text); 25 | DROP FUNCTION if exists hashids.encode(bigint, text, integer); 26 | DROP FUNCTION if exists hashids.encode(bigint, text, integer, text); 27 | DROP FUNCTION if exists hashids.encode(bigint, text, integer, text, boolean); 28 | 29 | DROP FUNCTION if exists hashids.decode(text, text, integer, text, boolean); 30 | DROP FUNCTION if exists hashids.decode(text, text, integer, text); 31 | DROP FUNCTION if exists hashids.decode(text, text, integer); 32 | DROP FUNCTION if exists hashids.decode(text, text); 33 | DROP FUNCTION if exists hashids.decode(text); 34 | 35 | -- constent shuffle 36 | CREATE OR REPLACE FUNCTION hashids.consistent_shuffle 37 | ( 38 | p_alphabet text, 39 | p_salt text 40 | ) 41 | RETURNS text AS 42 | $$ 43 | DECLARE p_alphabet ALIAS FOR $1; 44 | p_salt ALIAS FOR $2; 45 | v_ls int; 46 | v_i int; 47 | v_v int := 0; 48 | v_p int := 0; 49 | v_n int := 0; 50 | v_j int := 0; 51 | v_temp char(1); 52 | BEGIN 53 | 54 | -- Null or Whitespace? 55 | IF p_salt IS NULL OR length(LTRIM(RTRIM(p_salt))) = 0 THEN 56 | RETURN p_alphabet; 57 | END IF; 58 | 59 | v_ls := length(p_salt); 60 | v_i := length(p_alphabet) - 1; 61 | 62 | WHILE v_i > 0 LOOP 63 | 64 | v_v := v_v % v_ls; 65 | v_n := ascii(SUBSTRING(p_salt, v_v + 1, 1)); -- need some investigation to see if +1 here is because of 1 based arrays in sql ... this isn't in the reference JS or .net code. 66 | v_p := v_p + v_n; 67 | v_j := (v_n + v_v + v_p) % v_i; 68 | v_temp := SUBSTRING(p_alphabet, v_j + 1, 1); 69 | p_alphabet := 70 | SUBSTRING(p_alphabet, 1, v_j) || 71 | SUBSTRING(p_alphabet, v_i + 1, 1) || 72 | SUBSTRING(p_alphabet, v_j + 2, 255); 73 | p_alphabet := SUBSTRING(p_alphabet, 1, v_i) || v_temp || SUBSTRING(p_alphabet, v_i + 2, 255); 74 | v_i := v_i - 1; 75 | v_v := v_v + 1; 76 | 77 | END LOOP; -- WHILE 78 | 79 | RETURN p_alphabet; 80 | 81 | END; 82 | $$ 83 | LANGUAGE plpgsql IMMUTABLE 84 | COST 200; 85 | 86 | -- setup seps 87 | CREATE OR REPLACE FUNCTION hashids.clean_seps_from_alphabet( 88 | in p_seps text, 89 | in p_alphabet text 90 | ) 91 | RETURNS text AS 92 | $$ 93 | DECLARE 94 | p_seps ALIAS for $1; 95 | p_alphabet ALIAS for $2; 96 | v_split_seps text[] := regexp_split_to_array(p_seps, ''); 97 | v_split_alphabet text[] := regexp_split_to_array(p_alphabet, ''); 98 | v_i integer := 1; 99 | v_length integer := length(p_seps); 100 | v_ret text := ''; 101 | BEGIN 102 | -- had to add this function because doing this: 103 | -- p_seps := array_to_string(ARRAY(select chars.cha from (select unnest(regexp_split_to_array(p_seps, '')) as cha intersect select unnest(regexp_split_to_array(p_alphabet, '')) as cha ) as chars order by ascii(cha) desc), ''); 104 | -- doesn't preserve the order of the input 105 | 106 | for v_i in 1..v_length loop 107 | -- raise notice 'v_split_seps[%]: % == %', v_i, v_split_seps[v_i], v_split_seps[v_i] = any (v_split_alphabet); 108 | if (v_split_seps[v_i] = any (v_split_alphabet)) then 109 | v_ret = v_ret || v_split_seps[v_i]; 110 | end if; 111 | end loop; 112 | 113 | -- raise notice 'v_ret: %', v_ret; 114 | return v_ret; 115 | END; 116 | $$ 117 | LANGUAGE plpgsql IMMUTABLE 118 | COST 200; 119 | 120 | 121 | CREATE OR REPLACE FUNCTION hashids.clean_alphabet_from_seps( 122 | in p_seps text, 123 | in p_alphabet text 124 | ) 125 | RETURNS text AS 126 | $$ 127 | DECLARE 128 | p_seps ALIAS for $1; 129 | p_alphabet ALIAS for $2; 130 | v_split_seps text[] := regexp_split_to_array(p_seps, ''); 131 | v_split_alphabet text[] := regexp_split_to_array(p_alphabet, ''); 132 | v_i integer := 1; 133 | v_length integer := length(p_alphabet); 134 | v_ret text := ''; 135 | BEGIN 136 | -- had to add this function because doing this: 137 | -- p_alphabet := array_to_string(ARRAY( select chars.cha from (select unnest(regexp_split_to_array(p_alphabet, '')) as cha EXCEPT select unnest(regexp_split_to_array(p_seps, '')) as cha) as chars ), ''); 138 | -- doesn't preserve the order of the input 139 | 140 | for v_i in 1..v_length loop 141 | --raise notice 'v_split_alphabet[%]: % != %', v_i, v_split_alphabet[v_i], v_split_alphabet[v_i] <> all (v_split_seps); 142 | if (v_split_alphabet[v_i] <> all (v_split_seps)) then 143 | v_ret = v_ret || v_split_alphabet[v_i]; 144 | end if; 145 | end loop; 146 | 147 | -- raise notice 'v_ret: %', v_ret; 148 | return v_ret; 149 | END; 150 | $$ 151 | LANGUAGE plpgsql IMMUTABLE 152 | COST 200; 153 | 154 | CREATE OR REPLACE FUNCTION hashids.distinct_alphabet(in p_alphabet text) 155 | RETURNS text AS 156 | $$ 157 | DECLARE 158 | p_alphabet ALIAS for $1; 159 | v_split_alphabet text[] := regexp_split_to_array(p_alphabet, ''); 160 | v_i integer := 2; 161 | v_length integer := length(p_alphabet); 162 | v_ret_array text[]; 163 | BEGIN 164 | -- had to add this function because doing this: 165 | -- p_alphabet := string_agg(distinct chars.split_chars, '') from (select unnest(regexp_split_to_array(p_alphabet, '')) as split_chars) as chars; 166 | -- doesn't preserve the order of the input, which was causing issues 167 | if (v_length = 0) then 168 | RAISE EXCEPTION 'alphabet must contain at least 1 char' USING HINT = 'Please check your alphabet'; 169 | end if; 170 | v_ret_array := array_append(v_ret_array, v_split_alphabet[1]); 171 | 172 | -- starting at 2 because already appended 1 to it. 173 | for v_i in 2..v_length loop 174 | -- raise notice 'v_split_alphabet[%]: % != %', v_i, v_split_alphabet[v_i], v_split_alphabet[v_i] <> all (v_ret_array); 175 | 176 | if (v_split_alphabet[v_i] <> all (v_ret_array)) then 177 | v_ret_array := array_append(v_ret_array, v_split_alphabet[v_i]); 178 | end if; 179 | end loop; 180 | 181 | -- raise notice 'v_ret_array: %', array_to_string(v_ret_array, ''); 182 | return array_to_string(v_ret_array, ''); 183 | END; 184 | $$ 185 | LANGUAGE plpgsql IMMUTABLE 186 | COST 200; 187 | 188 | -- setup alphabet 189 | CREATE OR REPLACE FUNCTION hashids.setup_alphabet( 190 | in p_salt text default '', 191 | inout p_alphabet text default 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890', 192 | out p_seps text, 193 | out p_guards text) 194 | AS 195 | $$ 196 | DECLARE 197 | p_salt ALIAS for $1; 198 | p_alphabet ALIAS for $2; 199 | p_seps ALIAS for $3; 200 | p_guards ALIAS for $4; 201 | v_sep_div float := 3.5; 202 | v_guard_div float := 12.0; 203 | v_guard_count integer; 204 | v_seps_length integer; 205 | v_seps_diff integer; 206 | BEGIN 207 | p_seps := 'cfhistuCFHISTU'; 208 | -- p_alphabet := string_agg(distinct chars.split_chars, '') from (select unnest(regexp_split_to_array(p_alphabet, '')) as split_chars) as chars; 209 | -- this also doesn't preserve the order of alphabet, but it doesn't appear to matter, never mind on that 210 | p_alphabet := hashids.distinct_alphabet(p_alphabet); 211 | 212 | 213 | if length(p_alphabet) < 16 then 214 | RAISE EXCEPTION 'alphabet must contain 16 unique characters, it is: %', length(p_alphabet) USING HINT = 'Please check your alphabet'; 215 | end if; 216 | 217 | -- seps should only contain character present in the passed alphabet 218 | -- p_seps := array_to_string(ARRAY(select chars.cha from (select unnest(regexp_split_to_array(p_seps, '')) as cha intersect select unnest(regexp_split_to_array(p_alphabet, '')) as cha ) as chars order by ascii(cha) desc), ''); 219 | -- this doesn't preserve the input order, which is bad 220 | p_seps := hashids.clean_seps_from_alphabet(p_seps, p_alphabet); 221 | 222 | -- alphabet should not contain seps. 223 | -- p_alphabet := array_to_string(ARRAY( select chars.cha from (select unnest(regexp_split_to_array(p_alphabet, '')) as cha EXCEPT select unnest(regexp_split_to_array(p_seps, '')) as cha) as chars ), ''); 224 | -- this also doesn't prevserve the order 225 | p_alphabet := hashids.clean_alphabet_from_seps(p_seps, p_alphabet); 226 | 227 | 228 | p_seps := hashids.consistent_shuffle(p_seps, p_salt); 229 | 230 | if (length(p_seps) = 0) or ((length(p_alphabet) / length(p_seps)) > v_sep_div) then 231 | v_seps_length := cast( ceil( length(p_alphabet)/v_sep_div ) as integer); 232 | if v_seps_length = 1 then 233 | v_seps_length := 2; 234 | end if; 235 | if v_seps_length > length(p_seps) then 236 | v_seps_diff := v_seps_length - length(p_seps); 237 | p_seps := p_seps || SUBSTRING(p_alphabet, 1, v_seps_diff); 238 | p_alphabet := SUBSTRING(p_alphabet, v_seps_diff + 1); 239 | else 240 | p_seps := SUBSTRING(p_seps, 1, v_seps_length + 1); 241 | end if; 242 | end if; 243 | 244 | p_alphabet := hashids.consistent_shuffle(p_alphabet, p_salt); 245 | 246 | v_guard_count := cast(ceil(length(p_alphabet) / v_guard_div ) as integer); 247 | 248 | if length(p_alphabet) < 3 then 249 | p_guards := SUBSTRING(p_seps, 1, v_guard_count); 250 | p_seps := SUBSTRING(p_seps, v_guard_count + 1); 251 | else 252 | p_guards := SUBSTRING(p_alphabet, 1, v_guard_count); 253 | p_alphabet := SUBSTRING(p_alphabet, v_guard_count + 1); 254 | end if; 255 | 256 | END; 257 | $$ 258 | LANGUAGE plpgsql IMMUTABLE 259 | COST 200; 260 | 261 | -- create hash 262 | CREATE OR REPLACE FUNCTION hashids.hash( 263 | p_input bigint, 264 | p_alphabet text, 265 | p_zero_offset boolean DEFAULT true) 266 | RETURNS text AS 267 | $$ 268 | DECLARE 269 | p_input ALIAS for $1; 270 | p_alphabet ALIAS for $2; 271 | p_zero_offset integer := case when $3 = true then 1 else 0 end ; -- adding an offset so that this can work with values from a zero based array language 272 | v_hash varchar(255) := ''; 273 | v_alphabet_length integer := length($2); 274 | v_pos integer; 275 | BEGIN 276 | 277 | WHILE 1 = 1 LOOP 278 | v_pos := (p_input % v_alphabet_length) + p_zero_offset; -- have to add one, because SUBSTRING in SQL starts at 1 instead of 0 (like it does in other languages) 279 | --raise notice '% mod % == %', p_input, v_alphabet_length, v_pos; 280 | --raise notice 'SUBSTRING(%, %, 1): %', p_alphabet, v_pos, (SUBSTRING(p_alphabet, v_pos, 1)); 281 | --raise notice '% || % == %', SUBSTRING(p_alphabet, v_pos, 1), v_hash, SUBSTRING(p_alphabet, v_pos, 1) || v_hash; 282 | v_hash := SUBSTRING(p_alphabet, v_pos, 1) || v_hash; 283 | p_input := CAST((p_input / v_alphabet_length) as int); 284 | --raise notice 'p_input %', p_input; 285 | IF p_input <= 0 THEN 286 | EXIT; 287 | END IF; 288 | END LOOP; 289 | 290 | RETURN v_hash; 291 | END; 292 | $$ 293 | LANGUAGE plpgsql IMMUTABLE 294 | COST 250; 295 | 296 | -- unhash 297 | CREATE OR REPLACE FUNCTION hashids.unhash( 298 | p_input text, 299 | p_alphabet text, 300 | p_zero_offset boolean DEFAULT true) 301 | RETURNS bigint AS 302 | $$ 303 | DECLARE 304 | p_input ALIAS for $1; 305 | p_alphabet ALIAS for $2; 306 | p_zero_offset integer := case when $3 = true then 1 else 0 end ; -- adding an offset so that this can work with values from a zero based array language 307 | v_input_length integer := length($1); 308 | v_alphabet_length integer := length($2); 309 | v_ret bigint := 0; 310 | v_input_char char(1); 311 | v_pos integer; 312 | v_i integer := 1; 313 | BEGIN 314 | for v_i in 1..v_input_length loop 315 | v_input_char := SUBSTRING(p_input, (v_i), 1); 316 | v_pos := POSITION(v_input_char in p_alphabet) - p_zero_offset; -- have to remove one to interface with .net because it is a zero based index 317 | --raise notice '%[%] is % to position % in %', p_input, v_i, v_input_char, v_pos, p_alphabet; 318 | --raise notice ' % + (% * power(%, % - % - 1)) == %', v_ret, v_pos, v_alphabet_length, v_input_length, (v_i - 1), v_ret + (v_pos * power(v_alphabet_length, v_input_length - (v_i-1) - 1)); 319 | v_ret := v_ret + (v_pos * power(v_alphabet_length, v_input_length - (v_i-p_zero_offset) - 1)); 320 | end loop; 321 | 322 | RETURN v_ret; 323 | END; 324 | $$ 325 | LANGUAGE plpgsql IMMUTABLE 326 | COST 100; 327 | 328 | -- encode list 329 | CREATE OR REPLACE FUNCTION hashids.encode_list( 330 | in p_numbers bigint[], 331 | in p_salt text, -- DEFAULT '', 332 | in p_min_hash_length integer, -- integer default 0, 333 | in p_alphabet text, -- DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890', 334 | in p_zero_offset boolean DEFAULT true) 335 | RETURNS text AS 336 | $$ 337 | DECLARE 338 | p_numbers ALIAS for $1; 339 | p_salt ALIAS for $2; 340 | p_min_hash_length ALIAS for $3; 341 | p_alphabet ALIAS for $4; 342 | p_zero_offset integer := case when $5 = true then 1 else 0 end ; -- adding an offset so that this can work with values from a zero based array language 343 | v_seps text; 344 | v_guards text; 345 | 346 | -- Working Data 347 | v_alphabet text := p_alphabet; 348 | v_numbersHashInt int = 0; 349 | v_lottery char(1); 350 | v_buffer varchar(255); 351 | v_last varchar(255); 352 | v_ret varchar(255); 353 | v_sepsIndex int; 354 | v_lastId int; 355 | v_count int = array_length(p_numbers, 1); 356 | v_i int = 0; 357 | v_id int = 0; 358 | v_number int; 359 | v_guardIndex int; 360 | v_guard char(1); 361 | v_halfLength int; 362 | v_excess int; 363 | BEGIN 364 | 365 | select * from hashids.setup_alphabet(p_salt, p_alphabet) into v_alphabet, v_seps, v_guards; 366 | --raise notice 'v_seps: %', v_seps; 367 | --raise notice 'v_alphabet: %', v_alphabet; 368 | --raise notice 'v_guards: %', v_guards; 369 | 370 | -- Calculate numbersHashInt 371 | for v_lastId in 1..v_count LOOP 372 | v_numbersHashInt := v_numbersHashInt + (p_numbers[v_lastId] % ((v_lastId-p_zero_offset) + 100)); 373 | END LOOP; 374 | 375 | -- Choose lottery 376 | v_lottery := SUBSTRING(v_alphabet, (v_numbersHashInt % length(v_alphabet)) + 1, 1); -- is this a +1 because of sql 1 based index, need to double check to see if can be replaced with param. 377 | v_ret := v_lottery; 378 | 379 | -- Encode many 380 | v_i := 0; 381 | v_id := 0; 382 | for v_i in 1..v_count LOOP 383 | v_number := p_numbers[v_i]; 384 | raise notice '%[%]: % for %', p_numbers, v_i, v_number, v_count; 385 | 386 | v_buffer := v_lottery || p_salt || v_alphabet; 387 | v_alphabet := hashids.consistent_shuffle(v_alphabet, SUBSTRING(v_buffer, 1, length(v_alphabet))); 388 | v_last := hashids.hash(v_number, v_alphabet, cast(p_zero_offset as boolean)); 389 | v_ret := v_ret || v_last; 390 | --raise notice 'v_ret: %', v_ret; 391 | --raise notice '(v_i < v_count: % < % == %', v_i, v_count, (v_i < v_count); 392 | IF (v_i) < v_count THEN 393 | --raise notice 'v_sepsIndex: % mod (% + %) == %', v_number, ascii(SUBSTRING(v_last, 1, 1)), v_i, (v_number % (ascii(SUBSTRING(v_last, 1, 1)) + v_i)); 394 | v_sepsIndex := v_number % (ascii(SUBSTRING(v_last, 1, 1)) + (v_i-p_zero_offset)); -- since this is 1 base vs 0 based bringing the number back down so that the mod is the same for zero based records 395 | v_sepsIndex := v_sepsIndex % length(v_seps); 396 | v_ret := v_ret || SUBSTRING(v_seps, v_sepsIndex+1, 1); 397 | END IF; 398 | 399 | END LOOP; 400 | 401 | ---------------------------------------------------------------------------- 402 | -- Enforce minHashLength 403 | ---------------------------------------------------------------------------- 404 | IF length(v_ret) < p_min_hash_length THEN 405 | 406 | ------------------------------------------------------------------------ 407 | -- Add first 2 guard characters 408 | ------------------------------------------------------------------------ 409 | v_guardIndex := (v_numbersHashInt + ascii(SUBSTRING(v_ret, 1, 1))) % length(v_guards); 410 | v_guard := SUBSTRING(v_guards, v_guardIndex + 1, 1); 411 | --raise notice '% || % is %', v_guard, v_ret, v_guard || v_ret; 412 | v_ret := v_guard || v_ret; 413 | IF length(v_ret) < p_min_hash_length THEN 414 | v_guardIndex := (v_numbersHashInt + ascii(SUBSTRING(v_ret, 3, 1))) % length(v_guards); 415 | v_guard := SUBSTRING(v_guards, v_guardIndex + 1, 1); 416 | v_ret := v_ret || v_guard; 417 | END IF; 418 | ------------------------------------------------------------------------ 419 | -- Add the rest 420 | ------------------------------------------------------------------------ 421 | WHILE length(v_ret) < p_min_hash_length LOOP 422 | v_halfLength := COALESCE(v_halfLength, CAST((length(v_alphabet) / 2) as int)); 423 | v_alphabet := hashids.consistent_shuffle(v_alphabet, v_alphabet); 424 | v_ret := SUBSTRING(v_alphabet, v_halfLength + 1, 255) || v_ret || SUBSTRING(v_alphabet, 1, v_halfLength); 425 | v_excess := length(v_ret) - p_min_hash_length; 426 | IF v_excess > 0 THEN 427 | v_ret := SUBSTRING(v_ret, CAST((v_excess / 2) as int) + 1, p_min_hash_length); 428 | END IF; 429 | END LOOP; 430 | END IF; 431 | RETURN v_ret; 432 | END; 433 | $$ 434 | LANGUAGE plpgsql IMMUTABLE 435 | COST 350; 436 | 437 | 438 | CREATE OR REPLACE FUNCTION hashids.encode_list( in p_numbers bigint[] ) 439 | RETURNS text AS 440 | $$ 441 | -- Options Data - generated by hashids-tsql 442 | DECLARE 443 | p_numbers ALIAS for $1; 444 | p_salt text := ''; -- default 445 | p_min_hash_length integer := 0; -- default 446 | p_alphabet text := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; -- default 447 | p_zero_offset boolean := true ; -- adding an offset so that this can work with values from a zero based array language 448 | BEGIN 449 | RETURN hashids.encode_list(p_numbers, p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 450 | END; 451 | $$ 452 | LANGUAGE plpgsql IMMUTABLE 453 | COST 300; 454 | 455 | CREATE OR REPLACE FUNCTION hashids.encode_list( 456 | in p_numbers bigint[], 457 | in p_salt text ) 458 | RETURNS text AS 459 | $$ 460 | -- Options Data - generated by hashids-tsql 461 | DECLARE 462 | p_numbers ALIAS for $1; 463 | p_salt ALIAS for $2; -- default 464 | p_min_hash_length integer := 0; -- default 465 | p_alphabet text := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; -- default 466 | p_zero_offset boolean := true ; -- adding an offset so that this can work with values from a zero based array language 467 | BEGIN 468 | RETURN hashids.encode_list(p_numbers, p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 469 | END; 470 | $$ 471 | LANGUAGE plpgsql IMMUTABLE 472 | COST 300; 473 | 474 | CREATE OR REPLACE FUNCTION hashids.encode_list( 475 | in p_numbers bigint[], 476 | in p_salt text, 477 | in p_min_hash_length integer ) 478 | RETURNS text AS 479 | $$ 480 | -- Options Data - generated by hashids-tsql 481 | DECLARE 482 | p_numbers ALIAS for $1; 483 | p_salt ALIAS for $2; -- default 484 | p_min_hash_length ALIAS for $3; -- default 485 | p_alphabet text := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; -- default 486 | p_zero_offset boolean := true ; -- adding an offset so that this can work with values from a zero based array language 487 | BEGIN 488 | RETURN hashids.encode_list(p_numbers, p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 489 | END; 490 | $$ 491 | LANGUAGE plpgsql IMMUTABLE 492 | COST 300; 493 | 494 | -- encode 495 | CREATE OR REPLACE FUNCTION hashids.encode(in p_number bigint) 496 | RETURNS text AS 497 | $$ 498 | DECLARE 499 | p_number ALIAS for $1; 500 | p_salt text := ''; -- default 501 | p_min_hash_length integer := 0; -- default 502 | p_alphabet text := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; -- default 503 | p_zero_offset boolean := true ; -- adding an offset so that this can work with values from a zero based array language 504 | BEGIN 505 | RETURN hashids.encode_list(ARRAY[p_number::bigint]::bigint[], p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 506 | END; 507 | $$ 508 | LANGUAGE plpgsql IMMUTABLE 509 | COST 300; 510 | 511 | 512 | CREATE OR REPLACE FUNCTION hashids.encode( 513 | in p_number bigint, 514 | in p_salt text) 515 | RETURNS text AS 516 | $$ 517 | DECLARE 518 | p_number ALIAS for $1; 519 | p_salt ALIAS for $2; 520 | p_min_hash_length integer := 0; -- default 521 | p_alphabet text := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; -- default 522 | p_zero_offset boolean := true ; -- adding an offset so that this can work with values from a zero based array language 523 | BEGIN 524 | RETURN hashids.encode_list(ARRAY[p_number::bigint]::bigint[], p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 525 | END; 526 | $$ 527 | LANGUAGE plpgsql IMMUTABLE 528 | COST 300; 529 | 530 | 531 | CREATE OR REPLACE FUNCTION hashids.encode( 532 | in p_number bigint, 533 | in p_salt text, 534 | in p_min_hash_length integer) 535 | RETURNS text AS 536 | $$ 537 | DECLARE 538 | p_number ALIAS for $1; 539 | p_salt ALIAS for $2; 540 | p_min_hash_length ALIAS for $3; -- default 541 | p_alphabet text := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; -- default 542 | p_zero_offset boolean := true ; -- adding an offset so that this can work with values from a zero based array language 543 | BEGIN 544 | RETURN hashids.encode_list(ARRAY[p_number::bigint]::bigint[], p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 545 | END; 546 | $$ 547 | LANGUAGE plpgsql IMMUTABLE 548 | COST 300; 549 | 550 | CREATE OR REPLACE FUNCTION hashids.encode( 551 | in p_number bigint, 552 | in p_salt text, 553 | in p_min_hash_length integer, 554 | in p_alphabet text) 555 | RETURNS text AS 556 | $$ 557 | DECLARE 558 | p_number ALIAS for $1; 559 | p_salt ALIAS for $2; 560 | p_min_hash_length ALIAS for $3; -- default 561 | p_alphabet ALIAS for $4; -- default 562 | p_zero_offset boolean := true ; -- adding an offset so that this can work with values from a zero based array language 563 | BEGIN 564 | RETURN hashids.encode_list(ARRAY[p_number::bigint]::bigint[], p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 565 | END; 566 | $$ 567 | LANGUAGE plpgsql IMMUTABLE 568 | COST 300; 569 | 570 | CREATE OR REPLACE FUNCTION hashids.encode( 571 | in p_number bigint, 572 | in p_salt text, 573 | in p_min_hash_length integer, 574 | in p_alphabet text, 575 | in p_zero_offset boolean) 576 | RETURNS text AS 577 | $$ 578 | DECLARE 579 | p_number ALIAS for $1; 580 | p_salt ALIAS for $2; 581 | p_min_hash_length ALIAS for $3; -- default 582 | p_alphabet ALIAS for $4; -- default 583 | p_zero_offset ALIAS for $5 ; -- adding an offset so that this can work with values from a zero based array language 584 | BEGIN 585 | RETURN hashids.encode_list(ARRAY[p_number::bigint]::bigint[], p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 586 | END; 587 | $$ 588 | LANGUAGE plpgsql IMMUTABLE 589 | COST 300; 590 | 591 | -- decode 592 | CREATE OR REPLACE FUNCTION hashids.decode( 593 | in p_hash text, 594 | in p_salt text, 595 | in p_min_hash_length integer, 596 | in p_alphabet text, 597 | p_zero_offset boolean DEFAULT true) 598 | RETURNS bigint[] AS 599 | $$ 600 | DECLARE 601 | p_hash ALIAS for $1; 602 | p_salt ALIAS for $2; 603 | p_min_hash_length ALIAS for $3; 604 | p_alphabet ALIAS for $4; 605 | p_zero_offset ALIAS for $5; -- adding an offset so that this can work with values from a zero based array language 606 | 607 | v_seps text; 608 | v_guards text; 609 | v_alphabet text := p_alphabet; 610 | v_lottery char(1); 611 | 612 | v_hashBreakdown varchar(255); 613 | v_hashArray text[]; 614 | v_index integer := 1; 615 | v_j integer := 1; 616 | v_hashArrayLength integer; 617 | v_subHash varchar; 618 | v_buffer varchar(255); 619 | v_encodeCheck varchar(255); 620 | v_ret_temp bigint; 621 | v_ret bigint[]; 622 | BEGIN 623 | 624 | select * from hashids.setup_alphabet(p_salt, v_alphabet) into v_alphabet, v_seps, v_guards; 625 | --raise notice 'v_seps: %', v_seps; 626 | --raise notice 'v_alphabet: %', v_alphabet; 627 | --raise notice 'v_guards: %', v_guards; 628 | 629 | v_hashBreakdown := regexp_replace(p_hash, '[' || v_guards || ']', ' '); 630 | v_hashArray := regexp_split_to_array(p_hash, '[' || v_guards || ']'); 631 | 632 | -- take the guards and replace with space, 633 | -- split on space 634 | -- if length is 3 or 2, set index to 1 else start at zero 635 | 636 | -- if first index in idBreakDown isn't default 637 | 638 | if ((array_length(v_hashArray, 1) = 3) or (array_length(v_hashArray, 1) = 2)) then 639 | v_index := 2; -- in the example code (C# and js) it is 1 here, but postgresql arrays start at 1, so switching to 2 640 | END IF; 641 | --raise notice '%', v_hashArray; 642 | 643 | v_hashBreakdown := v_hashArray[v_index]; 644 | --raise notice 'v_hashArray[%] %', v_index, v_hashBreakdown; 645 | if (left(v_hashBreakdown, 1) <> '') IS NOT false then 646 | v_lottery := left(v_hashBreakdown, 1); 647 | --raise notice 'v_lottery %', v_lottery; 648 | --raise notice 'SUBSTRING(%, 2, % - 1) %', v_hashBreakdown, length(v_hashBreakdown), SUBSTRING(v_hashBreakdown, 2); 649 | 650 | v_hashBreakdown := SUBSTRING(v_hashBreakdown, 2); 651 | v_hashArray := regexp_split_to_array(v_hashBreakdown, '[' || v_seps || ']'); 652 | --raise notice 'v_hashArray % -- %', v_hashArray, array_length(v_hashArray, 1); 653 | v_hashArrayLength := array_length(v_hashArray, 1); 654 | for v_j in 1..v_hashArrayLength LOOP 655 | v_subHash := v_hashArray[v_j]; 656 | --raise notice 'v_subHash %', v_subHash; 657 | v_buffer := v_lottery || p_salt || v_alphabet; 658 | --raise notice 'v_buffer %', v_buffer; 659 | --raise notice 'v_alphabet: hashids.consistent_shuffle(%, %) == %', v_alphabet, SUBSTRING(v_buffer, 1, length(v_alphabet)), hashids.consistent_shuffle(v_alphabet, SUBSTRING(v_buffer, 1, length(v_alphabet))); 660 | v_alphabet := hashids.consistent_shuffle(v_alphabet, SUBSTRING(v_buffer, 1, length(v_alphabet))); 661 | v_ret_temp := hashids.unhash(v_subHash, v_alphabet, p_zero_offset); 662 | --raise notice 'v_ret_temp: %', v_ret_temp; 663 | v_ret := array_append(v_ret, v_ret_temp); 664 | END LOOP; 665 | v_encodeCheck := hashids.encode_list(v_ret, p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 666 | IF (v_encodeCheck <> p_hash) then 667 | raise notice 'hashids.encodeList(%): % <> %', v_ret, v_encodeCheck, p_hash; 668 | return ARRAY[]::bigint[]; 669 | end if; 670 | end if; 671 | 672 | RETURN v_ret; 673 | END; 674 | $$ 675 | LANGUAGE plpgsql IMMUTABLE 676 | COST 300; 677 | 678 | 679 | CREATE OR REPLACE FUNCTION hashids.decode( in p_hash text ) 680 | RETURNS bigint[] AS 681 | $$ 682 | DECLARE 683 | p_numbers ALIAS for $1; 684 | p_salt text := ''; -- default 685 | p_min_hash_length integer := 0; -- default 686 | p_alphabet text := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; -- default 687 | p_zero_offset boolean := true ; -- adding an offset so that this can work with values from a zero based array language 688 | BEGIN 689 | RETURN hashids.decode(p_hash, p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 690 | END; 691 | $$ 692 | LANGUAGE plpgsql IMMUTABLE 693 | COST 300; 694 | 695 | CREATE OR REPLACE FUNCTION hashids.decode( 696 | in p_hash text, 697 | in p_salt text) 698 | RETURNS text AS 699 | $$ 700 | DECLARE 701 | p_numbers ALIAS for $1; 702 | p_salt ALIAS for $2; -- default 703 | p_min_hash_length integer := 0; -- default 704 | p_alphabet text := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; -- default 705 | p_zero_offset boolean := true ; -- adding an offset so that this can work with values from a zero based array language 706 | BEGIN 707 | RETURN hashids.decode(p_hash, p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 708 | END; 709 | $$ 710 | LANGUAGE plpgsql IMMUTABLE 711 | COST 300; 712 | 713 | CREATE OR REPLACE FUNCTION hashids.decode( 714 | in p_hash text, 715 | in p_salt text, 716 | in p_min_hash_length integer) 717 | RETURNS bigint[] AS 718 | $$ 719 | DECLARE 720 | p_numbers ALIAS for $1; 721 | p_salt ALIAS for $2; -- default 722 | p_min_hash_length ALIAS for $3; -- default 723 | p_alphabet text := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; -- default 724 | p_zero_offset boolean := true ; -- adding an offset so that this can work with values from a zero based array language 725 | BEGIN 726 | RETURN hashids.decode(p_hash, p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 727 | END; 728 | $$ 729 | LANGUAGE plpgsql IMMUTABLE 730 | COST 300; 731 | 732 | CREATE OR REPLACE FUNCTION hashids.decode( 733 | in p_hash text, 734 | in p_salt text, 735 | in p_min_hash_length integer, 736 | in p_alphabet text) 737 | RETURNS bigint[] AS 738 | $$ 739 | DECLARE 740 | p_numbers ALIAS for $1; 741 | p_salt ALIAS for $2; -- default 742 | p_min_hash_length ALIAS for $3; -- default 743 | p_alphabet ALIAS for $4; -- default 744 | p_zero_offset boolean := true ; -- adding an offset so that this can work with values from a zero based array language 745 | BEGIN 746 | RETURN hashids.decode(p_hash, p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 747 | END; 748 | $$ 749 | LANGUAGE plpgsql IMMUTABLE 750 | COST 300; 751 | 752 | CREATE OR REPLACE FUNCTION hashids.decode( 753 | in p_hash text, 754 | in p_salt text, 755 | in p_min_hash_length integer, 756 | in p_alphabet text, 757 | in p_zero_offset boolean DEFAULT true) 758 | RETURNS bigint[] AS 759 | $$ 760 | DECLARE 761 | p_numbers ALIAS for $1; 762 | p_salt ALIAS for $2; -- default 763 | p_min_hash_length ALIAS for $3; -- default 764 | p_alphabet ALIAS for $4; -- default 765 | p_zero_offset ALIAS for $5 ; -- adding an offset so that this can work with values from a zero based array language 766 | BEGIN 767 | RETURN hashids.decode(p_hash, p_salt, p_min_hash_length, p_alphabet, p_zero_offset); 768 | END; 769 | $$ 770 | LANGUAGE plpgsql IMMUTABLE 771 | COST 300; 772 | 773 | 774 | 775 | 776 | 777 | --------------------------------------------------------------------------------