2025年4月17日木曜日

SQLclのmleコマンドでのRollup.jsのサポートを確認する

Oracle Database 23aiよりMLE(Multilingual Engine)の機能が拡張され、MLEモジュールを作成できるようになりました。概ねESMAScript標準のモジュール(ES module)に相当します。

Oracle CorporationでMLE/JavaScriptのプロダクト・マネージャを務めるMartin Bachさんは、彼のブログで、データベースのMLEおよび他の製品でもMLEに関連した機能について、いくつもの記事を公開されています。

以前の記事で、SQLclのmleコマンドを使ってMLEモジュールを作成する際に、同時にRollup.jsによるバンドル化を行う方法を紹介されています(SQLcl introduces support for Rollup.js to load MLE modules)。Rollup.jsによるバンドル化自体はSQLclの機能ですが、簡易なGraphQLの実装を例に、その使い方について解説されています。

簡易な例でもGraphQLは重いな、と思っていたところ、pdf-libを使ってデータベース内でPDFを生成する方法を紹介する記事が公開されました(Using PDF-LIB to generate PDFs in the database)。

本記事では、Martin Bachさんが紹介されているpdf-libを使った記事を参考にして、MLEモジュールのバンドル化を試します。また、SQL Developer Extension for VS Code 25.1から、MLE環境とMLEモジュールも扱えるようになったので、それらも使ってみます。

以前に記事「NPMパッケージからOracle Datase 23aiのMLEモジュールを作成する」にて、バンドル化とは異なる方法(インポートされているモジュールを個別にMLEモジュールとしてインポートし、MLE環境にまとめる)でESモジュールをデータベースにインポートしています。比較のため、この手順についても確認します。

最初に作成したPDFを保存する表PDF_TESTを作成します。Martin Bachさんの元記事では主キー列IDとBLOB列Bがあるだけの表ですが、APEXのアプリケーションからダウンロードして、簡単に出力されたPDFを確認できるように列FILENAMEを追加します。

クイックSQLの以下のモデルより表PDF_TESTを作成します。
# pk: identity
pdf_test
    filename vc80 /nn
    b blob
レビューおよび実行をクリックします。


SQLスクリプトスクリプト・エディタが開きます。スクリプト名pdftestと入力し、実行をクリックします。即時実行するかどうか確認を求めるダイアログが開くので、即時実行をクリックします。


表PDF_TESTが作成されます。アプリケーションの作成をクリックします。デフォルトでは、作成した表のCRUD操作ができる、対話モード・レポートとフォームの画面を含むアプリケーションが作成されます。

アプリケーションの作成対象の表がPDF_TESTであることの確認を求められます。続けて、アプリケーションの作成をクリックします。


アプリケーション作成ウィザードが開きます。名前Sample PDF-LIBに変更し、アプリケーションの作成をクリックします。


アプリケーションが作成されます。表PDF_TESTに保存されているPDFを適切にダウンロードできるように、設定をいくつか変更します。

ページ・デザイナでページ番号の対話モード・レポートのページを開きます。


対話モード・レポートの列Bを選択し、BLOB属性ファイル名列FILENAMEを選択します。以上の変更で保存します。


PDFファイルはJavaScriptのコードを実行して作成するため、フォームからアップロードすることはありません。とはいえ、使わないにしても必要な設定はしておきます。

ページ・デザイナでページ番号3のフォームのページを開きます。ページ・アイテムP3_Bを選択し、ストレージファイル名列FILENAMEを指定します。


以上で表PDF_TESTに保存されたPDFファイルを、手元にダウンロードするAPEXアプリケーションが作成できました。

次に記事「NPMパッケージからOracle Datase 23aiのMLEモジュールを作成する」で作成したMLE Module Managerアプリケーションをインポートします。APEX 24.2でエクスポートしているため、それ以前のAPEXのバージョンでは読み込めません。


アプリケーション・ビルダーからインポートを開きます。


インポートするファイルとして、GitHubよりダウンロードしたmle-module-manager+ords-handler.zipを選択します。

へ進みます。


アプリケーションのインストールを実行します。


以上でjsDelivrからESモジュールを取り出し、MLEモジュールを作成するアプリケーションがインポートできました。

Gistよりutl_mle_npm.sqlを参照しSQLコマンドで実行して、パッケージUTL_MLE_NPMの定義部を作成します。


