@marimo-team/frontend 0.23.11-dev1 → 0.23.11-dev10

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 (84) hide show
  1. package/dist/assets/{CellStatus-e0ex7Iei.js → CellStatus-CLGvVxcw.js} +1 -1
  2. package/dist/assets/{JsonOutput-DRNPZOvX.js → JsonOutput-uEJijGXp.js} +5 -5
  3. package/dist/assets/{MarimoErrorOutput-BH6hs0Ir.js → MarimoErrorOutput-DzoKyXWR.js} +2 -2
  4. package/dist/assets/{RenderHTML-BQ1PO4Wd.js → RenderHTML-DAt48X-F.js} +1 -1
  5. package/dist/assets/{RunButton-F8pLIvFp.js → RunButton-B7msyyYi.js} +1 -1
  6. package/dist/assets/{add-cell-with-ai-C2OtCghK.js → add-cell-with-ai-CAkcousR.js} +8 -8
  7. package/dist/assets/{add-connection-dialog-AhwxOztN.js → add-connection-dialog-ZyXXfAVG.js} +32 -32
  8. package/dist/assets/{agent-panel-D6ryE8xb.js → agent-panel-BfIguQXh.js} +3 -3
  9. package/dist/assets/{ai-model-dropdown-CG4B4rqH.js → ai-model-dropdown-rE64ytSX.js} +3 -3
  10. package/dist/assets/{app-config-button-D_sFrSql.js → app-config-button-DLRZ5d9c.js} +1 -1
  11. package/dist/assets/{cell-editor-XNa3wJ3P.js → cell-editor-2JIpvFn4.js} +5 -5
  12. package/dist/assets/{cell-link-Bj4-yOIx.js → cell-link-DY95GSb7.js} +1 -1
  13. package/dist/assets/{cells-DOA0Gew8.js → cells-D-v2lBet.js} +67 -67
  14. package/dist/assets/{chat-display-CezkHLLR.js → chat-display-DO-tqsbY.js} +1 -1
  15. package/dist/assets/{chat-panel-DEBTPtwE.js → chat-panel-BT5gLfs6.js} +1 -1
  16. package/dist/assets/{chat-ui-CIr1o1hq.js → chat-ui-Be4yDBZS.js} +1 -1
  17. package/dist/assets/column-preview-BzvFdLI5.js +1 -0
  18. package/dist/assets/{command-palette-_VEEjmkI.js → command-palette-CKKzfpGt.js} +1 -1
  19. package/dist/assets/{common-fDFYY_sv.js → common-Do4N1uVK.js} +1 -1
  20. package/dist/assets/{components--C6N-DXq.js → components-DUC0f1XD.js} +1 -1
  21. package/dist/assets/{datasource-CR6RRpTi.js → datasource-BQGI7c_u.js} +2 -2
  22. package/dist/assets/{dependency-graph-panel-BEgkvdX0.js → dependency-graph-panel-Czoya7c2.js} +1 -1
  23. package/dist/assets/{documentation-panel-HvbKykbI.js → documentation-panel-LokVYDIZ.js} +1 -1
  24. package/dist/assets/{download-DhxnAw14.js → download-WGU5w_3m.js} +3 -3
  25. package/dist/assets/{edit-page-D-02q6iz.js → edit-page-BPQO1Uuz.js} +6 -6
  26. package/dist/assets/{error-panel-WLBQ3q9p.js → error-panel-BCm_e4eM.js} +1 -1
  27. package/dist/assets/{file-explorer-panel-DtzGlnzT.js → file-explorer-panel-D1NN-z8x.js} +3 -3
  28. package/dist/assets/{file-icons-D2f3nfbq.js → file-icons-CF-YT4hq.js} +1 -1
  29. package/dist/assets/{file-name-input-BNYf9WWM.js → file-name-input-BRVXQ3OU.js} +1 -1
  30. package/dist/assets/{floating-outline-BvHFWRoz.js → floating-outline-D33Zu-Ad.js} +1 -1
  31. package/dist/assets/{focus-Ldqh99xE.js → focus-BfFAxl9X.js} +1 -1
  32. package/dist/assets/{form-CxBAInfg.js → form-CVBFx2z3.js} +1 -1
  33. package/dist/assets/{home-page-DwFpLpXK.js → home-page-CL9Hv1Hg.js} +2 -2
  34. package/dist/assets/{hooks-5gNQitDd.js → hooks-83y0XCiC.js} +1 -1
  35. package/dist/assets/{html-to-image-CyAtzePO.js → html-to-image-IPNqWX7V.js} +2 -2
  36. package/dist/assets/index-BNWrEIlp.css +2 -0
  37. package/dist/assets/{index-CGCBsDqs.js → index-CuhY66ZR.js} +14 -14
  38. package/dist/assets/{kiosk-mode-CTWHjzXs.js → kiosk-mode-BSufcIZH.js} +1 -1
  39. package/dist/assets/{layout-D3pd3R21.js → layout-ks0WJ2vR.js} +5 -5
  40. package/dist/assets/{logs-panel-BVLYycQb.js → logs-panel-DXN6sUaA.js} +1 -1
  41. package/dist/assets/{markdown-renderer-C9Ujvj0b.js → markdown-renderer-DWRPo52L.js} +1 -1
  42. package/dist/assets/{name-cell-input-DabvuruX.js → name-cell-input-BiDIx6YQ.js} +1 -1
  43. package/dist/assets/{outline-panel-B-ncbNWI.js → outline-panel-DVSGNRgP.js} +1 -1
  44. package/dist/assets/{packages-panel-CESeW_3T.js → packages-panel-DTX6CBnm.js} +1 -1
  45. package/dist/assets/{panels-CTy3hq0L.js → panels-NhGV0SBq.js} +1 -1
  46. package/dist/assets/{process-output-pPgH0ANl.js → process-output-BiWV34b7.js} +1 -1
  47. package/dist/assets/{radio-group-D7rh6Zek.js → radio-group-DV-lQvil.js} +1 -1
  48. package/dist/assets/{readonly-python-code-CvPx4CU_.js → readonly-python-code-B0glFTKW.js} +1 -1
  49. package/dist/assets/{reveal-component-lTtEZQh1.js → reveal-component-C2M2FixA.js} +12 -12
  50. package/dist/assets/{run-page-BB6ghGfO.js → run-page-B6I-Lshx.js} +1 -1
  51. package/dist/assets/{scratchpad-panel-Bvb8RxUv.js → scratchpad-panel-DU7perVk.js} +1 -1
  52. package/dist/assets/session-panel-B4UTmf-j.js +1 -0
  53. package/dist/assets/{snippets-panel-BSgcoo-M.js → snippets-panel-KRdl4bVk.js} +1 -1
  54. package/dist/assets/{state-BWM3qRkR.js → state-BSm0OM93.js} +1 -1
  55. package/dist/assets/{state-C8yHPSLN.js → state-CbkvENzk.js} +2 -2
  56. package/dist/assets/{textarea-bBuxbFm7.js → textarea-CBbixTxi.js} +1 -1
  57. package/dist/assets/{tracing-pt7eDHWc.js → tracing-D8LGpuXa.js} +1 -1
  58. package/dist/assets/{tracing-panel-BveZ5DZw.js → tracing-panel-TUP6kAd5.js} +2 -2
  59. package/dist/assets/{tree-actions-1KQDSu1H.js → tree-actions-BvrKFU0g.js} +1 -1
  60. package/dist/assets/{useCellActionButton-C4V7J6PS.js → useCellActionButton-CtOZIXD0.js} +1 -1
  61. package/dist/assets/{useDeleteCell-BvnurAZ9.js → useDeleteCell-BLvzLWAT.js} +1 -1
  62. package/dist/assets/{useDependencyPanelTab-DNlXaxbt.js → useDependencyPanelTab-BP0sNfUz.js} +1 -1
  63. package/dist/assets/{useNotebookActions-BIQkawja.js → useNotebookActions-BI5dWutQ.js} +1 -1
  64. package/dist/assets/{useRunCells-cSZpNqnR.js → useRunCells-BClkx9Pq.js} +1 -1
  65. package/dist/assets/{useSplitCell-s7dSiBUa.js → useSplitCell-CpddCX_u.js} +1 -1
  66. package/dist/index.html +23 -23
  67. package/package.json +1 -1
  68. package/src/components/datasources/__tests__/column-preview.test.tsx +97 -0
  69. package/src/components/datasources/__tests__/filter-empty.test.ts +81 -0
  70. package/src/components/datasources/__tests__/utils.test.ts +62 -1
  71. package/src/components/datasources/column-preview.tsx +2 -4
  72. package/src/components/datasources/components.tsx +15 -7
  73. package/src/components/datasources/datasources.tsx +311 -178
  74. package/src/components/datasources/utils.ts +40 -1
  75. package/src/components/editor/connections/components.tsx +13 -0
  76. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +4 -4
  77. package/src/components/editor/connections/storage/as-code.ts +11 -4
  78. package/src/core/cells/__tests__/cells.test.ts +33 -0
  79. package/src/core/cells/cells.ts +1 -1
  80. package/src/core/datasets/__tests__/data-source.test.ts +226 -0
  81. package/src/core/datasets/data-source-connections.ts +88 -24
  82. package/dist/assets/column-preview-nu3Qo2OW.js +0 -1
  83. package/dist/assets/index-BAYF7dcV.css +0 -2
  84. package/dist/assets/session-panel-CTJPYbAa.js +0 -1
