diff --git a/src/providers/postgres/qgspostgresprovider.cpp b/src/providers/postgres/qgspostgresprovider.cpp
index fc2a6b779c08..37427d28a7a4 100644
--- a/src/providers/postgres/qgspostgresprovider.cpp
+++ b/src/providers/postgres/qgspostgresprovider.cpp
@@ -1111,6 +1111,31 @@ bool QgsPostgresProvider::loadFields()
}
mAttrPalIndexName.insert( i, fieldName );
+ // If this is an identity field with constraints and there is no default, let's look for a sequence:
+ // we might have a default value created by a sequence named
__seq
+ if ( ! identityMap[tableoid ][ attnum ].isEmpty()
+ && notNullMap[tableoid][ attnum ]
+ && uniqueMap[tableoid][attnum]
+ && defValMap[tableoid][attnum].isEmpty() )
+ {
+ const QString seqName { mTableName + '_' + fieldName + QStringLiteral( "_seq" ) };
+ const QString seqSql { QStringLiteral( "SELECT c.oid "
+ " FROM pg_class c "
+ " LEFT JOIN pg_namespace n "
+ " ON ( n.oid = c.relnamespace ) "
+ " WHERE c.relkind = 'S' "
+ " AND c.relname = %1 "
+ " AND n.nspname = %2" )
+ .arg( quotedValue( seqName ) )
+ .arg( quotedValue( mSchemaName ) )
+ };
+ QgsPostgresResult seqResult( connectionRO()->PQexec( seqSql ) );
+ if ( seqResult.PQntuples() == 1 )
+ {
+ defValMap[tableoid][attnum] = QStringLiteral( "nextval(%1::regclass)" ).arg( quotedIdentifier( seqName ) );
+ }
+ }
+
mDefaultValues.insert( mAttributeFields.size(), defValMap[tableoid][attnum] );
QgsField newField = QgsField( fieldName, fieldType, fieldTypeName, fieldSize, fieldPrec, fieldComment, fieldSubType );
diff --git a/tests/src/python/test_provider_postgres.py b/tests/src/python/test_provider_postgres.py
index c063a7db9391..3ea154e0fb64 100644
--- a/tests/src/python/test_provider_postgres.py
+++ b/tests/src/python/test_provider_postgres.py
@@ -1420,6 +1420,25 @@ def testFeatureCountEstimatedOnView(self):
self.assertTrue(vl.isValid())
self.assertTrue(self.source.featureCount() > 0)
+ def testIdentityPk(self):
+ """Test a table with identity pk, see GH #29560"""
+
+ vl = QgsVectorLayer(self.dbconn + ' sslmode=disable key=\'gid\' srid=4326 type=POLYGON table="qgis_test"."b29560"(geom) sql=', 'testb29560', 'postgres')
+ self.assertTrue(vl.isValid())
+
+ feature = QgsFeature(vl.fields())
+ geom = QgsGeometry.fromWkt('POLYGON EMPTY')
+ feature.setGeometry(geom)
+ self.assertTrue(vl.dataProvider().addFeature(feature))
+
+ del(vl)
+
+ # Verify
+ vl = QgsVectorLayer(self.dbconn + ' sslmode=disable key=\'gid\' srid=4326 type=POLYGON table="qgis_test"."b29560"(geom) sql=', 'testb29560', 'postgres')
+ self.assertTrue(vl.isValid())
+ feature = next(vl.getFeatures())
+ self.assertIsNotNone(feature.id())
+
class TestPyQgsPostgresProviderCompoundKey(unittest.TestCase, ProviderTestCase):
diff --git a/tests/testdata/provider/testdata_pg.sql b/tests/testdata/provider/testdata_pg.sql
index 755f546d5601..96c3efe023ae 100644
--- a/tests/testdata/provider/testdata_pg.sql
+++ b/tests/testdata/provider/testdata_pg.sql
@@ -671,3 +671,18 @@ CREATE VIEW qgis_test.b31799_test_view_ctid AS (SELECT ctid, geom, random() FROM
CREATE VIEW qgis_test.b32523 AS
SELECT pk, random()
FROM qgis_test.some_poly_data;
+
+----------------------------------------------
+--
+-- IDENTITY pk
+-- See https://github.com/qgis/QGIS/issues/29560
+--
+
+CREATE TABLE qgis_test.b29560 (
+ gid int8 NOT NULL GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ geom geometry(polygon)
+);
+
+INSERT INTO qgis_test.b29560 (geom)
+VALUES ('POLYGON EMPTY'::geometry);
+