Skip to content

SET LOCAL support - #3101

Merged
zachmu merged 9 commits into
mainfrom
zachmu/set-local
Sep 1, 2026
Merged

SET LOCAL support#3101
zachmu merged 9 commits into
mainfrom
zachmu/set-local

Conversation

@zachmu

@zachmu zachmu commented Aug 17, 2026

Copy link
Copy Markdown
Member

This PR adds support for transaction-scoped session vars via SET LOCAL.

Companion PR: dolthub/go-mysql-server#3704

@itoqa

itoqa Bot commented Aug 17, 2026

Copy link
Copy Markdown

Ito QA test results
Commit: 9ee5b8d: 14 test cases ran, 14 passed ✅.

Summary

Coverage spans transaction-scoped configuration behavior across normal use, commits, rollbacks, savepoints, failed transactions, repeated changes, session-value preservation, planner visibility, and message boundaries. It also exercises edge cases and invalid or custom settings, with results indicating healthy behavior across these business-logic and database-boundary scenarios.

Safe to merge — the run found no failures attributable to this PR, regressions, or previously flagged issues, so there is no merge-blocking risk. The overall result is low risk.

Tests run by Ito

View full run

Result Severity Type Description
General An empty local setting is rejected, while a valid multi-value date setting keeps its value and rollback restores the session setting.
General Unknown settings and dotted settings are rejected when used with SET LOCAL. The regular session setting path still accepts custom.option and returns its value.
General The setting changed to off inside the transaction, and both SHOW and the query planner used that same value. After COMMIT, both returned to on.
General The database kept the temporary setting during the transaction, including after a query, and restored the original setting after COMMIT and in a new transaction.
General Local attempts to change a custom setting are rejected, while a regular session change succeeds and can be read later.
General The setting changed twice during one transaction and returned to its original value after commit.
General Repeated local settings used the newest value, then both commit and rollback restored the original session value.
Config The setting changed to off inside the transaction, and the value returned to on after COMMIT.
Local The planner setting changed to off during the transaction and returned to on after COMMIT.
Local After a statement failed, the transaction rejected the next query until rollback. Rolling back restored the setting to on.
Options The database rejected the namespaced custom option with the expected error, then completed the rollback.
Savepoint Rolling back to a savepoint kept the transaction usable and kept the planner setting off. Committing the outer transaction then restored the setting to on.
Session The connection kept its ordinary setting after a temporary transaction setting was removed. The value was off before the transaction, on inside it, and off again after COMMIT.
Session The later regular setting remained the final session value after the transaction ended.

Tip

Reply with @itoqa to send us feedback on this test run.

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor
Main PR
Total 42090 42090
Successful 19289 19360
Failures 22801 22730
Partial Successes1 5459 5464
Main PR
Successful 45.8280% 45.9967%
Failures 54.1720% 54.0033%

${\color{lightgreen}Progressions (62)}$

aggregates

QUERY: set local max_parallel_workers_per_gather = 0;
QUERY: select max(unique1) from tenk1 where unique1 > 42000;

cluster

QUERY: SET LOCAL enable_seqscan = false;
QUERY: SET LOCAL enable_seqscan = false;
QUERY: SET LOCAL enable_seqscan = false;

create_am

QUERY: SET LOCAL default_table_access_method = 'heap2';
QUERY: CREATE TABLE tableam_tbl_heapx(f1 int);

create_table_like

QUERY: SET LOCAL search_path = ctl_schema, public;

explain

QUERY: set local plan_cache_mode = force_generic_plan;

groupingsets

QUERY: SET LOCAL enable_hashagg = false;

guc

QUERY: SET LOCAL vacuum_cost_delay TO 50;
QUERY: SET LOCAL datestyle = 'SQL';
QUERY: SET LOCAL vacuum_cost_delay TO 50;
QUERY: SET LOCAL datestyle = 'SQL';
QUERY: SET LOCAL vacuum_cost_delay TO 30;
QUERY: SET LOCAL datestyle = 'Postgres, MDY';
QUERY: SET LOCAL vacuum_cost_delay TO 30;
QUERY: SET LOCAL datestyle = 'Postgres, MDY';
QUERY: RELEASE SAVEPOINT sp;
QUERY: SET LOCAL vacuum_cost_delay TO 50;
QUERY: SET datestyle = 'ISO, DMY';
QUERY: SET LOCAL datestyle = 'Postgres, MDY';

