@atscript/db-postgres 0.1.131 → 0.1.132

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/dist/index.cjs CHANGED
@@ -2,6 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let _atscript_db = require("@atscript/db");
3
3
  let _atscript_db_agg = require("@atscript/db/agg");
4
4
  let _atscript_db_sql_tools = require("@atscript/db-sql-tools");
5
+ let _uniqu_core = require("@uniqu/core");
5
6
  //#region src/sql-builder.ts
6
7
  /**
7
8
  * PostgreSQL-aware default value for a given design type.
@@ -76,12 +77,44 @@ const pgDialect = {
76
77
  ]
77
78
  };
78
79
  },
80
+ calendarBucket: pgCalendarBucket,
79
81
  createViewPrefix: "CREATE OR REPLACE VIEW",
80
82
  paramPlaceholder(index) {
81
83
  return `$${index}`;
82
84
  }
83
85
  };
84
86
  /**
87
+ * Calendar-bucket label over an epoch-ms column (BIGINT for
88
+ * `@db.default.now`, DOUBLE PRECISION otherwise): TEXT `'YYYY-MM-DD'` — the
89
+ * local date of the bucket's first day in `b.tz` — or NULL for a NULL source
90
+ * or one outside `[BUCKET_MIN_INSTANT, BUCKET_MAX_INSTANT)`.
91
+ *
92
+ * The local date is `(to_timestamp(col / 1000) AT TIME ZONE '<tz>')::date`:
93
+ * `timestamptz AT TIME ZONE` yields the wall time in that zone, independent
94
+ * of the session `TimeZone`. Truncation is calendar arithmetic on that date —
95
+ * the week uses the generic `(isodow - weekStart + 7) % 7` formula
96
+ * (`date_trunc('week')` is Monday-only). `date_trunc` and `to_char` get an
97
+ * explicit `::timestamp`: a bare `date` resolves to their `timestamptz`
98
+ * overloads, which would route through the session zone.
99
+ *
100
+ * Parameter-free (the zone is an inlined, charset-checked literal), so the
101
+ * SELECT and GROUP BY renderings match structurally.
102
+ */
103
+ function pgCalendarBucket(quotedCol, b) {
104
+ const local = `(to_timestamp(${quotedCol}::double precision / 1000) AT TIME ZONE ${(0, _atscript_db_sql_tools.sqlTimeZoneLiteral)(b.tz)})::date`;
105
+ let first;
106
+ switch (b.unit) {
107
+ case "day":
108
+ first = local;
109
+ break;
110
+ case "week":
111
+ first = `(${local} - ((EXTRACT(ISODOW FROM ${local})::int - ${b.weekStartIso} + 7) % 7))`;
112
+ break;
113
+ default: first = `date_trunc('${b.unit}', ${local}::timestamp)`;
114
+ }
115
+ return `CASE WHEN ${quotedCol} >= ${_uniqu_core.BUCKET_MIN_INSTANT} AND ${quotedCol} < ${_uniqu_core.BUCKET_MAX_INSTANT} THEN ${`to_char(${first}::timestamp, 'YYYY-MM-DD')`} END`;
116
+ }
117
+ /**
85
118
  * Distance-in-meters expression from a geography column to a query point.
86
119
  * Used as the `distExpr` of the shared geo search builder.
87
120
  */
