@mastra/libsql 1.0.0-beta.12 → 1.0.0-beta.14

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,8 @@
1
1
  import { createClient } from '@libsql/client';
2
2
  import { MastraError, ErrorCategory, ErrorDomain } from '@mastra/core/error';
3
- import { createVectorErrorId, AgentsStorage, AGENTS_SCHEMA, TABLE_AGENTS, createStorageErrorId, normalizePerPage, calculatePagination, MemoryStorage, TABLE_SCHEMAS, TABLE_THREADS, TABLE_MESSAGES, TABLE_RESOURCES, ObservabilityStorage, SPAN_SCHEMA, TABLE_SPANS, listTracesArgsSchema, ScoresStorage, SCORERS_SCHEMA, TABLE_SCORERS, transformScoreRow, WorkflowsStorage, TABLE_WORKFLOW_SNAPSHOT, MastraStorage, TraceStatus, getSqlType, safelyParseJSON } from '@mastra/core/storage';
3
+ import { createVectorErrorId, AgentsStorage, AGENTS_SCHEMA, TABLE_AGENTS, createStorageErrorId, normalizePerPage, calculatePagination, MemoryStorage, TABLE_SCHEMAS, TABLE_THREADS, TABLE_MESSAGES, TABLE_RESOURCES, ObservabilityStorage, SPAN_SCHEMA, TABLE_SPANS, listTracesArgsSchema, ScoresStorage, SCORERS_SCHEMA, TABLE_SCORERS, transformScoreRow, WorkflowsStorage, TABLE_WORKFLOW_SNAPSHOT, MastraCompositeStore, TraceStatus, getSqlType, safelyParseJSON } from '@mastra/core/storage';
4
4
  import { parseSqlIdentifier, parseFieldKey } from '@mastra/core/utils';
5
- import { MastraVector } from '@mastra/core/vector';
5
+ import { MastraVector, validateTopK, validateUpsertInput } from '@mastra/core/vector';
6
6
  import { BaseFilterTranslator } from '@mastra/core/vector/filter';
7
7
  import { MastraBase } from '@mastra/core/base';
8
8
  import { MessageList } from '@mastra/core/agent';
@@ -242,10 +242,10 @@ var FILTER_OPERATORS = {
242
242
  };
243
243
  },
244
244
  // Element Operators
