# kSQL read-only エンジン・ライブラリ
v3.19.0 から、kSQL の read-only エンジンを他の kintone プラグインや
カスタマイズへ組み込めます。既存 plugin / CLI / MCP の entry とは独立した、
純加法の公開面です。
## インストールと使用例
npm から取り込める plugin / bundler 環境では、依存解決と版固定が明確な npm bundle
を優先してください。
### ESM
```js
import {
createReadonlyKintoneClient,
KsqlEngineError,
runBatch,
runQuery,
version,
} from "@rex0220/kintone-sql-tools/engine";
const client = createReadonlyKintoneClient();
const result = await runQuery(
"SELECT 'ok' AS status, 19 AS release",
{ client, maxRecords: 3000, cursorMaxActive: 2 }
);
console.log(version, result.rows);
// => 3.19.0 [{ status: "ok", release: "19" }]
const batch = await runBatch(
"CREATE TEMP TABLE #ids AS SELECT $id FROM APP100; SELECT * FROM #ids",
{ client }
);
console.log(batch.results.at(-1)?.rows);
```
### CommonJS
```js
const {
createReadonlyKintoneClient,
runQuery,
version,
} = require("@rex0220/kintone-sql-tools/engine");
const client = createReadonlyKintoneClient();
runQuery("SELECT 'ok' AS status, 19 AS release", { client })
.then((result) => console.log(version, result.rows))
.catch(console.error);
```
### UMD
`dist-engine/ksql-engine.umd.js` を読み込んだ後、consumer は必ず
`window.ksql.get("3.19.0")` で版を明示してください。`window.ksql` 自体を engine
として扱ったり、`versions` の先頭を暗黙選択したりしないでください。
```html
```
実アプリを読む場合は SQL を、たとえば
`SELECT 顧客名, SUM(金額) AS 合計 FROM APP100 GROUP BY 顧客名` に置き換えます。
## 公開 API
value export は次の6つです。
- `version: string`: build 済みライブラリの版。v3.19.0 では `"3.19.0"`。
- `createReadonlyKintoneClient(options?)`: kintone browser global を使う read-only client。
- `runQuery(sql, options): Promise`: read-only 単文を実行。
- `runBatch(sql, options): Promise`: read-only 複文を順次実行。一時テーブル、
`SET` / `DECLARE`、`ASSERT` を含められる。
- `explainQuery(sql, options): Promise`: `SELECT` / `WITH` / `UNION`
の plan を返す。SQL 先頭の `EXPLAIN` はあってもなくてもよい。
- `KsqlEngineError`: `code`、`message`、任意の `cause` を持つ公開 error class。
公開型は次の専用 DTO だけです。内部 AST、executor、DML、IMPORT、APPLY、MCP の型は
公開しません。
- query/result: `RunQueryOptions`、`RunBatchOptions`、`QueryColumn`、`QueryMetrics`、
`QueryResult`、`BatchResultItem`、`BatchStatementInfo`、`BatchResult`、`ExplainResult`
- client: `ReadonlyKintoneClient`、`ReadonlyGetRecordsParams`、
`ReadonlyGetRecordsResult`、`ReadonlyKintoneRecord`、
`ReadonlyKintoneFieldValue`
- Cursor: `ReadonlyCursorOpenParams`、`ReadonlyCursorHandle`、
`ReadonlyCursorPage`
- metadata: `ReadonlyAppInfo`、`ReadonlyFieldInfo`、`ReadonlyNumberPrecision`、
`ReadonlyNumberRoundingMode`、`ReadonlyProcessStatuses`、
`ReadonlyProcessStatusState`
- browser: `CreateReadonlyKintoneClientOptions`
`RunQueryOptions` と `RunBatchOptions`(および `explainQuery` の options)は、任意の
`logicalApps?: Readonly>` を受け取ります。キーは `LAPP_` を除いた
論理名、値は正の安全整数の物理アプリ ID です。SQL の `LAPP_` は実行前に解決され、
未定義名・不正キー・重複する canonical キー・不正 ID は kintone API 呼び出し前に
名前入りエラーになります。
```ts
await runBatch(
"SELECT c.業種 FROM LAPP_案件管理 d JOIN LAPP_顧客管理 c ON d.顧客No_ = c.顧客No",
{ client, logicalApps: { 案件管理: 4149, 顧客管理: 4148 } }
);
```
論理名は NFC 正規化後に大文字化されます。ASCII / 全角英字は大小同一で、日本語名も
利用できます。browser engine-library は `@profile` を持たないため、`LAPP_X@prod` と
`APP1@prod` は明示エラーです。解決後の実行エラーには `LAPP_ -> APP` の対応が
併記されます。`LAPP_` を含まない通常 SQL の挙動とエラー文言は変わりません。
### 結果の値はすべて文字列
`QueryResult.rows` のセルは、NUMBER、日時、集計値、record id を含めて
**すべて `string`** です。`QueryColumn.valueType` も常に `"string"` です。
数値計算や日時処理が必要な consumer は、用途に応じて明示変換してください。
```ts
type QueryResult = {
type: "query";
rows: readonly Readonly>[];
columns: readonly {
name: string;
displayName?: string;
valueType: "string";
fieldType?: string;
sortKind?: "number" | "string";
sourceApp?: number;
}[];
rowCount: number;
warnings: readonly string[];
validateStats?: {
errorRecords: number;
errorCount: number;
constraintMetadata?: {
present: ("required" | "length" | "range" | "choice")[];
absent: ("required" | "length" | "range" | "choice")[];
};
};
metrics: QueryMetrics;
};
```
`QueryColumn` の追加メタはすべて optional です。
- `displayName`: 表示用の列名。明示別名では SQL に書かれた表記(バッククォートを
除いた中身)を保持します。列の照合と `rows` のキーには `name` を使用してください。
- `fieldType`: 元の kintone フィールド型(`NUMBER`、`DROP_DOWN`、`__ID__` など)、
または engine が導出した擬似型(`KSQL_NUMBER`、`KSQL_STRING`、
`KSQL_UNKNOWN` など)。
- `sortKind`: 列の比較種別。決定できる場合は `"number"` または `"string"`。
unsupported な列では `undefined`。型が安全に確定しない列は
`KSQL_UNKNOWN` / `"string"` へ degrade する場合があります。
- `sourceApp`: CTE / 一時テーブルを介さない物理 SELECT で、出力列が直接の
フィールド参照または `$id` 等のシステム列として一意な物理アプリへ解決できる場合の
app ID。式、集計、CASE、曖昧な JOIN 列、CTE / 一時テーブル由来の列では
`undefined` です。UNION では左右が同じ app ID に一致するときだけ保持します。
`undefined` のメタ項目は結果オブジェクト自体に含まれません。consumer は追加メタの
存在を前提にせず、必要に応じてフォールバックしてください。
`QueryMetrics` は `recordGetCalls`、`fetchedRows`、`elapsedMs`、
`cursorRecordsScanned` に加え、取得上限による打ち切りを表す
`limitReached?` と `limitReachedApps?` を返します。現行 engine は打ち切りが
ない場合も `limitReached: false` と `limitReachedApps: []` を常に設定します。
プロパティが optional なのは、旧版 engine の結果および consumer が構築する既存の
`QueryResult` との型互換性を保つためです。
打ち切りの判定には `limitReached` を使ってください。`limitReachedApps` は判明した
アプリ ID を重複なし・昇順で示す補助情報であり、空配列かどうかを判定条件には
しません。JOIN の上限判定はアプリごとで、`fetchedRows` の合計が `maxRecords` を
超えただけでは `limitReached` は `true` になりません。`runBatch` の各結果では、
ほかの metrics と同じくバッチ全体の集計値を返します。
`ExplainResult` は `lines`、`text` と同じ metrics shape に加え、取得計画を
`plan?` で返します。`plan.statements[]` は文ごと、各 `sources[]` は kintone から
取得する物理アプリだけを保持します。一時テーブルや CTE の参照は source に含めず、
存在しないアプリ ID を補いません。`role` は文の主 FROM を表す `main`、JOIN 先の
`join`、UNION 枝の `union`、CTE 本体の `cte`、サブクエリの `subquery` のいずれかです。
`CREATE TEMP TABLE ... AS SELECT` の物理アプリも内側 SELECT の主 FROM なので `main` です。
各 source は `fetch`、`pending`、`kintoneQuery`、`limit` を保持します。プロパティは旧版
および consumer が構築する既存オブジェクトとの型互換性のため optional ですが、
現行 engine は常に設定します。
文と source の `fetch` は同じ 5 値 `"none" | "count_only" | "exact" |
"prefiltered" | "all"` です。最悪値の順序は `none` < `count_only` < `exact` <
`prefiltered` < `all` です。`none` は kintone から取得する source が無い文でのみ現れ、
`COUNT(*)` を `totalCount` の単発 GET で解く source は `count_only` になります。
物理 source がない文も `statements[]` には残り、`sources: []` と `fetch: "none"` を
返します。一方、人間向け `text` / `lines` は、その文の `fetch summary:` を表示しません。
これは意図的な差です。構造は取得なしという事実と文ごとの形を保持し、テキストは表示上の
雑音を省いています。
`fetch` の値は将来増えうるため、consumer は未知の値をエラーにせず未分類として
扱ってください。**このとき、未知の値を「軽い側」(`none` / `count_only` / `exact`)へ
寄せないでください。**将来より重い区分が加わったとき、**全件取得を安全と誤って見せる**ためです。
未分類として扱い、必要なら計画本文を確認するよう促す形が安全です。
## client の供給
### browser factory
`createReadonlyKintoneClient({ cursorMaxActive? })` は kintone ページ内の
`kintone.api` / `fetch` を使います。write method や page lifecycle listener を
公開 client へ追加しません。factory instance ごとに Cursor lease を分離します。
### BYO readonly client
Node や独自 proxy、既存 transport を使う場合は、次の6 read methodをすべて持つ
`ReadonlyKintoneClient` を渡します。
```ts
interface ReadonlyKintoneClient {
getRecords(params: ReadonlyGetRecordsParams): Promise;
openCursor(params: ReadonlyCursorOpenParams): Promise;
getApps(): Promise;
getFields(appId: number): Promise;
getNumberPrecision(appId: number): Promise;
getProcessStatuses(appId: number): Promise;
}
```
BYO client に余分な write method があっても engine は6 methodだけを別 objectへ射影
します。ただし、guest space の URL、reverse proxy、認証 header、retry、network
policy などの route / transport 契約は **BYO client 側の責務**です。engine は
guest / proxy route を推測、補正、再構築しません。`openCursor()` が返す handle の
`close()` は idempotent にしてください。
### `getRecords()` の契約
**応答をそのまま返してください。`records` 以外の項目を落とさないでください。**
| | |
|---|---|
| **必ず残す** | **`searchAborted`。**応答に含まれている値を落とすと、検索打ち切り(10 万件)に対する fail-closed が無効になり、打ち切られた結果を完全な結果として扱います。これは結果の**正しさ**に関わります |
| **残すと速い** | `totalCount`。`params.totalCount === true` を kintone へ渡し、応答の値をそのまま返すと `SELECT COUNT(*)` を1リクエストで取得できます。落としても正しさは保たれ、全件取得へフォールバックします。影響するのは**性能**です |
**キャッシュ・計測・リトライのために client を包む場合も同じです。**
`createReadonlyKintoneClient()` を使っていても、応答を組み立て直すラッパーで項目が
落ちることがあります。次は説明用の断片であり、単独で実行するコードではありません。
```ts
// 誤り — totalCount と searchAborted が落ちる
return { records: [...res.records] };
// 正しい — 応答をそのまま返す
return res;
```
6 read method のうち、**追加項目が任意プロパティである応答は `getRecords()` だけ**です。
`ReadonlyCursorPage.next` と `ReadonlyCursorHandle.totalCount` は必須なので、落とすと
型エラーになります。`searchAborted` を保持した後の engine の挙動は
[検索打ち切りと Cursor](#検索打ち切りと-cursor)を参照してください。
### `getFields()` の契約
**`/k/v1/app/form/fields.json` が返すフィールドだけを返してください。**
| | |
|---|---|
| **渡す** | `code` / `label` / `fieldType`、および制約メタデータ(`required` / `minLength` / `maxLength` / `minValue` / `maxValue` / `optionOrder`)。渡さないと `VALIDATE` が該当する制約を検証しません |
| **渡さない** | **`$id` / `$revision` などの擬似フィールド。**engine が内部で合成するため、`getFields()` から返す必要はありません |
**擬似フィールドを足すと、0 行の `SELECT *` で列を復元する際に未知の `fieldType` として
`ArgumentError` で停止します**(推測で列を作らず fail-closed にしています)。
```
ArgumentError: getFields returned unknown fieldType "__ID__" for field "$id".
getFields must return only the fields from /k/v1/app/form/fields.json;
$id and $revision are synthesized by the engine and must not be added.
```
`$id` / `$revision` は `getFields()` へ足さなくても、`SELECT $id, 案件名` や
`WHERE $id <= 10`、JOIN の結合キー押し下げはすべて動作します。
`createReadonlyKintoneClient()` はこの契約を満たしているため、factory 利用者の対応は不要です。
## options
`runQuery(sql, options)`、`runBatch(sql, options)`、`explainQuery(sql, options)` は、
実行前に未知 key と不正値を拒否します。暗黙の clamp は行いません。
| option | 対象 | 契約 |
|---|---|---|
| `client` | 3 API | 必須。6 read methodを持つ client |
| `maxRecords` | 3 API | 正の safe integer。取得上限 |
| `onLimitReached` | `runQuery` / `runBatch` | `"error"` または `"truncate"`。完全入力が必要な query は truncate せず fail-closed |
| `fetchParallel` | 3 API | 正の safe integer。並列取得数 |
| `cursorMaxActive` | 3 API | 1〜5 の整数。query 内 Cursor 上限 |
| `variables` | `runBatch` | `DECLARE` 変数への文字列注入。キーは `@` なし・大文字小文字を区別しない。`SET` 変数へは注入不可。`DECLARE @x RELATIVE_DATE` には `THIS_MONTH()` / `FROM_TODAY(-1, MONTHS)` 等の日付系関数トークンを渡せる |
| `tempTableMaxRows` | `runBatch` | 一時テーブル1表の実体化上限。既定10,000。超過は `onLimitReached: "truncate"` でも error |
`createReadonlyKintoneClient()` 自体の option は `cursorMaxActive`(1〜5)のみです。
`RELATIVE_DATE` 変数は WHERE の比較右辺・BETWEEN 境界だけで使用でき、通常の相対日付関数と
同じ exact pushdown 判定を受けます。不正トークン・配置違反は read API の前に拒否されます。
library は read-only 境界を持つため DML へは到達しません。注釈なし `DECLARE` の文字列束縛は不変です。
## read-only 境界
利用可能な構文は API ごとに異なります。
| API | 利用可能な構文 |
|---|---|
| `runQuery()` | 単文の `SELECT`、`WITH`、`UNION [ALL]`、`SHOW APPS`、`DESCRIBE` / `DESC`、既存レコードの `VALIDATE` |
| `runBatch()` | 上記の行を返す文、`CREATE TEMP TABLE ... AS SELECT/WITH`、`DROP TEMP TABLE`、`SET`、`DECLARE`、`ASSERT`、`EXPLAIN` |
| `explainQuery()` | 単文の `SELECT`、`WITH`、`UNION [ALL]`。SQL 先頭の `EXPLAIN` は任意 |
次は parse できても `READ_ONLY_VIOLATION` で拒否します。
- DML: `INSERT`、`UPDATE`、`UPDATE ... FROM`、`UPSERT`、`DELETE`、`REORDER`
- `APPLY` を含む文
- `IMPORT`
- DML の `VALIDATE ONLY`
malformed SQL は `PARSE_ERROR` です。allowlist と、write methodを持たない client
射影の二重境界で mutation API を呼ばないようにします。
### `VALIDATE` のメタデータ完全性と内訳集計
既存レコードの `VALIDATE` が検証できる制約は、client の `getFields()` が返す
`ReadonlyFieldInfo` に依存します。`required`、`minLength`、`maxLength`、
`minValue`、`maxValue`、`optionOrder` を渡さない場合、`VALIDATE` は該当する制約を
検証せず、違反があっても0件を返すことがあります。
`createReadonlyKintoneClient()` は `/k/v1/app/form/fields.json` の制約メタデータを
自動的に渡すため、factory 利用者の変更は不要です。BYO readonly client は同 API の
値を `ReadonlyFieldInfo` へ渡してください。
`validateStats.constraintMetadata` は、実際の `VALIDATE` 対象フィールドについて、
client から渡された制約メタデータの種別を開示します。`present` は含まれていた種別、
`absent` は既知4種のうち含まれていなかった種別です。対応は
`required`=必須、`length`=`minLength` / `maxLength`、`range`=
`minValue` / `maxValue`、`choice`=`optionOrder` です。配列はこの順で安定します。
```json
{
"errorRecords": 0,
"errorCount": 0,
"constraintMetadata": {
"present": ["choice"],
"absent": ["required", "length", "range"]
}
}
```
これは**入力メタデータの観測事実**であり、「アプリに制約が無い」「BYO client が
制約を落とした」という推測や警告ではありません。たとえば上の結果は
「選択肢だけを検証対象にして0件」までを示します。単に「0件」と読むより、
検証範囲を同時に表示することで誤った安心を避けられます。全4種が `absent` でも
警告は返しません。`CHECK` と NUMBER の型・精度検証は、この4種の
フォーム制約メタデータ一覧には含みません。
`validateStats.errorCount` は集約前の違反総数で、結果行の `$err_count` 合計と一致します。
`$err_code` 別などの内訳は `COUNT(*)` ではなく `SUM($err_count)` で集計してください。
サブテーブルの同一違反は1行へまとまり、本数が `$err_count` に入るためです。たとえば
KPI カードに `errorCount`、隣の棒グラフに `$err_code` 別の `COUNT(*)` を置くと、
同じ画面で合計が食い違って見えます。
```sql
VALIDATE APP100 INTO #err;
SELECT $err_code, SUM($err_count) AS errorCount
FROM #err
GROUP BY $err_code;
```
### `runBatch` の成功・失敗契約
`runBatch()` は文が1つでも失敗したら `KsqlEngineError` を throw し、
`BatchResult` や途中までの `results` を返しません。成功結果に `ok` フィールドは
ありません。失敗した文は error の `statementIndex`(0-based)と
`statementType` で特定できます。
これは、プログラム API で部分結果が完全な結果に見えたままアプリケーションロジックへ
流れ込む事故を防ぐための fail-closed 契約です。
```js
try {
const batch = await runBatch(sql, { client });
render(batch.results);
} catch (error) {
if (error instanceof KsqlEngineError) {
console.error(error.code, error.statementIndex, error.statementType);
}
throw error;
}
```
`BatchResult.results[]` は行を返した文だけを `QueryResult` として格納します。
各要素の `metrics` は**文別計測ではなく、同一のバッチ全体集計値**です。
個々の文の性能コストとして解釈しないでください。
### 一時テーブルのメモリと上限
一時テーブルは `runBatch()` 呼び出し単位で、**利用者アプリのプロセス内メモリ**へ
実体化されます。1表の上限は `tempTableMaxRows`(既定10,000行)で、超過は
`onLimitReached: "truncate"` を指定しても常に error です。同時に存在できるのは
最大16表です。`DROP TEMP TABLE` でメモリと枠を解放すれば、同じバッチ内で次の表に
その枠を再利用できます。
### 一時テーブル、JOIN、server-only 関数
server-only 関数は、入力が物理アプリだけである間に絞り込みへ使ってください。
物理アプリ同士を JOIN する `CREATE TEMP TABLE ... AS SELECT` の source では使えます。
一方、実体化済みの一時テーブルが入力に1つでも含まれる SELECT / JOIN は文全体が
対象外となり、`..._CONTEXT_UNSUPPORTED` で拒否されます。関数を物理アプリ側の列へ
置いても許可されません。
```sql
-- OK: 物理アプリ同士を JOIN し、関数で絞ってから実体化
CREATE TEMP TABLE #当月 AS
SELECT d.顧客No AS k, d.売上, c.業種
FROM APP100 d INNER JOIN APP200 c ON d.顧客No = c.顧客No
WHERE d.受注日 = THIS_MONTH();
-- NG: 一時テーブルが入力に含まれる文で server-only 関数を使う
CREATE TEMP TABLE #x AS SELECT 顧客No, 受注日 FROM APP100;
SELECT *
FROM #x a INNER JOIN APP200 c ON a.顧客No = c.顧客No
WHERE c.受注日 = THIS_MONTH();
```
### 一時テーブル source の計画を読む
`EXPLAIN` / `explainQuery()` は `CREATE TEMP TABLE` 自体を受け付けません。
`AS` 以降の `SELECT`(または `WITH`)を単体で `EXPLAIN` すると、実体化前に使われる
同じ取得・JOIN 計画を確認できます。
### 検索打ち切りと Cursor
client が `searchAborted: true` を返した場合、simple query、JOIN、GROUP BY を問わず
常に `SEARCH_ABORTED` の **hard error** です。部分行や warning result は返しません。
client が応答項目を保持する義務は
[`getRecords()` の契約](#getrecords-の契約)を参照してください。
query が開いた Cursor は、成功、query error、次ページ error のいずれでも query 終了時
に `close()` します。close error が主 error を隠すことはありません。
## Cursor が使われる条件
`openCursor()`(kintone の Cursor API `/records/cursor.json`)が呼ばれるのは、
**`KORDER BY` の窓が単発 GET に収まらない場合だけ**です。それ以外の読み取りでは
呼ばれません。
### 使われる
`KORDER BY` を含み、かつ次の**いずれか**に当てはまるとき(=単発 GET の条件を
満たさないとき):
- `LIMIT` が **500 超**
- `OFFSET` が **10,000 超**
- `LIMIT` が `maxRecords` 超
さらに Cursor 実行には **`OFFSET + LIMIT ≤ maxRecords`** が必要です。これを超えると
**レコード取得も Cursor 作成もせずに**失敗し、**別方式へフォールバックしません**。
公開 `code` は `EXECUTION_ERROR` で、`message` に理由
`KORDER_SCAN_ROWS_EXCEEDS_MAX_RECORDS(scanRows=..., maxRecords=...)` を含みます。
`maxRecords` を上げるか、`LIMIT` / `OFFSET` を小さくするか、`ORDER BY`(ローカル整列)
へ切り替えてください。
```js
// Cursor を使う(LIMIT 501 > 500)
await runQuery("SELECT $id FROM APP100 KORDER BY $id LIMIT 501", {
client,
maxRecords: 1000, // OFFSET + LIMIT = 501 ≤ maxRecords が必要
});
```
### 使われない
- **通常の `SELECT` / `ORDER BY` / JOIN / 集計 / CTE などのページング**。1万件超でも
Cursor API ではなく `$id` シーク方式(前ページ末尾の `$id` より大きい行を取得)で
進みます。したがって `openCursor()` は呼ばれず、`cursorMaxActive` も影響しません。
- `KORDER BY` でも窓が単発 GET に収まる場合(`LIMIT ≤ 500` かつ `OFFSET ≤ 10,000`
かつ `LIMIT ≤ maxRecords`)。
- `explainQuery()`。plan の生成のみでレコード取得も Cursor 作成も行いません。
### 上限
`cursorMaxActive`(既定 2・1〜5)は **その client instance 内で同時に開ける Cursor の
上限**です。超過は Cursor 作成前に fail-closed になります。kintone 側のホスト単位の
上限は最大 5 で、独立コピー間では協調しません([複数コピーと Cursor 上限](#複数コピーと-cursor-上限))。
### KLIKE との関係
`KLIKE`(kintone ネイティブ検索への押し下げ)を使うクエリは **Cursor を使いません**。
`KORDER BY` が `KLIKE` との併用を受け付けないためです。
```js
// NG: KORDER BY と KLIKE は併用できない
// → EXECUTION_ERROR / message に KORDER_KLIKE_UNSUPPORTED
await runQuery("SELECT $id FROM APP100 WHERE 名前 KLIKE 'ケン' KORDER BY $id LIMIT 501", { client });
// OK: KLIKE は WHERE がネイティブ like へ押し下がる(Cursor は使わない)
await runQuery("SELECT $id FROM APP100 WHERE 名前 KLIKE 'ケン' ORDER BY $id", { client });
```
そのため `KLIKE` で並び順が必要なときは `ORDER BY`(ローカル整列)を使うか、順序を
指定せず既定順のまま受け取ります。
**注意**: `KLIKE` は kintone 側で検索が実行されるため、ヒット件数が多いと
**10 万件の検索打ち切り**に当たりやすくなります。打ち切りが起きた場合は
(他のクエリ形と同様に)常に `SEARCH_ABORTED` の hard error で、部分結果は返りません。
広くヒットする `KLIKE` は他の条件で絞り込んでください。
## error code
`KsqlEngineError.code` は次の固定 union です。
| code | 意味 |
|---|---|
| `PARSE_ERROR` | 字句/構文エラー、空文、複文など入力を単一文として解釈できない |
| `READ_ONLY_VIOLATION` | 公開 read-only allowlist 外、または write boundary への到達 |
| `SEARCH_ABORTED` | kintone の検索が打ち切られた。部分結果なし |
| `FETCH_LIMIT_EXCEEDED` | 許可された取得上限を超えた |
| `CLIENT_ERROR` | transport / kintone API client が返したエラー |
| `EXECUTION_ERROR` | 上記以外の planning / execution エラー |
`message` の完全一致へ依存せず `code` で分岐してください。元 error がある場合は
`cause` に保持されます。
```js
try {
await runQuery(sql, { client });
} catch (error) {
if (error instanceof KsqlEngineError && error.code === "SEARCH_ABORTED") {
// 部分結果は存在しない。再検索条件や取得範囲を見直す。
}
throw error;
}
```
## 複数コピーと Cursor 上限
UMD registry は複数版を共存させますが、独立した engine copy / client instance 間で
Cursor lease を協調しません。
- 同じページで使う独立コピーの `cursorMaxActive` の**合計を5以下**にする。
- 3コピー以上を同時に使う場合は、通常それぞれ `cursorMaxActive: 1` にする。
- 自分が管理しない第三者 plugin も Cursor を使うページでは、全 copy の合算上限を
consumer だけで保証できない。host の Cursor 上限エラーを前提に、同時実行数を減らす。
npm で依存を集約できる plugin は、別々の UMD copy を持ち込まず npm bundle を優先すると
版と Cursor 運用を管理しやすくなります。