@marimo-team/frontend 0.23.16-dev52 → 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 (70) hide show
  1. package/dist/assets/{CellStatus-BT88_2jw.js → CellStatus-BfPw99P2.js} +1 -1
  2. package/dist/assets/{JsonOutput-0XYUxjxO.js → JsonOutput-E3o1Uk0e.js} +1 -1
  3. package/dist/assets/{MarimoErrorOutput-DuwnzR5D.js → MarimoErrorOutput-D9Dypu7Q.js} +1 -1
  4. package/dist/assets/{RenderHTML-BPdXbewN.js → RenderHTML-DRJxD5FQ.js} +1 -1
  5. package/dist/assets/{RunButton-DJyvIhMO.js → RunButton-y7Y9rjmF.js} +1 -1
  6. package/dist/assets/{add-cell-with-ai-Dspw1-mO.js → add-cell-with-ai-ByfOWv0Z.js} +1 -1
  7. package/dist/assets/{add-connection-dialog-B1fFBVqJ.js → add-connection-dialog-DVSFAYXK.js} +1 -1
  8. package/dist/assets/{agent-panel-iPdDqSEd.js → agent-panel-B2OKzocQ.js} +1 -1
  9. package/dist/assets/{ai-model-dropdown-BC1SNLKb.js → ai-model-dropdown-Dr2QbDlZ.js} +1 -1
  10. package/dist/assets/{app-config-button-CuE-bLFn.js → app-config-button-DScmpqcV.js} +1 -1
  11. package/dist/assets/{blob-C23KHI9W.js → blob-BXcsY70H.js} +1 -1
  12. package/dist/assets/{cell-editor-D8UAfiUa.js → cell-editor-BuGvBHQI.js} +1 -1
  13. package/dist/assets/{cell-link-Bu5k33_H.js → cell-link-DGv7Q4Mc.js} +1 -1
  14. package/dist/assets/{cells-C3V6s-g5.js → cells-CMc2NsXm.js} +58 -58
  15. package/dist/assets/{chat-display-DSMFTDiK.js → chat-display-CeQftKrl.js} +1 -1
  16. package/dist/assets/{chat-panel-DuGaXa17.js → chat-panel-BP5hfvS7.js} +1 -1
  17. package/dist/assets/{chat-ui-TD66NC03.js → chat-ui-DoA0lexh.js} +1 -1
  18. package/dist/assets/{column-preview-Dqt5ygdi.js → column-preview-BeR1SI6Z.js} +1 -1
  19. package/dist/assets/{command-palette-DgyUILpb.js → command-palette-DZSYeVCO.js} +1 -1
  20. package/dist/assets/{common-CIJsW5yR.js → common-NWgkp5L1.js} +1 -1
  21. package/dist/assets/{components-CniQJAsf.js → components-Mbf0MEIC.js} +1 -1
  22. package/dist/assets/{datasource-YLl5kKpx.js → datasource-Wt8yUynT.js} +1 -1
  23. package/dist/assets/{dependency-graph-panel-MPHqARlX.js → dependency-graph-panel-B7TXWKYs.js} +1 -1
  24. package/dist/assets/{documentation-panel-DZnNktBv.js → documentation-panel-D9MZUKaD.js} +1 -1
  25. package/dist/assets/{download-CSWknseP.js → download-U_wgQ8mC.js} +1 -1
  26. package/dist/assets/{edit-page-DVXg8Ibp.js → edit-page-BBc9BGYT.js} +4 -4
  27. package/dist/assets/{error-entries-DIjVQBFa.js → error-entries-Bs30Z6uG.js} +1 -1
  28. package/dist/assets/{error-panel-CJSBDHOq.js → error-panel-KY679tDx.js} +1 -1
  29. package/dist/assets/{file-explorer-panel-sHVZyshk.js → file-explorer-panel-9y0hzNAa.js} +1 -1
  30. package/dist/assets/{file-icons-DDUQU--N.js → file-icons-D5l3kSYq.js} +1 -1
  31. package/dist/assets/{file-name-input-CQjQP_zG.js → file-name-input-D_xhjKFp.js} +1 -1
  32. package/dist/assets/{floating-outline-D1iQUiR_.js → floating-outline-BdgJT2B9.js} +1 -1
  33. package/dist/assets/{focus-D_UTw9HF.js → focus-D64hL1tZ.js} +1 -1
  34. package/dist/assets/{form-Bp7lk3pm.js → form-CoqwGbkO.js} +1 -1
  35. package/dist/assets/{form-B6jUSJBp.js → form-b4jQBV3T.js} +1 -1
  36. package/dist/assets/{home-page-CX6IV-hM.js → home-page-tVWY9YdF.js} +1 -1
  37. package/dist/assets/{html-to-image-kpOREGyr.js → html-to-image-Dm7huiQj.js} +1 -1
  38. package/dist/assets/{index-CrJhpWGp.js → index-Dz8dLbnI.js} +3 -3
  39. package/dist/assets/{layout-DdPE4xZU.js → layout-DKUs0ozP.js} +3 -3
  40. package/dist/assets/{logs-panel-DnNiJzUQ.js → logs-panel-BzS7VSh0.js} +1 -1
  41. package/dist/assets/{markdown-renderer-CqljiwGm.js → markdown-renderer-uWTrQId1.js} +1 -1
  42. package/dist/assets/{name-cell-input-BQzQI3Ja.js → name-cell-input-BgxcWTfV.js} +1 -1
  43. package/dist/assets/{outline-panel-BIgVnAC7.js → outline-panel-BT3zzwfa.js} +1 -1
  44. package/dist/assets/{packages-panel-_HiSQVV7.js → packages-panel-BKfeGuve.js} +1 -1
  45. package/dist/assets/{panels-b-OL36p2.js → panels-4jSJxEYp.js} +1 -1
  46. package/dist/assets/{process-output-CJkR5qgD.js → process-output-DE8TTXPC.js} +1 -1
  47. package/dist/assets/{radio-group-Dm-ZV-tP.js → radio-group-Da80KtoT.js} +1 -1
  48. package/dist/assets/{readonly-code-display-BiNt9i32.js → readonly-code-display-B8zhuSov.js} +1 -1
  49. package/dist/assets/{readonly-python-code-DMKb3G0K.js → readonly-python-code-Bmo6cMSq.js} +1 -1
  50. package/dist/assets/{reveal-component-DN6-mIsb.js → reveal-component-Cv7WZ4r6.js} +1 -1
  51. package/dist/assets/{run-page-BKCqJ0uJ.js → run-page-CA21zsME.js} +1 -1
  52. package/dist/assets/{scratchpad-panel-DwZEAfW1.js → scratchpad-panel-Br7RFJkK.js} +1 -1
  53. package/dist/assets/{session-panel-B-9BnZxS.js → session-panel-BiUht4zM.js} +1 -1
  54. package/dist/assets/{skeleton-B72xXZAF.js → skeleton-CY-B1asm.js} +1 -1
  55. package/dist/assets/{snippets-panel-Brc_pmTk.js → snippets-panel-CjiTlCpD.js} +1 -1
  56. package/dist/assets/{state-BTAocii6.js → state-BJ7IdtXW.js} +1 -1
  57. package/dist/assets/{state-D9oG57zx.js → state-TgAqts79.js} +1 -1
  58. package/dist/assets/{tracing-Cn9cTT65.js → tracing-DmQ7KQ3A.js} +1 -1
  59. package/dist/assets/{tracing-panel-FWoe3_eY.js → tracing-panel-C1TOV9kj.js} +2 -2
  60. package/dist/assets/{tree-actions-rPl1v1gM.js → tree-actions-enwITSy7.js} +1 -1
  61. package/dist/assets/{useCellActionButton-Du3JXpNf.js → useCellActionButton-CXvU9GmI.js} +1 -1
  62. package/dist/assets/{useDeleteCell-DHOwR-CG.js → useDeleteCell-Bp3S6pL8.js} +1 -1
  63. package/dist/assets/{useDependencyPanelTab-B2PCtSi6.js → useDependencyPanelTab-CewYv8cP.js} +1 -1
  64. package/dist/assets/{useNotebookActions-DfQAtofT.js → useNotebookActions-l9NAqPKm.js} +1 -1
  65. package/dist/assets/{useRunCells-Dg11M4pT.js → useRunCells-B-KIwrVZ.js} +1 -1
  66. package/dist/assets/{useSplitCell-BwWVHida.js → useSplitCell-B0jvSFsl.js} +1 -1
  67. package/dist/index.html +23 -23
  68. package/package.json +1 -1
  69. package/src/core/codemirror/language/__tests__/sql.test.ts +191 -7
  70. package/src/core/codemirror/language/languages/sql/sql.ts +41 -13
@@ -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
  */