@@ -4,15 +4,54 @@ import { BigQueryDialect } from "@marimo-team/codemirror-sql/dialects";
4
4
  import { isKnownDialect } from "@/core/codemirror/language/languages/sql/utils";
5
5
  import type { SQLTableContext } from "@/core/datasets/data-source-connections";
6
6
  import { DUCKDB_ENGINE } from "@/core/datasets/engines";
7
- import type { DataTable, DataType } from "@/core/kernel/messages";
7
+ import type {
8
+ Database,
9
+ DatabaseSchema,
10
+ DataTable,
11
+ DataType,
12
+ } from "@/core/kernel/messages";
8
13
  import { logNever } from "@/utils/assertNever";
9
14
  import type { ColumnHeaderStatsKey } from "../data-table/types";
10
15
 
16
+ /**
17
+ * Stable id for a table node in the datasources tree.
18
+ *
19
+ * schemaPath already includes the leaf schema for nested namespaces, so use it
20
+ * when present and fall back to the flat schema name otherwise (avoids
21
+ * duplicating the leaf, e.g. `top.nested.nested.table`).
22
+ */
23
+ export function tableUniqueId(
24
+ sqlTableContext: SQLTableContext | undefined,
25
+ tableName: string,
26
+ ): string {
27
+ if (!sqlTableContext) {
28
+ return tableName;
29
+ }
30
+ const segments = (
31
+ sqlTableContext.schemaPath?.length
32
+ ? sqlTableContext.schemaPath
33
+ : [sqlTableContext.schema]
34
+ ).filter(Boolean);
35
+ return [sqlTableContext.database, ...segments, tableName].join(".");
36
+ }
37
+
11
38
  // Some databases have no schemas, so we don't show it (eg. Clickhouse)
12
39
  export function isSchemaless(schemaName: string) {
13
40
  return schemaName === "";
14
41
  }
