├── .gitignore ├── .npm └── package │ ├── .gitignore │ ├── README │ └── npm-shrinkwrap.json ├── .versions ├── LICENSE ├── README.md ├── lib └── server │ ├── collection.js │ ├── helpers.js │ ├── oracle_collection.js │ ├── oracle_db.js │ ├── oracle_driver.js │ ├── oracle_fields.js │ ├── oracle_modifier.js │ ├── oracle_oplog_tailing.js │ ├── oracle_selector.js │ ├── oracle_sorter.js │ ├── remote_collection_driver.js │ ├── tests │ ├── collection_tests.js │ ├── oracle_db_tests.js │ ├── oracle_fields_tests.js │ ├── oracle_modifier_tests.js │ ├── oracle_selector_tests.js │ ├── oracle_sorter_tests.js │ └── oracle_tests.js │ └── wrapper.js └── package.js /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | 3 | -------------------------------------------------------------------------------- /.npm/package/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npm/package/README: -------------------------------------------------------------------------------- 1 | This directory and the files immediately inside it are automatically generated 2 | when you change this package's NPM dependencies. Commit the files in this 3 | directory (npm-shrinkwrap.json, .gitignore, and this README) to source control 4 | so that others run the same versions of sub-dependencies. 5 | 6 | You should NOT check in the node_modules directory that Meteor automatically 7 | creates; if you are using git, the .gitignore file tells git to ignore it. 8 | -------------------------------------------------------------------------------- /.npm/package/npm-shrinkwrap.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "oracledb": { 4 | "version": "1.6.0", 5 | "dependencies": { 6 | "nan": { 7 | "version": "2.1.0" 8 | } 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | babel-compiler@5.8.24_1 2 | babel-runtime@0.1.4 3 | base64@1.0.4 4 | binary-heap@1.0.4 5 | blaze@2.1.3 6 | blaze-tools@1.0.4 7 | boilerplate-generator@1.0.4 8 | callback-hook@1.0.4 9 | check@1.1.0 10 | ddp@1.2.2 11 | ddp-client@1.2.1 12 | ddp-common@1.2.2 13 | ddp-server@1.2.2 14 | deps@1.0.9 15 | diff-sequence@1.0.1 16 | ecmascript@0.1.6 17 | ecmascript-runtime@0.2.6 18 | ejson@1.0.7 19 | geojson-utils@1.0.4 20 | html-tools@1.0.5 21 | htmljs@1.0.5 22 | id-map@1.0.4 23 | jquery@1.11.4 24 | local-test:metstrike:meteor-oracle@0.2.0 25 | logging@1.0.8 26 | meteor@1.1.10 27 | metstrike:meteor-oracle@0.2.0 28 | minimongo@1.0.10 29 | mongo@1.1.3 30 | mongo-id@1.0.1 31 | npm-mongo@1.4.39_1 32 | observe-sequence@1.0.7 33 | ordered-dict@1.0.4 34 | promise@0.5.1 35 | random@1.0.5 36 | reactive-var@1.0.6 37 | retry@1.0.4 38 | routepolicy@1.0.6 39 | spacebars@1.0.7 40 | spacebars-compiler@1.0.7 41 | tinytest@1.0.6 42 | tracker@1.0.9 43 | ui@1.0.8 44 | underscore@1.0.4 45 | webapp@1.2.3 46 | webapp-hashing@1.0.5 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 AMI System LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # meteor-oracle 0.2.0 2 | This package allows thousands of enterprises to build and deploy **Meteor** applications that access their data stored in **Oracle** databases. 3 | 4 | ### Examples 5 | **Example 1:** Create Oracle.Collection and use it the same way as Mongo.Collection 6 | ```javascript 7 | var coll = new Oracle.Collection("todos"); 8 | 9 | coll.insert({name: "Get milk", priority:1, userId: "Jane"}); 10 | 11 | var rows = coll.find({priority:1}).fetch(); 12 | 13 | console.log(rows); 14 | ``` 15 | The example above executes the following SQL commands in Oracle database: 16 | ``` 17 | INSERT INTO "todos" ("name", "priority", "userId", "_id") VALUES (:0, :1, :2, :3) 18 | Parameters: [ 'Get milk', 1, 'Jane', '46662dSw2KkzpNXqo' ] 19 | 20 | SELECT * FROM (SELECT * FROM "todos") WHERE "priority" = 1 21 | ``` 22 | And will print the following rows result: 23 | ```js 24 | [ { _id: '46662dSw2KkzpNXqo', name: 'Get milk', priority: 1, userId: 'Jane' } ] 25 | ``` 26 | **Example 2:** Connect to a different Oracle account 27 | ```javascript 28 | Oracle.setDefaultOracleOptions( 29 | {connection: 30 | {user: "scott", 31 | password: "tiger", 32 | connectString : "host:1521/sid" 33 | } 34 | }); 35 | ``` 36 | **Example 3:** Turn on debug mode which prints all the executed SQL statements 37 | ```js 38 | Oracle.setDefaultOracleOptions({sqlDebug: true}); 39 | ``` 40 | **Example 4:** Prevent automatic changes in db schema (turn off elastic data model, turn on strict mode) 41 | ```js 42 | Oracle.setDefaultOracleOptions({strict: true}); 43 | ``` 44 | Prevents adding or changing any table or trigger. 45 | 46 | **Example 5:** Prevent adding or changing any DB object, except oplog table and oplog triggers 47 | ```js 48 | Oracle.setDefaultOracleOptions({strictAllowOplogAndTriggers: true}); 49 | ``` 50 | 51 | ### Installation 52 | See **Detailed Installation Instructions** below for more information. 53 | 54 | **Step 1:** Install or get access to **Oracle Database** 55 | 56 | **Step 2:** Install **Oracle Instant Client** 57 | 58 | **Step 3:** Create a meteor account in your oracle database. 59 | ```bash 60 | $ sqlplus sys as sysdba 61 | create user meteor identified by meteor; 62 | grant connect, resource to meteor; 63 | ``` 64 | **Step 4:** Add **meteor-oracle** package to your meteor project 65 | ```bash 66 | $ cd 67 | $ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/instantclient_11_2/ 68 | $ meteor add metstrike:meteor-oracle 69 | $ meteor 70 | ``` 71 | 72 | ### Run TODOS Example Application with Oracle 73 | It was very easy to convert the TODO example application. The modified version can be found here: [https://github.com/metstrike/todos-oracle](https://github.com/metstrike/todos-oracle). 74 | 75 | --- 76 | ### Approach 77 | There are several other projects trying to integrate Meteor with SQL database and this is just another attempt to fit the concept of relational database into the architecture of Meteor. The current approach allows developers to write Meteor applications the same way as if they were writing them for MongoDB. The only difference is that instead of **Mongo.Collection** they need to use **Oracle.Collection**. All the remaining code stays the same and the application should work. No database tables or indexes have to be created by developer, the driver will create and maintain them all automatically.Similarly no database triggers to populate the oplog table have to be created manually, the driver will generate them automatically whenever a new collecton is created and it will regenerate them whenever a new column is added to the collection. 78 | 79 | The simlicity and benefits of this approach can be demonstrated on existing sample Meteor applications like [TODOS](https://github.com/metstrike/todos-oracle) which can be very easily converted to work with Oracle. There is a caveat though, the developers need to be aware of current feature restrictions (and extensions) of this package. Not all features (e.g. the nested data structures) are currently supported and in the future some innovative features (e.g. the reactive $join operator) may be available in Oracle collections, while still not available in Mongo. 80 | 81 | Oracle database provides a lot of rich and robust features, that are used by thousands of enterprises accros the world. A few examples in no particular order are: inner and outer joins (duh), views, bitmap indexes, library of packages, analytical and window queries, full text indexing, high availability, partitioning, materialized views, CLOBs/BLOBs, etc. The most useful features might be gradually exposed in this package allowing developers to incorporate them into their enterprise applications. 82 | 83 | The current implementation is trying to reuse as much of existing Meteor code as possible so that it could be easily maintained in the future. The core objects (Oracle.Collection, OracleConnection) are inheriting most of the functionality from their mongo counterparts (Mongo.Collection, MongoConnection). In fact, the **Oracle.Collection** on the client side behaves 100% the same as Mongo.Collection. All the modifications are implemented on the server side only. 84 | 85 | This package is dependent on [node-oracledb](https://github.com/oracle/node-oracledb), the Oracle Database driver for Node.js maintained by Oracle Corp. All the SQL queries and commands are being sent to this module, which handles all the communication with the Oracle database. 86 | 87 | --- 88 | ### Releases 89 | ### 0.2.0 (2/1/2015) 90 | 91 | * Connectivity to external Oracle databases 92 | * Strict mode that prevents automatic changes in db schema (turns off elastic data model) 93 | * Automatic creation of OPLOG table and METEOR_PKG package 94 | * Removed dependency on DBMS_FLASHBACK 95 | 96 | ### 0.1.0 (12/15/2015) 97 | __Features:__ 98 | 99 | * **Oracle.Collection** class which behaves the same as **Mongo.Collection** 100 | * **SQL Generator** translating mongo style operations into SQL language 101 | * implemented in **OracleCollection** class 102 | * find(), findOne() operations are translated into **SELECT ... FROM ...** 103 | * most of the selector operators are supported 104 | * value operators: **$not, $eq, $ne, $exists** 105 | * element operators: **$lt, $gt, $lte, $gte, $like, $mod, $in, $nin, $regex** 106 | * logical operators: **$and, $or, $nor** 107 | * other operators: **$where, $comment** 108 | * **sorter** is supported 109 | * **field selection** is supported 110 | * **skip** and **limit** options are supported 111 | * **sqlScn** option has been added to support **flashback queries** 112 | * insert() operations are translated into **INSERT INTO ...** 113 | * remove() operations are translated into **DELETE FROM ...** 114 | * update() operations are translated into **UPDATE ... SET ...** 115 | * basic operators $inc, $set, $unset are supported 116 | * drop() operations are translated into **DROP TABLE** 117 | * ensureIndex() operations are translated into **CREATE INDEX** 118 | * drop() operations are translated into **DROP TABLE** 119 | * dropIndex() operations are translated into **DROP INDEX** 120 | * **Elastic Data Model** 121 | * the driver automatically creates and modifies **data tables** based on structure and size of data 122 | * the **primary key** constraint and related index is automaticaly created 123 | * the **new columns** are added automatically if needed 124 | * the **size** of the columns will be automatically augmented in new data would not fit 125 | * the **type** of columns gets automatically modified when possible 126 | * the **NULL/NOT NULL** constraint is automatically maintained 127 | * automatic conversion of **boolean** values to varchar2 (Oracle doesn't support boolean in their SQL language) 128 | * **Oplog Tailing** 129 | * the driver creates **oplog table** in the meteor schema 130 | * automatic creation and maintenance of **database triggers** that populate the **oplog** table in real time 131 | 132 | __Restrictons:__ 133 | 134 | * only linux environment is supported at this moment 135 | * column data types have to be consistent 136 | * nested data structures are not suported 137 | * some operators in find() selector are not implemented yet 138 | * $maxDistance, $all, $near, $options, regexp inside $in or $nin, $size, $type 139 | * some operators in update() selector are not implemented yet 140 | * $setOnInsert, $push, $pushAll, $addToSet, $pop, $pull, $pullAll, $rename, $bit, $elemMatch 141 | 142 | ### Future Improvements 143 | * use node-oracledb connection pool 144 | * use oracle resultsets to implement collection cursor 145 | * refresh table meta-data after any error occurred (maybe separate ORA errors should be considered) 146 | * refresh table meta-data after any DDL change is made by an external entity 147 | * support strict mode (driver will not make any DDL changes in the database) 148 | * support nested data structures 149 | * design reactive joins 150 | 151 | ### License 152 | Released under the MIT license. See the LICENSE file for more info. 153 | 154 | __Copyright (c) 2015 AMI System LLC__ 155 | - - - 156 | ### Detailed Installation Instructions 157 | **Step 1:** Install or get access to **Oracle Database** 158 | 159 | * It is very likely that you already have your Oracle database installed and this step can be skipped 160 | * If that is not a case you can easily download and install free [Oracle Database 11g Express Edition](http://www.oracle.com/technetwork/database/database-technologies/express-edition/downloads/index.html). 161 | 162 | **Step 2:** Install **Oracle Instant Client** 163 | 164 | * **node_oracledb** package, which is used for all database operations, requires free Oracle Instant Client libraries. 165 | * See https://github.com/oracle/node-oracledb for more information about node.js package "node-oracledb" 166 | * Detailed Installation Steps: 167 | * Go to http://www.oracle.com/technetwork/database/features/instant-client/index-097480.html 168 | * Choose the right Instant Client package for your platform 169 | * Accept Oracle's [OTN Development and Distribution License Agreement](http://www.oracle.com/technetwork/licenses/instant-client-lic-152016.html) for Instant Client 170 | * Find the version that matches your oracle database instance and download the corresponding zip file 171 | * Run sqlplus to determine the exact version of your database 172 | 173 | ```shell 174 | $ sqlplus 175 | 176 | SQL*Plus: Release 11.2.0.2.0 Production on Mon Dec 14 16:08:49 2015 177 | 178 | Copyright (c) 1982, 2011, Oracle. All rights reserved. 179 | 180 | Enter user-name: meteor 181 | Enter password: 182 | 183 | Connected to: 184 | Oracle Database 11g Express Edition Release 11.2.0.2.0 - 64bit Production 185 | 186 | SQL> 187 | ``` 188 | 189 | * (Detailed Installation Steps continued) 190 | * for example, for Oracle XE 11.2.0.2.0 on linux x64 you choose [instantclient-basic-linux-x86-64-11.2.0.2.0.zip](http://download.oracle.com/otn/linux/instantclient/112020/instantclient-basic-linux-x86-64-11.2.0.2.0.zip) 191 | * Login to your Oracle account. If you don't have one, you need to create it. It is free. 192 | * Unzip the downloded file into your workspace directory e.g. **~/workspace/** 193 | * The new subdirectory e.g. **~/workspace/instantclient_11_2** will be created 194 | * Environment variable **LD_LIBRARY_PATH** has to be set accordingly 195 | ```shell 196 | $ export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:~/workspace/instantclient_11_2 197 | ``` 198 | **Step 3:** Create a meteor account in your oracle database. 199 | ``` 200 | $ sqlplus sys as sysdba 201 | create user meteor identified by meteor; 202 | grant connect, resource to meteor; 203 | ``` 204 | Make sure that this user has privileges to use the default tablespace. In case of issues you can use the following command: 205 | ``` 206 | grant unlimited tablespace to meteor; 207 | ``` 208 | **Step 4:** Verify that your meteor account has been created: 209 | ```bash 210 | $ sqlplus meteor/meteor 211 | 212 | SQL*Plus: Release 11.2.0.2.0 Production on Mon Dec 14 17:37:40 2015 213 | 214 | Copyright (c) 1982, 2011, Oracle. All rights reserved. 215 | 216 | 217 | Connected to: 218 | Oracle Database 11g Express Edition Release 11.2.0.2.0 - 64bit Production 219 | 220 | SQL> exit 221 | ``` 222 | **Step 4:** Add **meteor-oracle** package to your meteor project 223 | ```bash 224 | $ cd 225 | $ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/instantclient_11_2/ 226 | $ meteor add metstrike:meteor-oracle 227 | $ meteor 228 | ``` 229 | - - - 230 | ### Get the Idea 231 | Let's get back to the simple example we listed at the top of this document: 232 | ```javascript 233 | var coll = new Oracle.Collection("todos"); 234 | 235 | coll.insert({name: "Get milk", priority:1, userId: "Jane"}); 236 | 237 | var rows = coll.find({priority:1}).fetch(); 238 | 239 | console.log(rows); 240 | ``` 241 | The **meteor-oracle** driver will not only generate the **insert** and **select** statements, it will also create database table (if it doesn't exist yet), its primary key constraint, and the trigger that will populate the **oplog** table. The complete list of generated SQL statements looks as follows: 242 | ``` 243 | create table "todos" ("_id" varchar2(17) not null) 244 | 245 | alter table "todos" add constraint "todos_PK" primary key ("_id") 246 | 247 | INSERT: todos { name: 'Get milk', priority: 1, userId: 'Jane', _id: '46662dSw2KkzpNXqo' } { safe: true } 248 | 249 | alter table "todos" add ("name" varchar2(8) not null) [] 250 | alter table "todos" add ("priority" number(1) not null) [] 251 | alter table "todos" add ("userId" varchar2(4) not null) [] 252 | 253 | create or replace trigger "todos_trg" 254 | after insert or update or delete 255 | on "todos" 256 | for each row 257 | declare 258 | op varchar2(1); 259 | ns varchar2(200); 260 | o varchar2(4000); 261 | o2 varchar2(4000); 262 | begin 263 | IF INSERTING THEN 264 | op := 'i'; 265 | ns := 'meteor@localhost/XE'||'.'||'todos'; 266 | o := ''; 267 | o := o || '"_id": "'||replace(replace(:NEW."_id", chr(10), '\n'), '"', '\"')||'"'; 268 | o := o || ', '; 269 | o := o || '"name": "'||replace(replace(:NEW."name", chr(10), '\n'), '"', '\"')||'"'; 270 | o := o || ', '; 271 | o := o || '"priority": '||nvl(meteor_pkg.js_number(:NEW."priority"), 'null'); 272 | o := o || ', '; 273 | o := o || '"userId": "'||replace(replace(:NEW."userId", chr(10), '\n'), '"', '\"')||'"'; 274 | o := '{'||o||'}'; 275 | o2 := null; 276 | insert into "oplog" ("id", "ts", "scn", "tr", "v", "op", "ns", "o", "o2") 277 | values ("oplog_seq".nextval, current_timestamp, dbms_flashback.get_system_change_number, dbms_transaction.local_transaction_id, 2, op, ns, o, o2); 278 | ELSIF UPDATING THEN 279 | op := 'u'; 280 | ns := 'meteor@localhost/XE'||'.'||'todos'; 281 | o := ''; 282 | IF (:NEW."name" <> :OLD."name" OR (:NEW."name" IS NOT NULL AND :OLD."name" IS NULL) 283 | OR (:NEW."name" IS NULL AND :OLD."name" IS NOT NULL)) THEN 284 | IF o is not null THEN := o || ', '; END IF; 285 | o := o || '"name": "'||replace(replace(:NEW."name", chr(10), '\n'), '"', '\"')||'"'; 286 | END IF; 287 | IF (:NEW."priority" <> :OLD."priority" OR (:NEW."priority" IS NOT NULL AND :OLD."priority" IS NULL) 288 | OR (:NEW."priority" IS NULL AND :OLD."priority" IS NOT NULL)) THEN 289 | IF o is not null THEN o := o || ', '; END IF; 290 | o := o || '"priority": '||nvl(meteor_pkg.js_number(:NEW."priority"), 'null'); 291 | END IF; 292 | IF (:NEW."userId" <> :OLD."userId" OR (:NEW."userId" IS NOT NULL AND :OLD."userId" IS NULL) 293 | OR (:NEW."userId" IS NULL AND :OLD."userId" IS NOT NULL)) THEN 294 | IF o is not null THEN o := o || ', '; END IF; 295 | o := o || '"userId": "'||replace(replace(:NEW."userId", chr(10), '\n'), '"', '\"')||'"'; 296 | END IF; 297 | IF o is not null THEN 298 | o := '{"$set": {'||o||'}}'; 299 | o2 := ''; 300 | o2 := o2 || '"_id": "'||replace(replace(:OLD."_id", chr(10), '\n'), '"', '\"')||'"'; 301 | o2 := '{'||o2||'}'; 302 | insert into "oplog" ("id", "ts", "scn", "tr", "v", "op", "ns", "o", "o2") 303 | values ("oplog_seq".nextval, current_timestamp, dbms_flashback.get_system_change_number, dbms_transaction.local_transaction_id, 2, op, ns, o, o2); 304 | END IF; 305 | ELSIF DELETING THEN 306 | op := 'd'; 307 | ns := 'meteor@localhost/XE'||'.'||'todos'; 308 | o := ''; 309 | o := o || '"_id": "'||replace(replace(:OLD."_id", chr(10), '\n'), '"', '\"')||'"'; 310 | o := '{'||o||'}'; 311 | o2 := null; 312 | insert into "oplog" ("id", "ts", "scn", "tr", "v", "op", "ns", "o", "o2") 313 | values ("oplog_seq".nextval, current_timestamp, dbms_flashback.get_system_change_number, dbms_transaction.local_transaction_id, 2, op, ns, o, o2); 314 | END IF; 315 | end; 316 | 317 | INSERT INTO "todos" ("name", "priority", "userId", "_id") VALUES (:0, :1, :2, :3) 318 | Parameters: [ 'Get milk', 1, 'Jane', '46662dSw2KkzpNXqo' ] 319 | 320 | SELECT * FROM (SELECT * FROM "todos") WHERE "priority" = 1 321 | ``` 322 | - - - 323 | The end of the document. 324 | -------------------------------------------------------------------------------- /lib/server/collection.js: -------------------------------------------------------------------------------- 1 | // options.connection, if given, is a LivedataClient or LivedataServer 2 | // XXX presently there is no way to destroy/clean up a Collection 3 | 4 | /** 5 | * @summary Namespace for OracleDB-related items 6 | * @namespace 7 | */ 8 | Oracle = {}; 9 | 10 | Oracle.Collection = function (name, options, oracleOptions) { 11 | options = options || {}; 12 | 13 | if (!options._driver) { 14 | if (Meteor.isServer) { 15 | options._driver = OracleInternals.defaultRemoteCollectionDriver(); 16 | } 17 | } 18 | 19 | Mongo.Collection.call(this, name, options); 20 | 21 | if (Meteor.isServer) { 22 | this._collection.oracleOptions = oracleOptions; 23 | } 24 | }; 25 | 26 | //extend from parent class prototype 27 | Oracle.Collection.prototype = Object.create(Mongo.Collection.prototype); // keeps the proto clean 28 | Oracle.Collection.prototype.constructor = Oracle.Collection; // repair the inherited constructor 29 | 30 | if(Meteor.isServer) { 31 | 32 | // 33 | //Default Oracle Options 34 | // 35 | Oracle._defaultOracleOptions = null; 36 | 37 | Oracle.resetDefaultOracleOptions = function(oracleOptions) { 38 | Oracle._defaultOracleOptions = oracleOptions; 39 | 40 | // Clear some items which do not make sense 41 | Oracle._defaultOracleOptions.sql = null; 42 | Oracle._defaultOracleOptions.sqlParameters = []; 43 | }; 44 | 45 | Oracle.setDefaultOracleOptions = function(oracleOptions) { 46 | Oracle._defaultOracleOptions = Oracle._mergeOptions(oracleOptions, Oracle._defaultOracleOptions); 47 | }; 48 | 49 | Oracle.getDefaultOracleOptions = function() { 50 | var ret = EJSON.clone(Oracle._defaultOracleOptions); 51 | 52 | // Remove the password 53 | if(ret.connection) { 54 | delete ret.connection.password; 55 | } 56 | 57 | return ret; 58 | }; 59 | 60 | Oracle._mergeOptions = function(oracleOptions, defaultOracleOptions) { 61 | var o = {}; 62 | 63 | for(var a in defaultOracleOptions) { 64 | o[a] = defaultOracleOptions[a]; 65 | } 66 | 67 | for(var a in oracleOptions) { 68 | o[a] = oracleOptions[a]; 69 | } 70 | 71 | return o; 72 | }; 73 | 74 | Oracle.resetDefaultOracleOptions({ 75 | connection: { 76 | user: "meteor", 77 | password: "meteor", 78 | connectString: "localhost/XE" 79 | }, 80 | strict: false, // strict mode, if true prevents automatic modification of db schema 81 | strictAllowOplogAndTriggers: false, // strict allows creation of oplog table and opllog triggers 82 | sql: null, 83 | sqlParameters: [], 84 | sqlScn: null, 85 | sqlAddId: false, 86 | sqlDebug: false, 87 | sqlConvertColumnNames: true, // Not implemented yet, for future use 88 | booleanTrueValue: "true", 89 | booleanFalseValue: "false" 90 | }); 91 | } 92 | 93 | Oracle.LocalDate = function (dateStr) { 94 | var utcDate = new Date(dateStr); 95 | 96 | var localDate = new Date(utcDate.getTime()+utcDate.getTimezoneOffset()*60*1000); 97 | 98 | return localDate; 99 | }; 100 | -------------------------------------------------------------------------------- /lib/server/helpers.js: -------------------------------------------------------------------------------- 1 | // Like _.isArray, but doesn't regard polyfilled Uint8Arrays on old browsers as 2 | // arrays. 3 | // XXX maybe this should be EJSON.isArray 4 | isArray = function (x) { 5 | return _.isArray(x) && !EJSON.isBinary(x); 6 | }; 7 | 8 | // XXX maybe this should be EJSON.isObject, though EJSON doesn't know about 9 | // RegExp 10 | // XXX note that _type(undefined) === 3!!!! 11 | isPlainObject = LocalCollection._isPlainObject = function (x) { 12 | return x && LocalCollection._f._type(x) === 3; 13 | }; 14 | 15 | isIndexable = function (x) { 16 | return isArray(x) || isPlainObject(x); 17 | }; 18 | 19 | // Returns true if this is an object with at least one key and all keys begin 20 | // with $. Unless inconsistentOK is set, throws if some keys begin with $ and 21 | // others don't. 22 | isOperatorObject = function (valueSelector, inconsistentOK) { 23 | if (!isPlainObject(valueSelector)) 24 | return false; 25 | 26 | var theseAreOperators = undefined; 27 | _.each(valueSelector, function (value, selKey) { 28 | var thisIsOperator = selKey.substr(0, 1) === '$'; 29 | if (theseAreOperators === undefined) { 30 | theseAreOperators = thisIsOperator; 31 | } else if (theseAreOperators !== thisIsOperator) { 32 | if (!inconsistentOK) 33 | throw new Error("Inconsistent operator: " + 34 | JSON.stringify(valueSelector)); 35 | theseAreOperators = false; 36 | } 37 | }); 38 | return !!theseAreOperators; // {} has no operators 39 | }; 40 | 41 | 42 | // string can be converted to integer 43 | isNumericKey = function (s) { 44 | return /^[0-9]+$/.test(s); 45 | }; 46 | -------------------------------------------------------------------------------- /lib/server/oracle_collection.js: -------------------------------------------------------------------------------- 1 | 2 | OracleCollection = function (db, collectionName, oracleOptions) { 3 | var self = this; 4 | 5 | self.db = db; 6 | self.collectionName = collectionName; 7 | self.oracleOptions = oracleOptions; 8 | 9 | if(self.oracleOptions.sqlTable === undefined) { 10 | self.oracleOptions.sqlTable = self.collectionName; 11 | } 12 | self.oracleOptions.sql = self.oracleOptions.sql; 13 | }; 14 | 15 | OracleCollection.prototype.find = function(selector, fields, options) { 16 | var self = this; 17 | 18 | if(self.oracleOptions.sqlDebug) { 19 | if(self.oracleOptions.sqlTable !== "oplog") 20 | console.log("FIND: ", self.oracleOptions.sqlTable, selector, fields, options); 21 | } 22 | 23 | // Construct the SQL 24 | var sql = self.oracleOptions.sql; 25 | var sqlTable = null; 26 | 27 | if(!sql) { 28 | sqlTable = self.oracleOptions.sqlTable; 29 | if(!sqlTable) { 30 | throw new Error("Missing sqlTable in oracle options"); 31 | } 32 | sql = "SELECT * FROM " + OracleDB.q(sqlTable); 33 | } 34 | 35 | var sqlParameters = EJSON.clone(self.oracleOptions.sqlParameters) || []; 36 | 37 | if(self.oracleOptions.sqlAddId) { 38 | sql = "SELECT rownum as "+OracleDB.q("_id")+", c.* FROM ("+sql+") c"; 39 | } 40 | 41 | var originalSelector = selector; 42 | selector = OracleSelector.process(selector, sqlTable, self.db); 43 | 44 | if(selector && selector["."]) { 45 | var where = selector["."]; 46 | 47 | if(where && where !== "") { 48 | sql = "SELECT * FROM ("+sql+") WHERE "+where; 49 | } 50 | } 51 | 52 | var sorter = undefined; 53 | var orderBy = ""; 54 | if(options.sort) { 55 | sorter = OracleSorter.process(options.sort, options); 56 | orderBy = sorter["."]; 57 | } 58 | 59 | if(orderBy && orderBy != "") { 60 | sql = sql + " ORDER BY " + orderBy; 61 | } 62 | 63 | var cl = OracleFields._prepare(fields); 64 | 65 | // Adding fields projection here 66 | if(cl["."]) { 67 | var s = OracleFields.getColumnList(self.db._tables[sqlTable], cl["."]); 68 | sql = "SELECT "+s+" FROM ("+sql+")"; 69 | } 70 | 71 | if(options.skip) { 72 | sql = "SELECT rownum as rowno, c.* FROM ("+sql+") c"; 73 | sql = "SELECT * FROM ("+sql+") WHERE rowno > :skip"; 74 | sqlParameters.push(options.skip); 75 | } 76 | 77 | if(options.limit) { 78 | sql = "SELECT * FROM ("+sql+") WHERE rownum <= :limit"; 79 | sqlParameters.push(options.limit); 80 | } 81 | 82 | var sql0 = sql; 83 | 84 | if(self.oracleOptions.sqlScn) { 85 | sql = "SELECT * FROM ("+sql+") AS OF SCN :scn"; 86 | sqlParameters.push(self.oracleOptions.sqlScn); 87 | } 88 | 89 | var details = []; 90 | 91 | if(sqlTable) { 92 | var tableDesc = self.db._tables[sqlTable]; 93 | 94 | self.db._ensureSelectableColumns(sqlTable, selector["*"], self.oracleOptions); 95 | 96 | for(var di in tableDesc.details) { 97 | var detRec = tableDesc.details[di]; 98 | var detTableName = detRec.tableName; 99 | 100 | self.db._ensureTable(detTableName, self.oracleOptions); 101 | 102 | // Exclude detail if it's not in the field list 103 | if(fields && fields[detRec.fieldName] !== undefined && !fields[detRec.fieldName]) { 104 | continue; 105 | } 106 | 107 | var detSql = "SELECT * FROM "+OracleDB.q(detTableName)+" WHERE "+OracleDB.q("_id")+" IN (SELECT "+OracleDB.q("_id")+" FROM ("+sql0+"))"; 108 | var detSqlParameters = []; 109 | 110 | if(selector && selector[detRec.fieldName] && selector[detRec.fieldName]["."]) { 111 | self.db._ensureSelectableColumns(detTableName, selector[detRec.fieldName]["*"], self.oracleOptions); 112 | 113 | var where = selector[detRec.fieldName]["."]; 114 | 115 | if(where && where !== "") { 116 | detSql = detSql + " AND " + where; 117 | } 118 | } 119 | 120 | if(sorter && sorter[detRec.fieldName] && sorter[detRec.fieldName]["."]) { 121 | detSql = detSql + " ORDER BY " + sorter[detRec.fieldName]["."] + ", "+OracleDB.q("_id")+", "+OracleDB.q("_indexNo"); 122 | } else { 123 | detSql = detSql + " ORDER BY "+OracleDB.q("_id")+", "+OracleDB.q("_indexNo"); 124 | } 125 | 126 | // Adding fields projection here 127 | var dcl = cl[detRec.fieldName]; 128 | 129 | if(dcl && dcl["."]) { 130 | var s = OracleFields.getColumnList(self.db._tables[detTableName], dcl["."], true); 131 | 132 | detSql = "SELECT "+s+" FROM ("+detSql+")"; 133 | } 134 | 135 | for(var i in sqlParameters) { 136 | detSqlParameters.push(sqlParameters[i]); 137 | } 138 | if(self.oracleOptions.sqlScn) { 139 | detSql = "SELECT * FROM ("+sql+") AS OF SCN :scn"; 140 | detSqlParameters.push(self.oracleOptions.sqlScn); 141 | } 142 | details.push({detailRecord: detRec, sql:detSql, sqlParameters:detSqlParameters}); 143 | } 144 | }; 145 | 146 | var dbCursor = new OracleCollection.Cursor(self, sql, sqlParameters, details, originalSelector, fields, options); 147 | 148 | return dbCursor; 149 | }; 150 | 151 | OracleCollection.prototype.insert = function(doc, options, callback) { 152 | var self = this; 153 | 154 | if(self.oracleOptions.sqlDebug) { 155 | console.log("INSERT: ", self.oracleOptions.sqlTable, doc, options); 156 | } 157 | 158 | var batch = []; 159 | 160 | self._insert(doc, self.oracleOptions.sqlTable, options, callback, null, null, null, batch); 161 | 162 | result = self.db.executeBatch(batch, self.oracleOptions); 163 | 164 | if(callback) callback(null, result); 165 | 166 | return result; 167 | }; 168 | 169 | // makes batch of all inserts to be executed within a single transaction 170 | OracleCollection.prototype._insert = function(doc, sqlTable, options, callback, parentSqlTable, parent_id, index_no, batch) { 171 | var self = this; 172 | var cmds = {}; 173 | 174 | var colList = ""; 175 | var valList = ""; 176 | 177 | var sqlParameters = []; 178 | var i = 0; 179 | var deferred = []; 180 | 181 | if(parentSqlTable) { 182 | colList = OracleDB.q("_id")+", "+OracleDB.q("_indexNo"); 183 | valList = ":0, :1"; 184 | sqlParameters.push(parent_id); 185 | sqlParameters.push(index_no); 186 | i = 2; 187 | } 188 | 189 | for(var column in doc) { 190 | var value = doc[column]; 191 | 192 | if(value instanceof Array) { 193 | var sqlTable2 = sqlTable + "$" + OracleDB._tableNameM2O(column); 194 | 195 | // Detail table 196 | for(var index in value) { 197 | var doc2 = value[index]; 198 | 199 | if(doc2 instanceof Object) { 200 | // Standard situation 201 | } else if(doc2 instanceof Array) { 202 | throw new Error("Embedded arrays are not supported.") 203 | } else { 204 | doc2 = {"_value": doc2}; 205 | } 206 | deferred.push([doc2, sqlTable2, options, null, sqlTable, doc["_id"], index, batch]); 207 | } 208 | continue; 209 | } else if(value instanceof Date) { 210 | // Fall through 211 | } else if(value instanceof Object) { 212 | var sqlTable2 = sqlTable + "$" + OracleDB._tableNameM2O(column); 213 | 214 | deferred.push([value, sqlTable2, options, null, sqlTable, doc["_id"], 0, batch]); 215 | continue; 216 | } 217 | 218 | column = OracleDB._columnNameM2O(column); 219 | 220 | column = OracleDB.q(column); 221 | 222 | if(i > 0) { 223 | colList = colList + ", "; 224 | valList = valList + ", "; 225 | } 226 | colList = colList + column; 227 | valList = valList + ":" + i; 228 | if(typeof value === 'boolean') { 229 | if(value === true) { 230 | value = self.oracleOptions.booleanTrueValue; 231 | } else if(value === false) { 232 | value = self.oracleOptions.booleanFalseValue; 233 | } 234 | } 235 | sqlParameters.push(value); 236 | i++; 237 | } 238 | 239 | var sql = "INSERT INTO "+OracleDB.q(sqlTable)+" ("+colList+") VALUES ("+valList+")" 240 | 241 | self.db._ensureColumns(sqlTable, doc, self.oracleOptions, parentSqlTable, true); 242 | 243 | batch.push({sql: sql, sqlParameters: sqlParameters}); 244 | 245 | // Process deferred details 246 | for(var di in deferred) { 247 | var dvi = deferred[di]; 248 | OracleCollection.prototype._insert.apply(self, dvi); 249 | } 250 | }; 251 | 252 | OracleCollection.prototype.remove = function(selector, options, callback) { 253 | var self = this; 254 | 255 | if(self.oracleOptions.sqlDebug) { 256 | console.log("REMOVE: ", self.oracleOptions.sqlTable, selector, options); 257 | } 258 | 259 | var sqlTable = self.oracleOptions.sqlTable; 260 | if(!sqlTable) { 261 | throw new Error("Missing sqlTable in oracle options for remove operation"); 262 | } 263 | var sql = "DELETE FROM " + OracleDB.q(sqlTable); 264 | 265 | var sqlParameters = []; 266 | 267 | selector = OracleSelector.process(selector, sqlTable, self.db); 268 | 269 | if(selector && selector["."]) { 270 | var where = selector["."]; 271 | 272 | if(where && where !== "") { 273 | sql = sql+" WHERE "+where; 274 | } 275 | } 276 | 277 | self.db._ensureSelectableColumns(sqlTable, selector["*"], self.oracleOptions); 278 | 279 | var result = self.db.executeCommand(sql, sqlParameters, self.oracleOptions); 280 | 281 | if(callback) { 282 | callback(null, result); 283 | } 284 | 285 | return result; 286 | }; 287 | 288 | OracleCollection.prototype.update = function(selector, modifier, options, callback) { 289 | var self = this; 290 | var result = undefined; 291 | 292 | if(self.oracleOptions.sqlDebug) { 293 | console.log("UPDATE: ", self.oracleOptions.sqlTable, selector, modifier, options); 294 | } 295 | 296 | var sqlTable = self.oracleOptions.sqlTable; 297 | if(!sqlTable) { 298 | throw new Error("Missing sqlTable in oracle options for remove operation"); 299 | } 300 | 301 | modifier = OracleModifier.process(modifier, sqlTable); 302 | 303 | selector = OracleSelector.process(selector, sqlTable, self.db); 304 | 305 | var batch = []; 306 | 307 | if(modifier && modifier["."]) { 308 | var sql = "UPDATE " + OracleDB.q(sqlTable) + " SET "; 309 | 310 | sql = sql + modifier["."]; 311 | 312 | var sqlParameters = modifier["$"]; 313 | 314 | if(selector && selector["."]) { 315 | var where = selector["."]; 316 | 317 | if(where && where !== "") { 318 | sql = sql+" WHERE "+where; 319 | } 320 | } 321 | 322 | self.db._ensureColumns(sqlTable, modifier["*"], self.oracleOptions); 323 | self.db._ensureSelectableColumns(sqlTable, selector["*"], self.oracleOptions); 324 | 325 | batch.push({sql: sql, sqlParameters: sqlParameters}); 326 | } 327 | 328 | // Process deferred operations 329 | if(modifier && modifier["@"]) { 330 | var ops = modifier["@"]; 331 | 332 | // TODO: make sure all these deferred operation are executed in a single transaction 333 | for(var i = 0; i < ops.length; i++) { 334 | var op = ops[i]; 335 | 336 | if(op.op === "insert") { 337 | var maxSql = '(SELECT NVL(MAX("_indexNo"), -1)+1 as "_indexNo" FROM ' + 338 | OracleDB.q(op.table) + 339 | ' "_detail" WHERE "_detail"."_id" = "_master"."_id")'; 340 | var sql = "INSERT INTO " + OracleDB.q(op.table) + 341 | '("_id", "_indexNo", "_value")' + 342 | ' SELECT "_master"."_id" as "_id", '+maxSql+' as "_indexNo", :0 as "_value" FROM '; 343 | 344 | sql = sql + '(SELECT "_id" FROM ' + OracleDB.q(sqlTable); 345 | 346 | if(selector && selector["."]) { 347 | var where = selector["."]; 348 | 349 | if(where && where !== "") { 350 | sql = sql+" WHERE "+where; 351 | } 352 | } 353 | 354 | sql = sql + ') "_master"'; 355 | 356 | var sqlParameters = [op.value]; 357 | 358 | self.db._ensureTable(op.table, self.oracleOptions, sqlTable); 359 | self.db._ensureColumns(op.table, {_value: op.value}, self.oracleOptions); 360 | 361 | batch.push({sql: sql, sqlParameters: sqlParameters}); 362 | } else if(op.op === "add") { 363 | var maxSql = '(SELECT NVL(MAX("_indexNo"), -1)+1 as "_indexNo" FROM ' + 364 | OracleDB.q(op.table) + 365 | ' "_detail" WHERE "_detail"."_id" = "_master"."_id")'; 366 | var sql = "INSERT INTO " + OracleDB.q(op.table) + 367 | '("_id", "_indexNo", "_value")' + 368 | ' SELECT "_master"."_id" as "_id", '+maxSql+' as "_indexNo", :0 as "_value" FROM '; 369 | 370 | sql = sql + '(SELECT "_id" FROM ' + OracleDB.q(sqlTable); 371 | 372 | if(selector && selector["."]) { 373 | var where = selector["."]; 374 | 375 | if(where && where !== "") { 376 | sql = sql+" WHERE "+where; 377 | } 378 | } 379 | 380 | sql = sql + ') "_master"'; 381 | sql = sql + ' WHERE NOT EXISTS ' + 382 | '(SELECT 1 FROM ' + 383 | OracleDB.q(op.table) + 384 | ' "_detail" WHERE "_detail"."_id" = "_master"."_id" and "_detail"."_value" = :1)'; 385 | 386 | var sqlParameters = [op.value, op.value]; 387 | 388 | self.db._ensureTable(op.table, self.oracleOptions, sqlTable); 389 | self.db._ensureColumns(op.table, {_value: op.value}, self.oracleOptions); 390 | 391 | batch.push({sql: sql, sqlParameters: sqlParameters}); 392 | } else if(op.op === "delete") { 393 | 394 | var sql = "DELETE FROM " + OracleDB.q(op.table) + ' WHERE "_id" IN '; 395 | sql = sql + '(SELECT "_id" FROM ' + OracleDB.q(sqlTable); 396 | 397 | if(selector && selector["."]) { 398 | var where = selector["."]; 399 | 400 | if(where && where !== "") { 401 | sql = sql+" WHERE "+where; 402 | } 403 | } 404 | 405 | sql = sql + ')'; 406 | 407 | if(op.selector) { 408 | var innerSelector = OracleSelector.process(op.selector, op.table, self.db); 409 | 410 | if(innerSelector && innerSelector["."]) { 411 | sql = sql + " AND " + innerSelector["."]; 412 | } 413 | } 414 | 415 | var sqlParameters = []; 416 | 417 | self.db._ensureTable(op.table, self.oracleOptions, sqlTable); 418 | 419 | batch.push({sql: sql, sqlParameters: sqlParameters}); 420 | } else { 421 | throw new Error("OracleCollection.update: Unknown deferred operation: "+ op.op); 422 | } 423 | } 424 | } 425 | 426 | result = self.db.executeBatch(batch, self.oracleOptions); 427 | 428 | if(callback) callback(null, result); 429 | 430 | return result; 431 | }; 432 | 433 | OracleCollection.Cursor = function (collection, sql, sqlParameters, details, selector, fields, options) { 434 | var self = this; 435 | self.collection = collection; 436 | self.sql = sql; 437 | self.sqlParameters = sqlParameters; 438 | self.details = details; 439 | 440 | self._selector = selector; 441 | self._field = fields; 442 | self._options = options; 443 | 444 | self.objects = null; 445 | self.nextObjectIndex = 0; 446 | self.loaded = false; 447 | }; 448 | 449 | OracleCollection.Cursor.prototype.nextObject = function (callback) { 450 | var self = this; 451 | var err = null; 452 | var r = null; 453 | 454 | if(!self.loaded) { 455 | self._loadObjects(); 456 | self.loaded = true; 457 | } 458 | 459 | if(self.nextObjectIndex < self.objects.length) { 460 | r = self.objects[self.nextObjectIndex++]; 461 | } 462 | 463 | callback(err, r); 464 | }; 465 | 466 | OracleCollection.Cursor.prototype.count = function (callback) { 467 | var self = this; 468 | var err = null; 469 | var cnt = null; 470 | 471 | if(!self.loaded) { 472 | err = self._loadObjects(); 473 | self.loaded = true; 474 | } 475 | 476 | cnt = self.objects.length; 477 | 478 | callback(err, cnt); 479 | }; 480 | 481 | OracleCollection.Cursor.prototype.rewind = function () { 482 | var self = this; 483 | 484 | self.loaded = false; 485 | self.objects = []; 486 | self.nextObjectIndex = 0; 487 | }; 488 | 489 | OracleCollection.Cursor.prototype._loadObjects = function () { 490 | var self = this; 491 | 492 | var result = undefined; 493 | 494 | try { 495 | result = self.collection.db.executeCommand(self.sql, self.sqlParameters, self.collection.oracleOptions); 496 | } catch(ex) { 497 | console.log("ERROR in FIND: ", {selector: self._selector, fields: self._fields, options: self._options}); 498 | throw ex; 499 | } 500 | 501 | self.objects = result.records; 502 | 503 | if(self.details && self.details.length > 0) { 504 | // Make an inverted map of records 505 | var recMap = {}; 506 | for(var i in self.objects) { 507 | var rec = self.objects[i]; 508 | 509 | recMap[rec._id] = rec; 510 | } 511 | 512 | for(var i in self.details) { 513 | var detail = self.details[i]; 514 | var detailCursor = new OracleCollection.Cursor(self.collection, detail.sql, detail.sqlParameters, null); 515 | detailCursor._loadObjects(); 516 | for(var obji in detailCursor.objects) { 517 | var objRec = detailCursor.objects[obji]; 518 | 519 | var targetRec = recMap[objRec._id]; 520 | if(!targetRec) { 521 | throw new Error("Queries with nested data must include _id field, SQL="+self.sql); 522 | continue; 523 | } 524 | var detField = detail.detailRecord.fieldName; 525 | var targetField = targetRec[detField]; 526 | if(!targetField) { 527 | targetField = []; 528 | targetRec[detField] = targetField; 529 | } 530 | 531 | delete objRec._id; 532 | delete objRec._indexNo; 533 | 534 | var cnt = Object.keys(objRec).length; 535 | if(cnt === 0) { 536 | continue; 537 | } 538 | 539 | if(objRec._value !== undefined && cnt === 1) { 540 | objRec = objRec._value; 541 | } 542 | targetField.push(objRec); 543 | } 544 | } 545 | } 546 | }; 547 | 548 | OracleCollection.prototype.drop = function(callback) { 549 | var self = this; 550 | 551 | var sqlTable = self.oracleOptions.sqlTable; 552 | if(!sqlTable) { 553 | throw new Error("Missing sqlTable in oracle options for drop operation"); 554 | } 555 | 556 | var tableDesc = self.db._tables[sqlTable]; 557 | 558 | var detResults = []; 559 | 560 | for(var di in tableDesc.details) { 561 | var detRec = tableDesc.details[di]; 562 | var detTableName = detRec.tableName; 563 | 564 | var oracleOptions = EJSON.clone(self.oracleOptions); 565 | delete oracleOptions.sqlTable; 566 | 567 | var detColl = self.db.collection(detTableName, oracleOptions); 568 | var result = detColl.drop(); 569 | 570 | detResults.push(result); 571 | } 572 | 573 | delete self.db._tables[sqlTable]; 574 | 575 | var sql = "DROP TABLE " + OracleDB.q(sqlTable); 576 | 577 | var sqlParameters = []; 578 | 579 | var result = self.db.executeCommand(sql, sqlParameters, self.oracleOptions); 580 | 581 | result.detResults = detResults; 582 | 583 | if(callback) { 584 | callback(null, result); 585 | } 586 | 587 | return result; 588 | }; 589 | 590 | OracleCollection.prototype._ensureIndexPrepared = function(sqlTable, cl, options) { 591 | var self = this; 592 | var results = {}; 593 | 594 | for(fn in cl) { 595 | var value = cl[fn]; 596 | 597 | if(fn === ".") { 598 | self.db._ensureSelectableColumns(sqlTable, value, self.oracleOptions) 599 | 600 | var tableDesc = self.db._tables[sqlTable]; 601 | 602 | var s = OracleFields.getIndexColumnList(tableDesc, cl["."], false, true); 603 | 604 | if(!s) { 605 | continue; 606 | } 607 | 608 | var ids = OracleFields.getColumnIdList(tableDesc, cl["."]); 609 | var indexName = sqlTable+"_I"+ids; 610 | 611 | if(tableDesc.indexes[indexName]) { 612 | // Index already exists, check its status 613 | if(tableDesc.indexes[indexName].status === "VALID") { 614 | continue; 615 | } else { 616 | var sql = "DROP INDEX "+OracleDB.q(indexName); 617 | var sqlParameters = []; 618 | 619 | results[fn] = self.db.executeCommand(sql, sqlParameters, self.oracleOptions); 620 | } 621 | } 622 | 623 | var sql = "CREATE "+(options.bitmap?"BITMAP ":"")+(options.unique?"UNIQUE ":"")+"INDEX "+OracleDB.q(indexName)+" ON "+OracleDB.q(sqlTable)+"("+s+")"; 624 | var sqlParameters = []; 625 | 626 | results[fn] = self.db.executeCommand(sql, sqlParameters, self.oracleOptions); 627 | 628 | self.db._refreshTable(sqlTable, self.oracleOptions); 629 | } else { 630 | var nestedTableName = sqlTable + "$" + fn; 631 | 632 | results[fn] = self._ensureIndexPrepared(nestedTableName, value, options); 633 | } 634 | } 635 | 636 | return results; 637 | }; 638 | 639 | OracleCollection.prototype.ensureIndex = function(fields, options, callback) { 640 | var self = this; 641 | options = options || {}; 642 | 643 | var sqlTable = self.oracleOptions.sqlTable; 644 | if(!sqlTable) { 645 | throw new Error("Missing sqlTable in oracle options for drop operation"); 646 | } 647 | 648 | var results = []; 649 | var cl = OracleFields._prepare(fields); 650 | 651 | self._ensureIndexPrepared(sqlTable, cl, options); 652 | 653 | if(callback) { 654 | callback(null, results); 655 | } 656 | }; 657 | 658 | OracleCollection.prototype._dropIndexPrepared = function(sqlTable, cl) { 659 | var self = this; 660 | var results = {}; 661 | 662 | for(fn in cl) { 663 | var value = cl[fn]; 664 | 665 | if(fn === ".") { 666 | var tableDesc = self.db._tables[sqlTable]; 667 | var s = OracleFields.getIndexColumnList(tableDesc, cl["."], false, true); 668 | 669 | if(!s) { 670 | continue; 671 | } 672 | 673 | var ids = OracleFields.getColumnIdList(tableDesc, cl["."]); 674 | var indexName = sqlTable+"_I"+ids; 675 | 676 | if(s && tableDesc.indexes[indexName]) { 677 | // Index exists, remove 678 | var sql = "DROP INDEX "+OracleDB.q(indexName); 679 | var sqlParameters = []; 680 | 681 | results[fn] = self.db.executeCommand(sql, sqlParameters, self.oracleOptions); 682 | self.db._refreshTable(sqlTable, self.oracleOptions); 683 | } 684 | } else { 685 | var nestedTableName = sqlTable + "$" + fn; 686 | 687 | results[fn] = self._dropIndexPrepared(nestedTableName, value); 688 | } 689 | } 690 | 691 | return results; 692 | }; 693 | 694 | OracleCollection.prototype.dropIndex = function(fields, callback) { 695 | var self = this; 696 | options = options || {}; 697 | 698 | var sqlTable = self.oracleOptions.sqlTable; 699 | if(!sqlTable) { 700 | throw new Error("Missing sqlTable in oracle options for drop operation"); 701 | } 702 | 703 | var results = []; 704 | var cl = OracleFields._prepare(fields); 705 | 706 | self._dropIndexPrepared(sqlTable, cl); 707 | 708 | if(callback) { 709 | callback(null, results); 710 | } 711 | }; 712 | -------------------------------------------------------------------------------- /lib/server/oracle_db.js: -------------------------------------------------------------------------------- 1 | var path = Npm.require('path'); 2 | var Fiber = Npm.require('fibers'); 3 | var Future = Npm.require(path.join('fibers', 'future')); 4 | 5 | OracleDB = function (connection) { 6 | var self = this; 7 | 8 | self.connection = connection; 9 | self.db = NpmModuleOracledb; 10 | self.db.maxRows = 1000; 11 | self.db.prefetchRows = 100; 12 | self.db.autoCommit = true; 13 | self._tables = {}; 14 | }; 15 | 16 | OracleDB._databases = {}; 17 | 18 | OracleDB.getDatabase = function(oracleOptions) { 19 | var connection = oracleOptions.connection; 20 | var key = connection.user+"@"+connection.connectString; 21 | var db = OracleDB._databases[key]; 22 | 23 | if(!db) { 24 | var user = OracleDB.executeCommand(connection, "select * from user_users", [], {}); 25 | 26 | db = new OracleDB(connection); 27 | db.user = user; 28 | db.databaseName = key; 29 | OracleDB._databases[key] = db; 30 | db._ensureOplog(oracleOptions); 31 | } 32 | 33 | return db; 34 | }; 35 | 36 | OracleDB.q = function(name) { 37 | return '"'+name+'"'; 38 | }; 39 | 40 | OracleDB.uq = function(name) { 41 | return name.substr(1, name.length-2); 42 | }; 43 | 44 | // Meteor -> Oracle 45 | OracleDB._tableNameM2O = function(name) { 46 | return name; 47 | }; 48 | 49 | OracleDB._columnNameM2O = function(name) { 50 | return name; 51 | }; 52 | 53 | //Oracle -> Meteor 54 | OracleDB._tableNameO2M = function(name) { 55 | return name; 56 | }; 57 | 58 | OracleDB._columnNameO2M = function(name) { 59 | return name; 60 | }; 61 | 62 | 63 | OracleDB._processResult = function(sqlOptions, result) { 64 | var md = result.metaData; 65 | var rows = result.rows; 66 | var records = []; 67 | var nameMap = {}; 68 | 69 | if(md && rows) { 70 | for (var i = 0; i < rows.length; i++) { 71 | var row = rows[i]; 72 | var rec = {}; 73 | for(var j = 0; j < md.length; j++) { 74 | var mdrec = md[j]; 75 | var val = row[j]; 76 | 77 | // Turn null into undefined so $exist work properly in minimongo 78 | if(val === null) { 79 | continue; 80 | } 81 | 82 | if(sqlOptions.booleanTrueValue !== undefined) { 83 | if(val === sqlOptions.booleanTrueValue) { 84 | val = true; 85 | } else if(val === sqlOptions.booleanFalseValue) { 86 | val = false; 87 | } 88 | } 89 | var name = mdrec['name']; 90 | 91 | var name2 = nameMap[name]; 92 | if(!name2) { 93 | var name2 = OracleDB._columnNameO2M(name); 94 | nameMap[name] = name2; 95 | } 96 | rec[name2] = val; 97 | } 98 | records.push(rec); 99 | } 100 | 101 | result.records = records; 102 | delete result.rows; 103 | } 104 | 105 | return result; 106 | } 107 | 108 | OracleDB.executeCommand = function(connection, sql, sqlParameters, sqlOptions) { 109 | sqlParameters = sqlParameters || {}; 110 | sqlOptions = sqlOptions || {}; 111 | 112 | var future = new Future; 113 | var onComplete = future.resolver(); 114 | 115 | try 116 | { 117 | NpmModuleOracledb.getConnection( 118 | connection, 119 | Meteor.bindEnvironment( 120 | 121 | function (err, db) { 122 | if (err) { 123 | onComplete(err); 124 | return; 125 | } 126 | 127 | if(sqlOptions.sqlDebug) { 128 | if(sql.indexOf("SELECT * FROM \"oplog\"") < 0) { 129 | console.log(sql, sqlParameters); 130 | } 131 | } 132 | 133 | db.execute( 134 | sql, 135 | sqlParameters, 136 | sqlOptions, 137 | function(err, result) { 138 | 139 | db.release( 140 | function(err2) { 141 | if (err2) { 142 | console.error(err2.message); 143 | onComplete(err2, result); 144 | } else { 145 | onComplete(err, result); 146 | } 147 | }); 148 | } 149 | ); 150 | }, 151 | onComplete // onException 152 | ) 153 | ); 154 | } 155 | catch(ex) 156 | { 157 | console.error("ERROR in SQL preparation: "+sql); 158 | throw ex; 159 | } 160 | 161 | try 162 | { 163 | var result = future.wait(); 164 | result = OracleDB._processResult(sqlOptions, result); 165 | return result; 166 | } 167 | catch(ex) 168 | { 169 | console.error("ERROR in SQL: ", sql); 170 | console.error("ERROR MESSAGE: ", ex.message); 171 | throw ex; 172 | } 173 | }; 174 | 175 | OracleDB.prototype.executeCommand = function(sql, sqlParameters, sqlOptions) { 176 | var self = this; 177 | 178 | return OracleDB.executeCommand(self.connection, sql, sqlParameters, sqlOptions); 179 | }; 180 | 181 | OracleDB.executeBatch = function(connection, batch, sqlOptions) { 182 | sqlOptions = sqlOptions || {}; 183 | 184 | var future = new Future; 185 | var onComplete = future.resolver(); 186 | 187 | try 188 | { 189 | NpmModuleOracledb.getConnection( 190 | connection, 191 | Meteor.bindEnvironment( 192 | 193 | function (err, db) { 194 | if (err) { 195 | onComplete(err); 196 | return; 197 | } 198 | 199 | var results = []; 200 | 201 | try 202 | { 203 | // For each SQL in the batch 204 | for(var bi = 0; bi < batch.length; bi++) { 205 | var bv = batch[bi]; 206 | 207 | if(sqlOptions.sqlDebug) { 208 | console.log("BATCH ITEM["+bi+"]", bv.sql, bv.sqlParameters); 209 | } 210 | 211 | var future2 = new Future; 212 | var onComplete2 = future2.resolver(); 213 | 214 | db.execute( 215 | bv.sql, 216 | bv.sqlParameters, 217 | {autoCommit: false}, 218 | function(err, result) { 219 | if(err) { 220 | console.log("BATCH ["+bi+"] EXECUTED WITH ERROR: ", err.message, bv); 221 | } 222 | onComplete2(err, result); 223 | } 224 | ); 225 | 226 | var result = future2.wait(); 227 | results.push(result); 228 | } 229 | 230 | var commitFuture = new Future; 231 | var onCommitComplete = commitFuture.resolver(); 232 | 233 | db.commit(function(err2){ 234 | if (err2) { 235 | console.error("BATCH COMMIT FAILED: ", err2.message); 236 | err = err || err2; 237 | } 238 | 239 | onCommitComplete(err2, null); 240 | }); 241 | 242 | commitFuture.wait(); 243 | } 244 | catch(e) { 245 | err = err || e; 246 | 247 | var rollbackFuture = new Future; 248 | var onRollbackComplete = rollbackFuture.resolver(); 249 | 250 | db.rollback(function(err2){ 251 | if (err2) { 252 | console.error("BATCH ROLLBACK FAILED: ", err2.message); 253 | err = err || err2; 254 | } 255 | 256 | onRollbackComplete(err2, null); 257 | }); 258 | 259 | rollbackFuture.wait(); 260 | } 261 | finally { 262 | db.release( 263 | function(err2) { 264 | if (err2) { 265 | console.error("BATCH CONNECTION RELEASE ERROR:", err.message); 266 | err = err || err2; 267 | } 268 | 269 | // Big return!!! 270 | onComplete(err, results); 271 | }); 272 | } 273 | }, 274 | onComplete // onException 275 | ) 276 | ); 277 | } 278 | catch(ex) 279 | { 280 | console.error("ERROR in SQL BATCH preparation: "+batch); 281 | throw ex; 282 | } 283 | 284 | try 285 | { 286 | var result = future.wait(); 287 | 288 | for(var i = 0; i < result.length; i++) { 289 | result[i] = OracleDB._processResult(sqlOptions, result[i]); 290 | } 291 | 292 | return result; 293 | } 294 | catch(ex) 295 | { 296 | console.error("ERROR in SQL BATCH: "+ex.message); 297 | throw ex; 298 | } 299 | }; 300 | 301 | OracleDB.prototype.executeBatch = function(batch, sqlOptions) { 302 | var self = this; 303 | 304 | return OracleDB.executeBatch(self.connection, batch, sqlOptions); 305 | }; 306 | 307 | OracleDB.prototype._getJsonColumns = function(sqlTable, op, oracleOptions, o) { 308 | var self = this; 309 | var tableResult = self._tables[sqlTable]; 310 | var columns = tableResult["columns"]; 311 | var s = ""; 312 | var nested = sqlTable.indexOf("$") >= 0; 313 | 314 | var i = 0; 315 | var nw = ":NEW"; 316 | if(op === 'w' || op === 'd') { 317 | nw = ":OLD"; 318 | } 319 | 320 | for(var c in columns) { 321 | var cv = columns[c]; 322 | 323 | if(op === 'u' && c === "_id") continue; // exclude for updates 324 | if(op === 'w' || op === 'd') { 325 | if(!(c === "_id" || c === "_value")) { 326 | continue; 327 | } 328 | } 329 | if (i > 0) { 330 | s = s + "\n"; 331 | } 332 | s = s + "\t\t"; 333 | 334 | if(op === 'u') { 335 | s = s + 'IF ('+nw+'."'+c+'" <> :OLD."'+c+'" OR ('+nw+'."'+c+'" IS NOT NULL AND :OLD."'+c+'" IS NULL) OR ('+nw+'."'+c+'" IS NULL AND :OLD."'+c+'" IS NOT NULL)) THEN '; 336 | s = s + "IF "+o+" is not null THEN "+o+" := "+o+" || ', '; END IF; "; 337 | } else { 338 | if(i > 0) { 339 | s = s + o+" := "+o+" || ', ';\n\t\t"; 340 | } 341 | } 342 | 343 | s = s + o+" := "+o+" || "; 344 | 345 | if(cv.isBoolean) { 346 | s = s + '\'"'+c+'": \'||nvl('+nw+'."'+c+'", \'null\')'; 347 | } else if(cv.dataType === 'VARCHAR2') { 348 | s = s + '\'"'+c+'": "\'||replace(replace('+nw+'."'+c+'", chr(10), \'\\n\'), \'"\', \'\\"\')||\'"\''; 349 | } else if(cv.dataType === 'NUMBER') { 350 | s = s + '\'"'+c+'": \'||nvl(meteor_pkg.js_number('+nw+'."'+c+'"), \'null\')'; 351 | } else if(cv.dataType === 'DATE') { 352 | s = s + '\'"'+c+'": {"$date": "\'||to_char('+nw+'."'+c+'", \'yyyy-mm-dd"T"hh24:mi:ss".000Z"\')||\'"}\''; 353 | } else if(cv.dataType === 'TIMESTAMP') { 354 | s = s + '\'"'+c+'": {"$date": "\'||to_char('+nw+'."'+c+'", \'yyyy-mm-dd"T"hh24:mi:ss.ff3"Z"\')||\'"}\''; 355 | } 356 | 357 | if(op === 'u') { 358 | s = s + '; END IF'; 359 | } 360 | s = s + ';'; 361 | i++; 362 | } 363 | 364 | return i === 0 ? null : s; 365 | }; 366 | 367 | OracleDB.prototype._refreshTable = function(sqlTable, oracleOptions) { 368 | var self = this; 369 | var tableResult = self._tables[sqlTable]; 370 | 371 | if(!tableResult) { 372 | var tableSql = 'select table_name as "tableName", status, num_rows as "numRows" from user_tables where table_name = :1'; 373 | var tableResult = self.executeCommand(tableSql, [sqlTable]) 374 | 375 | if(!tableResult.records || tableResult.records.length === 0) { 376 | // Table does not exist in the database 377 | return; 378 | } 379 | 380 | self._tables[sqlTable] = tableResult; 381 | } 382 | 383 | var colSql = 'select column_name as "columnName", column_id as "columnId", data_type as "dataType", data_length as "dataLength", data_precision as "dataPrecision", data_scale as "dataScale", nullable as "nullable", default_length as "defaultLength" from user_tab_columns where table_name = :1 order by column_id'; 384 | var colResult = self.executeCommand(colSql, [sqlTable], oracleOptions) 385 | var columns = {}; 386 | var ccn = {}; 387 | for(var i = 0; i < colResult.records.length; i++) { 388 | var rec = colResult.records[i]; 389 | 390 | var oraColumn = rec.columnName; 391 | var meteorColumn = OracleDB._columnNameO2M(oraColumn); 392 | columns[oraColumn] = rec; 393 | rec["meteorName"] = meteorColumn; 394 | ccn[meteorColumn] = oraColumn; 395 | } 396 | tableResult["colRecords"] = colResult.records; 397 | tableResult["columns"] = columns; 398 | tableResult["ccn"] = ccn; 399 | 400 | var consSql = 'select constraint_name as "constraintName", constraint_type as "constraintType" from user_constraints where table_name = :1 and status = \'ENABLED\' and invalid is null'; 401 | var consResult = self.executeCommand(consSql, [sqlTable], oracleOptions) 402 | var constraints = {}; 403 | for(var i = 0; i < consResult.records.length; i++) { 404 | var rec = consResult.records[i]; 405 | var s = sqlTable+'_BOOL_CHK'; 406 | 407 | if(rec.constraintName.substr(0, s.length) == s) { 408 | // It's boolean check! 409 | var index = rec.constraintName.substr(s.length); 410 | tableResult.colRecords[index-1]["isBoolean"] = true; 411 | } 412 | s = sqlTable+"_NULL_CHK"; 413 | if(rec.constraintName.substr(0, s.length) == s) { 414 | // It's null check! 415 | var index = rec.constraintName.substr(s.length); 416 | tableResult.colRecords[index-1]["isNull"] = true; 417 | } 418 | } 419 | tableResult["consRecords"] = consResult.records; 420 | 421 | var indSql = 'select index_name as "indexName", index_type as "indexType", uniqueness as "uniqueness", status as "status", compression as "compression" from user_indexes where table_name = :1'; 422 | var indResult = self.executeCommand(indSql, [sqlTable], oracleOptions) 423 | var indexes = {}; 424 | for(var i = 0; i < indResult.records.length; i++) { 425 | var rec = indResult.records[i]; 426 | indexes[rec.indexName] = rec; 427 | } 428 | tableResult["indexes"] = indexes; 429 | // tableResult["indResult"] = indResult; 430 | 431 | var detSql = 432 | 'select table_name as "tableName", substr(table_name, length(:0)+2) as "fieldName" from user_constraints '+ 433 | "where table_name like :1||'$%' and constraint_type='R' " + 434 | "and r_constraint_name in " + 435 | "(select constraint_name from user_constraints where constraint_type in ('P','U') and table_name=:2) "; 436 | 437 | var detResult = self.executeCommand(detSql, [sqlTable, sqlTable, sqlTable], oracleOptions) 438 | var details = {}; 439 | for(var key in detResult.records) { 440 | var rec = detResult.records[key]; 441 | details[OracleDB._columnNameO2M(rec.fieldName)] = {tableName: OracleDB._tableNameO2M(rec.tableName), fieldName: OracleDB._columnNameO2M(rec.fieldName)}; 442 | } 443 | tableResult["details"] = details; 444 | 445 | for(var di in details) { 446 | self._ensureTable(details[di].tableName, oracleOptions, sqlTable); 447 | } 448 | 449 | self._refreshTrigger(sqlTable, oracleOptions); 450 | } 451 | 452 | OracleDB.prototype._refreshTrigger = function(sqlTable, oracleOptions) { 453 | var self = this; 454 | 455 | if(oracleOptions.strict && !oracleOptions.strictAllowOplogAndTriggers) { 456 | return; 457 | } 458 | 459 | var ns = oracleOptions.connection.user+'@'+oracleOptions.connection.connectString+'.'+sqlTable; 460 | 461 | // Create or Replace the Trigger 462 | if(sqlTable !== 'oplog') { 463 | var trgSql = 'create or replace trigger "'+sqlTable+'_trg"\n'; 464 | trgSql = trgSql + "after insert or update or delete\n"; 465 | trgSql = trgSql + 'on "' + sqlTable + '"\n'; 466 | trgSql = trgSql + "for each row\n"; 467 | trgSql = trgSql + "declare\n"; 468 | trgSql = trgSql + "\top varchar2(1);\n"; 469 | trgSql = trgSql + "\tns varchar2(200);\n"; 470 | trgSql = trgSql + "\to varchar2(4000);\n"; 471 | trgSql = trgSql + "\to2 varchar2(4000);\n"; 472 | trgSql = trgSql + "begin\n"; 473 | trgSql = trgSql + "\tIF INSERTING THEN\n"; 474 | trgSql = trgSql + "\t\top := 'i';\n"; 475 | trgSql = trgSql + "\t\tns := '"+ns+"';\n"; 476 | trgSql = trgSql + "\t\to := '';\n"; 477 | trgSql = trgSql + self._getJsonColumns(sqlTable, 'i', oracleOptions, "o") + "\n"; 478 | trgSql = trgSql + "\t\to := '{'||o||'}';\n"; 479 | trgSql = trgSql + "\t\to2 := null;\n"; 480 | trgSql = trgSql + '\t\tinsert into "oplog" ("id", "ts", "tr", "v", "op", "ns", "o", "o2")\n' 481 | trgSql = trgSql + '\t\tvalues ("oplog_seq".nextval, current_timestamp, dbms_transaction.local_transaction_id, 2, op, ns, o, o2);\n'; 482 | trgSql = trgSql + "\tELSIF UPDATING THEN\n"; 483 | trgSql = trgSql + "\t\top := 'u';\n"; 484 | trgSql = trgSql + "\t\tns := '"+ns+"';\n"; 485 | var cl = self._getJsonColumns(sqlTable, 'u', oracleOptions, "o"); 486 | if(cl) { 487 | trgSql = trgSql + "\t\to := '';\n" 488 | trgSql = trgSql + cl + "\n"; 489 | trgSql = trgSql + "\t\tIF o is not null THEN\n"; 490 | trgSql = trgSql + "\t\t\to := '{\"$set\": {'||o||'}}';\n"; 491 | trgSql = trgSql + "\t\to2 := '';\n"; 492 | trgSql = trgSql + self._getJsonColumns(sqlTable, 'w', oracleOptions, "o2") + "\n"; 493 | trgSql = trgSql + "\t\to2 := '{'||o2||'}';\n"; 494 | trgSql = trgSql + '\t\t\tinsert into "oplog" ("id", "ts", "tr", "v", "op", "ns", "o", "o2")\n'; 495 | trgSql = trgSql + '\t\t\tvalues ("oplog_seq".nextval, current_timestamp, dbms_transaction.local_transaction_id, 2, op, ns, o, o2);\n'; 496 | trgSql = trgSql + "\t\tEND IF;\n"; 497 | } 498 | trgSql = trgSql + "\tELSIF DELETING THEN\n"; 499 | trgSql = trgSql + "\t\top := 'd';\n"; 500 | trgSql = trgSql + "\t\tns := '"+ns+"';\n"; 501 | trgSql = trgSql + "\t\to := '';\n"; 502 | trgSql = trgSql + self._getJsonColumns(sqlTable, 'w', oracleOptions, "o") + "\n"; 503 | trgSql = trgSql + "\t\to := '{'||o||'}';\n"; 504 | trgSql = trgSql + "\t\to2 := null;\n"; 505 | trgSql = trgSql + '\t\tinsert into "oplog" ("id", "ts", "tr", "v", "op", "ns", "o", "o2")\n'; 506 | trgSql = trgSql + '\t\tvalues ("oplog_seq".nextval, current_timestamp, dbms_transaction.local_transaction_id, 2, op, ns, o, o2);\n'; 507 | trgSql = trgSql + "\tEND IF;\n"; 508 | trgSql = trgSql + " end;\n"; 509 | var trgResult = self.executeCommand(trgSql, [], oracleOptions) 510 | } 511 | }; 512 | 513 | OracleDB.prototype._ensureTable = function(sqlTable, oracleOptions, parentSqlTable) { 514 | var self = this; 515 | 516 | if(!self._tables[sqlTable]) { 517 | self._refreshTable(sqlTable, oracleOptions); 518 | 519 | if(!self._tables[sqlTable]) { 520 | if(oracleOptions.strict || oracleOptions.strictAllowOplogAndTriggers) { 521 | throw new Error("Table \"" + sqlTable + "\" does not exist and can't be created in strict mode"); 522 | } 523 | 524 | // Table doesn't exist, we should create it 525 | var sql; 526 | var result; 527 | 528 | if(parentSqlTable) { 529 | sql = "create table "+OracleDB.q(sqlTable)+" ("+OracleDB.q("_id")+" varchar2(17) not null, "+OracleDB.q("_indexNo")+" number(20, 0) not null)"; 530 | } else { 531 | sql = "create table "+OracleDB.q(sqlTable)+" ("+OracleDB.q("_id")+" varchar2(17) not null)"; 532 | } 533 | if(oracleOptions.sqlDebug) { 534 | console.log(sql); 535 | }; 536 | 537 | result = self.executeCommand(sql, []); 538 | 539 | // Add a primary key 540 | if(parentSqlTable) { 541 | sql = "alter table "+OracleDB.q(sqlTable)+" add constraint "+OracleDB.q(sqlTable+"_PK")+" primary key ("+OracleDB.q("_id")+", "+OracleDB.q("_indexNo")+")"; 542 | } else { 543 | sql = "alter table "+OracleDB.q(sqlTable)+" add constraint "+OracleDB.q(sqlTable+"_PK")+" primary key ("+OracleDB.q("_id")+")"; 544 | } 545 | if(oracleOptions.sqlDebug) { 546 | console.log(sql); 547 | }; 548 | result = self.executeCommand(sql, []); 549 | 550 | // Add a foreign key 551 | if(parentSqlTable) { 552 | sql = "alter table "+OracleDB.q(sqlTable)+" add constraint "+OracleDB.q(sqlTable+"_FK")+" foreign key ("+OracleDB.q("_id")+") references "+OracleDB.q(parentSqlTable)+"("+OracleDB.q("_id")+") on delete cascade"; 553 | if(oracleOptions.sqlDebug) { 554 | console.log(sql); 555 | }; 556 | result = self.executeCommand(sql, []); 557 | } 558 | 559 | // Verify, that the table has been created 560 | self._refreshTable(sqlTable, oracleOptions); 561 | if(!self._tables[sqlTable]) { 562 | throw new Error("Table creation failed"); 563 | } 564 | } 565 | 566 | if(parentSqlTable) { 567 | self._refreshTable(parentSqlTable, oracleOptions); 568 | } 569 | } 570 | }; 571 | 572 | 573 | OracleDB.prototype._ensureOplog = function(oracleOptions) { 574 | var self = this; 575 | var sqlTable = "oplog"; 576 | 577 | if(oracleOptions.strict && !oracleOptions.strictAllowOplogAndTriggers) { 578 | return; 579 | } 580 | 581 | if(!self._tables[sqlTable]) { 582 | self._refreshTable(sqlTable, oracleOptions); 583 | 584 | if(!self._tables[sqlTable]) { 585 | // Oplog doesn't exist, we should create it 586 | var sql; 587 | var result; 588 | 589 | // Create table 590 | sql = 'create table "oplog" \n\ 591 | ( \n\ 592 | "id" number not null, \n\ 593 | "ts" timestamp not null, \n\ 594 | "tr" varchar2(20) not null, \n\ 595 | "v" number(1) not null, \n\ 596 | "op" varchar2(10) not null, \n\ 597 | "ns" varchar2(100) not null, \n\ 598 | "o" varchar2(4000) not null, \n\ 599 | "o2" varchar2(4000) null \n\ 600 | )'; 601 | 602 | 603 | if(oracleOptions.sqlDebug) { 604 | console.log(sql); 605 | }; 606 | 607 | result = self.executeCommand(sql, []); 608 | 609 | // Create sequence 610 | sql = 'create sequence "oplog_seq"'; 611 | 612 | if(oracleOptions.sqlDebug) { 613 | console.log(sql); 614 | }; 615 | 616 | result = self.executeCommand(sql, []); 617 | 618 | // Add primary key 619 | sql = 'alter table "oplog" add constraint "oplog_pk" primary key ("id")'; 620 | 621 | if(oracleOptions.sqlDebug) { 622 | console.log(sql); 623 | }; 624 | 625 | result = self.executeCommand(sql, []); 626 | 627 | // Add index 628 | sql = 'create index "oplog_i1" on "oplog" ("ts")'; 629 | 630 | if(oracleOptions.sqlDebug) { 631 | console.log(sql); 632 | }; 633 | 634 | result = self.executeCommand(sql, []); 635 | 636 | // Create package 637 | sql = 'create or replace package meteor_pkg is \n\ 638 | \n\ 639 | FUNCTION js_number(n IN NUMBER) RETURN VARCHAR2 DETERMINISTIC; \n\ 640 | \n\ 641 | end meteor_pkg;'; 642 | 643 | if(oracleOptions.sqlDebug) { 644 | console.log(sql); 645 | }; 646 | 647 | result = self.executeCommand(sql, []); 648 | 649 | // Create package body 650 | sql = 'create or replace package body meteor_pkg is \n\ 651 | \n\ 652 | FUNCTION js_number(n IN NUMBER) RETURN VARCHAR2 DETERMINISTIC IS \n\ 653 | jsn VARCHAR2(100); \n\ 654 | BEGIN \n\ 655 | jsn := to_char(n); \n\ 656 | if substr(jsn, 0, 1) = \'.\' then \n\ 657 | jsn := \'0\'||jsn; \n\ 658 | end if; \n\ 659 | \n\ 660 | return jsn; \n\ 661 | END; \n\ 662 | \n\ 663 | end meteor_pkg;'; 664 | 665 | if(oracleOptions.sqlDebug) { 666 | console.log(sql); 667 | }; 668 | 669 | result = self.executeCommand(sql, []); 670 | 671 | // Verify, that the table has been created 672 | self._refreshTable(sqlTable, oracleOptions); 673 | if(!self._tables[sqlTable]) { 674 | throw new Error("Table creation failed"); 675 | } 676 | } 677 | } 678 | }; 679 | 680 | 681 | OracleDB.prototype._getColumnType = function(sqlTable, column, value, oracleOptions, postponedBooleanColumns, postponedNullColumns) { 682 | var self = this; 683 | 684 | var type = undefined; 685 | 686 | if(value === null) { 687 | type = "varchar2(1)"; 688 | postponedNullColumns.push(column); 689 | } else if(typeof value === 'string') { 690 | var len = Buffer.byteLength(value, 'utf8'); 691 | if(len <= 0) len = 1; 692 | type = "varchar2("+len+")"; 693 | } else if(typeof value === 'number') { 694 | var ns = '' + value; 695 | var index = ns.indexOf("."); 696 | if(index >= 0) { 697 | // contains "." 698 | type = "number("+(ns.length-1)+", "+(ns.length - index - 1)+")"; 699 | } else { 700 | type = "number("+ns.length+")"; 701 | } 702 | } else if (typeof value === 'boolean') { 703 | if(typeof oracleOptions.booleanTrueValue === 'number' 704 | && typeof oracleOptions.booleanFalseValue === 'number') { 705 | type = "number"; 706 | if(0 <= oracleOptions.booleanTrueValue && oracleOptions.booleanTrueValue < 10 && 707 | 0 <= oracleOptions.booleanFalseValue && oracleOptions.booleanFalseValue < 10 ) { 708 | type = "number(1)"; 709 | } 710 | } else if(typeof oracleOptions.booleanTrueValue === 'string' 711 | && typeof oracleOptions.booleanFalseValue === 'string') { 712 | var maxLen = oracleOptions.booleanTrueValue.length; 713 | 714 | if(maxLen < oracleOptions.booleanFalseValue.length) { 715 | maxLen = oracleOptions.booleanFalseValue.length; 716 | } 717 | type = "varchar2("+maxLen+")"; // "true" or "false" 718 | } else { 719 | console.log("oracleOptions", oracleOptions); 720 | throw new Error("Invalid parameters booleanTrueValue or booleanFalseValue"); 721 | } 722 | postponedBooleanColumns.push(column); 723 | } else if(value instanceof Date) { 724 | type = "date"; 725 | } else if(value instanceof Array) { 726 | } else if(value !== null && typeof value === 'object') { 727 | } 728 | 729 | if(type) { 730 | if(value === null) { 731 | type = type + " null"; 732 | } else { 733 | // Check if there are any records in the table 734 | var sql = "select "+OracleDB.q("_id")+" from "+OracleDB.q(sqlTable)+" where rownum = 1"; 735 | 736 | var result = self.executeCommand(sql, [], oracleOptions); 737 | 738 | if(result.records.length === 0) { 739 | // No rows, turn the column not null 740 | type = type + " not null"; 741 | } 742 | 743 | } 744 | } 745 | return type; 746 | }; 747 | 748 | OracleDB.prototype._ensureColumnsPostProcess = function(sqlTable, oracleOptions, refreshCols, postponedBooleanColumns, postponedNullColumns) { 749 | var self = this; 750 | 751 | if(oracleOptions.strict || oracleOptions.strictAllowOplogAndTriggers) { 752 | return; 753 | } 754 | 755 | if(refreshCols) { 756 | self._refreshTable(sqlTable, oracleOptions); 757 | refreshCols = false; 758 | } 759 | 760 | var table = self._tables[sqlTable]; 761 | 762 | for(var i = 0; i < postponedBooleanColumns.length; i++) { 763 | var column = postponedBooleanColumns[i]; 764 | 765 | var columnDesc = table.columns[column]; 766 | 767 | var sql = "alter table "+OracleDB.q(sqlTable)+" add constraint "+ OracleDB.q(sqlTable + "_BOOL_CHK" + columnDesc.columnId) + " CHECK ("+ 768 | OracleDB.q(columnDesc.columnName)+" IN (NULL, '"+oracleOptions.booleanTrueValue+"', '"+oracleOptions.booleanFalseValue+"')) ENABLE"; 769 | 770 | var result = self.executeCommand(sql, [], oracleOptions); 771 | 772 | refreshCols = true; 773 | } 774 | 775 | for(var i = 0; i < postponedNullColumns.length; i++) { 776 | var column = postponedNullColumns[i]; 777 | 778 | var columnDesc = table.columns[column]; 779 | var sql = "alter table "+OracleDB.q(sqlTable)+" add constraint "+ OracleDB.q(sqlTable + "_NULL_CHK" + columnDesc.columnId) + " CHECK ("+ 780 | OracleDB.q(columnDesc.columnName)+" IS NULL) ENABLE"; 781 | 782 | var result = self.executeCommand(sql, [], oracleOptions); 783 | 784 | refreshCols = true; 785 | } 786 | 787 | if(refreshCols) { 788 | self._refreshTable(sqlTable, oracleOptions); 789 | } 790 | }; 791 | 792 | OracleDB.prototype._ensureSelectableColumns = function(sqlTable, fields, oracleOptions) { 793 | var self = this; 794 | var table = self._tables[sqlTable]; 795 | 796 | if(oracleOptions.strict || oracleOptions.strictAllowOplogAndTriggers) { 797 | return; 798 | } 799 | 800 | var postponedBooleanColumns = []; 801 | var postponedNullColumns = []; 802 | var refreshCols = false; 803 | 804 | for(var column in fields) { 805 | var value = fields[column]; 806 | 807 | if(value || value === null) { 808 | column = OracleDB._columnNameM2O(column); 809 | 810 | var columnDesc = table.columns[column]; 811 | 812 | if(!columnDesc) { 813 | var type = "varchar2(1) null"; 814 | 815 | var sql = "alter table "+OracleDB.q(sqlTable)+" add ("+OracleDB.q(column)+" "+type+")"; 816 | var result = self.executeCommand(sql, [], oracleOptions); 817 | 818 | postponedNullColumns.push(column); 819 | refreshCols = true; 820 | } 821 | } 822 | } 823 | 824 | self._ensureColumnsPostProcess(sqlTable, oracleOptions, refreshCols, postponedBooleanColumns, postponedNullColumns); 825 | }; 826 | 827 | OracleDB.prototype._ensureColumns = function(sqlTable, doc, oracleOptions, parentSqlTable, doXCheck) { 828 | var self = this; 829 | 830 | if(oracleOptions.strict || oracleOptions.strictAllowOplogAndTriggers) { 831 | return; 832 | } 833 | 834 | var table = self._tables[sqlTable]; 835 | if(!table) { 836 | self._ensureTable(sqlTable, oracleOptions, parentSqlTable); 837 | table = self._tables[sqlTable]; 838 | 839 | if(!table) { 840 | throw new Error("Failed to create a new table"); 841 | } 842 | } 843 | 844 | var postponedBooleanColumns = []; 845 | var postponedNullColumns = []; 846 | var refreshCols = false; 847 | 848 | for(var column in doc) { 849 | var value = doc[column]; 850 | 851 | var columnDesc = table.columns[column]; 852 | if(!columnDesc) { 853 | var type = self._getColumnType(sqlTable, column, value, oracleOptions, postponedBooleanColumns, postponedNullColumns); 854 | 855 | if(type) { 856 | var sql = "alter table "+OracleDB.q(sqlTable)+" add ("+OracleDB.q(column)+" "+type+")"; 857 | var result = self.executeCommand(sql, [], oracleOptions); 858 | refreshCols = true; 859 | } 860 | } else { 861 | if(value !== null && (value instanceof Date || !(value instanceof Array) && typeof value !== 'object')) { 862 | if(columnDesc.isNull) { 863 | // Drop the null constraint 864 | var sql = "alter table "+OracleDB.q(sqlTable)+" drop constraint "+ OracleDB.q(sqlTable + "_NULL_CHK" + columnDesc.columnId); 865 | 866 | var result = self.executeCommand(sql, [], oracleOptions); 867 | 868 | refreshCols = true; 869 | 870 | // Check if there are any records in the table 871 | sql = "select "+OracleDB.q("_id")+" from "+OracleDB.q(sqlTable)+" where rownum = 1"; 872 | 873 | result = self.executeCommand(sql, [], oracleOptions); 874 | 875 | if(result.records.length === 0) { 876 | // No rows, turn the column not null 877 | sql = "alter table "+OracleDB.q(sqlTable)+" modify ("+OracleDB.q(column)+" not null)"; 878 | 879 | result = self.executeCommand(sql, [], oracleOptions); 880 | } 881 | } 882 | } 883 | 884 | if(value === null) { 885 | if(columnDesc.nullable === 'N') { 886 | // Allow NULLs 887 | var sql = "alter table "+OracleDB.q(sqlTable)+" modify ("+OracleDB.q(column)+" null)"; 888 | 889 | var result = self.executeCommand(sql, [], oracleOptions); 890 | 891 | refreshCols = true; 892 | } 893 | } else if(typeof value === 'string') { 894 | if(columnDesc.dataType === "VARCHAR2" && !columnDesc.isBoolean) { 895 | var len = Buffer.byteLength(value, 'utf8'); 896 | if(len <= 0) len = 1; 897 | 898 | if(columnDesc.dataLength < len) { 899 | var sql = "alter table "+OracleDB.q(sqlTable)+" modify ("+OracleDB.q(column)+" varchar2("+len+"))"; 900 | 901 | var result = self.executeCommand(sql, [], oracleOptions); 902 | 903 | refreshCols = true; 904 | } 905 | } 906 | } else if(typeof value === 'number') { 907 | var ns = '' + value; 908 | 909 | var index = ns.indexOf("."); 910 | var precision; 911 | var scale; 912 | if(index >= 0) { 913 | // contains "." 914 | precision = (ns.length-1); 915 | scale = (ns.length - index - 1); 916 | } else { 917 | precision = ns.length; 918 | scale = 0; 919 | } 920 | 921 | if(!columnDesc.isBoolean) 922 | if(columnDesc.dataType === "VARCHAR2") { 923 | var part1 = (precision - scale); 924 | var part2 = scale; 925 | var dbPart1 = columnDesc.dataLength; 926 | var dbPart2 = 0; 927 | 928 | var newPrecision = part1 >= dbPart1 ? part1 : dbPart1; 929 | var newScale = part2 >= dbPart2 ? part2 : dbPart2; 930 | newPrecision += newScale; 931 | 932 | // Always alter table 933 | var sql = "alter table "+OracleDB.q(sqlTable)+" modify ("+OracleDB.q(column)+" number("+newPrecision+", "+newScale+"))"; 934 | 935 | var result = self.executeCommand(sql, [], oracleOptions); 936 | 937 | refreshCols = true; 938 | } else if(columnDesc.dataType === "NUMBER") { 939 | var part1 = (precision - scale); 940 | var part2 = scale; 941 | var dbPart1 = (columnDesc.dataPrecision - columnDesc.dataScale); 942 | var dbPart2 = columnDesc.dataScale; 943 | 944 | var newPrecision = part1 >= dbPart1 ? part1 : dbPart1; 945 | var newScale = part2 >= dbPart2 ? part2 : dbPart2; 946 | newPrecision += newScale; 947 | 948 | if(newPrecision > columnDesc.dataPrecision || newScale > columnDesc.dataScale) { 949 | var sql = "alter table "+OracleDB.q(sqlTable)+" modify ("+OracleDB.q(column)+" number("+newPrecision+", "+newScale+"))"; 950 | 951 | var result = self.executeCommand(sql, [], oracleOptions); 952 | 953 | refreshCols = true; 954 | } 955 | } 956 | } else if (typeof value === 'boolean') { 957 | if(columnDesc.isNull) { 958 | var type = undefined; 959 | 960 | if(typeof oracleOptions.booleanTrueValue === 'number' 961 | && typeof oracleOptions.booleanFalseValue === 'number') { 962 | type = "number"; 963 | if(0 <= oracleOptions.booleanTrueValue && oracleOptions.booleanTrueValue < 10 && 964 | 0 <= oracleOptions.booleanFalseValue && oracleOptions.booleanFalseValue < 10) { 965 | if(columnDesc.dataType !== "NUMBER") { 966 | type = "number(1)"; 967 | } 968 | } 969 | } else if(typeof oracleOptions.booleanTrueValue === 'string' 970 | && typeof oracleOptions.booleanFalseValue === 'string') { 971 | var maxLen = oracleOptions.booleanTrueValue.length; 972 | 973 | if(maxLen < oracleOptions.booleanFalseValue.length) { 974 | maxLen = oracleOptions.booleanFalseValue.length; 975 | } 976 | if(columnDesc.dataType !== "VARCHAR2" || columnDesc.dataLength < maxLen) { 977 | type = "varchar2("+maxLen+")"; // "true" or "false" 978 | } 979 | } else { 980 | throw new Error("Invalid parameters booleanTrueValue or booleanFalseValue", oracleOptions); 981 | } 982 | 983 | if(type) { 984 | var sql = "alter table "+OracleDB.q(sqlTable)+" modify ("+OracleDB.q(column)+" "+type+")"; 985 | 986 | var result = self.executeCommand(sql, [], oracleOptions); 987 | } 988 | 989 | postponedBooleanColumns.push(column); 990 | } 991 | } else if(value instanceof Date) { 992 | if(columnDesc.isNull) { 993 | var type = "date"; 994 | 995 | var sql = "alter table "+OracleDB.q(sqlTable)+" modify ("+OracleDB.q(column)+" "+type+")"; 996 | 997 | var result = self.executeCommand(sql, [], oracleOptions); 998 | } 999 | } else if(value instanceof Array) { 1000 | continue; 1001 | } else if(value != null && typeof value === 'object') { 1002 | continue; 1003 | } 1004 | } 1005 | } 1006 | 1007 | // Crosscheck is any columns are missing in the document 1008 | if(doXCheck) 1009 | for(var i = 0; i < table.colRecords.length; i++) { 1010 | var colRec = table.colRecords[i]; 1011 | 1012 | if(colRec.columnName === "_id" || colRec.columnName === "_indexNo") { 1013 | continue; 1014 | } 1015 | 1016 | var value = doc[colRec.meteorName]; 1017 | 1018 | if(value === undefined || (value instanceof Array) || (value != null && typeof value === 'object')) { 1019 | // missing column ... allow nulls 1020 | if(colRec.nullable === 'N') { 1021 | // Allow NULLs 1022 | var sql = "alter table "+OracleDB.q(sqlTable)+" modify ("+OracleDB.q(colRec.columnName)+" null)"; 1023 | 1024 | var result = self.executeCommand(sql, [], oracleOptions); 1025 | 1026 | refreshCols = true; 1027 | } 1028 | } 1029 | } 1030 | 1031 | self._ensureColumnsPostProcess(sqlTable, oracleOptions, refreshCols, postponedBooleanColumns, postponedNullColumns); 1032 | }; 1033 | 1034 | OracleDB.prototype.collection = function(name, oracleOptions, callback) { 1035 | var self = this; 1036 | if(typeof oracleOptions == 'function') callback = oracleOptions, oracleOptions = undefined; 1037 | oracleOptions = oracleOptions || Oracle.getDefaultOracleOptions(); 1038 | 1039 | try { 1040 | var collection = new OracleCollection(this, name, oracleOptions); 1041 | 1042 | if(collection.oracleOptions.sqlTable) { 1043 | self._ensureTable(collection.oracleOptions.sqlTable, collection.oracleOptions); 1044 | } 1045 | 1046 | if(callback) callback(null, collection); 1047 | return collection; 1048 | } catch(err) { 1049 | if(callback) return callback(err); 1050 | throw err; 1051 | } 1052 | }; 1053 | -------------------------------------------------------------------------------- /lib/server/oracle_driver.js: -------------------------------------------------------------------------------- 1 | var path = Npm.require('path'); 2 | var Fiber = Npm.require('fibers'); 3 | var Future = Npm.require(path.join('fibers', 'future')); 4 | 5 | OracleInternals = {}; 6 | OracleTest = {}; 7 | 8 | OracleInternals.NpmModules = { 9 | Oracledb: { 10 | version: NpmModuleOracledbVersion, 11 | module: OracleDB 12 | } 13 | }; 14 | 15 | OracleInternals.NpmModule = OracleDB; 16 | 17 | // Inherits from MongoConnection 18 | OracleConnection = function (options) { 19 | var self = this; 20 | 21 | options = options || {}; 22 | 23 | // Clone the options 24 | var options2 = Oracle._mergeOptions({}, options); 25 | 26 | var mongoOplogUrl = process.env.MONGO_OPLOG_URL; 27 | if(options2.oplogUrl) { 28 | options2.oplogUrl = mongoOplogUrl; 29 | } 30 | 31 | var mongoUrl = process.env.MONGO_URL; 32 | MongoInternals.Connection.call(this, mongoUrl, options2); 33 | 34 | // Closing the mongo connection created by parent 35 | self.close(); 36 | 37 | self.options = options; 38 | 39 | self.db = OracleDB.getDatabase(Oracle._defaultOracleOptions); 40 | 41 | if (options.oplogUrl && ! Package['disable-oplog']) { 42 | self._oplogHandle = new OplogHandle(options.oplogUrl, self.db.databaseName); 43 | } 44 | }; 45 | 46 | //extend from parent class prototype 47 | OracleConnection.prototype = Object.create(MongoInternals.Connection.prototype); // keeps the proto clean 48 | OracleConnection.prototype.constructor = OracleConnection; // repair the inherited constructor 49 | 50 | //Returns the Mongo Collection object; may yield. 51 | OracleConnection.prototype.rawCollection = function (collectionName) { 52 | var self = this; 53 | 54 | if (! self.db) 55 | throw Error("rawCollection called before Connection created?"); 56 | 57 | var future = new Future; 58 | 59 | self.db.collection(collectionName, Oracle.getDefaultOracleOptions(), future.resolver()); 60 | 61 | return future.wait(); 62 | }; 63 | -------------------------------------------------------------------------------- /lib/server/oracle_fields.js: -------------------------------------------------------------------------------- 1 | 2 | OracleFields = {}; 3 | 4 | OracleFields._prepare = function(fields) { 5 | var cls = {}; 6 | 7 | if(!fields) { 8 | return cls; 9 | } 10 | 11 | for(var fn in fields) { 12 | var fa = fn.split("."); 13 | 14 | var cl = cls; 15 | 16 | for(var i = 0; i < fa.length-1; i++) { 17 | var fi = fa[i]; 18 | if(cl[fi] === undefined) { 19 | var n = {}; 20 | cl[fi] = n; 21 | cl = n; 22 | } else { 23 | cl = cl[fi]; 24 | } 25 | } 26 | 27 | if(cl["."] === undefined) { 28 | var n = {}; 29 | cl["."] = n; 30 | cl = n; 31 | } else { 32 | cl = cl["."]; 33 | } 34 | 35 | // last field name 36 | var lfn = fa[fa.length-1]; 37 | 38 | cl[lfn] = fields[fn]; 39 | } 40 | 41 | return cls; 42 | }; 43 | 44 | OracleFields.getColumnList = function(tableDesc, fields, isDetailTable) { 45 | var cl = ""; 46 | var excludeId = false; 47 | 48 | if(tableDesc) { 49 | var incl = undefined; 50 | 51 | for(var fn in fields) { 52 | var value = fields[fn]; 53 | 54 | if(fn === "_id") { 55 | if(value) { 56 | // ok, include _id, fall through 57 | } else { 58 | // Exclude ID, continue to next cycle 59 | excludeId = true; 60 | continue; 61 | } 62 | } 63 | 64 | if(incl === undefined) { 65 | incl = value; 66 | } 67 | 68 | if(incl) { 69 | if(!value) { 70 | throw new Error("Inconsistent fields parameter (mixing includes and excludes)"); 71 | } 72 | 73 | // Formatted field name 74 | var ffn = OracleDB._columnNameM2O(fn); 75 | 76 | if(tableDesc.columns[ffn]) { 77 | if(cl.length > 0) { 78 | cl = cl + ", "; 79 | } 80 | 81 | ffn = OracleDB.q(ffn); 82 | 83 | cl = cl + ffn; 84 | } 85 | } else { 86 | if(value) { 87 | throw new Error("Inconsistent fields parameter (mixing includes and excludes)"); 88 | } 89 | } 90 | } 91 | 92 | if(incl) { 93 | // Add ID field if it wasn't included explicitly 94 | if(!excludeId && !fields["_id"]) { 95 | var s = OracleDB.q("_id"); 96 | 97 | if(isDetailTable) { 98 | s = OracleDB.q("_id")+", "+OracleDB.q("_indexNo"); 99 | } 100 | 101 | if(cl.length > 0) { 102 | s = s + ", "; 103 | } 104 | 105 | cl = s + cl; 106 | } 107 | } else { 108 | for(var c in tableDesc.columns) { 109 | 110 | if(fields[c] === undefined) { 111 | if(cl.length > 0) { 112 | cl = cl + ", "; 113 | } 114 | 115 | c = OracleDB.q(c); 116 | 117 | cl = cl + c; 118 | } 119 | } 120 | } 121 | } else { 122 | for(var fn in fields) { 123 | if(fields[fn]) { 124 | 125 | // Formatted field name 126 | var ffn = OracleDB._columnNameM2O(fn); 127 | 128 | ffn = OracleDB.q(ffn); 129 | 130 | if(cl.length > 0) { 131 | cl = cl + ", "; 132 | } 133 | 134 | cl = cl + ffn; 135 | 136 | } else { 137 | throw new Error("OracleFields.getColumnList when collection is query based then fields parameter can't contain exclusions"); 138 | } 139 | } 140 | } 141 | 142 | return cl; 143 | }; 144 | 145 | OracleFields.getIndexColumnList = function(tableDesc, fields) { 146 | var cl = ""; 147 | 148 | for(var fn in fields) { 149 | if(tableDesc.columns[fn]) { 150 | if(cl.length > 0) { 151 | cl = cl + ", "; 152 | } 153 | 154 | // Formatted field name 155 | var ffn = OracleDB._columnNameM2O(fn); 156 | 157 | ffn = OracleDB.q(ffn); 158 | 159 | 160 | cl = cl + ffn; 161 | 162 | var value = fields[fn]; 163 | 164 | if(typeof value === 'number' && value < 0) { 165 | cl = cl + " desc"; 166 | } 167 | } else { 168 | throw new Error("OracleFields.getColumnIdList missing column in table description: "+fn); 169 | } 170 | } 171 | 172 | return cl; 173 | } 174 | 175 | OracleFields.getColumnIdList = function(tableDesc, fields) { 176 | var cl = ""; 177 | 178 | for(var fn in fields) { 179 | if(tableDesc.columns[fn]) { 180 | if(cl.length > 0) { 181 | cl = cl + "_"; 182 | } 183 | 184 | cl = cl + tableDesc.columns[fn].columnId; 185 | 186 | var value = fields[fn]; 187 | 188 | if(typeof value === 'number' && value < 0) { 189 | cl = cl + "D"; 190 | } 191 | } else { 192 | throw new Error("OracleFields.getIndexColumnList missing column in table description: "+fn); 193 | } 194 | } 195 | 196 | return cl; 197 | } -------------------------------------------------------------------------------- /lib/server/oracle_modifier.js: -------------------------------------------------------------------------------- 1 | OracleModifier = function (modifier, tableName) { 2 | var self = this; 3 | 4 | self._fields = {}; 5 | self._parameters = []; 6 | self._modifier = modifier; 7 | self._tableName = tableName; 8 | self._deferred = []; 9 | self._setClause = self._compileModifier(modifier); 10 | } 11 | 12 | OracleModifier.prototype._prepareOperand = function(operand) { 13 | var self = this; 14 | var ret = null; 15 | if (typeof operand === 'boolean') { 16 | ret = operand ? "true" : "false"; 17 | } else if (typeof operand === 'string') { 18 | // operand = operand.replace("'", "''"); 19 | // ret = "'"+operand+"'"; 20 | ret = operand; 21 | } else { 22 | ret = operand; 23 | } 24 | 25 | self._parameters.push(ret); 26 | 27 | return ":"+(self._parameters.length-1); 28 | }; 29 | 30 | var MODIFIERS = { 31 | $inc: function (oracleModifier, field, arg) { 32 | if (typeof arg !== "number") 33 | throw new Error("Modifier $inc allowed for numbers only"); 34 | 35 | var s = field + " = " + field; 36 | 37 | if(arg >= 0) { 38 | s = s + " + " + oracleModifier._prepareOperand(arg); 39 | } else { 40 | s = s + " - " + oracleModifier._prepareOperand(-arg); 41 | } 42 | 43 | return s; 44 | }, 45 | $set: function (oracleModifier, field, arg) { 46 | if(arg instanceof Array) { 47 | // Must be embedded item 48 | oracleModifier._deferred.push({op: "delete", table: oracleModifier._tableName+"$"+OracleDB.uq(field)}); 49 | _.each(arg, function (value) { 50 | oracleModifier._deferred.push({op: "insert", table: oracleModifier._tableName+"$"+OracleDB.uq(field), value: value}); 51 | }); 52 | 53 | return undefined; 54 | } 55 | 56 | var s = field + " = " + oracleModifier._prepareOperand(arg); 57 | 58 | return s; 59 | }, 60 | $setOnInsert: function (oracleModifier, field, arg) { 61 | throw new Error("$setOnInsert is not supported"); 62 | 63 | var s = field + " = " + oracleModifier._prepareOperand(arg); 64 | 65 | return s; 66 | }, 67 | $unset: function (oracleModifier, field, arg) { 68 | var s = field + " = NULL"; 69 | return s; 70 | }, 71 | $push: function (target, field, arg) { 72 | throw new Error("$push is not supported"); 73 | 74 | if (target[field] === undefined) 75 | target[field] = []; 76 | if (!(target[field] instanceof Array)) 77 | throw MinimongoError("Cannot apply $push modifier to non-array"); 78 | 79 | if (!(arg && arg.$each)) { 80 | // Simple mode: not $each 81 | target[field].push(arg); 82 | return; 83 | } 84 | 85 | // Fancy mode: $each (and maybe $slice and $sort and $position) 86 | var toPush = arg.$each; 87 | if (!(toPush instanceof Array)) 88 | throw MinimongoError("$each must be an array"); 89 | 90 | // Parse $position 91 | var position = undefined; 92 | if ('$position' in arg) { 93 | if (typeof arg.$position !== "number") 94 | throw MinimongoError("$position must be a numeric value"); 95 | // XXX should check to make sure integer 96 | if (arg.$position < 0) 97 | throw MinimongoError("$position in $push must be zero or positive"); 98 | position = arg.$position; 99 | } 100 | 101 | // Parse $slice. 102 | var slice = undefined; 103 | if ('$slice' in arg) { 104 | if (typeof arg.$slice !== "number") 105 | throw MinimongoError("$slice must be a numeric value"); 106 | // XXX should check to make sure integer 107 | if (arg.$slice > 0) 108 | throw MinimongoError("$slice in $push must be zero or negative"); 109 | slice = arg.$slice; 110 | } 111 | 112 | // Parse $sort. 113 | var sortFunction = undefined; 114 | if (arg.$sort) { 115 | if (slice === undefined) 116 | throw MinimongoError("$sort requires $slice to be present"); 117 | // XXX this allows us to use a $sort whose value is an array, but that's 118 | // actually an extension of the Node driver, so it won't work 119 | // server-side. Could be confusing! 120 | // XXX is it correct that we don't do geo-stuff here? 121 | sortFunction = new Minimongo.Sorter(arg.$sort).getComparator(); 122 | for (var i = 0; i < toPush.length; i++) { 123 | if (LocalCollection._f._type(toPush[i]) !== 3) { 124 | throw MinimongoError("$push like modifiers using $sort " + 125 | "require all elements to be objects"); 126 | } 127 | } 128 | } 129 | 130 | // Actually push. 131 | if (position === undefined) { 132 | for (var j = 0; j < toPush.length; j++) 133 | target[field].push(toPush[j]); 134 | } else { 135 | var spliceArguments = [position, 0]; 136 | for (var j = 0; j < toPush.length; j++) 137 | spliceArguments.push(toPush[j]); 138 | Array.prototype.splice.apply(target[field], spliceArguments); 139 | } 140 | 141 | // Actually sort. 142 | if (sortFunction) 143 | target[field].sort(sortFunction); 144 | 145 | // Actually slice. 146 | if (slice !== undefined) { 147 | if (slice === 0) 148 | target[field] = []; // differs from Array.slice! 149 | else 150 | target[field] = target[field].slice(slice); 151 | } 152 | }, 153 | $pushAll: function (target, field, arg) { 154 | throw new Error("$pushAll is not supported"); 155 | 156 | if (!(typeof arg === "object" && arg instanceof Array)) 157 | throw MinimongoError("Modifier $pushAll/pullAll allowed for arrays only"); 158 | var x = target[field]; 159 | if (x === undefined) 160 | target[field] = arg; 161 | else if (!(x instanceof Array)) 162 | throw MinimongoError("Cannot apply $pushAll modifier to non-array"); 163 | else { 164 | for (var i = 0; i < arg.length; i++) 165 | x.push(arg[i]); 166 | } 167 | }, 168 | $addToSet: function (oracleModifier, field, arg) { 169 | var isEach = false; 170 | if (typeof arg === "object") { 171 | //check if first key is '$each' 172 | for (var k in arg) { 173 | if (k === "$each") 174 | isEach = true; 175 | break; 176 | } 177 | } 178 | var values = isEach ? arg["$each"] : [arg]; 179 | _.each(values, function (value) { 180 | oracleModifier._deferred.push({op: "add", table: oracleModifier._tableName+"$"+OracleDB.uq(field), value: value}); 181 | }); 182 | 183 | return undefined; 184 | }, 185 | $pop: function (target, field, arg) { 186 | console.log("ERROR: $pop is not supported"); 187 | throw new Error("$pop is not supported"); 188 | 189 | if (target === undefined) 190 | return; 191 | var x = target[field]; 192 | if (x === undefined) 193 | return; 194 | else if (!(x instanceof Array)) 195 | throw MinimongoError("Cannot apply $pop modifier to non-array"); 196 | else { 197 | if (typeof arg === 'number' && arg < 0) 198 | x.splice(0, 1); 199 | else 200 | x.pop(); 201 | } 202 | }, 203 | $pull: function (oracleModifier, field, arg) { 204 | oracleModifier._deferred.push({op: "delete", table: oracleModifier._tableName+"$"+OracleDB.uq(field), selector: {_value: arg}}); 205 | }, 206 | $pullAll: function (target, field, arg) { 207 | console.log("ERROR: $pullAll is not supported"); 208 | throw new Error("$pullAll is not supported"); 209 | 210 | if (!(typeof arg === "object" && arg instanceof Array)) 211 | throw MinimongoError("Modifier $pushAll/pullAll allowed for arrays only"); 212 | if (target === undefined) 213 | return; 214 | var x = target[field]; 215 | if (x === undefined) 216 | return; 217 | else if (!(x instanceof Array)) 218 | throw MinimongoError("Cannot apply $pull/pullAll modifier to non-array"); 219 | else { 220 | var out = []; 221 | for (var i = 0; i < x.length; i++) { 222 | var exclude = false; 223 | for (var j = 0; j < arg.length; j++) { 224 | if (LocalCollection._f._equal(x[i], arg[j])) { 225 | exclude = true; 226 | break; 227 | } 228 | } 229 | if (!exclude) 230 | out.push(x[i]); 231 | } 232 | target[field] = out; 233 | } 234 | }, 235 | $rename: function (target, field, arg, keypath, doc) { 236 | throw new Error("$rename is not supported"); 237 | 238 | if (keypath === arg) 239 | // no idea why mongo has this restriction.. 240 | throw MinimongoError("$rename source must differ from target"); 241 | if (target === null) 242 | throw MinimongoError("$rename source field invalid"); 243 | if (typeof arg !== "string") 244 | throw MinimongoError("$rename target must be a string"); 245 | if (target === undefined) 246 | return; 247 | var v = target[field]; 248 | delete target[field]; 249 | 250 | var keyparts = arg.split('.'); 251 | var target2 = findModTarget(doc, keyparts, {forbidArray: true}); 252 | if (target2 === null) 253 | throw MinimongoError("$rename target field invalid"); 254 | var field2 = keyparts.pop(); 255 | target2[field2] = v; 256 | }, 257 | $bit: function (target, field, arg) { 258 | // XXX mongo only supports $bit on integers, and we only support 259 | // native javascript numbers (doubles) so far, so we can't support $bit 260 | throw new Error("$bit is not supported"); 261 | } 262 | }; 263 | 264 | OracleModifier.prototype._compileModifier = function(modifier) { 265 | var self = this; 266 | 267 | var s = undefined; 268 | 269 | if(!modifier) { 270 | return s; 271 | } 272 | 273 | for(var mod in modifier) { 274 | var modv = modifier[mod]; 275 | 276 | for(var field in modv) { 277 | var arg = modv[field]; 278 | 279 | if(mod === "$unset") { 280 | arg = null; 281 | } 282 | 283 | var column = OracleDB._columnNameM2O(field); 284 | 285 | column = OracleDB.q(column); 286 | 287 | var ss = MODIFIERS[mod](self, column, arg); 288 | 289 | if(ss === undefined) { 290 | continue; 291 | } 292 | 293 | self._fields[field] = arg; 294 | 295 | if(s === undefined) { 296 | s = ss; 297 | } else { 298 | s = s + ", " + ss; 299 | } 300 | } 301 | } 302 | 303 | return s; 304 | }; 305 | 306 | OracleModifier._compile = function(modifier) { 307 | var m = new OracleModifier(modifier); 308 | 309 | return m._setClause; 310 | }; 311 | 312 | OracleModifier._prepare = function(modifier) { 313 | var mls = {}; 314 | 315 | if(!modifier) { 316 | return mls; 317 | } 318 | 319 | for(var fn in modifier) { 320 | var value = modifier[fn]; 321 | 322 | if(typeof fn === 'string') { 323 | var fa = fn.split("."); 324 | 325 | var ml = mls; 326 | 327 | for(var i = 0; i < fa.length-1; i++) { 328 | var fi = fa[i]; 329 | 330 | if(ml[fi] === undefined) { 331 | var n = {}; 332 | ml[fi] = n; 333 | ml = n; 334 | } 335 | } 336 | 337 | if(ml["."] === undefined) { 338 | var n = {}; 339 | ml["."] = n; 340 | ml = n; 341 | } else { 342 | ml = ml["."]; 343 | } 344 | 345 | ml[fa[fa.length-1]] = value; 346 | } else { 347 | // Add field to sls["."] 348 | var ml = mls["."]; 349 | 350 | if(ml === undefined) { 351 | ml = {}; 352 | mls["."] = ml; 353 | } 354 | 355 | ml[fn] = value; 356 | } 357 | } 358 | 359 | return mls; 360 | }; 361 | 362 | OracleModifier._compilePrepared = function(modifier, tableName) { 363 | var rs = {}; 364 | for(fn in modifier) { 365 | var value = modifier[fn]; 366 | 367 | if(fn === ".") { 368 | var m = new OracleModifier(value, tableName); 369 | 370 | rs[fn] = m._setClause; 371 | rs["*"] = m._fields; 372 | rs["$"] = m._parameters; 373 | rs["@"] = m._deferred; 374 | } else { 375 | var nestedTableName = tableName+"$"+fn; 376 | 377 | rs[fn] = OracleModifier._compilePrepared(value, nestedTableName); 378 | } 379 | } 380 | 381 | return rs; 382 | }; 383 | 384 | OracleModifier.process = function(modifier, tableName) { 385 | var m = OracleModifier._prepare(modifier); 386 | 387 | m = OracleModifier._compilePrepared(m, tableName); 388 | 389 | return m; 390 | }; 391 | 392 | -------------------------------------------------------------------------------- /lib/server/oracle_oplog_tailing.js: -------------------------------------------------------------------------------- 1 | var Future = Npm.require('fibers/future'); 2 | 3 | OPLOG_COLLECTION = 'oplog'; 4 | 5 | var TOO_FAR_BEHIND = process.env.METEOR_OPLOG_TOO_FAR_BEHIND || 2000; 6 | 7 | idForOp = function (op) { 8 | if (op.op === 'd') 9 | return op.o._id; 10 | else if (op.op === 'i') 11 | return op.o._id; 12 | else if (op.op === 'u') 13 | return op.o2._id; 14 | else if (op.op === 'c') 15 | throw Error("Operator 'c' doesn't supply an object with id: " + 16 | EJSON.stringify(op)); 17 | else 18 | throw Error("Unknown op: " + EJSON.stringify(op)); 19 | }; 20 | 21 | OplogHandle = function (oplogUrl, dbName) { 22 | var self = this; 23 | self._oplogUrl = oplogUrl; 24 | self._dbName = dbName; 25 | 26 | self._oplogConnection = null; 27 | self._stopped = false; 28 | self._tailHandle = null; 29 | self._readyFuture = new Future(); 30 | self._crossbar = new DDPServer._Crossbar({ 31 | factPackage: "mongo-livedata", factName: "oplog-watchers" 32 | }); 33 | self._baseOplogSelector = { 34 | ns: new RegExp('^' + Meteor._escapeRegExp(self._dbName) + '\\.'), 35 | $or: [ 36 | { op: {$in: ['i', 'u', 'd']} }, 37 | // drop collection 38 | { op: 'c', 'o.drop': { $exists: true } }, 39 | { op: 'c', 'o.dropDatabase': 1 }, 40 | ] 41 | }; 42 | 43 | // Data structures to support waitUntilCaughtUp(). Each oplog entry has a 44 | // MongoTimestamp object on it (which is not the same as a Date --- it's a 45 | // combination of time and an incrementing counter; see 46 | // http://docs.mongodb.org/manual/reference/bson-types/#timestamps). 47 | // 48 | // _catchingUpFutures is an array of {id: oplogId, future: Future} 49 | // objects, sorted by ascending id. _lastProcessedId is the 50 | // MongoTimestamp of the last oplog entry we've processed. 51 | // 52 | // Each time we call waitUntilCaughtUp, we take a peek at the final oplog 53 | // entry in the db. If we've already processed it (ie, it is not greater than 54 | // _lastProcessedId), waitUntilCaughtUp immediately returns. Otherwise, 55 | // waitUntilCaughtUp makes a new Future and inserts it along with the final 56 | // timestamp entry that it read, into _catchingUpFutures. waitUntilCaughtUp 57 | // then waits on that future, which is resolved once _lastProcessedId is 58 | // incremented to be past its timestamp by the worker fiber. 59 | // 60 | // XXX use a priority queue or something else that's faster than an array 61 | self._catchingUpFutures = []; 62 | self._lastProcessedId = null; 63 | 64 | self._onSkippedEntriesHook = new Hook({ 65 | debugPrintExceptions: "onSkippedEntries callback" 66 | }); 67 | 68 | self._entryQueue = new Meteor._DoubleEndedQueue(); 69 | self._workerActive = false; 70 | 71 | self._startTailing(); 72 | self._tailingJobActive = false; 73 | self._tailingJobIntervalId = null; 74 | }; 75 | 76 | OplogHandle._mergeLogs = function(log1, log2) { 77 | 78 | for(var a in log2) { 79 | if(log1[a] && log1[a] instanceof Object && log2[a] instanceof Object) { 80 | OplogHandle._mergeLogs(log1[a], log2[a]); 81 | } else { 82 | log1[a] = log2[a]; 83 | } 84 | } 85 | }; 86 | 87 | _.extend(OplogHandle.prototype, { 88 | stop: function () { 89 | var self = this; 90 | if (self._stopped) 91 | return; 92 | self._stopped = true; 93 | if (self._tailHandle) 94 | self._tailHandle.stop(); 95 | // XXX should close connections too 96 | }, 97 | onOplogEntry: function (trigger, callback) { 98 | var self = this; 99 | if (self._stopped) 100 | throw new Error("Called onOplogEntry on stopped handle!"); 101 | 102 | // Calling onOplogEntry requires us to wait for the tailing to be ready. 103 | self._readyFuture.wait(); 104 | 105 | var originalCallback = callback; 106 | callback = Meteor.bindEnvironment(function (notification) { 107 | // XXX can we avoid this clone by making oplog.js careful? 108 | originalCallback(EJSON.clone(notification)); 109 | }, function (err) { 110 | Meteor._debug("Error in oplog callback", err.stack); 111 | }); 112 | var listenHandle = self._crossbar.listen(trigger, callback); 113 | return { 114 | stop: function () { 115 | listenHandle.stop(); 116 | } 117 | }; 118 | }, 119 | // Register a callback to be invoked any time we skip oplog entries (eg, 120 | // because we are too far behind). 121 | onSkippedEntries: function (callback) { 122 | var self = this; 123 | if (self._stopped) 124 | throw new Error("Called onSkippedEntries on stopped handle!"); 125 | return self._onSkippedEntriesHook.register(callback); 126 | }, 127 | 128 | _transformOplogRecord: function(row) { 129 | var self = this; 130 | 131 | try { 132 | if(row.o2) { 133 | row.o2 = EJSON.parse(row.o2); 134 | } 135 | if(row.o) { 136 | row.o = EJSON.parse(row.o); 137 | 138 | var c = row.ns.substr(self._dbName.length + 1); 139 | 140 | var ca = c.split("$"); 141 | 142 | if(ca.length > 1) { 143 | row.ns = self._dbName+"."+ca[0]; 144 | 145 | var nestedName = ""; 146 | for(var i = 1; i < ca.length; i++) { 147 | nestedName = nestedName + ca[i]; 148 | } 149 | var x = {}; 150 | x[nestedName] = row.o._value; 151 | 152 | if(row.op === "i") { 153 | row.op = "u"; 154 | row.o2 = {"_id": row.o._id}; 155 | row.o = {"$push": x}; 156 | } else if(row.op === "u") { 157 | throw new Error("Nested oplog update is not supported yet"); 158 | row.o = {"$set": x}; 159 | } else if(row.op === "d") { 160 | row.op = "u"; 161 | row.o2 = {"_id": row.o._id}; 162 | row.o = {"$pull": x}; 163 | } 164 | } 165 | 166 | for(var i in row.o) { 167 | var v = row.o[i]; 168 | 169 | if(v === "") { 170 | delete row.o[i]; 171 | } 172 | } 173 | } 174 | } catch (e) 175 | { 176 | console.log("ERROR in oplog record: "+ row.id+", o="+row.o); 177 | throw e; 178 | } 179 | 180 | return row; 181 | }, 182 | 183 | _tailingJob : function() { 184 | var self = this; 185 | 186 | if(self._stopped && self._tailingJobIntervalId) { 187 | Meteor.clearInterval(self._tailingJobIntervalId); 188 | self._tailingJobIntervalId = null; 189 | self._tailingJobActive = false; 190 | } 191 | 192 | if(self._tailingJobActive) { 193 | return; 194 | } 195 | 196 | self._tailingJobActive = true; 197 | 198 | try { 199 | if(self._lastProcessedId !== null) { 200 | 201 | // Start after the last entry that currently exists. 202 | var oplogSelector = {}; 203 | 204 | oplogSelector.id = {$gt: self._lastProcessedId}; 205 | 206 | var tail = self._oplogConnection.find( 207 | OPLOG_COLLECTION, oplogSelector, {skip: 0, limit: 100, sort: {id: 1}, fields: {_id:0, id: 1, ts: 1, tr 208 | :1, v:1, op:1, ns:1, o:1 , o2:1}}); 209 | 210 | var rows = tail.fetch(); 211 | 212 | var trows = []; 213 | for(var i = 0; i < rows.length; i++) { 214 | var row = rows[i]; 215 | 216 | row = self._transformOplogRecord(row); 217 | trows.push(row); 218 | } 219 | 220 | 221 | for(var i = 0; i < trows.length; i++) { 222 | var row = trows[i]; 223 | 224 | // Try to merge the next one 225 | var cont = true; 226 | while(cont && i < rows.length-1) { 227 | var nrow = trows[i+1]; 228 | 229 | if(row.op === 'u' && nrow.op === 'u' && row.tr === nrow.tr && row.ns === nrow.ns && EJSON.equals(row.o2, nrow.o2)) { 230 | OplogHandle._mergeLogs(row.o, nrow.o); 231 | row.id = nrow.id; 232 | i++; 233 | } else { 234 | cont = false; 235 | } 236 | } 237 | 238 | self._entryQueue.push(row); 239 | self._maybeStartWorker(); 240 | self._lastProcessedId = trows[i].id; 241 | } 242 | 243 | if(rows.length > 0) { 244 | if(Oracle._defaultOracleOptions.sqlDebug) { 245 | console.log("OPLOG: processed "+rows.length+" records, lastProcessedId: "+self._lastProcessedId); 246 | } 247 | } 248 | } 249 | } catch(e) { 250 | console.log("ERROR in oplog tailing job: "+e); 251 | throw e; 252 | } finally { 253 | self._tailingJobActive = false; 254 | } 255 | }, 256 | // Calls `callback` once the oplog has been processed up to a point that is 257 | // roughly "now": specifically, once we've processed all ops that are 258 | // currently visible. 259 | // XXX become convinced that this is actually safe even if oplogConnection 260 | // is some kind of pool 261 | waitUntilCaughtUp: function () { 262 | var self = this; 263 | if (self._stopped) 264 | throw new Error("Called waitUntilCaughtUp on stopped handle!"); 265 | 266 | // Calling waitUntilCaughtUp requries us to wait for the oplog connection to 267 | // be ready. 268 | self._readyFuture.wait(); 269 | 270 | while (!self._stopped) { 271 | // We need to make the selector at least as restrictive as the actual 272 | // tailing selector (ie, we need to specify the DB name) or else we might 273 | // find a id that won't show up in the actual tail stream. 274 | try { 275 | var lastEntry = self._oplogConnection.findOne( 276 | OPLOG_COLLECTION, {}, 277 | {fields: {_id:0, id: 1}, sort: {id: -1}}); 278 | break; 279 | } catch (e) { 280 | // During failover (eg) if we get an exception we should log and retry 281 | // instead of crashing. 282 | Meteor._debug("Got exception while reading last entry: " + e); 283 | Meteor._sleepForMs(100); 284 | } 285 | } 286 | 287 | if (self._stopped) 288 | return; 289 | 290 | if (!lastEntry) { 291 | // Really, nothing in the oplog? Well, we've processed everything. 292 | return; 293 | } 294 | 295 | var id = lastEntry.id; 296 | if (!id) 297 | throw Error("oplog entry without id: " + EJSON.stringify(lastEntry)); 298 | 299 | if (self._lastProcessedId && id <= (self._lastProcessedId)) { 300 | // We've already caught up to here. 301 | return; 302 | } 303 | 304 | 305 | // Insert the future into our list. Almost always, this will be at the end, 306 | // but it's conceivable that if we fail over from one primary to another, 307 | // the oplog entries we see will go backwards. 308 | var insertAfter = self._catchingUpFutures.length; 309 | while (insertAfter - 1 > 0 310 | && self._catchingUpFutures[insertAfter - 1].id > (id)) { 311 | insertAfter--; 312 | } 313 | var f = new Future; 314 | self._catchingUpFutures.splice(insertAfter, 0, {id: id, future: f}); 315 | f.wait(); 316 | }, 317 | 318 | _startTailing: function () { 319 | var self = this; 320 | 321 | // The oplog connection 322 | self._oplogConnection = new OracleConnection(); 323 | 324 | if(self._lastProcessedId === null) { 325 | 326 | try { 327 | // Find the last oplog entry. 328 | var lastOplogEntry = self._oplogConnection.findOne( 329 | OPLOG_COLLECTION, {}, {sort: {id: -1}, fields: {_id:0, id: 1}}); 330 | 331 | if (lastOplogEntry) { 332 | // If there are any calls to callWhenProcessedLatest before any other 333 | // oplog entries show up, allow callWhenProcessedLatest to call its 334 | // callback immediately. 335 | self._setLastProcessedId(lastOplogEntry.id); 336 | } else { 337 | self._setLastProcessedId(0); 338 | } 339 | 340 | } catch(ex) 341 | { 342 | if(ex.message === "Table \"oplog\" does not exist and can't be created in strict mode") { 343 | console.log("WARNING: oplog tailing not started in strict mode due to missing oplog table"); 344 | self._readyFuture.return(); 345 | return; 346 | } else { 347 | throw ex; 348 | } 349 | } 350 | } 351 | 352 | self._tailingJobIntervalId = Meteor.setInterval(function f() {self._tailingJob();}, 100); 353 | 354 | self._readyFuture.return(); 355 | }, 356 | 357 | _maybeStartWorker: function () { 358 | var self = this; 359 | if (self._workerActive) 360 | return; 361 | self._workerActive = true; 362 | Meteor.defer(function () { 363 | try { 364 | while (! self._stopped && ! self._entryQueue.isEmpty()) { 365 | // Are we too far behind? Just tell our observers that they need to 366 | // repoll, and drop our queue. 367 | if (self._entryQueue.length > TOO_FAR_BEHIND) { 368 | var lastEntry = self._entryQueue.pop(); 369 | self._entryQueue.clear(); 370 | 371 | self._onSkippedEntriesHook.each(function (callback) { 372 | callback(); 373 | return true; 374 | }); 375 | 376 | // Free any waitUntilCaughtUp() calls that were waiting for us to 377 | // pass something that we just skipped. 378 | self._setLastProcessedId(lastEntry.id); 379 | continue; 380 | } 381 | 382 | var doc = self._entryQueue.shift(); 383 | 384 | if (!(doc.ns && doc.ns.length > self._dbName.length + 1 && 385 | doc.ns.substr(0, self._dbName.length + 1) === 386 | (self._dbName + '.'))) { 387 | throw new Error("Unexpected ns: "+doc.ns+"/"+self._dbName); 388 | } 389 | 390 | var trigger = {collection: doc.ns.substr(self._dbName.length + 1), 391 | dropCollection: false, 392 | dropDatabase: false, 393 | op: doc}; 394 | 395 | // Is it a special command and the collection name is hidden somewhere 396 | // in operator? 397 | if (trigger.collection === "$cmd") { 398 | if (doc.o.dropDatabase) { 399 | delete trigger.collection; 400 | trigger.dropDatabase = true; 401 | } else if (_.has(doc.o, 'drop')) { 402 | trigger.collection = doc.o.drop; 403 | trigger.dropCollection = true; 404 | trigger.id = null; 405 | } else { 406 | throw Error("Unknown command " + JSON.stringify(doc)); 407 | } 408 | } else { 409 | // All other ops have an id. 410 | trigger.id = idForOp(doc); 411 | } 412 | 413 | self._crossbar.fire(trigger); 414 | 415 | // Now that we've processed this operation, process pending 416 | // sequencers. 417 | if (!doc.id) 418 | throw Error("oplog entry without id: " + EJSON.stringify(doc)); 419 | self._setLastProcessedId(doc.id); 420 | } 421 | } finally { 422 | self._workerActive = false; 423 | } 424 | }); 425 | }, 426 | _setLastProcessedId: function (id) { 427 | var self = this; 428 | 429 | self._lastProcessedId = id; 430 | while (!_.isEmpty(self._catchingUpFutures) 431 | && self._catchingUpFutures[0].id <= ( 432 | self._lastProcessedId)) { 433 | var sequencer = self._catchingUpFutures.shift(); 434 | sequencer.future.return(); 435 | } 436 | } 437 | }); 438 | 439 | 440 | -------------------------------------------------------------------------------- /lib/server/oracle_selector.js: -------------------------------------------------------------------------------- 1 | // The minimongo selector compiler! 2 | 3 | // Terminology: 4 | // - a "selector" is the EJSON object representing a selector 5 | // - a "matcher" is its compiled form (whether a full OracleSelector 6 | // object or one of the component lambdas that matches parts of it) 7 | // - a "result object" is an object with a "result" field and maybe 8 | // distance and arrayIndices. 9 | // - a "branched value" is an object with a "value" field and maybe 10 | // "dontIterate" and "arrayIndices". 11 | // - a "document" is a top-level object that can be stored in a collection. 12 | // - a "lookup function" is a function that takes in a document and returns 13 | // an array of "branched values". 14 | // - a "branched matcher" maps from an array of branched values to a result 15 | // object. 16 | // - an "element matcher" maps from a single value to a bool. 17 | 18 | // Main entry point. 19 | // var matcher = new OracleSelector({a: {$gt: 5}}); 20 | // if (matcher.documentMatches({a: 7})) ... 21 | OracleSelector = function (selector, tableName, db) { 22 | var self = this; 23 | // A set (object mapping string -> *) of all of the document paths looked 24 | // at by the selector. Also includes the empty string if it may look at any 25 | // path (eg, $where). 26 | self._paths = {}; 27 | // Set to true if compilation finds a $near. 28 | self._hasGeoQuery = false; 29 | // Set to true if compilation finds a $where. 30 | self._hasWhere = false; 31 | // Set to false if compilation finds anything other than a simple equality or 32 | // one or more of '$gt', '$gte', '$lt', '$lte', '$ne', '$in', '$nin' used with 33 | // scalars as operands. 34 | self._isSimple = true; 35 | // Set to a dummy document which always matches this Matcher. Or set to null 36 | // if such document is too hard to find. 37 | self._matchingDocument = undefined; 38 | // A clone of the original selector. It may just be a function if the user 39 | // passed in a function; otherwise is definitely an object (eg, IDs are 40 | // translated into {_id: ID} first. Used by canBecomeTrueByModifier and 41 | // Sorter._useWithMatcher. 42 | self._selector = null; 43 | self._tableName = tableName; 44 | self._fields = {}; 45 | self._db = db; 46 | self._docMatcher = self._compileSelector(selector); 47 | }; 48 | 49 | OracleSelector._prepare = function(selector, tableName, db) { 50 | var sls = {}; 51 | 52 | if(!selector) { 53 | return sls; 54 | } 55 | 56 | var tableDesc = undefined; 57 | 58 | if(tableName && db) { 59 | tableDesc = db._tables[tableName]; 60 | } 61 | for(var fn in selector) { 62 | if(typeof fn === 'string' && fn[0] === '.') { 63 | var fa = fn.slice(1).split("."); 64 | 65 | var sl = sls; 66 | 67 | for(var i = 0; i < fa.length; i++) { 68 | var fi = fa[i]; 69 | 70 | if(sl[fi] === undefined) { 71 | var n = {}; 72 | sl[fi] = n; 73 | sl = n; 74 | } 75 | } 76 | 77 | if(sl["."] === undefined) { 78 | sl["."] = selector[fn]; 79 | } else { 80 | throw new Error("Duplicate embedded selector "+fn+", use $elemMatch to concatenate"); 81 | } 82 | } else if(fn.indexOf(".") < 0 && tableDesc && tableDesc.details && tableDesc.details[fn]) { 83 | // Add nested field to sls["."] 84 | var sl = sls["."]; 85 | 86 | if(sl === undefined) { 87 | sl = {}; 88 | sls["."] = sl; 89 | } 90 | 91 | sl[fn+"._value"] = selector[fn]; 92 | 93 | } else { 94 | // Add field to sls["."] 95 | var sl = sls["."]; 96 | 97 | if(sl === undefined) { 98 | sl = {}; 99 | sls["."] = sl; 100 | } 101 | 102 | sl[fn] = selector[fn]; 103 | } 104 | } 105 | 106 | return sls; 107 | }; 108 | 109 | OracleSelector._compile = function(selector, tableName) { 110 | var s = new OracleSelector(selector, tableName); 111 | 112 | return s._docMatcher; 113 | }; 114 | 115 | OracleSelector._compilePrepared = function(selector, tableName, db) { 116 | var rs = {}; 117 | for(fn in selector) { 118 | var value = selector[fn]; 119 | 120 | if(fn === ".") { 121 | var s = new OracleSelector(value, tableName, db); 122 | rs[fn] = s._docMatcher; 123 | rs["*"] = s._fields; 124 | } else { 125 | var nestedTableName = tableName+"$"+fn; 126 | 127 | rs[fn] = OracleSelector._compilePrepared(value, nestedTableName, db); 128 | } 129 | } 130 | 131 | return rs; 132 | }; 133 | 134 | OracleSelector.process = function(selector, tableName, db) { 135 | var s = OracleSelector._prepare(selector, tableName, db); 136 | 137 | s = OracleSelector._compilePrepared(s, tableName, db); 138 | 139 | return s; 140 | }; 141 | 142 | _.extend(OracleSelector.prototype, { 143 | hasGeoQuery: function () { 144 | return this._hasGeoQuery; 145 | }, 146 | hasWhere: function () { 147 | return this._hasWhere; 148 | }, 149 | isSimple: function () { 150 | return this._isSimple; 151 | }, 152 | 153 | // Given a selector, return a function that takes one argument, a 154 | // document. It returns a result object. 155 | _compileSelector: function (selector) { 156 | var self = this; 157 | 158 | // shorthand -- scalars match _id 159 | if (LocalCollection._selectorIsId(selector)) { 160 | self._selector = {_id: selector}; 161 | self._recordPathUsed('_id'); 162 | return OracleDB.q("_id")+" = "+_prepareOperand(selector); 163 | } 164 | 165 | // protect against dangerous selectors. falsey and {_id: falsey} are both 166 | // likely programmer error, and not what you want, particularly for 167 | // destructive operations. 168 | if (!selector || (('_id' in selector) && !selector._id)) { 169 | self._isSimple = false; 170 | return "1 = 0"; 171 | } 172 | 173 | // Top level can't be an array or true or binary. 174 | if (typeof(selector) === 'boolean' || isArray(selector) || 175 | EJSON.isBinary(selector)) 176 | throw new Error("Invalid selector: " + selector); 177 | 178 | self._selector = EJSON.clone(selector); 179 | return self.compileDocumentSelector(selector, self, {isRoot: true}); 180 | }, 181 | _recordPathUsed: function (path) { 182 | this._paths[path] = true; 183 | }, 184 | // Returns a list of key paths the given selector is looking for. It includes 185 | // the empty string if there is a $where. 186 | _getPaths: function () { 187 | return _.keys(this._paths); 188 | } 189 | }); 190 | 191 | var _convertDate = function(date) { 192 | // Create a date object with the current time 193 | var now = date; 194 | 195 | // Create an array with the current month, day and time 196 | var date = [ now.getMonth() + 1, now.getDate(), now.getFullYear() ]; 197 | 198 | // Create an array with the current hour, minute and second 199 | var time = [ now.getHours(), now.getMinutes(), now.getSeconds() ]; 200 | 201 | // If seconds and minutes are less than 10, add a zero 202 | for ( var i = 0; i < 2; i++ ) { 203 | if ( date[i] < 10 ) { 204 | date[i] = "0" + date[i]; 205 | } 206 | } 207 | 208 | // If seconds and minutes are less than 10, add a zero 209 | for ( var i = 0; i < 3; i++ ) { 210 | if ( time[i] < 10 ) { 211 | time[i] = "0" + time[i]; 212 | } 213 | } 214 | 215 | // Return the formatted string 216 | var s = date.join("/") + " " + time.join(":"); 217 | 218 | var ms = now.getMilliseconds(); 219 | 220 | if(ms === 0) { 221 | s = "to_date('"+s+"', 'mm/dd/yyyy hh24:mi:ss')"; 222 | } else { 223 | var mss = ms.toString(); 224 | while(mss.length < 3) { 225 | mss = '0' + mss; 226 | } 227 | s = s + "." + mss; 228 | s = "to_timestamp('"+s+"', 'mm/dd/yyyy hh24:mi:ss.ff3')"; 229 | } 230 | 231 | return s; 232 | }; 233 | 234 | var _prepareOperand = function(operand) { 235 | var ret = null; 236 | 237 | if(operand instanceof Array) { 238 | operand = operand[0]; 239 | } 240 | 241 | if (typeof operand === 'boolean') { 242 | ret = operand ? "'true'" : "'false'"; 243 | } else if (typeof operand === 'string') { 244 | operand = operand.replace("'", "''"); 245 | operand = operand.replace("\n", "' || chr(10) ||'"); 246 | ret = "'"+operand+"'"; 247 | } else if(operand instanceof Date) { 248 | ret = _convertDate(operand); 249 | } else { 250 | ret = operand; 251 | } 252 | 253 | return ret; 254 | }; 255 | 256 | 257 | // Takes in a selector that could match a full document (eg, the original 258 | // selector). Returns a function mapping document->result object. 259 | // 260 | // matcher is the Matcher object we are compiling. 261 | // 262 | // If this is the root document selector (ie, not wrapped in $and or the like), 263 | // then isRoot is true. (This is used by $near.) 264 | OracleSelector.prototype.compileDocumentSelector = function (docSelector, matcher, options) { 265 | var self = this; 266 | options = options || {}; 267 | var docMatchers = []; 268 | _.each(docSelector, function (subSelector, key) { 269 | if (key.substr(0, 1) === '$') { 270 | // Outer operators are either logical operators (they recurse back into 271 | // this function), or $where. 272 | if (!_.has(LOGICAL_OPERATORS, key)) 273 | throw new Error("Unrecognized logical operator: " + key); 274 | matcher._isSimple = false; 275 | var logicalMatcher = LOGICAL_OPERATORS[key](subSelector, matcher, 276 | options.inElemMatch, self); 277 | if(logicalMatcher && logicalMatcher !== "") { 278 | docMatchers.push(logicalMatcher); 279 | } 280 | } else { 281 | // Record this path, but only if we aren't in an elemMatcher, since in an 282 | // elemMatch this is a path inside an object in an array, not in the doc 283 | // root. 284 | if (!options.inElemMatch) 285 | matcher._recordPathUsed(key); 286 | 287 | var valueMatcher = 288 | self.compileValueSelector(subSelector, matcher, options.isRoot); 289 | 290 | if(valueMatcher && valueMatcher !== "") { 291 | var parts = key.split('.'); 292 | var firstPart = parts.length ? parts[0] : ''; 293 | 294 | if(isNumericKey(firstPart)) { 295 | throw new Error("Wrong key if firstPartIsNumeric"); 296 | } 297 | var nextPartIsNumeric = parts.length >= 2 && isNumericKey(parts[1]); 298 | 299 | var s; 300 | var firstPartTable = OracleDB._tableNameM2O(firstPart); 301 | 302 | if(parts.length <= 1) { 303 | self._fields[key] = null; 304 | 305 | var column = OracleDB._columnNameM2O(key); 306 | 307 | column = OracleDB.q(column); 308 | 309 | s = valueMatcher(column); 310 | } else if(parts.length === 2) { 311 | if(isNumericKey(parts[1])) { 312 | throw new Error("Wrong key field.N"); 313 | } 314 | 315 | var column = OracleDB._columnNameM2O(parts[1]); 316 | 317 | column = OracleDB.q(column); 318 | 319 | if(parts[1] === "_value" && subSelector["$ne"] !== undefined) { 320 | var valueMatcher2 = 321 | self.compileValueSelector({"$eq": subSelector["$ne"]}, matcher, options.isRoot); 322 | 323 | s = OracleDB.q("_id")+" NOT IN (SELECT DISTINCT "+OracleDB.q("_id")+" FROM "+OracleDB.q(self._tableName+"$"+firstPartTable)+" WHERE "+valueMatcher2(column)+")"; 324 | } else { 325 | s = OracleDB.q("_id")+" IN (SELECT DISTINCT "+OracleDB.q("_id")+" FROM "+OracleDB.q(self._tableName+"$"+firstPartTable)+" WHERE "+valueMatcher(column)+")"; 326 | } 327 | } else if(parts.length === 3) { 328 | if(!isNumericKey(parts[1])) { 329 | throw new Error("Wrong key field.field.field"); 330 | } 331 | if(isNumericKey(parts[2])) { 332 | throw new Error("Wrong key field.field.N"); 333 | } 334 | 335 | var column = OracleDB._columnNameM2O(parts[2]); 336 | 337 | column = OracleDB.q(column); 338 | 339 | s = OracleDB.q("_id")+" IN (SELECT DISTINCT "+OracleDB.q("_id")+" FROM "+OracleDB.q(self._tableName+"$"+firstPartTable)+" WHERE "+OracleDB.q("_indexNo")+" = "+parts[1]+" AND "+valueMatcher(column)+")"; 340 | } else { 341 | throw new Error("Wrong key with more than 2 dots"); 342 | } 343 | 344 | docMatchers.push(s); 345 | } 346 | } 347 | }); 348 | 349 | return andDocumentMatchers(docMatchers); 350 | }; 351 | 352 | // Takes in a selector that could match a key-indexed value in a document; eg, 353 | // {$gt: 5, $lt: 9}, or a regular expression, or any non-expression object (to 354 | // indicate equality). Returns a branched matcher: a function mapping 355 | // [branched value]->result object. 356 | OracleSelector.prototype.compileValueSelector = function (valueSelector, matcher, isRoot) { 357 | var self = this; 358 | 359 | if (valueSelector instanceof RegExp) { 360 | matcher._isSimple = false; 361 | return convertElementMatcherToBranchedMatcher( 362 | regexpElementMatcher(valueSelector)); 363 | } else if (isOperatorObject(valueSelector)) { 364 | return self.operatorBranchedMatcher(valueSelector, matcher, isRoot); 365 | } else { 366 | return equalityElementMatcher(valueSelector); 367 | } 368 | }; 369 | 370 | // Given an element matcher (which evaluates a single value), returns a branched 371 | // value (which evaluates the element matcher on all the branches and returns a 372 | // more structured return value possibly including arrayIndices). 373 | var convertElementMatcherToBranchedMatcher = function ( 374 | elementMatcher, options) { 375 | options = options || {}; 376 | return elementMatcher; 377 | }; 378 | 379 | // Takes a RegExp object and returns an element matcher. 380 | regexpElementMatcher = function (regexp) { 381 | return function (key) { 382 | if (regexp instanceof RegExp) { 383 | throw Error("Regular expression operant has to be a string (not RegExp object)"); 384 | } 385 | 386 | return "REGEXP_LIKE("+key+", "+_prepareOperand(regexp)+")"; 387 | }; 388 | }; 389 | 390 | //Takes something that is not an operator object and returns an element matcher 391 | //for equality with that thing. 392 | equalityElementMatcher = function (elementSelector) { 393 | if (isOperatorObject(elementSelector)) 394 | throw Error("Can't create equalityValueSelector for operator object"); 395 | 396 | // Special-case: null and undefined are equal (if you got undefined in there 397 | // somewhere, or if you got it due to some branch being non-existent in the 398 | // weird special case), even though they aren't with EJSON.equals. 399 | if (elementSelector == null) { // undefined or null 400 | return function (key) { 401 | return key + " IS NULL"; // undefined or null 402 | }; 403 | } 404 | 405 | return function (key) { 406 | return key + " = " + _prepareOperand(elementSelector); 407 | }; 408 | }; 409 | 410 | //Takes something that is not an operator object and returns an element matcher 411 | //for equality with that thing. 412 | inequalityElementMatcher = function (elementSelector) { 413 | if (isOperatorObject(elementSelector)) 414 | throw Error("Can't create equalityValueSelector for operator object"); 415 | 416 | // Special-case: null and undefined are equal (if you got undefined in there 417 | // somewhere, or if you got it due to some branch being non-existent in the 418 | // weird special case), even though they aren't with EJSON.equals. 419 | if (elementSelector == null) { // undefined or null 420 | return function (key) { 421 | return key + " IS NOT NULL"; // undefined or null 422 | }; 423 | } 424 | 425 | return function (key) { 426 | return "(" + key + " <> " + _prepareOperand(elementSelector) + " OR " + key + " IS NULL" + ")"; 427 | }; 428 | }; 429 | 430 | // Takes an operator object (an object with $ keys) and returns a branched 431 | // matcher for it. 432 | OracleSelector.prototype.operatorBranchedMatcher = function (valueSelector, matcher, isRoot) { 433 | var self = this; 434 | // Each valueSelector works separately on the various branches. So one 435 | // operator can match one branch and another can match another branch. This 436 | // is OK. 437 | 438 | var operatorMatchers = []; 439 | _.each(valueSelector, function (operand, operator) { 440 | // XXX we should actually implement $eq, which is new in 2.6 441 | var simpleRange = _.contains(['$lt', '$lte', '$gt', '$gte'], operator) && 442 | _.isNumber(operand); 443 | var simpleInequality = operator === '$ne' && !_.isObject(operand); 444 | var simpleInclusion = _.contains(['$in', '$nin'], operator) && 445 | _.isArray(operand) && !_.any(operand, _.isObject); 446 | 447 | if (! (operator === '$eq' || simpleRange || 448 | simpleInclusion || simpleInequality)) { 449 | matcher._isSimple = false; 450 | } 451 | 452 | if (_.has(VALUE_OPERATORS, operator)) { 453 | operatorMatchers.push( 454 | VALUE_OPERATORS[operator](operand, valueSelector, matcher, isRoot, self)); 455 | } else if (_.has(ELEMENT_OPERATORS, operator)) { 456 | var options = ELEMENT_OPERATORS[operator]; 457 | operatorMatchers.push( 458 | options.compileElementSelector( 459 | operand, self, valueSelector, matcher)); 460 | } else { 461 | throw new Error("Unrecognized operator: " + operator); 462 | } 463 | }); 464 | 465 | return andBranchedMatchers(operatorMatchers); 466 | }; 467 | 468 | OracleSelector.prototype.compileArrayOfDocumentSelectors = function ( 469 | selectors, matcher, inElemMatch) { 470 | var self = this; 471 | 472 | if (!isArray(selectors) || _.isEmpty(selectors)) 473 | throw Error("$and/$or/$nor must be nonempty array"); 474 | return _.map(selectors, function (subSelector) { 475 | if (!isPlainObject(subSelector)) 476 | throw Error("$or/$and/$nor entries need to be full objects"); 477 | return self.compileDocumentSelector( 478 | subSelector, matcher, {inElemMatch: inElemMatch}); 479 | }); 480 | }; 481 | 482 | // Operators that appear at the top level of a document selector. 483 | var LOGICAL_OPERATORS = { 484 | $and: function (subSelector, matcher, inElemMatch, oracleSelector) { 485 | var matchers = oracleSelector.compileArrayOfDocumentSelectors( 486 | subSelector, matcher, inElemMatch); 487 | return andDocumentMatchers(matchers); 488 | }, 489 | 490 | $or: function (subSelector, matcher, inElemMatch, oracleSelector) { 491 | var matchers = oracleSelector.compileArrayOfDocumentSelectors( 492 | subSelector, matcher, inElemMatch); 493 | 494 | // Special case: if there is only one matcher, use it directly, *preserving* 495 | // any arrayIndices it returns. 496 | if (matchers.length === 1) 497 | return matchers[0]; 498 | 499 | return orDocumentMatchers(matchers); 500 | }, 501 | 502 | $nor: function (subSelector, matcher, inElemMatch, oracleSelector) { 503 | var matchers = oracleSelector.compileArrayOfDocumentSelectors( 504 | subSelector, matcher, inElemMatch); 505 | 506 | return "NOT("+orDocumentMatchers(matchers)+")"; 507 | }, 508 | 509 | $where: function (selectorValue, matcher) { 510 | return selectorValue; 511 | }, 512 | 513 | // This is just used as a comment in the query (in MongoDB, it also ends up in 514 | // query logs); it has no effect on the actual selection. 515 | $comment: function () { 516 | return ""; 517 | } 518 | }; 519 | 520 | // Returns a branched matcher that matches iff the given matcher does not. 521 | // Note that this implicitly "deMorganizes" the wrapped function. ie, it 522 | // means that ALL branch values need to fail to match innerBranchedMatcher. 523 | var invertBranchedMatcher = function (branchedMatcher) { 524 | return function (key) { 525 | var invertMe = branchedMatcher(key); 526 | // We explicitly choose to strip arrayIndices here: it doesn't make sense to 527 | // say "update the array element that does not match something", at least 528 | // in mongo-land. 529 | return "NOT("+invertMe+")"; 530 | }; 531 | }; 532 | 533 | // Operators that (unlike LOGICAL_OPERATORS) pertain to individual paths in a 534 | // document, but (unlike ELEMENT_OPERATORS) do not have a simple definition as 535 | // "match each branched value independently and combine with 536 | // convertElementMatcherToBranchedMatcher". 537 | var VALUE_OPERATORS = { 538 | $not: function (operand, valueSelector, matcher, isRoot, oracleSelector) { 539 | return invertBranchedMatcher(oracleSelector.compileValueSelector(operand, matcher)); 540 | }, 541 | $eq: function (operand) { 542 | return convertElementMatcherToBranchedMatcher( 543 | equalityElementMatcher(operand)); 544 | }, 545 | $ne: function (operand) { 546 | return convertElementMatcherToBranchedMatcher( 547 | inequalityElementMatcher(operand)); 548 | }, 549 | $exists: function (operand) { 550 | var exists = convertElementMatcherToBranchedMatcher(function (key) { 551 | return key + (operand ? " IS NOT NULL" : " IS NULL"); 552 | }); 553 | return exists; 554 | }, 555 | // $options just provides options for $regex; its logic is inside $regex 556 | $options: function (operand, valueSelector) { 557 | if (!_.has(valueSelector, '$regex')) 558 | throw Error("$options needs a $regex"); 559 | return ""; 560 | }, 561 | // $maxDistance is basically an argument to $near 562 | $maxDistance: function (operand, valueSelector) { 563 | throw Error("$maxDistance operator is not supported yet"); 564 | }, 565 | $all: function (operand, valueSelector, matcher) { 566 | throw Error("$all operator is not supported yet"); 567 | }, 568 | $near: function (operand, valueSelector, matcher, isRoot) { 569 | throw Error("$near operator is not supported yet"); 570 | } 571 | }; 572 | 573 | // Helper for $lt/$gt/$lte/$gte. 574 | var makeInequality = function (cmpValueComparator) { 575 | return { 576 | compileElementSelector: function (operand, oracleSelector) { 577 | // Arrays never compare false with non-arrays for any inequality. 578 | // XXX This was behavior we observed in pre-release MongoDB 2.5, but 579 | // it seems to have been reverted. 580 | // See https://jira.mongodb.org/browse/SERVER-11444 581 | if (isArray(operand)) { 582 | return function () { 583 | return false; 584 | }; 585 | } 586 | 587 | // Special case: consider undefined and null the same (so true with 588 | // $gte/$lte). 589 | if (operand === undefined) 590 | operand = null; 591 | 592 | var operandType = LocalCollection._f._type(operand); 593 | 594 | return function (key) { 595 | return cmpValueComparator(key, operand); 596 | }; 597 | } 598 | }; 599 | }; 600 | 601 | // Each element selector contains: 602 | // - compileElementSelector, a function with args: 603 | // - operand - the "right hand side" of the operator 604 | // - valueSelector - the "context" for the operator (so that $regex can find 605 | // $options) 606 | // - matcher - the Matcher this is going into (so that $elemMatch can compile 607 | // more things) 608 | // returning a function mapping a single value to bool. 609 | // - dontExpandLeafArrays, a bool which prevents expandArraysInBranches from 610 | // being called 611 | // - dontIncludeLeafArrays, a bool which causes an argument to be passed to 612 | // expandArraysInBranches if it is called 613 | ELEMENT_OPERATORS = { 614 | $lt: makeInequality(function (key, operand) { 615 | return key + " < " + _prepareOperand(operand); 616 | }), 617 | $like: makeInequality(function (key, operand) { 618 | return key + " LIKE " + _prepareOperand(operand); 619 | }), 620 | $gt: makeInequality(function (key, operand) { 621 | return key + " > " + _prepareOperand(operand); 622 | }), 623 | $lte: makeInequality(function (key, operand) { 624 | return key + " <= " + _prepareOperand(operand); 625 | }), 626 | $gte: makeInequality(function (key, operand) { 627 | return key + " >= " + _prepareOperand(operand); 628 | }), 629 | $mod: { 630 | compileElementSelector: function (operand) { 631 | if (!(isArray(operand) && operand.length === 2 632 | && typeof(operand[0]) === 'number' 633 | && typeof(operand[1]) === 'number')) { 634 | throw Error("argument to $mod must be an array of two numbers"); 635 | } 636 | // XXX could require to be ints or round or something 637 | var divisor = operand[0]; 638 | var remainder = operand[1]; 639 | return function (key) { 640 | return "mod("+key+ ", " + _prepareOperand(divisor)+") = "+_prepareOperand(remainder); 641 | }; 642 | } 643 | }, 644 | $in: { 645 | compileElementSelector: function (operand, oracleSelector) { 646 | if (!isArray(operand)) 647 | throw Error("$in needs an array"); 648 | 649 | var elementMatchers = []; 650 | _.each(operand, function (option) { 651 | if (option instanceof RegExp) 652 | throw Error("regexp inside $in is not supported yet"); 653 | else if (isOperatorObject(option)) 654 | throw Error("cannot nest $ under $in"); 655 | else 656 | elementMatchers.push(option); 657 | }); 658 | 659 | if(elementMatchers.length === 0) { 660 | // throw Error("no values applied to $in"); 661 | // Looks like on the server side $in: [] should work, at least Telescope produces this query 662 | // So generate a FALSE expression 663 | return function(key) { 664 | return "(1=0)"; 665 | } 666 | } 667 | return function (key) { 668 | var tableDesc = oracleSelector._db && oracleSelector._db._tables[oracleSelector._tableName]; 669 | var ret = undefined; 670 | 671 | var inClause = "IN ("; 672 | var i = 0; 673 | _.any(elementMatchers, function (e) { 674 | if(i > 0) { 675 | inClause = inClause + ", "; 676 | } 677 | inClause = inClause + _prepareOperand(e); 678 | i++; 679 | 680 | return false; 681 | }); 682 | inClause = inClause + ")"; 683 | 684 | var detDesc = tableDesc && tableDesc.details[OracleDB.uq(key)]; 685 | 686 | if(detDesc) { 687 | // Complex detail field 688 | ret = '"_id" IN (SELECT UNIQUE "_id" FROM '+OracleDB.q(detDesc.tableName)+' WHERE "_value" '+inClause+')'; 689 | } else { 690 | ret = key + " " + inClause 691 | } 692 | 693 | return ret; 694 | }; 695 | } 696 | }, 697 | $nin: { 698 | compileElementSelector: function (operand) { 699 | if (!isArray(operand)) 700 | throw Error("$in needs an array"); 701 | 702 | var elementMatchers = []; 703 | _.each(operand, function (option) { 704 | if (option instanceof RegExp) 705 | throw Error("regexp inside $nin is not supported yet"); 706 | else if (isOperatorObject(option)) 707 | throw Error("cannot nest $ under $in"); 708 | else 709 | elementMatchers.push(option); 710 | }); 711 | 712 | if(elementMatchers.length === 0) { 713 | // throw Error("no values applied to $in"); 714 | // Looks like on the server side $nin: [] should work, at least Telescope produces this query with $in 715 | // So generate a TRUE expression 716 | return function(key) { 717 | return "(1=1)"; 718 | } 719 | } 720 | return function (key) { 721 | var i = 0; 722 | var ret = key + " NOT IN ("; 723 | _.any(elementMatchers, function (e) { 724 | if(i > 0) { 725 | ret = ret + ", "; 726 | } 727 | ret = ret + _prepareOperand(e); 728 | i++; 729 | 730 | return false; 731 | }); 732 | ret = ret + ")"; 733 | return ret; 734 | }; 735 | } 736 | }, 737 | $size: { 738 | // {a: [[5, 5]]} must match {a: {$size: 1}} but not {a: {$size: 2}}, so we 739 | // don't want to consider the element [5,5] in the leaf array [[5,5]] as a 740 | // possible value. 741 | dontExpandLeafArrays: true, 742 | compileElementSelector: function (operand) { 743 | throw Error("$size operator is not supported yet"); 744 | } 745 | }, 746 | $type: { 747 | // {a: [5]} must not match {a: {$type: 4}} (4 means array), but it should 748 | // match {a: {$type: 1}} (1 means number), and {a: [[5]]} must match {$a: 749 | // {$type: 4}}. Thus, when we see a leaf array, we *should* expand it but 750 | // should *not* include it itself. 751 | dontIncludeLeafArrays: true, 752 | compileElementSelector: function (operand) { 753 | throw Error("$type operator is not supported yet"); 754 | } 755 | }, 756 | $regex: { 757 | compileElementSelector: function (operand, oracleSelector, valueSelector) { 758 | if (!(typeof operand === 'string' || operand instanceof RegExp)) 759 | throw Error("$regex has to be a string or RegExp"); 760 | 761 | var regexp; 762 | if (valueSelector.$options !== undefined) { 763 | if (/[^gim]/.test(valueSelector.$options)) 764 | throw new Error("Only the i, m, and g regexp options are supported"); 765 | 766 | var regexSource = operand instanceof RegExp ? operand.source : operand; 767 | regexp = new RegExp(regexSource, valueSelector.$options).toString(); 768 | } else if (operand instanceof RegExp) { 769 | regexp = operand.toString(); 770 | } else { 771 | regexp = operand; 772 | } 773 | return regexpElementMatcher(regexp); 774 | } 775 | }, 776 | $elemMatch: { 777 | dontExpandLeafArrays: true, 778 | compileElementSelector: function (operand, oracleSelector, valueSelector, matcher) { 779 | return function (key) { 780 | var nestedTableName = OracleDB.q(oracleSelector._tableName+"$"+OracleDB.uq(key)); 781 | 782 | return OracleDB.q("_id")+" IN (SELECT DISTINCT "+OracleDB.q("_id")+" FROM "+nestedTableName+" WHERE "+ 783 | oracleSelector.compileDocumentSelector(operand, matcher, {inElemMatch: true}) + ")"; 784 | } 785 | } 786 | } 787 | }; 788 | 789 | // NB: We are cheating and using this function to implement "AND" for both 790 | // "document matchers" and "branched matchers". They both return result objects 791 | // but the argument is different: for the former it's a whole doc, whereas for 792 | // the latter it's an array of "branched values". 793 | var operatorMatchers = function (subMatchers, operator) { 794 | if (subMatchers.length === 0) 795 | return ""; 796 | if (subMatchers.length === 1) 797 | return subMatchers[0]; 798 | 799 | var i = 0; 800 | var ret = ""; 801 | _.all(subMatchers, function (f) { 802 | if(f && f !== "") { 803 | if(i > 0) { 804 | ret = ret + " "+operator+" "; 805 | } 806 | ret = ret + "(" + f + ")"; 807 | i++; 808 | } 809 | return true; 810 | }); 811 | 812 | return ret; 813 | }; 814 | 815 | var andDocumentMatchers = function(subMatchers) {return operatorMatchers(subMatchers, "AND");}; 816 | var orDocumentMatchers = function(subMatchers) {return operatorMatchers(subMatchers, "OR");}; 817 | 818 | var operatorBranchedMatchers = function (subMatchers, operator) { 819 | return function f(key) { 820 | if (subMatchers.length === 0) 821 | return ""; 822 | if (subMatchers.length === 1) 823 | return subMatchers[0](key); 824 | 825 | var i = 0; 826 | var ret = ""; 827 | _.all(subMatchers, function (f) { 828 | if(f && f !== "") { 829 | if(i > 0) { 830 | ret = ret + " "+operator+" "; 831 | } 832 | ret = ret + "(" + f(key) + ")"; 833 | i++; 834 | } 835 | return true; 836 | }); 837 | 838 | return ret; 839 | } 840 | }; 841 | 842 | var andBranchedMatchers = function(subMatchers) {return operatorBranchedMatchers(subMatchers, "AND");}; 843 | -------------------------------------------------------------------------------- /lib/server/oracle_sorter.js: -------------------------------------------------------------------------------- 1 | 2 | OracleSorter = {}; 3 | 4 | 5 | OracleSorter._compile = function(sort, options) { 6 | var sorter = new Minimongo.Sorter(sort, options); 7 | var orderBy = ""; 8 | 9 | var i = 0; 10 | 11 | for(key in sorter._sortSpecParts) { 12 | var part = sorter._sortSpecParts[key]; 13 | 14 | if(i > 0) { 15 | orderBy = orderBy + ", "; 16 | } 17 | var column = OracleDB._columnNameM2O(part.path); 18 | 19 | column = OracleDB.q(column); 20 | 21 | orderBy = orderBy + column; 22 | if(part.ascending === false) { 23 | orderBy = orderBy + " DESC"; 24 | } 25 | i++; 26 | } 27 | 28 | return orderBy; 29 | }; 30 | 31 | OracleSorter._prepare = function(sort) { 32 | var sls = {}; 33 | 34 | if(!sort) { 35 | return sls; 36 | } 37 | 38 | for(var fn in sort) { 39 | if(typeof fn === 'string' && fn[0] === '.') { 40 | var fa = fn.slice(1).split("."); 41 | 42 | var sl = sls; 43 | 44 | for(var i = 0; i < fa.length; i++) { 45 | var fi = fa[i]; 46 | 47 | if(sl[fi] === undefined) { 48 | var n = {}; 49 | sl[fi] = n; 50 | sl = n; 51 | } 52 | } 53 | 54 | if(sl["."] === undefined) { 55 | sl["."] = sort[fn]; 56 | } else { 57 | throw new Error("Duplicate embedded sort "+fn); 58 | } 59 | } else { 60 | // Add field to sls["."] 61 | var sl = sls["."]; 62 | 63 | if(sl === undefined) { 64 | sl = {}; 65 | sls["."] = sl; 66 | } 67 | 68 | sl[fn] = sort[fn]; 69 | } 70 | } 71 | 72 | return sls; 73 | }; 74 | 75 | OracleSorter._compilePrepared = function(sort, options) { 76 | var rs = {}; 77 | for(fn in sort) { 78 | var value = sort[fn]; 79 | 80 | rs[fn] = fn === "." ? OracleSorter._compile(value, options) : OracleSorter._compilePrepared(value, options); 81 | } 82 | 83 | return rs; 84 | }; 85 | 86 | OracleSorter.process = function(sort, options) { 87 | var s = OracleSorter._prepare(sort); 88 | 89 | s = OracleSorter._compilePrepared(s, options); 90 | 91 | return s; 92 | }; 93 | -------------------------------------------------------------------------------- /lib/server/remote_collection_driver.js: -------------------------------------------------------------------------------- 1 | OracleInternals.RemoteCollectionDriver = function (connectionOptions) { 2 | var self = this; 3 | self.oracleConnection = new OracleConnection(connectionOptions); 4 | }; 5 | 6 | _.extend(OracleInternals.RemoteCollectionDriver.prototype, { 7 | open: function (name) { 8 | var self = this; 9 | var ret = {}; 10 | 11 | _.each( 12 | ['find', 'findOne', 'insert', 'update', 'upsert', 13 | 'remove', '_ensureIndex', '_dropIndex', '_createCappedCollection', 14 | 'dropCollection', 'rawCollection'], 15 | function (m) { 16 | ret[m] = _.bind(self.oracleConnection[m], self.oracleConnection, name); 17 | }); 18 | 19 | return ret; 20 | } 21 | }); 22 | 23 | // Singleton collection driver with oplog tailing feature on 24 | OracleInternals.defaultRemoteCollectionDriver = _.once(function () { 25 | var connectionOptions = {}; 26 | 27 | if (process.env.ORACLE_OPLOG_URL) { 28 | connectionOptions.oplogUrl = process.env.ORACLE_OPLOG_URL; 29 | } else { 30 | connectionOptions.oplogUrl = "localhost/XE"; 31 | } 32 | 33 | return new OracleInternals.RemoteCollectionDriver(connectionOptions); 34 | }); 35 | 36 | -------------------------------------------------------------------------------- /lib/server/tests/collection_tests.js: -------------------------------------------------------------------------------- 1 | Tinytest.add( 2 | 'Oracle.Collection custom select', 3 | function (test) { 4 | // 5 | // Custom selects are not supported yet. 6 | // 7 | 8 | /* 9 | var options = {}; 10 | 11 | var oracleOptions = { 12 | sql: "select * from user_users where username = :1", 13 | sqlParameters: ['METEOR'], 14 | sqlTable: null, // null means no DML, use undefined to apply the collection name as a default 15 | sqlAddId: true 16 | }; 17 | 18 | var coll = new Oracle.Collection("users", options, oracleOptions); 19 | 20 | test.isNotNull(coll); 21 | 22 | var rows = coll.find({}, {skip: 0, limit: 10, sort: {USERNAME: 1}}).fetch(); 23 | 24 | test.equal(rows.length, 1); 25 | test.equal(rows[0].USERNAME, "METEOR"); 26 | 27 | rows = coll.findOne({}, {skip: 0, limit: 10, sort: {USERNAME: 1}}); 28 | 29 | test.equal(rows.USERNAME, "METEOR"); 30 | */ 31 | } 32 | ); 33 | 34 | Tinytest.add( 35 | 'create new collection testNames', 36 | function (test) { 37 | var options = {}; 38 | 39 | var testNames = new Oracle.Collection("testNames", options); 40 | 41 | test.isNotNull(testNames); 42 | 43 | testNames._collection.dropCollection(); 44 | 45 | var s = '## Welcome to Telescope!\n\nIf you\'re reading this, it means you\'ve successfully got Telescope to run.\n'; 46 | 47 | testNames.insert({name: s}); 48 | 49 | testNames._collection._ensureIndex({name: 1}, {unique:true, bitmap:false}); 50 | 51 | var rows = testNames.find({}, {fields: {name: 1, _id: 0}}).fetch(); 52 | 53 | test.equal(rows, [{"name": s}]); 54 | } 55 | ); 56 | 57 | Tinytest.add( 58 | 'create new collection test_lists', 59 | function (test) { 60 | var options = {}; 61 | 62 | var testLists = new Oracle.Collection("testLists", options); 63 | 64 | test.isNotNull(testLists); 65 | 66 | testLists._collection.dropCollection(); 67 | 68 | testLists.insert({name: "Principles", incompleteCount:37, userId: "Michael", closed: true, createdAt: Oracle.LocalDate("2010-05-19T00:00:00"), 69 | tags:[{tag:"CANCELLED", owner: "John"}, {tag: "SUBMITTED", owner: "Mary"}]}); 70 | 71 | testLists.insert({name: "Languages", incompleteCount:22, userId: "Andrea", closed: false, 72 | tags: [{tag:"OK", owner: "Helen" /*, groups: [{name: "ADMINS"}, {name: "USERS"}]*/}, 73 | {tag:"APPROVED", owner: "Keith"}, 74 | {tag: "QA PASSED", owner: "Curtis"}]}); 75 | 76 | testLists.insert({name: "Favorites", incompleteCount:28, userId: "Amit"}); 77 | 78 | // Test indexes 79 | testLists._collection._ensureIndex({userId: 1, name: 1}, {unique:true, bitmap:false}); 80 | testLists._collection._ensureIndex({userId: 1, name: -1}, {unique:true, bitmap:false}); 81 | testLists._collection._ensureIndex({"tags._id": 1, "tags.tag": 1}, {unique:true, bitmap:false}); 82 | testLists._collection._ensureIndex({"tags._id": 1, "tags.tag": -1}, {unique:true, bitmap:false}); 83 | 84 | testLists._collection._dropIndex({userId: 1, name: -1}); 85 | testLists._collection._dropIndex({"tags._id": 1, "tags.tag": -1}); 86 | 87 | // Test the find() functionality 88 | var rows; 89 | 90 | rows = testLists.findOne({}, {skip: 0, limit: 10, fields: {_id: 0, tags: 0}, sort: {name: 1}}); 91 | test.equal(rows, {name: 'Favorites', incompleteCount: 28, userId: 'Amit'}); 92 | 93 | rows = testLists.findOne({'tags.0.tag': "CANCELLED"}, {skip: 0, limit: 10, fields: {_id: 0, tags: 0}, sort: {name: 1}}); 94 | test.equal(rows, {name: "Principles", incompleteCount:37, userId: "Michael", closed: true, createdAt: Oracle.LocalDate("2010-05-19T00:00:00")}); 95 | 96 | rows = testLists.findOne({'tags.0.tag': "CANCELLED"}, {skip: 0, limit: 10, sort: {name: 1}}); 97 | delete rows._id; 98 | test.equal(rows, {"name":"Principles","incompleteCount":37,"userId":"Michael","closed":true,"createdAt":Oracle.LocalDate("2010-05-19T00:00:00"),"tags":[{"tag":"CANCELLED","owner":"John"},{"tag":"SUBMITTED","owner":"Mary"}]}); 99 | 100 | rows = testLists.findOne({incompleteCount:22, ".tags": {tag: {$like: '%A%'}}}, {skip: 0, limit: 10, sort: {name: 1}, fields: {incompleteCount: true, userId: true, tags: true, "tags.tag": true, "tags.owner": true}}); 101 | delete rows._id; 102 | test.equal(rows, {"incompleteCount":22,"userId":"Andrea","tags":[{"tag":"APPROVED","owner":"Keith"},{"tag":"QA PASSED","owner":"Curtis"}]}); 103 | 104 | // TODO: fix complex nested structures 105 | /* 106 | rows = testLists.find({"tags": {$elemMatch: {$or: [{tag: "OK", owner: "Helen"}, {tag: "CANCELLED"}]}}}, {skip: 0, limit: 10, sort: {name: 1}}).fetch(); 107 | for(var i = 0; i < rows.length; i++) { 108 | delete rows[i]._id; 109 | } 110 | 111 | test.equal(rows, [{"name":"Languages","incompleteCount":22,"userId":"Andrea","closed":false,"tags":[{"tag":"OK","owner":"Helen"},{"tag":"APPROVED","owner":"Keith"},{"tag":"QA PASSED","owner":"Curtis"}]}, 112 | {"name":"Principles","incompleteCount":37,"userId":"Michael","closed":true,"createdAt":Oracle.LocalDate("2010-05-19T00:00:00"),"tags":[{"tag":"CANCELLED","owner":"John"},{"tag":"SUBMITTED","owner":"Mary"}]}]); 113 | */ 114 | } 115 | ); 116 | 117 | Tinytest.add( 118 | 'create new todo test_todos', 119 | function (test) { 120 | var testTodos = new Oracle.Collection("testTodos"); 121 | 122 | test.isNotNull(testTodos); 123 | 124 | testTodos._collection.dropCollection(); 125 | 126 | testTodos.insert({name: "Get milk", priority:1, userId: "Amit"}); 127 | testTodos.insert({name: "Tennis match", priority:2, userId: "Amit", categories: ["Sport", "Fun"]}); 128 | 129 | var rows; 130 | 131 | rows = testTodos.find({priority:1}).fetch(); 132 | 133 | for(var i = 0; i < rows.length; i++) { 134 | delete rows[i]._id; 135 | } 136 | 137 | test.equal(rows, [{name: "Get milk", priority:1, userId: "Amit"}]); 138 | 139 | rows = testTodos.find({priority:2}).fetch(); 140 | 141 | for(var i = 0; i < rows.length; i++) { 142 | delete rows[i]._id; 143 | } 144 | 145 | test.equal(rows, [{name: "Tennis match", priority:2, userId: "Amit", categories: ["Sport", "Fun"]}]); 146 | 147 | 148 | // Test Update 149 | testTodos.update({priority:1}, {$set: {priority: 3}}); 150 | testTodos.update({priority:3}, {$addToSet: {categories: {$each:["Food", "Health"]}}}); 151 | 152 | rows = testTodos.find({priority:3}).fetch(); 153 | 154 | for(var i = 0; i < rows.length; i++) { 155 | delete rows[i]._id; 156 | } 157 | 158 | test.equal(rows, [{name: "Get milk", priority:3, userId: "Amit", categories: ["Food", "Health"]}]); 159 | 160 | var _id = testTodos.insert({name: "Swim practice", priority:3, userId: "Josh", score: 2.2}); 161 | 162 | testTodos.update({_id:_id}, {$set: {score: 0.000555000666}}); 163 | 164 | rows = testTodos.findOne({_id:_id}); 165 | 166 | test.equal(rows, {_id: _id, name: "Swim practice", priority:3, userId: "Josh", score: 0.000555000666}); 167 | 168 | } 169 | ); 170 | -------------------------------------------------------------------------------- /lib/server/tests/oracle_db_tests.js: -------------------------------------------------------------------------------- 1 | Tinytest.add( 2 | 'OracleDB', 3 | function (test) { 4 | var oracleOptions = { 5 | connection: { 6 | user: "meteor", 7 | password: "meteor", 8 | connectString : "localhost/XE" 9 | } 10 | }; 11 | 12 | var db = OracleDB.getDatabase(oracleOptions); 13 | 14 | test.isNotNull(db); 15 | 16 | var result = OracleDB.executeCommand(connection, "select * from user_users", [], {}); 17 | 18 | test.isNotNull(result); 19 | test.isNotNull(result.rows); 20 | test.isNotNull(result.metaData); 21 | } 22 | ); 23 | 24 | Tinytest.add( 25 | 'OracleDB Error', 26 | function (test) { 27 | var oracleOptions = { 28 | connection: { 29 | user: "meteor", 30 | password: "meteor", 31 | connectString : "localhost/XE" 32 | } 33 | }; 34 | 35 | var db = OracleDB.getDatabase(oracleOptions); 36 | 37 | 38 | test.isNotNull(db); 39 | 40 | test.throws( 41 | function() { 42 | var result = OracleDB.executeCommand(connection, "select * from missing_table", [], {}); 43 | }, 44 | "ORA-00942: table or view does not exist" 45 | ); 46 | } 47 | ); 48 | 49 | 50 | Tinytest.add( 51 | 'OracleDBBatch', 52 | function (test) { 53 | var oracleOptions = { 54 | connection: { 55 | user: "meteor", 56 | password: "meteor", 57 | connectString : "localhost/XE" 58 | } 59 | }; 60 | 61 | var db = OracleDB.getDatabase(oracleOptions); 62 | 63 | test.isNotNull(db); 64 | 65 | var batch = [{sql:"select * from user_users", sqlParameters: []}, {sql:"select * from user_users", sqlParameters: []}]; 66 | var result = OracleDB.executeBatch(connection, batch, {}); 67 | 68 | test.isNotNull(result); 69 | test.isTrue(result instanceof Array); 70 | test.equal(result.length, 2); 71 | } 72 | ); 73 | 74 | 75 | Tinytest.add( 76 | 'OracleDBBatch Error', 77 | function (test) { 78 | var oracleOptions = { 79 | connection: { 80 | user: "meteor", 81 | password: "meteor", 82 | connectString : "localhost/XE" 83 | } 84 | }; 85 | 86 | var db = OracleDB.getDatabase(oracleOptions); 87 | 88 | test.isNotNull(db); 89 | 90 | test.throws( 91 | function() { 92 | var batch = [{sql:"select * from missing_table", sqlParameters: []}, {sql:"select * from user_users", sqlParameters: []}]; 93 | var result = OracleDB.executeBatch(connection, batch, {}); 94 | }, 95 | "ORA-00942: table or view does not exist" 96 | ); 97 | } 98 | ); 99 | -------------------------------------------------------------------------------- /lib/server/tests/oracle_fields_tests.js: -------------------------------------------------------------------------------- 1 | Tinytest.add('test oracle fields _prepare', function (test, done) { 2 | 3 | var cl; 4 | 5 | // 6 | // Tests with numbers 7 | // 8 | cl = OracleFields._prepare({name: true, age: true}); 9 | test.equal(cl, {".":{"name":true,"age":true}}); 10 | 11 | cl = OracleFields._prepare({name: false, age:false}); 12 | test.equal(cl, {".":{"name":false,"age":false}}); 13 | 14 | cl = OracleFields._prepare({name: false, age:false, "tags.name": true, "tags.owner": true}); 15 | test.equal(cl, {".":{"name":false,"age":false},"tags":{".":{"name":true, "owner": true}}}); 16 | 17 | cl = OracleFields._prepare({"tags._id": 1, "tags.tag": 1}); 18 | test.equal(cl, {"tags":{".":{"_id":1, "tag":1}}}); 19 | }); 20 | 21 | Tinytest.add('test oracle fields getColumnList', function (test, done) { 22 | 23 | var cl; 24 | var s; 25 | 26 | var tableDesc = {columns: {name: {columnId: 1}, age: {columnId: 2}, birthDate: {columnId: 3}, sex: {columnId: 4}}}; 27 | 28 | cl = OracleFields._prepare({name: true, age: true}); 29 | s = OracleFields.getColumnList(tableDesc, cl["."]) 30 | test.equal(s, '"_id", "name", "age"'); 31 | 32 | cl = OracleFields._prepare({_id:false, name: true, age: true}); 33 | s = OracleFields.getColumnList(tableDesc, cl["."]) 34 | test.equal(s, '"name", "age"'); 35 | 36 | cl = OracleFields._prepare({name: true, age: true, _id: false}); 37 | s = OracleFields.getColumnList(tableDesc, cl["."]) 38 | test.equal(s, '"name", "age"'); 39 | 40 | cl = OracleFields._prepare({name: false, age: false, _id: false}); 41 | s = OracleFields.getColumnList(tableDesc, cl["."]) 42 | test.equal(s, '"birthDate", "sex"'); 43 | 44 | cl = OracleFields._prepare({_id: false, name: false, age: false}); 45 | s = OracleFields.getColumnList(tableDesc, cl["."]) 46 | test.equal(s, '"birthDate", "sex"'); 47 | 48 | cl = OracleFields._prepare({name: false, age: false}); 49 | s = OracleFields.getColumnIdList(tableDesc, cl["."]) 50 | test.equal(s, "1_2"); 51 | 52 | cl = OracleFields._prepare({name: true, age: true}); 53 | s = OracleFields.getColumnIdList(tableDesc, cl["."]) 54 | test.equal(s, "1_2"); 55 | }); -------------------------------------------------------------------------------- /lib/server/tests/oracle_modifier_tests.js: -------------------------------------------------------------------------------- 1 | 2 | Tinytest.add('test oracle modifier _compile', function (test, done) { 3 | 4 | var s; 5 | 6 | // 7 | // Tests with numbers 8 | // 9 | s = OracleModifier._compile({}); 10 | test.isUndefined(s); 11 | 12 | s = OracleModifier._compile({$inc: {age: 1}}); 13 | test.equal(s, "\"age\" = \"age\" + :0"); 14 | 15 | s = OracleModifier._compile({$set: {age: 19}}); 16 | test.equal(s, "\"age\" = :0"); 17 | 18 | s = OracleModifier._compile({$set: {name: "Amber", age: 37}}); 19 | test.equal(s, "\"name\" = :0, \"age\" = :1"); 20 | }); 21 | 22 | 23 | Tinytest.add('test oracle modifier _prepare', function (test, done) { 24 | 25 | var s; 26 | 27 | // 28 | // Tests with numbers 29 | // 30 | s = OracleModifier._prepare({$inc: {age: 1}}); 31 | test.equal(s, {".": {$inc: {age: 1}}}); 32 | 33 | s = OracleModifier._prepare({$set: {age: 19}}); 34 | test.equal(s, {".": {$set: {age: 19}}}); 35 | 36 | s = OracleModifier._prepare({$set: {name: "Amber", age: 37}}); 37 | test.equal(s, {".": {$set: {name: "Amber", age: 37}}}); 38 | 39 | s = OracleModifier._prepare({"tags.$set": {owner: "Amber"}}); 40 | test.equal(s, {"tags":{".":{"$set":{"owner":"Amber"}}}}); 41 | 42 | s = OracleModifier._prepare({$set: {name: "Amber", age: 37}, "tags.$set": {owner: "Amber"}}); 43 | test.equal(s, {".": {$set: {name: "Amber", age: 37}}, "tags":{".":{"$set":{"owner":"Amber"}}}}); 44 | }); 45 | 46 | Tinytest.add('test oracle modifier process', function (test, done) { 47 | 48 | var s; 49 | 50 | // 51 | // Tests with numbers 52 | // 53 | s = OracleModifier.process({$inc: {age: 1}}); 54 | test.equal(s, {".":"\"age\" = \"age\" + :0","*":{"age":1},"$":[1],"@":[]}); 55 | 56 | s = OracleModifier.process({$set:{age: 19}}); 57 | test.equal(s, {".":"\"age\" = :0","*":{"age":19},"$":[19],"@":[]}); 58 | 59 | s = OracleModifier.process({$set: {name: "Amber", age: 37}}); 60 | test.equal(s, {".":"\"name\" = :0, \"age\" = :1","*":{"name":"Amber","age":37},"$":["Amber",37],"@":[]}); 61 | }); 62 | -------------------------------------------------------------------------------- /lib/server/tests/oracle_selector_tests.js: -------------------------------------------------------------------------------- 1 | 2 | Tinytest.add('test oracle selector _compile', function (test, done) { 3 | 4 | var s; 5 | 6 | // 7 | // Tests internal _compile function 8 | // 9 | 10 | // 11 | // Tests with numbers 12 | // 13 | s = OracleSelector._compile({_id: 17}); 14 | test.equal(s, "\"_id\" = 17"); 15 | 16 | s = OracleSelector._compile({age: {$gt: 17}}); 17 | test.equal(s, "\"age\" > 17"); 18 | 19 | s = OracleSelector._compile({age: {$lt: 17}}); 20 | test.equal(s, "\"age\" < 17"); 21 | 22 | s = OracleSelector._compile({age: {$gte: 17}}); 23 | test.equal(s, "\"age\" >= 17"); 24 | 25 | s = OracleSelector._compile({age: {$lte: 17}}); 26 | test.equal(s, "\"age\" <= 17"); 27 | 28 | s = OracleSelector._compile({age: {$mod: [5, 3]}}); 29 | test.equal(s, "mod(\"age\", 5) = 3"); 30 | 31 | s = OracleSelector._compile({cnt: {$gte: 17}, age:{$mod: [5, 3]}}); 32 | test.equal(s, "(\"cnt\" >= 17) AND (mod(\"age\", 5) = 3)"); 33 | 34 | s = OracleSelector._compile({cnt: 2, age: {'$gte': 10, '$lt': 20}}); 35 | test.equal(s, "(\"cnt\" = 2) AND ((\"age\" >= 10) AND (\"age\" < 20))"); 36 | 37 | s = OracleSelector._compile({age: {$in: [17, 13, 35, 198]}}); 38 | test.equal(s, "\"age\" IN (17, 13, 35, 198)"); 39 | 40 | s = OracleSelector._compile({age: {$nin: [17, 13, 35, 198]}}); 41 | test.equal(s, "\"age\" NOT IN (17, 13, 35, 198)"); 42 | 43 | s = OracleSelector._compile({age: 17}); 44 | test.equal(s, "\"age\" = 17"); 45 | 46 | s = OracleSelector._compile({age: {$eq: 17}}); 47 | test.equal(s, "\"age\" = 17"); 48 | 49 | s = OracleSelector._compile({age: {$ne: 17}}); 50 | test.equal(s, "(\"age\" <> 17 OR \"age\" IS NULL)"); 51 | 52 | s = OracleSelector._compile({age: {$not: 17}}); 53 | test.equal(s, "NOT(\"age\" = 17)"); 54 | 55 | s = OracleSelector._compile({$and: [{age: {$gte: 17}}, {age: {$lte: 65}}]}); 56 | test.equal(s, "(\"age\" >= 17) AND (\"age\" <= 65)"); 57 | 58 | s = OracleSelector._compile({$or: [{age: {$gte: 17}}, {age: {$lte: 65}}]}); 59 | test.equal(s, "(\"age\" >= 17) OR (\"age\" <= 65)"); 60 | 61 | s = OracleSelector._compile({$nor: [{age: {$gte: 17}}, {age: {$lte: 65}}]}); 62 | test.equal(s, "NOT((\"age\" >= 17) OR (\"age\" <= 65))"); 63 | 64 | // 65 | // Tests with Strings 66 | // 67 | s = OracleSelector._compile({name: {$lte: "Bill"}}); 68 | test.equal(s, "\"name\" <= 'Bill'"); 69 | 70 | s = OracleSelector._compile({name: {$in: ["Bill", "Jane"]}}); 71 | test.equal(s, "\"name\" IN ('Bill', 'Jane')"); 72 | 73 | s = OracleSelector._compile({name: {$nin: ["Bill", "Jane"]}}); 74 | test.equal(s, "\"name\" NOT IN ('Bill', 'Jane')"); 75 | 76 | s = OracleSelector._compile({name: "Bill"}); 77 | test.equal(s, "\"name\" = 'Bill'"); 78 | 79 | s = OracleSelector._compile({name: {$eq: "Bill"}}); 80 | test.equal(s, "\"name\" = 'Bill'"); 81 | 82 | s = OracleSelector._compile({name: {$ne: "Bill"}}); 83 | test.equal(s, "(\"name\" <> 'Bill' OR \"name\" IS NULL)"); 84 | 85 | s = OracleSelector._compile({name: {$not: "Bill"}}); 86 | test.equal(s, "NOT(\"name\" = 'Bill')"); 87 | 88 | s = OracleSelector._compile({name: {$regex: "Bill*"}}); 89 | test.equal(s, "REGEXP_LIKE(\"name\", 'Bill*')"); 90 | 91 | s = OracleSelector._compile({$and: [{name: {$gte: "Bill"}}, {age: {$lte: "Jane's"}}]}); 92 | test.equal(s, "(\"name\" >= 'Bill') AND (\"age\" <= 'Jane''s')"); 93 | 94 | s = OracleSelector._compile({$or: [{name: {$gte: "Bill"}}, {age: {$lte: "Jane's"}}]}); 95 | test.equal(s, "(\"name\" >= 'Bill') OR (\"age\" <= 'Jane''s')"); 96 | 97 | s = OracleSelector._compile({$nor: [{name: {$gte: "Bill"}}, {age: {$lte: "Jane's"}}]}); 98 | test.equal(s, "NOT((\"name\" >= 'Bill') OR (\"age\" <= 'Jane''s'))"); 99 | 100 | s = OracleSelector._compile({name: {$lte: "Bill"}, $comment: "This is a comment"}); 101 | test.equal(s, "\"name\" <= 'Bill'"); 102 | 103 | s = OracleSelector._compile({$where: "substr(\"name\", 1, 3) = 'Bil'", $comment: "This is a comment"}); 104 | test.equal(s, "substr(\"name\", 1, 3) = 'Bil'"); 105 | 106 | // 107 | // Tests with Boolean 108 | // 109 | 110 | s = OracleSelector._compile({name: {$exists: true}}); 111 | test.equal(s, "\"name\" IS NOT NULL"); 112 | 113 | s = OracleSelector._compile({name: {$exists: false}}); 114 | test.equal(s, "\"name\" IS NULL"); 115 | 116 | // 117 | // Test with empty 118 | // 119 | s = OracleSelector._compile({}); 120 | test.equal(s, ""); 121 | 122 | s = OracleSelector._compile({"tags.0.tag": {$eq: "OK"}}, "lists"); 123 | test.equal(s, "\"_id\" IN (SELECT DISTINCT \"_id\" FROM \"lists$tags\" WHERE \"_indexNo\" = 0 AND \"tag\" = 'OK')"); 124 | 125 | s = OracleSelector._compile({"tags._value": {$ne: "OK"}}, "lists"); 126 | test.equal(s, "\"_id\" NOT IN (SELECT DISTINCT \"_id\" FROM \"lists$tags\" WHERE \"_value\" = 'OK')"); 127 | 128 | s = OracleSelector._compile({"tags": {$elemMatch: {tag: "OK", owner: "Helen"}}}, "lists"); 129 | test.equal(s, "\"_id\" IN (SELECT DISTINCT \"_id\" FROM \"lists$tags\" WHERE (\"tag\" = 'OK') AND (\"owner\" = 'Helen'))"); 130 | 131 | // 132 | // Custom extensions tests 133 | // 134 | s = OracleSelector._compile({name: {$like: "Bill%"}}); 135 | test.equal(s, "\"name\" LIKE 'Bill%'"); 136 | 137 | }); 138 | 139 | Tinytest.add('test oracle selector _prepare', function (test, done) { 140 | 141 | var s; 142 | 143 | // 144 | // Tests internal _prepare function 145 | // 146 | 147 | s = OracleSelector._prepare({name: "Bill", ".tags": {owner: "Helen"}}); 148 | test.equal(s, {".":{"name":"Bill"},"tags":{".":{"owner":"Helen"}}}); 149 | 150 | s = OracleSelector._prepare({name: "Bill", "tags.0.tag": {$eq: "OK"}, ".tags": {owner: "Helen"}}); 151 | test.equal(s, {".":{"name": "Bill", "tags.0.tag":{$eq: "OK"}},"tags":{".":{"owner":"Helen"}}}); 152 | 153 | }); 154 | 155 | 156 | Tinytest.add('test oracle selector process', function (test, done) { 157 | 158 | var s; 159 | 160 | // 161 | // Tests the process function 162 | // 163 | 164 | s = OracleSelector.process({name: "Bill"}); 165 | test.equal(s, {".":"\"name\" = 'Bill'","*":{"name":null}}); 166 | 167 | s = OracleSelector.process({name: "Bill", ".tags": {owner: "Helen"}}); 168 | test.equal(s, {".":"\"name\" = 'Bill'","*":{"name":null},"tags":{".":"\"owner\" = 'Helen'","*":{"owner":null}}}); 169 | 170 | s = OracleSelector.process({name: "Bill", "tags.0.tag": {$eq: "OK"}, ".tags": {owner: "Helen"}}); 171 | test.equal(s, {".":"(\"name\" = 'Bill') AND (\"_id\" IN (SELECT DISTINCT \"_id\" FROM \"undefined$tags\" WHERE \"_indexNo\" = 0 AND \"tag\" = 'OK'))","*":{"name":null},"tags":{".":"\"owner\" = 'Helen'","*":{"owner":null}}}); 172 | 173 | }); 174 | 175 | -------------------------------------------------------------------------------- /lib/server/tests/oracle_sorter_tests.js: -------------------------------------------------------------------------------- 1 | 2 | Tinytest.add('test oracle sorter _compile', function (test, done) { 3 | 4 | var s; 5 | 6 | // 7 | // Tests with numbers 8 | // 9 | s = OracleSorter._compile({}, {}); 10 | test.equal(s, ""); 11 | 12 | s = OracleSorter._compile({name: 1}, {}); 13 | test.equal(s, "\"name\""); 14 | 15 | s = OracleSorter._compile({name: 1, age: -1}, {}); 16 | test.equal(s, "\"name\", \"age\" DESC"); 17 | 18 | s = OracleSorter._compile({owner: 1, tag: 1}, {}); 19 | test.equal(s, "\"owner\", \"tag\""); 20 | }); 21 | 22 | 23 | Tinytest.add('test oracle sorter _prepare', function (test, done) { 24 | 25 | var s; 26 | 27 | // 28 | // Tests with numbers 29 | // 30 | s = OracleSorter._prepare({name: 1, age: 1}); 31 | test.equal(s, {".":{"name":1, "age": 1}}); 32 | 33 | s = OracleSorter._prepare({name: 1, "tags.0.tag": 1}); 34 | test.equal(s, {".":{"name":1,"tags.0.tag":1}}); 35 | 36 | s = OracleSorter._prepare({name: 1, ".tags": {owner: 1}}); 37 | test.equal(s, {".":{"name":1},"tags":{".":{"owner":1}}}); 38 | 39 | s = OracleSorter._prepare({name: 1, "tags.0.tag": 1, ".tags": {owner: 1}}); 40 | test.equal(s, {".":{"name":1,"tags.0.tag":1},"tags":{".":{"owner":1}}}); 41 | }); 42 | 43 | Tinytest.add('test oracle sorter process', function (test, done) { 44 | 45 | var s; 46 | 47 | // 48 | // Tests with numbers 49 | // 50 | s = OracleSorter.process({name: 1, age: 1}, {}); 51 | test.equal(s, {".":"\"name\", \"age\""}); 52 | 53 | // TODO: fix this case 54 | s = OracleSorter.process({name: 1, "tags.0.tag": 1}, {}); 55 | test.equal(s, {".":"\"name\", \"tags.0.tag\""}); 56 | 57 | s = OracleSorter.process({name: 1, ".tags": {owner: 1}}, {}); 58 | test.equal(s, {".":"\"name\"","tags":{".":"\"owner\""}}); 59 | 60 | s = OracleSorter.process({name: 1, "tags.0.tag": 1, ".tags": {owner: 1}}, {}); 61 | test.equal(s, {".":"\"name\", \"tags.0.tag\"","tags":{".":"\"owner\""}}); 62 | }); 63 | -------------------------------------------------------------------------------- /lib/server/tests/oracle_tests.js: -------------------------------------------------------------------------------- 1 | 2 | Tinytest.addAsync('oracle connection', function (test, done) { 3 | var oracledb = NpmModuleOracledb; 4 | 5 | oracledb.getConnection( 6 | { 7 | user : "meteor", 8 | password : "meteor", 9 | connectString : "localhost/XE" 10 | }, 11 | Meteor.bindEnvironment(function(err, connection) 12 | { 13 | if (err) { 14 | test.fail(err.message, "Connected"); 15 | done(); 16 | return; 17 | } 18 | 19 | connection.execute( 20 | "SELECT user from dual", 21 | [], 22 | Meteor.bindEnvironment(function(err, result) 23 | { 24 | test.isNotNull(result); 25 | test.isUndefined(err); 26 | var rows = result.rows; 27 | var row = rows[0]; 28 | var user = row[0]; 29 | test.equal(user, "METEOR", "Logged in as user METEOR"); 30 | done(); 31 | })); 32 | })); 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /lib/server/wrapper.js: -------------------------------------------------------------------------------- 1 | // 2 | // Wrapper around the oracledb npm package 3 | // 4 | 5 | NpmModuleOracledb = Npm.require('oracledb'); 6 | NpmModuleOracledbVersion = Npm.require('oracledb/package.json').version; 7 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | 2 | Package.describe({ 3 | name: 'metstrike:meteor-oracle', 4 | version: '0.2.0', 5 | // Brief, one-line summary of the package. 6 | summary: 'Oracle Database Driver for Meteor', 7 | // URL to the Git repository containing the source code for this package. 8 | git: 'https://github.com/metstrike/meteor-oracle.git', 9 | // By default, Meteor will default to using README.md for documentation. 10 | // To avoid submitting documentation, set this field to null. 11 | documentation: 'README.md' 12 | }); 13 | 14 | Npm.depends({ 15 | 'oracledb': '1.6.0' 16 | }); 17 | 18 | Package.onUse(function(api) { 19 | api.export(['NpmModuleOracledb', 'NpmModuleOracledbVersion'], 'server'); 20 | api.export(['OracleInternals', 'OracleDB', 'OracleTest', 'OracleSelector', 'OracleModifier', 'OracleSorter', 'OracleFields'], 'server'); 21 | api.export(['Oracle'], ['client', 'server']); 22 | api.versionsFrom('1.1.0.3'); 23 | api.use('underscore'); 24 | api.use('callback-hook', 'server'); 25 | api.use('mongo', ['client', 'server']); 26 | api.use('minimongo', 'server'); 27 | api.use('ejson', 'server'); 28 | api.use('ddp-server@1.2.1', 'server'); 29 | api.addFiles('lib/server/wrapper.js', 'server'); 30 | api.addFiles('lib/server/oracle_driver.js', 'server'); 31 | api.addFiles('lib/server/remote_collection_driver.js', 'server'); 32 | api.addFiles('lib/server/collection.js', ['client', 'server']); 33 | api.addFiles('lib/server/oracle_oplog_tailing.js', 'server'); 34 | api.addFiles('lib/server/oracle_collection.js', 'server'); 35 | api.addFiles('lib/server/oracle_db.js', 'server'); 36 | api.addFiles('lib/server/oracle_selector.js', 'server'); 37 | api.addFiles('lib/server/oracle_modifier.js', 'server'); 38 | api.addFiles('lib/server/oracle_sorter.js', 'server'); 39 | api.addFiles('lib/server/oracle_fields.js', 'server'); 40 | api.addFiles('lib/server/helpers.js', 'server'); 41 | }); 42 | 43 | Package.onTest(function(api) { 44 | api.use('tinytest'); 45 | api.use('metstrike:meteor-oracle'); 46 | api.addFiles('lib/server/tests/oracle_tests.js', 'server'); 47 | api.addFiles('lib/server/tests/oracle_db_tests.js', 'server'); 48 | api.addFiles('lib/server/tests/collection_tests.js', 'server'); 49 | api.addFiles('lib/server/tests/oracle_selector_tests.js', 'server'); 50 | api.addFiles('lib/server/tests/oracle_modifier_tests.js', 'server'); 51 | api.addFiles('lib/server/tests/oracle_sorter_tests.js', 'server'); 52 | api.addFiles('lib/server/tests/oracle_fields_tests.js', 'server'); 53 | }); 54 | --------------------------------------------------------------------------------