245
- $exists: (key) => {
245
+ $exists: (key, value) => {
246
246
  const jsonPath = getJsonPath(key);
247
247
  return {
248
- sql: `json_extract(metadata, ${jsonPath}) IS NOT NULL`,
248
+ sql: value === false ? `json_extract(metadata, ${jsonPath}) IS NULL` : `json_extract(metadata, ${jsonPath}) IS NOT NULL`,
249
249
  needsValue: false
250
250
  };
251
251
  },
@@ -538,7 +538,7 @@ var LibSQLVector = class extends MastraVector {
538
538
  try {
539
539
  return await operation();
540
540
  } catch (error) {
541
- if (error.code === "SQLITE_BUSY" || error.message && error.message.toLowerCase().includes("database is locked")) {
541
+ if (error.code === "SQLITE_BUSY" || error.code === "SQLITE_LOCKED" || error.code === "SQLITE_LOCKED_SHAREDCACHE" || error.message && error.message.toLowerCase().includes("database is locked") || error.message && error.message.toLowerCase().includes("database table is locked")) {
542
542
  attempts++;
543
543
  if (attempts >= this.maxRetries) {
544
544
  this.logger.error(
@@ -572,22 +572,14 @@ var LibSQLVector = class extends MastraVector {
572
572
  minScore = -1
573
573
  // Default to -1 to include all results (cosine similarity ranges from -1 to 1)
574
574
  }) {
575
- try {
576
- if (!Number.isInteger(topK) || topK <= 0) {
577
- throw new Error("topK must be a positive integer");
578
- }
579
- if (!Array.isArray(queryVector) || !queryVector.every((x) => typeof x === "number" && Number.isFinite(x))) {
580
- throw new Error("queryVector must be an array of finite numbers");
581
- }
582
- } catch (error) {
583
- throw new MastraError(
584
- {
585
- id: createVectorErrorId("LIBSQL", "QUERY", "INVALID_ARGS"),
586
- domain: ErrorDomain.STORAGE,
587
- category: ErrorCategory.USER
588
- },
589
- error
590
- );
575
+ validateTopK("LIBSQL", topK);
576
+ if (!Array.isArray(queryVector) || !queryVector.every((x) => typeof x === "number" && Number.isFinite(x))) {
577
+ throw new MastraError({
578
+ id: createVectorErrorId("LIBSQL", "QUERY", "INVALID_ARGS"),
579
+ domain: ErrorDomain.STORAGE,
580
+ category: ErrorCategory.USER,
581
+ details: { message: "queryVector must be an array of finite numbers" }
582
+ });
591
583
  }
592
584
  try {
593
585
  const parsedIndexName = parseSqlIdentifier(indexName, "index name");
@@ -647,6 +639,7 @@ var LibSQLVector = class extends MastraVector {
647
639
  }
648
640
  }
649
641
  async doUpsert({ indexName, vectors, metadata, ids }) {
642
+ validateUpsertInput("LIBSQL", vectors, metadata, ids);
650
643
  const tx = await this.turso.transaction("write");
651
644
  try {
652
645
  const parsedIndexName = parseSqlIdentifier(indexName, "index name");
@@ -1710,6 +1703,9 @@ var LibSQLDB = class extends MastraBase {
1710
1703
  if (tableName === TABLE_WORKFLOW_SNAPSHOT) {
1711
1704
  tableConstraints.push("UNIQUE (workflow_name, run_id)");
1712
1705
  }
1706
+ if (tableName === TABLE_SPANS) {
1707
+ tableConstraints.push("UNIQUE (spanId, traceId)");
1708
+ }
1713
1709
  const allDefinitions = [...columnDefinitions, ...tableConstraints].join(",\n ");
1714
1710
  const sql = `CREATE TABLE IF NOT EXISTS ${parsedTableName} (
1715
1711
  ${allDefinitions}
@@ -1720,6 +1716,9 @@ var LibSQLDB = class extends MastraBase {
1720
1716
  await this.migrateSpansTable();
1721
1717
  }
1722
1718
  } catch (error) {
1719
+ if (error instanceof MastraError) {
1720
+ throw error;
1721
+ }
1723
1722
  throw new MastraError(
1724
1723
  {
1725
1724
  id: createStorageErrorId("LIBSQL", "CREATE_TABLE", "FAILED"),
@@ -1733,7 +1732,7 @@ var LibSQLDB = class extends MastraBase {
1733
1732
  }
1734
1733
  /**
1735
1734
  * Migrates the spans table schema from OLD_SPAN_SCHEMA to current SPAN_SCHEMA.
1736
- * This adds new columns that don't exist in old schema.
1735
+ * This adds new columns that don't exist in old schema and ensures required indexes exist.
1737
1736
  */
1738
1737
  async migrateSpansTable() {
1739
1738
  const schema = TABLE_SCHEMAS[TABLE_SPANS];
@@ -1747,11 +1746,205 @@ var LibSQLDB = class extends MastraBase {
1747
1746
  this.logger.debug(`LibSQLDB: Added column '${columnName}' to ${TABLE_SPANS}`);
1748
1747
  }
1749
1748
  }
1749
+ const indexExists = await this.spansUniqueIndexExists();
1750
+ if (!indexExists) {
1751
+ const duplicateInfo = await this.checkForDuplicateSpans();
1752
+ if (duplicateInfo.hasDuplicates) {
1753
+ const errorMessage = `
1754
+ ===========================================================================
1755
+ MIGRATION REQUIRED: Duplicate spans detected in ${TABLE_SPANS}
1756
+ ===========================================================================
1757
+
1758
+ Found ${duplicateInfo.duplicateCount} duplicate (traceId, spanId) combinations.
1759
+
1760
+ The spans table requires a unique constraint on (traceId, spanId), but your
1761
+ database contains duplicate entries that must be resolved first.
1762
+
1763
+ To fix this, run the manual migration command:
1764
+
1765
+ npx mastra migrate
1766
+
1767
+ This command will:
1768
+ 1. Remove duplicate spans (keeping the most complete/recent version)
1769
+ 2. Add the required unique constraint
1770
+
1771
+ Note: This migration may take some time for large tables.
1772
+ ===========================================================================
1773
+ `;
1774
+ throw new MastraError({
1775
+ id: createStorageErrorId("LIBSQL", "MIGRATION_REQUIRED", "DUPLICATE_SPANS"),
1776
+ domain: ErrorDomain.STORAGE,
1777
+ category: ErrorCategory.USER,
1778
+ text: errorMessage
1779
+ });
1780
+ } else {
1781
+ await this.client.execute(
1782
+ `CREATE UNIQUE INDEX IF NOT EXISTS "mastra_ai_spans_spanid_traceid_idx" ON "${TABLE_SPANS}" ("spanId", "traceId")`
1783
+ );
1784
+ this.logger.debug(`LibSQLDB: Created unique index on (spanId, traceId) for ${TABLE_SPANS}`);
1785
+ }
1786
+ }
1750
1787
  this.logger.info(`LibSQLDB: Migration completed for ${TABLE_SPANS}`);
1751
1788
  } catch (error) {
1789
+ if (error instanceof MastraError) {
1790
+ throw error;
1791
+ }
1752
1792
  this.logger.warn(`LibSQLDB: Failed to migrate spans table ${TABLE_SPANS}:`, error);
1753
1793
  }
1754
1794
  }
1795
+ /**
1796
+ * Checks if the unique index on (spanId, traceId) already exists on the spans table.
1797
+ * Used to skip deduplication when the index already exists (migration already complete).
1798
+ */
1799
+ async spansUniqueIndexExists() {
1800
+ try {
1801
+ const result = await this.client.execute(
1802
+ `SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = 'mastra_ai_spans_spanid_traceid_idx'`
1803
+ );
1804
+ return (result.rows?.length ?? 0) > 0;
1805
+ } catch {
1806
+ return false;
1807
+ }
1808
+ }
1809
+ /**
1810
+ * Checks for duplicate (traceId, spanId) combinations in the spans table.
1811
+ * Returns information about duplicates for logging/CLI purposes.
1812
+ */
1813
+ async checkForDuplicateSpans() {
1814
+ try {
1815
+ const result = await this.client.execute(`
1816
+ SELECT COUNT(*) as duplicate_count FROM (
1817
+ SELECT "spanId", "traceId"
1818
+ FROM "${TABLE_SPANS}"
1819
+ GROUP BY "spanId", "traceId"
1820
+ HAVING COUNT(*) > 1
1821
+ )
1822
+ `);
1823
+ const duplicateCount = Number(result.rows?.[0]?.duplicate_count ?? 0);
1824
+ return {
1825
+ hasDuplicates: duplicateCount > 0,
1826
+ duplicateCount
1827
+ };
1828
+ } catch (error) {
1829
+ this.logger.debug(`LibSQLDB: Could not check for duplicates: ${error}`);
1830
+ return { hasDuplicates: false, duplicateCount: 0 };
1831
+ }
1832
+ }
1833
+ /**
1834
+ * Manually run the spans migration to deduplicate and add the unique constraint.
1835
+ * This is intended to be called from the CLI when duplicates are detected.
1836
+ *
1837
+ * @returns Migration result with status and details
1838
+ */
1839
+ async migrateSpans() {
1840
+ const indexExists = await this.spansUniqueIndexExists();
1841
+ if (indexExists) {
1842
+ return {
1843
+ success: true,
1844
+ alreadyMigrated: true,
1845
+ duplicatesRemoved: 0,
1846
+ message: `Migration already complete. Unique index exists on ${TABLE_SPANS}.`
1847
+ };
1848
+ }
1849
+ const duplicateInfo = await this.checkForDuplicateSpans();
1850
+ if (duplicateInfo.hasDuplicates) {
1851
+ this.logger.info(
1852
+ `Found ${duplicateInfo.duplicateCount} duplicate (traceId, spanId) combinations. Starting deduplication...`
1853
+ );
1854
+ await this.deduplicateSpans();
1855
+ } else {
1856
+ this.logger.info(`No duplicate spans found.`);
1857
+ }
1858
+ await this.client.execute(
1859
+ `CREATE UNIQUE INDEX IF NOT EXISTS "mastra_ai_spans_spanid_traceid_idx" ON "${TABLE_SPANS}" ("spanId", "traceId")`
1860
+ );
1861
+ return {
1862
+ success: true,
1863
+ alreadyMigrated: false,
1864
+ duplicatesRemoved: duplicateInfo.duplicateCount,
1865
+ message: duplicateInfo.hasDuplicates ? `Migration complete. Removed duplicates and added unique index to ${TABLE_SPANS}.` : `Migration complete. Added unique index to ${TABLE_SPANS}.`
1866
+ };
1867
+ }
1868
+ /**
1869
+ * Check migration status for the spans table.
1870
+ * Returns information about whether migration is needed.
1871
+ */
1872
+ async checkSpansMigrationStatus() {
1873
+ const indexExists = await this.spansUniqueIndexExists();
1874
+ if (indexExists) {
1875
+ return {
1876
+ needsMigration: false,
1877
+ hasDuplicates: false,
1878
+ duplicateCount: 0,
1879
+ constraintExists: true,
1880
+ tableName: TABLE_SPANS
1881
+ };
1882
+ }
1883
+ const duplicateInfo = await this.checkForDuplicateSpans();
1884
+ return {
1885
+ needsMigration: true,
1886
+ hasDuplicates: duplicateInfo.hasDuplicates,
1887
+ duplicateCount: duplicateInfo.duplicateCount,
1888
+ constraintExists: false,
1889
+ tableName: TABLE_SPANS
1890
+ };
1891
+ }
1892
+ /**
1893
+ * Deduplicates spans table by removing duplicate (spanId, traceId) combinations.
1894
+ * Keeps the "best" record for each duplicate group based on:
1895
+ * 1. Completed spans (endedAt IS NOT NULL) over incomplete ones
1896
+ * 2. Most recently updated (updatedAt DESC)
1897
+ * 3. Most recently created (createdAt DESC) as tiebreaker
1898
+ */
1899
+ async deduplicateSpans() {
1900
+ try {
1901
+ const duplicateCheck = await this.client.execute(`
1902
+ SELECT COUNT(*) as duplicate_count FROM (
1903
+ SELECT "spanId", "traceId"
1904
+ FROM "${TABLE_SPANS}"
1905
+ GROUP BY "spanId", "traceId"
1906
+ HAVING COUNT(*) > 1
1907
+ )
1908
+ `);
1909
+ const duplicateCount = Number(duplicateCheck.rows?.[0]?.duplicate_count ?? 0);
1910
+ if (duplicateCount === 0) {
1911
+ this.logger.debug(`LibSQLDB: No duplicate spans found, skipping deduplication`);
1912
+ return;
1913
+ }
1914
+ this.logger.warn(`LibSQLDB: Found ${duplicateCount} duplicate (spanId, traceId) combinations, deduplicating...`);
1915
+ const deleteResult = await this.client.execute(`
1916
+ DELETE FROM "${TABLE_SPANS}"
1917
+ WHERE rowid NOT IN (
1918
+ SELECT MIN(best_rowid) FROM (
1919
+ SELECT
1920
+ rowid as best_rowid,
1921
+ "spanId",
1922
+ "traceId",
1923
+ ROW_NUMBER() OVER (
1924
+ PARTITION BY "spanId", "traceId"
1925
+ ORDER BY
1926
+ CASE WHEN "endedAt" IS NOT NULL THEN 0 ELSE 1 END,
1927
+ "updatedAt" DESC,
1928
+ "createdAt" DESC
1929
+ ) as rn
1930
+ FROM "${TABLE_SPANS}"
1931
+ ) ranked
1932
+ WHERE rn = 1
1933
+ GROUP BY "spanId", "traceId"
1934
+ )
1935
+ AND ("spanId", "traceId") IN (
1936
+ SELECT "spanId", "traceId"
1937
+ FROM "${TABLE_SPANS}"
1938
+ GROUP BY "spanId", "traceId"
1939
+ HAVING COUNT(*) > 1
1940
+ )
1941
+ `);
1942
+ const deletedCount = deleteResult.rowsAffected ?? 0;
1943
+ this.logger.warn(`LibSQLDB: Deleted ${deletedCount} duplicate span records`);
1944
+ } catch (error) {
1945
+ this.logger.warn(`LibSQLDB: Failed to deduplicate spans:`, error);
1946
+ }
1947
+ }
1755
1948
  /**
1756
1949
  * Gets a default value for a column type (used when adding NOT NULL columns).
1757
1950
  */
@@ -2645,33 +2838,76 @@ var MemoryLibSQL = class extends MemoryStorage {
2645
2838
  );
2646
2839
  }
2647
2840
  }
2648
- async listThreadsByResourceId(args) {
2649
- const { resourceId, page = 0, perPage: perPageInput, orderBy } = args;
2650
- if (page < 0) {
2841
+ async listThreads(args) {
2842
+ const { page = 0, perPage: perPageInput, orderBy, filter } = args;
2843
+ try {
2844
+ this.validatePaginationInput(page, perPageInput ?? 100);
2845
+ } catch (error) {
2651
2846
  throw new MastraError(
2652
2847
  {
2653
- id: createStorageErrorId("LIBSQL", "LIST_THREADS_BY_RESOURCE_ID", "INVALID_PAGE"),
2848
+ id: createStorageErrorId("LIBSQL", "LIST_THREADS", "INVALID_PAGE"),
2654
2849
  domain: ErrorDomain.STORAGE,
2655
2850
  category: ErrorCategory.USER,
2656
- details: { page }
2851
+ details: { page, ...perPageInput !== void 0 && { perPage: perPageInput } }
2657
2852
  },
2658
- new Error("page must be >= 0")
2853
+ error instanceof Error ? error : new Error("Invalid pagination parameters")
2659
2854
  );
2660
2855
  }
2661
2856
  const perPage = normalizePerPage(perPageInput, 100);
2857
+ try {
2858
+ this.validateMetadataKeys(filter?.metadata);
2859
+ } catch (error) {
2860
+ throw new MastraError(
2861
+ {
2862
+ id: createStorageErrorId("LIBSQL", "LIST_THREADS", "INVALID_METADATA_KEY"),
2863
+ domain: ErrorDomain.STORAGE,
2864
+ category: ErrorCategory.USER,
2865
+ details: { metadataKeys: filter?.metadata ? Object.keys(filter.metadata).join(", ") : "" }
2866
+ },
2867
+ error instanceof Error ? error : new Error("Invalid metadata key")
2868
+ );
2869
+ }
2662
2870
  const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
2663
2871
  const { field, direction } = this.parseOrderBy(orderBy);
2664
2872
  try {
2665
- const baseQuery = `FROM ${TABLE_THREADS} WHERE resourceId = ?`;
2666
- const queryParams = [resourceId];
2873
+ const whereClauses = [];
2874
+ const queryParams = [];
2875
+ if (filter?.resourceId) {
2876
+ whereClauses.push("resourceId = ?");
2877
+ queryParams.push(filter.resourceId);
2878
+ }
2879
+ if (filter?.metadata && Object.keys(filter.metadata).length > 0) {
2880
+ for (const [key, value] of Object.entries(filter.metadata)) {
2881
+ if (value === null) {
2882
+ whereClauses.push(`json_extract(metadata, '$.${key}') IS NULL`);
2883
+ } else if (typeof value === "boolean") {
2884
+ whereClauses.push(`json_extract(metadata, '$.${key}') = ?`);
2885
+ queryParams.push(value ? 1 : 0);
2886
+ } else if (typeof value === "number") {
2887
+ whereClauses.push(`json_extract(metadata, '$.${key}') = ?`);
2888
+ queryParams.push(value);
2889
+ } else if (typeof value === "string") {
2890
+ whereClauses.push(`json_extract(metadata, '$.${key}') = ?`);
2891
+ queryParams.push(value);
2892
+ } else {
2893
+ throw new MastraError({
2894
+ id: createStorageErrorId("LIBSQL", "LIST_THREADS", "INVALID_METADATA_VALUE"),
2895
+ domain: ErrorDomain.STORAGE,
2896
+ category: ErrorCategory.USER,
2897
+ text: `Metadata filter value for key "${key}" must be a scalar type (string, number, boolean, or null), got ${typeof value}`,
2898
+ details: { key, valueType: typeof value }
2899
+ });
2900
+ }
2901
+ }
2902
+ }
2903
+ const whereClause = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
2904
+ const baseQuery = `FROM ${TABLE_THREADS} ${whereClause}`;
2667
2905
  const mapRowToStorageThreadType = (row) => ({
2668
2906
  id: row.id,
2669
2907
  resourceId: row.resourceId,
2670
2908
  title: row.title,
2671
2909
  createdAt: new Date(row.createdAt),
2672
- // Convert string to Date
2673
2910
  updatedAt: new Date(row.updatedAt),
2674
- // Convert string to Date
2675
2911
  metadata: typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata
2676
2912
  });
2677
2913
  const countResult = await this.#client.execute({
@@ -2702,12 +2938,18 @@ var MemoryLibSQL = class extends MemoryStorage {
2702
2938
  hasMore: perPageInput === false ? false : offset + perPage < total
2703
2939
  };
2704
2940
  } catch (error) {
2941
+ if (error instanceof MastraError && error.category === ErrorCategory.USER) {
2942
+ throw error;
2943
+ }
2705
2944
  const mastraError = new MastraError(
2706
2945
  {
2707
- id: createStorageErrorId("LIBSQL", "LIST_THREADS_BY_RESOURCE_ID", "FAILED"),
2946
+ id: createStorageErrorId("LIBSQL", "LIST_THREADS", "FAILED"),
2708
2947
  domain: ErrorDomain.STORAGE,
2709
2948
  category: ErrorCategory.THIRD_PARTY,
2710
- details: { resourceId }
2949
+ details: {
2950
+ ...filter?.resourceId && { resourceId: filter.resourceId },
2951
+ hasMetadataFilter: !!filter?.metadata
2952
+ }
2711
2953
  },
2712
2954
  error
2713
2955
  );
@@ -2970,6 +3212,22 @@ var ObservabilityLibSQL = class extends ObservabilityStorage {
2970
3212
  async dangerouslyClearAll() {
2971
3213
  await this.#db.deleteData({ tableName: TABLE_SPANS });
2972
3214
  }
3215
+ /**
3216
+ * Manually run the spans migration to deduplicate and add the unique constraint.
3217
+ * This is intended to be called from the CLI when duplicates are detected.
3218
+ *
3219
+ * @returns Migration result with status and details
3220
+ */
3221
+ async migrateSpans() {
3222
+ return this.#db.migrateSpans();
3223
+ }
3224
+ /**
3225
+ * Check migration status for the spans table.
3226
+ * Returns information about whether migration is needed.
3227
+ */
3228
+ async checkSpansMigrationStatus() {
3229
+ return this.#db.checkSpansMigrationStatus();
3230
+ }
2973
3231
  get tracingStrategy() {
2974
3232
  return {
2975
3233
  preferred: "batch-with-updates",
@@ -4005,7 +4263,7 @@ var WorkflowsLibSQL = class extends WorkflowsStorage {
4005
4263
  };
4006
4264
 
4007
4265
  // src/storage/index.ts
4008
- var LibSQLStore = class extends MastraStorage {
4266
+ var LibSQLStore = class extends MastraCompositeStore {
4009
4267
  client;
4010
4268
  maxRetries;
4011
4269
  initialBackoffMs;