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