Skip to content

Commit fe73587

Browse files
committed
Merge pull request brianc#91 from yc662/function
Introduce the concept of functions
2 parents c5400f6 + ebf53a1 commit fe73587

11 files changed

Lines changed: 202 additions & 15 deletions

lib/dialect/postgres.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Postgres.prototype._arrayAggFunctionName = 'array_agg';
1515

1616
Postgres.prototype.getQuery = function(queryNode) {
1717
// passed in a table, not a query
18-
if(queryNode instanceof Table) {
18+
if (queryNode instanceof Table) {
1919
queryNode = queryNode.select(queryNode.star());
2020
}
2121
this.output = this.visit(queryNode);
@@ -56,6 +56,7 @@ Postgres.prototype.visit = function(node) {
5656
case 'INDEXES' : return this.visitIndexes(node);
5757
case 'CREATE INDEX' : return this.visitCreateIndex(node);
5858
case 'DROP INDEX' : return this.visitDropIndex(node);
59+
case 'FUNCTION CALL' : return this.visitFunctionCall(node);
5960

6061
case 'UNARY' : return this.visitUnary(node);
6162
case 'BINARY' : return this.visitBinary(node);
@@ -385,6 +386,11 @@ Postgres.prototype.visitColumn = function(columnNode) {
385386
return [txt];
386387
};
387388

389+
Postgres.prototype.visitFunctionCall = function(functionCall) {
390+
var txt = functionCall.name + '(' + functionCall.nodes.map(this.visit.bind(this)).join(', ') + ')';
391+
return [txt];
392+
};
393+
388394
Postgres.prototype.visitParameter = function(parameter) {
389395
this.params.push(parameter.value());
390396
return "$"+this.params.length;

lib/functions.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
var _ = require('lodash');
3+
var sliced = require('sliced');
4+
var FunctionCall = require(__dirname + '/node/functionCall');
5+
6+
// create a function that creates a function call of the specific name, using the specified sql instance
7+
var getFunctionCallCreator = function(name, sql) {
8+
return function() {
9+
// turn array-like arguments object into a true array
10+
var functionCall = new FunctionCall(name, sliced(arguments));
11+
functionCall.sql = sql;
12+
return functionCall;
13+
};
14+
};
15+
16+
// creates a hash of functions for a sql instance
17+
var getFunctions = function(functionNames, sql) {
18+
var functions = _.reduce(functionNames, function(reducer, name) {
19+
reducer[name] = getFunctionCallCreator(name, sql);
20+
return reducer;
21+
}, {});
22+
return functions;
23+
};
24+
25+
// aggregate functions available to all databases
26+
var aggregateFunctions = [
27+
'AVG',
28+
'COUNT',
29+
'DISTINCT',
30+
'MAX',
31+
'MIN',
32+
'SUM'
33+
];
34+
35+
// common scalar functions available to most databases
36+
var scalarFunctions = [
37+
'ABS',
38+
'COALESC',
39+
'LENGTH',
40+
'LOWER',
41+
'LTRIM',
42+
'RANDOM',
43+
'ROUND',
44+
'RTRIM',
45+
'SUBSTR',
46+
'TRIM',
47+
'UPPER'
48+
];
49+
50+
var standardFunctionNames = aggregateFunctions.concat(scalarFunctions);
51+
52+
// creates a hash of standard functions for a sql instance
53+
var getStandardFunctions = function(sql) {
54+
return getFunctions(standardFunctionNames, sql);
55+
};
56+
57+
module.exports.getStandardFunctions = getStandardFunctions;

lib/index.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
'use strict';
22

33
var _ = require('lodash');
4+
var sliced = require('sliced');
5+
var FunctionCall = require(__dirname + '/node/functionCall');
6+
var functions = require(__dirname + '/functions');
47
var Query = require(__dirname + '/node/query');
58
var Table = require(__dirname + '/table');
69

@@ -11,8 +14,12 @@ var Sql = function(dialect) {
1114
dialect = dialect || DEFAULT_DIALECT;
1215

1316
this.setDialect(dialect);
17+
18+
// attach the standard SQL functions to this instance
19+
this.functions = functions.getStandardFunctions(this);
1420
};
1521

22+
// Define a table
1623
Sql.prototype.define = function(def) {
1724
def = _.defaults(def || {}, {
1825
sql: this
@@ -21,12 +28,24 @@ Sql.prototype.define = function(def) {
2128
return Table.define(def);
2229
};
2330

31+
// Returns a function call creator
32+
Sql.prototype.functionCallCreator = function(name) {
33+
var sql = this;
34+
return function() {
35+
var functionCall = new FunctionCall(name, sliced(arguments));
36+
functionCall.sql = sql;
37+
return functionCall;
38+
};
39+
};
40+
41+
// Returns a select statement
2442
Sql.prototype.select = function() {
2543
var query = new Query({sql: this});
2644
query.select.apply(query, arguments);
2745
return query;
2846
};
2947

48+
// Set the dialect
3049
Sql.prototype.setDialect = function(dialect) {
3150
switch(dialect.toLowerCase()) {
3251
case 'postgres':

lib/node/functionCall.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use strict';
2+
3+
var _ = require('lodash');
4+
var Node = require(__dirname);
5+
var ParameterNode = require(__dirname + '/parameter');
6+
var valueExpressionMixin = require(__dirname + '/valueExpression');
7+
8+
var FunctionCallNode = Node.define({
9+
type: 'FUNCTION CALL',
10+
constructor: function(name, args) {
11+
Node.call(this);
12+
this.name = name;
13+
this.addAll(args.map(function (v) {
14+
return v.toNode ? v.toNode() : new ParameterNode(v);
15+
}));
16+
}
17+
});
18+
19+
// mix in value expression
20+
_.extend(FunctionCallNode.prototype, valueExpressionMixin());
21+
22+
module.exports = FunctionCallNode;

lib/node/valueExpression.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ var processParams = function(val) {
1313
};
1414

1515
// Value expressions can be composed to form new value expressions.
16-
// Value expressions include binary expressions and unary expressions
17-
// so far. ValueExpressionMixin is evaluated at runtime, hence the
16+
// ValueExpressionMixin is evaluated at runtime, hence the
1817
// "thunk" around it.
1918
var ValueExpressionMixin = module.exports = function() {
2019
var BinaryNode = require(__dirname + '/binary');
@@ -66,8 +65,8 @@ var ValueExpressionMixin = module.exports = function() {
6665
gte : binaryMethod('>='),
6766
lt : binaryMethod('<'),
6867
lte : binaryMethod('<='),
69-
add : binaryMethod('+'),
70-
subtract : binaryMethod('-'),
68+
plus : binaryMethod('+'),
69+
minus : binaryMethod('-'),
7170
multiply : binaryMethod('*'),
7271
divide : binaryMethod('/'),
7372
modulo : binaryMethod('%'),

test/binary-clause-tests.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ test('operators', function() {
2222
assert.equal(Foo.baz.gte(1).operator, '>=');
2323
assert.equal(Foo.baz.lt(1).operator, '<');
2424
assert.equal(Foo.baz.lte(1).operator, '<=');
25-
assert.equal(Foo.baz.add(1).operator, '+');
26-
assert.equal(Foo.baz.subtract(1).operator, '-');
25+
assert.equal(Foo.baz.plus(1).operator, '+');
26+
assert.equal(Foo.baz.minus(1).operator, '-');
2727
assert.equal(Foo.baz.multiply(1).operator, '*');
2828
assert.equal(Foo.baz.divide(1).operator, '/');
2929
assert.equal(Foo.baz.modulo(1).operator, '%');

test/dialects/binary-clause-tests.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,23 @@ var post = Harness.definePostTable();
66
var Table = require(__dirname + '/../../lib/table');
77

88
Harness.test({
9-
query : customer.select(customer.name.add(customer.age)),
9+
query : customer.select(customer.name.plus(customer.age)),
1010
pg : 'SELECT ("customer"."name" + "customer"."age") FROM "customer"',
1111
sqlite: 'SELECT ("customer"."name" + "customer"."age") FROM "customer"',
1212
mysql : 'SELECT (`customer`.`name` + `customer`.`age`) FROM `customer`',
1313
params: []
1414
});
1515

1616
Harness.test({
17-
query : post.select(post.content.add('!')).where(post.userId.in(customer.subQuery().select(customer.id))),
17+
query : post.select(post.content.plus('!')).where(post.userId.in(customer.subQuery().select(customer.id))),
1818
pg : 'SELECT ("post"."content" + $1) FROM "post" WHERE ("post"."userId" IN (SELECT "customer"."id" FROM "customer"))',
1919
sqlite: 'SELECT ("post"."content" + $1) FROM "post" WHERE ("post"."userId" IN (SELECT "customer"."id" FROM "customer"))',
2020
mysql : 'SELECT (`post`.`content` + ?) FROM `post` WHERE (`post`.`userId` IN (SELECT `customer`.`id` FROM `customer`))',
2121
params: ['!']
2222
});
2323

2424
Harness.test({
25-
query : post.select(post.id.add(': ').add(post.content)).where(post.userId.notIn(customer.subQuery().select(customer.id))),
25+
query : post.select(post.id.plus(': ').plus(post.content)).where(post.userId.notIn(customer.subQuery().select(customer.id))),
2626
pg : 'SELECT (("post"."id" + $1) + "post"."content") FROM "post" WHERE ("post"."userId" NOT IN (SELECT "customer"."id" FROM "customer"))',
2727
sqlite : 'SELECT (("post"."id" + $1) + "post"."content") FROM "post" WHERE ("post"."userId" NOT IN (SELECT "customer"."id" FROM "customer"))',
2828
mysql : 'SELECT ((`post`.`id` + ?) + `post`.`content`) FROM `post` WHERE (`post`.`userId` NOT IN (SELECT `customer`.`id` FROM `customer`))',

test/dialects/value-expression-tests.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ var v = Harness.defineVariableTable();
66

77
// Test composition of binary methods +, *, -, =.
88
Harness.test({
9-
query : customer.select(customer.name, customer.income.modulo(100)).where(customer.age.add(5).multiply(customer.age.subtract(2)).equals(10)),
9+
query : customer.select(customer.name, customer.income.modulo(100)).where(customer.age.plus(5).multiply(customer.age.minus(2)).equals(10)),
1010
pg : 'SELECT "customer"."name", ("customer"."income" % $1) FROM "customer" WHERE ((("customer"."age" + $2) * ("customer"."age" - $3)) = $4)',
1111
sqlite: 'SELECT "customer"."name", ("customer"."income" % $1) FROM "customer" WHERE ((("customer"."age" + $2) * ("customer"."age" - $3)) = $4)',
1212
mysql : 'SELECT `customer`.`name`, (`customer`.`income` % ?) FROM `customer` WHERE (((`customer`.`age` + ?) * (`customer`.`age` - ?)) = ?)',
@@ -15,7 +15,7 @@ Harness.test({
1515

1616
// Test composition of binary (e.g. +) and unary (e.g. like) methods.
1717
Harness.test({
18-
query : customer.select(customer.name).where(customer.name.like(customer.id.add('hello'))),
18+
query : customer.select(customer.name).where(customer.name.like(customer.id.plus('hello'))),
1919
pg : 'SELECT "customer"."name" FROM "customer" WHERE ("customer"."name" LIKE ("customer"."id" + $1))',
2020
sqlite: 'SELECT "customer"."name" FROM "customer" WHERE ("customer"."name" LIKE ("customer"."id" + $1))',
2121
mysql : 'SELECT `customer`.`name` FROM `customer` WHERE (`customer`.`name` LIKE (`customer`.`id` + ?))',
@@ -25,7 +25,7 @@ Harness.test({
2525
// Test implementing simple formulas.
2626
// Acceleration formula. (a * t^2 / 2) + (v * t) = d
2727
Harness.test({
28-
query : v.select(v.a.multiply(v.a).divide(2).add(v.v.multiply(v.t)).equals(v.d)),
28+
query : v.select(v.a.multiply(v.a).divide(2).plus(v.v.multiply(v.t)).equals(v.d)),
2929
pg : 'SELECT (((("variable"."a" * "variable"."a") / $1) + ("variable"."v" * "variable"."t")) = "variable"."d") FROM "variable"',
3030
sqlite: 'SELECT (((("variable"."a" * "variable"."a") / $1) + ("variable"."v" * "variable"."t")) = "variable"."d") FROM "variable"',
3131
mysql : 'SELECT ((((`variable`.`a` * `variable`.`a`) / ?) + (`variable`.`v` * `variable`.`t`)) = `variable`.`d`) FROM `variable`',
@@ -34,7 +34,7 @@ Harness.test({
3434

3535
// Pythagorean theorem. a^2 + b^2 = c^2.
3636
Harness.test({
37-
query : v.select(v.a.multiply(v.a).add(v.b.multiply(v.b)).equals(v.c.multiply(v.c))),
37+
query : v.select(v.a.multiply(v.a).plus(v.b.multiply(v.b)).equals(v.c.multiply(v.c))),
3838
pg : 'SELECT ((("variable"."a" * "variable"."a") + ("variable"."b" * "variable"."b")) = ("variable"."c" * "variable"."c")) FROM "variable"',
3939
sqlite: 'SELECT ((("variable"."a" * "variable"."a") + ("variable"."b" * "variable"."b")) = ("variable"."c" * "variable"."c")) FROM "variable"',
4040
mysql : 'SELECT (((`variable`.`a` * `variable`.`a`) + (`variable`.`b` * `variable`.`b`)) = (`variable`.`c` * `variable`.`c`)) FROM `variable`',

test/function-tests.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/* global suite, test */
2+
'use strict';
3+
var assert = require('assert');
4+
5+
var sql = require(__dirname + '/../lib').setDialect('postgres');
6+
7+
var user = sql.define({
8+
name: 'user',
9+
columns: ['id', 'email', 'name']
10+
});
11+
12+
suite('function', function() {
13+
test('creating function call works', function() {
14+
var upper = sql.functionCallCreator('UPPER');
15+
var functionCall = upper('hello', 'world').toQuery();
16+
17+
assert.equal(functionCall.text, 'UPPER($1, $2)');
18+
assert.equal(functionCall.values[0], 'hello');
19+
assert.equal(functionCall.values[1], 'world');
20+
});
21+
22+
test('creating function call on columns works', function() {
23+
var upper = sql.functionCallCreator('UPPER');
24+
var functionCall = upper(user.id, user.email).toQuery();
25+
26+
assert.equal(functionCall.text, 'UPPER("user"."id", "user"."email")');
27+
assert.equal(functionCall.values.length, 0);
28+
});
29+
30+
test('function call inside select works', function() {
31+
var upper = sql.functionCallCreator('UPPER');
32+
var query = sql.select(upper(user.id, user.email)).from(user).where(user.email.equals('brian.m.carlson@gmail.com')).toQuery();
33+
34+
assert.equal(query.text, 'SELECT UPPER("user"."id", "user"."email") FROM "user" WHERE ("user"."email" = $1)');
35+
assert.equal(query.values[0], 'brian.m.carlson@gmail.com');
36+
});
37+
38+
test('standard aggregate functions with having clause', function() {
39+
var count = sql.functions.COUNT;
40+
var distinct = sql.functions.DISTINCT;
41+
var distinctEmailCount = count(distinct(user.email));
42+
43+
var query = user.select(user.id, distinctEmailCount).group(user.id).having(distinctEmailCount.gt(100)).toQuery();
44+
45+
assert.equal(query.text, 'SELECT "user"."id", COUNT(DISTINCT("user"."email")) FROM "user" GROUP BY "user"."id" HAVING (COUNT(DISTINCT("user"."email")) > $1)');
46+
assert.equal(query.values[0], 100);
47+
});
48+
49+
test('custom and standard functions behave the same', function() {
50+
var standardUpper = sql.functions.UPPER;
51+
var customUpper = sql.functionCallCreator('UPPER');
52+
53+
var standardQuery = user.select(standardUpper(user.name)).toQuery();
54+
var customQuery = user.select(customUpper(user.name)).toQuery();
55+
56+
var expectedQuery = 'SELECT UPPER("user"."name") FROM "user"';
57+
assert.equal(standardQuery.text, expectedQuery);
58+
assert.equal(customQuery.text, expectedQuery);
59+
});
60+
61+
test('combine function with operations', function() {
62+
var f = sql.functions;
63+
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();
64+
65+
assert.equal(query.text, 'SELECT (AVG((DISTINCT((COUNT("user"."id") + MAX("user"."id"))) - MIN("user"."id"))) * $1) FROM "user"');
66+
assert.equal(query.values[0], 100);
67+
});
68+
});

test/index-tests.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,4 @@ suite('index', function() {
6161
assert.equal(postgres.dialect, require(__dirname + '/../lib/dialect/postgres'));
6262
assert.equal(sqlite.dialect, require(__dirname + '/../lib/dialect/sqlite'));
6363
});
64-
6564
});

0 commit comments

Comments
 (0)