@astralarium/repo-postgres 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +38 -2
- package/container/init/.containerignore +2 -0
- package/container/init/Containerfile +26 -0
- package/container/init/entrypoint.d/init-db.sh +20 -0
- package/container/snapshot/run-snapshot.sh +23 -0
- package/container/util/.containerignore +2 -0
- package/container/util/Containerfile +7 -0
- package/container/util/entrypoint.d/dump-db.sh +19 -0
- package/container/util/entrypoint.d/upgrade-db.sh +103 -0
- package/dist/bin/stage-schema.d.ts +2 -0
- package/dist/bin/stage-schema.js +20 -0
- package/dist/src/provision/connection.d.ts +11 -0
- package/dist/src/provision/connection.js +33 -0
- package/dist/src/provision/container.d.ts +87 -0
- package/dist/src/provision/container.js +244 -0
- package/dist/src/provision/data.d.ts +8 -0
- package/dist/src/provision/data.js +47 -0
- package/dist/src/provision/disposable.d.ts +49 -0
- package/dist/src/provision/disposable.js +188 -0
- package/dist/src/provision/image.d.ts +63 -0
- package/dist/src/provision/image.js +159 -0
- package/dist/src/provision/index.d.ts +17 -0
- package/dist/src/provision/index.js +205 -0
- package/dist/src/provision/options.d.ts +23 -0
- package/dist/src/provision/options.js +1 -0
- package/dist/src/provision/schema.d.ts +11 -0
- package/dist/src/provision/schema.js +134 -0
- package/dist/src/provision/test-database.d.ts +29 -0
- package/dist/src/provision/test-database.js +130 -0
- package/dist/src/queue.d.ts +86 -0
- package/dist/src/queue.js +201 -0
- package/dist/src/schema/index.d.ts +3 -0
- package/dist/src/schema/index.js +3 -0
- package/dist/src/schema/module.d.ts +59 -0
- package/dist/src/schema/module.js +122 -0
- package/dist/src/schema/modules.d.ts +24 -0
- package/dist/src/schema/modules.js +71 -0
- package/dist/src/sql.d.ts +83 -0
- package/dist/src/sql.js +146 -0
- package/dist/testkit/index.d.ts +14 -0
- package/dist/testkit/index.js +32 -0
- package/dist/testkit/locks.d.ts +27 -0
- package/dist/testkit/locks.js +57 -0
- package/dist/testkit/schema.d.ts +14 -0
- package/dist/testkit/schema.js +89 -0
- package/package.json +63 -3
- package/scripts/build-images.sh +52 -0
- package/scripts/dump-db.sh +32 -0
- package/sql/app/schema.sql +8 -0
- package/sql/bootstrap.sql +27 -0
- package/sql/database-grants.sql +3 -0
- package/sql/drop-schemas.sql +11 -0
- package/sql/extensions/cron/schema.sql +23 -0
- package/sql/extensions/partman/before-reset.sql +38 -0
- package/sql/extensions/partman/schema.sql +8 -0
- package/sql/extensions/trgm/schema.sql +1 -0
- package/sql/reset.sql +22 -0
- package/sql/roles.sql +10 -0
- package/sql/slug/schema.sql +25 -0
- package/sql/taskclaim/jobs/schema.sql +19 -0
- package/sql/taskclaim/schema.sql +41 -0
- package/sql/taskclaim/scrub.sql +3 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Usage: postgres-dump-db > data.sql
|
|
3
|
+
# Data-only dump of the PG* database via the util image, rebuilt on each
|
|
4
|
+
# run (layer-cached); the replica role prefix skips triggers and FK checks
|
|
5
|
+
# on load.
|
|
6
|
+
# Source connection via PGHOST/PGPORT/PGUSER/PGPASSWORD/PGDATABASE.
|
|
7
|
+
# DB_NAME: the app's image base name (PostgresOptions.name); required.
|
|
8
|
+
# DB_DUMP_EXCLUDE: schema-qualified pg_dump table patterns whose rows stay
|
|
9
|
+
# out of the dump, space-separated (`app.session`, `app.*_log`); a
|
|
10
|
+
# partitioned table covers its partitions.
|
|
11
|
+
# CONTAINER_RUNTIME: container runtime (default podman).
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
runtime=${CONTAINER_RUNTIME:-podman}
|
|
15
|
+
name=${DB_NAME:?set to the app image base name (PostgresOptions.name)}
|
|
16
|
+
read -ra patterns <<< "${DB_DUMP_EXCLUDE:-}"
|
|
17
|
+
for pattern in ${patterns[@]+"${patterns[@]}"}; do
|
|
18
|
+
[[ $pattern == *.* ]] || { echo "postgres-dump-db: DB_DUMP_EXCLUDE pattern $pattern lacks a schema" >&2; exit 1; }
|
|
19
|
+
done
|
|
20
|
+
|
|
21
|
+
# Through the real path: a package manager's bin link sits outside the package.
|
|
22
|
+
scripts=$(dirname "$(realpath "${BASH_SOURCE[0]}")")
|
|
23
|
+
bash "$scripts/build-images.sh" "$runtime" "" util
|
|
24
|
+
|
|
25
|
+
"$runtime" run --rm --interactive \
|
|
26
|
+
--env PGHOST \
|
|
27
|
+
--env PGPORT \
|
|
28
|
+
--env PGUSER \
|
|
29
|
+
--env PGPASSWORD \
|
|
30
|
+
--env PGDATABASE \
|
|
31
|
+
--env DB_DUMP_EXCLUDE \
|
|
32
|
+
"$name-util" /entrypoint.d/dump-db.sh
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-- Fresh remote (Neon) init: roles + app database + the app's schema.
|
|
2
|
+
-- Local-container equivalent: container/init/entrypoint.d/init-db.sh (first boot).
|
|
3
|
+
-- Run with psql from a staged schema directory (schema.sql beside sql/, as
|
|
4
|
+
-- `@astralarium/repo-postgres/provision` stageSchema writes it), with -v pw=<app_service
|
|
5
|
+
-- password>. With existing roles and database, use reset.sql.
|
|
6
|
+
|
|
7
|
+
\i roles.sql
|
|
8
|
+
-- app_service is the Hyperdrive origin login.
|
|
9
|
+
ALTER USER app_service WITH PASSWORD :'pw';
|
|
10
|
+
|
|
11
|
+
CREATE DATABASE app;
|
|
12
|
+
|
|
13
|
+
-- Neon: extension objects are cloud_admin-owned, so the schema's per-object
|
|
14
|
+
-- grants on them no-op. These stand in for them.
|
|
15
|
+
DO $$
|
|
16
|
+
BEGIN
|
|
17
|
+
IF to_regrole('neon_superuser') IS NOT NULL THEN
|
|
18
|
+
SET ROLE neon_superuser;
|
|
19
|
+
GRANT pg_read_all_data, pg_write_all_data TO app_admin;
|
|
20
|
+
RESET ROLE;
|
|
21
|
+
END IF;
|
|
22
|
+
END $$;
|
|
23
|
+
|
|
24
|
+
\connect app
|
|
25
|
+
|
|
26
|
+
\i database-grants.sql
|
|
27
|
+
\i schema.sql
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
-- Drops every schema the current role (app_admin, under reset.sql) owns.
|
|
2
|
+
DO $$
|
|
3
|
+
DECLARE
|
|
4
|
+
schema_name text;
|
|
5
|
+
BEGIN
|
|
6
|
+
FOR schema_name IN
|
|
7
|
+
SELECT nspname FROM pg_namespace WHERE nspowner = to_regrole(current_user)
|
|
8
|
+
LOOP
|
|
9
|
+
EXECUTE format('DROP SCHEMA %I CASCADE', schema_name);
|
|
10
|
+
END LOOP;
|
|
11
|
+
END $$;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- pg_cron in the database `cron.database_name` selects, with a daily
|
|
2
|
+
-- `cron-history-ttl` deleting run history past 7 days. Skipped where
|
|
3
|
+
-- pg_cron is not loaded: disposable instances run stock Postgres.
|
|
4
|
+
DO $$
|
|
5
|
+
BEGIN
|
|
6
|
+
IF current_setting('cron.database_name', true) IS NULL THEN
|
|
7
|
+
RAISE NOTICE 'pg_cron is not loaded; skipping';
|
|
8
|
+
RETURN;
|
|
9
|
+
END IF;
|
|
10
|
+
|
|
11
|
+
CREATE EXTENSION IF NOT EXISTS pg_cron;
|
|
12
|
+
|
|
13
|
+
PERFORM cron.schedule(
|
|
14
|
+
'cron-history-ttl',
|
|
15
|
+
'0 3 * * *',
|
|
16
|
+
$job$
|
|
17
|
+
SET statement_timeout = '30s';
|
|
18
|
+
DELETE FROM cron.job_run_details
|
|
19
|
+
WHERE end_time < CURRENT_TIMESTAMP - interval '7 days';
|
|
20
|
+
$job$
|
|
21
|
+
);
|
|
22
|
+
END;
|
|
23
|
+
$$;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
-- pg_partman outlives reset.sql's drop (cloud_admin-owned on Neon) and
|
|
2
|
+
-- keeps what it owns: a surviving part_config row makes create_parent
|
|
3
|
+
-- reject its parent on reload, and each parent's template table lives in
|
|
4
|
+
-- partman's schema with its app-typed columns. Clears both for every
|
|
5
|
+
-- schema the current role (app_admin, under reset.sql) owns, so the
|
|
6
|
+
-- reload takes the new shape.
|
|
7
|
+
DO $$
|
|
8
|
+
DECLARE
|
|
9
|
+
partman text;
|
|
10
|
+
schema_name text;
|
|
11
|
+
template text;
|
|
12
|
+
BEGIN
|
|
13
|
+
SELECT n.nspname INTO partman
|
|
14
|
+
FROM pg_extension e JOIN pg_namespace n ON n.oid = e.extnamespace
|
|
15
|
+
WHERE e.extname = 'pg_partman';
|
|
16
|
+
|
|
17
|
+
IF partman IS NULL THEN
|
|
18
|
+
RETURN;
|
|
19
|
+
END IF;
|
|
20
|
+
|
|
21
|
+
FOR schema_name IN
|
|
22
|
+
SELECT nspname FROM pg_namespace WHERE nspowner = to_regrole(current_user)
|
|
23
|
+
LOOP
|
|
24
|
+
FOR template IN
|
|
25
|
+
EXECUTE format(
|
|
26
|
+
'SELECT template_table FROM %I.part_config WHERE template_table IS NOT NULL AND split_part(parent_table, ''.'', 1) = %L',
|
|
27
|
+
partman, schema_name)
|
|
28
|
+
LOOP
|
|
29
|
+
EXECUTE format('DROP TABLE IF EXISTS %s', template);
|
|
30
|
+
END LOOP;
|
|
31
|
+
EXECUTE format(
|
|
32
|
+
'DELETE FROM %I.part_config_sub WHERE split_part(sub_parent, ''.'', 1) = %L',
|
|
33
|
+
partman, schema_name);
|
|
34
|
+
EXECUTE format(
|
|
35
|
+
'DELETE FROM %I.part_config WHERE split_part(parent_table, ''.'', 1) = %L',
|
|
36
|
+
partman, schema_name);
|
|
37
|
+
END LOOP;
|
|
38
|
+
END $$;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- pg_partman in schema `partman`; app_admin creates and maintains parents.
|
|
2
|
+
CREATE SCHEMA IF NOT EXISTS partman;
|
|
3
|
+
CREATE EXTENSION IF NOT EXISTS pg_partman SCHEMA partman;
|
|
4
|
+
|
|
5
|
+
GRANT USAGE, CREATE ON SCHEMA partman TO app_admin;
|
|
6
|
+
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA partman TO app_admin;
|
|
7
|
+
GRANT EXECUTE ON ALL PROCEDURES IN SCHEMA partman TO app_admin;
|
|
8
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA partman TO app_admin;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
package/sql/reset.sql
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-- Drops every schema app_admin owns and reloads the app's schema, in one
|
|
2
|
+
-- transaction. Roles, the `app` database and the app_service password
|
|
3
|
+
-- survive, so the Hyperdrive origin keeps working.
|
|
4
|
+
-- Run with psql from a staged schema directory (schema.sql beside sql/, as
|
|
5
|
+
-- `@astralarium/repo-postgres/provision` stageSchema writes it).
|
|
6
|
+
|
|
7
|
+
\connect app
|
|
8
|
+
|
|
9
|
+
BEGIN;
|
|
10
|
+
|
|
11
|
+
SET LOCAL ROLE app_admin;
|
|
12
|
+
|
|
13
|
+
-- Each schema module's beforeReset, in load order.
|
|
14
|
+
\i before-reset.sql
|
|
15
|
+
|
|
16
|
+
\i drop-schemas.sql
|
|
17
|
+
|
|
18
|
+
RESET ROLE;
|
|
19
|
+
|
|
20
|
+
\i schema.sql
|
|
21
|
+
|
|
22
|
+
COMMIT;
|
package/sql/roles.sql
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- The app roles, ahead of the schema; they outlive reset.sql.
|
|
2
|
+
-- Run by the image's init-db.sh and by bootstrap.sql.
|
|
3
|
+
|
|
4
|
+
-- app_admin owns the schemas and is assumed via SET ROLE (NOLOGIN).
|
|
5
|
+
CREATE ROLE app_admin NOLOGIN;
|
|
6
|
+
-- app_service is the app's login.
|
|
7
|
+
CREATE USER app_service;
|
|
8
|
+
GRANT app_service TO app_admin;
|
|
9
|
+
GRANT app_admin TO CURRENT_USER;
|
|
10
|
+
ALTER ROLE app_service SET idle_in_transaction_session_timeout = '30s';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- `slug.slug(name)`: the URL slug of a name: lowercase letters and digits,
|
|
2
|
+
-- spaces as `-`, other characters dropped. `O'Brien` → `obrien`;
|
|
3
|
+
-- `Room 52` → `room-52`. Names normalize to NFC first: decomposed and
|
|
4
|
+
-- composed spellings slug alike. Case and letter classes follow Unicode
|
|
5
|
+
-- (`pg_c_utf8`), whatever the database's or argument's collation.
|
|
6
|
+
SET ROLE app_admin;
|
|
7
|
+
|
|
8
|
+
CREATE SCHEMA slug AUTHORIZATION app_admin;
|
|
9
|
+
|
|
10
|
+
GRANT USAGE ON SCHEMA slug TO app_service;
|
|
11
|
+
|
|
12
|
+
CREATE FUNCTION slug.slug(name text) RETURNS text
|
|
13
|
+
LANGUAGE SQL IMMUTABLE PARALLEL SAFE
|
|
14
|
+
RETURN replace(
|
|
15
|
+
regexp_replace(
|
|
16
|
+
lower(normalize(name, NFC) COLLATE pg_c_utf8),
|
|
17
|
+
'[^[:alnum:] ]',
|
|
18
|
+
'',
|
|
19
|
+
'g'
|
|
20
|
+
),
|
|
21
|
+
' ',
|
|
22
|
+
'-'
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
SET ROLE NONE;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- taskclaim's pg_cron job: hourly `taskclaim.delete_expired()`. Skipped
|
|
2
|
+
-- where pg_cron is not loaded.
|
|
3
|
+
DO $$
|
|
4
|
+
BEGIN
|
|
5
|
+
IF current_setting('cron.database_name', true) IS NULL THEN
|
|
6
|
+
RAISE NOTICE 'pg_cron is not loaded; skipping taskclaim jobs';
|
|
7
|
+
RETURN;
|
|
8
|
+
END IF;
|
|
9
|
+
|
|
10
|
+
PERFORM cron.schedule(
|
|
11
|
+
'taskclaim-ttl',
|
|
12
|
+
'15 * * * *',
|
|
13
|
+
$job$
|
|
14
|
+
SET statement_timeout = '30s';
|
|
15
|
+
SELECT taskclaim.delete_expired();
|
|
16
|
+
$job$
|
|
17
|
+
);
|
|
18
|
+
END;
|
|
19
|
+
$$;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
-- Task claims in schema `taskclaim`: one row per task key, de-duplicating
|
|
2
|
+
-- queued tasks (`@astralarium/repo-postgres/queue`), and expired-claim
|
|
3
|
+
-- cleanup.
|
|
4
|
+
SET ROLE app_admin;
|
|
5
|
+
|
|
6
|
+
CREATE SCHEMA taskclaim AUTHORIZATION app_admin;
|
|
7
|
+
|
|
8
|
+
GRANT USAGE ON SCHEMA taskclaim TO app_service;
|
|
9
|
+
|
|
10
|
+
CREATE TABLE taskclaim.claim
|
|
11
|
+
(
|
|
12
|
+
key text PRIMARY KEY,
|
|
13
|
+
-- End of the claim; past it, a claim of the key replaces the row.
|
|
14
|
+
expires_at timestamptz NOT NULL,
|
|
15
|
+
-- Token of the claiming task, carried in its queue message.
|
|
16
|
+
token uuid NOT NULL
|
|
17
|
+
);
|
|
18
|
+
CREATE INDEX claim_expires_at_idx ON taskclaim.claim (expires_at);
|
|
19
|
+
|
|
20
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON taskclaim.claim TO app_service;
|
|
21
|
+
|
|
22
|
+
-- Deletes expired claims; their count. Reserved for the owner;
|
|
23
|
+
-- `taskclaimJobs` schedules it.
|
|
24
|
+
CREATE FUNCTION taskclaim.delete_expired()
|
|
25
|
+
RETURNS bigint
|
|
26
|
+
LANGUAGE plpgsql
|
|
27
|
+
SET search_path = pg_catalog
|
|
28
|
+
AS $$
|
|
29
|
+
DECLARE
|
|
30
|
+
deleted bigint;
|
|
31
|
+
BEGIN
|
|
32
|
+
DELETE FROM taskclaim.claim WHERE expires_at < CURRENT_TIMESTAMP;
|
|
33
|
+
GET DIAGNOSTICS deleted = ROW_COUNT;
|
|
34
|
+
RETURN deleted;
|
|
35
|
+
END;
|
|
36
|
+
$$;
|
|
37
|
+
|
|
38
|
+
COMMENT ON FUNCTION taskclaim.delete_expired() IS 'Deletes expired claims.';
|
|
39
|
+
REVOKE ALL ON FUNCTION taskclaim.delete_expired() FROM PUBLIC;
|
|
40
|
+
|
|
41
|
+
SET ROLE NONE;
|