@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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -2
  3. package/container/init/.containerignore +2 -0
  4. package/container/init/Containerfile +26 -0
  5. package/container/init/entrypoint.d/init-db.sh +20 -0
  6. package/container/snapshot/run-snapshot.sh +23 -0
  7. package/container/util/.containerignore +2 -0
  8. package/container/util/Containerfile +7 -0
  9. package/container/util/entrypoint.d/dump-db.sh +19 -0
  10. package/container/util/entrypoint.d/upgrade-db.sh +103 -0
  11. package/dist/bin/stage-schema.d.ts +2 -0
  12. package/dist/bin/stage-schema.js +20 -0
  13. package/dist/src/provision/connection.d.ts +11 -0
  14. package/dist/src/provision/connection.js +33 -0
  15. package/dist/src/provision/container.d.ts +87 -0
  16. package/dist/src/provision/container.js +244 -0
  17. package/dist/src/provision/data.d.ts +8 -0
  18. package/dist/src/provision/data.js +47 -0
  19. package/dist/src/provision/disposable.d.ts +49 -0
  20. package/dist/src/provision/disposable.js +188 -0
  21. package/dist/src/provision/image.d.ts +63 -0
  22. package/dist/src/provision/image.js +159 -0
  23. package/dist/src/provision/index.d.ts +17 -0
  24. package/dist/src/provision/index.js +205 -0
  25. package/dist/src/provision/options.d.ts +23 -0
  26. package/dist/src/provision/options.js +1 -0
  27. package/dist/src/provision/schema.d.ts +11 -0
  28. package/dist/src/provision/schema.js +134 -0
  29. package/dist/src/provision/test-database.d.ts +29 -0
  30. package/dist/src/provision/test-database.js +130 -0
  31. package/dist/src/queue.d.ts +86 -0
  32. package/dist/src/queue.js +201 -0
  33. package/dist/src/schema/index.d.ts +3 -0
  34. package/dist/src/schema/index.js +3 -0
  35. package/dist/src/schema/module.d.ts +59 -0
  36. package/dist/src/schema/module.js +122 -0
  37. package/dist/src/schema/modules.d.ts +24 -0
  38. package/dist/src/schema/modules.js +71 -0
  39. package/dist/src/sql.d.ts +83 -0
  40. package/dist/src/sql.js +146 -0
  41. package/dist/testkit/index.d.ts +14 -0
  42. package/dist/testkit/index.js +32 -0
  43. package/dist/testkit/locks.d.ts +27 -0
  44. package/dist/testkit/locks.js +57 -0
  45. package/dist/testkit/schema.d.ts +14 -0
  46. package/dist/testkit/schema.js +89 -0
  47. package/package.json +63 -3
  48. package/scripts/build-images.sh +52 -0
  49. package/scripts/dump-db.sh +32 -0
  50. package/sql/app/schema.sql +8 -0
  51. package/sql/bootstrap.sql +27 -0
  52. package/sql/database-grants.sql +3 -0
  53. package/sql/drop-schemas.sql +11 -0
  54. package/sql/extensions/cron/schema.sql +23 -0
  55. package/sql/extensions/partman/before-reset.sql +38 -0
  56. package/sql/extensions/partman/schema.sql +8 -0
  57. package/sql/extensions/trgm/schema.sql +1 -0
  58. package/sql/reset.sql +22 -0
  59. package/sql/roles.sql +10 -0
  60. package/sql/slug/schema.sql +25 -0
  61. package/sql/taskclaim/jobs/schema.sql +19 -0
  62. package/sql/taskclaim/schema.sql +41 -0
  63. 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,8 @@
1
+ -- The `app` schema: owned by app_admin, used by app_service.
2
+ SET ROLE app_admin;
3
+
4
+ CREATE SCHEMA app AUTHORIZATION app_admin;
5
+
6
+ GRANT USAGE ON SCHEMA app TO app_service;
7
+
8
+ SET ROLE NONE;
@@ -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,3 @@
1
+ -- Database grants of the app database psql is connected to.
2
+ GRANT CREATE, CONNECT ON DATABASE :"DBNAME" TO app_admin;
3
+ GRANT CONNECT ON DATABASE :"DBNAME" TO app_service;
@@ -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;
@@ -0,0 +1,3 @@
1
+ -- Snapshot scrub: claims name tasks queued in the source environment; their
2
+ -- messages stay behind there. taskclaim's `scrub`.
3
+ TRUNCATE taskclaim.claim;