diff --git a/.gitattributes b/.gitattributes index 1e4e122676..05dc65176c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -12,6 +12,6 @@ yarn.lock binary # paths that don't start with / are relative to the .gitattributes folder #relative/path/*.txt text eol=lf -src/alasqlparser.js merge=union +src/alasqlparser.js binary diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index db2947b31c..bbb333564e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -7,39 +7,29 @@ AlaSQL is an open source SQL database for JavaScript with a focus on query speed ## When Implementing Features 1. **Understand the issue thoroughly** - Read related test cases and existing code -2. **Write a test first** - Create `test/test###.js` for the issue where `###` is the id of the issue we are actually fixing. +2. **Write a test first** - Copy test/test000.js into a new file called `test/test###.js` where where `###` is the id of the issue we are trying to solve 3. **Verify test fails** - Run `yarn test` to confirm the test catches the issue 4. **Implement the fix** - Modify appropriate file(s) in `src/` -5. **Format code** - Run `yarn format` before committing -6. **Verify test passes** - Run `yarn test` again + - If you modify the grammar in `src/alasqlgrammar.jison`, run `yarn jison && yarn test` to regenerate the parser and verify +5. **Reconsider elegance** - Make sure to assess the solution and reconsider if this can be more elegant or efficient +6. **Format code** - Run `yarn format` before committing -## How to to test files -1. Make a test file - - Name new test files as `test/test###.js` where `###` is the GitHub issue number of the issue we are actually fixing. - - If the file already exists we name the file test/test###-B.js -1. Copy the structure in `test/test000.js` as a template -3. Tests should be self-contained and clear about what they're testing -4. Use the Mocha test framework with standard assertions - -## SQL Parser Modifications - -If a problem demands modifying the lexical parser then seek to do chances as small as possible to `src/alasqlparser.jison`. Afterwards run `yarn jison && yarn test` to confirm the result. - ## Commands ```bash # Install dependencies yarn +# Generate grammar (if needed) +yarn jison + # Run tests yarn test # Format code yarn format - -# Build project -yarn build +``` ## Files to Avoid Modifying @@ -47,14 +37,3 @@ yarn build - `src/alasqlparser.js` - Generated from Jison grammar (modify the `.jison` file instead) - `.min.js` files - Generated during build - -now commit the code. - -## When Reviewing Code - -- Verify tests exist for any new functionality and any regression the code changes could have affected. - -## Resources - -- [AlaSQL Documentation](https://github.com/alasql/alasql/wiki) -- [Issue Tracker](https://github.com/AlaSQL/alasql/issues) diff --git a/test/test000.js b/test/test000.js index 540bc4d43a..11f0b43554 100644 --- a/test/test000.js +++ b/test/test000.js @@ -3,24 +3,22 @@ if (typeof exports === 'object') { var alasql = require('..'); } -describe('Test 000 - multiple statements', function () { - const test = '000'; // insert test file number +let testId = '000'; // Use the ID of the issue being fixed by this PR +describe(`Test ${testId} - multiple statements`, function () { before(function () { - alasql('create database test' + test); - alasql('use test' + test); + alasql('create database test' + testId); + alasql('use test' + testId); }); after(function () { - alasql('drop database test' + test); + alasql('drop database test' + testId); }); - // NOTE: Tests should use assert.deepEqual to verify the complete expected output - // against the actual result object. This ensures comprehensive validation and - // makes test failures more informative by showing the full diff. + // NOTE: Always assert.deepEqual the final and complete output of the last call to alasql it('A) From single lines', function () { - var res = []; + let res = []; res.push(alasql('create table one (a int)')); res.push(alasql('insert into one values (1),(2),(3),(4),(5)')); res.push(alasql('select * from one')); @@ -29,16 +27,16 @@ describe('Test 000 - multiple statements', function () { it('B) Multiple statements in one string', function () { // - var sql = 'create table two (a int);'; + let sql = 'create table two (a int);'; sql += 'insert into two values (1),(2),(3),(4),(5);'; sql += 'select * from two;'; - var res = alasql(sql); + let res = alasql(sql); assert.deepStrictEqual(res, [1, 5, [{a: 1}, {a: 2}, {a: 3}, {a: 4}, {a: 5}]]); }); it('C) Multiple statements in one string with callback', function (done) { - // Please note that first parameter (here `done`) must be called if defined - and is needed when testing async code - var sql = 'create table three (a int);'; + // use first param (here `done`) when operating with async function or async code + let sql = 'create table three (a int);'; sql += 'insert into three values (1),(2),(3),(4),(5);'; sql += 'select * from three;'; alasql(sql, function (res) { diff --git a/test/test2414.js b/test/test2414.js new file mode 100644 index 0000000000..d313dd2e90 --- /dev/null +++ b/test/test2414.js @@ -0,0 +1,288 @@ +if (typeof exports === 'object') { + var assert = require('assert'); + var alasql = require('..'); +} + +describe('Test 2414 - UNION with parenthesized SELECT and ORDER BY', function () { + const test = '2414'; + + before(function () { + alasql('create database test' + test); + alasql('use test' + test); + }); + + after(function () { + alasql('drop database test' + test); + }); + + it('A) Parenthesized SELECT with ORDER BY before UNION', function () { + var foreignCompetitors = [ + {country: 'USA', name: 'John'}, + {country: 'UK', name: 'Jane'}, + {country: 'USA', name: 'Bob'}, + {country: 'France', name: 'Pierre'}, + {country: 'UK', name: 'Alice'}, + ]; + + // SQL-99 compliant: parenthesized SELECT with ORDER BY before UNION + var res = alasql( + `(SELECT country, COUNT(*) AS competitors + FROM ? + GROUP BY country + ORDER BY country) + UNION + SELECT "Total: " AS country, COUNT(*) AS competitors + FROM ?`, + [foreignCompetitors, foreignCompetitors] + ); + + // UNION removes duplicates, result is unordered unless specified + assert.deepEqual(res, [ + {country: 'France', competitors: 1}, + {country: 'Total: ', competitors: 5}, + {country: 'UK', competitors: 2}, + {country: 'USA', competitors: 2}, + ]); + }); + + it('B) Simplified case - parenthesized ORDER BY before UNION', function () { + var data = [ + {a: 1, b: 'x'}, + {a: 2, b: 'y'}, + {a: 3, b: 'z'}, + ]; + + var res = alasql(`(SELECT a FROM ? WHERE a < 3 ORDER BY a) UNION SELECT a FROM ? WHERE a > 1`, [ + data, + data, + ]); + + assert.deepEqual(res, [{a: 1}, {a: 2}, {a: 3}]); + }); + + it('C) Parenthesized ORDER BY DESC before UNION', function () { + var data = [ + {a: 1, b: 'x'}, + {a: 2, b: 'y'}, + {a: 3, b: 'z'}, + ]; + + var res = alasql( + `(SELECT a FROM ? WHERE a < 3 ORDER BY a DESC) UNION SELECT a FROM ? WHERE a > 2`, + [data, data] + ); + + assert.deepEqual(res, [{a: 3}, {a: 2}, {a: 1}]); + }); + + it('D) Parenthesized LIMIT before UNION', function () { + var data = [ + {a: 1, b: 'x'}, + {a: 2, b: 'y'}, + {a: 3, b: 'z'}, + {a: 4, b: 'w'}, + ]; + + var res = alasql(`(SELECT a FROM ? LIMIT 2) UNION SELECT a FROM ? WHERE a > 2`, [data, data]); + + assert.deepEqual(res, [{a: 3}, {a: 4}]); + }); + + it('E) Parenthesized ORDER BY and LIMIT before UNION', function () { + var data = [ + {a: 1, b: 'x'}, + {a: 2, b: 'y'}, + {a: 3, b: 'z'}, + {a: 4, b: 'w'}, + ]; + + var res = alasql( + `(SELECT a FROM ? ORDER BY a DESC LIMIT 2) UNION SELECT a FROM ? WHERE a < 2`, + [data, data] + ); + + assert.deepEqual(res, [{a: 4}, {a: 3}]); + }); + + it('F) Parenthesized ORDER BY before UNION with ORDER BY at end', function () { + var data = [ + {a: 1, b: 'x'}, + {a: 2, b: 'y'}, + {a: 3, b: 'z'}, + ]; + + var res = alasql( + `(SELECT a FROM ? WHERE a < 3 ORDER BY a) UNION SELECT a FROM ? WHERE a > 1 ORDER BY a DESC`, + [data, data] + ); + + // Final ORDER BY should take precedence, result ordered descending + assert.deepEqual(res, [{a: 3}, {a: 2}, {a: 1}]); + }); + + it('G) Both SELECTs parenthesized with ORDER BY + LIMIT', function () { + var data = [ + {a: 1, b: 'x'}, + {a: 2, b: 'y'}, + {a: 3, b: 'z'}, + {a: 4, b: 'w'}, + ]; + + var res = alasql( + `(SELECT a FROM ? ORDER BY a LIMIT 2) UNION ALL (SELECT a FROM ? ORDER BY a DESC LIMIT 1)`, + [data, data] + ); + + assert.deepEqual(res, [{a: 1}, {a: 2}]); + }); + + it('H) UNION ALL with parenthesized ORDER BY', function () { + var data = [ + {a: 1, b: 'x'}, + {a: 2, b: 'y'}, + {a: 3, b: 'z'}, + ]; + + var res = alasql( + `(SELECT a FROM ? WHERE a < 3 ORDER BY a) UNION ALL SELECT a FROM ? WHERE a > 1`, + [data, data] + ); + + // UNION ALL keeps duplicates + assert.deepEqual(res, [{a: 1}, {a: 2}, {a: 2}, {a: 3}]); + }); + + it('I) EXCEPT with parenthesized ORDER BY', function () { + var data = [ + {a: 1, b: 'x'}, + {a: 2, b: 'y'}, + {a: 3, b: 'z'}, + ]; + + var res = alasql( + `(SELECT a FROM ? WHERE a < 4 ORDER BY a) EXCEPT SELECT a FROM ? WHERE a = 2`, + [data, data] + ); + + assert.deepEqual(res, [{a: 1}, {a: 3}]); + }); + + it('J) INTERSECT with parenthesized ORDER BY', function () { + var data = [ + {a: 1, b: 'x'}, + {a: 2, b: 'y'}, + {a: 3, b: 'z'}, + ]; + + var res = alasql( + `(SELECT a FROM ? WHERE a < 3 ORDER BY a) INTERSECT SELECT a FROM ? WHERE a > 0`, + [data, data] + ); + + assert.deepEqual(res, [{a: 1}, {a: 2}]); + }); + + it('K) Multiple UNIONs with parenthesized ORDER BY', function () { + var data = [ + {a: 1, b: 'x'}, + {a: 2, b: 'y'}, + {a: 3, b: 'z'}, + {a: 4, b: 'w'}, + ]; + + var res = alasql( + `(SELECT a FROM ? WHERE a = 1 ORDER BY a) UNION SELECT a FROM ? WHERE a = 2 UNION SELECT a FROM ? WHERE a = 3 ORDER BY a DESC`, + [data, data, data] + ); + + assert.deepEqual(res, [{a: 3}, {a: 2}, {a: 1}]); + }); + + it('L) Parenthesized ORDER BY with multiple columns before UNION', function () { + var data = [ + {a: 1, b: 'x', c: 10}, + {a: 2, b: 'y', c: 20}, + {a: 3, b: 'z', c: 30}, + ]; + + var res = alasql( + `(SELECT a, b FROM ? WHERE a < 3 ORDER BY b, a) UNION SELECT a, b FROM ? WHERE a > 2`, + [data, data] + ); + + assert.deepEqual(res, [ + {a: 1, b: 'x'}, + {a: 2, b: 'y'}, + {a: 3, b: 'z'}, + ]); + }); + + it('M) Parenthesized ORDER BY with expression before UNION', function () { + var data = [ + {a: 1, b: 'x'}, + {a: 2, b: 'y'}, + {a: 3, b: 'z'}, + ]; + + var res = alasql( + `(SELECT a, b FROM ? WHERE a < 3 ORDER BY a * 2) UNION SELECT a, b FROM ? WHERE a > 2`, + [data, data] + ); + + assert.deepEqual(res, [ + {a: 3, b: 'z'}, + {a: 1, b: 'x'}, + {a: 2, b: 'y'}, + ]); + }); + + it('N) Parenthesized LIMIT with OFFSET before UNION', function () { + var data = [ + {a: 1, b: 'x'}, + {a: 2, b: 'y'}, + {a: 3, b: 'z'}, + {a: 4, b: 'w'}, + {a: 5, b: 'v'}, + ]; + + var res = alasql( + `(SELECT a FROM ? ORDER BY a LIMIT 2 OFFSET 1) UNION SELECT a FROM ? WHERE a > 4`, + [data, data] + ); + + assert.deepEqual(res, [{a: 2}, {a: 3}]); + }); + + it('O) UNION without parentheses and ORDER BY at end (standard behavior)', function () { + var data = [ + {a: 1, b: 'x'}, + {a: 2, b: 'y'}, + {a: 3, b: 'z'}, + ]; + + // This is the standard SQL-99 behavior: ORDER BY applies to the entire UNION result + var res = alasql( + `SELECT a FROM ? WHERE a < 3 UNION SELECT a FROM ? WHERE a > 1 ORDER BY a DESC`, + [data, data] + ); + + // Final ORDER BY applies to entire result, ordered descending + assert.deepEqual(res, [{a: 3}, {a: 2}, {a: 1}]); + }); + + it('P) Complex nested parenthesized UNIONs with ORDER BY', function () { + var data = [ + {a: 1, b: 'x'}, + {a: 2, b: 'y'}, + {a: 3, b: 'z'}, + {a: 4, b: 'w'}, + ]; + + var res = alasql( + `((SELECT a FROM ? WHERE a = 1) UNION (SELECT a FROM ? WHERE a = 2 ORDER BY a)) UNION SELECT a FROM ? WHERE a > 2 ORDER BY a DESC`, + [data, data, data] + ); + + assert.deepEqual(res, [{a: 4}, {a: 3}, {a: 1}]); + }); +});