Skip to content

Add a converter for Alibaba Cloud Hologres Semantic View - #320

Open
TimothyHologres wants to merge 13 commits into
apache:mainfrom
aliyun:add_hologres_support
Open

Add a converter for Alibaba Cloud Hologres Semantic View#320
TimothyHologres wants to merge 13 commits into
apache:mainfrom
aliyun:add_hologres_support

Conversation

@TimothyHologres

Copy link
Copy Markdown

Summary

Adds converters/hologres/, a bidirectional converter between Apache Ossie semantic models and Alibaba Cloud Hologres Semantic View (available since Hologres V5.0.0).

A Hologres Semantic View is an in-database object declaring physical tables, their relationships, business dimensions and metrics. Queries reference dimensions and AGG(metric) rather than repeating joins and aggregations, and the engine aggregates each metric within its own minimal join subtree so a one-to-many join cannot inflate a total.

The two directions are asymmetric, because of how Hologres exposes definitions:

Direction Input Output
Export (Ossie → Hologres) Ossie YAML CREATE SEMANTIC VIEW SQL DDL
Import (Hologres → Ossie) model_yaml from hologres.hg_semantic_view_properties Ossie YAML

Export emits DDL because Hologres has no YAML import function (hg_create_semantic_view_from_yaml() does not exist); the documentation directs cross-environment rebuilds to the original ddl_text. Emitting the model_yaml shape instead would be symmetrical with the other spokes but produce a file Hologres cannot consume.

This PR contains no specification changes. An earlier revision proposed adding a HOLOGRES token to the Dialect enum; that was dropped after the motivation failed to hold up — see #319 for the reasoning. Everything is read and written as ANSI_SQL. The HOLOGRES vendor name used for the custom_extensions stash needs no spec change, since Vendor is free-form.

