Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 frontend/src/__mocks__/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const MockRequestClient = {
previewSQLTable: vi.fn().mockResolvedValue({}),
previewSQLTableList: vi.fn().mockResolvedValue({ tables: [] }),
previewDataSourceConnection: vi.fn().mockResolvedValue({}),
validateSQL: vi.fn().mockResolvedValue({}),
openFile: vi.fn().mockResolvedValue({}),
getUsageStats: vi.fn().mockResolvedValue({}),
sendPdb: vi.fn().mockResolvedValue({}),
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/editor/Cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import { useDeleteCellCallback } from "./cell/useDeleteCell";
import { useRunCell } from "./cell/useRunCells";
import { HideCodeButton } from "./code/readonly-python-code";
import { cellDomProps } from "./common";
import { SqlValidationErrorBanner } from "./errors/sql-validation-errors";
import { useCellNavigationProps } from "./navigation/navigation";
import {
useTemporarilyShownCode,
Expand Down Expand Up @@ -653,6 +654,7 @@ const EditableCellComponent = ({
)}
</div>
</div>
<SqlValidationErrorBanner cellId={cellId} />
{cellOutput === "below" && outputArea}
{cellRuntime.serialization && (
<div className="py-1 px-2 flex items-center justify-end gap-2 last:rounded-b">
Expand Down
34 changes: 34 additions & 0 deletions frontend/src/components/editor/errors/sql-validation-errors.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* Copyright 2024 Marimo. All rights reserved. */

import { AlertCircleIcon } from "lucide-react";
import type { CellId } from "@/core/cells/ids";
import { useSqlValidationErrorsForCell } from "@/core/codemirror/language/languages/sql/validation-errors";

export const SqlValidationErrorBanner = ({ cellId }: { cellId: CellId }) => {
const error = useSqlValidationErrorsForCell(cellId);

if (!error) {
return;
}

return (
<div className="p-3 text-sm flex flex-col text-muted-foreground gap-1.5 bg-destructive/5">
<div className="flex items-start gap-1.5">
<AlertCircleIcon size={13} className="mt-[3px] text-destructive" />
<p>
<span className="font-bold text-destructive">{error.errorType}:</span>{" "}
{error.errorMessage}
</p>
</div>

{error.codeblock && (
<pre
lang="sql"
className="text-xs bg-muted rounded p-2 pb-0 mx-3 overflow-x-auto font-mono whitespace-pre-wrap"
>
{error.codeblock}
</pre>
)}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
languageAdapterState,
switchLanguage,
} from "../extension";
import { exportedForTesting as sqlValidationErrorsForTesting } from "../languages/sql/validation-errors";
import { languageMetadataField } from "../metadata";

let view: EditorView | null = null;
Expand Down Expand Up @@ -258,3 +259,26 @@ describe("switchLanguage", () => {
});
});
});

describe("sqlValidationErrors", () => {
const { splitErrorMessage } = sqlValidationErrorsForTesting;

describe("split error message", () => {
it("should split the error message into error type and error message", () => {
const error = "SyntaxError: SELECT * FROM df";
const { errorType, errorMessage } = splitErrorMessage(error);
expect(errorType).toBe("SyntaxError");
expect(errorMessage).toBe("SELECT * FROM df");
});

it("should handle multiple colons", () => {
const error =
"SyntaxError: SELECT * FROM df:SyntaxError: SELECT * FROM df";
const { errorType, errorMessage } = splitErrorMessage(error);
expect(errorType).toBe("SyntaxError");
expect(errorMessage).toBe(
"SELECT * FROM df:SyntaxError: SELECT * FROM df",
);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/* Copyright 2024 Marimo. All rights reserved. */

import { describe, expect, it } from "vitest";
import { exportedForTesting } from "../languages/sql/validation-errors";

describe("Error Message Splitting", () => {
it("should handle error message splitting correctly", () => {
const { splitErrorMessage } = exportedForTesting;

const result1 = splitErrorMessage("Syntax error: unexpected token");
expect(result1.errorType).toBe("Syntax error");
expect(result1.errorMessage).toBe("unexpected token");

const result2 = splitErrorMessage("Multiple: colons: in error");
expect(result2.errorType).toBe("Multiple");
expect(result2.errorMessage).toBe("colons: in error");

const result3 = splitErrorMessage("No colon error");
expect(result3.errorType).toBe("No colon error");
expect(result3.errorMessage).toBe("");
});
});

describe("DuckDB Error Handling", () => {
it("should extract codeblock from error with LINE information", () => {
const { handleDuckdbError } = exportedForTesting;

const error =
'Binder Error: Referenced column "attacks" not found in FROM clause! Candidate bindings: "Attack", "Total" LINE 1:... from pokemon WHERE \'type_2\' = 32 and attack = 32 and not attacks = \'hi\' ^';

const result = handleDuckdbError(error);

expect(result.errorType).toBe("Binder Error");
expect(result.errorMessage).toBe(
'Referenced column "attacks" not found in FROM clause! Candidate bindings: "Attack", "Total"',
);
expect(result.codeblock).toBe(
"LINE 1:... from pokemon WHERE 'type_2' = 32 and attack = 32 and not attacks = 'hi' ^",
);
});

it("should handle error without LINE information", () => {
const { handleDuckdbError } = exportedForTesting;

const error = "Syntax Error: Invalid syntax near WHERE";

const result = handleDuckdbError(error);

expect(result.errorType).toBe("Syntax Error");
expect(result.errorMessage).toBe("Invalid syntax near WHERE");
expect(result.codeblock).toBeUndefined();
});

it("should handle error with LINE at the beginning", () => {
const { handleDuckdbError } = exportedForTesting;

const error = "LINE 1: SELECT * FROM table WHERE invalid_column = 1 ^";

const result = handleDuckdbError(error);

expect(result.errorType).toBe("LINE 1");
expect(result.errorMessage).toBe(
"SELECT * FROM table WHERE invalid_column = 1 ^",
);
expect(result.codeblock).toBeUndefined();
});

it("should handle error with multiple LINE occurrences", () => {
const { handleDuckdbError } = exportedForTesting;

const error =
"Error: Something went wrong LINE 1: SELECT * FROM table WHERE invalid_column = 1 ^";

const result = handleDuckdbError(error);

expect(result.errorType).toBe("Error");
expect(result.errorMessage).toBe("Something went wrong");
expect(result.codeblock).toBe(
"LINE 1: SELECT * FROM table WHERE invalid_column = 1 ^",
);
});

it("should handle complex error with nested quotes", () => {
const { handleDuckdbError } = exportedForTesting;

const error =
"Binder Error: Column \"name\" not found! LINE 1: SELECT * FROM users WHERE name = 'John' AND age > 25 ^";

const result = handleDuckdbError(error);

expect(result.errorType).toBe("Binder Error");
expect(result.errorMessage).toBe('Column "name" not found!');
expect(result.codeblock).toBe(
"LINE 1: SELECT * FROM users WHERE name = 'John' AND age > 25 ^",
);
});

it("should handle error with LINE but no caret", () => {
const { handleDuckdbError } = exportedForTesting;

const error = "Error: Invalid query LINE 1: SELECT * FROM table";

const result = handleDuckdbError(error);

expect(result.errorType).toBe("Error");
expect(result.errorMessage).toBe("Invalid query");
expect(result.codeblock).toBe("LINE 1: SELECT * FROM table");
});

it("should trim whitespace from codeblock", () => {
const { handleDuckdbError } = exportedForTesting;

const error = "Error: Something wrong LINE 1: SELECT * FROM table ^ ";

const result = handleDuckdbError(error);

expect(result.errorType).toBe("Error");
expect(result.errorMessage).toBe("Something wrong");
expect(result.codeblock).toBe("LINE 1: SELECT * FROM table ^");
});

it("should handle empty error message", () => {
const { handleDuckdbError } = exportedForTesting;

const error = "";

const result = handleDuckdbError(error);

expect(result.errorType).toBe("");
expect(result.errorMessage).toBe("");
expect(result.codeblock).toBeUndefined();
});
});
20 changes: 20 additions & 0 deletions frontend/src/core/codemirror/language/languages/sql/sql-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* Copyright 2024 Marimo. All rights reserved. */

import { useAtom } from "jotai";
import { atomWithStorage } from "jotai/utils";
import { store } from "@/core/state/jotai";

const BASE_KEY = "marimo:notebook-sql-mode";

export type SQLMode = "validate" | "default";

const sqlModeAtom = atomWithStorage<SQLMode>(BASE_KEY, "default");

export function useSQLMode() {
const [sqlMode, setSQLMode] = useAtom(sqlModeAtom);
return { sqlMode, setSQLMode };
}

export function getSQLMode() {
return store.get(sqlModeAtom);
}
Loading
Loading