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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions