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.
Files changed (95) hide show
  1. package/dist/cjs/adapters/sqlite.d.ts.map +1 -1
  2. package/dist/cjs/adapters/sqlite.js +4 -1
  3. package/dist/cjs/adapters/sqlite.js.map +1 -1
  4. package/dist/cjs/core/errors.d.ts +9 -0
  5. package/dist/cjs/core/errors.d.ts.map +1 -1
  6. package/dist/cjs/core/errors.js +10 -0
  7. package/dist/cjs/core/errors.js.map +1 -1
  8. package/dist/cjs/decorators/query.d.ts +14 -0
  9. package/dist/cjs/decorators/query.d.ts.map +1 -1
  10. package/dist/cjs/decorators/query.js +124 -17
  11. package/dist/cjs/decorators/query.js.map +1 -1
  12. package/dist/cjs/index.d.ts +2 -1
  13. package/dist/cjs/index.d.ts.map +1 -1
  14. package/dist/cjs/index.js +4 -2
  15. package/dist/cjs/index.js.map +1 -1
  16. package/dist/cjs/run.d.ts +140 -6
  17. package/dist/cjs/run.d.ts.map +1 -1
  18. package/dist/cjs/run.js +127 -17
  19. package/dist/cjs/run.js.map +1 -1
  20. package/dist/cjs/runtime.d.ts +8 -0
  21. package/dist/cjs/runtime.d.ts.map +1 -1
  22. package/dist/cjs/runtime.js +12 -0
  23. package/dist/cjs/runtime.js.map +1 -1
  24. package/dist/cjs/sql.d.ts +19 -0
  25. package/dist/cjs/sql.d.ts.map +1 -1
  26. package/dist/cjs/sql.js +66 -28
  27. package/dist/cjs/sql.js.map +1 -1
  28. package/dist/cjs/stack.d.ts +2 -0
  29. package/dist/cjs/stack.d.ts.map +1 -1
  30. package/dist/cjs/stack.js +40 -27
  31. package/dist/cjs/stack.js.map +1 -1
  32. package/dist/cjs/testing/builder.d.ts.map +1 -1
  33. package/dist/cjs/testing/builder.js +14 -5
  34. package/dist/cjs/testing/builder.js.map +1 -1
  35. package/dist/cjs/testing/fixture-provenance.d.ts +3 -0
  36. package/dist/cjs/testing/fixture-provenance.d.ts.map +1 -0
  37. package/dist/cjs/testing/fixture-provenance.js +21 -0
  38. package/dist/cjs/testing/fixture-provenance.js.map +1 -0
  39. package/dist/cjs/testing/index.d.ts +2 -0
  40. package/dist/cjs/testing/index.d.ts.map +1 -1
  41. package/dist/cjs/testing/index.js +5 -1
  42. package/dist/cjs/testing/index.js.map +1 -1
  43. package/dist/cjs/testing/postgres/database.d.ts +9 -1
  44. package/dist/cjs/testing/postgres/database.d.ts.map +1 -1
  45. package/dist/cjs/testing/postgres/database.js +17 -13
  46. package/dist/cjs/testing/postgres/database.js.map +1 -1
  47. package/dist/cjs/testing/resource.d.ts +76 -0
  48. package/dist/cjs/testing/resource.d.ts.map +1 -0
  49. package/dist/cjs/testing/resource.js +117 -0
  50. package/dist/cjs/testing/resource.js.map +1 -0
  51. package/dist/cjs/testing/sqlite/builder.d.ts.map +1 -1
  52. package/dist/cjs/testing/sqlite/builder.js +10 -2
  53. package/dist/cjs/testing/sqlite/builder.js.map +1 -1
  54. package/dist/cjs/testing/sqlite/database.d.ts +11 -1
  55. package/dist/cjs/testing/sqlite/database.d.ts.map +1 -1
  56. package/dist/cjs/testing/sqlite/database.js +32 -18
  57. package/dist/cjs/testing/sqlite/database.js.map +1 -1
  58. package/dist/cjs/transactions.d.ts +14 -0
  59. package/dist/cjs/transactions.d.ts.map +1 -1
  60. package/dist/cjs/transactions.js +27 -0
  61. package/dist/cjs/transactions.js.map +1 -1
  62. package/dist/esm/adapters/sqlite.js +4 -1
  63. package/dist/esm/adapters/sqlite.js.map +1 -1
  64. package/dist/esm/core/errors.js +10 -0
  65. package/dist/esm/core/errors.js.map +1 -1
  66. package/dist/esm/decorators/query.js +127 -20
  67. package/dist/esm/decorators/query.js.map +1 -1
  68. package/dist/esm/index.js +3 -2
  69. package/dist/esm/index.js.map +1 -1
  70. package/dist/esm/run.js +123 -18
  71. package/dist/esm/run.js.map +1 -1
  72. package/dist/esm/runtime.js +11 -0
  73. package/dist/esm/runtime.js.map +1 -1
  74. package/dist/esm/sql.js +63 -27
  75. package/dist/esm/sql.js.map +1 -1
  76. package/dist/esm/stack.js +40 -27
  77. package/dist/esm/stack.js.map +1 -1
  78. package/dist/esm/testing/builder.js +14 -5
  79. package/dist/esm/testing/builder.js.map +1 -1
  80. package/dist/esm/testing/fixture-provenance.js +15 -0
  81. package/dist/esm/testing/fixture-provenance.js.map +1 -0
  82. package/dist/esm/testing/index.js +1 -0
  83. package/dist/esm/testing/index.js.map +1 -1
  84. package/dist/esm/testing/postgres/database.js +17 -13
  85. package/dist/esm/testing/postgres/database.js.map +1 -1
  86. package/dist/esm/testing/resource.js +106 -0
  87. package/dist/esm/testing/resource.js.map +1 -0
  88. package/dist/esm/testing/sqlite/builder.js +10 -2
  89. package/dist/esm/testing/sqlite/builder.js.map +1 -1
  90. package/dist/esm/testing/sqlite/database.js +32 -18
  91. package/dist/esm/testing/sqlite/database.js.map +1 -1
  92. package/dist/esm/transactions.js +25 -0
  93. package/dist/esm/transactions.js.map +1 -1
  94. package/package.json +1 -1
  95. 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, SqlStackError } from "sqlstack";
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
- // Implementation bodies are never called; this method exists
88
- // only to define the TypeScript signature for @Query.
89
- throw new SqlStackError("replaced by @Query");
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 intercepts the method, loads and executes the SQL, and returns results.
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
- throw new SqlStackError("replaced by @Query");
130
+ return run();
129
131
  }
