Create MLE/JavaScript modules using Liquibase

Multilingual Engine, or MLE for short, allows developers to use the hugely popular JavaScript language to write server-side code since Oracle Database 23ai has been released for Linux/Intel and Linux/Arm. Oracle AI Database 26ai continues to enhance MLE functionality.

With the introduction of MLE/JavaScript in Oracle AI Database, you have 3 languages available to store business logic alongside the data:

  • PL/SQL
  • Java
  • JavaScript

Writing database lambdas has many benefits over a traditional approach.

Background and Motivation

Backend developers frequently use tools to deploy their application changes, such as Liquibase, Flyway, and others. Liquibase is an open-source database-independent library for tracking, managing and applying database schema changes, and it has great support in the form of SQLcl. SQLcl is a lightweight tool allowing you to interact with the Oracle database without having to install a client, amongst many other things. I use the SQLcl/Liquibase combination for any database schema migration tasks I might have. The official documentation has all the details on Liquibase support.

Updates

Since this article was written a couple of years ago, lots of things have changed.

  • SQLcl can now deploy changelogs in SQL format directly, without the need of runOracleScript
  • SQLcl Projects provides an opinionated framework for deploying changes to your schema, Oracle REST Data Service (ORDS), and APEX. In a nutshell it can be used for all your Database Application CI/CD needs
  • runOracleScript remains available but you probably don’t need it in 2026

Each of these is discussed in this post.

Personal opinion and recommendation

Automated schema migration is almost always required, especially for Continuous Integration/Delivery (CI/CD). You can of course hand-craft your releases using a hierarchy of changelogs per release, but it is probably easier to let a tool handle it all. SQLcl projects might be the easiest way towards database CI/CD, but it requires you to strictly follow process. And remember: once Liquibase, always Liquibase. You can’t make schema changes without Liquibase or else you’ll experience a world of pain trying to consolidate the database’s state with Liquibase state.

Implementation

The following sections detail the three options at your disposal for creating MLE/JavaScript modules in the database.

Vanilla Liquibase Changelog

If all you want to do is deploy a MLE module using Liquibase in SQLcl, you can use the SQL Format to do so. The most basic example is shown here:

