Migrating from Hasura DDN (v3) to Provisa¶
Prerequisites¶
- A Hasura DDN project with HML files (
.hmlextension). DDN projects typically have a directory structure like:
my-ddn-project/
app/
subgraph1/
models/
MyModel.hml
commands/
MyCommand.hml
subgraph2/
...
globals/
...
- Python 3.11+ with the
provisapackage installed.
CLI Usage¶
Arguments¶
| Argument | Required | Description |
|---|---|---|
hml_dir |
Yes | Path to the DDN HML project directory (scanned recursively for .hml files) |
Options¶
| Option | Default | Description |
|---|---|---|
-o, --output FILE |
stdout | Output YAML file path |
--source-overrides FILE |
None | YAML file with per-source connection overrides |
--domain-map KEY=VAL ... |
None | Subgraph-to-domain mappings (e.g., app=core analytics=reporting) |
--dry-run |
off | Parse and validate without writing output |
Source Overrides File¶
A YAML file keyed by connector name (after ID sanitization: spaces, dots, slashes become underscores) with connection properties:
my_pg_connector:
host: prod-db.example.com
port: 5432
database: chinook
username: provisa_user
password: "${env:PROD_DB_PASSWORD}"
Feature Parity Matrix¶
| DDN Kind | Provisa Equivalent | Notes |
|---|---|---|
| DataConnectorLink | sources[] |
Source type inferred from connector URL (postgres, mysql, mssql, mongo, clickhouse, snowflake, bigquery). Connection details default to placeholders; use --source-overrides to set actual values. |
| ObjectType | Column definitions on tables[] |
Fields become columns. dataConnectorTypeMapping.fieldMapping resolves GraphQL field names to physical column names. |
| Model | tables[] |
Each Model produces one table. source_id from connector, table_name from collection. graphql_type_name becomes alias. Subgraph (and thus domain_id) is derived from the file's directory: the first directory component under the project root. |
| Relationship | relationships[] |
Object type -> many-to-one, Array type -> one-to-many. Field mapping resolved through physical column lookup. |
| TypePermissions | columns[].visible_to[] |
allowedFields determines which roles can see each column. |
| ModelPermissions | rls_rules[] |
Filter predicates converted to SQL WHERE clauses. Supports _eq, _neq, _gt, _lt, _gte, _lte, _in, _nin, _like, _is_null, _and, _or, _not. Session variable references preserved as ${x-hasura-...}. |
| Command | functions[] |
Both functions and procedures mapped. Arguments, return type, and GraphQL root field name preserved. domain_id set from subgraph. |
| AggregateExpression | provisa-aggregates.yaml sidecar |
Count, count_distinct, and per-field aggregate functions preserved in a sidecar file and converted to Provisa aggregate config. |
| BooleanExpressionType | Skipped (silently) | Used internally by DDN for filtering; no direct Provisa equivalent needed. |
| AuthConfig | Skipped (silently) | DDN auth config not mapped; configure Provisa auth separately. |
| ScalarType | Skipped | Warning emitted with count. |
| GraphqlConfig | Skipped | Warning emitted with count. |
| CompatibilityConfig | Skipped | Warning emitted with count. |
| Other unrecognized Kinds | Skipped | Warning emitted with count per kind. |
Key Concept: GraphQL Field to Physical Column Resolution¶
DDN separates the GraphQL schema (field names) from the physical database schema
(column names) via dataConnectorTypeMapping on ObjectTypes. The converter:
- Reads
fieldMappingentries from each ObjectType's type mappings. - Builds a lookup:
{graphql_field_name -> physical_column_name}. - For fields without an explicit mapping, assumes field name equals column name.
- Uses this lookup when building columns, relationships, and RLS filter expressions.
This means the output provisa.yaml uses physical column names for columns[].name
and sets columns[].alias to the GraphQL field name when they differ.
Post-Conversion Steps¶
- Review the output YAML. Verify sources, tables, and column mappings.
- Configure source connections. Connectors only provide a URL hint for type
detection. Actual host/port/database/credentials must be supplied via
--source-overridesor by editing the output. - Verify domain assignments. Subgraph names are derived from directory structure
(the first directory component under the project root). Without
--domain-map, each subgraph name becomes a domain ID directly. Use--domain-mapto rename them. - Check RLS rules. DDN filter predicates are converted to SQL approximations.
Nested boolean logic (
_and/_or/_not) is supported but complex relationship-traversing filters may need manual review. - Review aggregate config. Aggregate expressions are written to a sidecar
provisa-aggregates.yamlfile and converted to Provisa aggregate config. - Review warnings. The converter prints a summary to stderr listing skipped DDN Kinds and any models referencing unknown ObjectTypes.
- Test. Start the Provisa server and verify queries against your data sources.
Common Issues and Troubleshooting¶
Source type detection fails¶
The connector URL is used heuristically (checking for keywords like "postgres",
"mysql", "mongo"). If the URL does not contain a recognizable keyword, the source
defaults to postgresql. Override with --source-overrides.
Missing ObjectType for a Model¶
If a Model references an ObjectType name that was not found in any .hml file,
the table is skipped and a warning is emitted. Ensure all HML files are included
in the scanned directory.
Subgraph discovery¶
Subgraphs are derived from the directory structure: the first directory component
under the project root is taken as the subgraph name. The subgraph field inside
HML documents is not used. Files under a globals/ directory are assigned the
globals subgraph and excluded from domain discovery.
Relationship source resolution¶
Relationships reference a source_type (ObjectType name) and target_model (Model
name). If no Model uses the given ObjectType, the relationship is skipped silently.
Column aliases everywhere¶
If your DDN project uses fieldMapping extensively, expect most columns to have
an alias in the output. This is correct behavior -- name is the physical column,
alias is the GraphQL name your application used.
Aggregate expressions¶
Aggregate expressions are preserved in a sidecar provisa-aggregates.yaml file written
alongside the output and converted to Provisa aggregate config. They are not stored on
the table description.
Example: Converting a Chinook DDN Project¶
# Convert the DDN project
python -m provisa.ddn ./chinook-ddn/ \
-o provisa.yaml \
--domain-map app=music \
--source-overrides overrides.yaml
# Dry run to check warnings first
python -m provisa.ddn ./chinook-ddn/ --dry-run
Output structure:
sources:
- id: chinook_pg
type: postgresql
host: prod-db.example.com
port: 5432
database: chinook
...
domains:
- id: music
tables:
- source_id: chinook_pg
domain_id: music
schema_name: public
table_name: Album
columns:
- name: AlbumId
visible_to: [admin, user]
- name: Title
visible_to: [admin, user]
- name: ArtistId
visible_to: [admin, user]
alias: Albums
- source_id: chinook_pg
domain_id: music
schema_name: public
table_name: Artist
columns:
- name: artist_id
visible_to: [admin, user]
alias: ArtistId
- name: artist_name
visible_to: [admin, user]
alias: Name
alias: Artists
roles:
- id: admin
capabilities: [read]
domain_access: ["*"]
- id: user
capabilities: [read]
domain_access: ["*"]
relationships:
- id: chinook_pg.public.Album.Artist
source_table_id: chinook_pg.public.Album
target_table_id: chinook_pg.public.Artist
source_column: ArtistId
target_column: artist_id
cardinality: many-to-one
functions:
- name: GetTopTracks
source_id: chinook_pg
schema_name: public
function_name: get_top_tracks
returns: Track
domain_id: music
description: "DDN function"