@mastra/mssql 0.0.0-new-scorer-api-20250801075530 → 0.0.0-new-button-export-20251219133013

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,372 +1,200 @@
1
- import { MessageList } from '@mastra/core/agent';
2
1
  import { MastraError, ErrorCategory, ErrorDomain } from '@mastra/core/error';
3
- import { MastraStorage, TABLE_EVALS, TABLE_TRACES, TABLE_WORKFLOW_SNAPSHOT, TABLE_THREADS, TABLE_MESSAGES, TABLE_RESOURCES } from '@mastra/core/storage';
4
- import { parseSqlIdentifier, parseFieldKey } from '@mastra/core/utils';
2
+ import { MastraStorage, createStorageErrorId, getDefaultValue, TABLE_WORKFLOW_SNAPSHOT, TABLE_SCHEMAS, TABLE_THREADS, TABLE_MESSAGES, TABLE_TRACES, TABLE_SCORERS, TABLE_SPANS, ScoresStorage, normalizePerPage, calculatePagination, WorkflowsStorage, MemoryStorage, TABLE_RESOURCES, ObservabilityStorage, SPAN_SCHEMA, transformScoreRow as transformScoreRow$1 } from '@mastra/core/storage';
5
3
  import sql from 'mssql';
4
+ import { MastraBase } from '@mastra/core/base';
5
+ import { parseSqlIdentifier } from '@mastra/core/utils';
6
+ import { MessageList } from '@mastra/core/agent';
7
+ import { randomUUID } from 'crypto';
8
+ import { saveScorePayloadSchema } from '@mastra/core/evals';
6
9
 
7
10
  // src/storage/index.ts
