UNPKG

@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
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