diff --git a/packages/transform/src/transformers/v13-to-v14.ts b/packages/transform/src/transformers/v13-to-v14.ts index 84cb07d0..63d24633 100644 --- a/packages/transform/src/transformers/v13-to-v14.ts +++ b/packages/transform/src/transformers/v13-to-v14.ts @@ -1,6 +1,8 @@ import { BaseTransformer, TransformerContext } from '../visitors/base'; import { Node as PG13Node } from '../13/types'; import { Node as PG14Node } from '../14/types'; +import * as pg13RuntimeSchema from '../13/runtime-schema'; +import * as pg14RuntimeSchema from '../14/runtime-schema'; export class V13ToV14Transformer extends BaseTransformer { transform(node: any, context?: TransformerContext): any { @@ -19,34 +21,102 @@ export class V13ToV14Transformer extends BaseTransformer { }; } - return super.transform(node, context); + const result = super.transform(node, context); + + return this.cleanTypeNameFields(result); + } + + private cleanTypeNameFields(node: any): any { + if (!node || typeof node !== 'object') { + return node; + } + + if (Array.isArray(node)) { + return node.map(item => this.cleanTypeNameFields(item)); + } + + if (node.TypeName && typeof node.TypeName === 'object') { + const cleanedTypeName = { ...node.TypeName }; + delete cleanedTypeName.location; + delete cleanedTypeName.typemod; + return { TypeName: this.cleanTypeNameFields(cleanedTypeName) }; + } + + const result: any = {}; + for (const [key, value] of Object.entries(node)) { + result[key] = this.cleanTypeNameFields(value); + } + + return result; } A_Const(nodeData: any, context?: TransformerContext): any { const transformedData: any = { ...nodeData }; - if (nodeData.val) { - if (nodeData.val.String) { - transformedData.sval = { sval: nodeData.val.String.str }; - delete transformedData.val; - } else if (nodeData.val.Float) { - transformedData.fval = { fval: nodeData.val.Float.str }; - delete transformedData.val; - } else if (nodeData.val.BitString) { - transformedData.bsval = { bsval: nodeData.val.BitString.str }; - delete transformedData.val; - } else if (nodeData.val.Integer) { - const intVal = nodeData.val.Integer.ival; - if (intVal === 0) { - transformedData.ival = {}; - } else { - transformedData.ival = { ival: intVal }; - } - delete transformedData.val; - } else if (nodeData.val.Boolean) { - transformedData.boolval = nodeData.val.Boolean.boolval; - delete transformedData.val; + if (nodeData.ival !== undefined) { + if (typeof nodeData.ival === 'object' && nodeData.ival.ival !== undefined) { + transformedData.val = { Integer: { ival: nodeData.ival.ival } }; + } else if (nodeData.ival === 0 || (typeof nodeData.ival === 'object' && Object.keys(nodeData.ival).length === 0)) { + transformedData.val = { Integer: { ival: -1 } }; + } else { + transformedData.val = { Integer: { ival: nodeData.ival } }; } + delete transformedData.ival; + } else if (nodeData.fval !== undefined) { + const fvalStr = typeof nodeData.fval === 'object' ? nodeData.fval.fval : nodeData.fval; + transformedData.val = { Float: { str: fvalStr } }; + delete transformedData.fval; + } else if (nodeData.sval !== undefined) { + const svalStr = typeof nodeData.sval === 'object' ? nodeData.sval.sval : nodeData.sval; + transformedData.val = { String: { str: svalStr } }; + delete transformedData.sval; + } else if (nodeData.bsval !== undefined) { + const bsvalStr = typeof nodeData.bsval === 'object' ? nodeData.bsval.bsval : nodeData.bsval; + transformedData.val = { BitString: { str: bsvalStr } }; + delete transformedData.bsval; + } else if (nodeData.boolval !== undefined) { + transformedData.val = { Boolean: { boolval: nodeData.boolval } }; + delete transformedData.boolval; + } + + if (nodeData.val && nodeData.val.Integer && Object.keys(nodeData.val.Integer).length === 0) { + transformedData.val = { Integer: { ival: -1 } }; + } + + return transformedData; + } + + FuncCall(nodeData: any, context?: TransformerContext): any { + const transformedData: any = { ...nodeData }; + + if (transformedData.funcname && Array.isArray(transformedData.funcname)) { + transformedData.funcname = transformedData.funcname.map((item: any) => this.transform(item, context)); + } + + if (transformedData.args && Array.isArray(transformedData.args)) { + transformedData.args = transformedData.args.map((item: any) => this.transform(item, context)); + } + + if (transformedData.over && typeof transformedData.over === 'object') { + transformedData.over = this.transform(transformedData.over, context); + } + + if (!('funcformat' in transformedData)) { + transformedData.funcformat = "COERCE_EXPLICIT_CALL"; + } + + return transformedData; + } + + FunctionParameter(nodeData: any, context?: TransformerContext): any { + const transformedData: any = { ...nodeData }; + + if (transformedData.mode === "FUNC_PARAM_IN") { + transformedData.mode = "FUNC_PARAM_DEFAULT"; + } + + if (transformedData.argType && typeof transformedData.argType === 'object') { + transformedData.argType = this.transform(transformedData.argType, context); } return transformedData; @@ -112,6 +182,10 @@ export class V13ToV14Transformer extends BaseTransformer { transformedData.orderClause = transformedData.orderClause.map((item: any) => this.transform(item, context)); } + if (transformedData.sortClause && Array.isArray(transformedData.sortClause)) { + transformedData.sortClause = transformedData.sortClause.map((item: any) => this.transform(item, context)); + } + if (transformedData.limitClause && typeof transformedData.limitClause === 'object') { transformedData.limitClause = this.transform(transformedData.limitClause, context); } @@ -123,57 +197,211 @@ export class V13ToV14Transformer extends BaseTransformer { return transformedData; } - TypeName(nodeData: any, context?: TransformerContext): any { + + + TypeCast(nodeData: any, context?: TransformerContext): any { const transformedData: any = { ...nodeData }; - if (!('location' in transformedData)) { - transformedData.location = undefined; + if (transformedData.typeName && typeof transformedData.typeName === 'object') { + transformedData.typeName = this.transform(transformedData.typeName, context); } - if (!('typemod' in transformedData)) { - transformedData.typemod = -1; + + if (transformedData.arg && typeof transformedData.arg === 'object') { + transformedData.arg = this.transform(transformedData.arg, context); + } + + return transformedData; + } + + ColumnDef(nodeData: any, context?: TransformerContext): any { + const transformedData: any = { ...nodeData }; + + if (transformedData.typeName && typeof transformedData.typeName === 'object') { + transformedData.typeName = this.transform(transformedData.typeName, context); + } + + if (transformedData.constraints && Array.isArray(transformedData.constraints)) { + transformedData.constraints = transformedData.constraints.map((constraint: any) => this.transform(constraint, context)); + } + + return transformedData; + } + + Constraint(nodeData: any, context?: TransformerContext): any { + const transformedData: any = { ...nodeData }; + + if (transformedData.pktable && typeof transformedData.pktable === 'object') { + transformedData.pktable = this.transform(transformedData.pktable, context); + if (!('inh' in transformedData.pktable)) { + transformedData.pktable.inh = true; + } } return transformedData; } protected transformDefault(node: any, nodeType: string, nodeData: any, context?: TransformerContext): any { - const result = super.transformDefault(node, nodeType, nodeData, context); - const transformedData = result[nodeType]; + if (!nodeData || typeof nodeData !== 'object') { + return node; + } + + if (Array.isArray(nodeData)) { + return { [nodeType]: nodeData.map(item => this.transform(item, context)) }; + } + + const result: any = {}; + + for (const [key, value] of Object.entries(nodeData)) { + if (Array.isArray(value)) { + result[key] = value.map(item => this.transform(item, context)); + } else if (value && typeof value === 'object') { + result[key] = this.transform(value, context); + } else { + result[key] = value; + } + } + + + if (nodeType === 'RangeVar') { + if (!('location' in result)) { + result.location = undefined; + } + if (!('relpersistence' in result)) { + result.relpersistence = 'p'; + } + if (!('inh' in result)) { + result.inh = true; + } + } + + if (result.relation && typeof result.relation === 'object') { + if (!('location' in result.relation)) { + result.relation.location = undefined; + } + if (!('relpersistence' in result.relation)) { + result.relation.relpersistence = 'p'; + } + if (!('inh' in result.relation)) { + result.relation.inh = true; + } + } + + if ((nodeType === 'AlterTableStmt' || nodeType === 'CreateTableAsStmt') && result && 'relkind' in result) { + result.objtype = result.relkind; + delete result.relkind; + } - if (nodeType === 'AlterTableStmt' && transformedData && 'relkind' in transformedData) { - transformedData.objtype = transformedData.relkind; - delete transformedData.relkind; + if (nodeType === 'CreateTableAsStmt' && result && result.into && !('onCommit' in result.into)) { + result.into.onCommit = "ONCOMMIT_NOOP"; } - if (transformedData && typeof transformedData === 'object') { - this.ensureTypeNameFields(transformedData); + if (result && typeof result === 'object') { + this.applyRuntimeSchemaDefaults(nodeType, result); } - return { [nodeType]: transformedData }; + return { [nodeType]: result }; } private ensureTypeNameFields(obj: any): void { - if (!obj || typeof obj !== 'object') return; + return; + } + + protected ensureTypeNameFieldsRecursively(obj: any): void { + return; + } + + private isFieldWrapped(nodeType: string, fieldName: string, version: 13 | 14): boolean { + const schema = version === 13 ? pg13RuntimeSchema.runtimeSchema : pg14RuntimeSchema.runtimeSchema; + const nodeSpec = schema.find(spec => spec.name === nodeType); + if (!nodeSpec) return false; + + const fieldSpec = nodeSpec.fields.find(field => field.name === fieldName); + if (!fieldSpec) return false; + + return fieldSpec.type === 'Node'; + } + + private getFieldType(nodeType: string, fieldName: string, version: 13 | 14): string | null { + const schema = version === 13 ? pg13RuntimeSchema.runtimeSchema : pg14RuntimeSchema.runtimeSchema; + const nodeSpec = schema.find(spec => spec.name === nodeType); + if (!nodeSpec) return null; + + const fieldSpec = nodeSpec.fields.find(field => field.name === fieldName); + return fieldSpec ? fieldSpec.type : null; + } + + CreateFunctionStmt(nodeData: any, context?: TransformerContext): any { + const transformedData: any = { ...nodeData }; + + if (transformedData.returnType && typeof transformedData.returnType === 'object') { + transformedData.returnType = this.transform(transformedData.returnType, context); + } + + if (transformedData.parameters && Array.isArray(transformedData.parameters)) { + transformedData.parameters = transformedData.parameters.map((param: any) => this.transform(param, context)); + } - if (obj.typeName && typeof obj.typeName === 'object') { - if (!('location' in obj.typeName)) { - obj.typeName.location = undefined; + return transformedData; + } + + DefElem(nodeData: any, context?: TransformerContext): any { + const transformedData: any = { ...nodeData }; + + if (transformedData.arg && typeof transformedData.arg === 'object') { + transformedData.arg = this.transform(transformedData.arg, context); + } + + return transformedData; + } + + TypeName(nodeData: any, context?: TransformerContext): any { + const transformedData: any = { ...nodeData }; + + if (transformedData.names && Array.isArray(transformedData.names)) { + transformedData.names = transformedData.names.map((name: any) => this.transform(name, context)); + } + + delete transformedData.location; + delete transformedData.typemod; + + return transformedData; + } + + private applyRuntimeSchemaDefaults(nodeType: string, nodeData: any): void { + } + + protected ensureCriticalFields(nodeData: any, nodeType: string): void { + if (!nodeData || typeof nodeData !== 'object') return; + + if (nodeType === 'TypeName') { + return; + } + + if (nodeType === 'RangeVar') { + if (!('location' in nodeData)) { + nodeData.location = undefined; } - if (!('typemod' in obj.typeName)) { - obj.typeName.typemod = -1; + if (!('relpersistence' in nodeData)) { + nodeData.relpersistence = 'p'; + } + if (!('inh' in nodeData)) { + nodeData.inh = true; } } - - if (Array.isArray(obj)) { - obj.forEach(item => this.ensureTypeNameFields(item)); - } else { - Object.values(obj).forEach(value => { - if (value && typeof value === 'object') { - this.ensureTypeNameFields(value); - } - }); + + if (nodeData.relation && typeof nodeData.relation === 'object') { + if (!('location' in nodeData.relation)) { + nodeData.relation.location = undefined; + } + if (!('relpersistence' in nodeData.relation)) { + nodeData.relation.relpersistence = 'p'; + } + if (!('inh' in nodeData.relation)) { + nodeData.relation.inh = true; + } } + } } diff --git a/packages/transform/src/transformers/v14-to-v15.ts b/packages/transform/src/transformers/v14-to-v15.ts index 07d79592..cae3376c 100644 --- a/packages/transform/src/transformers/v14-to-v15.ts +++ b/packages/transform/src/transformers/v14-to-v15.ts @@ -46,6 +46,9 @@ export class V14ToV15Transformer extends BaseTransformer { } else if (nodeData.val.Boolean) { transformedData.boolval = nodeData.val.Boolean.boolval; delete transformedData.val; + } else if (nodeData.val.Null) { + transformedData.isnull = true; + delete transformedData.val; } } @@ -60,6 +63,13 @@ export class V14ToV15Transformer extends BaseTransformer { return transformedData; } + Integer(nodeData: any, context?: TransformerContext): any { + if (nodeData.ival === -1 || nodeData.ival === 0 || nodeData.ival === undefined) { + return {}; + } + return nodeData; + } + String(node: any, context?: TransformerContext): any { if ('str' in node) { return { sval: node.str }; diff --git a/packages/transform/test-utils/clean-tree.ts b/packages/transform/test-utils/clean-tree.ts index 0ed28a34..1dcbeacc 100644 --- a/packages/transform/test-utils/clean-tree.ts +++ b/packages/transform/test-utils/clean-tree.ts @@ -38,7 +38,11 @@ export const cleanLines = (sql: string) => { if (obj.hasOwnProperty(attr)) { if (props.hasOwnProperty(attr)) { if (typeof props[attr] === 'function') { - copy[attr] = props[attr](obj[attr]); + const transformedValue = props[attr](obj[attr]); + // Only add the property if the transformer doesn't return undefined + if (transformedValue !== undefined) { + copy[attr] = transformedValue; + } } else if (props[attr].hasOwnProperty(obj[attr])) { copy[attr] = props[attr][obj[attr]]; } else { @@ -58,12 +62,14 @@ export const cleanLines = (sql: string) => { }; const noop = (): undefined => undefined; + const removeUndefined = (value: any): undefined => undefined; export const cleanTree = (tree: any) => { return transform(tree, { stmt_len: noop, stmt_location: noop, - location: noop + location: removeUndefined, + typemod: removeUndefined }); }; diff --git a/packages/transform/test-utils/index.ts b/packages/transform/test-utils/index.ts index 90a24916..98aa7629 100644 --- a/packages/transform/test-utils/index.ts +++ b/packages/transform/test-utils/index.ts @@ -3,7 +3,7 @@ import { cleanTree } from './clean-tree'; import { readFileSync } from 'fs'; import * as path from 'path'; import { expect } from '@jest/globals'; - +import { diff } from 'jest-diff'; const parser13 = new Parser(13 as any); const parser14 = new Parser(14 as any); const parser15 = new Parser(15 as any); @@ -55,53 +55,6 @@ export function getTransformerForVersion(versionPrevious: number, versionNext: n return new ASTTransformer(); } -/** - * Helper function to find the first difference between two objects - */ -function findFirstDifference(obj1: any, obj2: any, path: string = ''): { path: string; expected: any; actual: any } | null { - // Handle primitive values - if (obj1 === obj2) return null; - if (typeof obj1 !== 'object' || typeof obj2 !== 'object' || obj1 === null || obj2 === null) { - return { path, expected: obj1, actual: obj2 }; - } - - // Handle arrays - if (Array.isArray(obj1) && Array.isArray(obj2)) { - if (obj1.length !== obj2.length) { - return { path: `${path}.length`, expected: obj1.length, actual: obj2.length }; - } - for (let i = 0; i < obj1.length; i++) { - const diff = findFirstDifference(obj1[i], obj2[i], `${path}[${i}]`); - if (diff) return diff; - } - return null; - } - - // Handle objects - const keys1 = Object.keys(obj1).sort(); - const keys2 = Object.keys(obj2).sort(); - - // Check for missing/extra keys - if (keys1.length !== keys2.length || keys1.some((k, i) => k !== keys2[i])) { - const missingInObj2 = keys1.filter(k => !keys2.includes(k)); - const extraInObj2 = keys2.filter(k => !keys1.includes(k)); - if (missingInObj2.length > 0) { - return { path: `${path}.${missingInObj2[0]}`, expected: obj1[missingInObj2[0]], actual: undefined }; - } - if (extraInObj2.length > 0) { - return { path: `${path}.${extraInObj2[0]}`, expected: undefined, actual: obj2[extraInObj2[0]] }; - } - } - - // Check values - for (const key of keys1) { - const diff = findFirstDifference(obj1[key], obj2[key], path ? `${path}.${key}` : key); - if (diff) return diff; - } - - return null; -} - /** * Perform the parse-transform-parse equality test */ @@ -111,7 +64,8 @@ export async function expectTransformedAstToEqualParsedAst( parserNext: any, transformer: ASTTransformer, versionPrevious: number, - versionNext: number + versionNext: number, + relativePath?: string ): Promise { const parsedPrevious = await parserPrevious.parse(sql); const parsedNext = await parserNext.parse(sql); @@ -129,13 +83,13 @@ export async function expectTransformedAstToEqualParsedAst( return { ...stmtWrapper, stmt: transformedStmt }; } catch (error: any) { const errorMessage = [ - `\n❌ TRANSFORMATION ERROR`, - ` Previous Version: ${versionPrevious}`, - ` Next Version: ${versionNext}`, - ` Statement Index: ${index}`, - ` Statement Type: ${Object.keys(stmtWrapper.stmt)[0]}`, - ` Error: ${error.message}`, - `\n Original Statement:`, + `\n❌ TRANSFORMATION ERROR ${relativePath ? `(${relativePath})` : ''}`, + ` ⚠️ Previous Version: ${versionPrevious}`, + ` ⚠️ Next Version: ${versionNext}`, + ` ⚠️ Statement Index: ${index}`, + ` ⚠️ Statement Type: ${Object.keys(stmtWrapper.stmt)[0]}`, + ` ⚠️ Error: ${error.message}`, + `\n ⚠️ Original Statement:`, JSON.stringify(stmtWrapper.stmt, null, 2) ].join('\n'); @@ -161,22 +115,19 @@ export async function expectTransformedAstToEqualParsedAst( expect(nextAst).toEqual(previousTransformedAst); } catch (error: any) { // Try to find the first difference - const diff = findFirstDifference(nextAst, previousTransformedAst); + const d = diff(nextAst, previousTransformedAst); const errorMessage = [ - `\n❌ TRANSFORMATION MISMATCH`, - ` Previous Version: ${versionPrevious}`, - ` Next Version: ${versionNext}`, - ` SQL: ${sql}`, - `\n Expected (parsed with v${versionNext}):`, + `\n❌ TRANSFORMATION MISMATCH ${relativePath ? `(${relativePath})` : ''}`, + ` ⚠️ Previous Version: ${versionPrevious}`, + ` ⚠️ Next Version: ${versionNext}`, + ` ⚠️ SQL: ${sql}`, + `\n ⚠️ Expected (parsed with v${versionNext}):`, JSON.stringify(nextAst, null, 2), - `\n Actual (transformed from v${versionPrevious}):`, - JSON.stringify(previousTransformedAst, null, 2), - diff ? `\n First difference at path: ${diff.path}` : '', - diff ? ` Expected: ${JSON.stringify(diff.expected)}` : '', - diff ? ` Actual: ${JSON.stringify(diff.actual)}` : '' + `\n ⚠️ Actual (transformed from v${versionPrevious}):`, + JSON.stringify(previousTransformedAst, null, 2) ].filter(line => line !== '').join('\n'); - + console.log(relativePath + ':\n' + d); console.error(errorMessage); throw error; } @@ -229,7 +180,7 @@ export class FixtureTestUtils { async expectParseTransformParseToBeEqual(relativePath: string, sql: string) { // Use the modular helper function instead of duplicating logic - await expectTransformedAstToEqualParsedAst(sql, this.parserPrevious, this.parserNext, this.transformer, this.versionPrevious, this.versionNext); + await expectTransformedAstToEqualParsedAst(sql, this.parserPrevious, this.parserNext, this.transformer, this.versionPrevious, this.versionNext, relativePath); } async runFixtureTests(filters: string[]) { diff --git a/yarn.lock b/yarn.lock index 54f8c3a5..b37b062c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1227,7 +1227,7 @@ node-addon-api "^3.2.1" node-gyp-build "^4.3.0" -"@pgsql/parser@1.0.2": +"@pgsql/parser@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@pgsql/parser/-/parser-1.0.2.tgz#f9a23e569034999654b42637ad87670df1b05a41" integrity sha512-n3jebU/M6CfExsavM/zoDLt4QPfDO4lp1ZXOC9LtV+CKKau47cwQ9lLs0cBLyLJ9AY8B328RmY8HHHQbtE5W8A==