Skip to content
Draft
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
23 changes: 10 additions & 13 deletions .github/workflows/tagging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@ name: tagging
on:
# Manual dispatch.
workflow_dispatch:
inputs:
package:
description: 'Package to tag (leave empty to tag all packages with pending releases).'
required: false
type: string
default: ''
# No inputs are required for the manual dispatch.

# NOTE: Temporarily disable automated releases.
#
Expand Down Expand Up @@ -66,10 +61,12 @@ jobs:
env:
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
PACKAGE: ${{ inputs.package }}
run: |
if [ -n "$PACKAGE" ]; then
uv run --locked tagging.py --package "$PACKAGE"
else
uv run --locked tagging.py
fi
run: uv run --locked tagging.py

- name: Upload created tags artifact
if: always()
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: created-tags
path: created_tags.json
if-no-files-found: ignore
117 changes: 117 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,120 @@
> - **Breaking changes may occur at any time**
> - **APIs are experimental and unstable**
> - **Use for development and testing only**

## Design Notes

### FieldMask — Deferred Ergonomics Decisions

The current `FieldMask` implementation validates paths against a per-message
schema inside `FieldMask.build`, throws on mismatch, and stores the
wire-format paths privately so `toString()` produces the server-facing
comma-separated string. Construction entry point today is a **per-message
factory function** generated alongside each message — e.g.
`alertFieldMask('displayName', 'condition.op')` — which supplies the schema
and message name and delegates to `FieldMask.build`. This section records
two refinements we've discussed but deferred; they do not change the
validation semantics, only the call-site shape.

#### Q1 — Call-site ergonomics

**Current (Option C, per-message factory):**

```ts
import {alertFieldMask} from '@databricks/sdk-alerts/v1';
const mask = alertFieldMask('displayName', 'condition.op');
```

- Pros: best discoverability (auto-complete on import), single-argument call,
error messages name the target message, and users never think about
schemas or message names.
- Cons: one generated factory per message (~60+ across the SDK). Each is a
thin one-liner, but they multiply with every new message.

**Option A — raw `FieldMask.build` at the call site:**

```ts
import {FieldMask} from '@databricks/sdk-core/wkt';
import {Alert, alertFieldMaskSchema} from '@databricks/sdk-alerts/v1';

const mask = FieldMask.build<Alert>(
['displayName', 'condition.op'],
alertFieldMaskSchema,
);
```

- Pros: zero helper functions anywhere. Only `FieldMask.build` exists.
- Cons: two-argument low-level call at every usage site. Users must
import the schema explicitly and know which schema pairs with which
type.

**Option B — one generic helper in `sdk-core`:**

```ts
import {fieldMask} from '@databricks/sdk-core/wkt';
import {Alert} from '@databricks/sdk-alerts/v1';

const mask = fieldMask(Alert, 'displayName', 'condition.op');
```

- Pros: a single `fieldMask(...)` replaces the ~60+ per-message factories.
`Alert` supplies both the schema and the message name as properties on
itself, so the call site stays one argument plus paths.
- Cons: requires `Alert` the import to be **both** a TypeScript type and a
runtime descriptor value on the same name. See Q2 below.

#### Q2 — Interface + const declaration merging on the message name

A TypeScript pattern that lets one exported identifier occupy both the
type-space and the value-space:

```ts
// Type — describes instance shape. Zero runtime cost.
export interface Alert {
displayName?: string;
condition?: Condition;
}

// Runtime value — carries the schema under the same name.
export const Alert: MessageDescriptor<Alert> = {
fieldMaskSchema: {
displayName: {wire: 'display_name'},
condition: {wire: 'condition', children: () => Condition.fieldMaskSchema},
},
};
```

Usage after this change:

```ts
const a: Alert = {displayName: 'foo'}; // Alert as a type (literal shape)
const s = Alert.fieldMaskSchema; // Alert as a value (runtime)
const mask = fieldMask(Alert, 'displayName'); // Option B unblocked
```

TypeScript supports this via
[declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html).
`interface X` occupies type-space, `const X` occupies value-space; they do
not collide. The same pattern shows up in `lib.dom.d.ts`, `Promise`, and
various DI libraries.

