@marimo-team/islands 0.23.16-dev51 → 0.23.16-dev53

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 (30) hide show
  1. package/dist/{chat-ui-1e7PcW56.js → chat-ui-9neL0bYw.js} +2 -2
  2. package/dist/{common-CD6phlRu.js → common-BxtRKijk.js} +2 -2
  3. package/dist/{html-to-image-3w0AakWW.js → html-to-image-DzQw9y4M.js} +2113 -2103
  4. package/dist/main.js +7 -5
  5. package/dist/{process-output-BRwR4QGs.js → process-output-tYmEI9FZ.js} +1 -1
  6. package/dist/{reveal-component-DmGGKS8z.js → reveal-component-DuXe-jZN.js} +2 -2
  7. package/dist/style.css +1 -1
  8. package/package.json +1 -1
  9. package/src/__mocks__/requests.ts +1 -0
  10. package/src/components/editor/connections/__tests__/quick-add-data-sources.test.tsx +113 -0
  11. package/src/components/editor/connections/add-connection-dialog.tsx +2 -0
  12. package/src/components/editor/connections/quick-add-data-sources.tsx +106 -0
  13. package/src/core/codemirror/language/__tests__/sql.test.ts +191 -7
  14. package/src/core/codemirror/language/languages/sql/sql.ts +41 -13
  15. package/src/core/datasets/data-source-discovery.ts +5 -0
  16. package/src/core/datasets/request-registry.ts +11 -0
  17. package/src/core/islands/bootstrap.ts +1 -0
  18. package/src/core/islands/bridge.ts +1 -0
  19. package/src/core/kernel/messages.ts +2 -0
  20. package/src/core/network/__tests__/requests-lazy.test.ts +15 -0
  21. package/src/core/network/__tests__/requests-network.test.ts +14 -0
  22. package/src/core/network/requests-lazy.ts +1 -0
  23. package/src/core/network/requests-network.ts +8 -0
  24. package/src/core/network/requests-static.ts +1 -0
  25. package/src/core/network/requests-toasting.tsx +1 -0
  26. package/src/core/network/types.ts +2 -0
  27. package/src/core/wasm/bridge.ts +10 -0
  28. package/src/core/websocket/useMarimoKernelConnection.tsx +4 -0
  29. package/src/hooks/__tests__/useDataSourceDiscovery.test.ts +80 -0
  30. package/src/hooks/useDataSourceDiscovery.ts +18 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.23.16-dev51",
