@marimo-team/frontend 0.16.1 → 0.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/dist/assets/{ConnectedDataExplorerComponent-2wVcyvDj.js → ConnectedDataExplorerComponent-B5cPvWoQ.js} +1 -1
  2. package/dist/assets/{ImageComparisonComponent-D2j6i0hv.js → ImageComparisonComponent-CqR26LSv.js} +1 -1
  3. package/dist/assets/{VegaLite-BckFaf2D.js → VegaLite-DvQDATwI.js} +1 -1
  4. package/dist/assets/_baseEach--KDTwKbG.js +1 -0
  5. package/dist/assets/_baseMap-Cu3o-eyO.js +1 -0
  6. package/dist/assets/{_baseUniq-BKktIGQ1.js → _baseUniq-y7ZXnMo1.js} +1 -1
  7. package/dist/assets/{_createAggregator-C5CVY-0t.js → _createAggregator-ZcHkHPNJ.js} +1 -1
  8. package/dist/assets/{agent-panel-RGLNjkYe.js → agent-panel-B91RoLct.js} +6 -6
  9. package/dist/assets/{any-language-editor-DjuXwGCA.js → any-language-editor-CxfHcm5h.js} +1 -1
  10. package/dist/assets/{architectureDiagram-W76B3OCA-Dyj4ds_R.js → architectureDiagram-W76B3OCA-BQsvK8uR.js} +1 -1
  11. package/dist/assets/{between-horizontal-start-Dt2aKpPf.js → between-horizontal-start-BmYToIaM.js} +1 -1
  12. package/dist/assets/{blockDiagram-QIGZ2CNN-o-i7DDvN.js → blockDiagram-QIGZ2CNN-r3HgCj4w.js} +1 -1
  13. package/dist/assets/{c4Diagram-FPNF74CW-DGHEwWrx.js → c4Diagram-FPNF74CW-BJbPNt41.js} +1 -1
  14. package/dist/assets/channel-DFaEx1fu.js +1 -0
  15. package/dist/assets/{chat-panel-9alr8FS4.js → chat-panel-IoPMv8e2.js} +3 -3
  16. package/dist/assets/{chunk-4BX2VUAB-BJecb-Ri.js → chunk-4BX2VUAB-Dv4MZ9Hj.js} +1 -1
  17. package/dist/assets/{chunk-55IACEB6-CAATkc4w.js → chunk-55IACEB6-CM4AHquB.js} +1 -1
  18. package/dist/assets/{chunk-FMBD7UC4-DPuNbQ-f.js → chunk-FMBD7UC4-C_Zz0ENB.js} +1 -1
  19. package/dist/assets/{chunk-K7UQS3LO-C8TWVLiH.js → chunk-K7UQS3LO-DYSmiXYq.js} +1 -1
  20. package/dist/assets/{chunk-QN33PNHL-DiZZ09q4.js → chunk-QN33PNHL-QM4OPuQP.js} +1 -1
  21. package/dist/assets/{chunk-QZHKN3VN-BIUM7usu.js → chunk-QZHKN3VN-CfAsGyeB.js} +1 -1
  22. package/dist/assets/{chunk-TVAH2DTR-vGTArPBG.js → chunk-TVAH2DTR-6j_Cpjsi.js} +1 -1
  23. package/dist/assets/{chunk-TZMSLE5B-D2KRqp_x.js → chunk-TZMSLE5B-BHslFJQE.js} +1 -1
  24. package/dist/assets/{circle-play-cjeNez0N.js → circle-play-CK3UZRYQ.js} +1 -1
  25. package/dist/assets/classDiagram-KNZD7YFC-BsZtvV5O.js +1 -0
  26. package/dist/assets/classDiagram-v2-RKCZMP56-BsZtvV5O.js +1 -0
  27. package/dist/assets/{clear-button-C97JtAez.js → clear-button-C4fDVSv8.js} +1 -1
  28. package/dist/assets/clone-YBEvPE-s.js +1 -0
  29. package/dist/assets/command-palette-D7hOfvf6.js +1 -0
  30. package/dist/assets/{common-Du9rSOwD.js → common-D-lbuUwz.js} +1 -1
  31. package/dist/assets/{compile-CZXqyOxa.js → compile-DVQe1Mzk.js} +1 -1
  32. package/dist/assets/{cose-bilkent-S5V4N54A-CqUN5Y9b.js → cose-bilkent-S5V4N54A-D-IS7WC8.js} +1 -1
  33. package/dist/assets/{dagre-5GWH7T2D-RJqTI9DM.js → dagre-5GWH7T2D-lYu-tEWT.js} +1 -1
  34. package/dist/assets/{data-grid-overlay-editor-DZN0q1LV.js → data-grid-overlay-editor-C5peOCit.js} +1 -1
  35. package/dist/assets/datasources-panel-D3NA20uZ.js +1 -0
  36. package/dist/assets/{dependency-graph-panel-CEog_O7V.js → dependency-graph-panel-BGVYOfkV.js} +1 -1
  37. package/dist/assets/{diagram-N5W7TBWH-D-l4zZ9d.js → diagram-N5W7TBWH-BnvIuYUp.js} +1 -1
  38. package/dist/assets/{diagram-QEK2KX5R-CCOmBUt-.js → diagram-QEK2KX5R-DemedRK3.js} +1 -1
  39. package/dist/assets/{diagram-S2PKOQOG-C_I_9jnZ.js → diagram-S2PKOQOG-iiY7AuyH.js} +1 -1
  40. package/dist/assets/{documentation-panel-C1BtMZ3M.js → documentation-panel-C3dSwOSQ.js} +1 -1
  41. package/dist/assets/{edit-page-B-oevUZ9.js → edit-page-C5TsEeSo.js} +17 -17
  42. package/dist/assets/{ellipsis-vertical-BEb-J8z6.js → ellipsis-vertical-CazJl8M7.js} +1 -1
  43. package/dist/assets/{empty-state-C99UyDE3.js → empty-state-DW308mFO.js} +1 -1
  44. package/dist/assets/{erDiagram-AWTI2OKA-BePOLi5M.js → erDiagram-AWTI2OKA-6wQ8Ugg0.js} +1 -1
  45. package/dist/assets/{error-panel-Bs34jXFh.js → error-panel-D1VnJ1yP.js} +1 -1
  46. package/dist/assets/{file-explorer-panel-Ck6UL861.js → file-explorer-panel-0oVd4t-D.js} +1 -1
  47. package/dist/assets/{flowDiagram-PVAE7QVJ-BgjFu5l7.js → flowDiagram-PVAE7QVJ-C55IUWjm.js} +1 -1
  48. package/dist/assets/{ganttDiagram-OWAHRB6G-YOPb3XSV.js → ganttDiagram-OWAHRB6G-DmqCM6ME.js} +1 -1
  49. package/dist/assets/{gitGraphDiagram-NY62KEGX-CGhqaDTy.js → gitGraphDiagram-NY62KEGX-DBvhAeM_.js} +1 -1
  50. package/dist/assets/{glide-data-editor-9QUH6iso.js → glide-data-editor-CHNuHidQ.js} +11 -11
  51. package/dist/assets/{graph-DQQFGrho.js → graph-CG6BgUWQ.js} +1 -1
  52. package/dist/assets/{home-page-DRKpPCrF.js → home-page-dgivXuSR.js} +3 -3
  53. package/dist/assets/{index-SGLNXrGP.js → index-BTGpssVX.js} +1 -1
  54. package/dist/assets/{index-Aeo6WiK7.js → index-BYVZlBF8.js} +1 -1
  55. package/dist/assets/{index-CUFv_thQ.js → index-BelfnXwL.js} +1 -1
  56. package/dist/assets/{index-DdnKZNxM.js → index-BneyUujp.js} +1 -1
  57. package/dist/assets/{index-BJNCMUmG.js → index-C02SqeRj.js} +1 -1
  58. package/dist/assets/{index-aE43R74q.js → index-C7dtgr9A.js} +1 -1
  59. package/dist/assets/{index-C2MD0vgD.js → index-CAQvMTzM.js} +1 -1
  60. package/dist/assets/index-CGDMlQfO.css +1 -0
  61. package/dist/assets/index-CelXfcd8.js +580 -0
  62. package/dist/assets/{index-C_tkBKNO.js → index-Csd6QrCV.js} +1 -1
  63. package/dist/assets/{index-BAbIIxHU.js → index-CtPksxf0.js} +1 -1
  64. package/dist/assets/{index-2252nrk6.js → index-Cxyk7pt-.js} +1 -1
  65. package/dist/assets/{index-BW3k9Gss.js → index-DAZ-9ri2.js} +1 -1
  66. package/dist/assets/{index-ClzeQrN7.js → index-DONRrmA2.js} +1 -1
  67. package/dist/assets/{index-B8jXZ12t.js → index-Db36XTG_.js} +1 -1
  68. package/dist/assets/{index-BprjMYH5.js → index-DdIhdEVw.js} +1 -1
  69. package/dist/assets/{index-CFKO7WXI.js → index-M_pBKDSe.js} +1 -1
  70. package/dist/assets/{index-CfaDbEdi.js → index-_luCZMLM.js} +1 -1
  71. package/dist/assets/{index-BjgnbONl.js → index-mkubqy9-.js} +1 -1
  72. package/dist/assets/{index-C1ez98sk.js → index-sbO9UaUU.js} +1 -1
  73. package/dist/assets/{index-G5QZppK2.js → index-z4krxQ4j.js} +1 -1
  74. package/dist/assets/infoDiagram-STP46IZ2-wTALjfPc.js +2 -0
  75. package/dist/assets/{isEmpty-D-4c7sMv.js → isEmpty-CqX_YTIf.js} +1 -1
  76. package/dist/assets/{journeyDiagram-BIP6EPQ6-C94u3Mv3.js → journeyDiagram-BIP6EPQ6-Y5w_Tqe_.js} +1 -1
  77. package/dist/assets/{kanban-definition-6OIFK2YF-BEXYFzz7.js → kanban-definition-6OIFK2YF-DbXs5Rxi.js} +1 -1
  78. package/dist/assets/{layout-Bz2BJ2ru.js → layout-BCNPDACj.js} +1 -1
  79. package/dist/assets/{linear-D8s7K76e.js → linear-uO6UVhXt.js} +1 -1
  80. package/dist/assets/{links-BpXlz1GG.js → links-Drv7cJgN.js} +3 -3
  81. package/dist/assets/{logs-panel-DC7wpmPz.js → logs-panel-BEQ1eRUp.js} +1 -1
  82. package/dist/assets/{markdown-renderer-DRdSWR9X.js → markdown-renderer-Dmzbb00W.js} +3 -3
  83. package/dist/assets/{mermaid-Y3x4hmD0.js → mermaid-qRc4MXIj.js} +1 -1
  84. package/dist/assets/{mermaid.core-DzthE35Y.js → mermaid.core-CvvJtCRj.js} +4 -4
  85. package/dist/assets/min-DYUOb1RR.js +1 -0
  86. package/dist/assets/{mindmap-definition-Q6HEUPPD-DktvuLe1.js → mindmap-definition-Q6HEUPPD-G5NognM-.js} +1 -1
  87. package/dist/assets/{number-overlay-editor-BEfwI1IT.js → number-overlay-editor-DPr5sHFu.js} +1 -1
  88. package/dist/assets/{outline-panel-CdsnAy2w.js → outline-panel-gxQXvVi4.js} +1 -1
  89. package/dist/assets/{packages-panel-DiTA-d_D.js → packages-panel-B1T0VPlg.js} +1 -1
  90. package/dist/assets/{pieDiagram-ADFJNKIX-DQDNQ-de.js → pieDiagram-ADFJNKIX-DK9SHkfc.js} +1 -1
  91. package/dist/assets/{quadrantDiagram-LMRXKWRM-0kgIXc2-.js → quadrantDiagram-LMRXKWRM-D1DdWF8C.js} +1 -1
  92. package/dist/assets/{react-plotly-DJqqfM7c.js → react-plotly-CTwajqCb.js} +1 -1
  93. package/dist/assets/{requirementDiagram-4UW4RH46-B5rb0ypd.js → requirementDiagram-4UW4RH46-DnjDAypr.js} +1 -1
  94. package/dist/assets/{run-page-CFmLrv1R.js → run-page-CQY9im22.js} +1 -1
  95. package/dist/assets/{sankeyDiagram-GR3RE2ED-Dom7IlnF.js → sankeyDiagram-GR3RE2ED-B67Va-ER.js} +1 -1
  96. package/dist/assets/{scratchpad-panel-CuHWpHO8.js → scratchpad-panel-DlDfcDtW.js} +1 -1
  97. package/dist/assets/{secrets-panel-CfHc5YD0.js → secrets-panel-BDGyuGZA.js} +1 -1
  98. package/dist/assets/{sequenceDiagram-C3RYC4MD-PNJWXQbw.js → sequenceDiagram-C3RYC4MD-DiWgZPtN.js} +1 -1
  99. package/dist/assets/{slides-component-CJgaTRZ0.js → slides-component-DhpPRtQp.js} +1 -1
  100. package/dist/assets/{snippets-panel-B2EC1txM.js → snippets-panel-CLkBXhJ2.js} +1 -1
  101. package/dist/assets/{sortBy-DZnlX29-.js → sortBy-D4OG7w4O.js} +1 -1
  102. package/dist/assets/{state-CWict9RU.js → state-Dz_3JyED.js} +1 -1
  103. package/dist/assets/{stateDiagram-KXAO66HF-BE58aJnr.js → stateDiagram-KXAO66HF-ByF2AULw.js} +1 -1
  104. package/dist/assets/stateDiagram-v2-UMBNRL4Z-CtBJqosP.js +1 -0
  105. package/dist/assets/{storage-DRaR04wR.js → storage-Dr0CC44z.js} +6 -6
  106. package/dist/assets/{terminal-BX3Su5q7.js → terminal-BtdissBf.js} +1 -1
  107. package/dist/assets/{time-hUzZfpNE.js → time-DKdOTnQg.js} +1 -1
  108. package/dist/assets/{timeline-definition-XQNQX7LJ-CqQP9t51.js → timeline-definition-XQNQX7LJ-DzER9bf6.js} +1 -1
  109. package/dist/assets/{tracing-B10Q1n-L.js → tracing-Dpx5M-u3.js} +2 -2
  110. package/dist/assets/{tracing-panel-Du8WCnno.js → tracing-panel-hCjBkSER.js} +2 -2
  111. package/dist/assets/{trash-B81GTiv6.js → trash-C6Ko-g5q.js} +1 -1
  112. package/dist/assets/{tree-6vW2ogkh.js → tree-BHN2gcCF.js} +6 -6
  113. package/dist/assets/{treemap-75Q7IDZK-CdwDwwsz.js → treemap-75Q7IDZK-DR79Mhzt.js} +1 -1
  114. package/dist/assets/{variable-panel-D5qgJI7k.js → variable-panel-PFBCFz36.js} +1 -1
  115. package/dist/assets/{vega-component-DJaJWMJM.js → vega-component-Db6-uY4C.js} +1 -1
  116. package/dist/assets/{xychartDiagram-6GGTOJPD-WFtXqaM9.js → xychartDiagram-6GGTOJPD-DWzBP3tZ.js} +1 -1
  117. package/dist/index.html +2 -2
  118. package/package.json +3 -3
  119. package/src/__mocks__/requests.ts +1 -0
  120. package/src/components/data-table/__tests__/columns.test.tsx +38 -0
  121. package/src/components/data-table/cell-hover-template/feature.ts +1 -1
  122. package/src/components/data-table/cell-hover-template/types.ts +1 -1
  123. package/src/components/data-table/columns.tsx +21 -2
  124. package/src/components/data-table/renderers.tsx +16 -8
  125. package/src/components/data-table/schemas.ts +16 -0
  126. package/src/components/editor/Cell.tsx +2 -0
  127. package/src/components/editor/errors/sql-validation-errors.tsx +34 -0
  128. package/src/components/editor/output/ConsoleOutput.tsx +13 -1
  129. package/src/components/editor/output/MarimoErrorOutput.tsx +60 -1
  130. package/src/core/ai/context/providers/cell-output.ts +1 -18
  131. package/src/core/codemirror/language/__tests__/extension.test.ts +24 -0
  132. package/src/core/codemirror/language/__tests__/sql-validation.test.ts +133 -0
  133. package/src/core/codemirror/language/languages/sql/sql-mode.ts +20 -0
  134. package/src/core/codemirror/language/languages/sql/sql.ts +90 -3
  135. package/src/core/codemirror/language/languages/sql/validation-errors.ts +79 -0
  136. package/src/core/codemirror/language/panel/panel.tsx +8 -2
  137. package/src/core/codemirror/language/panel/sql.tsx +81 -4
  138. package/src/core/config/feature-flag.tsx +3 -1
  139. package/src/core/datasets/request-registry.ts +17 -1
  140. package/src/core/islands/bridge.ts +1 -0
  141. package/src/core/islands/main.ts +1 -0
  142. package/src/core/kernel/messages.ts +1 -0
  143. package/src/core/network/requests-network.ts +7 -0
  144. package/src/core/network/requests-static.ts +1 -0
  145. package/src/core/network/requests-toasting.ts +1 -0
  146. package/src/core/network/types.ts +2 -0
  147. package/src/core/wasm/bridge.ts +5 -0
  148. package/src/core/websocket/useMarimoWebSocket.tsx +4 -0
  149. package/src/plugins/core/registerReactComponent.tsx +23 -19
  150. package/src/plugins/impl/DataTablePlugin.tsx +11 -4
  151. package/src/plugins/impl/data-frames/DataFramePlugin.tsx +17 -5
  152. package/src/stories/dataframe.stories.tsx +2 -0
  153. package/src/utils/__tests__/dom.test.ts +167 -0
  154. package/src/utils/dom.ts +55 -0
  155. package/dist/assets/_baseEach-CvTX9w0Y.js +0 -1
  156. package/dist/assets/_baseMap-CtlwA90f.js +0 -1
  157. package/dist/assets/channel-Co6iMgWq.js +0 -1
  158. package/dist/assets/classDiagram-KNZD7YFC-BbJ0rY3y.js +0 -1
  159. package/dist/assets/classDiagram-v2-RKCZMP56-BbJ0rY3y.js +0 -1
  160. package/dist/assets/clone-BMP0PsTa.js +0 -1
  161. package/dist/assets/command-palette-B93Pjcky.js +0 -1
  162. package/dist/assets/datasources-panel-v7H3cR0p.js +0 -1
  163. package/dist/assets/index-C7CoaNFb.js +0 -578
  164. package/dist/assets/index-DadI618h.css +0 -1
  165. package/dist/assets/infoDiagram-STP46IZ2-CJLOpSAf.js +0 -2
  166. package/dist/assets/min-BBO3-1Hg.js +0 -1
  167. package/dist/assets/stateDiagram-v2-UMBNRL4Z-CdThjimL.js +0 -1
