toge-db 1.0.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/.env.example +2 -0
- package/LICENSE +7 -0
- package/README.md +197 -0
- package/example-app/.env.example +2 -0
- package/example-app/index.js +17 -0
- package/example-app/package.json +12 -0
- package/index.js +20 -0
- package/package.json +24 -0
- package/src/cli/repl.js +69 -0
- package/src/engine/storage.js +163 -0
- package/src/orm/model.js +74 -0
- package/src/orm/orm.js +18 -0
- package/src/parser/parser.js +215 -0
- package/test.js +177 -0
- package/test_es6_orm.js +44 -0
package/.env.example
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) 2026 M Iqbal Revantama
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Toge-DB ORM Documentation
|
|
2
|
+
|
|
3
|
+
Toge-DB is a lightweight JSON-based database with a simple ORM (Object-Relational Mapping) layer. It is specifically designed to be highly suitable for rapid prototyping and offline applications, such as those built with Electron or other JavaScript frameworks. This documentation explains how to define models, perform queries, and manage data using the TogeORM.
|
|
4
|
+
|
|
5
|
+
> [**WARNING**]
|
|
6
|
+
> **Disclaimer: AI-Generated Software**
|
|
7
|
+
> This library is approximately 99.99% generated by artificial intelligence (AI). While it is designed to provide a lightweight database and ORM solution, we cannot guarantee its reliability or performance in all scenarios. Use this library at your own risk. We are not responsible for any issues or damages that may arise from its use in your application.
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
- [CLI Usage](#cli-usage)
|
|
11
|
+
- [Initialization](#initialization)
|
|
12
|
+
- [Defining Models](#defining-models)
|
|
13
|
+
- [Creating Data](#creating-data)
|
|
14
|
+
- [Querying Data](#querying-data)
|
|
15
|
+
- [Updating Data](#updating-data)
|
|
16
|
+
- [Deleting Data](#deleting-data)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
## CLI Usage
|
|
21
|
+
|
|
22
|
+
TogeDB comes with a raw CLI that supports SQL-like queries. You can start the CLI by running `npm run toge-start`.
|
|
23
|
+
|
|
24
|
+
### Authentication
|
|
25
|
+
Please set the in .env file with the following credentials:
|
|
26
|
+
```
|
|
27
|
+
TOGE_DB_ADMIN_USER=your-username
|
|
28
|
+
TOGE_DB_ADMIN_PASSWORD=your-password
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Supported SQL Commands
|
|
32
|
+
|
|
33
|
+
The CLI supports the following raw commands:
|
|
34
|
+
|
|
35
|
+
#### CREATE TABLE
|
|
36
|
+
```sql
|
|
37
|
+
CREATE TABLE users (username string PRIMARY KEY, email string, age int)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
#### INSERT INTO
|
|
41
|
+
```sql
|
|
42
|
+
INSERT INTO users VALUES ('johndoe', 'john@example.com', 30)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
#### SELECT
|
|
46
|
+
Supports column selection, `WHERE` clause (simple `col=val`), and `LIMIT`.
|
|
47
|
+
```sql
|
|
48
|
+
SELECT * FROM users
|
|
49
|
+
SELECT username, email FROM users WHERE age=30
|
|
50
|
+
SELECT * FROM users LIMIT 5
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
#### UPDATE
|
|
54
|
+
```sql
|
|
55
|
+
UPDATE users SET age=31 WHERE username='johndoe'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
#### DELETE
|
|
59
|
+
```sql
|
|
60
|
+
DELETE FROM users WHERE username='johndoe'
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### DROP TABLE
|
|
64
|
+
```sql
|
|
65
|
+
DROP TABLE users
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
#### ALTER TABLE
|
|
69
|
+
```sql
|
|
70
|
+
ALTER TABLE users ADD bio string
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Initialization
|
|
76
|
+
|
|
77
|
+
To start using TogeDB ORM, initialize the `TogeORM` with a directory path where your data will be stored.
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
import { TogeORM } from 'toge-db';
|
|
81
|
+
const orm = new TogeORM('./data');
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Defining Models
|
|
85
|
+
|
|
86
|
+
Use `orm.define(modelName, schema)` to create a new model. If no primary key is defined in the schema, an auto-incrementing `id` field will be added automatically.
|
|
87
|
+
|
|
88
|
+
### Schema Options
|
|
89
|
+
- `type`: Data type (e.g., 'string', 'int').
|
|
90
|
+
- `primaryKey`: Boolean, marks the column as a primary key.
|
|
91
|
+
- `autoIncrement`: Boolean, enables auto-increment for the column.
|
|
92
|
+
|
|
93
|
+
```javascript
|
|
94
|
+
const User = orm.define('user', {
|
|
95
|
+
username: { type: 'string', primaryKey: true },
|
|
96
|
+
email: { type: 'string' },
|
|
97
|
+
age: { type: 'int' }
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const Post = orm.define('post', {
|
|
101
|
+
title: { type: 'string' },
|
|
102
|
+
content: { type: 'string' }
|
|
103
|
+
}); // Automatically adds an 'id' primary key
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Creating Data
|
|
109
|
+
|
|
110
|
+
There are two ways to create and persist data:
|
|
111
|
+
|
|
112
|
+
### 1. Using `Model.create()`
|
|
113
|
+
Directly creates and saves a new record.
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
const newUser = User.create({
|
|
117
|
+
username: 'johndoe',
|
|
118
|
+
email: 'john@example.com',
|
|
119
|
+
age: 30
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 2. Using `new Model()` and `save()`
|
|
124
|
+
Instantiate a model and save it later.
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
const post = new Post({
|
|
128
|
+
title: 'Hello TogeDB',
|
|
129
|
+
content: 'This is my first post.'
|
|
130
|
+
});
|
|
131
|
+
post.save();
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Querying Data
|
|
137
|
+
|
|
138
|
+
TogeDB uses JavaScript functions as conditions for querying.
|
|
139
|
+
|
|
140
|
+
### Find All
|
|
141
|
+
`Model.find(condition)` returns an array of model instances. If no condition is provided, it returns all records.
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
// Find all users
|
|
145
|
+
const allUsers = User.find();
|
|
146
|
+
|
|
147
|
+
// Find users older than 25
|
|
148
|
+
const seniors = User.find(user => user.age > 25);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Find One
|
|
152
|
+
`Model.findOne(condition)` returns the first matching model instance or `null`.
|
|
153
|
+
|
|
154
|
+
```javascript
|
|
155
|
+
const user = User.findOne(u => u.username === 'johndoe');
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Updating Data
|
|
161
|
+
|
|
162
|
+
### 1. Static Update
|
|
163
|
+
Update multiple records matching a condition.
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
User.update(
|
|
167
|
+
user => user.username === 'johndoe',
|
|
168
|
+
{ age: 31 }
|
|
169
|
+
);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### 2. Instance Update
|
|
173
|
+
Modify an instance and call `save()`. This requires the model to have a primary key.
|
|
174
|
+
|
|
175
|
+
```javascript
|
|
176
|
+
const user = User.findOne(u => u.username === 'johndoe');
|
|
177
|
+
if (user) {
|
|
178
|
+
user.email = 'newjohn@example.com';
|
|
179
|
+
user.save();
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Deleting Data
|
|
186
|
+
|
|
187
|
+
Use `Model.delete(condition)` to remove records from the database.
|
|
188
|
+
|
|
189
|
+
```javascript
|
|
190
|
+
// Delete a specific user
|
|
191
|
+
User.delete(user => user.username === 'johndoe');
|
|
192
|
+
|
|
193
|
+
// Delete all posts
|
|
194
|
+
Post.delete(() => true);
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { TogeORM } from 'toge-db';
|
|
2
|
+
const orm = new TogeORM('./data');
|
|
3
|
+
|
|
4
|
+
const User = orm.define('user', {
|
|
5
|
+
username: { type: 'string', primaryKey: true },
|
|
6
|
+
email: { type: 'string' },
|
|
7
|
+
age: { type: 'int' }
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const newUser = User.create({
|
|
11
|
+
username: 'johntest',
|
|
12
|
+
email: 'john@example.com',
|
|
13
|
+
age: 30
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const allUsers = User.find();
|
|
17
|
+
console.log('New user:', allUsers);
|
package/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import Storage from './src/engine/storage.js';
|
|
2
|
+
import Parser from './src/parser/parser.js';
|
|
3
|
+
import CLI from './src/cli/repl.js';
|
|
4
|
+
import TogeORM from './src/orm/orm.js';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
Storage,
|
|
9
|
+
Parser,
|
|
10
|
+
CLI,
|
|
11
|
+
TogeORM
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Only start CLI if this file is run directly
|
|
15
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
16
|
+
const storage = new Storage();
|
|
17
|
+
const parser = new Parser(storage);
|
|
18
|
+
const cli = new CLI(parser);
|
|
19
|
+
cli.start();
|
|
20
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "toge-db",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Toge-DB is a lightweight JSON-based database with a simple ORM (Object-Relational Mapping) layer. It is specifically designed to be highly suitable for rapid prototyping and offline applications, such as those built with Electron or other JavaScript frameworks. This documentation explains how to define models, perform queries, and manage data using the TogeORM.\n",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"database",
|
|
7
|
+
"sql"
|
|
8
|
+
],
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "Revtm",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "index.js",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"toge-start": "node index.js",
|
|
15
|
+
"toge-test": "node test.js"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/Revtm/toge-db"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"dotenv": "^17.2.3"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/cli/repl.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
import dotenv from 'dotenv';
|
|
3
|
+
dotenv.config();
|
|
4
|
+
|
|
5
|
+
class CLI {
|
|
6
|
+
constructor(parser) {
|
|
7
|
+
this.parser = parser;
|
|
8
|
+
this.rl = readline.createInterface({
|
|
9
|
+
input: process.stdin,
|
|
10
|
+
output: process.stdout,
|
|
11
|
+
prompt: 'Toge-DB> '
|
|
12
|
+
});
|
|
13
|
+
this.authenticated = false;
|
|
14
|
+
|
|
15
|
+
const adminUser = process.env.Toge_DB_ADMIN_USER;
|
|
16
|
+
const adminPass = process.env.Toge_DB_ADMIN_PASSWORD;
|
|
17
|
+
this.credentials = { [adminUser]: adminPass };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
start() {
|
|
21
|
+
console.log('Welcome to Toge-DB');
|
|
22
|
+
this.login();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
login() {
|
|
26
|
+
this.rl.question('Username: ', (username) => {
|
|
27
|
+
this.rl.question('Password: ', (password) => {
|
|
28
|
+
if (this.credentials[username] && this.credentials[username] === password) {
|
|
29
|
+
this.authenticated = true;
|
|
30
|
+
console.log('Login successful.');
|
|
31
|
+
this.runREPL();
|
|
32
|
+
} else {
|
|
33
|
+
console.log('Invalid credentials.');
|
|
34
|
+
this.login();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
runREPL() {
|
|
41
|
+
this.rl.prompt();
|
|
42
|
+
this.rl.on('line', (line) => {
|
|
43
|
+
const query = line.trim();
|
|
44
|
+
if (query.toLowerCase() === 'exit' || query.toLowerCase() === 'quit') {
|
|
45
|
+
this.rl.close();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (query) {
|
|
50
|
+
try {
|
|
51
|
+
const result = this.parser.execute(query);
|
|
52
|
+
if (Array.isArray(result)) {
|
|
53
|
+
console.table(result);
|
|
54
|
+
} else {
|
|
55
|
+
console.log(result);
|
|
56
|
+
}
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error(`Error: ${err.message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
this.rl.prompt();
|
|
62
|
+
}).on('close', () => {
|
|
63
|
+
console.log('Goodbye!');
|
|
64
|
+
process.exit(0);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default CLI;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
class Storage {
|
|
5
|
+
constructor(dataDir = 'data') {
|
|
6
|
+
this.dataDir = path.resolve(dataDir);
|
|
7
|
+
if (!fs.existsSync(this.dataDir)) {
|
|
8
|
+
fs.mkdirSync(this.dataDir);
|
|
9
|
+
}
|
|
10
|
+
this.metadataPath = path.join(this.dataDir, 'metadata.json');
|
|
11
|
+
this.metadata = this.loadMetadata();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
loadMetadata() {
|
|
15
|
+
if (fs.existsSync(this.metadataPath)) {
|
|
16
|
+
return JSON.parse(fs.readFileSync(this.metadataPath, 'utf8'));
|
|
17
|
+
}
|
|
18
|
+
return { tables: {} };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
saveMetadata() {
|
|
22
|
+
fs.writeFileSync(this.metadataPath, JSON.stringify(this.metadata, null, 2));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getTablePath(tableName) {
|
|
26
|
+
return path.join(this.dataDir, `${tableName}.json`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
createTable(tableName, schema) {
|
|
30
|
+
if (this.metadata.tables[tableName]) {
|
|
31
|
+
throw new Error(`Table "${tableName}" already exists.`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check for primary key, if not exists add auto-incrementing 'id'
|
|
35
|
+
let hasPK = Object.values(schema).some(col => col.primaryKey);
|
|
36
|
+
if (!hasPK) {
|
|
37
|
+
schema = { id: { type: 'int', primaryKey: true, autoIncrement: true }, ...schema };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.metadata.tables[tableName] = {
|
|
41
|
+
schema,
|
|
42
|
+
nextId: 1
|
|
43
|
+
};
|
|
44
|
+
this.saveMetadata();
|
|
45
|
+
fs.writeFileSync(this.getTablePath(tableName), JSON.stringify([], null, 2));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
dropTable(tableName) {
|
|
49
|
+
if (!this.metadata.tables[tableName]) {
|
|
50
|
+
throw new Error(`Table "${tableName}" does not exist.`);
|
|
51
|
+
}
|
|
52
|
+
delete this.metadata.tables[tableName];
|
|
53
|
+
this.saveMetadata();
|
|
54
|
+
const tablePath = this.getTablePath(tableName);
|
|
55
|
+
if (fs.existsSync(tablePath)) {
|
|
56
|
+
fs.unlinkSync(tablePath);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
loadRows(tableName) {
|
|
61
|
+
if (!this.metadata.tables[tableName]) {
|
|
62
|
+
throw new Error(`Table "${tableName}" does not exist.`);
|
|
63
|
+
}
|
|
64
|
+
const tablePath = this.getTablePath(tableName);
|
|
65
|
+
return JSON.parse(fs.readFileSync(tablePath, 'utf8'));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
saveRows(tableName, rows) {
|
|
69
|
+
fs.writeFileSync(this.getTablePath(tableName), JSON.stringify(rows, null, 2));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
insert(tableName, rowData) {
|
|
73
|
+
const tableMeta = this.metadata.tables[tableName];
|
|
74
|
+
if (!tableMeta) throw new Error(`Table "${tableName}" does not exist.`);
|
|
75
|
+
|
|
76
|
+
const rows = this.loadRows(tableName);
|
|
77
|
+
const newRow = {};
|
|
78
|
+
|
|
79
|
+
// Prepare new row and handle auto-increment
|
|
80
|
+
for (const [colName, colSpec] of Object.entries(tableMeta.schema)) {
|
|
81
|
+
if (colSpec.autoIncrement) {
|
|
82
|
+
newRow[colName] = tableMeta.nextId++;
|
|
83
|
+
} else {
|
|
84
|
+
newRow[colName] = rowData[colName] !== undefined ? rowData[colName] : null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Enforce primary key uniqueness
|
|
89
|
+
const pkColumns = Object.entries(tableMeta.schema)
|
|
90
|
+
.filter(([_, spec]) => spec.primaryKey)
|
|
91
|
+
.map(([name, _]) => name);
|
|
92
|
+
|
|
93
|
+
if (pkColumns.length > 0) {
|
|
94
|
+
const isDuplicate = rows.some(row =>
|
|
95
|
+
pkColumns.every(col => String(row[col]) === String(newRow[col]))
|
|
96
|
+
);
|
|
97
|
+
if (isDuplicate) {
|
|
98
|
+
throw new Error(`Duplicate entry for primary key.`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
rows.push(newRow);
|
|
103
|
+
this.saveRows(tableName, rows);
|
|
104
|
+
this.saveMetadata();
|
|
105
|
+
return newRow;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
update(tableName, condition, newData) {
|
|
109
|
+
const tableMeta = this.metadata.tables[tableName];
|
|
110
|
+
if (!tableMeta) throw new Error(`Table "${tableName}" does not exist.`);
|
|
111
|
+
|
|
112
|
+
let rows = this.loadRows(tableName);
|
|
113
|
+
let updatedCount = 0;
|
|
114
|
+
|
|
115
|
+
rows = rows.map(row => {
|
|
116
|
+
if (condition(row)) {
|
|
117
|
+
updatedCount++;
|
|
118
|
+
return { ...row, ...newData };
|
|
119
|
+
}
|
|
120
|
+
return row;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
this.saveRows(tableName, rows);
|
|
124
|
+
return updatedCount;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
delete(tableName, condition) {
|
|
128
|
+
if (!this.metadata.tables[tableName]) throw new Error(`Table "${tableName}" does not exist.`);
|
|
129
|
+
|
|
130
|
+
let rows = this.loadRows(tableName);
|
|
131
|
+
const initialCount = rows.length;
|
|
132
|
+
rows = rows.filter(row => !condition(row));
|
|
133
|
+
|
|
134
|
+
this.saveRows(tableName, rows);
|
|
135
|
+
return initialCount - rows.length;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
select(tableName, condition = () => true) {
|
|
139
|
+
const rows = this.loadRows(tableName);
|
|
140
|
+
return rows.filter(condition);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
alterTable(tableName, action, details) {
|
|
144
|
+
const tableMeta = this.metadata.tables[tableName];
|
|
145
|
+
if (!tableMeta) throw new Error(`Table "${tableName}" does not exist.`);
|
|
146
|
+
|
|
147
|
+
if (action === 'ADD_COLUMN') {
|
|
148
|
+
const { colName, colSpec } = details;
|
|
149
|
+
if (tableMeta.schema[colName]) throw new Error(`Column "${colName}" already exists.`);
|
|
150
|
+
|
|
151
|
+
tableMeta.schema[colName] = colSpec;
|
|
152
|
+
this.saveMetadata();
|
|
153
|
+
|
|
154
|
+
const rows = this.loadRows(tableName);
|
|
155
|
+
const updatedRows = rows.map(row => ({ ...row, [colName]: null }));
|
|
156
|
+
this.saveRows(tableName, updatedRows);
|
|
157
|
+
} else {
|
|
158
|
+
throw new Error(`Action "${action}" not supported for ALTER TABLE.`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export default Storage;
|
package/src/orm/model.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
class Model {
|
|
2
|
+
constructor(data = {}) {
|
|
3
|
+
Object.assign(this, data);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
static init(tableName, schema, storage) {
|
|
7
|
+
this.tableName = tableName;
|
|
8
|
+
this.schema = schema;
|
|
9
|
+
this.storage = storage;
|
|
10
|
+
|
|
11
|
+
// Create table if it doesn't exist
|
|
12
|
+
try {
|
|
13
|
+
this.storage.createTable(this.tableName, this.schema);
|
|
14
|
+
} catch (e) {
|
|
15
|
+
// Ignore if table already exists
|
|
16
|
+
if (!e.message.includes('already exists')) {
|
|
17
|
+
throw e;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static create(data) {
|
|
23
|
+
const result = this.storage.insert(this.tableName, data);
|
|
24
|
+
return new this(result);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static find(condition = () => true) {
|
|
28
|
+
const results = this.storage.select(this.tableName, condition);
|
|
29
|
+
return results.map(row => new this(row));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static findOne(condition) {
|
|
33
|
+
const results = this.find(condition);
|
|
34
|
+
return results.length > 0 ? results[0] : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
save() {
|
|
38
|
+
const tableMeta = this.constructor.storage.metadata.tables[this.constructor.tableName];
|
|
39
|
+
const pkColumns = Object.entries(tableMeta.schema)
|
|
40
|
+
.filter(([_, spec]) => spec.primaryKey)
|
|
41
|
+
.map(([name, _]) => name);
|
|
42
|
+
|
|
43
|
+
if (pkColumns.length === 0) {
|
|
44
|
+
throw new Error('Cannot save model without primary key');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check if all primary keys are present
|
|
48
|
+
const condition = (row) => pkColumns.every(col => row[col] === this[col]);
|
|
49
|
+
|
|
50
|
+
// Try to find if it exists
|
|
51
|
+
const existing = this.constructor.storage.select(this.constructor.tableName, condition);
|
|
52
|
+
|
|
53
|
+
if (existing.length > 0) {
|
|
54
|
+
// Update
|
|
55
|
+
const dataToUpdate = { ...this };
|
|
56
|
+
// Remove storage and metadata from data if they were somehow attached
|
|
57
|
+
this.constructor.storage.update(this.constructor.tableName, condition, dataToUpdate);
|
|
58
|
+
} else {
|
|
59
|
+
// Insert
|
|
60
|
+
const result = this.constructor.storage.insert(this.constructor.tableName, this);
|
|
61
|
+
Object.assign(this, result);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static update(condition, newData) {
|
|
66
|
+
return this.storage.update(this.tableName, condition, newData);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static delete(condition) {
|
|
70
|
+
return this.storage.delete(this.tableName, condition);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default Model;
|
package/src/orm/orm.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import Storage from '../engine/storage.js';
|
|
2
|
+
import Model from './model.js';
|
|
3
|
+
|
|
4
|
+
class TogeORM {
|
|
5
|
+
constructor(dataDir) {
|
|
6
|
+
this.storage = new Storage(dataDir);
|
|
7
|
+
this.models = {};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
define(modelName, schema) {
|
|
11
|
+
const NewModel = class extends Model {};
|
|
12
|
+
NewModel.init(modelName, schema, this.storage);
|
|
13
|
+
this.models[modelName] = NewModel;
|
|
14
|
+
return NewModel;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default TogeORM;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
class Parser {
|
|
2
|
+
constructor(storage) {
|
|
3
|
+
this.storage = storage;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
execute(query) {
|
|
7
|
+
query = query.trim();
|
|
8
|
+
if (query.toUpperCase().startsWith('CREATE TABLE')) {
|
|
9
|
+
return this.handleCreateTable(query);
|
|
10
|
+
} else if (query.toUpperCase().startsWith('INSERT INTO')) {
|
|
11
|
+
return this.handleInsert(query);
|
|
12
|
+
} else if (query.toUpperCase().startsWith('SELECT')) {
|
|
13
|
+
return this.handleSelect(query);
|
|
14
|
+
} else if (query.toUpperCase().startsWith('UPDATE')) {
|
|
15
|
+
return this.handleUpdate(query);
|
|
16
|
+
} else if (query.toUpperCase().startsWith('DELETE FROM')) {
|
|
17
|
+
return this.handleDelete(query);
|
|
18
|
+
} else if (query.toUpperCase().startsWith('DROP TABLE')) {
|
|
19
|
+
return this.handleDropTable(query);
|
|
20
|
+
} else if (query.toUpperCase().startsWith('ALTER TABLE')) {
|
|
21
|
+
return this.handleAlterTable(query);
|
|
22
|
+
} else {
|
|
23
|
+
throw new Error('Unsupported command.');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
handleCreateTable(query) {
|
|
28
|
+
// CREATE TABLE <table_name> (<col1> <type> [PRIMARY KEY], ..., [PRIMARY KEY (<col1>, <col2>)])
|
|
29
|
+
const match = query.match(/CREATE TABLE (\w+) \((.*)\)/i);
|
|
30
|
+
if (!match) throw new Error('Invalid CREATE TABLE syntax.');
|
|
31
|
+
|
|
32
|
+
const tableName = match[1];
|
|
33
|
+
const columnsStr = match[2];
|
|
34
|
+
const schema = {};
|
|
35
|
+
let compositePK = null;
|
|
36
|
+
|
|
37
|
+
// Split by comma but ignore commas inside parentheses (for composite PK)
|
|
38
|
+
const parts = [];
|
|
39
|
+
let currentPart = '';
|
|
40
|
+
let bracketLevel = 0;
|
|
41
|
+
for (let i = 0; i < columnsStr.length; i++) {
|
|
42
|
+
const char = columnsStr[i];
|
|
43
|
+
if (char === '(') bracketLevel++;
|
|
44
|
+
if (char === ')') bracketLevel--;
|
|
45
|
+
if (char === ',' && bracketLevel === 0) {
|
|
46
|
+
parts.push(currentPart.trim());
|
|
47
|
+
currentPart = '';
|
|
48
|
+
} else {
|
|
49
|
+
currentPart += char;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
parts.push(currentPart.trim());
|
|
53
|
+
|
|
54
|
+
parts.forEach(part => {
|
|
55
|
+
if (part.toUpperCase().startsWith('PRIMARY KEY')) {
|
|
56
|
+
const pkMatch = part.match(/PRIMARY KEY\s*\((.*)\)/i);
|
|
57
|
+
if (pkMatch) {
|
|
58
|
+
compositePK = pkMatch[1].split(',').map(c => c.trim());
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
const segments = part.split(/\s+/);
|
|
62
|
+
const name = segments[0];
|
|
63
|
+
const type = segments[1];
|
|
64
|
+
const isPK = part.toUpperCase().includes('PRIMARY KEY');
|
|
65
|
+
schema[name] = { type: type.toLowerCase() };
|
|
66
|
+
if (isPK) {
|
|
67
|
+
schema[name].primaryKey = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (compositePK) {
|
|
73
|
+
compositePK.forEach(colName => {
|
|
74
|
+
if (schema[colName]) {
|
|
75
|
+
schema[colName].primaryKey = true;
|
|
76
|
+
} else {
|
|
77
|
+
throw new Error(`Primary key column "${colName}" not found in schema.`);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.storage.createTable(tableName, schema);
|
|
83
|
+
return `Table "${tableName}" created successfully.`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
handleInsert(query) {
|
|
87
|
+
// INSERT INTO <table_name> VALUES (...)
|
|
88
|
+
// Simplified: assuming values are in same order as schema (excluding auto-increment)
|
|
89
|
+
const match = query.match(/INSERT INTO (\w+) VALUES \((.*)\)/i);
|
|
90
|
+
if (!match) throw new Error('Invalid INSERT INTO syntax.');
|
|
91
|
+
|
|
92
|
+
const tableName = match[1];
|
|
93
|
+
const valuesStr = match[2];
|
|
94
|
+
const values = valuesStr.split(',').map(v => v.trim().replace(/^'|'$/g, ''));
|
|
95
|
+
|
|
96
|
+
const tableMeta = this.storage.metadata.tables[tableName];
|
|
97
|
+
if (!tableMeta) throw new Error(`Table "${tableName}" does not exist.`);
|
|
98
|
+
|
|
99
|
+
const rowData = {};
|
|
100
|
+
const colNames = Object.keys(tableMeta.schema).filter(name => !tableMeta.schema[name].autoIncrement);
|
|
101
|
+
|
|
102
|
+
colNames.forEach((name, index) => {
|
|
103
|
+
rowData[name] = values[index];
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const newRow = this.storage.insert(tableName, rowData);
|
|
107
|
+
return `Row inserted into "${tableName}": ${JSON.stringify(newRow)}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
handleSelect(query) {
|
|
111
|
+
// SELECT <columns> FROM <table_name> [WHERE <condition>] [LIMIT <number>]
|
|
112
|
+
const match = query.match(/SELECT\s+(.*?)\s+FROM\s+(\w+)(?:\s+WHERE\s+(.*?))?(?:\s+LIMIT\s+(\d+))?$/i);
|
|
113
|
+
if (!match) throw new Error('Invalid SELECT syntax.');
|
|
114
|
+
|
|
115
|
+
const columnsStr = match[1].trim();
|
|
116
|
+
const tableName = match[2];
|
|
117
|
+
const whereClause = match[3];
|
|
118
|
+
const limit = match[4] ? parseInt(match[4], 10) : null;
|
|
119
|
+
|
|
120
|
+
let condition = () => true;
|
|
121
|
+
if (whereClause) {
|
|
122
|
+
condition = this.parseCondition(whereClause);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let rows = this.storage.select(tableName, condition);
|
|
126
|
+
|
|
127
|
+
// Handle columns
|
|
128
|
+
if (columnsStr !== '*') {
|
|
129
|
+
const columns = columnsStr.split(',').map(c => c.trim());
|
|
130
|
+
rows = rows.map(row => {
|
|
131
|
+
const filteredRow = {};
|
|
132
|
+
columns.forEach(col => {
|
|
133
|
+
filteredRow[col] = row[col];
|
|
134
|
+
});
|
|
135
|
+
return filteredRow;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Handle limit
|
|
140
|
+
if (limit !== null) {
|
|
141
|
+
rows = rows.slice(0, limit);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return rows;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
handleUpdate(query) {
|
|
148
|
+
// UPDATE <table_name> SET <col>=<val> WHERE <condition>
|
|
149
|
+
const match = query.match(/UPDATE (\w+) SET (\w+)=['"]?(.*?)['"]? WHERE (.*)/i);
|
|
150
|
+
if (!match) throw new Error('Invalid UPDATE syntax.');
|
|
151
|
+
|
|
152
|
+
const tableName = match[1];
|
|
153
|
+
const colName = match[2];
|
|
154
|
+
const newVal = match[3];
|
|
155
|
+
const whereClause = match[4];
|
|
156
|
+
|
|
157
|
+
const condition = this.parseCondition(whereClause);
|
|
158
|
+
const count = this.storage.update(tableName, condition, { [colName]: newVal });
|
|
159
|
+
|
|
160
|
+
return `${count} row(s) updated in "${tableName}".`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
handleDelete(query) {
|
|
164
|
+
// DELETE FROM <table_name> WHERE <condition>
|
|
165
|
+
const match = query.match(/DELETE FROM (\w+) WHERE (.*)/i);
|
|
166
|
+
if (!match) throw new Error('Invalid DELETE syntax.');
|
|
167
|
+
|
|
168
|
+
const tableName = match[1];
|
|
169
|
+
const whereClause = match[2];
|
|
170
|
+
|
|
171
|
+
const condition = this.parseCondition(whereClause);
|
|
172
|
+
const count = this.storage.delete(tableName, condition);
|
|
173
|
+
|
|
174
|
+
return `${count} row(s) deleted from "${tableName}".`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
handleDropTable(query) {
|
|
178
|
+
// DROP TABLE <table_name>
|
|
179
|
+
const match = query.match(/DROP TABLE (\w+)/i);
|
|
180
|
+
if (!match) throw new Error('Invalid DROP TABLE syntax.');
|
|
181
|
+
|
|
182
|
+
const tableName = match[1];
|
|
183
|
+
this.storage.dropTable(tableName);
|
|
184
|
+
return `Table "${tableName}" dropped.`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
handleAlterTable(query) {
|
|
188
|
+
// ALTER TABLE <table_name> ADD <col> <type>
|
|
189
|
+
const match = query.match(/ALTER TABLE (\w+) ADD (\w+) (\w+)/i);
|
|
190
|
+
if (!match) throw new Error('Invalid ALTER TABLE syntax.');
|
|
191
|
+
|
|
192
|
+
const tableName = match[1];
|
|
193
|
+
const colName = match[2];
|
|
194
|
+
const colType = match[3];
|
|
195
|
+
|
|
196
|
+
this.storage.alterTable(tableName, 'ADD_COLUMN', { colName, colSpec: { type: colType.toLowerCase() } });
|
|
197
|
+
return `Table "${tableName}" altered: column "${colName}" added.`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
parseCondition(whereClause) {
|
|
201
|
+
// Simple condition parser: col=val
|
|
202
|
+
const match = whereClause.match(/(\w+)\s*=\s*(.*)$/);
|
|
203
|
+
if (!match) throw new Error('Invalid WHERE clause syntax (only col=val supported).');
|
|
204
|
+
|
|
205
|
+
const col = match[1];
|
|
206
|
+
let val = match[2].trim();
|
|
207
|
+
|
|
208
|
+
// Remove surrounding quotes if any
|
|
209
|
+
val = val.replace(/^['"](.*)['"]$/, '$1');
|
|
210
|
+
|
|
211
|
+
return (row) => String(row[col]) === String(val);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export default Parser;
|
package/test.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import Storage from './src/engine/storage.js';
|
|
2
|
+
import Parser from './src/parser/parser.js';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
async function runTests() {
|
|
7
|
+
console.log('Running All Tests...');
|
|
8
|
+
const testDataDir = 'test_data';
|
|
9
|
+
|
|
10
|
+
function cleanup() {
|
|
11
|
+
if (fs.existsSync(testDataDir)) {
|
|
12
|
+
fs.rmSync(testDataDir, { recursive: true, force: true });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
cleanup();
|
|
17
|
+
|
|
18
|
+
const storage = new Storage(testDataDir);
|
|
19
|
+
const parser = new Parser(storage);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// --- Core Functionality Tests (from original test.js) ---
|
|
23
|
+
console.log('\n--- Core Functionality Tests ---');
|
|
24
|
+
|
|
25
|
+
// Test CREATE TABLE
|
|
26
|
+
console.log('Testing CREATE TABLE...');
|
|
27
|
+
parser.execute('CREATE TABLE users (name string, age int)');
|
|
28
|
+
if (!storage.metadata.tables.users) throw new Error('Table users not created');
|
|
29
|
+
if (!storage.metadata.tables.users.schema.id) throw new Error('Auto-increment ID not added');
|
|
30
|
+
console.log('✓ CREATE TABLE passed');
|
|
31
|
+
|
|
32
|
+
// Test INSERT
|
|
33
|
+
console.log('Testing INSERT...');
|
|
34
|
+
parser.execute("INSERT INTO users VALUES ('Alice', 30)");
|
|
35
|
+
parser.execute("INSERT INTO users VALUES ('Bob', 25)");
|
|
36
|
+
const users = storage.loadRows('users');
|
|
37
|
+
if (users.length !== 2) throw new Error('Rows not inserted correctly');
|
|
38
|
+
if (users[0].id !== 1 || users[1].id !== 2) throw new Error('Auto-increment ID mismatch');
|
|
39
|
+
console.log('✓ INSERT passed');
|
|
40
|
+
|
|
41
|
+
// Test SELECT (Simple)
|
|
42
|
+
console.log('Testing SELECT...');
|
|
43
|
+
const result = parser.execute('SELECT * FROM users WHERE name=Alice');
|
|
44
|
+
if (result.length !== 1 || result[0].name !== 'Alice') throw new Error('SELECT failed');
|
|
45
|
+
console.log('✓ SELECT passed');
|
|
46
|
+
|
|
47
|
+
// Test UPDATE
|
|
48
|
+
console.log('Testing UPDATE...');
|
|
49
|
+
parser.execute("UPDATE users SET age=31 WHERE name=Alice");
|
|
50
|
+
const alice = storage.select('users', (r) => r.name === 'Alice')[0];
|
|
51
|
+
if (alice.age !== '31') throw new Error('UPDATE failed'); // Parser converts to string currently
|
|
52
|
+
console.log('✓ UPDATE passed');
|
|
53
|
+
|
|
54
|
+
// Test ALTER TABLE
|
|
55
|
+
console.log('Testing ALTER TABLE...');
|
|
56
|
+
parser.execute("ALTER TABLE users ADD email string");
|
|
57
|
+
if (!storage.metadata.tables.users.schema.email) throw new Error('ALTER TABLE failed');
|
|
58
|
+
const updatedUsers = storage.loadRows('users');
|
|
59
|
+
if (updatedUsers[0].email !== null) throw new Error('ALTER TABLE did not update existing rows');
|
|
60
|
+
console.log('✓ ALTER TABLE passed');
|
|
61
|
+
|
|
62
|
+
// Test DELETE
|
|
63
|
+
console.log('Testing DELETE...');
|
|
64
|
+
parser.execute("DELETE FROM users WHERE name=Bob");
|
|
65
|
+
if (storage.loadRows('users').length !== 1) throw new Error('DELETE failed');
|
|
66
|
+
console.log('✓ DELETE passed');
|
|
67
|
+
|
|
68
|
+
// Test DROP TABLE
|
|
69
|
+
console.log('Testing DROP TABLE...');
|
|
70
|
+
parser.execute("DROP TABLE users");
|
|
71
|
+
if (storage.metadata.tables.users) throw new Error('DROP TABLE failed');
|
|
72
|
+
console.log('✓ DROP TABLE passed');
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
// --- Primary Key Tests (from test_pk.js) ---
|
|
76
|
+
console.log('\n--- Primary Key Tests ---');
|
|
77
|
+
|
|
78
|
+
console.log("Test: Auto-increment primary key when none specified");
|
|
79
|
+
parser.execute("CREATE TABLE test1 (name TEXT)");
|
|
80
|
+
parser.execute("INSERT INTO test1 VALUES ('Alice')");
|
|
81
|
+
parser.execute("INSERT INTO test1 VALUES ('Bob')");
|
|
82
|
+
const rows1 = parser.execute("SELECT * FROM test1");
|
|
83
|
+
if (rows1[0].id === 1 && rows1[1].id === 2) {
|
|
84
|
+
console.log("✓ Auto-increment id added");
|
|
85
|
+
} else {
|
|
86
|
+
throw new Error("Auto-increment id missing or incorrect");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log("Test: Single column primary key");
|
|
90
|
+
parser.execute("CREATE TABLE test2 (email TEXT PRIMARY KEY, name TEXT)");
|
|
91
|
+
parser.execute("INSERT INTO test2 VALUES ('alice@example.com', 'Alice')");
|
|
92
|
+
try {
|
|
93
|
+
parser.execute("INSERT INTO test2 VALUES ('alice@example.com', 'Alice Duplicate')");
|
|
94
|
+
throw new Error("Duplicate primary key allowed");
|
|
95
|
+
} catch (e) {
|
|
96
|
+
console.log("✓ Duplicate primary key rejected: " + e.message);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log("Test: Composite primary key");
|
|
100
|
+
parser.execute("CREATE TABLE test3 (order_id INT, item_id INT, PRIMARY KEY (order_id, item_id))");
|
|
101
|
+
parser.execute("INSERT INTO test3 VALUES (1, 101)");
|
|
102
|
+
parser.execute("INSERT INTO test3 VALUES (1, 102)");
|
|
103
|
+
parser.execute("INSERT INTO test3 VALUES (2, 101)");
|
|
104
|
+
try {
|
|
105
|
+
parser.execute("INSERT INTO test3 VALUES (1, 101)");
|
|
106
|
+
throw new Error("Duplicate composite primary key allowed");
|
|
107
|
+
} catch (e) {
|
|
108
|
+
console.log("✓ Duplicate composite primary key rejected: " + e.message);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log("Test: Mixed case and inline PRIMARY KEY");
|
|
112
|
+
parser.execute("CREATE TABLE test4 (id INT primary key, code TEXT)");
|
|
113
|
+
parser.execute("INSERT INTO test4 VALUES (10, 'A1')");
|
|
114
|
+
try {
|
|
115
|
+
parser.execute("INSERT INTO test4 VALUES (10, 'A2')");
|
|
116
|
+
throw new Error("Duplicate primary key allowed (mixed case)");
|
|
117
|
+
} catch (e) {
|
|
118
|
+
console.log("✓ Duplicate primary key rejected: " + e.message);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
// --- SELECT Advanced Tests (from test_select.js) ---
|
|
123
|
+
console.log('\n--- SELECT Advanced Tests ---');
|
|
124
|
+
|
|
125
|
+
parser.execute('CREATE TABLE sel_users (name string, age int, country string)');
|
|
126
|
+
parser.execute("INSERT INTO sel_users VALUES ('Alice', 30, 'USA')");
|
|
127
|
+
parser.execute("INSERT INTO sel_users VALUES ('Bob', 25, 'UK')");
|
|
128
|
+
parser.execute("INSERT INTO sel_users VALUES ('Charlie', 35, 'USA')");
|
|
129
|
+
parser.execute("INSERT INTO sel_users VALUES ('David', 40, 'Canada')");
|
|
130
|
+
|
|
131
|
+
// 1. asterisk (*)
|
|
132
|
+
console.log('Testing SELECT * ...');
|
|
133
|
+
const all = parser.execute('SELECT * FROM sel_users');
|
|
134
|
+
if (all.length !== 4) throw new Error(`Expected 4 rows, got ${all.length}`);
|
|
135
|
+
if (!all[0].id || !all[0].name || !all[0].age || !all[0].country) throw new Error('Missing columns in SELECT *');
|
|
136
|
+
console.log('✓ SELECT * passed');
|
|
137
|
+
|
|
138
|
+
// 2. select some columns
|
|
139
|
+
console.log('Testing SELECT name, age ...');
|
|
140
|
+
const some = parser.execute('SELECT name, age FROM sel_users');
|
|
141
|
+
if (some.length !== 4) throw new Error(`Expected 4 rows, got ${some.length}`);
|
|
142
|
+
if (some[0].name === undefined || some[0].age === undefined) throw new Error('Selected columns missing');
|
|
143
|
+
if (some[0].id !== undefined || some[0].country !== undefined) throw new Error('Unselected columns present');
|
|
144
|
+
console.log('✓ SELECT columns passed');
|
|
145
|
+
|
|
146
|
+
// 3. WHERE clause
|
|
147
|
+
console.log('Testing SELECT * WHERE country=USA ...');
|
|
148
|
+
const usa = parser.execute('SELECT * FROM sel_users WHERE country=USA');
|
|
149
|
+
if (usa.length !== 2) throw new Error(`Expected 2 rows for USA, got ${usa.length}`);
|
|
150
|
+
|
|
151
|
+
console.log('Testing SELECT name WHERE id=1 ...');
|
|
152
|
+
const id1 = parser.execute('SELECT name FROM sel_users WHERE id=1');
|
|
153
|
+
if (id1.length !== 1 || id1[0].name !== 'Alice') throw new Error('SELECT WHERE id=1 failed');
|
|
154
|
+
console.log('✓ SELECT WHERE passed');
|
|
155
|
+
|
|
156
|
+
// 5. LIMIT
|
|
157
|
+
console.log('Testing SELECT * LIMIT 2 ...');
|
|
158
|
+
const limited = parser.execute('SELECT * FROM sel_users LIMIT 2');
|
|
159
|
+
if (limited.length !== 2) throw new Error(`Expected 2 rows, got ${limited.length}`);
|
|
160
|
+
|
|
161
|
+
console.log('Testing SELECT name FROM sel_users WHERE country=USA LIMIT 1 ...');
|
|
162
|
+
const limitedUsa = parser.execute('SELECT name FROM sel_users WHERE country=USA LIMIT 1');
|
|
163
|
+
if (limitedUsa.length !== 1) throw new Error(`Expected 1 row, got ${limitedUsa.length}`);
|
|
164
|
+
if (limitedUsa[0].name !== 'Alice') throw new Error(`Expected Alice, got ${limitedUsa[0].name}`);
|
|
165
|
+
console.log('✓ SELECT LIMIT passed');
|
|
166
|
+
|
|
167
|
+
console.log('\nAll tests passed successfully!');
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.error('\nTest failed!');
|
|
170
|
+
console.error(err);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
} finally {
|
|
173
|
+
cleanup();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
runTests();
|
package/test_es6_orm.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { TogeORM } from './index.js';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
|
|
5
|
+
async function testORM() {
|
|
6
|
+
console.log('Testing TogeORM with ES6 Import...');
|
|
7
|
+
const dataDir = './test_orm_data';
|
|
8
|
+
|
|
9
|
+
if (fs.existsSync(dataDir)) {
|
|
10
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const orm = new TogeORM(dataDir);
|
|
15
|
+
|
|
16
|
+
const User = orm.define('users', {
|
|
17
|
+
username: { type: 'string', primaryKey: true },
|
|
18
|
+
email: { type: 'string' }
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
console.log('Creating user...');
|
|
22
|
+
const user = User.create({ username: 'revtm', email: 'revtm@example.com' });
|
|
23
|
+
console.log('User created:', user);
|
|
24
|
+
|
|
25
|
+
console.log('Finding user...');
|
|
26
|
+
const found = User.findOne(u => u.username === 'revtm');
|
|
27
|
+
if (found && found.username === 'revtm') {
|
|
28
|
+
console.log('✓ Found user');
|
|
29
|
+
} else {
|
|
30
|
+
throw new Error('User not found');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log('✓ TogeORM ES6 test passed');
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error('TogeORM ES6 test failed:', err);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
} finally {
|
|
38
|
+
if (fs.existsSync(dataDir)) {
|
|
39
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
testORM();
|