From 3596d6c90d845fa95c648ca853aff07f4314513e Mon Sep 17 00:00:00 2001 From: emrberk Date: Tue, 23 Sep 2025 19:15:48 +0300 Subject: [PATCH 1/6] fix(web-console): handle timestamp_ns format in CSV imports --- .../browser-tests/cypress/fixtures/nanos.csv | 8 +++ .../integration/console/import.spec.js | 66 ++++++++++++++++++- packages/browser-tests/questdb | 2 +- .../components/TableSchemaDialog/column.tsx | 11 +++- .../src/components/TableSchemaDialog/const.ts | 1 - .../components/TableSchemaDialog/dialog.tsx | 3 +- .../src/scenes/Import/ImportCSVFiles/const.ts | 1 + .../Import/ImportCSVFiles/file-status.tsx | 2 +- .../scenes/Import/ImportCSVFiles/index.tsx | 9 +-- .../Import/ImportCSVFiles/upload-actions.tsx | 1 + .../src/scenes/Import/ImportCSVFiles/utils.ts | 5 ++ .../src/scenes/Schema/Row/index.tsx | 4 +- .../src/utils/formatTableSchemaQuery.ts | 3 +- 13 files changed, 100 insertions(+), 16 deletions(-) create mode 100644 packages/browser-tests/cypress/fixtures/nanos.csv delete mode 100644 packages/web-console/src/components/TableSchemaDialog/const.ts diff --git a/packages/browser-tests/cypress/fixtures/nanos.csv b/packages/browser-tests/cypress/fixtures/nanos.csv new file mode 100644 index 000000000..123534ae2 --- /dev/null +++ b/packages/browser-tests/cypress/fixtures/nanos.csv @@ -0,0 +1,8 @@ +"timestamp","symbol","bids","asks" +"2025-09-12T23:26:12.696868898Z","USDCHF","[[0.7938],[72902.0]]","[[0.7942],[75254.0]]" +"2025-09-12T23:26:12.698402234Z","USDNOK","[[9.8337,9.8336],[56453.0,1.16831898E8]]","[[9.8341,9.8342],[64603.0,3.51343759E8]]" +"2025-09-12T23:26:12.701577329Z","USDCHF","[[0.7938],[65290.0]]","[[0.7942],[57934.0]]" +"2025-09-12T23:26:12.704578561Z","USDZAR","[[17.364],[53529.0]]","[[17.3644],[90619.0]]" +"2025-09-12T23:26:12.704809736Z","AUDNZD","[[1.1168,1.1167],[63576.0,8.71920887E8]]","[[1.1172,1.1173],[66756.0,2.540549E7]]" +"2025-09-12T23:26:12.706899510Z","USDHKD","[[7.778,7.7779],[91917.0,2.04926212E8]]","[[7.7784,7.7785],[68413.0,5.64775853E8]]" +"2025-09-12T23:26:12.710846084Z","USDZAR","[[17.364,17.3639],[98866.0,6.3619799E8]]","[[17.3644,17.3645],[67646.0,8.68348153E8]]" \ No newline at end of file diff --git a/packages/browser-tests/cypress/integration/console/import.spec.js b/packages/browser-tests/cypress/integration/console/import.spec.js index 8e85d51e6..8ad205521 100644 --- a/packages/browser-tests/cypress/integration/console/import.spec.js +++ b/packages/browser-tests/cypress/integration/console/import.spec.js @@ -1,17 +1,79 @@ /// describe("questdb import", () => { - before(() => { + beforeEach(() => { cy.loadConsoleWithAuth(); }); + afterEach(() => { + cy.loadConsoleWithAuth(); + cy.typeQuery("drop all tables;"); + cy.runLine(); + cy.getByDataHook("success-notification").should("be.visible"); + }); + it("display import panel", () => { cy.getByDataHook("import-panel-button").click(); cy.getByDataHook("import-dropbox").should("be.visible"); cy.getByDataHook("import-browse-from-disk").should("be.visible"); - cy.get('input[type="file"]').selectFile("cypress/fixtures/test.csv", { force: true }); + cy.get('input[type="file"]').selectFile("cypress/fixtures/test.csv", { + force: true, + }); cy.getByDataHook("import-table-column-schema").should("be.visible"); cy.getByDataHook("import-table-column-owner").should("not.exist"); }); + + it("should import csv with a nanosecond timestamp", () => { + cy.getByDataHook("import-panel-button").click(); + cy.getByDataHook("import-dropbox").should("be.visible"); + cy.getByDataHook("import-browse-from-disk").should("be.visible"); + + cy.get('input[type="file"]').selectFile("cypress/fixtures/nanos.csv", { + force: true, + }); + cy.getByDataHook("import-table-column-schema").should("be.visible"); + cy.getByDataHook("import-upload-button").should("be.enabled"); + cy.getByDataHook("import-upload-button").click(); + + cy.getByDataHook("import-file-status").should("contain", "Imported 7 rows"); + cy.getByDataHook("schema-table-title").should("contain", "nanos.csv"); + + cy.getByDataHook("schema-table-title").dblclick(); + cy.getByDataHook("schema-folder-title").contains("Columns").dblclick(); + cy.get('[data-id="questdb:expanded:tables:nanos.csv:columns:timestamp"]') + .should("be.visible") + .should("contain", "timestamp") + .should("contain", "TIMESTAMP_NS"); + cy.getByDataHook("designated-timestamp-icon").should("not.exist"); + + cy.getByDataHook("table-schema-dialog-trigger") + .should("be.visible") + .should("contain", "4 cols"); + cy.getByDataHook("table-schema-dialog-trigger").click(); + + cy.getByDataHook("create-table-panel").should("be.visible"); + cy.getByDataHook("table-schema-dialog-column-0").should("be.visible"); + cy.get("input[name='schemaColumns.0.name']").should( + "have.value", + "timestamp" + ); + + cy.get("select[name='schemaColumns.0.type']") + .get("option[value='TIMESTAMP_NS']") + .should("be.selected"); + + cy.getByDataHook("table-schema-dialog-column-0-designated-button").click(); + cy.getByDataHook("form-submit-button").click(); + + cy.getByDataHook("create-table-panel").should("not.be.visible"); + + cy.get('select[name="overwrite"]').select("true"); + + cy.getByDataHook("import-upload-button").should("be.enabled"); + cy.getByDataHook("import-upload-button").click(); + + cy.getByDataHook("import-file-status").should("contain", "Imported 7 rows"); + cy.getByDataHook("designated-timestamp-icon").should("be.visible"); + }); }); diff --git a/packages/browser-tests/questdb b/packages/browser-tests/questdb index 926556fb6..7f02ddd4f 160000 --- a/packages/browser-tests/questdb +++ b/packages/browser-tests/questdb @@ -1 +1 @@ -Subproject commit 926556fb65136bbdc6aa0e55ebbb296dd7961123 +Subproject commit 7f02ddd4fa5f27cb92839435c0b5240ae4b37758 diff --git a/packages/web-console/src/components/TableSchemaDialog/column.tsx b/packages/web-console/src/components/TableSchemaDialog/column.tsx index 789a31fa8..a89a6d938 100644 --- a/packages/web-console/src/components/TableSchemaDialog/column.tsx +++ b/packages/web-console/src/components/TableSchemaDialog/column.tsx @@ -3,12 +3,14 @@ import { Form } from "../../components/Form" import {IconWithTooltip, Link, Text} from "../../components" import { Box } from "../../components/Box" import { Button } from "@questdb/react-components" -import { DEFAULT_TIMESTAMP_FORMAT } from "./const" +import { DEFAULT_TIMESTAMP_FORMAT, DEFAULT_TIMESTAMP_FORMAT_NS } from "../../scenes/Import/ImportCSVFiles/const" import styled from "styled-components" import { SchemaColumn } from "utils" import { Controls } from "./controls" import { Action } from "./types" import { DocsLink } from "./docs-link" +import { isTimestamp } from "../../scenes/Import/ImportCSVFiles/utils" +import { getTimestampFormat } from "../../scenes/Import/ImportCSVFiles/utils" const supportedColumnTypes: { label: string; value: string }[] = [ { label: "AUTO", value: "" }, @@ -29,6 +31,7 @@ const supportedColumnTypes: { label: string; value: string }[] = [ { label: "VARCHAR", value: "VARCHAR" }, { label: "SYMBOL", value: "SYMBOL" }, { label: "TIMESTAMP", value: "TIMESTAMP" }, + { label: "TIMESTAMP_NS", value: "TIMESTAMP_NS" }, { label: "UUID", value: "UUID" }, ] @@ -85,6 +88,7 @@ export const Column = ({ key={column.name} odd={index % 2 !== 0} disabled={disabled} + data-hook={`table-schema-dialog-column-${index}`} onFocus={() => onFocus(index)} > @@ -117,7 +121,7 @@ export const Column = ({ - {column.type === "TIMESTAMP" && ( + {isTimestamp(column.type) && ( {action === "import" && ( @@ -128,7 +132,7 @@ export const Column = ({ defaultValue={ column.pattern !== "" ? column.pattern - : DEFAULT_TIMESTAMP_FORMAT + : getTimestampFormat(column.type) } required /> @@ -139,6 +143,7 @@ export const Column = ({ icon={