3
+ "version": "0.23.16-dev53",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -41,6 +41,7 @@ export const MockRequestClient = {
41
41
  previewSQLTableList: vi.fn().mockResolvedValue({ tables: [] }),
42
42
  previewSQLSchemaList: vi.fn().mockResolvedValue({ schemas: [] }),
43
43
  previewDataSourceConnection: vi.fn().mockResolvedValue({}),
44
+ discoverDataSources: vi.fn().mockResolvedValue({}),
44
45
  validateSQL: vi.fn().mockResolvedValue({}),
45
46
  openFile: vi.fn().mockResolvedValue({}),
46
47
  getUsageStats: vi.fn().mockResolvedValue({}),
@@ -0,0 +1,113 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import type { DetectedDataSource } from "@/core/datasets/data-source-discovery";
6
+ import { QuickAddDataSources } from "../quick-add-data-sources";
7
+
8
+ const sources: DetectedDataSource[] = [
9
+ {
10
+ id: "postgres-libpq-environment",
11
+ integration: "postgres",
12
+ category: "database",
13
+ displayName: "PostgreSQL",
14
+ confidence: "high",
15
+ origins: [{ type: "environment", label: "Kernel environment" }],
16
+ configuration: [
17
+ {
18
+ field: "Host",
19
+ value: { kind: "environment-variable", name: "PGHOST" },
20
+ },
21
+ {
22
+ field: "Username",
23
+ value: { kind: "environment-variable", name: "PGUSER" },
24
+ },
25
+ {
26
+ field: "Database",
27
+ value: { kind: "environment-variable", name: "PGDATABASE" },
28
+ },
29
+ ],
30
+ code: "engine = create_engine()",
31
+ },
32
+ {
33
+ id: "pyiceberg-prod",
34
+ integration: "pyiceberg",
35
+ category: "catalog",
36
+ displayName: "PyIceberg (prod)",
37
+ confidence: "high",
38
+ origins: [
39
+ {
40
+ type: "configuration",
41
+ label: "Resolved PyIceberg configuration",
42
+ },
43
+ ],
44
+ configuration: [
45
+ {
46
+ field: "Catalog",
47
+ value: { kind: "safe-literal", value: "prod" },
48
+ },
49
+ {
50
+ field: "Type",
51
+ value: { kind: "safe-literal", value: "REST" },
52
+ },
53
+ ],
54
+ code: 'catalog = load_catalog("prod")',
55
+ },
56
+ ];
57
+
58
+ describe("QuickAddDataSources", () => {
59
+ it("does not render an empty section", () => {
60
+ const { container } = render(
61
+ <QuickAddDataSources sources={[]} onAdd={vi.fn()} />,
62
+ );
63
+
64
+ expect(container).toBeEmptyDOMElement();
65
+ });
66
+
67
+ it("renders detected sources as clickable tags", () => {
68
+ const onAdd = vi.fn();
69
+
70
+ render(<QuickAddDataSources sources={sources} onAdd={onAdd} />);
71
+ fireEvent.click(
72
+ screen.getByRole("button", {
73
+ name: "Add PostgreSQL connection",
74
+ }),
75
+ );
76
+
77
+ expect(screen.getByText("Quick add")).toBeInTheDocument();
78
+ expect(onAdd).toHaveBeenCalledWith(sources[0]);
79
+ });
80
+
81
+ it("shows environment references on hover", async () => {
82
+ render(<QuickAddDataSources sources={sources} onAdd={vi.fn()} />);
83
+ const tag = screen.getByRole("button", {
84
+ name: "Add PostgreSQL connection",
85
+ });
86
+
87
+ fireEvent.pointerMove(tag);
88
+ fireEvent.mouseOver(tag);
89
+
90
+ await waitFor(() => {
91
+ expect(screen.getAllByText('os.environ["PGHOST"]')).not.toHaveLength(0);
92
+ });
93
+ expect(screen.getAllByText('os.environ["PGUSER"]')).not.toHaveLength(0);
94
+ expect(screen.getAllByText('os.environ["PGDATABASE"]')).not.toHaveLength(0);
95
+ });
96
+
97
+ it("shows safe configuration metadata on hover", async () => {
98
+ render(<QuickAddDataSources sources={sources} onAdd={vi.fn()} />);
99
+ const tag = screen.getByRole("button", {
100
+ name: "Add PyIceberg (prod) connection",
101
+ });
102
+
103
+ fireEvent.pointerMove(tag);
104
+ fireEvent.mouseOver(tag);
105
+
106
+ await waitFor(() => {
107
+ expect(
108
+ screen.getAllByText("Detected from Resolved PyIceberg configuration"),
109
+ ).not.toHaveLength(0);
110
+ });
111
+ expect(screen.getAllByText("REST")).not.toHaveLength(0);
112
+ });
113
+ });
@@ -12,6 +12,7 @@ import {
12
12
  import { ExternalLink } from "@/components/ui/links";
13
13
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
14
14
  import { AddDatabaseForm } from "./database/add-database-form";
15
+ import { AutoDiscoveredDataSources } from "./quick-add-data-sources";
15
16
  import { AddStorageForm } from "./storage/add-storage-form";
16
17
 
17
18
  type ConnectionTab = "databases" | "storage";
@@ -86,6 +87,7 @@ export const AddConnectionDialogContent: React.FC<{
86
87
  <span className="block">{codeSnippetHint}</span>
87
88
  </DialogDescription>
88
89
  </DialogHeader>
90
+ <AutoDiscoveredDataSources onSubmit={onClose} />
89
91
  <Tabs
90
92
  value={activeTab}
91
93
  onValueChange={(v) => setActiveTab(v as ConnectionTab)}
@@ -0,0 +1,106 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { PlusIcon, SparklesIcon } from "lucide-react";
4
+ import { Tooltip, TooltipProvider } from "@/components/ui/tooltip";
5
+ import type { DetectedDataSource } from "@/core/datasets/data-source-discovery";
6
+ import { useDataSourceDiscovery } from "@/hooks/useDataSourceDiscovery";
7
+ import { useInsertCode } from "./components";
8
+
9
+ export const QuickAddDataSources: React.FC<{
10
+ sources: DetectedDataSource[];
11
+ onAdd: (source: DetectedDataSource) => void;
12
+ }> = ({ sources, onAdd }) => {
13
+ if (sources.length === 0) {
14
+ return null;
15
+ }
16
+
17
+ return (
18
+ <section
19
+ aria-labelledby="quick-add-data-sources-title"
20
+ className="rounded-md border bg-muted/30 px-3 py-2"
21
+ >
22
+ <div className="flex flex-wrap items-center gap-2">
23
+ <div className="mr-1 flex items-center gap-1.5">
24
+ <SparklesIcon className="h-3.5 w-3.5 text-muted-foreground" />
25
+ <h3 id="quick-add-data-sources-title" className="text-sm font-medium">
26
+ Quick add
27
+ </h3>
28
+ </div>
29
+ <TooltipProvider delayDuration={200}>
30
+ {sources.map((source) => (
31
+ <Tooltip
32
+ key={source.id}
33
+ side="bottom"
34
+ content={<DetectedDataSourceDetails source={source} />}
35
+ >
36
+ <button
37
+ type="button"
38
+ aria-label={`Add ${source.displayName} connection`}
39
+ className="inline-flex items-center gap-1 rounded-full border border-(--blue-8) bg-(--blue-2) px-2.5 py-1 text-xs font-semibold text-(--blue-11) transition-colors hover:bg-(--blue-3) focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2"
40
+ onClick={() => onAdd(source)}
41
+ >
42
+ <PlusIcon className="h-3 w-3" />
43
+ {source.displayName}
44
+ </button>
45
+ </Tooltip>
46
+ ))}
47
+ </TooltipProvider>
48
+ </div>
49
+ </section>
50
+ );
51
+ };
52
+
53
+ const DetectedDataSourceDetails: React.FC<{
54
+ source: DetectedDataSource;
55
+ }> = ({ source }) => (
56
+ <div className="min-w-64 space-y-2 py-1">
57
+ <div>
58
+ <div className="font-medium">{source.displayName}</div>
59
+ <div className="text-xs text-muted-foreground">
60
+ Detected from{" "}
61
+ {source.origins.map((origin) => origin.label).join(" and ")}
62
+ </div>
63
+ </div>
64
+ <dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs">
65
+ {source.configuration.map((item) => (
66
+ <div
67
+ className="contents"
68
+ key={
69
+ item.value.kind === "environment-variable"
70
+ ? item.value.name
71
+ : `${item.field}:${item.value.value}`
72
+ }
73
+ >
74
+ <dt className="text-muted-foreground">{item.field}</dt>
75
+ <dd>
76
+ <code>
77
+ {item.value.kind === "environment-variable"
78
+ ? `os.environ["${item.value.name}"]`
79
+ : item.value.value}
80
+ </code>
81
+ </dd>
82
+ </div>
83
+ ))}
84
+ </dl>
85
+ <div className="text-xs text-muted-foreground">
86
+ Click to add a configured cell.
87
+ </div>
88
+ </div>
89
+ );
90
+
91
+ export const AutoDiscoveredDataSources: React.FC<{
92
+ onSubmit: () => void;
93
+ }> = ({ onSubmit }) => {
94
+ const insertCode = useInsertCode();
95
+ const { data } = useDataSourceDiscovery();
96
+
97
+ return (
98
+ <QuickAddDataSources
99
+ sources={data ?? []}
100
+ onAdd={(source) => {
101
+ insertCode(source.code);
102
+ onSubmit();
103
+ }}
104
+ />
105
+ );
106
+ };
@@ -27,6 +27,7 @@ import type { HotkeyProvider } from "@/core/hotkeys/hotkeys";
27
27
  import { store } from "@/core/state/jotai";
28
28
  import type { PlaceholderType } from "../../config/types";
29
29
  import { TestSQLCompletionStore } from "../languages/sql/completion-store";
30
+ import * as sqlMode from "../languages/sql/sql-mode";
30
31
  import {
31
32
  exportedForTesting,
32
33
  SQLLanguageAdapter,
@@ -971,13 +972,63 @@ describe("SQL analysis features", () => {
971
972
  });
972
973
 
973
974
  describe("CustomSqlParser", () => {
975
+ beforeEach(() => {
976
+ store.set(dataSourceConnectionsAtom, {
977
+ connectionsMap: new Map(),
978
+ latestEngineSelected: DUCKDB_ENGINE,
979
+ });
980
+ });
981
+
974
982
  afterEach(() => {
975
983
  vi.useRealTimers();
976
984
  vi.restoreAllMocks();
985
+ store.set(dataSourceConnectionsAtom, {
986
+ connectionsMap: new Map(),
987
+ latestEngineSelected: DUCKDB_ENGINE,
988
+ });
977
989
  });
978
990
 
991
+ const createNamedConnectionState = ({
992
+ doc,
993
+ engine,
994
+ dialect,
995
+ }: {
996
+ doc: string;
997
+ engine: ConnectionName;
998
+ dialect: string;
999
+ }) => {
1000
+ store.set(dataSourceConnectionsAtom, {
1001
+ connectionsMap: new Map([
1002
+ [
1003
+ engine,
1004
+ {
1005
+ name: engine,
1006
+ dialect,
1007
+ display_name: engine,
1008
+ source: dialect,
1009
+ databases: [],
1010
+ },
1011
+ ],
1012
+ ]),
1013
+ latestEngineSelected: engine,
1014
+ });
1015
+ return EditorState.create({
1016
+ doc,
1017
+ extensions: [
1018
+ languageMetadataField.init(() => ({
1019
+ dataframeName: "_df",
1020
+ quotePrefix: "f",
1021
+ commentLines: [],
1022
+ showOutput: true,
1023
+ engine,
1024
+ })),
1025
+ ],
1026
+ });
1027
+ };
1028
+
979
1029
  it("uses backend DuckDB validation", async () => {
980
1030
  vi.useFakeTimers();
1031
+ vi.spyOn(sqlMode, "getSQLMode").mockReturnValue("validate");
981
1032
  const error = {
982
1033
  message: "Backend syntax error",
983
1034
  line: 1,
@@ -1009,13 +1060,146 @@ describe("CustomSqlParser", () => {
1009
1060
  await vi.runAllTimersAsync();
1010
1061
 
1011
1062
  await expect(result).resolves.toEqual([error]);
1012
- expect(request).toHaveBeenCalledWith(
1013
- expect.objectContaining({
1014
- engine: DUCKDB_ENGINE,
1015
- onlyParse: true,
1016
- query: "SELECT",
1017
- }),
1018
- );
1063
+ // No connection is registered for the internal engine, so the dialect
1064
+ // falls back to DEFAULT_PARSER_DIALECT ("DuckDB").
1065
+ expect(request).toHaveBeenCalledWith({
1066
+ dialect: "DuckDB",
1067
+ engine: DUCKDB_ENGINE,
1068
+ onlyParse: false,
1069
+ query: "SELECT",
1070
+ });
1071
+ });
1072
+
1073
+ it("resolves superseded validation requests instead of leaving them pending", async () => {
1074
+ vi.useFakeTimers();
1075
+ vi.spyOn(sqlMode, "getSQLMode").mockReturnValue("validate");
1076
+ const error = {
1077
+ message: "Backend syntax error",
1078
+ line: 1,
1079
+ column: 1,
1080
+ severity: "error" as const,
1081
+ };
1082
+ vi.spyOn(ValidateSQL, "request").mockResolvedValue({
1083
+ error: null,
1084
+ parse_result: { success: false, errors: [error] },
1085
+ request_id: "request-id",
1086
+ validate_result: null,
1087
+ });
1088
+ const state = EditorState.create({
1089
+ doc: "SELECT",
1090
+ extensions: [
1091
+ languageMetadataField.init(() => ({
1092
+ dataframeName: "_df",
1093
+ quotePrefix: "f",
1094
+ commentLines: [],
1095
+ showOutput: true,
1096
+ engine: DUCKDB_ENGINE,
1097
+ })),
1098
+ ],
1099
+ });
1100
+ const parser = new exportedForTesting.CustomSqlParser();
1101
+ parser.setFocusState(true);
1102
+
1103
+ // Simulate a rapid edit: a second validation call arrives before the
1104
+ // first call's internal debounce timer has fired. The first call should
1105
+ // resolve (with no errors) rather than hang forever.
1106
+ const first = parser.validateSql("SELECT 1", { state });
1107
+ await vi.advanceTimersByTimeAsync(100);
1108
+ const second = parser.validateSql("SELECT 2", { state });
1109
+ await vi.runAllTimersAsync();
1110
+
1111
+ await expect(first).resolves.toEqual([]);
1112
+ await expect(second).resolves.toEqual([error]);
1113
+ });
1114
+
1115
+ it("uses backend validation for a named DuckDB connection", async () => {
1116
+ vi.useFakeTimers();
1117
+ vi.spyOn(sqlMode, "getSQLMode").mockReturnValue("validate");
1118
+ const query =
1119
+ 'INSERT OR IGNORE INTO "rebalance-dates" (Timestamp) VALUES (CURRENT_TIMESTAMP);';
1120
+ const error = {
1121
+ message: "Named DuckDB backend result",
1122
+ line: 1,
1123
+ column: 1,
1124
+ severity: "error" as const,
1125
+ };
1126
+ const request = vi.spyOn(ValidateSQL, "request").mockResolvedValue({
1127
+ error: null,
1128
+ parse_result: { success: false, errors: [error] },
1129
+ request_id: "request-id",
1130
+ validate_result: null,
1131
+ });
1132
+ const state = createNamedConnectionState({
1133
+ doc: query,
1134
+ engine: "con" as ConnectionName,
1135
+ dialect: "duckdb",
1136
+ });
1137
+ const parser = new exportedForTesting.CustomSqlParser();
1138
+ parser.setFocusState(true);
1139
+
1140
+ const result = parser.validateSql(query, { state });
1141
+ await vi.runAllTimersAsync();
1142
+
1143
+ await expect(result).resolves.toEqual([error]);
1144
+ expect(request).toHaveBeenCalledWith({
1145
+ dialect: "DuckDB",
1146
+ engine: "con",
1147
+ onlyParse: true,
1148
+ query,
1149
+ });
1150
+ });
1151
+
1152
+ it("skips client-side parsing for named DuckDB SQL", async () => {
1153
+ const query = "CHECKPOINT;";
1154
+ const state = createNamedConnectionState({
1155
+ doc: query,
1156
+ engine: "con" as ConnectionName,
1157
+ dialect: "duckdb",
1158
+ });
1159
+ const parser = new exportedForTesting.CustomSqlParser();
1160
+
1161
+ await expect(parser.parse(query, { state })).resolves.toEqual({
1162
+ success: true,
1163
+ errors: [],
1164
+ });
1165
+ });
1166
+
1167
+ it("preserves client-side parsing for non-DuckDB connections", async () => {
1168
+ const query = "SELECT TOP 1 * FROM users";
1169
+ const state = createNamedConnectionState({
1170
+ doc: query,
1171
+ engine: "postgres_con" as ConnectionName,
1172
+ dialect: "postgres",
1173
+ });
1174
+ const parser = new exportedForTesting.CustomSqlParser();
1175
+
1176
+ await expect(parser.parse(query, { state })).resolves.toMatchObject({
1177
+ success: false,
1178
+ });
1179
+ });
1180
+
1181
+ it("preserves client-side parsing for an unregistered connection", async () => {
1182
+ // No entry is registered in the data source connections store for this
1183
+ // engine, so its dialect cannot be resolved (unlike a known non-DuckDB
1184
+ // dialect, which resolves to a non-null, non-DuckDB value).
1185
+ const query = "SELECT TOP 1 * FROM users";
1186
+ const state = EditorState.create({
1187
+ doc: query,
1188
+ extensions: [
1189
+ languageMetadataField.init(() => ({
1190
+ dataframeName: "_df",
1191
+ quotePrefix: "f",
1192
+ commentLines: [],
1193
+ showOutput: true,
1194
+ engine: "unregistered_con" as ConnectionName,
1195
+ })),
1196
+ ],
1197
+ });
1198
+ const parser = new exportedForTesting.CustomSqlParser();
1199
+
1200
+ await expect(parser.parse(query, { state })).resolves.toMatchObject({
1201
+ success: false,
1202
+ });
1019
1203
  });
1020
1204
  });
1021
1205
 
@@ -66,7 +66,7 @@ import { getSQLMode, type SQLMode } from "./sql-mode";
66
66
  import { isKnownDialect } from "./utils";
67
67
 
68
68
  const DEFAULT_DIALECT = DuckDBDialect;
69
- const DEFAULT_PARSER_DIALECT = "DuckDB";
69
+ const DEFAULT_PARSER_DIALECT: ParserDialects = "DuckDB";
70
70
 
71
71
  // A compartment for the SQL config, so we can update the config of codemirror
72
72
  const sqlConfigCompartment = new Compartment();
@@ -372,6 +372,8 @@ class DialectAwareSqlStructureAnalyzer extends SqlStructureAnalyzer {
372
372
 
373
373
  class CustomSqlParser extends NodeSqlParser {
374
374
  private validationTimeout: number | null = null;
375
+ private pendingValidationResolve: ((errors: SqlParseError[]) => void) | null =
376
+ null;
375
377
  private readonly VALIDATION_DELAY_MS = 300; // Wait 300ms after user stops typing
376
378
  private isFocused = false; // Only validate if the editor is focused
377
379
 
@@ -379,37 +381,51 @@ class CustomSqlParser extends NodeSqlParser {
379
381
  this.isFocused = focused;
380
382
  }
381
383
 
384
+ private resolvePendingValidation(errors: SqlParseError[]): void {
385
+ this.pendingValidationResolve?.(errors);
386
+ this.pendingValidationResolve = null;
387
+ }
388
+
382
389
  private async validateWithDelay(
383
390
  sql: string,
384
- engine: string,
391
+ engine: ConnectionName,
385
392
  dialect: ParserDialects | null,
386
393
  ): Promise<SqlParseError[]> {
387
- // Clear any existing delay call
394
+ // Clear any existing delay call, resolving its promise so a superseded
395
+ // request doesn't hang forever.
388
396
  if (this.validationTimeout) {
389
397
  window.clearTimeout(this.validationTimeout);
398
+ this.resolvePendingValidation([]);
390
399
  }
391
400
 
392
401
  // Set up a new request to be called after the delay
393
402
  return new Promise((resolve) => {
403
+ this.pendingValidationResolve = resolve;
394
404
  this.validationTimeout = window.setTimeout(async () => {
405
+ this.validationTimeout = null;
406
+
395
407
  // Only validate if the editor is still focused
396
408
  if (!this.isFocused) {
397
- resolve([]);
409
+ this.resolvePendingValidation([]);
398
410
  return;
399
411
  }
400
412
 
401
413
  try {
402
- const sqlMode = getSQLMode();
414
+ // For validate mode, we run EXPLAIN queries on the engine, which can be
415
+ // expensive for remote databases. So, we only run for internal engines.
416
+ const sqlMode = INTERNAL_SQL_ENGINES.has(engine)
417
+ ? getSQLMode()
418
+ : "default";
403
419
  const result = await validateSQL(sql, engine, dialect, sqlMode);
404
420
  if (result.error) {
405
421
  Logger.error("Failed to validate SQL", { error: result.error });
406
- resolve([]);
422
+ this.resolvePendingValidation([]);
407
423
  return;
408
424
  }
409
- resolve(result.parse_result?.errors ?? []);
425
+ this.resolvePendingValidation(result.parse_result?.errors ?? []);
410
426
  } catch (error) {
411
427
  Logger.error("Failed to validate SQL", { error });
412
- resolve([]);
428
+ this.resolvePendingValidation([]);
413
429
  }
414
430
  }, this.VALIDATION_DELAY_MS);
415
431
  });
@@ -420,19 +436,23 @@ class CustomSqlParser extends NodeSqlParser {
420
436
  opts: { state: EditorState },
421
437
  ): Promise<SqlParseError[]> {
422
438
  const metadata = getSQLMetadata(opts.state);
439
+ const dialect = connectionNameToParserDialect(metadata.engine);
423
440
 
424
441
  // Only validate if the editor is focused
425
442
  if (!this.isFocused) {
426
443
  return [];
427
444
  }
428
445
 
429
- // Only perform custom validation for DuckDB
430
- if (!INTERNAL_SQL_ENGINES.has(metadata.engine)) {
446
+ // Only perform custom validation for DuckDB as we have a custom validation endpoint for it.
447
+ if (!isDuckDBConnection(metadata.engine, dialect)) {
431
448
  return super.validateSql(sql, opts);
432
449
  }
433
450
 
434
- const dialect = guessParserDialect(opts.state);
435
- return this.validateWithDelay(sql, metadata.engine, dialect);
451
+ return this.validateWithDelay(
452
+ sql,
453
+ metadata.engine,
454
+ dialect ?? DEFAULT_PARSER_DIALECT,
455
+ );
436
456
  }
437
457
 
438
458
  override async parse(
@@ -441,9 +461,10 @@ class CustomSqlParser extends NodeSqlParser {
441
461
  ): Promise<NodeSqlParserResult> {
442
462
  const metadata = getSQLMetadata(opts.state);
443
463
  const engine = metadata.engine;
464
+ const dialect = connectionNameToParserDialect(engine);
444
465
 
445
466
  // For now, always return success for DuckDB
446
- if (engine === DUCKDB_ENGINE) {
467
+ if (isDuckDBConnection(engine, dialect)) {
447
468
  return { success: true, errors: [] };
448
469
  }
449
470
 
@@ -451,6 +472,13 @@ class CustomSqlParser extends NodeSqlParser {
451
472
  }
452
473
  }
453
474
 
475
+ function isDuckDBConnection(
476
+ engine: ConnectionName,
477
+ dialect: ParserDialects | null,
478
+ ): boolean {
479
+ return engine === DUCKDB_ENGINE || dialect === "DuckDB";
480
+ }
481
+
454
482
  /**
455
483
  * Update the SQL dialect in the editor view.
456
484
  */
@@ -0,0 +1,5 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { components } from "@marimo-team/marimo-api";
4
+
5
+ export type DetectedDataSource = components["schemas"]["DetectedDataSource"];
@@ -1,5 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import type {
3
+ DataSourceDiscoveryResult,
3
4
  SQLSchemaListPreview,
4
5
  SQLTableListPreview,
5
6
  SQLTablePreview,
@@ -19,6 +20,16 @@ import type {
19
20
  // The backend returns data tables, which could also exist in other engines, dbs, schemas
20
21
  // Thus, we use the request ID pattern to match the response to the request
21
22
 
23
+ export const DiscoverDataSources = new DeferredRequestRegistry<
24
+ {},
25
+ DataSourceDiscoveryResult
26
+ >("data-source-discovery-result", async (requestId) => {
27
+ const client = getRequestClient();
28
+ await client.discoverDataSources({
29
+ requestId,
30
+ });
31
+ });
32
+
22
33
  export const PreviewSQLTable = new DeferredRequestRegistry<
23
34
  Omit<PreviewSQLTableRequest, "requestId">,
24
35
  SQLTablePreview
@@ -194,6 +194,7 @@ function handleMessage(
194
194
  case "sql-schema-list-preview":
195
195
  case "datasets":
196
196
  case "data-source-connections":
197
+ case "data-source-discovery-result":
197
198
  case "validate-sql-result":
198
199
  case "storage-namespaces":
199
200
  case "storage-entries":
@@ -341,6 +341,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
341
341
  previewSQLTableList = throwNotImplemented;
342
342
  previewSQLSchemaList = throwNotImplemented;
343
343
  previewDataSourceConnection = throwNotImplemented;
344
+ discoverDataSources = throwNotImplemented;
344
345
  validateSQL = throwNotImplemented;
345
346
  openFile = throwNotImplemented;
346
347
  sendListFiles = throwNotImplemented;
@@ -44,6 +44,8 @@ export type SQLTableListPreview =
44
44
  export type SQLSchemaListPreview =
45
45
  NotificationMessageData<"sql-schema-list-preview">;
46
46
  export type ValidateSQLResult = NotificationMessageData<"validate-sql-result">;
47
+ export type DataSourceDiscoveryResult =
48
+ NotificationMessageData<"data-source-discovery-result">;
47
49
  export type SecretKeysResult = NotificationMessageData<"secret-keys-result">;
48
50
  export type StartupLogs = NotificationMessageData<"startup-logs">;
49
51
  export type CellMessage = NotificationMessageData<"cell-op">;