<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[RowCove Data Workflows]]></title><description><![CDATA[Practical guides to inspect large JSONL, Parquet and Arrow datasets, trace source shards and verify edits. Local workflows with reproducible mock data.]]></description><link>https://rowcove.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 04 Nov 2026 11:08:25 GMT</lastBuildDate><atom:link href="https://rowcove.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[A clean first shard does not validate a training dataset]]></title><description><![CDATA[Check later training-data shards for changed ID types and missing fields. Runnable mock Python checks distinguish file schemas from record validation.]]></description><link>https://rowcove.hashnode.dev/check-schema-drift-jsonl-parquet-arrow-shards</link><guid isPermaLink="true">https://rowcove.hashnode.dev/check-schema-drift-jsonl-parquet-arrow-shards</guid><category><![CDATA[Parquet]]></category><category><![CDATA[Python]]></category><category><![CDATA[data-engineering]]></category><dc:creator><![CDATA[RowCove]]></dc:creator><pubDate>Tue, 03 Nov 2026 14:19:36 GMT</pubDate><content:encoded><![CDATA[<p>When the first training-data shard opens cleanly, it is tempting to assume the folder is ready. A later export can still change an ID type or omit a field. This guide catches both cases in JSONL, Parquet and Arrow files before you prepare the full dataset.</p>
<p>All files and records in this guide are generated mock data. We build RowCove, a commercial local data workspace, and prepared this guide with AI assistance. The code runs without RowCove or a purchase. It checks a diagnostic method, not a product speed benchmark.</p>
<p>The practical question is narrow: do the identifier, quality and message fields still have the structure the next stage expects? You do not need to prepare a complete training dataset to ask that question. You do need to distinguish a file's declared schema from the values inside its records.</p>
<h2>A reproducible failure, using only mock data</h2>
<p>The accompanying Python reference creates three shards in each of JSONL, Parquet and Arrow IPC file format. Each shard contains three records. The intended contract has a string record_id, numeric quality, and a list of messages containing string role and content fields.</p>
<p>The first shard follows the contract. The second changes record_id to an integer. The third omits quality. Opening only the first shard misses both changes. Automatically coercing every identifier to a number would also be a poor repair: identifier representation can carry meaning, including leading zeroes.</p>
<p>The reference checks all three declared schemas for Parquet and Arrow IPC. It reports the identifier type change in the second file and the missing quality field in the third. For JSONL, which does not carry the same declared table schema, it reads every mock record and checks its required fields and value shapes. Six of the nine JSONL records fail the narrow contract.</p>
<h2>Do not confuse these two scopes</h2>
<p>For Parquet, the example reads file schema metadata with pyarrow.parquet.read_schema. For Arrow IPC files, it opens the file reader and obtains its schema. It does not read all columnar records, validate every data page, or prove that all values satisfy the training application's rules. IPC streams require their own reader and are outside this small example.</p>
<p>For JSONL, the example reads all nine lines. On a real source, sampling the first hundred records would establish only what those records contain. A complete JSONL value check requires parsing the relevant full source. Line-by-line iteration also does not cap the size of a single record; production tools need explicit size and error handling policies.</p>
<p>Our columnar check deliberately focuses on expected field names and types. It is not a complete validator for metadata, extra fields, nullability, vocabulary, duplicate IDs or semantic quality. The JSONL predicate requires present, non-null values of the expected shape. Those scopes are stated separately because equal field types do not establish valid record values.</p>
<h2>Trace before changing</h2>
<p>A useful failure report names the source shard and, for JSONL, the source line. That lets the engineer inspect the actual export rather than repair a disconnected preview. Decide whether the downstream contract should change or the exporter should be corrected before casting, filling or merging fields automatically.</p>
<p>The reference hashes its tiny generated source files before and after inspection and asserts they are unchanged. These hashes verify this mock exercise only. Hashing large real inputs adds I/O, so it must be counted if included in a performance measurement.</p>
<h2>Where a local workspace helps</h2>
<p>If you repeatedly move between scripts, truncated previews and individual source files, keep the failing shard and record location with your findings. A local workspace can make that inspection easier. <a href="https://rowcove.com/?utm_source=hashnode">RowCove</a> provides viewing, supported querying and separate-copy editing on Windows and macOS. Query/edit support varies by format. Free Preview is read-only and shows up to 100 records; Pro enables supported full-file workflows and local MCP tools. This script is a separate educational example, not a list of automated RowCove checks.</p>
<h2>Run the complete example</h2>
<p>Use Python 3.10 or later and a separate virtual environment. The example was executed with PyArrow 23.0.1; pinning it makes the dependency version explicit. Save the code below as <code>check_mock_shards.py</code>.</p>
<pre><code class="language-shell">python -m pip install pyarrow==23.0.1
python check_mock_shards.py
</code></pre>
<p>The script creates its own temporary directory, writes only generated fixtures, checks them and removes that directory when done. It does not accept or open a private dataset path.</p>
<pre><code class="language-python">"""Runnable mock-only content reference; not a RowCove performance benchmark.

Columnar checks compare declared file schemas, not every stored value. JSONL
checks read every mock line against an explicit narrow record contract.
"""
import hashlib
import json
import tempfile
from pathlib import Path

