From 729ee74fc57d40d9c3322eacd8fadb4e02e820cb Mon Sep 17 00:00:00 2001 From: "Narcisse D. ASSOGBA" Date: Thu, 29 Mar 2018 17:44:35 +0100 Subject: [PATCH] Add definitions for sql-bricks package --- types/sql-bricks/index.d.ts | 404 ++++++++++++ types/sql-bricks/sql-bricks-tests.ts | 935 +++++++++++++++++++++++++++ types/sql-bricks/tsconfig.json | 23 + types/sql-bricks/tslint.json | 10 + 4 files changed, 1372 insertions(+) create mode 100644 types/sql-bricks/index.d.ts create mode 100644 types/sql-bricks/sql-bricks-tests.ts create mode 100644 types/sql-bricks/tsconfig.json create mode 100644 types/sql-bricks/tslint.json diff --git a/types/sql-bricks/index.d.ts b/types/sql-bricks/index.d.ts new file mode 100644 index 0000000000..d09bfee715 --- /dev/null +++ b/types/sql-bricks/index.d.ts @@ -0,0 +1,404 @@ +// Type definitions for sql-bricks 2.0 +// Project: http://csnw.github.io/sql-bricks +// Definitions by: Narcisse Assogba +// Paleo +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +declare namespace SqlBricks { + /** + * Statement is an abstract base class for all statements (SELECT, INSERT, UPDATE, DELETE) + * and should never be instantiated directly. It is exposed because it can be used with the + * instanceof operator to easily determine whether something is a SQL Bricks statement: my_var instanceof Statement. + */ + interface Statement { + /** + * Clones a statement so that subsequent modifications do not affect the original statement. + */ + clone(): this + + /** + * Returns the non-parameterized SQL for the statement. This is called implicitly by Javascript when using a Statement anywhere that a string is expected (string concatenation, Array.join(), etc). + * While toString() is easy to use, it is not recommended in most cases because: + * It doesn't provide robust protection against SQL injection attacks (it just does basic escaping) + * It doesn't provide as much support for complex data types (objects, arrays, etc, are "stringified" before being passed to your database driver, which then has to interpret them correctly) + * It does not provide the same level of detail in error messages (see this issue) + * For the above reasons, it is usually better to use toParams(). + */ + toString(): string + + /** + * Returns an object with two properties: a parameterized text string and a values array. The values are populated with anything on the right-hand side + * of a WHERE criteria,as well as any values passed into an insert() or update() (they can be passed explicitly with val() or opted out of with sql()) + * @param options A placeholder option of '?%d' can be passed to generate placeholders compatible with node-sqlite3 (%d is replaced with the parameter #): + * @example + * update('person', {'first_name': 'Fred'}).where({'last_name': 'Flintstone'}).toParams({placeholder: '?%d'}); + * // {"text": "UPDATE person SET first_name = ?1 WHERE last_name = ?2", "values": ["Fred", "Flintstone"]} + */ + toParams(options?: { placeholder: string }): SqlBricksParam + } + + interface SqlBricksParam { + text: string + values: any[] + } + + type TableName = string | SelectStatement + + interface OnCriteria { + [column: string]: string + } + + interface WhereObject { + [column: string]: any + } + + interface WhereGroup { + op?: string + expressions: WhereExpression[] + } + + interface WhereBinary { + op: string + col: string | SelectStatement + val: any + quantifier: string + } + + /** + * When a non-expression object is passed somewhere a whereExpression is expected, + * each key/value pair will be ANDed together: + */ + type WhereExpression = WhereGroup | WhereBinary | WhereObject | string + + /** + * A SELECT statement + */ + interface SelectStatement extends Statement { + /** + * Appends additional columns to an existing query. + * @param columns can be passed as multiple arguments, a comma-delimited string or an array. + */ + select(...columns: string[]): SelectStatement + /** + * Appends additional columns to an existing query. + * @param columns can be passed as multiple arguments, a comma-delimited string or an array. + */ + select(columns: string[]): SelectStatement + select(...params): SelectStatement + as(alias: string): SelectStatement + + distinct(...columns): SelectStatement + + /** + * Makes the query a SELECT ... INTO query (which creates a new table with the results of the query). + * @alias intoTable + * @param tbl new table to create + */ + into(tbl: TableName): SelectStatement + /** + * Makes the query a SELECT ... INTO query (which creates a new table with the results of the query). + * @alias into + * @param tbl new table to create + */ + intoTable(tbl: TableName): SelectStatement + + intoTemp(tbl: TableName): SelectStatement, + intoTempTable(tbl: TableName): SelectStatement, + + /** + * Table names can be passed in as multiple string arguments, a comma-delimited string or an array. + * @param tbls table names + */ + from(...tbls: TableName[]): SelectStatement + /** + * Table names can be passed in as multiple string arguments, a comma-delimited string or an array. + * @param tbls array of table names + */ + from(tbls: TableName[]): SelectStatement + + /** + * Adds the specified join to the query. + * @alias innerJoin + * @param tbl can include an alias after a space or after the 'AS' keyword ('my_table my_alias'). + * @param onCriteria is optional if a joinCriteria function has been supplied. + */ + join(tbl: string, criteria?: OnCriteria | string[] | WhereExpression): SelectStatement + join(tbl: string, onCol1: string, onCol2: string): SelectStatement + join(firstTbl: string, ...otherTbls: string[]): SelectStatement + + leftJoin(tbl: string, criteria?: OnCriteria | string[] | WhereExpression): SelectStatement + leftJoin(tbl: string, onCol1: string, onCol2: string): SelectStatement + leftJoin(firstTbl: string, ...otherTbls: string[]): SelectStatement + rightJoin(tbl: string, criteria?: OnCriteria | string[] | WhereExpression): SelectStatement + rightJoin(tbl: string, onCol1: string, onCol2: string): SelectStatement + rightJoin(firstTbl: string, ...otherTbls: string[]): SelectStatement + fullJoin(tbl: string, criteria?: OnCriteria | string[] | WhereExpression): SelectStatement + fullJoin(tbl: string, onCol1: string, onCol2: string): SelectStatement + fullJoin(firstTbl: string, ...otherTbls: string[]): SelectStatement + crossJoin(tbl: string, criteria?: OnCriteria | string[] | WhereExpression): SelectStatement + crossJoin(tbl: string, onCol1: string, onCol2: string): SelectStatement + crossJoin(firstTbl: string, ...otherTbls: string[]): SelectStatement + innerJoin(tbl: string, criteria?: OnCriteria | string[] | WhereExpression): SelectStatement + innerJoin(tbl: string, onCol1: string, onCol2: string): SelectStatement + innerJoin(firstTbl: string, ...otherTbls: string[]): SelectStatement + leftOuterJoin(tbl: string, criteria?: OnCriteria | string[] | WhereExpression): SelectStatement + leftOuterJoin(tbl: string, onCol1: string, onCol2: string): SelectStatement + leftOuterJoin(firstTbl: string, ...otherTbls: string[]): SelectStatement + rightOuterJoin(tbl: string, criteria?: OnCriteria | string[] | WhereExpression): SelectStatement + rightOuterJoin(tbl: string, onCol1: string, onCol2: string): SelectStatement + rightOuterJoin(firstTbl: string, ...otherTbls: string[]): SelectStatement + fullOuterJoin(tbl: string, criteria?: OnCriteria | string[] | WhereExpression): SelectStatement + fullOuterJoin(tbl: string, onCol1: string, onCol2: string): SelectStatement + fullOuterJoin(firstTbl: string, ...otherTbls: string[]): SelectStatement + + on(onCriteria: OnCriteria | WhereExpression): SelectStatement, + on(onCol1: string, onCol2: string): SelectStatement, + + /** + * Joins using USING instead of ON. + * @param columnList columnList can be passed in as one or more string arguments, a comma-delimited string, or an array. + * @example + * select('*').from('person').join('address').using('address_id', 'country_id'); + * // SELECT * FROM person INNER JOIN address USING (address_id, country_id) + */ + using(...columnList: string[]): SelectStatement + using(columnList: string[]): SelectStatement + + /** + * Adds the specified natural join to the query. + * @param tbl can include an alias after a space or after the 'AS' keyword ('my_table my_alias'). + */ + naturalJoin(tbl: string): SelectStatement + naturalLeftJoin(tbl: string): SelectStatement + naturalRightJoin(tbl: string): SelectStatement + naturalFullJoin(tbl: string): SelectStatement + + naturalInnerJoin(tbl: string): SelectStatement + naturalLeftOuterJoin(tbl: string): SelectStatement + naturalRightOuterJoin(tbl: string): SelectStatement + naturalFullOuterJoin(tbl: string): SelectStatement + + where(column: string, value?: string): SelectStatement + where(whereExpr: WhereExpression): SelectStatement + where(...options): SelectStatement + + and(...options): SelectStatement + + /** + * Sets or extends the GROUP BY columns. + * @param columns can take multiple arguments, a single comma-delimited string or an array. + */ + groupBy(...columns: string[]): SelectStatement + groupBy(columns: string[]): SelectStatement + + having(column: string, value: string): SelectStatement + having(whereExpr: WhereExpression): SelectStatement + + /** + * Sets or extends the list of columns in the ORDER BY clause. + * @param columns can be passed as multiple arguments, a single comma-delimited string or an array. + */ + orderBy(...columns: string[]): SelectStatement + orderBy(columns: string[]): SelectStatement + order(...columns: string[]): SelectStatement + order(columns: string[]): SelectStatement + + forUpdate(...tbls: string[]): SelectStatement + of(tlb: string): SelectStatement + noWait(): SelectStatement + + union(...stmt: Statement[]): SelectStatement + intersect(...stmt: Statement[]): SelectStatement + minus(...stmt: Statement[]): SelectStatement + except(...stmt: Statement[]): SelectStatement + } + + /** + * An INSERT statement + */ + interface InsertStatement extends Statement { + into(tbl: TableName, ...columns: any[]): InsertStatement + intoTable(tbl: TableName, ...columns: any[]): InsertStatement + select(...params): SelectStatement, + values(...values): InsertStatement + } + + /** + * An UPDATE statement + */ + interface UpdateStatement extends Statement { + values(...values): UpdateStatement + set(...values): UpdateStatement, + where(...options): UpdateStatement, + and(...options): UpdateStatement, + } + + /** + * A DELETE statement + */ + interface DeleteStatement extends Statement { + from(...tbls: string[]): DeleteStatement, + using(...columnList): DeleteStatement, + where(...options): DeleteStatement, + and(...options): DeleteStatement, + } +} + +interface SqlBricksFn { + (...params: any[]): any + /** + * Wraps a value (user-supplied string, number, boolean, etc) so that it can be passed into SQL Bricks + * anywhere that a column is expected (the left-hand side of WHERE criteria and many other SQL Bricks APIs) + * @param value value to be wraped + */ + val(value): any + + /** + * Returns a new INSERT statement. It can be used with or without the new operator. + * @alias insertInto + * @param tbl table name + * @param values a values object or a columns list. Passing a set of columns (as multiple arguments, a comma-delimited string or an array) + * will put the statement into split keys/values mode, where a matching array of values is expected in values() + * @example + * insert('person', {'first_name': 'Fred', 'last_name': 'Flintstone'}); + * // INSERT INTO person (first_name, last_name) VALUES ('Fred', 'Flintstone') + */ + insert(tbl?: string, ...values): SqlBricks.InsertStatement + + /** + * Returns a new INSERT statement. It can be used with or without the new operator. + * @alias insert + * @param tbl table name + * @param values a values object or a columns list. Passing a set of columns (as multiple arguments, a comma-delimited string or an array) + * will put the statement into split keys/values mode, where a matching array of values is expected in values() + * @example + * insert('person', {'first_name': 'Fred', 'last_name': 'Flintstone'}); + * // INSERT INTO person (first_name, last_name) VALUES ('Fred', 'Flintstone') + */ + insertInto(tbl?: string, ...values): SqlBricks.InsertStatement + + /** + * Returns a new select statement, seeded with a set of columns. It can be used with or without the new keyword. + * @param columns it can be passed in here (or appended later via sel.select() or sel.distinct()) via multiple arguments + * or a comma-delimited string or an array. If no columns are specified, toString() will default to SELECT *. + */ + select(...columns: string[]): SqlBricks.SelectStatement + select(columns: string[] | SqlBricks.SelectStatement[]): SqlBricks.SelectStatement + select(...params): SqlBricks.SelectStatement + + /** + * Returns a new UPDATE statement. It can be used with or without the new operator. + * @param tbl table name + * @param values + */ + update(tbl: string, ...values): SqlBricks.UpdateStatement + + /** + * Returns a new DELETE statement. It can be used with or without the new operator. + * @alias deleteFrom + * @param tbl table name + */ + delete(tbl?: string): SqlBricks.DeleteStatement + /** + * Returns a new DELETE statement. It can be used with or without the new operator. + * @alias delete + * @param tbl table name + */ + deleteFrom(tbl?: string): SqlBricks.DeleteStatement + + /** + * Registers a set of frequently-used table aliases with SQL Bricks. + * These table aliases can then be used by themselves in from(), join(), etc + * and SQL Bricks will automatically expand them to include the table name as well as the alias. + * @param expansions + * @example + * sql.aliasExpansions({'psn': 'person', 'addr': 'address', 'zip': 'zipcode', 'usr': 'user'}); + * select().from('psn').join('addr', {'psn.addr_id': 'addr.id'}); + * // SELECT * FROM person psn INNER JOIN address addr ON psn.addr_id = addr.id + */ + aliasExpansions(expansions: { [tbl: string]: string }): void + + /** + * Sets a user-supplied function to automatically generate the .on() criteria for joins whenever it is not supplied explicitly. + * @param func + */ + joinCriteria(func?: (...args) => SqlBricks.OnCriteria): any + + _extension(): any + prop: number + conversions: any + + ////////////////////////////////////////// + ////// Where Expression functions ////// + ////////////////////////////////////////// + + /** + * Joins the passed expressions with AND + * @param whereExprs + */ + and(...whereExprs: SqlBricks.WhereExpression[]): SqlBricks.WhereGroup + + /** + * Joins the passed expressions with OR: + * @param whereExprs + */ + or(...whereExprs: SqlBricks.WhereExpression[]): SqlBricks.WhereGroup + + /** + * Negates the expression by wrapping it in NOT (...) + * (if it is at the top level, the parentheses are unnecessary and will be omitted) + * @param whereExpr + */ + not(whereExpr: SqlBricks.WhereExpression): SqlBricks.WhereGroup + + /** + * Generates a BETWEEN + * @param column + * @param value1 + * @param value2 + */ + between(column: string, value1, value2): SqlBricks.WhereExpression + isNull(column: string): SqlBricks.WhereExpression + isNotNull(column: string): SqlBricks.WhereExpression + like(column: string, value, escapeStr?: string): SqlBricks.WhereExpression + exists(stmt): SqlBricks.WhereExpression + in(column: string, stmt: SqlBricks.SelectStatement): SqlBricks.WhereExpression + in(column: string, ...values): SqlBricks.WhereExpression + + /** + * Generates the appropriate relational operator (=, <>, <, <=, > or >=). + * @param column column name or query result + * @param value column value + */ + eq(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary + equal(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary + notEq(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary + lt(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary + lte(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary + gt(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary + gte(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary + + eqAll(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary + notEqAll(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary + ltAll(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary + lteAll(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary + gtAll(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary + gteAll(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary + + eqAny(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary + notEqAny(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary + ltAny(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary + lteAny(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary + gtAny(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary + gteAny(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary + + eqSome(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary + notEqSome(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary + ltSome(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary + lteSome(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary + gtSome(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary + gteSome(column: string | SqlBricks.SelectStatement, value?): SqlBricks.WhereBinary +} + +declare const SqlBricks: SqlBricksFn +export = SqlBricks diff --git a/types/sql-bricks/sql-bricks-tests.ts b/types/sql-bricks/sql-bricks-tests.ts new file mode 100644 index 0000000000..94014d913d --- /dev/null +++ b/types/sql-bricks/sql-bricks-tests.ts @@ -0,0 +1,935 @@ +// Test file from sql-briks package tests +// https://github.com/CSNW/sql-bricks/blob/master/tests/tests.js +// taked the 03/29/2018. Some dtslint rules has been deabled to be +// close as possible of the original js file. + +import * as sql from "sql-bricks"; + +// tslint:disable-next-line:prefer-const +let assert; +const select = sql.select; +const insertInto = sql.insertInto, insert = sql.insert, + update = sql.update, del = sql.delete; +const and = sql.and, or = sql.or, like = sql.like, not = sql.not, $in = sql.in, + isNull = sql.isNull, isNotNull = sql.isNotNull, equal = sql.equal, eq = sql.eq, + notEq = sql.notEq, lt = sql.lt, lte = sql.lte, gt = sql.gt, gte = sql.gte, + between = sql.between, exists = sql.exists, eqAny = sql.eqAny, notEqAny = sql.notEqAny; + +const alias_expansions = { usr: 'user', psn: 'person', addr: 'address' }; +const table_to_alias = alias_expansions; +sql.aliasExpansions(alias_expansions); + +sql.joinCriteria(function(left_tbl, left_alias, right_tbl, right_alias) { + const criteria = {}; + criteria[left_alias + '.' + table_to_alias[right_tbl] + '_fk'] = right_alias + '.pk'; + return criteria; +}); + +describe('SQL Bricks', function() { + describe('parameterized sql', function() { + it('should generate for insert statements', function() { + const values = { first_name: 'Fred', last_name: 'Flintstone' }; + checkParams(insert('user', values), + 'INSERT INTO "user" (first_name, last_name) VALUES ($1, $2)', + ['Fred', 'Flintstone']); + }); + it('should generate for UPDATEs', function() { + const values = { first_name: 'Fred', last_name: 'Flintstone' }; + checkParams(update('user', values), + 'UPDATE "user" SET first_name = $1, last_name = $2', + ['Fred', 'Flintstone']); + }); + it('should generate for WHERE clauses', function() { + checkParams(select().from('user').where({ + removed: 0, + name: 'Fred Flintstone' + }), 'SELECT * FROM "user" WHERE removed = $1 AND name = $2', + [0, 'Fred Flintstone']); + }); + it('should generate WHERE clauses with arbitrary SQL', function() { + const stmt = select().from('time_limits'); + stmt.where(sql( + "tsrange(start_date_time, end_date_time, '[]') @> tsrange($1, $2, '[]')")); + check(stmt, + "SELECT * FROM time_limits WHERE tsrange(start_date_time, end_date_time, '[]') @> tsrange($1, $2, '[]')"); + }); + it('should generate WHERE clauses with arbitrary SQL and parameters', function() { + const stmt = select().from('time_limits'); + const time1 = '2014-12-06T22:35:00'; + const time2 = '2014-12-06T22:36:00'; + stmt.where(sql( + "tsrange(start_date_time, end_date_time, '[]') @> tsrange($, $, '[]')", time1, time2)); + checkParams(stmt, + "SELECT * FROM time_limits WHERE tsrange(start_date_time, end_date_time, '[]') @> tsrange($1, $2, '[]')", + ['2014-12-06T22:35:00', '2014-12-06T22:36:00']); + }); + it('should handle WHERE clauses with sql() and a null check', function() { + check(select().from('test').where(sql('my_col_val()'), null), + "SELECT * FROM test WHERE my_col_val() IS NULL"); + }); + it('should not escape single quotes in the values returned by toParams()', function() { + checkParams(update('user', { name: "Muad'Dib" }), + 'UPDATE "user" SET name = $1', + ["Muad'Dib"]); + }); + it('should not attempt to serialize arrays (should be done by subsequent layer)', function() { + checkParams(update('user', { name: ["Paul", "Muad'Dib"] }), + 'UPDATE "user" SET name = $1', + [["Paul", "Muad'Dib"]]); + }); + it('should not attempt to serialize dates', function() { + const dt = new Date(); + checkParams(update('user', { birthdate: dt }), + 'UPDATE "user" SET birthdate = $1', + [dt]); + }); + it('should generate node-sqlite3 style params', function() { + const values = { first_name: 'Fred', last_name: 'Flintstone' }; + const result = insert('user', values).toParams({ placeholder: '?%d' }); + assert.equal(result.text, 'INSERT INTO "user" (first_name, last_name) VALUES (?1, ?2)'); + assert.deepEqual(result.values, ['Fred', 'Flintstone']); + }); + it('should generate node-mysql style params', function() { + const values = { first_name: 'Fred', last_name: 'Flintstone' }; + const result = insert('user', values).toParams({ placeholder: '?' }); + assert.equal(result.text, 'INSERT INTO "user" (first_name, last_name) VALUES (?, ?)'); + assert.deepEqual(result.values, ['Fred', 'Flintstone']); + }); + it('should output non-numeric params in SQL order', function() { + const result = select().from('user').where($in(sql.val(5), [3, 5, 10])).toParams({ placeholder: '?' }); + assert.equal(result.text, 'SELECT * FROM "user" WHERE ? IN (?, ?, ?)'); + assert.deepEqual(result.values, [5, 3, 5, 10]); + }); + it('should properly parameterize subqueries', function() { + const values = { first_name: 'Fred' }; + checkParams(select(select('last_name').from('user').where(values)), + 'SELECT (SELECT last_name FROM "user" WHERE first_name = $1)', + ['Fred']); + }); + it('should properly parameterize subqueries with a join', function() { + const values = { first_name: 'Fred' }; + const query = select().from(select().from('user').where(values).as('subuser')) + .join('other_table', { 'other_table.id': 'subuser.id' }); + checkParams(query, + 'SELECT * FROM (SELECT * FROM "user" WHERE first_name = $1) subuser INNER JOIN other_table ON other_table.id = subuser.id', + ['Fred']); + }); + it('should properly parameterize subqueries in updates', function() { + const addr_id_for_usr = select('id').from('address').where('usr_id', sql('"user".id')).and('active', true); + checkParams(update('user').set('addr_id', addr_id_for_usr), + 'UPDATE "user" SET addr_id = (SELECT id FROM address WHERE usr_id = "user".id AND active = $1)', + [true]); + }); + it('should properly merge parameterized sub-expressions with $%d placeholders', function() { + checkParams(select().from('tbl').where(or(sql('a = $1', 444), sql('b = $1', 555), sql('c = $1', 666))), + 'SELECT * FROM tbl WHERE a = $1 OR b = $2 OR c = $3', + [444, 555, 666]); + }); + }); + + describe('value handling', function() { + it('should escape single quotes when toString() is used', function() { + check(update('user', { name: "Muad'Dib" }), + "UPDATE \"user\" SET name = 'Muad''Dib'"); + }); + it('should escape multiple single quotes in the same string', function() { + check(update('address', { city: "Liu'e, Hawai'i" }), + "UPDATE address SET city = 'Liu''e, Hawai''i'"); + }); + it('should support sql.val() to pass in values where columns are expected', function() { + check(select().from('user').where(sql.val('Fred'), sql('first_name')), + "SELECT * FROM \"user\" WHERE 'Fred' = first_name"); + }); + it('should support value objects with differing field orders', function() { + check(insert('user').values([{ id: 1, name: 'Fred' }, { name: 'Barney', id: 2 }]), + "INSERT INTO \"user\" (id, name) VALUES (1, 'Fred'), (2, 'Barney')"); + }); + + describe('.toString() value conversions', function() { + it('should, by default, convert dates to SQL TIMESTAMP WITH TIME ZONE format', function() { + check(update('user', { birthdate: new Date('1980-01-01T00:00:00Z') }), + "UPDATE \"user\" SET birthdate = TIMESTAMP WITH TIME ZONE '1980-01-01 00:00:00.000+00:00'"); + }); + it('should work with sql() SQL literal strings', function() { + check(select().from('person').where(sql('some_func($, $)', ['a', 'b'])), + "SELECT * FROM person WHERE some_func('a', 'b')"); + }); + it('should escape single quotes when sql() SQL literal strings are used', function() { + check(select().from('person').where(sql('some_func($)', ["Muad'Dib"])), + "SELECT * FROM person WHERE some_func('Muad''Dib')"); + }); + it('should support user-supplied conversions', function() { + const orig_bool = sql.conversions.Boolean; + sql.conversions.Boolean = function(bool) { return bool ? '1' : '0'; }; + const str = del('user').where('active', false).toString(); + sql.conversions.Boolean = orig_bool; + assert.equal(str, 'DELETE FROM "user" WHERE active = 0'); + }); + }); + }); + + it('should expand abbreviations in FROM and JOINs', function() { + check(select().from('usr').join('psn', { 'usr.psn_fk': 'psn.pk' }), + 'SELECT * FROM "user" usr INNER JOIN person psn ON usr.psn_fk = psn.pk'); + }); + + it('should expand left_tbl on all joins', function() { + const left_tbls: any[] = []; + const orig = sql.joinCriteria(); + sql.joinCriteria(function(left_tbl) { + left_tbls.push(left_tbl); + // return orig.apply(this, arguments); + return orig.apply(); + }); + + select().from('test').join('psn').join('usr').toString(); + sql.joinCriteria(orig); + assert(left_tbls.indexOf('person') > -1 && left_tbls.indexOf('psn') === -1, 'left_tbl is not expanded: [' + left_tbls.join(',') + ']'); + }); + + it('should support aliases', function() { + check(select().from('user usr2').join('address addr2'), + 'SELECT * FROM "user" usr2 INNER JOIN address addr2 ON usr2.addr_fk = addr2.pk'); + }); + + it('should auto-generate join criteria using supplied joinCriteria() function', function() { + check(select().from('usr').join('psn'), + 'SELECT * FROM "user" usr INNER JOIN person psn ON usr.psn_fk = psn.pk'); + }); + it('should auto-generate join criteria to multiple tables', function() { + check(select().from('usr').join('psn').join('addr'), + 'SELECT * FROM "user" usr ' + + 'INNER JOIN person psn ON usr.psn_fk = psn.pk ' + + 'INNER JOIN address addr ON psn.addr_fk = addr.pk'); + }); + it('should auto-generate join criteria from a single table to multiple tables', function() { + check(select().from('usr').join('psn', 'addr'), + 'SELECT * FROM "user" usr ' + + 'INNER JOIN person psn ON usr.psn_fk = psn.pk ' + + 'INNER JOIN address addr ON usr.addr_fk = addr.pk'); + }); + it('should handle unions', function() { + check(select().from('usr').where({ name: 'Roy' }) + .union(select().from('usr').where({ name: 'Moss' })) + .union(select().from('usr').where({ name: 'The elders of the internet' })), + "SELECT * FROM \"user\" usr WHERE name = 'Roy'" + + " UNION SELECT * FROM \"user\" usr WHERE name = 'Moss'" + + " UNION SELECT * FROM \"user\" usr WHERE name = 'The elders of the internet'"); + }); + it('should handle chained unions', function() { + check(select().from('usr').where({ name: 'Roy' }) + .union().select().from('usr').where({ name: 'blurns' }), + "SELECT * FROM \"user\" usr WHERE name = 'Roy'" + + " UNION SELECT * FROM \"user\" usr WHERE name = 'blurns'"); + }); + it('should handle chained unions with params', function() { + checkParams(select().from('usr').where({ name: 'Roy' }) + .union().select().from('usr').where({ name: 'Moss' }), + "SELECT * FROM \"user\" usr WHERE name = $1" + + " UNION SELECT * FROM \"user\" usr WHERE name = $2", ['Roy', 'Moss']); + }); + it('should handle unions with params', function() { + checkParams(select().from('usr').where({ name: 'Roy' }) + .union(select().from('usr').where({ name: 'Moss' })) + .union(select().from('usr').where({ name: 'The elders of the internet' })), + 'SELECT * FROM "user" usr WHERE name = $1' + + ' UNION SELECT * FROM "user" usr WHERE name = $2' + + ' UNION SELECT * FROM "user" usr WHERE name = $3', + ['Roy', 'Moss', 'The elders of the internet']); + }); + + describe('UPDATE statements', function() { + it('should handle .set() with (key, value)', function() { + check(update('user').set('name', 'Fred'), + "UPDATE \"user\" SET name = 'Fred'"); + }); + it('should handle .values() with an object literal', function() { + check(update('user').values({ name: 'Fred' }), + "UPDATE \"user\" SET name = 'Fred'"); + }); + it('should handle multiple .set()s with object literals', function() { + check(update('user').set({ name: 'Fred' }).set({ last_name: 'Flintstone' }), + "UPDATE \"user\" SET name = 'Fred', last_name = 'Flintstone'"); + }); + it('should handle multiple .values() with (key, value)', function() { + check(update('user').values('name', 'Fred').values('last_name', 'Flintstone'), + "UPDATE \"user\" SET name = 'Fred', last_name = 'Flintstone'"); + }); + it('should handle values argument', function() { + check(update('user', { name: 'Fred' }), + "UPDATE \"user\" SET name = 'Fred'"); + }); + }); + + describe('INSERT statements', function() { + it('should take an object of column/value pairs', function() { + check(insert('user', { id: 33, name: 'Fred' }), + "INSERT INTO \"user\" (id, name) VALUES (33, 'Fred')"); + }); + it('should insert a null for undefined', function() { + check(insert('user', { id: 33, name: undefined }), + "INSERT INTO \"user\" (id, name) VALUES (33, null)"); + }); + it('should insert a null for null', function() { + check(insert('user', { id: 33, name: null }), + "INSERT INTO \"user\" (id, name) VALUES (33, null)"); + }); + it('should take an array of columns & values', function() { + check(insert('user', ['id', 'name']).values([33, 'Fred']), + "INSERT INTO \"user\" (id, name) VALUES (33, 'Fred')"); + }); + it('should take multiple parameters of columns & values', function() { + check(insert('user', 'id', 'name').values(33, 'Fred'), + "INSERT INTO \"user\" (id, name) VALUES (33, 'Fred')"); + }); + it('should take an array of objects', function() { + check(insert('user', [{ id: 33, name: 'Fred' }, { id: 34, name: 'Wilma' }]), + "INSERT INTO \"user\" (id, name) VALUES (33, 'Fred'), (34, 'Wilma')"); + }); + + describe('.values()', function() { + it('should take an array of arrays', function() { + check(insert('user', ['id', 'name']).values([[33, 'Fred'], [34, 'Wilma']]), + "INSERT INTO \"user\" (id, name) VALUES (33, 'Fred'), (34, 'Wilma')"); + }); + }); + + describe('.into()', function() { + it('should take an object of column/value pairs', function() { + check(insert().into('user', { id: 33, name: 'Fred' }), + "INSERT INTO \"user\" (id, name) VALUES (33, 'Fred')"); + }); + it('should take an array of columns & values', function() { + check(insert().into('user', ['id', 'name']).values([33, 'Fred']), + "INSERT INTO \"user\" (id, name) VALUES (33, 'Fred')"); + }); + it('should take multiple parameters of columns & values', function() { + check(insert().into('user', 'id', 'name').values(33, 'Fred'), + "INSERT INTO \"user\" (id, name) VALUES (33, 'Fred')"); + }); + }); + }); + + describe('SELECT clause', function() { + it('should handle an array', function() { + check(select(['one', 'order']).from('user'), + 'SELECT one, "order" FROM "user"'); + }); + it('should handle multiple args', function() { + check(select('one', 'order').from('user'), + 'SELECT one, "order" FROM "user"'); + }); + it('should default to *', function() { + check(select().from('user'), + 'SELECT * FROM "user"'); + }); + it('should handle a comma-delimited str', function() { + check(select('one, order').from('user'), + 'SELECT one, "order" FROM "user"'); + }); + it('should handle being called multiple times', function() { + check(select('one, order').select(['two', 'desc']).select('three', 'four').from('user'), + 'SELECT one, "order", two, "desc", three, four FROM "user"'); + }); + it('should support DISTINCT', function() { + check(select('one, order').distinct('two, desc').from('user'), + 'SELECT DISTINCT one, "order", two, "desc" FROM "user"'); + }); + it('should support FOR UPDATE', function() { + check(select().from('user').forUpdate(), + 'SELECT * FROM "user" FOR UPDATE'); + }); + it('should support FOR UPDATE w/ col list', function() { + check(select().from('user').forUpdate().of('user'), + 'SELECT * FROM "user" FOR UPDATE OF "user"'); + }); + it('should support FOR UPDATE OF ... NO WAIT', function() { + check(select().from('user').forUpdate().of('user').noWait(), + 'SELECT * FROM "user" FOR UPDATE OF "user" NO WAIT'); + }); + }); + + describe('.from()', function() { + it('should handle an array', function() { + check(select().from(['one', 'two', 'usr']), + 'SELECT * FROM one, two, "user" usr'); + }); + it('should handle multiple args', function() { + check(select().from('one', 'two', 'usr'), + 'SELECT * FROM one, two, "user" usr'); + }); + it('should handle a comma-delimited string', function() { + check(select().from('one, two, usr'), + 'SELECT * FROM one, two, "user" usr'); + }); + it('should handle being called multiple times', function() { + check(select().from('one', 'usr').from(['two', 'psn']).from('three, addr'), + 'SELECT * FROM one, "user" usr, two, person psn, three, address addr'); + }); + it('should handle subquery object', function() { + check(select().from(select().from('user').as('subq')), + 'SELECT * FROM (SELECT * FROM "user") subq'); + }); + it('should handle subquery string', function() { + check(select().from('(SELECT * FROM "user") subq'), + 'SELECT * FROM (SELECT * FROM "user") subq'); + }); + }); + + describe('select().into() should insert into a new table', function() { + it('.into()', function() { + check(select().into('new_user').from('user'), + 'SELECT * INTO new_user FROM "user"'); + }); + it('.intoTable()', function() { + check(select().intoTable('new_user').from('user'), + 'SELECT * INTO new_user FROM "user"'); + }); + it('intoTemp()', function() { + check(select().intoTemp('new_user').from('user'), + 'SELECT * INTO TEMP new_user FROM "user"'); + }); + it('intoTempTable()', function() { + check(select().intoTempTable('new_user').from('user'), + 'SELECT * INTO TEMP new_user FROM "user"'); + }); + }); + + describe('insert().into() should insert into a preexisting table', function() { + it('insert().into().select()', function() { + check(insert().into('new_user', 'id', 'addr_id') + .select('id', 'addr_id').from('user'), + 'INSERT INTO new_user (id, addr_id) SELECT id, addr_id FROM "user"'); + }); + it('insert().into().select() with no columns', function() { + check(insert().into('new_user') + .select('id', 'addr_id').from('user'), + 'INSERT INTO new_user SELECT id, addr_id FROM "user"'); + }); + it('insert().select()', function() { + check(insert('new_user', 'id', 'addr_id') + .select('id', 'addr_id').from('user'), + 'INSERT INTO new_user (id, addr_id) SELECT id, addr_id FROM "user"'); + }); + it('insert().select() with params', function() { + check(insert('new_user', 'id', 'addr_id') + .select('id', 'addr_id').from('user').where({ active: true }).toParams().text, + 'INSERT INTO new_user (id, addr_id) SELECT id, addr_id FROM "user" WHERE active = $1'); + }); + }); + + describe('GROUP BY clause', function() { + it('should support single group by', function() { + check(select().from('user').groupBy('last_name'), + 'SELECT * FROM "user" GROUP BY last_name'); + }); + it('should support multiple groupBy() args w/ reserved words quoted', function() { + check(select().from('user').groupBy('last_name', 'order'), + 'SELECT * FROM "user" GROUP BY last_name, "order"'); + }); + it('should support .groupBy().groupBy()', function() { + check(select().from('user').groupBy('last_name').groupBy('order'), + 'SELECT * FROM "user" GROUP BY last_name, "order"'); + }); + it('should support an array', function() { + check(select().from('user').groupBy(['last_name', 'order']), + 'SELECT * FROM "user" GROUP BY last_name, "order"'); + }); + }); + + describe('.order() / .orderBy()', function() { + it('should support .orderBy(arg1, arg2)', function() { + check(select().from('user').orderBy('last_name', 'order'), + 'SELECT * FROM "user" ORDER BY last_name, "order"'); + }); + it('should support an array', function() { + check(select().from('user').orderBy(['last_name', 'order']), + 'SELECT * FROM "user" ORDER BY last_name, "order"'); + }); + it('should support being called multiple times', function() { + check(select().from('user').orderBy('last_name').orderBy('order'), + 'SELECT * FROM "user" ORDER BY last_name, "order"'); + }); + }); + + describe('joins', function() { + it('.join() should accept a comma-delimited string', function() { + check(select().from('usr').join('psn, addr'), + 'SELECT * FROM "user" usr ' + + 'INNER JOIN person psn ON usr.psn_fk = psn.pk ' + + 'INNER JOIN address addr ON usr.addr_fk = addr.pk'); + }); + it('.leftJoin() should generate a left join', function() { + check(select().from('usr').leftJoin('addr'), + 'SELECT * FROM "user" usr ' + + 'LEFT JOIN address addr ON usr.addr_fk = addr.pk'); + }); + it('.leftOuterJoin() should generate a left join', function() { + check(select().from('usr').leftOuterJoin('addr'), + 'SELECT * FROM "user" usr ' + + 'LEFT JOIN address addr ON usr.addr_fk = addr.pk'); + }); + it('.rightJoin() should generate a right join', function() { + check(select().from('usr').rightJoin('addr'), + 'SELECT * FROM "user" usr ' + + 'RIGHT JOIN address addr ON usr.addr_fk = addr.pk'); + }); + it('.rightOuterJoin() should generate a right join', function() { + check(select().from('usr').rightOuterJoin('addr'), + 'SELECT * FROM "user" usr ' + + 'RIGHT JOIN address addr ON usr.addr_fk = addr.pk'); + }); + it('.fullJoin() should generate a full join', function() { + check(select().from('usr').fullJoin('addr'), + 'SELECT * FROM "user" usr ' + + 'FULL JOIN address addr ON usr.addr_fk = addr.pk'); + }); + it('.fullOuterJoin() should generate a full join', function() { + check(select().from('usr').fullOuterJoin('addr'), + 'SELECT * FROM "user" usr ' + + 'FULL JOIN address addr ON usr.addr_fk = addr.pk'); + }); + it('.crossJoin() should generate a cross join', function() { + check(select().from('usr').crossJoin('addr'), + 'SELECT * FROM "user" usr ' + + 'CROSS JOIN address addr'); + }); + it('join() should accept an expression for the on argument', function() { + check(select().from('usr').join('addr', eq('usr.addr_id', sql('addr.id'))), + 'SELECT * FROM "user" usr INNER JOIN address addr ON usr.addr_id = addr.id'); + }); + it('join() should accept an array for the on argument (for JOIN USING)', function() { + check(select().from('usr').join('addr', ['addr_id', 'country_id']), + 'SELECT * FROM "user" usr INNER JOIN address addr USING (addr_id, country_id)'); + }); + }); + + describe('.on()', function() { + it('should accept an object literal', function() { + check(select().from('usr').join('addr').on({ 'usr.addr_id': 'addr.id' }), + 'SELECT * FROM "user" usr ' + + 'INNER JOIN address addr ON usr.addr_id = addr.id'); + }); + it('should accept a (key, value) pair', function() { + check(select().from('usr').join('addr').on('usr.addr_id', 'addr.id'), + 'SELECT * FROM "user" usr ' + + 'INNER JOIN address addr ON usr.addr_id = addr.id'); + }); + it('can be called multiple times', function() { + check(select().from('usr', 'psn').join('addr').on({ 'usr.addr_id': 'addr.id' }) + .on('psn.addr_id', 'addr.id'), + 'SELECT * FROM "user" usr, person psn ' + + 'INNER JOIN address addr ON usr.addr_id = addr.id AND psn.addr_id = addr.id'); + }); + it('can be called multiple times w/ an object literal', function() { + check(select().from('usr', 'psn').join('addr').on({ 'usr.addr_id': 'addr.id' }) + .on({ 'psn.addr_id': 'addr.id' }), + 'SELECT * FROM "user" usr, person psn ' + + 'INNER JOIN address addr ON usr.addr_id = addr.id AND psn.addr_id = addr.id'); + }); + it('should accept an expression', function() { + check(select().from('usr').join('addr').on(eq('usr.addr_id', sql('addr.id'))), + 'SELECT * FROM "user" usr INNER JOIN address addr ON usr.addr_id = addr.id'); + }); + it('should not proceed if .using() has already been used', function() { + assert.throws(function() { + select().from('t1').join('t2').using('id').on({ 't1.t2_id': 't2.id' }); + }); + }); + }); + + describe('.using()', function() { + it('should accept a comma-delimited string', function() { + check(select().from('usr').join('addr').using('addr_id, contrived_id'), + 'SELECT * FROM "user" usr ' + + 'INNER JOIN address addr USING (addr_id, contrived_id)'); + }); + it('should accept multiple arguments', function() { + check(select().from('usr').join('addr').using('addr_id', 'contrived_id'), + 'SELECT * FROM "user" usr ' + + 'INNER JOIN address addr USING (addr_id, contrived_id)'); + }); + it('should accept an array literal', function() { + check(select().from('usr').join('addr').using(['addr_id', 'contrived_id']), + 'SELECT * FROM "user" usr ' + + 'INNER JOIN address addr USING (addr_id, contrived_id)'); + }); + it('should not proceed if .on() has already been used', function() { + assert.throws(function() { + select().from('t1').join('t2').on({ 't1.t2_id': 't2.id' }).using('id'); + }); + }); + }); + + describe('natural joins', function() { + it('.naturalJoin() should accept a comma-delimited string', function() { + check(select().from('usr').naturalJoin('psn, addr'), + 'SELECT * FROM "user" usr ' + + 'NATURAL INNER JOIN person psn ' + + 'NATURAL INNER JOIN address addr'); + }); + it('.naturalLeftJoin() should generate a natural left join', function() { + check(select().from('usr').naturalLeftJoin('addr'), + 'SELECT * FROM "user" usr ' + + 'NATURAL LEFT JOIN address addr'); + }); + it('.naturalLeftOuterJoin() should generate a natural left join', function() { + check(select().from('usr').naturalLeftOuterJoin('addr'), + 'SELECT * FROM "user" usr ' + + 'NATURAL LEFT JOIN address addr'); + }); + it('.naturalRightJoin() should generate a natural right join', function() { + check(select().from('usr').naturalRightJoin('addr'), + 'SELECT * FROM "user" usr ' + + 'NATURAL RIGHT JOIN address addr'); + }); + it('.naturalRightOuterJoin() should generate a natural right join', function() { + check(select().from('usr').naturalRightOuterJoin('addr'), + 'SELECT * FROM "user" usr ' + + 'NATURAL RIGHT JOIN address addr'); + }); + it('.naturalFullJoin() should generate a natural full join', function() { + check(select().from('usr').naturalFullJoin('addr'), + 'SELECT * FROM "user" usr ' + + 'NATURAL FULL JOIN address addr'); + }); + it('.naturalFullOuterJoin() should generate a natural full join', function() { + check(select().from('usr').naturalFullOuterJoin('addr'), + 'SELECT * FROM "user" usr ' + + 'NATURAL FULL JOIN address addr'); + }); + }); + + describe('WHERE clauses', function() { + it('should AND multiple where() criteria by default', function() { + check(select().from('user').where({ + first_name: 'Fred', + last_name: 'Flintstone' + }), + "SELECT * FROM \"user\" WHERE first_name = 'Fred' AND last_name = 'Flintstone'"); + }); + it('should AND multiple where()s by default', function() { + check(select().from('user').where({ first_name: 'Fred' }) + .where({ last_name: 'Flintstone' }), + "SELECT * FROM \"user\" WHERE first_name = 'Fred' AND last_name = 'Flintstone'"); + }); + it('should handle explicit .and() with (key, value) args', function() { + check(select().from('user').where('first_name', 'Fred') + .and('last_name', 'Flintstone'), + "SELECT * FROM \"user\" WHERE first_name = 'Fred' AND last_name = 'Flintstone'"); + }); + it('should handle nested and(or())', function() { + check(select().from('user').where(and({ last_name: 'Flintstone' }, or({ first_name: 'Fred' }, { first_name: 'Wilma' }))), + "SELECT * FROM \"user\" WHERE last_name = 'Flintstone' AND (first_name = 'Fred' OR first_name = 'Wilma')"); + }); + it('should handle or([...])', function() { + check(select().from('table').where({ this: 'test' }).and(or(['test1', 'test2'].map(function(val) { return eq('that', val); }))), + "SELECT * FROM \"table\" WHERE this = 'test' AND (that = 'test1' OR that = 'test2')"); + }); + it('and() should be implicit', function() { + check(select().from('user').where({ last_name: 'Flintstone' }, or({ first_name: 'Fred' }, { first_name: 'Wilma' })), + "SELECT * FROM \"user\" WHERE last_name = 'Flintstone' AND (first_name = 'Fred' OR first_name = 'Wilma')"); + }); + it('should handle like()', function() { + check(select().from('user').where(like('last_name', 'Flint%')), + "SELECT * FROM \"user\" WHERE last_name LIKE 'Flint%'"); + }); + it('should accept a 3rd escape param to like()', function() { + check(select().from('user').where(like('percent', '100\\%', '\\')), + "SELECT * FROM \"user\" WHERE percent LIKE '100\\%' ESCAPE '\\'"); + }); + it('should handle not()', function() { + check(select().from('user').where(not({ first_name: 'Fred' })), + "SELECT * FROM \"user\" WHERE NOT first_name = 'Fred'"); + }); + it('should handle in()', function() { + check(select().from('user').where($in('first_name', ['Fred', 'Wilma'])), + "SELECT * FROM \"user\" WHERE first_name IN ('Fred', 'Wilma')"); + }); + it('should handle .in() with multiple args', function() { + check(select().from('user').where($in('name', 'Jimmy', 'Owen')), + "SELECT * FROM \"user\" WHERE name IN ('Jimmy', 'Owen')"); + }); + it('should handle .in() with a subquery', function() { + check(select().from('user').where($in('addr_id', select('id').from('address'))), + 'SELECT * FROM "user" WHERE addr_id IN (SELECT id FROM address)'); + }); + it('should handle exists() with a subquery', function() { + check(select().from('user').where(exists(select().from('address').where({ 'user.addr_id': sql('address.id') }))), + 'SELECT * FROM "user" WHERE EXISTS (SELECT * FROM address WHERE "user".addr_id = address.id)'); + }); + it('should handle exists() with a subquery, parameterized', function() { + checkParams(select().from('user').where('active', true).where(exists(select().from('address').where({ 'user.addr_id': 37 }))), + 'SELECT * FROM "user" WHERE active = $1 AND EXISTS (SELECT * FROM address WHERE "user".addr_id = $2)', + [true, 37]); + }); + it('should handle isNull()', function() { + check(select().from('user').where(isNull('first_name')), + 'SELECT * FROM "user" WHERE first_name IS NULL'); + }); + it('should handle isNotNull()', function() { + check(select().from('user').where(isNotNull('first_name')), + 'SELECT * FROM "user" WHERE first_name IS NOT NULL'); + }); + it('should automatically generate IS NULL', function() { + check(select().from('user').where({ first_name: null }), + 'SELECT * FROM "user" WHERE first_name IS NULL'); + }); + it('should handle explicit equal()', function() { + check(select().from('user').where(equal('first_name', 'Fred')), + "SELECT * FROM \"user\" WHERE first_name = 'Fred'"); + }); + it('should automatically generate IS NOT NULL', function() { + check(select().from('user').where(notEq('first_name', null)), + "SELECT * FROM \"user\" WHERE first_name IS NOT NULL"); + }); + it('should handle lt()', function() { + check(select().from('user').where(lt('order', 5)), + 'SELECT * FROM "user" WHERE "order" < 5'); + }); + it('should handle lte()', function() { + check(select().from('user').where(lte('order', 5)), + 'SELECT * FROM "user" WHERE "order" <= 5'); + }); + it('should handle gt()', function() { + check(select().from('user').where(gt('order', 5)), + 'SELECT * FROM "user" WHERE "order" > 5'); + }); + it('should handle gte()', function() { + check(select().from('user').where(gte('order', 5)), + 'SELECT * FROM "user" WHERE "order" >= 5'); + }); + it('should handle between()', function() { + check(select().from('user').where(between('name', 'Frank', 'Fred')), + "SELECT * FROM \"user\" WHERE name BETWEEN 'Frank' AND 'Fred'"); + }); + it('should do nothing for null, undefined, {}', function() { + check(select().from('user').where(), + "SELECT * FROM \"user\""); + check(select().from('user').where(null), + "SELECT * FROM \"user\""); + check(select().from('user').where({}), + "SELECT * FROM \"user\""); + check(select().from('user').where(undefined), + "SELECT * FROM \"user\""); + }); + it('should not ignore the 2nd arg if the first is {}', function() { + check(select().from('user').where({}, { name: 'Fred' }), + "SELECT * FROM \"user\" WHERE name = 'Fred'"); + }); + + describe('customizations', function() { + it('should support customized criteria', function() { + check(select().from('table').where(arraysToOrs({ this: 'test', that: ['test1', 'test2'] })), + "SELECT * FROM \"table\" WHERE this = 'test' AND (that = 'test1' OR that = 'test2')"); + }); + + it('should support changing the default array handling', function() { + const proto = sql.select.prototype; + const orig = proto.where; + proto.where = function(criteria) { + // return orig.call(this, arraysToOrs(criteria)); + return orig.call() + }; + + check(select().from('table').where({ this: 'test', that: ['test1', 'test2'] }), + "SELECT * FROM \"table\" WHERE this = 'test' AND (that = 'test1' OR that = 'test2')"); + + proto.where = orig; + }); + + function arraysToOrs(criteria) { + const where = and(); + for (const col in criteria) { + const val = criteria[col]; + let expr; + if (Array.isArray(val)) + expr = or(val.map(function(val) { return eq(col, val); })); + else + expr = eq(col, val); + where.expressions.push(expr); + } + return where; + } + + it('should support converting expressions to string', function() { + check(and({ this: 'test' }, $in('that', ['v1', 'v2'])), + "(this = 'test' AND that IN ('v1', 'v2'))"); + }); + + it('should support raw sql blocks in expressions', function() { + check(and({ this: 'test' }, sql('field is null')), + "(this = 'test' AND field is null)"); + }); + + it('should support converting ?-parameterized sql blocks to string', function() { + check(sql('field = ?', 123).toString({ placeholder: '?' }), + "field = 123"); + }); + + it('should support converting default-parameterized sql blocks to string', function() { + check(sql('field = $1', 123), + "field = 123"); + }); + }); + }); + + describe('should quote column names with capitals in them', function() { + it('in SELECT', function() { + check(select('Name').from('user'), + 'SELECT "Name" FROM "user"'); + }); + it('in SELECT, after tbl prefix, before AS suffix', function() { + check(select('user.Name AS UserName').from('user'), + 'SELECT "user"."Name" AS UserName FROM "user"'); + }); + it('in SELECT, after tbl prefix, with the optional "AS" omitted', function() { + check(select('user.Name UserName').from('user'), + 'SELECT "user"."Name" UserName FROM "user"'); + }); + it('in SELECT, with the optional "AS" omitted', function() { + check(select('Name UserName').from('user'), + 'SELECT "Name" UserName FROM "user"'); + }); + }); + + describe('should quote table names that are reserved words or have capitals', function() { + it('in joins', function() { + check(select('Name UserName').from('person').join('user', { 'person.usr_id': 'user.id' }), + 'SELECT "Name" UserName FROM person INNER JOIN "user" ON person.usr_id = "user".id'); + }); + it('unless they are wrapped in sql() literals (to query reserved system tables)', function() { + check(select(sql('user.id')).from(sql('user')), + 'SELECT user.id FROM user'); + }); + }); + + describe('should quote reserved words in column names', function() { + it('in ORDER BY', function() { + check(select().from('usr').orderBy('order'), + 'SELECT * FROM "user" usr ORDER BY "order"'); + }); + it('in SELECT', function() { + check(select('desc').from('usr'), + 'SELECT "desc" FROM "user" usr'); + }); + it('in JOIN ONs', function() { + check(select().from('usr').join('psn', { 'usr.order': 'psn.order' }), + 'SELECT * FROM "user" usr INNER JOIN person psn ON usr."order" = psn."order"'); + }); + it('in JOIN USINGs', function() { + check(select().from('usr').join('psn').using('order'), + 'SELECT * FROM "user" usr INNER JOIN person psn USING ("order")'); + }); + it('in INSERT', function() { + check(insert('user').values({ order: 1 }), + 'INSERT INTO "user" ("order") VALUES (1)'); + }); + it('in alternative insert() API', function() { + check(insert('user', 'order').values(1), + 'INSERT INTO "user" ("order") VALUES (1)'); + }); + it('with a db and table prefix and a suffix', function() { + check(select('db.person.desc AS psn_desc').from('person'), + 'SELECT db.person."desc" AS psn_desc FROM person'); + }); + it('should quote sqlite reserved words', function() { + check(select('action').from('user'), + 'SELECT "action" FROM "user"'); + }); + it('should not quote reserved words in SELECT expressions', function() { + check(select("CASE WHEN name = 'Fred' THEN 1 ELSE 0 AS security_level").from('user'), + "SELECT CASE WHEN name = 'Fred' THEN 1 ELSE 0 AS security_level FROM \"user\""); + }); + }); + + describe('subqueries in <, >, etc', function() { + it('should support a subquery in >', function() { + const count_addrs_for_usr = select('count(*)').from('address').where({ 'user.addr_id': sql('address.id') }); + check(select().from('user').where(gt(count_addrs_for_usr, 5)), + 'SELECT * FROM "user" WHERE (SELECT count(*) FROM address WHERE "user".addr_id = address.id) > 5'); + }); + it('should support a subquery in <=', function() { + const count_addrs_for_usr = select('count(*)').from('address').where({ 'user.addr_id': sql('address.id') }); + check(select().from('user').where(lte(count_addrs_for_usr, 5)), + 'SELECT * FROM "user" WHERE (SELECT count(*) FROM address WHERE "user".addr_id = address.id) <= 5'); + }); + it('should support = ANY (subquery) quantifier', function() { + check(select().from('user').where(eqAny('user.id', select('user_id').from('address'))), + 'SELECT * FROM "user" WHERE "user".id = ANY (SELECT user_id FROM address)'); + }); + it('should support <> ANY (subquery) quantifier', function() { + check(select().from('user').where(notEqAny('user.id', select('user_id').from('address'))), + 'SELECT * FROM "user" WHERE "user".id <> ANY (SELECT user_id FROM address)'); + }); + }); + + describe('deep Statement.clone()', function() { + it('should deep clone WHERE expressions', function() { + const sel = select().from('user').where({ first_name: 'Fred' }); + sel.clone().where({ last_name: 'Flintstone' }); + check(sel, "SELECT * FROM \"user\" WHERE first_name = 'Fred'"); + }); + it('should deep clone .order()', function() { + const sel = select().from('user').order('name'); + sel.clone().order('last_name'); + check(sel, 'SELECT * FROM "user" ORDER BY name'); + }); + it('should deep clone .join()', function() { + const sel = select().from('user').join('addr'); + sel.clone().join('psn'); + check(sel, 'SELECT * FROM "user" INNER JOIN address addr ON "user".addr_fk = addr.pk'); + }); + it('should clone values', function() { + const ins = insert('user', { first_name: 'Fred' }); + ins.clone().values({ last_name: 'Flintstone' }); + check(ins, "INSERT INTO \"user\" (first_name) VALUES ('Fred')"); + }); + it('should clone $in subqueries', function() { + const sel = select().from('user').where($in('first_name', select('first_name').from('user'))); + sel.clone().where('last_name', 'Flintstone'); + check(sel, "SELECT * FROM \"user\" WHERE first_name IN (SELECT first_name FROM \"user\")"); + }); + }); + + describe('the AS keyword', function() { + it('should not generate invalid SQL', function() { + check(select().from('user AS usr').join('addr'), + 'SELECT * FROM "user" AS usr INNER JOIN address addr ON usr.addr_fk = addr.pk'); + }); + }); + + describe('delete()', function() { + it('should generate a DELETE statement', function() { + check(del('user'), + 'DELETE FROM "user"'); + }); + it('should support .from()', function() { + check(del().from('user'), + 'DELETE FROM "user"'); + }); + it('should generate a DELETE statement with a WHERE clause', function() { + check(del('user').where('first_name', 'Fred'), + "DELETE FROM \"user\" WHERE first_name = 'Fred'"); + }); + }); + + describe('_extension()', function() { + it('should shield base', function() { + const ext = sql._extension(); + + ext.prop = 1; + assert.equal(sql.prop, undefined); + + ext.select.prop = 1; + // assert.equal(sql.select.returning, undefined); + + assert(ext.select.prototype.clauses !== sql.select.prototype.clauses); + }); + }); +}); + +function describe(...param) { } +function it(...param) { } +function check(...param) { } +function checkParams(...param) { } diff --git a/types/sql-bricks/tsconfig.json b/types/sql-bricks/tsconfig.json new file mode 100644 index 0000000000..3c5db90a4f --- /dev/null +++ b/types/sql-bricks/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "module": "commonjs", + "lib": [ + "es6" + ], + "noImplicitAny": false, + "noImplicitThis": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "baseUrl": "../", + "typeRoots": [ + "../" + ], + "types": [], + "noEmit": true, + "forceConsistentCasingInFileNames": true + }, + "files": [ + "index.d.ts", + "sql-bricks-tests.ts" + ] +} \ No newline at end of file diff --git a/types/sql-bricks/tslint.json b/types/sql-bricks/tslint.json new file mode 100644 index 0000000000..1e7260a20f --- /dev/null +++ b/types/sql-bricks/tslint.json @@ -0,0 +1,10 @@ +{ + "extends": "dtslint/dt.json", + "rules": { + "only-arrow-functions": false, + "prefer-template": false, + "prefer-conditional-expression": false, + "one-variable-per-declaration": false, + "semicolon":false + } +} \ No newline at end of file