8
- var MSSQLStore = class extends MastraStorage {
11
+ function getSchemaName(schema) {
12
+ return schema ? `[${parseSqlIdentifier(schema, "schema name")}]` : void 0;
13
+ }
14
+ function getTableName({ indexName, schemaName }) {
15
+ const parsedIndexName = parseSqlIdentifier(indexName, "index name");
16
+ const quotedIndexName = `[${parsedIndexName}]`;
17
+ const quotedSchemaName = schemaName;
18
+ return quotedSchemaName ? `${quotedSchemaName}.${quotedIndexName}` : quotedIndexName;
19
+ }
20
+
21
+ // src/storage/db/index.ts
22
+ function resolveMssqlConfig(config) {
23
+ if ("pool" in config && "db" in config) {
24
+ return { pool: config.pool, db: config.db, schema: config.schema, needsConnect: false };
25
+ }
26
+ const pool = new sql.ConnectionPool({
27
+ server: config.server,
28
+ database: config.database,
29
+ user: config.user,
30
+ password: config.password,
31
+ port: config.port,
32
+ options: config.options || { encrypt: true, trustServerCertificate: true }
33
+ });
34
+ const db = new MssqlDB({ pool, schemaName: config.schemaName });
35
+ return { pool, db, schema: config.schemaName, needsConnect: true };
36
+ }
37
+ var MssqlDB = class extends MastraBase {
9
38
  pool;
10
- schema;
39
+ schemaName;
11
40
  setupSchemaPromise = null;
12
41
  schemaSetupComplete = void 0;
13
- isConnected = null;
14
- constructor(config) {
15
- super({ name: "MSSQLStore" });
16
- try {
17
- if ("connectionString" in config) {
18
- if (!config.connectionString || typeof config.connectionString !== "string" || config.connectionString.trim() === "") {
19
- throw new Error("MSSQLStore: connectionString must be provided and cannot be empty.");
20
- }
21
- } else {
22
- const required = ["server", "database", "user", "password"];
23
- for (const key of required) {
24
- if (!(key in config) || typeof config[key] !== "string" || config[key].trim() === "") {
25
- throw new Error(`MSSQLStore: ${key} must be provided and cannot be empty.`);
26
- }
42
+ getSqlType(type, isPrimaryKey = false, useLargeStorage = false) {
43
+ switch (type) {
44
+ case "text":
45
+ if (useLargeStorage) {
46
+ return "NVARCHAR(MAX)";
27
47
  }
28
- }
29
- this.schema = config.schemaName;
30
- this.pool = "connectionString" in config ? new sql.ConnectionPool(config.connectionString) : new sql.ConnectionPool({
31
- server: config.server,
32
- database: config.database,
33
- user: config.user,
34
- password: config.password,
35
- port: config.port,
36
- options: config.options || { encrypt: true, trustServerCertificate: true }
37
- });
38
- } catch (e) {
39
- throw new MastraError(
40
- {
41
- id: "MASTRA_STORAGE_MSSQL_STORE_INITIALIZATION_FAILED",
42
- domain: ErrorDomain.STORAGE,
43
- category: ErrorCategory.USER
44
- },
45
- e
46
- );
47
- }
48
- }
49
- async init() {
50
- if (this.isConnected === null) {
51
- this.isConnected = this._performInitializationAndStore();
52
- }
53
- try {
54
- await this.isConnected;
55
- await super.init();
56
- } catch (error) {
57
- this.isConnected = null;
58
- throw new MastraError(
59
- {
60
- id: "MASTRA_STORAGE_MSSQL_STORE_INIT_FAILED",
48
+ return isPrimaryKey ? "NVARCHAR(255)" : "NVARCHAR(400)";
49
+ case "timestamp":
50
+ return "DATETIME2(7)";
51
+ case "uuid":
52
+ return "UNIQUEIDENTIFIER";
53
+ case "jsonb":
54
+ return "NVARCHAR(MAX)";
55
+ case "integer":
56
+ return "INT";
57
+ case "bigint":
58
+ return "BIGINT";
59
+ case "float":
60
+ return "FLOAT";
61
+ case "boolean":
62
+ return "BIT";
63
+ default:
64
+ throw new MastraError({
65
+ id: createStorageErrorId("MSSQL", "TYPE", "NOT_SUPPORTED"),
61
66
  domain: ErrorDomain.STORAGE,
62
67
  category: ErrorCategory.THIRD_PARTY
63
- },
64
- error
65
- );
66
- }
67
- }
68
- async _performInitializationAndStore() {
69
- try {
70
- await this.pool.connect();
71
- return true;
72
- } catch (err) {
73
- throw err;
68
+ });
74
69
  }
75
70
  }
76
- get supports() {
77
- return {
78
- selectByIncludeResourceScope: true,
79
- resourceWorkingMemory: true,
80
- hasColumn: true,
81
- createTable: true,
82
- deleteMessages: false
83
- };
84
- }
85
- getTableName(indexName) {
86
- const parsedIndexName = parseSqlIdentifier(indexName, "index name");
87
- const quotedIndexName = `[${parsedIndexName}]`;
88
- const quotedSchemaName = this.getSchemaName();
89
- return quotedSchemaName ? `${quotedSchemaName}.${quotedIndexName}` : quotedIndexName;
90
- }
91
- getSchemaName() {
92
- return this.schema ? `[${parseSqlIdentifier(this.schema, "schema name")}]` : void 0;
71
+ constructor({ pool, schemaName }) {
72
+ super({ component: "STORAGE", name: "MssqlDB" });
73
+ this.pool = pool;
74
+ this.schemaName = schemaName;
93
75
  }
94
- transformEvalRow(row) {
95
- let testInfoValue = null, resultValue = null;
96
- if (row.test_info) {
97
- try {
98
- testInfoValue = typeof row.test_info === "string" ? JSON.parse(row.test_info) : row.test_info;
99
- } catch {
100
- }
101
- }
102
- if (row.test_info) {
103
- try {
104
- resultValue = typeof row.result === "string" ? JSON.parse(row.result) : row.result;
105
- } catch {
106
- }
107
- }
108
- return {
109
- agentName: row.agent_name,
110
- input: row.input,
111
- output: row.output,
112
- result: resultValue,
113
- metricName: row.metric_name,
114
- instructions: row.instructions,
115
- testInfo: testInfoValue,
116
- globalRunId: row.global_run_id,
117
- runId: row.run_id,
118
- createdAt: row.created_at
119
- };
76
+ async hasColumn(table, column) {
77
+ const schema = this.schemaName || "dbo";
78
+ const request = this.pool.request();
79
+ request.input("schema", schema);
80
+ request.input("table", table);
81
+ request.input("column", column);
82
+ request.input("columnLower", column.toLowerCase());
83
+ const result = await request.query(
84
+ `SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = @table AND (COLUMN_NAME = @column OR COLUMN_NAME = @columnLower)`
85
+ );
86
+ return result.recordset.length > 0;
120
87
  }
121
- /** @deprecated use getEvals instead */
122
- async getEvalsByAgentName(agentName, type) {
123
- try {
124
- let query = `SELECT * FROM ${this.getTableName(TABLE_EVALS)} WHERE agent_name = @p1`;
125
- if (type === "test") {
126
- query += " AND test_info IS NOT NULL AND JSON_VALUE(test_info, '$.testPath') IS NOT NULL";
127
- } else if (type === "live") {
128
- query += " AND (test_info IS NULL OR JSON_VALUE(test_info, '$.testPath') IS NULL)";
129
- }
130
- query += " ORDER BY created_at DESC";
131
- const request = this.pool.request();
132
- request.input("p1", agentName);
133
- const result = await request.query(query);
134
- const rows = result.recordset;
135
- return typeof this.transformEvalRow === "function" ? rows?.map((row) => this.transformEvalRow(row)) ?? [] : rows ?? [];
136
- } catch (error) {
137
- if (error && error.number === 208 && error.message && error.message.includes("Invalid object name")) {
138
- return [];
139
- }
140
- console.error("Failed to get evals for the specified agent: " + error?.message);
141
- throw error;
88
+ async setupSchema() {
89
+ if (!this.schemaName || this.schemaSetupComplete) {
90
+ return;
142
91
  }
143
- }
144
- async batchInsert({ tableName, records }) {
145
- const transaction = this.pool.transaction();
146
- try {
147
- await transaction.begin();
148
- for (const record of records) {
149
- await this.insert({ tableName, record });
150
- }
151
- await transaction.commit();
152
- } catch (error) {
153
- await transaction.rollback();
154
- throw new MastraError(
155
- {
156
- id: "MASTRA_STORAGE_MSSQL_STORE_BATCH_INSERT_FAILED",
157
- domain: ErrorDomain.STORAGE,
158
- category: ErrorCategory.THIRD_PARTY,
159
- details: {
160
- tableName,
161
- numberOfRecords: records.length
92
+ if (!this.setupSchemaPromise) {
93
+ this.setupSchemaPromise = (async () => {
94
+ try {
95
+ const checkRequest = this.pool.request();
96
+ checkRequest.input("schemaName", this.schemaName);
97
+ const checkResult = await checkRequest.query(`
98
+ SELECT 1 AS found FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = @schemaName
99
+ `);
100
+ const schemaExists = Array.isArray(checkResult.recordset) && checkResult.recordset.length > 0;
101
+ if (!schemaExists) {
102
+ try {
103
+ await this.pool.request().query(`CREATE SCHEMA [${this.schemaName}]`);
104
+ this.logger?.info?.(`Schema "${this.schemaName}" created successfully`);
105
+ } catch (error) {
106
+ this.logger?.error?.(`Failed to create schema "${this.schemaName}"`, { error });
107
+ throw new Error(
108
+ `Unable to create schema "${this.schemaName}". This requires CREATE privilege on the database. Either create the schema manually or grant CREATE privilege to the user.`
109
+ );
110
+ }
162
111
  }
163
- },
164
- error
165
- );
166
- }
167
- }
168
- /** @deprecated use getTracesPaginated instead*/
169
- async getTraces(args) {
170
- if (args.fromDate || args.toDate) {
171
- args.dateRange = {
172
- start: args.fromDate,
173
- end: args.toDate
174
- };
112
+ this.schemaSetupComplete = true;
113
+ this.logger?.debug?.(`Schema "${this.schemaName}" is ready for use`);
114
+ } catch (error) {
115
+ this.schemaSetupComplete = void 0;
116
+ this.setupSchemaPromise = null;
117
+ throw error;
118
+ } finally {
119
+ this.setupSchemaPromise = null;
120
+ }
121
+ })();
175
122
  }
176
- const result = await this.getTracesPaginated(args);
177
- return result.traces;
123
+ await this.setupSchemaPromise;
178
124
  }
179
- async getTracesPaginated(args) {
180
- const { name, scope, page = 0, perPage: perPageInput, attributes, filters, dateRange } = args;
181
- const fromDate = dateRange?.start;
182
- const toDate = dateRange?.end;
183
- const perPage = perPageInput !== void 0 ? perPageInput : 100;
184
- const currentOffset = page * perPage;
185
- const paramMap = {};
186
- const conditions = [];
187
- let paramIndex = 1;
188
- if (name) {
189
- const paramName = `p${paramIndex++}`;
190
- conditions.push(`[name] LIKE @${paramName}`);
191
- paramMap[paramName] = `${name}%`;
192
- }
193
- if (scope) {
194
- const paramName = `p${paramIndex++}`;
195
- conditions.push(`[scope] = @${paramName}`);
196
- paramMap[paramName] = scope;
197
- }
198
- if (attributes) {
199
- Object.entries(attributes).forEach(([key, value]) => {
200
- const parsedKey = parseFieldKey(key);
201
- const paramName = `p${paramIndex++}`;
202
- conditions.push(`JSON_VALUE([attributes], '$.${parsedKey}') = @${paramName}`);
203
- paramMap[paramName] = value;
204
- });
205
- }
206
- if (filters) {
207
- Object.entries(filters).forEach(([key, value]) => {
208
- const parsedKey = parseFieldKey(key);
209
- const paramName = `p${paramIndex++}`;
210
- conditions.push(`[${parsedKey}] = @${paramName}`);
211
- paramMap[paramName] = value;
212
- });
213
- }
214
- if (fromDate instanceof Date && !isNaN(fromDate.getTime())) {
215
- const paramName = `p${paramIndex++}`;
216
- conditions.push(`[createdAt] >= @${paramName}`);
217
- paramMap[paramName] = fromDate.toISOString();
218
- }
219
- if (toDate instanceof Date && !isNaN(toDate.getTime())) {
220
- const paramName = `p${paramIndex++}`;
221
- conditions.push(`[createdAt] <= @${paramName}`);
222
- paramMap[paramName] = toDate.toISOString();
223
- }
224
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
225
- const countQuery = `SELECT COUNT(*) as total FROM ${this.getTableName(TABLE_TRACES)} ${whereClause}`;
226
- let total = 0;
125
+ async insert({
126
+ tableName,
127
+ record,
128
+ transaction
129
+ }) {
227
130
  try {
228
- const countRequest = this.pool.request();
229
- Object.entries(paramMap).forEach(([key, value]) => {
230
- if (value instanceof Date) {
231
- countRequest.input(key, sql.DateTime, value);
131
+ const columns = Object.keys(record);
132
+ const parsedColumns = columns.map((col) => parseSqlIdentifier(col, "column name"));
133
+ const paramNames = columns.map((_, i) => `@param${i}`);
134
+ const insertSql = `INSERT INTO ${getTableName({ indexName: tableName, schemaName: getSchemaName(this.schemaName) })} (${parsedColumns.map((c) => `[${c}]`).join(", ")}) VALUES (${paramNames.join(", ")})`;
135
+ const request = transaction ? transaction.request() : this.pool.request();
136
+ columns.forEach((col, i) => {
137
+ const value = record[col];
138
+ const preparedValue = this.prepareValue(value, col, tableName);
139
+ if (preparedValue instanceof Date) {
140
+ request.input(`param${i}`, sql.DateTime2, preparedValue);
141
+ } else if (preparedValue === null || preparedValue === void 0) {
142
+ request.input(`param${i}`, this.getMssqlType(tableName, col), null);
232
143
  } else {
233
- countRequest.input(key, value);
144
+ request.input(`param${i}`, preparedValue);
234
145
  }
235
146
  });
236
- const countResult = await countRequest.query(countQuery);
237
- total = parseInt(countResult.recordset[0].total, 10);
147
+ await request.query(insertSql);
238
148
  } catch (error) {
239
149
  throw new MastraError(
240
150
  {
241
- id: "MASTRA_STORAGE_MSSQL_STORE_GET_TRACES_PAGINATED_FAILED_TO_RETRIEVE_TOTAL_COUNT",
151
+ id: createStorageErrorId("MSSQL", "INSERT", "FAILED"),
242
152
  domain: ErrorDomain.STORAGE,
243
153
  category: ErrorCategory.THIRD_PARTY,
244
154
  details: {
245
- name: args.name ?? "",
246
- scope: args.scope ?? ""
155
+ tableName
247
156
  }
248
157
  },
249
158
  error
250
159
  );
251
160
  }
252
- if (total === 0) {
253
- return {
254
- traces: [],
255
- total: 0,
256
- page,
257
- perPage,
258
- hasMore: false
259
- };
260
- }
261
- const dataQuery = `SELECT * FROM ${this.getTableName(TABLE_TRACES)} ${whereClause} ORDER BY [seq_id] DESC OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY`;
262
- const dataRequest = this.pool.request();
263
- Object.entries(paramMap).forEach(([key, value]) => {
264
- if (value instanceof Date) {
265
- dataRequest.input(key, sql.DateTime, value);
266
- } else {
267
- dataRequest.input(key, value);
268
- }
269
- });
270
- dataRequest.input("offset", currentOffset);
271
- dataRequest.input("limit", perPage);
161
+ }
162
+ async clearTable({ tableName }) {
163
+ const fullTableName = getTableName({ indexName: tableName, schemaName: getSchemaName(this.schemaName) });
272
164
  try {
273
- const rowsResult = await dataRequest.query(dataQuery);
274
- const rows = rowsResult.recordset;
275
- const traces = rows.map((row) => ({
276
- id: row.id,
277
- parentSpanId: row.parentSpanId,
278
- traceId: row.traceId,
279
- name: row.name,
280
- scope: row.scope,
281
- kind: row.kind,
282
- status: JSON.parse(row.status),
283
- events: JSON.parse(row.events),
284
- links: JSON.parse(row.links),
285
- attributes: JSON.parse(row.attributes),
286
- startTime: row.startTime,
287
- endTime: row.endTime,
288
- other: row.other,
289
- createdAt: row.createdAt
290
- }));
291
- return {
292
- traces,
293
- total,
294
- page,
295
- perPage,
296
- hasMore: currentOffset + traces.length < total
297
- };
165
+ try {
166
+ await this.pool.request().query(`TRUNCATE TABLE ${fullTableName}`);
167
+ } catch (truncateError) {
168
+ if (truncateError?.number === 4712) {
169
+ await this.pool.request().query(`DELETE FROM ${fullTableName}`);
170
+ } else {
171
+ throw truncateError;
172
+ }
173
+ }
298
174
  } catch (error) {
299
175
  throw new MastraError(
300
176
  {
301
- id: "MASTRA_STORAGE_MSSQL_STORE_GET_TRACES_PAGINATED_FAILED_TO_RETRIEVE_TRACES",
177
+ id: createStorageErrorId("MSSQL", "CLEAR_TABLE", "FAILED"),
302
178
  domain: ErrorDomain.STORAGE,
303
179
  category: ErrorCategory.THIRD_PARTY,
304
180
  details: {
305
- name: args.name ?? "",
306
- scope: args.scope ?? ""
181
+ tableName
307
182
  }
308
183
  },
309
184
  error
310
185
  );
311
186
  }
312
187
  }
313
- async setupSchema() {
314
- if (!this.schema || this.schemaSetupComplete) {
315
- return;
316
- }
317
- if (!this.setupSchemaPromise) {
318
- this.setupSchemaPromise = (async () => {
319
- try {
320
- const checkRequest = this.pool.request();
321
- checkRequest.input("schemaName", this.schema);
322
- const checkResult = await checkRequest.query(`
323
- SELECT 1 AS found FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = @schemaName
324
- `);
325
- const schemaExists = Array.isArray(checkResult.recordset) && checkResult.recordset.length > 0;
326
- if (!schemaExists) {
327
- try {
328
- await this.pool.request().query(`CREATE SCHEMA [${this.schema}]`);
329
- this.logger?.info?.(`Schema "${this.schema}" created successfully`);
330
- } catch (error) {
331
- this.logger?.error?.(`Failed to create schema "${this.schema}"`, { error });
332
- throw new Error(
333
- `Unable to create schema "${this.schema}". This requires CREATE privilege on the database. Either create the schema manually or grant CREATE privilege to the user.`
334
- );
335
- }
336
- }
337
- this.schemaSetupComplete = true;
338
- this.logger?.debug?.(`Schema "${this.schema}" is ready for use`);
339
- } catch (error) {
340
- this.schemaSetupComplete = void 0;
341
- this.setupSchemaPromise = null;
342
- throw error;
343
- } finally {
344
- this.setupSchemaPromise = null;
345
- }
346
- })();
347
- }
348
- await this.setupSchemaPromise;
349
- }
350
- getSqlType(type, isPrimaryKey = false) {
188
+ getDefaultValue(type) {
351
189
  switch (type) {
352
- case "text":
353
- return isPrimaryKey ? "NVARCHAR(255)" : "NVARCHAR(MAX)";
354
190
  case "timestamp":
355
- return "DATETIME2(7)";
356
- case "uuid":
357
- return "UNIQUEIDENTIFIER";
191
+ return "DEFAULT SYSUTCDATETIME()";
358
192
  case "jsonb":
359
- return "NVARCHAR(MAX)";
360
- case "integer":
361
- return "INT";
362
- case "bigint":
363
- return "BIGINT";
193
+ return "DEFAULT N'{}'";
194
+ case "boolean":
195
+ return "DEFAULT 0";
364
196
  default:
365
- throw new MastraError({
366
- id: "MASTRA_STORAGE_MSSQL_STORE_TYPE_NOT_SUPPORTED",
367
- domain: ErrorDomain.STORAGE,
368
- category: ErrorCategory.THIRD_PARTY
369
- });
197
+ return getDefaultValue(type);
370
198
  }
371
199
  }
372
200
  async createTable({
@@ -375,25 +203,44 @@ var MSSQLStore = class extends MastraStorage {
375
203
  }) {
376
204
  try {
377
205
  const uniqueConstraintColumns = tableName === TABLE_WORKFLOW_SNAPSHOT ? ["workflow_name", "run_id"] : [];
206
+ const largeDataColumns = [
207
+ "workingMemory",
208
+ "snapshot",
209
+ "metadata",
210
+ "content",
211
+ // messages.content - can be very long conversation content
212
+ "input",
213
+ // evals.input - test input data
214
+ "output",
215
+ // evals.output - test output data
216
+ "instructions",
217
+ // evals.instructions - evaluation instructions
218
+ "other"
219
+ // traces.other - additional trace data
220
+ ];
378
221
  const columns = Object.entries(schema).map(([name, def]) => {
379
222
  const parsedName = parseSqlIdentifier(name, "column name");
380
223
  const constraints = [];
381
224
  if (def.primaryKey) constraints.push("PRIMARY KEY");
382
225
  if (!def.nullable) constraints.push("NOT NULL");
383
226
  const isIndexed = !!def.primaryKey || uniqueConstraintColumns.includes(name);
384
- return `[${parsedName}] ${this.getSqlType(def.type, isIndexed)} ${constraints.join(" ")}`.trim();
227
+ const useLargeStorage = largeDataColumns.includes(name);
228
+ return `[${parsedName}] ${this.getSqlType(def.type, isIndexed, useLargeStorage)} ${constraints.join(" ")}`.trim();
385
229
  }).join(",\n");
386
- if (this.schema) {
230
+ if (this.schemaName) {
387
231
  await this.setupSchema();
388
232
  }
389
233
  const checkTableRequest = this.pool.request();
390
- checkTableRequest.input("tableName", this.getTableName(tableName).replace(/[[\]]/g, "").split(".").pop());
234
+ checkTableRequest.input(
235
+ "tableName",
236
+ getTableName({ indexName: tableName, schemaName: getSchemaName(this.schemaName) }).replace(/[[\]]/g, "").split(".").pop()
237
+ );
391
238
  const checkTableSql = `SELECT 1 AS found FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = @tableName`;
392
- checkTableRequest.input("schema", this.schema || "dbo");
239
+ checkTableRequest.input("schema", this.schemaName || "dbo");
393
240
  const checkTableResult = await checkTableRequest.query(checkTableSql);
394
241
  const tableExists = Array.isArray(checkTableResult.recordset) && checkTableResult.recordset.length > 0;
395
242
  if (!tableExists) {
396
- const createSql = `CREATE TABLE ${this.getTableName(tableName)} (
243
+ const createSql = `CREATE TABLE ${getTableName({ indexName: tableName, schemaName: getSchemaName(this.schemaName) })} (
397
244
  ${columns}
398
245
  )`;
399
246
  await this.pool.request().query(createSql);
@@ -404,12 +251,15 @@ ${columns}
404
251
  WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = @tableName AND COLUMN_NAME = 'seq_id'
405
252
  `;
406
253
  const checkColumnRequest = this.pool.request();
407
- checkColumnRequest.input("schema", this.schema || "dbo");
408
- checkColumnRequest.input("tableName", this.getTableName(tableName).replace(/[[\]]/g, "").split(".").pop());
254
+ checkColumnRequest.input("schema", this.schemaName || "dbo");
255
+ checkColumnRequest.input(
256
+ "tableName",
257
+ getTableName({ indexName: tableName, schemaName: getSchemaName(this.schemaName) }).replace(/[[\]]/g, "").split(".").pop()
258
+ );
409
259
  const columnResult = await checkColumnRequest.query(columnCheckSql);
410
260
  const columnExists = Array.isArray(columnResult.recordset) && columnResult.recordset.length > 0;
411
261
  if (!columnExists) {
412
- const alterSql = `ALTER TABLE ${this.getTableName(tableName)} ADD seq_id BIGINT IDENTITY(1,1)`;
262
+ const alterSql = `ALTER TABLE ${getTableName({ indexName: tableName, schemaName: getSchemaName(this.schemaName) })} ADD seq_id BIGINT IDENTITY(1,1)`;
413
263
  await this.pool.request().query(alterSql);
414
264
  }
415
265
  if (tableName === TABLE_WORKFLOW_SNAPSHOT) {
@@ -420,14 +270,14 @@ ${columns}
420
270
  const constraintResult = await checkConstraintRequest.query(checkConstraintSql);
421
271
  const constraintExists = Array.isArray(constraintResult.recordset) && constraintResult.recordset.length > 0;
422
272
  if (!constraintExists) {
423
- const addConstraintSql = `ALTER TABLE ${this.getTableName(tableName)} ADD CONSTRAINT ${constraintName} UNIQUE ([workflow_name], [run_id])`;
273
+ const addConstraintSql = `ALTER TABLE ${getTableName({ indexName: tableName, schemaName: getSchemaName(this.schemaName) })} ADD CONSTRAINT ${constraintName} UNIQUE ([workflow_name], [run_id])`;
424
274
  await this.pool.request().query(addConstraintSql);
425
275
  }
426
276
  }
427
277
  } catch (error) {
428
278
  throw new MastraError(
429
279
  {
430
- id: "MASTRA_STORAGE_MSSQL_STORE_CREATE_TABLE_FAILED",
280
+ id: createStorageErrorId("MSSQL", "CREATE_TABLE", "FAILED"),
431
281
  domain: ErrorDomain.STORAGE,
432
282
  category: ErrorCategory.THIRD_PARTY,
433
283
  details: {
@@ -438,35 +288,43 @@ ${columns}
438
288
  );
439
289
  }
440
290
  }
441
- getDefaultValue(type) {
442
- switch (type) {
443
- case "timestamp":
444
- return "DEFAULT SYSDATETIMEOFFSET()";
445
- case "jsonb":
446
- return "DEFAULT N'{}'";
447
- default:
448
- return super.getDefaultValue(type);
449
- }
450
- }
291
+ /**
292
+ * Alters table schema to add columns if they don't exist
293
+ * @param tableName Name of the table
294
+ * @param schema Schema of the table
295
+ * @param ifNotExists Array of column names to add if they don't exist
296
+ */
451
297
  async alterTable({
452
298
  tableName,
453
299
  schema,
454
300
  ifNotExists
455
301
  }) {
456
- const fullTableName = this.getTableName(tableName);
302
+ const fullTableName = getTableName({ indexName: tableName, schemaName: getSchemaName(this.schemaName) });
457
303
  try {
458
304
  for (const columnName of ifNotExists) {
459
305
  if (schema[columnName]) {
460
306
  const columnCheckRequest = this.pool.request();
461
307
  columnCheckRequest.input("tableName", fullTableName.replace(/[[\]]/g, "").split(".").pop());
462
308
  columnCheckRequest.input("columnName", columnName);
463
- columnCheckRequest.input("schema", this.schema || "dbo");
309
+ columnCheckRequest.input("schema", this.schemaName || "dbo");
464
310
  const checkSql = `SELECT 1 AS found FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = @tableName AND COLUMN_NAME = @columnName`;
465
311
  const checkResult = await columnCheckRequest.query(checkSql);
466
312
  const columnExists = Array.isArray(checkResult.recordset) && checkResult.recordset.length > 0;
467
313
  if (!columnExists) {
468
314
  const columnDef = schema[columnName];
469
- const sqlType = this.getSqlType(columnDef.type);
315
+ const largeDataColumns = [
316
+ "workingMemory",
317
+ "snapshot",
318
+ "metadata",
319
+ "content",
320
+ "input",
321
+ "output",
322
+ "instructions",
323
+ "other"
324
+ ];
325
+ const useLargeStorage = largeDataColumns.includes(columnName);
326
+ const isIndexed = !!columnDef.primaryKey;
327
+ const sqlType = this.getSqlType(columnDef.type, isIndexed, useLargeStorage);
470
328
  const nullable = columnDef.nullable === false ? "NOT NULL" : "";
471
329
  const defaultValue = columnDef.nullable === false ? this.getDefaultValue(columnDef.type) : "";
472
330
  const parsedColumnName = parseSqlIdentifier(columnName, "column name");
@@ -479,69 +337,7 @@ ${columns}
479
337
  } catch (error) {
480
338
  throw new MastraError(
481
339
  {
482
- id: "MASTRA_STORAGE_MSSQL_STORE_ALTER_TABLE_FAILED",
483
- domain: ErrorDomain.STORAGE,
484
- category: ErrorCategory.THIRD_PARTY,
485
- details: {
486
- tableName
487
- }
488
- },
489
- error
490
- );
491
- }
492
- }
493
- async clearTable({ tableName }) {
494
- const fullTableName = this.getTableName(tableName);
495
- try {
496
- const fkQuery = `
497
- SELECT
498
- OBJECT_SCHEMA_NAME(fk.parent_object_id) AS schema_name,
499
- OBJECT_NAME(fk.parent_object_id) AS table_name
500
- FROM sys.foreign_keys fk
501
- WHERE fk.referenced_object_id = OBJECT_ID(@fullTableName)
502
- `;
503
- const fkResult = await this.pool.request().input("fullTableName", fullTableName).query(fkQuery);
504
- const childTables = fkResult.recordset || [];
505
- for (const child of childTables) {
506
- const childTableName = this.schema ? `[${child.schema_name}].[${child.table_name}]` : `[${child.table_name}]`;
507
- await this.clearTable({ tableName: childTableName });
508
- }
509
- await this.pool.request().query(`TRUNCATE TABLE ${fullTableName}`);
510
- } catch (error) {
511
- throw new MastraError(
512
- {
513
- id: "MASTRA_STORAGE_MSSQL_STORE_CLEAR_TABLE_FAILED",
514
- domain: ErrorDomain.STORAGE,
515
- category: ErrorCategory.THIRD_PARTY,
516
- details: {
517
- tableName
518
- }
519
- },
520
- error
521
- );
522
- }
523
- }
524
- async insert({ tableName, record }) {
525
- try {
526
- const columns = Object.keys(record).map((col) => parseSqlIdentifier(col, "column name"));
527
- const values = Object.values(record);
528
- const paramNames = values.map((_, i) => `@param${i}`);
529
- const insertSql = `INSERT INTO ${this.getTableName(tableName)} (${columns.map((c) => `[${c}]`).join(", ")}) VALUES (${paramNames.join(", ")})`;
530
- const request = this.pool.request();
531
- values.forEach((value, i) => {
532
- if (value instanceof Date) {
533
- request.input(`param${i}`, sql.DateTime2, value);
534
- } else if (typeof value === "object" && value !== null) {
535
- request.input(`param${i}`, JSON.stringify(value));
536
- } else {
537
- request.input(`param${i}`, value);
538
- }
539
- });
540
- await request.query(insertSql);
541
- } catch (error) {
542
- throw new MastraError(
543
- {
544
- id: "MASTRA_STORAGE_MSSQL_STORE_INSERT_FAILED",
340
+ id: createStorageErrorId("MSSQL", "ALTER_TABLE", "FAILED"),
545
341
  domain: ErrorDomain.STORAGE,
546
342
  category: ErrorCategory.THIRD_PARTY,
547
343
  details: {
@@ -556,13 +352,17 @@ ${columns}
556
352
  try {
557
353
  const keyEntries = Object.entries(keys).map(([key, value]) => [parseSqlIdentifier(key, "column name"), value]);
558
354
  const conditions = keyEntries.map(([key], i) => `[${key}] = @param${i}`).join(" AND ");
559
- const values = keyEntries.map(([_, value]) => value);
560
- const sql2 = `SELECT * FROM ${this.getTableName(tableName)} WHERE ${conditions}`;
355
+ const sqlQuery = `SELECT * FROM ${getTableName({ indexName: tableName, schemaName: getSchemaName(this.schemaName) })} WHERE ${conditions}`;
561
356
  const request = this.pool.request();
562
- values.forEach((value, i) => {
563
- request.input(`param${i}`, value);
357
+ keyEntries.forEach(([key, value], i) => {
358
+ const preparedValue = this.prepareValue(value, key, tableName);
359
+ if (preparedValue === null || preparedValue === void 0) {
360
+ request.input(`param${i}`, this.getMssqlType(tableName, key), null);
361
+ } else {
362
+ request.input(`param${i}`, preparedValue);
363
+ }
564
364
  });
565
- const resultSet = await request.query(sql2);
365
+ const resultSet = await request.query(sqlQuery);
566
366
  const result = resultSet.recordset[0] || null;
567
367
  if (!result) {
568
368
  return null;
@@ -578,7 +378,7 @@ ${columns}
578
378
  } catch (error) {
579
379
  throw new MastraError(
580
380
  {
581
- id: "MASTRA_STORAGE_MSSQL_STORE_LOAD_FAILED",
381
+ id: createStorageErrorId("MSSQL", "LOAD", "FAILED"),
582
382
  domain: ErrorDomain.STORAGE,
583
383
  category: ErrorCategory.THIRD_PARTY,
584
384
  details: {
@@ -589,1226 +389,3024 @@ ${columns}
589
389
  );
590
390
  }
591
391
  }
592
- async getThreadById({ threadId }) {
392
+ async batchInsert({ tableName, records }) {
393
+ const transaction = this.pool.transaction();
593
394
  try {
594
- const sql2 = `SELECT
595
- id,
596
- [resourceId],
597
- title,
598
- metadata,
599
- [createdAt],
600
- [updatedAt]
601
- FROM ${this.getTableName(TABLE_THREADS)}
602
- WHERE id = @threadId`;
603
- const request = this.pool.request();
604
- request.input("threadId", threadId);
605
- const resultSet = await request.query(sql2);
606
- const thread = resultSet.recordset[0] || null;
607
- if (!thread) {
608
- return null;
395
+ await transaction.begin();
396
+ for (const record of records) {
397
+ await this.insert({ tableName, record, transaction });
609
398
  }
610
- return {
611
- ...thread,
612
- metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata,
613
- createdAt: thread.createdAt,
614
- updatedAt: thread.updatedAt
615
- };
399
+ await transaction.commit();
616
400
  } catch (error) {
401
+ await transaction.rollback();
617
402
  throw new MastraError(
618
403
  {
619
- id: "MASTRA_STORAGE_MSSQL_STORE_GET_THREAD_BY_ID_FAILED",
404
+ id: createStorageErrorId("MSSQL", "BATCH_INSERT", "FAILED"),
620
405
  domain: ErrorDomain.STORAGE,
621
406
  category: ErrorCategory.THIRD_PARTY,
622
407
  details: {
623
- threadId
408
+ tableName,
409
+ numberOfRecords: records.length
624
410
  }
625
411
  },
626
412
  error
627
413
  );
628
414
  }
629
415
  }
630
- async getThreadsByResourceIdPaginated(args) {
631
- const { resourceId, page = 0, perPage: perPageInput } = args;
416
+ async dropTable({ tableName }) {
632
417
  try {
633
- const perPage = perPageInput !== void 0 ? perPageInput : 100;
634
- const currentOffset = page * perPage;
635
- const baseQuery = `FROM ${this.getTableName(TABLE_THREADS)} WHERE [resourceId] = @resourceId`;
636
- const countQuery = `SELECT COUNT(*) as count ${baseQuery}`;
637
- const countRequest = this.pool.request();
638
- countRequest.input("resourceId", resourceId);
639
- const countResult = await countRequest.query(countQuery);
640
- const total = parseInt(countResult.recordset[0]?.count ?? "0", 10);
641
- if (total === 0) {
642
- return {
643
- threads: [],
644
- total: 0,
645
- page,
646
- perPage,
647
- hasMore: false
648
- };
649
- }
650
- const dataQuery = `SELECT id, [resourceId], title, metadata, [createdAt], [updatedAt] ${baseQuery} ORDER BY [seq_id] DESC OFFSET @offset ROWS FETCH NEXT @perPage ROWS ONLY`;
651
- const dataRequest = this.pool.request();
652
- dataRequest.input("resourceId", resourceId);
653
- dataRequest.input("perPage", perPage);
654
- dataRequest.input("offset", currentOffset);
655
- const rowsResult = await dataRequest.query(dataQuery);
656
- const rows = rowsResult.recordset || [];
657
- const threads = rows.map((thread) => ({
658
- ...thread,
659
- metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata,
660
- createdAt: thread.createdAt,
661
- updatedAt: thread.updatedAt
662
- }));
663
- return {
664
- threads,
665
- total,
666
- page,
667
- perPage,
668
- hasMore: currentOffset + threads.length < total
669
- };
418
+ const tableNameWithSchema = getTableName({ indexName: tableName, schemaName: getSchemaName(this.schemaName) });
419
+ await this.pool.request().query(`DROP TABLE IF EXISTS ${tableNameWithSchema}`);
670
420
  } catch (error) {
671
- const mastraError = new MastraError(
421
+ throw new MastraError(
672
422
  {
673
- id: "MASTRA_STORAGE_MSSQL_STORE_GET_THREADS_BY_RESOURCE_ID_PAGINATED_FAILED",
423
+ id: createStorageErrorId("MSSQL", "DROP_TABLE", "FAILED"),
674
424
  domain: ErrorDomain.STORAGE,
675
425
  category: ErrorCategory.THIRD_PARTY,
676
426
  details: {
677
- resourceId,
678
- page
427
+ tableName
679
428
  }
680
429
  },
681
430
  error
682
431
  );
683
- this.logger?.error?.(mastraError.toString());
684
- this.logger?.trackException?.(mastraError);
685
- return { threads: [], total: 0, page, perPage: perPageInput || 100, hasMore: false };
686
432
  }
687
433
  }
688
- async saveThread({ thread }) {
689
- try {
690
- const table = this.getTableName(TABLE_THREADS);
691
- const mergeSql = `MERGE INTO ${table} WITH (HOLDLOCK) AS target
692
- USING (SELECT @id AS id) AS source
693
- ON (target.id = source.id)
694
- WHEN MATCHED THEN
695
- UPDATE SET
696
- [resourceId] = @resourceId,
697
- title = @title,
698
- metadata = @metadata,
699
- [createdAt] = @createdAt,
700
- [updatedAt] = @updatedAt
701
- WHEN NOT MATCHED THEN
702
- INSERT (id, [resourceId], title, metadata, [createdAt], [updatedAt])
703
- VALUES (@id, @resourceId, @title, @metadata, @createdAt, @updatedAt);`;
704
- const req = this.pool.request();
705
- req.input("id", thread.id);
706
- req.input("resourceId", thread.resourceId);
707
- req.input("title", thread.title);
708
- req.input("metadata", thread.metadata ? JSON.stringify(thread.metadata) : null);
709
- req.input("createdAt", thread.createdAt);
710
- req.input("updatedAt", thread.updatedAt);
711
- await req.query(mergeSql);
712
- return thread;
713
- } catch (error) {
714
- throw new MastraError(
715
- {
716
- id: "MASTRA_STORAGE_MSSQL_STORE_SAVE_THREAD_FAILED",
717
- domain: ErrorDomain.STORAGE,
718
- category: ErrorCategory.THIRD_PARTY,
719
- details: {
720
- threadId: thread.id
434
+ /**
435
+ * Prepares a value for database operations, handling Date objects and JSON serialization
436
+ */
437
+ prepareValue(value, columnName, tableName) {
438
+ if (value === null || value === void 0) {
439
+ return value;
440
+ }
441
+ if (value instanceof Date) {
442
+ return value;
443
+ }
444
+ const schema = TABLE_SCHEMAS[tableName];
445
+ const columnSchema = schema?.[columnName];
446
+ if (columnSchema?.type === "boolean") {
447
+ return value ? 1 : 0;
448
+ }
449
+ if (columnSchema?.type === "jsonb") {
450
+ if (typeof value === "string") {
451
+ const trimmed = value.trim();
452
+ if (trimmed.length > 0) {
453
+ try {
454
+ JSON.parse(trimmed);
455
+ return trimmed;
456
+ } catch {
721
457
  }
722
- },
723
- error
724
- );
458
+ }
459
+ return JSON.stringify(value);
460
+ }
461
+ if (typeof value === "bigint") {
462
+ return value.toString();
463
+ }
464
+ return JSON.stringify(value);
465
+ }
466
+ if (typeof value === "object") {
467
+ return JSON.stringify(value);
725
468
  }
469
+ return value;
726
470
  }
727
471
  /**
728
- * @deprecated use getThreadsByResourceIdPaginated instead
472
+ * Maps TABLE_SCHEMAS types to mssql param types (used when value is null)
729
473
  */
730
- async getThreadsByResourceId(args) {
731
- const { resourceId } = args;
732
- try {
733
- const baseQuery = `FROM ${this.getTableName(TABLE_THREADS)} WHERE [resourceId] = @resourceId`;
734
- const dataQuery = `SELECT id, [resourceId], title, metadata, [createdAt], [updatedAt] ${baseQuery} ORDER BY [seq_id] DESC`;
735
- const request = this.pool.request();
736
- request.input("resourceId", resourceId);
737
- const resultSet = await request.query(dataQuery);
738
- const rows = resultSet.recordset || [];
739
- return rows.map((thread) => ({
740
- ...thread,
741
- metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata,
742
- createdAt: thread.createdAt,
743
- updatedAt: thread.updatedAt
744
- }));
745
- } catch (error) {
746
- this.logger?.error?.(`Error getting threads for resource ${resourceId}:`, error);
747
- return [];
474
+ getMssqlType(tableName, columnName) {
475
+ const col = TABLE_SCHEMAS[tableName]?.[columnName];
476
+ switch (col?.type) {
477
+ case "text":
478
+ return sql.NVarChar;
479
+ case "timestamp":
480
+ return sql.DateTime2;
481
+ case "uuid":
482
+ return sql.UniqueIdentifier;
483
+ case "jsonb":
484
+ return sql.NVarChar;
485
+ case "integer":
486
+ return sql.Int;
487
+ case "bigint":
488
+ return sql.BigInt;
489
+ case "float":
490
+ return sql.Float;
491
+ case "boolean":
492
+ return sql.Bit;
493
+ default:
494
+ return sql.NVarChar;
748
495
  }
749
496
  }
750
497
  /**
751
- * Updates a thread's title and metadata, merging with existing metadata. Returns the updated thread.
498
+ * Update a single record in the database
752
499
  */
753
- async updateThread({
754
- id,
755
- title,
756
- metadata
500
+ async update({
501
+ tableName,
502
+ keys,
503
+ data,
504
+ transaction
757
505
  }) {
758
- const existingThread = await this.getThreadById({ threadId: id });
759
- if (!existingThread) {
760
- throw new MastraError({
761
- id: "MASTRA_STORAGE_MSSQL_STORE_UPDATE_THREAD_FAILED",
762
- domain: ErrorDomain.STORAGE,
763
- category: ErrorCategory.USER,
764
- text: `Thread ${id} not found`,
765
- details: {
766
- threadId: id,
767
- title
768
- }
769
- });
770
- }
771
- const mergedMetadata = {
772
- ...existingThread.metadata,
773
- ...metadata
774
- };
775
506
  try {
776
- const table = this.getTableName(TABLE_THREADS);
777
- const sql2 = `UPDATE ${table}
778
- SET title = @title,
779
- metadata = @metadata,
780
- [updatedAt] = @updatedAt
781
- OUTPUT INSERTED.*
782
- WHERE id = @id`;
783
- const req = this.pool.request();
784
- req.input("id", id);
785
- req.input("title", title);
786
- req.input("metadata", JSON.stringify(mergedMetadata));
787
- req.input("updatedAt", (/* @__PURE__ */ new Date()).toISOString());
788
- const result = await req.query(sql2);
789
- let thread = result.recordset && result.recordset[0];
790
- if (thread && "seq_id" in thread) {
791
- const { seq_id, ...rest } = thread;
792
- thread = rest;
507
+ if (!data || Object.keys(data).length === 0) {
508
+ throw new MastraError({
509
+ id: createStorageErrorId("MSSQL", "UPDATE", "EMPTY_DATA"),
510
+ domain: ErrorDomain.STORAGE,
511
+ category: ErrorCategory.USER,
512
+ text: "Cannot update with empty data payload"
513
+ });
793
514
  }
794
- if (!thread) {
515
+ if (!keys || Object.keys(keys).length === 0) {
795
516
  throw new MastraError({
796
- id: "MASTRA_STORAGE_MSSQL_STORE_UPDATE_THREAD_FAILED",
517
+ id: createStorageErrorId("MSSQL", "UPDATE", "EMPTY_KEYS"),
797
518
  domain: ErrorDomain.STORAGE,
798
519
  category: ErrorCategory.USER,
799
- text: `Thread ${id} not found after update`,
800
- details: {
801
- threadId: id,
802
- title
803
- }
520
+ text: "Cannot update without keys to identify records"
804
521
  });
805
522
  }
806
- return {
807
- ...thread,
808
- metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata,
809
- createdAt: thread.createdAt,
810
- updatedAt: thread.updatedAt
811
- };
523
+ const setClauses = [];
524
+ const request = transaction ? transaction.request() : this.pool.request();
525
+ let paramIndex = 0;
526
+ Object.entries(data).forEach(([key, value]) => {
527
+ const parsedKey = parseSqlIdentifier(key, "column name");
528
+ const paramName = `set${paramIndex++}`;
529
+ setClauses.push(`[${parsedKey}] = @${paramName}`);
530
+ const preparedValue = this.prepareValue(value, key, tableName);
531
+ if (preparedValue === null || preparedValue === void 0) {
532
+ request.input(paramName, this.getMssqlType(tableName, key), null);
533
+ } else {
534
+ request.input(paramName, preparedValue);
535
+ }
536
+ });
537
+ const whereConditions = [];
538
+ Object.entries(keys).forEach(([key, value]) => {
539
+ const parsedKey = parseSqlIdentifier(key, "column name");
540
+ const paramName = `where${paramIndex++}`;
541
+ whereConditions.push(`[${parsedKey}] = @${paramName}`);
542
+ const preparedValue = this.prepareValue(value, key, tableName);
543
+ if (preparedValue === null || preparedValue === void 0) {
544
+ request.input(paramName, this.getMssqlType(tableName, key), null);
545
+ } else {
546
+ request.input(paramName, preparedValue);
547
+ }
548
+ });
549
+ const tableName_ = getTableName({
550
+ indexName: tableName,
551
+ schemaName: getSchemaName(this.schemaName)
552
+ });
553
+ const updateSql = `UPDATE ${tableName_} SET ${setClauses.join(", ")} WHERE ${whereConditions.join(" AND ")}`;
554
+ await request.query(updateSql);
812
555
  } catch (error) {
813
556
  throw new MastraError(
814
557
  {
815
- id: "MASTRA_STORAGE_MSSQL_STORE_UPDATE_THREAD_FAILED",
558
+ id: createStorageErrorId("MSSQL", "UPDATE", "FAILED"),
816
559
  domain: ErrorDomain.STORAGE,
817
560
  category: ErrorCategory.THIRD_PARTY,
818
561
  details: {
819
- threadId: id,
820
- title
562
+ tableName
821
563
  }
822
564
  },
823
565
  error
824
566
  );
825
567
  }
826
568
  }
827
- async deleteThread({ threadId }) {
828
- const messagesTable = this.getTableName(TABLE_MESSAGES);
829
- const threadsTable = this.getTableName(TABLE_THREADS);
830
- const deleteMessagesSql = `DELETE FROM ${messagesTable} WHERE [thread_id] = @threadId`;
831
- const deleteThreadSql = `DELETE FROM ${threadsTable} WHERE id = @threadId`;
832
- const tx = this.pool.transaction();
569
+ /**
570
+ * Update multiple records in a single batch transaction
571
+ */
572
+ async batchUpdate({
573
+ tableName,
574
+ updates
575
+ }) {
576
+ const transaction = this.pool.transaction();
833
577
  try {
834
- await tx.begin();
835
- const req = tx.request();
836
- req.input("threadId", threadId);
837
- await req.query(deleteMessagesSql);
838
- await req.query(deleteThreadSql);
839
- await tx.commit();
578
+ await transaction.begin();
579
+ for (const { keys, data } of updates) {
580
+ await this.update({ tableName, keys, data, transaction });
581
+ }
582
+ await transaction.commit();
840
583
  } catch (error) {
841
- await tx.rollback().catch(() => {
842
- });
584
+ await transaction.rollback();
843
585
  throw new MastraError(
844
586
  {
845
- id: "MASTRA_STORAGE_MSSQL_STORE_DELETE_THREAD_FAILED",
587
+ id: createStorageErrorId("MSSQL", "BATCH_UPDATE", "FAILED"),
846
588
  domain: ErrorDomain.STORAGE,
847
589
  category: ErrorCategory.THIRD_PARTY,
848
590
  details: {
849
- threadId
591
+ tableName,
592
+ numberOfRecords: updates.length
850
593
  }
851
594
  },
852
595
  error
853
596
  );
854
597
  }
855
598
  }
856
- async _getIncludedMessages({
857
- threadId,
858
- selectBy,
859
- orderByStatement
860
- }) {
861
- const include = selectBy?.include;
862
- if (!include) return null;
863
- const unionQueries = [];
864
- const paramValues = [];
865
- let paramIdx = 1;
866
- const paramNames = [];
867
- for (const inc of include) {
868
- const { id, withPreviousMessages = 0, withNextMessages = 0 } = inc;
869
- const searchId = inc.threadId || threadId;
870
- const pThreadId = `@p${paramIdx}`;
871
- const pId = `@p${paramIdx + 1}`;
872
- const pPrev = `@p${paramIdx + 2}`;
873
- const pNext = `@p${paramIdx + 3}`;
874
- unionQueries.push(
875
- `
876
- SELECT
877
- m.id,
878
- m.content,
879
- m.role,
880
- m.type,
881
- m.[createdAt],
882
- m.thread_id AS threadId,
883
- m.[resourceId],
884
- m.seq_id
885
- FROM (
886
- SELECT *, ROW_NUMBER() OVER (${orderByStatement}) as row_num
887
- FROM ${this.getTableName(TABLE_MESSAGES)}
888
- WHERE [thread_id] = ${pThreadId}
889
- ) AS m
890
- WHERE m.id = ${pId}
891
- OR EXISTS (
892
- SELECT 1
893
- FROM (
894
- SELECT *, ROW_NUMBER() OVER (${orderByStatement}) as row_num
895
- FROM ${this.getTableName(TABLE_MESSAGES)}
896
- WHERE [thread_id] = ${pThreadId}
897
- ) AS target
898
- WHERE target.id = ${pId}
899
- AND (
900
- (m.row_num <= target.row_num + ${pPrev} AND m.row_num > target.row_num)
901
- OR
902
- (m.row_num >= target.row_num - ${pNext} AND m.row_num < target.row_num)
903
- )
904
- )
905
- `
906
- );
907
- paramValues.push(searchId, id, withPreviousMessages, withNextMessages);
908
- paramNames.push(`p${paramIdx}`, `p${paramIdx + 1}`, `p${paramIdx + 2}`, `p${paramIdx + 3}`);
909
- paramIdx += 4;
910
- }
911
- const finalQuery = `
912
- SELECT * FROM (
913
- ${unionQueries.join(" UNION ALL ")}
914
- ) AS union_result
915
- ORDER BY [seq_id] ASC
916
- `;
917
- const req = this.pool.request();
918
- for (let i = 0; i < paramValues.length; ++i) {
919
- req.input(paramNames[i], paramValues[i]);
599
+ /**
600
+ * Delete multiple records by keys
601
+ */
602
+ async batchDelete({ tableName, keys }) {
603
+ if (keys.length === 0) {
604
+ return;
920
605
  }
921
- const result = await req.query(finalQuery);
922
- const includedRows = result.recordset || [];
923
- const seen = /* @__PURE__ */ new Set();
924
- const dedupedRows = includedRows.filter((row) => {
925
- if (seen.has(row.id)) return false;
926
- seen.add(row.id);
927
- return true;
606
+ const tableName_ = getTableName({
607
+ indexName: tableName,
608
+ schemaName: getSchemaName(this.schemaName)
928
609
  });
929
- return dedupedRows;
930
- }
931
- async getMessages(args) {
932
- const { threadId, format, selectBy } = args;
933
- const selectStatement = `SELECT seq_id, id, content, role, type, [createdAt], thread_id AS threadId`;
934
- const orderByStatement = `ORDER BY [seq_id] DESC`;
935
- const limit = this.resolveMessageLimit({ last: selectBy?.last, defaultLimit: 40 });
610
+ const transaction = this.pool.transaction();
936
611
  try {
937
- let rows = [];
938
- const include = selectBy?.include || [];
939
- if (include?.length) {
940
- const includeMessages = await this._getIncludedMessages({ threadId, selectBy, orderByStatement });
941
- if (includeMessages) {
942
- rows.push(...includeMessages);
943
- }
944
- }
945
- const excludeIds = rows.map((m) => m.id).filter(Boolean);
946
- let query = `${selectStatement} FROM ${this.getTableName(TABLE_MESSAGES)} WHERE [thread_id] = @threadId`;
947
- const request = this.pool.request();
948
- request.input("threadId", threadId);
949
- if (excludeIds.length > 0) {
950
- const excludeParams = excludeIds.map((_, idx) => `@id${idx}`);
951
- query += ` AND id NOT IN (${excludeParams.join(", ")})`;
952
- excludeIds.forEach((id, idx) => {
953
- request.input(`id${idx}`, id);
612
+ await transaction.begin();
613
+ for (const keySet of keys) {
614
+ const conditions = [];
615
+ const request = transaction.request();
616
+ let paramIndex = 0;
617
+ Object.entries(keySet).forEach(([key, value]) => {
618
+ const parsedKey = parseSqlIdentifier(key, "column name");
619
+ const paramName = `p${paramIndex++}`;
620
+ conditions.push(`[${parsedKey}] = @${paramName}`);
621
+ const preparedValue = this.prepareValue(value, key, tableName);
622
+ if (preparedValue === null || preparedValue === void 0) {
623
+ request.input(paramName, this.getMssqlType(tableName, key), null);
624
+ } else {
625
+ request.input(paramName, preparedValue);
626
+ }
954
627
  });
628
+ const deleteSql = `DELETE FROM ${tableName_} WHERE ${conditions.join(" AND ")}`;
629
+ await request.query(deleteSql);
955
630
  }
956
- query += ` ${orderByStatement} OFFSET 0 ROWS FETCH NEXT @limit ROWS ONLY`;
957
- request.input("limit", limit);
958
- const result = await request.query(query);
959
- const remainingRows = result.recordset || [];
960
- rows.push(...remainingRows);
961
- rows.sort((a, b) => {
962
- const timeDiff = a.seq_id - b.seq_id;
963
- return timeDiff;
964
- });
965
- rows = rows.map(({ seq_id, ...rest }) => rest);
966
- const fetchedMessages = (rows || []).map((message) => {
967
- if (typeof message.content === "string") {
968
- try {
969
- message.content = JSON.parse(message.content);
970
- } catch {
971
- }
972
- }
973
- if (format === "v1") {
974
- if (Array.isArray(message.content)) ; else if (typeof message.content === "object" && message.content && Array.isArray(message.content.parts)) {
975
- message.content = message.content.parts;
976
- } else {
977
- message.content = [{ type: "text", text: "" }];
631
+ await transaction.commit();
632
+ } catch (error) {
633
+ await transaction.rollback();
634
+ throw new MastraError(
635
+ {
636
+ id: createStorageErrorId("MSSQL", "BATCH_DELETE", "FAILED"),
637
+ domain: ErrorDomain.STORAGE,
638
+ category: ErrorCategory.THIRD_PARTY,
639
+ details: {
640
+ tableName,
641
+ numberOfRecords: keys.length
978
642
  }
979
- } else {
980
- if (typeof message.content !== "object" || !message.content || !("parts" in message.content)) {
981
- message.content = { format: 2, parts: [{ type: "text", text: "" }] };
643
+ },
644
+ error
645
+ );
646
+ }
647
+ }
648
+ /**
649
+ * Create a new index on a table
650
+ */
651
+ async createIndex(options) {
652
+ try {
653
+ const { name, table, columns, unique = false, where } = options;
654
+ const schemaName = this.schemaName || "dbo";
655
+ const fullTableName = getTableName({
656
+ indexName: table,
657
+ schemaName: getSchemaName(this.schemaName)
658
+ });
659
+ const indexNameSafe = parseSqlIdentifier(name, "index name");
660
+ const checkRequest = this.pool.request();
661
+ checkRequest.input("indexName", indexNameSafe);
662
+ checkRequest.input("schemaName", schemaName);
663
+ checkRequest.input("tableName", table);
664
+ const indexExists = await checkRequest.query(`
665
+ SELECT 1 as found
666
+ FROM sys.indexes i
667
+ INNER JOIN sys.tables t ON i.object_id = t.object_id
668
+ INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
669
+ WHERE i.name = @indexName
670
+ AND s.name = @schemaName
671
+ AND t.name = @tableName
672
+ `);
673
+ if (indexExists.recordset && indexExists.recordset.length > 0) {
674
+ return;
675
+ }
676
+ const uniqueStr = unique ? "UNIQUE " : "";
677
+ const columnsStr = columns.map((col) => {
678
+ if (col.includes(" DESC") || col.includes(" ASC")) {
679
+ const [colName, ...modifiers] = col.split(" ");
680
+ if (!colName) {
681
+ throw new Error(`Invalid column specification: ${col}`);
982
682
  }
683
+ return `[${parseSqlIdentifier(colName, "column name")}] ${modifiers.join(" ")}`;
983
684
  }
984
- if (message.type === "v2") delete message.type;
985
- return message;
986
- });
987
- return format === "v2" ? fetchedMessages.map(
988
- (m) => ({ ...m, content: m.content || { format: 2, parts: [{ type: "text", text: "" }] } })
989
- ) : fetchedMessages;
685
+ return `[${parseSqlIdentifier(col, "column name")}]`;
686
+ }).join(", ");
687
+ const whereStr = where ? ` WHERE ${where}` : "";
688
+ const createIndexSql = `CREATE ${uniqueStr}INDEX [${indexNameSafe}] ON ${fullTableName} (${columnsStr})${whereStr}`;
689
+ await this.pool.request().query(createIndexSql);
990
690
  } catch (error) {
991
- const mastraError = new MastraError(
691
+ throw new MastraError(
992
692
  {
993
- id: "MASTRA_STORAGE_MSSQL_STORE_GET_MESSAGES_FAILED",
693
+ id: createStorageErrorId("MSSQL", "INDEX_CREATE", "FAILED"),
994
694
  domain: ErrorDomain.STORAGE,
995
695
  category: ErrorCategory.THIRD_PARTY,
996
696
  details: {
997
- threadId
697
+ indexName: options.name,
698
+ tableName: options.table
998
699
  }
999
700
  },
1000
701
  error
1001
702
  );
1002
- this.logger?.error?.(mastraError.toString());
1003
- this.logger?.trackException(mastraError);
1004
- return [];
1005
703
  }
1006
704
  }
1007
- async getMessagesPaginated(args) {
1008
- const { threadId, selectBy } = args;
1009
- const { page = 0, perPage: perPageInput } = selectBy?.pagination || {};
1010
- const orderByStatement = `ORDER BY [seq_id] DESC`;
1011
- if (selectBy?.include?.length) {
1012
- await this._getIncludedMessages({ threadId, selectBy, orderByStatement });
1013
- }
705
+ /**
706
+ * Drop an existing index
707
+ */
708
+ async dropIndex(indexName) {
1014
709
  try {
1015
- const { threadId: threadId2, format, selectBy: selectBy2 } = args;
1016
- const { page: page2 = 0, perPage: perPageInput2, dateRange } = selectBy2?.pagination || {};
1017
- const fromDate = dateRange?.start;
1018
- const toDate = dateRange?.end;
1019
- const selectStatement = `SELECT seq_id, id, content, role, type, [createdAt], thread_id AS threadId`;
1020
- const orderByStatement2 = `ORDER BY [seq_id] DESC`;
1021
- let messages2 = [];
1022
- if (selectBy2?.include?.length) {
1023
- const includeMessages = await this._getIncludedMessages({ threadId: threadId2, selectBy: selectBy2, orderByStatement: orderByStatement2 });
1024
- if (includeMessages) messages2.push(...includeMessages);
1025
- }
1026
- const perPage = perPageInput2 !== void 0 ? perPageInput2 : this.resolveMessageLimit({ last: selectBy2?.last, defaultLimit: 40 });
1027
- const currentOffset = page2 * perPage;
1028
- const conditions = ["[thread_id] = @threadId"];
1029
- const request = this.pool.request();
1030
- request.input("threadId", threadId2);
1031
- if (fromDate instanceof Date && !isNaN(fromDate.getTime())) {
1032
- conditions.push("[createdAt] >= @fromDate");
1033
- request.input("fromDate", fromDate.toISOString());
1034
- }
1035
- if (toDate instanceof Date && !isNaN(toDate.getTime())) {
1036
- conditions.push("[createdAt] <= @toDate");
1037
- request.input("toDate", toDate.toISOString());
710
+ const schemaName = this.schemaName || "dbo";
711
+ const indexNameSafe = parseSqlIdentifier(indexName, "index name");
712
+ const checkRequest = this.pool.request();
713
+ checkRequest.input("indexName", indexNameSafe);
714
+ checkRequest.input("schemaName", schemaName);
715
+ const result = await checkRequest.query(`
716
+ SELECT t.name as table_name
717
+ FROM sys.indexes i
718
+ INNER JOIN sys.tables t ON i.object_id = t.object_id
719
+ INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
720
+ WHERE i.name = @indexName
721
+ AND s.name = @schemaName
722
+ `);
723
+ if (!result.recordset || result.recordset.length === 0) {
724
+ return;
1038
725
  }
1039
- const whereClause = `WHERE ${conditions.join(" AND ")}`;
1040
- const countQuery = `SELECT COUNT(*) as total FROM ${this.getTableName(TABLE_MESSAGES)} ${whereClause}`;
1041
- const countResult = await request.query(countQuery);
1042
- const total = parseInt(countResult.recordset[0]?.total, 10) || 0;
1043
- if (total === 0 && messages2.length > 0) {
1044
- const parsedIncluded = this._parseAndFormatMessages(messages2, format);
1045
- return {
1046
- messages: parsedIncluded,
1047
- total: parsedIncluded.length,
1048
- page: page2,
1049
- perPage,
1050
- hasMore: false
1051
- };
726
+ if (result.recordset.length > 1) {
727
+ const tables = result.recordset.map((r) => r.table_name).join(", ");
728
+ throw new MastraError({
729
+ id: createStorageErrorId("MSSQL", "INDEX", "AMBIGUOUS"),
730
+ domain: ErrorDomain.STORAGE,
731
+ category: ErrorCategory.USER,
732
+ text: `Index "${indexNameSafe}" exists on multiple tables (${tables}) in schema "${schemaName}". Please drop indexes manually or ensure unique index names.`
733
+ });
1052
734
  }
1053
- const excludeIds = messages2.map((m) => m.id);
1054
- if (excludeIds.length > 0) {
1055
- const excludeParams = excludeIds.map((_, idx) => `@id${idx}`);
1056
- conditions.push(`id NOT IN (${excludeParams.join(", ")})`);
1057
- excludeIds.forEach((id, idx) => request.input(`id${idx}`, id));
1058
- }
1059
- const finalWhereClause = `WHERE ${conditions.join(" AND ")}`;
1060
- const dataQuery = `${selectStatement} FROM ${this.getTableName(TABLE_MESSAGES)} ${finalWhereClause} ${orderByStatement2} OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY`;
1061
- request.input("offset", currentOffset);
1062
- request.input("limit", perPage);
1063
- const rowsResult = await request.query(dataQuery);
1064
- const rows = rowsResult.recordset || [];
1065
- rows.sort((a, b) => a.seq_id - b.seq_id);
1066
- messages2.push(...rows);
1067
- const parsed = this._parseAndFormatMessages(messages2, format);
1068
- return {
1069
- messages: parsed,
1070
- total: total + excludeIds.length,
1071
- page: page2,
1072
- perPage,
1073
- hasMore: currentOffset + rows.length < total
1074
- };
735
+ const tableName = result.recordset[0].table_name;
736
+ const fullTableName = getTableName({
737
+ indexName: tableName,
738
+ schemaName: getSchemaName(this.schemaName)
739
+ });
740
+ const dropSql = `DROP INDEX [${indexNameSafe}] ON ${fullTableName}`;
741
+ await this.pool.request().query(dropSql);
1075
742
  } catch (error) {
1076
- const mastraError = new MastraError(
743
+ throw new MastraError(
1077
744
  {
1078
- id: "MASTRA_STORAGE_MSSQL_STORE_GET_MESSAGES_PAGINATED_FAILED",
745
+ id: createStorageErrorId("MSSQL", "INDEX_DROP", "FAILED"),
1079
746
  domain: ErrorDomain.STORAGE,
1080
747
  category: ErrorCategory.THIRD_PARTY,
1081
748
  details: {
1082
- threadId,
1083
- page
749
+ indexName
1084
750
  }
1085
751
  },
1086
752
  error
1087
753
  );
1088
- this.logger?.error?.(mastraError.toString());
1089
- this.logger?.trackException(mastraError);
1090
- return { messages: [], total: 0, page, perPage: perPageInput || 40, hasMore: false };
1091
754
  }
1092
755
  }
1093
- _parseAndFormatMessages(messages, format) {
1094
- const parsedMessages = messages.map((message) => {
1095
- let parsed = message;
1096
- if (typeof parsed.content === "string") {
1097
- try {
1098
- parsed = { ...parsed, content: JSON.parse(parsed.content) };
1099
- } catch {
1100
- }
1101
- }
1102
- if (format === "v1") {
1103
- if (Array.isArray(parsed.content)) ; else if (parsed.content?.parts) {
1104
- parsed.content = parsed.content.parts;
1105
- } else {
1106
- parsed.content = [{ type: "text", text: "" }];
1107
- }
756
+ /**
757
+ * List indexes for a specific table or all tables
758
+ */
759
+ async listIndexes(tableName) {
760
+ try {
761
+ const schemaName = this.schemaName || "dbo";
762
+ let query;
763
+ const request = this.pool.request();
764
+ request.input("schemaName", schemaName);
765
+ if (tableName) {
766
+ query = `
767
+ SELECT
768
+ i.name as name,
769
+ o.name as [table],
770
+ i.is_unique as is_unique,
771
+ CAST(SUM(s.used_page_count) * 8 / 1024.0 AS VARCHAR(50)) + ' MB' as size
772
+ FROM sys.indexes i
773
+ INNER JOIN sys.objects o ON i.object_id = o.object_id
774
+ INNER JOIN sys.schemas sch ON o.schema_id = sch.schema_id
775
+ LEFT JOIN sys.dm_db_partition_stats s ON i.object_id = s.object_id AND i.index_id = s.index_id
776
+ WHERE sch.name = @schemaName
777
+ AND o.name = @tableName
778
+ AND i.name IS NOT NULL
779
+ GROUP BY i.name, o.name, i.is_unique
780
+ `;
781
+ request.input("tableName", tableName);
1108
782
  } else {
1109
- if (!parsed.content?.parts) {
1110
- parsed = { ...parsed, content: { format: 2, parts: [{ type: "text", text: "" }] } };
1111
- }
783
+ query = `
784
+ SELECT
785
+ i.name as name,
786
+ o.name as [table],
787
+ i.is_unique as is_unique,
788
+ CAST(SUM(s.used_page_count) * 8 / 1024.0 AS VARCHAR(50)) + ' MB' as size
789
+ FROM sys.indexes i
790
+ INNER JOIN sys.objects o ON i.object_id = o.object_id
791
+ INNER JOIN sys.schemas sch ON o.schema_id = sch.schema_id
792
+ LEFT JOIN sys.dm_db_partition_stats s ON i.object_id = s.object_id AND i.index_id = s.index_id
793
+ WHERE sch.name = @schemaName
794
+ AND i.name IS NOT NULL
795
+ GROUP BY i.name, o.name, i.is_unique
796
+ `;
1112
797
  }
1113
- return parsed;
1114
- });
1115
- const list = new MessageList().add(parsedMessages, "memory");
1116
- return format === "v2" ? list.get.all.v2() : list.get.all.v1();
1117
- }
1118
- async saveMessages({
1119
- messages,
1120
- format
1121
- }) {
1122
- if (messages.length === 0) return messages;
1123
- const threadId = messages[0]?.threadId;
1124
- if (!threadId) {
1125
- throw new MastraError({
1126
- id: "MASTRA_STORAGE_MSSQL_STORE_SAVE_MESSAGES_FAILED",
1127
- domain: ErrorDomain.STORAGE,
1128
- category: ErrorCategory.THIRD_PARTY,
1129
- text: `Thread ID is required`
1130
- });
1131
- }
1132
- const thread = await this.getThreadById({ threadId });
1133
- if (!thread) {
1134
- throw new MastraError({
1135
- id: "MASTRA_STORAGE_MSSQL_STORE_SAVE_MESSAGES_FAILED",
1136
- domain: ErrorDomain.STORAGE,
1137
- category: ErrorCategory.THIRD_PARTY,
1138
- text: `Thread ${threadId} not found`,
1139
- details: { threadId }
1140
- });
1141
- }
1142
- const tableMessages = this.getTableName(TABLE_MESSAGES);
1143
- const tableThreads = this.getTableName(TABLE_THREADS);
1144
- try {
1145
- const transaction = this.pool.transaction();
1146
- await transaction.begin();
1147
- try {
1148
- for (const message of messages) {
1149
- if (!message.threadId) {
1150
- throw new Error(
1151
- `Expected to find a threadId for message, but couldn't find one. An unexpected error has occurred.`
1152
- );
1153
- }
1154
- if (!message.resourceId) {
1155
- throw new Error(
1156
- `Expected to find a resourceId for message, but couldn't find one. An unexpected error has occurred.`
1157
- );
1158
- }
1159
- const request = transaction.request();
1160
- request.input("id", message.id);
1161
- request.input("thread_id", message.threadId);
1162
- request.input(
1163
- "content",
1164
- typeof message.content === "string" ? message.content : JSON.stringify(message.content)
1165
- );
1166
- request.input("createdAt", message.createdAt.toISOString() || (/* @__PURE__ */ new Date()).toISOString());
1167
- request.input("role", message.role);
1168
- request.input("type", message.type || "v2");
1169
- request.input("resourceId", message.resourceId);
1170
- const mergeSql = `MERGE INTO ${tableMessages} AS target
1171
- USING (SELECT @id AS id) AS src
1172
- ON target.id = src.id
1173
- WHEN MATCHED THEN UPDATE SET
1174
- thread_id = @thread_id,
1175
- content = @content,
1176
- [createdAt] = @createdAt,
1177
- role = @role,
1178
- type = @type,
1179
- resourceId = @resourceId
1180
- WHEN NOT MATCHED THEN INSERT (id, thread_id, content, [createdAt], role, type, resourceId)
1181
- VALUES (@id, @thread_id, @content, @createdAt, @role, @type, @resourceId);`;
1182
- await request.query(mergeSql);
1183
- }
1184
- const threadReq = transaction.request();
1185
- threadReq.input("updatedAt", (/* @__PURE__ */ new Date()).toISOString());
1186
- threadReq.input("id", threadId);
1187
- await threadReq.query(`UPDATE ${tableThreads} SET [updatedAt] = @updatedAt WHERE id = @id`);
1188
- await transaction.commit();
1189
- } catch (error) {
1190
- await transaction.rollback();
1191
- throw error;
798
+ const result = await request.query(query);
799
+ const indexes = [];
800
+ for (const row of result.recordset) {
801
+ const colRequest = this.pool.request();
802
+ colRequest.input("indexName", row.name);
803
+ colRequest.input("schemaName", schemaName);
804
+ const colResult = await colRequest.query(`
805
+ SELECT c.name as column_name
806
+ FROM sys.indexes i
807
+ INNER JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
808
+ INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
809
+ INNER JOIN sys.objects o ON i.object_id = o.object_id
810
+ INNER JOIN sys.schemas s ON o.schema_id = s.schema_id
811
+ WHERE i.name = @indexName
812
+ AND s.name = @schemaName
813
+ ORDER BY ic.key_ordinal
814
+ `);
815
+ indexes.push({
816
+ name: row.name,
817
+ table: row.table,
818
+ columns: colResult.recordset.map((c) => c.column_name),
819
+ unique: row.is_unique || false,
820
+ size: row.size || "0 MB",
821
+ definition: ""
822
+ // MSSQL doesn't store definition like PG
823
+ });
1192
824
  }
1193
- const messagesWithParsedContent = messages.map((message) => {
1194
- if (typeof message.content === "string") {
1195
- try {
1196
- return { ...message, content: JSON.parse(message.content) };
1197
- } catch {
1198
- return message;
1199
- }
1200
- }
1201
- return message;
1202
- });
1203
- const list = new MessageList().add(messagesWithParsedContent, "memory");
1204
- if (format === "v2") return list.get.all.v2();
1205
- return list.get.all.v1();
825
+ return indexes;
1206
826
  } catch (error) {
1207
827
  throw new MastraError(
1208
828
  {
1209
- id: "MASTRA_STORAGE_MSSQL_STORE_SAVE_MESSAGES_FAILED",
829
+ id: createStorageErrorId("MSSQL", "INDEX_LIST", "FAILED"),
1210
830
  domain: ErrorDomain.STORAGE,
1211
831
  category: ErrorCategory.THIRD_PARTY,
1212
- details: { threadId }
832
+ details: tableName ? {
833
+ tableName
834
+ } : {}
1213
835
  },
1214
836
  error
1215
837
  );
1216
838
  }
1217
839
  }
1218
- async persistWorkflowSnapshot({
1219
- workflowName,
1220
- runId,
1221
- snapshot
1222
- }) {
1223
- const table = this.getTableName(TABLE_WORKFLOW_SNAPSHOT);
1224
- const now = (/* @__PURE__ */ new Date()).toISOString();
840
+ /**
841
+ * Get detailed statistics for a specific index
842
+ */
843
+ async describeIndex(indexName) {
1225
844
  try {
845
+ const schemaName = this.schemaName || "dbo";
1226
846
  const request = this.pool.request();
1227
- request.input("workflow_name", workflowName);
1228
- request.input("run_id", runId);
1229
- request.input("snapshot", JSON.stringify(snapshot));
1230
- request.input("createdAt", now);
1231
- request.input("updatedAt", now);
1232
- const mergeSql = `MERGE INTO ${table} AS target
1233
- USING (SELECT @workflow_name AS workflow_name, @run_id AS run_id) AS src
1234
- ON target.workflow_name = src.workflow_name AND target.run_id = src.run_id
1235
- WHEN MATCHED THEN UPDATE SET
1236
- snapshot = @snapshot,
1237
- [updatedAt] = @updatedAt
1238
- WHEN NOT MATCHED THEN INSERT (workflow_name, run_id, snapshot, [createdAt], [updatedAt])
1239
- VALUES (@workflow_name, @run_id, @snapshot, @createdAt, @updatedAt);`;
1240
- await request.query(mergeSql);
847
+ request.input("indexName", indexName);
848
+ request.input("schemaName", schemaName);
849
+ const query = `
850
+ SELECT
851
+ i.name as name,
852
+ o.name as [table],
853
+ i.is_unique as is_unique,
854
+ CAST(SUM(s.used_page_count) * 8 / 1024.0 AS VARCHAR(50)) + ' MB' as size,
855
+ i.type_desc as method,
856
+ ISNULL(us.user_scans, 0) as scans,
857
+ ISNULL(us.user_seeks + us.user_scans, 0) as tuples_read,
858
+ ISNULL(us.user_lookups, 0) as tuples_fetched
859
+ FROM sys.indexes i
860
+ INNER JOIN sys.objects o ON i.object_id = o.object_id
861
+ INNER JOIN sys.schemas sch ON o.schema_id = sch.schema_id
862
+ LEFT JOIN sys.dm_db_partition_stats s ON i.object_id = s.object_id AND i.index_id = s.index_id
863
+ LEFT JOIN sys.dm_db_index_usage_stats us ON i.object_id = us.object_id AND i.index_id = us.index_id
864
+ WHERE i.name = @indexName
865
+ AND sch.name = @schemaName
866
+ GROUP BY i.name, o.name, i.is_unique, i.type_desc, us.user_seeks, us.user_scans, us.user_lookups
867
+ `;
868
+ const result = await request.query(query);
869
+ if (!result.recordset || result.recordset.length === 0) {
870
+ throw new Error(`Index "${indexName}" not found in schema "${schemaName}"`);
871
+ }
872
+ const row = result.recordset[0];
873
+ const colRequest = this.pool.request();
874
+ colRequest.input("indexName", indexName);
875
+ colRequest.input("schemaName", schemaName);
876
+ const colResult = await colRequest.query(`
877
+ SELECT c.name as column_name
878
+ FROM sys.indexes i
879
+ INNER JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
880
+ INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
881
+ INNER JOIN sys.objects o ON i.object_id = o.object_id
882
+ INNER JOIN sys.schemas s ON o.schema_id = s.schema_id
883
+ WHERE i.name = @indexName
884
+ AND s.name = @schemaName
885
+ ORDER BY ic.key_ordinal
886
+ `);
887
+ return {
888
+ name: row.name,
889
+ table: row.table,
890
+ columns: colResult.recordset.map((c) => c.column_name),
891
+ unique: row.is_unique || false,
892
+ size: row.size || "0 MB",
893
+ definition: "",
894
+ method: row.method?.toLowerCase() || "nonclustered",
895
+ scans: Number(row.scans) || 0,
896
+ tuples_read: Number(row.tuples_read) || 0,
897
+ tuples_fetched: Number(row.tuples_fetched) || 0
898
+ };
1241
899
  } catch (error) {
1242
900
  throw new MastraError(
1243
901
  {
1244
- id: "MASTRA_STORAGE_MSSQL_STORE_PERSIST_WORKFLOW_SNAPSHOT_FAILED",
902
+ id: createStorageErrorId("MSSQL", "INDEX_DESCRIBE", "FAILED"),
1245
903
  domain: ErrorDomain.STORAGE,
1246
904
  category: ErrorCategory.THIRD_PARTY,
1247
905
  details: {
1248
- workflowName,
1249
- runId
906
+ indexName
1250
907
  }
1251
908
  },
1252
909
  error
1253
910
  );
1254
911
  }
1255
912
  }
1256
- async loadWorkflowSnapshot({
1257
- workflowName,
1258
- runId
1259
- }) {
913
+ /**
914
+ * Returns definitions for automatic performance indexes
915
+ * IMPORTANT: Uses seq_id DESC instead of createdAt DESC for MSSQL due to millisecond accuracy limitations
916
+ * NOTE: Using NVARCHAR(400) for text columns (800 bytes) leaves room for composite indexes
917
+ */
918
+ getAutomaticIndexDefinitions() {
919
+ const schemaPrefix = this.schemaName ? `${this.schemaName}_` : "";
920
+ return [
921
+ // Composite indexes for optimal filtering + sorting performance
922
+ // NVARCHAR(400) = 800 bytes, plus BIGINT (8 bytes) = 808 bytes total (under 900-byte limit)
923
+ {
924
+ name: `${schemaPrefix}mastra_threads_resourceid_seqid_idx`,
925
+ table: TABLE_THREADS,
926
+ columns: ["resourceId", "seq_id DESC"]
927
+ },
928
+ {
929
+ name: `${schemaPrefix}mastra_messages_thread_id_seqid_idx`,
930
+ table: TABLE_MESSAGES,
931
+ columns: ["thread_id", "seq_id DESC"]
932
+ },
933
+ {
934
+ name: `${schemaPrefix}mastra_traces_name_seqid_idx`,
935
+ table: TABLE_TRACES,
936
+ columns: ["name", "seq_id DESC"]
937
+ },
938
+ {
939
+ name: `${schemaPrefix}mastra_scores_trace_id_span_id_seqid_idx`,
940
+ table: TABLE_SCORERS,
941
+ columns: ["traceId", "spanId", "seq_id DESC"]
942
+ },
943
+ // Spans indexes for optimal trace querying
944
+ {
945
+ name: `${schemaPrefix}mastra_ai_spans_traceid_startedat_idx`,
946
+ table: TABLE_SPANS,
947
+ columns: ["traceId", "startedAt DESC"]
948
+ },
949
+ {
950
+ name: `${schemaPrefix}mastra_ai_spans_parentspanid_startedat_idx`,
951
+ table: TABLE_SPANS,
952
+ columns: ["parentSpanId", "startedAt DESC"]
953
+ },
954
+ {
955
+ name: `${schemaPrefix}mastra_ai_spans_name_idx`,
956
+ table: TABLE_SPANS,
957
+ columns: ["name"]
958
+ },
959
+ {
960
+ name: `${schemaPrefix}mastra_ai_spans_spantype_startedat_idx`,
961
+ table: TABLE_SPANS,
962
+ columns: ["spanType", "startedAt DESC"]
963
+ }
964
+ ];
965
+ }
966
+ /**
967
+ * Creates automatic indexes for optimal query performance
968
+ * Uses getAutomaticIndexDefinitions() to determine which indexes to create
969
+ */
970
+ async createAutomaticIndexes() {
1260
971
  try {
1261
- const result = await this.load({
1262
- tableName: TABLE_WORKFLOW_SNAPSHOT,
1263
- keys: {
1264
- workflow_name: workflowName,
1265
- run_id: runId
972
+ const indexes = this.getAutomaticIndexDefinitions();
973
+ for (const indexOptions of indexes) {
974
+ try {
975
+ await this.createIndex(indexOptions);
976
+ } catch (error) {
977
+ this.logger?.warn?.(`Failed to create index ${indexOptions.name}:`, error);
1266
978
  }
1267
- });
1268
- if (!result) {
1269
- return null;
1270
979
  }
1271
- return result.snapshot;
1272
980
  } catch (error) {
1273
981
  throw new MastraError(
1274
982
  {
1275
- id: "MASTRA_STORAGE_MSSQL_STORE_LOAD_WORKFLOW_SNAPSHOT_FAILED",
983
+ id: createStorageErrorId("MSSQL", "CREATE_PERFORMANCE_INDEXES", "FAILED"),
1276
984
  domain: ErrorDomain.STORAGE,
1277
- category: ErrorCategory.THIRD_PARTY,
1278
- details: {
1279
- workflowName,
1280
- runId
1281
- }
985
+ category: ErrorCategory.THIRD_PARTY
1282
986
  },
1283
987
  error
1284
988
  );
1285
989
  }
1286
990
  }
1287
- async hasColumn(table, column) {
1288
- const schema = this.schema || "dbo";
1289
- const request = this.pool.request();
1290
- request.input("schema", schema);
1291
- request.input("table", table);
1292
- request.input("column", column);
1293
- request.input("columnLower", column.toLowerCase());
1294
- const result = await request.query(
1295
- `SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = @table AND (COLUMN_NAME = @column OR COLUMN_NAME = @columnLower)`
1296
- );
1297
- return result.recordset.length > 0;
991
+ };
992
+ function getSchemaName2(schema) {
993
+ return schema ? `[${parseSqlIdentifier(schema, "schema name")}]` : void 0;
994
+ }
995
+ function getTableName2({ indexName, schemaName }) {
996
+ const parsedIndexName = parseSqlIdentifier(indexName, "index name");
997
+ const quotedIndexName = `[${parsedIndexName}]`;
998
+ const quotedSchemaName = schemaName;
999
+ return quotedSchemaName ? `${quotedSchemaName}.${quotedIndexName}` : quotedIndexName;
1000
+ }
1001
+ function buildDateRangeFilter(dateRange, fieldName) {
1002
+ const filters = {};
1003
+ if (dateRange?.start) {
1004
+ filters[`${fieldName}_gte`] = dateRange.start;
1298
1005
  }
1299
- parseWorkflowRun(row) {
1300
- let parsedSnapshot = row.snapshot;
1301
- if (typeof parsedSnapshot === "string") {
1302
- try {
1303
- parsedSnapshot = JSON.parse(row.snapshot);
1304
- } catch (e) {
1305
- console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
1306
- }
1307
- }
1308
- return {
1309
- workflowName: row.workflow_name,
1310
- runId: row.run_id,
1311
- snapshot: parsedSnapshot,
1312
- createdAt: row.createdAt,
1313
- updatedAt: row.updatedAt,
1314
- resourceId: row.resourceId
1315
- };
1006
+ if (dateRange?.end) {
1007
+ filters[`${fieldName}_lte`] = dateRange.end;
1316
1008
  }
1317
- async getWorkflowRuns({
1318
- workflowName,
1319
- fromDate,
1320
- toDate,
1321
- limit,
1322
- offset,
1323
- resourceId
1324
- } = {}) {
1325
- try {
1326
- const conditions = [];
1327
- const paramMap = {};
1328
- if (workflowName) {
1329
- conditions.push(`[workflow_name] = @workflowName`);
1330
- paramMap["workflowName"] = workflowName;
1331
- }
1332
- if (resourceId) {
1333
- const hasResourceId = await this.hasColumn(TABLE_WORKFLOW_SNAPSHOT, "resourceId");
1334
- if (hasResourceId) {
1335
- conditions.push(`[resourceId] = @resourceId`);
1336
- paramMap["resourceId"] = resourceId;
1337
- } else {
1338
- console.warn(`[${TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
1009
+ return filters;
1010
+ }
1011
+ function isInOperator(value) {
1012
+ return typeof value === "object" && value !== null && "$in" in value && Array.isArray(value.$in);
1013
+ }
1014
+ function prepareWhereClause(filters, _schema) {
1015
+ const conditions = [];
1016
+ const params = {};
1017
+ let paramIndex = 1;
1018
+ Object.entries(filters).forEach(([key, value]) => {
1019
+ if (value === void 0) return;
1020
+ if (key.endsWith("_gte")) {
1021
+ const paramName = `p${paramIndex++}`;
1022
+ const fieldName = key.slice(0, -4);
1023
+ conditions.push(`[${parseSqlIdentifier(fieldName, "field name")}] >= @${paramName}`);
1024
+ params[paramName] = value instanceof Date ? value.toISOString() : value;
1025
+ } else if (key.endsWith("_lte")) {
1026
+ const paramName = `p${paramIndex++}`;
1027
+ const fieldName = key.slice(0, -4);
1028
+ conditions.push(`[${parseSqlIdentifier(fieldName, "field name")}] <= @${paramName}`);
1029
+ params[paramName] = value instanceof Date ? value.toISOString() : value;
1030
+ } else if (value === null) {
1031
+ conditions.push(`[${parseSqlIdentifier(key, "field name")}] IS NULL`);
1032
+ } else if (isInOperator(value)) {
1033
+ const inValues = value.$in;
1034
+ if (inValues.length === 0) {
1035
+ conditions.push("1 = 0");
1036
+ } else if (inValues.length === 1) {
1037
+ const paramName = `p${paramIndex++}`;
1038
+ conditions.push(`[${parseSqlIdentifier(key, "field name")}] = @${paramName}`);
1039
+ params[paramName] = inValues[0] instanceof Date ? inValues[0].toISOString() : inValues[0];
1040
+ } else {
1041
+ const inParamNames = [];
1042
+ for (const item of inValues) {
1043
+ const paramName = `p${paramIndex++}`;
1044
+ inParamNames.push(`@${paramName}`);
1045
+ params[paramName] = item instanceof Date ? item.toISOString() : item;
1339
1046
  }
1047
+ conditions.push(`[${parseSqlIdentifier(key, "field name")}] IN (${inParamNames.join(", ")})`);
1340
1048
  }
1341
- if (fromDate instanceof Date && !isNaN(fromDate.getTime())) {
1342
- conditions.push(`[createdAt] >= @fromDate`);
1343
- paramMap[`fromDate`] = fromDate.toISOString();
1049
+ } else if (Array.isArray(value)) {
1050
+ if (value.length === 0) {
1051
+ conditions.push("1 = 0");
1052
+ } else if (value.length === 1) {
1053
+ const paramName = `p${paramIndex++}`;
1054
+ conditions.push(`[${parseSqlIdentifier(key, "field name")}] = @${paramName}`);
1055
+ params[paramName] = value[0] instanceof Date ? value[0].toISOString() : value[0];
1056
+ } else {
1057
+ const inParamNames = [];
1058
+ for (const item of value) {
1059
+ const paramName = `p${paramIndex++}`;
1060
+ inParamNames.push(`@${paramName}`);
1061
+ params[paramName] = item instanceof Date ? item.toISOString() : item;
1062
+ }
1063
+ conditions.push(`[${parseSqlIdentifier(key, "field name")}] IN (${inParamNames.join(", ")})`);
1344
1064
  }
1345
- if (toDate instanceof Date && !isNaN(toDate.getTime())) {
1346
- conditions.push(`[createdAt] <= @toDate`);
1347
- paramMap[`toDate`] = toDate.toISOString();
1065
+ } else {
1066
+ const paramName = `p${paramIndex++}`;
1067
+ conditions.push(`[${parseSqlIdentifier(key, "field name")}] = @${paramName}`);
1068
+ params[paramName] = value instanceof Date ? value.toISOString() : value;
1069
+ }
1070
+ });
1071
+ return {
1072
+ sql: conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "",
1073
+ params
1074
+ };
1075
+ }
1076
+ function transformFromSqlRow({
1077
+ tableName,
1078
+ sqlRow
1079
+ }) {
1080
+ const schema = TABLE_SCHEMAS[tableName];
1081
+ const result = {};
1082
+ Object.entries(sqlRow).forEach(([key, value]) => {
1083
+ const columnSchema = schema?.[key];
1084
+ if (columnSchema?.type === "jsonb" && typeof value === "string") {
1085
+ try {
1086
+ result[key] = JSON.parse(value);
1087
+ } catch {
1088
+ result[key] = value;
1348
1089
  }
1349
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1350
- let total = 0;
1351
- const tableName = this.getTableName(TABLE_WORKFLOW_SNAPSHOT);
1352
- const request = this.pool.request();
1353
- Object.entries(paramMap).forEach(([key, value]) => {
1354
- if (value instanceof Date) {
1355
- request.input(key, sql.DateTime, value);
1356
- } else {
1357
- request.input(key, value);
1090
+ } else if (columnSchema?.type === "timestamp" && value && typeof value === "string") {
1091
+ result[key] = new Date(value);
1092
+ } else if (columnSchema?.type === "timestamp" && value instanceof Date) {
1093
+ result[key] = value;
1094
+ } else if (columnSchema?.type === "boolean") {
1095
+ result[key] = Boolean(value);
1096
+ } else {
1097
+ result[key] = value;
1098
+ }
1099
+ });
1100
+ return result;
1101
+ }
1102
+
1103
+ // src/storage/domains/memory/index.ts
1104
+ var MemoryMSSQL = class extends MemoryStorage {
1105
+ pool;
1106
+ schema;
1107
+ db;
1108
+ needsConnect;
1109
+ _parseAndFormatMessages(messages, format) {
1110
+ const messagesWithParsedContent = messages.map((message) => {
1111
+ if (typeof message.content === "string") {
1112
+ try {
1113
+ return { ...message, content: JSON.parse(message.content) };
1114
+ } catch {
1115
+ return message;
1358
1116
  }
1359
- });
1360
- if (limit !== void 0 && offset !== void 0) {
1361
- const countQuery = `SELECT COUNT(*) as count FROM ${tableName} ${whereClause}`;
1362
- const countResult = await request.query(countQuery);
1363
- total = Number(countResult.recordset[0]?.count || 0);
1364
1117
  }
1365
- let query = `SELECT * FROM ${tableName} ${whereClause} ORDER BY [seq_id] DESC`;
1366
- if (limit !== void 0 && offset !== void 0) {
1367
- query += ` OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY`;
1368
- request.input("limit", limit);
1369
- request.input("offset", offset);
1118
+ return message;
1119
+ });
1120
+ const cleanMessages = messagesWithParsedContent.map(({ seq_id, ...rest }) => rest);
1121
+ const list = new MessageList().add(cleanMessages, "memory");
1122
+ return format === "v2" ? list.get.all.db() : list.get.all.v1();
1123
+ }
1124
+ constructor(config) {
1125
+ super();
1126
+ const { pool, db, schema, needsConnect } = resolveMssqlConfig(config);
1127
+ this.pool = pool;
1128
+ this.schema = schema;
1129
+ this.db = db;
1130
+ this.needsConnect = needsConnect;
1131
+ }
1132
+ async init() {
1133
+ if (this.needsConnect) {
1134
+ await this.pool.connect();
1135
+ this.needsConnect = false;
1136
+ }
1137
+ await this.db.createTable({ tableName: TABLE_THREADS, schema: TABLE_SCHEMAS[TABLE_THREADS] });
1138
+ await this.db.createTable({ tableName: TABLE_MESSAGES, schema: TABLE_SCHEMAS[TABLE_MESSAGES] });
1139
+ await this.db.createTable({ tableName: TABLE_RESOURCES, schema: TABLE_SCHEMAS[TABLE_RESOURCES] });
1140
+ }
1141
+ async dangerouslyClearAll() {
1142
+ await this.db.clearTable({ tableName: TABLE_MESSAGES });
1143
+ await this.db.clearTable({ tableName: TABLE_THREADS });
1144
+ await this.db.clearTable({ tableName: TABLE_RESOURCES });
1145
+ }
1146
+ async getThreadById({ threadId }) {
1147
+ try {
1148
+ const sql5 = `SELECT
1149
+ id,
1150
+ [resourceId],
1151
+ title,
1152
+ metadata,
1153
+ [createdAt],
1154
+ [updatedAt]
1155
+ FROM ${getTableName2({ indexName: TABLE_THREADS, schemaName: getSchemaName2(this.schema) })}
1156
+ WHERE id = @threadId`;
1157
+ const request = this.pool.request();
1158
+ request.input("threadId", threadId);
1159
+ const resultSet = await request.query(sql5);
1160
+ const thread = resultSet.recordset[0] || null;
1161
+ if (!thread) {
1162
+ return null;
1370
1163
  }
1371
- const result = await request.query(query);
1372
- const runs = (result.recordset || []).map((row) => this.parseWorkflowRun(row));
1373
- return { runs, total: total || runs.length };
1164
+ return {
1165
+ ...thread,
1166
+ metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata,
1167
+ createdAt: thread.createdAt,
1168
+ updatedAt: thread.updatedAt
1169
+ };
1374
1170
  } catch (error) {
1375
1171
  throw new MastraError(
1376
1172
  {
1377
- id: "MASTRA_STORAGE_MSSQL_STORE_GET_WORKFLOW_RUNS_FAILED",
1173
+ id: createStorageErrorId("MSSQL", "GET_THREAD_BY_ID", "FAILED"),
1378
1174
  domain: ErrorDomain.STORAGE,
1379
1175
  category: ErrorCategory.THIRD_PARTY,
1380
1176
  details: {
1381
- workflowName: workflowName || "all"
1177
+ threadId
1382
1178
  }
1383
1179
  },
1384
1180
  error
1385
1181
  );
1386
1182
  }
1387
1183
  }
1388
- async getWorkflowRunById({
1389
- runId,
1390
- workflowName
1391
- }) {
1184
+ async listThreadsByResourceId(args) {
1185
+ const { resourceId, page = 0, perPage: perPageInput, orderBy } = args;
1186
+ if (page < 0) {
1187
+ throw new MastraError({
1188
+ id: createStorageErrorId("MSSQL", "LIST_THREADS_BY_RESOURCE_ID", "INVALID_PAGE"),
1189
+ domain: ErrorDomain.STORAGE,
1190
+ category: ErrorCategory.USER,
1191
+ text: "Page number must be non-negative",
1192
+ details: {
1193
+ resourceId,
1194
+ page
1195
+ }
1196
+ });
1197
+ }
1198
+ const perPage = normalizePerPage(perPageInput, 100);
1199
+ const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
1200
+ const { field, direction } = this.parseOrderBy(orderBy);
1392
1201
  try {
1393
- const conditions = [];
1394
- const paramMap = {};
1395
- if (runId) {
1396
- conditions.push(`[run_id] = @runId`);
1397
- paramMap["runId"] = runId;
1202
+ const baseQuery = `FROM ${getTableName2({ indexName: TABLE_THREADS, schemaName: getSchemaName2(this.schema) })} WHERE [resourceId] = @resourceId`;
1203
+ const countQuery = `SELECT COUNT(*) as count ${baseQuery}`;
1204
+ const countRequest = this.pool.request();
1205
+ countRequest.input("resourceId", resourceId);
1206
+ const countResult = await countRequest.query(countQuery);
1207
+ const total = parseInt(countResult.recordset[0]?.count ?? "0", 10);
1208
+ if (total === 0) {
1209
+ return {
1210
+ threads: [],
1211
+ total: 0,
1212
+ page,
1213
+ perPage: perPageForResponse,
1214
+ hasMore: false
1215
+ };
1398
1216
  }
1399
- if (workflowName) {
1400
- conditions.push(`[workflow_name] = @workflowName`);
1401
- paramMap["workflowName"] = workflowName;
1217
+ const orderByField = field === "createdAt" ? "[createdAt]" : "[updatedAt]";
1218
+ const dir = (direction || "DESC").toUpperCase() === "ASC" ? "ASC" : "DESC";
1219
+ const limitValue = perPageInput === false ? total : perPage;
1220
+ const dataQuery = `SELECT id, [resourceId], title, metadata, [createdAt], [updatedAt] ${baseQuery} ORDER BY ${orderByField} ${dir} OFFSET @offset ROWS FETCH NEXT @perPage ROWS ONLY`;
1221
+ const dataRequest = this.pool.request();
1222
+ dataRequest.input("resourceId", resourceId);
1223
+ dataRequest.input("offset", offset);
1224
+ if (limitValue > 2147483647) {
1225
+ dataRequest.input("perPage", sql.BigInt, limitValue);
1226
+ } else {
1227
+ dataRequest.input("perPage", limitValue);
1402
1228
  }
1403
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1404
- const tableName = this.getTableName(TABLE_WORKFLOW_SNAPSHOT);
1405
- const query = `SELECT * FROM ${tableName} ${whereClause}`;
1406
- const request = this.pool.request();
1407
- Object.entries(paramMap).forEach(([key, value]) => request.input(key, value));
1408
- const result = await request.query(query);
1409
- if (!result.recordset || result.recordset.length === 0) {
1410
- return null;
1229
+ const rowsResult = await dataRequest.query(dataQuery);
1230
+ const rows = rowsResult.recordset || [];
1231
+ const threads = rows.map((thread) => ({
1232
+ ...thread,
1233
+ metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata,
1234
+ createdAt: thread.createdAt,
1235
+ updatedAt: thread.updatedAt
1236
+ }));
1237
+ return {
1238
+ threads,
1239
+ total,
1240
+ page,
1241
+ perPage: perPageForResponse,
1242
+ hasMore: perPageInput === false ? false : offset + perPage < total
1243
+ };
1244
+ } catch (error) {
1245
+ const mastraError = new MastraError(
1246
+ {
1247
+ id: createStorageErrorId("MSSQL", "LIST_THREADS_BY_RESOURCE_ID", "FAILED"),
1248
+ domain: ErrorDomain.STORAGE,
1249
+ category: ErrorCategory.THIRD_PARTY,
1250
+ details: {
1251
+ resourceId,
1252
+ page
1253
+ }
1254
+ },
1255
+ error
1256
+ );
1257
+ this.logger?.error?.(mastraError.toString());
1258
+ this.logger?.trackException?.(mastraError);
1259
+ return {
1260
+ threads: [],
1261
+ total: 0,
1262
+ page,
1263
+ perPage: perPageForResponse,
1264
+ hasMore: false
1265
+ };
1266
+ }
1267
+ }
1268
+ async saveThread({ thread }) {
1269
+ try {
1270
+ const table = getTableName2({ indexName: TABLE_THREADS, schemaName: getSchemaName2(this.schema) });
1271
+ const mergeSql = `MERGE INTO ${table} WITH (HOLDLOCK) AS target
1272
+ USING (SELECT @id AS id) AS source
1273
+ ON (target.id = source.id)
1274
+ WHEN MATCHED THEN
1275
+ UPDATE SET
1276
+ [resourceId] = @resourceId,
1277
+ title = @title,
1278
+ metadata = @metadata,
1279
+ [updatedAt] = @updatedAt
1280
+ WHEN NOT MATCHED THEN
1281
+ INSERT (id, [resourceId], title, metadata, [createdAt], [updatedAt])
1282
+ VALUES (@id, @resourceId, @title, @metadata, @createdAt, @updatedAt);`;
1283
+ const req = this.pool.request();
1284
+ req.input("id", thread.id);
1285
+ req.input("resourceId", thread.resourceId);
1286
+ req.input("title", thread.title);
1287
+ const metadata = thread.metadata ? JSON.stringify(thread.metadata) : null;
1288
+ if (metadata === null) {
1289
+ req.input("metadata", sql.NVarChar, null);
1290
+ } else {
1291
+ req.input("metadata", metadata);
1411
1292
  }
1412
- return this.parseWorkflowRun(result.recordset[0]);
1293
+ req.input("createdAt", sql.DateTime2, thread.createdAt);
1294
+ req.input("updatedAt", sql.DateTime2, thread.updatedAt);
1295
+ await req.query(mergeSql);
1296
+ return thread;
1413
1297
  } catch (error) {
1414
1298
  throw new MastraError(
1415
1299
  {
1416
- id: "MASTRA_STORAGE_MSSQL_STORE_GET_WORKFLOW_RUN_BY_ID_FAILED",
1300
+ id: createStorageErrorId("MSSQL", "SAVE_THREAD", "FAILED"),
1417
1301
  domain: ErrorDomain.STORAGE,
1418
1302
  category: ErrorCategory.THIRD_PARTY,
1419
1303
  details: {
1420
- runId,
1421
- workflowName: workflowName || ""
1304
+ threadId: thread.id
1422
1305
  }
1423
1306
  },
1424
1307
  error
1425
1308
  );
1426
1309
  }
1427
1310
  }
1428
- async updateMessages({
1429
- messages
1311
+ /**
1312
+ * Updates a thread's title and metadata, merging with existing metadata. Returns the updated thread.
1313
+ */
1314
+ async updateThread({
1315
+ id,
1316
+ title,
1317
+ metadata
1430
1318
  }) {
1431
- if (!messages || messages.length === 0) {
1432
- return [];
1433
- }
1434
- const messageIds = messages.map((m) => m.id);
1319
+ const existingThread = await this.getThreadById({ threadId: id });
1320
+ if (!existingThread) {
1321
+ throw new MastraError({
1322
+ id: createStorageErrorId("MSSQL", "UPDATE_THREAD", "NOT_FOUND"),
1323
+ domain: ErrorDomain.STORAGE,
1324
+ category: ErrorCategory.USER,
1325
+ text: `Thread ${id} not found`,
1326
+ details: {
1327
+ threadId: id,
1328
+ title
1329
+ }
1330
+ });
1331
+ }
1332
+ const mergedMetadata = {
1333
+ ...existingThread.metadata,
1334
+ ...metadata
1335
+ };
1336
+ try {
1337
+ const table = getTableName2({ indexName: TABLE_THREADS, schemaName: getSchemaName2(this.schema) });
1338
+ const sql5 = `UPDATE ${table}
1339
+ SET title = @title,
1340
+ metadata = @metadata,
1341
+ [updatedAt] = @updatedAt
1342
+ OUTPUT INSERTED.*
1343
+ WHERE id = @id`;
1344
+ const req = this.pool.request();
1345
+ req.input("id", id);
1346
+ req.input("title", title);
1347
+ req.input("metadata", JSON.stringify(mergedMetadata));
1348
+ req.input("updatedAt", /* @__PURE__ */ new Date());
1349
+ const result = await req.query(sql5);
1350
+ let thread = result.recordset && result.recordset[0];
1351
+ if (thread && "seq_id" in thread) {
1352
+ const { seq_id, ...rest } = thread;
1353
+ thread = rest;
1354
+ }
1355
+ if (!thread) {
1356
+ throw new MastraError({
1357
+ id: createStorageErrorId("MSSQL", "UPDATE_THREAD", "NOT_FOUND"),
1358
+ domain: ErrorDomain.STORAGE,
1359
+ category: ErrorCategory.USER,
1360
+ text: `Thread ${id} not found after update`,
1361
+ details: {
1362
+ threadId: id,
1363
+ title
1364
+ }
1365
+ });
1366
+ }
1367
+ return {
1368
+ ...thread,
1369
+ metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata,
1370
+ createdAt: thread.createdAt,
1371
+ updatedAt: thread.updatedAt
1372
+ };
1373
+ } catch (error) {
1374
+ throw new MastraError(
1375
+ {
1376
+ id: createStorageErrorId("MSSQL", "UPDATE_THREAD", "FAILED"),
1377
+ domain: ErrorDomain.STORAGE,
1378
+ category: ErrorCategory.THIRD_PARTY,
1379
+ details: {
1380
+ threadId: id,
1381
+ title
1382
+ }
1383
+ },
1384
+ error
1385
+ );
1386
+ }
1387
+ }
1388
+ async deleteThread({ threadId }) {
1389
+ const messagesTable = getTableName2({ indexName: TABLE_MESSAGES, schemaName: getSchemaName2(this.schema) });
1390
+ const threadsTable = getTableName2({ indexName: TABLE_THREADS, schemaName: getSchemaName2(this.schema) });
1391
+ const deleteMessagesSql = `DELETE FROM ${messagesTable} WHERE [thread_id] = @threadId`;
1392
+ const deleteThreadSql = `DELETE FROM ${threadsTable} WHERE id = @threadId`;
1393
+ const tx = this.pool.transaction();
1394
+ try {
1395
+ await tx.begin();
1396
+ const req = tx.request();
1397
+ req.input("threadId", threadId);
1398
+ await req.query(deleteMessagesSql);
1399
+ await req.query(deleteThreadSql);
1400
+ await tx.commit();
1401
+ } catch (error) {
1402
+ await tx.rollback().catch(() => {
1403
+ });
1404
+ throw new MastraError(
1405
+ {
1406
+ id: createStorageErrorId("MSSQL", "DELETE_THREAD", "FAILED"),
1407
+ domain: ErrorDomain.STORAGE,
1408
+ category: ErrorCategory.THIRD_PARTY,
1409
+ details: {
1410
+ threadId
1411
+ }
1412
+ },
1413
+ error
1414
+ );
1415
+ }
1416
+ }
1417
+ async _getIncludedMessages({ include }) {
1418
+ if (!include || include.length === 0) return null;
1419
+ const unionQueries = [];
1420
+ const paramValues = [];
1421
+ let paramIdx = 1;
1422
+ const paramNames = [];
1423
+ const tableName = getTableName2({ indexName: TABLE_MESSAGES, schemaName: getSchemaName2(this.schema) });
1424
+ for (const inc of include) {
1425
+ const { id, withPreviousMessages = 0, withNextMessages = 0 } = inc;
1426
+ const pId = `@p${paramIdx}`;
1427
+ const pPrev = `@p${paramIdx + 1}`;
1428
+ const pNext = `@p${paramIdx + 2}`;
1429
+ unionQueries.push(
1430
+ `
1431
+ SELECT
1432
+ m.id,
1433
+ m.content,
1434
+ m.role,
1435
+ m.type,
1436
+ m.[createdAt],
1437
+ m.thread_id AS threadId,
1438
+ m.[resourceId],
1439
+ m.seq_id
1440
+ FROM (
1441
+ SELECT *, ROW_NUMBER() OVER (ORDER BY [createdAt] ASC) as row_num
1442
+ FROM ${tableName}
1443
+ WHERE [thread_id] = (SELECT thread_id FROM ${tableName} WHERE id = ${pId})
1444
+ ) AS m
1445
+ WHERE m.id = ${pId}
1446
+ OR EXISTS (
1447
+ SELECT 1
1448
+ FROM (
1449
+ SELECT *, ROW_NUMBER() OVER (ORDER BY [createdAt] ASC) as row_num
1450
+ FROM ${tableName}
1451
+ WHERE [thread_id] = (SELECT thread_id FROM ${tableName} WHERE id = ${pId})
1452
+ ) AS target
1453
+ WHERE target.id = ${pId}
1454
+ AND (
1455
+ -- Get previous messages (messages that come BEFORE the target)
1456
+ (m.row_num < target.row_num AND m.row_num >= target.row_num - ${pPrev})
1457
+ OR
1458
+ -- Get next messages (messages that come AFTER the target)
1459
+ (m.row_num > target.row_num AND m.row_num <= target.row_num + ${pNext})
1460
+ )
1461
+ )
1462
+ `
1463
+ );
1464
+ paramValues.push(id, withPreviousMessages, withNextMessages);
1465
+ paramNames.push(`p${paramIdx}`, `p${paramIdx + 1}`, `p${paramIdx + 2}`);
1466
+ paramIdx += 3;
1467
+ }
1468
+ const finalQuery = `
1469
+ SELECT * FROM (
1470
+ ${unionQueries.join(" UNION ALL ")}
1471
+ ) AS union_result
1472
+ ORDER BY [seq_id] ASC
1473
+ `;
1474
+ const req = this.pool.request();
1475
+ for (let i = 0; i < paramValues.length; ++i) {
1476
+ req.input(paramNames[i], paramValues[i]);
1477
+ }
1478
+ const result = await req.query(finalQuery);
1479
+ const includedRows = result.recordset || [];
1480
+ const seen = /* @__PURE__ */ new Set();
1481
+ const dedupedRows = includedRows.filter((row) => {
1482
+ if (seen.has(row.id)) return false;
1483
+ seen.add(row.id);
1484
+ return true;
1485
+ });
1486
+ return dedupedRows;
1487
+ }
1488
+ async listMessagesById({ messageIds }) {
1489
+ if (messageIds.length === 0) return { messages: [] };
1490
+ const selectStatement = `SELECT seq_id, id, content, role, type, [createdAt], thread_id AS threadId, resourceId`;
1491
+ const orderByStatement = `ORDER BY [seq_id] DESC`;
1492
+ try {
1493
+ let rows = [];
1494
+ let query = `${selectStatement} FROM ${getTableName2({ indexName: TABLE_MESSAGES, schemaName: getSchemaName2(this.schema) })} WHERE [id] IN (${messageIds.map((_, i) => `@id${i}`).join(", ")})`;
1495
+ const request = this.pool.request();
1496
+ messageIds.forEach((id, i) => request.input(`id${i}`, id));
1497
+ query += ` ${orderByStatement}`;
1498
+ const result = await request.query(query);
1499
+ const remainingRows = result.recordset || [];
1500
+ rows.push(...remainingRows);
1501
+ rows.sort((a, b) => {
1502
+ const timeDiff = a.seq_id - b.seq_id;
1503
+ return timeDiff;
1504
+ });
1505
+ const messagesWithParsedContent = rows.map((row) => {
1506
+ if (typeof row.content === "string") {
1507
+ try {
1508
+ return { ...row, content: JSON.parse(row.content) };
1509
+ } catch {
1510
+ return row;
1511
+ }
1512
+ }
1513
+ return row;
1514
+ });
1515
+ const cleanMessages = messagesWithParsedContent.map(({ seq_id, ...rest }) => rest);
1516
+ const list = new MessageList().add(cleanMessages, "memory");
1517
+ return { messages: list.get.all.db() };
1518
+ } catch (error) {
1519
+ const mastraError = new MastraError(
1520
+ {
1521
+ id: createStorageErrorId("MSSQL", "LIST_MESSAGES_BY_ID", "FAILED"),
1522
+ domain: ErrorDomain.STORAGE,
1523
+ category: ErrorCategory.THIRD_PARTY,
1524
+ details: {
1525
+ messageIds: JSON.stringify(messageIds)
1526
+ }
1527
+ },
1528
+ error
1529
+ );
1530
+ this.logger?.error?.(mastraError.toString());
1531
+ this.logger?.trackException?.(mastraError);
1532
+ return { messages: [] };
1533
+ }
1534
+ }
1535
+ async listMessages(args) {
1536
+ const { threadId, resourceId, include, filter, perPage: perPageInput, page = 0, orderBy } = args;
1537
+ const threadIds = Array.isArray(threadId) ? threadId : [threadId];
1538
+ if (threadIds.length === 0 || threadIds.some((id) => !id.trim())) {
1539
+ throw new MastraError(
1540
+ {
1541
+ id: createStorageErrorId("MSSQL", "LIST_MESSAGES", "INVALID_THREAD_ID"),
1542
+ domain: ErrorDomain.STORAGE,
1543
+ category: ErrorCategory.THIRD_PARTY,
1544
+ details: { threadId: Array.isArray(threadId) ? threadId.join(",") : threadId }
1545
+ },
1546
+ new Error("threadId must be a non-empty string or array of non-empty strings")
1547
+ );
1548
+ }
1549
+ if (page < 0) {
1550
+ throw new MastraError({
1551
+ id: createStorageErrorId("MSSQL", "LIST_MESSAGES", "INVALID_PAGE"),
1552
+ domain: ErrorDomain.STORAGE,
1553
+ category: ErrorCategory.USER,
1554
+ text: "Page number must be non-negative",
1555
+ details: {
1556
+ threadId: Array.isArray(threadId) ? threadId.join(",") : threadId,
1557
+ page
1558
+ }
1559
+ });
1560
+ }
1561
+ const perPage = normalizePerPage(perPageInput, 40);
1562
+ const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
1563
+ try {
1564
+ const { field, direction } = this.parseOrderBy(orderBy, "ASC");
1565
+ const orderByStatement = `ORDER BY [${field}] ${direction}, [seq_id] ${direction}`;
1566
+ const tableName = getTableName2({ indexName: TABLE_MESSAGES, schemaName: getSchemaName2(this.schema) });
1567
+ const baseQuery = `SELECT seq_id, id, content, role, type, [createdAt], thread_id AS threadId, resourceId FROM ${tableName}`;
1568
+ const filters = {
1569
+ thread_id: threadIds.length === 1 ? threadIds[0] : { $in: threadIds },
1570
+ ...resourceId ? { resourceId } : {},
1571
+ ...buildDateRangeFilter(filter?.dateRange, "createdAt")
1572
+ };
1573
+ const { sql: actualWhereClause = "", params: whereParams } = prepareWhereClause(
1574
+ filters);
1575
+ const bindWhereParams = (req) => {
1576
+ Object.entries(whereParams).forEach(([paramName, paramValue]) => req.input(paramName, paramValue));
1577
+ };
1578
+ const countRequest = this.pool.request();
1579
+ bindWhereParams(countRequest);
1580
+ const countResult = await countRequest.query(`SELECT COUNT(*) as total FROM ${tableName}${actualWhereClause}`);
1581
+ const total = parseInt(countResult.recordset[0]?.total, 10) || 0;
1582
+ const fetchBaseMessages = async () => {
1583
+ const request = this.pool.request();
1584
+ bindWhereParams(request);
1585
+ if (perPageInput === false) {
1586
+ const result2 = await request.query(`${baseQuery}${actualWhereClause} ${orderByStatement}`);
1587
+ return result2.recordset || [];
1588
+ }
1589
+ request.input("offset", offset);
1590
+ request.input("limit", perPage > 2147483647 ? sql.BigInt : sql.Int, perPage);
1591
+ const result = await request.query(
1592
+ `${baseQuery}${actualWhereClause} ${orderByStatement} OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY`
1593
+ );
1594
+ return result.recordset || [];
1595
+ };
1596
+ const baseRows = perPage === 0 ? [] : await fetchBaseMessages();
1597
+ const messages = [...baseRows];
1598
+ const seqById = /* @__PURE__ */ new Map();
1599
+ messages.forEach((msg) => {
1600
+ if (typeof msg.seq_id === "number") seqById.set(msg.id, msg.seq_id);
1601
+ });
1602
+ if (total === 0 && messages.length === 0 && (!include || include.length === 0)) {
1603
+ return {
1604
+ messages: [],
1605
+ total: 0,
1606
+ page,
1607
+ perPage: perPageForResponse,
1608
+ hasMore: false
1609
+ };
1610
+ }
1611
+ if (include?.length) {
1612
+ const messageIds = new Set(messages.map((m) => m.id));
1613
+ const includeMessages = await this._getIncludedMessages({ include });
1614
+ includeMessages?.forEach((msg) => {
1615
+ if (!messageIds.has(msg.id)) {
1616
+ messages.push(msg);
1617
+ messageIds.add(msg.id);
1618
+ if (typeof msg.seq_id === "number") seqById.set(msg.id, msg.seq_id);
1619
+ }
1620
+ });
1621
+ }
1622
+ const parsed = this._parseAndFormatMessages(messages, "v2");
1623
+ const mult = direction === "ASC" ? 1 : -1;
1624
+ const finalMessages = parsed.sort((a, b) => {
1625
+ const aVal = field === "createdAt" ? new Date(a.createdAt).getTime() : a[field];
1626
+ const bVal = field === "createdAt" ? new Date(b.createdAt).getTime() : b[field];
1627
+ if (aVal == null || bVal == null) {
1628
+ return aVal == null && bVal == null ? a.id.localeCompare(b.id) : aVal == null ? 1 : -1;
1629
+ }
1630
+ const diff = (typeof aVal === "number" && typeof bVal === "number" ? aVal - bVal : String(aVal).localeCompare(String(bVal))) * mult;
1631
+ if (diff !== 0) return diff;
1632
+ const seqA = seqById.get(a.id);
1633
+ const seqB = seqById.get(b.id);
1634
+ return seqA != null && seqB != null ? (seqA - seqB) * mult : a.id.localeCompare(b.id);
1635
+ });
1636
+ const threadIdSet = new Set(threadIds);
1637
+ const returnedThreadMessageCount = finalMessages.filter((m) => m.threadId && threadIdSet.has(m.threadId)).length;
1638
+ const hasMore = perPageInput !== false && returnedThreadMessageCount < total && offset + perPage < total;
1639
+ return {
1640
+ messages: finalMessages,
1641
+ total,
1642
+ page,
1643
+ perPage: perPageForResponse,
1644
+ hasMore
1645
+ };
1646
+ } catch (error) {
1647
+ const mastraError = new MastraError(
1648
+ {
1649
+ id: createStorageErrorId("MSSQL", "LIST_MESSAGES", "FAILED"),
1650
+ domain: ErrorDomain.STORAGE,
1651
+ category: ErrorCategory.THIRD_PARTY,
1652
+ details: {
1653
+ threadId: Array.isArray(threadId) ? threadId.join(",") : threadId,
1654
+ resourceId: resourceId ?? ""
1655
+ }
1656
+ },
1657
+ error
1658
+ );
1659
+ this.logger?.error?.(mastraError.toString());
1660
+ this.logger?.trackException?.(mastraError);
1661
+ return {
1662
+ messages: [],
1663
+ total: 0,
1664
+ page,
1665
+ perPage: perPageForResponse,
1666
+ hasMore: false
1667
+ };
1668
+ }
1669
+ }
1670
+ async saveMessages({ messages }) {
1671
+ if (messages.length === 0) return { messages: [] };
1672
+ const threadId = messages[0]?.threadId;
1673
+ if (!threadId) {
1674
+ throw new MastraError({
1675
+ id: createStorageErrorId("MSSQL", "SAVE_MESSAGES", "INVALID_THREAD_ID"),
1676
+ domain: ErrorDomain.STORAGE,
1677
+ category: ErrorCategory.THIRD_PARTY,
1678
+ text: `Thread ID is required`
1679
+ });
1680
+ }
1681
+ const thread = await this.getThreadById({ threadId });
1682
+ if (!thread) {
1683
+ throw new MastraError({
1684
+ id: createStorageErrorId("MSSQL", "SAVE_MESSAGES", "THREAD_NOT_FOUND"),
1685
+ domain: ErrorDomain.STORAGE,
1686
+ category: ErrorCategory.THIRD_PARTY,
1687
+ text: `Thread ${threadId} not found`,
1688
+ details: { threadId }
1689
+ });
1690
+ }
1691
+ const tableMessages = getTableName2({ indexName: TABLE_MESSAGES, schemaName: getSchemaName2(this.schema) });
1692
+ const tableThreads = getTableName2({ indexName: TABLE_THREADS, schemaName: getSchemaName2(this.schema) });
1693
+ try {
1694
+ const transaction = this.pool.transaction();
1695
+ await transaction.begin();
1696
+ try {
1697
+ for (const message of messages) {
1698
+ if (!message.threadId) {
1699
+ throw new Error(
1700
+ `Expected to find a threadId for message, but couldn't find one. An unexpected error has occurred.`
1701
+ );
1702
+ }
1703
+ if (!message.resourceId) {
1704
+ throw new Error(
1705
+ `Expected to find a resourceId for message, but couldn't find one. An unexpected error has occurred.`
1706
+ );
1707
+ }
1708
+ const request = transaction.request();
1709
+ request.input("id", message.id);
1710
+ request.input("thread_id", message.threadId);
1711
+ request.input(
1712
+ "content",
1713
+ typeof message.content === "string" ? message.content : JSON.stringify(message.content)
1714
+ );
1715
+ request.input("createdAt", sql.DateTime2, message.createdAt);
1716
+ request.input("role", message.role);
1717
+ request.input("type", message.type || "v2");
1718
+ request.input("resourceId", message.resourceId);
1719
+ const mergeSql = `MERGE INTO ${tableMessages} AS target
1720
+ USING (SELECT @id AS id) AS src
1721
+ ON target.id = src.id
1722
+ WHEN MATCHED THEN UPDATE SET
1723
+ thread_id = @thread_id,
1724
+ content = @content,
1725
+ [createdAt] = @createdAt,
1726
+ role = @role,
1727
+ type = @type,
1728
+ resourceId = @resourceId
1729
+ WHEN NOT MATCHED THEN INSERT (id, thread_id, content, [createdAt], role, type, resourceId)
1730
+ VALUES (@id, @thread_id, @content, @createdAt, @role, @type, @resourceId);`;
1731
+ await request.query(mergeSql);
1732
+ }
1733
+ const threadReq = transaction.request();
1734
+ threadReq.input("updatedAt", sql.DateTime2, /* @__PURE__ */ new Date());
1735
+ threadReq.input("id", threadId);
1736
+ await threadReq.query(`UPDATE ${tableThreads} SET [updatedAt] = @updatedAt WHERE id = @id`);
1737
+ await transaction.commit();
1738
+ } catch (error) {
1739
+ await transaction.rollback();
1740
+ throw error;
1741
+ }
1742
+ const messagesWithParsedContent = messages.map((message) => {
1743
+ if (typeof message.content === "string") {
1744
+ try {
1745
+ return { ...message, content: JSON.parse(message.content) };
1746
+ } catch {
1747
+ return message;
1748
+ }
1749
+ }
1750
+ return message;
1751
+ });
1752
+ const list = new MessageList().add(messagesWithParsedContent, "memory");
1753
+ return { messages: list.get.all.db() };
1754
+ } catch (error) {
1755
+ throw new MastraError(
1756
+ {
1757
+ id: createStorageErrorId("MSSQL", "SAVE_MESSAGES", "FAILED"),
1758
+ domain: ErrorDomain.STORAGE,
1759
+ category: ErrorCategory.THIRD_PARTY,
1760
+ details: { threadId }
1761
+ },
1762
+ error
1763
+ );
1764
+ }
1765
+ }
1766
+ async updateMessages({
1767
+ messages
1768
+ }) {
1769
+ if (!messages || messages.length === 0) {
1770
+ return [];
1771
+ }
1772
+ const messageIds = messages.map((m) => m.id);
1435
1773
  const idParams = messageIds.map((_, i) => `@id${i}`).join(", ");
1436
- let selectQuery = `SELECT id, content, role, type, createdAt, thread_id AS threadId, resourceId FROM ${this.getTableName(TABLE_MESSAGES)}`;
1774
+ let selectQuery = `SELECT id, content, role, type, createdAt, thread_id AS threadId, resourceId FROM ${getTableName2({ indexName: TABLE_MESSAGES, schemaName: getSchemaName2(this.schema) })}`;
1437
1775
  if (idParams.length > 0) {
1438
1776
  selectQuery += ` WHERE id IN (${idParams})`;
1439
1777
  } else {
1440
1778
  return [];
1441
1779
  }
1442
- const selectReq = this.pool.request();
1443
- messageIds.forEach((id, i) => selectReq.input(`id${i}`, id));
1444
- const existingMessagesDb = (await selectReq.query(selectQuery)).recordset;
1445
- if (!existingMessagesDb || existingMessagesDb.length === 0) {
1446
- return [];
1780
+ const selectReq = this.pool.request();
1781
+ messageIds.forEach((id, i) => selectReq.input(`id${i}`, id));
1782
+ const existingMessagesDb = (await selectReq.query(selectQuery)).recordset;
1783
+ if (!existingMessagesDb || existingMessagesDb.length === 0) {
1784
+ return [];
1785
+ }
1786
+ const existingMessages = existingMessagesDb.map((msg) => {
1787
+ if (typeof msg.content === "string") {
1788
+ try {
1789
+ msg.content = JSON.parse(msg.content);
1790
+ } catch {
1791
+ }
1792
+ }
1793
+ return msg;
1794
+ });
1795
+ const threadIdsToUpdate = /* @__PURE__ */ new Set();
1796
+ const transaction = this.pool.transaction();
1797
+ try {
1798
+ await transaction.begin();
1799
+ for (const existingMessage of existingMessages) {
1800
+ const updatePayload = messages.find((m) => m.id === existingMessage.id);
1801
+ if (!updatePayload) continue;
1802
+ const { id, ...fieldsToUpdate } = updatePayload;
1803
+ if (Object.keys(fieldsToUpdate).length === 0) continue;
1804
+ threadIdsToUpdate.add(existingMessage.threadId);
1805
+ if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) {
1806
+ threadIdsToUpdate.add(updatePayload.threadId);
1807
+ }
1808
+ const setClauses = [];
1809
+ const req = transaction.request();
1810
+ req.input("id", id);
1811
+ const columnMapping = { threadId: "thread_id" };
1812
+ const updatableFields = { ...fieldsToUpdate };
1813
+ if (updatableFields.content) {
1814
+ const newContent = {
1815
+ ...existingMessage.content,
1816
+ ...updatableFields.content,
1817
+ ...existingMessage.content?.metadata && updatableFields.content.metadata ? { metadata: { ...existingMessage.content.metadata, ...updatableFields.content.metadata } } : {}
1818
+ };
1819
+ setClauses.push(`content = @content`);
1820
+ req.input("content", JSON.stringify(newContent));
1821
+ delete updatableFields.content;
1822
+ }
1823
+ for (const key in updatableFields) {
1824
+ if (Object.prototype.hasOwnProperty.call(updatableFields, key)) {
1825
+ const dbColumn = columnMapping[key] || key;
1826
+ setClauses.push(`[${dbColumn}] = @${dbColumn}`);
1827
+ req.input(dbColumn, updatableFields[key]);
1828
+ }
1829
+ }
1830
+ if (setClauses.length > 0) {
1831
+ const updateSql = `UPDATE ${getTableName2({ indexName: TABLE_MESSAGES, schemaName: getSchemaName2(this.schema) })} SET ${setClauses.join(", ")} WHERE id = @id`;
1832
+ await req.query(updateSql);
1833
+ }
1834
+ }
1835
+ if (threadIdsToUpdate.size > 0) {
1836
+ const threadIdParams = Array.from(threadIdsToUpdate).map((_, i) => `@tid${i}`).join(", ");
1837
+ const threadReq = transaction.request();
1838
+ Array.from(threadIdsToUpdate).forEach((tid, i) => threadReq.input(`tid${i}`, tid));
1839
+ threadReq.input("updatedAt", (/* @__PURE__ */ new Date()).toISOString());
1840
+ const threadSql = `UPDATE ${getTableName2({ indexName: TABLE_THREADS, schemaName: getSchemaName2(this.schema) })} SET updatedAt = @updatedAt WHERE id IN (${threadIdParams})`;
1841
+ await threadReq.query(threadSql);
1842
+ }
1843
+ await transaction.commit();
1844
+ } catch (error) {
1845
+ await transaction.rollback();
1846
+ throw new MastraError(
1847
+ {
1848
+ id: createStorageErrorId("MSSQL", "UPDATE_MESSAGES", "FAILED"),
1849
+ domain: ErrorDomain.STORAGE,
1850
+ category: ErrorCategory.THIRD_PARTY
1851
+ },
1852
+ error
1853
+ );
1854
+ }
1855
+ const refetchReq = this.pool.request();
1856
+ messageIds.forEach((id, i) => refetchReq.input(`id${i}`, id));
1857
+ const updatedMessages = (await refetchReq.query(selectQuery)).recordset;
1858
+ return (updatedMessages || []).map((message) => {
1859
+ if (typeof message.content === "string") {
1860
+ try {
1861
+ message.content = JSON.parse(message.content);
1862
+ } catch {
1863
+ }
1864
+ }
1865
+ return message;
1866
+ });
1867
+ }
1868
+ async deleteMessages(messageIds) {
1869
+ if (!messageIds || messageIds.length === 0) {
1870
+ return;
1871
+ }
1872
+ try {
1873
+ const messageTableName = getTableName2({ indexName: TABLE_MESSAGES, schemaName: getSchemaName2(this.schema) });
1874
+ const threadTableName = getTableName2({ indexName: TABLE_THREADS, schemaName: getSchemaName2(this.schema) });
1875
+ const placeholders = messageIds.map((_, idx) => `@p${idx + 1}`).join(",");
1876
+ const request = this.pool.request();
1877
+ messageIds.forEach((id, idx) => {
1878
+ request.input(`p${idx + 1}`, id);
1879
+ });
1880
+ const messages = await request.query(
1881
+ `SELECT DISTINCT [thread_id] FROM ${messageTableName} WHERE [id] IN (${placeholders})`
1882
+ );
1883
+ const threadIds = messages.recordset?.map((msg) => msg.thread_id).filter(Boolean) || [];
1884
+ const transaction = this.pool.transaction();
1885
+ await transaction.begin();
1886
+ try {
1887
+ const deleteRequest = transaction.request();
1888
+ messageIds.forEach((id, idx) => {
1889
+ deleteRequest.input(`p${idx + 1}`, id);
1890
+ });
1891
+ await deleteRequest.query(`DELETE FROM ${messageTableName} WHERE [id] IN (${placeholders})`);
1892
+ if (threadIds.length > 0) {
1893
+ for (const threadId of threadIds) {
1894
+ const updateRequest = transaction.request();
1895
+ updateRequest.input("p1", threadId);
1896
+ await updateRequest.query(`UPDATE ${threadTableName} SET [updatedAt] = GETDATE() WHERE [id] = @p1`);
1897
+ }
1898
+ }
1899
+ await transaction.commit();
1900
+ } catch (error) {
1901
+ try {
1902
+ await transaction.rollback();
1903
+ } catch {
1904
+ }
1905
+ throw error;
1906
+ }
1907
+ } catch (error) {
1908
+ throw new MastraError(
1909
+ {
1910
+ id: createStorageErrorId("MSSQL", "DELETE_MESSAGES", "FAILED"),
1911
+ domain: ErrorDomain.STORAGE,
1912
+ category: ErrorCategory.THIRD_PARTY,
1913
+ details: { messageIds: messageIds.join(", ") }
1914
+ },
1915
+ error
1916
+ );
1917
+ }
1918
+ }
1919
+ async getResourceById({ resourceId }) {
1920
+ const tableName = getTableName2({ indexName: TABLE_RESOURCES, schemaName: getSchemaName2(this.schema) });
1921
+ try {
1922
+ const req = this.pool.request();
1923
+ req.input("resourceId", resourceId);
1924
+ const result = (await req.query(`SELECT * FROM ${tableName} WHERE id = @resourceId`)).recordset[0];
1925
+ if (!result) {
1926
+ return null;
1927
+ }
1928
+ return {
1929
+ id: result.id,
1930
+ createdAt: result.createdAt,
1931
+ updatedAt: result.updatedAt,
1932
+ workingMemory: result.workingMemory,
1933
+ metadata: typeof result.metadata === "string" ? JSON.parse(result.metadata) : result.metadata
1934
+ };
1935
+ } catch (error) {
1936
+ const mastraError = new MastraError(
1937
+ {
1938
+ id: createStorageErrorId("MSSQL", "GET_RESOURCE_BY_ID", "FAILED"),
1939
+ domain: ErrorDomain.STORAGE,
1940
+ category: ErrorCategory.THIRD_PARTY,
1941
+ details: { resourceId }
1942
+ },
1943
+ error
1944
+ );
1945
+ this.logger?.error?.(mastraError.toString());
1946
+ this.logger?.trackException?.(mastraError);
1947
+ throw mastraError;
1948
+ }
1949
+ }
1950
+ async saveResource({ resource }) {
1951
+ await this.db.insert({
1952
+ tableName: TABLE_RESOURCES,
1953
+ record: {
1954
+ ...resource,
1955
+ metadata: resource.metadata
1956
+ }
1957
+ });
1958
+ return resource;
1959
+ }
1960
+ async updateResource({
1961
+ resourceId,
1962
+ workingMemory,
1963
+ metadata
1964
+ }) {
1965
+ try {
1966
+ const existingResource = await this.getResourceById({ resourceId });
1967
+ if (!existingResource) {
1968
+ const newResource = {
1969
+ id: resourceId,
1970
+ workingMemory,
1971
+ metadata: metadata || {},
1972
+ createdAt: /* @__PURE__ */ new Date(),
1973
+ updatedAt: /* @__PURE__ */ new Date()
1974
+ };
1975
+ return this.saveResource({ resource: newResource });
1976
+ }
1977
+ const updatedResource = {
1978
+ ...existingResource,
1979
+ workingMemory: workingMemory !== void 0 ? workingMemory : existingResource.workingMemory,
1980
+ metadata: {
1981
+ ...existingResource.metadata,
1982
+ ...metadata
1983
+ },
1984
+ updatedAt: /* @__PURE__ */ new Date()
1985
+ };
1986
+ const tableName = getTableName2({ indexName: TABLE_RESOURCES, schemaName: getSchemaName2(this.schema) });
1987
+ const updates = [];
1988
+ const req = this.pool.request();
1989
+ if (workingMemory !== void 0) {
1990
+ updates.push("workingMemory = @workingMemory");
1991
+ req.input("workingMemory", workingMemory);
1992
+ }
1993
+ if (metadata) {
1994
+ updates.push("metadata = @metadata");
1995
+ req.input("metadata", JSON.stringify(updatedResource.metadata));
1996
+ }
1997
+ updates.push("updatedAt = @updatedAt");
1998
+ req.input("updatedAt", updatedResource.updatedAt.toISOString());
1999
+ req.input("id", resourceId);
2000
+ await req.query(`UPDATE ${tableName} SET ${updates.join(", ")} WHERE id = @id`);
2001
+ return updatedResource;
2002
+ } catch (error) {
2003
+ const mastraError = new MastraError(
2004
+ {
2005
+ id: createStorageErrorId("MSSQL", "UPDATE_RESOURCE", "FAILED"),
2006
+ domain: ErrorDomain.STORAGE,
2007
+ category: ErrorCategory.THIRD_PARTY,
2008
+ details: { resourceId }
2009
+ },
2010
+ error
2011
+ );
2012
+ this.logger?.error?.(mastraError.toString());
2013
+ this.logger?.trackException?.(mastraError);
2014
+ throw mastraError;
2015
+ }
2016
+ }
2017
+ };
2018
+ var ObservabilityMSSQL = class extends ObservabilityStorage {
2019
+ pool;
2020
+ db;
2021
+ schema;
2022
+ needsConnect;
2023
+ constructor(config) {
2024
+ super();
2025
+ const { pool, db, schema, needsConnect } = resolveMssqlConfig(config);
2026
+ this.pool = pool;
2027
+ this.db = db;
2028
+ this.schema = schema;
2029
+ this.needsConnect = needsConnect;
2030
+ }
2031
+ async init() {
2032
+ if (this.needsConnect) {
2033
+ await this.pool.connect();
2034
+ this.needsConnect = false;
2035
+ }
2036
+ await this.db.createTable({ tableName: TABLE_SPANS, schema: SPAN_SCHEMA });
2037
+ }
2038
+ async dangerouslyClearAll() {
2039
+ await this.db.clearTable({ tableName: TABLE_SPANS });
2040
+ }
2041
+ get tracingStrategy() {
2042
+ return {
2043
+ preferred: "batch-with-updates",
2044
+ supported: ["batch-with-updates", "insert-only"]
2045
+ };
2046
+ }
2047
+ async createSpan(span) {
2048
+ try {
2049
+ const startedAt = span.startedAt instanceof Date ? span.startedAt.toISOString() : span.startedAt;
2050
+ const endedAt = span.endedAt instanceof Date ? span.endedAt.toISOString() : span.endedAt;
2051
+ const record = {
2052
+ ...span,
2053
+ startedAt,
2054
+ endedAt
2055
+ // Note: createdAt/updatedAt will be set by default values
2056
+ };
2057
+ return this.db.insert({ tableName: TABLE_SPANS, record });
2058
+ } catch (error) {
2059
+ throw new MastraError(
2060
+ {
2061
+ id: createStorageErrorId("MSSQL", "CREATE_SPAN", "FAILED"),
2062
+ domain: ErrorDomain.STORAGE,
2063
+ category: ErrorCategory.USER,
2064
+ details: {
2065
+ spanId: span.spanId,
2066
+ traceId: span.traceId,
2067
+ spanType: span.spanType,
2068
+ spanName: span.name
2069
+ }
2070
+ },
2071
+ error
2072
+ );
2073
+ }
2074
+ }
2075
+ async getTrace(traceId) {
2076
+ try {
2077
+ const tableName = getTableName2({
2078
+ indexName: TABLE_SPANS,
2079
+ schemaName: getSchemaName2(this.schema)
2080
+ });
2081
+ const request = this.pool.request();
2082
+ request.input("traceId", traceId);
2083
+ const result = await request.query(
2084
+ `SELECT
2085
+ [traceId], [spanId], [parentSpanId], [name], [scope], [spanType],
2086
+ [attributes], [metadata], [links], [input], [output], [error], [isEvent],
2087
+ [startedAt], [endedAt], [createdAt], [updatedAt]
2088
+ FROM ${tableName}
2089
+ WHERE [traceId] = @traceId
2090
+ ORDER BY [startedAt] DESC`
2091
+ );
2092
+ if (!result.recordset || result.recordset.length === 0) {
2093
+ return null;
2094
+ }
2095
+ return {
2096
+ traceId,
2097
+ spans: result.recordset.map(
2098
+ (span) => transformFromSqlRow({
2099
+ tableName: TABLE_SPANS,
2100
+ sqlRow: span
2101
+ })
2102
+ )
2103
+ };
2104
+ } catch (error) {
2105
+ throw new MastraError(
2106
+ {
2107
+ id: createStorageErrorId("MSSQL", "GET_TRACE", "FAILED"),
2108
+ domain: ErrorDomain.STORAGE,
2109
+ category: ErrorCategory.USER,
2110
+ details: {
2111
+ traceId
2112
+ }
2113
+ },
2114
+ error
2115
+ );
2116
+ }
2117
+ }
2118
+ async updateSpan({
2119
+ spanId,
2120
+ traceId,
2121
+ updates
2122
+ }) {
2123
+ try {
2124
+ const data = { ...updates };
2125
+ if (data.endedAt instanceof Date) {
2126
+ data.endedAt = data.endedAt.toISOString();
2127
+ }
2128
+ if (data.startedAt instanceof Date) {
2129
+ data.startedAt = data.startedAt.toISOString();
2130
+ }
2131
+ await this.db.update({
2132
+ tableName: TABLE_SPANS,
2133
+ keys: { spanId, traceId },
2134
+ data
2135
+ });
2136
+ } catch (error) {
2137
+ throw new MastraError(
2138
+ {
2139
+ id: createStorageErrorId("MSSQL", "UPDATE_SPAN", "FAILED"),
2140
+ domain: ErrorDomain.STORAGE,
2141
+ category: ErrorCategory.USER,
2142
+ details: {
2143
+ spanId,
2144
+ traceId
2145
+ }
2146
+ },
2147
+ error
2148
+ );
2149
+ }
2150
+ }
2151
+ async getTracesPaginated({
2152
+ filters,
2153
+ pagination
2154
+ }) {
2155
+ const page = pagination?.page ?? 0;
2156
+ const perPage = pagination?.perPage ?? 10;
2157
+ const { entityId, entityType, ...actualFilters } = filters || {};
2158
+ const filtersWithDateRange = {
2159
+ ...actualFilters,
2160
+ ...buildDateRangeFilter(pagination?.dateRange, "startedAt"),
2161
+ parentSpanId: null
2162
+ // Only get root spans for traces
2163
+ };
2164
+ const whereClause = prepareWhereClause(filtersWithDateRange);
2165
+ let actualWhereClause = whereClause.sql;
2166
+ const params = { ...whereClause.params };
2167
+ let currentParamIndex = Object.keys(params).length + 1;
2168
+ if (entityId && entityType) {
2169
+ let name = "";
2170
+ if (entityType === "workflow") {
2171
+ name = `workflow run: '${entityId}'`;
2172
+ } else if (entityType === "agent") {
2173
+ name = `agent run: '${entityId}'`;
2174
+ } else {
2175
+ const error = new MastraError({
2176
+ id: createStorageErrorId("MSSQL", "GET_TRACES_PAGINATED", "INVALID_ENTITY_TYPE"),
2177
+ domain: ErrorDomain.STORAGE,
2178
+ category: ErrorCategory.USER,
2179
+ details: {
2180
+ entityType
2181
+ },
2182
+ text: `Cannot filter by entity type: ${entityType}`
2183
+ });
2184
+ throw error;
2185
+ }
2186
+ const entityParam = `p${currentParamIndex++}`;
2187
+ if (actualWhereClause) {
2188
+ actualWhereClause += ` AND [name] = @${entityParam}`;
2189
+ } else {
2190
+ actualWhereClause = ` WHERE [name] = @${entityParam}`;
2191
+ }
2192
+ params[entityParam] = name;
2193
+ }
2194
+ const tableName = getTableName2({
2195
+ indexName: TABLE_SPANS,
2196
+ schemaName: getSchemaName2(this.schema)
2197
+ });
2198
+ try {
2199
+ const countRequest = this.pool.request();
2200
+ Object.entries(params).forEach(([key, value]) => {
2201
+ countRequest.input(key, value);
2202
+ });
2203
+ const countResult = await countRequest.query(
2204
+ `SELECT COUNT(*) as count FROM ${tableName}${actualWhereClause}`
2205
+ );
2206
+ const total = countResult.recordset[0]?.count ?? 0;
2207
+ if (total === 0) {
2208
+ return {
2209
+ pagination: {
2210
+ total: 0,
2211
+ page,
2212
+ perPage,
2213
+ hasMore: false
2214
+ },
2215
+ spans: []
2216
+ };
2217
+ }
2218
+ const dataRequest = this.pool.request();
2219
+ Object.entries(params).forEach(([key, value]) => {
2220
+ dataRequest.input(key, value);
2221
+ });
2222
+ dataRequest.input("offset", page * perPage);
2223
+ dataRequest.input("limit", perPage);
2224
+ const dataResult = await dataRequest.query(
2225
+ `SELECT * FROM ${tableName}${actualWhereClause} ORDER BY [startedAt] DESC OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY`
2226
+ );
2227
+ const spans = dataResult.recordset.map(
2228
+ (row) => transformFromSqlRow({
2229
+ tableName: TABLE_SPANS,
2230
+ sqlRow: row
2231
+ })
2232
+ );
2233
+ return {
2234
+ pagination: {
2235
+ total,
2236
+ page,
2237
+ perPage,
2238
+ hasMore: (page + 1) * perPage < total
2239
+ },
2240
+ spans
2241
+ };
2242
+ } catch (error) {
2243
+ throw new MastraError(
2244
+ {
2245
+ id: createStorageErrorId("MSSQL", "GET_TRACES_PAGINATED", "FAILED"),
2246
+ domain: ErrorDomain.STORAGE,
2247
+ category: ErrorCategory.USER
2248
+ },
2249
+ error
2250
+ );
2251
+ }
2252
+ }
2253
+ async batchCreateSpans(args) {
2254
+ if (!args.records || args.records.length === 0) {
2255
+ return;
1447
2256
  }
1448
- const existingMessages = existingMessagesDb.map((msg) => {
1449
- if (typeof msg.content === "string") {
1450
- try {
1451
- msg.content = JSON.parse(msg.content);
1452
- } catch {
2257
+ try {
2258
+ await this.db.batchInsert({
2259
+ tableName: TABLE_SPANS,
2260
+ records: args.records.map((span) => ({
2261
+ ...span,
2262
+ startedAt: span.startedAt instanceof Date ? span.startedAt.toISOString() : span.startedAt,
2263
+ endedAt: span.endedAt instanceof Date ? span.endedAt.toISOString() : span.endedAt
2264
+ }))
2265
+ });
2266
+ } catch (error) {
2267
+ throw new MastraError(
2268
+ {
2269
+ id: createStorageErrorId("MSSQL", "BATCH_CREATE_SPANS", "FAILED"),
2270
+ domain: ErrorDomain.STORAGE,
2271
+ category: ErrorCategory.USER,
2272
+ details: {
2273
+ count: args.records.length
2274
+ }
2275
+ },
2276
+ error
2277
+ );
2278
+ }
2279
+ }
2280
+ async batchUpdateSpans(args) {
2281
+ if (!args.records || args.records.length === 0) {
2282
+ return;
2283
+ }
2284
+ try {
2285
+ const updates = args.records.map(({ traceId, spanId, updates: data }) => {
2286
+ const processedData = { ...data };
2287
+ if (processedData.endedAt instanceof Date) {
2288
+ processedData.endedAt = processedData.endedAt.toISOString();
2289
+ }
2290
+ if (processedData.startedAt instanceof Date) {
2291
+ processedData.startedAt = processedData.startedAt.toISOString();
2292
+ }
2293
+ return {
2294
+ keys: { spanId, traceId },
2295
+ data: processedData
2296
+ };
2297
+ });
2298
+ await this.db.batchUpdate({
2299
+ tableName: TABLE_SPANS,
2300
+ updates
2301
+ });
2302
+ } catch (error) {
2303
+ throw new MastraError(
2304
+ {
2305
+ id: createStorageErrorId("MSSQL", "BATCH_UPDATE_SPANS", "FAILED"),
2306
+ domain: ErrorDomain.STORAGE,
2307
+ category: ErrorCategory.USER,
2308
+ details: {
2309
+ count: args.records.length
2310
+ }
2311
+ },
2312
+ error
2313
+ );
2314
+ }
2315
+ }
2316
+ async batchDeleteTraces(args) {
2317
+ if (!args.traceIds || args.traceIds.length === 0) {
2318
+ return;
2319
+ }
2320
+ try {
2321
+ const keys = args.traceIds.map((traceId) => ({ traceId }));
2322
+ await this.db.batchDelete({
2323
+ tableName: TABLE_SPANS,
2324
+ keys
2325
+ });
2326
+ } catch (error) {
2327
+ throw new MastraError(
2328
+ {
2329
+ id: createStorageErrorId("MSSQL", "BATCH_DELETE_TRACES", "FAILED"),
2330
+ domain: ErrorDomain.STORAGE,
2331
+ category: ErrorCategory.USER,
2332
+ details: {
2333
+ count: args.traceIds.length
2334
+ }
2335
+ },
2336
+ error
2337
+ );
2338
+ }
2339
+ }
2340
+ };
2341
+ function transformScoreRow(row) {
2342
+ return transformScoreRow$1(row, {
2343
+ convertTimestamps: true
2344
+ });
2345
+ }
2346
+ var ScoresMSSQL = class extends ScoresStorage {
2347
+ pool;
2348
+ db;
2349
+ schema;
2350
+ needsConnect;
2351
+ constructor(config) {
2352
+ super();
2353
+ const { pool, db, schema, needsConnect } = resolveMssqlConfig(config);
2354
+ this.pool = pool;
2355
+ this.db = db;
2356
+ this.schema = schema;
2357
+ this.needsConnect = needsConnect;
2358
+ }
2359
+ async init() {
2360
+ if (this.needsConnect) {
2361
+ await this.pool.connect();
2362
+ this.needsConnect = false;
2363
+ }
2364
+ await this.db.createTable({ tableName: TABLE_SCORERS, schema: TABLE_SCHEMAS[TABLE_SCORERS] });
2365
+ }
2366
+ async dangerouslyClearAll() {
2367
+ await this.db.clearTable({ tableName: TABLE_SCORERS });
2368
+ }
2369
+ async getScoreById({ id }) {
2370
+ try {
2371
+ const request = this.pool.request();
2372
+ request.input("p1", id);
2373
+ const result = await request.query(
2374
+ `SELECT * FROM ${getTableName2({ indexName: TABLE_SCORERS, schemaName: getSchemaName2(this.schema) })} WHERE id = @p1`
2375
+ );
2376
+ if (result.recordset.length === 0) {
2377
+ return null;
2378
+ }
2379
+ return transformScoreRow(result.recordset[0]);
2380
+ } catch (error) {
2381
+ throw new MastraError(
2382
+ {
2383
+ id: createStorageErrorId("MSSQL", "GET_SCORE_BY_ID", "FAILED"),
2384
+ domain: ErrorDomain.STORAGE,
2385
+ category: ErrorCategory.THIRD_PARTY,
2386
+ details: { id }
2387
+ },
2388
+ error
2389
+ );
2390
+ }
2391
+ }
2392
+ async saveScore(score) {
2393
+ let validatedScore;
2394
+ try {
2395
+ validatedScore = saveScorePayloadSchema.parse(score);
2396
+ } catch (error) {
2397
+ throw new MastraError(
2398
+ {
2399
+ id: createStorageErrorId("MSSQL", "SAVE_SCORE", "VALIDATION_FAILED"),
2400
+ domain: ErrorDomain.STORAGE,
2401
+ category: ErrorCategory.USER,
2402
+ details: {
2403
+ scorer: score.scorer?.id ?? "unknown",
2404
+ entityId: score.entityId ?? "unknown",
2405
+ entityType: score.entityType ?? "unknown",
2406
+ traceId: score.traceId ?? "",
2407
+ spanId: score.spanId ?? ""
2408
+ }
2409
+ },
2410
+ error
2411
+ );
2412
+ }
2413
+ try {
2414
+ const scoreId = randomUUID();
2415
+ const now = /* @__PURE__ */ new Date();
2416
+ const {
2417
+ scorer,
2418
+ preprocessStepResult,
2419
+ analyzeStepResult,
2420
+ metadata,
2421
+ input,
2422
+ output,
2423
+ additionalContext,
2424
+ requestContext,
2425
+ entity,
2426
+ ...rest
2427
+ } = validatedScore;
2428
+ await this.db.insert({
2429
+ tableName: TABLE_SCORERS,
2430
+ record: {
2431
+ id: scoreId,
2432
+ ...rest,
2433
+ input: input || "",
2434
+ output: output || "",
2435
+ preprocessStepResult: preprocessStepResult || null,
2436
+ analyzeStepResult: analyzeStepResult || null,
2437
+ metadata: metadata || null,
2438
+ additionalContext: additionalContext || null,
2439
+ requestContext: requestContext || null,
2440
+ entity: entity || null,
2441
+ scorer: scorer || null,
2442
+ createdAt: now.toISOString(),
2443
+ updatedAt: now.toISOString()
1453
2444
  }
2445
+ });
2446
+ return { score: { ...validatedScore, id: scoreId, createdAt: now, updatedAt: now } };
2447
+ } catch (error) {
2448
+ throw new MastraError(
2449
+ {
2450
+ id: createStorageErrorId("MSSQL", "SAVE_SCORE", "FAILED"),
2451
+ domain: ErrorDomain.STORAGE,
2452
+ category: ErrorCategory.THIRD_PARTY
2453
+ },
2454
+ error
2455
+ );
2456
+ }
2457
+ }
2458
+ async listScoresByScorerId({
2459
+ scorerId,
2460
+ pagination,
2461
+ entityId,
2462
+ entityType,
2463
+ source
2464
+ }) {
2465
+ try {
2466
+ const conditions = ["[scorerId] = @p1"];
2467
+ const params = { p1: scorerId };
2468
+ let paramIndex = 2;
2469
+ if (entityId) {
2470
+ conditions.push(`[entityId] = @p${paramIndex}`);
2471
+ params[`p${paramIndex}`] = entityId;
2472
+ paramIndex++;
1454
2473
  }
1455
- return msg;
1456
- });
1457
- const threadIdsToUpdate = /* @__PURE__ */ new Set();
2474
+ if (entityType) {
2475
+ conditions.push(`[entityType] = @p${paramIndex}`);
2476
+ params[`p${paramIndex}`] = entityType;
2477
+ paramIndex++;
2478
+ }
2479
+ if (source) {
2480
+ conditions.push(`[source] = @p${paramIndex}`);
2481
+ params[`p${paramIndex}`] = source;
2482
+ paramIndex++;
2483
+ }
2484
+ const whereClause = conditions.join(" AND ");
2485
+ const tableName = getTableName2({ indexName: TABLE_SCORERS, schemaName: getSchemaName2(this.schema) });
2486
+ const countRequest = this.pool.request();
2487
+ Object.entries(params).forEach(([key, value]) => {
2488
+ countRequest.input(key, value);
2489
+ });
2490
+ const totalResult = await countRequest.query(`SELECT COUNT(*) as count FROM ${tableName} WHERE ${whereClause}`);
2491
+ const total = totalResult.recordset[0]?.count || 0;
2492
+ const { page, perPage: perPageInput } = pagination;
2493
+ if (total === 0) {
2494
+ return {
2495
+ pagination: {
2496
+ total: 0,
2497
+ page,
2498
+ perPage: perPageInput,
2499
+ hasMore: false
2500
+ },
2501
+ scores: []
2502
+ };
2503
+ }
2504
+ const perPage = normalizePerPage(perPageInput, 100);
2505
+ const { offset: start, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
2506
+ const limitValue = perPageInput === false ? total : perPage;
2507
+ const end = perPageInput === false ? total : start + perPage;
2508
+ const dataRequest = this.pool.request();
2509
+ Object.entries(params).forEach(([key, value]) => {
2510
+ dataRequest.input(key, value);
2511
+ });
2512
+ dataRequest.input("perPage", limitValue);
2513
+ dataRequest.input("offset", start);
2514
+ const dataQuery = `SELECT * FROM ${tableName} WHERE ${whereClause} ORDER BY [createdAt] DESC OFFSET @offset ROWS FETCH NEXT @perPage ROWS ONLY`;
2515
+ const result = await dataRequest.query(dataQuery);
2516
+ return {
2517
+ pagination: {
2518
+ total: Number(total),
2519
+ page,
2520
+ perPage: perPageForResponse,
2521
+ hasMore: end < total
2522
+ },
2523
+ scores: result.recordset.map((row) => transformScoreRow(row))
2524
+ };
2525
+ } catch (error) {
2526
+ throw new MastraError(
2527
+ {
2528
+ id: createStorageErrorId("MSSQL", "LIST_SCORES_BY_SCORER_ID", "FAILED"),
2529
+ domain: ErrorDomain.STORAGE,
2530
+ category: ErrorCategory.THIRD_PARTY,
2531
+ details: { scorerId }
2532
+ },
2533
+ error
2534
+ );
2535
+ }
2536
+ }
2537
+ async listScoresByRunId({
2538
+ runId,
2539
+ pagination
2540
+ }) {
2541
+ try {
2542
+ const request = this.pool.request();
2543
+ request.input("p1", runId);
2544
+ const totalResult = await request.query(
2545
+ `SELECT COUNT(*) as count FROM ${getTableName2({ indexName: TABLE_SCORERS, schemaName: getSchemaName2(this.schema) })} WHERE [runId] = @p1`
2546
+ );
2547
+ const total = totalResult.recordset[0]?.count || 0;
2548
+ const { page, perPage: perPageInput } = pagination;
2549
+ if (total === 0) {
2550
+ return {
2551
+ pagination: {
2552
+ total: 0,
2553
+ page,
2554
+ perPage: perPageInput,
2555
+ hasMore: false
2556
+ },
2557
+ scores: []
2558
+ };
2559
+ }
2560
+ const perPage = normalizePerPage(perPageInput, 100);
2561
+ const { offset: start, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
2562
+ const limitValue = perPageInput === false ? total : perPage;
2563
+ const end = perPageInput === false ? total : start + perPage;
2564
+ const dataRequest = this.pool.request();
2565
+ dataRequest.input("p1", runId);
2566
+ dataRequest.input("p2", limitValue);
2567
+ dataRequest.input("p3", start);
2568
+ const result = await dataRequest.query(
2569
+ `SELECT * FROM ${getTableName2({ indexName: TABLE_SCORERS, schemaName: getSchemaName2(this.schema) })} WHERE [runId] = @p1 ORDER BY [createdAt] DESC OFFSET @p3 ROWS FETCH NEXT @p2 ROWS ONLY`
2570
+ );
2571
+ return {
2572
+ pagination: {
2573
+ total: Number(total),
2574
+ page,
2575
+ perPage: perPageForResponse,
2576
+ hasMore: end < total
2577
+ },
2578
+ scores: result.recordset.map((row) => transformScoreRow(row))
2579
+ };
2580
+ } catch (error) {
2581
+ throw new MastraError(
2582
+ {
2583
+ id: createStorageErrorId("MSSQL", "LIST_SCORES_BY_RUN_ID", "FAILED"),
2584
+ domain: ErrorDomain.STORAGE,
2585
+ category: ErrorCategory.THIRD_PARTY,
2586
+ details: { runId }
2587
+ },
2588
+ error
2589
+ );
2590
+ }
2591
+ }
2592
+ async listScoresByEntityId({
2593
+ entityId,
2594
+ entityType,
2595
+ pagination
2596
+ }) {
2597
+ try {
2598
+ const request = this.pool.request();
2599
+ request.input("p1", entityId);
2600
+ request.input("p2", entityType);
2601
+ const totalResult = await request.query(
2602
+ `SELECT COUNT(*) as count FROM ${getTableName2({ indexName: TABLE_SCORERS, schemaName: getSchemaName2(this.schema) })} WHERE [entityId] = @p1 AND [entityType] = @p2`
2603
+ );
2604
+ const total = totalResult.recordset[0]?.count || 0;
2605
+ const { page, perPage: perPageInput } = pagination;
2606
+ const perPage = normalizePerPage(perPageInput, 100);
2607
+ const { offset: start, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
2608
+ if (total === 0) {
2609
+ return {
2610
+ pagination: {
2611
+ total: 0,
2612
+ page,
2613
+ perPage: perPageForResponse,
2614
+ hasMore: false
2615
+ },
2616
+ scores: []
2617
+ };
2618
+ }
2619
+ const limitValue = perPageInput === false ? total : perPage;
2620
+ const end = perPageInput === false ? total : start + perPage;
2621
+ const dataRequest = this.pool.request();
2622
+ dataRequest.input("p1", entityId);
2623
+ dataRequest.input("p2", entityType);
2624
+ dataRequest.input("p3", limitValue);
2625
+ dataRequest.input("p4", start);
2626
+ const result = await dataRequest.query(
2627
+ `SELECT * FROM ${getTableName2({ indexName: TABLE_SCORERS, schemaName: getSchemaName2(this.schema) })} WHERE [entityId] = @p1 AND [entityType] = @p2 ORDER BY [createdAt] DESC OFFSET @p4 ROWS FETCH NEXT @p3 ROWS ONLY`
2628
+ );
2629
+ return {
2630
+ pagination: {
2631
+ total: Number(total),
2632
+ page,
2633
+ perPage: perPageForResponse,
2634
+ hasMore: end < total
2635
+ },
2636
+ scores: result.recordset.map((row) => transformScoreRow(row))
2637
+ };
2638
+ } catch (error) {
2639
+ throw new MastraError(
2640
+ {
2641
+ id: createStorageErrorId("MSSQL", "LIST_SCORES_BY_ENTITY_ID", "FAILED"),
2642
+ domain: ErrorDomain.STORAGE,
2643
+ category: ErrorCategory.THIRD_PARTY,
2644
+ details: { entityId, entityType }
2645
+ },
2646
+ error
2647
+ );
2648
+ }
2649
+ }
2650
+ async listScoresBySpan({
2651
+ traceId,
2652
+ spanId,
2653
+ pagination
2654
+ }) {
2655
+ try {
2656
+ const request = this.pool.request();
2657
+ request.input("p1", traceId);
2658
+ request.input("p2", spanId);
2659
+ const totalResult = await request.query(
2660
+ `SELECT COUNT(*) as count FROM ${getTableName2({ indexName: TABLE_SCORERS, schemaName: getSchemaName2(this.schema) })} WHERE [traceId] = @p1 AND [spanId] = @p2`
2661
+ );
2662
+ const total = totalResult.recordset[0]?.count || 0;
2663
+ const { page, perPage: perPageInput } = pagination;
2664
+ const perPage = normalizePerPage(perPageInput, 100);
2665
+ const { offset: start, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
2666
+ if (total === 0) {
2667
+ return {
2668
+ pagination: {
2669
+ total: 0,
2670
+ page,
2671
+ perPage: perPageForResponse,
2672
+ hasMore: false
2673
+ },
2674
+ scores: []
2675
+ };
2676
+ }
2677
+ const limitValue = perPageInput === false ? total : perPage;
2678
+ const end = perPageInput === false ? total : start + perPage;
2679
+ const dataRequest = this.pool.request();
2680
+ dataRequest.input("p1", traceId);
2681
+ dataRequest.input("p2", spanId);
2682
+ dataRequest.input("p3", limitValue);
2683
+ dataRequest.input("p4", start);
2684
+ const result = await dataRequest.query(
2685
+ `SELECT * FROM ${getTableName2({ indexName: TABLE_SCORERS, schemaName: getSchemaName2(this.schema) })} WHERE [traceId] = @p1 AND [spanId] = @p2 ORDER BY [createdAt] DESC OFFSET @p4 ROWS FETCH NEXT @p3 ROWS ONLY`
2686
+ );
2687
+ return {
2688
+ pagination: {
2689
+ total: Number(total),
2690
+ page,
2691
+ perPage: perPageForResponse,
2692
+ hasMore: end < total
2693
+ },
2694
+ scores: result.recordset.map((row) => transformScoreRow(row))
2695
+ };
2696
+ } catch (error) {
2697
+ throw new MastraError(
2698
+ {
2699
+ id: createStorageErrorId("MSSQL", "LIST_SCORES_BY_SPAN", "FAILED"),
2700
+ domain: ErrorDomain.STORAGE,
2701
+ category: ErrorCategory.THIRD_PARTY,
2702
+ details: { traceId, spanId }
2703
+ },
2704
+ error
2705
+ );
2706
+ }
2707
+ }
2708
+ };
2709
+ var WorkflowsMSSQL = class extends WorkflowsStorage {
2710
+ pool;
2711
+ db;
2712
+ schema;
2713
+ needsConnect;
2714
+ constructor(config) {
2715
+ super();
2716
+ const { pool, db, schema, needsConnect } = resolveMssqlConfig(config);
2717
+ this.pool = pool;
2718
+ this.db = db;
2719
+ this.schema = schema;
2720
+ this.needsConnect = needsConnect;
2721
+ }
2722
+ async init() {
2723
+ if (this.needsConnect) {
2724
+ await this.pool.connect();
2725
+ this.needsConnect = false;
2726
+ }
2727
+ await this.db.createTable({ tableName: TABLE_WORKFLOW_SNAPSHOT, schema: TABLE_SCHEMAS[TABLE_WORKFLOW_SNAPSHOT] });
2728
+ }
2729
+ async dangerouslyClearAll() {
2730
+ await this.db.clearTable({ tableName: TABLE_WORKFLOW_SNAPSHOT });
2731
+ }
2732
+ parseWorkflowRun(row) {
2733
+ let parsedSnapshot = row.snapshot;
2734
+ if (typeof parsedSnapshot === "string") {
2735
+ try {
2736
+ parsedSnapshot = JSON.parse(row.snapshot);
2737
+ } catch (e) {
2738
+ this.logger?.warn?.(`Failed to parse snapshot for workflow ${row.workflow_name}:`, e);
2739
+ }
2740
+ }
2741
+ return {
2742
+ workflowName: row.workflow_name,
2743
+ runId: row.run_id,
2744
+ snapshot: parsedSnapshot,
2745
+ createdAt: row.createdAt,
2746
+ updatedAt: row.updatedAt,
2747
+ resourceId: row.resourceId
2748
+ };
2749
+ }
2750
+ async updateWorkflowResults({
2751
+ workflowName,
2752
+ runId,
2753
+ stepId,
2754
+ result,
2755
+ requestContext
2756
+ }) {
2757
+ const table = getTableName2({ indexName: TABLE_WORKFLOW_SNAPSHOT, schemaName: getSchemaName2(this.schema) });
1458
2758
  const transaction = this.pool.transaction();
1459
2759
  try {
1460
2760
  await transaction.begin();
1461
- for (const existingMessage of existingMessages) {
1462
- const updatePayload = messages.find((m) => m.id === existingMessage.id);
1463
- if (!updatePayload) continue;
1464
- const { id, ...fieldsToUpdate } = updatePayload;
1465
- if (Object.keys(fieldsToUpdate).length === 0) continue;
1466
- threadIdsToUpdate.add(existingMessage.threadId);
1467
- if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) {
1468
- threadIdsToUpdate.add(updatePayload.threadId);
1469
- }
1470
- const setClauses = [];
1471
- const req = transaction.request();
1472
- req.input("id", id);
1473
- const columnMapping = { threadId: "thread_id" };
1474
- const updatableFields = { ...fieldsToUpdate };
1475
- if (updatableFields.content) {
1476
- const newContent = {
1477
- ...existingMessage.content,
1478
- ...updatableFields.content,
1479
- ...existingMessage.content?.metadata && updatableFields.content.metadata ? { metadata: { ...existingMessage.content.metadata, ...updatableFields.content.metadata } } : {}
1480
- };
1481
- setClauses.push(`content = @content`);
1482
- req.input("content", JSON.stringify(newContent));
1483
- delete updatableFields.content;
1484
- }
1485
- for (const key in updatableFields) {
1486
- if (Object.prototype.hasOwnProperty.call(updatableFields, key)) {
1487
- const dbColumn = columnMapping[key] || key;
1488
- setClauses.push(`[${dbColumn}] = @${dbColumn}`);
1489
- req.input(dbColumn, updatableFields[key]);
1490
- }
1491
- }
1492
- if (setClauses.length > 0) {
1493
- const updateSql = `UPDATE ${this.getTableName(TABLE_MESSAGES)} SET ${setClauses.join(", ")} WHERE id = @id`;
1494
- await req.query(updateSql);
1495
- }
1496
- }
1497
- if (threadIdsToUpdate.size > 0) {
1498
- const threadIdParams = Array.from(threadIdsToUpdate).map((_, i) => `@tid${i}`).join(", ");
1499
- const threadReq = transaction.request();
1500
- Array.from(threadIdsToUpdate).forEach((tid, i) => threadReq.input(`tid${i}`, tid));
1501
- threadReq.input("updatedAt", (/* @__PURE__ */ new Date()).toISOString());
1502
- const threadSql = `UPDATE ${this.getTableName(TABLE_THREADS)} SET updatedAt = @updatedAt WHERE id IN (${threadIdParams})`;
1503
- await threadReq.query(threadSql);
2761
+ const selectRequest = new sql.Request(transaction);
2762
+ selectRequest.input("workflow_name", workflowName);
2763
+ selectRequest.input("run_id", runId);
2764
+ const existingSnapshotResult = await selectRequest.query(
2765
+ `SELECT snapshot FROM ${table} WITH (UPDLOCK, HOLDLOCK) WHERE workflow_name = @workflow_name AND run_id = @run_id`
2766
+ );
2767
+ let snapshot;
2768
+ if (!existingSnapshotResult.recordset || existingSnapshotResult.recordset.length === 0) {
2769
+ snapshot = {
2770
+ context: {},
2771
+ activePaths: [],
2772
+ activeStepsPath: {},
2773
+ timestamp: Date.now(),
2774
+ suspendedPaths: {},
2775
+ resumeLabels: {},
2776
+ serializedStepGraph: [],
2777
+ status: "pending",
2778
+ value: {},
2779
+ waitingPaths: {},
2780
+ runId,
2781
+ requestContext: {}
2782
+ };
2783
+ } else {
2784
+ const existingSnapshot = existingSnapshotResult.recordset[0].snapshot;
2785
+ snapshot = typeof existingSnapshot === "string" ? JSON.parse(existingSnapshot) : existingSnapshot;
1504
2786
  }
2787
+ snapshot.context[stepId] = result;
2788
+ snapshot.requestContext = { ...snapshot.requestContext, ...requestContext };
2789
+ const upsertReq = new sql.Request(transaction);
2790
+ upsertReq.input("workflow_name", workflowName);
2791
+ upsertReq.input("run_id", runId);
2792
+ upsertReq.input("snapshot", JSON.stringify(snapshot));
2793
+ upsertReq.input("createdAt", sql.DateTime2, /* @__PURE__ */ new Date());
2794
+ upsertReq.input("updatedAt", sql.DateTime2, /* @__PURE__ */ new Date());
2795
+ await upsertReq.query(
2796
+ `MERGE ${table} AS target
2797
+ USING (SELECT @workflow_name AS workflow_name, @run_id AS run_id) AS src
2798
+ ON target.workflow_name = src.workflow_name AND target.run_id = src.run_id
2799
+ WHEN MATCHED THEN UPDATE SET snapshot = @snapshot, [updatedAt] = @updatedAt
2800
+ WHEN NOT MATCHED THEN INSERT (workflow_name, run_id, snapshot, [createdAt], [updatedAt])
2801
+ VALUES (@workflow_name, @run_id, @snapshot, @createdAt, @updatedAt);`
2802
+ );
1505
2803
  await transaction.commit();
2804
+ return snapshot.context;
1506
2805
  } catch (error) {
1507
- await transaction.rollback();
2806
+ try {
2807
+ await transaction.rollback();
2808
+ } catch {
2809
+ }
1508
2810
  throw new MastraError(
1509
2811
  {
1510
- id: "MASTRA_STORAGE_MSSQL_UPDATE_MESSAGES_FAILED",
2812
+ id: createStorageErrorId("MSSQL", "UPDATE_WORKFLOW_RESULTS", "FAILED"),
1511
2813
  domain: ErrorDomain.STORAGE,
1512
- category: ErrorCategory.THIRD_PARTY
2814
+ category: ErrorCategory.THIRD_PARTY,
2815
+ details: {
2816
+ workflowName,
2817
+ runId,
2818
+ stepId
2819
+ }
1513
2820
  },
1514
2821
  error
1515
2822
  );
1516
2823
  }
1517
- const refetchReq = this.pool.request();
1518
- messageIds.forEach((id, i) => refetchReq.input(`id${i}`, id));
1519
- const updatedMessages = (await refetchReq.query(selectQuery)).recordset;
1520
- return (updatedMessages || []).map((message) => {
1521
- if (typeof message.content === "string") {
1522
- try {
1523
- message.content = JSON.parse(message.content);
1524
- } catch {
1525
- }
1526
- }
1527
- return message;
1528
- });
1529
2824
  }
1530
- async close() {
1531
- if (this.pool) {
2825
+ async updateWorkflowState({
2826
+ workflowName,
2827
+ runId,
2828
+ opts
2829
+ }) {
2830
+ const table = getTableName2({ indexName: TABLE_WORKFLOW_SNAPSHOT, schemaName: getSchemaName2(this.schema) });
2831
+ const transaction = this.pool.transaction();
2832
+ try {
2833
+ await transaction.begin();
2834
+ const selectRequest = new sql.Request(transaction);
2835
+ selectRequest.input("workflow_name", workflowName);
2836
+ selectRequest.input("run_id", runId);
2837
+ const existingSnapshotResult = await selectRequest.query(
2838
+ `SELECT snapshot FROM ${table} WITH (UPDLOCK, HOLDLOCK) WHERE workflow_name = @workflow_name AND run_id = @run_id`
2839
+ );
2840
+ if (!existingSnapshotResult.recordset || existingSnapshotResult.recordset.length === 0) {
2841
+ await transaction.rollback();
2842
+ return void 0;
2843
+ }
2844
+ const existingSnapshot = existingSnapshotResult.recordset[0].snapshot;
2845
+ const snapshot = typeof existingSnapshot === "string" ? JSON.parse(existingSnapshot) : existingSnapshot;
2846
+ if (!snapshot || !snapshot?.context) {
2847
+ await transaction.rollback();
2848
+ throw new MastraError(
2849
+ {
2850
+ id: createStorageErrorId("MSSQL", "UPDATE_WORKFLOW_STATE", "SNAPSHOT_NOT_FOUND"),
2851
+ domain: ErrorDomain.STORAGE,
2852
+ category: ErrorCategory.SYSTEM,
2853
+ details: {
2854
+ workflowName,
2855
+ runId
2856
+ }
2857
+ },
2858
+ new Error(`Snapshot not found for runId ${runId}`)
2859
+ );
2860
+ }
2861
+ const updatedSnapshot = { ...snapshot, ...opts };
2862
+ const updateRequest = new sql.Request(transaction);
2863
+ updateRequest.input("snapshot", JSON.stringify(updatedSnapshot));
2864
+ updateRequest.input("workflow_name", workflowName);
2865
+ updateRequest.input("run_id", runId);
2866
+ updateRequest.input("updatedAt", sql.DateTime2, /* @__PURE__ */ new Date());
2867
+ await updateRequest.query(
2868
+ `UPDATE ${table} SET snapshot = @snapshot, [updatedAt] = @updatedAt WHERE workflow_name = @workflow_name AND run_id = @run_id`
2869
+ );
2870
+ await transaction.commit();
2871
+ return updatedSnapshot;
2872
+ } catch (error) {
1532
2873
  try {
1533
- if (this.pool.connected) {
1534
- await this.pool.close();
1535
- } else if (this.pool.connecting) {
1536
- await this.pool.connect();
1537
- await this.pool.close();
1538
- }
1539
- } catch (err) {
1540
- if (err.message && err.message.includes("Cannot close a pool while it is connecting")) ; else {
1541
- throw err;
1542
- }
2874
+ await transaction.rollback();
2875
+ } catch {
1543
2876
  }
2877
+ if (error instanceof MastraError) throw error;
2878
+ throw new MastraError(
2879
+ {
2880
+ id: createStorageErrorId("MSSQL", "UPDATE_WORKFLOW_STATE", "FAILED"),
2881
+ domain: ErrorDomain.STORAGE,
2882
+ category: ErrorCategory.THIRD_PARTY,
2883
+ details: {
2884
+ workflowName,
2885
+ runId
2886
+ }
2887
+ },
2888
+ error
2889
+ );
1544
2890
  }
1545
2891
  }
1546
- async getEvals(options = {}) {
1547
- const { agentName, type, page = 0, perPage = 100, dateRange } = options;
1548
- const fromDate = dateRange?.start;
1549
- const toDate = dateRange?.end;
1550
- const where = [];
1551
- const params = {};
1552
- if (agentName) {
1553
- where.push("agent_name = @agentName");
1554
- params["agentName"] = agentName;
2892
+ async persistWorkflowSnapshot({
2893
+ workflowName,
2894
+ runId,
2895
+ resourceId,
2896
+ snapshot
2897
+ }) {
2898
+ const table = getTableName2({ indexName: TABLE_WORKFLOW_SNAPSHOT, schemaName: getSchemaName2(this.schema) });
2899
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2900
+ try {
2901
+ const request = this.pool.request();
2902
+ request.input("workflow_name", workflowName);
2903
+ request.input("run_id", runId);
2904
+ request.input("resourceId", resourceId);
2905
+ request.input("snapshot", JSON.stringify(snapshot));
2906
+ request.input("createdAt", sql.DateTime2, new Date(now));
2907
+ request.input("updatedAt", sql.DateTime2, new Date(now));
2908
+ const mergeSql = `MERGE INTO ${table} AS target
2909
+ USING (SELECT @workflow_name AS workflow_name, @run_id AS run_id) AS src
2910
+ ON target.workflow_name = src.workflow_name AND target.run_id = src.run_id
2911
+ WHEN MATCHED THEN UPDATE SET
2912
+ resourceId = @resourceId,
2913
+ snapshot = @snapshot,
2914
+ [updatedAt] = @updatedAt
2915
+ WHEN NOT MATCHED THEN INSERT (workflow_name, run_id, resourceId, snapshot, [createdAt], [updatedAt])
2916
+ VALUES (@workflow_name, @run_id, @resourceId, @snapshot, @createdAt, @updatedAt);`;
2917
+ await request.query(mergeSql);
2918
+ } catch (error) {
2919
+ throw new MastraError(
2920
+ {
2921
+ id: createStorageErrorId("MSSQL", "PERSIST_WORKFLOW_SNAPSHOT", "FAILED"),
2922
+ domain: ErrorDomain.STORAGE,
2923
+ category: ErrorCategory.THIRD_PARTY,
2924
+ details: {
2925
+ workflowName,
2926
+ runId
2927
+ }
2928
+ },
2929
+ error
2930
+ );
1555
2931
  }
1556
- if (type === "test") {
1557
- where.push("test_info IS NOT NULL AND JSON_VALUE(test_info, '$.testPath') IS NOT NULL");
1558
- } else if (type === "live") {
1559
- where.push("(test_info IS NULL OR JSON_VALUE(test_info, '$.testPath') IS NULL)");
2932
+ }
2933
+ async loadWorkflowSnapshot({
2934
+ workflowName,
2935
+ runId
2936
+ }) {
2937
+ try {
2938
+ const result = await this.db.load({
2939
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
2940
+ keys: {
2941
+ workflow_name: workflowName,
2942
+ run_id: runId
2943
+ }
2944
+ });
2945
+ if (!result) {
2946
+ return null;
2947
+ }
2948
+ return result.snapshot;
2949
+ } catch (error) {
2950
+ throw new MastraError(
2951
+ {
2952
+ id: createStorageErrorId("MSSQL", "LOAD_WORKFLOW_SNAPSHOT", "FAILED"),
2953
+ domain: ErrorDomain.STORAGE,
2954
+ category: ErrorCategory.THIRD_PARTY,
2955
+ details: {
2956
+ workflowName,
2957
+ runId
2958
+ }
2959
+ },
2960
+ error
2961
+ );
1560
2962
  }
1561
- if (fromDate instanceof Date && !isNaN(fromDate.getTime())) {
1562
- where.push(`[created_at] >= @fromDate`);
1563
- params[`fromDate`] = fromDate.toISOString();
2963
+ }
2964
+ async getWorkflowRunById({
2965
+ runId,
2966
+ workflowName
2967
+ }) {
2968
+ try {
2969
+ const conditions = [];
2970
+ const paramMap = {};
2971
+ if (runId) {
2972
+ conditions.push(`[run_id] = @runId`);
2973
+ paramMap["runId"] = runId;
2974
+ }
2975
+ if (workflowName) {
2976
+ conditions.push(`[workflow_name] = @workflowName`);
2977
+ paramMap["workflowName"] = workflowName;
2978
+ }
2979
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2980
+ const tableName = getTableName2({ indexName: TABLE_WORKFLOW_SNAPSHOT, schemaName: getSchemaName2(this.schema) });
2981
+ const query = `SELECT * FROM ${tableName} ${whereClause}`;
2982
+ const request = this.pool.request();
2983
+ Object.entries(paramMap).forEach(([key, value]) => request.input(key, value));
2984
+ const result = await request.query(query);
2985
+ if (!result.recordset || result.recordset.length === 0) {
2986
+ return null;
2987
+ }
2988
+ return this.parseWorkflowRun(result.recordset[0]);
2989
+ } catch (error) {
2990
+ throw new MastraError(
2991
+ {
2992
+ id: createStorageErrorId("MSSQL", "GET_WORKFLOW_RUN_BY_ID", "FAILED"),
2993
+ domain: ErrorDomain.STORAGE,
2994
+ category: ErrorCategory.THIRD_PARTY,
2995
+ details: {
2996
+ runId,
2997
+ workflowName: workflowName || ""
2998
+ }
2999
+ },
3000
+ error
3001
+ );
1564
3002
  }
1565
- if (toDate instanceof Date && !isNaN(toDate.getTime())) {
1566
- where.push(`[created_at] <= @toDate`);
1567
- params[`toDate`] = toDate.toISOString();
3003
+ }
3004
+ async deleteWorkflowRunById({ runId, workflowName }) {
3005
+ const table = getTableName2({ indexName: TABLE_WORKFLOW_SNAPSHOT, schemaName: getSchemaName2(this.schema) });
3006
+ const transaction = this.pool.transaction();
3007
+ try {
3008
+ await transaction.begin();
3009
+ const deleteRequest = new sql.Request(transaction);
3010
+ deleteRequest.input("workflow_name", workflowName);
3011
+ deleteRequest.input("run_id", runId);
3012
+ await deleteRequest.query(`DELETE FROM ${table} WHERE workflow_name = @workflow_name AND run_id = @run_id`);
3013
+ await transaction.commit();
3014
+ } catch (error) {
3015
+ try {
3016
+ await transaction.rollback();
3017
+ } catch {
3018
+ }
3019
+ throw new MastraError(
3020
+ {
3021
+ id: createStorageErrorId("MSSQL", "DELETE_WORKFLOW_RUN_BY_ID", "FAILED"),
3022
+ domain: ErrorDomain.STORAGE,
3023
+ category: ErrorCategory.THIRD_PARTY,
3024
+ details: {
3025
+ runId,
3026
+ workflowName
3027
+ }
3028
+ },
3029
+ error
3030
+ );
1568
3031
  }
1569
- const whereClause = where.length > 0 ? `WHERE ${where.join(" AND ")}` : "";
1570
- const tableName = this.getTableName(TABLE_EVALS);
1571
- const offset = page * perPage;
1572
- const countQuery = `SELECT COUNT(*) as total FROM ${tableName} ${whereClause}`;
1573
- const dataQuery = `SELECT * FROM ${tableName} ${whereClause} ORDER BY seq_id DESC OFFSET @offset ROWS FETCH NEXT @perPage ROWS ONLY`;
3032
+ }
3033
+ async listWorkflowRuns({
3034
+ workflowName,
3035
+ fromDate,
3036
+ toDate,
3037
+ page,
3038
+ perPage,
3039
+ resourceId,
3040
+ status
3041
+ } = {}) {
1574
3042
  try {
1575
- const countReq = this.pool.request();
1576
- Object.entries(params).forEach(([key, value]) => {
1577
- if (value instanceof Date) {
1578
- countReq.input(key, sql.DateTime, value);
3043
+ const conditions = [];
3044
+ const paramMap = {};
3045
+ if (workflowName) {
3046
+ conditions.push(`[workflow_name] = @workflowName`);
3047
+ paramMap["workflowName"] = workflowName;
3048
+ }
3049
+ if (status) {
3050
+ conditions.push(`JSON_VALUE([snapshot], '$.status') = @status`);
3051
+ paramMap["status"] = status;
3052
+ }
3053
+ if (resourceId) {
3054
+ const hasResourceId = await this.db.hasColumn(TABLE_WORKFLOW_SNAPSHOT, "resourceId");
3055
+ if (hasResourceId) {
3056
+ conditions.push(`[resourceId] = @resourceId`);
3057
+ paramMap["resourceId"] = resourceId;
1579
3058
  } else {
1580
- countReq.input(key, value);
3059
+ this.logger?.warn?.(`[${TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
1581
3060
  }
1582
- });
1583
- const countResult = await countReq.query(countQuery);
1584
- const total = countResult.recordset[0]?.total || 0;
1585
- if (total === 0) {
1586
- return {
1587
- evals: [],
1588
- total: 0,
1589
- page,
1590
- perPage,
1591
- hasMore: false
1592
- };
1593
3061
  }
1594
- const req = this.pool.request();
1595
- Object.entries(params).forEach(([key, value]) => {
3062
+ if (fromDate instanceof Date && !isNaN(fromDate.getTime())) {
3063
+ conditions.push(`[createdAt] >= @fromDate`);
3064
+ paramMap[`fromDate`] = fromDate.toISOString();
3065
+ }
3066
+ if (toDate instanceof Date && !isNaN(toDate.getTime())) {
3067
+ conditions.push(`[createdAt] <= @toDate`);
3068
+ paramMap[`toDate`] = toDate.toISOString();
3069
+ }
3070
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3071
+ let total = 0;
3072
+ const tableName = getTableName2({ indexName: TABLE_WORKFLOW_SNAPSHOT, schemaName: getSchemaName2(this.schema) });
3073
+ const request = this.pool.request();
3074
+ Object.entries(paramMap).forEach(([key, value]) => {
1596
3075
  if (value instanceof Date) {
1597
- req.input(key, sql.DateTime, value);
3076
+ request.input(key, sql.DateTime, value);
1598
3077
  } else {
1599
- req.input(key, value);
3078
+ request.input(key, value);
1600
3079
  }
1601
3080
  });
1602
- req.input("offset", offset);
1603
- req.input("perPage", perPage);
1604
- const result = await req.query(dataQuery);
1605
- const rows = result.recordset;
1606
- return {
1607
- evals: rows?.map((row) => this.transformEvalRow(row)) ?? [],
1608
- total,
1609
- page,
1610
- perPage,
1611
- hasMore: offset + (rows?.length ?? 0) < total
1612
- };
3081
+ const usePagination = typeof perPage === "number" && typeof page === "number";
3082
+ if (usePagination) {
3083
+ const countQuery = `SELECT COUNT(*) as count FROM ${tableName} ${whereClause}`;
3084
+ const countResult = await request.query(countQuery);
3085
+ total = Number(countResult.recordset[0]?.count || 0);
3086
+ }
3087
+ let query = `SELECT * FROM ${tableName} ${whereClause} ORDER BY [seq_id] DESC`;
3088
+ if (usePagination) {
3089
+ const normalizedPerPage = normalizePerPage(perPage, Number.MAX_SAFE_INTEGER);
3090
+ const offset = page * normalizedPerPage;
3091
+ query += ` OFFSET @offset ROWS FETCH NEXT @perPage ROWS ONLY`;
3092
+ request.input("perPage", normalizedPerPage);
3093
+ request.input("offset", offset);
3094
+ }
3095
+ const result = await request.query(query);
3096
+ const runs = (result.recordset || []).map((row) => this.parseWorkflowRun(row));
3097
+ return { runs, total: total || runs.length };
1613
3098
  } catch (error) {
1614
- const mastraError = new MastraError(
3099
+ throw new MastraError(
1615
3100
  {
1616
- id: "MASTRA_STORAGE_MSSQL_STORE_GET_EVALS_FAILED",
3101
+ id: createStorageErrorId("MSSQL", "LIST_WORKFLOW_RUNS", "FAILED"),
1617
3102
  domain: ErrorDomain.STORAGE,
1618
3103
  category: ErrorCategory.THIRD_PARTY,
1619
3104
  details: {
1620
- agentName: agentName || "all",
1621
- type: type || "all",
1622
- page,
1623
- perPage
3105
+ workflowName: workflowName || "all"
1624
3106
  }
1625
3107
  },
1626
3108
  error
1627
3109
  );
1628
- this.logger?.error?.(mastraError.toString());
1629
- this.logger?.trackException(mastraError);
1630
- throw mastraError;
1631
3110
  }
1632
3111
  }
1633
- async saveResource({ resource }) {
1634
- const tableName = this.getTableName(TABLE_RESOURCES);
1635
- try {
1636
- const req = this.pool.request();
1637
- req.input("id", resource.id);
1638
- req.input("workingMemory", resource.workingMemory);
1639
- req.input("metadata", JSON.stringify(resource.metadata));
1640
- req.input("createdAt", resource.createdAt.toISOString());
1641
- req.input("updatedAt", resource.updatedAt.toISOString());
1642
- await req.query(
1643
- `INSERT INTO ${tableName} (id, workingMemory, metadata, createdAt, updatedAt) VALUES (@id, @workingMemory, @metadata, @createdAt, @updatedAt)`
1644
- );
1645
- return resource;
1646
- } catch (error) {
1647
- const mastraError = new MastraError(
1648
- {
1649
- id: "MASTRA_STORAGE_MSSQL_SAVE_RESOURCE_FAILED",
1650
- domain: ErrorDomain.STORAGE,
1651
- category: ErrorCategory.THIRD_PARTY,
1652
- details: { resourceId: resource.id }
1653
- },
1654
- error
1655
- );
1656
- this.logger?.error?.(mastraError.toString());
1657
- this.logger?.trackException(mastraError);
1658
- throw mastraError;
3112
+ };
3113
+
3114
+ // src/storage/index.ts
3115
+ var MSSQLStore = class extends MastraStorage {
3116
+ pool;
3117
+ schema;
3118
+ isConnected = null;
3119
+ #db;
3120
+ stores;
3121
+ constructor(config) {
3122
+ if (!config.id || typeof config.id !== "string" || config.id.trim() === "") {
3123
+ throw new Error("MSSQLStore: id must be provided and cannot be empty.");
1659
3124
  }
1660
- }
1661
- async updateResource({
1662
- resourceId,
1663
- workingMemory,
1664
- metadata
1665
- }) {
3125
+ super({ id: config.id, name: "MSSQLStore", disableInit: config.disableInit });
1666
3126
  try {
1667
- const existingResource = await this.getResourceById({ resourceId });
1668
- if (!existingResource) {
1669
- const newResource = {
1670
- id: resourceId,
1671
- workingMemory,
1672
- metadata: metadata || {},
1673
- createdAt: /* @__PURE__ */ new Date(),
1674
- updatedAt: /* @__PURE__ */ new Date()
1675
- };
1676
- return this.saveResource({ resource: newResource });
3127
+ if ("connectionString" in config) {
3128
+ if (!config.connectionString || typeof config.connectionString !== "string" || config.connectionString.trim() === "") {
3129
+ throw new Error("MSSQLStore: connectionString must be provided and cannot be empty.");
3130
+ }
3131
+ } else {
3132
+ const required = ["server", "database", "user", "password"];
3133
+ for (const key of required) {
3134
+ if (!(key in config) || typeof config[key] !== "string" || config[key].trim() === "") {
3135
+ throw new Error(`MSSQLStore: ${key} must be provided and cannot be empty.`);
3136
+ }
3137
+ }
1677
3138
  }
1678
- const updatedResource = {
1679
- ...existingResource,
1680
- workingMemory: workingMemory !== void 0 ? workingMemory : existingResource.workingMemory,
1681
- metadata: {
1682
- ...existingResource.metadata,
1683
- ...metadata
1684
- },
1685
- updatedAt: /* @__PURE__ */ new Date()
3139
+ this.schema = config.schemaName || "dbo";
3140
+ this.pool = "connectionString" in config ? new sql.ConnectionPool(config.connectionString) : new sql.ConnectionPool({
3141
+ server: config.server,
3142
+ database: config.database,
3143
+ user: config.user,
3144
+ password: config.password,
3145
+ port: config.port,
3146
+ options: config.options || { encrypt: true, trustServerCertificate: true }
3147
+ });
3148
+ this.#db = new MssqlDB({ pool: this.pool, schemaName: this.schema });
3149
+ const domainConfig = { pool: this.pool, db: this.#db, schema: this.schema };
3150
+ const scores = new ScoresMSSQL(domainConfig);
3151
+ const workflows = new WorkflowsMSSQL(domainConfig);
3152
+ const memory = new MemoryMSSQL(domainConfig);
3153
+ const observability = new ObservabilityMSSQL(domainConfig);
3154
+ this.stores = {
3155
+ scores,
3156
+ workflows,
3157
+ memory,
3158
+ observability
1686
3159
  };
1687
- const tableName = this.getTableName(TABLE_RESOURCES);
1688
- const updates = [];
1689
- const req = this.pool.request();
1690
- if (workingMemory !== void 0) {
1691
- updates.push("workingMemory = @workingMemory");
1692
- req.input("workingMemory", workingMemory);
1693
- }
1694
- if (metadata) {
1695
- updates.push("metadata = @metadata");
1696
- req.input("metadata", JSON.stringify(updatedResource.metadata));
1697
- }
1698
- updates.push("updatedAt = @updatedAt");
1699
- req.input("updatedAt", updatedResource.updatedAt.toISOString());
1700
- req.input("id", resourceId);
1701
- await req.query(`UPDATE ${tableName} SET ${updates.join(", ")} WHERE id = @id`);
1702
- return updatedResource;
1703
- } catch (error) {
1704
- const mastraError = new MastraError(
3160
+ } catch (e) {
3161
+ throw new MastraError(
1705
3162
  {
1706
- id: "MASTRA_STORAGE_MSSQL_UPDATE_RESOURCE_FAILED",
3163
+ id: createStorageErrorId("MSSQL", "INITIALIZATION", "FAILED"),
1707
3164
  domain: ErrorDomain.STORAGE,
1708
- category: ErrorCategory.THIRD_PARTY,
1709
- details: { resourceId }
3165
+ category: ErrorCategory.USER
1710
3166
  },
1711
- error
3167
+ e
1712
3168
  );
1713
- this.logger?.error?.(mastraError.toString());
1714
- this.logger?.trackException(mastraError);
1715
- throw mastraError;
1716
3169
  }
1717
3170
  }
1718
- async getResourceById({ resourceId }) {
1719
- const tableName = this.getTableName(TABLE_RESOURCES);
3171
+ async init() {
3172
+ if (this.isConnected === null) {
3173
+ this.isConnected = this._performInitializationAndStore();
3174
+ }
1720
3175
  try {
1721
- const req = this.pool.request();
1722
- req.input("resourceId", resourceId);
1723
- const result = (await req.query(`SELECT * FROM ${tableName} WHERE id = @resourceId`)).recordset[0];
1724
- if (!result) {
1725
- return null;
3176
+ await this.isConnected;
3177
+ await super.init();
3178
+ try {
3179
+ await this.#db.createAutomaticIndexes();
3180
+ } catch (indexError) {
3181
+ this.logger?.warn?.("Failed to create indexes:", indexError);
1726
3182
  }
1727
- return {
1728
- ...result,
1729
- workingMemory: typeof result.workingMemory === "object" ? JSON.stringify(result.workingMemory) : result.workingMemory,
1730
- metadata: typeof result.metadata === "string" ? JSON.parse(result.metadata) : result.metadata
1731
- };
1732
3183
  } catch (error) {
1733
- const mastraError = new MastraError(
3184
+ this.isConnected = null;
3185
+ throw new MastraError(
1734
3186
  {
1735
- id: "MASTRA_STORAGE_MSSQL_GET_RESOURCE_BY_ID_FAILED",
3187
+ id: createStorageErrorId("MSSQL", "INIT", "FAILED"),
1736
3188
  domain: ErrorDomain.STORAGE,
1737
- category: ErrorCategory.THIRD_PARTY,
1738
- details: { resourceId }
3189
+ category: ErrorCategory.THIRD_PARTY
1739
3190
  },
1740
3191
  error
1741
3192
  );
1742
- this.logger?.error?.(mastraError.toString());
1743
- this.logger?.trackException(mastraError);
1744
- throw mastraError;
1745
3193
  }
1746
3194
  }
1747
- async getScoreById({ id }) {
1748
- throw new MastraError({
1749
- id: "STORAGE_MONGODB_STORE_GET_SCORE_BY_ID_FAILED",
1750
- domain: ErrorDomain.STORAGE,
1751
- category: ErrorCategory.THIRD_PARTY,
1752
- details: { id },
1753
- text: "getScoreById is not implemented yet in MongoDBStore"
1754
- });
3195
+ async _performInitializationAndStore() {
3196
+ try {
3197
+ await this.pool.connect();
3198
+ return true;
3199
+ } catch (err) {
3200
+ throw err;
3201
+ }
1755
3202
  }
1756
- async saveScore(_score) {
1757
- throw new MastraError({
1758
- id: "STORAGE_MONGODB_STORE_SAVE_SCORE_FAILED",
1759
- domain: ErrorDomain.STORAGE,
1760
- category: ErrorCategory.THIRD_PARTY,
1761
- details: {},
1762
- text: "saveScore is not implemented yet in MongoDBStore"
1763
- });
3203
+ get supports() {
3204
+ return {
3205
+ selectByIncludeResourceScope: true,
3206
+ resourceWorkingMemory: true,
3207
+ hasColumn: true,
3208
+ createTable: true,
3209
+ deleteMessages: true,
3210
+ listScoresBySpan: true,
3211
+ observabilityInstance: true,
3212
+ indexManagement: true
3213
+ };
1764
3214
  }
1765
- async getScoresByScorerId({
1766
- scorerId,
3215
+ /**
3216
+ * Memory
3217
+ */
3218
+ async getThreadById({ threadId }) {
3219
+ return this.stores.memory.getThreadById({ threadId });
3220
+ }
3221
+ async saveThread({ thread }) {
3222
+ return this.stores.memory.saveThread({ thread });
3223
+ }
3224
+ async updateThread({
3225
+ id,
3226
+ title,
3227
+ metadata
3228
+ }) {
3229
+ return this.stores.memory.updateThread({ id, title, metadata });
3230
+ }
3231
+ async deleteThread({ threadId }) {
3232
+ return this.stores.memory.deleteThread({ threadId });
3233
+ }
3234
+ async listMessagesById({ messageIds }) {
3235
+ return this.stores.memory.listMessagesById({ messageIds });
3236
+ }
3237
+ async saveMessages(args) {
3238
+ return this.stores.memory.saveMessages(args);
3239
+ }
3240
+ async updateMessages({
3241
+ messages
3242
+ }) {
3243
+ return this.stores.memory.updateMessages({ messages });
3244
+ }
3245
+ async deleteMessages(messageIds) {
3246
+ return this.stores.memory.deleteMessages(messageIds);
3247
+ }
3248
+ async getResourceById({ resourceId }) {
3249
+ return this.stores.memory.getResourceById({ resourceId });
3250
+ }
3251
+ async saveResource({ resource }) {
3252
+ return this.stores.memory.saveResource({ resource });
3253
+ }
3254
+ async updateResource({
3255
+ resourceId,
3256
+ workingMemory,
3257
+ metadata
3258
+ }) {
3259
+ return this.stores.memory.updateResource({ resourceId, workingMemory, metadata });
3260
+ }
3261
+ /**
3262
+ * Workflows
3263
+ */
3264
+ async updateWorkflowResults({
3265
+ workflowName,
3266
+ runId,
3267
+ stepId,
3268
+ result,
3269
+ requestContext
3270
+ }) {
3271
+ return this.stores.workflows.updateWorkflowResults({ workflowName, runId, stepId, result, requestContext });
3272
+ }
3273
+ async updateWorkflowState({
3274
+ workflowName,
3275
+ runId,
3276
+ opts
3277
+ }) {
3278
+ return this.stores.workflows.updateWorkflowState({ workflowName, runId, opts });
3279
+ }
3280
+ async persistWorkflowSnapshot({
3281
+ workflowName,
3282
+ runId,
3283
+ resourceId,
3284
+ snapshot
3285
+ }) {
3286
+ return this.stores.workflows.persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot });
3287
+ }
3288
+ async loadWorkflowSnapshot({
3289
+ workflowName,
3290
+ runId
3291
+ }) {
3292
+ return this.stores.workflows.loadWorkflowSnapshot({ workflowName, runId });
3293
+ }
3294
+ async listWorkflowRuns(args = {}) {
3295
+ return this.stores.workflows.listWorkflowRuns(args);
3296
+ }
3297
+ async getWorkflowRunById({
3298
+ runId,
3299
+ workflowName
3300
+ }) {
3301
+ return this.stores.workflows.getWorkflowRunById({ runId, workflowName });
3302
+ }
3303
+ async deleteWorkflowRunById({ runId, workflowName }) {
3304
+ return this.stores.workflows.deleteWorkflowRunById({ runId, workflowName });
3305
+ }
3306
+ async close() {
3307
+ await this.pool.close();
3308
+ }
3309
+ /**
3310
+ * Index Management
3311
+ */
3312
+ async createIndex(options) {
3313
+ return this.#db.createIndex(options);
3314
+ }
3315
+ async listIndexes(tableName) {
3316
+ return this.#db.listIndexes(tableName);
3317
+ }
3318
+ async describeIndex(indexName) {
3319
+ return this.#db.describeIndex(indexName);
3320
+ }
3321
+ async dropIndex(indexName) {
3322
+ return this.#db.dropIndex(indexName);
3323
+ }
3324
+ /**
3325
+ * Tracing / Observability
3326
+ */
3327
+ getObservabilityStore() {
3328
+ if (!this.stores.observability) {
3329
+ throw new MastraError({
3330
+ id: createStorageErrorId("MSSQL", "OBSERVABILITY", "NOT_INITIALIZED"),
3331
+ domain: ErrorDomain.STORAGE,
3332
+ category: ErrorCategory.SYSTEM,
3333
+ text: "Observability storage is not initialized"
3334
+ });
3335
+ }
3336
+ return this.stores.observability;
3337
+ }
3338
+ async createSpan(span) {
3339
+ return this.getObservabilityStore().createSpan(span);
3340
+ }
3341
+ async updateSpan({
3342
+ spanId,
3343
+ traceId,
3344
+ updates
3345
+ }) {
3346
+ return this.getObservabilityStore().updateSpan({ spanId, traceId, updates });
3347
+ }
3348
+ async getTrace(traceId) {
3349
+ return this.getObservabilityStore().getTrace(traceId);
3350
+ }
3351
+ async getTracesPaginated(args) {
3352
+ return this.getObservabilityStore().getTracesPaginated(args);
3353
+ }
3354
+ async batchCreateSpans(args) {
3355
+ return this.getObservabilityStore().batchCreateSpans(args);
3356
+ }
3357
+ async batchUpdateSpans(args) {
3358
+ return this.getObservabilityStore().batchUpdateSpans(args);
3359
+ }
3360
+ async batchDeleteTraces(args) {
3361
+ return this.getObservabilityStore().batchDeleteTraces(args);
3362
+ }
3363
+ /**
3364
+ * Scorers
3365
+ */
3366
+ async getScoreById({ id: _id }) {
3367
+ return this.stores.scores.getScoreById({ id: _id });
3368
+ }
3369
+ async listScoresByScorerId({
3370
+ scorerId: _scorerId,
1767
3371
  pagination: _pagination,
1768
- entityId,
1769
- entityType
3372
+ entityId: _entityId,
3373
+ entityType: _entityType,
3374
+ source: _source
1770
3375
  }) {
1771
- throw new MastraError({
1772
- id: "STORAGE_MONGODB_STORE_GET_SCORES_BY_SCORER_ID_FAILED",
1773
- domain: ErrorDomain.STORAGE,
1774
- category: ErrorCategory.THIRD_PARTY,
1775
- details: { scorerId, entityId: entityId || "", entityType: entityType || "" },
1776
- text: "getScoresByScorerId is not implemented yet in MongoDBStore"
3376
+ return this.stores.scores.listScoresByScorerId({
3377
+ scorerId: _scorerId,
3378
+ pagination: _pagination,
3379
+ entityId: _entityId,
3380
+ entityType: _entityType,
3381
+ source: _source
1777
3382
  });
1778
3383
  }
1779
- async getScoresByRunId({
1780
- runId,
3384
+ async saveScore(score) {
3385
+ return this.stores.scores.saveScore(score);
3386
+ }
3387
+ async listScoresByRunId({
3388
+ runId: _runId,
1781
3389
  pagination: _pagination
1782
3390
  }) {
1783
- throw new MastraError({
1784
- id: "STORAGE_MONGODB_STORE_GET_SCORES_BY_RUN_ID_FAILED",
1785
- domain: ErrorDomain.STORAGE,
1786
- category: ErrorCategory.THIRD_PARTY,
1787
- details: { runId },
1788
- text: "getScoresByRunId is not implemented yet in MongoDBStore"
1789
- });
3391
+ return this.stores.scores.listScoresByRunId({ runId: _runId, pagination: _pagination });
1790
3392
  }
1791
- async getScoresByEntityId({
1792
- entityId,
1793
- entityType,
3393
+ async listScoresByEntityId({
3394
+ entityId: _entityId,
3395
+ entityType: _entityType,
1794
3396
  pagination: _pagination
1795
3397
  }) {
1796
- throw new MastraError({
1797
- id: "STORAGE_MONGODB_STORE_GET_SCORES_BY_ENTITY_ID_FAILED",
1798
- domain: ErrorDomain.STORAGE,
1799
- category: ErrorCategory.THIRD_PARTY,
1800
- details: { entityId, entityType },
1801
- text: "getScoresByEntityId is not implemented yet in MongoDBStore"
3398
+ return this.stores.scores.listScoresByEntityId({
3399
+ entityId: _entityId,
3400
+ entityType: _entityType,
3401
+ pagination: _pagination
1802
3402
  });
1803
3403
  }
1804
- async dropTable({ tableName }) {
1805
- throw new MastraError({
1806
- id: "STORAGE_MONGODB_STORE_DROP_TABLE_FAILED",
1807
- domain: ErrorDomain.STORAGE,
1808
- category: ErrorCategory.THIRD_PARTY,
1809
- details: { tableName },
1810
- text: "dropTable is not implemented yet in MongoDBStore"
1811
- });
3404
+ async listScoresBySpan({
3405
+ traceId,
3406
+ spanId,
3407
+ pagination: _pagination
3408
+ }) {
3409
+ return this.stores.scores.listScoresBySpan({ traceId, spanId, pagination: _pagination });
1812
3410
  }
1813
3411
  };
1814
3412