@danceroutine/tango-orm 1.5.0 → 1.7.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/dist/index.d.ts CHANGED
@@ -14,9 +14,9 @@ export type { Adapter, AdapterConfig, DBClient } from './connection/index';
14
14
  export { PostgresAdapter, SqliteAdapter } from './connection/index';
15
15
  export { ModelManager } from './manager/index';
16
16
  export type { ManagerLike } from './manager/index';
17
- export { Q, QBuilder, QueryCompiler, QuerySet } from './query/index';
17
+ export { Q, QBuilder, QueryCompiler, QueryResult, QuerySet } from './query/index';
18
18
  export type { QueryExecutor } from './query/index';
19
- export type { CompiledQuery, Dialect, Direction, FilterInput, FilterKey, FilterValue, LookupType, OrderSpec, OrderToken, QNode, QueryResult, QuerySetState, RelationMeta, TableMeta, WhereClause, } from './query/domain/index';
19
+ export type { CompiledQuery, Dialect, Direction, FilterInput, FilterKey, FilterValue, LookupType, OrderSpec, OrderToken, QNode, QuerySetState, RelationMeta, TableMeta, WhereClause, } from './query/domain/index';
20
20
  export { getTangoRuntime, initializeTangoRuntime, resetTangoRuntime, TangoRuntime } from './runtime/index';
21
21
  export { atomic, UnitOfWork } from './transaction/index';
22
22
  export type { AtomicTransaction, OnCommitOptions, SavepointOptions, SavepointResult } from './transaction/index';
package/dist/index.js CHANGED
@@ -3,11 +3,11 @@ import { PostgresAdapter } from "./PostgresAdapter-CMiEpHya.js";
3
3
  import "./SqliteClient-CjOK9-ki.js";
4
4
  import { SqliteAdapter } from "./SqliteAdapter-CeqhyrPC.js";
5
5
  import { AdapterRegistry, connectDB, connection_exports, getDefaultAdapterRegistry } from "./connection-B_K2ZAf7.js";
6
- import { QBuilder, QueryCompiler, QuerySet, query_exports } from "./query-DYiJ5m_B.js";
6
+ import { QBuilder, QueryCompiler, QueryResult, QuerySet, query_exports } from "./query-FZJoSCg4.js";
7
7
  import { TangoRuntime, getTangoRuntime, initializeTangoRuntime, resetTangoRuntime } from "./defaultRuntime-BPK9kWEW.js";
8
- import { ModelManager } from "./registerModelObjects-B1VzZ072.js";
8
+ import { ModelManager } from "./registerModelObjects-C-1RbUHS.js";
9
9
  import { manager_exports } from "./manager-C6oJ2tAF.js";
10
10
  import { runtime_exports } from "./runtime-ByXbpVBS.js";
11
11
  import { UnitOfWork, atomic, transaction_exports } from "./transaction-Cs0Z9tbW.js";
12
12
 
13
- export { AdapterRegistry, ModelManager, PostgresAdapter, QBuilder as Q, QBuilder, QueryCompiler, QuerySet, SqliteAdapter, TangoRuntime, UnitOfWork, atomic, connectDB, connection_exports as connection, getDefaultAdapterRegistry, getTangoRuntime, initializeTangoRuntime, manager_exports as manager, query_exports as query, resetTangoRuntime, runtime_exports as runtime, transaction_exports as transaction };
13
+ export { AdapterRegistry, ModelManager, PostgresAdapter, QBuilder as Q, QBuilder, QueryCompiler, QueryResult, QuerySet, SqliteAdapter, TangoRuntime, UnitOfWork, atomic, connectDB, connection_exports as connection, getDefaultAdapterRegistry, getTangoRuntime, initializeTangoRuntime, manager_exports as manager, query_exports as query, resetTangoRuntime, runtime_exports as runtime, transaction_exports as transaction };
@@ -1,3 +1,5 @@
1
+ import type { QNode } from '../query/domain/QNode';
2
+ import type { FilterInput } from '../query/domain/FilterInput';
1
3
  import type { QuerySet } from '../query/index';
2
4
  import type { TableMeta } from '../query/domain/index';
