diff --git a/1kyu/functional-sql/description.md b/1kyu/functional-sql/description.md new file mode 100644 index 0000000..13a3915 --- /dev/null +++ b/1kyu/functional-sql/description.md @@ -0,0 +1,261 @@ +In this Kata we are going to mimic the SQL syntax with JavaScript (or TypeScript). + +To do this, you must implement the query() function. This function returns and object with the next methods: + +{ + select: ..., + from: ..., + where: ..., + orderBy: ..., + groupBy: ..., + having: ..., + execute: ... +} +The methods are chainable and the query is executed by calling the execute() method. + +⚠️ Note: The order of appearance of a clause in a query doesn't matter. However, when it comes time for you to run the query, you MUST execute the clauses in this logical order: from first, then where, then groupBy, then having, then select and finally orderBy. + +SELECT * FROM numbers +var numbers = [1, 2, 3]; +query().select().from(numbers).execute(); //[1, 2, 3] + +//clauses order does not matter +query().from(numbers).select().execute(); //[1, 2, 3] +Of course, you can make queries over object collections: + +var persons = [ + {name: 'Peter', profession: 'teacher', age: 20, maritalStatus: 'married'}, + {name: 'Michael', profession: 'teacher', age: 50, maritalStatus: 'single'}, + {name: 'Peter', profession: 'teacher', age: 20, maritalStatus: 'married'}, + {name: 'Anna', profession: 'scientific', age: 20, maritalStatus: 'married'}, + {name: 'Rose', profession: 'scientific', age: 50, maritalStatus: 'married'}, + {name: 'Anna', profession: 'scientific', age: 20, maritalStatus: 'single'}, + {name: 'Anna', profession: 'politician', age: 50, maritalStatus: 'married'} +]; + +//SELECT * FROM persons +query().select().from(persons).execute(); // [{name: 'Peter',...}, {name: 'Michael', ...}] +You can select some fields: + +function profession(person) { + return person.profession; +} + +//SELECT profession FROM persons +query().select(profession).from(persons).execute(); //select receives a function that will be called with the values of the array //["teacher","teacher","teacher","scientific","scientific","scientific","politician"] +If you repeat a SQL clause (except where() or having()), an exception will be thrown + +query().select().select().execute(); //Error('Duplicate SELECT'); +query().select().from([]).select().execute(); //Error('Duplicate SELECT'); +query().select().from([]).from([]).execute(); //Error('Duplicate FROM'); +query().select().from([]).where().where() //This is an AND filter (see below) +You can omit any SQLclause: + +var numbers = [1, 2, 3]; + +query().select().execute(); //[] +query().from(numbers).execute(); // [1, 2, 3] +query().execute(); // [] +You can apply filters: + +function isTeacher(person) { + return person.profession === 'teacher'; +} + +//SELECT profession FROM persons WHERE profession="teacher" +query().select(profession).from(persons).where(isTeacher).execute(); //["teacher", "teacher", "teacher"] + +//SELECT * FROM persons WHERE profession="teacher" +query().select().from(persons).where(isTeacher).execute(); //[{person: 'Peter', profession: 'teacher', ...}, ...] + +function name(person) { + return person.name; +} + +//SELECT name FROM persons WHERE profession="teacher" +query().select(name).from(persons).where(isTeacher).execute();//["Peter", "Michael", "Peter"] +Agrupations are also possible: + +//SELECT * FROM persons GROUP BY profession <- Bad in SQL but possible in this kata +query().select().from(persons).groupBy(profession).execute(); +[ + ["teacher", + [ + { + name: "Peter", + profession: "teacher" + ... + }, + { + name: "Michael", + profession: "teacher" + ... + } + ] + ], + ["scientific", + [ + { + name: "Anna", + profession: "scientific" + }, + ... + ] + ] + ... +] +You can mix where() with groupBy(): + +//SELECT * FROM persons WHERE profession='teacher' GROUP BY profession +query().select().from(persons).where(isTeacher).groupBy(profession).execute(); +Or with select(): + +function professionGroup(group) { + return group[0]; +} + +//SELECT profession FROM persons GROUP BY profession +query().select(professionGroup).from(persons).groupBy(profession).execute(); //["teacher","scientific","politician"] +Another example: + +function isEven(number) { + return number % 2 === 0; +} + +function parity(number) { + return isEven(number) ? 'even' : 'odd'; +} + +var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + +//SELECT * FROM numbers +query().select().from(numbers).execute(); //[1, 2, 3, 4, 5, 6, 7, 8, 9] + +//SELECT * FROM numbers GROUP BY parity +query().select().from(numbers).groupBy(parity).execute(); //[["odd",[1,3,5,7,9]],["even",[2,4,6,8]]] +Multilevel grouping: + +function isPrime(number) { + if (number < 2) { + return false; + } + var divisor = 2; + for(; number % divisor !== 0; divisor++); + return divisor === number; +} + +function prime(number) { + return isPrime(number) ? 'prime' : 'divisible'; +} + +//SELECT * FROM numbers GROUP BY parity, isPrime +query().select().from(numbers).groupBy(parity, prime).execute(); // [["odd",[["divisible",[1,9]],["prime",[3,5,7]]]],["even",[["prime",[2]],["divisible",[4,6,8]]]]] +orderBy should be called after groupBy, so the values passed to orderBy function are the grouped results by the groupBy function. + +Filter groups with having(): + +function odd(group) { + return group[0] === 'odd'; +} + +//SELECT * FROM numbers GROUP BY parity HAVING odd(number) = true <- I know, this is not a valid SQL statement, but you can understand what I am doing +query().select().from(numbers).groupBy(parity).having(odd).execute(); //[["odd",[1,3,5,7,9]]] +You can order the results: + +var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + +function descendentCompare(number1, number2) { + return number2 - number1; +} + +//SELECT * FROM numbers ORDER BY value DESC + query().select().from(numbers).orderBy(descendentCompare).execute(); //[9,8,7,6,5,4,3,2,1] +from() supports multiple collections: + +var teachers = [ + { + teacherId: '1', + teacherName: 'Peter' + }, + { + teacherId: '2', + teacherName: 'Anna' + } +]; + + +var students = [ + { + studentName: 'Michael', + tutor: '1' + }, + { + studentName: 'Rose', + tutor: '2' + } +]; + +function teacherJoin(join) { + return join[0].teacherId === join[1].tutor; +} + +function student(join) { + return {studentName: join[1].studentName, teacherName: join[0].teacherName}; +} + +//SELECT studentName, teacherName FROM teachers, students WHERE teachers.teacherId = students.tutor +query().select(student).from(teachers, students).where(teacherJoin).execute(); //[{"studentName":"Michael","teacherName":"Peter"},{"studentName":"Rose","teacherName":"Anna"}] +Finally, where() and having() admit multiple AND and OR filters: + +function tutor1(join) { + return join[1].tutor === "1"; +} + +//SELECT studentName, teacherName FROM teachers, students WHERE teachers.teacherId = students.tutor AND tutor = 1 +query().select(student).from(teachers, students).where(teacherJoin).where(tutor1).execute(); //[{"studentName":"Michael","teacherName":"Peter"}] <- AND filter + +var numbers = [1, 2, 3, 4, 5, 7]; + +function lessThan3(number) { + return number < 3; +} + +function greaterThan4(number) { + return number > 4; +} + +//SELECT * FROM number WHERE number < 3 OR number > 4 +query().select().from(numbers).where(lessThan3, greaterThan4).execute(); //[1, 2, 5, 7] <- OR filter + +var numbers = [1, 2, 1, 3, 5, 6, 1, 2, 5, 6]; + +function greatThan1(group) { + return group[1].length > 1; +} + +function isPair(group) { + return group[0] % 2 === 0; +} + +function id(value) { + return value; +} + +function frequency(group) { + return { value: group[0], frequency: group[1].length }; +} + +//SELECT number, count(number) FROM numbers GROUP BY number HAVING count(number) > 1 AND isPair(number) +query().select(frequency).from(numbers).groupBy(id).having(greatThan1).having(isPair).execute(); // [{"value":2,"frequency":2},{"value":6,"frequency":2}]) +Requirements Recap +Clause ⚠️ Must be executed... Arg(s) Count Arg Type Repeatable? +from First 1 or More (=> cartesian product of specified tables) Table(s) (i.e., arrays) No +where Second 1 or More (=> to be logically OR'd) Functions Yes (each repetition is a logical AND) +groupBy Third 1 or More (=> groups by the 1st fn, then, within each subgroup, groups by the 2nd fn, ...) Functions No +having Fourth 1 or More (=> to be logically OR'd) Functions Yes (each repetition is a logical AND) +select Fifth 0 (selects everything) or 1 Function No +orderBy Last 1 Function No +execute - None (just executes the entire query) - - +If any of the unrepeatable clauses are repeated in the query, your solution MUST raise an Error object with the error message "duplicate " followed by the name of the duplicated clause. If the clause is multi-word, merge it into one (ex: groupby). + +For example, if the groupBy clause is duplicated, you should throw an Error with the exact string message "duplicate groupby" (capitalization doesn't matter). \ No newline at end of file diff --git a/1kyu/functional-sql/solution.ts b/1kyu/functional-sql/solution.ts new file mode 100644 index 0000000..2ebed2f --- /dev/null +++ b/1kyu/functional-sql/solution.ts @@ -0,0 +1,232 @@ +type GroupedData = Map; + +class Query { + private data: any[]; + private selector: (obj: any) => any = (obj) => obj; + private filterGroups: ((obj: any) => boolean)[][] = []; + private groupBySelectors: ((obj: any) => any)[] = []; + private groupFilterGroups: ((obj: any) => boolean)[][] = []; + private orderFunction: (obj1: any, obj2: any) => number = (obj1, obj2) => 0; + + constructor() { + this.data = []; + } + + // If you repeat a SQL clause (except where() or having()), an exception will be thrown + private _selected = false; + private _fromed = false; + private _grouped = false; + private _ordered = false; + + select(newSelector?: (obj: any) => any) { + if (this._selected) { + throw new Error('Duplicate SELECT'); + } + + if (newSelector) { + this.selector = newSelector; + }; + + this._selected = true; + return this; + } + + from(...dataRows: any[][]) { + if (this._fromed) { + throw new Error('Duplicate FROM'); + } + + this._fromed = true; + + // cartesian product of all data arrays + dataRows.forEach((dataRow) => { + if (this.data.length === 0) { + this.data = dataRow; + return; + } + + const newData: any[] = []; + this.data.forEach((obj) => { + dataRow.forEach((newObj) => { + newData.push([obj, newObj]); + }); + }) + + this.data = newData; + }) + + return this; + } + + where(...newFilters: ((obj: any) => boolean)[]) { + this.filterGroups.push(newFilters); + return this; + } + + groupBy(...newGroupBySelectors: ((obj: any) => any)[]) { + if (this._grouped) { + throw new Error('Duplicate GROUPBY'); + } + + this._grouped = true; + this.groupBySelectors = newGroupBySelectors; + return this; + } + + having(...newGroupFilters: ((obj: any) => boolean)[]) { + this.groupFilterGroups.push(newGroupFilters); + return this; + } + + orderBy(newOrderFunction: (obj1: any, obj2: any) => number) { + if (this._ordered) { + throw new Error('Duplicate ORDERBY'); + } + + this._ordered = true; + this.orderFunction = newOrderFunction; + return this; + } + + execute() { + // filter + this.data = this.executeFilter(); + + // group by + const groupedMap = this.executeGroupBy(); + + // filter groups + this.executeHaving(groupedMap); + + const selected = this.executeSelect(groupedMap); + + const result = this.executeOrderBy(selected); + return result; + } + + private executeFilter() { + // this.filterGroups is a list of groups of filters + // filters in the same group are ORed + // groups are ANDed + let filteredData = this.data; + this.filterGroups.forEach((filterGroup) => { + filteredData = filteredData.filter((obj) => { + return filterGroup.some((filter) => filter(obj)); + }); + }) + return filteredData; + } + + private groupOnce(data: any[], groupBySelector: (obj: any) => any) { + const groupedData = new Map(); + data.forEach((obj) => { + const key = groupBySelector(obj); + if (!groupedData.has(key)) { + groupedData.set(key, []); + } + (groupedData.get(key) as any[]).push(obj); + }); + + return groupedData; + } + + private groupRecursively(data: any[], groupBySelectors: ((obj: any) => any)[]) { + + const [groupBySelector, ...restGroupBySelectors] = groupBySelectors; + const groupedData = this.groupOnce(data, groupBySelector); + if (restGroupBySelectors.length === 0) { + return groupedData; + } + + Array.from(groupedData.keys()).forEach((key) => { + const group = groupedData.get(key) as any[]; + groupedData.set(key, this.groupRecursively(group, restGroupBySelectors) as Map); + }); + + return groupedData; + } + + private executeGroupBy(filteredData: any[] = this.data) { + if (this.groupBySelectors.length === 0) { + return new Map([['', filteredData]]); + } + + const groupedData = this.groupRecursively(filteredData, this.groupBySelectors); + return groupedData; + } + + private executeHaving(groupedData: GroupedData) { + if (this.groupFilterGroups.length === 0) return groupedData; + // filter groups + // groupFilterGroups is a list of groups of filters + // filters in the same group are ORed + // groups are ANDed + + // first flatten the map to arrays + let flattened: { flatGroup: any[], key: string }[] = []; + Array.from(groupedData.keys()).forEach((key) => { + const group = groupedData.get(key) as GroupedData | any[]; + const flattenedGroup = Array.isArray(group) ? + group + : this.flattenMapToArrays(group as GroupedData, this.selector); + flattened.push({ flatGroup: [key, flattenedGroup], key }); + }); + + this.groupFilterGroups.forEach((groupFilters) => { + flattened = flattened.filter((group) => { + const toKeep = groupFilters.some((filter) => filter(group.flatGroup)); + if (!toKeep) groupedData.delete(group.key); + return toKeep; + }) + }); + + return groupedData; + } + + private flattenMapToArrays(groupedData: GroupedData, selector?: (obj: any) => any) { + // flatten maps to arrays + const result: any[] = []; + Array.from(groupedData.keys()).forEach((key) => { + const group = groupedData.get(key) as GroupedData | any[]; + const flattened = Array.isArray(group) ? + selector ? (group as any[]).map(selector) + : group as any[] + : this.flattenMapToArrays(group as GroupedData, selector) + result.push([key, flattened]); + }); + + return result; + } + + private executeSelect(groupedData: GroupedData) { + const keys = Array.from(groupedData.keys()); + if (keys.length === 1 && keys[0] === '') { + return (groupedData.get('') as any[])?.map(this.selector); + } + + return Array.from(keys).map((key) => { + const group = groupedData.get(key) as GroupedData | any[]; + if (this.groupBySelectors.length === 0) { + // no group by was done, + // selector selects from objects + return [key, (group as any[]).map(this.selector)]; + } + + const flattenedGroup = Array.isArray(group) ? + group + : this.flattenMapToArrays(group as GroupedData, this.selector); + // selector selects from groups + return this.selector([key, flattenedGroup]); + }); + } + + private executeOrderBy(selected: any[]) { + return selected.sort(this.orderFunction); + } +} + + +export function query() { + const q = new Query(); + return q; +}; diff --git a/1kyu/functional-sql/tests.ts b/1kyu/functional-sql/tests.ts new file mode 100644 index 0000000..caffa2e --- /dev/null +++ b/1kyu/functional-sql/tests.ts @@ -0,0 +1,632 @@ +import { + query +} from './solution'; +import { + expect +} from "chai"; + +describe("SQL tests", function() { + it("Basic SELECT tests", () => { + var numbers = [1, 2, 3]; + expect(query().select().from(numbers).execute()).to.deep.equal(numbers); + expect(query().select().execute()).to.deep.equal([], 'No FROM clause produces empty array'); + expect(query().from(numbers).execute()).to.deep.equal(numbers, 'SELECT can be omited'); + expect(query().execute()).to.deep.equal([]); + expect(query().from(numbers).select().execute()).to.deep.equal(numbers, 'The order does not matter'); + }); + + it("Basic SELECT and WHERE over objects", () => { + var persons = [{ + name: 'Peter', + profession: 'teacher', + age: 20, + maritalStatus: 'married' + }, + { + name: 'Michael', + profession: 'teacher', + age: 50, + maritalStatus: 'single' + }, + { + name: 'Peter', + profession: 'teacher', + age: 20, + maritalStatus: 'married' + }, + { + name: 'Anna', + profession: 'scientific', + age: 20, + maritalStatus: 'married' + }, + { + name: 'Rose', + profession: 'scientific', + age: 50, + maritalStatus: 'married' + }, + { + name: 'Anna', + profession: 'scientific', + age: 20, + maritalStatus: 'single' + }, + { + name: 'Anna', + profession: 'politician', + age: 50, + maritalStatus: 'married' + } + ]; + + expect(query().select().from(persons).execute()).to.deep.equal(persons); + + function profession(person: any) { + return person.profession; + } + + // SELECT profession FROM persons + expect(query().select(profession).from(persons).execute()).to.deep.equal(["teacher", "teacher", "teacher", "scientific", "scientific", "scientific", "politician"]); + expect(query().select(profession).execute()).to.deep.equal([], 'No FROM clause produces empty array'); + + function isTeacher(person: any) { + return person.profession === 'teacher'; + } + + // SELECT profession FROM persons WHERE profession="teacher" + expect(query().select(profession).from(persons).where(isTeacher).execute()).to.deep.equal(["teacher", "teacher", "teacher"]); + + // SELECT * FROM persons WHERE profession="teacher" + expect(query().from(persons).where(isTeacher).execute()).to.deep.equal(persons.slice(0, 3)); + + function name(person: any) { + return person.name; + } + + // SELECT name FROM persons WHERE profession="teacher" + expect(query().select(name).from(persons).where(isTeacher).execute()).to.deep.equal(["Peter", "Michael", "Peter"]); + expect(query().where(isTeacher).from(persons).select(name).execute()).to.deep.equal(["Peter", "Michael", "Peter"]); + }); + + it("GROUP BY tests", () => { + var persons = [{ + name: 'Peter', + profession: 'teacher', + age: 20, + maritalStatus: 'married' + }, + { + name: 'Michael', + profession: 'teacher', + age: 50, + maritalStatus: 'single' + }, + { + name: 'Peter', + profession: 'teacher', + age: 20, + maritalStatus: 'married' + }, + { + name: 'Anna', + profession: 'scientific', + age: 20, + maritalStatus: 'married' + }, + { + name: 'Rose', + profession: 'scientific', + age: 50, + maritalStatus: 'married' + }, + { + name: 'Anna', + profession: 'scientific', + age: 20, + maritalStatus: 'single' + }, + { + name: 'Anna', + profession: 'politician', + age: 50, + maritalStatus: 'married' + } + ]; + + function profession(person: any) { + return person.profession; + } + + // SELECT * FROM persons GROUPBY profession <- Bad in SQL but possible in JavaScript + expect(query().select().from(persons).groupBy(profession).execute()).to.deep.equal([ + ["teacher", [{ + "name": "Peter", + "profession": "teacher", + "age": 20, + "maritalStatus": "married" + }, { + "name": "Michael", + "profession": "teacher", + "age": 50, + "maritalStatus": "single" + }, { + "name": "Peter", + "profession": "teacher", + "age": 20, + "maritalStatus": "married" + }]], + ["scientific", [{ + "name": "Anna", + "profession": "scientific", + "age": 20, + "maritalStatus": "married" + }, { + "name": "Rose", + "profession": "scientific", + "age": 50, + "maritalStatus": "married" + }, { + "name": "Anna", + "profession": "scientific", + "age": 20, + "maritalStatus": "single" + }]], + ["politician", [{ + "name": "Anna", + "profession": "politician", + "age": 50, + "maritalStatus": "married" + }]] + ]); + + function isTeacher(person: any) { + return person.profession === 'teacher'; + } + + // SELECT * FROM persons WHERE profession='teacher' GROUPBY profession + expect(query().select().from(persons).where(isTeacher).groupBy(profession).execute()).to.deep.equal([ + ["teacher", [{ + "name": "Peter", + "profession": "teacher", + "age": 20, + "maritalStatus": "married" + }, { + "name": "Michael", + "profession": "teacher", + "age": 50, + "maritalStatus": "single" + }, { + "name": "Peter", + "profession": "teacher", + "age": 20, + "maritalStatus": "married" + }]] + ]); + + function professionGroup(group: any) { + return group[0]; + } + + // SELECT profession FROM persons GROUPBY profession + expect(query().select(professionGroup).from(persons).groupBy(profession).execute()).to.deep.equal(["teacher", "scientific", "politician"]); + + function name(person: any) { + return person.name; + } + + // SELECT * FROM persons WHERE profession='teacher' GROUPBY profession, name + expect(query().select().from(persons).groupBy(profession, name).execute()).to.deep.equal([ + ["teacher", [ + ["Peter", [{ + "name": "Peter", + "profession": "teacher", + "age": 20, + "maritalStatus": "married" + }, { + "name": "Peter", + "profession": "teacher", + "age": 20, + "maritalStatus": "married" + }]], + ["Michael", [{ + "name": "Michael", + "profession": "teacher", + "age": 50, + "maritalStatus": "single" + }]] + ]], + ["scientific", [ + ["Anna", [{ + "name": "Anna", + "profession": "scientific", + "age": 20, + "maritalStatus": "married" + }, { + "name": "Anna", + "profession": "scientific", + "age": 20, + "maritalStatus": "single" + }]], + ["Rose", [{ + "name": "Rose", + "profession": "scientific", + "age": 50, + "maritalStatus": "married" + }]] + ]], + ["politician", [ + ["Anna", [{ + "name": "Anna", + "profession": "politician", + "age": 50, + "maritalStatus": "married" + }]] + ]] + ]); + + function age(person: any) { + return person.age; + } + + function maritalStatus(person: any) { + return person.maritalStatus; + } + + // SELECT * FROM persons WHERE profession='teacher' GROUPBY profession, name, age + expect(query().select().from(persons).groupBy(profession, name, age, maritalStatus).execute()).to.deep.equal([ + ["teacher", [ + ["Peter", [ + [20, [ + ["married", [{ + "name": "Peter", + "profession": "teacher", + "age": 20, + "maritalStatus": "married" + }, { + "name": "Peter", + "profession": "teacher", + "age": 20, + "maritalStatus": "married" + }]] + ]] + ]], + ["Michael", [ + [50, [ + ["single", [{ + "name": "Michael", + "profession": "teacher", + "age": 50, + "maritalStatus": "single" + }]] + ]] + ]] + ]], + ["scientific", [ + ["Anna", [ + [20, [ + ["married", [{ + "name": "Anna", + "profession": "scientific", + "age": 20, + "maritalStatus": "married" + }]], + ["single", [{ + "name": "Anna", + "profession": "scientific", + "age": 20, + "maritalStatus": "single" + }]] + ]] + ]], + ["Rose", [ + [50, [ + ["married", [{ + "name": "Rose", + "profession": "scientific", + "age": 50, + "maritalStatus": "married" + }]] + ]] + ]] + ]], + ["politician", [ + ["Anna", [ + [50, [ + ["married", [{ + "name": "Anna", + "profession": "politician", + "age": 50, + "maritalStatus": "married" + }]] + ]] + ]] + ]] + ]); + + function professionCount(group: any) { + return [group[0], group[1].length]; + } + + // SELECT profession, count(profession) FROM persons GROUPBY profession + expect(query().select(professionCount).from(persons).groupBy(profession).execute()).to.deep.equal([ + ["teacher", 3], + ["scientific", 3], + ["politician", 1] + ]); + + function naturalCompare(value1: any, value2: any) { + if (value1 < value2) { + return -1; + } else if (value1 > value2) { + return 1; + } else { + return 0; + } + } + + // SELECT profession, count(profession) FROM persons GROUPBY profession ORDER BY profession + expect(query().select(professionCount).from(persons).groupBy(profession).orderBy(naturalCompare).execute()).to.deep.equal([ + ["politician", 1], + ["scientific", 3], + ["teacher", 3] + ]); + }); + + it("Number tests", () => { + function isEven(number: number) { + return number % 2 === 0; + } + + function parity(number: number) { + return isEven(number) ? 'even' : 'odd'; + } + + function isPrime(number: number) { + if (number < 2) { + return false; + } + var divisor = 2; + for (; number % divisor !== 0; divisor++); + return divisor === number; + } + + function prime(number: number) { + return isPrime(number) ? 'prime' : 'divisible'; + } + + var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + + // SELECT * FROM numbers + expect(query().select().from(numbers).execute()).to.deep.equal(numbers); + + // SELECT * FROM numbers GROUP BY parity + expect(query().select().from(numbers).groupBy(parity).execute()).to.deep.equal([ + ["odd", [1, 3, 5, 7, 9]], + ["even", [2, 4, 6, 8]] + ]); + + // SELECT * FROM numbers GROUP BY parity, isPrime + expect(query().select().from(numbers).groupBy(parity, prime).execute()).to.deep.equal([ + ["odd", [ + ["divisible", [1, 9]], + ["prime", [3, 5, 7]] + ]], + ["even", [ + ["prime", [2]], + ["divisible", [4, 6, 8]] + ]] + ]); + + function odd(group: any) { + return group[0] === 'odd'; + } + + // SELECT * FROM numbers GROUP BY parity HAVING + expect(query().select().from(numbers).groupBy(parity).having(odd).execute()).to.deep.equal([ + ["odd", [1, 3, 5, 7, 9]] + ]); + + function descendentCompare(number1: number, number2: number) { + return number2 - number1; + } + + // SELECT * FROM numbers ORDER BY value DESC + expect(query().select().from(numbers).orderBy(descendentCompare).execute()).to.deep.equal([9, 8, 7, 6, 5, 4, 3, 2, 1]); + + function lessThan3(number: number) { + return number < 3; + } + + function greaterThan4(number: number) { + return number > 4; + } + + // SELECT * FROM number WHERE number < 3 OR number > 4 + expect(query().select().from(numbers).where(lessThan3, greaterThan4).execute()).to.deep.equal([1, 2, 5, 6, 7, 8, 9]); + }); + + it("Frequency tests", () => { + var persons = [ + ['Peter', 3], + ['Anna', 4], + ['Peter', 7], + ['Michael', 10] + ]; + + function nameGrouping(person: any) { + return person[0]; + } + + function sumValues(value: any) { + return [value[0], value[1].reduce(function(result: any, person: any) { + return result + person[1]; + }, 0)]; + } + + function naturalCompare(value1: any, value2: any) { + if (value1 < value2) { + return -1; + } else if (value1 > value2) { + return 1; + } else { + return 0; + } + } + + // SELECT name, sum(value) FROM persons ORDER BY naturalCompare GROUP BY nameGrouping + expect(query().select(sumValues).from(persons).orderBy(naturalCompare).groupBy(nameGrouping).execute()).to.deep.equal([ + ["Anna", 4], + ["Michael", 10], + ["Peter", 10] + ]); + + var numbers = [1, 2, 1, 3, 5, 6, 1, 2, 5, 6]; + + function id(value: any) { + return value; + } + + function frequency(group: any) { + return { + value: group[0], + frequency: group[1].length + }; + } + + // SELECT number, count(number) FROM numbers GROUP BY number + expect(query().select(frequency).from(numbers).groupBy(id).execute()).to.deep.equal([{ + "value": 1, + "frequency": 3 + }, { + "value": 2, + "frequency": 2 + }, { + "value": 3, + "frequency": 1 + }, { + "value": 5, + "frequency": 2 + }, { + "value": 6, + "frequency": 2 + }]); + + function greatThan1(group: any) { + return group[1].length > 1; + } + + function isPair(group: any) { + return group[0] % 2 === 0; + } + + // SELECT number, count(number) FROM numbers GROUP BY number HAVING count(number) > 1 AND isPair(number) + expect(query().select(frequency).from(numbers).groupBy(id).having(greatThan1).having(isPair).execute()).to.deep.equal([{ + "value": 2, + "frequency": 2 + }, { + "value": 6, + "frequency": 2 + }]); + }); + + it("Join tests", () => { + var teachers = [{ + teacherId: '1', + teacherName: 'Peter' + }, + { + teacherId: '2', + teacherName: 'Anna' + } + ]; + + + var students = [{ + studentName: 'Michael', + tutor: '1' + }, + { + studentName: 'Rose', + tutor: '2' + } + ]; + + function teacherJoin(join: any) { + return join[0].teacherId === join[1].tutor; + } + + function student(join: any) { + return { + studentName: join[1].studentName, + teacherName: join[0].teacherName + }; + } + + // SELECT studentName, teacherName FROM teachers, students WHERE teachers.teacherId = students.tutor + expect(query().select(student).from(teachers, students).where(teacherJoin).execute()).to.deep.equal([{ + "studentName": "Michael", + "teacherName": "Peter" + }, { + "studentName": "Rose", + "teacherName": "Anna" + }]); + + var numbers1 = [1, 2]; + var numbers2 = [4, 5]; + + expect(query().select().from(numbers1, numbers2).execute()).to.deep.equal([ + [1, 4], + [1, 5], + [2, 4], + [2, 5] + ]); + + function tutor1(join: any) { + return join[1].tutor === "1"; + } + + // SELECT studentName, teacherName FROM teachers, students WHERE teachers.teacherId = students.tutor AND tutor = 1 + expect(query().select(student).from(teachers, students).where(teacherJoin).where(tutor1).execute()).to.deep.equal([{ + "studentName": "Michael", + "teacherName": "Peter" + }]); + expect(query().where(teacherJoin).select(student).where(tutor1).from(teachers, students).execute()).to.deep.equal([{ + "studentName": "Michael", + "teacherName": "Peter" + }]); + + }); + + it("Duplication exception tests", () => { + function checkError(fn: any, duplicate: any) { + try { + fn(); + expect(false).to.equal(false, 'An error should be throw'); + } catch (e) { + expect(e instanceof Error).to.equal(true); + expect((e as Error).message).to.equal('Duplicate ' + duplicate); + } + } + + function id(value: any) { + return value; + } + + checkError(function() { + query().select().select().execute(); + }, 'SELECT'); + checkError(function() { + query().select().from([]).select().execute(); + }, 'SELECT'); + checkError(function() { + query().select().from([]).from([]).execute(); + }, 'FROM'); + checkError(function() { + query().select().from([]).orderBy(id).orderBy(id).execute(); + }, 'ORDERBY'); + checkError(function() { + query().select().groupBy(id).from([]).groupBy(id).execute(); + }, 'GROUPBY'); + }); +}); \ No newline at end of file