Skip to content

Commit 7a4b6de

Browse files
bchapuisclaude
andcommitted
Enhance database explorer with column property icons and FK direction arrows
Add unique/autoincrement detection via PRAGMA index_list and sqlite_master DDL parsing. Show PK, autoincrement, unique, required icons and default value badges on columns. Add arrow markers to foreign key edges to indicate direction. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e8be19d commit 7a4b6de

3 files changed

Lines changed: 77 additions & 8 deletions

File tree

apps/api/src/routes/databases.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -218,13 +218,50 @@ databaseRoutes.get("/:databaseId/schema", apiKeyOrJwtMiddleware, async (c) => {
218218
(r) => r.name
219219
);
220220

221-
// Fetch columns and foreign keys for each table
221+
// Fetch columns, foreign keys, indexes, and DDL for each table
222222
const tables: DatabaseSchemaTable[] = await Promise.all(
223223
tableNames.map(async (tableName) => {
224-
const [columnsResult, fksResult] = await Promise.all([
225-
connection.query(`PRAGMA table_info("${tableName}")`),
226-
connection.query(`PRAGMA foreign_key_list("${tableName}")`),
227-
]);
224+
const [columnsResult, fksResult, indexListResult, ddlResult] =
225+
await Promise.all([
226+
connection.query(`PRAGMA table_info("${tableName}")`),
227+
connection.query(`PRAGMA foreign_key_list("${tableName}")`),
228+
connection.query(`PRAGMA index_list("${tableName}")`),
229+
connection.query(
230+
`SELECT sql FROM sqlite_master WHERE type='table' AND name=?`,
231+
[tableName]
232+
),
233+
]);
234+
235+
// Detect AUTOINCREMENT from the CREATE TABLE DDL
236+
const ddl = (ddlResult.results as { sql: string }[])[0]?.sql ?? "";
237+
const hasAutoIncrement = /AUTOINCREMENT/i.test(ddl);
238+
239+
// Collect unique columns from single-column unique indexes
240+
const uniqueColumns = new Set<string>();
241+
const indexes = indexListResult.results as {
242+
seq: number;
243+
name: string;
244+
unique: number;
245+
origin: string;
246+
partial: number;
247+
}[];
248+
await Promise.all(
249+
indexes
250+
.filter((idx) => idx.unique === 1)
251+
.map(async (idx) => {
252+
const infoResult = await connection.query(
253+
`PRAGMA index_info("${idx.name}")`
254+
);
255+
const cols = infoResult.results as {
256+
seqno: number;
257+
cid: number;
258+
name: string;
259+
}[];
260+
if (cols.length === 1) {
261+
uniqueColumns.add(cols[0].name);
262+
}
263+
})
264+
);
228265

229266
const columns: DatabaseSchemaColumn[] = (
230267
columnsResult.results as {
@@ -241,6 +278,8 @@ databaseRoutes.get("/:databaseId/schema", apiKeyOrJwtMiddleware, async (c) => {
241278
notnull: col.notnull === 1,
242279
defaultValue: col.dflt_value,
243280
primaryKey: col.pk > 0,
281+
unique: uniqueColumns.has(col.name),
282+
autoIncrement: col.pk > 0 && hasAutoIncrement,
244283
}));
245284

246285
const foreignKeys: DatabaseSchemaForeignKey[] = (

apps/app/src/pages/database-explorer-page.tsx

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,18 @@ import {
1111
BackgroundVariant,
1212
Controls,
1313
Handle,
14+
MarkerType,
1415
Position,
1516
ReactFlow,
1617
ReactFlowProvider,
1718
useNodesInitialized,
1819
useNodesState,
1920
useReactFlow,
2021
} from "@xyflow/react";
22+
import ArrowUp01 from "lucide-react/icons/arrow-up-0-1";
23+
import Asterisk from "lucide-react/icons/asterisk";
24+
import Hash from "lucide-react/icons/hash";
25+
import KeyRound from "lucide-react/icons/key-round";
2126
import { useCallback, useEffect, useMemo, useRef } from "react";
2227
import { useParams } from "react-router";
2328

@@ -57,13 +62,31 @@ function SchemaTableNode({ data }: NodeProps<Node<SchemaTableNodeData>>) {
5762
isConnectable={false}
5863
/>
5964
<span
60-
className={
65+
className={cn(
66+
"flex items-center gap-1",
6167
col.primaryKey ? "font-semibold" : "text-foreground/80"
62-
}
68+
)}
6369
>
70+
{col.primaryKey && (
71+
<KeyRound className="h-3 w-3 text-muted-foreground shrink-0" />
72+
)}
73+
{col.autoIncrement && (
74+
<ArrowUp01 className="h-3 w-3 text-muted-foreground shrink-0" />
75+
)}
76+
{col.unique && (
77+
<Hash className="h-3 w-3 text-muted-foreground shrink-0" />
78+
)}
79+
{col.notnull && !col.primaryKey && (
80+
<Asterisk className="h-3 w-3 text-muted-foreground shrink-0" />
81+
)}
6482
{col.name}
6583
</span>
66-
<span className="text-muted-foreground/60 font-light uppercase">
84+
<span className="flex items-center gap-1.5 text-muted-foreground/60 font-light uppercase">
85+
{col.defaultValue !== null && (
86+
<span className="text-[11px] normal-case font-normal bg-muted px-1 py-0.5 rounded text-muted-foreground">
87+
{col.defaultValue}
88+
</span>
89+
)}
6790
{col.type || "TEXT"}
6891
</span>
6992
<Handle
@@ -154,6 +177,11 @@ function SchemaFlowCanvas({ tables }: SchemaFlowCanvasProps) {
154177
target: fk.referencedTable,
155178
targetHandle: `${fk.referencedTable}-${targetCol}-target`,
156179
type: "smoothstep",
180+
markerEnd: {
181+
type: MarkerType.ArrowClosed,
182+
width: 16,
183+
height: 16,
184+
},
157185
};
158186
})
159187
.filter((e) => e !== null)

packages/types/src/database.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ export interface DatabaseSchemaColumn {
6666
notnull: boolean;
6767
defaultValue: string | null;
6868
primaryKey: boolean;
69+
unique: boolean;
70+
autoIncrement: boolean;
6971
}
7072

7173
export interface DatabaseSchemaForeignKey {

0 commit comments

Comments
 (0)