同様にutl_mle_npm-jsdelivr.sqlを参照しSQLコマンドで実行して、パッケージUTL_MLE_NPMのパッケージ本体を作成します。


APEXのワークスペース・スキーマにMLEでJavaScriptを実行する権限を与えます。Autonomous Databaseであれば管理者ユーザADMIN、Oracle Database 23ai Freeであれば、ユーザーSYSで以下のコマンドを実行します。
grant execute on javascript to [APEXワークスペース名];
grant execute dynamic mle to [APEXワークスペース名];
grant create mle to [APEXワークスペース名];

SQL> grant execute on javascript to wksp_apexdev;


Grantが正常に実行されました。


SQL> grant execute dynamic mle to wksp_apexdev;


Grantが正常に実行されました。


SQL> grant create mle to wksp_apexdev;


Grantが正常に実行されました。


SQL> 


以上でインポートしたアプリケーションMLE Module Managerを実行します。

最初にボタンInitをクリックし、アプリケーションを初期化します。


Module Namepdf-libを入力し、ボタンAdd Es Moduleをクリックします。

jsDelivrからESモジュールとしてpdf-libを取得します。まだMLEモジュールは作成されていません。pdf-libが依存しているモジュールをすべて作成した後に、pdf-libをMLEモジュールとして作成します。


ボタンResolve Onceをクリックし、pdf-libが依存しているESモジュールを解析します。

モジュールpdf-libStatusRESOLVEDに変わり、pdf-libが依存しているモジュールがロードされます。


再度ボタンResolved Onceをクリックし、StatusLOADEDになっているモジュールの依存モジュールを解析します。

pakoLOADEDのまま残っています。


もう一度Resolve Onceをクリックします。pakoStatusRESOLVEDに変わり、pdf-libの依存関係が解決されました。

すべての依存関係が解決される場合は多くはありません。モジュールfsなどMLEでサポートできないモジュールが含まれていると、そのモジュールの代替となるポリフィルが必要になります。


ボタンCreate Mle Modulesをクリックします。StatusRESOLVEDのモジュールから、MLEモジュールを作成します。

正常にMLEモジュールが作成されると、StatusVALIDに変わります。


MLE EnvPDFWRITER_ENVを入力し、ボタンAdd Importsをクリックします。

MLE環境としてPDFWRITER_ENVを作成し、レポートに表示されているMLEモジュールをMLE環境に追加します。


以上でMLEで実行するJavaScriptのコードより、pdf-libを呼び出し可能になりました。

SQLコマンドを開き、ロードしたpdf-libを使ってPDFファイルを作成します。

言語JavaScript(MLE)を選択し、環境としてPDFWRITER_ENVを選択します。

以下のコードを実行します。created by MLE/JavaScriptと記載されたPDFが表PDF_TESTに、ファイル名pdflib.pdfとして保存されます。
const { PDFDocument, StandardFonts, rgb } = await import('/npm/pdf-lib@1.17.1/+esm');

const pdfDoc = await PDFDocument.create();
const timesRoman = await pdfDoc.embedFont(StandardFonts.TimesRoman);
     
const page = pdfDoc.addPage();
const { _, height } = page.getSize();
const fontSize = 30;
     
page.drawText("created by MLE/JavaScript", {
    x: 50,
    y: height - 4 * fontSize,
    size: 30,
    font: timesRoman,
    color: rgb(0, 0.53, 0.71),
});
     
const pdfBytes = await pdfDoc.save();

session.execute("insert into pdf_test (filename, b) values ('pdflib.pdf', :pdf)", [pdfBytes]);

先に作成したAPEXアプリケーションSample PDF-LIBより、pdflib.pdfをダウンロードして内容を確認します。


PDFの内容として以下が確認できます。pdf-libは適切に呼び出せています。


次にMartin Bachさんの記事「Using PDF-LIB to generate PDFs in the database」で紹介されている手順を実施します。記事の最初は、単純にpdf-libのESモジュールからMLEモジュールを作成するだけでは依存関係が解決されないため、ORA-4161が発生することを説明しています。今までの作業では、ESモジュールのソース・コードを読んでESモジュールの依存関係を解決しています。

この記事では、pdf-libが依存しているライブラリをesbuildの実行により解決しています。

最初に作業ディレクトリとしてpdflib-testを作成します。

mkdir pdflib-test
cd pdflib-test

% mkdir pdflib-test

% cd pdflib-test

