multi_compress 0.3.5 → 0.6.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +111 -0
  3. data/GET_STARTED.md +207 -0
  4. data/README.md +69 -0
  5. data/db_core/include/mcdb_format.h +113 -0
  6. data/db_core/src/mcdb_format.c +594 -0
  7. data/db_deployment/mysql/Makefile +48 -0
  8. data/db_deployment/mysql/README.md +61 -0
  9. data/db_deployment/mysql/bin/common +212 -0
  10. data/db_deployment/mysql/bin/disable +40 -0
  11. data/db_deployment/mysql/bin/doctor +64 -0
  12. data/db_deployment/mysql/bin/enable +61 -0
  13. data/db_deployment/mysql/bin/install +64 -0
  14. data/db_deployment/mysql/bin/status +67 -0
  15. data/db_deployment/mysql/bin/uninstall +47 -0
  16. data/db_deployment/mysql/bin/upgrade +127 -0
  17. data/db_deployment/mysql/bin/verify +14 -0
  18. data/db_deployment/postgres/Makefile +45 -0
  19. data/db_deployment/postgres/README.md +84 -0
  20. data/db_deployment/postgres/bin/doctor +61 -0
  21. data/db_deployment/postgres/bin/enable +138 -0
  22. data/db_deployment/postgres/bin/install +106 -0
  23. data/db_deployment/postgres/bin/uninstall +54 -0
  24. data/db_deployment/postgres/bin/verify +14 -0
  25. data/docs/database-envelope-v1.md +159 -0
  26. data/docs/database-envelope-v2.md +147 -0
  27. data/docs/rfcs/0002-mcdb2-dictionaries.md +75 -0
  28. data/exe/multi_compress +228 -0
  29. data/ext/multi_compress/extconf.rb +4 -0
  30. data/ext/multi_compress/multi_compress.c +62 -0
  31. data/lib/multi_compress/active_record.rb +61 -0
  32. data/lib/multi_compress/codec.rb +164 -0
  33. data/lib/multi_compress/database.rb +312 -0
  34. data/lib/multi_compress/db_deployment.rb +868 -0
  35. data/lib/multi_compress/version.rb +1 -1
  36. data/multi_compress.gemspec +55 -0
  37. data/mysql_udf/Makefile +96 -0
  38. data/mysql_udf/README.md +140 -0
  39. data/mysql_udf/sql/examples.sql +16 -0
  40. data/mysql_udf/sql/install.sql +39 -0
  41. data/mysql_udf/sql/uninstall.sql +10 -0
  42. data/mysql_udf/sql/views.sql +20 -0
  43. data/mysql_udf/src/multi_compress_mysql.c +472 -0
  44. data/mysql_udf/src/mysql_udf_abi_57.h +34 -0
  45. data/mysql_udf/test/mcdb_cli.c +60 -0
  46. data/mysql_udf/test/run_e2e.sh +414 -0
  47. data/postgres_extension/Makefile +74 -0
  48. data/postgres_extension/README.md +160 -0
  49. data/postgres_extension/multi_compress.control +4 -0
  50. data/postgres_extension/multi_compress_pg.exports +24 -0
  51. data/postgres_extension/sql/examples.sql +23 -0
  52. data/postgres_extension/sql/multi_compress--0.5.0--0.6.0.sql +59 -0
  53. data/postgres_extension/sql/multi_compress--0.5.0.sql +24 -0
  54. data/postgres_extension/sql/multi_compress--0.6.0.sql +85 -0
  55. data/postgres_extension/sql/uninstall.sql +2 -0
  56. data/postgres_extension/sql/views.sql +7 -0
  57. data/postgres_extension/src/mcdb_format.c +2 -0
  58. data/postgres_extension/src/multi_compress_pg.c +381 -0
  59. data/postgres_extension/test/run_e2e.sh +411 -0
  60. metadata +56 -3
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ cd "$ROOT"
6
+
7
+ if command -v sha256sum >/dev/null 2>&1; then
8
+ sha256sum -c SHA256SUMS
9
+ elif command -v shasum >/dev/null 2>&1; then
10
+ shasum -a 256 -c SHA256SUMS
11
+ else
12
+ echo "multi_compress mysql verify: sha256sum or shasum is required" >&2
13
+ exit 2
14
+ fi
@@ -0,0 +1,45 @@
1
+ SHELL := /bin/bash
2
+
3
+ # PG_CONFIG is the normal public override. It is used for both compilation and
4
+ # installation paths. BUILD_PG_CONFIG/RUNTIME_PG_CONFIG exist for uncommon
5
+ # containerised deployments where headers are installed separately from the
6
+ # running server; ordinary DBAs never need to set them.
7
+ PG_CONFIG ?=
8
+ BUILD_PG_CONFIG ?= $(PG_CONFIG)
9
+ RUNTIME_PG_CONFIG ?= $(PG_CONFIG)
10
+ DB ?=
11
+ SCHEMA ?= multi_compress
12
+ MIGRATION_ROLE ?=
13
+ READ_ROLE ?=
14
+ CONFIRM ?=
15
+
16
+ .PHONY: help verify doctor install enable uninstall
17
+
18
+ help:
19
+ @printf '%s\n' \
20
+ 'MultiCompress PostgreSQL deployment bundle' \
21
+ '' \
22
+ ' make verify' \
23
+ ' make doctor [PG_CONFIG=/path/to/pg_config]' \
24
+ ' sudo make install [PG_CONFIG=/path/to/pg_config]' \
25
+ ' sudo -u postgres make enable DB=app_production MIGRATION_ROLE=app_migrations READ_ROLE=dbeaver_readonly [SCHEMA=multi_compress]' \
26
+ ' sudo make uninstall CONFIRM=REMOVE_MULTI_COMPRESS [PG_CONFIG=/path/to/pg_config]'
27
+
28
+ verify:
29
+ ./bin/verify
30
+
31
+ doctor:
32
+ ./bin/doctor $(if $(BUILD_PG_CONFIG),--pg-config "$(BUILD_PG_CONFIG)")
33
+
34
+ install: verify
35
+ ./bin/install $(if $(BUILD_PG_CONFIG),--build-pg-config "$(BUILD_PG_CONFIG)") $(if $(RUNTIME_PG_CONFIG),--runtime-pg-config "$(RUNTIME_PG_CONFIG)")
36
+
37
+ enable:
38
+ @test -n "$(DB)" || { echo 'DB is required: make enable DB=app_production' >&2; exit 2; }
39
+ @test -n "$(MIGRATION_ROLE)" || { echo 'MIGRATION_ROLE is required: make enable ... MIGRATION_ROLE=app_migrations' >&2; exit 2; }
40
+ @test -n "$(READ_ROLE)" || { echo 'READ_ROLE is required: make enable ... READ_ROLE=dbeaver_readonly' >&2; exit 2; }
41
+ ./bin/enable --database "$(DB)" --schema "$(SCHEMA)" --migration-role "$(MIGRATION_ROLE)" --read-role "$(READ_ROLE)"
42
+
43
+ uninstall:
44
+ @test "$(CONFIRM)" = "REMOVE_MULTI_COMPRESS" || { echo 'uninstall requires CONFIRM=REMOVE_MULTI_COMPRESS' >&2; exit 2; }
45
+ ./bin/uninstall $(if $(RUNTIME_PG_CONFIG),--pg-config "$(RUNTIME_PG_CONFIG)")
@@ -0,0 +1,84 @@
1
+ # MultiCompress PostgreSQL deployment bundle
2
+
3
+ This bundle is generated by the exact `multi_compress` gem version used by the
4
+ application. It builds the native reader on the PostgreSQL host, avoiding any
5
+ assumption about glibc, CPU architecture or PostgreSQL build flags.
6
+
7
+ Install PostgreSQL server development files for the running major version and a
8
+ C toolchain first. On a package-managed host this is normally the matching
9
+ `postgresql-server-dev-<major>` package; `make doctor` checks that prerequisite
10
+ before changing the server.
11
+
12
+ ```bash
13
+ make verify
14
+ make doctor
15
+ sudo make install
16
+ sudo -u postgres make enable DB=app_production \
17
+ MIGRATION_ROLE=app_migrations READ_ROLE=dbeaver_readonly
18
+ ```
19
+
20
+ Pass a non-default server installation explicitly:
21
+
22
+ ```bash
23
+ sudo make install PG_CONFIG=/usr/lib/postgresql/17/bin/pg_config
24
+ ```
25
+
26
+ `install` only places the extension files on the host. `enable` rejects
27
+ non-UTF8 databases, refuses an existing extension in another schema, and grants
28
+ `USAGE` to the migration role plus `EXECUTE` to the migration and read roles.
29
+ Grant the read role `SELECT` on each generated view in the application migration.
30
+ Run the native install on every database host that must query MCDB1 columns,
31
+ including reporting databases, failover targets and restored environments.
32
+
33
+ The application stores `MultiCompress::Database.compress(text)` in a `bytea`
34
+ column. Generate the DBeaver/read-side view in the application repository. Use
35
+ `--extension-schema NAME` below only if `make enable` used a non-default schema:
36
+
37
+ ```bash
38
+ bundle exec multi_compress db view postgres \
39
+ --table app.events \
40
+ --column payload_compressed \
41
+ --view admin.events_readable \
42
+ --columns id,created_at,status \
43
+ --as payload \
44
+ --output db/views/events_readable.sql
45
+ ```
46
+
47
+ Commit that SQL (or put it in a Rails migration). DBeaver users then query:
48
+
49
+ ```sql
50
+ SELECT id, created_at, status, payload
51
+ FROM admin.events_readable
52
+ WHERE id = 123;
53
+ ```
54
+
55
+ The view decompresses selected rows. Always filter with indexed, uncompressed
56
+ columns first; do not use it for unbounded `LIKE '%text%'` searches.
57
+
58
+ `make verify` catches accidental corruption after extraction; it does not
59
+ authenticate an archive. Use an out-of-band SHA-256, a minisign/GPG signature,
60
+ or signed release provenance for a real trust chain.
61
+
62
+ ## MCDB2 after reader installation
63
+
64
+ Reader installation is deliberately separate from application data. Once a 0.6
65
+ reader is installed and `make enable` has run, generate application-owned
66
+ append-only dictionary registry DDL and a dictionary-readable view from the gem:
67
+
68
+ ```bash
69
+ multi_compress db registry postgres \
70
+ --schema app --owner mcdb_dictionary_owner --migration-role app_migrations
71
+
72
+ multi_compress db view postgres \
73
+ --table app.events --column payload_compressed \
74
+ --dictionary-table app.mcdb_dictionary_versions \
75
+ --dictionary-id-column payload_dictionary_id \
76
+ --view admin.events_readable --columns id,created_at,status
77
+ ```
78
+
79
+ Do not place trained dictionaries in this DBA bundle. They belong in the registry
80
+ so backups, replicas and restored databases retain the exact bytes needed by each
81
+ MCDB2 row. The registry generator grants schema `USAGE` to its dedicated NOLOGIN
82
+ owner because PostgreSQL internal FK checks execute with relation-owner privileges.
83
+
84
+ For MCDB2 registry DDL, pass `--payload-table`, `--payload-column`, and `--payload-dictionary-id-column` to `multi_compress db registry`; this creates the payload FK and enforces header dictionary-reference consistency.
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ PG_CONFIG="${PG_CONFIG:-pg_config}"
6
+
7
+ usage() {
8
+ cat <<'TEXT'
9
+ Usage: ./bin/doctor [--pg-config PATH]
10
+
11
+ Checks whether this PostgreSQL host can build the MultiCompress extension.
12
+ TEXT
13
+ }
14
+
15
+ die() {
16
+ echo "multi_compress postgres doctor: $*" >&2
17
+ exit 2
18
+ }
19
+
20
+ while [ "$#" -gt 0 ]; do
21
+ case "$1" in
22
+ --pg-config)
23
+ [ "$#" -ge 2 ] || die "--pg-config requires a path"
24
+ PG_CONFIG="$2"
25
+ shift 2
26
+ ;;
27
+ --help|-h)
28
+ usage
29
+ exit 0
30
+ ;;
31
+ *)
32
+ die "unknown option: $1"
33
+ ;;
34
+ esac
35
+ done
36
+
37
+ if [[ "$PG_CONFIG" == */* ]]; then
38
+ [ -x "$PG_CONFIG" ] || die "pg_config is not executable: $PG_CONFIG"
39
+ else
40
+ PG_CONFIG="$(command -v "$PG_CONFIG" || true)"
41
+ [ -n "$PG_CONFIG" ] || die "pg_config was not found; install PostgreSQL server development files or pass --pg-config PATH"
42
+ fi
43
+
44
+ command -v make >/dev/null 2>&1 || die "make is required"
45
+ command -v cc >/dev/null 2>&1 || die "a C compiler is required (cc/gcc/clang)"
46
+ command -v ar >/dev/null 2>&1 || die "ar is required"
47
+
48
+ PGXS="$($PG_CONFIG --pgxs 2>/dev/null || true)"
49
+ HEADERS="$($PG_CONFIG --includedir-server 2>/dev/null || true)"
50
+ [ -f "$PGXS" ] || die "PGXS is missing; install postgresql-server-dev for $($PG_CONFIG --version)"
51
+ [ -f "$HEADERS/postgres.h" ] || die "PostgreSQL server headers are missing; install postgresql-server-dev for $($PG_CONFIG --version)"
52
+
53
+ printf '%s\n' \
54
+ "PostgreSQL: $($PG_CONFIG --version)" \
55
+ "pg_config: $PG_CONFIG" \
56
+ "PGXS: $PGXS" \
57
+ "headers: $HEADERS" \
58
+ "library directory: $($PG_CONFIG --pkglibdir)" \
59
+ "extension directory: $($PG_CONFIG --sharedir)/extension" \
60
+ "bundle: $ROOT" \
61
+ "doctor: OK"
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ DATABASE="${PGDATABASE:-}"
5
+ SCHEMA="multi_compress"
6
+ MIGRATION_ROLE=""
7
+ READ_ROLE=""
8
+ PSQL="${PSQL:-psql}"
9
+ HOST=""
10
+ PORT=""
11
+ USERNAME=""
12
+
13
+ usage() {
14
+ cat <<'TEXT'
15
+ Usage: ./bin/enable --database DATABASE --migration-role ROLE --read-role ROLE [OPTIONS]
16
+
17
+ Options:
18
+ --schema NAME extension schema (default: multi_compress)
19
+ --migration-role ROLE role that creates the generated view migration
20
+ --read-role ROLE role that reads the generated view
21
+ --host HOST PostgreSQL host passed to psql
22
+ --port PORT PostgreSQL port passed to psql
23
+ --username USER PostgreSQL user passed to psql
24
+ --psql PATH psql binary (default: psql)
25
+ TEXT
26
+ }
27
+
28
+ die() {
29
+ echo "multi_compress postgres enable: $*" >&2
30
+ exit 2
31
+ }
32
+
33
+ validate_identifier() {
34
+ local value="$1"
35
+ local option="$2"
36
+ printf '%s' "$value" | grep -Eq '^[A-Za-z_][A-Za-z0-9_]*$' || die "$option must be a simple SQL identifier"
37
+ }
38
+
39
+ while [ "$#" -gt 0 ]; do
40
+ case "$1" in
41
+ --database)
42
+ [ "$#" -ge 2 ] || die "--database requires a value"
43
+ DATABASE="$2"
44
+ shift 2
45
+ ;;
46
+ --schema)
47
+ [ "$#" -ge 2 ] || die "--schema requires a value"
48
+ SCHEMA="$2"
49
+ shift 2
50
+ ;;
51
+ --migration-role)
52
+ [ "$#" -ge 2 ] || die "--migration-role requires a value"
53
+ MIGRATION_ROLE="$2"
54
+ shift 2
55
+ ;;
56
+ --read-role)
57
+ [ "$#" -ge 2 ] || die "--read-role requires a value"
58
+ READ_ROLE="$2"
59
+ shift 2
60
+ ;;
61
+ --host)
62
+ [ "$#" -ge 2 ] || die "--host requires a value"
63
+ HOST="$2"
64
+ shift 2
65
+ ;;
66
+ --port)
67
+ [ "$#" -ge 2 ] || die "--port requires a value"
68
+ PORT="$2"
69
+ shift 2
70
+ ;;
71
+ --username)
72
+ [ "$#" -ge 2 ] || die "--username requires a value"
73
+ USERNAME="$2"
74
+ shift 2
75
+ ;;
76
+ --psql)
77
+ [ "$#" -ge 2 ] || die "--psql requires a path"
78
+ PSQL="$2"
79
+ shift 2
80
+ ;;
81
+ --help|-h)
82
+ usage
83
+ exit 0
84
+ ;;
85
+ *)
86
+ die "unknown option: $1"
87
+ ;;
88
+ esac
89
+ done
90
+
91
+ [ -n "$DATABASE" ] || die "--database is required"
92
+ [ -n "$MIGRATION_ROLE" ] || die "--migration-role is required"
93
+ [ -n "$READ_ROLE" ] || die "--read-role is required"
94
+ validate_identifier "$SCHEMA" '--schema'
95
+ validate_identifier "$MIGRATION_ROLE" '--migration-role'
96
+ validate_identifier "$READ_ROLE" '--read-role'
97
+
98
+ if [[ "$PSQL" == */* ]]; then
99
+ [ -x "$PSQL" ] || die "psql is not executable: $PSQL"
100
+ else
101
+ PSQL="$(command -v "$PSQL" || true)"
102
+ [ -n "$PSQL" ] || die "psql was not found"
103
+ fi
104
+
105
+ ARGS=(-X -v ON_ERROR_STOP=1 -d "$DATABASE")
106
+ [ -n "$HOST" ] && ARGS+=(-h "$HOST")
107
+ [ -n "$PORT" ] && ARGS+=(-p "$PORT")
108
+ [ -n "$USERNAME" ] && ARGS+=(-U "$USERNAME")
109
+
110
+ SERVER_ENCODING="$("$PSQL" "${ARGS[@]}" -Atq -c 'SHOW server_encoding;' | tr -d '\r\n')"
111
+ [ "$SERVER_ENCODING" = 'UTF8' ] || die "database $DATABASE uses $SERVER_ENCODING; multi_compress requires UTF8"
112
+
113
+ EXISTING_SCHEMA="$("$PSQL" "${ARGS[@]}" -Atq -c "SELECT n.nspname FROM pg_extension e JOIN pg_namespace n ON n.oid = e.extnamespace WHERE e.extname = 'multi_compress';" | tr -d '\r\n')"
114
+ if [ -n "$EXISTING_SCHEMA" ] && [ "$EXISTING_SCHEMA" != "$SCHEMA" ]; then
115
+ die "multi_compress is already installed in schema $EXISTING_SCHEMA; requested schema is $SCHEMA"
116
+ fi
117
+
118
+ if [ -z "$EXISTING_SCHEMA" ]; then
119
+ "$PSQL" "${ARGS[@]}" <<SQL
120
+ CREATE SCHEMA IF NOT EXISTS "$SCHEMA";
121
+ CREATE EXTENSION multi_compress WITH SCHEMA "$SCHEMA";
122
+ SQL
123
+ else
124
+ "$PSQL" "${ARGS[@]}" -c 'ALTER EXTENSION multi_compress UPDATE;'
125
+ fi
126
+
127
+ "$PSQL" "${ARGS[@]}" <<SQL
128
+ REVOKE ALL ON SCHEMA "$SCHEMA" FROM PUBLIC;
129
+ REVOKE EXECUTE ON ALL FUNCTIONS IN SCHEMA "$SCHEMA" FROM PUBLIC;
130
+ GRANT USAGE ON SCHEMA "$SCHEMA" TO "$MIGRATION_ROLE";
131
+ GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA "$SCHEMA" TO "$MIGRATION_ROLE", "$READ_ROLE";
132
+ SELECT "$SCHEMA".multi_compress_db_version() AS multi_compress_version;
133
+ SQL
134
+
135
+ printf '%s\n' \
136
+ "MultiCompress is enabled in $DATABASE.$SCHEMA." \
137
+ "Grant SELECT on each generated readable view to $READ_ROLE in the application migration." \
138
+ 'Apply the generated readable-view SQL in the application migration, then query that view from DBeaver.'
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ DEFAULT_PG_CONFIG="${PG_CONFIG:-pg_config}"
6
+ BUILD_PG_CONFIG="$DEFAULT_PG_CONFIG"
7
+ RUNTIME_PG_CONFIG="$DEFAULT_PG_CONFIG"
8
+
9
+ usage() {
10
+ cat <<'TEXT'
11
+ Usage: sudo ./bin/install [--pg-config PATH]
12
+
13
+ Builds the extension on this PostgreSQL host and installs its library, control
14
+ file and extension SQL into the directories reported by the selected pg_config.
15
+
16
+ Advanced: --build-pg-config and --runtime-pg-config let a container build with
17
+ separate development headers while installing into the running server's paths.
18
+ Ordinary installations should use only --pg-config, or no option at all.
19
+ TEXT
20
+ }
21
+
22
+ die() {
23
+ echo "multi_compress postgres install: $*" >&2
24
+ exit 2
25
+ }
26
+
27
+ resolve_pg_config() {
28
+ local candidate="$1"
29
+
30
+ if [[ "$candidate" == */* ]]; then
31
+ [ -x "$candidate" ] || die "pg_config is not executable: $candidate"
32
+ printf '%s\n' "$candidate"
33
+ else
34
+ candidate="$(command -v "$candidate" || true)"
35
+ [ -n "$candidate" ] || die "pg_config was not found"
36
+ printf '%s\n' "$candidate"
37
+ fi
38
+ }
39
+
40
+ pg_major() {
41
+ "$1" --version | awk '{print $2}' | cut -d. -f1
42
+ }
43
+
44
+ while [ "$#" -gt 0 ]; do
45
+ case "$1" in
46
+ --pg-config)
47
+ [ "$#" -ge 2 ] || die "--pg-config requires a path"
48
+ BUILD_PG_CONFIG="$2"
49
+ RUNTIME_PG_CONFIG="$2"
50
+ shift 2
51
+ ;;
52
+ --build-pg-config)
53
+ [ "$#" -ge 2 ] || die "--build-pg-config requires a path"
54
+ BUILD_PG_CONFIG="$2"
55
+ shift 2
56
+ ;;
57
+ --runtime-pg-config)
58
+ [ "$#" -ge 2 ] || die "--runtime-pg-config requires a path"
59
+ RUNTIME_PG_CONFIG="$2"
60
+ shift 2
61
+ ;;
62
+ --help|-h)
63
+ usage
64
+ exit 0
65
+ ;;
66
+ *)
67
+ die "unknown option: $1"
68
+ ;;
69
+ esac
70
+ done
71
+
72
+ [ "${EUID:-$(id -u)}" -eq 0 ] || die "run as root: sudo ./bin/install"
73
+ BUILD_PG_CONFIG="$(resolve_pg_config "$BUILD_PG_CONFIG")"
74
+ RUNTIME_PG_CONFIG="$(resolve_pg_config "$RUNTIME_PG_CONFIG")"
75
+
76
+ BUILD_MAJOR="$(pg_major "$BUILD_PG_CONFIG")"
77
+ RUNTIME_MAJOR="$(pg_major "$RUNTIME_PG_CONFIG")"
78
+ case "$BUILD_MAJOR" in
79
+ ""|*[!0-9]*) die "could not determine PostgreSQL build major version" ;;
80
+ esac
81
+ case "$RUNTIME_MAJOR" in
82
+ ""|*[!0-9]*) die "could not determine PostgreSQL runtime major version" ;;
83
+ esac
84
+ [ "$BUILD_MAJOR" = "$RUNTIME_MAJOR" ] || die "build PostgreSQL major $BUILD_MAJOR does not match runtime major $RUNTIME_MAJOR"
85
+
86
+ "$ROOT/bin/doctor" --pg-config "$BUILD_PG_CONFIG"
87
+
88
+ make -C "$ROOT/postgres_extension" clean all PG_CONFIG="$BUILD_PG_CONFIG"
89
+
90
+ PKGLIBDIR="$($RUNTIME_PG_CONFIG --pkglibdir)"
91
+ EXTENSION_DIR="$($RUNTIME_PG_CONFIG --sharedir)/extension"
92
+ install -d -m 0755 "$PKGLIBDIR" "$EXTENSION_DIR"
93
+ install -m 0755 "$ROOT/postgres_extension/multi_compress_pg.so" "$PKGLIBDIR/multi_compress_pg.so"
94
+ install -m 0644 "$ROOT/postgres_extension/multi_compress.control" "$EXTENSION_DIR/multi_compress.control"
95
+ for sql in "$ROOT"/postgres_extension/sql/multi_compress--*.sql; do
96
+ install -m 0644 "$sql" "$EXTENSION_DIR/$(basename "$sql")"
97
+ done
98
+
99
+ printf '%s\n' \
100
+ 'MultiCompress PostgreSQL files installed.' \
101
+ " library: $PKGLIBDIR/multi_compress_pg.so" \
102
+ " control: $EXTENSION_DIR/multi_compress.control" \
103
+ " SQL: $EXTENSION_DIR/multi_compress--0.6.0.sql (plus upgrade scripts)" \
104
+ '' \
105
+ 'Activate once per application database:' \
106
+ ' sudo -u postgres make enable DB=app_production'
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ PG_CONFIG="${PG_CONFIG:-pg_config}"
6
+
7
+ usage() {
8
+ cat <<'TEXT'
9
+ Usage: sudo ./bin/uninstall [--pg-config PATH]
10
+
11
+ Removes installed PostgreSQL extension files from this host. Drop the extension
12
+ from every database first; this command cannot discover all databases/views. The
13
+ Makefile requires CONFIRM=REMOVE_MULTI_COMPRESS before this script can run.
14
+ TEXT
15
+ }
16
+
17
+ die() {
18
+ echo "multi_compress postgres uninstall: $*" >&2
19
+ exit 2
20
+ }
21
+
22
+ while [ "$#" -gt 0 ]; do
23
+ case "$1" in
24
+ --pg-config)
25
+ [ "$#" -ge 2 ] || die "--pg-config requires a path"
26
+ PG_CONFIG="$2"
27
+ shift 2
28
+ ;;
29
+ --help|-h)
30
+ usage
31
+ exit 0
32
+ ;;
33
+ *)
34
+ die "unknown option: $1"
35
+ ;;
36
+ esac
37
+ done
38
+
39
+ [ "${EUID:-$(id -u)}" -eq 0 ] || die "run as root: sudo ./bin/uninstall"
40
+
41
+ if [[ "$PG_CONFIG" == */* ]]; then
42
+ [ -x "$PG_CONFIG" ] || die "pg_config is not executable: $PG_CONFIG"
43
+ else
44
+ PG_CONFIG="$(command -v "$PG_CONFIG" || true)"
45
+ [ -n "$PG_CONFIG" ] || die "pg_config was not found"
46
+ fi
47
+ PKGLIBDIR="$($PG_CONFIG --pkglibdir)"
48
+ EXTENSION_DIR="$($PG_CONFIG --sharedir)/extension"
49
+
50
+ rm -f "$PKGLIBDIR/multi_compress_pg.so" \
51
+ "$EXTENSION_DIR/multi_compress.control" \
52
+ "$EXTENSION_DIR/multi_compress--0.5.0.sql"
53
+
54
+ echo "MultiCompress PostgreSQL files removed from this host."
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ cd "$ROOT"
6
+
7
+ if command -v sha256sum >/dev/null 2>&1; then
8
+ sha256sum -c SHA256SUMS
9
+ elif command -v shasum >/dev/null 2>&1; then
10
+ shasum -a 256 -c SHA256SUMS
11
+ else
12
+ echo "multi_compress postgres verify: sha256sum or shasum is required" >&2
13
+ exit 2
14
+ fi
@@ -0,0 +1,159 @@
1
+ # MultiCompress Database Envelope — Format v1 (`MCDB1`)
2
+
3
+ Status: **frozen**. This is a long-term storage contract. Once data is written
4
+ to a database column, these bytes must remain readable forever, so v1 will not
5
+ change. New capabilities go into a future `v2` with a new version byte.
6
+
7
+ Two independent implementations MUST agree on this document byte-for-byte:
8
+
9
+ * the Ruby writer/reader — `MultiCompress::Database` (`lib/multi_compress/database.rb`)
10
+ * the MySQL 5.7 reader — `multi_compress_db_decompress()` UDF (`mysql_udf/`)
11
+ * the PostgreSQL reader — `multi_compress_db_decompress(bytea)` extension function
12
+ (`postgres_extension/`)
13
+
14
+ ## Scope of v1 (deliberately narrow)
15
+
16
+ | Aspect | v1 rule |
17
+ |---------------------|------------------------------------------------------|
18
+ | Algorithm | **zstd only** (no lz4, brotli) |
19
+ | Dictionaries | **not allowed** |
20
+ | Transport encoding | **raw bytes** (no base64) |
21
+ | Payload text | **UTF-8 without U+0000/NUL** (validated on write) |
22
+ | Column type (MySQL) | **`LONGBLOB`** recommended; `MEDIUMBLOB` only if the stored envelope stays < 16 MiB (see note) |
23
+ | Column type (PostgreSQL) | **`bytea`** |
24
+ | Max decompressed | **16 MiB** (16777216 bytes), enforced on read AND write |
25
+
26
+ Anything outside this table is a v2 concern and is out of scope for v1.
27
+
28
+ **Column sizing.** The 16 MiB cap is on the *decompressed* text. What the column
29
+ must hold is the *stored envelope* (`19 bytes + zstd frame`), which is normally
30
+ far smaller than the text. But `MEDIUMBLOB` tops out at `2^24 - 1 = 16,777,215`
31
+ bytes, one byte below the plaintext cap, and incompressible input can make the
32
+ envelope exceed the plaintext size. So v1 recommends **`LONGBLOB`**. Use
33
+ `MEDIUMBLOB` only if the application also checks the final `blob.bytesize` before
34
+ insert.
35
+
36
+ ## Byte layout
37
+
38
+ All multi-byte integers are **little-endian**. The envelope is a fixed 19-byte
39
+ header followed by a standard zstd frame:
40
+
41
+ ```
42
+ offset size field value / meaning
43
+ ------ ---- -------------- -----------------------------------------------
44
+ 0 4 magic ASCII "MCDB" = 0x4D 0x43 0x44 0x42
45
+ 4 1 format_version 0x01
46
+ 5 1 codec 0x01 = zstd (only value allowed in v1)
47
+ 6 1 flags 0x00 (reserved; MUST be 0 in v1)
48
+ 7 8 original_size uint64 LE: byte length of the original UTF-8 text
49
+ 15 4 crc32 uint32 LE: CRC-32 (zlib/IEEE) of the original text
50
+ 19 N zstd_frame a standard zstd frame produced from the text
51
+ ```
52
+
53
+ Total stored size = `19 + N` bytes.
54
+
55
+ `original_size` is the length of the **decompressed** UTF-8 bytes, stored
56
+ explicitly so a reader can pre-allocate exactly. `crc32` is the standard
57
+ zlib/IEEE CRC-32 of those same original bytes.
58
+
59
+ **Why an explicit CRC-32.** A raw zstd frame carries no content checksum by
60
+ default, so a single flipped byte in the payload can decode to a value of the
61
+ correct length with corrupted contents that zstd cannot detect. v1 therefore
62
+ stores a CRC-32 of the plaintext and every reader verifies it after
63
+ decompression. This matches Ruby's `MultiCompress.crc32`.
64
+
65
+ CRC-32 detects accidental corruption (bit flips, truncation, wrong bytes) with a
66
+ small residual collision probability; it is **not** a cryptographic integrity or
67
+ authenticity mechanism. For trusted database storage that is the intended use.
68
+ If envelopes could be altered by an untrusted party, a keyed cryptographic
69
+ digest would be required — that is a different, future format, not v1.
70
+
71
+ ### Empty text
72
+
73
+ An empty string is valid: `original_size = 0`, and `zstd_frame` is the zstd
74
+ frame of a zero-length input. The reader returns an empty string.
75
+
76
+ ## Writer algorithm (compress)
77
+
78
+ 1. Require input is a `String` whose bytes are valid UTF-8 and contain no NUL byte; else error.
79
+ 2. Let `n = bytesize`. If `n > 16777216`, error (too large for v1).
80
+ 3. `crc = crc32(bytes)` (zlib/IEEE).
81
+ 4. `frame = zstd_compress(bytes)`.
82
+ 5. Emit `"MCDB" + 0x01 + 0x01 + 0x00 + le_uint64(n) + le_uint32(crc) + frame`.
83
+
84
+ ## Reader algorithm (decompress / validate)
85
+
86
+ A reader MUST reject any input that fails any check below. Ruby raises
87
+ `MultiCompress::DataError`, PostgreSQL raises a SQL error, and the MySQL UDF
88
+ returns SQL `NULL` while `multi_compress_db_is_valid(blob)` returns `0`.
89
+
90
+ 1. `19 <= length <= 16842771` (a complete, bounded envelope).
91
+ 2. `magic == "MCDB"`.
92
+ 3. `format_version == 0x01`.
93
+ 4. `codec == 0x01`.
94
+ 5. `flags == 0x00`.
95
+ 6. `original_size <= 16777216`.
96
+ 7. The payload is **exactly one** standard zstd frame: no skippable frame,
97
+ concatenated frame, or trailing byte is allowed.
98
+ 8. The zstd frame declares a content size and that size equals `original_size`.
99
+ 9. `zstd_decompress(frame)` succeeds with a bounded output of at most
100
+ `original_size` bytes.
101
+ 10. The produced byte length equals `original_size` exactly.
102
+ 11. `crc32(produced) == crc32` from the header.
103
+ 12. The produced bytes are valid UTF-8 and contain no NUL byte.
104
+
105
+ On success the reader returns a UTF-8 string. MySQL callers can expose it with
106
+ `CONVERT(... USING utf8mb4)`. PostgreSQL callers use the `text`-returning
107
+ extension function and therefore require a UTF-8 database encoding.
108
+
109
+ `multi_compress_db_is_valid(blob)` performs checks 1–12 and returns 1/0 without
110
+ raising, so it can be used in `WHERE`/`CASE`.
111
+
112
+ ## Error taxonomy
113
+
114
+ | Condition | Ruby | PostgreSQL extension | MySQL UDF |
115
+ |-----------|------|----------------------|-----------|
116
+ | truncated / oversized envelope / bad magic / version | `MultiCompress::DataError` | SQL error | `NULL`, `is_valid = 0` |
117
+ | codec/flags not v1 | `MultiCompress::DataError` | SQL error | `NULL`, `is_valid = 0` |
118
+ | `original_size` over the cap | `MultiCompress::DataError` | SQL error | `NULL`, `is_valid = 0` |
119
+ | invalid, trailing, or concatenated zstd frame / size mismatch | `MultiCompress::DataError` | SQL error | `NULL`, `is_valid = 0` |
120
+ | crc32 mismatch (payload corruption) | `MultiCompress::DataError` | SQL error | `NULL`, `is_valid = 0` |
121
+ | non-UTF-8 or NUL text on write or read | `ArgumentError` / `MultiCompress::DataError` | SQL error | `NULL`, `is_valid = 0` |
122
+
123
+
124
+ **MySQL NULL diagnostics.** In a MySQL readable view, `payload IS NULL` cannot
125
+ distinguish an original SQL `NULL` from a corrupt MCDB1 envelope. Diagnose a
126
+ specific stored value with `multi_compress_db_is_valid(payload_compressed)`.
127
+
128
+ ## Versioning policy
129
+
130
+ * The 4-byte magic `MCDB` is a reserved namespace.
131
+ * `format_version` gates the whole layout. A reader that does not recognise the
132
+ version MUST error, not guess.
133
+ * v1 is immutable. Format changes ship as v2 (`format_version = 0x02`) with its
134
+ own spec document; readers dispatch on the version byte.
135
+
136
+ ## Migration note
137
+
138
+ v1 has **no legacy/plaintext auto-detection**. To migrate an existing text
139
+ column, add a new `LONGBLOB` column and write the envelope there; never
140
+ reinterpret old plaintext bytes as an envelope. See the rollout steps in
141
+ `mysql_udf/README.md` or `postgres_extension/README.md`.
142
+
143
+ ## Deployment and integrity notes
144
+
145
+ `make verify` detects accidental corruption after extraction. It does not
146
+ authenticate an archive because the archive's own Makefile is executed during
147
+ installation. Use an out-of-band SHA-256, a minisign/GPG signature, or signed
148
+ release provenance for a real trust chain.
149
+
150
+ For MySQL, `@@GLOBAL.max_allowed_packet`, the Ruby driver, and the DBeaver/JDBC
151
+ connection must all allow MCDB1 payloads up to the 16 MiB limit; the deployment
152
+ bundle's mysql client uses `--max_allowed_packet=32M` but cannot configure Rails
153
+ or DBeaver.
154
+
155
+ Install the native library on every host that will read through a generated
156
+ view—primary, replicas, reporting, failover, and restore targets—before
157
+ registering UDFs, creating the PostgreSQL extension, or applying a view
158
+ migration. A read-only view does not itself enter MySQL replication; replication
159
+ requires the UDF on a replica only when a replicated statement invokes it.