@effect/sql-pg 4.0.0-rc.115 → 4.0.0-rc.117

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/src/PgMigrator.ts CHANGED
@@ -13,12 +13,12 @@ import * as Effect from "effect/Effect"
13
13
  import * as FileSystem from "effect/FileSystem"
14
14
  import * as Layer from "effect/Layer"
15
15
  import * as Path from "effect/Path"
16
- import * as Redacted from "effect/Redacted"
17
16
  import * as ChildProcess from "effect/unstable/process/ChildProcess"
18
17
  import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"
19
18
  import * as Migrator from "effect/unstable/sql/Migrator"
20
19
  import type { SqlClient } from "effect/unstable/sql/SqlClient"
21
20
  import type { SqlError } from "effect/unstable/sql/SqlError"
21
+ import * as Password from "./internal/password.ts"
22
22
  import { PgClient } from "./PgClient.ts"
23
23
 
24
24
  /**
@@ -45,7 +45,7 @@ export const run: <R2 = never>(
45
45
  | R2
46
46
  > = Migrator.make({
47
47
  dumpSchema(path, table) {
48
- const pgDump = (args: Array<string>) =>
48
+ const pgDump = (args: Array<string>, password: string | undefined) =>
49
49
  Effect.gen(function*() {
50
50
  const sql = yield* PgClient
51
51
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
@@ -55,9 +55,7 @@ export const run: <R2 = never>(
55
55
  PGHOST: sql.config.host,
56
56
  PGPORT: sql.config.port?.toString(),
57
57
  PGUSER: sql.config.username,
58
- PGPASSWORD: sql.config.password
59
- ? Redacted.value(sql.config.password)
60
- : undefined,
58
+ PGPASSWORD: password,
61
59
  PGDATABASE: sql.config.database,
62
60
  PGSSLMODE: sql.config.ssl ? "require" : "prefer"
63
61
  }
@@ -69,21 +67,20 @@ export const run: <R2 = never>(
69
67
  .replace(/\n{2,}/gm, "\n\n")
70
68
  .trim();
71
69
  }).pipe(
72
- Effect.mapError((error) => new Migrator.MigrationError({ kind: "Failed", message: error.message }))
70
+ Effect.mapError((error) =>
71
+ new Migrator.MigrationError({ kind: "Failed", message: error.message, cause: error })
72
+ )
73
73
  )
74
74
 
75
- const pgDumpSchema = pgDump(["--schema-only"])
76
-
77
- const pgDumpMigrations = pgDump([
78
- "--column-inserts",
79
- "--data-only",
80
- `--table=${table}`
81
- ])
82
-
83
- const pgDumpAll = Effect.map(
84
- Effect.all([pgDumpSchema, pgDumpMigrations], { concurrency: 2 }),
85
- ([schema, migrations]) => schema + "\n\n" + migrations
86
- )
75
+ const pgDumpAll = Effect.gen(function*() {
76
+ const sql = yield* PgClient
77
+ const password = yield* Password.resolve(sql.config.password)
78
+ const [schema, migrations] = yield* Effect.all([
79
+ pgDump(["--schema-only"], password),
80
+ pgDump(["--column-inserts", "--data-only", `--table=${table}`], password)
81
+ ], { concurrency: 2 })
82
+ return schema + "\n\n" + migrations
83
+ })
87
84
 
88
85
  const pgDumpFile = (path: string) =>
89
86
  Effect.gen(function*() {
@@ -93,7 +90,9 @@ export const run: <R2 = never>(
93
90
  yield* fs.makeDirectory(path_.dirname(path), { recursive: true })
94
91
  yield* fs.writeFileString(path, dump)
95
92
  }).pipe(
96
- Effect.mapError((error) => new Migrator.MigrationError({ kind: "Failed", message: error.message }))
93
+ Effect.mapError((error) =>
94
+ new Migrator.MigrationError({ kind: "Failed", message: error.message, cause: error })
95
+ )
97
96
  )
98
97
 
99
98
  return pgDumpFile(path)
package/src/PgPool.ts CHANGED
@@ -140,7 +140,7 @@ export const make = Effect.fnUntraced(function*(options: Config): Effect.fn.Retu
140
140
  Effect.sync(() => {
141
141
  createdAt.set(connection, clock.currentTimeMillisUnsafe())
142
142
  const internals = connectionInternals(connection)
143
- internals.fatalHooks.add(() => {
143
+ internals.retireHooks.add(() => {
144
144
  deadConnections.add(connection)
145
145
  // `deadConnections` is only read by the next checkout, and a checkout
146
146
  // already waiting for this connection would never get that far. Tell
@@ -204,12 +204,9 @@ export const make = Effect.fnUntraced(function*(options: Config): Effect.fn.Retu
204
204
  // `pin` reserves the pool item itself, so this needs no help.
205
205
  const reserve = Effect.flatMap(get, (connection) => connection.pin)
206
206
 
207
- // `Pool.use` cannot check the session it hands over before running the
208
- // effect, so it is only taken when there is nothing to check: no session is
209
- // known dead, and no lifetime can have run out. Otherwise the scoped
210
- // checkout does its replacement pass first. Checking inside the callback
211
- // instead would hold one lease while acquiring another, which deadlocks a
212
- // pool of one.
207
+ // `Pool.use` cannot pre-check a session. Use it only when no TTL applies and
208
+ // no connection awaits retirement. Checking inside its callback can deadlock
209
+ // a size-one pool while acquiring a replacement.
213
210
  const use = <A, E, R>(
214
211
  f: (connection: PgConnection.PgConnection) => Effect.Effect<A, E, R>
215
212
  ): Effect.Effect<A, E | SqlError, R> =>
package/src/PgTypes.ts CHANGED
@@ -10,9 +10,21 @@
10
10
  * There is no `typeof` inference: an OID is always supplied, either directly
11
11
  * or through a constructor such as `int4` that carries it.
12
12
  *
13
- * `timestamp` has no time zone on the wire and is treated as UTC in both
14
- * directions. Decoding drops sub-millisecond precision by truncating toward
15
- * zero, including for timestamps before the PostgreSQL epoch.
13
+ * `timestamp` and `timestamptz` values, including array elements, decode to
14
+ * `Date`. Encoders accept `Date` or epoch milliseconds.
15
+ * Decoding truncates to milliseconds toward zero relative to the PostgreSQL
16
+ * epoch. To restore numeric decoding, override the codecs with `register`
17
+ * or a client `Registry`.
18
+ *
19
+ * `infinity`, `-infinity` and values outside the JavaScript `Date` range
20
+ * (±8.64e15 epoch milliseconds) decode to an invalid `Date`. Numeric
21
+ * `±Infinity` encodes the PostgreSQL sentinels; encoding an invalid `Date` fails.
22
+ *
23
+ * The `timestamp` codec maps wall-clock fields to UTC fields of a `Date`.
24
+ * Date parameters bind as `timestamptz`, so inserting one into a `timestamp`
25
+ * column applies the session `TimeZone`. Use UTC or `timestamp(value)` to
26
+ * preserve its UTC fields. `timestamptz` round trips preserve the instant
27
+ * regardless of session timezone.
16
28
  *
17
29
  * @since 4.0.0
18
30
  */