15
42
 
43
+ // Lazy discovery: the `*_resolved` flags default to `true` and are only `false`
44
+ // when enumeration was deferred. Helper functions to centralize logic
45
+ export function areSchemasResolved(database: Database): boolean {
46
+ return database.schemas_resolved !== false;
47
+ }
48
+ export function areTablesResolved(schema: DatabaseSchema): boolean {
49
+ return schema.tables_resolved !== false;
50
+ }
51
+ export function areChildSchemasResolved(schema: DatabaseSchema): boolean {
52
+ return schema.child_schemas_resolved !== false;
53
+ }
54
+
16
55
  interface SqlCodeFormatter {
17
56
  /**
18
57
  * Format the table path based on dialect-specific rules
@@ -1,6 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { zodResolver } from "@hookform/resolvers/zod";
4
+ import { useAtomValue } from "jotai";
4
5
  import React from "react";
5
6
  import { type DefaultValues, type FieldValues, useForm } from "react-hook-form";
6
7
  import type { z } from "zod";
@@ -16,8 +17,10 @@ import {
16
17
  SelectTrigger,
17
18
  SelectValue,
18
19
  } from "@/components/ui/select";
20
+ import { maybeAddMarimoImport } from "@/core/cells/add-missing-import";
19
21
  import { useCellActions } from "@/core/cells/cells";
20
22
  import { useLastFocusedCellId } from "@/core/cells/focus";
23
+ import { autoInstantiateAtom } from "@/core/config/config";
21
24
  import { ENV_RENDERER, SecretsProvider } from "./form-renderers";
22
25
 
23
26
  const RENDERERS: FormRenderer[] = [ENV_RENDERER];
@@ -106,8 +109,18 @@ export const ConnectionFormFooter = <L extends string>({
106
109
  export function useInsertCode() {
107
110
  const { createNewCell } = useCellActions();
108
111
  const lastFocusedCellId = useLastFocusedCellId();
112
+ const autoInstantiate = useAtomValue(autoInstantiateAtom);
109
113
 
110
114
  return (code: string) => {
115
+ // Ensure `mo` is importable when the generated code references it
116
+ if (/\bmo\./.test(code)) {
117
+ maybeAddMarimoImport({
118
+ autoInstantiate,
119
+ createNewCell,
120
+ fromCellId: lastFocusedCellId,
121
+ });
122
+ }
123
+
111
124
  createNewCell({
112
125
  code,
113
126
  before: false,
@@ -85,14 +85,14 @@ store = GCSStore("my-bucket")"
85
85
  exports[`generateStorageCode > Google Drive > with default auth (no credentials) 1`] = `
86
86
  "from gdrive_fsspec import GoogleDriveFileSystem
87
87
 
88
- fs = GoogleDriveFileSystem(use_listings_cache=False)"
88
+ fs = GoogleDriveFileSystem(use_listings_cache=False, skip_instance_cache=True)"
89
89
  `;
90
90
 
91
91
  exports[`generateStorageCode > Google Drive > with embedded auth (no credentials) 1`] = `
92
92
  "from gdrive_fsspec import GoogleDriveFileSystem
93
93
 
94
- fs = GoogleDriveFileSystem(use_listings_cache=False, auth_kwargs={"use_local_webserver": False})
95
- print("Google Drive connected! Important: Run this cell again to clear the console")"
94
+ fs = GoogleDriveFileSystem(use_listings_cache=False, skip_instance_cache=True, auth_kwargs={"use_local_webserver": False})
95
+ mo.output.clear_console()"
96
96
  `;
97
97
 
98
98
  exports[`generateStorageCode > Google Drive > with service account credentials 1`] = `
@@ -100,7 +100,7 @@ exports[`generateStorageCode > Google Drive > with service account credentials 1
100
100
  import json
101
101
 
102
102
  _creds = json.loads("""{"type": "service_account", "client_email": "test@test.iam.gserviceaccount.com"}""")
103
- fs = GoogleDriveFileSystem(creds=_creds, token="service_account", use_listings_cache=False)"
103
+ fs = GoogleDriveFileSystem(creds=_creds, token="service_account", use_listings_cache=False, skip_instance_cache=True)"
104
104
  `;
105
105
 
106
106
  exports[`generateStorageCode > S3 > basic connection with all fields 1`] = `
@@ -168,6 +168,10 @@ function generateGDriveCode(
168
168
  connection: Extract<StorageConnection, { type: "gdrive" }>,
169
169
  options: { secrets: SecretContainer; isEmbedded?: boolean },
170
170
  ): { imports: Set<string>; code: string } {
171
+ /**
172
+ * Skip instance cache True so you can create multiple connections which don't reference the same creds.
173
+ * Use listings cache False so we don't get stale reads.
174
+ */
171
175
  const { secrets, isEmbedded = false } = options;
172
176
  const imports = new Set(["from gdrive_fsspec import GoogleDriveFileSystem"]);
173
177
 
@@ -179,18 +183,21 @@ function generateGDriveCode(
179
183
  );
180
184
  const code = dedent(`
181
185
  _creds = json.loads("""${connection.credentials_json?.startsWith("ENV:") ? `{${creds}}` : connection.credentials_json}""")
182
- fs = GoogleDriveFileSystem(creds=_creds, token="service_account", use_listings_cache=False)
186
+ fs = GoogleDriveFileSystem(creds=_creds, token="service_account", use_listings_cache=False, skip_instance_cache=True)
183
187
  `);
184
188
  return { imports, code };
185
189
  }
186
190
 
191
+ // In the iframe (embedded) flow we authenticate via the console-based OOB
192
+ // flow, which prints an auth URL and reads the code from stdin. Clear the
193
+ // console afterwards so the (single-use) auth code doesn't linger.
187
194
  const code = isEmbedded