import pyarrow as pa
import pyarrow.parquet as pq


CONTRACT = pa.schema([
    ("record_id", pa.string()),
    ("quality", pa.float64()),
    ("messages", pa.list_(pa.struct([("role", pa.string()), ("content", pa.string())]))),
])


def schema_issues(schema):
    issues = []
    for field in CONTRACT:
        if field.name not in schema.names:
            issues.append(f"missing:{field.name}")
        elif not schema.field(field.name).type.equals(field.type):
            issues.append(f"type:{field.name}")
    return issues


def json_issues(row):
    # This example requires non-null values; Arrow schema types alone don't.
    issues = []
    if not isinstance(row, dict):
        return ["record:not-object"]
    for field in CONTRACT.names:
        if field not in row:
            issues.append(f"missing:{field}")
    if "record_id" in row and not isinstance(row["record_id"], str):
        issues.append("type:record_id")
    if "quality" in row and type(row["quality"]) not in (float, int):
        issues.append("type:quality")
    messages = row.get("messages")
    if "messages" in row and (
        not isinstance(messages, list) or any(
            not isinstance(m, dict)
            or any(not isinstance(m.get(k), str) for k in ("role", "content"))
            for m in messages
        )
    ):
        issues.append("type:messages")
    return issues


def digest(path):
    result = hashlib.sha256()
    with path.open("rb") as source:
        for block in iter(lambda: source.read(1024 * 1024), b""):
            result.update(block)
    return result.hexdigest()


def run(root):
    files = []
    for shard in range(3):
        rows = [{"record_id": f"mock-{shard}-{i:03d}", "quality": 0.8,
                 "messages": [{"role": "user", "content": "Generated mock text."}]}
                for i in range(3)]
        schema = CONTRACT
        if shard == 1:
            schema = CONTRACT.set(0, pa.field("record_id", pa.int64()))
            for i, row in enumerate(rows):
                row["record_id"] = i
        if shard == 2:
            schema = CONTRACT.remove(1)
            for row in rows:
                del row["quality"]
        table = pa.Table.from_pylist(rows, schema=schema)
        parquet = root / f"mock-{shard:03d}.parquet"
        pq.write_table(table, parquet)
        arrow = root / f"mock-{shard:03d}.arrow"
        with pa.OSFile(str(arrow), "wb") as sink:
            with pa.ipc.new_file(sink, schema) as writer:
                writer.write_table(table)
        jsonl = root / f"mock-{shard:03d}.jsonl"
        jsonl.write_text("".join(json.dumps(row) + "\n" for row in rows), encoding="utf-8")
        files.extend([parquet, arrow, jsonl])
    before = {p.name: digest(p) for p in files}
    report = {}
    for extension in ("parquet", "arrow"):
        found = []
        for path in sorted(root.glob(f"*.{extension}")):
            if extension == "parquet":
                schema = pq.read_schema(path)
            else:
                with pa.memory_map(str(path), "r") as source:
                    schema = pa.ipc.open_file(source).schema
            found.append({"shard": path.name, "issues": schema_issues(schema)})
        assert [x["issues"] for x in found] == [[], ["type:record_id"], ["missing:quality"]]
        report[extension] = {"scope": "all three declared file schemas", "shards": found}
    scanned, failures = 0, []
    for path in sorted(root.glob("*.jsonl")):
        with path.open(encoding="utf-8") as source:
            for line_number, line in enumerate(source, 1):
                scanned += 1
                issues = json_issues(json.loads(line))
                if issues:
                    failures.append({"shard": path.name, "line": line_number, "issues": issues})
    assert scanned == 9 and len(failures) == 6
    assert json_issues({"record_id": "mock", "quality": True, "messages": []}) == ["type:quality"]
    assert json_issues({"record_id": "mock", "quality": 1, "messages": [{"content": "mock"}]}) == ["type:messages"]
    null_row = {"record_id": "mock-null", "quality": None, "messages": []}
    null_table = pa.Table.from_pylist([null_row], schema=CONTRACT)
    assert schema_issues(null_table.schema) == []
    assert json_issues(null_row) == ["type:quality"]
    report["jsonl"] = {"scope": "all nine mock records", "records_scanned": scanned, "failures": failures}
    assert before == {p.name: digest(p) for p in files}
    return {"mock_only": True, "pyarrow": pa.__version__, "source_unchanged": True,
            "same_schema_null_value_counterexample_verified": True,
            "performance_claim": False, "formats": report}


