Skip to content

Commit 61e7d25

Browse files
authoredMar 20, 2025
feat(web-console): add materialized views support (#405)
1 parent 4cae3d4 commit 61e7d25

32 files changed

+1179
-611
lines changed
 

‎.github/workflows/ci.yml

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ jobs:
6565
QDB_DEV_MODE_ENABLED: "true"
6666
QDB_HTTP_USER: "admin"
6767
QDB_HTTP_PASSWORD: "quest"
68+
QDB_CAIRO_MAT_VIEW_ENABLED: "true"
6869

6970
- name: Run browser-tests test
7071
run: yarn workspace browser-tests test

‎.github/workflows/tests_with_context_path.yml

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ jobs:
6868
QDB_DEV_MODE_ENABLED: "true"
6969
QDB_HTTP_USER: "admin"
7070
QDB_HTTP_PASSWORD: "quest"
71+
QDB_CAIRO_MAT_VIEW_ENABLED: "true"
7172

7273
- name: Run browser-tests test
7374
run: yarn workspace browser-tests test

‎.pnp.cjs

+216-65
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

‎packages/browser-tests/cypress/commands.js

+79
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ const tableSchemas = {
3030
my_secrets2: "CREATE TABLE IF NOT EXISTS 'my_secrets2' (secret STRING);",
3131
};
3232

33+
const materializedViewSchemas = {
34+
btc_trades_mv:
35+
"CREATE MATERIALIZED VIEW IF NOT EXISTS btc_trades_mv WITH BASE btc_trades as (" +
36+
"SELECT timestamp, avg(amount) FROM btc_trades SAMPLE BY 1m) PARTITION BY week;",
37+
};
38+
3339
before(() => {
3440
Cypress.on("uncaught:exception", (err) => {
3541
// this error can be safely ignored
@@ -218,6 +224,19 @@ Cypress.Commands.add("createTable", (name) => {
218224
});
219225
});
220226

227+
Cypress.Commands.add("createMaterializedView", (name) => {
228+
const authHeader = localStorage.getItem("basic.auth.header");
229+
cy.request({
230+
method: "GET",
231+
url: `${baseUrl}/exec?query=${encodeURIComponent(
232+
materializedViewSchemas[name]
233+
)};`,
234+
headers: {
235+
Authorization: authHeader,
236+
},
237+
});
238+
});
239+
221240
Cypress.Commands.add("dropTable", (name) => {
222241
const authHeader = localStorage.getItem("basic.auth.header");
223242
cy.request({
@@ -229,6 +248,32 @@ Cypress.Commands.add("dropTable", (name) => {
229248
});
230249
});
231250

251+
Cypress.Commands.add("dropTableIfExists", (name) => {
252+
const authHeader = localStorage.getItem("basic.auth.header");
253+
cy.request({
254+
method: "GET",
255+
url: `${baseUrl}/exec?query=${encodeURIComponent(
256+
`DROP TABLE IF EXISTS ${name};`
257+
)}`,
258+
headers: {
259+
Authorization: authHeader,
260+
},
261+
});
262+
});
263+
264+
Cypress.Commands.add("dropMaterializedView", (name) => {
265+
const authHeader = localStorage.getItem("basic.auth.header");
266+
cy.request({
267+
method: "GET",
268+
url: `${baseUrl}/exec?query=${encodeURIComponent(
269+
`DROP MATERIALIZED VIEW ${name};`
270+
)}`,
271+
headers: {
272+
Authorization: authHeader,
273+
},
274+
});
275+
});
276+
232277
Cypress.Commands.add("interceptQuery", (query, alias, response) => {
233278
cy.intercept(
234279
{
@@ -289,6 +334,40 @@ Cypress.Commands.add("refreshSchema", () => {
289334
cy.getByDataHook("schema-auto-refresh-button").click();
290335
});
291336

337+
Cypress.Commands.add("expandTables", () => {
338+
cy.get("body").then((body) => {
339+
if (body.find('[data-hook="expand-tables"]').length > 0) {
340+
cy.get('[data-hook="expand-tables"]').click({ force: true });
341+
}
342+
});
343+
});
344+
345+
Cypress.Commands.add("collapseTables", () => {
346+
cy.get("body").then((body) => {
347+
if (body.find('[data-hook="collapse-tables"]').length > 0) {
348+
cy.get('[data-hook="collapse-tables"]').click({ force: true });
349+
}
350+
});
351+
});
352+
353+
Cypress.Commands.add("expandMatViews", () => {
354+
cy.get("body").then((body) => {
355+
if (body.find('[data-hook="expand-materialized-views"]').length > 0) {
356+
cy.get('[data-hook="expand-materialized-views"]').click({ force: true });
357+
}
358+
});
359+
});
360+
361+
Cypress.Commands.add("collapseMatViews", () => {
362+
cy.get("body").then((body) => {
363+
if (body.find('[data-hook="collapse-materialized-views"]').length > 0) {
364+
cy.get('[data-hook="collapse-materialized-views"]').click({
365+
force: true,
366+
});
367+
}
368+
});
369+
});
370+
292371
Cypress.Commands.add("getEditorTabs", () => {
293372
return cy.get(".chrome-tab");
294373
});

‎packages/browser-tests/cypress/integration/console/editor.spec.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,8 @@ describe("editor tabs", () => {
463463
cy.getByDataHook("editor-tabs-history-item").should("not.exist");
464464
});
465465

466-
it("should drag tabs", () => {
466+
// TODO: fix the flakiness
467+
it.skip("should drag tabs", () => {
467468
cy.get(".new-tab-button").click();
468469
cy.get(getTabDragHandleByTitle("SQL 1")).drag(
469470
getTabDragHandleByTitle("SQL")

‎packages/browser-tests/cypress/integration/console/schema.spec.js

+107-6
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,24 @@ const tables = [
1010
"gitlog",
1111
];
1212

13+
const materializedViews = ["btc_trades_mv"];
14+
1315
describe("questdb schema with working tables", () => {
1416
before(() => {
1517
cy.loadConsoleWithAuth();
1618

1719
tables.forEach((table) => {
1820
cy.createTable(table);
1921
});
22+
cy.expandTables();
2023
cy.refreshSchema();
2124
});
2225
it("should show all the tables when there are no suspended", () => {
2326
tables.forEach((table) => {
2427
cy.getByDataHook("schema-table-title").should("contain", table);
2528
});
2629
cy.getByDataHook("schema-filter-suspended-button").should("not.exist");
27-
cy.getByDataHook("schema-suspension-popover-trigger").should("not.exist");
30+
cy.getByDataHook("schema-row-error-icon").should("not.exist");
2831
});
2932

3033
it("should filter the table with input field", () => {
@@ -75,6 +78,7 @@ describe("questdb schema with suspended tables with Linux OS error codes", () =>
7578
});
7679
beforeEach(() => {
7780
cy.loadConsoleWithAuth();
81+
cy.expandTables();
7882
});
7983

8084
it("should work with 2 suspended tables, btc_trades and ecommerce_stats", () => {
@@ -97,9 +101,11 @@ describe("questdb schema with suspended tables with Linux OS error codes", () =>
97101
cy.getByDataHook("schema-filter-suspended-button").click();
98102
});
99103

100-
it("should show the suspension dialog on click with details for btc_trades", () => {
101-
cy.get('input[name="table_filter"]').click().type("btc_trades");
102-
cy.getByDataHook("schema-suspension-dialog-trigger").click();
104+
it("should show the suspension dialog on context menu click with details for btc_trades", () => {
105+
cy.getByDataHook("schema-table-title").contains("btc_trades").rightclick();
106+
cy.getByDataHook("table-context-menu-resume-wal")
107+
.filter(":visible")
108+
.click();
103109
cy.getByDataHook("schema-suspension-dialog").should(
104110
"have.attr",
105111
"data-table-name",
@@ -117,8 +123,10 @@ describe("questdb schema with suspended tables with Linux OS error codes", () =>
117123
});
118124

119125
it("should resume WAL for btc_trades from the suspension popover", () => {
120-
cy.get('input[name="table_filter"]').click().type("btc_trades");
121-
cy.contains("Suspended").click();
126+
cy.getByDataHook("schema-table-title").contains("btc_trades").rightclick();
127+
cy.getByDataHook("table-context-menu-resume-wal")
128+
.filter(":visible")
129+
.click();
122130
cy.getByDataHook("schema-suspension-dialog-restart-transaction").click();
123131
cy.getByDataHook("schema-suspension-dialog-dismiss").click();
124132
cy.getByDataHook("schema-suspension-dialog").should("not.exist");
@@ -144,6 +152,7 @@ describe("table select UI", () => {
144152
});
145153
beforeEach(() => {
146154
cy.loadConsoleWithAuth();
155+
cy.expandTables();
147156
});
148157

149158
it("should show select ui on click", () => {
@@ -208,3 +217,95 @@ describe("questdb schema in read-only mode", () => {
208217
cy.getByDataHook("create-table-panel").should("not.exist");
209218
});
210219
});
220+
221+
describe("materialized views", () => {
222+
before(() => {
223+
cy.loadConsoleWithAuth();
224+
225+
tables.forEach((table) => {
226+
cy.createTable(table);
227+
});
228+
materializedViews.forEach((mv) => {
229+
cy.createMaterializedView(mv);
230+
});
231+
cy.refreshSchema();
232+
});
233+
234+
afterEach(() => {
235+
cy.collapseTables();
236+
cy.collapseMatViews();
237+
});
238+
239+
it("should create materialized views", () => {
240+
cy.getByDataHook("expand-tables").contains(`Tables (${tables.length})`);
241+
cy.getByDataHook("expand-materialized-views").contains(
242+
`Materialized views (${materializedViews.length})`
243+
);
244+
245+
cy.expandTables();
246+
cy.getByDataHook("schema-table-title").should("contain", "btc_trades");
247+
cy.expandMatViews();
248+
cy.getByDataHook("schema-matview-title").should("contain", "btc_trades_mv");
249+
});
250+
251+
it("should show the base table and copy DDL for a materialized view", () => {
252+
cy.expandMatViews();
253+
cy.getByDataHook("schema-matview-title").contains("btc_trades_mv").click();
254+
cy.getByDataHook("base-table-name").contains("btc_trades").should("exist");
255+
cy.getByDataHook("schema-matview-title")
256+
.contains("btc_trades_mv")
257+
.rightclick();
258+
cy.getByDataHook("table-context-menu-copy-schema")
259+
.filter(":visible")
260+
.click();
261+
262+
if (Cypress.isBrowser("electron")) {
263+
cy.window()
264+
.its("navigator.clipboard")
265+
.invoke("readText")
266+
.should(
267+
"match",
268+
/^CREATE MATERIALIZED VIEW.*'btc_trades_mv' WITH BASE 'btc_trades'/
269+
);
270+
}
271+
});
272+
273+
it("should show a warning icon and tooltip when the view is invalidated", () => {
274+
cy.intercept({
275+
method: "GET",
276+
pathname: "/exec",
277+
query: {
278+
query: "materialized_views()",
279+
},
280+
},
281+
(req) => {
282+
req.continue((res) => {
283+
// [view_name, refresh_type, base_table_name, last_refresh_timestamp, view_sql, view_table_dir_name, invalidation_reason, view_status, base_table_txn, applied_base_table_txn]
284+
res.body.dataset[0][6] = "this is an invalidation reason";
285+
res.body.dataset[0][7] = "invalid";
286+
return res;
287+
});
288+
}
289+
);
290+
cy.refreshSchema();
291+
cy.expandMatViews();
292+
cy.getByDataHook("schema-row-error-icon").trigger("mouseover");
293+
294+
cy.getByDataHook("tooltip").should(
295+
"contain",
296+
"Materialized view is invalid: this is an invalidation reason"
297+
);
298+
});
299+
300+
after(() => {
301+
cy.loadConsoleWithAuth();
302+
303+
materializedViews.forEach((mv) => {
304+
cy.dropMaterializedView(mv);
305+
});
306+
307+
tables.forEach((table) => {
308+
cy.dropTableIfExists(table);
309+
});
310+
});
311+
});

‎packages/browser-tests/questdb

Submodule questdb updated 400 files

‎packages/web-console/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
"@monaco-editor/react": "^4.6.0",
3030
"@popperjs/core": "2.4.2",
3131
"@questdb/react-components": "workspace:^",
32-
"@questdb/sql-grammar": "1.2.2",
32+
"@questdb/sql-grammar": "1.2.3",
33+
"@radix-ui/react-context-menu": "2.1.5",
3334
"@radix-ui/react-dialog": "^1.0.3",
3435
"@styled-icons/bootstrap": "10.47.0",
3536
"@styled-icons/boxicons-logos": "10.47.0",
@@ -69,7 +70,6 @@
6970
"ramda": "0.27.1",
7071
"react": "17.0.2",
7172
"react-calendar": "^4.0.0",
72-
"react-contextmenu": "2.14.0",
7373
"react-dom": "17.0.2",
7474
"react-highlight-words": "^0.20.0",
7575
"react-hook-form": "7.22.3",

‎packages/web-console/src/components/ContextMenu/ContextMenu.tsx

-46
This file was deleted.

0 commit comments

Comments
 (0)
Failed to load comments.