130
132
 
131
133
  @Query()
132
134
  async updateProfile(params: { id: string; name?: string; status?: string }): Promise<WriteResult> {
133
- throw new SqlStackError("replaced by @Query");
135
+ return run();
134
136
  }
135
137
 
136
138
  @Query()
137
139
  async createUser(params: { id: string; email: string; name?: string }): Promise<WriteResult> {
138
- throw new SqlStackError("replaced by @Query");
140
+ return run();
139
141
  }
140
142
 
141
143
  @Query()
142
144
  async search(filters: { name?: string; status?: string }): Promise<User[]> {
143
- throw new SqlStackError("replaced by @Query");
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, SqlStackError } from "sqlstack";
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
- throw new SqlStackError("replaced");
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
- throw new SqlStackError("replaced");
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
- throw new SqlStackError("replaced");
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
- throw new Error();
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, SqlStackError } from "sqlstack";
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
- throw new SqlStackError();
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, SqlStackError } from "sqlstack";
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
- throw new SqlStackError();
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, SqlStackError } from "sqlstack";
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
- throw new SqlStackError();
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
- throw new SqlStackError();
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
- throw new SqlStackError();
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
- throw new SqlStackError();
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, SqlStackError } from "sqlstack";
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
- throw new SqlStackError();
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> { throw new Error('replaced'); }
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
- throw new Error();
811
+ return run();
810
812
  }
811
813
 
812
814
  @Single
813
815
  @Query()
814
816
  async firstUser(): Promise<User | null> {
815
- throw new Error();
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 provides a base error class and specific error types. Import and catch as needed:
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 ExactlyOneRowError) {
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
- - Database driver errors — pass through unchanged
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
- throw new Error("replaced by @Query");
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
- throw new Error("replaced by @Query");
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 or dynamic queries, use `@Query({ sql: "..." })` with inline SQL:
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
- throw new Error("replaced");
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 queries when needed:
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: "pg" })` if needed.
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