@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.cjs CHANGED
@@ -29,6 +29,7 @@ __export(index_exports, {
29
29
  module.exports = __toCommonJS(index_exports);
30
30
  var import_node_crypto = require("crypto");
31
31
  var import_orm = require("@farming-labs/orm");
32
+ var nativeNodeIdentity = /* @__PURE__ */ Symbol("nativeNodeIdentity");
32
33
  var manifestCache = /* @__PURE__ */ new WeakMap();
33
34
  function getManifest(schema) {
34
35
  const cached = manifestCache.get(schema);
@@ -168,12 +169,12 @@ function mergeWhere(...clauses) {
168
169
  AND: defined
169
170
  };
170
171
  }
171
- function compileFieldFilter(model, fieldName, filter, dialect, state) {
172
+ function compileFieldFilter(model, fieldName, filter, dialect, state, tableAlias = model.table) {
172
173
  const field = model.fields[fieldName];
173
174
  if (!field) {
174
175
  throw new Error(`Unknown field "${fieldName}" on model "${model.name}".`);
175
176
  }
176
- const column = `${quoteIdentifier(model.table, dialect)}.${quoteIdentifier(field.column, dialect)}`;
177
+ const column = `${quoteIdentifier(tableAlias, dialect)}.${quoteIdentifier(field.column, dialect)}`;
177
178
  const createValueExpression = (value) => {
178
179
  const placeholder = createPlaceholder(dialect, state, encodeValue(field, dialect, value));
179
180
  if (field.kind === "json" && dialect === "mysql") {
@@ -235,39 +236,39 @@ function compileFieldFilter(model, fieldName, filter, dialect, state) {
235
236
  if (clauses.length === 1) return clauses[0];
236
237
  return `(${clauses.join(" and ")})`;
237
238
  }
238
- function compileWhere(model, where, dialect, state) {
239
+ function compileWhere(model, where, dialect, state, tableAlias = model.table) {
239
240
  if (!where) return void 0;
240
241
  const clauses = [];
241
242
  for (const [key, value] of Object.entries(where)) {
242
243
  if (key === "AND") {
243
244
  const items = Array.isArray(value) ? value : [];
244
245
  if (!items.length) continue;
245
- const nested = items.map((item) => compileWhere(model, item, dialect, state)).filter(Boolean).map((item) => `(${item})`);
246
+ const nested = items.map((item) => compileWhere(model, item, dialect, state, tableAlias)).filter(Boolean).map((item) => `(${item})`);
246
247
  if (nested.length) clauses.push(nested.join(" and "));
247
248
  continue;
248
249
  }
249
250
  if (key === "OR") {
250
251
  const items = Array.isArray(value) ? value : [];
251
252
  if (!items.length) continue;
252
- const nested = items.map((item) => compileWhere(model, item, dialect, state)).filter(Boolean).map((item) => `(${item})`);
253
+ const nested = items.map((item) => compileWhere(model, item, dialect, state, tableAlias)).filter(Boolean).map((item) => `(${item})`);
253
254
  if (nested.length) clauses.push(`(${nested.join(" or ")})`);
254
255
  continue;
255
256
  }
256
257
  if (key === "NOT") {
257
- const nested = compileWhere(model, value, dialect, state);
258
+ const nested = compileWhere(model, value, dialect, state, tableAlias);
258
259
  if (nested) clauses.push(`not (${nested})`);
259
260
  continue;
260
261
  }
261
- clauses.push(compileFieldFilter(model, key, value, dialect, state));
262
+ clauses.push(compileFieldFilter(model, key, value, dialect, state, tableAlias));
262
263
  }
263
264
  if (!clauses.length) return void 0;
264
265
  return clauses.join(" and ");
265
266
  }
266
- function compileOrderBy(model, orderBy, dialect) {
267
+ function compileOrderBy(model, orderBy, dialect, tableAlias = model.table) {
267
268
  if (!orderBy) return "";
268
269
  const parts = Object.entries(orderBy).filter(([fieldName]) => fieldName in model.fields).map(([fieldName, direction]) => {
269
270
  const field = model.fields[fieldName];
270
- return `${quoteIdentifier(model.table, dialect)}.${quoteIdentifier(field.column, dialect)} ${direction === "desc" ? "desc" : "asc"}`;
271
+ return `${quoteIdentifier(tableAlias, dialect)}.${quoteIdentifier(field.column, dialect)} ${direction === "desc" ? "desc" : "asc"}`;
271
272
  });
272
273
  if (!parts.length) return "";
273
274
  return ` order by ${parts.join(", ")}`;
@@ -290,13 +291,14 @@ function compilePagination(dialect, take, skip) {
290
291
  }
291
292
  function buildSelectStatement(model, dialect, args) {
292
293
  const state = { params: [] };
294
+ const tableAlias = args.tableAlias ?? model.table;
293
295
  const selectList = Object.values(model.fields).map(
294
- (field) => `${quoteIdentifier(model.table, dialect)}.${quoteIdentifier(field.column, dialect)} as ${quoteIdentifier(field.name, dialect)}`
296
+ (field) => `${quoteIdentifier(tableAlias, dialect)}.${quoteIdentifier(field.column, dialect)} as ${quoteIdentifier(field.name, dialect)}`
295
297
  );
296
- let sql = `select ${selectList.join(", ")} from ${quoteIdentifier(model.table, dialect)}`;
297
- const where = compileWhere(model, args.where, dialect, state);
298
+ let sql = `select ${selectList.join(", ")} from ${quoteIdentifier(model.table, dialect)} as ${quoteIdentifier(tableAlias, dialect)}`;
299
+ const where = compileWhere(model, args.where, dialect, state, tableAlias);
298
300
  if (where) sql += ` where ${where}`;
299
- sql += compileOrderBy(model, args.orderBy, dialect);
301
+ sql += compileOrderBy(model, args.orderBy, dialect, tableAlias);
300
302
  sql += compilePagination(dialect, args.take, args.skip);
301
303
  return { sql, params: state.params };
302
304
  }
@@ -598,6 +600,10 @@ function createMysqlPoolAdapter(pool) {
598
600
  }
599
601
  function createSqlDriver(adapter) {
600
602
  async function loadRows(schema, modelName, args) {
603
+ const nativeRows = await loadRowsWithNativeJoins(schema, modelName, args);
604
+ if (nativeRows) {
605
+ return nativeRows;
606
+ }
601
607
  const manifest = getManifest(schema);
602
608
  const model = manifest.models[modelName];
603
609
  const statement = buildSelectStatement(model, adapter.dialect, args);
@@ -623,6 +629,253 @@ function createSqlDriver(adapter) {
623
629
  const row = result.rows[0];
624
630
  return row ? decodeRow(model, adapter.dialect, row) : null;
625
631
  }
632
+ function createNativePresenceAlias(model, alias, includeAllScalars, selectedScalarKeys) {
633
+ const occupiedAliases = new Set(
634
+ (includeAllScalars ? Object.keys(model.fields) : selectedScalarKeys).map(
635
+ (fieldName) => `${alias}__${fieldName}`
636
+ )
637
+ );
638
+ let candidate = `${alias}__orm_presence`;
639
+ let suffix = 0;
640
+ while (occupiedAliases.has(candidate)) {
641
+ suffix += 1;
642
+ candidate = `${alias}__orm_presence_${suffix}`;
643
+ }
644
+ return candidate;
645
+ }
646
+ function buildNativeJoinPlan(schema, modelName, select, aliasState) {
647
+ const manifest = getManifest(schema);
648
+ const model = manifest.models[modelName];
649
+ const alias = `t${aliasState.next++}`;
650
+ const entries = select ? Object.entries(select) : [];
651
+ const selectedScalarKeys = select ? entries.filter(([key, value]) => key in model.fields && value === true).map(([key]) => key) : Object.keys(model.fields);
652
+ const node = {
653
+ modelName,
654
+ model,
655
+ alias,
656
+ presenceAlias: createNativePresenceAlias(model, alias, !select, selectedScalarKeys),
657
+ includeAllScalars: !select,
658
+ selectedScalarKeys,
659
+ children: []
660
+ };
661
+ for (const [key, value] of entries) {
662
+ if (value === void 0 || !(key in schema.models[modelName].relations)) continue;
663
+ const relation = schema.models[modelName].relations[key];
664
+ const relationArgs = value === true ? {} : value;
665
+ if (relationArgs.where !== void 0 || relationArgs.orderBy !== void 0 || relationArgs.take !== void 0 || relationArgs.skip !== void 0) {
666
+ return null;
667
+ }
668
+ const child = buildNativeJoinPlan(
669
+ schema,
670
+ relation.target,
671
+ relationArgs.select,
672
+ aliasState
673
+ );
674
+ if (!child) return null;
675
+ child.relationName = key;
676
+ child.relationKind = relation.kind;
677
+ if (relation.kind === "belongsTo") {
678
+ const sourceField = model.fields[relation.foreignKey];
679
+ if (!sourceField) return null;
680
+ const targetReference = parseReference(sourceField.references);
681
+ const targetFieldName = targetReference?.field ?? identityField(manifest.models[relation.target]).name;
682
+ const targetField = child.model.fields[targetFieldName];
683
+ if (!targetField) return null;
684
+ child.sourceField = sourceField;
685
+ child.targetField = targetField;
686
+ } else if (relation.kind === "hasOne" || relation.kind === "hasMany") {
687
+ const targetForeignField = child.model.fields[relation.foreignKey];
688
+ if (!targetForeignField) return null;
689
+ const sourceReference = parseReference(targetForeignField.references);
690
+ const sourceFieldName = sourceReference?.field ?? identityField(manifest.models[modelName]).name;
691
+ const sourceField = model.fields[sourceFieldName];
692
+ if (!sourceField) return null;
693
+ child.sourceField = sourceField;
694
+ child.targetField = targetForeignField;
695
+ } else {
696
+ const throughModel = manifest.models[relation.through];
697
+ const throughFromField = throughModel.fields[relation.from];
698
+ const throughToField = throughModel.fields[relation.to];
699
+ if (!throughFromField || !throughToField) return null;
700
+ const throughFromReference = parseReference(throughFromField.references);
701
+ const throughToReference = parseReference(throughToField.references);
702
+ const sourceFieldName = throughFromReference?.field ?? identityField(manifest.models[modelName]).name;
703
+ const targetFieldName = throughToReference?.field ?? identityField(child.model).name;
704
+ const sourceField = model.fields[sourceFieldName];
705
+ const targetField = child.model.fields[targetFieldName];
706
+ if (!sourceField || !targetField) return null;
707
+ child.sourceField = sourceField;
708
+ child.targetField = targetField;
709
+ child.throughModel = throughModel;
710
+ child.throughAlias = `t${aliasState.next++}`;
711
+ child.throughFromField = throughFromField;
712
+ child.throughToField = throughToField;
713
+ }
714
+ node.children.push(child);
715
+ }
716
+ return node;
717
+ }
718
+ function hasNativeJoinableRelations(schema, modelName, select) {
719
+ if (!select) return false;
720
+ const plan = buildNativeJoinPlan(schema, modelName, select, { next: 0 });
721
+ return !!plan && plan.children.length > 0;
722
+ }
723
+ function collectNativeJoinSelects(node, selectList) {
724
+ const scalarKeys = node.includeAllScalars ? Object.keys(node.model.fields) : node.selectedScalarKeys;
725
+ for (const fieldName of scalarKeys) {
726
+ const field = node.model.fields[fieldName];
727
+ if (!field) continue;
728
+ selectList.push(
729
+ `${quoteIdentifier(node.alias, adapter.dialect)}.${quoteIdentifier(field.column, adapter.dialect)} as ${quoteIdentifier(`${node.alias}__${field.name}`, adapter.dialect)}`
730
+ );
731
+ }
732
+ const identity = identityField(node.model);
733
+ selectList.push(
734
+ `${quoteIdentifier(node.alias, adapter.dialect)}.${quoteIdentifier(identity.column, adapter.dialect)} as ${quoteIdentifier(node.presenceAlias, adapter.dialect)}`
735
+ );
736
+ for (const child of node.children) {
737
+ collectNativeJoinSelects(child, selectList);
738
+ }
739
+ }
740
+ function collectNativeJoinClauses(node, joins) {
741
+ for (const child of node.children) {
742
+ if (child.relationKind === "manyToMany") {
743
+ joins.push(
744
+ ` 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)}`
745
+ );
746
+ joins.push(
747
+ ` 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)}`
748
+ );
749
+ } else {
750
+ 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)}`;
751
+ 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)}`;
752
+ joins.push(
753
+ ` left join ${quoteIdentifier(child.model.table, adapter.dialect)} as ${quoteIdentifier(child.alias, adapter.dialect)} on ${leftColumn} = ${rightColumn}`
754
+ );
755
+ }
756
+ collectNativeJoinClauses(child, joins);
757
+ }
758
+ }
759
+ function buildNativeJoinRootSource(root, args) {
760
+ const state = { params: [] };
761
+ const sourceAlias = `${root.alias}__src`;
762
+ const selectList = Object.values(root.model.fields).map(
763
+ (field) => `${quoteIdentifier(sourceAlias, adapter.dialect)}.${quoteIdentifier(field.column, adapter.dialect)} as ${quoteIdentifier(field.column, adapter.dialect)}`
764
+ );
765
+ let sql = `select ${selectList.join(", ")} from ${quoteIdentifier(root.model.table, adapter.dialect)} as ${quoteIdentifier(sourceAlias, adapter.dialect)}`;
766
+ const where = compileWhere(root.model, args.where, adapter.dialect, state, sourceAlias);
767
+ if (where) sql += ` where ${where}`;
768
+ sql += compileOrderBy(root.model, args.orderBy, adapter.dialect, sourceAlias);
769
+ sql += compilePagination(adapter.dialect, args.take, args.skip);
770
+ return {
771
+ sql: `(${sql})`,
772
+ params: state.params
773
+ };
774
+ }
775
+ function buildNativeJoinStatement(root, args) {
776
+ const state = { params: [] };
777
+ const selectList = [];
778
+ const joins = [];
779
+ collectNativeJoinSelects(root, selectList);
780
+ collectNativeJoinClauses(root, joins);
781
+ const rootSource = buildNativeJoinRootSource(root, args);
782
+ state.params.push(...rootSource.params);
783
+ let sql = `select ${selectList.join(", ")} from ${rootSource.sql} as ${quoteIdentifier(root.alias, adapter.dialect)}`;
784
+ if (joins.length) sql += joins.join("");
785
+ sql += compileOrderBy(root.model, args.orderBy, adapter.dialect, root.alias);
786
+ return { sql, params: state.params };
787
+ }
788
+ function nodePresenceValue(node, rawRow) {
789
+ return rawRow[node.presenceAlias];
790
+ }
791
+ function projectNativeJoinNode(node, rawRow) {
792
+ if (nodePresenceValue(node, rawRow) == null) {
793
+ return null;
794
+ }
795
+ const output = {};
796
+ Object.defineProperty(output, nativeNodeIdentity, {
797
+ value: rawRow[node.presenceAlias],
798
+ enumerable: false,
799
+ configurable: true
800
+ });
801
+ const scalarKeys = node.includeAllScalars ? Object.keys(node.model.fields) : node.selectedScalarKeys;
802
+ for (const fieldName of scalarKeys) {
803
+ const field = node.model.fields[fieldName];
804
+ if (!field) continue;
805
+ output[field.name] = decodeValue(
806
+ field,
807
+ adapter.dialect,
808
+ rawRow[`${node.alias}__${field.name}`]
809
+ );
810
+ }
811
+ for (const child of node.children) {
812
+ const childValue = projectNativeJoinNode(child, rawRow);
813
+ output[child.relationName] = child.relationKind === "hasMany" || child.relationKind === "manyToMany" ? childValue ? [childValue] : [] : childValue;
814
+ }
815
+ return output;
816
+ }
817
+ function mergeNativeJoinNode(node, target, next) {
818
+ for (const child of node.children) {
819
+ const relationName = child.relationName;
820
+ if (child.relationKind === "hasMany" || child.relationKind === "manyToMany") {
821
+ const targetRows = Array.isArray(target[relationName]) ? target[relationName] : [];
822
+ const nextRows = Array.isArray(next[relationName]) ? next[relationName] : [];
823
+ if (!Array.isArray(target[relationName])) {
824
+ target[relationName] = targetRows;
825
+ }
826
+ for (const nextRow of nextRows) {
827
+ const identity = nextRow[nativeNodeIdentity];
828
+ const existing2 = targetRows.find((entry) => entry[nativeNodeIdentity] === identity);
829
+ if (existing2) {
830
+ mergeNativeJoinNode(child, existing2, nextRow);
831
+ } else {
832
+ targetRows.push(nextRow);
833
+ }
834
+ }
835
+ continue;
836
+ }
837
+ const nextValue = next[relationName];
838
+ if (nextValue === void 0) continue;
839
+ if (nextValue === null) {
840
+ if (!(relationName in target)) {
841
+ target[relationName] = null;
842
+ }
843
+ continue;
844
+ }
845
+ const existing = target[relationName];
846
+ if (!existing || typeof existing !== "object") {
847
+ target[relationName] = nextValue;
848
+ continue;
849
+ }
850
+ mergeNativeJoinNode(child, existing, nextValue);
851
+ }
852
+ }
853
+ async function loadRowsWithNativeJoins(schema, modelName, args) {
854
+ if (!hasNativeJoinableRelations(schema, modelName, args.select)) {
855
+ return null;
856
+ }
857
+ const plan = buildNativeJoinPlan(schema, modelName, args.select, { next: 0 });
858
+ if (!plan || !plan.children.length) {
859
+ return null;
860
+ }
861
+ const statement = buildNativeJoinStatement(plan, args);
862
+ const result = await adapter.query(statement.sql, statement.params);
863
+ const groupedRows = [];
864
+ const groupedByIdentity = /* @__PURE__ */ new Map();
865
+ for (const row of result.rows) {
866
+ const projected = projectNativeJoinNode(plan, row);
867
+ if (!projected) continue;
868
+ const identity = projected[nativeNodeIdentity];
869
+ const existing = groupedByIdentity.get(identity);
870
+ if (existing) {
871
+ mergeNativeJoinNode(plan, existing, projected);
872
+ continue;
873
+ }
874
+ groupedByIdentity.set(identity, projected);
875
+ groupedRows.push(projected);
876
+ }
877
+ return groupedRows;
878
+ }
626
879
  async function projectRow(schema, modelName, row, select) {
627
880
  const manifest = getManifest(schema);
628
881
  const model = manifest.models[modelName];