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
1 change: 1 addition & 0 deletions .nextchanges/bundles/grant-privilege-enum-no-validate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Accept grant `privileges` values that the SDK enum does not list yet: `bundle validate` no longer warns (or errors under `--strict`) on them, and the PyDABs `Privilege` enum accepts unknown strings instead of raising. The JSON schema and PyDABs still advertise the known values for completion. ([#7315](https://github.com/databricks/cli/pull/7315))
11 changes: 11 additions & 0 deletions acceptance/bundle/validate/enum/databricks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,14 @@ resources:
ebs_volume_type: "INVALID_VOLUME_TYPE"
node_type_id: "i3.xlarge"
num_workers: 1

schemas:
my_schema:
name: my_schema
catalog_name: main
grants:
- principal: user@databricks.com
privileges:
# Grant privileges are not validated against the enum, so this
# does not trigger a warning (the SDK enum lags the platform).
- INVALID_PRIVILEGE
9 changes: 9 additions & 0 deletions bundle/internal/validation/enum.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/databricks/cli/libs/structs/structpath"
"github.com/databricks/cli/libs/structs/structtag"
"github.com/databricks/cli/libs/structs/structwalk"
"github.com/databricks/databricks-sdk-go/service/catalog"
)

type EnumPatternInfo struct {
Expand Down Expand Up @@ -126,6 +127,14 @@ func extractEnumFields(typ reflect.Type) ([]EnumPatternInfo, error) {
}
}

// The platform accepts grant privileges that the SDK's catalog.Privilege
// enum does not yet know about, so validating against it rejects values the
// platform allows (see https://github.com/databricks/cli/issues/6770). The
// JSON schema still advertises the enum; we just don't warn/error on it here.
if fieldType == reflect.TypeFor[catalog.Privilege]() {
return false
}

// Check if this field type is an enum
if isEnumType(fieldType) {
// Get enum values
Expand Down
16 changes: 1 addition & 15 deletions bundle/internal/validation/generated/enum_fields.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 40 additions & 0 deletions python/codegen/codegen/generated_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
from codegen.generated_dataclass import _append_description
from codegen.jsonschema import Schema, is_experimental_stage

# Enums whose platform value set grows faster than the schema. They keep their
# members for completion, but unknown strings are accepted instead of rejected.
OPEN_ENUMS = {"catalog.Privilege"}


@dataclass(kw_only=True)
class GeneratedEnum:
Expand All @@ -16,6 +20,7 @@ class GeneratedEnum:
description: Optional[str]
experimental: bool
deprecated: bool
open: bool = False


def generate_enum(namespace: str, schema_name: str, schema: Schema) -> GeneratedEnum:
Expand Down Expand Up @@ -43,6 +48,7 @@ def generate_enum(namespace: str, schema_name: str, schema: Schema) -> Generated
description=schema.description,
experimental=is_experimental_stage(schema.stage),
deprecated=schema.deprecated or False,
open=schema_name in OPEN_ENUMS,
)


Expand Down Expand Up @@ -71,6 +77,10 @@ def get_code(generated: GeneratedEnum) -> str:
b.indent().append(f'{key} = "{value}"')
b.newline()

if generated.open:
b.newline()
_append_missing(b, generated.class_name)

b.newline()

# Example:
Expand All @@ -80,11 +90,41 @@ def get_code(generated: GeneratedEnum) -> str:
b.append(generated.class_name).append('Param = Literal["')
b.append_list(list(generated.values.values()), sep='", "')
b.append('"] | ', generated.class_name)

if generated.open:
b.append(" | str")

b.newline()

return b.build()


def _append_missing(b: CodeBuilder, class_name: str):
# Example:
#
# @classmethod
# def _missing_(cls, value: object) -> "Color | None":
# ...
#
# Unknown strings become pseudo-members, the same way stdlib Flag builds
# them. They are cached in _value2member_map_ so repeated lookups return
# the same object and dataclass equality keeps working.
b.indent().append("@classmethod").newline()
b.indent().append(
f'def _missing_(cls, value: object) -> "{class_name} | None":'
).newline()
b.indent().indent().append("if not isinstance(value, str):").newline()
b.indent().indent().indent().append("return None").newline()
b.newline()
b.indent().indent().append("member = object.__new__(cls)").newline()
b.indent().indent().append("member._name_ = value").newline()
b.indent().indent().append("member._value_ = value").newline()
b.newline()
b.indent().indent().append(
"return cls._value2member_map_.setdefault(value, member) # type:ignore"
).newline()


def _camel_to_upper_snake(value):
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", value)
s1 = re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1)
Expand Down
Loading
Loading