snowflake

This commit is contained in:
Diego Imbert
2026-01-16 17:34:52 +01:00
parent 02eb644bcb
commit b7db21efcb
5 changed files with 83 additions and 61 deletions
+50 -42
View File
@@ -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
}
+8 -6
View File
@@ -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')
+19 -1
View File
@@ -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()
}
}
+4 -11
View File
@@ -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())
)
</script>
@@ -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',
@@ -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
}))}