tangbao-he-db-helper 1.0.21 → 1.0.24

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 CHANGED
@@ -29,6 +29,7 @@ MyBatis-style database helper for Node-RED with MySQL and SQLite support.
29
29
 
30
30
  - Node.js >= 18.0
31
31
  - Node-RED >= 4.0.0
32
+ - **For SQLite support**: `better-sqlite3` >= 12.0.0 (installed separately)
32
33
 
33
34
  ## Installation
34
35
 
@@ -41,7 +42,7 @@ Or install from a local `.tgz` file:
41
42
 
42
43
  ```bash
43
44
  cd ~/.node-red
44
- npm install /path/to/tangbao-he-db-helper-1.0.17.tgz
45
+ npm install /path/to/tangbao-he-db-helper-*.tgz
45
46
  ```
46
47
 
47
48
  **For SQLite support, also install:**
@@ -72,7 +73,8 @@ When you select a driver in the configuration panel, the available fields automa
72
73
  - **SQLite**: Database File Path only (e.g., `/data/mydb.db` or `./mydb.db`)
73
74
 
74
75
  **MySQL Settings:**
75
- - Host, Port, Database, User, Password
76
+ - Host, Port, Database
77
+ - User / Password (stored in Node-RED credentials, encrypted at rest)
76
78
  - Charset (default: `UTF8_GENERAL_CI`)
77
79
  - Timezone (default: `local`)
78
80
  - Connection Pool Limit (default: `50`)
@@ -163,6 +165,8 @@ Pre-built CRUD operations. No SQL writing needed.
163
165
  **SQLite Upsert Note:**
164
166
  SQLite uses `ON CONFLICT(id) DO UPDATE SET col = excluded.col` instead of MySQL's `ON DUPLICATE KEY UPDATE col = VALUES(col)`. The CRUD node automatically selects the correct syntax based on your configured driver.
165
167
 
168
+ > **Important:** For `upsertBatch` to work with SQLite, the target table must have a **PRIMARY KEY** or **UNIQUE** constraint on the `idColumn`. Otherwise SQLite will raise a conflict resolution error.
169
+
166
170
  **Array to IN:**
167
171
 
168
172
  For `selectList`, `selectOne`, `selectCount`, and `selectPage`, array values are automatically converted to `IN` conditions:
@@ -250,10 +254,23 @@ msg.params = { userName: "Alice", userAge: 20 };
250
254
 
251
255
  2. Restart Node-RED.
252
256
 
253
- 3. Create a `tangbao-db-config` node, select **SQLite** as the driver, and enter the database file path (e.g., `./data.db`).
257
+ 3. Create a `tangbao-db-config` node, select **SQLite** as the driver, and enter the database file path (e.g., `./data.db` or an absolute path like `/data/mydb.db`).
254
258
 
255
259
  4. Use `tangbao-db-crud` or `tangbao-db-query` / `tangbao-db-execute` as usual. All CRUD operations work the same way.
256
260
 
261
+ ### SQLite vs MySQL Quick Reference
262
+
263
+ | Feature | MySQL | SQLite |
264
+ |---------|-------|--------|
265
+ | Connection | TCP (host:port) | Local file |
266
+ | Auth | User / Password | None |
267
+ | Pooling | Yes (configurable) | No (single connection) |
268
+ | Auto-reconnect | Yes | N/A |
269
+ | `upsertBatch` | `ON DUPLICATE KEY UPDATE` | `ON CONFLICT DO UPDATE` (requires PK/UNIQUE) |
270
+ | `deleteAndInsertBatch` | Transaction | Transaction |
271
+ | Table discovery | `information_schema` | `sqlite_master` |
272
+ | Multi-statement execute | Supported with params | Supported **without** params |
273
+
257
274
  ## Logging Configuration (日志配置)
258
275
 
259
276
  本节点包使用 `node.log` / `node.error` 输出运行日志,其输出级别受 Node-RED 全局日志配置控制。
package/db-config.js CHANGED
@@ -180,6 +180,117 @@ module.exports = function(RED) {
180
180
  }
181
181
  };
182
182
 
