turbine-orm 0.54.0 → 0.55.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/README.md +3 -3
- package/dist/cjs/client.d.ts +85 -15
- package/dist/cjs/client.js +22 -2
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/query/aggregates.js +38 -4
- package/dist/cjs/query/builder.d.ts +56 -1
- package/dist/cjs/query/builder.js +145 -23
- package/dist/cjs/query/deferred.d.ts +35 -0
- package/dist/cjs/query/index.d.ts +1 -1
- package/dist/cjs/query/utils.d.ts +83 -0
- package/dist/cjs/query/utils.js +174 -4
- package/dist/cjs/query/warn-registry.d.ts +14 -0
- package/dist/cjs/query/warn-registry.js +14 -0
- package/dist/cjs/query/where.d.ts +8 -0
- package/dist/client.d.ts +85 -15
- package/dist/client.js +23 -3
- package/dist/index.d.ts +1 -1
- package/dist/query/aggregates.js +39 -5
- package/dist/query/builder.d.ts +56 -1
- package/dist/query/builder.js +146 -24
- package/dist/query/deferred.d.ts +35 -0
- package/dist/query/index.d.ts +1 -1
- package/dist/query/utils.d.ts +83 -0
- package/dist/query/utils.js +170 -4
- package/dist/query/warn-registry.d.ts +14 -0
- package/dist/query/warn-registry.js +14 -0
- package/dist/query/where.d.ts +8 -0
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -27,7 +27,7 @@ import { setErrorMessageMode, TimeoutError, UnsupportedFeatureError, ValidationE
|
|
|
27
27
|
import { ObserveEngine } from './observe.js';
|
|
28
28
|
import { executePipeline, pipelineSupported } from './pipeline.js';
|
|
29
29
|
import { QueryInterface, } from './query/index.js';
|
|
30
|
-
import { closestName, quoteIdent, registerUtcTemporalParsers } from './query/utils.js';
|
|
30
|
+
import { closestName, markTurbineParser, quoteIdent, registerUtcTemporalParsers, warnParserOverwrite, } from './query/utils.js';
|
|
31
31
|
import { shouldWarnOnce, WARN_NS } from './query/warn-registry.js';
|
|
32
32
|
import { createSubscription, validateChannel, } from './realtime.js';
|
|
33
33
|
import { buildTypedSql, TypedSqlQuery } from './typed-sql.js';
|
|
@@ -102,6 +102,7 @@ const TURBINE_CONFIG_KEYS = {
|
|
|
102
102
|
defaultLimit: true,
|
|
103
103
|
warnOnUnlimited: true,
|
|
104
104
|
utcTimestamps: true,
|
|
105
|
+
temporalInfinity: true,
|
|
105
106
|
planCacheMode: true,
|
|
106
107
|
scopedConnect: true,
|
|
107
108
|
relationLoadStrategy: true,
|
|
@@ -591,10 +592,11 @@ export class TurbineClient {
|
|
|
591
592
|
// constructor-gated by the static flags, so it happens at most once.
|
|
592
593
|
const ownsAnyPool = !config.pool;
|
|
593
594
|
if (ownsAnyPool && !TurbineClient.int8ParserRegistered) {
|
|
594
|
-
|
|
595
|
+
warnParserOverwrite(20, 'int8');
|
|
596
|
+
pg.types.setTypeParser(20, markTurbineParser((val) => {
|
|
595
597
|
const n = Number(val);
|
|
596
598
|
return Number.isSafeInteger(n) ? n : val;
|
|
597
|
-
});
|
|
599
|
+
}));
|
|
598
600
|
TurbineClient.int8ParserRegistered = true;
|
|
599
601
|
}
|
|
600
602
|
// Parse the zone-less temporal types (`timestamp` OID 1114, `date` OID
|
|
@@ -638,6 +640,7 @@ export class TurbineClient {
|
|
|
638
640
|
defaultLimit: config.defaultLimit,
|
|
639
641
|
warnOnUnlimited: config.warnOnUnlimited,
|
|
640
642
|
utcTimestamps: config.utcTimestamps,
|
|
643
|
+
temporalInfinity: TurbineClient.resolveTemporalInfinity(config.temporalInfinity),
|
|
641
644
|
scopedConnect: config.scopedConnect,
|
|
642
645
|
relationLoadStrategy: config.relationLoadStrategy,
|
|
643
646
|
stableRelationOrder: config.stableRelationOrder,
|
|
@@ -809,6 +812,23 @@ export class TurbineClient {
|
|
|
809
812
|
* `UnsupportedFeatureError` (E017), in the same style as the other
|
|
810
813
|
* capability refusals.
|
|
811
814
|
*/
|
|
815
|
+
/**
|
|
816
|
+
* Validate the `temporalInfinity` reading. A closed two-value enum, checked
|
|
817
|
+
* at construction so a typo (`'preserved'`, `'raw'`) fails loudly rather than
|
|
818
|
+
* silently falling back to the default reading the caller was trying to
|
|
819
|
+
* change.
|
|
820
|
+
*/
|
|
821
|
+
static resolveTemporalInfinity(value) {
|
|
822
|
+
if (value === undefined)
|
|
823
|
+
return undefined;
|
|
824
|
+
if (value !== 'null' && value !== 'preserve') {
|
|
825
|
+
throw new ValidationError(`Invalid temporalInfinity: ${JSON.stringify(value)}. Expected 'preserve' (default: read a Postgres ` +
|
|
826
|
+
'temporal `infinity` as the JS number `Infinity` / `-Infinity`, which round-trips through a write ' +
|
|
827
|
+
"but breaks the declared `Date` type) or 'null' (read it as null, which serializes cleanly but " +
|
|
828
|
+
'makes it indistinguishable from a stored NULL, so a read-modify-write destroys the value).');
|
|
829
|
+
}
|
|
830
|
+
return value;
|
|
831
|
+
}
|
|
812
832
|
static resolvePlanCacheMode(mode, dialect) {
|
|
813
833
|
if (mode === undefined || mode === null)
|
|
814
834
|
return undefined;
|
package/dist/index.d.ts
CHANGED
|
@@ -43,7 +43,7 @@ export { type IntrospectOptions, introspect } from './introspect.js';
|
|
|
43
43
|
export { executeNestedCreate, executeNestedUpdate, hasRelationFields, type NestedWriteContext, } from './nested-write.js';
|
|
44
44
|
export { HttpJsonSink, type HttpJsonSinkOptions, type MetricsFlushBatch, type MetricsFlushRow, type ObserveConfig, type ObserveHandle, type ObserveSink, PgMetricsSink, type PgMetricsSinkOptions, } from './observe.js';
|
|
45
45
|
export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
|
|
46
|
-
export { type AggregateArgs, type AggregateResult, type ArrayFilter, AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, type ColumnRef, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GlobalFilters, type GroupByAggregateSpec, type GroupByArgs, type GroupByDistinctOn, type GroupByResult, type HavingClause, type JsonFilter, type JsonPathAggregateTarget, type JsonPathGroupKey, type JsonPathOrderBy, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderByObject, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type RelationLoadStrategy, type RelationPickBy, type RelationPickOrderBy, type SelectResult, type SkipGlobalFilters, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithOrderByObject, type WithResult, } from './query/index.js';
|
|
46
|
+
export { type AggregateArgs, type AggregateResult, type ArrayFilter, AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, type ColumnRef, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GlobalFilters, type GroupByAggregateSpec, type GroupByArgs, type GroupByDistinctOn, type GroupByResult, type HavingClause, type JsonFilter, type JsonPathAggregateTarget, type JsonPathGroupKey, type JsonPathOrderBy, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderByObject, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type RelationLoadStrategy, type RelationPickBy, type RelationPickOrderBy, type SelectResult, type SkipGlobalFilters, type TemporalInfinityReading, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithOrderByObject, type WithResult, } from './query/index.js';
|
|
47
47
|
export { type ActiveSubscription, type NotificationHandler, type Subscription, validateChannel } from './realtime.js';
|
|
48
48
|
export type { CheckMetadata, ColumnMetadata, IndexMetadata, PrismaCompatMap, PrismaModelMap, PrismaRelationMap, ReferentialAction, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
|
|
49
49
|
export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, withDbFieldNames, } from './schema.js';
|
package/dist/query/aggregates.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { UnsupportedFeatureError, ValidationError } from '../errors.js';
|
|
12
12
|
import { snakeToCamel } from '../schema.js';
|
|
13
13
|
import { isJsonPathOrderBy, isUnmatchedPlainObject, isVectorOrderBy, isWhereOperator, normalizeOrderBy, orderByEntries, } from './filters.js';
|
|
14
|
-
import { ownLookup } from './utils.js';
|
|
14
|
+
import { isTemporalInfinity, ownLookup } from './utils.js';
|
|
15
15
|
import * as whereMod from './where.js';
|
|
16
16
|
/**
|
|
17
17
|
* Enforce the PII contract on the aggregate surface. A PII-tagged
|
|
@@ -310,12 +310,14 @@ export function buildGroupBy(qi, args) {
|
|
|
310
310
|
}
|
|
311
311
|
else if (rawKey.startsWith('_min_')) {
|
|
312
312
|
const j = jsonAgg(rawKey);
|
|
313
|
-
minObj[fieldFor(rawKey, rawKey.slice(5))] =
|
|
313
|
+
minObj[fieldFor(rawKey, rawKey.slice(5))] =
|
|
314
|
+
j?.numeric && rawValue !== null ? Number(rawValue) : temporalAggValue(qi, rawKey.slice(5), rawValue);
|
|
314
315
|
hasMins = true;
|
|
315
316
|
}
|
|
316
317
|
else if (rawKey.startsWith('_max_')) {
|
|
317
318
|
const j = jsonAgg(rawKey);
|
|
318
|
-
maxObj[fieldFor(rawKey, rawKey.slice(5))] =
|
|
319
|
+
maxObj[fieldFor(rawKey, rawKey.slice(5))] =
|
|
320
|
+
j?.numeric && rawValue !== null ? Number(rawValue) : temporalAggValue(qi, rawKey.slice(5), rawValue);
|
|
319
321
|
hasMaxs = true;
|
|
320
322
|
}
|
|
321
323
|
}
|
|
@@ -725,6 +727,38 @@ export function buildHavingNumericClauses(qi, expr, filter, params) {
|
|
|
725
727
|
}
|
|
726
728
|
return clauses;
|
|
727
729
|
}
|
|
730
|
+
/**
|
|
731
|
+
* `_min` / `_max` over a TEMPORAL column, aligned with the row parser.
|
|
732
|
+
*
|
|
733
|
+
* These two are the only aggregates that hand back a row's stored cell rather
|
|
734
|
+
* than something computed across rows, so they are the only ones that can carry
|
|
735
|
+
* a Postgres `infinity`. They are assembled from the RAW row (a Date must stay
|
|
736
|
+
* a Date, and `parseRow`'s snake→camel mapping would collide with the `_min_`
|
|
737
|
+
* alias), so the infinity mapping has to be applied here too. Without it
|
|
738
|
+
* `aggregate({ _max: { ts: true } })` would return the driver's `Infinity`
|
|
739
|
+
* while `findMany` returned `null` for the very same value.
|
|
740
|
+
*
|
|
741
|
+
* Gated on the column being temporal, so a numeric `_max` is untouched, and on
|
|
742
|
+
* the client's `temporalInfinity` reading, so it cannot disagree with the rows
|
|
743
|
+
* `findMany` returns for the same column. An absent reading resolves to the
|
|
744
|
+
* default, `'preserve'`, exactly as it does in the row parser.
|
|
745
|
+
*
|
|
746
|
+
* Note what the opt-in `'null'` reading then means for `_max` on a table that
|
|
747
|
+
* plainly has rows: `null` is the same value an empty table and an all-NULL
|
|
748
|
+
* column return, so `_min` can report a real date while `_max` reports
|
|
749
|
+
* "nothing" (documented, and one of the reasons `'null'` is not the default).
|
|
750
|
+
*/
|
|
751
|
+
function temporalAggValue(qi, col, value) {
|
|
752
|
+
if (!qi.tableMeta.dateColumns.has(col))
|
|
753
|
+
return value;
|
|
754
|
+
if (!isTemporalInfinity(value))
|
|
755
|
+
return value;
|
|
756
|
+
if (qi.temporalInfinity === 'null')
|
|
757
|
+
return null;
|
|
758
|
+
if (typeof value === 'number')
|
|
759
|
+
return value;
|
|
760
|
+
return value === '-infinity' ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
|
|
761
|
+
}
|
|
728
762
|
export function buildAggregate(qi, args) {
|
|
729
763
|
qi.currentSkip = args.skipGlobalFilters;
|
|
730
764
|
const aggWhere = whereMod.mergeGlobalFilter(qi, args.where);
|
|
@@ -869,13 +903,13 @@ export function buildAggregate(qi, args) {
|
|
|
869
903
|
else if (key.startsWith('_min_')) {
|
|
870
904
|
const col = key.slice(5);
|
|
871
905
|
const field = qi.tableMeta.reverseColumnMap[col] ?? snakeToCamel(col);
|
|
872
|
-
minObj[field] = val;
|
|
906
|
+
minObj[field] = temporalAggValue(qi, col, val);
|
|
873
907
|
hasMins = true;
|
|
874
908
|
}
|
|
875
909
|
else if (key.startsWith('_max_')) {
|
|
876
910
|
const col = key.slice(5);
|
|
877
911
|
const field = qi.tableMeta.reverseColumnMap[col] ?? snakeToCamel(col);
|
|
878
|
-
maxObj[field] = val;
|
|
912
|
+
maxObj[field] = temporalAggValue(qi, col, val);
|
|
879
913
|
hasMaxs = true;
|
|
880
914
|
}
|
|
881
915
|
}
|
package/dist/query/builder.d.ts
CHANGED
|
@@ -154,7 +154,7 @@ export declare const AUTO_TO_ONE_JOIN_ROWS_MAX = 100000;
|
|
|
154
154
|
* it.
|
|
155
155
|
*/
|
|
156
156
|
export declare const AUTO_COUNT_BATCH_MIN_PARENT_ROWS = 2;
|
|
157
|
-
export type { DeferredQuery, MiddlewareFn, QueryEvent, QueryEventListener, QueryInterfaceOptions, ReselectExecutor, } from './deferred.js';
|
|
157
|
+
export type { DeferredQuery, MiddlewareFn, QueryEvent, QueryEventListener, QueryInterfaceOptions, ReselectExecutor, TemporalInfinityReading, } from './deferred.js';
|
|
158
158
|
import type { DeferredQuery, MiddlewareFn, QueryInterfaceOptions } from './deferred.js';
|
|
159
159
|
export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
160
160
|
private readonly pool;
|
|
@@ -181,6 +181,14 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
181
181
|
private readonly warnOnUnlimited;
|
|
182
182
|
private readonly scopedConnect;
|
|
183
183
|
private readonly utcTimestamps;
|
|
184
|
+
/**
|
|
185
|
+
* How a Postgres temporal `infinity` / `-infinity` is handed back:
|
|
186
|
+
* `'preserve'` (default) or `'null'`. See {@link TemporalInfinityReading}
|
|
187
|
+
* for the trade, and {@link parseRow} for where it is applied.
|
|
188
|
+
*/
|
|
189
|
+
private readonly temporalInfinity;
|
|
190
|
+
/** Whether `temporalInfinity` was left unset, i.e. the warning still applies. */
|
|
191
|
+
private readonly warnTemporalInfinityUnset;
|
|
184
192
|
private readonly preparedStatementsEnabled;
|
|
185
193
|
/**
|
|
186
194
|
* Whether the SQL template cache is active. Set once in the constructor.
|
|
@@ -1010,5 +1018,52 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
1010
1018
|
* memoized per table. Used so nested relation rows (camelCase keys) coerce
|
|
1011
1019
|
* dates the same way top-level rows do.
|
|
1012
1020
|
*/
|
|
1021
|
+
/**
|
|
1022
|
+
* Say ONCE per `table.field` that a stored temporal `infinity` was actually
|
|
1023
|
+
* read, and describe the reading the caller is getting. JavaScript has no
|
|
1024
|
+
* `Date` for either infinity, so BOTH readings cost something and a caller
|
|
1025
|
+
* whose rows carry the value needs to know which cost they are paying.
|
|
1026
|
+
*
|
|
1027
|
+
* Under the default `'preserve'` the value comes back as the JS number
|
|
1028
|
+
* `Infinity` / `-Infinity` on a field the generated types declare as `Date`,
|
|
1029
|
+
* so `.toISOString()` / `.getTime()` throw a TypeError on exactly those rows
|
|
1030
|
+
* and `JSON.stringify` still renders them `null` (JSON has no infinity
|
|
1031
|
+
* literal). That is the price of the reading being LOSSLESS: the number binds
|
|
1032
|
+
* straight back, so a read-modify-write stores `infinity` again. The
|
|
1033
|
+
* alternative, `'null'`, is silently destructive, which is why it is not the
|
|
1034
|
+
* default and why the warning names it as a deliberate choice rather than a
|
|
1035
|
+
* recommendation.
|
|
1036
|
+
*
|
|
1037
|
+
* ONLY WHEN THE OPTION WAS LEFT UNSET. Naming a reading in the config, either
|
|
1038
|
+
* one, is the acknowledgement, and the warning exists to surface an
|
|
1039
|
+
* unacknowledged trade rather than to nag.
|
|
1040
|
+
*
|
|
1041
|
+
* NOT DEV-ONLY, unlike every other warning in this codebase, and deliberately
|
|
1042
|
+
* so. Production is exactly where a destructive write commits and where the
|
|
1043
|
+
* row cannot be recovered afterwards, so a warning that goes quiet under
|
|
1044
|
+
* `NODE_ENV=production` is silent in the only place it matters. The cost is
|
|
1045
|
+
* bounded to the point of irrelevance: once per process per field, and only
|
|
1046
|
+
* on a row that actually held an infinity.
|
|
1047
|
+
*
|
|
1048
|
+
* KEYED ON THE FIELD, not the row key: top-level rows arrive snake_case and
|
|
1049
|
+
* nested `json_build_object` rows arrive camelCase, so keying on the raw key
|
|
1050
|
+
* warned twice for one column.
|
|
1051
|
+
*/
|
|
1052
|
+
private warnTemporalInfinity;
|
|
1053
|
+
/**
|
|
1054
|
+
* Apply the configured reading to the infinity elements of a temporal ARRAY
|
|
1055
|
+
* value, returning the original array by identity when there are none (the
|
|
1056
|
+
* overwhelmingly common case, so no per-row allocation on ordinary data).
|
|
1057
|
+
*/
|
|
1058
|
+
private mapArrayTemporalInfinity;
|
|
1059
|
+
/**
|
|
1060
|
+
* The configured reading of one infinity value: the JS number (`'preserve'`,
|
|
1061
|
+
* the default) or `null`. Under `'preserve'` the JSON-wire STRING form
|
|
1062
|
+
* (`"infinity"`, what the join and positional strategies see) is normalized
|
|
1063
|
+
* to the number too, so the reading is identical on every strategy; 0.54
|
|
1064
|
+
* shipped the number on some paths and an Invalid Date on others, which is
|
|
1065
|
+
* the bug that made a single reading necessary in the first place.
|
|
1066
|
+
*/
|
|
1067
|
+
private readTemporalInfinity;
|
|
1013
1068
|
private parseRow;
|
|
1014
1069
|
}
|
package/dist/query/builder.js
CHANGED
|
@@ -20,7 +20,7 @@ import { defaultProjectionFields, includeKeysForBatching, loadRelationsBatched,
|
|
|
20
20
|
import { expandCompoundUniqueWhere } from './compound-unique.js';
|
|
21
21
|
import { isJsonPathOrderBy, isOrderBySpec, isRelationPickOrderBy, isVectorOrderBy, isWhereOperator, orderByEntries, sortedEntries, } from './filters.js';
|
|
22
22
|
import * as relationsMod from './relations.js';
|
|
23
|
-
import { LRUCache, ownLookup, parseDbDate, resolveColumnName, sqlToPreparedName, unknownFieldMessage, } from './utils.js';
|
|
23
|
+
import { isTemporalInfinity, LRUCache, ownLookup, parseDbDate, resolveColumnName, sqlToPreparedName, unknownFieldMessage, } from './utils.js';
|
|
24
24
|
import { shouldWarnOnce, WARN_NS } from './warn-registry.js';
|
|
25
25
|
import * as whereMod from './where.js';
|
|
26
26
|
import * as writesMod from './writes.js';
|
|
@@ -341,6 +341,14 @@ export class QueryInterface {
|
|
|
341
341
|
warnOnUnlimited;
|
|
342
342
|
scopedConnect;
|
|
343
343
|
utcTimestamps;
|
|
344
|
+
/**
|
|
345
|
+
* How a Postgres temporal `infinity` / `-infinity` is handed back:
|
|
346
|
+
* `'preserve'` (default) or `'null'`. See {@link TemporalInfinityReading}
|
|
347
|
+
* for the trade, and {@link parseRow} for where it is applied.
|
|
348
|
+
*/
|
|
349
|
+
temporalInfinity;
|
|
350
|
+
/** Whether `temporalInfinity` was left unset, i.e. the warning still applies. */
|
|
351
|
+
warnTemporalInfinityUnset;
|
|
344
352
|
preparedStatementsEnabled;
|
|
345
353
|
/**
|
|
346
354
|
* Whether the SQL template cache is active. Set once in the constructor.
|
|
@@ -487,6 +495,17 @@ export class QueryInterface {
|
|
|
487
495
|
: warnOpt !== false;
|
|
488
496
|
this.scopedConnect = options?.scopedConnect === true;
|
|
489
497
|
this.utcTimestamps = options?.utcTimestamps !== false;
|
|
498
|
+
// Unset resolves to `'preserve'`. `'null'` destroys data: it makes a stored
|
|
499
|
+
// infinity indistinguishable from a stored NULL, so the ordinary
|
|
500
|
+
// read-modify-write (`update({ data: { ...row } })`) stores SQL NULL over a
|
|
501
|
+
// nullable temporal column and the infinity is gone with no error, measured
|
|
502
|
+
// on a live server. A lossy default is the wrong price for a nicer
|
|
503
|
+
// `JSON.stringify`, so the lossless reading is the default and `'null'`
|
|
504
|
+
// stays available as an explicit opt-in.
|
|
505
|
+
this.temporalInfinity = options?.temporalInfinity === 'null' ? 'null' : 'preserve';
|
|
506
|
+
// The warning exists to surface an UNACKNOWLEDGED trade. Naming either
|
|
507
|
+
// reading in the config is the acknowledgement, so it stops.
|
|
508
|
+
this.warnTemporalInfinityUnset = options?.temporalInfinity === undefined;
|
|
490
509
|
this.preparedStatementsEnabled = options?.preparedStatements ?? true;
|
|
491
510
|
// SQL template cache capacity. `sqlCacheSize: 0` disables caching entirely
|
|
492
511
|
// (mirrors `sqlCache: false`); any positive integer sets the LRU bound;
|
|
@@ -583,6 +602,9 @@ export class QueryInterface {
|
|
|
583
602
|
// building a client whose writes and reads disagree (see
|
|
584
603
|
// `assertUtcTimestampsAgree` in client.ts).
|
|
585
604
|
utcTimestamps: this.utcTimestamps,
|
|
605
|
+
// Reaches aggregates.ts, where `_min` / `_max` are assembled from the raw
|
|
606
|
+
// row and so need the same infinity reading as `parseRow`.
|
|
607
|
+
temporalInfinity: this.temporalInfinity,
|
|
586
608
|
crossSchemaTypeColumns: this.crossSchemaTypeColumns,
|
|
587
609
|
get currentSkip() {
|
|
588
610
|
return self.currentSkip;
|
|
@@ -3019,6 +3041,87 @@ export class QueryInterface {
|
|
|
3019
3041
|
* memoized per table. Used so nested relation rows (camelCase keys) coerce
|
|
3020
3042
|
* dates the same way top-level rows do.
|
|
3021
3043
|
*/
|
|
3044
|
+
/**
|
|
3045
|
+
* Say ONCE per `table.field` that a stored temporal `infinity` was actually
|
|
3046
|
+
* read, and describe the reading the caller is getting. JavaScript has no
|
|
3047
|
+
* `Date` for either infinity, so BOTH readings cost something and a caller
|
|
3048
|
+
* whose rows carry the value needs to know which cost they are paying.
|
|
3049
|
+
*
|
|
3050
|
+
* Under the default `'preserve'` the value comes back as the JS number
|
|
3051
|
+
* `Infinity` / `-Infinity` on a field the generated types declare as `Date`,
|
|
3052
|
+
* so `.toISOString()` / `.getTime()` throw a TypeError on exactly those rows
|
|
3053
|
+
* and `JSON.stringify` still renders them `null` (JSON has no infinity
|
|
3054
|
+
* literal). That is the price of the reading being LOSSLESS: the number binds
|
|
3055
|
+
* straight back, so a read-modify-write stores `infinity` again. The
|
|
3056
|
+
* alternative, `'null'`, is silently destructive, which is why it is not the
|
|
3057
|
+
* default and why the warning names it as a deliberate choice rather than a
|
|
3058
|
+
* recommendation.
|
|
3059
|
+
*
|
|
3060
|
+
* ONLY WHEN THE OPTION WAS LEFT UNSET. Naming a reading in the config, either
|
|
3061
|
+
* one, is the acknowledgement, and the warning exists to surface an
|
|
3062
|
+
* unacknowledged trade rather than to nag.
|
|
3063
|
+
*
|
|
3064
|
+
* NOT DEV-ONLY, unlike every other warning in this codebase, and deliberately
|
|
3065
|
+
* so. Production is exactly where a destructive write commits and where the
|
|
3066
|
+
* row cannot be recovered afterwards, so a warning that goes quiet under
|
|
3067
|
+
* `NODE_ENV=production` is silent in the only place it matters. The cost is
|
|
3068
|
+
* bounded to the point of irrelevance: once per process per field, and only
|
|
3069
|
+
* on a row that actually held an infinity.
|
|
3070
|
+
*
|
|
3071
|
+
* KEYED ON THE FIELD, not the row key: top-level rows arrive snake_case and
|
|
3072
|
+
* nested `json_build_object` rows arrive camelCase, so keying on the raw key
|
|
3073
|
+
* warned twice for one column.
|
|
3074
|
+
*/
|
|
3075
|
+
warnTemporalInfinity(table, field) {
|
|
3076
|
+
if (!this.warnTemporalInfinityUnset)
|
|
3077
|
+
return;
|
|
3078
|
+
if (!shouldWarnOnce(WARN_NS.temporalInfinity, `${table}.${field}`))
|
|
3079
|
+
return;
|
|
3080
|
+
console.warn(`[turbine] ${table}.${field} holds the Postgres value \`infinity\` (or \`-infinity\`). JavaScript has ` +
|
|
3081
|
+
'no Date for it, so it reads as the JS number `Infinity` / `-Infinity` on a field the generated ' +
|
|
3082
|
+
'types declare as `Date`: `.toISOString()` and `.getTime()` throw a TypeError on these rows, and ' +
|
|
3083
|
+
'`JSON.stringify` still renders them null because JSON has no infinity literal. The number is the ' +
|
|
3084
|
+
'lossless reading, though: it binds straight back, so writing a row you just read stores `infinity` ' +
|
|
3085
|
+
'again. Two more things this column can no longer do: `where: { col: null }` still means IS NULL and ' +
|
|
3086
|
+
"does NOT match these rows (filter them with `{ col: 'infinity' }`), and `groupBy` / `distinct` " +
|
|
3087
|
+
"cannot tell infinity, -infinity and NULL apart. Set `temporalInfinity: 'preserve'` to confirm this " +
|
|
3088
|
+
"reading and silence the warning, or `'null'` to read null instead, accepting that a stored " +
|
|
3089
|
+
'infinity then looks exactly like a stored NULL and a read-modify-write over a nullable column ' +
|
|
3090
|
+
'stores SQL NULL, destroying the value with no error.');
|
|
3091
|
+
}
|
|
3092
|
+
/**
|
|
3093
|
+
* Apply the configured reading to the infinity elements of a temporal ARRAY
|
|
3094
|
+
* value, returning the original array by identity when there are none (the
|
|
3095
|
+
* overwhelmingly common case, so no per-row allocation on ordinary data).
|
|
3096
|
+
*/
|
|
3097
|
+
mapArrayTemporalInfinity(value, table, field) {
|
|
3098
|
+
let hit = false;
|
|
3099
|
+
for (let i = 0; i < value.length; i++) {
|
|
3100
|
+
if (isTemporalInfinity(value[i])) {
|
|
3101
|
+
hit = true;
|
|
3102
|
+
break;
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
if (!hit)
|
|
3106
|
+
return value;
|
|
3107
|
+
this.warnTemporalInfinity(table, field);
|
|
3108
|
+
return value.map((el) => (isTemporalInfinity(el) ? this.readTemporalInfinity(el) : el));
|
|
3109
|
+
}
|
|
3110
|
+
/**
|
|
3111
|
+
* The configured reading of one infinity value: the JS number (`'preserve'`,
|
|
3112
|
+
* the default) or `null`. Under `'preserve'` the JSON-wire STRING form
|
|
3113
|
+
* (`"infinity"`, what the join and positional strategies see) is normalized
|
|
3114
|
+
* to the number too, so the reading is identical on every strategy; 0.54
|
|
3115
|
+
* shipped the number on some paths and an Invalid Date on others, which is
|
|
3116
|
+
* the bug that made a single reading necessary in the first place.
|
|
3117
|
+
*/
|
|
3118
|
+
readTemporalInfinity(value) {
|
|
3119
|
+
if (this.temporalInfinity === 'null')
|
|
3120
|
+
return null;
|
|
3121
|
+
if (typeof value === 'number')
|
|
3122
|
+
return value;
|
|
3123
|
+
return value === '-infinity' ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
|
|
3124
|
+
}
|
|
3022
3125
|
parseRow(row, table) {
|
|
3023
3126
|
const parsed = {};
|
|
3024
3127
|
const meta = this.schema.tables[table];
|
|
@@ -3035,29 +3138,48 @@ export class QueryInterface {
|
|
|
3035
3138
|
const value = row[col];
|
|
3036
3139
|
const field = reverseMap[col] ?? col; // fall back to raw col name, not regex
|
|
3037
3140
|
// Top-level rows are snake_case (dateCols); nested rows are camelCase (camelDateFields).
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3141
|
+
if ((dateCols.has(col) || camelDateFields.has(field)) && value !== null && !(value instanceof Date)) {
|
|
3142
|
+
if (isTemporalInfinity(value)) {
|
|
3143
|
+
// Postgres `infinity` / `-infinity`. No JS Date means either, so
|
|
3144
|
+
// both readings cost something and the default is the one that is
|
|
3145
|
+
// not lossy. `'preserve'` hands back the JS number, which breaks
|
|
3146
|
+
// the declared `Date` type at runtime (`.toISOString()` throws) and
|
|
3147
|
+
// still serializes as null because JSON has no infinity literal,
|
|
3148
|
+
// but binds straight back, so a read-modify-write stores `infinity`
|
|
3149
|
+
// again. `'null'` reads nicer and DESTROYS the value on that same
|
|
3150
|
+
// write, because a stored infinity and a stored NULL become
|
|
3151
|
+
// indistinguishable. Whichever is configured, it is the SAME on
|
|
3152
|
+
// every read strategy: the driver hands back the number,
|
|
3153
|
+
// `json_build_object` hands back the string "infinity", and both
|
|
3154
|
+
// land here (see `isTemporalInfinity`).
|
|
3155
|
+
//
|
|
3156
|
+
// The warning below fires once per column when the option was left
|
|
3157
|
+
// unset, on a row that actually held an infinity, and describes the
|
|
3158
|
+
// reading in force rather than gating on which one it is.
|
|
3159
|
+
this.warnTemporalInfinity(table, field);
|
|
3160
|
+
parsed[field] = this.readTemporalInfinity(value);
|
|
3161
|
+
}
|
|
3162
|
+
else if (Array.isArray(value)) {
|
|
3163
|
+
// `dateColumns` includes array-of-date columns (`date[]`,
|
|
3164
|
+
// `timestamp[]`, `timestamptz[]`), for which the driver already
|
|
3165
|
+
// hands back a `Date[]`. Coercing the array itself ran
|
|
3166
|
+
// `new Date(String(theArray))` and replaced the whole column with
|
|
3167
|
+
// one Invalid Date. Its ELEMENTS get the same infinity mapping as a
|
|
3168
|
+
// scalar (same declared element type, same JSON rendering);
|
|
3169
|
+
// everything else is passed through by identity.
|
|
3170
|
+
parsed[field] = this.mapArrayTemporalInfinity(value, table, field);
|
|
3171
|
+
}
|
|
3172
|
+
else if (typeof value === 'number') {
|
|
3173
|
+
// Any other number on a date column is left alone rather than run
|
|
3174
|
+
// through `parseDbDate(String(n))`, which would produce an Invalid
|
|
3175
|
+
// Date.
|
|
3176
|
+
parsed[field] = value;
|
|
3177
|
+
}
|
|
3178
|
+
else {
|
|
3179
|
+
// Offset-less strings (Postgres `timestamp`, json_agg output) are
|
|
3180
|
+
// pinned to UTC so results don't depend on the server's time zone.
|
|
3181
|
+
parsed[field] = this.utcTimestamps ? parseDbDate(String(value)) : new Date(value);
|
|
3182
|
+
}
|
|
3061
3183
|
}
|
|
3062
3184
|
else {
|
|
3063
3185
|
parsed[field] = value;
|
package/dist/query/deferred.d.ts
CHANGED
|
@@ -35,6 +35,36 @@ export interface DeferredQuery<T> {
|
|
|
35
35
|
*/
|
|
36
36
|
reselect?: (exec: ReselectExecutor) => Promise<pg.QueryResult>;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* How the ORM hands back a Postgres temporal `infinity` / `-infinity`.
|
|
40
|
+
*
|
|
41
|
+
* JavaScript has no `Date` for either value, so every available reading is
|
|
42
|
+
* wrong in some way and the choice is which way:
|
|
43
|
+
*
|
|
44
|
+
* `'preserve'` the default. The JS numbers `Infinity` / `-Infinity`,
|
|
45
|
+
* normalized to the same value on EVERY read strategy (the
|
|
46
|
+
* join and positional paths see the JSON string `"infinity"`
|
|
47
|
+
* and are mapped to the number, which is what 0.54 got wrong).
|
|
48
|
+
* LOSSLESS: binding the number back to a temporal column
|
|
49
|
+
* stores `infinity` again, so `update({ data: { ...row } })`
|
|
50
|
+
* round-trips. THE COST: the field's declared type is `Date`,
|
|
51
|
+
* so `row.validUntil.toISOString()` throws a TypeError on
|
|
52
|
+
* those rows, and `JSON.stringify` still renders the value
|
|
53
|
+
* `null`.
|
|
54
|
+
* `'null'` opt-in. Matches what a caller serializing the row already
|
|
55
|
+
* saw (`JSON.stringify` renders every other candidate as null
|
|
56
|
+
* too) and is permitted by the declared type of a nullable
|
|
57
|
+
* column, so no method call throws. THE COST is data loss: a
|
|
58
|
+
* stored `infinity` and a stored NULL become
|
|
59
|
+
* indistinguishable, so a read-modify-write over a nullable
|
|
60
|
+
* column (`update({ data: { ...row } })`) writes SQL NULL and
|
|
61
|
+
* the infinity is gone, with no error.
|
|
62
|
+
*
|
|
63
|
+
* The default is the reading that cannot lose a value. Pick `'null'` when the
|
|
64
|
+
* declared type contract matters more than the stored value, knowing that rows
|
|
65
|
+
* read under it must not be written back.
|
|
66
|
+
*/
|
|
67
|
+
export type TemporalInfinityReading = 'null' | 'preserve';
|
|
38
68
|
/** Middleware function type, imported from client to avoid circular deps */
|
|
39
69
|
export type MiddlewareFn = (params: {
|
|
40
70
|
model: string;
|
|
@@ -142,6 +172,11 @@ export interface QueryInterfaceOptions {
|
|
|
142
172
|
* the pre-0.26 behavior (JS local-time interpretation).
|
|
143
173
|
*/
|
|
144
174
|
utcTimestamps?: boolean;
|
|
175
|
+
/**
|
|
176
|
+
* How a Postgres temporal `infinity` / `-infinity` is handed back. See
|
|
177
|
+
* {@link TemporalInfinityReading}. Default `'preserve'`.
|
|
178
|
+
*/
|
|
179
|
+
temporalInfinity?: TemporalInfinityReading;
|
|
145
180
|
/**
|
|
146
181
|
* Client-level default relation-loading strategy for `with` clauses; a
|
|
147
182
|
* per-query `relationLoadStrategy` arg overrides it. On SQL engines the default
|
package/dist/query/index.d.ts
CHANGED
|
@@ -10,5 +10,5 @@ export type { BuiltStatement, BulkInsertStatementInput, ColumnDefinitionInput, C
|
|
|
10
10
|
export { postgresDialect } from '../dialect.js';
|
|
11
11
|
export type { SqlCacheEntry } from './utils.js';
|
|
12
12
|
export { buildCorrelation, escapeLike, escSingleQuote, fnv1a64Hex, LRUCache, OPERATOR_KEYS, quoteIdent, sqlToPreparedName, } from './utils.js';
|
|
13
|
-
export type { DeferredQuery, MiddlewareFn, QueryEvent, QueryEventListener, QueryInterfaceOptions, ReselectExecutor, } from './builder.js';
|
|
13
|
+
export type { DeferredQuery, MiddlewareFn, QueryEvent, QueryEventListener, QueryInterfaceOptions, ReselectExecutor, TemporalInfinityReading, } from './builder.js';
|
|
14
14
|
export { AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, QueryInterface, } from './builder.js';
|
package/dist/query/utils.d.ts
CHANGED
|
@@ -176,6 +176,38 @@ export declare function coerceTemporalValue(dbType: string | undefined, value: u
|
|
|
176
176
|
* as-is.
|
|
177
177
|
*/
|
|
178
178
|
export declare function parseDbDate(value: string): Date;
|
|
179
|
+
/**
|
|
180
|
+
* Is `value` one of the two representations a Postgres temporal `infinity` /
|
|
181
|
+
* `-infinity` reaches the ORM row parser in?
|
|
182
|
+
*
|
|
183
|
+
* TWO representations, because a temporal column is read two different ways
|
|
184
|
+
* and they disagree on the wire:
|
|
185
|
+
*
|
|
186
|
+
* driver the pg text parser for `timestamp` / `date` does not
|
|
187
|
+
* recognise the word, so it falls through to the driver's own
|
|
188
|
+
* parser, which returns the JS NUMBERS `Infinity` /
|
|
189
|
+
* `-Infinity`. This is what a top-level row, the batched and
|
|
190
|
+
* flatten strategies, a write `RETURNING` projection and a
|
|
191
|
+
* `groupBy` key all see.
|
|
192
|
+
* JSON wire `json_build_object` renders the same value as the STRING
|
|
193
|
+
* `"infinity"`, and scalar `timestamp` / `timestamptz` are
|
|
194
|
+
* deliberately absent from {@link JSON_WIRE_COERCION_OIDS},
|
|
195
|
+
* so no driver parser runs over it. This is what the `'join'`
|
|
196
|
+
* strategy and the positional encoding see.
|
|
197
|
+
*
|
|
198
|
+
* Both are normalized in one place ({@link QueryInterface}'s row parser), to
|
|
199
|
+
* whichever reading `temporalInfinity` selects (the JS number by default, or
|
|
200
|
+
* `null`), so the same stored value cannot read differently depending on which
|
|
201
|
+
* plan the query happened to take. The string form is only ever consulted for a column
|
|
202
|
+
* the schema says is temporal, so a `text` column holding the word "infinity"
|
|
203
|
+
* is untouched.
|
|
204
|
+
*
|
|
205
|
+
* Not dialect-gated. Postgres is the only engine with an infinite temporal
|
|
206
|
+
* value, but the row parser is engine-shared and the alternative reading on the
|
|
207
|
+
* other engines (a stray `'infinity'` string becoming an Invalid Date) is not
|
|
208
|
+
* one worth preserving.
|
|
209
|
+
*/
|
|
210
|
+
export declare function isTemporalInfinity(value: unknown): boolean;
|
|
179
211
|
/**
|
|
180
212
|
* Build the driver parser for Postgres `date` (OID 1082) that reads a
|
|
181
213
|
* zone-less calendar day as **UTC midnight**.
|
|
@@ -243,6 +275,52 @@ export declare function parseUtcTimestampText(text: string): Date;
|
|
|
243
275
|
* dependency. NULL elements stay `null` and are never handed to `element`.
|
|
244
276
|
*/
|
|
245
277
|
export declare function createPgArrayParser(element: (text: string) => unknown): (text: string) => unknown[];
|
|
278
|
+
/** Tag `parser` as Turbine's own and return it (see {@link TURBINE_PARSER}). */
|
|
279
|
+
export declare function markTurbineParser<F extends (text: string) => unknown>(parser: F): F;
|
|
280
|
+
/**
|
|
281
|
+
* Is the parser currently registered for `oid` still pg's own default?
|
|
282
|
+
*
|
|
283
|
+
* DETECTED BY BEHAVIOUR, NOT BY IDENTITY, and deliberately so. `pg-types` keeps
|
|
284
|
+
* its default parser table private: `getTypeParser` hands back whatever is
|
|
285
|
+
* registered NOW, and there is no exported way to ask what the default WAS, so
|
|
286
|
+
* a function-identity comparison would need a deep import of a file the package
|
|
287
|
+
* does not publish as an entry point. Instead this runs the registered parser
|
|
288
|
+
* over a canonical wire value and compares the result with what pg's default
|
|
289
|
+
* produces for it.
|
|
290
|
+
*
|
|
291
|
+
* What that buys and what it costs, stated honestly:
|
|
292
|
+
* - Every parser that behaves OBSERVABLY differently on the probe value is
|
|
293
|
+
* detected, which is the case worth warning about (someone else's reading
|
|
294
|
+
* is about to be replaced by Turbine's).
|
|
295
|
+
* - A replacement that is observably EQUIVALENT on the probe is reported as
|
|
296
|
+
* the default and draws no warning. That is a false negative, and an
|
|
297
|
+
* acceptable one: if it agrees with the default here it is not a reading
|
|
298
|
+
* anybody would notice Turbine overwriting.
|
|
299
|
+
* - A parser that THROWS on the probe is reported as non-default; pg's own
|
|
300
|
+
* never throws on a valid value of its type.
|
|
301
|
+
* - An OID with no probe entry is reported as default (never warn on a guess).
|
|
302
|
+
*
|
|
303
|
+
* The registered parser is invoked once, on a synthetic value, at client
|
|
304
|
+
* construction. A decode parser with side effects would be surprising, and pg's
|
|
305
|
+
* own have none.
|
|
306
|
+
*/
|
|
307
|
+
export declare function isDefaultTextParser(oid: number, parser: (text: string) => unknown): boolean;
|
|
308
|
+
/**
|
|
309
|
+
* Warn ONCE per OID when Turbine is about to replace a text parser that is not
|
|
310
|
+
* pg's default, i.e. when some other module in the process has already
|
|
311
|
+
* customized it.
|
|
312
|
+
*
|
|
313
|
+
* `pg.types.setTypeParser` is process-global and retroactive: it changes how
|
|
314
|
+
* every `pg.Pool` in the process decodes that OID, including pools that were
|
|
315
|
+
* constructed and were already querying before the Turbine client existed. When
|
|
316
|
+
* the OID was still on pg's default that is the documented, intended trade (the
|
|
317
|
+
* whole point of `utcTimestamps`). When somebody else had already installed
|
|
318
|
+
* their own reading, Turbine is silently rewriting an expectation it cannot see
|
|
319
|
+
* the origin of, and the resulting bug is order-dependent: which reading wins
|
|
320
|
+
* depends on module evaluation order, which lazy route imports make unstable
|
|
321
|
+
* between requests. So say it out loud, once.
|
|
322
|
+
*/
|
|
323
|
+
export declare function warnParserOverwrite(oid: number, typeName: string): void;
|
|
246
324
|
/**
|
|
247
325
|
* Register the UTC readings of the four zone-less temporal OIDs on the pg
|
|
248
326
|
* module: `timestamp` (1114), `date` (1082) and their array forms (1115, 1182).
|
|
@@ -258,6 +336,11 @@ export declare function createPgArrayParser(element: (text: string) => unknown):
|
|
|
258
336
|
* Each fallback is read BEFORE its parser is installed, so an unrecognised wire
|
|
259
337
|
* value (`infinity`, and whatever a future server adds) still reaches the
|
|
260
338
|
* driver's own parser.
|
|
339
|
+
*
|
|
340
|
+
* Registration is RETROACTIVE for the whole process, pools included that were
|
|
341
|
+
* created and are already querying (there is one parser table, and it is read
|
|
342
|
+
* per row at decode time, not captured per pool). If any of the four OIDs is
|
|
343
|
+
* already on a NON-default parser, {@link warnParserOverwrite} says so once.
|
|
261
344
|
*/
|
|
262
345
|
export declare function registerUtcTemporalParsers(): void;
|
|
263
346
|
/**
|