Skip to content

# Migrating from Hasura DDN (v3) to Provisa

## Prerequisites

1. A Hasura DDN project with HML files (`.hml` extension). DDN projects typically have a directory structure like:

       my-ddn-project/
         app/
           subgraph1/
             models/
               MyModel.hml
             commands/
               MyCommand.hml
           subgraph2/
             ...
         globals/
           ...

2. Python 3.11+ with the `provisa` package installed.

## CLI Usage

    python -m provisa.ddn <hml-dir> -o provisa.yaml

### 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:

1. Reads `fieldMapping` entries from each ObjectType's type mappings.
2. Builds a lookup: `{graphql_field_name -> physical_column_name}`.
3. For fields without an explicit mapping, assumes field name equals column name.
4. 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

1. Review the output YAML. Verify sources, tables, and column mappings.
2. Configure source connections. Connectors only provide a URL hint for type detection. Actual host/port/database/credentials must be supplied via `--source-overrides` or by editing the output.
3. 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-map` to rename them.
4. 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.
5. Review aggregate config. Aggregate expressions are written to a sidecar `provisa-aggregates.yaml` file and converted to Provisa aggregate config.
6. Review warnings. The converter prints a summary to stderr listing skipped DDN Kinds and any models referencing unknown ObjectTypes.
7. 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"