Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -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
44 changes: 40 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 10 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
62 changes: 62 additions & 0 deletions test/sql.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
53 changes: 53 additions & 0 deletions test/table.test.js
Original file line number Diff line number Diff line change
@@ -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']);
});
Loading