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
3 changes: 3 additions & 0 deletions e2e/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ const materializedViewSchemas = {
btc_trades_mv:
"CREATE MATERIALIZED VIEW IF NOT EXISTS btc_trades_mv WITH BASE btc_trades as (" +
"SELECT timestamp, avg(amount) avg FROM btc_trades SAMPLE BY 1m) PARTITION BY week;",
btc_trades_mv_on_mv:
"CREATE MATERIALIZED VIEW IF NOT EXISTS btc_trades_mv_on_mv as (" +
"SELECT timestamp, avg(avg) avg FROM btc_trades_mv SAMPLE BY 1h) PARTITION BY week;",
}

const viewSchemas = {
Expand Down
2 changes: 1 addition & 1 deletion e2e/questdb
Submodule questdb updated 845 files
95 changes: 95 additions & 0 deletions e2e/tests/console/schema.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -692,3 +692,98 @@ describe("materialized views", () => {
})
})
})

describe("create materialized view from context menu", () => {
const sourceTable = "btc_trades"
const nonWalTable = "btc_trades_no_wal"
const nonPartitionedTable = "my_publics"
// btc_trades is PARTITION BY DAY → derived SAMPLE BY 1h → view name btc_trades_1h.
const generatedMatView = "btc_trades_1h"

before(() => {
cy.loadConsoleWithAuth()
cy.createTable(sourceTable)
cy.createTable(nonWalTable)
cy.createTable(nonPartitionedTable)
cy.refreshSchema()
})

after(() => {
cy.loadConsoleWithAuth()
cy.dropMaterializedView(generatedMatView)
cy.dropTableIfExists(sourceTable)
cy.dropTableIfExists(nonWalTable)
cy.dropTableIfExists(nonPartitionedTable)
})

it("disables the menu item for non-WAL and non-partitioned tables, and generates a runnable matview DDL from a valid source", () => {
cy.getByDataHook("schema-table-title").contains(nonWalTable).rightclick()
cy.getByDataHook("table-context-menu-create-matview").should(
"have.attr",
"data-disabled",
)
cy.realPress("Escape")

cy.getByDataHook("schema-table-title")
.contains(nonPartitionedTable)
.rightclick()
cy.getByDataHook("table-context-menu-create-matview").should(
"have.attr",
"data-disabled",
)
cy.realPress("Escape")

cy.clearEditor()
cy.getByDataHook("schema-table-title").contains(sourceTable).rightclick()
cy.getByDataHook("table-context-menu-create-matview")
.filter(":visible")
.click()

cy.runLine().clearEditor()

cy.refreshSchema()
cy.expandMatViews()
cy.getByDataHook("schema-matview-title").should("contain", generatedMatView)
})
})

describe("create materialized view from matview context menu", () => {
const sourceTable = "btc_trades"
const sourceMatView = "btc_trades_mv"
// btc_trades_mv is SAMPLE BY 1m → next rung 5m; name has no period token,
// so the generator appends `_5m`.
const generatedMatView = "btc_trades_mv_5m"

before(() => {
cy.loadConsoleWithAuth()
cy.createTable(sourceTable)
cy.createMaterializedView(sourceMatView)
cy.refreshSchema()
})

after(() => {
cy.loadConsoleWithAuth()
cy.dropMaterializedView(generatedMatView)
cy.dropMaterializedView(sourceMatView)
cy.dropTableIfExists(sourceTable)
})

it("generates a runnable chained matview DDL from a matview source", () => {
cy.expandMatViews()

cy.clearEditor()
cy.getByDataHook("schema-matview-title")
.contains(sourceMatView)
.rightclick()
cy.getByDataHook("table-context-menu-create-matview")
.filter(":visible")
.should("not.have.attr", "data-disabled")
.click()

cy.runLine().clearEditor()

cy.refreshSchema()
cy.expandMatViews()
cy.getByDataHook("schema-matview-title").should("contain", generatedMatView)
})
})
48 changes: 48 additions & 0 deletions e2e/tests/console/tableDetails.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const {
const TEST_TABLE = "btc_trades"
const TEST_TABLE_NO_WAL = "btc_trades_no_wal"
const TEST_MATVIEW = "btc_trades_mv"
const TEST_MATVIEW_ON_MV = "btc_trades_mv_on_mv"
const TEST_VIEW = "btc_trades_view"

function interceptTablesQuery(modifications) {
Expand Down Expand Up @@ -637,6 +638,53 @@ describe("TableDetailsDrawer", () => {
})
})

describe("materialized view based on another materialized view", () => {
before(() => {
cy.loadConsoleWithAuth()
cy.createTable(TEST_TABLE)
cy.createMaterializedView(TEST_MATVIEW)
cy.createMaterializedView(TEST_MATVIEW_ON_MV)
})

beforeEach(() => {
cy.loadConsoleWithAuth()
cy.refreshSchema()
cy.expandMatViews()
})

it("should open as matview and navigate to a matview base table preserving the matview kind", () => {
cy.openDetailsDrawer(TEST_MATVIEW_ON_MV, "matview")

cy.getByDataHook("table-details-type-badge").should(
"contain",
"Materialized View",
)

cy.getByDataHook("table-details-tab-details").click()

cy.getByDataHook("table-details-base-table-section").should("be.visible")
cy.getByDataHook("table-details-base-table-link").should(
"contain",
TEST_MATVIEW,
)

cy.getByDataHook("table-details-base-table-link").click()

cy.getByDataHook("table-details-name").should("have.value", TEST_MATVIEW)
cy.getByDataHook("table-details-type-badge").should(
"contain",
"Materialized View",
)
})

after(() => {
cy.loadConsoleWithAuth()
cy.dropMaterializedView(TEST_MATVIEW_ON_MV)
cy.dropMaterializedView(TEST_MATVIEW)
cy.dropTable(TEST_TABLE)
})
})