183
+ /**
184
+ * 安全拆分 SQL 语句(排除字符串和注释内的分号)
185
+ */
186
+ function splitSqlStatements(sql) {
187
+ var statements = [];
188
+ var current = '';
189
+ var inString = false;
190
+ var stringChar = null;
191
+ var inComment = false;
192
+ var commentType = null; // 'line' or 'block'
193
+ var i = 0;
194
+ while (i < sql.length) {
195
+ var ch = sql[i];
196
+ var next = sql[i + 1];
197
+ if (inComment) {
198
+ if (commentType === 'line' && ch === '\n') {
199
+ inComment = false;
200
+ commentType = null;
201
+ } else if (commentType === 'block' && ch === '*' && next === '/') {
202
+ inComment = false;
203
+ commentType = null;
204
+ i++;
205
+ }
206
+ current += ch;
207
+ } else if (inString) {
208
+ if (ch === '\\' && i + 1 < sql.length) {
209
+ current += ch + sql[i + 1];
210
+ i++;
211
+ } else if (ch === stringChar) {
212
+ inString = false;
213
+ stringChar = null;
214
+ current += ch;
215
+ } else {
216
+ current += ch;
217
+ }
218
+ } else {
219
+ if (ch === '-' && next === '-') {
220
+ inComment = true;
221
+ commentType = 'line';
222
+ current += ch;
223
+ } else if (ch === '/' && next === '*') {
224
+ inComment = true;
225
+ commentType = 'block';
226
+ current += ch;
227
+ } else if (ch === "'" || ch === '"') {
228
+ inString = true;
229
+ stringChar = ch;
230
+ current += ch;
231
+ } else if (ch === ';') {
232
+ statements.push(current.trim());
233
+ current = '';
234
+ } else {
235
+ current += ch;
236
+ }
237
+ }
238
+ i++;
239
+ }
240
+ if (current.trim()) {
241
+ statements.push(current.trim());
242
+ }
243
+ return statements;
244
+ }
245
+
246
+ /**
247
+ * 统计 SQL 中 ? 占位符数量(排除字符串/注释内)
248
+ */
249
+ function countPlaceholders(sql) {
250
+ var count = 0;
251
+ var inString = false;
252
+ var stringChar = null;
253
+ var inComment = false;
254
+ var commentType = null;
255
+ var i = 0;
256
+ while (i < sql.length) {
257
+ var ch = sql[i];
258
+ var next = sql[i + 1];
259
+ if (inComment) {
260
+ if (commentType === 'line' && ch === '\n') {
261
+ inComment = false;
262
+ commentType = null;
263
+ } else if (commentType === 'block' && ch === '*' && next === '/') {
264
+ inComment = false;
265
+ commentType = null;
266
+ i++;
267
+ }
268
+ } else if (inString) {
269
+ if (ch === '\\' && i + 1 < sql.length) {
270
+ i++;
271
+ } else if (ch === stringChar) {
272
+ inString = false;
273
+ stringChar = null;
274
+ }
275
+ } else {
276
+ if (ch === '-' && next === '-') {
277
+ inComment = true;
278
+ commentType = 'line';
279
+ } else if (ch === '/' && next === '*') {
280
+ inComment = true;
281
+ commentType = 'block';
282
+ } else if (ch === "'" || ch === '"') {
283
+ inString = true;
284
+ stringChar = ch;
285
+ } else if (ch === '?') {
286
+ count++;
287
+ }
288
+ }
289
+ i++;
290
+ }
291
+ return count;
292
+ }
293
+
183
294
  /**
184
295
  * SQLite 驱动实现
185
296
  */
@@ -227,9 +338,34 @@ module.exports = function(RED) {
227
338
  if (!this.db) {
228
339
  throw new Error('SQLite database is not available');
229
340
  }
230
- var stmt = this.db.prepare(sql);
231
- var info = stmt.run(params || []);
232
- return { affectedRows: info.changes || 0, insertId: info.lastInsertRowid };
341
+ params = params || [];
342
+ // 安全拆分多语句:排除字符串/注释内的分号
343
+ var statements = splitSqlStatements(sql);
344
+ if (statements.length === 0) {
345
+ throw new Error('No valid SQL statement found');
346
+ }
347
+ var totalAffected = 0;
348
+ var lastInsertId = null;
349
+ var paramIdx = 0;
350
+ for (var i = 0; i < statements.length; i++) {
351
+ var stmtSql = statements[i].trim();
352
+ if (!stmtSql) continue;
353
+ // 统计该语句中的 ? 占位符数量(排除字符串内的)
354
+ var placeholderCount = countPlaceholders(stmtSql);
355
+ var stmtParams = params.slice(paramIdx, paramIdx + placeholderCount);
356
+ paramIdx += placeholderCount;
357
+ if (placeholderCount === 0) {
358
+ this.db.exec(stmtSql);
359
+ } else {
360
+ var stmt = this.db.prepare(stmtSql);
361
+ var info = stmt.run(stmtParams);
362
+ totalAffected += info.changes || 0;
363
+ if (info.lastInsertRowid != null) {
364
+ lastInsertId = info.lastInsertRowid;
365
+ }
366
+ }
367
+ }
368
+ return { affectedRows: totalAffected, insertId: lastInsertId };
233
369
  } catch (err) {
234
370
  this.node.error('SQLite execute failed: ' + err.message);
235
371
  throw err;
package/db-crud.html CHANGED
@@ -159,7 +159,7 @@
159
159
  }
160
160
 
161
161
  $select.html('<option value="">-- 加载中 --</option>');
162
- $.getJSON('/tangbao-db-config/' + dbConfigId + '/tables', function(tables) {
162
+ $.getJSON('tangbao-db-config/' + dbConfigId + '/tables', function(tables) {
163
163
  var html = '<option value="">-- 请选择表 --</option>';
164
164
  var hasCurrent = false;
165
165
  tables.forEach(function(t) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tangbao-he-db-helper",
3
- "version": "1.0.21",
3
+ "version": "1.0.24",
4
4
  "description": "MyBatis-style database helper for Node-RED with CRUD, SQL mapper, and multi-database support",
5
5
  "main": "db-config.js",
6
6
  "scripts": {