Shape / inspect · All operations

Read a file's schema and metadata

A variant of List columns and types

Read a data file's schema, row groups, statistics, header or detected format without loading its data.

Status

In arcform

Runs as a SQL step

arcform carries it as a sql: step today.

The DuckDB step

Read a file's schema, row groups, statistics, header and detected format without loading its rows

It reads the orders table the proofs share. Create it first:

SQL
CREATE TABLE orders AS SELECT * FROM (VALUES
  (1,  'Alice Ng',      'north', DATE '2026-01-03', 120.00, 'widget', 'a|b',   '{"channel":"web","items":[1,2]}'),
  (2,  'alice ng',      'north', DATE '2026-01-05',  80.50, 'gadget', 'b',     '{"channel":"shop","items":[3]}'),
  (3,  'Bob  Okafor',   'south', DATE '2026-01-05', 300.00, 'widget', 'a|c',   '{"channel":"web","items":[4,5,6]}'),
  (4,  'Carla Díaz',    'east',  DATE '2026-01-09',  NULL,  'gizmo',  NULL,    '{"channel":"web","items":[]}'),
  (5,  'Bob Okafor',    'south', DATE '2026-01-12',  45.25, 'gadget', 'c',     '{"channel":"phone","items":[7]}'),
  (6,  'Dev Patel',     'west',  DATE '2026-01-12', 510.00, 'widget', 'a',     '{"channel":"web","items":[8,9]}'),
  (7,  'Carla Diaz',    'east',  DATE '2026-01-20',  99.99, 'gizmo',  'b|c',   '{"channel":"shop","items":[10]}'),
  (8,  'Dev Patel',     'west',  DATE '2026-02-02', 510.00, 'widget', 'a',     '{"channel":"web","items":[8,9]}'),
  (9,  'Eve Larsen',    NULL,    DATE '2026-02-03',  15.00, 'gadget', NULL,    '{"channel":"web","items":[11]}'),
  (10, 'Alice Ng',      'north', DATE '2026-02-10', 220.00, 'gizmo',  'a|b|c', '{"channel":"phone","items":[12,13]}')
) t(order_id, customer, region, order_date, amount, product, tags, meta);

Then run the statement:

SQL
-- 5000 rows written as three row groups of at most 2048 rows
COPY (SELECT i AS id, 'row ' || i AS label FROM range(5000) t(i)) TO '<your_scratch_dir>/inspect_many.parquet' (FORMAT parquet, ROW_GROUP_SIZE 2048);
COPY (SELECT order_id, customer, order_date, amount FROM orders) TO '<your_scratch_dir>/inspect_orders.csv' (HEADER);
CREATE TEMP TABLE file_schema AS SELECT name, duckdb_type FROM parquet_schema('<your_scratch_dir>/inspect_many.parquet') WHERE num_children IS NULL;
CREATE TEMP TABLE file_info AS SELECT num_rows, num_row_groups FROM parquet_file_metadata('<your_scratch_dir>/inspect_many.parquet');
CREATE TEMP TABLE row_groups AS
SELECT row_group_id, stats_min::BIGINT AS id_min, stats_max::BIGINT AS id_max, num_values
FROM parquet_metadata('<your_scratch_dir>/inspect_many.parquet') WHERE path_in_schema = 'id';
CREATE TEMP TABLE csv_format AS
SELECT "Delimiter" AS delimiter, "HasHeader" AS has_header, [c.name FOR c IN "Columns"] AS column_names, [c.type FOR c IN "Columns"] AS column_types
FROM sniff_csv('<your_scratch_dir>/inspect_orders.csv');
SELECT * FROM file_schema;
SELECT * FROM file_info;
SELECT * FROM row_groups ORDER BY row_group_id;
SELECT * FROM csv_format;
SELECT CASE WHEN ((SELECT list((name, duckdb_type) ORDER BY name) FROM file_schema) = [('id', 'BIGINT'), ('label', 'VARCHAR')] AND (SELECT (num_rows, num_row_groups) FROM file_info) = (5000, 3) AND (SELECT list((id_min, id_max, num_values) ORDER BY row_group_id) FROM row_groups) = [(0, 2047, 2048), (2048, 4095, 2048), (4096, 4999, 904)] AND (SELECT (delimiter, has_header) FROM csv_format) = (',', true) AND (SELECT column_names FROM csv_format) = ['order_id', 'customer', 'order_date', 'amount'] AND (SELECT column_types FROM csv_format) = ['BIGINT', 'VARCHAR', 'DATE', 'DOUBLE']) THEN 'as expected' ELSE error('the Parquet footer was not 3 row groups over 5000 rows with their id ranges, or the CSV was not detected as comma-separated with a header and four typed columns') END AS verdict;

