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,411 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ cd "$(dirname "$0")/../.."
4
+
5
+ # Platform-specific manifest for postgres:17.9-bookworm on linux/amd64.
6
+ DEFAULT_POSTGRES_IMAGE="postgres:17.9-bookworm@sha256:3a69b9de644363f30110fbaa78b9e3298ce71bb1aa9124c0b0ceb0a2602c3283"
7
+ IMAGE="${MCDB_POSTGRES_IMAGE:-$DEFAULT_POSTGRES_IMAGE}"
8
+ PLATFORM="${MCDB_POSTGRES_PLATFORM:-linux/amd64}"
9
+ CONTAINER="${MCDB_POSTGRES_CONTAINER:-mcdb-postgres-e2e}"
10
+ WAIT_SECONDS="${MCDB_POSTGRES_WAIT_SECONDS:-90}"
11
+ WORK_ROOT="${MCDB_POSTGRES_WORK_ROOT:-$(mktemp -d -t multi-compress-postgres.XXXXXX)}"
12
+ REMOVE_WORK_ROOT=0
13
+ ARCHIVE="$WORK_ROOT/multi_compress-postgres.tar.gz"
14
+ GEM_UNPACK_DIR="$WORK_ROOT/unpacked"
15
+ GEM_FILE="$WORK_ROOT/multi_compress.gem"
16
+ PACKAGED_GEM_ROOT=""
17
+ BUNDLE_PARENT="/opt"
18
+
19
+ command -v docker >/dev/null 2>&1 || { echo "docker is required" >&2; exit 2; }
20
+
21
+ cleanup() {
22
+ docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
23
+ [ "$REMOVE_WORK_ROOT" -eq 1 ] && rm -rf "$WORK_ROOT"
24
+ }
25
+ trap cleanup EXIT
26
+
27
+ if [ -z "${MCDB_POSTGRES_WORK_ROOT:-}" ]; then
28
+ REMOVE_WORK_ROOT=1
29
+ fi
30
+
31
+ ensure_ruby_extension() {
32
+ if ruby -Ilib -e 'require "multi_compress"; require "multi_compress/database"' >/dev/null 2>&1; then
33
+ return
34
+ fi
35
+
36
+ echo "building Ruby extension..."
37
+ if command -v bundle >/dev/null 2>&1; then
38
+ bundle exec rake compile
39
+ else
40
+ rake compile
41
+ fi
42
+ ruby -Ilib -e 'require "multi_compress"; require "multi_compress/database"' >/dev/null
43
+ }
44
+
45
+ require_fixture() {
46
+ local path="$1"
47
+ [ -f "$path" ] || { echo "missing committed fixture: $path" >&2; exit 2; }
48
+ }
49
+
50
+ fixture_hex() {
51
+ ruby -e 'print File.binread(ARGV.fetch(0)).unpack1("H*")' "$1"
52
+ }
53
+
54
+ prepare_packaged_gem() {
55
+ gem build multi_compress.gemspec --output "$GEM_FILE" >/dev/null
56
+ mkdir -p "$GEM_UNPACK_DIR"
57
+ gem unpack "$GEM_FILE" --target "$GEM_UNPACK_DIR" >/dev/null
58
+ shopt -s nullglob
59
+ roots=("$GEM_UNPACK_DIR"/*)
60
+ shopt -u nullglob
61
+ [ "${#roots[@]}" -eq 1 ] && [ -d "${roots[0]}" ] || { echo "could not unpack exactly one multi_compress gem root" >&2; exit 2; }
62
+ PACKAGED_GEM_ROOT="${roots[0]}"
63
+ [ -f "$PACKAGED_GEM_ROOT/multi_compress.gemspec" ] || { echo "packaged gem lacks gemspec" >&2; exit 2; }
64
+ [ -d "$PACKAGED_GEM_ROOT/lib" ] || { echo "packaged gem lacks lib" >&2; exit 2; }
65
+ [ -d "$PACKAGED_GEM_ROOT/exe" ] || { echo "packaged gem lacks exe" >&2; exit 2; }
66
+ [ -f "$PACKAGED_GEM_ROOT/lib/multi_compress/db_deployment.rb" ] || { echo "packaged gem lacks db deployment CLI" >&2; exit 2; }
67
+ }
68
+
69
+ db_cli() {
70
+ ruby -I "$PACKAGED_GEM_ROOT/lib" "$PACKAGED_GEM_ROOT/exe/multi_compress" db "$@"
71
+ }
72
+
73
+ compress_hex() {
74
+ MCDB_TEXT="$1" ruby -Ilib -r multi_compress -r multi_compress/database \
75
+ -e 'print MultiCompress::Database.compress(ENV.fetch("MCDB_TEXT")).unpack1("H*")'
76
+ }
77
+
78
+ mcdb2_fixture() {
79
+ MCDB_TEXT="$1" ruby -Ilib -r multi_compress -r multi_compress/database <<'RUBY'
80
+ samples = 256.times.map do |i|
81
+ %({"kind":"event","tenant":#{i % 8},"metadata":{"source":"worker","version":1,"name":"same-shape-#{i % 16}"},"payload":"#{"x" * (48 + i % 64)}"})
82
+ end
83
+
84
+ dictionary = MultiCompress::Database::Dictionary.train(samples, id: 42, size: 4096)
85
+ blob = MultiCompress::Database.compress(ENV.fetch("MCDB_TEXT"), dictionary: dictionary)
86
+
87
+ puts dictionary.id
88
+ puts dictionary.zstd_id
89
+ puts dictionary.sha256.unpack1("H*")
90
+ puts dictionary.bytes.unpack1("H*")
91
+ puts blob.unpack1("H*")
92
+ RUBY
93
+ }
94
+
95
+ bundle_name() {
96
+ ruby -I "$PACKAGED_GEM_ROOT/lib" -r multi_compress/version \
97
+ -e 'print "multi_compress-postgres-#{MultiCompress::VERSION}"'
98
+ }
99
+
100
+ build_deployment_bundle() {
101
+ db_cli package postgres --output "$ARCHIVE" --force
102
+ test -s "$ARCHIVE"
103
+ }
104
+
105
+ install_postgres_build_tools() {
106
+ local runtime_pg_config="$1"
107
+
108
+ docker exec -u 0 \
109
+ -e "MCDB_RUNTIME_PG_CONFIG=$runtime_pg_config" \
110
+ "$CONTAINER" sh -ceu '
111
+ runtime_pg_config="${MCDB_RUNTIME_PG_CONFIG:?missing runtime pg_config}"
112
+ pg_major="$("$runtime_pg_config" --version | awk '\''{print $2}'\'' | cut -d. -f1)"
113
+ case "$pg_major" in
114
+ ""|*[!0-9]*)
115
+ echo "could not determine PostgreSQL major version" >&2
116
+ exit 2
117
+ ;;
118
+ esac
119
+
120
+ if ! command -v make >/dev/null 2>&1 || ! command -v cc >/dev/null 2>&1; then
121
+ if ! command -v apt-get >/dev/null 2>&1; then
122
+ echo "PostgreSQL container has no supported package manager" >&2
123
+ exit 2
124
+ fi
125
+ apt-get update >&2
126
+ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
127
+ build-essential ca-certificates curl gnupg tar gzip >&2
128
+ fi
129
+
130
+ runtime_pgxs="$("$runtime_pg_config" --pgxs 2>/dev/null || true)"
131
+ runtime_headers="$("$runtime_pg_config" --includedir-server 2>/dev/null || true)"
132
+ if [ ! -f "$runtime_pgxs" ] || [ ! -f "$runtime_headers/postgres.h" ]; then
133
+ if ! command -v apt-get >/dev/null 2>&1; then
134
+ echo "PostgreSQL development headers are unavailable" >&2
135
+ exit 2
136
+ fi
137
+
138
+ install -d -m 0755 /etc/apt/keyrings
139
+ curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | \
140
+ gpg --dearmor --yes -o /etc/apt/keyrings/postgresql.gpg
141
+ . /etc/os-release
142
+ echo "deb [signed-by=/etc/apt/keyrings/postgresql.gpg] https://apt.postgresql.org/pub/repos/apt ${VERSION_CODENAME}-pgdg main" \
143
+ >/etc/apt/sources.list.d/pgdg.list
144
+ apt-get update >&2
145
+ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
146
+ "postgresql-server-dev-${pg_major}" >&2
147
+ fi
148
+
149
+ build_pg_config=""
150
+ for candidate in "/usr/lib/postgresql/${pg_major}/bin/pg_config" "$(command -v pg_config || true)"; do
151
+ [ -n "$candidate" ] && [ -x "$candidate" ] || continue
152
+ pgxs="$("$candidate" --pgxs 2>/dev/null || true)"
153
+ headers="$("$candidate" --includedir-server 2>/dev/null || true)"
154
+ if [ -f "$pgxs" ] && [ -f "$headers/postgres.h" ]; then
155
+ build_pg_config="$candidate"
156
+ break
157
+ fi
158
+ done
159
+
160
+ [ -n "$build_pg_config" ] || {
161
+ echo "PostgreSQL PGXS/server headers are unavailable" >&2
162
+ exit 2
163
+ }
164
+ printf "%s\n" "$build_pg_config"
165
+ '
166
+ }
167
+
168
+ install_bundle_on_db_host() {
169
+ local bundle_name="$1"
170
+ local build_pg_config="$2"
171
+ local runtime_pg_config="$3"
172
+ local bundle_root="${BUNDLE_PARENT}/${bundle_name}"
173
+
174
+ tar -tzf "$ARCHIVE" | grep -Fx "${bundle_name}/THIRD_PARTY_NOTICES.md" >/dev/null
175
+ if tar -tzf "$ARCHIVE" | grep -E '/\.DS_Store$' >/dev/null; then
176
+ echo 'deployment bundle must not contain .DS_Store' >&2
177
+ exit 2
178
+ fi
179
+ docker cp "$ARCHIVE" "$CONTAINER:/tmp/${bundle_name}.tar.gz"
180
+ docker exec -u 0 \
181
+ -e "MCDB_BUNDLE_ROOT=$bundle_root" \
182
+ -e "MCDB_BUILD_PG_CONFIG=$build_pg_config" \
183
+ -e "MCDB_RUNTIME_PG_CONFIG=$runtime_pg_config" \
184
+ "$CONTAINER" sh -ceu '
185
+ rm -rf "$MCDB_BUNDLE_ROOT"
186
+ mkdir -p "$(dirname "$MCDB_BUNDLE_ROOT")"
187
+ tar -xzf "/tmp/$(basename "$MCDB_BUNDLE_ROOT").tar.gz" -C "$(dirname "$MCDB_BUNDLE_ROOT")"
188
+ cd "$MCDB_BUNDLE_ROOT"
189
+ make verify
190
+ make doctor PG_CONFIG="$MCDB_BUILD_PG_CONFIG"
191
+ make install BUILD_PG_CONFIG="$MCDB_BUILD_PG_CONFIG" RUNTIME_PG_CONFIG="$MCDB_RUNTIME_PG_CONFIG"
192
+ '
193
+
194
+ }
195
+
196
+ apply_readable_view() {
197
+ local path="$1"
198
+ db_cli view postgres \
199
+ --table app.events \
200
+ --column payload_compressed \
201
+ --view admin.events_readable \
202
+ --columns id \
203
+ --output "$path"
204
+ docker exec -i "$CONTAINER" psql -X -U app_migrations -d postgres -v ON_ERROR_STOP=1 < "$path"
205
+ }
206
+
207
+ apply_dictionary_readable_view() {
208
+ local path="$1"
209
+ db_cli view postgres \
210
+ --table app.dictionary_events \
211
+ --column payload_compressed \
212
+ --dictionary-table app.mcdb_dictionary_versions \
213
+ --dictionary-id-column payload_dictionary_id \
214
+ --view admin.dictionary_events_readable \
215
+ --columns id \
216
+ --output "$path"
217
+ docker exec -i "$CONTAINER" psql -X -U app_migrations -d postgres -v ON_ERROR_STOP=1 < "$path"
218
+ }
219
+
220
+ ensure_ruby_extension
221
+ prepare_packaged_gem
222
+ build_deployment_bundle
223
+
224
+ for name in corrupt_magic corrupt_payload corrupt_crc invalid_utf8 nul_text trailing_skippable trailing_frame; do
225
+ require_fixture "test/fixtures/database_v1/${name}.mcdb"
226
+ done
227
+
228
+ TEXT='Привет, DBeaver! Сәлем! 🌍'
229
+ BIG=$(ruby -e 'print "a" * 300')
230
+ VALID_HEX=$(compress_hex "$TEXT")
231
+ BIG_HEX=$(compress_hex "$BIG")
232
+ MCDB2_FIXTURE_OUTPUT="$(mcdb2_fixture "$TEXT")"
233
+ readarray -t MCDB2_FIXTURE <<< "$MCDB2_FIXTURE_OUTPUT"
234
+ if [ "${#MCDB2_FIXTURE[@]}" -ne 5 ] || [ -z "${MCDB2_FIXTURE[0]}" ] || [ -z "${MCDB2_FIXTURE[4]}" ]; then
235
+ echo "MCDB2 fixture generator returned an invalid payload" >&2
236
+ exit 2
237
+ fi
238
+ DICT_REF="${MCDB2_FIXTURE[0]}"
239
+ DICT_ZSTD_ID="${MCDB2_FIXTURE[1]}"
240
+ DICT_SHA_HEX="${MCDB2_FIXTURE[2]}"
241
+ DICT_HEX="${MCDB2_FIXTURE[3]}"
242
+ DICT_BLOB_HEX="${MCDB2_FIXTURE[4]}"
243
+ TEXT_BYTES=$(MCDB_TEXT="$TEXT" ruby -e 'print ENV.fetch("MCDB_TEXT").bytesize')
244
+ BUNDLE_NAME="$(bundle_name)"
245
+ VIEW_SQL="$(mktemp -t multi-compress-postgres-view.XXXXXX.sql)"
246
+ DICT_VIEW_SQL="$(mktemp -t multi-compress-postgres-dictionary-view.XXXXXX.sql)"
247
+ REGISTRY_SQL="$(mktemp -t multi-compress-postgres-registry.XXXXXX.sql)"
248
+ trap 'rm -f "$VIEW_SQL" "$DICT_VIEW_SQL" "$REGISTRY_SQL"; cleanup' EXIT
249
+
250
+ docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
251
+ docker run --platform "$PLATFORM" -d --name "$CONTAINER" \
252
+ -e POSTGRES_HOST_AUTH_METHOD=trust "$IMAGE" >/dev/null
253
+
254
+ echo "waiting for PostgreSQL in $IMAGE..."
255
+ deadline=$(( $(date +%s) + WAIT_SECONDS ))
256
+ until docker exec "$CONTAINER" pg_isready -U postgres -d postgres >/dev/null 2>&1; do
257
+ if [ "$(date +%s)" -ge "$deadline" ]; then
258
+ echo "PostgreSQL did not become ready within ${WAIT_SECONDS}s" >&2
259
+ docker logs "$CONTAINER" >&2 || true
260
+ exit 1
261
+ fi
262
+ sleep 2
263
+ done
264
+
265
+ q() { docker exec "$CONTAINER" psql -X -U postgres -d postgres -Atq -v ON_ERROR_STOP=1 -c "$1"; }
266
+ qfail() {
267
+ if q "$1" >/dev/null 2>&1; then return 1; else return 0; fi
268
+ }
269
+ q_read() { docker exec "$CONTAINER" psql -X -U dbeaver_readonly -d postgres -Atq -v ON_ERROR_STOP=1 -c "$1"; }
270
+ q_read_fail() {
271
+ if q_read "$1" >/dev/null 2>&1; then return 1; else return 0; fi
272
+ }
273
+
274
+ RUNTIME_PG_CONFIG=$(docker exec "$CONTAINER" sh -ceu 'command -v pg_config' | tr -d '\r\n')
275
+ [ -n "$RUNTIME_PG_CONFIG" ] || { echo "PostgreSQL runtime does not provide pg_config" >&2; exit 2; }
276
+ BUILD_PG_CONFIG=$(install_postgres_build_tools "$RUNTIME_PG_CONFIG")
277
+ install_bundle_on_db_host "$BUNDLE_NAME" "$BUILD_PG_CONFIG" "$RUNTIME_PG_CONFIG"
278
+
279
+ q "CREATE ROLE app_migrations LOGIN;
280
+ CREATE ROLE dbeaver_readonly LOGIN;
281
+ CREATE ROLE mcdb_dictionary_owner NOLOGIN;
282
+ CREATE SCHEMA app;
283
+ CREATE SCHEMA admin;
284
+ CREATE TABLE app.events (id integer PRIMARY KEY, payload_compressed bytea NULL);
285
+ INSERT INTO app.events VALUES
286
+ (1, decode('$VALID_HEX', 'hex')),
287
+ (2, NULL),
288
+ (3, decode('$BIG_HEX', 'hex')),
289
+ (4, decode('$VALID_HEX', 'hex'));
290
+ GRANT USAGE ON SCHEMA app, admin TO app_migrations, dbeaver_readonly;
291
+ GRANT CREATE ON SCHEMA admin TO app_migrations;
292
+ GRANT SELECT ON app.events TO app_migrations;"
293
+ docker exec -u postgres -e "MCDB_BUNDLE_ROOT=${BUNDLE_PARENT}/${BUNDLE_NAME}" "$CONTAINER" sh -ceu '
294
+ cd "$MCDB_BUNDLE_ROOT"
295
+ make enable DB=postgres MIGRATION_ROLE=app_migrations READ_ROLE=dbeaver_readonly
296
+ '
297
+ apply_readable_view "$VIEW_SQL"
298
+ q 'GRANT SELECT ON admin.events_readable TO dbeaver_readonly;'
299
+
300
+ # MCDB2: package-provided registry DDL, immutable dictionary row, generated INNER JOIN view.
301
+ q "CREATE TABLE app.dictionary_events (
302
+ id integer PRIMARY KEY,
303
+ payload_compressed bytea NOT NULL,
304
+ payload_dictionary_id bigint NOT NULL
305
+ );
306
+ GRANT SELECT ON app.dictionary_events TO app_migrations;"
307
+ db_cli registry postgres \
308
+ --schema app --owner mcdb_dictionary_owner --migration-role app_migrations \
309
+ --payload-table dictionary_events --payload-column payload_compressed \
310
+ --payload-dictionary-id-column payload_dictionary_id \
311
+ --output "$REGISTRY_SQL"
312
+ docker exec -i "$CONTAINER" psql -X -U postgres -d postgres -v ON_ERROR_STOP=1 < "$REGISTRY_SQL"
313
+ q "
314
+ INSERT INTO app.mcdb_dictionary_versions (id, family, zstd_dict_id, sha256, bytes)
315
+ VALUES ($DICT_REF, 'events_payload_v1', $DICT_ZSTD_ID, decode('$DICT_SHA_HEX', 'hex'), decode('$DICT_HEX', 'hex'));
316
+ INSERT INTO app.dictionary_events (id, payload_compressed, payload_dictionary_id)
317
+ SELECT g, decode('$DICT_BLOB_HEX', 'hex'), $DICT_REF FROM generate_series(1, 1000) AS g;
318
+ ANALYZE app.dictionary_events;"
319
+ apply_dictionary_readable_view "$DICT_VIEW_SQL"
320
+ q 'GRANT SELECT ON admin.dictionary_events_readable TO dbeaver_readonly;'
321
+
322
+ q "CREATE DATABASE mcdb_latin1 WITH TEMPLATE template0 ENCODING 'LATIN1' LC_COLLATE 'C' LC_CTYPE 'C';"
323
+ docker exec -u postgres -e "MCDB_BUNDLE_ROOT=${BUNDLE_PARENT}/${BUNDLE_NAME}" "$CONTAINER" sh -ceu '
324
+ cd "$MCDB_BUNDLE_ROOT"
325
+ if make enable DB=mcdb_latin1 MIGRATION_ROLE=app_migrations READ_ROLE=dbeaver_readonly >/tmp/mcdb-latin1.out 2>&1; then
326
+ echo "LATIN1 enable unexpectedly succeeded" >&2
327
+ exit 1
328
+ fi
329
+ grep -F "requires UTF8" /tmp/mcdb-latin1.out
330
+ '
331
+ q "CREATE DATABASE mcdb_other_schema WITH TEMPLATE template0 ENCODING 'UTF8';"
332
+ docker exec "$CONTAINER" psql -X -U postgres -d mcdb_other_schema -v ON_ERROR_STOP=1 -c 'CREATE SCHEMA wrong_schema; CREATE EXTENSION multi_compress WITH SCHEMA wrong_schema;' >/dev/null
333
+ docker exec -u postgres -e "MCDB_BUNDLE_ROOT=${BUNDLE_PARENT}/${BUNDLE_NAME}" "$CONTAINER" sh -ceu '
334
+ cd "$MCDB_BUNDLE_ROOT"
335
+ if make enable DB=mcdb_other_schema SCHEMA=multi_compress MIGRATION_ROLE=app_migrations READ_ROLE=dbeaver_readonly >/tmp/mcdb-schema.out 2>&1; then
336
+ echo "mismatched extension schema unexpectedly succeeded" >&2
337
+ exit 1
338
+ fi
339
+ grep -F "already installed in schema wrong_schema" /tmp/mcdb-schema.out
340
+ '
341
+
342
+ fails=0
343
+ check() {
344
+ if [ "$2" = "$3" ]; then echo " PASS $1"; else echo " FAIL $1 (got [$2] want [$3])"; fails=$((fails + 1)); fi
345
+ }
346
+ check_err() {
347
+ if qfail "$2"; then echo " PASS $1 (rejected)"; else echo " FAIL $1 (should have errored)"; fails=$((fails + 1)); fi
348
+ }
349
+
350
+ check "UTF8 database" "$(q 'SHOW server_encoding;' | tr -d '\r\n')" "UTF8"
351
+ check "deployment bundle version() smoke" "$(q 'SELECT strpos(multi_compress.multi_compress_db_version(), chr(77) || chr(67) || chr(68) || chr(66) || chr(49)) > 0;' | tr -d '\r\n')" "t"
352
+ check "PostgreSQL reader uses vendored zstd" "$(q "SELECT strpos(multi_compress.multi_compress_db_version(), 'zstd 1.5.7') > 0;" | tr -d '\r\n')" "t"
353
+ check "decode roundtrip" "$(q 'SELECT multi_compress.multi_compress_db_decompress(payload_compressed) FROM app.events WHERE id=1;' | tr -d '\r')" "$TEXT"
354
+ check "NULL -> NULL" "$(q 'SELECT multi_compress.multi_compress_db_decompress(payload_compressed) IS NULL FROM app.events WHERE id=2;' | tr -d '\r\n')" "t"
355
+ check "big (>255, >input)" "$(q 'SELECT octet_length(multi_compress.multi_compress_db_decompress(payload_compressed)) FROM app.events WHERE id=3;' | tr -d '\r\n')" "300"
356
+ check "generated readable view" "$(q 'SELECT count(*) FROM admin.events_readable WHERE id IN (1, 4);' | tr -d '\r\n')" "2"
357
+ check "read role reads generated Unicode view" "$(q_read 'SELECT payload FROM admin.events_readable WHERE id=1;' | tr -d '\r')" "$TEXT"
358
+ if q_read_fail 'SELECT payload_compressed FROM app.events WHERE id=1;'; then
359
+ echo " PASS read role cannot read source table"
360
+ else
361
+ echo " FAIL read role unexpectedly read source table"
362
+ fails=$((fails + 1))
363
+ fi
364
+ RUNTIME_MAJOR="$(docker exec "$CONTAINER" "$RUNTIME_PG_CONFIG" --version | awk '{print $2}' | cut -d. -f1 | tr -d '\r\n')"
365
+ SERVER_MAJOR="$(q 'SHOW server_version;' | cut -d. -f1 | tr -d '\r\n')"
366
+ check "connected server major matches PG_CONFIG" "$SERVER_MAJOR" "$RUNTIME_MAJOR"
367
+ check "is_valid true" "$(q 'SELECT multi_compress.multi_compress_db_is_valid(payload_compressed) FROM app.events WHERE id=1;' | tr -d '\r\n')" "t"
368
+ check "is_valid on NULL" "$(q 'SELECT multi_compress.multi_compress_db_is_valid(payload_compressed) IS NULL FROM app.events WHERE id=2;' | tr -d '\r\n')" "t"
369
+
370
+ for name in corrupt_magic corrupt_payload corrupt_crc invalid_utf8 nul_text trailing_skippable trailing_frame; do
371
+ hex=$(fixture_hex "test/fixtures/database_v1/${name}.mcdb")
372
+ check_err "$name errors" "SELECT multi_compress.multi_compress_db_decompress(decode('$hex', 'hex'));"
373
+ done
374
+ check "is_valid false (crc)" "$(q "SELECT multi_compress.multi_compress_db_is_valid(decode('$(fixture_hex test/fixtures/database_v1/corrupt_crc.mcdb)', 'hex'));" | tr -d '\r\n')" "f"
375
+ check "MCDB2 registry owner has schema usage" "$(q "SELECT has_schema_privilege('mcdb_dictionary_owner', 'app', 'USAGE');" | tr -d '\r\n')" "t"
376
+ check "MCDB2 dictionary reference" "$(q 'SELECT multi_compress.multi_compress_db_dictionary_ref(payload_compressed) FROM app.dictionary_events WHERE id=1;' | tr -d '\r\n')" "$DICT_REF"
377
+ check "MCDB2 original size" "$(q 'SELECT multi_compress.multi_compress_db_original_size(payload_compressed) FROM app.dictionary_events WHERE id=1;' | tr -d '\r\n')" "$TEXT_BYTES"
378
+ check "MCDB2 dictionary view roundtrip" "$(q_read 'SELECT payload FROM admin.dictionary_events_readable WHERE id=1;' | tr -d '\r')" "$TEXT"
379
+ check "MCDB2 dictionary is valid" "$(q "SELECT multi_compress.multi_compress_db_is_valid_dict(e.payload_compressed, e.payload_dictionary_id, d.sha256, d.bytes) FROM app.dictionary_events e JOIN app.mcdb_dictionary_versions d ON d.id = e.payload_dictionary_id WHERE e.id=1;" | tr -d '\r\n')" "t"
380
+ check_err "MCDB2 payload/FK mismatch rejected" "INSERT INTO app.dictionary_events VALUES (1001, decode('$DICT_BLOB_HEX', 'hex'), $((DICT_REF + 1)));"
381
+ check "MCDB2 rejects wrong dictionary digest" "$(q "SELECT multi_compress.multi_compress_db_is_valid_dict(e.payload_compressed, e.payload_dictionary_id, decode(repeat('00', 32), 'hex'), d.bytes) FROM app.dictionary_events e JOIN app.mcdb_dictionary_versions d ON d.id = e.payload_dictionary_id WHERE e.id=1;" | tr -d '\r\n')" "f"
382
+ if q_read_fail 'SELECT bytes FROM app.mcdb_dictionary_versions WHERE id=42;'; then
383
+ echo " PASS read role cannot read dictionary registry"
384
+ else
385
+ echo " FAIL read role unexpectedly read dictionary registry"
386
+ fails=$((fails + 1))
387
+ fi
388
+ PG_DICT_PLAN="$(q 'SET enable_seqscan = off; EXPLAIN (COSTS OFF) SELECT id, payload FROM admin.dictionary_events_readable WHERE id >= 3 ORDER BY id LIMIT 100;')"
389
+ if printf '%s\n' "$PG_DICT_PLAN" | grep -Eq 'Index Scan|Index Only Scan'; then
390
+ echo " PASS MCDB2 view preserves indexed LIMIT plan"
391
+ else
392
+ echo " FAIL MCDB2 view did not expose an indexed LIMIT plan"
393
+ printf '%s\n' "$PG_DICT_PLAN" >&2
394
+ fails=$((fails + 1))
395
+ fi
396
+
397
+ q 'DROP VIEW admin.events_readable; DROP VIEW admin.dictionary_events_readable; DROP EXTENSION multi_compress;'
398
+ docker exec "$CONTAINER" psql -X -U postgres -d mcdb_other_schema -v ON_ERROR_STOP=1 -c 'DROP EXTENSION multi_compress;' >/dev/null
399
+ docker exec -u 0 -e "MCDB_BUNDLE_ROOT=${BUNDLE_PARENT}/${BUNDLE_NAME}" -e "MCDB_RUNTIME_PG_CONFIG=$RUNTIME_PG_CONFIG" "$CONTAINER" sh -ceu '
400
+ cd "$MCDB_BUNDLE_ROOT"
401
+ if make uninstall RUNTIME_PG_CONFIG="$MCDB_RUNTIME_PG_CONFIG" >/dev/null 2>&1; then
402
+ echo "uninstall succeeded without CONFIRM" >&2
403
+ exit 1
404
+ fi
405
+ make uninstall CONFIRM=REMOVE_MULTI_COMPRESS RUNTIME_PG_CONFIG="$MCDB_RUNTIME_PG_CONFIG"
406
+ '
407
+ check_err "uninstall removes functions" "SELECT multi_compress.multi_compress_db_version();"
408
+ check_err "uninstall removes extension files" "CREATE EXTENSION multi_compress;"
409
+
410
+ if [ "$fails" -ne 0 ]; then echo "E2E FAILED ($fails)"; exit 1; fi
411
+ echo "E2E OK"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: multi_compress
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.5
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Haydarov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-01 00:00:00.000000000 Z
11
+ date: 2026-07-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -72,7 +72,8 @@ description: 'Unified C-extension gem for zstd, lz4, and brotli compression. One
72
72
  sources — no system libraries required.'
73
73
  email:
74
74
  - romnhajdarov@gmail.com
75
- executables: []
75
+ executables:
76
+ - multi_compress
76
77
  extensions:
77
78
  - ext/multi_compress/extconf.rb
78
79
  extra_rdoc_files: []
@@ -82,6 +83,30 @@ files:
82
83
  - LICENSE.txt
83
84
  - README.md
84
85
  - THIRD_PARTY_NOTICES.md
86
+ - db_core/include/mcdb_format.h
87
+ - db_core/src/mcdb_format.c
88
+ - db_deployment/mysql/Makefile
89
+ - db_deployment/mysql/README.md
90
+ - db_deployment/mysql/bin/common
91
+ - db_deployment/mysql/bin/disable
92
+ - db_deployment/mysql/bin/doctor
93
+ - db_deployment/mysql/bin/enable
94
+ - db_deployment/mysql/bin/install
95
+ - db_deployment/mysql/bin/status
96
+ - db_deployment/mysql/bin/uninstall
97
+ - db_deployment/mysql/bin/upgrade
98
+ - db_deployment/mysql/bin/verify
99
+ - db_deployment/postgres/Makefile
100
+ - db_deployment/postgres/README.md
101
+ - db_deployment/postgres/bin/doctor
102
+ - db_deployment/postgres/bin/enable
103
+ - db_deployment/postgres/bin/install
104
+ - db_deployment/postgres/bin/uninstall
105
+ - db_deployment/postgres/bin/verify
106
+ - docs/database-envelope-v1.md
107
+ - docs/database-envelope-v2.md
108
+ - docs/rfcs/0002-mcdb2-dictionaries.md
109
+ - exe/multi_compress
85
110
  - ext/multi_compress/brotli_dec_static_init.c
86
111
  - ext/multi_compress/brotli_enc_static_init.c
87
112
  - ext/multi_compress/extconf.rb
@@ -320,7 +345,35 @@ files:
320
345
  - ext/multi_compress/vendor/zstd/lib/zstd.h
321
346
  - ext/multi_compress/vendor/zstd/lib/zstd_errors.h
322
347
  - lib/multi_compress.rb
348
+ - lib/multi_compress/active_record.rb
349
+ - lib/multi_compress/codec.rb
350
+ - lib/multi_compress/database.rb
351
+ - lib/multi_compress/db_deployment.rb
323
352
  - lib/multi_compress/version.rb
353
+ - multi_compress.gemspec
354
+ - mysql_udf/Makefile
355
+ - mysql_udf/README.md
356
+ - mysql_udf/sql/examples.sql
357
+ - mysql_udf/sql/install.sql
358
+ - mysql_udf/sql/uninstall.sql
359
+ - mysql_udf/sql/views.sql
360
+ - mysql_udf/src/multi_compress_mysql.c
361
+ - mysql_udf/src/mysql_udf_abi_57.h
362
+ - mysql_udf/test/mcdb_cli.c
363
+ - mysql_udf/test/run_e2e.sh
364
+ - postgres_extension/Makefile
365
+ - postgres_extension/README.md
366
+ - postgres_extension/multi_compress.control
367
+ - postgres_extension/multi_compress_pg.exports
368
+ - postgres_extension/sql/examples.sql
369
+ - postgres_extension/sql/multi_compress--0.5.0--0.6.0.sql
370
+ - postgres_extension/sql/multi_compress--0.5.0.sql
371
+ - postgres_extension/sql/multi_compress--0.6.0.sql
372
+ - postgres_extension/sql/uninstall.sql
373
+ - postgres_extension/sql/views.sql
374
+ - postgres_extension/src/mcdb_format.c
375
+ - postgres_extension/src/multi_compress_pg.c
376
+ - postgres_extension/test/run_e2e.sh
324
377
  homepage: https://github.com/roman-haidarov/multi_compress
325
378
  licenses:
326
379
  - MIT