Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 62 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,39 +16,49 @@

`pg_jsonschema` is a PostgreSQL extension adding support for [JSON schema](https://json-schema.org/) validation on `json` and `jsonb` data types.


## API

This extension exposes the following four SQL functions:

- json_matches_schema
- jsonb_matches_schema (note the **jsonb** in front)
- jsonschema_is_valid
- jsonschema_validation_errors

With the following signatures

```sql
-- Validates a json *instance* against a *schema*
json_matches_schema(schema json, instance json) returns bool
```

and

```sql
-- Validates a jsonb *instance* against a *schema*
jsonb_matches_schema(schema json, instance jsonb) returns bool
```

and

```sql
-- Validates whether a json *schema* is valid
jsonschema_is_valid(schema json) returns bool
```

and

```sql
-- Returns an array of errors if a *schema* is invalid
jsonschema_validation_errors(schema json, instance json) returns text[]
```

## Usage

Those functions can be used to constrain `json` and `jsonb` columns to conform to a schema.

For example:

```sql
create extension pg_jsonschema;

Expand Down Expand Up @@ -102,19 +112,18 @@ pg_jsonschema is a (very) thin wrapper around the [jsonschema](https://docs.rs/j

Spin up Postgres with pg_jsonschema installed in a docker container via `docker-compose up`. The database is available at `postgresql://postgres:password@localhost:5407/app`


## Installation


Requires:
- [pgrx](https://github.com/tcdi/pgrx)

- [pgrx](https://github.com/tcdi/pgrx)

```shell
cargo pgrx run
```

which drops into a psql prompt.

```psql
psql (13.6)
Type "help" for help.
Expand All @@ -123,14 +132,53 @@ pg_jsonschema=# create extension pg_jsonschema;
CREATE EXTENSION

pg_jsonschema=# select json_matches_schema('{"type": "object"}', '{}');
json_matches_schema
json_matches_schema
---------------------
t
(1 row)
```

for more complete installation guidelines see the [pgrx](https://github.com/tcdi/pgrx) docs.

## Releasing

Releases are automated via a single command:

```shell
./scripts/release.sh <major.minor.patch>
```

For example:

```shell
./scripts/release.sh 0.4.0
```

This orchestrates the full release process end-to-end:

1. **Verifies** that the tag and GitHub release don't already exist
2. **Updates versions** in `Cargo.toml`, `META.json`, and `Cargo.lock`; creates a `release/<version>` branch; commits, pushes, and waits for the PR to be merged into `master`
3. **Verifies** all file versions match before tagging
4. **Creates and pushes** the `v<version>` tag, which triggers the CI workflows (`release.yml` for GitHub release + `.deb` artifacts, `pgxn-release.yml` for PGXN)
5. **Polls** GitHub until the release is published and prints the release URL

> **Note:** `pg_jsonschema.control` uses `@CARGO_VERSION@` which is substituted by pgrx at build time from `Cargo.toml`, so it doesn't need manual updates.

### Idempotency

The script is safe to re-run if interrupted — it detects what has already been completed (branch exists, tag exists, release exists) and picks up where it left off.

### Individual Scripts

The release process is composed of smaller scripts that can also be run independently:

| Script | Purpose |
| -------------------------------------------------- | ----------------------------------------------------------------------- |
| `scripts/check-version.sh <version>` | Checks if `Cargo.toml` and `META.json` match the given version |
| `scripts/update-version.sh <version>` | Updates version files, creates a release branch, and waits for PR merge |
| `scripts/update-version.sh --files-only <version>` | Updates version files without any git operations |
| `scripts/push-tag.sh <version>` | Creates and pushes the git tag, then monitors for the GitHub release |
| `scripts/push-tag.sh --dry-run <version>` | Validates versions without creating a tag |

## Prior Art

Expand All @@ -140,25 +188,25 @@ for more complete installation guidelines see the [pgrx](https://github.com/tcdi

[pgx_json_schema](https://github.com/jefbarn/pgx_json_schema) - JSON Schema Postgres extension written with pgrx + jsonschema


## Benchmark


#### System

- 2021 MacBook Pro M1 Max (32GB)
- macOS 14.2
- PostgreSQL 16.2

### Setup

Validating the following schema on 20k unique inserts

```json
{
"type": "object",
"properties": {
"a": {"type": "number"},
"b": {"type": "string"}
}
"type": "object",
"properties": {
"a": { "type": "number" },
"b": { "type": "string" }
}
}
```

Expand All @@ -181,6 +229,7 @@ select
)
from
generate_series(1, 20000) t(i);
-- Query Completed in 351 ms
-- Query Completed in 351 ms
```

for comparison, the equivalent test using postgres-json-schema's `validate_json_schema` function ran in 5.54 seconds. pg_jsonschema's ~15x speedup on this example JSON schema grows quickly as the schema becomes more complex.
48 changes: 48 additions & 0 deletions scripts/check-version.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/bin/bash

# This script checks if the provided version matches the versions in Cargo.toml and META.json
# Usage: source ./scripts/check-version.sh <version> [--warn-only]
# After sourcing, the following variables will be available:
# - CARGO_VERSION: Version from Cargo.toml
# - META_VERSION: Version from META.json
# - HAS_MISMATCH: true if versions don't match, false otherwise

VERSION_TO_CHECK=$1
WARN_ONLY=false

# Check for --warn-only flag
if [ "$2" = "--warn-only" ]; then
WARN_ONLY=true
fi

if [ -z "$VERSION_TO_CHECK" ]; then
echo "Error: Version argument required for check-version.sh"
exit 1
fi

# Extract versions from Cargo.toml and META.json
CARGO_VERSION=$(grep -E '^version = ' Cargo.toml | head -n 1 | sed -E 's/version = "(.*)"/\1/')
META_VERSION=$(jq -r '.version' META.json)

# Check for version mismatches
HAS_MISMATCH=false

if [ "$VERSION_TO_CHECK" != "$CARGO_VERSION" ]; then
echo ""
if [ "$WARN_ONLY" = true ]; then
echo "⚠️ Warning: Cargo.toml has version $CARGO_VERSION"
else
echo "❌ Error: Cargo.toml has version $CARGO_VERSION"
fi
HAS_MISMATCH=true
fi

if [ "$VERSION_TO_CHECK" != "$META_VERSION" ]; then
echo ""
if [ "$WARN_ONLY" = true ]; then
echo "⚠️ Warning: META.json has version $META_VERSION"
else
echo "❌ Error: META.json has version $META_VERSION"
fi
HAS_MISMATCH=true
fi
165 changes: 165 additions & 0 deletions scripts/push-tag.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
#!/bin/bash
set -e

# Parse arguments
DRY_RUN=false
VERSION=""

while [[ $# -gt 0 ]]; do
case $1 in
--dry-run)
DRY_RUN=true
shift
;;
*)
VERSION=$1
shift
;;
esac
done

# Check if version argument is provided
if [ -z "$VERSION" ]; then
echo "Error: Version argument required"
echo "Usage: ./scripts/push-tag.sh [--dry-run] <major.minor.patch>"
exit 1
fi

# Validate version format
if ! [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Invalid version format. Expected: major.minor.patch (e.g., 1.2.3)"
exit 1
fi

# Check version mismatches using shared script
source "$(dirname "$0")/check-version.sh" "$VERSION" "--warn-only"

if [ "$HAS_MISMATCH" = true ]; then
echo ""
echo "⚠️ Warning: Version mismatch detected, but continuing..."
echo " Ensure versions are updated before creating a release."
fi

# Exit early if dry run
if [ "$DRY_RUN" = true ]; then
echo ""
echo "✅ Dry run successful - all version checks passed"
echo " Version: $VERSION"
echo " Tag that would be created: v$VERSION"
exit 0
fi

# Create and push tag
TAG="v$VERSION"
echo ""
echo "Creating tag: $TAG"

# Fetch all tags from remote
echo "Fetching latest tags from remote..."
git fetch --tags

# Check if tag already exists
TAG_EXISTS=false
if git rev-parse "$TAG" >/dev/null 2>&1; then
TAG_EXISTS=true
TAG_COMMIT=$(git rev-parse "$TAG")
CURRENT_COMMIT=$(git rev-parse HEAD)

echo "⚠️ Warning: Tag $TAG already exists"

if [ "$TAG_COMMIT" = "$CURRENT_COMMIT" ]; then
echo "✅ Tag points to current commit, continuing..."
else
echo "⚠️ Warning: Tag points to a different commit"
echo " Tag commit: $TAG_COMMIT"
echo " Current commit: $CURRENT_COMMIT"
echo " Continuing anyway..."
fi
else
# Create the tag
git tag "$TAG"
echo "✅ Tag $TAG created"
fi

# Push the tag to remote
echo "Pushing tag to remote..."
if git push origin "$TAG" 2>&1 | tee /tmp/git_push_tag_output.txt; then
echo "✅ Tag $TAG pushed successfully"
else
# Check if error is because tag already exists on remote
if grep -q "already exists" /tmp/git_push_tag_output.txt; then
echo "⚠️ Warning: Tag already exists on remote, continuing..."
else
echo "⚠️ Warning: Failed to push tag, but continuing..."
fi
fi
rm -f /tmp/git_push_tag_output.txt

echo ""

# Poll for GitHub release
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "⏳ Monitoring GitHub release creation..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""

# Extract repository info from git remote
GIT_REMOTE=$(git remote get-url origin)
if [[ $GIT_REMOTE =~ github\.com[:/]([^/]+)/([^/.]+)(\.git)?$ ]]; then
REPO_OWNER="${BASH_REMATCH[1]}"
REPO_NAME="${BASH_REMATCH[2]}"

echo "Waiting for release.yml workflow to create the release..."
echo "(This may take a few minutes)"
echo ""

# Poll for release with timeout (30s interval keeps us well under the 60 req/hr unauthenticated API limit)
POLL_INTERVAL=30
MAX_ATTEMPTS=40 # 20 minutes (40 * 30 seconds)
ATTEMPT=0
RELEASE_FOUND=false
RELEASE_URL=""

while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
HTTP_STATUS=$(curl -s -o /tmp/release_check.json -w "%{http_code}" "https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/releases/tags/$TAG")

if [ "$HTTP_STATUS" = "200" ]; then
RELEASE_FOUND=true
RELEASE_URL=$(grep -o '"html_url":"[^"]*"' /tmp/release_check.json | head -1 | cut -d'"' -f4)
rm -f /tmp/release_check.json
break
fi

ATTEMPT=$((ATTEMPT + 1))
ELAPSED=$((ATTEMPT * POLL_INTERVAL))
printf "\r⏳ Waiting... %dm%02ds elapsed" $((ELAPSED / 60)) $((ELAPSED % 60))
sleep $POLL_INTERVAL
done

rm -f /tmp/release_check.json

echo ""
echo ""

if [ "$RELEASE_FOUND" = true ]; then
echo "✅ GitHub release is now available!"
echo ""
if [ -n "$RELEASE_URL" ]; then
echo "🔗 Release URL: $RELEASE_URL"
else
echo "🔗 Release URL: https://github.com/$REPO_OWNER/$REPO_NAME/releases/tag/$TAG"
fi
else
echo "⚠️ Timeout waiting for GitHub release"
echo " The release workflow may still be running."
echo " Check the Actions tab on GitHub for status."
echo " https://github.com/$REPO_OWNER/$REPO_NAME/actions"
fi
else
echo "⚠️ Could not parse GitHub repository from git remote"
echo " The release workflow has been triggered by pushing the tag."
fi

echo ""
echo "✅ Successfully released version $VERSION"
echo " Tag: $TAG"
Loading