sqlstack 1.0.17 → 1.0.18

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
@@ -262,56 +262,320 @@ await repo.listByIds({ ids });
262
262
  - `null` → SQL `NULL`
263
263
  - `undefined` → throws an error; always provide a value or omit the parameter
264
264
 
265
- ### Automatic NULL Binding with `@MissingAsNull`
265
+ ### :update() Syntax for Partial Updates
266
266
 
267
- By default, missing or undefined named parameters throw an error. Use `@MissingAsNull` to automatically bind missing parameters as `NULL` instead, enabling partial updates with `COALESCE`/`IFNULL`:
267
+ The `:update()` syntax simplifies UPDATE statements by automatically generating SET clauses. It supports both parameterized columns and literal SQL expressions, making partial updates easy without needing `COALESCE` or `IFNULL`.
268
+
269
+ **Key Features:**
270
+ - Automatically generates `SET` keyword (don't write it yourself)
271
+ - Literal expressions: `col = datetime('now')` → always included
272
+ - Parameterized columns: `col` → only included if defined in args
273
+ - Mixed usage: combine literals and parameterized columns
274
+
275
+ **Basic Usage:**
276
+
277
+ ```sql
278
+ -- updateInvoice.sql
279
+ UPDATE invoices
280
+ :update(invoice_date, due_date, subtotal_cents)
281
+ WHERE id = :id
282
+ ```
268
283
 
269
284
  ```ts
270
- import { QueryBinder, MissingAsNull, Query, SqlStackError } from "sqlstack";
285
+ await repo.updateInvoice({
286
+ invoice_date: '2024-01-01',
287
+ id: 1
288
+ });
289
+ // Generates: UPDATE invoices SET invoice_date = ? WHERE id = ?
290
+ // due_date and subtotal_cents are skipped (undefined)
291
+ ```
271
292
 
272
- @QueryBinder()
273
- class UsersRepo {
274
- @MissingAsNull()
275
- @Query({
276
- sql: `
277
- UPDATE users
278
- SET name = COALESCE(:name, name),
279
- email = COALESCE(:email, email),
280
- status = COALESCE(:status, status)
293
+ **Literal SQL Expressions:**
294
+
295
+ Use `=` to include literal SQL expressions that are always included:
296
+
297
+ ```sql
298
+ UPDATE invoices
299
+ :update(updated_at = datetime('now'), invoice_date, due_date)
281
300
  WHERE id = :id
282
- `
283
- })
284
- async updateUserPartial(_a: { id: string; name?: string; email?: string; status?: string }): Promise<any> {
285
- throw new SqlStackError("replaced");
286
- }
287
- }
288
301
  ```
289
302
 
290
- Call with partial arguments:
291
303
  ```ts
292
- await repo.updateUserPartial({ id: "u1", name: "Alice Updated" });
293
- // Only updates name; email and status keep their old values via COALESCE
304
+ await repo.updateInvoice({
305
+ invoice_date: '2024-01-01',
306
+ id: 1
307
+ });
308
+ // Generates: UPDATE invoices SET updated_at = datetime('now'), invoice_date = ? WHERE id = ?
294
309
  ```
295
310
 
296
- **Optional: Require Specific Parameters**
311
+ **Mixed Literal and Parameterized:**
297
312
 
298
- Use `@MissingAsNull({ require: ['id'] })` to mark certain parameters as required:
313
+ ```sql
314
+ UPDATE invoices
315
+ :update(
316
+ updated_at = datetime('now'),
317
+ invoice_date,
318
+ due_date,
319
+ status = 'active'
320
+ )
321
+ WHERE id = :id
322
+ ```
299
323
 
300
324
  ```ts
301
- @MissingAsNull({ require: ['id'] })
302
- @Query({
303
- sql: `UPDATE users SET name = COALESCE(:name, name) WHERE id = :id`
304
- })
305
- async updateUser(_a: { id: string; name?: string }): Promise<any> {
306
- throw new SqlStackError("replaced");
307
- }
325
+ await repo.updateInvoice({
326
+ invoice_date: '2024-01-01',
327
+ due_date: '2024-01-15',
328
+ id: 1
329
+ });
330
+ // Generates: UPDATE invoices SET updated_at = datetime('now'), invoice_date = ?, due_date = ?, status = 'active' WHERE id = ?
308
331
  ```
309
332
 
310
- If `id` is missing or undefined, an error is thrown. Other parameters default to `NULL`.
333
+ **Notes:**
334
+ - `:update()` only works with named parameters (not positional)
335
+ - Literal expressions are always included
336
+ - Parameterized columns are only included if defined and not `undefined`
337
+ - If all parameterized columns are undefined (and no literals), throws an error
338
+ - Case-insensitive: `:update`, `:UPDATE`, `:Update` all work
311
339
 
312
- **Note:** `@MissingAsNull` only applies to named parameters. Positional parameters (`:arg1`, `:arg2`, etc.) ignore the decorator.
340
+ ### :insert() Syntax for INSERT Statements
313
341
 
314
- ---
342
+ The `:insert()` syntax simplifies INSERT statements by automatically generating both the column list and VALUES clause. It supports both parameterized columns and literal SQL expressions, making INSERT statements much more concise.
343
+
344
+ **Key Features:**
345
+ - Automatically generates column list `(col1, col2, ...)` and VALUES clause `VALUES (?, ?, ...)`
346
+ - Literal expressions: `col = datetime('now')` → column name and literal value included
347
+ - Parameterized columns: `col` → only included if defined in args
348
+ - Mixed usage: combine literals and parameterized columns
349
+
350
+ **Basic Usage:**
351
+
352
+ ```sql
353
+ -- createInvoice.sql
354
+ INSERT INTO invoices :insert(case_id, invoice_number, invoice_date, due_date)
355
+ ```
356
+
357
+ ```ts
358
+ await repo.createInvoice({
359
+ case_id: 1,
360
+ invoice_number: 'INV-001',
361
+ invoice_date: '2024-01-01',
362
+ due_date: '2024-01-15'
363
+ });
364
+ // Generates: INSERT INTO invoices (case_id, invoice_number, invoice_date, due_date) VALUES (?, ?, ?, ?)
365
+ // Params: [1, 'INV-001', '2024-01-01', '2024-01-15']
366
+ ```
367
+
368
+ **With Undefined Columns:**
369
+
370
+ ```sql
371
+ INSERT INTO invoices :insert(case_id, invoice_number, invoice_date, due_date)
372
+ ```
373
+
374
+ ```ts
375
+ await repo.createInvoice({
376
+ case_id: 1,
377
+ invoice_number: 'INV-001'
378
+ });
379
+ // Generates: INSERT INTO invoices (case_id, invoice_number) VALUES (?, ?)
380
+ // invoice_date and due_date are skipped (undefined)
381
+ ```
382
+
383
+ **Literal SQL Expressions:**
384
+
385
+ Use `=` to include literal SQL expressions that are always included:
386
+
387
+ ```sql
388
+ INSERT INTO invoices :insert(created_at = datetime('now'), case_id, invoice_number)
389
+ ```
390
+
391
+ ```ts
392
+ await repo.createInvoice({
393
+ case_id: 1,
394
+ invoice_number: 'INV-001'
395
+ });
396
+ // Generates: INSERT INTO invoices (created_at, case_id, invoice_number) VALUES (datetime('now'), ?, ?)
397
+ // Params: [1, 'INV-001']
398
+ ```
399
+
400
+ **Mixed Literal and Parameterized:**
401
+
402
+ ```sql
403
+ INSERT INTO invoices :insert(
404
+ created_at = datetime('now'),
405
+ case_id,
406
+ invoice_number,
407
+ status = 'draft'
408
+ )
409
+ ```
410
+
411
+ ```ts
412
+ await repo.createInvoice({
413
+ case_id: 1,
414
+ invoice_number: 'INV-001'
415
+ });
416
+ // Generates: INSERT INTO invoices (created_at, case_id, invoice_number, status) VALUES (datetime('now'), ?, ?, 'draft')
417
+ // Params: [1, 'INV-001']
418
+ ```
419
+
420
+ **Notes:**
421
+ - `:insert()` only works with named parameters (not positional)
422
+ - Literal expressions are always included (both column name and value)
423
+ - Parameterized columns are only included if defined and not `undefined`
424
+ - If all parameterized columns are undefined (and no literals), throws an error
425
+ - Case-insensitive: `:insert`, `:INSERT`, `:Insert` all work
426
+
427
+ ### :filter() Syntax for WHERE/HAVING Clauses
428
+
429
+ The `:filter()` syntax simplifies WHERE and HAVING clauses by automatically generating conditions. It supports simple column names (equality), literal comparisons, and parameterized comparisons, making dynamic filtering much easier.
430
+
431
+ **Key Features:**
432
+ - Simple column names → equality: `name` → `name = ?`
433
+ - Literal comparisons → included as-is: `age > 30` → `age > 30`
434
+ - Parameterized comparisons → bind parameters: `age > :min_age` → `age > ?`
435
+ - Mixed usage: combine all three types
436
+ - Works in both WHERE and HAVING clauses
437
+ - Conditions are joined with `AND`
438
+
439
+ **Basic Usage:**
440
+
441
+ ```sql
442
+ -- findUsers.sql
443
+ SELECT * FROM users WHERE :filter(name, age)
444
+ ```
445
+
446
+ ```ts
447
+ await repo.findUsers({ name: 'John', age: 30 });
448
+ // Generates: SELECT * FROM users WHERE name = ? AND age = ?
449
+ // Params: ['John', 30]
450
+ ```
451
+
452
+ **With Some Undefined Columns:**
453
+
454
+ ```sql
455
+ SELECT * FROM users WHERE :filter(name, age, status)
456
+ ```
457
+
458
+ ```ts
459
+ await repo.findUsers({ name: 'John' });
460
+ // Generates: SELECT * FROM users WHERE name = ?
461
+ // age and status are skipped (undefined)
462
+ ```
463
+
464
+ **Literal Comparisons:**
465
+
466
+ ```sql
467
+ SELECT * FROM users WHERE :filter(age > 30, status = 'active')
468
+ ```
469
+
470
+ ```ts
471
+ await repo.findUsers({});
472
+ // Generates: SELECT * FROM users WHERE age > 30 AND status = 'active'
473
+ // Params: []
474
+ ```
475
+
476
+ **Parameterized Comparisons:**
477
+
478
+ ```sql
479
+ SELECT * FROM users WHERE :filter(age > :min_age, name LIKE :search)
480
+ ```
481
+
482
+ ```ts
483
+ await repo.findUsers({ min_age: 30, search: '%John%' });
484
+ // Generates: SELECT * FROM users WHERE age > ? AND name LIKE ?
485
+ // Params: [30, '%John%']
486
+ ```
487
+
488
+ **Mixed Usage:**
489
+
490
+ ```sql
491
+ SELECT * FROM users WHERE :filter(name, age > :min_age, status = 'active')
492
+ ```
493
+
494
+ ```ts
495
+ await repo.findUsers({ name: 'John', min_age: 30 });
496
+ // Generates: SELECT * FROM users WHERE name = ? AND age > ? AND status = 'active'
497
+ // Params: ['John', 30]
498
+ ```
499
+
500
+ **With Array Parameters (IN clause):**
501
+
502
+ ```sql
503
+ SELECT * FROM users WHERE :filter(id IN (:ids))
504
+ ```
505
+
506
+ ```ts
507
+ await repo.findUsers({ ids: [1, 2, 3] });
508
+ // Generates: SELECT * FROM users WHERE id IN (?, ?, ?)
509
+ // Params: [1, 2, 3]
510
+ ```
511
+
512
+ **With :or() Expansion:**
513
+
514
+ The `:or()` syntax expands array parameters into multiple OR conditions:
515
+
516
+ ```sql
517
+ SELECT * FROM users WHERE :filter(name = :or(:names), age > :min_age)
518
+ ```
519
+
520
+ ```ts
521
+ await repo.findUsers({ names: ['John', 'Jane', 'Bob'], min_age: 30 });
522
+ // Generates: SELECT * FROM users WHERE (name = ? OR name = ? OR name = ?) AND age > ?
523
+ // Params: ['John', 'Jane', 'Bob', 30]
524
+ ```
525
+
526
+ **Complex :or() Usage:**
527
+
528
+ ```sql
529
+ SELECT * FROM users WHERE :filter(
530
+ (name = :or(:names)),
531
+ age > :min_age,
532
+ status = 'active',
533
+ (department = :or(:depts) OR role = :or(:roles))
534
+ )
535
+ ```
536
+
537
+ ```ts
538
+ await repo.findUsers({
539
+ names: ['John', 'Jane'],
540
+ min_age: 25,
541
+ depts: ['Engineering'],
542
+ roles: ['Manager']
543
+ });
544
+ // Generates: SELECT * FROM users WHERE (name = ? OR name = ?) AND age > ? AND status = 'active' AND (department = ? OR role = ?)
545
+ // Params: ['John', 'Jane', 25, 'Engineering', 'Manager']
546
+ ```
547
+
548
+ **HAVING Clause:**
549
+
550
+ ```sql
551
+ SELECT department, COUNT(*) as count
552
+ FROM employees
553
+ GROUP BY department
554
+ HAVING :filter(count > :min_count, department = 'Sales')
555
+ ```
556
+
557
+ ```ts
558
+ await repo.findDepartments({ min_count: 10 });
559
+ // Generates: SELECT department, COUNT(*) as count FROM employees GROUP BY department HAVING count > ? AND department = 'Sales'
560
+ // Params: [10]
561
+ ```
562
+
563
+ **Supported Operators:**
564
+
565
+ - Comparison: `=`, `!=`, `<>`, `>`, `<`, `>=`, `<=`
566
+ - Pattern matching: `LIKE`
567
+ - Membership: `IN`
568
+ - Range: `BETWEEN`
569
+ - Null checks: `IS NULL`, `IS NOT NULL`
570
+
571
+ **Notes:**
572
+ - `:filter()` only works with named parameters (not positional)
573
+ - Literal expressions are always included
574
+ - Parameterized conditions are only included if the parameter is defined and not `undefined`
575
+ - If all parameterized conditions are undefined (and no literals), throws an error
576
+ - Case-insensitive: `:filter`, `:FILTER`, `:Filter` all work
577
+ - `:or()` expands arrays into multiple OR conditions
578
+ - Operators inside string literals are not treated as operators (e.g., `name = 'value > test'`)
315
579
 
316
580
  ## SQL File Resolution
317
581