3
5
  /**
@@ -6,6 +8,23 @@ import type { TableMeta } from '../query/domain/index';
6
8
  export interface ManagerLike<TModelRow extends Record<string, unknown>, TSourceModel = unknown> {
7
9
  readonly meta: TableMeta;
8
10
  query(): QuerySet<TModelRow, TModelRow, TSourceModel>;
11
+ all(): QuerySet<TModelRow, TModelRow, TSourceModel>;
12
+ getOrCreate(args: {
13
+ where: FilterInput<TModelRow> | QNode<TModelRow>;
14
+ defaults?: Partial<TModelRow>;
15
+ }): Promise<{
16
+ record: TModelRow;
17
+ created: boolean;
18
+ }>;
19
+ updateOrCreate(args: {
20
+ where: FilterInput<TModelRow> | QNode<TModelRow>;
21
+ defaults?: Partial<TModelRow>;
22
+ update?: Partial<TModelRow>;
23
+ }): Promise<{
24
+ record: TModelRow;
25
+ created: boolean;
26
+ updated: boolean;
27
+ }>;
9
28
  findById(id: TModelRow[keyof TModelRow]): Promise<TModelRow | null>;
10
29
  getOrThrow(id: TModelRow[keyof TModelRow]): Promise<TModelRow>;
11
30
  create(input: Partial<TModelRow>): Promise<TModelRow>;
@@ -1,6 +1,7 @@
1
+ import type { QNode } from '../query/domain/QNode';
1
2
  import type { ModelWriteHooks } from '@danceroutine/tango-schema';
2
3
  import type { Model as SchemaModel } from '@danceroutine/tango-schema/domain';
3
- import type { TableMeta } from '../query/domain/index';
4
+ import type { FilterInput, TableMeta } from '../query/domain/index';
4
5
  import type { QuerySet } from '../query/index';
5
6
  import type { TangoRuntime } from '../runtime/TangoRuntime';
6
7
  import type { ManagerLike } from './ManagerLike';
@@ -39,7 +40,29 @@ export declare class ModelManager<TModelRow extends Record<string, unknown>, TSo
39
40
  */
40
41
  static isModelManager<TModelRow extends Record<string, unknown>>(value: unknown): value is ModelManager<TModelRow>;
41
42
  private static createTableMeta;
43
+ private static mergeCreatePayloadFromWhere;
44
+ private static countNonPkValues;
45
+ private static collectPlainFieldsFromQNode;
46
+ private static omitLookupKeysFromAtom;
47
+ private static mergeCompatiblePartials;
42
48
  query(): QuerySet<TModelRow, TModelRow, TSourceModel>;
49
+ all(): QuerySet<TModelRow, TModelRow, TSourceModel>;
50
+ getOrCreate(args: {
51
+ where: FilterInput<TModelRow> | QNode<TModelRow>;
52
+ defaults?: Partial<TModelRow>;
53
+ }): Promise<{
54
+ record: TModelRow;
55
+ created: boolean;
56
+ }>;
57
+ updateOrCreate(args: {
58
+ where: FilterInput<TModelRow> | QNode<TModelRow>;
59
+ defaults?: Partial<TModelRow>;
60
+ update?: Partial<TModelRow>;
61
+ }): Promise<{
62
+ record: TModelRow;
63
+ created: boolean;
64
+ updated: boolean;
65
+ }>;
43
66
  findById(id: TModelRow[keyof TModelRow]): Promise<TModelRow | null>;
44
67
  getOrThrow(id: TModelRow[keyof TModelRow]): Promise<TModelRow>;
45
68
  create(input: Partial<TModelRow>): Promise<TModelRow>;
@@ -1,8 +1,8 @@
1
1
  import "../PostgresClient-BQJZfEOT.js";
2
2
  import "../SqliteClient-CjOK9-ki.js";
3
- import "../query-DYiJ5m_B.js";
3
+ import "../query-FZJoSCg4.js";
4
4
  import "../defaultRuntime-BPK9kWEW.js";
5
- import { ModelManager, registerModelObjects } from "../registerModelObjects-B1VzZ072.js";
5
+ import { ModelManager, registerModelObjects } from "../registerModelObjects-C-1RbUHS.js";
6
6
  import "../manager-C6oJ2tAF.js";
7
7
 
8
8
  export { ModelManager, registerModelObjects };
@@ -1,5 +1,5 @@
1
1
  import { __export } from "./chunk-DLY2FNSh.js";
2
- import { ModelManager, registerModelObjects } from "./registerModelObjects-B1VzZ072.js";
2
+ import { ModelManager, registerModelObjects } from "./registerModelObjects-C-1RbUHS.js";
3
3
 
4
4
  //#region src/manager/index.ts
5
5
  var manager_exports = {};
@@ -3,10 +3,10 @@ import type { Dialect } from './domain/Dialect';
3
3
  import type { QuerySetState } from './domain/QuerySetState';