@@ -113,6 +146,10 @@ function parseEwkbPointHex(hex) {
113
146
  function buildInsert(table, data) {
114
147
  return (0, _atscript_db_sql_tools.buildInsert)(pgDialect, table, data);
115
148
  }
149
+ /** Multi-row INSERT over `columns` (a missing column → `DEFAULT`). */
150
+ function buildInsertMany(table, rows, columns) {
151
+ return (0, _atscript_db_sql_tools.buildInsertMany)(pgDialect, table, rows, columns);
152
+ }
116
153
  function buildSelect(table, where, controls) {
117
154
  return (0, _atscript_db_sql_tools.buildSelect)(pgDialect, table, where, controls);
118
155
  }
@@ -428,6 +465,13 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
428
465
  nativeDefaultFns() {
429
466
  return PostgresAdapter.NATIVE_DEFAULT_FNS;
430
467
  }
468
+ /**
469
+ * Every unit: `pgCalendarBucket` renders them all over any zone in the
470
+ * server's tz database (an unknown one maps to `BUCKET_TZ_UNAVAILABLE`).
471
+ */
472
+ calendarBucketUnits() {
473
+ return _atscript_db.ALL_BUCKET_UNITS;
474
+ }
431
475
  onBeforeFlatten(_type) {}
432
476
  onAfterFlatten() {
433
477
  for (const fd of this._table.fieldDescriptors) if (fd.collate === "nocase") this._nocaseColumns.add(fd.physicalName);
@@ -545,31 +589,19 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
545
589
  const tableName = this.resolveTableName();
546
590
  const pkCols = this._table.primaryKeys;
547
591
  const returningSuffix = pkCols.length > 0 ? ` RETURNING ${pkCols.map((pk) => qi(pk)).join(", ")}` : "";
548
- const keys = Object.keys(data[0]);
549
- const colsClause = keys.map((k) => qi(k)).join(", ");
550
- const paramsPerRow = keys.length;
551
- const maxRowsPerBatch = paramsPerRow > 0 ? Math.floor(6e4 / paramsPerRow) : data.length;
592
+ const columns = (0, _atscript_db_sql_tools.insertManyColumns)(data);
593
+ const maxRowsPerBatch = columns.length > 0 ? Math.floor(6e4 / columns.length) : data.length;
552
594
  const allIds = [];
553
595
  for (let offset = 0; offset < data.length; offset += maxRowsPerBatch) {
554
- const batchEnd = Math.min(offset + maxRowsPerBatch, data.length);
555
- const batchSize = batchEnd - offset;
556
- const params = [];
557
- const valuesClauses = [];
558
- for (let i = offset; i < batchEnd; i++) {
559
- const row = data[i];
560
- const rowPlaceholders = [];
561
- for (const k of keys) {
562
- params.push(pgDialect.toValue(row[k]));
563
- rowPlaceholders.push(`$${params.length}`);
564
- }
565
- valuesClauses.push(`(${rowPlaceholders.join(", ")})`);
566
- }
567
- const sql = `INSERT INTO ${quoteTableName(tableName)} (${colsClause}) VALUES ${valuesClauses.join(", ")}${returningSuffix}`;
596
+ const batch = data.slice(offset, offset + maxRowsPerBatch);
597
+ const insert = buildInsertMany(tableName, batch, columns);
598
+ const sql = insert.sql + returningSuffix;
599
+ const params = insert.params;
568
600
  this._log(sql, params);
569
601
  const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
570
- for (let i = 0; i < batchSize; i++) {
602
+ for (let i = 0; i < batch.length; i++) {
571
603
  const returned = result.rows?.[i];
572
- allIds.push(this._resolveInsertedId(data[offset + i], returned ? Object.values(returned)[0] : void 0));
604
+ allIds.push(this._resolveInsertedId(batch[i], returned ? Object.values(returned)[0] : void 0));
573
605
  }
574
606
  }
575
607
  return {
@@ -610,11 +642,28 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
610
642
  if (query.controls.$count) {
611
643
  const { sql, params } = buildAggregateCount(tableName, where, query.controls);
612
644
  this._log(sql, params);
613
- return [{ count: parseCount((await this._exec().get(sql, params))?.count) }];
645
+ return [{ count: parseCount((await this._wrapBucketZoneError(() => this._exec().get(sql, params)))?.count) }];
614
646
  }
615
647
  const { sql, params } = buildAggregateSelect(tableName, where, query.controls);
616
648
  this._log(sql, params);
617
- return this._exec().all(sql, params);
649
+ return this._wrapBucketZoneError(() => this._exec().all(sql, params));
650
+ }
651
+ /**
652
+ * Maps PostgreSQL's unknown-zone error — SQLSTATE 22023
653
+ * (`invalid_parameter_value`), `time zone "…" not recognized` — raised by a
654
+ * calendar bucket's `AT TIME ZONE '<tz>'` to `BUCKET_TZ_UNAVAILABLE`. The
655
+ * zone passed the core's IANA validation, so the server's tz database is
656
+ * older than the runtime's. Other errors propagate unchanged.
657
+ */
658
+ async _wrapBucketZoneError(fn) {
659
+ try {
660
+ return await fn();
661
+ } catch (error) {
662
+ const err = error;
663
+ const zone = err?.code === "22023" && typeof err.message === "string" ? /time zone "([^"]*)" not recognized/.exec(err.message)?.[1] : void 0;
664
+ if (zone !== void 0) throw (0, _atscript_db.bucketTimeZoneUnavailable)(`PostgreSQL does not recognize time zone "${zone}" — update the server's time zone data`);
665
+ throw error;
666
+ }
618
667
  }
619
668
  async updateOne(filter, data, ops, expectedVersion) {
620
669
  const where = buildWhere(filter);
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair } from "@atscript/db";
1
+ import { BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair } from "@atscript/db";
2
2
  import { TMetadataMap } from "@atscript/typescript/utils";
3
3
  import { TSqlFragment } from "@atscript/db-sql-tools";
4
4
 
@@ -113,6 +113,11 @@ declare class PostgresAdapter extends BaseDbAdapter {
113
113
  prepareId(id: unknown, _fieldType: unknown): unknown;
114
114
  supportsNativeValueDefaults(): boolean;
115
115
  nativeDefaultFns(): ReadonlySet<TDbDefaultFn>;
116
+ /**
117
+ * Every unit: `pgCalendarBucket` renders them all over any zone in the
118
+ * server's tz database (an unknown one maps to `BUCKET_TZ_UNAVAILABLE`).
119
+ */
120
+ calendarBucketUnits(): ReadonlySet<BucketUnit>;
116
121
  onBeforeFlatten(_type: unknown): void;
117
122
  onAfterFlatten(): void;
118
123
  onFieldScanned(field: string, _type: unknown, metadata: TMetadataMap<AtscriptMetadata>): void;
@@ -141,6 +146,14 @@ declare class PostgresAdapter extends BaseDbAdapter {
141
146
  findMany(query: DbQuery): Promise<Array<Record<string, unknown>>>;
142
147
  count(query: DbQuery): Promise<number>;
143
148
  aggregate(query: DbQuery): Promise<Array<Record<string, unknown>>>;
149
+ /**
150
+ * Maps PostgreSQL's unknown-zone error — SQLSTATE 22023
151
+ * (`invalid_parameter_value`), `time zone "…" not recognized` — raised by a
152
+ * calendar bucket's `AT TIME ZONE '<tz>'` to `BUCKET_TZ_UNAVAILABLE`. The
153
+ * zone passed the core's IANA validation, so the server's tz database is
154
+ * older than the runtime's. Other errors propagate unchanged.
155
+ */
156
+ private _wrapBucketZoneError;
144
157
  updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number): Promise<TDbUpdateResult>;
145
158
  updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps): Promise<TDbUpdateResult>;
146
159
  replaceOne(filter: FilterExpr, data: Record<string, unknown>, expectedVersion?: number): Promise<TDbUpdateResult>;
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair } from "@atscript/db";
1
+ import { BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair } from "@atscript/db";
2
2
  import { TSqlFragment } from "@atscript/db-sql-tools";
