multi_compress 0.5.0 → 0.6.1
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 +45 -0
- data/GET_STARTED.md +20 -1
- data/README.md +15 -2
- data/THIRD_PARTY_NOTICES.md +1 -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 +261 -76
- data/ext/multi_compress/vendor/.vendored +1 -1
- data/ext/multi_compress/vendor/brotli/c/common/version.h +2 -2
- data/ext/multi_compress/vendor/brotli/c/dec/decode.c +59 -30
- data/ext/multi_compress/vendor/brotli/c/include/brotli/decode.h +1 -3
- data/lib/multi_compress/active_record.rb +11 -5
- data/lib/multi_compress/codec.rb +164 -94
- data/lib/multi_compress/database/api.rb +42 -0
- data/lib/multi_compress/database/decoding.rb +42 -0
- data/lib/multi_compress/database/dictionary.rb +72 -0
- data/lib/multi_compress/database/encoding.rb +31 -0
- data/lib/multi_compress/database/header.rb +71 -0
- data/lib/multi_compress/database/operations.rb +73 -0
- data/lib/multi_compress/database/validation.rb +95 -0
- data/lib/multi_compress/database.rb +29 -95
- data/lib/multi_compress/db_deployment/bundle_archiver.rb +70 -0
- data/lib/multi_compress/db_deployment/bundle_metadata.rb +56 -0
- data/lib/multi_compress/db_deployment/bundle_stager.rb +62 -0
- data/lib/multi_compress/db_deployment/commands.rb +309 -0
- data/lib/multi_compress/db_deployment/deployment_bundle.rb +80 -0
- data/lib/multi_compress/db_deployment/deterministic_tar_writer.rb +76 -0
- data/lib/multi_compress/db_deployment/dictionary_registry.rb +244 -0
- data/lib/multi_compress/db_deployment/readable_view.rb +112 -0
- data/lib/multi_compress/db_deployment/readable_view_options.rb +51 -0
- data/lib/multi_compress/db_deployment/sql_identifiers.rb +45 -0
- data/lib/multi_compress/db_deployment.rb +13 -435
- data/lib/multi_compress/reader/buffer_operations.rb +57 -0
- data/lib/multi_compress/reader/read_operations.rb +67 -0
- data/lib/multi_compress/reader.rb +97 -0
- data/lib/multi_compress/stream_io.rb +13 -0
- data/lib/multi_compress/version.rb +1 -1
- data/lib/multi_compress/writer.rb +85 -0
- data/lib/multi_compress.rb +42 -333
- 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 +29 -6
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MultiCompress
|
|
4
|
+
class Writer
|
|
5
|
+
CHUNK_BUFFER_SIZE = 8192
|
|
6
|
+
|
|
7
|
+
def self.open(path_or_io, algo: nil, level: nil, dictionary: nil, &block)
|
|
8
|
+
io, algo, owned = StreamIO.resolve_io(path_or_io, algo, mode: "wb")
|
|
9
|
+
writer = new(io, algo: algo || :zstd, level: level, dictionary: dictionary)
|
|
10
|
+
writer.instance_variable_set(:@owned_io, owned)
|
|
11
|
+
|
|
12
|
+
return writer unless block
|
|
13
|
+
|
|
14
|
+
begin
|
|
15
|
+
yield writer
|
|
16
|
+
ensure
|
|
17
|
+
writer.close
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(io, algo: :zstd, level: nil, dictionary: nil)
|
|
22
|
+
@io = io
|
|
23
|
+
@deflater = Deflater.new(algo: algo, level: level, dictionary: dictionary)
|
|
24
|
+
@closed = false
|
|
25
|
+
@owned_io = false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def write(data)
|
|
29
|
+
ensure_open!
|
|
30
|
+
bytes = data.to_s
|
|
31
|
+
flush_compressed(@deflater.write(bytes))
|
|
32
|
+
bytes.bytesize
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def flush
|
|
36
|
+
ensure_open!
|
|
37
|
+
flush_compressed(@deflater.flush)
|
|
38
|
+
@io.flush if @io.respond_to?(:flush)
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def close
|
|
43
|
+
return if @closed
|
|
44
|
+
|
|
45
|
+
flush_compressed(@deflater.finish)
|
|
46
|
+
@deflater.close
|
|
47
|
+
@io.close if @owned_io && @io.respond_to?(:close)
|
|
48
|
+
@closed = true
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def closed?
|
|
53
|
+
@closed
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def <<(data)
|
|
57
|
+
write(data)
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def puts(*args)
|
|
62
|
+
args.flatten.each do |arg|
|
|
63
|
+
str = arg.to_s
|
|
64
|
+
write(str)
|
|
65
|
+
write("\n") unless str.end_with?("\n")
|
|
66
|
+
end
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def print(*args)
|
|
71
|
+
args.each { |arg| write(arg.to_s) }
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def ensure_open!
|
|
78
|
+
raise StreamError, "writer is closed" if @closed
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def flush_compressed(compressed)
|
|
82
|
+
@io.write(compressed) if compressed && !compressed.empty?
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/multi_compress.rb
CHANGED
|
@@ -5,11 +5,11 @@ require "multi_compress/version"
|
|
|
5
5
|
begin
|
|
6
6
|
require "multi_compress/multi_compress"
|
|
7
7
|
rescue LoadError
|
|
8
|
-
ext_dir = File.expand_path("
|
|
8
|
+
ext_dir = File.expand_path("multi_compress", __dir__)
|
|
9
9
|
|
|
10
10
|
so_path = %w[.bundle .so]
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
.map { |ext| File.join(ext_dir, "multi_compress#{ext}") }
|
|
12
|
+
.find { |path| File.exist?(path) }
|
|
13
13
|
|
|
14
14
|
raise LoadError, "Could not find compiled MultiCompress extension. Run: bundle exec rake compile" unless so_path
|
|
15
15
|
|
|
@@ -25,10 +25,10 @@ module MultiCompress
|
|
|
25
25
|
DEFAULT_STREAMING_MAX_OUTPUT_SIZE = 2 * 1024 * 1024 * 1024
|
|
26
26
|
|
|
27
27
|
EXTENSION_MAP = {
|
|
28
|
-
".zst"
|
|
28
|
+
".zst" => :zstd,
|
|
29
29
|
".zstd" => :zstd,
|
|
30
|
-
".lz4"
|
|
31
|
-
".br"
|
|
30
|
+
".lz4" => :lz4,
|
|
31
|
+
".br" => :brotli
|
|
32
32
|
}.freeze
|
|
33
33
|
|
|
34
34
|
private_constant :EXTENSION_MAP
|
|
@@ -91,361 +91,70 @@ module MultiCompress
|
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
unless private_method_defined?(:_c_decompress)
|
|
94
|
-
|
|
94
|
+
alias _c_decompress decompress
|
|
95
95
|
private :_c_decompress
|
|
96
96
|
end
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def self.zstd(data, level: nil)
|
|
100
|
-
compress(data, algo: :zstd, level: level)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def self.lz4(data, level: nil, format: nil)
|
|
104
|
-
if format
|
|
105
|
-
compress(data, algo: :lz4, level: level, format: format)
|
|
106
|
-
else
|
|
107
|
-
compress(data, algo: :lz4, level: level)
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def self.brotli(data, level: nil)
|
|
112
|
-
compress(data, algo: :brotli, level: level)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def self.decompress(data, **opts)
|
|
116
|
-
_c_decompress(data, **resolved_one_shot_options(opts))
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def self.zstd_decompress(data)
|
|
120
|
-
decompress(data, algo: :zstd)
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def self.lz4_decompress(data, format: nil)
|
|
124
|
-
if format
|
|
125
|
-
decompress(data, algo: :lz4, format: format)
|
|
126
|
-
else
|
|
127
|
-
decompress(data, algo: :lz4)
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def self.brotli_decompress(data)
|
|
132
|
-
decompress(data, algo: :brotli)
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def self.algo_from_ext(path)
|
|
136
|
-
EXTENSION_MAP[File.extname(path).downcase]
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def self.resolved_one_shot_options(opts)
|
|
140
|
-
return opts.merge(max_output_size: config.max_output_size) unless opts.key?(:max_output_size)
|
|
141
|
-
|
|
142
|
-
opts
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
private_class_method :resolved_one_shot_options
|
|
146
|
-
|
|
147
|
-
module InflaterDefaults
|
|
148
|
-
def initialize(*args, **opts)
|
|
149
|
-
resolved = opts.dup
|
|
150
|
-
resolved[:max_output_size] = MultiCompress.config.streaming_max_output_size unless resolved.key?(:max_output_size)
|
|
151
|
-
super(*args, **resolved)
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
Inflater.prepend(InflaterDefaults)
|
|
156
97
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def self.open(path_or_io, algo: nil, level: nil, dictionary: nil, &block)
|
|
161
|
-
io, algo, owned = resolve_io(path_or_io, algo, mode: "wb")
|
|
162
|
-
writer = new(io, algo: algo || :zstd, level: level, dictionary: dictionary)
|
|
163
|
-
writer.instance_variable_set(:@owned_io, owned)
|
|
164
|
-
|
|
165
|
-
return writer unless block
|
|
166
|
-
|
|
167
|
-
begin
|
|
168
|
-
yield writer
|
|
169
|
-
ensure
|
|
170
|
-
writer.close
|
|
171
|
-
end
|
|
98
|
+
def zstd(data, level: nil)
|
|
99
|
+
compress(data, algo: :zstd, level: level)
|
|
172
100
|
end
|
|
173
101
|
|
|
174
|
-
def
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
@closed = false
|
|
178
|
-
@owned_io = false
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def write(data)
|
|
182
|
-
ensure_open!
|
|
183
|
-
bytes = data.to_s
|
|
184
|
-
flush_compressed(@deflater.write(bytes))
|
|
185
|
-
bytes.bytesize
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
def flush
|
|
189
|
-
ensure_open!
|
|
190
|
-
flush_compressed(@deflater.flush)
|
|
191
|
-
@io.flush if @io.respond_to?(:flush)
|
|
192
|
-
self
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def close
|
|
196
|
-
return if @closed
|
|
197
|
-
|
|
198
|
-
flush_compressed(@deflater.finish)
|
|
199
|
-
@deflater.close
|
|
200
|
-
@io.close if @owned_io && @io.respond_to?(:close)
|
|
201
|
-
@closed = true
|
|
202
|
-
nil
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
def closed?
|
|
206
|
-
@closed
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
def <<(data)
|
|
210
|
-
write(data)
|
|
211
|
-
self
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
def puts(*args)
|
|
215
|
-
args.flatten.each do |arg|
|
|
216
|
-
str = arg.to_s
|
|
217
|
-
write(str)
|
|
218
|
-
write("\n") unless str.end_with?("\n")
|
|
219
|
-
end
|
|
220
|
-
nil
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
def print(*args)
|
|
224
|
-
args.each { |arg| write(arg.to_s) }
|
|
225
|
-
nil
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
private
|
|
229
|
-
|
|
230
|
-
def ensure_open!
|
|
231
|
-
raise StreamError, "writer is closed" if @closed
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
def flush_compressed(compressed)
|
|
235
|
-
@io.write(compressed) if compressed && !compressed.empty?
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
def self.resolve_io(path_or_io, algo, mode:)
|
|
239
|
-
if path_or_io.is_a?(String)
|
|
240
|
-
[File.open(path_or_io, mode), algo || MultiCompress.algo_from_ext(path_or_io), true]
|
|
102
|
+
def lz4(data, level: nil, format: nil)
|
|
103
|
+
if format
|
|
104
|
+
compress(data, algo: :lz4, level: level, format: format)
|
|
241
105
|
else
|
|
242
|
-
|
|
106
|
+
compress(data, algo: :lz4, level: level)
|
|
243
107
|
end
|
|
244
108
|
end
|
|
245
109
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
class Reader
|
|
250
|
-
CHUNK_SIZE = 8192
|
|
251
|
-
BUFFER_COMPACT_THRESHOLD = 64 * 1024
|
|
252
|
-
|
|
253
|
-
def self.open(path_or_io, algo: nil, dictionary: nil, **opts, &block)
|
|
254
|
-
io, algo, owned = resolve_io(path_or_io, algo, mode: "rb")
|
|
255
|
-
reader = new(io, algo: algo, dictionary: dictionary, **opts)
|
|
256
|
-
reader.instance_variable_set(:@owned_io, owned)
|
|
257
|
-
|
|
258
|
-
return reader unless block
|
|
259
|
-
|
|
260
|
-
begin
|
|
261
|
-
yield reader
|
|
262
|
-
ensure
|
|
263
|
-
reader.close
|
|
264
|
-
end
|
|
110
|
+
def brotli(data, level: nil)
|
|
111
|
+
compress(data, algo: :brotli, level: level)
|
|
265
112
|
end
|
|
266
113
|
|
|
267
|
-
def
|
|
268
|
-
|
|
269
|
-
@inflater = Inflater.new(algo: algo, dictionary: dictionary, **opts)
|
|
270
|
-
@closed = false
|
|
271
|
-
@owned_io = false
|
|
272
|
-
@buffer = +"".b
|
|
273
|
-
@buffer_pos = 0
|
|
274
|
-
@eof = false
|
|
114
|
+
def decompress(data, **opts)
|
|
115
|
+
_c_decompress(data, **resolved_one_shot_options(opts))
|
|
275
116
|
end
|
|
276
117
|
|
|
277
|
-
def
|
|
278
|
-
|
|
279
|
-
return read_all if length.nil?
|
|
280
|
-
|
|
281
|
-
read_exactly(length)
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
def gets(separator = "\n")
|
|
285
|
-
ensure_open!
|
|
286
|
-
return nil if @eof && buffer_empty?
|
|
287
|
-
|
|
288
|
-
fill_buffer_until { buffer_includes?(separator) }
|
|
289
|
-
|
|
290
|
-
return extract_line(separator) if buffer_includes?(separator)
|
|
291
|
-
return consume_buffer unless buffer_empty?
|
|
292
|
-
|
|
293
|
-
nil
|
|
118
|
+
def zstd_decompress(data)
|
|
119
|
+
decompress(data, algo: :zstd)
|
|
294
120
|
end
|
|
295
121
|
|
|
296
|
-
def
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
@closed = true
|
|
302
|
-
nil
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
def closed?
|
|
306
|
-
@closed
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
def eof?
|
|
310
|
-
@eof && buffer_empty?
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
def each_line
|
|
314
|
-
return enum_for(:each_line) unless block_given?
|
|
315
|
-
|
|
316
|
-
while (line = gets)
|
|
317
|
-
yield line
|
|
122
|
+
def lz4_decompress(data, format: nil)
|
|
123
|
+
if format
|
|
124
|
+
decompress(data, algo: :lz4, format: format)
|
|
125
|
+
else
|
|
126
|
+
decompress(data, algo: :lz4)
|
|
318
127
|
end
|
|
319
128
|
end
|
|
320
129
|
|
|
321
|
-
def
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
while (chunk = read(size))
|
|
325
|
-
yield chunk
|
|
326
|
-
end
|
|
130
|
+
def brotli_decompress(data)
|
|
131
|
+
decompress(data, algo: :brotli)
|
|
327
132
|
end
|
|
328
133
|
|
|
329
|
-
def
|
|
330
|
-
|
|
134
|
+
def algo_from_ext(path)
|
|
135
|
+
EXTENSION_MAP[File.extname(path).downcase]
|
|
331
136
|
end
|
|
332
137
|
|
|
333
138
|
private
|
|
334
139
|
|
|
335
|
-
def
|
|
336
|
-
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
def buffer_size
|
|
340
|
-
@buffer.bytesize - @buffer_pos
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
def buffer_empty?
|
|
344
|
-
@buffer_pos >= @buffer.bytesize
|
|
345
|
-
end
|
|
346
|
-
|
|
347
|
-
def buffer_append(data)
|
|
348
|
-
compact_buffer_if_needed
|
|
349
|
-
@buffer << data
|
|
350
|
-
end
|
|
351
|
-
|
|
352
|
-
def compact_buffer_if_needed
|
|
353
|
-
return if @buffer_pos == 0
|
|
354
|
-
|
|
355
|
-
total = @buffer.bytesize
|
|
356
|
-
return unless @buffer_pos >= BUFFER_COMPACT_THRESHOLD && @buffer_pos * 2 >= total
|
|
140
|
+
def resolved_one_shot_options(opts)
|
|
141
|
+
return opts.merge(max_output_size: config.max_output_size) unless opts.key?(:max_output_size)
|
|
357
142
|
|
|
358
|
-
|
|
359
|
-
@buffer_pos = 0
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
def buffer_includes?(separator)
|
|
363
|
-
idx = @buffer.index(separator, @buffer_pos)
|
|
364
|
-
!idx.nil?
|
|
365
|
-
end
|
|
366
|
-
|
|
367
|
-
def read_all
|
|
368
|
-
return nil if @eof && buffer_empty?
|
|
369
|
-
|
|
370
|
-
result = if buffer_empty?
|
|
371
|
-
+"".b
|
|
372
|
-
else
|
|
373
|
-
@buffer.byteslice(@buffer_pos, @buffer.bytesize - @buffer_pos) || +"".b
|
|
374
|
-
end
|
|
375
|
-
@buffer.clear
|
|
376
|
-
@buffer_pos = 0
|
|
377
|
-
|
|
378
|
-
until @eof
|
|
379
|
-
chunk = read_compressed_chunk
|
|
380
|
-
break unless chunk && !chunk.empty?
|
|
381
|
-
|
|
382
|
-
decompressed = @inflater.write(chunk)
|
|
383
|
-
result << decompressed if decompressed
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
final = @inflater.finish
|
|
387
|
-
result << final if final && !final.empty?
|
|
388
|
-
|
|
389
|
-
result.empty? ? nil : result
|
|
390
|
-
end
|
|
391
|
-
|
|
392
|
-
def read_exactly(length)
|
|
393
|
-
return nil if @eof && buffer_empty?
|
|
394
|
-
|
|
395
|
-
fill_buffer_until { buffer_size >= length }
|
|
396
|
-
|
|
397
|
-
if buffer_size >= length
|
|
398
|
-
result = @buffer.byteslice(@buffer_pos, length)
|
|
399
|
-
@buffer_pos += length
|
|
400
|
-
compact_buffer_if_needed
|
|
401
|
-
result
|
|
402
|
-
elsif !buffer_empty?
|
|
403
|
-
consume_buffer
|
|
404
|
-
end
|
|
405
|
-
end
|
|
406
|
-
|
|
407
|
-
def fill_buffer_until
|
|
408
|
-
until yield || @eof
|
|
409
|
-
chunk = read_compressed_chunk
|
|
410
|
-
if chunk.nil? || chunk.empty?
|
|
411
|
-
@eof = true
|
|
412
|
-
break
|
|
413
|
-
end
|
|
414
|
-
decompressed = @inflater.write(chunk)
|
|
415
|
-
buffer_append(decompressed) if decompressed
|
|
416
|
-
end
|
|
417
|
-
end
|
|
418
|
-
|
|
419
|
-
def extract_line(separator)
|
|
420
|
-
idx = @buffer.index(separator, @buffer_pos)
|
|
421
|
-
end_pos = idx + separator.bytesize
|
|
422
|
-
result = @buffer.byteslice(@buffer_pos, end_pos - @buffer_pos)
|
|
423
|
-
@buffer_pos = end_pos
|
|
424
|
-
compact_buffer_if_needed
|
|
425
|
-
result
|
|
426
|
-
end
|
|
427
|
-
|
|
428
|
-
def consume_buffer
|
|
429
|
-
result = @buffer.byteslice(@buffer_pos, @buffer.bytesize - @buffer_pos) || +"".b
|
|
430
|
-
@buffer.clear
|
|
431
|
-
@buffer_pos = 0
|
|
432
|
-
result
|
|
143
|
+
opts
|
|
433
144
|
end
|
|
145
|
+
end
|
|
434
146
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
147
|
+
module InflaterDefaults
|
|
148
|
+
def initialize(*args, **opts)
|
|
149
|
+
resolved = opts.dup
|
|
150
|
+
resolved[:max_output_size] = MultiCompress.config.streaming_max_output_size unless resolved.key?(:max_output_size)
|
|
151
|
+
super(*args, **resolved)
|
|
439
152
|
end
|
|
153
|
+
end
|
|
440
154
|
|
|
441
|
-
|
|
442
|
-
if path_or_io.is_a?(String)
|
|
443
|
-
[File.open(path_or_io, mode), algo || MultiCompress.algo_from_ext(path_or_io), true]
|
|
444
|
-
else
|
|
445
|
-
[path_or_io, algo, false]
|
|
446
|
-
end
|
|
447
|
-
end
|
|
155
|
+
Inflater.prepend(InflaterDefaults)
|
|
448
156
|
|
|
449
|
-
|
|
450
|
-
|
|
157
|
+
require "multi_compress/stream_io"
|
|
158
|
+
require "multi_compress/writer"
|
|
159
|
+
require "multi_compress/reader"
|
|
451
160
|
end
|
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;
|