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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ebc73c83a9de27fcde1080879572c025a30a3f2bb391313d5c21e56fd558cde3
|
|
4
|
+
data.tar.gz: 20a2db05bc0ecafbb399a01275391e5d1c08feafe8fc51f683859463b9c14ff8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aeac010c1419ad5b10919ba1d3e18b2053950555117dffc1075be2ce7b1e6ae9b2a9ff14d2684556a9aa0904ad030f688f4d954cb710692b263e8bb4ca238365
|
|
7
|
+
data.tar.gz: 9493318cf1321bbe525a8f65992675ef7b0760bf853192cf04146b8280f32011c925a5576876787849737d26eea7cc807c739ff0d5182c652b939c0150c3bf5b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,116 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.0]
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **MCDB2** — opt-in dictionary-backed SQL-readable envelopes for a homogeneous
|
|
7
|
+
`bytea`/`LONGBLOB` column. MCDB1 remains immutable and dictionary-free.
|
|
8
|
+
- **`MultiCompress::Database::Dictionary`** — strict zstd dictionary wrapper with
|
|
9
|
+
a signed-bigint-compatible application registry id, non-zero zstd DictID and
|
|
10
|
+
SHA-256 fingerprint. `MultiCompress::Database.compress(..., dictionary:)`
|
|
11
|
+
emits the 27-byte MCDB2 envelope.
|
|
12
|
+
- **Dictionary registry generator** — `multi_compress db registry postgres|mysql`
|
|
13
|
+
emits append-only registry/head DDL, metadata validation and freeze triggers.
|
|
14
|
+
- **Dictionary readable views** — `multi_compress db view ... --dictionary-table
|
|
15
|
+
... --dictionary-id-column ...` emits an INNER JOIN view that supplies the
|
|
16
|
+
registry dictionary to native decoder functions. MySQL output requests
|
|
17
|
+
`ALGORITHM=MERGE SQL SECURITY DEFINER`.
|
|
18
|
+
- **Native MCDB2 readers** for PostgreSQL and MySQL 5.7: dictionary reference,
|
|
19
|
+
dictionary SHA-256/DictID helpers, validity check and dictionary decode functions.
|
|
20
|
+
They use bounded DDict caches keyed by dictionary id + SHA-256 and verify bytes
|
|
21
|
+
before cache reuse.
|
|
22
|
+
- **`multi_compress_db_original_size(blob)`** on PostgreSQL and MySQL for optional
|
|
23
|
+
size-aware administrative filtering.
|
|
24
|
+
- PostgreSQL 17 and MySQL 5.7 E2E coverage for dictionary registry validation,
|
|
25
|
+
generated INNER JOIN views, wrong-dictionary rejection and plan shape checks.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- PostgreSQL deployment `make enable` upgrades an existing extension before
|
|
29
|
+
refreshing grants, so reader deployment precedes MCDB2 writes.
|
|
30
|
+
- MySQL UDF upgrade now preserves registrations for all MCDB1 and MCDB2 functions.
|
|
31
|
+
|
|
32
|
+
### Notes
|
|
33
|
+
- A dictionary is application data, not part of a DBA reader archive. It must stay
|
|
34
|
+
immutable and be backed up/replicated with every payload that references it.
|
|
35
|
+
- After the first MCDB2 write, retain a 0.6 reader on every reader host until all
|
|
36
|
+
MCDB2 rows have been retired; a 0.5 reader cannot decode version 2 envelopes.
|
|
37
|
+
|
|
38
|
+
## [0.5.0]
|
|
39
|
+
|
|
40
|
+
- MCDB1 now rejects trailing bytes, skippable frames, and concatenated zstd frames.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
- Pin the MySQL 5.7 Docker acceptance target to the official linux/amd64 image digest.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
- **Casual database deployment bundles** — `multi_compress db package postgres`
|
|
48
|
+
and `multi_compress db package mysql` build a version-locked tarball directly
|
|
49
|
+
from the installed gem. A DBA extracts it on the database host and uses
|
|
50
|
+
`make verify`, `make doctor`, `sudo make install`, and `make enable` instead
|
|
51
|
+
of cloning the repository or manually locating/copying a `.so`.
|
|
52
|
+
- **Readable-view generator** — `multi_compress db view postgres|mysql` emits
|
|
53
|
+
safely quoted, migration-ready views for DBeaver. MySQL output always converts
|
|
54
|
+
UDF output through `utf8mb4`.
|
|
55
|
+
- **Database envelope v1 (`MCDB1`)** — a frozen, self-describing storage format
|
|
56
|
+
(`docs/database-envelope-v1.md`): 19-byte header (magic/version/codec/flags/
|
|
57
|
+
original_size/crc32) + a zstd frame. zstd-only, UTF-8 text without NUL bytes, 16 MiB cap, no
|
|
58
|
+
dictionaries/base64. Corruption raises in Ruby and PostgreSQL; MySQL returns
|
|
59
|
+
`NULL` and exposes `multi_compress_db_is_valid(blob) = 0` for diagnostics.
|
|
60
|
+
- **`MultiCompress::Database`** — narrow Ruby writer/reader for `MCDB1`
|
|
61
|
+
(`compress`/`decompress`/`valid?`), independent of ActiveRecord. Validates
|
|
62
|
+
UTF-8 and size on write; requires exactly one zstd frame and verifies size +
|
|
63
|
+
CRC-32 on read.
|
|
64
|
+
- **MySQL 5.7 UDF** (`mysql_udf/`) — a separate native target (not built by the
|
|
65
|
+
gem) implementing the same `MCDB1` contract in C over the vendored zstd, with
|
|
66
|
+
`multi_compress_db_version()`, `multi_compress_db_is_valid(blob)`, and
|
|
67
|
+
`multi_compress_db_decompress(blob)`, plus SQL install/uninstall/views,
|
|
68
|
+
a Makefile, a real-MySQL Docker acceptance script, and a Ruby↔C parity
|
|
69
|
+
harness.
|
|
70
|
+
- **PostgreSQL extension** (`postgres_extension/`) — a PGXS target over the
|
|
71
|
+
same shared C decoder, exposing the matching `version`, `is_valid(bytea)`, and
|
|
72
|
+
`decompress(bytea)` SQL functions through `CREATE EXTENSION multi_compress`.
|
|
73
|
+
It includes SQL/view templates and a Docker e2e gate.
|
|
74
|
+
- Golden fixtures (`test/fixtures/database_v1/`) shared by the Ruby tests and the
|
|
75
|
+
C parity harness, proving both implementations agree byte-for-byte.
|
|
76
|
+
|
|
77
|
+
### Notes
|
|
78
|
+
- The 16 MiB cap and CRC-32 check are enforced identically in Ruby, the MySQL
|
|
79
|
+
UDF, and the PostgreSQL extension. The CRC-32 matches `MultiCompress.crc32`
|
|
80
|
+
(zlib/IEEE). MCDB1 payload text must be valid UTF-8 without NUL bytes so it
|
|
81
|
+
maps safely to PostgreSQL `text`.
|
|
82
|
+
- The ActiveRecord adapter remains experimental pending real Rails integration
|
|
83
|
+
tests; the `MCDB1` path does not depend on it.
|
|
84
|
+
|
|
85
|
+
## [0.4.0]
|
|
86
|
+
|
|
87
|
+
### Added
|
|
88
|
+
- **`multi_compress` command-line tool.** Gzip-style file (de)compression with
|
|
89
|
+
`-a/--algo`, `-l/--level`, `-d/--decompress`, `-o`, `-c/--stdout`, `-k/--keep`,
|
|
90
|
+
`-f/--force`, `--max-output`. Streams through `Reader`/`Writer`, writes output
|
|
91
|
+
atomically (temp + fsync + rename, directory fsync before removing the source),
|
|
92
|
+
refuses `input == output` and `-o` with multiple inputs.
|
|
93
|
+
- **`MultiCompress::Codec`** — strict, self-describing compression envelope for
|
|
94
|
+
values stored in a database. Always compresses (one storage format), 7-byte
|
|
95
|
+
magic/version/algo header, strict decode (a corrupt envelope raises
|
|
96
|
+
`DataError`, never silent passthrough), opt-in `legacy:` handling
|
|
97
|
+
(`:reject` default, `:plain`, `{ compressed: :algo }`), `encode: :base64` for
|
|
98
|
+
text columns (explicit `mc1:` prefix), locally-frozen `max_output_size`,
|
|
99
|
+
configurable `encoding:` (validated on write), and optional `serializer:`
|
|
100
|
+
(JSON/Marshal) for non-String values.
|
|
101
|
+
- **ActiveRecord integration** via `MultiCompress::ActiveRecordSupport::Type`
|
|
102
|
+
(for `attribute`) and `::Coder` (for `serialize`). Opt-in `mutable:` enables
|
|
103
|
+
in-place dirty tracking.
|
|
104
|
+
|
|
105
|
+
### Notes
|
|
106
|
+
- The CLI writes this gem's internal LZ4 block format with the **`.mclz4`**
|
|
107
|
+
extension (not `.lz4`), since it is not interchangeable with the standard
|
|
108
|
+
`lz4` CLI frame.
|
|
109
|
+
- `MultiCompress::Codec` treats one column as one fixed encoding. Use
|
|
110
|
+
`encoding: Encoding::BINARY` (a `:binary`/`bytea` column) for arbitrary bytes.
|
|
111
|
+
- The AR `Type` default does not detect in-place mutation; reassign the
|
|
112
|
+
attribute, or pass `mutable: true`.
|
|
113
|
+
|
|
3
114
|
## [0.3.5]
|
|
4
115
|
|
|
5
116
|
### Changed
|
data/GET_STARTED.md
CHANGED
|
@@ -862,4 +862,211 @@ puts results
|
|
|
862
862
|
# => {:lz4=>{:time=>0.0012, :ratio=>0.234, :size=>1024}, ...}
|
|
863
863
|
```
|
|
864
864
|
|
|
865
|
+
## Command-Line Tool
|
|
866
|
+
|
|
867
|
+
Installing the gem provides a `multi_compress` executable (gzip-style):
|
|
868
|
+
|
|
869
|
+
```bash
|
|
870
|
+
multi_compress file.json # -> file.json.zst (zstd default)
|
|
871
|
+
multi_compress -a brotli -l best a.css # -> a.css.br
|
|
872
|
+
multi_compress -d file.json.zst # -> file.json (algo from extension)
|
|
873
|
+
cat big.log | multi_compress -a zstd -c > big.log.zst # pipe mode
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
Flags: `-a/--algo` (`zstd|lz4|brotli`), `-l/--level` (number or `fastest|default|best`),
|
|
877
|
+
`-d/--decompress`, `-o/--output`, `-c/--stdout`, `-k/--keep`, `-f/--force`,
|
|
878
|
+
`-q/--quiet`, `--max-output SIZE` (e.g. `64MB`), `--version`.
|
|
879
|
+
|
|
880
|
+
Safety: output is written to a temp file in the destination directory, fsynced,
|
|
881
|
+
atomically renamed, and the directory is fsynced before the source is removed —
|
|
882
|
+
an interrupted run never leaves a partial file or touches the source. The tool
|
|
883
|
+
refuses `input == output` and refuses `-o` with multiple inputs.
|
|
884
|
+
|
|
885
|
+
**LZ4 note:** this gem's LZ4 uses an internal block format that is *not*
|
|
886
|
+
interchangeable with the standard `lz4` CLI. To avoid a false compatibility
|
|
887
|
+
promise, LZ4 output uses the **`.mclz4`** extension; `.lz4` is never produced.
|
|
888
|
+
|
|
889
|
+
## Database Column Compression
|
|
890
|
+
|
|
891
|
+
There are two intentionally separate database paths:
|
|
892
|
+
|
|
893
|
+
1. **`MultiCompress::Codec`** is a general application-side envelope for one
|
|
894
|
+
column. It is useful when only Ruby needs to read the data.
|
|
895
|
+
2. **`MultiCompress::Database` (`MCDB1`)** is the narrow, frozen format for
|
|
896
|
+
MySQL 5.7 or PostgreSQL + DBeaver. It has matching server-side C readers,
|
|
897
|
+
so SQL can decode the BLOB / `bytea`.
|
|
898
|
+
|
|
899
|
+
### MySQL 5.7 or PostgreSQL / DBeaver: `MultiCompress::Database` (`MCDB1`)
|
|
900
|
+
|
|
901
|
+
Use this path when employees must inspect decoded values in DBeaver, DataGrip,
|
|
902
|
+
or the MySQL console:
|
|
903
|
+
|
|
904
|
+
```ruby
|
|
905
|
+
require "multi_compress/database"
|
|
906
|
+
|
|
907
|
+
blob = MultiCompress::Database.compress("Привет") # UTF-8 bytes for LONGBLOB / bytea
|
|
908
|
+
text = MultiCompress::Database.decompress(blob) # => "Привет"
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
MCDB1 is deliberately narrow: zstd only, valid non-NUL UTF-8 text, exactly one
|
|
912
|
+
zstd frame, no dictionaries/Base64, and a 16 MiB decompressed limit.
|
|
913
|
+
|
|
914
|
+
#### 1. Application developer: create the DBA bundle
|
|
915
|
+
|
|
916
|
+
The native reader is intentionally **not** built during `gem install`: it must
|
|
917
|
+
be compiled on the database host, against that host's PostgreSQL/MySQL ABI.
|
|
918
|
+
The developer does not hand a DBA a Git revision or a loose `.so`; they create
|
|
919
|
+
a version-locked source bundle from the installed gem:
|
|
920
|
+
|
|
921
|
+
```bash
|
|
922
|
+
bundle exec multi_compress db package postgres --output tmp/multi-compress-postgres.tar.gz
|
|
923
|
+
# or
|
|
924
|
+
bundle exec multi_compress db package mysql --output tmp/multi-compress-mysql.tar.gz
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
The archive contains the shared MCDB1 decoder, the right server-side source,
|
|
928
|
+
vendored zstd, checksum manifest, and a small `Makefile`/installer. It is the
|
|
929
|
+
only artifact the DBA needs.
|
|
930
|
+
|
|
931
|
+
#### 2. DBA: install and enable on the database server
|
|
932
|
+
|
|
933
|
+
PostgreSQL, on the PostgreSQL host:
|
|
934
|
+
|
|
935
|
+
```bash
|
|
936
|
+
tar -xzf multi-compress-postgres.tar.gz
|
|
937
|
+
cd multi_compress-postgres-0.5.0
|
|
938
|
+
make verify
|
|
939
|
+
make doctor
|
|
940
|
+
sudo make install
|
|
941
|
+
sudo -u postgres make enable DB=app_production \
|
|
942
|
+
MIGRATION_ROLE=app_migrations READ_ROLE=dbeaver_readonly
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
`install` builds `multi_compress_pg.so` on that host, discovers paths through
|
|
946
|
+
`pg_config`, and installs the library/control/extension SQL. Install the matching
|
|
947
|
+
`postgresql-server-dev-<major>` package and a C toolchain first; `make doctor`
|
|
948
|
+
checks those prerequisites before changing the server. `enable` runs
|
|
949
|
+
`CREATE EXTENSION` in exactly one database. Repeat `make enable DB=...` for
|
|
950
|
+
reporting databases or any separate database that must query MCDB1 columns.
|
|
951
|
+
|
|
952
|
+
MySQL 5.7, on the MySQL host:
|
|
953
|
+
|
|
954
|
+
```bash
|
|
955
|
+
tar -xzf multi-compress-mysql.tar.gz
|
|
956
|
+
cd multi_compress-mysql-0.5.0
|
|
957
|
+
make verify
|
|
958
|
+
make doctor MYSQL_DEFAULTS_FILE=/etc/mysql/admin.cnf
|
|
959
|
+
sudo make install MYSQL_DEFAULTS_FILE=/etc/mysql/admin.cnf
|
|
960
|
+
make enable MYSQL_DEFAULTS_FILE=/etc/mysql/admin.cnf
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
The MySQL installer verifies the local server is MySQL 5.7, asks that same
|
|
964
|
+
server for `@@plugin_dir`, builds the UDF locally, and installs the library
|
|
965
|
+
there. `enable` registers the functions once for that MySQL server.
|
|
966
|
+
|
|
967
|
+
#### 3. Application developer: commit the readable view
|
|
968
|
+
|
|
969
|
+
Do not ask DBAs or DBeaver users to type decoder calls. Generate a safe,
|
|
970
|
+
quoted view definition and commit it as a Rails migration or versioned SQL:
|
|
971
|
+
|
|
972
|
+
```bash
|
|
973
|
+
bundle exec multi_compress db view postgres \
|
|
974
|
+
--table app.events \
|
|
975
|
+
--column payload_compressed \
|
|
976
|
+
--view admin.events_readable \
|
|
977
|
+
--columns id,created_at,status \
|
|
978
|
+
--as payload \
|
|
979
|
+
--output db/views/events_readable.sql
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
For MySQL use `db view mysql` with the same arguments. The MySQL definition
|
|
983
|
+
includes `CONVERT(... USING utf8mb4)`, so Cyrillic, Kazakh text and emoji are
|
|
984
|
+
returned correctly to DBeaver. If PostgreSQL was enabled in a non-default schema,
|
|
985
|
+
pass `--extension-schema that_schema` to the PostgreSQL view generator.
|
|
986
|
+
|
|
987
|
+
DBeaver users now make ordinary queries against the view:
|
|
988
|
+
|
|
989
|
+
```sql
|
|
990
|
+
SELECT id, created_at, status, payload
|
|
991
|
+
FROM admin.events_readable
|
|
992
|
+
WHERE id = 123;
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
The view decompresses selected rows. Filter by indexed, uncompressed fields
|
|
996
|
+
before reading it; do not perform unbounded `LIKE '%text%'` searches on the
|
|
997
|
+
decoded payload.
|
|
998
|
+
|
|
999
|
+
The shared, long-term format contract is
|
|
1000
|
+
[`docs/database-envelope-v1.md`](docs/database-envelope-v1.md).
|
|
1001
|
+
|
|
1002
|
+
### General Ruby-side `Codec`
|
|
1003
|
+
|
|
1004
|
+
`MultiCompress::Codec` transparently compresses on write and "unwraps" on read,
|
|
1005
|
+
with a strict, self-describing envelope suitable for persistent storage.
|
|
1006
|
+
|
|
1007
|
+
```ruby
|
|
1008
|
+
require "multi_compress/codec"
|
|
1009
|
+
|
|
1010
|
+
codec = MultiCompress::Codec.new(algo: :zstd, level: 6)
|
|
1011
|
+
blob = codec.dump(big_json) # always-compressed envelope for a :binary column
|
|
1012
|
+
json = codec.load(blob) # exact original string back (UTF-8 by default)
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
Guarantees: always one compressed format (no mixed raw/compressed rows); a
|
|
1016
|
+
recognized-but-corrupt envelope raises `MultiCompress::DataError` (never silent
|
|
1017
|
+
data loss); invalid encoding is rejected at **write** time; the decompression
|
|
1018
|
+
cap is frozen locally so global config can't loosen it.
|
|
1019
|
+
|
|
1020
|
+
Options: `encode: :base64` (for text/varchar columns; prefer `:binary`/`bytea`),
|
|
1021
|
+
`serializer:` (JSON or Marshal for non-String values), `encoding:`
|
|
1022
|
+
(default UTF-8; use `Encoding::BINARY` for arbitrary bytes), `max_output_size:`,
|
|
1023
|
+
`dictionary:`, and `legacy:` for reading pre-existing values:
|
|
1024
|
+
|
|
1025
|
+
```ruby
|
|
1026
|
+
MultiCompress::Codec.new(legacy: :reject) # default: no envelope -> raise
|
|
1027
|
+
MultiCompress::Codec.new(legacy: :plain) # treat as uncompressed string
|
|
1028
|
+
MultiCompress::Codec.new(legacy: { compressed: :zstd }) # decompress old raw blobs
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
### ActiveRecord (experimental)
|
|
1032
|
+
|
|
1033
|
+
Requires ActiveModel. The adapter has unit coverage but must still be integration-tested
|
|
1034
|
+
against each Rails/database combination before being treated as a stable deployment contract. Register the type (`attribute`) or use a coder (`serialize`):
|
|
1035
|
+
|
|
1036
|
+
```ruby
|
|
1037
|
+
require "multi_compress/active_record"
|
|
1038
|
+
|
|
1039
|
+
class Event < ApplicationRecord
|
|
1040
|
+
# attribute API (prefer a :binary / bytea column)
|
|
1041
|
+
attribute :payload, MultiCompress::ActiveRecordSupport::Type.new(algo: :zstd, level: 6)
|
|
1042
|
+
|
|
1043
|
+
# or the serialize macro with JSON objects
|
|
1044
|
+
# serialize :payload, coder: MultiCompress::ActiveRecordSupport::Coder.new(algo: :zstd, serializer: JSON)
|
|
1045
|
+
end
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
Dirty tracking: by default in-place mutation isn't detected — reassign the
|
|
1049
|
+
attribute (`event.payload = event.payload.merge(...)`) or pass `mutable: true`
|
|
1050
|
+
to the `Type` to enable `changed_in_place?` (which decompresses the old value
|
|
1051
|
+
on each check).
|
|
1052
|
+
|
|
865
1053
|
This guide covers comprehensive usage of the MultiCompress gem. For advanced use cases or questions, see the source code or create an issue on GitHub.
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
## MCDB2 dictionary-backed SQL columns
|
|
1057
|
+
|
|
1058
|
+
Use `MultiCompress::Database` when PostgreSQL/MySQL/DBeaver must read a compressed
|
|
1059
|
+
column. MCDB1 is dictionary-free. MCDB2 is for many small, similarly shaped JSON or
|
|
1060
|
+
text rows and requires one immutable dictionary registry version per row:
|
|
1061
|
+
|
|
1062
|
+
```ruby
|
|
1063
|
+
dictionary = MultiCompress::Database::Dictionary.train(samples, id: 42, size: 32 * 1024)
|
|
1064
|
+
blob = MultiCompress::Database.compress(payload_json, dictionary: dictionary)
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
Install/upgrade the native database reader before writing MCDB2 data, generate the
|
|
1068
|
+
append-only registry DDL with `multi_compress db registry`, then generate an INNER
|
|
1069
|
+
JOIN readable view with `multi_compress db view --dictionary-table ...`. The complete
|
|
1070
|
+
protocol and rollout contract are in [`docs/database-envelope-v2.md`](docs/database-envelope-v2.md).
|
|
1071
|
+
|
|
1072
|
+
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.
|
data/README.md
CHANGED
|
@@ -172,6 +172,73 @@ Or use the build script:
|
|
|
172
172
|
./build.sh
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
+
## Command-line tool & DB column compression
|
|
176
|
+
|
|
177
|
+
Installing the gem provides a gzip-style `multi_compress` executable:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
multi_compress file.json # -> file.json.zst
|
|
181
|
+
multi_compress -d file.json.zst # -> file.json
|
|
182
|
+
cat big.log | multi_compress -a zstd -c > big.log.zst
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`MultiCompress::Codec` is a general-purpose application-side envelope for a
|
|
186
|
+
single database column (optional ActiveRecord `Type`/`Coder` adapters and
|
|
187
|
+
optional Base64 for text columns). It is **not** the format for SQL-side
|
|
188
|
+
inspection.
|
|
189
|
+
|
|
190
|
+
For MySQL 5.7 or PostgreSQL + DBeaver, use the SQL-readable database API. `MCDB1` is dictionary-free; `MCDB2` is opt-in for a homogeneous column with an immutable zstd dictionary registry:
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
require "multi_compress/database"
|
|
194
|
+
|
|
195
|
+
blob = MultiCompress::Database.compress("JSON or UTF-8 text") # store in LONGBLOB / bytea
|
|
196
|
+
text = MultiCompress::Database.decompress(blob)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
For repeated small JSON/text records with the same shape, use MCDB2 and keep the
|
|
200
|
+
dictionary as append-only application data:
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
dictionary = MultiCompress::Database::Dictionary.train(samples, id: 42)
|
|
204
|
+
blob = MultiCompress::Database.compress(payload_json, dictionary: dictionary)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
See [`docs/database-envelope-v2.md`](docs/database-envelope-v2.md) for the
|
|
208
|
+
registry, generated view, rollout and DBeaver flow.
|
|
209
|
+
|
|
210
|
+
For database-side reading, the installed gem creates a **self-contained DBA
|
|
211
|
+
bundle** from the exact gem version — no Git clone, manual `.so` copy, or
|
|
212
|
+
hand-written extension SQL:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
bundle exec multi_compress db package postgres --output tmp/multi-compress-postgres.tar.gz
|
|
216
|
+
# or
|
|
217
|
+
bundle exec multi_compress db package mysql --output tmp/multi-compress-mysql.tar.gz
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The DBA extracts that archive on the database host and runs `make verify`,
|
|
221
|
+
`make doctor`, and `sudo make install`. PostgreSQL enablement also requires the
|
|
222
|
+
migration and read roles; MySQL uses a local Unix socket and has explicit
|
|
223
|
+
`upgrade`, `disable`, and `uninstall` confirmations. The bundle compiles the
|
|
224
|
+
native reader against that host's real PostgreSQL/MySQL environment and installs
|
|
225
|
+
it into the server-reported directory. Generate the DBeaver-facing read view
|
|
226
|
+
from the application repository as well:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
bundle exec multi_compress db view postgres \
|
|
230
|
+
--table app.events --column payload_compressed \
|
|
231
|
+
--view admin.events_readable --columns id,created_at,status \
|
|
232
|
+
--output db/views/events_readable.sql
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Use `--extension-schema NAME` with the PostgreSQL view generator only when the
|
|
236
|
+
DBA enabled the extension outside the default `multi_compress` schema. See
|
|
237
|
+
[GET_STARTED.md](GET_STARTED.md#database-column-compression) for the full
|
|
238
|
+
app-server → DBA → DBeaver flow.
|
|
239
|
+
|
|
240
|
+
> The CLI writes this gem's internal LZ4 format as `.mclz4` (not `.lz4`), since it is not interchangeable with the standard `lz4` CLI.
|
|
241
|
+
|
|
175
242
|
## Requirements
|
|
176
243
|
|
|
177
244
|
- Ruby >= 2.7.1
|
|
@@ -180,3 +247,5 @@ Or use the build script:
|
|
|
180
247
|
## License
|
|
181
248
|
|
|
182
249
|
MIT — see [LICENSE.txt](LICENSE.txt).
|
|
250
|
+
|
|
251
|
+
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,113 @@
|
|
|
1
|
+
#ifndef MCDB_FORMAT_H
|
|
2
|
+
#define MCDB_FORMAT_H
|
|
3
|
+
|
|
4
|
+
#include <stddef.h>
|
|
5
|
+
#include <stdint.h>
|
|
6
|
+
|
|
7
|
+
#include <zstd.h>
|
|
8
|
+
|
|
9
|
+
#define MCDB_MAGIC0 0x4D /* 'M' */
|
|
10
|
+
#define MCDB_MAGIC1 0x43 /* 'C' */
|
|
11
|
+
#define MCDB_MAGIC2 0x44 /* 'D' */
|
|
12
|
+
#define MCDB_MAGIC3 0x42 /* 'B' */
|
|
13
|
+
|
|
14
|
+
#define MCDB_VERSION_V1 1
|
|
15
|
+
#define MCDB_VERSION_V2 2
|
|
16
|
+
#define MCDB_CODEC_ZSTD 1
|
|
17
|
+
#define MCDB_FLAGS_NONE 0
|
|
18
|
+
|
|
19
|
+
#define MCDB_V1_HEADER_SIZE 19u
|
|
20
|
+
#define MCDB_V2_HEADER_SIZE 27u
|
|
21
|
+
#define MCDB_HEADER_SIZE MCDB_V1_HEADER_SIZE /* MCDB1 compatibility alias */
|
|
22
|
+
#define MCDB_ORIGINAL_SIZE_OFFSET 7u
|
|
23
|
+
#define MCDB_CRC_OFFSET 15u
|
|
24
|
+
#define MCDB_DICTIONARY_REF_OFFSET 19u
|
|
25
|
+
|
|
26
|
+
#define MCDB_MAX_OUTPUT (16u * 1024u * 1024u) /* 16 MiB decompressed */
|
|
27
|
+
#define MCDB_MAX_DICTIONARY_BYTES (256u * 1024u)
|
|
28
|
+
#define MCDB_MAX_DICTIONARY_REF UINT64_C(0x7FFFFFFFFFFFFFFF)
|
|
29
|
+
#define MCDB_MAX_ENVELOPE_V1 16842771u
|
|
30
|
+
#define MCDB_MAX_ENVELOPE_V2 16842779u
|
|
31
|
+
#define MCDB_MAX_ENVELOPE MCDB_MAX_ENVELOPE_V2
|
|
32
|
+
|
|
33
|
+
#define MCDB_SHA256_BYTES 32u
|
|
34
|
+
#define MCDB_ERRLEN 256
|
|
35
|
+
#define MCDB_READER_VERSION "0.6.0"
|
|
36
|
+
|
|
37
|
+
typedef struct {
|
|
38
|
+
uint8_t version;
|
|
39
|
+
uint64_t original_size;
|
|
40
|
+
uint64_t dictionary_ref;
|
|
41
|
+
size_t header_size;
|
|
42
|
+
} mcdb_header;
|
|
43
|
+
|
|
44
|
+
typedef enum {
|
|
45
|
+
MCDB_OK = 0,
|
|
46
|
+
MCDB_ERR_TRUNCATED,
|
|
47
|
+
MCDB_ERR_ENVELOPE_SIZE,
|
|
48
|
+
MCDB_ERR_MAGIC,
|
|
49
|
+
MCDB_ERR_VERSION,
|
|
50
|
+
MCDB_ERR_CODEC,
|
|
51
|
+
MCDB_ERR_FLAGS,
|
|
52
|
+
MCDB_ERR_SIZE_LIMIT,
|
|
53
|
+
MCDB_ERR_DICTIONARY_REQUIRED,
|
|
54
|
+
MCDB_ERR_DICTIONARY_REF,
|
|
55
|
+
MCDB_ERR_DICTIONARY_SIZE,
|
|
56
|
+
MCDB_ERR_DICTIONARY_ID,
|
|
57
|
+
MCDB_ERR_DECOMPRESS,
|
|
58
|
+
MCDB_ERR_TRAILING_DATA,
|
|
59
|
+
MCDB_ERR_FRAME_CONTENT_SIZE,
|
|
60
|
+
MCDB_ERR_SIZE_MISMATCH,
|
|
61
|
+
MCDB_ERR_CRC,
|
|
62
|
+
MCDB_ERR_UTF8,
|
|
63
|
+
MCDB_ERR_ALLOC
|
|
64
|
+
} mcdb_status;
|
|
65
|
+
|
|
66
|
+
/* Parses MCDB1 or MCDB2. It never decompresses. */
|
|
67
|
+
mcdb_status mcdb_parse_header(const unsigned char *in, size_t in_len, mcdb_header *out_header);
|
|
68
|
+
|
|
69
|
+
/* MCDB1 compatibility API. Rejects MCDB2. */
|
|
70
|
+
mcdb_status mcdb_validate_header(const unsigned char *in, size_t in_len,
|
|
71
|
+
uint64_t *out_original_size);
|
|
72
|
+
|
|
73
|
+
/* MCDB2 parser. Rejects MCDB1. */
|
|
74
|
+
mcdb_status mcdb_validate_v2_header(const unsigned char *in, size_t in_len,
|
|
75
|
+
uint64_t *out_original_size,
|
|
76
|
+
uint64_t *out_dictionary_ref);
|
|
77
|
+
|
|
78
|
+
/* The caller must validate the MCDB1 header before calling. */
|
|
79
|
+
mcdb_status mcdb_decode_into(const unsigned char *in, size_t in_len,
|
|
80
|
+
unsigned char *out, size_t out_capacity,
|
|
81
|
+
size_t *out_len, char *errbuf);
|
|
82
|
+
|
|
83
|
+
mcdb_status mcdb_decode(const unsigned char *in, size_t in_len,
|
|
84
|
+
unsigned char **out, size_t *out_len,
|
|
85
|
+
char *errbuf);
|
|
86
|
+
|
|
87
|
+
/* MCDB2 decode. supplied_dictionary_ref must be the registry id passed by SQL. */
|
|
88
|
+
mcdb_status mcdb_decode_v2_into(const unsigned char *in, size_t in_len,
|
|
89
|
+
uint64_t supplied_dictionary_ref,
|
|
90
|
+
const unsigned char *dictionary,
|
|
91
|
+
size_t dictionary_len,
|
|
92
|
+
ZSTD_DCtx *dctx,
|
|
93
|
+
ZSTD_DDict *ddict,
|
|
94
|
+
unsigned char *out, size_t out_capacity,
|
|
95
|
+
size_t *out_len, char *errbuf);
|
|
96
|
+
|
|
97
|
+
mcdb_status mcdb_decode_v2(const unsigned char *in, size_t in_len,
|
|
98
|
+
uint64_t supplied_dictionary_ref,
|
|
99
|
+
const unsigned char *dictionary,
|
|
100
|
+
size_t dictionary_len,
|
|
101
|
+
unsigned char **out, size_t *out_len,
|
|
102
|
+
char *errbuf);
|
|
103
|
+
|
|
104
|
+
mcdb_status mcdb_validate_dictionary(const unsigned char *dictionary, size_t dictionary_len,
|
|
105
|
+
uint32_t *out_zstd_dictionary_id);
|
|
106
|
+
|
|
107
|
+
uint32_t mcdb_zstd_frame_dictionary_id(const unsigned char *frame, size_t frame_len);
|
|
108
|
+
uint64_t mcdb_dictionary_ref(const unsigned char *in, size_t in_len, mcdb_status *out_status);
|
|
109
|
+
void mcdb_sha256(const unsigned char *data, size_t len, unsigned char out[MCDB_SHA256_BYTES]);
|
|
110
|
+
|
|
111
|
+
const char *mcdb_status_str(mcdb_status s);
|
|
112
|
+
|
|
113
|
+
#endif
|