diff --git a/.cargo/config b/.cargo/config deleted file mode 100644 index 2b25fcd..0000000 --- a/.cargo/config +++ /dev/null @@ -1,3 +0,0 @@ -[build] -# Postgres symbols won't be available until runtime -rustflags = ["-C", "link-args=-Wl,-undefined,dynamic_lookup"] diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..13c456b --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ +[target.'cfg(target_os="macos")'] +# Postgres symbols won't be available until runtime +rustflags = ["-Clink-arg=-Wl,-undefined,dynamic_lookup"] diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b4efeae --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.git +.github +.gitignore +*.md +*.yml + +target +Cargo.lock \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..512dce1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +name: CI +on: + pull_request: + branches: [main, rewrite] + types: [opened, reopened, synchronize] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + ci: + name: CI + needs: [test, lint, lockfile] + runs-on: ubuntu-latest + steps: + - name: Done + run: exit 0 + test: + name: Tests + strategy: + fail-fast: false + matrix: + postgres: [14, 15, 16] + runner: + - ubuntu-22.04 + - buildjet-8vcpu-ubuntu-2204-arm + runs-on: ${{ matrix.runner }} + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Build + run: docker buildx build --build-arg PG_MAJOR=${{ matrix.postgres }} -t test . + - name: Test + run: docker run test cargo pgrx test pg${{ matrix.postgres }} + lint: + name: Linting (fmt + clippy) + runs-on: ubuntu-latest + steps: + - name: Install rust + uses: dtolnay/rust-toolchain@1.74.0 + with: + components: rustfmt, clippy + - name: Checkout + uses: actions/checkout@v3 + - name: Format check + uses: actions-rs/cargo@v1 + with: + command: fmt + args: -- --check + + lockfile: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install rust + uses: dtolnay/rust-toolchain@1.74.0 + - name: Lockfile check + run: cargo update -w --locked \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..57db33c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,120 @@ +env: + NAME: uids_postgres + EXT_NAME: ulid + PKG_NAME: uids_postgres +name: Release +on: + push: + tags: [v*] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} +jobs: + create-release: + name: Create release + runs-on: ubuntu-latest + outputs: + upload_url: ${{ steps.create-release.outputs.upload_url }} + steps: + - name: Create Release + id: create-release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ github.token }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }} + build-linux-gnu: + name: Build & Release for linux + needs: + - create-release + strategy: + fail-fast: false + matrix: + postgres: [14, 15, 16] + box: + - runner: ubuntu-22.04 + arch: amd64 + - runner: buildjet-8vcpu-ubuntu-2204-arm + arch: arm64 + runs-on: ${{ matrix.box.runner }} + timeout-minutes: 45 + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install rust + uses: dtolnay/rust-toolchain@1.74.0 + - name: Install dependencies + run: | + # Add postgres package repo + sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' + wget -qO- https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo tee /etc/apt/trusted.gpg.d/pgdg.asc &>/dev/null + + sudo apt-get update + sudo apt-get install -y --no-install-recommends git build-essential libpq-dev curl libreadline6-dev zlib1g-dev pkg-config cmake + sudo apt-get install -y --no-install-recommends libreadline-dev zlib1g-dev flex bison libxml2-dev libxslt-dev libssl-dev libxml2-utils xsltproc ccache + sudo apt-get install -y --no-install-recommends clang libclang-dev llvm-dev gcc tree + + # Install requested postgres version + sudo apt-get install -y postgresql-${{ matrix.postgres }} postgresql-server-dev-${{ matrix.postgres }} -y + + # Ensure installed pg_config is first on path + export PATH=$PATH:/usr/lib/postgresql/${{ matrix.postgres }}/bin + + cargo install cargo-pgrx --version 0.11.2 --locked + cargo pgrx init --pg${{ matrix.postgres }}=/usr/lib/postgresql/${{ matrix.postgres }}/bin/pg_config + - name: Build artifacts + run: | + # selects the pgVer from pg_config on path + # https://github.com/tcdi/pgrx/issues/288 + cargo pgrx package --no-default-features --features pg${{ matrix.postgres }} + + # Create installable package + mkdir archive + cp `find target/release -type f -name "${{ env.EXT_NAME }}*"` archive + + # Copy files into directory structure + mkdir -p package/usr/lib/postgresql/lib + mkdir -p package/var/lib/postgresql/extension + cp archive/*.so package/usr/lib/postgresql/lib + cp archive/*.control package/var/lib/postgresql/extension + cp archive/*.sql package/var/lib/postgresql/extension + + # symlinks to Copy files into directory structure + mkdir -p package/usr/lib/postgresql/${{ matrix.postgres }}/lib + cd package/usr/lib/postgresql/${{ matrix.postgres }}/lib + cp -s ../../lib/*.so . + cd ../../../../../.. + + mkdir -p package/usr/share/postgresql/${{ matrix.postgres }}/extension + cd package/usr/share/postgresql/${{ matrix.postgres }}/extension + + cp -s ../../../../../var/lib/postgresql/extension/${{ env.EXT_NAME }}.control . + cp -s ../../../../../var/lib/postgresql/extension/${{ env.EXT_NAME }}*.sql . + cd ../../../../../.. + + # Create install control file + extension_version=${{ github.ref_name }} + # strip the leading v + deb_version=${extension_version:1} + + mkdir -p package/DEBIAN + touch package/DEBIAN/control + echo 'Package: ${{ env.PKG_NAME }}' >> package/DEBIAN/control + echo 'Version:' ${deb_version} >> package/DEBIAN/control + echo 'Architecture: ${{ matrix.box.arch }}' >> package/DEBIAN/control + echo 'Maintainer: Pavan Sunkara' >> package/DEBIAN/control + echo 'Description: A PostgreSQL extension for ULID' >> package/DEBIAN/control + + # Create deb package + sudo chown -R root:root package + sudo chmod -R 00755 package + sudo dpkg-deb -Zxz --build --root-owner-group package + - name: Upload artifacts + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ github.token }} + with: + upload_url: ${{ needs.create-release.outputs.upload_url }} + asset_path: ./package.deb + asset_name: ${{ env.NAME }}-${{ github.ref_name }}-pg${{ matrix.postgres }}-${{ matrix.box.arch }}-linux-gnu.deb + asset_content_type: application/vnd.debian.binary-package \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 1ff8bf7..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "conventionalCommits.scopes": [ - "main", - "readme", - "nanoid" - ] -} diff --git a/Cargo.toml b/Cargo.toml index fa645de..e4b7f82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,33 +1,45 @@ [package] name = "uids" -version = "0.2.0" +version = "0.0.0" edition = "2021" -description = "PostgreSQL Extension to generate various type of Unique IDS." [lib] crate-type = ["cdylib"] [features] -default = ["pg13"] -pg10 = ["pgx/pg10", "pgx-tests/pg10"] -pg11 = ["pgx/pg11", "pgx-tests/pg11"] -pg12 = ["pgx/pg12", "pgx-tests/pg12"] -pg13 = ["pgx/pg13", "pgx-tests/pg13"] -pg14 = ["pgx/pg14", "pgx-tests/pg14"] +default = ["pg16"] +pg11 = ["pgrx/pg11", "pgrx-tests/pg11"] +pg12 = ["pgrx/pg12", "pgrx-tests/pg12"] +pg13 = ["pgrx/pg13", "pgrx-tests/pg13"] +pg14 = ["pgrx/pg14", "pgrx-tests/pg14"] +pg15 = ["pgrx/pg15", "pgrx-tests/pg15"] +pg16 = ["pgrx/pg16", "pgrx-tests/pg16"] pg_test = [] [dependencies] nanoid = "0.4.0" -pgx = "0.4.5" -rusty_ulid = "1.0.0" -svix-ksuid = "0.6.0" +pgrx = "=0.11.4" +rusty_ulid = "2.0.0" +svix-ksuid = "0.8.0" +type-safe-id = "0.3.0" +uuid = { version = "1.8.0", features = [ + "v7", + "v4", + "v6", + "v8", + "fast-rng", + "macro-diagnostics", +] } +getrandom = "0.2" +cuid2 = "0.1.2" +timeflake-rs = "0.3.0" +pushid = "0.0.1" [dev-dependencies] -pgx-tests = "0.4.5" +pgrx-tests = "=0.11.4" [profile.dev] panic = "unwind" -lto = "thin" [profile.release] panic = "unwind" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b4b92de --- /dev/null +++ b/Dockerfile @@ -0,0 +1,51 @@ +ARG PG_MAJOR + +FROM postgres:${PG_MAJOR} + +RUN apt-get update + +ENV build_deps ca-certificates \ + git \ + build-essential \ + libpq-dev \ + postgresql-server-dev-${PG_MAJOR} \ + curl \ + libreadline6-dev \ + zlib1g-dev + +RUN apt-get install -y --no-install-recommends $build_deps pkg-config cmake + +WORKDIR /home/postgres + +ENV HOME=/home/postgres +ENV PATH=/home/postgres/.cargo/bin:$PATH + +RUN chown postgres:postgres /home/postgres + +USER postgres + +RUN \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile minimal --default-toolchain 1.74.0 && \ + rustup --version && \ + rustc --version && \ + cargo --version + +# pgrx +RUN cargo install cargo-pgrx --version 0.11.2 --locked + +RUN cargo pgrx init --pg${PG_MAJOR} $(which pg_config) + +USER root + +COPY . . + +RUN cargo pgrx install + +RUN chown -R postgres:postgres /home/postgres +RUN chown -R postgres:postgres /usr/share/postgresql/${PG_MAJOR}/extension +RUN chown -R postgres:postgres /usr/lib/postgresql/${PG_MAJOR}/lib + +USER postgres + +ENV POSTGRES_HOST_AUTH_METHOD=trust +ENV USER=postgres \ No newline at end of file diff --git a/README.md b/README.md index 7608897..2e9c1fe 100644 --- a/README.md +++ b/README.md @@ -1,152 +1,449 @@ -# PostgreSQL Extension to generate various type of Unique IDS. +Here's the updated README with the corrected links in the table: -## 1. Supported IDs +# PostgreSQL Extension for Generating Unique IDs -1. [NanoId](https://github.com/ai/nanoid) +

