sqlstack 3.3.0 → 3.5.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/dist/cjs/adapters/sqlite.d.ts.map +1 -1
- package/dist/cjs/adapters/sqlite.js +4 -1
- package/dist/cjs/adapters/sqlite.js.map +1 -1
- package/dist/cjs/core/errors.d.ts +9 -0
- package/dist/cjs/core/errors.d.ts.map +1 -1
- package/dist/cjs/core/errors.js +10 -0
- package/dist/cjs/core/errors.js.map +1 -1
- package/dist/cjs/decorators/query.d.ts +14 -0
- package/dist/cjs/decorators/query.d.ts.map +1 -1
- package/dist/cjs/decorators/query.js +124 -17
- package/dist/cjs/decorators/query.js.map +1 -1
- package/dist/cjs/index.d.ts +2 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +4 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/run.d.ts +140 -6
- package/dist/cjs/run.d.ts.map +1 -1
- package/dist/cjs/run.js +127 -17
- package/dist/cjs/run.js.map +1 -1
- package/dist/cjs/runtime.d.ts +8 -0
- package/dist/cjs/runtime.d.ts.map +1 -1
- package/dist/cjs/runtime.js +12 -0
- package/dist/cjs/runtime.js.map +1 -1
- package/dist/cjs/sql.d.ts +19 -0
- package/dist/cjs/sql.d.ts.map +1 -1
- package/dist/cjs/sql.js +66 -28
- package/dist/cjs/sql.js.map +1 -1
- package/dist/cjs/stack.d.ts +2 -0
- package/dist/cjs/stack.d.ts.map +1 -1
- package/dist/cjs/stack.js +40 -27
- package/dist/cjs/stack.js.map +1 -1
- package/dist/cjs/testing/builder.d.ts.map +1 -1
- package/dist/cjs/testing/builder.js +14 -5
- package/dist/cjs/testing/builder.js.map +1 -1
- package/dist/cjs/testing/fixture-provenance.d.ts +3 -0
- package/dist/cjs/testing/fixture-provenance.d.ts.map +1 -0
- package/dist/cjs/testing/fixture-provenance.js +21 -0
- package/dist/cjs/testing/fixture-provenance.js.map +1 -0
- package/dist/cjs/testing/index.d.ts +2 -0
- package/dist/cjs/testing/index.d.ts.map +1 -1
- package/dist/cjs/testing/index.js +5 -1
- package/dist/cjs/testing/index.js.map +1 -1
- package/dist/cjs/testing/postgres/database.d.ts +9 -1
- package/dist/cjs/testing/postgres/database.d.ts.map +1 -1
- package/dist/cjs/testing/postgres/database.js +17 -13
- package/dist/cjs/testing/postgres/database.js.map +1 -1
- package/dist/cjs/testing/resource.d.ts +76 -0
- package/dist/cjs/testing/resource.d.ts.map +1 -0
- package/dist/cjs/testing/resource.js +117 -0
- package/dist/cjs/testing/resource.js.map +1 -0
- package/dist/cjs/testing/sqlite/builder.d.ts.map +1 -1
- package/dist/cjs/testing/sqlite/builder.js +10 -2
- package/dist/cjs/testing/sqlite/builder.js.map +1 -1
- package/dist/cjs/testing/sqlite/database.d.ts +11 -1
- package/dist/cjs/testing/sqlite/database.d.ts.map +1 -1
- package/dist/cjs/testing/sqlite/database.js +32 -18
- package/dist/cjs/testing/sqlite/database.js.map +1 -1
- package/dist/cjs/transactions.d.ts +14 -0
- package/dist/cjs/transactions.d.ts.map +1 -1
- package/dist/cjs/transactions.js +27 -0
- package/dist/cjs/transactions.js.map +1 -1
- package/dist/esm/adapters/sqlite.js +4 -1
- package/dist/esm/adapters/sqlite.js.map +1 -1
- package/dist/esm/core/errors.js +10 -0
- package/dist/esm/core/errors.js.map +1 -1
- package/dist/esm/decorators/query.js +127 -20
- package/dist/esm/decorators/query.js.map +1 -1
- package/dist/esm/index.js +3 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/run.js +123 -18
- package/dist/esm/run.js.map +1 -1
- package/dist/esm/runtime.js +11 -0
- package/dist/esm/runtime.js.map +1 -1
- package/dist/esm/sql.js +63 -27
- package/dist/esm/sql.js.map +1 -1
- package/dist/esm/stack.js +40 -27
- package/dist/esm/stack.js.map +1 -1
- package/dist/esm/testing/builder.js +14 -5
- package/dist/esm/testing/builder.js.map +1 -1
- package/dist/esm/testing/fixture-provenance.js +15 -0
- package/dist/esm/testing/fixture-provenance.js.map +1 -0
- package/dist/esm/testing/index.js +1 -0
- package/dist/esm/testing/index.js.map +1 -1
- package/dist/esm/testing/postgres/database.js +17 -13
- package/dist/esm/testing/postgres/database.js.map +1 -1
- package/dist/esm/testing/resource.js +106 -0
- package/dist/esm/testing/resource.js.map +1 -0
- package/dist/esm/testing/sqlite/builder.js +10 -2
- package/dist/esm/testing/sqlite/builder.js.map +1 -1
- package/dist/esm/testing/sqlite/database.js +32 -18
- package/dist/esm/testing/sqlite/database.js.map +1 -1
- package/dist/esm/transactions.js +25 -0
- package/dist/esm/transactions.js.map +1 -1
- package/package.json +1 -1
- package/readme.md +128 -39
package/readme.md
CHANGED
|
@@ -16,6 +16,7 @@ Write real SQL next to your code, and use small, composable decorators to bind,
|
|
|
16
16
|
- [Error Handling](#error-handling)
|
|
17
17
|
- [Transactions](#transactions)
|
|
18
18
|
- [Inline SQL](#inline-sql)
|
|
19
|
+
- [Choosing the Database at Call Time](#choosing-the-database-at-call-time)
|
|
19
20
|
- [Advanced: Direct Database Access](#advanced-direct-database-access)
|
|
20
21
|
- [Database Support](#database-support)
|
|
21
22
|
- [Philosophy](#philosophy)
|
|
@@ -78,15 +79,16 @@ Load this file once during app startup.
|
|
|
78
79
|
|
|
79
80
|
```ts
|
|
80
81
|
// src/users/index.ts
|
|
81
|
-
import { QueryBinder, Query,
|
|
82
|
+
import { QueryBinder, Query, run } from "sqlstack";
|
|
82
83
|
|
|
83
84
|
@QueryBinder() // looks for .sql files in the same folder by default
|
|
84
85
|
export class UsersRepository {
|
|
85
86
|
@Query()
|
|
86
87
|
async findByEmail(params: { email: string }): Promise<User[]> {
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
88
|
+
// run() executes this method's SQL (findByEmail.sql) with the method's
|
|
89
|
+
// arguments; the result type is inferred from the declared return type.
|
|
90
|
+
// Code before/after `await run()` runs normally (logging, try/catch, ...).
|
|
91
|
+
return run();
|
|
90
92
|
}
|
|
91
93
|
}
|
|
92
94
|
```
|
|
@@ -116,7 +118,7 @@ const users = await repo.findByEmail({ email: "alice@example.com" });
|
|
|
116
118
|
console.log(users); // User[]
|
|
117
119
|
```
|
|
118
120
|
|
|
119
|
-
That's it! The `@Query` decorator
|
|
121
|
+
That's it! The `@Query` decorator wraps the method; the body's `return run()` loads and executes the SQL and returns the shaped results. (Older code whose body is a placeholder `throw` still works — the wrapper falls back to running the SQL — but `return run()` is the supported form.)
|
|
120
122
|
|
|
121
123
|
You can then add more methods on the same repository that use sqlstack’s SQL helpers:
|
|
122
124
|
|
|
@@ -125,22 +127,22 @@ You can then add more methods on the same repository that use sqlstack’s SQL h
|
|
|
125
127
|
export class UsersRepository {
|
|
126
128
|
@Query()
|
|
127
129
|
async findByEmail(params: { email: string }): Promise<User[]> {
|
|
128
|
-
|
|
130
|
+
return run();
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
@Query()
|
|
132
134
|
async updateProfile(params: { id: string; name?: string; status?: string }): Promise<WriteResult> {
|
|
133
|
-
|
|
135
|
+
return run();
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
@Query()
|
|
137
139
|
async createUser(params: { id: string; email: string; name?: string }): Promise<WriteResult> {
|
|
138
|
-
|
|
140
|
+
return run();
|
|
139
141
|
}
|
|
140
142
|
|
|
141
143
|
@Query()
|
|
142
144
|
async search(filters: { name?: string; status?: string }): Promise<User[]> {
|
|
143
|
-
|
|
145
|
+
return run();
|
|
144
146
|
}
|
|
145
147
|
}
|
|
146
148
|
```
|
|
@@ -407,13 +409,13 @@ SqlStackDB.register('sqlite-existing', createSqliteDb(sqlite));
|
|
|
407
409
|
Use `@QueryBinder({ db: "name" })` to set a default database for all methods in a class:
|
|
408
410
|
|
|
409
411
|
```ts
|
|
410
|
-
import { QueryBinder, Query,
|
|
412
|
+
import { QueryBinder, Query, run } from "sqlstack";
|
|
411
413
|
|
|
412
414
|
@QueryBinder({ db: "analytics" }) // all methods use "analytics"
|
|
413
415
|
export class ReportsRepo {
|
|
414
416
|
@Query()
|
|
415
417
|
async topPages(_a: { since: string }): Promise<Row[]> {
|
|
416
|
-
|
|
418
|
+
return run();
|
|
417
419
|
}
|
|
418
420
|
}
|
|
419
421
|
```
|
|
@@ -427,12 +429,12 @@ Use `@Query({ db: "name" })` to override the class default for a specific method
|
|
|
427
429
|
export class MixedRepo {
|
|
428
430
|
@Query() // uses "primary"
|
|
429
431
|
async dailySummary(_a: { date: string }): Promise<Row[]> {
|
|
430
|
-
|
|
432
|
+
return run();
|
|
431
433
|
}
|
|
432
434
|
|
|
433
435
|
@Query({ db: "analytics" }) // override to use "analytics"
|
|
434
436
|
async topPages(_a: { since: string }): Promise<Row[]> {
|
|
435
|
-
|
|
437
|
+
return run();
|
|
436
438
|
}
|
|
437
439
|
}
|
|
438
440
|
```
|
|
@@ -514,7 +516,7 @@ No argument to `@Query()` → looks in the same folder as the class file:
|
|
|
514
516
|
class UsersRepo {
|
|
515
517
|
@Query()
|
|
516
518
|
async findByEmail(_a: { email: string }): Promise<User[]> {
|
|
517
|
-
|
|
519
|
+
return run();
|
|
518
520
|
}
|
|
519
521
|
}
|
|
520
522
|
```
|
|
@@ -560,14 +562,14 @@ Use decorators to add behavior to your methods. They compose freely and always e
|
|
|
560
562
|
Provide default parameter values:
|
|
561
563
|
|
|
562
564
|
```ts
|
|
563
|
-
import { QueryBinder, Defaults, Query,
|
|
565
|
+
import { QueryBinder, Defaults, Query, run } from "sqlstack";
|
|
564
566
|
|
|
565
567
|
@QueryBinder()
|
|
566
568
|
class UsersRepo {
|
|
567
569
|
@Defaults({ status: "active", limit: 10 })
|
|
568
570
|
@Query()
|
|
569
571
|
async listUsers(_a: { status?: string; limit?: number }): Promise<User[]> {
|
|
570
|
-
|
|
572
|
+
return run();
|
|
571
573
|
}
|
|
572
574
|
}
|
|
573
575
|
```
|
|
@@ -583,14 +585,14 @@ await repo.listUsers({ status: "inactive" }); // { status: "inactive", limit: 10
|
|
|
583
585
|
Add pagination with `:offset` and `:limit` parameters. Pages are 1-based by default:
|
|
584
586
|
|
|
585
587
|
```ts
|
|
586
|
-
import { QueryBinder, Page, Query,
|
|
588
|
+
import { QueryBinder, Page, Query, run } from "sqlstack";
|
|
587
589
|
|
|
588
590
|
@QueryBinder()
|
|
589
591
|
class UsersRepo {
|
|
590
592
|
@Page(10) // limit = 10, pages 1-based
|
|
591
593
|
@Query()
|
|
592
594
|
async listUsers(params: { status?: string; page?: number }): Promise<User[]> {
|
|
593
|
-
|
|
595
|
+
return run();
|
|
594
596
|
}
|
|
595
597
|
}
|
|
596
598
|
```
|
|
@@ -623,7 +625,7 @@ await repo.list(userId, { page: 2 }); // positional id + trailing option
|
|
|
623
625
|
Shape single-query results without parameters:
|
|
624
626
|
|
|
625
627
|
```ts
|
|
626
|
-
import { QueryBinder, Single, Only, Exist, None, Query,
|
|
628
|
+
import { QueryBinder, Single, Only, Exist, None, Query, run } from "sqlstack";
|
|
627
629
|
|
|
628
630
|
@QueryBinder()
|
|
629
631
|
class UsersRepo {
|
|
@@ -631,28 +633,28 @@ class UsersRepo {
|
|
|
631
633
|
@Single
|
|
632
634
|
@Query()
|
|
633
635
|
async firstOrNull(_a: { email: string }): Promise<User | null> {
|
|
634
|
-
|
|
636
|
+
return run();
|
|
635
637
|
}
|
|
636
638
|
|
|
637
639
|
// @Only → require exactly one row; throw if 0 or >1
|
|
638
640
|
@Only
|
|
639
641
|
@Query()
|
|
640
642
|
async exactlyOne(_a: { id: string }): Promise<User> {
|
|
641
|
-
|
|
643
|
+
return run();
|
|
642
644
|
}
|
|
643
645
|
|
|
644
646
|
// @Exist → require at least one row; return first row; throw if none
|
|
645
647
|
@Exist
|
|
646
648
|
@Query()
|
|
647
649
|
async requireOne(_a: { email: string }): Promise<User> {
|
|
648
|
-
|
|
650
|
+
return run();
|
|
649
651
|
}
|
|
650
652
|
|
|
651
653
|
// @None → require zero rows; return null; throw if any rows
|
|
652
654
|
@None
|
|
653
655
|
@Query()
|
|
654
656
|
async ensureNone(_a: { status: string }): Promise<null> {
|
|
655
|
-
|
|
657
|
+
return run();
|
|
656
658
|
}
|
|
657
659
|
}
|
|
658
660
|
```
|
|
@@ -670,7 +672,7 @@ Validate query results against a JSON Schema:
|
|
|
670
672
|
|
|
671
673
|
```ts
|
|
672
674
|
import schema from "./user.row.schema.json" assert { type: "json" };
|
|
673
|
-
import { QueryBinder, ValidateResult, Single, Query,
|
|
675
|
+
import { QueryBinder, ValidateResult, Single, Query, run } from "sqlstack";
|
|
674
676
|
|
|
675
677
|
@QueryBinder()
|
|
676
678
|
class UsersRepo {
|
|
@@ -678,7 +680,7 @@ class UsersRepo {
|
|
|
678
680
|
@Single
|
|
679
681
|
@Query()
|
|
680
682
|
async findByEmail(_a: { email: string }): Promise<User | null> {
|
|
681
|
-
|
|
683
|
+
return run();
|
|
682
684
|
}
|
|
683
685
|
}
|
|
684
686
|
```
|
|
@@ -735,7 +737,7 @@ class Repo {
|
|
|
735
737
|
WHERE g.id = :id
|
|
736
738
|
ORDER BY m.name, e.name
|
|
737
739
|
` })
|
|
738
|
-
async getGroup(_a: { id: string }): Promise<any | null> {
|
|
740
|
+
async getGroup(_a: { id: string }): Promise<any | null> { return run(); }
|
|
739
741
|
}
|
|
740
742
|
```
|
|
741
743
|
|
|
@@ -806,13 +808,13 @@ Returns an array of rows, which can be shaped via decorators:
|
|
|
806
808
|
class Repo {
|
|
807
809
|
@Query()
|
|
808
810
|
async allUsers(): Promise<User[]> {
|
|
809
|
-
|
|
811
|
+
return run();
|
|
810
812
|
}
|
|
811
813
|
|
|
812
814
|
@Single
|
|
813
815
|
@Query()
|
|
814
816
|
async firstUser(): Promise<User | null> {
|
|
815
|
-
|
|
817
|
+
return run();
|
|
816
818
|
}
|
|
817
819
|
}
|
|
818
820
|
```
|
|
@@ -867,16 +869,18 @@ This lets you start with very simple queries and incrementally adopt more comple
|
|
|
867
869
|
|
|
868
870
|
## Error Handling
|
|
869
871
|
|
|
870
|
-
sqlstack
|
|
872
|
+
sqlstack throws; it does not return result objects. Its errors extend `SqlStackError` (with one historical exception, noted below). Import and catch as needed:
|
|
871
873
|
|
|
872
874
|
```ts
|
|
873
|
-
import { SqlStackError } from "sqlstack";
|
|
875
|
+
import { SqlStackError, DatabaseError } from "sqlstack";
|
|
874
876
|
import { ExactlyOneRowError, NoRowsError, ValidationError } from "sqlstack/errors";
|
|
875
877
|
|
|
876
878
|
try {
|
|
877
879
|
const user = await repo.findByEmail({ email: "alice@example.com" });
|
|
878
880
|
} catch (err) {
|
|
879
|
-
if (err instanceof
|
|
881
|
+
if (err instanceof DatabaseError) {
|
|
882
|
+
console.log("statement failed:", err.sql, err.code, err.cause);
|
|
883
|
+
} else if (err instanceof ExactlyOneRowError) {
|
|
880
884
|
console.log("@Only: expected exactly one row, got a different number");
|
|
881
885
|
} else if (err instanceof NoRowsError) {
|
|
882
886
|
console.log("@Exist: expected at least one row, got none");
|
|
@@ -891,11 +895,15 @@ try {
|
|
|
891
895
|
```
|
|
892
896
|
|
|
893
897
|
Available error classes:
|
|
894
|
-
- `SqlStackError` — base class for all sqlstack errors
|
|
898
|
+
- `SqlStackError` — base class for all sqlstack errors (also used for usage errors such as calling `run()` outside a `@Query` method)
|
|
899
|
+
- `DatabaseError` — a statement failed. `.sql` holds the statement (with placeholders), `.cause` holds the original error (the driver's error, or the macro/binding/name-resolution error), and `.code` copies the cause's string driver code (`SQLITE_CONSTRAINT_UNIQUE`, `23505`, `ER_DUP_ENTRY`, ...) when present
|
|
895
900
|
- `ExactlyOneRowError` — thrown by `@Only` when row count ≠ 1
|
|
896
901
|
- `NoRowsError` — thrown by `@Exist` when no rows found
|
|
902
|
+
- `RowsExistError` — thrown by `@None` when rows exist
|
|
903
|
+
- `ExpectedRowsError` — a shape decorator was applied to a statement that produced no result set
|
|
897
904
|
- `ValidationError` — thrown by `@ValidateResult` on schema mismatch
|
|
898
|
-
|
|
905
|
+
|
|
906
|
+
**The error contract.** Driver errors never pass through raw: they are always wrapped in `DatabaseError` with the original in `.cause`. Parameter-binding errors such as `Missing named parameter :email` are a `DatabaseError` with the original in `.cause` in every `run(...)` form, including the zero-argument `run()`. For the `run(...)` forms that take arguments (`run(query)`, `run(db, query)`, `run(db, params)`), *every* failure before result shaping — macro errors from an `` sql`...` `` fragment, parameter-binding errors, an unknown database name — is a `DatabaseError` with the original in `.cause`; invalid arguments are `SqlStackError`. Errors thrown by your own code — the method body or a `@Transform` function — propagate unchanged.
|
|
899
907
|
|
|
900
908
|
---
|
|
901
909
|
|
|
@@ -929,7 +937,7 @@ Queries in `@Query` decorated methods automatically participate in the active tr
|
|
|
929
937
|
class UsersRepo {
|
|
930
938
|
@Query({ sql: 'INSERT INTO users (email, name) VALUES (:email, :name)' })
|
|
931
939
|
async insert(data: { email: string; name: string }): Promise<WriteResult> {
|
|
932
|
-
|
|
940
|
+
return run();
|
|
933
941
|
}
|
|
934
942
|
}
|
|
935
943
|
|
|
@@ -937,7 +945,7 @@ class UsersRepo {
|
|
|
937
945
|
class ProfilesRepo {
|
|
938
946
|
@Query({ sql: 'INSERT INTO profiles (user_id, bio) VALUES (:userId, :bio)' })
|
|
939
947
|
async insert(data: { userId: string; bio: string }): Promise<WriteResult> {
|
|
940
|
-
|
|
948
|
+
return run();
|
|
941
949
|
}
|
|
942
950
|
}
|
|
943
951
|
```
|
|
@@ -1184,7 +1192,7 @@ async createUser() {
|
|
|
1184
1192
|
|
|
1185
1193
|
## Inline SQL
|
|
1186
1194
|
|
|
1187
|
-
For simple
|
|
1195
|
+
For simple queries, use `@Query({ sql: "..." })` with inline SQL:
|
|
1188
1196
|
|
|
1189
1197
|
```ts
|
|
1190
1198
|
@QueryBinder()
|
|
@@ -1195,18 +1203,99 @@ class Repo {
|
|
|
1195
1203
|
WHERE email = :email
|
|
1196
1204
|
` })
|
|
1197
1205
|
async findByEmail(_a: { email: string }): Promise<User[]> {
|
|
1198
|
-
|
|
1206
|
+
return run();
|
|
1199
1207
|
}
|
|
1200
1208
|
}
|
|
1201
1209
|
```
|
|
1202
1210
|
|
|
1203
1211
|
sqlstack applies the same parameter binding and result shaping as file-based SQL.
|
|
1204
1212
|
|
|
1213
|
+
For SQL built in code, pass an `` sql`...` `` fragment to `run`. The fragment replaces the method's SQL file; its `${}` values are the bound parameters (the method's own arguments are not auto-bound). The database still comes from `@Query({ db })` / `@QueryBinder({ db })` / the default, the active transaction is joined, and the shape/transform/validation decorators still apply:
|
|
1214
|
+
|
|
1215
|
+
```ts
|
|
1216
|
+
import { QueryBinder, Query, Single, run, sql } from "sqlstack";
|
|
1217
|
+
|
|
1218
|
+
@QueryBinder()
|
|
1219
|
+
class Repo {
|
|
1220
|
+
@Single
|
|
1221
|
+
@Query()
|
|
1222
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
1223
|
+
return run(sql`SELECT id, email, name FROM users WHERE email = ${email}`);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
SQL is never accepted as a plain string — always build it with the `sql` tag, and pass the fragment itself (not its `.toSQL()` output) so `run` can bind it for the target dialect.
|
|
1229
|
+
|
|
1230
|
+
> **Deprecated:** a `@Query` body that *returns* an `` sql`...` `` fragment (instead of `` return run(sql`...`) ``) still executes, but only type-checks with an `as any`-style cast. It emits a one-time `DeprecationWarning` (`SQLSTACK_FRAGMENT_BODY`) per method and will be removed in the next major version.
|
|
1231
|
+
|
|
1232
|
+
---
|
|
1233
|
+
|
|
1234
|
+
## Choosing the Database at Call Time
|
|
1235
|
+
|
|
1236
|
+
When the database handle is only known at runtime (per-tenant or per-thread databases, a handle passed into a repository method), pass it to `run` as the first argument. The handle is anything with `query(sql, params)` — a sqlstack `Database` from `createPgDb` / `createMysqlDb` / `createSqliteDb`, or your application's own wrapper — or the name of a registered database. sqlstack never opens databases by path.
|
|
1237
|
+
|
|
1238
|
+
### `run(db, query)` — anywhere
|
|
1239
|
+
|
|
1240
|
+
```ts
|
|
1241
|
+
import { run, sql } from "sqlstack";
|
|
1242
|
+
|
|
1243
|
+
const rows = await run<ImportRow[]>(accountsDb, sql`
|
|
1244
|
+
SELECT 1 AS present FROM data_root_imports
|
|
1245
|
+
WHERE data_root = ${dataRoot} AND kind = ${kind}
|
|
1246
|
+
LIMIT 1
|
|
1247
|
+
`);
|
|
1248
|
+
|
|
1249
|
+
await run(accountsDb, sql`INSERT INTO data_root_imports :insert(${{ data_root: dataRoot, kind }})`);
|
|
1250
|
+
```
|
|
1251
|
+
|
|
1252
|
+
Outside a `@Query` method this returns the raw result: a rows array for statements that produce a result set, `{ rowsAffected, lastInsertId? }` otherwise. Without an explicit type argument the result is `unknown`, never `any`. Inside a `@Query` method, `` return run(db, sql`...`) `` runs the fragment on `db` and applies the method's shape/transform/validation decorators; the result type is inferred from the method's declared return type.
|
|
1253
|
+
|
|
1254
|
+
### `run(db, params)` — inside a `@Query` method
|
|
1255
|
+
|
|
1256
|
+
Runs the method's own SQL (its `.sql` file or inline `@Query({ sql })`) against `db`, binding `params` — an object for `:named` placeholders, an array for `:arg1, :arg2, ...`. In this form the method's raw arguments are **not** auto-bound; `params` is the whole binding input. `@Defaults`, `@Page`, shaping, transform, and validation still apply.
|
|
1257
|
+
|
|
1258
|
+
```ts
|
|
1259
|
+
@QueryBinder()
|
|
1260
|
+
class AccountsRepo {
|
|
1261
|
+
@Single
|
|
1262
|
+
@Query() // AccountsRepo/findByEmail.sql: SELECT * FROM accounts WHERE email = :email
|
|
1263
|
+
async findByEmail(db: Database, params: { email: string }): Promise<Account | null> {
|
|
1264
|
+
return run(db, params);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
```
|
|
1268
|
+
|
|
1269
|
+
The SQL file is located through the same resolver as `run()` (under an active IoC environment that means the root's registered `SqlStack`). Calling `run(db, params)` outside a `@Query` method throws `SqlStackError` — there is no SQL to run.
|
|
1270
|
+
|
|
1271
|
+
### The forms at a glance
|
|
1272
|
+
|
|
1273
|
+
| Call | Where | SQL | Database | Binding |
|
|
1274
|
+
|---|---|---|---|---|
|
|
1275
|
+
| `run()` | `@Query` body | method's file / inline SQL | decorator context | method arguments |
|
|
1276
|
+
| `run(query)` | `@Query` body | the fragment | decorator context | fragment values |
|
|
1277
|
+
| `run(db, query)` | anywhere | the fragment | `db` | fragment values |
|
|
1278
|
+
| `run(db, params)` | `@Query` body | method's file / inline SQL | `db` | `params` only |
|
|
1279
|
+
|
|
1280
|
+
Type rules (enforced by the overloads, and mirrored at runtime for untyped callers): SQL strings are rejected, `.toSQL()` output (`{ sql, params }`) is rejected, the first argument must be a handle or a name, and params must be an object or array. All forms follow the [error contract](#error-handling): failures surface as `DatabaseError` with the original error in `.cause`.
|
|
1281
|
+
|
|
1282
|
+
### Dialects
|
|
1283
|
+
|
|
1284
|
+
`run` binds fragments for the target database: `$1, $2, ...` for Postgres, `?` for MySQL and SQLite (including inside `:insert`, `:update`, `:filter`, and `:batch_insert`). The dialect comes from the handle's `dialect` property and defaults to `sqlite` when the handle has none. `fragment.toSQL()` itself is unchanged and always emits `?`.
|
|
1285
|
+
|
|
1286
|
+
### Transactions
|
|
1287
|
+
|
|
1288
|
+
A database passed to `run` joins an active transaction (from `@transaction` / `withTransaction`) that was opened **on that same database**: by name, or — for a handle — when it is the very `Database` object registered under the transaction's name. A handle that is not registered with sqlstack (for example an application-owned SQLite connection) never joins a sqlstack transaction; manage its transactions with your driver.
|
|
1289
|
+
|
|
1290
|
+
### Inside `@Query`, all `run` calls belong to the method
|
|
1291
|
+
|
|
1292
|
+
`run` finds the enclosing `@Query` invocation through async context. Any `run(...)` executed while a `@Query` method body is running — including inside helper functions the body awaits — is treated as that method's query and gets its shape/transform/validation decorators. Keep `run(db, query)` calls meant to return raw results out of `@Query` bodies.
|
|
1293
|
+
|
|
1205
1294
|
---
|
|
1206
1295
|
|
|
1207
1296
|
## Advanced: Direct Database Access
|
|
1208
1297
|
|
|
1209
|
-
Every `Database` instance exposes the underlying driver via `.conn`, so you can drop down to raw
|
|
1298
|
+
For SQL that should stay typed and dialect-safe, prefer `` run(db, sql`...`) `` (see [Choosing the Database at Call Time](#choosing-the-database-at-call-time)). Every `Database` instance also exposes the underlying driver via `.conn`, so you can drop down to raw driver calls when needed:
|
|
1210
1299
|
|
|
1211
1300
|
```ts
|
|
1212
1301
|
import { SqlStackDB } from 'sqlstack/registry';
|
|
@@ -1238,7 +1327,7 @@ More adapters are planned.
|
|
|
1238
1327
|
|
|
1239
1328
|
### Dialect Inference
|
|
1240
1329
|
|
|
1241
|
-
The placeholder style is inferred from the selected database. Override per-method with `@Query({ dialect: "
|
|
1330
|
+
The placeholder style is inferred from the selected database. Override per-method with `@Query({ dialect: "postgres" })` if needed.
|
|
1242
1331
|
|
|
1243
1332
|
---
|
|
1244
1333
|
|