if __name__ == "__main__":
    with tempfile.TemporaryDirectory(prefix="rowcove-mock-contract-") as directory:
        print(json.dumps(run(Path(directory)), indent=2))
</code></pre>
<h2>Read the result without overstating it</h2>
<table>
<thead>
<tr>
<th>Format</th>
<th>Scope</th>
<th>Known failures found</th>
</tr>
</thead>
<tbody><tr>
<td>Parquet</td>
<td>Declared schema of all three mock files</td>
<td>ID type in shard 001; missing quality in shard 002</td>
</tr>
<tr>
<td>Arrow IPC file</td>
<td>Declared schema of all three mock files</td>
<td>Same two file-level issues</td>
</tr>
<tr>
<td>JSONL</td>
<td>All nine mock records</td>
<td>Three type failures and three missing-field failures</td>
</tr>
</tbody></table>
<p>The reference also proves a limitation: a table containing a null quality value can have the expected declared schema while failing this JSONL value contract. Schema agreement and value validity are separate checks. Additional assertions reject a boolean quality value and an incomplete nested message. Source hashes match before and after inspection.</p>
<p>This is a correctness exercise with tiny fixtures. It provides no GB/TB timing, peak-memory or disk-space superiority result. Malformed JSON handling, line-size budgets, duplicate field names, complete nullability rules and training-specific semantics need explicit production policies. Do not silently cast all shards to make the report green; establish the intended contract first.</p>
<h2>Questions that change the workflow</h2>
<h3>Does checking a Parquet schema validate every record?</h3>
<p>No. This example checks declared field names and types. A correctly declared numeric field can still contain a null that your training contract rejects. Scan values separately when the task requires it.</p>
<h3>Can the first hundred JSONL records validate all later shards?</h3>
<p>No. They describe that sample. The reference parses every generated JSONL record; a complete claim about your own sources needs the corresponding full scan and error handling.</p>
<h3>Should I merge schemas to make the failures disappear?</h3>
<p>First decide which contract is correct. Combining or casting schemas changes what a loader accepts; it does not establish that the original records are semantically valid. Keep the failing source location so the correction can be reviewed.</p>
<h2>Continue with a specific task</h2>
<ul>
<li><a href="https://rowcove.hashnode.dev/find-record-jsonl-parquet-arrow-shards">Find one record and retain its source shard</a>.</li>
<li><a href="https://rowcove.hashnode.dev/inspect-parquet-without-full-arrow-cache">Separate Parquet inspection from full dataset preparation</a>.</li>
<li><a href="https://rowcove.com/formats?utm_source=hashnode">Review supported formats and limits</a>, or <a href="https://rowcove.com/download?utm_source=hashnode">try RowCove on your own local data</a>.</li>
</ul>
<h2>API references</h2>
<ul>
<li><a href="https://arrow.apache.org/docs/python/generated/pyarrow.parquet.read_schema.html">Apache Arrow: read a Parquet file schema</a>.</li>
<li><a href="https://arrow.apache.org/docs/python/generated/pyarrow.ipc.open_file.html">Apache Arrow: open an Arrow IPC file</a>.</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Inspect Parquet shards without building a full Arrow dataset cache]]></title><description><![CDATA[Inspect Parquet and Arrow shards with typed IDs, bounded batches and source locations. Runnable mock checks explain preparation and cache tradeoffs.]]></description><link>https://rowcove.hashnode.dev/inspect-parquet-without-full-arrow-cache</link><guid isPermaLink="true">https://rowcove.hashnode.dev/inspect-parquet-without-full-arrow-cache</guid><category><![CDATA[Parquet]]></category><category><![CDATA[Python]]></category><category><![CDATA[data-engineering]]></category><dc:creator><![CDATA[RowCove]]></dc:creator><pubDate>Sun, 01 Nov 2026 15:34:32 GMT</pubDate><content:encoded><![CDATA[<p><em>By the RowCove team, developers of a commercial local data workspace. This
article is AI-assisted; the Python reference was executed and checked. All
example records are generated mock data. The reference works without RowCove.</em></p>
<p>The question is small: which shard contains a suspicious training record, and what
does that record contain? The work can still be large. A normal dataset-loading
workflow can prepare another representation on disk before you inspect anything.
For a one-record investigation, that preparation may be more work than the task
needs. For repeated training epochs or indexed access, it may be worth doing.</p>
<p>A Hugging Face user reported this with roughly 500 GB of local Parquet shards:
<a href="https://discuss.huggingface.co/t/load-dataset-creates-a-duplicate-in-cache/175561">the loading operation created a large additional cache</a>.
That is one user's workflow report, not a RowCove customer or a measured result of
our own. It is a useful reminder to choose the operation before choosing the tool.</p>
<p>Hugging Face's <a href="https://huggingface.co/docs/datasets/en/stream">streaming documentation</a>
describes local-file streaming without the complete Arrow conversion. It also
explains the tradeoff: an iterable dataset does not provide ordinary random row
access. A lookup near the end can still require traversing earlier records.
Streaming is useful; it does not make an arbitrary full-file question constant-time.</p>
<h2>Separate preview, search and preparation</h2>
<p>Before starting, decide which answer you need:</p>
<ul>
<li><strong>A preview:</strong> inspect schema and a few values, with a clear sample boundary.</li>
<li><strong>An exact-ID search:</strong> visit the required scope, retaining the source filename
and file-local row ordinal. A negative answer is valid only when the scope is complete.</li>
<li><strong>A prepared training dataset:</strong> pay the conversion and storage cost deliberately
when indexed access or the training pipeline requires it.</li>
</ul>
<p>For a narrow Parquet investigation, select the columns needed for identification
before materializing Python objects. Read bounded batches and retain the source
location. Inspect the full nested payload of the selected record afterward.
For Arrow IPC, the source record-batch layout also matters: selecting fields from
an already obtained batch does not prove that physical reads or decompression
were reduced. Do not equate a small displayed table with a small I/O workload.</p>
<h2>Choose a narrower read before preparing the whole dataset</h2>
<p>For a local Parquet shard, this is the small operation at the center of the
reference below:</p>
<pre><code class="language-python">import pyarrow.parquet as pq