+ uids +

-2. [Ksuid](https://github.com/segmentio/ksuid) +```sql +postgres=# CREATE EXTENSION uids; +CREATE EXTENSION +postgres=# SELECT generate_typeid('user'); + generate_typeid +-------------------------------------- + user_01h2xcejqtf2nbrexx3vqjhp41 +``` + +## Description + +| Methodology | Function | Crate | Description | +|---------------------------|---------------------------------------------|--------------------------------------|----------------------------------------------------------| +| [UUID v6][uuidv6] | `generate_uuidv6()` | [`uuid`][crate-uuid] | UUID v6 ([RFC 4122][rfc-4122-update]) | +| | `generate_uuidv6_text()` | | UUID v6 as text | +| | `generate_uuidv6_uuid()` | | UUID v6 as Postgres UUID object | +| [UUID v7][uuidv7] | `generate_uuidv7()` | [`uuid`][crate-uuid] | UUID v7 ([RFC 4122][rfc-4122-update]) | +| | `generate_uuidv7_bytes()` | | UUID v7 as bytes | +| | `generate_uuidv7_from_string(TEXT)` | | Generate UUID v7 from a string | +| | `parse_uuidv7(TEXT)` | | Parse UUID v7 | +| [NanoId][nanoid] | `generate_nanoid()` | [`nanoid`][crate-nanoid] | NanoID, developed by [Andrey Sitnik][github-ai] | +| | `generate_nanoid_length(INT)` | | NanoID with a custom length | +| | `generate_nanoid_c(TEXT)` | | NanoID with custom alphabets | +| | `generate_nanoid_length_c(INT, TEXT)` | | NanoID with custom length and alphabets | +| [Ksuid][ksuid] | `generate_ksuid()` | [`svix-ksuid`][crate-svix-ksuid] | Created by [Segment][segment] | +| | `generate_ksuid_bytes()` | | KSUID as bytes | +| [Ulid][ulid] | `generate_ulid()` | [`ulid`][crate-ulid] | Unique, lexicographically sortable identifiers | +| | `generate_ulid_bytes()` | | ULID as bytes | +| | `generate_ulid_from_string(TEXT)` | | Generate ULID from a string | +| [Timeflake][timeflake] | `generate_timeflake()` | [`timeflake-rs`][crate-timeflake-rs] | Twitter's Snowflake + Instagram's ID + Firebase's PushID | +| | `generate_timeflake_bytes()` | | Timeflake as bytes | +| | `generate_timeflake_uuid()` | | Timeflake as UUID | +| [PushId][pushid] | `generate_pushid()` | [`pushid`][crate-pushid] | Google Firebase's PushID | +| | `generate_pushid_text()` | | PushID as text | +| [Cuid2][cuid2] | `generate_cuid2()` | [`cuid2`][crate-cuid2] | CUID2 | +| | `check_cuid2(TEXT)` | | Check if a string is a valid CUID2 | +| [TypeId][typeid] | `generate_typeid(TEXT)` | [`typeid`][crate-typeid] | Generate TypeId with a specific prefix | +| | `check_typeid(TEXT, TEXT)` | | Check if a TypeId matches a specific prefix | + +This Postgres extension is made possible thanks to [`pgrx`][pgrx]. + +## Supported IDs + +1. [NanoId](https://github.com/ai/nanoid) +2. [Ksuid](https://github.com/segmentio/ksuid) 3. [Ulid](https://github.com/ulid/spec) +4. [TypeId](https://github.com/jetpack-io/typeid) +5. [UUIDv7](https://github.com/uuid-rs/uuid) +6. [Cuid2](https://github.com/paralleldrive/cuid2) +7. [PushId](https://github.com/firebase/firebase-js-sdk) +8. [Timeflake](https://github.com/anthonynsimon/timeflake) +9. [UUIDv6](https://github.com/uuid-rs/uuid) -## 2. Installation +[crate-uuid]: https://crates.io/crates/uuid +[crate-nanoid]: https://crates.io/crates/nanoid +[crate-svix-ksuid]: https://crates.io/crates/svix-ksuid +[crate-ulid]: https://crates.io/crates/ulid +[crate-timeflake-rs]: https://crates.io/crates/timeflake-rs +[crate-pushid]: https://crates.io/crates/pushid +[crate-cuid2]: https://crates.io/crates/cuid2 +[crate-typeid]: https://crates.io/crates/typeid +[postgres]: https://www.postgresql.org/ +[uuidv6]: https://github.com/uuid-rs/uuid +[uuidv7]: https://github.com/uuid-rs/uuid +[nanoid]: https://github.com/ai/nanoid +[ksuid]: https://github.com/segmentio/ksuid +[ulid]: https://github.com/ulid/spec +[timeflake]: https://github.com/anthonynsimon/timeflake +[pushid]: https://github.com/firebase/firebase-js-sdk +[cuid2]: https://github.com/paralleldrive/cuid2 +[typeid]: https://github.com/jetpack-io/typeid +[pgrx]: https://github.com/zombodb/pgx -### 2.1. Installation on non docker environment -2.1.1. Install rust through rustup. -```bash -curl https://sh.rustup.rs -sSf | sh -``` +## Installation -2.1.2. Prepare your postgres installation -2.1.3. Install pgx +### Non-Docker Environment -```bash -cargo install cargo-pgx -``` +1. **Install Rust via rustup:** -2.1.4. Initialize pgx for the postgres version you have already installed + ```bash + curl https://sh.rustup.rs -sSf | sh + ``` -Handle the number accordingly. +2. **Prepare your PostgreSQL installation.** -```bash -cargo pgx init --pg14 $(which pg_config) -``` +3. **Install pgx:** -2.1.5. Install the extension + ```bash + cargo install cargo-pgx + ``` -```bash -git clone https://github.com/spa5k/uids-postgres \ -&& cd uids-postgres \ -&& cargo pgx install -``` +4. **Initialize pgx for your PostgreSQL version:** + + ```bash + cargo pgx init --pg14 $(which pg_config) + ``` + +5. **Clone the repository and install the extension:** -### 2.2 Installation on docker environment + ```bash + git clone https://github.com/spa5k/uids-postgres + cd uids-postgres + cargo pgx install + ``` -Check the included [Dockerfile](./docker/Dockerfile) for the installation template. +### Docker Environment -## 3. Functions available +Refer to the included [Dockerfile](./docker/Dockerfile) for the installation template. -### 3.0. Enable the extension +## Usage + +### Enable the Extension ```sql CREATE EXTENSION IF NOT EXISTS uids; ``` -### 3.1. KSUID - +### Functions -1. Generate a new KSUID +#### KSUID -```sql -select generate_ksuid(); +1. **Generate a new KSUID:** ------------------------------ + ```sql + SELECT generate_ksuid(); + ``` -28KKKI8lpDkK2lHbAdWdgJYoLWF -``` + Example output: -2. Generate a KSUID bytes. + ``` + 28KKKI8lpDkK2lHbAdWdgJYoLWF + ``` -```sql -select generate_ksuid_bytes(); +2. **Generate KSUID bytes:** ------------------------------ + ```sql + SELECT generate_ksuid_bytes(); + ``` -\x0ef557bc9b5b8027f222e2b32ed65e91b6bb8eb6 -``` + Example output: -### 3.2. NanoId - + ``` + \x0ef557bc9b5b8027f222e2b32ed65e91b6bb8eb6 + ``` -1. Generate a new NanoId with default size of 21 +#### NanoId -```sql -select generate_nanoid(); +1. **Generate a new NanoId (default size 21):** ------------------------------ + ```sql + SELECT generate_nanoid(); + ``` -FfuwjZHjS5j5rATHVyl8M -``` + Example output: -2. Generate a NanoId with a custom size + ``` + FfuwjZHjS5j5rATHVyl8M + ``` -```sql -select generate_nanoid_length(10); +2. **Generate a NanoId with a custom size:** ------------------------------ + ```sql + SELECT generate_nanoid_length(10); + ``` -V2D2D7-dnw -``` + Example output: -3. Generate a NanoId with a custom alphabets with length of 21 + ``` + V2D2D7-dnw + ``` -```sql --- Length of the nanoid is first argument, while the alphabets one is second. -select generate_nanoid_c('1234567890abcdef'); +3. **Generate a NanoId with custom alphabets (length 21):** ------------------------------ + ```sql + SELECT generate_nanoid_c('1234567890abcdef'); + ``` -6df80ad84587f4a20838c -``` + Example output: -4. Generate a NanoId with a custom alphabets and custom length + ``` + 6df80ad84587f4a20838c + ``` -```sql --- Length of the nanoid is first argument, while the alphabets one is second. -select generate_nanoid_length_c(10, '1234567890abcdef'); +4. **Generate a NanoId with custom alphabets and custom length:** ------------------------------ + ```sql + SELECT generate_nanoid_length_c(10, '1234567890abcdef'); + ``` -050487bff0 -``` + Example output: -### 3.3. Ulid - + ``` + 050487bff0 + ``` -1. Generate a new Ulid +#### Ulid -```sql -select generate_ulid(); +1. **Generate a new Ulid:** ------------------------------ + ```sql + SELECT generate_ulid(); + ``` -01G1JE4GXWC1A9PXHG0SXQDE1J -``` + Example output: -2. Generate Ulid bytes + ``` + 01G1JE4GXWC1A9PXHG0SXQDE1J + ``` -```sql -select generate_ulid_bytes(); +2. **Generate Ulid bytes:** ------------------------------ + ```sql + SELECT generate_ulid_bytes(); + ``` -\x018064e2bff9e6bb876aa8948e50d9c6 -``` + Example output: -3. Generate Ulid from a custom string + ``` + \x018064e2bff9e6bb876aa8948e50d9c6 + ``` -```sql -select generate_ulid_from_string('01CAT3X5Y5G9A62F1rFA6Tnice'); +3. **Generate Ulid from a custom string:** ------------------------------ + ```sql + SELECT generate_ulid_from_string('01CAT3X5Y5G9A62F1rFA6Tnice'); + ``` -01CAT3X5Y5G9A62F1RFA6TN1CE -``` + Example output: + + ``` + 01CAT3X5Y5G9A62F1RFA6TN1CE + ``` + +#### TypeId + +1. **Generate a TypeId with a specific prefix:** + + ```sql + SELECT generate_typeid('user'); + ``` + + Example output: + + ``` + user_01h2xcejqtf2nbrexx3vqjhp41 + ``` + +2. **Check if a TypeId matches a specific prefix:** + + ```sql + SELECT check_typeid('user', 'user_01h2xcejqtf2nbrexx3vqjhp41'); + ``` + + Example output: + + ``` + true + ``` + +#### UUIDv7 + +1. **Generate a new UUIDv7:** + + ```sql + SELECT generate_uuidv7(); + ``` + + Example output: + + ``` + 01809424-3e59-7c05-9219-566f82fff672 + ``` + +2. **Generate UUIDv7 bytes:** + + ```sql + SELECT generate_uuidv7_bytes(); + ``` + + Example output: + + ``` + \x018094243e597c059219566f82fff672 + ``` + +3. **Generate UUIDv7 from a string:** + + ```sql + SELECT generate_uuidv7_from_string('67e55044-10b1-426f-9247-bb680e5fe0c8'); + ``` + + Example output: + + ``` + 67e55044-10b1-426f-9247-bb680e5fe0c8 + ``` + +4. **Parse UUIDv7:** + + ```sql + SELECT parse_uuidv7('67e55044-10b1-426f-9247-bb680e5fe0c8'); + ``` + + Example output: + + ``` + 67e55044-10b1-426f-9247-bb680e5fe0c8 + ``` + +#### Cuid2 + +1. **Generate a new Cuid2:** + + ```sql + SELECT generate_cuid2(); + ``` + + Example output: + + ``` + cl8f8y8f80000000000000000 + ``` + +2. **Check if a string is a valid Cuid2:** + + ```sql + SELECT check_cuid2('cl8f8y8f80000000000000000'); + ``` + + Example output: + + ``` + true + ``` + +#### PushId + +1. **Generate a new PushId:** + + ```sql + SELECT generate_pushid(); + ``` + + Example output: + + ``` + -MZ1e2f3g4h5i6j7k8l9 + ``` + +2. **Generate a PushId as text:** + + ```sql + SELECT generate_pushid_text(); + ``` + + Example output: + + ``` + -MZ1e2f3g4h5i6j7k8l9 + ``` + +#### Timeflake + +1. **Generate a new Timeflake:** + + ```sql + SELECT generate_timeflake(); + ``` + + Example output: + + ``` + 01F8MECHJ8KZ9Q9J8KZ9Q9J8KZ + ``` + +2. **Generate Timeflake bytes:** + + ```sql + SELECT generate_timeflake_bytes(); + ``` + + Example output: + + ``` + \x018064e2bff9e6bb876aa8948e50d9c6 + ``` + +3. **Generate Timeflake UUID:** + + ```sql + SELECT generate_timeflake_uuid(); + ``` + + Example output: + + ``` + 018064e2-bff9-e6bb-876a-a8948e50d9c6 + ``` + +#### UUIDv6 + +1. **Generate a new UUIDv6:** + + ```sql + SELECT generate_uuidv6(); + ``` + + Example output: + + ``` + 1e4eaa4e-7c4b-6e5d-8a4e-7c4b6e5d8a4e + ``` + +2. **Generate a UUIDv6 as text:** + + ```sql + SELECT generate_uuidv6_text(); + ``` + + Example output: + + ``` + 1e4eaa4e-7c4b-6e5d-8a4e-7c4b6e5d8a4e + ``` + +3. **Generate a UUIDv6 as a Postgres UUID object:** + + ```sql + SELECT generate_uuidv6_uuid(); + ``` + + Example output: + + ``` + 1e4eaa4e-7c4b-6e5d-8a4e-7c4b6e5d8a4e + ``` + +This setup provides a comprehensive set of functions to generate and validate various types of unique identifiers within a PostgreSQL extension using `pgx`. diff --git a/docker/Dockerfile b/docker/Dockerfile index b8fd3d6..64dea04 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM postgres:14 +FROM postgres:16 RUN apt-get update && apt-get upgrade -y ENV build_deps ca-certificates \ @@ -13,10 +13,10 @@ ENV build_deps ca-certificates \ RUN apt-get install -y --no-install-recommends $build_deps pkg-config cmake -WORKDIR /home/spark +WORKDIR /home/spa5k -ENV HOME=/home/spark \ - PATH=/home/spark/.cargo/bin:$PATH +ENV HOME=/home/spa5k \ + PATH=/home/spa5k/.cargo/bin:$PATH RUN chown postgres:postgres /home/spark USER postgres @@ -27,9 +27,9 @@ RUN \ cargo --version # PGX -RUN cargo install cargo-pgx +RUN cargo install cargo-pgrx -RUN cargo pgx init --pg14 $(which pg_config) +RUN cargo pgx init --pg16 $(which pg_config) USER root diff --git a/dprint.json b/dprint.json deleted file mode 100644 index bb3430f..0000000 --- a/dprint.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "json": {}, - "markdown": {}, - "toml": {}, - "includes": [ - "**/*.{ts,tsx,js,jsx,cjs,mjs,json,md,toml,rs,sql}" - ], - "excludes": [ - "**/node_modules", - "**/*-lock.json", - "target" - ], - "plugins": [ - "https://plugins.dprint.dev/json-0.15.3.wasm", - "https://plugins.dprint.dev/markdown-0.13.3.wasm", - "https://plugins.dprint.dev/toml-0.5.4.wasm", - "https://plugins.dprint.dev/rustfmt-0.6.2.json@886c6f3161cf020c2d75160262b0f56d74a521e05cfb91ec4f956650c8ca76ca", - "https://plugins.dprint.dev/sql-0.1.2.wasm" - ] -} diff --git a/justfile b/justfile new file mode 100644 index 0000000..64b4549 --- /dev/null +++ b/justfile @@ -0,0 +1,267 @@ +# Choose sell based on platform +shell := if os() == "macos" { "zsh" } else { "bash" } + +docker := env_var_or_default("DOCKER", "docker") +git := env_var_or_default("GIT", "git") +tar := env_var_or_default("TAR", "tar") +strip := env_var_or_default("STRIP", "strip") +just := env_var_or_default("JUST", just_executable()) + +cargo := env_var_or_default("CARGO", "cargo") +cargo_get := env_var_or_default("CARGO_GET", "cargo-get") +cargo_generate_rpm := env_var_or_default("CARGO_GENERATE_RPM", "cargo-generate-rpm") +cargo_watch := env_var_or_default("CARGO_WATCH", "cargo-watch") +cargo_profile := env_var_or_default("CARGO_PROFILE", "") +cargo_profile_arg := if cargo_profile != "" { + "--profile " + cargo_profile +} else { + "" +} +cargo_features := env_var_or_default("CARGO_FEATURES", "") +cargo_features_arg := if cargo_features != "" { + "--no-default-features --features " + cargo_features +} else { + "" +} + +changelog_file_path := absolute_path(justfile_directory() / "CHANGELOG") + +pkg_pg_version := env_var_or_default("PKG_PG_VERSION", "16.2") +pkg_pg_config_path := env_var_or_default("PKG_PG_CONFIG_PATH", "~/.pgrx/" + pkg_pg_version + "/pgrx-install/bin/pg_config") +pkg_tarball_suffix := env_var_or_default("PKG_TARBALL_SUFFIX", "") + +pgrx_pg_version := env_var_or_default("PGRX_PG_VERSION", "pg16") +pgrx_pkg_path_prefix := env_var_or_default("PGRX_PKG_PATH_PREFIX", "target") +# If /root, 'home' does not appear in the generated prefix +pkg_user_dir_prefix := if docker_build_user == "root" { docker_build_user } else { "home/" + docker_build_user } +pgrx_pkg_output_dir := pgrx_pkg_path_prefix / "release" / "uids-postgres" + pgrx_pg_version / pkg_user_dir_prefix / ".pgrx" / pkg_pg_version / "pgrx-install" + +docker_build_user := env_var_or_default('DOCKER_BUILD_USER', "root") + +default: + {{just}} --list + +########### +# Tooling # +########### + +_check-installed-version tool msg: + #!/usr/bin/env -S {{shell}} -euo pipefail + if [ -z "$(command -v {{tool}})" ]; then + echo "{{msg}}"; + exit 1; + fi + +@_check-tool-cargo: + {{just}} _check-installed-version {{cargo}} "'cargo' not available, please install the Rust toolchain (see: https://github.com/rust-lang/cargo/)"; + +@_check-tool-cargo-watch: + {{just}} _check-installed-version {{cargo_watch}} "'cargo-watch' not available, please install cargo-watch (https://github.com/passcod/cargo-watch)" + +@_check-tool-cargo-get: + {{just}} _check-installed-version {{cargo_get}} "'cargo-get' not available, please install cargo-get (https://crates.io/crates/cargo-get)" + +@_check-tool-strip: + {{just}} _check-installed-version {{strip}} "'strip' not available, please install strip (https://www.man7.org/linux/man-pages/man1/strip.1.html)" + +@_check-tool-cargo-generate-rpm: + {{just}} _check-installed-version {{cargo_generate_rpm}} "'cargo-generate-rpm' not available, please install cargo-generate-rpm (https://crates.io/crates/cargo-generate-rpm)" + +######### +# Build # +######### + +version := env_var_or_default("VERSION", `cargo get package.version`) +revision := env_var_or_default("REVISION", `git rev-parse --short HEAD`) + +@get-version: _check-tool-cargo-get + echo -n {{version}} + +@get-revision: _check-tool-cargo-get + echo -n {{revision}} + +print-version: + #!/usr/bin/env -S {{shell}} -euo pipefail + echo -n `{{just}} get-version` + +print-revision: + #!/usr/bin/env -S {{shell}} -euo pipefail + echo -n `{{just}} get-revision` + +print-pkg-output-dir: + echo -n {{pgrx_pkg_output_dir}} + +changelog: + {{git}} cliff --unreleased --tag={{version}} --prepend={{changelog_file_path}} + +# Set the version on the package +set-version version: + {{cargo}} set-version {{version}} + +lint: + {{cargo}} clippy {{cargo_features_arg}} {{cargo_profile_arg}} --all-targets + +build: + {{cargo}} build {{cargo_features_arg}} {{cargo_profile_arg}} + +build-release: + {{cargo}} build --release {{cargo_features_arg}} + +build-watch: _check-tool-cargo _check-tool-cargo-watch + {{cargo_watch}} -x "build $(CARGO_BUILD_FLAGS)" --watch src + +build-test-watch: _check-tool-cargo _check-tool-cargo-watch + {{cargo_watch}} -x "test $(CARGO_BUILD_FLAGS)" --watch src + +build-package: + PGRX_IGNORE_RUST_VERSIONS=y {{cargo}} pgrx package --pg-config {{pkg_pg_config_path}} -vvv + +package: build-package + mkdir -p pkg/uids-postgres$({{just}} print-version) + cp -r $({{just}} print-pkg-output-dir)/* pkg/uids-postgres$({{just}} print-version) + {{tar}} -C pkg -cvf uids-postgres$(just print-version){{pkg_tarball_suffix}}.tar.gz uids-postgres$({{just}} print-version) + +test: + {{cargo}} test {{cargo_profile_arg}} + {{cargo}} pgrx test + +pgrx-init: + #!/usr/bin/env -S {{shell}} -euo pipefail + if [ ! -d "{{pkg_pg_config_path}}" ]; then + echo "failed to find pgrx init dir [{{pkg_pg_config_path}}], running pgrx init..."; + {{cargo}} pgrx init + fi + +########## +# Docker # +########## + +container_img_arch := env_var_or_default("CONTAINER_IMAGE_ARCH", "amd64") + +pg_image_version := env_var_or_default("POSTGRES_IMAGE_VERSION", "16.2") +pg_os_image_version := env_var_or_default("POSTGRES_OS_IMAGE_VERSION", "alpine3.18") + +uids_postgres_image_name := env_var_or_default("UIDS_POSTGRES_IMAGE_NAME", "ghcr.io/spa5k/pg_idkit") +uids_postgres_image_tag := env_var_or_default("POSGRES_IMAGE_VERSION", version + "-" + "pg" + pg_image_version + "-" + pg_os_image_version + "-" + container_img_arch) +uids_postgres_image_tag_suffix := env_var_or_default("UIDS_POSTGRES_IMAGE_TAG_SUFFIX", "") +uids_postgres_image_name_full := env_var_or_default("UIDS_POSTGRES_IMAGE_NAME_FULL", uids_postgres_image_name + ":" + uids_postgres_image_tag + uids_postgres_image_tag_suffix) +uids_postgres_dockerfile_path := env_var_or_default("UIDS_POSTGRES_DOCKERFILE_PATH", "infra" / "docker" / uids_postgres_image_tag + ".Dockerfile") + +docker_password_path := env_var_or_default("DOCKER_PASSWORD_PATH", "secrets/docker/password.secret") +docker_username_path := env_var_or_default("DOCKER_USERNAME_PATH", "secrets/docker/username.secret") +docker_image_registry := env_var_or_default("DOCKER_IMAGE_REGISTRY", "ghcr.io/spa5k/pg_idkit") +docker_config_dir := env_var_or_default("DOCKER_CONFIG", "secrets/docker") + +img_dockerfile_path := "infra" / "docker" / "uids-postgrespg" + pg_image_version + "-" + pg_os_image_version + "-" + container_img_arch + ".Dockerfile" + +# Ensure that that a given file is present +_ensure-file file: + #!/usr/bin/env -S {{shell}} -euo pipefail + if [ ! -f "{{file}}" ] ; then + echo "[error] file [{{file}}] is required, but missing"; + exit 1; + fi; + +# Log in with docker using local credentials +docker-login: + {{just}} _ensure-file {{docker_password_path}} + {{just}} _ensure-file {{docker_username_path}} + cat {{docker_password_path}} | {{docker}} login {{docker_image_registry}} -u `cat {{docker_username_path}}` --password-stdin + cp {{docker_config_dir}}/config.json {{docker_config_dir}}/.dockerconfigjson + +docker_platform_arg := env_var_or_default("DOCKER_PLATFORM_ARG", "") +docker_progress_arg := env_var_or_default("DOCKER_PROGRESS_ARG", "") + +########################## +# Docker Image - builder # +########################## +# +# This image is used as a cache for speeding up CI builds, +# and for performing builds when building release artifacts +# + +builder_gnu_dockerfile_path := env_var_or_default("BUILDER_DOCKERFILE_PATH", "infra" / "docker" / "builder-gnu.Dockerfile") +builder_gnu_image_name := env_var_or_default("BUILDER_IMAGE_NAME", "ghcr.io/spa5k/pg_idkit/builder-gnu") +builder_gnu_image_tag := env_var_or_default("BUILDER_IMAGE_TAG", "0.1.x") +builder_gnu_image_name_full := env_var_or_default("BUILDER_IMAGE_NAME_FULL", builder_gnu_image_name + ":" + builder_gnu_image_tag) + +## TODO: uncomment and edit build-builder-image once musl machinery is available +## https://github.com/spa5k/pg_idkit/issues/55 +# +# builder_musl_dockerfile_path := env_var_or_default("BUILDER_DOCKERFILE_PATH", "infra" / "docker" / "builder-musl.Dockerfile") +# builder_musl_image_name := env_var_or_default("BUILDER_IMAGE_NAME", "ghcr.io/spa5k/pg_idkit/builder-musl") +# builder_musl_image_tag := env_var_or_default("BUILDER_IMAGE_TAG", "0.1.x") +# builder_musl_image_name_full := env_var_or_default("BUILDER_IMAGE_NAME_FULL", builder_musl_image_name + ":" + builder_musl_image_tag) + +# Build the docker image used in BUILDER +build-builder-image: + {{docker}} build -f {{builder_gnu_dockerfile_path}} -t {{builder_gnu_image_name_full}} . + +# Push the docker image used in BUILDER (to GitHub Container Registry) +push-builder-image: + {{docker}} push {{builder_gnu_image_name_full}} + +########################### +# Docker Image - base-pkg # +########################### +# +# This image is used as a base for packaging flows, usually while building +# the end-user facing Docker image that is contains Postgres & pg_idkit +# + +# Determine the Dockerfile to use when building the packaging utility base image +base_pkg_dockerfile_path := "infra/docker/base-pkg-" + pg_os_image_version + "-" + container_img_arch + ".Dockerfile" +base_pkg_image_name := env_var_or_default("PKG_IMAGE_NAME", "ghcr.io/spa5k/pg_idkit/base-pkg") +base_pkg_version := env_var_or_default("PKG_IMAGE_NAME", "0.1.x") +base_pkg_image_tag := env_var_or_default("POSGRES_IMAGE_VERSION", base_pkg_version + "-" + pg_os_image_version + "-" + container_img_arch) +base_pkg_image_name_full := env_var_or_default("PKG_IMAGE_NAME_FULL", base_pkg_image_name + ":" + base_pkg_image_tag) + +# Build the base image for packaging +build-base-pkg-image: + {{docker}} build --build-arg USER={{docker_build_user}} -f {{base_pkg_dockerfile_path}} . -t {{base_pkg_image_name_full}}; + +# Push the base image for packaging +push-base-pkg-image: + {{docker}} push {{base_pkg_image_name_full}} + +########################### +# Docker Image - pg_idkit # +########################### +# +# This image is the pg_idkit image itself, normally built FROM +# a image of base-pkg +# + +# Build the docker image for pg_idkit +build-image: + {{docker}} build {{docker_platform_arg}} {{docker_progress_arg}} -f {{img_dockerfile_path}} -t {{uids_postgres_image_name_full}} --build-arg USER={{docker_build_user}} --build-arg UIDS_POSTGRES_REVISION={{revision}} --build-arg UIDS_POSTGRES_VERSION={{uids_postgres_image_tag}} . + +# Push the docker image for pg_idkit +push-image: + {{docker}} push {{uids_postgres_image_name_full}} + +####### +# RPM # +####### + +rpm_arch := env_var_or_default("RPM_ARCH", "x86_64") + +rpm_file_name := env_var_or_default("RPM_OUTPUT_PATH", "uids-postgres" + version + "-" + pgrx_pg_version + "." + rpm_arch + ".rpm") +rpm_output_path := "target" / "generate-rpm" / rpm_file_name + +# Cargo.toml depends on this file being at the location below. +rpm_scratch_location := "/tmp/pg_idkit/rpm/scratch" + +# Build an RPM distribution for pg_idkit +build-rpm: _check-tool-strip _check-tool-cargo-generate-rpm + CARGO_FEATURES={{pgrx_pg_version}} {{just}} package + {{strip}} -s {{pgrx_pkg_output_dir}}/lib/postgresql/pg_idkit.so + mkdir -p {{rpm_scratch_location}} + cp -r {{pgrx_pkg_output_dir}} {{rpm_scratch_location}} + {{cargo_generate_rpm}} --variant {{pgrx_pg_version}} + +@print-rpm-output-file-name: + echo -n {{rpm_file_name}} + +@print-rpm-output-path: + echo -n {{rpm_output_path}} \ No newline at end of file diff --git a/src/cuid2.rs b/src/cuid2.rs new file mode 100644 index 0000000..4d55402 --- /dev/null +++ b/src/cuid2.rs @@ -0,0 +1,36 @@ +pub mod cuid2_rs { + use cuid2; + use pgrx::prelude::*; + + #[pg_extern] + pub(crate) fn generate_cuid2() -> String { + let id = cuid2::create_id(); + id.to_string() + } + + #[pg_extern] + pub(crate) fn check_cuid2(id_str: &str) -> bool { + cuid2::is_cuid2(id_str) + } + + // tests + #[cfg(any(test, feature = "pg_test"))] + #[pgrx::pg_schema] + mod tests { + use pgrx::pg_test; + + use crate::cuid2::cuid2_rs::{check_cuid2, generate_cuid2}; + + #[pg_test] + fn test_generate_cuid2() { + let cuid2_string: String = generate_cuid2(); + assert!(cuid2_string.len() == 24); + } + + #[pg_test] + fn test_check_cuid2() { + let cuid2_string: String = generate_cuid2(); + assert!(check_cuid2(&cuid2_string)); + } + } +} diff --git a/src/ksuid.rs b/src/ksuid.rs index 71c0059..1598f64 100644 --- a/src/ksuid.rs +++ b/src/ksuid.rs @@ -1,5 +1,5 @@ pub mod ksuid_rs { - use pgx::*; + use pgrx::prelude::*; use svix_ksuid::{KsuidLike, KsuidMs}; #[pg_extern] @@ -13,4 +13,25 @@ pub mod ksuid_rs { let ksuid_bytes = KsuidMs::new(None, None); ksuid_bytes.bytes().to_vec() } + + // tests + #[cfg(any(test, feature = "pg_test"))] + #[pgrx::pg_schema] + mod tests { + use pgrx::pg_test; + + // Test Ksuid length + #[pg_test] + fn test_generate_ksuid_length() { + let ksuid_string: String = crate::ksuid::ksuid_rs::generate_ksuid(); + assert_eq!(ksuid_string.len(), 27); + } + + // Test ksuid bytes + #[pg_test] + fn test_generate_ksuid_bytes() { + let ksuid_bytes: Vec = crate::ksuid::ksuid_rs::generate_ksuid_bytes(); + assert_eq!(ksuid_bytes.len(), 20); + } + } } diff --git a/src/lib.rs b/src/lib.rs index 3d6dc09..c46e662 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,71 +1,19 @@ +mod cuid2; mod ksuid; mod nanoid; +mod pushid; +mod timeflake; +mod typeid; mod ulid; +mod uuid_v6; +mod uuidv7; -use pgx::*; +use pgrx::prelude::*; -pg_module_magic!(); - -#[cfg(any(test, feature = "pg_test"))] -#[pg_schema] -mod tests { - use pgx::*; - - #[pg_test] - fn test_generate_ulid() { - let ulid_string: String = crate::ulid::ulid_rs::generate_ulid(); - assert_eq!(ulid_string.len(), 26); - } - - #[pg_test] - fn test_generate_ulid_from_string() { - let ulid_string: String = "01CAT3X5Y5G9A62F1rFA6Tnice".to_string(); - // copy of ulid_string - let ulid_from_string: String = crate::ulid::ulid_rs::generate_ulid_from_string(ulid_string); - assert_eq!(ulid_from_string, "01CAT3X5Y5G9A62F1RFA6TN1CE"); - } - - #[pg_test] - fn test_generate_ulid_bytes() { - let ulid_bytes: Vec = crate::ulid::ulid_rs::generate_ulid_bytes(); - assert_eq!(ulid_bytes.len(), 16); - } - - // Test Ksuid length - #[pg_test] - fn test_generate_ksuid_length() { - let ksuid_string: String = crate::ksuid::ksuid_rs::generate_ksuid(); - assert_eq!(ksuid_string.len(), 27); - } - - // Test ksuid bytes - #[pg_test] - fn test_generate_ksuid_bytes() { - let ksuid_bytes: Vec = crate::ksuid::ksuid_rs::generate_ksuid_bytes(); - assert_eq!(ksuid_bytes.len(), 20); - } - - // Test nanoid length - #[pg_test] - fn test_generate_nanoid_length() { - let nanoid_string: String = crate::nanoid::nanoid_rs::generate_nanoid_length(10); - assert_eq!(nanoid_string.len(), 10); - } - - // test nanoid without legnth - #[pg_test] - fn test_generate_nanoid() { - let nanoid_string: String = crate::nanoid::nanoid_rs::generate_nanoid(); - assert_eq!(nanoid_string.len(), 21); - } - - #[pg_test] - fn test_generate_nanoida() { - let nanoid_string: String = crate::nanoid::nanoid_rs::generate_nanoid_c("1234567890abcdef"); - assert_eq!(nanoid_string.len(), 21); - } -} +pgrx::pg_module_magic!(); +/// This module is required by `cargo pgrx test` invocations. +/// It must be visible at the root of your extension crate. #[cfg(test)] pub mod pg_test { pub fn setup(_options: Vec<&str>) { diff --git a/src/nanoid.rs b/src/nanoid.rs index b544a52..7f1c6fa 100644 --- a/src/nanoid.rs +++ b/src/nanoid.rs @@ -1,6 +1,6 @@ pub mod nanoid_rs { use nanoid::nanoid; - use pgx::*; + use pgrx::prelude::*; #[pg_extern] pub(crate) fn generate_nanoid() -> String { @@ -30,4 +30,32 @@ pub mod nanoid_rs { let id = nanoid!(21, &alphabets_vec); id } + + // tests + #[cfg(any(test, feature = "pg_test"))] + #[pgrx::pg_schema] + mod tests { + use pgrx::pg_test; + + // Test Nanoid length + #[pg_test] + fn test_generate_nanoid_length() { + let nanoid_string: String = crate::nanoid::nanoid_rs::generate_nanoid_length(10); + assert_eq!(nanoid_string.len(), 10); + } + + // test nanoid without legnth + #[pg_test] + fn test_generate_nanoid() { + let nanoid_string: String = crate::nanoid::nanoid_rs::generate_nanoid(); + assert_eq!(nanoid_string.len(), 21); + } + + #[pg_test] + fn test_generate_nanoid_custom() { + let nanoid_string: String = + crate::nanoid::nanoid_rs::generate_nanoid_c("1234567890abcdef"); + assert_eq!(nanoid_string.len(), 21); + } + } } diff --git a/src/pushid.rs b/src/pushid.rs new file mode 100644 index 0000000..b4ebb53 --- /dev/null +++ b/src/pushid.rs @@ -0,0 +1,36 @@ +pub mod pushid_rs { + use pgrx::*; + use pushid::PushId; + use pushid::PushIdGen; + + /// Generate a PushId + #[pg_extern] + fn generate_pushid() -> String { + PushId::new().get_id() + } + + /// Generate a PushId as text + #[pg_extern] + fn generate_pushid_text() -> String { + generate_pushid() + } + + // tests + #[cfg(any(test, feature = "pg_test"))] + #[pg_schema] + mod tests { + use pgrx::*; + + #[pg_test] + fn test_pushid_len() { + let generated = crate::pushid::pushid_rs::generate_pushid(); + assert_eq!(generated.len(), 20); + } + + #[pg_test] + fn test_pushid_text_len() { + let generated = crate::pushid::pushid_rs::generate_pushid_text(); + assert_eq!(generated.len(), 20); + } + } +} diff --git a/src/timeflake.rs b/src/timeflake.rs new file mode 100644 index 0000000..0dc77b3 --- /dev/null +++ b/src/timeflake.rs @@ -0,0 +1,49 @@ +pub mod timeflake_rs { + use pgrx::prelude::*; + use timeflake_rs::Timeflake; + + #[pg_extern] + pub(crate) fn generate_timeflake() -> String { + let result = Timeflake::random(); + result.unwrap().to_string() + } + + #[pg_extern] + pub(crate) fn generate_timeflake_bytes() -> Vec { + let result = Timeflake::random(); + result.unwrap().as_uuid().as_bytes().to_vec() + } + + #[pg_extern] + pub fn generate_timeflake_uuid() -> pgrx::Uuid { + pgrx::Uuid::from_slice(Timeflake::random().unwrap().as_uuid().as_bytes()).unwrap() + } + + #[cfg(any(test, feature = "pg_test"))] + #[pgrx::pg_schema] + mod tests { + use pgrx::pg_test; + + use crate::timeflake::timeflake_rs::{ + generate_timeflake, generate_timeflake_bytes, generate_timeflake_uuid, + }; + + #[pg_test] + fn test_generate_timeflake() { + let timeflake_string: String = generate_timeflake(); + assert!(timeflake_string.len() == 36); + } + + #[pg_test] + fn test_generate_timeflake_bytes() { + let timeflake_bytes: Vec = generate_timeflake_bytes(); + assert!(timeflake_bytes.len() == 16); + } + + #[pg_test] + fn test_generate_timeflake_uuid() { + let timeflake_uuid: pgrx::Uuid = generate_timeflake_uuid(); + assert!(timeflake_uuid.len() == 16); + } + } +} diff --git a/src/typeid.rs b/src/typeid.rs new file mode 100644 index 0000000..505e0f8 --- /dev/null +++ b/src/typeid.rs @@ -0,0 +1,50 @@ +pub mod typeid_rs { + use pgrx::prelude::*; + use type_safe_id::{DynamicType, TypeSafeId}; + + #[pg_extern] + pub(crate) fn generate_typeid(prefix: &str) -> String { + let dynamic_type = DynamicType::new(prefix).expect("invalid prefix"); + let id: TypeSafeId = TypeSafeId::new_with_type(dynamic_type); + id.to_string() + } + + #[pg_extern] + pub(crate) fn check_typeid(prefix: &str, id_str: &str) -> bool { + let id: TypeSafeId = id_str.parse().expect("invalid id"); + id.type_prefix() == prefix + } + + // tests + #[cfg(any(test, feature = "pg_test"))] + #[pgrx::pg_schema] + mod tests { + use pgrx::pg_test; + + // Test TypeId length + #[pg_test] + fn test_generate_typeid_length() { + let typeid_string: String = crate::typeid::typeid_rs::generate_typeid("custom"); + assert_eq!(typeid_string.len(), 33); + } + + // Test TypeId prefix + #[pg_test] + fn test_generate_typeid_prefix() { + let typeid_string: String = crate::typeid::typeid_rs::generate_typeid("custom"); + assert!(typeid_string.starts_with("custom_")); + } + + // Test TypeId check + #[pg_test] + fn test_check_typeid() { + let prefix = "custom"; + let id: String = crate::typeid::typeid_rs::generate_typeid(prefix); + let is_match: bool = crate::typeid::typeid_rs::check_typeid(prefix, &id); + assert!(is_match); + + let is_not_match: bool = crate::typeid::typeid_rs::check_typeid("other", &id); + assert!(!is_not_match); + } + } +} diff --git a/src/ulid.rs b/src/ulid.rs index 9c2d73d..c401c78 100644 --- a/src/ulid.rs +++ b/src/ulid.rs @@ -1,7 +1,7 @@ pub mod ulid_rs { use std::str::FromStr; - use pgx::*; + use pgrx::prelude::*; use rusty_ulid::{generate_ulid_bytes as ulid_bytes, Ulid}; use rusty_ulid::generate_ulid_string; @@ -23,4 +23,35 @@ pub mod ulid_rs { let result = Ulid::from_str(&from_str); result.unwrap().to_string() } + + // tests + #[cfg(any(test, feature = "pg_test"))] + #[pgrx::pg_schema] + mod tests { + use pgrx::pg_test; + + // Test Ulid length + #[pg_test] + fn test_generate_ulid_length() { + let ulid_string: String = crate::ulid::ulid_rs::generate_ulid(); + assert_eq!(ulid_string.len(), 26); + } + + // Test Ulid bytes + #[pg_test] + fn test_generate_ulid_bytes() { + let ulid_bytes: Vec = crate::ulid::ulid_rs::generate_ulid_bytes(); + assert_eq!(ulid_bytes.len(), 16); + } + + // Test Ulid from string + #[pg_test] + fn test_generate_ulid_from_string() { + let ulid_string: String = "01CAT3X5Y5G9A62F1rFA6Tnice".to_string(); + // copy of ulid_string + let ulid_from_string: String = + crate::ulid::ulid_rs::generate_ulid_from_string(ulid_string); + assert_eq!(ulid_from_string, "01CAT3X5Y5G9A62F1RFA6TN1CE"); + } + } } diff --git a/src/uuid_v6.rs b/src/uuid_v6.rs new file mode 100644 index 0000000..499f2a9 --- /dev/null +++ b/src/uuid_v6.rs @@ -0,0 +1,66 @@ +pub mod uuidv6_rs { + use std::io::{Error as IoError, ErrorKind}; + + use getrandom::getrandom; + use pgrx::pg_extern; + use uuid::Uuid; + + /// Generate a new UUIDv6 + fn new_uuidv6() -> Uuid { + let mut buf = [0u8; 6]; + let res = getrandom(&mut buf); + if res.is_err() { + panic!("failed to get random bytes for building uuidv6"); + } + Uuid::now_v6(&buf) + } + + /// Generate a UUID v6 + #[pg_extern] + pub(crate) fn generate_uuidv6() -> String { + new_uuidv6().as_hyphenated().to_string() + } + + /// Generate a UUID v6, producing a Postgres text object + #[pg_extern] + pub(crate) fn generate_uuidv6_text() -> String { + generate_uuidv6() + } + + /// Generate a UUID v6, producing a Postgres uuid object + #[pg_extern] + pub(crate) fn generate_uuidv6_uuid() -> pgrx::Uuid { + pgrx::Uuid::from_slice(new_uuidv6().as_bytes()) + .map_err(|e| IoError::new(ErrorKind::Other, e)) + .unwrap() + } + + #[cfg(any(test, feature = "pg_test"))] + #[pgrx::pg_schema] + mod tests { + use pgrx::pg_test; + + use crate::uuid_v6::uuidv6_rs::{generate_uuidv6, generate_uuidv6_uuid}; + + /// Basic length test + #[pg_test] + fn test_uuidv6_len() { + assert_eq!(generate_uuidv6().len(), 36); + } + + /// Basic length test for bytes + #[pg_test] + fn test_uuidv6_len_uuid() { + assert_eq!(generate_uuidv6_uuid().len(), 16); + } + + /// Check version integer in UUID string + #[pg_test] + fn test_uuidv6_version_int() { + let generated = generate_uuidv6(); + let c9 = generated.chars().nth(14); + assert!(c9.is_some()); + assert_eq!(c9.unwrap(), '6'); + } + } +} diff --git a/src/uuidv7.rs b/src/uuidv7.rs new file mode 100644 index 0000000..81ac071 --- /dev/null +++ b/src/uuidv7.rs @@ -0,0 +1,74 @@ +pub mod uuidv7_rs { + use pgrx::prelude::*; + use uuid::Uuid; + + #[pg_extern] + pub(crate) fn generate_uuidv7() -> String { + let uuid = Uuid::now_v7(); + uuid.to_string() + } + + #[pg_extern] + pub(crate) fn generate_uuidv7_bytes() -> Vec { + let uuid = Uuid::now_v7(); + uuid.as_bytes().to_vec() + } + + #[pg_extern] + pub fn generate_uuidv7_from_string(from_str: String) -> String { + let result = Uuid::parse_str(&from_str); + result.unwrap().to_string() + } + + #[pg_extern] + pub fn parse_uuidv7(from_str: String) -> String { + let result = Uuid::parse_str(&from_str); + result.unwrap().to_string() + } + + // tests + #[cfg(any(test, feature = "pg_test"))] + #[pgrx::pg_schema] + mod tests { + use pgrx::pg_test; + + // Test UUIDv7 length + #[pg_test] + fn test_generate_uuidv7_length() { + let uuidv7_string: String = crate::uuidv7::uuidv7_rs::generate_uuidv7(); + assert_eq!(uuidv7_string.len(), 36); + } + + // Test UUIDv7 bytes + #[pg_test] + fn test_generate_uuidv7_bytes() { + let uuidv7_bytes: Vec = crate::uuidv7::uuidv7_rs::generate_uuidv7_bytes(); + assert_eq!(uuidv7_bytes.len(), 16); + } + + // Test UUIDv7 from string + #[pg_test] + fn test_generate_uuidv7_from_string() { + let uuidv7_string: String = "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(); + // copy of uuidv7_string + let uuidv7_from_string: String = + crate::uuidv7::uuidv7_rs::generate_uuidv7_from_string(uuidv7_string); + assert_eq!(uuidv7_from_string, "67e55044-10b1-426f-9247-bb680e5fe0c8"); + } + + // Test UUIDv7 parse + #[pg_test] + fn test_parse_uuidv7() { + let uuidv7_string: String = "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(); + let uuidv7: String = crate::uuidv7::uuidv7_rs::parse_uuidv7(uuidv7_string.clone()); + assert_eq!(uuidv7, uuidv7_string); + } + + #[pg_test] + fn test_generate_uuidv7() { + let uuid: String = crate::uuidv7::uuidv7_rs::generate_uuidv7(); + assert_eq!(uuid.len(), 36); // UUIDv7 length + assert!(uuid.contains('-')); // UUID format check + } + } +} diff --git a/uids.control b/uids.control index 34a516e..4fc9151 100644 --- a/uids.control +++ b/uids.control @@ -1,5 +1,5 @@ -comment = 'uids: Created by pgx' +comment = 'uids: generate various types of unique identifiers' default_version = '@CARGO_VERSION@' module_pathname = '$libdir/uids' relocatable = false -superuser = false +superuser = true