Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions frontend/src/components/datasources/__tests__/filter-empty.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/* Copyright 2026 Marimo. All rights reserved. */

import { describe, expect, it } from "vitest";
import type { Database, DataTable } from "@/core/kernel/messages";
import { filterEmptyDatabases } from "../datasources";

function makeTable(name: string): DataTable {
return {
name,
columns: [],
source: "memory",
source_type: "local",
type: "table",
engine: null,
indexes: null,
num_columns: null,
num_rows: null,
variable_name: null,
primary_keys: null,
};
}

function makeDatabase(
name: string,
schemas: Array<{ name: string; tables: DataTable[] }>,
): Database {
return {
name,
dialect: "duckdb",
schemas,
engine: null,
};
}

describe("filterEmptyDatabases", () => {
it("hides schemas with no tables", () => {
const databases = [
makeDatabase("memory", [
{ name: "main", tables: [makeTable("t1")] },
{ name: "empty_schema", tables: [] },
]),
];

expect(filterEmptyDatabases(databases)).toEqual([
makeDatabase("memory", [{ name: "main", tables: [makeTable("t1")] }]),
]);
});

it("hides databases where every schema is empty", () => {
const databases = [
makeDatabase("only_empty", [
{ name: "a", tables: [] },
{ name: "b", tables: [] },
]),
makeDatabase("has_tables", [{ name: "main", tables: [makeTable("t1")] }]),
];

expect(filterEmptyDatabases(databases)).toEqual([
makeDatabase("has_tables", [{ name: "main", tables: [makeTable("t1")] }]),
]);
});

it("preserves databases with no schemas (lazy state)", () => {
const databases = [makeDatabase("not_loaded_yet", [])];

expect(filterEmptyDatabases(databases)).toEqual([
makeDatabase("not_loaded_yet", []),
]);
});

it("returns an empty list when all databases are empty", () => {
const databases = [
makeDatabase("a", [{ name: "main", tables: [] }]),
makeDatabase("b", [{ name: "main", tables: [] }]),
];

expect(filterEmptyDatabases(databases)).toEqual([]);
});

it("does not mutate the input", () => {
const databases = [
makeDatabase("memory", [
{ name: "main", tables: [makeTable("t1")] },
{ name: "empty_schema", tables: [] },
]),
];
const snapshot = JSON.parse(JSON.stringify(databases));

filterEmptyDatabases(databases);

expect(databases).toEqual(snapshot);
});
});
98 changes: 95 additions & 3 deletions frontend/src/components/datasources/datasources.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
/* Copyright 2026 Marimo. All rights reserved. */

import { CommandList } from "cmdk";
import { atom, useAtomValue, useSetAtom } from "jotai";
import { PlusIcon, PlusSquareIcon, XIcon } from "lucide-react";
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
import { atomWithStorage } from "jotai/utils";
import {
EyeIcon,
EyeOffIcon,
PlusIcon,
PlusSquareIcon,
XIcon,
} from "lucide-react";
import React from "react";
import { dbDisplayName } from "@/components/databases/display";
import { EngineVariable } from "@/components/databases/engine-variable";
Expand Down Expand Up @@ -52,6 +59,7 @@ import { sortBy } from "@/utils/arrays";
import { logNever } from "@/utils/assertNever";
import { cn } from "@/utils/cn";
import { Events } from "@/utils/events";
import { jotaiJsonStorage } from "@/utils/storage/jotai";
import {
DatabaseIcon,
SchemaIcon,
Expand Down Expand Up @@ -116,6 +124,51 @@ const sortedTablesAtom = atom((get) => {
});
});

/**
* Whether to hide empty schemas and databases (those with no tables) in the
* datasources panel.
*/
export const hideEmptyDatasourcesAtom = atomWithStorage<boolean>(
"marimo:datasources:hideEmpty",
false,
jotaiJsonStorage,
{ getOnInit: true },
);

