@marimo-team/islands 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 (99) hide show
  1. package/dist/{ConnectedDataExplorerComponent-DyqLQGPc.js → ConnectedDataExplorerComponent-KvtsEmcw.js} +2 -2
  2. package/dist/{ImageComparisonComponent-CQDGJfUA.js → ImageComparisonComponent-R2tjqLSx.js} +1 -1
  3. package/dist/{_baseUniq-B2Nna6Kt.js → _baseUniq-BykZEXIq.js} +1 -1
  4. package/dist/{any-language-editor-D-wq0tOG.js → any-language-editor-CMt4Y6oz.js} +1 -1
  5. package/dist/{architectureDiagram-W76B3OCA-C6tdnMBf.js → architectureDiagram-W76B3OCA-mQ3sJdEW.js} +4 -4
  6. package/dist/{blockDiagram-QIGZ2CNN-IagL8LCN.js → blockDiagram-QIGZ2CNN-BxLRv5EM.js} +5 -5
  7. package/dist/{c4Diagram-FPNF74CW-D3_lIWUP.js → c4Diagram-FPNF74CW-Cfz16aWq.js} +2 -2
  8. package/dist/{channel-DCJI_DKk.js → channel-zr1uJJ5g.js} +1 -1
  9. package/dist/{chunk-4BX2VUAB-B2DrODwN.js → chunk-4BX2VUAB-B8iHvpDe.js} +1 -1
  10. package/dist/{chunk-55IACEB6-BUWDsQ-t.js → chunk-55IACEB6-CJs4dL1H.js} +1 -1
  11. package/dist/{chunk-FMBD7UC4-BExPNFv1.js → chunk-FMBD7UC4-C5irHg20.js} +1 -1
  12. package/dist/{chunk-K7UQS3LO-Cixi-Yko.js → chunk-K7UQS3LO-B2XW75WS.js} +4 -4
  13. package/dist/{chunk-QN33PNHL-B83MtvER.js → chunk-QN33PNHL-BtwctqGa.js} +1 -1
  14. package/dist/{chunk-QZHKN3VN-CXvbu85X.js → chunk-QZHKN3VN-Sb8ZD0FY.js} +1 -1
  15. package/dist/{chunk-TVAH2DTR-CpiumCHg.js → chunk-TVAH2DTR-CEJ5zkLX.js} +3 -3
  16. package/dist/{chunk-TZMSLE5B-DIzaZjcI.js → chunk-TZMSLE5B-Ccm4T92V.js} +1 -1
  17. package/dist/{classDiagram-v2-RKCZMP56-DyN5HPdk.js → classDiagram-KNZD7YFC-DWTbT0ww.js} +2 -2
  18. package/dist/{classDiagram-KNZD7YFC-DyN5HPdk.js → classDiagram-v2-RKCZMP56-DWTbT0ww.js} +2 -2
  19. package/dist/{clone-DrJYap2i.js → clone-CDDiMerE.js} +1 -1
  20. package/dist/{cose-bilkent-S5V4N54A-D39b4WrQ.js → cose-bilkent-S5V4N54A-B74aLjZ_.js} +2 -2
  21. package/dist/{dagre-5GWH7T2D-BLjRxDpS.js → dagre-5GWH7T2D-Cie_wUzI.js} +6 -6
  22. package/dist/{data-grid-overlay-editor-DTALqerV.js → data-grid-overlay-editor-OsCMRzfP.js} +2 -2
  23. package/dist/{diagram-N5W7TBWH-MM8AIKGR.js → diagram-N5W7TBWH-CCeFeV2B.js} +5 -5
  24. package/dist/{diagram-QEK2KX5R-BZGarWuJ.js → diagram-QEK2KX5R-BPROiVV7.js} +3 -3
  25. package/dist/{diagram-S2PKOQOG-CnPinN9Q.js → diagram-S2PKOQOG-BkLFbUa_.js} +3 -3
  26. package/dist/{dockerfile-U8DnCJ4X.js → dockerfile-CuJXUZQ_.js} +1 -1
  27. package/dist/{erDiagram-AWTI2OKA-CvDVbxOO.js → erDiagram-AWTI2OKA-CpBWOTMK.js} +4 -4
  28. package/dist/{flowDiagram-PVAE7QVJ-C2uuBTZS.js → flowDiagram-PVAE7QVJ-C_X4bmq3.js} +5 -5
  29. package/dist/{ganttDiagram-OWAHRB6G-BEff10RF.js → ganttDiagram-OWAHRB6G-CruldwEp.js} +4 -4
  30. package/dist/{gitGraphDiagram-NY62KEGX-wggu0kb2.js → gitGraphDiagram-NY62KEGX-ZJnVxaQP.js} +4 -4
  31. package/dist/{glide-data-editor-Bqh5_dzJ.js → glide-data-editor-CnOBht4I.js} +3 -3
  32. package/dist/{graph-DKpp_wzf.js → graph-DjTtWtcG.js} +3 -3
  33. package/dist/{index-DzJ_YPCG.js → index-CZ9vIBEc.js} +3 -3
  34. package/dist/{index-DdfF_cLK.js → index-DSpjUDnr.js} +1 -1
  35. package/dist/{index-DW0BCGJE.js → index-DeOkA9fC.js} +1 -1
  36. package/dist/{index-4XruEJkp.js → index-saLjL5eo.js} +1 -1
  37. package/dist/{infoDiagram-STP46IZ2-DF7KW-Op.js → infoDiagram-STP46IZ2-RZgl96nR.js} +2 -2
  38. package/dist/{journeyDiagram-BIP6EPQ6-B_jmhmqd.js → journeyDiagram-BIP6EPQ6-9ytZy-zY.js} +3 -3
  39. package/dist/{kanban-definition-6OIFK2YF-B-M9FTyw.js → kanban-definition-6OIFK2YF-CpjPWkgX.js} +2 -2
  40. package/dist/{layout-C4oVYZZD.js → layout-BAvhX25D.js} +4 -4
  41. package/dist/{linear-C-HCGr0T.js → linear-BKZuvJrK.js} +1 -1
  42. package/dist/{main-B9x2-9f2.js → main-pE28kbH0.js} +37316 -36749
  43. package/dist/main.js +1 -1
  44. package/dist/{mermaid-BE4cM3Qs.js → mermaid-BGd7kEsf.js} +30 -30
  45. package/dist/{min-DTpHJ698.js → min-DmsBJf5S.js} +2 -2
  46. package/dist/{mindmap-definition-Q6HEUPPD-Cpd-hO1E.js → mindmap-definition-Q6HEUPPD-BerypnVD.js} +3 -3
  47. package/dist/{number-overlay-editor-CvURA2Ud.js → number-overlay-editor-A7ZQ6sSs.js} +2 -2
  48. package/dist/{pieDiagram-ADFJNKIX-D9f_f6fn.js → pieDiagram-ADFJNKIX-ChcQLZM2.js} +3 -3
  49. package/dist/{quadrantDiagram-LMRXKWRM-DgllE7xw.js → quadrantDiagram-LMRXKWRM-BOMxo-5G.js} +2 -2
  50. package/dist/{react-plotly-BU-JRJSi.js → react-plotly-8f0vAPdB.js} +1 -1
  51. package/dist/{requirementDiagram-4UW4RH46-Dk_G8eUb.js → requirementDiagram-4UW4RH46-rql0vkIr.js} +3 -3
  52. package/dist/{sankeyDiagram-GR3RE2ED-BhLIhDc1.js → sankeyDiagram-GR3RE2ED-BMz3hqw4.js} +1 -1
  53. package/dist/{sequenceDiagram-C3RYC4MD-DHoZdMFJ.js → sequenceDiagram-C3RYC4MD-DFK39LUK.js} +3 -3
  54. package/dist/{slides-component-DXAgdf7K.js → slides-component-BNsZuUgg.js} +1 -1
  55. package/dist/{stateDiagram-KXAO66HF-C1Ie-7Xf.js → stateDiagram-KXAO66HF-B_3SWOCl.js} +4 -4
  56. package/dist/{stateDiagram-v2-UMBNRL4Z--CRuIHtM.js → stateDiagram-v2-UMBNRL4Z-BWAYN6aU.js} +2 -2
  57. package/dist/style.css +1 -1
  58. package/dist/{time-yQjlGPwa.js → time-DCvYzQ5t.js} +2 -2
  59. package/dist/{timeline-definition-XQNQX7LJ-D_PjxB1B.js → timeline-definition-XQNQX7LJ-CSbxJ5mV.js} +1 -1
  60. package/dist/{treemap-75Q7IDZK--NYqQjUZ.js → treemap-75Q7IDZK-LMXGKln_.js} +5 -5
  61. package/dist/{vega-component-CCUOMM5K.js → vega-component-CRmon7pH.js} +2 -2
  62. package/dist/{xychartDiagram-6GGTOJPD-WLKsEnzs.js → xychartDiagram-6GGTOJPD-DaUD4dq3.js} +2 -2
  63. package/package.json +3 -3
  64. package/src/__mocks__/requests.ts +1 -0
  65. package/src/components/data-table/__tests__/columns.test.tsx +38 -0
  66. package/src/components/data-table/cell-hover-template/feature.ts +1 -1
  67. package/src/components/data-table/cell-hover-template/types.ts +1 -1
  68. package/src/components/data-table/columns.tsx +21 -2
  69. package/src/components/data-table/renderers.tsx +16 -8
  70. package/src/components/data-table/schemas.ts +16 -0
  71. package/src/components/editor/Cell.tsx +2 -0
  72. package/src/components/editor/errors/sql-validation-errors.tsx +34 -0
  73. package/src/components/editor/output/ConsoleOutput.tsx +13 -1
  74. package/src/components/editor/output/MarimoErrorOutput.tsx +60 -1
  75. package/src/core/ai/context/providers/cell-output.ts +1 -18
  76. package/src/core/codemirror/language/__tests__/extension.test.ts +24 -0
  77. package/src/core/codemirror/language/__tests__/sql-validation.test.ts +133 -0
  78. package/src/core/codemirror/language/languages/sql/sql-mode.ts +20 -0
  79. package/src/core/codemirror/language/languages/sql/sql.ts +90 -3
  80. package/src/core/codemirror/language/languages/sql/validation-errors.ts +79 -0
  81. package/src/core/codemirror/language/panel/panel.tsx +8 -2
  82. package/src/core/codemirror/language/panel/sql.tsx +81 -4
  83. package/src/core/config/feature-flag.tsx +3 -1
  84. package/src/core/datasets/request-registry.ts +17 -1
  85. package/src/core/islands/bridge.ts +1 -0
  86. package/src/core/islands/main.ts +1 -0
  87. package/src/core/kernel/messages.ts +1 -0
  88. package/src/core/network/requests-network.ts +7 -0
  89. package/src/core/network/requests-static.ts +1 -0
  90. package/src/core/network/requests-toasting.ts +1 -0
  91. package/src/core/network/types.ts +2 -0
  92. package/src/core/wasm/bridge.ts +5 -0
  93. package/src/core/websocket/useMarimoWebSocket.tsx +4 -0
  94. package/src/plugins/core/registerReactComponent.tsx +23 -19
  95. package/src/plugins/impl/DataTablePlugin.tsx +11 -4
  96. package/src/plugins/impl/data-frames/DataFramePlugin.tsx +17 -5
  97. package/src/stories/dataframe.stories.tsx +2 -0
  98. package/src/utils/__tests__/dom.test.ts +167 -0
  99. package/src/utils/dom.ts +55 -0
