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
61 changes: 54 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,58 @@ jobs:
- name: Run tests
run: uv run pytest -v

base-install-cli:
name: Base Install CLI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true

- name: Set up Python
run: uv python install 3.12

- name: Smoke base CLI install
run: |
set -euo pipefail
tmpdir="$(mktemp -d)"
mkdir -p "$tmpdir/models"
cat > "$tmpdir/models/models.yml" <<'YAML'
models:
- name: orders
table: orders
primary_key: id
dimensions:
- name: status
type: categorical
metrics:
- name: order_count
agg: count
YAML

uv run --no-project --with . sidemantic --version
uv run --no-project --with . sidemantic validate "$tmpdir/models"
uv run --no-project --with . sidemantic query "SELECT order_count, status FROM orders" --models "$tmpdir/models" --dry-run

set +e
timeout 10s uv run --no-project --with . sidemantic serve "$tmpdir/models" >"$tmpdir/serve.out" 2>"$tmpdir/serve.err"
serve_status=$?
set -e
if [ "$serve_status" -eq 0 ]; then
echo "base install unexpectedly ran sidemantic serve without the serve extra"
exit 1
fi
if [ "$serve_status" -eq 124 ]; then
echo "base install unexpectedly started sidemantic serve without the serve extra"
cat "$tmpdir/serve.out"
cat "$tmpdir/serve.err" >&2
exit 1
fi
grep -q "sidemantic\\[serve\\]" "$tmpdir/serve.err"

update-schema:
name: Update JSON Schema
needs: python
Expand Down Expand Up @@ -267,14 +319,9 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Clone DuckDB dependencies
- name: Fetch DuckDB dependencies
working-directory: sidemantic-duckdb
run: |
# These are listed as submodules in .gitmodules but aren't tracked properly
# Clone them directly at the commits the extension was built against
rm -rf duckdb extension-ci-tools
git clone --depth 1 --branch v1.4.2 https://github.com/duckdb/duckdb.git duckdb
git clone --depth 1 --branch v1.4.2 https://github.com/duckdb/extension-ci-tools.git extension-ci-tools
run: make deps DUCKDB_VERSION=v1.4.2

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
Expand Down
9 changes: 3 additions & 6 deletions .github/workflows/duckdb-extension-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs:
duckdb_version:
description: "DuckDB tag used for extension build and tests"
description: "DuckDB tag used for extension build and tests; currently v1.4.2 only"
required: true
default: "v1.4.2"
type: string
Expand Down Expand Up @@ -42,12 +42,9 @@ jobs:

echo "version=$VERSION" >> "$GITHUB_OUTPUT"

- name: Clone DuckDB dependencies
- name: Fetch DuckDB dependencies
working-directory: sidemantic-duckdb
run: |
rm -rf duckdb extension-ci-tools
git clone --depth 1 --branch "${{ steps.duckdb.outputs.version }}" https://github.com/duckdb/duckdb.git duckdb
git clone --depth 1 --branch "${{ steps.duckdb.outputs.version }}" https://github.com/duckdb/extension-ci-tools.git extension-ci-tools
run: make deps DUCKDB_VERSION="${{ steps.duckdb.outputs.version }}"

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
Expand Down
25 changes: 11 additions & 14 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,15 @@ jobs:
- name: Update lock file
run: uv lock

- name: Validate release tree
run: |
uv run ruff check . --exclude docs/_extensions --exclude sidemantic-duckdb/extension-ci-tools --exclude sidemantic-duckdb/scripts --exclude sidemantic-duckdb/duckdb --exclude sidemantic/adapters/malloy_grammar --exclude sidemantic/adapters/holistics_grammar
uv run ruff format --check . --exclude docs/_extensions --exclude sidemantic-duckdb/extension-ci-tools --exclude sidemantic-duckdb/scripts --exclude sidemantic-duckdb/duckdb --exclude sidemantic/adapters/malloy_grammar --exclude sidemantic/adapters/holistics_grammar
uv run pytest -v

- name: Build package
run: uv build

- name: Publish to PyPI
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
run: uv publish --token $UV_PUBLISH_TOKEN

- name: Commit version bump and create tag
run: |
git config user.name "github-actions[bot]"
Expand All @@ -91,19 +92,15 @@ jobs:
git push origin main
git push origin "v${{ steps.version.outputs.new_version }}"

- name: Publish to PyPI
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
run: uv publish --token $UV_PUBLISH_TOKEN

- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "v${{ steps.version.outputs.new_version }}" \
--title "v${{ steps.version.outputs.new_version }}" \
--generate-notes

