@xpert-ai/plugin-sdk 3.18.6 → 3.19.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 (47) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/MIDDLEWARE_APPS.md +71 -0
  3. package/README.md +2 -0
  4. package/data-workbench.cjs.js +217 -66
  5. package/data-workbench.esm.js +217 -66
  6. package/index.cjs.js +162 -46
  7. package/index.esm.js +148 -47
  8. package/package.json +2 -2
  9. package/runtime-capabilities.md +35 -0
  10. package/src/index.d.ts +3 -0
  11. package/src/lib/agent/index.d.ts +1 -0
  12. package/src/lib/agent/middleware/capabilities/assistant-task.d.ts +55 -20
  13. package/src/lib/agent/middleware/types.d.ts +13 -3
  14. package/src/lib/agent/runtime/capability.d.ts +29 -0
  15. package/src/lib/agent/runtime/index.d.ts +3 -0
  16. package/src/lib/agent/runtime/strategy.d.ts +50 -0
  17. package/src/lib/agent/runtime/types.d.ts +132 -0
  18. package/src/lib/assistant-capability/index.d.ts +42 -0
  19. package/src/lib/connector/runtime-capability.d.ts +2 -0
  20. package/src/lib/connector/strategy.interface.d.ts +8 -0
  21. package/src/lib/core/context/request-context.d.ts +6 -0
  22. package/src/lib/data/datasource/workbench-pagination.d.ts +6 -0
  23. package/src/lib/data/datasource/workbench-sql-lexer.d.ts +11 -0
  24. package/src/lib/data/datasource/workbench-sql-syntax.d.ts +3 -0
  25. package/src/lib/evolution/runtime-capability.d.ts +2 -1
  26. package/src/lib/integration/strategy.interface.d.ts +22 -0
  27. package/src/lib/mcp/app.d.ts +10 -0
  28. package/src/lib/project-task/index.d.ts +59 -0
  29. package/src/lib/project-type/index.d.ts +64 -0
  30. package/src/lib/rag/analyzer/index.d.ts +3 -0
  31. package/src/lib/rag/analyzer/strategy.decorator.d.ts +2 -0
  32. package/src/lib/rag/analyzer/strategy.interface.d.ts +11 -0
  33. package/src/lib/rag/analyzer/strategy.registry.d.ts +8 -0
  34. package/src/lib/rag/index.d.ts +1 -0
  35. package/src/lib/runtime/capabilities/knowledge-graph.d.ts +59 -0
  36. package/src/lib/runtime/capabilities/knowledgebase-documents.d.ts +21 -0
  37. package/src/lib/runtime/capabilities/knowledgebase.d.ts +8 -3
  38. package/src/lib/runtime/capabilities/project-access.d.ts +2 -1
  39. package/src/lib/runtime/capabilities/project-provisioning.d.ts +3 -0
  40. package/src/lib/runtime/runtime-scope.d.ts +7 -1
  41. package/src/lib/sandbox/execution-environment.d.ts +10 -0
  42. package/src/lib/sandbox/execution.d.ts +3 -0
  43. package/src/lib/sandbox/index.d.ts +1 -0
  44. package/src/lib/sandbox/protocol.d.ts +2 -0
  45. package/src/lib/tool-provider/types.d.ts +6 -0
  46. package/src/lib/types.d.ts +2 -1
  47. package/src/lib/workflow/trigger/strategy.interface.d.ts +2 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # @xpert-ai/plugin-sdk
2
2
 
