Skip to content

Gen1→Gen2 migration: @primaryKey ignored for imported DynamoDB tables — deploy fails with "Imported table properties did not match the expected table properties" #3489

Description

@francescoRobin

Description

When migrating a Gen 1 app to Gen 2 with migratedAmplifyGen1DynamoDbTableMappings, any model using a custom @primaryKey (e.g. userId: String! @primaryKey(sortKeyFields: ["typeStartDate"])) fails to deploy. The Custom::ImportedAmplifyDynamoDBTable synthesizes with the default id HASH key, so the import validation in the table manager rejects it:

Imported table properties did not match the expected table properties.
AttributeDefinitions does not match the expected value.
Imported Value: [{"AttributeName":"startDate","AttributeType":"S"},{"AttributeName":"type","AttributeType":"S"},{"AttributeName":"typeStartDate","AttributeType":"S"},{"AttributeName":"userId","AttributeType":"S"}]
Expected: [{"AttributeType":"S","AttributeName":"id"}]
KeySchema does not match the expected value.
Imported Value: [{"AttributeName":"typeStartDate","KeyType":"RANGE"},{"AttributeName":"userId","KeyType":"HASH"}]
Expected: [{"AttributeName":"id","KeyType":"HASH"}]

In our migration (23 models), the 22 models with the default id key imported fine; the single model with a custom @primaryKey failed every deploy.

Reproduction

  1. Gen 1 app with a model using a custom primary key:
type HealthRecord @model {
  userId: String! @primaryKey(sortKeyFields: ["typeStartDate"])
  typeStartDate: String!
  type: String! @index(name: "MetricTypeIndex", sortKeyFields: ["startDate"])
  startDate: AWSDateTime!
}
  1. Run the gen2-migration flow (assesslockgenerate), keeping the generated migratedAmplifyGen1DynamoDbTableMappings so the Gen 1 tables are reused.
  2. Deploy the Gen 2 branch (ampx pipeline-deploy).
  3. The model's nested stack fails with the error above; the root stack ends in ROLLBACK_COMPLETE.

Root cause

replaceDdbPrimaryKey() in amplify-graphql-index-transformer/src/resolvers/resolvers.ts gates its CFN-level override on isAmplifyDynamoDbModelDataSourceStrategy(strategy):

const useAmplifyManagedTableResources = isAmplifyDynamoDbModelDataSourceStrategy(strategy);
const cfnTable = useAmplifyManagedTableResources ? table.node.defaultChild.node.defaultChild : table.table;
...
if (useAmplifyManagedTableResources) {
  cfnTable.addPropertyOverride('keySchema', table.keySchema);
  cfnTable.addPropertyOverride('attributeDefinitions', table.attributeDefinitions);
} else {
  cfnTable.keySchema = table.keySchema;
  cfnTable.attributeDefinitions = table.attributeDefinitions;
}

Imported tables use the IMPORTED_AMPLIFY_TABLE strategy (isImportedAmplifyDynamoDbModelDataSourceStrategy), which fails that check, so the code takes the plain-CfnTable branch whose property assignments never reach the synthesized custom resource — the table keeps the default id key schema. (@index/GSIs survive because the index transformer has separate amplify-table-aware handling, which makes the resulting mismatch error extra confusing: the GSI is correct while the primary key is not.)

Suggested fix

const useAmplifyManagedTableResources =
  isAmplifyDynamoDbModelDataSourceStrategy(strategy) ||
  isImportedAmplifyDynamoDbModelDataSourceStrategy(strategy);

The same gating pattern may be worth auditing elsewhere in the transformer (e.g. secondary-index attribute-definition handling) for the imported strategy.

Workaround

Pin the expected import properties to the live table via an escape hatch in amplify/backend.ts / data resource:

const cfnImportedTable = backend.data.resources.nestedStacks['HealthRecord'].node
  .findChild('HealthRecordTable')
  .node.defaultChild?.node.defaultChild as CfnResource;
cfnImportedTable.addPropertyOverride('keySchema', [
  { attributeName: 'userId', keyType: 'HASH' },
  { attributeName: 'typeStartDate', keyType: 'RANGE' },
]);
cfnImportedTable.addPropertyOverride('attributeDefinitions', [
  { attributeName: 'userId', attributeType: 'S' },
  { attributeName: 'typeStartDate', attributeType: 'S' },
  { attributeName: 'type', attributeType: 'S' },
  { attributeName: 'startDate', attributeType: 'S' },
]);

This is safe because the import path only validates against the live table and never mutates it (validation deep-equals after sorting, so ordering doesn't matter).

Impact

Blocks the Gen1→Gen2 migration for any schema containing a custom @primaryKey — the failure surfaces late (at branch deploy), leaves the root stack in ROLLBACK_COMPLETE, and orphans retained auth resources on every retry.

Environment

  • @aws-amplify/backend 1.23.0
  • @aws-amplify/data-construct 1.17.1 (bundled @aws-amplify/graphql-index-transformer 3.1.1)
  • Amplify CLI 14.5.0 (gen2-migration)
  • Bug still present in packages/amplify-graphql-index-transformer/src/resolvers/resolvers.ts on main as of 2026-06-11.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions