turbine-orm 0.91.0 → 0.92.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.
@@ -824,6 +824,13 @@ function renderTemporal(value, kind) {
824
824
  * or a `±HH`, `±HHMM`, `±HH:MM` offset.
825
825
  */
826
826
  const TZ_SUFFIX_RE = /(?:Z|[+-]\d{2}(?::?\d{2})?)$/;
827
+ const COLON_CHAR = 58;
828
+ const PLUS_CHAR = 43;
829
+ const MINUS_CHAR = 45;
830
+ /** `0`-`9` as a char code. */
831
+ function isDigitCode(code) {
832
+ return code >= 48 && code <= 57;
833
+ }
827
834
  /**
828
835
  * Parse a database date-time string deterministically.
829
836
  *
@@ -846,6 +853,29 @@ export function parseDbDate(value) {
846
853
  return Number.isNaN(date.getTime()) ? (parseDbDateGeneral(value) ?? date) : date;
847
854
  }
848
855
  function parseDbDateCommon(value) {
856
+ // Fast path for the shape `timestamptz` arrives in, a trailing `±HH:MM`
857
+ // offset (`2026-07-07T17:15:41.896+00:00`). It is exactly the general path's
858
+ // answer for that shape, not an approximation of it: the string contains a
859
+ // ':' (at length-3), TZ_SUFFIX_RE matches, and neither normalizing replace
860
+ // below can fire (the first needs four digits after the sign, the second two
861
+ // digits at the very end after it), so that path also ends in
862
+ // `new Date(value)`. All six offset characters are checked, so a near miss
863
+ // (`+0a:00`, `05:30` without a sign) still takes the general path. It skips
864
+ // one regex test and two regex replaces per value, which on a nested read is
865
+ // the single largest cost in the decode. The invalid-date fallback in
866
+ // parseDbDate still runs on the result, so a BC or five-digit year is
867
+ // unchanged.
868
+ const n = value.length;
869
+ if (n >= 6 && value.charCodeAt(n - 3) === COLON_CHAR) {
870
+ const sign = value.charCodeAt(n - 6);
871
+ if ((sign === PLUS_CHAR || sign === MINUS_CHAR) &&
872
+ isDigitCode(value.charCodeAt(n - 5)) &&
873
+ isDigitCode(value.charCodeAt(n - 4)) &&
874
+ isDigitCode(value.charCodeAt(n - 2)) &&
875
+ isDigitCode(value.charCodeAt(n - 1))) {
876
+ return new Date(value);
877
+ }
878
+ }
849
879
  // Date-only values (`2026-07-07`, from `date` columns in json_agg output)
850
880
  // have no time to zone-pin, and their `-07` tail must not be read as an
851
881
  // offset. JS parses bare ISO dates as UTC midnight already.
@@ -885,6 +915,19 @@ function parseDbDateGeneral(value) {
885
915
  }
886
916
  return date;
887
917
  }
918
+ /**
919
+ * Per-cell decode work in a row decode plan (`RowDecodePlan` in builder.ts).
920
+ * Defined here rather than in builder.ts because the positional nested-row
921
+ * plan in relations.ts reads the same codes, and relations.ts cannot import
922
+ * builder.ts.
923
+ */
924
+ export const CELL_PLAIN = 0;
925
+ export const CELL_TEMPORAL = 1;
926
+ export const CELL_FLOAT = 2;
927
+ export const CELL_FLOAT_ARRAY = 3;
928
+ export const CELL_TEXT_ARRAY = 4;
929
+ /** The dialect's own decode for a type whose storage differs (see `Dialect.decodeColumnValue`). */
930
+ export const CELL_DIALECT = 5;
888
931
  /**
889
932
  * A float column's value as the driver reads it. JSON has no NaN or infinity,
890
933
  * so `json_build_object` renders them as the strings "NaN", "Infinity" and
@@ -133,6 +133,21 @@ export interface BuilderCtx {
133
133
  toColumn(field: string): string;
134
134
  castAgg(expr: string, target: 'int' | 'float'): string;
135
135
  parseRow(row: Record<string, unknown>, table: string): Record<string, unknown>;
136
+ /**
137
+ * The field name and cell kind {@link parseRow} applies to each key of a row
138
+ * whose keys are exactly `keys`, in order (`undefined` for a table with no
139
+ * metadata). Read once per relation shape by the positional nested-row plan
140
+ * in relations.ts, so a nested row decodes by the same answers `parseRow`
141
+ * would give. Optional, like the members above, so a hand-built ctx keeps
142
+ * compiling; without it (or {@link decodeCell}) the nested parser takes the
143
+ * general path.
144
+ */
145
+ rowDecodePlan?(table: string, keys: readonly string[]): {
146
+ readonly fields: readonly string[];
147
+ readonly kinds: readonly number[];
148
+ } | undefined;
149
+ /** {@link parseRow}'s decode of one cell whose kind is not `CELL_PLAIN` (see utils.ts). */
150
+ decodeCell?(kind: number, value: unknown, table: string, field: string): unknown;
136
151
  nullsSuffix(nulls: 'first' | 'last' | undefined): string;
137
152
  isRelationOrderByValue(value: unknown): boolean;
138
153
  resolveOrderByColumn(table: string, meta: TableMetadata, key: string): string;
@@ -73,8 +73,10 @@ export interface ColumnDef {
73
73
  * code-first declaration: it carries onto {@link ColumnConfig.pii} and, via
74
74
  * `schemaDefToMetadata` / codegen, onto
75
75
  * {@link import('./schema.js').ColumnMetadata.pii}. A PII column is excluded
76
- * from default projections (read back only via an explicit `select` or
77
- * `includePii: UNSAFE`, the imported sentinel symbol) and redacted by Studio.
76
+ * from default projections and comes back only from a read that passes
77
+ * `includePii: UNSAFE` (the imported sentinel symbol). Naming it in `select`
78
+ * without that flag throws (E003), a write never returns it, and Studio
79
+ * redacts it.
78
80
  * Introspection never auto-tags PII.
79
81
  */
80
82
  pii?: boolean;
@@ -248,9 +250,10 @@ export interface TableDef {
248
250
  checks?: readonly CheckDef[];
249
251
  /**
250
252
  * Index declarations for this table (plain column indexes and/or PowDB
251
- * doc-field expression indexes). Consumed by the PowDB DDL generator
252
- * (`powqlSchemaDDL`) and carried onto `IndexMetadata` by
253
- * `schemaDefToMetadata`; the SQL DDL generators do not consume them yet.
253
+ * doc-field expression indexes). Plain column indexes are emitted as `CREATE [UNIQUE] INDEX` by
254
+ * `schemaToSQL` and added by `schemaDiff`; doc-field indexes are PowDB-only
255
+ * (`powqlSchemaDDL`). Both are carried onto `IndexMetadata` by
256
+ * `schemaDefToMetadata`.
254
257
  */
255
258
  indexes?: readonly SchemaIndexDef[];
256
259
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "turbine-orm",
3
- "version": "0.91.0",
3
+ "version": "0.92.0",
4
4
  "description": "Postgres-native TypeScript ORM, runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. One dependency, no WASM engine",
5
5
  "type": "module",
6
6
  "//exports": "Each subpath declares its types PER CONDITION. A single shared top-level \"types\" resolves to the ESM declarations for `require` too, which is TS1479 (\"is an ES module ... cannot be require()d\") for any CJS consumer on moduleResolution node16/nodenext. The require condition points at dist/cjs, which ships its own {\"type\":\"commonjs\"} package.json, so those declarations are CJS declarations. Gated in CI by publint + @arethetypeswrong/cli + a real .cts consumer typecheck (see the package-types job in ci.yml).",