incremental_sort

QUERY: set local enable_hashjoin = off;
QUERY: set local enable_mergejoin = off;
QUERY: set local enable_material = off;
QUERY: set local enable_sort = off;

join_hash

QUERY: set local min_parallel_table_scan_size = 0;
QUERY: set local parallel_setup_cost = 0;
QUERY: set local enable_hashjoin = on;
QUERY: create or replace function find_hash(node json)
returns json language plpgsql
as
$$
declare
  x json;
  child json;
begin
  if node->>'Node Type' = 'Hash' then
    return node;
  else
    for child in select json_array_elements(node->'Plans')
    loop
      x := find_hash(child);
      if x is not null then
        return x;
      end if;
    end loop;
    return null;
  end if;
end;
$$;
QUERY: SET LOCAL enable_sort = OFF;
QUERY: SET LOCAL from_collapse_limit = 1;
QUERY: CREATE TABLE hjtest_1 (a text, b int, id int, c bool);
QUERY: CREATE TABLE hjtest_2 (a bool, id int, b text, c int);
QUERY: INSERT INTO hjtest_1(a, b, id, c) VALUES ('text', 2, 1, false);
QUERY: INSERT INTO hjtest_1(a, b, id, c) VALUES ('text', 1, 2, false);
QUERY: INSERT INTO hjtest_1(a, b, id, c) VALUES ('text', 20, 1, false);
QUERY: INSERT INTO hjtest_1(a, b, id, c) VALUES ('text', 1, 1, false);
QUERY: INSERT INTO hjtest_2(a, id, b, c) VALUES (true, 1, 'another', 2);
QUERY: INSERT INTO hjtest_2(a, id, b, c) VALUES (true, 3, 'another', 7);

Footnotes

  1. These are tests that we're marking as Successful, however they do not match the expected output in some way. This is due to small differences, such as different wording on the error messages, or the column names being incorrect while the data itself is correct.

@zachmu
zachmu requested a review from fulghum September 1, 2026 01:05
@itoqa

itoqa Bot commented Sep 1, 2026

Copy link
Copy Markdown

Ito QA test results
Ito Diff Report9ee5b8d51d8265: 18 test cases ran, 2 new failures ❌, 14 passing ✅, 2 additional findings ⚠️.

Diff Summary

The run covers transaction-setting behavior, rollback and cleanup, data import/export across text, CSV, and binary formats, corruption detection and repair, schema isolation, and catalog metadata. It exercises normal workflows plus malformed-input and failure-path cases, with overall coverage showing most core behaviors working but gaps in binary compatibility and error consistency.

Merge with caution — a medium-severity PR-attributable failure prevents binary data exports from being imported successfully, and a minor failure reports the wrong error for one rejected setting operation. Separate medium findings are not attributable to this PR and are caveats rather than merge drivers.

Tests run by Ito

View full run

