Working with User-Defined Data Types
A user-defined type can be defined in the following ways:
- In a PL/SQL block. In this case, the user-defined type is a local type and is only available to be referenced in the block. It is stored in the database only if the PL/SQL block is in a standalone or package subprogram.
- In a package specification. Here, the user-defined type is a public item that can be referenced outside of the package by qualifying it with the package name.
- At schema level. In this case, the user-defined type is a standalone
type that is created using the
CREATE TYPEstatement. It is stored in the database until you drop it with theDROP TYPEstatement. Note that you cannot create aRECORDtype at schema level.
Local and standalone types, as well as public items, are supported for use with JavaScript.
Note:
It is not possible to invoke member functions of objects with MLE. If attempted, the action will fail withORA-04161: Methods invocations are not allowed.
- Using Record Data Types in JavaScript
Record Data Types can be used, for example, as parameters in JavaScript functions and can be inserted into the database. - Using Collections in JavaScript
Oracle AI Database supports several collection types, including Associative Arrays (or index-by tables), Variable-Size Arrays (orVARRAY), and nested tables. These collection types are supported with in-database JavaScript.
Using Record Data Types in JavaScript
Use Record Data Types as Function Arguments
A typical use case for records is as data being passed into a function. Consider the following example in which a local record is declared in a PL/SQL package:
CREATE OR REPLACE PACKAGE rec_mle_js AS
TYPE person_t IS RECORD (
surname VARCHAR2(100),
firstname VARCHAR2(100),
street VARCHAR2(100),
city_name VARCHAR2(50),
country_name VARCHAR2(50)
);
END rec_mle_js;
/
This record can be used to describe a person's address. A function within an MLE module can be used to persist the address record in the database. The data model is normalized; there are separate tables for countries, cities, and the actual address record. A potential implementation serializing the record in JavaScript could look like the following:
CREATE OR REPLACE MLE MODULE rec_module
LANGUAGE JAVASCRIPT AS
export function insertPerson(personRec) {
// Validate input: personRec must be a non-null object
if (
personRec === null ||
personRec === undefined ||
typeof personRec !== "object" ||
Array.isArray(personRec)
) {
throw new TypeError('insertPerson: "personRec" must be a non-null object');
}
// validate if the country exists
let result = session.execute(
"select country_id from country where country_name = :country_name",
[personRec.COUNTRY_NAME],
);
// insert the country if it does not exist
if (result.rows.length === 0) {
session.execute(
"insert into country (country_name) values (:country_name)",
[personRec.COUNTRY_NAME],
);
}
// validate if the city exists
result = session.execute(
"select city_id from city where city_name = :city_name",
[personRec.CITY_NAME],
);
// insert the city if it does not exist
let city_id;
if (result.rows.length === 0) {
result = session.execute(
`insert into city (city_name) values (:city_name)
returning city_id into :city_id`,
{
city_name: {
type: oracledb.STRING,
dir: oracledb.BIND_IN,
val: personRec.CITY_NAME,
},
city_id: {
type: oracledb.NUMBER,
dir: oracledb.BIND_OUT,
},
},
);
city_id = result.outBinds.city_id[0];
} else {
city_id = result.rows[0].CITY_ID;
}
// insert into the address table
result = session.execute(
`insert into address set
surname = :surname,
firstname = :firstname,
street = :street,
city_id = :city_id`,
{
surname: { val: personRec.SURNAME },
firstname: { val: personRec.FIRSTNAME },
street: { val: personRec.STREET },
city_id: { val: city_id },
},
);
return result.rowsAffected === 1;
}
/
The only function included in the module rec_module,
insertPerson(), deconstructs the data in the record and uses it
to populate the various tables.
If you want to expose this function in SQL and PL/SQL, you can simply extend the package definition as follows:
CREATE OR REPLACE PACKAGE rec_mle_js as
TYPE person_t IS RECORD (
surname VARCHAR2(100),
firstname VARCHAR2(100),
street VARCHAR2(100),
city_name VARCHAR2(50),
country_name VARCHAR2(50)
);
FUNCTION insert_person(p_person person_t)
RETURN BOOLEAN
AS MLE MODULE rec_module
SIGNATURE 'insertPerson(object)';
END rec_mle_js;
/
In some cases, you may choose to implement nested tables based on records, which is another option to persist the data. For more information about using nested tables with in-database JavaScript, see Using Collections in JavaScript.
Return a Record from a JavaScript Function
In addition to receiving records as parameters into your function, you can return records as well.
export function getPerson(id) {
// Validate input: id must be a provided positive integer
if (id === undefined || id === null) {
throw new TypeError('getPerson: "id" is required');
}
if (typeof id !== "number" || !Number.isInteger(id) || id <= 0) {
throw new TypeError('getPerson: "id" must be a positive integer');
}
// fetch the person's details from the database based on the ID
// provided as input to the function
const result = session.execute(
`select
a.address_id,
a.firstname,
a.surname,
a.street,
ci.city_name,
co.country_name
from
address a
left join city ci on (a.city_id = ci.city_id)
left join country co on (ci.country_id = co.country_id)
where
a.address_id = :address_id`,
[id],
);
// raise an error if the fetch returned no rows
if (result.rows.length !== 1) {
throw new Error(`getPerson: cannot find a person with ID ${id}`);
}
// a call to session.getDbObjectClass() returns a DbObject prototype object,
// representing the database type
const person_t = session.getDbObjectClass("REC_MLE_JS.PERSON_T");
// you can inspect it if you like
// console.log(JSON.stringify(person_t.prototype));
// Now that the object prototype has been found, an object can be created by passing a
// JavaScript object to the constructor. Note that the keys are ALL IN UPPERCASE
const person = new person_t({
"SURNAME": result.rows[0].SURNAME,
"FIRSTNAME": result.rows[0].FIRSTNAME,
"STREET": result.rows[0].STREET,
"CITY_NAME": result.rows[0].CITY_NAME,
"COUNTRY_NAME": result.rows[0].COUNTRY_NAME,
});
return person;
}
Use Records Returned by PL/SQL
In addition to function parameters and JavaScript function return types, records that are returned by PL/SQL can also be used in the context of in-database JavaScript.
CREATE OR REPLACE FUNCTION get_person_name("id" NUMBER)
RETURN VARCHAR2
AS MLE LANGUAGE JAVASCRIPT
{{
// assume for the sake of this example that get_person() is implemented
// as a public function in REC_MLE_PKG. It takes an ID as the search
// parameter and returns a person record as defined earlier
const result = session.execute(
'begin :person := rec_mle_pkg.get_person(:id); end;',
{
person: {
type: "REC_MLE_PKG.PERSON_T",
dir: oracledb.BIND_OUT
},
id: {
type: oracledb.NUMBER,
val: id,
dir: oracledb.BIND_IN
}
}
);
return `${result.outBinds.person.FIRSTNAME} ${result.outBinds.person.SURNAME}`;
}};
/
It is also possible to use the Foreign Function Interface (FFI), rather than a PL/SQL anonymous block:
CREATE OR REPLACE FUNCTION get_person_name_ffi("id" NUMBER)
RETURN VARCHAR2
AS MLE LANGUAGE JAVASCRIPT
{{
// assume for the sake of this example that get_person() is implemented
// as a public function in REC_MLE_PKG. It takes an ID as the search
// parameter and returns a person record as defined earlier
// this example uses the Foreign Function Interface (FFI) but is otherwise
// identical to the previous one
const rec_mle_pkg = plsffi.resolvePackage('REC_MLE_PKG');
const person = rec_mle_pkg.get_person(id);
return `${person.FIRSTNAME} ${person.SURNAME}`;
}};
/
Using FFI, as in this case, can result in more concise and easily understood code. For more information about using FFI, see Introduction to the PL/SQL Foreign Function Interface.
Parent topic: Working with User-Defined Data Types
Using Collections in JavaScript
VARRAY), and nested tables. These collection types are supported
with in-database JavaScript.
The in-database JavaScript implementation of collection support closely aligns with that
of node-oracledb. For more information about the
node-oracledb implementation, see Node-oracledb Documentation.
Pass an Associative Array to a PL/SQL Function in JavaScript
A common use case involving collections is to provide them to PL/SQL APIs.
Consider an example in which a PL/SQL function accepts an associative array and returns the sum of its elements. While you can bind associative arrays using named types, as shown in examples from Using Record Data Types in JavaScript, it is more efficient to use the method implemented in the following examples. Here, the type of each element is used, rather than the name of the associative array type.
oracledb.STRINGoracledb.DB_TYPE_VARCHARoracledb.NUMBERoracledb.DB_TYPE_NUMBERoracledb.DB_TYPE_NVARCHARoracledb.DB_TYPE_CHARoracledb.DB_TYPE_NCHARoracledb.DB_TYPE_BINARY_FLOAToracledb.DB_TYPE_BINARY_FLOAToracledb.DB_TYPE_DATEoracledb.DB_TYPE_TIMESTAMPoracledb.DB_TYPE_TIMESTAMP_LTZoracledb.DB_TYPE_TIMESTAMP_TZoracledb.DB_TYPE_RAW
The following example uses two functions: associative_array_in()
computes the sum of its elements and associative_array_out()
returns the elements of an associative array.
CREATE OR REPLACE PACKAGE aa_mle_pkg AS
-- some setup code has been omitted for brevity
TYPE numtype IS TABLE OF NUMBER INDEX BY BINARY_INTEGER;
FUNCTION associative_array_in(p_values numtype) RETURN NUMBER;
FUNCTION associative_array_out RETURN numtype;
END aa_mle_pkg;
/
You can use the associative_array_in() function in JavaScript as
follows:
CREATE OR REPLACE PROCEDURE mle_associative_array_in
AS MLE LANGUAGE JAVASCRIPT
{{
const result = session.execute(
`begin
:sum := aa_mle_pkg.associative_array_in(:values);
end;`,
// you can alternatively use positional binds, too
{
sum: {
type: oracledb.NUMBER,
dir: oracledb.BIND_OUT
},
values: {
type: oracledb.NUMBER,
dir: oracledb.BIND_IN,
val: [ 1, 2, 3 ]
}
}
);
console.log(`the sum of numbers in the array is ${result.outBinds.sum}`);
}};
/
For OUT and IN OUT binds, the
maxArraySize bind property must be set. Its value is the
maximum number of elements that can be returned in an array. An error occurs if the
PL/SQL block attempts to insert data beyond this limit.
Rather than just passing a collection as an argument to a PL/SQL code
unit, your code may need to accept a collection returned by PL/SQL. Continuing with
the previous example using AA_MLE_PKG, here is an example of how to
use the associative_array_out() function:
CREATE OR REPLACE PROCEDURE mle_associative_array_out
AS MLE LANGUAGE JAVASCRIPT
{{
const result = session.execute(
"begin :values := aa_mle_pkg.associative_array_out; end;",
{
values: {
dir: oracledb.BIND_OUT,
type: "AA_MLE_PKG.NUMTYPE",
maxArraySize: 3,
},
},
);
const res = result.outBinds.values;
console.log(JSON.stringify(res));
}};
/
Pass a Nested Table to a PL/SQL Function in JavaScript
In addition to associative arrays, nested tables are another option to integrate
collection types into your work with in-database JavaScript. In this example, the
function initCaps takes a nested table as input and returns a new
list with each element capitalized.
CREATE OR REPLACE PACKAGE nt_mle_js AS
TYPE roster_t IS TABLE OF VARCHAR2(15);
FUNCTION initCaps(p_list roster_t)
RETURN JSON
AS MLE MODULE nt_module
SIGNATURE 'initCaps';
END;
/
CREATE OR REPLACE MLE MODULE nt_module
LANGUAGE JAVASCRIPT AS
/**
* Capitalizes each non-empty string in the given array:
* first character uppercased, remaining characters lowercased.
* Falsy elements (such as '', null, undefined) are returned as-is.
* The input array is not mutated; a new array is returned.
* This works only with US-ASCII characters and does not respect
* non-English language input.
*
* @param {(Array<string>)} names - List of names to transform
* @returns {(Array<string>)} A new list with each item init-capped
* @throws {Error} If names is not a provided array
*
*/
export function initCaps(names) {
if (! names || ! Array.isArray(names)) {
throw new Error("Must provide a list of names to the initCaps function");
}
return names.map( (s) => s ? s[0].toUpperCase() +
s.slice(1).toLowerCase() : s)
}
/
DECLARE
l_names nt_mle_js.roster_t := nt_mle_js.roster_t(
'john',
'paul',
'ringo',
'george'
);
l_uc_names JSON;
BEGIN
l_uc_names := nt_mle_js.initCaps(l_names);
DBMS_OUTPUT.PUT_LINE(
JSON_SERIALIZE(
l_uc_names
pretty
)
);
END;
/
Result:
[
"John",
"Paul",
"Ringo",
"George"
]
Parent topic: Working with User-Defined Data Types