Parquet Metadata Inspector
blockdevopsApache Parquet file format deep-dive inspector with telemetry overview, row groups switcher, column chunk statistics, dictionary encoding metrics, and hierarchical schema tree with repetition and definition levels.
Installation
$pnpm dlx uipkge-ng@latest add parquet-metadata-inspector$npx uipkge-ng@latest add parquet-metadata-inspector$yarn dlx uipkge-ng@latest add parquet-metadata-inspector$bunx uipkge-ng@latest add parquet-metadata-inspectorManifest:
https://uipkge.dev/r/angular/parquet-metadata-inspector.jsonInstalls to:src/app/components/blocks/parquet-metadata-inspector/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
ColumnChunkinterface ColumnChunk {
name: string
physicalType: string
logicalType: string
fieldId: number
encodings: string[]
compression: string
numValues: string
nullCount: number
nullPct: string
minStat: string
maxStat: string
uncompressedBytes: string
compressedBytes: string
compressionRatio: string
compressionPct: number
isDictionaryEncoded: boolean
dictEntries: number | null
dictPageOffset: string | null
dataPageOffset: string
bitWidth: number | null
}RowGroupMetainterface RowGroupMeta {
id: number
numRows: number
numRowsFormatted: string
totalByteSize: string
totalCompressedSize: string
compressionRatio: string
fileOffset: string
columns: ColumnChunk[]
}SchemaTreeNodeinterface SchemaTreeNode {
id: number
path: string
name: string
repetition: 'REQUIRED' | 'OPTIONAL' | 'REPEATED'
physicalType: string
logicalType: string
defLevel: number
repLevel: number
depth: number
isGroup?: boolean
description: string
}Files installed (5)
src/app/components/blocks/parquet-metadata-inspector/parquet-metadata-inspector.component.ts40.8 kB
// Apache Parquet file deep-dive inspector: file header with S3 URI copy + header-JSON // copy/download, four telemetry cards, a row-group switcher banner, and a four-tab view // (column-chunk statistics with search + encoding filter, schema tree, dictionary metrics, // Thrift FileMetaData JSON). Consolidates the five Vue SFCs + data files 1:1; the data, // types and schema siblings are verbatim copies of the Vue sources. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { ArrowRight, Binary, Check, Code2, Columns3, Copy, Download, FileCode2, FolderTree, HardDrive, Layers, LucideAngularModule, Search, Table2, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiProgressComponent } from '@/ui/progress/progress.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' import { UiTabsComponent, UiTabsContentComponent, UiTabsListComponent, UiTabsTriggerComponent, } from '@/ui/tabs/tabs.component' import { defaultRowGroupsData, parquetHeaderJson } from './parquet-metadata-data' import type { ColumnChunk, RowGroupMeta } from './parquet-metadata-types' import { schemaTreeNodes, type SchemaTreeNode } from './parquet-schema' export type { ColumnChunk, RowGroupMeta } @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-parquet-metadata-inspector, [ui-parquet-metadata-inspector]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiProgressComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, UiTabsComponent, UiTabsContentComponent, UiTabsListComponent, UiTabsTriggerComponent, ], template: ` <div data-slot="parquet-metadata-inspector" [class]="rootClass"> <!-- Top Parquet File Header --> <header class="border-border bg-card/70 border-b px-4 py-4 sm:px-6"> <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"> <div class="space-y-1.5"> <div class="flex flex-wrap items-center gap-2.5"> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg"> <lucide-icon [img]="FileCode2" class="size-4" /> </div> <h2 class="font-mono text-sm font-semibold tracking-tight break-all sm:text-base"> {{ filePath() }} </h2> <button type="button" class="text-muted-foreground hover:text-foreground cursor-pointer transition-colors" aria-label="Copy S3 URI" (click)="copyUriToClipboard()" > @if (copiedUri()) { <lucide-icon [img]="Check" class="text-success size-3.5" /> } @else { <lucide-icon [img]="Copy" class="size-3.5" /> } </button> </div> <div class="flex flex-wrap items-center gap-x-3 gap-y-1.5 text-xs"> <span ui-badge variant="outline" class="gap-1.5 font-mono text-xs font-normal"> <span class="bg-success size-1.5 rounded-full"></span> <span>{{ formatSpec() }}</span> </span> <span class="text-muted-foreground"> Total Rows: <strong class="text-foreground font-semibold tabular-nums">{{ totalRows() }}</strong> </span> <div class="border-success/30 bg-success/10 text-success inline-flex items-center gap-1 rounded-md border px-2 py-0.5 font-mono text-xs font-medium" > <span>{{ fileSize() }}</span> <span>·</span> <span class="tabular-nums">{{ compressionRatio() }} Compression Ratio</span> </div> </div> </div> <div class="flex flex-wrap items-center gap-2"> <button ui-button variant="outline" size="sm" class="h-8 gap-1.5 text-xs shadow-xs" (click)="copyJsonToClipboard()" > @if (copiedJson()) { <lucide-icon [img]="Check" class="text-success size-3.5" /> } @else { <lucide-icon [img]="Copy" class="size-3.5" /> } <span>{{ copiedJson() ? 'JSON Copied' : 'Copy Header JSON' }}</span> </button> <button ui-button aria-label="Download attachment" size="sm" class="h-8 gap-1.5 text-xs shadow-xs" [disabled]="downloadingJson()" (click)="handleDownloadJson()" > @if (!downloadingJson()) { <lucide-icon [img]="Download" class="size-3.5" /> } @else { <span class="size-3.5 animate-spin rounded-full border-2 border-current border-t-transparent"></span> } <span>Download Parquet Header JSON</span> </button> </div> </div> </header> <div class="space-y-6 p-4 sm:p-6"> <!-- 4 Parquet File Telemetry Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <div ui-card class="flex flex-col justify-between shadow-xs"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between gap-2"> <div class="flex items-center gap-2"> <div class="border-info/20 bg-info/10 text-info flex size-7 items-center justify-center rounded-md border" > <lucide-icon [img]="Layers" class="size-3.5" /> </div> <h3 ui-card-title class="text-xs font-medium">Row Groups</h3> </div> <span ui-badge variant="outline" class="font-mono text-xs tabular-nums">2 Groups</span> </div> </div> <div ui-card-content class="space-y-2 pt-1"> <div class="flex items-baseline gap-1.5"> <span class="text-foreground text-2xl font-bold tracking-tight tabular-nums sm:text-3xl">2</span> <span class="text-muted-foreground text-xs font-medium">Row Groups</span> </div> <div class="border-border/60 border-t pt-2 text-xs"> <div class="text-muted-foreground flex items-center justify-between gap-x-2"> <span>Chunk Partitioning:</span> <span class="text-foreground font-medium tabular-nums">125,000 rows / group</span> </div> </div> </div> </div> <div ui-card class="flex flex-col justify-between shadow-xs"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between gap-2"> <div class="flex items-center gap-2"> <div class="border-chart-1/20 bg-chart-1/10 text-chart-1 flex size-7 items-center justify-center rounded-md border" > <lucide-icon [img]="Columns3" class="size-3.5" /> </div> <h3 ui-card-title class="text-xs font-medium">Columns / Schema</h3> </div> <span ui-badge variant="outline" class="font-mono text-xs tabular-nums">14 Chunks</span> </div> </div> <div ui-card-content class="space-y-2 pt-1"> <div class="flex items-baseline gap-1.5"> <span class="text-foreground text-2xl font-bold tracking-tight tabular-nums sm:text-3xl">14</span> <span class="text-muted-foreground text-xs font-medium">Column Chunks</span> </div> <div class="border-border/60 border-t pt-2 text-xs"> <div class="text-muted-foreground flex items-center justify-between gap-x-2"> <span>Nested Topology:</span> <span class="text-foreground font-medium tabular-nums">2 Structs · 1 List</span> </div> </div> </div> </div> <div ui-card class="flex flex-col justify-between shadow-xs"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between gap-2"> <div class="flex items-center gap-2"> <div class="border-success/20 bg-success/10 text-success flex size-7 items-center justify-center rounded-md border" > <lucide-icon [img]="HardDrive" class="size-3.5" /> </div> <h3 ui-card-title class="text-xs font-medium">Uncompressed Size</h3> </div> <span ui-badge variant="secondary" class="border-success/30 text-success font-mono text-xs tabular-nums" > 3.88x Savings </span> </div> </div> <div ui-card-content class="space-y-2 pt-1"> <div class="flex items-baseline gap-1.5"> <span class="text-foreground text-2xl font-bold tracking-tight tabular-nums sm:text-3xl">{{ uncompressedSize() }}</span> <span class="text-muted-foreground text-xs font-medium">Raw</span> </div> <div class="border-border/60 border-t pt-2 text-xs"> <div class="text-muted-foreground flex items-center justify-between gap-x-2"> <span>Snappy Footprint:</span> <span class="text-foreground font-medium tabular-nums">{{ fileSize() }} (Saved 53.0 MB)</span> </div> </div> </div> </div> <div ui-card class="flex flex-col justify-between shadow-xs"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between gap-2"> <div class="flex items-center gap-2"> <div class="border-warning/20 bg-warning/10 text-warning flex size-7 items-center justify-center rounded-md border" > <lucide-icon [img]="Binary" class="size-3.5" /> </div> <h3 ui-card-title class="text-xs font-medium">Dictionary Encoding</h3> </div> <span ui-badge variant="outline" class="font-mono text-xs tabular-nums">85.7%</span> </div> </div> <div ui-card-content class="space-y-2 pt-1"> <div class="flex items-baseline gap-1.5"> <span class="text-foreground text-2xl font-bold tracking-tight tabular-nums sm:text-3xl">12 / 14</span> <span class="text-muted-foreground text-xs font-medium">Columns Encoded</span> </div> <div class="border-border/60 border-t pt-2 text-xs"> <div class="text-muted-foreground flex items-center justify-between gap-x-2"> <span>Encoding Algorithms:</span> <span class="text-foreground font-medium">RLE + PLAIN_DICT</span> </div> </div> </div> </div> </div> <!-- Row Groups Switcher Banner --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-content class="p-3"> <div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between"> <div class="flex flex-wrap items-center gap-2"> <span class="text-muted-foreground text-xs font-semibold tracking-wider uppercase" >Inspect Row Group:</span > <div class="flex flex-wrap items-center gap-1.5"> @for (rg of rowGroups(); track rg.id) { <button type="button" [class]="rowGroupClass(rg.id)" (click)="selectedRowGroup.set(rg.id)"> <div class="flex items-center gap-2"> <lucide-icon [img]="Layers" class="size-3.5 opacity-70" /> <span class="font-mono font-semibold">Row Group #{{ rg.id }}</span> <span class="bg-muted text-muted-foreground rounded px-1.5 py-0.5 font-mono text-xs tabular-nums" > {{ rg.numRowsFormatted }} </span> <span class="text-muted-foreground font-mono text-xs tabular-nums"> ({{ rg.totalCompressedSize }}) </span> </div> </button> } </div> </div> <div class="flex flex-wrap items-center gap-2 text-xs"> <span class="text-muted-foreground whitespace-nowrap">Row Group Offset:</span> <span ui-badge variant="outline" class="min-w-0 truncate font-mono text-xs">{{ activeRowGroup().fileOffset }}</span> <span class="text-muted-foreground">Raw Footprint:</span> <span class="text-foreground font-mono font-medium tabular-nums">{{ activeRowGroup().totalByteSize }}</span> </div> </div> </div> </div> <!-- Main Tabs View --> <ui-tabs [value]="activeTab()" (valueChange)="activeTab.set($event)" class="w-full"> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <ui-tabs-list class="grid h-9 w-full grid-cols-2 p-1 sm:w-auto md:grid-cols-4"> <button ui-tabs-trigger value="chunks" class="gap-1.5 text-xs"> <lucide-icon [img]="Table2" class="size-3.5" /> <span>Column Chunks</span> <span class="bg-muted py-0.2 rounded-full px-1.5 font-mono text-xs">{{ activeRowGroup().columns.length }}</span> </button> <button ui-tabs-trigger value="schema" class="gap-1.5 text-xs"> <lucide-icon [img]="FolderTree" class="size-3.5" /> <span>Schema Tree</span> <span class="bg-muted py-0.2 rounded-full px-1.5 font-mono text-xs">{{ nodes().length }}</span> </button> <button ui-tabs-trigger value="dictionary" class="gap-1.5 text-xs"> <lucide-icon [img]="Binary" class="size-3.5" /> <span>Dictionary Metrics</span> <span class="bg-muted py-0.2 rounded-full px-1.5 font-mono text-xs">{{ dictionaryColumns().length }}</span> </button> <button ui-tabs-trigger value="thrift" class="gap-1.5 text-xs"> <lucide-icon [img]="Code2" class="size-3.5" /> <span>Thrift Metadata</span> </button> </ui-tabs-list> @if (activeTab() === 'chunks') { <div class="flex flex-wrap items-center gap-2"> <div class="relative w-full sm:w-56"> <lucide-icon [img]="Search" class="text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2" /> <input [value]="searchQuery()" (input)="searchQuery.set($any($event.target).value)" type="text" placeholder="Search column chunks..." class="border-border bg-background text-foreground focus-visible:border-ring focus-visible:ring-ring/50 h-8 w-full rounded-md border pr-2.5 pl-8 text-xs focus-visible:ring-2 focus-visible:outline-none" /> </div> <div class="bg-muted border-border flex items-center rounded-md border p-0.5 text-xs"> <button type="button" [class]="encodingClass('all')" (click)="encodingFilter.set('all')">All</button> <button type="button" [class]="encodingClass('dictionary')" (click)="encodingFilter.set('dictionary')" > Dict Encoded </button> <button type="button" [class]="encodingClass('plain')" (click)="encodingFilter.set('plain')"> Plain </button> </div> </div> } </div> <!-- TAB 1: Column Chunks Breakdown Table --> <ui-tabs-content value="chunks" class="mt-4 space-y-4"> <div ui-card class="overflow-hidden border shadow-none"> <div class="overflow-x-auto"> <table ui-table> <thead ui-table-header> <tr ui-table-row class="bg-muted/40 hover:bg-muted/40"> <th ui-table-head class="min-w-[200px] text-xs font-semibold">Column Name & Type</th> <th ui-table-head class="min-w-[170px] text-xs font-semibold">Encoding & Codec</th> <th ui-table-head class="min-w-[280px] text-xs font-semibold">Min / Max Column Statistics</th> <th ui-table-head class="min-w-[130px] text-xs font-semibold">Null Values</th> <th ui-table-head class="min-w-[190px] text-xs font-semibold">Compressed / Raw Footprint</th> </tr> </thead> <tbody ui-table-body> @for (col of filteredColumns(); track col.name) { <tr ui-table-row class="text-xs"> <td ui-table-cell> <div class="space-y-1"> <div class="flex items-center gap-1.5"> <span class="text-foreground font-mono text-xs font-semibold">{{ col.name }}</span> <span ui-badge variant="outline" class="font-mono text-xs">ID: {{ col.fieldId }}</span> </div> <div class="flex flex-wrap items-center gap-1 text-xs"> <span ui-badge variant="secondary" class="font-mono text-xs font-normal"> {{ col.physicalType }} </span> <lucide-icon [img]="ArrowRight" class="text-muted-foreground size-2.5" /> <span class="text-muted-foreground font-mono text-xs">{{ col.logicalType }}</span> </div> </div> </td> <td ui-table-cell> <div class="space-y-1.5"> <div class="flex flex-wrap items-center gap-1"> @for (enc of col.encodings; track enc) { <span ui-badge [variant]="enc.includes('DICTIONARY') ? 'default' : 'outline'" class="font-mono text-xs font-normal" > {{ enc }} </span> } </div> <div class="text-muted-foreground flex items-center gap-1.5 text-xs"> <span class="text-foreground font-medium">Codec:</span> <span ui-badge variant="secondary" class="font-mono text-xs">{{ col.compression }}</span> </div> </div> </td> <td ui-table-cell> <div class="border-border/70 bg-muted/20 space-y-1 rounded-md border p-2 font-mono text-xs"> <div class="flex items-baseline gap-1.5"> <span class="text-muted-foreground shrink-0">Min:</span> <span class="text-foreground truncate font-medium" [attr.title]="col.minStat">{{ col.minStat }}</span> </div> <div class="flex items-baseline gap-1.5"> <span class="text-muted-foreground shrink-0">Max:</span> <span class="text-foreground truncate font-medium" [attr.title]="col.maxStat">{{ col.maxStat }}</span> </div> </div> </td> <td ui-table-cell> <div class="space-y-1"> <div class="flex items-center gap-1.5"> <span ui-badge [variant]="col.nullCount === 0 ? 'secondary' : 'outline'" [class]=" col.nullCount === 0 ? 'border-success/30 text-success font-mono text-xs' : 'font-mono text-xs' " > {{ col.nullCount }} nulls </span> </div> <p class="text-muted-foreground font-mono text-xs tabular-nums"> {{ col.nullPct }} of {{ col.numValues }} rows </p> </div> </td> <td ui-table-cell> <div class="space-y-1.5"> <div class="flex items-center justify-between gap-x-2 font-mono text-xs"> <span class="text-foreground font-semibold tabular-nums">{{ col.compressedBytes }}</span> <span class="text-muted-foreground tabular-nums">/ {{ col.uncompressedBytes }}</span> </div> <div class="space-y-1"> <ui-progress [value]="col.compressionPct" class="h-1.5" /> <div class="text-muted-foreground flex items-center justify-between gap-x-2 text-xs"> <span class="text-success font-mono font-medium tabular-nums" >{{ col.compressionRatio }} saved</span > <span class="font-mono text-xs">Offset: {{ col.dataPageOffset }}</span> </div> </div> </div> </td> </tr> } @if (filteredColumns().length === 0) { <tr ui-table-row> <td ui-table-cell colspan="5" class="text-muted-foreground py-8 text-center text-xs"> No matching column chunks found for filter criteria. </td> </tr> } </tbody> </table> </div> </div> </ui-tabs-content> <!-- TAB 2: Parquet File Schema Tree Visualizer --> <ui-tabs-content value="schema" class="mt-4 space-y-4"> <div ui-card class="shadow-none"> <div ui-card-header class="border-border border-b pb-3"> <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div> <h3 ui-card-title class="text-sm font-semibold">Parquet Schema Definition & Field Hierarchy</h3> <p ui-card-description class="text-xs"> Hierarchical view of logical schema nodes, repetition rules (REQUIRED, OPTIONAL, REPEATED), and maximum definition / repetition levels for column projection. </p> </div> <div class="flex flex-wrap items-center gap-1.5"> <span ui-badge variant="outline" class="font-mono text-xs">D: Def Level</span> <span ui-badge variant="outline" class="font-mono text-xs">R: Rep Level</span> </div> </div> </div> <div ui-card-content class="p-0"> <div class="divide-border divide-y"> <div class="bg-muted/40 flex items-center justify-between gap-x-2 px-4 py-2.5 font-mono text-xs"> <div class="flex items-center gap-2"> <lucide-icon [img]="FolderTree" class="text-primary size-4" /> <span class="text-foreground font-bold">message schema</span> <span ui-badge variant="secondary" class="font-mono text-xs">Root Message</span> </div> <span class="text-muted-foreground font-mono text-xs">14 column fields</span> </div> @for (node of nodes(); track node.id) { <div class="hover:bg-muted/30 flex flex-col justify-between gap-2 px-4 py-3 text-xs transition-colors md:flex-row md:items-center" [style.padding-left.rem]="indentRem(node.depth)" > <div class="flex min-w-0 items-center gap-2"> <span class="text-muted-foreground font-mono select-none"> {{ node.depth === 0 ? '├─' : '└─' }} </span> <span [class]="nodeNameClass(node.isGroup)"> {{ node.name }} </span> <span ui-badge [variant]=" node.repetition === 'REQUIRED' ? 'default' : node.repetition === 'REPEATED' ? 'secondary' : 'outline' " class="font-mono text-xs font-normal" > {{ node.repetition }} </span> <span ui-badge variant="secondary" class="font-mono text-xs font-normal"> {{ node.physicalType }} </span> <lucide-icon [img]="ArrowRight" class="text-muted-foreground size-3 shrink-0" /> <span ui-badge variant="outline" class="text-muted-foreground font-mono text-xs font-normal"> {{ node.logicalType }} </span> </div> <div class="flex shrink-0 flex-wrap items-center gap-2"> <span class="text-muted-foreground hidden max-w-[220px] truncate text-xs lg:inline-block" [attr.title]="node.description" > {{ node.description }} </span> <div class="bg-muted/60 border-border flex items-center gap-2 rounded border px-2 py-0.5 font-mono text-xs" > <span title="Max Definition Level" >max_def: <strong class="text-foreground font-bold tabular-nums">{{ node.defLevel }}</strong></span > <span class="text-muted-foreground">|</span> <span title="Max Repetition Level" >max_rep: <strong class="text-foreground font-bold tabular-nums">{{ node.repLevel }}</strong></span > </div> <span ui-badge variant="outline" class="font-mono text-xs">ID: {{ node.id }}</span> </div> </div> } </div> </div> </div> </ui-tabs-content> <!-- TAB 3: Dictionary & Compression Deep Dive --> <ui-tabs-content value="dictionary" class="mt-4 space-y-4"> <div class="grid grid-cols-1 gap-4 lg:grid-cols-3"> <div ui-card class="shadow-none lg:col-span-1"> <div ui-card-header class="pb-3"> <h3 ui-card-title class="text-sm font-semibold">Dictionary Optimization Impact</h3> <p ui-card-description class="text-xs"> Plain dictionary replaces repeated byte strings and integers with compact 3-bit to 16-bit dictionary index pointers. </p> </div> <div ui-card-content class="space-y-4 text-xs"> <div class="border-border bg-muted/20 space-y-2 rounded-lg border p-3"> <div class="flex items-center justify-between gap-x-2"> <span class="text-muted-foreground">Dictionary Enabled Columns:</span> <span class="text-foreground font-mono font-semibold">6 / 8 Monitored</span> </div> <div class="flex items-center justify-between gap-x-2"> <span class="text-muted-foreground">Avg Space Reduction:</span> <span class="text-success font-mono font-semibold tabular-nums">78.4%</span> </div> <div class="flex items-center justify-between gap-x-2"> <span class="text-muted-foreground">Dictionary Page Overhead:</span> <span class="text-foreground font-mono font-semibold tabular-nums">0.34 MB total</span> </div> <div class="flex items-center justify-between gap-x-2"> <span class="text-muted-foreground">RLE Run Length Efficiency:</span> <span class="text-foreground font-mono font-semibold">Optimal</span> </div> </div> <div class="space-y-2"> <span class="text-muted-foreground font-medium">Compression Distribution by Codec:</span> <div class="space-y-1.5"> <div class="flex items-center justify-between gap-x-2 font-mono"> <span>Snappy + RLE Dict</span> <span class="font-semibold tabular-nums">74.2%</span> </div> <ui-progress [value]="74.2" class="h-1.5" /> </div> </div> </div> </div> <div ui-card class="overflow-hidden shadow-none lg:col-span-2"> <div class="overflow-x-auto"> <table ui-table> <thead ui-table-header> <tr ui-table-row class="bg-muted/40 hover:bg-muted/40"> <th ui-table-head class="text-xs font-semibold">Column</th> <th ui-table-head class="text-xs font-semibold">Distinct Entries</th> <th ui-table-head class="text-xs font-semibold">Bit Width</th> <th ui-table-head class="text-xs font-semibold">Dict Page Offset</th> <th ui-table-head class="text-xs font-semibold">Space Savings</th> </tr> </thead> <tbody ui-table-body> @for (col of dictionaryColumns(); track col.name) { <tr ui-table-row class="text-xs"> <td ui-table-cell class="text-foreground font-mono font-medium">{{ col.name }}</td> <td ui-table-cell class="font-mono tabular-nums"> <span ui-badge variant="secondary" class="font-mono text-xs"> {{ col.dictEntries?.toLocaleString() }} values </span> </td> <td ui-table-cell class="font-mono tabular-nums">{{ col.bitWidth }} bits / row</td> <td ui-table-cell class="text-muted-foreground font-mono text-xs"> {{ col.dictPageOffset }} </td> <td ui-table-cell> <div class="flex items-center gap-2"> <span class="text-success font-mono font-semibold tabular-nums"> {{ col.compressionRatio }} </span> <span class="text-muted-foreground font-mono text-xs">({{ col.compressedBytes }})</span> </div> </td> </tr> } </tbody> </table> </div> </div> </div> </ui-tabs-content> <!-- TAB 4: Parquet Header JSON (Thrift) --> <ui-tabs-content value="thrift" class="mt-4 space-y-4"> <div class="border-border bg-muted/30 overflow-hidden rounded-lg border"> <div class="border-border bg-card flex items-center justify-between gap-x-2 border-b px-4 py-2.5"> <div class="flex items-center gap-2"> <lucide-icon [img]="Code2" class="text-primary size-4" /> <span class="text-foreground font-mono text-xs font-medium">FileMetaData.thrift.json</span> <span ui-badge variant="secondary" class="font-mono text-xs">Thrift Compact Protocol</span> </div> <button ui-button variant="ghost" size="sm" class="h-7 gap-1 text-xs" (click)="copyJsonToClipboard()"> @if (copiedJson()) { <lucide-icon [img]="Check" class="text-success size-3" /> } @else { <lucide-icon [img]="Copy" class="size-3" /> } <span>{{ copiedJson() ? 'Copied' : 'Copy JSON' }}</span> </button> </div> <div class="overflow-x-auto bg-neutral-950 p-4 font-mono text-xs leading-relaxed text-neutral-100 dark:bg-neutral-950" > <pre class="whitespace-pre"><code>{{ headerJson() }}</code></pre> </div> </div> </ui-tabs-content> </ui-tabs> </div> </div> `, }) export class UiParquetMetadataInspectorComponent { protected readonly ArrowRight = ArrowRight protected readonly Binary = Binary protected readonly Check = Check protected readonly Code2 = Code2 protected readonly Columns3 = Columns3 protected readonly Copy = Copy protected readonly Download = Download protected readonly FileCode2 = FileCode2 protected readonly FolderTree = FolderTree protected readonly HardDrive = HardDrive protected readonly Layers = Layers protected readonly Search = Search protected readonly Table2 = Table2 readonly filePath = signal('s3://lakehouse-gold/orders_2026_q3_part0042.parquet') readonly formatSpec = signal('Apache Parquet v2.10 · Snappy Compressed') readonly totalRows = signal('250,000 Rows') readonly fileSize = signal('18.4 MB') readonly compressionRatio = signal('74.2%') readonly uncompressedSize = signal('71.4 MB') readonly rowGroups = signal<RowGroupMeta[]>(defaultRowGroupsData.map((rg) => ({ ...rg, columns: [...rg.columns] }))) @Input('filePath') set filePathAttr(v: string | undefined) { if (v !== undefined) this.filePath.set(v) } @Input('formatSpec') set formatSpecAttr(v: string | undefined) { if (v !== undefined) this.formatSpec.set(v) } @Input('totalRows') set totalRowsAttr(v: string | undefined) { if (v !== undefined) this.totalRows.set(v) } @Input('fileSize') set fileSizeAttr(v: string | undefined) { if (v !== undefined) this.fileSize.set(v) } @Input('compressionRatio') set compressionRatioAttr(v: string | undefined) { if (v !== undefined) this.compressionRatio.set(v) } @Input('uncompressedSize') set uncompressedSizeAttr(v: string | undefined) { if (v !== undefined) this.uncompressedSize.set(v) } @Input('rowGroups') set rowGroupsAttr(v: RowGroupMeta[] | undefined) { if (v !== undefined) this.rowGroups.set(v.map((rg) => ({ ...rg, columns: [...rg.columns] }))) } @Input('class') className?: string readonly selectedRowGroup = signal(0) readonly activeTab = signal('chunks') readonly searchQuery = signal('') readonly encodingFilter = signal<'all' | 'dictionary' | 'plain'>('all') readonly copiedJson = signal(false) readonly copiedUri = signal(false) readonly downloadingJson = signal(false) protected readonly nodes = signal<SchemaTreeNode[]>(schemaTreeNodes) readonly activeRowGroup = computed(() => { const groups = this.rowGroups() return groups[this.selectedRowGroup()] ?? groups[0]! }) readonly filteredColumns = computed(() => { const query = this.searchQuery().trim().toLowerCase() return this.activeRowGroup().columns.filter((col) => { const matchesSearch = !query || col.name.toLowerCase().includes(query) || col.physicalType.toLowerCase().includes(query) || col.logicalType.toLowerCase().includes(query) || col.encodings.some((enc) => enc.toLowerCase().includes(query)) const matchesEncoding = this.encodingFilter() === 'all' || (this.encodingFilter() === 'dictionary' && col.isDictionaryEncoded) || (this.encodingFilter() === 'plain' && !col.isDictionaryEncoded) return matchesSearch && matchesEncoding }) }) readonly dictionaryColumns = computed(() => this.activeRowGroup().columns.filter((col) => col.isDictionaryEncoded)) headerJson(): string { return parquetHeaderJson } get rootClass(): string { return cn( 'bg-background text-foreground border-border w-full overflow-hidden rounded-xl border shadow-xs', this.className, ) } rowGroupClass(id: number): string { return cn( 'cursor-pointer rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors', this.selectedRowGroup() === id ? 'bg-primary/10 text-primary border-primary/30 shadow-xs' : 'bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground border-transparent', ) } encodingClass(filter: 'all' | 'dictionary' | 'plain'): string { return cn( 'cursor-pointer rounded px-2 py-1 transition-colors', this.encodingFilter() === filter ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', ) } nodeNameClass(isGroup?: boolean): string { return cn('truncate font-mono font-medium', isGroup ? 'text-primary font-bold' : 'text-foreground') } indentRem(depth: number): number { return Math.max(1, depth * 1.5 + 1) } copyJsonToClipboard(): void { if (typeof navigator !== 'undefined' && navigator.clipboard) { void navigator.clipboard.writeText(parquetHeaderJson) this.copiedJson.set(true) setTimeout(() => this.copiedJson.set(false), 2000) } } copyUriToClipboard(): void { if (typeof navigator !== 'undefined' && navigator.clipboard) { void navigator.clipboard.writeText(this.filePath()) this.copiedUri.set(true) setTimeout(() => this.copiedUri.set(false), 2000) } } handleDownloadJson(): void { this.downloadingJson.set(true) setTimeout(() => { if (typeof document !== 'undefined') { const blob = new Blob([parquetHeaderJson], { type: 'application/json' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = 'parquet-file-metadata.json' document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) } this.downloadingJson.set(false) }, 400) } }src/app/components/blocks/parquet-metadata-inspector/parquet-metadata-data.ts13.8 kB
import type { RowGroupMeta } from './parquet-metadata-types' export const parquetHeaderJson = `{ "version": 2, "created_by": "parquet-mr version 1.13.1 (build e29fa2b)", "num_rows": 250000, "num_row_groups": 2, "file_size_bytes": 19293798, "uncompressed_bytes": 74868326, "compression_codec": "SNAPPY", "encryption_algorithm": null, "schema": [ { "name": "schema", "num_children": 10 }, { "name": "order_id", "type": "FIXED_LEN_BYTE_ARRAY", "type_length": 16, "repetition_type": "REQUIRED", "field_id": 1, "converted_type": "UUID" }, { "name": "customer_id", "type": "INT64", "repetition_type": "REQUIRED", "field_id": 2, "converted_type": "INT_64" }, { "name": "order_status", "type": "BYTE_ARRAY", "repetition_type": "OPTIONAL", "field_id": 3, "converted_type": "UTF8" }, { "name": "gross_amount", "type": "FIXED_LEN_BYTE_ARRAY", "type_length": 9, "repetition_type": "REQUIRED", "field_id": 4, "converted_type": "DECIMAL", "scale": 2, "precision": 18 }, { "name": "created_at", "type": "INT64", "repetition_type": "REQUIRED", "field_id": 5, "converted_type": "TIMESTAMP_MICROS" }, { "name": "customer_metadata", "num_children": 2, "repetition_type": "OPTIONAL", "field_id": 6 }, { "name": "loyalty_tier", "type": "BYTE_ARRAY", "repetition_type": "OPTIONAL", "field_id": 7, "converted_type": "UTF8" }, { "name": "lifetime_orders", "type": "INT32", "repetition_type": "OPTIONAL", "field_id": 8, "converted_type": "INT_32" }, { "name": "line_items", "num_children": 1, "repetition_type": "OPTIONAL", "field_id": 9, "converted_type": "LIST" }, { "name": "list", "num_children": 1, "repetition_type": "REPEATED" }, { "name": "element", "num_children": 2, "repetition_type": "REQUIRED" }, { "name": "sku", "type": "BYTE_ARRAY", "repetition_type": "REQUIRED", "field_id": 12, "converted_type": "UTF8" }, { "name": "quantity", "type": "INT32", "repetition_type": "REQUIRED", "field_id": 13, "converted_type": "INT_32" }, { "name": "shipping_country", "type": "BYTE_ARRAY", "repetition_type": "OPTIONAL", "field_id": 14, "converted_type": "UTF8" }, { "name": "discount_rate", "type": "FLOAT", "repetition_type": "OPTIONAL", "field_id": 15 }, { "name": "fulfillment_node", "type": "BYTE_ARRAY", "repetition_type": "OPTIONAL", "field_id": 16, "converted_type": "UTF8" } ], "row_groups": [ { "ordinal": 0, "num_rows": 125000, "total_byte_size": 37434163, "total_compressed_size": 9646899, "file_offset": 4, "columns_count": 14 }, { "ordinal": 1, "num_rows": 125000, "total_byte_size": 37434163, "total_compressed_size": 9646899, "file_offset": 9646903, "columns_count": 14 } ], "key_value_metadata": [ { "key": "org.apache.spark.sql.parquet.row.metadata", "value": "{\\"type\\":\\"struct\\",\\"fields\\":[...]}" }, { "key": "writer.model.name", "value": "lakehouse-batch-pipeline-gold-orders" }, { "key": "parquet.version", "value": "2.10.0" } ] }` export const defaultRowGroupsData: RowGroupMeta[] = [ { id: 0, numRows: 125000, numRowsFormatted: '125,000 rows', totalByteSize: '35.7 MB', totalCompressedSize: '9.2 MB', compressionRatio: '74.2%', fileOffset: '0x00000004', columns: [ { name: 'order_id', physicalType: 'FIXED_LEN_BYTE_ARRAY(16)', logicalType: 'UUID', fieldId: 1, encodings: ['PLAIN', 'RLE'], compression: 'Snappy', numValues: '125,000', nullCount: 0, nullPct: '0.0%', minStat: '001a4e21-9a7c-4821-bc10-1a29f8c00192', maxStat: 'ffe918b2-3c11-4fa0-8914-df7201bca908', uncompressedBytes: '4.8 MB', compressedBytes: '1.2 MB', compressionRatio: '75.0%', compressionPct: 75, isDictionaryEncoded: false, dictEntries: null, dictPageOffset: null, dataPageOffset: '0x00000020', bitWidth: null, }, { name: 'customer_id', physicalType: 'INT64', logicalType: 'INTEGER(64, true)', fieldId: 2, encodings: ['PLAIN_DICTIONARY', 'RLE'], compression: 'Snappy', numValues: '125,000', nullCount: 0, nullPct: '0.0%', minStat: '10,001', maxStat: '994,210', uncompressedBytes: '3.2 MB', compressedBytes: '0.9 MB', compressionRatio: '71.9%', compressionPct: 71.9, isDictionaryEncoded: true, dictEntries: 54200, dictPageOffset: '0x00125000', dataPageOffset: '0x00140200', bitWidth: 16, }, { name: 'order_status', physicalType: 'BYTE_ARRAY', logicalType: 'STRING (UTF8)', fieldId: 3, encodings: ['PLAIN_DICTIONARY', 'RLE'], compression: 'Snappy', numValues: '125,000', nullCount: 0, nullPct: '0.0%', minStat: "'cancelled'", maxStat: "'shipped'", uncompressedBytes: '1.8 MB', compressedBytes: '0.2 MB', compressionRatio: '88.9%', compressionPct: 88.9, isDictionaryEncoded: true, dictEntries: 5, dictPageOffset: '0x0021a400', dataPageOffset: '0x0021a480', bitWidth: 3, }, { name: 'gross_amount', physicalType: 'FIXED_LEN_BYTE_ARRAY(9)', logicalType: 'DECIMAL(18, 2)', fieldId: 4, encodings: ['PLAIN_DICTIONARY', 'RLE'], compression: 'Snappy', numValues: '125,000', nullCount: 0, nullPct: '0.0%', minStat: '$4.50', maxStat: '$14,820.00', uncompressedBytes: '3.4 MB', compressedBytes: '0.8 MB', compressionRatio: '76.5%', compressionPct: 76.5, isDictionaryEncoded: true, dictEntries: 28450, dictPageOffset: '0x0023a100', dataPageOffset: '0x0026e000', bitWidth: 15, }, { name: 'created_at', physicalType: 'INT64', logicalType: 'TIMESTAMP(MICROS, UTC)', fieldId: 5, encodings: ['DELTA_BINARY_PACKED', 'RLE'], compression: 'Snappy', numValues: '125,000', nullCount: 0, nullPct: '0.0%', minStat: '2026-07-01 00:00:00.124000', maxStat: '2026-07-26 23:59:58.910000', uncompressedBytes: '3.1 MB', compressedBytes: '0.7 MB', compressionRatio: '77.4%', compressionPct: 77.4, isDictionaryEncoded: false, dictEntries: null, dictPageOffset: null, dataPageOffset: '0x002e1000', bitWidth: null, }, { name: 'shipping_country', physicalType: 'BYTE_ARRAY', logicalType: 'STRING (UTF8)', fieldId: 6, encodings: ['PLAIN_DICTIONARY', 'RLE'], compression: 'Snappy', numValues: '125,000', nullCount: 24, nullPct: '0.02%', minStat: "'AE'", maxStat: "'US'", uncompressedBytes: '1.6 MB', compressedBytes: '0.3 MB', compressionRatio: '81.2%', compressionPct: 81.2, isDictionaryEncoded: true, dictEntries: 48, dictPageOffset: '0x0034a000', dataPageOffset: '0x0034a240', bitWidth: 6, }, { name: 'discount_rate', physicalType: 'FLOAT', logicalType: 'FLOAT(32)', fieldId: 7, encodings: ['PLAIN_DICTIONARY', 'RLE'], compression: 'Snappy', numValues: '125,000', nullCount: 0, nullPct: '0.0%', minStat: '0.00', maxStat: '0.35', uncompressedBytes: '1.5 MB', compressedBytes: '0.4 MB', compressionRatio: '73.3%', compressionPct: 73.3, isDictionaryEncoded: true, dictEntries: 12, dictPageOffset: '0x00390100', dataPageOffset: '0x00390180', bitWidth: 4, }, { name: 'fulfillment_node', physicalType: 'BYTE_ARRAY', logicalType: 'STRING (UTF8)', fieldId: 8, encodings: ['PLAIN_DICTIONARY', 'RLE'], compression: 'Snappy', numValues: '125,000', nullCount: 82, nullPct: '0.07%', minStat: "'wh-ap-east-1'", maxStat: "'wh-us-west-2'", uncompressedBytes: '1.7 MB', compressedBytes: '0.3 MB', compressionRatio: '82.4%', compressionPct: 82.4, isDictionaryEncoded: true, dictEntries: 16, dictPageOffset: '0x003e4000', dataPageOffset: '0x003e4120', bitWidth: 4, }, ], }, { id: 1, numRows: 125000, numRowsFormatted: '125,000 rows', totalByteSize: '35.7 MB', totalCompressedSize: '9.2 MB', compressionRatio: '74.2%', fileOffset: '0x00933250', columns: [ { name: 'order_id', physicalType: 'FIXED_LEN_BYTE_ARRAY(16)', logicalType: 'UUID', fieldId: 1, encodings: ['PLAIN', 'RLE'], compression: 'Snappy', numValues: '125,000', nullCount: 0, nullPct: '0.0%', minStat: '00021c90-410a-49bf-a870-19efbc441200', maxStat: 'fff8a910-1840-4221-a109-ccb001928371', uncompressedBytes: '4.8 MB', compressedBytes: '1.2 MB', compressionRatio: '75.0%', compressionPct: 75, isDictionaryEncoded: false, dictEntries: null, dictPageOffset: null, dataPageOffset: '0x00933270', bitWidth: null, }, { name: 'customer_id', physicalType: 'INT64', logicalType: 'INTEGER(64, true)', fieldId: 2, encodings: ['PLAIN_DICTIONARY', 'RLE'], compression: 'Snappy', numValues: '125,000', nullCount: 0, nullPct: '0.0%', minStat: '10,042', maxStat: '995,800', uncompressedBytes: '3.2 MB', compressedBytes: '0.9 MB', compressionRatio: '71.9%', compressionPct: 71.9, isDictionaryEncoded: true, dictEntries: 53900, dictPageOffset: '0x00a58000', dataPageOffset: '0x00a73000', bitWidth: 16, }, { name: 'order_status', physicalType: 'BYTE_ARRAY', logicalType: 'STRING (UTF8)', fieldId: 3, encodings: ['PLAIN_DICTIONARY', 'RLE'], compression: 'Snappy', numValues: '125,000', nullCount: 0, nullPct: '0.0%', minStat: "'cancelled'", maxStat: "'shipped'", uncompressedBytes: '1.8 MB', compressedBytes: '0.2 MB', compressionRatio: '88.9%', compressionPct: 88.9, isDictionaryEncoded: true, dictEntries: 5, dictPageOffset: '0x00b41000', dataPageOffset: '0x00b41080', bitWidth: 3, }, { name: 'gross_amount', physicalType: 'FIXED_LEN_BYTE_ARRAY(9)', logicalType: 'DECIMAL(18, 2)', fieldId: 4, encodings: ['PLAIN_DICTIONARY', 'RLE'], compression: 'Snappy', numValues: '125,000', nullCount: 0, nullPct: '0.0%', minStat: '$3.90', maxStat: '$16,240.00', uncompressedBytes: '3.4 MB', compressedBytes: '0.8 MB', compressionRatio: '76.5%', compressionPct: 76.5, isDictionaryEncoded: true, dictEntries: 28120, dictPageOffset: '0x00b65000', dataPageOffset: '0x00b99000', bitWidth: 15, }, { name: 'created_at', physicalType: 'INT64', logicalType: 'TIMESTAMP(MICROS, UTC)', fieldId: 5, encodings: ['DELTA_BINARY_PACKED', 'RLE'], compression: 'Snappy', numValues: '125,000', nullCount: 0, nullPct: '0.0%', minStat: '2026-07-27 00:00:01.002000', maxStat: '2026-08-21 14:28:00.000000', uncompressedBytes: '3.1 MB', compressedBytes: '0.7 MB', compressionRatio: '77.4%', compressionPct: 77.4, isDictionaryEncoded: false, dictEntries: null, dictPageOffset: null, dataPageOffset: '0x00c12000', bitWidth: null, }, { name: 'shipping_country', physicalType: 'BYTE_ARRAY', logicalType: 'STRING (UTF8)', fieldId: 6, encodings: ['PLAIN_DICTIONARY', 'RLE'], compression: 'Snappy', numValues: '125,000', nullCount: 18, nullPct: '0.01%', minStat: "'AE'", maxStat: "'US'", uncompressedBytes: '1.6 MB', compressedBytes: '0.3 MB', compressionRatio: '81.2%', compressionPct: 81.2, isDictionaryEncoded: true, dictEntries: 48, dictPageOffset: '0x00c7b000', dataPageOffset: '0x00c7b240', bitWidth: 6, }, { name: 'discount_rate', physicalType: 'FLOAT', logicalType: 'FLOAT(32)', fieldId: 7, encodings: ['PLAIN_DICTIONARY', 'RLE'], compression: 'Snappy', numValues: '125,000', nullCount: 0, nullPct: '0.0%', minStat: '0.00', maxStat: '0.35', uncompressedBytes: '1.5 MB', compressedBytes: '0.4 MB', compressionRatio: '73.3%', compressionPct: 73.3, isDictionaryEncoded: true, dictEntries: 12, dictPageOffset: '0x00cc1000', dataPageOffset: '0x00cc1080', bitWidth: 4, }, { name: 'fulfillment_node', physicalType: 'BYTE_ARRAY', logicalType: 'STRING (UTF8)', fieldId: 8, encodings: ['PLAIN_DICTIONARY', 'RLE'], compression: 'Snappy', numValues: '125,000', nullCount: 64, nullPct: '0.05%', minStat: "'wh-ap-east-1'", maxStat: "'wh-us-west-2'", uncompressedBytes: '1.7 MB', compressedBytes: '0.3 MB', compressionRatio: '82.4%', compressionPct: 82.4, isDictionaryEncoded: true, dictEntries: 16, dictPageOffset: '0x00d15000', dataPageOffset: '0x00d15120', bitWidth: 4, }, ], }, ]src/app/components/blocks/parquet-metadata-inspector/parquet-metadata-types.ts0.7 kB
export interface ColumnChunk { name: string physicalType: string logicalType: string fieldId: number encodings: string[] compression: string numValues: string nullCount: number nullPct: string minStat: string maxStat: string uncompressedBytes: string compressedBytes: string compressionRatio: string compressionPct: number isDictionaryEncoded: boolean dictEntries: number | null dictPageOffset: string | null dataPageOffset: string bitWidth: number | null } export interface RowGroupMeta { id: number numRows: number numRowsFormatted: string totalByteSize: string totalCompressedSize: string compressionRatio: string fileOffset: string columns: ColumnChunk[] }src/app/components/blocks/parquet-metadata-inspector/parquet-schema.ts4.8 kB
export interface SchemaTreeNode { id: number path: string name: string repetition: 'REQUIRED' | 'OPTIONAL' | 'REPEATED' physicalType: string logicalType: string defLevel: number repLevel: number depth: number isGroup?: boolean description: string } export const schemaTreeNodes: SchemaTreeNode[] = [ { id: 1, path: 'schema.order_id', name: 'order_id', repetition: 'REQUIRED', physicalType: 'FIXED_LEN_BYTE_ARRAY(16)', logicalType: 'UUID', defLevel: 0, repLevel: 0, depth: 0, description: 'Unique RFC 4122 order identifier binary token.', }, { id: 2, path: 'schema.customer_id', name: 'customer_id', repetition: 'REQUIRED', physicalType: 'INT64', logicalType: 'INTEGER(64, true)', defLevel: 0, repLevel: 0, depth: 0, description: 'Foreign purchaser identity key.', }, { id: 3, path: 'schema.order_status', name: 'order_status', repetition: 'OPTIONAL', physicalType: 'BYTE_ARRAY', logicalType: 'STRING (UTF8)', defLevel: 1, repLevel: 0, depth: 0, description: 'Lifecycle transition stage enumerated string.', }, { id: 4, path: 'schema.gross_amount', name: 'gross_amount', repetition: 'REQUIRED', physicalType: 'FIXED_LEN_BYTE_ARRAY(9)', logicalType: 'DECIMAL(18, 2)', defLevel: 0, repLevel: 0, depth: 0, description: 'High-precision billing total charge with 2 scale points.', }, { id: 5, path: 'schema.created_at', name: 'created_at', repetition: 'REQUIRED', physicalType: 'INT64', logicalType: 'TIMESTAMP(MICROS, UTC)', defLevel: 0, repLevel: 0, depth: 0, description: 'UTC microsecond epoch transaction timestamp.', }, { id: 6, path: 'schema.customer_metadata', name: 'customer_metadata', repetition: 'OPTIONAL', physicalType: 'GROUP', logicalType: 'STRUCT', defLevel: 1, repLevel: 0, depth: 0, isGroup: true, description: 'Nested profile attributes struct.', }, { id: 7, path: 'schema.customer_metadata.loyalty_tier', name: 'loyalty_tier', repetition: 'OPTIONAL', physicalType: 'BYTE_ARRAY', logicalType: 'STRING (UTF8)', defLevel: 2, repLevel: 0, depth: 1, description: 'Customer loyalty tier status membership level.', }, { id: 8, path: 'schema.customer_metadata.lifetime_orders', name: 'lifetime_orders', repetition: 'OPTIONAL', physicalType: 'INT32', logicalType: 'INTEGER(32, true)', defLevel: 2, repLevel: 0, depth: 1, description: 'Cumulative prior purchases count.', }, { id: 9, path: 'schema.line_items', name: 'line_items', repetition: 'OPTIONAL', physicalType: 'GROUP', logicalType: 'LIST', defLevel: 1, repLevel: 0, depth: 0, isGroup: true, description: '3-level Parquet Standard list layout.', }, { id: 10, path: 'schema.line_items.list', name: 'list', repetition: 'REPEATED', physicalType: 'GROUP', logicalType: 'BAG', defLevel: 2, repLevel: 1, depth: 1, isGroup: true, description: 'Repeated list item wrapper group.', }, { id: 11, path: 'schema.line_items.list.element', name: 'element', repetition: 'REQUIRED', physicalType: 'GROUP', logicalType: 'STRUCT', defLevel: 2, repLevel: 1, depth: 2, isGroup: true, description: 'Item tuple struct.', }, { id: 12, path: 'schema.line_items.list.element.sku', name: 'sku', repetition: 'REQUIRED', physicalType: 'BYTE_ARRAY', logicalType: 'STRING (UTF8)', defLevel: 2, repLevel: 1, depth: 3, description: 'Stock keeping unit barcode tag.', }, { id: 13, path: 'schema.line_items.list.element.quantity', name: 'quantity', repetition: 'REQUIRED', physicalType: 'INT32', logicalType: 'INTEGER(32, true)', defLevel: 2, repLevel: 1, depth: 3, description: 'Purchased quantity count.', }, { id: 14, path: 'schema.shipping_country', name: 'shipping_country', repetition: 'OPTIONAL', physicalType: 'BYTE_ARRAY', logicalType: 'STRING (UTF8)', defLevel: 1, repLevel: 0, depth: 0, description: 'ISO-3166-1 alpha-2 destination country.', }, { id: 15, path: 'schema.discount_rate', name: 'discount_rate', repetition: 'OPTIONAL', physicalType: 'FLOAT', logicalType: 'FLOAT(32)', defLevel: 1, repLevel: 0, depth: 0, description: 'Promotional discount multiplier applied.', }, { id: 16, path: 'schema.fulfillment_node', name: 'fulfillment_node', repetition: 'OPTIONAL', physicalType: 'BYTE_ARRAY', logicalType: 'STRING (UTF8)', defLevel: 1, repLevel: 0, depth: 0, description: 'Assigned logistics warehouse cluster.', }, ]src/app/components/blocks/parquet-metadata-inspector/index.ts0.2 kB
export { UiParquetMetadataInspectorComponent } from './parquet-metadata-inspector.component' export type { ColumnChunk, RowGroupMeta } from './parquet-metadata-types' export type { SchemaTreeNode } from './parquet-schema'
Raw manifest:https://uipkge.dev/r/angular/parquet-metadata-inspector.json