@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.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);
@@ -77,6 +78,9 @@ function encodeValue(field, dialect, value) {
77
78
  if (dialect === "postgres") return Boolean(value);
78
79
  return value ? 1 : 0;
79
80
  }
81
+ if (field.kind === "integer") {
82
+ return Number(value);
83
+ }
80
84
  if (field.kind === "datetime") {
81
85
  if (value instanceof Date) {
82
86
  if (dialect === "mysql") {
@@ -86,6 +90,12 @@ function encodeValue(field, dialect, value) {
86
90
  }
87
91
  return value;
88
92
  }
93
+ if (field.kind === "json") {
94
+ if (dialect === "postgres") {
95
+ return value;
96
+ }
97
+ return JSON.stringify(value);
98
+ }
89
99
  return value;
90
100
  }
91
101
  function normalizeNaiveSqlDate(value) {
@@ -129,6 +139,19 @@ function decodeValue(field, dialect, value) {
129
139
  }
130
140
  return new Date(String(value));
131
141
  }
142
+ if (field.kind === "integer") {
143
+ return typeof value === "number" ? value : Number(value);
144
+ }
145
+ if (field.kind === "json") {
146
+ if (typeof value === "string") {
147
+ try {
148
+ return JSON.parse(value);
149
+ } catch {
150
+ return value;
151
+ }
152
+ }
153
+ return value;
154
+ }
132
155
  return value;
133
156
  }
134
157
  function decodeRow(model, dialect, row) {
@@ -146,39 +169,36 @@ function mergeWhere(...clauses) {
146
169
  AND: defined
147
170
  };
148
171
  }
