> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cockroachlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# What's New in v26.4

export const InternalLink = ({version, path = "", children, ...props}) => {
  let detectedVersion = version || "stable";
  if (typeof window !== 'undefined' && !version) {
    const match = window.location.pathname.match(/\/docs\/([^/]+)/);
    if (match) {
      detectedVersion = match[1];
    }
  }
  const normalizedPath = path.startsWith("/") ? path.slice(1) : path;
  return <a href={`/docs/${detectedVersion}/${normalizedPath}`} {...props}>
      {children}
    </a>;
};

export const MarketoEmailForm = ({successMessage = "Thanks!", formId = 1083}) => {
  useEffect(() => {
    function initializeReleaseNotesSignup() {
      if (window.__cockroachReleaseNotesSignupInitialized) {
        if (window.__cockroachReleaseNotesRefresh) {
          window.__cockroachReleaseNotesRefresh();
        }
        return;
      }
      window.__cockroachReleaseNotesSignupInitialized = true;
      const MARKETO_BASE_URL = "https://go.cockroachlabs.com";
      const MARKETO_MUNCHKIN_ID = "350-QIN-827";
      const MARKETO_FORMS_SCRIPT = "https://go.cockroachlabs.com/js/forms2/js/forms2.min.js";
      const DEFAULT_FORM_ID = 1083;
      const EMAIL_ERROR = "Enter a valid email address.";
      const FORM_LOAD_ERROR = "Unable to load the release notes form. Disable content blockers and try again.";
      const LOCALHOST_ERROR = "Marketo rejects submissions from localhost. Test this form on a deployed preview URL.";
      const SUCCESS_MESSAGE = "Thanks!";
      const WIDGET_SELECTOR = "[data-release-notes-signup]";
      const FORM_MOUNT_ID = "cockroachReleaseNotesFormMount";
      const SUBMIT_FRAME_NAME = "cockroachReleaseNotesSubmitFrame";
      const widgetState = new WeakMap();
      let marketoScriptPromise;
      const marketoFormPromises = {};
      let activeWidget = null;
      function getWidgetFormId(widget) {
        const formId = widget && Number(widget.getAttribute("data-form-id"));
        return formId || DEFAULT_FORM_ID;
      }
      function toSecureMarketoUrl(url) {
        if (typeof url !== "string") {
          return url;
        }
        return url.replace("http://go.cockroachlabs.com", "https://go.cockroachlabs.com");
      }
      function patchMarketoFrameTransport() {
        if (window.__cockroachReleaseNotesFrameTransportPatched || typeof HTMLIFrameElement === "undefined") {
          return;
        }
        const iframeProto = HTMLIFrameElement.prototype;
        const originalSetAttribute = iframeProto.setAttribute;
        iframeProto.setAttribute = function (name, value) {
          if (name === "src") {
            value = toSecureMarketoUrl(value);
          }
          return originalSetAttribute.call(this, name, value);
        };
        const srcDescriptor = Object.getOwnPropertyDescriptor(iframeProto, "src");
        if (srcDescriptor && typeof srcDescriptor.get === "function" && typeof srcDescriptor.set === "function") {
          try {
            Object.defineProperty(iframeProto, "src", {
              configurable: true,
              enumerable: srcDescriptor.enumerable,
              get: function () {
                return srcDescriptor.get.call(this);
              },
              set: function (value) {
                srcDescriptor.set.call(this, toSecureMarketoUrl(value));
              }
            });
          } catch (error) {}
        }
        window.__cockroachReleaseNotesFrameTransportPatched = true;
      }
      function getState(widget) {
        let state = widgetState.get(widget);
        if (!state) {
          state = {
            error: "",
            isSubmitted: false,
            isSubmitting: false,
            successMessage: widget.getAttribute("data-success-message") || SUCCESS_MESSAGE,
            submitTimeoutId: null
          };
          widgetState.set(widget, state);
        }
        return state;
      }
      function getElements(widget) {
        return {
          emailInput: widget.querySelector("[data-release-notes-email]"),
          error: widget.querySelector("[data-release-notes-error]"),
          errorText: widget.querySelector("[data-release-notes-error-text]"),
          submitButton: widget.querySelector("[data-release-notes-submit]"),
          success: widget.querySelector("[data-release-notes-success]"),
          successText: widget.querySelector("[data-release-notes-success-text]")
        };
      }
      function ensureFormMount() {
        let mount = document.getElementById(FORM_MOUNT_ID);
        if (mount) {
          return mount;
        }
        mount = document.createElement("div");
        mount.id = FORM_MOUNT_ID;
        mount.setAttribute("aria-hidden", "true");
        mount.style.display = "none";
        document.body.appendChild(mount);
        return mount;
      }
      function getDomForm(form) {
        const formElement = typeof form.getFormElem === "function" ? form.getFormElem() : null;
        return formElement && formElement[0] ? formElement[0] : formElement;
      }
      function mountMarketoForm(form) {
        const domForm = getDomForm(form);
        if (!domForm || domForm.isConnected) {
          return domForm;
        }
        ensureFormMount().appendChild(domForm);
        return domForm;
      }
      function applyFormValues(form, values) {
        if (typeof form.setValues === "function") {
          form.setValues(values);
          return;
        }
        if (typeof form.vals === "function") {
          form.vals(values);
        }
      }
      function ensureSubmitFrame() {
        let frame = document.querySelector('iframe[name="' + SUBMIT_FRAME_NAME + '"]');
        if (frame) {
          return frame;
        }
        frame = document.createElement("iframe");
        frame.name = SUBMIT_FRAME_NAME;
        frame.setAttribute("aria-hidden", "true");
        frame.tabIndex = -1;
        frame.style.display = "none";
        document.body.appendChild(frame);
        return frame;
      }
      function clearSubmitTimeout(widget) {
        const state = getState(widget);
        if (state.submitTimeoutId) {
          window.clearTimeout(state.submitTimeoutId);
          state.submitTimeoutId = null;
        }
      }
      function forceSecureMarketoFrame() {
        const frame = document.querySelector("#MktoForms2XDIframe");
        if (!frame) {
          return;
        }
        const currentSrc = frame.getAttribute("src") || frame.src;
        if (!currentSrc) {
          return;
        }
        const secureSrc = toSecureMarketoUrl(currentSrc);
        if (secureSrc !== currentSrc) {
          frame.setAttribute("src", secureSrc);
        }
      }
      function ensureSecureMarketoFrame() {
        patchMarketoFrameTransport();
        forceSecureMarketoFrame();
        if (window.__cockroachReleaseNotesFrameObserverInitialized || typeof MutationObserver === "undefined") {
          return;
        }
        const observer = new MutationObserver(function () {
          forceSecureMarketoFrame();
        });
        observer.observe(document.documentElement, {
          childList: true,
          subtree: true,
          attributes: true,
          attributeFilter: ["src"]
        });
        window.__cockroachReleaseNotesFrameObserverInitialized = true;
        window.__cockroachReleaseNotesFrameObserver = observer;
      }
      function render(widget) {
        if (!widget || !document.contains(widget)) {
          return;
        }
        const state = getState(widget);
        const elements = getElements(widget);
        if (elements.submitButton) {
          elements.submitButton.disabled = state.isSubmitting;
        }
        if (elements.emailInput) {
          elements.emailInput.setAttribute("aria-invalid", state.error ? "true" : "false");
        }
        if (elements.error) {
          elements.error.hidden = !state.error;
          elements.error.style.display = state.error ? "block" : "none";
        }
        if (elements.errorText) {
          elements.errorText.textContent = state.error;
        }
        if (elements.success) {
          elements.success.hidden = !state.isSubmitted;
          elements.success.style.display = state.isSubmitted ? "block" : "none";
        }
        if (elements.successText) {
          elements.successText.textContent = state.successMessage || SUCCESS_MESSAGE;
        }
      }
      function hideMarketoForm(form) {
        const formElement = form && typeof form.getFormElem === "function" ? form.getFormElem() : null;
        if (!formElement) {
          return;
        }
        if (typeof formElement.hide === "function") {
          formElement.hide();
          return;
        }
        if (formElement[0] && formElement[0].style) {
          formElement[0].style.display = "none";
          return;
        }
        if (formElement.style) {
          formElement.style.display = "none";
        }
      }
      function isValidEmail(value) {
        return (/^[^\s@]+@[^\s@]+\.[^\s@]+$/).test(value);
      }
      function isFormLoadError(error) {
        return Boolean(error && typeof error === "object" && (error.code === "marketo_load_failed" || error.code === "marketo_not_available" || error.code === "marketo_form_init_failed"));
      }
      function isLocalhost() {
        return window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
      }
      function loadMarketoForms() {
        ensureSecureMarketoFrame();
        if (window.MktoForms2) {
          return Promise.resolve(window.MktoForms2);
        }
        if (!marketoScriptPromise) {
          marketoScriptPromise = new Promise(function (resolve, reject) {
            function finalizeLoad() {
              if (window.MktoForms2) {
                forceSecureMarketoFrame();
                resolve(window.MktoForms2);
                return;
              }
              marketoScriptPromise = null;
              const error = new Error("MktoForms2 did not load.");
              error.code = "marketo_not_available";
              reject(error);
            }
            function handleError() {
              marketoScriptPromise = null;
              const error = new Error("Failed to load Marketo forms.");
              error.code = "marketo_load_failed";
              reject(error);
            }
            const existingScript = document.querySelector('script[src="' + MARKETO_FORMS_SCRIPT + '"]');
            if (existingScript) {
              existingScript.addEventListener("load", finalizeLoad, {
                once: true
              });
              existingScript.addEventListener("error", handleError, {
                once: true
              });
              return;
            }
            const script = document.createElement("script");
            script.src = MARKETO_FORMS_SCRIPT;
            script.async = true;
            script.addEventListener("load", finalizeLoad, {
              once: true
            });
            script.addEventListener("error", handleError, {
              once: true
            });
            document.body.appendChild(script);
          });
        }
        return marketoScriptPromise;
      }
      function attachFormCallbacks(form) {
        if (form.__releaseNotesCallbacksAttached) {
          return;
        }
        form.__releaseNotesCallbacksAttached = true;
        form.onSuccess(function () {
          if (!activeWidget || !document.contains(activeWidget)) {
            return false;
          }
          const state = getState(activeWidget);
          clearSubmitTimeout(activeWidget);
          state.isSubmitting = false;
          state.isSubmitted = true;
          state.error = "";
          state.successMessage = activeWidget.getAttribute("data-success-message") || SUCCESS_MESSAGE;
          render(activeWidget);
          return false;
        });
        form.onValidate(function (isValid) {
          if (isValid || !activeWidget || !document.contains(activeWidget)) {
            return;
          }
          const state = getState(activeWidget);
          clearSubmitTimeout(activeWidget);
          state.isSubmitting = false;
          state.error = EMAIL_ERROR;
          render(activeWidget);
        });
      }
      function ensureForm(formId) {
        if (!marketoFormPromises[formId]) {
          marketoFormPromises[formId] = loadMarketoForms().then(function (MktoForms2) {
            const existingForm = typeof MktoForms2.getForm === "function" ? MktoForms2.getForm(formId) : null;
            if (existingForm) {
              forceSecureMarketoFrame();
              mountMarketoForm(existingForm);
              hideMarketoForm(existingForm);
              attachFormCallbacks(existingForm);
              ensureSubmitFrame();
              return existingForm;
            }
            return new Promise(function (resolve, reject) {
              MktoForms2.loadForm(MARKETO_BASE_URL, MARKETO_MUNCHKIN_ID, formId, function (form) {
                if (!form) {
                  const error = new Error("Marketo form failed to initialize.");
                  error.code = "marketo_form_init_failed";
                  reject(error);
                  return;
                }
                forceSecureMarketoFrame();
                mountMarketoForm(form);
                hideMarketoForm(form);
                attachFormCallbacks(form);
                ensureSubmitFrame();
                resolve(form);
              });
            });
          }).catch(function (error) {
            marketoFormPromises[formId] = null;
            throw error;
          });
        }
        return marketoFormPromises[formId];
      }
      function handleEmailInput(target) {
        const widget = target.closest(WIDGET_SELECTOR);
        if (!widget) {
          return;
        }
        const state = getState(widget);
        if (!state.error) {
          return;
        }
        state.error = "";
        render(widget);
      }
      function handleSubmit(button) {
        const widget = button.closest(WIDGET_SELECTOR);
        if (!widget) {
          return;
        }
        const state = getState(widget);
        const elements = getElements(widget);
        if (!elements.emailInput) {
          return;
        }
        const email = elements.emailInput.value.trim();
        if (!email) {
          state.error = EMAIL_ERROR;
          render(widget);
          return;
        }
        state.error = "";
        state.isSubmitted = false;
        state.isSubmitting = true;
        state.successMessage = widget.getAttribute("data-success-message") || SUCCESS_MESSAGE;
        render(widget);
        if (!isValidEmail(email)) {
          state.isSubmitting = false;
          state.error = EMAIL_ERROR;
          render(widget);
          return;
        }
        if (isLocalhost()) {
          state.isSubmitting = false;
          state.error = LOCALHOST_ERROR;
          render(widget);
          return;
        }
        activeWidget = widget;
        ensureForm(getWidgetFormId(widget)).then(function (form) {
          const formValues = {
            Email: email,
            Send_me_product_and_feature_updates__c: "TRUE",
            subscriptionProductUpdates: "TRUE",
            optin: "TRUE"
          };
          applyFormValues(form, formValues);
          if (typeof form.validate === "function" && !form.validate()) {
            state.isSubmitting = false;
            state.error = EMAIL_ERROR;
            render(widget);
            return;
          }
          clearSubmitTimeout(widget);
          state.submitTimeoutId = window.setTimeout(function () {
            state.isSubmitting = false;
            state.error = EMAIL_ERROR;
            render(widget);
          }, 10000);
          if (typeof form.submit !== "function") {
            throw new Error("marketo_submit_missing");
          }
          form.submit();
        }).catch(function (error) {
          clearSubmitTimeout(widget);
          state.isSubmitting = false;
          state.error = isFormLoadError(error) ? FORM_LOAD_ERROR : EMAIL_ERROR;
          render(widget);
        });
      }
      document.addEventListener("click", function (event) {
        const submitButton = event.target.closest("[data-release-notes-submit]");
        if (!submitButton) {
          return;
        }
        event.preventDefault();
        handleSubmit(submitButton);
      });
      document.addEventListener("input", function (event) {
        if (!(event.target instanceof HTMLInputElement) || !event.target.matches("[data-release-notes-email]")) {
          return;
        }
        handleEmailInput(event.target);
      });
      window.__cockroachReleaseNotesRefresh = function () {
        ensureSecureMarketoFrame();
        const widgets = document.querySelectorAll(WIDGET_SELECTOR);
        widgets.forEach(function (widget) {
          render(widget);
          ensureForm(getWidgetFormId(widget)).catch(function () {});
        });
      };
      window.__cockroachReleaseNotesRefresh();
    }
    initializeReleaseNotesSignup();
  }, []);
  return <div data-release-notes-signup data-form-id={formId} data-success-message={successMessage} className="not-prose my-4 max-w-xl">
      <div className="flex flex-col gap-3 sm:flex-row sm:items-start">
        <input data-release-notes-email type="email" inputMode="email" autoComplete="email" placeholder="Email*" aria-label="Email" className="min-w-0 flex-1 rounded-2xl border border-gray-300 bg-white px-4 py-3 text-sm text-gray-900 shadow-sm outline-none transition focus:border-primary dark:border-gray-700 dark:bg-gray-950 dark:text-white" />
        <button data-release-notes-submit type="button" className="inline-flex items-center justify-center rounded-2xl bg-primary px-5 py-3 text-sm font-semibold text-white transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-70">
          Submit
        </button>
      </div>

      <div data-release-notes-error hidden className="mt-3 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/50 dark:bg-red-950/40 dark:text-red-300">
        <span data-release-notes-error-text />
      </div>

      <div data-release-notes-success hidden className="mt-3 rounded-2xl border border-green-200 bg-green-50 px-4 py-3 text-sm font-medium text-green-800 dark:border-green-900/50 dark:bg-green-950/40 dark:text-green-200">
        <span data-release-notes-success-text>{successMessage}</span>
      </div>
    </div>;
};

