Skip to content

TIMX 738 - Parquet IO - #281

Merged
ghukill merged 3 commits into
mainfrom
timx-738-parquet-io
Nov 3, 2026
Merged

ghukill merged 3 commits into
mainfrom
timx-738-parquet-io

Conversation

@ghukill

@ghukill ghukill commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Note

This is the first of three stacked PRs.

Purpose and background context

This PR updates Transmogrifier to accept parquet files as input and output. This builds on work from TIMX-700 that introduced the TIMDEX dataset as an input source.

Before that work, the only input source for Transmogrifier was individual files either locally or in S3. Dataset as input unlocked running a transformation job against a subset of records in the dataset (e.g. all current records for a source) without the need to re-harvest. Parquet as input allows transformation of a subset of records, where the subset is most likely created by Timmy.

The updated README shows the new CLI arguments, whichi also introduce an --output-file to provide some parity with --input-file.

It is worth observing that in TIMDEX, Transmogrifier is the step/application that creates an ETL run in the datatset. Therefore, using parquet files as input extends the idea of a "patch" run -- as opposed to a "daily" or "full" -- because we are transforming source metadata records already in the dataset; they may not have come directly from the dataset like --input-dataset, but the parquet files are likely a direct export of those records.

There are two primary things we get from these new input/output options:

  1. Creation of a subset in Timmy, exported to parquet, becomes a true "patch" ETL job of a subset of records.

  2. Timmy exports a subset, Transmogrifier transforms and writes back to parquet, Timmy imports and analysis is performed showing what the transformation did to the records compared to the previous versions.

It is # 2 that drove this PR, as we now can run huge alma transformations jobs using parquet as input and output, with the goal being analysis in Timmy. Once we are satisfied with the results of the transformation logic, we will likely use --input-dataset for a "real" ETL job that also writes back to the TIMDEX dataset.

How can a reviewer manually see the effects of these changes?

1- Set TIMDEX Dev1 AWS credentials

2- Perform a Transmogrifier run using parquet files from S3, and write them locally for quick analysis:

uv run transform \
--verbose \
--source alma \
--input-parquet s3://timdex-extract-dev-222053980223/timmy/exports/alma-subject-criticism-and-interpretation/ \
--output-parquet /tmp/timmy/exports/alma-subject-criticism-and-interpretation

You should see some similar output:

2026-11-03 09:27:18,862 INFO transmogrifier.cli.main(): Logger 'root' configured with level=INFO
2026-11-03 09:27:18,862 INFO transmogrifier.cli.main(): No Sentry DSN found, exceptions will not be sent to Sentry
2026-11-03 09:27:18,862 INFO transmogrifier.cli.main(): Running transform for source alma
2026-11-03 09:27:20,062 INFO transmogrifier.sources.transformer.parse_source_records_from_parquet(): Reading source records from 1 parquet file(s)
2026-11-03 09:32:14,901 INFO transmogrifier.sources.transformer.write_to_parquet_files(): Parquet files write complete - files: 1, rows: 13941
2026-11-03 09:32:14,902 INFO transmogrifier.cli.main(): Completed transform, total records processed: 13941, transformed records: 13941, skipped records: 0, deleted records: 0
2026-11-03 09:32:14,902 INFO transmogrifier.cli.main(): Total time to complete transform: 0:04:56.040478

3- Quick analysis of the output parquet using DuckDB. First, start a shell:

duckdb

Then select some rows from the parquet data output by Transmogrifier:

select *
from read_parquet('/tmp/timmy/exports/alma-subject-criticism-and-interpretation')
limit 3;

You should see rows that contain columns [timdex_record_id, source_record, transformed_record, source, run_date, run_type, action, run_id, run_record_offset, run_timestamp]. By no accident or coincidence, these are precisely what Transmogrifier would otherwise write to the TIMDEX dataset.

We might start to consider this the most minimal "Submission Information Package" (SIP) for TIMDEX, as it includes the source + transformed record and metadata about the ETL run that produced it.