3
+ ## 3.19.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 99d09ec: Release a coordinated minor update across all Xpert workspace packages, including the API, Web and Bosi applications, shared SDK and contracts, backend and UI libraries, bundled plugins, sandbox runtimes and documentation.
8
+
9
+ ### Patch Changes
10
+
11
+ - 8a78318: Add explicit application Project types, provider-backed domain bindings, and typed Project provisioning/access contracts. App plugins using ProjectTypeProvider require these releases (3.19.0 or newer) and the matching host Project type endpoints/schema migration.
12
+ - Updated dependencies [8a78318]
13
+ - Updated dependencies [1e44173]
14
+ - Updated dependencies [c953822]
15
+ - Updated dependencies [b7983c8]
16
+ - Updated dependencies [08f66e7]
17
+ - Updated dependencies [b62543f]
18
+ - Updated dependencies [5f26e05]
19
+ - Updated dependencies [99d09ec]
20
+ - @xpert-ai/contracts@3.19.0
21
+
22
+ ## 3.18.7
23
+
24
+ ### Patch Changes
25
+
26
+ - 5213185: Expose keyword analyzer strategy interfaces, discovery metadata, and a scoped registry with exact source lookup. Add optional plugin removal preflight guards so hosts can protect active analyzer dependencies.
27
+
28
+ Correct read-only SQL classification for derived tables, CTE column declarations and type modifiers, and preserve duplicate MySQL output names with direct bounded pagination.
29
+
3
30
  ## 3.18.6
4
31
 
5
32
  ### Patch Changes
@@ -0,0 +1,71 @@
1
+ # MCP Apps in Agent middleware
2
+
3
+ An `AgentMiddleware` can return `apps` alongside `tools`. It does not need a remote
4
+ MCP server or a published Toolset. The host renders the bundled HTML using its
5
+ existing MCP App sandbox and RPC bridge.
6
+
7
+ ChatKit accesses resources, RPC, approvals and teardown through `/api/ai/mcp-apps`,
8
+ using the AI module's `ApiKeyOrClientSecretAuthGuard`. The ordinary management
9
+ routes keep the platform's standard authorization. Use an SDK version whose
10
+ `client.mcp.apps` targets this AI endpoint.
11
+
12
+ ```ts
13
+ return {
14
+ name: 'ProjectSettings',
15
+ tools: [configure, readSettings, saveSettings],
16
+ apps: {
17
+ definitions: [{ key: 'settings', entry: 'dist/mcp-apps/settings.html' }],
18
+ tools: {
19
+ configure: { resourceKey: 'settings', visibility: ['model', 'app'] },
20
+ read_settings: { visibility: ['app'], approval: 'none' },
21
+ save_settings: { visibility: ['app'], approval: 'none' }
22
+ }
23
+ }
24
+ }
25
+ ```
26
+
27
+ Keys in `apps.tools` must match actual tool names. App-only tools are excluded
28
+ from the model's tool list. `approval: 'none'` is a trusted plugin declaration for
29
+ operations whose own services enforce scope, revision and validation. Omit it
30
+ to retain host approval. App access never grants access to another middleware.
31
+ For decorated providers, use `@XpertToolProvider({ apps: [...] })` and
32
+ `@XpertTool({ middleware: true, app: { ... } })` with the same binding contract.
33
+
34
+ To pause after a successful tool, configure the Assistant:
35
+
36
+ ```yaml
37
+ team:
38
+ agentConfig:
39
+ interruptAfter: [configure]
40
+ ```
41
+
42
+ The completed tool result is checkpointed before a separate gate calls
43
+ LangGraph `interrupt`. Resume targets the gate, so tool side effects are not
44
+ replayed. Ordinary tools display a Continue control; an App sends a standard
45
+ `ui/message` after saving. The host supplies the original tool-call and execution
46
+ identity, and ChatKit uses the existing `action: resume` protocol. No `ui/resume`
47
+ extension is required. The App's message is added once at user priority on resume,
48
+ so the Agent can distinguish the confirmed input from the pre-pause tool result.
49
+ Inline images, audio and binary resources are retained in this message (25 MiB
50
+ combined); arbitrary file paths and remote attachment URLs are rejected. ChatKit
51
+ acknowledges continuation only after the run response supplies `Content-Location`,
52
+ not when the local thread is resolved. Cross-origin hosts must expose that header.
53
+ Error ToolMessages skip this gate so the Agent can repair
54
+ the failure. Prefer calling interactive tools alone rather than mixing them with
55
+ unrelated parallel work.
56
+
57
+ The runtime re-resolves middleware bindings and enabled tools for each App RPC.
58
+ UI teardown retains paused native form snapshots for 7 days; it does not cancel
59
+ the Agent. Business settings must still be persisted by domain services before
60
+ continuation. This requires the host capability `platform.middleware.mcp-apps`
61
+ and the corresponding ChatKit continuation support; it adds no ORM dependency
62
+ to the SDK.
63
+
64
+ On hosts with schema synchronization disabled, apply
65
+ `packages/server-ai/src/mcp-app-runtime/migrations/20260928-middleware-app-audit.sql`
66
+ before enabling native middleware Apps. Their audit rows retain provider identity
67
+ in `source` and leave `toolsetId` null. Interactive ChatKit user sessions are
68
+ accepted only for their bound Assistant; existing user JWT access remains valid.
69
+ An App produced by an external Assistant is also accessible when its persisted
70
+ execution parent chain terminates at the session's entry Assistant in the same
71
+ thread. Each execution remains subject to the normal access check.
package/README.md CHANGED
@@ -115,6 +115,8 @@ For completed operations, `prepareToolResult(() => project(result), () => minima
115
115
 
116
116
  For a host-native MCP App, declare each static HTML bundle once in the Provider's `apps` array, then bind a Tool with `mcp.app.resourceKey` and include `app` visibility. The App key must exist in the same Provider and its entry must be a relative `.html` path inside the packaged plugin. The host publishes the bundle as a `ui://` resource; plugins must not create a stdio server or put CSP/credentials in Tool results.
