@fizzog/sqlx 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +228 -0
- package/dist/engine/SqlEngine.d.ts +13 -0
- package/dist/executor/Executor.d.ts +19 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +566 -0
- package/dist/lexer/Lexer.d.ts +10 -0
- package/dist/lexer/Token.d.ts +18 -0
- package/dist/parser/AST.d.ts +38 -0
- package/dist/parser/Parser.d.ts +23 -0
- package/dist/parser/TokenStream.d.ts +12 -0
- package/dist/runtime/ExpressionEvaluator.d.ts +4 -0
- package/dist/storage/Database.d.ts +6 -0
- package/dist/storage/Table.d.ts +7 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# TypeScript SQL Engine
|
|
2
|
+
|
|
3
|
+
A lightweight SQL engine written in TypeScript.
|
|
4
|
+
|
|
5
|
+
`@fizzog/sqlx` is a small, experimental SQL implementation designed for learning how databases work internally. It includes a SQL lexer, parser, AST generation, and execution engine.
|
|
6
|
+
|
|
7
|
+
The goal of this project is not to replace production databases, but to provide a simple and understandable implementation of SQL concepts.
|
|
8
|
+
|
|
9
|
+
And perhaps, this can be something useful when more features are added.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
Currently supported:
|
|
14
|
+
|
|
15
|
+
* `CREATE TABLE`
|
|
16
|
+
* `INSERT`
|
|
17
|
+
* `SELECT`
|
|
18
|
+
* `UPDATE`
|
|
19
|
+
* `DELETE`
|
|
20
|
+
* `WHERE` expressions
|
|
21
|
+
* Multiple column selection
|
|
22
|
+
* Multiple assignments in `UPDATE`
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
|
|
26
|
+
```sql
|
|
27
|
+
CREATE TABLE users (id, name, age);
|
|
28
|
+
|
|
29
|
+
INSERT INTO users VALUES (1, 'Alice', 30);
|
|
30
|
+
|
|
31
|
+
SELECT * FROM users WHERE age > 18;
|
|
32
|
+
|
|
33
|
+
UPDATE users SET age = 31 WHERE name = 'Alice';
|
|
34
|
+
|
|
35
|
+
DELETE FROM users WHERE age < 18;
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
Install with npm:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install @fizzog/sqlx
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
or with Bun:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
bun add @fizzog/sqlx
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
Import the engine and database classes:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import {
|
|
58
|
+
SqlEngine,
|
|
59
|
+
Database,
|
|
60
|
+
Table
|
|
61
|
+
} from "@fizzog/sqlx";
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Create a database:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const db = new Database();
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Create a table:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
db.addTable(
|
|
74
|
+
"users", // name
|
|
75
|
+
["id", "name", "age"], // columns
|
|
76
|
+
[] // rows
|
|
77
|
+
);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Create a SQL engine:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
const engine = SqlEngine.start(db);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Execute SQL:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
engine.consume(
|
|
90
|
+
"INSERT INTO users VALUES (1, 'Alice', 30);"
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const users = engine.consume(
|
|
94
|
+
"SELECT * FROM users;"
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
console.log(users);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Output:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
[
|
|
104
|
+
{
|
|
105
|
+
"id": 1,
|
|
106
|
+
"name": "Alice",
|
|
107
|
+
"age": 30
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## API
|
|
113
|
+
|
|
114
|
+
### `SqlEngine.start(database)`
|
|
115
|
+
|
|
116
|
+
Creates a new SQL engine instance.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
const engine = SqlEngine.start(db);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### `engine.consume(sql)`
|
|
123
|
+
|
|
124
|
+
Parses and executes a SQL statement.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const result = engine.consume(
|
|
128
|
+
"SELECT * FROM users;"
|
|
129
|
+
);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Returns a JavaScript value depending on the SQL statement.
|
|
133
|
+
|
|
134
|
+
Examples:
|
|
135
|
+
|
|
136
|
+
`SELECT`:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
[
|
|
140
|
+
{
|
|
141
|
+
id: 1,
|
|
142
|
+
name: "Alice",
|
|
143
|
+
age: 30
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`INSERT`:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
{
|
|
152
|
+
inserted: 1
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`UPDATE`:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
{
|
|
160
|
+
updated: 1
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`DELETE`:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
{
|
|
168
|
+
deleted: 1
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Architecture
|
|
173
|
+
|
|
174
|
+
The engine processes SQL through several stages:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
SQL String
|
|
178
|
+
|
|
|
179
|
+
v
|
|
180
|
+
Lexer
|
|
181
|
+
|
|
|
182
|
+
v
|
|
183
|
+
Tokens
|
|
184
|
+
|
|
|
185
|
+
v
|
|
186
|
+
Parser
|
|
187
|
+
|
|
|
188
|
+
v
|
|
189
|
+
Abstract Syntax Tree
|
|
190
|
+
|
|
|
191
|
+
v
|
|
192
|
+
Executor
|
|
193
|
+
|
|
|
194
|
+
v
|
|
195
|
+
Database
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Each stage is intentionally separated to make the internals easier to understand and extend.
|
|
199
|
+
|
|
200
|
+
## Limitations
|
|
201
|
+
|
|
202
|
+
This project is intentionally simple.
|
|
203
|
+
|
|
204
|
+
Current limitations include:
|
|
205
|
+
|
|
206
|
+
* In-memory storage only
|
|
207
|
+
* No persistence
|
|
208
|
+
* No indexes
|
|
209
|
+
* No joins
|
|
210
|
+
* No transactions
|
|
211
|
+
* Limited SQL syntax support
|
|
212
|
+
* No type system enforcement
|
|
213
|
+
|
|
214
|
+
## Project Status
|
|
215
|
+
|
|
216
|
+
This project is primarily a learning project focused on understanding:
|
|
217
|
+
|
|
218
|
+
* SQL parsing
|
|
219
|
+
* Tokenization
|
|
220
|
+
* AST design
|
|
221
|
+
* Query execution
|
|
222
|
+
* Database architecture concepts
|
|
223
|
+
|
|
224
|
+
More SQL features will be added over time.
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Database } from "../storage/Database";
|
|
2
|
+
export declare class SqlEngine {
|
|
3
|
+
private executor;
|
|
4
|
+
private constructor();
|
|
5
|
+
static start(db: Database): SqlEngine;
|
|
6
|
+
consume(sql: string): Record<string, unknown> | import("../storage/Table").Row[] | {
|
|
7
|
+
success: boolean;
|
|
8
|
+
} | {
|
|
9
|
+
updated: number;
|
|
10
|
+
} | {
|
|
11
|
+
deleted: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Statement } from "../parser/AST";
|
|
2
|
+
import { Database } from "../storage/Database";
|
|
3
|
+
export declare class Executor {
|
|
4
|
+
private db;
|
|
5
|
+
private expressionEvaluator;
|
|
6
|
+
constructor(db: Database);
|
|
7
|
+
execute(statement: Statement): Record<string, unknown> | import("../storage/Table").Row[] | {
|
|
8
|
+
success: boolean;
|
|
9
|
+
} | {
|
|
10
|
+
updated: number;
|
|
11
|
+
} | {
|
|
12
|
+
deleted: number;
|
|
13
|
+
};
|
|
14
|
+
private executeSelect;
|
|
15
|
+
private executeInsert;
|
|
16
|
+
private executeCreateTable;
|
|
17
|
+
private executeUpdate;
|
|
18
|
+
private executeDelete;
|
|
19
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
// src/runtime/ExpressionEvaluator.ts
|
|
2
|
+
class ExpressionEvaluator {
|
|
3
|
+
evaluate(expression, row) {
|
|
4
|
+
const left = row[expression.left];
|
|
5
|
+
if (typeof left !== "number" && typeof left !== "string") {
|
|
6
|
+
throw new Error(`Cannot compare value: ${String(left)}`);
|
|
7
|
+
}
|
|
8
|
+
switch (expression.operator) {
|
|
9
|
+
case ">":
|
|
10
|
+
return left > expression.right;
|
|
11
|
+
case "<":
|
|
12
|
+
return left < expression.right;
|
|
13
|
+
case "=":
|
|
14
|
+
return left === expression.right;
|
|
15
|
+
default:
|
|
16
|
+
throw new Error(`Unknown operator ${expression.operator}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/storage/Table.ts
|
|
22
|
+
class Table {
|
|
23
|
+
name;
|
|
24
|
+
columns;
|
|
25
|
+
rows;
|
|
26
|
+
constructor(name, columns, rows) {
|
|
27
|
+
this.name = name;
|
|
28
|
+
this.columns = columns;
|
|
29
|
+
this.rows = rows;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/executor/Executor.ts
|
|
34
|
+
class Executor {
|
|
35
|
+
db;
|
|
36
|
+
expressionEvaluator = new ExpressionEvaluator;
|
|
37
|
+
constructor(db) {
|
|
38
|
+
this.db = db;
|
|
39
|
+
}
|
|
40
|
+
execute(statement) {
|
|
41
|
+
switch (statement.type) {
|
|
42
|
+
case "SelectStatement":
|
|
43
|
+
return this.executeSelect(statement);
|
|
44
|
+
case "InsertStatement":
|
|
45
|
+
return this.executeInsert(statement);
|
|
46
|
+
case "CreateTableStatement":
|
|
47
|
+
return this.executeCreateTable(statement);
|
|
48
|
+
case "UpdateStatement":
|
|
49
|
+
return this.executeUpdate(statement);
|
|
50
|
+
case "DeleteStatement":
|
|
51
|
+
return this.executeDelete(statement);
|
|
52
|
+
default:
|
|
53
|
+
throw new Error(`Unsupported statement`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
executeSelect(statement) {
|
|
57
|
+
const table = this.db.getTable(statement.table);
|
|
58
|
+
let rows = table.rows;
|
|
59
|
+
if (statement.where) {
|
|
60
|
+
rows = rows.filter((row) => this.expressionEvaluator.evaluate(statement.where, row));
|
|
61
|
+
}
|
|
62
|
+
if (statement.columns.length === 1 && statement.columns[0] === "*") {
|
|
63
|
+
return rows;
|
|
64
|
+
}
|
|
65
|
+
return rows.map((row) => {
|
|
66
|
+
const result = {};
|
|
67
|
+
for (const column of statement.columns) {
|
|
68
|
+
result[column] = row[column];
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
executeInsert(statement) {
|
|
74
|
+
const table = this.db.getTable(statement.table);
|
|
75
|
+
if (statement.values.length !== table.columns.length) {
|
|
76
|
+
throw new Error("Number of values does not match number of columns");
|
|
77
|
+
}
|
|
78
|
+
const row = {};
|
|
79
|
+
table.columns.forEach((column, index) => {
|
|
80
|
+
row[column] = statement.values[index];
|
|
81
|
+
});
|
|
82
|
+
table.rows.push(row);
|
|
83
|
+
return row;
|
|
84
|
+
}
|
|
85
|
+
executeCreateTable(statement) {
|
|
86
|
+
this.db.addTable(new Table(statement.table, statement.columns, []));
|
|
87
|
+
return {
|
|
88
|
+
success: true
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
executeUpdate(statement) {
|
|
92
|
+
const table = this.db.getTable(statement.table);
|
|
93
|
+
let updated = 0;
|
|
94
|
+
for (const row of table.rows) {
|
|
95
|
+
if (statement.where && !this.expressionEvaluator.evaluate(statement.where, row)) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
for (const assignment of statement.assignments) {
|
|
99
|
+
row[assignment.column] = assignment.value;
|
|
100
|
+
}
|
|
101
|
+
updated++;
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
updated
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
executeDelete(statement) {
|
|
108
|
+
const table = this.db.getTable(statement.table);
|
|
109
|
+
const originalCount = table.rows.length;
|
|
110
|
+
if (!statement.where) {
|
|
111
|
+
table.rows = [];
|
|
112
|
+
} else {
|
|
113
|
+
table.rows = table.rows.filter((row) => !this.expressionEvaluator.evaluate(statement.where, row));
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
deleted: originalCount - table.rows.length
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/lexer/Token.ts
|
|
122
|
+
var TokenType;
|
|
123
|
+
((TokenType2) => {
|
|
124
|
+
TokenType2[TokenType2["Keyword"] = 0] = "Keyword";
|
|
125
|
+
TokenType2[TokenType2["Identifier"] = 1] = "Identifier";
|
|
126
|
+
TokenType2[TokenType2["Semicolon"] = 2] = "Semicolon";
|
|
127
|
+
TokenType2[TokenType2["Asterisk"] = 3] = "Asterisk";
|
|
128
|
+
TokenType2[TokenType2["Number"] = 4] = "Number";
|
|
129
|
+
TokenType2[TokenType2["String"] = 5] = "String";
|
|
130
|
+
TokenType2[TokenType2["Operator"] = 6] = "Operator";
|
|
131
|
+
TokenType2[TokenType2["LeftParen"] = 7] = "LeftParen";
|
|
132
|
+
TokenType2[TokenType2["RightParen"] = 8] = "RightParen";
|
|
133
|
+
TokenType2[TokenType2["Comma"] = 9] = "Comma";
|
|
134
|
+
TokenType2[TokenType2["EOF"] = 10] = "EOF";
|
|
135
|
+
})(TokenType ||= {});
|
|
136
|
+
var keywords = new Set([
|
|
137
|
+
"SELECT",
|
|
138
|
+
"FROM",
|
|
139
|
+
"WHERE",
|
|
140
|
+
"INSERT",
|
|
141
|
+
"INTO",
|
|
142
|
+
"VALUES",
|
|
143
|
+
"UPDATE",
|
|
144
|
+
"DELETE",
|
|
145
|
+
"CREATE",
|
|
146
|
+
"TABLE",
|
|
147
|
+
"SET",
|
|
148
|
+
"DELETE"
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
// src/lexer/Lexer.ts
|
|
152
|
+
class Lexer {
|
|
153
|
+
input;
|
|
154
|
+
position = 0;
|
|
155
|
+
constructor(input) {
|
|
156
|
+
this.input = input;
|
|
157
|
+
}
|
|
158
|
+
tokenize() {
|
|
159
|
+
const tokens = [];
|
|
160
|
+
while (this.position < this.input.length) {
|
|
161
|
+
const char = this.input[this.position];
|
|
162
|
+
if (char === undefined)
|
|
163
|
+
break;
|
|
164
|
+
if (/\s/.test(char)) {
|
|
165
|
+
this.position++;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (/[a-zA-Z_]/.test(char)) {
|
|
169
|
+
tokens.push(this.readWord());
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (/[0-9]/.test(char)) {
|
|
173
|
+
tokens.push(this.readNumber());
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (char === ";") {
|
|
177
|
+
tokens.push({
|
|
178
|
+
type: 2 /* Semicolon */,
|
|
179
|
+
value: ";"
|
|
180
|
+
});
|
|
181
|
+
this.position++;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (char === "*") {
|
|
185
|
+
tokens.push({
|
|
186
|
+
type: 3 /* Asterisk */,
|
|
187
|
+
value: "*"
|
|
188
|
+
});
|
|
189
|
+
this.position++;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (["=", ">", "<"].includes(char)) {
|
|
193
|
+
tokens.push({
|
|
194
|
+
type: 6 /* Operator */,
|
|
195
|
+
value: char
|
|
196
|
+
});
|
|
197
|
+
this.position++;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (char === "'") {
|
|
201
|
+
tokens.push(this.readString());
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (char === "(") {
|
|
205
|
+
tokens.push({
|
|
206
|
+
type: 7 /* LeftParen */,
|
|
207
|
+
value: "("
|
|
208
|
+
});
|
|
209
|
+
this.position++;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (char === ")") {
|
|
213
|
+
tokens.push({
|
|
214
|
+
type: 8 /* RightParen */,
|
|
215
|
+
value: ")"
|
|
216
|
+
});
|
|
217
|
+
this.position++;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (char === ",") {
|
|
221
|
+
tokens.push({
|
|
222
|
+
type: 9 /* Comma */,
|
|
223
|
+
value: ","
|
|
224
|
+
});
|
|
225
|
+
this.position++;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
throw new Error(`Unexpected character: ${char}`);
|
|
229
|
+
}
|
|
230
|
+
tokens.push({
|
|
231
|
+
type: 10 /* EOF */,
|
|
232
|
+
value: ""
|
|
233
|
+
});
|
|
234
|
+
return tokens;
|
|
235
|
+
}
|
|
236
|
+
readWord() {
|
|
237
|
+
let word = "";
|
|
238
|
+
while (this.position < this.input.length) {
|
|
239
|
+
const char = this.input[this.position];
|
|
240
|
+
if (!char || !/[a-zA-Z_]/.test(char)) {
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
word += char;
|
|
244
|
+
this.position++;
|
|
245
|
+
}
|
|
246
|
+
const upper = word.toUpperCase();
|
|
247
|
+
if (keywords.has(upper)) {
|
|
248
|
+
return {
|
|
249
|
+
type: 0 /* Keyword */,
|
|
250
|
+
value: upper
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
type: 1 /* Identifier */,
|
|
255
|
+
value: word
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
readNumber() {
|
|
259
|
+
let number = "";
|
|
260
|
+
while (this.position < this.input.length) {
|
|
261
|
+
const char = this.input[this.position];
|
|
262
|
+
if (!char || !/[0-9]/.test(char))
|
|
263
|
+
break;
|
|
264
|
+
number += char;
|
|
265
|
+
this.position++;
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
type: 4 /* Number */,
|
|
269
|
+
value: number
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
readString() {
|
|
273
|
+
this.position++;
|
|
274
|
+
let value = "";
|
|
275
|
+
while (this.position < this.input.length && this.input[this.position] !== "'") {
|
|
276
|
+
value += this.input[this.position];
|
|
277
|
+
this.position++;
|
|
278
|
+
}
|
|
279
|
+
if (this.position >= this.input.length) {
|
|
280
|
+
throw new Error("Unterminated string literal");
|
|
281
|
+
}
|
|
282
|
+
this.position++;
|
|
283
|
+
return {
|
|
284
|
+
type: 5 /* String */,
|
|
285
|
+
value
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/parser/TokenStream.ts
|
|
291
|
+
class TokenStream {
|
|
292
|
+
tokens;
|
|
293
|
+
position = 0;
|
|
294
|
+
constructor(tokens) {
|
|
295
|
+
this.tokens = tokens;
|
|
296
|
+
}
|
|
297
|
+
current() {
|
|
298
|
+
return this.peek();
|
|
299
|
+
}
|
|
300
|
+
peek(offset = 0) {
|
|
301
|
+
const token = this.tokens[this.position + offset];
|
|
302
|
+
if (!token) {
|
|
303
|
+
throw new Error("Unexpected end of input");
|
|
304
|
+
}
|
|
305
|
+
return token;
|
|
306
|
+
}
|
|
307
|
+
advance() {
|
|
308
|
+
const token = this.current();
|
|
309
|
+
this.position++;
|
|
310
|
+
return token;
|
|
311
|
+
}
|
|
312
|
+
expect(type) {
|
|
313
|
+
const token = this.current();
|
|
314
|
+
if (token.type !== type) {
|
|
315
|
+
throw new Error(`Expected ${TokenType[type]}, got ${TokenType[token.type]}`);
|
|
316
|
+
}
|
|
317
|
+
this.position++;
|
|
318
|
+
return token;
|
|
319
|
+
}
|
|
320
|
+
expectKeyword(keyword) {
|
|
321
|
+
const token = this.expect(0 /* Keyword */);
|
|
322
|
+
if (token.value !== keyword) {
|
|
323
|
+
throw new Error(`Expected ${keyword}, got ${token.value}`);
|
|
324
|
+
}
|
|
325
|
+
return token;
|
|
326
|
+
}
|
|
327
|
+
expectIdentifier() {
|
|
328
|
+
return this.expect(1 /* Identifier */).value;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/parser/Parser.ts
|
|
333
|
+
class Parser {
|
|
334
|
+
tokens;
|
|
335
|
+
stream;
|
|
336
|
+
constructor(tokens) {
|
|
337
|
+
this.tokens = tokens;
|
|
338
|
+
this.stream = new TokenStream(tokens);
|
|
339
|
+
}
|
|
340
|
+
parse() {
|
|
341
|
+
const token = this.stream.current();
|
|
342
|
+
if (token.type !== 0 /* Keyword */) {
|
|
343
|
+
throw new Error("Expected statement");
|
|
344
|
+
}
|
|
345
|
+
switch (token.value) {
|
|
346
|
+
case "SELECT":
|
|
347
|
+
return this.parseSelect();
|
|
348
|
+
case "INSERT":
|
|
349
|
+
return this.parseInsert();
|
|
350
|
+
case "CREATE":
|
|
351
|
+
return this.parseCreateTable();
|
|
352
|
+
case "UPDATE":
|
|
353
|
+
return this.parseUpdate();
|
|
354
|
+
case "DELETE":
|
|
355
|
+
return this.parseDelete();
|
|
356
|
+
default:
|
|
357
|
+
throw new Error(`Unknown statement ${token.value}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
parseSelect() {
|
|
361
|
+
this.expectKeyword("SELECT");
|
|
362
|
+
const columns = this.parseColumns();
|
|
363
|
+
this.expectKeyword("FROM");
|
|
364
|
+
const table = this.expectIdentifier();
|
|
365
|
+
let where;
|
|
366
|
+
if (this.stream.current().type === 0 /* Keyword */ && this.stream.current().value === "WHERE") {
|
|
367
|
+
this.stream.advance();
|
|
368
|
+
where = this.parseExpression();
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
type: "SelectStatement",
|
|
372
|
+
columns,
|
|
373
|
+
table,
|
|
374
|
+
where
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
parseInsert() {
|
|
378
|
+
this.expectKeyword("INSERT");
|
|
379
|
+
this.expectKeyword("INTO");
|
|
380
|
+
const table = this.expectIdentifier();
|
|
381
|
+
this.expectKeyword("VALUES");
|
|
382
|
+
this.expect(7 /* LeftParen */);
|
|
383
|
+
const values = this.parseValues();
|
|
384
|
+
this.expect(8 /* RightParen */);
|
|
385
|
+
return {
|
|
386
|
+
type: "InsertStatement",
|
|
387
|
+
table,
|
|
388
|
+
values
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
parseCreateTable() {
|
|
392
|
+
this.stream.expectKeyword("CREATE");
|
|
393
|
+
this.stream.expectKeyword("TABLE");
|
|
394
|
+
const table = this.stream.expectIdentifier();
|
|
395
|
+
this.stream.expect(7 /* LeftParen */);
|
|
396
|
+
const columns = this.parseIdentifiers();
|
|
397
|
+
this.stream.expect(8 /* RightParen */);
|
|
398
|
+
return {
|
|
399
|
+
type: "CreateTableStatement",
|
|
400
|
+
table,
|
|
401
|
+
columns
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
parseValues() {
|
|
405
|
+
const values = [];
|
|
406
|
+
values.push(this.parseLiteral());
|
|
407
|
+
while (this.stream.current().type === 9 /* Comma */) {
|
|
408
|
+
this.stream.advance();
|
|
409
|
+
values.push(this.parseLiteral());
|
|
410
|
+
}
|
|
411
|
+
return values;
|
|
412
|
+
}
|
|
413
|
+
parseColumns() {
|
|
414
|
+
if (this.stream.current().type === 3 /* Asterisk */) {
|
|
415
|
+
this.stream.advance();
|
|
416
|
+
return ["*"];
|
|
417
|
+
}
|
|
418
|
+
const columns = [];
|
|
419
|
+
columns.push(this.expectIdentifier());
|
|
420
|
+
while (this.stream.current().type === 9 /* Comma */) {
|
|
421
|
+
this.stream.advance();
|
|
422
|
+
columns.push(this.expectIdentifier());
|
|
423
|
+
}
|
|
424
|
+
return columns;
|
|
425
|
+
}
|
|
426
|
+
expectKeyword(keyword) {
|
|
427
|
+
const token = this.expect(0 /* Keyword */);
|
|
428
|
+
if (token.value !== keyword) {
|
|
429
|
+
throw new Error(`Expected ${keyword}, got ${token.value}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
expectIdentifier() {
|
|
433
|
+
return this.expect(1 /* Identifier */).value;
|
|
434
|
+
}
|
|
435
|
+
expect(type) {
|
|
436
|
+
const token = this.stream.current();
|
|
437
|
+
if (token.type !== type) {
|
|
438
|
+
throw new Error(`Expected ${TokenType[type]}, got ${TokenType[token.type]}`);
|
|
439
|
+
}
|
|
440
|
+
this.stream.advance();
|
|
441
|
+
return token;
|
|
442
|
+
}
|
|
443
|
+
parseLiteral() {
|
|
444
|
+
const token = this.stream.current();
|
|
445
|
+
switch (token.type) {
|
|
446
|
+
case 4 /* Number */:
|
|
447
|
+
this.stream.advance();
|
|
448
|
+
return Number(token.value);
|
|
449
|
+
case 5 /* String */:
|
|
450
|
+
this.stream.advance();
|
|
451
|
+
return token.value;
|
|
452
|
+
default:
|
|
453
|
+
throw new Error(`Expected literal, got ${token.value}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
parseExpression() {
|
|
457
|
+
const left = this.expectIdentifier();
|
|
458
|
+
const operator = this.stream.current();
|
|
459
|
+
if (operator.type !== 6 /* Operator */) {
|
|
460
|
+
throw new Error(`Expected operator, got ${operator.value}`);
|
|
461
|
+
}
|
|
462
|
+
this.stream.advance();
|
|
463
|
+
return {
|
|
464
|
+
type: "BinaryExpression",
|
|
465
|
+
left,
|
|
466
|
+
operator: operator.value,
|
|
467
|
+
right: this.parseLiteral()
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
parseIdentifiers() {
|
|
471
|
+
const identifiers = [];
|
|
472
|
+
identifiers.push(this.stream.expectIdentifier());
|
|
473
|
+
while (this.stream.current().type === 9 /* Comma */) {
|
|
474
|
+
this.stream.expect(9 /* Comma */);
|
|
475
|
+
identifiers.push(this.stream.expectIdentifier());
|
|
476
|
+
}
|
|
477
|
+
return identifiers;
|
|
478
|
+
}
|
|
479
|
+
parseUpdate() {
|
|
480
|
+
this.stream.expectKeyword("UPDATE");
|
|
481
|
+
const table = this.stream.expectIdentifier();
|
|
482
|
+
this.stream.expectKeyword("SET");
|
|
483
|
+
const assignments = this.parseAssignments();
|
|
484
|
+
let where;
|
|
485
|
+
if (this.stream.current().value === "WHERE") {
|
|
486
|
+
this.stream.expectKeyword("WHERE");
|
|
487
|
+
where = this.parseExpression();
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
type: "UpdateStatement",
|
|
491
|
+
table,
|
|
492
|
+
assignments,
|
|
493
|
+
where
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
parseAssignments() {
|
|
497
|
+
const assignments = [];
|
|
498
|
+
assignments.push(this.parseAssignment());
|
|
499
|
+
while (this.stream.current().type === 9 /* Comma */) {
|
|
500
|
+
this.stream.expect(9 /* Comma */);
|
|
501
|
+
assignments.push(this.parseAssignment());
|
|
502
|
+
}
|
|
503
|
+
return assignments;
|
|
504
|
+
}
|
|
505
|
+
parseAssignment() {
|
|
506
|
+
const column = this.stream.expectIdentifier();
|
|
507
|
+
this.stream.expect(6 /* Operator */);
|
|
508
|
+
const value = this.parseLiteral();
|
|
509
|
+
return {
|
|
510
|
+
column,
|
|
511
|
+
value
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
parseDelete() {
|
|
515
|
+
this.stream.expectKeyword("DELETE");
|
|
516
|
+
this.stream.expectKeyword("FROM");
|
|
517
|
+
const table = this.stream.expectIdentifier();
|
|
518
|
+
let where;
|
|
519
|
+
if (this.stream.current().value === "WHERE") {
|
|
520
|
+
this.stream.expectKeyword("WHERE");
|
|
521
|
+
where = this.parseExpression();
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
type: "DeleteStatement",
|
|
525
|
+
table,
|
|
526
|
+
where
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/engine/SqlEngine.ts
|
|
532
|
+
class SqlEngine {
|
|
533
|
+
executor;
|
|
534
|
+
constructor(executor) {
|
|
535
|
+
this.executor = executor;
|
|
536
|
+
}
|
|
537
|
+
static start(db) {
|
|
538
|
+
return new SqlEngine(new Executor(db));
|
|
539
|
+
}
|
|
540
|
+
consume(sql) {
|
|
541
|
+
const lexer = new Lexer(sql);
|
|
542
|
+
const tokens = lexer.tokenize();
|
|
543
|
+
const parser = new Parser(tokens);
|
|
544
|
+
const statement = parser.parse();
|
|
545
|
+
return this.executor.execute(statement);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// src/storage/Database.ts
|
|
549
|
+
class Database {
|
|
550
|
+
tables = new Map;
|
|
551
|
+
addTable(table) {
|
|
552
|
+
this.tables.set(table.name, table);
|
|
553
|
+
}
|
|
554
|
+
getTable(name) {
|
|
555
|
+
const table = this.tables.get(name);
|
|
556
|
+
if (!table) {
|
|
557
|
+
throw new Error(`Table ${name} does not exist`);
|
|
558
|
+
}
|
|
559
|
+
return table;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
export {
|
|
563
|
+
Table,
|
|
564
|
+
SqlEngine,
|
|
565
|
+
Database
|
|
566
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare enum TokenType {
|
|
2
|
+
Keyword = 0,
|
|
3
|
+
Identifier = 1,
|
|
4
|
+
Semicolon = 2,
|
|
5
|
+
Asterisk = 3,
|
|
6
|
+
Number = 4,
|
|
7
|
+
String = 5,
|
|
8
|
+
Operator = 6,
|
|
9
|
+
LeftParen = 7,
|
|
10
|
+
RightParen = 8,
|
|
11
|
+
Comma = 9,
|
|
12
|
+
EOF = 10
|
|
13
|
+
}
|
|
14
|
+
export interface Token {
|
|
15
|
+
type: TokenType;
|
|
16
|
+
value: string;
|
|
17
|
+
}
|
|
18
|
+
export declare const keywords: Set<string>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface SelectStatement {
|
|
2
|
+
type: "SelectStatement";
|
|
3
|
+
columns: string[];
|
|
4
|
+
table: string;
|
|
5
|
+
where?: BinaryExpression;
|
|
6
|
+
}
|
|
7
|
+
export interface InsertStatement {
|
|
8
|
+
type: "InsertStatement";
|
|
9
|
+
table: string;
|
|
10
|
+
values: (string | number)[];
|
|
11
|
+
}
|
|
12
|
+
export interface CreateTableStatement {
|
|
13
|
+
type: "CreateTableStatement";
|
|
14
|
+
table: string;
|
|
15
|
+
columns: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface BinaryExpression {
|
|
18
|
+
type: "BinaryExpression";
|
|
19
|
+
left: string;
|
|
20
|
+
operator: string;
|
|
21
|
+
right: string | number;
|
|
22
|
+
}
|
|
23
|
+
export interface Assignment {
|
|
24
|
+
column: string;
|
|
25
|
+
value: string | number;
|
|
26
|
+
}
|
|
27
|
+
export interface UpdateStatement {
|
|
28
|
+
type: "UpdateStatement";
|
|
29
|
+
table: string;
|
|
30
|
+
assignments: Assignment[];
|
|
31
|
+
where?: BinaryExpression;
|
|
32
|
+
}
|
|
33
|
+
export interface DeleteStatement {
|
|
34
|
+
type: "DeleteStatement";
|
|
35
|
+
table: string;
|
|
36
|
+
where?: BinaryExpression;
|
|
37
|
+
}
|
|
38
|
+
export type Statement = SelectStatement | InsertStatement | CreateTableStatement | UpdateStatement | DeleteStatement;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type Token } from "../lexer/Token";
|
|
2
|
+
import type { Statement } from "./AST";
|
|
3
|
+
export declare class Parser {
|
|
4
|
+
private tokens;
|
|
5
|
+
private stream;
|
|
6
|
+
constructor(tokens: Token[]);
|
|
7
|
+
parse(): Statement;
|
|
8
|
+
private parseSelect;
|
|
9
|
+
private parseInsert;
|
|
10
|
+
private parseCreateTable;
|
|
11
|
+
private parseValues;
|
|
12
|
+
private parseColumns;
|
|
13
|
+
private expectKeyword;
|
|
14
|
+
private expectIdentifier;
|
|
15
|
+
private expect;
|
|
16
|
+
private parseLiteral;
|
|
17
|
+
private parseExpression;
|
|
18
|
+
private parseIdentifiers;
|
|
19
|
+
private parseUpdate;
|
|
20
|
+
private parseAssignments;
|
|
21
|
+
private parseAssignment;
|
|
22
|
+
private parseDelete;
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { TokenType, type Token } from "../lexer/Token";
|
|
2
|
+
export declare class TokenStream {
|
|
3
|
+
private tokens;
|
|
4
|
+
private position;
|
|
5
|
+
constructor(tokens: Token[]);
|
|
6
|
+
current(): Token;
|
|
7
|
+
peek(offset?: number): Token;
|
|
8
|
+
advance(): Token;
|
|
9
|
+
expect(type: TokenType): Token;
|
|
10
|
+
expectKeyword(keyword: string): Token;
|
|
11
|
+
expectIdentifier(): string;
|
|
12
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fizzog/sqlx",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "bun run src/cli.ts",
|
|
18
|
+
"test": "bun test",
|
|
19
|
+
"dry": "npm pack --dry-run",
|
|
20
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target node && tsc -p tsconfig.build.json"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/bun": "latest"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"typescript": "^5"
|
|
27
|
+
}
|
|
28
|
+
}
|