dsh-lh-data 0.1.0

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.
Files changed (89) hide show
  1. package/README.md +332 -0
  2. package/cordis.patch.yml +7 -0
  3. package/lib/admin-contract.d.ts +274 -0
  4. package/lib/admin-http.d.ts +31 -0
  5. package/lib/admin-validate.d.ts +87 -0
  6. package/lib/admin.d.ts +60 -0
  7. package/lib/client.js +2969 -0
  8. package/lib/client.js.map +1 -0
  9. package/lib/datasource/columns.d.ts +13 -0
  10. package/lib/datasource/connection.d.ts +33 -0
  11. package/lib/datasource/connector/base.d.ts +21 -0
  12. package/lib/datasource/connector/mysql.d.ts +23 -0
  13. package/lib/datasource/connector/postgresql.d.ts +23 -0
  14. package/lib/datasource/crypto.d.ts +13 -0
  15. package/lib/datasource/driver.d.ts +39 -0
  16. package/lib/datasource/errors.d.ts +25 -0
  17. package/lib/datasource/importer.d.ts +38 -0
  18. package/lib/datasource/source-sql.d.ts +11 -0
  19. package/lib/datasource/source-store.d.ts +28 -0
  20. package/lib/datasource/types.d.ts +119 -0
  21. package/lib/db.d.ts +91 -0
  22. package/lib/http-common.d.ts +31 -0
  23. package/lib/http.d.ts +13 -0
  24. package/lib/index.d.ts +50 -0
  25. package/lib/index.js +5735 -0
  26. package/lib/parse.d.ts +41 -0
  27. package/lib/preview.d.ts +75 -0
  28. package/lib/render.d.ts +100 -0
  29. package/lib/scope-registry.d.ts +47 -0
  30. package/lib/scope.d.ts +56 -0
  31. package/lib/sql.d.ts +133 -0
  32. package/lib/store.d.ts +174 -0
  33. package/lib/table.d.ts +38 -0
  34. package/lib/tooling.d.ts +267 -0
  35. package/lib/tools/datasource.d.ts +16 -0
  36. package/lib/tools/import.d.ts +21 -0
  37. package/lib/tools/read.d.ts +77 -0
  38. package/lib/tools/registry.d.ts +12 -0
  39. package/lib/tools/write.d.ts +37 -0
  40. package/lib/view.d.ts +138 -0
  41. package/package.json +61 -0
  42. package/src/admin-contract.ts +351 -0
  43. package/src/admin-http.ts +254 -0
  44. package/src/admin-validate.ts +393 -0
  45. package/src/admin.ts +558 -0
  46. package/src/client/index.ts +403 -0
  47. package/src/client/settings/CreateForm.tsx +186 -0
  48. package/src/client/settings/DataSourceForm.tsx +278 -0
  49. package/src/client/settings/DataSourcesPanel.tsx +301 -0
  50. package/src/client/settings/DatasetEditor.tsx +245 -0
  51. package/src/client/settings/DatasetTable.tsx +110 -0
  52. package/src/client/settings/DatasetsPanel.tsx +207 -0
  53. package/src/client/settings/RowsPanel.tsx +196 -0
  54. package/src/client/settings/Section.tsx +41 -0
  55. package/src/client/settings/SourceTablesPanel.tsx +226 -0
  56. package/src/client/settings/api.ts +154 -0
  57. package/src/client/settings/styles.ts +125 -0
  58. package/src/datasource/columns.ts +56 -0
  59. package/src/datasource/connection.ts +124 -0
  60. package/src/datasource/connector/base.ts +54 -0
  61. package/src/datasource/connector/mysql.ts +187 -0
  62. package/src/datasource/connector/postgresql.ts +212 -0
  63. package/src/datasource/crypto.ts +61 -0
  64. package/src/datasource/driver.ts +108 -0
  65. package/src/datasource/errors.ts +69 -0
  66. package/src/datasource/importer.ts +262 -0
  67. package/src/datasource/index.ts +62 -0
  68. package/src/datasource/source-sql.ts +64 -0
  69. package/src/datasource/source-store.ts +152 -0
  70. package/src/datasource/types.ts +133 -0
  71. package/src/db.ts +277 -0
  72. package/src/http-common.ts +91 -0
  73. package/src/http.ts +130 -0
  74. package/src/index.ts +486 -0
  75. package/src/parse.ts +213 -0
  76. package/src/preview.ts +198 -0
  77. package/src/render.ts +294 -0
  78. package/src/scope-registry.ts +111 -0
  79. package/src/scope.ts +159 -0
  80. package/src/sql.ts +491 -0
  81. package/src/store.ts +412 -0
  82. package/src/table.ts +160 -0
  83. package/src/tooling.ts +551 -0
  84. package/src/tools/datasource.ts +378 -0
  85. package/src/tools/import.ts +282 -0
  86. package/src/tools/read.ts +536 -0
  87. package/src/tools/registry.ts +56 -0
  88. package/src/tools/write.ts +241 -0
  89. package/src/view.ts +371 -0
