@owlmeans/postgres 0.1.18-rc.37 → 0.1.18-rc.38

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
@@ -14,7 +14,7 @@ provisioning, and an opt-in least-privilege bootstrap path.
14
14
  ## Installation
15
15
 
16
16
  ```bash
17
- bun add @owlmeans/postgres@^0.1.18-rc.37 @owlmeans/postgres-resource@^0.1.18-rc.36
17
+ bun add @owlmeans/postgres@^0.1.18-rc.38 @owlmeans/postgres-resource@^0.1.18-rc.37
18
18
  ```
19
19
 
20
20
  ## Usage
@@ -129,7 +129,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
129
129
  your project's skill store (`.agents/skills/`):
130
130
 
131
131
  ```sh
132
- npx @owlmeans/agent-skills@^0.1.18-rc.40
132
+ npx @owlmeans/agent-skills@^0.1.18-rc.41
133
133
  ```
134
134
 
135
135
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/postgres",
4
- "version": "0.1.18-rc.37",
5
- "generatedAt": "2026-09-25T21:19:38.015Z",
4
+ "version": "0.1.18-rc.38",
5
+ "generatedAt": "2026-09-28T02:06:37.894Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -8,7 +8,7 @@ user-invocable: false
8
8
  # @owlmeans/postgres
9
9
 
10
10
  **Layer:** Infra
11
- **Install:** `"@owlmeans/postgres": "^0.1.18-rc.37"` in `dependencies`
11
+ **Install:** `"@owlmeans/postgres": "^0.1.18-rc.38"` in `dependencies`
12
12
 
13
13
  Pooled `pg` connections for a server context, plus the admin path that provisions the role, database
14
14
  and schema an app connects with. All DDL for application tables belongs to [[postgres-resource]] —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/postgres",
3
- "version": "0.1.18-rc.37",
3
+ "version": "0.1.18-rc.38",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -22,9 +22,9 @@
22
22
  }
23
23
  },