rust-binaries:
name: Release Rust binaries
needs: publish
permissions:
contents: write
uses: ./.github/workflows/release-rust-binaries.yml
with:
tag: ${{ needs.publish.outputs.tag }}
27 changes: 26 additions & 1 deletion .github/workflows/pyodide-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,31 @@ jobs:
- name: Build sidemantic wheel
run: uv build

- name: Inspect wheel metadata
run: |
python - <<'PY'
import re
from email.parser import Parser
from pathlib import Path
from zipfile import ZipFile

wheel = next(Path("dist").glob("*.whl"))
with ZipFile(wheel) as archive:
metadata_name = next(name for name in archive.namelist() if name.endswith(".dist-info/METADATA"))
metadata = Parser().parsestr(archive.read(metadata_name).decode())

print(metadata)
requires = metadata.get_all("Requires-Dist", [])
extras = set(metadata.get_all("Provides-Extra", []))
heavy_optional = ("textual", "pyarrow", "mcp", "riffq", "altair", "vl-convert-python")
for requirement in requires:
name = re.split(r"[<>=!~;\[ ]", requirement.strip(), maxsplit=1)[0]
if name in heavy_optional:
assert "extra ==" in requirement, requirement
assert "serve" in extras
assert "workbench" in extras
PY

- name: Install Pyodide and dependencies
run: npm install pyodide glob

Expand Down Expand Up @@ -51,7 +76,7 @@ jobs:
const wheelName = wheelPath.split('/').pop();
pyodide.FS.writeFile(`/tmp/${wheelName}`, wheelData);