Result State Severity Type Description
❌ New Failure Medium severity Copy The binary stream is written successfully, but the matching binary import stops with a partial-record error instead of restoring the four source rows.
❌ New Failure Minor severity Custom The SET LOCAL statement returns the unsupported custom-option error, but the equivalent set_config call returns a generic error saying transaction-local configuration is not supported. Both local custom-option entry points should return the custom-option error.
Passing General A bad later record caused the import to fail, and the table stayed empty after rollback. A corrected retry then inserted exactly three rows, including the expected NULL value.
Passing General The offline repair fixed the damaged database across its branches and working data. A follow-up scan found zero corrupt rows or values, so the database was safe to start again.
Passing General Verified acceptable by independent adversarial review: the observed behavior is intended and documented in this codebase. Review notes: The reported outcome is explicitly specified and tested by the new feature: a current-version sentinel means that database is not scanned on later startups, and the integration test intentionally installs that sentinel on the corrupt fixture and expects startup to succeed. The finding's proposed requirement that the marker be bound to database state contradicts this affirmative repository evidence…
Passing Config The database rejected the requested temporary setting with the expected error. The setting stayed on before and after the transaction.
Passing Integrity The integrity report identified corruption in the expected database, tables, branches, and working data, with nonzero row and adaptive-value counts.
Passing Rev The source keeps a SET LOCAL value inside its transaction and clears it when the transaction is committed. The earlier leak could not be checked again because the local test server was no longer available.
Passing Rev The database rejected an attempt to export a table directly to a server file. No file was created, and the same connection still returned the table row count afterward.
Passing Rev Verified acceptable by independent adversarial review: the observed behavior is intended and documented in this codebase. Review notes: The omission is real at the implementation level, but the finding turns the repair traversal scope into an unsupported report requirement. The newly added command explicitly and consistently defines report output as a branch-head summary, while repair deliberately has broader branch, tag, history, and working-set coverage; therefore missing tag and working-set report sections are documented design…
Passing Rev The setting changed to off during the multi-statement query and returned to on for the next standalone query.
Passing Rev Copying six rows out and back preserved delimiters, backslashes, newlines, empty text, and NULL values.
Passing Rev The corrected local check produced a valid binary stream, restored all three rows, and left the connection ready for another query. The earlier error came from the local fixture and a later unavailable runtime, not from a code-backed application defect.
Passing Rev Malformed binary imports were rejected, the target table stayed empty, and the connection accepted later statements.
Passing Rev Aborting a COPY stream removes rows received before the abort, and the connection remains ready for the next statement. The earlier failure came from closing the client, which completed the stream instead of sending a COPY FAIL message.
Passing Rev The qualified export returned the row from s1, and later unqualified lookup still used s2. The temporary schema change did not leak into the next statement.
⏸️ Skipped General An empty local setting is rejected, while a valid multi-value date setting keeps its value and rollback restores the session setting.
⏸️ Skipped General Unknown settings and dotted settings are rejected when used with SET LOCAL. The regular session setting path still accepts custom.option and returns its value.
⏸️ Skipped General The setting changed to off inside the transaction, and both SHOW and the query planner used that same value. After COMMIT, both returned to on.
⏸️ Skipped General The database kept the temporary setting during the transaction, including after a query, and restored the original setting after COMMIT and in a new transaction.
⏸️ Skipped General Local attempts to change a custom setting are rejected, while a regular session change succeeds and can be read later.
⏸️ Skipped General The setting changed twice during one transaction and returned to its original value after commit.
⏸️ Skipped General Repeated local settings used the newest value, then both commit and rollback restored the original session value.
⏸️ Skipped Local The planner setting changed to off during the transaction and returned to on after COMMIT.
⏸️ Skipped Local After a statement failed, the transaction rejected the next query until rollback. Rolling back restored the setting to on.
⏸️ Skipped Options The database rejected the namespaced custom option with the expected error, then completed the rollback.
⏸️ Skipped Savepoint Rolling back to a savepoint kept the transaction usable and kept the planner setting off. Committing the outer transaction then restored the setting to on.
⏸️ Skipped Session The connection kept its ordinary setting after a temporary transaction setting was removed. The value was off before the transaction, on inside it, and off again after COMMIT.
⏸️ Skipped Session The later regular setting remained the final session value after the transaction ended.
⚠️ Additional Finding Medium severity General The failed-state commands were rejected as expected, but ROLLBACK left enable_hashjoin at 0 instead of restoring the original value of 1.
⚠️ Additional Finding Medium severity Rev The three tables use the expected enum values, but information_schema.columns reports pg_catalog as udt_schema for all of them instead of reporting rev13_s1 or rev13_s2.
Additional Findings Details

These findings are unrelated to the current changes but were observed during testing.

