@gallopsystems/agent-skills 1.13.0 → 1.15.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 +1 -1
- package/plugins/kysely-postgres/skills/kysely-postgres/SKILL.md +16 -1239
- package/plugins/kysely-postgres/skills/kysely-postgres/references/common-pitfalls.md +132 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/deep-types.md +39 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/expression-builder.md +132 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/full-text-and-grouping.md +26 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/helpers-and-extending.md +81 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/json-jsonb-arrays.md +374 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/migrations-and-codegen.md +162 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/query-patterns.md +235 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/seeding-pattern.md +143 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/set-lateral-locking.md +46 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/window-functions.md +37 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
## Common Pitfalls to Avoid
|
|
2
|
+
|
|
3
|
+
### 1. Don't Resort to `sql`` When Kysely Has a Method
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
// WRONG
|
|
7
|
+
.select(sql`count(*)`.as("count"))
|
|
8
|
+
|
|
9
|
+
// RIGHT
|
|
10
|
+
.select((eb) => eb.fn.countAll().as("count"))
|
|
11
|
+
|
|
12
|
+
// WRONG - raw SQL for FILTER (WHERE ...) on aggregates
|
|
13
|
+
.having(sql<number>`count(*) filter (where status != 'signed')`, "=", 0)
|
|
14
|
+
|
|
15
|
+
// RIGHT - .filterWhere() works on all aggregate function builders
|
|
16
|
+
.having(
|
|
17
|
+
(eb) => eb.fn.countAll().filterWhere("status", "!=", "signed"),
|
|
18
|
+
"=",
|
|
19
|
+
0
|
|
20
|
+
)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 2. Don't Forget .execute()
|
|
24
|
+
|
|
25
|
+
Queries are lazy - they won't run without calling an execute method:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// This does nothing!
|
|
29
|
+
db.selectFrom("user").selectAll();
|
|
30
|
+
|
|
31
|
+
// This runs the query
|
|
32
|
+
await db.selectFrom("user").selectAll().execute();
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 3. Use whereRef for Column-to-Column Comparisons
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
// WRONG - Compares to string literal "other.column"
|
|
39
|
+
.where("table.column", "=", "other.column")
|
|
40
|
+
|
|
41
|
+
// RIGHT - Compares to actual column value
|
|
42
|
+
.whereRef("table.column", "=", "other.column")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 4. Type Your Function Returns
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// Better type inference
|
|
49
|
+
eb.fn<string>("concat", [...])
|
|
50
|
+
eb.fn<number>("length", [...])
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 5. PostgreSQL Does NOT Auto-Index Foreign Keys
|
|
54
|
+
|
|
55
|
+
Always create indexes on foreign key columns:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
await db.schema.createIndex("idx_order_user_id").on("order").column("user_id").execute();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 6. Always Type `sql` Template Literals
|
|
62
|
+
|
|
63
|
+
When using `sql` template literals, the inferred type is `unknown` since Kysely can't know what the SQL expression resolves to. Always provide an explicit type:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// WRONG - Returns unknown type
|
|
67
|
+
eb.fn.coalesce("some_json_col", sql`'{}'::jsonb`)
|
|
68
|
+
|
|
69
|
+
// RIGHT - Explicit type annotation
|
|
70
|
+
eb.fn.coalesce("some_json_col", sql<Record<string, unknown>>`'{}'::jsonb`)
|
|
71
|
+
|
|
72
|
+
// For complex types (e.g., JSON column from a CTE), use typeof with eb.ref
|
|
73
|
+
// This ensures the fallback type matches the column type exactly
|
|
74
|
+
eb.fn
|
|
75
|
+
.coalesce(
|
|
76
|
+
eb.ref("jobs_agg.jobs"),
|
|
77
|
+
sql<typeof eb.ref<"jobs_agg.jobs">>`'[]'::json`
|
|
78
|
+
)
|
|
79
|
+
.as("jobs")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Key rule**: Every `sql` template literal should have a type parameter: `sql<TYPE>`. This ensures proper type inference throughout your query chain.
|
|
83
|
+
|
|
84
|
+
### 7. DATE Columns Cause Timezone Issues
|
|
85
|
+
|
|
86
|
+
By default, the `pg` driver converts DATE columns to JavaScript `Date` objects. This causes timezone problems:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
Database: 2025-01-01 (just a date, no time)
|
|
90
|
+
JS Date: 2025-01-01T00:00:00.000Z (interpreted as UTC midnight)
|
|
91
|
+
User in NYC sees: Dec 31, 2024 (5 hours behind UTC)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Solution: Parse DATE as string and let the frontend handle formatting**
|
|
95
|
+
|
|
96
|
+
Step 1: Configure `pg` to return DATE as string:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import pg from "pg";
|
|
100
|
+
|
|
101
|
+
// Tell pg to return DATE columns as strings instead of Date objects
|
|
102
|
+
const DATE_OID = 1082;
|
|
103
|
+
pg.types.setTypeParser(DATE_OID, (val: string) => val);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Step 2: Update `kysely-codegen` to generate matching types:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npx kysely-codegen \
|
|
110
|
+
--url="$DATABASE_URL" \
|
|
111
|
+
--out-file=server/db/db.d.ts \
|
|
112
|
+
--dialect=postgres \
|
|
113
|
+
--date-parser=string
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Now DATE columns return strings like `"2025-01-01"` and the frontend can parse/format respecting the user's timezone.
|
|
117
|
+
|
|
118
|
+
**Note**: This applies to DATE columns only. TIMESTAMPTZ columns already handle timezones correctly by storing UTC and converting on read.
|
|
119
|
+
|
|
120
|
+
### 8. Don't Use the `between` String Operator — It Emits Invalid SQL
|
|
121
|
+
|
|
122
|
+
The `"between"` operator looks like it should work but compiles to a tuple, which is a PostgreSQL syntax error:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// WRONG - compiles to: "age" between ($1, $2) -> Postgres syntax error
|
|
126
|
+
.where("age", "between", [18, 65])
|
|
127
|
+
|
|
128
|
+
// RIGHT - use the expression-builder helpers
|
|
129
|
+
.where((eb) => eb.between("age", 18, 65)) // "age" between $1 and $2
|
|
130
|
+
.where((eb) => eb.betweenSymmetric("age", 65, 18)) // swaps bounds if needed
|
|
131
|
+
```
|
|
132
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
## Handling "Excessively Deep Types" Error
|
|
2
|
+
|
|
3
|
+
### The Problem
|
|
4
|
+
|
|
5
|
+
Complex queries with many CTEs can overwhelm TypeScript's type instantiation limits:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Type instantiation is excessively deep and possibly infinite
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This commonly occurs with 12+ `with` clauses, as Kysely's nested helper types accumulate.
|
|
12
|
+
|
|
13
|
+
### The Solution: `$assertType`
|
|
14
|
+
|
|
15
|
+
Use `$assertType` to simplify the type chain at intermediate points:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
const result = await db
|
|
19
|
+
.with("cte1", (qb) =>
|
|
20
|
+
qb.selectFrom("user")
|
|
21
|
+
.select(["id", "email"])
|
|
22
|
+
.$assertType<{ id: number; email: string }>() // Simplify type here
|
|
23
|
+
)
|
|
24
|
+
.with("cte2", (qb) =>
|
|
25
|
+
qb.selectFrom("cte1")
|
|
26
|
+
.select("email")
|
|
27
|
+
.$assertType<{ email: string }>()
|
|
28
|
+
)
|
|
29
|
+
// ... more CTEs
|
|
30
|
+
.selectFrom("cteN")
|
|
31
|
+
.selectAll()
|
|
32
|
+
.execute();
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Key points**:
|
|
36
|
+
- The asserted type must structurally match the actual type (full type safety preserved)
|
|
37
|
+
- Apply to several intermediate `with` clauses in large queries
|
|
38
|
+
- TypeScript cannot automatically simplify these types - explicit assertion is required
|
|
39
|
+
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
## ExpressionBuilder (eb) - The Foundation
|
|
2
|
+
|
|
3
|
+
The `eb` parameter in select/where callbacks provides all expression methods:
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
.select((eb) => [
|
|
7
|
+
eb.ref("column").as("alias"), // Column reference
|
|
8
|
+
eb.fn<string>("upper", [eb.ref("email")]), // Function call (typed!)
|
|
9
|
+
eb.fn.count("id").as("count"), // Aggregate function
|
|
10
|
+
eb.fn.sum("amount").as("total"), // SUM
|
|
11
|
+
eb.fn.avg("rating").as("avgRating"), // AVG
|
|
12
|
+
eb.fn.coalesce("nullable_col", eb.val(0)), // COALESCE
|
|
13
|
+
eb.case().when("status", "=", "active") // CASE expression
|
|
14
|
+
.then("Active").else("Inactive").end(),
|
|
15
|
+
eb("quantity", "*", eb.ref("unit_price")), // Binary expression
|
|
16
|
+
eb.exists(subquery), // EXISTS
|
|
17
|
+
eb.not(expression), // NOT / negation
|
|
18
|
+
eb.cast(eb.val(" "), "text"), // Cast value to type
|
|
19
|
+
eb.and([...]), // AND conditions
|
|
20
|
+
eb.or([...]), // OR conditions
|
|
21
|
+
])
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### eb.val() vs eb.lit()
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// eb.val() - Creates a parameterized value ($1, $2, etc.) - PREFERRED for user input
|
|
28
|
+
// Note: eb.val() alone may fail with "could not determine data type of parameter"
|
|
29
|
+
// Use eb.cast(eb.val(...), "text") for string values in function arguments
|
|
30
|
+
eb.val("user input") // Becomes: $1 with parameter "user input"
|
|
31
|
+
eb.cast(eb.val("safe"), "text") // Becomes: $1::text - always works
|
|
32
|
+
|
|
33
|
+
// eb.lit() - Creates a literal value in SQL
|
|
34
|
+
// ONLY accepts: numbers, booleans, null - NOT strings (throws "unsafe immediate value")
|
|
35
|
+
eb.lit(1) // Becomes: 1 (directly in SQL)
|
|
36
|
+
eb.lit(true) // Becomes: true
|
|
37
|
+
eb.lit(null) // Becomes: NULL
|
|
38
|
+
|
|
39
|
+
// For string literals, use sql`` template instead
|
|
40
|
+
sql`'active'` // Becomes: 'active' (directly in SQL)
|
|
41
|
+
sql<string>`'label'` // Typed string literal
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Standalone ExpressionBuilder
|
|
45
|
+
|
|
46
|
+
For reusable helpers outside query callbacks:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { expressionBuilder } from "kysely";
|
|
50
|
+
import type { DB } from "./db.d.ts";
|
|
51
|
+
|
|
52
|
+
// Create standalone expression builder
|
|
53
|
+
const eb = expressionBuilder<DB, "user">();
|
|
54
|
+
|
|
55
|
+
// Use in helper functions
|
|
56
|
+
function isActiveUser() {
|
|
57
|
+
return eb.and([
|
|
58
|
+
eb("is_active", "=", true),
|
|
59
|
+
eb("role", "!=", "banned"),
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
A predicate helper can also return a callback that receives the query's
|
|
65
|
+
`eb`, so the same helper works in any `.where()`/`.having()` on that table:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import type { ExpressionBuilder } from "kysely";
|
|
69
|
+
|
|
70
|
+
// AVOID - raw sql means the column name is an unchecked string.
|
|
71
|
+
// A typo or renamed column only fails at runtime.
|
|
72
|
+
function nameMatches(name: string) {
|
|
73
|
+
return sql<boolean>`lower(name) = ${name.toLowerCase()}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// PREFER - eb.fn keeps the column reference type-checked against DB,
|
|
77
|
+
// and the value stays parameterized.
|
|
78
|
+
function nameMatches(name: string) {
|
|
79
|
+
return (eb: ExpressionBuilder<DB, "user">) =>
|
|
80
|
+
eb(eb.fn("lower", ["name"]), "=", name.toLowerCase());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Both call sites stay identical:
|
|
84
|
+
db.selectFrom("user").where(nameMatches(input)).execute();
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Conditional Expressions with Arrays
|
|
88
|
+
|
|
89
|
+
Build dynamic filters by collecting expressions:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
.where((eb) => {
|
|
93
|
+
const filters: Expression<SqlBool>[] = [];
|
|
94
|
+
|
|
95
|
+
if (firstName) filters.push(eb("first_name", "=", firstName));
|
|
96
|
+
if (lastName) filters.push(eb("last_name", "=", lastName));
|
|
97
|
+
if (minAge) filters.push(eb("age", ">=", minAge));
|
|
98
|
+
|
|
99
|
+
// Combine all filters with AND (empty array = no filter)
|
|
100
|
+
return eb.and(filters);
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## String Concatenation
|
|
105
|
+
|
|
106
|
+
Use the `||` operator with `sql` template for clean string concatenation:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// RECOMMENDED - Clean and type-safe with eb.ref()
|
|
110
|
+
.select((eb) => [
|
|
111
|
+
sql<string>`${eb.ref("first_name")} || ' ' || ${eb.ref("last_name")}`.as("full_name"),
|
|
112
|
+
])
|
|
113
|
+
// Output: "first_name" || ' ' || "last_name"
|
|
114
|
+
|
|
115
|
+
// ALTERNATIVE - Pure eb() chaining (parameterized literals)
|
|
116
|
+
.select((eb) => [
|
|
117
|
+
eb(eb("first_name", "||", " "), "||", eb.ref("last_name")).as("full_name"),
|
|
118
|
+
])
|
|
119
|
+
// Output: "first_name" || $1 || "last_name"
|
|
120
|
+
|
|
121
|
+
// VERBOSE - concat() function (avoid unless you need NULL handling)
|
|
122
|
+
.select((eb) => [
|
|
123
|
+
eb.fn<string>("concat", [
|
|
124
|
+
eb.ref("first_name"),
|
|
125
|
+
eb.cast(eb.val(" "), "text"),
|
|
126
|
+
eb.ref("last_name"),
|
|
127
|
+
]).as("full_name"),
|
|
128
|
+
])
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Note**: `concat()` treats NULL as empty string, while `||` propagates NULL. Use `concat()` only when you need that NULL behavior.
|
|
132
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
## Full-Text Search (PostgreSQL)
|
|
2
|
+
|
|
3
|
+
`@@` is a real Kysely operator — keep it as the operator and put the FTS functions on each side as `sql` fragments. See [full-text-search.ts](references/full-text-search.ts).
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
db.selectFrom("document").selectAll()
|
|
7
|
+
.where(
|
|
8
|
+
sql`to_tsvector('english', ${sql.ref("body")})`,
|
|
9
|
+
"@@",
|
|
10
|
+
sql`websearch_to_tsquery('english', ${userInput})`,
|
|
11
|
+
)
|
|
12
|
+
.execute();
|
|
13
|
+
// A stored tsvector column can be the typed LHS directly:
|
|
14
|
+
// .where("search_vector", "@@", sql`plainto_tsquery('english', ${userInput})`)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Advanced Grouping (ROLLUP / CUBE / GROUPING SETS)
|
|
18
|
+
|
|
19
|
+
No builder exists — pass the grouping spec to `.groupBy()` as a `sql` fragment; the SELECT list stays typed. See [aggregations.ts](references/aggregations.ts).
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
.groupBy(sql`rollup("status")`) // hierarchical subtotals
|
|
23
|
+
.groupBy(sql`cube("order_id", "product_id")`) // all combinations
|
|
24
|
+
.groupBy(sql`grouping sets (("status"), ("user_id"), ())`) // explicit sets
|
|
25
|
+
```
|
|
26
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
## PostgreSQL Helpers Summary
|
|
2
|
+
|
|
3
|
+
All helpers from `kysely/helpers/postgres`:
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import {
|
|
7
|
+
jsonArrayFrom, // One-to-many relations (subquery → array)
|
|
8
|
+
jsonObjectFrom, // Many-to-one relations (subquery → object | null)
|
|
9
|
+
jsonBuildObject, // Build JSON object from expressions
|
|
10
|
+
mergeAction, // Get action performed in MERGE query (PostgreSQL 15+)
|
|
11
|
+
} from "kysely/helpers/postgres";
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
**Note**: `jsonAgg` is NOT imported - use `eb.fn.jsonAgg()` instead.
|
|
15
|
+
|
|
16
|
+
### mergeAction (PostgreSQL 15+)
|
|
17
|
+
|
|
18
|
+
For MERGE queries, get which action was performed:
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { mergeAction } from "kysely/helpers/postgres";
|
|
22
|
+
|
|
23
|
+
const result = await db
|
|
24
|
+
.mergeInto("person")
|
|
25
|
+
.using("person_updates", "person.id", "person_updates.id")
|
|
26
|
+
.whenMatched()
|
|
27
|
+
.thenUpdateSet({ name: eb.ref("person_updates.name") })
|
|
28
|
+
.whenNotMatched()
|
|
29
|
+
.thenInsertValues({ id: eb.ref("person_updates.id"), name: eb.ref("person_updates.name") })
|
|
30
|
+
.returning([mergeAction().as("action"), "id"])
|
|
31
|
+
.execute();
|
|
32
|
+
|
|
33
|
+
// result[0].action is 'INSERT' | 'UPDATE' | 'DELETE'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Extending Kysely
|
|
37
|
+
|
|
38
|
+
### Custom Helper Functions
|
|
39
|
+
|
|
40
|
+
Most extensions use the `sql` template tag with `RawBuilder<T>`:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { sql, RawBuilder } from "kysely";
|
|
44
|
+
|
|
45
|
+
// Create a typed helper function
|
|
46
|
+
function json<T>(value: T): RawBuilder<T> {
|
|
47
|
+
return sql`CAST(${JSON.stringify(value)} AS JSONB)`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Use in queries
|
|
51
|
+
.select((eb) => [
|
|
52
|
+
json({ name: "value" }).as("data"),
|
|
53
|
+
])
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Custom Expression Classes
|
|
57
|
+
|
|
58
|
+
For reusable expressions, implement the `Expression<T>` interface:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { Expression, OperationNode, sql } from "kysely";
|
|
62
|
+
|
|
63
|
+
class JsonValue<T> implements Expression<T> {
|
|
64
|
+
readonly #value: T;
|
|
65
|
+
|
|
66
|
+
constructor(value: T) {
|
|
67
|
+
this.#value = value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get expressionType(): T | undefined {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
toOperationNode(): OperationNode {
|
|
75
|
+
return sql`CAST(${JSON.stringify(this.#value)} AS JSONB)`.toOperationNode();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Note**: Module augmentation and inheritance-based extension are not recommended.
|
|
81
|
+
|