feat(er-diagram): infer relationship cardinality and export schema to SQL (#1335)#1759
Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
There was a problem hiding this comment.
💡 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, |
There was a problem hiding this comment.
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 👍 / 👎.
| if let defaultValue = column.defaultValue, !defaultValue.isEmpty { | ||
| definition += " DEFAULT \(defaultValue)" |
There was a problem hiding this comment.
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 👍 / 👎.
| let uniqueSingleColumnsByTable: [String: Set<String>] = allIndexes.mapValues { indexes in | ||
| Set(indexes.filter { $0.isUnique && $0.columns.count == 1 }.compactMap { $0.columns.first }) | ||
| } |
There was a problem hiding this comment.
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 👍 / 👎.
…, narrow index fetch (#1335) Claude-Session: https://claude.ai/code/session_01KVYHFmY2TShriMzSyxFwcZ
There was a problem hiding this comment.
💡 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, |
There was a problem hiding this comment.
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 👍 / 👎.
| let cols = group.map { quoteIdentifier($0.column) }.joined(separator: ", ") | ||
| let refCols = group.map { quoteIdentifier($0.referencedColumn) }.joined(separator: ", ") |
There was a problem hiding this comment.
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 } |
There was a problem hiding this comment.
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 👍 / 👎.
…ults, persist junction positions (#1335) Claude-Session: https://claude.ai/code/session_01KVYHFmY2TShriMzSyxFwcZ
There was a problem hiding this comment.
💡 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)" |
There was a problem hiding this comment.
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 👍 / 👎.
| if primaryKeyColumns.count > 1 { | ||
| let cols = primaryKeyColumns.map(quoteIdentifier).joined(separator: ", ") | ||
| lines.append("PRIMARY KEY (\(cols))") |
There was a problem hiding this comment.
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 👍 / 👎.
Closes #1335.
Relationship cardinality
The ER diagram previously rendered every edge the same way:
ERCardinalityhad 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: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 onDatabaseDriver(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 TABLEstatements come first, thenALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY(so circular references work). SQLite inlines foreign keys instead. Composite primary keys and composite foreign keys are grouped;NO ACTIONreferential 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 vsALTER TABLE, circular-FK ordering, composite FK grouping, referential actions, empty-table skip.Notes
docs/features/er-diagram.mdx.project.pbxprojis intentionally excluded; new source files are picked up by the project's filesystem-synchronized groups.https://claude.ai/code/session_01KVYHFmY2TShriMzSyxFwcZ