From b7db21efcb5d412f6f92daa3208dcbcde60f91d2 Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Fri, 16 Jan 2026 17:34:52 +0100 Subject: [PATCH] snowflake --- frontend/e2e/DbManagerPage.ts | 92 ++++++++++--------- frontend/e2e/db-manager.spec.ts | 14 +-- frontend/e2e/utils.ts | 20 +++- frontend/src/lib/components/DBManager.svelte | 15 +-- .../src/lib/components/DBTableEditor.svelte | 3 +- 5 files changed, 83 insertions(+), 61 deletions(-) diff --git a/frontend/e2e/DbManagerPage.ts b/frontend/e2e/DbManagerPage.ts index 9c1a5dcb37..8e4c7d7f44 100644 --- a/frontend/e2e/DbManagerPage.ts +++ b/frontend/e2e/DbManagerPage.ts @@ -2,7 +2,7 @@ import { expect, Locator, Page } from '@playwright/test' import { getDbFeatures } from '../src/lib/components/apps/components/display/dbtable/dbFeatures' -import { ConfirmationModal, Toast } from './utils' +import { ConfirmationModal, Dropdown, Toast } from './utils' import { DbInput, DbType } from '../src/lib/components/dbTypes' import { DB_TYPES } from '../src/lib/consts' @@ -10,12 +10,12 @@ export async function runDbManagerSimpleCRUDTest(page: Page, dbType: _DbType) { let dbManager = new DbManagerPage(page) await dbManager.expectToBeVisible() - let friendTableName = `friend_${Date.now()}` + let friendTableName = identifier(dbType, `friend_${Date.now()}`) // Create table const tableEditor = await dbManager.openCreateTableDrawer() await tableEditor.setTableName(friendTableName) - await tableEditor.addColumn('name', getDbDatatype(dbType, 'TEXT')) + await tableEditor.addColumn(identifier(dbType, 'name'), getDbDatatype(dbType, 'TEXT')) await tableEditor.getColumn('id').delete() // remove default id column await tableEditor.createTable() @@ -26,7 +26,7 @@ export async function runDbManagerSimpleCRUDTest(page: Page, dbType: _DbType) { // Insert a row const insertDrawer = await dbManager.openInsertDrawer() - await insertDrawer.fillField('name', 'Alice') + await insertDrawer.fillField(identifier(dbType, 'name'), 'Alice') await insertDrawer.insert() await Toast.expectSuccess(page, 'Row inserted') @@ -53,12 +53,12 @@ export async function runDbManagerAlterTableTest(page: Page, dbType: _DbType) { await dbManager.expectToBeVisible() if (dbFeatures.schemas) { - const schemaName = `schema_${timestamp}` + const schemaName = identifier(dbType, `schema_${timestamp}`) await dbManager.setCurrentSchema(schemaName, { create: true }) } // Create friend table - let friendTableName = `friend_${timestamp}` + let friendTableName = identifier(dbType, `friend_${timestamp}`) let tableEditor = await dbManager.openCreateTableDrawer() await tableEditor.setTableName(friendTableName) let friendIdCol = await tableEditor.getColumn('id') // deafult id column @@ -68,28 +68,36 @@ export async function runDbManagerAlterTableTest(page: Page, dbType: _DbType) { } else { await expect(await friendIdCol.primaryKeyCheckbox()).toBeHidden() } - await tableEditor.addColumn('name', getDbDatatype(dbType, 'TEXT')) - await tableEditor.addColumn('created_at', getDbDatatype(dbType, 'TIMESTAMP')) + await tableEditor.addColumn(identifier(dbType, 'name'), getDbDatatype(dbType, 'TEXT')) + await tableEditor.addColumn(identifier(dbType, 'created_at'), getDbDatatype(dbType, 'TIMESTAMP')) await tableEditor.createTable() await Toast.expectSuccess(page, `${friendTableName} created`) await page.waitForTimeout(100) // Create message table - let messageTableName = `message_${timestamp}` + let messageTableName = identifier(dbType, `message_${timestamp}`) tableEditor = await dbManager.openCreateTableDrawer() await tableEditor.setTableName(messageTableName) let messageIdCol = await tableEditor.getColumn('id') // deafult id column await messageIdCol.setType(getDbDatatype(dbType, 'INT')) if (dbFeatures.primaryKeys) messageIdCol.setPrimaryKey(true) - await tableEditor.addColumn('friend_id', getDbDatatype(dbType, 'INT')) - let contentColumn = await tableEditor.addColumn('content', getDbDatatype(dbType, 'TEXT')) + await tableEditor.addColumn(identifier(dbType, 'friend_id'), getDbDatatype(dbType, 'INT')) + let contentColumn = await tableEditor.addColumn( + identifier(dbType, 'content'), + getDbDatatype(dbType, 'TEXT') + ) await contentColumn.setSettings({ nullable: true }) - await tableEditor.addColumn('created_at', getDbDatatype(dbType, 'TIMESTAMP')) + await tableEditor.addColumn(identifier(dbType, 'created_at'), getDbDatatype(dbType, 'TIMESTAMP')) if (dbFeatures.foreignKeys) { - await tableEditor.addForeignKey(friendTableName, 'friend_id', 'id', { - onDelete: 'Cascade', - onUpdate: 'Cascade' - }) + await tableEditor.addForeignKey( + friendTableName, + identifier(dbType, 'friend_id'), + identifier(dbType, 'id'), + { + onDelete: 'Cascade', + onUpdate: 'Cascade' + } + ) } else { await expect(tableEditor.foreignKeySection()).toBeHidden() } @@ -100,10 +108,10 @@ export async function runDbManagerAlterTableTest(page: Page, dbType: _DbType) { // Alter message table await (await dbManager.openActionsMenu(messageTableName)).alterTable() await tableEditor.expectNoChangesDetected() - let postsTableName = `posts_${timestamp}` - let friendCol = tableEditor.getColumn('friend_id') - let createdAtCol = tableEditor.getColumn('created_at') - let idCol = tableEditor.getColumn('id') + let postsTableName = identifier(dbType, `posts_${timestamp}`) + let friendCol = tableEditor.getColumn(identifier(dbType, 'friend_id')) + let createdAtCol = tableEditor.getColumn(identifier(dbType, 'created_at')) + let idCol = tableEditor.getColumn(identifier(dbType, 'id')) // Predicate checks if (dbFeatures.primaryKeys) { @@ -120,7 +128,7 @@ export async function runDbManagerAlterTableTest(page: Page, dbType: _DbType) { // Apply alterations await tableEditor.setTableName(postsTableName) await idCol.delete() - await friendCol.setName('person_id') + await friendCol.setName(identifier(dbType, 'person_id')) if (dbType !== 'bigquery' && dbType !== 'snowflake') { await friendCol.setType(getDbDatatype(dbType, 'BIGINT')) } @@ -144,7 +152,7 @@ export async function runDbManagerAlterTableTest(page: Page, dbType: _DbType) { await tableEditor.expectNoChangesDetected() tableEditor = new TableEditorDrawer(page) await idCol.checkNotExists() - await friendCol.checkNameIs('person_id') + await friendCol.checkNameIs(identifier(dbType, 'person_id')) if (dbType !== 'bigquery' && dbType !== 'snowflake') { await friendCol.checkTypeIs(getDbDatatype(dbType, 'BIGINT')) } @@ -152,7 +160,7 @@ export async function runDbManagerAlterTableTest(page: Page, dbType: _DbType) { await friendCol.checkSettingsIs({ defaultValue: /123/ }) } await createdAtCol.checkTypeIs(getDbDatatype(dbType, 'TIMESTAMP')) - await createdAtCol.checkNameIs('created_at') + await createdAtCol.checkNameIs(identifier(dbType, 'created_at')) if (dbFeatures.primaryKeys && dbType !== 'bigquery') { await friendCol.checkPrimaryKeyIs(true) await createdAtCol.checkPrimaryKeyIs(true) @@ -171,9 +179,9 @@ export class DbManagerPage { const schemaSelect = this.dbManager().locator('input[id="db-schema-select"]') await schemaSelect.click() await schemaSelect.fill(schemaName) - const option = this.page - .locator(`.select-dropdown-open li:has(:text-is("${schemaName}"))`) - .or(this.page.locator('.select-dropdown-open li:has-text("Add new")')) + const option = Dropdown.getOption(this.page, schemaName).or( + Dropdown.getOption(this.page, 'Add new', { exact: false }) + ) await option.click() if (options?.create) { await ConfirmationModal.confirm(this.page, '#db-create-schema-confirmation-modal', 'Create') @@ -270,21 +278,13 @@ class TableEditorDrawer { const fkSection = this.foreignKeySection() const addFkButton = fkSection.locator('button:has-text("Add")') await addFkButton.click() - const lastFkRow = fkSection.locator('tr').nth(-2) + const lastFk = fkSection.locator('tr').nth(-2) - const tableSelect = lastFkRow.locator('input.fk-table-select') - await tableSelect.click() - await this.page.locator(`.select-dropdown-open li:has(:text-is("${referencedTable}"))`).click() + await Dropdown.selectOption(this.page, lastFk.locator('input.fk-table-select'), referencedTable) + await Dropdown.selectOption(this.page, lastFk.locator('.fk-source-col-select input'), fromCol) + await Dropdown.selectOption(this.page, lastFk.locator('.fk-target-col-select input'), toCol) - const fromColSelect = lastFkRow.locator('.fk-source-col-select') - await fromColSelect.click() - await this.page.locator(`.select-dropdown-open li:has(:text-is("${fromCol}"))`).click() - - const toColSelect = lastFkRow.locator('.fk-target-col-select') - await toColSelect.click() - await this.page.locator(`.select-dropdown-open li:has(:text-is("${toCol}"))`).click() - - const fkSettings = lastFkRow.locator('.fk-settings-btn') + const fkSettings = lastFk.locator('.fk-settings-btn') if (options?.onDelete || options?.onUpdate) { await fkSettings.click() if (options?.onDelete) { @@ -352,9 +352,11 @@ class Column { } async setType(columnType: string) { - const newColTypeSelect = (await this.row()).locator('td').nth(1).locator('input') - await newColTypeSelect.click() - await this.page.locator(`.select-dropdown-open li:has(:text-is("${columnType}"))`).click() + await Dropdown.selectOption( + this.page, + (await this.row()).locator('td').nth(1).locator('input'), + columnType + ) } async checkTypeIs(columnType: string) { @@ -507,3 +509,9 @@ function getDbDatatype(dbType: _DbType, datatype: string): string { const allDataTypes = DB_TYPES[dbType == 'ducklake' ? 'duckdb' : dbType] || [] return allDataTypes.find((dt) => dt.toLowerCase() === datatype.toLowerCase()) || datatype } + +function identifier(dbType: _DbType, baseName: string): string { + baseName = baseName.replace(/[^a-zA-Z0-9_]/g, '_').trim() + if (dbType === 'snowflake') return baseName.toUpperCase() + return baseName +} diff --git a/frontend/e2e/db-manager.spec.ts b/frontend/e2e/db-manager.spec.ts index 1abb15455a..2ed328cb13 100644 --- a/frontend/e2e/db-manager.spec.ts +++ b/frontend/e2e/db-manager.spec.ts @@ -1,7 +1,7 @@ import { test, expect, Page, Locator } from '@playwright/test' import { runDbManagerAlterTableTest, runDbManagerSimpleCRUDTest } from './DbManagerPage' import { DbType } from '../src/lib/components/dbTypes' -import { Toast, prettify } from './utils' +import { Dropdown, Toast, prettify } from './utils' const resourceByDbType = { postgresql: getJsonEnv('POSTGRESQL_RESOURCE') ?? { @@ -94,7 +94,7 @@ async function openDucklakeDbManager(page: Page, resource_type: StorageResourceT let lastRow = page.locator('.ducklake-settings-table').locator('tr').nth(-2) await lastRow.locator('input.ducklake-name').fill(`ducklake_${timestamp}`) await lastRow.locator('.ducklake-workspace-storage-select').click() - await page.locator(`.select-dropdown-open li:has(:has-text("${storage}"))`).click() + await Dropdown.getOption(page, storage, { exact: false }).click() await lastRow.locator('input.ducklake-storage-data-path').fill(`ducklake_${timestamp}`) await setupCustomInstanceDb(lastRow, page, storage) await page.locator('button:has-text("Save ducklake settings")').click() @@ -173,8 +173,9 @@ async function setupCustomInstanceDb(row: Locator, page: Page, name: string) { await customInstanceDbSelect.fill(name) // Check if the database already exists in the dropdown - if (await page.locator(`.select-dropdown-open li:has(:text-is("${name}"))`).isVisible()) { - await page.locator(`.select-dropdown-open li:has(:text-is("${name}"))`).click() + + if (await Dropdown.getOption(page, name).isVisible()) { + await Dropdown.getOption(page, name).click() return } @@ -264,11 +265,12 @@ async function setupWsStorage( await lastRow.locator('#storage-resource-type-select').click() const dropdownResourceTypeLabel = storageResourceTypeDropdownLabels[resource_type] ?? resource_type - await page.locator(`.select-dropdown-open li:has(:text-is("${dropdownResourceTypeLabel}"))`) + + await Dropdown.getOption(page, dropdownResourceTypeLabel).click() // Select resource await lastRow.locator('#resource-picker-select').click() - await page.locator(`.select-dropdown-open li:has(:has-text("${resourceName}"))`).click() + await Dropdown.getOption(page, resourceName, { exact: false }).click() await page.locator('button:has-text("Save storage settings")').click() await Toast.expectSuccess(page, 'storage settings changed') diff --git a/frontend/e2e/utils.ts b/frontend/e2e/utils.ts index 9afa92db37..44ba3359e1 100644 --- a/frontend/e2e/utils.ts +++ b/frontend/e2e/utils.ts @@ -1,4 +1,4 @@ -import { expect, Page } from '@playwright/test' +import { expect, Locator, Page } from '@playwright/test' export class Toast { static async expectSuccess(page: Page, message: string) { @@ -26,3 +26,21 @@ export class ConfirmationModal { } export const prettify = (s: string) => (s.charAt(0).toUpperCase() + s.slice(1)).replace(/_/g, ' ') + +export class Dropdown { + static getOption(page: Page, optionText: string, { exact = true } = {}) { + if (exact) return page.locator(`.select-dropdown-open li:has(:text-is("${optionText}"))`) + else return page.locator(`.select-dropdown-open li:has-text("${optionText}")`) + } + static async selectOption( + page: Page, + selectInput: Locator, + optionText: string, + { exact = true } = {} + ) { + await selectInput.click() + await selectInput.fill(optionText) + const option = this.getOption(page, optionText, { exact }) + await option.click() + } +} diff --git a/frontend/src/lib/components/DBManager.svelte b/frontend/src/lib/components/DBManager.svelte index 24db41cd85..496bd3874a 100644 --- a/frontend/src/lib/components/DBManager.svelte +++ b/frontend/src/lib/components/DBManager.svelte @@ -220,14 +220,10 @@ let newSchemaName = $state('') // Check if the sanitized schema name already exists - const sanitizedNewSchemaName = $derived( - newSchemaName - .trim() - .toLowerCase() - .replace(/[^a-zA-Z0-9_]/g, '') - ) + const sanitizedNewSchemaName = $derived(newSchemaName.trim().replace(/[^a-zA-Z0-9_]/g, '')) const schemaAlreadyExists = $derived( - sanitizedNewSchemaName !== '' && schemaKeys.includes(sanitizedNewSchemaName) + sanitizedNewSchemaName !== '' && + schemaKeys.map((s) => s.toLowerCase()).includes(sanitizedNewSchemaName.toLowerCase()) ) @@ -247,10 +243,7 @@ placeholder="Search or create schema..." showPlaceholderOnOpen onCreateItem={(schema) => { - schema = schema - .trim() - .toLowerCase() - .replace(/[^a-zA-Z0-9_]/g, '') + schema = schema.trim().replace(/[^a-zA-Z0-9_]/g, '') askingForConfirmation = { confirmationText: `Create ${schema}`, type: 'reload', diff --git a/frontend/src/lib/components/DBTableEditor.svelte b/frontend/src/lib/components/DBTableEditor.svelte index 69818a0dd9..a6c6efb2dc 100644 --- a/frontend/src/lib/components/DBTableEditor.svelte +++ b/frontend/src/lib/components/DBTableEditor.svelte @@ -314,7 +314,8 @@ items={getFlatTableNamesFromSchema(dbSchema).map((o) => ({ value: o, label: - (currentSchema && o.startsWith(currentSchema)) || !features?.schemas + !features?.schemas || + (currentSchema && o.toLowerCase().startsWith(currentSchema.toLowerCase())) ? o.split('.')[1] : o }))}