fix(datatables): provision protected instance dbs at save, guard stale deprovision, require pg16

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013fBWmrMs2DbpU1iYdCvGX6
This commit is contained in:
Diego Imbert
2026-07-31 14:26:30 +02:00
parent 30b6f05fd6
commit bfa6b02e3f
58 changed files with 1488 additions and 61 deletions
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_diff_full_scan SET source_workspace_id = $1 WHERE source_workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "02bdda30f56376073333e45fdf67a874a0cc1a26ca295b0e9b4a6f26e2e83db7"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET error_handler_fallback_to_instance_alerts = false WHERE workspace_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "0c5558686b4e5a9bacc73c7b6709ad4e3e6e69582605eeb84a93c29d629b94be"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT parent_workspace_id = $2 FROM workspace WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "0d7bd92d4a8bc6261978dcf416fd583b1879d4eaade35a1e2e3a02b76a8a3c04"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_diff_full_scan (source_workspace_id, fork_workspace_id)\n VALUES ($1, $2)\n ON CONFLICT (source_workspace_id, fork_workspace_id) DO UPDATE SET scanned_at = now()\n RETURNING scanned_at",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "scanned_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Varchar",
"Varchar"
]
},
"nullable": [
false
]
},
"hash": "104873cf0863b063ebd8e4e8f54ec32e3aea2c5bd09fe972a385c1954b4657a8"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO datatable_migrations (workspace_id, datatable, timestamp, name, code_up)\n VALUES\n ('test-workspace', 'dt', 20260101000001, 'renamed_here', 'ALTER TABLE t ADD COLUMN a int'),\n ('other-workspace', 'dt', 20260101000001, 'named_there', 'ALTER TABLE t ADD COLUMN a int')",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "18a813c420dac379962cc0fc5777f7be4baa9ac83fd01ac054761976e82d9794"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET\n ai_config = source_ws.ai_config,\n large_file_storage = source_ws.large_file_storage,\n ducklake = source_ws.ducklake,\n datatable = source_ws.datatable,\n git_app_installations = source_ws.git_app_installations\n FROM workspace_settings source_ws\n WHERE source_ws.workspace_id = $1\n AND workspace_settings.workspace_id = $2\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "2213dc4b594d27b3788c48b19e13c600224681e11c016eea5cee3086a7bea8d0"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_settings SELECT $1, slack_team_id, slack_name, slack_command_script, slack_email, customer_id, plan, webhook, deploy_to, ai_config, large_file_storage, git_sync, default_app, default_scripts, deploy_ui, mute_critical_alerts, color, operator_settings, teams_command_script, teams_team_id, teams_team_name, git_app_installations, ducklake, slack_oauth_client_id, slack_oauth_client_secret, datatable, teams_team_guid, auto_invite, error_handler, success_handler, public_app_execution_limit_per_minute, error_handler_fallback_to_instance_alerts FROM workspace_settings WHERE workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "2dc5a74c4e614b43148925cdbac234f6a2f295a9c46897f5e9aab8f7298ff0d7"
}
@@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "SELECT DISTINCT flow_step_id as \"flow_step_id!\" FROM v2_job\n WHERE parent_job = $1 AND workspace_id = $2 AND flow_step_id IS NOT NULL\n AND ($3::text[] IS NULL OR tag = ANY($3))\n ORDER BY flow_step_id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "flow_step_id!",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid",
"Text",
"TextArray"
]
},
"nullable": [
true
]
},
"hash": "2ef832a6d8e9feb83de6706b0ffe8e849e56f8db795df2ad4d907cdb3c9fce60"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT path FROM resource WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "2f2d26375589ac3df48f476e8200791351c334a13a2ba180ca1f120454c63111"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO usr (workspace_id, email, username, is_admin, role)\n VALUES ('other-workspace', 'test@windmill.dev', 'test-user', true, 'Admin')",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "385f671934471f725c964b45314c737d3000f148512ae0e8f63a28bef5d7e998"
}
@@ -0,0 +1,59 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email, username, is_admin, is_operator, groups, folders, end_user_email FROM job_perms WHERE job_id = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "username",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "is_admin",
"type_info": "Bool"
},
{
"ordinal": 3,
"name": "is_operator",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "groups",
"type_info": "TextArray"
},
{
"ordinal": 5,
"name": "folders",
"type_info": "JsonbArray"
},
{
"ordinal": 6,
"name": "end_user_email",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
true
]
},
"hash": "3e869b95799300dff973a3c858f823a2872e33a360ff5a02169e4e80ce3d3b7b"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM workspace_diff_full_scan WHERE source_workspace_id = $1 OR fork_workspace_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "40c76629ba533ad9c523cb9dae2ee8a2a78a250884acff600f3cf0eb9bbaf058"
}
@@ -0,0 +1,37 @@
{
"db_name": "PostgreSQL",
"query": "SELECT j.id,\n COALESCE(c.status::text,\n CASE WHEN q.running AND q.suspend > 0 THEN 'suspended'\n WHEN q.running THEN 'running'\n ELSE 'queued' END) as \"status!\",\n COALESCE((\n SELECT m->'value'->>'type'\n FROM v2_job parent_j\n LEFT JOIN flow_version fv ON fv.id = parent_j.runnable_id\n AND parent_j.kind::text = 'flow'\n LEFT JOIN flow f ON f.path = parent_j.runnable_path\n AND f.workspace_id = parent_j.workspace_id\n LEFT JOIN flow_node fn ON fn.id = parent_j.runnable_id\n CROSS JOIN LATERAL jsonb_array_elements(\n COALESCE(parent_j.raw_flow, fv.value, f.value, fn.flow)->'modules'\n ) m\n WHERE parent_j.id = $1\n AND m->>'id' = $3\n LIMIT 1\n ), '')::text as \"parent_module_type!\"\n FROM v2_job j\n LEFT JOIN v2_job_completed c ON c.id = j.id\n LEFT JOIN v2_job_queue q ON q.id = j.id\n LEFT JOIN (\n SELECT fj.jid, fj.ord\n FROM (SELECT COALESCE(\n (SELECT flow_status FROM v2_job_completed WHERE id = $1),\n (SELECT flow_status FROM v2_job_status WHERE id = $1)\n ) AS fs) pf\n CROSS JOIN LATERAL (\n SELECT m FROM jsonb_array_elements(pf.fs->'modules') m\n WHERE m->>'id' = $3\n LIMIT 1\n ) md\n CROSS JOIN LATERAL jsonb_array_elements_text(md.m->'flow_jobs')\n WITH ORDINALITY fj(jid, ord)\n ) pos ON pos.jid = j.id::text\n WHERE j.parent_job = $1 AND j.workspace_id = $2 AND j.flow_step_id = $3\n AND ($4::text[] IS NULL OR j.tag = ANY($4))\n ORDER BY pos.ord NULLS LAST, j.id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "status!",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "parent_module_type!",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid",
"Text",
"Text",
"TextArray"
]
},
"nullable": [
false,
null,
null
]
},
"hash": "48c2eeca50aedc1895256f615aecc8b68627c8ed9a6da969674734854ef2742a"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO script (workspace_id, path, hash, content, summary, description, language, created_by, created_at, archived, schema_validation, ws_error_handler_muted, deleted)\n VALUES\n ('test-workspace', 'f/shared/same', 1, 'def main(): pass', 'Same', '', 'python3', 'test@windmill.dev', NOW(), false, false, false, false),\n ('test-workspace', 'f/shared/differs', 2, 'def main(): return 1', 'Differs', '', 'python3', 'test@windmill.dev', NOW(), false, false, false, false),\n ('test-workspace', 'f/shared/only_here', 3, 'def main(): return 2', 'Only here', '', 'python3', 'test@windmill.dev', NOW(), false, false, false, false),\n ('other-workspace', 'f/shared/same', 4, 'def main(): pass', 'Same', '', 'python3', 'test@windmill.dev', NOW(), false, false, false, false),\n ('other-workspace', 'f/shared/differs', 5, 'def main(): return 99', 'Differs', '', 'python3', 'test@windmill.dev', NOW(), false, false, false, false)",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "4a0b3f5e2daa723f16fc005afc2c5cb901a9d13e874e35ba399ac3a1d31aa470"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT pg_advisory_xact_lock(hashtext('workspace_diff_pair:' || $1 || '/' || $2))",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "pg_advisory_xact_lock",
"type_info": "Void"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "5d1e758bb2f907ceb85fbd8b9991fafabb0ab8a2155062a20f1a229567d1bddf"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM workspace WHERE id = $1 AND deleted = false)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "5e27017f770c9315205e310902441c01f6b65c123c7b877902a1129fda0b6fe5"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO app (workspace_id, path, summary, policy, versions)\n VALUES ($1, 'f/shared/converted', 'Converted', '{}'::jsonb, ARRAY[]::bigint[])\n RETURNING id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Varchar"
]
},
"nullable": [
false
]
},
"hash": "63bf7697facb7a989a7cddac67ae48716ad4269aeaabbd9d3d938d22c268f374"
}
@@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO app_version (app_id, value, created_by, created_at, raw_app)\n VALUES ($1, $2, 'test@windmill.dev', NOW(), $3) RETURNING id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8",
"Json",
"Bool"
]
},
"nullable": [
false
]
},
"hash": "6658faa1fcf54885ee6313d39cf3ee4a13c4b30daefc0c67b63d263d77f0e9b7"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT EXISTS(\n SELECT 1\n FROM workspace_settings ws\n CROSS JOIN LATERAL jsonb_each(\n CASE WHEN jsonb_typeof(ws.ducklake->'ducklakes') = 'object'\n THEN ws.ducklake->'ducklakes'\n ELSE '{}'::jsonb END\n ) AS dl(key, value)\n WHERE value->'catalog'->>'resource_type' = 'instance'\n AND value->'catalog'->>'resource_path' = $1\n ) AS \"e!\"\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "e!",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "673f2a8dccad6bfc4439adaf20013054aac4d94d60827e36cb81a484d2daadd7"
}
@@ -0,0 +1,66 @@
{
"db_name": "PostgreSQL",
"query": "SELECT j.kind::text as kind,\n c.status::text as completed_status,\n c.duration_ms as \"duration_ms?\",\n COALESCE(c.started_at, q.started_at) as started_at,\n LEFT(c.result::text, $3) as result_prefix,\n length(c.result::text) as result_length,\n q.running as \"q_running?\",\n q.suspend as \"q_suspend?\"\n FROM v2_job j\n LEFT JOIN v2_job_completed c ON c.id = j.id\n LEFT JOIN v2_job_queue q ON q.id = j.id\n WHERE j.id = $1 AND j.workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "kind",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "completed_status",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "duration_ms?",
"type_info": "Int8"
},
{
"ordinal": 3,
"name": "started_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "result_prefix",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "result_length",
"type_info": "Int4"
},
{
"ordinal": 6,
"name": "q_running?",
"type_info": "Bool"
},
{
"ordinal": 7,
"name": "q_suspend?",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Uuid",
"Text",
"Int4"
]
},
"nullable": [
null,
null,
false,
null,
null,
null,
false,
false
]
},
"hash": "6981553b904f9ae38d6eaeae05716ac71390028dfe8c54798b1701b01591fff7"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT ahead FROM workspace_diff\n WHERE source_workspace_id = 'test-workspace'\n AND fork_workspace_id = 'wm-fork-no-deploy-to'\n AND kind = 'script'\n AND path = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "ahead",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "6ade445fab717d2dadbebfb5747db8b49c98709cfc99a6ee25c29a230bf9ec46"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET git_sync = jsonb_set(\n git_sync,\n '{repositories}',\n (SELECT jsonb_agg(\n CASE WHEN elem->>'git_repo_resource_path' = $2\n AND jsonb_typeof(elem->'auto_pull') = 'object'\n THEN jsonb_set(elem, '{auto_pull}',\n ((elem->'auto_pull') - 'webhook_id' - 'webhook_secret' - 'webhook_url' - 'webhook_error') || $3)\n ELSE elem END)\n FROM jsonb_array_elements(git_sync->'repositories') AS elem)\n )\n WHERE workspace_id = $1\n AND jsonb_typeof(git_sync->'repositories') = 'array'\n AND EXISTS (\n SELECT 1 FROM jsonb_array_elements(git_sync->'repositories') AS e\n WHERE e->>'git_repo_resource_path' = $2\n AND jsonb_typeof(e->'auto_pull') = 'object'\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Jsonb"
]
},
"nullable": []
},
"hash": "6ddf798848b75affad1333b76c95b300c08ef40ef59a1cc159840cac51cc4789"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT 1 FROM datatable_ephemeral_role WHERE role_name = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "754d417acf38a659897488b9f0583a77398a3932ed1c5e29fa8284b76e383084"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE datatable_migrations SET name = 'renamed_again'\n WHERE workspace_id = 'test-workspace' AND datatable = 'dt'",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "767d4e0097a79b864d413b28153c1bcfd03be03bbb9ebd141513e57869f445d3"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM workspace_diff WHERE source_workspace_id = $1 AND fork_workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "7776414a7f0d2f0677b8c81e6b2ef1a20b025696c9b9ab807f8957cde9a21a05"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM workspace_diff WHERE (source_workspace_id = $1 AND fork_workspace_id = $2)\n OR (source_workspace_id = $2 AND fork_workspace_id = $1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "780cc55995eb5d5f88955381cdd71f46addafdabfe7dde0953d5c58a18a9274a"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_settings SELECT $1, slack_team_id, slack_name, slack_command_script, slack_email, customer_id, plan, webhook, deploy_to, ai_config, large_file_storage, git_sync, default_app, default_scripts, deploy_ui, mute_critical_alerts, color, operator_settings, teams_command_script, teams_team_id, teams_team_name, git_app_installations, ducklake, slack_oauth_client_id, slack_oauth_client_secret, datatable, teams_team_guid, auto_invite, error_handler, success_handler FROM workspace_settings WHERE workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "7c45f8d05a10ccf538c1b63aa1337e6d0491a2e8d04fe87eddb5a573de00d125"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_settings (workspace_id, slack_team_id, slack_name, slack_command_script, slack_email, customer_id, plan, webhook, ai_config, large_file_storage, git_sync, default_app, default_scripts, deploy_ui, mute_critical_alerts, color, operator_settings, teams_command_script, teams_team_id, teams_team_name, git_app_installations, ducklake, slack_oauth_client_id, slack_oauth_client_secret, datatable, teams_team_guid, auto_invite, error_handler, success_handler, public_app_execution_limit_per_minute, error_handler_fallback_to_instance_alerts) SELECT $1, slack_team_id, slack_name, slack_command_script, slack_email, customer_id, plan, webhook, ai_config, large_file_storage, git_sync, default_app, default_scripts, deploy_ui, mute_critical_alerts, color, operator_settings, teams_command_script, teams_team_id, teams_team_name, git_app_installations, ducklake, slack_oauth_client_id, slack_oauth_client_secret, datatable, teams_team_guid, auto_invite, error_handler, success_handler, public_app_execution_limit_per_minute, error_handler_fallback_to_instance_alerts FROM workspace_settings WHERE workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "8058042d5564a934534517e9c0446c5fcc0c3654746c5e65994123ffb4d05f78"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE app SET versions = ARRAY[$2::bigint] WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8"
]
},
"nullable": []
},
"hash": "81f4c030c77b423ff112012ad542e91975745846cade8b1a0b26882e4a3f3cc9"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n workspace_id,\n slack_team_id,\n teams_team_id,\n teams_team_name,\n teams_team_guid,\n slack_name,\n slack_command_script,\n teams_command_script,\n slack_email,\n slack_oauth_client_id,\n slack_oauth_client_secret,\n customer_id,\n plan,\n webhook,\n deploy_to,\n ai_config,\n large_file_storage,\n datatable,\n ducklake,\n git_sync,\n deploy_ui,\n default_app,\n default_scripts,\n mute_critical_alerts,\n color,\n operator_settings,\n git_app_installations,\n auto_invite,\n error_handler,\n success_handler,\n public_app_execution_limit_per_minute\n FROM\n workspace_settings\n WHERE\n workspace_id = $1\n ",
"query": "\n SELECT\n workspace_id,\n slack_team_id,\n teams_team_id,\n teams_team_name,\n teams_team_guid,\n slack_name,\n slack_command_script,\n teams_command_script,\n slack_email,\n slack_oauth_client_id,\n slack_oauth_client_secret,\n customer_id,\n plan,\n webhook,\n ai_config,\n large_file_storage,\n datatable,\n ducklake,\n git_sync,\n deploy_ui,\n default_app,\n default_scripts,\n mute_critical_alerts,\n color,\n operator_settings,\n git_app_installations,\n auto_invite,\n error_handler,\n success_handler,\n public_app_execution_limit_per_minute,\n error_handler_fallback_to_instance_alerts\n FROM\n workspace_settings\n WHERE\n workspace_id = $1\n ",
"describe": {
"columns": [
{
@@ -75,88 +75,88 @@
},
{
"ordinal": 14,
"name": "deploy_to",
"type_info": "Varchar"
},
{
"ordinal": 15,
"name": "ai_config",
"type_info": "Jsonb"
},
{
"ordinal": 16,
"ordinal": 15,
"name": "large_file_storage",
"type_info": "Jsonb"
},
{
"ordinal": 17,
"ordinal": 16,
"name": "datatable",
"type_info": "Jsonb"
},
{
"ordinal": 18,
"ordinal": 17,
"name": "ducklake",
"type_info": "Jsonb"
},
{
"ordinal": 19,
"ordinal": 18,
"name": "git_sync",
"type_info": "Jsonb"
},
{
"ordinal": 20,
"ordinal": 19,
"name": "deploy_ui",
"type_info": "Jsonb"
},
{
"ordinal": 21,
"ordinal": 20,
"name": "default_app",
"type_info": "Varchar"
},
{
"ordinal": 22,
"ordinal": 21,
"name": "default_scripts",
"type_info": "Jsonb"
},
{
"ordinal": 23,
"ordinal": 22,
"name": "mute_critical_alerts",
"type_info": "Bool"
},
{
"ordinal": 24,
"ordinal": 23,
"name": "color",
"type_info": "Varchar"
},
{
"ordinal": 25,
"ordinal": 24,
"name": "operator_settings",
"type_info": "Jsonb"
},
{
"ordinal": 26,
"ordinal": 25,
"name": "git_app_installations",
"type_info": "Jsonb"
},
{
"ordinal": 27,
"ordinal": 26,
"name": "auto_invite",
"type_info": "Jsonb"
},
{
"ordinal": 28,
"ordinal": 27,
"name": "error_handler",
"type_info": "Jsonb"
},
{
"ordinal": 29,
"ordinal": 28,
"name": "success_handler",
"type_info": "Jsonb"
},
{
"ordinal": 30,
"ordinal": 29,
"name": "public_app_execution_limit_per_minute",
"type_info": "Int4"
},
{
"ordinal": 30,
"name": "error_handler_fallback_to_instance_alerts",
"type_info": "Bool"
}
],
"parameters": {
@@ -190,13 +190,13 @@
true,
true,
true,
true,
false,
true,
true,
true,
true
true,
false
]
},
"hash": "a479cd371fb5d1f52e7c727730cf48ab229e63b8dfe377975d48dcd223251e7c"
"hash": "88a6a86285fcfaa778e9716d5072f555dff175feb704c3d825d6100defe91bce"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO usr (workspace_id, email, username, is_admin, role)\n VALUES ('other-workspace', 'test2@windmill.dev', 'test-user-2', true, 'Admin')",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "89e284ee0e2a95a12acb8f97373cb6ff95fc498935281928104e71ada9370140"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(\n SELECT 1 FROM workspace\n WHERE (id = $1 AND parent_workspace_id = $2) OR (id = $2 AND parent_workspace_id = $1)\n )",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "925936b0d87ed4b39552eeda5a8b41514101d27f242100cfd8f2669f0eeb3786"
}
@@ -0,0 +1,32 @@
{
"db_name": "PostgreSQL",
"query": "SELECT workspace_id, elem->>'git_repo_resource_path' AS \"repo_path!\",\n elem->'auto_pull'->>'webhook_url' AS webhook_url\n FROM workspace_settings, LATERAL jsonb_array_elements(git_sync->'repositories') AS elem\n WHERE jsonb_typeof(git_sync->'repositories') = 'array'\n AND elem->'auto_pull'->'webhook_id' IS NOT NULL\n AND elem->>'git_repo_resource_path' IS NOT NULL",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "repo_path!",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "webhook_url",
"type_info": "Text"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
null,
null
]
},
"hash": "9293d1ae9aa0da9a6a80c1cb74029b71af84859db23f24dcfc0f710ebd78a4d4"
}
@@ -0,0 +1,84 @@
{
"db_name": "PostgreSQL",
"query": "WITH RECURSIVE job_tree AS (\n SELECT j.id, j.kind::text, j.flow_step_id, j.parent_job,\n '' as path_label, 0 as depth,\n j.id::text as id_path,\n ''::text as parent_module_type\n FROM v2_job j\n WHERE j.id = $2 AND j.workspace_id = $1\n UNION ALL\n SELECT j.id, j.kind::text, j.flow_step_id, j.parent_job,\n CASE\n WHEN jt.path_label = '' THEN COALESCE(j.flow_step_id, '')\n ELSE jt.path_label || '/' || COALESCE(j.flow_step_id, '')\n END,\n jt.depth + 1,\n jt.id_path || '/' || j.id::text,\n COALESCE((\n SELECT m->'value'->>'type'\n FROM v2_job parent_j\n LEFT JOIN flow_version fv ON fv.id = parent_j.runnable_id\n AND parent_j.kind::text = 'flow'\n LEFT JOIN flow f ON f.path = parent_j.runnable_path\n AND f.workspace_id = parent_j.workspace_id\n LEFT JOIN flow_node fn ON fn.id = parent_j.runnable_id\n CROSS JOIN LATERAL jsonb_array_elements(\n COALESCE(parent_j.raw_flow, fv.value, f.value, fn.flow)->'modules'\n ) m\n WHERE parent_j.id = jt.id\n AND m->>'id' = j.flow_step_id\n LIMIT 1\n ), '')::text\n FROM v2_job j\n JOIN job_tree jt ON j.parent_job = jt.id\n WHERE j.workspace_id = $1\n AND ($3::text[] IS NULL OR j.tag = ANY($3))\n ),\n positions AS (\n SELECT g.parent_job, g.flow_step_id, fj.jid, fj.ord\n FROM (SELECT DISTINCT parent_job, flow_step_id FROM job_tree\n WHERE parent_job IS NOT NULL AND flow_step_id IS NOT NULL) g\n CROSS JOIN LATERAL (\n SELECT COALESCE(\n (SELECT flow_status FROM v2_job_completed WHERE id = g.parent_job),\n (SELECT flow_status FROM v2_job_status WHERE id = g.parent_job)\n ) AS fs\n ) pf\n CROSS JOIN LATERAL (\n SELECT m FROM jsonb_array_elements(pf.fs->'modules') m\n WHERE m->>'id' = g.flow_step_id\n LIMIT 1\n ) md\n CROSS JOIN LATERAL jsonb_array_elements_text(md.m->'flow_jobs')\n WITH ORDINALITY fj(jid, ord)\n ),\n with_sibling_index AS (\n SELECT jt.*,\n ROW_NUMBER() OVER (\n PARTITION BY jt.parent_job, jt.flow_step_id\n ORDER BY pos.ord NULLS LAST, jt.id\n ) as sibling_index,\n COUNT(*) OVER (\n PARTITION BY jt.parent_job, jt.flow_step_id\n ) as sibling_count\n FROM job_tree jt\n LEFT JOIN positions pos ON pos.parent_job = jt.parent_job\n AND pos.flow_step_id = jt.flow_step_id\n AND pos.jid = jt.id::text\n )\n SELECT w.id, w.kind, w.flow_step_id, w.path_label,\n w.sibling_index::int as sibling_index,\n w.sibling_count::int as sibling_count,\n w.depth::int as depth,\n w.parent_module_type,\n coalesce(job_logs.logs, '') as logs,\n COALESCE(job_logs.log_offset, 0) as log_offset,\n job_logs.log_file_index\n FROM with_sibling_index w\n LEFT JOIN job_logs ON job_logs.job_id = w.id\n ORDER BY w.id_path ASC",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "kind",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "flow_step_id",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "path_label",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "sibling_index",
"type_info": "Int4"
},
{
"ordinal": 5,
"name": "sibling_count",
"type_info": "Int4"
},
{
"ordinal": 6,
"name": "depth",
"type_info": "Int4"
},
{
"ordinal": 7,
"name": "parent_module_type",
"type_info": "Text"
},
{
"ordinal": 8,
"name": "logs",
"type_info": "Text"
},
{
"ordinal": 9,
"name": "log_offset",
"type_info": "Int4"
},
{
"ordinal": 10,
"name": "log_file_index",
"type_info": "TextArray"
}
],
"parameters": {
"Left": [
"Text",
"Uuid",
"TextArray"
]
},
"nullable": [
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
true
]
},
"hash": "94f0e271a67179c78de217ae1f4d7190324533119792b650c6db734a3b99575c"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT EXISTS(\n SELECT 1\n FROM workspace_settings ws\n CROSS JOIN LATERAL jsonb_each(\n CASE WHEN jsonb_typeof(ws.datatable->'datatables') = 'object'\n THEN ws.datatable->'datatables'\n ELSE '{}'::jsonb END\n ) AS dt(key, value)\n WHERE dt.value->'database'->>'resource_type' = 'instance'\n AND dt.value->'database'->>'resource_path' = $1\n AND COALESCE((dt.value->'permissions'->>'enabled')::boolean, false)\n ) AS \"e!\"\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "e!",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "966e630d2f933e4c699e8462bfff3f8a8219f5621138ebd49dfe56cc20704124"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT ahead FROM workspace_diff\n WHERE source_workspace_id = 'test-workspace'\n AND fork_workspace_id = 'wm-fork-tally'\n AND kind = 'script'\n AND path = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "ahead",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "9e19f8203f97e1159928f9afe36d1c5190b6168ded757adf1791fbb9312b41a3"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT path FROM flow WHERE workspace_id = $1 AND archived = false",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "a6396f4efe3c790da61988981ea5fce4df51a7658b8626158608f929d9682e11"
}
@@ -0,0 +1,41 @@
{
"db_name": "PostgreSQL",
"query": "SELECT app.summary, app.policy, app_version.value, app_version.raw_app\n FROM app\n JOIN app_version\n ON app_version.id = app.versions[array_upper(app.versions, 1)]\n WHERE app.workspace_id = $1 AND app.path = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "policy",
"type_info": "Jsonb"
},
{
"ordinal": 2,
"name": "value",
"type_info": "Json"
},
{
"ordinal": 3,
"name": "raw_app",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "ac09d8de92d8e1ee9b463b87b2374e11ba46e9b392414282d2d67161a6544d11"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_diff_full_scan SET fork_workspace_id = $1 WHERE fork_workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "af4048f53cb6ade24d825129d22624ba95b56ef9e5e4c38da329b3d9e8d2269c"
}
@@ -0,0 +1,110 @@
{
"db_name": "PostgreSQL",
"query": "WITH RECURSIVE job_tree AS (\n SELECT j.id, j.kind::text, j.flow_step_id, j.parent_job,\n '' as path_label, 0 as depth,\n j.id::text as id_path,\n ''::text as parent_module_type\n FROM v2_job j\n WHERE j.id = $2 AND j.workspace_id = $1\n UNION ALL\n SELECT j.id, j.kind::text, j.flow_step_id, j.parent_job,\n CASE\n WHEN jt.path_label = '' THEN COALESCE(j.flow_step_id, '')\n ELSE jt.path_label || '/' || COALESCE(j.flow_step_id, '')\n END,\n jt.depth + 1,\n jt.id_path || '/' || j.id::text,\n COALESCE((\n SELECT m->'value'->>'type'\n FROM v2_job parent_j\n LEFT JOIN flow_version fv ON fv.id = parent_j.runnable_id\n AND parent_j.kind::text = 'flow'\n LEFT JOIN flow f ON f.path = parent_j.runnable_path\n AND f.workspace_id = parent_j.workspace_id\n LEFT JOIN flow_node fn ON fn.id = parent_j.runnable_id\n CROSS JOIN LATERAL jsonb_array_elements(\n COALESCE(parent_j.raw_flow, fv.value, f.value, fn.flow)->'modules'\n ) m\n WHERE parent_j.id = jt.id\n AND m->>'id' = j.flow_step_id\n LIMIT 1\n ), '')::text\n FROM v2_job j\n JOIN job_tree jt ON j.parent_job = jt.id\n WHERE j.workspace_id = $1\n AND ($4::text[] IS NULL OR j.tag = ANY($4))\n ),\n positions AS (\n SELECT g.parent_job, g.flow_step_id, fj.jid, fj.ord\n FROM (SELECT DISTINCT parent_job, flow_step_id FROM job_tree\n WHERE parent_job IS NOT NULL AND flow_step_id IS NOT NULL) g\n CROSS JOIN LATERAL (\n SELECT COALESCE(\n (SELECT flow_status FROM v2_job_completed WHERE id = g.parent_job),\n (SELECT flow_status FROM v2_job_status WHERE id = g.parent_job)\n ) AS fs\n ) pf\n CROSS JOIN LATERAL (\n SELECT m FROM jsonb_array_elements(pf.fs->'modules') m\n WHERE m->>'id' = g.flow_step_id\n LIMIT 1\n ) md\n CROSS JOIN LATERAL jsonb_array_elements_text(md.m->'flow_jobs')\n WITH ORDINALITY fj(jid, ord)\n ),\n with_sibling_index AS (\n SELECT jt.*,\n ROW_NUMBER() OVER (\n PARTITION BY jt.parent_job, jt.flow_step_id\n ORDER BY pos.ord NULLS LAST, jt.id\n ) as sibling_index,\n COUNT(*) OVER (\n PARTITION BY jt.parent_job, jt.flow_step_id\n ) as sibling_count\n FROM job_tree jt\n LEFT JOIN positions pos ON pos.parent_job = jt.parent_job\n AND pos.flow_step_id = jt.flow_step_id\n AND pos.jid = jt.id::text\n ),\n limited AS (\n SELECT * FROM with_sibling_index ORDER BY id_path ASC LIMIT $5\n )\n SELECT w.id, w.kind, w.flow_step_id, w.path_label,\n w.sibling_index::int as sibling_index,\n w.sibling_count::int as sibling_count,\n w.depth::int as depth,\n w.parent_module_type,\n c.status::text as completed_status,\n c.duration_ms as \"duration_ms?\",\n COALESCE(c.started_at, q.started_at) as started_at,\n LEFT(c.result::text, $3) as result_prefix,\n length(c.result::text) as result_length,\n q.running as \"q_running?\",\n q.suspend as \"q_suspend?\"\n FROM limited w\n LEFT JOIN v2_job_completed c ON c.id = w.id\n LEFT JOIN v2_job_queue q ON q.id = w.id\n ORDER BY w.id_path ASC",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "kind",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "flow_step_id",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "path_label",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "sibling_index",
"type_info": "Int4"
},
{
"ordinal": 5,
"name": "sibling_count",
"type_info": "Int4"
},
{
"ordinal": 6,
"name": "depth",
"type_info": "Int4"
},
{
"ordinal": 7,
"name": "parent_module_type",
"type_info": "Text"
},
{
"ordinal": 8,
"name": "completed_status",
"type_info": "Text"
},
{
"ordinal": 9,
"name": "duration_ms?",
"type_info": "Int8"
},
{
"ordinal": 10,
"name": "started_at",
"type_info": "Timestamptz"
},
{
"ordinal": 11,
"name": "result_prefix",
"type_info": "Text"
},
{
"ordinal": 12,
"name": "result_length",
"type_info": "Int4"
},
{
"ordinal": 13,
"name": "q_running?",
"type_info": "Bool"
},
{
"ordinal": 14,
"name": "q_suspend?",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text",
"Uuid",
"Int4",
"TextArray",
"Int8"
]
},
"nullable": [
null,
null,
null,
null,
null,
null,
null,
null,
null,
false,
null,
null,
null,
false,
false
]
},
"hash": "b8e732a03969666444f73397ac153d23ec2af084b2f93da24c920532c1916384"
}
@@ -0,0 +1,30 @@
{
"db_name": "PostgreSQL",
"query": "SELECT created_by, COALESCE(flow_innermost_root_job, parent_job) as enclosing_job FROM v2_job WHERE id = $1 AND workspace_id = $2 AND ($3::text[] IS NULL OR tag = ANY($3))",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "created_by",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "enclosing_job",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid",
"Text",
"TextArray"
]
},
"nullable": [
false,
null
]
},
"hash": "bd594f06413dd21d7e6923e6754b58fbce8c09e6ec8172d9ed94716958a29b40"
}
@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_diff (source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)\n SELECT $1, $2, path, kind, 1, 0, NULL\n FROM unnest($3::varchar[], $4::varchar[]) AS t(kind, path)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"VarcharArray",
"VarcharArray"
]
},
"nullable": []
},
"hash": "c12d7865400f91bfb4e5b3077672b5bf3ca91f9d9bed76d2a70c454e468c5c62"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM workspace_diff_full_scan WHERE (source_workspace_id = $1 AND fork_workspace_id = $2)\n OR (source_workspace_id = $2 AND fork_workspace_id = $1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "c9df59253032d0c67cb1992db849efe65c060eda6f97095ffc538d8cb89af0f1"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT git_sync FROM workspace_settings WHERE workspace_id = $1 FOR UPDATE",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "git_sync",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "cdfa16e7a80edc366a64823ca0e0d7f53f71b178d1b7290b1b96b4d3e73f547b"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT raw_app FROM app_version WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "raw_app",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false
]
},
"hash": "d20a717824d89e89969e94b024b11d425cb1dbdd132f3fba16c5fec72dd31490"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT scanned_at FROM workspace_diff_full_scan\n WHERE source_workspace_id = $1 AND fork_workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "scanned_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "d27405e2a5dcdcc4eb6d2d9c32e4438fdd1aa44a365d201bb1bb9a19edfac6b1"
}
@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT app.path, app_version.raw_app FROM app\n JOIN app_version ON app_version.id = app.versions[array_upper(app.versions, 1)]\n WHERE app.workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "raw_app",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "d51263081dc8bd13d13d6b3d4eac1420f42b3904dbe9d733a02def6307d711d9"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT name FROM resource_type WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "d6e209f9fc9af2a29702ddc4529b8596daba82911d9354ba53371b509c314732"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT datatable || '/' || timestamp || '_' || name FROM datatable_migrations\n WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "de5db89535e4e04e8814d8b0d220c3659eeb80034fcb890c5a9943b4ef23e4d4"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT path FROM variable WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "dfc33795385cb5eef70b481938542d061a5c8474b1cd8092505dcc4507270a62"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH RECURSIVE chain AS (\n SELECT id, parent_workspace_id, is_dev_workspace, 0 AS depth\n FROM workspace WHERE id = $1\n UNION ALL\n SELECT w.id, w.parent_workspace_id, w.is_dev_workspace, chain.depth + 1\n FROM workspace w\n JOIN chain ON w.id = chain.parent_workspace_id\n WHERE chain.depth < 20\n )\n SELECT id AS \"id!\" FROM chain\n WHERE parent_workspace_id IS NULL\n OR (is_dev_workspace AND id NOT LIKE 'wm-fork-%')\n ORDER BY depth LIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id!",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "e30be7c812aa294df6899892531083221146338193e38ef7268bacff58270bf8"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace (id, name, owner) VALUES ('other-workspace', 'other-workspace', 'test-user')",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "e903f5857d2e15f0472d9f3536493201e0ee5210f684e2735b79f929276cbc04"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT DISTINCT path FROM script WHERE workspace_id = $1 AND archived = false",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "f1adf9d7a8a53d8e8f3c4eab8a64c2ba707ca8dcda69eba28397c3b0259fc876"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_settings (workspace_id) VALUES ('other-workspace')",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "f6f59f768dc0e34f4cad118b5252a554bf51c4581dae48c0f93f859672997544"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT 'f/' || name FROM folder WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "fb518edc29faa45a1b1503b151cc77c509608825721c01cb3b860c20cda49377"
}
@@ -119,6 +119,26 @@ pub(crate) async fn check_shared_database_forbid(
database: &DataTableDatabase,
self_perms_enabled: bool,
) -> Result<()> {
// A DuckLake catalog on the same instance database always connects as the
// shared role, which protection revokes — enabling would silently break
// every attach to that catalog.
if self_perms_enabled
&& database.resource_type
== windmill_common::workspaces::DataTableCatalogResourceType::Instance
&& windmill_common::datatable_permissions::instance_database_used_by_ducklake(
db,
&database.resource_path,
)
.await?
{
return Err(Error::BadRequest(format!(
"A DuckLake catalog is configured on instance database '{}'. DuckLake connects as \
the shared instance role, which fine-grained permissions revoke on protected \
databases, so the two cannot share one database. Move the data table (or the \
lake) to its own database first.",
database.resource_path
)));
}
let resource_type = match database.resource_type {
windmill_common::workspaces::DataTableCatalogResourceType::Instance => "instance",
windmill_common::workspaces::DataTableCatalogResourceType::Postgresql => "postgresql",
@@ -3139,9 +3139,6 @@ async fn edit_datatable_config(
}
}
// An external target must be able to create the enforcement roles.
// Instance targets are skipped: their roles are managed by the main
// pool user, and the instance database may legitimately not be set up
// yet when the config is saved.
if self_enabled && dt.database.resource_type == DataTableCatalogResourceType::Postgresql {
if let Err(e) =
crate::datatable_permissions_api::check_owner_can_create_roles(&db, &w_id, dt).await
@@ -3153,6 +3150,31 @@ async fn edit_datatable_config(
}
}
}
// An instance target must get its dedicated owner role NOW: saving the
// config as protected while the shared role still holds CONNECT would
// advertise an enforcement that isn't there — and with the default
// empty grants nothing would ever reach the lazy provisioning path to
// repair it. If the database isn't ready (not set up yet), fall back to
// disabled rather than claim protection.
if self_enabled && dt.database.resource_type == DataTableCatalogResourceType::Instance {
if let Err(e) = windmill_common::datatable_permissions::provision_instance_owner_role(
&db,
&w_id,
&dt.database.resource_path,
)
.await
{
if self_defaulted {
tracing::info!(
"not enabling data table permissions by default on {}: {e:#}",
dt.database.resource_path
);
default_disabled.push(name.clone());
} else {
return Err(e);
}
}
}
}
for name in default_disabled {
if let Some(dt) = new_config.settings.datatables.get_mut(&name) {
@@ -4755,6 +4777,16 @@ async fn set_encryption_key(
let mut tx = db.begin().await?;
// Before touching the key row: a resolver that already holds
// WORKSPACE_ROLES_LOCK reads the workspace key on a SEPARATE pool
// connection, so updating the row first and then waiting for that lock
// deadlocks in a way Postgres cannot see (the wait crosses connections).
// Taking the lock first makes the two operations strictly ordered.
sqlx::query(windmill_common::datatable_permissions::WORKSPACE_ROLES_LOCK)
.bind(&w_id)
.execute(&mut *tx)
.await?;
sqlx::query!(
"UPDATE workspace_key SET key = $1 WHERE workspace_id = $2",
request.new_key.clone(),
@@ -4819,10 +4851,6 @@ async fn set_encryption_key(
// workspace lock keeps role creation from inserting old-key
// ciphertext after this scan; the per-role locks keep an in-flight
// teardown/sweep from acting across the key switch. Held to commit.
sqlx::query(windmill_common::datatable_permissions::WORKSPACE_ROLES_LOCK)
.bind(&w_id)
.execute(&mut *tx)
.await?;
let dt_roles = sqlx::query!(
"SELECT role_name, password, owner_creds FROM datatable_ephemeral_role WHERE workspace_id = $1",
w_id
@@ -447,6 +447,26 @@ async fn connect_instance_db_as_superuser(db: &DB, dbname: &str) -> Result<tokio
connect_target(&pg, db).await
}
/// Instance enforcement leans on PG16's `CREATEROLE` restriction (a role may
/// only alter roles it created). Before 16, any role holding `CREATEROLE`
/// could reset the dedicated owner's password, so protection would be
/// advisory only — refuse rather than claim an enforcement that does not hold.
pub async fn ensure_instance_enforcement_supported(db: &DB) -> Result<()> {
let version: i32 = sqlx::query_scalar("SELECT current_setting('server_version_num')::int")
.fetch_one(db)
.await
.map_err(|e| Error::internal_err(format!("reading Postgres version: {e:#}")))?;
if version < 160000 {
return Err(Error::BadRequest(format!(
"Fine-grained permissions on instance data tables require PostgreSQL 16 or later \
(this cluster reports {version}). On older versions a role with CREATEROLE can \
take over the database's owner role, so the grants could not be enforced. Use an \
external PostgreSQL data table instead, or upgrade the cluster."
)));
}
Ok(())
}
/// Make sure a protected instance database has its dedicated owner role: a
/// `LOGIN` role that owns the database's objects, can create the ephemeral
/// roles, and is never handed to non-admin SQL. `custom_instance_user` loses
@@ -455,6 +475,7 @@ async fn connect_instance_db_as_superuser(db: &DB, dbname: &str) -> Result<tokio
/// Idempotent, and re-asserts the revoke on every call (superadmin re-running
/// the instance-database setup re-grants it).
pub async fn provision_instance_owner_role(db: &DB, w_id: &str, dbname: &str) -> Result<()> {
ensure_instance_enforcement_supported(db).await?;
let role = instance_owner_role_name(dbname);
let mut tx = db.begin().await?;
sqlx::query(OWNER_ROLE_LOCK)
@@ -670,12 +691,79 @@ pub async fn deprovision_instance_owner_role(db: &DB, dbname: &str) -> Result<()
/// Best-effort [`deprovision_instance_owner_role`] for the paths where a
/// failure must not block the operation (config saves, database re-points).
///
/// Re-reads the live config first: these run after their transaction commits,
/// so a concurrent enable may already have re-provisioned this database. A
/// blind deprovision would then hand a still-protected database back to the
/// shared role.
pub async fn deprovision_instance_owner_role_best_effort(db: &DB, dbname: &str) {
match instance_database_still_protected(db, dbname).await {
Ok(true) => {
tracing::info!(
"skipping deprovision of {dbname}: a data table with permissions enabled still \
points at it"
);
return;
}
Ok(false) => {}
Err(e) => {
tracing::warn!("checking protection state of {dbname} before deprovision: {e:#}");
return;
}
}
if let Err(e) = deprovision_instance_owner_role(db, dbname).await {
tracing::warn!("deprovisioning owner role of instance database {dbname}: {e:#}");
}
}
/// Whether any workspace still has a permissions-enabled data table pointing
/// at this instance database.
async fn instance_database_still_protected(db: &DB, dbname: &str) -> Result<bool> {
Ok(sqlx::query_scalar!(
r#"
SELECT EXISTS(
SELECT 1
FROM workspace_settings ws
CROSS JOIN LATERAL jsonb_each(
CASE WHEN jsonb_typeof(ws.datatable->'datatables') = 'object'
THEN ws.datatable->'datatables'
ELSE '{}'::jsonb END
) AS dt(key, value)
WHERE dt.value->'database'->>'resource_type' = 'instance'
AND dt.value->'database'->>'resource_path' = $1
AND COALESCE((dt.value->'permissions'->>'enabled')::boolean, false)
) AS "e!"
"#,
dbname
)
.fetch_one(db)
.await?)
}
/// Whether a DuckLake catalog is configured on this instance database. Such a
/// catalog always connects as the shared role, which protection revokes — so
/// the two cannot coexist on one database.
pub async fn instance_database_used_by_ducklake(db: &DB, dbname: &str) -> Result<bool> {
Ok(sqlx::query_scalar!(
r#"
SELECT EXISTS(
SELECT 1
FROM workspace_settings ws
CROSS JOIN LATERAL jsonb_each(
CASE WHEN jsonb_typeof(ws.ducklake->'ducklakes') = 'object'
THEN ws.ducklake->'ducklakes'
ELSE '{}'::jsonb END
) AS dl(key, value)
WHERE value->'catalog'->>'resource_type' = 'instance'
AND value->'catalog'->>'resource_path' = $1
) AS "e!"
"#,
dbname
)
.fetch_one(db)
.await?)
}
// ---------------------------------------------------------------------------
// Role lifecycle
// ---------------------------------------------------------------------------
@@ -1392,6 +1480,33 @@ pub async fn teardown_snapshot_roles_best_effort(db: &DB, w_id: &str, snapshot:
}
}
/// Revoke one caller's ephemeral role on a data table, if it still exists.
/// Best-effort: a failure leaves the bookkeeping row in place, so the expiry
/// sweep retries; the caller is denied either way.
pub(crate) async fn revoke_caller_role(
db: &DB,
w_id: &str,
datatable: &str,
permissioned_as: &str,
) {
let role = ephemeral_role_name(w_id, permissioned_as, datatable);
let exists = sqlx::query_scalar!(
"SELECT 1 FROM datatable_ephemeral_role WHERE role_name = $1",
&role
)
.fetch_optional(db)
.await
.ok()
.flatten()
.is_some();
if !exists {
return;
}
if let Err(e) = teardown_role(db, w_id, datatable, &role, None).await {
tracing::warn!("revoking datatable ephemeral role {role} after losing its grants: {e:#}");
}
}
/// Strict teardown of every role of a workspace, for workspace deletion: the
/// bookkeeping rows (and the workspace key their targets are encrypted with)
/// are about to cascade away, so every role must be revoked NOW — dropped, or
@@ -1644,6 +1759,11 @@ pub async fn get_datatable_resource_from_db_checked(
let (matched, memberships) =
compute_effective_grants(db, w_id, permissioned_as, grants).await?;
if matched.is_empty() {
// Losing the last grant (group/folder membership change, edited
// statements) must revoke the role NOW, not at expiry: the caller may
// have set its password from their own SQL, so a role left LOGIN-capable
// is a live direct connection long after Windmill says "denied".
revoke_caller_role(db, w_id, name, permissioned_as).await;
return Err(Error::PermissionDenied(format!(
"You have no permissions on data table '{name}'. Ask a workspace admin to grant \
you access in the data table's permission settings."