Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c471fdc
fix(formula): update LEN function to use coerced text comparable for …
nichenqin Nov 24, 2025
665304b
fix(formula): coerce non-text inputs to text for string functions in …
nichenqin Nov 24, 2025
9a7d21c
fix(formula): enhance validation for numeric functions with non-numer…
nichenqin Nov 24, 2025
e919e07
fix(formula): add numeric coercion for JSON and multi-value fields in…
nichenqin Nov 25, 2025
23f40bb
fix(formula): normalize empty-string comparisons and ensure BLANK() b…
nichenqin Nov 25, 2025
8e50551
fix(formula): refactor normalization logic for string comparisons and…
nichenqin Nov 25, 2025
8823eb3
fix(formula): normalize datetime format expressions in queries and tests
nichenqin Nov 25, 2025
9fd431d
fix(formula): enhance numeric in sum and average functions for better…
nichenqin Nov 25, 2025
3fb4477
fix(formula): update array functions to accept array inputs and enhan…
nichenqin Nov 25, 2025
4f39522
fix(formula): enhance numeric handling and add dbFieldName metadata f…
nichenqin Nov 26, 2025
2a0dab2
fix(formula): add date arithmetic support in generated column and sel…
nichenqin Nov 26, 2025
324e6a7
fix(formula): update dateAdd function to return false for immutability
nichenqin Nov 26, 2025
ad8e664
fix(formula): add support for datetime concatenation in generated col…
nichenqin Nov 26, 2025
5c70e13
fix(formula): enhance numeric handling for text inputs in generated f…
nichenqin Nov 26, 2025
e49367b
fix(formula): enhance timestamp sanitization to handle non-datetime t…
nichenqin Nov 26, 2025
6ed5dfb
fix(formula): improve datetime handling and numeric coercion in workd…
nichenqin Nov 27, 2025
2b0d2ac
fix(formula): enhance numeric coercion and field type resolution in P…
nichenqin Nov 27, 2025
4622adc
fix(formula): add aggregation tests for multi-value lookups with SUM,…
nichenqin Nov 27, 2025
3a4c45e
fix(formula): ensure text collation for numeric blank comparisons to …
nichenqin Nov 27, 2025
a54ab9d
fix(formula): enhance datetime format handling and add regression tes…
nichenqin Nov 27, 2025
cf6b254
fix(formula): default DATETIME_FORMAT to ISO-like pattern for missing…
nichenqin Nov 27, 2025
55ea214
fix(formula): enhance handling of NULL literals and improve datetime …
nichenqin Nov 27, 2025
d9edba9
fix(formula): prevent persisting generated-column meta for formulas i…
nichenqin Nov 28, 2025
0f8910a
chore: delete field confirm loading
nichenqin Nov 28, 2025
0ce7c68
fix(formula): update interfaces for formula conversion context and qu…
nichenqin Nov 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { FormulaFieldCore, TableDomain } from '@teable/core';
import { validateFormulaSupport } from '../../features/record/query-builder/formula-validation';
import type { IGeneratedColumnQuerySupportValidator } from '../../features/record/query-builder/sql-conversion.visitor';