for batch in pq.ParquetFile("mock-000.parquet").iter_batches(
    batch_size=512, columns=["record_id", "status"]
):
    # Compare the ID in its original type; retain source and row location.
    # Continue through the required scope before claiming a negative result.
    pass
</code></pre>
<p>This projects Parquet columns, rather than creating a normal prepared Hugging
Face Dataset first. It does not build an index or promise a constant-time lookup.
A full scan is still a full scan, and duplicate IDs may produce multiple matches.
The standalone script below generates the named mock files before reading them.</p>
<h2>A reference with a known answer</h2>
<p>The accompanying mock reference creates four Parquet shards and four Arrow IPC
shards, each containing 4,096 records. Its identifier exceeds JavaScript's safe
integer range, and each row includes a nested message payload. The inspection
step visits every record, compares typed IDs with PyArrow compute and materializes
only matching positions and scalar values as Python objects. It must find exactly one match in <code>mock-002</code>, at file-local
one-based row ordinal 18, and return the same answer for both formats.</p>
<p>The executed reference verifies 16,384 visited rows per format, at most 512 rows
per returned batch for this fixture, equal results, unchanged source hashes and
no extra files in the fixture directory. It does not invoke Hugging Face Datasets. The extra-file assertion covers only
the mock fixture directory; it is not a claim of zero OS, library or global cache.
It does <strong>not</strong> measure peak RAM, physical bytes read, OS cache activity, training
throughput or RowCove performance. Row count per batch is not a byte-memory limit,
and the IPC assertion depends on the fixture's explicitly chosen batch layout.</p>
<p>The reference then reopens the matched shard and checks the complete nested
message against its known value. Save the following as a Python file and run it
with Python 3.10+ and PyArrow installed (<code>python -m pip install pyarrow</code>).
Tested with Python 3.10.11 and PyArrow 23.0.1. Run <code>python inspect_records.py</code>.
It creates and removes its own temporary mock fixture. Hash verification reads
in chunks; it does not load an entire source file into a Python byte string.</p>
<pre><code class="language-python">"""Mock correctness reference; no RowCove benchmark or cache-size comparison."""
import hashlib
import json
import tempfile
from pathlib import Path

import pyarrow as pa
import pyarrow.compute as pc
import pyarrow.parquet as pq


def digest(path):
    result = hashlib.sha256()
    with path.open('rb') as source:
        for block in iter(lambda: source.read(1024 * 1024), b''):
            result.update(block)
    return result.hexdigest()


