├── .clippy.toml ├── .gitignore ├── CHANGELOG.md ├── .cargo └── config.toml ├── pg_sqids.control ├── rustfmt.toml ├── src ├── utils.rs ├── error.rs └── lib.rs ├── Cargo.toml ├── LICENSE ├── README.md └── .github └── workflows └── build.yml /.clippy.toml: -------------------------------------------------------------------------------- 1 | too-many-arguments-threshold = 10 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | /target 4 | *.iml 5 | **/*.rs.bk 6 | Cargo.lock 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | **v0.1.0:** 4 | - Initial implementation of [the spec](https://github.com/sqids/sqids-spec) -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(target_os="macos")'] 2 | # Postgres symbols won't be available until runtime 3 | rustflags = ["-Clink-arg=-Wl,-undefined,dynamic_lookup"] 4 | -------------------------------------------------------------------------------- /pg_sqids.control: -------------------------------------------------------------------------------- 1 | comment = 'pg_sqids: Created by pgrx' 2 | default_version = '@CARGO_VERSION@' 3 | module_pathname = '$libdir/pg_sqids' 4 | relocatable = false 5 | superuser = true 6 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | comment_width = 100 3 | hard_tabs = true 4 | edition = "2021" 5 | reorder_imports = true 6 | imports_granularity = "Crate" 7 | use_small_heuristics = "Max" 8 | wrap_comments = true 9 | binop_separator = "Back" 10 | trailing_comma = "Vertical" 11 | trailing_semicolon = true 12 | use_field_init_shorthand = true 13 | format_macro_bodies = true 14 | format_code_in_doc_comments = true 15 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use pgrx::VariadicArray; 2 | 3 | use crate::error::{Error, PgError}; 4 | 5 | pub fn process_min_length(min_length: i16) -> Result { 6 | if !(0..=255).contains(&min_length) { 7 | return Err(PgError::CustomError(Error::MinLengthRange)); 8 | } 9 | 10 | Ok(min_length as u8) 11 | } 12 | 13 | pub fn process_numbers(numbers: VariadicArray) -> Result, PgError> { 14 | numbers 15 | .into_iter() 16 | .map(|n| match n.unwrap_or(0) { 17 | n if n < 0 => Err(PgError::CustomError(Error::NegativeNumbers)), 18 | n => Ok(n as u64), 19 | }) 20 | .collect() 21 | } 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pg_sqids" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [features] 10 | default = ["pg13"] 11 | pg13 = ["pgrx/pg13", "pgrx-tests/pg13" ] 12 | pg14 = ["pgrx/pg14", "pgrx-tests/pg14" ] 13 | pg15 = ["pgrx/pg15", "pgrx-tests/pg15" ] 14 | pg16 = ["pgrx/pg16", "pgrx-tests/pg16" ] 15 | pg_test = [] 16 | 17 | [dependencies] 18 | pgrx = "0.13.1" 19 | sqids = "0.4.2" 20 | thiserror = "2.0.12" 21 | 22 | [dev-dependencies] 23 | pgrx-tests = "0.13.1" 24 | 25 | [profile.dev] 26 | panic = "unwind" 27 | 28 | [profile.release] 29 | panic = "unwind" 30 | opt-level = 3 31 | lto = "fat" 32 | codegen-units = 1 33 | 34 | [package.metadata.pgrx] 35 | version = "0.1.0" 36 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use pgrx::prelude::*; 2 | use std::fmt; 3 | use thiserror::Error; 4 | 5 | #[derive(Error, Debug, Eq, PartialEq)] 6 | pub enum Error { 7 | #[error("Min length has to be between 0 and 255")] 8 | MinLengthRange, 9 | #[error("Numbers cannot be negative")] 10 | NegativeNumbers, 11 | } 12 | 13 | #[derive(Debug)] 14 | pub enum PgError { 15 | SqidsError(sqids::Error), 16 | CustomError(Error), 17 | } 18 | 19 | impl fmt::Display for PgError { 20 | fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result { 21 | match self { 22 | PgError::SqidsError(err) => error!("{}", err), 23 | PgError::CustomError(err) => error!("{}", err), 24 | } 25 | } 26 | } 27 | 28 | impl From for PgError { 29 | fn from(err: sqids::Error) -> Self { 30 | PgError::SqidsError(err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Sqids PostgreSQL](https://sqids.org/postgresql) 2 | 3 | [![Github Actions](https://img.shields.io/github/actions/workflow/status/sqids/sqids-postgresql/build.yml)](https://github.com/sqids/sqids-postgresql/actions) 4 | 5 | [Sqids](https://sqids.org/postgresql) (*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. 6 | 7 | Features: 8 | 9 | - **Encode multiple numbers** - generate short IDs from one or several non-negative numbers 10 | - **Quick decoding** - easily decode IDs back into numbers 11 | - **Unique IDs** - generate unique IDs by shuffling the alphabet once 12 | - **ID padding** - provide minimum length to make IDs more uniform 13 | - **URL safe** - auto-generated IDs do not contain common profanity 14 | - **Randomized output** - Sequential input provides nonconsecutive IDs 15 | - **Many implementations** - Support for [40+ programming languages](https://sqids.org/) 16 | 17 | ## 🧰 Use-cases 18 | 19 | Good for: 20 | 21 | - Generating IDs for public URLs (eg: link shortening) 22 | - Generating IDs for internal systems (eg: event tracking) 23 | - Decoding for quicker database lookups (eg: by primary keys) 24 | 25 | Not good for: 26 | 27 | - Sensitive data (this is not an encryption library) 28 | - User IDs (can be decoded revealing user count) 29 | 30 | ## 🚀 Getting started 31 | 32 | ### Development 33 | 34 | 1. [Install Rust](https://www.rust-lang.org/) if you don't have it. 35 | 36 | 1. Install [pgrx](https://github.com/pgcentralfoundation/pgrx?tab=readme-ov-file#getting-started): 37 | 38 | ```bash 39 | cargo install --locked cargo-pgrx 40 | ``` 41 | 42 | 1. Install dependencies and run psql session: 43 | 44 | ```bash 45 | cargo build 46 | cargo pgrx run 47 | ``` 48 | 49 | 1. Install extension: 50 | 51 | ```sql 52 | DROP EXTENSION pg_sqids; 53 | CREATE EXTENSION pg_sqids; 54 | ``` 55 | 56 | 1. Run sample query: 57 | 58 | ```sql 59 | SELECT sqids_encode(1, 2, 3); -- 86Rf07 60 | ``` 61 | 62 | ### Installing 63 | 64 | 1. Create the extension: 65 | 66 | ```bash 67 | cargo pgrx package 68 | ``` 69 | 70 | 1. Extension file should be in `target/release` 71 | 72 | - For Linux: `libpg_sqids.so` 73 | - For macOS: `libpg_sqids.dylib` 74 | - For Windows: `pg_sqids.dll` 75 | 76 | 1. Install the extension: 77 | 78 | ```sql 79 | DROP EXTENSION pg_sqids; 80 | CREATE EXTENSION pg_sqids; 81 | ``` 82 | 83 | ## 👩‍💻 Examples 84 | 85 | Simple encode & decode: 86 | 87 | ```sql 88 | SELECT sqids_encode(1, 2, 3); -- 86Rf07 89 | SELECT sqids_decode('86Rf07'); -- {1,2,3} 90 | ``` 91 | 92 | > **Note** 93 | > 🚧 Because of the algorithm's design, **multiple IDs can decode back into the same sequence of numbers**. If it's important to your design that IDs are canonical, you have to manually re-encode decoded numbers and check that the generated ID matches. 94 | 95 | Enforce a *minimum* length for IDs: 96 | 97 | ```sql 98 | SELECT sqids_encode(10::smallint, 1, 2, 3); -- 86Rf07xd4z 99 | SELECT sqids_decode(10::smallint, '86Rf07xd4z'); -- {1,2,3} 100 | ``` 101 | 102 | Randomize IDs by providing a custom alphabet: 103 | 104 | ```sql 105 | SELECT sqids_encode('k3G7QAe51FCsPW92uEOyq4Bg6Sp8YzVTmnU0liwDdHXLajZrfxNhobJIRcMvKt', 1, 2, 3); -- XRKUdQ 106 | SELECT sqids_decode('k3G7QAe51FCsPW92uEOyq4Bg6Sp8YzVTmnU0liwDdHXLajZrfxNhobJIRcMvKt', 'XRKUdQ'); -- {1,2,3} 107 | ``` 108 | 109 | Prevent specific words from appearing anywhere in the auto-generated IDs: 110 | 111 | ```sql 112 | SELECT sqids_encode(array['86Rf07'], 1, 2, 3); -- se8ojk 113 | SELECT sqids_decode(array['86Rf07'], 'se8ojk'); -- {1,2,3} 114 | ``` 115 | 116 | ### Using multiple parameters 117 | 118 | Alphabet + min length: 119 | 120 | ```sql 121 | SELECT sqids_encode('k3G7QAe51FCsPW92uEOyq4Bg6Sp8YzVTmnU0liwDdHXLajZrfxNhobJIRcMvKt', 10::smallint, 1, 2, 3); -- XRKUdQVBzg 122 | SELECT sqids_decode('k3G7QAe51FCsPW92uEOyq4Bg6Sp8YzVTmnU0liwDdHXLajZrfxNhobJIRcMvKt', 10::smallint, 'XRKUdQVBzg'); -- {1,2,3} 123 | ``` 124 | 125 | Alphabet + blocklist: 126 | 127 | ```sql 128 | SELECT sqids_encode('k3G7QAe51FCsPW92uEOyq4Bg6Sp8YzVTmnU0liwDdHXLajZrfxNhobJIRcMvKt', array['XRKUdQ'], 1, 2, 3); -- WyXQfF 129 | SELECT sqids_decode('k3G7QAe51FCsPW92uEOyq4Bg6Sp8YzVTmnU0liwDdHXLajZrfxNhobJIRcMvKt', array['XRKUdQ'], 'WyXQfF'); -- {1,2,3} 130 | ``` 131 | 132 | Min length + blocklist: 133 | 134 | ```sql 135 | SELECT sqids_encode(10::smallint, array['86Rf07'], 1, 2, 3); -- se8ojkCQvX 136 | SELECT sqids_decode(10::smallint, array['86Rf07'], 'se8ojkCQvX'); -- {1,2,3} 137 | ``` 138 | 139 | Alphabet + min length + blocklist: 140 | 141 | ```sql 142 | SELECT sqids_encode('k3G7QAe51FCsPW92uEOyq4Bg6Sp8YzVTmnU0liwDdHXLajZrfxNhobJIRcMvKt', 10::smallint, array['XRKUdQVBzg'], 1, 2, 3); -- WyXQfFQ21T 143 | SELECT sqids_decode('k3G7QAe51FCsPW92uEOyq4Bg6Sp8YzVTmnU0liwDdHXLajZrfxNhobJIRcMvKt', 10::smallint, array['XRKUdQVBzg'], 'WyXQfFQ21T'); -- {1,2,3} 144 | ``` 145 | 146 | ## 📝 License 147 | 148 | [MIT](LICENSE) 149 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | strategy: 6 | matrix: 7 | os: [ubuntu-latest, macos-latest, windows-latest] 8 | include: 9 | - os: ubuntu-latest 10 | pg-version: 15 11 | - os: macos-latest 12 | pg-version: 15 13 | - os: windows-latest 14 | pg-version: 15 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - name: Check out 18 | uses: actions/checkout@v3 19 | 20 | - name: Install Rust 21 | uses: actions-rs/toolchain@v1 22 | with: 23 | profile: minimal 24 | toolchain: stable 25 | override: true 26 | components: rustfmt, clippy 27 | 28 | # Ubuntu PostgreSQL setup 29 | - name: Install dependencies (Ubuntu) 30 | if: runner.os == 'Linux' 31 | run: | 32 | sudo apt-get update 33 | sudo apt-get install -y pkg-config libssl-dev 34 | sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' 35 | wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - 36 | sudo apt-get update 37 | sudo apt-get install -y postgresql-${{ matrix.pg-version }} postgresql-server-dev-${{ matrix.pg-version }} 38 | 39 | - name: Install dependencies (macOS) 40 | if: runner.os == 'macOS' 41 | run: | 42 | brew install git icu4c pkg-config postgresql@${{ matrix.pg-version }} 43 | echo "/opt/homebrew/opt/postgresql@${{ matrix.pg-version }}/bin" >> $GITHUB_PATH 44 | 45 | - name: Install PostgreSQL (Windows) 46 | if: runner.os == 'Windows' 47 | run: | 48 | choco install postgresql${{ matrix.pg-version }} --params '/Password:postgres' -y 49 | echo "C:\Program Files\PostgreSQL\${{ matrix.pg-version }}\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append 50 | echo "C:\Program Files\PostgreSQL\${{ matrix.pg-version }}\lib" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append 51 | 52 | - name: Cache Rust dependencies 53 | uses: actions/cache@v3 54 | with: 55 | path: | 56 | ~/.cargo/registry 57 | ~/.cargo/git 58 | target 59 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 60 | restore-keys: | 61 | ${{ runner.os }}-cargo- 62 | 63 | - name: Cache pgrx 64 | uses: actions/cache@v3 65 | with: 66 | path: ~/.pgrx 67 | key: ${{ runner.os }}-pgrx-pg${{ matrix.pg-version }}-${{ hashFiles('**/Cargo.lock') }} 68 | restore-keys: | 69 | ${{ runner.os }}-pgrx-pg${{ matrix.pg-version }}- 70 | 71 | - name: Debug pg_config (macOS) 72 | if: runner.os == 'macOS' 73 | run: | 74 | which pg_config || echo "pg_config not found" 75 | find /opt/homebrew -name pg_config | grep pg_config || echo "pg_config not found in homebrew" 76 | brew info postgresql@${{ matrix.pg-version }} 77 | 78 | - name: Install pgrx (macOS) 79 | if: runner.os == 'macOS' 80 | run: | 81 | cargo install --locked cargo-pgrx --version 0.13.1 || true 82 | PG_CONFIG_PATH=$(find /opt/homebrew -name pg_config | grep "@${{ matrix.pg-version }}" | head -n 1) 83 | echo "Found pg_config at: $PG_CONFIG_PATH" 84 | 85 | # First do a clean init 86 | cargo pgrx init --control-file 87 | 88 | # Then explicitly register PostgreSQL 15 89 | cargo pgrx init --pg${{ matrix.pg-version }} $PG_CONFIG_PATH 90 | env: 91 | CARGO_HTTP_TIMEOUT: 300 92 | PKG_CONFIG_PATH: /opt/homebrew/opt/icu4c/lib/pkgconfig:/opt/homebrew/opt/postgresql@${{ matrix.pg-version }}/lib/pkgconfig 93 | 94 | - name: Install pgrx (Linux/Windows) 95 | if: runner.os != 'macOS' 96 | run: | 97 | cargo install --locked cargo-pgrx --version 0.13.1 || true 98 | cargo pgrx init 99 | env: 100 | CARGO_HTTP_TIMEOUT: 300 101 | 102 | - name: Lint 103 | run: | 104 | cargo fmt --all -- --check 105 | cargo clippy --all -- -D warnings 106 | 107 | - name: Build Extension (Windows) 108 | if: runner.os == 'Windows' 109 | run: | 110 | cargo pgrx package --pg-config "C:\Program Files\PostgreSQL\${{ matrix.pg-version }}\bin\pg_config.exe" --target x86_64-pc-windows-msvc 111 | 112 | - name: Build Extension (macOS) 113 | if: runner.os == 'macOS' 114 | run: | 115 | PG_CONFIG_PATH=$(find /opt/homebrew -name pg_config | grep "@${{ matrix.pg-version }}" | head -n 1) 116 | cargo pgrx package --pg-config $PG_CONFIG_PATH 117 | 118 | - name: Build Extension (Linux) 119 | if: runner.os == 'Linux' 120 | run: cargo pgrx package --pg-config $(which pg_config) 121 | 122 | - name: Upload Extension Artifact (Linux) 123 | if: runner.os == 'Linux' 124 | uses: actions/upload-artifact@v4 125 | with: 126 | name: pg_sqids-${{ runner.os }} 127 | path: target/release/pg_sqids-pg${{ matrix.pg-version }}/lib/pg_sqids.so 128 | 129 | - name: Upload Extension Artifact (macOS) 130 | if: runner.os == 'macOS' 131 | uses: actions/upload-artifact@v4 132 | with: 133 | name: pg_sqids-${{ runner.os }} 134 | path: target/release/pg_sqids-pg${{ matrix.pg-version }}/lib/pg_sqids.dylib 135 | 136 | - name: Upload Extension Artifact (Windows) 137 | if: runner.os == 'Windows' 138 | uses: actions/upload-artifact@v4 139 | with: 140 | name: pg_sqids-${{ runner.os }} 141 | path: target/release/pg_sqids-pg${{ matrix.pg-version }}/lib/pg_sqids.dll 142 | 143 | - name: Test 144 | run: cargo test --all 145 | 146 | check: 147 | runs-on: ubuntu-latest 148 | steps: 149 | - name: Check out 150 | uses: actions/checkout@v3 151 | 152 | - name: Install Rust 153 | uses: actions-rs/toolchain@v1 154 | with: 155 | profile: minimal 156 | toolchain: stable 157 | override: true 158 | 159 | - name: Cache Rust dependencies 160 | uses: actions/cache@v3 161 | with: 162 | path: | 163 | ~/.cargo/registry 164 | ~/.cargo/git 165 | target 166 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 167 | restore-keys: | 168 | ${{ runner.os }}-cargo- -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use pgrx::prelude::*; 2 | use sqids::Sqids; 3 | 4 | use error::PgError; 5 | 6 | mod error; 7 | mod utils; 8 | 9 | pgrx::pg_module_magic!(); 10 | 11 | #[pg_extern(name = "sqids_encode")] 12 | fn sqids_encode(numbers: VariadicArray) -> Result { 13 | let sqids = Sqids::builder().build().map_err(PgError::SqidsError)?; 14 | 15 | sqids.encode(&utils::process_numbers(numbers)?).map_err(PgError::SqidsError) 16 | } 17 | 18 | #[pg_extern(name = "sqids_encode")] 19 | fn sqids_encode_with_alphabet( 20 | alphabet: &str, 21 | numbers: VariadicArray, 22 | ) -> Result { 23 | let sqids = Sqids::builder() 24 | .alphabet(alphabet.chars().collect()) 25 | .build() 26 | .map_err(PgError::SqidsError)?; 27 | 28 | sqids.encode(&utils::process_numbers(numbers)?).map_err(PgError::SqidsError) 29 | } 30 | 31 | #[pg_extern(name = "sqids_encode")] 32 | fn sqids_encode_with_min_length( 33 | min_length: i16, 34 | numbers: VariadicArray, 35 | ) -> Result { 36 | let sqids = Sqids::builder() 37 | .min_length(utils::process_min_length(min_length)?) 38 | .build() 39 | .map_err(PgError::SqidsError)?; 40 | 41 | sqids.encode(&utils::process_numbers(numbers)?).map_err(PgError::SqidsError) 42 | } 43 | 44 | #[pg_extern(name = "sqids_encode")] 45 | fn sqids_encode_with_blocklist( 46 | blocklist: Array, 47 | numbers: VariadicArray, 48 | ) -> Result { 49 | let sqids = Sqids::builder() 50 | .blocklist(blocklist.iter().flatten().collect()) 51 | .build() 52 | .map_err(PgError::SqidsError)?; 53 | 54 | sqids.encode(&utils::process_numbers(numbers)?).map_err(PgError::SqidsError) 55 | } 56 | 57 | #[pg_extern(name = "sqids_encode")] 58 | fn sqids_encode_with_alphabet_min_length( 59 | alphabet: &str, 60 | min_length: i16, 61 | numbers: VariadicArray, 62 | ) -> Result { 63 | let sqids = Sqids::builder() 64 | .alphabet(alphabet.chars().collect()) 65 | .min_length(utils::process_min_length(min_length)?) 66 | .build() 67 | .map_err(PgError::SqidsError)?; 68 | 69 | sqids.encode(&utils::process_numbers(numbers)?).map_err(PgError::SqidsError) 70 | } 71 | 72 | #[pg_extern(name = "sqids_encode")] 73 | fn sqids_encode_with_alphabet_blocklist( 74 | alphabet: &str, 75 | blocklist: Array, 76 | numbers: VariadicArray, 77 | ) -> Result { 78 | let sqids = Sqids::builder() 79 | .alphabet(alphabet.chars().collect()) 80 | .blocklist(blocklist.iter().flatten().collect()) 81 | .build() 82 | .map_err(PgError::SqidsError)?; 83 | 84 | sqids.encode(&utils::process_numbers(numbers)?).map_err(PgError::SqidsError) 85 | } 86 | 87 | #[pg_extern(name = "sqids_encode")] 88 | fn sqids_encode_with_min_length_blocklist( 89 | min_length: i16, 90 | blocklist: Array, 91 | numbers: VariadicArray, 92 | ) -> Result { 93 | let sqids = Sqids::builder() 94 | .min_length(utils::process_min_length(min_length)?) 95 | .blocklist(blocklist.iter().flatten().collect()) 96 | .build() 97 | .map_err(PgError::SqidsError)?; 98 | 99 | sqids.encode(&utils::process_numbers(numbers)?).map_err(PgError::SqidsError) 100 | } 101 | 102 | #[pg_extern(name = "sqids_encode")] 103 | fn sqids_encode_with_alphabet_min_length_blocklist( 104 | alphabet: &str, 105 | min_length: i16, 106 | blocklist: Array, 107 | numbers: VariadicArray, 108 | ) -> Result { 109 | let sqids = Sqids::builder() 110 | .alphabet(alphabet.chars().collect()) 111 | .min_length(utils::process_min_length(min_length)?) 112 | .blocklist(blocklist.iter().flatten().collect()) 113 | .build() 114 | .map_err(PgError::SqidsError)?; 115 | 116 | sqids.encode(&utils::process_numbers(numbers)?).map_err(PgError::SqidsError) 117 | } 118 | 119 | #[pg_extern(name = "sqids_decode")] 120 | fn sqids_decode(id: &str) -> Result, PgError> { 121 | let sqids = Sqids::builder().build().map_err(PgError::SqidsError)?; 122 | 123 | Ok(sqids.decode(id).iter().map(|n| *n as i64).collect()) 124 | } 125 | 126 | #[pg_extern(name = "sqids_decode")] 127 | fn sqids_decode_with_alphabet(alphabet: &str, id: &str) -> Result, PgError> { 128 | let sqids = Sqids::builder() 129 | .alphabet(alphabet.chars().collect()) 130 | .build() 131 | .map_err(PgError::SqidsError)?; 132 | 133 | Ok(sqids.decode(id).iter().map(|n| *n as i64).collect()) 134 | } 135 | 136 | #[pg_extern(name = "sqids_decode")] 137 | fn sqids_decode_with_min_length(min_length: i16, id: &str) -> Result, PgError> { 138 | let sqids = Sqids::builder() 139 | .min_length(utils::process_min_length(min_length)?) 140 | .build() 141 | .map_err(PgError::SqidsError)?; 142 | 143 | Ok(sqids.decode(id).iter().map(|n| *n as i64).collect()) 144 | } 145 | 146 | #[pg_extern(name = "sqids_decode")] 147 | fn sqids_decode_with_blocklist(blocklist: Array, id: &str) -> Result, PgError> { 148 | let sqids = Sqids::builder() 149 | .blocklist(blocklist.iter().flatten().collect()) 150 | .build() 151 | .map_err(PgError::SqidsError)?; 152 | 153 | Ok(sqids.decode(id).iter().map(|n| *n as i64).collect()) 154 | } 155 | 156 | #[pg_extern(name = "sqids_decode")] 157 | fn sqids_decode_with_alphabet_min_length( 158 | alphabet: &str, 159 | min_length: i16, 160 | id: &str, 161 | ) -> Result, PgError> { 162 | let sqids = Sqids::builder() 163 | .alphabet(alphabet.chars().collect()) 164 | .min_length(utils::process_min_length(min_length)?) 165 | .build() 166 | .map_err(PgError::SqidsError)?; 167 | 168 | Ok(sqids.decode(id).iter().map(|n| *n as i64).collect()) 169 | } 170 | 171 | #[pg_extern(name = "sqids_decode")] 172 | fn sqids_decode_with_alphabet_blocklist( 173 | alphabet: &str, 174 | blocklist: Array, 175 | id: &str, 176 | ) -> Result, PgError> { 177 | let sqids = Sqids::builder() 178 | .alphabet(alphabet.chars().collect()) 179 | .blocklist(blocklist.iter().flatten().collect()) 180 | .build() 181 | .map_err(PgError::SqidsError)?; 182 | 183 | Ok(sqids.decode(id).iter().map(|n| *n as i64).collect()) 184 | } 185 | 186 | #[pg_extern(name = "sqids_decode")] 187 | fn sqids_decode_with_min_length_blocklist( 188 | min_length: i16, 189 | blocklist: Array, 190 | id: &str, 191 | ) -> Result, PgError> { 192 | let sqids = Sqids::builder() 193 | .min_length(utils::process_min_length(min_length)?) 194 | .blocklist(blocklist.iter().flatten().collect()) 195 | .build() 196 | .map_err(PgError::SqidsError)?; 197 | 198 | Ok(sqids.decode(id).iter().map(|n| *n as i64).collect()) 199 | } 200 | 201 | #[pg_extern(name = "sqids_decode")] 202 | fn sqids_decode_with_alphabet_min_length_blocklist( 203 | alphabet: &str, 204 | min_length: i16, 205 | blocklist: Array, 206 | id: &str, 207 | ) -> Result, PgError> { 208 | let sqids = Sqids::builder() 209 | .alphabet(alphabet.chars().collect()) 210 | .min_length(utils::process_min_length(min_length)?) 211 | .blocklist(blocklist.iter().flatten().collect()) 212 | .build() 213 | .map_err(PgError::SqidsError)?; 214 | 215 | Ok(sqids.decode(id).iter().map(|n| *n as i64).collect()) 216 | } 217 | 218 | #[cfg(any(test, feature = "pg_test"))] 219 | #[pg_schema] 220 | mod tests { 221 | use pgrx::prelude::*; 222 | use std::collections::HashMap; 223 | 224 | // @NOTE since this extension uses `https://crates.io/crates/sqids`, this repo does not include the standard Sqids unit tests (which are included 225 | // in the external Rust library). Therefore, we are testing the SQL functions only. 226 | 227 | #[pg_test] 228 | fn test_sqids_encode() { 229 | let tests: HashMap<&str, Option> = HashMap::from([ 230 | ("SELECT sqids_encode(1, 2, 3)", Some("86Rf07".to_string())), 231 | ("SELECT sqids_encode(10::smallint, 1, 2, 3)", Some("86Rf07xd4z".to_string())), 232 | ("SELECT sqids_encode('k3G7QAe51FCsPW92uEOyq4Bg6Sp8YzVTmnU0liwDdHXLajZrfxNhobJIRcMvKt', 1, 2, 3)", Some("XRKUdQ".to_string())), 233 | ("SELECT sqids_encode(array['86Rf07'], 1, 2, 3)", Some("se8ojk".to_string())), 234 | ("SELECT sqids_encode('k3G7QAe51FCsPW92uEOyq4Bg6Sp8YzVTmnU0liwDdHXLajZrfxNhobJIRcMvKt', 10::smallint, 1, 2, 3)", Some("XRKUdQVBzg".to_string())), 235 | ("SELECT sqids_encode('k3G7QAe51FCsPW92uEOyq4Bg6Sp8YzVTmnU0liwDdHXLajZrfxNhobJIRcMvKt', array['XRKUdQ'], 1, 2, 3)", Some("WyXQfF".to_string())), 236 | ("SELECT sqids_encode(10::smallint, array['86Rf07'], 1, 2, 3)", Some("se8ojkCQvX".to_string())), 237 | ("SELECT sqids_encode('k3G7QAe51FCsPW92uEOyq4Bg6Sp8YzVTmnU0liwDdHXLajZrfxNhobJIRcMvKt', 10::smallint, array['XRKUdQVBzg'], 1, 2, 3)", Some("WyXQfFQ21T".to_string())), 238 | ]); 239 | 240 | for (sql, expected) in tests { 241 | let result = Spi::get_one(sql).expect("Query failed"); 242 | assert_eq!(result, expected, "Failed on query: {}", sql); 243 | } 244 | } 245 | 246 | #[pg_test] 247 | fn test_sqids_decode() { 248 | let tests: HashMap<&str, Option>> = HashMap::from([ 249 | ("SELECT sqids_decode('86Rf07')", Some(vec![1, 2, 3])), 250 | ("SELECT sqids_decode(10::smallint, '86Rf07xd4z')", Some(vec![1, 2, 3])), 251 | ("SELECT sqids_decode('k3G7QAe51FCsPW92uEOyq4Bg6Sp8YzVTmnU0liwDdHXLajZrfxNhobJIRcMvKt', 'XRKUdQ')", Some(vec![1, 2, 3])), 252 | ("SELECT sqids_decode(array['86Rf07'], 'se8ojk')", Some(vec![1, 2, 3])), 253 | ("SELECT sqids_decode('k3G7QAe51FCsPW92uEOyq4Bg6Sp8YzVTmnU0liwDdHXLajZrfxNhobJIRcMvKt', 10::smallint, 'XRKUdQVBzg')", Some(vec![1, 2, 3])), 254 | ("SELECT sqids_decode('k3G7QAe51FCsPW92uEOyq4Bg6Sp8YzVTmnU0liwDdHXLajZrfxNhobJIRcMvKt', array['XRKUdQ'], 'WyXQfF')", Some(vec![1, 2, 3])), 255 | ("SELECT sqids_decode(10::smallint, array['86Rf07'], 'se8ojkCQvX')", Some(vec![1, 2, 3])), 256 | ("SELECT sqids_decode('k3G7QAe51FCsPW92uEOyq4Bg6Sp8YzVTmnU0liwDdHXLajZrfxNhobJIRcMvKt', 10::smallint, array['XRKUdQVBzg'], 'WyXQfFQ21T')", Some(vec![1, 2, 3])), 257 | ]); 258 | 259 | for (sql, expected) in tests { 260 | let result = Spi::get_one(sql).expect("Query failed"); 261 | assert_eq!(result, expected, "Failed on query: {}", sql); 262 | } 263 | } 264 | } 265 | 266 | #[cfg(test)] 267 | pub mod pg_test { 268 | pub fn setup(_options: Vec<&str>) {} 269 | 270 | pub fn postgresql_conf_options() -> Vec<&'static str> { 271 | vec![] 272 | } 273 | } 274 | --------------------------------------------------------------------------------