diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6010e27 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,22 @@ +name: Tests + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: ['18.x', '20.x', '22.x', '24.x'] + name: Node ${{ matrix.node-version }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - run: npm test diff --git a/README.md b/README.md index ecda564..36cfcc3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # sql-nodejs +[![Tests](https://github.com/Megapixel99/sql-nodejs/actions/workflows/test.yml/badge.svg)](https://github.com/Megapixel99/sql-nodejs/actions/workflows/test.yml) +[![npm version](https://img.shields.io/npm/v/sql-nodejs.svg)](https://www.npmjs.com/package/sql-nodejs) +[![npm downloads](https://img.shields.io/npm/dm/sql-nodejs.svg)](https://www.npmjs.com/package/sql-nodejs) +[![license](https://img.shields.io/npm/l/sql-nodejs.svg)](LICENSE) + An **in-memory SQL database written from scratch in JavaScript** — a hand-written SQL parser and storage engine with **zero dependencies**. You give it SQL strings; it creates databases and tables, stores rows, and answers `SELECT` queries. @@ -14,6 +19,18 @@ it creates databases and tables, stores rows, and answers `SELECT` queries. npm install sql-nodejs ``` +Zero dependencies; requires Node 18 or newer. To pin a specific release: + +```bash +npm install sql-nodejs@0.0.6 +``` + +To work on the project itself: + +```bash +git clone https://github.com/Megapixel99/sql-nodejs.git +``` + ## Usage ```javascript @@ -67,10 +84,29 @@ column values in the order you asked for them. npm test ``` -Runs the assertion suite in `test/` (Node's built-in test runner — no -dependencies). It covers projection order and subsets, `WHERE` filtering, -name-based table/database resolution, `DROP`, and the error paths -(no database selected, missing table, unsupported statement). +That runs `node --test` over `test/` — Node's built-in test runner, so there is +nothing to install first. 28 assertions across two files: + +| File | Covers | +|---|---| +| `test/sql.test.js` | statement level: projection order and subsets, `WHERE` filtering (one match, many matches, none), name-based table/database resolution, `USE`, `DROP TABLE` / `DROP DATABASE`, case-insensitivity, the optional trailing semicolon, and the error paths (no database selected, missing table, unknown column, unsupported statement) | +| `test/table.test.js` | storage level: column names and types, `getDataForColumn`, inserts naming an unknown column, table lookup/drop on `Database`, and `Row` get/set | + +To run a single file: + +```bash +node --test test/sql.test.js +``` + +To re-run the suite as you edit: + +```bash +node --test --watch +``` + +Every push and pull request runs the same suite on Node 18, 20, 22, and 24 via +GitHub Actions ([`.github/workflows/test.yml`](.github/workflows/test.yml)) — +that's the badge at the top. ## Limitations diff --git a/package-lock.json b/package-lock.json index ecb24bc..62e9b82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { "name": "@megapixel99/sql-mock", - "version": "0.0.1", + "version": "0.0.6", "lockfileVersion": 1 } diff --git a/package.json b/package.json index ccd3f77..964855c 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,26 @@ { "name": "sql-nodejs", - "version": "0.0.5", + "version": "0.0.6", "description": "Use an SQL database stored in memory with JavaScript", "main": "index.js", + "files": [ + "index.js", + "database.js", + "table.js", + "row.js" + ], "scripts": { "test": "node --test", "start": "node index.js" }, "repository": { "type": "git", - "url": "https://github.com/Megapixel99/sqlJS.git" + "url": "git+https://github.com/Megapixel99/sql-nodejs.git" }, "bugs": { - "url": "https://github.com/Megapixel99/sqlJS/issues" + "url": "https://github.com/Megapixel99/sql-nodejs/issues" }, - "homepage": "https://github.com/Megapixel99/sqlJS", + "homepage": "https://github.com/Megapixel99/sql-nodejs", "keywords": [ "oracle", "oracledb", diff --git a/test/sql.test.js b/test/sql.test.js index ae47698..44f4f15 100644 --- a/test/sql.test.js +++ b/test/sql.test.js @@ -77,3 +77,65 @@ test('unsupported statements throw', () => { const p = freshDb(); assert.throws(() => p.Parse('UPDATE t SET a=1;'), /not supported/); }); + +test('WHERE matching several rows returns all of them', () => { + const p = freshDb(); + p.Parse('INSERT INTO t (a, b, c) VALUES (1, 9, 9);'); + assert.deepStrictEqual(p.Parse('SELECT * FROM t WHERE a=1;'), [['1', '2', '3'], ['1', '9', '9']]); +}); + +test('WHERE matching nothing returns an empty result', () => { + const p = freshDb(); + assert.deepStrictEqual(p.Parse('SELECT * FROM t WHERE a=99;'), []); +}); + +test('statements are case-insensitive', () => { + const p = freshDb(); + assert.deepStrictEqual(p.Parse('SeLeCt * FROM T;'), [['1', '2', '3'], ['4', '5', '6']]); +}); + +test('the trailing semicolon is optional', () => { + const p = freshDb(); + assert.deepStrictEqual(p.Parse('SELECT * FROM t'), [['1', '2', '3'], ['4', '5', '6']]); +}); + +test('SELECT of an unknown column throws', () => { + const p = freshDb(); + assert.throws(() => p.Parse('SELECT z FROM t;'), /COLUMN\(S\) not found/); +}); + +test('WHERE on an unknown column throws', () => { + const p = freshDb(); + assert.throws(() => p.Parse('SELECT * FROM t WHERE z=1;'), /COLUMN\(S\) not found/); +}); + +test('INSERT into a missing table throws', () => { + const p = freshDb(); + assert.throws(() => p.Parse('INSERT INTO missing (a) VALUES (1);'), /TABLE not found/); +}); + +test('CREATE TABLE before choosing a database throws', () => { + const p = new SqlParser(); + assert.throws(() => p.Parse('CREATE TABLE t (a INT);'), /No Database selected/); +}); + +test('INSERT before choosing a database throws', () => { + const p = new SqlParser(); + assert.throws(() => p.Parse('INSERT INTO t (a) VALUES (1);'), /No Database selected/); +}); + +test('DROP DATABASE clears the active database', () => { + const p = freshDb(); + p.Parse('DROP DATABASE d;'); + assert.strictEqual(p.getCurrentDatabase(), null); + assert.throws(() => p.Parse('SELECT * FROM t;'), /No Database selected/); +}); + +test('DROP TABLE leaves the other tables alone', () => { + const p = freshDb(); + p.Parse('CREATE TABLE u (x INT);'); + p.Parse('INSERT INTO u (x) VALUES (9);'); + p.Parse('DROP TABLE t;'); + assert.deepStrictEqual(p.Parse('SELECT * FROM u;'), [['9']]); + assert.strictEqual(p.getCurrentDatabase().getTables().length, 1); +}); diff --git a/test/table.test.js b/test/table.test.js new file mode 100644 index 0000000..addaad4 --- /dev/null +++ b/test/table.test.js @@ -0,0 +1,53 @@ +const test = require('node:test'); +const assert = require('node:assert'); +const Database = require('../database.js'); +const Table = require('../table.js'); +const Row = require('../row.js'); + +// A table matching `CREATE TABLE t (a INT, b VARCHAR)` holding two rows. +function freshTable() { + const t = new Table('t', ['a int', 'b varchar']); + t.insertDataIntoTable(['a', 'b'], ['1', 'x']); + t.insertDataIntoTable(['a', 'b'], ['2', 'y']); + return t; +} + +test('a table records its column names and types', () => { + const t = freshTable(); + assert.deepStrictEqual(t.getColmunNames(), ['a', 'b']); + assert.deepStrictEqual(t.getColmunTypes(), ['int', 'varchar']); +}); + +test('getDataForColumn returns one column across every row', () => { + const t = freshTable(); + assert.deepStrictEqual(t.getDataForColumn('b'), ['x', 'y']); +}); + +test('getDataForColumn on an unknown column throws', () => { + const t = freshTable(); + assert.throws(() => t.getDataForColumn('z'), /COLUMN\(S\) not found/); +}); + +test('inserting with an unknown column stores nothing', () => { + const t = freshTable(); + t.insertDataIntoTable(['z'], ['9']); + assert.deepStrictEqual(t.selectAllDataFromTable(), [['1', 'x'], ['2', 'y']]); +}); + +test('a database resolves and drops tables by name', () => { + const d = new Database('d'); + const t = freshTable(); + d.createTable(t); + assert.strictEqual(d.getName(), 'd'); + assert.strictEqual(d.getTable('t'), t); + assert.strictEqual(d.getTable('missing'), undefined); + d.dropTable('t'); + assert.deepStrictEqual(d.getTables(), []); +}); + +test('a row holds and replaces its data', () => { + const r = new Row(['1', 'x']); + assert.deepStrictEqual(r.getRow(), ['1', 'x']); + r.setRow(['2', 'y']); + assert.deepStrictEqual(r.getRow(), ['2', 'y']); +});