おい、最近アセンブラ崩れの先輩から「PL/Iは古い言語だから適当に書いても動く」なんて教わってないか?甘いな。基幹システムの現場でDB2を叩くオンラインやバッチにおいて、ホスト構造体(Host Structure)の扱いを誤ると、何の前触れもなく突然のS0C4や、原因究明に丸一日かかるSQLCODE -303(データ型不一致)の悪夢を見る羽目になる。
今回は、PL/Iの「識別子には予約語が存在しない」という変態的な……いや、懐の深い構文規則と、DB2埋め込みSQLにおけるホスト構造体の絶妙かつ危険な展開挙動について、現場のノウハウを叩き込んでやる。しっかりついてこい。
—
1. PL/Iの「予約語なし」という思想とホスト構造体の罠
まず、PL/Iの根本的な言語仕様を思い出してくれ。C言語やJavaと違って、PL/Iには「予約語(Reserved Word)」というものが存在しない。
`IF`、`THEN`、`READ`、さらには `DECLARE` でさえも、すべて「文脈依存のキーワード(Contextual Keyword)」に過ぎない。
1
/ これ、PL/Iでは合法なコードだ /
DECLARE IF FIXED BIN(31);
DECLARE THEN CHARACTER(10);
IF = 10;
コンパイラは、その位置にある単語が変数名なのか命令なのかを、前後の文脈だけで判断している。この「何でも変数名に使える」柔軟性が、DB2の埋め込みSQL(SQL/PLIプリプロセッサ)と組み合わさったとき、恐ろしい罠を生む。
SQL文の中でホスト構造体やホスト変数を参照するとき、プリプロセッサはCOBOLのように「これは変数のエリアだ」と厳密に区別しているわけではない。特に、構造体(大構造体およびグループ化された小構造体)を定義して `FETCH` や `SELECT INTO` で一括受け渡しをする場合、PL/Iのデータ属性とコンパイラの展開結果を完璧に理解していないと、コンパイルエラーか実行時異常のどちらかの壁に激突することになる。
—
2. 実践:DB2ホスト構造体定義の黄金ルール
百聞は一見に如かず。実際に現場のバッチプログラムで通用する、厳格なコーディング作法に基づいたコードを見てみよう。月次売上データを一括取得するバッチの抜粋だ。
1
DCL 1 WK-SALES-REC,
/ 顧客ID:CHAR(5) + ヌルターミネータ考慮はDB2プリコンパイラの仕様による /
5 WK-CUST-ID CHAR(5),
/ 顧客名:漢字混じりのためグラフィック型を使用する場合もあるが今回はCHAR /
5 WK-CUST-NAME CHAR(40),
/ 売上金額:COMP(固定小数点二進数) /
5 WK-SALES-AMT FIXED DEC(11,2),
/ 更新タイムスタンプ /
5 WK-UPD-DATE CHAR(26);
/ ヌル標識変数用の構造体(インディケータ・アレイ) /
DCL 1 WK-IND-STRUCT,
5 IND-CUST-ID FIXED BIN(15),
5 IND-CUST-NAME FIXED BIN(15),
5 IND-SALES-AMT FIXED BIN(15),
5 IND-UPD-DATE FIXED BIN(15);
ここが現場のポイントだ!
1. レベル番号の整合性: DB2のプリプロセッサ(DSNHPC)は、PL/Iの構造体の階層を解釈し、SQL生成時にフラットな変数群へと展開する。レベル1をルートとし、レベル5などでグループ化するのがメインフレーム界隈の標準的なコーディング規約だ(COBOLの影響で01や05を使う感覚に近い)。
2. データ型のマッピング:
- SQLの `VARCHAR` はPL/Iでは長さを示す半精度整数(`FIXED BIN(15)`)と文字列の構造体に展開されるべきだが、固定長 `CHAR` ならそのままマッピングできる。
- 金額系の `DECIMAL(11,2)` は必ず `FIXED DEC(11,2)` にマップすること。これを `FLOAT` や `FIXED BIN` で受けると、DB2側で暗黙の型変換が発生し、最悪の場合は桁落ちやSQLCODE -303を引き起こす。
—
3. SQL文での一括参照とプリコンパイラの挙動
定義した構造体を使って、実際にカーソルからデータをフェッチする処理を見てみよう。ここでもBUILTIN関数の活用や、エラーハンドリング(ONユニット)の絡みが重要になってくる。
1
/ カーソルのオープン /
EXEC SQL OPEN C1;
DO WHILE (SQLCODE = 0);
/ ホスト構造体による一括フェッチ /
EXEC SQL
FETCH C1 INTO :WK-SALES-REC INDICATOR :WK-IND-STRUCT;
IF SQLCODE = 0 THEN DO;
/ 正常取得時の処理:ここでBUILTIN関数などを駆使してデータ加工 /
CALL PROCESS_DATA();
;
ELSE IF SQLCODE = 100 THEN DO;
/ データ終了(EOF) /
LEAVE;
;
ELSE DO;
/ 予期せぬSQLエラー:ONユニットやエラーログ出力へ /
DISPLAY(‘DB2 FETCH ERROR. SQLCODE = ‘ || TRIM(CHAR(SQLCODE)));
SIGNAL ERROR;
;
END;
EXEC SQL CLOSE C1;
コンパイラとプリコンパイラの裏側
DB2プリプロセッサ(DSNHPC)を通すと、上記の `EXEC SQL FETCH … INTO :WK-SALES-REC` は、最終的にPL/IのMOVE(代入)文の嵐、あるいはDB2ランタイムスタブ(DSNHLIなど)を呼び出すコードへと展開される。
ここで注意すべきは、構造体の中に配列(DIMENSION)や、ポインタ(POINTER)、さらには可変長文字列(VARYING)が混ざっている場合のエクスパンション(展開挙動)だ。
特に `VARYING` 属性を持つ文字列を構造体に含めると、PL/I上では「前半2バイトが長さ、後半が実データ」という隠し構造になるため、プリプロセッサが予期せぬC言語風のヌル終端処理を付加したりして、メモリ上のオフセットがズレる事故が多発する。基幹系で構造体を使うなら、原則として固定長(CHAR, FIXED DEC, FIXED BIN)のみで構成されたフラットな構造体に留めておくのが、夜間バッチを平穏に終わらせるための極意だ。
—
4. ONユニットによる異常系制御フロー
メインフレームのバッチタスクにおいて、データ異常やシステム障害が発生した際の振る舞いは極めて重要だ。PL/Iには強力な例外処理機構である `ON` ユニットがある。
1
/ 予期せぬエラー(データ溢れやゼロ割など)をトラップするONユニット /
ON ERROR BEGIN;
DISPLAY(‘ CRITICAL ERROR OCCURRED IN BATCH RUN ‘);
DISPLAY(‘PL/I ERROR CODE: ‘ || ONCODE());
/ DB2のロールバックを明示的に実行 /
EXEC SQL ROLLBACK;
CLOSE SYSPRINT;
STOP;
END;
SQLCODEがマイナスになった場合(致命的なDBエラー)や、構造体への代入時にデータ型が溢れた場合のハードウェア割込み(S0C7など)は、この `ON ERROR` ユニットが拾い上げて安全にジョブを異常終了(ABEND)させる。ダンプリストを出力してコンソールに泣き言を言う前に、しっかりとハンドリングしておくのがプロのアーキテクトの仕事だ。
—
5. ベテランからの最後のアドバイス
PL/Iのホスト構造体は、何十個もあるカラムを一つずつ `:VAR1, :VAR2, :VAR3…` と書く手間を省き、ソースコードの可視性を劇的に高めてくれる強力な武器だ。しかし、その裏ではコンパイラとDB2プリプロセッサが複雑な型チェックとメモリマッピングを行っている。
「動けばいいや」と適当に型を定義するのではなく、「DB2の列定義」「PL/Iのデータ属性」「ストレージの境界」の3つが完璧に一致していることを、コンパイルリスト(Cross-Reference Listing)で常に確認する癖をつけてほしい。
レガシーシステムの寿命を延ばすも縮めるも、君たちのその丁寧なコーディングにかかっている。次のバッチ改修でも、美しい構造体定義でまわりをうならせてくれよ。