@@ -1,7 +1,7 @@
1
1
  import { R as q, r as ln, o as P, q as X, C as Z, u as D, v as fn } from "./timer-B0-z63CM.js";
2
- import { c as hn, a as on } from "./linear-C-HCGr0T.js";
2
+ import { c as hn, a as on } from "./linear-BKZuvJrK.js";
3
3
  import { i as bn } from "./init-DxzjmxYy.js";
4
- import { W as gn, X as d, Y as v, Z as k, _ as x, $ as L, a0 as yn, a1 as R, a2 as H, a3 as W, a4 as pn, a5 as mn, a6 as wn, a7 as Mn, a8 as dn, a9 as vn, aa as kn, ab as $, ac as A, ad as B, ae as K, af as _, ag as j, ah as xn } from "./main-B9x2-9f2.js";
4
+ import { W as gn, X as d, Y as v, Z as k, _ as x, $ as L, a0 as yn, a1 as R, a2 as H, a3 as W, a4 as pn, a5 as mn, a6 as wn, a7 as Mn, a8 as dn, a9 as vn, aa as kn, ab as $, ac as A, ad as B, ae as K, af as _, ag as j, ah as xn } from "./main-pE28kbH0.js";
5
5
  function Pn(n, t) {
6
6
  let i;
7
7
  if (t === void 0)
@@ -1,4 +1,4 @@
1
- import { _ as s, c as xt, l as E, d as j, V as kt, W as vt, X as _t, Y as bt, D as wt, $ as St, z as Et } from "./mermaid-BE4cM3Qs.js";
1
+ import { _ as s, c as xt, l as E, d as j, V as kt, W as vt, X as _t, Y as bt, D as wt, $ as St, z as Et } from "./mermaid-BGd7kEsf.js";
2
2
  import { d as nt } from "./arc-BOhn-m2C.js";
3
3
  var Q = (function() {
4
4
  var n = /* @__PURE__ */ s(function(x, r, a, c) {
@@ -2,11 +2,11 @@ var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
  var _a2, _b, _c2, _d2, _e, _f2, _g, _h2, _i2, _j, _k, _l2, _m2, _n2, _o2, _p2, _q;
5
- import { d_ as Kl, bQ as Vl, al as Ma, aT as Hl, u as _t, aO as Wl, ao as Z, bk as jl, bl as zl, k as ql, n as Ni, m as Da, am as Zs, d$ as Fa, aK as Yl, b0 as Ga, e0 as Ua, aM as Xl, b2 as wi, an as Jl, e1 as Ql, bg as Zl, e2 as eu, aP as Ie, q as vt, ar as _i, b3 as tu, e3 as H, e4 as Re } from "./main-B9x2-9f2.js";
6
- import { g as ei, d as bt, k as nu, v as W, l as Ba, m as ru, n as su, a as Ka, c as x, i as qe, r as ae, f as ke, o as z } from "./_baseUniq-B2Nna6Kt.js";
7
- import { m as k, f as Lt, h as I, e as ti, l as Ot, d as iu } from "./min-DTpHJ698.js";
8
- import { ad as P } from "./mermaid-BE4cM3Qs.js";
9
- import { c as te } from "./clone-DrJYap2i.js";
5
+ import { d_ as Kl, bQ as Vl, al as Ma, aT as Hl, u as _t, aO as Wl, ao as Z, bk as jl, bl as zl, k as ql, n as Ni, m as Da, am as Zs, d$ as Fa, aK as Yl, b0 as Ga, e0 as Ua, aM as Xl, b2 as wi, an as Jl, e1 as Ql, bg as Zl, e2 as eu, aP as Ie, q as vt, ar as _i, b3 as tu, e3 as H, e4 as Re } from "./main-pE28kbH0.js";
6
+ import { g as ei, d as bt, k as nu, v as W, l as Ba, m as ru, n as su, a as Ka, c as x, i as qe, r as ae, f as ke, o as z } from "./_baseUniq-BykZEXIq.js";
7
+ import { m as k, f as Lt, h as I, e as ti, l as Ot, d as iu } from "./min-DmsBJf5S.js";
8
+ import { ad as P } from "./mermaid-BGd7kEsf.js";
9
+ import { c as te } from "./clone-CDDiMerE.js";
10
10
  var au = Object.prototype, ou = au.hasOwnProperty, Ee = Kl(function(n, e) {
11
11
  if (Vl(e) || Ma(e)) {
12
12
  Hl(e, _t(e), n);
@@ -1,5 +1,5 @@
1
- import { D as V, F as K, A as Z, G as z, H as R, I as G, j as d, E as W, J as U, r as k, K as j, M, N as $, L as x, O as _, P as B, Q, S as q, T as J, U as Y, V as X } from "./main-B9x2-9f2.js";
2
- import { M as m, V as ee } from "./index-DzJ_YPCG.js";
1
+ import { D as V, F as K, A as Z, G as z, H as R, I as G, j as d, E as W, J as U, r as k, K as j, M, N as $, L as x, O as _, P as B, Q, S as q, T as J, U as Y, V as X } from "./main-pE28kbH0.js";
2
+ import { M as m, V as ee } from "./index-CZ9vIBEc.js";
3
3
  function te(e) {
4
4
  return e.data && "url" in e.data && (e.data.url = V(e.data.url).href), e;
5
5
  }
@@ -1,9 +1,9 @@
1
1
  var _a, _b, _c, _d, _e, _f, _g, _h, _i2, _j;
2
- import { _ as a, s as ei, g as si, t as Lt, q as ni, a as ai, b as oi, l as Et, K as ri, e as hi, z as li, G as xt, F as It, H as ci, M as ui, i as gi, ac as xi } from "./mermaid-BE4cM3Qs.js";
2
+ import { _ as a, s as ei, g as si, t as Lt, q as ni, a as ai, b as oi, l as Et, K as ri, e as hi, z as li, G as xt, F as It, H as ci, M as ui, i as gi, ac as xi } from "./mermaid-BGd7kEsf.js";
3
3
  import { i as di } from "./init-DxzjmxYy.js";
4
4
  import { o as fi } from "./ordinal-CYN5qNoq.js";
5
5
  import { r as pi } from "./range-DdOGybNB.js";
6
- import { l as Tt } from "./linear-C-HCGr0T.js";
6
+ import { l as Tt } from "./linear-BKZuvJrK.js";
7
7
  import { l as Dt } from "./timer-B0-z63CM.js";
8
8
  function ht() {
9
9
  var t = fi().unknown(void 0), i = t.domain, e = t.range, s = 0, n = 1, d, g, m = false, S = 0, D = 0, v = 0.5;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.16.1",
3
+ "version": "0.16.2",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -245,8 +245,8 @@
245
245
  "storybook": "^9.1.3",
246
246
  "stylelint": "^16.23.1",
247
247
  "stylelint-config-standard": "^36.0.1",
248
- "tailwindcss": "^4.1.12",
249
- "vite": "^6.3.5",
248
+ "tailwindcss": "^4.1.13",
249
+ "vite": "^6.3.6",
250
250
  "vite-plugin-top-level-await": "^1.6.0",
251
251
  "vite-plugin-wasm": "^3.5.0",
252
252
  "vite-tsconfig-paths": "^5.1.4",
@@ -40,6 +40,7 @@ export const MockRequestClient = {
40
40
  previewSQLTable: vi.fn().mockResolvedValue({}),
41
41
  previewSQLTableList: vi.fn().mockResolvedValue({ tables: [] }),
42
42
  previewDataSourceConnection: vi.fn().mockResolvedValue({}),
43
+ validateSQL: vi.fn().mockResolvedValue({}),
43
44
  openFile: vi.fn().mockResolvedValue({}),
44
45
  getUsageStats: vi.fn().mockResolvedValue({}),
45
46
  sendPdb: vi.fn().mockResolvedValue({}),
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { render } from "@testing-library/react";
4
4
  import { describe, expect, it, test } from "vitest";
5
+ import { TooltipProvider } from "@/components/ui/tooltip";
5
6
  import { generateColumns, inferFieldTypes } from "../columns";
6
7
  import { getMimeValues, isMimeValue, MimeCell } from "../mime-cell";
7
8
  import type { FieldTypesWithExternalType } from "../types";
@@ -245,6 +246,43 @@ describe("generateColumns", () => {
245
246
  expect(columns[0].id).toBe("name");
246
247
  expect(columns[1].id).toBe("age");
247
248
  });
249
+
250
+ it("should render header with tooltip when headerTooltip is provided", () => {
251
+ const columns = generateColumns({
252
+ rowHeaders: [],
253
+ selection: null,
254
+ fieldTypes,
255
+ headerTooltip: { name: "Custom Name Tooltip" },
256
+ });
257
+
258
+ // Get the header function for the first column
259
+ const headerFunction = columns[0].header;
260
+ expect(headerFunction).toBeTypeOf("function");
261
+
262
+ const mockColumn = {
263
+ id: "name",
264
+ getCanSort: () => false,
265
+ getCanFilter: () => false,
266
+ columnDef: {
267
+ meta: {
268
+ dtype: "string",
269
+ dataType: "string",
270
+ },
271
+ },
272
+ };
273
+
274
+ const { container } = render(
275
+ <TooltipProvider>
276
+ {/* @ts-expect-error: mock column and header function */}
277
+ {headerFunction({ column: mockColumn })}
278
+ </TooltipProvider>,
279
+ );
280
+
281
+ expect(container.textContent).toContain("name");
282
+ // The tooltip functionality is tested by verifying that the header renders correctly
283
+ // when headerTooltip is provided.
284
+ expect(container.firstChild).toBeTruthy();
285
+ });
248
286
  });
249
287
 
250
288
  describe("MimeCell", () => {
@@ -1,4 +1,4 @@
1
- /* Copyright 2025 Marimo. All rights reserved. */
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
2
  "use no memo";
3
3
 
4
4
  import type { InitialTableState, TableFeature } from "@tanstack/react-table";
@@ -1,4 +1,4 @@
1
- /* Copyright 2025 Marimo. All rights reserved. */
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
2
  /* eslint-disable @typescript-eslint/no-empty-interface */
3
3
 
4
4
  export interface CellHoverTemplateTableState {
@@ -17,6 +17,7 @@ import { JsonOutput } from "../editor/output/JsonOutput";
17
17
  import { Button } from "../ui/button";
18
18
  import { Checkbox } from "../ui/checkbox";
19
19
  import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
20
+ import { Tooltip } from "../ui/tooltip";
20
21
  import { DataTableColumnHeader } from "./column-header";
21
22
  import type { ColumnChartSpecModel } from "./column-summary/chart-spec-model";
22
23
  import { TableColumnSummary } from "./column-summary/column-summary";
@@ -103,6 +104,7 @@ export function generateColumns<T>({
103
104
  chartSpecModel,
104
105
  textJustifyColumns,
105
106
  wrappedColumns,
107
+ headerTooltip,
106
108
  showDataTypes,
107
109
  calculateTopKRows,
108
110
  }: {
@@ -112,6 +114,7 @@ export function generateColumns<T>({
112
114
  chartSpecModel?: ColumnChartSpecModel<unknown>;
113
115
  textJustifyColumns?: Record<string, "left" | "center" | "right">;
114
116
  wrappedColumns?: string[];
117
+ headerTooltip?: Record<string, string>;
115
118
  showDataTypes?: boolean;
116
119
  calculateTopKRows?: CalculateTopKRows;
117
120
  }): Array<ColumnDef<T>> {
@@ -165,6 +168,7 @@ export function generateColumns<T>({
165
168
  header: ({ column }) => {
166
169
  const stats = chartSpecModel?.getColumnStats(key);
167
170
  const dtype = column.columnDef.meta?.dtype;
171
+ const headerTitle = headerTooltip?.[key];
168
172
  const dtypeHeader =
169
173
  showDataTypes && dtype ? (
170
174
  <div className="flex flex-row gap-1">
@@ -179,14 +183,29 @@ export function generateColumns<T>({
179
183
 
180
184
  const headerWithType = (
181
185
  <div className="flex flex-col">
182
- <span className="font-bold">{key === "" ? " " : key}</span>
186
+ <span
187
+ className={cn(
188
+ "font-bold",
189
+ headerTitle && "underline decoration-dotted",
190
+ )}
191
+ >
192
+ {key === "" ? " " : key}
193
+ </span>
183
194
  {dtypeHeader}
184
195
  </div>
185
196
  );
186
197
 
198
+ const headerWithTooltip = headerTitle ? (
199
+ <Tooltip content={headerTitle} delayDuration={300}>
200
+ {headerWithType}
201
+ </Tooltip>
202
+ ) : (
203
+ headerWithType
204
+ );
205
+
187
206
  const dataTableColumnHeader = (
188
207
  <DataTableColumnHeader
189
- header={headerWithType}
208
+ header={headerWithTooltip}
190
209
  column={column}
191
210
  calculateTopKRows={calculateTopKRows}
192
211
  />
@@ -106,9 +106,9 @@ export const DataTableBody = <TData,>({
106
106
  const s = renderUnknownValue({ value: v, nullAsEmptyString: true });
107
107
  idToValue.set(c.column.id, s);
108
108
  }
109
- return template.replace(variableRegex, (_substr, varName: string) => {
109
+ return template.replaceAll(variableRegex, (_substr, varName: string) => {
110
110
  const val = idToValue.get(varName);
111
- return val !== undefined ? val : `{{${varName}}}`;
111
+ return val === undefined ? `{{${varName}}}` : val;
112
112
  });
113
113
  }
114
114
 
@@ -154,6 +154,8 @@ export const DataTableBody = <TData,>({
154
154
  }
155
155
  };
156
156
 
157
+ const hoverTemplate = table.getState().cellHoverTemplate || null;
158
+
157
159
  return (
158
160
  <TableBody onKeyDown={handleCellsKeyDown} ref={tableRef}>
159
161
  {table.getRowModel().rows?.length ? (
@@ -165,12 +167,18 @@ export const DataTableBody = <TData,>({
165
167
  const isRowViewedInPanel =
166
168
  rowViewerPanelOpen && viewedRowIdx === rowIndex;
167
169
 
168
- // Compute hover title once per row using this row's cells (visible or hidden)
169
- const hoverTemplate = table.getState().cellHoverTemplate || null;
170
- const rowCells = row.getAllCells();
171
- const rowTitle = hoverTemplate
172
- ? applyHoverTemplate(hoverTemplate, rowCells)
173
- : undefined;
170
+ // Compute hover title once per row using all visible cells
171
+ let rowTitle: string | undefined;
172
+ if (hoverTemplate) {
173
+ const visibleCells = row.getVisibleCells?.() ?? [
174
+ ...row.getLeftVisibleCells(),
175
+ ...row.getCenterVisibleCells(),
176
+ ...row.getRightVisibleCells(),
177
+ ];
178
+ rowTitle = hoverTemplate
179
+ ? applyHoverTemplate(hoverTemplate, visibleCells)
180
+ : undefined;
181
+ }
174
182
 
175
183
  return (
176
184
  <TableRow
@@ -0,0 +1,16 @@
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
+
3
+ import z from "zod";
4
+ import { rpc } from "@/plugins/core/rpc";
5
+
6
+ export type DownloadAsArgs = (req: {
7
+ format: "csv" | "json" | "parquet";
8
+ }) => Promise<string>;
9
+
10
+ export const DownloadAsSchema = rpc
11
+ .input(
12
+ z.object({
13
+ format: z.enum(["csv", "json", "parquet"]),
14
+ }),
15
+ )
16
+ .output(z.string());
@@ -80,6 +80,7 @@ import { useDeleteCellCallback } from "./cell/useDeleteCell";
80
80
  import { useRunCell } from "./cell/useRunCells";
81
81
  import { HideCodeButton } from "./code/readonly-python-code";
82
82
  import { cellDomProps } from "./common";
83
+ import { SqlValidationErrorBanner } from "./errors/sql-validation-errors";
83
84
  import { useCellNavigationProps } from "./navigation/navigation";
84
85
  import {
85
86
  useTemporarilyShownCode,
@@ -653,6 +654,7 @@ const EditableCellComponent = ({
653
654
  )}
654
655
  </div>
655
656
  </div>
657
+ <SqlValidationErrorBanner cellId={cellId} />
656
658
  {cellOutput === "below" && outputArea}
657
659
  {cellRuntime.serialization && (
658
660
  <div className="py-1 px-2 flex items-center justify-end gap-2 last:rounded-b">
@@ -0,0 +1,34 @@
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
+
3
+ import { AlertCircleIcon } from "lucide-react";
4
+ import type { CellId } from "@/core/cells/ids";
5
+ import { useSqlValidationErrorsForCell } from "@/core/codemirror/language/languages/sql/validation-errors";
6
+
7
+ export const SqlValidationErrorBanner = ({ cellId }: { cellId: CellId }) => {
8
+ const error = useSqlValidationErrorsForCell(cellId);
9
+
10
+ if (!error) {
11
+ return;
12
+ }
13
+
14
+ return (
15
+ <div className="p-3 text-sm flex flex-col text-muted-foreground gap-1.5 bg-destructive/5">
16
+ <div className="flex items-start gap-1.5">
17
+ <AlertCircleIcon size={13} className="mt-[3px] text-destructive" />
18
+ <p>
19
+ <span className="font-bold text-destructive">{error.errorType}:</span>{" "}
20
+ {error.errorMessage}
21
+ </p>
22
+ </div>
23
+
24
+ {error.codeblock && (
25
+ <pre
26
+ lang="sql"
27
+ className="text-xs bg-muted rounded p-2 pb-0 mx-3 overflow-x-auto font-mono whitespace-pre-wrap"
28
+ >
29
+ {error.codeblock}
30
+ </pre>
31
+ )}
32
+ </div>
33
+ );
34
+ };
@@ -14,6 +14,7 @@ import type { OutputMessage } from "@/core/kernel/messages";
14
14
  import { useSelectAllContent } from "@/hooks/useSelectAllContent";
15
15
  import { cn } from "@/utils/cn";
16
16
  import { copyToClipboard } from "@/utils/copy";
17
+ import { ansiToPlainText, parseHtmlContent } from "@/utils/dom";
17
18
  import { invariant } from "@/utils/invariant";
18
19
  import { Strings } from "@/utils/strings";
19
20
  import { NameCellContentEditable } from "../actions/name-cell-input";
@@ -124,7 +125,18 @@ const ConsoleOutputInternal = (props: Props): React.ReactNode => {
124
125
  onClick={() => {
125
126
  const text = reversedOutputs
126
127
  .filter((output) => output.channel !== "pdb")
127
- .map((output) => Strings.asString(output.data))
128
+ .map((output) => {
129
+ // If starts with `<`, then assume it's HTML
130
+ if (
131
+ typeof output.data === "string" &&
132
+ output.data.startsWith("<")
133
+ ) {
134
+ return parseHtmlContent(output.data);
135
+ }
136
+
137
+ // Otherwise, convert the ANSI to HTML, then parse as HTML
138
+ return ansiToPlainText(Strings.asString(output.data));
139
+ })
128
140
  .join("\n");
129
141
  void copyToClipboard(text);
130
142
  }}
@@ -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
+ });