multi_compress 0.3.5 → 0.5.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -0
  3. data/GET_STARTED.md +188 -0
  4. data/README.md +56 -0
  5. data/db_core/include/mcdb_format.h +55 -0
  6. data/db_core/src/mcdb_format.c +341 -0
  7. data/db_deployment/mysql/Makefile +48 -0
  8. data/db_deployment/mysql/README.md +42 -0
  9. data/db_deployment/mysql/bin/common +160 -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 +57 -0
  13. data/db_deployment/mysql/bin/install +64 -0
  14. data/db_deployment/mysql/bin/status +65 -0
  15. data/db_deployment/mysql/bin/uninstall +47 -0
  16. data/db_deployment/mysql/bin/upgrade +115 -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 +60 -0
  20. data/db_deployment/postgres/bin/doctor +61 -0
  21. data/db_deployment/postgres/bin/enable +136 -0
  22. data/db_deployment/postgres/bin/install +104 -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/exe/multi_compress +228 -0
  27. data/ext/multi_compress/extconf.rb +4 -0
  28. data/ext/multi_compress/multi_compress.c +30 -0
  29. data/lib/multi_compress/active_record.rb +61 -0
  30. data/lib/multi_compress/codec.rb +164 -0
  31. data/lib/multi_compress/database.rb +106 -0
  32. data/lib/multi_compress/db_deployment.rb +456 -0
  33. data/lib/multi_compress/version.rb +1 -1
  34. data/multi_compress.gemspec +55 -0
  35. data/mysql_udf/Makefile +96 -0
  36. data/mysql_udf/README.md +95 -0
  37. data/mysql_udf/sql/examples.sql +11 -0
  38. data/mysql_udf/sql/install.sql +15 -0
  39. data/mysql_udf/sql/uninstall.sql +4 -0
  40. data/mysql_udf/sql/views.sql +8 -0
  41. data/mysql_udf/src/multi_compress_mysql.c +133 -0
  42. data/mysql_udf/src/mysql_udf_abi_57.h +34 -0
  43. data/mysql_udf/test/mcdb_cli.c +60 -0
  44. data/mysql_udf/test/run_e2e.sh +296 -0
  45. data/postgres_extension/Makefile +74 -0
  46. data/postgres_extension/README.md +112 -0
  47. data/postgres_extension/multi_compress.control +4 -0
  48. data/postgres_extension/multi_compress_pg.exports +12 -0
  49. data/postgres_extension/sql/examples.sql +12 -0
  50. data/postgres_extension/sql/multi_compress--0.5.0.sql +24 -0
  51. data/postgres_extension/sql/uninstall.sql +2 -0
  52. data/postgres_extension/sql/views.sql +7 -0
  53. data/postgres_extension/src/mcdb_format.c +2 -0
  54. data/postgres_extension/src/multi_compress_pg.c +96 -0
  55. data/postgres_extension/test/run_e2e.sh +323 -0
  56. metadata +52 -3
@@ -0,0 +1,136 @@
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
+ fi
124
+
125
+ "$PSQL" "${ARGS[@]}" <<SQL
126
+ REVOKE ALL ON SCHEMA "$SCHEMA" FROM PUBLIC;
127
+ REVOKE EXECUTE ON ALL FUNCTIONS IN SCHEMA "$SCHEMA" FROM PUBLIC;
128
+ GRANT USAGE ON SCHEMA "$SCHEMA" TO "$MIGRATION_ROLE";
129
+ GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA "$SCHEMA" TO "$MIGRATION_ROLE", "$READ_ROLE";
130
+ SELECT "$SCHEMA".multi_compress_db_version() AS multi_compress_version;
131
+ SQL
132
+
133
+ printf '%s\n' \
134
+ "MultiCompress is enabled in $DATABASE.$SCHEMA." \
135
+ "Grant SELECT on each generated readable view to $READ_ROLE in the application migration." \
136
+ 'Apply the generated readable-view SQL in the application migration, then query that view from DBeaver.'
@@ -0,0 +1,104 @@
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
+ install -m 0644 "$ROOT/postgres_extension/sql/multi_compress--0.5.0.sql" "$EXTENSION_DIR/multi_compress--0.5.0.sql"
96
+
97
+ printf '%s\n' \
98
+ 'MultiCompress PostgreSQL files installed.' \
99
+ " library: $PKGLIBDIR/multi_compress_pg.so" \
100
+ " control: $EXTENSION_DIR/multi_compress.control" \
101
+ " SQL: $EXTENSION_DIR/multi_compress--0.5.0.sql" \
102
+ '' \
103
+ 'Activate once per application database:' \
104
+ ' 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.