console.log('Installing missing deps and sidemantic...');
console.log('Installing sidemantic with the documented Pyodide no-deps path...');
await pyodide.runPythonAsync(`
import micropip
# Install missing pure-Python deps
Expand Down
34 changes: 26 additions & 8 deletions .github/workflows/release-rust-binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
workflow_dispatch:
inputs:
tag:
description: Git tag for the GitHub Release to attach binaries to, for example v0.1.0.
description: Git tag for the GitHub Release to attach binaries to, for example sidemantic-rs-v0.1.0.
required: true
type: string

Expand Down Expand Up @@ -72,6 +72,27 @@ jobs:
with:
ref: ${{ needs.verify-release.outputs.tag }}

- name: Resolve Rust crate version
id: rust_version
shell: bash
run: |
set -euo pipefail
VERSION=$(grep '^version = ' sidemantic-rs/Cargo.toml | sed 's/version = "\(.*\)"/\1/')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"

- name: Verify release tag matches crate version
shell: bash
env:
RELEASE_TAG: ${{ needs.verify-release.outputs.tag }}
RUST_VERSION: ${{ steps.rust_version.outputs.version }}
run: |
set -euo pipefail
expected_tag="sidemantic-rs-v${RUST_VERSION}"
if [ "$RELEASE_TAG" != "$expected_tag" ]; then
echo "Release tag '$RELEASE_TAG' does not match Rust crate version tag '$expected_tag'" >&2
exit 1
fi

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
Expand Down Expand Up @@ -115,11 +136,11 @@ jobs:
if: runner.os != 'Windows'
env:
ASSET_SUFFIX: ${{ matrix.asset_suffix }}
TAG: ${{ needs.verify-release.outputs.tag }}
RUST_VERSION: ${{ steps.rust_version.outputs.version }}
TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
version="${TAG#v}"
version="$RUST_VERSION"
asset_name="sidemantic-rs-${version}-${ASSET_SUFFIX}"
archive_path="dist/${asset_name}.tar.gz"
checksum_path="${archive_path}.sha256"
Expand Down Expand Up @@ -147,14 +168,11 @@ jobs:
if: runner.os == 'Windows'
env:
ASSET_SUFFIX: ${{ matrix.asset_suffix }}
TAG: ${{ needs.verify-release.outputs.tag }}
RUST_VERSION: ${{ steps.rust_version.outputs.version }}
TARGET: ${{ matrix.target }}
shell: pwsh
run: |
$version = $env:TAG
if ($version.StartsWith("v")) {
$version = $version.Substring(1)
}
$version = $env:RUST_VERSION
$assetName = "sidemantic-rs-$version-$env:ASSET_SUFFIX"
$archivePath = "dist/$assetName.zip"
$checksumPath = "$archivePath.sha256"
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,13 @@ result = layer.sql("SELECT revenue, status FROM orders")
sidemantic query "SELECT revenue FROM orders" --db data.duckdb

# Interactive workbench (TUI with SQL editor + charts)
sidemantic workbench models/ --db data.duckdb
uvx --from "sidemantic[workbench]" sidemantic workbench models/ --db data.duckdb

# PostgreSQL server (connect Tableau, DBeaver, etc.)
sidemantic serve models/ --port 5433
uvx --from "sidemantic[serve]" sidemantic serve models/ --port 5433

# HTTP API server (JSON or Arrow)
sidemantic api-serve models/ --port 4400 --auth-token secret
uvx --from "sidemantic[api]" sidemantic api-serve models/ --port 4400 --auth-token secret

# Validate definitions
sidemantic validate models/
Expand All @@ -159,7 +159,7 @@ uvx --from "sidemantic[workbench]" sidemantic workbench --demo

**PostgreSQL server** (connect Tableau, DBeaver, etc.):
```bash
uvx sidemantic serve --demo --port 5433
uvx --from "sidemantic[serve]" sidemantic serve --demo --port 5433
```

**HTTP API server** (JSON or Arrow):
Expand Down Expand Up @@ -227,7 +227,7 @@ See `examples/` for more.
- Multi-format adapters (Cube, MetricFlow, LookML, Hex, Rill, Superset, Omni, BSL, GoodData LDM, OSI, AtScale SML, ThoughtSpot TML, Graphene GSQL)
- SQLGlot-based SQL generation and transpilation
- Pydantic validation and type safety
- Pre-aggregations with automatic routing
- Pre-aggregations with explicit routing
- Predicate pushdown for faster queries
- Segments and metric-level filters
- Jinja2 templating for dynamic SQL
Expand Down Expand Up @@ -285,7 +285,7 @@ For Cloudflare Worker + Container deployment, see [`examples/cloudflare_containe
Start the API server:

```bash
sidemantic api-serve models/ --db data.duckdb --port 4400 --auth-token secret
uvx --from "sidemantic[api]" sidemantic api-serve models/ --db data.duckdb --port 4400 --auth-token secret
```

Compile a structured semantic query:
Expand Down
21 changes: 17 additions & 4 deletions docs/duckdb-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,27 @@ LOAD sidemantic;
```

Until community publication is complete, use a local extension artifact.
Local artifacts are unsigned, so start the DuckDB CLI with unsigned-extension loading enabled:

```bash
duckdb -unsigned
```

## Build From Source

The extension build needs Rust, DuckDB extension build tooling, and Ninja.

```bash
cd sidemantic-duckdb
rm -rf duckdb extension-ci-tools
git clone --depth 1 --branch v1.4.2 https://github.com/duckdb/duckdb.git duckdb
git clone --depth 1 --branch v1.4.2 https://github.com/duckdb/extension-ci-tools.git extension-ci-tools
make deps DUCKDB_VERSION=v1.4.2
make
make test
```

`DUCKDB_VERSION` is intentionally guarded to `v1.4.2` because the repository
vendors a matching `extension-ci-tools` checkout. Update both together before
building against a different DuckDB tag.

The local loadable extension is produced at:

```text
Expand All @@ -36,10 +43,16 @@ sidemantic-duckdb/build/release/extension/sidemantic/sidemantic.duckdb_extension

Load it in the DuckDB shell built by the extension workflow:

```bash
./build/release/duckdb -unsigned
```

```sql
LOAD 'build/release/extension/sidemantic/sidemantic.duckdb_extension';
```

For embedded clients, set DuckDB's `allow_unsigned_extensions` database configuration before opening the connection.

## Runtime API

Load native YAML:
Expand Down Expand Up @@ -88,7 +101,7 @@ Use `.github/workflows/duckdb-extension-release.yml`.

The workflow:

- clones DuckDB and `extension-ci-tools` for the selected DuckDB tag,
- fetches DuckDB with `make deps` and verifies the vendored `extension-ci-tools`,
- builds the Rust-backed extension,
- runs the DuckDB sqllogictests, including native YAML load, native SQL definition file load, relationship rewrite, semantic select, persistence, and invalid-version coverage,
- uploads a Linux extension artifact,
Expand Down
20 changes: 20 additions & 0 deletions docs/pyodide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Pyodide Runtime

Sidemantic's browser/WASM path is a no-dependency wheel install. Install the Pyodide-compatible runtime packages first, then install the Sidemantic wheel with dependency resolution disabled:

```python
import micropip

await pyodide.loadPackage(["micropip", "pydantic", "pyyaml", "jinja2"])
await micropip.install(["sqlglot", "lkml", "inflect"], deps=False)
await micropip.install("emfs:/tmp/sidemantic-<version>-py3-none-any.whl", deps=False)
```

This is intentional. The published Python package includes CLI/database dependencies that are not part of the Pyodide runtime contract. The supported Pyodide import surface is the core semantic model API, for example:

```python
from sidemantic import Model, Dimension, Metric, Relationship
from sidemantic.core.semantic_graph import SemanticGraph
```

Optional server, workbench, chart, and database execution paths are not Pyodide targets.
Loading
Loading