117
117
 
118
+ Agent middleware can also expose these Apps without a published Toolset. See [middleware Apps and post-tool continuation](./MIDDLEWARE_APPS.md) for `app` bindings and `interruptAfter` checkpoints.
119
+
118
120
  The host owns the public MCP endpoint identity. It combines the plugin's stable `artifactNamespace`, the Provider key, and an opaque scope hash; client names and product-specific identifiers do not belong in the slug. A tenant- or system-level plugin has one tenant-scoped Publication and capability snapshot, while each organization receives an independent access grant and organization-bound credential. An organization-level plugin receives a dedicated organization-scoped Publication.
119
121
 
120
122
  ## View extensions
@@ -8,6 +8,159 @@ const DataSourceRuntimeCapability = Object.freeze({
8
8
  __type: undefined
9
9
  });
10
10
 
11
+ /** Mask comments and quoted content without changing source offsets. Shared by guards and pagination. */ function lexWorkbenchSql(source) {
12
+ if (!source.trim() || source.length > 100000 || source.includes('\0')) throw new Error('invalid_sql');
13
+ const masked = source.split('');
14
+ const quoted = new Map();
15
+ const hide = (start, end, kind)=>{
16
+ for(let i = start; i < end; i++)if (masked[i] !== '\n') masked[i] = ' ';
17
+ if (kind) {
18
+ const marker = start + 1;
19
+ quoted.set(marker, kind);
20
+ masked[marker] = kind === 'literal' ? '?' : 'i';
21
+ }
22
+ };
23
+ let index = 0;
24
+ while(index < source.length){
25
+ const start = index, c = source[index], next = source[index + 1];
26
+ if (c === '-' && next === '-' || c === '#') {
27
+ while(index < source.length && source[index] !== '\n')index++;
28
+ hide(start, index);
29
+ continue;
30
+ }
31
+ if (c === '/' && next === '*') {
32
+ if (source[index + 2] === '!') throw new Error('executable_comment_not_supported');
33
+ const end = source.indexOf('*/', index + 2);
34
+ if (end < 0) throw new Error('unterminated_comment');
35
+ index = end + 2;
36
+ hide(start, index);
37
+ continue;
38
+ }
39
+ if (c === "'" || c === '"' || c === '`') {
40
+ let closed = false;
41
+ index++;
42
+ while(index < source.length){
43
+ if (source[index] === '\\') throw new Error('use_parameters_for_backslash_literals');
44
+ if (source[index] === c) {
45
+ if (source[index + 1] === c) {
46
+ index += 2;
47
+ continue;
48
+ }
49
+ index++;
50
+ closed = true;
51
+ break;
52
+ }
53
+ index++;
54
+ }
55
+ if (!closed) throw new Error('unterminated_literal');
56
+ hide(start, index, c === "'" ? 'literal' : 'identifier');
57
+ continue;
58
+ }
59
+ if (c === '$') {
60
+ var _source_slice_match;
61
+ const tag = (_source_slice_match = source.slice(index).match(/^\$(?:[A-Za-z_][A-Za-z_0-9]*)?\$/)) == null ? void 0 : _source_slice_match[0];
62
+ if (tag) {
63
+ const end = source.indexOf(tag, index + tag.length);
64
+ if (end < 0) throw new Error('unterminated_literal');
65
+ index = end + tag.length;
66
+ hide(start, index, 'literal');
67
+ continue;
68
+ }
69
+ }
70
+ index++;
71
+ }
72
+ const text = masked.join('');
73
+ const tokens = [
74
+ ...text.matchAll(/[A-Z_][A-Z_0-9]*|[0-9]+|[^\s]/gi)
75
+ ].map((match)=>{
76
+ var _quoted_get;
77
+ return {
78
+ text: match[0].toUpperCase(),
79
+ start: match.index,
80
+ end: match.index + match[0].length,
81
+ kind: (_quoted_get = quoted.get(match.index)) != null ? _quoted_get : /^[A-Z_]/i.test(match[0]) ? 'word' : /^[0-9]/.test(match[0]) ? 'number' : 'symbol'
82
+ };
83
+ });
84
+ return {
85
+ masked: text,
86
+ tokens
87
+ };
88
+ }
89
+
90
+ const MODIFIED_TYPES = new Set('DECIMAL NUMERIC CHAR VARCHAR NCHAR NVARCHAR BINARY VARBINARY DATETIME TIME TIMESTAMP'.split(' '));
91
+ /** Positions where name(...) declares columns or a built-in type, rather than invoking a function. */ function sqlDeclarationPositions(tokens) {
92
+ const declarations = new Set();
93
+ const closing = new Map(), parents = new Map();
94
+ const stack = [];
95
+ tokens.forEach((token, index)=>{
96
+ if (stack.length) parents.set(index, stack[stack.length - 1]);
97
+ if (token.text === '(') stack.push(index);
98
+ if (token.text === ')') {
99
+ const open = stack.pop();
100
+ if (open !== undefined) closing.set(open, index);
101
+ }
102
+ });
103
+ const name = (index)=>{
104
+ var _tokens_index;
105
+ return [
106
+ 'word',
107
+ 'identifier'
108
+ ].includes((_tokens_index = tokens[index]) == null ? void 0 : _tokens_index.kind);
109
+ };
110
+ const is = (index, word)=>{
111
+ var _tokens_index;
112
+ return ((_tokens_index = tokens[index]) == null ? void 0 : _tokens_index.text) === word;
113
+ };
114
+ for(let i = 0; i < tokens.length; i++){
115
+ if (tokens[i].kind !== 'word' || !is(i, 'WITH')) continue;
116
+ let cursor = i + 1;
117
+ if (is(cursor, 'RECURSIVE')) cursor++;
118
+ while(name(cursor)){
119
+ const declaration = cursor++;
120
+ let columns = false;
121
+ if (is(cursor, '(')) {
122
+ const end = closing.get(cursor);
123
+ if (end === undefined || end === cursor + 1) break;
124
+ const columnTokens = tokens.slice(cursor + 1, end);
125
+ if (columnTokens.length % 2 === 0 || !columnTokens.every((token, n)=>n % 2 === 0 ? [
126
+ 'word',
127
+ 'identifier'
128
+ ].includes(token.kind) : token.text === ',')) break;
129
+ columns = true;
130
+ cursor = end + 1;
131
+ }
132
+ if (!is(cursor++, 'AS')) break;
133
+ if (is(cursor, 'NOT')) cursor++;
134
+ if (is(cursor, 'MATERIALIZED')) cursor++;
135
+ if (!is(cursor, '(')) break;
136
+ const end = closing.get(cursor);
137
+ if (end === undefined) break;
138
+ if (columns) declarations.add(tokens[declaration].start);
139
+ cursor = end + 1;
140
+ if (!is(cursor, ',')) break;
141
+ cursor++;
142
+ }
143
+ }
144
+ tokens.forEach((token, i)=>{
145
+ var _tokens_;
146
+ if (token.kind !== 'word' || !MODIFIED_TYPES.has(token.text) || !is(i + 1, '(')) return;
147
+ const end = closing.get(i + 1);
148
+ if (end === undefined) return;
149
+ const args = tokens.slice(i + 2, end);
150
+ if (![
151
+ 1,
152
+ 3
153
+ ].includes(args.length) || !args.every((arg, n)=>n % 2 ? arg.text === ',' : arg.kind === 'number')) return;
154
+ const parent = parents.get(i);
155
+ const owner = parent === undefined ? undefined : (_tokens_ = tokens[parent - 1]) == null ? void 0 : _tokens_.text;
156
+ const cast = owner === 'CAST' && is(i - 1, 'AS');
157
+ const convert = owner === 'CONVERT' && is(i - 1, ',');
158
+ const postgresCast = is(i - 1, ':') && is(i - 2, ':');
159
+ if (cast || convert || postgresCast) declarations.add(token.start);
160
+ });
161
+ return declarations;
162
+ }
163
+
11
164
  const WRITE = new Set([
12
165
  'INSERT',
13
166
  'UPDATE',
@@ -42,15 +195,16 @@ const FORBIDDEN = new Set([
42
195
  'DETACH'
43
196
  ]);
44
197
  const SAFE_FUNCTIONS = new Set('ABS ACOS ASIN ATAN ATAN2 AVG CAST CEIL CEILING COALESCE CONCAT CONCAT_WS CONVERT COS COUNT CURDATE CURRENT_DATE CURRENT_TIMESTAMP DATE DATE_ADD DATE_SUB DATE_FORMAT DATEDIFF DAY DAYOFMONTH DAYOFWEEK EXTRACT FLOOR FORMAT GREATEST GROUP_CONCAT IF IFNULL JSON_EXTRACT JSON_LENGTH JSON_UNQUOTE LEAST LEFT LENGTH LN LOG LOWER LPAD LTRIM MAX MD5 MIN MOD MONTH NOW NULLIF POWER RAND REPLACE REVERSE RIGHT ROUND ROW_NUMBER RANK DENSE_RANK LAG LEAD FIRST_VALUE LAST_VALUE LPAD RPAD RTRIM SIGN SIN SQRT STDDEV STDDEV_POP STDDEV_SAMP SUBSTR SUBSTRING SUM TAN TO_CHAR TO_DATE TO_TIMESTAMP TRIM TRUNC UPPER VAR_POP VAR_SAMP VERSION WEEK YEAR DATABASE CURRENT_DATABASE CURRENT_SCHEMA ARRAY_AGG STRING_AGG JSON_AGG JSON_BUILD_OBJECT PG_GET_VIEWDEF'.split(' '));
45
- const SQL_PARENS = new Set('IN AS EXISTS OVER PARTITION VALUES SELECT WITH AND OR NOT ON USING FILTER GROUPING ROLLUP CUBE DISTINCT EXPLAIN'.split(' '));
198
+ // FROM/JOIN can introduce a parenthesized subquery, not a function call.
199
+ const SQL_PARENS = new Set('IN AS EXISTS OVER PARTITION VALUES SELECT WITH FROM JOIN AND OR NOT ON USING FILTER GROUPING ROLLUP CUBE DISTINCT EXPLAIN'.split(' '));
46
200
  const UNSAFE_FUNCTIONS = /\b(pg_sleep|sleep|benchmark|dblink\w*|lo_\w+|pg_(?:read|write|ls|stat|terminate|cancel|reload|rotate|log|create|drop|promote|switch|backup|replication|advisory)\w*|get_lock|release_lock|load_file|nextval|setval|set_config|http\w*|s3|hdfs|jdbc|mysql|postgresql|file|url)\s*\(/i;
47
201
  function splitWorkbenchSql(source) {
48
- if (!source.trim() || source.length > 100000 || source.includes('\0')) throw new Error('invalid_sql');
202
+ const { masked, tokens } = lexWorkbenchSql(source);
49
203
  const statements = [];
50
- let start = 0, index = 0, clean = '';
204
+ let start = 0;
51
205
  const finish = (end)=>{
52
- const text = clean.trim();
53
- if (text) {
206
+ const text = masked.slice(start, end);
207
+ if (text.trim()) {
54
208
  var _text_toUpperCase_match;
55
209
  const words = (_text_toUpperCase_match = text.toUpperCase().match(/[A-Z_][A-Z_0-9]*/g)) != null ? _text_toUpperCase_match : [];
56
210
  const first = words[0];
@@ -61,10 +215,11 @@ function splitWorkbenchSql(source) {
61
215
  if (words.some((word, index)=>WRITE.has(word) && !(first === 'SHOW' && index === 1 && word === 'CREATE') || FORBIDDEN.has(word)) || UNSAFE_FUNCTIONS.test(text) || /\bFOR\s+(UPDATE|SHARE)\b/i.test(text) || /:=|\bEXPLAIN\s+ANALYZE\b|@/i.test(text)) effect = 'unsupported';
62
216
  // eslint-disable-next-line no-control-regex -- intentional: reject any non-ASCII/control input
63
217
  if (/[^\x00-\x7f]/.test(text) || /\.\s*[A-Z_][A-Z_0-9]*\s*\(/i.test(text)) effect = 'unsupported';
218
+ const declarations = sqlDeclarationPositions(tokens.filter((token)=>token.start >= start && token.end <= end));
64
219
  if ([
65
220
  ...text.matchAll(/([A-Z_][A-Z_0-9]*)\s*\(/gi)
66
- ].some((match)=>!SAFE_FUNCTIONS.has(match[1].toUpperCase()) && !SQL_PARENS.has(match[1].toUpperCase()))) effect = 'unsupported';
67
- if (first === 'SHOW' && !/^(SHOW\s+(DATABASES|SCHEMAS|CATALOGS|TABLES|FULL\s+TABLES|COLUMNS|FULL\s+COLUMNS|INDEX|INDEXES|KEYS|CREATE\s+TABLE|CREATE\s+VIEW|PARTITIONS|VARIABLES|STATUS|GRANTS))\b/i.test(text)) effect = 'unsupported';
221
+ ].some((match)=>!declarations.has(start + match.index) && !SAFE_FUNCTIONS.has(match[1].toUpperCase()) && !SQL_PARENS.has(match[1].toUpperCase()))) effect = 'unsupported';
222
+ if (first === 'SHOW' && !/^(SHOW\s+(DATABASES|SCHEMAS|CATALOGS|TABLES|FULL\s+TABLES|COLUMNS|FULL\s+COLUMNS|INDEX|INDEXES|KEYS|CREATE\s+TABLE|CREATE\s+VIEW|PARTITIONS|VARIABLES|STATUS|GRANTS))\b/i.test(text.trim())) effect = 'unsupported';
68
223
  }
69
224
  // CREATE/ALTER routines, external resources and privileged SQL are outside this app.
70
225
  if (effect === 'write' && /\b(FUNCTION|PROCEDURE|TRIGGER|USER|ROLE|CATALOG|RESOURCE|SYSTEM|SERVER|EXTENSION|FILE|OUTFILE|DUMPFILE|EXECUTE)\b/i.test(text)) effect = 'unsupported';
@@ -74,63 +229,10 @@ function splitWorkbenchSql(source) {
74
229
  effect
75
230
  });
76
231
  }
77
- clean = '';
78
232
  start = end + 1;
79
233
  };
80
- while(index < source.length){
81
- const c = source[index], next = source[index + 1];
82
- if (c === '-' && next === '-' || c === '#') {
83
- while(index < source.length && source[index] !== '\n')index++;
84
- clean += ' ';
85
- continue;
86
- }
87
- if (c === '/' && next === '*') {
88
- if (source[index + 2] === '!') throw new Error('executable_comment_not_supported');
89
- const end = source.indexOf('*/', index + 2);
90
- if (end < 0) throw new Error('unterminated_comment');
91
- index = end + 2;
92
- clean += ' ';
93
- continue;
94
- }
95
- if (c === "'" || c === '"' || c === '`') {
96
- const quote = c;
97
- let closed = false;
98
- index++;
99
- while(index < source.length){
100
- if (source[index] === '\\') throw new Error('use_parameters_for_backslash_literals');
101
- if (source[index] === quote) {
102
- if (source[index + 1] === quote) {
103
- index += 2;
104
- continue;
105
- }
106
- index++;
107
- closed = true;
108
- break;
109
- }
110
- index++;
111
- }
112
- if (!closed) throw new Error('unterminated_literal');
113
- clean += quote === "'" ? ' ? ' : ' identifier ';
114
- continue;
115
- }
116
- if (c === '$') {
117
- var _source_slice_match;
118
- const tag = (_source_slice_match = source.slice(index).match(/^\$(?:[A-Za-z_][A-Za-z_0-9]*)?\$/)) == null ? void 0 : _source_slice_match[0];
119
- if (tag) {
120
- const end = source.indexOf(tag, index + tag.length);
121
- if (end < 0) throw new Error('unterminated_literal');
122
- index = end + tag.length;
123
- clean += ' ? ';
124
- continue;
125
- }
126
- }
127
- if (c === ';') {
128
- finish(index);
129
- index++;
130
- continue;
131
- }
132
- clean += c;
133
- index++;
234
+ for (const token of tokens){
235
+ if (token.kind === 'symbol' && token.text === ';') finish(token.start);
134
236
  }
135
237
  finish(source.length);
136
238
  if (!statements.length || statements.length > 50) throw new Error('statement_count_exceeded');
@@ -148,6 +250,53 @@ function quoteDatabaseIdentifier(name, engine) {
148
250
  return quote + name.split(quote).join(quote + quote) + quote;
149
251
  }
150
252
 
253
+ /** Apply a page to a MySQL result without turning its (possibly duplicate) output names into table columns. */ function paginateMysqlQuery(sql, parameters = [], limit, offset) {
254
+ const { tokens } = lexWorkbenchSql(sql);
255
+ const top = [];
256
+ let depth = 0;
257
+ for (const token of tokens){
258
+ if (token.text === '(') depth++;
259
+ if (!depth) top.push(token);
260
+ if (token.text === ')') depth--;
261
+ if (depth < 0) throw new Error('unsupported_pagination');
262
+ }
263
+ if (depth) throw new Error('unsupported_pagination');
264
+ const start = top.findIndex((token)=>token.kind === 'word' && token.text === 'LIMIT');
265
+ if (start < 0) return {
266
+ sql: `${sql}\nLIMIT ${limit + 1} OFFSET ${offset}`,
267
+ parameters
268
+ };
269
+ const clause = top.slice(start + 1);
270
+ const bindings = tokens.filter((token)=>token.text === '?' && token.kind === 'symbol');
271
+ const consumed = new Set();
272
+ const integer = (token)=>{
273
+ if ((token == null ? void 0 : token.kind) === 'number') return BigInt(token.text);
274
+ if ((token == null ? void 0 : token.kind) === 'symbol' && token.text === '?') {
275
+ const index = bindings.indexOf(token), value = parameters[index];
276
+ if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0) {
277
+ consumed.add(index);
278
+ return BigInt(value);
279
+ }
280
+ }
281
+ throw new Error('unsupported_pagination');
282
+ };
283
+ let count, skip = 0n;
284
+ if (clause.length === 1) count = integer(clause[0]);
285
+ else if (clause.length === 3 && clause[1].text === ',') {
286
+ skip = integer(clause[0]);
287
+ count = integer(clause[2]);
288
+ } else if (clause.length === 3 && clause[1].kind === 'word' && clause[1].text === 'OFFSET') {
289
+ count = integer(clause[0]);
290
+ skip = integer(clause[2]);
291
+ } else throw new Error('unsupported_pagination');
292
+ const remaining = count > BigInt(offset) ? count - BigInt(offset) : 0n;
293
+ const size = remaining < BigInt(limit + 1) ? remaining : BigInt(limit + 1);
294
+ return {
295
+ sql: `${sql.slice(0, top[start].start)}LIMIT ${size} OFFSET ${skip + BigInt(offset)}${sql.slice(clause[clause.length - 1].end)}`,
296
+ parameters: parameters.filter((_, index)=>!consumed.has(index))
297
+ };
298
+ }
299
+
151
300
  const number = (value)=>Number(value != null ? value : 0);
152
301
  const string = (value)=>value == null ? '' : String(value);
153
302
  const cell = (result, row, name)=>row[result.columns.findIndex((column)=>column.name.toLowerCase() === name.toLowerCase())];
@@ -389,12 +538,14 @@ const cell = (result, row, name)=>row[result.columns.findIndex((column)=>column.
389
538
  if (input.mode === 'write' && !(await this.capabilities()).writes) throw new Error('engine_version_writes_unavailable');
390
539
  if (input.mode === 'write' && offset) throw new Error('mutation_cannot_be_paginated');
391
540
  let sql = statements[0].sql;
392
- // Wrap a SELECT instead of appending LIMIT after an existing LIMIT or trailing comment.
541
+ let parameters = input.parameters;
393
542
  if (input.mode === 'read' && [
394
543
  'SELECT',
395
544
  'WITH'
396
- ].includes(statements[0].words[0])) sql = `SELECT * FROM (\n${sql}\n) AS ${this.quote('_db_studio_page')} LIMIT ${limit + 1} OFFSET ${offset}`;
397
- else if (offset) throw new Error('statement_cannot_be_paginated');
545
+ ].includes(statements[0].words[0])) {
546
+ if (this.engine === 'mysql') ({ sql, parameters } = paginateMysqlQuery(sql, parameters, limit, offset));
547
+ else sql = `SELECT * FROM (\n${sql}\n) AS ${this.quote('_db_studio_page')} LIMIT ${limit + 1} OFFSET ${offset}`;
548
+ } else if (offset) throw new Error('statement_cannot_be_paginated');
398
549
  this.busy = true;
399
550
  let readTransaction = false;
400
551
  try {
@@ -403,7 +554,7 @@ const cell = (result, row, name)=>row[result.columns.findIndex((column)=>column.
403
554
  readTransaction = true;
404
555
  }
405
556
  var _input_timeoutMs;
406
- const result = await this.raw(sql, input.parameters, limit + 1, signal, Math.max(1, Math.min((_input_timeoutMs = input.timeoutMs) != null ? _input_timeoutMs : 30000, 120000)));
557
+ const result = await this.raw(sql, parameters, limit + 1, signal, Math.max(1, Math.min((_input_timeoutMs = input.timeoutMs) != null ? _input_timeoutMs : 30000, 120000)));
407
558
  const hasMore = result.rows.length > limit || result.truncated;
408
559
  return _extends._extends({}, result, {
409
560
  rows: result.rows.slice(0, limit),