🟡 Rollback leaves local setting changed
  • Severity: Medium Medium severity
  • Description: The failed-state commands were rejected as expected, but ROLLBACK left enable_hashjoin at 0 instead of restoring the original value of 1.
  • Impact: After a failed transaction is rolled back, the connection keeps the wrong planner setting. Later queries on that connection may use slower plans until the setting is corrected.
  • Steps to Reproduce:
    1. Connect to the local database and confirm that enable_hashjoin is 1.
    2. Begin a transaction and run SET LOCAL enable_hashjoin = off.
    3. Run a statement that fails, then try SET LOCAL again and issue another BEGIN while the transaction is failed.
    4. Run ROLLBACK and check enable_hashjoin.
    5. Begin and commit a fresh transaction, then check the setting again.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: The SET LOCAL path in server/config/parameters.go:222-234 calls ctx.Session.SetTransactionLocalVariable for the local scope, so the setting is stored as transaction-local state while current_setting reads the effective value through PgsqlScope.GetValue at lines 239-254. The connection handler is responsible for removing that overlay: handleQueryOutsideEngine calls clearTransactionLocalVars before engine rollback for failed COMMIT at server/connection_handler.go:603-612 and for ROLLBACK at lines 618-621; implicit rollback and commit also call it at lines 1346-1357 and 1326-1343. clearTransactionLocalVars creates a new context and invokes ctx.Session.ClearTransactionLocalVariables at lines 1361-1373. The recorded SQL output shows that the error correctly moves the transaction into the failed state and rejects subsequent commands, but after ROLLBACK the overlay remains active (0 rather than baseline 1). That directly indicates the cleanup call is not clearing the transaction-local state for this connection or is acting on a session/context that does not own the overlay. The smallest practical fix is to clear the transaction-local map on the same session instance used by SET LOCAL, and to verify the clear operation succeeds before completing ROLLBACK; add or repair the failed-transaction rollback test so the post-rollback value is the saved session value.
Evidence Package
🟡 Custom type schema is reported incorrectly
  • Severity: Medium Medium severity
  • Description: The three tables use the expected enum values, but information_schema.columns reports pg_catalog as udt_schema for all of them instead of reporting rev13_s1 or rev13_s2.
  • Impact: Database tools and applications that inspect column metadata may assign a custom type to the wrong schema. Same-named types can become indistinguishable, although storing and casting the enum values still works.
  • Steps to Reproduce:
    1. Create schemas rev13_s1 and rev13_s2.
    2. Create an enum named mood in each schema and create a table in each schema with a column using that schema's enum.
    3. Set the search path to rev13_s2 and create another table whose column uses the unqualified mood type.
    4. Query information_schema.columns for the three tables and compare udt_schema and udt_name with the explicit casts to rev13_s1.mood and rev13_s2.mood.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: server/tables/information_schema/columns_table.go:getRowFromColumn (lines 131-198) receives the table schema as schName and correctly places it in table_schema at line 155. It derives dataType and udtName at line 138, but line 180 unconditionally sets udt_schema to the literal pg_catalog. This means the row cannot preserve the namespace of a user-defined enum, domain, or other custom type, even though the type resolver and pg_type cache retain schema-qualified user types. The captured local query output confirms the same wrong value for rev13_s1.t, rev13_s2.t, and the unqualified table. The smallest practical fix is to resolve the column's underlying type schema while computing the UDT metadata, pass that schema into getRowFromColumn, and use it at the udt_schema position; built-in types should continue to map to pg_catalog.
Evidence Package

Tip

Reply with @itoqa to send us feedback on this test run.

Comment thread server/connection_handler.go
Comment thread server/functions/set_config.go

@fulghum fulghum left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Looks like the TestSet generated test needs a quick update from the new syntax support.

@coffeegoddd

coffeegoddd commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

@zachmu DOLT

