herringbone 0.1.0 → 0.3.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/README.md +198 -74
- data/bin/herringbone +38 -26
- data/lib/herringbone/active_record.rb +48 -19
- data/lib/herringbone/bloom_filter.rb +370 -0
- data/lib/herringbone/byte_values.rb +29 -0
- data/lib/herringbone/codecs/lz4.rb +66 -3
- data/lib/herringbone/codecs/snappy.rb +132 -3
- data/lib/herringbone/compression.rb +121 -16
- data/lib/herringbone/encodings/delta.rb +76 -19
- data/lib/herringbone/encodings/plain.rb +29 -7
- data/lib/herringbone/encodings/rle.rb +55 -3
- data/lib/herringbone/format.rb +203 -0
- data/lib/herringbone/inspector.rb +1784 -0
- data/lib/herringbone/io_buffer_support.rb +3 -1
- data/lib/herringbone/reader/column_chunk_reader.rb +388 -0
- data/lib/herringbone/reader/column_cursor.rb +225 -0
- data/lib/herringbone/reader/numo.rb +365 -0
- data/lib/herringbone/reader/page_stream.rb +615 -0
- data/lib/herringbone/reader/scan.rb +350 -0
- data/lib/herringbone/reader.rb +585 -272
- data/lib/herringbone/schema.rb +326 -90
- data/lib/herringbone/thrift.rb +155 -4
- data/lib/herringbone/types.rb +177 -42
- data/lib/herringbone/version.rb +2 -1
- data/lib/herringbone/visualizer.rb +1136 -0
- data/lib/herringbone/writer.rb +550 -105
- data/lib/herringbone/xxhash.rb +425 -0
- data/lib/herringbone.rb +64 -9
- metadata +14 -33
|
@@ -0,0 +1,1136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Herringbone
|
|
6
|
+
# Renders a Parquet file's layout as one self-contained HTML page: a to-scale byte map of the
|
|
7
|
+
# file (row groups, column chunks, dictionary and data pages, page indexes, bloom filters,
|
|
8
|
+
# footer), the schema, per-column and per-page tables, page indexes and key/value metadata.
|
|
9
|
+
# CSS, JS and data are inline; the only external resource is highlight.js from cdnjs, used to
|
|
10
|
+
# colour JSON (the page works without it).
|
|
11
|
+
#
|
|
12
|
+
# The design and idea come from Parquet X-ray by cfahlgren1
|
|
13
|
+
# (https://huggingface.co/spaces/cfahlgren1/parquet-xray), credited at the top of every page.
|
|
14
|
+
#
|
|
15
|
+
# Used through Inspector#to_html:
|
|
16
|
+
#
|
|
17
|
+
# File.open("data.parquet", "rb") { |io| Herringbone::Inspector.new(io).to_html }
|
|
18
|
+
class Visualizer
|
|
19
|
+
# highlight.js build loaded by the page to colour the footer JSON; optional.
|
|
20
|
+
HIGHLIGHT_JS = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"
|
|
21
|
+
# The design and idea of this page come from Parquet X-ray; credited at the top of every page
|
|
22
|
+
CREDIT_URL = "https://huggingface.co/spaces/cfahlgren1/parquet-xray"
|
|
23
|
+
|
|
24
|
+
# Beyond this many pages in the file, per-page detail is kept only for the first chunks
|
|
25
|
+
# (the rest are drawn as whole column chunks) to keep the page small
|
|
26
|
+
MAX_PAGES = 60_000
|
|
27
|
+
# Footer JSON is embedded only when the footer is smaller than this
|
|
28
|
+
MAX_FOOTER_JSON = 512 * 1024
|
|
29
|
+
|
|
30
|
+
# PageType names to their Thrift ids, the page type code the embedded JS expects in page rows
|
|
31
|
+
# (unknown page types are passed on as their raw Integer).
|
|
32
|
+
PAGE_TYPES = {DATA_PAGE: 0, INDEX_PAGE: 1, DICTIONARY_PAGE: 2, DATA_PAGE_V2: 3}.freeze
|
|
33
|
+
|
|
34
|
+
# +inspector+ is an Inspector. +title+ defaults to the file's name.
|
|
35
|
+
# @param inspector [Inspector] inspector over the file to render
|
|
36
|
+
# @param title [String, nil] page title; defaults to the file's name
|
|
37
|
+
# @param max_pages [Integer] page budget for per-page detail (see MAX_PAGES)
|
|
38
|
+
# @raise [ArgumentError] if +inspector+ is not an Inspector
|
|
39
|
+
def initialize(inspector, title: nil, max_pages: MAX_PAGES)
|
|
40
|
+
raise ArgumentError, "Expected a Herringbone::Inspector, got #{inspector.class}" unless inspector.is_a?(Inspector)
|
|
41
|
+
@inspector = inspector
|
|
42
|
+
@title = title
|
|
43
|
+
@max_pages = max_pages
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Builds the page. Walks every page header of the file (and reads the page indexes).
|
|
47
|
+
# @return [String] a complete, self-contained HTML document
|
|
48
|
+
def to_html
|
|
49
|
+
json = JSON.generate(payload).gsub("<", "\\u003c").gsub("\u2028", "\\u2028").gsub("\u2029", "\\u2029")
|
|
50
|
+
name = @title || @inspector.name || "Parquet file"
|
|
51
|
+
values = {"TITLE" => escape_html("#{name} · Parquet layout"), "HIGHLIGHT_JS" => HIGHLIGHT_JS,
|
|
52
|
+
"CREDIT_URL" => CREDIT_URL, "DATA" => json}
|
|
53
|
+
# One pass, so placeholder-like text in the data is never substituted
|
|
54
|
+
TEMPLATE.gsub(/%%(TITLE|HIGHLIGHT_JS|CREDIT_URL|DATA)%%/) { values.fetch(Regexp.last_match(1)) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# The data embedded in the page
|
|
60
|
+
# @return [Hash{Symbol => Object}] file, schema, columns, row groups (with chunks and pages),
|
|
61
|
+
# key/value metadata and footer JSON, ready for JSON.generate
|
|
62
|
+
def payload
|
|
63
|
+
i = @inspector
|
|
64
|
+
budget = @max_pages
|
|
65
|
+
totals = i.column_totals.to_h { |t| [t[:column], t] }
|
|
66
|
+
{
|
|
67
|
+
generator: "herringbone #{VERSION}",
|
|
68
|
+
file: file_info,
|
|
69
|
+
schema: i.schema_tree,
|
|
70
|
+
columns: i.columns.map { |c| column_info(c, totals[c.index]) },
|
|
71
|
+
row_groups: i.row_groups.map do |rg|
|
|
72
|
+
{
|
|
73
|
+
i: rg.index, rows: rg.num_rows, first_row: rg.first_row, start: rg.start_offset, end: rg.end_offset,
|
|
74
|
+
cs: rg.compressed_size, us: rg.uncompressed_size, total_byte_size: rg.total_byte_size,
|
|
75
|
+
file_offset: rg.row_group.file_offset, ordinal: rg.row_group.ordinal, sorting: rg.sorting_columns,
|
|
76
|
+
chunks: rg.columns.map do |c|
|
|
77
|
+
keep = c.pages.size <= budget
|
|
78
|
+
budget -= c.pages.size if keep
|
|
79
|
+
chunk_info(c, keep)
|
|
80
|
+
end
|
|
81
|
+
}
|
|
82
|
+
end,
|
|
83
|
+
kv: i.key_value_metadata,
|
|
84
|
+
arrow_error: i.arrow_schema_error,
|
|
85
|
+
footer_json: footer_json
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Inspector#summary plus the display name and whether per-page detail was truncated.
|
|
90
|
+
# @return [Hash{Symbol => Object}] file-level facts for the page header
|
|
91
|
+
def file_info
|
|
92
|
+
s = @inspector.summary
|
|
93
|
+
name = @title || @inspector.name || "(IO)"
|
|
94
|
+
s.merge(name: name, pages_truncated: @inspector.column_chunks.sum { |c| c.pages.size } > @max_pages,
|
|
95
|
+
index_mismatches: @inspector.column_chunks.sum { |c| c.index_mismatches.size })
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# One leaf column with its totals over all row groups.
|
|
99
|
+
# @param col [Schema::Column] leaf column
|
|
100
|
+
# @param t [Hash{Symbol => Object}] the column's entry from Inspector#column_totals
|
|
101
|
+
# @return [Hash{Symbol => Object}] column row, keyed with the short names the JS uses
|
|
102
|
+
def column_info(col, t)
|
|
103
|
+
node = col.node
|
|
104
|
+
{
|
|
105
|
+
i: col.index, path: col.dotted_path, name: node.name, type: Inspector.type_name(col),
|
|
106
|
+
physical: Format::Type::NAMES[node.type].to_s, logical: Inspector.logical_type_name(node),
|
|
107
|
+
repetition: node.repetition, max_def: col.max_definition_level, max_rep: col.max_repetition_level,
|
|
108
|
+
sort_order: Inspector.sort_order(col),
|
|
109
|
+
codecs: t[:codecs], encodings: t[:encodings], cs: t[:compressed_size], us: t[:uncompressed_size],
|
|
110
|
+
values: t[:num_values], nulls: t[:null_count], pages: t[:num_pages], data_pages: t[:num_data_pages],
|
|
111
|
+
dict_pages: t[:dictionary_pages], dict_bytes: t[:dictionary_bytes],
|
|
112
|
+
min: disp(t[:min]), max: disp(t[:max])
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# One column chunk: its metadata, statistics and index ranges and, when +with_pages+, its page
|
|
117
|
+
# rows, ColumnIndex and OffsetIndex.
|
|
118
|
+
# @param c [Inspector::ColumnChunkInfo] chunk to describe
|
|
119
|
+
# @param with_pages [Boolean] whether to include per-page detail (false once over the page budget)
|
|
120
|
+
# @return [Hash{Symbol => Object}] JSON-safe chunk entry, nil members omitted
|
|
121
|
+
def chunk_info(c, with_pages)
|
|
122
|
+
st = c.statistics
|
|
123
|
+
h = {
|
|
124
|
+
c: c.column.index, codec: c.codec.to_s, enc: c.encodings, estats: c.encoding_stats,
|
|
125
|
+
values: c.num_values, nulls: c.null_count, cs: c.compressed_size, us: c.uncompressed_size,
|
|
126
|
+
start: c.start_offset, end: c.end_offset, declared_end: c.declared_end_offset,
|
|
127
|
+
data_off: c.data_page_offset, dict_off: c.dictionary_page_offset, file_offset: c.chunk.file_offset,
|
|
128
|
+
stats: st && stats_info(st),
|
|
129
|
+
bloom: c.bloom_filter_offset && [c.bloom_filter_offset, c.bloom_filter_length],
|
|
130
|
+
ci: c.column_index_range, oi: c.offset_index_range,
|
|
131
|
+
dict: c.dictionary_page && {n: c.dictionary_page.num_values, cs: c.dictionary_page.compressed_size,
|
|
132
|
+
us: c.dictionary_page.uncompressed_size, sorted: c.dictionary_page.is_sorted},
|
|
133
|
+
np: c.pages.size, ndp: c.data_pages.size,
|
|
134
|
+
size_stats: c.size_statistics,
|
|
135
|
+
kv: c.key_value_metadata.empty? ? nil : c.key_value_metadata,
|
|
136
|
+
err: c.error,
|
|
137
|
+
# [page, field, value in page header, value in column index]; page is nil for :page_count
|
|
138
|
+
idx_mm: c.index_mismatches.empty? ? nil : c.index_mismatches.map { |m| mismatch_row(m) },
|
|
139
|
+
crc_bad: c.pages.count { |p| p.checksum == :mismatch }.then { |n| n.zero? ? nil : n }
|
|
140
|
+
}
|
|
141
|
+
if with_pages
|
|
142
|
+
h[:pages] = c.pages.map { |p| page_row(p) }
|
|
143
|
+
if (ci = c.column_index)
|
|
144
|
+
h[:column_index] = {
|
|
145
|
+
boundary: ci.boundary_order,
|
|
146
|
+
rows: ci.null_pages.each_index.map do |k|
|
|
147
|
+
[ci.null_pages[k] ? 1 : 0, disp(ci.min_values[k]), disp(ci.max_values[k]), ci.null_counts&.[](k)]
|
|
148
|
+
end
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
if (oi = c.offset_index)
|
|
152
|
+
h[:offset_index] = oi.page_locations.map { |l| [l.offset, l.compressed_page_size, l.first_row_index] }
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
Inspector.jsonable(h.compact)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# @param st [Inspector::Stats] chunk statistics
|
|
159
|
+
# @return [Hash{Symbol => Object}] statistics with min/max in display form, nil members omitted
|
|
160
|
+
def stats_info(st)
|
|
161
|
+
{
|
|
162
|
+
min: disp(st.min), max: disp(st.max), nulls: st.null_count, distinct: st.distinct_count,
|
|
163
|
+
min_exact: st.min_exact, max_exact: st.max_exact, source: st.source, caveat: st.caveat
|
|
164
|
+
}.compact
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# [type, offset, header_size, compressed, uncompressed, values, nulls, rows, first_row, encoding,
|
|
168
|
+
# min, max, crc, extra]; crc is 0 (none), 1 (present, not verified), 2 (verified ok) or 3 (mismatch)
|
|
169
|
+
# @param p [Inspector::PageInfo] page header to describe
|
|
170
|
+
# @return [Array] the page row, compact so pages of large files stay small
|
|
171
|
+
def page_row(p)
|
|
172
|
+
st = p.statistics
|
|
173
|
+
extra = if p.type == :DATA_PAGE_V2
|
|
174
|
+
"levels #{p.repetition_levels_byte_length}+#{p.definition_levels_byte_length} B" \
|
|
175
|
+
"#{", not compressed" unless p.is_compressed}"
|
|
176
|
+
elsif p.type == :DATA_PAGE
|
|
177
|
+
[p.definition_level_encoding && "def #{p.definition_level_encoding}",
|
|
178
|
+
p.repetition_level_encoding && "rep #{p.repetition_level_encoding}"].compact.join(", ")
|
|
179
|
+
elsif p.dictionary?
|
|
180
|
+
p.is_sorted ? "sorted" : nil
|
|
181
|
+
end
|
|
182
|
+
[PAGE_TYPES.fetch(p.type) { p.type }, p.offset, p.header_size, p.compressed_size, p.uncompressed_size, p.num_values,
|
|
183
|
+
p.num_nulls, p.num_rows, p.first_row_index, p.encoding, st && disp(st.min), st && disp(st.max),
|
|
184
|
+
CRC_STATES.fetch(p.checksum) { p.crc.nil? ? 0 : 1 }, extra]
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# PageInfo#checksum states to the crc codes of page_row (1, present but unverified, is the fallback).
|
|
188
|
+
CRC_STATES = {absent: 0, ok: 2, mismatch: 3}.freeze
|
|
189
|
+
|
|
190
|
+
# A page statistics vs ColumnIndex disagreement as a compact row.
|
|
191
|
+
# @param m [Hash{Symbol => Object}] entry from Inspector::ColumnChunkInfo#index_mismatches
|
|
192
|
+
# @return [Array] [page, field, value in page header, value in column index]; min/max in display form
|
|
193
|
+
def mismatch_row(m)
|
|
194
|
+
values = [m[:page_value], m[:index_value]]
|
|
195
|
+
values = values.map { |v| disp(v) } if m[:field] == :min || m[:field] == :max
|
|
196
|
+
[m[:page], m[:field].to_s, *values]
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Display form of a decoded statistics value: strings quoted, the rest as text
|
|
200
|
+
# @param v [Object, nil] decoded value (String, numeric, Time, Date, Array, ...)
|
|
201
|
+
# @return [String, nil] at most 160 characters, nil for nil
|
|
202
|
+
def disp(v)
|
|
203
|
+
s = case v
|
|
204
|
+
when nil then return nil
|
|
205
|
+
when String
|
|
206
|
+
t = Inspector.text(v)
|
|
207
|
+
(v.encoding == Encoding::BINARY && t.start_with?("0x")) ? t : JSON.generate(t)
|
|
208
|
+
when Float then v.nan? ? "NaN" : v.to_s
|
|
209
|
+
when BigDecimal then v.to_s("F")
|
|
210
|
+
when Time then Inspector.jsonable(v)
|
|
211
|
+
when Date then v.iso8601
|
|
212
|
+
when Array then JSON.generate(Inspector.jsonable(v))
|
|
213
|
+
else v.to_s
|
|
214
|
+
end
|
|
215
|
+
(s.size > 160) ? "#{s[0, 159]}…" : s
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# The footer FileMetaData pretty-printed, or nil when it exceeds MAX_FOOTER_JSON bytes.
|
|
219
|
+
# @return [String, nil] JSON text
|
|
220
|
+
def footer_json
|
|
221
|
+
return nil if @inspector.footer_size > MAX_FOOTER_JSON
|
|
222
|
+
JSON.pretty_generate(Inspector.jsonable(raw(@inspector.metadata.to_h)))
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Footer structs as plain data; binary statistics as hex, long strings shortened
|
|
226
|
+
# @param v [Object] a value from FileMetaData#to_h (Hash, Array, String or scalar)
|
|
227
|
+
# @return [Object] +v+ with every String passed through Inspector.text and cut at 300 characters
|
|
228
|
+
def raw(v)
|
|
229
|
+
case v
|
|
230
|
+
when Hash then v.to_h { |k, x| [k, raw(x)] }
|
|
231
|
+
when Array then v.map { |x| raw(x) }
|
|
232
|
+
when String
|
|
233
|
+
t = Inspector.text(v)
|
|
234
|
+
(t.size > 300) ? "#{t[0, 300]}… (#{v.bytesize} bytes)" : t
|
|
235
|
+
else v
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# @param s [Object] text to put into HTML (converted with to_s)
|
|
240
|
+
# @return [String] +s+ with &, <, > and double quotes escaped
|
|
241
|
+
def escape_html(s)
|
|
242
|
+
s.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# The page: HTML, CSS and JS, with %%TITLE%%, %%HIGHLIGHT_JS%%, %%CREDIT_URL%% and %%DATA%%
|
|
246
|
+
# placeholders filled in by to_html.
|
|
247
|
+
TEMPLATE = <<~'HTML'
|
|
248
|
+
<!doctype html>
|
|
249
|
+
<html lang="en">
|
|
250
|
+
<head>
|
|
251
|
+
<meta charset="utf-8">
|
|
252
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
253
|
+
<meta name="color-scheme" content="light dark">
|
|
254
|
+
<title>%%TITLE%%</title>
|
|
255
|
+
<style>
|
|
256
|
+
:root {
|
|
257
|
+
--ink: #111827; --secondary: #374151; --subtle: #6b7280; --faint: #9ca3af;
|
|
258
|
+
--line: #e5e7eb; --line-soft: #f1f2f4; --bg: #ffffff; --surface: #ffffff; --surface-alt: #f9fafb;
|
|
259
|
+
--accent: #4f46e5; --accent-soft: #eef2ff; --accent-line: #c7d2fe;
|
|
260
|
+
--ok: #047857; --ok-soft: #ecfdf5; --ok-line: #a7f3d0; --warn: #b45309; --warn-soft: #fffbeb; --err: #b91c1c; --err-soft: #fef2f2;
|
|
261
|
+
--strip-bg: #f3f4f6; --seam: rgba(255,255,255,.85); --outline: #111827;
|
|
262
|
+
--k-dict: #e2e5ea; --k-data: #c3c8d0; --k-ci: #d8b4fe; --k-oi: #93c5fd; --k-bloom: #6ee7b7;
|
|
263
|
+
--k-cmeta: #fcd9a8; --k-footer: #4b5563; --k-flen: #1f2937; --k-magic: #111827; --k-unknown: #fca5a5;
|
|
264
|
+
--kw: #e11d48; --ty: #7c3aed; --ann: #0d9488; --num: #b45309; --str: #047857;
|
|
265
|
+
--shadow: 0 10px 30px -10px rgb(17 24 39 / 25%), 0 2px 6px rgb(17 24 39 / 6%);
|
|
266
|
+
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
267
|
+
}
|
|
268
|
+
@media (prefers-color-scheme: dark) {
|
|
269
|
+
:root {
|
|
270
|
+
--ink: #e5e7eb; --secondary: #cbd2dc; --subtle: #9aa3b2; --faint: #6b7483;
|
|
271
|
+
--line: #2a2f38; --line-soft: #20252c; --bg: #0e1116; --surface: #13171d; --surface-alt: #181d24;
|
|
272
|
+
--accent: #8b93ff; --accent-soft: #1d2140; --accent-line: #3b418a;
|
|
273
|
+
--ok: #34d399; --ok-soft: #0d2a22; --ok-line: #1f5d4a; --warn: #fbbf24; --warn-soft: #2a2110; --err: #f87171; --err-soft: #2c1414;
|
|
274
|
+
--strip-bg: #1b2028; --seam: rgba(14,17,22,.8); --outline: #f9fafb;
|
|
275
|
+
--k-dict: #394150; --k-data: #566072; --k-ci: #7e5aa8; --k-oi: #3f6fa8; --k-bloom: #2f8f6c;
|
|
276
|
+
--k-cmeta: #7a5a2e; --k-footer: #9ca3af; --k-flen: #d1d5db; --k-magic: #f3f4f6; --k-unknown: #b45353;
|
|
277
|
+
--kw: #fb7185; --ty: #c4b5fd; --ann: #5eead4; --num: #fbbf24; --str: #6ee7b7;
|
|
278
|
+
--shadow: 0 10px 30px -10px rgb(0 0 0 / 70%), 0 2px 6px rgb(0 0 0 / 40%);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
* { box-sizing: border-box; }
|
|
282
|
+
html { scrollbar-gutter: stable; }
|
|
283
|
+
body { margin: 0; background: var(--bg); color: var(--ink); font: 13px/1.45 Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
284
|
+
button, input, select { font: inherit; color: inherit; }
|
|
285
|
+
.wrap { max-width: 1320px; margin: 0 auto; padding: 24px 24px 64px; }
|
|
286
|
+
header.top { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; margin-bottom: 18px; }
|
|
287
|
+
header.top h1 { margin: 0; font-size: 20px; letter-spacing: -.01em; display: flex; align-items: center; gap: 10px; }
|
|
288
|
+
.logo { width: 18px; height: 18px; border-radius: 3px; background: repeating-linear-gradient(135deg, var(--accent) 0 3px, transparent 3px 6px), var(--accent-soft); outline: 1px solid var(--accent-line); }
|
|
289
|
+
header.top .sub { color: var(--subtle); }
|
|
290
|
+
.credit { flex-basis: 100%; margin: 4px 0 0; padding: 6px 10px; border-radius: 8px; background: var(--accent-soft); border: 1px solid var(--accent-line); color: var(--secondary); font-size: 12.5px; }
|
|
291
|
+
.credit a { color: var(--accent); font-weight: 600; }
|
|
292
|
+
h2 { margin: 0 0 8px; font-size: 14px; display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
|
|
293
|
+
h2 small { font-size: 12px; font-weight: 400; color: var(--faint); }
|
|
294
|
+
section { margin-bottom: 22px; }
|
|
295
|
+
.card { border: 1px solid var(--line); border-radius: 12px; background: var(--surface); }
|
|
296
|
+
.pad { padding: 14px 16px; }
|
|
297
|
+
.mono { font-family: var(--mono); }
|
|
298
|
+
.faint { color: var(--faint); } .subtle { color: var(--subtle); }
|
|
299
|
+
.num { font-variant-numeric: tabular-nums; }
|
|
300
|
+
.summary header { display: flex; gap: 10px; align-items: center; padding: 10px 16px; background: var(--surface-alt); border-bottom: 1px solid var(--line); border-radius: 12px 12px 0 0; flex-wrap: wrap; }
|
|
301
|
+
.summary header h2 { margin: 0; font-family: var(--mono); font-size: 14px; }
|
|
302
|
+
.stats { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)) minmax(0, 1.8fr); gap: 12px; padding: 12px 16px; margin: 0; }
|
|
303
|
+
.stats dt { font-size: 11.5px; color: var(--subtle); }
|
|
304
|
+
.stats dd { margin: 2px 0 0; font-size: 15px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
305
|
+
.stats dd.mono { font-size: 12.5px; font-weight: 500; margin-top: 4px; }
|
|
306
|
+
.badges { display: flex; flex-wrap: wrap; gap: 8px; padding: 0 16px 14px; }
|
|
307
|
+
.badge { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--line); border-radius: 999px; padding: 3px 11px; font-size: 12.5px; color: var(--subtle); }
|
|
308
|
+
.badge.ok { background: var(--ok-soft); border-color: var(--ok-line); color: var(--ok); }
|
|
309
|
+
.badge.warn { background: var(--warn-soft); border-color: var(--warn); color: var(--warn); }
|
|
310
|
+
.badge.err { background: var(--err-soft); border-color: var(--err); color: var(--err); }
|
|
311
|
+
.badge code { font-family: var(--mono); font-size: 12px; }
|
|
312
|
+
.map-grid { display: grid; grid-template-columns: minmax(0, 1fr) 240px; gap: 16px; }
|
|
313
|
+
.strip-label { display: flex; justify-content: space-between; gap: 8px; font-size: 11px; color: var(--faint); margin-bottom: 4px; }
|
|
314
|
+
.strip-label b { color: var(--secondary); font-weight: 500; }
|
|
315
|
+
.strip { height: 40px; position: relative; }
|
|
316
|
+
.strip canvas { display: block; width: 100%; height: 100%; border-radius: 3px; cursor: crosshair; }
|
|
317
|
+
.strip.pick canvas { cursor: pointer; }
|
|
318
|
+
.zoom { margin-top: 14px; }
|
|
319
|
+
.zoom .strip { height: 30px; }
|
|
320
|
+
.legend { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 10px; font-size: 11.5px; color: var(--subtle); align-items: center; }
|
|
321
|
+
.legend .sw { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 5px; vertical-align: -1px; }
|
|
322
|
+
.legend .item { cursor: pointer; white-space: nowrap; }
|
|
323
|
+
.legend .item.dim { opacity: .4; }
|
|
324
|
+
.legend .kinds { margin-left: auto; display: inline-flex; flex-wrap: wrap; gap: 4px 13px; color: var(--faint); }
|
|
325
|
+
.hint { margin: 6px 0 0; font-size: 11.5px; color: var(--faint); }
|
|
326
|
+
.two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 20px; align-items: start; }
|
|
327
|
+
.schema { background: var(--surface-alt); border-radius: 8px; outline: 1px solid var(--line); padding: 6px 0; font-family: var(--mono); font-size: 12.5px; }
|
|
328
|
+
.sline { display: grid; grid-template-columns: 30px minmax(0, 1fr) 140px; align-items: center; min-height: 24px; width: 100%; border: 0; background: transparent; text-align: left; padding: 0; }
|
|
329
|
+
button.sline { cursor: pointer; }
|
|
330
|
+
button.sline:hover, .sline.sel { background: var(--accent-soft); }
|
|
331
|
+
.sline.sel { box-shadow: inset 3px 0 0 var(--accent); }
|
|
332
|
+
.sline .ln { text-align: right; padding-right: 10px; color: var(--faint); opacity: .6; }
|
|
333
|
+
.sline .code { white-space: pre; overflow: hidden; text-overflow: ellipsis; padding-right: 12px; }
|
|
334
|
+
.sline .kw { color: var(--kw); } .sline .ty { color: var(--ty); } .sline .ann { color: var(--ann); } .sline .nm { font-weight: 600; }
|
|
335
|
+
.sline .lv { color: var(--faint); font-size: 11px; }
|
|
336
|
+
.sline .size { display: flex; align-items: center; gap: 8px; padding-right: 12px; font-size: 11px; color: var(--faint); }
|
|
337
|
+
.bar { position: relative; flex: 1; height: 4px; border-radius: 1px; background: var(--line-soft); }
|
|
338
|
+
.bar > span { position: absolute; top: 0; bottom: 0; left: 0; min-width: 1px; border-radius: 1px; }
|
|
339
|
+
.sline .size .v { width: 64px; text-align: right; white-space: nowrap; }
|
|
340
|
+
.rgs { overflow: hidden; }
|
|
341
|
+
.rg-row { display: grid; grid-template-columns: 14px 44px minmax(0, 1.1fr) minmax(0, 1fr) 76px; gap: 10px; align-items: center; height: 30px; width: 100%; padding: 0 12px; border: 0; border-bottom: 1px solid var(--line-soft); background: transparent; text-align: left; cursor: pointer; }
|
|
342
|
+
.rg-row:hover { background: var(--surface-alt); }
|
|
343
|
+
.rg-row.open { background: var(--accent-soft); }
|
|
344
|
+
.rg-row .caret { font-size: 10px; color: var(--faint); }
|
|
345
|
+
.rg-row .gbar { height: 8px; border-radius: 1px; display: block; }
|
|
346
|
+
.rg-body { padding: 8px 12px 12px 36px; border-bottom: 1px solid var(--line-soft); background: var(--surface-alt); }
|
|
347
|
+
.kvline { display: flex; flex-wrap: wrap; gap: 4px 18px; margin-bottom: 6px; }
|
|
348
|
+
.kvline .k { color: var(--faint); margin-right: 4px; }
|
|
349
|
+
.chunk-row { display: grid; grid-template-columns: 130px minmax(0, 1fr) 70px; grid-template-areas: "name pages size" "name minmax info"; gap: 2px 12px; padding: 6px 4px; border-bottom: 1px solid var(--line-soft); cursor: pointer; border-radius: 4px; }
|
|
350
|
+
.chunk-row:last-child { border-bottom: 0; }
|
|
351
|
+
.chunk-row:hover { background: var(--surface); }
|
|
352
|
+
.chunk-row.sel { background: var(--accent-soft); }
|
|
353
|
+
.chunk-row .name { grid-area: name; font-family: var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; align-self: start; }
|
|
354
|
+
.chunk-row .pages { grid-area: pages; display: flex; gap: 1px; height: 10px; align-items: stretch; }
|
|
355
|
+
.chunk-row .pages > span { min-width: 2px; border-radius: 1px; }
|
|
356
|
+
.chunk-row .size { grid-area: size; text-align: right; font-family: var(--mono); }
|
|
357
|
+
.chunk-row .minmax { grid-area: minmax; font-family: var(--mono); font-size: 11px; color: var(--subtle); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
358
|
+
.chunk-row .info { grid-area: info; text-align: right; font-size: 11px; color: var(--faint); white-space: nowrap; }
|
|
359
|
+
.more { margin: 6px 0 0; border: 0; background: transparent; color: var(--accent); cursor: pointer; padding: 4px; font-size: 12px; }
|
|
360
|
+
.tablewrap { overflow-x: auto; }
|
|
361
|
+
table { border-collapse: collapse; width: 100%; font-size: 12px; }
|
|
362
|
+
th { text-align: left; font-weight: 500; font-size: 11px; color: var(--faint); padding: 6px 8px; border-bottom: 1px solid var(--line); white-space: nowrap; position: sticky; top: 0; background: var(--surface); }
|
|
363
|
+
td { padding: 5px 8px; border-bottom: 1px solid var(--line-soft); vertical-align: top; }
|
|
364
|
+
td.r, th.r { text-align: right; }
|
|
365
|
+
td.mono { font-size: 11.5px; }
|
|
366
|
+
td.clip { max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
367
|
+
tbody tr.click { cursor: pointer; }
|
|
368
|
+
tbody tr.click:hover { background: var(--surface-alt); }
|
|
369
|
+
tbody tr.sel { background: var(--accent-soft); }
|
|
370
|
+
tbody tr.hl { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
371
|
+
.ratio { display: flex; align-items: center; gap: 6px; min-width: 110px; }
|
|
372
|
+
.ratio .bar { height: 6px; }
|
|
373
|
+
.pill { display: inline-block; padding: 0 6px; border-radius: 4px; background: var(--line-soft); font-family: var(--mono); font-size: 11px; margin: 1px 2px 1px 0; white-space: nowrap; }
|
|
374
|
+
.pill.warn { background: var(--warn-soft); color: var(--warn); }
|
|
375
|
+
.pill.err { color: var(--err); background: var(--err-soft); }
|
|
376
|
+
.pill.ok { color: var(--ok); background: var(--ok-soft); }
|
|
377
|
+
.sline .arrow { color: var(--faint); }
|
|
378
|
+
.sw-inline { display: inline-block; width: 9px; height: 9px; border-radius: 2px; margin-right: 6px; vertical-align: 0; }
|
|
379
|
+
.detail .dl { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 8px 18px; margin: 0 0 12px; }
|
|
380
|
+
.detail .dl dt { font-size: 11px; color: var(--faint); }
|
|
381
|
+
.detail .dl dd { margin: 1px 0 0; font-family: var(--mono); font-size: 12px; overflow-wrap: anywhere; }
|
|
382
|
+
.note { font-size: 12px; padding: 6px 10px; border-radius: 6px; background: var(--warn-soft); color: var(--warn); margin: 6px 0; }
|
|
383
|
+
.note.err { color: var(--err); }
|
|
384
|
+
.tabs { display: flex; gap: 4px; margin: 10px 0 8px; border-bottom: 1px solid var(--line); }
|
|
385
|
+
.tabs button { border: 0; background: transparent; padding: 6px 10px; cursor: pointer; color: var(--subtle); border-bottom: 2px solid transparent; margin-bottom: -1px; }
|
|
386
|
+
.tabs button.on { color: var(--ink); border-bottom-color: var(--accent); }
|
|
387
|
+
.pager { display: flex; gap: 8px; align-items: center; margin-top: 8px; font-size: 12px; color: var(--subtle); }
|
|
388
|
+
.pager button { border: 1px solid var(--line); background: var(--surface); border-radius: 6px; padding: 2px 10px; cursor: pointer; }
|
|
389
|
+
.pager button:disabled { opacity: .4; cursor: default; }
|
|
390
|
+
.kv { border-top: 1px solid var(--line-soft); padding: 10px 0; }
|
|
391
|
+
.kv:first-child { border-top: 0; padding-top: 0; }
|
|
392
|
+
.kv .head { display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; margin-bottom: 6px; }
|
|
393
|
+
.kv .key { font-family: var(--mono); font-weight: 600; }
|
|
394
|
+
pre { margin: 0; max-height: 420px; overflow: auto; background: var(--surface-alt); border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; font: 12px/1.5 var(--mono); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
395
|
+
pre code { font: inherit; background: none; padding: 0; }
|
|
396
|
+
details > summary { cursor: pointer; color: var(--subtle); margin-bottom: 8px; }
|
|
397
|
+
.hljs-attr { color: var(--ty); } .hljs-string { color: var(--str); } .hljs-number, .hljs-literal { color: var(--num); } .hljs-keyword { color: var(--kw); } .hljs-punctuation { color: var(--faint); }
|
|
398
|
+
#pop { position: fixed; z-index: 10; pointer-events: none; min-width: 240px; max-width: 420px; background: var(--surface); border: 1px solid var(--line); border-radius: 10px; box-shadow: var(--shadow); padding: 10px 12px; font-size: 12px; display: none; }
|
|
399
|
+
#pop .t { font-weight: 600; font-size: 13px; display: flex; align-items: center; gap: 6px; margin-bottom: 2px; }
|
|
400
|
+
#pop .p { font-family: var(--mono); color: var(--subtle); font-size: 11.5px; }
|
|
401
|
+
#pop .w { color: var(--secondary); margin: 6px 0; }
|
|
402
|
+
#pop dl { display: grid; grid-template-columns: auto 1fr; gap: 1px 12px; margin: 6px 0 0; }
|
|
403
|
+
#pop dt { color: var(--faint); } #pop dd { margin: 0; font-family: var(--mono); overflow-wrap: anywhere; }
|
|
404
|
+
footer.foot { color: var(--faint); font-size: 11.5px; margin-top: 30px; }
|
|
405
|
+
@media (max-width: 900px) {
|
|
406
|
+
.two, .map-grid { grid-template-columns: minmax(0, 1fr); }
|
|
407
|
+
.stats { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
408
|
+
.stats .wide { grid-column: 1 / -1; }
|
|
409
|
+
}
|
|
410
|
+
@media (max-width: 600px) {
|
|
411
|
+
.wrap { padding: 16px 12px 48px; }
|
|
412
|
+
.sline { grid-template-columns: 24px minmax(0, 1fr) 70px; }
|
|
413
|
+
.sline .size .bar { display: none; }
|
|
414
|
+
.rg-row { grid-template-columns: 14px 36px minmax(0, 1fr) 70px; }
|
|
415
|
+
.rg-row .gcell { display: none; }
|
|
416
|
+
.chunk-row { grid-template-columns: minmax(0, 1fr) 64px; grid-template-areas: "name size" "pages pages" "minmax info"; }
|
|
417
|
+
}
|
|
418
|
+
</style>
|
|
419
|
+
</head>
|
|
420
|
+
<body>
|
|
421
|
+
<div class="wrap">
|
|
422
|
+
<header class="top">
|
|
423
|
+
<h1><span class="logo" aria-hidden="true"></span>Parquet layout</h1>
|
|
424
|
+
<span class="sub">Footer, page headers and page indexes only · no values were decoded</span>
|
|
425
|
+
<p class="credit" id="credit">Design and idea from <a href="%%CREDIT_URL%%" rel="noopener">Parquet X-ray</a> by cfahlgren1 — this page reimplements its file map, schema and row group views for Herringbone.</p>
|
|
426
|
+
</header>
|
|
427
|
+
<section class="card summary" id="summary"></section>
|
|
428
|
+
<section class="card pad" id="map"></section>
|
|
429
|
+
<div class="two">
|
|
430
|
+
<section id="schema"></section>
|
|
431
|
+
<section id="rowgroups"></section>
|
|
432
|
+
</div>
|
|
433
|
+
<section id="detail"></section>
|
|
434
|
+
<section id="columns"></section>
|
|
435
|
+
<section id="kv"></section>
|
|
436
|
+
<section id="footer"></section>
|
|
437
|
+
<footer class="foot" id="gen"></footer>
|
|
438
|
+
</div>
|
|
439
|
+
<div id="pop" role="tooltip"></div>
|
|
440
|
+
<noscript><p style="padding:24px">This page needs JavaScript to draw the file layout.</p></noscript>
|
|
441
|
+
<script type="application/json" id="hb-data">%%DATA%%</script>
|
|
442
|
+
<script>
|
|
443
|
+
(function () {
|
|
444
|
+
"use strict";
|
|
445
|
+
var D = JSON.parse(document.getElementById("hb-data").textContent);
|
|
446
|
+
var F = D.file;
|
|
447
|
+
var S = { rg: null, col: null, page: null, tab: "pages", pageOffset: 0, allRgs: false, allCols: false, allSchema: false, allChunkRgs: false };
|
|
448
|
+
|
|
449
|
+
// ---------- helpers ----------
|
|
450
|
+
function h(tag, props) {
|
|
451
|
+
var el = document.createElement(tag);
|
|
452
|
+
if (props) for (var k in props) {
|
|
453
|
+
var v = props[k];
|
|
454
|
+
if (v == null || v === false) continue;
|
|
455
|
+
if (k === "class") el.className = v;
|
|
456
|
+
else if (k === "text") el.textContent = v;
|
|
457
|
+
else if (k === "style") el.setAttribute("style", v);
|
|
458
|
+
else if (k.slice(0, 2) === "on") el.addEventListener(k.slice(2), v);
|
|
459
|
+
else el.setAttribute(k, v === true ? "" : v);
|
|
460
|
+
}
|
|
461
|
+
for (var i = 2; i < arguments.length; i++) add(el, arguments[i]);
|
|
462
|
+
return el;
|
|
463
|
+
}
|
|
464
|
+
function add(el, c) {
|
|
465
|
+
if (c == null || c === false) return;
|
|
466
|
+
if (Array.isArray(c)) { c.forEach(function (x) { add(el, x); }); return; }
|
|
467
|
+
el.appendChild(typeof c === "object" ? c : document.createTextNode(String(c)));
|
|
468
|
+
}
|
|
469
|
+
function clear(el) { while (el.firstChild) el.removeChild(el.firstChild); return el; }
|
|
470
|
+
function num(n) { return n == null ? "–" : Number(n).toLocaleString("en-US"); }
|
|
471
|
+
function bytes(n) {
|
|
472
|
+
if (n == null) return "–";
|
|
473
|
+
var u = ["B", "KB", "MB", "GB", "TB"], i = 0, f = Number(n);
|
|
474
|
+
while (f >= 1024 && i < u.length - 1) { f /= 1024; i++; }
|
|
475
|
+
return i === 0 ? f + " B" : f.toFixed(f < 10 ? 2 : 1) + " " + u[i];
|
|
476
|
+
}
|
|
477
|
+
function pct(a, b) { if (!b) return "–"; var p = a / b * 100; return (p < 0.1 && p > 0 ? "<0.1" : p.toFixed(p < 10 ? 1 : 0)) + "%"; }
|
|
478
|
+
function ratio(us, cs) { return cs ? (us / cs).toFixed(2) + "×" : "–"; }
|
|
479
|
+
function plural(n, w) { return num(n) + " " + w + (n === 1 ? "" : "s"); }
|
|
480
|
+
var dark = window.matchMedia && matchMedia("(prefers-color-scheme: dark)");
|
|
481
|
+
function isDark() { return !!(dark && dark.matches); }
|
|
482
|
+
function cssVar(name) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); }
|
|
483
|
+
|
|
484
|
+
// ---------- colours ----------
|
|
485
|
+
var HUES = [215, 28, 150, 330, 262, 45, 180, 0, 95, 300, 195, 60];
|
|
486
|
+
function colColor(c, dict, ignoreSel) {
|
|
487
|
+
var hue = HUES[c % HUES.length], d = isDark();
|
|
488
|
+
if (ignoreSel || S.col == null) return d ? "hsl(" + hue + " 32% " + (dict ? 30 : 46) + "%)" : "hsl(" + hue + " 45% " + (dict ? 87 : 74) + "%)";
|
|
489
|
+
if (c === S.col) return d ? "hsl(" + hue + " 60% " + (dict ? 40 : 60) + "%)" : "hsl(" + hue + " 60% " + (dict ? 80 : 60) + "%)";
|
|
490
|
+
return dict ? (d ? "#22272f" : "#eef0f3") : (d ? "#2c323c" : "#dfe2e6");
|
|
491
|
+
}
|
|
492
|
+
var KINDS = {
|
|
493
|
+
dict: { label: "Dictionary page", v: "--k-dict", what: "Each distinct value stored once; the data pages after it store small integer ids that point here." },
|
|
494
|
+
data: { label: "Data page", v: "--k-data", what: "The unit a reader fetches and decompresses: a page header followed by levels and values." },
|
|
495
|
+
chunk: { label: "Column chunk", v: "--k-data", what: "Pages of this chunk were not listed individually." },
|
|
496
|
+
ci: { label: "Column index", v: "--k-ci", what: "Min, max and null count for every page of one column chunk, so readers can skip pages that can't match a filter." },
|
|
497
|
+
oi: { label: "Offset index", v: "--k-oi", what: "Byte offset and first row of every page of one column chunk, so readers can jump straight to a row." },
|
|
498
|
+
bloom: { label: "Bloom filter", v: "--k-bloom", what: "Answers \"is this value definitely absent?\" so = and IN lookups can skip a whole row group." },
|
|
499
|
+
footer: { label: "Footer", v: "--k-footer", what: "Thrift-encoded FileMetaData: the schema, every row group and column chunk, their statistics and the offsets of everything else." },
|
|
500
|
+
flen: { label: "Footer length", v: "--k-flen", what: "4-byte little-endian length of the footer. Readers fetch the last 8 bytes first to find it." },
|
|
501
|
+
magic: { label: "PAR1 magic", v: "--k-magic", what: "4 bytes that open and close every Parquet file." },
|
|
502
|
+
cmeta: { label: "Column metadata copy", v: "--k-cmeta", what: "A copy of the chunk's ColumnMetaData written right after its pages (ColumnChunk.file_offset points here). Readers use the footer's copy." },
|
|
503
|
+
unknown: { label: "Unaccounted bytes", v: "--k-unknown", what: "Bytes not covered by any page, index or footer (padding, or data the metadata doesn't describe)." }
|
|
504
|
+
};
|
|
505
|
+
function kindColor(k) { return cssVar(KINDS[k].v); }
|
|
506
|
+
function segColor(s) {
|
|
507
|
+
if (s.k === "data" || s.k === "chunk") return colColor(s.c, false);
|
|
508
|
+
if (s.k === "dict") return colColor(s.c, true);
|
|
509
|
+
if ((s.k === "ci" || s.k === "oi" || s.k === "bloom" || s.k === "cmeta") && S.col != null && s.c !== S.col) return isDark() ? "#2c323c" : "#dfe2e6";
|
|
510
|
+
return kindColor(s.k);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ---------- byte segments ----------
|
|
514
|
+
var PT = ["DATA_PAGE", "INDEX_PAGE", "DICTIONARY_PAGE", "DATA_PAGE_V2"];
|
|
515
|
+
var CRC = ["", "present, not verified", "ok", "MISMATCH"];
|
|
516
|
+
var segs = [{ k: "magic", s: 0, e: 4 }];
|
|
517
|
+
D.row_groups.forEach(function (rg) {
|
|
518
|
+
rg.chunks.forEach(function (ch) {
|
|
519
|
+
if (ch.pages && ch.pages.length) {
|
|
520
|
+
ch.pages.forEach(function (p, i) {
|
|
521
|
+
segs.push({ k: p[0] === 2 ? "dict" : "data", s: p[1], e: p[1] + p[2] + p[3], rg: rg.i, c: ch.c, p: i });
|
|
522
|
+
});
|
|
523
|
+
} else if (ch.cs > 0) {
|
|
524
|
+
segs.push({ k: "chunk", s: ch.start, e: ch.end, rg: rg.i, c: ch.c });
|
|
525
|
+
}
|
|
526
|
+
if (ch.bloom && ch.bloom[1]) segs.push({ k: "bloom", s: ch.bloom[0], e: ch.bloom[0] + ch.bloom[1], rg: rg.i, c: ch.c });
|
|
527
|
+
if (ch.ci) segs.push({ k: "ci", s: ch.ci[0], e: ch.ci[0] + ch.ci[1], rg: rg.i, c: ch.c });
|
|
528
|
+
if (ch.oi) segs.push({ k: "oi", s: ch.oi[0], e: ch.oi[0] + ch.oi[1], rg: rg.i, c: ch.c });
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
segs.push({ k: "footer", s: F.footer_offset, e: F.footer_offset + F.footer_size });
|
|
532
|
+
segs.push({ k: "flen", s: F.file_size - 8, e: F.file_size - 4 });
|
|
533
|
+
segs.push({ k: "magic", s: F.file_size - 4, e: F.file_size });
|
|
534
|
+
segs.sort(function (a, b) { return a.s - b.s || a.e - b.e; });
|
|
535
|
+
(function () {
|
|
536
|
+
// gaps starting at a chunk's file_offset hold an inline copy of its ColumnMetaData
|
|
537
|
+
var metaAt = {};
|
|
538
|
+
D.row_groups.forEach(function (rg) { rg.chunks.forEach(function (ch) { if (ch.file_offset != null && ch.file_offset >= ch.end) metaAt[ch.file_offset] = { rg: rg.i, c: ch.c }; }); });
|
|
539
|
+
var out = [], pos = 0;
|
|
540
|
+
segs.forEach(function (s) {
|
|
541
|
+
if (s.s > pos) { var m = metaAt[pos]; out.push(m ? { k: "cmeta", s: pos, e: s.s, rg: m.rg, c: m.c } : { k: "unknown", s: pos, e: s.s }); }
|
|
542
|
+
out.push(s); pos = Math.max(pos, s.e);
|
|
543
|
+
});
|
|
544
|
+
segs = out;
|
|
545
|
+
})();
|
|
546
|
+
var dataEnd = 4;
|
|
547
|
+
segs.forEach(function (s) { if (s.k === "data" || s.k === "dict" || s.k === "chunk") dataEnd = Math.max(dataEnd, s.e); });
|
|
548
|
+
var tailStart = F.footer_offset;
|
|
549
|
+
segs.forEach(function (s) { if (s.s >= dataEnd && s.s < tailStart && s.k !== "unknown") tailStart = s.s; });
|
|
550
|
+
var bodySegs = segs.filter(function (s) { return s.s < tailStart; });
|
|
551
|
+
var tailSegs = segs.filter(function (s) { return s.s >= tailStart; });
|
|
552
|
+
function lowerBound(list, off) { var lo = 0, hi = list.length; while (lo < hi) { var m = (lo + hi) >> 1; if (list[m].e <= off) lo = m + 1; else hi = m; } return lo; }
|
|
553
|
+
function segsIn(list, from, to) { var out = []; for (var i = lowerBound(list, from); i < list.length && list[i].s < to; i++) out.push(list[i]); return out; }
|
|
554
|
+
|
|
555
|
+
// ---------- canvas strips ----------
|
|
556
|
+
var strips = [];
|
|
557
|
+
function Strip(host, from, to, list, opts) {
|
|
558
|
+
this.host = host; this.from = from; this.to = Math.max(to, from + 1); this.list = list; this.opts = opts || {};
|
|
559
|
+
this.canvas = h("canvas", { "aria-label": this.opts.label || "byte layout" });
|
|
560
|
+
host.appendChild(this.canvas);
|
|
561
|
+
var self = this;
|
|
562
|
+
this.canvas.addEventListener("pointermove", function (ev) { if (ev.pointerType !== "mouse") return; var s = self.at(ev); self.hover = s; self.paint(); showPop(s, ev); });
|
|
563
|
+
this.canvas.addEventListener("pointerleave", function () { self.hover = null; self.paint(); hidePop(); });
|
|
564
|
+
this.canvas.addEventListener("click", function (ev) { var s = self.at(ev); if (self.opts.onpick) self.opts.onpick(s, ev); else showPop(s, ev); });
|
|
565
|
+
strips.push(this);
|
|
566
|
+
this.render();
|
|
567
|
+
}
|
|
568
|
+
Strip.prototype.at = function (ev) {
|
|
569
|
+
var r = this.canvas.getBoundingClientRect();
|
|
570
|
+
var span = this.to - this.from, off = this.from + (ev.clientX - r.left) / r.width * span, slack = span / r.width * 2;
|
|
571
|
+
var list = this.list, lo = 0, hi = list.length - 1, best = -1;
|
|
572
|
+
while (lo <= hi) { var m = (lo + hi) >> 1; if (list[m].s <= off) { best = m; lo = m + 1; } else hi = m - 1; }
|
|
573
|
+
var hit = list[best];
|
|
574
|
+
if (hit && off <= hit.e + slack) return hit;
|
|
575
|
+
var next = list[best + 1];
|
|
576
|
+
return next && next.s - off < slack ? next : null;
|
|
577
|
+
};
|
|
578
|
+
Strip.prototype.render = function () {
|
|
579
|
+
var w = this.host.clientWidth, hh = this.host.clientHeight;
|
|
580
|
+
if (!w || !hh) return;
|
|
581
|
+
var dpr = window.devicePixelRatio || 1, W = Math.round(w * dpr), H = Math.round(hh * dpr);
|
|
582
|
+
var base = document.createElement("canvas"); base.width = W; base.height = H;
|
|
583
|
+
var ctx = base.getContext("2d"), from = this.from, to = this.to, list = this.list;
|
|
584
|
+
ctx.fillStyle = cssVar("--strip-bg"); ctx.fillRect(0, 0, W, H);
|
|
585
|
+
// each device pixel takes the colour of the segment covering most of its bytes
|
|
586
|
+
var bpp = (to - from) / W, first = 0, runStart = 0, runColor = null;
|
|
587
|
+
for (var px = 0; px < W; px++) {
|
|
588
|
+
var b0 = from + px * bpp, b1 = b0 + bpp, best = null, bestBytes = 0;
|
|
589
|
+
while (first < list.length && list[first].e <= b0) first++;
|
|
590
|
+
for (var i = first; i < list.length && list[i].s < b1; i++) {
|
|
591
|
+
var sg = list[i], cov = Math.min(b1, sg.e) - Math.max(b0, sg.s);
|
|
592
|
+
if (cov > bestBytes) { bestBytes = cov; best = sg; }
|
|
593
|
+
}
|
|
594
|
+
var col = best ? segColor(best) : null;
|
|
595
|
+
if (col !== runColor) { if (runColor) { ctx.fillStyle = runColor; ctx.fillRect(runStart, 0, px - runStart, H); } runStart = px; runColor = col; }
|
|
596
|
+
}
|
|
597
|
+
if (runColor) { ctx.fillStyle = runColor; ctx.fillRect(runStart, 0, W - runStart, H); }
|
|
598
|
+
ctx.fillStyle = cssVar("--seam");
|
|
599
|
+
var x = function (o) { return (o - from) / (to - from) * W; };
|
|
600
|
+
for (var j = 0; j < list.length; j++) { var a = x(list[j].s), b = x(list[j].e); if (b - a > 4 * dpr) ctx.fillRect(a, 0, dpr, H); }
|
|
601
|
+
if (this.opts.targets) {
|
|
602
|
+
ctx.fillStyle = cssVar("--bg");
|
|
603
|
+
this.opts.targets.forEach(function (t) { if (x(t.e) - x(t.s) > 3 * dpr) ctx.fillRect(x(t.s), 0, 1.5 * dpr, H); });
|
|
604
|
+
}
|
|
605
|
+
this.base = base; this.dpr = dpr;
|
|
606
|
+
this.paint();
|
|
607
|
+
};
|
|
608
|
+
Strip.prototype.paint = function () {
|
|
609
|
+
if (!this.base) return;
|
|
610
|
+
var c = this.canvas, dpr = this.dpr, ctx = c.getContext("2d");
|
|
611
|
+
c.width = this.base.width; c.height = this.base.height;
|
|
612
|
+
ctx.drawImage(this.base, 0, 0);
|
|
613
|
+
var from = this.from, to = this.to, W = c.width, H = c.height;
|
|
614
|
+
var x = function (o) { return (o - from) / (to - from) * W; };
|
|
615
|
+
ctx.lineWidth = 2 * dpr;
|
|
616
|
+
var outline = function (s, color) { if (!s) return; ctx.strokeStyle = color; ctx.strokeRect(Math.max(0, x(s.s)) + dpr, dpr, Math.max(Math.min(W, x(s.e)) - Math.max(0, x(s.s)) - 2 * dpr, 2 * dpr), H - 2 * dpr); };
|
|
617
|
+
outline(this.opts.selected && this.opts.selected(), cssVar("--accent"));
|
|
618
|
+
outline(this.hover, cssVar("--outline"));
|
|
619
|
+
};
|
|
620
|
+
function redrawStrips() { strips = strips.filter(function (s) { return s.canvas.isConnected; }); strips.forEach(function (s) { s.render(); }); }
|
|
621
|
+
var rsTimer;
|
|
622
|
+
window.addEventListener("resize", function () { clearTimeout(rsTimer); rsTimer = setTimeout(redrawStrips, 60); });
|
|
623
|
+
if (dark && dark.addEventListener) dark.addEventListener("change", function () { renderAll(); });
|
|
624
|
+
|
|
625
|
+
// ---------- popover ----------
|
|
626
|
+
var pop = document.getElementById("pop");
|
|
627
|
+
function colPath(c) { return D.columns[c] ? D.columns[c].path : "#" + c; }
|
|
628
|
+
function chunkOf(rg, c) { var g = D.row_groups[rg]; return g && g.chunks.find(function (x) { return x.c === c; }); }
|
|
629
|
+
function showPop(s, ev) {
|
|
630
|
+
if (!s) { hidePop(); return; }
|
|
631
|
+
var K = KINDS[s.k], rows = [];
|
|
632
|
+
rows.push(["Bytes", num(s.s) + " – " + num(s.e)]);
|
|
633
|
+
rows.push(["Size", bytes(s.e - s.s) + " · " + pct(s.e - s.s, F.file_size) + " of file"]);
|
|
634
|
+
var ch = s.rg != null ? chunkOf(s.rg, s.c) : null;
|
|
635
|
+
if (ch && (s.k === "data" || s.k === "dict") && ch.pages) {
|
|
636
|
+
var p = ch.pages[s.p];
|
|
637
|
+
rows.push(["Header", bytes(p[2])]);
|
|
638
|
+
rows.push(["Body", bytes(p[3]) + " (" + bytes(p[4]) + " uncompressed)"]);
|
|
639
|
+
rows.push([s.k === "dict" ? "Entries" : "Values", num(p[5]) + (p[6] != null ? " · " + num(p[6]) + " nulls" : "")]);
|
|
640
|
+
if (p[7] != null && s.k !== "dict") rows.push(["Rows", num(p[7]) + (p[8] != null ? " from row " + num(p[8]) : "")]);
|
|
641
|
+
rows.push(["Encoding", p[9] || "–"]);
|
|
642
|
+
if (p[12]) rows.push(["CRC", CRC[p[12]]]);
|
|
643
|
+
if (p[10] != null || p[11] != null) rows.push(["Min … max", (p[10] || "–") + " … " + (p[11] || "–")]);
|
|
644
|
+
}
|
|
645
|
+
if (ch && (s.k === "data" || s.k === "dict" || s.k === "chunk")) {
|
|
646
|
+
rows.push(["Codec", ch.codec]);
|
|
647
|
+
if (s.k === "chunk") rows.push(["Encodings", ch.enc.join(", ")]);
|
|
648
|
+
}
|
|
649
|
+
clear(pop);
|
|
650
|
+
add(pop, [
|
|
651
|
+
h("div", { class: "t" }, h("span", { class: "sw-inline", style: "background:" + segColor(s) }), K.label + (s.p != null ? " " + s.p : "")),
|
|
652
|
+
ch || s.c != null ? h("div", { class: "p" }, colPath(s.c) + " · row group " + s.rg) : null,
|
|
653
|
+
h("div", { class: "w" }, K.what),
|
|
654
|
+
h("dl", null, rows.map(function (r) { return [h("dt", { text: r[0] }), h("dd", { text: r[1] })]; }))
|
|
655
|
+
]);
|
|
656
|
+
pop.style.display = "block";
|
|
657
|
+
var pw = pop.offsetWidth, ph = pop.offsetHeight, px = ev.clientX + 14, py = ev.clientY + 16;
|
|
658
|
+
if (px + pw > innerWidth - 8) px = Math.max(8, ev.clientX - pw - 14);
|
|
659
|
+
if (py + ph > innerHeight - 8) py = Math.max(8, ev.clientY - ph - 12);
|
|
660
|
+
pop.style.left = px + "px"; pop.style.top = py + "px";
|
|
661
|
+
}
|
|
662
|
+
function hidePop() { pop.style.display = "none"; }
|
|
663
|
+
|
|
664
|
+
// ---------- state changes ----------
|
|
665
|
+
function selectRg(i, toggle) { S.rg = toggle && S.rg === i ? null : i; S.page = null; S.pageOffset = 0; renderAll(); }
|
|
666
|
+
function selectCol(c, toggle) { S.col = toggle && S.col === c ? null : c; S.page = null; S.pageOffset = 0; renderAll(); }
|
|
667
|
+
function selectChunk(rg, c, page) {
|
|
668
|
+
S.rg = rg; S.col = c; S.page = page == null ? null : page; S.tab = "pages";
|
|
669
|
+
S.pageOffset = page == null ? 0 : Math.floor(page / PAGE_SIZE) * PAGE_SIZE;
|
|
670
|
+
renderAll();
|
|
671
|
+
var d = document.getElementById("detail"); if (d && d.scrollIntoView) d.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// ---------- summary ----------
|
|
675
|
+
function renderSummary() {
|
|
676
|
+
var el = clear(document.getElementById("summary"));
|
|
677
|
+
var dicts = D.columns.some(function (c) { return c.dict_pages > 0; });
|
|
678
|
+
var stats = D.row_groups.some(function (g) { return g.chunks.some(function (c) { return c.stats && (c.stats.min != null || c.stats.max != null); }); });
|
|
679
|
+
var crc = D.row_groups.some(function (g) { return g.chunks.some(function (c) { return c.pages && c.pages.some(function (p) { return p[12]; }); }); });
|
|
680
|
+
var sorting = (D.row_groups[0] && D.row_groups[0].sorting) || [];
|
|
681
|
+
var sortedCols = sortedAcrossRowGroups();
|
|
682
|
+
function badge(ok, label, extra) { return h("span", { class: "badge" + (ok ? " ok" : "") }, h("span", { "aria-hidden": "true", text: ok ? "✓" : "–" }), h("span", { text: label }), extra ? h("code", { text: extra }) : null); }
|
|
683
|
+
function flag(cls, label, extra) { return h("span", { class: "badge " + cls }, h("span", { "aria-hidden": "true", text: "✗" }), h("span", { text: label }), extra ? h("code", { text: extra }) : null); }
|
|
684
|
+
var cs = F.checksums, crcBadge;
|
|
685
|
+
if (cs && cs.mismatch) crcBadge = flag("err", "Page CRC mismatches", num(cs.mismatch) + " of " + num(cs.ok + cs.mismatch));
|
|
686
|
+
else if (cs && cs.ok) crcBadge = badge(true, "Page CRCs verified", num(cs.ok) + " ok" + (cs.absent ? ", " + num(cs.absent) + " without" : ""));
|
|
687
|
+
else crcBadge = badge(crc, "Page CRCs", crc ? "not verified" : null);
|
|
688
|
+
add(el, [
|
|
689
|
+
h("header", null, h("h2", { text: F.name }), h("span", { class: "subtle" }, "created by ", h("span", { class: "mono", text: F.created_by || "unknown" }))),
|
|
690
|
+
h("dl", { class: "stats" },
|
|
691
|
+
[["Size", bytes(F.file_size)], ["Rows", num(F.num_rows)], ["Row groups", num(F.num_row_groups)], ["Columns", num(F.num_columns)],
|
|
692
|
+
["Footer", bytes(F.footer_size)], ["Compression", ratio(F.uncompressed_size, F.compressed_size)]].map(function (p) {
|
|
693
|
+
return h("div", null, h("dt", { text: p[0] }), h("dd", { class: "num", text: p[1] }));
|
|
694
|
+
}),
|
|
695
|
+
h("div", { class: "wide" }, h("dt", { text: "Codecs · format version" }), h("dd", { class: "mono", text: (F.codecs.join(", ") || "none") + " · v" + F.format_version }))),
|
|
696
|
+
h("div", { class: "badges" },
|
|
697
|
+
badge(F.page_index, "Page index"),
|
|
698
|
+
badge(F.bloom_filters, "Bloom filters"),
|
|
699
|
+
badge(stats, "Statistics"),
|
|
700
|
+
badge(dicts, "Dictionary encoding"),
|
|
701
|
+
crcBadge,
|
|
702
|
+
F.index_mismatches ? flag("warn", "Page stats disagree with page index", num(F.index_mismatches)) : null,
|
|
703
|
+
badge(sorting.length > 0, "Sorting columns", sorting.length ? sorting.map(function (s) { return s.column + (s.descending ? " desc" : ""); }).join(", ") : null),
|
|
704
|
+
badge(sortedCols.length > 0, "Sorted across row groups", sortedCols.length ? (sortedCols.length > 2 ? sortedCols[0] + " +" + (sortedCols.length - 1) : sortedCols.join(", ")) : null),
|
|
705
|
+
F.pages_truncated ? h("span", { class: "badge" }, "page detail capped for size") : null)
|
|
706
|
+
]);
|
|
707
|
+
}
|
|
708
|
+
// Columns whose row group [min, max] ranges don't overlap, in order (can't compare display strings reliably, so only numbers)
|
|
709
|
+
function sortedAcrossRowGroups() {
|
|
710
|
+
if (D.row_groups.length < 2) return [];
|
|
711
|
+
return D.columns.filter(function (col) {
|
|
712
|
+
var prev = null;
|
|
713
|
+
for (var g = 0; g < D.row_groups.length; g++) {
|
|
714
|
+
var ch = chunkOf(g, col.i); if (!ch || !ch.stats) return false;
|
|
715
|
+
var lo = Number(ch.stats.min), hi = Number(ch.stats.max);
|
|
716
|
+
if (!isFinite(lo) || !isFinite(hi)) return false;
|
|
717
|
+
if (prev != null && lo < prev) return false;
|
|
718
|
+
prev = hi;
|
|
719
|
+
}
|
|
720
|
+
return true;
|
|
721
|
+
}).map(function (c) { return c.path; });
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// ---------- file map ----------
|
|
725
|
+
function renderMap() {
|
|
726
|
+
var el = clear(document.getElementById("map"));
|
|
727
|
+
var rgTargets = D.row_groups.map(function (g) { return { s: g.start, e: g.end, rg: g.i }; });
|
|
728
|
+
var bodyHost = h("div", { class: "strip pick" }), tailHost = h("div", { class: "strip" });
|
|
729
|
+
add(el, h("div", { class: "map-grid" },
|
|
730
|
+
h("div", null, h("div", { class: "strip-label" }, h("span", null, h("b", { text: "File" }), " · " + plural(D.row_groups.length, "row group") + " · click one to open it"), h("span", { text: bytes(tailStart) })), bodyHost),
|
|
731
|
+
h("div", null, h("div", { class: "strip-label" }, h("span", null, h("b", { text: F.page_index || F.bloom_filters ? "Indexes + footer" : "Footer" }), " magnified"), h("span", { text: bytes(F.file_size - tailStart) })), tailHost)));
|
|
732
|
+
new Strip(bodyHost, 0, tailStart, bodySegs, {
|
|
733
|
+
label: "File layout", targets: rgTargets,
|
|
734
|
+
selected: function () { var g = S.rg != null && D.row_groups[S.rg]; return g ? { s: g.start, e: g.end } : null; },
|
|
735
|
+
onpick: function (s) {
|
|
736
|
+
if (!s) return;
|
|
737
|
+
var rg = s.rg; if (rg == null) { var t = rgTargets.find(function (t) { return s.s >= t.s && s.s < t.e; }); rg = t ? t.rg : null; }
|
|
738
|
+
if (rg != null) { hidePop(); selectRg(rg, true); }
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
new Strip(tailHost, tailStart, F.file_size, tailSegs, { label: "Indexes and footer",
|
|
742
|
+
onpick: function (s, ev) { if (s && s.rg != null && s.c != null) { hidePop(); selectChunk(s.rg, s.c); } else showPop(s, ev); } });
|
|
743
|
+
if (S.rg != null && D.row_groups[S.rg]) {
|
|
744
|
+
var g = D.row_groups[S.rg], host = h("div", { class: "strip pick" });
|
|
745
|
+
add(el, h("div", { class: "zoom" }, h("div", { class: "strip-label" }, h("span", null, h("b", { text: "Row group " + g.i }), " · bytes " + num(g.start) + "–" + num(g.end) + " · click a page to open its column chunk"), h("span", { text: bytes(g.end - g.start) })), host));
|
|
746
|
+
new Strip(host, g.start, g.end, segsIn(bodySegs, g.start, g.end), { label: "Row group layout",
|
|
747
|
+
selected: function () { var ch = S.col != null && chunkOf(S.rg, S.col); return ch ? { s: ch.start, e: ch.end } : null; },
|
|
748
|
+
onpick: function (s) { if (s && s.c != null) { hidePop(); selectChunk(s.rg, s.c, s.p); } } });
|
|
749
|
+
var ch = S.col != null ? chunkOf(S.rg, S.col) : null;
|
|
750
|
+
if (ch && ch.pages && ch.pages.length > 1) {
|
|
751
|
+
var host2 = h("div", { class: "strip pick" });
|
|
752
|
+
add(el, h("div", { class: "zoom" }, h("div", { class: "strip-label" }, h("span", null, h("b", { text: colPath(ch.c) }), " in row group " + g.i + " · " + plural(ch.pages.length, "page")), h("span", { text: bytes(ch.end - ch.start) })), host2));
|
|
753
|
+
new Strip(host2, ch.start, ch.end, segsIn(bodySegs, ch.start, ch.end).filter(function (s) { return s.c === ch.c; }), { label: "Column chunk pages",
|
|
754
|
+
selected: function () { if (S.page == null || !ch.pages[S.page]) return null; var p = ch.pages[S.page]; return { s: p[1], e: p[1] + p[2] + p[3] }; },
|
|
755
|
+
onpick: function (s) { if (s && s.p != null) { hidePop(); selectChunk(s.rg, s.c, s.p); } } });
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
var legend = h("div", { class: "legend" });
|
|
759
|
+
D.columns.slice(0, 16).forEach(function (c) {
|
|
760
|
+
add(legend, h("span", { class: "item" + (S.col != null && S.col !== c.i ? " dim" : ""), onclick: function () { selectCol(c.i, true); } },
|
|
761
|
+
h("span", { class: "sw", style: "background:" + colColor(c.i, false, true) }), h("span", { class: "mono", text: c.path })));
|
|
762
|
+
});
|
|
763
|
+
if (D.columns.length > 16) add(legend, h("span", { class: "faint", text: "+" + num(D.columns.length - 16) + " more" }));
|
|
764
|
+
var kinds = h("span", { class: "kinds" });
|
|
765
|
+
["dict", "data", "ci", "oi", "bloom", "footer"].forEach(function (k) {
|
|
766
|
+
if ((k === "ci" || k === "oi") && !F.page_index) return;
|
|
767
|
+
if (k === "bloom" && !F.bloom_filters) return;
|
|
768
|
+
var color = k === "dict" ? (isDark() ? "#394150" : "#e2e5ea") : k === "data" ? (isDark() ? "#566072" : "#b8bec8") : kindColor(k);
|
|
769
|
+
add(kinds, h("span", null, h("span", { class: "sw", style: "background:" + color }), KINDS[k].label.toLowerCase()));
|
|
770
|
+
});
|
|
771
|
+
if (segs.some(function (s) { return s.k === "cmeta"; })) add(kinds, h("span", null, h("span", { class: "sw", style: "background:" + kindColor("cmeta") }), "column metadata copy"));
|
|
772
|
+
if (segs.some(function (s) { return s.k === "unknown" && s.e - s.s > 0; })) add(kinds, h("span", null, h("span", { class: "sw", style: "background:" + kindColor("unknown") }), "unaccounted"));
|
|
773
|
+
add(legend, kinds);
|
|
774
|
+
add(el, legend);
|
|
775
|
+
add(el, h("p", { class: "hint", text: "Drawn to scale by compressed bytes; each pixel shows whatever covers most of its bytes. Hover for details, click to drill down." }));
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// ---------- schema ----------
|
|
779
|
+
function renderSchema() {
|
|
780
|
+
var el = clear(document.getElementById("schema"));
|
|
781
|
+
var total = D.columns.reduce(function (a, c) { return a + c.cs; }, 0) || 1;
|
|
782
|
+
var lines = [];
|
|
783
|
+
lines.push({ depth: 0, html: [h("span", { class: "kw", text: "message " }), h("span", { class: "nm", text: "schema" }), " {"] });
|
|
784
|
+
function walk(n, depth) {
|
|
785
|
+
var ann = n.logical_type || n.converted_type;
|
|
786
|
+
if (n.children) {
|
|
787
|
+
lines.push({ depth: depth, html: [h("span", { class: "kw", text: n.repetition + " " }), h("span", { class: "ty", text: "group " }), h("span", { class: "nm", text: n.name }), ann ? h("span", { class: "ann", text: " (" + ann + ")" }) : null, " {", arrowNote(n)] });
|
|
788
|
+
n.children.forEach(function (c) { walk(c, depth + 1); });
|
|
789
|
+
lines.push({ depth: depth, html: ["}"] });
|
|
790
|
+
} else {
|
|
791
|
+
var phys = n.physical_type.toLowerCase() + (n.type_length ? "(" + n.type_length + ")" : "");
|
|
792
|
+
lines.push({ depth: depth, leaf: n.column, html: [h("span", { class: "kw", text: n.repetition + " " }), h("span", { class: "ty", text: phys + " " }), h("span", { class: "nm", text: n.name }), ann ? h("span", { class: "ann", text: " (" + ann + ")" }) : null, ";",
|
|
793
|
+
h("span", { class: "lv", text: " d" + n.max_definition_level + " r" + n.max_repetition_level }), arrowNote(n)] });
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function arrowNote(n) { return n.arrow_type ? h("span", { class: "arrow", title: "Arrow type (from ARROW:schema): " + n.arrow_type, text: " // arrow: " + n.arrow_type }) : null; }
|
|
797
|
+
D.schema.forEach(function (n) { walk(n, 1); });
|
|
798
|
+
lines.push({ depth: 0, html: ["}"] });
|
|
799
|
+
var box = h("div", { class: "schema" });
|
|
800
|
+
var limit = S.allSchema ? lines.length : 400;
|
|
801
|
+
lines.slice(0, limit).forEach(function (l, n) {
|
|
802
|
+
var code = h("span", { class: "code", style: "padding-left:" + (l.depth * 2) + "ch" }, l.html);
|
|
803
|
+
if (l.leaf == null) { add(box, h("div", { class: "sline" }, h("span", { class: "ln", text: n + 1 }), code)); return; }
|
|
804
|
+
var c = D.columns[l.leaf];
|
|
805
|
+
add(box, h("button", { type: "button", class: "sline" + (S.col === c.i ? " sel" : ""), title: c.type + " · max definition level " + c.max_def + ", max repetition level " + c.max_rep, onclick: function () { selectCol(c.i, true); } },
|
|
806
|
+
h("span", { class: "ln", text: n + 1 }), code,
|
|
807
|
+
h("span", { class: "size" }, h("span", { class: "bar" }, h("span", { style: "width:" + (c.cs / total * 100) + "%;background:" + colColor(c.i, false) })), h("span", { class: "v", text: bytes(c.cs) }))));
|
|
808
|
+
});
|
|
809
|
+
var hasArrow = D.schema.some(function (n) { return n.arrow_type; });
|
|
810
|
+
add(el, [h("h2", null, "Schema", h("small", { text: "size on disk · d/r = max definition/repetition level" + (hasArrow ? " · Arrow types from ARROW:schema" : "") + " · click a column" })), box]);
|
|
811
|
+
if (D.arrow_error) add(el, h("div", { class: "note", text: D.arrow_error }));
|
|
812
|
+
if (lines.length > limit) add(el, h("button", { class: "more", onclick: function () { S.allSchema = true; renderSchema(); } }, "Show all " + num(lines.length) + " lines"));
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// ---------- row groups ----------
|
|
816
|
+
function renderRowGroups() {
|
|
817
|
+
var el = clear(document.getElementById("rowgroups"));
|
|
818
|
+
var maxSize = Math.max.apply(null, D.row_groups.map(function (g) { return g.end - g.start; }).concat([1]));
|
|
819
|
+
var box = h("div", { class: "card rgs" });
|
|
820
|
+
var shown = S.allRgs ? D.row_groups : D.row_groups.slice(0, 100);
|
|
821
|
+
if (!D.row_groups.length) add(box, h("div", { class: "pad faint", text: "No row groups: the file has no data." }));
|
|
822
|
+
shown.forEach(function (g) {
|
|
823
|
+
var open = S.rg === g.i;
|
|
824
|
+
add(box, h("button", { type: "button", class: "rg-row" + (open ? " open" : ""), "aria-expanded": open ? "true" : "false", onclick: function () { selectRg(g.i, true); } },
|
|
825
|
+
h("span", { class: "caret", text: open ? "▾" : "▸" }), h("span", { class: "mono", text: g.i }),
|
|
826
|
+
h("span", { class: "mono faint", text: g.rows ? num(g.first_row) + "–" + num(g.first_row + g.rows - 1) : "0 rows" }),
|
|
827
|
+
h("span", { class: "gcell" }, h("span", { class: "gbar", style: "width:" + Math.max(1, (g.end - g.start) / maxSize * 100) + "%;background:" + gradient(g) })),
|
|
828
|
+
h("span", { class: "mono", style: "text-align:right", text: bytes(g.end - g.start) })));
|
|
829
|
+
if (open) add(box, rowGroupBody(g));
|
|
830
|
+
});
|
|
831
|
+
add(el, [h("h2", null, "Row groups", h("small", { text: num(D.row_groups.length) + " · click one, or click it in the file map" })), box]);
|
|
832
|
+
if (shown.length < D.row_groups.length) add(el, h("button", { class: "more", onclick: function () { S.allRgs = true; renderRowGroups(); } }, "Show all " + num(D.row_groups.length) + " row groups"));
|
|
833
|
+
}
|
|
834
|
+
function gradient(g) {
|
|
835
|
+
var total = g.chunks.reduce(function (a, c) { return a + (c.end - c.start); }, 0) || 1, at = 0, stops = [];
|
|
836
|
+
var step = Math.max(1, Math.ceil(g.chunks.length / 300));
|
|
837
|
+
for (var i = 0; i < g.chunks.length; i += step) {
|
|
838
|
+
var sz = 0; for (var j = i; j < Math.min(i + step, g.chunks.length); j++) sz += g.chunks[j].end - g.chunks[j].start;
|
|
839
|
+
var from = at / total * 100; at += sz;
|
|
840
|
+
stops.push(colColor(g.chunks[i].c, false) + " " + from + "% " + (at / total * 100) + "%");
|
|
841
|
+
}
|
|
842
|
+
return stops.length ? "linear-gradient(to right," + stops.join(",") + ")" : "var(--line)";
|
|
843
|
+
}
|
|
844
|
+
function rowGroupBody(g) {
|
|
845
|
+
var body = h("div", { class: "rg-body" });
|
|
846
|
+
add(body, h("div", { class: "kvline" },
|
|
847
|
+
[["rows", num(g.rows)], ["compressed", bytes(g.cs)], ["uncompressed", bytes(g.us)], ["bytes", num(g.start) + "–" + num(g.end)],
|
|
848
|
+
g.sorting.length ? ["sorted by", g.sorting.map(function (s) { return s.column + (s.descending ? " desc" : "") + (s.nulls_first ? " nulls first" : ""); }).join(", ")] : null]
|
|
849
|
+
.filter(Boolean).map(function (p) { return h("span", null, h("span", { class: "k", text: p[0] }), h("span", { class: "mono", text: p[1] })); })));
|
|
850
|
+
var maxChunk = Math.max.apply(null, g.chunks.map(function (c) { return c.end - c.start; }).concat([1]));
|
|
851
|
+
var chunks = g.chunks.length > 200 && !S.allChunkRgs ? g.chunks.slice(0, 200) : g.chunks;
|
|
852
|
+
chunks.forEach(function (ch) {
|
|
853
|
+
var pages = h("span", { class: "pages", style: "width:" + Math.max(6, (ch.end - ch.start) / maxChunk * 100) + "%" });
|
|
854
|
+
if (ch.pages && ch.pages.length <= 40) ch.pages.forEach(function (p) { add(pages, h("span", { style: "flex:" + (p[2] + p[3]) + " 0 0;background:" + colColor(ch.c, p[0] === 2) })); });
|
|
855
|
+
else {
|
|
856
|
+
// Too many pages to draw one by one: dictionary pages, then all data pages as one hatched block
|
|
857
|
+
var dictBytes = 0, dataBytes = 0;
|
|
858
|
+
(ch.pages || []).forEach(function (p) { if (p[0] === 2) dictBytes += p[2] + p[3]; else dataBytes += p[2] + p[3]; });
|
|
859
|
+
if (!ch.pages) dataBytes = ch.end - ch.start;
|
|
860
|
+
if (dictBytes) add(pages, h("span", { style: "flex:" + dictBytes + " 0 0;background:" + colColor(ch.c, true) }));
|
|
861
|
+
add(pages, h("span", { title: plural(ch.np, "page"), style: "flex:" + Math.max(1, dataBytes) + " 0 0;background:repeating-linear-gradient(90deg," + colColor(ch.c, false) + " 0 3px,var(--seam) 3px 4px)" }));
|
|
862
|
+
}
|
|
863
|
+
var info = [ch.np ? plural(ch.ndp, "page") : "no pages", ch.nulls ? num(ch.nulls) + " nulls" : null,
|
|
864
|
+
ch.crc_bad ? "✗ CRC" : null, ch.idx_mm ? "✗ index" : null].filter(Boolean).join(" · ");
|
|
865
|
+
add(body, h("div", { class: "chunk-row" + (S.col === ch.c ? " sel" : ""), onclick: function (ev) { ev.stopPropagation(); selectChunk(g.i, ch.c); } },
|
|
866
|
+
h("span", { class: "name", title: colPath(ch.c), text: colPath(ch.c) }), pages,
|
|
867
|
+
h("span", { class: "size", text: bytes(ch.cs) }),
|
|
868
|
+
h("span", { class: "minmax", text: ch.stats && (ch.stats.min != null || ch.stats.max != null) ? (ch.stats.min || "–") + " … " + (ch.stats.max || "–") : "no min/max" }),
|
|
869
|
+
h("span", { class: "info", text: info })));
|
|
870
|
+
});
|
|
871
|
+
if (chunks.length < g.chunks.length) add(body, h("button", { class: "more", onclick: function (ev) { ev.stopPropagation(); S.allChunkRgs = true; renderRowGroups(); } }, "Show all " + num(g.chunks.length) + " column chunks"));
|
|
872
|
+
return body;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// ---------- columns table ----------
|
|
876
|
+
function renderColumns() {
|
|
877
|
+
var el = clear(document.getElementById("columns"));
|
|
878
|
+
var maxRatio = Math.max.apply(null, D.columns.map(function (c) { return c.cs ? c.us / c.cs : 0; }).concat([1]));
|
|
879
|
+
var shown = S.allCols ? D.columns : D.columns.slice(0, 200);
|
|
880
|
+
var tb = h("tbody");
|
|
881
|
+
shown.forEach(function (c) {
|
|
882
|
+
var r = c.cs ? c.us / c.cs : 0;
|
|
883
|
+
add(tb, h("tr", { class: "click" + (S.col === c.i ? " sel" : ""), onclick: function () { selectCol(c.i, true); } },
|
|
884
|
+
h("td", { class: "mono clip", title: c.path }, h("span", { class: "sw-inline", style: "background:" + colColor(c.i, false, true) }), c.path),
|
|
885
|
+
h("td", { class: "mono", text: c.type }),
|
|
886
|
+
h("td", null, c.codecs.map(function (x) { return h("span", { class: "pill", text: x }); })),
|
|
887
|
+
h("td", null, c.encodings.map(function (x) { return h("span", { class: "pill", text: x }); })),
|
|
888
|
+
h("td", { class: "r num", text: bytes(c.cs) }),
|
|
889
|
+
h("td", { class: "r num", text: bytes(c.us) }),
|
|
890
|
+
h("td", null, h("span", { class: "ratio" }, h("span", { class: "bar" }, h("span", { style: "width:" + (r / maxRatio * 100) + "%;background:var(--accent)" })), h("span", { class: "num", text: ratio(c.us, c.cs) }))),
|
|
891
|
+
h("td", { class: "r num", text: num(c.values) }),
|
|
892
|
+
h("td", { class: "r num", text: num(c.nulls) }),
|
|
893
|
+
h("td", { class: "r num", text: num(c.data_pages) }),
|
|
894
|
+
h("td", { class: "r num", text: c.dict_pages ? bytes(c.dict_bytes) : "–" }),
|
|
895
|
+
h("td", { class: "mono clip", title: c.min || "", text: c.min != null ? c.min : "–" }),
|
|
896
|
+
h("td", { class: "mono clip", title: c.max || "", text: c.max != null ? c.max : "–" })));
|
|
897
|
+
});
|
|
898
|
+
var head = h("tr", null, ["column", "type", "codec", "encodings", "compressed", "uncompressed", "ratio", "values", "nulls", "data pages", "dictionary", "min", "max"].map(function (t, i) {
|
|
899
|
+
return h("th", { class: (i >= 4 && i <= 5) || (i >= 7 && i <= 10) ? "r" : null, text: t });
|
|
900
|
+
}));
|
|
901
|
+
add(el, [h("h2", null, "Columns", h("small", { text: "totals across all row groups · click a row for per-row-group and per-page detail" })),
|
|
902
|
+
h("div", { class: "card tablewrap" }, h("table", null, h("thead", null, head), tb))]);
|
|
903
|
+
if (shown.length < D.columns.length) add(el, h("button", { class: "more", onclick: function () { S.allCols = true; renderColumns(); } }, "Show all " + num(D.columns.length) + " columns"));
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// ---------- column / chunk detail ----------
|
|
907
|
+
var PAGE_SIZE = 100;
|
|
908
|
+
function renderDetail() {
|
|
909
|
+
var el = clear(document.getElementById("detail"));
|
|
910
|
+
if (S.col == null) return;
|
|
911
|
+
var col = D.columns[S.col];
|
|
912
|
+
var card = h("div", { class: "card pad detail" });
|
|
913
|
+
add(el, [h("h2", null, h("span", { class: "mono", text: col.path }), h("small", { text: col.type + " · " + col.repetition + " · " + col.sort_order + " sort order" }),
|
|
914
|
+
h("button", { class: "more", style: "margin:0 0 0 auto", onclick: function () { selectCol(col.i, true); } }, "close")), card]);
|
|
915
|
+
add(card, h("div", { class: "kvline" },
|
|
916
|
+
[["compressed", bytes(col.cs) + " (" + pct(col.cs, F.file_size) + " of file)"], ["uncompressed", bytes(col.us)], ["ratio", ratio(col.us, col.cs)],
|
|
917
|
+
["values", num(col.values)], ["nulls", num(col.nulls)], ["pages", num(col.pages)], ["max def/rep", col.max_def + "/" + col.max_rep]]
|
|
918
|
+
.map(function (p) { return h("span", null, h("span", { class: "k", text: p[0] }), h("span", { class: "mono", text: p[1] })); })));
|
|
919
|
+
// per row group
|
|
920
|
+
var tb = h("tbody");
|
|
921
|
+
var rgs = D.row_groups.length > 500 && !S.allChunkRgs ? D.row_groups.slice(0, 500) : D.row_groups;
|
|
922
|
+
rgs.forEach(function (g) {
|
|
923
|
+
var ch = chunkOf(g.i, col.i); if (!ch) return;
|
|
924
|
+
var st = ch.stats || {};
|
|
925
|
+
add(tb, h("tr", { class: "click" + (S.rg === g.i ? " sel" : ""), onclick: function () { selectChunk(g.i, col.i); } },
|
|
926
|
+
h("td", { class: "mono", text: g.i }),
|
|
927
|
+
h("td", { class: "mono clip", title: st.min || "", text: st.min != null ? st.min : "–" }),
|
|
928
|
+
h("td", { class: "mono clip", title: st.max || "", text: st.max != null ? st.max : "–" }),
|
|
929
|
+
h("td", { class: "r num", text: num(st.nulls) }),
|
|
930
|
+
h("td", { class: "r num", text: num(st.distinct) }),
|
|
931
|
+
h("td", { class: "r num", text: num(ch.values) }),
|
|
932
|
+
h("td", { class: "r num", text: num(ch.ndp) }),
|
|
933
|
+
h("td", { class: "r num", text: ch.dict ? num(ch.dict.n) : "–" }),
|
|
934
|
+
h("td", { class: "r num", text: bytes(ch.cs) }),
|
|
935
|
+
h("td", { class: "r num", text: ratio(ch.us, ch.cs) }),
|
|
936
|
+
h("td", null, h("span", { class: "pill", text: ch.codec }), st.caveat ? h("span", { class: "pill warn", title: st.caveat, text: "legacy stats" }) : null, st.source && st.source.indexOf("legacy") >= 0 && !st.caveat ? h("span", { class: "pill", text: "legacy min/max" }) : null,
|
|
937
|
+
st.min_exact === false || st.max_exact === false ? h("span", { class: "pill", title: "min/max were truncated by the writer", text: "truncated" }) : null,
|
|
938
|
+
ch.err ? h("span", { class: "pill err", title: ch.err, text: "error" }) : null,
|
|
939
|
+
ch.crc_bad ? h("span", { class: "pill err", text: "CRC mismatch" }) : null,
|
|
940
|
+
ch.idx_mm ? h("span", { class: "pill warn", title: "page statistics disagree with the column index", text: "index mismatch" }) : null)));
|
|
941
|
+
});
|
|
942
|
+
add(card, h("div", { class: "tablewrap", style: "max-height:360px;overflow:auto" }, h("table", null,
|
|
943
|
+
h("thead", null, h("tr", null, ["rg", "min", "max", "nulls", "distinct", "values", "data pages", "dict entries", "size", "ratio", ""].map(function (t, i) { return h("th", { class: i >= 3 && i <= 9 ? "r" : null, text: t }); }))), tb)));
|
|
944
|
+
if (S.rg == null) { add(card, h("p", { class: "hint", text: "Click a row group to see its column chunk: page headers, page index and metadata." })); return; }
|
|
945
|
+
var ch = chunkOf(S.rg, col.i);
|
|
946
|
+
if (ch) add(card, chunkDetail(D.row_groups[S.rg], ch, col));
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function chunkDetail(g, ch, col) {
|
|
950
|
+
var box = h("div", { style: "margin-top:16px" });
|
|
951
|
+
add(box, h("h2", null, "Column chunk", h("small", { text: col.path + " · row group " + g.i })));
|
|
952
|
+
if (ch.err) add(box, h("div", { class: "note err", text: "While walking page headers: " + ch.err }));
|
|
953
|
+
if (ch.stats && ch.stats.caveat) add(box, h("div", { class: "note", text: "Statistics: " + ch.stats.caveat }));
|
|
954
|
+
if (ch.crc_bad) add(box, h("div", { class: "note err", text: plural(ch.crc_bad, "page") + " failed CRC verification: the bytes stored don't match the checksum in the page header." }));
|
|
955
|
+
if (ch.idx_mm) add(box, h("div", { class: "note" }, "Page statistics disagree with the column index:",
|
|
956
|
+
h("ul", { style: "margin:4px 0 0;padding-left:18px" }, ch.idx_mm.slice(0, 20).map(function (m) {
|
|
957
|
+
return h("li", { class: "mono", text: m[0] == null ? m[2] + " data pages but " + m[3] + " column index entries" : "page " + m[0] + " " + m[1] + ": page header " + (m[2] == null ? "–" : m[2]) + ", column index " + (m[3] == null ? "–" : m[3]) });
|
|
958
|
+
}), ch.idx_mm.length > 20 ? h("li", { text: "+" + num(ch.idx_mm.length - 20) + " more" }) : null)));
|
|
959
|
+
var items = [
|
|
960
|
+
["codec", ch.codec], ["encodings", ch.enc.join(", ")],
|
|
961
|
+
["compressed / uncompressed", bytes(ch.cs) + " / " + bytes(ch.us) + " (" + ratio(ch.us, ch.cs) + ")"],
|
|
962
|
+
["bytes", num(ch.start) + " – " + num(ch.end) + (ch.end > ch.declared_end ? " (metadata says " + num(ch.declared_end) + ")" : "")],
|
|
963
|
+
["data page offset", num(ch.data_off)], ["dictionary page offset", ch.dict_off != null ? num(ch.dict_off) : "–"],
|
|
964
|
+
["values · nulls", num(ch.values) + " · " + num(ch.nulls)],
|
|
965
|
+
["dictionary", ch.dict ? num(ch.dict.n) + " entries, " + bytes(ch.dict.cs) + (ch.dict.sorted ? ", sorted" : "") : "none"],
|
|
966
|
+
["statistics", ch.stats ? (ch.stats.source || "null count only") : "none"],
|
|
967
|
+
["min … max", ch.stats && (ch.stats.min != null || ch.stats.max != null) ? (ch.stats.min || "–") + " … " + (ch.stats.max || "–") : "–"],
|
|
968
|
+
["distinct count", ch.stats && ch.stats.distinct != null ? num(ch.stats.distinct) : "–"],
|
|
969
|
+
["bloom filter", ch.bloom ? "at " + num(ch.bloom[0]) + (ch.bloom[1] ? ", " + bytes(ch.bloom[1]) : "") : "none"],
|
|
970
|
+
["column index", ch.ci ? "at " + num(ch.ci[0]) + ", " + bytes(ch.ci[1]) : "none"],
|
|
971
|
+
["offset index", ch.oi ? "at " + num(ch.oi[0]) + ", " + bytes(ch.oi[1]) : "none"]
|
|
972
|
+
];
|
|
973
|
+
if (ch.estats && ch.estats.length) items.push(["encoding stats", ch.estats.map(function (e) { return e.page_type + " " + e.encoding + " ×" + e.count; }).join("; ")]);
|
|
974
|
+
if (ch.size_stats && ch.size_stats.unencoded_byte_array_data_bytes != null) items.push(["unencoded byte array data", bytes(ch.size_stats.unencoded_byte_array_data_bytes)]);
|
|
975
|
+
if (ch.kv) Object.keys(ch.kv).forEach(function (k) { items.push(["metadata: " + k, ch.kv[k]]); });
|
|
976
|
+
add(box, h("dl", { class: "dl" }, items.map(function (p) { return h("div", null, h("dt", { text: p[0] }), h("dd", { text: p[1] })); })));
|
|
977
|
+
if (!ch.pages) { add(box, h("p", { class: "hint", text: "Page detail was left out to keep this page small (" + num(ch.np) + " pages in this chunk)." })); return box; }
|
|
978
|
+
var hasIdx = !!(ch.column_index || ch.offset_index);
|
|
979
|
+
var tabs = h("div", { class: "tabs" });
|
|
980
|
+
[["pages", "Pages (" + num(ch.pages.length) + ")"], hasIdx ? ["index", "Page index"] : null].filter(Boolean).forEach(function (t) {
|
|
981
|
+
add(tabs, h("button", { class: S.tab === t[0] ? "on" : null, onclick: function () { S.tab = t[0]; S.pageOffset = 0; renderDetail(); } }, t[1]));
|
|
982
|
+
});
|
|
983
|
+
add(box, tabs);
|
|
984
|
+
if (S.tab === "index" && hasIdx) add(box, pageIndexTable(ch)); else add(box, pagesTable(ch));
|
|
985
|
+
return box;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function pager(total, rerender) {
|
|
989
|
+
if (total <= PAGE_SIZE) return null;
|
|
990
|
+
var from = S.pageOffset, to = Math.min(total, from + PAGE_SIZE);
|
|
991
|
+
return h("div", { class: "pager" },
|
|
992
|
+
h("button", { disabled: from === 0 ? true : null, onclick: function () { S.pageOffset = Math.max(0, from - PAGE_SIZE); rerender(); } }, "‹ prev"),
|
|
993
|
+
h("span", { class: "num", text: num(from + 1) + "–" + num(to) + " of " + num(total) }),
|
|
994
|
+
h("button", { disabled: to >= total ? true : null, onclick: function () { S.pageOffset = from + PAGE_SIZE; rerender(); } }, "next ›"));
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function pagesTable(ch) {
|
|
998
|
+
var wrap = h("div");
|
|
999
|
+
var tb = h("tbody"), from = S.pageOffset, rows = ch.pages.slice(from, from + PAGE_SIZE);
|
|
1000
|
+
var bad = {};
|
|
1001
|
+
(ch.idx_mm || []).forEach(function (m) { if (m[0] != null) (bad[m[0]] = bad[m[0]] || []).push(m[1] + ": index says " + (m[3] == null ? "–" : m[3])); });
|
|
1002
|
+
rows.forEach(function (p, k) {
|
|
1003
|
+
var i = from + k;
|
|
1004
|
+
add(tb, h("tr", { class: "click" + (S.page === i ? " hl" : ""), onclick: function () { S.page = S.page === i ? null : i; renderMap(); renderDetail(); } },
|
|
1005
|
+
h("td", { class: "num", text: i }),
|
|
1006
|
+
h("td", null, h("span", { class: "sw-inline", style: "background:" + colColor(ch.c, p[0] === 2, true) }), PT[p[0]] || p[0]),
|
|
1007
|
+
h("td", { class: "r num", text: num(p[1]) }),
|
|
1008
|
+
h("td", { class: "r num", text: num(p[2]) }),
|
|
1009
|
+
h("td", { class: "r num", text: num(p[3]) }),
|
|
1010
|
+
h("td", { class: "r num", text: num(p[4]) }),
|
|
1011
|
+
h("td", { class: "r num", text: num(p[5]) }),
|
|
1012
|
+
h("td", { class: "r num", text: p[6] != null ? num(p[6]) : "–" }),
|
|
1013
|
+
h("td", { class: "r num", text: p[7] != null ? num(p[7]) : "–" }),
|
|
1014
|
+
h("td", { class: "r num", text: p[8] != null ? num(p[8]) : "–" }),
|
|
1015
|
+
h("td", null, p[9] ? h("span", { class: "pill", text: p[9] }) : "–"),
|
|
1016
|
+
h("td", { class: "mono clip", title: p[10] || "", text: p[10] != null ? p[10] : "–" }),
|
|
1017
|
+
h("td", { class: "mono clip", title: p[11] || "", text: p[11] != null ? p[11] : "–" }),
|
|
1018
|
+
h("td", null, crcCell(p[12]), bad[i] ? h("span", { class: "pill warn", title: bad[i].join("\n"), text: "≠ index" }) : null),
|
|
1019
|
+
h("td", { class: "faint", text: p[13] || "" })));
|
|
1020
|
+
});
|
|
1021
|
+
add(wrap, h("div", { class: "tablewrap" }, h("table", null,
|
|
1022
|
+
h("thead", null, h("tr", null, ["#", "type", "offset", "header", "compressed", "uncompressed", "values", "nulls", "rows", "first row", "encoding", "min", "max", "crc", "levels"].map(function (t, i) { return h("th", { class: i >= 2 && i <= 9 ? "r" : null, text: t }); }))), tb)));
|
|
1023
|
+
add(wrap, pager(ch.pages.length, renderDetail));
|
|
1024
|
+
return wrap;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function crcCell(v) {
|
|
1028
|
+
if (v === 2) return h("span", { class: "pill ok", title: "CRC verified", text: "✓ ok" });
|
|
1029
|
+
if (v === 3) return h("span", { class: "pill err", title: "the page bytes don't match the CRC in its header", text: "✗ mismatch" });
|
|
1030
|
+
return v ? h("span", { title: "the page header has a CRC (not verified; use --verify-checksums)", text: "✓" }) : "";
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function pageIndexTable(ch) {
|
|
1034
|
+
var wrap = h("div");
|
|
1035
|
+
var ci = ch.column_index, oi = ch.offset_index || [];
|
|
1036
|
+
var n = Math.max(ci ? ci.rows.length : 0, oi.length);
|
|
1037
|
+
if (ci) add(wrap, h("div", { class: "kvline" }, h("span", null, h("span", { class: "k", text: "boundary order" }), h("span", { class: "mono", text: String(ci.boundary) })), h("span", null, h("span", { class: "k", text: "entries" }), h("span", { class: "mono", text: num(n) }))));
|
|
1038
|
+
var tb = h("tbody"), from = S.pageOffset;
|
|
1039
|
+
// column index entries follow the data pages; map them back to page numbers
|
|
1040
|
+
var dataPages = [], badEntry = {};
|
|
1041
|
+
ch.pages.forEach(function (p, k) { if (p[0] !== 2) dataPages.push(k); });
|
|
1042
|
+
(ch.idx_mm || []).forEach(function (m) { if (m[0] != null) badEntry[dataPages.indexOf(m[0])] = true; });
|
|
1043
|
+
for (var i = from; i < Math.min(n, from + PAGE_SIZE); i++) {
|
|
1044
|
+
var c = ci && ci.rows[i], o = oi[i];
|
|
1045
|
+
add(tb, h("tr", { class: badEntry[i] ? "hl" : null, title: badEntry[i] ? "disagrees with the page header's statistics" : null },
|
|
1046
|
+
h("td", { class: "num", text: i }),
|
|
1047
|
+
h("td", { class: "r num", text: o ? num(o[0]) : "–" }),
|
|
1048
|
+
h("td", { class: "r num", text: o ? num(o[1]) : "–" }),
|
|
1049
|
+
h("td", { class: "r num", text: o ? num(o[2]) : "–" }),
|
|
1050
|
+
h("td", { text: c ? (c[0] ? "yes" : "") : "–" }),
|
|
1051
|
+
h("td", { class: "mono clip", title: c && c[1] || "", text: c && c[1] != null ? c[1] : "–" }),
|
|
1052
|
+
h("td", { class: "mono clip", title: c && c[2] || "", text: c && c[2] != null ? c[2] : "–" }),
|
|
1053
|
+
h("td", { class: "r num", text: c && c[3] != null ? num(c[3]) : "–" })));
|
|
1054
|
+
}
|
|
1055
|
+
add(wrap, h("div", { class: "tablewrap" }, h("table", null,
|
|
1056
|
+
h("thead", null, h("tr", null, ["#", "offset", "compressed size", "first row", "null page", "min", "max", "null count"].map(function (t, i) { return h("th", { class: (i >= 1 && i <= 3) || i === 7 ? "r" : null, text: t }); }))), tb)));
|
|
1057
|
+
add(wrap, pager(n, renderDetail));
|
|
1058
|
+
return wrap;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// ---------- key/value metadata ----------
|
|
1062
|
+
function renderKV() {
|
|
1063
|
+
var el = clear(document.getElementById("kv"));
|
|
1064
|
+
if (!D.kv.length) return;
|
|
1065
|
+
var card = h("div", { class: "card pad" });
|
|
1066
|
+
D.kv.forEach(function (kv) {
|
|
1067
|
+
var body;
|
|
1068
|
+
if (kv.json !== undefined) body = code(JSON.stringify(kv.json, null, 2), "json");
|
|
1069
|
+
else body = h("pre", null, h("code", { text: kv.value }));
|
|
1070
|
+
var extra = kv.arrow_schema ? arrowTree(kv.arrow_schema)
|
|
1071
|
+
: kv.arrow_fields ? h("div", { class: "hint", text: "Arrow schema fields: " + kv.arrow_fields.join(", ") }) : null;
|
|
1072
|
+
if (kv.arrow_error) extra = [h("div", { class: "note", text: kv.arrow_error }), extra];
|
|
1073
|
+
var head = h("div", { class: "head" }, h("span", { class: "key", text: kv.key }), h("span", { class: "pill", text: kv.format }), h("span", { class: "faint", text: bytes(kv.bytesize) }), kv.summary ? h("span", { class: "subtle", text: kv.summary }) : null);
|
|
1074
|
+
if (kv.format === "arrow_schema" || kv.bytesize > 4000) add(card, h("div", { class: "kv" }, head, extra, h("details", null, h("summary", { text: "show value" }), body)));
|
|
1075
|
+
else add(card, h("div", { class: "kv" }, head, body));
|
|
1076
|
+
});
|
|
1077
|
+
add(el, [h("h2", null, "Key/value metadata", h("small", { text: num(D.kv.length) + " entries" })), card]);
|
|
1078
|
+
}
|
|
1079
|
+
function arrowTree(a) {
|
|
1080
|
+
var box = h("div", { class: "schema", style: "margin:4px 0 8px" }), n = 0;
|
|
1081
|
+
function walk(f, depth) {
|
|
1082
|
+
if (n++ > 2000) return;
|
|
1083
|
+
var notes = [];
|
|
1084
|
+
if (!f.nullable) notes.push("not null");
|
|
1085
|
+
if (f.extension) notes.push("extension " + f.extension);
|
|
1086
|
+
if (f.dictionary) notes.push("dictionary-encoded, " + f.dictionary.index_type + " indices" + (f.dictionary.ordered ? ", ordered" : ""));
|
|
1087
|
+
var meta = f.metadata ? Object.keys(f.metadata).filter(function (k) { return k.indexOf("ARROW:extension:") !== 0; }) : [];
|
|
1088
|
+
if (meta.length) notes.push("metadata " + meta.map(function (k) { return k + "=" + JSON.stringify(f.metadata[k]).slice(0, 80); }).join(", "));
|
|
1089
|
+
add(box, h("div", { class: "sline", title: f.type },
|
|
1090
|
+
h("span", { class: "ln" }),
|
|
1091
|
+
h("span", { class: "code", style: "padding-left:" + (depth * 2) + "ch" }, h("span", { class: "nm", text: f.name }), ": ",
|
|
1092
|
+
h("span", { class: "ty", text: f.children && f.type.length > 60 ? f.type.slice(0, f.type.indexOf("<") + 1) + "…>" : f.type }),
|
|
1093
|
+
notes.length ? h("span", { class: "ann", text: " " + notes.join("; ") }) : null)));
|
|
1094
|
+
(f.children || []).forEach(function (c) { walk(c, depth + 1); });
|
|
1095
|
+
}
|
|
1096
|
+
a.fields.forEach(function (f) { walk(f, 0); });
|
|
1097
|
+
var meta = a.metadata ? Object.keys(a.metadata) : [];
|
|
1098
|
+
return [h("div", { class: "hint", text: "Arrow schema" + (a.endianness === "big" ? " (big-endian)" : "") + (meta.length ? " · schema metadata: " + meta.join(", ") : "") }), box];
|
|
1099
|
+
}
|
|
1100
|
+
function code(text, lang) {
|
|
1101
|
+
var c = h("code", { class: "language-" + lang, text: text });
|
|
1102
|
+
highlight(c);
|
|
1103
|
+
return h("pre", null, c);
|
|
1104
|
+
}
|
|
1105
|
+
var pending = [];
|
|
1106
|
+
function highlight(el) {
|
|
1107
|
+
if (window.hljs) { try { window.hljs.highlightElement(el); } catch (e) {} }
|
|
1108
|
+
else pending.push(el);
|
|
1109
|
+
}
|
|
1110
|
+
window.__hbHighlight = function () { var p = pending; pending = []; p.forEach(function (el) { if (el.isConnected) highlight(el); }); };
|
|
1111
|
+
|
|
1112
|
+
// ---------- footer JSON ----------
|
|
1113
|
+
function renderFooter() {
|
|
1114
|
+
var el = clear(document.getElementById("footer"));
|
|
1115
|
+
var det = h("details", null, h("summary", { text: "FileMetaData as JSON (" + bytes(F.footer_size) + " footer)" }));
|
|
1116
|
+
if (D.footer_json) {
|
|
1117
|
+
var done = false;
|
|
1118
|
+
det.addEventListener("toggle", function () { if (det.open && !done) { done = true; add(det, code(D.footer_json, "json")); } });
|
|
1119
|
+
} else add(det, h("p", { class: "hint", text: "The footer is too large to embed." }));
|
|
1120
|
+
add(el, [h("h2", null, "Footer", h("small", { text: "the raw metadata this page was drawn from" })), h("div", { class: "card pad" }, det)]);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function renderAll() {
|
|
1124
|
+
renderSummary(); renderMap(); renderSchema(); renderRowGroups(); renderDetail(); renderColumns();
|
|
1125
|
+
}
|
|
1126
|
+
renderAll(); renderKV(); renderFooter();
|
|
1127
|
+
document.getElementById("gen").textContent = "Generated by " + D.generator + " · " + F.name;
|
|
1128
|
+
document.addEventListener("click", function (ev) { if (ev.target.tagName !== "CANVAS") hidePop(); });
|
|
1129
|
+
})();
|
|
1130
|
+
</script>
|
|
1131
|
+
<script src="%%HIGHLIGHT_JS%%" async onload="window.__hbHighlight && window.__hbHighlight()"></script>
|
|
1132
|
+
</body>
|
|
1133
|
+
</html>
|
|
1134
|
+
HTML
|
|
1135
|
+
end
|
|
1136
|
+
end
|