4
4
  import type { TableMeta } from './domain/TableMeta';
5
5
  import type { QNode } from './domain/QNode';
6
- import type { QueryResult } from './domain/QueryResult';
7
6
  import type { OrderToken } from './domain/OrderToken';
8
7
  import type { FilterInput } from './domain/FilterInput';
9
8
  import type { CompiledQuery } from './domain/CompiledQuery';
9
+ import { QueryResult } from './domain/QueryResult';
10
10
  import type { GeneratedHydratedRelationMap, GeneratedPrefetchRelatedPathKeys, GeneratedSelectRelatedPathKeys, HydratedQueryResult, ManyRelationHydrationCardinality, MaybeHydratedRelationMap, PrefetchRelatedRelations, RelationKeys, SelectRelatedRelations, SingleRelationHydrationCardinality } from './domain/RelationTyping';
11
11
  /**
12
12
  * Query execution seam consumed by `QuerySet`.
@@ -34,6 +34,12 @@ type ProjectedResult<TModel extends Record<string, unknown>, TKeys extends reado
34
34
  * Provides a fluent API for filtering, ordering, pagination, projection, and
35
35
  * nested relation hydration.
36
36
  *
37
+ * Refinements such as `filter`, `orderBy`, `select`, and relation loaders build
38
+ * query state only. SQL runs when you call an evaluation method (`fetch`,
39
+ * `fetchOne`, `count`, `exists`, or `for await` over this queryset). After the
40
+ * first row-returning evaluation, this queryset instance reuses its cached
41
+ * materialized result on later `fetch()` or async-iteration calls.
42
+ *
37
43
  * @template TModel - The full model row type used for query composition
38
44
  * @template TBaseResult - The selected base-row shape returned by execution methods
39
45
  * @template TSourceModel - The source Tango model used for typed relation metadata
@@ -50,16 +56,19 @@ type ProjectedResult<TModel extends Record<string, unknown>, TKeys extends reado
50
56
  * .fetch();
51
57
  * ```
52
58
  */
