@keyv/postgres 2.2.3 → 6.0.0-alpha.2

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
@@ -1,8 +1,8 @@
1
- # @keyv/postgres [<img width="100" align="right" src="https://jaredwray.com/images/keyv-symbol.svg" alt="keyv">](https://github.com/jaredwra/keyv)
1
+ # @keyv/postgres [<img width="100" align="right" src="https://jaredwray.com/images/keyv-symbol.svg" alt="keyv">](https://github.com/jaredwray/keyv)
2
2
 
3
3
  > PostgreSQL storage adapter for Keyv
4
4
 
5
- [![build](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml/badge.svg)](https://github.com/jaredwray/keyv/actions/workflows/btestsuild.yaml)
5
+ [![build](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml/badge.svg)](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml)
6
6
  [![codecov](https://codecov.io/gh/jaredwray/keyv/branch/main/graph/badge.svg?token=bRzR3RyOXZ)](https://codecov.io/gh/jaredwray/keyv)
7
7
  [![npm](https://img.shields.io/npm/v/@keyv/postgres.svg)](https://www.npmjs.com/package/@keyv/postgres)
8
8
  [![npm](https://img.shields.io/npm/dm/@keyv/postgres)](https://npmjs.com/package/@keyv/postgres)
@@ -11,55 +11,450 @@ PostgreSQL storage adapter for [Keyv](https://github.com/jaredwray/keyv).
11
11
 
12
12
  Requires Postgres 9.5 or newer for `ON CONFLICT` support to allow performant upserts. [Why?](https://stackoverflow.com/questions/17267417/how-to-upsert-merge-insert-on-duplicate-update-in-postgresql/17267423#17267423)
13
13
 
14
- ## Install
14
+ ## Table of Contents
15
+
16
+ - [Install](#install)
17
+ - [Usage](#usage)
18
+ - [Migrating to v6](#migrating-to-v6)
19
+ - [Constructor Options](#constructor-options)
20
+ - [Properties](#properties)
21
+ - [uri](#uri)
22
+ - [table](#table)
23
+ - [keyLength](#keylength)
24
+ - [namespaceLength](#namespacelength)
25
+ - [schema](#schema)
26
+ - [ssl](#ssl)
27
+ - [iterationLimit](#iterationlimit)
28
+ - [useUnloggedTable](#useunloggedtable)
29
+ - [clearExpiredInterval](#clearexpiredinterval)
30
+ - [namespace](#namespace)
31
+ - [Methods](#methods)
32
+ - [.set(key, value)](#setkey-value)
33
+ - [.setMany(entries)](#setmanyentries)
34
+ - [.get(key)](#getkey)
35
+ - [.getMany(keys)](#getmanykeys)
36
+ - [.has(key)](#haskey)
37
+ - [.hasMany(keys)](#hasmanykeys)
38
+ - [.delete(key)](#deletekey)
39
+ - [.deleteMany(keys)](#deletemanykeys)
40
+ - [.clear()](#clear)
41
+ - [.clearExpired()](#clearexpired)
42
+ - [.iterator(namespace?)](#iteratornamespace)
43
+ - [.disconnect()](#disconnect)
44
+ - [Using an Unlogged Table for Performance](#using-an-unlogged-table-for-performance)
45
+ - [Connection Pooling](#connection-pooling)
46
+ - [SSL/TLS Connections](#ssltls-connections)
47
+ - [Testing](#testing)
48
+ - [License](#license)
49
+
50
+ # Install
15
51
 
16
52
  ```shell
17
53
  npm install --save keyv @keyv/postgres
18
54
  ```
19
55
 
20
- ## Usage
56
+ # Usage
21
57
 
22
58
  ```js
23
59
  import Keyv from 'keyv';
24
60
  import KeyvPostgres from '@keyv/postgres';
25
61
 
26
- const keyv = new Keyv(new KeyvPostgres('postgresql://user:pass@localhost:5432/dbname'));
62
+ const keyv = new Keyv({ store: new KeyvPostgres('postgresql://user:pass@localhost:5432/dbname') });
27
63
  keyv.on('error', handleConnectionError);
28
64
  ```
29
65
 
30
- You can specify the `table` option.
66
+ You can specify the `table` and `schema` options:
31
67
 
32
- e.g:
68
+ ```js
69
+ const keyvPostgres = new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname', table: 'cache', schema: 'keyv' });
70
+ const keyv = new Keyv({ store: keyvPostgres });
71
+ ```
72
+
73
+ You can also use the `createKeyv` helper function to create `Keyv` with `KeyvPostgres` store:
33
74
 
34
75
  ```js
35
- const keyvPostgres = new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname', table: 'cache' });
36
- const keyv = new Keyv(keyvPostgres);
76
+ import { createKeyv } from '@keyv/postgres';
77
+
78
+ const keyv = createKeyv({ uri: 'postgresql://user:pass@localhost:5432/dbname', table: 'cache', schema: 'keyv' });
37
79
  ```
38
80
 
39
- You can specify the `schema` option (default is `public`).
81
+ # Migrating to v6
82
+
83
+ ## Breaking changes
40
84
 
41
- e.g:
85
+ ### Properties instead of opts
86
+
87
+ In v5, configuration was accessed through the `opts` object:
42
88
 
43
89
  ```js
44
- const keyvPostgres = new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname', schema: 'keyv' });
45
- const keyv = new Keyv(keyvPostgres);
90
+ // v5
91
+ store.opts.table; // 'keyv'
92
+ store.opts.schema; // 'public'
46
93
  ```
47
94
 
48
- You can also use the `createKeyv` helper function to create `Keyv` with `KeyvPostgres` store.
95
+ In v6, all configuration options are exposed as top-level properties with getters and setters:
49
96
 
50
97
  ```js
51
- import {createKeyv} from '@keyv/postgres';
98
+ // v6
99
+ store.table; // 'keyv'
100
+ store.schema; // 'public'
101
+ store.table = 'cache';
102
+ ```
52
103
 
53
- const keyv = createKeyv({ uri: 'postgresql://user:pass@localhost:5432/dbname', table: 'cache', schema: 'keyv' });
104
+ The `opts` getter still exists for backward compatibility but should not be used for new code.
105
+
106
+ ### Native namespace support
107
+
108
+ In v5, namespaces were stored as key prefixes in the `key` column (e.g. `key="myns:mykey"` with `namespace=NULL`). In v6, the namespace is stored in a dedicated `namespace` column (e.g. `key="mykey"`, `namespace="myns"`). This enables more efficient queries and proper namespace isolation.
109
+
110
+ The adapter automatically adds the `namespace` column and creates the appropriate index when it connects, so no manual schema changes are needed for new installations.
111
+
112
+ ### Hookified integration
113
+
114
+ The adapter now extends [Hookified](https://hookified.org) instead of a custom EventEmitter. Events work the same (`on`, `emit`), but hooks are also available via the standard Hookified API.
115
+
116
+ ## New features
117
+
118
+ ### Native TTL support with `expires` column
119
+
120
+ v6 adds an `expires BIGINT` column to the table. When values are stored with a TTL via Keyv core, the adapter automatically extracts the `expires` timestamp from the serialized value and stores it in the column. A partial index is created on the `expires` column for efficient cleanup queries.
121
+
122
+ The schema migration is automatic on connect — existing tables get the column added via `ADD COLUMN IF NOT EXISTS`.
123
+
124
+ ### `clearExpired()` method
125
+
126
+ A new utility method that deletes all rows where the `expires` column is set and the timestamp is in the past:
127
+
128
+ ```js
129
+ await store.clearExpired();
130
+ ```
131
+
132
+ ### `clearExpiredInterval` option
133
+
134
+ Set an interval (in milliseconds) to automatically call `clearExpired()` on a schedule. Disabled by default (`0`). The timer uses `unref()` so it won't keep the Node.js process alive.
135
+
136
+ ```js
137
+ const store = new KeyvPostgres({
138
+ uri: 'postgresql://user:pass@localhost:5432/dbname',
139
+ clearExpiredInterval: 60_000, // clean up every 60 seconds
140
+ });
141
+ ```
142
+
143
+ ### Bulk operations
144
+
145
+ New methods for efficient multi-key operations:
146
+
147
+ - `.setMany(entries)` — bulk upsert using PostgreSQL `UNNEST`
148
+ - `.getMany(keys)` — bulk retrieve using `ANY`
149
+ - `.deleteMany(keys)` — bulk delete using `ANY`
150
+ - `.hasMany(keys)` — bulk existence check
151
+
152
+ ### `createKeyv()` helper
153
+
154
+ A convenience function to create a `Keyv` instance with `KeyvPostgres` as the store in one call:
155
+
156
+ ```js
157
+ import { createKeyv } from '@keyv/postgres';
158
+
159
+ const keyv = createKeyv({ uri: 'postgresql://user:pass@localhost:5432/dbname' });
160
+ ```
161
+
162
+ ### Improved iterator
163
+
164
+ The iterator now uses cursor-based (keyset) pagination instead of `OFFSET`. This handles concurrent deletions during iteration without skipping entries and is more efficient for large datasets.
165
+
166
+ ## Running the migration script
167
+
168
+ If you have existing data from v5, you need to run the migration script to move namespace prefixes from keys into the new `namespace` column. The script is located at `scripts/migrate-v6.ts` in the `@keyv/postgres` package.
169
+
170
+ Preview the changes first with `--dry-run`:
171
+
172
+ ```shell
173
+ npx tsx scripts/migrate-v6.ts --uri postgresql://user:pass@localhost:5432/dbname --dry-run
174
+ ```
175
+
176
+ Run the migration:
177
+
178
+ ```shell
179
+ npx tsx scripts/migrate-v6.ts --uri postgresql://user:pass@localhost:5432/dbname
180
+ ```
181
+
182
+ You can also specify a custom table, schema, and column lengths:
183
+
184
+ ```shell
185
+ npx tsx scripts/migrate-v6.ts --uri postgresql://user:pass@localhost:5432/dbname --table cache --schema keyv
186
+ npx tsx scripts/migrate-v6.ts --uri postgresql://user:pass@localhost:5432/dbname --keyLength 512 --namespaceLength 512
187
+ ```
188
+
189
+ The migration runs inside a transaction and will roll back automatically if anything fails.
190
+
191
+ **Important notes:**
192
+ - The script only migrates rows where `namespace IS NULL`. Rows that already have a namespace value (e.g. from a partial earlier migration) are skipped.
193
+ - Keys are split on the first colon — the part before becomes the namespace, the rest becomes the key. Namespaces containing colons are not supported.
194
+
195
+ # Constructor Options
196
+
197
+ `KeyvPostgres` accepts a connection URI string or an options object. The options object accepts the following properties along with any [`PoolConfig`](https://node-postgres.com/apis/pool) properties from the `pg` library (e.g. `max`, `idleTimeoutMillis`, `connectionTimeoutMillis`):
198
+
199
+ | Option | Type | Default | Description |
200
+ | --- | --- | --- | --- |
201
+ | `uri` | `string` | `'postgresql://localhost:5432'` | PostgreSQL connection URI |
202
+ | `table` | `string` | `'keyv'` | Table name for key-value storage |
203
+ | `keyLength` | `number` | `255` | Maximum key column length (VARCHAR length) |
204
+ | `namespaceLength` | `number` | `255` | Maximum namespace column length (VARCHAR length) |
205
+ | `schema` | `string` | `'public'` | PostgreSQL schema name (created automatically if it doesn't exist) |
206
+ | `ssl` | `object` | `undefined` | SSL/TLS configuration passed to the `pg` driver |
207
+ | `iterationLimit` | `number` | `10` | Number of rows fetched per batch during iteration |
208
+ | `useUnloggedTable` | `boolean` | `false` | Use a PostgreSQL UNLOGGED table for better write performance |
209
+ | `clearExpiredInterval` | `number` | `0` | Interval in milliseconds to automatically clear expired entries (0 = disabled) |
210
+
211
+ # Properties
212
+
213
+ All configuration options are exposed as properties with getters and setters on the `KeyvPostgres` instance. You can read or update them after construction.
214
+
215
+ ## uri
216
+
217
+ Get or set the PostgreSQL connection URI.
218
+
219
+ - Type: `string`
220
+ - Default: `'postgresql://localhost:5432'`
221
+
222
+ ```js
223
+ const store = new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname' });
224
+ console.log(store.uri); // 'postgresql://user:pass@localhost:5432/dbname'
225
+ ```
226
+
227
+ ## table
228
+
229
+ Get or set the table name used for storage.
230
+
231
+ - Type: `string`
232
+ - Default: `'keyv'`
233
+
234
+ ```js
235
+ const store = new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname' });
236
+ console.log(store.table); // 'keyv'
237
+ store.table = 'cache';
238
+ ```
239
+
240
+ ## keyLength
241
+
242
+ Get or set the maximum key length (VARCHAR length) for the key column.
243
+
244
+ - Type: `number`
245
+ - Default: `255`
246
+
247
+ ```js
248
+ const store = new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname', keyLength: 512 });
249
+ console.log(store.keyLength); // 512
250
+ ```
251
+
252
+ ## namespaceLength
253
+
254
+ Get or set the maximum namespace length (VARCHAR length) for the namespace column.
255
+
256
+ - Type: `number`
257
+ - Default: `255`
258
+
259
+ ```js
260
+ const store = new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname', namespaceLength: 512 });
261
+ console.log(store.namespaceLength); // 512
262
+ ```
263
+
264
+ ## schema
265
+
266
+ Get or set the PostgreSQL schema name. Non-public schemas are created automatically if they don't exist.
267
+
268
+ - Type: `string`
269
+ - Default: `'public'`
270
+
271
+ ```js
272
+ const store = new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname', schema: 'keyv' });
273
+ console.log(store.schema); // 'keyv'
274
+ ```
275
+
276
+ ## ssl
277
+
278
+ Get or set the SSL configuration for the PostgreSQL connection. Passed directly to the `pg` driver.
279
+
280
+ - Type: `object | undefined`
281
+ - Default: `undefined`
282
+
283
+ ```js
284
+ const store = new KeyvPostgres({
285
+ uri: 'postgresql://user:pass@localhost:5432/dbname',
286
+ ssl: { rejectUnauthorized: false },
287
+ });
288
+ console.log(store.ssl); // { rejectUnauthorized: false }
289
+ ```
290
+
291
+ ## iterationLimit
292
+
293
+ Get or set the number of rows to fetch per iteration batch.
294
+
295
+ - Type: `number`
296
+ - Default: `10`
297
+
298
+ ```js
299
+ const store = new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname', iterationLimit: 50 });
300
+ console.log(store.iterationLimit); // 50
301
+ ```
302
+
303
+ ## useUnloggedTable
304
+
305
+ Get or set whether to use a PostgreSQL unlogged table for better write performance. Unlogged tables are faster but data is lost on crash.
306
+
307
+ - Type: `boolean`
308
+ - Default: `false`
309
+
310
+ ```js
311
+ const store = new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname', useUnloggedTable: true });
312
+ console.log(store.useUnloggedTable); // true
313
+ ```
314
+
315
+ ## clearExpiredInterval
316
+
317
+ Get or set the interval in milliseconds between automatic expired-entry cleanup runs. When set to a value greater than 0, the adapter will automatically call `clearExpired()` at the specified interval. The timer uses `unref()` so it won't keep the Node.js process alive. Setting to 0 disables the automatic cleanup.
318
+
319
+ - Type: `number`
320
+ - Default: `0` (disabled)
321
+
322
+ ```js
323
+ // Clean up expired entries every 60 seconds
324
+ const store = new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname', clearExpiredInterval: 60_000 });
325
+ console.log(store.clearExpiredInterval); // 60000
326
+
327
+ // Disable it later
328
+ store.clearExpiredInterval = 0;
329
+ ```
330
+
331
+ ## namespace
332
+
333
+ Get or set the namespace for the adapter. Used for key prefixing and scoping operations like `clear()`.
334
+
335
+ - Type: `string | undefined`
336
+ - Default: `undefined`
337
+
338
+ ```js
339
+ const store = new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname' });
340
+ store.namespace = 'my-namespace';
341
+ console.log(store.namespace); // 'my-namespace'
342
+ ```
343
+
344
+ # Methods
345
+
346
+ ## .set(key, value)
347
+
348
+ Set a key-value pair.
349
+
350
+ ```js
351
+ await keyv.set('foo', 'bar');
352
+ ```
353
+
354
+ ## .setMany(entries)
355
+
356
+ Set multiple key-value pairs at once using PostgreSQL `UNNEST` for efficient bulk operations.
357
+
358
+ ```js
359
+ await keyv.setMany([
360
+ { key: 'foo', value: 'bar' },
361
+ { key: 'baz', value: 'qux' },
362
+ ]);
363
+ ```
364
+
365
+ ## .get(key)
366
+
367
+ Get a value by key. Returns `undefined` if the key does not exist.
368
+
369
+ ```js
370
+ const value = await keyv.get('foo'); // 'bar'
371
+ ```
372
+
373
+ ## .getMany(keys)
374
+
375
+ Get multiple values at once. Returns an array of values in the same order as the keys, with `undefined` for missing keys.
376
+
377
+ ```js
378
+ const values = await keyv.getMany(['foo', 'baz']); // ['bar', 'qux']
54
379
  ```
55
380
 
56
- ## Using an Unlogged Table for Performance
381
+ ## .has(key)
382
+
383
+ Check if a key exists. Returns a boolean.
384
+
385
+ ```js
386
+ const exists = await keyv.has('foo'); // true
387
+ ```
388
+
389
+ ## .hasMany(keys)
390
+
391
+ Check if multiple keys exist. Returns an array of booleans in the same order as the input keys.
392
+
393
+ ```js
394
+ await keyv.set('foo', 'bar');
395
+ await keyv.set('baz', 'qux');
396
+
397
+ const results = await keyv.hasMany(['foo', 'baz', 'unknown']); // [true, true, false]
398
+ ```
399
+
400
+ ## .delete(key)
401
+
402
+ Delete a key. Returns `true` if the key existed, `false` otherwise.
403
+
404
+ ```js
405
+ const deleted = await keyv.delete('foo'); // true
406
+ ```
407
+
408
+ ## .deleteMany(keys)
409
+
410
+ Delete multiple keys at once. Returns `true` if any of the keys existed.
411
+
412
+ ```js
413
+ const deleted = await keyv.deleteMany(['foo', 'baz']); // true
414
+ ```
415
+
416
+ ## .clear()
417
+
418
+ Clear all keys in the current namespace.
419
+
420
+ ```js
421
+ await keyv.clear();
422
+ ```
423
+
424
+ ## .clearExpired()
425
+
426
+ Utility helper method to delete all expired entries from the store. This removes any rows where the `expires` column is set and the timestamp is in the past. This is useful for periodic cleanup of expired data.
427
+
428
+ ```js
429
+ await keyv.clearExpired();
430
+ ```
431
+
432
+ ## .iterator(namespace?)
433
+
434
+ Iterate over all key-value pairs, optionally filtered by namespace. Uses cursor-based pagination controlled by the `iterationLimit` property.
435
+
436
+ ```js
437
+ const iterator = keyv.iterator();
438
+ for await (const [key, value] of iterator) {
439
+ console.log(key, value);
440
+ }
441
+ ```
442
+
443
+ ## .disconnect()
444
+
445
+ Disconnect from the PostgreSQL database and release the connection pool.
446
+
447
+ ```js
448
+ await keyv.disconnect();
449
+ ```
450
+
451
+ # Using an Unlogged Table for Performance
57
452
 
58
453
  By default, the adapter creates a logged table. If you want to use an unlogged table for performance, you can pass the `useUnloggedTable` option to the constructor.
59
454
 
60
455
  ```js
61
456
  const keyvPostgres = new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname', useUnloggedTable: true });
62
- const keyv = new Keyv(keyvPostgres);
457
+ const keyv = new Keyv({ store: keyvPostgres });
63
458
  ```
64
459
 
65
460
  From the [PostgreSQL documentation](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-UNLOGGED):
@@ -68,15 +463,31 @@ If specified, the table is created as an unlogged table. Data written to unlogge
68
463
 
69
464
  If this is specified, any sequences created together with the unlogged table (for identity or serial columns) are also created as unlogged.
70
465
 
71
- ## Connection pooling
466
+ # Connection Pooling
72
467
 
73
468
  The adapter automatically uses the default settings on the `pg` package for connection pooling. You can override these settings by passing the options to the constructor such as setting the `max` pool size.
74
469
 
75
470
  ```js
76
- const keyv = new Keyv(new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname', max: 20 }));
471
+ const keyv = new Keyv({ store: new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname', max: 20 }) });
77
472
  ```
78
473
 
79
- ## Testing
474
+ # SSL/TLS Connections
475
+
476
+ You can configure SSL/TLS connections by passing the `ssl` option. This is passed directly to the underlying `pg` driver.
477
+
478
+ ```js
479
+ const keyvPostgres = new KeyvPostgres({
480
+ uri: 'postgresql://user:pass@localhost:5432/dbname',
481
+ ssl: {
482
+ rejectUnauthorized: false,
483
+ },
484
+ });
485
+ const keyv = new Keyv({ store: keyvPostgres });
486
+ ```
487
+
488
+ For more details on SSL configuration, see the [node-postgres SSL documentation](https://node-postgres.com/features/ssl).
489
+
490
+ # Testing
80
491
 
81
492
  When testing you can use our `docker compose` postgresql instance by having docker installed and running. This will start a postgres server, run the tests, and stop the server:
82
493
 
@@ -85,11 +496,11 @@ At the root of the Keyv mono repo:
85
496
  pnpm test:services:start
86
497
  ```
87
498
 
88
- To just test the postgres adapter go to the postgres directory (packages/postgres) and run:
499
+ To just test the postgres adapter go to the postgres directory (storage/postgres) and run:
89
500
  ```shell
90
501
  pnpm test
91
502
  ```
92
503
 
93
- ## License
504
+ # License
94
505
 
95
506
  [MIT © Jared Wray](LISCENCE)