WebAssembly support is available with Oracle AI Database 26ai! The latest Release Update (RU), 23.26.4, adds another language to Oracle Database’s polyglot runtime, Multilingual Engine (MLE).
As of today, MLE supports the following languages:
- JavaScript
- Typescript (must be transpiled before loaded into the database)
- WebAssembly (Wasm)
The official announcement post promised examples, and this is the first of many to follow. It features photon, a popular image processing library available in WebAssembly format. This is interesting, because photon is written in Rust 😱
Before you read on, please note: this is not an officially sanctioned way of resizing photos by Oracle, it’s my personal playground where I experiment with what I think is cool technology.
In the following example a select few photon features will be exposed as a REST API, powered by Oracle REST Data Services (ORDS). Autonomous AI Database 26ai provides the storage and processing logic. The idea is to allow (OAuth2) authenticated users to upload photos into the database in any of the formats supported by photon. Once uploaded, users can use photon’s API to get information about the image. Finally, a REST call shrinks the photo’s size by 50 percent.
Also please note that the code serves as an example for using photon with Oracle AI Database 26ai, it’s not complete and neither does it aim to be. Feel free to use it as inspiration! Some checks to enforce a maximum file size of 10MiB have been added, likewise a 16 mega pixel format should not be exceeded, but these limits aren’t enforced in every place. If used in production you must ensure users cannot submit malicious content!
You can find the complete example, end-to-end, on GitHub.
Architecture
The sample application consists of an Oracle AI Autonomous Database and its corresponding ORDS instance. A user needs to authenticate (via a bearer token) first, before any of the endpoints can be invoked.
ORDS offers the following 3 endpoints:
/uploads/:filenameto upload a new photo/photon/photos/:id/resizeto resize an existing image/photon/photos/:idto get meta-information about the photo
The example restricts declared MIME types both in JavaScript and ORDS handlers, and rejects encoded files larger than 10 MiB. Upload and resize check the 16-megapixel limit after decoding; the details function currently has no pixel limit. These are preliminary safeguards; validation before decoding and tighter resource controls remain production-readiness to-do items.
Let’s look at the implementation in more detail.
Creating the Application
Before any of these endpoints can be implemented in ORDS, the photon Wasm library and corresponding JavaScript bindings must be loaded into the database. In this example they have been staged into a temporary staging table (wasm_modules) and can be created using a slight variation of the existing create mle module command:
-- load the JavaScript bindingscreate or replace mle module photon language javascriptusing blob select module_blob from wasm_modules where module_name = 'photon_rs.js' and module_version = '0.3.3';/-- followed by the actual Web Assembly modulecreate or replace mle module photon_wasmlanguage wasmusing blob select module_blob from wasm_modules where module_name = 'photon_rs_bg.wasm' and module_version = '0.3.3';/
The JavaScript bindings (photon module) expose the callable interface, while the Wasm module (photon_wasm) contains the compiled library.
With the modules created, you can use the bindings in your own code. The photon utilities module defines the following JavaScript functions (shown here without the create mle module DDL command for better syntax highlighting):
import source photonWasm from "photon_wasm";import * as photon from "photon";// arbitrary limits concerning the photo size and dimensions// they are NOT enforced strictly enough!const maxBytes = 10 * 1024 * 1024; // 10 MiB = 10,485,760 bytesconst maxPixels = 16000000; // 16 megapixels// .../** * Looks up a photo by primary key and returns and logs its decoded statistics. * Statistics describe Photon's RGBA8 pixels, not the source encoding's color * depth or metadata. Releases the decoded WASM image before returning. * * @param {string} id - uploaded_files ID represented as a string that converts * to a positive JavaScript safe integer. ORDS provides variables as strings! * @returns {PhotoDetails} Decoded dimensions, sizes, color and pixel statistics. * @throws {Error} If the ID is invalid, the row is missing, the BLOB is empty, * or database access, validation or decoding fails. */export function photoDetails(id) { if (!id || typeof id !== 'string') { throw new Error('photoDetails() requires the ID to be a string'); } // A safe integer avoids precision loss when binding the JavaScript number. const numericId = Number(id.trim()); if (!Number.isSafeInteger(numericId) || numericId <= 0) { throw new Error('you must provide an ID greater than 0 to photoDetails()'); } const result = session.execute( "select file_content from uploaded_files where id = :id", { id: { val: numericId, dir: oracledb.BIND_IN, type: oracledb.NUMBER } } ); if (result.rows.length === 0) { throw new Error(`No photo found for ID ${id}`); } const photoBlob = result.rows[0].FILE_CONTENT; if (photoBlob === null || photoBlob === undefined) { throw new Error(`The photo BLOB with id ${id} is empty`); } // TODO: check photoBlob.length() before copying the BLOB into JavaScript. const bytes = photoBlob.getData(); if (bytes.length === 0) { throw new Error(`The photo BLOB with id ${id} is empty`); } // TODO: size and filename are not defined in this function. This currently // throws ReferenceError for a nonempty BLOB. Use the BLOB length and id, // and move the size check above getData() to enforce it before the copy. if (size > maxBytes) { throw new Error(`The uploaded file ${filename} exceeds the 10 MiB size limit`); } // this is where the WebAssembly module is initialised. The following JavaScript // code calls the JS bindings, not Wasm directly. You can find the documentation // here: https://docs.rs/photon-rs/latest/photon_rs/index.html photon.initSync({ module: photonWasm }); const image = photon.PhotonImage.new_from_byteslice(bytes); // TODO: enforce the 16 megapixel limit before decoding, with a second // check afterward before copying the decoded pixel array. try { const width = image.get_width(); const height = image.get_height(); // Copies RGBA8 pixels out of WASM; source bit depth is not retained. const pixels = image.get_raw_pixels(); const pixelCount = width * height; let hasTransparency = false; let isGrayscale = true; let red = 0; let green = 0; let blue = 0; // Each pixel occupies four consecutive bytes: red, green, blue, alpha. for (let i = 0; i < pixels.length; i += 4) { red += pixels[i]; green += pixels[i + 1]; blue += pixels[i + 2]; if (pixels[i + 3] < 255) hasTransparency = true; if (pixels[i] !== pixels[i + 1] || pixels[i] !== pixels[i + 2]) { isGrayscale = false; } } const details = { width, height, aspect_ratio: width / height, orientation: width === height ? "square" : width > height ? "landscape" : "portrait", pixel_count: pixelCount, megapixels: pixelCount / 1000000, file_size_bytes: bytes.length, decoded_size_bytes: pixels.length, color_model: "RGBA", channels: 4, bits_per_channel: 8, bits_per_pixel: 32, has_transparency: hasTransparency, is_grayscale: isGrayscale, // Arithmetic mean of all decoded pixels, including transparent ones. average_rgb: { red: red / pixelCount, green: green / pixelCount, blue: blue / pixelCount } }; // Emits derived image statistics to the host's JavaScript log output. console.log(JSON.stringify(details, null, 2)); return details; } finally { image.free(); }};// ...
The JavaScript code in photo_details accepts a string (variables in ORDS handlers are strings and this code will later be used in APEX where page items are strings, too), which is then converted to an integer used for the database lookup operation. Once the photo is read, several checks are performed as safeguards, before photon is initialised in line 81. The photon project publishes the API calls in their documentation in case you are curious.
You can find the details about these operations on GitHub. If you want to run a quick test, create a PL/SQL wrapper and execute it:
select id, filename, content_typefrom uploaded_files; ID FILENAME CONTENT_TYPE_____ ______________________ _______________ 1 martin.jpg image/jpeg 2 martin.jpg image/jpeg 3 photo_1_resized.png image/pngdesc photon_apiFUNCTION PHOTO_DETAILS RETURNS JSON Argument Name Type In/Out Default? ------------------------- -------------------- ------ -------- ID VARCHAR2 INFUNCTION RESIZE_PHOTO RETURNS NUMBER Argument Name Type In/Out Default? ------------------------- -------------------- ------ -------- ID VARCHAR2 INPROCEDURE SAVE_UPLOADED_FILE Argument Name Type In/Out Default? ------------------------- -------------------- ------ -------- p_filename VARCHAR2 IN p_content_type VARCHAR2 IN p_content BLOB INselect json_serialize( photon_api.photo_details('1') pretty ) as photo_details;PHOTO_DETAILS ________________________________________ { "average_rgb" : { "red" : 95.15023254470758, "green" : 95.15023254470758, "blue" : 95.15023254470758 }, "bits_per_pixel" : 32, "width" : 2151, "orientation" : "landscape", "file_size_bytes" : 985344, "bits_per_channel" : 8, "height" : 1964, "channels" : 4, "pixel_count" : 4224564, "is_grayscale" : true, "color_model" : "RGBA", "decoded_size_bytes" : 16898256, "aspect_ratio" : 1.095213849287169, "has_transparency" : false, "megapixels" : 4.224564 }
Summary
Oracle AI Database 26ai introduces WebAssembly support in Multilingual Engine (MLE), enabling suitable libraries written in languages such as Rust in this example to run inside the database. This little proof of concept uses photon to inspect and resize images stored as BLOBs, exposing the functionality through PL/SQL and OAuth-protected ORDS endpoints. It demonstrates loading JavaScript bindings and Wasm modules, retrieving decoded image statistics, and storing resized copies, while documenting validation and resource-control work needed before production use.