@apisr/drizzle-model 2.0.2 → 2.0.3

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.
@@ -0,0 +1,334 @@
1
+ import { ProjectionBuilder } from "./projection.mjs";
2
+ import { WhereCompiler } from "./where.mjs";
3
+ import { and, eq } from "drizzle-orm";
4
+
5
+ //#region src/core/query/joins.ts
6
+ /**
7
+ * Executes queries that load related entities via LEFT JOINs.
8
+ *
9
+ * Builds a join tree from the `.with()` descriptor, constructs a single
10
+ * multi-join query, and then groups the flat rows back into a nested
11
+ * object structure matching the requested relations.
12
+ *
13
+ * Usage:
14
+ * ```ts
15
+ * const executor = new JoinExecutor(dialectHelper);
16
+ * const result = await executor.execute(config);
17
+ * ```
18
+ */
19
+ var JoinExecutor = class {
20
+ dialect;
21
+ projection;
22
+ whereCompiler;
23
+ constructor(dialect) {
24
+ this.dialect = dialect;
25
+ this.projection = new ProjectionBuilder();
26
+ this.whereCompiler = new WhereCompiler();
27
+ }
28
+ /**
29
+ * Executes a query with LEFT JOINs for the requested relations,
30
+ * then groups the flat result into a nested object tree.
31
+ *
32
+ * @param config - The full join execution configuration.
33
+ * @returns A single object (when `limitOne`) or an array of grouped results.
34
+ */
35
+ async execute(config) {
36
+ const root = await this.buildJoinTree(config);
37
+ const flatNodes = this.flattenNodes(root);
38
+ const rows = await this.executeQuery(config, root, flatNodes);
39
+ const grouped = this.groupRows(rows, root, flatNodes);
40
+ return config.limitOne ? grouped[0] : grouped;
41
+ }
42
+ /**
43
+ * Builds the full join tree from the `.with()` descriptor.
44
+ *
45
+ * The root node represents the base table. Each key in the `withValue`
46
+ * map becomes a child node, potentially with nested children.
47
+ */
48
+ async buildJoinTree(config) {
49
+ const usedAliasKeys = /* @__PURE__ */ new Set();
50
+ usedAliasKeys.add(`table:${config.baseTableName}`);
51
+ const root = {
52
+ path: [],
53
+ key: "$root",
54
+ relationType: "one",
55
+ sourceTableName: config.baseTableName,
56
+ targetTableName: config.baseTableName,
57
+ sourceTable: config.baseTable,
58
+ targetTable: config.baseTable,
59
+ targetAliasTable: config.baseTable,
60
+ aliasKey: "$base",
61
+ sourceColumns: [],
62
+ targetColumns: [],
63
+ pkField: this.getPrimaryKeyField(config.baseTable),
64
+ children: []
65
+ };
66
+ for (const [key, value] of Object.entries(config.withValue)) {
67
+ if (value !== true && (typeof value !== "object" || value == null)) continue;
68
+ const child = await this.buildNode(config, usedAliasKeys, void 0, config.baseTableName, config.baseTable, key, value, []);
69
+ root.children.push(child);
70
+ }
71
+ return root;
72
+ }
73
+ /**
74
+ * Recursively builds a single join node and its children.
75
+ *
76
+ * Resolves relation metadata, determines whether aliasing is needed,
77
+ * and descends into nested sub-relations.
78
+ */
79
+ async buildNode(config, usedAliasKeys, parent, currentTableName, currentTable, key, value, path) {
80
+ const { whereValue, nestedWith } = this.extractRelationDescriptor(value);
81
+ const relMeta = this.getRelationMeta(config.relations, currentTableName, key);
82
+ const targetTableName = relMeta.targetTableName;
83
+ const targetTable = config.schema[targetTableName] ?? {};
84
+ const aliasKey = this.resolveUniqueAlias(usedAliasKeys, [...path, key]);
85
+ const needsAlias = targetTableName === currentTableName || usedAliasKeys.has(`table:${targetTableName}`);
86
+ usedAliasKeys.add(`table:${targetTableName}`);
87
+ const targetAliasTable = needsAlias ? await this.dialect.createTableAlias(targetTable, aliasKey) : targetTable;
88
+ const whereFilter = whereValue ? this.whereCompiler.compile(targetAliasTable, whereValue) : void 0;
89
+ const node = {
90
+ path: [...path, key],
91
+ key,
92
+ relationType: relMeta.relationType,
93
+ sourceTableName: currentTableName,
94
+ targetTableName,
95
+ sourceTable: currentTable,
96
+ targetTable,
97
+ targetAliasTable,
98
+ aliasKey,
99
+ sourceColumns: relMeta.sourceColumns ?? [],
100
+ targetColumns: relMeta.targetColumns ?? [],
101
+ pkField: this.getPrimaryKeyField(targetAliasTable),
102
+ parent,
103
+ children: [],
104
+ whereFilter
105
+ };
106
+ if (nestedWith && typeof nestedWith === "object") for (const [childKey, childVal] of Object.entries(nestedWith)) {
107
+ if (childVal !== true && (typeof childVal !== "object" || childVal == null)) continue;
108
+ const child = await this.buildNode(config, usedAliasKeys, node, targetTableName, targetAliasTable, childKey, childVal, [...path, key]);
109
+ node.children.push(child);
110
+ }
111
+ return node;
112
+ }
113
+ /**
114
+ * Builds and executes the multi-join SELECT query.
115
+ *
116
+ * Constructs a select map namespaced by alias key (base + each join),
117
+ * applies LEFT JOINs in preorder, and optionally limits to one row.
118
+ */
119
+ async executeQuery(config, root, nodes) {
120
+ const selectMap = { base: this.projection.build(root.targetAliasTable, config.select, config.exclude).selectMap };
121
+ for (const node of nodes) selectMap[node.aliasKey] = this.projection.extractColumns(node.targetAliasTable);
122
+ let query = config.db.select(selectMap);
123
+ query = query.from(config.baseTable);
124
+ if (config.whereSql) query = query.where(config.whereSql);
125
+ for (const node of nodes) {
126
+ const onCondition = this.buildJoinOn(node);
127
+ query = query.leftJoin(node.targetAliasTable, onCondition);
128
+ }
129
+ if (config.limitOne) query = query.limit(1);
130
+ return await query;
131
+ }
132
+ /**
133
+ * Groups flat joined rows back into a nested object structure.
134
+ *
135
+ * Uses the base table's primary key to deduplicate base rows, then
136
+ * attaches relation data to the correct parent in the tree.
137
+ */
138
+ groupRows(rows, root, nodes) {
139
+ const basePk = root.pkField;
140
+ const baseMap = /* @__PURE__ */ new Map();
141
+ const manyIndexByPath = /* @__PURE__ */ new Map();
142
+ for (const row of rows) {
143
+ const baseRow = row.base;
144
+ const baseId = baseRow[basePk];
145
+ if (baseId === void 0) continue;
146
+ const baseObj = this.getOrCreateBase(baseMap, baseId, baseRow);
147
+ for (const node of nodes) {
148
+ const data = row[node.aliasKey];
149
+ const relPath = node.path.join(".");
150
+ const parentObj = this.resolveParentObject(node, baseObj, manyIndexByPath);
151
+ if (this.isAllNull(data)) {
152
+ this.ensureContainer(parentObj, node);
153
+ continue;
154
+ }
155
+ const pk = data[node.pkField];
156
+ if (node.relationType === "one") parentObj[node.key] = { ...data };
157
+ else {
158
+ this.ensureManyArray(parentObj, node.key);
159
+ const indexMap = this.getOrCreateIndexMap(manyIndexByPath, relPath);
160
+ if (!indexMap.has(pk)) {
161
+ const obj = { ...data };
162
+ indexMap.set(pk, obj);
163
+ parentObj[node.key].push(obj);
164
+ }
165
+ }
166
+ }
167
+ }
168
+ return Array.from(baseMap.values());
169
+ }
170
+ /**
171
+ * Builds the ON clause for a single join node.
172
+ *
173
+ * Maps source columns to their corresponding aliased target columns
174
+ * and produces an equality (or multi-equality with AND) condition.
175
+ */
176
+ buildJoinOn(node) {
177
+ const parts = node.sourceColumns.map((src, i) => {
178
+ const tgt = node.targetColumns[i];
179
+ const tgtKey = Object.entries(node.targetTable).find(([, v]) => v === tgt)?.[0];
180
+ return eq(tgtKey ? node.targetAliasTable[tgtKey] : tgt, src);
181
+ });
182
+ if (node.whereFilter) parts.push(node.whereFilter);
183
+ return parts.length === 1 ? parts[0] : and(...parts);
184
+ }
185
+ /**
186
+ * Flattens the join tree into a preorder list (excluding the root).
187
+ *
188
+ * The order matches the LEFT JOIN application order in the query.
189
+ */
190
+ flattenNodes(root) {
191
+ const nodes = [];
192
+ const walk = (node) => {
193
+ for (const child of node.children) {
194
+ nodes.push(child);
195
+ walk(child);
196
+ }
197
+ };
198
+ walk(root);
199
+ return nodes;
200
+ }
201
+ /**
202
+ * Generates a unique alias key for a join node, avoiding collisions
203
+ * with previously used keys.
204
+ */
205
+ resolveUniqueAlias(usedKeys, path) {
206
+ const base = path.join("__");
207
+ let alias = base;
208
+ let counter = 1;
209
+ while (usedKeys.has(alias)) alias = `${base}_${counter++}`;
210
+ usedKeys.add(alias);
211
+ return alias;
212
+ }
213
+ /**
214
+ * Retrieves the relation metadata for a given table and relation key.
215
+ *
216
+ * @throws {Error} When the relation is not found in the schema metadata.
217
+ */
218
+ getRelationMeta(relations, tableName, key) {
219
+ const relMeta = (relations[tableName]?.relations)?.[key];
220
+ if (!relMeta) throw new Error(`Unknown relation '${key}' on table '${tableName}'.`);
221
+ return relMeta;
222
+ }
223
+ /**
224
+ * Detects the primary key field name of a Drizzle table.
225
+ *
226
+ * Tries (in order):
227
+ * 1. A column with `primary === true`.
228
+ * 2. A column with `config.primaryKey === true`.
229
+ * 3. A field named `"id"`.
230
+ * 4. The first Drizzle column found.
231
+ */
232
+ getPrimaryKeyField(table) {
233
+ for (const [key, value] of Object.entries(table)) {
234
+ if (!this.isDrizzleColumn(value)) continue;
235
+ const col = value;
236
+ if (col.primary === true) return key;
237
+ if (col.config?.primaryKey === true) return key;
238
+ }
239
+ if ("id" in table) return "id";
240
+ return Object.keys(table).find((k) => this.isDrizzleColumn(table[k])) ?? "id";
241
+ }
242
+ /** Checks whether a value is a Drizzle column reference. */
243
+ isDrizzleColumn(value) {
244
+ return !!value && typeof value === "object" && typeof value.getSQL === "function";
245
+ }
246
+ /**
247
+ * Extracts relation where clause and nested relations from a `.with()` value.
248
+ *
249
+ * Handles three cases:
250
+ * - `true` → no filter, no nested relations.
251
+ * - A ModelRuntime (has `$model === "model"`) → extract `$where`, no nested.
252
+ * - A model descriptor (`__modelRelation: true`) → extract `whereValue` and `with`.
253
+ * - A plain object → treat as nested relation map.
254
+ */
255
+ extractRelationDescriptor(value) {
256
+ if (value === true || value == null) return {
257
+ whereValue: void 0,
258
+ nestedWith: void 0
259
+ };
260
+ if (typeof value !== "object") return {
261
+ whereValue: void 0,
262
+ nestedWith: void 0
263
+ };
264
+ const rec = value;
265
+ if (rec.$model === "model") return {
266
+ whereValue: rec.$where,
267
+ nestedWith: void 0
268
+ };
269
+ if (rec.__modelRelation === true) return {
270
+ whereValue: rec.whereValue,
271
+ nestedWith: rec.with
272
+ };
273
+ return {
274
+ whereValue: void 0,
275
+ nestedWith: value
276
+ };
277
+ }
278
+ /**
279
+ * Gets or creates the base-row object for a given primary key.
280
+ */
281
+ getOrCreateBase(baseMap, baseId, baseRow) {
282
+ const existing = baseMap.get(baseId);
283
+ if (existing) return existing;
284
+ const created = { ...baseRow };
285
+ baseMap.set(baseId, created);
286
+ return created;
287
+ }
288
+ /**
289
+ * Resolves the parent object that a join node's data should attach to.
290
+ *
291
+ * For root-level relations, the parent is the base row. For nested
292
+ * relations, it is the last inserted instance of the parent node.
293
+ */
294
+ resolveParentObject(node, baseObj, manyIndexByPath) {
295
+ if (!node.parent) return baseObj;
296
+ const parentPath = node.parent.path.join(".");
297
+ if (!parentPath) return baseObj;
298
+ const parentIndex = manyIndexByPath.get(parentPath);
299
+ if (parentIndex && parentIndex.size > 0) return Array.from(parentIndex.values()).at(-1);
300
+ return baseObj[node.parent.key] ?? baseObj;
301
+ }
302
+ /** Checks whether all values in a row are `null` or `undefined`. */
303
+ isAllNull(obj) {
304
+ if (!obj || typeof obj !== "object") return true;
305
+ for (const value of Object.values(obj)) if (value !== null && value !== void 0) return false;
306
+ return true;
307
+ }
308
+ /**
309
+ * Ensures the correct empty container exists on the parent for a node.
310
+ *
311
+ * Creates `[]` for many-relations and `null` for one-relations.
312
+ */
313
+ ensureContainer(parentObj, node) {
314
+ if (node.relationType === "one") {
315
+ if (!(node.key in parentObj)) parentObj[node.key] = null;
316
+ } else this.ensureManyArray(parentObj, node.key);
317
+ }
318
+ /** Ensures `parentObj[key]` is an array. */
319
+ ensureManyArray(parentObj, key) {
320
+ if (!Array.isArray(parentObj[key])) parentObj[key] = [];
321
+ }
322
+ /** Gets or creates a deduplication index map for a many-relation path. */
323
+ getOrCreateIndexMap(manyIndexByPath, relPath) {
324
+ let indexMap = manyIndexByPath.get(relPath);
325
+ if (!indexMap) {
326
+ indexMap = /* @__PURE__ */ new Map();
327
+ manyIndexByPath.set(relPath, indexMap);
328
+ }
329
+ return indexMap;
330
+ }
331
+ };
332
+
333
+ //#endregion
334
+ export { JoinExecutor };
@@ -0,0 +1,78 @@
1
+ //#region src/core/query/projection.ts
2
+ /**
3
+ * Builds column projections for Drizzle `select()` calls.
4
+ *
5
+ * Resolves which columns should be included in the SQL query
6
+ * based on user-supplied `select` and `exclude` maps.
7
+ */
8
+ var ProjectionBuilder = class {
9
+ /**
10
+ * Builds a select map from a Drizzle table, optionally filtered
11
+ * by a `select` whitelist or an `exclude` blacklist.
12
+ *
13
+ * Priority:
14
+ * 1. If `select` is provided, only the listed columns are included.
15
+ * 2. If `exclude` is provided, all columns *except* the listed ones are included.
16
+ * 3. If neither is provided, all columns are included.
17
+ *
18
+ * Falls back to all columns when the filter would result in an empty map.
19
+ *
20
+ * @param table - The Drizzle table object whose columns are introspected.
21
+ * @param select - Optional whitelist: `{ columnName: true }`.
22
+ * @param exclude - Optional blacklist: `{ columnName: true }`.
23
+ * @returns A {@link ProjectionResult} containing the resolved column map.
24
+ */
25
+ build(table, select, exclude) {
26
+ const allColumns = this.extractColumns(table);
27
+ if (select && typeof select === "object") return this.buildFromSelect(allColumns, select);
28
+ if (exclude && typeof exclude === "object") return this.buildFromExclude(allColumns, exclude);
29
+ return { selectMap: allColumns };
30
+ }
31
+ /**
32
+ * Extracts a column map for a table, including only actual Drizzle columns.
33
+ *
34
+ * Useful for building join select maps where every column of an
35
+ * aliased table needs to be enumerated.
36
+ *
37
+ * @param table - The Drizzle table (or aliased table) to extract from.
38
+ * @returns A record mapping column names to their Drizzle column references.
39
+ */
40
+ extractColumns(table) {
41
+ const columns = {};
42
+ for (const [key, value] of Object.entries(table)) if (this.isDrizzleColumn(value)) columns[key] = value;
43
+ return columns;
44
+ }
45
+ /**
46
+ * Builds a select map by picking only the columns listed in `select`.
47
+ *
48
+ * Falls back to all columns if the resulting map is empty.
49
+ */
50
+ buildFromSelect(allColumns, select) {
51
+ const picked = {};
52
+ for (const [key, value] of Object.entries(select)) if (value === true && key in allColumns) picked[key] = allColumns[key];
53
+ if (Object.keys(picked).length > 0) return { selectMap: picked };
54
+ return { selectMap: allColumns };
55
+ }
56
+ /**
57
+ * Builds a select map by omitting columns listed in `exclude`.
58
+ *
59
+ * Falls back to all columns if everything would be excluded.
60
+ */
61
+ buildFromExclude(allColumns, exclude) {
62
+ const remaining = { ...allColumns };
63
+ for (const [key, value] of Object.entries(exclude)) if (value === true) delete remaining[key];
64
+ if (Object.keys(remaining).length > 0) return { selectMap: remaining };
65
+ return { selectMap: allColumns };
66
+ }
67
+ /**
68
+ * Checks whether a value is a Drizzle column reference.
69
+ *
70
+ * Drizzle columns expose a `.getSQL()` method used for query building.
71
+ */
72
+ isDrizzleColumn(value) {
73
+ return !!value && typeof value === "object" && typeof value.getSQL === "function";
74
+ }
75
+ };
76
+
77
+ //#endregion
78
+ export { ProjectionBuilder };
@@ -0,0 +1,265 @@
1
+ import { and, between, eq, gt, gte, ilike, inArray, isNull, like, lt, lte, ne, notBetween, notInArray, or } from "drizzle-orm";
2
+
3
+ //#region src/core/query/where.ts
4
+ /**
5
+ * Compiles user-facing where objects into Drizzle SQL conditions.
6
+ *
7
+ * Handles plain column equality, operator objects (`eq`, `gt`, `like`, …),
8
+ * logical combinators (`and`, `or`), and the `esc()` escape-hatch values.
9
+ *
10
+ * Designed as a stateless utility — every method receives its inputs
11
+ * explicitly so the class can be instantiated once and reused.
12
+ */
13
+ var WhereCompiler = class {
14
+ /**
15
+ * Compiles a where value against a set of table fields into a Drizzle SQL condition.
16
+ *
17
+ * Accepts multiple forms:
18
+ * - A raw Drizzle `SQL` expression (passed through as-is).
19
+ * - A plain object mapping column names to values or operator descriptors.
20
+ * - `undefined` / `null` / falsy (returns `undefined`).
21
+ *
22
+ * @param fields - The Drizzle column map for the table (e.g. `schema.users`).
23
+ * @param where - The user-supplied where clause.
24
+ * @returns A compiled `SQL` fragment, or `undefined` when no conditions apply.
25
+ */
26
+ compile(fields, where) {
27
+ if (!where) return;
28
+ if (typeof where !== "object" || this.isPromiseLike(where)) return where;
29
+ if (this.isDrizzleSql(where)) return where;
30
+ return this.compileObject(fields, where);
31
+ }
32
+ /**
33
+ * Merges two independent where sources (e.g. model-level + call-level)
34
+ * into a single `AND` condition.
35
+ *
36
+ * Either source may be `undefined`; the other is returned unchanged.
37
+ * When both are present they are joined with `AND`.
38
+ *
39
+ * @param fields - The Drizzle column map.
40
+ * @param optionsWhere - The where clause defined on the model options.
41
+ * @param stateWhere - The where clause set via `.where()` at call-site.
42
+ * @returns The merged SQL condition, or `undefined`.
43
+ */
44
+ compileEffective(fields, optionsWhere, stateWhere) {
45
+ const base = this.compile(fields, optionsWhere);
46
+ const extra = this.compile(fields, stateWhere);
47
+ if (base && extra) return and(base, extra);
48
+ return base ?? extra;
49
+ }
50
+ /**
51
+ * Compiles a plain object where clause against the table's column map.
52
+ *
53
+ * Each key in `where` is matched to a column in `fields`.
54
+ * The value is compiled via {@link compileColumnValue}.
55
+ *
56
+ * @param fields - The Drizzle column map.
57
+ * @param where - A record of column-name → condition entries.
58
+ * @returns The combined `SQL` condition, or `undefined`.
59
+ */
60
+ compileObject(fields, where) {
61
+ const parts = [];
62
+ for (const [key, value] of Object.entries(where)) {
63
+ if (value === void 0) continue;
64
+ const column = fields[key];
65
+ if (column) {
66
+ const sql = this.compileColumnValue(column, value);
67
+ if (sql) parts.push(sql);
68
+ continue;
69
+ }
70
+ if (value && typeof value === "object") throw new Error(`Relation where is not implemented yet for key '${key}'.`);
71
+ }
72
+ return this.combineWithAnd(parts);
73
+ }
74
+ /**
75
+ * Compiles a single column's condition into an SQL fragment.
76
+ *
77
+ * Supports:
78
+ * - Escaped values produced by `esc()`.
79
+ * - Operator objects (`{ eq, not, gt, in, like, … }`).
80
+ * - Logical combinators (`{ or: [...], and: [...] }`).
81
+ * - Plain scalar values (compiled as equality).
82
+ *
83
+ * @param column - The Drizzle column reference.
84
+ * @param value - The condition value (scalar, operator object, or escaped).
85
+ * @returns A compiled `SQL` fragment, or `undefined`.
86
+ */
87
+ compileColumnValue(column, value) {
88
+ if (this.isEscapedValue(value)) return this.compileEscapedValue(column, value);
89
+ if (value && typeof value === "object" && !Array.isArray(value)) return this.compileOperatorObject(column, value);
90
+ return eq(column, value);
91
+ }
92
+ /**
93
+ * Compiles an operator object (e.g. `{ gt: 5, lt: 10 }`) for a column.
94
+ *
95
+ * Iterates through all recognised operator keys and produces
96
+ * individual SQL fragments, then combines them with `AND`.
97
+ */
98
+ compileOperatorObject(column, value) {
99
+ const parts = [];
100
+ this.compileEquality(column, value, parts);
101
+ this.compileComparison(column, value, parts);
102
+ this.compileRange(column, value, parts);
103
+ this.compilePattern(column, value, parts);
104
+ this.compileSet(column, value, parts);
105
+ this.compileNull(column, value, parts);
106
+ this.compileLogical(column, value, parts);
107
+ return this.combineWithAnd(parts);
108
+ }
109
+ /** Handles `eq` and `equal` operators. */
110
+ compileEquality(column, value, parts) {
111
+ if ("eq" in value) {
112
+ const u = this.unwrapEscaped(column, value.eq);
113
+ this.pushIfDefined(parts, u.sql ?? eq(column, u.value));
114
+ }
115
+ if ("equal" in value) {
116
+ const u = this.unwrapEscaped(column, value.equal);
117
+ this.pushIfDefined(parts, u.sql ?? eq(column, u.value));
118
+ }
119
+ if ("not" in value) {
120
+ const u = this.unwrapEscaped(column, value.not);
121
+ this.pushIfDefined(parts, u.sql ?? ne(column, u.value));
122
+ }
123
+ }
124
+ /** Handles `gt`, `gte`, `lt`, `lte` operators. */
125
+ compileComparison(column, value, parts) {
126
+ if ("gt" in value) {
127
+ const u = this.unwrapEscaped(column, value.gt);
128
+ this.pushIfDefined(parts, u.sql ?? gt(column, u.value));
129
+ }
130
+ if ("gte" in value) {
131
+ const u = this.unwrapEscaped(column, value.gte);
132
+ this.pushIfDefined(parts, u.sql ?? gte(column, u.value));
133
+ }
134
+ if ("lt" in value) {
135
+ const u = this.unwrapEscaped(column, value.lt);
136
+ this.pushIfDefined(parts, u.sql ?? lt(column, u.value));
137
+ }
138
+ if ("lte" in value) {
139
+ const u = this.unwrapEscaped(column, value.lte);
140
+ this.pushIfDefined(parts, u.sql ?? lte(column, u.value));
141
+ }
142
+ }
143
+ /** Handles `between` and `notBetween` operators. */
144
+ compileRange(column, value, parts) {
145
+ if ("between" in value) {
146
+ const pair = value.between;
147
+ if (pair) {
148
+ const a = this.unwrapEscaped(column, pair[0]);
149
+ const b = this.unwrapEscaped(column, pair[1]);
150
+ this.pushIfDefined(parts, a.sql);
151
+ this.pushIfDefined(parts, b.sql);
152
+ this.pushIfDefined(parts, between(column, a.value, b.value));
153
+ }
154
+ }
155
+ if ("notBetween" in value) {
156
+ const pair = value.notBetween;
157
+ if (pair) {
158
+ const a = this.unwrapEscaped(column, pair[0]);
159
+ const b = this.unwrapEscaped(column, pair[1]);
160
+ this.pushIfDefined(parts, a.sql);
161
+ this.pushIfDefined(parts, b.sql);
162
+ this.pushIfDefined(parts, notBetween(column, a.value, b.value));
163
+ }
164
+ }
165
+ }
166
+ /** Handles `like` and `ilike` operators. */
167
+ compilePattern(column, value, parts) {
168
+ if ("like" in value) {
169
+ const u = this.unwrapEscaped(column, value.like);
170
+ this.pushIfDefined(parts, u.sql ?? like(column, u.value));
171
+ }
172
+ if ("ilike" in value) {
173
+ const u = this.unwrapEscaped(column, value.ilike);
174
+ this.pushIfDefined(parts, u.sql ?? ilike(column, u.value));
175
+ }
176
+ }
177
+ /** Handles `in` and `nin` (not-in) operators. */
178
+ compileSet(column, value, parts) {
179
+ if ("in" in value) this.compileArrayOperator(column, value.in, parts, inArray);
180
+ if ("nin" in value) this.compileArrayOperator(column, value.nin, parts, notInArray);
181
+ }
182
+ /** Handles `isNull` operator. */
183
+ compileNull(column, value, parts) {
184
+ if ("isNull" in value && value.isNull) this.pushIfDefined(parts, isNull(column));
185
+ }
186
+ /** Handles `or` and `and` logical combinators at the column level. */
187
+ compileLogical(column, value, parts) {
188
+ if (Array.isArray(value.or)) {
189
+ const sub = value.or.map((item) => this.compileColumnValue(column, item)).filter(Boolean);
190
+ if (sub.length > 0) this.pushIfDefined(parts, or(...sub));
191
+ }
192
+ if (Array.isArray(value.and)) {
193
+ const sub = value.and.map((item) => this.compileColumnValue(column, item)).filter(Boolean);
194
+ if (sub.length > 0) this.pushIfDefined(parts, and(...sub));
195
+ }
196
+ }
197
+ /**
198
+ * Compiles an array-based set operator (`IN` or `NOT IN`).
199
+ *
200
+ * Each element is unwrapped; raw SQL fragments are combined with `OR`,
201
+ * plain values are passed to the set operator.
202
+ */
203
+ compileArrayOperator(column, items, parts, setFn) {
204
+ if (!items) return;
205
+ const unwrapped = items.map((item) => this.unwrapEscaped(column, item));
206
+ const sqlFragments = unwrapped.map((u) => u.sql).filter(Boolean);
207
+ const plainValues = unwrapped.map((u) => u.value).filter((v) => v !== void 0);
208
+ if (sqlFragments.length > 0) this.pushIfDefined(parts, or(...sqlFragments));
209
+ if (plainValues.length > 0) this.pushIfDefined(parts, setFn(column, plainValues));
210
+ }
211
+ /**
212
+ * Compiles an `esc()`-produced escaped value.
213
+ *
214
+ * If the escaped value carries an explicit operator, it is invoked
215
+ * with the column. Otherwise, implicit equality is applied.
216
+ */
217
+ compileEscapedValue(column, value) {
218
+ if ("__kind" in value && value.__kind === "esc-op") {
219
+ const escaped = value;
220
+ return escaped.op(column, escaped.value);
221
+ }
222
+ return eq(column, value.equal);
223
+ }
224
+ /**
225
+ * Unwraps a potentially escaped value into either a raw SQL fragment
226
+ * or a plain value suitable for parameterised queries.
227
+ */
228
+ unwrapEscaped(column, value) {
229
+ if (!this.isEscapedValue(value)) return { value };
230
+ if ("__kind" in value && value.__kind === "esc-op") {
231
+ const escaped = value;
232
+ return { sql: escaped.op(column, escaped.value) };
233
+ }
234
+ return { value: value.equal };
235
+ }
236
+ /** Checks whether a value is an `esc()` descriptor. */
237
+ isEscapedValue(value) {
238
+ return !!value && typeof value === "object" && ("equal" in value || value.__kind === "esc-op");
239
+ }
240
+ /** Checks whether a value exposes a Drizzle `.getSQL()` method. */
241
+ isDrizzleSql(value) {
242
+ return !!value && typeof value === "object" && typeof value.getSQL === "function";
243
+ }
244
+ /** Checks whether a value is thenable (a promise). */
245
+ isPromiseLike(value) {
246
+ return !!value && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
247
+ }
248
+ /** Pushes an SQL fragment into `parts` only when it is defined. */
249
+ pushIfDefined(parts, sql) {
250
+ if (sql) parts.push(sql);
251
+ }
252
+ /**
253
+ * Combines an array of SQL fragments with `AND`.
254
+ *
255
+ * Returns `undefined` for an empty array, the single element
256
+ * for a one-item array, or a full `AND(…)` expression otherwise.
257
+ */
258
+ combineWithAnd(parts) {
259
+ if (parts.length === 0) return;
260
+ return parts.length === 1 ? parts[0] : and(...parts);
261
+ }
262
+ };
263
+
264
+ //#endregion
265
+ export { WhereCompiler };