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
- 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!
}
- Run the
gen2-migration flow (assess → lock → generate), keeping the generated migratedAmplifyGen1DynamoDbTableMappings so the Gen 1 tables are reused.
- Deploy the Gen 2 branch (
ampx pipeline-deploy).
- 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.
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. TheCustom::ImportedAmplifyDynamoDBTablesynthesizes with the defaultidHASH key, so the import validation in the table manager rejects it:In our migration (23 models), the 22 models with the default
idkey imported fine; the single model with a custom@primaryKeyfailed every deploy.Reproduction
gen2-migrationflow (assess→lock→generate), keeping the generatedmigratedAmplifyGen1DynamoDbTableMappingsso the Gen 1 tables are reused.ampx pipeline-deploy).ROLLBACK_COMPLETE.Root cause
replaceDdbPrimaryKey()inamplify-graphql-index-transformer/src/resolvers/resolvers.tsgates its CFN-level override onisAmplifyDynamoDbModelDataSourceStrategy(strategy):Imported tables use the
IMPORTED_AMPLIFY_TABLEstrategy (isImportedAmplifyDynamoDbModelDataSourceStrategy), which fails that check, so the code takes the plain-CfnTablebranch whose property assignments never reach the synthesized custom resource — the table keeps the defaultidkey 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
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: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 inROLLBACK_COMPLETE, and orphans retained auth resources on every retry.Environment
@aws-amplify/backend1.23.0@aws-amplify/data-construct1.17.1 (bundled@aws-amplify/graphql-index-transformer3.1.1)gen2-migration)packages/amplify-graphql-index-transformer/src/resolvers/resolvers.tsonmainas of 2026-06-11.