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
1 change: 1 addition & 0 deletions MODULES.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
### Job Management
- [ ] `packages/jobs/jobs` - Core job system
- [ ] `packages/jobs/database-jobs` - Database-specific job handling
- [ ] `packages/pgpm-cron` - pg_cron sync for the scheduled_cron registry

## Meta & Database Introspection

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
-- Deploy schemas/metaschema_public/tables/scheduled_cron/table to pg

-- requires: schemas/metaschema_public/schema
-- requires: schemas/metaschema_public/tables/database/table

BEGIN;

CREATE TABLE metaschema_public.scheduled_cron (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
database_id uuid NOT NULL,
name text NOT NULL,
schedule text NOT NULL,
command text NOT NULL,
is_enabled boolean NOT NULL DEFAULT true,

created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now(),

CONSTRAINT scheduled_cron_database_fkey
FOREIGN KEY (database_id)
REFERENCES metaschema_public.database (id)
ON DELETE CASCADE,

CONSTRAINT scheduled_cron_database_name_unique
UNIQUE (database_id, name)
);

CREATE INDEX scheduled_cron_database_id_idx
ON metaschema_public.scheduled_cron (database_id);

COMMIT;
1 change: 1 addition & 0 deletions packages/metaschema-schema/pgpm.plan
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ schemas/metaschema_public/tables/function/table [schemas/metaschema_public/schem
schemas/metaschema_public/tables/partition/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/tables/table/table schemas/metaschema_public/tables/field/table] 2026-05-26T00:00:00Z Constructive <developers@constructive.io> # add metaschema_public.partition table for pg_partman lifecycle config
schemas/metaschema_public/tables/composite_type/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/tables/schema/table schemas/metaschema_public/types/object_category] 2026-05-29T00:00:00Z devin <devin@cognition.ai> # add metaschema_public.composite_type table for generated composite types
schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet [schemas/metaschema_public/tables/schema/table] 2026-06-18T00:00:03Z devin <devin@cognition.ai> # enforce one-way ratchet on api_exposure (never_expose is permanent)
schemas/metaschema_public/tables/scheduled_cron/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table] 2026-07-08T00:00:00Z devin <devin@cognition.ai> # add metaschema_public.scheduled_cron table for declarative pg_cron job tracking
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Revert schemas/metaschema_public/tables/scheduled_cron/table from pg

BEGIN;

DROP TABLE metaschema_public.scheduled_cron;

COMMIT;
21 changes: 20 additions & 1 deletion packages/metaschema-schema/sql/metaschema-schema--0.15.5.sql
Original file line number Diff line number Diff line change
Expand Up @@ -868,4 +868,23 @@ CREATE TRIGGER _000003_enforce_api_exposure_ratchet
ON metaschema_public.schema
FOR EACH ROW
WHEN (new.api_exposure IS DISTINCT FROM old.api_exposure)
EXECUTE PROCEDURE metaschema_public.tg_enforce_api_exposure_ratchet();
EXECUTE PROCEDURE metaschema_public.tg_enforce_api_exposure_ratchet();

CREATE TABLE metaschema_public.scheduled_cron (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
database_id uuid NOT NULL,
name text NOT NULL,
schedule text NOT NULL,
command text NOT NULL,
is_enabled boolean NOT NULL DEFAULT true,
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now(),
CONSTRAINT scheduled_cron_database_fkey
FOREIGN KEY(database_id)
REFERENCES metaschema_public.database (id)
ON DELETE CASCADE,
CONSTRAINT scheduled_cron_database_name_unique
UNIQUE (database_id, name)
);

CREATE INDEX scheduled_cron_database_id_idx ON metaschema_public.scheduled_cron (database_id);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Verify schemas/metaschema_public/tables/scheduled_cron/table on pg

BEGIN;

SELECT verify_table ('metaschema_public.scheduled_cron');

ROLLBACK;
2 changes: 2 additions & 0 deletions packages/pgpm-cron/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__tests__
jest.config.js
22 changes: 22 additions & 0 deletions packages/pgpm-cron/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
The MIT License (MIT)

Copyright (c) 2025 Dan Lynch <pyramation@gmail.com>
Copyright (c) 2025 Constructive

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
6 changes: 6 additions & 0 deletions packages/pgpm-cron/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
EXTENSION = pgpm-cron
DATA = sql/pgpm-cron--0.0.1.sql

PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)
52 changes: 52 additions & 0 deletions packages/pgpm-cron/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# @pgpm/cron

Open-code pg_cron sync for the `metaschema_public.scheduled_cron` registry.

## Overview

`metaschema_public.scheduled_cron` is the declarative source of truth for which
recurring jobs should exist for a database (populated automatically by metaschema
triggers — e.g. registering `maintenance:partman` when a partitioned table is
parented). This module contains the **open-code** mechanism that reconciles that
registry into [pg_cron](https://github.com/citusdata/pg_cron), so it is safe to
ship to exported/consumer databases without any platform-proprietary code.

It targets the **local, single-database** deployment shape, where `pg_cron` is
installed in the same database that holds the `scheduled_cron` rows. The platform
cross-database case (one `pg_cron` in `postgres` fanning out to many app/tenant
databases via `cron.schedule_in_database()`) is handled separately by the
platform deploy flow, not by this module.

## pg_cron placement (important)

`pg_cron` is installed **once per cluster** — in a single database (the "cron
database", `postgres` by default), which is the only place `CREATE EXTENSION
pg_cron` and the `cron.job` table exist. It also requires
`shared_preload_libraries = 'pg_cron'` + a restart. So `pg_cron` is **not**
present in the platform database or in tenant/app databases, and it cannot be a
hard `requires` in this module's control file.

- **Local single-DB dev:** app, registry, and `pg_cron` are all one database, so
`sync_scheduled_cron()` uses `cron.schedule()` directly (this module).
- **Platform:** the registry lives in the platform DB but scheduling must run
from the cron DB via `cron.schedule_in_database(..., target => platform_db)`.
That cross-database fan-out is handled by the platform deploy flow, not here.

## What it does

`pgpm_cron.sync_scheduled_cron()`:

- Silently no-ops when `pg_cron` is not installed — the table remains the source
of truth for an external scheduler.
- Schedules every `is_enabled` row via `cron.schedule(name, schedule, command)`.
- Unschedules rows that are present but disabled.

`database_id` on `scheduled_cron` is operational metadata (which database a job
targets / cascade cleanup) and is intentionally ignored here: in the local
single-database shape every row belongs to the one database that runs the sync.

## Notes

- Removal of a job when its `scheduled_cron` row is deleted is expected to be
driven by a delete-side sync in the platform metadatabase; this reconcile
function only handles enabled/disabled state for rows that still exist.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
-- Deploy schemas/pgpm_cron/procedures/sync_scheduled_cron to pg

-- requires: schemas/pgpm_cron/schema
-- requires: metaschema-schema:schemas/metaschema_public/tables/scheduled_cron/table

BEGIN;

-- Reconcile the declarative metaschema_public.scheduled_cron registry into
-- pg_cron for the local (single-database) deployment shape, where pg_cron lives
-- in the same database that owns the scheduled_cron rows. Open-code only: safe
-- to ship to exported/consumer databases. The platform cross-database case
-- (one pg_cron fanning out via cron.schedule_in_database) is handled by the
-- platform deploy flow, not here.
--
-- database_id is intentionally not consulted: in the local shape every row
-- belongs to the one database running the sync.
CREATE FUNCTION pgpm_cron.sync_scheduled_cron()
RETURNS void
AS $$
DECLARE
v_row record;
BEGIN
-- pg_cron lives in one database per cluster (the cron database), so it is
-- absent in most databases this module ships to. Where absent, the registry
-- is still the source of truth for an external scheduler; nothing to do here.
IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_cron') THEN
RETURN;
END IF;

FOR v_row IN
SELECT name, schedule, command, is_enabled
FROM metaschema_public.scheduled_cron
LOOP
IF v_row.is_enabled THEN
PERFORM cron.schedule(v_row.name, v_row.schedule, v_row.command);
ELSIF EXISTS (SELECT 1 FROM cron.job WHERE jobname = v_row.name) THEN
PERFORM cron.unschedule(v_row.name);
END IF;
END LOOP;
END;
$$
LANGUAGE 'plpgsql' VOLATILE;

COMMIT;
7 changes: 7 additions & 0 deletions packages/pgpm-cron/deploy/schemas/pgpm_cron/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Deploy schemas/pgpm_cron/schema to pg

BEGIN;

CREATE SCHEMA pgpm_cron;

COMMIT;
15 changes: 15 additions & 0 deletions packages/pgpm-cron/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',

// Match both __tests__ and colocated test files
testMatch: ['**/?(*.)+(test|spec).{ts,tsx,js,jsx}'],

// Ignore build artifacts and type declarations
testPathIgnorePatterns: ['/dist/', '\\.d\\.ts$'],
modulePathIgnorePatterns: ['<rootDir>/dist/'],
watchPathIgnorePatterns: ['/dist/'],

moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
};
35 changes: 35 additions & 0 deletions packages/pgpm-cron/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "@pgpm/cron",
"version": "0.0.1",
"description": "open-code pg_cron sync for the scheduled_cron registry",
"author": "Dan Lynch <pyramation@gmail.com>",
"contributors": [
"Constructive <developers@constructive.io>"
],
"keywords": [
"postgresql",
"pgpm",
"cron",
"pg_cron",
"scheduler"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"bundle": "pgpm package",
"test": "jest",
"test:watch": "jest --watch"
},
"devDependencies": {
"pgpm": "^4.28.7"
},
"repository": {
"type": "git",
"url": "https://github.com/constructive-io/pgpm-modules"
},
"homepage": "https://github.com/constructive-io/pgpm-modules",
"bugs": {
"url": "https://github.com/constructive-io/pgpm-modules/issues"
}
}
7 changes: 7 additions & 0 deletions packages/pgpm-cron/pgpm-cron.control
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# pgpm-cron extension
comment = 'open-code pg_cron sync for the scheduled_cron registry'
default_version = '0.0.1'
module_pathname = '$libdir/pgpm-cron'
requires = 'plpgsql,metaschema-schema'
relocatable = false
superuser = false
6 changes: 6 additions & 0 deletions packages/pgpm-cron/pgpm.plan
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
%syntax-version=1.0.0
%project=pgpm-cron
%uri=pgpm-cron

schemas/pgpm_cron/schema 2026-07-08T00:00:00Z devin <devin@cognition.ai> # add pgpm_cron schema
schemas/pgpm_cron/procedures/sync_scheduled_cron [schemas/pgpm_cron/schema metaschema-schema:schemas/metaschema_public/tables/scheduled_cron/table] 2026-07-08T00:00:01Z devin <devin@cognition.ai> # sync scheduled_cron registry rows into pg_cron (local single-database deployments)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Revert schemas/pgpm_cron/procedures/sync_scheduled_cron from pg

BEGIN;

DROP FUNCTION pgpm_cron.sync_scheduled_cron();

COMMIT;
7 changes: 7 additions & 0 deletions packages/pgpm-cron/revert/schemas/pgpm_cron/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Revert schemas/pgpm_cron/schema from pg

BEGIN;

DROP SCHEMA pgpm_cron;

COMMIT;
26 changes: 26 additions & 0 deletions packages/pgpm-cron/sql/pgpm-cron--0.0.1.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
\echo Use "CREATE EXTENSION pgpm-cron" to load this file. \quit
CREATE SCHEMA pgpm_cron;

CREATE FUNCTION pgpm_cron.sync_scheduled_cron() RETURNS void AS $EOFCODE$
DECLARE
v_row record;
BEGIN
-- pg_cron lives in one database per cluster (the cron database), so it is
-- absent in most databases this module ships to. Where absent, the registry
-- is still the source of truth for an external scheduler; nothing to do here.
IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_cron') THEN
RETURN;
END IF;

FOR v_row IN
SELECT name, schedule, command, is_enabled
FROM metaschema_public.scheduled_cron
LOOP
IF v_row.is_enabled THEN
PERFORM cron.schedule(v_row.name, v_row.schedule, v_row.command);
ELSIF EXISTS (SELECT 1 FROM cron.job WHERE jobname = v_row.name) THEN
PERFORM cron.unschedule(v_row.name);
END IF;
END LOOP;
END;
$EOFCODE$ LANGUAGE plpgsql VOLATILE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Verify schemas/pgpm_cron/procedures/sync_scheduled_cron on pg

BEGIN;

SELECT has_function_privilege('pgpm_cron.sync_scheduled_cron()', 'execute');

ROLLBACK;
7 changes: 7 additions & 0 deletions packages/pgpm-cron/verify/schemas/pgpm_cron/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Verify schemas/pgpm_cron/schema on pg

BEGIN;

SELECT 1/count(*) FROM pg_catalog.pg_namespace WHERE nspname = 'pgpm_cron';

ROLLBACK;
Loading
Loading