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 with Oracle Database 23ai. With the introduction of MLE in Oracle Database 23ai you have 3 languages available to store business logic alongside the data:

  • PL/SQL
  • Java
  • JavaScript

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.

In this short post I’d like to draw your attention to a feature I haven’t seen before: runOracleScript.

Please note that this post was written using Oracle Database 23ai Free (23.4) and SQLcl (version 24.2). Things may change in the future, always make sure to check the documentation!

Demo

Using the runOracleScript changeset-type allows me to execute anything that SQLcl would execute, which is exactly what I need for MLE modules. The standard Liquibase driver doesn’t yet “know” about MLE modules and environments. Here is a short example how this works. Initially I don’t have any Liquibase deployments in my schema:

SQL> lb history
--Starting Liquibase at 2024-08-15T10:33:52.488207604 (version 4.25.0 #3966 built at 2023-11-10 23:07:29 UTC)
Liquibase History for jdbc:oracle:thin:@localhost:1522/freepdb1
No changesets deployed



Operation completed successfully.

Let’s change that. First of all I need a changelog to drive the schema migration. Think of a changelog as a release. The changelog contains changesets, or things to do. There are multiple formats available like SQL, XML, YAML, etc. to define changelogs and changesets. I am using the XML format in this article.

This particular changelog is rather simple, as it refers to a single changeset, but it should hopefully be sufficient to bring the point home:

<?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.

This looks promising, let’s see if the module was created (I’m pretty sure it was…)

SQL> select
2 module_name
3 from
4* user_mle_modules;

MODULE_NAME
______________
DEMO_MODULE

The module is present, and it contains the code specified in the Liquibase changeset

SQL> select
2 replace(text, chr(10), '') text_wo_nl
3 from
4 user_source
5 where
6* name = 'DEMO_MODULE';

TEXT_WO_NL
_______________________________________________________________
/**
* 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}`;
}

8 rows selected.

Liquibase knows about it, too:

SQL> lb history
--Starting Liquibase at 2024-08-15T11:25:23.315218209 (version 4.25.0 #3966 built at 2023-11-10 23:07:29 UTC)
Liquibase History for jdbc:oracle:thin:@localhost:1522/freepdb1
- Database updated at 8/15/24, 9:21 AM. Applied 1 changeset(s), DeploymentId: 3713692836
example-module.xml::mcb001::martincarstenbach




Operation completed successfully.

Test

Let’s run a quick test, too. I added the following changeset to the changelog:

<?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-latest.xsd">
	<changeSet
		id="mcb002" 
		author="martincarstenbach"
		failOnError="true"
		runOnChange="false"
		runAlways="false">
			<n0:createOracleFunction objectName="HELLO" objectType="FUNCTION" ownerName="EMILY"  replaceIfExists="false" >
				<n0:source><![CDATA[
create function if not exists hello(
	p_who varchar2
)
return varchar2 as
	mle module demo_module
	signature 'hello';
/
]]>
				</n0:source>
			</n0:createOracleFunction>
	</changeSet>
</databaseChangeLog>

And voila, it works, too:

SQL> lb update -changelog-file controller.xml -log
--Starting Liquibase at 2024-08-15T11:27:35.449049342 (version 4.25.0 #3966 built at 2023-11-10 23:07:29 UTC)
Running Changeset: hello-function.xml::mcb002::martincarstenbach

Function HELLO compiled



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

Liquibase: Update has been successful. Rows affected: 1

Produced logfile: sqlcl-lb-1723714055448.log

Operation completed successfully.

SQL> select hello('mle')
2* /

HELLO('MLE')
_______________
hello, mle

A big shout out to Jeff Smith for his help. Read more about runOracleScript on his blog.

Happy automating!