@farming-labs/orm-sql 0.0.10 → 0.0.12

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.js CHANGED
@@ -3,11 +3,13 @@ import { randomUUID } from "crypto";
3
3
  import {
4
4
  createManifest,
5
5
  mergeUniqueLookupCreateData,
6
+ isOperatorFilterObject,
6
7
  requireUniqueLookup,
7
8
  resolveRowIdentityLookup,
8
9
  toUniqueLookupWhere,
9
10
  validateUniqueLookupUpdateData
10
11
  } from "@farming-labs/orm";
12
+ var nativeNodeIdentity = /* @__PURE__ */ Symbol("nativeNodeIdentity");
11
13
  var manifestCache = /* @__PURE__ */ new WeakMap();
12
14
  function getManifest(schema) {
13
15
  const cached = manifestCache.get(schema);
@@ -56,6 +58,9 @@ function encodeValue(field, dialect, value) {
56
58
  if (dialect === "postgres") return Boolean(value);
57
59
  return value ? 1 : 0;
58
60
  }
61
+ if (field.kind === "integer") {
62
+ return Number(value);
63
+ }
59
64
  if (field.kind === "datetime") {
60
65
  if (value instanceof Date) {
61
66
  if (dialect === "mysql") {
@@ -65,6 +70,12 @@ function encodeValue(field, dialect, value) {
65
70
  }
66
71
  return value;
67
72
  }
73
+ if (field.kind === "json") {
74
+ if (dialect === "postgres") {
75
+ return value;
76
+ }
77
+ return JSON.stringify(value);
78
+ }
68
79
  return value;
69
80
  }
70
81
  function normalizeNaiveSqlDate(value) {
@@ -108,6 +119,19 @@ function decodeValue(field, dialect, value) {
108
119
  }
109
120
  return new Date(String(value));
110
121
  }
122
+ if (field.kind === "integer") {
123
+ return typeof value === "number" ? value : Number(value);
124
+ }
125
+ if (field.kind === "json") {
126
+ if (typeof value === "string") {
127
+ try {
128
+ return JSON.parse(value);
129
+ } catch {
130
+ return value;
131
+ }
132
+ }
133
+ return value;
134
+ }
111
135
  return value;
112
136
  }
113
137
  function decodeRow(model, dialect, row) {
@@ -125,39 +149,36 @@ function mergeWhere(...clauses) {
125
149
  AND: defined
126
150
  };
127
151
  }
128
- function isFilterObject(value) {
129
- return !!value && typeof value === "object" && !(value instanceof Date) && !Array.isArray(value);
130
- }
131
- function compileFieldFilter(model, fieldName, filter, dialect, state) {
152
+ function compileFieldFilter(model, fieldName, filter, dialect, state, tableAlias = model.table) {
132
153
  const field = model.fields[fieldName];
133
154
  if (!field) {
134
155
  throw new Error(`Unknown field "${fieldName}" on model "${model.name}".`);
135
156
  }
136
- const column = `${quoteIdentifier(model.table, dialect)}.${quoteIdentifier(field.column, dialect)}`;
137
- if (!isFilterObject(filter)) {
157
+ const column = `${quoteIdentifier(tableAlias, dialect)}.${quoteIdentifier(field.column, dialect)}`;
158
+ const createValueExpression = (value) => {
159
+ const placeholder = createPlaceholder(dialect, state, encodeValue(field, dialect, value));
160
+ if (field.kind === "json" && dialect === "mysql") {
161
+ return `cast(${placeholder} as json)`;
162
+ }
163
+ return placeholder;
164
+ };
165
+ if (!isOperatorFilterObject(filter)) {
138
166
  if (filter === null) return `${column} is null`;
139
- const placeholder = createPlaceholder(dialect, state, encodeValue(field, dialect, filter));
140
- return `${column} = ${placeholder}`;
167
+ return `${column} = ${createValueExpression(filter)}`;
141
168
  }
142
169
  const clauses = [];
143
170
  if ("eq" in filter) {
144
171
  if (filter.eq === null) {
145
172
  clauses.push(`${column} is null`);
146
173
  } else {
147
- const placeholder = createPlaceholder(dialect, state, encodeValue(field, dialect, filter.eq));
148
- clauses.push(`${column} = ${placeholder}`);
174
+ clauses.push(`${column} = ${createValueExpression(filter.eq)}`);
149
175
  }
150
176
  }
151
177
  if ("not" in filter) {
152
178
  if (filter.not === null) {
153
179
  clauses.push(`${column} is not null`);
154
180
  } else {
155
- const placeholder = createPlaceholder(
156
- dialect,
157
- state,
158
- encodeValue(field, dialect, filter.not)
159
- );
160
- clauses.push(`${column} <> ${placeholder}`);
181
+ clauses.push(`${column} <> ${createValueExpression(filter.not)}`);
161
182
  }
162
183
  }
163
184
  if ("in" in filter) {
@@ -165,9 +186,7 @@ function compileFieldFilter(model, fieldName, filter, dialect, state) {
165
186
  if (!values.length) {
166
187
  clauses.push("1 = 0");
167
188
  } else {
168
- const placeholders = values.map(
169
- (value) => createPlaceholder(dialect, state, encodeValue(field, dialect, value))
170
- );
189
+ const placeholders = values.map((value) => createValueExpression(value));
171
190
  clauses.push(`${column} in (${placeholders.join(", ")})`);
172
191
  }
173
192
  }
@@ -197,39 +216,39 @@ function compileFieldFilter(model, fieldName, filter, dialect, state) {
197
216
  if (clauses.length === 1) return clauses[0];
198
217
  return `(${clauses.join(" and ")})`;
199
218
  }
200
- function compileWhere(model, where, dialect, state) {
219
+ function compileWhere(model, where, dialect, state, tableAlias = model.table) {
201
220
  if (!where) return void 0;
202
221
  const clauses = [];
203
222
  for (const [key, value] of Object.entries(where)) {
204
223
  if (key === "AND") {
205
224
  const items = Array.isArray(value) ? value : [];
206
225
  if (!items.length) continue;
207
- const nested = items.map((item) => compileWhere(model, item, dialect, state)).filter(Boolean).map((item) => `(${item})`);
226
+ const nested = items.map((item) => compileWhere(model, item, dialect, state, tableAlias)).filter(Boolean).map((item) => `(${item})`);
208
227
  if (nested.length) clauses.push(nested.join(" and "));
209
228
  continue;
210
229
  }
211
230
  if (key === "OR") {
212
231
  const items = Array.isArray(value) ? value : [];
213
232
  if (!items.length) continue;
214
- const nested = items.map((item) => compileWhere(model, item, dialect, state)).filter(Boolean).map((item) => `(${item})`);
233
+ const nested = items.map((item) => compileWhere(model, item, dialect, state, tableAlias)).filter(Boolean).map((item) => `(${item})`);
215
234
  if (nested.length) clauses.push(`(${nested.join(" or ")})`);
216
235
  continue;
217
236
  }
218
237
  if (key === "NOT") {
219
- const nested = compileWhere(model, value, dialect, state);
238
+ const nested = compileWhere(model, value, dialect, state, tableAlias);
220
239
  if (nested) clauses.push(`not (${nested})`);
221
240
  continue;
222
241
  }
223
- clauses.push(compileFieldFilter(model, key, value, dialect, state));
242
+ clauses.push(compileFieldFilter(model, key, value, dialect, state, tableAlias));
224
243
  }
225
244
  if (!clauses.length) return void 0;
226
245
  return clauses.join(" and ");
227
246
  }
228
- function compileOrderBy(model, orderBy, dialect) {
247
+ function compileOrderBy(model, orderBy, dialect, tableAlias = model.table) {
229
248
  if (!orderBy) return "";
230
249
  const parts = Object.entries(orderBy).filter(([fieldName]) => fieldName in model.fields).map(([fieldName, direction]) => {
231
250
  const field = model.fields[fieldName];
232
- return `${quoteIdentifier(model.table, dialect)}.${quoteIdentifier(field.column, dialect)} ${direction === "desc" ? "desc" : "asc"}`;
251
+ return `${quoteIdentifier(tableAlias, dialect)}.${quoteIdentifier(field.column, dialect)} ${direction === "desc" ? "desc" : "asc"}`;
233
252
  });
234
253
  if (!parts.length) return "";
235
254
  return ` order by ${parts.join(", ")}`;
@@ -252,13 +271,14 @@ function compilePagination(dialect, take, skip) {
252
271
  }
253
272
  function buildSelectStatement(model, dialect, args) {
254
273
  const state = { params: [] };
274
+ const tableAlias = args.tableAlias ?? model.table;
255
275
  const selectList = Object.values(model.fields).map(
256
- (field) => `${quoteIdentifier(model.table, dialect)}.${quoteIdentifier(field.column, dialect)} as ${quoteIdentifier(field.name, dialect)}`
276
+ (field) => `${quoteIdentifier(tableAlias, dialect)}.${quoteIdentifier(field.column, dialect)} as ${quoteIdentifier(field.name, dialect)}`
257
277
  );
258
- let sql = `select ${selectList.join(", ")} from ${quoteIdentifier(model.table, dialect)}`;
259
- const where = compileWhere(model, args.where, dialect, state);
278
+ let sql = `select ${selectList.join(", ")} from ${quoteIdentifier(model.table, dialect)} as ${quoteIdentifier(tableAlias, dialect)}`;
279
+ const where = compileWhere(model, args.where, dialect, state, tableAlias);
260
280
  if (where) sql += ` where ${where}`;
261
- sql += compileOrderBy(model, args.orderBy, dialect);
281
+ sql += compileOrderBy(model, args.orderBy, dialect, tableAlias);
262
282
  sql += compilePagination(dialect, args.take, args.skip);
263
283
  return { sql, params: state.params };
264
284
  }
@@ -560,6 +580,10 @@ function createMysqlPoolAdapter(pool) {
560
580
  }
561
581
  function createSqlDriver(adapter) {
562
582
  async function loadRows(schema, modelName, args) {
583
+ const nativeRows = await loadRowsWithNativeJoins(schema, modelName, args);
584
+ if (nativeRows) {
585
+ return nativeRows;
586
+ }
563
587
  const manifest = getManifest(schema);
564
588
  const model = manifest.models[modelName];
565
589
  const statement = buildSelectStatement(model, adapter.dialect, args);
@@ -585,6 +609,253 @@ function createSqlDriver(adapter) {
585
609
  const row = result.rows[0];
586
610
  return row ? decodeRow(model, adapter.dialect, row) : null;
587
611
  }
612
+ function createNativePresenceAlias(model, alias, includeAllScalars, selectedScalarKeys) {
613
+ const occupiedAliases = new Set(
614
+ (includeAllScalars ? Object.keys(model.fields) : selectedScalarKeys).map(
615
+ (fieldName) => `${alias}__${fieldName}`
616
+ )
617
+ );
618
+ let candidate = `${alias}__orm_presence`;
619
+ let suffix = 0;
620
+ while (occupiedAliases.has(candidate)) {
621
+ suffix += 1;
622
+ candidate = `${alias}__orm_presence_${suffix}`;
623
+ }
624
+ return candidate;
625
+ }
626
+ function buildNativeJoinPlan(schema, modelName, select, aliasState) {
627
+ const manifest = getManifest(schema);
628
+ const model = manifest.models[modelName];
629
+ const alias = `t${aliasState.next++}`;
630
+ const entries = select ? Object.entries(select) : [];
631
+ const selectedScalarKeys = select ? entries.filter(([key, value]) => key in model.fields && value === true).map(([key]) => key) : Object.keys(model.fields);
632
+ const node = {
633
+ modelName,
634
+ model,
635
+ alias,
636
+ presenceAlias: createNativePresenceAlias(model, alias, !select, selectedScalarKeys),
637
+ includeAllScalars: !select,
638
+ selectedScalarKeys,
639
+ children: []
640
+ };
641
+ for (const [key, value] of entries) {
642
+ if (value === void 0 || !(key in schema.models[modelName].relations)) continue;
643
+ const relation = schema.models[modelName].relations[key];
644
+ const relationArgs = value === true ? {} : value;
645
+ if (relationArgs.where !== void 0 || relationArgs.orderBy !== void 0 || relationArgs.take !== void 0 || relationArgs.skip !== void 0) {
646
+ return null;
647
+ }
648
+ const child = buildNativeJoinPlan(
649
+ schema,
650
+ relation.target,
651
+ relationArgs.select,
652
+ aliasState
653
+ );
654
+ if (!child) return null;
655
+ child.relationName = key;
656
+ child.relationKind = relation.kind;
657
+ if (relation.kind === "belongsTo") {
658
+ const sourceField = model.fields[relation.foreignKey];
659
+ if (!sourceField) return null;
660
+ const targetReference = parseReference(sourceField.references);
661
+ const targetFieldName = targetReference?.field ?? identityField(manifest.models[relation.target]).name;
662
+ const targetField = child.model.fields[targetFieldName];
663
+ if (!targetField) return null;
664
+ child.sourceField = sourceField;
665
+ child.targetField = targetField;
666
+ } else if (relation.kind === "hasOne" || relation.kind === "hasMany") {
667
+ const targetForeignField = child.model.fields[relation.foreignKey];
668
+ if (!targetForeignField) return null;
669
+ const sourceReference = parseReference(targetForeignField.references);
670
+ const sourceFieldName = sourceReference?.field ?? identityField(manifest.models[modelName]).name;
671
+ const sourceField = model.fields[sourceFieldName];
672
+ if (!sourceField) return null;
673
+ child.sourceField = sourceField;
674
+ child.targetField = targetForeignField;
675
+ } else {
676
+ const throughModel = manifest.models[relation.through];
677
+ const throughFromField = throughModel.fields[relation.from];
678
+ const throughToField = throughModel.fields[relation.to];
679
+ if (!throughFromField || !throughToField) return null;
680
+ const throughFromReference = parseReference(throughFromField.references);
681
+ const throughToReference = parseReference(throughToField.references);
682
+ const sourceFieldName = throughFromReference?.field ?? identityField(manifest.models[modelName]).name;
683
+ const targetFieldName = throughToReference?.field ?? identityField(child.model).name;
684
+ const sourceField = model.fields[sourceFieldName];
685
+ const targetField = child.model.fields[targetFieldName];
686
+ if (!sourceField || !targetField) return null;
687
+ child.sourceField = sourceField;
688
+ child.targetField = targetField;
689
+ child.throughModel = throughModel;
690
+ child.throughAlias = `t${aliasState.next++}`;
691
+ child.throughFromField = throughFromField;
692
+ child.throughToField = throughToField;
693
+ }
694
+ node.children.push(child);
695
+ }
696
+ return node;
697
+ }
698
+ function hasNativeJoinableRelations(schema, modelName, select) {
699
+ if (!select) return false;
700
+ const plan = buildNativeJoinPlan(schema, modelName, select, { next: 0 });
701
+ return !!plan && plan.children.length > 0;
702
+ }
703
+ function collectNativeJoinSelects(node, selectList) {
704
+ const scalarKeys = node.includeAllScalars ? Object.keys(node.model.fields) : node.selectedScalarKeys;
705
+ for (const fieldName of scalarKeys) {
706
+ const field = node.model.fields[fieldName];
707
+ if (!field) continue;
708
+ selectList.push(
709
+ `${quoteIdentifier(node.alias, adapter.dialect)}.${quoteIdentifier(field.column, adapter.dialect)} as ${quoteIdentifier(`${node.alias}__${field.name}`, adapter.dialect)}`
710
+ );
711
+ }
712
+ const identity = identityField(node.model);
713
+ selectList.push(
714
+ `${quoteIdentifier(node.alias, adapter.dialect)}.${quoteIdentifier(identity.column, adapter.dialect)} as ${quoteIdentifier(node.presenceAlias, adapter.dialect)}`
715
+ );
716
+ for (const child of node.children) {
717
+ collectNativeJoinSelects(child, selectList);
718
+ }
719
+ }
720
+ function collectNativeJoinClauses(node, joins) {
721
+ for (const child of node.children) {
722
+ if (child.relationKind === "manyToMany") {
723
+ joins.push(
724
+ ` left join ${quoteIdentifier(child.throughModel.table, adapter.dialect)} as ${quoteIdentifier(child.throughAlias, adapter.dialect)} on ${quoteIdentifier(node.alias, adapter.dialect)}.${quoteIdentifier(child.sourceField.column, adapter.dialect)} = ${quoteIdentifier(child.throughAlias, adapter.dialect)}.${quoteIdentifier(child.throughFromField.column, adapter.dialect)}`
725
+ );
726
+ joins.push(
727
+ ` left join ${quoteIdentifier(child.model.table, adapter.dialect)} as ${quoteIdentifier(child.alias, adapter.dialect)} on ${quoteIdentifier(child.throughAlias, adapter.dialect)}.${quoteIdentifier(child.throughToField.column, adapter.dialect)} = ${quoteIdentifier(child.alias, adapter.dialect)}.${quoteIdentifier(child.targetField.column, adapter.dialect)}`
728
+ );
729
+ } else {
730
+ const leftColumn = child.relationKind === "belongsTo" ? `${quoteIdentifier(node.alias, adapter.dialect)}.${quoteIdentifier(child.sourceField.column, adapter.dialect)}` : `${quoteIdentifier(child.alias, adapter.dialect)}.${quoteIdentifier(child.targetField.column, adapter.dialect)}`;
731
+ const rightColumn = child.relationKind === "belongsTo" ? `${quoteIdentifier(child.alias, adapter.dialect)}.${quoteIdentifier(child.targetField.column, adapter.dialect)}` : `${quoteIdentifier(node.alias, adapter.dialect)}.${quoteIdentifier(child.sourceField.column, adapter.dialect)}`;
732
+ joins.push(
733
+ ` left join ${quoteIdentifier(child.model.table, adapter.dialect)} as ${quoteIdentifier(child.alias, adapter.dialect)} on ${leftColumn} = ${rightColumn}`
734
+ );
735
+ }
736
+ collectNativeJoinClauses(child, joins);
737
+ }
738
+ }
739
+ function buildNativeJoinRootSource(root, args) {
740
+ const state = { params: [] };
741
+ const sourceAlias = `${root.alias}__src`;
742
+ const selectList = Object.values(root.model.fields).map(
743
+ (field) => `${quoteIdentifier(sourceAlias, adapter.dialect)}.${quoteIdentifier(field.column, adapter.dialect)} as ${quoteIdentifier(field.column, adapter.dialect)}`
744
+ );
745
+ let sql = `select ${selectList.join(", ")} from ${quoteIdentifier(root.model.table, adapter.dialect)} as ${quoteIdentifier(sourceAlias, adapter.dialect)}`;
746
+ const where = compileWhere(root.model, args.where, adapter.dialect, state, sourceAlias);
747
+ if (where) sql += ` where ${where}`;
748
+ sql += compileOrderBy(root.model, args.orderBy, adapter.dialect, sourceAlias);
749
+ sql += compilePagination(adapter.dialect, args.take, args.skip);
750
+ return {
751
+ sql: `(${sql})`,
752
+ params: state.params
753
+ };
754
+ }
755
+ function buildNativeJoinStatement(root, args) {
756
+ const state = { params: [] };
757
+ const selectList = [];
758
+ const joins = [];
759
+ collectNativeJoinSelects(root, selectList);
760
+ collectNativeJoinClauses(root, joins);
761
+ const rootSource = buildNativeJoinRootSource(root, args);
762
+ state.params.push(...rootSource.params);
763
+ let sql = `select ${selectList.join(", ")} from ${rootSource.sql} as ${quoteIdentifier(root.alias, adapter.dialect)}`;
764
+ if (joins.length) sql += joins.join("");
765
+ sql += compileOrderBy(root.model, args.orderBy, adapter.dialect, root.alias);
766
+ return { sql, params: state.params };
767
+ }
768
+ function nodePresenceValue(node, rawRow) {
769
+ return rawRow[node.presenceAlias];
770
+ }
771
+ function projectNativeJoinNode(node, rawRow) {
772
+ if (nodePresenceValue(node, rawRow) == null) {
773
+ return null;
774
+ }
775
+ const output = {};
776
+ Object.defineProperty(output, nativeNodeIdentity, {
777
+ value: rawRow[node.presenceAlias],
778
+ enumerable: false,
779
+ configurable: true
780
+ });
781
+ const scalarKeys = node.includeAllScalars ? Object.keys(node.model.fields) : node.selectedScalarKeys;
782
+ for (const fieldName of scalarKeys) {
783
+ const field = node.model.fields[fieldName];
784
+ if (!field) continue;
785
+ output[field.name] = decodeValue(
786
+ field,
787
+ adapter.dialect,
788
+ rawRow[`${node.alias}__${field.name}`]
789
+ );
790
+ }
791
+ for (const child of node.children) {
792
+ const childValue = projectNativeJoinNode(child, rawRow);
793
+ output[child.relationName] = child.relationKind === "hasMany" || child.relationKind === "manyToMany" ? childValue ? [childValue] : [] : childValue;
794
+ }
795
+ return output;
796
+ }
797
+ function mergeNativeJoinNode(node, target, next) {
798
+ for (const child of node.children) {
799
+ const relationName = child.relationName;
800
+ if (child.relationKind === "hasMany" || child.relationKind === "manyToMany") {
801
+ const targetRows = Array.isArray(target[relationName]) ? target[relationName] : [];
802
+ const nextRows = Array.isArray(next[relationName]) ? next[relationName] : [];
803
+ if (!Array.isArray(target[relationName])) {
804
+ target[relationName] = targetRows;
805
+ }
806
+ for (const nextRow of nextRows) {
807
+ const identity = nextRow[nativeNodeIdentity];
808
+ const existing2 = targetRows.find((entry) => entry[nativeNodeIdentity] === identity);
809
+ if (existing2) {
810
+ mergeNativeJoinNode(child, existing2, nextRow);
811
+ } else {
812
+ targetRows.push(nextRow);
813
+ }
814
+ }
815
+ continue;
816
+ }
817
+ const nextValue = next[relationName];
818
+ if (nextValue === void 0) continue;
819
+ if (nextValue === null) {
820
+ if (!(relationName in target)) {
821
+ target[relationName] = null;
822
+ }
823
+ continue;
824
+ }
825
+ const existing = target[relationName];
826
+ if (!existing || typeof existing !== "object") {
827
+ target[relationName] = nextValue;
828
+ continue;
829
+ }
830
+ mergeNativeJoinNode(child, existing, nextValue);
831
+ }
832
+ }
833
+ async function loadRowsWithNativeJoins(schema, modelName, args) {
834
+ if (!hasNativeJoinableRelations(schema, modelName, args.select)) {
835
+ return null;
836
+ }
837
+ const plan = buildNativeJoinPlan(schema, modelName, args.select, { next: 0 });
838
+ if (!plan || !plan.children.length) {
839
+ return null;
840
+ }
841
+ const statement = buildNativeJoinStatement(plan, args);
842
+ const result = await adapter.query(statement.sql, statement.params);
843
+ const groupedRows = [];
844
+ const groupedByIdentity = /* @__PURE__ */ new Map();
845
+ for (const row of result.rows) {
846
+ const projected = projectNativeJoinNode(plan, row);
847
+ if (!projected) continue;
848
+ const identity = projected[nativeNodeIdentity];
849
+ const existing = groupedByIdentity.get(identity);
850
+ if (existing) {
851
+ mergeNativeJoinNode(plan, existing, projected);
852
+ continue;
853
+ }
854
+ groupedByIdentity.set(identity, projected);
855
+ groupedRows.push(projected);
856
+ }
857
+ return groupedRows;
858
+ }
588
859
  async function projectRow(schema, modelName, row, select) {
589
860
  const manifest = getManifest(schema);
590
861
  const model = manifest.models[modelName];