patio
Version:
Patio query engine and ORM
1,257 lines (1,194 loc) • 51.6 kB
JavaScript
var comb = require("comb"),
errors = require("../errors"),
asyncArray = comb.async.array,
NotImplemented = errors.NotImplemented,
QueryError = errors.QueryError,
sql = require("../sql").sql,
Identifier = sql.Identifier,
isUndefinedOrNull = comb.isUndefinedOrNull,
argsToArray = comb.argsToArray,
isFunction = comb.isFunction,
isNumber = comb.isNumber,
QualifiedIdentifier = sql.QualifiedIdentifier,
AliasedExpression = sql.AliasedExpression,
define = comb.define,
isInstanceOf = comb.isInstanceOf,
merge = comb.merge,
isBoolean = comb.isBoolean,
isString = comb.isString,
flatten = comb.array.flatten,
when = comb.when,
logging = comb.logging,
Logger = logging.Logger,
Promise = comb.Promise,
PromiseList = comb.PromiseList,
TransformStream = require("stream").Transform,
pipeAll = require("../utils").pipeAll;
var Dataset;
var LOGGER = Logger.getLogger("patio.Dataset");
function partition(arr, sliceSize) {
var output = [], j = 0;
for (var i = 0, l = arr.length; i < l; i += sliceSize) {
output[j++] = arr.slice(i, i + sliceSize);
}
return output;
}
define({
instance: {
/**@lends patio.Dataset.prototype*/
/**@ignore*/
constructor: function () {
if (!Dataset) {
Dataset = require("../index").Dataset;
}
this._super(arguments);
},
/**
* Returns a [Stream](http://nodejs.org/api/stream.html) for streaming data from the database.
*
*```
* User
* .stream()
* .on("data", function(record){
* console.log(record);
* })
* .on("error", errorHandler)
* .on("end", function(){
* console.log("all done")
* });
*
* //postgres options
* User
* .stream({batchSize: 100, highWaterMark: 1000})
* .on("data", function(record){
* console.log(record);
* })
* .on("error", errorHandler)
* .on("end", function(){
* console.log("all done")
* });
*```
* @param {Object} opts an object to pass to the adapters connection stream implementation
* @return {Stream}
*/
stream: function (opts) {
var queryStream = this.fetchRows(this.selectSql, merge(opts || {}, {stream: true})), rowCb, ret;
if ((rowCb = this.rowCb)) {
ret = new TransformStream({objectMode: true});
ret._transform = function (data, encoding, done) {
when(rowCb(data)).chain(function (data) {
ret.push(data);
done();
}, done);
};
pipeAll(queryStream, ret);
} else {
ret = queryStream;
}
return ret;
},
/**
* Returns a Promise that is resolved with an array with all records in the dataset.
* If a block is given, the array is iterated over after all items have been loaded.
*
* @example
*
* // SELECT * FROM table
* DB.from("table").all().chain(function(res){
* //res === [{id : 1, ...}, {id : 2, ...}, ...];
* });
* // Iterate over all rows in the table
* var myArr = [];
* var rowPromise = DB.from("table").all(function(row){ myArr.push(row);});
* rowPromise.chain(function(rows){
* //=> rows == myArr;
* });
*
* @param {Function} block a block to be called with each item. The return value of the block is ignored.
* @param {Function} [cb] a block to invoke when the action is done
*
* @return {comb.Promise} a promise that is resolved with an array of rows.
*/
all: function (block, cb) {
var self = this;
var ret = asyncArray(this.forEach().chain(function (records) {
return self.postLoad(records);
}));
if (block) {
ret = ret.forEach(block);
}
return ret.classic(cb).promise();
},
/**
* Returns a promise that is resolved with the average value for the given column.
*
* @example
*
* // SELECT avg(number) FROM table LIMIT 1
* DB.from("table").avg("number").chain(function(avg){
* // avg === 3
* });
*
* @param {String|patio.sql.Identifier} column the column to average
* @param {Function} [cb] the callback to invoke when the action is done.
*
* @return {comb.Promise} a promise that is resolved with the average value of the column.
*/
avg: function (column, cb) {
return this.__aggregateDataset().get(sql.avg(this.stringToIdentifier(column)), cb);
},
/**
* Returns a promise that is resolved with the number of records in the dataset.
*
* @example
*
* // SELECT COUNT(*) AS count FROM table LIMIT 1
* DB.from("table").count().chain(function(count){
* //count === 3;
* });
*
* @param {Function} [cb] the callback to invoke when the action is done.
*
* @return {comb.Promise} a promise that is resolved with the the number of records in the dataset.
*/
count: function (cb) {
return this.__aggregateDataset().get(sql.COUNT(sql.literal("*")).as("count")).chain(function (res) {
return parseInt(res, 10);
}).classic(cb);
},
/**@ignore*/
"delete": function () {
return this.remove();
},
/**
* Deletes the records in the dataset. The returned Promise should be resolved with the
* number of records deleted, but that is adapter dependent.
*
* @example
*
* // DELETE * FROM table
* DB.from("table").remove().chain(function(numDeleted){
* //numDeleted === 3
* });
*
* @param {Function} [cb] the callback to invoke when the action is done.
*
* @return {comb.Promise} a promise resolved with the
* number of records deleted, but that is adapter dependent.
*/
remove: function (cb) {
return this.executeDui(this.deleteSql).classic(cb).promise();
},
/**
* Iterates over the records in the dataset as they are returned from the
* database adapter.
*
* @example
*
* // SELECT * FROM table
* DB.from("table").forEach(function(row){
* //....do something
* });
*
* @param {Function} [block] the block to invoke for each row.
* @param {Function} [cb] the callback to invoke when the action is done.
*
* @return {comb.Promise} a promise that is resolved when the action has completed.
*/
forEach: function (block, cb) {
var rowCb, ret;
if (this.__opts.graph) {
ret = this.graphEach(block);
} else {
ret = this.fetchRows(this.selectSql);
if ((rowCb = this.rowCb)) {
ret = ret.map(function (r) {
return rowCb(r);
});
}
if (block) {
ret = ret.forEach(block);
}
}
return ret.classic(cb);
},
/**
* Returns a promise that is resolved with true if no records exist in the dataset,
* false otherwise.
* @example
*
* // SELECT 1 FROM table LIMIT 1
* DB.from("table").isEmpty().chain(function(isEmpty){
* // isEmpty === false
* });
*
* @param {Function} [cb] a function to callback when action is done
*
* @return {comb.Promise} a promise that is resolved with a boolean indicating if the table is empty.
*/
isEmpty: function (cb) {
return this.get(1).chain(function (res) {
return isUndefinedOrNull(res) || res.length === 0;
}.bind(this)).classic(cb);
},
__processFields: function (fields) {
throw new Error("Not Implemented");
},
__processRow: function (row, cols) {
var h = {}, i = -1, l = cols.length, col;
while (++i < l) {
col = cols[i];
h[col[0]] = col[1](row[col[2]]);
}
return h;
},
__processRows: function (rows, cols) {
//dp this so the callbacks are called in appropriate order also.
var ret = [], i = -1, l = rows.length, processRow = this.__processRow;
while (++i < l) {
ret[i] = processRow.call(this, rows[i], cols);
}
cols = null;
rows.length = 0;
return ret;
},
fetchStreamedRows: function (sql, opts) {
var ret = new TransformStream({objectMode: true}), cols, self = this;
ret._transform = function (row, encoding, callback) {
ret.push(self.__processRow(row, cols));
callback();
};
var queryStream = this.execute(sql, opts);
queryStream.on("fields", function (fields) {
cols = self.__processFields(fields);
});
pipeAll(queryStream, ret);
return ret;
},
fetchPromisedRows: function (sql, opts) {
var self = this;
return asyncArray(this.execute(sql, opts).chain(function (rows, fields) {
return self.__processRows(rows, self.__processFields(fields));
}));
},
/**
* @private
* Executes a select query and fetches records, passing each record to the
* supplied cb. This method should not be called by user code, use {@link patio.Dataset#forEach}
* instead.
*/
fetchRows: function (sql, opts) {
opts = opts || {};
var ret;
if (opts.stream) {
ret = this.fetchStreamedRows(sql, opts);
} else {
ret = this.fetchPromisedRows(sql, opts);
}
return ret;
},
/**
* If a integer argument is given, it is interpreted as a limit, and then returns all
* matching records up to that limit.
*
* If no arguments are passed, it returns the first matching record.
*
* If a function taking no arguments is passed in as the last parameter then it
* is assumed to be a filter block. If the a funciton is passed in that takes arguments
* then it is assumed to be a callback. You may also pass in both the second to last argument
* being a filter function, and the last being a callback.
*
* If any other type of argument(s) is passed, it is given to {@link patio.Dataset#filter} and the
* first matching record is returned. Examples:
*
* @example
*
* comb.executeInOrder(DB.from("table"), function(ds){
* // SELECT * FROM table LIMIT 1
* ds.first(); // => {id : 7}
*
* // SELECT * FROM table LIMIT 2
* ds.first(2); // => [{id : 6}, {id : 4}]
*
* // SELECT * FROM table WHERE (id = 2) LIMIT 1
* ds.first({id : 2}) // => {id : 2}
*
*
* // SELECT * FROM table WHERE (id = 3) LIMIT 1
* ds.first("id = 3"); // => {id : 3}
*
* // SELECT * FROM table WHERE (id = 4) LIMIT 1
* ds.first("id = ?", 4); // => {id : 4}
*
* // SELECT * FROM table WHERE (id > 2) LIMIT 1
* ds.first(function(){return this.id.gt(2);}); // => {id : 5}
*
*
* // SELECT * FROM table WHERE ((id > 4) AND (id < 6)) LIMIT 1
* ds.first("id > ?", 4, function(){
* return this.id.lt(6);
* }); // => {id : 5}
*
* // SELECT * FROM table WHERE (id < 2) LIMIT 2
* ds.first(2, function(){
* return this.id.lt(2)
* }); // => [{id:1}]
* });
*
* @param {*} args varargs to be used to limit/filter the result set.
*
* @return {comb.Promise} a promise that is resolved with the either the first matching record.
* Or an array of items if a limit was provided as the first argument.
*/
first: function (args) {
args = comb(arguments).toArray();
var cb,
block = isFunction(args[args.length - 1]) ? args.pop() : null;
if (block && block.length > 0) {
cb = block;
block = isFunction(args[args.length - 1]) ? args.pop() : null;
}
var ds = block ? this.filter(block) : this;
if (!args.length) {
return ds.singleRecord(cb);
} else {
args = (args.length === 1) ? args[0] : args;
if (isNumber(args)) {
return ds.limit(args).all(null, cb);
} else {
return ds.filter(args).singleRecord(cb);
}
}
},
/**
* Return the column value for the first matching record in the dataset.
*
* @example
* // SELECT id FROM table LIMIT 1
* DB.from("table").get("id").chain(function(val){
* // val === 3
* });
*
*
* // SELECT sum(id) FROM table LIMIT 1
* ds.get(sql.sum("id")).chain(function(val){
* // val === 6;
* });
*
* // SELECT sum(id) FROM table LIMIT 1
* ds.get(function(){
* return this.sum("id");
* }).chain(function(val){
* // val === 6;
* });
*
* @param {*} column the column to filter on can be anything that
* {@link patio.Dataset#select} accepts.
* @param {Function} [cb] the callback to invoke when the action is done.
*
* @return {comb.Promise} a promise that will be resolved will the value requested.
*/
get: function (column, cb) {
return this.select(column).singleValue(cb);
},
/**
* Inserts multiple records into the associated table. This method can be
* used to efficiently insert a large number of records into a table in a
* single query if the database supports it. Inserts
* are automatically wrapped in a transaction.
*
* This method is called with a columns array and an array of value arrays:
* <pre class="code">
* // INSERT INTO table (x, y) VALUES (1, 2)
* // INSERT INTO table (x, y) VALUES (3, 4)
* DB.from("table").import(["x", "y"], [[1, 2], [3, 4]]).
* </pre>
*
* This method also accepts a dataset instead of an array of value arrays:
*
* <pre class="code">
* // INSERT INTO table (x, y) SELECT a, b FROM table2
* DB.from("table").import(["x", "y"], DB.from("table2").select("a", "b"));
* </pre>
*
* The method also accepts a commitEvery option that specifies
* the number of records to insert per transaction. This is useful especially
* when inserting a large number of records, e.g.:
*
* <pre class="code">
* // this will commit every 50 records
* DB.from("table").import(["x", "y"], [[1, 2], [3, 4], ...], {commitEvery : 50});
* </pre>
*
* @param {Array} columns The columns to insert values for.
* This array will be used as the base for each values item in the values array.
* @param {Array[Array]} values Array of arrays of values to insert into the columns.
* @param {Object} [opts] options
* @param {Number} [opts.commitEvery] the number of records to insert per transaction.
* @param {Function} [cb] the callback to invoke when the action is done.
*
* @return {comb.Promise} a promise that is resolved once all records have been inserted.
*/
"import": function (columns, values, opts, cb) {
if (isFunction(opts)) {
cb = opts;
opts = null;
}
opts = opts || {};
var ret, self = this;
if (isInstanceOf(values, Dataset)) {
ret = this.db.transaction(function () {
return self.insert(columns, values);
});
} else {
if (!values.length) {
ret = new Promise().callback();
} else if (!columns.length) {
throw new QueryError("Invalid columns in import");
}
var sliceSize = opts.commitEvery || opts.slice, result = [];
if (sliceSize) {
ret = asyncArray(partition(values, sliceSize)).forEach(function (entries, offset) {
offset = (offset * sliceSize);
return self.db.transaction(opts, function () {
return when(self.multiInsertSql(columns, entries).map(function (st, index) {
return self.executeDui(st).chain(function (res) {
result[offset + index] = res;
});
}));
});
}, 1);
} else {
var statements = this.multiInsertSql(columns, values);
ret = this.db.transaction(function () {
return when(statements.map(function (st, index) {
return self.executeDui(st).chain(function (res) {
result[index] = res;
});
}));
});
}
}
return ret.chain(function () {
return flatten(result);
}).classic(cb).promise();
},
/**
* This is the recommended function to do the insert of multiple items into the
* database. This acts as a proxy to the {@link patio.Dataset#import} method so
* one can use an array of hashes rather than an array of columns and an array of values.
* See {@link patio.Dataset#import} for more information regarding the method of inserting.
* <p>
* <b>NOTE:</b>All hashes should have the same keys other wise some values could be missed</b>
* </p>
*
* @example
*
* // INSERT INTO table (x) VALUES (1)
* // INSERT INTO table (x) VALUES (2)
* DB.from("table").multiInsert([{x : 1}, {x : 2}]).chain(function(){
* //...do something
* })
*
* //commit every 50 inserts
* DB.from("table").multiInsert([{x : 1}, {x : 2},....], {commitEvery : 50}).chain(function(){
* //...do something
* });
*
* @param {Object[]} hashes an array of objects to insert into the database. The keys of
* the first item in the array will be used to look up columns in all subsequent objects. If the
* array is empty then the promise is resolved immediatly.
*
* @param {Object} opts See {@link patio.Dataset#import}.
* @param {Function} [cb] the callback to invoke when the action is done.
*
* @return {comb.Promise} See {@link patio.Dataset#import} for return functionality.
*/
multiInsert: function (hashes, opts, cb) {
if (isFunction(opts)) {
cb = opts;
opts = null;
}
opts = opts || {};
hashes = hashes || [];
var ret = new Promise();
if (!hashes.length) {
ret.callback();
} else {
var columns = Object.keys(hashes[0]);
ret = this["import"](columns, hashes.map(function (h) {
return columns.map(function (c) {
return h[c];
});
}), opts, cb);
}
return ret.classic(cb).promise();
},
/**
* Inserts values into the associated table. The returned value is generally
* the value of the primary key for the inserted row, but that is adapter dependent.
*
* @example
*
* // INSERT INTO items DEFAULT VALUES
* DB.from("items").insert()
*
* // INSERT INTO items DEFAULT VALUES
* DB.from("items").insert({});
*
* // INSERT INTO items VALUES (1, 2, 3)
* DB.from("items").insert([1,2,3]);
*
* // INSERT INTO items (a, b) VALUES (1, 2)
* DB.from("items").insert(["a", "b"], [1,2]);
*
* // INSERT INTO items (a, b) VALUES (1, 2)
* DB.from("items").insert({a : 1, b : 2});
*
* // INSERT INTO items SELECT * FROM old_items
* DB.from("items").insert(DB.from("old_items"));
*
* // INSERT INTO items (a, b) SELECT * FROM old_items
* DB.from("items").insert(["a", "b"], DB.from("old_items"));
*
*
*
* @param {patio.Dataset|patio.sql.LiteralString|Array|Object|patio.sql.BooleanExpression|...} values values to
* insert into the database. The INSERT statement generated depends on the type.
* <ul>
* <li>Empty object| Or no arugments: then DEFAULT VALUES is used.</li>
* <li>Object: the keys will be used as the columns, and values will be the values inserted.</li>
* <li>Single {@link patio.Dataset} : an insert with subselect will be performed.</li>
* <li>Array with {@link patio.Dataset} : The array will be used for columns and a subselect will performed with the dataset for the values.</li>
* <li>{@link patio.sql.LiteralString} : the literal value will be used.</li>
* <li>Single Array : the values in the array will be used as the VALUES clause.</li>
* <li>Two Arrays: the first array is the columns the second array is the values.</li>
* <li>{@link patio.sql.BooleanExpression} : the expression will be used as the values.
* <li>An arbitrary number of arguments : the {@link patio.Dataset#literal} version of the values will be used</li>
* </ul>
* @param {Function} [cb] the callback to invoke when the action is done.
*
* @return {comb.Promise} a promise that is typically resolved with the ID of the inserted row.
*/
insert: function () {
var args = argsToArray(arguments);
var cb = isFunction(args[args.length - 1]) ? args.pop() : null;
return this.executeInsert(this.insertSql.apply(this, args)).classic(cb);
},
/**
* @see patio.Dataset#insert
*/
save: function () {
return this.insert.apply(this, arguments);
},
/**
* Inserts multiple values. If a block is given it is invoked for each
* item in the given array before inserting it. See {@link patio.Dataset#multiInsert} as
* a possible faster version that inserts multiple records in one SQL statement.
*
* <b> Params see @link patio.Dataset#insert</b>
*
* @example
*
* DB.from("table").insertMultiple([{x : 1}, {x : 2}]);
* //=> INSERT INTO table (x) VALUES (1)
* //=> INSERT INTO table (x) VALUES (2)
*
* DB.from("table").insertMultiple([{x : 1}, {x : 2}], function(row){
* row.y = row.x * 2;
* });
* //=> INSERT INTO table (x, y) VALUES (1, 2)
* //=> INSERT INTO table (x, y) VALUES (2, 4)
*
* @param array See {@link patio.Dataset#insert} for possible values.
* @param {Function} [block] a function to be called before each item is inserted.
* @param {Function} [cb] a function to be called when the aciton is complete
*
* @return {comb.PromiseList} a promiseList that should be resolved with the id of each item inserted
* in the order that was in the array.
*/
insertMultiple: function (array, block, cb) {
var promises, ret;
if (block) {
ret = when(array.map(function (i) {
return this.insert(block(i));
}, this));
} else {
ret = when(array.map(function (i) {
return this.insert(i);
}, this));
}
return ret.classic(cb).promise();
},
/**
* @see patio.Dataset#insertMultiple
*/
saveMultiple: function () {
return this.insertMultiple.apply(this, arguments);
},
/**
* Returns a promise that is resolved with the interval between minimum and maximum values
* for the given column.
*
* @example
* // SELECT (max(id) - min(id)) FROM table LIMIT 1
* DB.from("table").interval("id").chain(function(interval){
* //(e.g) interval === 6
* });
*
* @param {String|patio.sql.Identifier} column to find the interval of.
* @param {Function} [cb] a function to be called when the aciton is complete
*
* @return {comb.Promise} a promise that will be resolved with the interval between the min and max values
* of the column.
*/
interval: function (column, cb) {
return this.__aggregateDataset().get(sql.max(column).minus(sql.min(column)), cb);
},
/**
* Reverses the order and then runs first. Note that this
* will not necessarily give you the last record in the dataset,
* unless you have an unambiguous order.
*
* @example
*
* // SELECT * FROM table ORDER BY id DESC LIMIT 1
* DB.from("table").order("id").last().chain(function(lastItem){
* //...(e.g lastItem === {id : 10})
* });
*
* // SELECT * FROM table ORDER BY id ASC LIMIT 2
* DB.from("table").order(sql.id.desc()).last(2).chain(function(lastItems){
* //...(e.g lastItems === [{id : 1}, {id : 2});
* });
*
* @throws {patio.error.QueryError} If there is not currently an order for this dataset.
*
* @param {*} args See {@link patio.Dataset#first} for argument types.
*
* @return {comb.Promise} a promise that will be resolved with a single object or array depending on the
* arguments provided.
*/
last: function (args) {
if (!this.__opts.order) {
throw new QueryError("No order specified");
}
var ds = this.reverse();
return ds.first.apply(ds, arguments);
},
/**
* Maps column values for each record in the dataset (if a column name is
* given).
*
* @example
*
* // SELECT * FROM table
* DB.from("table").map("id").chain(function(ids){
* // e.g. ids === [1, 2, 3, ...]
* });
*
* // SELECT * FROM table
* DB.from("table").map(function(r){
* return r.id * 2;
* }).chain(function(ids){
* // e.g. ids === [2, 4, 6, ...]
* });
*
* @param {Function|String} column if a string is provided then then it is assumed
* to be the name of a column in that table and the value of the column for each row
* will be returned. If column is a function then the return value of the function will
* be used.
* @param {Function} [cb] a function to be called when the aciton is complete
*
* @return {comb.Promise} a promise resolved with the array of mapped values.
*/
map: function (column, cb) {
var ret = this.forEach();
column && (ret = ret[isFunction(column) ? "map" : "pluck"](column));
return ret.classic(cb).promise();
},
/**
* Returns a promise resolved with the maximum value for the given column.
*
* @example
*
* // SELECT max(id) FROM table LIMIT 1
* DB.from("table").max("id").chain(function(max){
* // e.g. max === 10.
* });
*
*
* @param {String|patio.sql.Identifier} column the column to find the maximum value for.
* @param {Function} [cb] callback to invoke when action is done
*
* @return {*} the maximum value for the column.
*/
max: function (column, cb) {
return this.__aggregateDataset().get(sql.max(this.stringToIdentifier(column)), cb);
},
/**
* Returns a promise resolved with the minimum value for the given column.
*
* @example
*
* // SELECT min(id) FROM table LIMIT 1
* DB.from("table").min("id").chain(function(min){
* // e.g. max === 0.
* });
*
*
* @param {String|patio.sql.Identifier} column the column to find the minimum value for.
* @param {Function} [cb] callback to invoke when action is done
*
* @return {*} the minimum value for the column.
*/
min: function (column, cb) {
return this.__aggregateDataset().get(sql.min(this.stringToIdentifier(column)), cb);
},
/**
* Returns a promise resolved with a range from the minimum and maximum values for the
* given column.
*
* @example
* // SELECT max(id) AS v1, min(id) AS v2 FROM table LIMIT 1
* DB.from("table").range("id").chain(function(min, max){
* //e.g min === 1 AND max === 10
* });
*
* @param {String|patio.sql.Identifier} column the column to find the min and max value for.
* @param {Function} [cb] the callback to invoke when the action is done.
*
* @return {comb.Promise} a promise that is resolved with the min and max value, as the first
* and second args respectively.
*/
range: function (column, cb) {
var ret = new Promise();
this.__aggregateDataset()
.select(sql.min(this.stringToIdentifier(column)).as("v1"), sql.max(this.stringToIdentifier(column)).as("v2"))
.first()
.chain(function (r) {
ret.callback(r.v1, r.v2);
}, ret.errback);
return ret.classic(cb).promise();
},
/**
* Selects the column given (either as an argument or as a callback), and
* returns an array of all values of that column in the dataset. If you
* give a block argument that returns an array with multiple entries,
* the contents of the resulting array are undefined.
*
*
* @example
* // SELECT id FROM table
* DB.from("table").selectMap("id").chain(function(selectMap){
* // e,g. selectMap === [3, 5, 8, 1, ...]
* });
*
* // SELECT abs(id) FROM table
* DB.from("table").selectMap(function(){
* return this.abs("id");
* }).chain(function(selectMap){
* //e.g selectMap === [3, 5, 8, 1, ...]
* });
*
* @param {*} column The column to return the values for.
* See {@link patio.Dataset#select} for valid column values.
* @param {Function} [cb] a function to be called when the aciton is complete
*
* @return {comb.Promise} a promise resolved with the array of mapped values.
*/
selectMap: function (column, cb) {
var ds = this.naked().ungraphed().select(column), col;
return ds.map(function (r) {
return r[col || (col = Object.keys(r)[0])];
}, cb);
},
/**
* The same as {@link patio.Dataset#selectMap}, but in addition orders the array by the column.
*
* @example
* // SELECT id FROM table ORDER BY id
* DB.from("table").selectOrderMap("id").chain(function(mappedIds){
* //e.g. [1, 2, 3, 4, ...]
* });
*
* // SELECT abs(id) FROM table ORDER BY abs(id)
* DB.from("table").selectOrderMap(function(){
* return this.abs("id");
* }).chain(function(mappedIds){
* //e.g. [1, 2, 3, 4, ...]
* });
*
* @param {*} column The column to return the values for.
* See {@link patio.Dataset#select} for valid column values.
*
* @return {comb.Promise} a promise resolved with the array of mapped values.
*/
selectOrderMap: function (column, cb) {
var col, ds = this.naked()
.ungraphed()
.select(column)
.order(isFunction(column) ? column : this._unaliasedIdentifier(column));
return ds.map(function (r) {
return r[col || (col = Object.keys(r)[0])];
}, cb);
},
/**
* Same as {@link patio.Dataset#singleRecord} but accepts arguments
* to filter the dataset. See {@link patio.Dataset#filter} for argument types.
*
* <b>NOTE</b> If the last argument is a function that accepts arguments it is not assumed to
* be a filter function but instead a callback.
*
* @return {comb.Promise} a promise resolved with a single row from the database that matched the filter.
*/
one: function () {
var args = comb(arguments).toArray(), cb;
var last = args[args.length - 1];
if (isFunction(last) && last.length > 0) {
cb = args.pop();
}
var ret = this;
if (args.length) {
ret = ret.filter.apply(ret, args);
}
return ret.singleRecord(cb);
},
/**
* Returns a promise resolved with the first record in the dataset, or null if the dataset
* has no records. Users should probably use {@link patio.Dataset#first} instead of
* this method.
*
* @example
*
* //'SELECT * FROM test LIMIT 1'
* DB.from("test").singleRecord().chain(function(r) {
* //e.g r === {id : 1, name : "firstName"}
* });
*
* @param {Function} [cb] a function to be called when the aciton is complete
*
* @return {comb.Promise} a promise resolved with the first record returned from the query.
*/
singleRecord: function (cb) {
return this.mergeOptions({limit: 1}).all().chain(function (r) {
return r && r.length ? r[0] : null;
}).classic(cb).promise();
},
/**
* Returns a promise resolved with the first value of the first record in the dataset.
* Returns null if dataset is empty. Users should generally use
* {@link patio.Dataset#get} instead of this method.
*
* @example
*
* //'SELECT * FROM test LIMIT 1'
* DB.from("test").singleValue().chain(function(r) {
* //e.g r === 1
* });
*
* @param {Function} [cb] the callback to invoke when the action is done.
*
* @return {comb.Promise} a promise that will be resolved with the first value of the first row returned
* from the dataset.
*
*/
singleValue: function (cb) {
return this.naked().ungraphed().singleRecord().chain(function (r) {
return r ? r[Object.keys(r)[0]] : null;
}).classic(cb).promise();
},
/**
* Returns a promise resolved the sum for the given column.
*
* @example
*
* // SELECT sum(id) FROM table LIMIT 1
* DB.from("table").sum("id").chain(function(sum){
* // e.g sum === 55
* });
*
* @param {String|patio.sql.Identifier\patio.sql.QualifiedIdentifier|patio.sql.AliasedExpression} column
* the column to find the sum of.
* @param {Function} [cb] the callback to invoke when the action is done.
*
* @return {comb.Promise} a promise resolved with the sum of the column.
*/
sum: function (column, cb) {
return this.__aggregateDataset().get(sql.sum(this.stringToIdentifier(column)), cb);
},
/**
* Returns a promise resolved with a string in CSV format containing the dataset records. By
* default the CSV representation includes the column titles in the
* first line. You can turn that off by passing false as the
* includeColumnTitles argument.
*
* <p>
* <b>NOTE:</b> This does not use a CSV library or handle quoting of values in
* any way. If any values in any of the rows could include commas or line
* endings, you shouldn't use this.
* </p>
*
* @example
* // SELECT * FROM table
* DB.from("table").toCsv().chain(function(csv){
* console.log(csv);
* //outputs
* id,name
* 1,Jim
* 2,Bob
* });
*
* // SELECT * FROM table
* DB.from("table").toCsv(false).chain(function(csv){
* console.log(csv);
* //outputs
* 1,Jim
* 2,Bob
* });
*
* @param {Boolean} [includeColumnTitles=true] Set to false to prevent the printing of the column
* titles as the first line.
*
* @param {Function} [cb] the callback to invoke when the action is done.
*
* @return {comb.Promise} a promise that will be resolved with the CSV string of the results of the
* query.
*/
toCsv: function (includeColumnTitles, cb) {
var n = this.naked();
if (isFunction(includeColumnTitles)) {
cb = includeColumnTitles;
includeColumnTitles = true;
}
includeColumnTitles = isBoolean(includeColumnTitles) ? includeColumnTitles : true;
return n.columns.chain(function (cols) {
var vals = [];
if (includeColumnTitles) {
vals.push(cols.join(", "));
}
return n.forEach(function (r) {
vals.push(cols.map(function (c) {
return r[c] || "";
}).join(", "));
}).chain(function () {
return vals.join("\r\n") + "\r\n";
});
}.bind(this)).classic(cb).promise();
},
/**
* Returns a promise resolved with a hash with keyColumn values as keys and valueColumn values as
* values. Similar to {@link patio.Dataset#toHash}, but only selects the two columns.
*
* @example
* // SELECT id, name FROM table
* DB.from("table").selectHash("id", "name").chain(function(hash){
* // e.g {1 : 'a', 2 : 'b', ...}
* });
*
* @param {String|patio.sql.Identifier\patio.sql.QualifiedIdentifier|patio.sql.AliasedExpression} keyColumn the column
* to use as the key in the hash.
*
* @param {String|patio.sql.Identifier\patio.sql.QualifiedIdentifier|patio.sql.AliasedExpression} valueColumn the column
* to use as the value in the hash.
*
* @param {Function} [cb] the callback to invoke when the action is done.
*
* @return {comb.Promise} a promise that is resolved with an array of hashes, that have the keyColumn
* as the key and the valueColumn as the value.
*/
selectHash: function (keyColumn, valueColumn, cb) {
var map = {}, args = comb.argsToArray(arguments);
cb = isFunction(args[args.length - 1]) ? args.pop() : null;
var k = this.__hashIdentifierToName(keyColumn),
v = this.__hashIdentifierToName(valueColumn);
return this.select.apply(this, args).map(function (r) {
map[r[k]] = v ? r[v] : r;
}).chain(function () {
return map;
}).classic(cb).promise();
},
/**
* Returns a promise resolved with a hash with one column used as key and another used as value.
* If rows have duplicate values for the key column, the latter row(s)
* will overwrite the value of the previous row(s). If the valueColumn
* is not given or null, uses the entire hash as the value.
*
* @example
* // SELECT * FROM table
* DB.from("table").toHash("id", "name").chain(function(hash){
* // {1 : 'Jim', 2 : 'Bob', ...}
* });
*
* // SELECT * FROM table
* DB.from("table").toHash("id").chain(function(hash){
* // {1 : {id : 1, name : 'Jim'}, 2 : {id : 2, name : 'Bob'}, ...}
* });
*
* @param {String|patio.sql.Identifier\patio.sql.QualifiedIdentifier|patio.sql.AliasedExpression} keyColumn the column
* to use as the key in the returned hash.
*
* @param {String|patio.sql.Identifier\patio.sql.QualifiedIdentifier|patio.sql.AliasedExpression} [keyValue=null] the
* key of the column to use as the value in the hash
*
* @param {Function} [cb] the callback to invoke when the action is done.
*
* @return {comb.Promise} a promise that will be resolved with the resulting hash.
*/
toHash: function (keyColumn, valueColumn, cb) {
var ret = new Promise(), map = {};
if (isFunction(valueColumn)) {
cb = valueColumn;
valueColumn = null;
}
var k = this.__hashIdentifierToName(keyColumn), v = this.__hashIdentifierToName(valueColumn);
return this.map(function (r) {
map[r[k]] = v ? r[v] : r;
}).chain(function () {
return map;
}).classic(cb).promise();
},
/**
* Truncates the dataset. Returns a promise that is resolved once truncation is complete.
*
* @example
*
* // TRUNCATE table
* DB.from("table").truncate().chain(function(){
* //...do something
* });
*
* @param {Function} [cb] the callback to invoke when the action is done.
*
* @return {comb.Promise} a promise that is resolved once truncation is complete.
*/
truncate: function (cb) {
return this.executeDdl(this.truncateSql).classic(cb);
},
/**
* Updates values for the dataset. The returned promise is resolved with a value that is generally the
* number of rows updated, but that is adapter dependent.
*
* @example
* // UPDATE table SET x = NULL
* DB.from("table").update({x : null}).chain(function(numRowsUpdated){
* //e.g. numRowsUpdated === 10
* });
*
* // UPDATE table SET x = (x + 1), y = 0
* DB.from("table").update({ x : sql.x.plus(1), y : 0}).chain(function(numRowsUpdated){
* // e.g. numRowsUpdated === 10
* });
*
* @param {Object} values See {@link patio.Dataset#updateSql} for parameter types.
* @param {Function} [cb] the callback to invoke when the action is done.
*
* @return {comb.Promise} a promise that is generally resolved with the
* number of rows updated, but that is adapter dependent.
*/
update: function (values, cb) {
return this.executeDui(this.updateSql(values)).classic(cb);
},
/**
* @see patio.Dataset#set
*/
set: function () {
this.update.apply(this, arguments);
},
/**
* @private
* Execute the given select SQL on the database using execute. Use the
* readOnly server unless a specific server is set.
*/
execute: function (sql, opts) {
return this.db.execute(sql, merge({server: this.__opts.server || "readOnly"}, opts || {}));
},
/**
* @private
* Execute the given SQL on the database using {@link patio.Database#executeDdl}.
*/
executeDdl: function (sql, opts) {
return this.db.executeDdl(sql, this.__defaultServerOpts(opts || {}));
},
/**
* @private
* Execute the given SQL on the database using {@link patio.Database#executeDui}.
*/
executeDui: function (sql, opts) {
return this.db.executeDui(sql, this.__defaultServerOpts(opts || {}));
},
/**
* @private
* Execute the given SQL on the database using {@link patio.Database#executeInsert}.
*/
executeInsert: function (sql, opts) {
return this.db.executeInsert(sql, this.__defaultServerOpts(opts || {}));
},
/**
* This is run inside {@link patio.Dataset#all}, after all of the records have been loaded
* via {@link patio.Dataset#forEach}, but before any block passed to all is called. It is called with
* a single argument, an array of all returned records. Does nothing by
* default.
*/
postLoad: function (allRecords) {
return allRecords;
},
/**
* @private
*
* Clone of this dataset usable in aggregate operations. Does
* a {@link patio.Dataset#fromSelf} if dataset contains any parameters that would
* affect normal aggregation, or just removes an existing
* order if not.
*/
__aggregateDataset: function () {
return this._optionsOverlap(this._static.COUNT_FROM_SELF_OPTS) ? this.fromSelf() : this.unordered();
},
/**
* @private
* Set the server to use to "default" unless it is already set in the passed opts
*/
__defaultServerOpts: function (opts) {
return merge({server: this.__opts.server || "default"}, opts || {});
},
/**
* @private
*
* Returns the string version of the identifier.
*
* @param {String|patio.sql.Identifier\patio.sql.QualifiedIdentifier|patio.sql.AliasedExpression} identifier
* identifier to resolve to a string.
* @return {String} the string version of the identifier.
*/
__hashIdentifierToName: function (identifier) {
return isString(identifier) ? this.__hashIdentifierToName(this.stringToIdentifier(identifier)) :
isInstanceOf(identifier, Identifier) ? identifier.value :
isInstanceOf(identifier, QualifiedIdentifier) ? identifier.column :
isInstanceOf(identifier, AliasedExpression) ? identifier.alias : identifier;
},
/**@ignore*/
getters: {
/**@lends patio.Dataset.prototype*/
/**
* @field
* @type {comb.Promise}
*
* Returns a promise that is resolved with the columns in the result set in order as an array of strings.
* If the columns are currently cached, then the promise is immediately resolved with the cached value. Otherwise,
* a SELECT query is performed to retrieve a single row in order to get the columns.
*
* If you are looking for all columns for a single table and maybe some information about
* each column (e.g. database type), see {@link patio.Database#schema}.
*
* <pre class="code">
* DB.from("table").columns.chain(function(columns){
* // => ["id", "name"]
* });
* </pre>
**/
columns: function () {
if (this.__columns) {
return asyncArray(this.__columns);
} else {
var ds = this.unfiltered().unordered().mergeOptions({distinct: null, limit: 1});
return asyncArray(ds.forEach().chain