Compare commits

...

2 Commits

Author SHA1 Message Date
Diego Imbert
6bafef661c Merge branch 'main' into di/db-manager-backend-scripts 2026-03-09 14:00:11 +01:00
Diego Imbert
822723ae69 DB Manager backend scripts 2026-03-06 15:32:37 +01:00
10 changed files with 1295 additions and 53 deletions

View File

@@ -1877,6 +1877,15 @@ pub struct ExecuteApp {
pub run_query_params: Option<RunJobQuery>, pub run_query_params: Option<RunJobQuery>,
} }
fn maybe_replace_internal_db_script(mut raw_code: RawCode) -> RawCode {
if let Some(replaced) =
crate::db_studio_scripts::maybe_replace_internal_script(&raw_code.content)
{
raw_code.content = replaced;
}
raw_code
}
fn digest(code: &str) -> String { fn digest(code: &str) -> String {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(code); hasher.update(code);
@@ -2138,13 +2147,30 @@ async fn execute_component(
// flow or script: // flow or script:
(Some(path), None, None) => get_payload_tag_from_prefixed_path(&path, &db, &w_id).await?, (Some(path), None, None) => get_payload_tag_from_prefixed_path(&path, &db, &w_id).await?,
// inline script: in "preview" mode or without entry in the `app_script` table. // inline script: in "preview" mode or without entry in the `app_script` table.
(None, Some(raw_code), None) => (JobPayload::Code(raw_code), None, None), (None, Some(raw_code), None) => {
let raw_code = maybe_replace_internal_db_script(raw_code);
(JobPayload::Code(raw_code), None, None)
}
// inline script: in "run" mode and with an entry in the `app_script` table. // inline script: in "run" mode and with an entry in the `app_script` table.
(None, Some(RawCode { language, path, cache_ttl, .. }), Some(id)) => ( (None, Some(raw_code), Some(id)) => {
JobPayload::AppScript { id: AppScriptId(id), cache_ttl, language, path }, // Check if this is an internal DB script marker — if so, replace content and
None, // execute as Code (preview-style) since the content is server-controlled.
None, if raw_code
), .content
.trim_start()
.starts_with(crate::db_studio_scripts::WM_INTERNAL_PREFIX)
{
let raw_code = maybe_replace_internal_db_script(raw_code);
(JobPayload::Code(raw_code), None, None)
} else {
let RawCode { language, path, cache_ttl, .. } = raw_code;
(
JobPayload::AppScript { id: AppScriptId(id), cache_ttl, language, path },
None,
None,
)
}
}
_ => unreachable!(), _ => unreachable!(),
}; };
let tx = PushIsolationLevel::IsolatedRoot(db.clone()); let tx = PushIsolationLevel::IsolatedRoot(db.clone());

File diff suppressed because it is too large Load Diff

View File

@@ -4612,21 +4612,26 @@ async fn run_preview_script(
match preview.kind { match preview.kind {
Some(PreviewKind::Identity) => JobPayload::Identity, Some(PreviewKind::Identity) => JobPayload::Identity,
Some(PreviewKind::Noop) => JobPayload::Noop, Some(PreviewKind::Noop) => JobPayload::Noop,
_ => JobPayload::Code(RawCode { _ => {
hash: preview let content = preview.content.unwrap_or_default();
.script_hash let content = crate::db_studio_scripts::maybe_replace_internal_script(&content)
.as_ref() .unwrap_or(content);
.and_then(|s| windmill_common::scripts::to_i64(s).ok()), JobPayload::Code(RawCode {
content: preview.content.unwrap_or_default(), hash: preview
path: preview.path, .script_hash
language: preview.language.unwrap_or(ScriptLang::Deno), .as_ref()
lock: preview.lock, .and_then(|s| windmill_common::scripts::to_i64(s).ok()),
concurrency_settings: ConcurrencySettingsWithCustom::default(), // TODO(gbouv): once I find out how to store limits in the content of a script, should be easy to plug limits here content,
debouncing_settings: DebouncingSettings::default(), // TODO(pyra): same as for concurrency limits. path: preview.path,
cache_ttl: None, language: preview.language.unwrap_or(ScriptLang::Deno),
cache_ignore_s3_path: None, lock: preview.lock,
dedicated_worker: preview.dedicated_worker, concurrency_settings: ConcurrencySettingsWithCustom::default(), // TODO(gbouv): once I find out how to store limits in the content of a script, should be easy to plug limits here
}), debouncing_settings: DebouncingSettings::default(), // TODO(pyra): same as for concurrency limits.
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: preview.dedicated_worker,
})
}
}, },
push_args, push_args,
authed.display_username(), authed.display_username(),

View File

@@ -76,6 +76,7 @@ mod bedrock;
mod capture; mod capture;
mod concurrency_groups; mod concurrency_groups;
mod db; mod db;
mod db_studio_scripts;
mod drafts; mod drafts;
#[cfg(feature = "private")] #[cfg(feature = "private")]

View File

@@ -1,5 +1,4 @@
import type { AppInput, RunnableByName } from '$lib/components/apps/inputType' import type { AppInput, RunnableByName } from '$lib/components/apps/inputType'
import { wrapDucklakeQuery } from '../../../../../ducklake'
import type { DbType, DbInput } from '$lib/components/dbTypes' import type { DbType, DbInput } from '$lib/components/dbTypes'
import { buildParameters } from '../utils' import { buildParameters } from '../utils'
import { getLanguageByResourceType, type ColumnDef, buildVisibleFieldList } from '../utils' import { getLanguageByResourceType, type ColumnDef, buildVisibleFieldList } from '../utils'
@@ -142,6 +141,28 @@ export function makeCountQuery(
return query return query
} }
export function buildCountMarker(
table: string,
columnDefs: ColumnDef[],
whereClause: string | undefined,
dbType: DbType,
ducklake?: string
): string {
const params: Record<string, unknown> = {
table,
column_defs: columnDefs.map((c) => ({
field: c.field,
datatype: c.datatype,
isprimarykey: c.isprimarykey,
ignored: c.ignored ?? false
})),
where_clause: whereClause ?? null,
db_type: dbType
}
if (ducklake) params.ducklake = ducklake
return `-- WM_INTERNAL_DB_COUNT_SCRIPT\n${JSON.stringify(params)}`
}
export function getCountInput( export function getCountInput(
dbInput: DbInput, dbInput: DbInput,
table: string, table: string,
@@ -157,8 +178,8 @@ export function getCountInput(
return undefined return undefined
} }
const dbType = dbInput.type === 'ducklake' ? 'duckdb' : dbInput.resourceType const dbType = dbInput.type === 'ducklake' ? 'duckdb' : dbInput.resourceType
let query = makeCountQuery(dbType, table, whereClause, columnDefs) const ducklake = dbInput.type === 'ducklake' ? dbInput.ducklake : undefined
if (dbInput.type === 'ducklake') query = wrapDucklakeQuery(query, dbInput.ducklake) const query = buildCountMarker(table, columnDefs, whereClause, dbType, ducklake)
const updateRunnable: RunnableByName = { const updateRunnable: RunnableByName = {
name: 'AppDbExplorer', name: 'AppDbExplorer',

View File

@@ -1,6 +1,5 @@
import type { AppInput, RunnableByName } from '$lib/components/apps/inputType' import type { AppInput, RunnableByName } from '$lib/components/apps/inputType'
import type { DbType, DbInput } from '$lib/components/dbTypes' import type { DbType, DbInput } from '$lib/components/dbTypes'
import { wrapDucklakeQuery } from '../../../../../ducklake'
import { getLanguageByResourceType, type ColumnDef, buildParameters } from '../utils' import { getLanguageByResourceType, type ColumnDef, buildParameters } from '../utils'
export function makeDeleteQuery(table: string, columns: ColumnDef[], dbType: DbType) { export function makeDeleteQuery(table: string, columns: ColumnDef[], dbType: DbType) {
@@ -66,6 +65,21 @@ export function makeDeleteQuery(table: string, columns: ColumnDef[], dbType: DbT
} }
} }
export function buildDeleteMarker(
table: string,
columns: ColumnDef[],
dbType: DbType,
ducklake?: string
): string {
const params: Record<string, unknown> = {
table,
columns: columns.map((c) => ({ field: c.field, datatype: c.datatype })),
db_type: dbType
}
if (ducklake) params.ducklake = ducklake
return `-- WM_INTERNAL_DB_DELETE_SCRIPT\n${JSON.stringify(params)}`
}
export function getDeleteInput( export function getDeleteInput(
dbInput: DbInput, dbInput: DbInput,
table: string, table: string,
@@ -79,8 +93,8 @@ export function getDeleteInput(
return undefined return undefined
} }
const dbType = dbInput.type === 'ducklake' ? 'duckdb' : dbInput.resourceType const dbType = dbInput.type === 'ducklake' ? 'duckdb' : dbInput.resourceType
let query = makeDeleteQuery(table, columns, dbType) const ducklake = dbInput.type === 'ducklake' ? dbInput.ducklake : undefined
if (dbInput.type === 'ducklake') query = wrapDucklakeQuery(query, dbInput.ducklake) const query = buildDeleteMarker(table, columns, dbType, ducklake)
const deleteRunnable: RunnableByName = { const deleteRunnable: RunnableByName = {
name: 'AppDbExplorer', name: 'AppDbExplorer',
type: 'inline', type: 'inline',

View File

@@ -1,5 +1,4 @@
import type { AppInput } from '$lib/components/apps/inputType' import type { AppInput } from '$lib/components/apps/inputType'
import { wrapDucklakeQuery } from '../../../../../ducklake'
import type { DbType, DbInput } from '$lib/components/dbTypes' import type { DbType, DbInput } from '$lib/components/dbTypes'
import { buildParameters, ColumnIdentity } from '../utils' import { buildParameters, ColumnIdentity } from '../utils'
import { getLanguageByResourceType, type ColumnDef } from '../utils' import { getLanguageByResourceType, type ColumnDef } from '../utils'
@@ -106,10 +105,37 @@ export function makeInsertQuery(table: string, columns: ColumnDef[], dbType: DbT
return query return query
} }
export function buildInsertMarker(
table: string,
columns: ColumnDef[],
dbType: DbType,
ducklake?: string
): string {
const params: Record<string, unknown> = {
table,
columns: columns.map((c) => ({
field: c.field,
datatype: c.datatype,
isprimarykey: c.isprimarykey,
ignored: c.ignored ?? false,
isnullable: c.isnullable ?? 'YES',
isidentity: c.isidentity ?? 'No',
defaultvalue: c.defaultvalue ?? null,
hideInsert: c.hideInsert ?? false,
overrideDefaultValue: c.overrideDefaultValue ?? false,
defaultUserValue: c.defaultUserValue ?? null,
defaultValueNull: c.defaultValueNull ?? false
})),
db_type: dbType
}
if (ducklake) params.ducklake = ducklake
return `-- WM_INTERNAL_DB_INSERT_SCRIPT\n${JSON.stringify(params)}`
}
export function getInsertInput(dbInput: DbInput, table: string, columns: ColumnDef[]): AppInput { export function getInsertInput(dbInput: DbInput, table: string, columns: ColumnDef[]): AppInput {
const dbType = dbInput.type === 'ducklake' ? 'duckdb' : dbInput.resourceType const dbType = dbInput.type === 'ducklake' ? 'duckdb' : dbInput.resourceType
let query = makeInsertQuery(table, columns, dbType) const ducklake = dbInput.type === 'ducklake' ? dbInput.ducklake : undefined
if (dbInput.type === 'ducklake') query = wrapDucklakeQuery(query, dbInput.ducklake) const query = buildInsertMarker(table, columns, dbType, ducklake)
return { return {
runnable: { runnable: {
name: 'AppDbExplorer', name: 'AppDbExplorer',

View File

@@ -1,5 +1,4 @@
import type { AppInput, RunnableByName } from '$lib/components/apps/inputType' import type { AppInput, RunnableByName } from '$lib/components/apps/inputType'
import { wrapDucklakeQuery } from '../../../../../ducklake'
import type { DbType, DbInput } from '$lib/components/dbTypes' import type { DbType, DbInput } from '$lib/components/dbTypes'
import { buildParameters } from '../utils' import { buildParameters } from '../utils'
import { getLanguageByResourceType, type ColumnDef, buildVisibleFieldList } from '../utils' import { getLanguageByResourceType, type ColumnDef, buildVisibleFieldList } from '../utils'
@@ -318,6 +317,33 @@ function coerceToNumber(value: any): number {
return 0 return 0
} }
export function buildSelectMarker(
table: string,
columnDefs: ColumnDef[],
whereClause: string | undefined,
dbType: DbType,
ducklake?: string
): string {
const params: Record<string, unknown> = {
table,
column_defs: columnDefs.map((c) => ({
field: c.field,
datatype: c.datatype,
isprimarykey: c.isprimarykey,
ignored: c.ignored ?? false,
editable: c.editable ?? false,
isnullable: c.isnullable ?? 'YES',
isidentity: c.isidentity ?? 'No',
defaultvalue: c.defaultvalue ?? null,
hideInsert: c.hideInsert ?? false
})),
where_clause: whereClause ?? null,
db_type: dbType
}
if (ducklake) params.ducklake = ducklake
return `-- WM_INTERNAL_DB_SELECT_SCRIPT\n${JSON.stringify(params)}`
}
export function getSelectInput( export function getSelectInput(
dbInput: DbInput, dbInput: DbInput,
table: string | undefined, table: string | undefined,
@@ -335,8 +361,8 @@ export function getSelectInput(
} }
const dbType = dbInput.type === 'ducklake' ? 'duckdb' : dbInput.resourceType const dbType = dbInput.type === 'ducklake' ? 'duckdb' : dbInput.resourceType
let content = makeSelectQuery(table, columnDefs, whereClause, dbType, options) const ducklake = dbInput.type === 'ducklake' ? dbInput.ducklake : undefined
if (dbInput.type === 'ducklake') content = wrapDucklakeQuery(content, dbInput.ducklake) const content = buildSelectMarker(table, columnDefs, whereClause, dbType, ducklake)
const getRunnable: RunnableByName = { const getRunnable: RunnableByName = {
name: 'AppDbExplorer', name: 'AppDbExplorer',
type: 'inline', type: 'inline',

View File

@@ -1,5 +1,4 @@
import type { AppInput, RunnableByName } from '$lib/components/apps/inputType' import type { AppInput, RunnableByName } from '$lib/components/apps/inputType'
import { wrapDucklakeQuery } from '../../../../../ducklake'
import type { DbInput, DbType } from '$lib/components/dbTypes' import type { DbInput, DbType } from '$lib/components/dbTypes'
import { getLanguageByResourceType, type ColumnDef, buildParameters } from '../utils' import { getLanguageByResourceType, type ColumnDef, buildParameters } from '../utils'
@@ -76,6 +75,23 @@ export function makeUpdateQuery(
} }
} }
export function buildUpdateMarker(
table: string,
column: { datatype: string; field: string },
columns: { datatype: string; field: string }[],
dbType: DbType,
ducklake?: string
): string {
const params: Record<string, unknown> = {
table,
column: { field: column.field, datatype: column.datatype },
columns: columns.map((c) => ({ field: c.field, datatype: c.datatype })),
db_type: dbType
}
if (ducklake) params.ducklake = ducklake
return `-- WM_INTERNAL_DB_UPDATE_SCRIPT\n${JSON.stringify(params)}`
}
export function getUpdateInput( export function getUpdateInput(
dbInput: DbInput, dbInput: DbInput,
table: string, table: string,
@@ -90,8 +106,8 @@ export function getUpdateInput(
return undefined return undefined
} }
const dbType = dbInput.type === 'ducklake' ? 'duckdb' : dbInput.resourceType const dbType = dbInput.type === 'ducklake' ? 'duckdb' : dbInput.resourceType
let query = makeUpdateQuery(table, column, columns, dbType) const ducklake = dbInput.type === 'ducklake' ? dbInput.ducklake : undefined
if (dbInput.type === 'ducklake') query = wrapDucklakeQuery(query, dbInput.ducklake) const query = buildUpdateMarker(table, column, columns, dbType, ducklake)
const updateRunnable: RunnableByName = { const updateRunnable: RunnableByName = {
name: 'AppDbExplorer', name: 'AppDbExplorer',

View File

@@ -3,12 +3,12 @@ import {
type ColumnDef, type ColumnDef,
type TableMetadata type TableMetadata
} from './apps/components/display/dbtable/utils' } from './apps/components/display/dbtable/utils'
import { makeSelectQuery } from './apps/components/display/dbtable/queries/select' import { buildSelectMarker } from './apps/components/display/dbtable/queries/select'
import { runScriptAndPollResult } from './jobs/utils' import { runScriptAndPollResult } from './jobs/utils'
import { makeCountQuery } from './apps/components/display/dbtable/queries/count' import { buildCountMarker } from './apps/components/display/dbtable/queries/count'
import { makeUpdateQuery } from './apps/components/display/dbtable/queries/update' import { buildUpdateMarker } from './apps/components/display/dbtable/queries/update'
import { makeDeleteQuery } from './apps/components/display/dbtable/queries/delete' import { buildDeleteMarker } from './apps/components/display/dbtable/queries/delete'
import { makeInsertQuery } from './apps/components/display/dbtable/queries/insert' import { buildInsertMarker } from './apps/components/display/dbtable/queries/insert'
import { makeDeleteTableQuery } from './apps/components/display/dbtable/queries/deleteTable' import { makeDeleteTableQuery } from './apps/components/display/dbtable/queries/deleteTable'
import type { DBSchema, SQLSchema } from '$lib/stores' import type { DBSchema, SQLSchema } from '$lib/stores'
import { stringifySchema } from './copilot/lib' import { stringifySchema } from './copilot/lib'
@@ -68,8 +68,8 @@ export function dbTableOpsWithPreviewScripts({
tableKey, tableKey,
colDefs, colDefs,
getCount: async ({ quicksearch }) => { getCount: async ({ quicksearch }) => {
let countQuery = makeCountQuery(dbType, tableKey, undefined, colDefs) const ducklake = input.type === 'ducklake' ? input.ducklake : undefined
if (input.type === 'ducklake') countQuery = wrapDucklakeQuery(countQuery, input.ducklake) const countQuery = buildCountMarker(tableKey, colDefs, undefined, dbType, ducklake)
const result = await runScriptAndPollResult({ const result = await runScriptAndPollResult({
workspace, workspace,
requestBody: { args: { ...dbArg, quicksearch }, language, content: countQuery } requestBody: { args: { ...dbArg, quicksearch }, language, content: countQuery }
@@ -78,10 +78,8 @@ export function dbTableOpsWithPreviewScripts({
return count return count
}, },
getRows: async (params) => { getRows: async (params) => {
let query = makeSelectQuery(tableKey, colDefs, undefined, dbType, undefined, { const ducklake = input.type === 'ducklake' ? input.ducklake : undefined
fixPgIntTypes: true const query = buildSelectMarker(tableKey, colDefs, undefined, dbType, ducklake)
})
if (input.type === 'ducklake') query = wrapDucklakeQuery(query, input.ducklake)
let items = (await runScriptAndPollResult({ let items = (await runScriptAndPollResult({
workspace, workspace,
requestBody: { args: { ...dbArg, ...params }, language, content: query } requestBody: { args: { ...dbArg, ...params }, language, content: query }
@@ -92,8 +90,8 @@ export function dbTableOpsWithPreviewScripts({
return items return items
}, },
onUpdate: async ({ values }, colDef, newValue) => { onUpdate: async ({ values }, colDef, newValue) => {
let updateQuery = makeUpdateQuery(tableKey, colDef, colDefs, dbType) const ducklake = input.type === 'ducklake' ? input.ducklake : undefined
if (input.type === 'ducklake') updateQuery = wrapDucklakeQuery(updateQuery, input.ducklake) const updateQuery = buildUpdateMarker(tableKey, colDef, colDefs, dbType, ducklake)
await runScriptAndPollResult({ await runScriptAndPollResult({
workspace, workspace,
requestBody: { requestBody: {
@@ -104,16 +102,16 @@ export function dbTableOpsWithPreviewScripts({
}) })
}, },
onDelete: async ({ values }) => { onDelete: async ({ values }) => {
let deleteQuery = makeDeleteQuery(tableKey, colDefs, dbType) const ducklake = input.type === 'ducklake' ? input.ducklake : undefined
if (input.type === 'ducklake') deleteQuery = wrapDucklakeQuery(deleteQuery, input.ducklake) const deleteQuery = buildDeleteMarker(tableKey, colDefs, dbType, ducklake)
await runScriptAndPollResult({ await runScriptAndPollResult({
workspace, workspace,
requestBody: { args: { ...dbArg, ...values }, language, content: deleteQuery } requestBody: { args: { ...dbArg, ...values }, language, content: deleteQuery }
}) })
}, },
onInsert: async ({ values }) => { onInsert: async ({ values }) => {
let insertQuery = makeInsertQuery(tableKey, colDefs, dbType) const ducklake = input.type === 'ducklake' ? input.ducklake : undefined
if (input.type === 'ducklake') insertQuery = wrapDucklakeQuery(insertQuery, input.ducklake) const insertQuery = buildInsertMarker(tableKey, colDefs, dbType, ducklake)
await runScriptAndPollResult({ await runScriptAndPollResult({
workspace, workspace,
requestBody: { args: { ...dbArg, ...values }, language, content: insertQuery } requestBody: { args: { ...dbArg, ...values }, language, content: insertQuery }