24
24
  "dependencies": {
25
- "@owlmeans/basic-keys": "^0.1.18-rc.40",
25
+ "@owlmeans/basic-keys": "^0.1.18-rc.41",
26
26
  "@owlmeans/context": "^0.1.18-rc.32",
27
- "@owlmeans/postgres-resource": "^0.1.18-rc.36",
27
+ "@owlmeans/postgres-resource": "^0.1.18-rc.37",
28
28
  "@owlmeans/resource": "^0.1.18-rc.36",
29
29
  "@owlmeans/server-context": "^0.1.18-rc.37",
30
30
  "drizzle-orm": "~0.45.2",
@@ -0,0 +1,107 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
2
+ import { makePostgresResource } from '@owlmeans/postgres-resource'
3
+ import type { PostgresResource } from '@owlmeans/postgres-resource'
4
+ import { matchCriteria, UnsupportedArgumentError } from '@owlmeans/resource'
5
+ import type { Criteria, ResourceRecord } from '@owlmeans/resource'
6
+
7
+ import { gate, makeSuite } from './context.js'
8
+
9
+ interface Plot extends ResourceRecord {
10
+ id?: string
11
+ garden: string
12
+ crops: string[]
13
+ soil: Record<string, unknown>
14
+ season?: string
15
+ }
16
+
17
+ /** A community garden: plots carry a native text[] of crops and a jsonb soil report. */
18
+ const plotSchema = {
19
+ type: 'object',
20
+ properties: {
21
+ id: { type: 'string', format: 'uuid' },
22
+ garden: { type: 'string' },
23
+ crops: { type: 'array', items: { type: 'string' } },
24
+ soil: { type: 'object' },
25
+ season: { type: 'string', nullable: true }
26
+ },
27
+ required: ['id', 'garden', 'crops', 'soil']
28
+ } as never
29
+
30
+ const suite = makeSuite('criteria')
31
+ const it = gate.skip ? test.skip : test
32
+
33
+ describe('@owlmeans/postgres — criteria answer what the in-memory engine answers', () => {
34
+ if (gate.skip) {
35
+ test.skip(gate.reason ?? 'postgres gate closed', () => {})
36
+
37
+ return
38
+ }
39
+
40
+ let plots: PostgresResource<Plot>
41
+ let seeded: Plot[]
42
+
43
+ beforeAll(async () => {
44
+ const resource = makePostgresResource<Plot, PostgresResource<Plot>>('criteria-plots')
45
+ resource.schema = plotSchema
46
+ const booted = await suite.boot({ resources: [resource] })
47
+ plots = booted.context.resource<PostgresResource<Plot>>('criteria-plots')
48
+
49
+ seeded = []
50
+ for (const plot of [
51
+ { garden: 'north', crops: ['beans', 'kale'], soil: { ph: 6.5, wet: true, beds: ['a', 'b'], state: 'loam' }, season: 'spring' },
52
+ { garden: 'north', crops: ['kale'], soil: { ph: 7, wet: false, beds: ['c'], state: 'clay', 'a,b': 1 } },
53
+ { garden: 'south', crops: [], soil: { ph: 5.5, state: null, beds: [] }, season: 'autumn' },
54
+ ]) {
55
+ seeded.push(await plots.create(plot))
56
+ }
57
+ })
58
+
59
+ afterAll(async () => {
60
+ await suite.teardown()
61
+ })
62
+
63
+ const byBoth = async (criteria: Criteria<Plot>): Promise<void> => {
64
+ const listed = (await plots.list(criteria, { size: 0 })).items.map(plot => plot.id).sort()
65
+ const expected = seeded.filter(plot => matchCriteria(plot, criteria)).map(plot => plot.id).sort()
66
+ expect([JSON.stringify(criteria), listed]).toEqual([JSON.stringify(criteria), expected])
67
+ }
68
+
69
+ it('native array operators take a list or a scalar', async () => {
70
+ await byBoth({ crops: { $contains: 'kale' } })
71
+ await byBoth({ crops: { $contains: ['beans', 'kale'] } })
72
+ await byBoth({ crops: { $overlaps: ['beans', 'rye'] } })
73
+ await byBoth({ crops: { $overlaps: 'rye' } })
74
+ await byBoth({ crops: { $contained: ['kale', 'beans', 'rye'] } })
75
+ })
76
+
77
+ it('dotted jsonb paths compare typed values, lists, null and operators', async () => {
78
+ await byBoth({ 'soil.ph': 7 } as Criteria<Plot>)
79
+ await byBoth({ 'soil.wet': true } as Criteria<Plot>)
80
+ await byBoth({ 'soil.state': ['loam', 'clay'] } as Criteria<Plot>)
81
+ await byBoth({ 'soil.state': null } as Criteria<Plot>)
82
+ await byBoth({ 'soil.state': { $in: ['clay', null] } } as Criteria<Plot>)
83
+ /** `$nin` reads as it does on a column: `NOT IN`, which never matches an absent value. */
84
+ expect((await plots.list({ 'soil.state': { $nin: ['clay'] } } as Criteria<Plot>, { size: 0 })).items.map(plot => plot.garden))
85
+ .toEqual(['north'])
86
+ await byBoth({ 'soil.ph': { $gte: 6.5 } } as Criteria<Plot>)
87
+ await byBoth({ 'soil.state': { $startsWith: 'lo' } } as Criteria<Plot>)
88
+ await byBoth({ 'soil.state': { $exists: true } } as Criteria<Plot>)
89
+ await byBoth({ 'soil.beds': { $contains: 'a' } } as Criteria<Plot>)
90
+ await byBoth({ 'soil.beds': { $overlaps: ['c', 'z'] } } as Criteria<Plot>)
91
+ await byBoth({ 'soil.a,b': 1 } as Criteria<Plot>)
92
+ })
93
+
94
+ it('counts per group in one round trip', async () => {
95
+ const counts = await plots.countBy({ crops: { $contains: 'kale' } }, ['garden'])
96
+ expect(counts).toEqual([{ garden: 'north', count: 2 }])
97
+
98
+ const grouped = (await plots.countBy(undefined, ['garden', 'season']))
99
+ .sort((left, right) => `${left.garden}${left.season}`.localeCompare(`${right.garden}${right.season}`))
100
+ expect(grouped).toEqual([
101
+ { garden: 'north', season: null, count: 1 },
102
+ { garden: 'north', season: 'spring', count: 1 },
103
+ { garden: 'south', season: 'autumn', count: 1 },
104
+ ])
105
+ await expect(plots.countBy(undefined, ['nope' as never])).rejects.toThrow(UnsupportedArgumentError)
106
+ })
107
+ })
@@ -1,5 +1,7 @@
1
1
  import { afterAll, describe, expect, test } from 'bun:test'
2
- import { makePostgresResource, PgAutoSync, PostgresCastRequired } from '@owlmeans/postgres-resource'
2
+ import {
3
+ introspectTable, makePostgresResource, PgAutoSync, planSync, PostgresCastRequired,
4
+ } from '@owlmeans/postgres-resource'
3
5
  import type { PostgresResource } from '@owlmeans/postgres-resource'
4
6
  import type { ResourceRecord } from '@owlmeans/resource'
5
7
 
@@ -154,3 +156,91 @@ describe('@owlmeans/postgres — structure reconciliation', () => {
154
156
  })).rejects.toThrow(PostgresCastRequired)
