UNPKG

@bytebase/dbhub

Version:

Token-efficient database MCP server for PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, SQLite

755 lines (753 loc) • 28.8 kB
import { computeHitRatioPct, toNullableNumber } from "./chunk-FU2ZJE4E.js"; import { SQLRowLimiter, closeQuietly } from "./chunk-X5OCFRNL.js"; import { isReadOnlySQL } from "./chunk-55X2VY5D.js"; import { ConnectorRegistry, LEADING_SQL_NOISE, SafeURL, blankCommentsAndStrings, obfuscateDSNPassword, stripCommentsAndStrings } from "./chunk-IXC4NQGC.js"; // src/connectors/oracle/index.ts import oracledb from "oracledb"; var OracleDSNParser = class { async parse(dsn, config) { if (!this.isValidDSN(dsn)) { throw new Error( `Invalid Oracle DSN format. Provided: ${obfuscateDSNPassword(dsn)} Expected: ${this.getSampleDSN()}` ); } try { const url = new SafeURL(dsn); let sslmode; let sid; url.forEachSearchParam((value, key) => { if (key === "sslmode") { sslmode = value; } else if (key === "sid") { sid = value; } }); if (sslmode !== void 0 && !["disable", "require", "verify-full"].includes(sslmode)) { throw new Error( `Unsupported sslmode '${sslmode}' for Oracle. Supported: disable, require, verify-full` ); } const host = url.hostname; const port = url.port ? parseInt(url.port, 10) : 1521; const service = url.pathname ? url.pathname.substring(1) : ""; if (!host) { throw new Error("Oracle DSN must include a host"); } if (!service && !sid) { throw new Error("Oracle DSN must include a service name in the path (or ?sid=)"); } const useTls = sslmode === "require" || sslmode === "verify-full"; let connectString; if (sid) { const protocol = useTls ? "TCPS" : "TCP"; connectString = `(DESCRIPTION=(ADDRESS=(PROTOCOL=${protocol})(HOST=${host})(PORT=${port}))(CONNECT_DATA=(SID=${sid})))`; } else { connectString = `${useTls ? "tcps://" : ""}${host}:${port}/${service}`; } const pool = { user: url.username, password: url.password, connectString, poolMin: 0, poolMax: config?.poolMaxConnections ?? 4, poolIncrement: 1 }; if (config?.connectionTimeoutSeconds !== void 0) { pool.connectTimeout = config.connectionTimeoutSeconds; } if (useTls) { pool.sslServerDNMatch = sslmode === "verify-full"; } return { pool, ...config?.queryTimeoutSeconds !== void 0 && { callTimeoutMs: config.queryTimeoutSeconds * 1e3 } }; } catch (error) { throw new Error( `Failed to parse Oracle DSN: ${error instanceof Error ? error.message : String(error)}` ); } } getSampleDSN() { return "oracle://username:password@localhost:1521/FREEPDB1?sslmode=disable"; } isValidDSN(dsn) { return dsn.startsWith("oracle://"); } }; var _OracleConnector = class _OracleConnector { constructor() { this.id = "oracle"; this.name = "Oracle"; this.dsnParser = new OracleDSNParser(); /** CURRENT_SCHEMA of the connected session, resolved once at connect time */ this.defaultSchema = ""; // Source ID is set by ConnectorManager after cloning this.sourceId = "default"; } getId() { return this.sourceId; } clone() { return new _OracleConnector(); } async connect(dsn, initScript, config) { try { const parsed = await this.dsnParser.parse(dsn, config); this.callTimeoutMs = parsed.callTimeoutMs; this.pool = await oracledb.createPool(parsed.pool); const rows = await this.query( "SELECT SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') AS schema_name FROM dual" ); this.defaultSchema = rows[0]?.SCHEMA_NAME ?? _OracleConnector.foldIdentifier(parsed.pool.user ?? ""); if (initScript) { await this.executeSQL(initScript, {}); } } catch (error) { await closeQuietly(() => this.disconnect()); throw error; } } async disconnect() { if (this.pool) { const pool = this.pool; this.pool = void 0; await pool.close(0); } } /** Check out a pooled connection with the configured statement timeout applied. */ async acquire() { if (!this.pool) { throw new Error("Not connected to Oracle database"); } const connection = await this.pool.getConnection(); if (this.callTimeoutMs !== void 0) { connection.callTimeout = this.callTimeoutMs; } return connection; } /** Run one or more catalog queries on a single short-lived pooled connection. */ async withConnection(fn) { const connection = await this.acquire(); try { return await fn(connection); } finally { await connection.close(); } } static async fetchRows(connection, sql, binds = {}) { const result = await connection.execute(sql, binds, { outFormat: oracledb.OUT_FORMAT_OBJECT, fetchTypeHandler: _OracleConnector.fetchTypeHandler }); return result.rows ?? []; } /** Run one catalog query on a short-lived pooled connection. */ query(sql, binds = {}) { return this.withConnection((connection) => _OracleConnector.fetchRows(connection, sql, binds)); } /** * Per-column fetch rules: * - CLOB/NCLOB come back as strings and BLOB as a Buffer instead of Lob * streams, so result rows are plain JSON. (LONG columns such as * ALL_TAB_COLUMNS.DATA_DEFAULT are already fetched as strings by default.) * - NUMBER is fetched as its exact decimal string and converted here: * integers within Number's safe range become numbers, larger integers * become BigInt (the response serializer renders those as strings), and * anything with a fraction or exponent becomes a number. The driver's * default would round a NUMBER(20) identifier above 2^53 silently. */ static fetchTypeHandler(metaData) { if (metaData.dbType === oracledb.DB_TYPE_CLOB || metaData.dbType === oracledb.DB_TYPE_NCLOB) { return { type: oracledb.STRING }; } if (metaData.dbType === oracledb.DB_TYPE_BLOB) { return { type: oracledb.BUFFER }; } if (metaData.dbType === oracledb.DB_TYPE_NUMBER) { return { type: oracledb.STRING, converter: _OracleConnector.convertNumber }; } return void 0; } /** * See fetchTypeHandler. Receives the NUMBER's decimal string (the generic * signature is what oracledb's converter type requires). Public for unit * testing. */ static convertNumber(value) { if (value === null || value === void 0) { return null; } const text = String(value); if (/^-?\d+$/.test(text)) { const asNumber = Number(text); return Number.isSafeInteger(asNumber) ? asNumber : BigInt(text); } return Number(text); } /** * The catalog spelling of an identifier a caller wrote unquoted: Oracle * folds those to upper case. A name that already contains an upper-case * letter is taken as spelled, so a case-sensitive quoted identifier like * "MyTable" is still reachable by its exact name. (An all-lower-case quoted * identifier is not; that trade keeps every catalog predicate a plain * equality on an indexed column.) */ static foldIdentifier(name) { return /[A-Z]/.test(name) ? name : name.toUpperCase(); } schemaOrDefault(schema) { return schema ? _OracleConnector.foldIdentifier(schema) : this.defaultSchema; } async getSchemas() { try { const rows = await this.query( `SELECT username FROM all_users WHERE oracle_maintained = 'N' OR username = :current_schema ORDER BY username`, { current_schema: this.schemaOrDefault() } ); return rows.map((row) => row.USERNAME); } catch (error) { throw new Error(`Failed to get schemas: ${error.message}`); } } async getDefaultSchema() { return this.defaultSchema || null; } async getTables(schema) { try { const rows = await this.query( `SELECT table_name FROM all_tables WHERE owner = :schema AND nested = 'NO' AND secondary = 'N' AND (iot_type IS NULL OR iot_type = 'IOT') AND table_name NOT LIKE 'BIN$%' ORDER BY table_name`, { schema: this.schemaOrDefault(schema) } ); return rows.map((row) => row.TABLE_NAME); } catch (error) { throw new Error(`Failed to get tables: ${error.message}`); } } async getViews(schema) { try { const rows = await this.query( `SELECT view_name FROM all_views WHERE owner = :schema ORDER BY view_name`, { schema: this.schemaOrDefault(schema) } ); return rows.map((row) => row.VIEW_NAME); } catch (error) { throw new Error(`Failed to get views: ${error.message}`); } } async tableExists(tableName, schema) { try { const rows = await this.query( `SELECT COUNT(*) AS cnt FROM all_tables WHERE owner = :schema AND table_name = :table_name`, { schema: this.schemaOrDefault(schema), table_name: _OracleConnector.foldIdentifier(tableName) } ); return Number(rows[0]?.CNT ?? 0) > 0; } catch (error) { throw new Error(`Failed to check if table exists: ${error.message}`); } } async getTableSchema(tableName, schema) { try { const rows = await this.query( `SELECT c.column_name, c.data_type, c.data_length, c.char_length, c.data_precision, c.data_scale, c.nullable, c.data_default, cc.comments AS description FROM all_tab_columns c LEFT JOIN all_col_comments cc ON cc.owner = c.owner AND cc.table_name = c.table_name AND cc.column_name = c.column_name WHERE c.owner = :schema AND c.table_name = :table_name ORDER BY c.column_id`, { schema: this.schemaOrDefault(schema), table_name: _OracleConnector.foldIdentifier(tableName) } ); return rows.map((row) => ({ column_name: row.COLUMN_NAME, data_type: _OracleConnector.formatDataType(row), is_nullable: row.NULLABLE === "Y" ? "YES" : "NO", // DATA_DEFAULT is a LONG that keeps the DDL's trailing whitespace. column_default: row.DATA_DEFAULT?.trim() || null, description: row.DESCRIPTION || null })); } catch (error) { throw new Error(`Failed to get schema for table ${tableName}: ${error.message}`); } } /** * Render a column's type the way it appears in DDL: `VARCHAR2(100)`, * `NUMBER(10,2)`, `NUMBER`, `TIMESTAMP(6)`. Oracle's catalog splits these * across several columns. */ static formatDataType(row) { const type = row.DATA_TYPE; if (/^(?:N?VARCHAR2|N?CHAR|RAW)$/.test(type)) { const length = type === "RAW" ? row.DATA_LENGTH : row.CHAR_LENGTH; return length ? `${type}(${length})` : type; } if (type === "NUMBER") { if (row.DATA_PRECISION === null) { return row.DATA_SCALE === null ? type : `NUMBER(*,${row.DATA_SCALE})`; } return row.DATA_SCALE ? `NUMBER(${row.DATA_PRECISION},${row.DATA_SCALE})` : `NUMBER(${row.DATA_PRECISION})`; } if (type === "FLOAT" && row.DATA_PRECISION !== null) { return `FLOAT(${row.DATA_PRECISION})`; } return type; } async getTableIndexes(tableName, schema) { try { const rows = await this.query( `SELECT i.index_name, i.uniqueness, CASE WHEN pk.constraint_name IS NOT NULL THEN 1 ELSE 0 END AS is_primary, ic.column_name FROM all_indexes i JOIN all_ind_columns ic ON ic.index_owner = i.owner AND ic.index_name = i.index_name LEFT JOIN all_constraints pk ON pk.owner = i.table_owner AND pk.table_name = i.table_name AND pk.constraint_type = 'P' AND pk.index_owner = i.owner AND pk.index_name = i.index_name WHERE i.table_owner = :schema AND i.table_name = :table_name ORDER BY i.index_name, ic.column_position`, { schema: this.schemaOrDefault(schema), table_name: _OracleConnector.foldIdentifier(tableName) } ); const indexMap = /* @__PURE__ */ new Map(); for (const row of rows) { let index = indexMap.get(row.INDEX_NAME); if (!index) { index = { index_name: row.INDEX_NAME, column_names: [], is_unique: row.UNIQUENESS === "UNIQUE", is_primary: Number(row.IS_PRIMARY) === 1 }; indexMap.set(row.INDEX_NAME, index); } index.column_names.push(row.COLUMN_NAME); } return Array.from(indexMap.values()); } catch (error) { throw new Error(`Failed to get indexes for table ${tableName}: ${error.message}`); } } async getTableComment(tableName, schema) { try { const rows = await this.query( `SELECT comments FROM all_tab_comments WHERE owner = :schema AND table_name = :table_name`, { schema: this.schemaOrDefault(schema), table_name: _OracleConnector.foldIdentifier(tableName) } ); return rows[0]?.COMMENTS || null; } catch { return null; } } async getTableRowCount(tableName, schema) { try { const rows = await this.query( `SELECT num_rows FROM all_tables WHERE owner = :schema AND table_name = :table_name`, { schema: this.schemaOrDefault(schema), table_name: _OracleConnector.foldIdentifier(tableName) } ); const numRows = rows[0]?.NUM_ROWS; return numRows === null || numRows === void 0 ? null : Number(numRows); } catch { return null; } } async getHealthCheck() { if (!this.pool) { throw new Error("Not connected to Oracle database"); } const notes = []; const result = {}; try { const [sessions, params] = await this.withConnection( (connection) => Promise.all([ _OracleConnector.fetchRows( connection, // STATUS is ACTIVE (running a call), INACTIVE (idle), or one of // the transitional states KILLED / SNIPED / CACHED, which count // toward the total but are neither active nor idle. TADDR is // non-null while the session has an open transaction; // LAST_CALL_ET is seconds since the current call began (ACTIVE) // or since the last call ended (otherwise). `SELECT COUNT(*) AS total, SUM(CASE WHEN status = 'ACTIVE' THEN 1 ELSE 0 END) AS active, SUM(CASE WHEN status = 'INACTIVE' THEN 1 ELSE 0 END) AS idle, SUM(CASE WHEN status = 'INACTIVE' AND taddr IS NOT NULL THEN 1 ELSE 0 END) AS idle_in_transaction, MAX(CASE WHEN status = 'INACTIVE' AND taddr IS NOT NULL THEN last_call_et END) AS longest_idle_in_transaction_seconds, MAX(CASE WHEN status = 'ACTIVE' THEN last_call_et END) AS longest_active_query_seconds FROM v$session WHERE type = 'USER' AND sid <> SYS_CONTEXT('USERENV', 'SID')` ), _OracleConnector.fetchRows( connection, `SELECT value FROM v$parameter WHERE name = 'sessions'` ) ]) ); const conn = sessions[0]; const maxConnections = params.length > 0 ? Number(params[0].VALUE) : null; result.connections = { total: Number(conn.TOTAL ?? 0), active: Number(conn.ACTIVE ?? 0), idle: Number(conn.IDLE ?? 0), idleInTransaction: Number(conn.IDLE_IN_TRANSACTION ?? 0), // Oracle has no equivalent of Postgres's "idle in transaction // (aborted)" state: a failed statement is rolled back on its own // and leaves the transaction usable. maxConnections: maxConnections !== null && maxConnections > 0 ? maxConnections : null, longestIdleInTransactionSeconds: toNullableNumber(conn.LONGEST_IDLE_IN_TRANSACTION_SECONDS), longestActiveQuerySeconds: toNullableNumber(conn.LONGEST_ACTIVE_QUERY_SECONDS) }; } catch { notes.push( "Connection pool metrics unavailable: connecting user lacks SELECT on V$SESSION / V$PARAMETER (grant SELECT_CATALOG_ROLE or SELECT ANY DICTIONARY)." ); } try { const stats = await this.query( `SELECT name, value FROM v$sysstat WHERE name IN ('db block gets', 'consistent gets', 'physical reads')` ); const byName = Object.fromEntries(stats.map((row) => [row.NAME, Number(row.VALUE)])); const logicalReads = (byName["db block gets"] ?? 0) + (byName["consistent gets"] ?? 0); const physicalReads = byName["physical reads"] ?? 0; result.bufferCache = { hitRatioPct: computeHitRatioPct(logicalReads, physicalReads), blocksHit: logicalReads - physicalReads, blocksRead: physicalReads }; } catch { notes.push( "Buffer cache metrics unavailable: connecting user lacks SELECT on V$SYSSTAT (grant SELECT_CATALOG_ROLE or SELECT ANY DICTIONARY)." ); } if (notes.length > 0) { result.notes = notes; } return result; } async getStoredProcedures(schema, routineType) { try { const typeFilter = routineType === "function" ? "object_type = 'FUNCTION'" : routineType === "procedure" ? "object_type = 'PROCEDURE'" : "object_type IN ('PROCEDURE', 'FUNCTION')"; const rows = await this.query( `SELECT object_name FROM all_objects WHERE owner = :schema AND ${typeFilter} ORDER BY object_name`, { schema: this.schemaOrDefault(schema) } ); return rows.map((row) => row.OBJECT_NAME); } catch (error) { throw new Error(`Failed to get stored procedures: ${error.message}`); } } async getStoredProcedureDetail(procedureName, schema) { try { const schemaToUse = this.schemaOrDefault(schema); const name = _OracleConnector.foldIdentifier(procedureName); return await this.withConnection(async (connection) => { const objects = await _OracleConnector.fetchRows( connection, `SELECT object_type FROM all_objects WHERE owner = :schema AND object_name = :name AND object_type IN ('PROCEDURE', 'FUNCTION')`, { schema: schemaToUse, name } ); if (objects.length === 0) { throw new Error(`Stored procedure '${procedureName}' not found in schema '${schemaToUse}'`); } const objectType = objects[0].OBJECT_TYPE; const isFunction = objectType === "FUNCTION"; const [args, source] = await Promise.all([ // Standalone routines only (package_name IS NULL). Position 0 with // no argument name is a function's return value. _OracleConnector.fetchRows( connection, `SELECT argument_name, position, in_out, data_type FROM all_arguments WHERE owner = :schema AND object_name = :name AND package_name IS NULL AND data_level = 0 ORDER BY position`, { schema: schemaToUse, name } ), _OracleConnector.fetchRows( connection, `SELECT text FROM all_source WHERE owner = :schema AND name = :name AND type = :object_type ORDER BY line`, { schema: schemaToUse, name, object_type: objectType } ) ]); const returnType = args.find((arg) => arg.POSITION === 0 && arg.ARGUMENT_NAME === null)?.DATA_TYPE; const parameterList = args.filter((arg) => arg.ARGUMENT_NAME !== null).map((arg) => `${arg.ARGUMENT_NAME} ${arg.IN_OUT} ${arg.DATA_TYPE ?? ""}`.trim()).join(", "); return { procedure_name: name, procedure_type: isFunction ? "function" : "procedure", language: "plsql", parameter_list: parameterList, return_type: isFunction ? returnType ?? void 0 : void 0, definition: source.length > 0 ? source.map((row) => row.TEXT).join("") : void 0 }; }); } catch (error) { throw new Error(`Failed to get stored procedure details: ${error.message}`); } } /** * Bind values for one statement, keyed by placeholder name. DBHub's * placeholders are `:1`, `:2`, ... and each names parameters[N-1]; binding * by name (rather than handing the driver a positional array) lets a * placeholder repeat (`:1 ... :1`) or appear out of order, and lets a batch * hand each statement only the binds it uses, since the driver rejects a * bind object naming a placeholder the statement lacks (NJS-097). */ static bindsFor(statement, parameters) { const binds = {}; const blanked = blankCommentsAndStrings(statement, "oracle"); for (const match of blanked.matchAll(/(?<!:):(\d+)\b/g)) { const index = parseInt(match[1], 10); if (index >= 1 && index <= parameters.length) { binds[match[1]] = parameters[index - 1]; } } return binds; } /** * Wrap a driver error with context while keeping the properties the * connection-error classifier reads (`code`, `errorNum`), which a plain * `new Error(message)` would drop. */ static wrapError(prefix, error) { const wrapped = new Error(`${prefix}: ${error.message}`, { cause: error }); for (const key of ["code", "errorNum", "offset"]) { const value = error?.[key]; if (value !== void 0) { wrapped[key] = value; } } return wrapped; } async executeSQL(sqlQuery, options, parameters) { const afterNoise = sqlQuery.replace(LEADING_SQL_NOISE, ""); if (/^explain\b/i.test(afterNoise)) { return this.explainQuery(afterNoise.slice("explain".length), options.readonly, parameters); } const statements = _OracleConnector.splitStatements(afterNoise); const connection = await this.acquire(); try { if (options.readonly) { await connection.execute("SET TRANSACTION READ ONLY"); } const resultSets = []; for (const statement of statements) { const { sql: processedSQL, probeApplied } = SQLRowLimiter.applyMaxRowsForOracleWithTruncationProbe(statement, options.maxRows); const result = await connection.execute( processedSQL, _OracleConnector.bindsFor(statement, parameters ?? []), { outFormat: oracledb.OUT_FORMAT_OBJECT, fetchTypeHandler: _OracleConnector.fetchTypeHandler } ); const rows = result.rows ?? []; const resultSet = { sql: statement, rows, rowCount: result.rows ? rows.length : result.rowsAffected ?? 0 }; SQLRowLimiter.flagTruncation(resultSet, options.maxRows, probeApplied); resultSets.push(resultSet); } if (options.readonly) { await connection.rollback(); } else { await connection.commit(); } return { resultSets }; } catch (error) { await closeQuietly(() => connection.rollback()); throw _OracleConnector.wrapError("Failed to execute query", error); } finally { await connection.close(); } } /** * Split a batch into the statements Oracle executes one per round trip. * * Plain SQL ends at a top-level semicolon, which is stripped: Oracle * rejects a trailing `;` on a SQL statement (ORA-00933). A PL/SQL block, * or the DDL that creates one, *requires* its semicolons and is sent * whole: it ends at the `;` that closes its outermost BEGIN ... END * (depth-tracked over `begin`/`case` ... `end`, with `end if` / `end loop` * neutral), or at a SQL*Plus `/` line, which is dropped either way. The * scan runs on the comment/string-blanked text so nothing inside a * literal or comment counts. */ static splitStatements(sql) { const blanked = blankCommentsAndStrings(sql, "oracle"); const statements = []; const boundary = /(?:\s|\/(?=[ \t]*(?:\r?\n|$)))*/y; const plainEnd = /;|^[ \t]*\/[ \t]*$/gm; const token = /\b(begin|case|end|compound\s+trigger)\b(?:\s+(if|loop|case))?|;|^[ \t]*\/[ \t]*$/gim; const push = (start, end) => { const text = sql.slice(start, end).trim(); if (text) statements.push(text); }; let i = 0; while (i < blanked.length) { boundary.lastIndex = i; i += boundary.exec(blanked)[0].length; if (i >= blanked.length) break; const start = i; const rest = blanked.slice(i); const isUnit = _OracleConnector.PLSQL_UNIT.test(rest); if (!isUnit && !_OracleConnector.PLSQL_BODY.test(rest)) { plainEnd.lastIndex = i; const m2 = plainEnd.exec(blanked); const end2 = m2?.index ?? blanked.length; push(start, end2); i = end2 + (m2?.[0] === ";" ? 1 : 0); continue; } let depth = isUnit ? 1 : 0; let opened = isUnit; let end = blanked.length; token.lastIndex = i; let m; while ((m = token.exec(blanked)) !== null) { if (m[0] === ";") { if (opened && depth === 0) { end = m.index + 1; break; } } else if (m[1] === void 0) { end = m.index; break; } else { const keyword = m[1].toLowerCase(); const closes = m[2]?.toLowerCase(); if (keyword !== "end") { depth++; opened = true; } else if (closes === void 0 || closes === "case") { depth--; } } } push(start, end); i = end; } return statements; } /** * Run `EXPLAIN PLAN FOR <statement>` and return the formatted plan. * * EXPLAIN PLAN parses and optimizes the statement without executing it. It * stores the plan in PLAN_TABLE (a session-private global temporary table), * which is why this path runs outside the READ ONLY transaction used by * executeSQL and cleans up its rows afterwards. * * Accepts the Postgres-style `EXPLAIN <stmt>` the explain_sql tool emits as * well as Oracle's own `EXPLAIN PLAN [SET STATEMENT_ID = '...'] FOR <stmt>`. */ async explainQuery(afterExplain, readonly, parameters) { const innerQuery = afterExplain.replace(/^\s*plan\b(?:\s+set\s+statement_id\s*=\s*'[^']*')?\s+for\b/i, "").replace(/;\s*$/, "").trim(); if (!stripCommentsAndStrings(innerQuery, "oracle").trim()) { throw new Error("EXPLAIN requires a statement to analyze"); } if (readonly && !isReadOnlySQL(innerQuery, "oracle")) { throw new Error("Read-only mode: EXPLAIN is only allowed for read statements"); } const statementId = `dbhub_${Math.random().toString(36).slice(2, 14)}`; return this.withConnection(async (connection) => { try { await connection.execute( `EXPLAIN PLAN SET STATEMENT_ID = '${statementId}' FOR ${innerQuery}`, _OracleConnector.bindsFor(innerQuery, parameters ?? []) ); const lines = await _OracleConnector.fetchRows( connection, "SELECT plan_table_output FROM TABLE(DBMS_XPLAN.DISPLAY('PLAN_TABLE', :id, 'TYPICAL'))", { id: statementId } ); const plan = lines.map((row) => row.PLAN_TABLE_OUTPUT).join("\n"); return { resultSets: [ { rows: lines.length > 0 ? [{ plan }] : [], rowCount: lines.length > 0 ? 1 : 0 } ] }; } catch (error) { throw _OracleConnector.wrapError("Failed to explain query", error); } finally { await closeQuietly(async () => { await connection.execute("DELETE FROM plan_table WHERE statement_id = :id", { id: statementId }); await connection.commit(); }); } }); } }; /** * Leading keywords of a statement whose body is PL/SQL. Such a statement * ends at the semicolon that closes its outermost BEGIN ... END, not at * the first semicolon (see splitStatements). */ _OracleConnector.PLSQL_BODY = /^(?:begin|declare|create\s+(?:or\s+replace\s+)?(?:(?:editionable|noneditionable)\s+)?(?:procedure|function|trigger))\b/i; /** * Package specs/bodies and type bodies: `IS ... END name;` with no BEGIN * of their own at the top level, so the IS/AS opens the block. */ _OracleConnector.PLSQL_UNIT = /^create\s+(?:or\s+replace\s+)?(?:(?:editionable|noneditionable)\s+)?(?:package(?:\s+body)?|type\s+body)\b/i; var OracleConnector = _OracleConnector; var oracleConnector = new OracleConnector(); ConnectorRegistry.register(oracleConnector); export { OracleConnector, OracleDSNParser };