Browse Source

Merge pull request #91 from yc662/function

Introduce the concept of functions
dev
Brian C 12 years ago
parent
commit
fe735871ac
  1. 8
      lib/dialect/postgres.js
  2. 57
      lib/functions.js
  3. 19
      lib/index.js
  4. 22
      lib/node/functionCall.js
  5. 7
      lib/node/valueExpression.js
  6. 4
      test/binary-clause-tests.js
  7. 6
      test/dialects/binary-clause-tests.js
  8. 8
      test/dialects/value-expression-tests.js
  9. 68
      test/function-tests.js
  10. 1
      test/index-tests.js
  11. 17
      test/value-expression-tests.js

8
lib/dialect/postgres.js

@ -15,7 +15,7 @@ Postgres.prototype._arrayAggFunctionName = 'array_agg';
Postgres.prototype.getQuery = function(queryNode) {
// passed in a table, not a query
if(queryNode instanceof Table) {
if (queryNode instanceof Table) {
queryNode = queryNode.select(queryNode.star());
}
this.output = this.visit(queryNode);
@ -56,6 +56,7 @@ Postgres.prototype.visit = function(node) {
case 'INDEXES' : return this.visitIndexes(node);
case 'CREATE INDEX' : return this.visitCreateIndex(node);
case 'DROP INDEX' : return this.visitDropIndex(node);
case 'FUNCTION CALL' : return this.visitFunctionCall(node);
case 'UNARY' : return this.visitUnary(node);
case 'BINARY' : return this.visitBinary(node);
@ -385,6 +386,11 @@ Postgres.prototype.visitColumn = function(columnNode) {
return [txt];
};
Postgres.prototype.visitFunctionCall = function(functionCall) {
var txt = functionCall.name + '(' + functionCall.nodes.map(this.visit.bind(this)).join(', ') + ')';
return [txt];
};
Postgres.prototype.visitParameter = function(parameter) {
this.params.push(parameter.value());
return "$"+this.params.length;

57
lib/functions.js

@ -0,0 +1,57 @@
'use strict';
var _ = require('lodash');
var sliced = require('sliced');
var FunctionCall = require(__dirname + '/node/functionCall');
// create a function that creates a function call of the specific name, using the specified sql instance
var getFunctionCallCreator = function(name, sql) {
return function() {
// turn array-like arguments object into a true array
var functionCall = new FunctionCall(name, sliced(arguments));
functionCall.sql = sql;
return functionCall;
};
};
// creates a hash of functions for a sql instance
var getFunctions = function(functionNames, sql) {
var functions = _.reduce(functionNames, function(reducer, name) {
reducer[name] = getFunctionCallCreator(name, sql);
return reducer;
}, {});
return functions;
};
// aggregate functions available to all databases
var aggregateFunctions = [
'AVG',
'COUNT',
'DISTINCT',
'MAX',
'MIN',
'SUM'
];
// common scalar functions available to most databases
var scalarFunctions = [
'ABS',
'COALESC',
'LENGTH',
'LOWER',
'LTRIM',
'RANDOM',
'ROUND',
'RTRIM',
'SUBSTR',
'TRIM',
'UPPER'
];
var standardFunctionNames = aggregateFunctions.concat(scalarFunctions);
// creates a hash of standard functions for a sql instance
var getStandardFunctions = function(sql) {
return getFunctions(standardFunctionNames, sql);
};
module.exports.getStandardFunctions = getStandardFunctions;

19
lib/index.js

@ -1,6 +1,9 @@
'use strict';
var _ = require('lodash');
var sliced = require('sliced');
var FunctionCall = require(__dirname + '/node/functionCall');
var functions = require(__dirname + '/functions');
var Query = require(__dirname + '/node/query');
var Table = require(__dirname + '/table');
@ -11,8 +14,12 @@ var Sql = function(dialect) {
dialect = dialect || DEFAULT_DIALECT;
this.setDialect(dialect);
// attach the standard SQL functions to this instance
this.functions = functions.getStandardFunctions(this);
};
// Define a table
Sql.prototype.define = function(def) {
def = _.defaults(def || {}, {
sql: this
@ -21,12 +28,24 @@ Sql.prototype.define = function(def) {
return Table.define(def);
};
// Returns a function call creator
Sql.prototype.functionCallCreator = function(name) {
var sql = this;
return function() {
var functionCall = new FunctionCall(name, sliced(arguments));
functionCall.sql = sql;
return functionCall;
};
};
// Returns a select statement
Sql.prototype.select = function() {
var query = new Query({sql: this});
query.select.apply(query, arguments);
return query;
};
// Set the dialect
Sql.prototype.setDialect = function(dialect) {
switch(dialect.toLowerCase()) {
case 'postgres':

22
lib/node/functionCall.js

@ -0,0 +1,22 @@
'use strict';
var _ = require('lodash');
var Node = require(__dirname);
var ParameterNode = require(__dirname + '/parameter');
var valueExpressionMixin = require(__dirname + '/valueExpression');
var FunctionCallNode = Node.define({
type: 'FUNCTION CALL',
constructor: function(name, args) {
Node.call(this);
this.name = name;
this.addAll(args.map(function (v) {
return v.toNode ? v.toNode() : new ParameterNode(v);
}));
}
});
// mix in value expression
_.extend(FunctionCallNode.prototype, valueExpressionMixin());
module.exports = FunctionCallNode;

7
lib/node/valueExpression.js

@ -13,8 +13,7 @@ var processParams = function(val) {
};
// Value expressions can be composed to form new value expressions.
// Value expressions include binary expressions and unary expressions
// so far. ValueExpressionMixin is evaluated at runtime, hence the
// ValueExpressionMixin is evaluated at runtime, hence the
// "thunk" around it.
var ValueExpressionMixin = module.exports = function() {
var BinaryNode = require(__dirname + '/binary');
@ -66,8 +65,8 @@ var ValueExpressionMixin = module.exports = function() {
gte : binaryMethod('>='),
lt : binaryMethod('<'),
lte : binaryMethod('<='),
add : binaryMethod('+'),
subtract : binaryMethod('-'),
plus : binaryMethod('+'),
minus : binaryMethod('-'),
multiply : binaryMethod('*'),
divide : binaryMethod('/'),
modulo : binaryMethod('%'),

4
test/binary-clause-tests.js

@ -22,8 +22,8 @@ test('operators', function() {
assert.equal(Foo.baz.gte(1).operator, '>=');
assert.equal(Foo.baz.lt(1).operator, '<');
assert.equal(Foo.baz.lte(1).operator, '<=');
assert.equal(Foo.baz.add(1).operator, '+');
assert.equal(Foo.baz.subtract(1).operator, '-');
assert.equal(Foo.baz.plus(1).operator, '+');
assert.equal(Foo.baz.minus(1).operator, '-');
assert.equal(Foo.baz.multiply(1).operator, '*');
assert.equal(Foo.baz.divide(1).operator, '/');
assert.equal(Foo.baz.modulo(1).operator, '%');

6
test/dialects/binary-clause-tests.js

@ -6,7 +6,7 @@ var post = Harness.definePostTable();
var Table = require(__dirname + '/../../lib/table');
Harness.test({
query : customer.select(customer.name.add(customer.age)),
query : customer.select(customer.name.plus(customer.age)),
pg : 'SELECT ("customer"."name" + "customer"."age") FROM "customer"',
sqlite: 'SELECT ("customer"."name" + "customer"."age") FROM "customer"',
mysql : 'SELECT (`customer`.`name` + `customer`.`age`) FROM `customer`',
@ -14,7 +14,7 @@ Harness.test({
});
Harness.test({
query : post.select(post.content.add('!')).where(post.userId.in(customer.subQuery().select(customer.id))),
query : post.select(post.content.plus('!')).where(post.userId.in(customer.subQuery().select(customer.id))),
pg : 'SELECT ("post"."content" + $1) FROM "post" WHERE ("post"."userId" IN (SELECT "customer"."id" FROM "customer"))',
sqlite: 'SELECT ("post"."content" + $1) FROM "post" WHERE ("post"."userId" IN (SELECT "customer"."id" FROM "customer"))',
mysql : 'SELECT (`post`.`content` + ?) FROM `post` WHERE (`post`.`userId` IN (SELECT `customer`.`id` FROM `customer`))',
@ -22,7 +22,7 @@ Harness.test({
});
Harness.test({
query : post.select(post.id.add(': ').add(post.content)).where(post.userId.notIn(customer.subQuery().select(customer.id))),
query : post.select(post.id.plus(': ').plus(post.content)).where(post.userId.notIn(customer.subQuery().select(customer.id))),
pg : 'SELECT (("post"."id" + $1) + "post"."content") FROM "post" WHERE ("post"."userId" NOT IN (SELECT "customer"."id" FROM "customer"))',
sqlite : 'SELECT (("post"."id" + $1) + "post"."content") FROM "post" WHERE ("post"."userId" NOT IN (SELECT "customer"."id" FROM "customer"))',
mysql : 'SELECT ((`post`.`id` + ?) + `post`.`content`) FROM `post` WHERE (`post`.`userId` NOT IN (SELECT `customer`.`id` FROM `customer`))',

8
test/dialects/value-expression-tests.js

@ -6,7 +6,7 @@ var v = Harness.defineVariableTable();
// Test composition of binary methods +, *, -, =.
Harness.test({
query : customer.select(customer.name, customer.income.modulo(100)).where(customer.age.add(5).multiply(customer.age.subtract(2)).equals(10)),
query : customer.select(customer.name, customer.income.modulo(100)).where(customer.age.plus(5).multiply(customer.age.minus(2)).equals(10)),
pg : 'SELECT "customer"."name", ("customer"."income" % $1) FROM "customer" WHERE ((("customer"."age" + $2) * ("customer"."age" - $3)) = $4)',
sqlite: 'SELECT "customer"."name", ("customer"."income" % $1) FROM "customer" WHERE ((("customer"."age" + $2) * ("customer"."age" - $3)) = $4)',
mysql : 'SELECT `customer`.`name`, (`customer`.`income` % ?) FROM `customer` WHERE (((`customer`.`age` + ?) * (`customer`.`age` - ?)) = ?)',
@ -15,7 +15,7 @@ Harness.test({
// Test composition of binary (e.g. +) and unary (e.g. like) methods.
Harness.test({
query : customer.select(customer.name).where(customer.name.like(customer.id.add('hello'))),
query : customer.select(customer.name).where(customer.name.like(customer.id.plus('hello'))),
pg : 'SELECT "customer"."name" FROM "customer" WHERE ("customer"."name" LIKE ("customer"."id" + $1))',
sqlite: 'SELECT "customer"."name" FROM "customer" WHERE ("customer"."name" LIKE ("customer"."id" + $1))',
mysql : 'SELECT `customer`.`name` FROM `customer` WHERE (`customer`.`name` LIKE (`customer`.`id` + ?))',
@ -25,7 +25,7 @@ Harness.test({
// Test implementing simple formulas.
// Acceleration formula. (a * t^2 / 2) + (v * t) = d
Harness.test({
query : v.select(v.a.multiply(v.a).divide(2).add(v.v.multiply(v.t)).equals(v.d)),
query : v.select(v.a.multiply(v.a).divide(2).plus(v.v.multiply(v.t)).equals(v.d)),
pg : 'SELECT (((("variable"."a" * "variable"."a") / $1) + ("variable"."v" * "variable"."t")) = "variable"."d") FROM "variable"',
sqlite: 'SELECT (((("variable"."a" * "variable"."a") / $1) + ("variable"."v" * "variable"."t")) = "variable"."d") FROM "variable"',
mysql : 'SELECT ((((`variable`.`a` * `variable`.`a`) / ?) + (`variable`.`v` * `variable`.`t`)) = `variable`.`d`) FROM `variable`',
@ -34,7 +34,7 @@ Harness.test({
// Pythagorean theorem. a^2 + b^2 = c^2.
Harness.test({
query : v.select(v.a.multiply(v.a).add(v.b.multiply(v.b)).equals(v.c.multiply(v.c))),
query : v.select(v.a.multiply(v.a).plus(v.b.multiply(v.b)).equals(v.c.multiply(v.c))),
pg : 'SELECT ((("variable"."a" * "variable"."a") + ("variable"."b" * "variable"."b")) = ("variable"."c" * "variable"."c")) FROM "variable"',
sqlite: 'SELECT ((("variable"."a" * "variable"."a") + ("variable"."b" * "variable"."b")) = ("variable"."c" * "variable"."c")) FROM "variable"',
mysql : 'SELECT (((`variable`.`a` * `variable`.`a`) + (`variable`.`b` * `variable`.`b`)) = (`variable`.`c` * `variable`.`c`)) FROM `variable`',

68
test/function-tests.js

@ -0,0 +1,68 @@
/* global suite, test */
'use strict';
var assert = require('assert');
var sql = require(__dirname + '/../lib').setDialect('postgres');
var user = sql.define({
name: 'user',
columns: ['id', 'email', 'name']
});
suite('function', function() {
test('creating function call works', function() {
var upper = sql.functionCallCreator('UPPER');
var functionCall = upper('hello', 'world').toQuery();
assert.equal(functionCall.text, 'UPPER($1, $2)');
assert.equal(functionCall.values[0], 'hello');
assert.equal(functionCall.values[1], 'world');
});
test('creating function call on columns works', function() {
var upper = sql.functionCallCreator('UPPER');
var functionCall = upper(user.id, user.email).toQuery();
assert.equal(functionCall.text, 'UPPER("user"."id", "user"."email")');
assert.equal(functionCall.values.length, 0);
});
test('function call inside select works', function() {
var upper = sql.functionCallCreator('UPPER');
var query = sql.select(upper(user.id, user.email)).from(user).where(user.email.equals('brian.m.carlson@gmail.com')).toQuery();
assert.equal(query.text, 'SELECT UPPER("user"."id", "user"."email") FROM "user" WHERE ("user"."email" = $1)');
assert.equal(query.values[0], 'brian.m.carlson@gmail.com');
});
test('standard aggregate functions with having clause', function() {
var count = sql.functions.COUNT;
var distinct = sql.functions.DISTINCT;
var distinctEmailCount = count(distinct(user.email));
var query = user.select(user.id, distinctEmailCount).group(user.id).having(distinctEmailCount.gt(100)).toQuery();
assert.equal(query.text, 'SELECT "user"."id", COUNT(DISTINCT("user"."email")) FROM "user" GROUP BY "user"."id" HAVING (COUNT(DISTINCT("user"."email")) > $1)');
assert.equal(query.values[0], 100);
});
test('custom and standard functions behave the same', function() {
var standardUpper = sql.functions.UPPER;
var customUpper = sql.functionCallCreator('UPPER');
var standardQuery = user.select(standardUpper(user.name)).toQuery();
var customQuery = user.select(customUpper(user.name)).toQuery();
var expectedQuery = 'SELECT UPPER("user"."name") FROM "user"';
assert.equal(standardQuery.text, expectedQuery);
assert.equal(customQuery.text, expectedQuery);
});
test('combine function with operations', function() {
var f = sql.functions;
var query = user.select(f.AVG(f.DISTINCT(f.COUNT(user.id).plus(f.MAX(user.id))).minus(f.MIN(user.id))).multiply(100)).toQuery();
assert.equal(query.text, 'SELECT (AVG((DISTINCT((COUNT("user"."id") + MAX("user"."id"))) - MIN("user"."id"))) * $1) FROM "user"');
assert.equal(query.values[0], 100);
});
});

1
test/index-tests.js

@ -61,5 +61,4 @@ suite('index', function() {
assert.equal(postgres.dialect, require(__dirname + '/../lib/dialect/postgres'));
assert.equal(sqlite.dialect, require(__dirname + '/../lib/dialect/sqlite'));
});
});

17
test/value-expression-tests.js

@ -0,0 +1,17 @@
/* global suite, test */
'use strict';
var assert = require('assert');
var valueExpressionMixin = require(__dirname + './../lib/node/valueExpression');
var Node = require(__dirname + './../lib/node');
suite('value-expression', function() {
test("value expression mixin should not overwrite Node prototype properties", function() {
var mixin = valueExpressionMixin();
// make sure that the node class doesn't have any conflicting properties
for (var key in mixin) {
assert.equal(Node.prototype[key], undefined);
}
});
});
Loading…
Cancel
Save