pdflib-test % 


pdf-libesbuildをインストールします。

npm install --save pdf-lib
npm install --save-exact --save-dev esbuild


pdflib-test % npm install --save pdf-lib


added 5 packages in 232ms

pdflib-test % npm install --save-exact --save-dev esbuild


added 2 packages, and audited 8 packages in 393ms


found 0 vulnerabilities

pdflib-test % 


フォルダpdflib-testにフォルダsrcを作成し、ファイルpdfWriter.jsを作成します。

作成したファイルに以下の内容を記述します。元の記事で紹介されているpdfWriter.jsとほぼ同じですが、表PDF_TESTの列FILENAMEにesbuild.pdfと記入するように変更しています。

VS Codeの使用を想定しています。
import { PDFDocument, StandardFonts, rgb } from "pdf-lib";
 
/**
 * Creates a PDF document using JavaScript and saves it as a byte array before
 * persisting it into a table. Relies on the "pdf-lib" library to do so.
 * 
 * A faithful representation of https://pdf-lib.js.org/#create-document
 *
 * @async
 * @returns {Promise<void>} A promise that resolves when the PDF document has been created and saved.
 */
export async function createPDFInJavaScript() {
    const pdfDoc = await PDFDocument.create();
    const timesRoman = await pdfDoc.embedFont(StandardFonts.TimesRoman);
     
    const page = pdfDoc.addPage();
    const { _, height } = page.getSize();
    const fontSize = 30;
     
    page.drawText("created by MLE/JavaScript", {
        x: 50,
        y: height - 4 * fontSize,
        size: 30,
        font: timesRoman,
        color: rgb(0, 0.53, 0.71),
    });
     
    const pdfBytes = await pdfDoc.save();
     
    // now persist the PDF as a BLOB in a table - see below for the
    // table DDL statement
    session.execute("insert into pdf_test(filenanme, b) values ('esbuild.pdf', :pdf)", [pdfBytes]);
}

ビルドします。

mkdir dist
npx esbuild src/pdfWriter.js --bundle --outfile=dist/bundle.js --format=esm

pdflib-test % mkdir dist

pdflib-test % npx esbuild src/pdfWriter.js --bundle --outfile=dist/bundle.js --format=esm


  dist/bundle.js  801.0kb


Done in 12ms

pdflib-test %


SQL Developer Extension for VS Codeを開きます。

APEXのパーシング・スキーマへの接続を選択し、SQLclを開きます。


ターミナルに表示されるSQLclのプロンプトにて、フォルダpdflib-testに移動して、MLEモジュールを作成するコマンドを実行します。dist/bundle.jsからMLEモジュールpdfwriter_moduleが作成されます。

