diff --git a/packages/cli/src/modules/data-table/utils/sql-utils.ts b/packages/cli/src/modules/data-table/utils/sql-utils.ts index b9fc0efbce2..7971dca6a23 100644 --- a/packages/cli/src/modules/data-table/utils/sql-utils.ts +++ b/packages/cli/src/modules/data-table/utils/sql-utils.ts @@ -20,6 +20,7 @@ import type { DataTableColumn } from '../data-table-column.entity'; import type { DataTableUserTableName } from '../data-table.types'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; +import { parsePath, toPostgresPath, toSQLitePath } from './path-utils'; export function toDslColumns(columns: DataTableCreateColumnSchema[]): DslColumn[] { return columns.map((col) => { @@ -42,7 +43,7 @@ export function toDslColumns(columns: DataTableCreateColumnSchema[]): DslColumn[ }); } -function dataTableColumnTypeToSql( +export function dataTableColumnTypeToSql( type: DataTableCreateColumnSchema['type'], dbType: DataSourceOptions['type'], ) { @@ -372,3 +373,49 @@ export function toTableName(dataTableId: string): DataTableUserTableName { export function toTableId(tableName: DataTableUserTableName) { return tableName.replace(/.*data_table_user_/, ''); } + +export function resolvePath( + ref: string, + dbType: DataSourceOptions['type'], + value: unknown, + path?: string, +) { + if (path) { + const pathArray = parsePath(path); + if (dbType === 'postgres') { + const base = `${ref}${toPostgresPath(pathArray)}`; + if (typeof value === 'number') { + return `(${base})::numeric`; + } + if (value instanceof Date) { + return `(${base})::timestamp`; + } + if (typeof value === 'boolean') { + return `(${base})::boolean`; + } + + // by converting to text by default we end up with `true` for an equals NULL check + // both for cases where the key exists and is literally NULL and where it doesn't exist + return `(${base})::text`; + } else { + // this is mostly for sqlite, behavior in MariaDB and MySQL mostly aligns though there are subtle + // difference we don't care for in the face of imminent removal of support + const path = toSQLitePath(pathArray); + const base = `json_extract(${ref}, '${path.replaceAll("'", "\\'")}')`; + if (typeof value === 'number') { + return `CAST(${base} as ${dataTableColumnTypeToSql('number', dbType)})`; + } + if (value instanceof Date) { + return `CAST(${base} as ${dataTableColumnTypeToSql('date', dbType)}})`; + } + if (typeof value === 'boolean') { + return `CAST(${base} as ${dataTableColumnTypeToSql('boolean', dbType)}})`; + } + if (typeof value === 'string') { + return `CAST(${base} AS ${dataTableColumnTypeToSql('string', dbType)}})`; + } + return base; + } + } + return ref; +} diff --git a/packages/nodes-base/nodes/DataTable/common/utils.ts b/packages/nodes-base/nodes/DataTable/common/utils.ts index c91b177407f..66b6cecdf0f 100644 --- a/packages/nodes-base/nodes/DataTable/common/utils.ts +++ b/packages/nodes-base/nodes/DataTable/common/utils.ts @@ -163,15 +163,10 @@ export function dataObjectToApiInput( if (v === undefined || v === null) return [k, null]; if (Array.isArray(v)) { - const z = v as GenericValue[] | IDataObject[]; - return [ - k, - z.map((x) => - typeof x === 'object' && x !== null - ? dataObjectToApiInput(x as IDataObject, node, row) - : (x ?? null), - ), - ]; + throw new NodeOperationError( + node, + `unexpected array input '${JSON.stringify(v)}' in row ${row}`, + ); } if (v instanceof Date) { diff --git a/packages/nodes-base/nodes/DataTable/test/common/utils.test.ts b/packages/nodes-base/nodes/DataTable/test/common/utils.test.ts index 1786dd5db1a..64a8faae60a 100644 --- a/packages/nodes-base/nodes/DataTable/test/common/utils.test.ts +++ b/packages/nodes-base/nodes/DataTable/test/common/utils.test.ts @@ -107,18 +107,6 @@ describe('dataObjectToApiInput', () => { expect(result.createdAt).toBeInstanceOf(Date); expect((result.createdAt as Date).toISOString()).toBe('2025-09-01T12:00:00.000Z'); }); - - it('should handle date-like objects where toISOString throws', () => { - const dateLikeObject = { - toISOString: () => { - throw new Error('toISOString failed'); - }, - }; - const input = { createdAt: dateLikeObject, name: 'test' }; - - expect(() => dataObjectToApiInput(input, mockNode, 0)).toThrow(NodeOperationError); - expect(() => dataObjectToApiInput(input, mockNode, 0)).toThrow('unexpected object input'); - }); }); describe('error cases', () => { @@ -134,17 +122,8 @@ describe('dataObjectToApiInput', () => { it('should throw error for plain objects', () => { const input = { metadata: { key: 'value' } }; - expect(() => dataObjectToApiInput(input, mockNode, 0)).toThrow(NodeOperationError); - expect(() => dataObjectToApiInput(input, mockNode, 0)).toThrow( - 'unexpected object input \'{"key":"value"}\' in row 0', - ); - }); - - it('should throw error for objects without toISOString method', () => { - const input = { config: { setting1: true, setting2: 'value' } }; - - expect(() => dataObjectToApiInput(input, mockNode, 0)).toThrow(NodeOperationError); - expect(() => dataObjectToApiInput(input, mockNode, 0)).toThrow('unexpected object input'); + const result = dataObjectToApiInput(input, mockNode, 0); + expect(result).toEqual(input); }); test('dataObjectToApiInput throws on invalid date-like object', () => { diff --git a/packages/workflow/src/data-table.types.ts b/packages/workflow/src/data-table.types.ts index b579efbef6a..f35caefc4ea 100644 --- a/packages/workflow/src/data-table.types.ts +++ b/packages/workflow/src/data-table.types.ts @@ -87,7 +87,6 @@ export type DataTableColumnJsType = | boolean | Date | { [k: string | number]: DataTableColumnJsType } - | DataTableColumnJsType[] | null; export const DATA_TABLE_SYSTEM_COLUMN_TYPE_MAP: Record = {