multi_compress 0.5.0 → 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 +35 -0
- data/GET_STARTED.md +19 -0
- data/README.md +14 -1
- data/db_core/include/mcdb_format.h +67 -9
- data/db_core/src/mcdb_format.c +392 -139
- data/db_deployment/mysql/README.md +19 -0
- data/db_deployment/mysql/bin/common +70 -18
- data/db_deployment/mysql/bin/enable +5 -1
- data/db_deployment/mysql/bin/status +2 -0
- data/db_deployment/mysql/bin/upgrade +17 -5
- data/db_deployment/postgres/README.md +24 -0
- data/db_deployment/postgres/bin/enable +2 -0
- data/db_deployment/postgres/bin/install +4 -2
- data/docs/database-envelope-v2.md +147 -0
- data/docs/rfcs/0002-mcdb2-dictionaries.md +75 -0
- data/ext/multi_compress/multi_compress.c +32 -0
- data/lib/multi_compress/database.rb +255 -49
- data/lib/multi_compress/db_deployment.rb +423 -11
- data/lib/multi_compress/version.rb +1 -1
- data/mysql_udf/README.md +45 -0
- data/mysql_udf/sql/examples.sql +5 -0
- data/mysql_udf/sql/install.sql +25 -1
- data/mysql_udf/sql/uninstall.sql +7 -1
- data/mysql_udf/sql/views.sql +16 -4
- data/mysql_udf/src/multi_compress_mysql.c +383 -44
- data/mysql_udf/test/run_e2e.sh +119 -1
- data/postgres_extension/Makefile +2 -2
- data/postgres_extension/README.md +48 -0
- data/postgres_extension/multi_compress.control +2 -2
- data/postgres_extension/multi_compress_pg.exports +12 -0
- data/postgres_extension/sql/examples.sql +11 -0
- data/postgres_extension/sql/multi_compress--0.5.0--0.6.0.sql +59 -0
- data/postgres_extension/sql/multi_compress--0.6.0.sql +85 -0
- data/postgres_extension/src/multi_compress_pg.c +301 -16
- data/postgres_extension/test/run_e2e.sh +90 -2
- metadata +6 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#include <stdint.h>
|
|
1
2
|
#include <stdio.h>
|
|
2
3
|
#include <stdlib.h>
|
|
3
4
|
#include <string.h>
|
|
@@ -24,6 +25,145 @@ typedef my_bool mcdb_udf_init_result_t;
|
|
|
24
25
|
|
|
25
26
|
#include "mcdb_format.h"
|
|
26
27
|
|
|
28
|
+
#define MCDB_MYSQL_DDICT_CACHE_ENTRIES 4
|
|
29
|
+
|
|
30
|
+
typedef struct {
|
|
31
|
+
uint64_t dictionary_ref;
|
|
32
|
+
unsigned char sha256[MCDB_SHA256_BYTES];
|
|
33
|
+
unsigned char *bytes;
|
|
34
|
+
size_t bytes_len;
|
|
35
|
+
ZSTD_DDict *ddict;
|
|
36
|
+
uint64_t used_at;
|
|
37
|
+
} mcdb_mysql_cache_entry;
|
|
38
|
+
|
|
39
|
+
typedef struct {
|
|
40
|
+
char *output;
|
|
41
|
+
ZSTD_DCtx *dctx;
|
|
42
|
+
uint64_t clock;
|
|
43
|
+
mcdb_mysql_cache_entry entries[MCDB_MYSQL_DDICT_CACHE_ENTRIES];
|
|
44
|
+
} mcdb_mysql_dictionary_state;
|
|
45
|
+
|
|
46
|
+
static void mcdb_mysql_state_free(mcdb_mysql_dictionary_state *state) {
|
|
47
|
+
int i;
|
|
48
|
+
if (!state)
|
|
49
|
+
return;
|
|
50
|
+
free(state->output);
|
|
51
|
+
if (state->dctx)
|
|
52
|
+
ZSTD_freeDCtx(state->dctx);
|
|
53
|
+
for (i = 0; i < MCDB_MYSQL_DDICT_CACHE_ENTRIES; i++) {
|
|
54
|
+
free(state->entries[i].bytes);
|
|
55
|
+
if (state->entries[i].ddict)
|
|
56
|
+
ZSTD_freeDDict(state->entries[i].ddict);
|
|
57
|
+
}
|
|
58
|
+
free(state);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static mcdb_mysql_dictionary_state *mcdb_mysql_state_new(void) {
|
|
62
|
+
mcdb_mysql_dictionary_state *state = (mcdb_mysql_dictionary_state *)calloc(1, sizeof(*state));
|
|
63
|
+
if (!state)
|
|
64
|
+
return NULL;
|
|
65
|
+
state->dctx = ZSTD_createDCtx();
|
|
66
|
+
if (!state->dctx) {
|
|
67
|
+
free(state);
|
|
68
|
+
return NULL;
|
|
69
|
+
}
|
|
70
|
+
return state;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static int mcdb_mysql_parse_ref(UDF_ARGS *args, uint64_t *out_ref) {
|
|
74
|
+
long long value;
|
|
75
|
+
if (!args->args[1])
|
|
76
|
+
return 0;
|
|
77
|
+
memcpy(&value, args->args[1], sizeof(value));
|
|
78
|
+
if (value <= 0)
|
|
79
|
+
return 0;
|
|
80
|
+
*out_ref = (uint64_t)value;
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static int mcdb_mysql_cache_victim(mcdb_mysql_dictionary_state *state, int same_key) {
|
|
85
|
+
int i;
|
|
86
|
+
int victim = 0;
|
|
87
|
+
uint64_t oldest = UINT64_MAX;
|
|
88
|
+
if (same_key >= 0)
|
|
89
|
+
return same_key;
|
|
90
|
+
for (i = 0; i < MCDB_MYSQL_DDICT_CACHE_ENTRIES; i++) {
|
|
91
|
+
if (!state->entries[i].ddict)
|
|
92
|
+
return i;
|
|
93
|
+
if (state->entries[i].used_at < oldest) {
|
|
94
|
+
oldest = state->entries[i].used_at;
|
|
95
|
+
victim = i;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return victim;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
static ZSTD_DDict *mcdb_mysql_cached_ddict(mcdb_mysql_dictionary_state *state,
|
|
102
|
+
uint64_t dictionary_ref,
|
|
103
|
+
const unsigned char *dictionary_sha256,
|
|
104
|
+
const unsigned char *dictionary, size_t dictionary_len) {
|
|
105
|
+
unsigned char actual_sha256[MCDB_SHA256_BYTES];
|
|
106
|
+
int i;
|
|
107
|
+
int same_key = -1;
|
|
108
|
+
int victim;
|
|
109
|
+
ZSTD_DDict *ddict;
|
|
110
|
+
|
|
111
|
+
for (i = 0; i < MCDB_MYSQL_DDICT_CACHE_ENTRIES; i++) {
|
|
112
|
+
mcdb_mysql_cache_entry *entry = &state->entries[i];
|
|
113
|
+
if (!entry->ddict || entry->dictionary_ref != dictionary_ref ||
|
|
114
|
+
memcmp(entry->sha256, dictionary_sha256, MCDB_SHA256_BYTES) != 0)
|
|
115
|
+
continue;
|
|
116
|
+
same_key = i;
|
|
117
|
+
if (entry->bytes_len == dictionary_len &&
|
|
118
|
+
memcmp(entry->bytes, dictionary, dictionary_len) == 0) {
|
|
119
|
+
entry->used_at = ++state->clock;
|
|
120
|
+
return entry->ddict;
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
mcdb_sha256(dictionary, dictionary_len, actual_sha256);
|
|
126
|
+
if (memcmp(actual_sha256, dictionary_sha256, MCDB_SHA256_BYTES) != 0)
|
|
127
|
+
return NULL;
|
|
128
|
+
if (mcdb_validate_dictionary(dictionary, dictionary_len, NULL) != MCDB_OK)
|
|
129
|
+
return NULL;
|
|
130
|
+
ddict = ZSTD_createDDict(dictionary, dictionary_len);
|
|
131
|
+
if (!ddict)
|
|
132
|
+
return NULL;
|
|
133
|
+
|
|
134
|
+
victim = mcdb_mysql_cache_victim(state, same_key);
|
|
135
|
+
free(state->entries[victim].bytes);
|
|
136
|
+
if (state->entries[victim].ddict)
|
|
137
|
+
ZSTD_freeDDict(state->entries[victim].ddict);
|
|
138
|
+
state->entries[victim].bytes = (unsigned char *)malloc(dictionary_len);
|
|
139
|
+
if (!state->entries[victim].bytes) {
|
|
140
|
+
ZSTD_freeDDict(ddict);
|
|
141
|
+
state->entries[victim].ddict = NULL;
|
|
142
|
+
state->entries[victim].bytes_len = 0;
|
|
143
|
+
return NULL;
|
|
144
|
+
}
|
|
145
|
+
memcpy(state->entries[victim].bytes, dictionary, dictionary_len);
|
|
146
|
+
state->entries[victim].dictionary_ref = dictionary_ref;
|
|
147
|
+
memcpy(state->entries[victim].sha256, dictionary_sha256, MCDB_SHA256_BYTES);
|
|
148
|
+
state->entries[victim].bytes_len = dictionary_len;
|
|
149
|
+
state->entries[victim].ddict = ddict;
|
|
150
|
+
state->entries[victim].used_at = ++state->clock;
|
|
151
|
+
return ddict;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
static mcdb_udf_init_result_t mcdb_one_blob_init(UDF_INIT *initid, UDF_ARGS *args, char *message,
|
|
155
|
+
const char *signature, unsigned long max_length) {
|
|
156
|
+
if (args->arg_count != 1) {
|
|
157
|
+
snprintf(message, 255, "%s takes exactly one argument", signature);
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
args->arg_type[0] = STRING_RESULT;
|
|
161
|
+
initid->maybe_null = 1;
|
|
162
|
+
initid->const_item = 0;
|
|
163
|
+
initid->max_length = max_length;
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
27
167
|
mcdb_udf_init_result_t multi_compress_db_version_init(UDF_INIT *initid, UDF_ARGS *args,
|
|
28
168
|
char *message) {
|
|
29
169
|
(void)args;
|
|
@@ -33,101 +173,300 @@ mcdb_udf_init_result_t multi_compress_db_version_init(UDF_INIT *initid, UDF_ARGS
|
|
|
33
173
|
}
|
|
34
174
|
initid->maybe_null = 0;
|
|
35
175
|
initid->const_item = 1;
|
|
36
|
-
initid->max_length =
|
|
176
|
+
initid->max_length = 96;
|
|
37
177
|
return 0;
|
|
38
178
|
}
|
|
39
179
|
|
|
40
180
|
char *multi_compress_db_version(UDF_INIT *initid, UDF_ARGS *args, char *result,
|
|
41
181
|
unsigned long *length, char *is_null, char *error) {
|
|
182
|
+
int n;
|
|
42
183
|
(void)initid;
|
|
43
184
|
(void)args;
|
|
44
185
|
*is_null = 0;
|
|
45
186
|
*error = 0;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
ZSTD_versionString());
|
|
187
|
+
n = snprintf(result, 255, "multi_compress reader %s; MCDB1 + MCDB2; zstd %s",
|
|
188
|
+
MCDB_READER_VERSION, ZSTD_versionString());
|
|
49
189
|
*length = (unsigned long)(n > 0 ? n : 0);
|
|
50
190
|
return result;
|
|
51
191
|
}
|
|
52
192
|
|
|
53
193
|
mcdb_udf_init_result_t multi_compress_db_is_valid_init(UDF_INIT *initid, UDF_ARGS *args,
|
|
54
194
|
char *message) {
|
|
55
|
-
|
|
56
|
-
strcpy(message, "multi_compress_db_is_valid(blob) takes exactly one argument");
|
|
57
|
-
return 1;
|
|
58
|
-
}
|
|
59
|
-
args->arg_type[0] = STRING_RESULT;
|
|
60
|
-
initid->maybe_null = 1;
|
|
61
|
-
initid->const_item = 0;
|
|
62
|
-
initid->max_length = 1;
|
|
63
|
-
return 0;
|
|
195
|
+
return mcdb_one_blob_init(initid, args, message, "multi_compress_db_is_valid(blob)", 1);
|
|
64
196
|
}
|
|
65
197
|
|
|
66
198
|
long long multi_compress_db_is_valid(UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error) {
|
|
199
|
+
unsigned char *out = NULL;
|
|
200
|
+
size_t out_len = 0;
|
|
201
|
+
char err[MCDB_ERRLEN];
|
|
202
|
+
mcdb_status st;
|
|
67
203
|
(void)initid;
|
|
68
204
|
*error = 0;
|
|
69
|
-
if (args->args[0]
|
|
205
|
+
if (!args->args[0]) {
|
|
70
206
|
*is_null = 1;
|
|
71
207
|
return 0;
|
|
72
208
|
}
|
|
73
209
|
*is_null = 0;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
size_t out_len = 0;
|
|
77
|
-
char err[MCDB_ERRLEN];
|
|
78
|
-
mcdb_status st = mcdb_decode((const unsigned char *)args->args[0], (size_t)args->lengths[0],
|
|
79
|
-
&out, &out_len, err);
|
|
210
|
+
st = mcdb_decode((const unsigned char *)args->args[0], (size_t)args->lengths[0], &out, &out_len,
|
|
211
|
+
err);
|
|
80
212
|
free(out);
|
|
81
213
|
return st == MCDB_OK ? 1 : 0;
|
|
82
214
|
}
|
|
83
215
|
|
|
84
216
|
mcdb_udf_init_result_t multi_compress_db_decompress_init(UDF_INIT *initid, UDF_ARGS *args,
|
|
85
217
|
char *message) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
initid->maybe_null = 1;
|
|
92
|
-
initid->const_item = 0;
|
|
93
|
-
initid->max_length = MCDB_MAX_OUTPUT; /* result can be up to 16 MiB, not the input size */
|
|
94
|
-
initid->ptr = NULL; /* holds the malloc'd result between _decompress calls */
|
|
218
|
+
mcdb_udf_init_result_t result = mcdb_one_blob_init(
|
|
219
|
+
initid, args, message, "multi_compress_db_decompress(blob)", MCDB_MAX_OUTPUT);
|
|
220
|
+
if (result)
|
|
221
|
+
return result;
|
|
222
|
+
initid->ptr = NULL;
|
|
95
223
|
return 0;
|
|
96
224
|
}
|
|
97
225
|
|
|
98
226
|
void multi_compress_db_decompress_deinit(UDF_INIT *initid) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
initid->ptr = NULL;
|
|
102
|
-
}
|
|
227
|
+
free(initid->ptr);
|
|
228
|
+
initid->ptr = NULL;
|
|
103
229
|
}
|
|
104
230
|
|
|
105
231
|
char *multi_compress_db_decompress(UDF_INIT *initid, UDF_ARGS *args, char *result,
|
|
106
232
|
unsigned long *length, char *is_null, char *error) {
|
|
233
|
+
unsigned char *out = NULL;
|
|
234
|
+
size_t out_len = 0;
|
|
235
|
+
char err[MCDB_ERRLEN];
|
|
236
|
+
mcdb_status st;
|
|
107
237
|
(void)result;
|
|
108
238
|
*error = 0;
|
|
109
|
-
if (args->args[0]
|
|
239
|
+
if (!args->args[0]) {
|
|
110
240
|
*is_null = 1;
|
|
111
241
|
return NULL;
|
|
112
242
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
243
|
+
free(initid->ptr);
|
|
244
|
+
initid->ptr = NULL;
|
|
245
|
+
st = mcdb_decode((const unsigned char *)args->args[0], (size_t)args->lengths[0], &out, &out_len,
|
|
246
|
+
err);
|
|
247
|
+
if (st != MCDB_OK) {
|
|
248
|
+
*is_null = 1;
|
|
249
|
+
free(out);
|
|
250
|
+
return NULL;
|
|
116
251
|
}
|
|
252
|
+
initid->ptr = (char *)out;
|
|
253
|
+
*length = (unsigned long)out_len;
|
|
254
|
+
*is_null = 0;
|
|
255
|
+
return (char *)out;
|
|
256
|
+
}
|
|
117
257
|
|
|
118
|
-
|
|
119
|
-
|
|
258
|
+
mcdb_udf_init_result_t multi_compress_db_original_size_init(UDF_INIT *initid, UDF_ARGS *args,
|
|
259
|
+
char *message) {
|
|
260
|
+
return mcdb_one_blob_init(initid, args, message, "multi_compress_db_original_size(blob)", 20);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
long long multi_compress_db_original_size(UDF_INIT *initid, UDF_ARGS *args, char *is_null,
|
|
264
|
+
char *error) {
|
|
265
|
+
mcdb_header header;
|
|
266
|
+
mcdb_status st;
|
|
267
|
+
(void)initid;
|
|
268
|
+
*error = 0;
|
|
269
|
+
if (!args->args[0]) {
|
|
270
|
+
*is_null = 1;
|
|
271
|
+
return 0;
|
|
272
|
+
}
|
|
273
|
+
st = mcdb_parse_header((const unsigned char *)args->args[0], (size_t)args->lengths[0], &header);
|
|
274
|
+
if (st != MCDB_OK) {
|
|
275
|
+
*is_null = 1;
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
*is_null = 0;
|
|
279
|
+
return (long long)header.original_size;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
mcdb_udf_init_result_t multi_compress_db_dictionary_ref_init(UDF_INIT *initid, UDF_ARGS *args,
|
|
283
|
+
char *message) {
|
|
284
|
+
return mcdb_one_blob_init(initid, args, message, "multi_compress_db_dictionary_ref(blob)", 20);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
long long multi_compress_db_dictionary_ref(UDF_INIT *initid, UDF_ARGS *args, char *is_null,
|
|
288
|
+
char *error) {
|
|
289
|
+
uint64_t ref;
|
|
290
|
+
mcdb_status st;
|
|
291
|
+
(void)initid;
|
|
292
|
+
*error = 0;
|
|
293
|
+
if (!args->args[0]) {
|
|
294
|
+
*is_null = 1;
|
|
295
|
+
return 0;
|
|
296
|
+
}
|
|
297
|
+
ref = mcdb_dictionary_ref((const unsigned char *)args->args[0], (size_t)args->lengths[0], &st);
|
|
298
|
+
if (st != MCDB_OK) {
|
|
299
|
+
*is_null = 1;
|
|
300
|
+
return 0;
|
|
301
|
+
}
|
|
302
|
+
*is_null = 0;
|
|
303
|
+
return (long long)ref;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
mcdb_udf_init_result_t multi_compress_db_dictionary_zstd_id_init(UDF_INIT *initid, UDF_ARGS *args,
|
|
307
|
+
char *message) {
|
|
308
|
+
return mcdb_one_blob_init(initid, args, message,
|
|
309
|
+
"multi_compress_db_dictionary_zstd_id(dictionary)", 20);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
long long multi_compress_db_dictionary_zstd_id(UDF_INIT *initid, UDF_ARGS *args, char *is_null,
|
|
313
|
+
char *error) {
|
|
314
|
+
uint32_t id;
|
|
315
|
+
(void)initid;
|
|
316
|
+
*error = 0;
|
|
317
|
+
if (!args->args[0] || mcdb_validate_dictionary((const unsigned char *)args->args[0],
|
|
318
|
+
(size_t)args->lengths[0], &id) != MCDB_OK) {
|
|
319
|
+
*is_null = 1;
|
|
320
|
+
return 0;
|
|
321
|
+
}
|
|
322
|
+
*is_null = 0;
|
|
323
|
+
return (long long)id;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
mcdb_udf_init_result_t multi_compress_db_dictionary_sha256_init(UDF_INIT *initid, UDF_ARGS *args,
|
|
327
|
+
char *message) {
|
|
328
|
+
return mcdb_one_blob_init(initid, args, message,
|
|
329
|
+
"multi_compress_db_dictionary_sha256(dictionary)", MCDB_SHA256_BYTES);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
char *multi_compress_db_dictionary_sha256(UDF_INIT *initid, UDF_ARGS *args, char *result,
|
|
333
|
+
unsigned long *length, char *is_null, char *error) {
|
|
334
|
+
(void)initid;
|
|
335
|
+
*error = 0;
|
|
336
|
+
if (!args->args[0]) {
|
|
337
|
+
*is_null = 1;
|
|
338
|
+
return NULL;
|
|
339
|
+
}
|
|
340
|
+
mcdb_sha256((const unsigned char *)args->args[0], (size_t)args->lengths[0],
|
|
341
|
+
(unsigned char *)result);
|
|
342
|
+
*length = MCDB_SHA256_BYTES;
|
|
343
|
+
*is_null = 0;
|
|
344
|
+
return result;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
static mcdb_udf_init_result_t mcdb_dict_init(UDF_INIT *initid, UDF_ARGS *args, char *message,
|
|
348
|
+
const char *signature, unsigned long max_length) {
|
|
349
|
+
mcdb_mysql_dictionary_state *state;
|
|
350
|
+
if (args->arg_count != 4) {
|
|
351
|
+
snprintf(message, 255, "%s takes exactly four arguments", signature);
|
|
352
|
+
return 1;
|
|
353
|
+
}
|
|
354
|
+
args->arg_type[0] = STRING_RESULT;
|
|
355
|
+
args->arg_type[1] = INT_RESULT;
|
|
356
|
+
args->arg_type[2] = STRING_RESULT;
|
|
357
|
+
args->arg_type[3] = STRING_RESULT;
|
|
358
|
+
initid->maybe_null = 1;
|
|
359
|
+
initid->const_item = 0;
|
|
360
|
+
initid->max_length = max_length;
|
|
361
|
+
state = mcdb_mysql_state_new();
|
|
362
|
+
if (!state) {
|
|
363
|
+
strcpy(message, "multi_compress: could not allocate zstd dictionary cache");
|
|
364
|
+
return 1;
|
|
365
|
+
}
|
|
366
|
+
initid->ptr = (char *)state;
|
|
367
|
+
return 0;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
mcdb_udf_init_result_t multi_compress_db_is_valid_dict_init(UDF_INIT *initid, UDF_ARGS *args,
|
|
371
|
+
char *message) {
|
|
372
|
+
return mcdb_dict_init(
|
|
373
|
+
initid, args, message,
|
|
374
|
+
"multi_compress_db_is_valid_dict(blob, dictionary_id, dictionary_sha256, dictionary)", 1);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
mcdb_udf_init_result_t multi_compress_db_decompress_dict_init(UDF_INIT *initid, UDF_ARGS *args,
|
|
378
|
+
char *message) {
|
|
379
|
+
return mcdb_dict_init(
|
|
380
|
+
initid, args, message,
|
|
381
|
+
"multi_compress_db_decompress_dict(blob, dictionary_id, dictionary_sha256, dictionary)",
|
|
382
|
+
MCDB_MAX_OUTPUT);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
void multi_compress_db_is_valid_dict_deinit(UDF_INIT *initid) {
|
|
386
|
+
mcdb_mysql_state_free((mcdb_mysql_dictionary_state *)initid->ptr);
|
|
387
|
+
initid->ptr = NULL;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
void multi_compress_db_decompress_dict_deinit(UDF_INIT *initid) {
|
|
391
|
+
mcdb_mysql_state_free((mcdb_mysql_dictionary_state *)initid->ptr);
|
|
392
|
+
initid->ptr = NULL;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
static mcdb_status mcdb_mysql_decode_v2(UDF_INIT *initid, UDF_ARGS *args, unsigned char **out,
|
|
396
|
+
size_t *out_len) {
|
|
397
|
+
mcdb_mysql_dictionary_state *state = (mcdb_mysql_dictionary_state *)initid->ptr;
|
|
398
|
+
uint64_t dictionary_ref;
|
|
399
|
+
ZSTD_DDict *ddict;
|
|
400
|
+
uint64_t original_size;
|
|
401
|
+
unsigned char *buffer;
|
|
120
402
|
char err[MCDB_ERRLEN];
|
|
121
|
-
mcdb_status st
|
|
122
|
-
|
|
403
|
+
mcdb_status st;
|
|
404
|
+
|
|
405
|
+
if (!args->args[0] || !args->args[1] || !args->args[2] || !args->args[3])
|
|
406
|
+
return MCDB_ERR_DICTIONARY_REQUIRED;
|
|
407
|
+
if (args->lengths[2] != MCDB_SHA256_BYTES || !mcdb_mysql_parse_ref(args, &dictionary_ref))
|
|
408
|
+
return MCDB_ERR_DICTIONARY_REF;
|
|
409
|
+
ddict = mcdb_mysql_cached_ddict(state, dictionary_ref, (const unsigned char *)args->args[2],
|
|
410
|
+
(const unsigned char *)args->args[3], (size_t)args->lengths[3]);
|
|
411
|
+
if (!ddict)
|
|
412
|
+
return MCDB_ERR_DICTIONARY_ID;
|
|
413
|
+
st = mcdb_validate_v2_header((const unsigned char *)args->args[0], (size_t)args->lengths[0],
|
|
414
|
+
&original_size, NULL);
|
|
415
|
+
if (st != MCDB_OK)
|
|
416
|
+
return st;
|
|
417
|
+
buffer = (unsigned char *)malloc(original_size ? (size_t)original_size : 1);
|
|
418
|
+
if (!buffer)
|
|
419
|
+
return MCDB_ERR_ALLOC;
|
|
420
|
+
st = mcdb_decode_v2_into((const unsigned char *)args->args[0], (size_t)args->lengths[0],
|
|
421
|
+
dictionary_ref, (const unsigned char *)args->args[3],
|
|
422
|
+
(size_t)args->lengths[3], state->dctx, ddict, buffer,
|
|
423
|
+
(size_t)original_size, out_len, err);
|
|
123
424
|
if (st != MCDB_OK) {
|
|
425
|
+
free(buffer);
|
|
426
|
+
return st;
|
|
427
|
+
}
|
|
428
|
+
*out = buffer;
|
|
429
|
+
return MCDB_OK;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
long long multi_compress_db_is_valid_dict(UDF_INIT *initid, UDF_ARGS *args, char *is_null,
|
|
433
|
+
char *error) {
|
|
434
|
+
unsigned char *out = NULL;
|
|
435
|
+
size_t out_len = 0;
|
|
436
|
+
mcdb_status st;
|
|
437
|
+
*error = 0;
|
|
438
|
+
if (!args->args[0] || !args->args[1] || !args->args[2] || !args->args[3]) {
|
|
439
|
+
*is_null = 1;
|
|
440
|
+
return 0;
|
|
441
|
+
}
|
|
442
|
+
st = mcdb_mysql_decode_v2(initid, args, &out, &out_len);
|
|
443
|
+
free(out);
|
|
444
|
+
*is_null = 0;
|
|
445
|
+
return st == MCDB_OK ? 1 : 0;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
char *multi_compress_db_decompress_dict(UDF_INIT *initid, UDF_ARGS *args, char *result,
|
|
449
|
+
unsigned long *length, char *is_null, char *error) {
|
|
450
|
+
mcdb_mysql_dictionary_state *state = (mcdb_mysql_dictionary_state *)initid->ptr;
|
|
451
|
+
unsigned char *out = NULL;
|
|
452
|
+
size_t out_len = 0;
|
|
453
|
+
mcdb_status st;
|
|
454
|
+
(void)result;
|
|
455
|
+
*error = 0;
|
|
456
|
+
if (!args->args[0] || !args->args[1] || !args->args[2] || !args->args[3]) {
|
|
124
457
|
*is_null = 1;
|
|
458
|
+
return NULL;
|
|
459
|
+
}
|
|
460
|
+
free(state->output);
|
|
461
|
+
state->output = NULL;
|
|
462
|
+
st = mcdb_mysql_decode_v2(initid, args, &out, &out_len);
|
|
463
|
+
if (st != MCDB_OK) {
|
|
125
464
|
free(out);
|
|
465
|
+
*is_null = 1;
|
|
126
466
|
return NULL;
|
|
127
467
|
}
|
|
128
|
-
|
|
129
|
-
initid->ptr = (char *)out;
|
|
468
|
+
state->output = (char *)out;
|
|
130
469
|
*length = (unsigned long)out_len;
|
|
131
470
|
*is_null = 0;
|
|
132
|
-
return
|
|
471
|
+
return state->output;
|
|
133
472
|
}
|
data/mysql_udf/test/run_e2e.sh
CHANGED
|
@@ -74,6 +74,23 @@ compress_hex() {
|
|
|
74
74
|
-e 'print MultiCompress::Database.compress(ENV.fetch("MCDB_TEXT")).unpack1("H*")'
|
|
75
75
|
}
|
|
76
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
|
+
|
|
77
94
|
bundle_name() {
|
|
78
95
|
ruby -I "$PACKAGED_GEM_ROOT/lib" -r multi_compress/version \
|
|
79
96
|
-e 'print "multi_compress-mysql-#{MultiCompress::VERSION}"'
|
|
@@ -138,6 +155,26 @@ apply_readable_view() {
|
|
|
138
155
|
docker exec -i "$CONTAINER" mysql --default-character-set=utf8mb4 < "$path"
|
|
139
156
|
}
|
|
140
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
|
+
|
|
141
178
|
ensure_ruby_extension
|
|
142
179
|
prepare_packaged_gem
|
|
143
180
|
build_deployment_bundle
|
|
@@ -151,9 +188,23 @@ BIG=$(ruby -e 'print "a" * 300')
|
|
|
151
188
|
VALID_HEX=$(compress_hex "$TEXT")
|
|
152
189
|
BIG_HEX=$(compress_hex "$BIG")
|
|
153
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')
|
|
154
203
|
BUNDLE_NAME="$(bundle_name)"
|
|
155
204
|
VIEW_SQL="$(mktemp -t multi-compress-mysql-view.XXXXXX.sql)"
|
|
156
|
-
|
|
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
|
|
157
208
|
|
|
158
209
|
docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
|
|
159
210
|
docker run --platform "$PLATFORM" -d --name "$CONTAINER" \
|
|
@@ -202,6 +253,35 @@ q "DROP DATABASE IF EXISTS app;
|
|
|
202
253
|
(6, UNHEX('$VALID_HEX'));"
|
|
203
254
|
apply_readable_view "$VIEW_SQL"
|
|
204
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
|
+
|
|
205
285
|
fails=0
|
|
206
286
|
check() {
|
|
207
287
|
if [ "$2" = "$3" ]; then echo " PASS $1"; else echo " FAIL $1 (got [$2] want [$3])"; fails=$((fails + 1)); fi
|
|
@@ -238,18 +318,56 @@ done
|
|
|
238
318
|
check "is_valid false (crc)" \
|
|
239
319
|
"$(q "SELECT multi_compress_db_is_valid(UNHEX('$(fixture_hex test/fixtures/database_v1/corrupt_crc.mcdb)'));")" \
|
|
240
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
|
|
241
347
|
check "max_allowed_packet is reported" "$(q 'SELECT @@GLOBAL.max_allowed_packet > 0;')" "1"
|
|
242
348
|
|
|
243
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.
|
|
244
353
|
docker exec -u 0 \
|
|
245
354
|
-e "MCDB_BUNDLE_ROOT=${BUNDLE_PARENT}/${BUNDLE_NAME}" \
|
|
246
355
|
-e "MCDB_MYSQL_SOCKET=$MYSQL_SOCKET_PATH" \
|
|
247
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;"
|
|
248
364
|
cd "$MCDB_BUNDLE_ROOT"
|
|
365
|
+
make status MYSQL_SOCKET="$MCDB_MYSQL_SOCKET" | grep -F "state: MCDB1-only"
|
|
249
366
|
make doctor MYSQL_SOCKET="$MCDB_MYSQL_SOCKET" | grep -F "server socket: $MCDB_MYSQL_SOCKET"
|
|
250
367
|
make upgrade CONFIRM=UPGRADE_MULTI_COMPRESS MYSQL_SOCKET="$MCDB_MYSQL_SOCKET"
|
|
251
368
|
make status MYSQL_SOCKET="$MCDB_MYSQL_SOCKET" | grep -F "state: enabled"
|
|
252
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"
|
|
253
371
|
check "upgrade keeps version() available" "$(q 'SELECT LOCATE(CHAR(77,67,68,66,49), multi_compress_db_version()) > 0;')" "1"
|
|
254
372
|
|
|
255
373
|
# Register one expected name to a different SONAME and prove enable refuses without
|
data/postgres_extension/Makefile
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
EXTENSION = multi_compress
|
|
2
2
|
MODULE_big = multi_compress_pg
|
|
3
3
|
OBJS = src/multi_compress_pg.o src/mcdb_format.o
|
|
4
|
-
DATA = sql/multi_compress--0.5.0.sql
|
|
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
5
|
|
|
6
6
|
PG_CONFIG ?= pg_config
|
|
7
7
|
ZSTD_DIR ?= ../ext/multi_compress/vendor/zstd/lib
|
|
@@ -56,7 +56,7 @@ $(ZSTD_OBJ_DIR)/%.o: $(ZSTD_DIR)/%.S
|
|
|
56
56
|
$(ZSTD_LIB): $(ZSTD_OBJS) | $(BUILD)
|
|
57
57
|
$(AR) rcs $@ $^
|
|
58
58
|
|
|
59
|
-
# The source wrapper includes the
|
|
59
|
+
# The source wrapper includes the shared MCDB1/MCDB2 decoder; it is not a fork.
|
|
60
60
|
src/mcdb_format.o: src/mcdb_format.c $(CORE_DIR)/src/mcdb_format.c $(CORE_DIR)/include/mcdb_format.h
|
|
61
61
|
|
|
62
62
|
# Golden fixtures are committed. Regeneration is a development action only.
|