Skip to content

TIMX 496 - add migrations folder and run_timestamp migration#147

Merged
ghukill merged 3 commits into
mainfrom
TIMX-496-establish-migrations-and-backfill-migration
Jun 3, 2025
Merged

TIMX 496 - add migrations folder and run_timestamp migration#147
ghukill merged 3 commits into
mainfrom
TIMX-496-establish-migrations-and-backfill-migration

Conversation

@ghukill

@ghukill ghukill commented May 30, 2025

Copy link
Copy Markdown
Contributor

Purpose and background context

This PR introduces a new root level migrations/ folder where we can locate scripts used for making bulk updates to the parquet dataset. As noted in the commit, while not as formal as migrations for SQL databases -- which often support "replaying" the migrations in an upward or downward fashion -- it provides some structure and code-as-documentation for bulk operations we may need to perform on the TIMDEX parquet dataset.

The following is a rendered form of the migrations/ folder README: https://github.com/MITLibraries/timdex-dataset-api/tree/TIMX-496-establish-migrations-and-backfill-migration/migrations.

If we find ourselves needing to do this fairly often, or make invasive changes, we could explore how this migrations/ structure could impose some mandatory guardrails like making it transactional (e.g. a full dataset backup --> perform migration --> remove backup pattern).

The first migration added is related to TIMX-496, backfilling all pre-existing parquet files with a run_timestamp column. The value for that column, for each file, is pulled from the parquet file's creation date in S3. For our purposes, this is accurate enough.

As noted in #146, the order of actually running this migration will be as follows:

  • perform terraform updates in Dev, Stage, Production, thereby picking up new TDA code to write the run_timestamp column for all future runs
  • ensure no TIMDEX StepFunction runs are actively taking place (they can start after the script starts)
  • 👉 run the migration script to backfill old parquet files with a run_timestamp column

Approval and merge of this PR is for code review only; the migration will be manually run from a developer's machine according to this plan.

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

The migration can be run with a --dry-run flag, showing what will happen before we actually rewrite any parquet files.

The following dry-run will not perform any write operations back to S3, but it will download full parquet files locally for a moment in memory. You can ctrl + c anytime once satisfied with early debugging log statements.

The data backup used here, unfortunately, updated the S3 object creaete time on copy so the run_timestamp for each is 2025-05-30. But some clicking around in the real dataset should confirm that it will pickup accurate dates for the real migration work.

1- Set AWS production credentials in terminal

2- Perform a dry run of a backup of the production dataset:

PYTHONPATH=. \
pipenv run python migrations/001_2025_05_30_backfill_run_timestamp_column.py \
s3://timdex-extract-prod-300442551476/dataset-2025-05-30-backup/dataset \
--dry-run

As noted above, you can ctrl + c to kill the process anytime after you are satisfied with how it's working sans actually writing a new parquet file to S3.

Includes new or updated dependencies?

NO

Changes expectations for external applications?

YES(ish)

  • TDA becomes the location for storing code related to "migrations" for the parquet dataset
  • All pre-existing parquet files will now have a run_timestamp column

What are the relevant tickets?

Developer

  • All new ENV is documented in README
  • All new ENV has been added to staging and production environments
  • All related Jira tickets are linked in commit message(s)
  • Stakeholder approval has been confirmed (or is not needed)

Code Reviewer(s)

  • The commit message is clear and follows our guidelines (not just this PR message)
  • There are appropriate tests covering any new functionality
  • The provided documentation is sufficient for understanding any new functionality introduced
  • Any manual tests have been performed or provided examples verified
  • New dependencies are appropriate or there were no changes

Why these changes are being introduced:

We have a need to perform a onetime bulk edit of the parquet dataset,
which is very similar to a SQL database migration: addition of a column
and backfilling of data.

While we may not require the strictness of SQL schema migrations, which
often can be "replayed" in order to upgrade or reverse order to downgrade,
we nonetheless would benefit from some structure and code-as-documentation
for bulk operations we perform on the actual parquet dataset.

How this addresses that need:
* Creates a new root level migrations/ folder
* Includes a README with a proposed simple structure for migrations (mostly
naming conventions)
* Includes our first migration to backfill all pre-existing parquet files
with a new 'run_timestamp' column
  * The run_timestamp will be pulled from the creation date of the parquet
    file in S3, which is close enough for our purposes