/**
* Apply the "hide empty" filter to a connection's databases.
*
* - Schemas with no tables are hidden.
* - Databases are hidden when they have at least one schema and every schema
* is empty.
* - Databases with no schemas yet (lazy state) are preserved so users can
* still expand them to trigger a schema fetch.
*/
export function filterEmptyDatabases(databases: Database[]): Database[] {
let changed = false;
const result: Database[] = [];
for (const database of databases) {
if (database.schemas.length === 0) {
result.push(database);
continue;
}
const nonEmptySchemas = database.schemas.filter(
(schema) => schema.tables.length > 0,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The new empty-schema filter treats tables.length === 0 as definitively empty, but 0 is also the lazy-loading state. This can hide schemas/databases before table fetches run, preventing users from ever loading their tables.

Prompt for AI agents
Check if this issue is valid β€” if so, understand the root cause and fix it. At frontend/src/components/datasources/datasources.tsx, line 156:

<comment>The new empty-schema filter treats `tables.length === 0` as definitively empty, but `0` is also the lazy-loading state. This can hide schemas/databases before table fetches run, preventing users from ever loading their tables.</comment>

<file context>
@@ -116,6 +124,51 @@ const sortedTablesAtom = atom((get) => {
+      continue;
+    }
+    const nonEmptySchemas = database.schemas.filter(
+      (schema) => schema.tables.length > 0,
+    );
+    if (nonEmptySchemas.length === 0) {
</file context>

);
if (nonEmptySchemas.length === 0) {
changed = true;
continue;
}
if (nonEmptySchemas.length === database.schemas.length) {
result.push(database);
continue;
}
changed = true;
result.push({ ...database, schemas: nonEmptySchemas });
}
return changed ? result : databases;
}

/**
* This atom is used to get the data connections that are available to the user.
* It filters out the internal engines if it has no databases or if it has only the in-memory database and no schemas.
Expand Down Expand Up @@ -152,10 +205,27 @@ export const connectionsAtom = atom((get) => {

export const DataSources: React.FC = () => {
const [searchValue, setSearchValue] = React.useState<string>("");
const [hideEmpty, setHideEmpty] = useAtom(hideEmptyDatasourcesAtom);

const closeAllColumns = useSetAtom(closeAllColumnsAtom);
const tables = useAtomValue(sortedTablesAtom);
const dataConnections = useAtomValue(connectionsAtom);
const rawConnections = useAtomValue(connectionsAtom);

const dataConnections = React.useMemo(() => {
if (!hideEmpty) {
return rawConnections;
}
let changed = false;
const filtered = rawConnections.map((connection) => {
const databases = filterEmptyDatabases(connection.databases);
if (databases === connection.databases) {
return connection;
}
changed = true;
return { ...connection, databases };
});
return changed ? filtered : rawConnections;
}, [rawConnections, hideEmpty]);

if (tables.length === 0 && dataConnections.length === 0) {
return (
Expand Down Expand Up @@ -204,6 +274,28 @@ export const DataSources: React.FC = () => {
</button>
)}

<Tooltip
content={
hideEmpty
? "Show empty schemas and databases"
: "Hide empty schemas and databases"
}
>
<Button
data-testid="datasources-hide-empty-button"
variant="ghost"
size="sm"
className="px-2 rounded-none focus-visible:outline-hidden"
onClick={() => setHideEmpty(!hideEmpty)}
>
{hideEmpty ? (
<EyeOffIcon className="h-4 w-4" />
) : (
<EyeIcon className="h-4 w-4" />
)}
</Button>
</Tooltip>

<AddConnectionDialog>
<Button
variant="ghost"
Expand Down
6 changes: 0 additions & 6 deletions marimo/_sql/engines/ibis.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,12 +242,6 @@ def get_schemas(
include_table_details=include_table_details,
)

# ignore schemas with 0 tables
if len(tables) == 0:
LOGGER.debug(
f"No table found for schema `{schema_name}`. Not displaying schema."
)

schema = Schema(name=schema_name, tables=tables)
schemas.append(schema)

Expand Down
Loading