mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 16:02:28 +00:00
feat: scope default instance db name to workspace (dt_/dl_) (#9699)
* feat: default instance db name to dt_/dl_ workspace scope Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: cap instance db name at 63 chars and add unit tests Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,8 +41,6 @@
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
let DEFAULT_DATATABLE_DB_NAME = 'datatable_db'
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -61,7 +59,7 @@
|
||||
import Row from '../table/Row.svelte'
|
||||
import TextInput from '../text_input/TextInput.svelte'
|
||||
import Tooltip from '../Tooltip.svelte'
|
||||
import { isCustomInstanceDbEnabled } from './utils.svelte'
|
||||
import { isCustomInstanceDbEnabled, getUnusedInstanceDbName } from './utils.svelte'
|
||||
import { random_adj } from '../random_positive_adjetive'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { SettingService, WorkspaceService, type GetSettingsResponse } from '$lib/gen'
|
||||
@@ -97,6 +95,18 @@
|
||||
tempSettings.dataTables.splice(index, 1)
|
||||
}
|
||||
|
||||
const customInstanceDbs = resource([() => $workspaceStore], SettingService.listCustomInstanceDbs)
|
||||
|
||||
function defaultInstanceDbName(): string {
|
||||
const usedNames = [
|
||||
...Object.keys(customInstanceDbs.current ?? {}),
|
||||
...tempSettings.dataTables
|
||||
.filter((d) => d.database.resource_type === 'instance' && d.database.resource_path)
|
||||
.map((d) => d.database.resource_path!)
|
||||
]
|
||||
return getUnusedInstanceDbName('dt', $workspaceStore ?? '', usedNames)
|
||||
}
|
||||
|
||||
function onNewDataTable() {
|
||||
const name = tempSettings.dataTables.some((d) => d.name === 'main')
|
||||
? `${random_adj()}_datatable`
|
||||
@@ -105,13 +115,11 @@
|
||||
name,
|
||||
database: {
|
||||
resource_type: $isCustomInstanceDbEnabled ? 'instance' : 'postgresql',
|
||||
resource_path: $isCustomInstanceDbEnabled ? DEFAULT_DATATABLE_DB_NAME : undefined
|
||||
resource_path: $isCustomInstanceDbEnabled ? defaultInstanceDbName() : undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const customInstanceDbs = resource([() => $workspaceStore], SettingService.listCustomInstanceDbs)
|
||||
|
||||
async function onSave() {
|
||||
try {
|
||||
if (
|
||||
@@ -228,7 +236,7 @@
|
||||
dataTable.database = {
|
||||
resource_type,
|
||||
resource_path:
|
||||
resource_type === 'instance' ? DEFAULT_DATATABLE_DB_NAME : undefined
|
||||
resource_type === 'instance' ? defaultInstanceDbName() : undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,13 +80,11 @@
|
||||
import Popover from '../meltComponents/Popover.svelte'
|
||||
import TextInput from '../text_input/TextInput.svelte'
|
||||
import { slide } from 'svelte/transition'
|
||||
import { isCustomInstanceDbEnabled } from './utils.svelte'
|
||||
import { isCustomInstanceDbEnabled, getUnusedInstanceDbName } from './utils.svelte'
|
||||
import { resource } from 'runed'
|
||||
import CustomInstanceDbSelect from './CustomInstanceDbSelect.svelte'
|
||||
import Label from '../Label.svelte'
|
||||
|
||||
const DEFAULT_DUCKLAKE_CATALOG_NAME = 'ducklake_catalog'
|
||||
|
||||
type Props = {
|
||||
ducklakeSettings: DucklakeSettingsType
|
||||
ducklakeSavedSettings: DucklakeSettingsType
|
||||
@@ -100,6 +98,16 @@
|
||||
onDiscard = undefined
|
||||
}: Props = $props()
|
||||
|
||||
function defaultInstanceDbName(): string {
|
||||
const usedNames = [
|
||||
...Object.keys(customInstanceDbs.current ?? {}),
|
||||
...ducklakeSettings.ducklakes
|
||||
.filter((d) => d.catalog.resource_type === 'instance' && d.catalog.resource_path)
|
||||
.map((d) => d.catalog.resource_path!)
|
||||
]
|
||||
return getUnusedInstanceDbName('dl', $workspaceStore ?? '', usedNames)
|
||||
}
|
||||
|
||||
function onNewDucklake() {
|
||||
const name = ducklakeSettings.ducklakes.some((d) => d.name === 'main')
|
||||
? `${random_adj()}_ducklake`
|
||||
@@ -108,7 +116,7 @@
|
||||
name,
|
||||
catalog: {
|
||||
resource_type: $isCustomInstanceDbEnabled ? 'instance' : 'postgresql',
|
||||
resource_path: $isCustomInstanceDbEnabled ? DEFAULT_DUCKLAKE_CATALOG_NAME : undefined
|
||||
resource_path: $isCustomInstanceDbEnabled ? defaultInstanceDbName() : undefined
|
||||
},
|
||||
storage: {
|
||||
storage: undefined,
|
||||
@@ -272,7 +280,7 @@
|
||||
ducklake.catalog = {
|
||||
resource_type,
|
||||
resource_path:
|
||||
resource_type === 'instance' ? DEFAULT_DUCKLAKE_CATALOG_NAME : undefined
|
||||
resource_type === 'instance' ? defaultInstanceDbName() : undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,3 +7,31 @@ export let isCustomInstanceDbEnabled = derived(
|
||||
[superadmin],
|
||||
([superadmin_]) => superadmin_ && !isCloudHosted()
|
||||
)
|
||||
|
||||
// Postgres caps identifiers at 63 bytes; the backend rejects longer db names.
|
||||
const MAX_INSTANCE_DB_NAME_LEN = 63
|
||||
|
||||
// Builds a default instance database name scoped to the workspace (e.g. `dt_myworkspace`),
|
||||
// appending `_1`, `_2`... until an unused name is found. Workspace ids may contain hyphens,
|
||||
// which are not valid in unquoted postgres identifiers, so they are replaced with underscores.
|
||||
// The result is truncated to keep it within the postgres identifier length limit.
|
||||
export function getUnusedInstanceDbName(
|
||||
prefix: string,
|
||||
workspaceId: string,
|
||||
usedNames: Iterable<string>
|
||||
): string {
|
||||
const used = new Set(usedNames)
|
||||
const base = `${prefix}_${workspaceId.toLowerCase().replace(/-/g, '_')}`.slice(
|
||||
0,
|
||||
MAX_INSTANCE_DB_NAME_LEN
|
||||
)
|
||||
if (!used.has(base)) return base
|
||||
let i = 1
|
||||
let candidate: string
|
||||
do {
|
||||
const suffix = `_${i}`
|
||||
candidate = base.slice(0, MAX_INSTANCE_DB_NAME_LEN - suffix.length) + suffix
|
||||
i++
|
||||
} while (used.has(candidate))
|
||||
return candidate
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { getUnusedInstanceDbName } from './utils.svelte'
|
||||
|
||||
describe('getUnusedInstanceDbName', () => {
|
||||
it('scopes the name to the workspace with the given prefix', () => {
|
||||
expect(getUnusedInstanceDbName('dt', 'myworkspace', [])).toBe('dt_myworkspace')
|
||||
expect(getUnusedInstanceDbName('dl', 'myworkspace', [])).toBe('dl_myworkspace')
|
||||
})
|
||||
|
||||
it('lowercases and replaces hyphens with underscores', () => {
|
||||
expect(getUnusedInstanceDbName('dt', 'My-Team', [])).toBe('dt_my_team')
|
||||
})
|
||||
|
||||
it('appends an incrementing suffix when the name is already used', () => {
|
||||
expect(getUnusedInstanceDbName('dt', 'abc', ['dt_abc'])).toBe('dt_abc_1')
|
||||
expect(getUnusedInstanceDbName('dt', 'abc', ['dt_abc', 'dt_abc_1'])).toBe('dt_abc_2')
|
||||
})
|
||||
|
||||
it('skips over already-used suffixed names', () => {
|
||||
expect(getUnusedInstanceDbName('dt', 'abc', ['dt_abc', 'dt_abc_2'])).toBe('dt_abc_1')
|
||||
expect(getUnusedInstanceDbName('dt', 'abc', ['dt_abc', 'dt_abc_1', 'dt_abc_2'])).toBe(
|
||||
'dt_abc_3'
|
||||
)
|
||||
})
|
||||
|
||||
it('accepts any iterable of used names', () => {
|
||||
expect(getUnusedInstanceDbName('dt', 'abc', new Set(['dt_abc']))).toBe('dt_abc_1')
|
||||
})
|
||||
|
||||
it('truncates the base name to the postgres 63-char identifier limit', () => {
|
||||
const longId = 'w'.repeat(100)
|
||||
const name = getUnusedInstanceDbName('dt', longId, [])
|
||||
expect(name.length).toBe(63)
|
||||
expect(name.startsWith('dt_')).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps the result within 63 chars even when appending a suffix', () => {
|
||||
const longId = 'w'.repeat(100)
|
||||
const base = getUnusedInstanceDbName('dt', longId, []) // length 63
|
||||
const name = getUnusedInstanceDbName('dt', longId, [base])
|
||||
expect(name.length).toBeLessThanOrEqual(63)
|
||||
expect(name.endsWith('_1')).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user