Skip to content

feat(er-diagram): infer relationship cardinality and export schema to SQL (#1335)#1759

Merged
datlechin merged 3 commits into
mainfrom
feat/er-diagram-cardinality-sql-export
Jun 24, 2026
Merged

feat(er-diagram): infer relationship cardinality and export schema to SQL (#1335)#1759
datlechin merged 3 commits into
mainfrom
feat/er-diagram-cardinality-sql-export

Conversation

@datlechin

Copy link
Copy Markdown
Member

Closes #1335.

Relationship cardinality

The ER diagram previously rendered every edge the same way: ERCardinality had one case and the renderer never read it. Now each foreign key's cardinality is inferred from primary key and single-column unique index data plus column nullability:

FK column Marker Meaning
unique + NOT NULL bar + bar one-to-one
unique + nullable bar + circle zero-or-one-to-one
non-unique + NOT NULL fork + bar one-to-many
non-unique + nullable fork + circle zero-or-many-to-one

Junction tables (a composite primary key made of two foreign keys to two distinct tables) are detected and, by default, collapsed into a single many-to-many edge between the two related tables. A toolbar toggle (shown only when the schema has junction tables) expands them back to the underlying table and its two one-to-many edges; expanded junction nodes get a distinct header icon.

Unique index data comes from a new fetchAllIndexes() default on DatabaseDriver (per-table fallback through the existing plugin call). No PluginKit or ABI change.

Export to SQL

A new "Export as SQL" toolbar button opens a query tab with the schema's DDL in the connection's dialect. All CREATE TABLE statements come first, then ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY (so circular references work). SQLite inlines foreign keys instead. Composite primary keys and composite foreign keys are grouped; NO ACTION referential actions are omitted.

Tests

  • ERDiagramGraphBuilderTests: cardinality inference (incl. composite-unique not implying 1:1, and missing-column fallback), junction detection with three negative cases, and the collapse/expand projection.
  • ERDiagramSQLExporterTests: per-dialect DDL, SQLite inline FK vs ALTER TABLE, circular-FK ordering, composite FK grouping, referential actions, empty-table skip.

Notes

  • Documented in docs/features/er-diagram.mdx.
  • The unrelated working-tree change to project.pbxproj is intentionally excluded; new source files are picked up by the project's filesystem-synchronized groups.

https://claude.ai/code/session_01KVYHFmY2TShriMzSyxFwcZ

@mintlify

mintlify Bot commented Jun 23, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
TablePro 🟢 Ready View Preview Jun 23, 2026, 8:52 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 46399e4ef1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

tableNames: fullGraph.nodes.map(\.tableName),
allColumns: allColumns,
allForeignKeys: allForeignKeys,
isSQLite: databaseType == .sqlite,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat libSQL and D1 as SQLite for FK export

When ER SQL export runs on a libSQL/Turso or Cloudflare D1 connection, databaseType is not .sqlite even though these backends use SQLite-style DDL and do not support ALTER TABLE ... ADD CONSTRAINT. With any foreign key, this routes the export through the non-SQLite path and opens a query tab containing ALTER statements that cannot be run; please include the SQLite-compatible types here or derive the decision from dialect/capabilities.

Useful? React with 👍 / 👎.

Comment on lines +85 to +86
if let defaultValue = column.defaultValue, !defaultValue.isEmpty {
definition += " DEFAULT \(defaultValue)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Quote raw string defaults before emitting DDL

For MySQL/MariaDB schemas with string defaults, the column metadata stores the raw default value (for example active, not 'active'; the MySQL DDL builder quotes these). Appending it verbatim generates invalid or semantically different DDL such as DEFAULT active for a VARCHAR column, so exported schemas with string defaults will not recreate correctly unless defaults are formatted per dialect before being emitted.

Useful? React with 👍 / 👎.

Comment on lines +94 to +96
let uniqueSingleColumnsByTable: [String: Set<String>] = allIndexes.mapValues { indexes in
Set(indexes.filter { $0.isUnique && $0.columns.count == 1 }.compactMap { $0.columns.first })
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore partial unique indexes for cardinality inference

This treats every single-column unique index as proving the FK is globally unique, but PostgreSQL index metadata includes partial unique indexes via whereClause. For a relationship like UNIQUE (user_id) WHERE active, multiple rows for the same user are still allowed outside the predicate, yet the diagram will mark the FK as one-to-one; skip indexes with a non-empty predicate (and other non-global uniqueness cases) when inferring cardinality.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e8879eb51b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

tableNames: fullGraph.nodes.map(\.tableName),
allColumns: allColumns,
allForeignKeys: allForeignKeys,
isSQLite: databaseType == .sqlite,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Inline DuckDB foreign keys during SQL export

For DuckDB connections with any foreign key, this passes false and the exporter emits ALTER TABLE ... ADD CONSTRAINT, but DuckDB does not support ADD CONSTRAINT (official docs: https://duckdb.org/docs/current/sql/statements/alter_table.html; the bundled DuckDB DDL generator also inlines FKs in Plugins/DuckDBDriverPlugin/DuckDBPlugin.swift). The generated query tab therefore cannot recreate DuckDB schemas with FKs; route DuckDB through an inline-FK path or a dialect capability rather than only checking .sqlite.

Useful? React with 👍 / 👎.

Comment on lines +126 to +127
let cols = group.map { quoteIdentifier($0.column) }.joined(separator: ", ")
let refCols = group.map { quoteIdentifier($0.referencedColumn) }.joined(separator: ", ")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve composite FK column order

For SQL Server composite foreign keys, fetchAllForeignKeys orders rows only by table/fk name (Plugins/MSSQLDriverPlugin/MSSQLPluginDriver+Schema.swift:374) and not by fkc.constraint_column_id, so the order inside group is nondeterministic. Mapping child and referenced columns by that order can emit FOREIGN KEY (a, b) REFERENCES parent (b, a), recreating the wrong relationship or failing; include and preserve the FK ordinal before joining these arrays.

Useful? React with 👍 / 👎.

foreignKeys: [ForeignKeyInfo]
) -> (String, String)? {
let pkColumns = Set(columns.filter { $0.isPrimaryKey }.map(\.name))
guard pkColumns.count >= 2 else { return nil }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require binary PKs before collapsing junctions

When a composite primary key contains more than two FK columns but only two distinct referenced tables, such as buyer_id and seller_id both referencing users plus product_id referencing products, this still classifies the table as a junction. The default collapsed view then hides the real table and its edges and replaces the ternary association with a single users-products many-to-many edge, so require exactly two PK FK columns or otherwise skip non-binary associations.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7a45cc7927

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

inlinePrimaryKey: Bool,
quoteIdentifier: (String) -> String
) -> String {
var definition = "\(quoteIdentifier(column.name)) \(column.dataType)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve identity clauses during SQL export

When exporting a table whose generated key is represented in metadata, such as MySQL extra containing auto_increment or SQL Server setting extra == "IDENTITY", this column definition starts from only the quoted name and data type and then appends null/default/PK clauses, never using ColumnInfo.extra. The exported DDL recreates those primary keys as plain integer columns, so subsequent inserts that rely on database-generated ids fail or require manual values; please emit the dialect-specific identity/auto-increment clause or delegate this part to the driver's DDL builder.

Useful? React with 👍 / 👎.

Comment on lines +63 to +65
if primaryKeyColumns.count > 1 {
let cols = primaryKeyColumns.map(quoteIdentifier).joined(separator: ", ")
lines.append("PRIMARY KEY (\(cols))")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Emit unique keys needed by exported FKs

When a foreign key references a natural key or other unique non-primary column, this exporter creates the parent table without any UNIQUE constraint and then emits the child FK. PostgreSQL rejects the later ADD CONSTRAINT because the referenced columns are not declared unique, and SQLite schemas hit foreign-key mismatch behavior at use time; include the relevant unique index/constraint metadata before emitting those FKs.

Useful? React with 👍 / 👎.

@datlechin datlechin merged commit ec78a26 into main Jun 24, 2026
3 checks passed
@datlechin datlechin deleted the feat/er-diagram-cardinality-sql-export branch June 24, 2026 07:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(er-diagram): show all relationship types and export diagram to SQL

1 participant