4- Lastly, we can demonstrate the roundtrip-ability of this data by using what Transmogrifier just output as input for another transformation:

uv run transform \
--source alma \
--input-parquet /tmp/timmy/exports/alma-subject-criticism-and-interpretation \
--output-parquet /tmp/timmy/exports/alma-subject-criticism-and-interpretation-v2

Obviously the outputs will be the same, but it demonstrates the input + output schema is identical. This demonstrates how Timmy can either produce or consume the parquet "SIPs" as well.

Includes new or updated dependencies?

NO

Changes expectations for external applications?

YES: this opens the door to a "patch" ETL run consisting of a subset of records that came from the TIMDEX dataset.

What are the relevant tickets?

Code review

  • Code review best practices are documented here and you are encouraged to have a constructive dialogue with your reviewers about their preferences and expectations.

Why these changes are being introduced:
* Re-transforming a subset of records and comparing before/after in Timmy
requires reading source records from a Timmy parquet export, and writing
transformed records Timmy can import.

How this addresses that need:
* Add CLI option --input-parquet, which streams source records from a parquet
file or directory (e.g. a Timmy export), filtered by source and
action='index', in sorted file order.  Non-parquet files are ignored.
* Treat parquet input as a 'patch' run, the same as dataset input.
* Add CLI option --output-parquet, which writes parquet files matching the
Timmy export schema and layout: transformed_record as string,
run_record_offset as int64, no partitioning or metadata, 100k rows per file,
5k rows per row group, ZSTD compression, and rows in run order.
* Require exactly one input option and exactly one output option;
--output-dataset is no longer required on its own.
* Add pyarrow as a direct dependency.

Side effects of this change:
* None for existing --input-file / --input-dataset / --output-dataset runs.

Relevant ticket(s):
* https://mitlibraries.atlassian.net/browse/TIMX-738
Why these changes are being introduced:
* A plain file of transformed records is handy for local debugging and quick
inspection, without writing a TIMDEX dataset or parquet files.

How this addresses that need:
* Add CLI option --output-file, which writes transformed records to a local or
S3 JSONLines file, one per line.
* Only records with action 'index' are written; deleted, skipped, and errored
records are still counted and logged.

Side effects of this change:
* None.

Relevant ticket(s):
* https://mitlibraries.atlassian.net/browse/TIMX-738
Why these changes are being introduced:
* The README CLI section still showed --output-location, and did not
describe the parquet and file input/output options.

How this addresses that need:
* Add tables of input and output options, and refresh the transform --help
output.

Side effects of this change:
* None.

Relevant ticket(s):
* https://mitlibraries.atlassian.net/browse/TIMX-738
@ghukill
ghukill added this pull request to stack #284 November 3, 2026 02:16
@ghukill
ghukill marked this pull request as ready for review November 3, 2026 14:48
@ghukill
ghukill requested a review from a team as a code owner November 3, 2026 14:48
Comment on lines +462 to +470
def write_to_file(self, output_file: str) -> int:
"""Write transformed records to a JSONLines file, one record per line.

Only records with action 'index' are written. Deleted, skipped, and errored
records are counted and logged as usual, but not written.

Args:
output_file: Local or S3 filepath to write to.
"""

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jonavellecuerdo - here is the (re)introduction of Transmogrifier also supporting writing to a simple file. Again, may not get used much, but we almost get it for free and it provides nice parity I think at the input + output formats.

@ehanson8 ehanson8 self-assigned this Nov 3, 2026

@ehanson8 ehanson8 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good and works as expected!

raise ValueError(
"Exactly one input source is required: 'source_file' or 'source_dataset'"
)
if len([i for i in (source_file, source_dataset, source_parquet) if i]) != 1:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting way for the agent to address this! 😆

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought the same! It's not what I would have written... but I'm growing to like it.

@ghukill
ghukill merged commit a9dc0cf into main Nov 3, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants