dsh-db-tool 0.1.3 → 0.1.5
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/client/client.js +52 -10
- package/dist/adapters/sql-shared/pg-like.js +71 -17
- package/dist/http/index.js +3 -0
- package/dist/manager.js +15 -0
- package/package.json +1 -1
- package/skills/db-admin/SKILL.md +1 -0
package/client/client.js
CHANGED
|
@@ -66,6 +66,7 @@ window.__ModuleLoader__.load({
|
|
|
66
66
|
modeRw: "读写",
|
|
67
67
|
// 浏览
|
|
68
68
|
noDatabases: "无可用数据库",
|
|
69
|
+
noSchemas: "无模式",
|
|
69
70
|
noTables: "无表",
|
|
70
71
|
loadFailed: "加载失败",
|
|
71
72
|
structure: "结构",
|
|
@@ -150,6 +151,7 @@ window.__ModuleLoader__.load({
|
|
|
150
151
|
modeRo: "read-only ro",
|
|
151
152
|
modeRw: "read-write rw",
|
|
152
153
|
noDatabases: "No databases available",
|
|
154
|
+
noSchemas: "No schemas",
|
|
153
155
|
noTables: "No tables",
|
|
154
156
|
loadFailed: "Failed to load",
|
|
155
157
|
structure: "Structure",
|
|
@@ -708,7 +710,8 @@ window.__ModuleLoader__.load({
|
|
|
708
710
|
const [loading, setLoading] = React.useState({});
|
|
709
711
|
const [error, setError] = React.useState({}); // 树节点加载失败信息(就地显示,可点重试)
|
|
710
712
|
const [dbs, setDbs] = React.useState({}); // connId -> string[]
|
|
711
|
-
const [
|
|
713
|
+
const [schemasMap, setSchemasMap] = React.useState({}); // "<connId>/<db>" -> string[](PG/GaussDB 库内 schema 层)
|
|
714
|
+
const [tablesMap, setTablesMap] = React.useState({}); // "<connId>/<db|schema>" -> TableInfo[]
|
|
712
715
|
const [sel, setSel] = React.useState(null); // {connId, db, table: TableInfo}
|
|
713
716
|
const [schema, setSchema] = React.useState([]);
|
|
714
717
|
const [preview, setPreview] = React.useState(null); // QueryResult
|
|
@@ -718,7 +721,7 @@ window.__ModuleLoader__.load({
|
|
|
718
721
|
|
|
719
722
|
// 会话项目切换 / 连接列表变化时清空树缓存,避免陈旧授权下的旧数据
|
|
720
723
|
React.useEffect(() => {
|
|
721
|
-
setOpen({}); setLoading({}); setError({}); setDbs({}); setTablesMap({}); setSel(null); setSchema([]); setPreview(null);
|
|
724
|
+
setOpen({}); setLoading({}); setError({}); setDbs({}); setSchemasMap({}); setTablesMap({}); setSel(null); setSchema([]); setPreview(null);
|
|
722
725
|
}, [projectPath]);
|
|
723
726
|
|
|
724
727
|
function toggle(key, load) {
|
|
@@ -744,22 +747,37 @@ window.__ModuleLoader__.load({
|
|
|
744
747
|
setDbs((m) => Object.assign({}, m, { [c.id]: list || [] }));
|
|
745
748
|
}));
|
|
746
749
|
}
|
|
750
|
+
// PG/GaussDB 官方层级为 数据库 → 模式(schema) → 表:库节点下先列 schema 再列表
|
|
751
|
+
const HAS_SCHEMAS = { postgresql: true, gaussdb: true };
|
|
747
752
|
function toggleDb(c, d) {
|
|
748
753
|
if (!projectPath) return;
|
|
754
|
+
const useSchemas = !!HAS_SCHEMAS[c.kind];
|
|
755
|
+
const what = useSchemas ? "schemas" : "tables";
|
|
749
756
|
toggle("d:" + c.id + "/" + d, () =>
|
|
750
|
-
api(
|
|
751
|
-
|
|
757
|
+
api(what + qs({ project: projectPath, connId: c.id, database: d })).then((list) => {
|
|
758
|
+
const setter = useSchemas ? setSchemasMap : setTablesMap;
|
|
759
|
+
setter((m) => Object.assign({}, m, { [c.id + "/" + d]: list || [] }));
|
|
760
|
+
}));
|
|
761
|
+
}
|
|
762
|
+
function toggleSchema(c, d, s) {
|
|
763
|
+
if (!projectPath) return;
|
|
764
|
+
// PG 系跨库浏览:tables 的 database 传 "库名.schema"(Navicat 官方行为,服务端按库开连接)
|
|
765
|
+
toggle("s:" + c.id + "/" + d + "/" + s, () =>
|
|
766
|
+
api("tables" + qs({ project: projectPath, connId: c.id, database: d + "." + s })).then((list) => {
|
|
767
|
+
setTablesMap((m) => Object.assign({}, m, { [c.id + "/" + d + "/" + s]: list || [] }));
|
|
752
768
|
}));
|
|
753
769
|
}
|
|
754
770
|
// 请求序号守卫:快速切换选中表/翻页时,丢弃晚到的旧响应,防止旧数据覆盖新选中项
|
|
755
771
|
const openSeq = React.useRef(0);
|
|
756
772
|
const openTable = React.useCallback((s, pg) => {
|
|
757
773
|
if (!s || !projectPath) return;
|
|
774
|
+
// PG 系跨库:database 传 "库名.schema";其它库传库名
|
|
775
|
+
const dbRef = s.schemaName ? s.db + "." + s.schemaName : s.db;
|
|
758
776
|
const seq = ++openSeq.current;
|
|
759
777
|
setBusy("open");
|
|
760
778
|
Promise.all([
|
|
761
|
-
api("schema" + qs({ project: projectPath, connId: s.connId, database:
|
|
762
|
-
api("preview" + qs({ project: projectPath, connId: s.connId, database:
|
|
779
|
+
api("schema" + qs({ project: projectPath, connId: s.connId, database: dbRef, table: s.table.name })),
|
|
780
|
+
api("preview" + qs({ project: projectPath, connId: s.connId, database: dbRef, table: s.table.name, limit: PAGE_SIZE, offset: ((pg || 1) - 1) * PAGE_SIZE })),
|
|
763
781
|
])
|
|
764
782
|
.then(([sch, prev]) => {
|
|
765
783
|
if (seq !== openSeq.current) return; // 旧请求晚到,丢弃
|
|
@@ -795,19 +813,43 @@ window.__ModuleLoader__.load({
|
|
|
795
813
|
if (!list) continue;
|
|
796
814
|
if (list.length === 0) { treeRows.push(treerow(ck + ":e", 1, false, true, t("noDatabases"))); continue; }
|
|
797
815
|
for (const d of list) {
|
|
816
|
+
const useSchemas = !!HAS_SCHEMAS[c.kind];
|
|
798
817
|
const dk = "d:" + c.id + "/" + d;
|
|
799
818
|
const dOpen = !!open[dk];
|
|
800
819
|
treeRows.push(treerow(dk, 1, dOpen, false, d, () => toggleDb(c, d)));
|
|
801
820
|
if (!dOpen) continue;
|
|
802
|
-
const tkey = c.id + "/" + d;
|
|
803
|
-
const tlist = tablesMap[tkey];
|
|
804
821
|
if (loading[dk]) { treeRows.push(treerow(dk + ":l", 2, false, true, "…")); continue; }
|
|
805
822
|
if (error[dk]) { treeRows.push(treerow(dk + ":x", 2, false, true, t("loadFailed") + ":" + error[dk], () => retry(dk, () => toggleDb(c, d)))); continue; }
|
|
823
|
+
// PG/GaussDB:库 → 模式 → 表(三层,Navicat 官方层级);schema 节点复用 open/loading/error 状态
|
|
824
|
+
if (useSchemas) {
|
|
825
|
+
const slist = schemasMap[c.id + "/" + d];
|
|
826
|
+
if (!slist) continue;
|
|
827
|
+
if (slist.length === 0) { treeRows.push(treerow(dk + ":e", 2, false, true, t("noSchemas"))); continue; }
|
|
828
|
+
for (const s of slist) {
|
|
829
|
+
const sk = "s:" + c.id + "/" + d + "/" + s;
|
|
830
|
+
const sOpen = !!open[sk];
|
|
831
|
+
treeRows.push(treerow(sk, 2, sOpen, false, s, () => toggleSchema(c, d, s)));
|
|
832
|
+
if (!sOpen) continue;
|
|
833
|
+
const tlist = tablesMap[c.id + "/" + d + "/" + s];
|
|
834
|
+
if (loading[sk]) { treeRows.push(treerow(sk + ":l", 3, false, true, "…")); continue; }
|
|
835
|
+
if (error[sk]) { treeRows.push(treerow(sk + ":x", 3, false, true, t("loadFailed") + ":" + error[sk], () => retry(sk, () => toggleSchema(c, d, s)))); continue; }
|
|
836
|
+
if (!tlist) continue;
|
|
837
|
+
if (tlist.length === 0) { treeRows.push(treerow(sk + ":e", 3, false, true, t("noTables"))); continue; }
|
|
838
|
+
for (const tb of tlist) {
|
|
839
|
+
const active = !!sel && sel.connId === c.id && sel.db === d && sel.schemaName === s && sel.table.name === tb.name;
|
|
840
|
+
treeRows.push(treerow("t:" + sk + "/" + tb.name, 3, false, true,
|
|
841
|
+
tb.name + (tb.type && tb.type !== "table" ? " · " + tb.type : ""),
|
|
842
|
+
() => setSel({ connId: c.id, db: d, schemaName: s, table: tb }), active));
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
const tlist = tablesMap[c.id + "/" + d];
|
|
806
848
|
if (!tlist) continue;
|
|
807
849
|
if (tlist.length === 0) { treeRows.push(treerow(dk + ":e", 2, false, true, t("noTables"))); continue; }
|
|
808
850
|
for (const tb of tlist) {
|
|
809
851
|
const active = !!sel && sel.connId === c.id && sel.db === d && sel.table.name === tb.name;
|
|
810
|
-
treeRows.push(treerow("t:" +
|
|
852
|
+
treeRows.push(treerow("t:" + c.id + "/" + d + "/" + tb.name, 2, false, true,
|
|
811
853
|
tb.name + (tb.type && tb.type !== "table" ? " · " + tb.type : ""),
|
|
812
854
|
() => setSel({ connId: c.id, db: d, table: tb }), active));
|
|
813
855
|
}
|
|
@@ -832,7 +874,7 @@ window.__ModuleLoader__.load({
|
|
|
832
874
|
["structure", "preview"].map((v) =>
|
|
833
875
|
React.createElement("button", { key: v, className: view === v ? "active" : "", onClick: () => setView(v) }, v === "structure" ? t("structure") : t("preview"))),
|
|
834
876
|
),
|
|
835
|
-
React.createElement("strong", null, (view === "structure" ? t("structure") : t("preview")) + " · " + sel.db + " / " + sel.table.name),
|
|
877
|
+
React.createElement("strong", null, (view === "structure" ? t("structure") : t("preview")) + " · " + sel.db + (sel.schemaName ? "." + sel.schemaName : "") + " / " + sel.table.name),
|
|
836
878
|
view === "structure"
|
|
837
879
|
? resultTable(
|
|
838
880
|
[t("column"), t("dataType"), t("nullable"), t("keyCol"), t("defaultVal"), t("comment")],
|
|
@@ -28,25 +28,64 @@ function poolConfig(conn) {
|
|
|
28
28
|
export async function createPgLikeAdapter(kind, Driver, conn, opts) {
|
|
29
29
|
const pool = new Driver.Pool(poolConfig(conn));
|
|
30
30
|
const readOnly = opts?.mode === 'ro';
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
// 服务器级 ro 强制(官方手段):每个新连接会话设为只读事务。
|
|
32
|
+
// fail-closed:SET 失败时 release(err) 让驱动销毁该连接、等待的 acquire 收到错误——
|
|
33
|
+
// 会话级只读是 ro 的最后防线,不允许静默降级成可写连接。
|
|
34
|
+
const setupReadOnly = (p) => {
|
|
35
|
+
p.on('connect', (c) => {
|
|
36
36
|
void c.query('SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY').catch((err) => {
|
|
37
37
|
c.release(err instanceof Error ? err : new Error(String(err)));
|
|
38
38
|
});
|
|
39
39
|
});
|
|
40
40
|
// pg 约定:release(err) 若无等待者会 emit pool 'error',不监听会崩进程
|
|
41
|
-
|
|
42
|
-
}
|
|
41
|
+
p.on('error', () => { });
|
|
42
|
+
};
|
|
43
|
+
if (readOnly)
|
|
44
|
+
setupReadOnly(pool);
|
|
43
45
|
// 事务激活期间所有 query/execute 路由到同一 client
|
|
44
46
|
let txClient = null;
|
|
45
47
|
const run = (sql, params) => txClient ? txClient.query(sql, params) : pool.query(sql, params);
|
|
48
|
+
// 跨库浏览(Navicat 行为):树列出服务器上所有库,展开非连接库时用同一凭据开指向该库的池。
|
|
49
|
+
// 池按库缓存复用;ro 模式下新池同样套会话只读。连接自身的库(fields.database 或 url 库名)直接复用主池。
|
|
50
|
+
const dbPools = new Map();
|
|
51
|
+
const mainDb = () => {
|
|
52
|
+
if (conn.fields)
|
|
53
|
+
return conn.fields.database ? String(conn.fields.database) : null;
|
|
54
|
+
if (!conn.url)
|
|
55
|
+
return null;
|
|
56
|
+
try {
|
|
57
|
+
return decodeURIComponent(new URL(conn.url).pathname.replace(/^\//, '')) || null;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const poolFor = (db) => {
|
|
64
|
+
if (!db || db === mainDb())
|
|
65
|
+
return pool;
|
|
66
|
+
const cached = dbPools.get(db);
|
|
67
|
+
if (cached)
|
|
68
|
+
return cached;
|
|
69
|
+
const created = new Driver.Pool({ ...poolConfig(conn), database: db });
|
|
70
|
+
if (readOnly)
|
|
71
|
+
setupReadOnly(created);
|
|
72
|
+
dbPools.set(db, created);
|
|
73
|
+
return created;
|
|
74
|
+
};
|
|
46
75
|
const defaultSchema = () => {
|
|
47
76
|
const f = conn.fields;
|
|
48
77
|
return f && typeof f.schema === 'string' && f.schema ? f.schema : 'public';
|
|
49
78
|
};
|
|
79
|
+
/** 浏览目标解析:"db.schema"(跨库)| "schema"(当前库)| 空(当前库默认 schema)。
|
|
80
|
+
* PG 标识符不允许裸点,按第一个点切分安全;多余点由 assertIdent 拒绝。 */
|
|
81
|
+
const parseBrowseTarget = (ref, what) => {
|
|
82
|
+
if (!ref)
|
|
83
|
+
return { db: null, schema: defaultSchema() };
|
|
84
|
+
const i = ref.indexOf('.');
|
|
85
|
+
if (i < 0)
|
|
86
|
+
return { db: null, schema: assertIdent(ref, what) };
|
|
87
|
+
return { db: assertIdent(ref.slice(0, i), '数据库'), schema: assertIdent(ref.slice(i + 1), what) };
|
|
88
|
+
};
|
|
50
89
|
function toQueryResult(res) {
|
|
51
90
|
let columns = (res.fields ?? []).map((f) => f.name);
|
|
52
91
|
if (columns.length === 0 && res.rows.length > 0) {
|
|
@@ -116,30 +155,41 @@ export async function createPgLikeAdapter(kind, Driver, conn, opts) {
|
|
|
116
155
|
const res = await pool.query('SELECT datname FROM pg_database WHERE datistemplate = false AND datallowconn = true ORDER BY datname');
|
|
117
156
|
return res.rows.map((r) => String(r.datname));
|
|
118
157
|
}),
|
|
158
|
+
// 库内 schema 清单(Navicat 官方层级:数据库 → 模式 → 表)。
|
|
159
|
+
// database 参数 = 库名(UI 树第一层);pg_* 前缀覆盖 pg_catalog/pg_toast/pg_temp 系。
|
|
160
|
+
listSchemas: (database) => humanize(`${kind} 列出模式`, async () => {
|
|
161
|
+
const p = poolFor(database ? assertIdent(database, '数据库') : null);
|
|
162
|
+
const res = await p.query(`SELECT nspname FROM pg_catalog.pg_namespace
|
|
163
|
+
WHERE nspname NOT LIKE 'pg\\_%' AND nspname <> 'information_schema'
|
|
164
|
+
ORDER BY nspname`);
|
|
165
|
+
return res.rows.map((r) => String(r.nspname));
|
|
166
|
+
}),
|
|
167
|
+
// database 参数 = "库名.schema"(跨库浏览)或 "schema"(当前库)
|
|
119
168
|
listTables: (database) => humanize(`${kind} 列出表`, async () => {
|
|
120
|
-
const schema =
|
|
121
|
-
const res = await
|
|
169
|
+
const { db, schema } = parseBrowseTarget(database, 'schema');
|
|
170
|
+
const res = await poolFor(db).query(`SELECT table_name, table_type FROM information_schema.tables
|
|
122
171
|
WHERE table_schema = $1 ORDER BY table_name`, [schema]);
|
|
123
172
|
return res.rows.map((r) => {
|
|
124
173
|
const rawType = String(r.table_type);
|
|
125
174
|
return {
|
|
126
175
|
name: String(r.table_name),
|
|
127
176
|
type: rawType === 'BASE TABLE' ? 'TABLE' : rawType,
|
|
128
|
-
database: schema,
|
|
177
|
+
database: db ? `${db}.${schema}` : schema,
|
|
129
178
|
};
|
|
130
179
|
});
|
|
131
180
|
}),
|
|
132
181
|
describeTable: (table, database) => humanize(`${kind} 查看表结构`, async () => {
|
|
133
|
-
const schema =
|
|
182
|
+
const { db, schema } = parseBrowseTarget(database, 'schema');
|
|
134
183
|
const tbl = assertIdent(table, '表名');
|
|
135
|
-
const
|
|
184
|
+
const p = poolFor(db);
|
|
185
|
+
const res = await p.query(`SELECT c.column_name, c.data_type, c.is_nullable, c.column_default, c.character_maximum_length,
|
|
136
186
|
col_description((quote_ident($1) || '.' || quote_ident($2))::regclass, c.ordinal_position) AS col_comment
|
|
137
187
|
FROM information_schema.columns c
|
|
138
188
|
WHERE c.table_schema = $1 AND c.table_name = $2
|
|
139
189
|
ORDER BY c.ordinal_position`, [schema, tbl]);
|
|
140
190
|
if (res.rows.length === 0)
|
|
141
|
-
throw new Error(`表不存在: ${schema}.${tbl}`);
|
|
142
|
-
const pks = await
|
|
191
|
+
throw new Error(`表不存在: ${db ? db + '.' : ''}${schema}.${tbl}`);
|
|
192
|
+
const pks = await p.query(`SELECT kcu.column_name FROM information_schema.table_constraints tc
|
|
143
193
|
JOIN information_schema.key_column_usage kcu
|
|
144
194
|
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
145
195
|
WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = $1 AND tc.table_name = $2`, [schema, tbl]);
|
|
@@ -158,13 +208,17 @@ export async function createPgLikeAdapter(kind, Driver, conn, opts) {
|
|
|
158
208
|
});
|
|
159
209
|
}),
|
|
160
210
|
previewRows: (table, limit, database, offset) => humanize(`${kind} 预览行`, async () => {
|
|
161
|
-
const schema =
|
|
211
|
+
const { db, schema } = parseBrowseTarget(database, 'schema');
|
|
162
212
|
const tbl = assertIdent(table, '表名');
|
|
163
213
|
const lim = clampLimit(limit);
|
|
164
214
|
const off = clampOffset(offset);
|
|
165
|
-
const
|
|
215
|
+
const sql = `SELECT * FROM ${quoteIdent(schema)}.${quoteIdent(tbl)} LIMIT $1 OFFSET $2`;
|
|
216
|
+
// 当前库保持事务路由(run);跨库必须用该库自己的池
|
|
217
|
+
const res = db ? await poolFor(db).query(sql, [lim, off]) : await run(sql, [lim, off]);
|
|
166
218
|
return toQueryResult(res);
|
|
167
219
|
}),
|
|
168
|
-
close: () => humanize(`${kind} 关闭连接`, () =>
|
|
220
|
+
close: () => humanize(`${kind} 关闭连接`, async () => {
|
|
221
|
+
await Promise.all([pool.end(), ...[...dbPools.values()].map((p) => p.end())]);
|
|
222
|
+
}),
|
|
169
223
|
};
|
|
170
224
|
}
|
package/dist/http/index.js
CHANGED
|
@@ -210,6 +210,9 @@ async function route(req, res, service, path, q, opts) {
|
|
|
210
210
|
if (path === '/api/tables' && req.method === 'GET') {
|
|
211
211
|
return sendOk(res, await service.tables(projectOf(q), required(q, 'connId'), q.get('database') ?? undefined));
|
|
212
212
|
}
|
|
213
|
+
if (path === '/api/schemas' && req.method === 'GET') {
|
|
214
|
+
return sendOk(res, await service.schemas(projectOf(q), required(q, 'connId'), q.get('database') ?? undefined));
|
|
215
|
+
}
|
|
213
216
|
if (path === '/api/schema' && req.method === 'GET') {
|
|
214
217
|
return sendOk(res, await service.schema(projectOf(q), required(q, 'connId'), required(q, 'table'), q.get('database') ?? undefined));
|
|
215
218
|
}
|
package/dist/manager.js
CHANGED
|
@@ -146,6 +146,21 @@ export class DbToolService {
|
|
|
146
146
|
throw this.toDriverError(e);
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
|
+
/** 库内 schema 清单(仅 PG/GaussDB 等三层语义适配器实现;ro 即可)。 */
|
|
150
|
+
async schemas(projectPath, connId, database) {
|
|
151
|
+
const { key, adapter } = await this.authorize(projectPath, connId, false);
|
|
152
|
+
if (!adapter.listSchemas)
|
|
153
|
+
return [];
|
|
154
|
+
try {
|
|
155
|
+
const result = await adapter.listSchemas(database);
|
|
156
|
+
this.audit(key, connId, 'schemas', database ?? 'default', 'none', false, true);
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
this.audit(key, connId, 'schemas', database ?? 'default', 'none', false, false, this.errText(e));
|
|
161
|
+
throw this.toDriverError(e);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
149
164
|
async schema(projectPath, connId, table, database) {
|
|
150
165
|
const t = assertNonEmpty(table, 'table');
|
|
151
166
|
const { key, adapter } = await this.authorize(projectPath, connId, false);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-db-tool",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "DSH community plugin: chat-operated multi-database admin tool with sidebar management, project-scoped grants, ro/rw modes and dangerous-operation confirmation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/skills/db-admin/SKILL.md
CHANGED
|
@@ -57,6 +57,7 @@ DatabaseManager({ action: "schema", conn_id: "c1", database: "shop", table: "use
|
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
- 表列表返回 `TableInfo[]`:`{name, type?, comment?, database?}`(Redis 的 type 为 hash|list|set|zset|string|stream)。
|
|
60
|
+
- PostgreSQL/GaussDB 为「数据库 → 模式(schema) → 表」三层语义,支持跨库浏览(Navicat 行为):`database` 参数传 `"库名.schema"` 即可浏览连接库之外的库(如 `gycwd.public`,服务端自动用同一凭据开指向该库的连接);只传 schema 名(如 `public`)则查连接自身库。`listSchemas(database)` 的参数是**纯库名**。不确定 schema 时先执行 `SELECT nspname FROM pg_catalog.pg_namespace WHERE nspname NOT LIKE 'pg\_%' AND nspname <> 'information_schema'`。
|
|
60
61
|
- 列结构返回 `ColumnInfo[]`:`{name, dataType, nullable, key?, default?, comment?}`。
|
|
61
62
|
|
|
62
63
|
### 5. preview — 预览表数据
|