read_tests from_latency to_latency percent_change
covering_index_scan_postgres 2.43 2.43 0.0
groupby_scan_postgres 77.19 77.19 0.0
index_join_postgres 2.26 2.26 0.0
index_join_scan_postgres 1.61 1.61 0.0
index_scan_postgres 475.79 475.79 0.0
oltp_point_select 0.37 0.37 0.0
oltp_read_only 6.43 6.32 -1.71
select_random_points 0.72 0.72 0.0
select_random_ranges 1.03 1.03 0.0
table_scan_postgres 475.79 484.44 1.82
types_table_scan_postgres 1191.92 1191.92 0.0
write_tests from_latency to_latency percent_change
oltp_delete_insert_postgres 6.67 6.79 1.8
oltp_insert 3.36 3.36 0.0
oltp_read_write 13.46 13.46 0.0
oltp_update_index 3.62 3.62 0.0
oltp_update_non_index 3.25 3.25 0.0
oltp_write_only 7.04 7.04 0.0
types_delete_insert_postgres 7.17 7.17 0.0

@zachmu
zachmu merged commit ff17a32 into main Sep 1, 2026
25 checks passed
@zachmu
zachmu deleted the zachmu/set-local branch September 1, 2026 20:22
@itoqa

itoqa Bot commented Sep 1, 2026

Copy link
Copy Markdown

Ito QA test results
Ito Diff Report51d82652011a09: 10 test cases ran, 1 fixed ✅, 8 passing ✅, 1 additional finding ⚠️.

Diff Summary

Coverage focuses on transaction-scoped configuration behavior across normal commit and rollback flows, failed statements, concurrent sessions, and multi-statement use. It also exercises edge cases such as invalid, unknown, and custom settings, plus compatibility checks for configuration formatting and behavior outside the change.

Safe to merge — the only failure is a medium-severity configuration-formatting issue explicitly assessed as unrelated to this PR, while the change's transaction isolation and cleanup behavior passed. The unrelated finding is a flag for later rather than a merge blocker.

Tests run by Ito

View full run

Result State Severity Type Description
❌->✅ Fixed General The setting changed only inside the first transaction, reset after commit, and stayed correct after a second transaction was rolled back.
Passing General Dates use the temporary display style only inside each transaction. After both saving and cancelling a transaction, dates return to the normal display style.
Passing General After a statement failed, committing the transaction cleared the temporary setting. The next transaction used the original value and the connection still worked.
Passing General Two database sessions changed the same setting independently. Each session kept its own value until its transaction ended, and cleanup in one session did not change the other.
Passing Config Verified acceptable by independent adversarial review: the observed behavior is intended and documented in this codebase. Review notes: The claimed failure is contradicted by affirmative repository expectations: enable_seqscan and many other boolean configuration parameters are intentionally exposed by current_setting as text containing their numeric system-variable representation. That may differ from PostgreSQL's preferred on/off spelling, but this repository explicitly and repeatedly tests the observed value as correct, so the …
Passing Config An invalid array value was rejected before it could change the setting. The setting stayed at its original value after the transaction was rolled back.
Passing Local The setting changed to off inside the transaction and returned to its original value after COMMIT.
Passing Local The database rejected an unknown setting, and the connection stayed usable after rollback.
Passing Local The database rejected a namespaced transaction setting with the expected unsupported-operation error. Rolling back the transaction succeeded, and the same connection could run SELECT 1 afterward.
⏸️ Skipped General A bad later record caused the import to fail, and the table stayed empty after rollback. A corrected retry then inserted exactly three rows, including the expected NULL value.
⏸️ Skipped General The offline repair fixed the damaged database across its branches and working data. A follow-up scan found zero corrupt rows or values, so the database was safe to start again.
⏸️ Skipped General Verified acceptable by independent adversarial review: the observed behavior is intended and documented in this codebase. Review notes: The reported outcome is explicitly specified and tested by the new feature: a current-version sentinel means that database is not scanned on later startups, and the integration test intentionally installs that sentinel on the corrupt fixture and expects startup to succeed. The finding's proposed requirement that the marker be bound to database state contradicts this affirmative repository evidence…
⏸️ Skipped Integrity The integrity report identified corruption in the expected database, tables, branches, and working data, with nonzero row and adaptive-value counts.
⏸️ Skipped Rev The source keeps a SET LOCAL value inside its transaction and clears it when the transaction is committed. The earlier leak could not be checked again because the local test server was no longer available.
⏸️ Skipped Rev The database rejected an attempt to export a table directly to a server file. No file was created, and the same connection still returned the table row count afterward.
⏸️ Skipped Rev Verified acceptable by independent adversarial review: the observed behavior is intended and documented in this codebase. Review notes: The omission is real at the implementation level, but the finding turns the repair traversal scope into an unsupported report requirement. The newly added command explicitly and consistently defines report output as a branch-head summary, while repair deliberately has broader branch, tag, history, and working-set coverage; therefore missing tag and working-set report sections are documented design…
⏸️ Skipped Rev The setting changed to off during the multi-statement query and returned to on for the next standalone query.
⏸️ Skipped Rev Copying six rows out and back preserved delimiters, backslashes, newlines, empty text, and NULL values.
⏸️ Skipped Rev The corrected local check produced a valid binary stream, restored all three rows, and left the connection ready for another query. The earlier error came from the local fixture and a later unavailable runtime, not from a code-backed application defect.
⏸️ Skipped Rev Malformed binary imports were rejected, the target table stayed empty, and the connection accepted later statements.
⏸️ Skipped Rev Aborting a COPY stream removes rows received before the abort, and the connection remains ready for the next statement. The earlier failure came from closing the client, which completed the stream instead of sending a COPY FAIL message.
⏸️ Skipped Rev The qualified export returned the row from s1, and later unqualified lookup still used s2. The temporary schema change did not leak into the next statement.
⚠️ Additional Finding Medium severity Config The local assignment returns off, but reading the active value returns 0 instead of off. The rollback step restores the original setting value of 1.
Tests that are no longer relevant