188
195
  ? dedent(`
189
- fs = GoogleDriveFileSystem(use_listings_cache=False, auth_kwargs={"use_local_webserver": False})
190
- print("Google Drive connected! Important: Run this cell again to clear the console")
196
+ fs = GoogleDriveFileSystem(use_listings_cache=False, skip_instance_cache=True, auth_kwargs={"use_local_webserver": False})
197
+ mo.output.clear_console()
191
198
  `)
192
199
  : dedent(`
193
- fs = GoogleDriveFileSystem(use_listings_cache=False)
200
+ fs = GoogleDriveFileSystem(use_listings_cache=False, skip_instance_cache=True)
194
201
  `);
195
202
  return { imports, code };
196
203
  }
@@ -2920,6 +2920,39 @@ describe("cell reducer", () => {
2920
2920
  expect(state.untouchedNewCells.has(newCellId)).toBe(true);
2921
2921
  });
2922
2922
 
2923
+ it("adds interactively-created hidden cell with boilerplate code to untouchedNewCells", () => {
2924
+ // Markdown cells are created with hideCode and non-empty default code
2925
+ // (e.g. `mo.md(r"""\n""")`). They are user-initiated, so their editor
2926
+ // should be shown until first blur.
2927
+ actions.createNewCell({
2928
+ cellId: "__end__",
2929
+ before: false,
2930
+ code: 'mo.md(r"""\n""")',
2931
+ hideCode: true,
2932
+ autoFocus: true,
2933
+ });
2934
+
2935
+ const newCellId =
2936
+ state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
2937
+ expect(state.untouchedNewCells.has(newCellId)).toBe(true);
2938
+ });
2939
+
2940
+ it("does not add programmatically-created hidden cell with code to untouchedNewCells", () => {
2941
+ // Cells created by the kernel (e.g. via code_mode) carry code and
2942
+ // autoFocus=false; their hide_code must take effect immediately.
2943
+ actions.createNewCell({
2944
+ cellId: "__end__",
2945
+ before: false,
2946
+ code: "x = 1",
2947
+ hideCode: true,
2948
+ autoFocus: false,
2949
+ });
2950
+
2951
+ const newCellId =
2952
+ state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
2953
+ expect(state.untouchedNewCells.has(newCellId)).toBe(false);
2954
+ });
2955
+
2923
2956
  it("does not add cell to untouchedNewCells when hideCode is false", () => {
2924
2957
  actions.createNewCell({
2925
2958
  cellId: "__end__",
@@ -276,7 +276,7 @@ const {
276
276
  },
277
277
  scrollKey: autoFocus ? newCellId : null,
278
278
  untouchedNewCells:
279
- hideCode && !code
279
+ hideCode && autoFocus
280
280
  ? new Set([...state.untouchedNewCells, newCellId])
281
281
  : state.untouchedNewCells,
282
282
  };
@@ -2,9 +2,12 @@
2
2
  import { beforeEach, describe, expect, it } from "vitest";
3
3
  import { variableName } from "@/__tests__/branded";
4
4
  import type { DatabaseSchema, DataTable } from "@/core/kernel/messages";
5
+ import { store } from "@/core/state/jotai";
5
6
  import type { VariableName } from "@/core/variables/types";
6
7
  import {
8
+ allTablesAtom,
7
9
  type DataSourceConnection,
10
+ dataSourceConnectionsAtom,
8
11
  type DataSourceState,
9
12
  exportedForTesting,
10
13
  type SQLTableContext,
@@ -575,3 +578,226 @@ describe("add table", () => {
575
578
  expect(db1?.schemas.length).toBe(1);
576
579
  });
577
580
  });
581
+
582
+ describe("nested namespaces", () => {
583
+ // Iceberg-style: database "top" with a schemaless schema and a nested
584
+ // namespace "nested" that has a deferred child namespace "deep".
585
+ const nestedConnections: DataSourceConnection[] = [
586
+ {
587
+ name: "ice" as ConnectionName,
588
+ source: "iceberg",
589
+ display_name: "Iceberg",
590
+ dialect: "iceberg",
591
+ databases: [
592
+ {
593
+ name: "top",
594
+ dialect: "iceberg",
595
+ schemas_resolved: true,
596
+ schemas: [
597
+ { name: "", tables: [], tables_resolved: true },
598
+ {
599
+ name: "nested",
600
+ tables: [],
601
+ tables_resolved: false,
602
+ child_schemas: [],
603
+ child_schemas_resolved: false,
604
+ },
605
+ ],
606
+ },
607
+ ],
608
+ },
609
+ ];
610
+
611
+ let baseState: DataSourceState;
612
+
613
+ beforeEach(() => {
614
+ baseState = addConnection(nestedConnections, initialState());
615
+ });
616
+
617
+ const findSchema = (
618
+ state: DataSourceState,
619
+ path: string[],
620
+ ): DatabaseSchema | undefined => {
621
+ const conn = state.connectionsMap.get("ice" as ConnectionName);
622
+ const db = conn?.databases.find((d) => d.name === "top");
623
+ let schemas = db?.schemas ?? [];
624
+ let found: DatabaseSchema | undefined;
625
+ for (const segment of path) {
626
+ found = schemas.find((s) => s.name === segment);
627
+ schemas = found?.child_schemas ?? [];
628
+ }
629
+ return found;
630
+ };
631
+
632
+ it("sets child namespaces at a nested path", () => {
633
+ const children: DatabaseSchema[] = [
634
+ { name: "deep", tables: [], tables_resolved: false },
635
+ ];
636
+ const newState = reducer(baseState, {
637
+ type: "addSchemaList",
638
+ payload: {
639
+ schemas: children,
640
+ sqlSchemaContext: {
641
+ engine: "ice",
642
+ database: "top",
643
+ schemaPath: ["nested"],
644
+ },
645
+ },
646
+ });
647
+
648
+ const nested = findSchema(newState, ["nested"]);
649
+ expect(nested?.child_schemas_resolved).toBe(true);
650
+ expect(nested?.child_schemas?.map((s) => s.name)).toEqual(["deep"]);
651
+ // The schemaless sibling is untouched.
652
+ expect(findSchema(newState, [""])?.name).toBe("");
653
+ });
654
+
655
+ it("sets tables at a nested path", () => {
656
+ const tables: DataTable[] = [
657
+ {
658
+ name: "table4",
659
+ columns: [],
660
+ num_columns: 0,
661
+ num_rows: 0,
662
+ variable_name: null,
663
+ source: "iceberg",
664
+ source_type: "catalog",
665
+ type: "table",
666
+ },
667
+ ];
668
+ const newState = reducer(baseState, {
669
+ type: "addTableList",
670
+ payload: {
671
+ tables,
672
+ sqlTableContext: {
673
+ engine: "ice",
674
+ database: "top",
675
+ schema: "nested",
676
+ dialect: "iceberg",
677
+ schemaPath: ["nested"],
678
+ },
679
+ },
680
+ });
681
+
682
+ const nested = findSchema(newState, ["nested"]);
683
+ expect(nested?.tables_resolved).toBe(true);
684
+ expect(nested?.tables.map((t) => t.name)).toEqual(["table4"]);
685
+ });
686
+
687
+ it("replaces a single table at a nested path", () => {
688
+ const makeTable = (numRows: number): DataTable => ({
689
+ name: "table4",
690
+ columns: [],
691
+ num_columns: 0,
692
+ num_rows: numRows,
693
+ variable_name: null,
694
+ source: "iceberg",
695
+ source_type: "catalog",
696
+ type: "table",
697
+ });
698
+ const context = {
699
+ engine: "ice",
700
+ database: "top",
701
+ schema: "nested",
702
+ dialect: "iceberg",
703
+ schemaPath: ["nested"],
704
+ };
705
+ let state = reducer(baseState, {
706
+ type: "addTableList",
707
+ payload: { tables: [makeTable(1)], sqlTableContext: context },
708
+ });
709
+ state = reducer(state, {
710
+ type: "addTable",
711
+ payload: { table: makeTable(42), sqlTableContext: context },
712
+ });
713
+
714
+ const nested = findSchema(state, ["nested"]);
715
+ expect(nested?.tables).toHaveLength(1);
716
+ expect(nested?.tables[0].num_rows).toBe(42);
717
+ });
718
+
719
+ it("does not change anything for a missing nested path", () => {
720
+ const newState = reducer(baseState, {
721
+ type: "addSchemaList",
722
+ payload: {
723
+ schemas: [{ name: "deep", tables: [] }],
724
+ sqlSchemaContext: {
725
+ engine: "ice",
726
+ database: "top",
727
+ schemaPath: ["does_not_exist"],
728
+ },
729
+ },
730
+ });
731
+ // The result is unchanged: nested namespace stays unresolved and the
732
+ // database keeps its two schemas (schemaless + nested).
733
+ const newDb = newState.connectionsMap
734
+ .get("ice" as ConnectionName)
735
+ ?.databases.find((d) => d.name === "top");
736
+ expect(findSchema(newState, ["nested"])?.child_schemas_resolved).toBe(
737
+ false,
738
+ );
739
+ expect(newDb?.schemas.length).toBe(2);
740
+ });
741
+ });
742
+
743
+ describe("allTablesAtom with nested namespaces", () => {
744
+ it("enumerates tables from nested namespaces", () => {
745
+ const table = (name: string): DataTable => ({
746
+ name,
747
+ columns: [],
748
+ num_columns: 0,
749
+ num_rows: 0,
750
+ variable_name: null,
751
+ source: "iceberg",
752
+ source_type: "catalog",
753
+ type: "table",
754
+ });
755
+
756
+ const state = addConnection(
757
+ [
758
+ {
759
+ name: "ice" as ConnectionName,
760
+ source: "iceberg",
761
+ display_name: "Iceberg",
762
+ dialect: "iceberg",
763
+ databases: [
764
+ {
765
+ name: "top",
766
+ dialect: "iceberg",
767
+ schemas_resolved: true,
768
+ schemas: [
769
+ {
770
+ name: "",
771
+ tables: [table("toptable")],
772
+ tables_resolved: true,
773
+ },
774
+ {
775
+ name: "nested",
776
+ tables: [table("nestedtable")],
777
+ tables_resolved: true,
778
+ child_schemas_resolved: true,
779
+ child_schemas: [
780
+ {
781
+ name: "deep",
782
+ tables: [table("deeptable")],
783
+ tables_resolved: true,
784
+ child_schemas: [],
785
+ child_schemas_resolved: true,
786
+ },
787
+ ],
788
+ },
789
+ ],
790
+ },
791
+ ],
792
+ },
793
+ ],
794
+ initialState(),
795
+ );
796
+
797
+ store.set(dataSourceConnectionsAtom, state);
798
+ const names = [...store.get(allTablesAtom).values()].map((t) => t.name);
799
+ expect(names).toContain("toptable");
800
+ expect(names).toContain("nestedtable");
801
+ expect(names).toContain("deeptable");
802
+ });
803
+ });
@@ -49,6 +49,9 @@ export interface DataSourceState {
49
49
  export interface SQLSchemaContext {
50
50
  engine: string;
51
51
  database: string;
52
+ // Parent schema path (relative to `database`) for nested schemas.
53
+ // Empty/undefined for the database's top level.
54
+ schemaPath?: string[];
52
55
  }
53
56
 
54
57
  export interface SQLTableContext {
@@ -58,6 +61,49 @@ export interface SQLTableContext {
58
61
  dialect: string;
59
62
  defaultSchema?: string | null;
60
63
  defaultDatabase?: string | null;
64
+ // Nested schema path (relative to `database`). Empty/undefined at top level.
65
+ schemaPath?: string[];
66
+ }
67
+
68
+ /**
69
+ * Immutably descend `path` (schema segment names) into a nested schema list
70
+ * and apply `update` to the matching schema. Unmatched branches are unchanged.
71
+ */
72
+ function updateSchemaAtPath(
73
+ schemas: DatabaseSchema[],
74
+ path: string[],
75
+ update: (schema: DatabaseSchema) => DatabaseSchema,
76
+ ): DatabaseSchema[] {
77
+ if (path.length === 0) {
78
+ return schemas;
79
+ }
80
+ const [head, ...rest] = path;
81
+ return schemas.map((schema) => {
82
+ if (schema.name !== head) {
83
+ return schema;
84
+ }
85
+ if (rest.length === 0) {
86
+ return update(schema);
87
+ }
88
+ return {
89
+ ...schema,
90
+ child_schemas: updateSchemaAtPath(
91
+ schema.child_schemas ?? [],
92
+ rest,
93
+ update,
94
+ ),
95
+ };
96
+ });
97
+ }
98
+
99
+ /**
100
+ * The path (schema/namespace segment names within a database) that locates the
101
+ * schema holding a set of tables. For nested namespaces this is the
102
+ * `schemaPath`; otherwise it is the single (possibly schemaless) schema name.
103
+ */
104
+ function tableSchemaPath(sqlTableContext: SQLTableContext): string[] {
105
+ const { schemaPath, schema } = sqlTableContext;
106
+ return schemaPath && schemaPath.length > 0 ? schemaPath : [schema];
61
107
  }
62
108
 
63
109
  function initialState(): DataSourceState {
@@ -159,6 +205,7 @@ const {
159
205
  return state;
160
206
  }
161
207
 
208
+ const schemaPath = sqlSchemaContext.schemaPath ?? [];
162
209
  const newMap = new Map(connectionsMap);
163
210
  const newConn: DataSourceConnection = {
164
211
  ...conn,
@@ -166,10 +213,22 @@ const {
166
213
  if (db.name !== sqlSchemaContext.database) {
167
214
  return db;
168
215
  }
216
+ // Top level: replace the database's schemas.
217
+ if (schemaPath.length === 0) {
218
+ return {
219
+ ...db,
220
+ schemas: schemas,
221
+ schemas_resolved: true,
222
+ };
223
+ }
224
+ // Nested namespace: replace the child schemas of the namespace at path.
169
225
  return {
170
226
  ...db,
171
- schemas: schemas,
172
- schemas_resolved: true,
227
+ schemas: updateSchemaAtPath(db.schemas, schemaPath, (schema) => ({
228
+ ...schema,
229
+ child_schemas: schemas,
230
+ child_schemas_resolved: true,
231
+ })),
173
232
  };
174
233
  }),
175
234
  };
@@ -198,6 +257,7 @@ const {
198
257
  return state;
199
258
  }
200
259
 
260
+ const path = tableSchemaPath(sqlTableContext);
201
261
  const newMap = new Map(connectionsMap);
202
262
  const newConn: DataSourceConnection = {
203
263
  ...conn,
@@ -207,16 +267,11 @@ const {
207
267
  }
208
268
  return {
209
269
  ...db,
210
- schemas: db.schemas.map((schema) => {
211
- if (schema.name !== sqlTableContext.schema) {
212
- return schema;
213
- }
214
- return {
215
- ...schema,
216
- tables: tables,
217
- tables_resolved: true,
218
- };
219
- }),
270
+ schemas: updateSchemaAtPath(db.schemas, path, (schema) => ({
271
+ ...schema,
272
+ tables: tables,
273
+ tables_resolved: true,
274
+ })),
220
275
  };
221
276
  }),
222
277
  };
@@ -246,6 +301,7 @@ const {
246
301
  return state;
247
302
  }
248
303
 
304
+ const path = tableSchemaPath(sqlTableContext);
249
305
  const newMap = new Map(connectionsMap);
250
306
  const newConn: DataSourceConnection = {
251
307
  ...conn,
@@ -253,21 +309,15 @@ const {
253
309
  if (db.name !== sqlTableContext.database) {
254
310
  return db;
255
311
  }
256
-
257
312
  return {
258
313
  ...db,
259
- schemas: db.schemas.map((schema) => {
260
- if (schema.name !== sqlTableContext.schema) {
261
- return schema;
262
- }
263
-
314
+ schemas: updateSchemaAtPath(db.schemas, path, (schema) => {
264
315
  // If tables array is empty, add the table
265
316
  // Otherwise, replace existing table or keep unchanged tables
266
317
  const tables =
267
318
  schema.tables.length === 0
268
319
  ? [table]
269
320
  : schema.tables.map((t) => (t.name === tableName ? table : t));
270
-
271
321
  return {
272
322
  ...schema,
273
323
  tables,
@@ -327,9 +377,14 @@ export const allTablesAtom = atom((get) => {
327
377
  const isDefaultDb =
328
378
  database.name === conn.default_database || conn.databases.length === 1;
329
379
 
330
- for (const schema of database.schemas) {
331
- const isDefaultSchema = schema.name === conn.default_schema;
332
- const schemalessDb = isSchemaless(schema.name);
380
+ // Walk schemas recursively so nested namespaces (e.g. Iceberg
381
+ // `top.nested`) are enumerated. `segments` is the path of named
382
+ // (non-schemaless) namespace segments down to this schema.
383
+ const walkSchema = (schema: DatabaseSchema, segments: string[]): void => {
384
+ const schemalessDb = segments.length === 0;
385
+ const isDefaultSchema =
386
+ segments.length === 1 && segments[0] === conn.default_schema;
387
+ const schemaQualifier = segments.join(".");
333
388
 
334
389
  for (const table of schema.tables) {
335
390
  let nameToSave: string;
@@ -358,14 +413,14 @@ export const allTablesAtom = atom((get) => {
358
413
  continue;
359
414
  }
360
415
 
361
- nameToSave = `${schema.name}.${table.name}`;
416
+ nameToSave = `${schemaQualifier}.${table.name}`;
362
417
 
363
418
  if (isDefaultDb && !tableNames.has(nameToSave)) {
364
419
  tableNames.set(nameToSave, table);
365
420
  continue;
366
421
  }
367
422
 
368
- nameToSave = `${database.name}.${schema.name}.${table.name}`;
423
+ nameToSave = `${database.name}.${schemaQualifier}.${table.name}`;
369
424
 
370
425
  if (tableNames.has(nameToSave)) {
371
426
  Logger.warn(`Table name collision for ${nameToSave}. Skipping.`);
@@ -373,6 +428,15 @@ export const allTablesAtom = atom((get) => {
373
428
  tableNames.set(nameToSave, table);
374
429
  }
375
430
  }
431
+
432
+ // Recurse into nested child namespaces. Children are always named.
433
+ for (const child of schema.child_schemas ?? []) {
434
+ walkSchema(child, [...segments, child.name]);
435
+ }
436
+ };
437
+
438
+ for (const schema of database.schemas) {
439
+ walkSchema(schema, isSchemaless(schema.name) ? [] : [schema.name]);
376
440
  }
377
441
  }
378
442
  }
@@ -1 +0,0 @@
1
- import{s as I}from"./chunk-LvLJmgfZ.js";import{u as F}from"./useEvent-D91BmmQi.js";import{t as H}from"./react-Bj1aDYRI.js";import{Br as J,D as R,Hr as M}from"./cells-DOA0Gew8.js";import{t as G}from"./compiler-runtime-B3qBwwSJ.js";import{c as K}from"./utils-CvI-39U6.js";import{t as U}from"./jsx-runtime-BqBOg78p.js";import{c as V,d as W,gt as X}from"./JsonOutput-DRNPZOvX.js";import{t as q}from"./tooltip-B_PkSKN3.js";import{r as D,t as A}from"./button-BbCh-29a.js";import{r as Y}from"./requests-DIwGYs0l.js";import{t as Z}from"./createLucideIcon-D5guW7EU.js";import{t as ee}from"./useLifecycle-DieWOfXE.js";import{t as te}from"./spinner-Bhir8k53.js";import{r as ae}from"./useTheme-CI2eq4XN.js";import{t as re}from"./copy-icon-BuRdHNPA.js";import{t as se}from"./context-QkTujrKn.js";import{i as oe}from"./numbers-DGxQfQ-A.js";import{n as ne}from"./html-to-image-CyAtzePO.js";import{o as ce}from"./focus-Ldqh99xE.js";var P=Z("square-plus",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}],["path",{d:"M8 12h8",key:"1wcyev"}],["path",{d:"M12 8v8",key:"napkw2"}]]),L=G(),le=I(H(),1),s=I(U(),1);const ie=i=>{let e=(0,L.c)(53),{table:t,column:a,preview:r,onAddColumnChart:c,sqlTableContext:o}=i,{theme:u}=ae(),{previewDatasetColumn:l}=Y(),{locale:d}=se(),m;e[0]!==a.name||e[1]!==l||e[2]!==o||e[3]!==t.name||e[4]!==t.source||e[5]!==t.source_type?(m=()=>{l({source:t.source,tableName:t.name,columnName:a.name,sourceType:t.source_type,fullyQualifiedTableName:o?`${o.database}.${o.schema}.${t.name}`:t.name})},e[0]=a.name,e[1]=l,e[2]=o,e[3]=t.name,e[4]=t.source,e[5]=t.source_type,e[6]=m):m=e[6];let p=m,f;if(e[7]!==r||e[8]!==p||e[9]!==t.source_type?(f=()=>{r||t.source_type==="connection"||t.source_type==="catalog"||p()},e[7]=r,e[8]=p,e[9]=t.source_type,e[10]=f):f=e[10],ee(f),t.source_type==="connection"){let n=a.name,E=a.external_type,x;e[11]!==a.name||e[12]!==c||e[13]!==o||e[14]!==t?(x=D.stopPropagation(()=>{c(M({table:t,columnName:a.name,sqlTableContext:o}))}),e[11]=a.name,e[12]=c,e[13]=o,e[14]=t,e[15]=x):x=e[15];let C;e[16]===Symbol.for("react.memo_cache_sentinel")?(C=(0,s.jsx)(P,{className:"h-3 w-3 mr-1"}),e[16]=C):C=e[16];let h;e[17]===x?h=e[18]:(h=(0,s.jsxs)(A,{variant:"outline",size:"xs",onClick:x,children:[C," Add SQL cell"]}),e[17]=x,e[18]=h);let S;return e[19]!==a.external_type||e[20]!==a.name||e[21]!==h?(S=(0,s.jsxs)("span",{className:"text-xs text-muted-foreground gap-2 flex items-center justify-between pl-7",children:[n," (",E,")",h]}),e[19]=a.external_type,e[20]=a.name,e[21]=h,e[22]=S):S=e[22],S}if(t.source_type==="catalog"){let n;return e[23]!==a.external_type||e[24]!==a.name?(n=(0,s.jsxs)("span",{className:"text-xs text-muted-foreground gap-2 flex items-center justify-between pl-7",children:[a.name," (",a.external_type,")"]}),e[23]=a.external_type,e[24]=a.name,e[25]=n):n=e[25],n}if(!r){let n;return e[26]===Symbol.for("react.memo_cache_sentinel")?(n=(0,s.jsx)("span",{className:"text-xs text-muted-foreground",children:"Loading..."}),e[26]=n):n=e[26],n}let g;e[27]!==r.error||e[28]!==r.missing_packages||e[29]!==p?(g=r.error&&O({error:r.error,missingPackages:r.missing_packages,refetchPreview:p}),e[27]=r.error,e[28]=r.missing_packages,e[29]=p,e[30]=g):g=e[30];let y=g,_;e[31]!==a.type||e[32]!==d||e[33]!==r.stats?(_=r.stats&&Q({stats:r.stats,dataType:a.type,locale:d}),e[31]=a.type,e[32]=d,e[33]=r.stats,e[34]=_):_=e[34];let b=_,j;e[35]!==r.chart_spec||e[36]!==u?(j=r.chart_spec&&$(r.chart_spec,u),e[35]=r.chart_spec,e[36]=u,e[37]=j):j=e[37];let N=j,k;e[38]!==r.chart_code||e[39]!==t.source_type?(k=r.chart_code&&t.source_type==="local"&&(0,s.jsx)(B,{chartCode:r.chart_code}),e[38]=r.chart_code,e[39]=t.source_type,e[40]=k):k=e[40];let z=k,v;e[41]!==a.name||e[42]!==c||e[43]!==o||e[44]!==t?(v=t.source_type==="duckdb"&&(0,s.jsx)(q,{content:"Add SQL cell",delayDuration:400,children:(0,s.jsx)(A,{variant:"outline",size:"icon",className:"z-10 bg-background absolute right-1 -top-1",onClick:D.stopPropagation(()=>{c(M({table:t,columnName:a.name,sqlTableContext:o}))}),children:(0,s.jsx)(P,{className:"h-3 w-3"})})}),e[41]=a.name,e[42]=c,e[43]=o,e[44]=t,e[45]=v):v=e[45];let T=v;if(!y&&!b&&!N){let n;return e[46]===Symbol.for("react.memo_cache_sentinel")?(n=(0,s.jsx)("span",{className:"text-xs text-muted-foreground",children:"No data"}),e[46]=n):n=e[46],n}let w;return e[47]!==z||e[48]!==T||e[49]!==N||e[50]!==y||e[51]!==b?(w=(0,s.jsxs)(W,{children:[y,z,T,N,b]}),e[47]=z,e[48]=T,e[49]=N,e[50]=y,e[51]=b,e[52]=w):w=e[52],w};function O({error:i,missingPackages:e,refetchPreview:t}){return(0,s.jsxs)("div",{className:"text-xs text-muted-foreground p-2 border border-border rounded flex items-center justify-between",children:[(0,s.jsx)("span",{children:i}),e&&(0,s.jsx)(V,{packages:e,showMaxPackages:1,className:"w-32",onInstall:t})]})}function Q({stats:i,dataType:e,locale:t}){return(0,s.jsx)("div",{className:"gap-x-16 gap-y-1 grid grid-cols-2-fit border rounded p-2 empty:hidden",children:Object.entries(i).map(([a,r])=>r==null?null:(0,s.jsxs)("div",{className:"flex items-center gap-1 group",children:[(0,s.jsx)("span",{className:"text-xs min-w-[60px] capitalize",children:J(a,e)}),(0,s.jsx)("span",{className:"text-xs font-bold text-muted-foreground tracking-wide",children:oe(r,t)}),(0,s.jsx)(re,{className:"h-3 w-3 invisible group-hover:visible",value:String(r)})]},a))})}var me=(0,s.jsx)("div",{className:"flex justify-center",children:(0,s.jsx)(te,{className:"size-4"})});function $(i,e){let t=a=>({...a,background:"transparent",config:{...a.config,background:"transparent"}});return(0,s.jsx)(le.Suspense,{fallback:me,children:(0,s.jsx)(X,{"data-container-width":"container",spec:t(JSON.parse(i)),options:{theme:e==="dark"?"dark":"vox",height:100,width:"container",actions:!1,renderer:"canvas"}})})}const B=i=>{let e=(0,L.c)(10),{chartCode:t}=i,a=F(K),r=ce(),{createNewCell:c}=R(),o;e[0]!==a||e[1]!==c||e[2]!==r?(o=p=>{p.includes("alt")&&ne({autoInstantiate:a,createNewCell:c,fromCellId:r}),c({code:p,before:!1,cellId:r??"__end__"})},e[0]=a,e[1]=c,e[2]=r,e[3]=o):o=e[3];let u=o,l;e[4]!==t||e[5]!==u?(l=D.stopPropagation(()=>u(t)),e[4]=t,e[5]=u,e[6]=l):l=e[6];let d;e[7]===Symbol.for("react.memo_cache_sentinel")?(d=(0,s.jsx)(P,{className:"h-3 w-3"}),e[7]=d):d=e[7];let m;return e[8]===l?m=e[9]:(m=(0,s.jsx)(q,{content:"Add chart to notebook",delayDuration:400,children:(0,s.jsx)(A,{variant:"outline",size:"icon",className:"z-10 bg-background absolute right-1 -top-0.5",onClick:l,children:d})}),e[8]=l,e[9]=m),m};export{Q as a,O as i,ie as n,P as o,$ as r,B as t};