77const {
88 getEnv,
99 assertDataSource,
10+ Pool,
1011} = require ( '@cubejs-backend/shared' ) ;
11- const { BaseDriver, TableColumn } = require ( '@cubejs-backend/base-driver' ) ;
12+ const { BaseDriver, TableColumn, createPoolName } = require ( '@cubejs-backend/base-driver' ) ;
1213const oracledb = require ( 'oracledb' ) ;
1314const { reduce } = require ( 'ramda' ) ;
1415
16+ // Maps Oracle `metaData.dbTypeName` strings to Cube generic types. NUMBER and the
17+ // TIMESTAMP* family are handled separately (scale-based / prefix match) below.
18+ const OracleTypeToGenericType = {
19+ varchar2 : 'text' ,
20+ nvarchar2 : 'text' ,
21+ char : 'text' ,
22+ nchar : 'text' ,
23+ clob : 'text' ,
24+ nclob : 'text' ,
25+ long : 'text' ,
26+ binary_float : 'float' ,
27+ binary_double : 'double' ,
28+ date : 'timestamp' ,
29+ 'number' : 'decimal' ,
30+ } ;
31+
1532const sortByKeys = ( unordered ) => {
1633 const ordered = { } ;
1734
@@ -43,20 +60,11 @@ const reduceCb = (result, i) => {
4360 return sortByKeys ( result ) ;
4461} ;
4562
46- /**
47- * Oracle driver class.
48- */
4963class OracleDriver extends BaseDriver {
50- /**
51- * Returns default concurrency value.
52- */
5364 static getDefaultConcurrency ( ) {
5465 return 2 ;
5566 }
5667
57- /**
58- * Class constructor.
59- */
6068 constructor ( config = { } ) {
6169 super ( {
6270 testConnectionTimeout : config . testConnectionTimeout ,
@@ -72,20 +80,51 @@ class OracleDriver extends BaseDriver {
7280 this . db . partRows = 100000 ;
7381 this . db . maxRows = 100000 ;
7482 this . db . prefetchRows = 500 ;
83+
84+ const { maxPoolSize, pool, ...connectionConfig } = config ;
85+
7586 this . config = {
7687 user : getEnv ( 'dbUser' , { dataSource, preAggregations } ) ,
7788 password : getEnv ( 'dbPass' , { dataSource, preAggregations } ) ,
7889 db : getEnv ( 'dbName' , { dataSource, preAggregations } ) ,
7990 host : getEnv ( 'dbHost' , { dataSource, preAggregations } ) ,
8091 port : getEnv ( 'dbPort' , { dataSource, preAggregations } ) || 1521 ,
81- poolMin : 0 ,
82- poolMax :
83- config . maxPoolSize ||
84- getEnv ( 'dbMaxPoolSize' , { dataSource, preAggregations } ) ||
85- 50 ,
86- ...config
92+ ...connectionConfig ,
8793 } ;
8894 this . config . connectionString = this . config . connectionString || `${ this . config . host } :${ this . config . port } /${ this . config . db } ` ;
95+
96+ const poolName = createPoolName ( 'oracle' , dataSource , preAggregations ) ;
97+ this . pool = new Pool ( poolName , {
98+ create : async ( ) => {
99+ const connection = await this . db . getConnection ( this . config ) ;
100+ await OracleDriver . initConnection ( connection ) ;
101+
102+ return connection ;
103+ } ,
104+ validate : async ( connection ) => {
105+ try {
106+ await connection . ping ( ) ;
107+ } catch ( e ) {
108+ this . databasePoolError ( e ) ;
109+ return false ;
110+ }
111+
112+ return true ;
113+ } ,
114+ destroy : ( connection ) => connection . close ( ) ,
115+ } , {
116+ min : 0 ,
117+ max :
118+ maxPoolSize ||
119+ getEnv ( 'dbMaxPoolSize' , { dataSource, preAggregations } ) ||
120+ 50 ,
121+ evictionRunIntervalMillis : 10000 ,
122+ softIdleTimeoutMillis : 30000 ,
123+ idleTimeoutMillis : 30000 ,
124+ testOnBorrow : true ,
125+ acquireTimeoutMillis : 20000 ,
126+ ...pool ,
127+ } ) ;
89128 }
90129
91130 async tablesSchema ( ) {
@@ -110,12 +149,33 @@ class OracleDriver extends BaseDriver {
110149 return reduce ( reduceCb , { } , data ) ;
111150 }
112151
113- async getConnectionFromPool ( ) {
114- if ( ! this . pool ) {
115- this . pool = await this . db . createPool ( this . config ) ;
116- }
152+ /**
153+ * Runs once per pooled session. Aligns the session NLS formats with the ISO-ish
154+ * date strings Cube binds, so implicit string→DATE/TIMESTAMP conversions (e.g.
155+ * the native planner's `CAST(? AS TIMESTAMP)` over a 'YYYY-MM-DD' filter bound)
156+ * parse instead of failing with ORA-01843 under Oracle's default NLS. Explicit
157+ * TO_DATE/TO_TIMESTAMP calls carry their own masks and are unaffected.
158+ * @protected
159+ */
160+ static async initConnection ( connection ) {
161+ await connection . execute (
162+ "ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD' NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD' NLS_TIMESTAMP_TZ_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF TZH:TZM'"
163+ ) ;
164+ }
165+
166+ /**
167+ * Acquires a connection from the pool, runs `fn`, and always releases the
168+ * connection back to the pool (on both success and failure).
169+ * @protected
170+ */
171+ async withConnection ( fn ) {
172+ const connection = await this . pool . acquire ( ) ;
117173
118- return this . pool . getConnection ( )
174+ try {
175+ return await fn ( connection ) ;
176+ } finally {
177+ await this . pool . release ( connection ) ;
178+ }
119179 }
120180
121181 async testConnection ( ) {
@@ -127,36 +187,93 @@ class OracleDriver extends BaseDriver {
127187 throw new Error ( 'Oracle can not work with table names longer than 128 symbols. ' +
128188 `Consider using the 'sqlAlias' attribute in your cube definition for ${ quotedTableName } .` ) ;
129189 }
190+
130191 return super . createTable ( quotedTableName , columns ) ;
131192 }
132193
133- async query ( query , values ) {
134- const conn = await this . getConnectionFromPool ( ) ;
194+ static normalizeParams ( query , values ) {
195+ if ( ! values || values . length === 0 ) {
196+ return { sql : query , binds : { } } ;
197+ }
135198
136- try {
137- const res = await conn . execute ( query , values || { } ) ;
199+ const binds = { } ;
200+ const valueToName = new Map ( ) ;
201+ let idx = 0 ;
202+ let nextName = 0 ;
203+
204+ // `:"?"` must be matched as a whole before a lone `?`, so it appears first
205+ // in the alternation; since it starts with `:`, its inner `?` is consumed
206+ // as part of the match and never matched again on its own.
207+ //
208+ // Placeholders carrying the same value share a single named bind. This is
209+ // semantically identical (the same value is bound) and keeps repeated
210+ // expressions textually identical across clauses — required by Oracle, which
211+ // otherwise rejects e.g. a CASE expression in both SELECT and GROUP BY when
212+ // its param renders as two different bind names (ORA-00979).
213+ const sql = query . replace ( / : " \? " | \? / g, ( ) => {
214+ const value = values [ idx ] ;
215+ idx += 1 ;
216+ // A Map distinguishes values by SameValueZero, so 1 and '1' stay separate;
217+ // the raw value works as the key without stringifying.
218+ let name = valueToName . get ( value ) ;
219+ if ( name === undefined ) {
220+ name = `cb_param_${ nextName } ` ;
221+ nextName += 1 ;
222+ valueToName . set ( value , name ) ;
223+ binds [ name ] = value ;
224+ }
225+ return `:${ name } ` ;
226+ } ) ;
227+
228+ return { sql, binds } ;
229+ }
230+
231+ async query ( query , values ) {
232+ return this . withConnection ( async ( conn ) => {
233+ const { sql, binds } = OracleDriver . normalizeParams ( query , values ) ;
234+ const res = await conn . execute ( sql , binds ) ;
138235 return res && res . rows ;
139- } catch ( e ) {
140- throw ( e ) ;
141- } finally {
142- try {
143- await conn . close ( ) ;
144- } catch ( e ) {
145- throw e ;
236+ } ) ;
237+ }
238+
239+ static metaDataToColumnTypes ( metaData ) {
240+ return ( metaData || [ ] ) . map ( ( column ) => {
241+ const dbTypeName = ( column . dbTypeName || '' ) . toLowerCase ( ) ;
242+ let type = 'text' ;
243+
244+ if ( dbTypeName . startsWith ( 'timestamp' ) ) {
245+ type = 'timestamp' ;
246+ } else {
247+ type = OracleTypeToGenericType [ dbTypeName ] || 'text' ;
146248 }
147- }
249+
250+ return { name : column . name , type } ;
251+ } ) ;
252+ }
253+
254+ async downloadQueryResults ( query , values , _options ) {
255+ return this . withConnection ( async ( conn ) => {
256+ const { sql, binds } = OracleDriver . normalizeParams ( query , values ) ;
257+ const res = await conn . execute ( sql , binds ) ;
258+ return {
259+ rows : ( res && res . rows ) || [ ] ,
260+ types : OracleDriver . metaDataToColumnTypes ( res && res . metaData ) ,
261+ } ;
262+ } ) ;
148263 }
149264
150- release ( ) {
151- return this . pool && this . pool . close ( ) ;
265+ async release ( ) {
266+ await this . pool . drain ( ) ;
267+ await this . pool . clear ( ) ;
152268 }
153269
154270 readOnly ( ) {
155271 return true ;
156272 }
157273
158274 wrapQueryWithLimit ( query ) {
159- query . query = `SELECT * FROM (${ query . query } ) AS t WHERE ROWNUM <= ${ query . limit } ` ;
275+ // Oracle forbids the `AS` keyword for table/subquery aliases.
276+ query . query = `SELECT * FROM (${ query . query } ) t WHERE ROWNUM <= ${ query . limit } ` ;
160277 }
161278}
162279
0 commit comments