Below are tests that previously ran and are no longer relevant:

Type Test Description
Copy Binary COPY round trips fail on import Dropped because COPY is outside this commit's four-file SET LOCAL scope and has no changed dependency or affected path requiring retest.
Custom Local custom setting shows wrong error Dropped because The prior custom-option test is outside the changed files; the current plan's LOCAL-8 covers the preserved rejection boundary, so no separate prior test is needed.
Rev Custom type schema is reported incorrectly Dropped because Schema and user-defined-type resolution are outside this commit's changed files and affected behavior, so the prior failure is obsolete for this diff.
Additional Findings Details

These findings are unrelated to the current changes but were observed during testing.

🟡 Rollback leaves the setting restored but misreported
  • Severity: Medium Medium severity
  • Description: The local assignment returns off, but reading the active value returns 0 instead of off. The rollback step restores the original setting value of 1.
  • Impact: Clients that inspect a transaction-local setting can receive 0 or 1 instead of off or on. Code that compares the returned value as text may make the wrong decision or report misleading configuration state.
  • Steps to Reproduce:
    1. Record SHOW enable_seqscan in a PostgreSQL session.
    2. Begin a transaction and run SELECT set_config('enable_seqscan', 'off', true).
    3. Run SELECT current_setting('enable_seqscan') while the transaction is active.
    4. Roll back the transaction and run SHOW enable_seqscan again.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: server/functions/set_config.go:59-63 sends is_local=true assignments to ctx.Session.SetTransactionLocalVariable using the supplied string value. For enable_seqscan, server/config/parameters_list.go:1276-1285 declares the setting as a boolean system parameter whose engine representation is int8 (the observed values are 1 and 0). server/functions/current_setting.go:64-77 first checks user scope, then server/functions/current_setting.go:88-98 reads the session-visible value and returns fmt.Sprintf("%v", variable). That generic formatting converts the boolean parameter's engine value to the numeric text 0 rather than PostgreSQL's required text off. The PR diff in server/functions/set_config.go:44-48 removes the prior rejection of local mode, but does not change current_setting.go, so the smallest fix is to normalize boolean system values to PostgreSQL text (on/off) in the current_setting return path, while leaving rollback cleanup unchanged.
Evidence Package

Tip

Reply with @itoqa to send us feedback on this test run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants