mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-02 02:34:42 +08:00
feat(editor): locate Oracle errors by message position with cursor anchor
This commit is contained in:
@@ -139,7 +139,7 @@ import { TABLE_FONT_SIZE_MAX, TABLE_FONT_SIZE_MIN, useSettingsStore, type DataGr
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { canCancelQueryExecution, isActiveResultLoading, queryExecutionLabelKey } from "@/lib/sql/queryExecutionState";
|
||||
import { sqlErrorEditorOffset, logSqlErrorPosition } from "@/lib/sql/errorPosition";
|
||||
import { sqlErrorDisplayPosition, sqlErrorEditorOffset, logSqlErrorPosition } from "@/lib/sql/errorPosition";
|
||||
import {
|
||||
databaseDisplayNameForTab,
|
||||
executionSummaryItems,
|
||||
@@ -349,8 +349,14 @@ const activeResultExecutionTarget = computed(() => queryStore.activeResultExecut
|
||||
const activeResultConnection = computed(() => (activeResultExecutionTarget.value ? connectionStore.getConfig(activeResultExecutionTarget.value.connectionId) : props.activeConnection));
|
||||
const activeResultConnectionId = computed(() => activeResultExecutionTarget.value?.connectionId ?? props.activeTab.connectionId);
|
||||
// Row/column locate only makes sense for SQL editor tabs: data/preview tabs have
|
||||
// no user statement to map the backend position onto.
|
||||
const activeResultErrorPosition = computed(() => (props.activeTab.mode === "query" ? props.activeTab.result?.error?.errorPosition : undefined));
|
||||
// no user statement to map the backend position onto. Engines without a typed
|
||||
// position (Oracle) report it in the error text, so the label falls back to the
|
||||
// same resolver the jump uses — the button never appears when clicking it could
|
||||
// not move the caret.
|
||||
const activeResultErrorPosition = computed(() => {
|
||||
if (props.activeTab.mode !== "query") return undefined;
|
||||
return sqlErrorDisplayPosition(activeResultErrorOffsetOptions());
|
||||
});
|
||||
const activeResultDatabase = computed(() => activeResultExecutionTarget.value?.database ?? props.activeTab.database);
|
||||
const activeResultSchema = computed(() => activeResultExecutionTarget.value?.schema ?? props.activeTab.schema);
|
||||
const activeEffectiveDatabaseType = computed(() => effectiveDatabaseTypeForConnection(activeResultConnection.value));
|
||||
@@ -1377,6 +1383,17 @@ function focusErrorPosition(offset: number): boolean {
|
||||
* to a cross-surface event when this surface only renders the shared result pane
|
||||
* (the editor lives in another group).
|
||||
*/
|
||||
function activeResultErrorOffsetOptions() {
|
||||
const result = props.activeTab.result;
|
||||
return {
|
||||
editorSql: props.activeTab.sql,
|
||||
result,
|
||||
resultIndex: result?.statement_index ?? props.activeTab.activeResultIndex,
|
||||
databaseType: activeEffectiveDatabaseType.value,
|
||||
parameterOptions: activeSqlStatementParameterOptions.value,
|
||||
};
|
||||
}
|
||||
|
||||
function locateActiveResultError() {
|
||||
const result = props.activeTab.result;
|
||||
logSqlErrorPosition("locate:invoke", {
|
||||
@@ -1390,13 +1407,7 @@ function locateActiveResultError() {
|
||||
editorLength: props.activeTab.sql.length,
|
||||
resultIsError: Boolean(result && isQueryExecutionErrorResult(result)),
|
||||
});
|
||||
const mapped = sqlErrorEditorOffset({
|
||||
editorSql: props.activeTab.sql,
|
||||
result,
|
||||
resultIndex: result?.statement_index ?? props.activeTab.activeResultIndex,
|
||||
databaseType: activeEffectiveDatabaseType.value,
|
||||
parameterOptions: activeSqlStatementParameterOptions.value,
|
||||
});
|
||||
const mapped = sqlErrorEditorOffset(activeResultErrorOffsetOptions());
|
||||
if (!mapped) {
|
||||
logSqlErrorPosition("locate:unavailable", {
|
||||
tabId: props.activeTab.id,
|
||||
|
||||
+73
-3
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { BackendError, SqlErrorPosition } from "@/lib/backend/errorUtils";
|
||||
import { mapExecutedOffsetToSource, scalarPositionToUtf16Offset, sqlErrorEditorOffset } from "@/lib/sql/errorPosition";
|
||||
import { mapExecutedOffsetToSource, scalarPositionToUtf16Offset, sqlErrorDisplayPosition, sqlErrorEditorOffset, sqlErrorHasMessagePosition } from "@/lib/sql/errorPosition";
|
||||
import type { QueryResult } from "@/types/database";
|
||||
|
||||
function backendError(position?: SqlErrorPosition): BackendError {
|
||||
@@ -17,11 +17,11 @@ function backendError(position?: SqlErrorPosition): BackendError {
|
||||
};
|
||||
}
|
||||
|
||||
function errorResult(options: { editorStatement?: string; sourceFrom?: number; sourceTo?: number; executedStatement?: string; position?: SqlErrorPosition }): QueryResult {
|
||||
function errorResult(options: { editorStatement?: string; sourceFrom?: number; sourceTo?: number; executedStatement?: string; position?: SqlErrorPosition; message?: string }): QueryResult {
|
||||
const statement = options.editorStatement ?? "";
|
||||
return {
|
||||
columns: ["Error"],
|
||||
rows: [["ERROR: relation does not exist"]],
|
||||
rows: [[options.message ?? "ERROR: relation does not exist"]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 0,
|
||||
execution_error: true,
|
||||
@@ -163,6 +163,76 @@ describe("sqlErrorEditorOffset", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the engine reports its position only in the error message", () => {
|
||||
it("resolves the Oracle Agent offset and labels it with the caret's row/column", () => {
|
||||
const sql = "SELECT nope_col\nFROM dual";
|
||||
const result = errorResult({
|
||||
editorStatement: sql,
|
||||
message: 'ORA-00904: "NOPE_COL": invalid identifier error occur at position: 7',
|
||||
});
|
||||
|
||||
expect(sqlErrorEditorOffset({ editorSql: sql, result })).toEqual({ offset: 7, line: 1, column: 8 });
|
||||
// The label must describe the caret, not the executed statement it was projected from.
|
||||
expect(sqlErrorDisplayPosition({ editorSql: sql, result })).toEqual({ line: 1, column: 8 });
|
||||
});
|
||||
|
||||
it("maps an offset on a later line to that line and column", () => {
|
||||
const sql = "SELECT 1\nFROM dual\nWHERE to_number('abc') = 1";
|
||||
const offset = sql.indexOf("'abc'");
|
||||
const result = errorResult({ editorStatement: sql, message: `ORA-01722: invalid number error occur at position: ${offset}` });
|
||||
|
||||
const mapped = sqlErrorEditorOffset({ editorSql: sql, result });
|
||||
// `'abc'` starts at column 17 of "WHERE to_number('abc') = 1".
|
||||
expect(mapped).toEqual({ offset, line: 3, column: 17 });
|
||||
expect(sqlErrorDisplayPosition({ editorSql: sql, result })).toEqual({ line: 3, column: 17 });
|
||||
});
|
||||
|
||||
it("projects a message offset back onto the source when DBX rewrote the statement", () => {
|
||||
const source = "SELECT * FROM users";
|
||||
const executed = 'SELECT *, "id" AS "__dbx_hidden_pk" FROM users LIMIT 100 OFFSET 0;';
|
||||
const result = errorResult({
|
||||
editorStatement: source,
|
||||
executedStatement: executed,
|
||||
message: `ORA-00942: table or view does not exist error occur at position: ${executed.indexOf("users")}`,
|
||||
});
|
||||
|
||||
const mapped = sqlErrorEditorOffset({ editorSql: source, result });
|
||||
expect(mapped?.offset).toBe(source.indexOf("users"));
|
||||
// The label follows the projected source text, so it stays consistent with the caret.
|
||||
expect(sqlErrorDisplayPosition({ editorSql: source, result })).toEqual({ line: 1, column: mapped!.offset + 1 });
|
||||
});
|
||||
|
||||
it("uses a textual location when the message carries one", () => {
|
||||
const sql = "SELECT 1\nFROM missing";
|
||||
const result = errorResult({ editorStatement: sql, message: "syntax error at line 2, column 6" });
|
||||
|
||||
expect(sqlErrorEditorOffset({ editorSql: sql, result })).toEqual({ offset: 14, line: 2, column: 6 });
|
||||
});
|
||||
|
||||
it("returns undefined when neither the envelope nor the message carries a position", () => {
|
||||
const sql = "SELECT nope_col FROM dual";
|
||||
const result = errorResult({ editorStatement: sql, message: 'ORA-00904: "NOPE_COL": invalid identifier' });
|
||||
|
||||
expect(sqlErrorEditorOffset({ editorSql: sql, result })).toBeUndefined();
|
||||
expect(sqlErrorDisplayPosition({ editorSql: sql, result })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("still returns undefined when the message offset cannot be found in the editor", () => {
|
||||
const result = errorResult({ editorStatement: "SELECT * FROM gone", message: "ORA-00942: table or view does not exist error occur at position: 14" });
|
||||
|
||||
expect(sqlErrorEditorOffset({ editorSql: "SELECT 1", result })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("sqlErrorHasMessagePosition", () => {
|
||||
it("detects the positions engines put in the message text", () => {
|
||||
expect(sqlErrorHasMessagePosition('ORA-00904: "X": invalid identifier error occur at position: 9')).toBe(true);
|
||||
expect(sqlErrorHasMessagePosition("syntax error at line 2, column 4")).toBe(true);
|
||||
expect(sqlErrorHasMessagePosition("ERROR 1054 (42S22): Unknown column 'nope_col' in 'field list'")).toBe(false);
|
||||
expect(sqlErrorHasMessagePosition("")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("scalarPositionToUtf16Offset", () => {
|
||||
it("converts scalar line/column to a UTF-16 offset", () => {
|
||||
expect(scalarPositionToUtf16Offset("SELECT '😀' FROM t", 1, 12)).toBe(12);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { DatabaseType, QueryResult } from "@/types/database";
|
||||
import type { SqlParameterOptions } from "@/lib/sql/sqlParameters";
|
||||
import { sqlErrorDecorationRange, sqlErrorMessagePosition } from "@/lib/sql/sqlDiagnostics";
|
||||
import { splitSqlStatementRanges } from "@/lib/sql/sqlStatementRanges";
|
||||
import { resultSourceRange } from "@/lib/tabs/tabPresentation";
|
||||
|
||||
@@ -12,6 +13,12 @@ export interface EditorErrorPosition {
|
||||
column: number;
|
||||
}
|
||||
|
||||
/** Row/column shown next to the error; always relative to the editor statement. */
|
||||
export interface SqlErrorDisplayPosition {
|
||||
line: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
export interface SqlErrorOffsetOptions {
|
||||
editorSql: string;
|
||||
result: QueryResult | undefined | null;
|
||||
@@ -86,6 +93,25 @@ function logDiagnostics(stage: string, options: SqlErrorOffsetOptions, extra: Re
|
||||
logSqlErrorPosition(stage, details);
|
||||
}
|
||||
|
||||
/**
|
||||
* The user-facing error text of a result, i.e. the same string the editor
|
||||
* underlines in red (see `QueryEditor`'s `executionError` prop).
|
||||
*/
|
||||
export function sqlErrorMessageText(result: QueryResult | undefined | null): string {
|
||||
return String(result?.rows?.[0]?.[0] ?? "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an error message names a position DBX can resolve.
|
||||
*
|
||||
* Used to decide whether a synthesized single-statement error result is worth
|
||||
* annotating with its source range: engines that report no typed position
|
||||
* (Oracle) still carry a parseable one in the message text.
|
||||
*/
|
||||
export function sqlErrorHasMessagePosition(message: string): boolean {
|
||||
return !!message && sqlErrorMessagePosition(message) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a backend-reported SQL error position into an absolute offset in the
|
||||
* current editor document.
|
||||
@@ -96,15 +122,36 @@ function logDiagnostics(stage: string, options: SqlErrorOffsetOptions, extra: Re
|
||||
* is first resolved inside `executedStatement` and then projected back onto the
|
||||
* user's `sourceStatement` before it is placed in the editor.
|
||||
*
|
||||
* Returns `undefined` only when the result carries no position, or when
|
||||
* {@link resultSourceRange} cannot prove the result still maps to the same
|
||||
* statement text in the editor (stale editor / different statement) — jumping
|
||||
* anywhere in that case would be wrong.
|
||||
* Falls back to the position carried by the error text when the backend reports
|
||||
* no typed one (Oracle: `error occur at position: N`), using the same parser the
|
||||
* editor's red underline uses, so both surfaces always agree.
|
||||
*
|
||||
* Returns `undefined` only when neither the envelope nor the error text carries a
|
||||
* position, or when {@link resultSourceRange} cannot prove the result still maps
|
||||
* to the same statement text in the editor (stale editor / different statement) —
|
||||
* jumping anywhere in that case would be wrong.
|
||||
*/
|
||||
export function sqlErrorEditorOffset(options: SqlErrorOffsetOptions): EditorErrorPosition | undefined {
|
||||
return resolveSqlErrorOffset(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* The row/column to show next to the error, using the driver-reported position
|
||||
* when the backend provides one and the position parsed from the error message
|
||||
* otherwise. Sharing {@link resolveSqlErrorOffset} with the jump means the
|
||||
* "locate error" button is only shown when clicking it can actually move the
|
||||
* caret.
|
||||
*/
|
||||
export function sqlErrorDisplayPosition(options: SqlErrorOffsetOptions): SqlErrorDisplayPosition | undefined {
|
||||
const resolved = resolveSqlErrorOffset(options);
|
||||
return resolved ? { line: resolved.line, column: resolved.column } : undefined;
|
||||
}
|
||||
|
||||
function resolveSqlErrorOffset(options: SqlErrorOffsetOptions): EditorErrorPosition | undefined {
|
||||
const position = options.result?.error?.errorPosition;
|
||||
if (!position) {
|
||||
if (isSqlErrorPositionDebugEnabled()) logDiagnostics("skip:no-error-position", options, {});
|
||||
const messageText = position ? "" : sqlErrorMessageText(options.result);
|
||||
if (!position && !sqlErrorHasMessagePosition(messageText)) {
|
||||
if (isSqlErrorPositionDebugEnabled()) logDiagnostics("skip:no-error-position", options, { hasMessagePosition: false });
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -125,18 +172,36 @@ export function sqlErrorEditorOffset(options: SqlErrorOffsetOptions): EditorErro
|
||||
const executedStatement = options.result?.executedStatement;
|
||||
const positionBasis = executedStatement ?? range.sql;
|
||||
|
||||
// Walk the line/column in the text the position is relative to (scalar values,
|
||||
// matching PostgreSQL's character-based cursor), then convert to UTF-16.
|
||||
const basisOffset = scalarPositionToUtf16Offset(positionBasis, position.line, position.column);
|
||||
let basisOffset: number;
|
||||
if (position) {
|
||||
// Walk the line/column in the text the position is relative to (scalar
|
||||
// values, matching PostgreSQL's character-based cursor), then convert to UTF-16.
|
||||
basisOffset = scalarPositionToUtf16Offset(positionBasis, position.line, position.column);
|
||||
} else {
|
||||
// Engines without a typed position report it inside the message text
|
||||
// (Oracle's Agent offset, `LINE n:` carets). Parse against the same basis the
|
||||
// typed position would use, and reuse the exact range the editor already
|
||||
// underlines in red so both surfaces agree.
|
||||
const derived = sqlErrorDecorationRange(positionBasis, messageText);
|
||||
if (!derived) {
|
||||
if (isSqlErrorPositionDebugEnabled()) logDiagnostics("unresolved:message-position", options, { hasMessagePosition: true });
|
||||
return undefined;
|
||||
}
|
||||
basisOffset = Math.min(derived.from, positionBasis.length);
|
||||
}
|
||||
const drifted = Boolean(executedStatement && executedStatement !== range.sql);
|
||||
const sourceOffset = drifted ? mapExecutedOffsetToSource(executedStatement!, range.sql, basisOffset) : basisOffset;
|
||||
|
||||
// Clamp inside the resolved statement range so a residual mismatch can never
|
||||
// place the caret outside the statement it belongs to.
|
||||
const editorOffset = Math.max(range.from, Math.min(range.from + sourceOffset, range.to));
|
||||
// Report the row/column of the *source* statement, so the label always points
|
||||
// at the same character the caret lands on (the position itself is relative to
|
||||
// the executed statement, which DBX may have rewritten).
|
||||
const display = utf16OffsetToScalarPosition(range.sql, clamp(sourceOffset, 0, range.sql.length));
|
||||
if (isSqlErrorPositionDebugEnabled()) {
|
||||
logDiagnostics("resolved", options, {
|
||||
position,
|
||||
position: position ?? { derivedFromMessage: true },
|
||||
rangeFrom: range.from,
|
||||
rangeTo: range.to,
|
||||
rangeSql: previewText(range.sql),
|
||||
@@ -145,9 +210,36 @@ export function sqlErrorEditorOffset(options: SqlErrorOffsetOptions): EditorErro
|
||||
basisOffset,
|
||||
sourceOffset,
|
||||
editorOffset,
|
||||
display,
|
||||
});
|
||||
}
|
||||
return { offset: editorOffset, line: position.line, column: position.column };
|
||||
return { offset: editorOffset, line: display.line, column: display.column };
|
||||
}
|
||||
|
||||
/**
|
||||
* Inverse of {@link scalarPositionToUtf16Offset}: the 1-based line and column
|
||||
* (counted in Unicode scalar values) of a UTF-16 offset. Offsets past the end
|
||||
* clamp to the last character.
|
||||
*/
|
||||
export function utf16OffsetToScalarPosition(text: string, offset: number): { line: number; column: number } {
|
||||
const characters = Array.from(text);
|
||||
if (characters.length === 0) return { line: 1, column: 1 };
|
||||
const target = Math.max(0, Math.min(Math.floor(offset), text.length - 1));
|
||||
let consumed = 0;
|
||||
let line = 1;
|
||||
let column = 1;
|
||||
for (const character of characters) {
|
||||
const width = character.length;
|
||||
if (consumed >= target) break;
|
||||
consumed += width;
|
||||
if (character === "\n") {
|
||||
line += 1;
|
||||
column = 1;
|
||||
} else {
|
||||
column += 1;
|
||||
}
|
||||
}
|
||||
return { line, column };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -104,20 +104,47 @@ function oracleInvalidIdentifierRange(sql: string, message: string, position: nu
|
||||
return bestRange;
|
||||
}
|
||||
|
||||
export function sqlErrorDecorationRange(sql: string, message: string): SqlErrorRange | null {
|
||||
/**
|
||||
* A position a driver reported in its error text, without needing the SQL it
|
||||
* refers to. Engines that do not attach a typed position to their error
|
||||
* envelope (Oracle) still name one in the message.
|
||||
*/
|
||||
export type SqlErrorMessagePosition = { kind: "location"; location: SqlErrorLocation } | { kind: "offset"; offset: number };
|
||||
|
||||
const ORACLE_AGENT_POSITION_PATTERN = /\berror\s+occur(?:red)?\s+at\s+position\s*:\s*(\d+)\b/i;
|
||||
|
||||
/**
|
||||
* Parse the position carried by a driver error message.
|
||||
*
|
||||
* Textual locations (`line N, column M`, `LINE N:` with a `^` caret, and the
|
||||
* line-only form MySQL emits) win over the Oracle Agent's absolute offset, the
|
||||
* same precedence {@link sqlErrorDecorationRange} has always used.
|
||||
*/
|
||||
export function sqlErrorMessagePosition(message: string): SqlErrorMessagePosition | null {
|
||||
const location = parseSqlErrorLocation(message);
|
||||
if (location) {
|
||||
const offset = lineColumnToOffset(sql, location);
|
||||
if (location) return { kind: "location", location };
|
||||
|
||||
// Oracle Agent reports a zero-based absolute offset into the statement text.
|
||||
const positionMatch = ORACLE_AGENT_POSITION_PATTERN.exec(message);
|
||||
if (!positionMatch?.[1]) return null;
|
||||
const offset = Number.parseInt(positionMatch[1], 10);
|
||||
if (!Number.isSafeInteger(offset) || offset < 0) return null;
|
||||
return { kind: "offset", offset };
|
||||
}
|
||||
|
||||
export function sqlErrorDecorationRange(sql: string, message: string): SqlErrorRange | null {
|
||||
const position = sqlErrorMessagePosition(message);
|
||||
if (!position) return null;
|
||||
|
||||
if (position.kind === "location") {
|
||||
const offset = lineColumnToOffset(sql, position.location);
|
||||
if (offset == null || offset >= sql.length) return null;
|
||||
return { from: offset, to: offset + 1 };
|
||||
}
|
||||
|
||||
// Oracle Agent reports a zero-based absolute offset. For qualified invalid
|
||||
// identifiers it can point later in the selector, so prefer the named token.
|
||||
const positionMatch = /\berror\s+occur(?:red)?\s+at\s+position\s*:\s*(\d+)\b/i.exec(message);
|
||||
if (!positionMatch?.[1]) return null;
|
||||
const position = Number.parseInt(positionMatch[1], 10);
|
||||
if (!Number.isSafeInteger(position) || position < 0 || position >= sql.length) return null;
|
||||
|
||||
return oracleInvalidIdentifierRange(sql, message, position) ?? { from: position, to: position + 1 };
|
||||
const { offset } = position;
|
||||
if (offset >= sql.length) return null;
|
||||
// For qualified invalid identifiers the offset can point later in the
|
||||
// selector, so prefer the named token.
|
||||
return oracleInvalidIdentifierRange(sql, message, offset) ?? { from: offset, to: offset + 1 };
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { DeletedConnectionTabKeepMode } from "@/lib/tabs/deletedConnectionT
|
||||
import type { BatchSqlExecution, ConnectionConfig, DatabaseType, IndexInfo, NacosConfigEditorViewport, ObjectBrowserFilter, ObjectBrowserViewport, ObjectSource, ObjectSourceKind, QueryResult, QueryResultSourceColumnRef, QueryTab, TableInfoTab, TableStructureEditorTarget } from "@/types/database";
|
||||
import { orderPinnedFirst } from "@/lib/app/pinnedItems";
|
||||
import { canCancelQueryExecution } from "@/lib/sql/queryExecutionState";
|
||||
import { isSqlErrorPositionDebugEnabled, logSqlErrorPosition } from "@/lib/sql/errorPosition";
|
||||
import { isSqlErrorPositionDebugEnabled, logSqlErrorPosition, sqlErrorHasMessagePosition, sqlErrorMessageText } from "@/lib/sql/errorPosition";
|
||||
import { buildExplainSql, parseExplainResult, parseDamengExplainText, parseOracleExplainText, sqlServerExplainResult, type BuildExplainSqlResult, type ExplainPlanDatabaseType } from "@/lib/diagram/explainPlan";
|
||||
import { mysqlExplainCompatibilityHint } from "@/lib/diagram/mysqlExplainCompatibility";
|
||||
import { allEditableColumnsWriteable, allPrimaryKeysPresent, analyzeEditableQueryEditability, analyzeSelectStructureForDisplay, resolveMetadataColumnName, resolveSourceColumnsByOrdinal, sourceColumnsForResult, type EditableQueryInfo, type EditableQuerySource } from "@/lib/sql/sqlAnalysis";
|
||||
@@ -425,11 +425,12 @@ function annotateQueryResultSources(
|
||||
* The core returns per-statement error results for batches, but a
|
||||
* single-statement failure aborts the whole execute-multi command, leaving the
|
||||
* frontend to synthesize the error result here. Only annotated when the error
|
||||
* actually carries a position and the submission is a single statement (a
|
||||
* multi-statement thrown error's position cannot be attributed to one statement).
|
||||
* actually carries a position — either typed (PostgreSQL) or parseable from the
|
||||
* message text (Oracle's Agent offset) — and the submission is a single statement
|
||||
* (a multi-statement thrown error's position cannot be attributed to one statement).
|
||||
*/
|
||||
function annotateSingleStatementErrorResult(errorResult: QueryResult, sourceSql: string, databaseType: DatabaseType | undefined, sourceOffset: number | undefined, parameterOptions: SqlParameterOptions | undefined, executedSql: string | undefined): void {
|
||||
if (!errorResult.error?.errorPosition) return;
|
||||
if (!errorResult.error?.errorPosition && !sqlErrorHasMessagePosition(sqlErrorMessageText(errorResult))) return;
|
||||
if (splitSqlStatementRanges(sourceSql, databaseType, parameterOptions).length !== 1) return;
|
||||
annotateQueryResultSources([errorResult], sourceSql, undefined, databaseType, sourceOffset, parameterOptions, executedSql);
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
+17
-1
@@ -1,6 +1,6 @@
|
||||
import { strict as assert } from "node:assert";
|
||||
import { test } from "vitest";
|
||||
import { parseSqlErrorLocation, sqlErrorDecorationRange, sqlErrorSqlMatchesEditor } from "../../apps/desktop/src/lib/sql/sqlDiagnostics.ts";
|
||||
import { parseSqlErrorLocation, sqlErrorDecorationRange, sqlErrorMessagePosition, sqlErrorSqlMatchesEditor } from "../../apps/desktop/src/lib/sql/sqlDiagnostics.ts";
|
||||
|
||||
test("locates the Oracle invalid identifier reported by the Agent", () => {
|
||||
const sql = "select x.* from si_price_adjust_task t;";
|
||||
@@ -49,6 +49,22 @@ test("keeps existing line-column and PostgreSQL caret parsing", () => {
|
||||
assert.deepEqual(sqlErrorDecorationRange("SELECT 1\nFROM bad", "syntax error at line 2, column 2"), { from: 10, to: 11 });
|
||||
});
|
||||
|
||||
test("parses the Oracle Agent offset without needing the SQL it refers to", () => {
|
||||
assert.deepEqual(sqlErrorMessagePosition('ORA-00904: "NOPE_COL": invalid identifier error occur at position: 2270'), { kind: "offset", offset: 2270 });
|
||||
assert.deepEqual(sqlErrorMessagePosition("error occurred at position: 0"), { kind: "offset", offset: 0 });
|
||||
});
|
||||
|
||||
test("prefers a textual location over the Oracle Agent offset", () => {
|
||||
assert.deepEqual(sqlErrorMessagePosition("syntax error at line 2, column 4 error occur at position: 9"), { kind: "location", location: { line: 1, column: 3 } });
|
||||
assert.deepEqual(sqlErrorMessagePosition('ERROR: column "bad" does not exist\nLINE 3: SELECT bad\n ^'), { kind: "location", location: { line: 2, column: 15 } });
|
||||
});
|
||||
|
||||
test("returns null when the message carries no usable position", () => {
|
||||
assert.equal(sqlErrorMessagePosition("ERROR 1054 (42S22): Unknown column 'nope_col' in 'field list'"), null);
|
||||
assert.equal(sqlErrorMessagePosition("error occur at position: nope"), null);
|
||||
assert.equal(sqlErrorMessagePosition('ORA-00904: "X": invalid identifier'), null);
|
||||
});
|
||||
|
||||
test("matches DBX pagination rewrites without accepting unrelated stale SQL", () => {
|
||||
const editorSql = "SELECT *\nFROM projects\nWHERE ;";
|
||||
const executedSql = "SELECT *\nFROM projects\nWHERE LIMIT 100;";
|
||||
|
||||
Reference in New Issue
Block a user