@dbcube/query-builder 5.1.5 → 5.2.1

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
@@ -1,5 +1,7 @@
1
1
  // src/lib/Database.ts
2
2
  import fs from "fs";
3
+ import path2 from "path";
4
+ import { randomUUID } from "crypto";
3
5
  import { QueryEngine, ComputedFieldProcessor, TriggerProcessor } from "@dbcube/core";
4
6
 
5
7
  // src/lib/Trigger.ts
@@ -36,7 +38,10 @@ var Trigger = class {
36
38
  const pathFile = path.resolve(process.cwd(), "dbcube", "triggers", `${trigger.database_ref}_${trigger.table_ref}_${trigger.type}.js`);
37
39
  const requireUrl = typeof __filename !== "undefined" ? __filename : process.cwd();
38
40
  const require2 = createRequire(requireUrl);
39
- delete require2.cache[require2.resolve(pathFile)];
41
+ try {
42
+ delete require2.cache[require2.resolve(pathFile)];
43
+ } catch {
44
+ }
40
45
  const triggerModule = require2(pathFile);
41
46
  const dataProcess = triggerModule.default || triggerModule;
42
47
  await dataProcess({ db: this.instance, oldData: row, newData: row });
@@ -53,6 +58,7 @@ var Database = class _Database {
53
58
  engine;
54
59
  computedFields;
55
60
  triggers;
61
+ txId = null;
56
62
  constructor(name) {
57
63
  this.name = name;
58
64
  const engine = new QueryEngine(name);
@@ -60,6 +66,57 @@ var Database = class _Database {
60
66
  this.computedFields = [];
61
67
  this.triggers = [];
62
68
  }
69
+ /**
70
+ * Executes raw SQL (MySQL/PostgreSQL/SQLite) or a raw command document (MongoDB)
71
+ * with bound parameters. The escape hatch for anything the builder doesn't cover.
72
+ *
73
+ * @example
74
+ * const rows = await db.raw('SELECT * FROM users WHERE age > ?', [25]);
75
+ * await db.raw('CREATE INDEX idx_users_email ON users(email)');
76
+ */
77
+ async raw(query, params = []) {
78
+ const response = await this.engine.rawQuery(query, params, this.txId ?? void 0);
79
+ if (response.status != 200) {
80
+ returnFormattedError(response.status, response.message);
81
+ throw new Error(response.message);
82
+ }
83
+ return response.data;
84
+ }
85
+ /**
86
+ * Runs a callback inside a database transaction. Everything executed through
87
+ * the received connection is committed atomically; any thrown error rolls
88
+ * the whole transaction back.
89
+ *
90
+ * Requires daemon mode (enabled by default with an up-to-date query-engine).
91
+ *
92
+ * @example
93
+ * await db.transaction(async (trx) => {
94
+ * await trx.table('accounts').where('id', '=', 1).update({ balance: 50 });
95
+ * await trx.table('accounts').where('id', '=', 2).update({ balance: 150 });
96
+ * });
97
+ */
98
+ async transaction(callback) {
99
+ const txId = await this.engine.beginTransaction();
100
+ const trx = new _Database(this.name);
101
+ trx.engine = this.engine;
102
+ trx.computedFields = this.computedFields;
103
+ trx.triggers = this.triggers;
104
+ trx.txId = txId;
105
+ try {
106
+ const result = await callback(trx);
107
+ await this.engine.commitTransaction(txId);
108
+ return result;
109
+ } catch (error) {
110
+ try {
111
+ await this.engine.rollbackTransaction(txId);
112
+ } catch (rollbackError) {
113
+ console.error(
114
+ `[dbcube] Rollback of transaction ${txId} failed: ${rollbackError?.message ?? rollbackError}`
115
+ );
116
+ }
117
+ throw error;
118
+ }
119
+ }
63
120
  async useComputes() {
64
121
  const newDatabase = new _Database(this.name);
65
122
  const arrayComputedFields = await ComputedFieldProcessor.getComputedFields(this.name);
@@ -80,10 +137,9 @@ var Database = class _Database {
80
137
  if (response.status != 200) {
81
138
  returnFormattedError(response.status, response.message);
82
139
  }
83
- return response.data;
84
140
  }
85
141
  async disconnect() {
86
- return this.engine.run("query_engine", [
142
+ await this.engine.run("query_engine", [
87
143
  "--action",
88
144
  "disconnect"
89
145
  ]);
@@ -126,7 +182,7 @@ var Database = class _Database {
126
182
  * const columns = await db.table('users').columns().get();
127
183
  */
128
184
  table(tableName) {
129
- return new Table(this, this.name, tableName, this.engine, this.computedFields, this.triggers);
185
+ return new Table(this, this.name, tableName, this.engine, this.computedFields, this.triggers, this.txId);
130
186
  }
131
187
  setComputedFields(computedFields) {
132
188
  this.computedFields = computedFields;
@@ -142,12 +198,19 @@ var Table = class _Table {
142
198
  computedFields = [];
143
199
  trigger;
144
200
  triggers;
145
- constructor(instance, databaseName, tableName, engine = null, computedFields = [], triggers = []) {
201
+ txId = null;
202
+ relations = [];
203
+ /** Builders de grupo mutan en sitio (ver clone()) */
204
+ _mutable = false;
205
+ instance;
206
+ constructor(instance, databaseName, tableName, engine, computedFields = [], triggers = [], txId = null) {
146
207
  this.engine = engine;
208
+ this.instance = instance;
147
209
  this.computedFields = computedFields;
148
210
  this.triggers = triggers;
149
211
  this.trigger = new Trigger(instance, databaseName, triggers);
150
212
  this.nextType = "AND";
213
+ this.txId = txId;
151
214
  this.dml = {
152
215
  type: "select",
153
216
  database: databaseName,
@@ -161,7 +224,8 @@ var Table = class _Table {
161
224
  limit: null,
162
225
  offset: null,
163
226
  data: null,
164
- aggregation: null
227
+ aggregation: null,
228
+ having: []
165
229
  };
166
230
  }
167
231
  /**
@@ -218,7 +282,8 @@ var Table = class _Table {
218
282
  */
219
283
  whereGroup(callback) {
220
284
  const clone = this.clone();
221
- const groupQuery = new _Table(this, clone.dml.database, clone.dml.table, clone.engine);
285
+ const groupQuery = new _Table(this.instance, clone.dml.database, clone.dml.table, clone.engine);
286
+ groupQuery._mutable = true;
222
287
  callback(groupQuery);
223
288
  clone.dml.where.push({
224
289
  type: clone.nextType,
@@ -723,6 +788,9 @@ var Table = class _Table {
723
788
  clone.dml.data = null;
724
789
  clone.dml.aggregation = null;
725
790
  const result = await clone.getResponse();
791
+ if (this.relations.length > 0 && Array.isArray(result)) {
792
+ return await this.attachRelations(result);
793
+ }
726
794
  return result;
727
795
  } catch (error) {
728
796
  throw error;
@@ -762,11 +830,10 @@ var Table = class _Table {
762
830
  * console.log(user); // { id: 1, name: 'John' }
763
831
  */
764
832
  async find(value, column = "id") {
765
- const clone = this.clone();
833
+ const clone = this.clone().where(column, "=", value);
766
834
  clone.dml.type = "select";
767
835
  clone.dml.data = null;
768
836
  clone.dml.aggregation = null;
769
- clone.where(column, "=", value);
770
837
  clone.dml.limit = 1;
771
838
  try {
772
839
  const result = await clone.getResponse();
@@ -823,8 +890,7 @@ var Table = class _Table {
823
890
  }
824
891
  clone.dml.type = "update";
825
892
  clone.dml.data = data;
826
- await clone.getResponse(clone.dml, "Update");
827
- return data;
893
+ return clone.getResponse(clone.dml, "Update");
828
894
  }
829
895
  /**
830
896
  * Deletes rows from the table based on the defined conditions.
@@ -844,6 +910,338 @@ var Table = class _Table {
844
910
  const deleteData = await clone.getResponse(clone.dml, "Delete");
845
911
  return deleteData;
846
912
  }
913
+ /**
914
+ * Adds a WHERE NOT IN condition to the query.
915
+ *
916
+ * @example
917
+ * const users = await db.table('users').whereNotIn('status', ['banned', 'deleted']).get();
918
+ */
919
+ whereNotIn(column, values) {
920
+ const clone = this.clone();
921
+ if (Array.isArray(values) && values.length > 0) {
922
+ clone.dml.where.push({
923
+ column,
924
+ operator: "NOT IN",
925
+ value: values,
926
+ type: clone.nextType,
927
+ isGroup: false
928
+ });
929
+ clone.nextType = "AND";
930
+ }
931
+ return clone;
932
+ }
933
+ /**
934
+ * Sets an explicit OFFSET for the query (alternative to page()).
935
+ *
936
+ * @example
937
+ * const rows = await db.table('logs').orderBy('id', 'ASC').limit(50).offset(100).get();
938
+ */
939
+ offset(number) {
940
+ const clone = this.clone();
941
+ clone.dml.offset = Number(number);
942
+ return clone;
943
+ }
944
+ /**
945
+ * Appends raw expressions to the SELECT list (aggregates, functions, aliases).
946
+ * Combine with groupBy() and having() for per-group metrics.
947
+ *
948
+ * @example
949
+ * const stats = await db.table('orders')
950
+ * .select(['user_id'])
951
+ * .selectRaw(['COUNT(*) AS order_count', 'SUM(amount) AS total'])
952
+ * .groupBy('user_id')
953
+ * .having('order_count', '>', 5)
954
+ * .get();
955
+ */
956
+ selectRaw(expressions) {
957
+ const clone = this.clone();
958
+ if (clone.dml.columns.length === 1 && clone.dml.columns[0] === "*") {
959
+ clone.dml.columns = [...expressions];
960
+ } else {
961
+ clone.dml.columns = [...clone.dml.columns, ...expressions];
962
+ }
963
+ return clone;
964
+ }
965
+ /**
966
+ * Adds a HAVING condition (filters grouped results).
967
+ *
968
+ * @example
969
+ * .groupBy('user_id').having('COUNT(*)', '>', 5)
970
+ */
971
+ having(column, operator, value) {
972
+ const clone = this.clone();
973
+ clone.dml.having = clone.dml.having ?? [];
974
+ clone.dml.having.push({
975
+ column,
976
+ operator,
977
+ value,
978
+ type: "AND",
979
+ isGroup: false
980
+ });
981
+ return clone;
982
+ }
983
+ /**
984
+ * Checks if at least one row matches the current conditions.
985
+ * Cheaper than first(): selects a constant with LIMIT 1.
986
+ *
987
+ * @example
988
+ * const taken = await db.table('users').where('email', '=', email).exists();
989
+ */
990
+ async exists() {
991
+ const clone = this.clone();
992
+ clone.dml.type = "select";
993
+ clone.dml.columns = ["1 AS dbcube_exists"];
994
+ clone.dml.data = null;
995
+ clone.dml.aggregation = null;
996
+ clone.dml.limit = 1;
997
+ clone.dml.offset = null;
998
+ const result = await clone.getResponse();
999
+ return Array.isArray(result) && result.length > 0;
1000
+ }
1001
+ /**
1002
+ * Fetches one page of results plus pagination metadata in a single call.
1003
+ * Runs the page query and the total count with the same conditions.
1004
+ *
1005
+ * @example
1006
+ * const { items, total, totalPages, hasNext } = await db.table('products')
1007
+ * .where('published', '=', true)
1008
+ * .orderBy('id', 'ASC')
1009
+ * .paginate(2, 25);
1010
+ */
1011
+ async paginate(page = 1, perPage = 20) {
1012
+ const pageNum = Math.max(1, Number(page));
1013
+ const size = Math.max(1, Number(perPage));
1014
+ const itemsQuery = this.limit(size).page(pageNum);
1015
+ const [items, total] = await Promise.all([
1016
+ itemsQuery.get(),
1017
+ this.count()
1018
+ ]);
1019
+ const totalNum = Number(total);
1020
+ const totalPages = Math.ceil(totalNum / size);
1021
+ return {
1022
+ items,
1023
+ page: pageNum,
1024
+ perPage: size,
1025
+ total: totalNum,
1026
+ totalPages,
1027
+ hasNext: pageNum < totalPages,
1028
+ hasPrev: pageNum > 1
1029
+ };
1030
+ }
1031
+ /**
1032
+ * Processes all matching rows in batches of `size`, keeping memory flat.
1033
+ * Return `false` from the callback to stop early.
1034
+ *
1035
+ * @example
1036
+ * await db.table('logs').orderBy('id', 'ASC').chunk(500, async (rows) => {
1037
+ * await processBatch(rows);
1038
+ * });
1039
+ */
1040
+ async chunk(size, callback) {
1041
+ const batchSize = Math.max(1, Number(size));
1042
+ let page = 1;
1043
+ while (true) {
1044
+ const rows = await this.limit(batchSize).page(page).get();
1045
+ if (rows.length === 0) break;
1046
+ const result = await callback(rows, page);
1047
+ if (result === false) break;
1048
+ if (rows.length < batchSize) break;
1049
+ page++;
1050
+ }
1051
+ }
1052
+ /**
1053
+ * Atomically increments a numeric column: `SET col = col + amount`.
1054
+ * Requires at least one WHERE condition. Extra fields can be updated in the same statement.
1055
+ *
1056
+ * @example
1057
+ * await db.table('products').where('id', '=', 5).increment('stock', 3);
1058
+ * await db.table('posts').where('id', '=', 1).increment('views', 1, { last_viewed_at: new Date().toISOString() });
1059
+ */
1060
+ async increment(column, amount = 1, extra = {}) {
1061
+ const clone = this.clone();
1062
+ if (clone.dml.where.length === 0) {
1063
+ throw new Error("You must specify at least one WHERE condition to perform an increment.");
1064
+ }
1065
+ clone.dml.type = "update";
1066
+ clone.dml.data = { ...extra, [column]: { $inc: Number(amount) } };
1067
+ return clone.getResponse();
1068
+ }
1069
+ /**
1070
+ * Atomically decrements a numeric column: `SET col = col - amount`.
1071
+ *
1072
+ * @example
1073
+ * await db.table('products').where('id', '=', 5).decrement('stock', 1);
1074
+ */
1075
+ async decrement(column, amount = 1, extra = {}) {
1076
+ return this.increment(column, -Math.abs(Number(amount)), extra);
1077
+ }
1078
+ /**
1079
+ * Deletes ALL rows from the table. The only write operation allowed
1080
+ * without a WHERE — the destructive intent is explicit in the name.
1081
+ *
1082
+ * @example
1083
+ * await db.table('temp_imports').truncate();
1084
+ */
1085
+ async truncate() {
1086
+ const clone = this.clone();
1087
+ clone.dml.type = "delete";
1088
+ clone.dml.where = [];
1089
+ return clone.getResponse();
1090
+ }
1091
+ /**
1092
+ * Inserts rows, updating them instead when a conflict occurs on the given keys.
1093
+ * MySQL → ON DUPLICATE KEY UPDATE · PostgreSQL/SQLite → ON CONFLICT ... DO UPDATE.
1094
+ *
1095
+ * @param data Rows to insert.
1096
+ * @param conflictKeys Column(s) with the UNIQUE/PK constraint that triggers the update.
1097
+ * @param updateColumns Columns to overwrite on conflict (defaults to every non-conflict column).
1098
+ *
1099
+ * @example
1100
+ * await db.table('settings').upsert(
1101
+ * [{ key: 'theme', value: 'dark' }],
1102
+ * ['key']
1103
+ * );
1104
+ */
1105
+ async upsert(data, conflictKeys, updateColumns) {
1106
+ if (!Array.isArray(data) || data.length === 0) {
1107
+ throw new Error("The upsert method requires a non-empty array of objects.");
1108
+ }
1109
+ if (!Array.isArray(conflictKeys) || conflictKeys.length === 0) {
1110
+ throw new Error("The upsert method requires at least one conflict key column.");
1111
+ }
1112
+ const motor = this.engine?.getConfig?.()?.type ?? "mysql";
1113
+ const table = this.dml.table;
1114
+ const columns = Object.keys(data[0]);
1115
+ const targets = updateColumns ?? columns.filter((c) => !conflictKeys.includes(c));
1116
+ assertValidIdentifier(table, "table");
1117
+ for (const c of columns) assertValidIdentifier(c, "column");
1118
+ for (const c of conflictKeys) assertValidIdentifier(c, "conflict key");
1119
+ for (const c of targets) assertValidIdentifier(c, "update column");
1120
+ if (motor === "mongodb") {
1121
+ const updates = data.map((row) => {
1122
+ const q = {};
1123
+ for (const k of conflictKeys) q[k] = row[k];
1124
+ const set = {};
1125
+ for (const c of targets) if (c in row) set[c] = row[c];
1126
+ const setOnInsert = {};
1127
+ for (const c of columns) {
1128
+ if (!targets.includes(c) && !conflictKeys.includes(c)) setOnInsert[c] = row[c];
1129
+ }
1130
+ if (!("uuid" in row)) setOnInsert["uuid"] = randomUUID();
1131
+ const u = {};
1132
+ if (Object.keys(set).length) u["$set"] = set;
1133
+ if (Object.keys(setOnInsert).length) u["$setOnInsert"] = setOnInsert;
1134
+ return { q, u, upsert: true, multi: false };
1135
+ });
1136
+ const command = JSON.stringify({ update: table, updates });
1137
+ const response2 = await this.engine.rawQuery(command, [], this.txId ?? void 0);
1138
+ if (response2.status != 200) {
1139
+ returnFormattedError(response2.status, response2.message);
1140
+ throw new Error(response2.message);
1141
+ }
1142
+ return response2.data;
1143
+ }
1144
+ const quote = (id) => motor === "mysql" ? `\`${id}\`` : `"${id}"`;
1145
+ const params = [];
1146
+ const placeholder = () => motor === "postgres" || motor === "postgresql" ? `$${params.length}` : "?";
1147
+ const rowsSql = data.map((row) => {
1148
+ const cells = columns.map((col) => {
1149
+ params.push(row[col] === void 0 ? null : row[col]);
1150
+ return placeholder();
1151
+ });
1152
+ return `(${cells.join(", ")})`;
1153
+ }).join(", ");
1154
+ const colsSql = columns.map(quote).join(", ");
1155
+ let sql;
1156
+ if (motor === "mysql") {
1157
+ const updates = targets.map((c) => `${quote(c)} = VALUES(${quote(c)})`).join(", ");
1158
+ sql = `INSERT INTO ${quote(table)} (${colsSql}) VALUES ${rowsSql} ON DUPLICATE KEY UPDATE ${updates}`;
1159
+ } else {
1160
+ const conflict = conflictKeys.map(quote).join(", ");
1161
+ const updates = targets.map((c) => `${quote(c)} = excluded.${quote(c)}`).join(", ");
1162
+ sql = `INSERT INTO ${quote(table)} (${colsSql}) VALUES ${rowsSql} ON CONFLICT (${conflict}) DO UPDATE SET ${updates}`;
1163
+ }
1164
+ if (motor === "sqlite") {
1165
+ await this.engine.rawQuery("SELECT name FROM sqlite_master LIMIT 1", [], this.txId ?? void 0);
1166
+ }
1167
+ const response = await this.engine.rawQuery(sql, params, this.txId ?? void 0);
1168
+ if (response.status != 200) {
1169
+ returnFormattedError(response.status, response.message);
1170
+ throw new Error(response.message);
1171
+ }
1172
+ return response.data;
1173
+ }
1174
+ /**
1175
+ * Declares a relation to eager-load with the results of get().
1176
+ * Relations are resolved from `foreign` definitions in your .cube files,
1177
+ * or explicitly via options. Loads each relation with ONE batched query
1178
+ * (whereIn) — no N+1.
1179
+ *
1180
+ * @example
1181
+ * // hasMany: orders.user_id → users.id (auto-detected from orders.table.cube)
1182
+ * const users = await db.table('users').with('orders').get();
1183
+ * // users[0].orders === [{...}, {...}]
1184
+ *
1185
+ * // belongsTo: posts.author_id → users.id, attached as a single object
1186
+ * const posts = await db.table('posts')
1187
+ * .with('author', { table: 'users', foreignKey: 'author_id', type: 'one' })
1188
+ * .get();
1189
+ */
1190
+ with(relation, options = {}) {
1191
+ const clone = this.clone();
1192
+ clone.relations.push({ name: relation, options });
1193
+ return clone;
1194
+ }
1195
+ async attachRelations(rows) {
1196
+ if (rows.length === 0 || this.relations.length === 0) return rows;
1197
+ for (const { name, options } of this.relations) {
1198
+ const relatedTable = options.table ?? name;
1199
+ const cubeMeta = loadCubeRelations();
1200
+ let type = options.type;
1201
+ let foreignKey = options.foreignKey;
1202
+ let localKey = options.localKey ?? "id";
1203
+ if (!foreignKey || !type) {
1204
+ const childFks = cubeMeta[relatedTable]?.foreigns ?? [];
1205
+ const ownFks = cubeMeta[this.dml.table]?.foreigns ?? [];
1206
+ const childToParent = childFks.find((fk) => fk.table === this.dml.table);
1207
+ const parentToChild = ownFks.find((fk) => fk.table === relatedTable);
1208
+ if (childToParent) {
1209
+ type = type ?? "many";
1210
+ foreignKey = foreignKey ?? childToParent.column_name;
1211
+ localKey = options.localKey ?? childToParent.column;
1212
+ } else if (parentToChild) {
1213
+ type = type ?? "one";
1214
+ foreignKey = foreignKey ?? parentToChild.column_name;
1215
+ localKey = options.localKey ?? parentToChild.column;
1216
+ } else {
1217
+ type = type ?? "many";
1218
+ foreignKey = foreignKey ?? `${singularize(this.dml.table)}_id`;
1219
+ }
1220
+ }
1221
+ const relatedQuery = new _Table(this.instance, this.dml.database, relatedTable, this.engine, this.computedFields, this.triggers, this.txId);
1222
+ if (type === "one") {
1223
+ const fkValues = [...new Set(rows.map((r) => r[foreignKey]).filter((v) => v !== null && v !== void 0))];
1224
+ const related = fkValues.length > 0 ? await relatedQuery.whereIn(localKey, fkValues).get() : [];
1225
+ const index = new Map(related.map((r) => [r[localKey], r]));
1226
+ for (const row of rows) {
1227
+ row[name] = index.get(row[foreignKey]) ?? null;
1228
+ }
1229
+ } else {
1230
+ const ids = [...new Set(rows.map((r) => r[localKey]).filter((v) => v !== null && v !== void 0))];
1231
+ const related = ids.length > 0 ? await relatedQuery.whereIn(foreignKey, ids).get() : [];
1232
+ const groups = /* @__PURE__ */ new Map();
1233
+ for (const rel of related) {
1234
+ const key = rel[foreignKey];
1235
+ if (!groups.has(key)) groups.set(key, []);
1236
+ groups.get(key).push(rel);
1237
+ }
1238
+ for (const row of rows) {
1239
+ row[name] = groups.get(row[localKey]) ?? [];
1240
+ }
1241
+ }
1242
+ }
1243
+ return rows;
1244
+ }
847
1245
  async getResponse(dml = null, type = null) {
848
1246
  const localDML = dml ? dml : this.dml;
849
1247
  const computedFieldsNeeded = [];
@@ -873,52 +1271,32 @@ var Table = class _Table {
873
1271
  const newDML = { ...localDML, data: [data] };
874
1272
  if (beffore) {
875
1273
  const interceptor = await this.trigger.execute("before" + type, data);
876
- const response = await this.engine.run("query_engine", [
877
- "--action",
878
- "execute",
879
- "--dml",
880
- JSON.stringify(newDML)
881
- ]);
1274
+ const response = await this.engine.executeDml(newDML, this.txId ?? void 0);
882
1275
  if (response.status != 200) {
883
- interceptor.discard();
1276
+ interceptor?.discard();
884
1277
  returnFormattedError(response.status, response.message);
885
1278
  }
886
- await interceptor.commit();
1279
+ await interceptor?.commit();
887
1280
  arrayResult = response.data;
888
1281
  }
889
1282
  if (after) {
890
- const response = await this.engine.run("query_engine", [
891
- "--action",
892
- "execute",
893
- "--dml",
894
- JSON.stringify(newDML)
895
- ]);
1283
+ const response = await this.engine.executeDml(newDML, this.txId ?? void 0);
896
1284
  if (response.status != 200) {
897
1285
  returnFormattedError(response.status, response.message);
898
1286
  }
899
1287
  const interceptor = await this.trigger.execute("after" + type, data);
900
- await interceptor.commit();
1288
+ await interceptor?.commit();
901
1289
  }
902
1290
  }
903
1291
  } else {
904
- const response = await this.engine.run("query_engine", [
905
- "--action",
906
- "execute",
907
- "--dml",
908
- JSON.stringify(localDML)
909
- ]);
1292
+ const response = await this.engine.executeDml(localDML, this.txId ?? void 0);
910
1293
  if (response.status != 200) {
911
1294
  returnFormattedError(response.status, response.message);
912
1295
  }
913
1296
  arrayResult = response.data;
914
1297
  }
915
1298
  } else {
916
- const response = await this.engine.run("query_engine", [
917
- "--action",
918
- "execute",
919
- "--dml",
920
- JSON.stringify(localDML)
921
- ]);
1299
+ const response = await this.engine.executeDml(localDML, this.txId ?? void 0);
922
1300
  if (response.status != 200) {
923
1301
  returnFormattedError(response.status, response.message);
924
1302
  }
@@ -936,12 +1314,16 @@ var Table = class _Table {
936
1314
  return arrayResult;
937
1315
  }
938
1316
  clone() {
1317
+ if (this._mutable) return this;
939
1318
  const cloned = Object.create(Object.getPrototypeOf(this));
940
1319
  cloned.engine = this.engine;
1320
+ cloned.instance = this.instance;
941
1321
  cloned.nextType = this.nextType;
942
1322
  cloned.computedFields = this.computedFields;
943
1323
  cloned.trigger = this.trigger;
944
1324
  cloned.triggers = this.triggers;
1325
+ cloned.txId = this.txId;
1326
+ cloned.relations = [...this.relations];
945
1327
  cloned.dml = {
946
1328
  ...this.dml,
947
1329
  columns: [...this.dml.columns],
@@ -949,6 +1331,7 @@ var Table = class _Table {
949
1331
  where: [...this.dml.where],
950
1332
  orderBy: [...this.dml.orderBy],
951
1333
  groupBy: [...this.dml.groupBy],
1334
+ having: [...this.dml.having ?? []],
952
1335
  // Clonar propiedades que faltaban para evitar mutación compartida
953
1336
  data: this.dml.data ? Array.isArray(this.dml.data) ? [...this.dml.data] : { ...this.dml.data } : null,
954
1337
  aggregation: this.dml.aggregation ? { ...this.dml.aggregation } : null
@@ -956,6 +1339,63 @@ var Table = class _Table {
956
1339
  return cloned;
957
1340
  }
958
1341
  };
1342
+ var cubeRelationsCache = null;
1343
+ function loadCubeRelations() {
1344
+ if (cubeRelationsCache) return cubeRelationsCache;
1345
+ const result = {};
1346
+ const scanDir = (dir) => {
1347
+ let entries;
1348
+ try {
1349
+ entries = fs.readdirSync(dir, { withFileTypes: true });
1350
+ } catch {
1351
+ return;
1352
+ }
1353
+ for (const entry of entries) {
1354
+ const full = path2.join(dir, entry.name);
1355
+ if (entry.isDirectory()) {
1356
+ if (entry.name !== "node_modules" && entry.name !== "triggers" && entry.name !== "logs") {
1357
+ scanDir(full);
1358
+ }
1359
+ } else if (entry.name.endsWith(".table.cube")) {
1360
+ try {
1361
+ const content = fs.readFileSync(full, "utf8");
1362
+ const nameMatch = content.match(/@meta\s*\(\s*\{[\s\S]*?name\s*:\s*"([^"]+)"/);
1363
+ const tableName = nameMatch ? nameMatch[1] : path2.basename(entry.name, ".table.cube");
1364
+ const foreigns = [];
1365
+ const colRegex = /(\w+)\s*:\s*\{(?:[^{}]|\{[^{}]*\})*?foreign\s*:\s*\{([^}]*)\}/g;
1366
+ let m;
1367
+ while ((m = colRegex.exec(content)) !== null) {
1368
+ const fkTable = m[2].match(/table\s*:\s*"([^"]+)"/);
1369
+ const fkColumn = m[2].match(/column\s*:\s*"([^"]+)"/);
1370
+ if (fkTable) {
1371
+ foreigns.push({
1372
+ column_name: m[1],
1373
+ table: fkTable[1],
1374
+ column: fkColumn ? fkColumn[1] : "id"
1375
+ });
1376
+ }
1377
+ }
1378
+ result[tableName] = { foreigns };
1379
+ } catch {
1380
+ }
1381
+ }
1382
+ }
1383
+ };
1384
+ scanDir(path2.join(process.cwd(), "dbcube"));
1385
+ cubeRelationsCache = result;
1386
+ return result;
1387
+ }
1388
+ function assertValidIdentifier(name, kind) {
1389
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
1390
+ throw new Error(`Invalid ${kind} name: '${name}'. Only letters, numbers and underscore are allowed.`);
1391
+ }
1392
+ }
1393
+ function singularize(word) {
1394
+ if (word.endsWith("ies")) return word.slice(0, -3) + "y";
1395
+ if (word.endsWith("ses")) return word.slice(0, -2);
1396
+ if (word.endsWith("s") && !word.endsWith("ss")) return word.slice(0, -1);
1397
+ return word;
1398
+ }
959
1399
  function returnFormattedError(status, message) {
960
1400
  const RESET = "\x1B[0m";
961
1401
  const RED = "\x1B[31m";