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
151 changes: 138 additions & 13 deletions toolbox/mdcode/docs/semantic-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ model to two destinations at once:
Both are generated from the same source document — you never author them
separately, and a single `push` keeps them in sync.

This guide covers authoring, deploying, and updating a model. For the Ossie
document format itself, see [ossie.apache.org](https://ossie.apache.org/).
This guide covers authoring, deploying, pulling back, and updating a model.
For the Ossie document format itself, see
[ossie.apache.org](https://ossie.apache.org/).

## Prerequisites

Expand Down Expand Up @@ -137,6 +138,12 @@ statement runs in the right region; without that permission it falls back to
BigQuery's own location inference and warns. Nothing runs under
`--validate-only`; add `--print` to see the DDL.

Push to BigQuery is **lossy**: the graph captures the queryable structure —
node tables, edge tables, and measures — but not descriptive metadata
(descriptions, `ai_context`, synonyms, labels), and a metric that does not
reduce to a single MEASURE is dropped with a warning. It is a query surface,
not a copy of your model.

### What gets created in Knowledge Catalog

Each element of your model maps to one catalog resource. Every resource type
Expand All @@ -154,23 +161,35 @@ An entity entry carries its columns in the `schema` aspect (name, data type, and
description per field); a `schema-join` link carries the relationship detail — the
paired columns and foreign-key direction — in its aspect.

> **Note — the catalog is not a full copy of your model.** By default the SQL
> expressions are **not** written to Knowledge Catalog: the published system-type
> templates do not yet carry a per-field `semantics` block or a
> `semantic-metric.expression` field, so the default push omits them (pass
> `--emit-expressions` to write them once the templates gain the fields). The
> original vendor SQL (`importedExpression` — e.g. the MAQL or Snowflake form a
> metric was imported from) is never written either. All of it stays in your
> authored document and is still used when generating BigQuery SQL. Keep your
> model document as the source of truth: a model reconstructed only from the
> catalog would come back without its SQL.
> **Note — push to Knowledge Catalog is lossy.** The catalog holds metadata,
> not a full copy of your model. It **stores** names, descriptions, data
> sources, field datatypes and roles, and 1:1 / 1:N relationships (as
> `schema-join` links). By default it does **not** store the SQL expressions:
> the published system-type templates do not yet carry a per-field `semantics`
> block or a `semantic-metric.expression` field, so the default push omits them
> (pass `--emit-expressions` to write the canonical GoogleSQL/ANSI expression
> once the templates gain the fields). It never stores entity keys, `ai_context`,
> field labels, the original vendor SQL (`importedExpression` — e.g. the MAQL or
> Snowflake form a metric was imported from), or M:N relationships. Those stay in
> your authored document (and, for the edges, in the BigQuery property graph); the
> vendor SQL and expressions are still used when generating BigQuery SQL. Keep
> your model document as the source of truth.

## Validation

`push` and `--validate-only` run the same checks, **before either destination is
touched**, so a model that cannot deploy fails fast instead of half-deploying:

* **Exactly one deployment target per model.** *(static)*
* **Exactly one deployment target per model, and it must be a valid BigQuery
Graph URI.** A model with no target — or with more than one — is rejected, and
so is a single target whose URI does not match
`//bigquery.googleapis.com/projects/<p>/datasets/<d>/propertyGraphs/<g>` (for
example a `propertyGraph`/`propertyGraphs` typo, or a
`…/entryGroups/@bigquery/entries/…` entry form). The error names the offending
URI and the expected form. This gate runs before any destination leg and for
every `--target`, so a malformed target writes **nothing** — not to BigQuery
and **not to Knowledge Catalog**; the push aborts with a non-zero exit and no
entries are created. *(static)*
* **Every metric on a BigQuery Graph model resolves to exactly one entity** —
otherwise it would be dropped from the BigQuery Graph. Set the metric's attach
entity, or scope its expression to a single entity. *(static)*
Expand Down Expand Up @@ -220,3 +239,109 @@ Every push prints one line per destination summarizing what it did. For a
Deployed 1 BigQuery Graph(s).
Wrote 5 new and 2 updated Knowledge Catalog entries; removed 1 orphaned entry; linked 2 relationships; unlinked 1 orphaned link.
```

## Pull

`kcmd pull` is the inverse of push's Knowledge Catalog leg: it reads the
`semantic-*` entries back from the catalog and reconstructs local model
documents at `catalog/EntryGroups/<entryGroupId>/<model>.yaml`. Use it to
recover a workspace from a catalog someone else deployed, or to see what the
catalog actually holds.

```bash
kcmd pull
```

Pull reads only from Knowledge Catalog (never BigQuery). Its coordinates come
from the same scope you authored under (`<projectId>.<locationId>.<entryGroupId>`).

| Flag | Effect |
|------|--------|
| `--dry-run` | Reconstruct from the catalog and report what would be written, but write no files. |
| `--model <name>` | Pull a single model by name; other models in the entry group are left alone. |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

for now we assume there's only on model in the entry group. We don't need --model flag


One entry group can hold **many models** — each `semantic-model` entry is a
separate anchor, and pull reconstructs one document per anchor. `--model`
narrows both the fetch and the write to a single anchor.

Pull writes with the same last-write-wins policy as the core pull: a model that
already exists locally is overwritten in place, and a local-only document (one
with no matching catalog entry) is left untouched — pull never deletes.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

"pull never deletes" --- can we fail pull by default if the id of the model available in the entryGroup that we're pulling from is different from the id of the model available locally on disk? if user wants to proceed then they'll need to explicitly use the new --force-remove flag we already support in push. If force-remove is specified then pull will erase the existing model and will pull the model from the KC.


> **Note — pull reconstructs what the catalog holds, not your original file.**
> Pull can only recover what push wrote (see the note under [What gets created in
> Knowledge Catalog](#what-gets-created-in-knowledge-catalog)). What that means in
> practice:
>
> **Recovered exactly** — these come back as authored:
> - Model structure: the model, its entities, and each entity's fields.
> - Field data source and data type.
> - Metrics: name, data type, and attach entity.
> - 1:1 / 1:N relationships: endpoints, foreign-key direction, and join columns
> (from the `schema-join` links).
> - Deployment targets.
>
> **Recovered only if pushed with `--emit-expressions`** — the per-field
> `semantics` block (expressions and the dimension role) and the metric
> expression are omitted from the catalog by default (see the note above), so
> pull returns them only when the push that wrote them used `--emit-expressions`:
> - Field expressions and metric expressions (the canonical GoogleSQL/ANSI form).
> - A field's dimension role, which comes back as a bare `dimension: {}` marker,
> without its detail (`is_time`, and so on). A default push drops the marker
> entirely.
>
> **Recovered, but normalized** — the content survives, the form changes:
> - Relationship *names* come back lowercased/hyphenated (the catalog stores the

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

we should also mention the order of the items emitted by pull. Field order within each entity will be preserved, but the order of datasets, metrics won't be. Let's also say that any comments in the original Yaml file are not preserved either.

> name only in the link id, e.g. `Places Order` → `places-order`).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

if we change relationship name on push then kcmd push should warn user about name change.

> - A metric authored with no data type comes back as an explicit `Decimal`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think we should use 'Opaque' type instead (for both fields and metrics) if type is not provided by the user.

> (push must write a type, and defaults it to `NUMERIC`).
>
> **Not recovered** — push never wrote these, so pull cannot return them:
> - Entity keys / unique keys.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

pk/unique keys should be persisted in the schema aspect. let's add their support in a follow up PR.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agreed — tracked as a follow-up (persist pk/unique keys in the schema aspect). Listed under "Deferred" in #298; not in this follow-up.

> - `ai_context`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this should go to the guidelines aspect. see design doc.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agreed — tracked as a follow-up (ai_context → guidelines aspect, per the design doc). Listed under "Deferred" in #298.

> - Field labels.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

schema aspect support 'annotations' map for a field. perhaps we should put label there: { <label>: "<label>" }

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agreed — tracked as a follow-up (field label → schema aspect annotations map, { <label>: "<label>" }). Listed under "Deferred" in #298.

> - The original vendor SQL (`importedExpression`).
> - M:N relationships (the edge lives only in the BigQuery property graph).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit. let's remove M:N, since Ossie doesn't have those yet

>
> **So: a push followed by a pull does not return your original file.** Treat a
> pulled document as a faithful copy of the catalog metadata, not of the authored
> model, and keep the authored document as the source of truth.

> **Note — writer-side follow-up (not inherent to pull).** One reduction above is
> a limit of what push currently *writes*, not of what pull can recover. It is
> recorded here as a write-side follow-up; the reader (pull) already returns
> everything the catalog holds.
>
> - **Relationship names.** The `schema-join` aspect type's `metadataTemplate` has
> no field for the relationship name, so push cannot store it and pull recovers
> it from the link id — which is lowercased and hyphenated (the entry-link id
> format forbids the original casing/underscores). Returning the name verbatim
> requires adding a name field to the built-in `schema-join` aspect type in
> Knowledge Catalog (server-side), after which the client write/read is trivial;
> it is the same class of gap as the `semantics` field that gates
> `--emit-expressions`.
>
> (A non-canonical deployment target is **not** a pull gap: push rejects it at the
> validation gate before any leg runs, so it is never written — see
> [Validation](#validation).)

## Permissions

`push` needs access to whichever destinations you deploy to.

**BigQuery** — for `--target bq` or `all`, and for the validation pre-flight:

* `bigquery.jobs.create` in the deployment-target project — to run the deploy's
`CREATE OR REPLACE PROPERTY GRAPH` and the validation dry-run query
* read access on each entity's source table, so the dry-run can resolve it
* `bigquery.datasets.get` on the target dataset (region detection; optional —
push degrades gracefully without it)

**Knowledge Catalog / Dataplex** — for `--target kc` or `all`:

* `dataplex.entryGroups.useSemanticModelAspect` on the destination entry group

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think dataplex.entryGroups.useSemanticEntityAspect and dataplex.entryGroups.useSemanticMetricAspect will also be required.

* `dataplex.entryGroups.useSchemaJoinEntryLink` and
`dataplex.entryGroups.useSchemaJoinAspect` when the model has relationships

`kcmd pull` needs read access to the same entry group instead — to list its
entries and fetch each `semantic-*` entry with its aspects.
30 changes: 30 additions & 0 deletions toolbox/mdcode/src/libts/layouts/semantic-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,36 @@ export class SemanticModelLayout implements CatalogLayout {
return docs;
}

// True when a model document with this handle already exists on disk. `pull`
// uses it to report which files it would overwrite vs. create.
hasModel(name: string): boolean {
return fs.existsSync(this.modelPath(name));
}

// The absolute path a model document with this handle maps to:
// `<catalog>/EntryGroups/<entryGroup>/<name>.yaml`. Path separators in the
// model name are replaced so a name still yields a single flat file. Requires
// the layout to be scoped to an entry group (the semantic-model source always
// is).
modelPath(name: string): string {
if (!this._entryGroup) {
throw new Error(
'SemanticModel layout has no entry group; cannot resolve a model path.');
}
const file = `${name.replace(/[/\\]/g, '_')}.yaml`;
return path.join(this._catalogPath, 'EntryGroups', this._entryGroup, file);
}

// Writes a model's serialized document to its path, creating the EntryGroup
// directory if needed, and indexes it so a later modelDocuments() sees it.
// This is the sink `pull` writes reconstructed models to.
writeModelDocument(name: string, text: string): void {
const localPath = this.modelPath(name);
fs.mkdirSync(path.dirname(localPath), {recursive: true});
fs.writeFileSync(localPath, text);
this._index.set(name, localPath);
}

// The Knowledge Catalog entry-level members are not applicable to this
// push-only layout; the model is authored as a single Ossie document, not as
// per-entry Knowledge Catalog files. These are wired when KC-resource emit
Expand Down
Loading