Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,3 @@ EnvMatrix.DMS = []
Old = "Field 'spec\\.[a-z_.]+' is in update_mask but not provided"
New = "Field '[SPEC_FIELD]' is in update_mask but not provided"
Order = 1

[[Repls]]
# The whole-spec 500 carries a per-request trace id.
Old = '\[TraceId: [0-9a-f]+\]'
New = '[TraceId: [TRACE_ID]]'
Order = 1
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,27 @@

=== update_mask=spec with a body that now omits the non-default default_endpoint_settings
>>> [CLI] postgres update-project projects/factcheck-spec-[UNIQUE_NAME] spec --json {"spec":{"display_name":"changed2","pg_version":16,"history_retention_duration":"604800s"}}
Error: The request could not be processed. Please try again later and contact Databricks if the problem persists. [TraceId: [TRACE_ID]]

Exit code: 1
{
"name": "projects/factcheck-spec-[UNIQUE_NAME]",
"project_id": "factcheck-spec-[UNIQUE_NAME]",
"status": {
"branch_logical_size_limit_bytes": [BRANCH_SIZE_LIMIT],
"default_branch": "projects/factcheck-spec-[UNIQUE_NAME]/branches/production",
"default_endpoint_settings": {
"autoscaling_limit_max_cu": 4,
"autoscaling_limit_min_cu": 0.5,
"suspend_timeout_duration": "300s"
},
"display_name": "changed2",
"enable_pg_native_login": false,
"history_retention_duration": "604800s",
"owner": "[USERNAME]",
"pg_version": 16,
"project_id": "factcheck-spec-[UNIQUE_NAME]",
"synthetic_storage_size_bytes": 0
},
"uid": "[UUID]"
}

>>> print_requests.py //postgres/projects --method PATCH
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,8 @@ trace $CLI postgres get-project "$name" | project_fields
trace print_requests.py //postgres/projects --method PATCH

title "update_mask=spec with a body that now omits the non-default default_endpoint_settings"
# 500: masking the whole spec resets the omitted sub-message and its emptied autoscaling
# limits fail validation. The other fields are applied first, so it is a partial write —
# the final read below shows display_name changed to changed2 while the response errored.
# Masking the whole spec keeps the omitted sub-message: the stored endpoint settings
# survive. (Until 2026-11-02 the API returned 500 here, after a partial write.)
errcode trace $CLI postgres update-project "$name" spec --json '{"spec":{"display_name":"changed2","pg_version":16,"history_retention_duration":"604800s"}}' | project_fields
trace print_requests.py //postgres/projects --method PATCH
trace $CLI postgres get-project "$name" | project_fields
49 changes: 2 additions & 47 deletions libs/testserver/postgres.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package testserver

import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"regexp"
Expand All @@ -19,14 +17,6 @@ func nowTime() *sdktime.Time {
return sdktime.New(time.Now().UTC())
}

// nextTraceID returns a 32-hex-character id shaped like the trace ids the real API puts
// in error messages, so acceptance goldens normalize local and cloud identically.
func nextTraceID() string {
b := make([]byte, 16)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
}

// Backend defaults applied to a project's status when the spec omits them.
// These mirror the values the real API materializes on create.
const (
Expand Down Expand Up @@ -261,20 +251,8 @@ func (s *FakeWorkspace) PostgresProjectUpdate(req Request, name string) Response
project.UpdateTime = nowTime()
s.PostgresProjects[name] = project

// Whole-spec mask + a body that omits default_endpoint_settings, while the stored
// settings are non-default, makes the real API 500 ("could not be processed"): it
// resets the message and the emptied autoscaling limits fail validation. The other
// masked fields are applied first, though — the failure is a partial write, so the
// mutation above is persisted and only this response reports the error. When the
// settings are already default, or the message is masked by its own path, the
// omission is tolerated. Probed against a real workspace 2026-09-21.
if wholeSpecMasked(req.URL.Query().Get("update_mask")) &&
(updateProject.Spec == nil || updateProject.Spec.DefaultEndpointSettings == nil) &&
project.Status != nil && isNonDefaultEndpointSettings(project.Status.DefaultEndpointSettings) {
return postgresErrorResponse(500, "INTERNAL_ERROR",
fmt.Sprintf("The request could not be processed. Please try again later and contact Databricks if the problem persists. [TraceId: %s]", nextTraceID()))
}

// Whole-spec mask + a body that omits default_endpoint_settings keeps the stored
// settings. Until 2026-11-02 the real API returned 500 here when they were non-default.
return Response{
Body: s.createOperationLocked(project.Name, project),
}
Expand Down Expand Up @@ -997,29 +975,6 @@ func validateUpdateMask(req Request, allowed []string, oneofGroups map[string][]
return nil
}

// wholeSpecMasked reports whether the update_mask names the whole spec message (a bare
// "spec" path), as the Terraform provider does.
func wholeSpecMasked(mask string) bool {
for path := range strings.SplitSeq(mask, ",") {
if strings.TrimSpace(path) == "spec" {
return true
}
}
return false
}

// isNonDefaultEndpointSettings reports whether the settings differ from what create
// materializes by default (autoscaling 1..1, 86400s suspend).
func isNonDefaultEndpointSettings(s *postgres.ProjectDefaultEndpointSettings) bool {
if s == nil {
return false
}
if s.AutoscalingLimitMinCu != defaultAutoscalingLimitMinCu || s.AutoscalingLimitMaxCu != defaultAutoscalingLimitMaxCu {
return true
}
return s.SuspendTimeoutDuration != nil && s.SuspendTimeoutDuration.AsDuration() != defaultSuspendTimeout
}

// wildcardMissingField returns the first spec field that update_mask=* requires but the
// body does not carry. The wildcard expands to every settable leaf, and — unlike a bare
// message mask — the API then demands each one be present (probed against a real
Expand Down
Loading