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
4 changes: 4 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ converter/ontology:
- changed-files:
- any-glob-to-any-file: 'converters/ontology/**'

converter/holistics:
- changed-files:
- any-glob-to-any-file: 'converters/holistics/**'

# Other areas
cli:
- changed-files:
Expand Down
94 changes: 94 additions & 0 deletions .github/workflows/converter-holistics-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#

name: Converters Holistics CI
on:
push:
branches: [ "main" ]
paths:
- 'converters/holistics/**'
# core-spec/** is the convention every sibling workflow adopted in #396.
# examples/** is load-bearing here rather than merely conventional: the
# reverse-path snapshot converts examples/tpcds_semantic_model.yaml, so an
# edit there fails this converter's tests without touching a converter file.
- 'core-spec/**'
- 'examples/**'
- '.github/workflows/converter-holistics-ci.yml'
pull_request:
branches: [ "main" ]
paths:
- 'converters/holistics/**'
- 'core-spec/**'
- 'examples/**'
- '.github/workflows/converter-holistics-ci.yml'

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- name: Checkout project
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}

- name: Install uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: "0.12.19"

- name: Set up Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: "22"

- name: Install the Holistics CLI
# The tests that run `holistics aml compile` and `holistics aml validate`
# skip without the CLI. @holistics/cli-core is the CLI's logic as one
# bundled JavaScript file, with no native binary and no platform
# restriction, so it runs on the runner as it does on a developer's
# machine. @commander-js/extra-typings is imported by its `bin` script
# and missing from its dependencies, so it is installed alongside.
#
# Pinned: 2.3.32 is the first version whose compiler accepts the
# composite relationship in the ecommerce fixture.
run: |
npm install --no-package-lock --prefix "$RUNNER_TEMP/holistics" \
@holistics/cli-core@2.3.32 @commander-js/extra-typings@14.0.0
echo "$RUNNER_TEMP/holistics/node_modules/.bin" >> "$GITHUB_PATH"

- name: Sync dependencies
working-directory: converters/holistics
run: |
uv sync

- name: Unit Tests
working-directory: converters/holistics
run: |
uv run pytest
1 change: 1 addition & 0 deletions converters/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ The Ossie specification currently defines extensions for the following vendors:
| `THOUGHTSPOT` | ThoughtSpot TML (Model + Table/SQL View) |
| `CUBE` | Cube data model |
| `HEX` | Hex semantic project |
| `HOLISTICS` | Holistics AML dataset |

Each vendor may define custom extensions (via the `custom_extensions` field in the Ossie spec) to carry vendor-specific metadata that does not have an equivalent in the core specification.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
OssieDialect.SIGMA: None,
OssieDialect.THOUGHTSPOT: None,
OssieDialect.DAX: None,
OssieDialect.HOLISTICS_AQL: None,
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ def _to_sqlglot_dialect(ossie_dialect: OssieDialect) -> SQLGlotDialect | None:
OssieDialect.THOUGHTSPOT,
OssieDialect.SIGMA,
OssieDialect.DAX,
OssieDialect.HOLISTICS_AQL,
):
# There's no parallel for these
read = None
Expand Down
2 changes: 1 addition & 1 deletion converters/hex/tests/cli/test_cli_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,5 +225,5 @@ def test_invalid_dialect(capsys: pytest.CaptureFixture[str], tmp_path: Path) ->
message = message + separator + choices.replace("'", "")
assert message == snapshot("""\
usage: ossie-hex export [-h] -i INPUT [-o OUTPUT] [-d DIALECT] [-v]
ossie-hex export: error: argument -d/--dialect: invalid choice: 'invalid' (choose from ansi_sql, snowflake, mdx, maql, tableau, databricks, bigquery, sigma, thoughtspot, dax, ossie_sql_2026)
ossie-hex export: error: argument -d/--dialect: invalid choice: 'invalid' (choose from ansi_sql, snowflake, mdx, maql, tableau, databricks, bigquery, sigma, thoughtspot, dax, holistics_aql, ossie_sql_2026)
""")
28 changes: 28 additions & 0 deletions converters/holistics/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