describe("materialized view invalid state (R2)", () => {
before(() => {
cy.loadConsoleWithAuth()
Expand Down
1 change: 1 addition & 0 deletions src/modules/ConsoleEventTracker/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export enum ConsoleEvent {
SCHEMA_RESUME_WAL_SUBMIT = "schema.resume_wal_submit",
SCHEMA_CONTEXT_COPY_DDL = "schema.context_copy_ddl",
SCHEMA_CONTEXT_EXPLAIN = "schema.context_explain",
SCHEMA_CONTEXT_CREATE_MATVIEW = "schema.context_create_matview",
SCHEMA_COPY_MULTIPLE = "schema.copy_multiple",

TABLE_DETAILS_TAB_SWITCH = "table_details.tab_switch",
Expand Down
1 change: 0 additions & 1 deletion src/modules/OAuth2/views/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { RawDqlResult } from "utils/questdb/types"
import { LoadingSpinner } from "../../../components/LoadingSpinner"
import { Box } from "../../../components/Box"


const LoginContainer = styled.div`
width: 100%;
height: 100%;
Expand Down
10 changes: 7 additions & 3 deletions src/scenes/Schema/TableDetailsDrawer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -259,17 +259,21 @@ export const TableDetailsDrawer = () => {

const handleNavigateToBaseTable = useCallback(() => {
if (!matViewData?.base_table_name || !baseTableExists) return
const baseTable = tables.find(
(t) => t.table_name === matViewData.base_table_name,
)
const kind = baseTable ? getTableKind(baseTable) : "table"
dispatch(
actions.console.pushSidebarHistory({
type: "tableDetails",
payload: {
tableName: matViewData.base_table_name,
isMatView: false,
isView: false,
isMatView: kind === "matview",
isView: kind === "view",
},
}),
)
}, [dispatch, matViewData?.base_table_name, baseTableExists])
}, [dispatch, matViewData?.base_table_name, baseTableExists, tables])

const { handleExplainSchema, handleAskAIForHealthIssue } = useAIQuickActions()

Expand Down
53 changes: 53 additions & 0 deletions src/scenes/Schema/VirtualTables/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import React, {
import { Virtuoso, VirtuosoHandle, ListRange } from "react-virtuoso"
import styled from "styled-components"
import { Loader3, FileCopy, Restart } from "@styled-icons/remix-line"
import { MaterializedViewIcon } from "../table-icon"
import { InfoIcon } from "@phosphor-icons/react"
import { spinAnimation, toast } from "../../../components"
import { trackEvent } from "../../../modules/ConsoleEventTracker"
Expand Down Expand Up @@ -48,6 +49,8 @@ import {
MenuItem,
} from "../../../components/ContextMenu"
import { copyToClipboard } from "../../../utils/copyToClipboard"
import { useEditor } from "../../../providers/EditorProvider"
import { generateMatViewDDL } from "../../../utils/generateMatViewDDL"
import { SuspensionDialog } from "../SuspensionDialog"
import {
useAIStatus,
Expand Down Expand Up @@ -199,6 +202,7 @@ const VirtualTables: FC<VirtualTablesProps> = ({
} = useAIStatus()

const { handleExplainSchema } = useAIQuickActions()
const { appendQuery } = useEditor()

const [schemaTree, setSchemaTree] = useState<SchemaTree>({})
const [openedContextMenu, setOpenedContextMenu] = useState<string | null>(
Expand Down Expand Up @@ -684,6 +688,55 @@ const VirtualTables: FC<VirtualTablesProps> = ({
>
Copy schema
</MenuItem>
{(item.kind === "table" || item.kind === "matview") && (
<span
title={
item.kind === "table" &&
(!item.table?.designatedTimestamp ||
!item.table?.walEnabled)
? "Only WAL tables with a designated timestamp can be used as a materialized view base"
: undefined
}
>
<MenuItem
data-hook="table-context-menu-create-matview"
onClick={async () => {
void trackEvent(
ConsoleEvent.SCHEMA_CONTEXT_CREATE_MATVIEW,
{ kind: item.kind },
)
const sourceDDL = await getTableSchema(
item.name,
item.kind as "table" | "matview",
)
if (!sourceDDL) return
try {
const existingNames = tables.map((t) => t.table_name)
const ddl = generateMatViewDDL(
sourceDDL,
existingNames,
)
appendQuery(ddl, { appendAt: "end" })
} catch (e) {
console.error(e)
toast.error(
e instanceof Error
? `Failed to generate materialized view DDL: ${e.message}`
: "Failed to generate materialized view DDL",
)
}
}}
icon={<MaterializedViewIcon size="16px" />}
disabled={
item.kind === "table" &&
(!item.table?.designatedTimestamp ||
!item.table?.walEnabled)
}
>
Create materialized view
</MenuItem>
</span>
)}
{isConfigured && (
<MenuItem
data-hook="table-context-menu-explain-schema"
Expand Down
Loading
Loading