@astralarium/repo-postgres 0.0.0-stage → 0.1.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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -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/schema/index.d.ts +3 -0
  32. package/dist/src/schema/index.js +3 -0
  33. package/dist/src/schema/module.d.ts +59 -0
  34. package/dist/src/schema/module.js +122 -0
  35. package/dist/src/schema/modules.d.ts +15 -0
  36. package/dist/src/schema/modules.js +48 -0
  37. package/dist/src/sql.d.ts +83 -0
  38. package/dist/src/sql.js +146 -0
  39. package/dist/testkit/index.d.ts +13 -0
  40. package/dist/testkit/index.js +31 -0
  41. package/dist/testkit/schema.d.ts +14 -0
  42. package/dist/testkit/schema.js +89 -0
  43. package/package.json +62 -3
  44. package/scripts/build-images.sh +52 -0
  45. package/scripts/dump-db.sh +32 -0
  46. package/sql/app/schema.sql +8 -0
  47. package/sql/bootstrap.sql +27 -0
  48. package/sql/database-grants.sql +3 -0
  49. package/sql/drop-schemas.sql +11 -0
  50. package/sql/extensions/cron/schema.sql +23 -0
  51. package/sql/extensions/partman/before-reset.sql +38 -0
  52. package/sql/extensions/partman/schema.sql +8 -0
  53. package/sql/extensions/trgm/schema.sql +1 -0
  54. package/sql/reset.sql +22 -0
  55. package/sql/roles.sql +10 -0
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ # Usage: build-images.sh <runtime> <schema-hash> [db|util|all] [db-iidfile]
3
+ # <schema-hash>: labels the db image and stamps the initialized database
4
+ # volume; required for db and all, empty for util.
5
+ # DB_NAME: the app's image base name (PostgresOptions.name); required.
6
+ # DB_SCHEMA_DIR: the staged schema directory (schema.sql beside sql/, as
7
+ # `@astralarium/repo-postgres/provision` stageSchema writes it), baked into the db image.
8
+ # DB_EXTENSIONS: extension names the db image installs, space-separated
9
+ # (Debian postgresql-<major>-<name> packages).
10
+ # DB_REFRESH_BASE=true: pull the base images.
11
+ set -euo pipefail
12
+
13
+ cd "$( dirname "${BASH_SOURCE[0]}" )/.."
14
+
15
+ DB_IMAGE=${DB_NAME:?set to the app image base name (PostgresOptions.name)}
16
+ UTIL_IMAGE=$DB_IMAGE-util
17
+
18
+ runtime=${1:?container runtime}
19
+ hash=${2:-}
20
+ target=${3:-all}
21
+ case $target in
22
+ db|util|all) ;;
23
+ *) echo "build-images: unknown target $target (db, util or all)" >&2; exit 1 ;;
24
+ esac
25
+ build_args=(--file container/init/Containerfile)
26
+ util_args=(--file container/util/Containerfile)
27
+ if [[ ${DB_REFRESH_BASE:-false} == true ]]; then
28
+ build_args+=(--pull)
29
+ util_args+=(--pull)
30
+ fi
31
+ if [[ -n ${4:-} ]]; then
32
+ build_args+=(--iidfile "$4")
33
+ fi
34
+
35
+ if [[ $target == db || $target == all ]]; then
36
+ [[ -n $hash ]] || { echo "build-images: $target needs a schema hash" >&2; exit 1; }
37
+ schema_dir=${DB_SCHEMA_DIR:?staged schema directory}
38
+ [[ -f $schema_dir/schema.sql ]] || { echo "build-images: $schema_dir holds no schema.sql" >&2; exit 1; }
39
+ "$runtime" build container/init \
40
+ "${build_args[@]}" \
41
+ --build-context "schema=$schema_dir" \
42
+ --build-arg DB_EXTENSIONS="${DB_EXTENSIONS:-}" \
43
+ --build-arg DB_SCHEMA="$hash" \
44
+ --tag "$DB_IMAGE:$hash" \
45
+ --tag "$DB_IMAGE:latest" >&2
46
+ fi
47
+
48
+ if [[ $target == util || $target == all ]]; then
49
+ "$runtime" build container/util \
50
+ "${util_args[@]}" \
51
+ --tag "$UTIL_IMAGE:latest" >&2
52
+ fi
@@ -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';