def inspect(root):
    schema = pa.schema([
        ('record_id', pa.uint64()), ('status', pa.string()),
        ('messages', pa.list_(pa.struct([
            ('role', pa.string()), ('content', pa.string()),
        ]))),
    ])
    count, shard_rows, batch_rows = 4, 4096, 512
    first_id = 9007199254740993
    target = first_id + 2 * shard_rows + 17
    files = []
    for shard in range(count):
        rows = [
            {'record_id': first_id + shard * shard_rows + i,
             'status': 'review' if first_id + shard * shard_rows + i == target else 'ready',
             'messages': [{'role': 'user',
                           'content': f'Mock shard {shard} record {i}. ' + 'x' * 2048}]}
            for i in range(shard_rows)
        ]
        table = pa.Table.from_pylist(rows, schema=schema)
        parquet = root / f'mock-{shard:03d}.parquet'
        pq.write_table(table, parquet, row_group_size=1024)
        arrow = root / f'mock-{shard:03d}.arrow'
        with pa.OSFile(str(arrow), 'wb') as sink:
            with pa.ipc.new_file(sink, schema) as writer:
                writer.write_table(table, max_chunksize=batch_rows)
        files.extend([parquet, arrow])
    before = {p.name: digest(p) for p in files}
    expected = [('mock-002', 18, target, 'review')]
    results, observations = {}, {}
    for extension in ('parquet', 'arrow'):
        matches, seen, largest_batch = [], 0, 0
        for path in sorted(root.glob('*.' + extension)):
            file_row = 0

            def consume(batch):
                nonlocal file_row, largest_batch
                largest_batch = max(largest_batch, batch.num_rows)
                mask = pc.equal(
                    batch.column('record_id'), pa.scalar(target, type=pa.uint64()),
                )
                # Materialize match positions only; compare IDs as typed uint64.
                for position in pc.indices_nonzero(mask).to_pylist():
                    matches.append((path.stem, file_row + position + 1,
                                    batch.column('record_id')[position].as_py(),
                                    batch.column('status')[position].as_py()))
                file_row += batch.num_rows

            if extension == 'parquet':
                for batch in pq.ParquetFile(path).iter_batches(
                        batch_size=batch_rows, columns=['record_id', 'status']):
                    consume(batch)
            else:
                with pa.memory_map(str(path), 'r') as source:
                    reader = pa.ipc.open_file(source)
                    for index in range(reader.num_record_batches):
                        # Selection here does not prove physical I/O was pruned.
                        consume(reader.get_batch(index).select(['record_id', 'status']))
            seen += file_row
        assert seen == count * shard_rows
        # Fixture IPC batches were explicitly written at this size.
        assert largest_batch &lt;= batch_rows
        assert matches == expected
        # Reopen only the matched shard and verify its complete nested value.
        ordinal = matches[0][1]
        path = root / (matches[0][0] + '.' + extension)
        def full_record(batches):
            remaining = ordinal - 1
            for batch in batches:
                if remaining &lt; batch.num_rows:
                    return batch.slice(remaining, 1).to_pylist()[0]
                remaining -= batch.num_rows
            raise AssertionError('Matched source ordinal is missing')
        if extension == 'parquet':
            row = full_record(pq.ParquetFile(path).iter_batches(batch_size=batch_rows))
        else:
            with pa.memory_map(str(path), 'r') as source:
                reader = pa.ipc.open_file(source)
                row = full_record(
                    reader.get_batch(i) for i in range(reader.num_record_batches)
                )
        assert row == {
            'record_id': target, 'status': 'review',
            'messages': [{'role': 'user',
                          'content': 'Mock shard 2 record 17. ' + 'x' * 2048}],
        }
        results[extension] = matches
        observations[extension] = {'rowsVisited': seen, 'matches': len(matches),
                                   'largestReturnedBatchRows': largest_batch}
    assert results['parquet'] == results['arrow']
    assert before == {p.name: digest(p) for p in files}
    assert {p.name for p in root.iterdir()} == set(before)
    return {'mockOnly': True, 'formats': observations, 'exactId': target,
            'sourceHashesUnchanged': True, 'fullNestedTargetVerified': True,
            'noExtraFilesInFixtureDirectory': True,
            'huggingFaceDatasetsInvoked': False, 'performanceClaim': False,
            'peakMemoryMeasured': False, 'physicalReadBytesMeasured': False,
            'rowcoveAcceptance': False}


if __name__ == '__main__':
    with tempfile.TemporaryDirectory(prefix='rowcove-inspection-') as directory:
        print(json.dumps(inspect(Path(directory))))
