Metadata-Version: 2.4
Name: streamlit-schema-editor
Version: 0.2.0
Summary: Streamlit custom component for rendering and editing schema graphs
Keywords: streamlit,schema,er-diagram,lineage,metadata
Author: Parth Mishra
Author-email: Parth Mishra <pmishraworld@mac.com>
License-Expression: MIT
License-File: LICENSE
Requires-Dist: streamlit>=1.51
Requires-Python: >=3.11
Project-URL: Homepage, https://github.com/parthmishra/streamlit-schema-editor
Project-URL: Repository, https://github.com/parthmishra/streamlit-schema-editor
Project-URL: Issues, https://github.com/parthmishra/streamlit-schema-editor/issues
Description-Content-Type: text/markdown

# streamlit-schema-editor

`streamlit-schema-editor` is a Streamlit custom component for schema
visualization, editing, ER-style canvases, and advanced lineage or mapping
workflows built on React Flow and Streamlit Custom Components v2.

| Light mode | Dark mode |
| --- | --- |
| ![Schema editor preview in light mode](docs/schema-editor-preview-light.png) | ![Schema editor preview in dark mode](docs/schema-editor-preview-dark.png) |

## Features

- Schema viewer and editable ER diagram canvas in one component
- Table, column, relationship, and optional group-based lane model
- Inline column create, update, and delete interactions
- Relationship create/delete flows with duplicate and per-handle limit checks
- Optional mid-edge action button for relationship inspection
- Validation-aware styling for tables, columns, and relationships
- Automatic group layout in `manual`, `columns`, or `rows` mode
- Structured `event` and `event_context` payloads for Streamlit workflows
- Metadata passthrough on groups, tables, columns, and relationships

## Demo App

The deployable demo in `demo/` is a getting-started sandbox for trying the
component inside a regular Streamlit app. It starts with a relationship-free
schema viewer preset, then lets you switch to an ER diagram or a grouped
source-to-target example for richer mapping workflows.

- `demo/requirements.txt` pins the published `streamlit-schema-editor` package
  used by the demo on Streamlit Community Cloud.
- The demo imports that installed package instead of the repository checkout, so
  deployments do not need to build frontend assets on the Cloud machine.
- Switch between schema viewer, editable ER diagram, and mapping canvases
- Filter or reshape tables and relationships before passing them into
  `streamlit_schema_editor(...)`
- Test editing, selection, validation styling, layout options, and edge actions
- Compare the default relationship-free viewer with ER and mapping workflows

## How it Works