Replace <your_scratch_dir> with a directory of your own that you can write to before running the statement.

  • The statement writes inspect_many.parquet and inspect_orders.csv itself, in <your_scratch_dir>.
  • It needs nothing installed beyond DuckDB itself.
  • The statement checks its own result and raises an error when it is wrong: “the Parquet footer was not 3 row groups over 5000 rows with their id ranges, or the CSV was not detected as comma-separated with a header and four typed columns”.
  • It passed on DuckDB v2.0.0 (Cyanoptera) d54cbbe3df.

What other tools call it

Meridian's classification; an independent blind rating agreed on 95.7% of 324 rows (variant, version 3).

Apache DataFusion (1)
  • parquet_metadataread 2026-09-27

    Table function returning row-group and column metadata and statistics of a Parquet file

Apache Hop (1)
  • File metadataread 2026-09-27

    Scan a text file to detect its delimiter, enclosure, encoding and field types

Apache NiFi (2)
  • ExtractAvroMetadataread 2026-09-27

    Extract metadata from the header of an Avro data file

  • IdentifyMimeTyperead 2026-09-27

    Detect the MIME type of content and write it to the mime.type attribute

DuckDB (6)
  • parquet_bloom_proberead 2026-09-27

    parquet_bloom_probe table function

  • parquet_file_metadataread 2026-09-27

    parquet_file_metadata table function

  • parquet_full_metadataread 2026-09-27

    parquet_full_metadata table function

  • parquet_kv_metadataread 2026-09-27

    parquet_kv_metadata table function

  • parquet_metadataread 2026-09-27

    parquet_metadata table function

  • parquet_schemaread 2026-09-27

    parquet_schema table function

Pentaho Data Integration (1)
Polars (3)
VisiData (1)
  • dive-metadataread 2026-09-23

    Open attribute metadata of the HDF5 object in current row

Background

In Apache Parquet, file metadata is written after the data, and readers are expected to read the file metadata first to find the column chunks they need.[1] In Polars, read_parquet_schema gets the schema of a Parquet file without reading data.[2] DuckDB's CSV sniffer infers how to read a CSV file, because CSV files are not self-describing and come in many different dialects.[3]

When to use it

In DuckDB, parquet_metadata supports glob patterns, to query metadata across multiple files in parallel.[4] In DuckDB, DESCRIBE is the easier way to find the column names and types of a Parquet file, while parquet_schema shows the schema as it is held in the file's metadata.[4] Use DESCRIBE when you want the columns as a query will see them, and parquet_schema when you want to see how the file itself records them. (Meridian's view)

In DuckDB, the CSV sniffer detects the dialect of the file, the type of each column and whether the file has a header row.[3] In DuckDB, CSV type detection works on a sample of the file, by default 20,480 rows.[3] In DuckDB, a file the reader cannot jump into, such as a .gz compressed CSV file or stdin, is sampled only from its beginning.[3] Treat the types sniffed from a CSV file as a guess from a sample, and fix them in the read once you have checked them against the whole file. (Meridian's view) In DuckDB, sniff_csv runs the CSV sniffer as a separate step and returns the detected properties as a table with a single row.[3] In DuckDB, the Prompt column of sniff_csv holds a SQL command with the configurations the sniffer detected.[3]

The method

In Apache Parquet, a file consists of one or more row groups, a row group contains exactly one column chunk per column, and column chunks contain one or more pages.[5] In Apache Parquet, the file metadata contains the start locations of all the column chunks.[1] Apache Parquet is designed to separate the metadata from the data, which allows columns to be split across files and one metadata file to reference several Parquet files.[1] The Apache Parquet project began as a joint effort between Twitter and Cloudera, and Apache Parquet 1.0 was released in July 2013.[6]

In DuckDB, parquet_file_metadata returns file-level metadata such as the format version and the encryption algorithm, and parquet_kv_metadata returns custom metadata held as key-value pairs.[4]