Skip to content

Commit 504cea8

Browse files
committed
fix(tables): carry type metadata through the import column-create path
`addTableColumnsWithTx` built each column from five fields and dropped everything else the caller supplied, so an imported date column landed with `includeTime` absent — the "predates the key" state — and coercion and the grid then treated a column created moments ago as a legacy instant column. It now carries the type's owned keys and defaults, the same way `addTableColumn` does. An inferred date column is stamped `includeTime: true` rather than taking the sidebar's date-only default. The pattern that infers `date` accepts `2024-01-15T14:30`, so the file may genuinely contain times, and defaulting to date-only would truncate every one of them on write. Creating a Date column by hand still defaults to date-only: there the user sees the toggle and there is no data to lose yet.
1 parent 96f5a12 commit 504cea8

3 files changed

Lines changed: 61 additions & 4 deletions

File tree

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
} from '@/lib/table/column-types'
2222
import { metadataMigrationFor } from '@/lib/table/column-types/registry.server'
2323
import { buildConvertedColumn } from '@/lib/table/columns/service'
24-
import { coerceValue } from '@/lib/table/import'
24+
import { coerceValue, inferSchemaFromCsv } from '@/lib/table/import'
2525
import { filterRulesToFilter, prunePredicateForColumns } from '@/lib/table/query-builder/converters'
2626
import { buildFilterClause } from '@/lib/table/sql'
2727
import type { ColumnDefinition } from '@/lib/table/types'
@@ -586,6 +586,31 @@ describe('server-side operand canonicalization', () => {
586586
})
587587
})
588588

589+
describe('imported column metadata', () => {
590+
it('gives an inferred date column an explicit includeTime that keeps its times', () => {
591+
// The pattern that infers `date` accepts `2024-01-15T14:30`, so the file
592+
// may genuinely contain times. Stamping the sidebar's date-only default
593+
// would truncate every one of them; leaving it ABSENT would put a freshly
594+
// created column into the "predates the key" state.
595+
const { columns } = inferSchemaFromCsv(
596+
['when'],
597+
[{ when: '2024-01-15T14:30' }, { when: '2024-02-20T09:00' }]
598+
)
599+
expect(columns[0].type).toBe('date')
600+
expect(columns[0].includeTime).toBe(true)
601+
602+
// And the value survives the write path unchanged.
603+
const coerced = coerceValue('2024-01-15T14:30', 'date', { column: columns[0] })
604+
expect(String(coerced)).toContain('14:30')
605+
})
606+
607+
it('leaves a non-date inferred column without date metadata', () => {
608+
const { columns } = inferSchemaFromCsv(['n'], [{ n: '1' }, { n: '2' }])
609+
expect(columns[0].type).toBe('number')
610+
expect(columns[0].includeTime).toBeUndefined()
611+
})
612+
})
613+
589614
describe('date includeTime', () => {
590615
it('truncates to a calendar day only when includeTime is explicitly false', () => {
591616
const dateOnly = column('date', { includeTime: false })

apps/sim/lib/table/import.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,9 +372,19 @@ export function inferSchemaFromCsv(
372372
seen.add(colName.toLowerCase())
373373
headerToColumn.set(header, colName)
374374

375+
const type = inferColumnType(sample.map((r) => r[header]))
375376
return {
376377
name: colName,
377-
type: inferColumnType(sample.map((r) => r[header])),
378+
type,
379+
// An inferred date column keeps its times. The pattern that infers `date`
380+
// accepts `2024-01-15T14:30`, so the file may genuinely contain them —
381+
// and stamping the sidebar's date-only default here would truncate every
382+
// one of them on write. Set explicitly rather than left absent so a fresh
383+
// column is never in the "predates the key" state.
384+
//
385+
// Deliberately different from creating a Date column by hand: there the
386+
// user sees the toggle and there is no data to lose yet.
387+
...(type === 'date' ? { includeTime: true } : {}),
378388
} satisfies ColumnDefinition
379389
})
380390

apps/sim/lib/table/service.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ import { resolveRestoredFolderId } from '@/lib/folders/queries'
2020
import { notifyWorkspaceTablesChanged } from '@/lib/realtime/notify'
2121
import { assertRowCapacity, notifyTableRowUsage } from '@/lib/table/billing'
2222
import { generateColumnId, getColumnId, withGeneratedColumnIds } from '@/lib/table/column-keys'
23+
import {
24+
columnTypeById,
25+
metadataWithoutClears,
26+
ownedKeysOf,
27+
pickMetadata,
28+
} from '@/lib/table/column-types'
2329
import { COLUMN_TYPES, NAME_PATTERN, TABLE_LIMITS } from '@/lib/table/constants'
2430
import { tableNotFound } from '@/lib/table/errors'
2531
import { appendTableEvent } from '@/lib/table/events'
@@ -29,6 +35,7 @@ import { nKeysBetween } from '@/lib/table/order-key'
2935
import type { DbTransaction } from '@/lib/table/planner'
3036
import { setTableTxTimeouts } from '@/lib/table/tx'
3137
import {
38+
type ColumnTypeMetadata,
3239
type CreateTableData,
3340
TABLE_LOCK_FLAGS,
3441
TABLE_LOCK_KINDS,
@@ -465,7 +472,13 @@ export async function createTable(
465472
export async function addTableColumnsWithTx(
466473
trx: DbTransaction,
467474
table: TableDefinition,
468-
columns: { id?: string; name: string; type: string; required?: boolean; unique?: boolean }[],
475+
columns: (ColumnTypeMetadata & {
476+
id?: string
477+
name: string
478+
type: string
479+
required?: boolean
480+
unique?: boolean
481+
})[],
469482
requestId: string
470483
): Promise<TableDefinition> {
471484
if (columns.length === 0) return table
@@ -501,12 +514,21 @@ export async function addTableColumnsWithTx(
501514
// Honor a caller-assigned id (the CSV append path pre-assigns so coercion
502515
// and persistence agree); otherwise mint one.
503516
const id = column.id ?? generateColumnId()
517+
const type = column.type as TableSchema['columns'][number]['type']
504518
additions.push({
505519
id,
506520
name: column.name,
507-
type: column.type as TableSchema['columns'][number]['type'],
521+
type,
508522
required: column.required ?? false,
509523
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 }),
510532
})
511533
}
512534

0 commit comments

Comments
 (0)