Side effects of this change:
* TDA becomes the location for storing code related to "migrations" for
the parquet dataset
* All pre-existing parquet files will now have a run_timestamp column

Relevant ticket(s):
* https://mitlibraries.atlassian.net/browse/TIMX-496
@ghukill ghukill marked this pull request as ready for review May 30, 2025 15:49
@ghukill ghukill requested a review from a team May 30, 2025 15:54
Comment on lines +137 to +164
# Read all rows from the parquet file into a pyarrow Table
# NOTE: memory intensive for very large parquet files, though suitable for onetime
# migration work.
table = parquet_file.read()

# Get S3 object creation date
creation_date = get_s3_object_creation_date(parquet_filepath, dataset.filesystem) # type: ignore[attr-defined]

# Create run_timestamp column using the exact schema definition
num_rows = len(table)
run_timestamp_field = TIMDEX_DATASET_SCHEMA.field("run_timestamp")
run_timestamp_array = pa.array(
[creation_date] * num_rows, type=run_timestamp_field.type
)

# Add the run_timestamp column to the table
table_with_timestamp = table.append_column("run_timestamp", run_timestamp_array)

# Write the updated table back to the same file
if not dry_run:
pq.write_table(
table_with_timestamp, # type: ignore[attr-defined]
parquet_filepath,
filesystem=dataset.filesystem, # type: ignore[attr-defined]
)
logger.info(f"Successfully updated file: {parquet_filepath}")
else:
logger.info(f"DRY RUN: Would update file: {parquet_filepath}")

@ghukill ghukill May 30, 2025

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 think this is arguably the most complex part of this migration (and potentially future migrations). Everything else is loop structure and familiar operations (like getting S3 data).

What we do here is:

  1. read the full parquet file into memory as a pyarrow table
  2. create a new pyarrow field run_timestamp, using our schema TIMDEX_DATASET_SCHEMA to ensure we get the type right
  3. create an array of values for this field, all the same value, that matches the lenght of the parquet file we read into memory
  4. add this to the in-memory pyarrow table as a new column
  5. write the pyarrow table back to S3, overwriting the original parquet file

We have some gaurantees that make this safe:

  • we use our own TIMDEX_DATASET_SCHEMA to ensure the new run_timestamp column has the right type
  • when we perform the overwrite, the schema of the pyarrow table is the same as when we read it from S3

@jonavellecuerdo jonavellecuerdo self-assigned this May 30, 2025

@jonavellecuerdo jonavellecuerdo 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.

Just a couple of clarification questions and minor change requests. I also have some follow-up questions on some of the information included in the PR description:

  1. The PR description reads:

    we could explore how this migrations/ structure could impose some mandatory guardrails like making it transactional (e.g. a full dataset backup --> perform migration --> remove backup pattern).
    Can you clarify what it would mean to make migrations "transactional"? I don't think I understood the example noted in parentheses. 🤔

  2. The PR description reads:

    The data backup used here, unfortunately, updated the S3 object create time on copy so the run_timestamp for each is 2025-05-30. But some clicking around in the real dataset should confirm that it will pickup accurate dates for the real migration work.

    • How are data backups performed in production?
    • Was this backup manually created?
    • When you mention "clicking around in the real dataset", are you referring to "clicking around the actual dataset in S3 and observing the "Last modified" dates shown in the console?"

Comment thread migrations/README.md Outdated

## Structure

Each migration is either a single python file, or a dedicated directory, with that follows the naming convention:

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.

Can you clarify "with ..."? 🤔

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.

Haha, sorry if poorly worded.

The "with" is referring to "Each migration is either a single python file, or a dedicated directory,..."

I intended to convey:

"...each migration is a single python file, or a dedicated directory, and therefore the following naming conventions should be used..."

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.

Got it! If adding this blurb, as suggested in #147 (comment), can you make this small update, too? 🤓

Comment thread migrations/README.md
Comment thread migrations/001_2025_05_30_backfill_run_timestamp_column.py
Comment on lines +98 to +99
if creation_date.tzinfo is None:
creation_date = creation_date.replace(tzinfo=UTC)

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.

To clarify, if creation_date.tzinfo is None, the creation_date value remains the same, the tzinfo is just set to UTC?

