@mastra/spanner 1.2.4-alpha.0 → 1.3.0-alpha.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # @mastra/spanner
2
2
 
3
+ ## 1.3.0-alpha.2
4
+
5
+ ### Minor Changes
6
+
7
+ - Added atomic caller-defined dataset IDs for idempotent dataset creation across built-in storage adapters. Supplying `id` to `mastra.datasets.create()` now creates the dataset once and resolves compatible retries to the persisted record; incompatible immutable identity fields throw `DATASET_ID_CONFLICT`. ([#19370](https://github.com/mastra-ai/mastra/pull/19370))
8
+
9
+ - Added caller-defined dataset item identities for safe retries across all dataset storage adapters. ([#19384](https://github.com/mastra-ai/mastra/pull/19384))
10
+
11
+ Dataset items can now include an `externalId` when calling `addItem` or `addItems`:
12
+
13
+ ```ts
14
+ await dataset.addItem({
15
+ externalId: 'source-item-123',
16
+ input: { prompt: 'Hello' },
17
+ });
18
+ ```
19
+
20
+ Retrying with the same identity and payload returns the existing item. Reusing an identity with different content returns a typed conflict, including during concurrent writes. Updates and deletes preserve the identity, Spanner retries transactions without changing the outcome, and MySQL batch writes now preserve every supported dataset item field.
21
+
22
+ ### Patch Changes
23
+
24
+ - Raised the `@mastra/core` peer dependency floor to `>=1.51.0-0` so the dataset item identity helpers used by the storage adapters are available at runtime. ([#19384](https://github.com/mastra-ai/mastra/pull/19384))
25
+
26
+ - Updated dependencies [[`a99eae8`](https://github.com/mastra-ai/mastra/commit/a99eae8908e500c1b2d12f9d277be616b98617a5), [`fd13f8e`](https://github.com/mastra-ai/mastra/commit/fd13f8e21990f9904c3eedba3a626bb4a929cdb8), [`f703f87`](https://github.com/mastra-ai/mastra/commit/f703f878de072d51fda557f9c50867d8252bef05), [`0ad646f`](https://github.com/mastra-ai/mastra/commit/0ad646f71a530f2454664299e5e01bfd13fa12e5)]:
27
+ - @mastra/core@1.51.0-alpha.13
28
+
29
+ ## 1.2.4-alpha.1
30
+
31
+ ### Patch Changes
32
+
33
+ - dependencies updates: ([#19038](https://github.com/mastra-ai/mastra/pull/19038))
34
+ - Updated dependency [`@google-cloud/spanner@^8.8.0` ↗︎](https://www.npmjs.com/package/@google-cloud/spanner/v/8.8.0) (from `^8.7.1`, in `dependencies`)
35
+
36
+ - dependencies updates: ([#19390](https://github.com/mastra-ai/mastra/pull/19390))
37
+ - Updated dependency [`@google-cloud/spanner@^8.8.1` ↗︎](https://www.npmjs.com/package/@google-cloud/spanner/v/8.8.1) (from `^8.8.0`, in `dependencies`)
38
+ - Updated dependencies [[`bd6d240`](https://github.com/mastra-ai/mastra/commit/bd6d2402db93dddaef0721667e7e8a030e7c6e16), [`0111486`](https://github.com/mastra-ai/mastra/commit/01114867612593eef5cfa2fda6a1194dfedda841), [`96a3749`](https://github.com/mastra-ai/mastra/commit/96a37492235f5b8076b3e3177d83ed5a5e44a640), [`3e26c87`](https://github.com/mastra-ai/mastra/commit/3e26c87de0c5bc2583b795ce6ca5889b6b161acb), [`a5008f2`](https://github.com/mastra-ai/mastra/commit/a5008f22ae710ad9402ea9f2547d8c02f74d384b)]:
39
+ - @mastra/core@1.51.0-alpha.8
40
+
3
41
  ## 1.2.4-alpha.0
4
42
 
5
43
  ### Patch Changes
@@ -3,7 +3,7 @@ name: mastra-spanner
3
3
  description: Documentation for @mastra/spanner. Use when working with @mastra/spanner APIs, configuration, or implementation.
4
4
  metadata:
5
5
  package: "@mastra/spanner"
6
- version: "1.2.4-alpha.0"
6
+ version: "1.3.0-alpha.2"
7
7
  ---
8
8
 
9
9
  ## When to use
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.2.4-alpha.0",
2
+ "version": "1.3.0-alpha.2",
3
3
  "package": "@mastra/spanner",
4
4
  "exports": {},
5
5
  "modules": {}
package/dist/index.cjs CHANGED
@@ -2995,6 +2995,7 @@ function rowToItem(row) {
2995
2995
  id: String(t.id),
2996
2996
  datasetId: String(t.datasetId),
2997
2997
  datasetVersion: Number(t.datasetVersion),
2998
+ externalId: t.externalId ?? null,
2998
2999
  organizationId: t.organizationId ?? null,
2999
3000
  projectId: t.projectId ?? null,
3000
3001
  input: t.input,
@@ -3059,7 +3060,7 @@ var DatasetsSpanner = class _DatasetsSpanner extends storage.DatasetsStorage {
3059
3060
  await this.db.alterTable({
3060
3061
  tableName: storage.TABLE_DATASET_ITEMS,
3061
3062
  schema: storage.TABLE_SCHEMAS[storage.TABLE_DATASET_ITEMS],
3062
- ifNotExists: ["organizationId", "projectId"]
3063
+ ifNotExists: ["organizationId", "projectId", "externalId"]
3063
3064
  });
3064
3065
  await this.createDefaultIndexes();
3065
3066
  await this.createCustomIndexes();
@@ -3078,6 +3079,11 @@ var DatasetsSpanner = class _DatasetsSpanner extends storage.DatasetsStorage {
3078
3079
  table: storage.TABLE_DATASET_ITEMS,
3079
3080
  columns: ["datasetId", "datasetVersion"]
3080
3081
  },
3082
+ {
3083
+ name: "mastra_dataset_items_dataset_externalid_version_idx",
3084
+ table: storage.TABLE_DATASET_ITEMS,
3085
+ columns: ["datasetId", "externalId", "datasetVersion"]
3086
+ },
3081
3087
  {
3082
3088
  // Unique invariant: one snapshot row per (datasetId, version). The DESC
3083
3089
  // ordering also serves listDatasetVersions' newest-first scan.
@@ -3121,7 +3127,8 @@ var DatasetsSpanner = class _DatasetsSpanner extends storage.DatasetsStorage {
3121
3127
  async createDataset(input) {
3122
3128
  try {
3123
3129
  const now = /* @__PURE__ */ new Date();
3124
- const id = crypto.randomUUID();
3130
+ const id = input.id ?? crypto.randomUUID();
3131
+ if (input.id !== void 0) this.validateCallerDefinedDatasetId(input.id);
3125
3132
  const record = {
3126
3133
  id,
3127
3134
  name: input.name,
@@ -3167,6 +3174,11 @@ var DatasetsSpanner = class _DatasetsSpanner extends storage.DatasetsStorage {
3167
3174
  });
3168
3175
  return record;
3169
3176
  } catch (error$1) {
3177
+ if (input.id !== void 0 && storage.hasErrorCode(error$1, /* @__PURE__ */ new Set([6, "ALREADY_EXISTS"]))) {
3178
+ const existing = await this.getDatasetById({ id: input.id });
3179
+ if (existing) return this.resolveExistingDataset(existing, { ...input, id: input.id });
3180
+ }
3181
+ if (error$1 instanceof error.MastraError) throw error$1;
3170
3182
  throw new error.MastraError(
3171
3183
  {
3172
3184
  id: storage.createStorageErrorId("SPANNER", "CREATE_DATASET", "FAILED"),
@@ -3589,6 +3601,7 @@ var DatasetsSpanner = class _DatasetsSpanner extends storage.DatasetsStorage {
3589
3601
  id: args.id,
3590
3602
  datasetId: args.datasetId,
3591
3603
  datasetVersion: newVersion,
3604
+ externalId: existing.externalId ?? null,
3592
3605
  organizationId,
3593
3606
  projectId,
3594
3607
  validTo: null,
@@ -3611,6 +3624,7 @@ var DatasetsSpanner = class _DatasetsSpanner extends storage.DatasetsStorage {
3611
3624
  id: args.id,
3612
3625
  datasetId: args.datasetId,
3613
3626
  datasetVersion: newVersion,
3627
+ externalId: existing.externalId ?? null,
3614
3628
  organizationId,
3615
3629
  projectId,
3616
3630
  input: merged.input,
@@ -3670,6 +3684,7 @@ var DatasetsSpanner = class _DatasetsSpanner extends storage.DatasetsStorage {
3670
3684
  id: args.id,
3671
3685
  datasetId: args.datasetId,
3672
3686
  datasetVersion: newVersion,
3687
+ externalId: existing.externalId ?? null,
3673
3688
  organizationId,
3674
3689
  projectId,
3675
3690
  validTo: null,
@@ -3939,55 +3954,75 @@ var DatasetsSpanner = class _DatasetsSpanner extends storage.DatasetsStorage {
3939
3954
  async _doBatchInsertItems(input) {
3940
3955
  try {
3941
3956
  if (input.items.length === 0) return [];
3942
- const now = /* @__PURE__ */ new Date();
3943
- const prepared = input.items.map((item) => ({ id: crypto.randomUUID(), item }));
3944
3957
  let result = [];
3945
3958
  await this.db.runWithAbortRetry(
3946
3959
  () => this.database.runTransactionAsync(async (tx) => {
3947
3960
  try {
3948
- const { version: newVersion, organizationId, projectId } = await this.bumpVersion(tx, input.datasetId, now);
3949
- for (const { id, item } of prepared) {
3950
- await this.db.insert({
3951
- tableName: storage.TABLE_DATASET_ITEMS,
3952
- record: {
3953
- id,
3961
+ const [datasetRows] = await tx.run({
3962
+ sql: `SELECT * FROM ${quoteIdent(storage.TABLE_DATASETS, "table name")} WHERE ${quoteIdent("id", "column name")} = @id LIMIT 1`,
3963
+ params: { id: input.datasetId },
3964
+ json: true
3965
+ });
3966
+ const dataset = datasetRows[0];
3967
+ if (!dataset) {
3968
+ throw new error.MastraError({
3969
+ id: storage.createStorageErrorId("SPANNER", "BATCH_INSERT_ITEMS", "DATASET_NOT_FOUND"),
3970
+ domain: error.ErrorDomain.STORAGE,
3971
+ category: error.ErrorCategory.USER,
3972
+ details: { datasetId: input.datasetId }
3973
+ });
3974
+ }
3975
+ const externalIds = [...new Set(input.items.flatMap((item) => item.externalId ? [item.externalId] : []))];
3976
+ let historyRows = [];
3977
+ if (externalIds.length > 0) {
3978
+ const [rows] = await tx.run({
3979
+ sql: `SELECT * FROM ${quoteIdent(storage.TABLE_DATASET_ITEMS, "table name")} WHERE ${quoteIdent("datasetId", "column name")} = @datasetId AND ${quoteIdent("externalId", "column name")} IN UNNEST(@externalIds) ORDER BY ${quoteIdent("datasetVersion", "column name")}`,
3980
+ params: { datasetId: input.datasetId, externalIds },
3981
+ types: { externalIds: { type: "array", child: "string" } },
3982
+ json: true
3983
+ });
3984
+ historyRows = rows.map(rowToItemRow);
3985
+ }
3986
+ const plan = this.planDatasetItemBatch(input.items, historyRows, crypto.randomUUID);
3987
+ const resolved = new Map(
3988
+ [...plan.existingCurrentItems].map(([id, row]) => [id, this.datasetItemFromRow(row)])
3989
+ );
3990
+ if (plan.inserts.length > 0) {
3991
+ const now = /* @__PURE__ */ new Date();
3992
+ const {
3993
+ version: newVersion,
3994
+ organizationId,
3995
+ projectId
3996
+ } = await this.bumpVersion(tx, input.datasetId, now);
3997
+ for (const insert of plan.inserts) {
3998
+ const item = {
3999
+ id: insert.id,
3954
4000
  datasetId: input.datasetId,
3955
4001
  datasetVersion: newVersion,
4002
+ externalId: insert.item.externalId ?? null,
3956
4003
  organizationId,
3957
4004
  projectId,
3958
- validTo: null,
3959
- isDeleted: false,
3960
- input: item.input,
3961
- groundTruth: item.groundTruth ?? null,
3962
- expectedTrajectory: item.expectedTrajectory ?? null,
3963
- toolMocks: item.toolMocks ?? null,
3964
- requestContext: item.requestContext ?? null,
3965
- metadata: item.metadata ?? null,
3966
- source: item.source ?? null,
4005
+ input: insert.item.input,
4006
+ groundTruth: insert.item.groundTruth,
4007
+ expectedTrajectory: insert.item.expectedTrajectory,
4008
+ toolMocks: insert.item.toolMocks,
4009
+ requestContext: insert.item.requestContext,
4010
+ metadata: insert.item.metadata,
4011
+ source: insert.item.source,
3967
4012
  createdAt: now,
3968
4013
  updatedAt: now
3969
- },
3970
- transaction: tx
3971
- });
4014
+ };
4015
+ await this.db.insert({
4016
+ tableName: storage.TABLE_DATASET_ITEMS,
4017
+ record: { ...item, validTo: null, isDeleted: false },
4018
+ transaction: tx
4019
+ });
4020
+ resolved.set(item.id, item);
4021
+ }
4022
+ await this.insertVersionRow(tx, input.datasetId, newVersion, now);
3972
4023
  }
3973
- await this.insertVersionRow(tx, input.datasetId, newVersion, now);
3974
4024
  await tx.commit();
3975
- result = prepared.map(({ id, item }) => ({
3976
- id,
3977
- datasetId: input.datasetId,
3978
- datasetVersion: newVersion,
3979
- organizationId,
3980
- projectId,
3981
- input: item.input,
3982
- groundTruth: item.groundTruth,
3983
- expectedTrajectory: item.expectedTrajectory,
3984
- toolMocks: item.toolMocks,
3985
- requestContext: item.requestContext,
3986
- metadata: item.metadata,
3987
- source: item.source,
3988
- createdAt: now,
3989
- updatedAt: now
3990
- }));
4025
+ result = plan.resolvedIds.map((id) => resolved.get(id));
3991
4026
  } catch (err) {
3992
4027
  await tx.rollback().catch((rollbackErr) => {
3993
4028
  throw new AggregateError([err, rollbackErr], "Transaction and rollback both failed");
@@ -4031,6 +4066,7 @@ var DatasetsSpanner = class _DatasetsSpanner extends storage.DatasetsStorage {
4031
4066
  id: existing.id,
4032
4067
  datasetId: input.datasetId,
4033
4068
  datasetVersion: newVersion,
4069
+ externalId: existing.externalId ?? null,
4034
4070
  organizationId,
4035
4071
  projectId,
4036
4072
  validTo: null,