Skip to content

Commit 7fe7141

Browse files
authored
Merge pull request #2401 from teableio/sync/ee-20260107-104958
[sync] feat: add e2e tests for conditional lookup number type and field conversion from CreatedTime to Date
2 parents 41b2ee1 + 262e6db commit 7fe7141

File tree

4 files changed

+63
-2
lines changed

4 files changed

+63
-2
lines changed

apps/nestjs-backend/src/features/field/field-calculate/field-converting.service.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1344,6 +1344,10 @@ export class FieldConvertingService {
13441344
return false;
13451345
}
13461346

1347+
if (newField.hasError !== oldField.hasError) {
1348+
return true;
1349+
}
1350+
13471351
if (majorFieldKeysChanged(oldField, newField)) {
13481352
return true;
13491353
}
@@ -1384,11 +1388,12 @@ export class FieldConvertingService {
13841388
return;
13851389
}
13861390

1391+
const errorStateChanged = newField.hasError !== oldField.hasError;
13871392
const hasMajorChange = majorFieldKeysChanged(oldField, newField);
13881393
const conditionalLookupDiff = this.hasConditionalLookupDiff(newField, oldField);
13891394
const conditionalRollupDiff = this.hasConditionalRollupDiff(newField, oldField);
13901395

1391-
if (!hasMajorChange && !conditionalLookupDiff && !conditionalRollupDiff) {
1396+
if (!errorStateChanged && !hasMajorChange && !conditionalLookupDiff && !conditionalRollupDiff) {
13921397
return;
13931398
}
13941399

apps/nestjs-backend/src/features/field/field.service.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1447,6 +1447,33 @@ export class FieldService implements IReadonlyAdapterService {
14471447
if (oldField.type === newField.type) {
14481448
return;
14491449
}
1450+
1451+
const usesPersistedGeneratedColumn = (field: IFieldInstance) => {
1452+
if (field.isLookup) {
1453+
return false;
1454+
}
1455+
1456+
const persistedAsGeneratedColumn = (
1457+
field.meta as { persistedAsGeneratedColumn?: boolean } | undefined
1458+
)?.persistedAsGeneratedColumn;
1459+
1460+
if (persistedAsGeneratedColumn !== undefined) {
1461+
return persistedAsGeneratedColumn === true;
1462+
}
1463+
1464+
if (field.type === FieldType.CreatedTime) {
1465+
return true;
1466+
}
1467+
1468+
if (field.type === FieldType.LastModifiedTime) {
1469+
const maybeLastModified = field as unknown as { isTrackAll?: () => boolean };
1470+
if (typeof maybeLastModified.isTrackAll === 'function') {
1471+
return maybeLastModified.isTrackAll();
1472+
}
1473+
}
1474+
1475+
return false;
1476+
};
14501477
// If either side is Formula, we must reconcile the physical schema using modifyColumnSchema.
14511478
// This ensures that converting to Formula creates generated columns (or proper projection),
14521479
// and converting back from Formula recreates the original physical column.
@@ -1464,6 +1491,28 @@ export class FieldService implements IReadonlyAdapterService {
14641491
return;
14651492
}
14661493

1494+
// Some field types (e.g., CreatedTime / LastModifiedTime(track all)) are persisted as generated columns
1495+
// without a dbFieldType change. Converting them to a regular field type (e.g., Date) must recreate the
1496+
// physical column, otherwise UPDATEs will hit "cannot update a generated column".
1497+
if (oldField.dbFieldType === newField.dbFieldType) {
1498+
const oldGenerated = usesPersistedGeneratedColumn(oldField);
1499+
const newGenerated = usesPersistedGeneratedColumn(newField);
1500+
1501+
if (oldGenerated || newGenerated) {
1502+
const tableDomain = await this.tableDomainQueryService.getTableDomainById(tableId);
1503+
const modifyColumnSql = this.dbProvider.modifyColumnSchema(
1504+
dbTableName,
1505+
oldField,
1506+
newField,
1507+
tableDomain
1508+
);
1509+
for (const sql of modifyColumnSql) {
1510+
await this.prismaService.txClient().$executeRawUnsafe(sql);
1511+
}
1512+
return;
1513+
}
1514+
}
1515+
14671516
await this.handleFormulaUpdate(tableId, dbTableName, oldField, newField);
14681517
}
14691518

packages/core/src/models/field/derivate/date.field.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,20 +160,24 @@ describe('DateFieldCore', () => {
160160
it('should valid cellValue', () => {
161161
const date = new Date();
162162
const cellValue = date.toISOString();
163+
const cellValueWithoutMs = '2026-01-06T00:00:00+00:00';
163164
const lookupFieldOne = plainToInstance(DateFieldCore, {
164165
...json,
165166
lookupJson,
166167
isMultipleCellValue: false,
167168
});
168169
expect(field.validateCellValue(cellValue).success).toBe(true);
170+
expect(field.validateCellValue(cellValueWithoutMs).success).toBe(true);
169171
expect(field.validateCellValue(date.getTime()).success).toBe(false);
170172
expect(field.validateCellValue('xxx').success).toBe(false);
171173

172174
expect(lookupField.validateCellValue([cellValue]).success).toBe(true);
175+
expect(lookupField.validateCellValue([cellValueWithoutMs]).success).toBe(true);
173176
expect(lookupField.validateCellValue(cellValue).success).toBe(false);
174177
expect(lookupField.validateCellValue([{ id: 'actxxx' }]).success).toBe(false);
175178

176179
expect(lookupFieldOne.validateCellValue(cellValue).success).toBe(true);
180+
expect(lookupFieldOne.validateCellValue(cellValueWithoutMs).success).toBe(true);
177181
});
178182

179183
describe('validateOptions', () => {

packages/core/src/models/field/derivate/date.field.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ extend(timezone);
1414
extend(customParseFormat);
1515
extend(utc);
1616

17-
export const dataFieldCellValueSchema = z.string().datetime({ precision: 3, offset: true });
17+
// Stored date/time values can come from client-side `Date.toISOString()` (millisecond precision)
18+
// or from database JSON aggregation (which may omit fractional seconds when zero).
19+
// Accept both, while still requiring an explicit timezone offset (e.g. `Z`, `+00:00`).
20+
export const dataFieldCellValueSchema = z.string().datetime({ offset: true });
1821

1922
export type IDateCellValue = z.infer<typeof dataFieldCellValueSchema>;
2023

0 commit comments

Comments
 (0)