--liquibase formatted sql
--changeset mcb.hello:1
create or replace mle module hello_module
language javascript as
/**
* Returns a greeting for a validated human-readable name.
*
* @param {string} who A 1-100 character name containing letters, spaces,
* apostrophes, or hyphens.
* @returns {string} The greeting.
* @throws {TypeError} If {@link who} is not a valid name.
*/
export function hello(who) {
// Accept only a primitive, human-readable name. Do not coerce objects:
// coercion can invoke an attacker-controlled toString() implementation.
if (typeof who !== 'string') {
throw new TypeError('who must be a string');
}
if (
who.length === 0 ||
who.length > 100 ||
!/^[\p{L}\p{M}]+(?:[ '\u2019-][\p{L}\p{M}]+)*$/u.test(who)
) {
throw new TypeError(
'who must be a name of 1-100 letters, with optional spaces, apostrophes, or hyphens'
);
}
return `hello, ${who}`;
}

Changesets like the one above are typically grouped into a changelog (per release, sprint, …). A typical example is shown here:

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<include file="hello-module.sql"/>
<include file="hello-function.sql"/>
</databaseChangeLog>

As you can see, this changelog is made up of 2 changesets, the second one contains a function exposing the JavaScript code to SQL and PL/SQL.

--liquibase formatted sql
--changeset mcb.hello:2
create or replace function hello(p_who varchar2)
return varchar2 as
mle module hello_module
signature 'hello';
/

You can run this in modern SQLcl, and Liquibase will deploy your module faithfully:

SQL> lb update -chf test.xml
--Starting Liquibase at 2026-08-04T16:47:56.345662 using Java 21.0.12 (version 4.33.0 #0 built at 2025-12-09 17:47+0000)
Running Changeset: hello-module.sql::1::mcb.hello
MLE module HELLO_MODULE compiled
UPDATE SUMMARY
Run: 2
Previously run: 0
Filtered out: 0
-------------------------------
Total change sets: 2
Liquibase: Update has been successful. Rows affected: 0
Operation completed successfully.

And that’s typically all that is to it. A quick test reveals that everything works as expected:

SQL> select hello('world');
HELLO('WORLD')
_________________
hello, world

Deploying MLE modules and environments via SQLcl projects

I have written quite a few posts about SQLcl projects for Database CI/CD, please have a look at this blog and the documentation for more details.

In a nutshell SQLcl projects workflow follows the following pattern:

  1. You use a clone of production on your local laptop or in the cloud. For most of us, that means a redacted, downsized copy unless you are on Exascale where thin clones of TB-sized databases created in seconds are a reality
  2. You create a new branch for the ticket you’re working on, and start making changes to the schema
  3. Once you’re happy the task described in the ticket is complete, you export the database schema to disk
  4. SQLcl then creates a set of diff scripts to apply your changes to production and commits these into git
  5. You then run the full CI pipeline, and if everything is green, you can generate a build artifact and deploy it

The entire process is file based, with git as the source of truth. Rather than generating changelogs per spring/release/name your iteration here, you let SQLcl projects generate the changelogs. This entire process is much less error-prone than the manual example shown earlier, but it requires stronger team discipline.

The long-and-short of it is this: should you decide to use SQLcl projects, changelogs and changeset are created and maintained for you, but otherwise the same concept as shown earlier still applies: changelogs implement changesets, and these are maintained in SQL format.

RunOracleScript Demo

You probably don’t need to use runOracleScript changelogs in present day, but this post started out with it so I’ll leave the content here, but there are updates.

Using the runOracleScript changeset-type allows you to execute anything that SQLcl would execute interactively. Which, as I said earlier, is no longer needed.

At the time of writing the standard Liquibase driver didn’t “know” about MLE modules and environments. In the meantime, it has caught up – as demonstrated earlier in this article. Let’s have a look at what a changelog featuring runOracleScript might have looked like.

It all starts with a changelog referring to the changeset, just as in the above example:

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<include file="example-module.xml"/>
</databaseChangeLog>

The contents of example-module.xml is an adaption of the runOracleScript example shown in the SQLcl documentation. The XML CDATA section contains the code I want SQLcl/Liquibase to execute. Note the runOracleScript tag in line 15:

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:n0="http://www.oracle.com/xml/ns/dbchangelog-ext"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.6.xsd">
<changeSet
id="mcb001"
author="martincarstenbach"
failOnError="true"
runOnChange="false"
runAlways="false">
<n0:runOracleScript objectName="example-module" ownerName="EMILY" sourceType="STRING">
<n0:source><![CDATA[
-- it is possible to add directives to the script
set define off
set verify off
-- and comments
create mle module if not exists demo_module language javascript as
/**
* A simple function returning a string with a nice greeting
* @param {string} who who do you want to greet?
* @returns {string} the completed greeting
*/
export function hello(who) {
return `hello, ${who}`;
}
/
]]>
</n0:source>
</n0:runOracleScript>
</changeSet>
</databaseChangeLog>

With the changelog and a changest in place, it’s time to perform the database migration:

SQL> lb update -changelog-file controller.xml -log
--Starting Liquibase at 2024-08-15T11:21:32.274730132 (version 4.25.0 #3966 built at 2023-11-10 23:07:29 UTC)
Running Changeset: example-module.xml::mcb001::martincarstenbach

MLE module DEMO_MODULE compiled



UPDATE SUMMARY
Run: 1
Previously run: 0
Filtered out: 0
-------------------------------
Total change sets: 1

Liquibase: Update has been successful. Rows affected: 1

Produced logfile: sqlcl-lb-1723713692274.log

Operation completed successfully.

Happy scripting!