Scope: converters/hologres/**, a path-filtered CI workflow, and one row each in converters/README.md and ROADMAP.md.

Mapping

Ossie Hologres Semantic View
dataset + source + primary_key TABLES (alias AS schema.table PRIMARY KEY (...))
relationship (from many → to one) RELATIONSHIPS (name AS from(cols) REFERENCES to(cols))
dataset.fields[] DIMENSIONS (alias.name AS expr)
model-level metrics[] METRICS (owner.name AS agg_expr)
description / string ai_context COMMENT

Three mappings do real work:

  • Dataset name ⇄ table alias. The alias is what every dimension, metric and relationship references, so using it as the dataset name keeps those references valid with no rewriting, and makes Hologres' own sum(o.amount) already correct as a dataset-qualified Ossie metric expression.
  • Column qualification. Ossie field expressions are unqualified bare columns while Hologres requires alias.column, so columns are qualified through the sqlglot AST. A "prefix only bare identifiers" shortcut would leave c_first_name || ' ' || c_last_name — which is in this repo's own TPC-DS example — half-qualified and unresolvable.
  • Metric ownership. Ossie metrics are model-level; Hologres namespaces each under the table it aggregates. The owner is inferred from the expression's column references. COUNT(*) has none, so it requires an explicit owner and fails closed without one: guessing would silently change the number under a fan-out join.

One open question for reviewers

Whether a converter's export direction may emit SQL DDL rather than a data document. This would be the first such converter in the repo. I checked whether the CLI plugin contract constrains it and it appears not to — convert.from_ossie declares only invoke, with no output-format field, while Accepts is documented as "populated on ToOssie only" (cli/internal/plugin/plugin.go:26,44-46). Caveats and alternatives are discussed in #319. Since Snowflake Semantic Views are also created through DDL, this likely sets a precedent, so I am happy to follow the community's preference.

Limitations, all imposed by Hologres

The converter fails closed and names the offending field rather than emitting DDL the server will reject:

  • definition expressions must be row-level over a single table
  • aggregates limited to count / sum / avg / min / max
  • no derived, ratio or filtered metrics
  • a REFERENCES target must be the target table's primary key
  • dimension and metric names must be unique view-wide (queries reference them bare), so a field-name collision across datasets is rejected rather than silently renamed
  • no CREATE OR REPLACE / ALTER SEMANTIC VIEW; --drop-if-exists recreates

Using this repository's examples/tpcds_semantic_model.yaml, 2 of its 5 metrics (customer_lifetime_value, store_productivity) are cross-dataset ratios with no Semantic View form. The default is to fail naming them; --skip-unsupported-metrics converts the rest.

One limitation is worth flagging because it is not in the Hologres documentation: a bare top-level operator in a definition is a syntax error. a || b, a + 1 and a::text are all rejected in a DIMENSIONS clause, while (a || b), (a + 1) and cast(a as text) are accepted — even though the same operators are fine inside a function call's argument list. This was found by executing the generated TPC-DS DDL against a live instance. The exporter adds the required parentheses; the importer strips them back off.

Verification

Beyond the unit tests, the behaviour was verified end to end against a real Hologres 5.0.0 instance:

  • the generated DDL executes, for both the star fixture and the 5-table TPC-DS snowflake (composite primary key, 4 relationships, computed dimension)
  • the resulting views return correct results, including the fan-out case: with several order_items rows per order, revenue stays 300/200 rather than being multiplied, and one city's credit total includes a customer with no orders at all
  • importing Hologres' own readback of the exported DDL reproduces the original Ossie model exactly

The checked-in fixtures are therefore not hand-written: each *_semantic_view.sql was executed on that instance and each *_model_yaml.yaml is the instance's readback of it. A live test re-checks the readback against the committed fixture, so if Hologres changes its output shape the suite says so rather than quietly testing a stale format.

Those live tests are skipped unless HOLOGRES_* environment variables are set, and the database driver sits in a non-default dependency group, so CI stays hermetic. No credentials are in the repository.

Disclosure

This implementation was produced with AI assistance. In line with the ASF Generative Tooling Guidance, I have reviewed the code and take full responsibility for it; the behaviour described above was verified by me against a live instance.

Related Issues

Relates to #319

Checklist

Specification

  • Spec changes are included in core-spec/ and follow the existing structure — N/A: this PR makes no spec changes (an earlier revision did; it was dropped, see Add a converter for Alibaba Cloud Hologres Semantic View #319)
  • Spec changes have been discussed on the mailing list or in a linked issue — N/A
  • Breaking changes to the spec are clearly called out in the summary — N/A, none

Ontology

  • Ontology changes in ontology/ are consistent with spec changes — N/A, none
  • New or modified terms are defined and documented — N/A

Converters

  • Converter logic in converters/ is updated to reflect spec or ontology changes
  • New converters include tests under the converter's test directory

Validation

  • Validation rules in validation/ are updated if the spec changed — N/A, spec unchanged
  • New validation cases are covered by tests — the imported output is checked against validation/validate.py in a subprocess

Documentation

  • docs/ is updated to reflect any user-facing changes — N/A: no spec or CLI change outside this converter; converters/hologres/README.md, converters/README.md and ROADMAP.md are updated
  • New features or behaviors are documented with examples where appropriate
  • CONTRIBUTING.md is updated if the contribution process changed — N/A

Examples

  • examples/ are added or updated for any new spec constructs or converter support — N/A: no new spec constructs. The tests read the existing examples/tpcds_semantic_model.yaml directly rather than copying it, so they cannot drift from it; the CI path filter watches that file for the same reason.

Tests

  • All existing tests pass (pytest / CI green) — 256 offline tests pass; the 9 live tests pass against a Hologres 5.0.0 instance and skip without credentials
  • New functionality is covered by tests

Compliance

  • ASF license headers are present on all new source files
  • No third-party dependencies are added without PMC/IPMC approval — please confirm. sqlglot and jsonschema are already used by validation/ and converters/gsf. psycopg[binary] is new to the repo; it is used only by the live tests and lives in a live dependency group excluded from default-groups, so CI and a normal uv sync never install it. Happy to drop the live tests, or gate them differently, if adding it is not acceptable.

Hologres V5.0.0+ supports CREATE SEMANTIC VIEW, so an Ossie converter for it
needs a way to label Hologres expressions. Without a dedicated token,
PostgreSQL-specific syntax such as `col::text` would have to be mislabelled as
ANSI_SQL, which misleads consumers about portability.

Hologres is PostgreSQL wire- and dialect-compatible, so the validator maps the
new token to sqlglot's postgres dialect and keeps SQL parsing enabled rather
than skipping validation.
Sets up the converter sub-project and the cross-cutting helpers both directions
need: the custom_extensions stash protocol, YAML 1.2 loading, SQL quoting, and a
sqlglot expression layer.

Two details are worth calling out because they are easy to get wrong:

- sqlglot does not quote reserved words on output (it emits a bare `select` for
  the identifier `select`, a syntax error), so quote_identifier carries its own
  reserved-word set. It also quotes anything not already lower case, since
  PostgreSQL folds unquoted identifiers.
- Metric aggregates are matched by exact sqlglot node type rather than
  isinstance(node, AggFunc): `stddev` and `array_agg` are AggFuncs too, but
  Hologres only accepts count/sum/avg/min/max.

The live dependency group holds the database driver and is excluded from
default-groups so CI never installs a driver it cannot use.
Emits SQL DDL rather than YAML because Hologres has no YAML import function --
the DDL is the only way to create a Semantic View.

The conversion is fail-closed: Hologres enforces single-table definitions, a
five-function aggregate whitelist, and REFERENCES targets that must be the
referenced table's primary key. Each of those is checked offline with the
offending field named, rather than emitting DDL the server will reject.

Two mappings do real work. Ossie field expressions are unqualified bare columns
while Hologres needs alias.column, so columns are qualified through the sqlglot
AST -- a "prefix only bare identifiers" shortcut would leave `first || last`
half-qualified and unresolvable. And Ossie metrics are model-level while Hologres
namespaces them under an owning alias, so the owner is inferred from the
expression's column references; `count(*)` has none, so it needs an explicit
owner and fails closed without one, since guessing would silently change the
number under a fan-out join.

Also fixes reserved-word quoting inside expressions: sqlglot renders the
identifier `user` bare, so a column named `user` on an alias named `order`
produced the invalid `order.user`.

Both golden .sql fixtures were executed against a real Hologres 5.0.0 instance
and the resulting views queried, including the fan-out case where order revenue
must not be inflated by joined order_items rows.
Consumes the model_yaml that Hologres publishes in
hologres.hg_semantic_view_properties, rather than the ddl_text beside it, because
it is already structured and needs no SQL statement parser.

The Hologres table alias becomes the Ossie dataset name. That keeps every
dimension, metric and relationship reference valid without rewriting, and makes
Hologres' own `sum(o.amount)` already correct as a dataset-qualified Ossie metric
expression.

Expressions are labelled ANSI_SQL unless they actually need PostgreSQL syntax,
decided by checking whether sqlglot renders the AST identically outside the
postgres dialect. Labelling an ordinary SUM(x) as HOLOGRES would hide it from
every other Ossie converter looking for a portable expression; `j -> 'k'` and the
1-based `arr[1]` genuinely do differ and are labelled HOLOGRES.

Both model_yaml fixtures were read back from a real Hologres 5.0.0 instance after
executing the exported DDL, so the full loop is verified end to end: importing
them reproduces the original Ossie fixtures exactly, and the output passes the
repository's own validation/validate.py.
Wires both directions up to a command line, following the export/import
subcommand shape the other converters use.

Two options exist because of Hologres constraints rather than preference:
--drop-if-exists, since there is no CREATE OR REPLACE or ALTER SEMANTIC VIEW and
a definition can only be changed by recreating it; and --metric-owner, to supply
the owning table for a COUNT(*) metric whose expression names no column.
Also fixes a real grammar bug found by running the TPC-DS DDL against the
instance: Hologres rejects a bare top-level operator in a definition expression.
`a || b`, `a + 1` and `a::text` are all syntax errors in a DIMENSIONS clause,
while `(a || b)`, `(a + 1)` and `cast(a as text)` are accepted -- even though the
same operators are fine inside a function call's argument list. The manual's
"operators are supported" does not mention this, so the exporter now adds the
required parentheses and the importer strips them back off, keeping round trips
from accumulating wrapping.

The canonical examples/tpcds_semantic_model.yaml is referenced directly rather
than copied into a fixture, so these tests cannot drift from it. Two of its five
metrics are structurally inexpressible -- customer_lifetime_value and
store_productivity are ratios spanning two datasets -- so the default is to fail
closed naming the metric, with --skip-unsupported-metrics to convert the rest.

The resulting 5-table snowflake was created on a real Hologres 5.0.0 instance and
queried across all four dimension tables, including the computed
customer_full_name dimension that needs both full qualification and the
parentheses above.
A CREATE SEMANTIC VIEW statement can only really be validated by a Hologres
server, so these tests create the view, query it, and read the model back. They
prove three things the offline tests cannot: the generated DDL is accepted, the
resulting view answers queries with the right numbers, and importing Hologres'
own readback reproduces the model we started from.

The fan-out case is checked with real rows rather than just parsed: each order
has several order_items rows, and order revenue must stay 300/200 rather than
being multiplied by them. Beijing's credit of 2500 likewise includes a customer
with no orders at all, which is what aggregating before joining buys.

Skipped unless HOLOGRES_HOST/USER/PASSWORD/DB are set, and the driver lives in a
non-default dependency group, so CI stays hermetic. Credentials are read only
from the environment and none are stored in the repository. Everything is created
inside an ossie_hologres_it schema that is dropped on teardown.

Also renames the fixtures' placeholder database from a real instance's name to
`retail`, so no internal identifier ships upstream.
Mirrors the sibling converter workflows, reusing their pinned action SHAs and the
3.11-3.14 matrix.

The path filter additionally watches examples/tpcds_semantic_model.yaml,
validation/validate.py and core-spec/osi-schema.json. This converter's tests read
those three files directly instead of copying them, so without listing them a
change to the canonical example could break the golden fixture without CI noticing
on the PR that caused it.

`uv sync` deliberately omits the live group: those tests need a Hologres instance
and skip themselves without one.
Records the mapping, and separates limitations that come from Hologres from
choices the converter makes. The limitations section is the part worth reading
before using this: single-table definitions, the five-function aggregate
whitelist, no derived or ratio metrics, and REFERENCES targets that must be a
primary key.

Also notes what the fixtures actually are -- DDL executed against a real instance
and that instance's own readback -- so a future maintainer knows they are verified
artifacts rather than hand-written guesses, and that a live test guards them
against Hologres changing its output shape.
Follow-up to adding the HOLOGRES token to the Dialect enum. Two places described
the available dialects but not this one.

expression_language.md is where someone writing a dialect-specific expression
looks, and its Common Dialect Variations table already carries a PostgreSQL
column, so the note points HOLOGRES at that column and names the cases that
actually justify the tag rather than ANSI_SQL.

docs/index.md listed the dialects as if exhaustively but was already missing
BIGQUERY and MAQL, so the list is now completed against the schema enum instead of
having HOLOGRES appended to a wrong list.
Adds it to the Existing Artifacts list under Tooling & Ecosystem Support, noting
the asymmetry that distinguishes it from the other spokes: export emits
CREATE SEMANTIC VIEW DDL because Hologres has no YAML import function, and import
consumes the model_yaml Hologres publishes for every Semantic View.
Drops the per-expression dialect decision. Every expression is now labelled
ANSI_SQL, which removes this converter's need for a vendor dialect token.

The original motivation was to label `x::text` honestly, but that does not hold up:
the portable spelling is always available and sqlglot already normalizes to
`CAST(x AS TEXT)` in both directions, so `::text` never reached the output.

The per-expression test that remained was also unsound. It compared sqlglot's
postgres rendering against its default rendering, but sqlglot's default dialect is
not ANSI SQL, so it passed `ILIKE` as portable while flagging the standard
`SUBSTRING`, `EXTRACT` and `DATE_TRUNC` as vendor-specific.

And over-labelling turned out to be the more damaging error: a converter that
looks for an ANSI_SQL expression and finds none drops the field (see
converters/databricks/ossie_to_metric_view.py), so tagging a `DATE_TRUNC`
dimension as vendor-specific loses it silently, whereas an optimistic label at
worst surfaces as a SQL error on the target engine.

PostgreSQL-only syntax such as `j -> 'k'` is therefore labelled slightly
optimistically, which the README states plainly. This matches the NVIDIA GSF
converter's reasoning that anything else stays ANSI_SQL rather than being labelled
inaccurately.
This reverts commit cdfa891 and its follow-up documentation in b76db78,
restoring core-spec/, python/, validation/ and docs/ to their previous state.

The converter no longer needs the token. Its stated motivation -- labelling
`x::text` honestly -- does not survive scrutiny: Hologres is PostgreSQL-compatible
but the portable `CAST(x AS TEXT)` spelling is always available, and sqlglot
normalizes to it, so the shorthand never reached the output in the first place.

Removing it keeps the contribution out of the specification-change process
(7-day discussion, three binding +1) for a token that would have been used only
for occasional PostgreSQL-only operators, and where over-labelling costs more than
it saves: a converter that finds no ANSI_SQL expression drops the field.

The Vendor name HOLOGRES used by the custom_extensions stash is unaffected --
Ossie's Vendor field is free-form and needs no spec change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants