@axiosleo/orm-mysql 0.14.3 → 0.15.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 +43 -0
- package/bin/orm-mysql.js +1 -1
- package/commands/skills.js +223 -0
- package/index.d.ts +10 -0
- package/package.json +1 -1
- package/skills/SKILL.md +157 -0
- package/skills/crud-operations.md +223 -0
- package/skills/query-building.md +159 -0
- package/skills/transactions.md +171 -0
- package/skills/where-conditions.md +146 -0
- package/src/builder.js +12 -0
- package/src/migration.js +13 -0
package/README.md
CHANGED
|
@@ -477,6 +477,49 @@ const hanlder = new QueryHandler(conn, {
|
|
|
477
477
|
});
|
|
478
478
|
```
|
|
479
479
|
|
|
480
|
+
## AI Skills
|
|
481
|
+
|
|
482
|
+
This project ships with AI Skills that teach AI coding assistants how to use this library correctly. You can install them with a single command:
|
|
483
|
+
|
|
484
|
+
```bash
|
|
485
|
+
# Cursor
|
|
486
|
+
npx @axiosleo/orm-mysql skills --install=cursor
|
|
487
|
+
|
|
488
|
+
# Claude Code
|
|
489
|
+
npx @axiosleo/orm-mysql skills --install=claude
|
|
490
|
+
|
|
491
|
+
# Windsurf
|
|
492
|
+
npx @axiosleo/orm-mysql skills --install=windsurf
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
The command detects the locally installed version of `@axiosleo/orm-mysql` in your `node_modules/` and copies the matching skill files. If the package is not installed locally, it will use the bundled skills and remind you to run `npm install @axiosleo/orm-mysql`.
|
|
496
|
+
|
|
497
|
+
To uninstall:
|
|
498
|
+
|
|
499
|
+
```bash
|
|
500
|
+
npx @axiosleo/orm-mysql skills --uninstall=cursor
|
|
501
|
+
npx @axiosleo/orm-mysql skills --uninstall=claude
|
|
502
|
+
npx @axiosleo/orm-mysql skills --uninstall=windsurf
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Skill Files
|
|
506
|
+
|
|
507
|
+
| File | Content |
|
|
508
|
+
|------|---------|
|
|
509
|
+
| `SKILL.md` | Setup, class hierarchy, quick start |
|
|
510
|
+
| `query-building.md` | table, attr, join, orderBy, limit, groupBy |
|
|
511
|
+
| `where-conditions.md` | where, whereIn, whereLike, whereBetween |
|
|
512
|
+
| `crud-operations.md` | select, find, insert, update, delete, incrBy |
|
|
513
|
+
| `transactions.md` | beginTransaction, isolation levels, row locking |
|
|
514
|
+
|
|
515
|
+
### Manual Installation
|
|
516
|
+
|
|
517
|
+
For other AI tools, copy the skill files from `node_modules/@axiosleo/orm-mysql/skills/` into your tool's custom instructions directory:
|
|
518
|
+
|
|
519
|
+
```bash
|
|
520
|
+
cp -r node_modules/@axiosleo/orm-mysql/skills/ .your-tool/skills/orm-mysql-usage/
|
|
521
|
+
```
|
|
522
|
+
|
|
480
523
|
## License
|
|
481
524
|
|
|
482
525
|
This project is open-sourced software licensed under [MIT](LICENSE).
|
package/bin/orm-mysql.js
CHANGED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { Command, printer } = require('@axiosleo/cli-tool');
|
|
6
|
+
const { _exists, _copy, _read, _read_json, _mkdir } = require('@axiosleo/cli-tool/src/helper/fs');
|
|
7
|
+
|
|
8
|
+
const SKILL_FILES = [
|
|
9
|
+
'SKILL.md',
|
|
10
|
+
'query-building.md',
|
|
11
|
+
'where-conditions.md',
|
|
12
|
+
'crud-operations.md',
|
|
13
|
+
'transactions.md',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const SUPPORTED_TARGETS = ['cursor', 'claude', 'windsurf'];
|
|
17
|
+
|
|
18
|
+
const PKG_NAME = '@axiosleo/orm-mysql';
|
|
19
|
+
|
|
20
|
+
function getTargetDir(cwd, target) {
|
|
21
|
+
const map = {
|
|
22
|
+
cursor: path.join(cwd, '.cursor', 'skills', 'orm-mysql-usage'),
|
|
23
|
+
windsurf: path.join(cwd, '.windsurf', 'skills', 'orm-mysql-usage'),
|
|
24
|
+
};
|
|
25
|
+
return map[target] || null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function resolveSkillsSource(cwd) {
|
|
29
|
+
const localPkgJson = path.join(cwd, 'node_modules', PKG_NAME, 'package.json');
|
|
30
|
+
let localPkgFound = false;
|
|
31
|
+
let localVersion = null;
|
|
32
|
+
|
|
33
|
+
if (await _exists(localPkgJson)) {
|
|
34
|
+
localPkgFound = true;
|
|
35
|
+
const pkg = await _read_json(localPkgJson);
|
|
36
|
+
localVersion = pkg.version;
|
|
37
|
+
const skillsDir = path.join(cwd, 'node_modules', PKG_NAME, 'skills');
|
|
38
|
+
if (await _exists(skillsDir)) {
|
|
39
|
+
return { dir: skillsDir, version: localVersion, local: true, outdated: false };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const fallbackDir = path.join(__dirname, '..', 'skills');
|
|
44
|
+
if (await _exists(fallbackDir)) {
|
|
45
|
+
const fallbackPkg = path.join(__dirname, '..', 'package.json');
|
|
46
|
+
let version = 'unknown';
|
|
47
|
+
if (await _exists(fallbackPkg)) {
|
|
48
|
+
const pkg = await _read_json(fallbackPkg);
|
|
49
|
+
version = pkg.version;
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
dir: fallbackDir,
|
|
53
|
+
version,
|
|
54
|
+
local: false,
|
|
55
|
+
outdated: localPkgFound,
|
|
56
|
+
localVersion,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class SkillsCommand extends Command {
|
|
63
|
+
constructor() {
|
|
64
|
+
super({
|
|
65
|
+
name: 'skills',
|
|
66
|
+
desc: 'Install or uninstall AI skills for coding assistants (Cursor, Claude Code, Windsurf)',
|
|
67
|
+
});
|
|
68
|
+
this.addOption('install', 'i', 'Install skills for target tool (cursor, claude, windsurf)', 'optional', '');
|
|
69
|
+
this.addOption('uninstall', 'u', 'Uninstall skills for target tool (cursor, claude, windsurf)', 'optional', '');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async exec(args, options) {
|
|
73
|
+
const install = options.install;
|
|
74
|
+
const uninstall = options.uninstall;
|
|
75
|
+
|
|
76
|
+
if (!install && !uninstall) {
|
|
77
|
+
this.printUsage();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (install) {
|
|
82
|
+
await this.install(install);
|
|
83
|
+
} else if (uninstall) {
|
|
84
|
+
await this.uninstall(uninstall);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
printUsage() {
|
|
89
|
+
printer.println();
|
|
90
|
+
printer.println('Usage:');
|
|
91
|
+
printer.println(' orm-mysql skills --install=<target> Install AI skills');
|
|
92
|
+
printer.println(' orm-mysql skills --uninstall=<target> Uninstall AI skills');
|
|
93
|
+
printer.println();
|
|
94
|
+
printer.println('Supported targets: ' + SUPPORTED_TARGETS.join(', '));
|
|
95
|
+
printer.println();
|
|
96
|
+
printer.println('Examples:');
|
|
97
|
+
printer.println(' npx @axiosleo/orm-mysql skills --install=cursor');
|
|
98
|
+
printer.println(' npx @axiosleo/orm-mysql skills --install=claude');
|
|
99
|
+
printer.println(' npx @axiosleo/orm-mysql skills --uninstall=cursor');
|
|
100
|
+
printer.println();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async install(target) {
|
|
104
|
+
if (!SUPPORTED_TARGETS.includes(target)) {
|
|
105
|
+
printer.error(`Unsupported target: "${target}". Supported targets: ${SUPPORTED_TARGETS.join(', ')}`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const cwd = process.cwd();
|
|
110
|
+
const source = await resolveSkillsSource(cwd);
|
|
111
|
+
|
|
112
|
+
if (!source) {
|
|
113
|
+
printer.error('Could not find skills files. Please reinstall @axiosleo/orm-mysql.');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (source.local) {
|
|
118
|
+
printer.info(`Found ${PKG_NAME}@${source.version} in node_modules`);
|
|
119
|
+
} else if (source.outdated) {
|
|
120
|
+
printer.warning(`${PKG_NAME}@${source.localVersion} is installed locally but does not include skills files.`);
|
|
121
|
+
printer.warning('Skills files are available since v0.15.0. Please update:');
|
|
122
|
+
printer.warning(` npm install ${PKG_NAME}@latest`);
|
|
123
|
+
printer.println();
|
|
124
|
+
printer.info(`Using skills from npx ${PKG_NAME}@${source.version} instead.`);
|
|
125
|
+
} else {
|
|
126
|
+
printer.warning(`${PKG_NAME} is not installed locally in this project.`);
|
|
127
|
+
printer.warning(`Consider running: npm install ${PKG_NAME}`);
|
|
128
|
+
printer.println();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
printer.info(`Installing skills for ${target} from ${PKG_NAME}@${source.version}...`);
|
|
132
|
+
|
|
133
|
+
if (target === 'claude') {
|
|
134
|
+
await this.installClaude(cwd, source);
|
|
135
|
+
} else {
|
|
136
|
+
await this.installCopyTarget(cwd, target, source);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async installCopyTarget(cwd, target, source) {
|
|
141
|
+
const targetDir = getTargetDir(cwd, target);
|
|
142
|
+
await _mkdir(targetDir);
|
|
143
|
+
await _copy(source.dir, targetDir, true);
|
|
144
|
+
printer.success(`Skills installed to ${path.relative(cwd, targetDir)}/`);
|
|
145
|
+
printer.println();
|
|
146
|
+
printer.println('Files installed:');
|
|
147
|
+
for (const file of SKILL_FILES) {
|
|
148
|
+
printer.println(` - ${file}`);
|
|
149
|
+
}
|
|
150
|
+
printer.println();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async installClaude(cwd, source) {
|
|
154
|
+
const claudeFile = path.join(cwd, 'CLAUDE.md');
|
|
155
|
+
const header = `<!-- ${PKG_NAME}@${source.version} skills -->\n`;
|
|
156
|
+
const separator = '\n---\n\n';
|
|
157
|
+
|
|
158
|
+
let content = header;
|
|
159
|
+
for (const file of SKILL_FILES) {
|
|
160
|
+
const filePath = path.join(source.dir, file);
|
|
161
|
+
if (await _exists(filePath)) {
|
|
162
|
+
const fileContent = await _read(filePath);
|
|
163
|
+
content += separator + fileContent.trim() + '\n';
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (await _exists(claudeFile)) {
|
|
168
|
+
const existing = await _read(claudeFile);
|
|
169
|
+
if (existing.includes(`<!-- ${PKG_NAME}`) && existing.includes('skills -->')) {
|
|
170
|
+
printer.warning('CLAUDE.md already contains @axiosleo/orm-mysql skills.');
|
|
171
|
+
printer.warning('Please remove the existing skills section first, or run:');
|
|
172
|
+
printer.warning(' npx @axiosleo/orm-mysql skills --uninstall=claude');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fs.appendFileSync(claudeFile, '\n' + content);
|
|
178
|
+
printer.success(`Skills appended to ${path.relative(cwd, claudeFile)}`);
|
|
179
|
+
printer.println();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async uninstall(target) {
|
|
183
|
+
if (!SUPPORTED_TARGETS.includes(target)) {
|
|
184
|
+
printer.error(`Unsupported target: "${target}". Supported targets: ${SUPPORTED_TARGETS.join(', ')}`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const cwd = process.cwd();
|
|
189
|
+
|
|
190
|
+
if (target === 'claude') {
|
|
191
|
+
this.uninstallClaude(cwd);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const targetDir = getTargetDir(cwd, target);
|
|
196
|
+
if (await _exists(targetDir)) {
|
|
197
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
198
|
+
printer.success(`Skills removed from ${path.relative(cwd, targetDir)}/`);
|
|
199
|
+
} else {
|
|
200
|
+
printer.warning(`No skills found at ${path.relative(cwd, targetDir)}/`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
uninstallClaude(cwd) {
|
|
205
|
+
const claudeFile = path.join(cwd, 'CLAUDE.md');
|
|
206
|
+
if (!fs.existsSync(claudeFile)) {
|
|
207
|
+
printer.warning('CLAUDE.md not found.');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const content = fs.readFileSync(claudeFile, 'utf8');
|
|
211
|
+
const startMarker = `<!-- ${PKG_NAME}`;
|
|
212
|
+
const idx = content.indexOf(startMarker);
|
|
213
|
+
if (idx === -1) {
|
|
214
|
+
printer.warning('No @axiosleo/orm-mysql skills section found in CLAUDE.md.');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const cleaned = content.substring(0, idx).trimEnd() + '\n';
|
|
218
|
+
fs.writeFileSync(claudeFile, cleaned);
|
|
219
|
+
printer.success('Skills section removed from CLAUDE.md');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = SkillsCommand;
|
package/index.d.ts
CHANGED
|
@@ -708,6 +708,16 @@ export declare class MigrationInterface {
|
|
|
708
708
|
* @param newTableName
|
|
709
709
|
*/
|
|
710
710
|
renameTable(oldTableName: string, newTableName: string): void;
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* change column
|
|
714
|
+
* @param tableName
|
|
715
|
+
* @param columnName
|
|
716
|
+
* @param options
|
|
717
|
+
*/
|
|
718
|
+
changeColumn(tableName: string, columnName: string, options: {
|
|
719
|
+
type: FieldType,
|
|
720
|
+
} & CreateColumnOptions): void;
|
|
711
721
|
}
|
|
712
722
|
|
|
713
723
|
export type MigrateAction = 'up' | 'down' | 'UP' | 'DOWN';
|
package/package.json
CHANGED
package/skills/SKILL.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: orm-mysql-usage
|
|
3
|
+
description: Build MySQL queries, perform CRUD operations, and manage transactions using @axiosleo/orm-mysql. Use when writing database queries, building where conditions, inserting/updating/deleting rows, managing transactions, or working with the ORM query builder in this project.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @axiosleo/orm-mysql Usage Guide
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @axiosleo/orm-mysql
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Setup
|
|
15
|
+
|
|
16
|
+
### Create a Connection
|
|
17
|
+
|
|
18
|
+
```javascript
|
|
19
|
+
const { createClient, QueryHandler } = require("@axiosleo/orm-mysql");
|
|
20
|
+
|
|
21
|
+
const conn = createClient({
|
|
22
|
+
host: "localhost",
|
|
23
|
+
port: 3306,
|
|
24
|
+
user: "root",
|
|
25
|
+
password: "password",
|
|
26
|
+
database: "my_db",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const db = new QueryHandler(conn);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Create a Connection Pool (recommended for production)
|
|
33
|
+
|
|
34
|
+
```javascript
|
|
35
|
+
const { createPool, QueryHandler } = require("@axiosleo/orm-mysql");
|
|
36
|
+
|
|
37
|
+
const pool = createPool({
|
|
38
|
+
host: "localhost",
|
|
39
|
+
port: 3306,
|
|
40
|
+
user: "root",
|
|
41
|
+
password: "password",
|
|
42
|
+
database: "my_db",
|
|
43
|
+
connectionLimit: 10,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const db = new QueryHandler(pool);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Using MySQLClient
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
const { MySQLClient } = require("@axiosleo/orm-mysql");
|
|
53
|
+
|
|
54
|
+
const client = new MySQLClient({
|
|
55
|
+
host: "localhost",
|
|
56
|
+
port: 3306,
|
|
57
|
+
user: "root",
|
|
58
|
+
password: "password",
|
|
59
|
+
database: "my_db",
|
|
60
|
+
}, null, "pool"); // "default" | "promise" | "pool"
|
|
61
|
+
|
|
62
|
+
const rows = await client.table("users").select();
|
|
63
|
+
await client.close();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Class Hierarchy
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
QueryCondition -- where clauses (where, whereIn, whereLike, whereBetween...)
|
|
70
|
+
└── Query -- query building (table, attr, join, orderBy, limit, page...)
|
|
71
|
+
└── QueryOperator -- execution (select, find, insert, update, delete...)
|
|
72
|
+
└── TransactionOperator -- adds append() for row locking
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- `QueryHandler` wraps a connection/pool and creates `QueryOperator` via `.table(name)`
|
|
76
|
+
- `TransactionHandler` wraps a promise connection and creates `TransactionOperator` via `.table(name)`
|
|
77
|
+
|
|
78
|
+
## Quick Start
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
const db = new QueryHandler(conn);
|
|
82
|
+
|
|
83
|
+
// SELECT
|
|
84
|
+
const users = await db.table("users")
|
|
85
|
+
.where("age", ">", 18)
|
|
86
|
+
.orderBy("name", "asc")
|
|
87
|
+
.limit(10)
|
|
88
|
+
.select("id", "name", "age");
|
|
89
|
+
|
|
90
|
+
// INSERT
|
|
91
|
+
await db.table("users").insert({ name: "Joe", age: 25 });
|
|
92
|
+
|
|
93
|
+
// UPDATE
|
|
94
|
+
await db.table("users").where("id", 1).update({ age: 26 });
|
|
95
|
+
|
|
96
|
+
// DELETE
|
|
97
|
+
await db.table("users").where("id", 1).delete();
|
|
98
|
+
|
|
99
|
+
// COUNT
|
|
100
|
+
const total = await db.table("users").where("age", ">", 18).count();
|
|
101
|
+
|
|
102
|
+
// FIND single row
|
|
103
|
+
const user = await db.table("users").where("id", 1).find();
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Dry Run with notExec()
|
|
107
|
+
|
|
108
|
+
Call `notExec()` before any CRUD method to get a `Builder` object with `.sql` and `.values` instead of executing:
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
const builder = await db.table("users")
|
|
112
|
+
.where("age", ">", 18)
|
|
113
|
+
.notExec()
|
|
114
|
+
.select("id", "name");
|
|
115
|
+
|
|
116
|
+
console.log(builder.sql); // "SELECT `id`, `name` FROM `users` WHERE `age` > ?"
|
|
117
|
+
console.log(builder.values); // [18]
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Reference Files
|
|
121
|
+
|
|
122
|
+
| Scenario | File |
|
|
123
|
+
|----------|------|
|
|
124
|
+
| Building queries (table, join, orderBy, limit, groupBy, attr) | [query-building.md](query-building.md) |
|
|
125
|
+
| Where conditions (where, whereIn, whereLike, whereBetween...) | [where-conditions.md](where-conditions.md) |
|
|
126
|
+
| CRUD operations (select, find, count, insert, update, delete, incrBy, upsertRow) | [crud-operations.md](crud-operations.md) |
|
|
127
|
+
| Transactions (beginTransaction, commit, rollback, FOR UPDATE) | [transactions.md](transactions.md) |
|
|
128
|
+
|
|
129
|
+
## Hooks
|
|
130
|
+
|
|
131
|
+
Register pre/post hooks for query operations:
|
|
132
|
+
|
|
133
|
+
```javascript
|
|
134
|
+
const { Hook } = require("@axiosleo/orm-mysql");
|
|
135
|
+
|
|
136
|
+
Hook.pre(async (options) => {
|
|
137
|
+
console.log("Before:", options.operator, options.tables);
|
|
138
|
+
}, { table: "users", opt: "insert" });
|
|
139
|
+
|
|
140
|
+
Hook.post(async (options, result) => {
|
|
141
|
+
console.log("After:", result);
|
|
142
|
+
}, { table: "users", opt: "insert" });
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Schema Helpers
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
const exists = await db.existTable("users");
|
|
149
|
+
const dbExists = await db.existDatabase("my_db");
|
|
150
|
+
const fields = await db.getTableFields("my_db", "users", "COLUMN_NAME", "DATA_TYPE");
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Raw SQL
|
|
154
|
+
|
|
155
|
+
```javascript
|
|
156
|
+
const result = await db.query({ sql: "SELECT * FROM users WHERE id = ?", values: [1] });
|
|
157
|
+
```
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# CRUD Operations
|
|
2
|
+
|
|
3
|
+
The `QueryOperator` class provides all execution methods. You get an instance via `db.table("name")`.
|
|
4
|
+
|
|
5
|
+
All CRUD methods return a `Promise`. Write operations resolve to `MySQLQueryResult` (OkPacket | ResultSetHeader). If `notExec()` was called, they resolve to a `Builder` object instead.
|
|
6
|
+
|
|
7
|
+
## Read Operations
|
|
8
|
+
|
|
9
|
+
### select(...attrs)
|
|
10
|
+
|
|
11
|
+
Returns an array of rows. Optionally pass column names to override previously set `attr()`.
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
const rows = await db.table("users").select();
|
|
15
|
+
const rows = await db.table("users").select("id", "name", "email");
|
|
16
|
+
|
|
17
|
+
// With TypeScript generics
|
|
18
|
+
interface User { id: number; name: string; email: string; }
|
|
19
|
+
const users = await db.table("users").select<User>("id", "name", "email");
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### find()
|
|
23
|
+
|
|
24
|
+
Returns a single row (the first match). Automatically applies `LIMIT 1`.
|
|
25
|
+
|
|
26
|
+
```javascript
|
|
27
|
+
const user = await db.table("users").where("id", 1).find();
|
|
28
|
+
|
|
29
|
+
// With TypeScript generics
|
|
30
|
+
const user = await db.table("users").where("id", 1).find<User>();
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### count()
|
|
34
|
+
|
|
35
|
+
Returns the total number of matching rows as a number.
|
|
36
|
+
|
|
37
|
+
```javascript
|
|
38
|
+
const total = await db.table("users").where("status", "active").count();
|
|
39
|
+
// total is a number
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### explain(operator)
|
|
43
|
+
|
|
44
|
+
Returns the MySQL EXPLAIN result for the query.
|
|
45
|
+
|
|
46
|
+
```javascript
|
|
47
|
+
const plan = await db.table("users")
|
|
48
|
+
.where("status", "active")
|
|
49
|
+
.explain("select");
|
|
50
|
+
// plan is ExplainResult[]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Write Operations
|
|
54
|
+
|
|
55
|
+
### insert(row)
|
|
56
|
+
|
|
57
|
+
Insert a single row.
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
const result = await db.table("users").insert({
|
|
61
|
+
name: "Joe",
|
|
62
|
+
age: 25,
|
|
63
|
+
email: "joe@example.com",
|
|
64
|
+
});
|
|
65
|
+
// result.insertId -- the auto-increment ID
|
|
66
|
+
// result.affectedRows -- number of rows inserted
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Insert with ON DUPLICATE KEY UPDATE
|
|
70
|
+
|
|
71
|
+
Use `keys()` to specify unique columns. If a duplicate is found, the operation becomes an update.
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
const result = await db.table("users").keys("email").insert({
|
|
75
|
+
email: "joe@example.com",
|
|
76
|
+
name: "Joe Updated",
|
|
77
|
+
age: 26,
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### insertAll(rows)
|
|
82
|
+
|
|
83
|
+
Insert multiple rows. Returns an array of results.
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
const results = await db.table("users").insertAll([
|
|
87
|
+
{ name: "Alice", age: 30 },
|
|
88
|
+
{ name: "Bob", age: 28 },
|
|
89
|
+
{ name: "Charlie", age: 35 },
|
|
90
|
+
]);
|
|
91
|
+
// results is MySQLQueryResult[]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### update(row)
|
|
95
|
+
|
|
96
|
+
Update rows matching the current where conditions.
|
|
97
|
+
|
|
98
|
+
```javascript
|
|
99
|
+
const result = await db.table("users")
|
|
100
|
+
.where("id", 1)
|
|
101
|
+
.update({ name: "Joe Updated", age: 26 });
|
|
102
|
+
// result.affectedRows -- number of rows updated
|
|
103
|
+
// result.changedRows -- number of rows actually changed
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### delete(id?, index_field_name?)
|
|
107
|
+
|
|
108
|
+
Delete rows. Can be called with conditions or directly with an ID.
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
// Delete by conditions
|
|
112
|
+
const result = await db.table("users").where("status", "banned").delete();
|
|
113
|
+
|
|
114
|
+
// Delete by primary key (defaults to "id" field)
|
|
115
|
+
const result = await db.table("users").delete(1);
|
|
116
|
+
|
|
117
|
+
// Delete by a custom index field
|
|
118
|
+
const result = await db.table("users").delete(1, "user_id");
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### incrBy(attr, increment?)
|
|
122
|
+
|
|
123
|
+
Increment a column value atomically. Default increment is 1.
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
// Increment by 1 (default)
|
|
127
|
+
await db.table("users").where("id", 1).incrBy("login_count");
|
|
128
|
+
|
|
129
|
+
// Increment by a specific number
|
|
130
|
+
await db.table("products").where("id", 1).incrBy("views", 5);
|
|
131
|
+
|
|
132
|
+
// Increment by string value
|
|
133
|
+
await db.table("users").where("id", 1).incrBy("score", "10");
|
|
134
|
+
|
|
135
|
+
// Conditional increment with callback
|
|
136
|
+
await db.table("users").where("id", 1).incrBy("error_count", (current) => {
|
|
137
|
+
return shouldIncrement ? 1 : 0;
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### upsertRow(row, condition)
|
|
142
|
+
|
|
143
|
+
Insert a row or update it if a matching row exists based on the condition.
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
// Using QueryCondition
|
|
147
|
+
const condition = new QueryCondition();
|
|
148
|
+
condition.where("email", "joe@example.com");
|
|
149
|
+
|
|
150
|
+
await db.table("users").upsertRow(
|
|
151
|
+
{ email: "joe@example.com", name: "Joe", age: 25 },
|
|
152
|
+
condition
|
|
153
|
+
);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Dry Run with notExec()
|
|
157
|
+
|
|
158
|
+
Call `notExec()` before any CRUD method to get the generated SQL without executing it. Returns a `Builder` with `.sql` and `.values`.
|
|
159
|
+
|
|
160
|
+
```javascript
|
|
161
|
+
const builder = await db.table("users")
|
|
162
|
+
.where("status", "active")
|
|
163
|
+
.orderBy("name", "asc")
|
|
164
|
+
.limit(10)
|
|
165
|
+
.notExec()
|
|
166
|
+
.select("id", "name");
|
|
167
|
+
|
|
168
|
+
console.log(builder.sql); // The generated SQL string
|
|
169
|
+
console.log(builder.values); // The parameter values array
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
This works with all CRUD methods:
|
|
173
|
+
|
|
174
|
+
```javascript
|
|
175
|
+
const insertBuilder = await db.table("users")
|
|
176
|
+
.notExec()
|
|
177
|
+
.insert({ name: "Joe", age: 25 });
|
|
178
|
+
|
|
179
|
+
const updateBuilder = await db.table("users")
|
|
180
|
+
.where("id", 1)
|
|
181
|
+
.notExec()
|
|
182
|
+
.update({ age: 26 });
|
|
183
|
+
|
|
184
|
+
const deleteBuilder = await db.table("users")
|
|
185
|
+
.where("id", 1)
|
|
186
|
+
.notExec()
|
|
187
|
+
.delete();
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Complete Example
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
const db = new QueryHandler(pool);
|
|
194
|
+
|
|
195
|
+
// Create user
|
|
196
|
+
const insertResult = await db.table("users").insert({
|
|
197
|
+
name: "Joe",
|
|
198
|
+
email: "joe@example.com",
|
|
199
|
+
age: 25,
|
|
200
|
+
});
|
|
201
|
+
const userId = insertResult.insertId;
|
|
202
|
+
|
|
203
|
+
// Read back
|
|
204
|
+
const user = await db.table("users").where("id", userId).find();
|
|
205
|
+
|
|
206
|
+
// Update
|
|
207
|
+
await db.table("users").where("id", userId).update({ age: 26 });
|
|
208
|
+
|
|
209
|
+
// Increment login count
|
|
210
|
+
await db.table("users").where("id", userId).incrBy("login_count");
|
|
211
|
+
|
|
212
|
+
// Count active users
|
|
213
|
+
const activeCount = await db.table("users").where("status", "active").count();
|
|
214
|
+
|
|
215
|
+
// Bulk insert
|
|
216
|
+
await db.table("logs").insertAll([
|
|
217
|
+
{ user_id: userId, action: "login" },
|
|
218
|
+
{ user_id: userId, action: "view_profile" },
|
|
219
|
+
]);
|
|
220
|
+
|
|
221
|
+
// Delete
|
|
222
|
+
await db.table("sessions").where("expired_at", "<", new Date()).delete();
|
|
223
|
+
```
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Query Building
|
|
2
|
+
|
|
3
|
+
The `Query` class provides the fluent API for constructing SQL queries. You get a `Query`-based instance (actually `QueryOperator`) from `QueryHandler.table()`.
|
|
4
|
+
|
|
5
|
+
```javascript
|
|
6
|
+
const query = db.table("users"); // returns QueryOperator (extends Query)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Table Selection
|
|
10
|
+
|
|
11
|
+
### Single table
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
db.table("users");
|
|
15
|
+
db.table("users", "u"); // with alias
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Multiple tables
|
|
19
|
+
|
|
20
|
+
```javascript
|
|
21
|
+
db.tables(
|
|
22
|
+
{ table: "users", alias: "u" },
|
|
23
|
+
{ table: "orders", alias: "o" }
|
|
24
|
+
);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Selecting Columns with attr()
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
// Select specific columns
|
|
31
|
+
query.attr("id", "name", "email");
|
|
32
|
+
|
|
33
|
+
// Using sub-query as attribute
|
|
34
|
+
const { Query } = require("@axiosleo/orm-mysql");
|
|
35
|
+
|
|
36
|
+
query.attr(
|
|
37
|
+
"id",
|
|
38
|
+
"name",
|
|
39
|
+
() => {
|
|
40
|
+
const sub = new Query("select");
|
|
41
|
+
sub.table("orders").attr("COUNT(*)").where("orders.user_id", "users.id");
|
|
42
|
+
return sub;
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Calling `attr()` with no arguments clears all previously set attributes.
|
|
48
|
+
|
|
49
|
+
## Pagination
|
|
50
|
+
|
|
51
|
+
### limit and offset
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
query.limit(10); // LIMIT 10
|
|
55
|
+
query.offset(20); // OFFSET 20
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### page (shorthand)
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
query.page(10); // LIMIT 10 OFFSET 0
|
|
62
|
+
query.page(10, 2); // LIMIT 10 OFFSET 2
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Sorting
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
query.orderBy("created_at", "desc");
|
|
69
|
+
query.orderBy("name", "asc");
|
|
70
|
+
// Multiple orderBy calls are cumulative
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Grouping and Aggregation
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
query.groupBy("status");
|
|
77
|
+
query.groupBy("status", "category"); // multiple fields
|
|
78
|
+
|
|
79
|
+
// HAVING clause (requires groupBy)
|
|
80
|
+
query.groupBy("status").having("COUNT(*)", ">", 5);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Setting Data
|
|
84
|
+
|
|
85
|
+
### set() -- for insert/update data
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
query.set({ name: "Joe", age: 25 });
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### keys() -- specify columns for INSERT with ON DUPLICATE KEY UPDATE
|
|
92
|
+
|
|
93
|
+
```javascript
|
|
94
|
+
query.keys("uuid").insert({
|
|
95
|
+
uuid: "abc-123",
|
|
96
|
+
name: "Joe",
|
|
97
|
+
age: 25,
|
|
98
|
+
});
|
|
99
|
+
// If uuid already exists, the insert becomes an update
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Joins
|
|
103
|
+
|
|
104
|
+
### leftJoin / rightJoin / innerJoin (preferred)
|
|
105
|
+
|
|
106
|
+
```javascript
|
|
107
|
+
query.table("users", "u")
|
|
108
|
+
.leftJoin("orders", "u.id = orders.user_id", { alias: "o" })
|
|
109
|
+
.attr("u.id", "u.name", "o.total")
|
|
110
|
+
.select();
|
|
111
|
+
|
|
112
|
+
query.table("users", "u")
|
|
113
|
+
.rightJoin("orders", "u.id = orders.user_id")
|
|
114
|
+
.select();
|
|
115
|
+
|
|
116
|
+
query.table("users", "u")
|
|
117
|
+
.innerJoin("roles", "u.role_id = roles.id", { alias: "r" })
|
|
118
|
+
.select();
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### join() -- generic form
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
query.join({
|
|
125
|
+
table: "orders",
|
|
126
|
+
table_alias: "o",
|
|
127
|
+
self_column: "id",
|
|
128
|
+
foreign_column: "user_id",
|
|
129
|
+
join_type: "left",
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Sub-query as join table
|
|
134
|
+
|
|
135
|
+
```javascript
|
|
136
|
+
const { Query } = require("@axiosleo/orm-mysql");
|
|
137
|
+
|
|
138
|
+
const subQuery = new Query("select");
|
|
139
|
+
subQuery.table("orders").attr("user_id", "SUM(total) AS order_total").groupBy("user_id");
|
|
140
|
+
|
|
141
|
+
query.table("users", "u")
|
|
142
|
+
.leftJoin(subQuery, "u.id = sub.user_id", { alias: "sub" })
|
|
143
|
+
.select();
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Complete Example
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
const users = await db.table("users", "u")
|
|
150
|
+
.attr("u.id", "u.name", "u.email", "o.total")
|
|
151
|
+
.leftJoin("orders", "u.id = orders.user_id", { alias: "o" })
|
|
152
|
+
.where("u.status", "active")
|
|
153
|
+
.whereBetween("u.created_at", ["2024-01-01", "2024-12-31"])
|
|
154
|
+
.groupBy("u.id")
|
|
155
|
+
.having("COUNT(o.id)", ">", 0)
|
|
156
|
+
.orderBy("u.name", "asc")
|
|
157
|
+
.page(20, 0)
|
|
158
|
+
.select();
|
|
159
|
+
```
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Transactions
|
|
2
|
+
|
|
3
|
+
## Isolation Levels
|
|
4
|
+
|
|
5
|
+
| Shorthand | Full Name | Description |
|
|
6
|
+
|-----------|-----------|-------------|
|
|
7
|
+
| `RU` | `READ UNCOMMITTED` | Lowest isolation, may read dirty data |
|
|
8
|
+
| `RC` | `READ COMMITTED` | Prevents dirty reads |
|
|
9
|
+
| `RR` | `REPEATABLE READ` | MySQL default, prevents non-repeatable reads |
|
|
10
|
+
| `S` | `SERIALIZABLE` | Highest isolation, full serialization |
|
|
11
|
+
|
|
12
|
+
## Method 1: Pool + beginTransaction (Recommended)
|
|
13
|
+
|
|
14
|
+
Use `QueryHandler.beginTransaction()` with a connection pool. The transaction automatically gets its own connection from the pool and releases it on commit/rollback.
|
|
15
|
+
|
|
16
|
+
```javascript
|
|
17
|
+
const { createPool, QueryHandler } = require("@axiosleo/orm-mysql");
|
|
18
|
+
|
|
19
|
+
const pool = createPool({
|
|
20
|
+
host: "localhost",
|
|
21
|
+
port: 3306,
|
|
22
|
+
user: "root",
|
|
23
|
+
password: "password",
|
|
24
|
+
database: "my_db",
|
|
25
|
+
connectionLimit: 10,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const db = new QueryHandler(pool);
|
|
29
|
+
|
|
30
|
+
const tx = await db.beginTransaction({ level: "RC" });
|
|
31
|
+
try {
|
|
32
|
+
const result = await tx.table("users").insert({ name: "Joe", age: 25 });
|
|
33
|
+
const userId = result.insertId;
|
|
34
|
+
|
|
35
|
+
await tx.table("profiles").insert({ user_id: userId, bio: "Hello" });
|
|
36
|
+
|
|
37
|
+
await tx.commit();
|
|
38
|
+
} catch (err) {
|
|
39
|
+
await tx.rollback();
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Method 2: TransactionHandler Directly
|
|
45
|
+
|
|
46
|
+
For more control, create a `TransactionHandler` with a promise connection.
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
const { TransactionHandler, createPromiseClient } = require("@axiosleo/orm-mysql");
|
|
50
|
+
|
|
51
|
+
const conn = await createPromiseClient({
|
|
52
|
+
host: "localhost",
|
|
53
|
+
port: 3306,
|
|
54
|
+
user: "root",
|
|
55
|
+
password: "password",
|
|
56
|
+
database: "my_db",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const tx = new TransactionHandler(conn, { level: "SERIALIZABLE" });
|
|
60
|
+
await tx.begin();
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
await tx.table("users").insert({ name: "Joe", age: 25 });
|
|
64
|
+
await tx.commit();
|
|
65
|
+
} catch (err) {
|
|
66
|
+
await tx.rollback();
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Row Locking
|
|
72
|
+
|
|
73
|
+
`TransactionOperator` (returned by `tx.table()`) extends `QueryOperator` with `append()` for SQL suffixes.
|
|
74
|
+
|
|
75
|
+
### FOR UPDATE
|
|
76
|
+
|
|
77
|
+
Locks selected rows, preventing other transactions from reading or modifying them.
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
const tx = await db.beginTransaction({ level: "RC" });
|
|
81
|
+
try {
|
|
82
|
+
const product = await tx.table("products")
|
|
83
|
+
.where("sku", "LAPTOP-001")
|
|
84
|
+
.append("FOR UPDATE")
|
|
85
|
+
.find();
|
|
86
|
+
|
|
87
|
+
if (product.stock < 1) {
|
|
88
|
+
throw new Error("Out of stock");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await tx.table("products")
|
|
92
|
+
.where("sku", "LAPTOP-001")
|
|
93
|
+
.update({ stock: product.stock - 1 });
|
|
94
|
+
|
|
95
|
+
await tx.table("orders").insert({
|
|
96
|
+
product_id: product.id,
|
|
97
|
+
quantity: 1,
|
|
98
|
+
total: product.price,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await tx.commit();
|
|
102
|
+
} catch (err) {
|
|
103
|
+
await tx.rollback();
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### LOCK IN SHARE MODE
|
|
109
|
+
|
|
110
|
+
Allows other transactions to read but not modify the locked rows.
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
const rows = await tx.table("products")
|
|
114
|
+
.where("category", "electronics")
|
|
115
|
+
.append("LOCK IN SHARE MODE")
|
|
116
|
+
.select();
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## TransactionHandler API
|
|
120
|
+
|
|
121
|
+
| Method | Description |
|
|
122
|
+
|--------|-------------|
|
|
123
|
+
| `begin()` | Start the transaction |
|
|
124
|
+
| `commit()` | Commit and release the connection |
|
|
125
|
+
| `rollback()` | Rollback and release the connection |
|
|
126
|
+
| `table(name, alias?)` | Returns `TransactionOperator` for the table |
|
|
127
|
+
| `query(options)` | Execute raw SQL within the transaction |
|
|
128
|
+
| `execute(sql, values)` | Execute parameterized SQL |
|
|
129
|
+
| `lastInsertId(alias?)` | Get the last auto-increment ID |
|
|
130
|
+
| `upsert(table, data, condition)` | Insert or update within the transaction |
|
|
131
|
+
|
|
132
|
+
## Concurrent Transactions
|
|
133
|
+
|
|
134
|
+
With a connection pool, multiple transactions run on separate connections without blocking each other.
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
const db = new QueryHandler(pool);
|
|
138
|
+
|
|
139
|
+
await Promise.all([
|
|
140
|
+
(async () => {
|
|
141
|
+
const tx = await db.beginTransaction();
|
|
142
|
+
try {
|
|
143
|
+
await tx.table("users").insert({ name: "User1" });
|
|
144
|
+
await tx.commit();
|
|
145
|
+
} catch (err) {
|
|
146
|
+
await tx.rollback();
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
})(),
|
|
150
|
+
|
|
151
|
+
(async () => {
|
|
152
|
+
const tx = await db.beginTransaction();
|
|
153
|
+
try {
|
|
154
|
+
await tx.table("users").insert({ name: "User2" });
|
|
155
|
+
await tx.commit();
|
|
156
|
+
} catch (err) {
|
|
157
|
+
await tx.rollback();
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
})(),
|
|
161
|
+
]);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Best Practices
|
|
165
|
+
|
|
166
|
+
1. **Use connection pools in production** -- prevents connection exhaustion
|
|
167
|
+
2. **Always wrap in try/catch** -- ensure rollback on errors
|
|
168
|
+
3. **Keep transactions short** -- avoid long-running operations inside transactions
|
|
169
|
+
4. **Choose the right isolation level** -- `RC` is a good default for most cases
|
|
170
|
+
5. **Use row locking when needed** -- `FOR UPDATE` prevents concurrent modification conflicts
|
|
171
|
+
6. **Prefer `beginTransaction()` over manual `TransactionHandler`** -- automatic connection management
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Where Conditions
|
|
2
|
+
|
|
3
|
+
The `QueryCondition` class provides all WHERE clause methods. All methods return `this` for chaining.
|
|
4
|
+
|
|
5
|
+
## Basic where()
|
|
6
|
+
|
|
7
|
+
### Key-value equality
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
query.where("name", "Joe"); // WHERE `name` = ?
|
|
11
|
+
query.where("age", ">", 18); // WHERE `age` > ?
|
|
12
|
+
query.where("status", "!=", "banned"); // WHERE `status` != ?
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Object form (multiple equalities)
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
query.where({ name: "Joe", status: "active" });
|
|
19
|
+
// WHERE `name` = ? AND `status` = ?
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Supported operators
|
|
23
|
+
|
|
24
|
+
`=`, `!=`, `>`, `<`, `>=`, `<=`, `LIKE`, `NOT LIKE`, `IN`, `NOT IN`, `BETWEEN`, `NOT BETWEEN`, `IS`, `IS NOT`, `REGEXP`, `NOT REGEXP`, `CONTAIN`, `NOT CONTAIN`, `OVERLAPS`, `NOT OVERLAPS`
|
|
25
|
+
|
|
26
|
+
## Logical Operators
|
|
27
|
+
|
|
28
|
+
### AND / OR grouping
|
|
29
|
+
|
|
30
|
+
```javascript
|
|
31
|
+
// Switch to OR logic for subsequent conditions
|
|
32
|
+
query.where("OR");
|
|
33
|
+
// or equivalently:
|
|
34
|
+
query.whereOr();
|
|
35
|
+
|
|
36
|
+
// Switch back to AND logic
|
|
37
|
+
query.where("AND");
|
|
38
|
+
// or equivalently:
|
|
39
|
+
query.whereAnd();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Combining AND/OR
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
// WHERE `status` = ? AND (`age` > ? OR `vip` = ?)
|
|
46
|
+
query
|
|
47
|
+
.where("status", "active")
|
|
48
|
+
.whereOr()
|
|
49
|
+
.where("age", ">", 18)
|
|
50
|
+
.where("vip", true)
|
|
51
|
+
.whereAnd();
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## IN / NOT IN
|
|
55
|
+
|
|
56
|
+
```javascript
|
|
57
|
+
query.whereIn("status", ["active", "pending"]);
|
|
58
|
+
// WHERE `status` IN (?, ?)
|
|
59
|
+
|
|
60
|
+
query.whereNotIn("role", ["banned", "suspended"]);
|
|
61
|
+
// WHERE `role` NOT IN (?, ?)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Sub-query in whereIn
|
|
65
|
+
|
|
66
|
+
```javascript
|
|
67
|
+
const { Query } = require("@axiosleo/orm-mysql");
|
|
68
|
+
|
|
69
|
+
const subQuery = new Query("select");
|
|
70
|
+
subQuery.table("orders").attr("user_id").where("total", ">", 100);
|
|
71
|
+
|
|
72
|
+
query.whereIn("id", subQuery);
|
|
73
|
+
// WHERE `id` IN (SELECT `user_id` FROM `orders` WHERE `total` > ?)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## LIKE / NOT LIKE
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
query.whereLike("name", "%Joe%");
|
|
80
|
+
// WHERE `name` LIKE ?
|
|
81
|
+
|
|
82
|
+
query.whereNotLike("email", "%spam%");
|
|
83
|
+
|
|
84
|
+
// Multiple patterns (OR)
|
|
85
|
+
query.whereLike("name", ["%Joe%", "%Jane%"]);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## BETWEEN / NOT BETWEEN
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
query.whereBetween("age", [18, 65]);
|
|
92
|
+
// WHERE `age` BETWEEN ? AND ?
|
|
93
|
+
|
|
94
|
+
query.whereNotBetween("created_at", ["2024-01-01", "2024-06-30"]);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## CONTAIN / NOT CONTAIN
|
|
98
|
+
|
|
99
|
+
For JSON array or SET column checks:
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
query.whereContain("tags", "javascript");
|
|
103
|
+
// WHERE JSON_CONTAINS(`tags`, ?)
|
|
104
|
+
|
|
105
|
+
query.whereNotContain("tags", "deprecated");
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## OVERLAPS / NOT OVERLAPS
|
|
109
|
+
|
|
110
|
+
For JSON array overlap checks:
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
query.whereOverlaps("categories", [1, 2, 3]);
|
|
114
|
+
|
|
115
|
+
query.whereNotOverlaps("categories", [4, 5]);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Nested Conditions with whereCondition()
|
|
119
|
+
|
|
120
|
+
Use `QueryCondition` to build complex nested conditions:
|
|
121
|
+
|
|
122
|
+
```javascript
|
|
123
|
+
const { QueryCondition } = require("@axiosleo/orm-mysql");
|
|
124
|
+
|
|
125
|
+
const nested = new QueryCondition();
|
|
126
|
+
nested.where("age", ">", 18).where("age", "<", 65);
|
|
127
|
+
|
|
128
|
+
query
|
|
129
|
+
.where("status", "active")
|
|
130
|
+
.whereCondition(nested);
|
|
131
|
+
// WHERE `status` = ? AND (`age` > ? AND `age` < ?)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Complete Example
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
const results = await db.table("products", "p")
|
|
138
|
+
.where("p.status", "active")
|
|
139
|
+
.whereBetween("p.price", [10, 100])
|
|
140
|
+
.whereIn("p.category_id", [1, 2, 3])
|
|
141
|
+
.whereLike("p.name", "%phone%")
|
|
142
|
+
.whereNotIn("p.id", blockedIds)
|
|
143
|
+
.orderBy("p.price", "asc")
|
|
144
|
+
.page(20, 0)
|
|
145
|
+
.select();
|
|
146
|
+
```
|
package/src/builder.js
CHANGED
|
@@ -523,6 +523,18 @@ class ManageSQLBuilder extends Builder {
|
|
|
523
523
|
}
|
|
524
524
|
}
|
|
525
525
|
|
|
526
|
+
changeColumn(options) {
|
|
527
|
+
_validate(options, {
|
|
528
|
+
tableName: 'required|string',
|
|
529
|
+
columnName: 'required|string',
|
|
530
|
+
});
|
|
531
|
+
const columnOptions = {
|
|
532
|
+
...options.options,
|
|
533
|
+
name: options.columnName,
|
|
534
|
+
};
|
|
535
|
+
return _render('ALTER TABLE `${tableName}` MODIFY COLUMN ' + this.renderSingleColumn(columnOptions), { tableName: options.tableName });
|
|
536
|
+
}
|
|
537
|
+
|
|
526
538
|
renameTable(options) {
|
|
527
539
|
_validate(options, {
|
|
528
540
|
oldName: 'required|string',
|
package/src/migration.js
CHANGED
|
@@ -337,6 +337,19 @@ function _initMigration(file, queries = {}) {
|
|
|
337
337
|
}, ...baseAttr
|
|
338
338
|
});
|
|
339
339
|
|
|
340
|
+
Object.defineProperty(migration, 'changeColumn', {
|
|
341
|
+
value: function (tableName, columnName, options) {
|
|
342
|
+
const builder = new ManageSQLBuilder({
|
|
343
|
+
operator: 'change',
|
|
344
|
+
target: 'column',
|
|
345
|
+
tableName,
|
|
346
|
+
columnName,
|
|
347
|
+
options
|
|
348
|
+
});
|
|
349
|
+
queries[file].push({ sql: builder.sql, values: builder.values });
|
|
350
|
+
}, ...baseAttr
|
|
351
|
+
});
|
|
352
|
+
|
|
340
353
|
return migration;
|
|
341
354
|
}
|
|
342
355
|
|