diff --git a/sample/definitions/function/syntax_error_trigger_column_does_not_exist.pgsql b/sample/definitions/function/syntax_error_trigger_column_does_not_exist.pgsql new file mode 100644 index 0000000..3d79156 --- /dev/null +++ b/sample/definitions/function/syntax_error_trigger_column_does_not_exist.pgsql @@ -0,0 +1,36 @@ +CREATE TABLE users_1 ( + id integer not null PRIMARY KEY, + updated_at timestamp with time zone not null DEFAULT now() +); +CREATE TABLE users_2 ( + id integer not null PRIMARY KEY +); +CREATE TABLE users_3 ( + id integer not null PRIMARY KEY +); + +DROP trigger IF EXISTS update_users_1_modtime on user_1; +DROP trigger IF EXISTS update_users_2_modtime on user_2; +DROP trigger IF EXISTS update_users_3_modtime on user_3; + +create or replace function update_updated_at_column () + returns trigger + language plpgsql + as $function$ +begin + new.updated_at = NOW(); + return new; +end; +$function$; + +create trigger update_users_3_modtime -- should raise error + before update on users_3 for each row + execute function update_updated_at_column (); + +create trigger update_users_1_modtime + before update on users_1 for each row + execute function update_updated_at_column (); + +create trigger update_users_2_modtime -- should raise error + before update on users_2 for each row + execute function update_updated_at_column (); diff --git a/sample/definitions/function/syntax_error_trigger_column_does_not_exist.pgsql.json b/sample/definitions/function/syntax_error_trigger_column_does_not_exist.pgsql.json new file mode 100644 index 0000000..8c7d3d6 --- /dev/null +++ b/sample/definitions/function/syntax_error_trigger_column_does_not_exist.pgsql.json @@ -0,0 +1,418 @@ +[ + { + "RawStmt": { + "stmt": { + "CreateStmt": { + "relation": { + "relname": "users_1", + "inh": true, + "relpersistence": "p" + }, + "tableElts": [ + { + "ColumnDef": { + "colname": "id", + "typeName": { + "names": [ + { + "String": { + "str": "pg_catalog" + } + }, + { + "String": { + "str": "int4" + } + } + ], + "typemod": -1 + }, + "is_local": true, + "constraints": [ + { + "Constraint": { + "contype": "CONSTR_NOTNULL" + } + }, + { + "Constraint": { + "contype": "CONSTR_PRIMARY" + } + } + ] + } + }, + { + "ColumnDef": { + "colname": "updated_at", + "typeName": { + "names": [ + { + "String": { + "str": "pg_catalog" + } + }, + { + "String": { + "str": "timestamptz" + } + } + ], + "typemod": -1 + }, + "is_local": true, + "constraints": [ + { + "Constraint": { + "contype": "CONSTR_NOTNULL" + } + }, + { + "Constraint": { + "contype": "CONSTR_DEFAULT", + "raw_expr": { + "FuncCall": { + "funcname": [ + { + "String": { + "str": "now" + } + } + ] + } + } + } + } + ] + } + } + ], + "oncommit": "ONCOMMIT_NOOP" + } + }, + "stmt_len": 120 + } + }, + { + "RawStmt": { + "stmt": { + "CreateStmt": { + "relation": { + "relname": "users_2", + "inh": true, + "relpersistence": "p" + }, + "tableElts": [ + { + "ColumnDef": { + "colname": "id", + "typeName": { + "names": [ + { + "String": { + "str": "pg_catalog" + } + }, + { + "String": { + "str": "int4" + } + } + ], + "typemod": -1 + }, + "is_local": true, + "constraints": [ + { + "Constraint": { + "contype": "CONSTR_NOTNULL" + } + }, + { + "Constraint": { + "contype": "CONSTR_PRIMARY" + } + } + ] + } + } + ], + "oncommit": "ONCOMMIT_NOOP" + } + }, + "stmt_len": 59 + } + }, + { + "RawStmt": { + "stmt": { + "CreateStmt": { + "relation": { + "relname": "users_3", + "inh": true, + "relpersistence": "p" + }, + "tableElts": [ + { + "ColumnDef": { + "colname": "id", + "typeName": { + "names": [ + { + "String": { + "str": "pg_catalog" + } + }, + { + "String": { + "str": "int4" + } + } + ], + "typemod": -1 + }, + "is_local": true, + "constraints": [ + { + "Constraint": { + "contype": "CONSTR_NOTNULL" + } + }, + { + "Constraint": { + "contype": "CONSTR_PRIMARY" + } + } + ] + } + } + ], + "oncommit": "ONCOMMIT_NOOP" + } + }, + "stmt_len": 59 + } + }, + { + "RawStmt": { + "stmt": { + "DropStmt": { + "objects": [ + { + "List": { + "items": [ + { + "String": { + "str": "user_1" + } + }, + { + "String": { + "str": "update_users_1_modtime" + } + } + ] + } + } + ], + "removeType": "OBJECT_TRIGGER", + "behavior": "DROP_RESTRICT", + "missing_ok": true + } + }, + "stmt_len": 57 + } + }, + { + "RawStmt": { + "stmt": { + "DropStmt": { + "objects": [ + { + "List": { + "items": [ + { + "String": { + "str": "user_2" + } + }, + { + "String": { + "str": "update_users_2_modtime" + } + } + ] + } + } + ], + "removeType": "OBJECT_TRIGGER", + "behavior": "DROP_RESTRICT", + "missing_ok": true + } + }, + "stmt_len": 56 + } + }, + { + "RawStmt": { + "stmt": { + "DropStmt": { + "objects": [ + { + "List": { + "items": [ + { + "String": { + "str": "user_3" + } + }, + { + "String": { + "str": "update_users_3_modtime" + } + } + ] + } + } + ], + "removeType": "OBJECT_TRIGGER", + "behavior": "DROP_RESTRICT", + "missing_ok": true + } + }, + "stmt_len": 56 + } + }, + { + "RawStmt": { + "stmt": { + "CreateFunctionStmt": { + "replace": true, + "funcname": [ + { + "String": { + "str": "update_updated_at_column" + } + } + ], + "returnType": { + "names": [ + { + "String": { + "str": "trigger" + } + } + ], + "typemod": -1 + }, + "options": [ + { + "DefElem": { + "defname": "language", + "arg": { + "String": { + "str": "plpgsql" + } + }, + "defaction": "DEFELEM_UNSPEC" + } + }, + { + "DefElem": { + "defname": "as", + "arg": { + "List": { + "items": [ + { + "String": { + "str": "\nbegin\n new.updated_at = NOW();\n return new;\nend;\n" + } + } + ] + } + }, + "defaction": "DEFELEM_UNSPEC" + } + } + ] + } + }, + "stmt_len": 171 + } + }, + { + "RawStmt": { + "stmt": { + "CreateTrigStmt": { + "trigname": "update_users_3_modtime", + "relation": { + "relname": "users_3", + "inh": true, + "relpersistence": "p" + }, + "funcname": [ + { + "String": { + "str": "update_updated_at_column" + } + } + ], + "row": true, + "timing": 2, + "events": 16 + } + }, + "stmt_len": 148 + } + }, + { + "RawStmt": { + "stmt": { + "CreateTrigStmt": { + "trigname": "update_users_1_modtime", + "relation": { + "relname": "users_1", + "inh": true, + "relpersistence": "p" + }, + "funcname": [ + { + "String": { + "str": "update_updated_at_column" + } + } + ], + "row": true, + "timing": 2, + "events": 16 + } + }, + "stmt_len": 126 + } + }, + { + "RawStmt": { + "stmt": { + "CreateTrigStmt": { + "trigname": "update_users_2_modtime", + "relation": { + "relname": "users_2", + "inh": true, + "relpersistence": "p" + }, + "funcname": [ + { + "String": { + "str": "update_updated_at_column" + } + } + ], + "row": true, + "timing": 2, + "events": 16 + } + }, + "stmt_len": 148 + } + } +] \ No newline at end of file diff --git a/server/src/postgres/parsers/parseFunctions.ts b/server/src/postgres/parsers/parseFunctions.ts index 8e57832..27cddc0 100644 --- a/server/src/postgres/parsers/parseFunctions.ts +++ b/server/src/postgres/parsers/parseFunctions.ts @@ -16,6 +16,7 @@ export interface TriggerInfo { functionName: string, relname: string, stmtLocation?: number, + stmtLen: number, } export async function parseFunctions( @@ -138,7 +139,8 @@ function getCreateTriggers( { functionName, relname, - stmtLocation: statement?.stmt_location, + stmtLocation: statement.stmt_location, + stmtLen: statement.stmt_len, }, ] }, diff --git a/server/src/postgres/queries/queryFileStaticAnalysis.ts b/server/src/postgres/queries/queryFileStaticAnalysis.ts index 6a46657..c7ae4e6 100644 --- a/server/src/postgres/queries/queryFileStaticAnalysis.ts +++ b/server/src/postgres/queries/queryFileStaticAnalysis.ts @@ -8,7 +8,10 @@ import { } from "@/postgres/parameters" import { FunctionInfo, TriggerInfo } from "@/postgres/parsers/parseFunctions" import { Settings } from "@/settings" -import { getLineRangeFromBuffer, getTextAllRange } from "@/utilities/text" +import { + getLineRangeFromBuffer, + getRangeFromBuffer, getTextAllRange, +} from "@/utilities/text" export interface StaticAnalysisErrorRow { procedure: string @@ -108,7 +111,7 @@ export async function queryFileStaticAnalysis( try { for (const triggerInfo of triggerInfos) { - const { functionName, stmtLocation, relname } = triggerInfo + const { functionName, stmtLocation, relname, stmtLen } = triggerInfo logger.warn(` trigger::: relname: ${relname} @@ -140,7 +143,7 @@ export async function queryFileStaticAnalysis( continue } - extractError(rows, stmtLocation) + extractError(rows, stmtLocation, stmtLen) } } catch (error: unknown) { @@ -157,30 +160,35 @@ export async function queryFileStaticAnalysis( function extractError( rows: StaticAnalysisErrorRow[], location: number | undefined, + stmtLen?: number, ) { rows.forEach( (row) => { - let range: Range - // FIXME getLineRangeFromBuffer - // range may be larger than byte count for some cases at the end of the doc and throw err reading length of undefined. - // both fileText.length and location from parsed stmt are correct - try { - range = (() => { - return (location === undefined) - ? getTextAllRange(document) - : getLineRangeFromBuffer( - fileText, - location, - row.lineno ? row.lineno - 1 : 0, - ) ?? getTextAllRange(document) + const range = (() => { + if (location === undefined) { + return getTextAllRange(document) + } + if (stmtLen) { + return getRangeFromBuffer( + fileText, + location + 1, + location + 1 + stmtLen, + ) + } + + const lineRange = getLineRangeFromBuffer( + fileText, + location, + row.lineno ? row.lineno - 1 : 0, + ) + + if (!lineRange) { + return getTextAllRange(document) + } + + return lineRange })() - } catch (error: unknown) { - logger.error(`Could not extract error from row. - message: ${JSON.stringify(row.message)} - lineno: ${row.lineno} - location: ${location}`) - range = getTextAllRange(document) - } + errors.push({ level: row.level, range, message: row.message, }) diff --git a/server/src/services/validation.test.ts b/server/src/services/validation.test.ts index db3cc09..5045e40 100644 --- a/server/src/services/validation.test.ts +++ b/server/src/services/validation.test.ts @@ -68,6 +68,24 @@ describe("Validate Tests", () => { ]) }) + it("TRIGGER on inexistent field", async () => { + const diagnostics = await validateSampleFile( + "definitions/function/syntax_error_trigger_column_does_not_exist.pgsql", + ) + + expect(diagnostics).toStrictEqual([ + { + severity: DiagnosticSeverity.Error, + message: 'record "new" has no field "updated_at"', + range: Range.create(24, 0, 27, 47), + }, { + severity: DiagnosticSeverity.Error, + message: 'record "new" has no field "updated_at"', + range: Range.create(32, 0, 35, 47), + }, + ]) + }) + it("FUNCTION column does not exists", async () => { const diagnostics = await validateSampleFile( "definitions/function/syntax_error_function_column_does_not_exist.pgsql", diff --git a/server/src/utilities/text.ts b/server/src/utilities/text.ts index ce2880a..28802ae 100644 --- a/server/src/utilities/text.ts +++ b/server/src/utilities/text.ts @@ -88,7 +88,7 @@ export function getLineRangeFromBuffer( fileText: string, index: uinteger, offsetLine: uinteger = 0, ): Range | undefined { const textLines = Buffer.from(fileText) - .slice(0, index) + .subarray(0, index) .toString() .split("\n")