@farming-labs/orm-sql 0.0.11 → 0.0.13

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
  }
@@ -576,8 +578,17 @@ function createMysqlPoolAdapter(pool) {
576
578
  }
577
579
  };
578
580
  }
579
- function createSqlDriver(adapter) {
581
+ function createSqlDriver(adapter, handle) {
582
+ const resolvedHandle = handle ?? {
583
+ kind: "sql",
584
+ client: adapter,
585
+ dialect: adapter.dialect
586
+ };
580
587
  async function loadRows(schema, modelName, args) {
588
+ const nativeRows = await loadRowsWithNativeJoins(schema, modelName, args);
589
+ if (nativeRows) {
590
+ return nativeRows;
591
+ }
581
592
  const manifest = getManifest(schema);
582
593
  const model = manifest.models[modelName];
583
594
  const statement = buildSelectStatement(model, adapter.dialect, args);
@@ -603,6 +614,253 @@ function createSqlDriver(adapter) {
603
614
  const row = result.rows[0];
604
615
  return row ? decodeRow(model, adapter.dialect, row) : null;
605
616
  }
617
+ function createNativePresenceAlias(model, alias, includeAllScalars, selectedScalarKeys) {
618
+ const occupiedAliases = new Set(
619
+ (includeAllScalars ? Object.keys(model.fields) : selectedScalarKeys).map(
620
+ (fieldName) => `${alias}__${fieldName}`
621
+ )
622
+ );
623
+ let candidate = `${alias}__orm_presence`;
624
+ let suffix = 0;
625
+ while (occupiedAliases.has(candidate)) {
626
+ suffix += 1;
627
+ candidate = `${alias}__orm_presence_${suffix}`;
628
+ }
629
+ return candidate;
630
+ }
631
+ function buildNativeJoinPlan(schema, modelName, select, aliasState) {
632
+ const manifest = getManifest(schema);
633
+ const model = manifest.models[modelName];
634
+ const alias = `t${aliasState.next++}`;
635
+ const entries = select ? Object.entries(select) : [];
636
+ const selectedScalarKeys = select ? entries.filter(([key, value]) => key in model.fields && value === true).map(([key]) => key) : Object.keys(model.fields);
637
+ const node = {
638
+ modelName,
639
+ model,
640
+ alias,
641
+ presenceAlias: createNativePresenceAlias(model, alias, !select, selectedScalarKeys),
642
+ includeAllScalars: !select,
643
+ selectedScalarKeys,
644
+ children: []
645
+ };
646
+ for (const [key, value] of entries) {
647
+ if (value === void 0 || !(key in schema.models[modelName].relations)) continue;
648
+ const relation = schema.models[modelName].relations[key];
649
+ const relationArgs = value === true ? {} : value;
650
+ if (relationArgs.where !== void 0 || relationArgs.orderBy !== void 0 || relationArgs.take !== void 0 || relationArgs.skip !== void 0) {
651
+ return null;
652
+ }
653
+ const child = buildNativeJoinPlan(
654
+ schema,
655
+ relation.target,
656
+ relationArgs.select,
657
+ aliasState
658
+ );
659
+ if (!child) return null;
660
+ child.relationName = key;
661
+ child.relationKind = relation.kind;
662
+ if (relation.kind === "belongsTo") {
663
+ const sourceField = model.fields[relation.foreignKey];
664
+ if (!sourceField) return null;
665
+ const targetReference = parseReference(sourceField.references);
666
+ const targetFieldName = targetReference?.field ?? identityField(manifest.models[relation.target]).name;
667
+ const targetField = child.model.fields[targetFieldName];
668
+ if (!targetField) return null;
669
+ child.sourceField = sourceField;
670
+ child.targetField = targetField;
671
+ } else if (relation.kind === "hasOne" || relation.kind === "hasMany") {
672
+ const targetForeignField = child.model.fields[relation.foreignKey];
673
+ if (!targetForeignField) return null;
674
+ const sourceReference = parseReference(targetForeignField.references);
675
+ const sourceFieldName = sourceReference?.field ?? identityField(manifest.models[modelName]).name;
676
+ const sourceField = model.fields[sourceFieldName];
677
+ if (!sourceField) return null;
678
+ child.sourceField = sourceField;
679
+ child.targetField = targetForeignField;
680
+ } else {
681
+ const throughModel = manifest.models[relation.through];
682
+ const throughFromField = throughModel.fields[relation.from];
683
+ const throughToField = throughModel.fields[relation.to];
684
+ if (!throughFromField || !throughToField) return null;
685
+ const throughFromReference = parseReference(throughFromField.references);
686
+ const throughToReference = parseReference(throughToField.references);
687
+ const sourceFieldName = throughFromReference?.field ?? identityField(manifest.models[modelName]).name;
688
+ const targetFieldName = throughToReference?.field ?? identityField(child.model).name;
689
+ const sourceField = model.fields[sourceFieldName];
690
+ const targetField = child.model.fields[targetFieldName];
691
+ if (!sourceField || !targetField) return null;
692
+ child.sourceField = sourceField;
693
+ child.targetField = targetField;
694
+ child.throughModel = throughModel;
695
+ child.throughAlias = `t${aliasState.next++}`;
696
+ child.throughFromField = throughFromField;
697
+ child.throughToField = throughToField;
698
+ }
699
+ node.children.push(child);
700
+ }
701
+ return node;
702
+ }
703
+ function hasNativeJoinableRelations(schema, modelName, select) {
704
+ if (!select) return false;
705
+ const plan = buildNativeJoinPlan(schema, modelName, select, { next: 0 });
706
+ return !!plan && plan.children.length > 0;
707
+ }
708
+ function collectNativeJoinSelects(node, selectList) {
709
+ const scalarKeys = node.includeAllScalars ? Object.keys(node.model.fields) : node.selectedScalarKeys;
710
+ for (const fieldName of scalarKeys) {
711
+ const field = node.model.fields[fieldName];
712
+ if (!field) continue;
713
+ selectList.push(
714
+ `${quoteIdentifier(node.alias, adapter.dialect)}.${quoteIdentifier(field.column, adapter.dialect)} as ${quoteIdentifier(`${node.alias}__${field.name}`, adapter.dialect)}`
715
+ );
716
+ }
717
+ const identity = identityField(node.model);
718
+ selectList.push(
719
+ `${quoteIdentifier(node.alias, adapter.dialect)}.${quoteIdentifier(identity.column, adapter.dialect)} as ${quoteIdentifier(node.presenceAlias, adapter.dialect)}`
720
+ );
721
+ for (const child of node.children) {
722
+ collectNativeJoinSelects(child, selectList);
723
+ }
724
+ }
725
+ function collectNativeJoinClauses(node, joins) {
726
+ for (const child of node.children) {
727
+ if (child.relationKind === "manyToMany") {
728
+ joins.push(
729
+ ` 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)}`
730
+ );
731
+ joins.push(
732
+ ` 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)}`
733
+ );
734
+ } else {
735
+ 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)}`;
736
+ 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)}`;
737
+ joins.push(
738
+ ` left join ${quoteIdentifier(child.model.table, adapter.dialect)} as ${quoteIdentifier(child.alias, adapter.dialect)} on ${leftColumn} = ${rightColumn}`
739
+ );
740
+ }
741
+ collectNativeJoinClauses(child, joins);
742
+ }
743
+ }
744
+ function buildNativeJoinRootSource(root, args) {
745
+ const state = { params: [] };
746
+ const sourceAlias = `${root.alias}__src`;
747
+ const selectList = Object.values(root.model.fields).map(
748
+ (field) => `${quoteIdentifier(sourceAlias, adapter.dialect)}.${quoteIdentifier(field.column, adapter.dialect)} as ${quoteIdentifier(field.column, adapter.dialect)}`
749
+ );
750
+ let sql = `select ${selectList.join(", ")} from ${quoteIdentifier(root.model.table, adapter.dialect)} as ${quoteIdentifier(sourceAlias, adapter.dialect)}`;
751
+ const where = compileWhere(root.model, args.where, adapter.dialect, state, sourceAlias);
752
+ if (where) sql += ` where ${where}`;
753
+ sql += compileOrderBy(root.model, args.orderBy, adapter.dialect, sourceAlias);
754
+ sql += compilePagination(adapter.dialect, args.take, args.skip);
755
+ return {
756
+ sql: `(${sql})`,
757
+ params: state.params
758
+ };
759
+ }
760
+ function buildNativeJoinStatement(root, args) {
761
+ const state = { params: [] };
762
+ const selectList = [];
763
+ const joins = [];
764
+ collectNativeJoinSelects(root, selectList);
765
+ collectNativeJoinClauses(root, joins);
766
+ const rootSource = buildNativeJoinRootSource(root, args);
767
+ state.params.push(...rootSource.params);
768
+ let sql = `select ${selectList.join(", ")} from ${rootSource.sql} as ${quoteIdentifier(root.alias, adapter.dialect)}`;
769
+ if (joins.length) sql += joins.join("");
770
+ sql += compileOrderBy(root.model, args.orderBy, adapter.dialect, root.alias);
771
+ return { sql, params: state.params };
772
+ }
773
+ function nodePresenceValue(node, rawRow) {
774
+ return rawRow[node.presenceAlias];
775
+ }
776
+ function projectNativeJoinNode(node, rawRow) {
777
+ if (nodePresenceValue(node, rawRow) == null) {
778
+ return null;
779
+ }
780
+ const output = {};
781
+ Object.defineProperty(output, nativeNodeIdentity, {
782
+ value: rawRow[node.presenceAlias],
783
+ enumerable: false,
784
+ configurable: true
785
+ });
786
+ const scalarKeys = node.includeAllScalars ? Object.keys(node.model.fields) : node.selectedScalarKeys;
787
+ for (const fieldName of scalarKeys) {
788
+ const field = node.model.fields[fieldName];
789
+ if (!field) continue;
790
+ output[field.name] = decodeValue(
791
+ field,
792
+ adapter.dialect,
793
+ rawRow[`${node.alias}__${field.name}`]
794
+ );
795
+ }
796
+ for (const child of node.children) {
797
+ const childValue = projectNativeJoinNode(child, rawRow);
798
+ output[child.relationName] = child.relationKind === "hasMany" || child.relationKind === "manyToMany" ? childValue ? [childValue] : [] : childValue;
799
+ }
800
+ return output;
801
+ }
802
+ function mergeNativeJoinNode(node, target, next) {
803
+ for (const child of node.children) {
804
+ const relationName = child.relationName;
805
+ if (child.relationKind === "hasMany" || child.relationKind === "manyToMany") {
806
+ const targetRows = Array.isArray(target[relationName]) ? target[relationName] : [];
807
+ const nextRows = Array.isArray(next[relationName]) ? next[relationName] : [];
808
+ if (!Array.isArray(target[relationName])) {
809
+ target[relationName] = targetRows;
810
+ }
811
+ for (const nextRow of nextRows) {
812
+ const identity = nextRow[nativeNodeIdentity];
813
+ const existing2 = targetRows.find((entry) => entry[nativeNodeIdentity] === identity);
814
+ if (existing2) {
815
+ mergeNativeJoinNode(child, existing2, nextRow);
816
+ } else {
817
+ targetRows.push(nextRow);
818
+ }
819
+ }
820
+ continue;
821
+ }
822
+ const nextValue = next[relationName];
823
+ if (nextValue === void 0) continue;
824
+ if (nextValue === null) {
825
+ if (!(relationName in target)) {
826
+ target[relationName] = null;
827
+ }
828
+ continue;
829
+ }
830
+ const existing = target[relationName];
831
+ if (!existing || typeof existing !== "object") {
832
+ target[relationName] = nextValue;
833
+ continue;
834
+ }
835
+ mergeNativeJoinNode(child, existing, nextValue);
836
+ }
837
+ }
838
+ async function loadRowsWithNativeJoins(schema, modelName, args) {
839
+ if (!hasNativeJoinableRelations(schema, modelName, args.select)) {
840
+ return null;
841
+ }
842
+ const plan = buildNativeJoinPlan(schema, modelName, args.select, { next: 0 });
843
+ if (!plan || !plan.children.length) {
844
+ return null;
845
+ }
846
+ const statement = buildNativeJoinStatement(plan, args);
847
+ const result = await adapter.query(statement.sql, statement.params);
848
+ const groupedRows = [];
849
+ const groupedByIdentity = /* @__PURE__ */ new Map();
850
+ for (const row of result.rows) {
851
+ const projected = projectNativeJoinNode(plan, row);
852
+ if (!projected) continue;
853
+ const identity = projected[nativeNodeIdentity];
854
+ const existing = groupedByIdentity.get(identity);
855
+ if (existing) {
856
+ mergeNativeJoinNode(plan, existing, projected);
857
+ continue;
858
+ }
859
+ groupedByIdentity.set(identity, projected);
860
+ groupedRows.push(projected);
861
+ }
862
+ return groupedRows;
863
+ }
606
864
  async function projectRow(schema, modelName, row, select) {
607
865
  const manifest = getManifest(schema);
608
866
  const model = manifest.models[modelName];
@@ -720,6 +978,7 @@ function createSqlDriver(adapter) {
720
978
  });
721
979
  }
