@farming-labs/orm-sql 0.0.11 → 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
@@ -9,6 +9,7 @@ import {
9
9
  toUniqueLookupWhere,
10
10
  validateUniqueLookupUpdateData
11
11
  } from "@farming-labs/orm";
12
+ var nativeNodeIdentity = /* @__PURE__ */ Symbol("nativeNodeIdentity");
12
13
  var manifestCache = /* @__PURE__ */ new WeakMap();
13
14
  function getManifest(schema) {
14
15
  const cached = manifestCache.get(schema);
@@ -148,12 +149,12 @@ function mergeWhere(...clauses) {
148
149
  AND: defined
149
150
  };
150
151
  }
151
- function compileFieldFilter(model, fieldName, filter, dialect, state) {
152
+ function compileFieldFilter(model, fieldName, filter, dialect, state, tableAlias = model.table) {
152
153
  const field = model.fields[fieldName];
153
154
  if (!field) {
154
155
  throw new Error(`Unknown field "${fieldName}" on model "${model.name}".`);
155
156
  }
156
- const column = `${quoteIdentifier(model.table, dialect)}.${quoteIdentifier(field.column, dialect)}`;
157
+ const column = `${quoteIdentifier(tableAlias, dialect)}.${quoteIdentifier(field.column, dialect)}`;
157
158
  const createValueExpression = (value) => {
158
159
  const placeholder = createPlaceholder(dialect, state, encodeValue(field, dialect, value));
159
160
  if (field.kind === "json" && dialect === "mysql") {
@@ -215,39 +216,39 @@ function compileFieldFilter(model, fieldName, filter, dialect, state) {
215
216
  if (clauses.length === 1) return clauses[0];
216
217
  return `(${clauses.join(" and ")})`;
217
218
  }
218
- function compileWhere(model, where, dialect, state) {
219
+ function compileWhere(model, where, dialect, state, tableAlias = model.table) {
219
220
  if (!where) return void 0;
220
221
  const clauses = [];
221
222
  for (const [key, value] of Object.entries(where)) {
222
223
  if (key === "AND") {
223
224
  const items = Array.isArray(value) ? value : [];
224
225
  if (!items.length) continue;
225
- 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})`);
226
227
  if (nested.length) clauses.push(nested.join(" and "));
227
228
  continue;
228
229
  }
229
230
  if (key === "OR") {
230
231
  const items = Array.isArray(value) ? value : [];
231
232
  if (!items.length) continue;
232
- 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})`);
233
234
  if (nested.length) clauses.push(`(${nested.join(" or ")})`);
234
235
  continue;
235
236
  }
236
237
  if (key === "NOT") {
237
- const nested = compileWhere(model, value, dialect, state);
238
+ const nested = compileWhere(model, value, dialect, state, tableAlias);
238
239
  if (nested) clauses.push(`not (${nested})`);
239
240
  continue;
240
241
  }
241
- clauses.push(compileFieldFilter(model, key, value, dialect, state));
242
+ clauses.push(compileFieldFilter(model, key, value, dialect, state, tableAlias));
242
243
  }
243
244
  if (!clauses.length) return void 0;
244
245
  return clauses.join(" and ");
245
246
  }
246
- function compileOrderBy(model, orderBy, dialect) {
247
+ function compileOrderBy(model, orderBy, dialect, tableAlias = model.table) {
247
248
  if (!orderBy) return "";
248
249
  const parts = Object.entries(orderBy).filter(([fieldName]) => fieldName in model.fields).map(([fieldName, direction]) => {
249
250
  const field = model.fields[fieldName];
250
- 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"}`;
251
252
  });
252
253
  if (!parts.length) return "";
253
254
  return ` order by ${parts.join(", ")}`;
@@ -270,13 +271,14 @@ function compilePagination(dialect, take, skip) {
270
271
  }
271
272
  function buildSelectStatement(model, dialect, args) {
272
273
  const state = { params: [] };
274
+ const tableAlias = args.tableAlias ?? model.table;
273
275
  const selectList = Object.values(model.fields).map(
274
- (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)}`
275
277
  );
276
- let sql = `select ${selectList.join(", ")} from ${quoteIdentifier(model.table, dialect)}`;
277
- 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);
278
280
  if (where) sql += ` where ${where}`;
279
- sql += compileOrderBy(model, args.orderBy, dialect);
281
+ sql += compileOrderBy(model, args.orderBy, dialect, tableAlias);
280
282
  sql += compilePagination(dialect, args.take, args.skip);
281
283
  return { sql, params: state.params };
282
284
  }
@@ -578,6 +580,10 @@ function createMysqlPoolAdapter(pool) {
578
580
  }
579
581
  function createSqlDriver(adapter) {
580
582
  async function loadRows(schema, modelName, args) {
583
+ const nativeRows = await loadRowsWithNativeJoins(schema, modelName, args);
584
+ if (nativeRows) {
585
+ return nativeRows;
586
+ }
581
587
  const manifest = getManifest(schema);
582
588
  const model = manifest.models[modelName];
583
589
  const statement = buildSelectStatement(model, adapter.dialect, args);
@@ -603,6 +609,253 @@ function createSqlDriver(adapter) {
603
609
  const row = result.rows[0];
604
610
  return row ? decodeRow(model, adapter.dialect, row) : null;
605
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
+ }
606
859
  async function projectRow(schema, modelName, row, select) {
607
860
  const manifest = getManifest(schema);
608
861
  const model = manifest.models[modelName];