diff --git a/aredis_om/model/model.py b/aredis_om/model/model.py index 5eb22099..61d5d745 100644 --- a/aredis_om/model/model.py +++ b/aredis_om/model/model.py @@ -2552,6 +2552,14 @@ class Config: # Set up PrimaryKeyAccessor descriptor for .pk access setattr(new_class, "pk", PrimaryKeyAccessor()) + # For embedded models, clear the primary_key from meta since they don't + # need primary keys - they're stored as part of their parent document, + # not as separate Redis keys. This fixes GitHub issue #496. + # Note: We keep the pk field in model_fields but the validator will + # return None and model_dump will exclude it. + if getattr(new_class._meta, "embedded", False): + new_class._meta.primary_key = None + if not getattr(new_class._meta, "global_key_prefix", None): new_class._meta.global_key_prefix = getattr( base_meta, "global_key_prefix", "" @@ -2770,6 +2778,9 @@ async def expire( @field_validator("pk", mode="after") def validate_pk(cls, v): + # Skip pk generation for embedded models - they don't need primary keys + if getattr(cls._meta, "embedded", False): + return None if not v or isinstance(v, ExpressionProxy): v = cls._meta.primary_key_creator_cls().create_pk() return v @@ -2778,13 +2789,24 @@ def validate_pk(cls, v): @field_validator("pk") def validate_pk(cls, v): + # Skip pk generation for embedded models - they don't need primary keys + if getattr(cls._meta, "embedded", False): + return None if not v or isinstance(v, ExpressionProxy): v = cls._meta.primary_key_creator_cls().create_pk() return v @classmethod def validate_primary_key(cls): - """Check for a primary key. We need one (and only one).""" + """Check for a primary key. We need one (and only one). + + Embedded models are exempt from this check since they don't need + primary keys - they're stored as part of their parent document. + """ + # Skip validation for embedded models - they don't need primary keys + if getattr(cls._meta, "embedded", False): + return + primary_keys = 0 for name, field in cls.model_fields.items(): if ( @@ -3823,5 +3845,17 @@ def schema_for_type( class EmbeddedJsonModel(JsonModel, abc.ABC): + """ + A model intended to be embedded within a JsonModel. + + EmbeddedJsonModels are stored as part of their parent document, not as + separate Redis keys, so they do not need or generate primary keys. + + The pk field is excluded from serialization by default. + """ + + # Override pk to exclude it from serialization - embedded models don't need pks + pk: Optional[str] = Field(default=None, exclude=True) + class Meta: embedded = True diff --git a/redis_om/__init__.py b/redis_om/__init__.py index 048982b5..a9772f0a 100644 --- a/redis_om/__init__.py +++ b/redis_om/__init__.py @@ -19,6 +19,7 @@ ) from .model.types import Coordinates, GeoFilter + # Backward compatibility alias - deprecated, use SchemaDetector or SchemaMigrator Migrator = SchemaDetector diff --git a/tests/test_json_model.py b/tests/test_json_model.py index 9853782a..a79d777c 100644 --- a/tests/test_json_model.py +++ b/tests/test_json_model.py @@ -892,6 +892,9 @@ async def test_schema(m, key_prefix): # We need to build the key prefix because it will differ based on whether # these tests were copied into the tests_sync folder and unasynce'd. key_prefix = m.Member.make_key(m.Member._meta.primary_key_pattern.format(pk="")) + # Note: EmbeddedJsonModel pk fields are not included in the schema since + # embedded models don't need primary keys (they're stored as part of their + # parent document, not as separate Redis keys). See GitHub issue #496. assert m.Member.redisearch_schema() == ( f"ON JSON PREFIX 1 {key_prefix} SCHEMA " "$.pk AS pk TAG SEPARATOR | " @@ -901,13 +904,9 @@ async def test_schema(m, key_prefix): "$.age AS age NUMERIC " "$.bio AS bio TAG SEPARATOR | " "$.bio AS bio_fts TEXT " - "$.address.pk AS address_pk TAG SEPARATOR | " "$.address.city AS address_city TAG SEPARATOR | " "$.address.postal_code AS address_postal_code TAG SEPARATOR | " - "$.address.note.pk AS address_note_pk TAG SEPARATOR | " "$.address.note.description AS address_note_description TAG SEPARATOR | " - "$.orders[*].pk AS orders_pk TAG SEPARATOR | " - "$.orders[*].items[*].pk AS orders_items_pk TAG SEPARATOR | " "$.orders[*].items[*].name AS orders_items_name TAG SEPARATOR |" )