@gallopsystems/agent-skills 1.19.0 → 1.20.1

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.19.0",
3
+ "version": "1.20.1",
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:
@@ -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
@@ -43,11 +43,27 @@ const upsertedProduct = await db
43
43
  oc.column("sku").doUpdateSet((eb) => ({
44
44
  // Update these columns on conflict - type-safe!
45
45
  stock_quantity: eb("product.stock_quantity", "+", eb.ref("excluded.stock_quantity")),
46
+ // Prefer typed builders over raw sql here too: eb.fn.coalesce(...) over both
47
+ // tables validates names against the schema (excluded is a real virtual table)
48
+ name: eb.fn.coalesce(eb.ref("product.name"), eb.ref("excluded.name")),
46
49
  }))
47
50
  )
48
51
  .returning(["id", "sku", "stock_quantity"])
49
52
  .executeTakeFirst();
50
53
 
54
+ // GOTCHA: when you DO need raw sql in a SET / doUpdateSet value (e.g. now()),
55
+ // do NOT parameterize it with a codegen ColumnType marker like the generated
56
+ // `Timestamp` (= ColumnType<Date, ...>). It breaks compilation:
57
+ // updated_at: sql<Timestamp>`now()` // ❌ TS2345: RawBuilder<Timestamp> not
58
+ // // assignable; "isSelectQueryBuilder" missing
59
+ // The SET target is the flattened Updateable value type (e.g. string | Date | undefined),
60
+ // and RawBuilder<ColumnType<...>> falls outside that plain-value branch. Use a plain
61
+ // value type or leave it bare instead:
62
+ // updated_at: sql<Date>`now()` // ✅ Date is a plain value type
63
+ // updated_at: sql`now()` // ✅ also assigns cleanly
64
+ // (The "always type your sql literals" rule is aimed at SELECT, where the type flows
65
+ // into the result. In write contexts, ColumnType markers are the wrong parameter.)
66
+
51
67
  // OnConflict variations:
52
68
  // - oc.column("col") - single column constraint
53
69
  // - oc.columns(["col1", "col2"]) - composite constraint