- Pros: single import, natural `Alert.fieldMaskSchema` access, enables
Option B without asking users to remember a separate `AlertSchema` name.
- Cons: the pattern is less common in everyday TS and can surprise readers
who expect classes for anything with static-like members. The alternative
is to use a distinct name — e.g. `AlertSchema` or `AlertDescriptor` — for
the runtime value, at the cost of one extra identifier to learn per
message.

#### Why we haven't landed Q1/Q2 yet

Both are call-site ergonomics refactors; neither changes the validation
semantics or the per-message schema generation. We want the current
`FieldMask.build` path (validate, translate, store wire paths privately,
`toString()` joins) to settle before adjusting the call-site shape, so
the two axes of change don't churn simultaneously. When we revisit, the
likely landing is **Option B + Option Q2** together — one generic
`fieldMask()` plus the interface+const merge, which most closely matches
what similar TypeScript validation libraries (Zod, Valibot, TypeBox, etc.)
expose while staying consistent with the SDK's existing interface-first
convention for message types.
2 changes: 1 addition & 1 deletion packages/abacpolicies/src/v1/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ export class Client {
const url = `${this.host}/api/2.1/unity-catalog/policies/${req.onSecurableType ?? ''}/${req.onSecurableFullname ?? ''}/${req.name ?? ''}`;
const params = new URLSearchParams();
if (req.updateMask !== undefined) {
params.append('update_mask', req.updateMask);
params.append('update_mask', req.updateMask.toString());
}
const query = params.toString();
const fullUrl = query !== '' ? `${url}?${query}` : url;
Expand Down
176 changes: 161 additions & 15 deletions packages/abacpolicies/src/v1/model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Code generated from API definition by Databricks SDK Generator. DO NOT EDIT.

import {FieldMask} from '@databricks/sdk-core/wkt';
import type {FieldMaskSchema} from '@databricks/sdk-core/wkt';
import {z} from 'zod';

export enum PolicyType {
Expand Down Expand Up @@ -292,7 +294,7 @@ export interface UpdatePolicy {
* Optional. The update mask field for specifying user intentions on which
* fields to update in the request.
*/
updateMask?: string | undefined;
updateMask?: FieldMask<PolicyInfo> | undefined;
}

export const unmarshalColumnMaskOptionsSchema: z.ZodType<ColumnMaskOptions> = z
Expand Down Expand Up @@ -474,9 +476,6 @@ export const marshalColumnTagValueExtractionSchema: z.ZodType = z
tag_key: d.tagKey,
}));

// eslint-disable-next-line @typescript-eslint/naming-convention -- Proto-style nested message name.
export const marshalDeletePolicy_ResponseSchema: z.ZodType = z.object({});

export const marshalDenyOptionsSchema: z.ZodType = z
.object({
privileges: z.array(z.string()).optional(),
Expand Down Expand Up @@ -507,17 +506,6 @@ export const marshalGrantOptionsSchema: z.ZodType = z
privileges: d.privileges,
}));

// eslint-disable-next-line @typescript-eslint/naming-convention -- Proto-style nested message name.
export const marshalListPolicies_ResponseSchema: z.ZodType = z
.object({
policies: z.array(z.lazy(() => marshalPolicyInfoSchema)).optional(),
nextPageToken: z.string().optional(),
})
.transform(d => ({
policies: d.policies,
next_page_token: d.nextPageToken,
}));