__pycache__/
*.py[cod]
.pytest_cache/
*.egg-info/
dist/
build/
.venv/
venv/

# Rendered previews
*.html
177 changes: 177 additions & 0 deletions converters/holistics/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License. -->

# Apache Ossie Holistics converter

Converts between a [Holistics](https://docs.holistics.io) AML dataset and an
[Apache Ossie](https://github.com/apache/ossie) semantic model.

- `to-ossie` reads the JSON `holistics aml compile` produces, and writes one Ossie semantic model.
- `to-aml` reads one Ossie semantic model, and writes AML source files.

One AML `Dataset` becomes one Ossie semantic model.

| AML | Ossie |
|---|---|
| the `Dataset` | the document root |
| each model | a `datasets[]` entry |
| each model `dimension` | a `fields[]` entry on that dataset |
| each model `measure` | a `metrics[]` entry named `<dataset>__<measure>` |
| each dataset-level `dimension` | a `fields[]` entry on the model it names |
| each dataset-level `metric` | a `metrics[]` entry |
| each `relationship` | a `relationships[]` entry |

[Mapping](docs/mapping.md) covers data types, relationship kinds and the rest.

AQL travels under the `HOLISTICS_AQL` dialect, registered upstream the way `THOUGHTSPOT` was in
[#351](https://github.com/apache/ossie/pull/351). The converter does not translate AQL into SQL, and does not
rewrite warehouse SQL from one dialect into another. [Limitations](docs/limitations.md) lists what that costs.

## The two SQLs

Both sides write SQL, and the two are not the same text.

- **Holistics SQL** is an AML `@sql` body. It interpolates through `{{ ... }}`, and it resolves inside the one
model it sits in.
- **Ossie SQL** is the `expression` of a `dialects[]` entry. It writes a reference as `dataset.field`, and its
dialect tag names the warehouse it was written for.

**Warehouse SQL** is what Holistics generates and sends to the database. This converter reads and writes the
first two, and Holistics generates the third.

## Installation

```bash
pip install -e . # from a checkout of this directory
```

Python 3.10+. The runtime dependencies are `PyYAML` and `sqlglot`. `sqlglot` is here for `to-aml`, which uses
it to find the column references in an Ossie SQL expression. [Ossie to AML](docs/reverse.md#expressions) says
why that is needed.

## Usage

AML to Ossie. `to-ossie` takes compiled JSON, so run the Holistics CLI first:

```bash
holistics aml compile ecommerce.dataset.aml -r . -o build/
ossie-holistics to-ossie build/ecommerce.dataset.aml.json -o ecommerce.yaml --sql-dialect BIGQUERY
```

Ossie to AML. `holistics aml validate` checks the result and needs no account:

```bash
ossie-holistics to-aml ecommerce.yaml -o aml/ --sql-dialect BIGQUERY --data-source-name my_warehouse
holistics aml validate aml/ecommerce.dataset.aml -r aml/
```

Options:

- `-o`, required in both directions. A file for `to-ossie`, a directory for `to-aml`.
- `--sql-dialect DIALECT`, required in both directions. The choices are `ANSI_SQL`, `OSSIE_SQL_2026`,
`BIGQUERY`, `DATABRICKS` and `SNOWFLAKE`, the Ossie dialects sqlglot can parse.
- On `to-ossie` it tags every `@sql` body. AML does not record which warehouse a body was written for.
- On `to-aml` it names the warehouse the Holistics connection reads. An expression already in that dialect
is written as it stands. One in `ANSI_SQL` or `OSSIE_SQL_2026` is rendered into it, with a WARNING
showing both texts. An expression offering neither gets an ERROR, and its body is written unchanged.
- `--data-source-name NAME`, on `to-aml`. The Holistics connection the models read from. AML requires it on a
dataset and Ossie has no field for it. Required when the Ossie document carries no `HOLISTICS` entry in
`custom_extensions`.
- `--issues FILE`. Where to write the issue log, a YAML sequence. Defaults to stderr. Never mixed into the
document output. A message runs to a paragraph, so YAML folds it rather than putting it on one line.
- `--force`. Overwrite existing output files. Without it, an existing target stops the run before anything is
written.

The process exits `1` when the issue log holds an ERROR-severity issue, and `0` otherwise.

`to-aml` writes one dataset file plus one model file per Ossie dataset. An Ossie dataset named
`reporting__calendar` goes to `modules/reporting/calendar.model.aml`:

```
aml/
ecommerce.dataset.aml
ecommerce_orders.model.aml
modules/reporting/calendar.model.aml
```

## Documents

| Document | What it covers |
|---|---|
| [The fixture](docs/fixture.md) | The AML source, how to recompile it, and the shape of the compiled JSON |
| [Mapping](docs/mapping.md) | Which AML construct becomes which Ossie construct, including relationships and data types |
| [Expression translation](docs/expressions.md) | What the converter emits for each definition body, and what it leaves untranslated |
| [Ossie to AML](docs/reverse.md) | The generated file layout, and what AML cannot express |
| [Ossie SQL to AQL](docs/aql.md) | How a cross-dataset metric's Ossie SQL becomes AQL, and what is not translated |
| [Measures](docs/measures.md) | How `aggregation_type` and a definition body compose into one Ossie metric expression |
| [The HOLISTICS payload](docs/vendor-payload.md) | Everything carried in `custom_extensions` |
| [Limitations](docs/limitations.md) | The coverage matrix, and what is known not to work |

## Fixtures

`tests/fixtures/ecommerce/` holds the AML source of the `ecommerce` dataset, written for this converter, and
that source compiled by the Holistics CLI. The `expected*` files are what the converter produces from it in
both directions.

`tests/fixtures/tpcds/expected_aml/` is `examples/tpcds_semantic_model.yaml` converted to AML. That document
carries no `HOLISTICS` stash, so every AML property with no Ossie field is derived or supplied rather than
read back.

`tests/fixtures/aql/` is written for the Ossie SQL to AQL translation. Every metric in it spans two datasets,
which is what makes AML take an `@aql` body, and each covers a different path: a function AQL has, a function
it does not, a column no dataset declares, a field with no `datatype`, and two that are dropped with an
ERROR.

`tests/fixtures/metric_refs/` is written for a metric that names another metric. AML gives a metric two homes
that take different bodies, so the home the named metric landed in decides where the naming one can go. Each
metric covers one way that lands.

## Development

```bash
uv run pytest
python3 tools/check_links.py # every cross-document link and anchor resolves
python3 tools/update_snapshots.py # rewrite the snapshots after a deliberate change
```

Most tests compare a committed snapshot against what the converter produces now. The rest check the output
against something other than itself.

These run the Holistics CLI:

- the generated AML through `holistics aml validate`, for all three fixtures
- the committed compiled JSON against what `holistics aml compile` produces from the committed AML source
- a round trip: compile the generated AML, convert that back to Ossie, and compare it with the document
`to-aml` started from. For `ecommerce` the two are byte-identical.

These need nothing beyond the package:

- the converted document against `core-spec/ossie-schema.json`
- every qualified `<dataset>.<name>` reference against the fields the document declares
- the issue log read back from its folded YAML, which a fold inside a URL would corrupt
- each path through the Ossie SQL to AQL translation, pinned by metric name so a snapshot update cannot quietly
stop covering one
- the issue-code table in `docs/limitations.md` against the codes the source raises, in both directions. See
`tests/test_issue_codes.py`
- a malformed document reaching `to-aml`, which must report rather than raise. See
`tests/test_malformed_input.py`.

CI installs the CLI, so the first group runs there too:

```bash
npm install --no-package-lock --prefix /tmp/holistics \
@holistics/cli-core@2.3.32 @commander-js/extra-typings@14.0.0
export PATH="/tmp/holistics/node_modules/.bin:$PATH"
```

`@holistics/cli-core` is the CLI's logic as one bundled JavaScript file, so it needs only Node. The
[native CLI](https://docs.holistics.io/docs/cli) works the same way for these tests. With neither installed,
that first group skips and the rest of the suite still runs.
Loading
Loading