722
980
  const driver = {
981
+ handle: resolvedHandle,
723
982
  async findMany(schema, model, args) {
724
983
  return loadRows(schema, model, args);
725
984
  },
@@ -866,26 +1125,56 @@ function createSqlDriver(adapter) {
866
1125
  return result.affectedRows;
867
1126
  },
868
1127
  async transaction(_schema, run) {
869
- return adapter.transaction(async (txAdapter) => run(createSqlDriver(txAdapter)));
1128
+ return adapter.transaction(
1129
+ async (txAdapter) => run(createSqlDriver(txAdapter, resolvedHandle))
1130
+ );
870
1131
  }
871
1132
  };
872
1133
  return driver;
873
1134
  }
874
1135
  function createSqliteDriver(database) {
875
- return createSqlDriver(createSqliteAdapter(database));
1136
+ return createSqlDriver(
1137
+ createSqliteAdapter(database),
1138
+ {
1139
+ kind: "sql",
1140
+ client: database,
1141
+ dialect: "sqlite"
1142
+ }
1143
+ );
876
1144
  }
877
- function createSqlDriverFromAdapter(adapter) {
878
- return createSqlDriver(adapter);
1145
+ function createSqlDriverFromAdapter(adapter, handle) {
1146
+ return createSqlDriver(adapter, handle);
879
1147
  }
880
1148
  function createPgPoolDriver(pool) {
881
- return createSqlDriver(createPgPoolAdapter(pool));
1149
+ return createSqlDriver(
1150
+ createPgPoolAdapter(pool),
1151
+ {
1152
+ kind: "sql",
1153
+ client: pool,
1154
+ dialect: "postgres"
1155
+ }
1156
+ );
882
1157
  }
883
1158
  function createPgClientDriver(client) {
884
- return createSqlDriver(createPgTransactionalAdapter(client));
1159
+ return createSqlDriver(
1160
+ createPgTransactionalAdapter(client),
1161
+ {
1162
+ kind: "sql",
1163
+ client,
1164
+ dialect: "postgres"
1165
+ }
1166
+ );
885
1167
  }
886
1168
  function createMysqlDriver(poolOrConnection) {
887
1169
  const adapter = "getConnection" in poolOrConnection ? createMysqlPoolAdapter(poolOrConnection) : createMysqlTransactionalAdapter(poolOrConnection);
888
- return createSqlDriver(adapter);
1170
+ return createSqlDriver(
1171
+ adapter,
1172
+ {
1173
+ kind: "sql",
1174
+ client: poolOrConnection,
1175
+ dialect: "mysql"
1176
+ }
1177
+ );
889
1178
  }
890
1179
  export {
891
1180
  createMysqlDriver,