export const marshalMatchColumnSchema: z.ZodType = z
.object({
condition: z.string().optional(),
Expand Down Expand Up @@ -603,3 +591,161 @@ export const marshalTagValueExtractionSchema: z.ZodType = z
.transform(d => ({
tag_key: d.tagKey,
}));

const columnMaskOptionsFieldMaskSchema: FieldMaskSchema = {
functionName: {wire: 'function_name'},
onColumn: {wire: 'on_column'},
using: {wire: 'using'},
};

export function columnMaskOptionsFieldMask(
...paths: string[]
): FieldMask<ColumnMaskOptions> {
return FieldMask.build<ColumnMaskOptions>(
paths,
columnMaskOptionsFieldMaskSchema
);
}

const columnTagValueExtractionFieldMaskSchema: FieldMaskSchema = {
columnAlias: {wire: 'column_alias'},
tagKey: {wire: 'tag_key'},
};

export function columnTagValueExtractionFieldMask(
...paths: string[]
): FieldMask<ColumnTagValueExtraction> {
return FieldMask.build<ColumnTagValueExtraction>(
paths,
columnTagValueExtractionFieldMaskSchema
);
}

const denyOptionsFieldMaskSchema: FieldMaskSchema = {
privileges: {wire: 'privileges'},
};

export function denyOptionsFieldMask(
...paths: string[]
): FieldMask<DenyOptions> {
return FieldMask.build<DenyOptions>(paths, denyOptionsFieldMaskSchema);
}

const functionArgumentFieldMaskSchema: FieldMaskSchema = {
alias: {wire: 'alias'},
constant: {wire: 'constant'},
metadataExtraction: {
wire: 'metadata_extraction',
children: () => metadataExtractionExpressionFieldMaskSchema,
},
};

export function functionArgumentFieldMask(
...paths: string[]
): FieldMask<FunctionArgument> {
return FieldMask.build<FunctionArgument>(
paths,
functionArgumentFieldMaskSchema
);
}

const grantOptionsFieldMaskSchema: FieldMaskSchema = {
privileges: {wire: 'privileges'},
};

export function grantOptionsFieldMask(
...paths: string[]
): FieldMask<GrantOptions> {
return FieldMask.build<GrantOptions>(paths, grantOptionsFieldMaskSchema);
}

const matchColumnFieldMaskSchema: FieldMaskSchema = {
alias: {wire: 'alias'},
condition: {wire: 'condition'},
};

export function matchColumnFieldMask(
...paths: string[]
): FieldMask<MatchColumn> {
return FieldMask.build<MatchColumn>(paths, matchColumnFieldMaskSchema);
}

const metadataExtractionExpressionFieldMaskSchema: FieldMaskSchema = {
columnTagValue: {
wire: 'column_tag_value',
children: () => columnTagValueExtractionFieldMaskSchema,
},
tagValue: {
wire: 'tag_value',
children: () => tagValueExtractionFieldMaskSchema,
},
};

export function metadataExtractionExpressionFieldMask(
...paths: string[]
): FieldMask<MetadataExtractionExpression> {
return FieldMask.build<MetadataExtractionExpression>(
paths,
metadataExtractionExpressionFieldMaskSchema
);
}

const policyInfoFieldMaskSchema: FieldMaskSchema = {
columnMask: {
wire: 'column_mask',
children: () => columnMaskOptionsFieldMaskSchema,
},
comment: {wire: 'comment'},
createdAt: {wire: 'created_at'},
createdBy: {wire: 'created_by'},
deny: {wire: 'deny', children: () => denyOptionsFieldMaskSchema},
exceptPrincipals: {wire: 'except_principals'},
forSecurableType: {wire: 'for_securable_type'},
grant: {wire: 'grant', children: () => grantOptionsFieldMaskSchema},
id: {wire: 'id'},
matchColumns: {wire: 'match_columns'},
name: {wire: 'name'},
onSecurableFullname: {wire: 'on_securable_fullname'},
onSecurableType: {wire: 'on_securable_type'},
policyType: {wire: 'policy_type'},
rowFilter: {
wire: 'row_filter',
children: () => rowFilterOptionsFieldMaskSchema,
},
toPrincipals: {wire: 'to_principals'},
updatedAt: {wire: 'updated_at'},
updatedBy: {wire: 'updated_by'},
useSessionIdentity: {wire: 'use_session_identity'},
whenCondition: {wire: 'when_condition'},
};

export function policyInfoFieldMask(...paths: string[]): FieldMask<PolicyInfo> {
return FieldMask.build<PolicyInfo>(paths, policyInfoFieldMaskSchema);
}

const rowFilterOptionsFieldMaskSchema: FieldMaskSchema = {
functionName: {wire: 'function_name'},
using: {wire: 'using'},
};

export function rowFilterOptionsFieldMask(
...paths: string[]
): FieldMask<RowFilterOptions> {
return FieldMask.build<RowFilterOptions>(
paths,
rowFilterOptionsFieldMaskSchema
);
}

const tagValueExtractionFieldMaskSchema: FieldMaskSchema = {
tagKey: {wire: 'tag_key'},
};

export function tagValueExtractionFieldMask(
...paths: string[]
): FieldMask<TagValueExtraction> {
return FieldMask.build<TagValueExtraction>(
paths,
tagValueExtractionFieldMaskSchema
);
}
Loading
Loading