<Note>
  The releases on this page are testing releases, not supported or intended for production environments. The new features and bug fixes noted on this page may not yet be documented across CockroachDB's documentation.

  * **CockroachDB self-hosted**: All v26.4 testing binaries and Docker images are available for download.
  * **CockroachDB Advanced**: v26.4 testing releases are not yet available.
  * **CockroachDB Standard** and **Basic**: v26.4 testing releases are not available.

  When v26.4 becomes Generally Available (GA), a new v26.4.0 section on this page will describe key features and additional upgrade considerations.
</Note>

CockroachDB v26.4 is in active development, and the following <InternalLink path="index#release-schedule">testing releases</InternalLink> are intended for testing and experimentation only, and are not qualified for production environments or eligible for support or uptime SLA commitments. When CockroachDB v26.4 is Generally Available (GA), production releases will also be announced on this page.

* For details about release types, naming, and licensing, refer to the <InternalLink path="index">Releases</InternalLink> page.
* Be sure to also review the <InternalLink path="release-support-policy">Release Support Policy</InternalLink>.
* After downloading a supported CockroachDB binary, learn how to <InternalLink version="stable" path="install-cockroachdb">install CockroachDB</InternalLink> or <InternalLink version="stable" path="upgrade-cockroach-version">upgrade your cluster</InternalLink>.

Get future release notes emailed to you:

<MarketoEmailForm />

## v26.4.0-alpha.1

Release Date: September 9, 2026

### Downloads

<Danger>
  CockroachDB v26.4.0-alpha.1 is a testing release. Testing releases are intended for testing and experimentation only, and are not qualified for production environments and not eligible for support or uptime SLA commitments.
</Danger>

<Note>
  Experimental downloads are not qualified for production use and not eligible for support or uptime SLA commitments, whether they are for testing releases or production releases.
</Note>

<table><thead><tr><th>Operating System</th><th>Architecture</th><th>Full executable</th><th>SQL-only executable</th></tr></thead><tbody><tr><td rowspan="2">Linux</td><td>Intel</td><td><a href="https://binaries.cockroachdb.com/cockroach-v26.4.0-alpha.1.linux-amd64.tgz">cockroach-v26.4.0-alpha.1.linux-amd64.tgz</a><br />(<a href="https://binaries.cockroachdb.com/cockroach-v26.4.0-alpha.1.linux-amd64.tgz.sha256sum">SHA256</a>)</td><td><a href="https://binaries.cockroachdb.com/cockroach-sql-v26.4.0-alpha.1.linux-amd64.tgz">cockroach-sql-v26.4.0-alpha.1.linux-amd64.tgz</a><br />(<a href="https://binaries.cockroachdb.com/cockroach-sql-v26.4.0-alpha.1.linux-amd64.tgz.sha256sum">SHA256</a>)</td></tr><tr><td>ARM</td><td><a href="https://binaries.cockroachdb.com/cockroach-v26.4.0-alpha.1.linux-arm64.tgz">cockroach-v26.4.0-alpha.1.linux-arm64.tgz</a><br />(<a href="https://binaries.cockroachdb.com/cockroach-v26.4.0-alpha.1.linux-arm64.tgz.sha256sum">SHA256</a>)</td><td><a href="https://binaries.cockroachdb.com/cockroach-sql-v26.4.0-alpha.1.linux-arm64.tgz">cockroach-sql-v26.4.0-alpha.1.linux-arm64.tgz</a><br />(<a href="https://binaries.cockroachdb.com/cockroach-sql-v26.4.0-alpha.1.linux-arm64.tgz.sha256sum">SHA256</a>)</td></tr><tr><td rowspan="2">Mac<br />(Experimental)</td><td>Intel</td><td><a href="https://binaries.cockroachdb.com/cockroach-v26.4.0-alpha.1.darwin-10.9-amd64.tgz">cockroach-v26.4.0-alpha.1.darwin-10.9-amd64.tgz</a><br />(<a href="https://binaries.cockroachdb.com/cockroach-v26.4.0-alpha.1.darwin-10.9-amd64.tgz.sha256sum">SHA256</a>)</td><td><a href="https://binaries.cockroachdb.com/cockroach-sql-v26.4.0-alpha.1.darwin-10.9-amd64.tgz">cockroach-sql-v26.4.0-alpha.1.darwin-10.9-amd64.tgz</a><br />(<a href="https://binaries.cockroachdb.com/cockroach-sql-v26.4.0-alpha.1.darwin-10.9-amd64.tgz.sha256sum">SHA256</a>)</td></tr><tr><td>ARM</td><td><a href="https://binaries.cockroachdb.com/cockroach-v26.4.0-alpha.1.darwin-11.0-arm64.tgz">cockroach-v26.4.0-alpha.1.darwin-11.0-arm64.tgz</a><br />(<a href="https://binaries.cockroachdb.com/cockroach-v26.4.0-alpha.1.darwin-11.0-arm64.tgz.sha256sum">SHA256</a>)</td><td><a href="https://binaries.cockroachdb.com/cockroach-sql-v26.4.0-alpha.1.darwin-11.0-arm64.tgz">cockroach-sql-v26.4.0-alpha.1.darwin-11.0-arm64.tgz</a><br />(<a href="https://binaries.cockroachdb.com/cockroach-sql-v26.4.0-alpha.1.darwin-11.0-arm64.tgz.sha256sum">SHA256</a>)</td></tr><tr><td>Windows<br />(Experimental)</td><td>Intel</td><td><a href="https://binaries.cockroachdb.com/cockroach-v26.4.0-alpha.1.windows-6.2-amd64.zip">cockroach-v26.4.0-alpha.1.windows-6.2-amd64.zip</a><br />(<a href="https://binaries.cockroachdb.com/cockroach-v26.4.0-alpha.1.windows-6.2-amd64.zip.sha256sum">SHA256</a>)</td><td><a href="https://binaries.cockroachdb.com/cockroach-sql-v26.4.0-alpha.1.windows-6.2-amd64.zip">cockroach-sql-v26.4.0-alpha.1.windows-6.2-amd64.zip</a><br />(<a href="https://binaries.cockroachdb.com/cockroach-sql-v26.4.0-alpha.1.windows-6.2-amd64.zip.sha256sum">SHA256</a>)</td></tr></tbody></table>

### Docker image

