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
|
@@ -14,8 +14,9 @@ require "zlib"
|
|
|
14
14
|
module MultiCompress
|
|
15
15
|
module DBDeployment
|
|
16
16
|
ROOT = File.expand_path("../..", __dir__)
|
|
17
|
-
FORMAT = "MCDB1"
|
|
18
|
-
BUNDLE_FORMAT_VERSION =
|
|
17
|
+
FORMAT = "MCDB1 + MCDB2"
|
|
18
|
+
BUNDLE_FORMAT_VERSION = 2
|
|
19
|
+
MAX_DICTIONARY_BYTES = 256 * 1024
|
|
19
20
|
|
|
20
21
|
class Error < StandardError; end
|
|
21
22
|
|
|
@@ -32,6 +33,8 @@ module MultiCompress
|
|
|
32
33
|
PackageCommand.new.run(argv)
|
|
33
34
|
when "view"
|
|
34
35
|
ViewCommand.new.run(argv)
|
|
36
|
+
when "registry"
|
|
37
|
+
RegistryCommand.new.run(argv)
|
|
35
38
|
when "help", "--help", "-h", nil
|
|
36
39
|
puts help
|
|
37
40
|
0
|
|
@@ -66,7 +69,14 @@ module MultiCompress
|
|
|
66
69
|
install`, then `make enable DB=...`.
|
|
67
70
|
|
|
68
71
|
`view` emits a safe readable-view definition so DBeaver users query
|
|
69
|
-
decoded text instead of the compressed bytea/BLOB column.
|
|
72
|
+
decoded text instead of the compressed bytea/BLOB column. Supply
|
|
73
|
+
--dictionary-table and --dictionary-id-column for MCDB2.
|
|
74
|
+
|
|
75
|
+
`registry` emits append-only MCDB2 dictionary-registry DDL. Dictionary
|
|
76
|
+
bytes are application data and are never placed in a DBA reader bundle.
|
|
77
|
+
Supplying --payload-table, --payload-column and
|
|
78
|
+
--payload-dictionary-id-column also emits the MCDB2 header/FK
|
|
79
|
+
consistency trigger for that source table.
|
|
70
80
|
TEXT
|
|
71
81
|
end
|
|
72
82
|
end
|
|
@@ -105,7 +115,12 @@ module MultiCompress
|
|
|
105
115
|
raise OptionParser::MissingArgument, "TARGET must be postgres or mysql" if target.nil?
|
|
106
116
|
raise OptionParser::InvalidArgument, "TARGET must be postgres or mysql" unless TARGETS.include?(target)
|
|
107
117
|
|
|
108
|
-
options = {
|
|
118
|
+
options = {
|
|
119
|
+
columns: [], as: "payload", output: nil, extension_schema: "multi_compress",
|
|
120
|
+
dictionary_table: nil, dictionary_id_column: nil,
|
|
121
|
+
registry_id_column: "id", dictionary_sha256_column: "sha256",
|
|
122
|
+
dictionary_bytes_column: "bytes"
|
|
123
|
+
}
|
|
109
124
|
parser = OptionParser.new do |opts|
|
|
110
125
|
opts.banner = "Usage: multi_compress db view #{target} --table SCHEMA.TABLE --column COLUMN --view SCHEMA.VIEW --columns ID,CREATED_AT [OPTIONS]"
|
|
111
126
|
opts.on("--table NAME", "source table, optionally schema-qualified") { |value| options[:table] = value }
|
|
@@ -113,6 +128,11 @@ module MultiCompress
|
|
|
113
128
|
opts.on("--view NAME", "destination view, optionally schema-qualified") { |value| options[:view] = value }
|
|
114
129
|
opts.on("--columns LIST", "comma-separated plain columns to expose") { |value| options[:columns] = value.split(",").map(&:strip) }
|
|
115
130
|
opts.on("--as NAME", "decoded column name (default: payload)") { |value| options[:as] = value }
|
|
131
|
+
opts.on("--dictionary-table NAME", "MCDB2 immutable dictionary registry table") { |value| options[:dictionary_table] = value }
|
|
132
|
+
opts.on("--dictionary-id-column NAME", "MCDB2 FK column on source table") { |value| options[:dictionary_id_column] = value }
|
|
133
|
+
opts.on("--registry-id-column NAME", "registry PK (default: id)") { |value| options[:registry_id_column] = value }
|
|
134
|
+
opts.on("--dictionary-sha256-column NAME", "registry SHA-256 column (default: sha256)") { |value| options[:dictionary_sha256_column] = value }
|
|
135
|
+
opts.on("--dictionary-bytes-column NAME", "registry dictionary bytes column (default: bytes)") { |value| options[:dictionary_bytes_column] = value }
|
|
116
136
|
if target == "postgres"
|
|
117
137
|
opts.on("--extension-schema NAME", "extension schema (default: multi_compress)") { |value| options[:extension_schema] = value }
|
|
118
138
|
end
|
|
@@ -145,6 +165,275 @@ module MultiCompress
|
|
|
145
165
|
end
|
|
146
166
|
end
|
|
147
167
|
|
|
168
|
+
class RegistryCommand
|
|
169
|
+
TARGETS = %w[postgres mysql].freeze
|
|
170
|
+
IDENTIFIER = /\A[A-Za-z_][A-Za-z0-9_]*\z/.freeze
|
|
171
|
+
|
|
172
|
+
def run(argv)
|
|
173
|
+
target = argv.shift
|
|
174
|
+
raise OptionParser::MissingArgument, "TARGET must be postgres or mysql" if target.nil?
|
|
175
|
+
raise OptionParser::InvalidArgument, "TARGET must be postgres or mysql" unless TARGETS.include?(target)
|
|
176
|
+
|
|
177
|
+
options = { output: nil, extension_schema: "multi_compress", table: "mcdb_dictionary_versions", heads_table: "mcdb_dictionary_heads" }
|
|
178
|
+
parser = OptionParser.new do |opts|
|
|
179
|
+
opts.banner = "Usage: multi_compress db registry #{target} [OPTIONS]"
|
|
180
|
+
if target == "postgres"
|
|
181
|
+
opts.on("--schema NAME", "application schema") { |value| options[:schema] = value }
|
|
182
|
+
opts.on("--owner ROLE", "NOLOGIN owner for append-only dictionary versions") { |value| options[:owner] = value }
|
|
183
|
+
opts.on("--migration-role ROLE", "role allowed to insert dictionary versions") { |value| options[:migration_role] = value }
|
|
184
|
+
opts.on("--extension-schema NAME", "extension schema (default: multi_compress)") { |value| options[:extension_schema] = value }
|
|
185
|
+
else
|
|
186
|
+
opts.on("--database NAME", "application database") { |value| options[:schema] = value }
|
|
187
|
+
end
|
|
188
|
+
opts.on("--table NAME", "versions table (default: mcdb_dictionary_versions)") { |value| options[:table] = value }
|
|
189
|
+
opts.on("--heads-table NAME", "heads table (default: mcdb_dictionary_heads)") { |value| options[:heads_table] = value }
|
|
190
|
+
opts.on("--payload-table NAME", "MCDB2 source table in this schema/database") { |value| options[:payload_table] = value }
|
|
191
|
+
opts.on("--payload-column NAME", "MCDB2 compressed envelope column on --payload-table") { |value| options[:payload_column] = value }
|
|
192
|
+
opts.on("--payload-dictionary-id-column NAME", "MCDB2 dictionary FK column on --payload-table") { |value| options[:payload_dictionary_id_column] = value }
|
|
193
|
+
opts.on("-o", "--output PATH", "write SQL to PATH instead of stdout") { |value| options[:output] = value }
|
|
194
|
+
opts.on("-h", "--help", "show this help") { puts opts; return 0 }
|
|
195
|
+
end
|
|
196
|
+
parser.parse!(argv)
|
|
197
|
+
raise OptionParser::InvalidOption, argv.join(" ") unless argv.empty?
|
|
198
|
+
sql = DictionaryRegistry.new(target, options).to_sql
|
|
199
|
+
if options[:output]
|
|
200
|
+
ViewCommand.new.send(:write_text_atomically, options[:output], sql)
|
|
201
|
+
puts File.expand_path(options[:output])
|
|
202
|
+
else
|
|
203
|
+
$stdout.write(sql)
|
|
204
|
+
end
|
|
205
|
+
0
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
class DictionaryRegistry
|
|
210
|
+
IDENTIFIER = /\A[A-Za-z_][A-Za-z0-9_]*\z/.freeze
|
|
211
|
+
|
|
212
|
+
def initialize(target, options)
|
|
213
|
+
@target = target
|
|
214
|
+
@schema = identifier!(options.fetch(:schema), target == "postgres" ? "--schema" : "--database")
|
|
215
|
+
@table = identifier!(options.fetch(:table), "--table")
|
|
216
|
+
@heads_table = identifier!(options.fetch(:heads_table), "--heads-table")
|
|
217
|
+
@owner = options[:owner] && identifier!(options[:owner], "--owner")
|
|
218
|
+
@migration_role = options[:migration_role] && identifier!(options[:migration_role], "--migration-role")
|
|
219
|
+
@extension_schema = identifier!(options.fetch(:extension_schema, "multi_compress"), "--extension-schema")
|
|
220
|
+
@payload_table = options[:payload_table] && identifier!(options[:payload_table], "--payload-table")
|
|
221
|
+
@payload_column = options[:payload_column] && identifier!(options[:payload_column], "--payload-column")
|
|
222
|
+
@payload_dictionary_id_column = options[:payload_dictionary_id_column] && identifier!(options[:payload_dictionary_id_column], "--payload-dictionary-id-column")
|
|
223
|
+
payload_options = [@payload_table, @payload_column, @payload_dictionary_id_column]
|
|
224
|
+
unless payload_options.all? || payload_options.none?
|
|
225
|
+
raise Error, "--payload-table, --payload-column and --payload-dictionary-id-column must be used together"
|
|
226
|
+
end
|
|
227
|
+
if @target == "postgres" && (!@owner || !@migration_role)
|
|
228
|
+
raise Error, "--owner and --migration-role are required for postgres registry DDL"
|
|
229
|
+
end
|
|
230
|
+
rescue KeyError
|
|
231
|
+
raise Error, @target == "postgres" ? "--schema, --owner and --migration-role are required" : "--database is required"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def to_sql
|
|
235
|
+
@target == "postgres" ? postgres_sql : mysql_sql
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
private
|
|
239
|
+
|
|
240
|
+
def postgres_sql
|
|
241
|
+
q = method(:pg_quote)
|
|
242
|
+
schema = q.call(@schema)
|
|
243
|
+
table = q.call(@table)
|
|
244
|
+
heads = q.call(@heads_table)
|
|
245
|
+
ext = q.call(@extension_schema)
|
|
246
|
+
<<~SQL
|
|
247
|
+
-- Generated by multi_compress #{MultiCompress::VERSION}. Apply as a privileged migration/DBA role.
|
|
248
|
+
-- The owner role must be a pre-existing NOLOGIN role. Dictionary bytes are append-only application data.
|
|
249
|
+
CREATE TABLE #{schema}.#{table} (
|
|
250
|
+
id bigint PRIMARY KEY CHECK (id > 0),
|
|
251
|
+
family text NOT NULL,
|
|
252
|
+
zstd_dict_id bigint NOT NULL CHECK (zstd_dict_id > 0),
|
|
253
|
+
sha256 bytea NOT NULL UNIQUE CHECK (octet_length(sha256) = 32),
|
|
254
|
+
bytes bytea NOT NULL CHECK (octet_length(bytes) BETWEEN 1 AND #{MAX_DICTIONARY_BYTES}),
|
|
255
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
256
|
+
);
|
|
257
|
+
ALTER TABLE #{schema}.#{table} OWNER TO #{q.call(@owner)};
|
|
258
|
+
-- PostgreSQL RI triggers execute the FK lookup under the relation owner.
|
|
259
|
+
-- A NOLOGIN owner therefore still needs schema USAGE for the payload FK.
|
|
260
|
+
GRANT USAGE ON SCHEMA #{schema} TO #{q.call(@owner)};
|
|
261
|
+
|
|
262
|
+
CREATE TABLE #{schema}.#{heads} (
|
|
263
|
+
family text PRIMARY KEY,
|
|
264
|
+
dictionary_id bigint NOT NULL REFERENCES #{schema}.#{table}(id) ON DELETE RESTRICT
|
|
265
|
+
);
|
|
266
|
+
ALTER TABLE #{schema}.#{heads} OWNER TO #{q.call(@owner)};
|
|
267
|
+
|
|
268
|
+
CREATE FUNCTION #{schema}.mcdb_dictionary_version_validate() RETURNS trigger
|
|
269
|
+
LANGUAGE plpgsql AS $$
|
|
270
|
+
BEGIN
|
|
271
|
+
IF NEW.id <= 0 OR NEW.zstd_dict_id <= 0 OR octet_length(NEW.sha256) <> 32 THEN
|
|
272
|
+
RAISE EXCEPTION 'invalid MCDB2 dictionary metadata';
|
|
273
|
+
END IF;
|
|
274
|
+
IF NEW.sha256 IS DISTINCT FROM #{ext}.multi_compress_db_dictionary_sha256(NEW.bytes) THEN
|
|
275
|
+
RAISE EXCEPTION 'MCDB2 dictionary sha256 does not match bytes';
|
|
276
|
+
END IF;
|
|
277
|
+
IF NEW.zstd_dict_id IS DISTINCT FROM #{ext}.multi_compress_db_dictionary_zstd_id(NEW.bytes) THEN
|
|
278
|
+
RAISE EXCEPTION 'MCDB2 zstd dictionary id does not match bytes';
|
|
279
|
+
END IF;
|
|
280
|
+
RETURN NEW;
|
|
281
|
+
END $$;
|
|
282
|
+
ALTER FUNCTION #{schema}.mcdb_dictionary_version_validate() OWNER TO #{q.call(@owner)};
|
|
283
|
+
|
|
284
|
+
CREATE FUNCTION #{schema}.mcdb_dictionary_version_frozen() RETURNS trigger
|
|
285
|
+
LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'MCDB dictionary versions are append-only'; END $$;
|
|
286
|
+
ALTER FUNCTION #{schema}.mcdb_dictionary_version_frozen() OWNER TO #{q.call(@owner)};
|
|
287
|
+
|
|
288
|
+
CREATE TRIGGER mcdb_dictionary_version_validate
|
|
289
|
+
BEFORE INSERT ON #{schema}.#{table}
|
|
290
|
+
FOR EACH ROW EXECUTE FUNCTION #{schema}.mcdb_dictionary_version_validate();
|
|
291
|
+
CREATE TRIGGER mcdb_dictionary_version_frozen
|
|
292
|
+
BEFORE UPDATE OR DELETE ON #{schema}.#{table}
|
|
293
|
+
FOR EACH ROW EXECUTE FUNCTION #{schema}.mcdb_dictionary_version_frozen();
|
|
294
|
+
|
|
295
|
+
REVOKE ALL ON #{schema}.#{table}, #{schema}.#{heads} FROM PUBLIC;
|
|
296
|
+
GRANT SELECT, INSERT ON #{schema}.#{table} TO #{q.call(@migration_role)};
|
|
297
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON #{schema}.#{heads} TO #{q.call(@migration_role)};
|
|
298
|
+
#{postgres_payload_consistency_sql}
|
|
299
|
+
SQL
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def mysql_sql
|
|
303
|
+
schema = mysql_quote(@schema)
|
|
304
|
+
table = mysql_quote(@table)
|
|
305
|
+
heads = mysql_quote(@heads_table)
|
|
306
|
+
<<~SQL
|
|
307
|
+
-- Generated by multi_compress #{MultiCompress::VERSION}. MySQL 5.7 registry DDL.
|
|
308
|
+
-- Apply this in the application database. Dictionary versions are append-only.
|
|
309
|
+
USE #{schema};
|
|
310
|
+
CREATE TABLE #{table} (
|
|
311
|
+
id BIGINT UNSIGNED NOT NULL,
|
|
312
|
+
family VARCHAR(191) NOT NULL,
|
|
313
|
+
zstd_dict_id BIGINT UNSIGNED NOT NULL,
|
|
314
|
+
sha256 BINARY(32) NOT NULL,
|
|
315
|
+
bytes LONGBLOB NOT NULL,
|
|
316
|
+
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
|
317
|
+
PRIMARY KEY (id),
|
|
318
|
+
UNIQUE KEY mcdb_dictionary_versions_sha256 (sha256)
|
|
319
|
+
) ENGINE=InnoDB;
|
|
320
|
+
CREATE TABLE #{heads} (
|
|
321
|
+
family VARCHAR(191) NOT NULL,
|
|
322
|
+
dictionary_id BIGINT UNSIGNED NOT NULL,
|
|
323
|
+
PRIMARY KEY (family),
|
|
324
|
+
CONSTRAINT mcdb_dictionary_heads_dictionary_fk
|
|
325
|
+
FOREIGN KEY (dictionary_id) REFERENCES #{table}(id) ON DELETE RESTRICT
|
|
326
|
+
) ENGINE=InnoDB;
|
|
327
|
+
|
|
328
|
+
DELIMITER //
|
|
329
|
+
CREATE TRIGGER #{mysql_quote("#{@table}_validate")}
|
|
330
|
+
BEFORE INSERT ON #{table} FOR EACH ROW
|
|
331
|
+
BEGIN
|
|
332
|
+
IF NEW.id = 0 OR NEW.id > 9223372036854775807 OR NEW.zstd_dict_id = 0 OR OCTET_LENGTH(NEW.sha256) <> 32 OR
|
|
333
|
+
OCTET_LENGTH(NEW.bytes) = 0 OR OCTET_LENGTH(NEW.bytes) > #{MAX_DICTIONARY_BYTES} OR
|
|
334
|
+
NOT (NEW.sha256 <=> multi_compress_db_dictionary_sha256(NEW.bytes)) OR
|
|
335
|
+
NOT (NEW.zstd_dict_id <=> multi_compress_db_dictionary_zstd_id(NEW.bytes)) THEN
|
|
336
|
+
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'invalid MCDB2 dictionary metadata';
|
|
337
|
+
END IF;
|
|
338
|
+
END//
|
|
339
|
+
CREATE TRIGGER #{mysql_quote("#{@table}_frozen_update")}
|
|
340
|
+
BEFORE UPDATE ON #{table} FOR EACH ROW
|
|
341
|
+
BEGIN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'MCDB dictionary versions are append-only'; END//
|
|
342
|
+
CREATE TRIGGER #{mysql_quote("#{@table}_frozen_delete")}
|
|
343
|
+
BEFORE DELETE ON #{table} FOR EACH ROW
|
|
344
|
+
BEGIN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'MCDB dictionary versions are append-only'; END//
|
|
345
|
+
#{mysql_payload_consistency_sql}
|
|
346
|
+
DELIMITER ;
|
|
347
|
+
SQL
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def payload_consistency?
|
|
351
|
+
!@payload_table.nil?
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def payload_consistency_token
|
|
355
|
+
Digest::SHA256.hexdigest([@schema, @payload_table, @payload_column, @payload_dictionary_id_column].join("\0"))[0, 12]
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def postgres_payload_consistency_sql
|
|
359
|
+
return "" unless payload_consistency?
|
|
360
|
+
|
|
361
|
+
q = method(:pg_quote)
|
|
362
|
+
token = payload_consistency_token
|
|
363
|
+
function_name = "mcdb_payload_ref_#{token}"
|
|
364
|
+
trigger_name = "mcdb_payload_ref_#{token}_check"
|
|
365
|
+
<<~SQL
|
|
366
|
+
|
|
367
|
+
ALTER TABLE #{q.call(@schema)}.#{q.call(@payload_table)}
|
|
368
|
+
ADD CONSTRAINT #{q.call("mcdb_payload_dictionary_#{token}_fk")}
|
|
369
|
+
FOREIGN KEY (#{q.call(@payload_dictionary_id_column)})
|
|
370
|
+
REFERENCES #{q.call(@schema)}.#{q.call(@table)}(id) ON DELETE RESTRICT;
|
|
371
|
+
|
|
372
|
+
CREATE FUNCTION #{q.call(@schema)}.#{q.call(function_name)}() RETURNS trigger
|
|
373
|
+
LANGUAGE plpgsql AS $$
|
|
374
|
+
BEGIN
|
|
375
|
+
IF NEW.#{q.call(@payload_column)} IS NULL OR NEW.#{q.call(@payload_dictionary_id_column)} IS NULL OR
|
|
376
|
+
#{q.call(@extension_schema)}.multi_compress_db_dictionary_ref(NEW.#{q.call(@payload_column)}) IS DISTINCT FROM NEW.#{q.call(@payload_dictionary_id_column)} THEN
|
|
377
|
+
RAISE EXCEPTION 'MCDB2 payload dictionary reference must match its dictionary FK';
|
|
378
|
+
END IF;
|
|
379
|
+
RETURN NEW;
|
|
380
|
+
END $$;
|
|
381
|
+
ALTER FUNCTION #{q.call(@schema)}.#{q.call(function_name)}() OWNER TO #{q.call(@owner)};
|
|
382
|
+
CREATE TRIGGER #{q.call(trigger_name)}
|
|
383
|
+
BEFORE INSERT OR UPDATE OF #{q.call(@payload_column)}, #{q.call(@payload_dictionary_id_column)}
|
|
384
|
+
ON #{q.call(@schema)}.#{q.call(@payload_table)}
|
|
385
|
+
FOR EACH ROW EXECUTE FUNCTION #{q.call(@schema)}.#{q.call(function_name)}();
|
|
386
|
+
SQL
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def mysql_payload_consistency_sql
|
|
390
|
+
return "" unless payload_consistency?
|
|
391
|
+
|
|
392
|
+
token = payload_consistency_token
|
|
393
|
+
insert_trigger = mysql_quote("mcdb_payload_ref_#{token}_insert")
|
|
394
|
+
update_trigger = mysql_quote("mcdb_payload_ref_#{token}_update")
|
|
395
|
+
payload_table = mysql_quote(@payload_table)
|
|
396
|
+
payload_column = mysql_quote(@payload_column)
|
|
397
|
+
payload_ref_column = mysql_quote(@payload_dictionary_id_column)
|
|
398
|
+
<<~SQL
|
|
399
|
+
|
|
400
|
+
ALTER TABLE #{payload_table}
|
|
401
|
+
ADD CONSTRAINT #{mysql_quote("mcdb_payload_dictionary_#{token}_fk")}
|
|
402
|
+
FOREIGN KEY (#{payload_ref_column}) REFERENCES #{mysql_quote(@table)}(id) ON DELETE RESTRICT;
|
|
403
|
+
CREATE TRIGGER #{insert_trigger}
|
|
404
|
+
BEFORE INSERT ON #{payload_table} FOR EACH ROW
|
|
405
|
+
BEGIN
|
|
406
|
+
IF NEW.#{payload_column} IS NULL OR NEW.#{payload_ref_column} IS NULL OR
|
|
407
|
+
NOT (multi_compress_db_dictionary_ref(NEW.#{payload_column}) <=> NEW.#{payload_ref_column}) THEN
|
|
408
|
+
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'MCDB2 payload dictionary reference must match its dictionary FK';
|
|
409
|
+
END IF;
|
|
410
|
+
END//
|
|
411
|
+
CREATE TRIGGER #{update_trigger}
|
|
412
|
+
BEFORE UPDATE ON #{payload_table} FOR EACH ROW
|
|
413
|
+
BEGIN
|
|
414
|
+
IF NEW.#{payload_column} IS NULL OR NEW.#{payload_ref_column} IS NULL OR
|
|
415
|
+
NOT (multi_compress_db_dictionary_ref(NEW.#{payload_column}) <=> NEW.#{payload_ref_column}) THEN
|
|
416
|
+
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'MCDB2 payload dictionary reference must match its dictionary FK';
|
|
417
|
+
END IF;
|
|
418
|
+
END//
|
|
419
|
+
SQL
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def identifier!(value, option)
|
|
423
|
+
string = value.to_s
|
|
424
|
+
raise Error, "#{option} must use letters, digits and underscores and cannot start with a digit" unless IDENTIFIER.match?(string)
|
|
425
|
+
string
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def pg_quote(identifier)
|
|
429
|
+
%Q("#{identifier}")
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def mysql_quote(identifier)
|
|
433
|
+
"`#{identifier}`"
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
148
437
|
class ReadableView
|
|
149
438
|
IDENTIFIER = /\A[A-Za-z_][A-Za-z0-9_]*\z/.freeze
|
|
150
439
|
|
|
@@ -156,6 +445,14 @@ module MultiCompress
|
|
|
156
445
|
@columns = Array(options.fetch(:columns)).reject(&:empty?).map { |value| identifier!(value, "--columns") }
|
|
157
446
|
@decoded_name = identifier!(options.fetch(:as), "--as")
|
|
158
447
|
@extension_schema = identifier!(options.fetch(:extension_schema, "multi_compress"), "--extension-schema")
|
|
448
|
+
@dictionary_table = options[:dictionary_table] && qualified_identifier!(options[:dictionary_table], "--dictionary-table")
|
|
449
|
+
@dictionary_id_column = options[:dictionary_id_column] && identifier!(options[:dictionary_id_column], "--dictionary-id-column")
|
|
450
|
+
@registry_id_column = identifier!(options.fetch(:registry_id_column, "id"), "--registry-id-column")
|
|
451
|
+
@dictionary_sha256_column = identifier!(options.fetch(:dictionary_sha256_column, "sha256"), "--dictionary-sha256-column")
|
|
452
|
+
@dictionary_bytes_column = identifier!(options.fetch(:dictionary_bytes_column, "bytes"), "--dictionary-bytes-column")
|
|
453
|
+
if !!@dictionary_table != !!@dictionary_id_column
|
|
454
|
+
raise Error, "--dictionary-table and --dictionary-id-column must be used together for MCDB2"
|
|
455
|
+
end
|
|
159
456
|
raise Error, "--columns must contain at least one plain column" if @columns.empty?
|
|
160
457
|
raise Error, "--columns must not contain duplicate names" unless @columns.uniq.length == @columns.length
|
|
161
458
|
raise Error, "--as must not duplicate a selected column" if @columns.include?(@decoded_name)
|
|
@@ -175,32 +472,71 @@ module MultiCompress
|
|
|
175
472
|
|
|
176
473
|
def postgres_sql
|
|
177
474
|
selections = @columns.map { |column| " source.#{pg_quote(column)}" }
|
|
178
|
-
selections << " #{
|
|
475
|
+
selections << " #{postgres_decoder_sql} AS #{pg_quote(@decoded_name)}"
|
|
179
476
|
|
|
180
477
|
<<~SQL
|
|
181
478
|
-- Generated by multi_compress #{MultiCompress::VERSION}. Keep this view in your schema migrations.
|
|
182
|
-
-- Query it in DBeaver; do not expose the compressed column
|
|
479
|
+
-- Query it in DBeaver; do not expose the compressed column or dictionary registry directly.
|
|
183
480
|
CREATE OR REPLACE VIEW #{pg_qualified(@view)} AS
|
|
184
481
|
SELECT
|
|
185
482
|
#{selections.join(",\n")}
|
|
186
|
-
FROM #{pg_qualified(@table)} AS source;
|
|
483
|
+
FROM #{pg_qualified(@table)} AS source#{postgres_dictionary_join};
|
|
187
484
|
SQL
|
|
188
485
|
end
|
|
189
486
|
|
|
190
487
|
def mysql_sql
|
|
191
488
|
selections = @columns.map { |column| " source.#{mysql_quote(column)}" }
|
|
192
|
-
selections << " CONVERT(
|
|
489
|
+
selections << " CONVERT(#{mysql_decoder_sql} USING utf8mb4) AS #{mysql_quote(@decoded_name)}"
|
|
490
|
+
algorithm = dictionary? ? " ALGORITHM=MERGE SQL SECURITY DEFINER" : ""
|
|
193
491
|
|
|
194
492
|
<<~SQL
|
|
195
493
|
-- Generated by multi_compress #{MultiCompress::VERSION}. Keep this view in your schema migrations.
|
|
196
|
-
-- Query it in DBeaver; do not expose the compressed column
|
|
197
|
-
|
|
494
|
+
-- Query it in DBeaver; do not expose the compressed column or dictionary registry directly.
|
|
495
|
+
-- MCDB2 requires this view to remain MERGE-able: no DISTINCT/GROUP BY/UNION/aggregate/subquery/LIMIT.
|
|
496
|
+
CREATE OR REPLACE#{algorithm} VIEW #{mysql_qualified(@view)} AS
|
|
198
497
|
SELECT
|
|
199
498
|
#{selections.join(",\n")}
|
|
200
|
-
FROM #{mysql_qualified(@table)} AS source;
|
|
499
|
+
FROM #{mysql_qualified(@table)} AS source#{mysql_dictionary_join};
|
|
201
500
|
SQL
|
|
202
501
|
end
|
|
203
502
|
|
|
503
|
+
def dictionary?
|
|
504
|
+
!@dictionary_table.nil?
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def postgres_decoder_sql
|
|
508
|
+
return "#{pg_quote(@extension_schema)}.multi_compress_db_decompress(source.#{pg_quote(@column)})" unless dictionary?
|
|
509
|
+
|
|
510
|
+
"#{pg_quote(@extension_schema)}.multi_compress_db_decompress_dict(" \
|
|
511
|
+
"source.#{pg_quote(@column)}, source.#{pg_quote(@dictionary_id_column)}, " \
|
|
512
|
+
"dictionary.#{pg_quote(@dictionary_sha256_column)}, dictionary.#{pg_quote(@dictionary_bytes_column)})"
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def mysql_decoder_sql
|
|
516
|
+
return "multi_compress_db_decompress(source.#{mysql_quote(@column)})" unless dictionary?
|
|
517
|
+
|
|
518
|
+
"multi_compress_db_decompress_dict(" \
|
|
519
|
+
"source.#{mysql_quote(@column)}, source.#{mysql_quote(@dictionary_id_column)}, " \
|
|
520
|
+
"dictionary.#{mysql_quote(@dictionary_sha256_column)}, dictionary.#{mysql_quote(@dictionary_bytes_column)})"
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
def postgres_dictionary_join
|
|
524
|
+
return "" unless dictionary?
|
|
525
|
+
|
|
526
|
+
"\nJOIN #{pg_qualified(@dictionary_table)} AS dictionary ON dictionary.#{pg_quote(@registry_id_column)} = source.#{pg_quote(@dictionary_id_column)}"
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def mysql_dictionary_join
|
|
530
|
+
return "" unless dictionary?
|
|
531
|
+
|
|
532
|
+
# MySQL 5.7 may otherwise start from the tiny registry and then examine a
|
|
533
|
+
# broad source set. The generated view always places source on the left, so
|
|
534
|
+
# STRAIGHT_JOIN preserves source-first predicate pushdown before dictionary
|
|
535
|
+
# lookup. An outer ORDER BY can still legitimately report filesort; that is
|
|
536
|
+
# distinct from a materialized view (<derived...> in EXPLAIN).
|
|
537
|
+
"\nSTRAIGHT_JOIN #{mysql_qualified(@dictionary_table)} AS dictionary ON dictionary.#{mysql_quote(@registry_id_column)} = source.#{mysql_quote(@dictionary_id_column)}"
|
|
538
|
+
end
|
|
539
|
+
|
|
204
540
|
def qualified_identifier!(value, option)
|
|
205
541
|
pieces = value.to_s.split(".", -1)
|
|
206
542
|
unless pieces.length.between?(1, 2) && pieces.all? { |piece| IDENTIFIER.match?(piece) }
|
|
@@ -209,6 +545,78 @@ module MultiCompress
|
|
|
209
545
|
pieces
|
|
210
546
|
end
|
|
211
547
|
|
|
548
|
+
def payload_consistency?
|
|
549
|
+
!@payload_table.nil?
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def payload_consistency_token
|
|
553
|
+
Digest::SHA256.hexdigest([@schema, @payload_table, @payload_column, @payload_dictionary_id_column].join("\0"))[0, 12]
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
def postgres_payload_consistency_sql
|
|
557
|
+
return "" unless payload_consistency?
|
|
558
|
+
|
|
559
|
+
q = method(:pg_quote)
|
|
560
|
+
token = payload_consistency_token
|
|
561
|
+
function_name = "mcdb_payload_ref_#{token}"
|
|
562
|
+
trigger_name = "mcdb_payload_ref_#{token}_check"
|
|
563
|
+
<<~SQL
|
|
564
|
+
|
|
565
|
+
ALTER TABLE #{q.call(@schema)}.#{q.call(@payload_table)}
|
|
566
|
+
ADD CONSTRAINT #{q.call("mcdb_payload_dictionary_#{token}_fk")}
|
|
567
|
+
FOREIGN KEY (#{q.call(@payload_dictionary_id_column)})
|
|
568
|
+
REFERENCES #{q.call(@schema)}.#{q.call(@table)}(id) ON DELETE RESTRICT;
|
|
569
|
+
|
|
570
|
+
CREATE FUNCTION #{q.call(@schema)}.#{q.call(function_name)}() RETURNS trigger
|
|
571
|
+
LANGUAGE plpgsql AS $$
|
|
572
|
+
BEGIN
|
|
573
|
+
IF NEW.#{q.call(@payload_column)} IS NULL OR NEW.#{q.call(@payload_dictionary_id_column)} IS NULL OR
|
|
574
|
+
#{q.call(@extension_schema)}.multi_compress_db_dictionary_ref(NEW.#{q.call(@payload_column)}) IS DISTINCT FROM NEW.#{q.call(@payload_dictionary_id_column)} THEN
|
|
575
|
+
RAISE EXCEPTION 'MCDB2 payload dictionary reference must match its dictionary FK';
|
|
576
|
+
END IF;
|
|
577
|
+
RETURN NEW;
|
|
578
|
+
END $$;
|
|
579
|
+
ALTER FUNCTION #{q.call(@schema)}.#{q.call(function_name)}() OWNER TO #{q.call(@owner)};
|
|
580
|
+
CREATE TRIGGER #{q.call(trigger_name)}
|
|
581
|
+
BEFORE INSERT OR UPDATE OF #{q.call(@payload_column)}, #{q.call(@payload_dictionary_id_column)}
|
|
582
|
+
ON #{q.call(@schema)}.#{q.call(@payload_table)}
|
|
583
|
+
FOR EACH ROW EXECUTE FUNCTION #{q.call(@schema)}.#{q.call(function_name)}();
|
|
584
|
+
SQL
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def mysql_payload_consistency_sql
|
|
588
|
+
return "" unless payload_consistency?
|
|
589
|
+
|
|
590
|
+
token = payload_consistency_token
|
|
591
|
+
insert_trigger = mysql_quote("mcdb_payload_ref_#{token}_insert")
|
|
592
|
+
update_trigger = mysql_quote("mcdb_payload_ref_#{token}_update")
|
|
593
|
+
payload_table = mysql_quote(@payload_table)
|
|
594
|
+
payload_column = mysql_quote(@payload_column)
|
|
595
|
+
payload_ref_column = mysql_quote(@payload_dictionary_id_column)
|
|
596
|
+
<<~SQL
|
|
597
|
+
|
|
598
|
+
ALTER TABLE #{payload_table}
|
|
599
|
+
ADD CONSTRAINT #{mysql_quote("mcdb_payload_dictionary_#{token}_fk")}
|
|
600
|
+
FOREIGN KEY (#{payload_ref_column}) REFERENCES #{mysql_quote(@table)}(id) ON DELETE RESTRICT;
|
|
601
|
+
CREATE TRIGGER #{insert_trigger}
|
|
602
|
+
BEFORE INSERT ON #{payload_table} FOR EACH ROW
|
|
603
|
+
BEGIN
|
|
604
|
+
IF NEW.#{payload_column} IS NULL OR NEW.#{payload_ref_column} IS NULL OR
|
|
605
|
+
NOT (multi_compress_db_dictionary_ref(NEW.#{payload_column}) <=> NEW.#{payload_ref_column}) THEN
|
|
606
|
+
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'MCDB2 payload dictionary reference must match its dictionary FK';
|
|
607
|
+
END IF;
|
|
608
|
+
END//
|
|
609
|
+
CREATE TRIGGER #{update_trigger}
|
|
610
|
+
BEFORE UPDATE ON #{payload_table} FOR EACH ROW
|
|
611
|
+
BEGIN
|
|
612
|
+
IF NEW.#{payload_column} IS NULL OR NEW.#{payload_ref_column} IS NULL OR
|
|
613
|
+
NOT (multi_compress_db_dictionary_ref(NEW.#{payload_column}) <=> NEW.#{payload_ref_column}) THEN
|
|
614
|
+
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'MCDB2 payload dictionary reference must match its dictionary FK';
|
|
615
|
+
END IF;
|
|
616
|
+
END//
|
|
617
|
+
SQL
|
|
618
|
+
end
|
|
619
|
+
|
|
212
620
|
def identifier!(value, option)
|
|
213
621
|
string = value.to_s
|
|
214
622
|
raise Error, "#{option} must use letters, digits and underscores and cannot start with a digit" unless IDENTIFIER.match?(string)
|
|
@@ -299,6 +707,8 @@ module MultiCompress
|
|
|
299
707
|
postgres_extension
|
|
300
708
|
ext/multi_compress/vendor/zstd
|
|
301
709
|
docs/database-envelope-v1.md
|
|
710
|
+
docs/database-envelope-v2.md
|
|
711
|
+
docs/rfcs/0002-mcdb2-dictionaries.md
|
|
302
712
|
LICENSE.txt
|
|
303
713
|
THIRD_PARTY_NOTICES.md
|
|
304
714
|
],
|
|
@@ -310,6 +720,8 @@ module MultiCompress
|
|
|
310
720
|
mysql_udf
|
|
311
721
|
ext/multi_compress/vendor/zstd
|
|
312
722
|
docs/database-envelope-v1.md
|
|
723
|
+
docs/database-envelope-v2.md
|
|
724
|
+
docs/rfcs/0002-mcdb2-dictionaries.md
|
|
313
725
|
LICENSE.txt
|
|
314
726
|
THIRD_PARTY_NOTICES.md
|
|
315
727
|
],
|
data/mysql_udf/README.md
CHANGED
|
@@ -93,3 +93,48 @@ invokes it.
|
|
|
93
93
|
`make verify` catches accidental corruption after extraction; it does not
|
|
94
94
|
authenticate an archive. Use an out-of-band SHA-256, minisign/GPG signature, or
|
|
95
95
|
signed release provenance for a real trust chain.
|
|
96
|
+
|
|
97
|
+
## MCDB2 dictionary-backed columns
|
|
98
|
+
|
|
99
|
+
MCDB2 is opt-in for one homogeneous column with many similar small JSON/text
|
|
100
|
+
values. Upgrade the UDF library first; the controlled `make upgrade` flow keeps
|
|
101
|
+
all MCDB1 and MCDB2 functions registered.
|
|
102
|
+
|
|
103
|
+
Dictionary bytes are application data. Generate the append-only registry in the
|
|
104
|
+
application database, register immutable versions, and retain all referenced
|
|
105
|
+
versions in backups and replicas:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
bundle exec multi_compress db registry mysql --database app \
|
|
109
|
+
--output db/mcdb_dictionary_registry.sql
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
For a new MCDB2 column add `payload_dictionary_id BIGINT UNSIGNED NOT NULL` with
|
|
113
|
+
a foreign key to `mcdb_dictionary_versions(id)`, then generate the view:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
bundle exec multi_compress db view mysql \
|
|
117
|
+
--table app.events \
|
|
118
|
+
--column payload_compressed \
|
|
119
|
+
--dictionary-table app.mcdb_dictionary_versions \
|
|
120
|
+
--dictionary-id-column payload_dictionary_id \
|
|
121
|
+
--view admin.events_readable \
|
|
122
|
+
--columns id,created_at,status \
|
|
123
|
+
--as payload \
|
|
124
|
+
--output db/views/events_readable.sql
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The generated MCDB2 view requests `ALGORITHM=MERGE SQL SECURITY DEFINER` and uses
|
|
128
|
+
a source-first `STRAIGHT_JOIN` to the registry. That keeps an indexed outer source
|
|
129
|
+
predicate from starting at the registry. It passes payload, registry id, SHA-256
|
|
130
|
+
and dictionary bytes to the UDF. Keep the definition free of `DISTINCT`, aggregates,
|
|
131
|
+
`GROUP BY`, `UNION`, subqueries and inner `LIMIT`; validate real queries with
|
|
132
|
+
`EXPLAIN`: source must be a selective `range`/`ref` access and no `<derived>` row
|
|
133
|
+
may appear. MySQL 5.7 can still report `Using temporary; Using filesort` for an
|
|
134
|
+
outer `ORDER BY` over the joined projection; that is sorting, not view
|
|
135
|
+
materialization. A stable definer account must remain part of backup/restore
|
|
136
|
+
procedures.
|
|
137
|
+
|
|
138
|
+
Details are in [`docs/database-envelope-v2.md`](../docs/database-envelope-v2.md).
|
|
139
|
+
|
|
140
|
+
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/mysql_udf/sql/examples.sql
CHANGED
|
@@ -9,3 +9,8 @@ FROM events
|
|
|
9
9
|
WHERE multi_compress_db_is_valid(payload_compressed) = 0;
|
|
10
10
|
|
|
11
11
|
SELECT multi_compress_db_version();
|
|
12
|
+
|
|
13
|
+
-- MCDB2: use the generated ALGORITHM=MERGE readable view. Avoid filtering on
|
|
14
|
+
-- decoded payload text; filter/order by indexed source columns before LIMIT.
|
|
15
|
+
--
|
|
16
|
+
-- SELECT id, payload FROM admin.events_readable WHERE id >= 100 ORDER BY id LIMIT 100;
|
data/mysql_udf/sql/install.sql
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
-- Install the MultiCompress MCDB1 reader UDFs (MySQL 5.7).
|
|
1
|
+
-- Install the MultiCompress MCDB1/MCDB2 reader UDFs (MySQL 5.7).
|
|
2
2
|
|
|
3
3
|
CREATE FUNCTION multi_compress_db_version
|
|
4
4
|
RETURNS STRING
|
|
@@ -12,4 +12,28 @@ CREATE FUNCTION multi_compress_db_decompress
|
|
|
12
12
|
RETURNS STRING
|
|
13
13
|
SONAME 'multi_compress_mysql.so';
|
|
14
14
|
|
|
15
|
+
CREATE FUNCTION multi_compress_db_original_size
|
|
16
|
+
RETURNS INTEGER
|
|
17
|
+
SONAME 'multi_compress_mysql.so';
|
|
18
|
+
|
|
19
|
+
CREATE FUNCTION multi_compress_db_dictionary_ref
|
|
20
|
+
RETURNS INTEGER
|
|
21
|
+
SONAME 'multi_compress_mysql.so';
|
|
22
|
+
|
|
23
|
+
CREATE FUNCTION multi_compress_db_dictionary_zstd_id
|
|
24
|
+
RETURNS INTEGER
|
|
25
|
+
SONAME 'multi_compress_mysql.so';
|
|
26
|
+
|
|
27
|
+
CREATE FUNCTION multi_compress_db_dictionary_sha256
|
|
28
|
+
RETURNS STRING
|
|
29
|
+
SONAME 'multi_compress_mysql.so';
|
|
30
|
+
|
|
31
|
+
CREATE FUNCTION multi_compress_db_is_valid_dict
|
|
32
|
+
RETURNS INTEGER
|
|
33
|
+
SONAME 'multi_compress_mysql.so';
|
|
34
|
+
|
|
35
|
+
CREATE FUNCTION multi_compress_db_decompress_dict
|
|
36
|
+
RETURNS STRING
|
|
37
|
+
SONAME 'multi_compress_mysql.so';
|
|
38
|
+
|
|
15
39
|
SELECT multi_compress_db_version();
|
data/mysql_udf/sql/uninstall.sql
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
--
|
|
1
|
+
-- Run only after verifying no generated readable view depends on these UDFs.
|
|
2
|
+
DROP FUNCTION IF EXISTS multi_compress_db_decompress_dict;
|
|
3
|
+
DROP FUNCTION IF EXISTS multi_compress_db_is_valid_dict;
|
|
4
|
+
DROP FUNCTION IF EXISTS multi_compress_db_dictionary_sha256;
|
|
5
|
+
DROP FUNCTION IF EXISTS multi_compress_db_dictionary_zstd_id;
|
|
6
|
+
DROP FUNCTION IF EXISTS multi_compress_db_original_size;
|
|
7
|
+
DROP FUNCTION IF EXISTS multi_compress_db_dictionary_ref;
|
|
2
8
|
DROP FUNCTION IF EXISTS multi_compress_db_decompress;
|
|
3
9
|
DROP FUNCTION IF EXISTS multi_compress_db_is_valid;
|
|
4
10
|
DROP FUNCTION IF EXISTS multi_compress_db_version;
|
data/mysql_udf/sql/views.sql
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
|
-
--
|
|
2
|
-
|
|
1
|
+
-- MCDB1 view example. Generate production SQL with:
|
|
2
|
+
-- multi_compress db view mysql ...
|
|
3
3
|
CREATE OR REPLACE VIEW events_readable AS
|
|
4
4
|
SELECT
|
|
5
5
|
id,
|
|
6
|
-
CONVERT(multi_compress_db_decompress(payload_compressed) USING utf8mb4) AS payload
|
|
7
|
-
created_at
|
|
6
|
+
CONVERT(multi_compress_db_decompress(payload_compressed) USING utf8mb4) AS payload
|
|
8
7
|
FROM events;
|
|
8
|
+
|
|
9
|
+
-- MCDB2 dictionary-backed shape. The generated view uses ALGORITHM=MERGE and
|
|
10
|
+
-- a source-first STRAIGHT_JOIN to the append-only registry, so an indexed outer
|
|
11
|
+
-- WHERE predicate is pushed into the source scan in MySQL 5.7. An outer ORDER BY
|
|
12
|
+
-- may still use filesort; that is not view materialization.
|
|
13
|
+
--
|
|
14
|
+
-- CREATE ALGORITHM=MERGE SQL SECURITY DEFINER VIEW events_readable AS
|
|
15
|
+
-- SELECT e.id,
|
|
16
|
+
-- CONVERT(multi_compress_db_decompress_dict(
|
|
17
|
+
-- e.payload_compressed, e.payload_dictionary_id, d.sha256, d.bytes
|
|
18
|
+
-- ) USING utf8mb4) AS payload
|
|
19
|
+
-- FROM events e
|
|
20
|
+
-- STRAIGHT_JOIN mcdb_dictionary_versions d ON d.id = e.payload_dictionary_id;
|