export function validateGeneratedColumnSupport(
field: FormulaFieldCore,
supportValidator: IGeneratedColumnQuerySupportValidator,
tableDomain: TableDomain
_field: FormulaFieldCore,
_supportValidator: IGeneratedColumnQuerySupportValidator,
_tableDomain: TableDomain
): boolean {
const expression = field.getExpression();
return validateFormulaSupport(supportValidator, expression, tableDomain);
// Temporarily disable persisting formulas as generated columns to avoid
// PostgreSQL restrictions (e.g., subqueries) that surface during field
// creation/duplication. All formulas should be computed via the runtime
// pipeline instead of database generated columns.
return false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ describe('GeneratedColumnQuerySupportValidator', () => {

it('should not support array functions due to technical limitations', () => {
expect(postgresValidator.arrayJoin('a', ',')).toBe(false);
expect(postgresValidator.arrayUnique('a')).toBe(false);
expect(postgresValidator.arrayFlatten('a')).toBe(false);
expect(postgresValidator.arrayCompact('a')).toBe(false);
expect(postgresValidator.arrayUnique(['a'])).toBe(false);
expect(postgresValidator.arrayFlatten(['a'])).toBe(false);
expect(postgresValidator.arrayCompact(['a'])).toBe(false);
});

it('should support basic time functions but not time-dependent ones', () => {
Expand All @@ -53,7 +53,7 @@ describe('GeneratedColumnQuerySupportValidator', () => {
});

it('should support basic date functions but not complex ones', () => {
expect(postgresValidator.dateAdd('a', 'b', 'c')).toBe(true);
expect(postgresValidator.dateAdd('a', 'b', 'c')).toBe(false);
expect(postgresValidator.datetimeDiff('a', 'b', 'c')).toBe(false); // Not immutable in PostgreSQL
expect(postgresValidator.year('a')).toBe(false); // Not immutable in PostgreSQL
expect(postgresValidator.month('a')).toBe(false); // Not immutable in PostgreSQL
Expand Down Expand Up @@ -96,9 +96,9 @@ describe('GeneratedColumnQuerySupportValidator', () => {

it('should not support array functions', () => {
expect(sqliteValidator.arrayJoin('a', ',')).toBe(false);
expect(sqliteValidator.arrayUnique('a')).toBe(false);
expect(sqliteValidator.arrayFlatten('a')).toBe(false);
expect(sqliteValidator.arrayCompact('a')).toBe(false);
expect(sqliteValidator.arrayUnique(['a'])).toBe(false);
expect(sqliteValidator.arrayFlatten(['a'])).toBe(false);
expect(sqliteValidator.arrayCompact(['a'])).toBe(false);
});

it('should support basic time functions but not time-dependent ones', () => {
Expand All @@ -122,7 +122,7 @@ describe('GeneratedColumnQuerySupportValidator', () => {
});

it('should support basic date functions', () => {
expect(sqliteValidator.dateAdd('a', 'b', 'c')).toBe(true);
expect(sqliteValidator.dateAdd('a', 'b', 'c')).toBe(false);
expect(sqliteValidator.datetimeDiff('a', 'b', 'c')).toBe(true);
expect(sqliteValidator.year('a')).toBe(false); // Not immutable in SQLite
expect(sqliteValidator.month('a')).toBe(false); // Not immutable in SQLite
Expand Down Expand Up @@ -162,10 +162,11 @@ describe('GeneratedColumnQuerySupportValidator', () => {
] as const;

restrictedFunctions.forEach((funcName) => {
const arg = funcName.startsWith('array') && funcName !== 'arrayJoin' ? ['test'] : 'test';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const postgresResult = (postgresValidator as any)[funcName]('test');
const postgresResult = (postgresValidator as any)[funcName](arg);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sqliteResult = (sqliteValidator as any)[funcName]('test');
const sqliteResult = (sqliteValidator as any)[funcName](arg);
expect(postgresResult).toBe(false);
expect(sqliteResult).toBe(false);
expect(postgresResult).toBe(sqliteResult);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ export abstract class GeneratedColumnQueryAbstract implements IGeneratedColumnQu
abstract countA(params: string[]): string;
abstract countAll(value: string): string;
abstract arrayJoin(array: string, separator?: string): string;
abstract arrayUnique(array: string): string;
abstract arrayFlatten(array: string): string;
abstract arrayCompact(array: string): string;
abstract arrayUnique(arrays: string[]): string;
abstract arrayFlatten(arrays: string[]): string;
abstract arrayCompact(arrays: string[]): string;

// System Functions
abstract recordId(): string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,9 @@ export class GeneratedColumnQuerySupportValidatorPostgres
}

dateAdd(_date: string, _count: string, _unit: string): boolean {
return true;
// DATE_ADD relies on timestamp input parsing which is not immutable in PostgreSQL
// (casts depend on DateStyle/TimeZone). Treat as unsupported for generated columns.
return false;
}

datestr(_date: string): boolean {
Expand Down Expand Up @@ -366,17 +368,17 @@ export class GeneratedColumnQuerySupportValidatorPostgres
return false;
}

arrayUnique(_array: string): boolean {
arrayUnique(_arrays: string[]): boolean {
// Uses subqueries not allowed in generated columns
return false;
}

arrayFlatten(_array: string): boolean {
arrayFlatten(_arrays: string[]): boolean {
// Uses subqueries not allowed in generated columns
return false;
}

arrayCompact(_array: string): boolean {
arrayCompact(_arrays: string[]): boolean {
// Uses subqueries not allowed in generated columns
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,52 @@ describe('GeneratedColumnQueryPostgres unit-aware helpers', () => {
);
});

it('coerces non-text inputs to text for string functions', () => {
const numericMetadata: IFormulaParamMetadata[] = [
{
type: 'number',
isFieldReference: true,
field: {
id: 'fldNum',
dbFieldName: 'AutoNumber',
dbFieldType: DbFieldType.Integer,
isMultiple: false,
},
} as unknown as IFormulaParamMetadata,
];
query.setCallMetadata(numericMetadata);

const lenSql = query.len('"AutoNumber"');
const lowerSql = query.lower('"AutoNumber"');
const upperSql = query.upper('"AutoNumber"');
const trimSql = query.trim('"AutoNumber"');
const reptSql = query.rept('"AutoNumber"', '3');

[lenSql, lowerSql, upperSql, trimSql, reptSql].forEach((sql) => {
expect(sql).toContain('::text');
});
});

it('casts nested text IF chains without recursive JSON coercion', () => {
const nestedIf = (depth: number): string => {
query.setCallMetadata([
{ type: 'boolean', isFieldReference: false } as unknown as IFormulaParamMetadata,
{ type: 'string', isFieldReference: false } as unknown as IFormulaParamMetadata,
{ type: 'string', isFieldReference: false } as unknown as IFormulaParamMetadata,
]);
const result =
depth === 0 ? `'leaf'` : query.if('1', `'branch_${depth}'`, nestedIf(depth - 1));
query.setCallMetadata(undefined);
return result;
};

const sql = nestedIf(8);

expect(sql).not.toContain('jsonb_typeof');
expect(sql).not.toContain('to_jsonb');
expect(sql.length).toBeLessThan(5000);
});

const dateAddCases: Array<{ literal: string; unit: string; factor: number }> = [
{ literal: 'millisecond', unit: 'millisecond', factor: 1 },
{ literal: 'milliseconds', unit: 'millisecond', factor: 1 },
Expand Down Expand Up @@ -90,6 +136,13 @@ describe('GeneratedColumnQueryPostgres unit-aware helpers', () => {
}
);

it('dateAdd with numeric literal count avoids regex and remains immutable', () => {
const sql = query.dateAdd('"Chuang_Jian_Ri_Qi"', '-7', `'day'`);

expect(sql).toContain("INTERVAL '1 day'");
expect(sql).not.toContain('REGEXP_REPLACE');
});

const diffSeconds = `(EXTRACT(EPOCH FROM ${castTs('date_start')} - ${castTs('date_end')}))`;
const datetimeDiffCases: Array<{ literal: string; expected: string }> = [
{
Expand Down Expand Up @@ -299,4 +352,20 @@ describe('GeneratedColumnQueryPostgres unit-aware helpers', () => {
const sql = query.if('"text_col"', "'yes'", "'no'");
expect(sql).toContain('pg_typeof("text_col")::text');
});

it('avoids regex coercion for unary minus numeric literals', () => {
query.setCallMetadata(undefined);
const sql = query.value(query.unaryMinus('7'));

expect(sql).not.toContain('REGEXP_REPLACE');
});

it('collates regex-based numeric coercion to avoid collation conflicts', () => {
query.setCallMetadata(undefined);
const sql = query.value('"text_col"');

expect(sql).toContain('REGEXP_REPLACE');
expect(sql).toContain('COLLATE "C"');
expect(sql).toContain('~ \'^[+-]{0,1}(\\d+(\\.\\d+){0,1}|\\.\\d+)$\' COLLATE "C"');
});
});
Loading
Loading