diff --git a/docs/application.md b/docs/application.md index 686961c..ece8867 100644 --- a/docs/application.md +++ b/docs/application.md @@ -29,7 +29,8 @@ Application( use_docker: bool = False, migrate: bool = False, stop_container: bool = False, - remove_container: bool = False + remove_container: bool = False, + log_statements: bool = False ) ``` @@ -60,6 +61,9 @@ Application( - `remove_container` (`bool`, optional): If `True`, removes the Docker container running the Polypheny instance after stopping. Defaults to `False`. +- `remove_container` (`bool`, optional): + If `True`, all data- or schema modifying statements executed by this application are logged. Defaults to `False`. + ## Examples ```python from polynom.application import Application diff --git a/docs/config.md b/docs/config.md index 4d1f7da..a7f781c 100644 --- a/docs/config.md +++ b/docs/config.md @@ -114,6 +114,9 @@ Each configuration key is available as a named constant (e.g., `cfg.DEFAULT_USER - `DEFAULT_PASS`: Password used for default authentication. Default: `''` (empty string) +- `STATEMENT_LOG_FILE_NAME`: + Name of the statement log file. Default: `statements.log` + ### Derived Configuration - `CHANGE_LOG_IDENTIFIER`: diff --git a/docs/dump.md b/docs/dump.md new file mode 100644 index 0000000..fcf2087 --- /dev/null +++ b/docs/dump.md @@ -0,0 +1,62 @@ +--- +layout: page +title: "Dump" +toc: true +docs_area: "PolyNOM" +tags: backup, dump, load, import +lang: en +--- + +# Dump + +The database state of a PolyNOM application can be persisted into a multi-language query (MLQ) file. While this procedure is explained in detail in the application documentation, this page takes a closer look at the MLQ file generated. + +## Structure + +Every MLQ file is split into two sections. The first section is the header containing meta-information about the file. The second section contains the statements in one or more query languages. MLQ is a format extending traditional SQL dumps. MLQ focuses on reverse compatibility with traditional SQL dumps in cases where the only query language used in the dump is SQL. Therefore, all extensions are wrapped in comments to be ignored by traditional systems. + +## Header + +The header of an MLQ file is a multiline comment containing three key-value pairs prefixed with an `@` symbol. + +- **@format_version**: Denotes the format used for the MLQ dump. As of now, the corresponding value is always `1`. If the MLQ format is extended in the future, this number will be increased to enable differentiation and proper parsing. + +- **@app_uuid**: The application UUID of the application from which the dump originates. This ensures that dumps are only loaded by the appropriate application. + +- **@snapshot**: A JSON string representing a list of all schemas defined by the application from which the dump originated. This can be used to easily check for schema changes. It is used on load to verify that the schema to be created by the dump actually matches the expectations of the application loading the dump. + +## Statements + +The header is followed by a series of statements in one or more query languages. Each line begins with a comment of the form: + +```sql +/*language@namespace*/ +``` + +- `language` specifies the query language used for the following statement. +- `namespace` specifies in which namespace the statement should be executed. + +## Example +Below an example of a short application dump as an MLQ is shown: + +```sql +/* +@format_version: 1 +@app_uuid: a8817239-9bae-4961-a619-1e9ef5575eff +@snapshot: {"version": "20250902T084338", "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": "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 ('93a8779e-3c40-4baf-a7a5-27765effd6f8', '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 ('1c87088a-7a23-47e5-be1d-6891b097138f', '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 ('0a6a071a-8fc3-470d-bec1-bb9adf7a0015', '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 ('a09435c6-6037-4d8e-aba0-8d2d67063945', '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 ('d63a89b6-f339-4e5e-955a-4d36ad5edb37', 'testuser5', 'u5@demo.ch', 'mia', 'müller', FALSE, FALSE) +/*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 ('d48d23ac-a308-45f0-9250-86fdde8d58dd', 'Trek', 'Marlin 7', '93a8779e-3c40-4baf-a7a5-27765effd6f8') +/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."Bike" (_entry_id, brand, model, owner_id) VALUES ('ba13bb9a-40cc-437a-8b66-8a8b00bb5f6d', 'Specialized', 'Rockhopper', '93a8779e-3c40-4baf-a7a5-27765effd6f8') +/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."Bike" (_entry_id, brand, model, owner_id) VALUES ('040ecfde-db16-4a3f-bb6a-4e93331b0925', 'Cannondale', 'Trail 8', '0a6a071a-8fc3-470d-bec1-bb9adf7a0015') +/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."Bike" (_entry_id, brand, model, owner_id) VALUES ('7c36a17b-deb8-45e5-91da-0c33943504ec', 'Giant', 'Talon 3', 'a09435c6-6037-4d8e-aba0-8d2d67063945') + +``` \ No newline at end of file diff --git a/docs/models.md b/docs/models.md index 82c26b4..6c6d770 100644 --- a/docs/models.md +++ b/docs/models.md @@ -297,3 +297,11 @@ A wrapper provided by PolyNOM for the native `TEXT` type. Stores geometry object - **Python type**: `bytes` - **Polytype**: `File` +## Flex Model +The `Flex Model` is a special model class to be used if the schema or the model class of an entry is only known at runtime and thus a static implementation is not possible. + +This situation might arise when working with data from data models that do not use fixed schemas such as the document model. A flex model instance can be created from a schema using the static `from_schema(cls, schema)` method provided by the `Flex Model` class. This instance will then have all fields specified in the specified schema. Further a kwargs constructor is provided that takes values for all specified fields. `Flex Model` instances can be added to sessions like regular models. + +Another usecase for a `Flex Model` arises if the schema is known but the model class is not. This might be the case if entries are retireved from origins not under the control of PolyNOM. + +WARNING: When developing, one should always first attempt to achieve the desired functionality without flex models. Flex models should only be used when it is not possible to know the schema or the model. \ No newline at end of file diff --git a/docs/query.md b/docs/query.md new file mode 100644 index 0000000..feef536 --- /dev/null +++ b/docs/query.md @@ -0,0 +1,143 @@ +--- +layout: page +title: "Query" +toc: true +docs_area: "PolyNOM" +tags: query, filter, search, manipulation +lang: en +--- + +## Query + +This page discusses the retrieval of data. This involves three classes. Namely those are the `Session`, `Model` and the `Query` class. Each retrieval triggers queries on the underlying polypheny instance and must thus take place as part of a `Session`. To avoid the need for user written statements, the `Query` class provides a variety of methods for filtering as well as calculations. Each `Model` provides a corresponding instance of the `Query` class by its `query` method: + +### `query(session: Session)` + +Returns a `Query` instance that can be used to query entries of the type of the Model class. + +- `session` (`Session`): The session under as part of which to execute the query. + +## Filter Methods +Filter methods are the first type of method provided by a `Query` instance. Filter methods allow to define restriction to filter the entries to be returned. Filter methods always return a `Query` instance allowing them to be chained to bulid more copmlex filters. + +### `filter_by(**kwargs) → Query` +Adds simple equality filters to the query based on model fields. + +- `**kwargs`: Key-value pairs where keys are model attribute names and values are the values to filter by. Their format is `key=value` (e.g. last_name='meyer'). + +Returns the updated `Query` instance to allow the chaining of query methods. + +--- + +#### `filter(*expressions) → Query` +Adds complex filter expressions to the query. Each expression must be a tuple of `(operator, field, value)`. + +- `expressions`: Tuples specifying conditions. +- `operator`: A string SQL operator (e.g., `"="`, `">"`, `"LIKE"`). +- `field`: The name of the model field to compare. +- `value`: The value to compare against. + +Returns the updated `Query` instance to allow the chaining of query methods. + +--- + +### `distinct() → Query` +Marks the query to return only distinct results. + +Returns the updated `Query` instance to allow the chaining of query methods. + +--- + +### `limit(n: int) → Query` +Limits the number of results returned by the query. + +- `n`: Maximum number of entries to return. + +Returns the updated `Query` instance to allow the chaining of query methods. + +--- + +#### `order_by(field_name: str) → Query` +Specifies a field to order the query results by. + +- `field_name`: The name of the model field to sort by. +Raises `ValueError` if the field does not exist. + +Returns the updated `Query` instance to allow the chaining of query methods. + +--- + +### `join(related_model: Type[BaseModel], on: Optional[str] = None) → Query` +Adds a JOIN to include related models in the query. + +- `related_model`: The model class to join. +- `on` (optional): Custom join condition as a SQL string. If omitted, the system attempts to infer the join based on foreign keys. + +Returns the updated `Query` instance to allow the chaining of query methods. + +--- + +### `options(*opts) → Query` +Applies query options, such as eager-loading instructions. + +- `*opts`: Option objects to apply. Currently supports `JoinedLoad` for eager loading of related models. +Returns the updated `Query` instance to allow the chaining of query methods. + + +## Result Methods +Result methods are the secon type of method provided by a `Query` instance. Result methods trigger the execution of the `Query` instance and return a method specific result. + +### `all() → List[BaseModel]` +Executes the query and returns all matching model instances. + +Returns a list of model instances. Related children are attached according to any configured eager loads. + +--- + +### `first() → Optional[BaseModel]` +Fetches the first row matching the query, if any. + +Returns a single model instance if a row exists; otherwise, `None`. + +--- + +### `exists() → bool` +Checks whether any records exist matching the current query filters. + +Returns `True` if at least one matching row exists; otherwise, `False`. + +--- + +#### `count() → int` +Counts the number of entries matching the current query filters. +Returns an integer representing the number of matching entries. + +--- + +### `delete() → int` +Deletes all model instances matching the current query. + +Returns the number of models deleted. + +--- + +### `update(values: Dict[str, Any]) → int` +Updates all models matching the current query with the specified field values. + +- `values`: Dictionary mapping model field names to new values. +Returns the number of models updated. +Raises `AttributeError` if a field in `values` does not exist on the model. + +--- + + +### `get(pk_value: Any) → Optional[BaseModel]` +Fetches a single entry by its primary key. + +- `pk_value`: The value of the primary key to search for. +Returns the model instance if found; otherwise, `None`. +Raises `ValueError` if the model does not have a `PrimaryKeyField`. + +--- + + diff --git a/docs/session.md b/docs/session.md index 6516f45..ef74bf2 100644 --- a/docs/session.md +++ b/docs/session.md @@ -88,7 +88,7 @@ Discards all changes made as part of this session. This invalidates all tracked --- -#### `_execute(language, statement, parameters=None, namespace=None, fetch=True)` +#### `_execute(language: str, statement: str, parameters=None, namespace: str=None, fetch: bool=True)` Executes a statement using the session's internal cursor. This method supports both DDL and DML operations across different query languages (e.g., SQL, Cypher, MQL). @@ -99,6 +99,16 @@ Executes a statement using the session's internal cursor. This method supports b - `fetch` (optional, default=`True`): If `True`, the result of the query is returned if present. If set to `False` no results are retrieved independent of the query type. --- +WARNING: This method is considered deprecated and will be removed in the future. + +#### `_execute(statement: Statement, fetch=True)` + +Executes a statement object using the session's internal cursor. This method is the replacement for the previous, deprecated method of the same name. + +- `statement`: The statement object to execute. +- `fetch` (optional, default=`True`): If `True`, the result of the query is returned if present. If set to `False` no results are retrieved independent of the query type. +--- + #### `get_session_state() → _SessionState` Returns the current state of the session. diff --git a/docs/statement.md b/docs/statement.md new file mode 100644 index 0000000..a2051bd --- /dev/null +++ b/docs/statement.md @@ -0,0 +1,54 @@ +--- +layout: page +title: "Statement" +toc: true +docs_area: "PolyNOM" +tags: query, statement, language +lang: en +--- + +## Statement + +The `Statement` class represents an individual statement to be executed as part of a session. It thereby combines a query language, a statement in that language, optional parameters and an optional namespace to execute the expression in. Statements enable users to write and execute queries directly beyond the methods provided by the `Query` object. + +## Initialization Parameters +```python +Statement( + language: str, + statement: str, + values: Tuple[Any, ...] = None, + namespace: str = None +) +``` + +- `language` (`str`, required): + The name of the query language of the provided statement (e.g., `'sql'`, `'cypher'`, `'mongo'`). + +- `statement`: + The statement string to be executed. This must be in the query language specified useing the `language` parameter. + +- `values` (`str`, optional): + If the statement string provided contains placeholders, their values must be specified here. Values are assigned to placeholders from left to right. + If no placeholders are present no values must be set. + +- `namespace` (`str`, optional): + The name of the namespace in which the statement should be executed. If not specified the default namespace configured in the PolyNOM config is used. + +## Examples +```python +from polynom.application import Application +from polynom.session import Session +from polynom.statement import Statement + +APP_UUID = 'a8817239-9bae-4961-a619-1e9ef5575eff' + +with Application(APP_UUID, ('localhost', 20590)) as app: + with Session(app) as session: + expensive_brands_statement = Statement( + 'sql', + 'SELECT DISTINCT brand FROM bikes WHERE price > ?;', + (6000,), + 'cycling' + ) + expensive_brands = session._execute(expensive_brands_statement, fetch=True) +``` \ No newline at end of file diff --git a/docs/statement_log.md b/docs/statement_log.md new file mode 100644 index 0000000..53802e6 --- /dev/null +++ b/docs/statement_log.md @@ -0,0 +1,34 @@ +--- +layout: page +title: "Statement Log" +toc: true +docs_area: "PolyNOM" +tags: backup, dump, file, log +lang: en +--- + +# Statement Log + +Every PolyNOM application maintains a statement log. This log records all committed, data-modifying statements executed by the application. The base name of the log file can be configured using the PolyNOM configuration. The base name is extended by the UUID of the application. A new file is created once the current one reaches a size of 1 GB. The new file's name is further extended with a timestamp of its creation. + +**WARNING:** Statement log files are kept indefinitely. It is the user's responsibility to manage and dispose of old logs that are no longer needed. + +## File Name + +If the base name in the configuration is set to `statements.log`, the first file created might be named: `statements_a8817239-9bae-4961-a619-1e9ef5575eff.log`. Here, the UUID of the application is `a8817239-9bae-4961-a619-1e9ef5575eff`. Once this file reaches 1 GB, a new file is created with a timestamp appended to the name: `statements_a8817239-9bae-4961-a619-1e9ef5575eff.20250902-100915.log`. + +## Structure + +The statement log contains one statement per line in one or more query languages. Similar to MLQ files used for application dumps, each line begins with comments storing metadata for the statement: + +1. **Timestamp comment** – indicates when the entry was created. +2. **Language and namespace comment** – specifies the query language and the namespace in which the statement was executed. + +Example: + +```sql +/*2025-09-02 08:44:06,008*/ /*sql@polynom_entities*/ DELETE FROM "polynom_entities"."User" WHERE _entry_id = '19b69a99-bc5a-4e10-b90e-c276f56b440f' +``` + +## Rollbacks and Reads +The log only contains statements from committed sessions. Statements from sessions that were rolled back, either manually or automatically, are omitted. Only statements that modify schema or data are recorded. Statements that do not modify any data are omitted. \ No newline at end of file diff --git a/polynom/application.py b/polynom/application.py index 2b8c935..021b18d 100644 --- a/polynom/application.py +++ b/polynom/application.py @@ -5,6 +5,7 @@ from enum import Enum, auto from polynom.schema.migration import Migrator from polynom.session import Session +from polynom.statement import Statement from polynom.schema.schema_registry import _get_ordered_schemas, _to_dict from polynom.schema.schema import DataModel from polynom.reflection import SchemaSnapshot, SchemaSnapshotSchema @@ -29,7 +30,8 @@ def __init__( use_docker: bool = False, migrate: bool = False, stop_container: bool = False, - remove_container: bool = False + remove_container: bool = False, + log_statements: bool = False ): cfg.lock() @@ -42,6 +44,7 @@ def __init__( self._migrate = migrate self._stop_container = stop_container self._remove_container = remove_container + self._log_statements = log_statements self._conn = None self._cursor = None @@ -126,14 +129,24 @@ def _process_schema(self, schema_class): generator = _SqlGenerator() - generator._create_namespace(namespace, data_model, if_not_exists=True).execute(self._cursor) + statement = generator._create_namespace(namespace, data_model, if_not_exists=True) + self._log_statement(statement) + statement.execute(self._cursor) logger.debug(f"Created namespace {namespace} if absent.") - generator._define_entity(schema_class, if_not_exists=True).execute(self._cursor) + statement = generator._define_entity(schema_class, if_not_exists=True) + self._log_statement(statement) + statement.execute(self._cursor) self._conn.commit() logger.debug(f"Created entity {entity} if absent.") + def _log_statement(self, statement: Statement): + if not self._log_statements: + return + statement.log(self._app_uuid) + + def dump(self, file_path): if self._state != _ApplicationState.ACTIVE: message = f'Application {self._app_uuid} must first be activated by using it in a "with" block' diff --git a/polynom/config.py b/polynom/config.py index d1b128c..8ae0c45 100644 --- a/polynom/config.py +++ b/polynom/config.py @@ -18,6 +18,8 @@ CHANGE_LOG_IDENTIFIER = 'CHANGE_LOG_IDENTIFIER' +STATEMENT_LOG_FILE_NAME = 'STATEMENT_LOG_FILE_NAME' + # constants _internals = { INTERNAL_NAMESPACE: 'polynom_internal', @@ -44,6 +46,7 @@ DEFAULT_TRANSPORT: 'plain', DEFAULT_USER: 'pa', DEFAULT_PASS: '', + STATEMENT_LOG_FILE_NAME: 'statements.log' } # derived options diff --git a/polynom/dump.py b/polynom/dump.py index fed7ea5..c53ac12 100644 --- a/polynom/dump.py +++ b/polynom/dump.py @@ -214,15 +214,16 @@ def _execute_statements(application, file): language, namespace, statement_text = match.groups() namespace = None if namespace == "None" else namespace - stmt = Statement( + statement = Statement( language=language, namespace=namespace, statement=statement_text, ) try: - logger.debug(stmt.dump()) - session._execute(stmt, fetch=False) + logger.debug(statement.dump()) + application._log_statement(statement) + session._execute(statement, fetch=False) except Exception as e: logger.error(f"Error executing line {line_number}: {e}") diff --git a/polynom/model/__init__.py b/polynom/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/polynom/query.py b/polynom/query.py index 7a11e4f..b652613 100644 --- a/polynom/query.py +++ b/polynom/query.py @@ -35,10 +35,8 @@ def filter(self, *expressions): if not isinstance(expr, tuple) or len(expr) != 3: raise ValueError("Invalid filter expression") op, field, value = expr - if not hasattr(field, '_db_field_name'): - raise TypeError(f"Expected a Field object, got {type(field)}") - - clause = f'"{field._db_field_name}" {op} ?' + + clause = f'"{field}" {op} ?' self._filters.setdefault('_extra_clauses', []).append((clause, value)) return self diff --git a/polynom/schema/migration.py b/polynom/schema/migration.py index 7ddd6c1..5320c05 100644 --- a/polynom/schema/migration.py +++ b/polynom/schema/migration.py @@ -1,6 +1,8 @@ from __future__ import annotations import logging +from polynom.statement import Statement + logger = logging.getLogger(__name__) class Migrator: @@ -114,6 +116,13 @@ def run(self, session: Session, diff: dict): self._generate_statements(diff) for namespace_name, statement in self.statements_with_namespace: logger.debug(f"Migration: namespace={namespace_name}, statement={statement}") - session._execute('sql', statement, namespace=namespace_name, fetch=False) + + current_statement = Statement( + language='sql', + statement=statement, + namespace=namespace_name + ) + session._application._log_statement(current_statement) + session._execute(current_statement, fetch=False) logger.info("Automatic schema migration complete.") diff --git a/polynom/session.py b/polynom/session.py index 1a53514..b4ec725 100644 --- a/polynom/session.py +++ b/polynom/session.py @@ -44,6 +44,7 @@ def __init__( self._cursor = None self._state = _SessionState.INITIALIZED self._tracked_models: dict[str, BaseModel] = {} + self._statements = [] self._generator = _SqlGenerator() @@ -78,6 +79,7 @@ def _update(self, model): raise ValueError("Model must have an _entry_id to perform update.") statement = self._generator._update(model) + self._statements.append(statement) statement.execute(self._cursor) def _update_change_log(self, model, diff: dict): @@ -115,6 +117,7 @@ def add(self, model, tracking=True): self._add_related_models(model) statement = self._generator._insert(model) + self._statements.append(statement) statement.execute(self._cursor) def add_all(self, models, tracking=True): @@ -152,17 +155,15 @@ def _track(self, model): def _track_all(self, models): for model in models: self._track(model) - - def _execute(self, language, statement, parameters=None, namespace=None, fetch=True): - self._cursor.executeany(language, statement, params=parameters, namespace=namespace) - if fetch: - try: - return self._cursor.fetchall() - except Exception: - return - return + + @DeprecationWarning + def _execute(self, language: str, statement: str, parameters=None, namespace: str =None, fetch: bool=True): + stmt = Statement(language, statement, parameters, namespace ) + self._statements.append(stmt) + return self._execute(stmt, fetch) def _execute(self, statement: Statement, fetch=True): + self._statements.append(statement) statement.execute(self._cursor) if fetch: try: @@ -175,6 +176,7 @@ def delete(self, model): self._throw_if_not_active() statement = self._generator._delete(model) + self._statements.append(statement) statement.execute(self._cursor) if model._entry_id in self._tracked_models: @@ -205,6 +207,9 @@ def commit(self): self.flush() self._conn.commit() + for statement in self._statements: + self._application._log_statement(statement) + # check if the model has a child or not and then only commit that self._invalidate_models() diff --git a/polynom/statement.py b/polynom/statement.py index a91cd7e..c21fb05 100644 --- a/polynom/statement.py +++ b/polynom/statement.py @@ -1,10 +1,10 @@ +from polynom.statement_logger import LoggerFactory from abc import ABC, abstractmethod from dataclasses import dataclass, field from typing import Any, Tuple from polynom.schema.schema import DataModel from polynom.schema.field import PrimaryKeyField, ForeignKeyField - @dataclass class Statement: language: str @@ -33,6 +33,12 @@ def dump(self) -> str: namespace_comment = f'{self.language}@{self.namespace}' if self.namespace else f'{self.language}@None' return f'/*{namespace_comment}*/ {stmt}' + def log(self, app_uuid: str = None) -> str: + if not app_uuid: + raise ValueError("app_uuid must be provided for logging") + logger = LoggerFactory.get_logger(app_uuid) + logger.info(self.dump()) + @staticmethod def _format_value(value: Any) -> str: if isinstance(value, str): diff --git a/polynom/statement_logger.py b/polynom/statement_logger.py new file mode 100644 index 0000000..afc5212 --- /dev/null +++ b/polynom/statement_logger.py @@ -0,0 +1,55 @@ +import logging +from logging.handlers import RotatingFileHandler +import time +import os +import threading +import polynom.config as config + +class InfiniteRotatingFileHandler(RotatingFileHandler): + """RotatingFileHandler that never deletes old log files.""" + def doRollover(self): + if self.stream: + self.stream.close() + self.stream = None + + timestamp = time.strftime("%Y%m%d-%H%M%S") + rollover_filename = f"{self.baseFilename}.{timestamp}" #self.baseFilename is set by RotatingFileHandler constructor + if os.path.exists(self.baseFilename): + os.rename(self.baseFilename, rollover_filename) + + self.mode = "w" + self.stream = self._open() + + +class LoggerFactory: + """Factory to provide per-application loggers.""" + _loggers = {} + _lock = threading.Lock() + + @classmethod + def get_logger(cls, app_uuid: str): + if not app_uuid: + raise ValueError("app_uuid must be provided") + + with cls._lock: + if app_uuid not in cls._loggers: + logger = logging.getLogger(f"statement_logger_{app_uuid}") + logger.setLevel(logging.INFO) + + log_file = config.get(config.STATEMENT_LOG_FILE_NAME) + # append UUID to filename + filename = f"{os.path.splitext(log_file)[0]}_{app_uuid}.log" + + handler = InfiniteRotatingFileHandler( + filename, + maxBytes=1 * 1024 * 1024 * 1024, + backupCount=0, + encoding="utf-8" + ) + formatter = logging.Formatter("/*%(asctime)s*/ %(message)s") + handler.setFormatter(formatter) + logger.addHandler(handler) + + cls._loggers[app_uuid] = logger + + return cls._loggers[app_uuid] diff --git a/test_dump.sql b/test_dump.sql index 468930e..36e94c7 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": "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"}]}]} +@snapshot: {"version": "20250902T130116", "schemas": [{"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": "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": "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_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 ('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 ('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_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 ('be4e5d02-556a-4d75-b593-ec12a9e30020', 'a8817239-9bae-4961-a619-1e9ef5575eff', '85e510a8-456c-4cda-85b3-e28e289e8e3e', 'polynom_entities', 'User', 'test', 2025-09-02 11:01:14+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 ('b01c705b-4355-486f-a050-a40dd0a0b5a7', 'a8817239-9bae-4961-a619-1e9ef5575eff', '0ded1d18-021c-4950-8d66-705adc7ab81c', 'polynom_entities', 'User', 'test', 2025-09-02 11:01:15+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 ('4134cc5e-ad41-4c22-9d5d-4aa7c019affe', 'a8817239-9bae-4961-a619-1e9ef5575eff', '39c3b556-4563-49dd-b02e-d67770dec2cc', 'polynom_entities', 'User', 'test', 2025-09-02 11:01:15+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 ('187562d8-a802-4053-a113-5b6a80a59c6e', 'a8817239-9bae-4961-a619-1e9ef5575eff', '51aca4be-c98b-4f30-a46f-a9f4e26dead0', 'polynom_entities', 'User', 'test', 2025-09-02 11:01:15+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 ('cdb54a8c-bed6-4e3a-a9f9-a23bd7997969', 'a8817239-9bae-4961-a619-1e9ef5575eff', '7fab1a2a-5101-4554-ad36-ada7c7dac8e8', 'polynom_entities', 'User', 'test', 2025-09-02 11:01:15+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 ('4ac3d749-ecc8-468c-87e1-528fbc2b99f8', 'a8817239-9bae-4961-a619-1e9ef5575eff', 'e2bb1175-bafb-4edf-8ff6-9d33a63bdab3', 'polynom_entities', 'Bike', 'test', 2025-09-02 11:01:15+00:00, '{"brand": [null, "Trek"], "model": [null, "Marlin 7"], "owner_id": [null, "85e510a8-456c-4cda-85b3-e28e289e8e3e"]}') +/*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 ('0696e254-513e-4f8c-8102-1b2b24522040', 'a8817239-9bae-4961-a619-1e9ef5575eff', 'f6eacf31-f44e-4c0d-8dd4-fff9d9f5079c', 'polynom_entities', 'Bike', 'test', 2025-09-02 11:01:15+00:00, '{"brand": [null, "Specialized"], "model": [null, "Rockhopper"], "owner_id": [null, "85e510a8-456c-4cda-85b3-e28e289e8e3e"]}') +/*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 ('56ef62c8-a167-4a07-8afe-78d4bf175d91', 'a8817239-9bae-4961-a619-1e9ef5575eff', 'be9b5f23-c20d-4693-8f53-df1f357ca23a', 'polynom_entities', 'Bike', 'test', 2025-09-02 11:01:15+00:00, '{"brand": [null, "Cannondale"], "model": [null, "Trail 8"], "owner_id": [null, "39c3b556-4563-49dd-b02e-d67770dec2cc"]}') +/*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 ('b0d38c33-e472-43bd-88dc-5859e83ccec6', 'a8817239-9bae-4961-a619-1e9ef5575eff', 'df141b62-beef-4e58-8590-235f0649edc0', 'polynom_entities', 'Bike', 'test', 2025-09-02 11:01:15+00:00, '{"brand": [null, "Giant"], "model": [null, "Talon 3"], "owner_id": [null, "51aca4be-c98b-4f30-a46f-a9f4e26dead0"]}') +/*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 ('85e510a8-456c-4cda-85b3-e28e289e8e3e', '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 ('0ded1d18-021c-4950-8d66-705adc7ab81c', '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 ('39c3b556-4563-49dd-b02e-d67770dec2cc', '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 ('51aca4be-c98b-4f30-a46f-a9f4e26dead0', '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 ('7fab1a2a-5101-4554-ad36-ada7c7dac8e8', 'testuser5', 'u5@demo.ch', 'mia', 'müller', FALSE, FALSE) /*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 ('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') +/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."Bike" (_entry_id, brand, model, owner_id) VALUES ('e2bb1175-bafb-4edf-8ff6-9d33a63bdab3', 'Trek', 'Marlin 7', '85e510a8-456c-4cda-85b3-e28e289e8e3e') +/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."Bike" (_entry_id, brand, model, owner_id) VALUES ('f6eacf31-f44e-4c0d-8dd4-fff9d9f5079c', 'Specialized', 'Rockhopper', '85e510a8-456c-4cda-85b3-e28e289e8e3e') +/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."Bike" (_entry_id, brand, model, owner_id) VALUES ('be9b5f23-c20d-4693-8f53-df1f357ca23a', 'Cannondale', 'Trail 8', '39c3b556-4563-49dd-b02e-d67770dec2cc') +/*sql@polynom_entities*/ INSERT INTO "polynom_entities"."Bike" (_entry_id, brand, model, owner_id) VALUES ('df141b62-beef-4e58-8590-235f0649edc0', 'Giant', 'Talon 3', '51aca4be-c98b-4f30-a46f-a9f4e26dead0') diff --git a/tests/test_query.py b/tests/test_query.py index 0d32ebd..774bdb9 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -6,7 +6,7 @@ @pytest.fixture(scope='module') def app(): - app = Application(APP_UUID, ('localhost', 20590), use_docker=True, stop_container=True) + app = Application(APP_UUID, ('localhost', 20590), use_docker=True, stop_container=True, log_statements=True) with app: yield app @@ -92,6 +92,28 @@ def test_query_all_filtered4(setup_test): assert len(result) == 1 assert result[0]._entry_id == users[0]._entry_id + +def test_query_all_filtered5(setup_test): + users, bikes, app = setup_test + session = Session(app, 'test') + with session: + result = User.query(session).filter(('=', 'active', True)).all() + expected_entry_ids = [users[0]._entry_id, users[3]._entry_id] + + assert len(result) == 2 + for user in result: + assert user._entry_id in expected_entry_ids + +def test_query_all_filtered6(setup_test): + users, bikes, app = setup_test + session = Session(app, 'test') + with session: + result = User.query(session).filter(('LIKE', 'first_name', 'mir_')).all() + expected_entry_ids = users[1]._entry_id + + assert len(result) == 1 + for user in result: + assert user._entry_id == expected_entry_ids def test_query_first_filtered(setup_test): users, bikes, app = setup_test