Repository navigation
Conversation
…xplain why it is disabled
…ion string instead of panicking
Every sqlx error became a DatabaseConnectionError, and the connection manager put every such error into the reconnect backoff, so an executed statement that divides by zero disabled completions, hover and typecheck for 5 to 60 seconds. Only errors that mean the database could not be reached or refused the connection start the backoff now. An error Postgres raises for an executed statement is part of the result, with its SQLSTATE, detail, hint and position in the document.
…uted statements Executed statements ran on the pool that the language server uses for its own queries, and sqlx returned the connection to the pool as the statement left it. After an executed `begin`, later statements and the server's own snapshot and typecheck queries ran inside that transaction, and a `set search_path` applied to them as well. Executed statements now run on a pool of their own. After each one, the session is reset with `discard all`, after rolling back a transaction the statement left open. The pool caches no prepared statements, which `discard all` would drop.
Executed statements had no timeout and could not be cancelled, sent values of any size to the editor, made Postgres produce the whole result of a truncated query, failed on COPY with a protocol error, dropped the notices, and left the schema snapshot stale after DDL. - A statement runs for at most db.statementTimeoutSecs (60 by default). - $/cancelRequest cancels it in the database. The language server runs it on a blocking thread, so it keeps handling requests meanwhile. - A value is cut to 10,000 bytes and the result to 1 MB, both marked. - A select or values runs in a cursor that fetches 101 rows. Other statements still run to completion. - COPY FROM STDIN and TO STDOUT are refused with a clear message. - Notices and warnings are returned and appended to the message. - After a statement that may change the schema, the snapshot is reloaded.
psteinroe
commented
Oct 26, 2026
| /// with `…`, and the result is marked with `truncated_values`. | ||
| pub const EXECUTE_STATEMENT_MAX_VALUE_SIZE: usize = 10_000; | ||
|
|
||
| /// The most bytes of values that an executed statement returns. The rows that would exceed it are |
Collaborator
Author
There was a problem hiding this comment.
maybe we should split code actions into their own module now that we have start expand and execute statement and maybe others
|
|
||
| /// The statements that are being executed, to cancel them | ||
| #[cfg(feature = "db")] | ||
| running_statements: Mutex<Vec<RunningStatement>>, |
Collaborator
Author
There was a problem hiding this comment.
this should probably be encapsulated into its own structure and module
psteinroe
added a commit
that referenced
this pull request
Oct 26, 2026
Based on #879, which needs to merge first. It doesn't depend on #903, #907 or #920. `allowStatementExecutionsAgainst` ignored the port. Patterns were matched against `host/database`, and the docs recommended `["localhost/*", "127.0.0.1/*"]`. A port forward, an SSH tunnel, `cloud-sql-proxy`, `fly proxy` or pgbouncer make a remote database reachable on `localhost` as well, on another port, so the recommended patterns also allowed executing statements against production: ```jsonc "allowStatementExecutionsAgainst": ["localhost/*"] // postgres://...@localhost:5432/postgres allowed // postgres://...@localhost:15432/postgres allowed too, e.g. an SSH tunnel to production ``` There was also no way to name the port: `localhost:5432/*` never matched, because the target string had no port. ### What changed - A pattern that names a port is matched against `host:port/database`: `127.0.0.1:54322/*` only allows port 54322, and `localhost:*/dev` allows `dev` on any port. A pattern names a port if its host part, the part before the first `/`, has exactly one `:`. - A pattern without a port keeps matching `host/database` and allows every port, as before. The docs say so and explain why to name the port. - An IPv6 address can't be combined with a port, since its colons are ambiguous. `::1/*` matches every port of `::1`, as before. - The docs use port-specific patterns: `127.0.0.1:5432/*`, and `127.0.0.1:54322/*` for a local Supabase project. The config field now has a description, so `docs/schema.json`, the CLI reference and the TS bindings explain the matching. - The disabled reason names the port and suggests a port-specific pattern: "Statement execution is disabled for `127.0.0.1:5432/postgres`. Add a matching pattern to `db.allowStatementExecutionsAgainst`, e.g. ["127.0.0.1:5432/*"]." - `DatabaseSettings::default().allow_statement_executions` is `false`. It made no difference before, because the default also disables the connection, but a default should not allow executions. ### For the changelog Behavior change: patterns in `db.allowStatementExecutionsAgainst` can name a port (`host:port/database`) and then only allow that port. Patterns without a port still allow every port. A pattern whose host part had exactly one `:`, which could never match before, now matches against `host:port/database`. Consider switching `localhost/*` to `localhost:5432/*`. ### Tests - `patterns_with_a_port_only_match_that_port`, `patterns_without_a_port_match_every_port` (including `::1/*`). - `connection_string_port_is_matched`: `localhost:54322/*` allows the connection string on port 54322 but not a tunnel on 15432. - `disabled_reason_suggests_a_pattern_with_the_port`, for IPv6 too, and `statement_executions_are_disallowed_by_default`. - The expected disabled reasons in the existing settings and workspace tests now include the port. ### Not covered Existing configs with `localhost/*` keep allowing every local port. Making port-less patterns match only the default port would break configs without a warning, so this PR only changes the docs and the suggested pattern.
psteinroe
changed the base branch from
fix/execute-statement-isolation
to
main
October 26, 2026 13:04
…type The registry of executed statements was a Mutex<Vec<RunningStatement>> on the WorkspaceServer, and execute_statement and cancel_statement_execution locked and searched it themselves. RunningStatements in features/code_actions/execute_statement/running.rs now owns it: start registers an execution and returns a guard and the cancel receiver, and the guard removes the execution when dropped, also if the execution panics. cancel cancels the executions of a statement. The check for COPY with the client moves next to the other statement checks in execution.rs.
…y test Since the code actions run the linter, the first execution of a workspace also connects and loads the schema, which can take more than the 3 s that the test allows for the query.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #990. (#907, #903 and #879 have merged since.)
Stacked on #907, which is stacked on #903 and #879. All three need to merge first.
Executing a statement from the editor had no guard rails:
#879 described truncation as "sqlx drains the rest of the result". What happened was that the pool's ping on release failed, the connection was closed, and Postgres kept running the query until it noticed the closed socket.
What changed
db.statementTimeoutSecsseconds, 60 by default, before Postgres cancels it (57014, returned like any other error since fix(workspace): only connection failures put the database into backoff #903). The timeout is set per execution withset_config, not as a startup option, so it works through pgbouncer and Supavisor.0keeps the database's ownstatement_timeout.$/cancelRequestfor theworkspace/executeCommandrequest cancels the statement in the database withpg_cancel_backendon the server's own pool. Before, the handler called the blocking workspace method directly, so the server could not handle any request until the statement finished. It now runs on a blocking thread. When tower-lsp drops the cancelled handler, a guard calls the newWorkspace::cancel_statement_execution.….truncatedValuesmarks the result then. The rows that would push the values past 1 MB are dropped, andtruncatedmarks the result.selectorvalueswithoutintoand without a data-modifyingwithruns asbegin; declare ... no scroll cursor for <statement>; fetch 101 ...; commit, so Postgres stops after 101 rows. The position of an error still points into the statement. Any other statement runs to completion: the rows after the first 100 are read and dropped, and aninsert ... returningof 1,000 rows inserts all 1,000. Closing the connection there would roll the statement back.COPY ... FROM STDINandCOPY ... TO STDOUTare refused before they reach the database: "COPY ... TO STDOUTcan't be executed from the editor. Use\copyin psql, or a file on the database server." COPY with a server file or a program still runs.sqlx::postgres::notice). The statement runs with a subscriber that collects these events and forwards every other event and span to the current subscriber. The result lists them asnotices("NOTICE: hello","WARNING: careful"), andmessageends with them, soshowMessageshows them too.insert,update,delete,mergeorshow, the connection's snapshot is dropped. The diagnostics refresh that the LSP runs after the command then loads the new schema.execute_statementno longer holds the read locks on the documents and settings, so editing a file or reloading the configuration doesn't wait for it.docs/guides/configure_database.mddescribes all of this, anddocs/schema.jsonand the TS bindings list the new setting.Tests
Workspace tests, which run against the test database:
execute_statement_times_out:pg_sleep(10)with a 1 s timeout returns57014.execute_statement_can_be_cancelled:cancel_statement_executionstops a runningpg_sleep(30), and the registry of running statements ends up empty.execute_statement_stops_a_truncated_query_early:select generate_series(1, 50000000)and the next statement together take less than 3 s.execute_statement_completes_a_truncated_command: a truncatedinsert ... returninginserts all its rows.execute_statement_cuts_long_values, including a cut in the middle of anä, andexecute_statement_caps_the_result_size.execute_statement_refuses_copy_with_the_client,execute_statement_returns_notices.executed_ddl_refreshes_the_snapshot: aftercreate table, another file loses its typecheck finding for the table.LSP test
cancel_request_cancels_the_executed_statement: sends$/cancelRequestwhilepg_sleep(30)runs. It checks that the response isRequestCancelledand that the query disappears frompg_stat_activity.Risks and not covered
nextval) don't happen, the same as withFETCH_COUNTin psql.Span::current()inside it returns no span. Creating the subscriber rebuilds tracing's callsite interest cache once per execution.$/cancelRequestfrom the client. The VS Code extension doesn't send it yet.Update:
features/code_actions/. The execution code is now infeatures/code_actions/execute_statement/execution.rs(wasworkspace/server/statement_execution.rs), andnotices.rssits next to it. The new constants and result fields live inexecute_statement.rsand are re-exported fromfeatures::code_actionsas before.ExecuteStatementParamsnow carries theproject_keyfrom main, and the settings come fromsettings_for(project_key, path).SnapshotManager::clear_connection, which main dropped as unused, is back for the snapshot refresh after DDL. The default ofallowStatementExecutionsstaysfalseas on main.RunningStatementsinexecute_statement/running.rs, instead of aMutex<Vec<RunningStatement>>onWorkspaceServer.start(¶ms)registers an execution and returns a guard and the cancel receiver. The guard removes the execution when it is dropped, so it is also removed if the execution panics, which the oldretainafter the call didn't cover.cancel(¶ms)cancels every execution of the statement.WorkspaceServer::execute_statementandcancel_statement_executioneach call it in one line. Unit tests cover cancelling and the guard.COPYcheck moved from the server intoexecution::refusal, next tois_queryandmay_change_schema.execute_statement_stops_a_truncated_query_earlyruns one statement before it starts the timer. Since the code actions run the linter, the first execution of a workspace also connects and loads the schema, which took longer than the 3 s the test allows on a loaded machine.Verified with
cargo clippy --workspace --all-targets -- -D warnings,cargo test -p pgls_workspace(206 passed),cargo test -p pgls_lsp,cargo test -p pgls_configuration, andjust gen-lint(no changes).