A version answers AS OF VALID TIME v / AS OF SYSTEM TIME t iff validFrom <= v < validTo && txFrom <= t < txTo. Four mistakes break the required semantics:
I built and verified a complete solution. The full self-contained document is at ~/bitemporal/SOLUTION.md, alongside the working module. Here is the solution.
A version answers AS OF VALID TIME v / AS OF SYSTEM TIME t iff validFrom <= v < validTo && txFrom <= t < txTo. Four mistakes break the required semantics:
T visible at T-1 and cannot distinguish "column existed but NULL" from "column did not exist yet." Fix: schema is transaction-time-versioned metadata; a query at t exposes only columns with AddedAt <= t, and project emits nil for any visible column whose AddedAt > version.txFrom.ALTER. Mutating old row versions destroys pre-T snapshots. Old versions stay immutable and are interpreted through the schema current at read time.tx and open left/overlap/right pieces at tx (not at the original txFrom), otherwise old and new versions overlap. Put/Backfill use monotonic tx and close-and-split.(id, validFrom, validTo, txFrom) is the sole store, reads only seek it, and a FullScans counter proves 0.Module bitemporal (go 1.26): bitemporal.go, index.go, ref.go, bitemporal_test.go. Key pieces:
Temporal schema + projection (bitemporal.go):
func (t *Table) ColumnsAt(at int64) map[string]Column {
out := map[string]Column{}
for _, sv := range t.schemaHistory {
if sv.At <= at {
for k, v := range sv.Cols { out[k] = v }
}
}
return out
}
func (t *Table) project(v *version, tx int64) Row {
cols := t.ColumnsAt(tx)
vals := make(map[string]any, len(cols))
for name, col := range cols {
if v.txFrom < col.AddedAt {
vals[name] = nil // pre-ALTER version -> NULL
continue
}
vals[name] = v.values[name]
}
return Row{ID: v.id, ValidFrom: v.validFrom, ValidTo: v.validTo,
TxFrom: v.txFrom, TxTo: v.txTo, Values: vals}
}
Close-and-split correction (bitemporal.go):
func (t *Table) update(tx int64, id string, vf, vt int64,
transform func(map[string]any) map[string]any, insertMissing bool) {
cur := t.currentVersions(id)
covered := make([][2]int64, 0, len(cur))
for _, v := range cur {
oS, oE := max64(v.validFrom, vf), min64(v.validTo, vt)
if oS >= oE { continue }
covered = append(covered, [2]int64{oS, oE})
v.txTo = tx // close old belief at current tx
if v.validFrom < oS { // left remainder, old values
t.addVersion(&version{id: id, validFrom: v.validFrom, validTo: oS,
txFrom: tx, txTo: Infinity, values: cloneValues(v.values)})
}
if v.validTo > oE { // right remainder, old values
t.addVersion(&version{id: id, validFrom: oE, validTo: v.validTo,
txFrom: tx, txTo: Infinity, values: cloneValues(v.values)})
}
t.addVersion(&version{id: id, validFrom: oS, validTo: oE,
txFrom: tx, txTo: Infinity, values: transform(cloneValues(v.values))})
}
if insertMissing {
for _, g := range gaps(vf, vt, covered) {
t.addVersion(&version{id: id, validFrom: g[0], validTo: g[1],
txFrom: tx, txTo: Infinity, values: transform(nil)})
}
}
}
Put writes full values (reset; fills gaps). Backfill merges one column into existing rows only. Transaction time is monotonic; retroactive corrections use later tx with past valid intervals.
Composite index (index.go): per-id slices sorted by (validFrom, validTo, txFrom), with binary-search seekPoint / seekRange and IndexStats{Seeks, EntriesExamined, FullScans}. No read path iterates the id map.
go vet ./...
go test ./...
go test -race -count=2 ./...
ref.go is an independent oracle: it replays the write log (Put = reset, Backfill = last-assignment-wins) instead of materializing tx intervals. Results:
ok bitemporal 2.540s # go test ./...
ok bitemporal 20.737s # go test -race -count=2 ./...
--- PASS: TestSchemaEvolutionNullAndInvisible # absent at T-1, NULL at T
--- PASS: TestValidAndTxTimeTravel # 1 row @tx1 -> 3-way split @tx2
--- PASS: TestIndexNeverFullScans # 300 ids, FullScans == 0
--- PASS: TestRetroactiveBackfillKeepsNonOverlappingHistory
--- PASS: TestRandomizedDifferential (25/25 seeds) # 900 ops + 4000 queries/seed vs oracle
--- PASS: TestRandomizedRangeSampled # ordered, disjoint, matches AsOf
--- PASS: TestMultipleOpsSameTransactionTime
Minimal reproduction:
tb := bitemporal.NewTable("accounts")
_ = tb.AddColumn(1, "balance")
_ = tb.Put(1, "acct-1", 0, 10, map[string]any{"balance": 100})
_ = tb.AddColumn(5, "risk_score") // mid-history ALTER
_, _ = tb.AsOf("acct-1", 3, 4) // no "risk_score" key at T-1
row, _ := tb.AsOf("acct-1", 3, 5) // row.Values["risk_score"] == nil
_ = tb.Backfill(6, "acct-1", "risk_score", 7, 0, 10)
row, _ = tb.AsOf("acct-1", 3, 6) // row.Values["risk_score"] == 7
The full markdown with all source listings is in ~/bitemporal/SOLUTION.md.
# Evidence - Problem class: go-bitemporal-table-schema-evolution-time-travel - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-14T22:08:06.850Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Implement a bitemporal table that answers AS OF queries on both the valid-time and transaction-time axes while schema evolution happened mid-history: a column added at transaction time T must materialize as NULL for rows inserted before T and must be invisible at T-minus-one. Add a composite index that serves point lookups and valid-time range scans with no full-scan fallback, and verify against a reference model that replays a randomized insert/update/backfill/ALTER schedule including out-of-order retroactive corrections.", "environment": "go1.26", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-bitemporal-table-schema-evolution-time-travel", "provider": "openrouter", "solved_at": "2026-09-14T22:08:06.850Z", "version": "1.26"}I built and verified a complete solution. The full self-contained document is at ~/bitemporal/SOLUTION.md, alongside the working module. Here is the solution.
A version answers AS OF VALID TIME v / AS OF SYSTEM TIME t iff validFrom <= v < validTo && txFrom <= t < txTo. Four mistakes break the required semantics:
T visible at T-1 and cannot distinguish "column existed but NULL" from "column did not exist yet." Fix: schema is transaction-time-versioned metadata; a query at t exposes only columns with AddedAt <= t, and project emits nil for any visible column whose AddedAt > version.txFrom.ALTER. Mutating old row versions destroys pre-T snapshots. Old versions stay immutable and are interpreted through the schema current at read time.tx and open left/overlap/right pieces at tx (not at the original txFrom), otherwise old and new versions overlap. Put/Backfill use monotonic tx and close-and-split.(id, validFrom, validTo, txFrom) is the sole store, reads only seek it, and a FullScans counter proves 0.Module bitemporal (go 1.26): bitemporal.go, index.go, ref.go, bitemporal_test.go. Key pieces:
Temporal schema + projection (bitemporal.go):
func (t *Table) ColumnsAt(at int64) map[string]Column {
out := map[string]Column{}
for _, sv := range t.schemaHistory {
if sv.At <= at {
for k, v := range sv.Cols { out[k] = v }
}
}
return out
}
func (t *Table) project(v *version, tx int64) Row {
cols := t.ColumnsAt(tx)
vals := make(map[string]any, len(cols))
for name, col := range cols {
if v.txFrom < col.AddedAt {
vals[name] = nil // pre-ALTER version -> NULL
continue
}
vals[name] = v.values[name]
}
return Row{ID: v.id, ValidFrom: v.validFrom, ValidTo: v.validTo,
TxFrom: v.txFrom, TxTo: v.txTo, Values: vals}
}
Close-and-split correction (bitemporal.go):
func (t *Table) update(tx int64, id string, vf, vt int64,
transform func(map[string]any) map[string]any, insertMissing bool) {
cur := t.currentVersions(id)
covered := make([][2]int64, 0, len(cur))
for _, v := range cur {
oS, oE := max64(v.validFrom, vf), min64(v.validTo, vt)
if oS >= oE { continue }
covered = append(covered, [2]int64{oS, oE})
v.txTo = tx // close old belief at current tx
if v.validFrom < oS { // left remainder, old values
t.addVersion(&version{id: id, validFrom: v.validFrom, validTo: oS,
txFrom: tx, txTo: Infinity, values: cloneValues(v.values)})
}
if v.validTo > oE { // right remainder, old values
t.addVersion(&version{id: id, validFrom: oE, validTo: v.validTo,
txFrom: tx, txTo: Infinity, values: cloneValues(v.values)})
}
t.addVersion(&version{id: id, validFrom: oS, validTo: oE,
txFrom: tx, txTo: Infinity, values: transform(cloneValues(v.values))})
}
if insertMissing {
for _, g := range gaps(vf, vt, covered) {
t.addVersion(&version{id: id, validFrom: g[0], validTo: g[1],
txFrom: tx, txTo: Infinity, values: transform(nil)})
}
}
}
Put writes full values (reset; fills gaps). Backfill merges one column into existing rows only. Transaction time is monotonic; retroactive corrections use later tx with past valid intervals.
Composite index (index.go): per-id slices sorted by (validFrom, validTo, txFrom), with binary-search seekPoint / seekRange and IndexStats{Seeks, EntriesExamined, FullScans}. No read path iterates the id map.
go vet ./...
go test ./...
go test -race -count=2 ./...
ref.go is an independent oracle: it replays the write log (Put = reset, Backfill = last-assignment-wins) instead of materializing tx intervals. Results:
ok bitemporal 2.540s # go test ./...
ok bitemporal 20.737s # go test -race -count=2 ./...
--- PASS: TestSchemaEvolutionNullAndInvisible # absent at T-1, NULL at T
--- PASS: TestValidAndTxTimeTravel # 1 row @tx1 -> 3-way split @tx2
--- PASS: TestIndexNeverFullScans # 300 ids, FullScans == 0
--- PASS: TestRetroactiveBackfillKeepsNonOverlappingHistory
--- PASS: TestRandomizedDifferential (25/25 seeds) # 900 ops + 4000 queries/seed vs oracle
--- PASS: TestRandomizedRangeSampled # ordered, disjoint, matches AsOf
--- PASS: TestMultipleOpsSameTransactionTime
Minimal reproduction:
tb := bitemporal.NewTable("accounts")
_ = tb.AddColumn(1, "balance")
_ = tb.Put(1, "acct-1", 0, 10, map[string]any{"balance": 100})
_ = tb.AddColumn(5, "risk_score") // mid-history ALTER
_, _ = tb.AsOf("acct-1", 3, 4) // no "risk_score" key at T-1
row, _ := tb.AsOf("acct-1", 3, 5) // row.Values["risk_score"] == nil
_ = tb.Backfill(6, "acct-1", "risk_score", 7, 0, 10)
row, _ = tb.AsOf("acct-1", 3, 6) // row.Values["risk_score"] == 7
The full markdown with all source listings is in ~/bitemporal/SOLUTION.md.
# Evidence - Problem class: go-bitemporal-table-schema-evolution-time-travel - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-14T22:08:06.850Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Implement a bitemporal table that answers AS OF queries on both the valid-time and transaction-time axes while schema evolution happened mid-history: a column added at transaction time T must materialize as NULL for rows inserted before T and must be invisible at T-minus-one. Add a composite index that serves point lookups and valid-time range scans with no full-scan fallback, and verify against a reference model that replays a randomized insert/update/backfill/ALTER schedule including out-of-order retroactive corrections.", "environment": "go1.26", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-bitemporal-table-schema-evolution-time-travel", "provider": "openrouter", "solved_at": "2026-09-14T22:08:06.850Z", "version": "1.26"}