# Changelog リリースごとの変更点。**本ファイルは v3.45.0 以降だけを保持する。** それ以前の詳細は [GitHub Releases](https://github.com/rex0220/kintone-sql-tools/releases) の各タグを参照。 ## v3.72.0(2026-08-22) ### 修正(B171 F-1: `ASSERT` / `ASSERT WARN` / `EXIT SUCCESS IF` の大小比較が辞書順だった)**※結果が変わります・dialect 0 含む** **件数ガードが fail-open になり得た正しさの修正です**(最適化ではありません)。ksql-flow からの 報告(実測マトリクス付き)で発覚し、起票前に両方向を実機再現して確認しました。 ```sql ASSERT (SELECT COUNT(*) FROM APP100) <= 9; -- 旧: 12 件でも成立('12' < '9' の辞書順)=大量更新前ガードが素通り -- 新: 12 件で AssertError(10 進の数値比較) ``` - **原因**: 比較の型(数値/文字列)を左オペランドの AST 型だけで決めており、`COUNT(*)` 等の スカラーサブクエリの数値性が捨てられて文字列(コードポイント順)比較になっていました。 等値 `=` だけ正常に見えるのは「桁が揃うと文字列比較でも正答する」ため(B119-B122 と同じ罠)。 ASSERT 導入時からの不具合で、`ASSERT WARN` / `EXIT SUCCESS IF` の条件部・`BETWEEN` も同一でした - **影響していた形(今回から正しくなる)**: 大小比較(`<` `<=` `>` `>=`)・`BETWEEN` で、 両辺が数値(数値リテラル・算術式・数値の `@変数`・`COUNT`/`SUM`/`AVG` 等の数値集計サブクエリ) - **変わらないもの(元から正しい経路)**: `=` / `!=` / `<>`・文字列同士の大小(辞書順= `ASSERT @d >= '2026-01-01'` の ISO 日付比較を含む)・文字列の `@変数`(外部注入含む)・ 算術式の計算自体・空文字等の既存規則。数値性は**値の出どころ**で決まり、実値からの推測はしません - レシピ R1 の `BETWEEN 0 AND 10000` ゲート等、公開例の件数ガードが桁跨ぎでも意図どおり動きます ### 機能追加(B171 F-2: dialect 1 の `INSERT ... VALUES` で as-of 関数)**※純加法** `@NOW()` / `@TODAY()` / `@MONTH_START()` / `@NEXT_MONTH_START()` を `INSERT ... VALUES` の値に 書けます(シード投入・バックフィル用)。実行・`previewStatement`・`EXPLAIN` の 3 面で同値に 展開されます。`UPSERT ... VALUES` / APPLY の値は対象外(拒否時に文型限定のヒントを表示・ `INSERT ... SELECT` / `UPSERT ... SELECT` で注入してください)。dialect 0 は従来どおりです。 ## v3.71.0(2026-08-22) ### 機能追加(B170 E-2: `previewStatement`=dry-run 差分プレビュー)**※`/flow` への純加法のみ・既存 API は不変** ksql-flow との設計往復(提案 R1 → 回答 → **R2 確定**)を経た契約です。ランナーの `--dry-run` 差分プレビュー(Flow 設計書 §10.2)を解禁します。 ```ts import { previewStatement } from "@rex0220/kintone-sql-tools/flow"; const preview = await previewStatement(stmt, ctx, { maxSamples: 5 }); // { kind: "PREVIEW", operation, appId, counts: { insert, update, delete }, // samples: [{ kind, key?, before?, after? }], reads, estimatedWrites } ``` - **DML 専用**(INSERT / INSERT...SELECT / UPDATE / UPDATE...FROM / DELETE / UPSERT / UPSERT...SELECT。MERGE 正規化後含む)。非 DML・`VALIDATE ONLY` / `ON ERROR SKIP` / CHECK / APPLY / IMPORT・サブテーブルは対処付き `ArgumentError`(read-only 文は `executeStatement` へ) - **書込 API 0 回を構造的に保証**(preview 経路に書込呼出なし+呼ばれたら throw する遮断ラッパー) - `counts.update` は「キー一致して UPDATE 対象となる件数」(unchanged 判定は初版対象外)。 `samples` は書込順先頭 N 件(既定 5・上限 50・範囲外は読取前にエラー)。before / after は **代入対象列のみ**・DELETE はキー(`$id`)のみ - `reads`=preview が実消費した読取 API 数(cursor 含む)。`estimatedWrites`=本実行時の 100 件単位の推定書込数。**preview は読取を実消費**し、TOCTOU・kintone 自動設定値・lookup 連動値は保証外(参考値) - context の一時テーブル・変数・as-of / timezone を共有し、preview 後も同じ context で続行可能。 `EXIT SUCCESS IF` 成立後は全ゼロの `PreviewResult`(skipped 相当) - UPSERT の事前読取は preview 時のみ代入対象列を同時取得(**通常実行の取得列・書込順は不変**) ## v3.70.0(2026-08-21) ### 機能追加(B170 ksql-flow ランナーからの依頼 E-1〜E-6 への対応)**※`/flow` への純加法のみ・dialect 0 と `/engine` は不変** ksql-flow ランナー実装(M1〜M7)からの正式依頼への対応です。着手前に依頼書を実測レビューし、 事実前提の誤り 4 件(DML 結果の型・metrics 差分の前提・書込のキー順・UPSERT pre-read の範囲)を 訂正のうえ実装しました。詳細な回答は ksql-flow への申し送り文書を参照。 - **E-6: `explainScript` に `asOf` / `timezone` を追加**(実質バグ修正)。dialect 1 の `@NOW()` 等を 含むスクリプトが EXPLAIN 経路で `ParseError` になっていたのを、planner 冒頭の as-of 一括注入で解消。 **CLI / MCP / プラグインの batch EXPLAIN・dry-run も同時に直っています**(既定値=実行時と同じ規則) - **E-3: `FlowDmlResult` 型と `isDmlResult` 型ガードを公開**。INSERT(`createdIds`/`insertedCount`)・ UPDATE(`updatedCount`)・DELETE(`deletedCount`)・UPSERT(`insertedCount`/`updatedCount`)の discriminated union。`StatementResult.result` の型は `unknown` のまま(利用側の型を壊さない) - **E-5: `StatementResult.metrics` を累積値のスナップショットへ安全化**。これまで共有参照だったため 「過去の結果の metrics が後続実行で進み、差分が常にゼロになる」形でした。文単位の消費は 前回スナップショットとの差分で計算できます(**`/flow` 利用側は要確認**・`/engine` は不変) - **E-1: `createExecutionContext` に `onChunkWritten` コールバックを追加**。書込 API(POST/PUT/DELETE) 1 リクエスト成功直後に await して通知(`statementIndex`/`appId`/`operation`/`records`/`chunkIndex`/ 単一キー UPSERT の `lastKeyValue`)。チェックポイント記録用。**書込のキー昇順整列は行いません** (現状も整列されていません=設計書 5.1-2 の前提はランナー側で再裁定) - E-4(`KsqlFlowError.code` の値域)は文書回答・E-2(dry-run)は設計提案 R1 を送付し往復中 ## v3.69.0(2026-08-21) ### 新機能(B168 Flow dialect 1 完成=Stage 4-6)**※opt-in・既存構文(dialect 0)は完全不変** v3.68.0 で解析基盤を入れた **dialect 1** が完成し、正式提供になりました。 `-- @ksql dialect: 1` を宣言したスクリプトを **CLI・MCP・プラグインで実行できます** (v3.68.0 の「エンジン内部 API のみ」制限を解消)。使い方は [言語リファレンス §27](docs/ksql_language_reference.md) と [レシピ R18](docs/ksql_batch_recipes.md) を参照。 - **as-of 固定の時刻関数**: `@NOW()` / `@TODAY()` / `@MONTH_START()` / `@NEXT_MONTH_START()`。 スクリプト全体で単一の基準時刻から導出され、公式 API の `asOf` / `timezone` 注入で **過去日付のバックフィルを同じスクリプトで再現**できます(省略時は実行開始時刻・ホスト TZ)。 `@` なしの `TODAY()` 等は kintone サーバー評価のままで、dialect 1 の WHERE で使うと警告が出ます - **validate 拡張**(dialect 1 のみ): UPSERT / MERGE キーの検証(重複禁止設定・文字列(1行)/数値型)・ 複合キー禁止・サブテーブル DML 禁止・素の `INSERT` 警告(`strict` でエラー化)。 診断は severity / コード(`KSQL1xxx`)/ 行・列付き。キー検証は**実行時にも書込 API の前に**働きます - **EXPLAIN の推定 API 消費**(dialect 1 バッチ): 読取(500 件/回)・メタデータ・UPSERT 事前 GET・ 書込(100 件/回)を分解表示。件数不明は「不明(上限 N と仮定)」で数字を捏造しません - **公式 API `@rex0220/kintone-sql-tools/flow`**(semver 対象): `parseScript` / `validateScript` / `explainScript` / **文単位実行**(`createExecutionContext` / `executeStatement` / `disposeExecutionContext`)/ `createKintoneClient`(書込可能・fetch ベース)。 既存 `/engine` は read-only のまま 1 バイトも変わりません - **MCP**: `ksql_validate` が dialect 1 スクリプトで `diagnostics` / `scriptMeta` を返します (**dialect 0 の応答はフィールド集合ごと完全不変**・ネットワーク 0 契約維持)。 `strict` 入力・文型カタログの Flow 6 文型・`ksql_docs` の §27 / R18 を追加 - **README**: 公式 API(`/engine`・`/flow`)と**エンジンバージョン × dialect 対応表**を掲載 - **変わらないもの**: 宣言なし(dialect 0)の SQL・応答・EXPLAIN 出力・エラー code はすべて不変 (dialect 0 の EXPLAIN は 1 行も変わりません) ## v3.68.0(2026-08-21) ### 新機能(B168 Flow dialect 1 の解析基盤=Stage 1-3)**※opt-in・既存構文は完全不変・実験的** kSQL Flow(別リポジトリのバッチ実行ランナー)向けの拡張構文 **dialect 1** の解析基盤です。 スクリプトの**先頭に `-- @ksql dialect: 1` を宣言したときだけ**有効になります。 宣言なし(dialect 0=既定)の SQL の挙動は一切変わりません。 > **【2026-08-21 追記・訂正】本版で dialect 1 を実行できるのはエンジン内部 API(`executeBatch`)だけです。** > CLI・MCP・プラグインの各面は実行前の事前解析がヘッダ非対応のため dialect 1 スクリプトを > 受け付けません(リリース後の実機確認で判明)。**各面からの実行対応は次リリースで提供予定**です。 > 本版の dialect 1 はエンジン実装+テストの先行公開とお考えください。 ```sql -- @ksql name: monthly_sales_sync -- @ksql dialect: 1 ASSERT (SELECT COUNT(*) FROM APP100 WHERE 金額 < 0) = 0, '異常データがあるため中断'; CREATE TEMP TABLE summary AS SELECT 顧客コード, SUM(金額) AS 合計 FROM APP100 GROUP BY 顧客コード; EXIT SUCCESS IF (SELECT COUNT(*) FROM summary) = 0, '対象 0 件のためスキップ'; UPSERT INTO APP200 (顧客コード, 実績) SELECT 顧客コード, 合計 FROM summary KEY (顧客コード); ``` - **`-- @ksql` ヘッダ**: `name` / `depends_on` / `timeout` / `dialect` を解析(値の制約検証つき・未知キーは警告扱い) - **`ASSERT <条件>, 'メッセージ'` / `ASSERT WARN`**: メッセージ付きアサートと「警告して続行」(既存のメッセージ無し ASSERT は dialect 0/1 双方で従来どおり) - **`EXIT SUCCESS IF <条件>, 'メッセージ'`**: 正常な早期終了(成立で後続文を skip・バッチは成功扱い=異常中断の ASSERT と区別) - **構文エイリアス**(パース時に既存構文へ正規化・実行系は同一): `CREATE TEMP TABLE 裸名 AS` ≡ `#裸名`/`UPSERT ... KEY (k)` ≡ `ON DUPLICATE (k)`/`MERGE INTO ... USING ... WHEN MATCHED/NOT MATCHED` → UPSERT(単一キー等値 ON・両句の式一致が条件・片側省略は代替構文を案内するエラー) - dialect 0 で dialect 1 構文を使うと「`-- @ksql dialect: 1` の宣言が必要です」エラー - **変わらないもの**: 宣言なしの既存 SQL すべて・既存 ASSERT・エラー code・MCP/エンジンライブラリの応答形状(dialect 1 の公式 API・MCP 対応・言語リファレンス掲載・as-of 注入・validate 拡張は後続リリース。**本版の dialect 1 は実験的扱い**で、細部は後続リリースで変わる可能性があります) - 内部: 構造化診断(行・列付き)の基盤・文単位実行文脈の整理(B169 のクロックを統合)を同梱(挙動不変) ## v3.67.0(2026-08-21) ### 修正(B169 `CURRENT_DATE()` / `CURRENT_TIMESTAMP()` を文単位の固定時刻評価へ)**※結果が変わる形があります** **両関数はこれまで式評価のたびに現在時刻を読み直していました**(最適化ではなく評価タイミングの修正です)。 文の実行開始時に 1 回だけ取得した時刻へ固定します。B168(Flow dialect 1)の調査で発見し、 as-of 固定評価の土台として先行して修正しました。 ```sql SELECT 顧客名, CURRENT_TIMESTAMP() AS 取得日時 FROM APP100 -- 旧: 行ごとに異なるミリ秒が入り得る。新: 全行が文の開始時刻で同値 ``` - **影響する形(結果が変わり得る)**: 同一文内で両関数を複数回評価する形すべて= 複数行 SELECT の射影・`WHERE` / `GROUP BY` / `HAVING` / `ORDER BY` での使用・ `UPDATE ... SET 列 = CURRENT_TIMESTAMP()` 等 DML の複数レコード値・APPLY 内の評価。 とくに**深夜 0 時を跨ぐ実行で「今日」が途中で変わる非決定**が解消します - **変わらないもの(元から正しい経路)**: - 値の形式(`CURRENT_DATE()` = `YYYY-MM-DD`・`CURRENT_TIMESTAMP()` = ISO 8601 ミリ秒付き)と タイムゾーンの扱い(前者はホストローカル・後者は UTC) - `SET @x = NOW()` / `DECLARE @x = TODAY()`(元から文単位 1 回評価) - WHERE 素通しの kintone サーバー評価関数(`TODAY()` / `NOW()` / `THIS_MONTH()` 等 16 種)= 従来どおり kintone がクエリ実行時に評価します - 単一行・単一評価の文は、実質同じ値を返します(開始時刻と評価時刻の差はこれまでも不定でした) - **バッチは文ごとに時刻が確定**します(文 1 と文 2 で同値にはしません。 スクリプト全体を 1 つの基準時刻へ固定する as-of 注入は B168 dialect 1 で提供予定) - 検証: 新規テスト 5 件(文の途中で時計を進めても開始時刻のままであることを 射影・WHERE・GROUP BY・ORDER BY・DML 値・バッチで固定)。既存テストは全件無変更で通過 ## v3.66.1(2026-08-10) ### 修正(B167 バッチの `EXPLAIN` / dry-run が「物理アプリ+一時テーブル JOIN」で失敗していた)**※EXPLAIN 面のみ・実行は元から正常** **バッチ内で物理アプリを `FROM` にして一時テーブルを JOIN の相手に置くと、 実行は正常なのに `EXPLAIN` / `--dry-run` / プラグインの EXPLAIN だけが `kintone API error 400 CB_VA01(app: 最小でも1以上です。)` で失敗していました** (v3.61.0 からの既存問題・v3.66.0 の回帰ではありません)。 ```sql CREATE TEMP TABLE #z AS SELECT 製品名, SUM(個数) AS 在庫数 FROM APP100 GROUP BY 製品名; SELECT SUM(z.在庫数 * m.仕入価格) AS 在庫金額 FROM APP200 m INNER JOIN #z z ON m.製品名 = z.製品名 -- 旧: 実行は正常・EXPLAIN だけ CB_VA01。新: EXPLAIN も一時テーブルの schema を静的解決して成功 ``` - **原因**: `EXPLAIN` の結合キー事前絞り込み表示(v3.61.0)が JOIN の相手側を 物理アプリと決め打ちし、一時テーブルの内部プレースホルダ(app 0)を 実 API へ送っていました。逆の配置(`FROM #temp JOIN 物理`)は元から正常です - **修正**: 一時テーブル・CTE 側は静的 schema から解決し、実 API を呼びません。 型メタが静的に確定しない場合の表示は従来の FALLBACK と同じです - **影響していた形**: バッチの `EXPLAIN` / dry-run で「物理 `FROM` + `#temp`(または CTE)を JOIN の相手」に置いた場合のみ。**実行・結果・records API 0 回の契約はすべて不変**です - 単文の `EXPLAIN WITH ...`・`FROM #temp JOIN 物理`・CTE→APP の既存表示(型別選択)は 元から正しく、変わりません ## v3.66.0(2026-08-09) ### 新機能(B53 `WITH RECURSIVE` / `CYCLE` — 再帰 CTE) **深さがデータ次第で変わる階層(BOM 部品表・組織図・分類ツリー)を read-only の `WITH RECURSIVE` で展開できます。** これまで「再帰 CTE は非対応」でした。 ```sql WITH RECURSIVE 展開 AS ( SELECT child_code AS item_code, qty AS acc_qty, 1 AS lvl FROM APP4238 WHERE parent_code = 'SET-001' AND is_active = '有効' UNION ALL SELECT b.child_code, e.acc_qty * b.qty, e.lvl + 1 FROM 展開 AS e INNER JOIN APP4238 AS b ON b.parent_code = e.item_code WHERE e.lvl < 10 AND b.is_active = '有効' ) SELECT item_code, SUM(acc_qty) AS 所要量 FROM 展開 GROUP BY item_code ``` - **今回の対応範囲**: 単一再帰 CTE・`seed UNION ALL 再帰項` の 2 枝・自己参照 1 回・ INNER 等値 JOIN 1 本・CTE 列名リスト(再帰 CTE のみ)・非再帰 sibling との共存・ 外側の JOIN/集計/ORDER BY。対象外(相互再帰・OUTER JOIN・再帰項の集計等)は すべて実行前に静的拒否します - **任意の `CYCLE` 句**(`CYCLE 列 SET mark TO 'Y' DEFAULT 'N'`)は**経路(path)単位**で 循環を検出して打ち切ります。共通部品を複数の親から使う「多重使用」は循環ではないため 打ち切りません(グローバル訪問済み判定はしません) - **安全境界(常時 fail-closed)**: 深さ 100・累積行 10,000・中間展開 100,000 を `CYCLE` の有無に関係なく強制。超過は部分結果を返さず専用エラーで停止します。 env / profile / CLI(`--recursive-cte-max-*`)/ MCP / プラグイン UI で変更可 - **API 消費は深さに依存しません**: 参照アプリを実行前に 1 回だけ完全実体化し (`onLimit=truncate` は無効・取得列は必要フィールドの和集合に最小化)、反復はメモリ内。 `EXPLAIN` は戦略・境界・source ごとの取得見積りを表示し、records API を呼びません - **型の安全**: seed と再帰項の列型・JOIN キー型は実行前に静的に証明し、 証明できない形は planning error(黙って文字列比較にフォールバックしません) - **空キーの実行時警告**: 再帰 JOIN の両側に空キーが実際に現れた最初の反復で 警告を 1 件返します(空=空一致の意味論は JOIN 全体で不変・結果は変わりません。 「親コード空=ルート」の階層で意図しない再展開に気づけます) - **実データ検証**: BOM 部品表(品目 110・エッジ 276・10 セット・多経路合流/レベル差/ 小数員数の積を含む)の展開 394 行が独立算出の期待値と全一致 ### 修正(B166 JOIN の `ON` をテーブルと逆順に書くと落ちていた)**※書ける形が増えます** **`FROM p JOIN c ON c.x = p.y`(JOIN 側のキーを左に書く形)が `ArgumentError: JOIN key ... is not available in the materialized table.` で 一律に失敗していました**(v3.65.0 以前から。誤結果ではなくエラー停止)。 `ON` の両辺を記述順ではなく**実際のテーブル帰属**で解決するよう修正しました。 - **影響していた形**: メモリ結合の全経路(物理×物理・CTE×CTE・CTE×物理・一時テーブル)で `ON` の左辺に JOIN 側のキーを書いた場合のみ - `FROM` 側を左に書く従来の形・結合キーの押し下げ(型別選択・targeted `IN`)・ LEFT/RIGHT JOIN の保存側の意味は元から正しく、変わりません ### 改善(B160 全順序警告の「無視してよい条件」を一般化) CTE・一時テーブル読みのウィンドウ警告の助言を、機構別の特例ではなく **「各パーティション内で `ORDER BY` の値の組が入力行を一意に識別できるか」**の 単一条件へ一般化しました。集約キー全含み(従来どおり無視可)・JOIN 後の系列値・ 再帰 CTE の出力のいずれも同じ文で判定できます。生成列・深さ列・`$id` 由来という 理由**だけ**では無視できないことも明記しました。 ### 改善(B165 再帰 CTE の診断とレシピ) - CTE 本体から自分自身を参照した(`RECURSIVE` なし)場合の診断を 「自己参照には `WITH RECURSIVE` が必要です」の専用文言にしました (従来は「テーブル名は APP + 数字…」で原因に辿り着けませんでした) - レシピ集に「固定深さの階層は自己 JOIN で書く」と再帰 CTE の基本形を追加しました (掲載 SQL は機械検証済み) ## v3.65.0(2026-08-08) ### 修正(B164 `@変数` を含む集計が比較位置で誤った値になっていた)**※結果が変わります(正しさの修正)** **`@変数` を引数に含む集計を `CASE WHEN` / `IF` の条件や `HAVING` の比較に書くと、 集計値ではなく「空(すべての値より小さい)」として比較されていました。** `SELECT` のリストでは正しい値が出るため、気づきにくい形です。 ```sql DECLARE @a = '2026-02'; SELECT 製品名, SUM(CASE WHEN DATE_FORMAT(日付,'%Y-%m') >= @a THEN 個数 ELSE 0 END) AS 合計, -- 0 ← 正しい CASE WHEN SUM(CASE WHEN DATE_FORMAT(日付,'%Y-%m') >= @a THEN 個数 ELSE 0 END) = 0 THEN 'ZERO' ELSE 'NONZERO' END AS 判定 FROM APP100 GROUP BY 製品名 -- 旧: 判定 = NONZERO(誤り・比較位置だけ空扱い)。HAVING では行が静かに消える -- 新: 判定 = ZERO(SELECT リストと同じ値で比較) ``` 原因は、比較位置の集計参照が**変数解決前に固定された参照名**で計算結果を探していたこと (計算側は解決後の名前で保存するため、一致せず空になっていました)。 参照名を**解決後の構造から再生成**するように修正しました。 - **影響していた形**: `CASE`/`IF` 条件・`HAVING` の直接集計比較で、集計の引数に `@変数` を 含む場合のみ。除数ガードの不発火(`NaN` が出る)・`HAVING` の行消失が実害でした - 集計算術式(`SUM(...) + 0`)・`THEN`/`ELSE` の集計・`ROUND(SUM(...))` 等・`ORDER BY`・ ウィンドウ関数は別経路で、元から正しく、変わりません - **あわせて診断を追加**: 計算されていない集計を比較位置で参照した場合 (`HAVING` にだけ書いて `SELECT` に出していない等・従来から黙って空でした)、 黙らせず警告を出します(値の挙動は互換のまま)。サブクエリ内の警告も外側へ届きます ## v3.64.0(2026-08-08) ### 改善(B162 `DECLARE` 変数の系列も `EXPLAIN` が通るように) **`DECLARE` 変数を `GENERATE_SERIES` の引数に使うと、実行は正常なのに `EXPLAIN` だけが 「実在する YYYY-MM-DD 形式の DATE を指定してください」と誤解を招くエラーになっていました。** 「保存クエリは `DECLARE` で書く」と「本番クエリは `EXPLAIN` まで通す」が両立しませんでした。 ```sql DECLARE @m_start = '2025-08-01'; DECLARE @m_stop = '2026-08-01'; WITH 月系列 AS (GENERATE_SERIES(@m_start, @m_stop, '1 month') AS 月) SELECT 月 FROM 月系列 -- 新: EXPLAIN が成功し、既定値に基づく条件付き計画を表示 -- series type: DATE (DECLARE default) / rows: 13 (DECLARE default estimate) -- binding: DECLARE defaults; runtime injection may change this plan ``` - **リテラル既定値の場合だけ**系列引数に束縛します(外部注入で変わり得る旨を表示・ 注入値そのものは使わず表示もしません) - `SET @x = TODAY()` など静的に確定しない場合は `series type: deferred (variable)` で **エラーにせず**計画を返します(実行時の検証は従来どおり) - `WHERE` 等の変数の扱い(placeholder / `pushdown candidate` 表示)は変わりません ### 改善(B163 一時テーブルの `GROUP BY` を含むバッチ `EXPLAIN` が通るように) **`CREATE TEMP TABLE ... AS SELECT` の後段で一時テーブルを `GROUP BY` する文を含むバッチの `EXPLAIN` が `InternalError` になっていました**(実行は正常・エンジンのバグに読める文言)。 `EXPLAIN` は一時テーブルを実体化しませんが、**出力列は文 1 の SELECT 句から静的に導出できる**ため、 schema を後続文へ伝播して通常どおり計画を表示するようにしました。 ```text CREATE TEMP TABLE #t schema: 年月, 製品名 schema source: SELECT output of statement 1 (後段) source: temp table #t (schema from statement 1) plan status: static schema / runtime rows ``` - 静的に導出できない形(wildcard 等)は `deferred` として**エラーにせず**通します - `InternalError` は利用者向け診断として出しません - `DROP TEMP TABLE` 後の参照・一時テーブルから一時テーブルの連鎖にも対応 ### 補足 - どちらも **`EXPLAIN`/dry-run 面のみの改善**で、実行・`ksql_validate` の結果と records API 0 回・一時テーブル非実体化の契約は変わりません - CLI `--dry-run` の静的経路(API 0 回)と metadata 解決の使い分けを、 混在バッチでも文の構成から正しく選ぶようにしました ## v3.63.0(2026-08-08) ### 新機能(B158 `CROSS JOIN` — 直積・2 軸の格子生成) **明示 `CROSS JOIN` で 2 つの入力の直積(左 N 行 × 右 M 行 = N×M 行)を生成できるようになりました。** 主用途は `GENERATE_SERIES` の日付系列と小さいマスタを掛け合わせた「日付 × 製品」の 歯抜けない格子です。これまで直積の標準 3 形(`CROSS JOIN`・`ON 1=1`・カンマ結合)は すべて構文エラーで、製品別の 0 埋めが言語内で完結しませんでした。 ```sql WITH d AS (GENERATE_SERIES('2026-01-01', '2026-12-31') AS 日付), m AS (SELECT 製品名 FROM APP200) SELECT d.日付, m.製品名 FROM d CROSS JOIN m -- 365 日 × 8 製品 = 2,920 行の格子。LEFT JOIN で実績を当てれば -- 「取引の無い日×製品」も 0 で並ぶ(レシピ R17 の製品別・暦日化) ``` - **生成上限 10,000 行**(`GENERATE_SERIES` と同じ公開上限・段ごとに行生成前判定・ `WHERE`/`LIMIT` を付けても免除しません)。`EXPLAIN` に左右行数と `row guard:` を表示します - 物理アプリを含む場合、`EXPLAIN` は取得件数を推測せず **runtime 算出式**を表示します - `CROSS JOIN` を含む SELECT は完全入力必須(`complete input reason: CROSS_JOIN`) - 安全な WHERE 条件は従来どおり各アプリへ押し下げます(結合キー prefilter は直積には非適用) - CLI `--dry-run` は API 0 回で計画を表示します - **注意(予約語の追加)**: `CROSS` が予約語になりました。`CROSS` という名前のフィールド・ 別名を未引用で使っていた場合は `` `CROSS` `` とバッククォートで囲ってください - `ON 1=1`・カンマ結合・`LEFT/RIGHT CROSS JOIN` は引き続き構文エラーです(開放していません) ### 新機能(B159 `GENERATE_SERIES` の month / year step) **日付系列の step に `'1 month'` / `'1 year'`(係数・負値可)を指定できるようになりました。** 月次の 0 埋めが可能になり、**空月があると `LAG` が静かに 2 か月前と比べる**問題を防げます。 ```sql WITH m AS (GENERATE_SERIES('2025-08-01', '2026-08-01', '1 month') AS 月) SELECT 月 FROM m -- 2025-08-01, 2025-09-01, ..., 2026-08-01(13 行・DATE 型) ``` - **start は月初(year は 1 月 1 日)限定**です。月末丸めの規則を系列に持ち込まないためで、 月末が必要な場合は `LAST_DAY(月)`、`'YYYY-MM'` 文字列が必要な場合は `DATE_FORMAT(月, '%Y-%m')` で 変換してください(うるう年も正しく `2024-02-29` になります・実測) - 各値は start と行番号から直接算出します(累積加算しないため、値がずれていきません) - stop は月初・年初でなくてもよく、超えない範囲だけを返します - 10,000 行ガード・EXPLAIN 表示・生成列の警告抑止・dry-run API 0 回は day step と同じです ### 修正(B157 複文バッチの `--dry-run` 表示が v3.62.0 で悲観側に落ちていた) 複文バッチの `--dry-run` でフォーム定義の解決が行われなくなり、同じ文の中で 診断ブロック(楽観)と計画本体(悲観・`(全件取得)`)の `kintone query:` が食い違う回帰を 修正しました(表示のみ・実行結果と API 契約は不変・単文は影響なし)。 ### 修正(B161 `WITH`+物理アプリの最小形が `--dry-run` で落ちていた・既存穴) `WHERE` 等の無い `WITH c AS (SELECT $id FROM APP100) SELECT $id FROM c` の形が `--dry-run` で `DryRunError` になっていました(**v3.61.0 以前から**の穴・B157 の 最終チェックが検出)。metadata 要否判定に CTE の物理ソースを追加して修正しました。 ## v3.62.0(2026-08-08) ### 改善(B155 WHERE の絞り込みが CTE・一時テーブルの JOIN と単一表の一部に届いていなかった)**※結果は変わりません** **v3.60.0(B151/B152)で開放した型×演算子の WHERE 条件が、CTE・一時テーブルを物理アプリへ JOIN する形では結合キーの絞り込みと合流せず、JS 判定に落ちていました。** ```sql WITH s AS (GENERATE_SERIES('2026-07-29','2026-08-04') AS 日付) SELECT s.日付, t.製品名, t.個数 FROM s INNER JOIN APP100 AS t ON s.日付 = t.日付 WHERE t.製品名 = '牛乳' AND t.個数 <= 100 AND t.入出庫区分 = '出庫' -- 旧: kintone query は 日付 >= "..." and 日付 <= "..." のみ(WHERE は全部 JS 判定) -- 新: (日付 >= "..." and 日付 <= "...") and ((製品名 = "牛乳" and 個数 <= 100) -- and 入出庫区分 in ("出庫")) — WHERE 葉が合流(実測・実機) ``` 原因は**型×演算子の安全判定が 2 実装に複製され、片方に B76 世代(v3.25.0 相当)の古い規則が 凍結残存していたこと**です。同じ CTE JOIN で `< 101` は合流するのに `<= 100` は落ちる、という 非対称が決定的な証拠でした。判定を共有 leaf policy(1 実装)へ統一しました。 - **単一表の全件取得(LIKE 併用など exact 直列化が崩れた形)でも同じ絞り込みが効きます** (`製品名 = '牛乳' AND 仕入先 LIKE '%乳業%'` → `製品名 = "牛乳"` で絞ってから LIKE を JS 判定) - **結果は変わりません**(絞り込みは superset・元の WHERE を取得後に再評価) - 実行と `EXPLAIN` は同一の計画オブジェクトを共有します(表示と実挙動の乖離を構造で防止) - CLI `--dry-run` は CTE→APP JOIN+WHERE 候補の形を **API 0 回**で `pushdown candidate:` として表示 ### 改善(B154 `join pushdown plan: not applied` の誤読対策) `not applied` は「絞れていない」ではなく別機構(field-vs-literal JOIN pushdown)の不適用です。 結合キー・WHERE の絞り込みは各ソース行に別途出るため、 `not applied (join key/WHERE prefilters are reported per source below)` と但し書きを追加しました。 取得の実態は従来どおり各ソースの `fetch:` 行が正です。 ### 修正(B155 の最終チェックで検出した dry-run 回帰・出荷前に修正) CLI `--dry-run` で B155 形と相対日付関数(`TODAY()` 等)が同一文・同一バッチに同居すると、 API を呼ばない静的経路が選ばれたまま相対日付の解決がフォーム定義取得に到達し `DryRunError` で落ちる回帰(v3.61.0 では通っていた形)。静的経路の採用条件に 「バッチ全体が相対日付の解決を必要としない」を追加して修正(判定は実行側と同じ collector を共有)。 ## v3.61.0(2026-08-07) ### 修正(B150 日付キーの JOIN が kintone の生エラーになっていた) **CTE・一時テーブルを日付キーで物理アプリへ直接 JOIN すると、`GAIA_IQ03`(日付フィールドに `in` は使用できません)の生エラーで落ちていました。** ```sql WITH s AS (GENERATE_SERIES('2025-08-04', '2025-08-06') AS 日付) SELECT s.日付, SUM(t.個数) FROM s INNER JOIN APP100 AS t ON s.日付 = t.日付 GROUP BY s.日付 -- 旧: kintone API error 400 GAIA_IQ03 -- 新: 日付 >= "2025-08-04" and 日付 <= "2025-08-06" の範囲で絞り込んで成功 ``` 結合キーの押し下げが、キーの**フィールド型を見ずに常に `in (...)` を生成していた**のが原因です。 型が受ける演算子(`NATIVE_OPERATORS`)で方式を選ぶようにしました。 | キーの型 | 方式 | relation | |---|---|---| | `in` を受ける型(数値・テキスト・選択系など) | 従来どおり `in` リスト | exact | | `in` は不可・範囲は可(日付・時刻・日時・作成/更新日時) | **キー実値の min/max による範囲**(広めに取得して JOIN で突合) | superset | | どちらも不可 | 押し下げずに全件取得(**エラーを出さない**) | — | - **結果は変わりません**(JOIN の突合が最終判定)。**エンジンが型の受けない演算子を選ぶ経路を全廃**しました - 空値・非 canonical 値・意味型不足の場合は安全にフォールバックします(reason code を `EXPLAIN` に表示) ### 修正(B153 空キーの JOIN 一致が静かに欠落していた)**※結果が変わります** **結合キーが空の行同士の JOIN 一致が、押し下げの `in` リスト生成時に静かに欠落していました。** kSQL の JOIN は空=空を一致とみなしますが、押し下げは空キーを捨てていました (前後空白付きキーも trim により欠落)。**空キーを `in ("")` としてリストへ含め、trim を廃止**して 突合と押し下げの意味論を統一しました。kintone は TEXT・数値・選択系で `in ("")` を受理します(実測)。 受理が未確認の型(レコード番号・ユーザー系)は空キー混在時に全件取得へフォールバックします。 **影響**=空キー・空白付きキーで JOIN していた場合、**欠落していた行が現れます**(正しさの修正)。 通常の非空キーの JOIN は変わりません。 ### 修正(B150 の実装中に EXPLAIN の回帰を 2 件検出・出荷前に修正) - CLI `--dry-run` が CTE→APP JOIN でフォーム定義取得に到達して失敗する回帰(**B123 と同じ 「metadata 要否判定の穴」の変種**=型依存 JOIN を見ていなかった)→ 判定に追加し、 CLI e2e(dry-run 3 形・API 0 回)で固定 - `EXPLAIN` が結合キーの実値取得まで実行してしまう回帰 → 実値は取得せず `join key prefilter: runtime candidate` を表示する形へ(EXPLAIN の API 0 回契約を維持) ## v3.60.0(2026-08-07) ### 改善(B151・B152 JOIN の押し下げを kintone 演算子表へ全面整合)**※結果は変わりません** **JOIN を含むクエリで、kintone のクエリ構文が受け付ける型×演算子は、単一表と同様に kintone 側で絞り込むようになりました。** 従来は JOIN 内の多くの条件が全件取得後の JS 判定に 落ちていました(`fetch: ALL`)。**結果は変わらず、取得量と `maxRecords` の当たり方だけが変わります** (押し下げ後も元の `WHERE` を JOIN 後に再評価します)。 ``` 修正前 JOIN 内の t.個数 <= 100 → JOIN 側 fetch: ALL(全件取得して JS 判定) 修正後 JOIN 内の t.個数 <= 100 → kintone query: 個数 <= 100・fetch: EXACT ``` 開放した組(`EXPLAIN` の `relation:` 表示): | 型 | 演算子 | relation | |---|---|---| | NUMBER | `=` `!=` `<` `>` `<=` `>=` `IN` `NOT IN`(数値リテラル・合計 30 桁/小数 10 桁以下) | **exact** | | DATE / TIME / DATETIME / 作成・更新日時 | `=` `!=` `<` `>` `<=` `>=`(canonical 形式リテラル) | **exact** | | 文字列(1行)/ リンク | `=` `!=` `IN` `NOT IN`(非空リテラル・逐語一致) | **exact** | | 作成者 / 更新者 / ユーザー / 組織 / グループ選択 / 作業者 | `IN` `NOT IN`(`code` の逐語一致) | **exact** | | 計算 / レコード番号 | 全 8 演算子 | **superset**(取得後に再評価) | - **`<` / `>` の安全整数リテラル制限を解除**し、`=` を superset から exact へ昇格しました。 従来の「`>= 5000000` を `> 4999999` へ書き換える」案内は不要になり、削除しました - **根拠=ローカルの型付き比較(v3.0.0〜)は kintone と同じ意味論**であることを実測で確認 (数値は 10 進厳密・空セルは最小値扱い・テキストは大小文字/全半角/Unicode 正規化なしの逐語)。 旧 B76 の「IEEE-754 境界のため inclusive 不可」は当時から実装と不整合だった疑いが強く、 歴史注記を付けて失効させました - **値が不正な指定は kintone のエラーがそのまま返ります**(単一表と同一挙動)= 存在しないユーザー code(`GAIA_IL26`)・プロセス管理無効アプリの作業者(`GAIA_ST02`)など。 従来の JOIN は静かに 0 行でしたが、単一表との一貫性を優先しました - 計算フィールドは表示書式(時間 `49:30` 等)で値の形が変わるため、レコード番号は アプリコード形式のため、**superset**(広めに取得して再評価)として開放しています - 対象外のまま=カテゴリー・複数行/リッチ/添付の比較演算子(kintone が受けない)・ `LIKE`(v2.0.0 の意図的な JS 意味論統一。kintone ネイティブ検索は従来どおり `KLIKE`) **検証**=境界すれすれ(binary64 で同値に丸まる 10 進ペア)・空セル両方向・エスケープ文字・ Unicode 正規化・`numberPrecision` 5 設定(専用検証アプリ APP4236 を新設・最大 30 桁/小数 10 桁)を 実機で 3 経路(押し下げ / FULL_SCAN / 単一表)照合。受入テスト群は押し下げ有無で 公開結果が一致することを固定しています。 **仕様・実装は codex、レビュー・実測・リリースは Claude。オーナーの連続した問い (「`<=` が全件取得になる」→「そのまま送ればよいのでは」→「単一表と同じでは」→ 「全型を見直せ」→「kintone の演算子表どおりに」)が、古い安全基準の前提を実測で覆し、 方針を「kintone 演算子表への全面整合」まで押し切った。** ## v3.59.0(2026-08-07) ### 新機能(B149 `GENERATE_SERIES` — 整数・日付系列の生成) **`WITH` の CTE 本体に `GENERATE_SERIES(start, stop [, step])` を書けるようにしました。** 入力レコード無しで整数または日付(`DATE`)の連続系列を生成します。 **価値の本体は、系列を左辺にした `LEFT JOIN` で「取引の無い日を 0 として並べる」こと**です (B134。`GROUP BY` はデータのある日しか行を作らないため、これまで 0 埋め日次系列は書けませんでした)。 ```sql WITH 日付系列 AS ( GENERATE_SERIES('2026-08-01', '2026-08-04', '1 day') AS 日付 ), 日別 AS (SELECT 日付, SUM(金額) AS 合計 FROM APP100 GROUP BY 日付) SELECT s.日付, CASE WHEN d.合計 = '' THEN 0 ELSE d.合計 END AS 合計 FROM 日付系列 AS s LEFT JOIN 日別 AS d ON s.日付 = d.日付 ORDER BY s.日付 -- 取引の無い 08-02 と 08-04 が 0 で並ぶ ``` - **境界は PostgreSQL 準拠**=生成値は `stop` を超えない(ちょうどは含む)・向きが逆なら エラーにならず 0 行・`step` 0 はエラー・負 step 可・省略時は `1` / `'1 day'` - **日付 step は `'1 day'` / `'-14 days'` 形式(`day` / `days` のみ)。** 月・年単位・小数・`DATETIME` / `TIME`・`FROM GENERATE_SERIES(...)` 直置きは Phase 1 対象外で、**それぞれ専用の診断**を出します(静かに間違いません) - **既定列名は `generate_series`、`AS` で改名できます。** 生成列は数値/日付の 型メタを持ち、JOIN・一時テーブル・`LAG` / `LEAD` の次段でも比較・ソート規則を維持します - **生成上限 10,000 行**(同一 `WITH` 文内の合計。`LIMIT` では回避できず、 件数は生成前に算出するため巨大配列を作りません) - **生成列を直接読むウィンドウでは「全順序でない」警告を出しません** (生成列は構築により厳密単調=一意が証明できるため。`LAG` の前日比が警告なしで書けます)。 **JOIN・`UNION`・一時テーブル経由など一意性が壊れ得る形では従来どおり警告が出ます** - `DECLARE @today = TODAY();` で解決した具体日付を終端に使えます。 **read-only 保存クエリでも利用できます** - **レコード API を一切呼びません**(`EXPLAIN` 含む。mock で API 0 回をテストに固定) - `ksql_validate` はリテラルで確定する違反(型不一致・step 0・上限超過など)を **取得前に拒否**し、変数に依存する判定は実行時(変数解決後)に行います **仕様・実装は codex、レビュー・実測・リリースは Claude**(2026-08-07 の分担)。 codex の実装後セルフチェックが出荷前に 2 件(数値リテラルの丸め後判定・`TIME` の誤診断)を 捕捉し、修正済みです(回帰テストは修正前 fail を確認してから追加)。 ## v3.58.0(2026-08-07) ### 挙動が変わります(B147 集計・ウィンドウの別名が入力フィールドを上書きしていた) **`SELECT` の出力別名が、同じ `SELECT` 句の他の式が参照する入力フィールド名と衝突すると、 その式が入力ではなく出力値を読んでいました。** ```sql SELECT 個数 * 2 AS 倍, SUM(個数) AS 個数 FROM APP100 GROUP BY 製品名, 個数 旧 倍 = 2584 (2 × 合計。エラーも警告も無し) 新 倍 = 1292 (2 × 入力の個数) ``` **症状は式の種類で変わっていました。** | 式 | 旧 | 新 | |---|---|---| | `DATE_FORMAT(日付,…) AS 年月, SUM(個数) AS 日付` | `年月` が**空** | `2025-08` | | `個数 * 2 AS 倍, SUM(個数) AS 個数` | **2584** | **1292** | | `製品名 \|\| '-x' AS 名札, SUM(個数) AS 製品名` | **`23429-x`** | **`食パン-x`** | | `CASE WHEN 個数 > 100 … AS 区分, SUM(個数) AS 個数` | **`大`** | **`小`** | | `個数 * 2 AS 倍, ROW_NUMBER() OVER (…) AS 個数` | **2, 4, 6** | **1292, 1412, 796** | **`CASE` は分類が反転していました**(「小」を「大」と言う)。 **ウィンドウ関数の別名でも起き、集計も `GROUP BY` も無い素の `SELECT` で踏みます。** **グループが 1 件だと合計とキーが一致するため、小さなデータでは正しく見えていました。** ### 変わらないもの ```sql SELECT 商品, SUM(数量) AS 数量 FROM APP100 GROUP BY 商品 -- 自然で無害。従来どおり SELECT 製品名, SUM(個数) AS 個数 FROM APP100 GROUP BY 製品名 HAVING 個数 >= 20000 ORDER BY 個数 DESC -- 別名参照は従来どおり ``` **`HAVING` とトップレベルの通常 `ORDER BY` は別の句なので、`SELECT` の別名を参照できます。** **CTE・一時テーブル・サブクエリを実体化した後は、その出力列が下流の通常の入力列になります。** **新しいエラー・警告・reason code は追加していません。** 本件は**成功していた SQL の値の修正**です。 ### 改善(B140 無視してよい条件を書く) CTE の上でウィンドウ関数を使うと出る「全順序でない」警告に、1 文足しました。 ``` …すでに一意な場合もこの警告が出ます。 元の集約のキーをすべて ORDER BY に含めているなら、この警告は無視して構いません。 ``` **「すべて」が要ります**——`GROUP BY a, b` に対し `ORDER BY a` だけでは同順が生じます。 **依頼元から「困っていたのは警告が出ることではなく、消し方が分からないこと」と回答があり、 消せないと分かれば運用に載る**ため、条件まで書く形にしました。 ## v3.57.0(2026-08-07) ### 挙動が変わります(B148 集計されていない列はエラーになります) **集計を含むクエリで、集計もグループ化もされていない列を書くとエラーになります。** ```sql SELECT 製品名, 個数, SUM(個数) AS 合計 FROM APP100 GROUP BY 製品名 旧 個数 は「そのグループの先頭レコードの値」(220 件のうち 1 件)。エラーも警告も無し 新 エラー(どう直すかを案内します) ``` **`GROUP BY` が無くても同じです**(入力全体が 1 グループになるため)。 ```sql SELECT 製品名, 個数, SUM(個数) AS 合計 FROM APP100 旧 1000 件が 1 行に畳まれ、製品名 と 個数 は先頭レコードの値。見た目は明細行と区別が付かない 新 エラー ``` **`HAVING` / `ORDER BY` / `SELECT *` も同じ規則です。** **移行** | したいこと | 書き方 | |---|---| | グループごとに 1 つに決まる値を出す | **`MIN(<列>)`** / `MAX(<列>)` | | その列でも行を分ける | **`GROUP BY` に足す** | **`ANY_VALUE()` はありません。** `MIN()` / `MAX()` が同じ役割を果たします。 **通る形は変わりません。** ```sql SELECT DATE_FORMAT(日付,'%Y-%m') AS 年月, SUM(個数) FROM APP100 GROUP BY 年月 SELECT 個数 + 1 AS 加算, SUM(個数) FROM APP100 GROUP BY 個数 SELECT YEAR(日付) + 1 AS 翌年, SUM(個数) FROM APP100 GROUP BY YEAR(日付) SELECT 製品名, 個数 FROM APP100 -- 集計もグループ化も無い素の SELECT SELECT SUM(個数) OVER () AS 総計 FROM APP100 -- ウィンドウだけ ``` **エラーはレコードを取得する前**に出ます。`ksql_explain` でも同じ診断になります。 ### 同じ規則は `ROLLUP` では以前から効いていました **これは「新しい規則を入れた」のではなく「片方の経路にしか効いていなかった」ものです。** `ROLLUP` / `CUBE` / `GROUPING SETS` では以前からエラーになっており、 **ordinary `GROUP BY` と `GROUP BY` 無し集計だけが素通り**していました。 **v3.56.1 の B145 と正確に鏡像です**(あちらは plain `GROUP BY` がエラーで拡張 grouping が素通り)。 ### 改善(エラー文) - **このエラーの本文に内部語を出しません**(`B65` / `Phase1` が出ていました)。 なお `GROUP BY DISTINCT` など**別のエラー**の本文には、まだ内部語が残っています - **どの式かを呼び名で示します**——素の列は列名、計算列は別名、別名が無ければ関数名 - **単純な列では、実行できる書き換え例をそのまま示します** - **複合式・`HAVING` / `ORDER BY`・JOIN・CTE では方針だけ示します** (推測で SQL を組み立てると、実行できない例を出すためです) ## v3.56.3(2026-08-06) ### 改善(B101 再開 — 常駐 MCP の版を `ksql_docs` で確かめられるようにする) **`ksql_docs` を引数なしで呼ぶと、索引の先頭で版を名乗ります。** ``` kSQL MCP server version 3.56.3 — the resident process that answered this call. A CLI `--version` reports a different process and can disagree. ``` **MCP は常駐プロセスなので、`npm install` してもクライアントを再読み込みするまで 差し替わりません。** `ksql.js --version` は**別プロセス(CLI)の版**です。 **v3.34.1 で `instructions` の 1 行目に版数を入れましたが、それだけでは足りませんでした。** `instructions` が文脈に入るのは**会話の開始時に 1 回**だけで、 **「版を確かめよう」と思った人が取りに行ける場所ではありません**。 実際に、CLI の `--version` を常駐 MCP の版だと思って測り、 **新しいほうの版を見て「更新済み」と判断する**取り違えが起きました。 **`instructions` の 1 行目は残ります。**押し付けと引き出しは別の役割です。 - **版数の出所は `src/mcp/serverVersion.ts` の 1 つ**にまとめました。 `instructions` と索引が別々に版を書くと、片方だけ古くなります - **`ksql_docs` の出力はこの 1 行が増えるだけ**で、章の内容も節キーも変わりません ## v3.56.2(2026-08-06) ### 改善(B145 警告文が症状と合っていなかった) 明細項目を親から参照したときの警告は「**全行が空になります**」でしたが、 **`HAVING` に書いた場合に見えるのは「0 行」**です。**「行が空」と「行が無い」は別物**なので、 **0 行を見た人がこの警告を自分の話だと思えません**でした。 **原因を書き、代表的な現れ方を並べる形**に変えました。 ``` 数量 はサブテーブル「明細」の中の項目です。APP100 からは値が取れず、エラーにならないまま 常に空になります(列は空、集計は空、`HAVING` などの条件は空との比較になります)。 APP100$明細 から選んでください(親のレコード ID は _pid、親項目は _p.<フィールドコード> になります)。 ``` **挙動は変わりません**(値も警告を出す条件も同じです)。 ### 文書(B145 `HAVING` は黙って 0 行になる) **`HAVING` がいちばん見分けが付きません。** - `GROUP BY` 系は**止まる**ので気づけます - 射影は**空の列**が見えるので、目視すれば気づけます - **`HAVING` は「行が無い」だけ**で、**分析としては正当な「該当なし」と区別が付きません** §19 に明記しました。**0 行が返ったときは、警告が出ていないかを必ず確認してください。** ## v3.56.1(2026-08-06) ### 挙動が変わります(B145 拡張 grouping で明細項目を書くとエラーになる) **`ROLLUP` / `CUBE` / `GROUPING SETS` / `GROUPING()` に明細項目を書くと、これまで エラーにならず「全レコードが空キーの 1 グループへ畳まれた表」が返っていました。** ``` SELECT 数量, COUNT(*) FROM APP100 GROUP BY ROLLUP(数量) 旧: 14 レコードが 2 行(数量 は全部空)になり、分布を見たつもりが単一バケット 新: エラー(どの表から集計するか案内します) ``` plain `GROUP BY` は v3.56.0 以前からエラーでしたが、**拡張 grouping だけが素通り**していました。 ### 修正(B145 「存在しない」ではなく「別の表にある」と伝える) 明細項目を親の `GROUP BY` に書いたときのメッセージが `unknown field code(s)` でした。 **その項目は存在します。** 「そんな項目は無い」と読まれると、静かに空になるのと同じ 誤った結論(データが無い/項目名が違う)へ誘導します。 ``` 数量 はサブテーブル「明細」(APP100)の中の項目です。親テーブルの GROUP BY には指定できません。 APP100$明細 から集計してください(親のレコード ID は _pid、親項目は _p.<フィールドコード> になります)。 ``` **案内先は「その項目を持つ APP」です。** 別アプリを JOIN したとき `FROM` 側の APP を案内して いました(**従うと落ちます**)。**別名で修飾した形**(`ROLLUP(a.数量)`)でも同じ案内になります。 ### 文書(B145 句ごとの挙動を表にする) **明細項目を親から参照したときの挙動が、書いた場所で 3 通りに分かれます。** §19 に表と原理を追加しました。 > **値として出す位置なら「空 + 警告」、キーとして使う位置なら「エラー」。** `WHERE` だけは kSQL が判定せず kintone の `GAIA_IQ07` になることも明記しています。 ## v3.56.0(2026-08-06) ### 挙動が変わります(B145 `DESCRIBE` に `サブテーブル` 列を追加) **`DESCRIBE` は親項目とサブテーブルの中の項目を両方返しますが、区別する列がありませんでした。** ``` フィールドコード | ラベル | タイプ | サブテーブル | ルックアップ | コピー元 | 重複禁止 | 計算式 ``` 明細項目ならサブテーブルのフィールドコード、親項目なら空文字を返します。`タイプ` の直後に置いています(**その項目がどの表にあるかは型の次に読む情報**のため)。 **互換性注意: `SELECT *` の列数が 7 → 8 に増えます。** ``` 移行 WITH d AS (DESCRIBE APP100) SELECT フィールドコード, ラベル, タイプ FROM d ``` `DESCRIBE` を他の表と JOIN して同名列があるときは `d.サブテーブル` のように修飾してください。`UNION` は位置対応、`INSERT ... SELECT` は列数一致なので、必要な列を双方で明示します。 ### 改善(B145 親から明細項目を選んだら警告する) **明細項目を親アプリから `SELECT` すると、エラーにならず全行が空になります。** ```sql SELECT $id, 数量 FROM APP100 -- 数量 が明細項目なら、全行が空 SELECT _pid, 数量 FROM APP100$明細 -- 正しい書き方 ``` 実行時に警告を出すようにしました。**値は変えません**(既存の SQL を壊さないため)。 ``` 数量 はサブテーブル「明細」の中の項目です。APP100 から選ぶと、エラーにならず全行が空になります。 APP100$明細 から選んでください(親のレコード ID は _pid、親項目は _p.<フィールドコード> になります)。 ``` **`FROM` を差し替えるだけでは足りません。** 仮想テーブルに `$id` は無く、そのまま書くと**今度は `$id` が空になります**——同じ静かな失敗を別の列で繰り返します。そのため警告文で `_pid` と `_p.` を明示しています。 **既知の穴**: `WHERE` にだけ明細項目が現れる形(`SELECT $id FROM APP100 WHERE 数量 = 5`)は警告しません。この形は kintone 側が `GAIA_IQ07` で止めるため、**静かには間違いません**。 ## v3.55.0(2026-08-06) ### 改善(B144 サブテーブルの `SELECT` で親項目の条件を押し下げる) **`EXPLAIN` が「押し下げる」と表示しているのに、実行は親レコードを全件取得していました。** ``` SELECT _pid FROM APP100$明細 WHERE _p.$id in (1, 2) EXPLAIN fetch: EXACT / kintone query: $id in (1,2) 実行 親を全件取得(maxRecords が小さいと FetchAllLimitError) ``` **実行側を計画に合わせました。** `WHERE` 全体を kintone クエリへ完全変換できる場合だけ、親レコード取得へ押し下げます。 | `WHERE` の内容 | 押し下げ | |---|---| | 親項目だけ(`_p.$id in (1,2)`・`_p.数値 = 100`) | **する**(変更点) | | 明細項目(`数値_0 = 5`) | しない | | サブテーブル側のシステム列(`_pid` / `_rid` / `_idx`) | しない | | 親項目と明細項目の混在 | しない | **結果は変わりません**(取得後に元の `WHERE` を再評価します)。**変わるのは取得量と `maxRecords` の当たり方**です。 原因は `const parentQuery = isMainTable ? "" : "";` で、**三項演算子の両辺が空文字**でした。**初版から placeholder のまま**で、`EXPLAIN` は別経路で計画を組むため食い違っていました。 ### 文書(B144 サブテーブルの `COUNT(*)` は全件取得になる) **`totalCount` は親レコードの件数**で、**サブテーブル行を数える手段が API にありません**。行はレコードを取得してから展開するため、**件数を知るだけでも親の取得が要ります**。 ``` SELECT COUNT(*) FROM APP100 → fetch: COUNT_ONLY (1 リクエスト・転送 1 件) SELECT COUNT(*) FROM APP100$明細 → fetch: ALL (親を取得して展開) ``` §19 の冒頭と `fetch:` の `COUNT_ONLY` の行に明示しました。**押し下げが効く場合に取得量が減るだけで、この事実自体は変わりません。** ## v3.54.4(2026-08-06) ### 修正(v3.54.3 で `npm test` が失敗する状態になっていた) **v3.25.0 の移行注記を削除した後、全体テストを回さずにリリースしました。** 2 件落ちていました。 - §6 から `server prefilter` の記述が消えた(削除したブロックにあったため)→ **現行仕様として書き直し** - テストが **v3.25.0 の告知そのもの**(`minor` / `破壊的` の文言)を固定していた → **削除の判断に合わせて外した**(拒否される代表例は現行仕様として固定を継続) ### 改善(B141 同じ値を書く場所を 5 か所から 2 か所へ) **照合の網を強くするより、書ける場所を減らすほうが効きます**(依頼元の提案)。 集計の空集合の戻り値を書いていた 5 か所を 2 か所にしました。 | 場所 | 対応 | |---|---| | §8「0 件時の挙動」 | **正本**(標準 SQL 列と刻印を統合) | | §8「集計する値が…」 | **参照へ置換** | | §8 集計関数の表の「値が無ければ空文字」 | **削除** | | §6 スカラーサブクエリの散文 | **値を持たない参照へ** | | §5 除数ガードの表 | **値は残す**(ガードの選び方を示す表のため)。**列見出しを揃えて機械照合の対象に入れた** | **残る 2 か所は、どちらも機械照合の対象**です。**v3.54.1 で古くなった §5 の表が、いま照合下に入りました。** パリティテストから**「表は 2 つ以上」という個数の固定も外しました**。数を書くと、場所を減らしたときに落ちます。 ### 改善(B141 除数ガードが要る指標を症状の側から書く) **踏みやすいのは「率」ではなく「〜あたり」の指標**でした。**在庫日数**(在庫 ÷ 出庫ペース)・**単価**・**1 件あたり**・**回転率**——いずれも**分母が「動きの無い対象」で空になります**。 §5 とレシピ R16 に明示しました。**「前月比」だけを挙げていると、在庫日数を書いている人は自分の話だと思いません。** ### 文書(B143 `EXPLAIN` は `warnings` を返さない) 計画の行には現れますが、**`warnings` は常に空**です。**「`EXPLAIN` が通ったから警告は無い」とは読めません。** `EXPLAIN` の制約に追記し、実装は B143 として起票しました。 **エンジンの変更はありません。** ## v3.54.3(2026-08-06) ### 修正(B141 §8「0 件時の挙動」の表が v3.54.0 の変更に追随していなかった) **同じ §8 の中に、空集合の戻り値を書いた表が 2 つありました。** 片方だけが古いままでした。 ``` 旧 | SUM / AVG / MAX / MIN | 0(標準 SQL の NULL とは異なります) | 新 | SUM / AVG | 0(標準 SQL の NULL とは異なります) | | MAX / MIN | ""(空文字・v3.54.0 で 0 から変更) | ``` ### 内部(B141 パリティテストが「もう 1 つの表」を素通りさせていた) **v3.54.1 で入れた照合は、マーカー 1 つ分の表しか見ていませんでした。** 機械化そのものが、防ごうとした穴と同じ形(**1 か所を直して、同じことを書いた別の場所が残る**)をしていました。 **「値が無いとき」列を持つ表を、文書全体から自動で見つけて全部照合する**ようにしました。**表を新しく足しても、その列名を使う限り自動で検査対象になります。** **強化したテストが、直す前の文書で落ちることを確認してから直しています。** ### 文書(v3.25.0 の移行注記を削除) **v3.25.0 の破壊的変更の告知**を言語リファレンスの冒頭と §6 から削除しました(**29 版前**の移行案内で、リファレンスに置く段階を過ぎています)。**移行案内は CHANGELOG と GitHub Releases にあります。** **§6 の注記に混じっていた現行仕様だけは残しました**(版数の枠を外し、現在の規則として書き直しています)。 - ユーザー系・複数選択系には `in` / `not in` を使う。型に合わない演算子は `WHERE_OPERATOR_INVALID_FOR_FIELD_TYPE` で**取得前に拒否**(0 件を静かに返さない) - `TODAY()` / `NOW()` / `LOGINUSER()` は押し下げられる形だけ使用できる **エンジンの変更はありません。** ## v3.54.2(2026-08-06) ### 改善(B141 `fetch` は同じ `WHERE` でもクエリの形で変わる) **`fetch:` の節に、形で値が変わることを追記しました。** ``` SELECT COUNT(*) FROM APP100 WHERE 担当者 in ('user1') → COUNT_ONLY (押し下げは効いているが、走査しないので COUNT_ONLY が優先される) SELECT 区分, COUNT(*) FROM APP100 WHERE 担当者 in ('user1') GROUP BY 区分 → PREFILTERED (同じ WHERE・同じ押し下げ。走査するので PREFILTERED) ``` **押し下げが効いているかを比べるときは、すべて同じ形で測ってください。** 形を混ぜた表を作ると、**押し下げの差ではなく形の差**を読んでしまいます。 分類表そのものは定義表なので誤りはありませんでしたが、**「同じ `WHERE` でも形で変わる」がどこにも書かれていませんでした**。 ### 改善(B141 挙動を写した表に「実測した版」を刻む) **挙動を写した表に `(実測・vX.Y.Z)` を刻むようにしました**(除数ガードの表・空集合の戻り値の表)。 **複製された先で staleness を検出できるようにするため**です。この文書は MCP リソースとして配られ、**利用側のドキュメントへ複製されます**。刻印があれば、複製先は「どの版の観測を写したか」を一緒に持ち帰れます。 **エンジンの変更はありません。** ## v3.54.1(2026-08-06) ### 修正(B141 除数ガードの表が v3.54.0 の変更に追随していなかった) **言語リファレンスの中で、同じ挙動を 2 か所が違う値で説明していました。** 除数ガードの表は `MIN` / `MAX` を「集計を通した値」に含め、**該当が無いときは `0`** としていました。v3.54.0 で **`MIN` / `MAX` は空文字を返すよう変えた**ため、この表だけが古いままでした。 | 除数の作り方 | 該当が無いときの値 | 必要なガード | |---|---|---| | `SUM` / `COUNT` / `AVG` を通した値 | `0` | `= 0` で足りる | | **`MIN` / `MAX` を通した値** | **空文字** | `= '' OR = 0` | | 素の列として渡ってきた値 | 空文字 | `= '' OR = 0` | **「集計を通したから安全」ではありません。** ### 内部(B141 の再発防止を機械化) **文書の表とエンジンの実際の戻り値を突き合わせるテスト**を追加しました。空集合の戻り値の表を解析し、各関数を空入力で実行して照合します。**エンジンが対応する集計関数がすべて表に載っていること**も確かめるため、関数を追加したら落ちます。 あわせて表を機械が読める形に揃えました(「統計集約」を `VAR_POP` / `VAR_SAMP` / `STDDEV_POP` / `STDDEV_SAMP` へ展開し、**書かれていなかった `GROUP_CONCAT` を追加**)。 **エンジンの変更はありません。** ## v3.54.0(2026-08-06) ### 挙動が変わります(B142 値が無いときの `MIN` / `MAX` が `0` から空文字へ) **集計する値が 1 つも無いときの `MIN` / `MAX` が、`0` ではなく空文字を返すようになりました。** ```sql SELECT MIN(製品名) FROM APP100 WHERE 分類 = '存在しない分類' -- 旧: "0"(長さ 1 の文字列。= '' をすり抜ける) -- 新: "" ``` **同じ状況が形によって割れていたのが理由**です。空セルは `MIN` / `MAX` では**値として保持される**ため、値が 1 個なら空文字、0 個なら `0` になっていました。 ``` LEFT JOIN で一致しないグループ(空セル 1 個) "" ← 従来から WHERE が全部落ちる(値 0 個) "0" ← ここが変わった CASE に ELSE が無く全行が非一致(値 0 個) "0" ← ここが変わった ``` `MODE` / `MEDIAN` / 統計集約は**以前から空文字**で、比較系で `0` を返すのは `MIN` / `MAX` だけでした。標準 SQL も `MIN` / `MAX` の空集合は型に依らず `NULL` です。 **`COUNT` / `SUM` / `AVG` は変わりません**(`0` のまま)。 #### 移行 **`MIN` / `MAX` の結果を 0 件かどうかの判定に使っている場合**は影響を受けます。 ```sql -- 旧: 該当なしでも "0" が返るため、空判定は効かなかった -- 新: 空判定が効く CASE WHEN MIN(製品名) = '' THEN '該当なし' ELSE MIN(製品名) END ``` **DML では書き込む値が変わります。** ```sql UPDATE APP88 SET 上限費用 = (SELECT MAX(合計費用) FROM APP88) WHERE 確度 IN ('80%') -- サブクエリが 0 件のとき、書き込む値が 0 から空になります ``` 該当が 1 件も無いときに「最大値は `0`」と書き込むのは事実に反するための変更ですが、**対象が必須フィールドなら、これまで通っていた `UPDATE` が失敗するようになります**(空を書けないため)。**既に `0` が書き込まれているレコードは遡って変わりません。** ### 改善(B141 除数のガードが要る形を絞り込む) 「除数には常に `= '' OR = 0`」ではなく、**どの除数にガードが要るか**を言語リファレンスに表で示しました。 | 除数の作り方 | 該当が無いときの値 | 必要なガード | |---|---|---| | 集計を通した値(`SUM` / `COUNT` / `AVG`) | `0` | `= 0` で足りる | | 素の列として渡ってきた値 | 空文字 | `= '' OR = 0` | **空文字は「行が無かった」ことから生まれます**——`LEFT JOIN` で一致しなかった行、`LAG` / `LEAD` の端の行、`NULLIF` の結果。この 3 つを除数にするときだけガードが要ります。 空集合の戻り値も関数ごとに表で示しました。 ## v3.53.0(2026-08-06) ### 修正(B140-C2 従っても消せない警告に、消せないことを書く) v3.52.0 で「**従うと壊れる**」助言は直したが、「**従いようがない**」助言が残っていた。 月次集約の CTE を `LAG` で読む形では、案内された「その表の中で一意になる列を `ORDER BY` に含めてください」に**従う手段が無い**。`年月` は既に `ORDER BY` にあり、それが `GROUP BY` キーそのもので、CTE の中にそれ以上「一意だと証明できる列」は存在しない。 限界を併記した。 ``` … その表の中で一意になる列(元の集約のキーなど)を ORDER BY に含めてください。 集約結果の列は一意でも証明できないため、すでに一意な場合もこの警告が出ます。 ``` **値は正しい。** 警告を出す条件も値も変えていない。 ### 修正(B141 文書の SQL の取り残し 2 件) **言語リファレンスのゼロ除算ガードの ○ が、その注記自身が説明した理由で `NaN` を出していた。** ```sql -- ✗ ゼロだけガードすると空セルがすり抜ける SELECT CASE WHEN 金額 = 0 THEN '' ELSE 1000 / 金額 END AS 単価 FROM APP100 -- ○ 空セルとゼロの両方をガードする SELECT CASE WHEN 金額 = '' OR 金額 = 0 THEN '' ELSE 1000 / 金額 END AS 単価 FROM APP100 ``` 空文字は**算術では 0 になるが、比較では 0 と等しくない**ため、`= 0` だけでは空セルの行が `ELSE` へ落ちる。除数が空になり得る場面(`LEFT JOIN` で一致しなかった行・`LAG` / `LEAD` の端の行・`NULLIF` の結果)では両方を書く。 レシピ R16 の注記も直した。CTE の話なのに `レコード番号` を名指ししていた点と、「`年月` は 1 行に定まる」と書きながら実行すると警告が出る点。 **説明を直しても例が残る形は 3 回目**のため、再発防止を B141 として起票した。 ## v3.52.0(2026-08-06) ### 修正(B140 実行できないタイブレーク助言を出さない) ウィンドウの `ORDER BY` が全順序と判定できないときの警告で、**CTE や一時テーブルを読む場合に「`ORDER BY` にレコード番号などのタイブレークキーを足してください」と案内していた**。しかし**その表に `レコード番号` は無い**ため、案内どおりに直すと `unknown field code(s): レコード番号` で落ちていた。 経路に応じて案内を変える。 ``` CTE / 一時テーブル その表の中で一意になる列(元の集約のキーなど)を ORDER BY に含めてください 物理アプリ ORDER BY にレコード番号などのタイブレークキーを足してください(従来どおり) ``` 既定フレーム(`RANGE`)の警告と `LAG` / `LEAD` の警告の両方に効く。**警告を出す条件と値は変えていない。** ### 文書(レシピ R17「行の無いもの」を 0 として並べる) **`GROUP BY` はデータのある行しか作らない。** 集計対象が 0 件のものは行ごと消え、エラーも警告も出ないため「0 だった」と「そもそも無かった」の区別が付かない。**検出したいのがまさにその 0 件**のとき(動きが止まった製品など)に効く。 マスタ起点の `LEFT JOIN` で 0 を出す形と、**条件を `WHERE` に書くと `LEFT JOIN` の後に効いて元の問題へ戻る**ことを示した。**日付には起点になるマスタが無いため同じ手が使えない**ことも明記している。 ## v3.51.0(2026-08-06) ### 新機能(B128 `LAG` / `LEAD`) `LAG(expr [, offset])` / `LEAD(expr [, offset])` を `OVER (PARTITION BY ... ORDER BY ...)` で使用できるようにした。前後の行がパーティション外なら空文字を返し、`offset` は省略時1、非負の safe integer リテラルだけを受け付ける。第3引数の既定値は未対応。 値参照ウィンドウは引数の型メタデータを引き継ぐため、CTE・一時テーブルの次段でも数値順、日付順、選択肢の定義順など元の列の比較規則を維持する。引数にしか現れない物理フィールドも取得対象に含める。 前月比は「月次集約 → `LAG` で前月列を付与 → 比率計算」の3段で記述する。ウィンドウ結果を同じSELECTの式に含める形は従来どおり拒否し、CTEまたは一時テーブルへ分ける診断を返す。 ## v3.50.0(2026-08-06) ### 修正(B137 列数の違う `UNION` / `UNION ALL` をエラーにする) `UNION` / `UNION ALL` の左右で列数が違う場合、`ArgumentError` で停止するようにした。従来は右辺の列が足りないと空文字で埋め、余ると黙って捨てていたため、成功しても意図しない結果になり得た。 ```sql -- 修正前: 成功し、右辺由来の行では 仕入先 が空文字になっていた SELECT 製品名, 仕入先 FROM APP4228 UNION SELECT 製品名 FROM APP4229 -- 移行後: 足りない側へリテラルを追加して列数を揃える SELECT 製品名, 仕入先 FROM APP4228 UNION SELECT 製品名, '' FROM APP4229 ``` 列名は従来どおり左辺から取り、左右の列を位置で対応させる。列名の一致と型の互換性は検査しない。 **言語リファレンスは以前から「両辺の列数が一致しない場合はエラー」と書いていた。** 文書が正しく、実装がその契約を守っていなかった(B136 と同じ乖離で向きが逆)。 ### 内部(B136 の再発防止 文書の列表と実装の定数をテストで突き合わせる) v3.49.0 で、言語リファレンスの `SHOW APPS` / `DESCRIBE` の例が**そのまま実行するとエラーになる**状態を修正した。この文書は MCP リソース `ksql://language-reference` の原本なので、**エージェントが壊れた例を写す**状態だった。 再発を機械で止める。 - `SHOW APPS` / `DESCRIBE` の列名を `SHOW_APPS_COLUMNS` / `DESCRIBE_COLUMNS` として定数化した - **言語リファレンス §14 の列表と定数が一致することをテストで固定**した。旧列名(`fieldCode` 等)が文書に残っていないことも見る 例文そのものの実行検証は重いので行わない。**列表が正しい限り、その表から引いて書かれた例文も正しくなる**という前提に乗る。 ### 内部(B139 リリース済みの CHANGELOG 節へ追記する取り違えを止める) `version:check:release` で、**`CHANGELOG.md` の先頭の版数見出しが `package.json` と一致すること**を要求するようにした。開発中の `npm test` では無効なので、`## Unreleased` を持つ運用は変わらない。 v3.49.0 と v3.50.0 の実装で、**リリース済みの節へ新しい記載を追加する取り違えを 2 回続けて踏んだ**ため。 ## v3.49.0(2026-08-06) ### 新機能(B130 `DESCRIBE` に「値の由来」4 列を追加) `DESCRIBE` は**フィールドコード・ラベル・型だけ**を返していた。**型からはルックアップか素の文字列かが分からず、推測がしばしば当たるので、外れたときだけ静かに間違う**(実例=ルックアップを素の文字列と誤認して「結合キーが無い」と分析レポートに書いた)。 ``` フィールドコード | ラベル | タイプ | ルックアップ | コピー元 | 重複禁止 | 計算式 ``` - 該当するとき文字列 `YES`、それ以外は空文字を返す(全列とも文字列) - 出すのは**「値をどう読むか」を変える情報だけ**。`required` / `maxLength` などの**値を書くための制約は含めない**(それらは `ksql_app_metadata`) - **`コピー元`** はルックアップが `fieldMappings` でコピーしてくる項目に立つ。**`GROUP BY` の意味が変わる**ため(マスタの現在値ではなく**入力時点のスナップショット**で割れる) - **4 列がすべて空でも、制約や設定の確認が不要とは限らない。** 完全な判定材料ではない **互換性注意: `SELECT *` の列数が 3 → 7 に増える。** ``` 移行 WITH d AS (DESCRIBE APP100) SELECT フィールドコード, ラベル, タイプ FROM d ``` `DESCRIBE` を他の表と JOIN して同名列があるときは `d.ルックアップ` のように修飾する。`UNION` は位置対応、`INSERT ... SELECT` は列数一致なので、必要な列を双方で明示する。 ### 改善(B129 ウィンドウ結果を式に使ったときの診断に「次の一手」を示す) `ROUND(SUM(x) OVER (), 1)` のようにウィンドウ結果を同じ `SELECT` の式へ入れると診断で止まるが、**次に何を書けばよいかが分からなかった**。一般形と ○× の例を示すようにした。 ``` ウィンドウ関数の結果は同じ SELECT の式では使えません。 × SELECT ROUND(SUM(x) OVER (), 1) AS a FROM t ○ WITH w AS (SELECT SUM(x) OVER () AS 総計 FROM t) SELECT ROUND(総計, 1) AS a FROM w ウィンドウ結果を列として出し、それを使う式は次の段(CTE または一時テーブル)に書いてください ``` 位置・トークンの表示は従来どおり残る。**レシピ R15「全体で割る(構成比・累積構成比・ABC)」も追加**した——診断文を読むのは止まった人だけだが、レシピは書く前に読まれるため。 ### 修正(B135 `mcp:smoke` の期待値ずれ) v3.45.0 でレシピ R14 を追加した際、`scripts/mcp-smoke.mjs` の期待値が `recipes/r1..r13` のまま残り、**`npm run mcp:smoke` が 3 版にわたり失敗していた**。`mcp:verify` にしか入っておらず `npm test` にも `prepack` にも無いため気づけなかった。**出荷物には影響しない。** 併せてレシピ数の二重管理を解消した(期待値を生成側から導出する形へ)。 ### 修正(B136 の一部 言語リファレンスの `SHOW APPS` / `DESCRIBE` 例が実行できなかった) 文書は `name` / `fieldCode` / `label` / `type` と書いていたが、実装は `アプリID` / `アプリ名` / `説明` / `フィールドコード` / `ラベル` / `タイプ` で、**例をそのまま実行するとエラーになっていた**。この文書は MCP リソース `ksql://language-reference` の原本なので、**エージェントが壊れた例を写す**状態だった。 ## v3.48.0(2026-08-05) ### 新機能(B133 保存クエリの複文対応+実行時の変数注入) **保存クエリは 1 文しか置けなかった**ため、`DECLARE @基準日 = ...; SELECT ...` のように**基準日を変数にした定番クエリが保存できなかった**。パラメータ化したクエリほど保存して使い回したいのに、パラメータ化した途端に保存できなくなるという逆転が起きていた。 ``` 保存する SQL DECLARE @d90 = '2026-05-08'; SELECT 製品名, SUM(個数) AS 出庫数 FROM APP4228 WHERE 入出庫区分 = '出庫' AND 日付 >= @d90 GROUP BY 製品名 実行 ksql_run_saved_query { name: "...", variables: { d90: "2026-07-20" } } ``` - **`ksql_run_saved_query` に `variables` を追加**した。`ksql_query` / `ksql_mutate` と同じ形で、`DECLARE` 変数の既定値を実行時に上書きする。キーは `@` を付けず、大文字小文字を区別しない - **`readOnly: true` の保存クエリは複文を保存・実行できる**ようになった。一時テーブルを使う多段クエリも保存できる。複文の結果は `ksql_query` と同じバッチエンベロープで返る - 制約の解除であって、意味論の変更ではない。**単文の保存クエリは従来と完全に同じ** **対象外**(従来どおり拒否する): - **実書き込み DML を含むバッチ**。`readOnly: true` でも `readOnly: false` でも保存・実行できない。DML の保存クエリは単文のみ - **`SET` への注入**。注入先は `DECLARE` だけで、これは `ksql_query` と同じ契約。`SET` だけで定義した変数に `variables` を渡すと未宣言変数として拒否する - `tempTableMaxRows` などの非公開 batch option。read-only バッチでもエンジン既定の行数上限が適用される **カタログは手で編集できる**ため、保存時だけでなく**実行時にも同じ判定をやり直す**。手編集で実書き込み DML を仕込んでも、読み取り経路には届かない。 ### 修正(B135 `mcp:smoke` の期待値ずれ) v3.45.0 でレシピ R14 を追加した際、`scripts/mcp-smoke.mjs` が持つエラー文言の期待値が `recipes/r1..r13` のまま残り、**`npm run mcp:smoke` が 3 版にわたり失敗していた**。`mcp:smoke` は `mcp:verify` にしか入っておらず `npm test` にも `prepack` にも無いため気づけなかった。**出荷物には影響しない**(smoke 専用の期待値)。 ## v3.47.0(2026-08-05) ### 改善(B126 選択系フィールドの `=` / `!=` を自動で押し下げる)**※結果は変わりません** `WHERE 区分 = '出庫'` のような**選択系フィールドの等値比較が、kintone 側で絞り込まれず全件取得になっていた**。kintone のクエリ構文が選択系に `in` / `not in` しか受け付けないためで、`IN ('出庫')` と書けば絞り込めるが、**エラーも警告も出ないので踏んでも気づけなかった**。 ``` 修正前 WHERE 入出庫区分 = '出庫' → fetch: ALL(全件取得してから JS で判定) 修正後 WHERE 入出庫区分 = '出庫' → fetch: EXACT(kintone 側で絞り込む) ``` - **利用者が書き換える必要はない。** エンジンが `= 'X'` を `IN ('X')` へ、`!= 'X'`(`<>` 含む)を `NOT IN ('X')` へ正規化して押し下げる。**利用者が最初からそう書いた場合とまったく同じ経路**を通る - **結果は変わらない。** kSQL は押し下げ後も元の `WHERE` をローカルで再評価するため、押し下げは「どの候補を取りに行くか」だけを変える - **`EXPLAIN` に `pushdown normalized:` 行**が出る。書いた SQL と `kintone query` の食い違いはこれで説明される - **対象は単一値の選択系のみ**(`RADIO_BUTTON` / `DROP_DOWN` / `STATUS` 等)。チェックボックス・複数選択・ユーザー選択などの**複数値フィールドは対象外**(`IN` が「いずれかを含む」判定になり、`=` と意味が変わるため) - **定義に無い選択肢値のときは正規化しない。** `= '存在しない値'` は従来どおり 0 行を返す(`IN ('存在しない値')` と書くと kintone が `GAIA_IQ10` を返すため、自動で載せると動いていたクエリが壊れる) - 空文字(`= ''`)も対象外 ### 新機能(B127 ウィンドウ関数の既定フレームを警告する) 集計ウィンドウで `ORDER BY` を書き `ROWS` / `RANGE` を明示していないとき、**既定は標準 SQL どおり `RANGE`** で、**`ORDER BY` の値が同じ行はすべて同じ値**になる。同日取引のある台帳では「その日を締めた残高」が並び、取引ごとの残高を期待して書くと**エラー無しで別の意味の値**が返っていた。 `warnings` に次を出すようにした。 > `累積` は既定フレーム(RANGE)で評価されます。ORDER BY の値が同じ行はすべて同じ値になります。行ごとの値が必要なら `ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW` を明示するか、`ORDER BY` にレコード番号などのタイブレークキーを足してください。 - **`ORDER BY` に `$id` / レコード番号を含み、単一アプリ・JOIN なし・サブテーブルなし・CTE / `UNION` を経ていない**ときは全順序が保証されるので警告を出さない。ウィンドウは JOIN 後の行を評価するため、1:N の JOIN やサブテーブル展開では同じ `$id` が複数回現れる。**証明できないときは警告を出す** - `UNION` / 実体化 CTE では、子 SELECT の警告を親へ集約するようにした(従来は捨てていた) ### 修正(B132 `ksql_docs` のセクションキーに章番号を付ける) v3.45.0 でウィンドウ関数を独立セクションにしたとき、`window-functions` だけ章番号が付かず、他の 26 セクション(`01-basic-rules` 〜 `26-assert`)と表記が揃っていなかった。**`10-1-window-functions` へ改めた。旧キーは引き続き解決できる**(索引には出さない)。 ## v3.46.0(2026-08-05) ### 新機能(B124 集計算術式に `GROUP BY` キーの列と `@変数` を書ける) `SUM(t.個数) * m.単価` のように、**集計結果へ `GROUP BY` キーの列や `@変数` を掛けられる**ようにした。これまでは `GROUP BY` に含めている列でも `ParseError: 集計算術式には集計関数または数値が必要です` になり、`SUM(t.個数 * m.単価)` と内側に書き換える必要があった。 ```sql SELECT m.製品番号, m.製品名, m.仕入価格, SUM(t.個数) * m.仕入価格 AS 在庫金額 FROM APP1 m LEFT JOIN APP2 t ON m.製品名 = t.製品名 GROUP BY m.製品番号, m.製品名, m.仕入価格 ``` - **SELECT 列と `HAVING` の両方**で使える。パーサを共有しているため、片方だけ許可すると B121・B122 で潰した「別名なら通る / 直接なら通らない」の非対称が再発する - **集計関数から始まる形に限る。** `単価 * SUM(a)` は専用の診断で拒否する(既存の構文解析は式が集計関数で始まるときだけ集計算術式の経路へ入るため) - **`GROUP BY` に書いた表記と一致する列だけ**を許可する。`GROUP BY m.単価` に対して非修飾の `単価` は使えない。`GROUP BY` の式・関数・SELECT alias もこの位置には書けない - **`ROLLUP` / `CUBE` / `GROUPING SETS` では書けない。** 小計・総計行ではその grouping set から外れた列が空になり、値が定まらないため。専用の診断で拒否する - **`SUM(a) * 単価` と `SUM(a * 単価)` は同じ値になるとは限らない。** 小数では丸めの位置が違い、非数値の列では前者が `NaN`、後者が `0` になる。言語リファレンス §8 に明記した - `@変数` の非数値は従来の算術と同じく `ArgumentError`(`NaN` にはしない) - 機能従属性の推論(主キーだから一意に決まる等)は**入れていない**。`GROUP BY` に書いてあるかどうかだけで判定する ### 直したかった動機 **起点は依頼ではなく、AI に kSQL を使わせた記録**。分析セッションで、エージェントが自然な依頼(在庫金額 = 現在庫 × 仕入価格)に対して**まずこの形を書いて `ParseError` を踏んだ**。「計算できるか」ではなく「**最初に書かれる形が通るか**」で実需を測るべきだった、という判断の修正が起票につながっている。 ## v3.45.0(2026-08-05) ### 新機能(B125 集計のウィンドウ関数 — 累計・累積件数) `SUM` / `COUNT` / `AVG` / `MIN` / `MAX` を `OVER (...)` で使えるようにした。従来のウィンドウ関数は順位系 3 つ(`ROW_NUMBER` / `RANK` / `DENSE_RANK`)だけで、**累積和が書けなかった**。在庫台帳の残高推移のような「1 行ずつ積み上げる」集計を、SQL の外へ出さずに書ける。 ```sql SELECT 製品名, 日付, 個数, SUM(個数) OVER ( PARTITION BY 製品名 ORDER BY 日付, レコード番号 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS 累積在庫 FROM APP100 ``` - **フレームは標準 SQL 準拠。** `ORDER BY` があるときの既定は `RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`、無いときはパーティション全体。明示できるのは `ROWS` / `RANGE` の同じ固定境界のみ。 - **`RANGE` と `ROWS` は同順の行で結果が変わる。** 同日 3 件の台帳なら `RANGE` は 3 行とも「その日を締めた残高」、`ROWS` は「取引ごとの残高」。日次残高なら既定の `RANGE`、取引ごとなら `ROWS` を明示するか `ORDER BY` にレコード番号などのタイブレークキーを足す。**`EXPLAIN` が実効フレームを表示し、既定のときだけ `(既定)` を付ける。** - **完全入力を要求する。** `ORDER BY` の有無にかかわらず、取得上限での `onLimit=truncate` は部分結果を返さずエラーになる(`complete input reason: AGGREGATE_WINDOW`)。FULL_SCAN は評価場所を決めるだけで部分入力を防がないため、専用の理由を追加した。 - 空値・`NaN` のスキップと `MIN`/`MAX` の比較規則は**通常の集計関数と共通**(行ごとの値抽出を共有する実装にした)。ただし小数を含む `SUM`/`AVG` はウィンドウの並び順で加算するため通常集計と最下位ビットが一致しないことがあり、`MIN`/`MAX` は canonical 同値の値(数値型の `"1"` と `"01"` など)で残る raw 表記が不定。 - **`SELECT DISTINCT` との併用は従来どおり可能**(ウィンドウ評価後に DISTINCT を適用)。 - **非対応**: 引数の `DISTINCT`(`SUM(DISTINCT x) OVER`)、`GROUP_CONCAT` / 統計集計の `OVER`、移動フレーム(`ROWS BETWEEN n PRECEDING`)、`LAG` / `LEAD`、`GROUP BY` / 通常集計との併用、ウィンドウ結果を同じ SELECT 内の式へ入れる形。いずれも専用の診断を出す。月次の累計は CTE 2 段で書く。 - 言語リファレンス §10.1 に集計ウィンドウの節、`ksql_docs` に `window-functions` セクション(従来は `order-by` に畳まれていた)、レシピ R14「累積残高(台帳)」を追加した。 ### 修正(B123 通常の `GROUP BY` だけの SELECT で `EXPLAIN` / `--dry-run` がエラーになる) - `SELECT 分類, COUNT(*) FROM APPx GROUP BY 分類` の実行計画を取ろうとすると `No-op client should not be called.`(MCP)/ `DryRunError: API call should not happen in dry-run.`(CLI)で落ちていた。**分かれ目は `GROUP BY` の有無ではなく「`GROUP BY` があり、かつフィールドを参照する `WHERE` も `ORDER BY` も無い」こと**で、`ORDER BY` を 1 つ足すと通っていた。 - 原因は、フォーム定義の要否を判定する述語が**通常の `GROUP BY` を見ていなかった**こと(B65 の `ROLLUP` / `GROUPING SETS` は入っていた)。要否が偽になるとレコード API を呼ばないためのダミークライアントが渡り、グループキーの型解決が弾かれていた。MCP と CLI が同じ述語を共有するため両方で再現していた。 - **誤った計画を返していたわけではない**(止まるべきでないところで止まっていた)。`EXPLAIN` がレコード API を呼ばない契約は不変で、増えるのはフォーム定義の取得のみ。 - 影響として、「JOIN や大量取得を含むクエリは `EXPLAIN` まで通す」という運用ルールが**集計クエリに対して最初から機能していなかった**。分析クエリはほぼ全部 `GROUP BY` を含むため。 ## v3.44.0 以前 **内容は [GitHub Releases](https://github.com/rex0220/kintone-sql-tools/releases) の各タグに残っています。** 本ファイルは v3.45.0 以降だけを保持します(2026-08-06 に 1,426 行 / 204KB から整理)。