Skip to content
Open
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
15 changes: 8 additions & 7 deletions src/scenes/Schema/Row/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ import { Checkbox } from "../checkbox"
import { PopperHover } from "../../../components/PopperHover"
import { Tooltip } from "../../../components/Tooltip"
import { mapColumnTypeToUI } from "../../../scenes/Import/ImportCSVFiles/utils"
import { MATVIEWS_GROUP_KEY, TABLES_GROUP_KEY } from "../localStorageUtils"
import { MATVIEWS_GROUP_KEY, TABLES_GROUP_KEY, VIEWS_GROUP_KEY } from "../localStorageUtils"
import { TreeNavigationOptions } from "../VirtualTables"

export type TreeNodeKind = "column" | "table" | "matview" | "folder" | "detail"
export type TreeNodeKind = "column" | "table" | "matview" | "view" | "folder" | "detail"

type Props = Readonly<{
id: string
Expand Down Expand Up @@ -334,12 +334,12 @@ const Row = ({
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const wrapperRef = useRef<HTMLDivElement>(null)
const isExpandable =
["folder", "table", "matview"].includes(kind) ||
["folder", "table", "matview", "view"].includes(kind) ||
(kind === "column" && type === "SYMBOL")
const isTableKind = ["table", "matview"].includes(kind)
const isRootFolder = [MATVIEWS_GROUP_KEY, TABLES_GROUP_KEY].includes(id ?? "")
const isTableKind = ["table", "matview", "view"].includes(kind)
const isRootFolder = [MATVIEWS_GROUP_KEY, TABLES_GROUP_KEY, VIEWS_GROUP_KEY].includes(id ?? "")
const matchesSearch =
["column", "table", "matview"].includes(kind) &&
["column", "table", "matview", "view"].includes(kind) &&
query &&
name.toLowerCase().includes(query.toLowerCase())

Expand Down Expand Up @@ -500,10 +500,11 @@ const Row = ({
partitionBy={partitionBy}
walEnabled={walEnabled}
isMaterializedView={kind === "matview"}
isView={kind === "view"}
/>
)}
{kind === "detail" && <InfoCircle size="14px" />}
{["column", "table", "matview"].includes(kind) ? (
{["column", "table", "matview", "view"].includes(kind) ? (
<Highlighter
highlightClassName="highlight"
searchWords={[query ?? ""]}
Expand Down
125 changes: 92 additions & 33 deletions src/scenes/Schema/VirtualTables/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
setSectionExpanded,
TABLES_GROUP_KEY,
MATVIEWS_GROUP_KEY,
VIEWS_GROUP_KEY,
} from "../localStorageUtils"
import { useSchema } from "../SchemaContext"
import { QuestContext } from "../../../providers"
Expand All @@ -50,6 +51,7 @@ type VirtualTablesProps = {
tables: QuestDB.Table[]
walTables?: QuestDB.WalTable[]
materializedViews?: QuestDB.MaterializedView[]
views?: QuestDB.View[]
filterSuspendedOnly: boolean
state: State
loadingError: ErrorResult | null
Expand Down Expand Up @@ -82,6 +84,7 @@ export type FlattenedTreeItem = {
table?: QuestDB.Table
column?: TreeColumn
matViewData?: QuestDB.MaterializedView
viewData?: QuestDB.View
walTableData?: QuestDB.WalTable
parent?: string
isExpanded?: boolean
Expand Down Expand Up @@ -155,6 +158,7 @@ const VirtualTables: FC<VirtualTablesProps> = ({
tables,
walTables,
materializedViews,
views,
filterSuspendedOnly,
state,
loadingError,
Expand All @@ -181,7 +185,7 @@ const VirtualTables: FC<VirtualTablesProps> = ({
const wrapperRef = useRef<HTMLDivElement>(null)
useRetainLastFocus({ virtuosoRef, focusedIndex, setFocusedIndex, wrapperRef })

const [regularTables, matViewTables] = useMemo(() => {
const [regularTables, matViewTables, viewTables] = useMemo(() => {
return tables
.reduce(
(acc, table: QuestDB.Table) => {
Expand All @@ -200,15 +204,19 @@ const VirtualTables: FC<VirtualTablesProps> = ({
const shownIfFilteredWithQuery = tableNameMatches || columnMatches

if (shownIfFilteredSuspendedOnly && shownIfFilteredWithQuery) {
acc[table.matView ? 1 : 0].push({
// Use table_type to categorize: 'T' = table, 'M' = matview, 'V' = view
// Default to 'T' (table) for backward compatibility with older servers
const tableType = table.table_type ?? "T"
const categoryIndex = tableType === "M" ? 1 : tableType === "V" ? 2 : 0
Comment on lines 206 to +210
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

Views are being filtered based on suspension status when filterSuspendedOnly is true. However, regular views typically don't have WAL support since they're virtual and cannot be suspended. This logic may incorrectly filter out views when the "Show suspended only" filter is active. Consider excluding views (table_type === 'V') from the suspension filter logic.

Copilot uses AI. Check for mistakes.
acc[categoryIndex].push({
...table,
hasColumnMatches: columnMatches,
})
return acc
}
return acc
},
[[], []] as (QuestDB.Table & { hasColumnMatches: boolean })[][],
[[], [], []] as (QuestDB.Table & { hasColumnMatches: boolean })[][],
)
.map((tables) =>
tables.sort((a, b) =>
Expand All @@ -224,23 +232,22 @@ const VirtualTables: FC<VirtualTablesProps> = ({
}, [] as FlattenedTreeItem[])
}, [schemaTree])

const handleCopyQuery = async (tableName: string, isMatView: boolean) => {
const handleCopyQuery = async (tableName: string, kind: "table" | "matview" | "view") => {
try {
let response
if (isMatView) {
response = await quest.showMatViewDDL(tableName)
} else {
response = await quest.showTableDDL(tableName)
}
const response =
kind === "matview"
? await quest.showMatViewDDL(tableName)
: kind === "view"
? await quest.showViewDDL(tableName)
: await quest.showTableDDL(tableName)

if (response?.type === QuestDB.Type.DQL && response.data?.[0]?.ddl) {
await copyToClipboard(response.data[0].ddl)
toast.success("Schema copied to clipboard")
}
} catch (error) {
toast.error(
`Cannot copy schema for ${isMatView ? "materialized view" : "table"} '${tableName}'`,
)
const kindLabel = kind === "matview" ? "materialized view" : kind === "view" ? "view" : "table"
toast.error(`Cannot copy schema for ${kindLabel} '${tableName}'`)
}
}

Expand Down Expand Up @@ -474,18 +481,24 @@ const VirtualTables: FC<VirtualTablesProps> = ({
)
}

if (item.id === TABLES_GROUP_KEY || item.id === MATVIEWS_GROUP_KEY) {
if (item.id === TABLES_GROUP_KEY || item.id === MATVIEWS_GROUP_KEY || item.id === VIEWS_GROUP_KEY) {
const isTable = item.id === TABLES_GROUP_KEY
const isMatView = item.id === MATVIEWS_GROUP_KEY
const isView = item.id === VIEWS_GROUP_KEY
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

Unused variable isView.

Suggested change
const isView = item.id === VIEWS_GROUP_KEY

Copilot uses AI. Check for mistakes.
const isEmpty = isTable
? regularTables.length === 0
: isMatView
? matViewTables.length === 0
: viewTables.length === 0
const hookLabel = isTable ? "tables" : isMatView ? "materialized-views" : "views"
return (
<SectionHeader
$disabled={
isTable ? regularTables.length === 0 : matViewTables.length === 0
}
$disabled={isEmpty}
name={item.name}
kind="folder"
index={index}
expanded={item.isExpanded}
data-hook={`${item.isExpanded ? "collapse" : "expand"}-${isTable ? "tables" : "materialized-views"}`}
data-hook={`${item.isExpanded ? "collapse" : "expand"}-${hookLabel}`}
onExpandCollapse={() => toggleNodeExpansion(item.id)}
id={item.id}
navigateInTree={navigateInTree}
Expand All @@ -507,7 +520,8 @@ const VirtualTables: FC<VirtualTablesProps> = ({
)
}

if (item.kind === "table" || item.kind === "matview") {
if (item.kind === "table" || item.kind === "matview" || item.kind === "view") {
const canSuspend = item.kind !== "view" // Views cannot be suspended
return (
<>
<ContextMenu
Expand Down Expand Up @@ -535,13 +549,18 @@ const VirtualTables: FC<VirtualTablesProps> = ({
`Materialized view is invalid${item.matViewData?.invalidation_reason && `: ${item.matViewData?.invalidation_reason}`}`,
]
: []),
...(item.viewData?.view_status === "invalid"
? [
`View is invalid${item.viewData?.invalidation_reason && `: ${item.viewData?.invalidation_reason}`}`,
]
: []),
...(item.walTableData?.suspended ? [`Suspended`] : []),
]}
/>
{item.walTableData?.suspended && (
{canSuspend && item.walTableData?.suspended && (
<SuspensionDialog
walTableData={item.walTableData}
kind={item.kind}
kind={item.kind as "table" | "matview"}
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

Type assertion might be unsafe here. The item.kind could theoretically be "view", but the SuspensionDialog is being passed item.kind as "table" | "matview" which excludes "view". While the code does check canSuspend before rendering the dialog (which ensures kind !== "view"), TypeScript doesn't recognize this relationship. Consider refactoring to make the type narrowing more explicit, or ensure SuspensionDialog can handle all valid kinds.

Copilot uses AI. Check for mistakes.
open={openedSuspensionDialog === item.id}
onOpenChange={(isOpen) => {
setOpenedSuspensionDialog(isOpen ? item.id : null)
Expand All @@ -554,23 +573,25 @@ const VirtualTables: FC<VirtualTablesProps> = ({
<MenuItem
data-hook="table-context-menu-copy-schema"
onClick={async () =>
await handleCopyQuery(item.name, item.kind === "matview")
await handleCopyQuery(item.name, item.kind as "table" | "matview" | "view")
}
icon={<FileCopy size={16} />}
>
Copy schema
</MenuItem>
<MenuItem
data-hook="table-context-menu-resume-wal"
onClick={() =>
item.walTableData?.suspended &&
setTimeout(() => setOpenedSuspensionDialog(item.id))
}
icon={<Restart size={16} />}
disabled={!item.walTableData?.suspended}
>
Resume WAL
</MenuItem>
{canSuspend && (
<MenuItem
data-hook="table-context-menu-resume-wal"
onClick={() =>
item.walTableData?.suspended &&
setTimeout(() => setOpenedSuspensionDialog(item.id))
}
icon={<Restart size={16} />}
disabled={!item.walTableData?.suspended}
>
Resume WAL
</MenuItem>
)}
</ContextMenuContent>
</ContextMenu>
</>
Expand Down Expand Up @@ -600,6 +621,7 @@ const VirtualTables: FC<VirtualTablesProps> = ({
flattenedItems,
regularTables,
matViewTables,
viewTables,
toggleNodeExpansion,
openedContextMenu,
openedSuspensionDialog,
Expand All @@ -624,7 +646,9 @@ const VirtualTables: FC<VirtualTablesProps> = ({
table,
TABLES_GROUP_KEY,
false,
false,
materializedViews,
views,
walTables,
allColumns[table.table_name] ?? [],
)
Expand Down Expand Up @@ -654,7 +678,40 @@ const VirtualTables: FC<VirtualTablesProps> = ({
table,
MATVIEWS_GROUP_KEY,
true,
false,
materializedViews,
views,
walTables,
allColumns[table.table_name] ?? [],
)
if (table.hasColumnMatches) {
node.isExpanded = true
const columnsFolder = node.children.find((child) =>
child.id.endsWith(":columns"),
)
if (columnsFolder) {
columnsFolder.isExpanded = true
}
}
return node
}),
},
[VIEWS_GROUP_KEY]: {
id: VIEWS_GROUP_KEY,
kind: "folder",
name: `Views (${viewTables.length})`,
isExpanded:
viewTables.length === 0
? false
: getSectionExpanded(VIEWS_GROUP_KEY),
children: viewTables.map((table) => {
const node = createTableNode(
table,
VIEWS_GROUP_KEY,
false,
true,
materializedViews,
views,
walTables,
allColumns[table.table_name] ?? [],
)
Expand All @@ -679,7 +736,9 @@ const VirtualTables: FC<VirtualTablesProps> = ({
state.view,
regularTables,
matViewTables,
viewTables,
materializedViews,
views,
walTables,
allColumns,
])
Expand Down
32 changes: 23 additions & 9 deletions src/scenes/Schema/VirtualTables/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,26 +131,35 @@ export const createTableNode = (
table: QuestDB.Table,
parentId: string,
isMatView: boolean = false,
isView: boolean = false,
materializedViews: QuestDB.MaterializedView[] | undefined,
views: QuestDB.View[] | undefined,
walTables: QuestDB.WalTable[] | undefined,
tableColumns: InformationSchemaColumn[],
): TreeNode => {
const tableId = `${parentId}:${table.table_name}`
const matViewData = isMatView
? materializedViews?.find((mv) => mv.view_name === table.table_name)
: undefined
const viewData = isView
? views?.find((v) => v.view_name === table.table_name)
: undefined
const walTableData = walTables?.find((wt) => wt.name === table.table_name)

const columnsId = `${tableId}:columns`
const baseTablesId = `${tableId}:baseTables`
const storageDetailsId = `${tableId}:storageDetails`

// Determine the kind
const kind = isMatView ? "matview" : isView ? "view" : "table"

const tableNode: TreeNode = {
id: tableId,
kind: isMatView ? "matview" : "table",
kind,
name: table.table_name,
table,
matViewData,
viewData,
parent: parentId,
isExpanded: getSectionExpanded(tableId),
partitionBy: table.partitionBy,
Expand All @@ -167,14 +176,19 @@ export const createTableNode = (
isExpanded: getSectionExpanded(columnsId),
children: createColumnNodes(table, columnsId, tableColumns),
},
{
id: storageDetailsId,
kind: "folder",
name: "Storage details",
parent: tableId,
isExpanded: getSectionExpanded(storageDetailsId),
children: createStorageDetailsNodes(table, storageDetailsId),
},
// Only show storage details for tables and materialized views (not for regular views)
...(!isView
? [
{
id: storageDetailsId,
kind: "folder" as const,
name: "Storage details",
parent: tableId,
isExpanded: getSectionExpanded(storageDetailsId),
children: createStorageDetailsNodes(table, storageDetailsId),
},
]
: []),
],
}

Expand Down
Loading
Loading