3
3
  import { TMetadataMap } from "@atscript/typescript/utils";
4
4
 
@@ -113,6 +113,11 @@ declare class PostgresAdapter extends BaseDbAdapter {
113
113
  prepareId(id: unknown, _fieldType: unknown): unknown;
114
114
  supportsNativeValueDefaults(): boolean;
115
115
  nativeDefaultFns(): ReadonlySet<TDbDefaultFn>;
116
+ /**
117
+ * Every unit: `pgCalendarBucket` renders them all over any zone in the
118
+ * server's tz database (an unknown one maps to `BUCKET_TZ_UNAVAILABLE`).
119
+ */
120
+ calendarBucketUnits(): ReadonlySet<BucketUnit>;
116
121
  onBeforeFlatten(_type: unknown): void;
117
122
  onAfterFlatten(): void;
118
123
  onFieldScanned(field: string, _type: unknown, metadata: TMetadataMap<AtscriptMetadata>): void;
@@ -141,6 +146,14 @@ declare class PostgresAdapter extends BaseDbAdapter {
141
146
  findMany(query: DbQuery): Promise<Array<Record<string, unknown>>>;
142
147
  count(query: DbQuery): Promise<number>;
143
148
  aggregate(query: DbQuery): Promise<Array<Record<string, unknown>>>;
149
+ /**
150
+ * Maps PostgreSQL's unknown-zone error — SQLSTATE 22023
151
+ * (`invalid_parameter_value`), `time zone "…" not recognized` — raised by a
152
+ * calendar bucket's `AT TIME ZONE '<tz>'` to `BUCKET_TZ_UNAVAILABLE`. The
153
+ * zone passed the core's IANA validation, so the server's tz database is
154
+ * older than the runtime's. Other errors propagate unchanged.
155
+ */
156
+ private _wrapBucketZoneError;
144
157
  updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number): Promise<TDbUpdateResult>;
145
158
  updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps): Promise<TDbUpdateResult>;