</code></pre>
<h2>Evaluate the repeated investigation, including the expensive parts</h2>
<p>A practical evaluation records opening/preparation, complete-query time, peak
memory, temporary storage and the time to verify an output. Count conversion or
index construction when the workflow requires it. Include failed and cancelled
operations; preserve the original shards.</p>
<p>RowCove is a commercial local data workspace for supported viewing, querying and
separate-copy editing on Windows and macOS. Its read-only collections preserve
source/partition context. Single-source Parquet/Arrow edit-copy work currently
has limits of 128 MiB, 200,000 rows and 512 columns; a collection is not a writable
training dataset. Preview shows up to 100 records read-only. Review the current
<a href="https://rowcove.com/formats">reader capabilities</a> before planning a full workflow.</p>
<p>Authorized MCP can return bounded results to compatible AI clients. The local
workspace does not upload source files for ordinary local processing, but an
external AI client may transmit returned content to its provider. Keep sensitive
values out of demonstrations and review the tool scope before granting access.</p>
<p>If this is a one-off inspection, the reference above may be all you need. If
locating source shards, inspecting complete values and checking corrections is
part of your daily work, compare a dedicated workspace against your current
script-and-notebook workflow. Use your own known-answer question and count the
whole job, including preparation and output verification.</p>
<p><a href="https://rowcove.com/guides/query-dataset-shards?utm_source=hashnode">See the local dataset workflow</a>
or <a href="https://rowcove.com/download?utm_source=hashnode">try the read-only Preview on Windows or macOS</a>.
Collections remain read-only; Pro is required for supported full-file work, editing
and MCP. Check capabilities before choosing a plan.</p>
]]></content:encoded></item><item><title><![CDATA[Finding one bad record in a large dataset: JSONL, Parquet and Arrow]]></title><description><![CDATA[Find records across large local shards, inspect nested values and verify saved edits. A practical JSONL, Parquet and Arrow workflow with mock data.]]></description><link>https://rowcove.hashnode.dev/find-record-jsonl-parquet-arrow-shards</link><guid isPermaLink="true">https://rowcove.hashnode.dev/find-record-jsonl-parquet-arrow-shards</guid><category><![CDATA[data-engineering]]></category><category><![CDATA[Python]]></category><category><![CDATA[Parquet]]></category><dc:creator><![CDATA[RowCove]]></dc:creator><pubDate>Sun, 01 Nov 2026 13:53:03 GMT</pubDate><content:encoded><![CDATA[<p>You need to correct one suspicious record in a folder of data shards. Before you can make that correction, you have to find the source file, read the complete value and establish that you have the right record.</p>
<p>That small task can turn into a long detour: convert Parquet into CSV, merge files, write a one-off script, wait for a full scan, then repeat the work because the result lost its source location. For AI training datasets and large research exports, reducing those detours matters as much as making the first table appear quickly.</p>
<p>We build <strong>RowCove</strong>, a Rust-powered local data viewer and editor for Windows and macOS Apple Silicon. Here is a practical workflow for finding and checking records across JSONL, Parquet and Arrow shards, including a reproducible reference you can use with other tools.</p>
<p><strong>All examples and measurements below use generated mock data.</strong> This article was prepared with AI assistance; the reference script was executed and its results checked.</p>
<h2>1. Define the record you need, and where to look</h2>
<p>Start with a question you can finish: <strong>find every record with <code>label = review</code> in the training split, keeping its identifier and source location.</strong></p>
<p>Keep the selected split, files, predicate, requested fields and completion state with the result. A folder listing establishes which files exist; it does not establish that their records have been searched.</p>
<p>Our reference contains six logical records, stored as two shards in each format:</p>
<pre><code class="language-text">mock-shard-story/
  split=train/shard-000.jsonl
  split=train/shard-001.jsonl
  split=train/shard-000.parquet
  split=train/shard-001.parquet
  split=train/shard-000.arrow
  split=train/shard-001.arrow
  mock-records.csv