@@ -1,6 +1,10 @@
1
1
  /* Copyright 2024 Marimo. All rights reserved. */
2
2
 
3
- import { NotebookPenIcon, SquareArrowOutUpRightIcon } from "lucide-react";
3
+ import {
4
+ InfoIcon,
5
+ NotebookPenIcon,
6
+ SquareArrowOutUpRightIcon,
7
+ } from "lucide-react";
4
8
  import { Fragment, type JSX } from "react";
5
9
  import {
6
10
  Accordion,
@@ -72,6 +76,8 @@ export const MarimoErrorOutput = ({
72
76
  titleContents = "Ancestor stopped";
73
77
  alertVariant = "default";
74
78
  titleColor = "text-secondary-foreground";
79
+ } else if (errors.some((e) => e.type === "sql-error")) {
80
+ titleContents = "SQL Error in statement";
75
81
  } else {
76
82
  // Check for exception type
77
83
  const exceptionError = errors.find((e) => e.type === "exception");
@@ -126,6 +132,10 @@ export const MarimoErrorOutput = ({
126
132
  const unknownErrors = errors.filter(
127
133
  (e): e is Extract<MarimoError, { type: "unknown" }> => e.type === "unknown",
128
134
  );
135
+ const sqlErrors = errors.filter(
136
+ (e): e is Extract<MarimoError, { type: "sql-error" }> =>
137
+ e.type === "sql-error",
138
+ );
129
139
 
130
140
  const openScratchpad = () => {
131
141
  chromeActions.openApplication("scratchpad");
@@ -485,6 +495,55 @@ export const MarimoErrorOutput = ({
485
495
  );
486
496
  }
487
497
 
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
+
488
547
  return messages;
489
548
  };
490
549
 
@@ -9,6 +9,7 @@ import { displayCellName } from "@/core/cells/names";
9
9
  import { isOutputEmpty } from "@/core/cells/outputs";
10
10
  import type { OutputMessage } from "@/core/kernel/messages";
11
11
  import type { JotaiStore } from "@/core/state/jotai";
12
+ import { parseHtmlContent } from "@/utils/dom";
12
13
  import { Logger } from "@/utils/Logger";
13
14
  import { type AIContextItem, AIContextProvider } from "../registry";
14
15
  import { contextToXml } from "../utils";
@@ -64,24 +65,6 @@ function isMediaMimetype(
64
65
  return false;
65
66
  }
66
67
 
67
- function parseHtmlContent(htmlString: string): string {
68
- try {
69
- // Create a temporary DOM element to parse HTML
70
- const tempDiv = document.createElement("div");
71
- tempDiv.innerHTML = htmlString;
72
-
73
- // Extract text content, removing HTML tags
74
- const textContent = tempDiv.textContent || tempDiv.innerText || "";
75
-
76
- // Clean up extra whitespace
77
- return textContent.replaceAll(/\s+/g, " ").trim();
78
- } catch (error) {
79
- Logger.error("Error parsing HTML content:", error);
80
- // If parsing fails, return the original string
81
- return htmlString;
82
- }
83
- }
84
-
85
68
  export class CellOutputContextProvider extends AIContextProvider<CellOutputContextItem> {
86
69
  readonly title = "Cell Outputs";
87
70
  readonly mentionPrefix = "@";
@@ -14,6 +14,7 @@ import {
14
14
  languageAdapterState,
15
15
  switchLanguage,
16
16
  } from "../extension";
17
+ import { exportedForTesting as sqlValidationErrorsForTesting } from "../languages/sql/validation-errors";
17
18
  import { languageMetadataField } from "../metadata";
18
19
 
19
20
  let view: EditorView | null = null;
@@ -258,3 +259,26 @@ describe("switchLanguage", () => {
258
259
  });
259
260
  });
260
261
  });
262
+
263
+ describe("sqlValidationErrors", () => {
264
+ const { splitErrorMessage } = sqlValidationErrorsForTesting;
265
+
266
+ describe("split error message", () => {
267
+ it("should split the error message into error type and error message", () => {
268
+ const error = "SyntaxError: SELECT * FROM df";
269
+ const { errorType, errorMessage } = splitErrorMessage(error);
270
+ expect(errorType).toBe("SyntaxError");
271
+ expect(errorMessage).toBe("SELECT * FROM df");
272
+ });
273
+
274
+ it("should handle multiple colons", () => {
275
+ const error =
276
+ "SyntaxError: SELECT * FROM df:SyntaxError: SELECT * FROM df";
277
+ const { errorType, errorMessage } = splitErrorMessage(error);
278
+ expect(errorType).toBe("SyntaxError");
279
+ expect(errorMessage).toBe(
280
+ "SELECT * FROM df:SyntaxError: SELECT * FROM df",
281
+ );
282
+ });
283
+ });
284
+ });
@@ -0,0 +1,133 @@
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
+
3
+ import { describe, expect, it } from "vitest";
4
+ import { exportedForTesting } from "../languages/sql/validation-errors";
5
+
6
+ describe("Error Message Splitting", () => {
7
+ it("should handle error message splitting correctly", () => {
8
+ const { splitErrorMessage } = exportedForTesting;
9
+
10
+ const result1 = splitErrorMessage("Syntax error: unexpected token");
11
+ expect(result1.errorType).toBe("Syntax error");
12
+ expect(result1.errorMessage).toBe("unexpected token");
13
+
14
+ const result2 = splitErrorMessage("Multiple: colons: in error");
15
+ expect(result2.errorType).toBe("Multiple");
16
+ expect(result2.errorMessage).toBe("colons: in error");
17
+
18
+ const result3 = splitErrorMessage("No colon error");
19
+ expect(result3.errorType).toBe("No colon error");
20
+ expect(result3.errorMessage).toBe("");
21
+ });
22
+ });
23
+
24
+ describe("DuckDB Error Handling", () => {
25
+ it("should extract codeblock from error with LINE information", () => {
26
+ const { handleDuckdbError } = exportedForTesting;
27
+
28
+ const error =
29
+ '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\' ^';
30
+
31
+ const result = handleDuckdbError(error);
32
+
33
+ expect(result.errorType).toBe("Binder Error");
34
+ expect(result.errorMessage).toBe(
35
+ 'Referenced column "attacks" not found in FROM clause! Candidate bindings: "Attack", "Total"',
36
+ );
37
+ expect(result.codeblock).toBe(
38
+ "LINE 1:... from pokemon WHERE 'type_2' = 32 and attack = 32 and not attacks = 'hi' ^",
39
+ );
40
+ });
41
+
42
+ it("should handle error without LINE information", () => {
43
+ const { handleDuckdbError } = exportedForTesting;
44
+
45
+ const error = "Syntax Error: Invalid syntax near WHERE";
46
+
47
+ const result = handleDuckdbError(error);
48
+
49
+ expect(result.errorType).toBe("Syntax Error");
50
+ expect(result.errorMessage).toBe("Invalid syntax near WHERE");
51
+ expect(result.codeblock).toBeUndefined();
52
+ });
53
+
54
+ it("should handle error with LINE at the beginning", () => {
55
+ const { handleDuckdbError } = exportedForTesting;
56
+
57
+ const error = "LINE 1: SELECT * FROM table WHERE invalid_column = 1 ^";
58
+
59
+ const result = handleDuckdbError(error);
60
+
61
+ expect(result.errorType).toBe("LINE 1");
62
+ expect(result.errorMessage).toBe(
63
+ "SELECT * FROM table WHERE invalid_column = 1 ^",
64
+ );
65
+ expect(result.codeblock).toBeUndefined();
66
+ });
67
+
68
+ it("should handle error with multiple LINE occurrences", () => {
69
+ const { handleDuckdbError } = exportedForTesting;
70
+
71
+ const error =
72
+ "Error: Something went wrong LINE 1: SELECT * FROM table WHERE invalid_column = 1 ^";
73
+
74
+ const result = handleDuckdbError(error);
75
+
76
+ expect(result.errorType).toBe("Error");
77
+ expect(result.errorMessage).toBe("Something went wrong");
78
+ expect(result.codeblock).toBe(
79
+ "LINE 1: SELECT * FROM table WHERE invalid_column = 1 ^",
80
+ );
81
+ });
82
+
83
+ it("should handle complex error with nested quotes", () => {
84
+ const { handleDuckdbError } = exportedForTesting;
85
+
86
+ const error =
87
+ "Binder Error: Column \"name\" not found! LINE 1: SELECT * FROM users WHERE name = 'John' AND age > 25 ^";
88
+
89
+ const result = handleDuckdbError(error);
90
+
91
+ expect(result.errorType).toBe("Binder Error");
92
+ expect(result.errorMessage).toBe('Column "name" not found!');
93
+ expect(result.codeblock).toBe(
94
+ "LINE 1: SELECT * FROM users WHERE name = 'John' AND age > 25 ^",
95
+ );
96
+ });
97
+
98
+ it("should handle error with LINE but no caret", () => {
99
+ const { handleDuckdbError } = exportedForTesting;
100
+
101
+ const error = "Error: Invalid query LINE 1: SELECT * FROM table";
102
+
103
+ const result = handleDuckdbError(error);
104
+
105
+ expect(result.errorType).toBe("Error");
106
+ expect(result.errorMessage).toBe("Invalid query");
107
+ expect(result.codeblock).toBe("LINE 1: SELECT * FROM table");
108
+ });
109
+
110
+ it("should trim whitespace from codeblock", () => {
111
+ const { handleDuckdbError } = exportedForTesting;
112
+
113
+ const error = "Error: Something wrong LINE 1: SELECT * FROM table ^ ";
114
+
115
+ const result = handleDuckdbError(error);
116
+
117
+ expect(result.errorType).toBe("Error");
118
+ expect(result.errorMessage).toBe("Something wrong");
119
+ expect(result.codeblock).toBe("LINE 1: SELECT * FROM table ^");
120
+ });
121
+
122
+ it("should handle empty error message", () => {
123
+ const { handleDuckdbError } = exportedForTesting;
124
+
125
+ const error = "";
126
+
127
+ const result = handleDuckdbError(error);
128
+
129
+ expect(result.errorType).toBe("");
130
+ expect(result.errorMessage).toBe("");
131
+ expect(result.codeblock).toBeUndefined();
132
+ });
133
+ });
@@ -0,0 +1,20 @@
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
+
3
+ import { useAtom } from "jotai";
4
+ import { atomWithStorage } from "jotai/utils";
5
+ import { store } from "@/core/state/jotai";
6
+
7
+ const BASE_KEY = "marimo:notebook-sql-mode";
8
+
9
+ export type SQLMode = "validate" | "default";
10
+
11
+ const sqlModeAtom = atomWithStorage<SQLMode>(BASE_KEY, "default");
12
+
13
+ export function useSQLMode() {
14
+ const [sqlMode, setSQLMode] = useAtom(sqlModeAtom);
15
+ return { sqlMode, setSQLMode };
16
+ }
17
+
18
+ export function getSQLMode() {
19
+ return store.get(sqlModeAtom);
20
+ }
@@ -5,7 +5,7 @@ import { insertTab } from "@codemirror/commands";
5
5
  import { type SQLDialect, type SQLNamespace, sql } from "@codemirror/lang-sql";
6
6
  import type { EditorState, Extension } from "@codemirror/state";
7
7
  import { Compartment } from "@codemirror/state";
8
- import { type EditorView, keymap } from "@codemirror/view";
8
+ import { EditorView, keymap } from "@codemirror/view";
9
9
  import type { SyntaxNode, TreeCursor } from "@lezer/common";
10
10
  import { parser } from "@lezer/python";
11
11
  import {
@@ -16,12 +16,19 @@ import {
16
16
  } from "@marimo-team/codemirror-sql";
17
17
  import { DuckDBDialect } from "@marimo-team/codemirror-sql/dialects";
18
18
  import dedent from "string-dedent";
19
+ import { cellIdState } from "@/core/codemirror/cells/state";
19
20
  import { getFeatureFlag } from "@/core/config/feature-flag";
20
21
  import {
21
22
  dataSourceConnectionsAtom,
22
23
  setLatestEngineSelected,
23
24
  } from "@/core/datasets/data-source-connections";
24
- import { type ConnectionName, DUCKDB_ENGINE } from "@/core/datasets/engines";
25
+ import {
26
+ type ConnectionName,
27
+ DUCKDB_ENGINE,
28
+ INTERNAL_SQL_ENGINES,
29
+ } from "@/core/datasets/engines";
30
+ import { ValidateSQL } from "@/core/datasets/request-registry";
31
+ import type { ValidateSQLResult } from "@/core/kernel/messages";
25
32
  import { store } from "@/core/state/jotai";
26
33
  import { resolvedThemeAtom } from "@/theme/useTheme";
27
34
  import { Logger } from "@/utils/Logger";
@@ -37,6 +44,11 @@ import {
37
44
  tablesCompletionSource,
38
45
  } from "./completion-sources";
39
46
  import { SCHEMA_CACHE } from "./completion-store";
47
+ import { getSQLMode } from "./sql-mode";
48
+ import {
49
+ clearSqlValidationError,
50
+ setSqlValidationError,
51
+ } from "./validation-errors";
40
52
 
41
53
  const DEFAULT_DIALECT = DuckDBDialect;
42
54
  const DEFAULT_PARSER_DIALECT = "DuckDB";
@@ -64,12 +76,15 @@ export class SQLLanguageAdapter
64
76
  {
65
77
  readonly type = "sql";
66
78
  sqlLinterEnabled: boolean;
79
+ sqlModeEnabled: boolean;
67
80
 
68
81
  constructor() {
69
82
  try {
70
83
  this.sqlLinterEnabled = getFeatureFlag("sql_linter");
84
+ this.sqlModeEnabled = getFeatureFlag("sql_mode");
71
85
  } catch {
72
86
  this.sqlLinterEnabled = false;
87
+ this.sqlModeEnabled = false;
73
88
  }
74
89
  }
75
90
 
@@ -265,6 +280,10 @@ export class SQLLanguageAdapter
265
280
  );
266
281
  }
267
282
 
283
+ if (this.sqlModeEnabled) {
284
+ extensions.push(sqlValidationExtension());
285
+ }
286
+
268
287
  return extensions;
269
288
  }
270
289
  }