cd [フォルダpdflib-testへのパス/pdflib-test
mle create-module -filename dist/bundle.js -module-name pdfwriter_module -replace


選択しているAPEXのパーシング・スキーマへの接続の中に、JavaScriptモジュールのノードが含まれます(SQL Developer Extension for VS Code 25.1の新機能です)。その中にPDFWRITER_MODULEが含まれます。

内容はdist/bundle.jsなので、読んだり編集したりできるようなコードではありません。


作成したMLEモジュールPDFWRITER_MODULEを、すでに作成されているMLE環境PDFWRITER_ENVに含めます。

MLE環境PDFWRITER_ENV上で右クリックしてコンテキスト・メニューを表示させ、Add Imports...を実行します。


Module Name"スキーマ名"."PDFWRITER_MODULE"を選択します。元記事に合わせてImport Namepdflibとして、適用をクリックします。


MLE環境PDFWRITER_ENVにインポート名pdflibとしてMLEモジュールPDFWRITER_MODULEが追加されました。


それでは、MLEモジュールPDFWRITER_MODULEに含まれるファンクションcreatePDFInJavaScriptを呼び出してみます。

SQLコマンドを開き、言語JavaScript(MLE)を選択し、環境としてPDFWRITER_ENVを選択します。

以下のコードを実行します。
const { createPDFInJavaScript } = await import('pdflib');
await createPDFInJavaScript();

APEXアプリケーションSample PDF-LIBから、PDFファイルとしてesbuild.pdfが作成されていることを確認できます。


最後にSQLclのmleコマンドを使ってバンドル化してみます。参照した記事は「SQLcl introduces support for Rollup.js to load MLE modules」です。

フォルダは新規にpdflib-test2を作成します。

mkdir pdflib-test2
cd pdflib-test2

% mkdir pdflib-test2

% cd pdflib-test2

pdflib-test2 % 


pdf-librollup@rollup/plugin-node-resolve@rollup/plugin-commonjsをインストールします。

npm install --save pdf-lib
npm install --save-exact --save-dev rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs


pdflib-test2 % npm install --save pdf-lib


added 5 packages in 493ms

pdflib-test2 % npm install --save-exact --save-dev rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs


added 22 packages, and audited 28 packages in 2s


5 packages are looking for funding

  run `npm fund` for details


found 0 vulnerabilities

pdflib-test2 % 


Martin Bachさんの記事ではRollup Issue 1645のため、現時点でのRollupは不可となっています。Circular dependenciesが発生するのはモジュール@pdf-lib/standard-fontsに関連しているようなので、rollup.config.mjsexternalに含めます。

import { nodeResolve } from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";

export default {
    input: 'src/pdfWriter.js',
    output: {
        format: 'esm'
    },
    plugins: [nodeResolve(), commonjs()],
    external: ['mle-js-oracledb', 'oracledb','@pdf-lib/standard-fonts'],
};
ファイルrollup.config.mjsはフォルダpdflib-test2の直下に配置します。


esbuildの実行との違いが分かるように、pdfWriter.jsに含まれているファイル名をesbuild.pdfからrollup.pdfに変更します。フォルダsrcを作成し、その下にpdfWriter.jsを作成します。


APEXのワークスペース・スキーマへの接続でSQLclを開き、pdflib-test2へ移動して以下のコマンドを実行します。SQLclのmleコマンドでバンドル化を実行した上でMLEモジュールPDFWRITER_MODULE2を作成します。

mle create-module -bundler rollup -bundler-config rollup.config.mjs -filename src/pdfWriter.js -module-name PDFWRITER_MODULE2 -replace


JavaScriptモジュールとしてPDFWRITER_MODULE2が作成されていることを確認します。


先ほどと同様の手順でMLE環境PDFWRITER_ENVに今回作成したMLEモジュールPDFWRITER_MODULE2を含めます。Import Namepdflib2とします。


Circular dependenciesの発生を回避するためにexternalに設定したモジュールを@pdf-lib/standard-fontsとしてインポートできるようにします。

Module Name"ESM__PDF_LIB_STANDARD_FONTS_1_0_0"を選択し、Import Name@pdf-lib/standard-fontsとして適用します。


MLE環境PDFWRITER_ENVImport Namepdflib2@pdf-lib/standard-fontsが追加されていることを確認します。


MLEモジュールPDFWRITER_MODULE2に含まれるファンクションcreatePDFInJavaScriptを呼び出してみます。

SQLコマンドを開き、言語JavaScript(MLE)を選択し、環境としてPDFWRITER_ENVを選択します。

以下のコードを実行します。
const { createPDFInJavaScript } = await import('pdflib2');
await createPDFInJavaScript();

APEXアプリケーションSample PDF-LIBから、PDFファイルとしてrollup.pdfが作成されていることを確認できます。


折角なので、APEXアプリケーションから上記の3種類のテスト・コードを呼び出せるようにしてみます。

APEXアプリケーションからMLEモジュールを呼び出すために、アプリケーション定義セキュリティを開き、データベース・セッションのセクションに含まれるMLE環境PDFWRITER_ENVを設定します。

変更の適用をクリックします。


対話モード・レポートのページに、外観テンプレートButtons Containerを選択した静的コンテンツのリージョンを作成します。そのリージョンにボタンNO_BUNDLERESBUILDROLLUPを作成します。動作アクションページの送信です。


ボタンNO_BUNDLERをクリックしたときに実行されるプロセスとしてNO_BUNDLERを作成します。

タイプコードを実行ソース言語JavaScript(MLE)を選択します。JavaScriptコードは動作確認の際にSQLコマンドで実行したコードをそのまま記述します。

サーバー側の条件ボタン押下時NO_BUNDLERを指定します。


同様にボタンESBUILDをクリックしたときに実行されるプロセスとしてESBUILDを作成します。


ボタンROLLUPをクリックしたときに実行されるプロセスとしてROLLUPを作成します。


今回の記事は以上になります。

今回作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/sample-pdf-lib.zip

Oracle APEXのアプリケーション作成の参考になれば幸いです。