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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +111 -0
- data/GET_STARTED.md +207 -0
- data/README.md +69 -0
- data/db_core/include/mcdb_format.h +113 -0
- data/db_core/src/mcdb_format.c +594 -0
- data/db_deployment/mysql/Makefile +48 -0
- data/db_deployment/mysql/README.md +61 -0
- data/db_deployment/mysql/bin/common +212 -0
- data/db_deployment/mysql/bin/disable +40 -0
- data/db_deployment/mysql/bin/doctor +64 -0
- data/db_deployment/mysql/bin/enable +61 -0
- data/db_deployment/mysql/bin/install +64 -0
- data/db_deployment/mysql/bin/status +67 -0
- data/db_deployment/mysql/bin/uninstall +47 -0
- data/db_deployment/mysql/bin/upgrade +127 -0
- data/db_deployment/mysql/bin/verify +14 -0
- data/db_deployment/postgres/Makefile +45 -0
- data/db_deployment/postgres/README.md +84 -0
- data/db_deployment/postgres/bin/doctor +61 -0
- data/db_deployment/postgres/bin/enable +138 -0
- data/db_deployment/postgres/bin/install +106 -0
- data/db_deployment/postgres/bin/uninstall +54 -0
- data/db_deployment/postgres/bin/verify +14 -0
- data/docs/database-envelope-v1.md +159 -0
- data/docs/database-envelope-v2.md +147 -0
- data/docs/rfcs/0002-mcdb2-dictionaries.md +75 -0
- data/exe/multi_compress +228 -0
- data/ext/multi_compress/extconf.rb +4 -0
- data/ext/multi_compress/multi_compress.c +62 -0
- data/lib/multi_compress/active_record.rb +61 -0
- data/lib/multi_compress/codec.rb +164 -0
- data/lib/multi_compress/database.rb +312 -0
- data/lib/multi_compress/db_deployment.rb +868 -0
- data/lib/multi_compress/version.rb +1 -1
- data/multi_compress.gemspec +55 -0
- data/mysql_udf/Makefile +96 -0
- data/mysql_udf/README.md +140 -0
- data/mysql_udf/sql/examples.sql +16 -0
- data/mysql_udf/sql/install.sql +39 -0
- data/mysql_udf/sql/uninstall.sql +10 -0
- data/mysql_udf/sql/views.sql +20 -0
- data/mysql_udf/src/multi_compress_mysql.c +472 -0
- data/mysql_udf/src/mysql_udf_abi_57.h +34 -0
- data/mysql_udf/test/mcdb_cli.c +60 -0
- data/mysql_udf/test/run_e2e.sh +414 -0
- data/postgres_extension/Makefile +74 -0
- data/postgres_extension/README.md +160 -0
- data/postgres_extension/multi_compress.control +4 -0
- data/postgres_extension/multi_compress_pg.exports +24 -0
- data/postgres_extension/sql/examples.sql +23 -0
- data/postgres_extension/sql/multi_compress--0.5.0--0.6.0.sql +59 -0
- data/postgres_extension/sql/multi_compress--0.5.0.sql +24 -0
- data/postgres_extension/sql/multi_compress--0.6.0.sql +85 -0
- data/postgres_extension/sql/uninstall.sql +2 -0
- data/postgres_extension/sql/views.sql +7 -0
- data/postgres_extension/src/mcdb_format.c +2 -0
- data/postgres_extension/src/multi_compress_pg.c +381 -0
- data/postgres_extension/test/run_e2e.sh +411 -0
- metadata +56 -3
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
cd "$(dirname "$0")/../.."
|
|
4
|
+
|
|
5
|
+
DEFAULT_MYSQL_IMAGE="mysql:5.7.44@sha256:dab0a802b44617303694fb17d166501de279c3031ddeb28c56ecf7fcab5ef0da"
|
|
6
|
+
IMAGE="${MCDB_MYSQL_IMAGE:-$DEFAULT_MYSQL_IMAGE}"
|
|
7
|
+
PLATFORM="${MCDB_MYSQL_PLATFORM:-linux/amd64}"
|
|
8
|
+
CONTAINER="${MCDB_MYSQL_CONTAINER:-mcdb-e2e}"
|
|
9
|
+
WAIT_SECONDS="${MCDB_MYSQL_WAIT_SECONDS:-90}"
|
|
10
|
+
WORK_ROOT="${MCDB_MYSQL_WORK_ROOT:-$(mktemp -d -t multi-compress-mysql.XXXXXX)}"
|
|
11
|
+
REMOVE_WORK_ROOT=0
|
|
12
|
+
ARCHIVE="$WORK_ROOT/multi_compress-mysql.tar.gz"
|
|
13
|
+
GEM_UNPACK_DIR="$WORK_ROOT/unpacked"
|
|
14
|
+
GEM_FILE="$WORK_ROOT/multi_compress.gem"
|
|
15
|
+
PACKAGED_GEM_ROOT=""
|
|
16
|
+
BUNDLE_PARENT="/opt"
|
|
17
|
+
|
|
18
|
+
command -v docker >/dev/null 2>&1 || { echo "docker is required" >&2; exit 2; }
|
|
19
|
+
|
|
20
|
+
cleanup() {
|
|
21
|
+
docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
|
|
22
|
+
[ "$REMOVE_WORK_ROOT" -eq 1 ] && rm -rf "$WORK_ROOT"
|
|
23
|
+
}
|
|
24
|
+
trap cleanup EXIT
|
|
25
|
+
|
|
26
|
+
if [ -z "${MCDB_MYSQL_WORK_ROOT:-}" ]; then
|
|
27
|
+
REMOVE_WORK_ROOT=1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
ensure_ruby_extension() {
|
|
31
|
+
if ruby -Ilib -e 'require "multi_compress"; require "multi_compress/database"' >/dev/null 2>&1; then
|
|
32
|
+
return
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
echo "building Ruby extension..."
|
|
36
|
+
if command -v bundle >/dev/null 2>&1; then
|
|
37
|
+
bundle exec rake compile
|
|
38
|
+
else
|
|
39
|
+
rake compile
|
|
40
|
+
fi
|
|
41
|
+
ruby -Ilib -e 'require "multi_compress"; require "multi_compress/database"' >/dev/null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
require_fixture() {
|
|
45
|
+
local path="$1"
|
|
46
|
+
[ -f "$path" ] || { echo "missing committed fixture: $path" >&2; exit 2; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fixture_hex() {
|
|
50
|
+
ruby -e 'print File.binread(ARGV.fetch(0)).unpack1("H*")' "$1"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
prepare_packaged_gem() {
|
|
54
|
+
gem build multi_compress.gemspec --output "$GEM_FILE" >/dev/null
|
|
55
|
+
mkdir -p "$GEM_UNPACK_DIR"
|
|
56
|
+
gem unpack "$GEM_FILE" --target "$GEM_UNPACK_DIR" >/dev/null
|
|
57
|
+
shopt -s nullglob
|
|
58
|
+
roots=("$GEM_UNPACK_DIR"/*)
|
|
59
|
+
shopt -u nullglob
|
|
60
|
+
[ "${#roots[@]}" -eq 1 ] && [ -d "${roots[0]}" ] || { echo "could not unpack exactly one multi_compress gem root" >&2; exit 2; }
|
|
61
|
+
PACKAGED_GEM_ROOT="${roots[0]}"
|
|
62
|
+
[ -f "$PACKAGED_GEM_ROOT/multi_compress.gemspec" ] || { echo "packaged gem lacks gemspec" >&2; exit 2; }
|
|
63
|
+
[ -d "$PACKAGED_GEM_ROOT/lib" ] || { echo "packaged gem lacks lib" >&2; exit 2; }
|
|
64
|
+
[ -d "$PACKAGED_GEM_ROOT/exe" ] || { echo "packaged gem lacks exe" >&2; exit 2; }
|
|
65
|
+
[ -f "$PACKAGED_GEM_ROOT/lib/multi_compress/db_deployment.rb" ] || { echo "packaged gem lacks db deployment CLI" >&2; exit 2; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
db_cli() {
|
|
69
|
+
ruby -I "$PACKAGED_GEM_ROOT/lib" "$PACKAGED_GEM_ROOT/exe/multi_compress" db "$@"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
compress_hex() {
|
|
73
|
+
MCDB_TEXT="$1" ruby -Ilib -r multi_compress -r multi_compress/database \
|
|
74
|
+
-e 'print MultiCompress::Database.compress(ENV.fetch("MCDB_TEXT")).unpack1("H*")'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
mcdb2_fixture() {
|
|
78
|
+
MCDB_TEXT="$1" ruby -Ilib -r multi_compress -r multi_compress/database <<'RUBY'
|
|
79
|
+
samples = 256.times.map do |i|
|
|
80
|
+
%({"kind":"event","tenant":#{i % 8},"metadata":{"source":"worker","version":1,"name":"same-shape-#{i % 16}"},"payload":"#{"x" * (48 + i % 64)}"})
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
dictionary = MultiCompress::Database::Dictionary.train(samples, id: 42, size: 4096)
|
|
84
|
+
blob = MultiCompress::Database.compress(ENV.fetch("MCDB_TEXT"), dictionary: dictionary)
|
|
85
|
+
|
|
86
|
+
puts dictionary.id
|
|
87
|
+
puts dictionary.zstd_id
|
|
88
|
+
puts dictionary.sha256.unpack1("H*")
|
|
89
|
+
puts dictionary.bytes.unpack1("H*")
|
|
90
|
+
puts blob.unpack1("H*")
|
|
91
|
+
RUBY
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
bundle_name() {
|
|
95
|
+
ruby -I "$PACKAGED_GEM_ROOT/lib" -r multi_compress/version \
|
|
96
|
+
-e 'print "multi_compress-mysql-#{MultiCompress::VERSION}"'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
build_deployment_bundle() {
|
|
100
|
+
db_cli package mysql --output "$ARCHIVE" --force
|
|
101
|
+
test -s "$ARCHIVE"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
install_mysql_build_tools() {
|
|
105
|
+
docker exec -u 0 "$CONTAINER" sh -ceu '
|
|
106
|
+
if command -v yum >/dev/null 2>&1; then
|
|
107
|
+
yum install -y gcc make binutils tar gzip
|
|
108
|
+
elif command -v microdnf >/dev/null 2>&1; then
|
|
109
|
+
microdnf install -y gcc make binutils tar gzip
|
|
110
|
+
elif command -v apt-get >/dev/null 2>&1; then
|
|
111
|
+
apt-get update
|
|
112
|
+
DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential tar gzip
|
|
113
|
+
else
|
|
114
|
+
echo "no supported package manager in MySQL container" >&2
|
|
115
|
+
exit 2
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
command -v cc
|
|
119
|
+
command -v make
|
|
120
|
+
command -v ar
|
|
121
|
+
'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
install_bundle_on_db_host() {
|
|
125
|
+
local bundle_name="$1"
|
|
126
|
+
local bundle_root="${BUNDLE_PARENT}/${bundle_name}"
|
|
127
|
+
|
|
128
|
+
tar -tzf "$ARCHIVE" | grep -Fx "${bundle_name}/THIRD_PARTY_NOTICES.md" >/dev/null
|
|
129
|
+
if tar -tzf "$ARCHIVE" | grep -E '/\.DS_Store$' >/dev/null; then
|
|
130
|
+
echo 'deployment bundle must not contain .DS_Store' >&2
|
|
131
|
+
exit 2
|
|
132
|
+
fi
|
|
133
|
+
docker cp "$ARCHIVE" "$CONTAINER:/tmp/${bundle_name}.tar.gz"
|
|
134
|
+
docker exec -u 0 -e "MCDB_BUNDLE_ROOT=$bundle_root" "$CONTAINER" sh -ceu '
|
|
135
|
+
rm -rf "$MCDB_BUNDLE_ROOT"
|
|
136
|
+
mkdir -p "$(dirname "$MCDB_BUNDLE_ROOT")"
|
|
137
|
+
tar -xzf "/tmp/$(basename "$MCDB_BUNDLE_ROOT").tar.gz" -C "$(dirname "$MCDB_BUNDLE_ROOT")"
|
|
138
|
+
cd "$MCDB_BUNDLE_ROOT"
|
|
139
|
+
make verify
|
|
140
|
+
make doctor
|
|
141
|
+
make install
|
|
142
|
+
make enable
|
|
143
|
+
make status
|
|
144
|
+
'
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
apply_readable_view() {
|
|
148
|
+
local path="$1"
|
|
149
|
+
db_cli view mysql \
|
|
150
|
+
--table app.events \
|
|
151
|
+
--column payload_compressed \
|
|
152
|
+
--view admin.events_readable \
|
|
153
|
+
--columns id \
|
|
154
|
+
--output "$path"
|
|
155
|
+
docker exec -i "$CONTAINER" mysql --default-character-set=utf8mb4 < "$path"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
apply_dictionary_readable_view() {
|
|
159
|
+
local path="$1"
|
|
160
|
+
local warnings
|
|
161
|
+
db_cli view mysql \
|
|
162
|
+
--table app.dictionary_events \
|
|
163
|
+
--column payload_compressed \
|
|
164
|
+
--dictionary-table app.mcdb_dictionary_versions \
|
|
165
|
+
--dictionary-id-column payload_dictionary_id \
|
|
166
|
+
--view admin.dictionary_events_readable \
|
|
167
|
+
--columns id \
|
|
168
|
+
--output "$path"
|
|
169
|
+
warnings=$( { cat "$path"; printf '\nSHOW WARNINGS;\n'; } | docker exec -i "$CONTAINER" mysql \
|
|
170
|
+
--default-character-set=utf8mb4 --batch --skip-column-names --raw)
|
|
171
|
+
if printf '%s\n' "$warnings" | grep -Eqi '(undefined|merge)'; then
|
|
172
|
+
echo "MySQL refused the required MERGE view algorithm:" >&2
|
|
173
|
+
printf '%s\n' "$warnings" >&2
|
|
174
|
+
exit 1
|
|
175
|
+
fi
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
ensure_ruby_extension
|
|
179
|
+
prepare_packaged_gem
|
|
180
|
+
build_deployment_bundle
|
|
181
|
+
|
|
182
|
+
for name in corrupt_magic corrupt_payload corrupt_crc invalid_utf8 nul_text trailing_skippable trailing_frame; do
|
|
183
|
+
require_fixture "test/fixtures/database_v1/${name}.mcdb"
|
|
184
|
+
done
|
|
185
|
+
|
|
186
|
+
TEXT='Привет, DBeaver! Сәлем! 🌍'
|
|
187
|
+
BIG=$(ruby -e 'print "a" * 300')
|
|
188
|
+
VALID_HEX=$(compress_hex "$TEXT")
|
|
189
|
+
BIG_HEX=$(compress_hex "$BIG")
|
|
190
|
+
CORRUPT_HEX=$(fixture_hex test/fixtures/database_v1/corrupt_crc.mcdb)
|
|
191
|
+
MCDB2_FIXTURE_OUTPUT="$(mcdb2_fixture "$TEXT")"
|
|
192
|
+
readarray -t MCDB2_FIXTURE <<< "$MCDB2_FIXTURE_OUTPUT"
|
|
193
|
+
if [ "${#MCDB2_FIXTURE[@]}" -ne 5 ] || [ -z "${MCDB2_FIXTURE[0]}" ] || [ -z "${MCDB2_FIXTURE[4]}" ]; then
|
|
194
|
+
echo "MCDB2 fixture generator returned an invalid payload" >&2
|
|
195
|
+
exit 2
|
|
196
|
+
fi
|
|
197
|
+
DICT_REF="${MCDB2_FIXTURE[0]}"
|
|
198
|
+
DICT_ZSTD_ID="${MCDB2_FIXTURE[1]}"
|
|
199
|
+
DICT_SHA_HEX="${MCDB2_FIXTURE[2]}"
|
|
200
|
+
DICT_HEX="${MCDB2_FIXTURE[3]}"
|
|
201
|
+
DICT_BLOB_HEX="${MCDB2_FIXTURE[4]}"
|
|
202
|
+
TEXT_BYTES=$(MCDB_TEXT="$TEXT" ruby -e 'print ENV.fetch("MCDB_TEXT").bytesize')
|
|
203
|
+
BUNDLE_NAME="$(bundle_name)"
|
|
204
|
+
VIEW_SQL="$(mktemp -t multi-compress-mysql-view.XXXXXX.sql)"
|
|
205
|
+
DICT_VIEW_SQL="$(mktemp -t multi-compress-mysql-dictionary-view.XXXXXX.sql)"
|
|
206
|
+
REGISTRY_SQL="$(mktemp -t multi-compress-mysql-registry.XXXXXX.sql)"
|
|
207
|
+
trap 'rm -f "$VIEW_SQL" "$DICT_VIEW_SQL" "$REGISTRY_SQL"; cleanup' EXIT
|
|
208
|
+
|
|
209
|
+
docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
|
|
210
|
+
docker run --platform "$PLATFORM" -d --name "$CONTAINER" \
|
|
211
|
+
-e MYSQL_ALLOW_EMPTY_PASSWORD=1 "$IMAGE" \
|
|
212
|
+
--character-set-server=utf8mb4 \
|
|
213
|
+
--collation-server=utf8mb4_unicode_ci >/dev/null
|
|
214
|
+
|
|
215
|
+
echo "waiting for mysqld in $IMAGE..."
|
|
216
|
+
deadline=$(( $(date +%s) + WAIT_SECONDS ))
|
|
217
|
+
until docker exec "$CONTAINER" mysqladmin ping --silent 2>/dev/null; do
|
|
218
|
+
if [ "$(date +%s)" -ge "$deadline" ]; then
|
|
219
|
+
echo "mysqld did not become ready within ${WAIT_SECONDS}s" >&2
|
|
220
|
+
docker logs "$CONTAINER" >&2 || true
|
|
221
|
+
exit 1
|
|
222
|
+
fi
|
|
223
|
+
sleep 2
|
|
224
|
+
done
|
|
225
|
+
|
|
226
|
+
q() {
|
|
227
|
+
docker exec "$CONTAINER" mysql \
|
|
228
|
+
--default-character-set=utf8mb4 \
|
|
229
|
+
--batch \
|
|
230
|
+
--skip-column-names \
|
|
231
|
+
--raw \
|
|
232
|
+
-e "$1"
|
|
233
|
+
}
|
|
234
|
+
qfail() {
|
|
235
|
+
if q "$1" >/dev/null 2>&1; then return 1; else return 0; fi
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
install_mysql_build_tools
|
|
239
|
+
install_bundle_on_db_host "$BUNDLE_NAME"
|
|
240
|
+
|
|
241
|
+
q "DROP DATABASE IF EXISTS app;
|
|
242
|
+
DROP DATABASE IF EXISTS admin;
|
|
243
|
+
CREATE DATABASE app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
244
|
+
CREATE DATABASE admin CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
245
|
+
USE app;
|
|
246
|
+
CREATE TABLE events (id INT PRIMARY KEY, payload_compressed LONGBLOB NULL);
|
|
247
|
+
INSERT INTO events VALUES
|
|
248
|
+
(1, UNHEX('$VALID_HEX')),
|
|
249
|
+
(2, NULL),
|
|
250
|
+
(3, UNHEX('$BIG_HEX')),
|
|
251
|
+
(4, UNHEX('$VALID_HEX')),
|
|
252
|
+
(5, UNHEX('$CORRUPT_HEX')),
|
|
253
|
+
(6, UNHEX('$VALID_HEX'));"
|
|
254
|
+
apply_readable_view "$VIEW_SQL"
|
|
255
|
+
|
|
256
|
+
# MCDB2: registry DDL lives in app and the generated view supplies dictionary bytes through an INNER JOIN.
|
|
257
|
+
q "USE app;
|
|
258
|
+
CREATE TABLE dictionary_events (
|
|
259
|
+
id INT PRIMARY KEY,
|
|
260
|
+
payload_compressed LONGBLOB NOT NULL,
|
|
261
|
+
payload_dictionary_id BIGINT UNSIGNED NOT NULL
|
|
262
|
+
) ENGINE=InnoDB;"
|
|
263
|
+
db_cli registry mysql --database app \
|
|
264
|
+
--payload-table dictionary_events --payload-column payload_compressed \
|
|
265
|
+
--payload-dictionary-id-column payload_dictionary_id \
|
|
266
|
+
--output "$REGISTRY_SQL"
|
|
267
|
+
docker exec -i "$CONTAINER" mysql --default-character-set=utf8mb4 < "$REGISTRY_SQL"
|
|
268
|
+
q "USE app;
|
|
269
|
+
INSERT INTO mcdb_dictionary_versions (id, family, zstd_dict_id, sha256, bytes)
|
|
270
|
+
VALUES ($DICT_REF, 'events_payload_v1', $DICT_ZSTD_ID, UNHEX('$DICT_SHA_HEX'), UNHEX('$DICT_HEX'));
|
|
271
|
+
INSERT INTO dictionary_events (id, payload_compressed, payload_dictionary_id)
|
|
272
|
+
SELECT seq.id, UNHEX('$DICT_BLOB_HEX'), $DICT_REF
|
|
273
|
+
FROM (
|
|
274
|
+
SELECT ones.n + tens.n * 10 + hundreds.n * 100 + 1 AS id
|
|
275
|
+
FROM (SELECT 0 AS n UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4
|
|
276
|
+
UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) AS ones
|
|
277
|
+
CROSS JOIN (SELECT 0 AS n UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4
|
|
278
|
+
UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) AS tens
|
|
279
|
+
CROSS JOIN (SELECT 0 AS n UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4
|
|
280
|
+
UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) AS hundreds
|
|
281
|
+
) AS seq
|
|
282
|
+
WHERE seq.id <= 1000;"
|
|
283
|
+
apply_dictionary_readable_view "$DICT_VIEW_SQL"
|
|
284
|
+
|
|
285
|
+
fails=0
|
|
286
|
+
check() {
|
|
287
|
+
if [ "$2" = "$3" ]; then echo " PASS $1"; else echo " FAIL $1 (got [$2] want [$3])"; fails=$((fails + 1)); fi
|
|
288
|
+
}
|
|
289
|
+
check_err() {
|
|
290
|
+
if qfail "$2"; then echo " PASS $1 (rejected)"; else echo " FAIL $1 (should have errored)"; fails=$((fails + 1)); fi
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
check "deployment bundle version() smoke" "$(q 'SELECT LOCATE(CHAR(77,67,68,66,49), multi_compress_db_version()) > 0;')" "1"
|
|
294
|
+
check "MySQL connection uses utf8mb4" \
|
|
295
|
+
"$(q "SELECT CONCAT(@@character_set_client, '/', @@character_set_connection, '/', @@character_set_results);")" \
|
|
296
|
+
"utf8mb4/utf8mb4/utf8mb4"
|
|
297
|
+
check "decode roundtrip" \
|
|
298
|
+
"$(q 'USE app; SELECT CONVERT(multi_compress_db_decompress(payload_compressed) USING utf8mb4) FROM events WHERE id=1;')" \
|
|
299
|
+
"$TEXT"
|
|
300
|
+
check "NULL -> NULL" "$(q 'USE app; SELECT multi_compress_db_decompress(payload_compressed) IS NULL FROM events WHERE id=2;')" "1"
|
|
301
|
+
check "big (>255, >input)" "$(q 'USE app; SELECT OCTET_LENGTH(multi_compress_db_decompress(payload_compressed)) FROM events WHERE id=3;')" "300"
|
|
302
|
+
check "generated readable view" "$(q 'SELECT COUNT(*) FROM admin.events_readable WHERE id IN (1, 4);')" "2"
|
|
303
|
+
check "generated readable view returns Unicode" \
|
|
304
|
+
"$(q 'SELECT payload FROM admin.events_readable WHERE id=1;')" \
|
|
305
|
+
"$TEXT"
|
|
306
|
+
check "valid-corrupt-valid scan preserves later valid row" \
|
|
307
|
+
"$(q "USE app; SELECT GROUP_CONCAT(CASE WHEN multi_compress_db_decompress(payload_compressed) IS NULL THEN '<NULL>' ELSE CONVERT(multi_compress_db_decompress(payload_compressed) USING utf8mb4) END ORDER BY id SEPARATOR '|') FROM events WHERE id IN (1, 5, 6);")" \
|
|
308
|
+
"$TEXT|<NULL>|$TEXT"
|
|
309
|
+
check "is_valid true" "$(q 'USE app; SELECT multi_compress_db_is_valid(payload_compressed) FROM events WHERE id=1;')" "1"
|
|
310
|
+
check "is_valid on NULL" "$(q 'USE app; SELECT multi_compress_db_is_valid(payload_compressed) IS NULL FROM events WHERE id=2;')" "1"
|
|
311
|
+
|
|
312
|
+
for name in corrupt_magic corrupt_payload corrupt_crc invalid_utf8 nul_text trailing_skippable trailing_frame; do
|
|
313
|
+
hex=$(fixture_hex "test/fixtures/database_v1/${name}.mcdb")
|
|
314
|
+
check "$name -> NULL" \
|
|
315
|
+
"$(q "USE app; SELECT multi_compress_db_decompress(UNHEX('$hex')) IS NULL;")" \
|
|
316
|
+
"1"
|
|
317
|
+
done
|
|
318
|
+
check "is_valid false (crc)" \
|
|
319
|
+
"$(q "SELECT multi_compress_db_is_valid(UNHEX('$(fixture_hex test/fixtures/database_v1/corrupt_crc.mcdb)'));")" \
|
|
320
|
+
"0"
|
|
321
|
+
check "MCDB2 dictionary reference" "$(q 'USE app; SELECT multi_compress_db_dictionary_ref(payload_compressed) FROM dictionary_events WHERE id=1;')" "$DICT_REF"
|
|
322
|
+
check "MCDB2 original size" "$(q 'USE app; SELECT multi_compress_db_original_size(payload_compressed) FROM dictionary_events WHERE id=1;')" "$TEXT_BYTES"
|
|
323
|
+
check "MCDB2 generated readable view" "$(q 'SELECT payload FROM admin.dictionary_events_readable WHERE id=1;')" "$TEXT"
|
|
324
|
+
check "MCDB2 dictionary is valid" \
|
|
325
|
+
"$(q 'USE app; SELECT multi_compress_db_is_valid_dict(e.payload_compressed, e.payload_dictionary_id, d.sha256, d.bytes) FROM dictionary_events e JOIN mcdb_dictionary_versions d ON d.id = e.payload_dictionary_id WHERE e.id=1;')" \
|
|
326
|
+
"1"
|
|
327
|
+
check_err "MCDB2 payload/FK mismatch rejected" "USE app; INSERT INTO dictionary_events VALUES (1001, UNHEX('$DICT_BLOB_HEX'), $((DICT_REF + 1)));"
|
|
328
|
+
check "MCDB2 rejects wrong dictionary digest" \
|
|
329
|
+
"$(q 'USE app; SELECT multi_compress_db_is_valid_dict(e.payload_compressed, e.payload_dictionary_id, UNHEX(REPEAT("00", 32)), d.bytes) FROM dictionary_events e JOIN mcdb_dictionary_versions d ON d.id = e.payload_dictionary_id WHERE e.id=1;')" \
|
|
330
|
+
"0"
|
|
331
|
+
MYSQL_DICT_PLAN="$(q 'EXPLAIN SELECT id, payload FROM admin.dictionary_events_readable WHERE id BETWEEN 100 AND 900 ORDER BY id LIMIT 100;')"
|
|
332
|
+
if printf '%s\n' "$MYSQL_DICT_PLAN" | grep -Fq '<derived'; then
|
|
333
|
+
echo " FAIL MCDB2 view materialized as a derived table"
|
|
334
|
+
printf '%s\n' "$MYSQL_DICT_PLAN" >&2
|
|
335
|
+
fails=$((fails + 1))
|
|
336
|
+
elif ! printf '%s\n' "$MYSQL_DICT_PLAN" | awk -F '\t' '$3 == "source" && $5 == "range" && $7 == "PRIMARY" { found = 1 } END { exit(found ? 0 : 1) }'; then
|
|
337
|
+
echo " FAIL MCDB2 view did not use the source PRIMARY range scan"
|
|
338
|
+
printf '%s\n' "$MYSQL_DICT_PLAN" >&2
|
|
339
|
+
fails=$((fails + 1))
|
|
340
|
+
else
|
|
341
|
+
# `Using temporary` / `Using filesort` here belongs to the outer ORDER BY
|
|
342
|
+
# over a joined projection. It is not evidence that the view was materialized:
|
|
343
|
+
# MySQL reports materialized views as <derived...>. The important contract is
|
|
344
|
+
# that the source predicate is merged and reaches the PRIMARY range scan.
|
|
345
|
+
echo " PASS MCDB2 view is MERGE-able and keeps the source PRIMARY range scan"
|
|
346
|
+
fi
|
|
347
|
+
check "max_allowed_packet is reported" "$(q 'SELECT @@GLOBAL.max_allowed_packet > 0;')" "1"
|
|
348
|
+
|
|
349
|
+
MYSQL_SOCKET_PATH="$(q 'SELECT @@socket;' | tr -d '\r\n')"
|
|
350
|
+
# Simulate an installed 0.5.x server: only the three MCDB1 UDF registrations
|
|
351
|
+
# remain. 0.6 must recognise this as upgradeable, not partial, then use the
|
|
352
|
+
# documented DROP -> replace library -> CREATE lifecycle to register all nine.
|
|
353
|
+
docker exec -u 0 \
|
|
354
|
+
-e "MCDB_BUNDLE_ROOT=${BUNDLE_PARENT}/${BUNDLE_NAME}" \
|
|
355
|
+
-e "MCDB_MYSQL_SOCKET=$MYSQL_SOCKET_PATH" \
|
|
356
|
+
"$CONTAINER" sh -ceu '
|
|
357
|
+
mysql --protocol=SOCKET --socket="$MCDB_MYSQL_SOCKET" -e "
|
|
358
|
+
DROP FUNCTION multi_compress_db_decompress_dict;
|
|
359
|
+
DROP FUNCTION multi_compress_db_is_valid_dict;
|
|
360
|
+
DROP FUNCTION multi_compress_db_dictionary_sha256;
|
|
361
|
+
DROP FUNCTION multi_compress_db_dictionary_zstd_id;
|
|
362
|
+
DROP FUNCTION multi_compress_db_dictionary_ref;
|
|
363
|
+
DROP FUNCTION multi_compress_db_original_size;"
|
|
364
|
+
cd "$MCDB_BUNDLE_ROOT"
|
|
365
|
+
make status MYSQL_SOCKET="$MCDB_MYSQL_SOCKET" | grep -F "state: MCDB1-only"
|
|
366
|
+
make doctor MYSQL_SOCKET="$MCDB_MYSQL_SOCKET" | grep -F "server socket: $MCDB_MYSQL_SOCKET"
|
|
367
|
+
make upgrade CONFIRM=UPGRADE_MULTI_COMPRESS MYSQL_SOCKET="$MCDB_MYSQL_SOCKET"
|
|
368
|
+
make status MYSQL_SOCKET="$MCDB_MYSQL_SOCKET" | grep -F "state: enabled"
|
|
369
|
+
'
|
|
370
|
+
check "upgrade from MCDB1-only restores MCDB2 functions" "$(q 'SELECT multi_compress_db_original_size(payload_compressed) FROM app.dictionary_events WHERE id=1;')" "$TEXT_BYTES"
|
|
371
|
+
check "upgrade keeps version() available" "$(q 'SELECT LOCATE(CHAR(77,67,68,66,49), multi_compress_db_version()) > 0;')" "1"
|
|
372
|
+
|
|
373
|
+
# Register one expected name to a different SONAME and prove enable refuses without
|
|
374
|
+
# dropping it. The copied library makes this a real MySQL registration, not a mock.
|
|
375
|
+
docker exec -u 0 \
|
|
376
|
+
-e "MCDB_BUNDLE_ROOT=${BUNDLE_PARENT}/${BUNDLE_NAME}" \
|
|
377
|
+
-e "MCDB_MYSQL_SOCKET=$MYSQL_SOCKET_PATH" \
|
|
378
|
+
"$CONTAINER" sh -ceu '
|
|
379
|
+
plugin_dir="$(mysql --protocol=SOCKET --socket="$MCDB_MYSQL_SOCKET" --batch --skip-column-names -e "SELECT @@plugin_dir;")"
|
|
380
|
+
mysql --protocol=SOCKET --socket="$MCDB_MYSQL_SOCKET" -e "DROP FUNCTION multi_compress_db_version;"
|
|
381
|
+
cp "$plugin_dir/multi_compress_mysql.so" "$plugin_dir/foreign_multi_compress.so"
|
|
382
|
+
foreign_soname="$(printf "\\047foreign_multi_compress.so\\047")"
|
|
383
|
+
mysql --protocol=SOCKET --socket="$MCDB_MYSQL_SOCKET" -e "CREATE FUNCTION multi_compress_db_version RETURNS STRING SONAME $foreign_soname;"
|
|
384
|
+
cd "$MCDB_BUNDLE_ROOT"
|
|
385
|
+
if make enable MYSQL_SOCKET="$MCDB_MYSQL_SOCKET" >/tmp/mcdb-foreign-enable.out 2>&1; then
|
|
386
|
+
echo "enable unexpectedly accepted a foreign registration" >&2
|
|
387
|
+
exit 1
|
|
388
|
+
fi
|
|
389
|
+
grep -F "foreign SONAME" /tmp/mcdb-foreign-enable.out
|
|
390
|
+
mysql --protocol=SOCKET --socket="$MCDB_MYSQL_SOCKET" --batch --skip-column-names \
|
|
391
|
+
-e "SELECT dl FROM mysql.func WHERE name = $(printf "\\047multi_compress_db_version\\047");" | grep -Fx "foreign_multi_compress.so"
|
|
392
|
+
mysql --protocol=SOCKET --socket="$MCDB_MYSQL_SOCKET" -e "DROP FUNCTION multi_compress_db_version;"
|
|
393
|
+
owned_soname="$(printf "\\047multi_compress_mysql.so\\047")"
|
|
394
|
+
mysql --protocol=SOCKET --socket="$MCDB_MYSQL_SOCKET" -e "CREATE FUNCTION multi_compress_db_version RETURNS STRING SONAME $owned_soname;"
|
|
395
|
+
rm -f "$plugin_dir/foreign_multi_compress.so"
|
|
396
|
+
'
|
|
397
|
+
|
|
398
|
+
docker exec -u 0 -e "MCDB_BUNDLE_ROOT=${BUNDLE_PARENT}/${BUNDLE_NAME}" "$CONTAINER" sh -ceu '
|
|
399
|
+
cd "$MCDB_BUNDLE_ROOT"
|
|
400
|
+
if make disable >/dev/null 2>&1; then
|
|
401
|
+
echo "disable succeeded without CONFIRM" >&2
|
|
402
|
+
exit 1
|
|
403
|
+
fi
|
|
404
|
+
if make uninstall >/dev/null 2>&1; then
|
|
405
|
+
echo "uninstall succeeded without CONFIRM" >&2
|
|
406
|
+
exit 1
|
|
407
|
+
fi
|
|
408
|
+
make enable | grep -F "already enabled; no changes made."
|
|
409
|
+
make uninstall CONFIRM=REMOVE_MULTI_COMPRESS
|
|
410
|
+
'
|
|
411
|
+
check_err "uninstall removes UDF" "SELECT multi_compress_db_version();"
|
|
412
|
+
|
|
413
|
+
if [ "$fails" -ne 0 ]; then echo "E2E FAILED ($fails)"; exit 1; fi
|
|
414
|
+
echo "E2E OK"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
EXTENSION = multi_compress
|
|
2
|
+
MODULE_big = multi_compress_pg
|
|
3
|
+
OBJS = src/multi_compress_pg.o src/mcdb_format.o
|
|
4
|
+
DATA = sql/multi_compress--0.5.0.sql sql/multi_compress--0.6.0.sql sql/multi_compress--0.5.0--0.6.0.sql
|
|
5
|
+
|
|
6
|
+
PG_CONFIG ?= pg_config
|
|
7
|
+
ZSTD_DIR ?= ../ext/multi_compress/vendor/zstd/lib
|
|
8
|
+
CORE_DIR ?= ../db_core
|
|
9
|
+
BUILD ?= build
|
|
10
|
+
CC ?= cc
|
|
11
|
+
AR ?= ar
|
|
12
|
+
|
|
13
|
+
WARNFLAGS ?= -Wall -Wextra -Werror
|
|
14
|
+
PG_CPPFLAGS += -I$(CORE_DIR)/include -I$(ZSTD_DIR)
|
|
15
|
+
PG_CFLAGS += -O2 -fPIC -std=c99 $(WARNFLAGS)
|
|
16
|
+
EXPORT_MAP := $(CURDIR)/multi_compress_pg.exports
|
|
17
|
+
SHLIB_LINK += -Wl,--version-script=$(EXPORT_MAP) $(ZSTD_LIB)
|
|
18
|
+
EXTRA_CLEAN += $(BUILD)
|
|
19
|
+
|
|
20
|
+
ZSTD_CFLAGS ?= -O2 -fPIC -std=c99 -I$(ZSTD_DIR) -I$(ZSTD_DIR)/common
|
|
21
|
+
ZSTD_OBJ_DIR := $(BUILD)/zstd
|
|
22
|
+
ZSTD_C_SRCS := $(wildcard $(ZSTD_DIR)/common/*.c) $(wildcard $(ZSTD_DIR)/decompress/*.c)
|
|
23
|
+
ZSTD_C_OBJS := $(patsubst $(ZSTD_DIR)/%.c,$(ZSTD_OBJ_DIR)/%.o,$(ZSTD_C_SRCS))
|
|
24
|
+
|
|
25
|
+
ARCH := $(shell uname -m)
|
|
26
|
+
ifneq (,$(filter x86_64 amd64,$(ARCH)))
|
|
27
|
+
ZSTD_ASM_SRC := $(ZSTD_DIR)/decompress/huf_decompress_amd64.S
|
|
28
|
+
ifneq (,$(wildcard $(ZSTD_ASM_SRC)))
|
|
29
|
+
ZSTD_ASM_OBJ := $(ZSTD_OBJ_DIR)/decompress/huf_decompress_amd64.o
|
|
30
|
+
endif
|
|
31
|
+
endif
|
|
32
|
+
|
|
33
|
+
ZSTD_OBJS := $(ZSTD_C_OBJS) $(ZSTD_ASM_OBJ)
|
|
34
|
+
ZSTD_LIB := $(BUILD)/libzstd_dec.a
|
|
35
|
+
|
|
36
|
+
PGXS := $(shell $(PG_CONFIG) --pgxs)
|
|
37
|
+
include $(PGXS)
|
|
38
|
+
|
|
39
|
+
.PHONY: all test parity e2e fixtures
|
|
40
|
+
|
|
41
|
+
all: $(ZSTD_LIB)
|
|
42
|
+
|
|
43
|
+
$(MODULE_big)$(DLSUFFIX): $(ZSTD_LIB)
|
|
44
|
+
|
|
45
|
+
$(BUILD):
|
|
46
|
+
mkdir -p $@
|
|
47
|
+
|
|
48
|
+
$(ZSTD_OBJ_DIR)/%.o: $(ZSTD_DIR)/%.c
|
|
49
|
+
@mkdir -p $(dir $@)
|
|
50
|
+
$(CC) $(ZSTD_CFLAGS) -MMD -MP -c $< -o $@
|
|
51
|
+
|
|
52
|
+
$(ZSTD_OBJ_DIR)/%.o: $(ZSTD_DIR)/%.S
|
|
53
|
+
@mkdir -p $(dir $@)
|
|
54
|
+
$(CC) -fPIC -I$(ZSTD_DIR) -c $< -o $@
|
|
55
|
+
|
|
56
|
+
$(ZSTD_LIB): $(ZSTD_OBJS) | $(BUILD)
|
|
57
|
+
$(AR) rcs $@ $^
|
|
58
|
+
|
|
59
|
+
# The source wrapper includes the shared MCDB1/MCDB2 decoder; it is not a fork.
|
|
60
|
+
src/mcdb_format.o: src/mcdb_format.c $(CORE_DIR)/src/mcdb_format.c $(CORE_DIR)/include/mcdb_format.h
|
|
61
|
+
|
|
62
|
+
# Golden fixtures are committed. Regeneration is a development action only.
|
|
63
|
+
fixtures:
|
|
64
|
+
RUBYLIB=../lib ruby ../test/fixtures/database_v1/generate.rb
|
|
65
|
+
|
|
66
|
+
parity:
|
|
67
|
+
$(MAKE) -C ../mysql_udf test
|
|
68
|
+
|
|
69
|
+
test: all parity
|
|
70
|
+
|
|
71
|
+
e2e:
|
|
72
|
+
./test/run_e2e.sh
|
|
73
|
+
|
|
74
|
+
-include $(ZSTD_C_OBJS:.o=.d)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# MultiCompress PostgreSQL extension (`MCDB1` reader)
|
|
2
|
+
|
|
3
|
+
This PostgreSQL extension reads the same frozen `MCDB1` envelopes as the Ruby
|
|
4
|
+
API and the MySQL UDF. Rails stores `MultiCompress::Database.compress(text)` in
|
|
5
|
+
`bytea`; PostgreSQL exposes decoded UTF-8 through SQL and a view for DBeaver.
|
|
6
|
+
|
|
7
|
+
It is a separate server-native artifact, not part of `gem install`. The shared
|
|
8
|
+
format contract is [`docs/database-envelope-v1.md`](../docs/database-envelope-v1.md).
|
|
9
|
+
|
|
10
|
+
## SQL API
|
|
11
|
+
|
|
12
|
+
| Function | Returns | Purpose |
|
|
13
|
+
|---|---:|---|
|
|
14
|
+
| `multi_compress_db_version()` | `text` | reader build, MCDB1 and zstd version smoke test |
|
|
15
|
+
| `multi_compress_db_is_valid(bytea)` | `boolean` | validates an MCDB1 blob without raising |
|
|
16
|
+
| `multi_compress_db_decompress(bytea)` | `text` | returns UTF-8 text or raises on corruption |
|
|
17
|
+
|
|
18
|
+
`multi_compress_db_decompress` requires a UTF-8 PostgreSQL database. MCDB1 does
|
|
19
|
+
not permit NUL bytes, so decoded values are valid PostgreSQL `text` values.
|
|
20
|
+
|
|
21
|
+
## Production deployment: generated DBA bundle
|
|
22
|
+
|
|
23
|
+
Do **not** ask a DBA to clone this repository, find a release tag, manually copy
|
|
24
|
+
`multi_compress_pg.so`, or guess PostgreSQL install paths. The application
|
|
25
|
+
developer generates a source bundle from the exact installed gem version:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bundle exec multi_compress db package postgres --output tmp/multi-compress-postgres.tar.gz
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The DBA receives that one archive and runs this on the PostgreSQL host:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
tar -xzf multi-compress-postgres.tar.gz
|
|
35
|
+
cd multi_compress-postgres-0.5.0
|
|
36
|
+
make verify
|
|
37
|
+
make doctor
|
|
38
|
+
sudo make install
|
|
39
|
+
sudo -u postgres make enable DB=app_production \
|
|
40
|
+
MIGRATION_ROLE=app_migrations READ_ROLE=dbeaver_readonly
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`install` compiles the extension against the selected host `pg_config` and puts
|
|
44
|
+
its library/control/SQL files in the directories reported by that exact server.
|
|
45
|
+
Install the matching `postgresql-server-dev-<major>` package and a C toolchain
|
|
46
|
+
beforehand; `make doctor` verifies both.
|
|
47
|
+
For a non-default PostgreSQL install use:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
sudo make install PG_CONFIG=/usr/lib/postgresql/17/bin/pg_config
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`enable` rejects non-UTF8 databases and an extension already installed in a
|
|
54
|
+
different schema. It runs `CREATE EXTENSION` once per application database,
|
|
55
|
+
revokes public access, grants the migration role `USAGE` on the extension schema,
|
|
56
|
+
and grants both named roles `EXECUTE` on the decoder functions. The application
|
|
57
|
+
migration must grant the read role `SELECT` on each generated view. Repeat the
|
|
58
|
+
native install on every reporting, failover and restore target before enabling
|
|
59
|
+
or querying MCDB1 there.
|
|
60
|
+
|
|
61
|
+
## DBeaver: generated readable view
|
|
62
|
+
|
|
63
|
+
Generate the view in the application repository and put the output in a Rails
|
|
64
|
+
migration or versioned SQL. It is application schema, not a hand-typed DBA task:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
bundle exec multi_compress db view postgres \
|
|
68
|
+
--table app.events \
|
|
69
|
+
--column payload_compressed \
|
|
70
|
+
--view admin.events_readable \
|
|
71
|
+
--columns id,created_at,status \
|
|
72
|
+
--as payload \
|
|
73
|
+
--output db/views/events_readable.sql
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
DBeaver users only query `admin.events_readable`. Their role needs `USAGE` on
|
|
77
|
+
`admin`, `SELECT` on this view, and `EXECUTE` on the decoder functions granted by
|
|
78
|
+
`make enable`; it does not need `USAGE` on `multi_compress`. If `make enable`
|
|
79
|
+
used a different extension schema, pass it to the generator with
|
|
80
|
+
`--extension-schema that_schema`:
|
|
81
|
+
|
|
82
|
+
```sql
|
|
83
|
+
SELECT id, created_at, status, payload
|
|
84
|
+
FROM admin.events_readable
|
|
85
|
+
WHERE id = 123;
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The view decompresses selected rows. Filter with indexed ordinary columns before
|
|
89
|
+
reading it; do not scan decoded payloads with an unbounded `LIKE '%text%'`.
|
|
90
|
+
|
|
91
|
+
## Migration and constraints
|
|
92
|
+
|
|
93
|
+
Do not reinterpret an existing plaintext column as MCDB1. Add a new nullable
|
|
94
|
+
`bytea` column, dual-write it from the application, backfill in batches, verify
|
|
95
|
+
`multi_compress.multi_compress_db_is_valid(blob)`, switch reads, then keep the
|
|
96
|
+
old column for a rollback window. MCDB1 v1 is zstd-only, accepts valid UTF-8
|
|
97
|
+
text without NUL bytes, and has a 16 MiB decompressed limit. The target database
|
|
98
|
+
must use UTF-8 encoding for the text-returning function.
|
|
99
|
+
|
|
100
|
+
## Development source build
|
|
101
|
+
|
|
102
|
+
The source target remains useful for contributors and CI:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
cd postgres_extension
|
|
106
|
+
make PG_CONFIG=/usr/lib/postgresql/17/bin/pg_config
|
|
107
|
+
make test PG_CONFIG=/usr/lib/postgresql/17/bin/pg_config
|
|
108
|
+
make e2e
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`test/run_e2e.sh` builds against a pinned PostgreSQL Docker target and is the
|
|
112
|
+
server-side acceptance gate. It is not part of the end-user installation flow.
|
|
113
|
+
|
|
114
|
+
## MCDB2 dictionary-backed columns
|
|
115
|
+
|
|
116
|
+
MCDB2 is an opt-in companion for one homogeneous column containing many similar
|
|
117
|
+
small JSON/text values. It does not replace MCDB1. First install the 0.6 reader
|
|
118
|
+
on every host, then run `make enable` once again to execute `ALTER EXTENSION
|
|
119
|
+
multi_compress UPDATE` and refresh function grants.
|
|
120
|
+
|
|
121
|
+
The dictionary is application data, not an extension file. Create the append-only
|
|
122
|
+
registry under a dedicated NOLOGIN owner and retain every version referenced by a
|
|
123
|
+
payload:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
bundle exec multi_compress db registry postgres \
|
|
127
|
+
--schema app \
|
|
128
|
+
--owner mcdb_dictionary_owner \
|
|
129
|
+
--migration-role app_migrations \
|
|
130
|
+
--output db/mcdb_dictionary_registry.sql
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
For a new MCDB2 column, add a `payload_dictionary_id bigint NOT NULL` FK to
|
|
134
|
+
`app.mcdb_dictionary_versions`, register one strict
|
|
135
|
+
`MultiCompress::Database::Dictionary`, then generate the readable view:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
bundle exec multi_compress db view postgres \
|
|
139
|
+
--table app.events \
|
|
140
|
+
--column payload_compressed \
|
|
141
|
+
--dictionary-table app.mcdb_dictionary_versions \
|
|
142
|
+
--dictionary-id-column payload_dictionary_id \
|
|
143
|
+
--view admin.events_readable \
|
|
144
|
+
--columns id,created_at,status \
|
|
145
|
+
--as payload \
|
|
146
|
+
--output db/views/events_readable.sql
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The generated view uses an `INNER JOIN` and calls the four-argument native
|
|
150
|
+
function with payload, registry id, dictionary SHA-256 and bytes. It keeps raw
|
|
151
|
+
payloads/dictionary bytes out of the DBeaver role. The view owner needs source
|
|
152
|
+
and registry access; DBeaver needs `SELECT` on the view and the decoder
|
|
153
|
+
`EXECUTE` grant made by `make enable`.
|
|
154
|
+
|
|
155
|
+
Check production query shape with `EXPLAIN (ANALYZE, BUFFERS)` on an indexed
|
|
156
|
+
filter plus `LIMIT`. Do not search/order/group by decoded payload text. See
|
|
157
|
+
[`docs/database-envelope-v2.md`](../docs/database-envelope-v2.md) for the full
|
|
158
|
+
format and rollback contract.
|
|
159
|
+
|
|
160
|
+
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,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
global:
|
|
3
|
+
Pg_magic_func;
|
|
4
|
+
pg_finfo_multi_compress_db_version;
|
|
5
|
+
pg_finfo_multi_compress_db_is_valid;
|
|
6
|
+
pg_finfo_multi_compress_db_decompress;
|
|
7
|
+
pg_finfo_multi_compress_db_original_size;
|
|
8
|
+
pg_finfo_multi_compress_db_dictionary_ref;
|
|
9
|
+
pg_finfo_multi_compress_db_dictionary_zstd_id;
|
|
10
|
+
pg_finfo_multi_compress_db_dictionary_sha256;
|
|
11
|
+
pg_finfo_multi_compress_db_is_valid_dict;
|
|
12
|
+
pg_finfo_multi_compress_db_decompress_dict;
|
|
13
|
+
multi_compress_db_version;
|
|
14
|
+
multi_compress_db_is_valid;
|
|
15
|
+
multi_compress_db_decompress;
|
|
16
|
+
multi_compress_db_original_size;
|
|
17
|
+
multi_compress_db_dictionary_ref;
|
|
18
|
+
multi_compress_db_dictionary_zstd_id;
|
|
19
|
+
multi_compress_db_dictionary_sha256;
|
|
20
|
+
multi_compress_db_is_valid_dict;
|
|
21
|
+
multi_compress_db_decompress_dict;
|
|
22
|
+
local:
|
|
23
|
+
*;
|
|
24
|
+
};
|