Skip to content

Configuration ​

What zapatos/db lets you set while your code runs: setConfig() and each option of Config, the listeners, the opt-ins and their warning codes. Then the types the generated schema gives values that JSON or pg hand back as strings, and the helpers that convert them.

setConfig() ​

ts
db.setConfig(newConfig: NewConfig): Config

Sets the keys of newConfig on the configuration, and leaves the others as they are. There is one configuration per process, shared by import and require.

ts
db.
setConfig
({
transactionAttemptsMax
: 3,
transactionRetryDelay
: {
minMs
: 50,
maxMs
: 500 },
undefinedMeansMissing
: true,
});
  • The merge is one level deep: transactionRetryDelay is replaced whole, so the type needs both minMs and maxMs.
  • A key that has a default and that you give as undefined (or null) goes back to its default: setConfig({ transactionRetryDelay: undefined }) sets { minMs: 25, maxMs: 250 }. A listener has none, so undefined removes it. Giving undefined compiles, except under exactOptionalPropertyTypes.
    • Zapatos 6.6.1 set the key to undefined. After transactionAttemptsMax: undefined, transaction() gave up after its first attempt ("… on attempt 1 of undefined, giving up"); after transactionRetryDelay: undefined, every transaction() rejected with "TypeError: Cannot destructure property 'minMs' of 'config.transactionRetryDelay' as it is undefined." and never released the client it took from a pool (#246).
  • It returns a copy of the new configuration, as getConfig() does: changing that object changes nothing. Zapatos 6.6.1 returned the configuration itself (#245).
  • It keeps the transactionRetryDelay object you give it, not a copy: changing its minMs or maxMs afterwards changes the configuration.
  • An unknown key does not compile (TS2353), and neither does a value of the wrong type (TS2322):
ts
db.
setConfig
({ retries: 3 });
Object literal may only specify known properties, and 'retries' does not exist in type 'Partial<Config>'.

getConfig() ​

ts
db.getConfig(): Config

A copy of the configuration, transactionRetryDelay included: getConfig().transactionRetryDelay.minMs = 1 changes nothing. The listeners are the functions you set. Zapatos 6.6.1 shared transactionRetryDelay, so that line changed the configuration (#245).

To change the configuration for a while, as tests do, keep the copy and give it back in a finally, so that a throw in between, a failing test, still restores it:

ts
const 
saved
= db.
getConfig
();
db.
setConfig
({
transactionRetryDelay
: {
minMs
: 0,
maxMs
: 0 } });
try { // … the transactions that should retry at once } finally { db.
setConfig
(
saved
);
}
  • It puts back each key saved holds.
  • A key saved doesn't hold stays as set: a listener set in between keeps listening. Give it as undefined to remove it: db.setConfig({ queryListener: undefined }).

Config ​

ts
interface Config {
  transactionAttemptsMax: number;
  transactionRetryDelay: { minMs: number; maxMs: number };
  castArrayParamsToJson: boolean;
  castObjectParamsToJson: boolean;
  upsertRequiresSameKeys?: boolean;
  undefinedMeansMissing?: boolean;
  transactionRequiresCommit?: boolean;
  selectExactlyOneChecksForSecondRow?: boolean;
  queryListener?(query: SQLQuery, txnId?: number): void;
  resultListener?(result: any, txnId?: number, elapsedMs?: number, query?: SQLQuery): void;
  transactionListener?(message: string, txnId?: number): void;
}

The keys pgcast adds are optional, so a Config written for Zapatos 6.6.1 still compiles; getConfig() types them boolean | undefined, though it always gives a value. No option changes a type.

OptionDefaultWhat it changesRead
transactionAttemptsMax5how many times transaction() runs its callbackat each transaction()
transactionRetryDelay{ minMs: 25, maxMs: 250 }the wait before each retryat each transaction()
castArrayParamsToJsonfalsean array parameter: CAST($1 AS "json") in place of $1at each compile()
castObjectParamsToJsonfalsea plain object parameter: CAST($1 AS "json") in place of $1at each compile()
upsertRequiresSameKeysfalsea multi-row upsert() with a key missing from some row, still set from EXCLUDED: throw, not warnat each upsert()
undefinedMeansMissingfalsean undefined value in insert(), update() and upsert(): a missing key, not NULLat each call of those
transactionRequiresCommitfalsea COMMIT that Postgres answers with ROLLBACK: reject, not resolveat each transaction()
selectExactlyOneChecksForSecondRowfalsea selectExactlyOne() run on its own: LIMIT 2, and a second row rejectsat each selectExactlyOne()
queryListenernonecalled with each query run() sendsat each run()
resultListenernonecalled with each result run() givesat each run()
transactionListenernonecalled when an attempt of transaction() fails with 40001 or 40P01, and at each retryat each transaction()

transactionAttemptsMax ​

ts
transactionAttemptsMax: number  // default: 5

How many times transaction() runs its callback. After a serialisation failure (40001) or a deadlock (40P01), it rolls back and runs the callback again, until this many attempts have failed; then it throws the last error. Any other error is rolled back and thrown at once.

  • 1, 0 or less: one attempt, no retry. With 0, the message reads "… on attempt 1 of 0, giving up" (#179).
  • Read when transaction() starts, so a change affects the next transaction, not the one running.
  • How to write a callback that can run twice: Retries.

transactionRetryDelay ​

ts
transactionRetryDelay: { minMs: number; maxMs: number }  // default: { minMs: 25, maxMs: 250 }

Before each retry, transaction() waits a whole number of milliseconds from minMs to maxMs, rounded from a uniform draw between them: Math.round(minMs + (maxMs - minMs) * Math.random()). So minMs and maxMs come up half as often as each value between them. transactionListener is told the wait.

castArrayParamsToJson ​

ts
castArrayParamsToJson: boolean  // default: false

With true, every array sent as a parameter is sent as JSON: CAST($1 AS "json"), its value JSON.stringify()'d. That is param(x), and the values of the shortcuts and Whereables, which are parameters too. Off, pg sends an array as a Postgres array.

A string cast wins (#36); Zapatos 6.6.1 dropped it for "json":

  • An array whose cast ends with [] or [n] is sent as an array: param([1, 2], 'int4[]') gives CAST($1 AS "int4"[]).
  • Any other array keeps the JSON, '_int4' and a domain over an array included: param([1, 2], 'jsonb') gives CAST($1 AS "jsonb").

TSYou write Open in playground

ts
db.
setConfig
({
castArrayParamsToJson
: true });
const
lengths
= await db.
sql
<db.
SQL
, {
n
: number }[]>`SELECT
json_array_length(${db.
param
(['mystery', 'drama'])}) AS n`.
run
(
pool
);

SQLPostgres runs

sql
SELECT
  json_array_length(CAST($1 AS "json")) AS n

values ["[\"mystery\",\"drama\"]"]

::TypeScript knows

ts
const lengths: {
  n: number;
}[]
  • Off, the same query fails: json_array_length($1) gives 22P02 "invalid input syntax for type json". param(x, true) sends the array as JSON under either setting.
  • On a Postgres array column, Postgres refuses an array sent as JSON: 42804 "column "genres" is of type text[] but expression is of type json" in an insert() or update(), 42883 "operator does not exist: text[] = json" in a Whereable, and "… text[] && json" with arrayOverlaps().
  • In an insert(), an update() or a Whereable, db.param(genres, 'text[]') sends it as an array. The array conditions take no Parameter (#40): compare in a sql fragment there.
  • param(x, false) sends the array as it is, under either setting: { genres: db.param(['mystery', 'drama'], false) } gives SET ("genres") = ROW($1).
  • The items of isIn() are parameters one by one, so an array of numbers there is unchanged: "id" IN ($1, $2).
  • How param() compiles each cast: param().

castObjectParamsToJson ​

ts
castObjectParamsToJson: boolean  // default: false

With true, every plain object sent as a parameter is sent as CAST($1 AS "json"), its value JSON.stringify()'d. Off, it is $1, and pg sends the same JSON text, with no type: Postgres then infers one from where $1 stands.

A string cast wins, the value still JSON text: param({ a: 1 }, 'jsonb') gives CAST($1 AS "jsonb"), which = and @> on a jsonb column take (#36); Zapatos 6.6.1 dropped it for "json", which they refuse (42883).

TSYou write Open in playground

ts
db.
setConfig
({
castObjectParamsToJson
: true });
const
pitches
= await db.
sql
<db.
SQL
, {
title
: string }[]>`SELECT
${db.
param
({
title
: 'The Lighthouse Wager',
runtime_minutes
: 101 })}
->> 'title' AS title`.
run
(
pool
);

SQLPostgres runs

sql
SELECT
  CAST($1 AS "json")
  ->> 'title' AS title

values ["{\"title\":\"The Lighthouse Wager\",\"runtime_minutes\":101}"]

::TypeScript knows

ts
const pitches: {
  title: string;
}[]
  • Off, the same query fails: $1 ->> 'title' gives 42725 "operator is not unique: unknown ->> unknown". A cast in the SQL, $1::jsonb, works either way.
  • A plain object is an object literal, {…}: a Date, a Buffer or a class instance is sent as $1, unchanged.
  • param(x, false) gives $1 under either setting; param(x, true) gives CAST($1 AS "json").

upsertRequiresSameKeys ​

ts
upsertRequiresSameKeys?: boolean  // default: false; the default in 7.0

With true, a multi-row upsert() without updateColumns, where a key missing from some row is still set from EXCLUDED, throws when called, before any query: "rows have different keys: pass updateColumns, or give every row the same keys".

Off, it runs as in Zapatos 6.6.1 and warns once per process: PGCAST_DEP001. Which keys count, and the SQL: Rows with different keys.

undefinedMeansMissing ​

ts
undefinedMeansMissing?: boolean  // default: false; the default in 7.0

With true, an undefined value in insert(), update() and upsert() is a missing key: DEFAULT in insert(), left out of update() and upsert(). Off, it is sent as NULL, as in Zapatos 6.6.1, and warns once per process: PGCAST_DEP002. The SQL of each: An undefined value.

transactionRequiresCommit ​

ts
transactionRequiresCommit?: boolean  // default: false; the default in 7.0

When the callback of transaction() catches a query's error and returns, Postgres has aborted the transaction: it answers the COMMIT with the command tag ROLLBACK, not an error, and nothing is saved.

With true, transaction() then rejects, without a retry, with "the transaction was rolled back, not committed: a query in it failed and the callback caught the error, so Postgres answered COMMIT with ROLLBACK; let the error out of the callback, or ROLLBACK TO a SAVEPOINT before going on".

Off, it resolves with what the callback returned, as in Zapatos 6.6.1, and warns once per process: PGCAST_DEP003. What to do instead: Let the error through.

  • Not covered: a callback that sends COMMIT or ROLLBACK itself.
  • Not tested with pg-native.

selectExactlyOneChecksForSecondRow ​

ts
selectExactlyOneChecksForSecondRow?: boolean  // default: false, in 7.0 too

With true, a selectExactlyOne() run on its own asks for two rows, LIMIT $2 with 2, and .run() rejects with a NotExactlyOneError when a second row comes: "One result expected but more returned (hint: check .query.compile() on this Error)". Off, it asks for one, LIMIT 1, and gives the first row that matches, as in Zapatos 6.6.1.

  • Nested in another query, in a lateral or a sql template, it keeps LIMIT 1, option or not: a second row there would join the parent row twice. "On its own" is how the query is compiled: .run() and .compile() compile it alone, a parent query compiles it as part of its own SQL.
  • Off, it never warns, and it stays off in 7.0: "at least one row" is how Zapatos reads selectExactlyOne(), nested in a lateral on a NOT NULL foreign key (decision 003).
  • Read when selectExactlyOne() is called, not at .run().

queryListener ​

ts
queryListener?(query: SQLQuery, txnId?: number): void

Called by run() once the query is compiled, before it is sent.

  • query: the SQLQuery run() sends, { text, values }, and name once prepared.
  • txnId: in a transaction, its id; otherwise undefined.
  • It is called for a no-op fragment too, which sends nothing: insert('tags', []) gives /* marked no-op: won't hit DB unless forced -> */ INSERT INTO "tags" SELECT null WHERE false.
  • It is not called when compiling throws. If it throws, run() rejects, and the query is not sent.
ts
db.
setConfig
({
queryListener
: (
query
,
txnId
) =>
console
.
log
(`(${
txnId
?? '-'})`,
query
.
text
,
query
.
values
),
resultListener
: (
result
,
txnId
,
elapsedMs
) =>
console
.
log
(`(${
txnId
?? '-'}, ${
elapsedMs
?.
toFixed
(1)}ms)`,
result
),
transactionListener
: (
message
,
txnId
) =>
console
.
log
(`(${
txnId
})`,
message
),
});

resultListener ​

ts
resultListener?(result: any, txnId?: number, elapsedMs?: number, query?: SQLQuery): void

Called by run() with what it resolves to, once runResultTransform has run.

  • result: { title: 'The Salt Orchard' } for selectOne('films', { id: 1000 }, { columns: ['title'] }), a number for count(), [] for a no-op.
  • txnId: as for queryListener.
  • elapsedMs: from just before the query is sent to just after the transform, from performance.now(), in fractions of a millisecond.
  • query: the query, as queryListener got it.
  • It is not called when the query rejects, nor when the transform throws (a selectExactlyOne() that finds no row).

transactionListener ​

ts
transactionListener?(message: string, txnId?: number): void

Called by transaction() when an attempt fails with 40001 or 40P01, and again when the next attempt starts, with one of these messages and the transaction's id:

WhenMessage
an attempt failed, another is left`Transaction rollback (code ${err.code}) on attempt ${attempt} of ${maxAttempts}, retrying in ${delayBeforeRetry}ms`
the next attempt starts`Retrying transaction, attempt ${attempt} of ${maxAttempts}`
the last attempt failed`Transaction rollback (code ${err.code}) on attempt ${attempt} of ${maxAttempts}, giving up`

With transactionAttemptsMax: 3 and transactionRetryDelay: { minMs: 1, maxMs: 2 }, a callback that fails with 40001 each time gives Transaction rollback (code 40001) on attempt 1 of 3, retrying in 1ms, Retrying transaction, attempt 2 of 3, then the same for attempt 2, and Transaction rollback (code 40001) on attempt 3 of 3, giving up.

It is not called for a transaction that commits on its first attempt, nor for any other error.

Transaction ids ​

The txnId the listeners get.

  • Each transaction() call that opens a transaction takes the next id: 0 for the first in the process, then 1…
  • Every query run() sends on its client gets that id, through every retry: yours, and transaction()'s own START TRANSACTION ISOLATION LEVEL SERIALIZABLE, COMMIT and ROLLBACK, which go through both query listeners like any other.
  • A transaction() given that client opens no transaction, and its queries keep the outer id.
  • Outside a transaction, and on the client once it has ended, the id is undefined.

NewConfig ​

ts
type NewConfig = Partial<Config>

What setConfig() takes: any keys of Config, each one whole.

SQLQuery ​

ts
interface SQLQuery { text: string; values: any[]; name?: string }

A compiled query, as pg's query() takes it: what compile() returns and the listeners get. name is set by prepared().

Opt-ins and warnings ​

A behaviour of Zapatos 6.6.1 that code may rely on, but that pgcast would change, stays the default until 7.0: an option turns the new one on, and the old one warns. Why this way: Versions.

OptionWarningIt warns whenDefault from
upsertRequiresSameKeys: truePGCAST_DEP001a multi-row upsert() without updateColumns has a key missing from some row that is still set from EXCLUDED (Rows with different keys)7.0
undefinedMeansMissing: truePGCAST_DEP002a value of insert(), update() or upsert() is undefined (An undefined value)7.0
transactionRequiresCommit: truePGCAST_DEP003Postgres answers the COMMIT of transaction() with ROLLBACK, after a query error the callback caught (Let the error through)7.0

One opt-in has no warning and no default in 7.0: selectExactlyOneChecksForSecondRow, since 6.6.1's reading of selectExactlyOne(), at least one row, is one Zapatos documents.

The generator has its own: typeEnumsAcrossSchemas and enumOrder, whose old paths are notes through warningListener, once per run, rather than a DeprecationWarning; they will be the defaults in 7.0 too.

Each warning is a Node.js DeprecationWarning, emitted once per process for each code, when the shortcut is called (PGCAST_DEP003: when transaction() resolves), and only when the old behaviour is used. Its message names the option. Node prints it to stderr:

txt
(node:4242) [PGCAST_DEP002] DeprecationWarning: An undefined value in insert(), update() or upsert() is sent as NULL. Set undefinedMeansMissing: true (db.setConfig) to treat it as a missing key instead (DEFAULT in insert(), left out of update() and upsert()); it will be the default in pgcast 7.0.
  • process.on('warning', …) gets it, with name 'DeprecationWarning' and code 'PGCAST_DEP002'.
  • node --no-deprecation hides it, from that hook too; node --no-warnings hides it from stderr only. node --throw-deprecation throws it, uncaught, so the process ends.
  • With the option on, the call never warns.

To pick out pgcast's warnings in that hook, to log them your way: @types/node types the warning Error, which has no code, so say it may have one. Node still prints each warning to stderr; run with node --no-warnings to leave the hook's as the only output.

ts
process
.
on
('warning', (
warning
: Error & {
code
?: string }) => {
if (
warning
.
code
?.
startsWith
('PGCAST_'))
console
.
warn
(
warning
.
code
,
warning
.
message
);
});

Value types ​

pg and JSON give some Postgres values as strings. The generated schema types them with the template string types below, by where the value is (The catalog):

Postgres typeSelectableJSONSelectableInsertable, Updatable, Whereable
int8Int8Stringnumbernumber | Int8String | bigint
numericNumericStringnumbernumber | NumericString
byteaBufferByteArrayStringByteArrayString | Buffer
dateDateDateStringDateString | Date
timestampDateTimestampStringTimestampString | Date
timestamptzDateTimestampTzStringTimestampTzString | Date
timeTimeStringTimeStringTimeString
timetzTimeTzStringTimeTzStringTimeTzString
int4range, int8range, numrangeNumberRangeStringNumberRangeStringNumberRangeString
tsrange, tstzrange, daterangeDateRangeStringDateRangeStringDateRangeString
json, jsonbJSONValueJSONValueJSONValue
  • An array column takes the array of its item's type: int8[] gives Int8String[] in a Selectable.
  • With the generator's customJSONParsingForLargeNumbers, int8 and numeric are number | Int8String and number | NumericString in a JSONSelectable (enableCustomJSONParsingForLargeNumbers()).
  • The shortcuts return JSONSelectables. A Selectable is what pg returns for a plain SELECT of the row.

Each type checks the shape of a string, not its value: '2030-13-45' is a DateString.

ts
const 
opening
: db.
DateString
= '2030-03-01';
const premiere: db.
TimestampTzString
= '2030-03-01T19:30:00';
Type '"2030-03-01T19:30:00"' is not assignable to type '`${number}-${number}-${number}T${number}:${number}Z` | `${number}-${number}-${number}T${number}:${number}+${number}` | `${number}-${number}-${number}T${number}:${number}+${number}:${number}` | `${number}-${number}-${number}T${number}:${number}-${number}` | `${number}-${number}-${number}T${number}:${number}-${number}...'.

JSON ​

JSONValue ​

ts
type JSONValue = null | boolean | number | string | JSONObject | JSONArray

A value JSON can hold: the type of a json or jsonb column. undefined, a Date and a function are not one.

JSONObject ​

ts
type JSONObject = { [k: string]: JSONValue }

JSONArray ​

ts
type JSONArray = JSONValue[]

Large numbers ​

Int8String ​

ts
type Int8String = `${number}`

An int8 as pg returns it, a string: '9007199254740993', which no number holds.

NumericString ​

ts
type NumericString = `${number}`

A numeric as pg returns it, a string that keeps its digits: '1.10'.

enableCustomJSONParsingForLargeNumbers() ​

ts
db.enableCustomJSONParsingForLargeNumbers(pg: typeof import('pg')): void

Makes pg parse json and jsonb values with json-custom-numbers, which gives a number as a string when a number would change it. Each shortcut except the aggregates returns jsonb, so it applies to their results.

ts
db.
enableCustomJSONParsingForLargeNumbers
(pg);
A number in JSONJSON.parse(), the defaultOnce enabled
12, 0.112, 0.112, 0.1
90071992547409939007199254740992'9007199254740993'
9007199254740992, an integer past Number.MAX_SAFE_INTEGER9007199254740992'9007199254740992'
1.000000000000000000011'1.00000000000000000001'
a numeric past Number.MAX_VALUEInfinityits digits, as a string
  • It does nothing for count(), sum(), avg(), min() and max(): their result is not JSON, and goes through Number() (count(), #163). A sum() of 9007199254740993 still gives 9007199254740992.
  • It sets the type parsers of the pg module you give, so every pool and client of that module, from then on, pgcast's queries or not.
  • It does not change the types: the generator's customJSONParsingForLargeNumbers does.
  • An int8 or numeric column outside JSON is a string already, by pg's own parser.

Ranges ​

RangeString ​

ts
type RangeString<Bound extends string | number> = `${'[' | '('}${Bound},${Bound}${']' | ')'}`

A range as Postgres writes it: a bracket, two bounds, a bracket. It does not cover an empty range, which Postgres writes empty: to_jsonb('empty'::int4range) is "empty", and const r: db.NumberRangeString = 'empty' does not compile (TS2322, #185).

NumberRangeString ​

ts
type NumberRangeString = RangeString<number | ''>

An int4range, int8range or numrange: '[1,5)', or '[1.5,)' with no upper bound.

DateRangeString ​

ts
type DateRangeString = RangeString<string>

A tsrange, tstzrange or daterange. Its bounds follow the session's DateStyle, so they are typed string: daterange('2030-03-01', '2030-03-08') is '[2030-03-01,2030-03-08)' with the ISO style, and '[01/03/2030,08/03/2030)' with SQL, DMY. The bounds of a tsrange or tstzrange come quoted, since they hold a space: '["2030-03-01 19:30:00","2030-03-02 00:00:00")'.

Bytes ​

ByteArrayString ​

ts
type ByteArrayString = `\\x${string}`

A bytea in JSON, as hex after \x: '\\x4c61' in TypeScript source.

toBuffer() ​

ts
db.toBuffer<T extends ByteArrayString | null>(d: T): T extends ByteArrayString ? Buffer<ArrayBuffer> | Exclude<T, ByteArrayString> : T

Decodes the hex of a ByteArrayString into a Buffer, typed Buffer<ArrayBuffer>. null gives null, and the type keeps it; undefined throws, and the type refuses it with strictNullChecks on (strict()). It decodes the whole value in memory.

ts
const 
bytes
= db.
toBuffer
('\\x4c61726b73707572');
const
word
=
bytes
.
toString
(); // 'Larkspur'

Dates and times ​

A date or time in a JSONSelectable is the string to_jsonb() writes, ISO 8601. A timestamptz carries the session's TimeZone offset: '2030-03-01T19:30:00+00:00' in UTC, '2030-03-02T01:00:00+05:30' in Asia/Kolkata.

TSYou write Open in playground

ts
const 
screening
= await db.
selectExactlyOne
(
'screenings', {
id
: 1000 }, {
columns
: ['starts_at'] },
).
run
(
pool
);
const
startsAt
= db.
toDate
(
screening
.
starts_at
);

SQLPostgres runs

sql
SELECT jsonb_build_object($1::text, "starts_at") AS result
FROM "screenings"
WHERE ("id" = $2)
LIMIT $3

values ["starts_at", 1000, 1]

::TypeScript knows

ts
const screening: db.JSONOnlyColsForTable<"screenings", "starts_at"[]>
// that is
const screening: {
  starts_at: db.TimestampTzString;
}
TypeIsFor example
DateString${number}-${number}-${number}'2030-03-01'
TimeString${number}:${number}, then an optional :${number}'19:30', '19:30:00'
TzSuffix'Z', or + or - and a number, then an optional :${number}'Z', '+01', '+05:30'
TimeTzString${TimeString}${TzSuffix}'19:30:00+01'
TimestampString${DateString}T${TimeString}'2030-03-01T19:30:00'
TimestampTzString${TimestampString}${TzSuffix}'2030-03-01T19:30:00+00:00'

DateString ​

ts
type DateString = `${number}-${number}-${number}`

A date.

TimeString ​

ts
type TimeString = `${number}:${number}${'' | `:${number}`}`

A time, with or without its seconds.

TzSuffix ​

ts
type TzSuffix = 'Z' | `${'+' | '-'}${number}${'' | `:${number}`}`

The time zone at the end of a TimeTzString or a TimestampTzString.

TimeTzString ​

ts
type TimeTzString = `${TimeString}${TzSuffix}`

A timetz.

TimestampString ​

ts
type TimestampString = `${DateString}T${TimeString}`

A timestamp: no time zone.

TimestampTzString ​

ts
type TimestampTzString = `${TimestampString}${TzSuffix}`

A timestamptz.

toDate() ​

ts
db.toDate<D extends null | TimestampTzString>(d: D, tzInterpretation?: undefined): D extends null ? null : Date
db.toDate<D extends null | TimestampString | DateString>(d: D, tzInterpretation: 'UTC' | 'local'): D extends null ? null : Date

Makes a Date of a string from Postgres. A TimestampTzString has its time zone. A TimestampString or a DateString has none, so you say which to read it in: 'UTC', or 'local', the time zone of the JavaScript process.

ts
const 
curtain
= db.
toDate
('2030-03-01T19:30:00+00:00');
const
rehearsal
= db.
toDate
('2030-03-01T15:00:00', 'UTC');
const
opening
= db.
toDate
('2030-03-01', 'local');
  • toDate('2030-03-01', 'UTC') is midnight UTC; 'local' is local midnight.
  • null gives null; the type keeps it, so TimestampTzString | null gives Date | null.
  • A Date keeps milliseconds: the microseconds of a Postgres value are dropped.
  • It does not check the string: one a Date cannot read gives an Invalid Date.

A TimestampString or a DateString without tzInterpretation does not compile (TS2345). Nor does a TimestampTzString with one, since the first overload takes tzInterpretation only as undefined (TS2769):

ts
db.
toDate
('2030-03-01T15:00:00');
Argument of type '"2030-03-01T15:00:00"' is not assignable to parameter of type '`${number}-${number}-${number}T${number}:${number}Z` | `${number}-${number}-${number}T${number}:${number}+${number}` | `${number}-${number}-${number}T${number}:${number}+${number}:${number}` | `${number}-${number}-${number}T${number}:${number}-${number}` | `${number}-${number}-${number}T${number}:${number}-${number}...'.
db.toDate('2030-03-01T19:30:00+00:00', 'UTC');
No overload matches this call. Overload 1 of 2, '(d: "2030-03-01T19:30:00+00:00", tzInterpretation?: undefined): Date', gave the following error. Argument of type '"UTC"' is not assignable to parameter of type 'undefined'. Overload 2 of 2, '(d: `${number}-${number}-${number}` | `${number}-${number}-${number}T${number}:${number}` | `${number}-${number}-${number}T${number}:${number}:${number}` | null, tzInterpretation: TzLocalOrUTC): Date | null', gave the following error. Argument of type '"2030-03-01T19:30:00+00:00"' is not assignable to parameter of type '`${number}-${number}-${number}` | `${number}-${number}-${number}T${number}:${number}` | `${number}-${number}-${number}T${number}:${number}:${number}` | null'.

toString() ​

ts
db.toString<D extends Date | null, T extends 'timestamptz' | 'timestamp:UTC' | 'timestamp:local' | 'date:UTC' | 'date:local'>(
  d: D,
  stringTypeTz: T,
): D extends null ? null : { timestamptz: TimestampTzString; 'timestamp:UTC': TimestampString; … }[T]

Makes a string Postgres reads of a Date, typed by stringTypeTz; null gives null.

ts
const 
curtain
= new
Date
('2030-03-01T19:30:00Z');
const
day
: db.
DateString
= db.
toString
(
curtain
, 'date:UTC'); // '2030-03-01'
const
stamp
: db.
TimestampTzString
= db.
toString
(
curtain
, 'timestamptz'); // '2030-03-01T19:30:00.000Z'
stringTypeTzGivesFor 19:30 UTC on 1 March 2030, in a process with TZ=Asia/Kolkata
'timestamptz'TimestampTzString, from toISOString()'2030-03-01T19:30:00.000Z'
'timestamp:UTC'TimestampString'2030-03-01T19:30:00.000'
'timestamp:local'TimestampString, in the process's time zone'2030-03-02T01:00:00.000'
'date:UTC'DateString'2030-03-01'
'date:local'DateString, in the process's time zone'2030-03-02'

strict() ​

ts
db.strict<FnIn, FnOut>(fn: (x: FnIn) => FnOut):
  <T extends FnIn | null>(d: T) => T extends FnIn ? Exclude<T, FnIn> | FnOut : T

Wraps a one-argument function so that null gives null without calling it, as a Postgres function declared STRICT does. The type follows: FnIn gives FnOut, FnIn | null gives FnOut | null, null gives null. toBuffer() is strict() of a ByteArrayString decoder.

ts
const 
shout
= db.
strict
((
s
: string) =>
s
.
toUpperCase
());
const
loud
=
shout
(
tagline
);
  • Only null passes through, as only NULL does for a STRICT function in Postgres: undefined reaches fn. A function that takes it handles it: db.strict((title: string | undefined) => title ?? 'untitled')(undefined) gives 'untitled'.
  • With strictNullChecks on, the type refuses undefined unless fn takes it: shout(undefined) and db.toBuffer(undefined) do not compile (TS2345).
  • With strictNullChecks off, a type has no null nor undefined in it, and none of these types say anything of them: toDate(null) is typed Date, shout(null) any. db.toBuffer(undefined) then compiles, and throws "TypeError: Cannot read properties of undefined (reading 'slice')".

SchemaVersionCanary ​

ts
interface SchemaVersionCanary { version: 104 }

The version of the generated schema's format that this zapatos/db reads. The generated zapatos/schema declares interface schemaVersionCanary extends db.SchemaVersionCanary { version: 104 }, so a schema written for another format does not compile:

txt
error TS2430: Interface 'schemaVersionCanary' incorrectly extends interface 'SchemaVersionCanary'.
  Types of property 'version' are incompatible.
  • pgcast's version is Zapatos 6.6.1's, 104, so the canary of a schema that Zapatos 6.6.1 generated passes.
  • Regenerating the schema clears the error (Generator).