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