Skip to content

Commit e2e1556

Browse files
feat: Wrap and pass SQL Errors (#6498)
## 📝 Summary Starting point to format SQL errors in a bit more friendly manner. @Light2Dark let me know if you want to take over this branch, or create a PR against it. <img width="838" height="298" alt="image" src="https://github.com/user-attachments/assets/7c51e7d6-8339-4a6a-b26b-dc8068a938da" /> Not sure what's up with my codegen? I can cherry pick the commit if it's a little too noisy. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 0520399 commit e2e1556

File tree

12 files changed

+1785
-320
lines changed

12 files changed

+1785
-320
lines changed

frontend/src/components/editor/output/MarimoErrorOutput.tsx

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
/* Copyright 2024 Marimo. All rights reserved. */
22

3-
import { NotebookPenIcon, SquareArrowOutUpRightIcon } from "lucide-react";
3+
import {
4+
InfoIcon,
5+
NotebookPenIcon,
6+
SquareArrowOutUpRightIcon,
7+
} from "lucide-react";
48
import { Fragment, type JSX } from "react";
59
import {
610
Accordion,
@@ -72,6 +76,8 @@ export const MarimoErrorOutput = ({
7276
titleContents = "Ancestor stopped";
7377
alertVariant = "default";
7478
titleColor = "text-secondary-foreground";
79+
} else if (errors.some((e) => e.type === "sql-error")) {
80+
titleContents = "SQL Error in statement";
7581
} else {
7682
// Check for exception type
7783
const exceptionError = errors.find((e) => e.type === "exception");
@@ -126,6 +132,10 @@ export const MarimoErrorOutput = ({
126132
const unknownErrors = errors.filter(
127133
(e): e is Extract<MarimoError, { type: "unknown" }> => e.type === "unknown",
128134
);
135+
const sqlErrors = errors.filter(
136+
(e): e is Extract<MarimoError, { type: "sql-error" }> =>
137+
e.type === "sql-error",
138+
);
129139

130140
const openScratchpad = () => {
131141
chromeActions.openApplication("scratchpad");
@@ -485,6 +495,55 @@ export const MarimoErrorOutput = ({
485495
);
486496
}
487497

498+
if (sqlErrors.length > 0) {
499+
messages.push(
500+
<div key="sql-errors">
501+
{sqlErrors.map((error, idx) => {
502+
const line =
503+
error.sql_line != null ? (error?.sql_line | 0) + 1 : null;
504+
const col = error.sql_col != null ? (error?.sql_col | 0) + 1 : null;
505+
return (
506+
<div key={`sql-error-${idx}`} className="space-y-2">
507+
<p className="text-muted-foreground">{error.msg}</p>
508+
{error.hint && (
509+
<div className="flex items-start gap-2">
510+
<InfoIcon className="h-4 w-4 text-muted-foreground mt-0.5 flex-shrink-0" />
511+
<pre className="whitespace-pre-wrap text-sm text-muted-foreground">
512+
{error.hint}
513+
</pre>
514+
</div>
515+
)}
516+
{error.sql_statement && (
517+
<div className="bg-muted/50 p-2 rounded text-xs font-mono">
518+
<pre className="whitespace-pre-wrap">
519+
{error.sql_statement}
520+
</pre>
521+
</div>
522+
)}
523+
{line !== null && col !== null && (
524+
<p className="text-xs text-muted-foreground">
525+
Error at line {line}, column {col}
526+
</p>
527+
)}
528+
</div>
529+
);
530+
})}
531+
{cellId && <AutoFixButton errors={sqlErrors} cellId={cellId} />}
532+
<Tip title="How to fix SQL errors">
533+
<p className="pb-2">
534+
SQL parsing errors often occur due to invalid syntax, missing
535+
keywords, or unsupported SQL features.
536+
</p>
537+
<p className="py-2">
538+
Check your SQL syntax and ensure you're using supported SQL
539+
dialect features. The error location can help you identify the
540+
problematic part of your query.
541+
</p>
542+
</Tip>
543+
</div>,
544+
);
545+
}
546+
488547
return messages;
489548
};
490549

marimo/_ast/sql_utils.py

Lines changed: 2 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
# Copyright 2025 Marimo. All rights reserved.
22

3-
import ast
4-
import re
5-
from typing import Callable, Literal, Optional, TypedDict, Union
3+
from typing import Literal, Optional, Union
64

75
from marimo import _loggers
86
from marimo._dependencies.dependencies import DependencyManager
7+
from marimo._sql.error_utils import log_sql_error
98

109
LOGGER = _loggers.marimo_logger()
1110

@@ -19,20 +18,6 @@
1918
]
2019

2120

22-
class SQLErrorMetadata(TypedDict):
23-
"""Structured metadata for SQL parsing errors."""
24-
25-
lint_rule: str
26-
error_type: str
27-
clean_message: str # Just the meaningful error without SQL trace
28-
node_lineno: int
29-
node_col_offset: int
30-
sql_statement: str # Truncated if needed
31-
sql_line: Optional[int] # 0-based line within SQL
32-
sql_col: Optional[int] # 0-based column within SQL
33-
context: str
34-
35-
3621
def classify_sql_statement(
3722
sql_statement: str, dialect: Optional[SQLGLOT_DIALECTS] = None
3823
) -> Union[SQL_TYPE, Literal["unknown"]]:
@@ -75,45 +60,3 @@ def classify_sql_statement(
7560
return "DQL"
7661

7762
return "unknown"
78-
79-
80-
def log_sql_error(
81-
logger: Callable[..., None],
82-
*,
83-
message: str,
84-
exception: BaseException,
85-
rule_code: str,
86-
node: Optional[ast.expr] = None,
87-
sql_content: str = "",
88-
context: str = "",
89-
) -> None:
90-
"""Utility to log SQL-related errors with consistent metadata."""
91-
# Parse SQL position from exception message if available
92-
sql_line = None
93-
sql_col = None
94-
95-
exception_msg = str(exception)
96-
line_col_match = re.search(r"Line (\d+), Col: (\d+)", exception_msg)
97-
if line_col_match:
98-
sql_line = int(line_col_match.group(1)) - 1 # Convert to 0-based
99-
sql_col = int(line_col_match.group(2)) - 1 # Convert to 0-based
100-
101-
# Truncate long SQL content
102-
truncated_sql = sql_content
103-
if sql_content and len(sql_content) > 200:
104-
truncated_sql = sql_content[:200] + "..."
105-
106-
# Create metadata using TypedDict
107-
metadata: SQLErrorMetadata = {
108-
"lint_rule": rule_code,
109-
"error_type": type(exception).__name__,
110-
"clean_message": exception_msg.split("\n", 1)[0],
111-
"node_lineno": node.lineno if node else 0,
112-
"node_col_offset": node.col_offset if node else 0,
113-
"sql_statement": truncated_sql,
114-
"sql_line": sql_line,
115-
"sql_col": sql_col,
116-
"context": context,
117-
}
118-
119-
logger(message, exception, extra=metadata)

marimo/_ast/visitor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
from marimo import _loggers
1414
from marimo._ast.errors import ImportStarError
15-
from marimo._ast.sql_utils import log_sql_error
1615
from marimo._ast.sql_visitor import (
1716
SQLDefs,
1817
SQLKind,
@@ -23,6 +22,7 @@
2322
)
2423
from marimo._ast.variables import is_local
2524
from marimo._dependencies.dependencies import DependencyManager
25+
from marimo._sql.error_utils import log_sql_error
2626
from marimo._utils.strings import standardize_annotation_quotes
2727

2828
LOGGER = _loggers.marimo_logger()

marimo/_messaging/errors.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,25 @@ def describe(self) -> str:
113113
return self.msg
114114

115115

116+
class MarimoSQLError(msgspec.Struct, tag="sql-error"):
117+
"""
118+
SQL-specific error with enhanced metadata for debugging.
119+
"""
120+
121+
msg: str
122+
sql_statement: str
123+
hint: Optional[str] = (
124+
None # Helpful hints like "Did you mean?" or "Candidate bindings"
125+
)
126+
sql_line: Optional[int] = None # 0-based line within SQL
127+
sql_col: Optional[int] = None # 0-based column within SQL
128+
node_lineno: int = 0
129+
node_col_offset: int = 0
130+
131+
def describe(self) -> str:
132+
return self.msg
133+
134+
116135
def is_unexpected_error(error: Error) -> bool:
117136
"""
118137
These errors are unexpected, in that they are not intentional.
@@ -154,5 +173,6 @@ def is_sensitive_error(error: Error) -> bool:
154173
MarimoInterruptionError,
155174
MarimoSyntaxError,
156175
MarimoInternalError,
176+
MarimoSQLError,
157177
UnknownError,
158178
]

marimo/_runtime/runner/cell_runner.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@
3838
get_executor,
3939
)
4040
from marimo._runtime.marimo_pdb import MarimoPdb
41+
from marimo._sql.error_utils import (
42+
create_sql_error_from_exception,
43+
is_sql_parse_error,
44+
)
4145
from marimo._types.ids import CellId_t
4246

4347
LOGGER = marimo_logger()
@@ -403,6 +407,13 @@ def _run_result_from_exception(
403407
exception = output
404408
except Exception:
405409
pass
410+
# Handle SQL parsing errors
411+
elif unwrapped_exception is not None and is_sql_parse_error(
412+
unwrapped_exception
413+
):
414+
cell = self.graph.cells[cell_id]
415+
output = create_sql_error_from_exception(unwrapped_exception, cell)
416+
exception = output
406417
elif isinstance(unwrapped_exception, MarimoStopError):
407418
output = unwrapped_exception.output
408419
exception = unwrapped_exception

marimo/_runtime/runner/hooks_post_execution.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from marimo._messaging.errors import (
1717
MarimoExceptionRaisedError,
1818
MarimoInterruptionError,
19+
MarimoSQLError,
1920
MarimoStrictExecutionError,
2021
)
2122
from marimo._messaging.ops import (
@@ -333,6 +334,13 @@ def _broadcast_outputs(
333334
clear_console=False,
334335
cell_id=cell.cell_id,
335336
)
337+
elif isinstance(run_result.exception, MarimoSQLError):
338+
LOGGER.debug("Cell %s raised a SQL error", cell.cell_id)
339+
CellOp.broadcast_error(
340+
data=[run_result.exception],
341+
clear_console=True,
342+
cell_id=cell.cell_id,
343+
)
336344
elif run_result.exception is not None:
337345
LOGGER.debug(
338346
"Cell %s raised %s",

0 commit comments

Comments
 (0)