</code></pre>
<p>The three pairs contain the <strong>same</strong> records. Select one format pair at a time; querying all three pairs together would count each logical record three times.</p>
<p>Only one record has the target label:</p>
<pre><code class="language-json">{"id":9007199254740995,"record_ref":"000129","label":"review","messages":[{"role":"user","content":"Synthetic example 2: comma, quote \" and\nnewline"}]}
</code></pre>
<p>It is the third logical record in <code>shard-000</code> in each format. Record-position and byte-offset conventions vary by reader, so retain the location convention alongside that answer.</p>
<h2>2. Read less unnecessary data, without losing the answer</h2>
<p>Choose the relevant partition and request the fields needed for the investigation. For this task, keep <code>id</code>, <code>record_ref</code>, <code>label</code> and source provenance; inspect <code>messages</code> when reviewing the match.</p>
<table>
<thead>
<tr>
<th>Source</th>
<th>What matters for this task</th>
</tr>
</thead>
<tbody><tr>
<td>JSONL / NDJSON</td>
<td>Inspect nested records directly. Searching a selected text scope still requires parsing that scope.</td>
</tr>
<tr>
<td>Parquet</td>
<td>Project relevant columns and use supported partition pruning to avoid unnecessary work.</td>
</tr>
<tr>
<td>Arrow IPC / Feather</td>
<td>Preserve typed values while browsing supported files and shard collections.</td>
</tr>
<tr>
<td>CSV / TSV</td>
<td>Check quoting, embedded newlines and identifier interpretation when reading an export.</td>
</tr>
</tbody></table>
<p>In RowCove, the shard workflow keeps partition and source context with the results. This saves the step of merging a folder merely to find which file needs attention. Collection browsing is read-only; corrections happen through a supported individual-source workflow.</p>
<p>When you need <strong>all matches</strong>, follow continuation until the query reports completion. A fast first page, or an empty intermediate page, does not prove the entire selected scope was searched. Keep the files unchanged during traversal so pages refer to the same source version.</p>
<h2>3. Inspect the complete value before changing it</h2>
<p>A shortened table cell can conceal the part of a message that explains the problem. Open the complete value and check its type.</p>
<p>Our mock record deliberately includes three easy-to-miss details:</p>
<ul>
<li><code>9007199254740995</code> exceeds JavaScript's safe integer range. A rounded display cannot establish identity.</li>
<li><code>"000129"</code> is text with significant leading zeros.</li>
<li><code>messages</code> is a nested array containing a quote and a newline, not a flat label.</li>
</ul>
<p>The CSV reference stores <code>messages</code> as JSON text inside a quoted field. JSONL, Parquet and Arrow preserve nested structure through their respective readers. Avoid flattening a record merely to make it fit a spreadsheet-shaped view.</p>
<h2>4. Save a separate copy, then check what was written</h2>
<p>Once you have traced the match to its source file, stage the supported correction and save to a new destination. Keep the original available for comparison.</p>
<p>Reopen the output. Verify the changed identifier and value, a few unaffected records, relevant types and the output population. One edited record does not mean the output contains only one record.</p>
<p>This distinction matters for large CSV files: a filtered view does not automatically make <strong>Save copy</strong> a filtered export. Saving a small correction can still require reading and writing the complete source. Budget for that operation when deciding how to perform a recurring cleanup.</p>
<p>RowCove supports separate-copy editing for supported readers. Current Parquet/Arrow edit-copy limits are <strong>128 MiB, 200,000 rows and 512 columns per source</strong>. Larger columnar query support does not imply unrestricted editing. When the source exceeds that budget, retain its provenance and use the appropriate downstream transformation.</p>
<h2>5. Judge performance by a completed task</h2>
<p>The useful question is how long it takes to get a correct answer or verified output, including required preparation.</p>
<p>For context, our published <strong>20 GB synthetic CSV</strong> test reported these RowCove Pro three-run medians on Windows:</p>
<table>
<thead>
<tr>
<th>Operation</th>
<th align="right">Median</th>
</tr>
</thead>
<tbody><tr>
<td>Full-file access plus the first 1,000 records across 16 fields</td>
<td align="right">3.48 s</td>
</tr>
<tr>
<td>Full-file column contains filter, after opening</td>
<td align="right">0.36 s</td>
</tr>
<tr>
<td>Opening through complete numeric sorting and the first result page</td>
<td align="right">10.14 s</td>
</tr>
</tbody></table>
<p>The access and sorting measurements include their required preparation. Timings stop at data return, before screen painting; applications were already running and the OS file cache was not cleared. These are our own measurements, not an independent audit or a claim about every file, format or Mac. <a href="https://rowcove.com/compare">The comparison page gives the task definitions and competitor test availability</a>.</p>
<p>The six-record example in this article checks correctness, <strong>not speed</strong>. It does not measure RowCove performance on JSONL, Parquet or Arrow.</p>
<p>RowCove combines a Rust processing engine with a bounded desktop data view, so large-file work does not require rendering the entire dataset. Its practical value is the time saved across repeated inspection, querying and supported corrections: fewer conversions, fewer disposable scripts and less time rebuilding context.</p>
<h2>Use MCP when it removes another manual step</h2>
<p>A compatible AI client can use RowCove's authorized local MCP tools to investigate permitted data. Set an explicit local root, inspect the installed tool schemas, and specify fields, predicates and result limits. Carry source provenance and completion state into the answer.</p>
<p>The MCP workspace includes tool lists and schemas, test calls, wire logs and history. Supported writes use an explicit confirmation flow. Reopen and check the output rather than treating an AI summary as proof of correctness.</p>
<p>File processing is local and does not require uploading the dataset to a hosted viewer. <strong>Results returned to a connected AI client may reach that client's model provider.</strong> Choose what you share, and keep private paths and records out of public screenshots.</p>
<h2>Reproduce the correctness checks</h2>
<p>The reference below generates all four representations, checks the one expected match, preserves the large integer and leading-zero text, checks nested values and confirms source hashes remain unchanged.</p>
<p>It requires Python and PyArrow (<code>python -m pip install pyarrow</code>). Save it as <code>shard-story-reference.py</code> and run <code>python shard-story-reference.py</code> in a new working directory. It refuses to overwrite an existing <code>mock-shard-story</code> directory. This is a standalone fixture, not RowCove implementation code.</p>
<pre><code class="language-python">"""Synthetic reference fixture, not a RowCove performance benchmark."""
import csv
import hashlib
import json
from pathlib import Path
import pyarrow as pa
import pyarrow.parquet as pq

