@operativa/verse-oracle
Version:
Verse is a modern, fast, object/relational mapper for TypeScript inspired by Entity Framework Core. It features LINQ-style queries, unit of work updates, and a powerful convention-based mapping system. It supports SQLite, Postgres, MySQL, SQL Server and O
490 lines • 15.2 kB
JavaScript
import { BooleansToOneOrZero, DateAsTimestampWithTimeZone, DefaultSequence, SeqHiloKey, UuidPropertyToBuffer, } from "@operativa/verse/conventions/database";
import { explodeIn } from "@operativa/verse/db/in";
import { SqlPrinter } from "@operativa/verse/db/printer";
import { SqlRewriter } from "@operativa/verse/db/rewriter";
import { sqlBin, SqlCase, SqlExists, SqlFunction, sqlId, SqlIdentifier, SqlMember, SqlNot, SqlNumber, SqlParameter, SqlRaw, SqlSelect, sqlStr, } from "@operativa/verse/db/sql";
import { notNull } from "@operativa/verse/utils/check";
import { logSql } from "@operativa/verse/utils/logging";
import { error } from "@operativa/verse/utils/utils";
import { List } from "immutable";
import { BIND_OUT, createPool, getConnection, NUMBER, } from "oracledb";
export function oracle(connectionAttributes, adminAttributes) {
return new OracleDriver(connectionAttributes, adminAttributes);
}
export class OracleDriver {
connectionAttributes;
adminAttributes;
#pool;
#logger;
constructor(connectionAttributes, adminAttributes) {
this.connectionAttributes = connectionAttributes;
this.adminAttributes = adminAttributes;
notNull({ connectionAttributes });
}
get info() {
return {
name: "oracle",
server: this.connectionAttributes.connectString,
database: this.connectionAttributes.user,
};
}
get conventions() {
return List.of(new SeqHiloKey(), new BooleansToOneOrZero(), new DefaultSequence(), new DateAsTimestampWithTimeZone(), new UuidPropertyToBuffer());
}
// @ts-ignore
validate(model) { }
set logger(logger) {
this.#logger = logger;
}
async pool() {
if (!this.#pool) {
this.#pool = await createPool({
...this.connectionAttributes,
user: sqlId(this.connectionAttributes.user).accept(new OraclePrinter()),
});
}
return this.#pool;
}
rows(sql) {
const printer = new OraclePrinter();
return (args) => {
const query = sql.accept(new DialectRewriter(args)).accept(printer);
return this.#query(query, args);
};
}
async *#query(sql, args) {
logSql(sql, args, this.#logger);
const pool = await this.pool();
let connection;
try {
const bind = {};
args.forEach((v, i) => (bind[i] = v));
connection = await pool.getConnection();
const stream = connection.queryStream(sql, bind);
for await (const row of stream) {
yield row;
}
}
finally {
await connection?.close();
}
}
async execute(statements, isolation, onCommit) {
const results = [];
let connection;
const pool = await this.pool();
try {
connection = await pool.getConnection();
if (isolation) {
await connection.execute(`set transaction isolation level ${isolation}`);
}
for (const statement of statements) {
const outBinds = [];
const sql = statement.sql
.accept(new DialectRewriter(statement.args))
.accept(new OraclePrinter(outBinds));
const args = statement.args ?? [];
const bind = {};
args.forEach((v, i) => (bind[i] = v));
outBinds.forEach(b => (bind[b] = { dir: BIND_OUT, type: NUMBER }));
logSql(sql, args, this.#logger);
statement.onBeforeExecute?.(args);
const response = await connection.execute(sql, bind);
const result = {
rowsAffected: response.rowsAffected,
returning: outBinds.map(b => response.outBinds[b][0]),
};
results.push(result);
statement.onAfterExecute?.(result);
}
await connection.commit();
onCommit?.(results);
}
finally {
await connection?.close();
}
return results;
}
script(statements) {
const dialect = new DialectRewriter([]);
const printer = new OraclePrinter();
return statements.map(stmt => stmt.sql.accept(dialect).accept(printer));
}
async exists() {
if (!this.adminAttributes) {
throw new Error("Cannot check for database without admin connection attributes.");
}
const user = this.connectionAttributes.user;
const ident = sqlId(user).accept(new OraclePrinter());
const param = user === ident ? user.toUpperCase() : user;
const sql = `select 1 from all_users where username = :0`;
const args = [param];
logSql(sql, args, this.#logger);
let connection;
try {
connection = await getConnection(this.adminAttributes);
// noinspection LoopStatementThatDoesntLoopJS
for await (const _ of connection.queryStream(sql, args)) {
return true;
}
return false;
}
finally {
await connection?.close();
}
}
static #TABLE_EXISTS = new SqlSelect({
projection: SqlNumber.ONE,
from: sqlId("all_tables"),
where: sqlBin(sqlId("table_name"), "=", new SqlParameter(0)),
});
async tableExists(name) {
// noinspection LoopStatementThatDoesntLoopJS
for await (const _ of this.rows(OracleDriver.#TABLE_EXISTS)([name])) {
return true;
}
return false;
}
async create() {
if (!this.adminAttributes) {
throw new Error("Cannot create a database without admin connection attributes.");
}
let connection;
try {
connection = await getConnection(this.adminAttributes);
const printer = new OraclePrinter();
const user = sqlId(this.connectionAttributes.user).accept(printer);
const password = sqlId(this.connectionAttributes.password).accept(printer);
const ops = [
`create user ${user} identified by ${password}`,
`grant create session to ${user}`,
`grant create table to ${user}`,
`grant create sequence to ${user}`,
`grant unlimited tablespace to ${user}`,
];
for (const sql of ops) {
logSql(sql, [], this.#logger);
await connection.execute(sql);
}
}
finally {
await connection?.close();
}
}
async drop() {
if (!(await this.exists())) {
return;
}
if (!this.adminAttributes) {
throw new Error("Cannot drop a database without admin connection attributes.");
}
const user = sqlId(this.connectionAttributes.user).accept(new OraclePrinter());
const sql = `drop user ${user} cascade`;
logSql(sql, [], this.#logger);
let connection;
try {
connection = await getConnection(this.adminAttributes);
await connection.execute(sql);
}
finally {
await connection?.close();
}
}
async [Symbol.asyncDispose]() {
return await this.#pool?.close();
}
}
class DialectRewriter extends SqlRewriter {
args;
constructor(args = []) {
super();
this.args = args;
}
visitSelect(select) {
let newSelect = super.visitSelect(select);
const newProjection = newSelect.projection.map(n => this.#selectBoolean(n));
newSelect = newSelect.withProjection(newProjection);
if (!newSelect.from) {
newSelect = newSelect.withFrom(new SqlIdentifier("DUAL"));
}
if (newSelect.where) {
const where = this.#whereBoolean(newSelect.where);
newSelect = newSelect.withWhere(where);
}
return newSelect;
}
#selectBoolean(node) {
if (node instanceof SqlNot || node instanceof SqlExists) {
return new SqlCase(node, SqlNumber.ONE, SqlNumber.ZERO);
}
return node;
}
visitBinary(binary) {
const newBinary = super.visitBinary(binary);
if (newBinary.op === "%") {
return new SqlFunction("MOD", List.of(newBinary.left, newBinary.right));
}
if (newBinary.op === "and" || newBinary.op === "or") {
const left = this.#whereBoolean(newBinary.left);
const right = this.#whereBoolean(newBinary.right);
if (left !== newBinary.left || right !== newBinary.right) {
return sqlBin(left, newBinary.op, right);
}
}
if (newBinary.op === "->" || newBinary.op === "->>") {
const number = newBinary.right;
return new SqlFunction("json_query", List.of(newBinary.left, sqlStr(`$[${number.value}]`)));
}
return newBinary;
}
visitNot(not) {
const operand = this.#whereBoolean(not.operand);
if (operand !== not.operand) {
return new SqlNot(operand);
}
return not;
}
#whereBoolean(node) {
if (node instanceof SqlMember) {
const member = node;
const identifier = member.member;
if (identifier.type === "boolean") {
return sqlBin(member, "=", SqlNumber.ONE);
}
}
return node;
}
visitFunction(func) {
if (func.name === "now") {
return new SqlRaw(List.of("SYSDATE"));
}
if (func.name === "nextval") {
return new SqlMember(func.args.get(0), sqlId("nextval"));
}
return super.visitFunction(func);
}
visitIn(_in) {
if (this.args.length > 0) {
return explodeIn(_in, this.args);
}
return _in;
}
visitNextValue(nextValue) {
return new SqlMember(nextValue.sequence, sqlId("nextval"));
}
visitColumn(column) {
const newColumn = super.visitColumn(column);
if (newColumn.type === "uuid") {
return newColumn.withType("binary(16)");
}
return newColumn;
}
}
const keywords = new Set([
"ACCESS",
"ELSE",
"MODIFY",
"START",
"ADD",
"EXCLUSIVE",
"NOAUDIT",
"SELECT",
"ALL",
"EXISTS",
"NOCOMPRESS",
"SESSION",
"ALTER",
"FILE",
"NOT",
"SET",
"AND",
"FLOAT",
"NOTFOUND",
"SHARE",
"ANY",
"FOR",
"NOWAIT",
"SIZE",
"ARRAYLEN",
"FROM",
"NULL",
"SMALLINT",
"AS",
"GRANT",
"NUMBER",
"SQLBUF",
"ASC",
"GROUP",
"OF",
"SUCCESSFUL",
"AUDIT",
"HAVING",
"OFFLINE",
"SYNONYM",
"BETWEEN",
"IDENTIFIED",
"ON",
"SYSDATE",
"BY",
"IMMEDIATE",
"ONLINE",
"TABLE",
"CHAR",
"IN",
"OPTION",
"THEN",
"CHECK",
"INCREMENT",
"OR",
"TO",
"CLUSTER",
"INDEX",
"ORDER",
"TRIGGER",
"COLUMN",
"INITIAL",
"PCTFREE",
"UID",
"COMMENT",
"INSERT",
"PRIOR",
"UNION",
"COMPRESS",
"INTEGER",
"PRIVILEGES",
"UNIQUE",
"CONNECT",
"INTERSECT",
"PUBLIC",
"UPDATE",
"CREATE",
"INTO",
"RAW",
"USER",
"CURRENT",
"IS",
"RENAME",
"VALIDATE",
"DATE",
"LEVEL",
"RESOURCE",
"VALUES",
"DECIMAL",
"LIKE",
"REVOKE",
"VARCHAR",
"DEFAULT",
"LOCK",
"ROW",
"VARCHAR2",
"DELETE",
"LONG",
"ROWID",
"VIEW",
"DESC",
"MAXEXTENTS",
"ROWLABEL",
"WHENEVER",
"DISTINCT",
"MINUS",
"ROWNUM",
"WHERE",
"DROP",
"MODE",
"ROWS",
"WITH",
]);
class OraclePrinter extends SqlPrinter {
outBinds;
constructor(outBinds = []) {
super();
this.outBinds = outBinds;
}
visitFunction(func) {
return `${func.name}(${this.visitFunctionArgs(func.args)}${func.name === "json_arrayagg" ? " returning varchar2(32767)" : ""})`;
}
visitAlterColumn(alterColumn) {
let sql = "";
const column = alterColumn.column;
if (column.identity) {
error("Oracle does not support adding identity to existing columns. Use raw SQL to recreate the column.");
}
else {
if (column.type) {
sql += ` ${this.visitType(column.type)}`;
}
if (column.nullable !== undefined) {
sql += column.nullable ? "null" : " not null";
}
if (column.default) {
sql += ` default ${column.default.accept(this)}`;
}
if (column.identity === false) {
sql += " drop identity";
}
}
return `alter table ${alterColumn.table.accept(this)} modify ${column.name.accept(this)}${sql}`;
}
visitColumn(column) {
return `${column.name.accept(this)} ${column.type ? this.visitType(column.type) : ""}${column.identity === true
? " generated by default on null as identity"
: column.default
? ` default ${column.default.accept(this)}`
: column.nullable === false
? " not null"
: ""}`;
}
visitOnDelete(onDelete) {
return onDelete && onDelete !== "no action" ? ` on delete ${onDelete}` : "";
}
visitReturning(insert) {
return (` returning ${insert.returning.map(r => r.accept(this)).join(", ")} into ` +
`${insert.returning
.map((_, i) => {
const out = `out${i}`;
this.outBinds.push(out);
return `:${out}`;
})
.join(", ")}`);
}
visitType(type) {
if (type.startsWith("binary")) {
return type.replace("binary", "raw");
}
if (type === "text") {
return "clob";
}
if (type === "boolean") {
return "number(1)";
}
return super.visitType(type);
}
visitParameter(parameter) {
return `:${parameter.id}`;
}
visitAlias(alias) {
return `${this.parens(alias.target)} ${alias.alias.accept(this)}`;
}
visitLimit(limit) {
return `\nfetch next ${limit.accept(this)} rows only`;
}
visitOffset(offset) {
return `\noffset ${offset.accept(this)} rows`;
}
visitIdentifier(identifier) {
const escaped = this.escapeIdent(identifier.name);
return escaped === identifier.name &&
!this.isKeyword(escaped) &&
!escaped.startsWith("_") &&
!/['.]/.test(escaped)
? escaped
: `"${escaped}"`;
}
visitTimestamp(timestamp) {
return `TIMESTAMP ${super.visitTimestamp(timestamp).replace("T", " ").replace("Z", " UTC")}`;
}
isKeyword(name) {
return keywords.has(name.toUpperCase());
}
}
//# sourceMappingURL=index.js.map