149
- function isFilterObject(value) {
150
- return !!value && typeof value === "object" && !(value instanceof Date) && !Array.isArray(value);
151
- }
152
- function compileFieldFilter(model, fieldName, filter, dialect, state) {
172
+ function compileFieldFilter(model, fieldName, filter, dialect, state, tableAlias = model.table) {
153
173
  const field = model.fields[fieldName];
154
174
  if (!field) {
155
175
  throw new Error(`Unknown field "${fieldName}" on model "${model.name}".`);
156
176
  }
157
- const column = `${quoteIdentifier(model.table, dialect)}.${quoteIdentifier(field.column, dialect)}`;
158
- if (!isFilterObject(filter)) {
177
+ const column = `${quoteIdentifier(tableAlias, dialect)}.${quoteIdentifier(field.column, dialect)}`;
178
+ const createValueExpression = (value) => {
179
+ const placeholder = createPlaceholder(dialect, state, encodeValue(field, dialect, value));
180
+ if (field.kind === "json" && dialect === "mysql") {
181
+ return `cast(${placeholder} as json)`;
182
+ }
183
+ return placeholder;
184
+ };
185
+ if (!(0, import_orm.isOperatorFilterObject)(filter)) {
159
186
  if (filter === null) return `${column} is null`;
160
- const placeholder = createPlaceholder(dialect, state, encodeValue(field, dialect, filter));
161
- return `${column} = ${placeholder}`;
187
+ return `${column} = ${createValueExpression(filter)}`;
162
188
  }
163
189
  const clauses = [];
164
190
  if ("eq" in filter) {
165
191
  if (filter.eq === null) {
166
192
  clauses.push(`${column} is null`);
167
193
  } else {
168
- const placeholder = createPlaceholder(dialect, state, encodeValue(field, dialect, filter.eq));
169
- clauses.push(`${column} = ${placeholder}`);
194
+ clauses.push(`${column} = ${createValueExpression(filter.eq)}`);
170
195
  }
171
196
  }
172
197
  if ("not" in filter) {
173
198
  if (filter.not === null) {
174
199
  clauses.push(`${column} is not null`);
175
200
  } else {
176
- const placeholder = createPlaceholder(
177
- dialect,
178
- state,
179
- encodeValue(field, dialect, filter.not)
180
- );
181
- clauses.push(`${column} <> ${placeholder}`);
201
+ clauses.push(`${column} <> ${createValueExpression(filter.not)}`);
182
202
  }
183
203
  }
184
204
  if ("in" in filter) {
@@ -186,9 +206,7 @@ function compileFieldFilter(model, fieldName, filter, dialect, state) {
186
206
  if (!values.length) {
187
207
  clauses.push("1 = 0");
188
208
  } else {
189
- const placeholders = values.map(
190
- (value) => createPlaceholder(dialect, state, encodeValue(field, dialect, value))
191
- );
209
+ const placeholders = values.map((value) => createValueExpression(value));
192
210
  clauses.push(`${column} in (${placeholders.join(", ")})`);
193
211
  }
194
212
  }
@@ -218,39 +236,39 @@ function compileFieldFilter(model, fieldName, filter, dialect, state) {
218
236
  if (clauses.length === 1) return clauses[0];
219
237
  return `(${clauses.join(" and ")})`;
220
238
  }
221
- function compileWhere(model, where, dialect, state) {
239
+ function compileWhere(model, where, dialect, state, tableAlias = model.table) {
222
240
  if (!where) return void 0;
223
241
  const clauses = [];
224
242
  for (const [key, value] of Object.entries(where)) {
225
243
  if (key === "AND") {
226
244
  const items = Array.isArray(value) ? value : [];
227
245
  if (!items.length) continue;
228
- 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})`);
229
247
  if (nested.length) clauses.push(nested.join(" and "));
230
248
  continue;
231
249
  }
232
250
  if (key === "OR") {
233
251
  const items = Array.isArray(value) ? value : [];
234
252
  if (!items.length) continue;
235
- 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})`);
236
254
  if (nested.length) clauses.push(`(${nested.join(" or ")})`);
237
255
  continue;
238
256
  }
239
257
  if (key === "NOT") {
240
- const nested = compileWhere(model, value, dialect, state);
258
+ const nested = compileWhere(model, value, dialect, state, tableAlias);
241
259
  if (nested) clauses.push(`not (${nested})`);
242
260
  continue;
243
261
  }
244
- clauses.push(compileFieldFilter(model, key, value, dialect, state));
262
+ clauses.push(compileFieldFilter(model, key, value, dialect, state, tableAlias));
245
263
  }
246
264
  if (!clauses.length) return void 0;
247
265
  return clauses.join(" and ");
248
266
  }
249
- function compileOrderBy(model, orderBy, dialect) {
267
+ function compileOrderBy(model, orderBy, dialect, tableAlias = model.table) {
250
268
  if (!orderBy) return "";
251
269
  const parts = Object.entries(orderBy).filter(([fieldName]) => fieldName in model.fields).map(([fieldName, direction]) => {
252
270
  const field = model.fields[fieldName];
253
- 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"}`;
254
272
  });
255
273
  if (!parts.length) return "";
256
274
  return ` order by ${parts.join(", ")}`;
@@ -273,13 +291,14 @@ function compilePagination(dialect, take, skip) {
273
291
  }
274
292
  function buildSelectStatement(model, dialect, args) {
275
293
  const state = { params: [] };
294
+ const tableAlias = args.tableAlias ?? model.table;
276
295
  const selectList = Object.values(model.fields).map(
277
- (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)}`
278
297
  );
279
- let sql = `select ${selectList.join(", ")} from ${quoteIdentifier(model.table, dialect)}`;
280
- 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);
281
300
  if (where) sql += ` where ${where}`;
282
- sql += compileOrderBy(model, args.orderBy, dialect);
301
+ sql += compileOrderBy(model, args.orderBy, dialect, tableAlias);
283
302
  sql += compilePagination(dialect, args.take, args.skip);
284
303
  return { sql, params: state.params };
285
304
  }
@@ -581,6 +600,10 @@ function createMysqlPoolAdapter(pool) {
581
600
  }
582
601
  function createSqlDriver(adapter) {
583
602
  async function loadRows(schema, modelName, args) {
603
+ const nativeRows = await loadRowsWithNativeJoins(schema, modelName, args);
604
+ if (nativeRows) {
605
+ return nativeRows;
606
+ }
584
607
  const manifest = getManifest(schema);
585
608
  const model = manifest.models[modelName];
586
609
  const statement = buildSelectStatement(model, adapter.dialect, args);
@@ -606,6 +629,253 @@ function createSqlDriver(adapter) {
606
629
  const row = result.rows[0];
607
630
  return row ? decodeRow(model, adapter.dialect, row) : null;
608
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
+ }
609
879
  async function projectRow(schema, modelName, row, select) {
610
880
  const manifest = getManifest(schema);
611
881
  const model = manifest.models[modelName];