Skip to content

Commit 1027349

Browse files
committed
fix(tables): stop defaultMetadata overwriting caller-supplied column metadata
The previous commit's fix was defeated by its own last line. `defaultMetadata` is spread AFTER the caller's metadata, and it was handed a bare `{ name, type }` — so it re-answered from nothing and overwrote the values it was meant to fall back to. An imported date column's `includeTime: true` became `false`: the rows were coerced WITH their times while the column was saved claiming to be date-only. It now sees what the caller supplied, so `column.includeTime ?? false` falls back only when the key is genuinely absent — the same shape `addTableColumn` already had. The builder is extracted as `buildAddedColumn` and exported, for the reason this slipped through: the earlier test asserted what INFERENCE returns, not what gets persisted, and the overwrite happens between the two. Testing the persisted shape needed a transaction otherwise. Mirrors `buildConvertedColumn`.
1 parent 99a0132 commit 1027349

2 files changed

Lines changed: 62 additions & 16 deletions

File tree

apps/sim/lib/table/__tests__/column-types-contact.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { metadataMigrationFor } from '@/lib/table/column-types/registry.server'
2323
import { buildConvertedColumn } from '@/lib/table/columns/service'
2424
import { coerceValue, inferSchemaFromCsv } from '@/lib/table/import'
2525
import { filterRulesToFilter, prunePredicateForColumns } from '@/lib/table/query-builder/converters'
26+
import { buildAddedColumn } from '@/lib/table/service'
2627
import { buildFilterClause } from '@/lib/table/sql'
2728
import type { ColumnDefinition } from '@/lib/table/types'
2829

@@ -604,6 +605,25 @@ describe('imported column metadata', () => {
604605
expect(String(coerced)).toContain('14:30')
605606
})
606607

608+
it('PERSISTS the inferred includeTime instead of re-defaulting over it', () => {
609+
// The gap that let a bug through: the test above only checked what
610+
// inference RETURNS. `defaultMetadata` runs last when the column is built
611+
// for storage, so handed a bare `{ name, type }` it re-answered from
612+
// nothing and overwrote the inferred `true` with `false` — rows coerced
613+
// with times, column saved as date-only.
614+
const { columns } = inferSchemaFromCsv(['when'], [{ when: '2024-01-15T14:30' }])
615+
const stored = buildAddedColumn(columns[0], 'col_1')
616+
expect(stored.type).toBe('date')
617+
expect(stored.includeTime).toBe(true)
618+
})
619+
620+
it('still applies a type default the caller did not supply', () => {
621+
const stored = buildAddedColumn({ name: 'd', type: 'date' }, 'col_2')
622+
expect(stored.includeTime).toBe(false)
623+
const money = buildAddedColumn({ name: 'm', type: 'currency' }, 'col_3')
624+
expect(money.currencyCode).toBeDefined()
625+
})
626+
607627
it('leaves a non-date inferred column without date metadata', () => {
608628
const { columns } = inferSchemaFromCsv(['n'], [{ n: '1' }, { n: '2' }])
609629
expect(columns[0].type).toBe('number')

apps/sim/lib/table/service.ts

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,47 @@ export async function createTable(
469469
* Use this when composing a column addition with other writes (e.g., row
470470
* inserts) that must succeed or roll back together.
471471
*/
472+
/**
473+
* The column definition a bulk add persists.
474+
*
475+
* Extracted and exported so the metadata carry-through is testable without a
476+
* transaction — the same reason `buildConvertedColumn` is exported from the
477+
* column service.
478+
*/
479+
export function buildAddedColumn(
480+
column: ColumnTypeMetadata & {
481+
id?: string
482+
name: string
483+
type: string
484+
required?: boolean
485+
unique?: boolean
486+
},
487+
id: string
488+
): TableSchema['columns'][number] {
489+
const type = column.type as TableSchema['columns'][number]['type']
490+
// A create has nothing to clear, so resolve any nulls away first.
491+
const supplied = metadataWithoutClears(pickMetadata(column, ownedKeysOf(type)))
492+
return {
493+
id,
494+
name: column.name,
495+
type,
496+
required: column.required ?? false,
497+
unique: column.unique ?? false,
498+
// Carry the type's own metadata, mirroring `addTableColumn`. Building the
499+
// column from the five fields above alone dropped everything else the
500+
// caller supplied — an imported date column lost its `includeTime` and
501+
// landed in the "predates the key" state, where the grid and coercion treat
502+
// a column created moments ago as a legacy instant column.
503+
...supplied,
504+
// `defaultMetadata` must SEE what the caller supplied. Handed a bare
505+
// `{ name, type }` it re-answers from nothing and, spread last, overwrites
506+
// the very values above — an imported date column's `includeTime: true`
507+
// became `false`, so its rows were coerced WITH times while the column
508+
// claimed to be date-only.
509+
...columnTypeById(type).defaultMetadata?.({ ...supplied, name: column.name, type }),
510+
}
511+
}
512+
472513
export async function addTableColumnsWithTx(
473514
trx: DbTransaction,
474515
table: TableDefinition,
@@ -514,22 +555,7 @@ export async function addTableColumnsWithTx(
514555
// Honor a caller-assigned id (the CSV append path pre-assigns so coercion
515556
// and persistence agree); otherwise mint one.
516557
const id = column.id ?? generateColumnId()
517-
const type = column.type as TableSchema['columns'][number]['type']
518-
additions.push({
519-
id,
520-
name: column.name,
521-
type,
522-
required: column.required ?? false,
523-
unique: column.unique ?? false,
524-
// Carry the type's own metadata, mirroring `addTableColumn`. Building the
525-
// column from these five fields alone dropped everything else the caller
526-
// supplied — an imported date column lost its `includeTime` and landed in
527-
// the "predates the key" state, where the grid and coercion treat it as a
528-
// legacy instant column rather than as what the import decided.
529-
// A create has nothing to clear, so resolve any nulls away.
530-
...metadataWithoutClears(pickMetadata(column, ownedKeysOf(type))),
531-
...columnTypeById(type).defaultMetadata?.({ name: column.name, type }),
532-
})
558+
additions.push(buildAddedColumn(column, id))
533559
}
534560

535561
if (table.schema.columns.length + additions.length > TABLE_LIMITS.MAX_COLUMNS_PER_TABLE) {

0 commit comments

Comments
 (0)