@gallopsystems/agent-skills 1.18.0 → 1.20.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gallopsystems/agent-skills",
3
- "version": "1.18.0",
3
+ "version": "1.20.0",
4
4
  "description": "Gallop Systems agent skills, symlinked into .claude/skills (Claude Code) and .agents/skills (Codex) on install.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -49,6 +49,39 @@ Rule of thumb: **convert when the raw SQL names a column or table; leave it when
49
49
  it is only parameters and literals.** The latter compiles to identical SQL and the
50
50
  "type-safe" rewrite verifies nothing.
51
51
 
52
+ **One more trap in the canonical `count(*)` swap above: it returns a *string* at
53
+ runtime, and a type parameter won't change that.** `eb.fn.countAll()` emits a bare
54
+ `count(*)`, which PostgreSQL returns as `bigint` — and the `pg` driver deserializes
55
+ `bigint` to a JavaScript **string** by default (see why below). So the row's `count`
56
+ is `"2"`, not `2`:
57
+
58
+ ```typescript
59
+ // MISLEADING - compiles, but lies about the runtime value
60
+ .select((eb) => eb.fn.countAll<number>().as("count"))
61
+ // `<number>` is a pure type assertion. The driver still hands back a string,
62
+ // so `row.count === 2` is false and `row.count.toFixed()` throws.
63
+
64
+ // RIGHT - cast in SQL so the driver parses a real number
65
+ .select((eb) => eb.cast<number>(eb.fn.countAll(), "integer").as("count"))
66
+ // emits `cast(count(*) as integer)` -> int4 -> the driver yields a JS number
67
+ ```
68
+
69
+ The rule generalizes: **typing a `sql`/builder expression as `<number>` does not
70
+ change the value the driver produces — only a SQL-level cast does.** Any
71
+ `bigint`-returning aggregate (`count`, `sum` over a `bigint`, etc.) needs an explicit
72
+ `cast` when downstream code expects a JS number.
73
+
74
+ > **Why `bigint` comes back as a string — and don't "fix" it globally.** The `pg`
75
+ > driver returns `int8`/`bigint` as a string on purpose: a JS `number` is a float64,
76
+ > exact only to `2^53 - 1`, while `bigint` runs to `2^63 - 1`, so auto-parsing would
77
+ > silently corrupt large IDs. You *can* register `pg.types.setTypeParser` to coerce
78
+ > `int8` to a number, but it reintroduces that precision footgun for **every**
79
+ > `bigint` column (primary keys included), and `kysely-codegen` has no
80
+ > `--bigint-parser` flag (only `--date-parser` / `--numeric-parser`) — so the
81
+ > generated `Int8` type still selects as `string` and you would be hand-overriding an
82
+ > alias that regeneration clobbers. Keep the string default and `cast` per query where
83
+ > you need a number.
84
+
52
85
  ### 2. Don't Forget .execute()
53
86
 
54
87
  Queries are lazy - they won't run without calling an execute method:
@@ -159,3 +192,40 @@ The `"between"` operator looks like it should work but compiles to a tuple, whic
159
192
  .where((eb) => eb.betweenSymmetric("age", 65, 18)) // swaps bounds if needed
160
193
  ```
161
194
 
195
+ ### 9. Builder Argument Strings Are Column References, Not Literals
196
+
197
+ This bites hardest when converting raw `sql` to builders. In `eb.fn.coalesce`,
198
+ binary `eb(lhs, op, rhs)`, and most `eb.fn` calls, a bare string argument is
199
+ interpreted as a **column reference**, not a string value. So porting
200
+ `` sql`coalesce(status, 'pending')` `` naively produces a query that looks for a
201
+ column named `pending`:
202
+
203
+ ```typescript
204
+ // WRONG - 'pending' is read as a column name -> "column \"pending\" does not exist"
205
+ eb.fn.coalesce("status", "pending")
206
+
207
+ // RIGHT - wrap a literal default in eb.val() (parameterized) ...
208
+ eb.fn.coalesce("status", eb.val("pending"))
209
+ // ... or a typed sql`` literal when you need it inlined (e.g. inside GROUP BY)
210
+ eb.fn.coalesce("status", sql<string>`'pending'`)
211
+
212
+ // Number/boolean/null literals can use eb.lit (eb.lit rejects strings, see
213
+ // expression-builder.md):
214
+ eb.fn.coalesce("score", eb.lit(0))
215
+
216
+ // Coalescing two real COLUMNS is the case where bare strings are correct:
217
+ eb.fn.coalesce("preferred_name", "legal_name") // both are column refs
218
+ ```
219
+
220
+ A companion trap: when you feed a `sql`` fragment as the right-hand operand of a
221
+ typed operator (e.g. a full-text `@@`), the fragment must carry a type parameter
222
+ or it infers `unknown` and the operator overload rejects it:
223
+
224
+ ```typescript
225
+ // WRONG - RawBuilder<unknown> is not a valid operand -> TS2345
226
+ .where("search_vector", "@@", sql`to_tsquery('english', ${q})`)
227
+
228
+ // RIGHT - type the fragment so it satisfies the operator's operand type
229
+ .where("search_vector", "@@", sql<string>`to_tsquery('english', ${q})`)
230
+ ```
231
+
@@ -351,6 +351,37 @@ await db
351
351
  .execute();
352
352
  ```
353
353
 
354
+ **`doUpdateSet` object vs callback form.** `doUpdateSet` takes either a plain
355
+ object `doUpdateSet({ col: value })` or a callback `doUpdateSet((eb) => ({ ... }))`.
356
+ The object form has no expression builder, so the moment you need a *builder
357
+ expression* for any column — arithmetic, an `EXCLUDED` reference, a coalesce — you
358
+ must switch the whole call to the callback form. Inside it, a **bare column name
359
+ refers to the target (existing) row**, and **`eb.ref("excluded.<col>")` references
360
+ the incoming row** (Kysely lowercases it to Postgres's `excluded` pseudo-table).
361
+ The other columns come along unchanged inside the returned object:
362
+
363
+ ```typescript
364
+ // Raw sql for the increment / EXCLUDED — column names are unchecked strings
365
+ .onConflict((oc) =>
366
+ oc.column("email").doUpdateSet({
367
+ reply_count: sql`reply_count + 1`,
368
+ email: sql`EXCLUDED.email`,
369
+ status: "active", // plain value, fine in either form
370
+ }),
371
+ )
372
+
373
+ // Callback form unlocks eb(): both column refs are now type-checked
374
+ .onConflict((oc) =>
375
+ oc.column("email").doUpdateSet((eb) => ({
376
+ reply_count: eb("reply_count", "+", 1),
377
+ email: eb.ref("excluded.email"),
378
+ status: "active",
379
+ })),
380
+ )
381
+ ```
382
+
383
+ The same object-vs-callback rule applies to `.set()` on a plain `UPDATE` (see below).
384
+
354
385
  ### UPDATE Operations
355
386
 
356
387
  ```typescript