@mastra/mssql 0.0.0-fix-zod4-schema-validation-20251212180638 → 0.0.0-fix-11329-windows-path-20251222155941

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