53
- export declare class QuerySet<TModel extends Record<string, unknown>, TBaseResult extends Record<string, unknown> = TModel, TSourceModel = unknown, THydrated extends Record<string, unknown> = Record<never, never>> {
59
+ export declare class QuerySet<TModel extends Record<string, unknown>, TBaseResult extends Record<string, unknown> = TModel, TSourceModel = unknown, THydrated extends Record<string, unknown> = Record<never, never>> implements AsyncIterable<HydratedQueryResult<TBaseResult, THydrated>> {
60
+ [Symbol.asyncIterator]: () => AsyncIterator<HydratedQueryResult<TBaseResult, THydrated>>;
54
61
  private executor;
55
62
  private state;
56
63
  static readonly BRAND: "tango.orm.query_set";
57
64
  readonly __tangoBrand: typeof QuerySet.BRAND;
65
+ private evaluationCache?;
58
66
  constructor(executor: QueryExecutor<TModel>, state?: QuerySetState<TModel>);
59
67
  /**
60
68
  * Narrow an unknown value to `QuerySet`.
61
69
  */
62
70
  static isQuerySet<TModel extends Record<string, unknown>, TResult extends Record<string, unknown> = TModel>(value: unknown): value is QuerySet<TModel, TResult>;
71
+ private static invertOrderSpec;
63
72
  /**
64
73
  * Add a filter expression to the query.
65
74
  *
@@ -113,6 +122,7 @@ export declare class QuerySet<TModel extends Record<string, unknown>, TBaseResul
113
122
  * app-local registry current.
114
123
  */
115
124
  prefetchRelated<TTargetModel = undefined, const TRelationName extends RelationKeys<PrefetchRelatedRelations<TSourceModel, NoInfer<TTargetModel>>> | GeneratedPrefetchRelatedPathKeys<TSourceModel> = RelationKeys<PrefetchRelatedRelations<TSourceModel, NoInfer<TTargetModel>>> | GeneratedPrefetchRelatedPathKeys<TSourceModel>>(...rels: readonly TRelationName[]): QuerySet<TModel, TBaseResult, TSourceModel, THydrated & MaybeHydratedRelationMap<TSourceModel, PrefetchRelatedRelations<TSourceModel, NoInfer<TTargetModel>>, Extract<TRelationName, RelationKeys<PrefetchRelatedRelations<TSourceModel, NoInfer<TTargetModel>>>>, ManyRelationHydrationCardinality> & GeneratedHydratedRelationMap<TSourceModel, Extract<TRelationName, GeneratedPrefetchRelatedPathKeys<TSourceModel>>>>;
125
+ all(): QuerySet<TModel, TBaseResult, TSourceModel, THydrated>;
116
126
  /**
117
127
  * Execute the query and optionally shape each row.
118
128
  *
@@ -123,6 +133,14 @@ export declare class QuerySet<TModel extends Record<string, unknown>, TBaseResul
123
133
  fetch<Out>(shape: QueryShapeFunction<HydratedQueryResult<TBaseResult, THydrated>, Out>): Promise<QueryResult<Out>>;
124
134
  fetch<Out>(shape: QueryShapeParser<HydratedQueryResult<TBaseResult, THydrated>, Out>): Promise<QueryResult<Out>>;
125
135
  fetch<TShape extends QueryShape<HydratedQueryResult<TBaseResult, THydrated>> | undefined>(shape: TShape): Promise<QueryResult<HydratedQueryResult<TBaseResult, THydrated> | QueryShapeOutput<HydratedQueryResult<TBaseResult, THydrated>, NonNullable<TShape>>>>;
136
+ /**
137
+ * Async iterable surface for `for await (... of queryset)`.
138
+ *
139
+ * Evaluates this queryset on first use by awaiting {@link QuerySet.fetch} without arguments, then
140
+ * yields each element from that {@link QueryResult}. Later async iterations over the same queryset
141
+ * instance reuse the cached materialized result instead of issuing another database round-trip.
142
+ */
143
+ [Symbol.asyncIterator](): AsyncIterator<HydratedQueryResult<TBaseResult, THydrated>>;
126
144
  /**
127
145
  * Execute the query and return the first row, or `null`.
128
146
  *
@@ -133,6 +151,15 @@ export declare class QuerySet<TModel extends Record<string, unknown>, TBaseResul
133
151
  fetchOne<Out>(shape: QueryShapeFunction<HydratedQueryResult<TBaseResult, THydrated>, Out>): Promise<Out | null>;
134
152
  fetchOne<Out>(shape: QueryShapeParser<HydratedQueryResult<TBaseResult, THydrated>, Out>): Promise<Out | null>;
135
153
  fetchOne<TShape extends QueryShape<HydratedQueryResult<TBaseResult, THydrated>> | undefined>(shape: TShape): Promise<HydratedQueryResult<TBaseResult, THydrated> | QueryShapeOutput<HydratedQueryResult<TBaseResult, THydrated>, NonNullable<TShape>> | null>;
154
+ first(): Promise<HydratedQueryResult<TBaseResult, THydrated> | null>;
155
+ first<Out>(shape: QueryShapeFunction<HydratedQueryResult<TBaseResult, THydrated>, Out>): Promise<Out | null>;
156
+ first<Out>(shape: QueryShapeParser<HydratedQueryResult<TBaseResult, THydrated>, Out>): Promise<Out | null>;
157
+ last(): Promise<HydratedQueryResult<TBaseResult, THydrated> | null>;
158
+ last<Out>(shape: QueryShapeFunction<HydratedQueryResult<TBaseResult, THydrated>, Out>): Promise<Out | null>;
159
+ last<Out>(shape: QueryShapeParser<HydratedQueryResult<TBaseResult, THydrated>, Out>): Promise<Out | null>;
160
+ get(q: FilterInput<TModel> | QNode<TModel>): Promise<HydratedQueryResult<TBaseResult, THydrated>>;
161
+ get<Out>(q: FilterInput<TModel> | QNode<TModel>, shape: QueryShapeFunction<HydratedQueryResult<TBaseResult, THydrated>, Out>): Promise<Out>;
162
+ get<Out>(q: FilterInput<TModel> | QNode<TModel>, shape: QueryShapeParser<HydratedQueryResult<TBaseResult, THydrated>, Out>): Promise<Out>;
136
163
  /**
137
164
  * Execute a `COUNT(*)` query for the current filtered state.
138
165
  */
@@ -141,7 +168,10 @@ export declare class QuerySet<TModel extends Record<string, unknown>, TBaseResul
141
168
  * Return whether at least one row matches the current query state.
142
169
  */
143
170
  exists(): Promise<boolean>;
144
- private normalizeRowsForSchemaParsing;
171
+ private shapeFetchedRow;
172
+ private getOrCreateEvaluationCache;
173
+ private evaluateRows;
174
+ private normalizeHydratedRowsForParserShape;
145
175
  private hydrateRows;
146
176
  private hydrateJoinNodesForOwner;
147
177
  private hydratePrefetchNode;
@@ -1,4 +1,35 @@
1
- export interface QueryResult<T> {
2
- results: T[];
3
- nextCursor?: string | null;
1
+ /**
2
+ * Values materialized by {@link QuerySet.fetch}, iterable like an array plus `length`, `map`, `at`, and `toArray`.
3
+ *
4
+ * Prefer iteration or `items` over the deprecated `results` getter, which warns once per process when accessed.
5
+ */
6
+ export declare class QueryResult<T> implements Iterable<T> {
7
+ /**
8
+ * Sync iteration over materialized rows.
9
+ */
10
+ [Symbol.iterator]: () => Iterator<T>;
11
+ static readonly BRAND: "tango.orm.query_result";
12
+ readonly __tangoBrand: typeof QueryResult.BRAND;
13
+ readonly items: readonly T[];
14
+ constructor(items: readonly T[]);
15
+ /**
16
+ * Runtime narrowing for values that may be a plain array or a `QueryResult` instance.
17
+ */
18
+ static isQueryResult<T>(value: unknown): value is QueryResult<T>;
19
+ /**
20
+ * Sync iteration over materialized rows.
21
+ */
22
+ [Symbol.iterator](): Iterator<T>;
23
+ /** Number of materialized rows. */
24
+ get length(): number;
25
+ /** Same as `Array#map` on the materialized rows. */
26
+ map<U>(callbackfn: (value: T, index: number, array: readonly T[]) => U, thisArg?: unknown): U[];
27
+ /** Indexed read with support for negative indices, like `Array#at`. */
28
+ at(index: number): T | undefined;
29
+ /** Returns a shallow copy of the materialized rows as a plain array. */
30
+ toArray(): T[];
31
+ /**
32
+ * @deprecated Use iteration, `length`, `map`, or `toArray()` instead.
33
+ */
34
+ get results(): readonly T[];
4
35
  }
@@ -11,7 +11,7 @@ export type { LookupType } from './LookupType';
11
11
  export type { OrderSpec } from './OrderSpec';
12
12
  export type { OrderToken } from './OrderToken';
13
13
  export type { QNode } from './QNode';
14
- export type { QueryResult } from './QueryResult';
14
+ export { QueryResult } from './QueryResult';
15
15
  export type { QuerySetState } from './QuerySetState';
16
16
  export type { RelationMeta } from './RelationMeta';
17
17
  export type { ForwardSingleRelations, GeneratedHydratedRelationMap, GeneratedPrefetchRelatedPathKeys, GeneratedSelectRelatedPathKeys, HydratedQueryResult, HydratedRelationMap, ManyRelationHydrationCardinality, MaybeHydratedRelationMap, PrefetchRelatedRelations, RelationKeys, RelationHydrationCardinality, ReverseCollectionRelations, ReverseSingleRelations, SelectRelatedRelations, SingleRelationHydrationCardinality, } from './RelationTyping';
@@ -6,6 +6,7 @@ export * as compiler from './compiler/index';
6
6
  export * as domain from './domain/index';
7
7
  export type * from './domain/index';
8
8
  export type { TableMeta } from './domain/index';
9
+ export { QueryResult } from './domain/index';
9
10
  export { QuerySet } from './QuerySet';
10
11
  export type { QueryExecutor } from './QuerySet';
11
12
  export { QBuilder, QBuilder as Q } from './QBuilder';
@@ -1,3 +1,3 @@
1
- import { QBuilder, QueryCompiler, QuerySet, compiler_exports, domain_exports } from "../query-DYiJ5m_B.js";
1
+ import { QBuilder, QueryCompiler, QueryResult, QuerySet, compiler_exports, domain_exports } from "../query-FZJoSCg4.js";
2
2
 
3
- export { QBuilder as Q, QBuilder, QueryCompiler, QuerySet, compiler_exports as compiler, domain_exports as domain };
3
+ export { QBuilder as Q, QBuilder, QueryCompiler, QueryResult, QuerySet, compiler_exports as compiler, domain_exports as domain };
@@ -0,0 +1,3 @@
1
+ import type { FilterInput } from '../domain/FilterInput';
2
+ import type { QNode } from '../domain/QNode';
3
+ export declare function isQNodeLike<T>(value: FilterInput<T> | QNode<T>): value is QNode<T>;
@@ -1,7 +1,16 @@
1
1
  import { __export } from "./chunk-DLY2FNSh.js";
2
- import { SqlSafetyEngine, isError } from "@danceroutine/tango-core";
2
+ import { MultipleObjectsReturned, NotFoundError, SqlSafetyEngine, getLogger, isError } from "@danceroutine/tango-core";
3
3
  import { ModelRegistry } from "@danceroutine/tango-schema";
4
4
 
5
+ //#region src/query/domain/internal/InternalQNodeType.ts
6
+ const InternalQNodeType = {
7
+ ATOM: "atom",
8
+ AND: "and",
9
+ OR: "or",
10
+ NOT: "not"
11
+ };
12
+
13
+ //#endregion
5
14
  //#region src/query/domain/internal/InternalRelationKind.ts
6
15
  const InternalRelationKind = {
7
16
  HAS_MANY: "hasMany",
@@ -83,15 +92,6 @@ const InternalDialect = {
83
92
  SQLITE: "sqlite"
84
93
  };
85
94
 
86
- //#endregion
87
- //#region src/query/domain/internal/InternalQNodeType.ts
88
- const InternalQNodeType = {
89
- ATOM: "atom",
90
- AND: "and",
91
- OR: "or",
92
- NOT: "not"
93
- };
94
-
95
95
  //#endregion
96
96
  //#region src/query/domain/internal/InternalLookupType.ts
97
97
  const InternalLookupType = {
@@ -755,11 +755,62 @@ var QueryCompiler = class QueryCompiler {
755
755
  var compiler_exports = {};
756
756
  __export(compiler_exports, { QueryCompiler: () => QueryCompiler });
757
757
 
758
+ //#endregion
759
+ //#region src/query/domain/QueryResult.ts
760
+ let didWarnDeprecatedResults = false;
761
+ var QueryResult = class QueryResult {
762
+ static BRAND = "tango.orm.query_result";
763
+ __tangoBrand = QueryResult.BRAND;
764
+ items;
765
+ constructor(items) {
766
+ this.items = items;
767
+ }
768
+ /**
769
+ * Runtime narrowing for values that may be a plain array or a `QueryResult` instance.
770
+ */
771
+ static isQueryResult(value) {
772
+ return typeof value === "object" && value !== null && value.__tangoBrand === QueryResult.BRAND;
773
+ }
774
+ /**
775
+ * Sync iteration over materialized rows.
776
+ */
777
+ [Symbol.iterator]() {
778
+ return this.items[Symbol.iterator]();
779
+ }
780
+ /** Number of materialized rows. */
781
+ get length() {
782
+ return this.items.length;
783
+ }
784
+ /** Same as `Array#map` on the materialized rows. */
785
+ map(callbackfn, thisArg) {
786
+ return this.items.map(callbackfn, thisArg);
787
+ }
788
+ /** Indexed read with support for negative indices, like `Array#at`. */
789
+ at(index) {
790
+ return this.items.at(index);
791
+ }
792
+ /** Returns a shallow copy of the materialized rows as a plain array. */
793
+ toArray() {
794
+ return [...this.items];
795
+ }
796
+ /**
797
+ * @deprecated Use iteration, `length`, `map`, or `toArray()` instead.
798
+ */
799
+ get results() {
800
+ if (!didWarnDeprecatedResults) {
801
+ didWarnDeprecatedResults = true;
802
+ getLogger("tango.orm.query_result").warn("`QueryResult.results` is deprecated. Use iteration, `length`, `map`, or `toArray()` instead.");
803
+ }
804
+ return this.items;
805
+ }
806
+ };
807
+
758
808
  //#endregion
759
809
  //#region src/query/domain/index.ts
760
810
  var domain_exports = {};
761
811
  __export(domain_exports, {
762
812
  InternalRelationHydrationCardinality: () => InternalRelationHydrationCardinality,
813
+ QueryResult: () => QueryResult,
763
814
  TableMetaFactory: () => TableMetaFactory
764
815
  });
765
816
 
@@ -770,6 +821,19 @@ const InternalDirection = {
770
821
  DESC: "desc"
771
822
  };
772
823
 
824
+ //#endregion
825
+ //#region src/query/internal/isQNodeLike.ts
826
+ function isQNodeLike(value) {
827
+ if (typeof value !== "object" || value === null || "__tangoBrand" in value) return false;
828
+ switch (value.kind) {
829
+ case InternalQNodeType.ATOM: return "where" in value;
830
+ case InternalQNodeType.AND:
831
+ case InternalQNodeType.OR: return Array.isArray(value.nodes);
832
+ case InternalQNodeType.NOT: return "node" in value;
833
+ default: return false;
834
+ }
835
+ }
836
+
773
837
  //#endregion
774
838
  //#region src/query/QBuilder.ts
775
839
  var QBuilder = class QBuilder {
@@ -809,7 +873,7 @@ var QBuilder = class QBuilder {
809
873
  };
810
874
  }
811
875
  static wrapNode(input) {
812
- if (input.kind) return input;
876
+ if (isQNodeLike(input)) return input;
813
877
  return {
814
878
  kind: InternalQNodeType.ATOM,
815
879
  where: input
@@ -822,6 +886,7 @@ var QBuilder = class QBuilder {
822
886
  var QuerySet = class QuerySet {
823
887
  static BRAND = "tango.orm.query_set";
824
888
  __tangoBrand = QuerySet.BRAND;
889
+ evaluationCache;
825
890
  constructor(executor, state = {}) {
826
891
  this.executor = executor;
827
892
  this.state = state;
@@ -832,13 +897,20 @@ var QuerySet = class QuerySet {
832
897
  static isQuerySet(value) {
833
898
  return typeof value === "object" && value !== null && value.__tangoBrand === QuerySet.BRAND;
834
899
  }
900
+ static invertOrderSpec(order) {
901
+ if (!order?.length) return [];
902
+ return order.map((spec) => ({
903
+ by: spec.by,
904
+ dir: spec.dir === InternalDirection.ASC ? InternalDirection.DESC : InternalDirection.ASC
905
+ }));
906
+ }
835
907
  /**
836
908
  * Add a filter expression to the query.
837
909
  *
838
910
  * Multiple `filter()` calls are composed with `AND`.
839
911
  */
840
912
  filter(q) {
841
- const wrapped = q.kind ? q : {
913
+ const wrapped = isQNodeLike(q) ? q : {
842
914
  kind: InternalQNodeType.ATOM,
843
915
  where: q
844
916
  };
@@ -854,7 +926,7 @@ var QuerySet = class QuerySet {
854
926
  * Exclusions are translated to `NOT (...)` predicates.
855
927
  */
856
928
  exclude(q) {
857
- const wrapped = q.kind ? q : {
929
+ const wrapped = isQNodeLike(q) ? q : {
858
930
  kind: InternalQNodeType.ATOM,
859
931
  where: q
860
932
  };
@@ -937,23 +1009,61 @@ var QuerySet = class QuerySet {
937
1009
  prefetchRelated: [...rels]
938
1010
  });
939
1011
  }
1012
+ all() {
1013
+ return new QuerySet(this.executor, { ...this.state });
1014
+ }
940
1015
  async fetch(shape) {
941
- const compiler = new QueryCompiler(this.executor.meta, this.executor.dialect);
942
- const compiled = compiler.compile(this.state);
943
- const rows = await this.executor.run(compiled);
944
- const normalizedRows = this.normalizeRowsForSchemaParsing(rows, shape);
945
- const hydratedRows = await this.hydrateRows(normalizedRows, compiled);
946
- const projectedRows = hydratedRows;
947
- const results = !shape ? projectedRows : typeof shape === "function" ? projectedRows.map(shape) : projectedRows.map((r) => shape.parse(r));
948
- return {
949
- results,
950
- nextCursor: null
951
- };
1016
+ const baseResult = await this.getOrCreateEvaluationCache();
1017
+ if (!shape) return baseResult;
1018
+ const results = typeof shape === "function" ? baseResult.items.map(shape) : this.normalizeHydratedRowsForParserShape(baseResult.items).map((row) => shape.parse(row));
1019
+ return new QueryResult(results);
1020
+ }
1021
+ /**
1022
+ * Async iterable surface for `for await (... of queryset)`.
1023
+ *
1024
+ * Evaluates this queryset on first use by awaiting {@link QuerySet.fetch} without arguments, then
1025
+ * yields each element from that {@link QueryResult}. Later async iterations over the same queryset
1026
+ * instance reuse the cached materialized result instead of issuing another database round-trip.
1027
+ */
1028
+ async *[Symbol.asyncIterator]() {
1029
+ const result = await this.fetch();
1030
+ for (const row of result) yield row;
952
1031
  }
953
1032
  async fetchOne(shape) {
954
1033
  const limited = this.limit(1);
955
1034
  const result = !shape ? await limited.fetch() : typeof shape === "function" ? await limited.fetch(shape) : await limited.fetch(shape);
956
- return result.results[0] ?? null;
1035
+ for (const row of result) return row;
1036
+ return null;
1037
+ }
1038
+ async first(shape) {
1039
+ return this.fetchOne(shape);
1040
+ }
1041
+ async last(shape) {
1042
+ if (this.state.limit !== undefined || this.state.offset !== undefined) {
1043
+ const page = await this.fetch();
1044
+ const row = page.at(-1);
1045
+ if (!row) return null;
1046
+ return this.shapeFetchedRow(row, shape);
1047
+ }
1048
+ const invertedOrder = QuerySet.invertOrderSpec(this.state.order);
1049
+ const effectiveOrder = invertedOrder.length > 0 ? invertedOrder : [{
1050
+ by: this.executor.meta.pk,
1051
+ dir: InternalDirection.DESC
1052
+ }];
1053
+ const qs = new QuerySet(this.executor, {
1054
+ ...this.state,
1055
+ order: effectiveOrder
1056
+ });
1057
+ return qs.limit(1).fetchOne(shape);
1058
+ }
1059
+ async get(q, shape) {
1060
+ const limited = this.filter(q).limit(2);
1061
+ const page = await limited.fetch();
1062
+ const rows = page.items;
1063
+ const count = rows.length;
1064
+ if (count === 0) throw new NotFoundError(`${this.executor.meta.table}: no matching record`);
1065
+ if (count > 1) throw new MultipleObjectsReturned(`${this.executor.meta.table}: more than one matching record`);
1066
+ return this.shapeFetchedRow(rows[0], shape);
957
1067
  }
958
1068
  /**
959
1069
  * Execute a `COUNT(*)` query for the current filtered state.
@@ -972,10 +1082,31 @@ var QuerySet = class QuerySet {
972
1082
  const count = await this.count();
973
1083
  return count > 0;
974
1084
  }
975
- normalizeRowsForSchemaParsing(rows, shape) {
976
- if (!shape || typeof shape === "function" || this.executor.dialect !== InternalDialect.SQLITE) return rows;
1085
+ shapeFetchedRow(row, shape) {
1086
+ if (!shape) return row;
1087
+ if (typeof shape === "function") return shape(row);
1088
+ const normalizedRow = this.normalizeHydratedRowsForParserShape([row])[0] ?? row;
1089
+ return shape.parse(normalizedRow);
1090
+ }
1091
+ getOrCreateEvaluationCache() {
1092
+ if (!this.evaluationCache) this.evaluationCache = this.evaluateRows().catch((error) => {
1093
+ this.evaluationCache = undefined;
1094
+ throw error;
1095
+ });
1096
+ return this.evaluationCache;
1097
+ }
1098
+ async evaluateRows() {
1099
+ const compiler = new QueryCompiler(this.executor.meta, this.executor.dialect);
1100
+ const compiled = compiler.compile(this.state);
1101
+ const rows = await this.executor.run(compiled);
1102
+ const hydratedRows = await this.hydrateRows(rows, compiled);
1103
+ const projectedRows = hydratedRows;
1104
+ return new QueryResult(projectedRows);
1105
+ }
1106
+ normalizeHydratedRowsForParserShape(rows) {
1107
+ if (this.executor.dialect !== InternalDialect.SQLITE) return [...rows];
977
1108
  const booleanColumns = Object.entries(this.executor.meta.columns).filter(([, value]) => this.isBooleanColumnType(value)).map(([column]) => column);
978
- if (booleanColumns.length === 0) return rows;
1109
+ if (booleanColumns.length === 0) return [...rows];
979
1110
  return rows.map((row) => this.normalizeBooleanColumns(row, booleanColumns));
980
1111
  }
981
1112
  async hydrateRows(rows, compiled) {
@@ -1115,11 +1246,12 @@ __export(query_exports, {
1115
1246
  Q: () => QBuilder,
1116
1247
  QBuilder: () => QBuilder,
1117
1248
  QueryCompiler: () => QueryCompiler,
1249
+ QueryResult: () => QueryResult,
1118
1250
  QuerySet: () => QuerySet,
1119
1251
  compiler: () => compiler_exports,
1120
1252
  domain: () => domain_exports
1121
1253
  });
1122
1254
 
1123
1255
  //#endregion
1124
- export { OrmSqlSafetyAdapter, QBuilder, QueryCompiler, QuerySet, TableMetaFactory, compiler_exports, domain_exports, query_exports };
1125
- //# sourceMappingURL=query-DYiJ5m_B.js.map
1256
+ export { InternalQNodeType, OrmSqlSafetyAdapter, QBuilder, QueryCompiler, QueryResult, QuerySet, TableMetaFactory, compiler_exports, domain_exports, isQNodeLike, query_exports };
1257
+ //# sourceMappingURL=query-FZJoSCg4.js.map