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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/GET_STARTED.md +19 -0
  4. data/README.md +14 -1
  5. data/db_core/include/mcdb_format.h +67 -9
  6. data/db_core/src/mcdb_format.c +392 -139
  7. data/db_deployment/mysql/README.md +19 -0
  8. data/db_deployment/mysql/bin/common +70 -18
  9. data/db_deployment/mysql/bin/enable +5 -1
  10. data/db_deployment/mysql/bin/status +2 -0
  11. data/db_deployment/mysql/bin/upgrade +17 -5
  12. data/db_deployment/postgres/README.md +24 -0
  13. data/db_deployment/postgres/bin/enable +2 -0
  14. data/db_deployment/postgres/bin/install +4 -2
  15. data/docs/database-envelope-v2.md +147 -0
  16. data/docs/rfcs/0002-mcdb2-dictionaries.md +75 -0
  17. data/ext/multi_compress/multi_compress.c +32 -0
  18. data/lib/multi_compress/database.rb +255 -49
  19. data/lib/multi_compress/db_deployment.rb +423 -11
  20. data/lib/multi_compress/version.rb +1 -1
  21. data/mysql_udf/README.md +45 -0
  22. data/mysql_udf/sql/examples.sql +5 -0
  23. data/mysql_udf/sql/install.sql +25 -1
  24. data/mysql_udf/sql/uninstall.sql +7 -1
  25. data/mysql_udf/sql/views.sql +16 -4
  26. data/mysql_udf/src/multi_compress_mysql.c +383 -44
  27. data/mysql_udf/test/run_e2e.sh +119 -1
  28. data/postgres_extension/Makefile +2 -2
  29. data/postgres_extension/README.md +48 -0
  30. data/postgres_extension/multi_compress.control +2 -2
  31. data/postgres_extension/multi_compress_pg.exports +12 -0
  32. data/postgres_extension/sql/examples.sql +11 -0
  33. data/postgres_extension/sql/multi_compress--0.5.0--0.6.0.sql +59 -0
  34. data/postgres_extension/sql/multi_compress--0.6.0.sql +85 -0
  35. data/postgres_extension/src/multi_compress_pg.c +301 -16
  36. data/postgres_extension/test/run_e2e.sh +90 -2
  37. 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 = 64;
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
- /* result buffer is 255 bytes per the UDF ABI */
47
- int n = snprintf(result, 255, "multi_compress reader %s; MCDB1; zstd %s", MCDB_READER_VERSION,
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
- if (args->arg_count != 1) {
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] == NULL) {
205
+ if (!args->args[0]) {
70
206
  *is_null = 1;
71
207
  return 0;
72
208
  }
73
209
  *is_null = 0;
74
-
75
- unsigned char *out = NULL;
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
- if (args->arg_count != 1) {
87
- strcpy(message, "multi_compress_db_decompress(blob) takes exactly one argument");
88
- return 1;
89
- }
90
- args->arg_type[0] = STRING_RESULT;
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
- if (initid->ptr) {
100
- free(initid->ptr);
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] == NULL) {
239
+ if (!args->args[0]) {
110
240
  *is_null = 1;
111
241
  return NULL;
112
242
  }
113
- if (initid->ptr) {
114
- free(initid->ptr);
115
- initid->ptr = NULL;
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
- unsigned char *out = NULL;
119
- size_t out_len = 0;
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 = mcdb_decode((const unsigned char *)args->args[0], (size_t)args->lengths[0],
122
- &out, &out_len, err);
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 (char *)out;
471
+ return state->output;
133
472
  }
@@ -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
- trap 'rm -f "$VIEW_SQL"; cleanup' EXIT
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
@@ -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 common MCDB1 decoder; it is not a fork.
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.