146
159
  replaceOne(filter: FilterExpr, data: Record<string, unknown>, expectedVersion?: number): Promise<TDbUpdateResult>;
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
- import { BaseDbAdapter, DbError, DbSpace } from "@atscript/db";
1
+ import { ALL_BUCKET_UNITS, BaseDbAdapter, DbError, DbSpace, bucketTimeZoneUnavailable } from "@atscript/db";
2
2
  import { resolveAggregateSearch } from "@atscript/db/agg";
3
- import { buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildSelect, buildUpdate, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, fillReplacePayload, finalizeParams, geoWindowFromControls, normalizeGeoPointValue, parseRegexString, refActionToSql, renameGeoDistance, replaceColumnsFor } from "@atscript/db-sql-tools";
3
+ import { buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildSelect, buildUpdate, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, fillReplacePayload, finalizeParams, geoWindowFromControls, insertManyColumns, normalizeGeoPointValue, parseRegexString, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlTimeZoneLiteral } from "@atscript/db-sql-tools";
4
+ import { BUCKET_MAX_INSTANT, BUCKET_MIN_INSTANT } from "@uniqu/core";
4
5
  //#region src/sql-builder.ts
5
6
  /**
6
7
  * PostgreSQL-aware default value for a given design type.
@@ -75,12 +76,44 @@ const pgDialect = {
75
76
  ]
76
77
  };
77
78
  },
79
+ calendarBucket: pgCalendarBucket,
78
80
  createViewPrefix: "CREATE OR REPLACE VIEW",
79
81
  paramPlaceholder(index) {
80
82
  return `$${index}`;
81
83
  }
82
84
  };
83
85
  /**
86
+ * Calendar-bucket label over an epoch-ms column (BIGINT for
87
+ * `@db.default.now`, DOUBLE PRECISION otherwise): TEXT `'YYYY-MM-DD'` — the
88
+ * local date of the bucket's first day in `b.tz` — or NULL for a NULL source
89
+ * or one outside `[BUCKET_MIN_INSTANT, BUCKET_MAX_INSTANT)`.
90
+ *
91
+ * The local date is `(to_timestamp(col / 1000) AT TIME ZONE '<tz>')::date`:
92
+ * `timestamptz AT TIME ZONE` yields the wall time in that zone, independent
93
+ * of the session `TimeZone`. Truncation is calendar arithmetic on that date —
94
+ * the week uses the generic `(isodow - weekStart + 7) % 7` formula
95
+ * (`date_trunc('week')` is Monday-only). `date_trunc` and `to_char` get an
96
+ * explicit `::timestamp`: a bare `date` resolves to their `timestamptz`
97
+ * overloads, which would route through the session zone.
98
+ *
99
+ * Parameter-free (the zone is an inlined, charset-checked literal), so the
100
+ * SELECT and GROUP BY renderings match structurally.
101
+ */
102
+ function pgCalendarBucket(quotedCol, b) {
103
+ const local = `(to_timestamp(${quotedCol}::double precision / 1000) AT TIME ZONE ${sqlTimeZoneLiteral(b.tz)})::date`;
104
+ let first;
105
+ switch (b.unit) {
106
+ case "day":
107
+ first = local;
108
+ break;
109
+ case "week":
110
+ first = `(${local} - ((EXTRACT(ISODOW FROM ${local})::int - ${b.weekStartIso} + 7) % 7))`;
111
+ break;
112
+ default: first = `date_trunc('${b.unit}', ${local}::timestamp)`;
113
+ }
114
+ return `CASE WHEN ${quotedCol} >= ${BUCKET_MIN_INSTANT} AND ${quotedCol} < ${BUCKET_MAX_INSTANT} THEN ${`to_char(${first}::timestamp, 'YYYY-MM-DD')`} END`;
115
+ }
116
+ /**
84
117
  * Distance-in-meters expression from a geography column to a query point.
85
118
  * Used as the `distExpr` of the shared geo search builder.
86
119
  */