155
157
  })
156
158
  })
159
+
160
+ interface Plot extends ResourceRecord {
161
+ id?: string
162
+ garden: string
163
+ bed?: string
164
+ crop?: string
165
+ season: string
166
+ soil?: Record<string, unknown>
167
+ }
168
+
169
+ /**
170
+ * A community garden's plots: a plain compound index, an expression index and two partial ones —
171
+ * the shapes `pg_indexes.indexdef` renders differently from the statement that created them
172
+ * (identifiers unquoted, literals cast, a partial predicate parenthesized), plus an enum CHECK.
173
+ */
174
+ const plots = {
175
+ type: 'object',
176
+ properties: {
177
+ id: { type: 'string', format: 'uuid' },
178
+ garden: { type: 'string' },
179
+ bed: { type: 'string', nullable: true },
180
+ crop: { type: 'string', nullable: true },
181
+ season: { type: 'string', enum: ['spring', 'summer', 'autumn'] },
182
+ soil: { type: 'object', nullable: true }
183
+ },
184
+ required: ['id', 'garden', 'season'],
185
+ pg: {
186
+ indexes: [
187
+ { name: 'sync_plots_garden_season', columns: ['garden', 'season'] },
188
+ { name: 'sync_plots_scope', unique: true, expression: `"garden", COALESCE("bed", ''), "season"` },
189
+ { name: 'sync_plots_crop', columns: ['garden', 'crop'], where: '"crop" IS NOT NULL' },
190
+ { name: 'sync_plots_wet', columns: ['garden'], where: `("soil"->>'state') = 'wet'` }
191
+ ]
192
+ }
193
+ }
194
+
195
+ const plotSuite = makeSuite('sync_idx')
196
+
197
+ describe('@owlmeans/postgres — reconciling a table already at its shape', () => {
198
+ if (gate.skip) {
199
+ test.skip(gate.reason ?? 'postgres gate closed', () => {})
200
+
201
+ return
202
+ }
203
+
204
+ afterAll(async () => {
205
+ await plotSuite.teardown()
206
+ })
207
+
208
+ const bootPlots = async (): Promise<{ pg: PostgresService, resource: PostgresResource<Plot> }> => {
209
+ const resource = makePostgresResource<Plot, PostgresResource<Plot>>('sync-plots')
210
+ resource.schema = plots as never
211
+ const booted = await plotSuite.boot({ resources: [resource] })
212
+
213
+ return { pg: booted.pg, resource: booted.context.resource<PostgresResource<Plot>>('sync-plots') }
214
+ }
215
+
216
+ const objectOids = async (pg: PostgresService): Promise<Record<string, number>> => Object.fromEntries(
217
+ (await pg.query<{ name: string, oid: number }>(
218
+ `SELECT c.relname AS name, c.oid::int AS oid FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
219
+ WHERE n.nspname = $1 AND c.relkind = 'i' AND c.relname LIKE 'sync_plots_%'
220
+ UNION ALL
221
+ SELECT k.conname AS name, k.oid::int AS oid FROM pg_constraint k JOIN pg_namespace n ON n.oid = k.connamespace
222
+ WHERE n.nspname = $1 AND k.contype = 'c' AND k.conname LIKE 'sync_plots_%'`, [plotSuite.schema]
223
+ )).map(row => [row.name, row.oid])
224
+ )
225
+
226
+ it('plans nothing on a second boot — no index or check is dropped and recreated', async () => {
227
+ const first = await bootPlots()
228
+ const before = await objectOids(first.pg)
229
+ expect(Object.keys(before).sort()).toEqual([
230
+ 'sync_plots_crop', 'sync_plots_garden_season', 'sync_plots_pkey', 'sync_plots_scope', 'sync_plots_season_enum',
231
+ 'sync_plots_wet'
232
+ ])
233
+
234
+ const second = await bootPlots()
235
+ const { pool } = await second.resource.db()
236
+ const client = await pool.connect()
237
+ try {
238
+ const table = second.resource.table
239
+ const live = await introspectTable(client, table.schema, table.table, table.qualified)
240
+ expect(planSync(table, live).statements.map(statement => statement.sql)).toEqual([])
241
+ } finally {
242
+ client.release()
243
+ }
244
+ expect(await objectOids(second.pg)).toEqual(before)
245
+ })
246
+ })