@hienlh/ppm 0.9.64 → 0.9.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/dist/web/assets/{_basePickBy-3Xe18azI.js → _basePickBy-5PGDJbfF.js} +1 -1
- package/dist/web/assets/{_baseUniq-Yy35llnn.js → _baseUniq-BT4Ow4Kk.js} +1 -1
- package/dist/web/assets/{api-settings-CgBII8jW.js → api-settings-Bn-bIxD1.js} +1 -1
- package/dist/web/assets/{arc-B9n1Gvb5.js → arc-BAOivWpI.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-DqAZP_F6.js → architectureDiagram-2XIMDMQ5-Z-4eN4za.js} +1 -1
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-h3cDF2vI.js → blockDiagram-WCTKOSBZ-BCLqzhuZ.js} +1 -1
- package/dist/web/assets/{c4Diagram-IC4MRINW--pF1r5lr.js → c4Diagram-IC4MRINW-0Vp0Jeas.js} +1 -1
- package/dist/web/assets/channel-By7bn0Yq.js +1 -0
- package/dist/web/assets/chat-tab-BSExiIao.js +10 -0
- package/dist/web/assets/{chunk-4BX2VUAB-C3aZvW7B.js → chunk-4BX2VUAB-D4tOov49.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-D5cABeB9.js → chunk-55IACEB6-DJ6BynZ4.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-CkFGv6Zs.js → chunk-7E7YKBS2-CiyUJxNI.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-Dvbyu4Zw.js → chunk-7R4GIKGN-Dv-4cAYn.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-CtqKiH4q.js → chunk-C72U2L5F-D21mS_6G.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-Cpr87sBR.js → chunk-EGIJ26TM-DzqmU2Z7.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-D23YVTOU.js → chunk-FMBD7UC4-DXncblvW.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-tDjHsAUs.js → chunk-GEFDOKGD-D-pKjlVd.js} +1 -1
- package/dist/web/assets/chunk-GLR3WWYH-DKikpoJM.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-C7vxA5i9.js +1 -0
- package/dist/web/assets/{chunk-JSJVCQXG-BBmymCjA.js → chunk-JSJVCQXG-99JzIdPr.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-DP36BDiU.js → chunk-KX2RTZJC-CRq1OBZv.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-Djw13C-3.js → chunk-KYZI473N-Bb0MCaIO.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL-HG_eMj_C.js → chunk-L3YUKLVL-C7qGJrfV.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-C2UEioMs.js → chunk-MX3YWQON-BpS_PtKp.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-DXUTQ-BL.js → chunk-NQ4KR5QH-z_blpjxi.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-BsUWb9d0.js → chunk-O4XLMI2P-nDhi_cVu.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-rG0P22U9.js → chunk-OZEHJAEY-BXhYx3nO.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-DX0xW7kO.js → chunk-PQ6SQG4A-TF58UVMU.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-C7Gry6md.js → chunk-PU5JKC2W-ek7k4QVB.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-CMY0PkRK.js → chunk-R5LLSJPH-CFwSJijQ.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-CXuQvlyu.js → chunk-WL4C6EOR-ByUrSRin.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-DRJEb7Zb.js → chunk-XIRO2GV7-Djlmrely.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-BPEX8KhL.js → chunk-XPW4576I-BPQQBakK.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-Cb0iqycX.js → chunk-XZSTWKYB-DxAOx4hG.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-av5aeHLq.js → chunk-YBOYWFTD-rQG3QH5s.js} +1 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-BA8Nj-_C.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-DjYu-6mn.js +1 -0
- package/dist/web/assets/clone-LRxlvnMj.js +1 -0
- package/dist/web/assets/code-editor-BiGbDwge.js +5 -0
- package/dist/web/assets/{cose-bilkent-S5V4N54A-qudEiMCT.js → cose-bilkent-S5V4N54A-B_AWZsOP.js} +1 -1
- package/dist/web/assets/csv-parser-CNNw2RVA.js +6 -0
- package/dist/web/assets/{csv-preview-sx6DC51G.js → csv-preview-D2pJJj3K.js} +3 -8
- package/dist/web/assets/{dagre-BFcnKyBF.js → dagre-DHq9bhnd.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-C3O-MTLf.js → dagre-KLK3FWXG-BdJr7Byp.js} +1 -1
- package/dist/web/assets/database-viewer-NyjI--ui.js +7 -0
- package/dist/web/assets/{diagram-E7M64L7V-DxPjK7_c.js → diagram-E7M64L7V-_db4pBVA.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-sqTog_XV.js → diagram-IFDJBPK2-xKoeuiJx.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO-hzmp0GHK.js → diagram-P4PSJMXO-C8tjJsev.js} +1 -1
- package/dist/web/assets/diff-viewer-p2tGGtgk.js +4 -0
- package/dist/web/assets/dist-CbZVY8RS.js +1 -0
- package/dist/web/assets/{erDiagram-INFDFZHY-DLeYhAAT.js → erDiagram-INFDFZHY-BSh2z9Df.js} +1 -1
- package/dist/web/assets/es2015-CFSGWybX.js +41 -0
- package/dist/web/assets/{extension-webview-2XjXXtyy.js → extension-webview-CUMHM0dE.js} +1 -1
- package/dist/web/assets/{flowDiagram-PKNHOUZH-CRxlE9Sr.js → flowDiagram-PKNHOUZH-oYaovqyp.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-BdjmoMLS.js → ganttDiagram-A5KZAMGK-DmL26q2P.js} +1 -1
- package/dist/web/assets/git-graph-mKama5oa.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-BeHSX7kk.js → gitGraphDiagram-K3NZZRJ6-CMoukSrY.js} +1 -1
- package/dist/web/assets/{graphlib-Duh_bWLa.js → graphlib-BcsNnGcW.js} +1 -1
- package/dist/web/assets/index-BoNuMDkJ.css +2 -0
- package/dist/web/assets/index-KxgzhD-N.js +30 -0
- package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-DWwumDkq.js +2 -0
- package/dist/web/assets/{isEmpty-B9L-Ge-H.js → isEmpty-bnrF3Qbc.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-Cu0Rt1Ok.js → ishikawaDiagram-PHBUUO56-D05_LyL7.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-CgDI-UG4.js → journeyDiagram-4ABVD52K-B_L20qMe.js} +1 -1
- package/dist/web/assets/{kanban-definition-K7BYSVSG-h4g10UHL.js → kanban-definition-K7BYSVSG-CZ535BbZ.js} +1 -1
- package/dist/web/assets/keybindings-store-BqfuJQDT.js +1 -0
- package/dist/web/assets/{line-B75-Rx70.js → line-CVvo3dRu.js} +1 -1
- package/dist/web/assets/{linear-Bcjv9FQt.js → linear-DP4mkX3m.js} +1 -1
- package/dist/web/assets/{markdown-renderer-Bb7OSpxF.js → markdown-renderer-NhtCBdv0.js} +5 -5
- package/dist/web/assets/{mermaid-parser.core-8u2leTXI.js → mermaid-parser.core-C7UwoIh6.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-BaOBwb-W.js → mindmap-definition-YRQLILUH-x0MTutJp.js} +1 -1
- package/dist/web/assets/{ordinal-LFEjVtwQ.js → ordinal-_K3x1fkz.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +1 -0
- package/dist/web/assets/{pieDiagram-SKSYHLDU-At5Kz0KK.js → pieDiagram-SKSYHLDU-C1Gjrtzy.js} +1 -1
- package/dist/web/assets/{port-forwarding-tab-bD8MKumH.js → port-forwarding-tab-DCeyw77P.js} +1 -1
- package/dist/web/assets/postgres-viewer-DSxa4fF1.js +13 -0
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-CdjGIDfw.js → quadrantDiagram-337W2JSQ-C8bzJCjQ.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +1 -0
- package/dist/web/assets/{requirementDiagram-Z7DCOOCP-B9F_Cx_p.js → requirementDiagram-Z7DCOOCP-pQyah6WB.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-RolPi8bU.js → sankeyDiagram-WA2Y5GQK-T6RgG-N8.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-DM-tMAhx.js → sequenceDiagram-2WXFIKYE-BQDJ4CVs.js} +1 -1
- package/dist/web/assets/settings-tab-9NIbwd-q.js +1 -0
- package/dist/web/assets/sql-completion-provider-DM9Qov6L.js +1 -0
- package/dist/web/assets/sql-query-editor-CMcXl1UJ.js +3 -0
- package/dist/web/assets/sqlite-viewer-DLBgkBb1.js +1 -0
- package/dist/web/assets/{stateDiagram-RAJIS63D-C4EMl6jf.js → stateDiagram-RAJIS63D-66vhiIuk.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-BGVqj_g9.js +1 -0
- package/dist/web/assets/{terminal-tab-Cq6vQ9W9.js → terminal-tab-XuU6mUdf.js} +2 -2
- package/dist/web/assets/text-wrap-BWNOVswA.js +1 -0
- package/dist/web/assets/{timeline-definition-YZTLITO2-A4PN_Efm.js → timeline-definition-YZTLITO2-DwZqB3nn.js} +1 -1
- package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +1 -0
- package/dist/web/assets/use-monaco-theme-BWw-hkx4.js +11 -0
- package/dist/web/assets/{vennDiagram-LZ73GAT5-ywK7LMaH.js → vennDiagram-LZ73GAT5-s9Z71fz-.js} +1 -1
- package/dist/web/assets/{xychartDiagram-JWTSCODW-DylHYNtJ.js → xychartDiagram-JWTSCODW-DRa_TH4B.js} +1 -1
- package/dist/web/index.html +7 -6
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/cli/commands/bot-cmd.ts +4 -0
- package/src/cli/commands/cloud.ts +1 -65
- package/src/cli/commands/db-cmd.ts +4 -3
- package/src/server/routes/database.ts +126 -9
- package/src/services/cloud.service.ts +2 -1
- package/src/services/database/sqlite-adapter.ts +1 -0
- package/src/services/db.service.ts +42 -3
- package/src/services/postgres.service.ts +37 -6
- package/src/services/sqlite.service.ts +18 -4
- package/src/services/table-cache.service.ts +2 -3
- package/src/types/database.ts +2 -0
- package/src/web/components/database/connection-form-dialog.tsx +17 -8
- package/src/web/components/database/connection-list.tsx +191 -139
- package/src/web/components/database/data-grid.tsx +714 -0
- package/src/web/components/database/database-sidebar.tsx +4 -1
- package/src/web/components/database/database-viewer.tsx +204 -225
- package/src/web/components/database/export-button.tsx +100 -0
- package/src/web/components/database/sql-completion-provider.ts +301 -0
- package/src/web/components/database/sql-query-editor.tsx +123 -0
- package/src/web/components/database/use-connections.ts +21 -1
- package/src/web/components/database/use-database.ts +59 -7
- package/src/web/components/editor/code-editor.tsx +176 -12
- package/src/web/components/sqlite/sqlite-query-editor.tsx +3 -90
- package/src/web/components/sqlite/sqlite-viewer.tsx +0 -2
- package/src/web/components/sqlite/use-sqlite.ts +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-CFzkFKEL.js +0 -1
- package/dist/web/assets/channel-C2fMafck.js +0 -1
- package/dist/web/assets/chat-tab-CfdMDCBK.js +0 -10
- package/dist/web/assets/chunk-GLR3WWYH-DBdWQ3zy.js +0 -2
- package/dist/web/assets/chunk-HHEYEP7N-BBw_z0fW.js +0 -1
- package/dist/web/assets/chunk-QZHKN3VN-DFKFM_C1.js +0 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-Dp4Kk3Yb.js +0 -1
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-D8IvcV_B.js +0 -1
- package/dist/web/assets/clone-B2hUek6n.js +0 -1
- package/dist/web/assets/code-editor-DhCfJIpG.js +0 -2
- package/dist/web/assets/database-viewer-DLkAUBpm.js +0 -1
- package/dist/web/assets/diff-viewer-DWVWsekJ.js +0 -4
- package/dist/web/assets/dist-C40JmyoH.js +0 -13
- package/dist/web/assets/dist-DRTW9IWi.js +0 -41
- package/dist/web/assets/git-graph-Ds5bs1cM.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-CNlas3Rz.js +0 -1
- package/dist/web/assets/index-8b0LM6IC.js +0 -30
- package/dist/web/assets/index-BnMECpW3.css +0 -2
- package/dist/web/assets/info-3K5VOQVL-BDzTLc11.js +0 -1
- package/dist/web/assets/infoDiagram-LFFYTUFH-ZZmpgc6t.js +0 -2
- package/dist/web/assets/keybindings-store-C06Z0Zhk.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-IVa5F-go.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-CvXHKAzp.js +0 -1
- package/dist/web/assets/postgres-viewer-C3OND65T.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-Z-Tr5wtS.js +0 -1
- package/dist/web/assets/settings-tab-cuMIkUNV.js +0 -1
- package/dist/web/assets/sqlite-viewer-CpjnwDtk.js +0 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-B-UjZch3.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-C9TYRE0k.js +0 -1
- package/dist/web/assets/use-monaco-theme-BH9sQ-Yu.js +0 -11
- /package/dist/web/assets/{api-client-BKIT_Qeg.js → api-client-BfBM3I7n.js} +0 -0
- /package/dist/web/assets/{array-DqLCdDFv.js → array-B9UHiPd-.js} +0 -0
- /package/dist/web/assets/{cytoscape.esm-CWPXKqbJ.js → cytoscape.esm-BW-DbntU.js} +0 -0
- /package/dist/web/assets/{defaultLocale-CrJzLgRD.js → defaultLocale-5eAKkKJC.js} +0 -0
- /package/dist/web/assets/{dist-Cep75xXf.js → dist-CSJdAyA9.js} +0 -0
- /package/dist/web/assets/{init-C0r9Gk5G.js → init-DlZdxViB.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-CGBoxvCD.js → isArrayLikeObject-B_v2FtYn.js} +0 -0
- /package/dist/web/assets/{katex-DzXRfQ_m.js → katex-Bqvo_ZG0.js} +0 -0
- /package/dist/web/assets/{lib-mag4ySk-.js → lib-DurwGtQO.js} +0 -0
- /package/dist/web/assets/{math-y9zN1W-N.js → math-069Z4SuC.js} +0 -0
- /package/dist/web/assets/{path-DIKpVbHL.js → path-6uRLdFF7.js} +0 -0
- /package/dist/web/assets/{rough.esm-nHaDi0Kw.js → rough.esm-JX0wREDd.js} +0 -0
- /package/dist/web/assets/{src-Dw4QhedI.js → src-BqX54PbV.js} +0 -0
- /package/dist/web/assets/{utils-DMiycH3O.js → utils-BNytJOb1.js} +0 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import type * as MonacoType from "monaco-editor";
|
|
2
|
+
|
|
3
|
+
export interface SchemaInfo {
|
|
4
|
+
tables: { name: string; schema: string }[];
|
|
5
|
+
getColumns: (table: string, schema?: string) => Promise<{ name: string; type: string }[]>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const SQL_KEYWORDS = [
|
|
9
|
+
"SELECT", "FROM", "WHERE", "INSERT", "INTO", "VALUES",
|
|
10
|
+
"UPDATE", "SET", "DELETE", "CREATE", "TABLE", "ALTER",
|
|
11
|
+
"DROP", "INDEX", "JOIN", "LEFT", "RIGHT", "INNER",
|
|
12
|
+
"OUTER", "ON", "AND", "OR", "NOT", "NULL", "IS",
|
|
13
|
+
"IN", "LIKE", "BETWEEN", "HAVING", "LIMIT", "OFFSET",
|
|
14
|
+
"AS", "DISTINCT", "COUNT", "SUM", "AVG", "MIN", "MAX",
|
|
15
|
+
"CASE", "WHEN", "THEN", "ELSE", "END", "EXISTS",
|
|
16
|
+
"UNION", "ALL", "ASC", "DESC", "ORDER BY", "GROUP BY",
|
|
17
|
+
"LEFT JOIN", "RIGHT JOIN", "INNER JOIN", "CROSS JOIN",
|
|
18
|
+
"FULL OUTER JOIN", "IS NULL", "IS NOT NULL",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export const AGGREGATE_FNS = ["COUNT", "SUM", "AVG", "MIN", "MAX"];
|
|
22
|
+
export const OPERATORS = ["=", "!=", "<>", ">", "<", ">=", "<=", "LIKE", "ILIKE", "IN", "NOT IN", "BETWEEN", "IS NULL", "IS NOT NULL"];
|
|
23
|
+
export const SORT_DIRS = ["ASC", "DESC"];
|
|
24
|
+
|
|
25
|
+
/** Client-side column cache to avoid redundant fetches */
|
|
26
|
+
const columnCache = new Map<string, { name: string; type: string }[]>();
|
|
27
|
+
|
|
28
|
+
const TABLE_KW_SET = new Set([
|
|
29
|
+
"WHERE", "SET", "ON", "ORDER", "GROUP", "HAVING", "LIMIT", "LEFT", "RIGHT",
|
|
30
|
+
"INNER", "OUTER", "CROSS", "FULL", "JOIN", "AND", "OR", "VALUES", "SELECT",
|
|
31
|
+
"FROM", "INTO", "UPDATE", "DELETE", "INSERT", "CREATE", "ALTER", "DROP",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
/** Extract tables referenced in FROM/JOIN/UPDATE/INTO clauses */
|
|
35
|
+
export function extractTableRefs(text: string) {
|
|
36
|
+
const tableRefs = new Set<string>();
|
|
37
|
+
const aliasMap = new Map<string, string>(); // alias → realTableName
|
|
38
|
+
// Match: FROM/JOIN/UPDATE/INTO "tablename" [AS] alias
|
|
39
|
+
// Use non-greedy alias capture to avoid consuming the next keyword
|
|
40
|
+
const matches = text.matchAll(/\b(?:FROM|JOIN|UPDATE|INTO)\s+"?(\w+)"?(?:\s+AS\s+(\w+)|\s+(?!(?:FROM|JOIN|UPDATE|INTO|WHERE|SET|ON|ORDER|GROUP|HAVING|LIMIT|LEFT|RIGHT|INNER|OUTER|CROSS|FULL|AND|OR|VALUES|SELECT)\b)(\w+))?/gi);
|
|
41
|
+
for (const m of matches) {
|
|
42
|
+
const tbl = m[1]!;
|
|
43
|
+
tableRefs.add(tbl);
|
|
44
|
+
const alias = m[2] ?? m[3]; // m[2] = explicit AS alias, m[3] = implicit alias
|
|
45
|
+
if (alias && !TABLE_KW_SET.has(alias.toUpperCase())) {
|
|
46
|
+
aliasMap.set(alias.toLowerCase(), tbl);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { tableRefs, aliasMap };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Resolve alias or table name to real table name */
|
|
53
|
+
export function resolveTable(name: string, aliasMap: Map<string, string>): string {
|
|
54
|
+
return aliasMap.get(name.toLowerCase()) ?? name;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Fetch columns for a table (cached) */
|
|
58
|
+
async function getColumns(tableName: string, schemaInfo: SchemaInfo): Promise<{ name: string; type: string }[]> {
|
|
59
|
+
const key = tableName.toLowerCase();
|
|
60
|
+
let cols = columnCache.get(key);
|
|
61
|
+
if (!cols) {
|
|
62
|
+
try {
|
|
63
|
+
cols = await schemaInfo.getColumns(tableName);
|
|
64
|
+
columnCache.set(key, cols);
|
|
65
|
+
} catch { cols = []; }
|
|
66
|
+
}
|
|
67
|
+
return cols;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Build column suggestions from all referenced tables */
|
|
71
|
+
async function columnSuggestions(
|
|
72
|
+
tableRefs: Set<string>,
|
|
73
|
+
schemaInfo: SchemaInfo,
|
|
74
|
+
monaco: typeof MonacoType,
|
|
75
|
+
range: MonacoType.IRange,
|
|
76
|
+
): Promise<MonacoType.languages.CompletionItem[]> {
|
|
77
|
+
const items: MonacoType.languages.CompletionItem[] = [];
|
|
78
|
+
const seen = new Set<string>();
|
|
79
|
+
for (const tbl of tableRefs) {
|
|
80
|
+
const cols = await getColumns(tbl, schemaInfo);
|
|
81
|
+
for (const col of cols) {
|
|
82
|
+
if (seen.has(col.name)) continue;
|
|
83
|
+
seen.add(col.name);
|
|
84
|
+
const needsQuote = /[A-Z]/.test(col.name);
|
|
85
|
+
items.push({
|
|
86
|
+
label: col.name,
|
|
87
|
+
kind: monaco.languages.CompletionItemKind.Field,
|
|
88
|
+
detail: `${tbl} · ${col.type}`,
|
|
89
|
+
insertText: needsQuote ? `"${col.name}"` : col.name,
|
|
90
|
+
range,
|
|
91
|
+
sortText: "0" + col.name,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return items;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Determine the SQL completion context from text before cursor.
|
|
100
|
+
* Returns a context tag used to decide which suggestions to show.
|
|
101
|
+
* Exported for testing.
|
|
102
|
+
*/
|
|
103
|
+
export function getCompletionContext(textUntilPosition: string): string {
|
|
104
|
+
// 1. After "alias." or "table." → dot completion
|
|
105
|
+
if (/(\w+)\.\s*$/.test(textUntilPosition)) return "dot";
|
|
106
|
+
|
|
107
|
+
// 2. After ORDER BY col or GROUP BY col → direction (ASC/DESC)
|
|
108
|
+
// Pattern: ORDER BY <col> <partial_word> — but NOT if partial is already ASC/DESC
|
|
109
|
+
const orderByColMatch = textUntilPosition.match(/\b(?:ORDER|GROUP)\s+BY\s+(?:[\w"]+\s+(?:ASC|DESC)\s*,\s*)*[\w"]+\s+(\w*)$/i);
|
|
110
|
+
if (orderByColMatch) {
|
|
111
|
+
const partial = orderByColMatch[1]!.toUpperCase();
|
|
112
|
+
if (partial === "ASC" || partial === "DESC") return "after-direction";
|
|
113
|
+
return "sort-direction";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 3. After ORDER BY col ASC/DESC, → more columns after comma
|
|
117
|
+
if (/\b(?:ORDER|GROUP)\s+BY\s+.*(?:ASC|DESC)\s*,\s*\w*$/i.test(textUntilPosition)) return "order-by-next-col";
|
|
118
|
+
|
|
119
|
+
// 4. After WHERE/AND/OR <col> → operators
|
|
120
|
+
if (/\b(?:WHERE|AND|OR)\s+[\w"]+\s+\S*$/i.test(textUntilPosition)) return "operator";
|
|
121
|
+
|
|
122
|
+
// 5. After FROM/JOIN/INTO/UPDATE/TABLE → table names
|
|
123
|
+
if (/\b(?:FROM|JOIN|INTO|UPDATE|TABLE)\s+\w*$/i.test(textUntilPosition)) return "table";
|
|
124
|
+
|
|
125
|
+
// 6. After INSERT INTO table ( → columns for insert
|
|
126
|
+
if (/\bINSERT\s+INTO\s+[\w"]+\s*\(\s*(?:[\w"]+\s*,\s*)*\w*$/i.test(textUntilPosition)) return "insert-cols";
|
|
127
|
+
|
|
128
|
+
// 7. After SELECT/WHERE/ORDER BY/GROUP BY/HAVING/SET/ON/AND/OR → columns
|
|
129
|
+
if (/\b(?:SELECT|WHERE|ORDER\s+BY|GROUP\s+BY|HAVING|SET|ON|AND|OR)\s+(?:[\w"]+\s*,\s*)*\w*$/i.test(textUntilPosition)) return "columns";
|
|
130
|
+
|
|
131
|
+
// 8. After comma with table refs → more columns
|
|
132
|
+
if (/,\s*\w*$/.test(textUntilPosition)) return "comma-cols";
|
|
133
|
+
|
|
134
|
+
// 9. Default
|
|
135
|
+
return "default";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function createSqlCompletionProvider(
|
|
139
|
+
monaco: typeof MonacoType,
|
|
140
|
+
schemaInfo: SchemaInfo,
|
|
141
|
+
): MonacoType.languages.CompletionItemProvider {
|
|
142
|
+
return {
|
|
143
|
+
triggerCharacters: [".", ","],
|
|
144
|
+
provideCompletionItems: async (model, position) => {
|
|
145
|
+
try {
|
|
146
|
+
const textUntilPosition = model.getValueInRange({
|
|
147
|
+
startLineNumber: 1, startColumn: 1,
|
|
148
|
+
endLineNumber: position.lineNumber, endColumn: position.column,
|
|
149
|
+
});
|
|
150
|
+
const word = model.getWordUntilPosition(position);
|
|
151
|
+
const range: MonacoType.IRange = {
|
|
152
|
+
startLineNumber: position.lineNumber, endLineNumber: position.lineNumber,
|
|
153
|
+
startColumn: word.startColumn, endColumn: word.endColumn,
|
|
154
|
+
};
|
|
155
|
+
const fullText = model.getValue();
|
|
156
|
+
const { tableRefs, aliasMap } = extractTableRefs(fullText);
|
|
157
|
+
const suggestions: MonacoType.languages.CompletionItem[] = [];
|
|
158
|
+
const ctx = getCompletionContext(textUntilPosition);
|
|
159
|
+
|
|
160
|
+
// ─── 1. After "alias." or "table." → columns of that table ───
|
|
161
|
+
if (ctx === "dot") {
|
|
162
|
+
const dotMatch = textUntilPosition.match(/(\w+)\.\s*$/);
|
|
163
|
+
if (dotMatch) {
|
|
164
|
+
const ref = dotMatch[1]!;
|
|
165
|
+
const realTable = resolveTable(ref, aliasMap);
|
|
166
|
+
const cols = await getColumns(realTable, schemaInfo);
|
|
167
|
+
for (const col of cols) {
|
|
168
|
+
const needsQuote = /[A-Z]/.test(col.name);
|
|
169
|
+
suggestions.push({
|
|
170
|
+
label: col.name,
|
|
171
|
+
kind: monaco.languages.CompletionItemKind.Field,
|
|
172
|
+
detail: col.type,
|
|
173
|
+
insertText: needsQuote ? `"${col.name}"` : col.name,
|
|
174
|
+
range,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { suggestions };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ─── 2. After ORDER BY col → ASC, DESC ───
|
|
182
|
+
if (ctx === "sort-direction") {
|
|
183
|
+
for (const dir of SORT_DIRS) {
|
|
184
|
+
suggestions.push({
|
|
185
|
+
label: dir,
|
|
186
|
+
kind: monaco.languages.CompletionItemKind.Keyword,
|
|
187
|
+
insertText: dir,
|
|
188
|
+
range,
|
|
189
|
+
sortText: "0" + dir,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return { suggestions };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── 3. After ASC/DESC → nothing special ───
|
|
196
|
+
if (ctx === "after-direction") return { suggestions: [] };
|
|
197
|
+
|
|
198
|
+
// ─── 4. After ORDER BY col ASC/DESC, → more columns ───
|
|
199
|
+
if (ctx === "order-by-next-col") {
|
|
200
|
+
suggestions.push(...await columnSuggestions(tableRefs, schemaInfo, monaco, range));
|
|
201
|
+
return { suggestions };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ─── 5. After WHERE/AND/OR col → operators ───
|
|
205
|
+
if (ctx === "operator") {
|
|
206
|
+
for (const op of OPERATORS) {
|
|
207
|
+
suggestions.push({
|
|
208
|
+
label: op,
|
|
209
|
+
kind: monaco.languages.CompletionItemKind.Operator,
|
|
210
|
+
insertText: op,
|
|
211
|
+
range,
|
|
212
|
+
sortText: "0" + op,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return { suggestions };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ─── 6. After FROM/JOIN/INTO/UPDATE/TABLE → table names ───
|
|
219
|
+
if (ctx === "table") {
|
|
220
|
+
for (const t of schemaInfo.tables) {
|
|
221
|
+
suggestions.push({
|
|
222
|
+
label: t.name,
|
|
223
|
+
kind: monaco.languages.CompletionItemKind.Struct,
|
|
224
|
+
detail: t.schema,
|
|
225
|
+
insertText: t.name,
|
|
226
|
+
range,
|
|
227
|
+
sortText: "0" + t.name,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return { suggestions };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ─── 7. After INSERT INTO table ( → columns ───
|
|
234
|
+
if (ctx === "insert-cols") {
|
|
235
|
+
suggestions.push(...await columnSuggestions(tableRefs, schemaInfo, monaco, range));
|
|
236
|
+
return { suggestions };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ─── 8. After SELECT/WHERE/ORDER BY/... → columns + keywords ───
|
|
240
|
+
if (ctx === "columns") {
|
|
241
|
+
suggestions.push(...await columnSuggestions(tableRefs, schemaInfo, monaco, range));
|
|
242
|
+
if (/\bSELECT\s+/i.test(textUntilPosition)) {
|
|
243
|
+
suggestions.push({
|
|
244
|
+
label: "*",
|
|
245
|
+
kind: monaco.languages.CompletionItemKind.Value,
|
|
246
|
+
insertText: "*",
|
|
247
|
+
range,
|
|
248
|
+
sortText: "00*",
|
|
249
|
+
});
|
|
250
|
+
for (const fn of AGGREGATE_FNS) {
|
|
251
|
+
suggestions.push({
|
|
252
|
+
label: `${fn}()`,
|
|
253
|
+
kind: monaco.languages.CompletionItemKind.Function,
|
|
254
|
+
insertText: `${fn}($0)`,
|
|
255
|
+
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
256
|
+
range,
|
|
257
|
+
sortText: "1" + fn,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
for (const kw of SQL_KEYWORDS) {
|
|
262
|
+
suggestions.push({
|
|
263
|
+
label: kw, kind: monaco.languages.CompletionItemKind.Keyword,
|
|
264
|
+
insertText: kw, range, sortText: "3" + kw,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return { suggestions };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ─── 9. After comma → more columns ───
|
|
271
|
+
if (ctx === "comma-cols" && tableRefs.size > 0) {
|
|
272
|
+
suggestions.push(...await columnSuggestions(tableRefs, schemaInfo, monaco, range));
|
|
273
|
+
return { suggestions };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ─── 10. Default: keywords + table names ───
|
|
277
|
+
for (const kw of SQL_KEYWORDS) {
|
|
278
|
+
suggestions.push({
|
|
279
|
+
label: kw, kind: monaco.languages.CompletionItemKind.Keyword,
|
|
280
|
+
insertText: kw, range, sortText: "2" + kw,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
for (const t of schemaInfo.tables) {
|
|
284
|
+
suggestions.push({
|
|
285
|
+
label: t.name, kind: monaco.languages.CompletionItemKind.Struct,
|
|
286
|
+
detail: t.schema, insertText: t.name, range, sortText: "1" + t.name,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return { suggestions };
|
|
290
|
+
} catch {
|
|
291
|
+
// Never let the provider throw — Monaco silently falls back to word-based suggestions
|
|
292
|
+
return { suggestions: [] };
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Clear the internal column cache (call on connection change) */
|
|
299
|
+
export function clearCompletionCache() {
|
|
300
|
+
columnCache.clear();
|
|
301
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from "react";
|
|
2
|
+
import Editor, { type OnMount } from "@monaco-editor/react";
|
|
3
|
+
import type * as MonacoType from "monaco-editor";
|
|
4
|
+
import { useMonacoTheme } from "@/lib/use-monaco-theme";
|
|
5
|
+
import { createSqlCompletionProvider, clearCompletionCache, type SchemaInfo } from "./sql-completion-provider";
|
|
6
|
+
|
|
7
|
+
interface SqlQueryEditorProps {
|
|
8
|
+
onExecute: (sql: string) => void;
|
|
9
|
+
loading: boolean;
|
|
10
|
+
defaultValue?: string;
|
|
11
|
+
schemaInfo?: SchemaInfo;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Find the SQL statement surrounding the cursor line (split by ;) */
|
|
15
|
+
export function getStatementAtCursor(text: string, cursorLine: number): string {
|
|
16
|
+
const lines = text.split("\n");
|
|
17
|
+
// Find statement boundaries (lines where a statement ends with ;)
|
|
18
|
+
let stmtStart = 0;
|
|
19
|
+
for (let i = 0; i < lines.length; i++) {
|
|
20
|
+
const trimmed = lines[i]!.trim();
|
|
21
|
+
if (i < cursorLine - 1 && trimmed.endsWith(";")) {
|
|
22
|
+
stmtStart = i + 1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Find statement end
|
|
26
|
+
let stmtEnd = lines.length - 1;
|
|
27
|
+
for (let i = cursorLine - 1; i < lines.length; i++) {
|
|
28
|
+
const trimmed = lines[i]!.trim();
|
|
29
|
+
if (trimmed.endsWith(";")) {
|
|
30
|
+
stmtEnd = i;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Skip leading empty/comment lines
|
|
35
|
+
while (stmtStart <= stmtEnd) {
|
|
36
|
+
const t = lines[stmtStart]!.trim();
|
|
37
|
+
if (t && !t.startsWith("--")) break;
|
|
38
|
+
stmtStart++;
|
|
39
|
+
}
|
|
40
|
+
return lines.slice(stmtStart, stmtEnd + 1).join("\n").trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Shared Monaco-based SQL query editor (editor only, no results) */
|
|
44
|
+
export function SqlQueryEditor({ onExecute, loading, defaultValue = "SELECT * FROM ", schemaInfo }: SqlQueryEditorProps) {
|
|
45
|
+
const [query, setQuery] = useState(defaultValue);
|
|
46
|
+
const editorRef = useRef<MonacoType.editor.IStandaloneCodeEditor | null>(null);
|
|
47
|
+
const monacoRef = useRef<typeof MonacoType | null>(null);
|
|
48
|
+
const disposableRef = useRef<MonacoType.IDisposable | null>(null);
|
|
49
|
+
const onExecuteRef = useRef(onExecute);
|
|
50
|
+
onExecuteRef.current = onExecute;
|
|
51
|
+
const monacoTheme = useMonacoTheme();
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!monacoRef.current || !schemaInfo) return;
|
|
55
|
+
disposableRef.current?.dispose();
|
|
56
|
+
clearCompletionCache();
|
|
57
|
+
disposableRef.current = monacoRef.current.languages.registerCompletionItemProvider(
|
|
58
|
+
"sql",
|
|
59
|
+
createSqlCompletionProvider(monacoRef.current, schemaInfo),
|
|
60
|
+
);
|
|
61
|
+
return () => { disposableRef.current?.dispose(); };
|
|
62
|
+
}, [schemaInfo]);
|
|
63
|
+
|
|
64
|
+
const handleMount: OnMount = useCallback((editor, monaco) => {
|
|
65
|
+
editorRef.current = editor;
|
|
66
|
+
monacoRef.current = monaco;
|
|
67
|
+
|
|
68
|
+
// Cmd/Ctrl+Enter: run statement at cursor
|
|
69
|
+
editor.addAction({
|
|
70
|
+
id: "run-query-at-cursor",
|
|
71
|
+
label: "Run Statement at Cursor",
|
|
72
|
+
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
|
|
73
|
+
run: (ed) => {
|
|
74
|
+
const pos = ed.getPosition();
|
|
75
|
+
if (!pos) return;
|
|
76
|
+
const text = ed.getValue();
|
|
77
|
+
const stmt = getStatementAtCursor(text, pos.lineNumber);
|
|
78
|
+
if (stmt) onExecuteRef.current(stmt);
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (schemaInfo) {
|
|
83
|
+
disposableRef.current?.dispose();
|
|
84
|
+
disposableRef.current = monaco.languages.registerCompletionItemProvider(
|
|
85
|
+
"sql",
|
|
86
|
+
createSqlCompletionProvider(monaco, schemaInfo),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}, [schemaInfo]);
|
|
90
|
+
|
|
91
|
+
useEffect(() => { setQuery(defaultValue); }, [defaultValue]);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className="h-full overflow-hidden">
|
|
95
|
+
<Editor
|
|
96
|
+
height="100%"
|
|
97
|
+
language="sql"
|
|
98
|
+
theme={monacoTheme}
|
|
99
|
+
value={query}
|
|
100
|
+
onChange={(v) => setQuery(v ?? "")}
|
|
101
|
+
onMount={handleMount}
|
|
102
|
+
options={{
|
|
103
|
+
minimap: { enabled: false },
|
|
104
|
+
lineNumbers: "off",
|
|
105
|
+
scrollBeyondLastLine: false,
|
|
106
|
+
wordWrap: "on",
|
|
107
|
+
fontSize: 12,
|
|
108
|
+
tabSize: 2,
|
|
109
|
+
renderLineHighlight: "none",
|
|
110
|
+
overviewRulerLanes: 0,
|
|
111
|
+
hideCursorInOverviewRuler: true,
|
|
112
|
+
scrollbar: { vertical: "auto", horizontal: "auto", verticalScrollbarSize: 6, horizontalScrollbarSize: 6 },
|
|
113
|
+
padding: { top: 4, bottom: 4 },
|
|
114
|
+
lineDecorationsWidth: 4,
|
|
115
|
+
lineNumbersMinChars: 0,
|
|
116
|
+
glyphMargin: false,
|
|
117
|
+
folding: false,
|
|
118
|
+
fixedOverflowWidgets: true,
|
|
119
|
+
}}
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -76,6 +76,13 @@ export function useConnections() {
|
|
|
76
76
|
return api.post(`/api/db/connections/${id}/test`);
|
|
77
77
|
}, []);
|
|
78
78
|
|
|
79
|
+
const testRawConnection = useCallback(async (
|
|
80
|
+
type: "sqlite" | "postgres",
|
|
81
|
+
connectionConfig: { type: string; path?: string; connectionString?: string },
|
|
82
|
+
): Promise<{ ok: boolean; error?: string }> => {
|
|
83
|
+
return api.post("/api/db/test", { type, connectionConfig });
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
79
86
|
const refreshTables = useCallback(async (id: number): Promise<void> => {
|
|
80
87
|
const raw = await api.get<{ name: string; schema: string; rowCount: number }[]>(`/api/db/connections/${id}/tables`);
|
|
81
88
|
const tables: CachedTable[] = raw.map((t) => ({
|
|
@@ -88,6 +95,19 @@ export function useConnections() {
|
|
|
88
95
|
setCachedTables((prev) => new Map(prev).set(id, tables));
|
|
89
96
|
}, []);
|
|
90
97
|
|
|
98
|
+
/** Fetch column metadata for a table (lazy loaded for schema tree) */
|
|
99
|
+
const [columnCache, setColumnCache] = useState<Map<string, { name: string; type: string; nullable: boolean; pk: boolean; fk: { table: string; column: string } | null }[]>>(new Map());
|
|
100
|
+
const fetchColumns = useCallback(async (connId: number, table: string, schema?: string): Promise<{ name: string; type: string; nullable: boolean; pk: boolean; fk: { table: string; column: string } | null }[]> => {
|
|
101
|
+
const cacheKey = `${connId}:${schema ?? "main"}.${table}`;
|
|
102
|
+
const cached = columnCache.get(cacheKey);
|
|
103
|
+
if (cached) return cached;
|
|
104
|
+
const cols = await api.get<{ name: string; type: string; nullable: boolean; pk: boolean; fk: { table: string; column: string } | null }[]>(
|
|
105
|
+
`/api/db/connections/${connId}/schema?table=${encodeURIComponent(table)}${schema ? `&schema=${encodeURIComponent(schema)}` : ""}`,
|
|
106
|
+
);
|
|
107
|
+
setColumnCache((prev) => new Map(prev).set(cacheKey, cols));
|
|
108
|
+
return cols;
|
|
109
|
+
}, [columnCache]);
|
|
110
|
+
|
|
91
111
|
const exportConnections = useCallback(async () => {
|
|
92
112
|
return api.get<{ version: number; exported_at: string; connections: unknown[] }>("/api/db/connections/export");
|
|
93
113
|
}, []);
|
|
@@ -100,5 +120,5 @@ export function useConnections() {
|
|
|
100
120
|
return result;
|
|
101
121
|
}, [fetchConnections]);
|
|
102
122
|
|
|
103
|
-
return { connections, loading, cachedTables, createConnection, updateConnection, deleteConnection, testConnection, refreshTables, exportConnections, importConnections };
|
|
123
|
+
return { connections, loading, cachedTables, columnCache, createConnection, updateConnection, deleteConnection, testConnection, testRawConnection, refreshTables, fetchColumns, exportConnections, importConnections };
|
|
104
124
|
}
|
|
@@ -3,7 +3,7 @@ import { api } from "@/lib/api-client";
|
|
|
3
3
|
|
|
4
4
|
export interface DbTableInfo { name: string; schema: string; rowCount: number }
|
|
5
5
|
export interface DbColumnInfo { name: string; type: string; nullable: boolean; pk: boolean; defaultValue: string | null }
|
|
6
|
-
export interface DbQueryResult { columns: string[]; rows: Record<string, unknown>[]; rowsAffected: number; changeType: "select" | "modify" }
|
|
6
|
+
export interface DbQueryResult { columns: string[]; rows: Record<string, unknown>[]; rowsAffected: number; changeType: "select" | "modify"; executionTimeMs?: number }
|
|
7
7
|
interface DbTableData { columns: string[]; rows: Record<string, unknown>[]; total: number; page: number; limit: number }
|
|
8
8
|
|
|
9
9
|
/** SessionStorage cache key for table data */
|
|
@@ -33,22 +33,28 @@ export function useDatabase(connectionId: number) {
|
|
|
33
33
|
const [selectedSchema, setSelectedSchema] = useState("public");
|
|
34
34
|
const [tableData, setTableData] = useState<DbTableData | null>(null);
|
|
35
35
|
const [schema, setSchema] = useState<DbColumnInfo[]>([]);
|
|
36
|
-
const [loading, setLoading] = useState(
|
|
36
|
+
const [loading, setLoading] = useState(false);
|
|
37
37
|
const [error, setError] = useState<string | null>(null);
|
|
38
38
|
const [page, setPageState] = useState(1);
|
|
39
39
|
const [queryResult, setQueryResult] = useState<DbQueryResult | null>(null);
|
|
40
40
|
const [queryError, setQueryError] = useState<string | null>(null);
|
|
41
41
|
const [queryLoading, setQueryLoading] = useState(false);
|
|
42
|
+
// Sort state
|
|
43
|
+
const [orderBy, setOrderBy] = useState<string | null>(null);
|
|
44
|
+
const [orderDir, setOrderDir] = useState<"ASC" | "DESC">("ASC");
|
|
42
45
|
|
|
43
46
|
// Fetch table data + schema for current selection
|
|
44
|
-
const fetchTableData = useCallback(async (table?: string, tableSchema?: string, p?: number) => {
|
|
47
|
+
const fetchTableData = useCallback(async (table?: string, tableSchema?: string, p?: number, sortCol?: string | null, sortDir?: "ASC" | "DESC") => {
|
|
45
48
|
const t = table ?? selectedTable;
|
|
46
49
|
const s = tableSchema ?? selectedSchema;
|
|
47
50
|
if (!t) return;
|
|
48
51
|
setLoading(true);
|
|
52
|
+
const ob = sortCol !== undefined ? sortCol : orderBy;
|
|
53
|
+
const od = sortDir ?? orderDir;
|
|
49
54
|
try {
|
|
55
|
+
const orderParams = ob ? `&orderBy=${encodeURIComponent(ob)}&orderDir=${od}` : "";
|
|
50
56
|
const [data, cols] = await Promise.all([
|
|
51
|
-
api.get<DbTableData>(`${base}/data?table=${encodeURIComponent(t)}&schema=${s}&page=${p ?? page}&limit=100`),
|
|
57
|
+
api.get<DbTableData>(`${base}/data?table=${encodeURIComponent(t)}&schema=${s}&page=${p ?? page}&limit=100${orderParams}`),
|
|
52
58
|
api.get<DbColumnInfo[]>(`${base}/schema?table=${encodeURIComponent(t)}&schema=${s}`),
|
|
53
59
|
]);
|
|
54
60
|
setTableData(data);
|
|
@@ -59,7 +65,7 @@ export function useDatabase(connectionId: number) {
|
|
|
59
65
|
} finally {
|
|
60
66
|
setLoading(false);
|
|
61
67
|
}
|
|
62
|
-
}, [base, connectionId, selectedTable, selectedSchema, page]);
|
|
68
|
+
}, [base, connectionId, selectedTable, selectedSchema, page, orderBy, orderDir]);
|
|
63
69
|
|
|
64
70
|
const selectTable = useCallback((name: string, tableSchema = "public") => {
|
|
65
71
|
setSelectedTable(name);
|
|
@@ -123,10 +129,56 @@ export function useDatabase(connectionId: number) {
|
|
|
123
129
|
}
|
|
124
130
|
}, [base, selectedTable, selectedSchema, fetchTableData]);
|
|
125
131
|
|
|
132
|
+
/** Toggle sort: none → ASC → DESC → none */
|
|
133
|
+
const toggleSort = useCallback((column: string) => {
|
|
134
|
+
let newCol: string | null;
|
|
135
|
+
let newDir: "ASC" | "DESC" = "ASC";
|
|
136
|
+
if (orderBy !== column) {
|
|
137
|
+
newCol = column; newDir = "ASC";
|
|
138
|
+
} else if (orderDir === "ASC") {
|
|
139
|
+
newCol = column; newDir = "DESC";
|
|
140
|
+
} else {
|
|
141
|
+
newCol = null; newDir = "ASC";
|
|
142
|
+
}
|
|
143
|
+
setOrderBy(newCol);
|
|
144
|
+
setOrderDir(newDir);
|
|
145
|
+
setPageState(1);
|
|
146
|
+
fetchTableData(undefined, undefined, 1, newCol, newDir);
|
|
147
|
+
}, [orderBy, orderDir, fetchTableData]);
|
|
148
|
+
|
|
149
|
+
/** Bulk delete rows */
|
|
150
|
+
const bulkDelete = useCallback(async (pkColumn: string, pkValues: unknown[]) => {
|
|
151
|
+
if (!selectedTable) return;
|
|
152
|
+
const t = selectedTable;
|
|
153
|
+
const s = selectedSchema;
|
|
154
|
+
try {
|
|
155
|
+
await api.post(`${base}/rows/delete`, { table: t, schema: s, pkColumn, pkValues });
|
|
156
|
+
fetchTableData(t, s);
|
|
157
|
+
} catch (e) {
|
|
158
|
+
setError((e as Error).message);
|
|
159
|
+
}
|
|
160
|
+
}, [base, selectedTable, selectedSchema, fetchTableData]);
|
|
161
|
+
|
|
162
|
+
/** Insert a new row */
|
|
163
|
+
const insertRow = useCallback(async (values: Record<string, unknown>) => {
|
|
164
|
+
if (!selectedTable) return;
|
|
165
|
+
const t = selectedTable;
|
|
166
|
+
const s = selectedSchema;
|
|
167
|
+
try {
|
|
168
|
+
await api.post(`${base}/row`, { table: t, schema: s, values });
|
|
169
|
+
fetchTableData(t, s);
|
|
170
|
+
} catch (e) {
|
|
171
|
+
setError((e as Error).message);
|
|
172
|
+
throw e;
|
|
173
|
+
}
|
|
174
|
+
}, [base, selectedTable, selectedSchema, fetchTableData]);
|
|
175
|
+
|
|
126
176
|
return {
|
|
127
|
-
selectedTable, selectTable, tableData, schema,
|
|
177
|
+
selectedTable, selectedSchema, selectTable, tableData, schema,
|
|
128
178
|
loading, error, page, setPage: changePage,
|
|
179
|
+
orderBy, orderDir, toggleSort,
|
|
129
180
|
queryResult, queryError, queryLoading, executeQuery,
|
|
130
|
-
updateCell, deleteRow,
|
|
181
|
+
updateCell, deleteRow, bulkDelete, insertRow,
|
|
182
|
+
refreshData: fetchTableData,
|
|
131
183
|
};
|
|
132
184
|
}
|