@@ -315,9 +334,14 @@ function getSchema(view: EditorView): SQLNamespace {
315
334
  function guessParserDialect(state: EditorState): ParserDialects | null {
316
335
  const metadata = getSQLMetadata(state);
317
336
  const connectionName = metadata.engine;
337
+ return connectionNameToParserDialect(connectionName);
338
+ }
339
+
340
+ function connectionNameToParserDialect(
341
+ connectionName: ConnectionName,
342
+ ): ParserDialects | null {
318
343
  const dialect =
319
344
  SCHEMA_CACHE.getInternalDialect(connectionName)?.toLowerCase();
320
-
321
345
  switch (dialect) {
322
346
  case "postgresql":
323
347
  case "postgres":
@@ -543,3 +567,66 @@ function safeDedent(code: string): string {
543
567
  return code;
544
568
  }
545
569
  }
570
+
571
+ function sqlValidationExtension(): Extension {
572
+ let debounceTimeout: NodeJS.Timeout | null = null;
573
+ let lastValidationRequest: string | null = null;
574
+
575
+ return EditorView.updateListener.of((update) => {
576
+ const sqlMode = getSQLMode();
577
+ if (sqlMode !== "validate") {
578
+ return;
579
+ }
580
+
581
+ const metadata = getSQLMetadata(update.state);
582
+ const connectionName = metadata.engine;
583
+ if (!INTERNAL_SQL_ENGINES.has(connectionName)) {
584
+ // Currently only internal engines are supported
585
+ return;
586
+ }
587
+
588
+ if (!update.docChanged) {
589
+ return;
590
+ }
591
+
592
+ const doc = update.state.doc;
593
+ const sqlContent = doc.toString();
594
+
595
+ // Clear existing timeout
596
+ if (debounceTimeout) {
597
+ clearTimeout(debounceTimeout);
598
+ }
599
+
600
+ // Debounce the validation call
601
+ debounceTimeout = setTimeout(async () => {
602
+ // Skip if content hasn't changed since last validation
603
+ if (lastValidationRequest === sqlContent) {
604
+ return;
605
+ }
606
+
607
+ lastValidationRequest = sqlContent;
608
+ const cellId = update.view.state.facet(cellIdState);
609
+
610
+ if (sqlContent === "") {
611
+ clearSqlValidationError(cellId);
612
+ return;
613
+ }
614
+
615
+ try {
616
+ const result: ValidateSQLResult = await ValidateSQL.request({
617
+ engine: connectionName,
618
+ query: sqlContent,
619
+ });
620
+
621
+ if (result.error) {
622
+ const dialect = connectionNameToParserDialect(connectionName);
623
+ setSqlValidationError({ cellId, error: result.error, dialect });
624
+ } else {
625
+ clearSqlValidationError(cellId);
626
+ }
627
+ } catch (error) {
628
+ Logger.warn("Failed to validate SQL", { error });
629
+ }
630
+ }, 300);
631
+ });
632
+ }
@@ -0,0 +1,79 @@
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
+
3
+ import type { SupportedDialects } from "@marimo-team/codemirror-sql";
4
+ import { atom, useAtomValue } from "jotai";
5
+ import type { CellId } from "@/core/cells/ids";
6
+ import { store } from "@/core/state/jotai";
7
+
8
+ export interface SQLValidationError {
9
+ errorType: string;
10
+ errorMessage: string;
11
+ codeblock?: string; // Code block that caused the error
12
+ }
13
+
14
+ type CellToSQLErrors = Map<CellId, SQLValidationError>;
15
+
16
+ export const sqlValidationErrorsAtom = atom<CellToSQLErrors>(
17
+ new Map<CellId, SQLValidationError>(),
18
+ );
19
+
20
+ export const useSqlValidationErrorsForCell = (cellId: CellId) => {
21
+ const sqlValidationErrors = useAtomValue(sqlValidationErrorsAtom);
22
+ return sqlValidationErrors.get(cellId);
23
+ };
24
+
25
+ export function clearSqlValidationError(cellId: CellId) {
26
+ const sqlValidationErrors = store.get(sqlValidationErrorsAtom);
27
+ const newErrors = new Map(sqlValidationErrors);
28
+ newErrors.delete(cellId);
29
+ store.set(sqlValidationErrorsAtom, newErrors);
30
+ }
31
+
32
+ export function setSqlValidationError({
33
+ cellId,
34
+ error,
35
+ dialect,
36
+ }: {
37
+ cellId: CellId;
38
+ error: string;
39
+ dialect: SupportedDialects | null;
40
+ }) {
41
+ const sqlValidationErrors = store.get(sqlValidationErrorsAtom);
42
+ const newErrors = new Map(sqlValidationErrors);
43
+
44
+ const errorResult: SQLValidationError =
45
+ dialect === "DuckDB" ? handleDuckdbError(error) : splitErrorMessage(error);
46
+
47
+ newErrors.set(cellId, errorResult);
48
+ store.set(sqlValidationErrorsAtom, newErrors);
49
+ }
50
+
51
+ function handleDuckdbError(error: string): SQLValidationError {
52
+ const { errorType, errorMessage } = splitErrorMessage(error);
53
+ let newErrorMessage = errorMessage;
54
+
55
+ // Extract the LINE and the rest of the message as codeblock, keep errorMessage as whatever is before
56
+ let codeblock: string | undefined;
57
+ const lineIndex = errorMessage.indexOf("LINE ");
58
+ if (lineIndex !== -1) {
59
+ codeblock = errorMessage.slice(Math.max(0, lineIndex)).trim();
60
+ newErrorMessage = errorMessage.slice(0, Math.max(0, lineIndex)).trim();
61
+ }
62
+
63
+ return {
64
+ errorType,
65
+ errorMessage: newErrorMessage,
66
+ codeblock,
67
+ };
68
+ }
69
+
70
+ function splitErrorMessage(error: string) {
71
+ const errorType = error.split(":")[0].trim();
72
+ const errorMessage = error.split(":").slice(1).join(":").trim();
73
+ return { errorType, errorMessage };
74
+ }
75
+
76
+ export const exportedForTesting = {
77
+ splitErrorMessage,
78
+ handleDuckdbError,
79
+ };
@@ -5,7 +5,8 @@ import { Button } from "@/components/ui/button";
5
5
  import { Checkbox } from "@/components/ui/checkbox";
6
6
  import { Tooltip, TooltipProvider } from "@/components/ui/tooltip";
7
7
  import { normalizeName } from "@/core/cells/names";
8
- import type { ConnectionName } from "@/core/datasets/engines";
8
+ import { getFeatureFlag } from "@/core/config/feature-flag";
9
+ import { type ConnectionName, DUCKDB_ENGINE } from "@/core/datasets/engines";
9
10
  import { useAutoGrowInputProps } from "@/hooks/useAutoGrowInputProps";
10
11
  import { formatSQL } from "../../format";
11
12
  import { languageAdapterState } from "../extension";
@@ -22,7 +23,7 @@ import {
22
23
  import type { LanguageMetadataOf } from "../types";
23
24
  import type { QuotePrefixKind } from "../utils/quotes";
24
25
  import { getQuotePrefix, MarkdownQuotePrefixTooltip } from "./markdown";
25
- import { SQLEngineSelect } from "./sql";
26
+ import { SQLEngineSelect, SQLModeSelect } from "./sql";
26
27
 
27
28
  const Divider = () => <div className="h-4 border-r border-border" />;
28
29
 
@@ -70,6 +71,8 @@ export const LanguagePanelComponent: React.FC<{
70
71
  updateSQLDialectFromConnection(view, engine);
71
72
  };
72
73
 
74
+ const sqlModeEnabled = getFeatureFlag("sql_mode");
75
+
73
76
  actions = (
74
77
  <div className="flex flex-1 gap-2 items-center">
75
78
  <label className="flex gap-2 items-center">
@@ -95,6 +98,9 @@ export const LanguagePanelComponent: React.FC<{
95
98
  onChange={switchEngine}
96
99
  />
97
100
  <div className="flex items-center gap-2 ml-auto">
101
+ {sqlModeEnabled && metadata.engine === DUCKDB_ENGINE && (
102
+ <SQLModeSelect />
103
+ )}
98
104
  <Tooltip content="Format SQL">
99
105
  <Button
100
106
  variant="text"