root=Path('mock-shard-story')
root.mkdir(exist_ok=False)
rows=[{'id':9007199254740993+i,'record_ref':f'{127+i:06d}',
       'label':'review' if i==2 else 'ready',
       'messages':[{'role':'user','content':f'Synthetic example {i}: comma, quote " and\nnewline'}]}
      for i in range(6)]
schema=pa.schema([('id',pa.uint64()),('record_ref',pa.string()),('label',pa.string()),
                  ('messages',pa.list_(pa.struct([('role',pa.string()),('content',pa.string())])))])
files=[]
for shard in range(2):
    subset=rows[shard*3:(shard+1)*3]
    base=root/'split=train'/f'shard-{shard:03d}'
    base.parent.mkdir(exist_ok=True)
    table=pa.Table.from_pylist(subset,schema=schema)
    jsonl=base.with_suffix('.jsonl')
    jsonl.write_text(''.join(json.dumps(row,ensure_ascii=False)+'\n' for row in subset),encoding='utf-8')
    parquet=base.with_suffix('.parquet');pq.write_table(table,parquet)
    arrow=base.with_suffix('.arrow')
    with pa.OSFile(str(arrow),'wb') as sink:
        with pa.ipc.new_file(sink,schema) as writer:writer.write_table(table)
    files.extend([jsonl,parquet,arrow])
csv_path=root/'mock-records.csv'
with csv_path.open('x',encoding='utf-8',newline='') as f:
    writer=csv.DictWriter(f,fieldnames=list(rows[0]));writer.writeheader()
    for row in rows:writer.writerow({**row,'messages':json.dumps(row['messages'])})
files.append(csv_path)
before={str(path):hashlib.sha256(path.read_bytes()).hexdigest() for path in files}
for ext in ['jsonl','parquet','arrow']:
    decoded=[];matches=[]
    for path in sorted((root/'split=train').glob('*.'+ext)):
        if ext=='jsonl':part=[json.loads(line) for line in path.read_text(encoding='utf-8').splitlines()]
        elif ext=='parquet':part=pq.read_table(path).to_pylist()
        else:
            with pa.memory_map(str(path),'r') as source:part=pa.ipc.open_file(source).read_all().to_pylist()
        for row in part:row.pop('split',None)
        decoded.extend(part)
        matches.extend((path.name,index+1,row['id']) for index,row in enumerate(part) if row['label']=='review')
    assert decoded==rows
    assert matches==[(f'shard-000.{ext}',3,9007199254740995)]
with csv_path.open(encoding='utf-8',newline='') as f:
    decoded=list(csv.DictReader(f))
assert decoded[0]['record_ref']=='000127'
assert decoded[2]['id']=='9007199254740995'
assert json.loads(decoded[2]['messages'])==rows[2]['messages']
assert before=={str(path):hashlib.sha256(path.read_bytes()).hexdigest() for path in files}
print(json.dumps({'mockOnly':True,'records':6,'shardsPerFormat':2,'formats':['CSV','JSONL','Parquet','Arrow IPC'],
                  'reviewMatches':1,'targetFileOrdinal':3,'exactIdentifiers':True,'nestedRoundTrip':True,'sourceHashesUnchanged':True,
                  'productAcceptance':False,'performanceClaim':False}))
</code></pre>
<p>Expected checks: six logical records; two shards per format; one <code>review</code> match in <code>shard-000</code>, position 3; exact identifiers; nested-value round trip; unchanged source hashes.</p>
<h2>Try the workflow on the task you repeat</h2>
<p>RowCove supports CSV, TSV, JSON, JSONL, NDJSON, Parquet, Arrow IPC, Feather and additional readers. Operations vary by format. Free Preview shows up to 100 records read-only; supported full-file queries, editing and local MCP require Pro.</p>
<p>Start with one recurring task and compare the time from opening to a checked result. The workflows are documented here:</p>
<ul>
<li><a href="https://rowcove.com/guides/query-dataset-shards">Find source records across dataset shards</a></li>
<li><a href="https://rowcove.com/guides/query-edit-jsonl">Review and save a JSONL correction</a></li>
<li><a href="https://rowcove.com/docs-mcp">Configure local MCP permissions and tools</a></li>
</ul>
<p>What slows your own investigation most: locating the source shard, reading a complete value, or verifying a saved correction? Format, approximate size and your current workaround are enough to describe the task. Please do not post private records or local paths.</p>
]]></content:encoded></item></channel></rss>