autotel-drizzle 0.0.51 → 0.0.52

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
@@ -109,6 +109,7 @@ await db.select().from(users).where(eq(users.id, 123));
109
109
  maxQueryTextLength?: number // Max SQL length (default: 1000)
110
110
  peerName?: string // Database host
111
111
  peerPort?: number // Database port
112
+ explain?: 'plan' | 'analyze' | false // Capture the postgres query plan (default: false)
112
113
  }
113
114
  ```
114
115
 
@@ -118,9 +119,94 @@ await db.select().from(users).where(eq(users.id, 123));
118
119
  - `db.operation` - Operation name (SELECT, INSERT, UPDATE, DELETE)
119
120
  - `db.name` - Database name
120
121
  - `db.statement` - SQL query text (if `captureQueryText: true`)
122
+ - `db.statement.hash` - Stable hash of the statement, present even when the text is suppressed
121
123
  - `net.peer.name` - Database host
122
124
  - `net.peer.port` - Database port
123
125
 
126
+ ## Drivers
127
+
128
+ Tested against postgres 16, mysql 8, and sqlite, driving each through drizzle's
129
+ own query builder, its relational API, and raw `db.execute`. Every driver below
130
+ emits one span per query, names the operation, and tags `db.collection.name`.
131
+
132
+ | Driver | Queries | Transaction span | Query plans |
133
+ | --- | --- | --- | --- |
134
+ | `node-postgres` | yes | yes | yes |
135
+ | `postgres.js` | yes | yes | yes |
136
+ | `pglite` | yes | yes | yes |
137
+ | `mysql2` | yes | yes | postgres only |
138
+ | `better-sqlite3` | yes | yes, synchronous | postgres only |
139
+ | `libsql` | yes | yes | postgres only |
140
+
141
+ Nothing about the instrumentation is dialect-specific except `explain`, which
142
+ sends postgres syntax and so turns itself off unless `dbSystem` is
143
+ `'postgresql'`. Table names are read from double-quoted, backtick-quoted, and
144
+ unquoted identifiers alike, so mysql groups by table the same way postgres does.
145
+
146
+ better-sqlite3 runs transactions synchronously and rejects a callback that
147
+ returns a promise. That is the driver's own rule and applies with or without
148
+ instrumentation; the wrapper keeps synchronous calls synchronous so those
149
+ transactions still commit.
150
+
151
+ ## Query Plans
152
+
153
+ A span records what you asked postgres for. It does not record what postgres
154
+ did. Add an index and `db.statement` is unchanged, byte for byte, with the same
155
+ `db.statement.hash`; the duration moves and nothing on the span says why.
156
+
157
+ `explain` puts the planner's answer on the span:
158
+
159
+ ```typescript
160
+ instrumentDrizzleClient(db, {
161
+ dbSystem: 'postgresql',
162
+ explain: 'analyze', // or 'plan' to plan without executing
163
+ });
164
+ ```
165
+
166
+ The same query, before and after `CREATE INDEX`:
167
+
168
+ | Attribute | Before | After |
169
+ | --- | --- | --- |
170
+ | `db.statement.hash` | `372269a8881e921a` | `372269a8881e921a` |
171
+ | `db.plan.hash` | `0b50591ce9f68f51` | `eb8765b8d1c4af5a` |
172
+ | `db.plan.node` | `Seq Scan` | `Bitmap Heap Scan` |
173
+ | `db.plan.indexes` | | `idx_comments_post_id` |
174
+ | `db.plan.seq_scan` | `true` | `false` |
175
+ | `db.plan.rows_examined` | `200199` | `1001` |
176
+ | `db.plan.rows_returned` | `1001` | `1001` |
177
+ | `db.plan.blocks` | `1861` | `1005` |
178
+ | `db.plan.cost` | | planner's total cost estimate |
179
+ | `db.plan.rows_estimated` | | rows the planner expected |
180
+ | `db.plan.execution_ms` | | measured time (`'analyze'` only) |
181
+
182
+ An unchanged statement hash beside a changed plan hash is a planner decision.
183
+ Grouping `db.plan.indexes` by `db.statement.hash` answers which queries an index
184
+ is serving.
185
+
186
+ `rows_examined` counts at the leaves of the plan, and counts the rows a scan
187
+ read and discarded. A parent node reports the rows its children handed up, so
188
+ summing every level counts one row per level; and the rows an index saves you
189
+ from reading never appear in the rows the query returns. `blocks` counts cached
190
+ and disk reads together, because a scan that walks a table already in memory
191
+ still walked the table.
192
+
193
+ ### Cost and safety
194
+
195
+ Both modes add a round trip per query, and `'analyze'` executes the statement a
196
+ second time to measure it. Both are off by default. Use them in development, in
197
+ CI, or behind a sample of production traffic.
198
+
199
+ - `'analyze'` runs on read-only statements only, so a traced insert never runs
200
+ twice. A leading `WITH` qualifies only when no writing keyword appears
201
+ anywhere in the statement, because `WITH gone AS (DELETE ... RETURNING *)
202
+ SELECT` opens exactly like a read.
203
+ - The plan is collected before the span opens, so the extra round trip stays out
204
+ of the duration the span reports.
205
+ - A failed `EXPLAIN` is swallowed. The query still runs and still gets its span.
206
+ - Ignored unless `dbSystem` is `'postgresql'`.
207
+ - Inside a transaction the plan is taken on the transaction's own connection, so
208
+ it sees the rows the query will see.
209
+
124
210
  ## Security Considerations
125
211
 
126
212
  ### Query Text Capture