@@ -112,6 +145,10 @@ function parseEwkbPointHex(hex) {
112
145
  function buildInsert$1(table, data) {
113
146
  return buildInsert(pgDialect, table, data);
114
147
  }
148
+ /** Multi-row INSERT over `columns` (a missing column → `DEFAULT`). */
149
+ function buildInsertMany$1(table, rows, columns) {
150
+ return buildInsertMany(pgDialect, table, rows, columns);
151
+ }
115
152
  function buildSelect$1(table, where, controls) {
116
153
  return buildSelect(pgDialect, table, where, controls);
117
154
  }
@@ -427,6 +464,13 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
427
464
  nativeDefaultFns() {
428
465
  return PostgresAdapter.NATIVE_DEFAULT_FNS;
429
466
  }
467
+ /**
468
+ * Every unit: `pgCalendarBucket` renders them all over any zone in the
469
+ * server's tz database (an unknown one maps to `BUCKET_TZ_UNAVAILABLE`).
470
+ */
471
+ calendarBucketUnits() {
472
+ return ALL_BUCKET_UNITS;
473
+ }
430
474
  onBeforeFlatten(_type) {}
431
475
  onAfterFlatten() {
432
476
  for (const fd of this._table.fieldDescriptors) if (fd.collate === "nocase") this._nocaseColumns.add(fd.physicalName);
@@ -544,31 +588,19 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
544
588
  const tableName = this.resolveTableName();
545
589
  const pkCols = this._table.primaryKeys;
546
590
  const returningSuffix = pkCols.length > 0 ? ` RETURNING ${pkCols.map((pk) => qi(pk)).join(", ")}` : "";
547
- const keys = Object.keys(data[0]);
548
- const colsClause = keys.map((k) => qi(k)).join(", ");
549
- const paramsPerRow = keys.length;
550
- const maxRowsPerBatch = paramsPerRow > 0 ? Math.floor(6e4 / paramsPerRow) : data.length;
591
+ const columns = insertManyColumns(data);
592
+ const maxRowsPerBatch = columns.length > 0 ? Math.floor(6e4 / columns.length) : data.length;
551
593
  const allIds = [];
552
594
  for (let offset = 0; offset < data.length; offset += maxRowsPerBatch) {
553
- const batchEnd = Math.min(offset + maxRowsPerBatch, data.length);
554
- const batchSize = batchEnd - offset;
555
- const params = [];
556
- const valuesClauses = [];
557
- for (let i = offset; i < batchEnd; i++) {
558
- const row = data[i];
559
- const rowPlaceholders = [];
560
- for (const k of keys) {
561
- params.push(pgDialect.toValue(row[k]));
562
- rowPlaceholders.push(`$${params.length}`);
563
- }
564
- valuesClauses.push(`(${rowPlaceholders.join(", ")})`);
565
- }
566
- const sql = `INSERT INTO ${quoteTableName(tableName)} (${colsClause}) VALUES ${valuesClauses.join(", ")}${returningSuffix}`;
595
+ const batch = data.slice(offset, offset + maxRowsPerBatch);
596
+ const insert = buildInsertMany$1(tableName, batch, columns);
597
+ const sql = insert.sql + returningSuffix;
598
+ const params = insert.params;
567
599
  this._log(sql, params);
568
600
  const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
569
- for (let i = 0; i < batchSize; i++) {
601
+ for (let i = 0; i < batch.length; i++) {
570
602
  const returned = result.rows?.[i];
571
- allIds.push(this._resolveInsertedId(data[offset + i], returned ? Object.values(returned)[0] : void 0));
603
+ allIds.push(this._resolveInsertedId(batch[i], returned ? Object.values(returned)[0] : void 0));
572
604
  }
573
605
  }
574
606
  return {
@@ -609,11 +641,28 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
609
641
  if (query.controls.$count) {
610
642
  const { sql, params } = buildAggregateCount$1(tableName, where, query.controls);
611
643
  this._log(sql, params);
612
- return [{ count: parseCount((await this._exec().get(sql, params))?.count) }];
644
+ return [{ count: parseCount((await this._wrapBucketZoneError(() => this._exec().get(sql, params)))?.count) }];
613
645
  }
614
646
  const { sql, params } = buildAggregateSelect$1(tableName, where, query.controls);
615
647
  this._log(sql, params);
616
- return this._exec().all(sql, params);
648
+ return this._wrapBucketZoneError(() => this._exec().all(sql, params));
649
+ }
650
+ /**
651
+ * Maps PostgreSQL's unknown-zone error — SQLSTATE 22023
652
+ * (`invalid_parameter_value`), `time zone "…" not recognized` — raised by a
653
+ * calendar bucket's `AT TIME ZONE '<tz>'` to `BUCKET_TZ_UNAVAILABLE`. The
654
+ * zone passed the core's IANA validation, so the server's tz database is
655
+ * older than the runtime's. Other errors propagate unchanged.
656
+ */
657
+ async _wrapBucketZoneError(fn) {
658
+ try {
659
+ return await fn();
660
+ } catch (error) {
661
+ const err = error;
662
+ const zone = err?.code === "22023" && typeof err.message === "string" ? /time zone "([^"]*)" not recognized/.exec(err.message)?.[1] : void 0;
663
+ if (zone !== void 0) throw bucketTimeZoneUnavailable(`PostgreSQL does not recognize time zone "${zone}" — update the server's time zone data`);
664
+ throw error;
665
+ }
617
666
  }
618
667
  async updateOne(filter, data, ops, expectedVersion) {
619
668
  const where = buildWhere(filter);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-postgres",
3
- "version": "0.1.131",
3
+ "version": "0.1.132",
4
4
  "description": "PostgreSQL adapter for @atscript/db with pg (node-postgres) driver support.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -46,17 +46,17 @@
46
46
  "@atscript/core": "^0.1.92",
47
47
  "@atscript/typescript": "^0.1.92",
48
48
  "@types/pg": "^8.11.0",
49
- "@uniqu/core": "^0.1.8",
49
+ "@uniqu/core": "^0.1.9",
50
50
  "pg": "^8.13.0",
51
51
  "unplugin-atscript": "^0.1.92"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@atscript/core": "^0.1.92",
55
55
  "@atscript/typescript": "^0.1.92",
56
- "@uniqu/core": "^0.1.8",
56
+ "@uniqu/core": "^0.1.9",
57
57
  "pg": ">=8.0.0",
58
- "@atscript/db": "^0.1.131",
59
- "@atscript/db-sql-tools": "^0.1.131"
58
+ "@atscript/db": "^0.1.132",
59
+ "@atscript/db-sql-tools": "^0.1.132"
60
60
  },
61
61
  "peerDependenciesMeta": {
62
62
  "pg": {