Is the following scenario possible:

  1. run-0 has creation_date=datetime.datetime(2025, 6, 3, 15, 10, 50, 996767, tzinfo=datetime.timezone.utc))
  2. run-1 has creation_date=datetime.datetime(2025, 6, 3, 11, 05, 50, 996767, tzinfo=None)
    • Because tzinfo=None, the end value is datetime.datetime(2025, 6, 3, 15, 5, 50, 996767, tzinfo=datetime.timezone.utc).
      ?

When ordering these runs, run-1 appears to have occurred before run-0. 🤔

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.

To clarify, if creation_date.tzinfo is None, the creation_date value remains the same, the tzinfo is just set to UTC?

Yes, that's correct. The timestamps in S3 are UTC when pulled down, so we're just ensuring the value is timezone aware.

Is the following scenario possible:

I think we can safely ignore this edge case, as all dates are coming from S3 and either will or will not (but they will!) have a timezone attached. This code is kind of just generically being safe to add timezones if not present. But it's assuming that all datetimes coming back are coming from the same source... so they are all with UTC timezone... or all without it.

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.

Can we add this lil' blurb to the method definition?:

This assumes that all datetimes coming back are coming from the same source and will be formatted similarly, which means either all values are timezone aware or not.

Comment thread migrations/001_2025_05_30_backfill_run_timestamp_column.py Outdated
@ghukill

ghukill commented Jun 3, 2025

Copy link
Copy Markdown
Contributor Author

@jonavellecuerdo - some answers to your review comment:

  1. The PR description reads:

    we could explore how this migrations/ structure could impose some mandatory guardrails like making it transactional (e.g. a full dataset backup --> perform migration --> remove backup pattern).
    Can you clarify what it would mean to make migrations "transactional"? I don't think I understood the example noted in parentheses. 🤔

  2. The PR description reads:

    The data backup used here, unfortunately, updated the S3 object create time on copy so the run_timestamp for each is 2025-05-30. But some clicking around in the real dataset should confirm that it will pickup accurate dates for the real migration work.

  • How are data backups performed in production?

This has been performed, informally, by making a copy of the s3://<TIMDEX_BUCKET/dataset directory, e.g. to s3://<TIMDEX_BUCKET/dataset_backups/2025-06-03.

We rely on S3 having backups if we were to accidentally lose the /dataset directory. This comment was proposing that we could perform some automatica backups as part of migrations, but that's not established yet.

  • Was this backup manually created?

The dataset backup outlined in the PR was created manually, but I have since removed it. There is a new on here, s3://timdex-extract-prod-300442551476/dataset_backups/2025-06-02/, created yesterday.

But it's tricky: these backups aren't that meaningful / helpful unless we created them automatically at the time a migraiton is run, such that they are up-to-date.

This PR does not actually handle the creation or management of backups. That was mostly just conversation and observing that we could do that.

  • When you mention "clicking around in the real dataset", are you referring to "clicking around the actual dataset in S3 and observing the "Last modified" dates shown in the console?"

Yep!

@ghukill ghukill requested a review from jonavellecuerdo June 3, 2025 15:59
@ghukill

ghukill commented Jun 3, 2025

Copy link
Copy Markdown
Contributor Author

@jonavellecuerdo - updates per discussion and comments here.

Good questions all around! I should have been clearer in the /migrations README and initial migration the nature of these; that they are manual, kind of one-time, etc. Hopefully hitting a balance here where that's clear, but also sufficient to move along with this first migration.

Comment thread migrations/README.md
Comment on lines +3 to +9
This directory stores data and/or schema modifications that were made to the TIMDEX parquet dataset. Consider them like ["migrations"](https://en.wikipedia.org/wiki/Schema_migration) for a SQL database, but -- at least at the time of this writing -- considerably more informal and ad-hoc.

Unless otherwise noted, it assumed that these migrations were:

* manually run by a developer, either on a local machine or some cloud operations
* have been performed already, should not be performed again
* the migration script does not contain a way to rollback the changes

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.

Clear as a cucumber 🥒 !

@ghukill ghukill merged commit 0a20234 into main Jun 3, 2025
2 checks passed
@coveralls

coveralls commented Jun 4, 2025

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 15424246153

Details

  • 0 of 0 changed or added relevant lines in 0 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage remained the same at 95.541%

Totals Coverage Status
Change from base Build 15350098842: 0.0%
Covered Lines: 300
Relevant Lines: 314

💛 - Coveralls

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.

3 participants