Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4f472d2
initial warning api support
noeldevelops Feb 3, 2026
e0ed4f8
combine all details, improve css
noeldevelops Feb 3, 2026
b2ea5b1
no badge
noeldevelops Feb 3, 2026
276e15d
simplify, reorder infos
noeldevelops Feb 3, 2026
13e6519
more css tweaks
noeldevelops Feb 3, 2026
c864e02
strip warnings from detail if they are provided by API
noeldevelops Feb 4, 2026
7b459f4
more obvious severity, reason, timestamp
noeldevelops Feb 4, 2026
ed2b348
make errors match new warnings
noeldevelops Feb 4, 2026
ffaa96f
DRYer model for messages
noeldevelops Feb 4, 2026
1e03e58
prettier format fix
noeldevelops Feb 25, 2026
3a5c943
add TODO for type assertion on status warnings
noeldevelops Feb 25, 2026
d30e97d
remove unused warning signals
noeldevelops Feb 25, 2026
0225b90
combine duplicate CSS selectors for error and critical severity
noeldevelops Feb 25, 2026
2d6e7be
inline detailText signal into detailItems
noeldevelops Feb 25, 2026
22ba7e8
remove text shrink
noeldevelops Mar 3, 2026
1a0c5e1
updates to use api types
noeldevelops Mar 3, 2026
c79df89
severity is a generic string
noeldevelops Mar 3, 2026
f1dd7bb
type fix, handle as if string | date
noeldevelops Mar 3, 2026
997027c
make StatementWarning properties readonly for API type compatibility
noeldevelops Mar 5, 2026
3ddbb44
use data-text instead of data-html for detail messages to prevent inj…
noeldevelops Mar 5, 2026
e1cec46
move data-testid to detail-message element for e2e test compatibility
noeldevelops Mar 5, 2026
ab708d6
fix self-review items: remove stale <br> replace, DRY regex, better d…
noeldevelops Mar 5, 2026
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
1 change: 1 addition & 0 deletions src/flinkSql/flinkStatementResultsManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ describe("FlinkStatementResultsViewModel and FlinkStatementResultsManager", () =
areResultsViewable: ctx.statement.canRequestResults,
possiblyViewable: ctx.statement.possiblyViewable,
isForeground: ctx.statement.isForeground,
warnings: ctx.statement.warnings,
});
});

Expand Down
3 changes: 3 additions & 0 deletions src/flinkSql/flinkStatementResultsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { ViewMode } from "./flinkStatementResultColumns";
import type { StatementResultsRow } from "./flinkStatementResults";
import { parseResults } from "./flinkStatementResults";
import { extractPageToken } from "./utils";
import type { StatementWarning } from "./warningParser";

const logger = new Logger("flink-statement-results");

