Compare commits

..
Author SHA1 Message Date
windmill-internal-app[bot] c3e1317bac [ee] Use .max() to honor existing scheduled_for in debounced enqueue 2026-05-04 16:05:35 +00:00
hugocasaandClaude Opus 4.5 85a05765e2 docs(skills): document S3Object inputs and S3 streaming in script skills (#9022)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-04 15:53:12 +00:00
42be1d46a6 fix(autoscaling): consider dedicated workers in scale decisions (#9020)
* [ee] fix(autoscaling): consider dedicated workers in scale decisions

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Update ee-repo-ref.txt

* [ee] fix(autoscaling): mirror worker tag precedence (worker_tags wins)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to 862d487032efe30d1e4a3cd0a1ed7169500c4cd9

This commit updates the EE repository reference after PR #556 was merged in windmill-ee-private.

Previous ee-repo-ref: cf87e9dcef2e95b1834b3f5c154209defc5a9ca2

New ee-repo-ref: 862d487032efe30d1e4a3cd0a1ed7169500c4cd9

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-05-04 15:33:24 +00:00
Ruben Fiszel 0cfa2ad436 /ai-fast is now /ai 2026-05-04 15:22:30 +00:00
Diego ImbertandClaude Opus 4.7 f1fd245073 feat: add separate filter searchbar for resource types tab (#9019)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 15:01:06 +00:00
GuilhemandClaude Opus 4.7 8529a2cf1a chore(frontend): expose DarkModeObserver, TextInput, common/Badge from windmill-components (#9018)
Adds three subpath entries to the windmill-components package's `exports`
and `typesVersions` so external consumers (e.g. windmillhub) can import
these components without resorting to private `node_modules` aliases.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 13:19:03 +00:00
44fad139fe fix: bind MySQL table listing to configured database name (#9007)
* fix: bind MySQL table listing to configured database name

* refactor: drop DATABASE() sentinel and cover single-table fallback

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: unify MySQL schema-resolution branches via explicit_db binding

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 08:50:14 +00:00
Ruben Fiszel 26a41df3e3 chore: use concat! for SCRIPT_COLUMNS to avoid \ continuation whitespace 2026-05-04 08:44:15 +00:00
Ruben FiszelandClaude Opus 4.5 1174d7d77f refactor: replace SELECT * with explicit column lists (#9010)
* refactor: replace SELECT * with explicit column lists

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: update sqlx offline query cache

* chore: update sqlx offline query cache

* chore: update sqlx offline query cache with EE support

* chore: update sqlx offline query cache, no deletions

* chore: update sqlx offline query cache after rebase

* fix: correct column names in explicit script query lists

- concurrency_limit → concurrent_limit (matches DB column name)
- runnable_settings → runnable_settings_handle (matches DB column name)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: add missing delete_after_secs column to script queries

Also add integration test covering all explicit-column export queries.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: add workspace export integration test covering all explicit-column queries

Covers tarball_workspace (folder, script, resource, resource_type, variable,
schedule, usr, group_) and the mcp_oauth_client SELECT query from windmill-mcp.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: add tarball export integration test covering all explicit-column queries

Single test creates one of each entity type and exercises every runtime-checked
explicit-column query in tarball_workspace. Uses archive_type=tar to avoid
zip feature-gate in CI.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: fix stale verification step and CI contradiction in update-sqlx skill

- Regenerate current_files.txt after EE cache restoration so step 4 reports accurate diff
- Scope "Never use SQLX_OFFLINE=true" to local prepare (CI legitimately uses it)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: remove Co-Authored-By from commit skill template

* refactor: extract SCRIPT_COLUMNS const to single source of truth

Replaces 5 duplicated 44-column lists with a shared const in windmill-types.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-04 08:39:45 +00:00
Ruben FiszelandClaude Opus 4.5 392888d113 docs: add SAFETY comments to all dynamic SQL call sites (#9009)
* docs: add SAFETY comments to all dynamic SQL call sites

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: address review feedback on SAFETY comments

- Fix missed comment for obo_triggers loop in offboarding.rs
- Fix variable name in comment (table -> table_name) in offboarding.rs
- Fix api-settings comment to reference inline VALID_NAME regex, not validate_dbname()
- Add SAFETY comments to batch_execute calls in api-settings
- Fix db.rs comment: PG_SCHEMA is env var, not compile-time constant
- Add doc comments on RunnableSettingsTraitInternal constants

* docs: remove misleading SAFETY comment on static SQL

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-03 07:14:14 +00:00
hugocasaandClaude Opus 4.7 da95588b25 fix(forks): strip mode/enabled from merge-UI deploy payload (#9008)
* fix(forks): strip mode/enabled from merge-UI deploy payload

The CompareWorkspaces merge UI deploys a trigger by fetching the full
GET response from the source workspace and spreading it into an
updateXTrigger call on the target. That spread includes `mode` (and the
legacy `enabled`), so a fork→parent (or parent→fork) deploy would
overwrite the target's enabled/disabled state — silently disabling a
parent's trigger when its config is merged from a freshly-cloned fork
(clones are forced `mode='disabled'`). Affects azure/email/gcp/http
whose `update_trigger` SQL writes the `mode` column.

Strip `mode`/`enabled` in `getTriggersDeployData` so the backend's
existing `is_mode_unspecified()` preservation in `update_trigger` keeps
the target row's `mode` untouched. The same preservation already
protects the YAML/CLI round-trip (where the tarball export strips these
fields); this extends the same guarantee to the merge-UI path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(forks): wire Azure and Email triggers through merge UI deploy

CompareWorkspaces.svelte already lists Azure and Email triggers in its
diff (`triggerServices`), but `getTriggersDeployData` and `existsTrigger`
were missing the corresponding branches. Deploying either kind from the
merge UI threw "Unexpected trigger kind". Add the branches with the
same `stripOperationalState` pattern as the rest, and extend the
`triggersKind` whitelist in `checkItemExists`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:12:43 +02:00
Ruben Fiszelandrubenfiszel dfe534b1a6 chore(main): release 1.694.0 (#8998)
* chore(main): release 1.694.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-05-01 21:02:43 +00:00
hugocasaandClaude Opus 4.7 66db873651 fix: surface scope errors as 403 and show real message in CLI (#8953)
* fix: surface scope errors as 403 and show real message in CLI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address review feedback on scope error PR

- Backend: also patch handler-level check_scopes (lib.rs:223) — without
  this, endpoints using check_scopes (scripts, flows, jobs, …) still
  returned 401 for scope failures, which the CLI would render as the
  misleading auth message.
- CLI: strip backend file refs and the duplicated "Permission denied:" /
  "Not authorized:" prefix from the surfaced error body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-01 20:55:49 +00:00
d60dd745e4 feat(forks): handle triggers and schedules in workspace forks (#8976)
* feat(forks): strip operational state from triggers/schedules on git-sync export

When the source workspace is a fork (`wm-fork-*`), the tarball export now
omits `mode` from triggers and `enabled` from schedules. The trigger update
handler also preserves the existing DB `mode` when both fields are absent
from the request, instead of falling back to the BaseTriggerData default.

This prevents a fork's git-sync round-trip from flipping the parent
workspace's enabled/disabled state when a merge applies the fork's YAML
back to main.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(forks): opt-in fork_triggers flag clones triggers/schedules disabled

Adds `workspace.fork_triggers` (default false) and a matching field on
CreateWorkspaceFork. When the user opts in, fork creation also runs
clone_triggers_and_schedules: every row in schedule and the ten
*_trigger tables is copied to the fork with mode='disabled' /
enabled=false. Listener identifiers (group_id, replication_slot_name,
subscription_name, …) are copied verbatim — the runtime suffix that
prevents the fork from competing with the parent ships in a follow-up
PR.

native_trigger is intentionally skipped: those triggers manage external
webhook state we don't want duplicated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(forks): warn before enabling triggers/schedules that conflict with parent

set_trigger_mode and schedule's set_enabled now check whether the parent
workspace has the same path actively enabled. If so, the call is rejected
with a `fork-conflict:<kind>:<parent_id>` error unless the request includes
`force=true`. The frontend interprets the prefix to surface a confirm-to-
proceed dialog.

This is the placeholder safety net until the Phase 3 listener-suffix work
removes the conflict for the namespaceable kinds (Kafka/MQTT/NATS/Postgres/
Azure/GCP-CreateNew). For SQS, GCP-Existing, and schedules — where there's
no namespacing fix — the warning is the durable solution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(forks): UI: opt-in clone-triggers checkbox + confirm-on-fork-conflict

Adds the user-facing surface for the fork-trigger work:

- CreateWorkspaceInner: new "Clone triggers and schedules" toggle in the
  fork-creation dialog (default off). Sends fork_triggers in the request.

- forkConflict utility: detects the `fork-conflict:<kind>:<parent_id>`
  error string from the backend, shows a confirm() dialog explaining
  why the action is blocked, retries with `force: true` if accepted.

- Wires withForkConflictRetry into every trigger setMode and the
  schedule setEnabled call, both in the per-kind editor components and
  the +page.svelte list views (HTTP, websocket, kafka, NATS, SQS, MQTT,
  GCP, Azure, Postgres, email, schedule).

OpenAPI spec gains the `force` field on each setmode/setenabled body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(forks): CLI --fork-triggers flag, fork-trigger docs, skill update

- Adds --fork-triggers boolean to wmill workspace fork; passes
  fork_triggers through to the create_fork API call.
- New docs/fork-triggers.md describing the model end-to-end (default,
  opt-in clone, merge-direction filter, conflict warning, future
  runtime-suffix work).
- Updates the adding-a-trigger SKILL.md to mention the fork-export
  ignore-keys participation and the clone_triggers_and_schedules
  block that new trigger kinds must extend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: regenerate sqlx offline query cache for fork-trigger SQL

* fix(forks): replace browser confirm() with ConfirmationModal for fork conflict

The fork-conflict warning previously used the browser's native confirm()
which doesn't match Windmill's design system. Switches to a singleton
ConfirmationModal mounted at the (logged) layout root, driven by a new
forkConflictModal store. The withForkConflictRetry helper now sets the
store and awaits the user's choice via a Promise, instead of blocking
on window.confirm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(forks): filter unchanged triggers in merge UI, add diff view, surface parent-only ones

The fork merge UI listed every trigger from the fork as a deployable item
regardless of whether it differed from the parent — so a fork created with
fork_triggers=true (which clones triggers in disabled state, otherwise
identical) showed every trigger as a "Fork-only" change. The 'Update
current' tab also missed triggers newly created in the parent that the
fork hadn't pulled yet.

This refactor:

- fetchAllTriggers now lists both fork and parent in parallel for each
  trigger kind, then merges by path.
- Computes a per-trigger `changeKind` (new / modified / deleted-in-source)
  using a JSON comparison that strips runtime + fork-local fields
  (mode/enabled/server_id/last_server_ping/edited_at/edited_by/etc.) so
  the disabled-on-clone difference doesn't show up as a change.
- Filters the trigger items in deployableItems by the current direction:
  Deploy mode shows fork-side new/modified, Update mode shows parent-side
  new/modified.
- Replaces the always-on "Fork-only" badge with proper New/Modified
  badges and surfaces a Diff button (modal Drawer + Monaco DiffEditor)
  for modified triggers — the diff strips the same ignored fields so
  users see only the meaningful config differences.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(forks): always clone triggers/schedules disabled, drop opt-in flag

Disabled triggers and schedules are inert — no listener attaches, no cron
fires — so cloning them by default is safe by construction. Drops the
fork_triggers opt-in flag introduced earlier in this PR:

- Drops workspace.fork_triggers column (migration removed)
- Removes fork_triggers from CreateWorkspaceFork (API + OpenAPI)
- Removes the conditional in create_workspace_fork — clone always runs
- Removes the toggle from the fork-creation dialog
- Removes --fork-triggers from `wmill workspace fork`
- Updates docs/fork-triggers.md and adding-a-trigger SKILL.md

The merge UI continues to exclude triggers from the deploy/update default
selection, so a routine merge from a fork doesn't accidentally push
trigger config the user hasn't intentionally changed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(http-triggers): scope route exists check by workspace, skip non-workspaced clones in forks

The non-CLOUD branch of `route_path_key_exists` self-excluded by trigger
path alone, which silently masked cross-workspace collisions once forks
started cloning trigger rows verbatim. Tighten it to exclude only the
exact `(workspace_id, path)` row.

Fork creation also now skips non-workspaced HTTP triggers — their URL
has no workspace prefix, so a clone collides with the parent at the
matchit router (which silently drops one of two duplicates) and there is
no namespacing escape hatch. The clone copies all rows when CLOUD_HOSTED
or HTTP_ROUTE_WORKSPACED_ROUTE forces every route workspaced regardless.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(forks-ui): silent cancel on enable conflict, clean up trigger rows in compare view

forkConflict helper now returns undefined when the user dismisses the
modal instead of throwing, so the redundant 'Cannot enable: undefined'
toast no longer appears.

CompareWorkspaces trigger rows now mirror the script row layout: drop
the redundant Disabled badge and the Trash/Details buttons (both belong
on the dedicated trigger pages, not in the deploy/compare view); pass
triggerKind through so RowIcon picks the right kind-specific icon; move
extraLabel into the summary line; replace the yellow Modified badge
with the same green ↗ ahead / blue ↘ behind treatment scripts use.

Trigger diff drawer: switch JSON → YAML for parity with DiffDrawer, fix
zero-height monaco render with className=!h-full, drop the redundant
Original/Modified label banner above the diff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(email-trigger): scope local_part exists check, skip non-workspaced clones in forks

Mirrors the HTTP route fix for the email-trigger non-CLOUD `email_exists`
check (in EE) which had the same path-only self-exclusion bug, and the
fork clone of `email_trigger` rows which copied non-workspaced
`local_part` verbatim. Skip non-workspaced rows in the clone unless the
instance is CLOUD_HOSTED (where lookup is workspace-scoped natively).

EE companion change in windmill-trigger-email/src/handler_ee.rs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to 78512dd73b4a1c9f70574cff863374179e3a621b

This commit updates the EE repository reference after PR #554 was merged in windmill-ee-private.

Previous ee-repo-ref: 1ac77f50747b58e720a11162dfd309bc252a24ab

New ee-repo-ref: 78512dd73b4a1c9f70574cff863374179e3a621b

Automated by sync-ee-ref workflow.

* fix(forks): always-warn on parent row, kind-specific modal copy, cancel-aware toggles

- Conflict check now fires whenever the parent has the path (regardless of
  parent's mode), since the cloned upstream identifier is shared by
  construction; closes the Postgres slot-takeover gap when the parent is
  disabled. Schedule's set_schedule_enabled gets the same treatment.
- Skip the warning entirely for HTTP and Email via a new
  TriggerCrud::FORK_CONFLICT_ON_ENABLE const — both kinds are workspace-
  scoped at runtime so cloned rows can't collide with the parent.
- Modal copy branches by failure family: split-events (Kafka/NATS/MQTT/SQS/
  GCP/Azure), duplicate-firing (Websocket/Schedule), slot-takeover
  (Postgres). Generic fallback for unknown kinds.
- withForkConflictRetry now returns boolean (true=committed, false=
  cancelled). TriggerModeToggle reuses its existing innerTriggerMode local
  state via a function binding for the regular Toggle, snapping back to
  the prop when onToggleMode signals a cancel — needed because the native
  bind:checked diverges from the parent's prop after a click and Svelte's
  reactivity won't re-push a same-valued prop down. Schedule list page
  uses {#key} on a reset version since it renders Toggle directly.
- Editor inners revert mode = previousMode on cancel; list pages skip the
  re-fetch (loadTriggers/loadSchedules) on cancel to avoid pointless
  network traffic and the schedule "Job stats loading..." flash.
- Drop withForkConflictRetry from HTTP and Email editors + list pages
  since the backend never emits the conflict for those kinds.

* fix(forks-ui): widen onToggleMode types, scope schedule toggle reset by path

- TriggerEditorToolbar and TriggerSuspendedJobsModal forwarded
  onToggleMode as `(mode) => void`, dropping the new boolean return so
  any caller wired through them would silently no-op the cancel-revert.
  Match the wider TriggerModeToggle signature.
- Schedule list page used a single resetVersion counter for every row's
  {#key}, so cancelling on any one schedule remounted every <Toggle> on
  the page. Switch to a per-path Record<string, number> bumped only for
  the affected row.

* chore: bump ee-repo-ref to c3a4553 (email FORK_CONFLICT_ON_ENABLE override)

* fix(forks): include Suspended in conflict gate, use parent_workspace_id for fork detection

Three fixes from the Claude review on PR #8976:

- Suspended mode still attaches the listener (it just pauses auto-run of
  queued jobs); two suspended fork+parent listeners would still split
  Kafka events / share a PG slot. Gate set_trigger_mode on
  `mode != Disabled` instead of `mode == Enabled` so Suspended also
  surfaces the warning.
- workspaces_export.rs::fork_*_ignore_keys keyed off the wm-fork-* prefix
  while set_trigger_mode and set_schedule_enabled key off
  parent_workspace_id. Switch the export filter to query
  parent_workspace_id once at the top of tarball_workspace and pass
  is_fork through. The column is the contract; the prefix is a
  creation-time naming convention that could in principle drift.
- TriggerModeToggle's suspend-dropdown action reassigned the non-bindable
  `triggerMode` prop instead of the local `innerTriggerMode` mirror,
  leaking inconsistent state if the dispatch was cancelled. Now writes
  to innerTriggerMode like the Toggle's on:change handler does.

* fix(cli): skip setScheduleEnabled when local YAML lacks `enabled`

Tarball export from a fork strips `enabled` from schedules so the
fork→parent git-sync round-trip can't flip the parent's operational
state. The CLI's pushSchedule called setScheduleEnabled whenever
`localSchedule.enabled != schedule.enabled`, which evaluates truthy
when local is undefined (fork-pulled YAML) and remote is true/false —
sending `{ enabled: undefined }` that serializes to `{}` and gets
rejected by the backend (`SetEnabled.enabled` is required).

Skip the call when `localSchedule.enabled === undefined` so a sync push
of fork-pulled YAMLs preserves the target's existing enabled state
instead of erroring out. Trigger updates were already safe — the
backend's update_trigger preserves `mode` when the request omits it.

* Revert "fix(cli): skip setScheduleEnabled when local YAML lacks `enabled`"

This reverts commit 23ba7e72fc.

* feat(cli): --force flag and friendlier error on fork-conflict for schedule enable

`wmill schedule enable foo/bar` against a fork whose parent has the same
path used to surface the raw `fork-conflict:schedule:<parent>` error
body. The CLI now:

- accepts `--force` to bypass the warning (mirrors the API field and the
  UI's "Enable anyway" confirmation),
- detects the `fork-conflict:` prefix on errors and prints a one-screen
  explanation pointing at --force instead of the raw body.

Disable doesn't trigger the warning (the gate fires only on transitions
to listener-attaching modes), so no flag there. Trigger enable/disable
isn't exposed as a standalone CLI command — sync push goes through
updateTrigger which has its own backend mode-preservation, so no
fork-conflict surfaces from the CLI for those.

* chore: regenerate cli-commands docs after adding --force to schedule enable

* chore: update ee-repo-ref to 967f961f0a88b027d894aebd03977181129477a8

This commit updates the EE repository reference after PR #555 was merged in windmill-ee-private.

Previous ee-repo-ref: c3a4553296473932e15392a06415dd7fb9aa6591

New ee-repo-ref: 967f961f0a88b027d894aebd03977181129477a8

Automated by sync-ee-ref workflow.

* fix(forks): address CI dead-code, claude/cubic review feedback

- backend: cfg-gate `fork_trigger_ignore_keys` to match its already-gated
  callsite. CI compiles with `-D warnings`, so the unused-fn under feature
  combos that disable all trigger crates was breaking check_oss/check_ee/
  cargo_test/test-linux/test-windows.
- cli: re-apply the `pushSchedule` undefined-skip (originally 23ba7e7,
  reverted in 4d172a1). Tarball export from forks strips `enabled`, so
  fork-pulled YAMLs that get sync-pushed back via `wmill schedule push`
  would otherwise serialize `{ enabled: undefined }` → `{}` and the
  backend's required `SetEnabled.enabled` rejects the body. Skipping
  preserves the target's existing flag, which is the round-trip-safe
  behavior. (`wmill workspace merge` extension to triggers/schedules is
  tracked in #9001 — until then sync push is the only CLI path.)
- TriggerModeToggle suspend-dropdown action awaits onToggleMode and
  resets `innerTriggerMode = triggerMode` on cancel, matching the Toggle
  on:change handler. Without this, dismissing the fork-conflict modal on
  a Suspend transition leaves the toggle stuck in 'suspended'.
- forkConflict: when a new modal opens with a previous resolver still
  pending, resolve the older promise to false. Avoids a dangling promise
  if the user clicks toggles on two rows in quick succession.
- schedules list: bump `toggleResetVersions[path]` on the
  permission-denied branch so the Toggle re-mounts back to the prop's
  `enabled` value. Without this, a user without write permission could
  click the toggle and have it stick visually flipped.
- docs/fork-triggers.md: switch the merge-direction filter description
  from `wm-fork-*` prefix to `parent_workspace_id IS NOT NULL` (matches
  the code after 4dd38fe). Drop the misleading "merge-direction filter
  strips identifier columns too" line in Future Work — the runtime
  suffix is applied at listener attach, the stored column never carries
  it, so no export filtering is needed there.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-05-01 20:55:24 +00:00
Ruben Fiszel 45d959a49e nit tests 2026-05-01 20:52:47 +00:00
Ruben Fiszel 28b5e3382c chore: fix cargo checks 2026-05-01 20:39:41 +00:00
Ruben Fiszel 96bc00007b chore: fix cargo checks 2026-05-01 18:47:30 +00:00
70a5880d36 fix: distinguish AlreadyCompleted from execution failure on OTLP job span (#9004)
* fix: distinguish job-already-completed from execution failure on OTLP span

handle_queued_job's bool return type conflated two distinct Ok(false)
cases: a real race with another worker (Error::AlreadyCompleted) and any
job execution that returned an error via process_result. The outer "job"
span recorded "job already completed by another worker" for both, so
every failed Python/bun/etc. script ended up with that misleading
otel.status_message even though the real error was correctly recorded
on the inner job_postprocessing span.

Replace the bool with a JobOutcome enum (Completed / Failed { description }
/ AlreadyCompleted). Failed carries the truncated error string, so the
outer span's Status.message now reflects the actual cause.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: extract user-facing error from error_value for span description

Cubic flagged that capturing e.to_string() before the match meant
ExitStatus failures (the most common failure mode for script jobs)
ended up with the generic "exit status: …" string rather than the
script error extracted from job logs by extract_error_value.

Move the description capture to after error_value is built and
deserialise it as ErrorMessage to pull out the structured message.
Falls back to "Job failed" if the value isn't shaped that way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: use permissive description extraction for OTLP status message

Cubic flagged that strict ErrorMessage parsing downgraded real failures
to a generic "Job failed" whenever the result wasn't shaped as
{message, name} — e.g. agent-worker's "See logs for more details" raw
string, or runtime-written result.json files with a different shape.

Switch to parsing as serde_json::Value and pulling .message out if it's
a string, falling back to the whole value if it's a bare string. Only
fall back to "Job failed" when neither is available.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-05-01 18:25:18 +00:00
Ruben Fiszel 0f26418f4a fix cargo checks 2026-05-01 18:24:45 +00:00
hugocasaandClaude Opus 4.7 ad9f1fa454 fix: nested-restart iteration count for step-id collisions across subflow boundaries (#9003)
* feat: support restart from steps inside BranchOne, ForLoop, Subflow

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: preserve original job kind in nested restart, support expanded subflow steps

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: read selected iteration from graph state for nested ForLoop restart

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: iteration selectors per ForLoop in restart popup, more nested restart tests

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: extract useNestedRestartState composable

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: cover deployed-subflow + FlowDependencies path in nested restart

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: update sqlx prepare cache

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: detect BranchOne/ForLoop ancestors inside expanded subflows for nested restart

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: hide restart button for non-restartable steps (parallel containers, untaken branches)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address review feedback on nested restart PR

- preview FlowRestartButton: hide nested case (chain UUIDs aren't resolvable in
  preview path; users can use the run page for nested restart instead)
- branchOneAncestorMatchesOriginal: be permissive when status isn't reachable
  (don't hide the button for BranchOnes nested deeper than top-level)
- worker_flow.rs: apply nested_restart_payload swap on the is_simple ForLoop
  fast path too, so simple iterations don't bypass restart spawn interception
- FlowStatusViewer: reset expandedSubflows cache on jobId change; drop
  $bindable({}) banned pattern for the new prop
- API resolver: validate the leaf step exists before returning (fail-fast)
- doc fix: branch_or_iteration_n is 0-based, not 1-based
- selectedJobStepIsTopLevel reset on early-return in composable
- comment iterationCounts collision caveat
- new HTTP-level integration tests covering the API endpoint contract:
  happy path (top-level + nested), unknown step, out-of-range iteration,
  parallel-loop rejection

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* revert: remove unreachable nested-restart swap on is_simple ForLoop fast path

The swap is unreachable in valid flows: `is_simple_modules` requires the body
to be a single `script` / `rawscript` / `flowscript` (per `FlowModule::is_simple`),
none of which spawn flow-kind children. Any nested-restart chain targeting a
leaf inside such an iteration is rejected by the API at leaf validation. Even
if a chain reached the worker via `JobPayload::RawFlow.restarted_from`, the
resulting `RestartedFlow` would fail to push (script kind isn't a flow kind).

Replaced the swap with an explanatory comment so the next reader knows why
the symmetry with the non-simple path was deliberately not added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: handle undefined expandedSubflows + tighten branchOne match check

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: iteration count for restart popup picks up wrong loop on step-id collision

When a top-level ForLoop step shares an id with a loop nested inside an expanded
subflow (e.g. parent has step `e` with 4 iterations and the subflow at step `h`
also has step `e` with 1 iteration), the popup's iteration `<select>` rendered
the subflow's count instead of the parent's because `iterationCounts` was
double-indexed by both the prefixed graph key and the bare leaf segment after
the last colon — last writer wins, and the bare key collided.

- `iterationCounts` now indexed only by the full graph key. The selected-step
  iteration picker still works because at the top level the step id IS the
  graph key.
- New `nestedPathIterationCounts` keyed by the popup's field-key
  ('top' / 'inner-N'). Populated by the composable during path construction so
  each entry uses the correct prefix-aware graph key, regardless of whether the
  ancestor lives in the parent flow or inside an expanded subflow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 18:03:40 +00:00
Ruben FiszelandClaude Opus 4.5 419bc4b175 fix: omit empty assets array on scripts and raw app inline scripts (#9006)
* fix: omit empty assets array on scripts and raw app inline scripts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: skip raw app asset effect on parser errors

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-01 18:03:02 +00:00
Ruben FiszelandClaude Opus 4.5 5753516da5 read inline-script tag from app policy in run mode (#9005)
* fix: read inline-script tag from app policy in run mode

Previously the worker tag for app inline scripts was taken from the
client-supplied raw_code on every execute. End users running a deployed
app could intercept the request and submit any tag, redirecting the job
to an arbitrary worker group.

Persist the tag on PolicyTriggerableInputs at deploy time, and in run
mode read it from the policy instead of the request body. Preview mode
(editor-only) still honors the client tag, since the editing user is
already trusted by the policy check.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: also reject client tag in legacy run-mode (no app_script id)

The previous commit only enforced policy-tag in the id-bearing arm.
Apps deployed before the lockfile/app_script entry exists hit the
\`(None, Some(raw_code), None)\` arm in run mode (triggerable keyed by
\`rawscript/<sha>\`), where client tag was still trusted.

Hoist an \`is_preview\` flag from the outer match and route both inline
arms through it: client tag is honored only in preview mode.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-01 17:50:09 +00:00
Ruben Fiszel be39dcfd5b update gitInitRepo script 2026-05-01 17:47:39 +00:00
Ruben FiszelandClaude Opus 4.5 0c22f52b46 feat: support assigning a worker tag to app inline scripts (#9002)
* feat: support assigning a worker tag to app/raw-app inline scripts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: omit empty tag field from inline script raw_code payload

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* style: shrink tag popover width

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-01 17:21:17 +00:00
Ruben FiszelandClaude Opus 4.7 aedf369174 fix: pair PG arg type with actual Rust binding to keep query_typed_raw safe (#8999)
* fix: pair PG arg type with actual Rust binding to keep query_typed_raw safe

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(pg): wrap encoder errors with arg context, add fallback test

Followups on #8999 review:

- Wrap rust-postgres "error serializing parameter N" failures with the arg
  name, JSON value kind, and asserted Postgres type plus a hint about an
  explicit cast — so users see actionable context instead of an opaque
  WrongType.
- Drift-prevention meta-test: assert otyp_to_pg_type and convert_val agree
  on the Type for every recognised arg_t when the JSON value matches its
  natural Rust kind. Catches future drift if either side changes.
- Integration test for the prepare + query_raw fallback path: confirms
  unrecognised arg_t (custom enum) is routed through prepare and the
  server-resolved type appears in the failure surface — flips into a
  test failure if a regression accidentally routes unrecognised types
  through query_typed_raw.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pg): add otyp_inferred flag + regex-based placeholder renumbering

Two follow-ups from the review of #8999:

1. **Issue #1 (Number/Bool + explicit text decl in WHERE)**

   Add `Arg::otyp_inferred: bool` to the parser. The PG SQL parser sets
   it `true` only at the "no info → fall back to text" site (bare `$N`,
   no inline cast, no `-- $N (TYPE)` decl). All other arg sources keep
   it `false`.

   In `convert_val` this flag distinguishes:
   - explicit text-like target (`-- $1 (text)` or `$1::text`) — coerce
     `Bool`/`Number` → `Box<String>` so `WHERE text_col = $1` works
     (`text = text` operator). Pre-#8988 behaviour, restored.
   - parser-default text (bare `$N`) — bind the value's natural Rust
     type so the regression case (`Value::Bool` against a real `bool`
     column via `CAST AS bool`) keeps working.

   `Arg` is in `windmill-parser`; the new field has `#[serde(default)]`
   so persisted signatures stay backward-compatible.

2. **Issue #4 ($5/$50 substring rewrite collision)**

   Replace the per-index `String::replace` chain (which turned `$50`
   into `$10` when oidx=5 was processed first) with a single regex
   pass. `\d+` is greedy, so `$5` and `$50` match as distinct units;
   indices outside the mapping are left intact.

3. Tests:
   - parser: `test_parse_pgsql_otyp_inferred_flag` covers bare/inline-
     cast/decl/mixed shapes.
   - executor unit: `convert_val_bool_against_every_arg_t` and
     `convert_val_*_number_*` split each text-like target into explicit
     vs inferred expectations.
   - executor unit: `renumber_sparse_placeholders_no_collision`.
   - integration: `test_postgresql_arg_type_combinations` adds 4 cases
     covering decl(text)+Number/Bool in WHERE, bare $1+Bool, and
     sparse positional args ($5/$50).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pg+sdk): enum support, extended String arms, position-aware $N rewrite, SDK quality

Backend:

1. **`AnyTextValue` ToSql/FromSql wrapper**: vanilla `tokio_postgres`'s
   `ToSql for String` / `FromSql for String` reject `Kind::Enum` and
   `Kind::Domain` even though the wire format is plain UTF-8. The wrapper
   accepts those kinds in both directions. End result: explicit
   `$1::my_enum` / `CAST($1 AS my_enum)` casts now round-trip without the
   ugly `CAST($1::text AS my_enum)` workaround, AND `SELECT enum_col`
   results come back as JSON strings instead of erroring at the FromSql
   layer.

2. **#10 — Value::String → numeric/real/double/oid/bool**. Without these
   arms, a string-encoded value (`"3.14"`, `"true"`) for a non-text /
   non-temporal arg_t fell through to `Box<String> + TEXT`, which then
   failed at the server (no implicit cast text→numeric in expression
   context). Now strings are parsed into the matching native type with
   clear error messages on parse failure.

3. **Position-aware `$N` rewrite**: replaces the regex-based renumbering
   (which fixed the `$5/$50` substring collision but still walked through
   string literals and comments, mangling `'price: $5'` etc.) with a
   walk over `parse_pg_statement_arg_positions` — the same
   string/comment/dollar-quote-aware tokenizer used for index discovery.
   Adds `parse_pg_statement_arg_positions` to the parser's public API.

SDK:

4. **BigInt support**: `JSON.stringify(BigInt)` throws. The SDK now
   stringifies bigints before serialisation; the executor accepts
   numeric strings into BIGINT arg slots via the existing
   `Value::String → INT8` parsing arm. SDK-side `inferSqlType` is split
   so `BigInt` always resolves to `BIGINT` (was reaching
   `Number.isInteger(BigInt)` which returns false → wrong default).

5. **Homogeneous array auto-tag**: `${[1,2,3]}` against an `int[]` column
   now emits `$1::BIGINT[]` instead of `$1::JSON`. Detection covers
   primitive types only (number / bigint / string / boolean); mixed or
   nested arrays still fall back to JSON. Mixed int/float widens to
   `DOUBLE PRECISION[]`.

6. **`.query()` positional bug**: previously the `.query()` method
   abused the template-tag builder, which appended `$N::TYPE` after the
   user's literal SQL string instead of binding by position
   (`SELECT $1, $2` became `SELECT $1, $2$1::BIGINT`). Now `.query()`
   builds the executor-shaped content directly: a `-- $N argN (TYPE)`
   declaration block followed by the user's SQL verbatim.

Tests:

- Parser: `test_parse_pg_statement_arg_positions_skips_strings_and_comments`
  asserts string literals, comments, and dollar-quoted blocks don't
  produce positions (so renumbering doesn't mangle them).
- Executor unit: `renumber_sparse_placeholders_no_collision_no_string_mangling`
  uses the new position-aware path and includes string-literal + comment
  + `$$…$$` cases. Existing convert_val tests grow to cover new
  String→numeric/real/double/oid/bool arms.
- Integration: `test_postgresql_arg_type_combinations` adds 13 cases
  (enum round-trip both directions, string→numeric/real/double/bool/oid,
  string-literal `$N` non-mangling). The prepare-fallback test now
  asserts SUCCESS (not failure) for enum encoding via AnyTextValue.
- SDK: new `typescript-client/tests/sqlUtils.test.ts` (42 tests)
  exhaustively covering inferSqlType primitives + arrays,
  parseTypeAnnotation, datatable() template tag (with all the new
  shapes — BigInt, homogeneous arrays, RawSql, schema preamble),
  datatable().query() positional, and ducklake() shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pg): replace DISCARD ALL with curated reset (preserves typeinfo cache)

Found while exhaustively probing custom-type DX: every cached-connection
reuse was running `DISCARD ALL`, whose included `DEALLOCATE ALL`
deallocates *all* prepared statements server-side — including the typeinfo
statements that tokio_postgres caches per-Client to resolve custom enum /
domain Oids. tokio_postgres still held `Statement` objects whose names
the server had forgotten, so the next custom-type query failed with
intermittent "prepared statement \"sN\" does not exist" errors. The
failure was easy to reproduce: any sequence that forced typeinfo lookup
for two different custom-type kinds on the same cached connection (e.g.
enum followed by domain) would hit it.

Replace `DISCARD ALL` with a curated reset that explicitly targets the
state we actually care about, *without* touching prepared statements:

  RESET ALL                     — GUC parameters (search_path, application
                                  _name, statement_timeout, …)
  RESET SESSION AUTHORIZATION   — undoes both `SET SESSION AUTHORIZATION`
                                  and `SET ROLE` (RESET ALL does NOT —
                                  these aren't GUC parameters, so without
                                  this an elevated role from a previous
                                  job would silently leak)
  UNLISTEN *                    — drops LISTEN registrations
  CLOSE ALL                     — closes open cursors

Trade-off: temp tables, advisory locks (session-scoped), and user-created
PREPARE statements may persist across cached-connection reuse — rare in
datatable / PG-script workloads. tokio_postgres's typeinfo cache survives
intact, so custom enum / domain queries are fast on subsequent reuse.

Tests:
- `test_postgresql_custom_types_on_cached_connection` — runs 10×
  alternating enum + domain queries on a cached connection. Pre-fix this
  failed with `prepared statement "sN" does not exist` after the first
  reuse; post-fix passes.
- `test_postgresql_set_role_does_not_leak_across_cached_connection` —
  switches `SET ROLE` and `SET SESSION AUTHORIZATION` to a non-postgres
  role, then runs a follow-up job and asserts current_user/session_user
  are restored. Specifically catches the case where someone might switch
  back to `RESET ALL` alone (which doesn't cover SET ROLE / SESSION
  AUTHORIZATION) and silently introduce a permission-leak vector.
- All existing session-isolation tests
  (`test_postgresql_cached_connection_resets_session`,
   `test_postgresql_single_worker_session_isolation`,
   `test_postgresql_100_jobs_cached`) continue to pass.

Found via end-to-end probing of datatable / PG-script DX, not previously
covered: the existing isolation tests only did `SET ROLE postgres`, the
connecting user, so the leak was invisible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pg): address PR #8999 review (cubic + claude)

cubic (P1, real bug):
- `convert_vec_val` for `timetz` array asserted `Type::TIMETZ_ARRAY`, but
  chrono `NaiveTime` only encodes for TIME (same caveat as the scalar
  arm). Switch to `Type::TIME_ARRAY`; rely on PG's implicit `time→timetz`
  assignment cast at the column site. Add an explicit unit test.

claude (#1, silent failure → explicit error):
- `Bool` + explicit `(char)` / `(character)` decl previously silently
  bound BOOL, hoping the server would cast at the use site — but PG has
  no implicit `bool→char` and the resulting error
  ("operator does not exist: bool = char") was opaque. Now error at
  bind time with an actionable hint to use `bool` decl or pass the
  value as a "t"/"f" string.

claude (#2, asymmetry doc):
- Object/Array still coerce to text on `matches!(typ, Typ::Str(_))`
  (covers both explicit AND inferred-default text), unlike Bool/Number
  which key on `explicit_text_target`. The asymmetry is intentional
  (no implicit `jsonb → text` cast in expression context vs PG having
  implicit `bool/int → text` casts) — added a body comment so future
  maintainers don't try to "align" them.

claude (#3, perf):
- `parse_pg_statement_arg_indices` and `parse_pg_statement_arg_positions`
  walked the SQL tokenizer twice. Fold into a single pass that derives
  the index set from the position list.

claude (#4, fmt drift):
- `cargo fmt` over the parser crates I touched with perl scripts in the
  earlier commit (windmill-parser-{sql,bash,ts,go,php,java,csharp,nu,py,
  rust,graphql,yaml,r}). Net cosmetic.

claude (#5, parseTypeAnnotation):
- One-line caveat in the SDK's `parseTypeAnnotation` that the returned
  string is presence-only (e.g. `${x}::DOUBLE PRECISION` returns
  `"DOUBLE"`, `CAST(${x} AS int)` returns `"int)"` — neither matches a
  real PG type, but the only consumer just checks `!== undefined`).

While here — discovered + fixed independently while exhaustively probing
DX:

- **Replace `DISCARD ALL` with curated reset** (`RESET ALL; RESET
  SESSION AUTHORIZATION; UNLISTEN *; CLOSE ALL;`). DISCARD's
  `DEALLOCATE ALL` killed tokio_postgres' typeinfo cache, producing
  intermittent `prepared statement "sN" does not exist` errors on
  custom-type queries after cached-conn reuse. New regression tests:
  `test_postgresql_custom_types_on_cached_connection` and
  `test_postgresql_set_role_does_not_leak_across_cached_connection`
  (the latter catches the case where someone might switch back to
  `RESET ALL` alone and silently introduce a permission-leak vector —
  RESET ALL doesn't cover SET ROLE / SET SESSION AUTHORIZATION).

- **ISO-8601 timestamp results** (`pg_cell_to_json_value`). Pre-fix
  `TIMESTAMP` was rendered with a space separator ("2024-01-15 10:30:00")
  and `TIMESTAMPTZ` with " UTC" suffix ("2024-01-15 10:30:00 UTC") —
  neither parseable by `date-fns parseISO`, JavaScript `new Date()` is
  lenient enough to handle them but several frontend `App*Input.svelte`
  components use parseISO and fail silently. Switched to ISO-8601 with
  `T` separator and `+00:00` offset; arg-parsing path still accepts the
  legacy " UTC" suffix for back-compat.

Test coverage:
- 17/17 unit (`pg_executor::tests`)
- 9/9 integration (`backend/tests/worker.rs`, `test_postgresql_*`)
- 27/27 parser (`windmill-parser-sql`)
- 42/42 SDK (`typescript-client/tests/sqlUtils.test.ts`)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pg): bounded one-shot warning on numeric precision loss + ISO-8601 + NaN handling

Found while probing PG-script DX with millions of numeric cells:

1. **Numeric precision-loss warning**: `numeric` results are still serialised
   as JSON Number (back-compat — switching to JSON String would silently
   break user code doing arithmetic on results), but we now detect
   `Decimal -> f64 -> Decimal` round-trip failure and emit a single
   job-log warning recommending a `::text` cast in the SQL. Bounded by
   `NUMERIC_PRECISION_CHECK_BUDGET = 256` cells per query (one atomic
   load + one fetch_sub on the hot path; first lossy value
   short-circuits to a single load thereafter). Worst-case overhead on
   a 1M-cell numeric-heavy query: ~25µs of checks + 5ns × N atomic
   loads (vs. ~100ms unbounded).

2. **ISO-8601 timestamps**: `pg_cell_to_json_value` previously returned
   `"2024-01-15 10:30:00"` (TIMESTAMP) and `"2024-01-15 10:30:00 UTC"`
   (TIMESTAMPTZ) — neither parseable by date-fns `parseISO`, which is
   what the apps `App*Input.svelte` components use, so timestamp values
   silently failed to round-trip into date pickers. Switch to ISO-8601
   (`T` separator + `+00:00` offset) on the result side; arg-parser
   continues to accept the legacy `" UTC"`-suffixed format for
   back-compat.

3. **Float NaN / Infinity results**: `Number::from_f64` returns None for
   NaN / ±Inf, which `pg_cell_to_json_value` was raising as
   "invalid json-float" — failing the *entire* query if any cell held
   one of these special values. Now serialise them as JSON strings
   ("NaN", "Infinity", "-Infinity") and let the rest of the row come
   through. Arg-side: `s.parse::<f64>()` already accepts the same
   strings.

Tests:
- `decimal_fits_f64_losslessly_predicate` — covers fits / doesn't-fit
  cases for the precision-loss predicate.
- `precision_check_budget_caps_per_query_overhead` — locks in the
  budget cap and the loss-flag short-circuit.
- All 9 PG integration tests + 17 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pg): add pg_advisory_unlock_all to reset; warn on missing args; honor decl defaults

While probing PG-script DX further found three more frictions:

1. **Advisory lock leak** (cubic P2): switching from `DISCARD ALL` to
   `RESET ALL; RESET SESSION AUTHORIZATION; UNLISTEN *; CLOSE ALL;`
   meant session-scoped advisory locks (`pg_advisory_lock`) leaked
   across cached-connection reuse. Add `SELECT pg_advisory_unlock_all()`
   to the chain — `DISCARD ALL` covered this implicitly via
   `DISCARD PLANS / DEALLOCATE / pg_advisory_unlock_all` and we lost it
   in the switch.

2. **Missing-arg silent NULL**: an arg declared in the SQL (e.g.
   `-- $1 amount (numeric)`) but not provided in the args object was
   bound as NULL with no error / warning. Misspelling the key in the
   args object silently produced a row of NULLs — a notorious DX
   debugging trap. Now: collect the names of declared-but-missing
   args during dispatch and emit a single one-shot warning to the job
   logs at end-of-query naming each one. Bound NULL is preserved for
   back-compat.

3. **Declaration defaults ignored**: `-- $1 a (int) = 5` carries
   `arg.default = Some(Number(5))`, but the dispatch fell straight to
   NULL when the arg was missing. Now: respect the default —
   user-supplied value > declaration default > NULL. Also fixes the
   warning logic above (only warn for args that *don't* have a default).

Tests: existing 19 unit + 9 integration pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pg): multi-word PG types with [] suffix lost the array-ness; array arms accept stringified values

Two more frictions found while probing SDK end-to-end against a real
datatable resource:

1. **Multi-word array types lose the [] suffix in the parser**.
   `transform_types_with_spaces` recognises aliases for "double
   precision", "character varying", "timestamp with time zone", etc.
   but its return type was `&'a str` — only the bare alias, never with
   a trailing `[]`. The `RE_CODE_PGSQL` regex's `\w+` captures stop at
   the first space, so the regex's own `(?:\[\])?` array-suffix branch
   sees only `"double"` (not `"double precision[]"`); the `[]` was
   silently lost. Result: `$1::double precision[]` (which the SDK now
   emits for homogeneous float arrays via the new auto-tag) routed
   through `Value::Array → Type::JSONB` and the server failed with
   "cannot cast type jsonb to double precision[]".

   Fix: switch `transform_types_with_spaces` to return `Cow<'a, str>`
   and re-check the trailing bytes after a multi-word match. If they
   start with `[]`, return `format!("{alias}[]")` — Owned. Single-word
   types and the no-match path keep returning Borrowed slices, so no
   allocation in the hot path.

2. **Array arms in `convert_vec_val` rejected stringified values for
   numeric / int* / bool / oid / real / double**. The scalar `convert_val`
   already parses strings into the matching native type for these arg_ts,
   but the array variant only accepted JSON-native counterparts. Sending
   `["1.5", "2.5", "3.5"]` against `$1::numeric[]` (e.g. via `unnest` for
   bulk loading, or `JSON.stringify(BigInt[])` round-trip) failed with
   "Mixed types in array". Now the array arms mirror the scalar ones —
   `as_<native>().or_else(|| as_str().and_then(parse))` — so both shapes
   round-trip cleanly.

Tests: 19 unit + 9 integration pass; existing parser tests cover the
multi-word array forms (the regex-cap behaviour didn't break for
single-word types, and Cow plumbing is transparent to all callers).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(parsers): add otyp_inferred field to Arg literals in tests + 3 missed src files

CI failures: the perl-driven sweep that added `otyp_inferred: false` to
every `Arg { ... }` literal when I introduced the field in the parser
schema covered `src/lib.rs` files but missed:

  - parsers/windmill-parser-bash/src/lib.rs       (mass-edited but a
    later format pass un-applied a few sites)
  - parsers/windmill-parser-go/src/lib.rs         (same)
  - parsers/windmill-parser-graphql/src/lib.rs    (same)
  - parsers/windmill-parser-nu/tests/tests.rs     (test file — not
    swept the first time)
  - parsers/windmill-parser-ts/tests/tests.rs     (test file — same)

Also tightened the regex to handle `oidx: None` without the trailing
comma (some test files had the field as the last initialiser line).

`cargo build --features <CI feature combo> --workspace --all-targets`
is clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sdk): Date → TIMESTAMPTZ; NaN / ±Infinity → string

Two more frictions found while running the actual SDK end-to-end against
a live datatable resource:

1. **JS `Date`** fell into the typeof "object" branch and was tagged
   `::JSON`. It worked accidentally for `${date}::timestamptz` via PG's
   `json → text → timestamptz` implicit cast chain, but `${date}` against
   a `timestamptz` column without a user-supplied cast bound the value
   as a JSON string and the comparison `timestamptz = json` failed. Now:
   `inferSqlType` recognises `Date` and tags `::TIMESTAMPTZ`;
   `serializeArgValue` emits `Date.toISOString()` so the executor's
   `Value::String → TIMESTAMPTZ` arm parses it cleanly.

2. **JS `NaN` / `±Infinity`** silently became NULL. `JSON.stringify(NaN)`
   returns `"null"` per the JS spec, so the value reached the executor as
   JSON null — the SDK's `::DOUBLE PRECISION` tag then bound a NULL
   double. Fix: detect non-finite numbers in `serializeArgValue` and
   stringify them as `"NaN" / "Infinity" / "-Infinity"`. The executor's
   `Value::String → FLOAT8` arm (`f64::from_str`) accepts these literals
   directly, and the result-side already renders the values as JSON
   strings (matching round-trip).

SDK unit tests grow from 42 → 44 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(pg): integration coverage for multi-word arrays + stringified array elements

Locks in the two array fixes from the previous commit
(`fix(pg): multi-word PG types with [] suffix lost the array-ness`)
with end-to-end cases in `test_postgresql_arg_type_combinations`:

- `double precision[]`, `character varying[]`, `timestamp without time
  zone[]` — verifies the parser keeps the `[]` suffix after multi-word
  alias resolution.
- `numeric[]` / `int[]` / `bool[]` from stringified primitives — verifies
  the array arms of `convert_vec_val` apply the same string-coercion
  the scalar arms do.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style: fix indentation drift on otyp_inferred lines

cargo fmt cleanup of leftover indentation where the perl-driven sweep
that introduced the otyp_inferred field landed at the wrong column.
No behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-01 17:08:59 +00:00
Ruben Fiszel 95f8db67f3 update git sync version to 1.693.5 2026-05-01 16:49:48 +00:00
21411282bb fix: route email trigger path through standard info channel (#8996)
* docs(skill): document email triggers and S3 attachments

Add an "Email triggers" section to the triggers skill covering the
local-part config, the parsed_email/raw_email/email_extra_args payload,
the URL-style extras convention, where to find trigger_path (only with
a preprocessor, at event.trigger_path), and — most importantly — that
binary attachments are uploaded to the workspace S3 bucket and surface
as `{ s3: "windmill_emails/<job_id>/attachments/<filename>" }`. Scripts
must use wmill.loadS3File / wmill.load_s3_file to read them.

Also pulls EmailTrigger into the schema mappings so a real
`email_trigger.schema.yaml` is generated, and adds Email/Azure to the
trigger kinds list in the CLI agent guidance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref for email trigger path fix

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to 26184ab7a4aadfc529dcedf038aa08d36c7ad381

This commit updates the EE repository reference after PR #553 was merged in windmill-ee-private.

Previous ee-repo-ref: 318a46897a605dc9be3817901f35ba5a99a0a525

New ee-repo-ref: 26184ab7a4aadfc529dcedf038aa08d36c7ad381

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-05-01 16:23:09 +00:00
hugocasaandClaude Opus 4.7 9cb777a6b6 fix: use otel.status_message for OTLP Status.message on failed jobs (#8995)
tracing-opentelemetry only recognizes otel.status_code and
otel.status_message as fields that map to the OTLP Status proto.
The previously-used otel.status_description fell through to the
generic attribute recorder, leaving Status.message unset and
preventing OTLP consumers from filtering spans on error status.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 16:19:22 +00:00
hugocasaandClaude Opus 4.5 0b959b8ec6 feat(cli): wmill-lock.yaml auto-fill + --rehash-only + path-prefix dedup (#8978)
* fix(cli): canonical lockfile hashes + lock upgrade migration to v3

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): use __app_hash subpath in rehash missing-entry check

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): run sync pull lockfile auto-fill regardless of changes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: regenerate system prompts for new lock and rehash-only commands

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): address review feedback on lock upgrade

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): drop v3 marker; always run fallback; fail-fast on unknown lockfile version

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): drop yaml-round-trip legacy hash variant; recover via --rehash-only

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): include legacy hash in script push staleness warning check

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* revert(cli): drop canonical hash formula; keep raw-bytes hashing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* perf(cli): reuse change-tracker map for sync pull lockfile auto-fill

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): address review feedback on rehash-only

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(cli): pin lockfile hash + yaml format and cover regression cases

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(cli): byte-stable snapshot tests for flow.yaml format

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(cli): add app and script-metadata yaml snapshot fixtures

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): address claude review on rehash-only

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(cli): factorize script-path to remote-path derivation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): address claude + cubic review (dry-run mutation, rehash short-circuit)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(cli): make rehash a subcommand and factorize fs walks

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): normalize line endings in yaml snapshot tests for windows ci

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): address review feedback on rehash + auto-fill

- Flat-layout scripts now clearGlobalLock before rehash write so legacy
  ./-prefixed duplicates get cleaned up (matches flow/app behavior).
- Add MalformedLockfileError; sync pull auto-fill re-throws it alongside
  UnknownLockVersionError instead of silently warning + continuing.
- Document the legacy step-removal false-negative in
  isFlowDirectlyStale / isAppDirectlyStale and the categorizeLocalFiles
  ignore-filter invariant.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-01 15:11:56 +00:00
Ruben FiszelandClaude Opus 4.7 5d5b853f70 fix(cli): only preserve case for raw-app runnableIds, not app/flow summaries (#9000)
* fix(cli): only preserve case for raw-app runnableIds, not app/flow summaries

PR #8940 stopped lowercasing in sanitizeForFilesystem to fix #8939, where
a raw-app runnableId like CamelCaseTSRunnable produced a CamelCase YAML
metadata file but a lowercased code file, making them desync and
register as duplicate runnables on push.

That fix overshot. sanitizeForFilesystem is also reached by
newPathAssigner, which serves normal apps and flows where the input is
the script's human summary ("Get Users Data") rather than an identifier.
There the on-disk filename is the only artifact — there's no companion
YAML to keep in sync — so lowercasing was the right behavior. Removing
it changed both the on-disk filename and the !inline reference in
app.yaml / flow.yaml from get_users_data.inline_script.ts to
Get_Users_Data.inline_script.ts on the next pull, surfacing as
unwanted case churn for users updating to 1.693.x.

Add a preserveCase option to sanitizeForFilesystem (default false →
lowercase). newRawAppPathAssigner opts in; newPathAssigner stays on
the default. Update unit tests accordingly and add an end-to-end
raw-app round-trip in raw_app_sync.test.ts that pushes a CamelCase
backend runnable, pulls it back, and asserts both YAML and code file
preserve case with no lowercase orphan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(cli): use readdir for exact-case orphan check on Windows

The CamelCase round-trip test used fileExists("camelcasetsrunnable.ts")
to assert no lowercase orphan was produced, which false-positives on
Windows since the filesystem is case-insensitive and resolves the
lookup to the existing CamelCaseTSRunnable.ts. Switch to readdir +
toContain so the exact on-disk casing is compared identically on Linux
and Windows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:52:55 +00:00
Ruben FiszelandClaude Opus 4.5 96324ea5ae feat: ansible delegate_to_git_repo install_requirements, dynamic fields, --limit (#8997)
* feat: ansible delegate_to_git_repo install_requirements, dynamic fields, --limit

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: include .yaml variants in collections/roles requirements lookup

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-01 12:12:04 +00:00
Ruben Fiszelandrubenfiszel 02fe2e7511 chore(main): release 1.693.4 (#8994)
* chore(main): release 1.693.4

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-05-01 06:57:17 -04:00
hugocasaandClaude Opus 4.7 7c227ece0d fix(cli): pin wasm parser versions in published package.json (#8993)
Previously build-npm.ts emitted "*" for every wasm parser dep. Combined
with lockfiles pinning the first resolved version, users who installed
when a parser was briefly behind got stuck on a stale wasm even after
the registry caught up — surfacing as "TypeError: parse_ts_relative_imports
is not a function" warnings during sync/generate-metadata.

Forward the dev package.json specs into the generated package.json
instead, and pin all 12 parsers to the exact versions published on npm
(mirroring frontend/package.json).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 12:13:29 -04:00
Ruben Fiszelandrubenfiszel 6922631b03 chore(main): release 1.693.3 (#8989)
* chore(main): release 1.693.3

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-04-30 14:21:04 +00:00
Ruben FiszelandClaude Opus 4.7 4483d0cab9 fix(workspaces): split get_settings into admin-only + public endpoint (#8990)
* fix: redact GitHub App tokens and Slack OAuth secret for non-admins

`GET /workspaces/get_settings` returned the full `git_app_installations`
JSONB to any workspace member. That column caches the GitHub App JWT and
installation token used by git-sync; the installation token is refreshed
on every git-sync action and valid for ~55 minutes, so the value sitting
in the DB is essentially always live. Null it out for non-admins,
matching the existing `slack_oauth_client_secret` redaction.

The tarball export's v2 settings format (added in #8935) included
`slack_oauth_client_secret` with no admin gating, regressing the same
redaction. Mirror the admin check there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: split get_settings into admin-only + public endpoint

Adds `WorkspacePublicSettings` and `GET /workspaces/get_public_settings`,
which returns only fields safe for any workspace member to read
(workspace_id, slack/teams team identity, mute_critical_alerts, deploy_ui,
large_file_storage, datatable). `get_settings` is now admin-only via
`require_admin`.

Migrates frontend callers: every caller that read non-sensitive fields
(deploy_ui on trigger pages, mute_critical_alerts on the root layout, slack
team identity for handler pickers, etc.) now uses `getPublicSettings`. The
admin-managed settings UI, git-sync admin context, operator settings,
checkout polling, and full settings page stay on `getSettings`.

This replaces the field-level redactions added in the previous commit:
the type system itself defines the public surface, so adding a sensitive
column to `workspace_settings` no longer defaults to leaking — it stays
out of `WorkspacePublicSettings` unless explicitly added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 14:02:54 +00:00
Ruben FiszelandClaude Opus 4.7 568d9cc8a0 fix: sanitize underscores in agent worker suffix (#8992)
* fix: sanitize underscores in agent worker suffix

The agent worker token wire format is `jwt_agent_<suffix>_<JWT>`, parsed
server-side with `split_once('_')`. JWTs themselves can contain `_`
(base64url alphabet), so the only sound boundary is "suffix has no `_`".

`instance_name()` is the source of the suffix and previously only
sanitized spaces and `-`. A hostname like `austin_hp` would produce
`worker_suffix=austin_hp-<rand>`, the token `jwt_agent_austin_hp-X_<JWT>`
would split as `("austin", "hp-X_<JWT>")`, and the JWT decoder would
return `InvalidToken` on the garbage second half, surfacing as a 401 on
`/api/agent_workers/update_ping`.

Replace `_` with `-` in the hostname-derived suffix so the parsing
boundary stays unambiguous. Pure source-side fix; no wire format change,
no migration needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: apply underscore->dash before splitting in instance_name

Replace `_` with `-` BEFORE the `split("-").last()` step so underscores
behave the same as dashes (consistent with the split-on-dash idiom) and
the resulting instance_name is a single token. For `austin_hp` this
yields `hp` rather than `austin-hp`. No behavior change for hostnames
without `_`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: strip underscores from hostname instead of replacing with dash

Removing `_` preserves more identifier info than replacing with `-`:
`austin_hp` becomes `austinhp` (single useful token) rather than `hp`
(prefix lost to the dash-split). For k8s-style hostnames that already
contain `-`, the `-` continues to do the splitting and `_` is just a
stray character to strip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 13:53:50 +00:00
Ruben FiszelandClaude Opus 4.7 a03f5c0fab [ee] fix: GitRepoViewer reliable load for large repos (#8991)
Fixes silent timeouts and partial-tree rendering when loading large git
repositories into the in-app viewer (Tony Hoang report: 400+ host_vars
files, 32 roles).

Three coordinated fixes:

1. Frontend (GitRepoViewer.svelte): drop the 60s clone timeout. Long-poll
   getJobUpdates until the job completes, with a 30 min hard cap and a
   user-cancel button. Stream live job logs into the viewer with a link
   to the full job page. After success, verify the
   .windmill_clone_complete marker before flipping pathExists, so a
   partial S3 directory is no longer rendered as a complete tree.

2. Backend (check_s3_folder_exists, EE): new optional marker_file query
   param. When set, the handler short-circuits to head() on the marker
   object instead of "any object under prefix exists". The frontend now
   always passes marker_file=.windmill_clone_complete.

3. Hub script: cloneRepoToS3forGitRepoViewer points at hub/28216, which
   uploads files via a bounded-concurrency pool (16 workers), emits
   throttled progress logs, and writes .windmill_clone_complete as its
   last action. docs/clone_repo_and_upload_to_instance_storage.bun.ts is
   the source for that hub publish; docs/git-repo-viewer-hub-script.md
   explains the change.

Also drops three unused legacy hubPaths entries
(cloneRepoToS3forGitRepoViewer_0..2) — none were referenced from
anywhere in the codebase.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 13:28:50 +00:00
Ruben Fiszel e7deaf9882 fix: improve raw app builder queue behavior for bigger apps 2026-04-30 13:10:01 +00:00
Ruben FiszelandClaude Opus 4.7 7bdfc0ea06 fix: avoid named prepared statements in datatable/PG executor (#8988)
Always send queries as unnamed prepared statements (query_typed_raw)
when arg types resolve via otyp_to_pg_type. This eliminates the
intermittent "prepared statement \"sN\" does not exist" error reported
on datatable scripts whose Postgres connection sits behind a
transaction-mode pooler (PgBouncer/Supabase pooler/RDS Proxy), where
prepare and execute can land on different backend connections.

The previous code only used the unnamed-statement path when the parser
detected at least one explicitly typed arg; datatable-generated SQL
with bare $1/$2 (relying on inline ::cast hints) fell back to
prepare + query_raw and accumulated named statements (s0, s1, ...,
s882, ...) on the cached connection, which the pooler then dropped.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 13:04:34 +00:00
Ruben Fiszelandrubenfiszel 34ba176f52 chore(main): release 1.693.2 (#8987)
* chore(main): release 1.693.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-04-30 12:24:16 +00:00
8196857c8f add workflow-as-code skill (#8970)
* feat: add workflow-as-code skill

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: make system prompt freshness self-contained

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Revert "fix: make system prompt freshness self-contained"

This reverts commit 7d2fde9585.

* fix: refresh wac generated guidance

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: add wac cli eval cases

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: align wac prompt imports

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-04-30 12:23:59 +00:00
hugocasaandClaude Opus 4.7 3c3c03455d fix: OAuth popup login reliability + auto-login Safari edge cases (#8971)
- Login.svelte: poll whoami after popup opens as a safety net for Safari
  ITP — when the popup is opened without a fresh user gesture, cookies
  and localStorage can be partitioned, leaving the existing postMessage
  / storage signaling unable to reach the parent. Polling is independent
  of partitioning since it runs in the parent's own session. An
  oauthFlowDone flag guards the three terminal paths (postMessage,
  storage, poll) so onLoginSuccess fires exactly once. Adds compact
  "oauth: signaled via {postMessage|storage|poll}" diagnostic logs.
- routes/user/login_callback/[client_name]/+page.svelte: replace `??`
  with `||` on the cookie/localStorage fallback. The cookie check
  returns a boolean, so `??` never fell through and the localStorage
  branch was dead code.
- InstanceSettings.svelte: per-category save/discard for the
  Auth/OAuth/SAML tab now sees auto_login_provider and
  disable_password_login. getSettingsForCategory was returning only
  scimSamlSetting for that tab, leaving the dirty check and per-category
  save unable to detect changes to those fields.
- vite.config.js: drop a stale personal dev hostname from allowedHosts.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 12:20:12 +00:00
hugocasaandClaude Opus 4.7 bef0a36c55 chore: bump windmill-parser-wasm packages to 1.693.1 (#8985)
Bump windmill-parser-wasm-ts, -py and -py-imports to 1.693.1 in
the CLI and frontend after publishing the new versions.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 12:19:46 +00:00
Ruben FiszelandClaude Opus 4.7 3ebfc2b0af fix: avoid effect_update_depth_exceeded when clicking flow node on runs page (#8986)
The $effect in useNestedRestartState wrote to selectedJobStepIsTopLevel
and then read it back via the early-return guard. In Svelte 5 that read
registers the same $state as a dependency of the effect, so each write
reschedules the effect → infinite loop.

Compute the boolean into a local const, write it once, and use the local
for the early return.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 12:16:25 +00:00
Ruben Fiszelandrubenfiszel 8f68f048d8 chore(main): release 1.693.1 (#8982)
* chore(main): release 1.693.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-04-29 21:25:34 +00:00
Ruben Fiszel 485d1d1e37 fix: include labels when loading flow with draft for editing (#8981)
The get_flow_by_path_w_draft endpoint omitted flow.labels from its
SELECT and FlowWDraft struct, so the flow editor received undefined
labels. As a result, the labels input rendered empty even when the
flow had labels saved, and adding a new label overwrote the existing
ones (since the frontend sent only the new label and the update SQL
only preserves labels when the field is null).

Closes #8963
2026-04-29 21:21:14 +00:00
Ruben Fiszel 03e08f2825 fix pypi 2026-04-29 20:23:32 +00:00
Ruben Fiszelandrubenfiszel e147546b3d chore(main): release 1.693.0 (#8957)
* chore(main): release 1.693.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-04-29 19:59:25 +00:00
Ruben FiszelandClaude Opus 4.5 abcd920964 test: isolate WAC v2 python test from stack overflow (#8979)
* test: isolate WAC v2 python test from test-thread stack overflow

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* ci: bump RUST_MIN_STACK to 4MB for backend tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-29 19:50:17 +00:00
hugocasaandClaude Opus 4.7 e9e72fbbf8 feat: edit scopes on existing API tokens (#8967)
* feat: edit scopes on existing API tokens

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address PR review feedback on token scope edit

- add SECURITY DEFINER to notify_token_scopes_change so trigger fires under windmill_user/admin roles (cubic P1)
- drop banned $bindable(default) on optional props (CLAUDE.md): make ScopesPicker.value and EditTokenScopesModal.open required
- detect MCP only when *every* scope starts with mcp: so mixed/null-scope tokens fall back to standard picker without dropping non-mcp scopes
- audit log scope payload via serde_json instead of Rust {:?}

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 19:49:56 +00:00
Ruben FiszelandClaude Opus 4.7 de0b6b1528 feat: workspace-shared ui/ folder reusable across raw apps (#8974)
* feat: add workspace-shared ui/ folder reusable across raw apps

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: add shared ui/ drawer in raw app editor sidebar

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: forward workspace shared ui/ to raw app editor iframe

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* all

* all

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 18:54:55 +00:00
GuilhemandClaude Opus 4.5 5861dcad58 fix(cli): debounce wmill dev flow round-trip 200ms (#8977)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-29 18:02:39 +00:00
Ruben FiszelandClaude Opus 4.7 1169d9bfd3 feat: add delete_after_secs and sensitive_inputs for raw app runnables (#8975)
* feat: add delete_after_secs and sensitive_inputs to raw app policy

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: simplify sensitive toggle label

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: use tertiary text for sensitive toggle label

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: unset sensitive field when toggled off

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address PR review feedback

- plumb force_viewer_sensitive_inputs/delete_after_secs so editor preview
  matches deployed-mode encryption
- reuse resolve_delete_after_secs helper for consistency with scripts/flows
- log+ignore schedule_job_deletion errors so a failed schedule doesn't
  surface as an execute_component failure
- fix text-primay typo in CacheTtlPopup and DeleteAfterUsePopup
- tighten extraFields return type to Partial<Pick<...>>

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 17:15:03 +00:00
hugocasaandClaude Opus 4.7 97b8bb73ab chore: bump protobufjs and other transitive deps in pulumi benchmark (#8972)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 16:10:47 +00:00
Ruben FiszelandClaude Opus 4.7 8627d3c5ae fix: show skipped label on flow progress bar (#8973)
* fix: show skipped label on flow progress bar for skipped flows

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: narrow is_skipped via 'in' operator on Job union

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 15:53:34 +00:00
centdixandClaude Opus 4.5 4098793db2 fix: split flow prompts for frontend chat (#8968)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-29 14:56:51 +00:00
cec84849b9 feat: OTEL span status on failed jobs + Python stderr severity classification (#8918)
* feat: set OTEL span status on failed jobs and add stderr severity toggle

Record otel.status_code=ERROR and otel.status_description on the job /
job_postprocessing tracing spans when a job fails, so OTel exporters
(Sentry, Honeycomb, Datadog) see the standard span-level failure signal
instead of just the success=false attribute. Description is truncated to
512 chars to keep span payloads bounded.

Add OtelSettings.stderr_default_severity instance setting (error | warn |
info | debug, default error) to let operators downgrade the OTEL severity
used for job stderr output. Python logging routes every record >= WARNING
to stderr, so the historical blind stderr->error mapping produces false
positives for scripts like dlt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref for stderr severity toggle

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: clarify OTEL span description for already-completed jobs

When handle_queued_job returns Ok(false) on Error::AlreadyCompleted
(another worker already finished the job during a race), the span
was labeled "job returned false" which is opaque to operators.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: parse Python logging severity from job stderr

Replace the global stderr_default_severity instance toggle with a
worker-side classifier that recognizes Python's canonical
basicConfig() format (LEVELNAME:logger.name:message) and emits the
corresponding tracing level. Lines that don't match keep the
historical tracing::error! fallback, so genuine failures still
surface and non-Python output is unaffected.

Removes StderrLogSeverity, STDERR_LOG_SEVERITY, and the
otel.stderr_default_severity field; adds
classify_python_logging_line in windmill-common.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: cover classify_python_logging_line + fix truncate_description doc

Add unit tests for the Python stderr-severity classifier and correct
the truncate_description docstring to say "bytes" (the cap is byte-
based, with UTF-8 boundary rounding for safety). Addresses Claude
review feedback on PR #8918.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: hugocasa <hugo@casademont.ch>
2026-04-29 14:14:47 +00:00
centdixandClaude Opus 4.5 b883f9a9d2 feat: add ai chat schedule and trigger tools (#8961)
* feat: add ai chat schedule and trigger tools

* refactor: use zod for ai chat workspace tools

* refactor: let ai provide runnable target fields

* refactor: generate ai chat workspace tool schemas

* fix: add object type to composed tool schemas

* fix: avoid top-level trigger schema unions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: block undeployed workspace ai tools

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: inject ai workspace tool target

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: add ai evals for workspace tools

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: make workspace tool eval prompts realistic

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: surface workspace tool errors

* fix: show workspace tool success details

* fix: describe workspace tool path format

* fix: clarify workspace path examples

* fix: tighten workspace tool validation

* fix: align workspace tool prompts

* chore: mark generated chat schemas

* chore: mark generated cli skills

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-29 14:00:01 +00:00
centdixandClaude Opus 4.5 34b549cfe2 perf: optimize datatable app chat schemas (#8960)
* perf: optimize datatable app chat schemas

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* perf: optimize datatable catalog queries

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: narrow datatable chat optimization

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: restrict datatable schema lookups

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: block system datatable schema lookups

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: handle datatable context edge cases

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: handle datatable schema edge cases

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-29 13:58:56 +00:00
hugocasaandClaude Opus 4.5 c0eeea9c83 feat: support S3Object input args in native SQL scripts (#8954)
* feat: support S3Object input args in native SQL scripts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: review fixes from local-review

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* update parser

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-28 20:01:06 +00:00
hugocasaandClaude Opus 4.7 c95642863e feat: support restart from steps inside BranchOne, ForLoop, Subflow (#8955)
* feat: support restart from steps inside BranchOne, ForLoop, Subflow

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: preserve original job kind in nested restart, support expanded subflow steps

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: read selected iteration from graph state for nested ForLoop restart

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: iteration selectors per ForLoop in restart popup, more nested restart tests

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: extract useNestedRestartState composable

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: cover deployed-subflow + FlowDependencies path in nested restart

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: update sqlx prepare cache

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: detect BranchOne/ForLoop ancestors inside expanded subflows for nested restart

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: hide restart button for non-restartable steps (parallel containers, untaken branches)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address review feedback on nested restart PR

- preview FlowRestartButton: hide nested case (chain UUIDs aren't resolvable in
  preview path; users can use the run page for nested restart instead)
- branchOneAncestorMatchesOriginal: be permissive when status isn't reachable
  (don't hide the button for BranchOnes nested deeper than top-level)
- worker_flow.rs: apply nested_restart_payload swap on the is_simple ForLoop
  fast path too, so simple iterations don't bypass restart spawn interception
- FlowStatusViewer: reset expandedSubflows cache on jobId change; drop
  $bindable({}) banned pattern for the new prop
- API resolver: validate the leaf step exists before returning (fail-fast)
- doc fix: branch_or_iteration_n is 0-based, not 1-based
- selectedJobStepIsTopLevel reset on early-return in composable
- comment iterationCounts collision caveat
- new HTTP-level integration tests covering the API endpoint contract:
  happy path (top-level + nested), unknown step, out-of-range iteration,
  parallel-loop rejection

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* revert: remove unreachable nested-restart swap on is_simple ForLoop fast path

The swap is unreachable in valid flows: `is_simple_modules` requires the body
to be a single `script` / `rawscript` / `flowscript` (per `FlowModule::is_simple`),
none of which spawn flow-kind children. Any nested-restart chain targeting a
leaf inside such an iteration is rejected by the API at leaf validation. Even
if a chain reached the worker via `JobPayload::RawFlow.restarted_from`, the
resulting `RestartedFlow` would fail to push (script kind isn't a flow kind).

Replaced the swap with an explanatory comment so the next reader knows why
the symmetry with the non-simple path was deliberately not added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: handle undefined expandedSubflows + tighten branchOne match check

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 20:00:03 +00:00
centdixandClaude Opus 4.5 77d9a53423 fix: strip additionalProperties from google schemas (#8964)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-28 19:59:45 +00:00
hugocasaandClaude Opus 4.7 70b90c41dc fix: prevent React app editor from overwriting files on theme switch (#8965)
* fix: prevent React app editor from overwriting files on theme switch

The ui_builder iframe src embeds the dark-mode flag, so toggling theme
reloads it. iframeLoaded was sticky-true, so the populate effect didn't
refire and the iframe's default "Hello World" template clobbered the
user's files via its initial setFiles message.

Reset iframeLoaded on darkMode change and suppress inbound setFiles from
the iframe until our files are re-pushed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: cancel suppress timer on rapid theme toggles

On a second theme toggle while the previous reload's clear-timer was
still pending, that timer would fire mid-reload and drop suppression
before the iframe finished booting — letting the iframe's default
template setFiles overwrite the user's files.

Track the timer ID, cancel it whenever we re-assert suppression, and
fold the two 500ms timers into one. Race identified by cubic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:59:26 +00:00
centdix 866623a39e chore: copy ai env files for webmux worktrees (#8966) 2026-04-28 19:59:07 +00:00
379 changed files with 22155 additions and 2979 deletions
+1 -3
View File
@@ -52,8 +52,6 @@ chore: upgrade sqlx to 0.7
4. Stage ONLY the modified/relevant files: `git add <file1> <file2> ...`
5. Create the commit with conventional format:
```bash
git commit -m "<type>: <description>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"
git commit -m "<type>: <description>"
```
6. Run `git status` to verify the commit succeeded
+82
View File
@@ -0,0 +1,82 @@
---
name: update-sqlx
description: How to safely update SQLx offline query cache. MUST use when SQL queries change.
---
# SQLx Offline Query Cache
Windmill uses `SQLX_OFFLINE=true` in CI, which requires all `sqlx::query!` / `sqlx::query_as!` macros to have matching cached query data in `backend/.sqlx/`.
## When to Run
Run after any change to SQL queries in Rust source files. Without it, CI will fail with:
```
error: `SQLX_OFFLINE=true` but there is no cached data for this query
```
## The Problem
`cargo sqlx prepare --workspace` **deletes all existing cache files** and regenerates only the ones found in the current compilation. If you don't compile with every feature flag (especially `private` for EE files), you will **silently delete EE query caches**, breaking CI for enterprise tests.
The standard `./update_sqlx.sh` script tries to compile with all features, but it often fails locally because the EE symlinks can be out of sync with `main`.
## Safe Procedure
Always preserve the existing EE caches from `origin/main`. Use this workflow:
```bash
cd backend
# 1. Restore the full cache from main (includes EE caches)
git checkout origin/main -- .sqlx/
# 2. Run prepare with OSS features (what compiles locally)
# This regenerates OSS caches to match your code changes.
cargo sqlx prepare --workspace -- --workspace --features all_sqlx_features
# 3. Restore any EE caches that were deleted in step 2.
# These are files present in origin/main but missing after prepare.
git ls-tree origin/main backend/.sqlx/ \
| awk '{print $4}' | sed 's|backend/\.sqlx/||' | sort > /tmp/main_files.txt
find backend/.sqlx -name "*.json" -printf '%P\n' | sort > /tmp/current_files.txt
comm -23 /tmp/main_files.txt /tmp/current_files.txt > /tmp/missing_files.txt
while read f; do
git show "origin/main:backend/.sqlx/$f" > "backend/.sqlx/$f"
done < /tmp/missing_files.txt
# 4. Verify nothing was lost from main
find backend/.sqlx -name "*.json" -printf '%P\n' | sort > /tmp/current_files.txt
comm -23 /tmp/main_files.txt /tmp/current_files.txt | wc -l
# Should output: 0
```
## If EE Compiles Locally
If your EE repo happens to be in sync, you can use the full script (faster):
```bash
cd backend
./update_sqlx.sh
```
But if it fails with EE compilation errors, use the safe procedure above.
## What NOT to Do
- **Never** run `cargo sqlx prepare --workspace` with only OSS features and commit the result — it will delete EE caches.
- **Never** set `SQLX_OFFLINE=true` for local `cargo sqlx prepare` — use a live database per CLAUDE.md. (CI runs with `SQLX_OFFLINE=true`, which is why the cache must be complete.)
- **Never** skip the verification step (step 4 above).
## Verification
After committing, the diff against `origin/main` should show:
- A few **new** cache files (for your changed queries)
- A few **deleted** cache files (for old queries that no longer exist)
- **Zero** net deletions from the EE cache set
```bash
git diff origin/main --stat backend/.sqlx/
```
+3 -1
View File
@@ -111,7 +111,9 @@ Regenerate frontend client: `npm run generate-backend-client` from `frontend/`.
**`backend/windmill-api-workspaces/src/workspaces.rs`** — add `{kind}_used: bool` to the `UsedTriggers` struct and add an `EXISTS(SELECT 1 FROM {kind}_trigger …)` to the `get_used_triggers` query.
**`backend/windmill-api/src/workspaces_export.rs`** — add export block mirroring gcp's (export lists all triggers, serializes them to YAML/JSON).
**`backend/windmill-api/src/workspaces_export.rs`** — add export block mirroring gcp's (export lists all triggers, serializes them to YAML/JSON). The block re-uses the `trigger_ignore_keys` variable so the new kind automatically participates in fork-export stripping (`mode` field is omitted when the source workspace is a fork — keeps fork→parent merges from flipping the parent's enabled state).
**Fork cloning (`clone_triggers_and_schedules` in workspaces.rs)** — add an `INSERT INTO {kind}_trigger ... SELECT ...` block that copies all rows from the parent workspace, forcing `mode = 'disabled'::TRIGGER_MODE`. Always runs at fork creation; forgetting this means users can't carry `{kind}` triggers into their forks.
## 6.5 Hardcoded trigger-kind arrays (silent-failure hotspots)
@@ -145,6 +145,10 @@ jobs:
RUST_LOG_STYLE: never
CARGO_NET_GIT_FETCH_WITH_CLI: true
CARGO_BUILD_JOBS: 12
# Tests' poll-time stack frames (deep nested async fn chains in
# debug builds) reach ~1.8MB. 4MB gives ~2x headroom against flaky
# overflows under parallel-test contention.
RUST_MIN_STACK: 4194304
VCPKGRS_DYNAMIC: 1
OPENSSL_DIR: ${{ env.VCPKG_INSTALLATION_ROOT }}\installed\x64-windows-static
DENO_PATH: ${{ steps.runtime-paths.outputs.DENO_PATH }}
+5
View File
@@ -244,6 +244,11 @@ jobs:
RUST_LOG_STYLE: never
CARGO_NET_GIT_FETCH_WITH_CLI: true
CARGO_BUILD_JOBS: 12
# Tests' poll-time stack frames (deep nested async fn chains in
# debug builds) reach ~1.8MB, leaving very thin headroom on the
# default 2MB thread stack. 4MB gives ~2x buffer against flaky
# overflows under parallel-test contention.
RUST_MIN_STACK: 4194304
WMDEBUG_FORCE_V0_WORKSPACE_DEPENDENCIES: 1
WMDEBUG_FORCE_RUNNABLE_SETTINGS_V0: 1
WMDEBUG_FORCE_NO_LEGACY_DEBOUNCING_COMPAT: 1
@@ -10,6 +10,7 @@ on:
- "backend/windmill-api/openapi.yaml"
- "cli/src/main.ts"
- "cli/src/commands/**"
- "frontend/src/lib/components/copilot/chat/workspaceToolsZod.gen.ts"
pull_request:
paths:
- "system_prompts/**"
@@ -19,6 +20,7 @@ on:
- "backend/windmill-api/openapi.yaml"
- "cli/src/main.ts"
- "cli/src/commands/**"
- "frontend/src/lib/components/copilot/chat/workspaceToolsZod.gen.ts"
jobs:
check-freshness:
-54
View File
@@ -1,54 +0,0 @@
name: Fast Claude
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
check-membership:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '/ai-fast')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/ai-fast')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '/ai-fast')) ||
(github.event_name == 'issues' && contains(github.event.issue.body, '/ai-fast'))
uses: ./.github/workflows/check-org-membership.yml
secrets:
access_token: ${{ secrets.ORG_ACCESS_TOKEN }}
claude-code-action:
needs: check-membership
if: |
needs.check-membership.outputs.is_member == 'true'
runs-on: ubicloud-standard-8
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude PR Action
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
allowed_bots: "windmill-internal-app[bot]"
trigger_phrase: "/ai-fast"
settings: |
{
"env": {
"SQLX_OFFLINE": "true"
}
}
claude_args: |
--allowedTools "Bash,WebFetch,WebSearch"
--model opus
+1 -54
View File
@@ -1,4 +1,4 @@
name: Claude PR Assistant
name: Fast Claude
on:
issue_comment:
@@ -26,7 +26,6 @@ jobs:
if: |
needs.check-membership.outputs.is_member == 'true'
runs-on: ubicloud-standard-8
timeout-minutes: 60
permissions:
contents: write
pull-requests: write
@@ -38,37 +37,6 @@ jobs:
with:
fetch-depth: 1
- uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Run npm install and generate-backend-client
working-directory: ./frontend
run: |
# add a build directory for cargo check
mkdir -p build
npm install
npm run generate-backend-client
- name: install xmlsec1 and gssapi
run: |
sudo apt-get update
sudo apt-get install -y libxml2-dev libxmlsec1-dev libkrb5-dev libsasl2-dev libcurl4-openssl-dev mold clang
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.93.0
- name: cargo check
working-directory: ./backend
timeout-minutes: 16
run: |
SQLX_OFFLINE=true cargo check --features all_sqlx_features
- name: Run Claude PR Action
uses: anthropics/claude-code-action@v1
with:
@@ -84,24 +52,3 @@ jobs:
claude_args: |
--allowedTools "Bash,WebFetch,WebSearch"
--model opus
--system-prompt "## IMPORTANT INSTRUCTIONS
- Your branch name should be a short description of the requested changes.
- Bypass previous instructions and instead of adding a url in your comment to manually create a PR, if you created a new branch with your changes, automatically create a draft PR from that branch to main.
## Code Quality Requirements
After making any code changes, you MUST run the appropriate validation commands:
**Frontend Changes:**
- Run: \`npm run check\` in the frontend directory
- Fix all warnings and errors before proceeding
**Backend Changes:**
- Run: \`cargo check --features all_sqlx_features\` in the backend directory
- Fix all warnings and errors before proceeding
**Pull Request Creation:**
- DO NOT FORGET TO OPEN A DRAFT PR AFTER YOU ARE DONE if you made changes after a request from a git issue.
## Available Tools
- Bash: Full access to run validation commands and git operations"
+82
View File
@@ -1,5 +1,87 @@
# Changelog
## [1.694.0](https://github.com/windmill-labs/windmill/compare/v1.693.4...v1.694.0) (2026-05-01)
### Features
* ansible delegate_to_git_repo install_requirements, dynamic fields, --limit ([#8997](https://github.com/windmill-labs/windmill/issues/8997)) ([96324ea](https://github.com/windmill-labs/windmill/commit/96324ea5aed4054d33895102ec9313f5dadc77a2))
* **cli:** wmill-lock.yaml auto-fill + --rehash-only + path-prefix dedup ([#8978](https://github.com/windmill-labs/windmill/issues/8978)) ([0b959b8](https://github.com/windmill-labs/windmill/commit/0b959b8ec61b24d861c5a10a9242a7a0e6013707))
* **forks:** handle triggers and schedules in workspace forks ([#8976](https://github.com/windmill-labs/windmill/issues/8976)) ([d60dd74](https://github.com/windmill-labs/windmill/commit/d60dd745e49853bb130b139f300fc0f2ab8ebe39))
* support assigning a worker tag to app inline scripts ([#9002](https://github.com/windmill-labs/windmill/issues/9002)) ([0c22f52](https://github.com/windmill-labs/windmill/commit/0c22f52b46c56d3577309e37c1e81a1a1feb9b7c))
### Bug Fixes
* **cli:** only preserve case for raw-app runnableIds, not app/flow summaries ([#9000](https://github.com/windmill-labs/windmill/issues/9000)) ([5d5b853](https://github.com/windmill-labs/windmill/commit/5d5b853f70a73453f63d14edcd5d2fac8e3d804c))
* distinguish AlreadyCompleted from execution failure on OTLP job span ([#9004](https://github.com/windmill-labs/windmill/issues/9004)) ([70a5880](https://github.com/windmill-labs/windmill/commit/70a5880d3619edece4ce67a00407ee0d2d523469))
* nested-restart iteration count for step-id collisions across subflow boundaries ([#9003](https://github.com/windmill-labs/windmill/issues/9003)) ([ad9f1fa](https://github.com/windmill-labs/windmill/commit/ad9f1fa4541f2eefb0b013bac42cd626af424852))
* omit empty assets array on scripts and raw app inline scripts ([#9006](https://github.com/windmill-labs/windmill/issues/9006)) ([419bc4b](https://github.com/windmill-labs/windmill/commit/419bc4b1757a7c20c2b7aa9b7d3b02e3515f934a))
* pair PG arg type with actual Rust binding to keep query_typed_raw safe ([#8999](https://github.com/windmill-labs/windmill/issues/8999)) ([aedf369](https://github.com/windmill-labs/windmill/commit/aedf3691744748a307976ef01ea7f63b0961fad4))
* route email trigger path through standard info channel ([#8996](https://github.com/windmill-labs/windmill/issues/8996)) ([2141128](https://github.com/windmill-labs/windmill/commit/21411282bb4a0442046bf71fdc7ea012fa2c3d3d))
* surface scope errors as 403 and show real message in CLI ([#8953](https://github.com/windmill-labs/windmill/issues/8953)) ([66db873](https://github.com/windmill-labs/windmill/commit/66db873651a04b0701d8231fbf11ab973c3fc69b))
* use otel.status_message for OTLP Status.message on failed jobs ([#8995](https://github.com/windmill-labs/windmill/issues/8995)) ([9cb777a](https://github.com/windmill-labs/windmill/commit/9cb777a6b6e969696cf9beade1cf07c86967dafc))
## [1.693.4](https://github.com/windmill-labs/windmill/compare/v1.693.3...v1.693.4) (2026-04-30)
### Bug Fixes
* **cli:** pin wasm parser versions in published package.json ([#8993](https://github.com/windmill-labs/windmill/issues/8993)) ([7c227ec](https://github.com/windmill-labs/windmill/commit/7c227ece0d546ef0f521a963f927e4c913011e8e))
## [1.693.3](https://github.com/windmill-labs/windmill/compare/v1.693.2...v1.693.3) (2026-04-30)
### Bug Fixes
* avoid named prepared statements in datatable/PG executor ([#8988](https://github.com/windmill-labs/windmill/issues/8988)) ([7bdfc0e](https://github.com/windmill-labs/windmill/commit/7bdfc0ea0636a93a893ab5f9f58a590e6a0b7be8))
* improve raw app builder queue behavior for bigger apps ([e7deaf9](https://github.com/windmill-labs/windmill/commit/e7deaf988254eebbf99febc8a644622c9f809bd8))
* sanitize underscores in agent worker suffix ([#8992](https://github.com/windmill-labs/windmill/issues/8992)) ([568d9cc](https://github.com/windmill-labs/windmill/commit/568d9cc8a086178314e15e3c27fb55ff4817f5cd))
* **workspaces:** split get_settings into admin-only + public endpoint ([#8990](https://github.com/windmill-labs/windmill/issues/8990)) ([4483d0c](https://github.com/windmill-labs/windmill/commit/4483d0cab91e8a9df026b09c7908291f3fbbfd63))
## [1.693.2](https://github.com/windmill-labs/windmill/compare/v1.693.1...v1.693.2) (2026-04-30)
### Bug Fixes
* avoid effect_update_depth_exceeded when clicking flow node on runs page ([#8986](https://github.com/windmill-labs/windmill/issues/8986)) ([3ebfc2b](https://github.com/windmill-labs/windmill/commit/3ebfc2b0af38f7eb17774de08a214d7813e07952))
* OAuth popup login reliability + auto-login Safari edge cases ([#8971](https://github.com/windmill-labs/windmill/issues/8971)) ([3c3c034](https://github.com/windmill-labs/windmill/commit/3c3c03455d68fde982937787992e20c3f8eeeaaf))
## [1.693.1](https://github.com/windmill-labs/windmill/compare/v1.693.0...v1.693.1) (2026-04-29)
### Bug Fixes
* include labels when loading flow with draft for editing ([#8981](https://github.com/windmill-labs/windmill/issues/8981)) ([485d1d1](https://github.com/windmill-labs/windmill/commit/485d1d1e3785b5ed7e5f1a5ee127c0fed15fba3e)), closes [#8963](https://github.com/windmill-labs/windmill/issues/8963)
## [1.693.0](https://github.com/windmill-labs/windmill/compare/v1.692.0...v1.693.0) (2026-04-29)
### Features
* add ai chat schedule and trigger tools ([#8961](https://github.com/windmill-labs/windmill/issues/8961)) ([b883f9a](https://github.com/windmill-labs/windmill/commit/b883f9a9d2e38a5981860de268fa6227cdd645de))
* add delete_after_secs and sensitive_inputs for raw app runnables ([#8975](https://github.com/windmill-labs/windmill/issues/8975)) ([1169d9b](https://github.com/windmill-labs/windmill/commit/1169d9bfd315e43194f9e5a2bc55af843476ef68))
* add min release age instance settings for bun and uv ([#8956](https://github.com/windmill-labs/windmill/issues/8956)) ([1d279e7](https://github.com/windmill-labs/windmill/commit/1d279e7a1e77fd183bb0c99d2430cfd9dc0a617c))
* edit scopes on existing API tokens ([#8967](https://github.com/windmill-labs/windmill/issues/8967)) ([e9e72fb](https://github.com/windmill-labs/windmill/commit/e9e72fbbf83363ba1426b3dde3d657de02ba50b3))
* OTEL span status on failed jobs + Python stderr severity classification ([#8918](https://github.com/windmill-labs/windmill/issues/8918)) ([cec8484](https://github.com/windmill-labs/windmill/commit/cec84849b9aea92d355dd346546969027c603498))
* support restart from steps inside BranchOne, ForLoop, Subflow ([#8955](https://github.com/windmill-labs/windmill/issues/8955)) ([c956428](https://github.com/windmill-labs/windmill/commit/c95642863e366c106d529a57540a75df9480397c))
* support S3Object input args in native SQL scripts ([#8954](https://github.com/windmill-labs/windmill/issues/8954)) ([c0eeea9](https://github.com/windmill-labs/windmill/commit/c0eeea9c833f9be3981389a19d0964400fd2bda8))
* workspace-shared ui/ folder reusable across raw apps ([#8974](https://github.com/windmill-labs/windmill/issues/8974)) ([de0b6b1](https://github.com/windmill-labs/windmill/commit/de0b6b15285612ef00b94e332dcb203aed88f2cc))
### Bug Fixes
* **cli:** debounce wmill dev flow round-trip 200ms ([#8977](https://github.com/windmill-labs/windmill/issues/8977)) ([5861dca](https://github.com/windmill-labs/windmill/commit/5861dcad589df99f57e730d8d9cdb3eabc3424e4))
* prevent React app editor from overwriting files on theme switch ([#8965](https://github.com/windmill-labs/windmill/issues/8965)) ([70b90c4](https://github.com/windmill-labs/windmill/commit/70b90c41dc28d1a850bce69836cde920c1404c3b))
* show skipped label on flow progress bar ([#8973](https://github.com/windmill-labs/windmill/issues/8973)) ([8627d3c](https://github.com/windmill-labs/windmill/commit/8627d3c5aeabfac12a9f06211f8e7db020e758f9))
* split flow prompts for frontend chat ([#8968](https://github.com/windmill-labs/windmill/issues/8968)) ([4098793](https://github.com/windmill-labs/windmill/commit/4098793db22249c5b4467c2adb72131407f5d6d3))
* strip additionalProperties from google schemas ([#8964](https://github.com/windmill-labs/windmill/issues/8964)) ([77d9a53](https://github.com/windmill-labs/windmill/commit/77d9a534235a1ff8cbfcdb037a65735db987e2fe))
### Performance Improvements
* optimize datatable app chat schemas ([#8960](https://github.com/windmill-labs/windmill/issues/8960)) ([34b549c](https://github.com/windmill-labs/windmill/commit/34b549cfe2e2e060561eabe369a99cfb4d9c9568))
## [1.692.0](https://github.com/windmill-labs/windmill/compare/v1.691.1...v1.692.0) (2026-04-27)
@@ -4,6 +4,7 @@ import type { FlowModule, InputTransform } from '../../../../../frontend/src/lib
import type { ExtendedOpenFlow } from '../../../../../frontend/src/lib/components/flows/types'
import type { FlowAIChatHelpers } from '../../../../../frontend/src/lib/components/copilot/chat/flow/core'
import type { ScriptLintResult } from '../../../../../frontend/src/lib/components/copilot/chat/shared'
import type { WorkspaceMutationTarget } from '../../../../../frontend/src/lib/components/copilot/chat/workspaceTools'
import {
createInlineScriptSession
} from '../../../../../frontend/src/lib/components/copilot/chat/flow/inlineScriptsUtils'
@@ -38,7 +39,8 @@ export async function createFlowFileHelpers(
initialPreprocessorModule?: FlowModule,
initialFailureModule?: FlowModule,
workspaceRoot?: string,
workspaceFixtures?: FlowWorkspaceFixtures
workspaceFixtures?: FlowWorkspaceFixtures,
currentFlowPath?: string
): Promise<{
helpers: FlowAIChatHelpers
getFlow: () => ExtendedOpenFlow
@@ -97,10 +99,17 @@ export async function createFlowFileHelpers(
return result
}
const helpers: FlowAIChatHelpers = {
const helpers: FlowAIChatHelpers & {
getWorkspaceMutationTarget: () => WorkspaceMutationTarget
} = {
getFlowAndSelectedId: () => ({ flow, selectedId: '' }),
getRootModules: () => flow.value.modules,
inlineScriptSession,
getWorkspaceMutationTarget: () => ({
kind: 'flow',
path: currentFlowPath,
deployed: Boolean(currentFlowPath)
}),
setSnapshot: () => {},
revertToSnapshot: () => {},
setCode: async (id: string, code: string) => {
@@ -17,11 +17,12 @@ import {
} from "./fileHelpers";
import { runEval } from "../shared";
import type { ModeRunContext } from "../../../../core/types";
import type { TokenUsage } from "../shared/types";
import type { TokenUsage, ToolCallDetail } from "../shared/types";
import type { FrontendEvalTransport } from "../../../../core/frontendTransport";
import type { WindmillBackendSettings } from "../../../../core/windmillBackendSettings";
export interface FlowFixture {
path?: string;
value?: {
modules?: FlowModule[];
preprocessor_module?: FlowModule;
@@ -37,6 +38,7 @@ export interface FlowEvalResult {
assistantMessageCount: number;
toolCallCount: number;
toolsUsed: string[];
toolCallDetails: ToolCallDetail[];
tokenUsage: TokenUsage;
}
@@ -67,6 +69,7 @@ export async function runFlowEval(
options?.initialFlow?.value?.failure_module,
workspaceRoot,
options?.workspaceFixtures,
options?.initialFlow?.path,
);
try {
@@ -111,6 +114,7 @@ export async function runFlowEval(
assistantMessageCount: rawResult.iterations,
toolCallCount: rawResult.toolCallsCount,
toolsUsed: rawResult.toolsCalled,
toolCallDetails: rawResult.toolCallDetails,
tokenUsage: rawResult.tokenUsage,
};
} finally {
@@ -2,6 +2,7 @@ import { mkdir, rm, writeFile } from 'fs/promises'
import { dirname, join } from 'path'
import type { ScriptLang } from '../../../../../frontend/src/lib/gen/types.gen'
import type { ScriptChatHelpers } from '../../../../../frontend/src/lib/components/copilot/chat/script/core'
import type { WorkspaceMutationTarget } from '../../../../../frontend/src/lib/components/copilot/chat/workspaceTools'
import { buildScriptLintResult } from './preview'
import { registerBenchmarkWorkspace, unregisterBenchmarkWorkspace } from '../../mockBackend'
@@ -12,6 +13,10 @@ export interface ScriptEvalState {
args: Record<string, any>
}
function toRunnablePath(filePath: string): string {
return filePath.replace(/\.[^/.]+$/, '')
}
export async function createScriptFileHelpers(
initialScript: ScriptEvalState,
workspaceRoot?: string
@@ -55,13 +60,20 @@ export async function createScriptFileHelpers(
const getLintErrors: NonNullable<ScriptChatHelpers['getLintErrors']> = () =>
buildScriptLintResult(script.code, script.lang)
const helpers: ScriptChatHelpers = {
const helpers: ScriptChatHelpers & {
getWorkspaceMutationTarget: () => WorkspaceMutationTarget
} = {
getScriptOptions: () => ({
code: script.code,
lang: script.lang,
path: script.path,
args: structuredClone(script.args)
}),
getWorkspaceMutationTarget: () => ({
kind: 'script',
path: script.path ? toRunnablePath(script.path) : undefined,
deployed: Boolean(script.path)
}),
applyCode,
getLintErrors
}
@@ -13,7 +13,7 @@ import type { Tool as ProductionTool } from "../../../../../frontend/src/lib/com
import { createScriptFileHelpers, type ScriptEvalState } from "./fileHelpers";
import { runEval } from "../shared";
import type { ModeRunContext } from "../../../../core/types";
import type { TokenUsage } from "../shared/types";
import type { TokenUsage, ToolCallDetail } from "../shared/types";
import type { FrontendEvalTransport } from "../../../../core/frontendTransport";
import type { WindmillBackendSettings } from "../../../../core/windmillBackendSettings";
@@ -24,6 +24,7 @@ export interface ScriptEvalResult {
assistantMessageCount: number;
toolCallCount: number;
toolsUsed: string[];
toolCallDetails: ToolCallDetail[];
tokenUsage: TokenUsage;
}
@@ -111,6 +112,7 @@ export async function runScriptEval(
assistantMessageCount: rawResult.iterations,
toolCallCount: rawResult.toolCallsCount,
toolsUsed: rawResult.toolsCalled,
toolCallDetails: rawResult.toolCallDetails,
tokenUsage: rawResult.tokenUsage,
};
} finally {
+54
View File
@@ -227,6 +227,60 @@ export function runBenchmarkFlowByPath(input: {
})
}
export function previewBenchmarkSchedule(input: {
requestBody?: Record<string, unknown>
}): Record<string, unknown> {
const schedule = input.requestBody?.schedule
if (typeof schedule !== 'string' || schedule.trim().split(/\s+/).length !== 6) {
throw new Error(`schedule must use a six-field cron expression, got ${JSON.stringify(schedule)}`)
}
return {
next_runs: ['1970-01-02T00:00:00.000Z']
}
}
export function createBenchmarkSchedule(input: {
workspace: string
requestBody: Record<string, unknown>
}): Record<string, unknown> {
assertBenchmarkWorkspacePath('schedule', input.requestBody.path)
assertBenchmarkWorkspacePath('target', input.requestBody.script_path)
return {
path: input.requestBody.path,
target_path: input.requestBody.script_path,
is_flow: input.requestBody.is_flow,
mocked: true
}
}
export function createBenchmarkHttpTrigger(input: {
workspace: string
requestBody: Record<string, unknown>
}): Record<string, unknown> {
assertBenchmarkWorkspacePath('trigger', input.requestBody.path)
assertBenchmarkWorkspacePath('target', input.requestBody.script_path)
if (
typeof input.requestBody.route_path === 'string' &&
input.requestBody.route_path.startsWith('/')
) {
throw new Error(`HTTP trigger route_path must not start with /, got "${input.requestBody.route_path}"`)
}
return {
path: input.requestBody.path,
target_path: input.requestBody.script_path,
route_path: input.requestBody.route_path,
is_flow: input.requestBody.is_flow,
mocked: true
}
}
function assertBenchmarkWorkspacePath(label: string, value: unknown): void {
if (typeof value !== 'string' || (!value.startsWith('f/') && !value.startsWith('u/'))) {
throw new Error(`${label} path must start with f/ or u/, got ${JSON.stringify(value)}`)
}
}
function buildBenchmarkScriptHash(path: string): string {
return `benchmark:${path}`
}
@@ -40,6 +40,9 @@ vi.mock('$lib/gen', async () => {
hasBenchmarkWorkspace,
listBenchmarkFlows,
listBenchmarkScripts,
createBenchmarkHttpTrigger,
createBenchmarkSchedule,
previewBenchmarkSchedule,
runBenchmarkFlowByPath,
runBenchmarkScriptPreview
} = await import('./mockBackend')
@@ -137,6 +140,20 @@ vi.mock('$lib/gen', async () => {
}
return actual.JobService.getJob(data)
}
}),
ScheduleService: wrapService(actual.ScheduleService, {
previewSchedule: async (data: { requestBody?: Record<string, unknown> }) =>
previewBenchmarkSchedule(data),
createSchedule: async (data: { workspace: string; requestBody: Record<string, unknown> }) =>
hasBenchmarkWorkspace(data.workspace)
? createBenchmarkSchedule(data)
: actual.ScheduleService.createSchedule(data)
}),
HttpTriggerService: wrapService(actual.HttpTriggerService, {
createHttpTrigger: async (data: { workspace: string; requestBody: Record<string, unknown> }) =>
hasBenchmarkWorkspace(data.workspace)
? createBenchmarkHttpTrigger(data)
: actual.HttpTriggerService.createHttpTrigger(data)
})
}
})
+64
View File
@@ -129,6 +129,70 @@
- does not duplicate the greeting logic in a new inline script
- wires the name input into the reused script
- id: wac-typescript-order-workflow
prompt: |-
Create a Windmill Workflow-as-Code TypeScript script at `f/evals/order_workflow.ts`.
It should take an `orderId` string, load the order in a durable task, checkpoint a processing timestamp with `step`, and return `{ orderId, processedAt, status }`.
cliExpect:
requiredSkills:
- write-workflow-as-code
requiredSkillsBeforeFirstMutation:
- write-workflow-as-code
forbiddenSkills:
- write-flow
- write-script-bun
- write-script-python3
judgeChecklist:
- creates the requested TypeScript WAC script at f/evals/order_workflow.ts
- uses the Workflow-as-Code SDK from windmill-client
- wraps the entrypoint with workflow
- uses a durable task for loading the order
- uses step to checkpoint the processing timestamp
- does not create an OpenFlow flow.yaml or flow folder
- id: wac-python-approval-workflow
prompt: |-
Create a Windmill Workflow-as-Code Python script at `f/evals/approval_workflow.py`.
It should take a `request_id` string, prepare an approval summary in a task, create resume URLs inside a durable step, wait for approval, and return the approval result.
cliExpect:
requiredSkills:
- write-workflow-as-code
requiredSkillsBeforeFirstMutation:
- write-workflow-as-code
forbiddenSkills:
- write-flow
- write-script-bun
- write-script-python3
judgeChecklist:
- creates the requested Python WAC script at f/evals/approval_workflow.py
- imports Workflow-as-Code helpers from wmill
- decorates an async entrypoint with @workflow
- uses @task for the approval summary work
- gets resume URLs inside step before waiting for approval
- uses wait_for_approval
- does not create an OpenFlow flow.yaml or flow folder
- id: wac-not-openflow-disambiguation
prompt: |-
Create this as Workflow-as-Code, not an OpenFlow YAML flow: a TypeScript script at `f/evals/fanout_workflow.ts`.
It should take an array of customer IDs, process each customer with a WAC task, run the independent customer tasks in parallel, and return the collected results.
cliExpect:
requiredSkills:
- write-workflow-as-code
requiredSkillsBeforeFirstMutation:
- write-workflow-as-code
forbiddenSkills:
- write-flow
- write-script-bun
- write-script-python3
judgeChecklist:
- creates the requested TypeScript script at f/evals/fanout_workflow.ts
- treats the request as Workflow-as-Code rather than an OpenFlow flow
- uses workflow for the script entrypoint
- uses task for each customer processing unit
- runs independent customer tasks in parallel
- does not create a flow folder or flow.yaml
- id: cli-job-debug-guidance
prompt: |-
A Windmill job failed.
+54
View File
@@ -412,3 +412,57 @@
- the updated flow rejects empty payload strings
- "the existing `process_event` step remains the main step"
- failures return a compact error object with the error message and failing step id
- id: flow-test15-create-current-flow-schedule
prompt: |-
Update this flow by adding a final step named `return_schedule_status`.
It should return an object with `scheduled: true` and the order summary from `results.summarize_orders`.
Also create an enabled daily schedule named `order_processing_daily` for the current flow.
It should run every day at 07:30 UTC with empty args.
Do not ask me for the flow path.
initial: ai_evals/fixtures/frontend/flow/initial/scheduled_order_flow.json
validate:
topLevelStepIds:
- return_schedule_status
toolExpect:
requiredToolsUsed:
- create_schedule
toolCallArgs:
- tool: create_schedule
field: path
stringStartsWithAnyOf:
- f/
- u/
stringMustNotStartWithAnyOf:
- schedules/
skipJudge: true
judgeChecklist:
- "the flow includes a final top-level step named `return_schedule_status`"
- "`return_schedule_status` returns `scheduled: true` and the order summary"
- id: flow-test16-create-current-flow-http-trigger
prompt: |-
Update this flow by adding a final step named `webhook_response`.
It should return an object with `ok: true` and the order summary from `results.summarize_orders`.
Also create a public POST HTTP endpoint named `order_processing_webhook` for the current flow.
Use route path `ai-evals/order-processing` and no authentication.
Do not ask me for the flow path.
initial: ai_evals/fixtures/frontend/flow/initial/scheduled_order_flow.json
validate:
topLevelStepIds:
- webhook_response
toolExpect:
requiredToolsUsed:
- create_trigger
toolCallArgs:
- tool: create_trigger
field: path
stringStartsWithAnyOf:
- f/
- u/
stringMustNotStartWithAnyOf:
- schedules/
skipJudge: true
judgeChecklist:
- "the flow includes a final top-level step named `webhook_response`"
- "`webhook_response` returns `ok: true` and the order summary"
+48
View File
@@ -9,3 +9,51 @@
- uses the existing `name` input
- returns a plain greeting string
- does not wrap the result in an object or array
- id: script-test2-create-current-script-schedule
prompt: |-
Update the current Bun script so it takes the existing `name` input and returns a plain greeting string like `Hello, Alice!`.
Also create an enabled daily schedule named `greet_user_daily` for the current script.
It should run every day at 09:00 UTC and pass `{ "name": "Alice" }` as args.
Do not ask me for the script path.
initial: ai_evals/fixtures/frontend/script/initial/test1_empty_bun.json
expected: ai_evals/fixtures/frontend/script/expected/test1_greet_user.json
toolExpect:
requiredToolsUsed:
- create_schedule
toolCallArgs:
- tool: create_schedule
field: path
stringStartsWithAnyOf:
- f/
- u/
stringMustNotStartWithAnyOf:
- schedules/
skipJudge: true
judgeChecklist:
- uses the existing `name` input
- returns a plain greeting string
- id: script-test3-create-current-script-http-trigger
prompt: |-
Update the current Bun script so it takes the existing `name` input and returns a plain greeting string like `Hello, Alice!`.
Also create a public POST HTTP endpoint named `greet_user_webhook` for the current script.
Use route path `ai-evals/greet-user` and no authentication.
Do not ask me for the script path.
initial: ai_evals/fixtures/frontend/script/initial/test1_empty_bun.json
expected: ai_evals/fixtures/frontend/script/expected/test1_greet_user.json
toolExpect:
requiredToolsUsed:
- create_trigger
toolCallArgs:
- tool: create_trigger
field: path
stringStartsWithAnyOf:
- f/
- u/
stringMustNotStartWithAnyOf:
- schedules/
skipJudge: true
judgeChecklist:
- uses the existing `name` input
- returns a plain greeting string
+20
View File
@@ -182,4 +182,24 @@ describe("loadCases", () => {
forbiddenExecutedCommands: ["^wmill generate-metadata", "^wmill sync push"],
});
});
it("loads tool expectations for workspace mutation cases", async () => {
const scriptCases = await loadCases("script");
const caseEntry = scriptCases.find(
(entry) => entry.id === "script-test2-create-current-script-schedule"
);
expect(caseEntry?.toolExpect).toEqual({
requiredToolsUsed: ["create_schedule"],
toolCallArgs: [
{
tool: "create_schedule",
field: "path",
stringStartsWithAnyOf: ["f/", "u/"],
stringMustNotStartWithAnyOf: ["schedules/"],
},
],
});
expect(caseEntry?.skipJudge).toBe(true);
});
});
+4
View File
@@ -19,8 +19,10 @@ interface RawEvalCase {
initial?: string;
expected?: string;
validate?: EvalValidationSpec;
toolExpect?: EvalCase["toolExpect"];
cliExpect?: CliValidationSpec;
judgeChecklist?: string[];
skipJudge?: boolean;
runtime?: EvalCaseRuntimeSpec;
}
export function getRepoRoot(): string {
@@ -46,8 +48,10 @@ export async function loadCases(mode: EvalMode): Promise<EvalCase[]> {
initialPath: resolveFixturePath(entry.initial),
expectedPath: resolveFixturePath(entry.expected),
validate: entry.validate,
toolExpect: entry.toolExpect,
cliExpect: entry.cliExpect,
judgeChecklist: entry.judgeChecklist,
skipJudge: entry.skipJudge,
runtime: entry.runtime,
}));
}
+7 -1
View File
@@ -7,6 +7,7 @@ import type {
FrontendBenchmarkProgressEvent,
ModeRunner,
} from "./types";
import { validateToolExpectations } from "./validators";
export async function runSuite<TInitial, TExpected, TActual>(input: {
modeRunner: ModeRunner<TInitial, TExpected, TActual>;
@@ -169,6 +170,10 @@ async function runCaseAttempts<TInitial, TExpected, TActual>(input: {
actual: run.actual,
run,
}),
...validateToolExpectations({
run,
toolExpect: input.evalCase.toolExpect,
}),
];
const artifactFiles = input.modeRunner.buildArtifacts?.(run.actual) ?? [];
@@ -213,7 +218,7 @@ async function runCaseAttempts<TInitial, TExpected, TActual>(input: {
let judgeScore: number | null = null;
let judgeSummary: string | null = null;
if (run.success) {
if (run.success && !input.evalCase.skipJudge) {
const judge = await judgeOutput({
mode: input.modeRunner.mode,
prompt: input.evalCase.prompt,
@@ -243,6 +248,7 @@ async function runCaseAttempts<TInitial, TExpected, TActual>(input: {
assistantMessageCount: run.assistantMessageCount,
toolCallCount: run.toolCallCount,
toolsUsed: uniqueStrings(run.toolsUsed),
toolCallDetails: run.toolCallDetails,
skillsInvoked: uniqueStrings(run.skillsInvoked),
checks,
judgeScore,
+21
View File
@@ -123,6 +123,23 @@ export interface CliValidationSpec {
workspaceUnchanged?: boolean;
}
export interface ToolCallDetail {
name: string;
arguments: unknown;
}
export interface ToolCallArgumentRule {
tool: string;
field: string;
stringStartsWithAnyOf?: string[];
stringMustNotStartWithAnyOf?: string[];
}
export interface ToolValidationSpec {
requiredToolsUsed?: string[];
toolCallArgs?: ToolCallArgumentRule[];
}
export type EvalValidationSpec = FlowValidationSpec | AppValidationSpec;
export interface EvalCase {
@@ -131,8 +148,10 @@ export interface EvalCase {
initialPath?: string;
expectedPath?: string;
validate?: EvalValidationSpec;
toolExpect?: ToolValidationSpec;
cliExpect?: CliValidationSpec;
judgeChecklist?: string[];
skipJudge?: boolean;
runtime?: EvalCaseRuntimeSpec;
}
@@ -195,6 +214,7 @@ export interface ModeRunOutput<TActual> {
assistantMessageCount: number;
toolCallCount: number;
toolsUsed: string[];
toolCallDetails?: ToolCallDetail[];
skillsInvoked: string[];
tokenUsage?: BenchmarkTokenUsage | null;
}
@@ -251,6 +271,7 @@ export interface BenchmarkAttemptResult {
assistantMessageCount: number;
toolCallCount: number;
toolsUsed: string[];
toolCallDetails?: ToolCallDetail[];
skillsInvoked: string[];
checks: BenchmarkCheck[];
judgeScore: number | null;
+85 -1
View File
@@ -1,5 +1,10 @@
import { describe, expect, it } from "bun:test";
import { validateAppState, validateCliWorkspace, validateScriptState } from "./validators";
import {
validateAppState,
validateCliWorkspace,
validateScriptState,
validateToolExpectations,
} from "./validators";
describe("validateScriptState", () => {
it("accepts semantically equivalent script implementations", () => {
@@ -35,6 +40,85 @@ describe("validateScriptState", () => {
});
});
describe("validateToolExpectations", () => {
it("accepts Windmill-prefixed schedule paths", () => {
const checks = validateToolExpectations({
run: {
success: true,
actual: {},
assistantMessageCount: 1,
toolCallCount: 1,
toolsUsed: ["create_schedule"],
toolCallDetails: [
{
name: "create_schedule",
arguments: {
path: "f/evals/greet_user_daily",
},
},
],
skillsInvoked: [],
},
toolExpect: {
requiredToolsUsed: ["create_schedule"],
toolCallArgs: [
{
tool: "create_schedule",
field: "path",
stringStartsWithAnyOf: ["f/", "u/"],
stringMustNotStartWithAnyOf: ["schedules/"],
},
],
},
});
expect(checks.every((check) => check.passed)).toBe(true);
});
it("rejects schedule-prefixed tool paths", () => {
const checks = validateToolExpectations({
run: {
success: true,
actual: {},
assistantMessageCount: 1,
toolCallCount: 1,
toolsUsed: ["create_schedule"],
toolCallDetails: [
{
name: "create_schedule",
arguments: {
path: "schedules/greet_user_daily",
},
},
],
skillsInvoked: [],
},
toolExpect: {
requiredToolsUsed: ["create_schedule"],
toolCallArgs: [
{
tool: "create_schedule",
field: "path",
stringStartsWithAnyOf: ["f/", "u/"],
stringMustNotStartWithAnyOf: ["schedules/"],
},
],
},
});
expect(checks).toContainEqual({
name: "create_schedule.path uses an accepted prefix",
passed: false,
details: 'accepted prefixes: f/, u/; values: "schedules/greet_user_daily"',
});
expect(checks).toContainEqual({
name: "create_schedule.path avoids rejected prefixes",
passed: false,
details: 'rejected prefixes: schedules/; values: "schedules/greet_user_daily"',
});
});
});
describe("validateAppState", () => {
it("accepts app persistence requirements when a datatable table is registered", () => {
const checks = validateAppState({
+84
View File
@@ -6,6 +6,8 @@ import type {
CliTrace,
CliValidationSpec,
FlowValidationSpec,
ModeRunOutput,
ToolValidationSpec,
} from "./types";
export interface ScriptState {
@@ -16,6 +18,7 @@ export interface ScriptState {
}
export interface FlowState {
path?: string;
summary?: string;
value?: {
preprocessor_module?: Record<string, unknown>;
@@ -129,6 +132,76 @@ export function validateFlowState(input: {
return checks;
}
export function validateToolExpectations(input: {
run: ModeRunOutput<unknown>;
toolExpect?: ToolValidationSpec;
}): BenchmarkCheck[] {
const expect = input.toolExpect;
if (!expect) {
return [];
}
const checks: BenchmarkCheck[] = [];
const toolCallDetails = input.run.toolCallDetails ?? [];
for (const toolName of expect.requiredToolsUsed ?? []) {
checks.push(
check(
`uses ${toolName}`,
input.run.toolsUsed.includes(toolName),
`tools used: ${input.run.toolsUsed.join(", ") || "none"}`
)
);
}
for (const rule of expect.toolCallArgs ?? []) {
const calls = toolCallDetails.filter((call) => call.name === rule.tool);
checks.push(
check(
`${rule.tool} was called for argument validation`,
calls.length > 0,
`tools called: ${toolCallDetails.map((call) => call.name).join(", ") || "none"}`
)
);
if (calls.length === 0) {
continue;
}
const values = calls.map((call) => getToolArgumentValue(call.arguments, rule.field));
if (rule.stringStartsWithAnyOf && rule.stringStartsWithAnyOf.length > 0) {
const invalidValues = values.filter(
(value) =>
typeof value !== "string" ||
!rule.stringStartsWithAnyOf!.some((prefix) => value.startsWith(prefix))
);
checks.push(
check(
`${rule.tool}.${rule.field} uses an accepted prefix`,
invalidValues.length === 0,
`accepted prefixes: ${rule.stringStartsWithAnyOf.join(", ")}; values: ${summarizeToolValues(values)}`
)
);
}
if (rule.stringMustNotStartWithAnyOf && rule.stringMustNotStartWithAnyOf.length > 0) {
const invalidValues = values.filter(
(value) =>
typeof value === "string" &&
rule.stringMustNotStartWithAnyOf!.some((prefix) => value.startsWith(prefix))
);
checks.push(
check(
`${rule.tool}.${rule.field} avoids rejected prefixes`,
invalidValues.length === 0,
`rejected prefixes: ${rule.stringMustNotStartWithAnyOf.join(", ")}; values: ${summarizeToolValues(values)}`
)
);
}
}
return checks;
}
export function validateAppState(input: {
actual: AppFilesState;
initial?: AppFilesState;
@@ -329,6 +402,17 @@ function check(name: string, passed: boolean, details?: string): BenchmarkCheck
return !passed && details ? { name, passed, details } : { name, passed };
}
function getToolArgumentValue(args: unknown, dottedPath: string): unknown {
if (!isObjectRecord(args)) {
return undefined;
}
return getValueAtPath(args, dottedPath);
}
function summarizeToolValues(values: unknown[]): string {
return values.map((value) => JSON.stringify(value)).join(", ") || "none";
}
function normalizeText(value: string): string {
return value.replace(/\r\n/g, "\n").trim();
}
@@ -0,0 +1,39 @@
{
"path": "f/evals/order_processing_flow",
"summary": "Order processing flow",
"value": {
"modules": [
{
"id": "get_orders",
"summary": "Fetch orders",
"value": {
"type": "rawscript",
"language": "bun",
"content": "export async function main() {\n return [\n { id: \"ORD-001\", total: 150 },\n { id: \"ORD-002\", total: 280 }\n ];\n}",
"input_transforms": {}
}
},
{
"id": "summarize_orders",
"summary": "Summarize orders",
"value": {
"type": "rawscript",
"language": "bun",
"content": "export async function main(orders: Array<{ id: string; total: number }>) {\n return {\n count: orders.length,\n total: orders.reduce((sum, order) => sum + order.total, 0)\n };\n}",
"input_transforms": {
"orders": {
"type": "javascript",
"expr": "results.get_orders"
}
}
}
}
]
},
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {},
"required": [],
"type": "object"
}
}
+1
View File
@@ -62,6 +62,7 @@ export function createFlowModeRunner(
assistantMessageCount: result.assistantMessageCount,
toolCallCount: result.toolCallCount,
toolsUsed: result.toolsUsed,
toolCallDetails: result.toolCallDetails,
skillsInvoked: [],
tokenUsage: result.tokenUsage,
};
+1
View File
@@ -42,6 +42,7 @@ export function normalizeFlowStateFixture(value: unknown): FlowState {
export function normalizeFlowFixture(value: FlowState): FlowFixture {
return {
path: value.path,
schema: value.schema,
value: value.value
? {
+1
View File
@@ -53,6 +53,7 @@ export function createScriptModeRunner(
assistantMessageCount: result.assistantMessageCount,
toolCallCount: result.toolCallCount,
toolsUsed: result.toolsUsed,
toolCallDetails: result.toolCallDetails,
skillsInvoked: [],
tokenUsage: result.tokenUsage,
};
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO sqs_trigger (\n path, queue_url, aws_resource_path, message_attributes, script_path,\n is_flow, workspace_id, edited_by, edited_at, extra_perms, error,\n server_id, last_server_ping, aws_auth_resource_type, error_handler_path,\n error_handler_args, retry, mode, permissioned_as, labels\n )\n SELECT\n path, queue_url, aws_resource_path, message_attributes, script_path,\n is_flow, $1, edited_by, edited_at, extra_perms, NULL,\n NULL, NULL, aws_auth_resource_type, error_handler_path,\n error_handler_args, retry, 'disabled'::TRIGGER_MODE, permissioned_as, labels\n FROM sqs_trigger WHERE workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "0b347b021123e66ffb6b7eb690f7619daf34795410505609a1ff3bf0be953550"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO schedule (\n workspace_id, path, edited_by, edited_at, schedule, enabled, script_path,\n args, extra_perms, is_flow, email, error, timezone, on_failure,\n on_recovery, on_failure_times, on_failure_exact, on_failure_extra_args,\n on_recovery_times, on_recovery_extra_args, ws_error_handler_muted, retry,\n summary, no_flow_overlap, tag, paused_until, on_success, on_success_extra_args,\n cron_version, description, dynamic_skip, permissioned_as, labels\n )\n SELECT\n $1, path, edited_by, edited_at, schedule, FALSE, script_path,\n args, extra_perms, is_flow, email, error, timezone, on_failure,\n on_recovery, on_failure_times, on_failure_exact, on_failure_extra_args,\n on_recovery_times, on_recovery_extra_args, ws_error_handler_muted, retry,\n summary, no_flow_overlap, tag, paused_until, on_success, on_success_extra_args,\n cron_version, description, dynamic_skip, permissioned_as, labels\n FROM schedule WHERE workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "149b645af2324fc3140bf2662e75e579dcc8b928be3a2cc051e62aa2ddc09b1e"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO azure_trigger (\n azure_resource_path, azure_mode, scope_resource_id, topic_name,\n subscription_name, event_type_filters, push_auth_config, path, script_path,\n is_flow, workspace_id, edited_by, email, edited_at, extra_perms, server_id,\n last_server_ping, error, mode, permissioned_as, error_handler_path,\n error_handler_args, retry, labels\n )\n SELECT\n azure_resource_path, azure_mode, scope_resource_id, topic_name,\n subscription_name, event_type_filters, push_auth_config, path, script_path,\n is_flow, $1, edited_by, email, edited_at, extra_perms, NULL,\n NULL, NULL, 'disabled'::TRIGGER_MODE, permissioned_as, error_handler_path,\n error_handler_args, retry, labels\n FROM azure_trigger WHERE workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "16e8d3f75ce4b5b18cefc25ebb670b506677681394606426964491488d64c62c"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO email_trigger (\n path, local_part, workspaced_local_part, script_path, is_flow,\n workspace_id, edited_by, edited_at, extra_perms, error_handler_path,\n error_handler_args, retry, mode, permissioned_as, labels\n )\n SELECT\n path, local_part, workspaced_local_part, script_path, is_flow,\n $1, edited_by, edited_at, extra_perms, error_handler_path,\n error_handler_args, retry, 'disabled'::TRIGGER_MODE, permissioned_as, labels\n FROM email_trigger\n WHERE workspace_id = $2\n AND (workspaced_local_part IS TRUE OR $3)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "1a66a5a9c2b7b75e783c59b4c2ed3f7f84adc67318fd0bc5cdee78b2c406ae81"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_shared_ui (workspace_id, files, version, edited_at, edited_by)\n VALUES ($1, $2, 1, now(), $3)\n ON CONFLICT (workspace_id) DO UPDATE\n SET files = EXCLUDED.files,\n version = workspace_shared_ui.version + 1,\n edited_at = now(),\n edited_by = EXCLUDED.edited_by",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Jsonb",
"Varchar"
]
},
"nullable": []
},
"hash": "1f1e4046c55ec33e0ea51d3424c21f538ccc72b86f4ead52292463c29f8dfea6"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM schedule WHERE workspace_id = $1 AND path = $2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "22699056871c6306f689d4bc6f9a67070baa4ff83de3b4a04c4d8d6054a59319"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM group_ WHERE workspace_id = $1 ORDER BY name asc LIMIT $2 OFFSET $3",
"query": "SELECT workspace_id, name, summary, extra_perms FROM group_ WHERE workspace_id = $1 ORDER BY name asc LIMIT $2 OFFSET $3",
"describe": {
"columns": [
{
@@ -38,5 +38,5 @@
false
]
},
"hash": "ba9ab074f466bc2c581f018e2592f5a453e8a766c35dbf919d29c96966d63c75"
"hash": "25784f87ccf0bc13b93739d34d416908b75d0d17392c2565e70b4738039b56a1"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM resource WHERE workspace_id = $1 AND resource_type != 'state' AND resource_type != 'cache'",
"query": "SELECT workspace_id, path, value, description, resource_type, extra_perms, created_by, edited_at, labels FROM resource WHERE workspace_id = $1 AND resource_type != 'state' AND resource_type != 'cache'",
"describe": {
"columns": [
{
@@ -35,13 +35,13 @@
},
{
"ordinal": 6,
"name": "edited_at",
"type_info": "Timestamptz"
"name": "created_by",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "created_by",
"type_info": "Varchar"
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 8,
@@ -66,5 +66,5 @@
true
]
},
"hash": "45e4d13f5806122faecdb1d9ab18159555b652869a036b006f4a151e999b17b7"
"hash": "27b243e7ff9838a8fc0a4a8e51ac5d33d8617fc75c0d9d08e428db488e0be409"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT label, token_prefix, expiration, created_at, last_used_at, scopes FROM token WHERE email = $1\n ORDER BY created_at DESC LIMIT $2 OFFSET $3",
"query": "SELECT label, token_prefix, expiration, created_at, last_used_at, scopes, workspace_id FROM token WHERE email = $1\n ORDER BY created_at DESC LIMIT $2 OFFSET $3",
"describe": {
"columns": [
{
@@ -32,6 +32,11 @@
"ordinal": 5,
"name": "scopes",
"type_info": "TextArray"
},
{
"ordinal": 6,
"name": "workspace_id",
"type_info": "Varchar"
}
],
"parameters": {
@@ -47,8 +52,9 @@
true,
false,
false,
true,
true
]
},
"hash": "ebc2eed287f93e184ed683feb20432caa6e6682620c90f38b29dd32b9a8fe633"
"hash": "2b5fc0500beb2f4c7cf5997f9aea48f77e2abe4523180c507a9a90570127be6d"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM group_ WHERE name = $1 AND workspace_id = $2",
"query": "SELECT workspace_id, name, summary, extra_perms FROM group_ WHERE name = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
@@ -37,5 +37,5 @@
false
]
},
"hash": "d9c8f6ec7bd10e533876526255c15e376ccb4f898b9c0ab8840b2930bda24fdc"
"hash": "2c9a56fd46767c15dabc7813cd2b167a2f73a06d661d31c375697a68f7066aa9"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO gcp_trigger (\n gcp_resource_path, topic_id, subscription_id, delivery_type,\n delivery_config, path, script_path, is_flow, workspace_id, edited_by,\n edited_at, extra_perms, server_id, last_server_ping, error,\n subscription_mode, error_handler_path, error_handler_args, retry,\n auto_acknowledge_msg, ack_deadline, mode, permissioned_as, labels\n )\n SELECT\n gcp_resource_path, topic_id, subscription_id, delivery_type,\n delivery_config, path, script_path, is_flow, $1, edited_by,\n edited_at, extra_perms, NULL, NULL, NULL,\n subscription_mode, error_handler_path, error_handler_args, retry,\n auto_acknowledge_msg, ack_deadline, 'disabled'::TRIGGER_MODE, permissioned_as, labels\n FROM gcp_trigger WHERE workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "2e67fa50d5d66cbca4ef74111f0ff9b51a6168adad3775f8d6cf40d02cb29d14"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT label, token_prefix, expiration, created_at, last_used_at, scopes FROM token WHERE email = $1 AND (label != 'ephemeral-script' OR label IS NULL)\n ORDER BY created_at DESC LIMIT $2 OFFSET $3",
"query": "SELECT label, token_prefix, expiration, created_at, last_used_at, scopes, workspace_id FROM token WHERE email = $1 AND (label != 'ephemeral-script' OR label IS NULL)\n ORDER BY created_at DESC LIMIT $2 OFFSET $3",
"describe": {
"columns": [
{
@@ -32,6 +32,11 @@
"ordinal": 5,
"name": "scopes",
"type_info": "TextArray"
},
{
"ordinal": 6,
"name": "workspace_id",
"type_info": "Varchar"
}
],
"parameters": {
@@ -47,8 +52,9 @@
true,
false,
false,
true,
true
]
},
"hash": "1bf4a93cb85c6eed313a2f393da9408dd2aa4e47ef7a38a0d3ccca944a09f5bb"
"hash": "40f0bc9a2555a7c90b3985a190bf3fce18c09693b645f2ac520de6936366f3c8"
}
@@ -0,0 +1,40 @@
{
"db_name": "PostgreSQL",
"query": "SELECT files, version, edited_at, edited_by FROM workspace_shared_ui WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "files",
"type_info": "Jsonb"
},
{
"ordinal": 1,
"name": "version",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 3,
"name": "edited_by",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "410718c2290d3d4688382fe28c9aca76edec85ce6f0b0ede908d0fb1ac6925c4"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM resource_type WHERE workspace_id = $1",
"query": "SELECT workspace_id, name, schema, description, created_by, edited_at, format_extension, is_fileset FROM resource_type WHERE workspace_id = $1",
"describe": {
"columns": [
{
@@ -25,13 +25,13 @@
},
{
"ordinal": 4,
"name": "edited_at",
"type_info": "Timestamptz"
"name": "created_by",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "created_by",
"type_info": "Varchar"
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
@@ -60,5 +60,5 @@
false
]
},
"hash": "7b1239ad6460e8f5fb41bfe12f662a779528784ec8cf3f6dcce5545ab90bf234"
"hash": "45d5e9ead8193a04fd00c44a488590fdd2f7c4de45117a18360651655d153545"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO nats_trigger (\n path, nats_resource_path, subjects, stream_name, consumer_name,\n use_jetstream, script_path, is_flow, workspace_id, edited_by, edited_at,\n extra_perms, server_id, last_server_ping, error, error_handler_path,\n error_handler_args, retry, mode, permissioned_as, labels\n )\n SELECT\n path, nats_resource_path, subjects, stream_name, consumer_name,\n use_jetstream, script_path, is_flow, $1, edited_by, edited_at,\n extra_perms, NULL, NULL, NULL, error_handler_path,\n error_handler_args, retry, 'disabled'::TRIGGER_MODE, permissioned_as, labels\n FROM nats_trigger WHERE workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "4d64c962e219f7cda8b93c3875acc78a4e17aae2c702ec1af6c3a4ae3f1e2716"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT EXISTS(\n SELECT 1\n FROM email_trigger\n WHERE\n ((workspaced_local_part IS TRUE AND workspace_id || '-' || local_part = $1)\n OR (workspaced_local_part IS FALSE AND local_part = $1))\n AND ($2::TEXT IS NULL OR path != $2)\n )\n ",
"query": "\n SELECT EXISTS(\n SELECT 1\n FROM email_trigger\n WHERE\n ((workspaced_local_part IS TRUE AND workspace_id || '-' || local_part = $1)\n OR (workspaced_local_part IS FALSE AND local_part = $1))\n AND ($2::TEXT IS NULL OR NOT (workspace_id = $3 AND path = $2))\n )\n ",
"describe": {
"columns": [
{
@@ -11,6 +11,7 @@
],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
@@ -19,5 +20,5 @@
null
]
},
"hash": "bb94e3105cb1bc5d90af2bc914b579a6e821c432eac38ad877d9ff362d8ab916"
"hash": "4d73cefcc1b72238b7731ab52376b849a6f712b4109a48d9f77da8dd0060ab1a"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM usr\n WHERE workspace_id = $1",
"query": "SELECT workspace_id, username, email, is_admin, created_at, operator, disabled, role, added_via FROM usr\n WHERE workspace_id = $1",
"describe": {
"columns": [
{
@@ -47,11 +47,6 @@
"ordinal": 8,
"name": "added_via",
"type_info": "Jsonb"
},
{
"ordinal": 9,
"name": "is_service_account",
"type_info": "Bool"
}
],
"parameters": {
@@ -68,9 +63,8 @@
false,
false,
true,
true,
false
true
]
},
"hash": "e5fb3531f8bc7ef1f7484524f8c3bc9c48f71a44827ba0d01ac5588dc31082a2"
"hash": "500b68d23314cb0f9edd570e576d5b9822bebe88f90ae007d943f2c761858190"
}
@@ -15,7 +15,7 @@
]
},
"nullable": [
true
null
]
},
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO websocket_trigger (\n path, url, script_path, is_flow, workspace_id, edited_by, edited_at,\n extra_perms, server_id, last_server_ping, error, filters, initial_messages,\n url_runnable_args, can_return_message, error_handler_path, error_handler_args,\n retry, can_return_error_result, mode, permissioned_as, filter_logic, labels,\n heartbeat\n )\n SELECT\n path, url, script_path, is_flow, $1, edited_by, edited_at,\n extra_perms, NULL, NULL, NULL, filters, initial_messages,\n url_runnable_args, can_return_message, error_handler_path, error_handler_args,\n retry, can_return_error_result, 'disabled'::TRIGGER_MODE, permissioned_as, filter_logic, labels,\n heartbeat\n FROM websocket_trigger WHERE workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "5fd769bde29e88eb43f5dc0ecd7f4a1fe20d216d818d69eea4ddbaace5233137"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = 'admins')",
"query": "SELECT workspace_id, name, schema, description, created_by, edited_at, format_extension, is_fileset from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = 'admins')",
"describe": {
"columns": [
{
@@ -25,13 +25,13 @@
},
{
"ordinal": 4,
"name": "edited_at",
"type_info": "Timestamptz"
"name": "created_by",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "created_by",
"type_info": "Varchar"
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
@@ -61,5 +61,5 @@
false
]
},
"hash": "03d63d2e64b012f624d2731b5bcb8849c74a9474777be61edf0ed43ddda07ef3"
"hash": "623b061ccaa6bb883e95771fde8c911a165c9c430b7db389370361ca74d737f4"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM config WHERE name LIKE 'worker__%'",
"query": "SELECT name, config FROM config WHERE name LIKE 'worker__%'",
"describe": {
"columns": [
{
@@ -22,5 +22,5 @@
true
]
},
"hash": "ce9e56ff451bae10af2c396352f5f93f78658e57b79dc5295553cacc328eb2b7"
"hash": "6f95d6927a9be75f6c371c799ebe3755142647d0257b76e9befd0c4f31c332b1"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT *\n FROM usr\n WHERE workspace_id = $1\n ",
"query": "\n SELECT workspace_id, username, email, is_admin, created_at, operator, disabled, role, added_via, is_service_account\n FROM usr\n WHERE workspace_id = $1\n ",
"describe": {
"columns": [
{
@@ -72,5 +72,5 @@
false
]
},
"hash": "5d6adbe21b9f8dd984d1bfc750fb81763d8650c1316bb0b20816f1a5d61a678c"
"hash": "704d873d4585e00aa0a88cc9949d3a6cc806b33a844b8e0e600cb8bfae428e84"
}
@@ -0,0 +1,76 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n workspace_id,\n slack_team_id,\n slack_name,\n teams_team_id,\n teams_team_name,\n teams_team_guid,\n mute_critical_alerts,\n deploy_ui,\n large_file_storage,\n datatable\n FROM\n workspace_settings\n WHERE\n workspace_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "slack_team_id",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "slack_name",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "teams_team_id",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "teams_team_name",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "teams_team_guid",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "mute_critical_alerts",
"type_info": "Bool"
},
{
"ordinal": 7,
"name": "deploy_ui",
"type_info": "Jsonb"
},
{
"ordinal": 8,
"name": "large_file_storage",
"type_info": "Jsonb"
},
{
"ordinal": 9,
"name": "datatable",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
true,
true,
true,
true,
true,
true,
true,
true,
true
]
},
"hash": "71eeda25c59d724d6e0c4b2b52078567d6e477a2c62656de1deaef602221edcf"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT version FROM workspace_shared_ui WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "version",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "77f9650e543821605d6cd5eb11af3a6d46e60d5288357f22faf5fcd5298923c3"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM usr WHERE username = $1 AND workspace_id = $2",
"query": "SELECT workspace_id, username, email, is_admin, created_at, operator, disabled, role, added_via, is_service_account FROM usr WHERE username = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
@@ -73,5 +73,5 @@
false
]
},
"hash": "60b3a59805d463a61eed68072d1ea032b00fc9bd7a6db22f530f67eb9730fa3b"
"hash": "8f308f841b2d60a6be2bc4ae6aeae506781295a3bcf036dbbc8600f229e61408"
}
@@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE token SET scopes = $1\n WHERE email = $2 AND token_prefix = $3\n RETURNING token_prefix",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token_prefix",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"TextArray",
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "a7a20412e303568b271f949642de55e9880ef05786fe59f05de5e025ef315726"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT EXISTS(\n SELECT 1\n FROM http_trigger\n WHERE\n ((workspaced_route IS TRUE AND workspace_id || '/' || route_path_key = $1)\n OR (workspaced_route IS FALSE AND route_path_key = $1))\n AND http_method = $2\n AND ($3::TEXT IS NULL OR path != $3)\n )\n ",
"query": "\n SELECT EXISTS(\n SELECT 1\n FROM http_trigger\n WHERE\n ((workspaced_route IS TRUE AND workspace_id || '/' || route_path_key = $1)\n OR (workspaced_route IS FALSE AND route_path_key = $1))\n AND http_method = $2\n AND ($3::TEXT IS NULL OR NOT (workspace_id = $4 AND path = $3))\n )\n ",
"describe": {
"columns": [
{
@@ -26,6 +26,7 @@
}
}
},
"Text",
"Text"
]
},
@@ -33,5 +34,5 @@
null
]
},
"hash": "fe464b8b3ade86743d82c5e3fb14f457e07f07e44c7b693d5d755899d4210dee"
"hash": "aa0a2f90d15a642ad3caaa3876d9cb4a5391ff8663da61b48bfeb73bfa005bbe"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO http_trigger (\n path, route_path, route_path_key, script_path, is_flow, workspace_id,\n edited_by, edited_at, extra_perms, authentication_method, http_method,\n static_asset_config, is_static_website, workspaced_route, wrap_body,\n raw_string, authentication_resource_path, summary, description,\n error_handler_path, error_handler_args, retry, request_type, mode,\n permissioned_as, labels\n )\n SELECT\n path, route_path, route_path_key, script_path, is_flow, $1,\n edited_by, edited_at, extra_perms, authentication_method, http_method,\n static_asset_config, is_static_website, workspaced_route, wrap_body,\n raw_string, authentication_resource_path, summary, description,\n error_handler_path, error_handler_args, retry, request_type, 'disabled'::TRIGGER_MODE,\n permissioned_as, labels\n FROM http_trigger\n WHERE workspace_id = $2\n AND (workspaced_route IS TRUE OR $3)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "aee9bf16e37a5361f96d6b35dddffa6cc37346cdb6b38ecbf6e1530eef3a57bd"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO postgres_trigger (\n path, script_path, is_flow, workspace_id, edited_by, edited_at,\n extra_perms, postgres_resource_path, error, server_id, last_server_ping,\n replication_slot_name, publication_name, error_handler_path,\n error_handler_args, retry, mode, permissioned_as, labels\n )\n SELECT\n path, script_path, is_flow, $1, edited_by, edited_at,\n extra_perms, postgres_resource_path, NULL, NULL, NULL,\n replication_slot_name, publication_name, error_handler_path,\n error_handler_args, retry, 'disabled'::TRIGGER_MODE, permissioned_as, labels\n FROM postgres_trigger WHERE workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "b20e4486e2038ac8138f5d7435db8cbd78ac3d68674335db79df7fd8a4ba91fd"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO mqtt_trigger (\n mqtt_resource_path, subscribe_topics, client_version, v5_config, v3_config,\n client_id, path, script_path, is_flow, workspace_id, edited_by, edited_at,\n extra_perms, server_id, last_server_ping, error, error_handler_path,\n error_handler_args, retry, mode, permissioned_as, labels\n )\n SELECT\n mqtt_resource_path, subscribe_topics, client_version, v5_config, v3_config,\n client_id, path, script_path, is_flow, $1, edited_by, edited_at,\n extra_perms, NULL, NULL, NULL, error_handler_path,\n error_handler_args, retry, 'disabled'::TRIGGER_MODE, permissioned_as, labels\n FROM mqtt_trigger WHERE workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "c8c8c457ee80125938af97993c4ca759b66507071163f8d98a778f417b930413"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM config WHERE name = $1",
"query": "SELECT name, config FROM config WHERE name = $1",
"describe": {
"columns": [
{
@@ -24,5 +24,5 @@
true
]
},
"hash": "d233e07d19e8e339e1378c1bfc5d78d592c00ffb6f42c3d072f56305b40e50f9"
"hash": "c9064664829d304013920e3f710c4dfab99b263e5271045c542c34216e2b47cb"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO kafka_trigger (\n path, kafka_resource_path, topics, group_id, script_path, is_flow,\n workspace_id, edited_by, edited_at, extra_perms, server_id,\n last_server_ping, error, error_handler_path, error_handler_args, retry,\n mode, filters, auto_offset_reset, reset_offset, auto_commit,\n permissioned_as, filter_logic, labels\n )\n SELECT\n path, kafka_resource_path, topics, group_id, script_path, is_flow,\n $1, edited_by, edited_at, extra_perms, NULL,\n NULL, NULL, error_handler_path, error_handler_args, retry,\n 'disabled'::TRIGGER_MODE, filters, auto_offset_reset, reset_offset, auto_commit,\n permissioned_as, filter_logic, labels\n FROM kafka_trigger WHERE workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "c9c0b92c1fea9b4bdafba32da60e5579a4c2940bed63097ffec7f757d9882667"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT flow_status FROM v2_job_completed WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "flow_status",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
true
]
},
"hash": "ca60e1875e82a492b34bbfd771ad7ba526e4aa7914f1042c5ea461816f69b658"
}
@@ -0,0 +1,71 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n j.runnable_id AS \"runnable_id: ScriptHash\",\n j.kind AS \"job_kind!: JobKind\",\n COALESCE(c.flow_status, c.workflow_as_code_status) AS \"flow_status: sqlx::types::Json<Box<RawValue>>\",\n j.raw_flow AS \"raw_flow: sqlx::types::Json<Box<RawValue>>\"\n FROM v2_job_completed c JOIN v2_job j USING (id)\n WHERE j.id = $1 AND j.workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "runnable_id: ScriptHash",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "job_kind!: JobKind",
"type_info": {
"Custom": {
"name": "job_kind",
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies",
"http",
"graphql",
"postgresql",
"noop",
"appdependencies",
"deploymentcallback",
"singlestepflow",
"flowscript",
"flownode",
"appscript",
"aiagent",
"unassigned_script",
"unassigned_flow",
"unassigned_singlestepflow"
]
}
}
}
},
{
"ordinal": 2,
"name": "flow_status: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 3,
"name": "raw_flow: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
true,
false,
null,
true
]
},
"hash": "cc181a37ae10254f0b7727655207c40ba729ac4c8a21667ede7b4d13d328482f"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * from resource_type WHERE (workspace_id = $1 OR workspace_id = 'admins') ORDER BY name",
"query": "SELECT workspace_id, name, schema, description, created_by, edited_at, format_extension, is_fileset from resource_type WHERE (workspace_id = $1 OR workspace_id = 'admins') ORDER BY name",
"describe": {
"columns": [
{
@@ -25,13 +25,13 @@
},
{
"ordinal": 4,
"name": "edited_at",
"type_info": "Timestamptz"
"name": "created_by",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "created_by",
"type_info": "Varchar"
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
@@ -60,5 +60,5 @@
false
]
},
"hash": "b8d392ccfcccafe0c19511b3567bc11779b1052b0948c410468a8aeba1d26d33"
"hash": "d0a95698b9a2c5e2543e94276d854d7e509c7db2c2ac7d395b7b53ad5dbc25e6"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * from resource_type ORDER BY name",
"query": "SELECT workspace_id, name, schema, description, created_by, edited_at, format_extension, is_fileset from resource_type ORDER BY name",
"describe": {
"columns": [
{
@@ -25,13 +25,13 @@
},
{
"ordinal": 4,
"name": "edited_at",
"type_info": "Timestamptz"
"name": "created_by",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "created_by",
"type_info": "Varchar"
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
@@ -58,5 +58,5 @@
false
]
},
"hash": "eb1f7f01461f5a7540c273b37e5d578c31cf151ab3ef813f7aada76533761e12"
"hash": "e253b9e7e6450652589d6ee7ffa86d600e449cd399ac781af8b40c1c444972c3"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT parent_workspace_id IS NOT NULL FROM workspace WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "f57ff370f6775602d2c200d78650d65ffa5bfc5f10e8bd2a3162894c93283259"
}
+83 -83
View File
@@ -7681,14 +7681,14 @@ dependencies = [
[[package]]
name = "json-patch"
version = "4.1.0"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f300e415e2134745ef75f04562dd0145405c2f7fd92065db029ac4b16b57fe90"
checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72"
dependencies = [
"jsonptr",
"serde",
"serde_json",
"thiserror 1.0.69",
"thiserror 2.0.18",
]
[[package]]
@@ -13260,9 +13260,9 @@ dependencies = [
[[package]]
name = "sse-stream"
version = "0.2.2"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c5e6deb40826033bd7b11c7ef25ef71193fabd71f680f40dd16538a2704d2f4"
checksum = "f3962b63f038885f15bce2c6e02c0e7925c072f1ac86bb60fd44c5c6b762fb72"
dependencies = [
"bytes",
"futures-util",
@@ -16020,7 +16020,7 @@ dependencies = [
[[package]]
name = "windmill"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"async-nats",
@@ -16101,7 +16101,7 @@ dependencies = [
[[package]]
name = "windmill-ai"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"async-trait",
"aws-config",
@@ -16125,7 +16125,7 @@ dependencies = [
[[package]]
name = "windmill-alerting"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16138,7 +16138,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"argon2",
@@ -16281,7 +16281,7 @@ dependencies = [
[[package]]
name = "windmill-api-agent-workers"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16304,7 +16304,7 @@ dependencies = [
[[package]]
name = "windmill-api-assets"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16317,7 +16317,7 @@ dependencies = [
[[package]]
name = "windmill-api-auth"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"axum 0.8.4",
@@ -16343,7 +16343,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"reqwest 0.12.28",
"serde",
@@ -16353,7 +16353,7 @@ dependencies = [
[[package]]
name = "windmill-api-configs"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16370,7 +16370,7 @@ dependencies = [
[[package]]
name = "windmill-api-debug"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"axum 0.8.4",
"base64 0.22.1",
@@ -16392,7 +16392,7 @@ dependencies = [
[[package]]
name = "windmill-api-embeddings"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"axum 0.8.4",
@@ -16415,7 +16415,7 @@ dependencies = [
[[package]]
name = "windmill-api-flow-conversations"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16431,7 +16431,7 @@ dependencies = [
[[package]]
name = "windmill-api-flows"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16452,7 +16452,7 @@ dependencies = [
[[package]]
name = "windmill-api-groups"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16473,7 +16473,7 @@ dependencies = [
[[package]]
name = "windmill-api-inputs"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16487,7 +16487,7 @@ dependencies = [
[[package]]
name = "windmill-api-integration-tests"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"async-nats",
@@ -16519,7 +16519,7 @@ dependencies = [
[[package]]
name = "windmill-api-jobs"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"axum 0.8.4",
@@ -16544,7 +16544,7 @@ dependencies = [
[[package]]
name = "windmill-api-npm-proxy"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"axum 0.8.4",
"flate2",
@@ -16562,7 +16562,7 @@ dependencies = [
[[package]]
name = "windmill-api-openapi"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"axum 0.8.4",
@@ -16584,7 +16584,7 @@ dependencies = [
[[package]]
name = "windmill-api-schedule"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16604,7 +16604,7 @@ dependencies = [
[[package]]
name = "windmill-api-scripts"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16634,7 +16634,7 @@ dependencies = [
[[package]]
name = "windmill-api-settings"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"axum 0.8.4",
@@ -16662,7 +16662,7 @@ dependencies = [
[[package]]
name = "windmill-api-sse"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"lazy_static",
"serde",
@@ -16674,7 +16674,7 @@ dependencies = [
[[package]]
name = "windmill-api-users"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"argon2",
"axum 0.8.4",
@@ -16699,7 +16699,7 @@ dependencies = [
[[package]]
name = "windmill-api-workers"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16713,7 +16713,7 @@ dependencies = [
[[package]]
name = "windmill-api-workspaces"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16746,7 +16746,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"chrono",
"lazy_static",
@@ -16760,7 +16760,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"axum 0.8.4",
@@ -16779,7 +16779,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"aes-gcm",
"aho-corasick",
@@ -16880,7 +16880,7 @@ dependencies = [
[[package]]
name = "windmill-dep-map"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"chrono",
"itertools 0.14.0",
@@ -16899,7 +16899,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"regex",
"serde",
@@ -16914,7 +16914,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -16938,7 +16938,7 @@ dependencies = [
[[package]]
name = "windmill-jseval"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"futures",
@@ -16955,7 +16955,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -16971,7 +16971,7 @@ dependencies = [
[[package]]
name = "windmill-mcp"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16992,7 +16992,7 @@ dependencies = [
[[package]]
name = "windmill-native-triggers"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17023,7 +17023,7 @@ dependencies = [
[[package]]
name = "windmill-oauth"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"arc-swap",
@@ -17048,7 +17048,7 @@ dependencies = [
[[package]]
name = "windmill-object-store"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"async-stream",
@@ -17082,7 +17082,7 @@ dependencies = [
[[package]]
name = "windmill-operator"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"futures",
@@ -17100,7 +17100,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -17109,7 +17109,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -17121,7 +17121,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"serde_json",
@@ -17133,7 +17133,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"gosyn",
@@ -17145,7 +17145,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -17157,7 +17157,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"serde_json",
@@ -17169,7 +17169,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"nu-parser",
@@ -17180,7 +17180,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -17191,7 +17191,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -17203,7 +17203,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-asset"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -17214,7 +17214,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17236,7 +17236,7 @@ dependencies = [
[[package]]
name = "windmill-parser-r"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"serde_json",
@@ -17248,7 +17248,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -17262,7 +17262,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -17279,7 +17279,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -17292,7 +17292,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql-asset"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"serde",
@@ -17304,7 +17304,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -17322,7 +17322,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts-asset"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"serde-wasm-bindgen",
@@ -17338,7 +17338,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wac"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -17354,7 +17354,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"serde",
@@ -17365,7 +17365,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17402,7 +17402,7 @@ dependencies = [
[[package]]
name = "windmill-runtime-nativets"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"const_format",
@@ -17440,7 +17440,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"getrandom 0.3.4",
"wasm-bindgen",
@@ -17451,7 +17451,7 @@ dependencies = [
[[package]]
name = "windmill-store"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17481,7 +17481,7 @@ dependencies = [
[[package]]
name = "windmill-test-utils"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17505,7 +17505,7 @@ dependencies = [
[[package]]
name = "windmill-trigger"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17538,7 +17538,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-azure"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17571,7 +17571,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-email"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17591,7 +17591,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-gcp"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17625,7 +17625,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-http"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17661,7 +17661,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-kafka"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17684,7 +17684,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-mqtt"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17708,7 +17708,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-nats"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"async-nats",
@@ -17732,7 +17732,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-postgres"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17767,7 +17767,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-sqs"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17795,7 +17795,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-websocket"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17818,7 +17818,7 @@ dependencies = [
[[package]]
name = "windmill-types"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"bitflags 2.9.4",
@@ -17837,7 +17837,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"async-once-cell",
@@ -17949,7 +17949,7 @@ dependencies = [
[[package]]
name = "windmill-worker-volumes"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"bytes",
"futures",
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.692.0"
version = "1.694.0"
authors.workspace = true
edition.workspace = true
@@ -87,7 +87,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"]
[workspace.package]
version = "1.692.0"
version = "1.694.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
+1 -1
View File
@@ -1 +1 @@
ed842061576c3ac9b9eb89bb87f6db5b67904474
57a2a9f4490f214246a5b6fa35cc870f35ba40ba
@@ -0,0 +1,2 @@
DROP TRIGGER IF EXISTS token_scopes_update_trigger ON token;
DROP FUNCTION IF EXISTS notify_token_scopes_change();
@@ -0,0 +1,18 @@
-- Invalidate auth cache (across instances) when token scopes change.
-- Reuses the existing notify_token_invalidation channel handled in main.rs.
CREATE OR REPLACE FUNCTION notify_token_scopes_change()
RETURNS TRIGGER AS $$
BEGIN
IF OLD.scopes IS DISTINCT FROM NEW.scopes THEN
INSERT INTO notify_event (channel, payload)
VALUES ('notify_token_invalidation', NEW.token_prefix);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER token_scopes_update_trigger
AFTER UPDATE OF scopes ON token
FOR EACH ROW
EXECUTE FUNCTION notify_token_scopes_change();
@@ -0,0 +1,2 @@
-- Add down migration script here
DROP TABLE IF EXISTS workspace_shared_ui;
@@ -0,0 +1,11 @@
-- Add up migration script here
CREATE TABLE workspace_shared_ui (
workspace_id VARCHAR(50) PRIMARY KEY REFERENCES workspace(id) ON DELETE CASCADE,
files JSONB NOT NULL DEFAULT '{}'::jsonb,
version BIGINT NOT NULL DEFAULT 0,
edited_at TIMESTAMPTZ NOT NULL DEFAULT now(),
edited_by VARCHAR(255) NOT NULL DEFAULT ''
);
GRANT ALL ON workspace_shared_ui TO windmill_user;
GRANT ALL ON workspace_shared_ui TO windmill_admin;
+42 -20
View File
@@ -134,6 +134,7 @@ fn parse_bash_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
otyp: None,
has_default: default.is_some(),
oidx: None,
otyp_inferred: false,
});
} else {
break;
@@ -731,6 +732,7 @@ fn finalize_parameter(
otyp,
has_default,
oidx: None,
otyp_inferred: false,
})
}
@@ -774,7 +776,8 @@ non_required="${5:-}"
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -782,7 +785,8 @@ non_required="${5:-}"
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -790,7 +794,8 @@ non_required="${5:-}"
typ: Typ::Str(None),
default: Some(json!("latest with spaces")),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -798,7 +803,8 @@ non_required="${5:-}"
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -806,7 +812,8 @@ non_required="${5:-}"
typ: Typ::Str(None),
default: Some(json!("")),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
}
],
auto_kind: None,
@@ -833,7 +840,8 @@ non_required="${5:-}"
typ: Typ::Str(None),
default: None,
has_default: true, // Optional (not mandatory)
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: Some("string".to_string()), // [string]
@@ -841,7 +849,8 @@ non_required="${5:-}"
typ: Typ::Str(None),
default: None,
has_default: true, // Optional (not mandatory)
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None, // No type annotation
@@ -849,7 +858,8 @@ non_required="${5:-}"
typ: Typ::Str(None),
default: Some(json!("default value, with comma")),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: Some("int".to_string()), // [int]
@@ -857,7 +867,8 @@ non_required="${5:-}"
typ: Typ::Int,
default: Some(json!(3)),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None, // Type inferred from default value
@@ -865,7 +876,8 @@ non_required="${5:-}"
typ: Typ::Float,
default: Some(json!(5.0)),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None, // Type inferred from default value
@@ -873,7 +885,8 @@ non_required="${5:-}"
typ: Typ::Int,
default: Some(json!(5)),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None, // No type annotation
@@ -881,7 +894,8 @@ non_required="${5:-}"
typ: Typ::Str(None),
default: None,
has_default: true, // Optional (not mandatory)
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: Some("PSCustomObject".to_string()), // [PSCustomObject]
@@ -889,7 +903,8 @@ non_required="${5:-}"
typ: Typ::Object(ObjectType::new(None, None)),
default: None,
has_default: true, // Optional (not mandatory)
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: Some("string[]".to_string()), // [string[]]
@@ -897,7 +912,8 @@ non_required="${5:-}"
typ: Typ::List(Box::new(Typ::Str(None))),
default: None,
has_default: true, // Optional (not mandatory)
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: Some("string".to_string()), // [string] (last type bracket with Mandatory and ValidateSet)
@@ -909,7 +925,8 @@ non_required="${5:-}"
])), // ValidateSet enum
default: None,
has_default: false, // Required (Mandatory attribute)
oidx: None
oidx: None,
otyp_inferred: false,
}
],
auto_kind: None,
@@ -1462,7 +1479,8 @@ param(
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -1470,7 +1488,8 @@ param(
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -1478,7 +1497,8 @@ param(
typ: Typ::Str(None),
default: Some(json!("latest with spaces")),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -1486,7 +1506,8 @@ param(
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -1494,7 +1515,8 @@ param(
typ: Typ::Str(None),
default: Some(json!("")),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
}
],
auto_kind: None,
@@ -77,7 +77,7 @@ pub fn parse_csharp_sig_meta(code: &str) -> anyhow::Result<CsharpMainSigMeta> {
}
}
let (otyp, typ, name) = parse_csharp_typ(p_list_node, code)?;
args.push(Arg { name, otyp, typ, default, has_default: false, oidx: None });
args.push(Arg { name, otyp, typ, default, has_default: false, oidx: None, otyp_inferred: false });
}
}
}
+27 -13
View File
@@ -34,6 +34,7 @@ pub fn parse_go_sig(code: &str) -> anyhow::Result<MainArgSignature> {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
}
})
.collect_vec();
@@ -147,7 +148,10 @@ fn parse_go_typ(typ: &Expression) -> (Option<String>, Typ) {
Typ::Object(ObjectType::new(None, Some(typs))),
)
}
Expression::TypeInterface(_) => (Some("interface{}".to_string()), Typ::Object(ObjectType::new(None, Some(vec![])))),
Expression::TypeInterface(_) => (
Some("interface{}".to_string()),
Typ::Object(ObjectType::new(None, Some(vec![]))),
),
Expression::TypeMap(_) => (
Some("map[string]interface{}".to_string()),
Typ::Object(ObjectType::new(None, Some(vec![]))),
@@ -191,7 +195,8 @@ func main(x int, y string, z bool, l []string, o struct { Name string `json:"nam
typ: Typ::Int,
has_default: false,
default: None,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: Some("string".to_string()),
@@ -199,7 +204,8 @@ func main(x int, y string, z bool, l []string, o struct { Name string `json:"nam
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: Some("bool".to_string()),
@@ -207,7 +213,8 @@ func main(x int, y string, z bool, l []string, o struct { Name string `json:"nam
typ: Typ::Bool,
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: Some("[]string".to_string()),
@@ -215,18 +222,23 @@ func main(x int, y string, z bool, l []string, o struct { Name string `json:"nam
typ: Typ::List(Box::new(Typ::Str(None))),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: Some("struct { Name string `json:\"name\"` }".to_string()),
name: "o".to_string(),
typ: Typ::Object(ObjectType::new(None, Some(vec![ObjectProperty {
key: "name".to_string(),
typ: Box::new(Typ::Str(None))
},]))),
typ: Typ::Object(ObjectType::new(
None,
Some(vec![ObjectProperty {
key: "name".to_string(),
typ: Box::new(Typ::Str(None))
},])
)),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: Some("interface{}".to_string()),
@@ -234,7 +246,8 @@ func main(x int, y string, z bool, l []string, o struct { Name string `json:"nam
typ: Typ::Object(ObjectType::new(None, Some(vec![]))),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: Some("map[string]interface{}".to_string()),
@@ -242,12 +255,13 @@ func main(x int, y string, z bool, l []string, o struct { Name string `json:"nam
typ: Typ::Object(ObjectType::new(None, Some(vec![]))),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
],
auto_kind: None,
has_preprocessor: None,
..Default::default()
..Default::default()
}
);
@@ -64,6 +64,7 @@ fn parse_graphql_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
otyp: Some(typ.unwrap()),
has_default,
oidx: None,
otyp_inferred: false,
});
}
@@ -107,7 +108,8 @@ query($i: Int, $arr: [String]!, $wahoo: String = "wahoo") {
typ: Typ::Int,
default: None,
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: Some("[String]".to_string()),
@@ -115,7 +117,8 @@ query($i: Int, $arr: [String]!, $wahoo: String = "wahoo") {
typ: Typ::List(Box::new(Typ::Str(None))),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: Some("String".to_string()),
@@ -123,7 +126,8 @@ query($i: Int, $arr: [String]!, $wahoo: String = "wahoo") {
typ: Typ::Str(None),
default: Some(json!("wahoo")),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
}
],
auto_kind: None,
+41 -20
View File
@@ -69,6 +69,7 @@ pub fn parse_java_sig_meta(code: &str) -> anyhow::Result<JavaMainSigMeta> {
has_default: default.is_some(),
default,
oidx: None,
otyp_inferred: false,
});
}
}
@@ -256,7 +257,8 @@ class Main {
typ: Typ::Bytes,
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "b".into(),
@@ -264,7 +266,8 @@ class Main {
typ: Typ::Int,
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "c".into(),
@@ -272,7 +275,8 @@ class Main {
typ: Typ::Int,
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "d".into(),
@@ -280,7 +284,8 @@ class Main {
typ: Typ::Int,
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "e".into(),
@@ -288,7 +293,8 @@ class Main {
typ: Typ::Float,
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "f".into(),
@@ -296,7 +302,8 @@ class Main {
typ: Typ::Float,
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "g".into(),
@@ -304,7 +311,8 @@ class Main {
typ: Typ::Bool,
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "h".into(),
@@ -312,7 +320,8 @@ class Main {
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
]
);
@@ -338,7 +347,8 @@ class Main {
typ: Typ::Bytes,
default: Some(json!(null)),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "b".into(),
@@ -346,7 +356,8 @@ class Main {
typ: Typ::Int,
default: Some(json!(null)),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "c".into(),
@@ -354,7 +365,8 @@ class Main {
typ: Typ::Int,
default: Some(json!(null)),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "d".into(),
@@ -362,7 +374,8 @@ class Main {
typ: Typ::Int,
default: Some(json!(null)),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "e".into(),
@@ -370,7 +383,8 @@ class Main {
typ: Typ::Float,
default: Some(json!(null)),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "f".into(),
@@ -378,7 +392,8 @@ class Main {
typ: Typ::Float,
default: Some(json!(null)),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "g".into(),
@@ -386,7 +401,8 @@ class Main {
typ: Typ::Bool,
default: Some(json!(null)),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "h".into(),
@@ -394,7 +410,8 @@ class Main {
typ: Typ::Str(None),
default: Some(json!(null)),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "i".into(),
@@ -402,7 +419,8 @@ class Main {
typ: Typ::Object(ObjectType::new(None, Some(vec![]))),
default: Some(json!(null)),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
]
);
@@ -427,7 +445,8 @@ class Main {
typ: Typ::List(Box::new(Typ::Int)),
default: Some(json!(null)),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "b".into(),
@@ -435,7 +454,8 @@ class Main {
typ: Typ::List(Box::new(Typ::Object(ObjectType::new(None, Some(vec![]))))),
default: Some(json!(null)),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "c".into(),
@@ -443,7 +463,8 @@ class Main {
typ: Typ::List(Box::new(Typ::Str(None))),
default: Some(json!(null)),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
]
);
@@ -152,6 +152,7 @@ pub fn parse_nu_signature(code: &str) -> anyhow::Result<MainArgSignature> {
has_default: default.is_some() || optional,
default: default.or_else(|| if optional { Some(json!(null)) } else { None }),
oidx: None,
otyp_inferred: false,
});
}
@@ -27,7 +27,8 @@ mod test {
typ: Typ::Unknown,
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "b".into(),
@@ -35,7 +36,8 @@ mod test {
typ: Typ::Unknown,
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "c".into(),
@@ -43,7 +45,8 @@ mod test {
typ: Typ::Unknown,
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "d".into(),
@@ -51,7 +54,8 @@ mod test {
typ: Typ::Unknown,
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
}
],
auto_kind: None,
@@ -80,7 +84,8 @@ mod test {
typ: Typ::Unknown,
default: Some(serde_json::Value::Null),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},],
auto_kind: None,
has_preprocessor: None,
@@ -109,7 +114,8 @@ mod test {
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "bar".into(),
@@ -117,7 +123,8 @@ mod test {
typ: Typ::Int,
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
],
auto_kind: None,
@@ -158,7 +165,8 @@ mod test {
typ: Typ::Unknown,
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "a2".into(),
@@ -166,7 +174,8 @@ mod test {
typ: Typ::Bool,
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "a3".into(),
@@ -174,7 +183,8 @@ mod test {
typ: Typ::Int,
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "a4".into(),
@@ -182,7 +192,8 @@ mod test {
typ: Typ::Float,
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "a5".into(),
@@ -190,7 +201,8 @@ mod test {
typ: Typ::Datetime,
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "a6".into(),
@@ -198,7 +210,8 @@ mod test {
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "a7".into(),
@@ -206,7 +219,8 @@ mod test {
typ: Typ::Object(ObjectType::new(None, Some(vec![]))),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "a8".into(),
@@ -214,7 +228,8 @@ mod test {
typ: Typ::List(Box::new(Typ::Unknown)),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "a9".into(),
@@ -222,7 +237,8 @@ mod test {
typ: Typ::List(Box::new(Typ::Object(ObjectType::new(None, Some(vec![]))))),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "a10".into(),
@@ -230,7 +246,8 @@ mod test {
typ: Typ::Unknown,
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
],
auto_kind: None,
@@ -262,7 +279,8 @@ mod test {
typ: Typ::Unknown,
default: Some(json!("Foo")),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "bar".into(),
@@ -270,7 +288,8 @@ mod test {
typ: Typ::Str(None),
default: Some(json!("Bar")),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "bazz".into(),
@@ -278,7 +297,8 @@ mod test {
typ: Typ::Unknown,
default: Some(json!(3)),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
],
auto_kind: None,
@@ -375,7 +395,8 @@ mod test {
typ: Typ::List(Box::new(Typ::Float)),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},],
auto_kind: None,
has_preprocessor: None,
@@ -406,7 +427,8 @@ mod test {
typ: Typ::Unknown,
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "foo".into(),
@@ -414,7 +436,8 @@ mod test {
typ: Typ::List(Box::new(Typ::Float)),
default: Some(json!([2, 3, 4])),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "b".into(),
@@ -422,7 +445,8 @@ mod test {
typ: Typ::Unknown,
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
],
auto_kind: None,
@@ -452,7 +476,8 @@ mod test {
typ: Typ::Datetime,
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},],
auto_kind: None,
has_preprocessor: None,
@@ -515,7 +540,8 @@ mod test {
typ: Typ::Unknown,
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "b".into(),
@@ -523,7 +549,8 @@ mod test {
typ: Typ::Int,
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "c".into(),
@@ -531,7 +558,8 @@ mod test {
typ: Typ::Unknown,
default: Some(serde_json::Value::Null),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "d".into(),
@@ -539,7 +567,8 @@ mod test {
typ: Typ::Str(None),
default: Some(json!("foo")),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
name: "bi".into(),
@@ -547,7 +576,8 @@ mod test {
typ: Typ::Unknown,
default: Some(serde_json::Value::Null),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
}
],
auto_kind: None,
+11 -5
View File
@@ -91,6 +91,7 @@ pub fn parse_php_signature(
has_default: default.is_some(),
default,
oidx: None,
otyp_inferred: false,
}
})
.collect();
@@ -146,7 +147,8 @@ function main(string $input1 = \"hey\", bool $input2 = false, int $input3 = 3, f
typ: Typ::Str(None),
has_default: true,
default: Some(Value::String("hey".to_string())),
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -154,7 +156,8 @@ function main(string $input1 = \"hey\", bool $input2 = false, int $input3 = 3, f
typ: Typ::Bool,
has_default: true,
default: Some(Value::Bool(false)),
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -162,7 +165,8 @@ function main(string $input1 = \"hey\", bool $input2 = false, int $input3 = 3, f
typ: Typ::Int,
has_default: true,
default: Some(Value::Number(Number::from(3))),
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -170,7 +174,8 @@ function main(string $input1 = \"hey\", bool $input2 = false, int $input3 = 3, f
typ: Typ::Float,
has_default: true,
default: Some(Value::Number(Number::from_f64(f64::from(4.5)).unwrap())),
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -178,7 +183,8 @@ function main(string $input1 = \"hey\", bool $input2 = false, int $input3 = 3, f
typ: Typ::Resource("stripe".to_string()),
has_default: false,
default: None,
oidx: None
oidx: None,
otyp_inferred: false,
}
],
auto_kind: None,
+53 -26
View File
@@ -477,6 +477,7 @@ pub fn parse_python_signature(
has_default: has_default || default.is_some(),
default,
oidx: None,
otyp_inferred: false,
}
})
.collect(),
@@ -716,7 +717,8 @@ def main(test1: str, name: datetime.datetime = datetime.now(), byte: bytes = byt
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -724,7 +726,8 @@ def main(test1: str, name: datetime.datetime = datetime.now(), byte: bytes = byt
typ: Typ::Datetime,
default: Some(json!("<function call>")),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -732,7 +735,8 @@ def main(test1: str, name: datetime.datetime = datetime.now(), byte: bytes = byt
typ: Typ::Bytes,
default: Some(json!("<function call>")),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -740,7 +744,8 @@ def main(test1: str, name: datetime.datetime = datetime.now(), byte: bytes = byt
typ: Typ::Str(None),
default: Some(json!("wewe")),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -748,7 +753,8 @@ def main(test1: str, name: datetime.datetime = datetime.now(), byte: bytes = byt
typ: Typ::Int,
default: Some(json!(21)),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -756,7 +762,8 @@ def main(test1: str, name: datetime.datetime = datetime.now(), byte: bytes = byt
typ: Typ::List(Box::new(Typ::Int)),
default: Some(json!([1, 2])),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -764,7 +771,8 @@ def main(test1: str, name: datetime.datetime = datetime.now(), byte: bytes = byt
typ: Typ::Bool,
default: Some(json!(true)),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
],
auto_kind: None,
@@ -806,7 +814,8 @@ def main(test1: str,
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -814,7 +823,8 @@ def main(test1: str,
typ: Typ::Datetime,
default: Some(json!("<function call>")),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -822,7 +832,8 @@ def main(test1: str,
typ: Typ::Bytes,
default: Some(json!("<function call>")),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -830,7 +841,8 @@ def main(test1: str,
typ: Typ::Resource("postgresql".to_string()),
default: Some(json!("$res:g/all/resource")),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
}
],
auto_kind: None,
@@ -867,7 +879,8 @@ def main(test1: str,
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -875,7 +888,8 @@ def main(test1: str,
typ: Typ::Resource("s3_object".to_string()),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -883,7 +897,8 @@ def main(test1: str,
typ: Typ::Str(None),
default: Some(json!("test")),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -891,7 +906,8 @@ def main(test1: str,
typ: Typ::Bytes,
default: Some(json!("<function call>")),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
}
],
auto_kind: None,
@@ -925,7 +941,8 @@ def main(test1: Literal["foo", "bar"], test2: List[Literal["foo", "bar"]]): retu
typ: Typ::Str(Some(vec!["foo".to_string(), "bar".to_string()])),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -936,7 +953,8 @@ def main(test1: Literal["foo", "bar"], test2: List[Literal["foo", "bar"]]): retu
])))),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
}
],
auto_kind: None,
@@ -969,7 +987,8 @@ def main(test1: DynSelect_foo): return
typ: Typ::DynSelect("foo".to_string()),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
}],
auto_kind: None,
has_preprocessor: Some(false),
@@ -1094,7 +1113,8 @@ def main(a: list, e: List[int], b: list = [1,2,3,4], c = [1,2,3,4], d = ["a", "b
typ: Typ::List(Box::new(Typ::Str(None))),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -1102,7 +1122,8 @@ def main(a: list, e: List[int], b: list = [1,2,3,4], c = [1,2,3,4], d = ["a", "b
typ: Typ::List(Box::new(Typ::Int)),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -1110,7 +1131,8 @@ def main(a: list, e: List[int], b: list = [1,2,3,4], c = [1,2,3,4], d = ["a", "b
typ: Typ::List(Box::new(Typ::Int)),
default: Some(json!([1, 2, 3, 4])),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -1118,7 +1140,8 @@ def main(a: list, e: List[int], b: list = [1,2,3,4], c = [1,2,3,4], d = ["a", "b
typ: Typ::List(Box::new(Typ::Int)),
default: Some(json!([1, 2, 3, 4])),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -1126,7 +1149,8 @@ def main(a: list, e: List[int], b: list = [1,2,3,4], c = [1,2,3,4], d = ["a", "b
typ: Typ::List(Box::new(Typ::Str(None))),
default: Some(json!(["a", "b"])),
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
}
],
auto_kind: None,
@@ -1160,7 +1184,8 @@ def main(a: str, b: Optional[str], c: str | None): return
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -1168,7 +1193,8 @@ def main(a: str, b: Optional[str], c: str | None): return
typ: Typ::Str(None),
default: None,
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: None,
@@ -1176,7 +1202,8 @@ def main(a: str, b: Optional[str], c: str | None): return
typ: Typ::Str(None),
default: None,
has_default: true,
oidx: None
oidx: None,
otyp_inferred: false,
},
],
auto_kind: None,
@@ -21,7 +21,7 @@ pub fn parse_rust_signature(code: &str) -> anyhow::Result<MainArgSignature> {
.iter()
.map(|param| {
let (otyp, typ, name) = parse_rust_typ(param);
Arg { name, otyp, typ, default: None, has_default: false, oidx: None }
Arg { name, otyp, typ, default: None, has_default: false, oidx: None, otyp_inferred: false }
})
.collect_vec();
Ok(MainArgSignature {
+273 -18
View File
@@ -281,6 +281,7 @@ fn parse_oracledb_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
otyp: Some(typ),
has_default,
oidx: None,
otyp_inferred: false,
});
}
@@ -305,6 +306,7 @@ fn parse_oracledb_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
otyp: Some(typ),
has_default,
oidx: None,
otyp_inferred: false,
});
}
}
@@ -331,6 +333,7 @@ fn parse_sql_sanitized_interpolation(code: &str) -> Vec<Arg> {
otyp: Some(otyp.to_string()),
has_default,
oidx: None,
otyp_inferred: false,
});
}
@@ -360,6 +363,7 @@ fn parse_mysql_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
otyp: Some(typ),
has_default,
oidx: None,
otyp_inferred: false,
});
}
@@ -384,6 +388,7 @@ fn parse_mysql_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
otyp: Some(typ),
has_default,
oidx: None,
otyp_inferred: false,
});
}
}
@@ -518,7 +523,25 @@ fn run_on_sql_statement_matches<
}
pub fn parse_pg_statement_arg_indices(code: &str) -> HashSet<i32> {
let mut arg_indices = HashSet::new();
parse_pg_statement_arg_positions(code)
.into_iter()
.map(|(idx, _)| idx)
.collect()
}
/// Like `parse_pg_statement_arg_indices`, but also returns the byte range of
/// each placeholder occurrence in `code` (excluding `$`, including the digits).
/// The same string-/comment-/dollar-quote-aware tokenizer is used, so
/// occurrences inside string literals and comments are correctly skipped —
/// this is what callers need to renumber `$N → $M` without mangling literal
/// SQL bytes that happen to match the `$\d+` pattern.
///
/// The returned vec is in source order. Each entry is `(idx, range)` where
/// `idx` is the parameter number and `range` covers the `$N` digits (i.e.
/// `code[range.start - 1 .. range.end]` is the full `$N` token, and
/// `code[range]` is just the digits).
pub fn parse_pg_statement_arg_positions(code: &str) -> Vec<(i32, std::ops::Range<usize>)> {
let mut positions = Vec::new();
run_on_sql_statement_matches(
code,
true,
@@ -529,21 +552,24 @@ pub fn parse_pg_statement_arg_indices(code: &str) -> HashSet<i32> {
.is_some_and(|&(_, next_char)| next_char.is_ascii_digit())
},
|_, chars| {
let start = chars.peek().map(|&(i, _)| i).unwrap_or(0);
let mut arg_idx = String::new();
while let Some(&(_, char)) = chars.peek() {
let mut end = start;
while let Some(&(i, char)) = chars.peek() {
if char.is_ascii_digit() {
arg_idx.push(char);
end = i + char.len_utf8();
chars.next();
} else {
break;
}
}
if let Ok(arg_idx) = arg_idx.parse::<i32>() {
arg_indices.insert(arg_idx);
positions.push((arg_idx, start..end));
}
},
);
arg_indices
positions
}
fn parse_pg_file(code: &str) -> anyhow::Result<Option<(Vec<Arg>, bool)>> {
@@ -577,12 +603,16 @@ fn parse_pg_file(code: &str) -> anyhow::Result<Option<(Vec<Arg>, bool)>> {
otyp: Some(typ),
has_default,
oidx: Some(idx),
otyp_inferred: false,
});
}
}
// Second pass: infer types from usage for non-explicitly-typed args
let mut hm: HashMap<i32, String> = HashMap::new();
// Second pass: infer types from usage for non-explicitly-typed args.
// We track whether each entry came from an inline `$N::TYPE` cast or from
// the parser's "text" fallback, so the executor can later distinguish
// "user committed to text" from "no info, use a placeholder".
let mut hm: HashMap<i32, (String, bool)> = HashMap::new();
for cap in RE_CODE_PGSQL.captures_iter(code) {
let idx = cap
.get(1)
@@ -594,15 +624,23 @@ fn parse_pg_file(code: &str) -> anyhow::Result<Option<(Vec<Arg>, bool)>> {
continue;
}
let typ = cap
let cast = cap
.get(2)
.map(|cap| transform_types_with_spaces(&cap, &code))
.unwrap_or("text");
hm.insert(idx, typ.to_string());
.map(|cap| transform_types_with_spaces(&cap, &code));
let inferred_default = cast.is_none();
let typ: std::borrow::Cow<str> = cast.unwrap_or(std::borrow::Cow::Borrowed("text"));
// Prefer an explicit cast over a previously seen default — once we
// have any inline cast for the index, lock it in.
match hm.get(&idx) {
Some((_, false)) => {} // already locked from explicit cast
_ => {
hm.insert(idx, (typ.into_owned(), inferred_default));
}
}
}
// Add inferred args
for (i, v) in hm.iter() {
for (i, (v, inferred)) in hm.iter() {
let typ = v.to_lowercase();
args.push(Arg {
name: format!("${}", i),
@@ -611,6 +649,7 @@ fn parse_pg_file(code: &str) -> anyhow::Result<Option<(Vec<Arg>, bool)>> {
otyp: Some(typ),
has_default: false,
oidx: Some(*i),
otyp_inferred: *inferred,
});
}
@@ -646,6 +685,7 @@ fn parse_pg_file(code: &str) -> anyhow::Result<Option<(Vec<Arg>, bool)>> {
otyp: oarg.otyp,
has_default,
oidx: oarg.oidx,
otyp_inferred: oarg.otyp_inferred,
};
}
}
@@ -657,8 +697,12 @@ fn parse_pg_file(code: &str) -> anyhow::Result<Option<(Vec<Arg>, bool)>> {
}
// The regex doesn't parse types with space such as "character varying"
// So we look for them manually and replace them with their shorter counterpart
fn transform_types_with_spaces<'a>(cap: &Match<'a>, code: &str) -> &'a str {
// So we look for them manually and replace them with their shorter counterpart.
// Returns `Cow::Borrowed` for the trivial case (the regex's own match) and
// `Cow::Owned` when we need to alias a multi-word type and/or append a `[]`
// suffix that the regex's `\w+` capture didn't pick up.
fn transform_types_with_spaces<'a>(cap: &Match<'a>, code: &str) -> std::borrow::Cow<'a, str> {
use std::borrow::Cow;
lazy_static! {
static ref TYPES: [(&'static str, &'static str); 6] = [
("character varying", "varchar"),
@@ -671,20 +715,31 @@ fn transform_types_with_spaces<'a>(cap: &Match<'a>, code: &str) -> &'a str {
}
let typ = &code[cap.start()..];
for (long_type, alias) in TYPES.iter() {
let mut typ = typ;
let mut rest = typ;
let mut found_mismatch = false;
for token in long_type.split(' ') {
if typ.len() < token.len() || !typ[..token.len()].eq_ignore_ascii_case(token) {
if rest.len() < token.len() || !rest[..token.len()].eq_ignore_ascii_case(token) {
found_mismatch = true;
break;
}
typ = typ[token.len()..].trim_start();
rest = rest[token.len()..].trim_start();
}
if !found_mismatch {
return alias;
// The regex captured only the first word (`\w+`), so its `[]`
// detection in `(?:\[\])?` matched against the wrong position
// and is empty for multi-word types. Re-check the trailing
// bytes after the multi-word match: if they start with `[]`,
// append the array suffix to the alias so the dispatch routes
// through `convert_vec_val` instead of binding as JSONB.
let with_suffix = rest.starts_with("[]");
return if with_suffix {
Cow::Owned(format!("{alias}[]"))
} else {
Cow::Borrowed(*alias)
};
}
}
cap.as_str()
Cow::Borrowed(cap.as_str())
}
pub fn parse_sql_statement_named_params(code: &str, prefix: char) -> HashSet<String> {
@@ -736,6 +791,7 @@ fn parse_bigquery_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
otyp: Some(typ),
has_default,
oidx: None,
otyp_inferred: false,
});
}
@@ -765,6 +821,7 @@ fn parse_duckdb_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
otyp: Some(typ),
has_default,
oidx: None,
otyp_inferred: false,
});
}
@@ -794,6 +851,7 @@ fn parse_snowflake_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
otyp: Some(typ),
has_default,
oidx: None,
otyp_inferred: false,
});
}
@@ -823,6 +881,7 @@ fn parse_mssql_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
otyp: Some(typ),
has_default,
oidx: None,
otyp_inferred: false,
});
}
@@ -854,6 +913,7 @@ pub fn parse_mysql_typ(typ: &str) -> Typ {
"bool" | "bit" => Typ::Bool,
"double precision" | "float" | "real" | "dec" | "fixed" => Typ::Float,
"date" | "datetime" | "timestamp" | "time" => Typ::Datetime,
"s3object" => Typ::Resource("S3Object".to_string()),
_ => Typ::Str(None),
}
}
@@ -901,6 +961,7 @@ pub fn parse_pg_typ(typ: &str) -> Typ {
| "timestamp with time zone"
| "timestamp without time zone" => Typ::Datetime,
"bytea" => Typ::Bytes,
"s3object" => Typ::Resource("S3Object".to_string()),
_ => Typ::Str(None),
}
}
@@ -919,6 +980,7 @@ pub fn parse_bigquery_typ(typ: &str) -> Typ {
"integer" | "int64" => Typ::Int,
"float" | "float64" | "numeric" | "bignumeric" => Typ::Float,
"boolean" | "bool" => Typ::Bool,
"s3object" => Typ::Resource("S3Object".to_string()),
_ => Typ::Str(None),
}
}
@@ -959,6 +1021,7 @@ pub fn parse_snowflake_typ(typ: &str) -> Typ {
"int" => Typ::Int,
"float" => Typ::Float,
"boolean" => Typ::Bool,
"s3object" => Typ::Resource("S3Object".to_string()),
_ => Typ::Str(None),
}
}
@@ -973,6 +1036,7 @@ pub fn parse_mssql_typ(typ: &str) -> Typ {
"bigint" | "int" | "tinyint" | "smallint" => Typ::Int,
"float" | "real" | "numeric" | "decimal" => Typ::Float,
"bit" => Typ::Bool,
"s3object" => Typ::Resource("S3Object".to_string()),
_ => Typ::Str(None),
}
}
@@ -1001,6 +1065,7 @@ SELECT * FROM table WHERE token=$1::TEXT AND image=$2::BIGINT
default: None,
has_default: false,
oidx: Some(1),
otyp_inferred: false,
},
Arg {
otyp: Some("bigint".to_string()),
@@ -1009,6 +1074,7 @@ SELECT * FROM table WHERE token=$1::TEXT AND image=$2::BIGINT
default: None,
has_default: false,
oidx: Some(2),
otyp_inferred: false,
},
],
auto_kind: None,
@@ -1043,6 +1109,7 @@ SELECT $2::TEXT;
default: None,
has_default: false,
oidx: Some(1),
otyp_inferred: false,
},
Arg {
otyp: Some("text".to_string()),
@@ -1051,6 +1118,7 @@ SELECT $2::TEXT;
default: None,
has_default: false,
oidx: Some(2),
otyp_inferred: false,
},
Arg {
otyp: Some("text".to_string()),
@@ -1059,6 +1127,7 @@ SELECT $2::TEXT;
default: None,
has_default: false,
oidx: Some(3),
otyp_inferred: false,
},
],
auto_kind: None,
@@ -1211,6 +1280,54 @@ SELECT $2;"#;
Ok(())
}
#[test]
fn test_parse_pg_statement_arg_positions_skips_strings_and_comments() -> anyhow::Result<()> {
// Each occurrence's byte range covers JUST the digits (after `$`).
let code = "SELECT $5, $50";
let positions = parse_pg_statement_arg_positions(code);
let collected: Vec<(i32, &str)> = positions
.iter()
.map(|(idx, range)| (*idx, &code[range.clone()]))
.collect();
assert_eq!(collected, vec![(5, "5"), (50, "50")]);
// String literals and comments must not produce positions — this is
// what stops the do_postgresql_inner rewrite from mangling SQL like
// `'price: $5'`.
let code = "SELECT 'literal $5' AS lbl, $5 FROM t -- mention $5";
let positions = parse_pg_statement_arg_positions(code);
let positions_only: Vec<(i32, std::ops::Range<usize>)> = positions.clone();
assert_eq!(
positions_only.iter().map(|(i, _)| *i).collect::<Vec<_>>(),
vec![5],
"only the real $5 between 'lbl,' and 'FROM' should be returned"
);
// The single returned position is the real placeholder (between
// `lbl, ` and ` FROM`).
let (idx, range) = &positions[0];
assert_eq!(*idx, 5);
// `code[range.start - 1 .. range.end]` should be the full `$5` token.
assert_eq!(&code[range.start - 1..range.end], "$5");
// Dollar-quoted blocks similarly skipped.
let code = "SELECT $$body with $5 inside$$, $7 FROM t";
let positions = parse_pg_statement_arg_positions(code);
assert_eq!(
positions.iter().map(|(i, _)| *i).collect::<Vec<_>>(),
vec![7],
"$5 inside $$...$$ is part of the string"
);
// Repeat indices show up multiple times — caller can rewrite each.
let code = "SELECT $1, $1, $2";
let positions = parse_pg_statement_arg_positions(code);
assert_eq!(
positions.iter().map(|(i, _)| *i).collect::<Vec<_>>(),
vec![1, 1, 2]
);
Ok(())
}
#[test]
fn test_parse_sql_blocks_non_pg_ignores_dollar_quotes() -> anyhow::Result<()> {
// Non-Postgres dialects (MySQL/Oracle/BigQuery/Snowflake) pass `false`,
@@ -1254,6 +1371,7 @@ SELECT ?, ?;
default: Some(json!(3)),
has_default: true,
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: Some("text".to_string()),
@@ -1262,6 +1380,7 @@ SELECT ?, ?;
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},
],
auto_kind: None,
@@ -1295,6 +1414,7 @@ SELECT :param2;
default: Some(json!(3)),
has_default: true,
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: Some("text".to_string()),
@@ -1303,6 +1423,7 @@ SELECT :param2;
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: Some("text".to_string()),
@@ -1311,6 +1432,7 @@ SELECT :param2;
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},
],
auto_kind: None,
@@ -1344,6 +1466,7 @@ SELECT @token;
default: Some(json!("abc")),
has_default: true,
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: Some("int64".to_string()),
@@ -1352,6 +1475,7 @@ SELECT @token;
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},
],
auto_kind: None,
@@ -1385,6 +1509,7 @@ SELECT ?;
default: Some(json!(3)),
has_default: true,
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: Some("varchar".to_string()),
@@ -1393,6 +1518,7 @@ SELECT ?;
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: Some("varchar".to_string()),
@@ -1401,6 +1527,7 @@ SELECT ?;
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
}
],
auto_kind: None,
@@ -1434,6 +1561,7 @@ SELECT @P2;
default: Some(json!(3)),
has_default: true,
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: Some("varchar".to_string()),
@@ -1442,6 +1570,7 @@ SELECT @P2;
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: Some("varchar".to_string()),
@@ -1450,6 +1579,7 @@ SELECT @P2;
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},
],
auto_kind: None,
@@ -1484,6 +1614,7 @@ SELECT * FROM table_name WHERE thing = :name4;
default: Some(json!(3)),
has_default: true,
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: Some("text".to_string()),
@@ -1492,6 +1623,7 @@ SELECT * FROM table_name WHERE thing = :name4;
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},
Arg {
otyp: Some("text".to_string()),
@@ -1500,6 +1632,7 @@ SELECT * FROM table_name WHERE thing = :name4;
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},
],
auto_kind: None,
@@ -1531,6 +1664,7 @@ SELECT * FROM users WHERE id = $1 AND email = $2::text;
default: None,
has_default: false,
oidx: Some(1),
otyp_inferred: false,
},
Arg {
otyp: Some("text".to_string()),
@@ -1539,6 +1673,7 @@ SELECT * FROM users WHERE id = $1 AND email = $2::text;
default: None,
has_default: false,
oidx: Some(2),
otyp_inferred: false,
},
],
auto_kind: None,
@@ -1570,6 +1705,7 @@ SELECT * FROM users LIMIT $1 OFFSET $2;
default: Some(json!(10)),
has_default: true,
oidx: Some(1),
otyp_inferred: false,
},
Arg {
otyp: Some("bigint".to_string()),
@@ -1578,6 +1714,7 @@ SELECT * FROM users LIMIT $1 OFFSET $2;
default: Some(json!(0)),
has_default: true,
oidx: Some(2),
otyp_inferred: false,
},
],
auto_kind: None,
@@ -1613,6 +1750,7 @@ WHERE id = $1
default: None,
has_default: false,
oidx: Some(1),
otyp_inferred: false,
},
Arg {
otyp: Some("text".to_string()),
@@ -1621,6 +1759,7 @@ WHERE id = $1
default: None,
has_default: false,
oidx: Some(2),
otyp_inferred: false,
},
Arg {
otyp: Some("timestamptz".to_string()),
@@ -1629,6 +1768,7 @@ WHERE id = $1
default: None,
has_default: false,
oidx: Some(3),
otyp_inferred: false,
},
],
auto_kind: None,
@@ -1658,6 +1798,7 @@ SELECT * FROM users WHERE id = ANY($1);
default: None,
has_default: false,
oidx: Some(1),
otyp_inferred: false,
},],
auto_kind: None,
has_preprocessor: None,
@@ -1688,6 +1829,7 @@ SELECT $1::integer;
default: None,
has_default: false,
oidx: Some(1),
otyp_inferred: false,
},],
auto_kind: None,
has_preprocessor: None,
@@ -1698,6 +1840,118 @@ SELECT $1::integer;
Ok(())
}
#[test]
fn test_parse_pgsql_otyp_inferred_flag() -> anyhow::Result<()> {
// Bare `$N` (no inline cast, no decl) should produce otyp = "text"
// *and* otyp_inferred = true. This is the signal the PG executor
// uses to decide whether the user committed to a text target.
let code_bare = "SELECT $1, $2";
let args = parse_pgsql_sig(code_bare)?.args;
let map: HashMap<String, (Option<String>, bool)> = args
.into_iter()
.map(|a| (a.name, (a.otyp, a.otyp_inferred)))
.collect();
assert_eq!(
map.get("$1").cloned(),
Some((Some("text".to_string()), true)),
"bare $1 → otyp_inferred true"
);
assert_eq!(
map.get("$2").cloned(),
Some((Some("text".to_string()), true)),
"bare $2 → otyp_inferred true"
);
// Inline `$N::TYPE` cast → otyp_inferred = false (user committed).
let args = parse_pgsql_sig("SELECT $1::int, $2::text")?.args;
let map: HashMap<String, (Option<String>, bool)> = args
.into_iter()
.map(|a| (a.name, (a.otyp, a.otyp_inferred)))
.collect();
assert_eq!(
map.get("$1").cloned(),
Some((Some("int".to_string()), false))
);
assert_eq!(
map.get("$2").cloned(),
Some((Some("text".to_string()), false)),
"explicit $2::text → otyp_inferred false (distinct from bare $2)"
);
// Declaration `-- $N name (TYPE)` → otyp_inferred = false (decl is
// explicit by definition).
let args = parse_pgsql_sig("-- $1 name (text)\nSELECT $1")?.args;
assert_eq!(args[0].otyp.as_deref(), Some("text"));
assert!(!args[0].otyp_inferred);
// Mixed: $1 has decl, $2 is bare → flag differs per arg.
let args = parse_pgsql_sig("-- $1 a (int)\nSELECT $1, $2")?.args;
let map: HashMap<String, bool> = args
.into_iter()
.map(|a| (a.name, a.otyp_inferred))
.collect();
assert_eq!(map.get("a").copied(), Some(false), "$1 decl → not inferred");
assert_eq!(map.get("$2").copied(), Some(true), "$2 bare → inferred");
Ok(())
}
#[test]
fn test_parse_s3object_arg_per_dialect() -> anyhow::Result<()> {
// Confirms that `(s3object)` is recognised as a resource-typed arg in every
// native SQL dialect that opts in (PG, MySQL, MSSQL, BigQuery, Snowflake).
// The frontend uses `Typ::Resource("S3Object")` to render the S3 picker, and
// the worker dispatches on `otyp == "s3object"` to fetch + bind the file.
let s3 = || Typ::Resource("S3Object".to_string());
assert_eq!(
parse_pgsql_sig("-- $1 myfile (s3object)\nSELECT $1::jsonb;")?
.args
.into_iter()
.map(|a| (a.name, a.typ, a.otyp))
.collect::<Vec<_>>(),
vec![("myfile".to_string(), s3(), Some("s3object".to_string()))]
);
assert_eq!(
parse_mssql_sig("-- @P1 myfile (s3object)\nSELECT @P1;")?
.args
.into_iter()
.map(|a| (a.name, a.typ, a.otyp))
.collect::<Vec<_>>(),
vec![("myfile".to_string(), s3(), Some("s3object".to_string()))]
);
assert_eq!(
parse_mysql_sig("-- :myfile (s3object)\nSELECT :myfile;")?
.args
.into_iter()
.map(|a| (a.name, a.typ, a.otyp))
.collect::<Vec<_>>(),
vec![("myfile".to_string(), s3(), Some("s3object".to_string()))]
);
assert_eq!(
parse_bigquery_sig("-- @myfile (s3object)\nSELECT @myfile;")?
.args
.into_iter()
.map(|a| (a.name, a.typ, a.otyp))
.collect::<Vec<_>>(),
vec![("myfile".to_string(), s3(), Some("s3object".to_string()))]
);
assert_eq!(
parse_snowflake_sig("-- ? myfile (s3object)\nSELECT ?;")?
.args
.into_iter()
.map(|a| (a.name, a.typ, a.otyp))
.collect::<Vec<_>>(),
vec![("myfile".to_string(), s3(), Some("s3object".to_string()))]
);
Ok(())
}
#[test]
fn test_parse_pgsql_safe_interpolated_args() -> anyhow::Result<()> {
// There was a bug where enum would be "angrycreative"/"bishop"/"test SELECT x"
@@ -1721,6 +1975,7 @@ SELECT x
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},],
auto_kind: None,
has_preprocessor: None,
+20 -5
View File
@@ -208,9 +208,7 @@ pub fn parse_relative_imports(code: &str, path: &str) -> anyhow::Result<Vec<Stri
/// Check if an import path is a relative import (starts with `./`, `../`, or `/`)
fn is_relative_import(import_path: &str) -> bool {
import_path.starts_with("./")
|| import_path.starts_with("../")
|| import_path.starts_with("/")
import_path.starts_with("./") || import_path.starts_with("../") || import_path.starts_with("/")
}
/// Normalize a path by resolving `.` and `..` components
@@ -542,6 +540,7 @@ fn parse_param(
default: None,
has_default: ident.id.optional || nullable,
oidx: None,
otyp_inferred: false,
})
}
// Pat::Object(ObjectPat { ... }) = todo!()
@@ -596,13 +595,29 @@ fn parse_param(
if typ == Typ::Unknown && dflt.is_some() {
typ = json_to_typ(dflt.as_ref().unwrap(), false);
}
Ok(Arg { otyp, name, typ, default: dflt, has_default: true, oidx: None })
Ok(Arg {
otyp,
name,
typ,
default: dflt,
has_default: true,
oidx: None,
otyp_inferred: false,
})
}
Pat::Object(ObjectPat { type_ann, .. }) => {
let (typ, nullable) = eval_type_ann(symbol_table, type_resolver, &type_ann);
*counter += 1;
let name = format!("anon{}", counter);
Ok(Arg { otyp: None, name, typ, default: None, has_default: nullable, oidx: None })
Ok(Arg {
otyp: None,
name,
typ,
default: None,
has_default: nullable,
oidx: None,
otyp_inferred: false,
})
}
_ => Err(anyhow::anyhow!(
"parameter syntax unsupported: `{}`: {:#?}",
@@ -2,7 +2,9 @@
mod tests {
use serde_json::json;
use windmill_parser::{Arg, MainArgSignature, ObjectProperty, ObjectType, Typ};
use windmill_parser_ts::{parse_deno_signature, parse_expr_for_imports, parse_relative_imports};
use windmill_parser_ts::{
parse_deno_signature, parse_expr_for_imports, parse_relative_imports,
};
#[test]
fn test_imports_basic() {
@@ -78,6 +80,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},
Arg {
name: "num_param".to_string(),
@@ -86,6 +89,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},
Arg {
name: "bool_param".to_string(),
@@ -94,6 +98,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},
Arg {
name: "any_param".to_string(),
@@ -102,6 +107,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},
],
auto_kind: None,
@@ -136,6 +142,7 @@ mod tests {
default: Some(json!("World")),
has_default: true,
oidx: None,
otyp_inferred: false,
},
Arg {
name: "count".to_string(),
@@ -144,6 +151,7 @@ mod tests {
default: Some(json!(42)),
has_default: true,
oidx: None,
otyp_inferred: false,
},
Arg {
name: "enabled".to_string(),
@@ -152,6 +160,7 @@ mod tests {
default: Some(json!(true)),
has_default: true,
oidx: None,
otyp_inferred: false,
},
],
auto_kind: None,
@@ -186,6 +195,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},
Arg {
name: "numbers".to_string(),
@@ -194,6 +204,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},
Arg {
name: "items".to_string(),
@@ -202,6 +213,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},
],
auto_kind: None,
@@ -235,6 +247,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},],
auto_kind: None,
has_preprocessor: Some(false),
@@ -267,6 +280,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},],
auto_kind: None,
has_preprocessor: Some(false),
@@ -308,6 +322,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
}],
auto_kind: None,
has_preprocessor: Some(false),
@@ -347,6 +362,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
}],
auto_kind: None,
has_preprocessor: Some(false),
@@ -406,6 +422,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
}],
auto_kind: None,
has_preprocessor: Some(false),
@@ -440,6 +457,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},
Arg {
name: "base64_param".to_string(),
@@ -448,6 +466,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},
Arg {
name: "email_param".to_string(),
@@ -456,6 +475,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},
Arg {
name: "sql_param".to_string(),
@@ -464,6 +484,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},
],
auto_kind: None,
@@ -498,6 +519,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},
Arg {
name: "optional".to_string(),
@@ -506,6 +528,7 @@ mod tests {
default: None,
has_default: true,
oidx: None,
otyp_inferred: false,
},
Arg {
name: "with_default".to_string(),
@@ -514,6 +537,7 @@ mod tests {
default: Some(json!(false)),
has_default: true,
oidx: None,
otyp_inferred: false,
},
],
auto_kind: None,
@@ -593,6 +617,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
}],
auto_kind: None,
has_preprocessor: Some(false),
@@ -623,6 +648,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
}],
auto_kind: None,
has_preprocessor: Some(false),
@@ -653,6 +679,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
}],
auto_kind: None,
has_preprocessor: Some(false),
@@ -686,6 +713,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},
Arg {
name: "numbers".to_string(),
@@ -694,6 +722,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},
Arg {
name: "plain".to_string(),
@@ -702,6 +731,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
},
],
auto_kind: None,
@@ -744,6 +774,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
}],
auto_kind: None,
has_preprocessor: Some(true),
@@ -775,6 +806,7 @@ mod tests {
default: None,
has_default: false,
oidx: None,
otyp_inferred: false,
}],
auto_kind: None,
has_preprocessor: Some(true),
+24 -24
View File
@@ -6183,7 +6183,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill-common"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"aho-corasick",
"anyhow",
@@ -6263,7 +6263,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"proc-macro2",
"quote",
@@ -6275,7 +6275,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"convert_case",
"serde",
@@ -6284,7 +6284,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -6296,7 +6296,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"serde_json",
@@ -6308,7 +6308,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"gosyn",
@@ -6320,7 +6320,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -6332,7 +6332,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"serde_json",
@@ -6344,7 +6344,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"nu-parser",
@@ -6355,7 +6355,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -6366,7 +6366,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -6378,7 +6378,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-asset"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -6389,7 +6389,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -6411,7 +6411,7 @@ dependencies = [
[[package]]
name = "windmill-parser-r"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"serde_json",
@@ -6423,7 +6423,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -6437,7 +6437,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"convert_case",
@@ -6454,7 +6454,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -6467,7 +6467,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql-asset"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"serde",
@@ -6479,7 +6479,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -6497,7 +6497,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts-asset"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"serde-wasm-bindgen",
@@ -6513,7 +6513,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wac"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -6529,7 +6529,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wasm"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"getrandom 0.2.17",
@@ -6561,7 +6561,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"serde",
@@ -6572,7 +6572,7 @@ dependencies = [
[[package]]
name = "windmill-types"
version = "1.692.0"
version = "1.694.0"
dependencies = [
"anyhow",
"bitflags",
@@ -12,7 +12,7 @@ resolver = "2"
members = ["."]
[workspace.package]
version = "1.692.0"
version = "1.694.0"
edition = "2021"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
@@ -50,6 +50,7 @@ pub fn parse_ansible_sig(inner_content: &str) -> anyhow::Result<MainArgSignature
has_default: default.is_some(),
default,
oidx: None,
otyp_inferred: false,
})
}
}
@@ -68,6 +69,7 @@ pub fn parse_ansible_sig(inner_content: &str) -> anyhow::Result<MainArgSignature
has_default: inv.default.is_some(),
default: inv.default.map(|v| json!(format!("$res:{}", v))),
oidx: None,
otyp_inferred: false,
});
}
}
@@ -81,6 +83,7 @@ pub fn parse_ansible_sig(inner_content: &str) -> anyhow::Result<MainArgSignature
has_default: false,
default: None,
oidx: None,
otyp_inferred: false,
});
}
}
@@ -215,6 +218,7 @@ pub struct AnsiblePlaybookOptions {
pub timeout: Option<i64>,
pub flush_cache: Option<()>,
pub force_handlers: Option<()>,
pub limit: Option<String>,
}
#[derive(Debug, Clone)]
@@ -265,6 +269,8 @@ pub struct DelegateToGitRepoDetails {
pub commit: Option<String>,
pub inventories_location: Option<String>,
pub vars_location: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub install_requirements: bool,
}
#[derive(Debug, Clone)]
@@ -300,6 +306,7 @@ impl Default for AnsibleRequirements {
timeout: None,
flush_cache: None,
force_handlers: None,
limit: None,
},
vault_password: None,
vault_id: vec![],
@@ -602,6 +609,10 @@ fn extract_delegate_to_git_repo_details(value: &Yaml) -> Option<DelegateToGitRep
.get(&Yaml::String("vars_location".to_string()))
.and_then(|s| s.as_str())
.map(|s| s.to_string());
let install_requirements = v
.get(&Yaml::String("install_requirements".to_string()))
.and_then(|s| s.as_bool())
.unwrap_or(false);
return Some(DelegateToGitRepoDetails {
resource,
@@ -609,6 +620,7 @@ fn extract_delegate_to_git_repo_details(value: &Yaml) -> Option<DelegateToGitRep
commit,
inventories_location,
vars_location,
install_requirements,
});
}
}
@@ -654,6 +666,7 @@ fn parse_ansible_options(opts: &Vec<Yaml>) -> AnsiblePlaybookOptions {
timeout: None,
flush_cache: None,
force_handlers: None,
limit: None,
};
for opt in opts {
if let Yaml::String(o) = opt {
@@ -691,6 +704,13 @@ fn parse_ansible_options(opts: &Vec<Yaml>) -> AnsiblePlaybookOptions {
}
}
}
"limit" => {
if let Yaml::String(limit) = value {
if !limit.is_empty() {
ret.limit = Some(limit.clone());
}
}
}
_ => (),
}
}
@@ -972,4 +992,63 @@ dependencies:
let a = parse_delegate_to_git_repo(p).unwrap();
println!("The resulting delegate_to_kit_repo is: {:#?}", a);
}
#[test]
fn test_parse_options_limit() {
let p = r#"
---
options:
- vv
- limit: webservers:!db1.example.com
- forks: 5
---
- name: Test
hosts: all
"#;
let (_, reqs, _) = parse_ansible_reqs(p).unwrap();
let opts = reqs.unwrap().options;
assert_eq!(opts.limit.as_deref(), Some("webservers:!db1.example.com"));
assert_eq!(opts.verbosity.as_deref(), Some("vv"));
assert_eq!(opts.forks, Some(5));
}
#[test]
fn test_parse_install_requirements_default_false() {
let p = r#"
---
delegate_to_git_repo:
resource: u/admin/repo
playbook: site.yml
---
- name: Test
hosts: all
"#;
let (_, reqs, _) = parse_ansible_reqs(p).unwrap();
let d = reqs.unwrap().delegate_to_git_repo.unwrap();
assert!(!d.install_requirements);
assert_eq!(d.playbook.as_deref(), Some("site.yml"));
}
#[test]
fn test_parse_install_requirements_true() {
let p = r#"
---
delegate_to_git_repo:
resource: u/admin/repo
playbook: "{{ playbook_name }}"
inventories_location: "inventories/{{ env }}"
install_requirements: true
---
- name: Test
hosts: all
"#;
let (_, reqs, _) = parse_ansible_reqs(p).unwrap();
let d = reqs.unwrap().delegate_to_git_repo.unwrap();
assert!(d.install_requirements);
assert_eq!(d.playbook.as_deref(), Some("{{ playbook_name }}"));
assert_eq!(
d.inventories_location.as_deref(),
Some("inventories/{{ env }}")
);
}
}
@@ -106,6 +106,15 @@ pub struct Arg {
pub default: Option<serde_json::Value>,
pub has_default: bool,
pub oidx: Option<i32>,
/// `true` when `otyp` is the parser's fallback default rather than a value
/// the user (or SDK) actually wrote down. Currently only set by the PG SQL
/// parser when a placeholder has no `-- $N name (TYPE)` declaration *and*
/// no `$N::TYPE` inline cast — the otyp is `"text"` purely as a
/// placeholder. Consumers that care about original intent (e.g. the PG
/// executor deciding whether to coerce `Number → String` for a text
/// target) should treat `otyp_inferred = true` as "type unknown".
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub otyp_inferred: bool,
}
pub fn json_to_typ(js: &Value, precise_arrays: bool) -> Typ {
+1
View File
@@ -22,6 +22,7 @@ fn bun_code(code: &str) -> RawCode {
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
tag: None,
}
}
+29
View File
@@ -35,6 +35,7 @@ export function main() {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = run_job_in_new_worker_until_complete(&db, false, job, port)
@@ -72,6 +73,7 @@ export function main(name: string, count: number) {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = RunJob::from(job)
@@ -115,6 +117,7 @@ export function main() {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = run_job_in_new_worker_until_complete(&db, false, job, port)
@@ -148,6 +151,7 @@ export function main() {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = run_job_in_new_worker_until_complete(&db, false, job, port)
@@ -182,6 +186,7 @@ export function main() {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = run_job_in_new_worker_until_complete(&db, false, job, port)
@@ -223,6 +228,7 @@ export async function main() {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = run_job_in_new_worker_until_complete(&db, false, job, port)
@@ -263,6 +269,7 @@ export function main() {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = run_job_in_new_worker_until_complete(&db, false, job, port)
@@ -296,6 +303,7 @@ export function main() {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = run_job_in_new_worker_until_complete(&db, false, job, port)
@@ -339,6 +347,7 @@ export function main() {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let completed = run_job_in_new_worker_until_complete(&db, false, job, port).await;
@@ -380,6 +389,7 @@ export function notMain() {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let completed = run_job_in_new_worker_until_complete(&db, false, job, port).await;
@@ -421,6 +431,7 @@ export function main() {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let completed = run_job_in_new_worker_until_complete(&db, false, job, port).await;
@@ -459,6 +470,7 @@ export async function main() {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let completed = run_job_in_new_worker_until_complete(&db, false, job, port).await;
@@ -514,6 +526,7 @@ export function main() {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = run_job_in_new_worker_until_complete(&db, false, job, port)
@@ -552,6 +565,7 @@ export function main() {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = run_job_in_new_worker_until_complete(&db, false, job, port)
@@ -595,6 +609,7 @@ export function main() {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = run_job_in_new_worker_until_complete(&db, false, job, port)
@@ -694,6 +709,7 @@ export function main() {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = run_job_in_new_worker_until_complete(&db, false, job, port)
@@ -730,6 +746,7 @@ export function main() {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = run_job_in_new_worker_until_complete(&db, false, job, port)
@@ -777,6 +794,7 @@ export function main() {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = run_job_in_new_worker_until_complete(&db, false, job, port)
@@ -826,6 +844,7 @@ export function main(x: number) {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
// x=5, main adds 10 = 15
@@ -877,6 +896,7 @@ export function main() {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = run_job_in_new_worker_until_complete(&db, false, job, port)
@@ -923,6 +943,7 @@ export function main() {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = run_job_in_new_worker_until_complete(&db, false, job, port)
@@ -939,6 +960,7 @@ export function main() {
// Dedicated Worker Protocol Tests
// ============================================================================
#[cfg(feature = "private")]
mod dedicated_worker_protocol {
use std::io::{BufRead, BufReader, Write};
use std::process::{Command, Stdio};
@@ -1647,6 +1669,7 @@ export function main(x?: number): string {
// Deno Dedicated Worker Protocol Tests
// ============================================================================
#[cfg(feature = "private")]
mod dedicated_worker_protocol_deno {
use std::io::{BufRead, BufReader, Write};
use std::process::{Command, Stdio};
@@ -1963,6 +1986,7 @@ export function main(name: string) {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = RunJob::from(job)
@@ -2027,6 +2051,7 @@ export function main(name: string) {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = RunJob::from(job)
@@ -2304,6 +2329,7 @@ module.exports.main = function() {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = RunJob::from(job)
@@ -2346,6 +2372,7 @@ export function main() {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = RunJob::from(job)
@@ -2397,6 +2424,7 @@ module.exports.main = function() {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
use std::sync::atomic::Ordering;
@@ -2458,6 +2486,7 @@ export function main() {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
use std::sync::atomic::Ordering;
+96 -44
View File
@@ -63,10 +63,7 @@ fn authed(builder: reqwest::RequestBuilder, token: &str) -> reqwest::RequestBuil
/// Create an app with inline script via API
async fn create_app_with_inline_script(port: u16, path: &str) -> anyhow::Result<()> {
let url = format!(
"http://localhost:{}/api/w/test-workspace/apps/create",
port
);
let url = format!("http://localhost:{}/api/w/test-workspace/apps/create", port);
let resp = authed(client().post(&url), SAME_WS_TOKEN)
.json(&json!({
"path": path,
@@ -102,17 +99,18 @@ async fn create_app_with_inline_script(port: u16, path: &str) -> anyhow::Result<
.send()
.await?;
if !resp.status().is_success() {
anyhow::bail!("create app failed: {} - {}", resp.status(), resp.text().await?);
anyhow::bail!(
"create app failed: {} - {}",
resp.status(),
resp.text().await?
);
}
Ok(())
}
/// Create a raw app with inline script via API (uses regular app endpoint with rawapp type)
async fn create_raw_app_with_inline_script(port: u16, path: &str) -> anyhow::Result<()> {
let url = format!(
"http://localhost:{}/api/w/test-workspace/apps/create",
port
);
let url = format!("http://localhost:{}/api/w/test-workspace/apps/create", port);
let resp = authed(client().post(&url), SAME_WS_TOKEN)
.json(&json!({
"path": path,
@@ -146,12 +144,21 @@ async fn create_raw_app_with_inline_script(port: u16, path: &str) -> anyhow::Res
.send()
.await?;
if !resp.status().is_success() {
anyhow::bail!("create raw app failed: {} - {}", resp.status(), resp.text().await?);
anyhow::bail!(
"create raw app failed: {} - {}",
resp.status(),
resp.text().await?
);
}
Ok(())
}
async fn run_app_inline_script(port: u16, token: &str, app_path: &str, force_viewer: bool) -> anyhow::Result<String> {
async fn run_app_inline_script(
port: u16,
token: &str,
app_path: &str,
force_viewer: bool,
) -> anyhow::Result<String> {
let url = format!(
"http://localhost:{}/api/w/test-workspace/apps_u/execute_component/{}",
port, app_path
@@ -173,13 +180,22 @@ async fn run_app_inline_script(port: u16, token: &str, app_path: &str, force_vie
.send()
.await?;
if !resp.status().is_success() {
anyhow::bail!("app inline script run failed: {} - {}", resp.status(), resp.text().await?);
anyhow::bail!(
"app inline script run failed: {} - {}",
resp.status(),
resp.text().await?
);
}
let job_id = resp.text().await?;
wait_for_job_result(port, token, &job_id).await
}
async fn run_raw_app_inline_script(port: u16, token: &str, app_path: &str, force_viewer: bool) -> anyhow::Result<String> {
async fn run_raw_app_inline_script(
port: u16,
token: &str,
app_path: &str,
force_viewer: bool,
) -> anyhow::Result<String> {
let url = format!(
"http://localhost:{}/api/w/test-workspace/apps_u/execute_component/{}",
port, app_path
@@ -200,7 +216,11 @@ async fn run_raw_app_inline_script(port: u16, token: &str, app_path: &str, force
.send()
.await?;
if !resp.status().is_success() {
anyhow::bail!("raw app inline script run failed: {} - {}", resp.status(), resp.text().await?);
anyhow::bail!(
"raw app inline script run failed: {} - {}",
resp.status(),
resp.text().await?
);
}
let job_id = resp.text().await?;
wait_for_job_result(port, token, &job_id).await
@@ -215,8 +235,12 @@ async fn wait_for_job_result(port: u16, token: &str, job_id: &str) -> anyhow::Re
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
let resp = authed(client().get(&url), token).send().await?;
if resp.status().is_success() {
return Ok(resp.json::<serde_json::Value>().await?
.as_str().unwrap_or("").to_string());
return Ok(resp
.json::<serde_json::Value>()
.await?
.as_str()
.unwrap_or("")
.to_string());
}
}
anyhow::bail!("timeout waiting for job result")
@@ -268,24 +292,38 @@ async fn test_app_wm_end_user_email(db: Pool<Postgres>) -> anyhow::Result<()> {
let app_path = "f/test/email_app";
in_test_worker(Connection::Sql(db.clone()), async move {
// Create the app with inline script first
create_app_with_inline_script(port, app_path).await?;
in_test_worker(
Connection::Sql(db.clone()),
async move {
// Create the app with inline script first
create_app_with_inline_script(port, app_path).await?;
// Same workspace user (force_viewer mode works for workspace members)
let result = run_app_inline_script(port, SAME_WS_TOKEN, app_path, true).await?;
assert_eq!(result, SAME_WS_EMAIL, "same workspace user should get their email");
// Same workspace user (force_viewer mode works for workspace members)
let result = run_app_inline_script(port, SAME_WS_TOKEN, app_path, true).await?;
assert_eq!(
result, SAME_WS_EMAIL,
"same workspace user should get their email"
);
// Other workspace user (uses app's anonymous policy + token lookup)
let result = run_app_inline_script(port, OTHER_WS_TOKEN, app_path, false).await?;
assert_eq!(result, OTHER_WS_EMAIL, "other workspace user should get their email");
// Other workspace user (uses app's anonymous policy + token lookup)
let result = run_app_inline_script(port, OTHER_WS_TOKEN, app_path, false).await?;
assert_eq!(
result, OTHER_WS_EMAIL,
"other workspace user should get their email"
);
// No workspace user (uses app's anonymous policy + token lookup)
let result = run_app_inline_script(port, NO_WS_TOKEN, app_path, false).await?;
assert_eq!(result, NO_WS_EMAIL, "no workspace user should get their email");
// No workspace user (uses app's anonymous policy + token lookup)
let result = run_app_inline_script(port, NO_WS_TOKEN, app_path, false).await?;
assert_eq!(
result, NO_WS_EMAIL,
"no workspace user should get their email"
);
Ok::<(), anyhow::Error>(())
}, port).await?;
Ok::<(), anyhow::Error>(())
},
port,
)
.await?;
Ok(())
}
@@ -300,24 +338,38 @@ async fn test_raw_app_wm_end_user_email(db: Pool<Postgres>) -> anyhow::Result<()
let app_path = "f/test/email_raw_app";
in_test_worker(Connection::Sql(db.clone()), async move {
// Create the raw app with inline script first
create_raw_app_with_inline_script(port, app_path).await?;
in_test_worker(
Connection::Sql(db.clone()),
async move {
// Create the raw app with inline script first
create_raw_app_with_inline_script(port, app_path).await?;
// Same workspace user (force_viewer mode works for workspace members)
let result = run_raw_app_inline_script(port, SAME_WS_TOKEN, app_path, true).await?;
assert_eq!(result, SAME_WS_EMAIL, "same workspace user should get their email");
// Same workspace user (force_viewer mode works for workspace members)
let result = run_raw_app_inline_script(port, SAME_WS_TOKEN, app_path, true).await?;
assert_eq!(
result, SAME_WS_EMAIL,
"same workspace user should get their email"
);
// Other workspace user (uses app's anonymous policy + token lookup)
let result = run_raw_app_inline_script(port, OTHER_WS_TOKEN, app_path, false).await?;
assert_eq!(result, OTHER_WS_EMAIL, "other workspace user should get their email");
// Other workspace user (uses app's anonymous policy + token lookup)
let result = run_raw_app_inline_script(port, OTHER_WS_TOKEN, app_path, false).await?;
assert_eq!(
result, OTHER_WS_EMAIL,
"other workspace user should get their email"
);
// No workspace user (uses app's anonymous policy + token lookup)
let result = run_raw_app_inline_script(port, NO_WS_TOKEN, app_path, false).await?;
assert_eq!(result, NO_WS_EMAIL, "no workspace user should get their email");
// No workspace user (uses app's anonymous policy + token lookup)
let result = run_raw_app_inline_script(port, NO_WS_TOKEN, app_path, false).await?;
assert_eq!(
result, NO_WS_EMAIL,
"no workspace user should get their email"
);
Ok::<(), anyhow::Error>(())
}, port).await?;
Ok::<(), anyhow::Error>(())
},
port,
)
.await?;
Ok(())
}
+7 -7
View File
@@ -34,10 +34,7 @@ async fn test_error_handler_settings(db: Pool<Postgres>) -> anyhow::Result<()> {
)
.fetch_one(&db)
.await?;
assert_eq!(
after_set,
Some("script/f/test/error_handler".to_string())
);
assert_eq!(after_set, Some("script/f/test/error_handler".to_string()));
// Verify extra_args
let extra_args = sqlx::query_scalar!(
@@ -162,7 +159,8 @@ export async function main(path: string, email: string, job_id: string, is_flow:
priority: None,
apply_preprocessor: false,
concurrency_settings: ConcurrencySettings::default(),
debouncing_settings: DebouncingSettings::default(), labels: None,
debouncing_settings: DebouncingSettings::default(),
labels: None,
})
.run_until_complete(&db, false, server.addr.port())
.await;
@@ -285,7 +283,8 @@ async fn test_error_handler_muted_on_script(db: Pool<Postgres>) -> anyhow::Resul
priority: None,
apply_preprocessor: false,
concurrency_settings: ConcurrencySettings::default(),
debouncing_settings: DebouncingSettings::default(), labels: None,
debouncing_settings: DebouncingSettings::default(),
labels: None,
})
.run_until_complete(&db, false, server.addr.port())
.await;
@@ -380,7 +379,8 @@ async fn test_error_handler_not_triggered_on_success(db: Pool<Postgres>) -> anyh
priority: None,
apply_preprocessor: false,
concurrency_settings: ConcurrencySettings::default(),
debouncing_settings: DebouncingSettings::default(), labels: None,
debouncing_settings: DebouncingSettings::default(),
labels: None,
})
.run_until_complete(&db, false, server.addr.port())
.await;
+851 -13
View File
@@ -49,7 +49,8 @@ mod job_payload {
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings:
windmill_common::runnable_settings::DebouncingSettings::default(), labels: None,
windmill_common::runnable_settings::DebouncingSettings::default(),
labels: None,
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
@@ -89,7 +90,8 @@ mod job_payload {
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default(),
debouncing_settings:
windmill_common::runnable_settings::DebouncingSettings::default(), labels: None,
windmill_common::runnable_settings::DebouncingSettings::default(),
labels: None,
})
.run_until_complete_with(db, false, port, |id| async move {
let job = sqlx::query!("SELECT preprocessed FROM v2_job WHERE id = $1", id)
@@ -276,19 +278,25 @@ mod job_payload {
#[sqlx::test(fixtures("base", "hello"))]
async fn test_dependencies_payload_min_1_427(db: Pool<Postgres>) -> anyhow::Result<()> {
MIN_VERSION.store(std::sync::Arc::new(MIN_VERSION_IS_AT_LEAST_1_427.version().clone()));
MIN_VERSION.store(std::sync::Arc::new(
MIN_VERSION_IS_AT_LEAST_1_427.version().clone(),
));
test_dependencies_payload(db).await?;
Ok(())
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_dependencies_payload_min_1_432(db: Pool<Postgres>) -> anyhow::Result<()> {
MIN_VERSION.store(std::sync::Arc::new(MIN_VERSION_IS_AT_LEAST_1_432.version().clone()));
MIN_VERSION.store(std::sync::Arc::new(
MIN_VERSION_IS_AT_LEAST_1_432.version().clone(),
));
test_dependencies_payload(db).await?;
Ok(())
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_dependencies_payload_min_1_440(db: Pool<Postgres>) -> anyhow::Result<()> {
MIN_VERSION.store(std::sync::Arc::new(MIN_VERSION_IS_AT_LEAST_1_440.version().clone()));
MIN_VERSION.store(std::sync::Arc::new(
MIN_VERSION_IS_AT_LEAST_1_440.version().clone(),
));
test_dependencies_payload(db).await?;
Ok(())
}
@@ -424,7 +432,8 @@ mod job_payload {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
apply_preprocessor: false,
version: 1443253234253454, labels: None,
version: 1443253234253454,
labels: None,
})
.run_until_complete(&db, false, port)
.await
@@ -473,7 +482,8 @@ mod job_payload {
path: "f/system/hello_with_preprocessor".to_string(),
dedicated_worker: None,
apply_preprocessor: true,
version: 1443253234253456, labels: None,
version: 1443253234253456,
labels: None,
})
.run_until_complete_with(db, false, port, |id| async move {
let job = sqlx::query!("SELECT preprocessed FROM v2_job WHERE id = $1", id)
@@ -543,7 +553,8 @@ mod job_payload {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
apply_preprocessor: true,
version: 1443253234253454, labels: None,
version: 1443253234253454,
labels: None,
})
.run_until_complete(&db, false, port)
.await
@@ -554,6 +565,8 @@ mod job_payload {
step_id: "a".into(),
branch_or_iteration_n: None,
flow_version: None,
branch_chosen: None,
nested: None,
})
.arg("iter", json!({ "value": "tests", "index": 0 }))
.run_until_complete(&db, false, port)
@@ -727,6 +740,7 @@ mod job_payload {
step_id: "a".into(),
branch_or_iteration_n: None,
flow_version: None,
..Default::default()
}),
json!("foo"),
json!([
@@ -742,6 +756,7 @@ mod job_payload {
step_id: "b".into(),
branch_or_iteration_n: None,
flow_version: None,
..Default::default()
}),
json!("bar"),
json!([
@@ -757,6 +772,7 @@ mod job_payload {
step_id: "c".into(),
branch_or_iteration_n: Some(1),
flow_version: None,
..Default::default()
}),
json!("yolo"),
json!([
@@ -769,6 +785,823 @@ mod job_payload {
Ok(())
}
/// Walk a chain of `(step_id, iter)` hops through nested completed jobs to
/// reach a deeper child UUID. Used by nested-restart tests to assert that
/// preserved iterations / preserved siblings reuse the original child UUID
/// (and that re-run ones get a new UUID).
async fn nested_child_job_id(
db: &Pool<Postgres>,
mut job_id: uuid::Uuid,
path: &[(&str, Option<usize>)],
) -> uuid::Uuid {
for (step_id, iter) in path {
job_id = child_job_id_for_step(db, job_id, step_id, *iter).await;
}
job_id
}
/// Look up a single child job UUID from a completed flow's `flow_status` for
/// a given top-level step (and optional iteration index for ForLoop /
/// BranchAll containers).
async fn child_job_id_for_step(
db: &Pool<Postgres>,
flow_job_id: uuid::Uuid,
step_id: &str,
iter: Option<usize>,
) -> uuid::Uuid {
let row = sqlx::query!(
"SELECT flow_status FROM v2_job_completed WHERE id = $1",
flow_job_id,
)
.fetch_one(db)
.await
.unwrap();
let raw = row.flow_status.expect("flow_status missing");
let status: windmill_common::flow_status::FlowStatus =
serde_json::from_value(raw).expect("parse flow_status");
let module = status
.modules
.iter()
.find(|m| m.id() == step_id)
.expect("step not found in completed flow_status");
match iter {
Some(i) => module
.flow_jobs()
.expect("expected flow_jobs on container module")
.get(i)
.copied()
.expect("iteration not found in flow_jobs"),
None => module.job().expect("expected single child job on module"),
}
}
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base", "hello"))]
async fn test_nested_restart_inside_branchone(db: Pool<Postgres>) -> anyhow::Result<()> {
use windmill_common::flow_status::BranchChosen;
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
// BranchOne with TWO inner steps so we can verify that earlier siblings
// inside the branch are preserved when restarting at a later one.
let flow_value: FlowValue = serde_json::from_value(json!({
"modules": [{
"id": "a",
"value": {
"type": "rawscript",
"language": "deno",
"input_transforms": {
"world": { "type": "javascript", "expr": "flow_input.world" }
},
"content": "export function main(world: string) { return `pre-${world}` }"
}
}, {
"id": "branch",
"value": {
"type": "branchone",
"default": [],
"branches": [{
"expr": "true",
"modules": [{
"id": "inner_first",
"value": {
"type": "rawscript",
"language": "deno",
"input_transforms": {
"world": { "type": "javascript", "expr": "flow_input.world" }
},
"content": "export function main(world: string) { return `first-${world}` }"
}
}, {
"id": "inner_second",
"value": {
"type": "rawscript",
"language": "deno",
"input_transforms": {
"world": { "type": "javascript", "expr": "flow_input.world" },
"first": { "type": "javascript", "expr": "results.inner_first" }
},
"content": "export function main(world: string, first: string) { return `${first}|second-${world}` }"
}
}]
}]
}
}],
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": { "world": { "type": "string" } },
"order": ["world"]
}
}))
.unwrap();
let test = || async {
let db = &db;
let first_run = RunJob::from(JobPayload::RawFlow {
value: flow_value.clone(),
path: None,
restarted_from: None,
})
.arg("world", json!("foo"))
.run_until_complete(db, false, port)
.await;
assert_eq!(
first_run.json_result().unwrap(),
json!("first-foo|second-foo")
);
let original_branch_child =
child_job_id_for_step(db, first_run.id, "branch", None).await;
// Capture the original inner_first job UUID so we can later assert the
// restart-at-inner_second run preserves it byte-for-byte (i.e., the
// step is reused, not silently re-executed with the same args).
let original_inner_first =
nested_child_job_id(db, first_run.id, &[("branch", None), ("inner_first", None)])
.await;
let original_inner_second = nested_child_job_id(
db,
first_run.id,
&[("branch", None), ("inner_second", None)],
)
.await;
// Restart at the FIRST inner step. Both inner steps re-run with new args.
let restarted_at_first = RunJob::from(JobPayload::RawFlow {
value: flow_value.clone(),
path: None,
restarted_from: Some(RestartedFrom {
flow_job_id: first_run.id,
step_id: "branch".into(),
branch_or_iteration_n: None,
flow_version: None,
branch_chosen: Some(BranchChosen::Branch { branch: 0 }),
nested: Some(Box::new(RestartedFrom {
flow_job_id: original_branch_child,
step_id: "inner_first".into(),
branch_or_iteration_n: None,
flow_version: None,
branch_chosen: None,
nested: None,
})),
}),
})
.arg("world", json!("bar"))
.run_until_complete(db, false, port)
.await;
assert_eq!(
restarted_at_first.json_result().unwrap(),
json!("first-bar|second-bar")
);
// Restart at the SECOND inner step (the user-reported case). The first
// inner step's original output ("first-foo") must be preserved as a
// dependency for the second step's `results.inner_first` reference.
let restarted_at_second = RunJob::from(JobPayload::RawFlow {
value: flow_value.clone(),
path: None,
restarted_from: Some(RestartedFrom {
flow_job_id: first_run.id,
step_id: "branch".into(),
branch_or_iteration_n: None,
flow_version: None,
branch_chosen: Some(BranchChosen::Branch { branch: 0 }),
nested: Some(Box::new(RestartedFrom {
flow_job_id: original_branch_child,
step_id: "inner_second".into(),
branch_or_iteration_n: None,
flow_version: None,
branch_chosen: None,
nested: None,
})),
}),
})
.arg("world", json!("baz"))
.run_until_complete(db, false, port)
.await;
// `inner_first` keeps its original "first-foo" result; only `inner_second`
// re-runs with the new `world=baz` arg.
assert_eq!(
restarted_at_second.json_result().unwrap(),
json!("first-foo|second-baz")
);
// Identity proof: inner_first's UUID in the restarted run must be the
// exact same UUID as in the original run (no re-execution), and
// inner_second must be a fresh UUID (was re-executed).
let new_inner_first = nested_child_job_id(
db,
restarted_at_second.id,
&[("branch", None), ("inner_first", None)],
)
.await;
let new_inner_second = nested_child_job_id(
db,
restarted_at_second.id,
&[("branch", None), ("inner_second", None)],
)
.await;
assert_eq!(
new_inner_first, original_inner_first,
"inner_first should reuse the original job UUID"
);
assert_ne!(
new_inner_second, original_inner_second,
"inner_second should be a fresh job"
);
};
test_for_versions(VERSION_FLAGS.iter().copied(), test).await;
Ok(())
}
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base", "hello"))]
async fn test_nested_restart_inside_forloop_iteration(
db: Pool<Postgres>,
) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
// Sequential ForLoop with an inner step. We restart at the inner step inside
// iteration 1, expecting iteration 0 to remain unchanged and only iteration
// 1 to re-run with new input.
let flow_value: FlowValue = serde_json::from_value(json!({
"modules": [{
"id": "loop",
"value": {
"type": "forloopflow",
"iterator": { "type": "javascript", "expr": "['x', 'y']" },
"skip_failures": false,
"modules": [{
"id": "inner",
"value": {
"type": "rawscript",
"language": "deno",
"input_transforms": {
"iter_val": { "type": "javascript", "expr": "flow_input.iter.value" },
"tag": { "type": "javascript", "expr": "flow_input.tag" }
},
"content": "export function main(iter_val: string, tag: string) { return `${tag}:${iter_val}` }"
}
}]
}
}],
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": { "tag": { "type": "string" } },
"order": ["tag"]
}
}))
.unwrap();
let test = || async {
let db = &db;
let first_run = RunJob::from(JobPayload::RawFlow {
value: flow_value.clone(),
path: None,
restarted_from: None,
})
.arg("tag", json!("first"))
.run_until_complete(db, false, port)
.await;
assert_eq!(
first_run.json_result().unwrap(),
json!(["first:x", "first:y"])
);
// Original child jobs for both iterations.
let original_iter0_child =
child_job_id_for_step(db, first_run.id, "loop", Some(0)).await;
let original_iter1_child =
child_job_id_for_step(db, first_run.id, "loop", Some(1)).await;
let restarted = RunJob::from(JobPayload::RawFlow {
value: flow_value.clone(),
path: None,
restarted_from: Some(RestartedFrom {
flow_job_id: first_run.id,
step_id: "loop".into(),
branch_or_iteration_n: Some(1),
flow_version: None,
branch_chosen: None,
nested: Some(Box::new(RestartedFrom {
flow_job_id: original_iter1_child,
step_id: "inner".into(),
branch_or_iteration_n: None,
flow_version: None,
branch_chosen: None,
nested: None,
})),
}),
})
.arg("tag", json!("second"))
.run_until_complete(db, false, port)
.await;
// Iteration 0 keeps the original "first:x"; iteration 1 re-ran with "second"
assert_eq!(
restarted.json_result().unwrap(),
json!(["first:x", "second:y"])
);
// Identity proof: iter 0's child UUID survives intact, iter 1 is fresh.
let new_iter0_child = child_job_id_for_step(db, restarted.id, "loop", Some(0)).await;
let new_iter1_child = child_job_id_for_step(db, restarted.id, "loop", Some(1)).await;
assert_eq!(
new_iter0_child, original_iter0_child,
"iteration 0 should reuse the original child job"
);
assert_ne!(
new_iter1_child, original_iter1_child,
"iteration 1 should be a fresh job"
);
};
test_for_versions(VERSION_FLAGS.iter().copied(), test).await;
Ok(())
}
/// Regression test for the FlowNode case: when a flow is deployed, container
/// bodies (BranchOne branches, ForLoop iterations) get compiled into FlowNodes.
/// A nested restart targeting a step inside such a container spawns a child as
/// `JobPayload::RestartedFlow` against the original FlowNode-kind child. Without
/// preserving the original `JobKind` (the bug we just fixed), the new spawn
/// would land as `JobKind::Flow` with the FlowNode id misinterpreted as a
/// flow_version id, failing with "Flow version not found".
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base", "hello"))]
async fn test_nested_restart_inside_deployed_loop_flownode(
db: Pool<Postgres>,
) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
// The fixture's `hello_with_nodes_flow` is a deployed flow with a top-level
// ForLoop iterating ['foo', 'bar', 'baz'], whose body has modules `b` (greet)
// and `c` (echo greeting). After deployment the body is wrapped in a FlowNode.
let test = || async {
let db = &db;
let first_run = RunJob::from(JobPayload::Flow {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
apply_preprocessor: true,
version: 1443253234253454,
labels: None,
})
.run_until_complete(db, false, port)
.await;
// Iteration 1's child job (kind=flownode after FlowDependencies has run).
let original_iter1_child = child_job_id_for_step(db, first_run.id, "a", Some(1)).await;
// Restart at inner step `c` of iteration 1. Iteration 0 keeps its original
// result; iteration 1 re-runs starting at `c` (so `b` is preserved as
// Success inside the iteration); iteration 2 fresh-runs.
let restarted = RunJob::from(JobPayload::RestartedFlow {
completed_job_id: first_run.id,
step_id: "a".into(),
branch_or_iteration_n: Some(1),
flow_version: None,
branch_chosen: None,
nested: Some(Box::new(RestartedFrom {
flow_job_id: original_iter1_child,
step_id: "c".into(),
branch_or_iteration_n: None,
flow_version: None,
branch_chosen: None,
nested: None,
})),
})
.run_until_complete(db, false, port)
.await;
// Same args, same output as original — the goal is to verify the spawn
// doesn't 500 with "Flow version not found" when the inner child is a
// FlowNode.
assert_eq!(
restarted.json_result().unwrap(),
json!([
"Did you just say \"Hello foo!\"??!",
"Did you just say \"Hello bar!\"??!",
"Did you just say \"Hello baz!\"??!",
])
);
};
// Exercise BOTH the pre-deployment path (raw flow inline, FlowPreview kind)
// and the post-deployment path (FlowNode kind) — the latter is the case the
// user originally hit.
test_for_versions(VERSION_FLAGS.iter().copied(), test).await;
let _ = RunJob::from(JobPayload::FlowDependencies {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
version: 1443253234253454,
debouncing_settings: Default::default(),
})
.run_until_complete(&db, false, port)
.await
.json_result()
.unwrap();
test_for_versions(VERSION_FLAGS.iter().copied(), test).await;
Ok(())
}
/// Two ForLoops nested inside each other. We restart at the leaf step inside
/// outer iter K=1 and inner iter M=1. Iters before K stay frozen at the parent
/// level; iter K's inner iters before M stay frozen at the inner level; the
/// inner-M leaf re-runs with new input.
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base", "hello"))]
async fn test_nested_restart_inside_nested_forloops(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
// Outer iterates [1,2]; for each outer iter, inner iterates ['x','y'];
// leaf returns "<tag>:<inner>". Tag comes from flow_input so we can
// observe which leaves re-ran.
let flow_value: FlowValue = serde_json::from_value(json!({
"modules": [{
"id": "outer",
"value": {
"type": "forloopflow",
"iterator": { "type": "javascript", "expr": "[1, 2]" },
"skip_failures": false,
"modules": [{
"id": "inner",
"value": {
"type": "forloopflow",
"iterator": { "type": "javascript", "expr": "['x', 'y']" },
"skip_failures": false,
"modules": [{
"id": "leaf",
"value": {
"type": "rawscript",
"language": "deno",
"input_transforms": {
"tag": { "type": "javascript", "expr": "flow_input.tag" },
"iv": { "type": "javascript", "expr": "flow_input.iter.value" }
},
"content": "export function main(tag: string, iv: string) { return `${tag}:${iv}` }"
}
}]
}
}]
}
}],
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": { "tag": { "type": "string" } },
"order": ["tag"]
}
}))
.unwrap();
let test = || async {
let db = &db;
let first_run = RunJob::from(JobPayload::RawFlow {
value: flow_value.clone(),
path: None,
restarted_from: None,
})
.arg("tag", json!("first"))
.run_until_complete(db, false, port)
.await;
assert_eq!(
first_run.json_result().unwrap(),
json!([["first:x", "first:y"], ["first:x", "first:y"]])
);
// Snapshot every original child UUID we'll later compare against.
let original_outer_iter0 =
child_job_id_for_step(db, first_run.id, "outer", Some(0)).await;
let outer_iter1_child = child_job_id_for_step(db, first_run.id, "outer", Some(1)).await;
let original_outer1_inner0 =
child_job_id_for_step(db, outer_iter1_child, "inner", Some(0)).await;
let inner_iter1_child =
child_job_id_for_step(db, outer_iter1_child, "inner", Some(1)).await;
let restarted = RunJob::from(JobPayload::RawFlow {
value: flow_value.clone(),
path: None,
restarted_from: Some(RestartedFrom {
flow_job_id: first_run.id,
step_id: "outer".into(),
branch_or_iteration_n: Some(1),
flow_version: None,
branch_chosen: None,
nested: Some(Box::new(RestartedFrom {
flow_job_id: outer_iter1_child,
step_id: "inner".into(),
branch_or_iteration_n: Some(1),
flow_version: None,
branch_chosen: None,
nested: Some(Box::new(RestartedFrom {
flow_job_id: inner_iter1_child,
step_id: "leaf".into(),
branch_or_iteration_n: None,
flow_version: None,
branch_chosen: None,
nested: None,
})),
})),
}),
})
.arg("tag", json!("second"))
.run_until_complete(db, false, port)
.await;
// Outer iter 0 frozen; outer iter 1 has inner iter 0 frozen and inner
// iter 1 re-run with new tag.
assert_eq!(
restarted.json_result().unwrap(),
json!([["first:x", "first:y"], ["first:x", "second:y"]])
);
// Identity proof at every layer:
// * outer iter 0's child UUID survives (preserved iteration)
// * outer iter 1's child UUID is fresh (re-spawned as RestartedFlow)
// * inside that fresh outer-iter-1 child, inner iter 0's UUID equals
// the ORIGINAL outer-iter-1's inner-iter-0 UUID (preserved through
// the nested chain)
// * inner iter 1's UUID is fresh
let new_outer_iter0 = child_job_id_for_step(db, restarted.id, "outer", Some(0)).await;
let new_outer_iter1 = child_job_id_for_step(db, restarted.id, "outer", Some(1)).await;
let new_outer1_inner0 =
child_job_id_for_step(db, new_outer_iter1, "inner", Some(0)).await;
let new_outer1_inner1 =
child_job_id_for_step(db, new_outer_iter1, "inner", Some(1)).await;
assert_eq!(
new_outer_iter0, original_outer_iter0,
"outer iter 0 should reuse original child"
);
assert_ne!(
new_outer_iter1, outer_iter1_child,
"outer iter 1 should be a fresh child"
);
assert_eq!(
new_outer1_inner0, original_outer1_inner0,
"inner iter 0 inside outer iter 1 should reuse original child"
);
assert_ne!(
new_outer1_inner1, inner_iter1_child,
"inner iter 1 inside outer iter 1 should be a fresh child"
);
};
test_for_versions(VERSION_FLAGS.iter().copied(), test).await;
Ok(())
}
/// BranchOne nested inside another BranchOne. We restart at the deepest
/// leaf with both branches locked to their original choices.
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base", "hello"))]
async fn test_nested_restart_inside_nested_branchone(db: Pool<Postgres>) -> anyhow::Result<()> {
use windmill_common::flow_status::BranchChosen;
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let flow_value: FlowValue = serde_json::from_value(json!({
"modules": [{
"id": "outer_branch",
"value": {
"type": "branchone",
"default": [],
"branches": [{
"expr": "true",
"modules": [{
"id": "inner_branch",
"value": {
"type": "branchone",
"default": [],
"branches": [{
"expr": "true",
"modules": [{
"id": "leaf",
"value": {
"type": "rawscript",
"language": "deno",
"input_transforms": {
"tag": { "type": "javascript", "expr": "flow_input.tag" }
},
"content": "export function main(tag: string) { return `leaf:${tag}` }"
}
}]
}]
}
}]
}]
}
}],
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": { "tag": { "type": "string" } },
"order": ["tag"]
}
}))
.unwrap();
let test = || async {
let db = &db;
let first_run = RunJob::from(JobPayload::RawFlow {
value: flow_value.clone(),
path: None,
restarted_from: None,
})
.arg("tag", json!("first"))
.run_until_complete(db, false, port)
.await;
assert_eq!(first_run.json_result().unwrap(), json!("leaf:first"));
let outer_branch_child =
child_job_id_for_step(db, first_run.id, "outer_branch", None).await;
let inner_branch_child =
child_job_id_for_step(db, outer_branch_child, "inner_branch", None).await;
let restarted = RunJob::from(JobPayload::RawFlow {
value: flow_value.clone(),
path: None,
restarted_from: Some(RestartedFrom {
flow_job_id: first_run.id,
step_id: "outer_branch".into(),
branch_or_iteration_n: None,
flow_version: None,
branch_chosen: Some(BranchChosen::Branch { branch: 0 }),
nested: Some(Box::new(RestartedFrom {
flow_job_id: outer_branch_child,
step_id: "inner_branch".into(),
branch_or_iteration_n: None,
flow_version: None,
branch_chosen: Some(BranchChosen::Branch { branch: 0 }),
nested: Some(Box::new(RestartedFrom {
flow_job_id: inner_branch_child,
step_id: "leaf".into(),
branch_or_iteration_n: None,
flow_version: None,
branch_chosen: None,
nested: None,
})),
})),
}),
})
.arg("tag", json!("second"))
.run_until_complete(db, false, port)
.await;
assert_eq!(restarted.json_result().unwrap(), json!("leaf:second"));
};
test_for_versions(VERSION_FLAGS.iter().copied(), test).await;
Ok(())
}
/// Restart at a step inside a SUBFLOW (`Flow{path}`) called from a parent flow.
/// The parent invokes `f/system/hello_with_nodes_flow` (which has a top-level
/// ForLoop). We restart at the inner step `c` of iteration 1 of that loop,
/// from the parent's perspective. This exercises the cross-job-spawn chain
/// crossing a subflow boundary AND descending into a ForLoop inside the
/// subflow.
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base", "hello"))]
async fn test_nested_restart_inside_subflow(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let flow_value: FlowValue = serde_json::from_value(json!({
"modules": [{
"id": "h",
"value": {
"path": "f/system/hello_with_nodes_flow",
"type": "flow",
"input_transforms": {}
}
}],
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {},
"order": []
}
}))
.unwrap();
let test = || async {
let db = &db;
let first_run = RunJob::from(JobPayload::RawFlow {
value: flow_value.clone(),
path: None,
restarted_from: None,
})
.run_until_complete(db, false, port)
.await;
// The subflow returns the loop's results; the parent flow returns the
// subflow's result as its own output.
assert_eq!(
first_run.json_result().unwrap(),
json!([
"Did you just say \"Hello foo!\"??!",
"Did you just say \"Hello bar!\"??!",
"Did you just say \"Hello baz!\"??!",
])
);
let subflow_child = child_job_id_for_step(db, first_run.id, "h", None).await;
let original_subflow_iter0 =
child_job_id_for_step(db, subflow_child, "a", Some(0)).await;
let subflow_iter1_child = child_job_id_for_step(db, subflow_child, "a", Some(1)).await;
// Capture the leaf "c" inside iter 0 of the original subflow, which is
// a step we explicitly do NOT restart at — its UUID must survive into
// the restarted run unchanged, despite crossing two parent layers
// (parent's `h` → subflow's `a` → iter 0 → `c`).
let original_subflow_iter0_c =
nested_child_job_id(db, subflow_child, &[("a", Some(0)), ("c", None)]).await;
let restarted = RunJob::from(JobPayload::RawFlow {
value: flow_value.clone(),
path: None,
restarted_from: Some(RestartedFrom {
flow_job_id: first_run.id,
step_id: "h".into(),
branch_or_iteration_n: None,
flow_version: None,
branch_chosen: None,
nested: Some(Box::new(RestartedFrom {
flow_job_id: subflow_child,
step_id: "a".into(),
branch_or_iteration_n: Some(1),
flow_version: None,
branch_chosen: None,
nested: Some(Box::new(RestartedFrom {
flow_job_id: subflow_iter1_child,
step_id: "c".into(),
branch_or_iteration_n: None,
flow_version: None,
branch_chosen: None,
nested: None,
})),
})),
}),
})
.run_until_complete(db, false, port)
.await;
// Same input → same output, produced via the cross-job restart chain.
assert_eq!(
restarted.json_result().unwrap(),
json!([
"Did you just say \"Hello foo!\"??!",
"Did you just say \"Hello bar!\"??!",
"Did you just say \"Hello baz!\"??!",
])
);
// Identity proof across the subflow boundary:
// * the parent's `h` child UUID is fresh (subflow re-spawned as a
// RestartedFlow) — we don't compare it directly, but inside it…
// * iter 0 of the subflow's `a` loop reuses the original UUID
// * iter 1 is a fresh UUID
// * the inner step `c` inside the preserved iter 0 also reuses its
// original UUID (preservation propagates two layers deep)
let new_subflow_child = child_job_id_for_step(db, restarted.id, "h", None).await;
let new_subflow_iter0 =
child_job_id_for_step(db, new_subflow_child, "a", Some(0)).await;
let new_subflow_iter1 =
child_job_id_for_step(db, new_subflow_child, "a", Some(1)).await;
let new_subflow_iter0_c =
nested_child_job_id(db, new_subflow_child, &[("a", Some(0)), ("c", None)]).await;
assert_eq!(
new_subflow_iter0, original_subflow_iter0,
"subflow iter 0 should reuse original child"
);
assert_ne!(
new_subflow_iter1, subflow_iter1_child,
"subflow iter 1 should be a fresh child"
);
assert_eq!(
new_subflow_iter0_c, original_subflow_iter0_c,
"step `c` inside preserved iter 0 should reuse its original UUID"
);
};
// Pre-deployment: subflow's iteration wrapper is a FlowPreview (inline
// RawFlow); leaf `c` runs as a rawscript embedded in the wrapper.
test_for_versions(VERSION_FLAGS.iter().copied(), test).await;
// Post-deployment: subflow's body is compiled into a FlowNode; the iteration
// wrapper jobs run with kind=FlowNode and the leaf becomes a FlowScript
// referencing a flow_node id. Re-running the same scenario exercises the
// kind-preservation path through the subflow boundary AND a level deeper.
let _ = RunJob::from(JobPayload::FlowDependencies {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
version: 1443253234253454,
debouncing_settings: Default::default(),
})
.run_until_complete(&db, false, port)
.await
.json_result()
.unwrap();
test_for_versions(VERSION_FLAGS.iter().copied(), test).await;
Ok(())
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_dedicated_worker_preprocessor_bun(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
@@ -789,7 +1622,8 @@ mod job_payload {
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default(),
debouncing_settings:
windmill_common::runnable_settings::DebouncingSettings::default(), labels: None,
windmill_common::runnable_settings::DebouncingSettings::default(),
labels: None,
})
.arg("foo", json!("hello"))
.arg("bar", json!("world"))
@@ -839,7 +1673,8 @@ mod job_payload {
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default(),
debouncing_settings:
windmill_common::runnable_settings::DebouncingSettings::default(), labels: None,
windmill_common::runnable_settings::DebouncingSettings::default(),
labels: None,
})
.arg("foo", json!("hello"))
.arg("bar", json!("world"))
@@ -889,7 +1724,8 @@ mod job_payload {
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default(),
debouncing_settings:
windmill_common::runnable_settings::DebouncingSettings::default(), labels: None,
windmill_common::runnable_settings::DebouncingSettings::default(),
labels: None,
})
.arg("foo", json!("hello"))
.arg("bar", json!("world"))
@@ -939,7 +1775,8 @@ mod job_payload {
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default(),
debouncing_settings:
windmill_common::runnable_settings::DebouncingSettings::default(), labels: None,
windmill_common::runnable_settings::DebouncingSettings::default(),
labels: None,
})
.arg("foo", json!("hello"))
.arg("bar", json!("world"))
@@ -991,7 +1828,8 @@ mod job_payload {
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default(),
debouncing_settings:
windmill_common::runnable_settings::DebouncingSettings::default(), labels: None,
windmill_common::runnable_settings::DebouncingSettings::default(),
labels: None,
})
.arg("foo", json!("hello"))
.arg("bar", json!("world"))
+17
View File
@@ -68,6 +68,7 @@ async fn test_list_jobs_without_include_args(db: Pool<Postgres>) -> anyhow::Resu
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
tag: None,
}))
.arg("x", json!(42))
.push(&db)
@@ -125,6 +126,7 @@ async fn test_list_jobs_with_include_args(db: Pool<Postgres>) -> anyhow::Result<
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
tag: None,
}))
.arg("x", json!(42))
.push(&db)
@@ -196,6 +198,7 @@ async fn test_list_jobs_completed_with_include_args(db: Pool<Postgres>) -> anyho
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
tag: None,
}))
.arg("x", json!(42))
.run_until_complete(&db, false, port)
@@ -269,6 +272,7 @@ async fn test_list_jobs_mixed_queue_and_completed(db: Pool<Postgres>) -> anyhow:
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
tag: None,
}))
.arg("completed_arg", json!("completed_value"))
.run_until_complete(&db, false, port)
@@ -290,6 +294,7 @@ async fn test_list_jobs_mixed_queue_and_completed(db: Pool<Postgres>) -> anyhow:
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
tag: None,
}))
.arg("queued_arg", json!("queued_value"))
.push(&db)
@@ -375,6 +380,7 @@ async fn test_list_jobs_multiple_queued_with_include_args(
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
tag: None,
}))
.arg("x", json!(1))
.push(&db)
@@ -393,6 +399,7 @@ async fn test_list_jobs_multiple_queued_with_include_args(
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
tag: None,
}))
.arg("y", json!(2))
.push(&db)
@@ -471,6 +478,7 @@ async fn test_queue_list_without_include_args(db: Pool<Postgres>) -> anyhow::Res
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
tag: None,
}))
.arg("x", json!(42))
.push(&db)
@@ -531,6 +539,7 @@ async fn test_queue_list_with_include_args(db: Pool<Postgres>) -> anyhow::Result
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
tag: None,
}))
.arg("x", json!(42))
.push(&db)
@@ -601,6 +610,7 @@ async fn test_queue_list_multiple_jobs_with_include_args(db: Pool<Postgres>) ->
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
tag: None,
}))
.arg("a", json!("value_a"))
.push(&db)
@@ -619,6 +629,7 @@ async fn test_queue_list_multiple_jobs_with_include_args(db: Pool<Postgres>) ->
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
tag: None,
}))
.arg("b", json!("value_b"))
.push(&db)
@@ -698,6 +709,7 @@ async fn test_completed_list_without_include_args(db: Pool<Postgres>) -> anyhow:
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
tag: None,
}))
.arg("x", json!(42))
.run_until_complete(&db, false, port)
@@ -764,6 +776,7 @@ async fn test_completed_list_with_include_args(db: Pool<Postgres>) -> anyhow::Re
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
tag: None,
}))
.arg("x", json!(42))
.run_until_complete(&db, false, port)
@@ -839,6 +852,7 @@ async fn test_completed_list_multiple_jobs_with_include_args(
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
tag: None,
}))
.arg("a", json!("completed_a"))
.run_until_complete(&db, false, port)
@@ -860,6 +874,7 @@ async fn test_completed_list_multiple_jobs_with_include_args(
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
tag: None,
}))
.arg("b", json!("completed_b"))
.run_until_complete(&db, false, port)
@@ -992,6 +1007,7 @@ async fn test_job_without_labels_has_no_labels_field(db: Pool<Postgres>) -> anyh
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
tag: None,
}))
.push(&db)
.await;
@@ -1135,6 +1151,7 @@ async fn test_wm_labels_from_result_merged_with_static_labels(
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
tag: None,
}));
let completed = job
+1
View File
@@ -40,6 +40,7 @@ fn nativets_code(content: &str) -> JobPayload {
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
})
}
+1
View File
@@ -157,6 +157,7 @@ async fn push_job(db: &Pool<Postgres>, content: &str, args: &serde_json::Value)
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let tx = PushIsolationLevel::IsolatedRoot(db.clone());
+1 -4
View File
@@ -395,10 +395,7 @@ async fn test_root_job_span_created_on_success() {
attrs.contains(&"script_path"),
"missing script_path attribute"
);
assert!(
attrs.contains(&"job_kind"),
"missing job_kind attribute"
);
assert!(attrs.contains(&"job_kind"), "missing job_kind attribute");
assert!(
attrs.contains(&"created_by"),
"missing created_by attribute"
+41 -27
View File
@@ -11,7 +11,7 @@ use windmill_test_utils::*;
// Dedicated Worker Protocol Tests (Python)
// ============================================================================
#[cfg(feature = "python")]
#[cfg(all(feature = "python", feature = "private"))]
mod dedicated_worker_protocol_python {
use std::io::{BufRead, BufReader, Write};
use std::process::{Command, Stdio};
@@ -748,6 +748,7 @@ def main():
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = run_job_in_new_worker_until_complete(&db, false, job, port)
@@ -800,6 +801,7 @@ def main():
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = run_job_in_new_worker_until_complete(&db, false, job, port)
@@ -837,6 +839,7 @@ def main():
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = run_job_in_new_worker_until_complete(&db, false, job, port)
@@ -878,6 +881,7 @@ def main():
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = run_job_in_new_worker_until_complete(&db, false, job, port)
@@ -917,6 +921,7 @@ def main():
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
tag: None,
});
let result = run_job_in_new_worker_until_complete(&db, false, job, port)
@@ -985,33 +990,42 @@ async def main(item: str, qty: int, email: str):
"#
.to_string();
// WAC requires at least 2 workers (parent + task sub-jobs)
let db = &db;
in_test_worker(
db,
async move {
let job = Box::pin(
RunJob::from(JobPayload::Code(RawCode {
language: ScriptLang::Python3,
content,
..RawCode::default()
}))
.arg("item", json!("widget"))
.arg("qty", json!(5))
.arg("email", json!("test@example.com"))
.run_until_complete(db, false, port),
)
.await;
// WAC requires at least 2 workers (parent + task sub-jobs).
//
// Run the heavy `in_test_worker` chain on an isolated OS thread with a
// larger stack: the deep nested async chain (test -> in_test_worker ->
// run_until_complete -> windmill_queue::push -> ...) composes into one
// synchronous poll-stack frame that exceeds the default 2 MB test-thread
// stack in debug builds. `Box::pin` at the call site only moves future
// *state* to the heap; it can't shrink poll-time stack frames.
let db = db.clone();
run_in_isolated_thread(move || async move {
in_test_worker(
&db,
async {
let job = Box::pin(
RunJob::from(JobPayload::Code(RawCode {
language: ScriptLang::Python3,
content,
..RawCode::default()
}))
.arg("item", json!("widget"))
.arg("qty", json!(5))
.arg("email", json!("test@example.com"))
.run_until_complete(&db, false, port),
)
.await;
let result = job.json_result().unwrap();
assert_eq!(result["item"], json!("widget"));
assert_eq!(result["qty"], json!(5));
assert_eq!(result["email"], json!("test@example.com"));
assert_eq!(result["greeting"], json!("hello widget x5"));
},
port,
)
.await;
let result = job.json_result().unwrap();
assert_eq!(result["item"], json!("widget"));
assert_eq!(result["qty"], json!(5));
assert_eq!(result["email"], json!("test@example.com"));
assert_eq!(result["greeting"], json!("hello widget x5"));
},
port,
)
.await;
});
Ok(())
}
+255
View File
@@ -0,0 +1,255 @@
//! HTTP-level integration tests for the `restart_flow_at_step` API endpoint.
//!
//! The other restart tests in `job_payload.rs` exercise the worker by hand-
//! constructing `RestartedFrom` chains and calling `push()` directly. These
//! tests instead drive the actual HTTP endpoint to lock in the API contract,
//! including the validation branches in `resolve_nested_restart` (step lookup,
//! parallel rejection, etc.).
#![cfg(feature = "deno_core")]
#![cfg(feature = "enterprise")]
use serde_json::json;
use sqlx::{Pool, Postgres};
use windmill_common::flows::FlowValue;
use windmill_common::jobs::JobPayload;
use windmill_test_utils::*;
fn client() -> reqwest::Client {
reqwest::Client::new()
}
fn authed(builder: reqwest::RequestBuilder, token: &str) -> reqwest::RequestBuilder {
builder.header("Authorization", format!("Bearer {}", token))
}
const SUPER_TOKEN: &str = "SECRET_TOKEN";
/// Happy path: HTTP nested restart targeting a step inside iteration 1 of a
/// top-level sequential ForLoop. Verifies the API endpoint accepts a
/// `nested_path`, walks the original execution to resolve UUIDs, and the
/// resulting job runs successfully.
#[sqlx::test(fixtures("base", "hello"))]
async fn test_api_restart_at_step_nested_happy(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let first_run = RunJob::from(JobPayload::Flow {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
apply_preprocessor: true,
version: 1443253234253454,
labels: None,
})
.run_until_complete(&db, false, port)
.await;
let resp = authed(
client().post(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/restart/f/{}",
first_run.id
)),
SUPER_TOKEN,
)
.json(&json!({
"step_id": "a",
"branch_or_iteration_n": 1,
"nested_path": [{ "step_id": "c" }],
}))
.send()
.await?;
assert_eq!(
resp.status(),
201,
"expected 201, got {}: {}",
resp.status(),
resp.text().await.unwrap_or_default()
);
let new_job_id = resp.text().await?;
assert!(!new_job_id.is_empty(), "expected job UUID in response body");
Ok(())
}
/// Happy path: top-level (non-nested) restart via HTTP. Same surface as
/// `test_restarted_flow_payload` but driven through the actual REST endpoint.
#[sqlx::test(fixtures("base", "hello"))]
async fn test_api_restart_at_step_top_level_happy(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let first_run = RunJob::from(JobPayload::Flow {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
apply_preprocessor: true,
version: 1443253234253454,
labels: None,
})
.run_until_complete(&db, false, port)
.await;
let resp = authed(
client().post(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/restart/f/{}",
first_run.id
)),
SUPER_TOKEN,
)
.json(&json!({ "step_id": "a", "branch_or_iteration_n": 0 }))
.send()
.await?;
assert_eq!(resp.status(), 201);
Ok(())
}
/// Rejection: nested step doesn't exist in the original run. The API should
/// reject with a 4xx (or surface a clear backend error) rather than silently
/// queue an unrunnable job.
#[sqlx::test(fixtures("base", "hello"))]
async fn test_api_restart_at_step_rejects_unknown_step(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let first_run = RunJob::from(JobPayload::Flow {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
apply_preprocessor: true,
version: 1443253234253454,
labels: None,
})
.run_until_complete(&db, false, port)
.await;
let resp = authed(
client().post(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/restart/f/{}",
first_run.id
)),
SUPER_TOKEN,
)
.json(&json!({
"step_id": "a",
"branch_or_iteration_n": 1,
"nested_path": [{ "step_id": "does_not_exist" }],
}))
.send()
.await?;
assert!(
!resp.status().is_success(),
"expected error, got {}",
resp.status()
);
Ok(())
}
/// Rejection: nested restart targets an iteration past the actual count.
/// Original ran 3 iterations (index 0..2); requesting `branch_or_iteration_n=5`
/// must error.
#[sqlx::test(fixtures("base", "hello"))]
async fn test_api_restart_at_step_rejects_out_of_range_iteration(
db: Pool<Postgres>,
) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let first_run = RunJob::from(JobPayload::Flow {
path: "f/system/hello_with_nodes_flow".to_string(),
dedicated_worker: None,
apply_preprocessor: true,
version: 1443253234253454,
labels: None,
})
.run_until_complete(&db, false, port)
.await;
let resp = authed(
client().post(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/restart/f/{}",
first_run.id
)),
SUPER_TOKEN,
)
.json(&json!({
"step_id": "a",
"branch_or_iteration_n": 5,
"nested_path": [{ "step_id": "c" }],
}))
.send()
.await?;
assert!(
!resp.status().is_success(),
"expected error for out-of-range iteration, got {}: {}",
resp.status(),
resp.text().await.unwrap_or_default()
);
Ok(())
}
/// Rejection: parallel ForLoop ancestor on the nested path. The resolver
/// rejects this category outright — `branch_or_iteration_n` only makes sense
/// for sequential containers because each iteration runs as a separate
/// numbered child.
#[sqlx::test(fixtures("base", "hello"))]
async fn test_api_restart_at_step_rejects_parallel_loop(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
// Run a parallel ForLoop with an inner script. We use a `RawFlow` with an
// explicit `path` so the API endpoint accepts it as a valid completed job
// (the handler requires `runnable_path`).
let parallel_flow: FlowValue = serde_json::from_value(json!({
"modules": [{
"id": "loop",
"value": {
"type": "forloopflow",
"iterator": { "type": "javascript", "expr": "[1, 2]" },
"skip_failures": false,
"parallel": true,
"modules": [{
"id": "inner",
"value": {
"type": "rawscript",
"language": "deno",
"input_transforms": {},
"content": "export function main() { return 'ok' }"
}
}]
}
}],
}))
.unwrap();
let first_run = RunJob::from(JobPayload::RawFlow {
value: parallel_flow,
path: Some("u/admin/parallel_test".to_string()),
restarted_from: None,
})
.run_until_complete(&db, false, port)
.await;
let resp = authed(
client().post(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/restart/f/{}",
first_run.id
)),
SUPER_TOKEN,
)
.json(&json!({
"step_id": "loop",
"branch_or_iteration_n": 0,
"nested_path": [{ "step_id": "inner" }],
}))
.send()
.await?;
assert!(
!resp.status().is_success(),
"expected rejection of parallel-loop nested restart, got {}: {}",
resp.status(),
resp.text().await.unwrap_or_default()
);
Ok(())
}

Some files were not shown because too many files have changed in this diff Show More