Skip to content

chore(datalake): catalog namespacing redesign + storage-format decision (post-demo migration) #373

Description

@thecaffiend

Background

The bactopia-single-sample-analysis report broke because it queried the Glue table input_ccd_dlh_t_seqauto_input_clean_vbkt_s3_b1f75c7, which Glue's default crawler grouping froze and orphaned once a second sibling prefix (manifest/) appeared under the crawled bucket root. New meta partitions now land only in input_meta, so the metadata query returned zero rows.

This exposed a deeper, known-in-advance issue: everything added to the catalog so far was built haphazardly to demonstrate capability, and there is no deliberate design for how catalog objects should be named, grouped, and schematized across tributaries. This issue captures that design and the migration to it.

Phase A stopgap (already shipped): #371 / PR #372 repoints the report to input_meta and guards the empty-result case. It is intentionally format-agnostic and unblocks the demo. This issue is Phase B: the durable redesign, to be done after the demo.

All current catalog data is disposable test data. The migration can tear down and rebuild the catalog rather than doing an in-place, dual-table transition.

Namespacing model (verified, and the basis for the locked decisions)

Catalog objects are namespaced on three levels (verified in capeinfra/datalake/datalake.py and capeinfra/pipeline/data.py):

  • Tributary -> one Glue catalog database ({trib}-catalog, e.g. ccd-dlh-t-seqauto-catalog_*). Tributary identity lives in the database, so cross-tributary differentiation is by database, not table name.
  • Bucket role -> table-name prefix (today the free-form crawler prefix: input for input-clean, result for result-clean). All buckets in a tributary share the one database; the prefix disambiguates them.
  • Folder within a bucket -> table name (meta/ -> input_meta). Full identity: {trib-db}.{prefix}_{folder}.

Consumers resolve a dataset by database (tributary) then {prefix}_{folder} table. The report selects the tributary DB via a substring match against athena.list_databases.

Locked decisions (durable regardless of storage format)

These hold whether we stay on CSV+crawler or move to Iceberg, so they are safe to implement now:

  • Keep one Glue catalog database per tributary. Already true.
  • Derive the table prefix from the bucket key with - swapped to _ (input-clean -> input_clean_), and drop the standalone free-form prefix: config knob so a prefix can never drift from the bucket it describes. Validate the derived name is a legal Glue identifier ([a-z0-9_], lowercase) and fail the build loudly otherwise. Apply uniformly across all tributaries (seqauto, genomics, hai), not just seqauto.
  • Reserve the prefixes input-raw / input-clean / result-raw / result-clean (and their _ forms) platform-wide; document them. Since tables today originate only from crawlers bound to a bucket, deriving prefix from bucket key inherently prevents cross-bucket prefix squatting. Forward-looking guard: if a user-facing "add an arbitrary table to a tributary" path is ever added, it must validate the requested name against the reserved-prefix set.
  • Do not crawl raw buckets for anything we have now (matches the existing convention), but keep the option open for a future pipeline that needs to track raw. Reserving input_raw/result_raw means an ad hoc raw table would land at a predictable, non-colliding name if ever needed.
  • Document the one-crawler-per-bucket limitation explicitly. configure_bucket creates exactly one aws.glue.Crawler per bucket, and the crawler-attrs DDB maps bucket_name -> crawler_name 1:1. Deriving a single prefix from a bucket key assumes this invariant; multiple crawlers per bucket would make it ambiguous.

Gated decision (TBD): storage format

The remaining design components ride on a storage-format decision that is upstream of them:

  • Option 1 - stay on CSV + crawler (near-term). Keep the crawler model. Then we must also add a Glue grouping Configuration knob to DataCrawler and force folder-level grouping so the root-table-vs-per-folder flip (the bug that started this) can never recur. Table identity becomes {trib-db}.{bucketkey_}_{folder}, deterministic.
  • Option 2 - move to Iceberg (long-standing on our radar; CSV was the easy first target). Iceberg makes much of the CSV machinery moot: no crawler grouping heuristic to flip (so the grouping Configuration fix is unnecessary), no CSV classifier, declared + evolvable schemas (kills the drift class we hit), and hidden partitioning (no partition projection / crawler partition discovery). The cost relocates to the write path and ops:
    • Every producing ETL currently builds a CSV buffer and calls etl_job.write_sink_file(...). Iceberg requires writing through an Iceberg-aware engine (Glue Spark with --datalake-formats iceberg, Athena INSERT/CTAS, or PyIceberg). These are already Glue jobs, so it is feasible in-model, but each ETL is a genuine rewrite.
    • The S3-object-landing notification model (fires on meta/, manifest/, etc.) must be rethought, since Iceberg manages its own data+metadata file layout under the table location.
    • New operational surface: snapshot expiration, compaction, orphan-file cleanup.
    • Current sample_id/year/month/... path partitioning (including the recent result-clean/input-clean partition alignment) must be re-expressed as Iceberg partition transforms.

Decision needed: is Iceberg near-term enough to adopt during this migration, or do we ship the CSV grouping stopgap now and defer Iceberg? Do not over-invest in the CSV-crawler determinism machinery if Iceberg is imminent - that work is throwaway once we switch.

Deferred design components (TBD, folded under the format decision)

These were discussed but intentionally not locked; resolve them at implementation time under whichever branch wins:

  • Schema pinning vs inference. The shared classifier cape-csv-standard-classifier (OpenCSVSerDe, contains_header=PRESENT, no explicit header list) pins header detection but still infers column names/set from each file's header, and types are all-string. These schemas are ETL-owned (code), so inference lets column changes silently reshape the catalog.
    • Under CSV+crawler: either declare the tables explicitly (fixed columns/types) or accept inference and document the ETL-owned column contract. Per-dataset CSV classifiers are a weak middle ground (two same-format CSVs disambiguate poorly, still all-string).
    • Under Iceberg: solved by declared, evolvable schemas; classifier is irrelevant.
  • Partition strategy. Partition projection vs declared tables vs crawler-discovered partitions (CSV), or Iceberg partition transforms. Subsumed by Iceberg if we go that way.
  • Table-name readability. Confirm the resulting names read well (input_clean_meta, input_clean_manifest, result_clean_software_versions).

Migration mechanics (Phase B)

Because current catalog data is disposable test data:

  1. capeinfra: implement the locked namespacing decisions (prefix-from-bucket-key, drop prefix: knob, validation) plus the format-gated crawler-grouping Configuration (only if staying on CSV).
  2. Rebuild the catalog clean: drop the tributary catalog databases/tables and let crawlers (or the Iceberg writers) recreate them with the new names, rather than leaving deprecated orphans.
  3. Consumer cutover for the result_* -> result_clean_* (and input_meta -> input_clean_meta) rename:
    • assets/report/bactopia-single-sample-analysis/data_function.py (input_meta, result_software_versions, result_sourmash_gtdb_rs207_k31, result_amrfinderplus, result_caerbannog_stoplight)
    • assets/etl/etl_bactopia_results.py, assets/etl/etl_caerbannog_results.py
    • any external saved Athena queries / dashboards
  4. Validate with pulumi preview --diff -s cape-cod-dev, reconcile every planned action, then deploy (user's step; agents never run pulumi up).
  5. Re-test the report end-to-end and confirm the new {prefix}_{folder} tables populate.

Out of scope

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0Highest PrioritydatalakeThings related to datalake health/org

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions