Skip to content
Open
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
17 changes: 17 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -239,8 +239,24 @@ jobs:
path: dist/${{ matrix.name == 'apple-xcframework' && 'CloudSync.*' || 'cloudsync.*'}}
if-no-files-found: error

postgres-migration-check:
if: ${{ !contains(github.event.head_commit.message, '[auto-update]') }}
runs-on: ubuntu-22.04
name: postgresql migration script check
timeout-minutes: 2
steps:
- uses: actions/checkout@v4.2.2
with:
# Need full history + tags so `git describe` can find the previous
# release tag that the check script compares against.
fetch-depth: 0

- name: verify migration script for current CLOUDSYNC_VERSION
run: make postgres-check-migration

postgres-test:
if: ${{ !contains(github.event.head_commit.message, '[auto-update]') }}
needs: [postgres-migration-check]
runs-on: ubuntu-22.04
name: postgresql ${{ matrix.postgres_tag }} build + test
timeout-minutes: 10
Expand Down Expand Up @@ -284,6 +300,7 @@ jobs:

postgres-build:
if: ${{ !contains(github.event.head_commit.message, '[auto-update]') }}
needs: [postgres-migration-check]
runs-on: ${{ matrix.os }}
name: postgresql${{ matrix.postgres_version }}-${{ matrix.name }}-${{ matrix.arch }} build
timeout-minutes: 15
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ dist/
/curl/src
openssl/

# Generated PostgreSQL extension files (produced from .in templates by
# docker/Makefile.postgresql; version is derived from src/cloudsync.h)
/docker/postgresql/cloudsync.control
/src/postgresql/cloudsync--*.sql