@@ -0,0 +1,536 @@
1
+ /**
2
+ * 只读工具:`dataset_list` / `dataset_schema` / `dataset_query`。
3
+ *
4
+ * 三个工具都不修改数据,因此走 `allow`(不触发审批);返回值只出现 datasetId 与登记名,
5
+ * 物理表名留在 `store.ts` 内部(设计文档 §7.1)。
6
+ */
7
+
8
+ import { renderColumns, renderDatasetList, renderQueryPreview } from '../render'
9
+ import type { ColumnInfo } from '../parse'
10
+ import type { Row } from '../db'
11
+ import { resolveDataset, type DataServices } from '../store'
12
+ import { selectRows } from '../table'
13
+ import {
14
+ planRawQuery,
15
+ planStructuredQuery,
16
+ substituteDatasetAlias,
17
+ validateReadOnlyQuery,
18
+ } from '../sql'
19
+ import {
20
+ buildPreviewSlice,
21
+ previewSlicePlan,
22
+ summarizeColumns,
23
+ type ColumnSummary,
24
+ type PreviewOptions,
25
+ } from '../preview'
26
+ import { debugLog, toolDef, ToolError, type ToolDefinition, type ToolExec } from '../tooling'
27
+
28
+ const DATASET_PARAM = {
29
+ type: 'string',
30
+ required: true,
31
+ description: '数据集的 datasetId 或登记名(来自 dataset_list)。不要传物理表名。',
32
+ } as const
33
+
34
+ export interface DatasetSummary {
35
+ datasetId: string
36
+ name: string
37
+ rowCount: number
38
+ columnCount: number
39
+ status: string
40
+ sourcePath: string | null
41
+ createdAt: number
42
+ updatedAt: number
43
+ }
44
+
45
+ export interface ListOutput {
46
+ count: number
47
+ datasets: DatasetSummary[]
48
+ }
49
+
50
+ export interface SchemaOutput {
51
+ datasetId: string
52
+ name: string
53
+ description: string | null
54
+ rowCount: number
55
+ status: string
56
+ columns: ColumnInfo[]
57
+ }
58
+
59
+ /** 结果视图的定位信息(设计文档 §5.1 / §5.4)。 */
60
+ export interface QueryViewInfo {
61
+ viewId: string
62
+ endpoint: string
63
+ pageSize: number
64
+ maxPageSize: number
65
+ /** 排序是否稳定(false 时前端只展示首页)。 */
66
+ stable: boolean
67
+ /** 允许前端排序的列名白名单。 */
68
+ sortable: string[]
69
+ }
70
+
71
+ export interface QueryOutput {
72
+ datasetId: string
73
+ name: string
74
+ columns: { name: string; type: string }[]
75
+ /** 实际命中的行数(不受上限收敛影响)。 */
76
+ matchedRows: number
77
+ /** 本次可服务的行数(已按 maxQueryRows / maxViewRows 收敛)。 */
78
+ totalRows: number
79
+ preview: {
80
+ rows: Row[]
81
+ columns: string[]
82
+ /** 头段行数(`gap` 为真时其后是尾段)。 */
83
+ headCount: number
84
+ gap: boolean
85
+ skipped: number
86
+ cellTruncated: boolean
87
+ columnTruncated: boolean
88
+ hiddenColumns: number
89
+ }
90
+ summary: ColumnSummary[]
91
+ limit: number
92
+ offset: number
93
+ /** 预览只是结果的一部分。 */
94
+ truncated: boolean
95
+ /** 存在前端视图(完整结果由前端分页拉取)。 */
96
+ view?: QueryViewInfo
97
+ }
98
+
99
+ const DEFAULT_QUERY_LIMIT = 50
100
+
101
+ export function createListTool(services: DataServices): ToolDefinition {
102
+ return toolDef({
103
+ name: 'dataset_list',
104
+ description: [
105
+ '列出当前工作区(会话 cwd)已导入的数据集:datasetId、登记名、行数、列数、状态、来源文件、创建时间。',
106
+ '只读工具,不会修改任何数据。后续所有 dataset_* 工具都用这里的 datasetId 或登记名指代数据集。',
107
+ '数据集按工作区隔离:看不到其它工作区的数据集。',
108
+ ].join(' '),
109
+ parameters: {},
110
+ output: {
111
+ schema: {
112
+ type: 'object',
113
+ additionalProperties: false,
114
+ properties: {
115
+ count: { type: 'integer', description: '数据集数量' },
116
+ datasets: {
117
+ type: 'array',
118
+ items: {
119
+ type: 'object',
120
+ additionalProperties: false,
121
+ properties: {
122
+ datasetId: { type: 'string' },
123
+ name: { type: 'string' },
124
+ rowCount: { type: 'integer' },
125
+ columnCount: { type: 'integer' },
126
+ status: { type: 'string', description: 'importing / ready / failed' },
127
+ sourcePath: { oneOf: [{ type: 'string' }, { type: 'null' }] },
128
+ createdAt: { type: 'integer' },
129
+ updatedAt: { type: 'integer' },
130
+ },
131
+ },
132
+ },
133
+ },
134
+ },
135
+ render: (_args, value: ListOutput) => [{ type: 'text', text: renderDatasetList(value.datasets) }],
136
+ },
137
+ presentCall: () => ({ card: 'generic', title: '列出数据集', kind: 'read' }),
138
+ execute: async (_args, exec: ToolExec): Promise<ListOutput> => {
139
+ exec.signal.throwIfAborted()
140
+ const scope = await services.scopeOf(exec)
141
+ const records = await services.store.list(scope.scopeKey)
142
+ exec.signal.throwIfAborted()
143
+ return {
144
+ count: records.length,
145
+ datasets: records.map(record => ({
146
+ datasetId: record.id,
147
+ name: record.name,
148
+ rowCount: record.rowCount,
149
+ columnCount: record.columns.length,
150
+ status: record.status,
151
+ sourcePath: record.sourcePath,
152
+ createdAt: record.createdAt,
153
+ updatedAt: record.updatedAt,
154
+ })),
155
+ }
156
+ },
157
+ })
158
+ }
159
+
160
+ export function createSchemaTool(services: DataServices): ToolDefinition {
161
+ return toolDef({
162
+ name: 'dataset_schema',
163
+ description: [
164
+ '查看数据集的列信息:列名(保留原始表头,含中文)、推断类型、是否可空、样例值与说明。',
165
+ '只读工具。写操作前先用它确认列名与类型;列名必须与这里列出的完全一致。',
166
+ ].join(' '),
167
+ parameters: {
168
+ dataset: DATASET_PARAM,
169
+ },
170
+ output: {
171
+ schema: {
172
+ type: 'object',
173
+ additionalProperties: false,
174
+ properties: {
175
+ datasetId: { type: 'string' },
176
+ name: { type: 'string' },
177
+ description: { oneOf: [{ type: 'string' }, { type: 'null' }] },
178
+ rowCount: { type: 'integer' },
179
+ status: { type: 'string' },
180
+ columns: {
181
+ type: 'array',
182
+ items: {
183
+ type: 'object',
184
+ additionalProperties: false,
185
+ properties: {
186
+ name: { type: 'string' },
187
+ sanitizedName: { type: 'string' },
188
+ type: { type: 'string' },
189
+ nullable: { type: 'boolean' },
190
+ description: { type: 'string' },
191
+ sample: { type: 'array', items: { type: 'json' } },
192
+ },
193
+ },
194
+ },
195
+ },
196
+ },
197
+ render: (_args, value: SchemaOutput) => [
198
+ {
199
+ type: 'text',
200
+ text: [
201
+ `数据集 ${value.name}(${value.datasetId}):${value.columns.length} 列,${value.rowCount} 行,状态 ${value.status}`,
202
+ '',
203
+ renderColumns(value.columns),
204
+ ].join('\n'),
205
+ },
206
+ ],
207
+ },
208
+ presentCall: args => ({ card: 'generic', title: `查看列信息:${args.dataset}`, kind: 'read' }),
209
+ execute: async (args, exec): Promise<SchemaOutput> => {
210
+ const { record } = await resolveDataset(services, exec, args.dataset, { requireReady: true })
211
+ exec.signal.throwIfAborted()
212
+ return {
213
+ datasetId: record.id,
214
+ name: record.name,
215
+ description: record.description,
216
+ rowCount: record.rowCount,
217
+ status: record.status,
218
+ columns: record.columns,
219
+ }
220
+ },
221
+ })
222
+ }
223
+
224
+ /** 从插件配置里取出片段相关的选项。 */
225
+ function previewOptionsOf(cfg: DataServices['cfg']): PreviewOptions {
226
+ return {
227
+ previewRows: cfg.previewRows,
228
+ previewStrategy: cfg.previewStrategy,
229
+ previewColumns: cfg.previewColumns,
230
+ summaryEnabled: cfg.summaryEnabled,
231
+ summaryMaxColumns: cfg.summaryMaxColumns,
232
+ summaryMaxTextColumns: cfg.summaryMaxTextColumns,
233
+ }
234
+ }
235
+
236
+ const COLUMN_ITEM_SCHEMA = {
237
+ type: 'object',
238
+ additionalProperties: false,
239
+ properties: {
240
+ name: { type: 'string' },
241
+ type: { type: 'string' },
242
+ },
243
+ } as const
244
+
245
+ export function createQueryTool(services: DataServices): ToolDefinition {
246
+ return toolDef({
247
+ name: 'dataset_query',
248
+ description: [
249
+ '只读查询数据集,返回列名与行。推荐用结构化参数:columns(列名数组,空表示全部)、where(SQL 布尔表达式,如 "单价 > 100 AND 城市 = \'深圳\'")、orderBy(如 "单价 DESC")、limit、offset。',
250
+ '需要聚合/分组时才用 sql:只允许单条 SELECT / WITH / EXPLAIN 语句,用保留别名 ds 指代本数据集(如 `SELECT 状态, COUNT(*) c FROM ds GROUP BY 状态`),'
251
+ + '其它表引用必须来自本工作区已登记的数据集,且不允许出现写操作与 DDL 关键字。',
252
+ '系统列 _row_id 是行的定位键,结果里会返回,写操作(update/delete)需要它。只读工具,不会修改数据。',
253
+ '结果较大时只返回「少量预览行 + 全量统计摘要」:完整结果会在前端表格里展示,你不需要逐页读取。',
254
+ '要下结论请用更精确的 where 或聚合 SQL 再查一次(例如 SELECT 状态, COUNT(*) c FROM ds GROUP BY 状态),而不是把全表读进来。',
255
+ ].join(' '),
256
+ parameters: {
257
+ dataset: DATASET_PARAM,
258
+ sql: { type: 'string', description: '可选:原始只读 SQL(单条 SELECT / WITH / EXPLAIN)。与 columns/where/orderBy 互斥,优先使用结构化参数。' },
259
+ columns: { type: 'array', items: { type: 'string' }, description: '可选:要返回的列名;省略或传空数组表示全部列。' },
260
+ where: { type: 'string', description: '可选:SQL 布尔表达式(不含 WHERE 关键字),如 "数量 >= 10 AND 名称 LIKE \'%钢%\'"。' },
261
+ orderBy: { type: 'string', description: '可选:排序,如 "单价 DESC, 名称 ASC"。' },
262
+ limit: { type: 'integer', description: '可选:可服务行数的上限(不做分页取数,只影响片段与视图上限)。' },
263
+ offset: { type: 'integer', description: '可选:预览片段从第 N 行开始,默认 0;视图始终从第一行开始。' },
264
+ },
265
+ output: {
266
+ schema: {
267
+ type: 'object',
268
+ additionalProperties: false,
269
+ properties: {
270
+ datasetId: { type: 'string' },
271
+ name: { type: 'string' },
272
+ columns: { type: 'array', items: COLUMN_ITEM_SCHEMA },
273
+ matchedRows: { type: 'integer' },
274
+ totalRows: { type: 'integer' },
275
+ preview: {
276
+ type: 'object',
277
+ additionalProperties: false,
278
+ properties: {
279
+ rows: { type: 'array', items: { type: 'object', additionalProperties: true } },
280
+ columns: { type: 'array', items: { type: 'string' } },
281
+ headCount: { type: 'integer' },
282
+ gap: { type: 'boolean' },
283
+ skipped: { type: 'integer' },
284
+ cellTruncated: { type: 'boolean' },
285
+ columnTruncated: { type: 'boolean' },
286
+ hiddenColumns: { type: 'integer' },
287
+ },
288
+ },
289
+ summary: { type: 'array', items: { type: 'object', additionalProperties: true } },
290
+ limit: { type: 'integer' },
291
+ offset: { type: 'integer' },
292
+ truncated: { type: 'boolean' },
293
+ view: {
294
+ type: 'object',
295
+ additionalProperties: false,
296
+ properties: {
297
+ viewId: { type: 'string' },
298
+ endpoint: { type: 'string' },
299
+ pageSize: { type: 'integer' },
300
+ maxPageSize: { type: 'integer' },
301
+ stable: { type: 'boolean' },
302
+ sortable: { type: 'array', items: { type: 'string' } },
303
+ expiresAt: { type: 'integer' },
304
+ },
305
+ },
306
+ },
307
+ },
308
+ render: (_args, value: QueryOutput) => [
309
+ {
310
+ type: 'text',
311
+ text: renderQueryPreview({
312
+ name: value.name,
313
+ datasetId: value.datasetId,
314
+ columnCount: value.columns.length,
315
+ totalRows: value.totalRows,
316
+ matchedRows: value.matchedRows,
317
+ preview: value.preview,
318
+ summary: value.summary,
319
+ truncated: value.truncated,
320
+ ...value.view === undefined
321
+ ? {}
322
+ : { view: { pageSize: value.view.pageSize, stable: value.view.stable } },
323
+ }),
324
+ },
325
+ ],
326
+ /**
327
+ * 只给前端的视图描述符(模型不可见)。
328
+ * 必须是 (args, value) 的纯函数(平台会重放),所以 viewId 随规范值携带、由 render 刻意不打印。
329
+ */
330
+ presentationMeta: (_args, value: QueryOutput) => {
331
+ if (value.view === undefined) return null
332
+ const meta = {
333
+ kind: 'dataset-view',
334
+ viewId: value.view.viewId,
335
+ endpoint: value.view.endpoint,
336
+ datasetId: value.datasetId,
337
+ name: value.name,
338
+ columns: value.columns,
339
+ totalRows: value.matchedRows,
340
+ pageSize: value.view.pageSize,
341
+ maxPageSize: value.view.maxPageSize,
342
+ stable: value.view.stable,
343
+ sortable: value.view.sortable,
344
+ }
345
+ debugLog('read:meta', meta)
346
+ return meta
347
+ },
348
+ },
349
+ presentCall: args => ({ card: 'generic', title: `查询数据集:${args.dataset}`, kind: 'read' }),
350
+ execute: async (args, exec): Promise<QueryOutput> => {
351
+ const { scope, record, db } = await resolveDataset(services, exec, args.dataset, { requireReady: true })
352
+ const cfg = services.cfg
353
+ const maxRows = cfg.maxQueryRows
354
+ const raw = typeof args.sql === 'string' && args.sql.trim().length > 0
355
+ // 视图可用时模型不必声明 limit:未声明即按 maxQueryRows 取上限,
356
+ // 声明了才是「我只要这么多行」的显式收敛。
357
+ const canView = services.views !== undefined
358
+
359
+ if (raw && !cfg.allowRawSql) {
360
+ throw new ToolError('当前配置禁用了原始 SQL(allowRawSql=false),请改用结构化参数', 'RAW_SQL_DISABLED')
361
+ }
362
+ const allowedTables = raw ? await services.store.tableNames(scope.scopeKey) : []
363
+
364
+ // 模型只写 `ds`,物理表名由插件替换进去(句柄化:表名从不出现在参数里)。
365
+ const plan = raw
366
+ ? planRawQuery(substituteDatasetAlias(String(args.sql), record.tableName), { allowedTables, maxRows })
367
+ : planStructuredQuery({
368
+ table: record.tableName,
369
+ columns: args.columns,
370
+ where: args.where,
371
+ orderBy: args.orderBy,
372
+ // 有视图时不设默认上限(未声明 limit 即取到 maxQueryRows,前端按页取);
373
+ // 降级路径沿用旧的默认 50 行,避免无前端时突然多返回几倍数据。
374
+ limit: canView ? args.limit : args.limit ?? DEFAULT_QUERY_LIMIT,
375
+ offset: args.offset,
376
+ allowedColumns: record.columns.map(column => column.sanitizedName),
377
+ maxRows,
378
+ })
379
+
380
+ exec.signal.throwIfAborted()
381
+
382
+ // EXPLAIN 不产生结果集:按原样执行,不走计数/视图。
383
+ if (plan.explain) {
384
+ const sql = validateReadOnlyQuery(substituteDatasetAlias(String(args.sql), record.tableName), { allowedTables, maxRows })
385
+ const { rows, columns } = await selectRows(db, sql, [])
386
+ return {
387
+ datasetId: record.id,
388
+ name: record.name,
389
+ columns: columns.map(name => ({ name, type: 'text' })),
390
+ matchedRows: rows.length,
391
+ totalRows: rows.length,
392
+ preview: {
393
+ rows,
394
+ columns,
395
+ headCount: rows.length,
396
+ gap: false,
397
+ skipped: 0,
398
+ cellTruncated: false,
399
+ columnTruncated: false,
400
+ hiddenColumns: 0,
401
+ },
402
+ summary: [],
403
+ limit: rows.length,
404
+ offset: 0,
405
+ truncated: false,
406
+ }
407
+ }
408
+
409
+ const previewOptions = previewOptionsOf(cfg)
410
+
411
+ const countRow = await db.prepare(plan.countSql).get()
412
+ const matched = Number(countRow?._total ?? 0)
413
+ const maxViewRows = Math.max(1, Math.floor(cfg.maxViewRows))
414
+ // 有视图时视图能翻到的行数由 maxViewRows 约束(模型显式 limit 再收敛一次);
415
+ // 降级路径沿用 maxQueryRows 语义(plan.rowCap)。
416
+ const rowCap = canView ? Math.min(plan.declaredLimit ?? maxViewRows, maxViewRows) : plan.rowCap
417
+ const totalRows = Math.max(0, Math.min(matched, rowCap))
418
+ const previewOffset = Math.max(0, plan.previewOffset)
419
+ debugLog('read:plan', {
420
+ canView,
421
+ matched,
422
+ rowCap,
423
+ maxViewRows,
424
+ declaredLimit: plan.declaredLimit,
425
+ totalRows,
426
+ })
427
+
428
+ // 探针:覆盖「建视图时的头段」;降级(无视图)时退化为旧行为,最多取满 rowCap。
429
+ const probeLimit = canView
430
+ ? Math.min(totalRows, Math.max(cfg.previewRows, cfg.viewThresholdRows))
431
+ : Math.min(totalRows, rowCap)
432
+ const probe = probeLimit > 0
433
+ ? await selectRows(db, plan.pageSql, [probeLimit, previewOffset])
434
+ : { rows: [] as Row[], columns: [] as string[] }
435
+ exec.signal.throwIfAborted()
436
+
437
+ const resultColumns = probe.columns.length > 0
438
+ ? probe.columns
439
+ : record.columns.map(column => column.sanitizedName)
440
+
441
+ const estimatedBytes = JSON.stringify(probe.rows).length
442
+ const useView = canView && (cfg.viewMode === 'always'
443
+ || (cfg.viewMode === 'auto' && (totalRows > cfg.viewThresholdRows || estimatedBytes > cfg.viewThresholdBytes)))
444
+
445
+ let headRows = probe.rows
446
+ let tailRows: Row[] = []
447
+ if (useView) {
448
+ const slicePlan = previewSlicePlan(totalRows, previewOptions)
449
+ headRows = probe.rows.slice(0, slicePlan.head)
450
+ if (slicePlan.tail > 0 && totalRows > headRows.length) {
451
+ const tailOffset = Math.max(0, totalRows - slicePlan.tail)
452
+ tailRows = (await selectRows(db, plan.pageSql, [slicePlan.tail, tailOffset])).rows
453
+ }
454
+ }
455
+ exec.signal.throwIfAborted()
456
+
457
+ const slice = buildPreviewSlice(headRows, tailRows, resultColumns, totalRows, previewOptions)
458
+ // 「还有行没展示」以真实命中数判定:上限收敛过的 totalRows 不能把它抹掉。
459
+ const truncated = matched > slice.rows.length
460
+
461
+ // 摘要基于全表聚合:只在「有视图」或「片段不完整」时才做,避免小结果集多跑一次扫描。
462
+ const typeByName = new Map(record.columns.map(column => [column.sanitizedName, column.type]))
463
+ const displayNameOf = new Map(record.columns.map(column => [column.sanitizedName, column.name]))
464
+ const summary = cfg.summaryEnabled && (useView || truncated)
465
+ ? (await summarizeColumns(
466
+ db,
467
+ record.tableName,
468
+ record.columns.map(column => ({ name: column.sanitizedName, type: column.type })),
469
+ previewOptions,
470
+ )).map(entry => ({ ...entry, name: displayNameOf.get(entry.name) ?? entry.name }))
471
+ : []
472
+
473
+ const columns = resultColumns.map(name => ({ name, type: typeByName.get(name) ?? 'text' }))
474
+
475
+ let view: QueryViewInfo | undefined
476
+ if (useView && services.views !== undefined) {
477
+ const registered = new Set(record.columns.map(column => column.sanitizedName))
478
+ const descriptor = await services.views.create({
479
+ scopeKey: scope.scopeKey,
480
+ datasetId: record.id,
481
+ name: record.name,
482
+ tableName: record.tableName,
483
+ columns,
484
+ countSql: plan.countSql,
485
+ totalRows: matched,
486
+ rowCap,
487
+ sortable: resultColumns.filter(name => registered.has(name)),
488
+ stable: plan.stable,
489
+ baseSql: plan.baseSql,
490
+ ...plan.baseOrder === undefined ? {} : { baseOrder: plan.baseOrder },
491
+ ...plan.tie === undefined ? {} : { tie: plan.tie },
492
+ })
493
+ debugLog('read:view', {
494
+ viewId: descriptor.viewId,
495
+ totalRows: descriptor.totalRows,
496
+ rowCap,
497
+ matched,
498
+ })
499
+ view = {
500
+ viewId: descriptor.viewId,
501
+ endpoint: descriptor.endpoint,
502
+ pageSize: descriptor.pageSize,
503
+ maxPageSize: descriptor.maxPageSize,
504
+ stable: descriptor.stable,
505
+ sortable: descriptor.sortable,
506
+ }
507
+ }
508
+
509
+ return {
510
+ datasetId: record.id,
511
+ name: record.name,
512
+ columns,
513
+ matchedRows: matched,
514
+ totalRows,
515
+ preview: {
516
+ rows: slice.rows,
517
+ columns: slice.columns,
518
+ headCount: headRows.length,
519
+ gap: slice.gap,
520
+ skipped: slice.skipped,
521
+ cellTruncated: slice.rows.some(row => slice.columns.some((column) => {
522
+ const value = row[column]
523
+ return value !== null && value !== undefined && String(value).length > cfg.previewCellChars
524
+ })),
525
+ columnTruncated: slice.columnTruncated,
526
+ hiddenColumns: Math.max(0, resultColumns.length - slice.columns.length),
527
+ },
528
+ summary,
529
+ limit: rowCap,
530
+ offset: previewOffset,
531
+ truncated,
532
+ ...view === undefined ? {} : { view },
533
+ }
534
+ },
535
+ })
536
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * 八个工具的聚合导出(设计文档 §5.1)。
3
+ */
4
+
5
+ import type { DataServices } from '../store'
6
+ import type { ToolDefinition } from '../tooling'
7
+ import { createImportTool } from './import'
8
+ import { createListTool, createQueryTool, createSchemaTool } from './read'
9
+ import { createDataSourceTools } from './datasource'
10
+ import { createDeleteTool, createDropTool, createInsertTool, createUpdateTool } from './write'
11
+
12
+ /** 数据源工具(`datasourceEnabled=false` 时整体不注册)。 */
13
+ export const DATA_SOURCE_TOOLS: ReadonlySet<string> = new Set([
14
+ 'datasource_list',
15
+ 'datasource_test',
16
+ 'datasource_tables',
17
+ 'datasource_import',
18
+ ])
19
+
20
+ /** 本插件注册的全部工具名。 */
21
+ export const DATASET_TOOLS: ReadonlySet<string> = new Set([
22
+ 'dataset_list',
23
+ 'dataset_schema',
24
+ 'dataset_query',
25
+ 'dataset_import',
26
+ 'dataset_insert',
27
+ 'dataset_update',
28
+ 'dataset_delete',
29
+ 'dataset_drop',
30
+ ...DATA_SOURCE_TOOLS,
31
+ ])
32
+
33
+ /** 破坏性工具:写类,默认走 `ctx.approval` 人工确认(fail-closed)。 */
34
+ export const WRITE_TOOLS: ReadonlySet<string> = new Set([
35
+ 'dataset_import',
36
+ 'dataset_insert',
37
+ 'dataset_update',
38
+ 'dataset_delete',
39
+ 'dataset_drop',
40
+ 'datasource_import',
41
+ ])
42
+
43
+ export function createToolDefinitions(services: DataServices): ToolDefinition[] {
44
+ const tools: ToolDefinition[] = [
45
+ createListTool(services),
46
+ createSchemaTool(services),
47
+ createQueryTool(services),
48
+ createImportTool(services),
49
+ createInsertTool(services),
50
+ createUpdateTool(services),
51
+ createDeleteTool(services),
52
+ createDropTool(services),
53
+ ]
54
+ if (services.cfg.datasourceEnabled) tools.push(...createDataSourceTools(services))
55
+ return tools
56
+ }