[Multi-platform images](https://docs.docker.com/build/building/multi-platform) include support for both Intel and ARM. Multi-platform images do not take up additional space on your Docker host.

Within the multi-platform image, both Intel and ARM images are available for testing.

To download the Docker image:

```shell theme={"theme":{"light":"catppuccin-mocha","dark":"catppuccin-mocha"}}
docker pull cockroachdb/cockroach-unstable:v26.4.0-alpha.1
```

### Backward-incompatible changes

* `ALTER FUNCTION ... RENAME` and `ALTER PROCEDURE ... RENAME` now require the `CREATE` privilege on the schema containing the object, matching PostgreSQL behavior. Renames in the `public` schema are unaffected in the default configuration because all users have `CREATE` on that schema.
* Fixed a security issue with SAN-required client certificate authentication (`security.client_cert.san_required.enabled`): if an HBA certificate rule omitted the `map` option, CockroachDB could accept a client certificate without verifying it was bound to the requested non-privileged SQL user. After this change, non-privileged users must be explicitly bound to the certificate via an identity map (`map` on the certificate HBA rule configured with `server.identity_map.configuration`) or an exact subject DN (the `SUBJECT` role option); otherwise, the login is rejected. If you rely on SAN-required client cert auth, configure one of these bindings before upgrading.
* `GROUP BY` and a window's `ORDER BY` or `PARTITION BY` clause no longer accept an expression of a type that has no comparison operators, namely `refcursor`, `jsonpath`, and the `trigger` pseudo-type. Such a query now returns the same "could not identify an ordering/equality operator" error that PostgreSQL returns, and that the equivalent statement-level `ORDER BY` already returned. A query that relied on this must cast the expression to a comparable type (for example `GROUP BY c::STRING`) or drop it from the clause.

### Security updates

* Security: Fixed an issue where a tenant-scoped client certificate could bypass tenant-scope checks and gain cluster-wide RPC access if its subject distinguished name (DN) matched the configured root or node DN. Fixed a related issue where certificate authentication could bypass the disallow-root-login policy when the certificate DN or subject alternative name (SAN) matched a root identity.
* Fixed a security issue by rejecting invalid transaction commit requests from secondary tenants that could have allowed an authenticated tenant to exceed its granted capabilities.
* When SSO/OIDC authentication fails because the user does not exist in CockroachDB or lacks DB Console login privileges, CockroachDB now emits a structured `ClientAuthenticationFailed` event to the `SESSIONS` log channel with reason `USER_NOT_FOUND` or `LOGIN_DISABLED`. This enables production alerting on SSO provisioning gaps without exposing user-existence details to unauthenticated clients.
* Fixed a security issue where a non-admin user could use a `LEAKPROOF` user-defined function to bypass row-level security (RLS). Setting `LEAKPROOF` via `CREATE FUNCTION`, `CREATE OR REPLACE FUNCTION`, or `ALTER FUNCTION` now requires membership in the `admin` role; `NOT LEAKPROOF` is unchanged. Existing `LEAKPROOF` functions are not modified. On upgrade, CockroachDB logs warnings for any existing `LEAKPROOF` UDFs owned by non-admin users so operators can review and, if needed, remove the `LEAKPROOF` designation.
* SASL OAUTHBEARER authentication now returns an RFC 7628–compliant SASL error response when token validation fails. In this case the broker responds with an AuthenticationSASLContinue message containing a JSON error body, allowing compliant clients to detect an `invalid_token` condition and (when provided) the required `scope` to request. Invalid SASL framing (e.g., unsupported channel binding data or a non-empty `authzid`) is unchanged and continues to fail the connection without an OAuth error response.
* The OIDC authorization-code authentication flow now supports PKCE (Proof Key for Code Exchange). PKCE can be required by some identity providers and helps protect against authorization-code interception. It is disabled by default and can be enabled with the `server.oidc_authentication.pkce.enabled` cluster setting.
* Fixed a DB Console security issue where the login endpoints returned a distinct error message when an account’s password had expired, which could allow username enumeration. The login endpoints now return a uniform error for all authentication failures.
* The DB Console login endpoints now take a comparable amount of time for all failed logins, whether the user does not exist, has no password, or has an expired password, preventing an unauthenticated attacker from enumerating valid usernames by measuring response latency. SQL clients now receive the same generic authentication error for an expired password as for a wrong one.
* String-valued group claims for JWT, OIDC, and OAUTHBEARER authorization are now split on commas only when synchronizing SQL role memberships. Identity providers that convey multiple groups should return a JSON array.
* CockroachDB no longer grants reserved or privileged SQL roles (such as `admin`) through JWT, OIDC, OAUTHBEARER, or LDAP group-based authorization, even when an identity-provider group is named after such a role.
* The OIDC JWT authentication endpoint now sets `Cache-Control: no-store` and `Pragma: no-cache` on its responses so intermediaries do not cache the returned SQL credential bundle.
* Improved synchronization of SQL role memberships from external identity providers (OIDC, JWT, LDAP, and OAUTHBEARER). CockroachDB now reconciles only a user’s direct role grants, preventing unintended removal of roles inherited through parent roles. Security update: OIDC logins are now denied without modifying role memberships if the groups claim cannot be found in the ID token, access token, or userinfo response (for example, due to a transient identity provider failure). If the identity provider explicitly returns an empty groups list, CockroachDB will continue to revoke roles and deny the login.
* When a TLS cipher-suite allowlist is configured via the `tls-cipher-suites` flag, a connection to the HTTP interface that negotiates a disallowed cipher is now rejected during the TLS handshake (surfaced to the client as a TLS handshake failure) instead of being closed immediately afterward. This also prevents a slow or stalled TLS handshake on the HTTP port from delaying other incoming HTTP connections.
* `CREATE LOGICAL REPLICATION STREAM` now requires `USAGE` on the external connection named as the source. Non-admin users must be granted `USAGE` on that connection; admins are unaffected.
* Fixed an information disclosure issue in the `/api/v2/grants/databases/` and `/api/v2/grants/tables/` HTTP API endpoints where an authenticated SQL user could enumerate database, schema, and table names for objects they did not have privileges on. These endpoints now return 404 for objects the user cannot access.
* Fixed a bug where the loss-of-quorum recovery plan file (`staged.bin`) was written unencrypted to disk even on stores with encryption-at-rest enabled, potentially exposing range start keys containing fragments of user data. The plan file is now encrypted like the rest of the store's data when EAR is enabled.
* Creating or altering a changefeed that uses an external connection (as a sink or for a Confluent schema registry) now requires the `USAGE` privilege on that external connection. This also applies to users with the `CONTROLCHANGEFEED` role option.
* `EXPORT` now requires the `USAGE` privilege on an external connection named in its destination URI.
* `ALTER BACKUP` now checks privileges on its collection URI, requiring the `USAGE` privilege when that URI names an external connection.
* Starting or restarting Physical Cluster Replication (PCR) from an external connection now requires the `USAGE` privilege on that external connection.
* `CREATE LOGICALLY REPLICATED TABLES ... WITH BIDIRECTIONAL ON` now reports a missing `USAGE` privilege on the reverse stream’s external connection when the statement is run, instead of allowing the job to start and fail later.
* The internal built-in `crdb_internal.backup_compaction` now requires the `BACKUP` system privilege, plus the same destination privileges as `BACKUP` (for example, `EXTERNALIOIMPLICITACCESS` for implicitly authenticated destinations and `USAGE` on external connections). These checks apply only to direct invocation of the built-in; compactions run via scheduled backups are unchanged.
* `crdb_internal.backup_compaction` now requires the `USAGE` privilege on an external connection named in a KMS URI, in addition to the privileges its destination requires. Compactions triggered automatically by a backup schedule are unaffected.
* `BACKUP`, `ALTER BACKUP`, `SHOW BACKUP` and `RESTORE` now require the `USAGE` privilege on an external connection named in a KMS URI. Existing backup schedules whose owner lacks `USAGE` on a KMS external connection will fail until the grant is made. A schedule created with `on_execution_failure = 'pause'` must also be resumed with `RESUME SCHEDULE` once the grant is in place.
* The `cluster.secret`, `enterprise.license`, and `cloudstorage.http.custom_ca` cluster settings are now marked sensitive. Their values are redacted for users without the `MODIFYCLUSTERSETTING` privilege when `server.redact_sensitive_settings.enabled` is set, and are suppressed from diagnostics artifacts. The `sql.override.allow_unsafe_internals.enabled` setting is no longer marked sensitive.
* The events.json file in debug zips no longer contains statement placeholder values, which can carry sensitive data such as passwords or sensitive cluster setting values bound via prepared statements. The value recorded for `ALTER TENANT ... SET CLUSTER SETTING` events is now also hidden there, matching regular setting-change events.
* The settings.json file in debug zips and the DB Console settings page no longer show the values of sensitive cluster settings (such as authentication secrets), even for users with the `MODIFYCLUSTERSETTING` privilege and even in debug zips collected without the `--redact` flag. Sensitive values are replaced with '`<redacted>`'. Privileged users can still read these values with `SHOW CLUSTER SETTING`.
* The `set_cluster_setting` and `set_tenant_cluster_setting` events written to `system.eventlog` no longer record the values of sensitive cluster settings (such as authentication secrets). The event's value field contains `<redacted>` instead; resets are still recorded as `DEFAULT`.
* Unredacted debug zips no longer contain the values of sensitive cluster settings (such as authentication secrets). This covers the `system.settings`, `system.tenant_settings`, `crdb_internal.cluster_settings` and `system.eventlog` dumps. Values for settings not known to the current version are conservatively redacted as well. The session and query dumps now hide SQL constants in statement text, so that a setting value typed in a session is no longer carried in an unredacted zip. Debug zips collected without `--redact` no longer contain the connection details of external connections or the execution arguments of scheduled jobs, both of which can hold cloud storage credentials.
* `CREATE ROLE` and `ALTER ROLE` statements that supply the password via a bound placeholder (for example, a prepared statement executed with arguments, the typical driver behavior) no longer record the raw password in the system event log or SQL logs; the recorded placeholder value is `*****`, matching the existing treatment of password literals. Debug zips collected without `--redact` also scrub bound placeholder values of historical role-change events from the event log dump and from log files.
* The error reported when the `cloudstorage.http.custom_ca cluster` setting holds an unparsable certificate no longer includes the setting's value.
* Debug zips collected without `--redact` and the output of `cockroach debug merge-logs` no longer include log entries that reveal the values of sensitive cluster settings (such as authentication secrets). Affected entries are redacted in place when possible (requires redactable logs, the default) and replaced with a tombstone otherwise. Entries that mention a sensitive setting without containing its value are scrubbed as well. When any such scrubbing occurs, the debug zip contains a sensitive\_settings\_warning.txt file describing what was removed.
* Fixed redaction of connection URIs that can embed credentials in the statement text of `CREATE CHANGEFEED ... AS SELECT`, `CREATE SCHEDULE FOR CHANGEFEED`, `CREATE LOGICAL REPLICATION STREAM`, and `CREATE LOGICALLY REPLICATED`. Such URIs now render as `'*****'` in contexts like `SHOW CREATE SCHEDULE`.
* Statements that reference external URIs, connection strings, or encryption passphrases that can embed credentials - such as `BACKUP`, `RESTORE`, `IMPORT`, `EXPORT`, changefeed, `EXTERNAL CONNECTION`, and replication statements - are now treated as possibly carrying a secret, so their constants, bound placeholder values, and recorded errors are kept out of diagnostics artifacts (e.g. `SHOW QUERIES`, recorded SQL event details, and statement statistics). The full error is still returned to the client.
* Statement diagnostic bundles now redact cleartext passwords and credential-bearing URIs in formatted statements by default (for example, in `BACKUP`, `RESTORE`, `IMPORT`, EXPORT, and `CREATE ROLE ... WITH PASSWORD`).
* Security improvement for replication statements: the source connection URI in `CREATE VIRTUAL CLUSTER ... FROM REPLICATION` and `ALTER VIRTUAL CLUSTER ... START REPLICATION` is now redacted by default in reformatted statement output, while job descriptions continue to display a sanitized URI.
* Security improvement for `IMPORT ... AVRO`: the `schema_uri` option is now redacted in formatted SQL output, and any secret query parameters in `schema_uri` are sanitized in `IMPORT` job descriptions (e.g., `SHOW JOBS`) to prevent leaking embedded cloud credentials.

### General changes

* `CREATE CHANGEFEED` now supports the `create_kafka_topics` option to control Kafka topic creation: `broker_auto` (default) relies on the Kafka cluster to auto-create topics, `explicit` creates topics using the Kafka Admin API, and `off` disables topic creation.
* The `admission.cpu_time_tokens.per_tenant.*` metrics have been removed. Use `admission.cpu_time_tokens.{admitted_count,wait_time_nanos, tokens_used,tokens_returned}`, labeled by (`tenant_id`, `group_id`), instead.
* The `no-linger` sink for changefeeds (the default for the kafka, webhook, and pubsub v2 sinks) no longer requires `Frequency` when `Messages` or `Bytes` is set. The no-linger sink interprets the `Flush.Frequency` sink config option as a minimum linger; a batch is held until its oldest event is at least `Frequency` old, coalescing events into fewer, larger requests at the cost of latency, for example `kafka_sink_config='{"Flush": {"Frequency": "1s"}}'`. Leave it unset for the lowest latency.
* Changefeeds using `format=avro` with a schema registry now support an `avro_mode` option. The default (`avro_mode=write`) preserves existing behavior by registering schemas with the registry. Setting `avro_mode=read` makes schema-registry interaction read-only: the changefeed performs no registry writes and instead looks up an already-registered schema ID, failing if no matching schema is found.
* Sinkless changefeeds now accept the `mvcc_ordered` option to emit rows in non-decreasing updated-timestamp order.
* Removed the `changefeed.new_kafka_sink.enabled` cluster setting. The Kafka changefeed sink now always uses the franz-go implementation.

### Enterprise edition changes

* Added support for automatic user provisioning for clients authenticating with SASL OAUTHBEARER When the cluster setting `security.provisioning.oauth.enabled` is enabled, a successful OAUTHBEARER authentication for a non-existent SQL user creates that user. The created role is marked with the `PROVISIONSRC` role option set to `oauth:<issuer>` to identify the token issuer as the provisioning source.
* Avro changefeeds can now resolve schema IDs from a Confluent-compatible schema registry without writing to it. Set `avro_mode='read'` to use read-only (GET) requests to resolve existing subjects. The new `avro_schemas` option lets you override subject names and select the registered version to use in read mode: `latest` (default), `latest_compatible`, or `pin(<n>)`. Subject-name overrides also apply in the default write mode.
* `CREATE LOGICAL REPLICATION STREAM` and `CREATE LOGICALLY REPLICATED TABLE` now accept a `DLQ = <schema>` (or `<database>.<schema>`) option with `MODE = 'transactional'`. When set, source transactions that fail to apply with a DLQ-eligible error, such as a unique constraint violation, are recorded in per-job dead letter queue tables under the specified schema and replication continues past them, instead of pausing the job.
* Avro changefeeds can now set `avro_resolved_subject` to store resolved-timestamp schemas in a dedicated Schema Registry subject. Read-only Avro mode now supports resolved timestamps when this option is set.

### SQL language changes

* Updated `EXPLAIN ANALYZE` to show the body plan for user-defined functions (UDFs) and stored procedures, including per-node execution statistics, invocation counts, and plan variants based on routine arguments. Updated `EXPLAIN (OPT, ...)` and statement bundles to show routine bodies under the routine node. Output is truncated with a note if more than 100 plan variants are encountered in the same statement.
* Added support for `ALTER DOMAIN ... DROP CONSTRAINT`.
* Fixed `EXPLAIN ANALYZE` to include KV read metrics from statements executed inside routine bodies (for example, `PL/pgSQL`), ensuring top-level statistics (such as `rows decoded from KV`) reflect the full work performed and are reported consistently.
* Added a `query_tags` column to `crdb_internal.node_active_session_history` and `crdb_internal.cluster_active_session_history` to expose sqlcommenter query tags associated with each sampled statement, as a JSONB array of `{name, value}` objects. Populating this column requires `sql.sqlcommenter.enabled` and `obs.ash.enrichment.query_tags.enabled` (disabled by default).
* Added a new `STATEMENTHINTS` privilege to control access to the statement-hints built-ins (e.g., `crdb_rewrite_inline_hints`, `crdb_delete_statement_hints`, `crdb_enable_statement_hints`, `crdb_set_session_variable_hint`, `crdb_pin_plan_gist`, `crdb_pin_plan_gram`) without requiring `REPAIRCLUSTER`. This privilege can be granted at the system level or per database; `REPAIRCLUSTER` continues to grant access for backward compatibility.
* The `crdb_delete_statement_hints` and `crdb_enable_statement_hints` built-ins now accept an optional database argument along with a hint row ID, allowing users with database-scoped `STATEMENTHINTS` to manage individual statement hints.
* Added `EXPLAIN ANALYZE` execution details for statements in the body of `PL/pgSQL` subroutines.
* The `optimizer_use_routine_decorrelation_fix` session setting is now on by default in 26.3+.
* Added the `optimizer_use_routine_decorrelation_fix` session setting (enabled by default) to improve query planning for user-defined functions (UDFs) and stored procedures that reference routine parameters or PL/pgSQL variables. With this setting enabled, the optimizer defers certain rewrites until the routine is inlined or executed, which can prevent parameter-dependent filters from being moved in ways that lead to less selective scans (e.g., full table scans instead of constrained access paths).
* `EXPLAIN EXECUTE` is now supported. It shows the query plan for a prepared statement with the specified parameters without executing it, matching `EXPLAIN` behavior for non-prepared statements.
* PL/pgSQL routines can now create or alter an object and reference it in a subsequent statement, even when the statements are nested within the same IF, loop, or block.
* Index recommendations can now suggest replacing an existing unique index with a new index that extends it, allowing you to collapse redundant indexes into a single index when doing so preserves uniqueness.
* Row-level TTL now finds expired rows through a secondary index on the expiration column automatically, instead of always scanning the primary index. Because expired rows are contiguous in such an index, this can reduce the cost of TTL jobs on large tables where only a small fraction of rows is expired at any time. The job uses such an index only when the estimated fraction of expired rows is below the new `sql.ttl.index_scan.auto_max_expired_fraction` cluster setting (default `0.05`); set it to `0` to disable the index-scan path or `1` to always use a usable index.
* Foreign key referential actions (for example, `ON DELETE CASCADE` and `ON UPDATE CASCADE`) now run with the privileges of the referencing table owner (including any triggered triggers and check constraints), rather than the user performing the mutation. This matches PostgreSQL behavior and can reduce the privileges required for users who modify referenced tables. The row-level TTL job now also runs as the table owner instead of the node user.
* Row-level TTL jobs now run their `SELECT` and `DELETE` operations as the TTL table’s owner instead of an internal superuser. As a result, any user-defined code invoked by TTL deletes (for example, `DELETE` triggers) runs with the table owner’s privileges and sees `current_user` as the table owner. Row-level TTL still deletes expired rows even when `FORCE ROW LEVEL SECURITY` would hide them from the owner, or when the owner lacks `EXECUTE` privileges on referenced functions.
* Schema owners can now `DROP FUNCTION` and `DROP PROCEDURE` objects in their schema, matching PostgreSQL behavior.
* Added the `admission.kv.cpu_time_token_filler.dampening.enabled` cluster setting, which lets operators disable runnable-goroutine-based dampening in the KV CPU time token filler.
* `CREATE / ALTER / DROP / SHOW RESOURCE GROUP` return an unimplemented error at plan time by default. Setting `sql.experimental_resource_groups.enabled` = true bypasses this and re-enables the previous behavior for internal testing.
* CockroachDB now supports statement-level (`FOR EACH STATEMENT`) triggers for `INSERT`, `UPDATE`, `DELETE`, and `UPSERT` on tables. A statement-level trigger fires its trigger function once per statement, before or after the mutation, even if the statement affects no rows.
* Added support for the `ST_BuildArea` and `ST_ConcaveHull` geospatial functions.
* Added the `disallow_ddl_in_explicit_transactions` session setting. When enabled, running DDL in an explicit transaction returns an error instead of auto-committing prior statements, helping prevent DML from being silently committed and therefore not rollable back. This setting is disabled by default and can be enabled for all users with `ALTER ROLE ALL SET`.
* `ALTER TABLE ... ALTER COLUMN ... SET DATA TYPE` operations that require rewriting on-disk data are now supported when the column is part of a secondary index; CockroachDB rebuilds the affected indexes to use the new column type. Altering the type of a column that is part of an inverted index is still not supported.
* The `REFERENCING` clause for `CREATE TRIGGER` is now supported. It allows `AFTER` triggers to reference the complete set of rows inserted, updated, or deleted by the triggering statement as relations (known as transition tables) inside the trigger function body, using the aliases given by `REFERENCING OLD TABLE AS` and `REFERENCING NEW TABLE AS`.
* `GRANT` and `REVOKE` statements that accidentally omit the `SYSTEM` keyword (e.g. `GRANT VIEWACTIVITY TO user`) now include a hint in the error message directing the user to the correct `GRANT SYSTEM <privilege> TO ...` syntax.
* Added the session setting `optimizer_use_histograms_for_multi_span_const_columns`, which controls whether the optimizer uses histogram statistics for columns constrained to a single constant value in every span of a multi-span index constraint, even when an earlier index column is not constrained to a single value. The setting defaults to false.
* For statements inside UDF and stored procedure bodies, `EXPLAIN ANALYZE` now reports per-operator execution statistics aggregated across all routine invocations, shown as an average with minimum and maximum values.
* The `cpu_sql_nanos`, `cpu_sql_nanos_sum`, and `cpu_sql_nanos_sum_sq` columns of the `information_schema.crdb_statement_statistics` and `information_schema.crdb_transaction_statistics` views are deprecated. They will be replaced by a column reporting the always-on SQL CPU measurement (recorded on every execution) in a future release, and remain populated until then.
* `EXPLAIN ANALYZE` now displays plans and invocation counts for inlined routine-body statements and user-defined function calls, which previously could execute without appearing in the output.
* The `transaction_rows_read_err` guardrail now accounts for rows read by earlier statements in the same explicit transaction when limiting how many rows a later statement may read. This causes a transaction that will exceed the limit to fail sooner, avoiding wasted work, rather than allowing each statement to read up to the full limit before erroring.
* Adding a VIRTUAL computed column no longer scans the table to validate the expression against existing rows by default. If the expression is invalid (for example, it can produce a value too large for the declared type), the error is now returned when the column is read. To enable validation during `ADD COLUMN`, set `sql.schema.validate_virtual_computed_columns.enabled` (disabled by default).
* The `ALTER TABLE ... AUDIT SET` syntax is now GA. The previous `EXPERIMENTAL_AUDIT` keyword is deprecated and will be removed in a future release.
* Added two new `information_schema` views, `information_schema.crdb_statement_execution_insights` and `information_schema.crdb_transaction_execution_insights`, which expose persisted user-workload statement and transaction execution insights with a stable schema.
* `EXPLAIN ANALYZE (VERBOSE)` now reports estimated max memory allocated and estimated max SQL temp disk usage used to store the result of a subquery operator.
* `EXPLAIN ANALYZE (VERBOSE)` now reports estimated max memory allocated and estimated max SQL temp disk usage for the buffer, apply join, and recursive CTE operators.
* `CREATE TABLE` with storage parameters other than the row-level TTL family is now handled by the declarative schema changer.
* `CREATE TABLE` with row-level TTL storage parameters is now handled by the declarative schema changer.
* When `sql.procedures.plpgsql.late_binding.enabled` is enabled, a PL/pgSQL exception handler can now reference objects created or altered by DDL earlier in the same handler. In addition, exception handlers that never execute are no longer name-resolved at `CALL` time.
* The owner of a schema can now execute `DROP TYPE` and `DROP DOMAIN` on types and domains in that schema without owning the type itself, matching PostgreSQL. `ALTER TYPE`, `ALTER DOMAIN`, and `COMMENT ON TYPE` still require ownership of the type.
* CockroachDB now supports `ALTER SCHEMA ... RENAME TO` in the declarative schema changer. Renaming a schema requires ownership of the schema, and is rejected if any object in the schema is referenced by name by a view, routine, or trigger.
* The `information_schema.domains`, `domain_constraints`, `domain_udt_usage`, and `column_domain_usage` tables are now populated. `SHOW TYPES` now lists domain types in addition to enums and composite types, and includes a new `category` column identifying the kind of each type. `information_schema.columns` now reports domain-typed columns using their base type for `data_type` and `udt_name` while identifying the domain in the `domain_catalog`, `domain_schema`, and `domain_name` columns.
* The `information_schema.crdb_statement_statistics` and `information_schema.crdb_transaction_statistics` views now include `sql_cpu_time_nanos_sum` and `sql_cpu_time_nanos_sum_sq`, which report the sum and sum of squares of the always-on SQL CPU time recorded for each execution.
* Added the `sql.stats.automatic_node_worker_count` cluster setting to control how many concurrent automatic table statistics collection jobs a single node can initiate. Cluster-wide concurrency limits for automatic statistics collection are still enforced.
* `ALTER TYPE ... DROP VALUE` now supports the `IF EXISTS` clause, allowing an enum value to be dropped only if it exists and preventing an error when the value is not present.
* Added support for `ALTER DOMAIN ... VALIDATE CONSTRAINT`, which validates existing rows against the constraint, fails if any rows violate it, and marks the constraint as validated on success.
* Added the `uuidv4()` built-in function as an alias for `gen_random_uuid()`.
* The `information_schema.crdb_index_usage_statistics` view now includes `total_writes` and last\_write.
* CockroachDB now supports the SQL/JSON `json_array()` and `json_object()` constructor functions from the SQL standard (as of PostgreSQL 16), including the `{ NULL | ABSENT } ON NULL`, `{ WITH | WITHOUT } UNIQUE [ KEYS ]`, and `RETURNING` clauses. For example, `json_object('a' VALUE 1, 'b' VALUE 2)` returns `{"a": 1, "b": 2}`.
* `CREATE DOMAIN` and `ALTER DOMAIN` now reject domain constraint names that match the reserved internal pattern `crdb_internal_constraint_<id>_name_placeholder`.
* The `information_schema.crdb_index_usage_statistics` view now includes `database_name`, `schema_name`, `table_name`, and `index_name` columns (in addition to `table_id` and `index_id`), allowing index usage statistics to be queried by object name without joining `crdb_internal` views.
* `IMPORT INTO` now supports `WITH execution_locality = '...'` to constrain import execution to SQL instances that match a given locality filter (for example, `region=us-east1`). This can reduce cross-region traffic and help avoid cloud authentication failures from out-of-region nodes. An empty value leaves the import unconstrained.
* Added the `information_schema.crdb_indexes` view to simplify querying per-index metadata. The view lists each index’s ID, name, owning table and database, key span, and index type (primary, secondary, unique, or inverted), providing stable schema-introspection columns that previously required joining multiple internal tables.
* Added the `information_schema.crdb_contention_activity` view for inspecting persisted lock and latch contention with waiting and blocking statement attribution.
* `DROP TYPE ... CASCADE` is now supported, dropping the type and dependent objects including table columns of that type (while preserving the table), dependent expressions and constraints, and dependent functions, views, and domains.
* PL/pgSQL dynamic `EXECUTE` now supports a `USING` clause to bind values to `$n` placeholders in the command string.
* Added the `information_schema.crdb_databases` view to expose per-database metadata via stable columns (database ID, owner, survival goal, and effective zone configuration SQL), simplifying schema introspection that previously required joining multiple `crdb_internal` tables.
* Added the `information_schema.crdb_tables` view to provide per-table metadata in a single query, including key spans, estimated row count, zone configuration SQL, and effective (inheritance-resolved) replica settings such as replica count, constraints, and lease preferences. The view returns one row per public table.
* Added the `any_value()` aggregate function for PostgreSQL compatibility. It returns an arbitrary non-`NULL` input value (or `NULL` if all inputs are `NULL`) and can also be used as a window function.

### Operational changes

* Added twelve `server.oauthbearer_authentication.*` cluster settings for configuring SASL `OAUTHBEARER` authentication.
* Added sixteen new SQL metrics for tracking EXPLAIN and EXPLAIN ANALYZE usage: `sql.explain.started.count` / `sql.explain.count`, `sql.explain_analyze.started.count` / `sql.explain_analyze.count`, plus four sub-counters (`sql.explain.procedure.count`, `sql.explain_analyze.procedure.count`, `sql.explain.udf.count`, `sql.explain_analyze.udf.count` and their `.internal` counterparts for internal queries) that fire when the EXPLAIN target is a CALL of a stored procedure or a statement that invokes a user-defined function. These statements were previously bucketed into the `sql.misc.*.count` series and were not visible individually.
* Added per-store metrics that expose the CPU cost breakdown for the multi-metric allocator, including `immovable`, `amplification`, `replica`, `sql_dist`, `sql_gateway`, and `overhead_k`, to help investigate allocator-related CPU usage.
* Reduced the default value of the `kv.range_merge.queue_interval` cluster setting (which controls how long the merge queue waits between processing replicas) from 5s to 200ms. This helps the merge queue keep up with range creation and reduces range accumulation.
* Added the `mma.store.shedding_overload_level` metric, which reports the per-store MMA shedding severity used during rebalancing to determine whether a local store is a shedding candidate. The value is updated each rebalancing pass and ranges from 0 (low load) to 4 (urgent overload); values >= 3 indicate the store is considered overloaded for shedding purposes.
* Added the `restore.wait_for_span_config_conformance.enabled` config property, which is recommended to meet data domiciling requirements if you use the `STRICT` option with `BACKUP`.
* Execution insights are now periodically persisted to the `system.statement_execution_insights` and `system.transaction_execution_insights` tables, providing a durable, cluster-wide record that survives node restarts and outlives the in-memory window. Persistence is controlled by the new `sql.insights.flush.enabled` (default `true`), `sql.insights.flush.interval` (default `10m`), `sql.insights.flush.jitter` (default `0.15`), and `sql.insights.flush.batch_size` (default `100`) cluster settings.
* Added the `admission.kv.cpu_time_token_filler.dampening.enabled` cluster setting to disable runnable-goroutine-based dampening in the CPU time token filler.
* Updated changefeed metric descriptions to indicate which metrics are not emitted by the default Kafka, Pub/Sub, and webhook (no-linger) sinks. Also clarified that Kafka batch-reduction metrics and internal retry metrics track different behaviors and are no longer co-mingled.
* Added new metrics that count finished SQL transactions (committed or aborted) by the isolation level they ran at: `sql.txn.count.serializable`, `sql.txn.count.snapshot`, and `sql.txn.count.read_committed`. On the Prometheus `/metrics` endpoint, these are exposed as a single `sql.txn.count` metric labeled by `isolation_level`.
* SQL CPU time is now measured for every statement and transaction instead of only for the subset selected for execution-statistics sampling. The measurement is exposed as `sqlCPUTimeNanos` in the `crdb_internal.statement_statistics` and `crdb_internal.transaction_statistics` tables, and SQL CPU time is now reported for all statements and transactions in executioninsights, where previously unsampled executions reported zero.
* The `raft.quota_pool.percent_used` metric has been removed.
* Bucket boundaries for histogram metrics have changed to align with Prometheus native-histogram schemas. As a result, quantiles computed from these metrics may shift. Some histograms now provide finer resolution for very low-latency operations, while others are exported at a coarser resolution to reduce the number of exported time series.
* Added the `sql.query_canceled.count` metric to track statement failures due to query cancellation (for example, client context cancellation, DistSQL flow errors, or session cancellation). This helps operators distinguish cancellations from other non-retryable errors counted in `sql.failure.count`, alongside `sql.statement_timeout.count` and `sql.transaction_timeout.count`.
* Added the `sql.schema_changer.index_backfill_version_barrier.enabled` cluster setting (default `true`). When enabled, an index backfill publishes an extra descriptor version and waits for it to propagate before its backfill and merge phases choose their timestamps, hardening `ALTER PRIMARY KEY` run concurrently with updates against a rare spurious "duplicate key value violates unique constraint" failure. This adds a short delay (up to two lease intervals per rebuilt index) to affected schema changes; set it to `false` to restore the previous behavior.
* New metrics expose the behavior of the per-node table statistics cache, whose capacity is controlled by the `sql.stats.table_statistics_cache.capacity` cluster setting: `sql.table_statistics.cache.hits` and `.misses` count lookup outcomes; `sql.table_statistics.cache.evictions.capacity` and `.evictions.invalidated` count evictions split by cause; `sql.table_statistics.cache.eviction_idle_duration` records the time since last access of entries evicted for capacity (low values indicate the cache is too small for the set of tables being queried); and `sql.table_statistics.cache.entries` and `.mem_bytes` report the current cache size.
* The following Active Session History (ASH) cluster settings are now application-level and can therefore be configured independently across separate-process virtual clusters: `obs.ash.enabled`, `obs.ash.sample_interval`, `obs.ash.buffer_size`, `obs.ash.log_interval`, `obs.ash.log_top_n`, `obs.ash.enrichment.enabled`, `obs.ash.enrichment.cache.max_entries`, `obs.ash.enrichment.rpc.timeout`, `obs.ash.enrichment.queue.lifetime`, `obs.ash.enrichment.queue.max_entries`. In shared-process deployments, ASH sampling and enrichment remain process-wide and are controlled only by the system virtual cluster (changes in secondary virtual clusters are ignored).
* Inbound TLS handshakes are now bounded by explicit, tunable timeouts, so a peer that stalls mid-handshake can no longer hold a serving goroutine and file descriptor open indefinitely. The gRPC server bounds connection establishment (the TLS handshake plus HTTP/2 setup) at 30 seconds by default, replacing gRPC's implicit 120-second limit; this is configurable via the `COCKROACH_RPC_SERVER_CONNECTION_TIMEOUT` environment variable. The DRPC server and the SQL cipher-suite allowlist path (`tls-cipher-suites`) bound the TLS handshake at 10 seconds by default, configurable via the `COCKROACH_TLS_HANDSHAKE_TIMEOUT` environment variable.
* Breaking change: Removed the deprecated `IMPORT` and `RESTORE` event logs. Use the job status change event logs instead.
* The `obs.ash.enrichment.enabled` cluster setting now defaults to `true`. Active Session History samples are enriched with the application name, user, plan gist, canary stats, transaction ID, and session ID of the execution that produced them, so the corresponding columns of `crdb_internal.node_active_session_history` and `crdb_internal.cluster_active_session_history` are populated without further configuration. Enrichment additionally requires the cluster version to be finalized; until then these columns remain `NULL`. Set the setting to `false` to turn enrichment off.
* Added the cluster metric `schedules.BACKUP.cluster.time_since_last_completed_by_type`, which reports the time since the most recently completed full, incremental, or compaction backup (distinguished by the `backup_type` label) for backup schedules configured with the `updates_cluster_last_backup_time_metric` option. This lets each backup type's completion cadence be monitored independently. The relevant per-type series are removed when the corresponding backup schedule is dropped.
* Added four new `basalt.tenant.*` metrics to track tenant object count and byte usage (replica, logical, and archived).
* Added metrics that provide observability into the range descriptor cache: `distsender.rangecache.size`, `distsender.rangecache.hits`, `distsender.rangecache.misses`, `distsender.rangecache.evictions.capacity`, and `distsender.rangecache.evictions.stale`.
* Added the `obs.ash.labels.kv_batch_key.enabled` cluster setting (default off). When enabled together with `obs.ash.labels.enabled`, active session history samples taken while a node processes a KV batch are labeled with the batch's start key (row values redacted when redaction is enabled) and the range ID, so samples can be attributed to the object being accessed. The `crdb_internal.key_to_table_id` and `crdb_internal.key_to_table_index_id` builtins now also accept the pretty-printed key string form stored in these labels.
* Added the `obs.ash.labels.kv_sync_wait.enabled` cluster setting to emit additional Active Session History (ASH) labels for samples captured during lock and latch waits (range, table, index, and blocking statement fingerprint). These labels are emitted only when both `obs.ash.labels.enabled` and `obs.ash.labels.kv_sync_wait.enabled` are set to `true`. Both settings default to `false`.
* Renamed metric `schedules.BACKUP.cluster.time_since_last_completed` to `schedules.BACKUP.cluster.recovery_point_lag`. The metric’s meaning is unchanged: it reports the elapsed time since the latest restorable recovery point for the backup schedule (not the backup job’s wall-clock completion time), and can be used to monitor RPO. This metric is emitted only when the `updates_cluster_last_backup_time_metric` schedule option is enabled.
* Removed the `schedules.backup.gc_protection.enabled` cluster setting. Protected timestamp chaining for scheduled backups is now always enabled and can no longer be disabled.
* Added cluster settings to control KV-side Active Session History (ASH) sample buffering for separate-process tenants: `obs.ash.kv_sample_egress.enabled` enables KV nodes to buffer KV ASH samples for tenants to pull, and `obs.ash.kv_sample_egress.per_tenant_capacity` / `obs.ash.kv_sample_egress.total_capacity` control per-tenant and total retained sample capacity.
* The `/debug/lsm` endpoint, and the LSM stats included in a debug zip, now describe a store's range-shared engines in addition to its store-local engine: the two combined, each on its own, and the individual range-shared engines with the highest read amplification, size, and compaction debt. Stores without range-shared engines are unchanged.
* The storage metrics a store reports (`rocksdb.*` and `storage.*`) now cover its range-shared engines in addition to its store-local engine, and each is also reported broken down by engine kind under `<name>.local` and `<name>.shared`, exported as the `<name>.by-engine` family with an `engine` label. On clusters using range-shared engines, `rocksdb.read-amplification` and `rocksdb.level-score` are now the mean across a store's engines rather than the store-local engine's value; `rocksdb.read-amplification.local` and `rocksdb.level-score.local` report what those metrics reported before.
* The `changefeed.mvcc_ordered_buffer.bytes` metric tracks the total memory and disk usage in `ordered_buffer` when `mvcc_ordered` is enabled on sinkless feeds.
* Added `basalt.fs.quorum_write.*` and `basalt.fs.replica_write.*` metrics for clusters using disaggregated storage to report write quorum outcomes, replica-level write failures, and write latency.

### Command-line changes

* The `cockroach sql` CLI now supports interactive OAuth login using the OAuth 2.0 device authorization grant. Configure `--oauth-issuer` and `--oauth-client-id` (optionally `--oauth-scope`) to open a browser for authentication and connect using the resulting token. For security, the SQL connection must use `sslmode=verify-full` and the issuer must use `https`; for local testing with a non-HTTPS issuer, set `COCKROACH_OAUTH_ALLOW_INSECURE_ISSUER` to relax only the issuer requirement.
* The `cockroach debug encryption-status`, `cockroach debug encryption-registry-list`, and `cockroach debug encryption-decrypt` commands can now inspect the store-local engine of a basalt store. Provide the store URI as the directory argument and pass the same basalt flags used to start the node (including `--cluster-key-dir` for the cluster key). The store must be offline.
* The `cockroach debug pebble` subcommands now take the target database directory and files using the `--dir` and `--file` flags instead of positional arguments.

### DB Console changes

* Added a Show internal checkbox to the DB Console SQL Activity pages (Statements, Transactions, and Sessions) to include internal sessions and statement/transaction statistics without changing the `sql.stats.response.show_internal.enabled` cluster setting.
* The debug zip workflow in the DB Console has been renamed as **Generate Debug zip** and moved under **Advanced Debug > Reports**. The page now acts as a command builder for `cockroach debug zip`, with preset configurations, category and scope selection (including time range and node selection), connection settings for local or remote clusters, and Copy/Download actions for the generated command.
* The SQL CPU Time shown for statements and transactions in the SQL Activity and Statement/Transaction Details pages is now recorded for every execution rather than only for sampled executions. Previously most statements and transactions displayed "no samples" for SQL CPU; they now show an accurate value.
* The DB Console Index Details page and the Indexes tab on the Table Details page now show Total Writes and Last Write for each index.

### Bug fixes

* Fixed an issue where the merge queue could repeatedly re-process ranges that could not be merged (for example, when the right-hand neighbor had an unexpired sticky bit), potentially starving other mergeable ranges. The merge queue now backs off before retrying an unmergeable range; this cooldown is configurable via the `kv.range_merge.cooldown` cluster setting.
* Fixed an issue where a partitioned index scan on a JSONB column could generate an empty key span and fail with `end key must be greater than start`.
* Fixed a bug that could cause an initial-scan changefeed with `diff` enabled, or a CDC query using `cdc_prev`, to fail with a GC threshold error if garbage collection ran during the scan.
* Fixed a bug where `CREATE TEMPORARY TABLE` could fail with an internal error in the same session after the session's first temporary table creation was rolled back.
* Fixed a bug where Avro-enriched changefeed messages could emit stale values for `source.ts_ns`, `source.ts_hlc`, and `source.mvcc_timestamp`, repeating the timestamp from an earlier event instead of the current event.
* Fixed a bug where importing Avro data with large records could cause unbounded memory growth, slowing or stalling the import. Such records now fail with a clear error suggesting that the `max_row_size` option be increased.
* Fixed a bug where `DISTINCT ON` queries with `ORDER BY` could return incorrect results during distributed execution when the operation spilled to disk (for example, due to memory pressure).
* Fixed an issue where `cockroach debug job-trace` could fail with “column "trace\_id" does not exist”. If no trace is available for the requested job, the command now reports that clearly.
* Fixed a bug where `CREATE SCHEMA` with an empty quoted schema name (for example, `""`) could trigger an internal error. It now returns a user-facing syntax error.
* Fixed a bug where `MATCH FULL` foreign key validation during schema changes could fail with a syntax error if the table’s primary key included column names that require quoting (for example, names containing hyphens).
* Fixed a bug where range-based lookup joins could rarely return incorrect results when the lookup column had redundant inequality filters (for example, `k >= 1 AND k >= 0`).
* Fixed an issue where calling a procedure, or invoking a user-defined function (UDF) in a query, in the same implicit-transaction statement batch that created or replaced it could fail with a “function does not exist” error if the transaction was automatically retried.
* Fixed a bug where binding a prepared `EXPLAIN` or `EXPLAIN ANALYZE` statement could fail with the error "EXPLAIN ANALYZE can only be used as a top-level statement".
* Fixed an issue where the MVCC GC queue per-range score was fully redacted in logs, which could obscure GC diagnostics in redacted debug output.
* Fixed a bug where execution statistics (for example, contention time, CPU time, and MVCC iterator statistics) from earlier attempts of an automatically retried transaction were discarded. `crdb_internal.transaction_statistics`, the DB Console **SQL Activity** page, and the `sql.txn.contended.count` metric now reflect execution across all retry attempts, consistent with the transaction’s service latency and retry count. Data-volume statistics (rows read/written, bytes read, and rows affected) continue to reflect only the final attempt.
* Fixed an issue where a transient error during the non-revertible stage of a declarative schema change could cause the schema change job to enter an infinite rollback/replan loop.
* Fixed an issue that could cause a lost write when a transaction used buffered writes, set `lock_timeout`, and attempted to continue after a lock-timeout error by issuing `ROLLBACK TO SAVEPOINT`.
* Improved the multi-metric allocator (MMA) (preview) to exclude down, dead, or decommissioned stores when computing cluster-wide load averages for overload detection. This prevents stale load from unavailable stores from skewing rebalancing decisions.
* Fixed an issue where `workload_index_recs` could return duplicate index recommendations and omit others when multiple recommendations for the same table shared a common column prefix.
* Fixed an issue where a schema change could hang indefinitely when `system.lease` contained leases held by many expired SQL sessions, which could also block other schema changes across the cluster.
* Fixed an issue where `SHOW CREATE FUNCTION` (and other schema output) could emit non-parseable SQL for a routine with a parameter named `index`, especially when the parameter type normalizes to a parenthesized form (for example, `BIT VARYING(61) ARRAY`).
* Fixed an issue in Logical Data Replication (LDR) where last-write-wins (LWW) conflict resolution could incorrectly drop a newer incoming row. This could happen when replicating tables with rows larger than \~3 KiB, where paginated reads might omit the MVCC origin timestamp required for conflict resolution.
* Fixed an issue where `node decommission` pre-checks could fail spuriously if candidate target stores were temporarily snapshot-throttled after a snapshot reservation was rejected.
* Fixed an issue where a transaction using buffered writes could return incorrect (including misordered) results for some queries that combine range scans and point lookups, particularly when a scan limit was reached and some point lookups were served from buffered data. Also fixed an issue where request statistics (keys/bytes read) could be underreported for point lookups served from buffered data.
* Fix a bug that would allow a `CHANGEFEED` to emit an incorrect `prev_value` when the `disable_changefeed_replication` option was used during a DELETE.
* Fixed a memory accounting leak in the rangefeed buffered sender. When a client's gRPC stream failed, budget allocations for events that had been dequeued but not yet sent were never returned to the per-range rangefeed memory budget. Repeated stream failures could cause rangefeed consumers of the affected range to be disconnected with spurious "budget exceeded" errors.
* Fixed an issue where `pg_advisory_xact_lock` or `pg_advisory_xact_lock_shared` could be silently lost while a transaction was still open (for example, during a range split or lease transfer) when write buffering was enabled. This could allow another session to acquire the same advisory lock concurrently.
* Fixed a bug that caused some queries to fail with the error "could not decorrelate subquery with mutation". This could happen when a correlated subquery's input performed a mutation, such as a PL/pgSQL routine (including a trigger function) with a loop containing a `SELECT ... INTO` statement that also performed an `INSERT`, `UPDATE`, or `DELETE`.
* Fixed a bug where a user could intermittently be denied a role membership immediately after it was granted.
* Fixed a bug where disabling or re-enabling an external statement hint via `information_schema.crdb_enable_statement_hints` did not invalidate cached query plans that depended on the hint.
* Fixed a bug where query text shown in `SHOW CLUSTER QUERIES`, `SHOW CLUSTER SESSIONS`, and the corresponding `crdb_internal` virtual tables could be truncated more aggressively than the configured limit when the text contained U+FFFD replacement characters near the truncation point.
* Fixed a bug where a Logical Data Replication (LDR) stream created by a non-admin user could fail to write to its dead letter queue (DLQ) due to insufficient privileges.
* Fixed a bug where large Raft commands (for example, AddSSTable operations used by index backfills, `IMPORT` and `RESTORE`) could be starved indefinitely on ranges with concurrent foreground writes. This could trip the per-replica circuit breaker and surface "replica unavailable ... slow proposal" errors even though the range was otherwise healthy.
* Fixed a bug where Active Session History (ASH) samples for SQL statements waiting on admission control (work events `sql-kv-response` and `kv-elastic-cpu-queue`) could be missing the application name and were not enriched with attributes such as user, transaction, session, or plan gist.
* Fixed a bug where a failure to delete an expired descriptor lease left a stale row in `system.lease` for as long as the owning node stayed up. The stale row could cause schema changes (such as `DROP TABLE` or `DROP DATABASE`) on that descriptor to hang indefinitely until the node holding the stale lease was restarted.
* Fixed a bug where an `ALTER PRIMARY KEY` on a `REGIONAL BY ROW` table with an inverted index could fail validation with an internal error.
* Fixed a bug that caused an internal error when creating a SQL or PL/pgSQL routine with polymorphic parameters (such as `ANYELEMENT`) whose body passed a polymorphic parameter as an argument to another routine expecting a concrete type. This now returns a clear error indicating the case is not yet supported.
* Changefeeds created with the `execution_locality` option no longer fail permanently if no nodes matching the filter are momentarily available, such as during a brief connectivity outage. The changefeed now retries instead.
* `EXPLAIN ANALYZE` now reports accurate per-operator execution statistics for statements inside UDF and stored procedure bodies; they were previously nondeterministic. The reported statistics reflect a single invocation of the routine.
* `EXPLAIN ANALYZE` could attribute execution statistics from unrelated parts of the query to operators of plans created during execution (routine bodies, FK cascades, triggers).
* Fixed a bug where a crash during the initial bootstrap of a new cluster could leave the first store looking initialized while missing its data, causing the node to hang on startup and `cockroach init` to be refused. Such a store is now correctly treated as uninitialized; the partially written store must be cleared before `cockroach init` can be retried.
* `CREATE TENANT` now fails with a clear error if the `RANGE tenants` zone configuration is invalid, instead of creating a tenant that cannot finish starting up.
* `ALTER RANGE default ... CONFIGURE ZONE` now rejects a change that would make an inheriting named range (such as `RANGE tenants`) invalid, instead of silently producing an invalid inherited configuration.
* An `INSERT` or `UPDATE` that writes the referencing columns of a foreign key no longer requires the writing user to hold the `SELECT` privilege on the referenced table, matching PostgreSQL. The foreign-key existence check is authorized by the constraint itself, whose creation already required the `REFERENCES` privilege on the referenced table.
* Fixed a bug where `ALTER RANGE ... RELOCATE` and `ALTER RANGE ... RELOCATE LEASE` loaded every range descriptor in the cluster into memory on each statement, even when relocating a single range. On clusters with a very large number of ranges, running many concurrent `RELOCATE` statements through a single node could exhaust memory and crash that node. These statements now read only the descriptors for the ranges being relocated.
* Zero-argument PL/pgSQL routines now display empty parentheses after the function name in error `CONTEXT` reporting, matching PostgreSQL.
* Fixed an internal error ("inconsistent Case return types") that could occur when a `CASE` expression referenced a column produced by unnesting a constant array whose only element was a typed `NULL`, such as `unnest(ARRAY[NULL]::TEXT[])`.
* Fixed a bug where the `chunk_size` option to `EXPORT INTO PARQUET` was ignored, so files were rotated only by `chunk_rows`. `EXPORT` now bounds the approximate size of each parquet file as documented.
* Fixed an internal error "inconsistent Case return types" that could occur when an `IFERROR` expression with a `NULL` first argument was used as a branch of a `CASE` expression.
* Fixed an internal error ("inconsistent Case return types") that could occur for `CASE`, `IF`, or `NULLIF` expressions when one branch was constant-folded to `NULL`. In addition, `CASE` and `IF` expressions over RECORD-returning user-defined functions with different result types now return a clear user-facing error instead of an internal error.
* Fixed a bug where a node configured with a TLS cipher-suite allowlist (via the `tls-cipher-suites` flag) could stop accepting new TLS connections when a single client handshake stalled. Cipher-suite enforcement held a process-global lock across the TLS handshake, so one stalled handshake serialized all new SQL, HTTP, and RPC connection attempts on the node. This bug was present in v24.1.18, v24.3.14, v25.1.7, v25.2.0, v25.3.0, and all later releases on those branches.
* Fixed a bug where planning a query with a filter that negates a wide disjunction (e.g. `WHERE NOT (b1 OR ... OR bN)`) could consume very large amounts of memory and potentially crash the node, before any rows were read. The optimizer now bounds this work via the new `optimizer_max_disjunction_split_count` session variable.
* Errors with uncategorized `SQLSTATE` codes and retryable errors in PL/pgSQL blocks with non-matching exception handlers now include the PostgreSQL-compatible `CONTEXT` field identifying the routine and line number. The line number is computed from CockroachDB's reformatted function body and may differ from PostgreSQL's when the original source contains blank lines or unusual indentation.
* The `cloud.read_bytes` and `cloud.write_bytes` metrics now account for bytes transferred via external storage opened during early boot (such as the remote storage used by online restore); previously these bytes were omitted.
* Fixed a rare race condition that could crash a node with a "pebble: batch already committing" error when a query buffered results that spilled to disk and then read them back concurrently (e.g. a query referencing a materialized common table expression multiple times).
* Fixed a bug where a statement using a lock timeout (via the `lock_timeout` session variable) could occasionally fail with a bare "context deadline exceeded" error instead of the expected lock-timeout error while waiting on a conflicting lock. This was more likely under heavy load.
* `CREATE TABLE ... AS <query>` with data inside a stored procedure is now rejected with a clear 'not supported' error at procedure-creation time, instead of failing at call time with a confusing 'table is being added' error. `CREATE TABLE ... AS ... WITH NO DATA` and plain `CREATE TABLE` remain supported inside stored procedures.
* Fixed a panic that could occur when using `= ANY(ARRAY[...])` with an array whose element type differs from the compared column’s type.
* Fixed a bug where a query using `OFFSET` could incorrectly return an empty result instead of an error when the `transaction_rows_read_err` session setting was enabled and the offset exceeded the configured row-read limit.
* Fixed a bug where DDL run inside a PL/pgSQL block whose exception handler caught an error remained visible to the rest of the transaction, so a later statement in the same routine could resolve an object that had been rolled back, or re-creating an object with the same name could fail with a spurious "already exists" error.
* Fixed a bug where `SHOW BACKUPS` would not include a leading slash when listing from prefixless collection URIs.
* Fix a bug that could result in a transaction being unexpectedly committed by transaction recovery, resulting in a transaction status error with `REASON_TXN_COMMITTED` when the transaction attempts to commit.
* Fix a bug where a range split could drop the rolled-back sequence number information associated with an unreplicated lock, potentially allowing a lock needed by a transaction to be released early after a savepoint rollback.
* Fixed a bug where a table column default that called a multi-argument sequence builtin such as `setval()` was stored incorrectly, causing later introspection and `DROP TABLE` to fail with an internal error.
* Fixed a bug where using `FETCH ABSOLUTE` on a holdable cursor after the transaction was committed could return unexpected errors.
* Fixed a bug where a changefeed could crash with an internal error like `got a span level timestamp ... that is less than the initial high-water` after being replanned (for example, after changing its sink destination via an external connection).
* Fixed a bug where a failed `ALTER DOMAIN ... SET DEFAULT` or `ALTER DOMAIN ... DROP DEFAULT` could roll back incorrectly and leave the domain default in an inconsistent state for subsequent statements. This schema change is a metadata-only change that cannot be rolled back.
* Fixed a bug where a changefeed could fail permanently with an error like “cannot create external storage before init” if an aggregator was scheduled onto a node (or virtual cluster) that was still starting up. This transient condition is now retried until the node finishes initializing.
* Fixed a bug where the optimizer could return incorrect results when a column was compared to a constant of an equivalent but different type (for example, a `NAME` column compared to a string literal) and a type-sensitive expression such as `pg_typeof` was applied to the same column. In some cases, the optimizer could substitute the constant using the wrong type, changing the result of the type-sensitive expression.
* Fixed a bug where a prepared statement sent over the PostgreSQL extended wire protocol and referencing a user-defined type (such as an enum) could fail with an internal error ("comparison of two different versions of enum") if the type definition changed between statement preparation and execution.
* Fixed a bug where `ALTER TABLE ... ADD CONSTRAINT ... PRIMARY KEY` could panic if the primary key definition included an expression (for example, `PRIMARY KEY ((expr))`). Such expressions are now rejected with a clear error message.
* Fixed an internal error when creating a function that returns the record type of a virtual table; it now returns a proper error.
* Fixed a bug where a transaction that performed a schema change could hang forever at commit time in the two-version invariant check after an internal automatic transaction retry.
* Fixed a bug where filtering an inverted-indexed array column with the `@>` containment operator could return an internal error if the right-hand array contained a `NULL` element.
* Fixed a rare issue that could cause an internal error like "unexpected replacement: original vector is ..." during SQL query execution.
* Fixed a bug where the `SECURITY DEFINER` option on a trigger function was silently ignored: the trigger body ran with the invoking user's identity and privileges instead of the function owner's. Trigger functions declared `SECURITY DEFINER` now execute as the function owner, matching PostgreSQL, so `current_user` and privilege checks inside the trigger body resolve to the owner.
* Fixed an internal error when a `CREATE DOMAIN` statement's `CHECK` constraint contained a subquery. Domain `CHECK` constraints containing subqueries or aggregate, window, set-returning, or procedure calls are now rejected by `CREATE DOMAIN` and `ALTER DOMAIN` instead of being accepted and failing later.
* Fixed a bug where preparing `EXECUTE p` and then re-preparing `p` with a different set of result columns caused an internal error. CockroachDB now returns the expected `cached plan must not change result type` error.
* Fixed a bug in the declarative schema changer where `DROP INDEX` could fail with a spurious `cannot drop index ... because <function/view> depends on it` error when an unrelated table had an index with the same numeric index ID that was referenced by a dependent function or view.
* Error messages that block a schema change because a procedure depends on the affected object now refer to the dependent object as a "procedure" rather than a "function".
* Fixed a bug in the declarative schema changer where creating an index whose temporary-index merge was interrupted more than once (e.g. by node restarts, retriable errors, or pause/resume) re-merged already-completed work on each retry, making the schema change slow to finish.
* Fixed a bug where `RESTORE` with the `skip_missing_sequences` option could fail with an internal validation error when a column’s `DEFAULT` expression referenced multiple sequences and only some of those sequences were included in the restore.
* Fixed a performance bug where the job that refreshes the table metadata cache used by the DB Console Databases and Tables pages could take time quadratic in the number of tables when checking for stale cache entries. This could significantly delay updates to reported table sizes, replica counts, and statistics timestamps on clusters with many tables.
* Fixed a rare bug that could lead to a linearizability violation.
* Fixed a bug where creating a PL/pgSQL function or running a DO block could fail with a parse error if a RAISE statement’s message contained doubled single quotes (for example, `RAISE EXCEPTION 'it''s invalid'`).
* Fixed a bug where `crdb_internal.kv_repairable_catalog_corruptions` reported a session's temporary schema as corruption and `crdb_internal.repair_catalog_corruption` deleted it, which stopped automatic temporary object cleanup from finding the session's tables and left their data on disk.
* Fixed a rare bug where a follower read issued while the replica serving it was applying a split or a snapshot that narrowed its key bounds could omit rows that were committed and still present in the range, rather than being re-routed to the replica that owns them.
* Fixed a bug where calling `pg_get_function_arguments` or `pg_get_function_identity_arguments` on certain built-in array functions could return an internal error ("unknown PG name for oid") and potentially crash catalog introspection queries that scan all functions.
* Fixed a bug where querying `crdb_internal.cluster_queries`, `crdb_internal.node_queries`, or `SHOW QUERIES` could fail with an "index out of range" error if an active query contained placeholder tokens (for example, `$1`) but no captured placeholder values. These views now return results and display the original placeholder token when the value is unavailable.
* Fixed a rare bug where a statement could be applied twice when it was internally retried after the transaction deadline was exceeded (for example, due to an expired descriptor lease or SQL liveness session). This could result in spurious duplicate-key errors or duplicated rows for `INSERT` statements with generated primary keys.
* Fixed a bug where some queries involving the `trigger` pseudo-type (for example, via the `trigger_in` builtin) could return an internal error or crash. These queries now fail gracefully with a user-facing error instead.
* Fixed a bug where a prepared statement that referenced a table via a non-default `search_path` could stop working after its connection was migrated between nodes, failing on execution with a result-shape error even though it worked before the migration.
* Statement bundles collected via `EXPLAIN ANALYZE (DEBUG)` and statement diagnostics no longer include the values of sensitive cluster settings (such as authentication secrets) in `env.sql`, even when collected by a privileged user. A comment indicates when a sensitive setting differs from its default.
* Sort arrows on the Indexes tab of the Table Details page in DB Console had no effect. The Last Read, Total Reads, Last Write and Total Writes columns now sort.
* Fixed a bug where `pg_locks` and `crdb_internal.cluster_held_advisory_locks` could report a transaction-scoped advisory lock as held after a statement in a `READ COMMITTED` transaction acquired the lock and was then automatically retried. The underlying lock state in KV was always correct; only these observability views were affected.
* Fixed a bug where a prepared statement comparing a `TIMESTAMPTZ` column with a `TIMESTAMP` or `DATE` constant could return incorrect results when executed under a session time zone different from the one used at prepare time.
* Fixed a bug where comparing an `INT` column to a `FLOAT` constant with magnitude 2^53 or larger could incorrectly return no rows.
* Fixed a bug where adding an incremental backup to a scheduled backup did not re-enable protected timestamp chaining, which could allow required data to be garbage-collected before the incremental backup could read it.
* Fixed a bug where a cluster `RESTORE` that was paused, or whose coordinator node failed, after its system tables had been restored but before the job completed would fail on resumption with an error such as `restoring system table users: has-column: relation "crdb_temp_system_<id>.users" does not exist`. Such a restore now resumes successfully.
* Fixed a bug where a job that failed into a terminal state could delay returning its error to a synchronous caller by up to the job adoption interval. For example, some invalid `CREATE STATISTICS` statements could appear to hang before surfacing their error.
* Fixed a bug where `BACKUP` and `RESTORE` of an individual table with a column of a user-defined `DOMAIN` type did not include the domain's base type, which could leave the restored table unusable.
* Fixed a rare bug where a follower read that pinned engine state while the serving replica was applying a split or a snapshot that narrowed its key bounds, and that only completed its validity checks after a subsequent merge restored those bounds, could silently omit committed rows.
* PL/pgSQL routines with a polymorphic (`ANYELEMENT`, `ANYARRAY`) parameter or return type no longer fail at definition time with `unable to coerce type <t> to anyelement` when the body assigns or returns an expression of some other type. The coercion is now checked against the concrete argument type at each invocation, as PostgreSQL does.
* Fixed an assertion failure that could occur when a `DOMAIN` type used a `CHECK` constraint containing a range condition.
* Fixed a bug where `TSVECTOR` or `TSQUERY` values containing lexemes with a backslash (`\`) or a single quote (`'`) could be corrupted or fail to parse when round-tripping through their text representation (for example, during `EXPORT` followed by `IMPORT`).
* Fixed a bug where `crdb_internal.sstable_metrics` could ignore its `end_key` argument and report SSTables for only a single key instead of the requested key span.
* Fixed a bug that could crash a node when a `DECLARE CURSOR` statement in an explicit `READ COMMITTED` transaction was automatically retried (for example, after a transaction conflict).
* Fixed a bug that could cause queries with inequality predicates (for example, `!= 'null'`) on a `JSONB` column in a forward (non-inverted) index to silently miss rows where the JSONB value was an empty array (`[]`).
* Fixed a bug where `SHOW PARTITIONS` and other queries that look up rows in `crdb_internal.table_indexes` by descriptor ID could fail with an "unknown schema" error when a dropped table was still awaiting garbage collection after its parent schema had been deleted.
* Improved schema change error messages by removing internal plan stage details from user-facing errors.
* Fixed a bug where `cockroach debug encryption-active-key` reported `Plaintext` for encrypted stores. The command now reports the active store key’s encryption algorithm and key ID.
* Fixed a bug where creating a vector index with an `ENUM` prefix column could fail with an assertion error.
* Fixed a bug where planning a JOIN with an ON clause containing many OR-connected equality conditions could use excessive memory and potentially crash a node before reading any rows. Planning work for these cases is now bounded by the `optimizer_max_disjunction_split_count` session setting.
* Fixed a compatibility issue where `BACKUP`, `RESTORE`, and `SHOW BACKUPS` could fail against some S3-compatible object stores that require single-character delimiters when listing objects (for example, Alibaba OSS).
* Fixed a bug where `CREATE TRIGGER` specifying a built-in function as the trigger function (for example, `EXECUTE FUNCTION now()`) could return an internal error instead of reporting that the function must return type `trigger`.
* Fixed a bug where `ALTER TABLE ... SET LOCALITY REGIONAL BY ROW AS ...` could fail with an internal error when the table was already REGIONAL BY ROW and its primary key was hash-sharded.
* Fixed a bug where hitting `statement_timeout` inside an explicit transaction could leave the transaction in an unrecoverable aborted state. Explicit transactions can now be recovered with `ROLLBACK TO SAVEPOINT`, matching PostgreSQL behavior.
* Fixed a bug where setting up a Physical Cluster Replication (PCR) readable standby (read-only virtual cluster) could fail to initialize its catalog if the replicated cluster contained a materialized view, preventing the standby from starting.
* Fixed a bug where changefeeds created `WITH diff` could emit confusing delete messages with both before and after images set to `NULL`. This could occur on `REGIONAL BY ROW` tables under `READ COMMITTED` or `SNAPSHOT` isolation, or when deleting a non-existent row from a table with no secondary indexes.
* Fixed a bug where a failed backup compaction job could leave a stale `BACKUP-LOCK` file in external storage, blocking subsequent compaction attempts to the same destination until the lock was removed.
* Fixed a bug where the optimizer could nondeterministically choose between a constrained index scan and a full scan for queries with an equality filter on an expression that is the key of multiple partial expression indexes.
* Fixed a bug where `width_bucket(operand, thresholds[])` could silently accept `NULL` elements in the `thresholds` array and return an incorrect bucket index. It now returns an error (`thresholds array must not contain NULLs`) if `thresholds` contains any `NULL`, matching PostgreSQL behavior.
* Fixed a bug where `make_date()`, `make_timestamp()`, and `make_timestamptz()` could silently normalize out-of-range month, day, hour, minute, or second values into a different valid date or time. These functions now return a "date/time field value out of range" error for invalid inputs (matching PostgreSQL), and validate day values against the actual month length (including leap years).
* Fixed a bug where starting a node with `--accept-proxy-protocol-headers` caused it to reject connections that did not begin with a PROXY protocol header. Nodes now accept direct connections (for example, health probes and `cockroach init`) and honor the PROXY header only when it is present.

### Performance improvements

* `SHOW CHANGEFEED JOBS` is now significantly faster and uses less temporary storage when listing changefeeds that watch many tables.
* Improved performance for some correlated `EXISTS` and `IN` subqueries that include `UNION ALL`. These queries can now be planned as more efficient (non-apply) joins instead of re-evaluating the subquery once per outer row, which can substantially speed up execution when the outer input is large.
* Improved performance of queries against `crdb_internal.kv_repairable_catalog_corruptions`, which are now significantly faster on clusters with many descriptors.
* Reduced the memory footprint of the table statistics cache: cached merged and forecast statistics no longer retain an extra encoded copy of their histograms in addition to the decoded form used by the optimizer.
* Sped up scans of `crdb_internal.create_statements` on clusters with many tables, which previously scaled quadratically with the number of tables and could cause debug zip collection to time out on large-schema clusters.
* Added the `cloudstorage.gs.transport_shard_count` cluster setting. When set above `1`, CockroachDB shards reads from Google Cloud Storage across that many HTTP connections, which can substantially increase read throughput for bulk operations such as online restore. Defaults to `0` (a single connection, as before).
* Reduced per-statement latency for queries against tables with row-level security (RLS) enabled. Previously, every such statement performed an uncached read of the `system.role_options` table to evaluate the `BYPASSRLS` exemption, adding a round-trip (potentially cross-region, since `system.role_options` is a single global table) to every query regardless of the number of rows scanned. This read is now cached and skipped while the relevant role state is unchanged.
* Repeatedly resolving an unqualified name that does not exist (for example, a user-defined type referenced by an unqualified name under a non-public schema) within a single transaction no longer performs redundant reads of `system.namespace` for each occurrence, reducing latency for such workloads.
* Online restore now begins downloading data as soon as the link phase completes, rather than waiting for the next job adoption cycle.
* Added the `cloudstorage.s3.chunked_upload.concurrency` cluster setting to control how many parts a single object upload to Amazon S3 can upload in parallel. Increasing this setting can improve upload throughput for large files.
* Improved performance for queries with `CROSS JOIN` (and outer joins with an always-true join condition) and a constant `LIMIT` by avoiding full scans of join inputs in more cases.

### Build changes

* Upgraded the Kafka client library used by changefeeds to improve producer resilience during Kafka broker restarts and transient network failures.

### Miscellaneous

* Added support to restore from encrypted backups with online fast (`WITH EXPERIMENTAL COPY`) restores. This functionality requires that the backup was taken on a 26.4 or later cluster, and that encryption at rest is enabled for all stores in the restoring cluster.
* Added the scheduled backup metric `schedules.BACKUP.cluster.time_since_last_completed`, which reports the time since the last successful backup (RPO) for a schedule without requiring additional aggregation or calculation.
* Added support for recommending inverted (`GIN`) indexes on `ARRAY` and `STRING` columns in the index recommendation engine, improving recommendations for queries that use array containment operators (such as `@>` and `<@`) and `LIKE`/`ILIKE` predicates.
* This patch introduces a format change for encrypted backups. Encrypted backups will use this format by default starting in 26.4. Restores work with both formats.
* Running a `RESTORE ... WITH EXPERIMENTAL COPY` now outputs the same 4 column result as traditional restore. `RESTORE ... WITH EXPERIMENTAL DEFERRED COPY` keeps the same 5 column result.
* Fixed an issue which can lead to too many WAL files written in the WAL failover location, causing out-of-disk.

## v26.4.0-alpha.2

Release Date: September 16, 2026

### Downloads

<Danger>
  CockroachDB v26.4.0-alpha.2 is a testing release. Testing releases are intended for testing and experimentation only, and are not qualified for production environments and not eligible for support or uptime SLA commitments.
</Danger>

<Note>
  Experimental downloads are not qualified for production use and not eligible for support or uptime SLA commitments, whether they are for testing releases or production releases.
</Note>

<table><thead><tr><th>Operating System</th><th>Architecture</th><th>Full executable</th><th>SQL-only executable</th></tr></thead><tbody><tr><td rowspan="2">Linux</td><td>Intel</td><td><a href="https://binaries.cockroachdb.com/cockroach-v26.4.0-alpha.2.linux-amd64.tgz">cockroach-v26.4.0-alpha.2.linux-amd64.tgz</a><br />(<a href="https://binaries.cockroachdb.com/cockroach-v26.4.0-alpha.2.linux-amd64.tgz.sha256sum">SHA256</a>)</td><td><a href="https://binaries.cockroachdb.com/cockroach-sql-v26.4.0-alpha.2.linux-amd64.tgz">cockroach-sql-v26.4.0-alpha.2.linux-amd64.tgz</a><br />(<a href="https://binaries.cockroachdb.com/cockroach-sql-v26.4.0-alpha.2.linux-amd64.tgz.sha256sum">SHA256</a>)</td></tr><tr><td>ARM</td><td><a href="https://binaries.cockroachdb.com/cockroach-v26.4.0-alpha.2.linux-arm64.tgz">cockroach-v26.4.0-alpha.2.linux-arm64.tgz</a><br />(<a href="https://binaries.cockroachdb.com/cockroach-v26.4.0-alpha.2.linux-arm64.tgz.sha256sum">SHA256</a>)</td><td><a href="https://binaries.cockroachdb.com/cockroach-sql-v26.4.0-alpha.2.linux-arm64.tgz">cockroach-sql-v26.4.0-alpha.2.linux-arm64.tgz</a><br />(<a href="https://binaries.cockroachdb.com/cockroach-sql-v26.4.0-alpha.2.linux-arm64.tgz.sha256sum">SHA256</a>)</td></tr><tr><td rowspan="2">Mac<br />(Experimental)</td><td>Intel</td><td><a href="https://binaries.cockroachdb.com/cockroach-v26.4.0-alpha.2.darwin-10.9-amd64.tgz">cockroach-v26.4.0-alpha.2.darwin-10.9-amd64.tgz</a><br />(<a href="https://binaries.cockroachdb.com/cockroach-v26.4.0-alpha.2.darwin-10.9-amd64.tgz.sha256sum">SHA256</a>)</td><td><a href="https://binaries.cockroachdb.com/cockroach-sql-v26.4.0-alpha.2.darwin-10.9-amd64.tgz">cockroach-sql-v26.4.0-alpha.2.darwin-10.9-amd64.tgz</a><br />(<a href="https://binaries.cockroachdb.com/cockroach-sql-v26.4.0-alpha.2.darwin-10.9-amd64.tgz.sha256sum">SHA256</a>)</td></tr><tr><td>ARM</td><td><a href="https://binaries.cockroachdb.com/cockroach-v26.4.0-alpha.2.darwin-11.0-arm64.tgz">cockroach-v26.4.0-alpha.2.darwin-11.0-arm64.tgz</a><br />(<a href="https://binaries.cockroachdb.com/cockroach-v26.4.0-alpha.2.darwin-11.0-arm64.tgz.sha256sum">SHA256</a>)</td><td><a href="https://binaries.cockroachdb.com/cockroach-sql-v26.4.0-alpha.2.darwin-11.0-arm64.tgz">cockroach-sql-v26.4.0-alpha.2.darwin-11.0-arm64.tgz</a><br />(<a href="https://binaries.cockroachdb.com/cockroach-sql-v26.4.0-alpha.2.darwin-11.0-arm64.tgz.sha256sum">SHA256</a>)</td></tr><tr><td>Windows<br />(Experimental)</td><td>Intel</td><td><a href="https://binaries.cockroachdb.com/cockroach-v26.4.0-alpha.2.windows-6.2-amd64.zip">cockroach-v26.4.0-alpha.2.windows-6.2-amd64.zip</a><br />(<a href="https://binaries.cockroachdb.com/cockroach-v26.4.0-alpha.2.windows-6.2-amd64.zip.sha256sum">SHA256</a>)</td><td><a href="https://binaries.cockroachdb.com/cockroach-sql-v26.4.0-alpha.2.windows-6.2-amd64.zip">cockroach-sql-v26.4.0-alpha.2.windows-6.2-amd64.zip</a><br />(<a href="https://binaries.cockroachdb.com/cockroach-sql-v26.4.0-alpha.2.windows-6.2-amd64.zip.sha256sum">SHA256</a>)</td></tr></tbody></table>

### Docker image

[Multi-platform images](https://docs.docker.com/build/building/multi-platform) include support for both Intel and ARM. Multi-platform images do not take up additional space on your Docker host.

Within the multi-platform image, both Intel and ARM images are available for testing.

To download the Docker image:

```shell theme={"theme":{"light":"catppuccin-mocha","dark":"catppuccin-mocha"}}
docker pull cockroachdb/cockroach-unstable:v26.4.0-alpha.2
```

### Backward-incompatible changes

* Removed the experimental, unsupported `COCKROACH_S3_LIST_WITH_PREFIX_SLASH_MARKER` flag.

### Security updates

* Redacted secret values for changefeed schedule options (for example, `webhook_auth_header`) in the `command` column of `SHOW SCHEDULES`, in addition to sanitizing the sink URI.
* The values of the changefeed options `webhook_auth_header`, `extra_headers`, and `confluent_schema_registry`, which can contain credentials, are now redacted by default when a changefeed statement is reformatted for display (e.g. in `SHOW CREATE SCHEDULE`), matching the existing handling of changefeed sink URIs.

### General changes

* Changed the default value of the `sql.insights.anomaly_detection.memory_limit` cluster setting to `10MiB` (from `1MiB`), so SQL Insights can keep more statement fingerprints in memory for anomaly detection. SQL Insights now records execution latencies for all statements to improve anomaly detection coverage, including for statements that are usually below the latency threshold. The latency threshold still controls which statements are surfaced as insights.
* With `jobs.execution.panic_recovery.enabled` set, a crash in a processor running part of a job can now fail the job instead of bringing down the nodes running that job's work.
* Added the cluster setting `jobs.execution.panic_recovery.enabled` to prevent a job panic from crashing the node running the job. When enabled, the job is failed (or paused if it panics while reverting), and the panic backtrace is recorded on the job. Nodes that recover a panic increment `jobs.execution.panics_recovered` and should be **restarted**.

### SQL language changes

* Fixed a bug where `LOOKUP JOIN` or `MERGE JOIN` hints could fail with an error like "could not produce a query plan conforming to the ... hint" when the join condition compared columns using a value-preserving cast. These predicates are now recognized as equijoins, allowing the hinted plan to be produced and avoiding fallback to an inefficient cross join.
* The `optimizer_span_limit` session setting now also bounds the number of spans the optimizer can generate for queries that use inverted, vector, and trigram indexes, helping avoid excessively large span sets.
* Added consistency checks for supported partial indexes in `INSPECT TABLE` and `INSPECT DATABASE`; selecting a partial index with the `INDEX` option no longer returns an error.
* `SHOW CREATE TABLE` now lists a column family's members in column-ID order for tables created after this change, rather than in the order the `FAMILY` clause named them. Families of existing tables are unaffected, and generated family names are unchanged.
* Added the `information_schema.crdb_stores` view to list per-store metrics (including `node_id`, range and lease counts, under/over/unavailable-replicated range counts, capacity/available/used bytes, percent available, and attributes) using stable, named columns instead of requiring JSON parsing of `crdb_internal.kv_store_status`. This view is not supported in secondary tenants.
* `CREATE TEMPORARY TABLE` now supports the Postgres `ON COMMIT DELETE ROWS` and `ON COMMIT DROP` options, in addition to the existing `ON COMMIT PRESERVE ROWS` default. `ON COMMIT DELETE ROWS` removes all rows at the end of every successful transaction. `ON COMMIT DROP` drops the table when its creating transaction commits. Temporary tables remain gated by the `experimental_enable_temp_tables` session setting. When `autocommit_before_ddl` is enabled, `CREATE TEMPORARY TABLE ... ON COMMIT DROP` itself is exempt, but other DDL can still commit the transaction and drop the temporary table before that statement runs.
* Changed `IMPORT INTO` for `REGIONAL BY TABLE` tables to default `execution_locality` to the table's home region when the option is not specified, which can reduce cross-region traffic. Set `execution_locality = ''` to opt out and run unconstrained.
* Implemented the `ST_ForceRHR(geometry)` builtin function, a synonym of `ST_ForcePolygonCW` that returns a geometry whose polygon exterior rings are clockwise and interior rings counter-clockwise (non-polygon objects are unchanged), matching PostGIS behavior.

### Operational changes

* Added cluster settings to control collection of KV-layer Active Session History (ASH) samples for separate-process tenants: `obs.ash.kv_sample_pull.enabled` enables or disables pulling KV-side ASH samples into the tenant. `obs.ash.kv_sample_pull.interval` sets how frequently the tenant pulls KV-side ASH samples.
* Added the `sql.routine.dynamic_execute.started.count` and `sql.routine.dynamic_execute.count` metrics, which count statements run through a PL/pgSQL dynamic `EXECUTE`.
* Changed routine statement metrics so SQL statements executed via PL/pgSQL `EXECUTE` are now counted in the corresponding `sql.routine.<type>.count` metrics (for `SELECT`, `INSERT`, `UPDATE`, and `DELETE`), and are also tracked separately in the new `sql.routine.dynamic_execute.*` metrics.
* Updated the `--external-io-dir` help text to clarify that passing an explicitly empty value (`--external-io-dir=`) disables local file I/O, and that `disabled` is treated as a directory name (not a special value).

### DB Console changes

* Updated the **DB Console** UI to match the latest Cockroach Labs branding: Updated the color palette and adjusted UI surfaces (white content area with a warm-gray sidebar). Updated typography to use Geist and Geist Mono. Updated the logo and favicon.
* Changed the **DB Console** on Basalt-backed clusters to show only **Storage Used** (bytes stored) and hide usable, available, and maximum capacity values and percent used.

### Bug fixes

* Fixed a bug where a KV request sent to a learner replica (for example, due to a stale cached range descriptor) could return a "replica unavailable" error instead of redirecting to the leaseholder, causing unnecessary latency.
* Fixed a bug where `TRUNCATE` could incorrectly treat indexes backing `UNIQUE` constraints as explicitly created, allowing `DROP INDEX` to drop them without requiring `CASCADE`.
* Fixed an issue where OpenID Connect (OIDC) single sign-on logins to the **DB Console** could block one another when the identity provider was slow or unavailable.
* Fixed a bug where `ALTER TYPE ... DROP VALUE` could succeed even when the enum value was referenced as an explicitly typed constant in a trigger `WHEN` condition or trigger function body. This could leave the trigger in an invalid state and cause subsequent `INSERT`, `UPDATE`, and `DELETE` statements on the table to fail; `ALTER TYPE ... DROP VALUE` now returns an error in this case. References where the constant’s type is only implied by the column it is compared against (for example, `WHEN (NEW.c = 'a')`) are not yet detected.
* Fixed a rare race condition in `IMPORT` progress tracking that could compute an incorrect resume position, causing some rows to be skipped when an `IMPORT` job was resumed.
* Fixed a bug where sessions with `default_int_size=4` could have unqualified `INTEGER` types re-resolved as `INT8` during automatic transaction retries or when prepared statements were refreshed after a referenced user-defined type changed, which could cause client decode errors for 32-bit integers.
* Fixed a bug that could cause queries using a partial index to scan more rows than necessary when the index predicate referenced leading index key columns.
* Fixed a bug where `RESTORE TABLE db.schema.*` could fail with an internal error if the restore target contained a user-defined function that referenced objects in another, non-restored schema; restore now either succeeds by including required function/type dependencies or fails with a clear error when it depends on missing table/view/sequence objects.
* Fixed a bug where planning a query with an inverted-index filter over a very large value (for example a JSON array containment with thousands of elements, or a long trigram or `tsquery` match) could cause the node to run out of memory during query planning. Such filters now respect the `optimizer_span_limit` session setting and fall back to a non-accelerated scan when the limit is exceeded.
* Fixed a rare row-based DistSQL bug that could cause queries to fail with the internal error `decoding unset EncDatum` after an `INTERVAL` overflow during stream encoding.
* Fixed a rare data race that could cause a node to **crash** when automatic statistics setting overrides for a table were updated concurrently with query planning. This issue could occur only when the `sql.log.scan_row_count_misestimate.enabled` cluster setting was enabled (it is disabled by default).
* Fixed a bug that could acknowledge a Raft log write before it was durable when the write required a sync but carried no data, risking the loss of an acknowledged entry on a crash. Only test builds were affected; production deployments are not.
* Fixed a bug where an error raised inside a PL/pgSQL `EXCEPTION` handler could be matched to the wrong enclosing `EXCEPTION` clause, causing the wrong handler to run or the correct enclosing handler to be skipped.
* Fixed a bug where a `REGIONAL BY TABLE IN <region>` table in a multi-region database with a secondary region could be assigned incorrect lease preferences. This bug was only in the declarative schema changer and affected tables set to `REGIONAL BY TABLE IN <region>` via `ALTER TABLE ... LOCALITY` in 26.2+.
* `pg_catalog.pg_depend` reported the wrong column for a view depending on a column whose type had been changed by an `ALTER COLUMN TYPE` that rewrote on-disk data.
* Foreign key, check, unique, and `NOT NULL` constraints on a column whose type had been changed by an `ALTER COLUMN TYPE` that rewrote on-disk data were not visible to tools that inspect constraints through `pg_catalog.pg_constraint`, such as ORMs. The constraints were still enforced.
* Fixed a bug where the optimizer could not use indexes on `OID`-typed columns (such as in `pg_catalog`) when the column was compared to an integer constant or parameter (for example, a client driver binding the value as `int4` or `int8`), which could cause a full table scan.
