Medical-Event-Data-Standard / Medical-Event-Data-Standard/flexible_schema

align() silently reinterprets int64 columns as epoch timestamps (int64 → timestamp[us]) instead of raising

Open Beginner friendly
#25 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
1
Forks
0
PR merge metrics
No merged PRs in 30d

Description

Schema.align() promises "safe, no-data-change operations" (PyArrowSchema docstring: "casting, when it can be done safely"), but when a column arrives as int64 and the schema wants timestamp[us], align silently reinterprets the integers as microseconds-since-epoch. Every value lands near 1970-01-01 with zero warnings — silent data corruption from a schema-alignment API.

Repro (flexible_schema 0.1.1, pyarrow 23.0.1, Python 3.12)

import pyarrow as pa
from flexible_schema import PyArrowSchema

class Data(PyArrowSchema):
    subject_id: pa.int64()
    time: pa.timestamp("us")
    code: pa.string()

tbl = pa.Table.from_pydict({
    "subject_id": [1, 1, 2],
    "time": [0, 10, 120],   # intended as minutes-from-admission, NOT epoch microseconds
    "code": ["A", "B", "C"],
})
out = Data.align(tbl)
print(out.column("time").to_pylist())
# [datetime(1970, 1, 1, 0, 0), datetime(1970, 1, 1, 0, 0, 0, 10), datetime(1970, 1, 1, 0, 0, 0, 120)]

No error, no warning. The same behavior is inherited by meds.DataSchema.align (meds 0.4.1), where it corrupts the mandatory time column of MEDS datasets.

Where the cast happens

src/flexible_schema/pyarrow.py:327-328 (v0.1.1; unchanged at current main):

@classmethod
def _cast_raw_table_column(cls, table: pa.Table, col: str, want_type: pa.DataType) -> pa.Table:
    return table.set_column(table.schema.get_field_index(col), col, table.column(col).cast(want_type))

reached from Schema.align_cast_raw_table (src/flexible_schema/base.py:397:351-352), which casts every mistyped column indiscriminately. ChunkedArray.cast defaults to safe=True, but pyarrow's "safe" mode does not help here: int64 → timestamp is defined in Arrow as an epoch-unit reinterpretation and is permitted even with safe=True (verified with both ChunkedArray.cast(..., safe=True) and pc.cast(..., safe=True)). So the permissive decision is effectively flexible_schema's — it needs its own cast policy rather than pyarrow's.

Current contract (empirically, for a timestamp[us] target)

source behavior
int64 silent epoch reinterpretation ← the bug
int32 raises SchemaValidationError (no pyarrow kernel)
string, ISO-parseable parses (reasonable)
string, non-date raises SchemaValidationError (from ArrowInvalid)
float64 raises SchemaValidationError (from ArrowNotImplementedError)

int64→timestamp is the lone pair where align succeeds while changing the meaning of the data. A schema aligner should restrict itself to semantic-preserving casts (widening within a kind, string parsing that can fail loudly) or raise; reinterpreting an integer's unit/epoch is a transformation, not an alignment.

Suggested fix

In PyArrowSchema._cast_raw_table_column (or a shared policy hook in base.py), reject cross-kind reinterpretation casts before delegating to pyarrow — minimally:

if pa.types.is_integer(current_type) and (pa.types.is_timestamp(want_type)
        or pa.types.is_date(want_type) or pa.types.is_time(want_type)):
    raise SchemaValidationError(...)  # report as a mistyped column, like the float64 case

or, more robustly, an explicit allowlist of permitted cast pairs (same-kind widening, string→parsed types, int→float) with everything else surfacing as the existing SchemaValidationError: Columns with incorrect types: .... If someone genuinely has epoch integers, an opt-in escape hatch (e.g. a per-column or per-call allow_reinterpret_casts=True) keeps that workflow available without making it the silent default.

This would make int64 behave like the already-correct float64/int32 cases: a loud SchemaValidationError naming the column and both types.

Context

Found via mmcdermott/MEDS_extract#207: a MEDS extraction config that binds time: to an integer offset column (e.g. eICU's observationoffset, forgetting the pseudotime derivation) sails through the whole pipeline and DataSchema.align at the finalize stage silently lands every event at 1970-01-01.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with src/flexible_schema/pyarrow.py:_cast_raw_table_column and trace its callers through src/flexible_schema/base.py:_cast_raw_table and Schema.align. Use the provided int64-to-timestamp reproduction to verify that alignment raises SchemaValidationError naming the column and types instead of silently producing epoch timestamps.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
data
Issue type
Bug
Difficulty
2/5
Estimated time
Half a day
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.