@@ -301,6 +313,7 @@ export const OID = {
301
313
  timestamptz: 1184,
302
314
  timetz: 1266,
303
315
  numeric: 1700,
316
+ regclass: 2205,
304
317
  uuid: 2950,
305
318
  jsonb: 3802,
306
319
  boolArray: 1000,
@@ -324,6 +337,7 @@ export const OID = {
324
337
  timestamptzArray: 1185,
325
338
  timetzArray: 1270,
326
339
  numericArray: 1231,
340
+ regclassArray: 2210,
327
341
  uuidArray: 2951,
328
342
  jsonbArray: 3807
329
343
  } as const
@@ -350,6 +364,7 @@ const arrayToElement = new Map<number, number>([
350
364
  [OID.timestamptzArray, OID.timestamptz],
351
365
  [OID.timetzArray, OID.timetz],
352
366
  [OID.numericArray, OID.numeric],
367
+ [OID.regclassArray, OID.regclass],
353
368
  [OID.uuidArray, OID.uuid],
354
369
  [OID.jsonbArray, OID.jsonb]
355
370
  ])
@@ -992,20 +1007,22 @@ const readTimeMicros = (bytes: Uint8Array, offset: number): number => {
992
1007
  return micros
993
1008
  }
994
1009
 
995
- /** The two halves of the int64 `timestampInt64` last produced. */
996
1010
  let timestampHigh = 0
997
1011
  let timestampLow = 0
998
1012
 
999
1013
  /**
1000
- * Converts epoch milliseconds to the halves of the wire int64. Both encoding
1001
- * paths read them from here rather than from a returned pair, so neither
1002
- * allocates.
1014
+ * Writes the wire int64 to timestampHigh/Low, avoiding a pair allocation.
1003
1015
  */
1004
1016
  const timestampInt64 = (value: unknown): void => {
1005
- const ms = requireNumber(value, "timestamp")
1006
- if (Number.isNaN(ms)) {
1007
- fail("timestamp cannot be NaN")
1008
- } else if (ms === Number.POSITIVE_INFINITY) {
1017
+ let ms: number
1018
+ if (value instanceof Date) {
1019
+ ms = value.getTime()
1020
+ if (Number.isNaN(ms)) fail("timestamp cannot be an invalid Date")
1021
+ } else {
1022
+ ms = typeof value === "number" ? value : fail("Expected a Date or number for timestamp")
1023
+ if (Number.isNaN(ms)) fail("timestamp cannot be NaN")
1024
+ }
1025
+ if (ms === Number.POSITIVE_INFINITY) {
1009
1026
  timestampHigh = INT32_MAX
1010
1027
  timestampLow = -1
1011
1028
  } else if (ms === Number.NEGATIVE_INFINITY) {
@@ -1037,17 +1054,14 @@ const timestampCodec: UnsafeCodec<any> = codecOf(
1037
1054
  requireSize(size, 8, "timestamp")
1038
1055
  const high = readInt32(bytes, offset)
1039
1056
  if (high >= -MAX_EXACT_HIGH && high < MAX_EXACT_HIGH) {
1040
- // Inside these bounds the whole conversion is float arithmetic, so it
1041
- // allocates no BigInt. Everything outside them, the sentinels included,
1042
- // needs the exact 64-bit value.
1057
+ // These bounds allow exact conversion without BigInt.
1043
1058
  const micros = high * 4294967296 + readUint32(bytes, offset + 4)
1044
- return (micros - micros % 1000) / 1000 + PG_EPOCH_MS
1059
+ return new Date((micros - micros % 1000) / 1000 + PG_EPOCH_MS)
1045
1060
  }
1046
1061
  stage8(bytes, offset)
1047
1062
  const micros = scratchView8.getBigInt64(0)
1048
- if (micros === INT64_MAX) return Number.POSITIVE_INFINITY
1049
- if (micros === INT64_MIN) return Number.NEGATIVE_INFINITY
1050
- return Number(micros / THOUSAND) + PG_EPOCH_MS
1063
+ if (micros === INT64_MAX || micros === INT64_MIN) return new Date(Number.NaN)
1064
+ return new Date(Number(micros / THOUSAND) + PG_EPOCH_MS)
1051
1065
  },
1052
1066
  (value) => {
1053
1067
  timestampInt64(value)
@@ -1056,7 +1070,6 @@ const timestampCodec: UnsafeCodec<any> = codecOf(
1056
1070
  writeInt32(bytes, 4, timestampLow)
1057
1071
  return bytes
1058
1072
  },
1059
- // Two int32s are the int64, so the sink needs nothing of its own for it.
1060
1073
  (sink, value) => {
1061
1074
  timestampInt64(value)
1062
1075
  sink.int32(timestampHigh)
@@ -1150,6 +1163,23 @@ const jsonbCodec: UnsafeCodec<any> = codecOf(
1150
1163
  }
1151
1164
  )
1152
1165
 
1166
+ const oidCodec: UnsafeCodec<any> = codecOf(
1167
+ (bytes, offset, size) => {
1168
+ requireSize(size, 4, "oid")
1169
+ return readUint32(bytes, offset)
1170
+ },
1171
+ (value) => {
1172
+ const num = requireInteger(value, "oid", 0, 4294967295)
1173
+ const bytes = new Uint8Array(4)
1174
+ bytes[0] = num >>> 24
1175
+ bytes[1] = num >>> 16
1176
+ bytes[2] = num >>> 8
1177
+ bytes[3] = num
1178
+ return bytes
1179
+ },
1180
+ (sink, value) => sink.int32(requireInteger(value, "oid", 0, 4294967295))
1181
+ )
1182
+
1153
1183
  const builtinScalars = new Map<number, UnsafeCodec<any>>([
1154
1184
  [
1155
1185
  OID.bool,
@@ -1214,25 +1244,8 @@ const builtinScalars = new Map<number, UnsafeCodec<any>>([
1214
1244
  (sink, value) => sink.int32(requireInteger(value, "int4", INT32_MIN, INT32_MAX))
1215
1245
  )
1216
1246
  ],
1217
- [
1218
- OID.oid,
1219
- codecOf(
1220
- (bytes, offset, size) => {
1221
- requireSize(size, 4, "oid")
1222
- return readUint32(bytes, offset)
1223
- },
1224
- (value) => {
1225
- const num = requireInteger(value, "oid", 0, 4294967295)
1226
- const bytes = new Uint8Array(4)
1227
- bytes[0] = num >>> 24
1228
- bytes[1] = num >>> 16
1229
- bytes[2] = num >>> 8
1230
- bytes[3] = num
1231
- return bytes
1232
- },
1233
- (sink, value) => sink.int32(requireInteger(value, "oid", 0, 4294967295))
1234
- )
1235
- ],
1247
+ [OID.oid, oidCodec],
1248
+ [OID.regclass, oidCodec],
1236
1249
  [
1237
1250
  OID.int8,
1238
1251
  codecOf(
@@ -1399,6 +1412,13 @@ const lookupFor = (registry: Registry | undefined): Lookup =>
1399
1412
  * Registers a binary codec for an OID the built-in catalogue does not cover,
1400
1413
  * or overrides a built-in one. Registered codecs take precedence.
1401
1414
  *
1415
+ * **Details**
1416
+ *
1417
+ * Unregistered OIDs decode as UTF-8 text. Register binary user-defined types
1418
+ * to avoid garbled output or codec errors, which close the connection when
1419
+ * reading rows. For arrays, use `makeRegistry().register` with
1420
+ * `RegisterOptions.arrayOid` and pass the registry as the client's `types` option.
1421
+ *
1402
1422
  * @category registry
1403
1423
  * @since 4.0.0
1404
1424
  */
@@ -1591,9 +1611,9 @@ export interface Column {
1591
1611
  *
1592
1612
  * **Details**
1593
1613
  *
1594
- * Codecs are resolved once per column. SQL `NULL` becomes `null`, and columns
1595
- * without a registered codec return a copy of their bytes. Text-format columns
1596
- * fail with `CodecError`.
1614
+ * Codecs are resolved once per column. SQL `NULL` becomes `null`.
1615
+ * Unregistered OIDs decode as UTF-8 text. Invalid UTF-8 and text-format
1616
+ * columns fail with `CodecError`.
1597
1617
  *
1598
1618
  * **Example** (Updating the reader after `RowDescription`)
1599
1619
  *
@@ -1624,7 +1644,7 @@ export const makeFieldReader = (
1624
1644
  return (bytes: Uint8Array, offset: number, size: number, column: number): unknown => {
1625
1645
  if (size < 0) return null
1626
1646
  const codec = codecs[column]
1627
- if (codec === undefined) return bytes.slice(offset, offset + size)
1647
+ if (codec === undefined) return decodeUtf8(bytes, offset, size)
1628
1648
  const read = codec.read
1629
1649
  return read === undefined ? codec.decode(bytes.subarray(offset, offset + size)) : read(bytes, offset, size)
1630
1650
  }
@@ -1653,8 +1673,9 @@ export const encode = (value: unknown, oid: number, registry?: Registry): Result
1653
1673
  *
1654
1674
  * **Details**
1655
1675
  *
1656
- * `format` must be `1`; the text format is not implemented. An OID that is
1657
- * neither built in nor registered decodes to the raw bytes.
1676
+ * Only binary format (`1`) is supported. Unregistered OIDs decode as UTF-8
1677
+ * text; invalid UTF-8 fails with `CodecError`. See `register` for binary
1678
+ * user-defined types, including arrays.
1658
1679
  *
1659
1680
  * @category decoding
1660
1681
  * @since 4.0.0
@@ -1670,7 +1691,7 @@ export const decode = (
1670
1691
  return fail(`Only the binary format is supported, received format ${format}`)
1671
1692
  }
1672
1693
  const codec = lookupFor(registry)(oid)
1673
- return codec === undefined ? bytes : codec.decode(bytes)
1694
+ return codec === undefined ? decodeUtf8(bytes, 0, bytes.length) : codec.decode(bytes)
1674
1695
  })
1675
1696
 
1676
1697
  // -----------------------------------------------------------------------------
@@ -1962,21 +1983,21 @@ export const time: (value: bigint | null) => Parameter = parameter(OID.time)
1962
1983
  export const timetz: (value: string | null) => Parameter = parameter(OID.timetz)
1963
1984
 
1964
1985
  /**
1965
- * A `timestamp` parameter, given as Unix epoch milliseconds and interpreted
1966
- * as UTC.
1986
+ * A `timestamp` parameter from a `Date` or epoch milliseconds. UTC fields
1987
+ * become the stored wall-clock fields, regardless of session `TimeZone`.
1967
1988
  *
1968
1989
  * @category constructors
1969
1990
  * @since 4.0.0
1970
1991
  */
1971
- export const timestamp: (value: number | null) => Parameter = parameter(OID.timestamp)
1992
+ export const timestamp: (value: Date | number | null) => Parameter = parameter(OID.timestamp)
1972
1993
 
1973
1994
  /**
1974
- * A `timestamptz` parameter, given as Unix epoch milliseconds.
1995
+ * A `timestamptz` parameter, given as a `Date` or Unix epoch milliseconds.
1975
1996
  *
1976
1997
  * @category constructors
1977
1998
  * @since 4.0.0
1978
1999
  */
1979
- export const timestamptz: (value: number | null) => Parameter = parameter(OID.timestamptz)
2000
+ export const timestamptz: (value: Date | number | null) => Parameter = parameter(OID.timestamptz)
1980
2001
 
1981
2002
  /**
1982
2003
  * A one-dimensional array parameter whose elements have the given OID.
@@ -10,8 +10,8 @@ export interface ConnectionInternals {
10
10
  /** The pool item identity: the unpinned base connection. */
11
11
  readonly base: object
12
12
  readonly deadError: () => SqlError | undefined
13
- /** Fired once when the connection dies outside its own scope release. */
14
- readonly fatalHooks: Set<() => void>
13
+ /** Fired when a pool must stop reusing the connection. */
14
+ readonly retireHooks: Set<() => void>
15
15
  /**
16
16
  * Installed by a multiplexed pool so that pinning the session also takes it
17
17
  * out of shared circulation for as long as the pin lasts. Every route to an
@@ -0,0 +1,11 @@
1
+ import * as Effect from "effect/Effect"
2
+ import * as Redacted from "effect/Redacted"
3
+
4
+ export const resolve = (
5
+ password: Redacted.Redacted | Effect.Effect<Redacted.Redacted> | undefined
6
+ ): Effect.Effect<string | undefined> =>
7
+ password === undefined
8
+ ? Effect.succeed(undefined)
9
+ : Effect.isEffect(password)
10
+ ? Effect.map(password, Redacted.value)
11
+ : Effect.succeed(Redacted.value(password))