diff --git a/docs/models.md b/docs/models.md index b74ac02..82c26b4 100644 --- a/docs/models.md +++ b/docs/models.md @@ -29,14 +29,15 @@ Optionally: All optional parameters can be omitted if not required. -Each schema class must inherit from `BaseSchema` and must be registered using `register_schema()`. An example of a schema to store road bikes is given below. +Each schema class must inherit from `BaseSchema` and must be registered using the `@polynom_schema` decorator. An example of a schema to store road bikes is given below. ```python -from polynom.schema.schema_registry import register_schema +from polynom.schema.schema_registry import polynom_schema from polynom.schema.field import Field from polynom.schema.polytypes import VarChar, Decimal from polynom.schema.schema import BaseSchema +@polynom_schema class BikeSchema(BaseSchema): namespace_name = 'vehicles' # optional entity_name = 'Bike' @@ -49,8 +50,6 @@ class BikeSchema(BaseSchema): Field('price', Decimal()) ] -# Make schemas discoverable by PolyNOM -register_schema(BikeSchema) ``` It can be observed that no primary key is defined in this schema. This is permitted as PolyNOM automatically creates a entry identifier of very high probabilistic uniqueness for each inserted entry. Thereby the probability to find a duplicate within 103 trillion entries is one in a billion. This identifier field is not listed in the schema but is accessible on the corresponding model class. The use of these internal identifiers as an alternative to autoincrement or manualy managed id fields is strongly encouraged. @@ -64,12 +63,16 @@ A model represents a single entry for a given schema. Each model must: Further each model offers the field `_entry_id` which returns a unique identifier for the given entry. These identifiers are handled by PolyNOM and should not be modified or set manually. +In contrast to schemas, registering models is optional. Models can be registered using the `@polynom_model` decorator. This allows them to be resolved by relationships using their fully qualified name string. + The following model matches our `BikeSchema` from step one. ```python -from polynom.model import BaseModel +from polynom.model.model import BaseModel +from polynom.model.model_registry import polynom_model from myproject.bike.model import Bike +@polynom_model class Bike(BaseModel): schema = BikeSchema() diff --git a/docs/relationships.md b/docs/relationships.md index 5ab80b4..2fd920e 100644 --- a/docs/relationships.md +++ b/docs/relationships.md @@ -21,11 +21,12 @@ Schema-level references define how entities relate at the database level. These First, a simple schema for the `Owner` id created. No references are defined here yet. ```python -from polynom.schema.schema_registry import register_schema +from polynom.schema.schema_registry import polynom_schema from polynom.schema.field import Field from polynom.schema.polytypes import VarChar from polynom.schema.schema import BaseSchema +@polynom_schema class OwnerSchema(BaseSchema): namespace_name = 'users' entity_name = 'Owner' @@ -34,8 +35,6 @@ class OwnerSchema(BaseSchema): Field('name', VarChar(100), nullable=False), Field('email', VarChar(100), nullable=False, unique=True), ] - -register_schema(OwnerSchema) ``` #### Step 1b: Define a Schema for the `Bike` @@ -45,6 +44,7 @@ This step creates the schema for the `Bike`. We use a `ForeignKeyField` to creat ```python from polynom.schema.field import ForeignKeyField +@polynom_schema class BikeSchema(BaseSchema): namespace_name = 'vehicles' entity_name = 'Bike' @@ -58,8 +58,6 @@ class BikeSchema(BaseSchema): ForeignKeyField('owner_id', referenced_schema=OwnerSchema), # example for specific field: ForeignKeyField('owner_id', referenced_schema=OwnerSchema, referenced_db_field_name='name'), ] - -register_schema(BikeSchema) ``` ### Object Relationships @@ -135,4 +133,42 @@ class TeamCyclistAssocSchema(BaseSchema): ] ``` -Note that PolyNOM does not automatically create assocoation entities for many-to-many relationships. \ No newline at end of file +## Circular Dependencies + +Consider a bidirectional relationship between two models, `Author` and `Book`. Model `Author` must define a relationship to `Book`, and `Book` must define a relationship back to `Author`. However, at the time `Author` is being defined, the `Book` class might not yet be declared. This results in a circular dependency issue if direct class references are used. +To circumvent this limitation, model classes can also be specified using their **fully qualified name** (FQN) as a string, instead of a direct class reference. The FQN follows Python's standard module path syntax, such as: + +``` +"myapp.models.author.Author" +``` + +All models intended to be used in this way must be registered using the `@polynom_model` decorator. The model can then be resolved lazily at runtime, avoiding import-time circular dependency errors. + +### Example + +```python +# myapp/models/author.py + +from polynom.model import BaseModel, Relationship, polynom_model + +@polynom_model +class Author(BaseModel): + books = Relationship( + target_model="myapp.models.book.Book", # FQN string reference + back_populates="author" + ) +``` + +```python +# myapp/models/book.py + +from polynom.model import BaseModel, Relationship, polynom_model +from myapp.models.author import Author # optional, needed only if directly referencing Author + +@polynom_model +class Book(BaseModel): + author = Relationship( + target_model=Author, + back_populates="books" + ) +``` diff --git a/polynom/dump.py b/polynom/dump.py index f0bd405..fed7ea5 100644 --- a/polynom/dump.py +++ b/polynom/dump.py @@ -5,7 +5,7 @@ from polynom.session import Session from polynom.schema.schema_registry import _get_ordered_schemas, _to_dict from polynom.statement import _SqlGenerator, Statement, get_generator_for_data_model -from polynom.model import FlexModel +from polynom.model.model import FlexModel from polynom.reflection import ChangeLog logger = logging.getLogger(__name__) diff --git a/polynom/model.py b/polynom/model/model.py similarity index 100% rename from polynom/model.py rename to polynom/model/model.py diff --git a/polynom/model/model_registry.py b/polynom/model/model_registry.py new file mode 100644 index 0000000..9760ca9 --- /dev/null +++ b/polynom/model/model_registry.py @@ -0,0 +1,26 @@ + +import logging +from polynom.model.model import BaseModel + +logger = logging.getLogger(__name__) + +_registered_models_by_fqname = {} + +def polynom_model(cls): + if not issubclass(cls, BaseModel): + raise TypeError(f"@polynom_model can only be applied to subclasses of BaseModel, got {cls}") + + fq_name = f"{cls.__module__}.{cls.__name__}" + if fq_name in _registered_models_by_fqname: + logger.warning(f"Model '{fq_name}' is already registered. Overwriting the existing entry.") + else: + logger.debug(f"Registering model '{fq_name}'") + + _registered_models_by_fqname[fq_name] = cls + return cls + +def _get_model_by_fqname(fq_name: str) -> type[BaseModel] | None: + return _registered_models_by_fqname.get(fq_name) + +def _get_registered_models() -> dict[str, type[BaseModel]]: + return dict(_registered_models_by_fqname) diff --git a/polynom/schema/relationship.py b/polynom/model/relationship.py similarity index 64% rename from polynom/schema/relationship.py rename to polynom/model/relationship.py index 32737ee..03b3e99 100644 --- a/polynom/schema/relationship.py +++ b/polynom/model/relationship.py @@ -1,6 +1,10 @@ +from polynom.model.model_registry import _get_model_by_fqname + class Relationship: - def __init__(self, target_model: type["BaseModel"] = None, back_populates: str = None, cascade: str = None): - self.target_model = target_model + def __init__(self, target_model: type["BaseModel"] | str, back_populates: str = None, cascade: str = None): + if target_model is None: + raise ValueError("target_model must be specified for Relationship") + self._target_model = target_model self._back_populates = back_populates self._internal_name = None self._cascade = cascade or "" @@ -12,9 +16,21 @@ def __set_name__(self, owner, name): self._key = name self._owner_class = owner + @property + def target_model(self): + # Resolve string to actual model class once + if isinstance(self._target_model, str): + model_cls = _get_model_by_fqname(self._target_model) + if model_cls is None: + raise ValueError(f"Model '{self._target_model}' not found in registry for relationship '{self._key}'") + self._target_model = model_cls + return self._target_model + def __set__(self, instance, value): - if value and not isinstance(value, self.target_model): - raise TypeError(f'Value must be of {type[self.target_model]} but is of {type[value]}') + tm = self.target_model # resolve first + + if value and not isinstance(value, tm): + raise TypeError(f'Value must be of {tm} but is of {type(value)}') old_value = getattr(instance, self._internal_name, None) if old_value is value: return @@ -38,8 +54,8 @@ def __set__(self, instance, value): if value and self._back_populates: if not hasattr(value, self._back_populates): raise AttributeError( - f"Backref attribute '{self._back_populates}' not found on {value.__class__.__name__}" - ) + f"Backref attribute '{self._back_populates}' not found on {value.__class__.__name__}" + ) current_back = getattr(value, self._back_populates, None) if isinstance(current_back, list): if instance not in current_back: @@ -51,21 +67,7 @@ def __set__(self, instance, value): if hasattr(instance, "_session") and instance._session: instance._session.add(value) - def get_target_model(self): - if not self._back_populates or not self._owner_class: - raise ValueError("Cannot infer target_model without 'back_populates' and 'owner_class' set") - - from polynom.model import BaseModel - for model_cls in BaseModel.__subclasses__(): - for attr_name, attr in vars(model_cls).items(): - if isinstance(attr, Relationship): - if attr._back_populates == self._key: - return model_cls - - raise ValueError(f"Could not infer target_model for relationship '{self._key}'") - def __get__(self, instance, owner): if instance is None: return self return getattr(instance, self._internal_name, None) - diff --git a/polynom/query.py b/polynom/query.py index 5a64d9c..7a11e4f 100644 --- a/polynom/query.py +++ b/polynom/query.py @@ -7,7 +7,7 @@ if TYPE_CHECKING: from polynom.session import Session - from polynom.model import BaseModel + from polynom.model.model import BaseModel logger = logging.getLogger(__name__) diff --git a/polynom/reflection.py b/polynom/reflection.py index 6a783fc..151246c 100644 --- a/polynom/reflection.py +++ b/polynom/reflection.py @@ -1,10 +1,12 @@ -from polynom.model import BaseModel -from polynom.schema.schema_registry import register_schema +from polynom.model.model import BaseModel +from polynom.schema.schema_registry import polynom_schema +from polynom.model.model_registry import polynom_model from polynom.schema.schema import BaseSchema from polynom.schema.field import Field from polynom.schema.polytypes import Timestamp, Text, Json import polynom.config as cfg +@polynom_schema class ChangeLogSchema(BaseSchema): entity_name = cfg.get(cfg.CHANGE_LOG_TABLE) namespace_name = cfg.get(cfg.INTERNAL_NAMESPACE) @@ -18,6 +20,7 @@ class ChangeLogSchema(BaseSchema): Field('changes', Json(), nullable=False), ] +@polynom_model class ChangeLog(BaseModel): schema = ChangeLogSchema() @@ -31,6 +34,7 @@ def __init__(self, app_uuid: str, modified_entry_id: str, modified_entity_namesp self.date_of_change = date_of_change self.changes = changes +@polynom_schema class SchemaSnapshotSchema(BaseSchema): entity_name = cfg.get(cfg.SNAPSHOT_TABLE) namespace_name = cfg.get(cfg.INTERNAL_NAMESPACE) @@ -38,12 +42,10 @@ class SchemaSnapshotSchema(BaseSchema): Field('snapshot', Json(), nullable=False), ] +@polynom_model class SchemaSnapshot(BaseModel): schema = SchemaSnapshotSchema() def __init__(self, snapshot: dict, _entry_id = None): super().__init__(_entry_id) self.snapshot = snapshot - -register_schema(ChangeLogSchema) -register_schema(SchemaSnapshotSchema) \ No newline at end of file diff --git a/polynom/schema/schema_registry.py b/polynom/schema/schema_registry.py index 57640c4..5e3442a 100644 --- a/polynom/schema/schema_registry.py +++ b/polynom/schema/schema_registry.py @@ -8,12 +8,10 @@ _registered_schemas = set() _sorted_schemas = None -def register_schema(schema): - logger.debug(f'Schema registered: {str(schema)}') - _registered_schemas.add(schema) - -def _get_registered_schemas(): - return _registered_schemas +def polynom_schema(cls): + logger.debug(f'Schema registered: {str(cls)}') + _registered_schemas.add(cls) + return cls def _get_ordered_schemas(): global _sorted_schemas diff --git a/polynom/session.py b/polynom/session.py index 1dbb8e1..1a53514 100644 --- a/polynom/session.py +++ b/polynom/session.py @@ -7,9 +7,9 @@ from datetime import datetime from enum import Enum, auto from dataclasses import dataclass, field -from polynom.model import BaseModel +from polynom.model.model import BaseModel from polynom.reflection import ChangeLog -from polynom.schema.relationship import Relationship +from polynom.model.relationship import Relationship from polynom.statement import _SqlGenerator, Statement logger = logging.getLogger(__name__) diff --git a/test_dump.sql b/test_dump.sql index 5a302c4..468930e 100644 --- a/test_dump.sql +++ b/test_dump.sql @@ -1,28 +1,28 @@ /* @format_version: 1 @app_uuid: a8817239-9bae-4961-a619-1e9ef5575eff -@snapshot: {"version": "20250804T091845", "schemas": [{"entity_name": "snapshot", "namespace_name": "polynom_internal", "data_model": "RELATIONAL", "fields": [{"name": "_entry_id", "db_name": "_entry_id", "type": "VarChar", "previous_name": null, "nullable": false, "unique": true, "default": null, "is_primary_key": true, "is_foreign_key": false}, {"name": "snapshot", "db_name": "snapshot", "type": "Json", "previous_name": null, "nullable": false, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}]}, {"entity_name": "User", "namespace_name": "polynom_entities", "data_model": "RELATIONAL", "fields": [{"name": "_entry_id", "db_name": "_entry_id", "type": "VarChar", "previous_name": null, "nullable": false, "unique": true, "default": null, "is_primary_key": true, "is_foreign_key": false}, {"name": "username", "db_name": "username", "type": "VarChar", "previous_name": "username2", "nullable": false, "unique": true, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "email", "db_name": "email", "type": "VarChar", "previous_name": null, "nullable": false, "unique": true, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "first_name", "db_name": "first_name", "type": "VarChar", "previous_name": null, "nullable": true, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "last_name", "db_name": "last_name", "type": "VarChar", "previous_name": null, "nullable": true, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "active", "db_name": "active", "type": "Boolean", "previous_name": null, "nullable": true, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "is_admin", "db_name": "is_admin", "type": "Boolean", "previous_name": null, "nullable": true, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}]}, {"entity_name": "change_log", "namespace_name": "polynom_internal", "data_model": "RELATIONAL", "fields": [{"name": "_entry_id", "db_name": "_entry_id", "type": "VarChar", "previous_name": null, "nullable": false, "unique": true, "default": null, "is_primary_key": true, "is_foreign_key": false}, {"name": "app_uuid", "db_name": "app_uuid", "type": "Text", "previous_name": null, "nullable": false, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "modified_entry_id", "db_name": "modified_entry_id", "type": "Text", "previous_name": null, "nullable": false, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "modified_entity_namespace", "db_name": "modified_entity_namespace", "type": "Text", "previous_name": null, "nullable": false, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "modified_entity_name", "db_name": "modified_entity_name", "type": "Text", "previous_name": null, "nullable": false, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "modified_by", "db_name": "modified_by", "type": "Text", "previous_name": null, "nullable": false, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "date_of_change", "db_name": "date_of_change", "type": "Timestamp", "previous_name": null, "nullable": false, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "changes", "db_name": "changes", "type": "Json", "previous_name": null, "nullable": false, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}]}, {"entity_name": "Bike", "namespace_name": "polynom_entities", "data_model": "RELATIONAL", "fields": [{"name": "_entry_id", "db_name": "_entry_id", "type": "VarChar", "previous_name": null, "nullable": false, "unique": true, "default": null, "is_primary_key": true, "is_foreign_key": false}, {"name": "brand", "db_name": "brand", "type": "VarChar", "previous_name": null, "nullable": false, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "model", "db_name": "model", "type": "VarChar", "previous_name": null, "nullable": false, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "owner_id", "db_name": "owner_id", "type": "VarChar", "previous_name": null, "nullable": false, "unique": null, "default": false, "is_primary_key": false, "is_foreign_key": true, "references_namespace": "polynom_entities", "references_entity": "User", "references_field": "_entry_id"}]}]} +@snapshot: {"version": "20250805T113739", "schemas": [{"entity_name": "User", "namespace_name": "polynom_entities", "data_model": "RELATIONAL", "fields": [{"name": "_entry_id", "db_name": "_entry_id", "type": "VarChar", "previous_name": null, "nullable": false, "unique": true, "default": null, "is_primary_key": true, "is_foreign_key": false}, {"name": "username", "db_name": "username", "type": "VarChar", "previous_name": "username2", "nullable": false, "unique": true, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "email", "db_name": "email", "type": "VarChar", "previous_name": null, "nullable": false, "unique": true, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "first_name", "db_name": "first_name", "type": "VarChar", "previous_name": null, "nullable": true, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "last_name", "db_name": "last_name", "type": "VarChar", "previous_name": null, "nullable": true, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "active", "db_name": "active", "type": "Boolean", "previous_name": null, "nullable": true, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "is_admin", "db_name": "is_admin", "type": "Boolean", "previous_name": null, "nullable": true, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}]}, {"entity_name": "change_log", "namespace_name": "polynom_internal", "data_model": "RELATIONAL", "fields": [{"name": "_entry_id", "db_name": "_entry_id", "type": "VarChar", "previous_name": null, "nullable": false, "unique": true, "default": null, "is_primary_key": true, "is_foreign_key": false}, {"name": "app_uuid", "db_name": "app_uuid", "type": "Text", "previous_name": null, "nullable": false, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "modified_entry_id", "db_name": "modified_entry_id", "type": "Text", "previous_name": null, "nullable": false, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "modified_entity_namespace", "db_name": "modified_entity_namespace", "type": "Text", "previous_name": null, "nullable": false, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "modified_entity_name", "db_name": "modified_entity_name", "type": "Text", "previous_name": null, "nullable": false, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "modified_by", "db_name": "modified_by", "type": "Text", "previous_name": null, "nullable": false, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "date_of_change", "db_name": "date_of_change", "type": "Timestamp", "previous_name": null, "nullable": false, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "changes", "db_name": "changes", "type": "Json", "previous_name": null, "nullable": false, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}]}, {"entity_name": "snapshot", "namespace_name": "polynom_internal", "data_model": "RELATIONAL", "fields": [{"name": "_entry_id", "db_name": "_entry_id", "type": "VarChar", "previous_name": null, "nullable": false, "unique": true, "default": null, "is_primary_key": true, "is_foreign_key": false}, {"name": "snapshot", "db_name": "snapshot", "type": "Json", "previous_name": null, "nullable": false, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}]}, {"entity_name": "Bike", "namespace_name": "polynom_entities", "data_model": "RELATIONAL", "fields": [{"name": "_entry_id", "db_name": "_entry_id", "type": "VarChar", "previous_name": null, "nullable": false, "unique": true, "default": null, "is_primary_key": true, "is_foreign_key": false}, {"name": "brand", "db_name": "brand", "type": "VarChar", "previous_name": null, "nullable": false, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "model", "db_name": "model", "type": "VarChar", "previous_name": null, "nullable": false, "unique": false, "default": null, "is_primary_key": false, "is_foreign_key": false}, {"name": "owner_id", "db_name": "owner_id", "type": "VarChar", "previous_name": null, "nullable": false, "unique": null, "default": false, "is_primary_key": false, "is_foreign_key": true, "references_namespace": "polynom_entities", "references_entity": "User", "references_field": "_entry_id"}]}]} */ -/*sql@None*/ CREATE RELATIONAL NAMESPACE IF NOT EXISTS "polynom_internal" /*sql@None*/ CREATE RELATIONAL NAMESPACE IF NOT EXISTS "polynom_entities" /*sql@polynom_entities*/ CREATE TABLE IF NOT EXISTS "polynom_entities"."User" ("_entry_id" VARCHAR(36) NOT NULL, "username" VARCHAR(80) NOT NULL, "email" VARCHAR(80) NOT NULL, "first_name" VARCHAR(30), "last_name" VARCHAR(30), "active" BOOLEAN, "is_admin" BOOLEAN, PRIMARY KEY (_entry_id), UNIQUE ("_entry_id"), UNIQUE ("username"), UNIQUE ("email")); -/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."User" (_entry_id, username, email, first_name, last_name, active, is_admin) VALUES ('ec0a1e28-9c90-4204-b0a7-733a69f24ce3', 'testuser', 'u1@demo.ch', 'max', 'muster', TRUE, FALSE) -/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."User" (_entry_id, username, email, first_name, last_name, active, is_admin) VALUES ('7635799d-9a3d-4258-9979-88fd592b0266', 'testuser2', 'u2@demo.ch', 'mira', 'muster', FALSE, TRUE) -/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."User" (_entry_id, username, email, first_name, last_name, active, is_admin) VALUES ('d93d9ec7-3a49-49c5-a09e-2ddc01f108d5', 'testuser3', 'u3@demo.ch', 'miraculix', 'musterin', FALSE, TRUE) -/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."User" (_entry_id, username, email, first_name, last_name, active, is_admin) VALUES ('09c9b9df-0c7b-4221-9bf6-07be51e610c8', 'testuser4', 'u4@demo.ch', 'maxine', 'meier', TRUE, FALSE) -/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."User" (_entry_id, username, email, first_name, last_name, active, is_admin) VALUES ('1c1e1d00-26bd-430a-9161-e4b02f3d96f9', 'testuser5', 'u5@demo.ch', 'mia', 'müller', FALSE, FALSE) +/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."User" (_entry_id, username, email, first_name, last_name, active, is_admin) VALUES ('5b6a8ad3-e85c-4266-86e1-079a1396d92d', 'testuser', 'u1@demo.ch', 'max', 'muster', TRUE, FALSE) +/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."User" (_entry_id, username, email, first_name, last_name, active, is_admin) VALUES ('15972b62-8957-4898-b849-e86d3aa31a1d', 'testuser2', 'u2@demo.ch', 'mira', 'muster', FALSE, TRUE) +/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."User" (_entry_id, username, email, first_name, last_name, active, is_admin) VALUES ('c29bb729-27ae-4062-bd2c-9de25e25e8e8', 'testuser3', 'u3@demo.ch', 'miraculix', 'musterin', FALSE, TRUE) +/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."User" (_entry_id, username, email, first_name, last_name, active, is_admin) VALUES ('17f96a97-44b0-488a-8149-415f24f4f774', 'testuser4', 'u4@demo.ch', 'maxine', 'meier', TRUE, FALSE) +/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."User" (_entry_id, username, email, first_name, last_name, active, is_admin) VALUES ('48e3da0f-df4b-44df-8500-a42936e24128', 'testuser5', 'u5@demo.ch', 'mia', 'müller', FALSE, FALSE) +/*sql@None*/ CREATE RELATIONAL NAMESPACE IF NOT EXISTS "polynom_internal" /*sql@polynom_internal*/ CREATE TABLE IF NOT EXISTS "polynom_internal"."change_log" ("_entry_id" VARCHAR(36) NOT NULL, "app_uuid" TEXT NOT NULL, "modified_entry_id" TEXT NOT NULL, "modified_entity_namespace" TEXT NOT NULL, "modified_entity_name" TEXT NOT NULL, "modified_by" TEXT NOT NULL, "date_of_change" TIMESTAMP(0) NOT NULL, "changes" TEXT NOT NULL, PRIMARY KEY (_entry_id), UNIQUE ("_entry_id")); -/*sql@polynom_internal*/ INSERT INTO "polynom_internal"."change_log" (_entry_id, app_uuid, modified_entry_id, modified_entity_namespace, modified_entity_name, modified_by, date_of_change, changes) VALUES ('0d97c6b5-67ce-44f6-bfb0-1717d6d09447', 'a8817239-9bae-4961-a619-1e9ef5575eff', 'ec0a1e28-9c90-4204-b0a7-733a69f24ce3', 'polynom_entities', 'User', 'test', 2025-08-04 07:18:45+00:00, '{"username": [null, "testuser"], "email": [null, "u1@demo.ch"], "first_name": [null, "max"], "last_name": [null, "muster"], "active": [null, true], "is_admin": [null, false]}') -/*sql@polynom_internal*/ INSERT INTO "polynom_internal"."change_log" (_entry_id, app_uuid, modified_entry_id, modified_entity_namespace, modified_entity_name, modified_by, date_of_change, changes) VALUES ('26b0ca9f-88bf-4a04-b183-e178ad5da4f5', 'a8817239-9bae-4961-a619-1e9ef5575eff', '7635799d-9a3d-4258-9979-88fd592b0266', 'polynom_entities', 'User', 'test', 2025-08-04 07:18:45+00:00, '{"username": [null, "testuser2"], "email": [null, "u2@demo.ch"], "first_name": [null, "mira"], "last_name": [null, "muster"], "active": [null, false], "is_admin": [null, true]}') -/*sql@polynom_internal*/ INSERT INTO "polynom_internal"."change_log" (_entry_id, app_uuid, modified_entry_id, modified_entity_namespace, modified_entity_name, modified_by, date_of_change, changes) VALUES ('288033a3-0a5e-4677-a5c2-27a494f4b93b', 'a8817239-9bae-4961-a619-1e9ef5575eff', 'd93d9ec7-3a49-49c5-a09e-2ddc01f108d5', 'polynom_entities', 'User', 'test', 2025-08-04 07:18:45+00:00, '{"username": [null, "testuser3"], "email": [null, "u3@demo.ch"], "first_name": [null, "miraculix"], "last_name": [null, "musterin"], "active": [null, false], "is_admin": [null, true]}') -/*sql@polynom_internal*/ INSERT INTO "polynom_internal"."change_log" (_entry_id, app_uuid, modified_entry_id, modified_entity_namespace, modified_entity_name, modified_by, date_of_change, changes) VALUES ('935a7f9e-4e48-4f1b-9711-7e0998bbb5df', 'a8817239-9bae-4961-a619-1e9ef5575eff', '09c9b9df-0c7b-4221-9bf6-07be51e610c8', 'polynom_entities', 'User', 'test', 2025-08-04 07:18:45+00:00, '{"username": [null, "testuser4"], "email": [null, "u4@demo.ch"], "first_name": [null, "maxine"], "last_name": [null, "meier"], "active": [null, true], "is_admin": [null, false]}') -/*sql@polynom_internal*/ INSERT INTO "polynom_internal"."change_log" (_entry_id, app_uuid, modified_entry_id, modified_entity_namespace, modified_entity_name, modified_by, date_of_change, changes) VALUES ('c430bf7e-b776-4f69-848d-4dc0c2de3d37', 'a8817239-9bae-4961-a619-1e9ef5575eff', '1c1e1d00-26bd-430a-9161-e4b02f3d96f9', 'polynom_entities', 'User', 'test', 2025-08-04 07:18:45+00:00, '{"username": [null, "testuser5"], "email": [null, "u5@demo.ch"], "first_name": [null, "mia"], "last_name": [null, "m\u00fcller"], "active": [null, false], "is_admin": [null, false]}') -/*sql@polynom_internal*/ INSERT INTO "polynom_internal"."change_log" (_entry_id, app_uuid, modified_entry_id, modified_entity_namespace, modified_entity_name, modified_by, date_of_change, changes) VALUES ('edece317-4bf9-469c-9091-95cf77354cce', 'a8817239-9bae-4961-a619-1e9ef5575eff', '7c2de745-2447-437d-885c-9b0b3364af3e', 'polynom_entities', 'Bike', 'test', 2025-08-04 07:18:45+00:00, '{"brand": [null, "Trek"], "model": [null, "Marlin 7"], "owner_id": [null, "ec0a1e28-9c90-4204-b0a7-733a69f24ce3"]}') -/*sql@polynom_internal*/ INSERT INTO "polynom_internal"."change_log" (_entry_id, app_uuid, modified_entry_id, modified_entity_namespace, modified_entity_name, modified_by, date_of_change, changes) VALUES ('65afbe27-d102-413b-a52c-fc2d736c08ef', 'a8817239-9bae-4961-a619-1e9ef5575eff', 'ab239e80-8d61-4262-a0ed-2c5f24db5112', 'polynom_entities', 'Bike', 'test', 2025-08-04 07:18:45+00:00, '{"brand": [null, "Specialized"], "model": [null, "Rockhopper"], "owner_id": [null, "ec0a1e28-9c90-4204-b0a7-733a69f24ce3"]}') -/*sql@polynom_internal*/ INSERT INTO "polynom_internal"."change_log" (_entry_id, app_uuid, modified_entry_id, modified_entity_namespace, modified_entity_name, modified_by, date_of_change, changes) VALUES ('28321993-066f-4c21-a9c5-fd97e7d05b79', 'a8817239-9bae-4961-a619-1e9ef5575eff', '7f0910c9-5943-4cbc-a89b-45abcc9bc509', 'polynom_entities', 'Bike', 'test', 2025-08-04 07:18:45+00:00, '{"brand": [null, "Cannondale"], "model": [null, "Trail 8"], "owner_id": [null, "d93d9ec7-3a49-49c5-a09e-2ddc01f108d5"]}') -/*sql@polynom_internal*/ INSERT INTO "polynom_internal"."change_log" (_entry_id, app_uuid, modified_entry_id, modified_entity_namespace, modified_entity_name, modified_by, date_of_change, changes) VALUES ('10371660-c400-49c5-be03-dd68ec884ca6', 'a8817239-9bae-4961-a619-1e9ef5575eff', 'ebcf6cff-ce3c-494c-8bdb-8b9fbe84cc31', 'polynom_entities', 'Bike', 'test', 2025-08-04 07:18:45+00:00, '{"brand": [null, "Giant"], "model": [null, "Talon 3"], "owner_id": [null, "09c9b9df-0c7b-4221-9bf6-07be51e610c8"]}') +/*sql@polynom_internal*/ INSERT INTO "polynom_internal"."change_log" (_entry_id, app_uuid, modified_entry_id, modified_entity_namespace, modified_entity_name, modified_by, date_of_change, changes) VALUES ('06872a5e-11d8-406c-93d5-36d7828028d5', 'a8817239-9bae-4961-a619-1e9ef5575eff', '5b6a8ad3-e85c-4266-86e1-079a1396d92d', 'polynom_entities', 'User', 'test', 2025-08-05 09:37:38+00:00, '{"username": [null, "testuser"], "email": [null, "u1@demo.ch"], "first_name": [null, "max"], "last_name": [null, "muster"], "active": [null, true], "is_admin": [null, false]}') +/*sql@polynom_internal*/ INSERT INTO "polynom_internal"."change_log" (_entry_id, app_uuid, modified_entry_id, modified_entity_namespace, modified_entity_name, modified_by, date_of_change, changes) VALUES ('74fb1843-3491-4920-a3bb-186154200b45', 'a8817239-9bae-4961-a619-1e9ef5575eff', '15972b62-8957-4898-b849-e86d3aa31a1d', 'polynom_entities', 'User', 'test', 2025-08-05 09:37:38+00:00, '{"username": [null, "testuser2"], "email": [null, "u2@demo.ch"], "first_name": [null, "mira"], "last_name": [null, "muster"], "active": [null, false], "is_admin": [null, true]}') +/*sql@polynom_internal*/ INSERT INTO "polynom_internal"."change_log" (_entry_id, app_uuid, modified_entry_id, modified_entity_namespace, modified_entity_name, modified_by, date_of_change, changes) VALUES ('42343e95-9d0d-4c1f-a14e-74165364d990', 'a8817239-9bae-4961-a619-1e9ef5575eff', 'c29bb729-27ae-4062-bd2c-9de25e25e8e8', 'polynom_entities', 'User', 'test', 2025-08-05 09:37:38+00:00, '{"username": [null, "testuser3"], "email": [null, "u3@demo.ch"], "first_name": [null, "miraculix"], "last_name": [null, "musterin"], "active": [null, false], "is_admin": [null, true]}') +/*sql@polynom_internal*/ INSERT INTO "polynom_internal"."change_log" (_entry_id, app_uuid, modified_entry_id, modified_entity_namespace, modified_entity_name, modified_by, date_of_change, changes) VALUES ('1100f7e2-129e-4288-a449-cb4596a130be', 'a8817239-9bae-4961-a619-1e9ef5575eff', '17f96a97-44b0-488a-8149-415f24f4f774', 'polynom_entities', 'User', 'test', 2025-08-05 09:37:38+00:00, '{"username": [null, "testuser4"], "email": [null, "u4@demo.ch"], "first_name": [null, "maxine"], "last_name": [null, "meier"], "active": [null, true], "is_admin": [null, false]}') +/*sql@polynom_internal*/ INSERT INTO "polynom_internal"."change_log" (_entry_id, app_uuid, modified_entry_id, modified_entity_namespace, modified_entity_name, modified_by, date_of_change, changes) VALUES ('b3dcf48f-ac0c-4411-be7e-9be84d4a80b8', 'a8817239-9bae-4961-a619-1e9ef5575eff', '48e3da0f-df4b-44df-8500-a42936e24128', 'polynom_entities', 'User', 'test', 2025-08-05 09:37:38+00:00, '{"username": [null, "testuser5"], "email": [null, "u5@demo.ch"], "first_name": [null, "mia"], "last_name": [null, "m\u00fcller"], "active": [null, false], "is_admin": [null, false]}') +/*sql@polynom_internal*/ INSERT INTO "polynom_internal"."change_log" (_entry_id, app_uuid, modified_entry_id, modified_entity_namespace, modified_entity_name, modified_by, date_of_change, changes) VALUES ('d0483f76-7fe0-496e-aa7f-9df0c3248267', 'a8817239-9bae-4961-a619-1e9ef5575eff', 'bb551925-1a78-44cf-b8c0-a48be0f66c3c', 'polynom_entities', 'Bike', 'test', 2025-08-05 09:37:39+00:00, '{"brand": [null, "Trek"], "model": [null, "Marlin 7"], "owner_id": [null, "5b6a8ad3-e85c-4266-86e1-079a1396d92d"]}') +/*sql@polynom_internal*/ INSERT INTO "polynom_internal"."change_log" (_entry_id, app_uuid, modified_entry_id, modified_entity_namespace, modified_entity_name, modified_by, date_of_change, changes) VALUES ('4d87ea6e-93a7-414d-97d8-533e256420ad', 'a8817239-9bae-4961-a619-1e9ef5575eff', 'fe99dd37-54d9-41aa-9513-c86e68605881', 'polynom_entities', 'Bike', 'test', 2025-08-05 09:37:39+00:00, '{"brand": [null, "Specialized"], "model": [null, "Rockhopper"], "owner_id": [null, "5b6a8ad3-e85c-4266-86e1-079a1396d92d"]}') +/*sql@polynom_internal*/ INSERT INTO "polynom_internal"."change_log" (_entry_id, app_uuid, modified_entry_id, modified_entity_namespace, modified_entity_name, modified_by, date_of_change, changes) VALUES ('b8892c96-4a6c-4297-9655-5dea04a342c6', 'a8817239-9bae-4961-a619-1e9ef5575eff', 'a4c96c97-490a-4cc5-be38-373b1d47cad2', 'polynom_entities', 'Bike', 'test', 2025-08-05 09:37:39+00:00, '{"brand": [null, "Cannondale"], "model": [null, "Trail 8"], "owner_id": [null, "c29bb729-27ae-4062-bd2c-9de25e25e8e8"]}') +/*sql@polynom_internal*/ INSERT INTO "polynom_internal"."change_log" (_entry_id, app_uuid, modified_entry_id, modified_entity_namespace, modified_entity_name, modified_by, date_of_change, changes) VALUES ('6c66795e-25fd-474b-80e4-53488bdf20cd', 'a8817239-9bae-4961-a619-1e9ef5575eff', '035a88b9-3852-4f93-a7ee-7a157c788b24', 'polynom_entities', 'Bike', 'test', 2025-08-05 09:37:39+00:00, '{"brand": [null, "Giant"], "model": [null, "Talon 3"], "owner_id": [null, "17f96a97-44b0-488a-8149-415f24f4f774"]}') /*sql@polynom_entities*/ CREATE TABLE IF NOT EXISTS "polynom_entities"."Bike" ("_entry_id" VARCHAR(36) NOT NULL, "brand" VARCHAR(50) NOT NULL, "model" VARCHAR(50) NOT NULL, "owner_id" VARCHAR(36) NOT NULL DEFAULT 'False', FOREIGN KEY ("owner_id") REFERENCES "polynom_entities"."User"("_entry_id"), PRIMARY KEY (_entry_id), UNIQUE ("_entry_id")); -/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."Bike" (_entry_id, brand, model, owner_id) VALUES ('7c2de745-2447-437d-885c-9b0b3364af3e', 'Trek', 'Marlin 7', 'ec0a1e28-9c90-4204-b0a7-733a69f24ce3') -/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."Bike" (_entry_id, brand, model, owner_id) VALUES ('ab239e80-8d61-4262-a0ed-2c5f24db5112', 'Specialized', 'Rockhopper', 'ec0a1e28-9c90-4204-b0a7-733a69f24ce3') -/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."Bike" (_entry_id, brand, model, owner_id) VALUES ('7f0910c9-5943-4cbc-a89b-45abcc9bc509', 'Cannondale', 'Trail 8', 'd93d9ec7-3a49-49c5-a09e-2ddc01f108d5') -/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."Bike" (_entry_id, brand, model, owner_id) VALUES ('ebcf6cff-ce3c-494c-8bdb-8b9fbe84cc31', 'Giant', 'Talon 3', '09c9b9df-0c7b-4221-9bf6-07be51e610c8') +/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."Bike" (_entry_id, brand, model, owner_id) VALUES ('bb551925-1a78-44cf-b8c0-a48be0f66c3c', 'Trek', 'Marlin 7', '5b6a8ad3-e85c-4266-86e1-079a1396d92d') +/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."Bike" (_entry_id, brand, model, owner_id) VALUES ('fe99dd37-54d9-41aa-9513-c86e68605881', 'Specialized', 'Rockhopper', '5b6a8ad3-e85c-4266-86e1-079a1396d92d') +/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."Bike" (_entry_id, brand, model, owner_id) VALUES ('a4c96c97-490a-4cc5-be38-373b1d47cad2', 'Cannondale', 'Trail 8', 'c29bb729-27ae-4062-bd2c-9de25e25e8e8') +/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."Bike" (_entry_id, brand, model, owner_id) VALUES ('035a88b9-3852-4f93-a7ee-7a157c788b24', 'Giant', 'Talon 3', '17f96a97-44b0-488a-8149-415f24f4f774') diff --git a/tests/model.py b/tests/model.py index 9e372ea..22eb001 100644 --- a/tests/model.py +++ b/tests/model.py @@ -1,10 +1,9 @@ -from polynom.schema.schema_registry import register_schema -from polynom.schema.field import Field, PrimaryKeyField, ForeignKeyField -from polynom.schema.polytypes import VarChar, Integer, Boolean -from polynom.model import BaseModel -from polynom.schema.relationship import Relationship +from polynom.model.model_registry import polynom_model +from polynom.model.model import BaseModel +from polynom.model.relationship import Relationship from tests.schema import UserSchema, BikeSchema +@polynom_model class User(BaseModel): schema = UserSchema() @@ -19,7 +18,8 @@ def __init__(self, username, email, first_name, last_name, active, is_admin, _en def get_full_name(self): return f"{self.first_name} {self.last_name}" - + +@polynom_model class Bike(BaseModel): schema = BikeSchema() user: User = Relationship(User, back_populates="bikes") diff --git a/tests/schema.py b/tests/schema.py index 91b2f44..2be7f2b 100644 --- a/tests/schema.py +++ b/tests/schema.py @@ -1,9 +1,9 @@ -from polynom.schema.schema_registry import register_schema -from polynom.schema.field import Field, PrimaryKeyField, ForeignKeyField -from polynom.schema.polytypes import VarChar, Integer, Boolean +from polynom.schema.schema_registry import polynom_schema +from polynom.schema.field import Field, ForeignKeyField +from polynom.schema.polytypes import VarChar, Boolean from polynom.schema.schema import BaseSchema -from polynom.schema.relationship import Relationship +@polynom_schema class UserSchema(BaseSchema): entity_name = 'User' fields = [ @@ -15,6 +15,7 @@ class UserSchema(BaseSchema): Field('is_admin', Boolean()), ] +@polynom_schema class BikeSchema(BaseSchema): entity_name = 'Bike' fields = [ @@ -26,6 +27,3 @@ class BikeSchema(BaseSchema): nullable=False, ), ] - -register_schema(UserSchema) -register_schema(BikeSchema) diff --git a/tests/test_relationship.py b/tests/test_relationship.py index d95366b..605e019 100644 --- a/tests/test_relationship.py +++ b/tests/test_relationship.py @@ -1,7 +1,10 @@ import pytest -from polynom.schema.relationship import Relationship +from polynom.model.relationship import Relationship +from polynom.model.model import BaseModel +from polynom.model.model_registry import polynom_model -class Cyclist: +@polynom_model +class Cyclist(BaseModel): def __init__(self, name): self.name = name self.bikes = [] @@ -15,18 +18,20 @@ def __init__(self, brand, model): self.brand = brand self.model = model + class Team: - member: Cyclist = Relationship(Cyclist, back_populates="teams") + member: Cyclist = Relationship("tests.test_relationship.Cyclist", back_populates="teams") def __init__(self, name): self.name = name class Sponsor: - cyclist: Cyclist = Relationship(Cyclist) + cyclist: Cyclist = Relationship("tests.test_relationship.Cyclist") def __init__(self, name): self.name = name + class TestRelationship: def test_bike_assignment_and_backref(self): """