The canvas layer is built on [React Flow](https://reactflow.dev/) from
[xyflow](https://xyflow.com/), using the
[`@xyflow/react`](https://www.npmjs.com/package/@xyflow/react) library under the
hood. `streamlit-schema-editor` wraps those primitives in a Streamlit-friendly
API while still exposing familiar capabilities from the React Flow ecosystem.

| Capability in `streamlit-schema-editor` | React Flow primitive | Docs |
| --- | --- | --- |
| Pan/zoom canvas, selection, and viewport management | `ReactFlow` | [API reference](https://reactflow.dev/api-reference/react-flow) |
| Zoom controls and fit-view button | `Controls` | [Component docs](https://reactflow.dev/api-reference/components/controls) |
| Canvas dot grid | `Background` | [Component docs](https://reactflow.dev/api-reference/components/background) |
| Column connection handles | `Handle` | [Component docs](https://reactflow.dev/api-reference/components/handle) |
| Relationship paths, markers, and custom edge actions | `BaseEdge` and `getBezierPath` | [BaseEdge](https://reactflow.dev/api-reference/components/base-edge), [getBezierPath](https://reactflow.dev/api-reference/utils/get-bezier-path) |

## Install

With `uv`:

```bash
uv add streamlit-schema-editor
```

With `pip`:

```bash
pip install streamlit-schema-editor
```

Then open the demo app and use the controls to:

- Create, inspect, and delete relationships between tables
- Drag tables around the canvas and preserve updated positions
- Toggle controls, grouping, validation display, and editability
- Inspect the returned `event` and `event_context` values in Streamlit

## Example

```python
import streamlit as st

from streamlit_schema_editor import streamlit_schema_editor


if "tables" not in st.session_state:
    st.session_state.tables = [
        {
            "id": "customers",
            "label": "customers",
            "metadata": {"schema": "sales"},
            "columns": [
                {"id": "id", "name": "id", "data_type": "uuid"},
                {"id": "email", "name": "email", "data_type": "varchar"},
                {"id": "created_at", "name": "created_at", "data_type": "timestamp"},
            ],
        },
        {
            "id": "orders",
            "label": "orders",
            "metadata": {"schema": "sales"},
            "columns": [
                {"id": "id", "name": "id", "data_type": "uuid"},
                {"id": "customer_id", "name": "customer_id", "data_type": "uuid"},
                {"id": "status", "name": "status", "data_type": "varchar"},
                {"id": "ordered_at", "name": "ordered_at", "data_type": "timestamp"},
            ],
        },
        {
            "id": "order_items",
            "label": "order_items",
            "metadata": {"schema": "sales"},
            "columns": [
                {"id": "id", "name": "id", "data_type": "uuid"},
                {"id": "order_id", "name": "order_id", "data_type": "uuid"},
                {"id": "product_id", "name": "product_id", "data_type": "uuid"},
                {"id": "quantity", "name": "quantity", "data_type": "int4"},
            ],
        },
        {
            "id": "products",
            "label": "products",
            "metadata": {"schema": "catalog"},
            "columns": [
                {"id": "id", "name": "id", "data_type": "uuid"},
                {"id": "sku", "name": "sku", "data_type": "varchar"},
                {"id": "price", "name": "price", "data_type": "money"},
            ],
        },
    ]

if "relationships" not in st.session_state:
    st.session_state.relationships = [
        {
            # Relationship ids only need to be unique and stable. A structured
            # id like this makes generated events easy to trace back to columns.
            "id": "rel::orders::customer_id::customers::id",
            "source_table": "orders",
            "source_column": "customer_id",
            "target_table": "customers",
            "target_column": "id",
            "label": "belongs to",
            "validation": {
                "status": "success",
                "summary": "Foreign key exists in the warehouse.",
            },
            "metadata": {"constraint_name": "fk_orders_customer_id"},
        },
        {
            "id": "rel::order_items::order_id::orders::id",
            "source_table": "order_items",
            "source_column": "order_id",
            "target_table": "orders",
            "target_column": "id",
            "label": "part of",
            "metadata": {"constraint_name": "fk_order_items_order_id"},
        },
        {
            "id": "rel::order_items::product_id::products::id",
            "source_table": "order_items",
            "source_column": "product_id",
            "target_table": "products",
            "target_column": "id",
            "label": "references",
            "metadata": {"constraint_name": "fk_order_items_product_id"},
        },
    ]

value = streamlit_schema_editor(
    st.session_state.tables,
    st.session_state.relationships,
    height=700,
    show_controls=True,
    show_arrowheads=False,
    max_connections_per_handle=2,
    key="schema-editor",
)

st.session_state.tables = value["tables"]
st.session_state.relationships = value["relationships"]

st.write(value["event"])
st.json(value["event_context"])
```

## More Examples

Interactive editor with inline type suggestions and relationship inspection:

```python
value = streamlit_schema_editor(
    tables,
    relationships,
    editable=True,
    show_edge_button=True,
    show_column_count_badge=True,
    column_type_options=["string", "bigint", "timestamp", "json"],
    key="schema-editor-with-edge-actions",
)

if value["event"] == "edge_details_requested":
    relationship_id = (value["event_context"] or {}).get("relationship_id")
    st.write(f"Inspect relationship: {relationship_id}")
```

Viewer-style canvas with grouping and auto layout:

```python
value = streamlit_schema_editor(
    tables,
    relationships,
    groups=[
        {"id": "source", "label": "Source"},
        {"id": "target", "label": "Target"},
    ],
    editable=False,
    connectable=False,
    deletable=False,
    draggable=True,
    show_groups=True,
    group_layout="columns",
    group_order=["source", "target"],
    table_layout_within_group="stack",
    show_controls=True,
    key="schema-viewer",
)
```

Hide validation visuals until the user opts in:

```python
show_validation = st.toggle("Show validation", value=True)

value = streamlit_schema_editor(
    tables,
    relationships,
    show_validation=show_validation,
    key="schema-editor-validation-toggle",
)
```

Force validation visuals to refresh on demand:

```python
if "validation_refresh_nonce" not in st.session_state:
    st.session_state.validation_refresh_nonce = 0

if st.button("Refresh validation visuals"):
    st.session_state.validation_refresh_nonce += 1

value = streamlit_schema_editor(
    tables,
    relationships,
    show_validation=True,
    validation_refresh_key=st.session_state.validation_refresh_nonce,
    key="schema-editor-validation-refresh",
)
```

Advanced source-to-target mapping with shared `mapping_group_id` metadata for
multi-branch targets:

```python
value = streamlit_schema_editor(
    groups=[
        {"id": "source", "label": "Source"},
        {"id": "target", "label": "Target"},
    ],
    tables=[
        {
            "id": "raw.customer_profile",
            "label": "raw.customer_profile",
            "group_id": "source",
            "columns": [
                {"id": "customer_id", "name": "customer_id", "data_type": "string"},
                {"id": "first_name", "name": "first_name", "data_type": "string"},
                {"id": "last_name", "name": "last_name", "data_type": "string"},
            ],
        },
        {
            "id": "curated.customer_dim",
            "label": "curated.customer_dim",
            "group_id": "target",
            "columns": [
                {"id": "customer_id", "name": "customer_id", "data_type": "string"},
                {
                    "id": "full_name",
                    "name": "full_name",
                    "data_type": "string",
                    "metadata": {
                        "mapping_group_id": "customer_full_name",
                        "expression": "concat_ws(' ', first_name, last_name)",
                    },
                },
            ],
        },
    ],
    relationships=[
        {
            "id": "rel::raw.customer_profile::first_name::curated.customer_dim::full_name",
            "source_table": "raw.customer_profile",
            "source_column": "first_name",
            "target_table": "curated.customer_dim",
            "target_column": "full_name",
            "label": "concat",
            "metadata": {
                "mapping_group_id": "customer_full_name",
                "branch_order": 1,
                "expression": "concat_ws(' ', first_name, last_name)",
            },
        },
        {
            "id": "rel::raw.customer_profile::last_name::curated.customer_dim::full_name",
            "source_table": "raw.customer_profile",
            "source_column": "last_name",
            "target_table": "curated.customer_dim",
            "target_column": "full_name",
            "label": "concat",
            "metadata": {
                "mapping_group_id": "customer_full_name",
                "branch_order": 2,
                "expression": "concat_ws(' ', first_name, last_name)",
            },
        },
    ],
    show_groups=True,
    group_layout="columns",
    table_layout_within_group="stack",
    show_edge_button=True,
    key="source-target-mapping",
)
```

The component treats `metadata` as application-owned payload, so you can attach
domain concepts like `mapping_group_id`, SQL expressions, generated-target
markers, lineage ids, or transformation notes without the wrapper needing to
know their schema.

Run the bundled examples from the project root:

```bash
uv run streamlit run examples/playground.py
uv run streamlit run examples/schema_viewer.py
uv run streamlit run examples/er_diagram.py
uv run streamlit run examples/databricks_mapping.py
```

- `demo/streamlit_app.py`: deployable demo app for Streamlit Community Cloud.
  It uses `demo/requirements.txt` to install the published package.
- `examples/playground.py`: interactive playground for runtime options,
  grouping visibility, and validation
- `examples/schema_viewer.py`: read-only schema browser
- `examples/er_diagram.py`: ER-style relationship view with arrowheads hidden
- `examples/databricks_mapping.py`: Databricks-inspired source-to-target mapping
  demo with labeled group lanes

## API

`streamlit_schema_editor(...)` accepts a domain-shaped API and returns the latest
graph state plus a semantic event payload for the current rerun.

### Component props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `tables` | `list[TableSpec]` | required | Schema tables to render. |
| `relationships` | `list[RelationshipSpec]` | required | Relationships between columns. |
| `groups` | `list[GroupSpec] \| None` | `None` | Optional group or lane containers. |
| `height` | `int` | `600` | Canvas height in pixels. |
| `fit_view` | `bool` | `True` | Auto-fit the visible graph on first render. |
| `editable` | `bool` | `True` | Enable inline column editing and add/delete actions. |
| `connectable` | `bool \| None` | `editable` | Enable column handles and relationship creation. |
| `draggable` | `bool \| None` | `True` | Allow tables to be repositioned. |
| `deletable` | `bool \| None` | `editable` | Allow delete-key removal for nodes and edges. |
| `show_controls` | `bool` | `False` | Show React Flow zoom and fit controls. |
| `show_arrowheads` | `bool` | `True` | Draw arrow markers on relationships. |
| `show_edge_button` | `bool` | `False` | Show the inline relationship action button. |
| `show_column_count_badge` | `bool` | `True` | Show a column-count badge in each table header. |
| `show_groups` | `bool` | `True` | Render group or lane panels when group data is present. |
| `group_layout` | `Literal["manual", "columns", "rows"]` | `"manual"` | Group placement strategy. |
| `group_order` | `list[str] \| None` | `None` | Stable display ordering for groups. |
| `table_layout_within_group` | `Literal["manual", "stack"]` | `"manual"` | Table placement strategy inside each group. |
| `show_validation` | `bool` | `True` | Enable validation badges and status coloring. |
| `validation_refresh_key` | `str \| int \| float \| None` | `None` | Force validation UI refresh after upstream state changes. |
| `column_type_options` | `list[str] \| None` | `None` | Suggestions for the inline data type picker. |
| `allow_zoom` | `bool` | `True` | Enable wheel, pinch, and double-click zoom. |
| `allow_duplicate_edges` | `bool` | `False` | Allow multiple identical relationships. |
| `max_connections_per_handle` | `int \| None` | `None` | Apply the same incoming and outgoing connection limit to each handle. |
| `max_incoming_connections_per_handle` | `int \| None` | `None` | Cap incoming relationships per handle. |
| `max_outgoing_connections_per_handle` | `int \| None` | `None` | Cap outgoing relationships per handle. |
| `max_incoming_per_target` | `int \| None` | `None` | Legacy alias for `max_incoming_connections_per_handle`. |
| `max_outgoing_per_source` | `int \| None` | `None` | Legacy alias for `max_outgoing_connections_per_handle`. |
| `key` | `str \| None` | `None` | Optional Streamlit component key. |

`editable` acts as the default for `connectable` and `deletable`. Dragging is
enabled by default even for read-only canvases, and `editable` also enables
inline column editing, row add controls, keyboard deletion, and column edit events. Override
the interaction flags individually when you need a mixed mode.

Use `show_arrowheads=False` for ER-style views where you want the canvas to read
more like an undirected diagram, and `show_column_count_badge=False` when long
table names need the extra header space. The legacy aliases
`max_incoming_per_target` and `max_outgoing_per_source` are still accepted for
backward compatibility, but the generic connection-limit parameters are the
preferred public API.

### Return value

| Key | Type | Description |
| --- | --- | --- |
| `groups` | `list[GroupSpec]` | Current group list with updated positions and sizes. |
| `tables` | `list[TableSpec]` | Current table list with updated positions and inline edits. |
| `relationships` | `list[RelationshipSpec]` | Current relationships after connect/delete actions. |
| `selection` | `SelectionState` | Current selected table, column, and relationship ids. |
| `event` | `SchemaEditorEvent` | Semantic event name or `None`. |
| `event_context` | `dict \| None` | Structured payload for the last event. |

### Key Classes

| Type | Required fields | Optional fields | Notes |
| --- | --- | --- | --- |
| `GroupSpec` | `id`, `label` | `position`, `width`, `height`, `metadata` | Use for group or lane containers. |
| `TableSpec` | `id`, `label`, `columns` | `position`, `group_id`, `validation`, `metadata` | `group_id` ties a table to a `GroupSpec`. |
| `ColumnSpec` | `id`, `name`, `data_type` | `validation`, `metadata` | `metadata` is passed through untouched. |
| `RelationshipSpec` | `id`, `source_table`, `source_column`, `target_table`, `target_column` | `label`, `validation`, `metadata` | `id` can be any unique, stable string. Structured ids are useful for deterministic event handling, debugging, and duplicate detection. |
| `ValidationSpec` | none | `status`, `code`, `summary`, `detail` | `status` supports `initial`, `success`, `loading`, and `error`. |
| `Position` | `x`, `y` | none | Used by groups, tables, and move events. |

Use `validation` for generic UI state that the component understands, and keep
app-specific semantics in `metadata`. For example, SQL expressions, lineage
attributes, ownership, or workflow IDs should live in `metadata`, not as
first-class component fields.

### Events

Events are dispatched in response to actions. You can use these to define your own event handlers. The full list of supported events are described in this table:

| Event | Description |
| --- | --- |
| `selection_changed` | Fires when table, column, or relationship selection changes. |
| `node_moved` | Fires when a table position changes. |
| `table_deleted` | Fires after tables are removed, including any deleted relationship ids. |
| `column_created` | Fires after a column is added. |
| `column_updated` | Fires after an inline column edit is saved. |
| `column_deleted` | Fires after a column is removed, including any deleted relationship ids. |
| `relationship_created` | Fires after a new relationship is created. |
| `relationship_deleted` | Fires with the deleted relationship id. |
| `relationship_rejected` | Fires when duplicate or handle-limit rules reject a relationship. |
| `edge_details_requested` | Fires when the relationship action button is clicked. |
| `history_undone` | Fires after the built-in undo control restores the previous graph state. |
| `history_redone` | Fires after the built-in redo control reapplies a graph state. |

`edge_details_requested` is only emitted when `show_edge_button=True`.

Handle inline column edit events in Streamlit:

```python
value = streamlit_schema_editor(
    tables,
    relationships,
    editable=True,
    key="schema-editor-events",
)

if value["event"] == "column_created":
    st.success(f"Added column: {(value['event_context'] or {}).get('column_id')}")

if value["event"] == "column_updated":
    context = value["event_context"] or {}
    st.info(
        "Updated "
        f"{context.get('table_id')}.{context.get('column_id')} "
        f"fields={context.get('fields')}"
    )

if value["event"] == "column_deleted":
    context = value["event_context"] or {}
    st.warning(
        "Deleted "
        f"{context.get('table_id')}.{context.get('column_id')} "
        f"and removed relationships={context.get('deleted_relationship_ids')}"
    )
```

## Usage Notes

- Inline editing, hover state, and connection gestures stay local to the React
  component until they emit durable graph state back to Streamlit.
- Arbitrary metadata is passed through untouched on groups, tables,
  columns, and relationships.
- The component is designed for rerun-driven usage: persist returned `tables` and
  `relationships` in `st.session_state` when edits should survive subsequent
  Streamlit reruns.
- Use `groups` plus per-table `group_id` for optional labeled containers such as
  `Source` / `Target`, database lanes, or bronze/silver/gold layers.
- Automatic layout modes own placement, so `group_layout="columns"` or
  `group_layout="rows"` pair best with read-only or lightly interactive canvases.
- Column ids remain stable and are not edited inline. If you delete a column, any
  attached relationships are removed from the graph at the same time.

## Repository Structure

This repository is structured like a standard Streamlit custom component
project:

- `streamlit_schema_editor/` contains the publishable Python package
- `streamlit_schema_editor/frontend/` contains the React Flow frontend component
- `demo/` contains the Streamlit Community Cloud demo app
- `examples/` contains smaller Streamlit examples for manual testing
- `tests/` contains Python-side and component regression coverage
- `pyproject.toml` builds the package as `streamlit-schema-editor`

## Local Validation

From the repository root:

```bash
uv sync --extra dev
cd streamlit_schema_editor/frontend
npm install
npm run build
cd ../..
uv build
uv run pytest
```

To run the primary demo app:

```bash
python -m pip install -r demo/requirements.txt
streamlit run demo/streamlit_app.py
```