# Test artifacts
/coverage
unittest
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,12 @@ Part of the **[SQLite AI](https://sqlite.ai)** ecosystem:
| **[SQLite-JS](https://github.com/sqliteai/sqlite-js)** | Custom SQLite functions in JavaScript |
| **[Liteparser](https://github.com/sqliteai/liteparser)** | Fully compliant SQLite SQL parser |

## Versioning

This project follows [semver](https://semver.org/). The single source of truth is `CLOUDSYNC_VERSION` in `src/cloudsync.h`; all packaged artifacts (NPM, Maven, pub.dev, Swift, Docker, native tarballs) inherit this version. PATCH releases never alter the exposed API — they ship bug fixes, performance improvements, and internal changes only.

The PostgreSQL extension differs only in how it surfaces the version: its catalog version (`default_version` / `installed_version`) exposes `MAJOR.MINOR` only, so PATCH releases are transparent binary upgrades and only MINOR/MAJOR releases need `ALTER EXTENSION cloudsync UPDATE`. The `cloudsync_version()` SQL function always reports the full semver of the loaded `.so`. See the [PostgreSQL upgrade docs](docs/postgresql/quickstarts/postgres.md#upgrading-a-later-release) for the user-facing procedure.

## License

This project is licensed under the [Elastic License 2.0](./LICENSE.md). For production or managed service use, [contact SQLite Cloud, Inc](mailto:info@sqlitecloud.io) for a commercial license.
67 changes: 61 additions & 6 deletions docker/Makefile.postgresql
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,17 @@ PG_INCLUDEDIR := $(shell $(PG_CONFIG) --includedir-server 2>/dev/null)

# Extension metadata
EXTENSION = cloudsync
EXTVERSION = 1.0
# Read the binary version (full semver) from src/cloudsync.h, and derive
# EXTVERSION as just MAJOR.MINOR. Rationale: PATCH bumps are binary-only
# (no SQL surface change, no ALTER EXTENSION UPDATE needed); MINOR/MAJOR bumps
# are the SQL-surface-changing ones that require a per-release upgrade script.
# cloudsync_version() in the .so still reports the full semver for debugging.
#
# Recursive (=) rather than immediate (:=) assignment so the shell calls only
# fire when a PG-related target actually references these variables. Non-PG
# targets parse this included makefile without invoking sed/cut at all.
CLOUDSYNC_VERSION_FULL = $(shell sed -n 's/^\#define CLOUDSYNC_VERSION[[:space:]]*"\([^"]*\)".*/\1/p' src/cloudsync.h)
EXTVERSION = $(shell echo '$(CLOUDSYNC_VERSION_FULL)' | cut -d. -f1-2)

# Detect OS for platform-specific settings
ifneq ($(OS),Windows_NT)
Expand Down Expand Up @@ -82,29 +92,61 @@ endif
PG_EXTENSION_SQL = src/postgresql/$(EXTENSION)--$(EXTVERSION).sql
PG_EXTENSION_CONTROL = docker/postgresql/$(EXTENSION).control

# Input templates (tracked). @EXTVERSION@ is substituted at build time.
PG_EXTENSION_SQL_IN = src/postgresql/$(EXTENSION).sql.in
PG_EXTENSION_CONTROL_IN = docker/postgresql/$(EXTENSION).control.in

# Upgrade scripts (cloudsync--<from>--<to>.sql) are hand-written per release.
PG_MIGRATIONS_DIR = src/postgresql/migrations
PG_MIGRATION_SQLS = $(wildcard $(PG_MIGRATIONS_DIR)/$(EXTENSION)--*--*.sql)

# ============================================================================
# PostgreSQL Build Targets
# ============================================================================

.PHONY: postgres-check postgres-build postgres-install postgres-package postgres-clean postgres-test \
.PHONY: postgres-check postgres-check-migration postgres-generate-files postgres-build postgres-install postgres-package postgres-clean postgres-test \
postgres-docker-build postgres-docker-build-asan postgres-docker-run postgres-docker-run-asan postgres-docker-stop postgres-docker-rebuild \
postgres-docker-debug-build postgres-docker-debug-run postgres-docker-debug-rebuild \
postgres-docker-shell postgres-dev-rebuild postgres-help unittest-pg \
postgres-supabase-build postgres-supabase-rebuild postgres-supabase-run-smoke-test \
postgres-docker-run-smoke-test

# Verify that a cloudsync--<prev>--<curr>.sql upgrade script exists for the
# current CLOUDSYNC_VERSION in src/cloudsync.h. Release-blocking: a missing
# upgrade script silently breaks ALTER EXTENSION cloudsync UPDATE for every
# existing deployment. Runs in <1s; safe to call on every PR.
postgres-check-migration:
@scripts/check-postgres-migration.sh

# Check if PostgreSQL is available
postgres-check:
@echo "Checking PostgreSQL installation..."
@which $(PG_CONFIG) > /dev/null || (echo "Error: pg_config not found. Install postgresql-server-dev." && exit 1)
@[ -n "$(CLOUDSYNC_VERSION_FULL)" ] || (echo "Error: could not read CLOUDSYNC_VERSION from src/cloudsync.h" && exit 1)
@[ -n "$(EXTVERSION)" ] || (echo "Error: could not derive MAJOR.MINOR EXTVERSION from CLOUDSYNC_VERSION '$(CLOUDSYNC_VERSION_FULL)'" && exit 1)
@echo "PostgreSQL version: $$($(PG_CONFIG) --version)"
@echo "CloudSync version : $(CLOUDSYNC_VERSION_FULL) (extension version $(EXTVERSION))"
@echo "Extension directory: $(PG_PKGLIBDIR)"
@echo "Share directory: $(PG_SHAREDIR)"
@echo "Include directory: $(PG_INCLUDEDIR)"

# Render the versioned .sql install script and .control file from their .in
# templates. This is a phony target (rather than file rules keyed on
# $(PG_EXTENSION_SQL) / $(PG_EXTENSION_CONTROL)) so that $(EXTVERSION) never
# appears in a rule's target or prerequisite position — those expansions
# happen at parse time and would force sed/cut to run on every `make`
# invocation, including for non-PG goals. Here, the references live inside
# the recipe body and are only evaluated when this target actually fires.
# Writes via a .tmp + atomic mv so a failed sed can't leave a half-rendered
# output file in the extension share dir.
postgres-generate-files: postgres-check
@echo "Rendering extension files at version $(EXTVERSION) (binary $(CLOUDSYNC_VERSION_FULL))"
@sed 's/@EXTVERSION@/$(EXTVERSION)/g' $(PG_EXTENSION_SQL_IN) > $(PG_EXTENSION_SQL).tmp && mv $(PG_EXTENSION_SQL).tmp $(PG_EXTENSION_SQL)
@sed 's/@EXTVERSION@/$(EXTVERSION)/g' $(PG_EXTENSION_CONTROL_IN) > $(PG_EXTENSION_CONTROL).tmp && mv $(PG_EXTENSION_CONTROL).tmp $(PG_EXTENSION_CONTROL)

# Build PostgreSQL extension
postgres-build: postgres-check
@echo "Building PostgreSQL extension..."
postgres-build: postgres-generate-files
@echo "Building PostgreSQL extension (version $(EXTVERSION))..."
@echo "Compiling source files..."
@for src in $(PG_ALL_SRC); do \
echo " CC $$src"; \
Expand All @@ -125,26 +167,37 @@ postgres-install: postgres-build
install -m 644 $(PG_EXTENSION_SQL) $(PG_SHAREDIR)/extension/
@echo "Installing control file to $(PG_SHAREDIR)/extension/"
install -m 644 $(PG_EXTENSION_CONTROL) $(PG_SHAREDIR)/extension/
@if [ -n "$(PG_MIGRATION_SQLS)" ]; then \
echo "Installing $(words $(PG_MIGRATION_SQLS)) migration script(s) to $(PG_SHAREDIR)/extension/"; \
install -m 644 $(PG_MIGRATION_SQLS) $(PG_SHAREDIR)/extension/; \
fi
@echo ""
@echo "Installation complete!"
@echo "To use the extension, run in psql:"
@echo " CREATE EXTENSION $(EXTENSION);"
@echo "To upgrade an existing installation, run in psql:"
@echo " ALTER EXTENSION $(EXTENSION) UPDATE;"

# Package extension files for distribution
PG_DIST_DIR = dist/postgresql

postgres-package: postgres-build
@echo "Packaging PostgreSQL extension..."
@echo "Packaging PostgreSQL extension (version $(EXTVERSION))..."
@mkdir -p $(PG_DIST_DIR)
cp $(PG_EXTENSION_LIB) $(PG_DIST_DIR)/
cp $(PG_EXTENSION_SQL) $(PG_DIST_DIR)/
cp $(PG_EXTENSION_CONTROL) $(PG_DIST_DIR)/
@if [ -n "$(PG_MIGRATION_SQLS)" ]; then \
echo "Including $(words $(PG_MIGRATION_SQLS)) migration script(s)"; \
cp $(PG_MIGRATION_SQLS) $(PG_DIST_DIR)/; \
fi
@echo "Package ready in $(PG_DIST_DIR)/"

# Clean PostgreSQL build artifacts
postgres-clean:
@echo "Cleaning PostgreSQL build artifacts..."
rm -f $(PG_OBJS) $(PG_EXTENSION_LIB)
rm -f $(PG_EXTENSION_SQL) $(PG_EXTENSION_CONTROL)
@echo "Clean complete"

# Test extension (requires running PostgreSQL)
Expand Down Expand Up @@ -314,7 +367,7 @@ postgres-supabase-build:
echo "Using base image: $$supabase_cli_image"; \
echo "Pulling fresh base image to avoid layer accumulation..."; \
docker pull "$$supabase_cli_image" 2>/dev/null || true; \
docker build --build-arg SUPABASE_POSTGRES_TAG="$(SUPABASE_POSTGRES_TAG)" -f "$$tmp_dockerfile" -t "$$supabase_cli_image" .; \
docker build --build-arg SUPABASE_POSTGRES_TAG="$(SUPABASE_POSTGRES_TAG)" --build-arg CLOUDSYNC_VERSION="$(CLOUDSYNC_VERSION_FULL)" -f "$$tmp_dockerfile" -t "$$supabase_cli_image" .; \
rm -f "$$tmp_dockerfile"; \
echo "Build complete: $$supabase_cli_image"

Expand Down Expand Up @@ -361,6 +414,8 @@ postgres-help:
@echo ""
@echo "Build & Install:"
@echo " postgres-check - Verify PostgreSQL installation"
@echo " postgres-check-migration - Verify a migration script exists for the current version"
@echo " postgres-generate-files - Render cloudsync.control and cloudsync--<ext>.sql from templates"
@echo " postgres-build - Build extension (.so file)"
@echo " postgres-install - Install extension to PostgreSQL"
@echo " postgres-clean - Clean build artifacts"
Expand Down
2 changes: 1 addition & 1 deletion docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ docker/
│ ├── Dockerfile # Custom PostgreSQL image
│ ├── docker-compose.yml
│ ├── init.sql # CloudSync metadata tables
│ └── cloudsync.control
│ └── cloudsync.control.in # template; cloudsync.control is generated at build time
```

## Option 1: Standalone PostgreSQL
Expand Down
2 changes: 1 addition & 1 deletion docker/postgresql/Dockerfile.release
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ RUN case "${TARGETARCH}" in \
mkdir -p /tmp/cloudsync && \
tar -xzf /tmp/cloudsync.tar.gz -C /tmp/cloudsync && \
install -m 755 /tmp/cloudsync/cloudsync.so "$(pg_config --pkglibdir)/" && \
install -m 644 /tmp/cloudsync/cloudsync--1.0.sql "$(pg_config --sharedir)/extension/" && \
install -m 644 /tmp/cloudsync/cloudsync--*.sql "$(pg_config --sharedir)/extension/" && \
install -m 644 /tmp/cloudsync/cloudsync.control "$(pg_config --sharedir)/extension/" && \
rm -rf /tmp/cloudsync /tmp/cloudsync.tar.gz && \
apt-get purge -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
Expand Down
14 changes: 11 additions & 3 deletions docker/postgresql/Dockerfile.supabase
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,21 @@ RUN if [ ! -x "$CLOUDSYNC_PG_CONFIG" ]; then \
# Collect build artifacts (avoid installing into the Nix store)
RUN mkdir -p /tmp/cloudsync-artifacts/lib /tmp/cloudsync-artifacts/extension && \
cp /tmp/cloudsync/cloudsync.so /tmp/cloudsync-artifacts/lib/ && \
cp /tmp/cloudsync/src/postgresql/cloudsync--1.0.sql /tmp/cloudsync-artifacts/extension/ && \
cp /tmp/cloudsync/docker/postgresql/cloudsync.control /tmp/cloudsync-artifacts/extension/
cp /tmp/cloudsync/src/postgresql/cloudsync--*.sql /tmp/cloudsync-artifacts/extension/ && \
cp /tmp/cloudsync/docker/postgresql/cloudsync.control /tmp/cloudsync-artifacts/extension/ && \
# Include per-release upgrade scripts so ALTER EXTENSION ... UPDATE works
if ls /tmp/cloudsync/src/postgresql/migrations/cloudsync--*--*.sql 1>/dev/null 2>&1; then \
cp /tmp/cloudsync/src/postgresql/migrations/cloudsync--*--*.sql /tmp/cloudsync-artifacts/extension/; \
fi

# Runtime image based on Supabase Postgres
ARG SUPABASE_POSTGRES_TAG=17.6.1.071
FROM public.ecr.aws/supabase/postgres:${SUPABASE_POSTGRES_TAG}

# Extension version (derived from src/cloudsync.h by the Makefile and passed in
# as a build arg); used only for the image label.
ARG CLOUDSYNC_VERSION=unknown

# Match builder pg_config path
ENV CLOUDSYNC_PG_CONFIG=/root/.nix-profile/bin/pg_config

Expand Down Expand Up @@ -85,5 +93,5 @@ EXPOSE 5432
WORKDIR /

# Add label with extension version
LABEL org.sqliteai.cloudsync.version="1.0" \
LABEL org.sqliteai.cloudsync.version="${CLOUDSYNC_VERSION}" \
org.sqliteai.cloudsync.description="PostgreSQL with CloudSync CRDT extension"
4 changes: 2 additions & 2 deletions docker/postgresql/Dockerfile.supabase.release
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ RUN case "${TARGETARCH}" in \
SHAREDIR_STD="/usr/share/postgresql" && \
install -d "$PKGLIBDIR" "$SHAREDIR_PGCONFIG/extension" && \
install -m 755 /tmp/cloudsync/cloudsync.so "$PKGLIBDIR/" && \
install -m 644 /tmp/cloudsync/cloudsync--1.0.sql /tmp/cloudsync/cloudsync.control "$SHAREDIR_PGCONFIG/extension/" && \
install -m 644 /tmp/cloudsync/cloudsync--*.sql /tmp/cloudsync/cloudsync.control "$SHAREDIR_PGCONFIG/extension/" && \
if [ "$SHAREDIR_STD" != "$SHAREDIR_PGCONFIG" ]; then \
install -d "$SHAREDIR_STD/extension" && \
install -m 644 /tmp/cloudsync/cloudsync--1.0.sql /tmp/cloudsync/cloudsync.control "$SHAREDIR_STD/extension/"; \
install -m 644 /tmp/cloudsync/cloudsync--*.sql /tmp/cloudsync/cloudsync.control "$SHAREDIR_STD/extension/"; \
fi && \
rm -rf /tmp/cloudsync /tmp/cloudsync.tar.gz && \
apt-get purge -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
Expand Down
22 changes: 0 additions & 22 deletions docker/postgresql/cloudsync.control

This file was deleted.

13 changes: 13 additions & 0 deletions docker/postgresql/cloudsync.control.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# CloudSync PostgreSQL Extension Control File
#
# Generated from cloudsync.control.in by docker/Makefile.postgresql.
# Do not edit the generated file; edit the .in template instead.
# The version below is read from CLOUDSYNC_VERSION in src/cloudsync.h.

comment = 'CloudSync - CRDT-based multi-master database synchronization'
default_version = '@EXTVERSION@'
relocatable = true
requires = ''
superuser = false
module_pathname = '$libdir/cloudsync'
trusted = true
9 changes: 5 additions & 4 deletions docs/internal/supabase-flyio.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,10 @@ The `make postgres-supabase-build` command does the following:
1. **Pulls the official Supabase Postgres base image** (e.g., `public.ecr.aws/supabase/postgres:15.8.1.085`) — this is Supabase's standard PostgreSQL image that ships with ~30 extensions pre-installed (PostGIS, pgvector, etc.)
2. **Runs a multi-stage Docker build** using `docker/postgresql/Dockerfile.supabase`:
- **Stage 1 (builder)**: Installs C build tools (`gcc`, `make`), copies the CloudSync source code (`src/`, `modules/`), and compiles `cloudsync.so` against Supabase's `pg_config`
- **Stage 2 (runtime)**: Starts from a clean Supabase Postgres image and copies in just three files:
- **Stage 2 (runtime)**: Starts from a clean Supabase Postgres image and copies in just three kinds of file:
- `cloudsync.so` — the compiled extension binary
- `cloudsync.control` — tells PostgreSQL the extension's name and version
- `cloudsync--1.0.sql` — the SQL that defines all CloudSync functions
- `cloudsync.control` — tells PostgreSQL the extension's name and default version (generated at build time from `cloudsync.control.in`, with the version read from `src/cloudsync.h`)
- `cloudsync--<version>.sql` — the SQL that defines all CloudSync functions for the current release (e.g. `cloudsync--1.0.16.sql`), plus any `cloudsync--<from>--<to>.sql` upgrade scripts shipped under `src/postgresql/migrations/`
3. **Tags the result** with the same name as the base image, so it's a drop-in replacement

To find the correct tag, clone the Supabase repo and check:
Expand Down Expand Up @@ -118,7 +118,8 @@ Verify CloudSync is installed inside the image:
```bash
docker run --rm <your-dockerhub-username>/supabase-postgres-cloudsync:15.8.1.085 \
find / -name "cloudsync*" -type f 2>/dev/null
# Should list cloudsync.so, cloudsync.control, and cloudsync--1.0.sql
# Should list cloudsync.so, cloudsync.control, and cloudsync--<version>.sql
# (plus any cloudsync--<from>--<to>.sql upgrade scripts)
# in /nix/store/...-postgresql-and-plugins-15.8/ paths
```

Expand Down
27 changes: 25 additions & 2 deletions docs/postgresql/quickstarts/postgres.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ docker compose up -d

If you already run PostgreSQL directly on a VM or bare metal, download the release tarball that matches your operating system, CPU architecture, and PostgreSQL major version.

Extract the archive, then copy the three extension files into PostgreSQL's extension directories:
Extract the archive, then copy the extension files into PostgreSQL's extension directories. The tarball ships `cloudsync.control`, a `cloudsync--<version>.sql` install script for the current release, and — from release 1.0.17 onward — any `cloudsync--<from>--<to>.sql` upgrade scripts needed so existing installations can run `ALTER EXTENSION cloudsync UPDATE`.

```bash
cp cloudsync.so "$(pg_config --pkglibdir)/"
cp cloudsync.control cloudsync--1.0.sql "$(pg_config --sharedir)/extension/"
cp cloudsync.control cloudsync--*.sql "$(pg_config --sharedir)/extension/"
```

Then connect to PostgreSQL and enable the extension:
Expand All @@ -80,6 +80,29 @@ psql -U postgres -d postgres -c "SELECT cloudsync_version();"

If the extension is installed correctly, PostgreSQL returns the CloudSync version string.

### Upgrading a later release

CloudSync uses the first two components of its semver as the PostgreSQL extension version (for example, `1.0.17` installs as extension version `1.0`). How you upgrade depends on which component changed:

- **PATCH release** (e.g. `1.0.17 → 1.0.18`): pull the new Docker image or replace the extension files on disk and restart PostgreSQL. No SQL-level upgrade is needed — `installed_version` stays at `1.0` and the new binary takes over on reconnect. `SELECT cloudsync_version();` confirms the new semver.
- **MINOR or MAJOR release** (e.g. `1.0.x → 1.1.0`): pull the new artifacts as above, then run once per database:

```sql
ALTER EXTENSION cloudsync UPDATE;
```

PostgreSQL applies any `cloudsync--<from>--<to>.sql` upgrade scripts shipped with the release and moves `installed_version` to the new value.

You can check the current state at any time:

```sql
SELECT name, default_version, installed_version
FROM pg_available_extensions
WHERE name = 'cloudsync';
```

If `installed_version` is behind `default_version` after a release, run `ALTER EXTENSION cloudsync UPDATE;` to catch up.

---

## Step 3: Register Your Database in the CloudSync Dashboard
Expand Down
Loading
Loading