Expand Down Expand Up @@ -85,6 +86,7 @@ export type PostFunction = {
failed: boolean;
areResultsViewable: boolean;
isForeground: boolean;
warnings: StatementWarning[];
}>;
(type: "StopStatement", body: { timestamp?: number }): Promise<null>;
(type: "ViewStatementSource", body: { timestamp?: number }): Promise<null>;
Expand Down Expand Up @@ -493,6 +495,7 @@ export class FlinkStatementResultsManager {
areResultsViewable: this.statement.canRequestResults,
possiblyViewable: this.statement.possiblyViewable,
isForeground: this.statement.isForeground,
warnings: this.statement.warnings,
};
}
case "StopStatement": {
Expand Down
147 changes: 147 additions & 0 deletions src/flinkSql/warningParser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import assert from "assert";
import {
extractWarnings,
parseLegacyWarnings,
stripWarningsFromDetail,
type StatementWarning,
} from "./warningParser";

describe("flinkSql/warningParser", () => {
describe("parseLegacyWarnings", () => {
it("should return empty array for undefined input", () => {
const result = parseLegacyWarnings(undefined);
assert.deepStrictEqual(result, []);
});

it("should return empty array for empty string", () => {
const result = parseLegacyWarnings("");
assert.deepStrictEqual(result, []);
});

it("should return empty array when no [Warning] markers present", () => {
const result = parseLegacyWarnings("Statement is running successfully.");
assert.deepStrictEqual(result, []);
});

it("should parse a single legacy warning", () => {
const result = parseLegacyWarnings(
"[Warning] The primary key does not match the upsert key.",
);

assert.strictEqual(result.length, 1);
assert.strictEqual(result[0].severity, "MODERATE");
assert.strictEqual(result[0].reason, "");
assert.strictEqual(result[0].message, "The primary key does not match the upsert key.");
// created_at is null for legacy warnings (no timestamp available)
assert.strictEqual(result[0].created_at, null);
});

it("should parse multiple legacy warnings", () => {
const detail =
"[Warning] First warning message. [Warning] Second warning message with more details.";
const result = parseLegacyWarnings(detail);

assert.strictEqual(result.length, 2);
assert.strictEqual(result[0].message, "First warning message.");
assert.strictEqual(result[1].message, "Second warning message with more details.");
});

it("should handle real-world legacy warning format", () => {
const detail =
"[Warning] The primary key does not match the upsert key derived from the query. " +
"If the primary key and upsert key don't match, the system needs to add a state-intensive " +
"operation for correction. [Warning] Your query includes one or more highly state-intensive " +
"operators but does not set a time-to-live (TTL) value.";

const result = parseLegacyWarnings(detail);

assert.strictEqual(result.length, 2);
assert.ok(result[0].message.includes("primary key does not match"));
assert.ok(result[1].message.includes("state-intensive operators"));
});

it("should handle case-insensitive [Warning] markers", () => {
const result = parseLegacyWarnings("[warning] lowercase warning message.");

assert.strictEqual(result.length, 1);
assert.strictEqual(result[0].message, "lowercase warning message.");
});

it("should handle brackets within warning messages", () => {
const detail =
"[Warning] Please revisit the query (upsert key: [customer_name]) or provide a primary key.";
const result = parseLegacyWarnings(detail);

assert.strictEqual(result.length, 1);
assert.ok(result[0].message.includes("[customer_name]"));
});
});

describe("extractWarnings", () => {
it("should return empty array when both inputs are undefined", () => {
const result = extractWarnings(undefined, undefined);
assert.deepStrictEqual(result, []);
});

it("should prefer structured warnings when available", () => {
const apiWarnings: StatementWarning[] = [
{
severity: "CRITICAL",
created_at: new Date("2025-11-14T16:01:00Z"),
reason: "UPSERT_PRIMARY_KEY_MISMATCH",
message: "API warning message",
},
];
const detail = "[Warning] Legacy warning message.";

const result = extractWarnings(apiWarnings, detail);

assert.strictEqual(result.length, 1);
assert.strictEqual(result[0].message, "API warning message");
assert.strictEqual(result[0].severity, "CRITICAL");
});

it("should fall back to legacy parsing when structured warnings empty", () => {
const result = extractWarnings([], "[Warning] Legacy warning.");

assert.strictEqual(result.length, 1);
assert.strictEqual(result[0].message, "Legacy warning.");
});

it("should fall back to legacy parsing when structured warnings undefined", () => {
const result = extractWarnings(undefined, "[Warning] Legacy warning.");

assert.strictEqual(result.length, 1);
assert.strictEqual(result[0].message, "Legacy warning.");
});

it("should return empty array when detail has no warnings", () => {
const result = extractWarnings(undefined, "Statement running successfully.");
assert.deepStrictEqual(result, []);
});
});

describe("stripWarningsFromDetail", () => {
it("should return null for undefined input", () => {
const result = stripWarningsFromDetail(undefined);
assert.strictEqual(result, null);
});

it("should return null when detail contains only warnings", () => {
const result = stripWarningsFromDetail("[Warning] First. [Warning] Second.");
assert.strictEqual(result, null);
});

it("should preserve non-warning content before warnings", () => {
const detail = "Statement running. [Warning] Some warning message.";
const result = stripWarningsFromDetail(detail);
assert.strictEqual(result, "Statement running.");
});

it("should handle detail with no warnings", () => {
const detail = "Statement completed successfully.";
const result = stripWarningsFromDetail(detail);
assert.strictEqual(result, "Statement completed successfully.");
});
});
});
79 changes: 79 additions & 0 deletions src/flinkSql/warningParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Warning parsing utilities for Flink statements.
* Handles both new structured API warnings and legacy [Warning] format in detail strings.
*/

/** Regex matching `[Warning] message` blocks in legacy detail strings. */
const LEGACY_WARNING_PATTERN = /\[Warning\]\s*([\s\S]*?)(?=\s*\[Warning\]|$)/gi;

Check failure on line 7 in src/flinkSql/warningParser.ts

View check run for this annotation

SonarQube-Confluent / SonarQube Code Analysis

Make sure the regex used here, which is vulnerable to super-linear runtime due to backtracking, cannot lead to denial of service.

[S5852] Using slow regular expressions is security-sensitive See more on https://sonarqube.confluent.io/project/issues?id=vscode&pullRequest=3254&issues=991abf2a-bddd-4f91-8503-4d9eeb6c8180&open=991abf2a-bddd-4f91-8503-4d9eeb6c8180

/** API warning shape from the new structured warnings field */
export interface StatementWarning {
readonly severity: string;
readonly created_at: Date | null;
readonly reason: string;
readonly message: string;
}

/**
* Parse legacy [Warning] format from detail string.
* Legacy format: "[Warning] message. [Warning] another message."
* @param detail The detail string that may contain legacy warnings
* @returns Array of parsed StatementWarning objects
*/
export function parseLegacyWarnings(detail: string | undefined): StatementWarning[] {
if (!detail) {
return [];
}

const warnings: StatementWarning[] = [];
LEGACY_WARNING_PATTERN.lastIndex = 0;
let match: RegExpExecArray | null;

while ((match = LEGACY_WARNING_PATTERN.exec(detail)) !== null) {
const message = match[1].trim();
if (message) {
warnings.push({
severity: "MODERATE",
created_at: null, // No timestamp in legacy format
reason: "", // No reason in legacy format
message,
});
}
}

return warnings;
}

/**
* Extract warnings from API response, preferring structured warnings over legacy parsing.
* @param warnings The structured warnings array from the API (may be undefined)
* @param detail The detail string that may contain legacy warnings
* @returns Array of parsed StatementWarning objects
*/
export function extractWarnings(
warnings: StatementWarning[] | undefined,
detail: string | undefined,
): StatementWarning[] {
// Prefer structured warnings if available
if (warnings && warnings.length > 0) {
return warnings;
}

// Fall back to legacy parsing
return parseLegacyWarnings(detail);
}

/**
* Strip [Warning] sections from a detail string.
* Used to avoid duplication when API warnings are displayed separately.
* @param detail The detail string that may contain legacy warnings
* @returns The detail string with [Warning] sections removed, or null if only warnings remain
*/
export function stripWarningsFromDetail(detail: string | undefined): string | null {
if (!detail) {
return null;
}
LEGACY_WARNING_PATTERN.lastIndex = 0;
const stripped = detail.replace(LEGACY_WARNING_PATTERN, "").trim();
return stripped.length > 0 ? stripped : null;
}
11 changes: 11 additions & 0 deletions src/models/flinkStatement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import type {
} from "../clients/flinkSql";
import { ConnectionType } from "../clients/sidecar";
import { CCLOUD_BASE_PATH, CCLOUD_CONNECTION_ID, UTM_SOURCE_VSCODE } from "../constants";
import type { StatementWarning } from "../flinkSql/warningParser";
import { extractWarnings } from "../flinkSql/warningParser";
import { IconNames } from "../icons";
import type { IdItem } from "./main";
import { CustomMarkdownString } from "./main";
Expand Down Expand Up @@ -266,6 +268,15 @@ export class FlinkStatement implements IResourceBase, IdItem, ISearchable, IEnvP
return this.status?.detail;
}

/**
* Get warnings for this statement.
* Prefers structured API warnings, falls back to parsing legacy [Warning] format from detail.
*/
get warnings(): StatementWarning[] {
const statusWarnings = this.status.warnings;
return extractWarnings(statusWarnings, this.detail);
}

get startTime(): Date | undefined {
return this.metadata?.created_at;
}
Expand Down
Loading