diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 15186a95..7a2b0170 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -123,13 +123,13 @@ jobs: --rootdir=. \ --confcutdir=. \ "$TESTSUITE_DIR/benchmark/crud/test_delete_async.py" \ - "$TESTSUITE_DIR/benchmark/crud/test_delete_sync.py" \ + "$TESTSUITE_DIR/benchmark/crud/test_delete.py" \ "$TESTSUITE_DIR/benchmark/crud/test_insert_async.py" \ - "$TESTSUITE_DIR/benchmark/crud/test_insert_sync.py" \ + "$TESTSUITE_DIR/benchmark/crud/test_insert.py" \ "$TESTSUITE_DIR/benchmark/crud/test_query_async.py" \ - "$TESTSUITE_DIR/benchmark/crud/test_query_sync.py" \ + "$TESTSUITE_DIR/benchmark/crud/test_query.py" \ "$TESTSUITE_DIR/benchmark/crud/test_update_async.py" \ - "$TESTSUITE_DIR/benchmark/crud/test_update_sync.py" \ + "$TESTSUITE_DIR/benchmark/crud/test_update.py" \ "$TESTSUITE_DIR/benchmark/query" \ "$TESTSUITE_DIR/benchmark/transaction" \ "$TESTSUITE_DIR/benchmark/mixin" \ diff --git a/changelog.d/49.added.md b/changelog.d/49.added.md new file mode 100644 index 00000000..966d1c1f --- /dev/null +++ b/changelog.d/49.added.md @@ -0,0 +1 @@ +Add PGQ enhancements: fulltext search capability, ON CONFLICT switches, JSON arrow operators, cursor result-set pollution fix, and PostgreSQL 19 compatibility gate for quantified path and comma-separated patterns \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py index 19aeea55..7aba7eb1 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py @@ -346,8 +346,6 @@ async def get_server_version(self) -> Optional[Tuple[int, int, int]]: cursor = await self._get_cursor() await cursor.execute("SELECT version()") version_row = await cursor.fetchone() - # Defensively consume remaining results to prevent cursor.description pollution - remaining = await cursor.fetchall() if not version_row or not version_row[0]: self.log(logging.WARNING, "PostgreSQL version query returned no result") return None diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py index dce98ddb..b23d26fa 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py @@ -347,8 +347,6 @@ def get_server_version(self) -> Optional[Tuple[int, int, int]]: cursor = self._get_cursor() cursor.execute("SELECT version()") result = cursor.fetchone() - # Defensively consume remaining results to prevent cursor.description pollution - cursor.fetchall() if not result or not result[0]: self.log(logging.WARNING, "PostgreSQL version query returned no result") return None diff --git a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py index 8a681cb0..c2127666 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py @@ -694,6 +694,10 @@ def supports_for_update(self) -> bool: """ return True + def supports_json_arrow_operators(self) -> bool: + """PostgreSQL supports -> and ->> operators for JSON/JSONB access.""" + return True + def supports_lock_strength(self, strength) -> bool: """ Check if a specific lock strength is supported. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/conftest.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/conftest.py index a3c1a4db..2ec48e6a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/conftest.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/conftest.py @@ -262,6 +262,14 @@ 'min_version': '9', 'max_version': '18', }, + 'pgq/recursive_cte_graph_traversal.py': { + 'title': 'Recursive CTE Graph Traversal (PGQ Quantified Path Alternative)', + 'dialect_protocols': [], + 'priority': 5, + 'min_version': '9', + 'max_version': '18', + 'note': 'Uses recursive CTE + UNION ALL to simulate PGQ quantified path patterns; works on all PG versions', + }, 'transaction/basic.py': { 'title': 'Transaction Control', 'dialect_protocols': [], diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/pgq/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/pgq/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/pgq/recursive_cte_graph_traversal.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/pgq/recursive_cte_graph_traversal.py new file mode 100644 index 00000000..2c7241df --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/pgq/recursive_cte_graph_traversal.py @@ -0,0 +1,389 @@ +""" +Recursive CTE graph traversal — PGQ quantified path alternative +=============================================================== + +PostgreSQL 19 natively supports SQL/PGQ (MATCH / GRAPH_TABLE), with limitations: + - supports_quantified_path() → False (no variable-length paths +/*/{n,m}) + - supports_comma_separated_patterns() → False (no comma-separated multi-pattern) + +This example demonstrates recursive CTE + SetOperationExpression + UNION ALL +as an equivalent alternative for graph traversal patterns. + +Common use cases: + 1. Social network N-degree friend recommendations (replaces MATCH (a)-[e]->+(b)) + 2. AML fund source tracing (replaces MATCH (s)-[t*1..5]->(d)) + +Key components: + - CTEExpression: defines a CTE (WITH ... AS ...) + - SetOperationExpression: combines base + recursive queries (UNION ALL) + - WithQueryExpression(recursive=True): generates WITH RECURSIVE ... SELECT ... +""" + +# ============================================================ +# SECTION: Setup (reference only — creates tables and data) +# ============================================================ +import os +from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig + +config = PostgresConnectionConfig( + host=os.getenv('POSTGRES_HOST', 'localhost'), + port=int(os.getenv('POSTGRES_PORT', 5432)), + database=os.getenv('POSTGRES_DATABASE', 'test'), + username=os.getenv('POSTGRES_USER', 'postgres'), + password=os.getenv('POSTGRES_PASSWORD', ''), +) +backend = PostgresBackend(connection_config=config) +backend.connect() +dialect = backend.dialect + +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, + InsertExpression, + ValuesSource, + DropTableExpression, + QueryExpression, + TableExpression, + CTEExpression, + WithQueryExpression, + SetOperationExpression, + FunctionCall, + ColumnDefinition, + ColumnConstraint, + ColumnConstraintType, +) +from rhosocial.activerecord.backend.expression.core import Literal, Column +from rhosocial.activerecord.backend.expression.predicates import BetweenPredicate +from rhosocial.activerecord.backend.expression.query_parts import ( + WhereClause, + OrderByClause, + JoinExpression, +) +from rhosocial.activerecord.backend.options import ExecutionOptions +from rhosocial.activerecord.backend.schema import StatementType + +dql_options = ExecutionOptions(stmt_type=StatementType.DQL) + +for t in ("transactions", "accounts", "follows", "users"): + sql, params = DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql() + backend.execute(sql, params) + +# ---- Social network tables ---- +users_cols = [ + ColumnDefinition('id', 'INTEGER', constraints=[ + ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition('name', 'VARCHAR(100)'), + ColumnDefinition('city', 'VARCHAR(50)'), +] +backend.execute(*CreateTableExpression(dialect, 'users', users_cols, if_not_exists=True).to_sql()) + +follows_cols = [ + ColumnDefinition('id', 'INTEGER', constraints=[ + ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition('follower_id', 'INTEGER'), + ColumnDefinition('followed_id', 'INTEGER'), +] +backend.execute(*CreateTableExpression(dialect, 'follows', follows_cols, if_not_exists=True).to_sql()) + +users_data = ValuesSource(dialect, [ + [Literal(dialect, 1), Literal(dialect, 'Alice'), Literal(dialect, 'NYC')], + [Literal(dialect, 2), Literal(dialect, 'Bob'), Literal(dialect, 'NYC')], + [Literal(dialect, 3), Literal(dialect, 'Charlie'), Literal(dialect, 'LA')], + [Literal(dialect, 4), Literal(dialect, 'Diana'), Literal(dialect, 'NYC')], + [Literal(dialect, 5), Literal(dialect, 'Eve'), Literal(dialect, 'LA')], +]) +backend.execute(*InsertExpression(dialect, 'users', columns=['id', 'name', 'city'], source=users_data).to_sql()) + +follows_data = ValuesSource(dialect, [ + [Literal(dialect, 1), Literal(dialect, 1), Literal(dialect, 2)], + [Literal(dialect, 2), Literal(dialect, 2), Literal(dialect, 3)], + [Literal(dialect, 3), Literal(dialect, 1), Literal(dialect, 3)], + [Literal(dialect, 4), Literal(dialect, 4), Literal(dialect, 1)], + [Literal(dialect, 5), Literal(dialect, 3), Literal(dialect, 5)], +]) +backend.execute(*InsertExpression(dialect, 'follows', columns=['id', 'follower_id', 'followed_id'], source=follows_data).to_sql()) + +# ---- AML tables ---- +accounts_cols = [ + ColumnDefinition('id', 'INTEGER', constraints=[ + ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition('account_holder', 'VARCHAR(100)'), + ColumnDefinition('account_type', 'VARCHAR(20)'), +] +backend.execute(*CreateTableExpression(dialect, 'accounts', accounts_cols, if_not_exists=True).to_sql()) + +txn_cols = [ + ColumnDefinition('id', 'INTEGER', constraints=[ + ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition('source_account_id', 'INTEGER'), + ColumnDefinition('target_account_id', 'INTEGER'), + ColumnDefinition('amount', 'NUMERIC(12,2)'), + ColumnDefinition('ts', 'VARCHAR(20)'), +] +backend.execute(*CreateTableExpression(dialect, 'transactions', txn_cols, if_not_exists=True).to_sql()) + +accounts_data = ValuesSource(dialect, [ + [Literal(dialect, 1), Literal(dialect, 'Alice Smith'), Literal(dialect, 'checking')], + [Literal(dialect, 2), Literal(dialect, 'Bob Corp'), Literal(dialect, 'business')], + [Literal(dialect, 3), Literal(dialect, 'Charlie Ltd'), Literal(dialect, 'business')], + [Literal(dialect, 4), Literal(dialect, 'Diana Offshore'), Literal(dialect, 'offshore')], + [Literal(dialect, 5), Literal(dialect, 'Eve Holding'), Literal(dialect, 'offshore')], + [Literal(dialect, 6), Literal(dialect, 'Foreign Entity'), Literal(dialect, 'offshore')], +]) +backend.execute(*InsertExpression(dialect, 'accounts', columns=['id', 'account_holder', 'account_type'], source=accounts_data).to_sql()) + +txn_data = ValuesSource(dialect, [ + [Literal(dialect, 1), Literal(dialect, 4), Literal(dialect, 3), Literal(dialect, 500000), Literal(dialect, '2024-01-15')], + [Literal(dialect, 2), Literal(dialect, 5), Literal(dialect, 3), Literal(dialect, 300000), Literal(dialect, '2024-02-01')], + [Literal(dialect, 3), Literal(dialect, 3), Literal(dialect, 2), Literal(dialect, 600000), Literal(dialect, '2024-03-01')], + [Literal(dialect, 4), Literal(dialect, 2), Literal(dialect, 1), Literal(dialect, 450000), Literal(dialect, '2024-04-01')], + [Literal(dialect, 5), Literal(dialect, 6), Literal(dialect, 5), Literal(dialect, 1000000), Literal(dialect, '2024-05-01')], +]) +backend.execute(*InsertExpression(dialect, 'transactions', columns=['id', 'source_account_id', 'target_account_id', 'amount', 'ts'], source=txn_data).to_sql()) + +# ============================================================ +# SECTION: Scenario 1 — Social Network N-degree Recommendation +# Equivalent to PGQ: MATCH (a:person {name: 'Alice'})-[e:follows]+{1,3}->(b:person) +# ============================================================ +print("=" * 72) +print("Scenario 1: Social Network N-degree Friend Recommendations") +print("Equivalent: MATCH (a)-[e:follows]+{1,3}->(b)") +print("Technique: Recursive CTE + UNION ALL + depth filter ({1,3})") +print("=" * 72) + +# --- Base query: start vertex Alice, depth=0 --- +base_query = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, 'id'), + Column(dialect, 'name'), + Literal(dialect, 0).as_('depth'), + ], + from_=TableExpression(dialect, 'users'), + where=WhereClause(dialect, condition=Column(dialect, 'name') == Literal(dialect, 'Alice')), +) + +# --- Recursive query: follow edges one hop, depth+1, bound depth<4 --- +recursive_join = JoinExpression( + dialect=dialect, + left_table=TableExpression(dialect, 'traversal', alias='t'), + right_table=TableExpression(dialect, 'follows', alias='f'), + join_type='INNER JOIN', + condition=Column(dialect, 'id', table='t') == Column(dialect, 'follower_id', table='f'), +).inner_join( + right_table=TableExpression(dialect, 'users', alias='u'), + condition=Column(dialect, 'followed_id', table='f') == Column(dialect, 'id', table='u'), +) + +recursive_query = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, 'id', table='u'), + Column(dialect, 'name', table='u'), + FunctionCall(dialect, '+', Column(dialect, 'depth', table='t'), Literal(dialect, 1)).as_('depth'), + ], + from_=recursive_join, + where=WhereClause(dialect, condition=Column(dialect, 'depth', table='t') < Literal(dialect, 4)), +) + +# --- Combine with UNION ALL --- +union_expr = SetOperationExpression( + dialect=dialect, + left=base_query, + right=recursive_query, + operation='UNION ALL', +) + +# --- Define CTE --- +traversal_cte = CTEExpression( + dialect=dialect, + name='traversal', + query=union_expr, + columns=['id', 'name', 'depth'], +) + +# --- Main query: filter depth 1~3 (simulating quantified path {1,3}) --- +friend_query = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, 'id'), + Column(dialect, 'name'), + Column(dialect, 'depth'), + ], + from_=TableExpression(dialect, 'traversal'), + where=WhereClause(dialect, condition=BetweenPredicate( + dialect, Column(dialect, 'depth'), Literal(dialect, 1), Literal(dialect, 3), + )), + order_by=OrderByClause(dialect, [Column(dialect, 'depth'), Column(dialect, 'name')]), +) + +with_query = WithQueryExpression( + dialect=dialect, + ctes=[traversal_cte], + main_query=friend_query, + recursive=True, +) + +sql, params = with_query.to_sql() +print(f"\nGenerated SQL:\n{sql}\n") +result = backend.execute(sql, params, options=dql_options) +print("Friend recommendations for Alice (depth 1~3):") +for row in result.data or []: + print(f" depth={row['depth']}: {row['name']}") +print() + +# ============================================================ +# SECTION: Scenario 2 — AML Fund Source Tracing +# Equivalent to PGQ: MATCH (s:account)-[t:transfer*1..5]->(d:account {id: 1}) +# ============================================================ +print("=" * 72) +print("Scenario 2: AML Fund Source Tracing") +print("Equivalent: MATCH (s)-[t:transfer*1..5]->(d {id: 1})") +print("Technique: Recursive CTE + UNION ALL + depth limit + amount aggregation") +print("=" * 72) + +# --- Base query: direct sources sending to target account ACC-001 --- +aml_base_join = JoinExpression( + dialect=dialect, + left_table=TableExpression(dialect, 'transactions', alias='tx'), + right_table=TableExpression(dialect, 'accounts', alias='a'), + join_type='INNER JOIN', + condition=Column(dialect, 'source_account_id', table='tx') == Column(dialect, 'id', table='a'), +) + +aml_base = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, 'id', table='a'), + Column(dialect, 'account_holder', table='a'), + Column(dialect, 'account_type', table='a'), + Column(dialect, 'amount', table='tx'), + Literal(dialect, 1).as_('depth'), + ], + from_=aml_base_join, + where=WhereClause(dialect, condition=Column(dialect, 'target_account_id', table='tx') == Literal(dialect, 1)), +) + +# --- Recursive query: trace upstream along the transaction chain --- +aml_recursive_join = JoinExpression( + dialect=dialect, + left_table=TableExpression(dialect, 'fund_trace', alias='tr'), + right_table=TableExpression(dialect, 'transactions', alias='tx'), + join_type='INNER JOIN', + condition=Column(dialect, 'target_account_id', table='tx') == Column(dialect, 'id', table='tr'), +).inner_join( + right_table=TableExpression(dialect, 'accounts', alias='a'), + condition=Column(dialect, 'source_account_id', table='tx') == Column(dialect, 'id', table='a'), +) + +aml_recursive = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, 'id', table='a'), + Column(dialect, 'account_holder', table='a'), + Column(dialect, 'account_type', table='a'), + Column(dialect, 'amount', table='tx'), + FunctionCall(dialect, '+', Column(dialect, 'depth', table='tr'), Literal(dialect, 1)).as_('depth'), + ], + from_=aml_recursive_join, + where=WhereClause(dialect, condition=Column(dialect, 'depth', table='tr') < Literal(dialect, 5)), +) + +# --- Combine with UNION ALL --- +aml_union = SetOperationExpression( + dialect=dialect, + left=aml_base, + right=aml_recursive, + operation='UNION ALL', +) + +# --- Define CTE --- +fund_trace_cte = CTEExpression( + dialect=dialect, + name='fund_trace', + query=aml_union, + columns=['id', 'account_holder', 'account_type', 'amount', 'depth'], +) + +# --- Main query: list the full fund chain --- +aml_query = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, 'id'), + Column(dialect, 'account_holder'), + Column(dialect, 'account_type'), + Column(dialect, 'amount'), + Column(dialect, 'depth'), + ], + from_=TableExpression(dialect, 'fund_trace'), + where=WhereClause(dialect, condition=Column(dialect, 'depth') >= Literal(dialect, 1)), + order_by=OrderByClause(dialect, [Column(dialect, 'depth'), Column(dialect, 'id')]), +) + +aml_with = WithQueryExpression( + dialect=dialect, + ctes=[fund_trace_cte], + main_query=aml_query, + recursive=True, +) + +sql, params = aml_with.to_sql() +print(f"\nGenerated SQL:\n{sql}\n") +result = backend.execute(sql, params, options=dql_options) +print("Fund source tracing (upstream chain for Alice Smith / ACC-001):") +print(f"{'depth':<6} {'Holder':<20} {'Type':<12} {'Amount':>12}") +print("-" * 52) +for row in result.data or []: + print(f" {row['depth']:<4} {row['account_holder']:<20} {row['account_type']:<12} {float(row['amount']):>10,.0f}") + +# --- Aggregation query: total suspicious amount per depth level --- +agg_query = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, 'depth'), + FunctionCall(dialect, 'COUNT', Column(dialect, 'id'), alias='account_count'), + FunctionCall(dialect, 'SUM', Column(dialect, 'amount'), alias='total_amount'), + ], + from_=TableExpression(dialect, 'fund_trace'), + order_by=OrderByClause(dialect, [Column(dialect, 'depth')]), +) + +agg_with = WithQueryExpression( + dialect=dialect, + ctes=[fund_trace_cte], + main_query=agg_query, + recursive=True, +) + +sql, params = agg_with.to_sql() +result = backend.execute(sql, params, options=dql_options) +print("\nAggregation: total suspicious amount per depth level") +print(f"{'depth':<8} {'Accounts':<10} {'Total Amount':>14}") +print("-" * 34) +for row in result.data or []: + print(f" {row['depth']:<6} {row['account_count']:<10} {float(row['total_amount']):>10,.0f}") + +# ============================================================ +# SECTION: Summary (key technical points) +# ============================================================ +print() +print("=" * 72) +print("Summary / Key Technical Points") +print("=" * 72) +print(""" +1. SetOperationExpression(operation='UNION ALL') combines base + recursive queries +2. CTEExpression(name=..., query=..., columns=...) defines the CTE column signature +3. WithQueryExpression(recursive=True) generates WITH RECURSIVE +4. Recursive query joins the CTE reference with physical tables via chained JoinExpression +5. depth field simulates the hop count of quantified path patterns +6. WHERE depth BETWEEN x AND y simulates {x,y} range constraint +7. Use cases: social recommendations, fund tracing, knowledge graph traversal +""") + +# ============================================================ +# SECTION: Teardown (reference only — cleanup resources) +# ============================================================ +for t in ("transactions", "accounts", "follows", "users"): + sql, params = DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql() + backend.execute(sql, params) +backend.disconnect() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/property_graph_query.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/property_graph_query.py index 8af4c289..806bd957 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/property_graph_query.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/property_graph_query.py @@ -5,6 +5,11 @@ - GRAPH_TABLE (MATCH ... COLUMNS ...) - CREATE / DROP / ALTER PROPERTY GRAPH - Vertex/edge pattern matching with WHERE + +Known PG 19 PGQ limitations (not supported): +- Variable-length (quantified) path patterns (``+``, ``*``, ``{n,m}``) +- Comma-separated multiple patterns in a single MATCH clause +- Heterogeneous graphs require explicit edge label specification """ @@ -18,6 +23,16 @@ def supports_graph_match(self) -> bool: """MATCH clause is supported since PostgreSQL 19.""" return self.version >= (19, 0, 0) + def supports_quantified_path(self) -> bool: + """Quantified (variable-length) path patterns are **not** supported + in PostgreSQL 19.""" + return False + + def supports_comma_separated_patterns(self) -> bool: + """Comma-separated multi-pattern MATCH is **not** supported + in PostgreSQL 19.""" + return False + def supports_graph_table(self) -> bool: """GRAPH_TABLE expression is supported since PostgreSQL 19.""" return self.version >= (19, 0, 0) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py index bb41a98e..73c155f6 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py @@ -4,7 +4,7 @@ from __future__ import annotations import re -from typing import Tuple, Tuple, TYPE_CHECKING +from typing import Tuple, TYPE_CHECKING from rhosocial.activerecord.backend.dialect.mixins.ddl_type import DDLTypeMixin from rhosocial.activerecord.backend.dialect.protocols import DDLTypeSupport @@ -471,7 +471,7 @@ def format_data_type_json_b(self, data_type: JsonBType) -> Tuple[str, tuple]: re.IGNORECASE, ) _PG_STRING_TYPES = re.compile( - r"^(?:CHAR|VARCHAR|TEXT)\b", + r"^(?:CHARACTER|CHAR|VARCHAR|TEXT)\b", re.IGNORECASE, ) _PG_BINARY_TYPES = re.compile( @@ -479,7 +479,7 @@ def format_data_type_json_b(self, data_type: JsonBType) -> Tuple[str, tuple]: re.IGNORECASE, ) _PG_DATE_TYPES = re.compile( - r"^(?:DATE|DATETIME|TIMESTAMP|TIME|INTERVAL)\b", + r"^(?:DATETIME|DATE|TIMESTAMPTZ|TIMESTAMP|TIMETZ|TIME|INTERVAL)\b", re.IGNORECASE, ) _PG_JSON_TYPES = re.compile( @@ -560,7 +560,7 @@ def parse_type(self, raw: str) -> DataType: # Serial family if self._PG_SERIAL_TYPES.match(upper): - from ..expression.types import ( + from ...expression.types import ( PostgresBigSerialType, PostgresSerialType, PostgresSmallSerialType, @@ -610,15 +610,15 @@ def parse_type(self, raw: str) -> DataType: # Binary if self._PG_BINARY_TYPES.match(upper): - from ..expression.types import PostgresByteaType + from ...expression.types import PostgresByteaType return PostgresByteaType() # Date/time if self._PG_DATE_TYPES.match(upper): - if upper.startswith("DATE"): - return DateType() if upper.startswith("DATETIME"): return DateTimeType() + if upper.startswith("DATE"): + return DateType() if upper.startswith("TIMESTAMP"): nums = re.findall(r"\d+", stripped) precision = int(nums[0]) if nums else None @@ -641,13 +641,13 @@ def parse_type(self, raw: str) -> DataType: if upper.startswith("JSONB"): return JsonBType() if upper.startswith("JSONPATH"): - from ..expression.types import PostgresJsonPathType + from ...expression.types import PostgresJsonPathType return PostgresJsonPathType() return JsonType() # UUID if self._PG_UUID_TYPES.match(upper): - from ..expression.types import PostgresUUIDType + from ...expression.types import PostgresUUIDType return PostgresUUIDType() # Boolean @@ -659,14 +659,14 @@ def parse_type(self, raw: str) -> DataType: nums = re.findall(r"\d+", stripped) n = int(nums[0]) if nums else None if upper.startswith("BIT"): - from ..expression.types import PostgresBitType + from ...expression.types import PostgresBitType return PostgresBitType(n) - from ..expression.types import PostgresVarBitType + from ...expression.types import PostgresVarBitType return PostgresVarBitType(n) # Network address if self._PG_NET_TYPES.match(upper): - from ..expression.types import ( + from ...expression.types import ( PostgresCidrType, PostgresInetType, PostgresMacAddr8Type, @@ -682,7 +682,7 @@ def parse_type(self, raw: str) -> DataType: # Geometric if self._PG_GEOM_TYPES.match(upper): - from ..expression.types import ( + from ...expression.types import ( PostgresBoxType, PostgresCircleType, PostgresLineSegmentType, @@ -707,7 +707,7 @@ def parse_type(self, raw: str) -> DataType: # Range types if self._PG_RANGE_TYPES.match(upper): - from ..expression.types import ( + from ...expression.types import ( PostgresDateRangeType, PostgresInt4RangeType, PostgresInt8RangeType, @@ -730,7 +730,7 @@ def parse_type(self, raw: str) -> DataType: # Multirange types if self._PG_MULTIRANGE_TYPES.match(upper): - from ..expression.types import ( + from ...expression.types import ( PostgresDateMultirangeType, PostgresInt4MultirangeType, PostgresInt8MultirangeType, @@ -753,7 +753,7 @@ def parse_type(self, raw: str) -> DataType: # OID types if self._PG_OID_TYPES.match(upper): - from ..expression.types import ( + from ...expression.types import ( PostgresCIDType, PostgresOIDType, PostgresRegClassType, @@ -778,7 +778,7 @@ def parse_type(self, raw: str) -> DataType: # Text search if self._PG_TS_TYPES.match(upper): - from ..expression.types import ( + from ...expression.types import ( PostgresTSQueryType, PostgresTSVectorType, ) @@ -788,7 +788,7 @@ def parse_type(self, raw: str) -> DataType: # Miscellaneous if self._PG_MISC_TYPES.match(upper): - from ..expression.types import ( + from ...expression.types import ( PostgresGeographyType, PostgresGeometryType, PostgresHstoreType, @@ -813,7 +813,7 @@ def parse_type(self, raw: str) -> DataType: if upper.startswith("VECTOR"): nums = re.findall(r"\d+", stripped) dim = int(nums[0]) if nums else 0 - from ..expression.types import PostgresVectorType + from ...expression.types import PostgresVectorType return PostgresVectorType(dim) # Fallback diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/upsert.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/upsert.py index b5c4140b..31612d0e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/upsert.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/upsert.py @@ -10,3 +10,10 @@ class PostgresUpsertMixin: def supports_upsert(self) -> bool: return self.version >= (9, 5, 0) + + def supports_on_conflict_clause(self) -> bool: + return True + + def supports_multiple_on_conflict_clauses(self) -> bool: + """PostgreSQL grammar allows only a single ON CONFLICT clause per INSERT.""" + return False diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/upsert.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/upsert.py index a6e144e4..a2ca5175 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/upsert.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/upsert.py @@ -8,4 +8,8 @@ class PostgresUpsertSupport(Protocol): """PostgreSQL upsert feature support protocol.""" - def supports_upsert(self)-> bool: ... + def supports_upsert(self) -> bool: ... + + def supports_on_conflict_clause(self) -> bool: ... + + def supports_multiple_on_conflict_clauses(self) -> bool: ... diff --git a/tests/benchmark/activerecord_bulk/test_bulk_create.py b/tests/benchmark/activerecord_bulk/test_bulk_create.py index 6713f01e..8abfa754 100644 --- a/tests/benchmark/activerecord_bulk/test_bulk_create.py +++ b/tests/benchmark/activerecord_bulk/test_bulk_create.py @@ -1,3 +1,5 @@ # Bridge file for bulk_create benchmark tests from the testsuite. -from rhosocial.activerecord.testsuite.benchmark.crud.test_bulk_create_sync import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.benchmark.crud.test_bulk_create import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.benchmark.crud.test_bulk_create_async import * # noqa: F403 from rhosocial.activerecord.testsuite.benchmark.crud.test_bulk_create_async import * # noqa: F401, F403 + diff --git a/tests/benchmark/relation/__init__.py b/tests/benchmark/relation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/benchmark/relation/conftest.py b/tests/benchmark/relation/conftest.py new file mode 100644 index 00000000..cce2ecbd --- /dev/null +++ b/tests/benchmark/relation/conftest.py @@ -0,0 +1,158 @@ +"""Benchmark fixtures for relation cache comparison. + +Bridges ``user_post_comment_classes`` from the testsuite's relation conftest +so that benchmarks can reuse the same model definitions and backend setup. + +Redis benchmark support is not introduced in this release. The related +source snippets are kept as comments for follow-up external cache design. +""" + +from rhosocial.activerecord.testsuite.feature.relation.conftest import ( # noqa: F401 + user_post_comment_classes, +) + +from dataclasses import dataclass +from typing import Any + +import pytest + +from rhosocial.activerecord.relation.cache import CacheConfig, InstanceCache +from rhosocial.activerecord.relation.cache_backends import InMemoryCache + +# Not introduced in this release; keep snippets for follow-up external cache design. +# from rhosocial.activerecord.relation.cache_backends import CacheSerializer, RedisCache + +pytestmark = [ + pytest.mark.benchmark, + pytest.mark.benchmark_relation, +] + + +def pytest_configure(config): + config.addinivalue_line("markers", "benchmark_relation: relation cache benchmark tests") + + +@dataclass +class RelationBenchmarkContext: + """Context for a single benchmark run.""" + + user: Any + post_class: Any + scenario: str + + +# --------------------------------------------------------------------------- +# Redis benchmark is not introduced in this release. Keep the fixture shape as +# comments for follow-up external cache design instead of deleting the source. +# --------------------------------------------------------------------------- +# @pytest.fixture(scope="session") +# def _redis_client(): +# redis = pytest.importorskip("redis") +# try: +# from providers.redis_scenarios import get_redis_scenario +# +# config = get_redis_scenario("benchmark") +# except (FileNotFoundError, KeyError): +# from rhosocial.activerecord.relation.cache_backends import RedisConfig +# +# config = RedisConfig.from_env() +# try: +# client = redis.Redis( +# host=config.host, +# port=config.port, +# password=config.password or None, +# db=config.db, +# socket_connect_timeout=config.socket_connect_timeout, +# ) +# client.ping() +# yield client +# client.flushdb() +# client.close() +# except Exception: +# pytest.skip("Redis not available for benchmark") +# yield None + + +# --------------------------------------------------------------------------- +# Shared model + data setup (function-scoped so each test gets fresh data) +# --------------------------------------------------------------------------- +@pytest.fixture(scope="function") +def _raw_models(user_post_comment_classes): # noqa: F811 + """Create a user + 100 posts + 200 comments under the same user.""" + user_cls, post_cls, comment_cls = user_post_comment_classes + user = user_cls(name="Bench User", email="bench@example.com") + user.save() + + for i in range(100): + p = post_cls(title=f"Post {i}", body=f"Body {i}", user_id=user.id) + p.save() + + yield user, post_cls, comment_cls + + for p in reversed(post_cls.find_all()): + p.delete() + user.delete() + + +# --------------------------------------------------------------------------- +# Cache scenario parametrization +# --------------------------------------------------------------------------- +CACHE_SCENARIOS = [ + pytest.param("no_cache", id="no_cache"), + pytest.param("in_memory", id="in_memory"), + # Not introduced in this release; keep for follow-up external cache design. + # pytest.param("redis", id="redis"), +] + + +@pytest.fixture(params=CACHE_SCENARIOS) +def cache_scenario(request): + """Parametrized cache scenario name.""" + return request.param + + +@pytest.fixture(scope="function") +def relation_benchmark_env(cache_scenario, _raw_models): + """Configure the InstanceCache backend and return a benchmark context. + + Each test function receives a ``RelationBenchmarkContext`` with: + - user — the seeded user instance + - post_class — for queries + - scenario — current scenario name + """ + user, post_cls, comment_cls = _raw_models + original_backend = InstanceCache._backend + + try: + if cache_scenario == "no_cache": + InstanceCache.set_backend(InMemoryCache()) + config = CacheConfig(enabled=False) + elif cache_scenario == "in_memory": + InstanceCache.set_backend(InMemoryCache()) + config = CacheConfig(enabled=True, ttl=None) + # Not introduced in this release; keep for follow-up external cache design. + # elif cache_scenario == "redis": + # InstanceCache.set_backend( + # RedisCache( + # client=_redis_client, + # prefix="bench:cache:", + # serializer=CacheSerializer(format="pickle"), + # ) + # ) + # config = CacheConfig(enabled=True, ttl=None) + else: + raise ValueError(f"Unknown scenario: {cache_scenario}") + + # Apply the cache config to all relation descriptors + for rel_name in ["posts", "comments"]: + rel = user.get_relation(rel_name) + if rel is not None: + rel._cache_config = config + + yield RelationBenchmarkContext( + user=user, + post_class=post_cls, + scenario=cache_scenario, + ) + finally: + InstanceCache.set_backend(original_backend) diff --git a/tests/benchmark/relation/test_cache_comparison.py b/tests/benchmark/relation/test_cache_comparison.py new file mode 100644 index 00000000..364b48af --- /dev/null +++ b/tests/benchmark/relation/test_cache_comparison.py @@ -0,0 +1,75 @@ +"""Benchmark comparing relation cache strategies. + +Measures HasMany relation access time under current-release scenarios: +- ``no_cache`` — CacheConfig(enabled=False), always queries DB +- ``in_memory`` — InMemoryCache, caches after first access + +Redis benchmark support is not introduced in this release. +""" + +import pytest + +pytestmark = [ + pytest.mark.benchmark, + pytest.mark.benchmark_relation, +] + + +class TestRelationFirstAccess: + """First access — always queries DB regardless of cache strategy.""" + + def test_posts_first_access(self, benchmark, relation_benchmark_env): + ctx = relation_benchmark_env + result = benchmark(ctx.user.posts) + assert len(result) == 100 + + +class TestRelationCachedAccess: + """Second+ access — cache hit (except no_cache which hits DB each time).""" + + def test_posts_second_access(self, benchmark, relation_benchmark_env): + ctx = relation_benchmark_env + ctx.user.posts() + result = benchmark(ctx.user.posts) + assert len(result) == 100 + + +class TestRelationRepeatedAccess: + """100 repeated accesses — amplifies cache vs no-cache gap.""" + + def test_posts_repeated_access(self, benchmark, relation_benchmark_env): + ctx = relation_benchmark_env + + def repeated(): + for _ in range(100): + ctx.user.posts() + + benchmark(repeated) + + +class TestRelationWithNewData: + """Access with new data — validates behaviour after mutations.""" + + def test_posts_after_new_record(self, benchmark, relation_benchmark_env): + """After a new record is added, cached backends return stale data. + + This is expected — cache invalidation is handled by model save/delete + events, not by the descriptor layer itself. + """ + ctx = relation_benchmark_env + ctx.user.posts() + ctx.post_class(title="New", body="New", user_id=ctx.user.id).save() + result = benchmark(ctx.user.posts) + if ctx.scenario == "no_cache": + assert len(result) == 101 + else: + assert len(result) == 100 # stale cache + + def test_posts_after_delete(self, benchmark, relation_benchmark_env): + ctx = relation_benchmark_env + ctx.user.posts() + p = ctx.post_class(title="Temp", body="Temp", user_id=ctx.user.id) + p.save() + p.delete() + result = benchmark(ctx.user.posts) + assert len(result) == 100 diff --git a/tests/providers/basic.py b/tests/providers/basic.py index ab385dbb..0889f99a 100644 --- a/tests/providers/basic.py +++ b/tests/providers/basic.py @@ -13,18 +13,23 @@ import os import sys import logging -from typing import Type, List, Tuple, Optional # noqa: F401 +from typing import Type, List, Tuple, Optional, Set # noqa: F401 logger = logging.getLogger(__name__) from rhosocial.activerecord.model import ActiveRecord # noqa: E402 from rhosocial.activerecord.backend.type_adapter import BaseSQLTypeAdapter # noqa: E402 -from rhosocial.activerecord.testsuite.feature.basic.interfaces import IBasicProvider # noqa: E402 +from rhosocial.activerecord.backend.options import ExecutionOptions, StatementType # noqa: E402 +from rhosocial.activerecord.testsuite.feature.basic.interfaces import IBasicSyncProvider, IBasicAsyncProvider # noqa: E402 from rhosocial.activerecord.testsuite.core.protocols import WorkerTestProtocol # noqa: E402 # Import the fixture selector utility from rhosocial.activerecord.testsuite.utils import select_fixture # noqa: E402 +# Expression-based DDL fixtures +from providers.fixtures._common import drop_table +from providers.fixtures.basic import TABLE_EXPRESSIONS as BASIC_TABLE_EXPRESSIONS + # Import base version models (Python 3.8+) from rhosocial.activerecord.testsuite.feature.basic.fixtures.models import ( # noqa: E402 User as UserBase, TypeCase as TypeCaseBase, ValidatedFieldUser as ValidatedFieldUserBase, @@ -179,144 +184,96 @@ def _select_model_class(base_cls, py312_cls, py311_cls, py310_cls, model_name: s from .scenarios import get_enabled_scenarios, get_scenario # noqa: E402 -class BasicProvider(IBasicProvider, WorkerTestProtocol): - """ - This is the postgres backend's implementation for the basic features test group. - It connects the generic tests in the testsuite with the actual postgres database. - """ - +class BasicProviderBase: def __init__(self): - self._active_backends = [] - self._active_async_backends = [] + self._scenario_db_files = {} + self._created_tables: Set[str] = set() def get_test_scenarios(self) -> List[str]: - """Returns a list of names for all enabled scenarios for this backend.""" return list(get_enabled_scenarios().keys()) - def _track_backend(self, backend_instance, collection: List) -> None: + def get_yes_no_adapter(self) -> 'BaseSQLTypeAdapter': + return YesOrNoBooleanAdapter() + + def _track_backend(self, backend_instance, collection) -> None: if backend_instance not in collection: collection.append(backend_instance) + def _load_postgres_schema(self, filename: str) -> str: + schema_dir = os.path.join( + os.path.dirname(__file__), "..", "rhosocial", "activerecord_postgres_test", "feature", "basic", "schema" + ) + schema_path = os.path.join(schema_dir, filename) + with open(schema_path, 'r', encoding='utf-8') as f: + return f.read() + + +class BasicSyncProvider(BasicProviderBase, IBasicSyncProvider, WorkerTestProtocol): + + def __init__(self): + super().__init__() + self._active_backends = [] + def _setup_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str) -> Type[ActiveRecord]: - """A generic helper method to handle the setup for any given model.""" backend_class, config = get_scenario(scenario_name) model_class.configure(config, backend_class) - backend_instance = model_class.__backend__ self._track_backend(backend_instance, self._active_backends) - self._reset_table_sync(model_class, table_name) - return model_class - - async def _setup_async_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str) -> Type[ActiveRecord]: # noqa: E501 - """A generic helper method to handle the setup for any given async model.""" - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend - - _, config = get_scenario(scenario_name) - await model_class.configure(config, AsyncPostgresBackend) - - backend_instance = model_class.__backend__ - self._track_backend(backend_instance, self._active_async_backends) - - await self._reset_table_async(model_class, table_name) + self._created_tables.add(table_name) return model_class def _reset_table_sync(self, model_class: Type[ActiveRecord], table_name: str) -> None: + opts = ExecutionOptions(stmt_type=StatementType.DDL) try: - model_class.__backend__.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') - except Exception as e: - print(f"Could not drop table {table_name}: {e}") - - schema_sql = self._load_postgres_schema(f"{table_name}.sql") - model_class.__backend__.execute(schema_sql) - - async def _reset_table_async(self, model_class: Type[ActiveRecord], table_name: str) -> None: - try: - await model_class.__backend__.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') + sql, params = drop_table(model_class.__backend__.dialect, table_name).to_sql() + model_class.__backend__.execute(sql, params, options=opts) except Exception as e: print(f"Could not drop table {table_name}: {e}") - - schema_sql = self._load_postgres_schema(f"{table_name}.sql") - await model_class.__backend__.execute(schema_sql) + if fn := BASIC_TABLE_EXPRESSIONS.get(table_name): + create_expr = fn(model_class.__backend__.dialect, table_name) + sql, params = create_expr.to_sql() + model_class.__backend__.execute(sql, params, options=opts) def _initialize_model_schema(self, model_class: Type[ActiveRecord], table_name: str) -> None: - """Initialize schema for a model that shares backend with another model.""" self._reset_table_sync(model_class, table_name) - async def _initialize_async_model_schema(self, model_class: Type[ActiveRecord], table_name: str) -> None: - """Initialize schema for an async model that shares backend with another model.""" - await self._reset_table_async(model_class, table_name) - - def _setup_multiple_models(self, model_classes: List[Tuple[Type[ActiveRecord], str]], scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: # noqa: E501 - """Helper to set up multiple related models for a test, sharing a single backend.""" + def _setup_multiple_models(self, model_classes: List[Tuple[Type[ActiveRecord], str]], scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: if not model_classes: return tuple() - first_model_class, first_table_name = model_classes[0] first_model = self._setup_model(first_model_class, scenario_name, first_table_name) shared_backend = first_model.__backend__ - result = [first_model] - for model_class, table_name in model_classes[1:]: model_class.__connection_config__ = first_model.__connection_config__ model_class.__backend_class__ = first_model.__backend_class__ model_class.__backend__ = shared_backend self._track_backend(shared_backend, self._active_backends) self._initialize_model_schema(model_class, table_name) + self._created_tables.add(table_name) result.append(model_class) - return tuple(result) - async def _setup_multiple_models_async(self, model_classes: List[Tuple[Type[ActiveRecord], str]], scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: # noqa: E501 - """Helper to set up multiple related async models for a test, sharing a single backend.""" - if not model_classes: - return tuple() - - first_model_class, first_table_name = model_classes[0] - first_model = await self._setup_async_model(first_model_class, scenario_name, first_table_name) - shared_backend = first_model.__backend__ - - result = [first_model] - - for model_class, table_name in model_classes[1:]: - model_class.__connection_config__ = first_model.__connection_config__ - model_class.__backend_class__ = first_model.__backend_class__ - model_class.__backend__ = shared_backend - self._track_backend(shared_backend, self._active_async_backends) - await self._initialize_async_model_schema(model_class, table_name) - result.append(model_class) - - return tuple(result) - - # --- Implementation of the IBasicProvider interface --- - def setup_user_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for user model tests.""" return self._setup_model(User, scenario_name, "users") def setup_type_case_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for type case model tests.""" return self._setup_model(TypeCase, scenario_name, "type_cases") def setup_type_test_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for type test model tests.""" return self._setup_model(TypeTestModel, scenario_name, "type_tests") def setup_validated_field_user_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for validated field user model tests.""" return self._setup_model(ValidatedFieldUser, scenario_name, "validated_field_users") def setup_validated_user_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for validated user model tests.""" return self._setup_model(ValidatedUser, scenario_name, "validated_users") def setup_pydantic_validated_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for Pydantic native validation tests.""" return self._setup_model(PydanticValidatedModel, scenario_name, "pydantic_validated_models") - def setup_mapped_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for MappedUser, MappedPost, and MappedComment models.""" + def setup_mapped_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: return self._setup_multiple_models([ (MappedUser, "users"), (MappedPost, "posts"), @@ -324,7 +281,6 @@ def setup_mapped_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], T ], scenario_name) def setup_mixed_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: - """Sets up the database for ColumnMappingModel and MixedAnnotationModel.""" from rhosocial.activerecord_postgres_test.feature.basic.fixtures.models import PostgresMixedAnnotationModel return self._setup_multiple_models([ (ColumnMappingModel, "column_mapping_items"), @@ -332,161 +288,40 @@ def setup_mixed_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], .. ], scenario_name) def setup_type_adapter_model_and_schema(self, scenario_name: str = None) -> Type[ActiveRecord]: - """Sets up the database for the `TypeAdapterTest` model tests.""" if scenario_name is None: scenario_name = self.get_test_scenarios()[0] if self.get_test_scenarios() else "default" return self._setup_model(TypeAdapterTest, scenario_name, "type_adapter_tests") - async def setup_async_type_adapter_model_and_schema(self, scenario_name: str = None) -> Type[ActiveRecord]: - """Sets up the database for the `AsyncTypeAdapterTest` model tests.""" - if scenario_name is None: - scenario_name = self.get_test_scenarios()[0] if self.get_test_scenarios() else "default" - return await self._setup_async_model(AsyncTypeAdapterTest, scenario_name, "type_adapter_tests") - def setup_bulk_user_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for the `BulkUser` model tests.""" return self._setup_model(BulkUser, scenario_name, "bulk_users") - async def setup_async_bulk_user_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for the `AsyncBulkUser` model tests.""" - return await self._setup_async_model(AsyncBulkUser, scenario_name, "bulk_users") + def setup_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError - def get_yes_no_adapter(self) -> 'BaseSQLTypeAdapter': - """Returns an instance of the YesOrNoBooleanAdapter.""" - return YesOrNoBooleanAdapter() + def setup_order_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError - # --- Async implementations --- + def setup_mapped_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError - async def setup_async_user_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for async user model tests.""" - return await self._setup_async_model(AsyncUser, scenario_name, "users") + def setup_product_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError - async def setup_async_type_case_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for async type case model tests.""" - return await self._setup_async_model(AsyncTypeCase, scenario_name, "type_cases") - - async def setup_async_type_test_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for async type test model tests.""" - return await self._setup_async_model(AsyncTypeTestModel, scenario_name, "type_tests") - - async def setup_async_validated_field_user_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for async validated field user model tests.""" - return await self._setup_async_model(AsyncValidatedFieldUser, scenario_name, "validated_field_users") - - async def setup_async_validated_user_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for async validated user model tests.""" - return await self._setup_async_model(AsyncValidatedUser, scenario_name, "validated_users") - - async def setup_async_pydantic_validated_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for async Pydantic native validation tests.""" - return await self._setup_async_model(AsyncPydanticValidatedModel, scenario_name, "pydantic_validated_models") - - async def setup_async_mapped_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for AsyncMappedUser, AsyncMappedPost, and AsyncMappedComment models.""" - return await self._setup_multiple_models_async([ - (AsyncMappedUser, "users"), - (AsyncMappedPost, "posts"), - (AsyncMappedComment, "comments") - ], scenario_name) - - async def setup_async_mixed_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: - """Sets up the database for AsyncColumnMappingModel and AsyncMixedAnnotationModel.""" - from rhosocial.activerecord_postgres_test.feature.basic.fixtures.models import AsyncPostgresMixedAnnotationModel - return await self._setup_multiple_models_async([ - (AsyncColumnMappingModel, "column_mapping_items"), - (AsyncPostgresMixedAnnotationModel, "mixed_annotation_items") - ], scenario_name) - - async def cleanup_after_test_async(self, scenario_name: str): - """ - Performs async cleanup after a test, dropping all tables and disconnecting async backends. - """ - tables_to_drop = [ - 'users', 'type_cases', 'type_tests', 'validated_field_users', - 'validated_users', 'pydantic_validated_models', 'type_adapter_tests', - 'posts', 'comments', 'column_mapping_items', 'mixed_annotation_items' - ] - for backend_instance in self._active_async_backends: - try: - for table_name in tables_to_drop: - try: - await backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') - except Exception: - pass - finally: - try: - await backend_instance.disconnect() - except: # noqa: E722 - pass - - self._active_async_backends.clear() - - def _load_postgres_schema(self, filename: str) -> str: - """Helper to load a SQL schema file from this project's fixtures.""" - # Schemas are stored in the centralized location for basic feature. - schema_dir = os.path.join(os.path.dirname(__file__), "..", "rhosocial", "activerecord_postgres_test", "feature", "basic", "schema") # noqa: E501 - schema_path = os.path.join(schema_dir, filename) - - with open(schema_path, 'r', encoding='utf-8') as f: - return f.read() - - def cleanup_after_test(self, scenario_name: str): - """ - Performs cleanup after a test. This now iterates through the backends - that were created during setup, drops tables, and explicitly disconnects them. - """ - tables_to_drop = [ - 'users', 'type_cases', 'type_tests', 'validated_field_users', - 'validated_users', 'pydantic_validated_models', 'type_adapter_tests', - 'posts', 'comments', 'column_mapping_items', 'mixed_annotation_items' - ] - for backend_instance in self._active_backends: - try: - # Drop all tables that might have been created for basic tests - for table_name in tables_to_drop: - try: - backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') - except Exception: - # Continue with other tables if one fails - pass - finally: - # Always disconnect the backend instance that was used in the test - try: - backend_instance.disconnect() - except: # noqa: E722 - # Ignore errors during disconnect - pass + def setup_product_form_a_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError - # Clear the list of active backends for the next test - self._active_backends.clear() + def setup_product_with_proxy_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError - # --- Implementation of WorkerTestProtocol --- + def setup_product_with_column_and_adapter_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError def get_worker_connection_params(self, scenario_name: str, fixture_type: str = None) -> dict: - """ - Return serializable connection parameters for Worker processes. - - This method provides all information needed to recreate the database - connection in a Worker process, including the schema SQL for table creation. - - Args: - scenario_name: The test scenario name - fixture_type: Optional fixture type hint (e.g., 'user', 'async_user') - Used to determine if async backend is needed - - Returns: - Dictionary with connection parameters and schema SQL - """ from .scenarios import SCENARIO_MAP - - # Determine if async backend is needed based on fixture_type is_async = fixture_type and fixture_type.startswith('async_') backend_class_name = 'AsyncPostgresBackend' if is_async else 'PostgresBackend' - - # Determine schema file based on fixture_type - table_name = 'users' # default + table_name = 'users' if fixture_type: - # Remove 'async_' prefix if present to get the base type base_type = fixture_type.replace('async_', '') table_map = { 'user': 'users', @@ -498,16 +333,12 @@ def get_worker_connection_params(self, scenario_name: str, fixture_type: str = N 'type_adapter_test': 'type_adapter_tests', } table_name = table_map.get(base_type, 'users') - - # Get connection config from scenario if scenario_name not in SCENARIO_MAP: if SCENARIO_MAP: scenario_name = next(iter(SCENARIO_MAP)) else: raise ValueError("No scenarios registered") - config_dict = SCENARIO_MAP[scenario_name] - return { 'backend_module': 'rhosocial.activerecord.backend.impl.postgres', 'backend_class_name': backend_class_name, @@ -518,14 +349,144 @@ def get_worker_connection_params(self, scenario_name: str, fixture_type: str = N } def get_worker_schema_sql(self, scenario_name: str, table_name: str) -> str: - """ - Return the SQL statement to create a specific table. + return self._load_postgres_schema(f'{table_name}.sql') + + def cleanup_after_test(self, scenario_name: str): + for backend_instance in self._active_backends: + try: + for table_name in list(self._created_tables): + try: + backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') + except Exception: + pass + finally: + try: + backend_instance.disconnect() + except Exception: + pass + self._active_backends.clear() + self._created_tables.clear() - Args: - scenario_name: The test scenario name (unused for Postgres as schema is fixed) - table_name: Name of the table to create - Returns: - CREATE TABLE SQL statement - """ - return self._load_postgres_schema(f'{table_name}.sql') +class BasicAsyncProvider(BasicProviderBase, IBasicAsyncProvider): + + def __init__(self): + super().__init__() + self._active_async_backends = [] + + async def _setup_async_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str) -> Type[ActiveRecord]: + from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + _, config = get_scenario(scenario_name) + await model_class.configure(config, AsyncPostgresBackend) + backend_instance = model_class.__backend__ + self._track_backend(backend_instance, self._active_async_backends) + await self._reset_table_async(model_class, table_name) + self._created_tables.add(table_name) + return model_class + + async def _reset_table_async(self, model_class: Type[ActiveRecord], table_name: str) -> None: + opts = ExecutionOptions(stmt_type=StatementType.DDL) + try: + sql, params = drop_table(model_class.__backend__.dialect, table_name).to_sql() + await model_class.__backend__.execute(sql, params, options=opts) + except Exception as e: + print(f"Could not drop table {table_name}: {e}") + if fn := BASIC_TABLE_EXPRESSIONS.get(table_name): + create_expr = fn(model_class.__backend__.dialect, table_name) + sql, params = create_expr.to_sql() + await model_class.__backend__.execute(sql, params, options=opts) + + async def _initialize_async_model_schema(self, model_class: Type[ActiveRecord], table_name: str) -> None: + await self._reset_table_async(model_class, table_name) + + async def _setup_multiple_models_async(self, model_classes: List[Tuple[Type[ActiveRecord], str]], scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: + if not model_classes: + return tuple() + first_model_class, first_table_name = model_classes[0] + first_model = await self._setup_async_model(first_model_class, scenario_name, first_table_name) + shared_backend = first_model.__backend__ + result = [first_model] + for model_class, table_name in model_classes[1:]: + model_class.__connection_config__ = first_model.__connection_config__ + model_class.__backend_class__ = first_model.__backend_class__ + model_class.__backend__ = shared_backend + self._track_backend(shared_backend, self._active_async_backends) + await self._initialize_async_model_schema(model_class, table_name) + self._created_tables.add(table_name) + result.append(model_class) + return tuple(result) + + async def setup_user_model(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(AsyncUser, scenario_name, "users") + + async def setup_type_case_model(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(AsyncTypeCase, scenario_name, "type_cases") + + async def setup_type_test_model(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(AsyncTypeTestModel, scenario_name, "type_tests") + + async def setup_validated_field_user_model(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(AsyncValidatedFieldUser, scenario_name, "validated_field_users") + + async def setup_validated_user_model(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(AsyncValidatedUser, scenario_name, "validated_users") + + async def setup_pydantic_validated_model(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(AsyncPydanticValidatedModel, scenario_name, "pydantic_validated_models") + + async def setup_mapped_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: + return await self._setup_multiple_models_async([ + (AsyncMappedUser, "users"), + (AsyncMappedPost, "posts"), + (AsyncMappedComment, "comments") + ], scenario_name) + + async def setup_mixed_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: + from rhosocial.activerecord_postgres_test.feature.basic.fixtures.models import AsyncPostgresMixedAnnotationModel + return await self._setup_multiple_models_async([ + (AsyncColumnMappingModel, "column_mapping_items"), + (AsyncPostgresMixedAnnotationModel, "mixed_annotation_items") + ], scenario_name) + + async def setup_type_adapter_model_and_schema(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(AsyncTypeAdapterTest, scenario_name, "type_adapter_tests") + + async def setup_bulk_user_model(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(AsyncBulkUser, scenario_name, "bulk_users") + + async def setup_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError + + async def setup_order_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError + + async def setup_mapped_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError + + async def setup_product_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError + + async def setup_product_form_a_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError + + async def setup_product_with_proxy_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError + + async def setup_product_with_column_and_adapter_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError + + async def cleanup_after_test(self, scenario_name: str): + for backend_instance in self._active_async_backends: + try: + for table_name in list(self._created_tables): + try: + await backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') + except Exception: + pass + finally: + try: + await backend_instance.disconnect() + except Exception: + pass + self._active_async_backends.clear() + self._created_tables.clear() diff --git a/tests/providers/basic_connection.py b/tests/providers/basic_connection.py index ebcfa369..c15a6668 100644 --- a/tests/providers/basic_connection.py +++ b/tests/providers/basic_connection.py @@ -134,6 +134,10 @@ def cleanup_sync(self, scenario_name: str, pool: BackendPool): # Disconnect backends for backend in self._active_backends: + try: + backend.execute("DROP TABLE IF EXISTS test_users CASCADE") + except Exception: + pass try: backend.disconnect() except Exception: @@ -146,6 +150,10 @@ async def cleanup_async(self, scenario_name: str, pool: AsyncBackendPool): # Disconnect backends for backend in self._active_async_backends: + try: + await backend.execute("DROP TABLE IF EXISTS test_users CASCADE") + except Exception: + pass try: await backend.disconnect() except Exception: diff --git a/tests/providers/composite_pk.py b/tests/providers/composite_pk.py deleted file mode 100644 index a2f7f39f..00000000 --- a/tests/providers/composite_pk.py +++ /dev/null @@ -1,130 +0,0 @@ -# tests/providers/composite_pk.py -import os -from typing import Type, List - -from rhosocial.activerecord.model import ActiveRecord - -from rhosocial.activerecord.testsuite.feature.composite_pk.fixtures.models import ( - OrderItem as OrderItemBase, - StoreInventory as StoreInventoryBase, - Order as OrderBase, - MappedOrderItem as MappedOrderItemBase, -) -from rhosocial.activerecord.testsuite.feature.composite_pk.fixtures.models import ( - AsyncOrderItem as AsyncOrderItemBase, - AsyncStoreInventory as AsyncStoreInventoryBase, - AsyncOrder as AsyncOrderBase, - AsyncMappedOrderItem as AsyncMappedOrderItemBase, -) - -from .scenarios import get_enabled_scenarios, get_scenario - - -class CompositePKProvider: - def __init__(self): - self._active_backends = [] - self._active_async_backends = [] - - def get_test_scenarios(self) -> List[str]: - return list(get_enabled_scenarios().keys()) - - def _setup_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str) -> Type[ActiveRecord]: - backend_class, config = get_scenario(scenario_name) - model_class.configure(config, backend_class) - backend_instance = model_class.__backend__ - if backend_instance not in self._active_backends: - self._active_backends.append(backend_instance) - try: - backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') - except Exception: - pass - schema_sql = self._load_postgres_schema(f"{table_name}.sql") - backend_instance.execute(schema_sql) - return model_class - - async def _setup_async_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str) -> Type[ActiveRecord]: - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend - _, config = get_scenario(scenario_name) - await model_class.configure(config, AsyncPostgresBackend) - backend_instance = model_class.__backend__ - if backend_instance not in self._active_async_backends: - self._active_async_backends.append(backend_instance) - try: - await backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') - except Exception: - pass - schema_sql = self._load_postgres_schema(f"{table_name}.sql") - await backend_instance.execute(schema_sql) - return model_class - - def _load_postgres_schema(self, filename: str) -> str: - schema_dir = os.path.join( - os.path.dirname(__file__), - "..", - "rhosocial", - "activerecord_postgres_test", - "feature", - "composite_pk", - "schema", - ) - schema_path = os.path.join(schema_dir, filename) - with open(schema_path, "r", encoding="utf-8") as f: - return f.read() - - def setup_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: - return self._setup_model(OrderItemBase, scenario_name, "order_items") - - def setup_store_inventory_model(self, scenario_name: str) -> Type[ActiveRecord]: - return self._setup_model(StoreInventoryBase, scenario_name, "store_inventory") - - def setup_order_model(self, scenario_name: str) -> Type[ActiveRecord]: - return self._setup_model(OrderBase, scenario_name, "orders") - - async def setup_async_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: - return await self._setup_async_model(AsyncOrderItemBase, scenario_name, "order_items") - - async def setup_async_store_inventory_model(self, scenario_name: str) -> Type[ActiveRecord]: - return await self._setup_async_model(AsyncStoreInventoryBase, scenario_name, "store_inventory") - - async def setup_async_order_model(self, scenario_name: str) -> Type[ActiveRecord]: - return await self._setup_async_model(AsyncOrderBase, scenario_name, "orders") - - def setup_mapped_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: - return self._setup_model(MappedOrderItemBase, scenario_name, "order_items") - - async def setup_async_mapped_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: - return await self._setup_async_model(AsyncMappedOrderItemBase, scenario_name, "order_items") - - def cleanup_after_test(self, scenario_name: str): - tables = ["order_items", "store_inventory", "orders"] - for backend in self._active_backends: - try: - for t in tables: - try: - backend.execute(f'DROP TABLE IF EXISTS "{t}" CASCADE') - except Exception: - pass - except Exception: - pass - try: - backend.disconnect() - except Exception: - pass - self._active_backends.clear() - - async def cleanup_after_test_async(self, scenario_name: str): - tables = ["order_items", "store_inventory", "orders"] - for backend in self._active_async_backends: - try: - for t in tables: - try: - await backend.execute(f'DROP TABLE IF EXISTS "{t}" CASCADE') - except Exception: - pass - except Exception: - pass - try: - await backend.disconnect() - except Exception: - pass - self._active_async_backends.clear() diff --git a/tests/providers/crud_benchmark.py b/tests/providers/crud_benchmark.py index 1c35af46..675a8d14 100644 --- a/tests/providers/crud_benchmark.py +++ b/tests/providers/crud_benchmark.py @@ -164,6 +164,10 @@ def _schema_sql(self): def _cleanup_sync(self): for backend in self._active_backends: + try: + backend.execute("DROP TABLE IF EXISTS benchmark_users CASCADE") + except Exception: + pass try: backend.disconnect() except Exception: @@ -172,6 +176,10 @@ def _cleanup_sync(self): async def _cleanup_async(self): for backend in self._active_async_backends: + try: + await backend.execute("DROP TABLE IF EXISTS benchmark_users CASCADE") + except Exception: + pass try: await backend.disconnect() except Exception: diff --git a/tests/providers/events.py b/tests/providers/events.py index b27cecc5..0b11a7ca 100644 --- a/tests/providers/events.py +++ b/tests/providers/events.py @@ -26,7 +26,8 @@ # Import base version models (Python 3.8+) from rhosocial.activerecord.testsuite.feature.events.fixtures.models import ( # noqa: E402 EventTestModel as EventTestModelBase, - EventTrackingModel as EventTrackingModelBase + EventTrackingModel as EventTrackingModelBase, + AsyncEventTestModel as AsyncEventTestModelBase, ) # Conditionally import Python 3.10+ models @@ -78,95 +79,121 @@ def _select_model_class(base_cls, py312_cls, py311_cls, py310_cls, model_name: s # Select models EventTestModel = _select_model_class(EventTestModelBase, EventTestModel312, EventTestModel311, EventTestModel310, "EventTestModel") # noqa: E501 EventTrackingModel = _select_model_class(EventTrackingModelBase, EventTrackingModel312, EventTrackingModel311, EventTrackingModel310, "EventTrackingModel") # noqa: E501 +AsyncEventTestModel = AsyncEventTestModelBase -from rhosocial.activerecord.testsuite.feature.events.interfaces import IEventsProvider # noqa: E402 +from rhosocial.activerecord.backend.options import ExecutionOptions, StatementType # noqa: E402 +from rhosocial.activerecord.testsuite.feature.events.interfaces import IEventsSyncProvider, IEventsAsyncProvider # noqa: E402 # ...and the scenarios are defined specifically for this backend. from .scenarios import get_enabled_scenarios, get_scenario # noqa: E402 +# Expression-based DDL fixtures +from .fixtures._common import drop_table +from .fixtures.events import TABLE_EXPRESSIONS as EVENTS_TABLE_EXPRESSIONS -class EventsProvider(IEventsProvider): - """ - This is the postgres backend's implementation for the events test group. - It connects the generic tests in the testsuite with the actual postgres database. - """ +class EventsProviderBase: def __init__(self): - # This list will track the backend instances created during the setup phase. self._active_backends = [] def get_test_scenarios(self) -> List[str]: - """Returns a list of names for all enabled scenarios for this backend.""" return list(get_enabled_scenarios().keys()) + def _load_postgres_schema(self, filename: str) -> str: + schema_dir = os.path.join( + os.path.dirname(__file__), "..", "rhosocial", "activerecord_postgres_test", "feature", "events", "schema" + ) + schema_path = os.path.join(schema_dir, filename) + with open(schema_path, 'r', encoding='utf-8') as f: + return f.read() + + +class EventsSyncProvider(EventsProviderBase, IEventsSyncProvider): + + def __init__(self): + super().__init__() + def _setup_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str) -> Type[ActiveRecord]: - """A generic helper method to handle the setup for any given model.""" - # 1. Get the backend class (PostgresBackend) and connection config for the requested scenario. backend_class, config = get_scenario(scenario_name) - - # 2. Configure the generic model class with our specific backend and config. model_class.configure(config, backend_class) - backend_instance = model_class.__backend__ if backend_instance not in self._active_backends: self._active_backends.append(backend_instance) - - # 3. Prepare the database schema. + opts = ExecutionOptions(stmt_type=StatementType.DDL) try: - # Drop the table if it exists, with cascade to handle dependencies. - model_class.__backend__.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') + sql, params = drop_table(backend_instance.dialect, table_name).to_sql() + backend_instance.execute(sql, params, options=opts) except Exception as e: - # Ignore errors if the table didn't exist, but print for debugging. print(f"Could not drop table {table_name}: {e}") - pass - - # Execute the schema SQL to create the table. - schema_sql = self._load_postgres_schema(f"{table_name}.sql") - model_class.__backend__.execute(schema_sql) - + if fn := EVENTS_TABLE_EXPRESSIONS.get(table_name): + create_expr = fn(backend_instance.dialect, table_name) + sql, params = create_expr.to_sql() + backend_instance.execute(sql, params, options=opts) return model_class - # --- Implementation of the IEventsProvider interface --- - def setup_event_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for event model tests.""" - return self._setup_model(EventTestModel, scenario_name, "event_tests") def setup_event_tracking_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for event tracking model tests.""" - return self._setup_model(EventTrackingModel, scenario_name, "event_tracking_models") - def _load_postgres_schema(self, filename: str) -> str: - """Helper to load a SQL schema file from this project's fixtures.""" - # Schemas are stored in the centralized location for events feature. - schema_dir = os.path.join(os.path.dirname(__file__), "..", "rhosocial", "activerecord_postgres_test", "feature", "events", "schema") # noqa: E501 - schema_path = os.path.join(schema_dir, filename) - - with open(schema_path, 'r', encoding='utf-8') as f: - return f.read() - def cleanup_after_test(self, scenario_name: str): - """ - Performs cleanup after a test. This now iterates through the backends - that were created during setup, drops tables, and explicitly disconnects them. - """ for backend_instance in self._active_backends: try: - # Drop all tables that might have been created for events tests for table_name in ['event_tests', 'event_tracking_models']: try: backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') except Exception: - # Continue with other tables if one fails pass finally: - # Always disconnect the backend instance that was used in the test try: backend_instance.disconnect() - except: # noqa: E722 - # Ignore errors during disconnect + except Exception: pass + self._active_backends.clear() + + +class EventsAsyncProvider(EventsProviderBase, IEventsAsyncProvider): + + def __init__(self): + super().__init__() + self._active_async_backends = [] + + async def _setup_async_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str) -> Type[ActiveRecord]: + from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + _, config = get_scenario(scenario_name) + await model_class.configure(config, AsyncPostgresBackend) + backend_instance = model_class.__backend__ + if backend_instance not in self._active_async_backends: + self._active_async_backends.append(backend_instance) + opts = ExecutionOptions(stmt_type=StatementType.DDL) + try: + sql, params = drop_table(backend_instance.dialect, table_name).to_sql() + await backend_instance.execute(sql, params, options=opts) + except Exception as e: + print(f"Could not drop table {table_name}: {e}") + if fn := EVENTS_TABLE_EXPRESSIONS.get(table_name): + create_expr = fn(backend_instance.dialect, table_name) + sql, params = create_expr.to_sql() + await backend_instance.execute(sql, params, options=opts) + return model_class + + async def setup_event_model(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(AsyncEventTestModel, scenario_name, "event_tests") + + async def setup_event_tracking_model(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(EventTrackingModel, scenario_name, "event_tracking_models") - # Clear the list of active backends for the next test - self._active_backends.clear() \ No newline at end of file + async def cleanup_after_test(self, scenario_name: str): + for backend_instance in self._active_async_backends: + try: + for table_name in ['event_tests', 'event_tracking_models']: + try: + await backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') + except Exception: + pass + finally: + try: + await backend_instance.disconnect() + except Exception: + pass + self._active_async_backends.clear() diff --git a/tests/providers/fixtures/__init__.py b/tests/providers/fixtures/__init__.py new file mode 100644 index 00000000..70857923 --- /dev/null +++ b/tests/providers/fixtures/__init__.py @@ -0,0 +1,14 @@ +# tests/providers/fixtures/__init__.py +"""PostgreSQL-specific DDL expression fixtures for the testsuite providers. + +Each module in this package exposes a ``TABLE_EXPRESSIONS`` mapping of +table name -> factory callable ``Callable[[DialectLike, str], CreateTableExpression]``. + +The factory functions build :class:`CreateTableExpression` instances that emit +PostgreSQL-compatible DDL (SERIAL / UUID / JSONB / BYTEA / TIMESTAMPTZ / BOOLEAN / +DECIMAL / VARCHAR / TEXT / FK constraints / CHECK / UNIQUE / NOT NULL / DEFAULT). +The pre-existing ``.sql`` schema files under +``tests/rhosocial/activerecord_postgres_test/feature//schema/`` +remain as the authoritative reference for what the expressions here must +produce; they are simply no longer read at runtime by the providers. +""" diff --git a/tests/providers/fixtures/_common.py b/tests/providers/fixtures/_common.py new file mode 100644 index 00000000..d07e02b9 --- /dev/null +++ b/tests/providers/fixtures/_common.py @@ -0,0 +1,34 @@ +# tests/providers/fixtures/_common.py +"""Shared helpers for the PostgreSQL DDL expression fixtures. + +Provides the ``drop_table`` helper used by all feature providers. +""" + +from __future__ import annotations + +from typing import Tuple + +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, + DropTableExpression, + TableExpression, +) + + +def drop_table(dialect, table_name: str) -> DropTableExpression: + """Build a ``DROP TABLE IF EXISTS table_name CASCADE`` expression.""" + return DropTableExpression( + dialect=dialect, + table=TableExpression(dialect, table_name), + if_exists=True, + ) + + +def create_table_sql(expr: CreateTableExpression) -> Tuple[str, tuple]: + """Generate canonical PostgreSQL DDL for a :class:`CreateTableExpression`. + + For PostgreSQL no post-processing is required (unlike MySQL where storage + option quotes must be stripped). This pass-through wrapper exists so the + call site is the same as the MySQL counterpart. + """ + return expr.to_sql() diff --git a/tests/providers/fixtures/basic.py b/tests/providers/fixtures/basic.py new file mode 100644 index 00000000..aaea9129 --- /dev/null +++ b/tests/providers/fixtures/basic.py @@ -0,0 +1,499 @@ +# tests/providers/fixtures/basic.py +"""DDL expressions for the ``feature/basic`` table group (PostgreSQL). + +Each factory builds a :class:`CreateTableExpression` whose generated DDL is +semantically equivalent to the reference ``.sql`` schema files under +``tests/rhosocial/activerecord_postgres_test/feature/basic/schema/``. Those +``.sql`` files are kept as the authoritative reference and are no longer +loaded at runtime. +""" + +from typing import Callable, Dict + +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, +) +from rhosocial.activerecord.backend.expression.statements import ( + ColumnDefinition, + ColumnConstraint, + ColumnConstraintType, + ForeignKeyConstraint, + TableConstraint, + TableConstraintType, + ReferentialAction, +) +from rhosocial.activerecord.backend.expression.types import ( + ArrayType, + BigIntType, + BooleanType, + DateType, + DecimalType, + DoubleType, + FloatType, + IntegerType, + JsonBType, + SmallIntType, + TextType, + TimeTzType, + TimestampTzType, + VarCharType, +) +from rhosocial.activerecord.backend.impl.postgres import ( + PostgresSerialType, + PostgresUUIDType, + PostgresByteaType, +) + +_CASCADE = ReferentialAction.CASCADE + + +# --------------------------------------------------------------------------- +# basic/users.sql +# --------------------------------------------------------------------------- + +def create_users_table(dialect, table_name: str = "users") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("username", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("email", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("age", IntegerType()), + ColumnDefinition("balance", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0.00)]), + ColumnDefinition("is_active", BooleanType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=True)]), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + ) + + +# --------------------------------------------------------------------------- +# basic/type_cases.sql +# --------------------------------------------------------------------------- + +def create_type_cases_table(dialect, table_name: str = "type_cases") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresUUIDType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("username", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("email", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("tiny_int", SmallIntType()), + ColumnDefinition("small_int", SmallIntType()), + ColumnDefinition("big_int", BigIntType()), + ColumnDefinition("float_val", FloatType()), + ColumnDefinition("double_val", DoubleType()), + ColumnDefinition("decimal_val", DecimalType(precision=10, scale=4)), + ColumnDefinition("char_val", VarCharType(255)), + ColumnDefinition("varchar_val", VarCharType(255)), + ColumnDefinition("text_val", TextType()), + ColumnDefinition("date_val", DateType()), + ColumnDefinition("time_val", TimeTzType()), + ColumnDefinition("timestamp_val", TimestampTzType()), + ColumnDefinition("blob_val", PostgresByteaType()), + ColumnDefinition("json_val", JsonBType()), + ColumnDefinition("array_val", ArrayType(IntegerType())), + ColumnDefinition("is_active", BooleanType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=True)]), + ], + table_constraints=[ + TableConstraint(constraint_type=TableConstraintType.PRIMARY_KEY, + columns=["id"]), + ], + ) + + +# --------------------------------------------------------------------------- +# basic/type_tests.sql +# --------------------------------------------------------------------------- + +def create_type_tests_table(dialect, table_name: str = "type_tests") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresUUIDType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("string_field", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("int_field", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("float_field", FloatType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("decimal_field", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("bool_field", BooleanType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("datetime_field", TimestampTzType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("json_field", JsonBType()), + ColumnDefinition("nullable_field", VarCharType(255)), + ], + table_constraints=[ + TableConstraint(constraint_type=TableConstraintType.PRIMARY_KEY, + columns=["id"]), + ], + ) + + +# --------------------------------------------------------------------------- +# basic/validated_field_users.sql +# --------------------------------------------------------------------------- + +def create_validated_field_users_table(dialect, table_name: str = "validated_field_users") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("username", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("email", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("age", IntegerType()), + ColumnDefinition("balance", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("credit_score", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=300)]), + ColumnDefinition("status", VarCharType(50), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="active")]), + ColumnDefinition("is_active", BooleanType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=True)]), + ], + ) + + +# --------------------------------------------------------------------------- +# basic/validated_users.sql +# --------------------------------------------------------------------------- + +def create_validated_users_table(dialect, table_name: str = "validated_users") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("username", VarCharType(50), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("email", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("age", IntegerType()), + ], + ) + + +# --------------------------------------------------------------------------- +# basic/pydantic_validated_models.sql +# --------------------------------------------------------------------------- + +def create_pydantic_validated_models_table(dialect, table_name: str = "pydantic_validated_models") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("code", VarCharType(32)), + ColumnDefinition("quantity", IntegerType()), + ColumnDefinition("step_count", IntegerType()), + ColumnDefinition("price", DecimalType(precision=10, scale=2)), + ColumnDefinition("start_at", TimestampTzType()), + ColumnDefinition("end_at", TimestampTzType()), + ColumnDefinition("status", VarCharType(32)), + ColumnDefinition("normalized_name", VarCharType(50)), + ColumnDefinition("created_token", VarCharType(255)), + ], + ) + + +# --------------------------------------------------------------------------- +# basic/bulk_users.sql +# --------------------------------------------------------------------------- + +def create_bulk_users_table(dialect, table_name: str = "bulk_users") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("age", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0)]), + ColumnDefinition("email", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="")]), + ], + ) + + +# --------------------------------------------------------------------------- +# basic/posts.sql +# --------------------------------------------------------------------------- + +def create_posts_table(dialect, table_name: str = "posts") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("author", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("title", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("content", TextType()), + ColumnDefinition("published_at", TimestampTzType()), + ColumnDefinition("published", BooleanType(), + constraints=[ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=False)]), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + table_constraints=[ + ForeignKeyConstraint(columns=["author"], foreign_key_table="users", foreign_key_columns=["id"], + on_delete=_CASCADE), + ], + ) + + +# --------------------------------------------------------------------------- +# basic/comments.sql +# --------------------------------------------------------------------------- + +def create_comments_table(dialect, table_name: str = "comments") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("post_ref", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("author", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("text", TextType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("created_at", TimestampTzType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("updated_at", TimestampTzType()), + ColumnDefinition("approved", BooleanType(), + constraints=[ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=False)]), + ], + table_constraints=[ + ForeignKeyConstraint(columns=["post_ref"], foreign_key_table="posts", foreign_key_columns=["id"], + on_delete=_CASCADE), + ForeignKeyConstraint(columns=["author"], foreign_key_table="users", foreign_key_columns=["id"], + on_delete=_CASCADE), + ], + ) + + +# --------------------------------------------------------------------------- +# basic/column_mapping_items.sql +# --------------------------------------------------------------------------- + +def create_column_mapping_items_table(dialect, table_name: str = "column_mapping_items") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("item_total", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("remarks", IntegerType()), + ], + ) + + +# --------------------------------------------------------------------------- +# basic/mixed_annotation_items.sql +# --------------------------------------------------------------------------- + +def create_mixed_annotation_items_table(dialect, table_name: str = "mixed_annotation_items") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("tags", ArrayType(TextType())), + ColumnDefinition("meta", TextType()), + ColumnDefinition("description", TextType()), + ColumnDefinition("status", TextType()), + ], + ) + + +# --------------------------------------------------------------------------- +# basic/type_adapter_tests.sql +# --------------------------------------------------------------------------- + +def create_type_adapter_tests_table(dialect, table_name: str = "type_adapter_tests") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=True, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("optional_name", VarCharType(255)), + ColumnDefinition("optional_age", IntegerType()), + ColumnDefinition("last_login", TimestampTzType()), + ColumnDefinition("is_premium", BooleanType()), + ColumnDefinition("unsupported_union", VarCharType(255)), + ColumnDefinition("custom_bool", VarCharType(10)), + ColumnDefinition("optional_custom_bool", VarCharType(10)), + ], + ) + + +# --------------------------------------------------------------------------- +# composite_pk/order_items.sql (composite PK) +# --------------------------------------------------------------------------- + +def create_composite_pk_order_items_table(dialect, table_name: str = "order_items") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("order_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("product_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("quantity", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=1)]), + ColumnDefinition("unit_price", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ], + table_constraints=[ + TableConstraint(constraint_type=TableConstraintType.PRIMARY_KEY, + columns=["order_id", "product_id"]), + ], + ) + + +# --------------------------------------------------------------------------- +# composite_pk/store_inventory.sql (composite PK, no FK) +# --------------------------------------------------------------------------- + +def create_store_inventory_table(dialect, table_name: str = "store_inventory") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("store_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("product_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("batch_id", VarCharType(64), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("stock", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0)]), + ], + table_constraints=[ + TableConstraint(constraint_type=TableConstraintType.PRIMARY_KEY, + columns=["store_id", "product_id", "batch_id"]), + ], + ) + + +# --------------------------------------------------------------------------- +# derived_field/orders.sql (single PK) +# --------------------------------------------------------------------------- + +def create_orders_table(dialect, table_name: str = "orders") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("total", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("created_at", TextType()), + ColumnDefinition("updated_at", TextType()), + ], + ) + + +# --------------------------------------------------------------------------- +# derived_field/product.sql (single PK) +# --------------------------------------------------------------------------- + +def create_product_table(dialect, table_name: str = "product") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=True, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", TextType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("price", FloatType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("quantity", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ], + ) + + +TABLE_EXPRESSIONS: Dict[str, Callable] = { + "users": create_users_table, + "type_cases": create_type_cases_table, + "type_tests": create_type_tests_table, + "validated_field_users": create_validated_field_users_table, + "validated_users": create_validated_users_table, + "pydantic_validated_models": create_pydantic_validated_models_table, + "bulk_users": create_bulk_users_table, + "posts": create_posts_table, + "comments": create_comments_table, + "column_mapping_items": create_column_mapping_items_table, + "mixed_annotation_items": create_mixed_annotation_items_table, + "type_adapter_tests": create_type_adapter_tests_table, + "order_items": create_composite_pk_order_items_table, + "store_inventory": create_store_inventory_table, + "orders": create_orders_table, + "product": create_product_table, +} diff --git a/tests/providers/fixtures/events.py b/tests/providers/fixtures/events.py new file mode 100644 index 00000000..39960fb5 --- /dev/null +++ b/tests/providers/fixtures/events.py @@ -0,0 +1,82 @@ +# tests/providers/fixtures/events.py +"""DDL expressions for the ``feature/events`` table group (PostgreSQL). + +Reference: ``tests/rhosocial/activerecord_postgres_test/feature/events/schema/``. +""" + +from typing import Callable, Dict + +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, +) +from rhosocial.activerecord.backend.expression.statements import ( + ColumnDefinition, + ColumnConstraint, + ColumnConstraintType, +) +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, + TextType, + TimestampTzType, + VarCharType, +) +from rhosocial.activerecord.backend.impl.postgres import ( + PostgresSerialType, +) + + +# --------------------------------------------------------------------------- +# events/event_tests.sql +# --------------------------------------------------------------------------- + +def create_event_tests_table(dialect, table_name: str = "event_tests") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("status", VarCharType(50), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="draft")]), + ColumnDefinition("revision", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=1)]), + ColumnDefinition("content", TextType()), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + ) + + +# --------------------------------------------------------------------------- +# events/event_tracking_models.sql +# --------------------------------------------------------------------------- + +def create_event_tracking_models_table(dialect, table_name: str = "event_tracking_models") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("title", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("content", TextType()), + ColumnDefinition("view_count", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0)]), + ColumnDefinition("last_viewed_at", TimestampTzType(), + constraints=[ColumnConstraint(ColumnConstraintType.NULL)]), + ], + ) + + +TABLE_EXPRESSIONS: Dict[str, Callable] = { + "event_tests": create_event_tests_table, + "event_tracking_models": create_event_tracking_models_table, +} diff --git a/tests/providers/fixtures/mixins.py b/tests/providers/fixtures/mixins.py new file mode 100644 index 00000000..b7c8f832 --- /dev/null +++ b/tests/providers/fixtures/mixins.py @@ -0,0 +1,132 @@ +# tests/providers/fixtures/mixins.py +"""DDL expressions for the ``feature/mixins`` table group (PostgreSQL). + +Reference: ``tests/rhosocial/activerecord_postgres_test/feature/mixins/schema/``. +""" + +from typing import Callable, Dict + +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, +) +from rhosocial.activerecord.backend.expression.statements import ( + ColumnDefinition, + ColumnConstraint, + ColumnConstraintType, +) +from rhosocial.activerecord.backend.expression.types import ( + BooleanType, + DecimalType, + IntegerType, + TextType, + TimestampTzType, + VarCharType, +) +from rhosocial.activerecord.backend.impl.postgres import ( + PostgresSerialType, +) + + +# --------------------------------------------------------------------------- +# mixins/combined_articles.sql +# --------------------------------------------------------------------------- + +def create_combined_articles_table(dialect, table_name: str = "combined_articles") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("title", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("content", TextType()), + ColumnDefinition("status", VarCharType(50), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="draft")]), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ColumnDefinition("version", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=1)]), + ColumnDefinition("deleted_at", TimestampTzType(), + constraints=[ColumnConstraint(ColumnConstraintType.NULL)]), + ], + ) + + +# --------------------------------------------------------------------------- +# mixins/tasks.sql +# --------------------------------------------------------------------------- + +def create_tasks_table(dialect, table_name: str = "tasks") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("title", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("is_completed", BooleanType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=False)]), + ColumnDefinition("deleted_at", TimestampTzType(), + constraints=[ColumnConstraint(ColumnConstraintType.NULL)]), + ], + ) + + +# --------------------------------------------------------------------------- +# mixins/timestamped_posts.sql +# --------------------------------------------------------------------------- + +def create_timestamped_posts_table(dialect, table_name: str = "timestamped_posts") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("title", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("content", TextType()), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + ) + + +# --------------------------------------------------------------------------- +# mixins/versioned_products.sql +# --------------------------------------------------------------------------- + +def create_versioned_products_table(dialect, table_name: str = "versioned_products") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("price", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0.00)]), + ColumnDefinition("version", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=1)]), + ], + ) + + +TABLE_EXPRESSIONS: Dict[str, Callable] = { + "combined_articles": create_combined_articles_table, + "tasks": create_tasks_table, + "timestamped_posts": create_timestamped_posts_table, + "versioned_products": create_versioned_products_table, +} diff --git a/tests/providers/fixtures/query.py b/tests/providers/fixtures/query.py new file mode 100644 index 00000000..86e279fc --- /dev/null +++ b/tests/providers/fixtures/query.py @@ -0,0 +1,383 @@ +# tests/providers/fixtures/query.py +"""DDL expressions for the ``feature/query`` table group (PostgreSQL). + +Reference: ``tests/rhosocial/activerecord_postgres_test/feature/query/schema/``. +""" + +from typing import Callable, Dict + +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, +) +from rhosocial.activerecord.backend.expression.statements import ( + ColumnDefinition, + ColumnConstraint, + ColumnConstraintType, + ForeignKeyConstraint, + ReferentialAction, +) +from rhosocial.activerecord.backend.expression.types import ( + BooleanType, + DecimalType, + IntegerType, + JsonBType, + TextType, + TimestampTzType, + VarCharType, +) +from rhosocial.activerecord.backend.impl.postgres import ( + PostgresSerialType, +) + +_CASCADE = ReferentialAction.CASCADE + + +# --------------------------------------------------------------------------- +# query/users.sql +# --------------------------------------------------------------------------- + +def create_users_table(dialect, table_name: str = "users") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("username", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("email", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("age", IntegerType()), + ColumnDefinition("balance", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0.00)]), + ColumnDefinition("is_active", BooleanType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=True)]), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + ) + + +# --------------------------------------------------------------------------- +# query/posts.sql +# --------------------------------------------------------------------------- + +def create_posts_table(dialect, table_name: str = "posts") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("user_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("title", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("content", TextType()), + ColumnDefinition("status", VarCharType(50), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="published")]), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + table_constraints=[ + ForeignKeyConstraint(columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"], + on_delete=_CASCADE), + ], + ) + + +# --------------------------------------------------------------------------- +# query/comments.sql +# --------------------------------------------------------------------------- + +def create_comments_table(dialect, table_name: str = "comments") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("user_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("post_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("content", TextType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("is_hidden", BooleanType(), + constraints=[ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=False)]), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + table_constraints=[ + ForeignKeyConstraint(columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"], + on_delete=_CASCADE), + ForeignKeyConstraint(columns=["post_id"], foreign_key_table="posts", foreign_key_columns=["id"], + on_delete=_CASCADE), + ], + ) + + +# --------------------------------------------------------------------------- +# query/orders.sql +# --------------------------------------------------------------------------- + +def create_orders_table(dialect, table_name: str = "orders") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("user_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("order_number", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("total_amount", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0.00)]), + ColumnDefinition("status", VarCharType(50), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="pending")]), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + table_constraints=[ + ForeignKeyConstraint(columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"]), + ], + ) + + +# --------------------------------------------------------------------------- +# query/order_items.sql +# --------------------------------------------------------------------------- + +def create_order_items_table(dialect, table_name: str = "order_items") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("order_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("product_name", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("quantity", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=1)]), + ColumnDefinition("unit_price", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("subtotal", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0.00)]), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + table_constraints=[ + ForeignKeyConstraint(columns=["order_id"], foreign_key_table="orders", foreign_key_columns=["id"], + on_delete=_CASCADE), + ], + ) + + +# --------------------------------------------------------------------------- +# query/profiles.sql +# --------------------------------------------------------------------------- + +def create_profiles_table(dialect, table_name: str = "profiles") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("user_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("bio", TextType()), + ColumnDefinition("avatar_url", VarCharType(512)), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + table_constraints=[ + ForeignKeyConstraint(columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"]), + ], + ) + + +# --------------------------------------------------------------------------- +# query/json_users.sql +# --------------------------------------------------------------------------- + +def create_json_users_table(dialect, table_name: str = "json_users") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("username", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("email", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("age", IntegerType()), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ColumnDefinition("settings", JsonBType()), + ColumnDefinition("tags", JsonBType()), + ColumnDefinition("profile", JsonBType()), + ColumnDefinition("roles", JsonBType()), + ColumnDefinition("scores", JsonBType()), + ColumnDefinition("subscription", JsonBType()), + ColumnDefinition("preferences", JsonBType()), + ], + ) + + +# --------------------------------------------------------------------------- +# query/nodes.sql (self-referential FK) +# --------------------------------------------------------------------------- + +def create_nodes_table(dialect, table_name: str = "nodes") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("parent_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NULL)]), + ColumnDefinition("value", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0.00)]), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + table_constraints=[ + ForeignKeyConstraint(columns=["parent_id"], foreign_key_table="nodes", foreign_key_columns=["id"], + on_delete=_CASCADE), + ], + ) + + +# --------------------------------------------------------------------------- +# query/searchable_items.sql +# --------------------------------------------------------------------------- + +def create_searchable_items_table(dialect, table_name: str = "searchable_items") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(255)), + ColumnDefinition("tags", TextType()), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + ) + + +# --------------------------------------------------------------------------- +# query/extended_orders.sql +# --------------------------------------------------------------------------- + +def create_extended_orders_table(dialect, table_name: str = "extended_orders") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("user_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("order_number", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("total_amount", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0.00)]), + ColumnDefinition("status", VarCharType(50), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="pending")]), + ColumnDefinition("priority", VarCharType(50), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="medium")]), + ColumnDefinition("region", VarCharType(50), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="default")]), + ColumnDefinition("category", VarCharType(255)), + ColumnDefinition("product", VarCharType(255)), + ColumnDefinition("department", VarCharType(255)), + ColumnDefinition("year", VarCharType(10)), + ColumnDefinition("quarter", VarCharType(10)), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + table_constraints=[ + ForeignKeyConstraint(columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"], + on_delete=_CASCADE), + ], + ) + + +# --------------------------------------------------------------------------- +# query/extended_order_items.sql +# --------------------------------------------------------------------------- + +def create_extended_order_items_table(dialect, table_name: str = "extended_order_items") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("order_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("product_name", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("quantity", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=1)]), + ColumnDefinition("price", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("category", VarCharType(255)), + ColumnDefinition("region", VarCharType(50)), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + table_constraints=[ + ForeignKeyConstraint(columns=["order_id"], foreign_key_table="extended_orders", foreign_key_columns=["id"], + on_delete=_CASCADE), + ], + ) + + +TABLE_EXPRESSIONS: Dict[str, Callable] = { + "users": create_users_table, + "posts": create_posts_table, + "comments": create_comments_table, + "orders": create_orders_table, + "order_items": create_order_items_table, + "profiles": create_profiles_table, + "json_users": create_json_users_table, + "nodes": create_nodes_table, + "searchable_items": create_searchable_items_table, + "extended_orders": create_extended_orders_table, + "extended_order_items": create_extended_order_items_table, +} diff --git a/tests/providers/mixins.py b/tests/providers/mixins.py index d803b6de..149fd6f8 100644 --- a/tests/providers/mixins.py +++ b/tests/providers/mixins.py @@ -28,7 +28,11 @@ TimestampedPost as TimestampedPostBase, VersionedProduct as VersionedProductBase, Task as TaskBase, - CombinedArticle as CombinedArticleBase + CombinedArticle as CombinedArticleBase, + AsyncTimestampedPost as AsyncTimestampedPostBase, + AsyncVersionedProduct as AsyncVersionedProductBase, + AsyncTask as AsyncTaskBase, + AsyncCombinedArticle as AsyncCombinedArticleBase, ) # Conditionally import Python 3.10+ models @@ -89,113 +93,138 @@ def _select_model_class(base_cls, py312_cls, py311_cls, py310_cls, model_name: s Task = _select_model_class(TaskBase, Task312, Task311, Task310, "Task") CombinedArticle = _select_model_class(CombinedArticleBase, CombinedArticle312, CombinedArticle311, CombinedArticle310, "CombinedArticle") # noqa: E501 -from rhosocial.activerecord.testsuite.feature.mixins.interfaces import IMixinsProvider # noqa: E402 +AsyncTimestampedPost = AsyncTimestampedPostBase +AsyncVersionedProduct = AsyncVersionedProductBase +AsyncTask = AsyncTaskBase +AsyncCombinedArticle = AsyncCombinedArticleBase + +from rhosocial.activerecord.backend.options import ExecutionOptions, StatementType # noqa: E402 +from rhosocial.activerecord.testsuite.feature.mixins.interfaces import IMixinsSyncProvider, IMixinsAsyncProvider # noqa: E402 # ...and the scenarios are defined specifically for this backend. from .scenarios import get_enabled_scenarios, get_scenario # noqa: E402 +# Expression-based DDL fixtures +from .fixtures._common import drop_table +from .fixtures.mixins import TABLE_EXPRESSIONS as MIXINS_TABLE_EXPRESSIONS -class MixinsProvider(IMixinsProvider): - """ - This is the postgres backend's implementation for the mixins test group. - It connects the generic tests in the testsuite with the actual postgres database. - """ +class MixinsProviderBase: def __init__(self): - # This list will track the backend instances created during the setup phase. self._active_backends = [] def get_test_scenarios(self) -> List[str]: - """Returns a list of names for all enabled scenarios for this backend.""" return list(get_enabled_scenarios().keys()) + def _load_postgres_schema(self, filename: str) -> str: + schema_dir = os.path.join( + os.path.dirname(__file__), "..", "rhosocial", "activerecord_postgres_test", "feature", "mixins", "schema" + ) + schema_path = os.path.join(schema_dir, filename) + with open(schema_path, 'r', encoding='utf-8') as f: + return f.read() + + +class MixinsSyncProvider(MixinsProviderBase, IMixinsSyncProvider): + + def __init__(self): + super().__init__() + def _setup_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str) -> Type[ActiveRecord]: - """A generic helper method to handle the setup for any given model.""" - # 1. Get the backend class (PostgresBackend) and connection config for the requested scenario. backend_class, config = get_scenario(scenario_name) - - # 2. Configure the generic model class with our specific backend and config. model_class.configure(config, backend_class) - backend_instance = model_class.__backend__ if backend_instance not in self._active_backends: self._active_backends.append(backend_instance) - - # 3. Prepare the database schema. + opts = ExecutionOptions(stmt_type=StatementType.DDL) try: - # Drop the table if it exists, with cascade to handle dependencies. - model_class.__backend__.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') + sql, params = drop_table(backend_instance.dialect, table_name).to_sql() + backend_instance.execute(sql, params, options=opts) except Exception as e: - # Ignore errors if the table didn't exist, but print for debugging. print(f"Could not drop table {table_name}: {e}") - pass - - # Execute the schema SQL to create the table. - schema_sql = self._load_postgres_schema(f"{table_name}.sql") - model_class.__backend__.execute(schema_sql) - + if fn := MIXINS_TABLE_EXPRESSIONS.get(table_name): + create_expr = fn(backend_instance.dialect, table_name) + sql, params = create_expr.to_sql() + backend_instance.execute(sql, params, options=opts) return model_class - # --- Implementation of the IMixinsProvider interface --- - - - - - - - def setup_timestamped_post_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for timestamps model tests.""" - return self._setup_model(TimestampedPost, scenario_name, "timestamped_posts") def setup_versioned_product_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for optimistic lock model tests.""" - return self._setup_model(VersionedProduct, scenario_name, "versioned_products") def setup_task_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for soft delete model tests.""" - return self._setup_model(Task, scenario_name, "tasks") def setup_combined_article_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for combined article model tests.""" - return self._setup_model(CombinedArticle, scenario_name, "combined_articles") - - - def _load_postgres_schema(self, filename: str) -> str: - """Helper to load a SQL schema file from this project's fixtures.""" - # Schemas are stored in the centralized location for mixins feature. - schema_dir = os.path.join(os.path.dirname(__file__), "..", "rhosocial", "activerecord_postgres_test", "feature", "mixins", "schema") # noqa: E501 - schema_path = os.path.join(schema_dir, filename) - - with open(schema_path, 'r', encoding='utf-8') as f: - return f.read() - def cleanup_after_test(self, scenario_name: str): - """ - Performs cleanup after a test. This now iterates through the backends - that were created during setup, drops tables, and explicitly disconnects them. - """ for backend_instance in self._active_backends: try: - # Drop all tables that might have been created for mixins tests for table_name in ['timestamped_posts', 'versioned_products', 'tasks', 'combined_articles']: try: backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') except Exception: - # Continue with other tables if one fails pass finally: - # Always disconnect the backend instance that was used in the test try: backend_instance.disconnect() - except: # noqa: E722 - # Ignore errors during disconnect + except Exception: pass + self._active_backends.clear() + + +class MixinsAsyncProvider(MixinsProviderBase, IMixinsAsyncProvider): - # Clear the list of active backends for the next test - self._active_backends.clear() \ No newline at end of file + def __init__(self): + super().__init__() + self._active_async_backends = [] + + async def _setup_async_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str) -> Type[ActiveRecord]: + from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + _, config = get_scenario(scenario_name) + await model_class.configure(config, AsyncPostgresBackend) + backend_instance = model_class.__backend__ + if backend_instance not in self._active_async_backends: + self._active_async_backends.append(backend_instance) + opts = ExecutionOptions(stmt_type=StatementType.DDL) + try: + sql, params = drop_table(backend_instance.dialect, table_name).to_sql() + await backend_instance.execute(sql, params, options=opts) + except Exception as e: + print(f"Could not drop table {table_name}: {e}") + if fn := MIXINS_TABLE_EXPRESSIONS.get(table_name): + create_expr = fn(backend_instance.dialect, table_name) + sql, params = create_expr.to_sql() + await backend_instance.execute(sql, params, options=opts) + return model_class + + async def setup_timestamped_post_model(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(AsyncTimestampedPost, scenario_name, "timestamped_posts") + + async def setup_versioned_product_model(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(AsyncVersionedProduct, scenario_name, "versioned_products") + + async def setup_task_model(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(AsyncTask, scenario_name, "tasks") + + async def setup_combined_article_model(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(AsyncCombinedArticle, scenario_name, "combined_articles") + + async def cleanup_after_test(self, scenario_name: str): + for backend_instance in self._active_async_backends: + try: + for table_name in ['timestamped_posts', 'versioned_products', 'tasks', + 'combined_articles']: + try: + await backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') + except Exception: + pass + finally: + try: + await backend_instance.disconnect() + except Exception: + pass + self._active_async_backends.clear() diff --git a/tests/providers/query.py b/tests/providers/query.py index 6fac6c84..2bf80f61 100644 --- a/tests/providers/query.py +++ b/tests/providers/query.py @@ -15,7 +15,7 @@ import logging from typing import Type, List, Tuple -from rhosocial.activerecord.model import ActiveRecord +from rhosocial.activerecord.model import ActiveRecord, AsyncActiveRecord # Setup logging for fixture selection debugging logger = logging.getLogger(__name__) @@ -104,82 +104,75 @@ def _select_model_class(base_cls, py312_cls, py311_cls, py310_cls, model_name: s MappedComment = _select_model_class(MappedCommentBase, MappedComment312, MappedComment311, MappedComment310, "MappedComment") # noqa: E501 Profile = _select_model_class(ProfileBase, Profile312, Profile311, Profile310, "Profile") -from rhosocial.activerecord.testsuite.feature.query.interfaces import IQueryProvider # noqa: E402 +from rhosocial.activerecord.backend.options import ExecutionOptions, StatementType # noqa: E402 +from rhosocial.activerecord.testsuite.feature.query.interfaces import IQuerySyncProvider, IQueryAsyncProvider # noqa: E402 from rhosocial.activerecord.testsuite.core.protocols import WorkerTestProtocol # noqa: E402 + +# Composite-PK model classes used by setup_order_item_model. +from rhosocial.activerecord.testsuite.feature.basic.fixtures.models import ( # noqa: E402 + OrderItem as CompositeOrderItemBase, + AsyncOrderItem as AsyncCompositeOrderItemBase, +) + +# Expression-based DDL fixtures +from providers.fixtures._common import drop_table +from providers.fixtures.query import TABLE_EXPRESSIONS as QUERY_TABLE_EXPRESSIONS + # The scenarios are defined specifically for this backend. from .scenarios import get_enabled_scenarios, get_scenario # noqa: E402 -class QueryProvider(IQueryProvider, WorkerTestProtocol): - """ - This is the postgres backend's implementation for the query features test group. - It connects the generic tests in the testsuite with the actual postgres database. - """ - +class QueryProviderBase: def __init__(self): - self._active_backends = [] - self._active_async_backends = [] + self._scenario_db_files = {} + self._created_tables: Set[str] = set() def get_test_scenarios(self) -> List[str]: - """Returns a list of names for all enabled scenarios for this backend.""" return list(get_enabled_scenarios().keys()) def _track_backend(self, backend_instance, collection): if backend_instance not in collection: collection.append(backend_instance) - def _setup_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str, shared_backend=None) -> Type[ActiveRecord]: # noqa: E501 - """A generic helper method to handle the setup for any given model.""" - backend_class, config = get_scenario(scenario_name) - - if shared_backend is None: - model_class.configure(config, backend_class) - else: - model_class.__connection_config__ = config - model_class.__backend_class__ = backend_class - model_class.__backend__ = shared_backend - - backend_instance = model_class.__backend__ - self._track_backend(backend_instance, self._active_backends) - - try: - backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') - except Exception: - pass - - schema_sql = self._load_postgres_schema(f"{table_name}.sql") - backend_instance.execute(schema_sql) + def _load_postgres_schema(self, filename: str) -> str: + schema_dir = os.path.join( + os.path.dirname(__file__), "..", "rhosocial", "activerecord_postgres_test", "feature", "query", "schema" + ) + schema_path = os.path.join(schema_dir, filename) + with open(schema_path, 'r', encoding='utf-8') as f: + return f.read() - return model_class - async def _setup_model_async(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str, shared_backend=None) -> Type[ActiveRecord]: # noqa: E501 - """A generic helper method to handle the setup for any given async model.""" - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend +class QuerySyncProvider(QueryProviderBase, IQuerySyncProvider, WorkerTestProtocol): - _, config = get_scenario(scenario_name) + def __init__(self): + super().__init__() + self._active_backends = [] + def _setup_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str, shared_backend=None) -> Type[ActiveRecord]: + backend_class, config = get_scenario(scenario_name) if shared_backend is None: - await model_class.configure(config, AsyncPostgresBackend) + model_class.configure(config, backend_class) else: model_class.__connection_config__ = config - model_class.__backend_class__ = AsyncPostgresBackend + model_class.__backend_class__ = backend_class model_class.__backend__ = shared_backend - backend_instance = model_class.__backend__ - self._track_backend(backend_instance, self._active_async_backends) - + self._track_backend(backend_instance, self._active_backends) + opts = ExecutionOptions(stmt_type=StatementType.DDL) try: - await backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') + sql, params = drop_table(backend_instance.dialect, table_name).to_sql() + backend_instance.execute(sql, params, options=opts) except Exception: pass - - schema_sql = self._load_postgres_schema(f"{table_name}.sql") - await backend_instance.execute(schema_sql) - + if fn := QUERY_TABLE_EXPRESSIONS.get(table_name): + create_expr = fn(backend_instance.dialect, table_name) + sql, params = create_expr.to_sql() + backend_instance.execute(sql, params, options=opts) + self._created_tables.add(table_name) return model_class - def _setup_multiple_models(self, models_and_tables: List[Tuple[Type[ActiveRecord], str]], scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: # noqa: E501 - """A helper to set up multiple models for fixture groups, sharing the same backend.""" + def _setup_multiple_models(self, models_and_tables: List[Tuple[Type[ActiveRecord], str]], scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: result = [] shared_backend = None for i, (model_class, table_name) in enumerate(models_and_tables): @@ -187,27 +180,11 @@ def _setup_multiple_models(self, models_and_tables: List[Tuple[Type[ActiveRecord configured_model = self._setup_model(model_class, scenario_name, table_name) shared_backend = configured_model.__backend__ else: - configured_model = self._setup_model(model_class, scenario_name, table_name, shared_backend=shared_backend) # noqa: E501 + configured_model = self._setup_model(model_class, scenario_name, table_name, shared_backend=shared_backend) result.append(configured_model) return tuple(result) - async def _setup_multiple_models_async(self, models_and_tables: List[Tuple[Type[ActiveRecord], str]], scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: # noqa: E501 - """A helper to set up multiple async models for fixture groups, sharing the same backend.""" - result = [] - shared_backend = None - for i, (model_class, table_name) in enumerate(models_and_tables): - if i == 0: - configured_model = await self._setup_model_async(model_class, scenario_name, table_name) - shared_backend = configured_model.__backend__ - else: - configured_model = await self._setup_model_async(model_class, scenario_name, table_name, shared_backend=shared_backend) # noqa: E501 - result.append(configured_model) - return tuple(result) - - # --- Implementation of the IQueryProvider interface --- - - def setup_order_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for order-related models (User, Order, OrderItem) tests.""" + def setup_order_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: models_and_tables = [ (User, "users"), (Order, "orders"), @@ -215,8 +192,7 @@ def setup_order_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ] return self._setup_multiple_models(models_and_tables, scenario_name) - def setup_blog_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for blog-related models (User, Post, Comment) tests.""" + def setup_blog_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: models_and_tables = [ (User, "users"), (Post, "posts"), @@ -225,34 +201,24 @@ def setup_blog_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], T return self._setup_multiple_models(models_and_tables, scenario_name) def setup_json_user_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: - """Sets up the database for the JSON user model. - - Registers PostgresJSONBAdapter for JSON fields so that psycopg3's - automatic JSONB deserialization (dict/list) is converted back to str - before Pydantic validation, matching the model's Optional[str] type. - """ from rhosocial.activerecord.backend.impl.postgres.adapters import PostgresJSONBAdapter - jsonb_adapter = PostgresJSONBAdapter() json_fields = ["settings", "tags", "profile", "roles", "scores", "subscription", "preferences"] for field_name in json_fields: if field_name not in JsonUser.__field_adapters__: JsonUser.__field_adapters__[field_name] = (jsonb_adapter, str) - models_and_tables = [ (JsonUser, "json_users"), ] return self._setup_multiple_models(models_and_tables, scenario_name) def setup_tree_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: - """Sets up the database for the tree structure (Node) model.""" models_and_tables = [ (Node, "nodes"), ] return self._setup_multiple_models(models_and_tables, scenario_name) - def setup_extended_order_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for extended order-related models (User, ExtendedOrder, ExtendedOrderItem) tests.""" + def setup_extended_order_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: models_and_tables = [ (User, "users"), (ExtendedOrder, "extended_orders"), @@ -260,8 +226,7 @@ def setup_extended_order_fixtures(self, scenario_name: str) -> Tuple[Type[Active ] return self._setup_multiple_models(models_and_tables, scenario_name) - def setup_combined_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for combined models (User, Order, OrderItem, Post, Comment) tests.""" + def setup_combined_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: models_and_tables = [ (User, "users"), (Order, "orders"), @@ -272,40 +237,155 @@ def setup_combined_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord return self._setup_multiple_models(models_and_tables, scenario_name) def setup_annotated_query_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: - """Sets up the database for the SearchableItem model tests.""" from rhosocial.activerecord.testsuite.feature.query.fixtures.annotated_adapter_models import SearchableItem return self._setup_multiple_models([ (SearchableItem, "searchable_items"), ], scenario_name) - def setup_mapped_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for MappedUser, MappedPost, and MappedComment models.""" + def setup_mapped_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: return self._setup_multiple_models([ (MappedUser, "users"), (MappedPost, "posts"), (MappedComment, "comments") ], scenario_name) - def setup_profile_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for User and Profile models.""" + def setup_profile_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord]]: return self._setup_multiple_models([ (User, "users"), (Profile, "profiles"), ], scenario_name) - # --- Async implementations --- + def setup_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: + """Set up the composite-PK OrderItem model for the query feature tests.""" + from rhosocial.activerecord.backend.options import ExecutionOptions, StatementType + from providers.fixtures._common import drop_table + from providers.fixtures.basic import TABLE_EXPRESSIONS as BASIC_EXPRS + + backend_class, config = get_scenario(scenario_name) + CompositeOrderItemBase.configure(config, backend_class) + backend_instance = CompositeOrderItemBase.__backend__ + self._track_backend(backend_instance, self._active_backends) + opts = ExecutionOptions(stmt_type=StatementType.DDL) + try: + sql, params = drop_table(backend_instance.dialect, "order_items").to_sql() + backend_instance.execute(sql, params, options=opts) + except Exception: + pass + if fn := BASIC_EXPRS.get("order_items"): + create_expr = fn(backend_instance.dialect, "order_items") + sql, params = create_expr.to_sql() + backend_instance.execute(sql, params, options=opts) + self._created_tables.add("order_items") + return CompositeOrderItemBase + + def _get_schema_sql_for_fixture_type(self, fixture_type: str) -> dict: + schemas = {} + if fixture_type == 'order': + tables = ['users', 'orders', 'order_items'] + elif fixture_type == 'blog': + tables = ['users', 'posts', 'comments'] + elif fixture_type == 'user': + tables = ['users'] + elif fixture_type == 'combined': + tables = ['users', 'orders', 'order_items', 'posts', 'comments'] + else: + tables = ['users'] + for table in tables: + schemas[table] = self._load_postgres_schema(f'{table}.sql') + return schemas + + def get_worker_connection_params(self, scenario_name: str, fixture_type: str = 'order') -> dict: + from .scenarios import SCENARIO_MAP + is_async = fixture_type and fixture_type.startswith('async_') + backend_class_name = 'AsyncPostgresBackend' if is_async else 'PostgresBackend' + base_fixture_type = fixture_type.replace('async_', '') if fixture_type else 'order' + schema_sql = self._get_schema_sql_for_fixture_type(base_fixture_type) + if scenario_name not in SCENARIO_MAP: + if SCENARIO_MAP: + scenario_name = next(iter(SCENARIO_MAP)) + else: + raise ValueError("No scenarios registered") + config_dict = SCENARIO_MAP[scenario_name] + return { + 'backend_module': 'rhosocial.activerecord.backend.impl.postgres', + 'backend_class_name': backend_class_name, + 'config_class_module': 'rhosocial.activerecord.backend.impl.postgres.config', + 'config_class_name': 'PostgresConnectionConfig', + 'config_kwargs': config_dict, + 'schema_sql': schema_sql, + } + + def get_worker_schema_sql(self, scenario_name: str, table_name: str) -> str: + return self._load_postgres_schema(f'{table_name}.sql') + + def cleanup_after_test(self, scenario_name: str): + for backend_instance in self._active_backends: + try: + for table_name in list(self._created_tables): + try: + backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') + except Exception: + pass + finally: + try: + backend_instance.disconnect() + except Exception: + pass + self._active_backends.clear() + self._created_tables.clear() + - async def setup_async_order_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for async order-related models (AsyncUser, AsyncOrder, AsyncOrderItem) tests.""" - from rhosocial.activerecord.testsuite.feature.query.fixtures.async_models import AsyncUser, AsyncOrder, AsyncOrderItem # noqa: E501 +class QueryAsyncProvider(QueryProviderBase, IQueryAsyncProvider): + + def __init__(self): + super().__init__() + self._active_async_backends = [] + + async def _setup_async_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str, shared_backend=None) -> Type[ActiveRecord]: + from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + _, config = get_scenario(scenario_name) + if shared_backend is None: + await model_class.configure(config, AsyncPostgresBackend) + else: + model_class.__connection_config__ = config + model_class.__backend_class__ = AsyncPostgresBackend + model_class.__backend__ = shared_backend + backend_instance = model_class.__backend__ + self._track_backend(backend_instance, self._active_async_backends) + opts = ExecutionOptions(stmt_type=StatementType.DDL) + try: + sql, params = drop_table(backend_instance.dialect, table_name).to_sql() + await backend_instance.execute(sql, params, options=opts) + except Exception: + pass + if fn := QUERY_TABLE_EXPRESSIONS.get(table_name): + create_expr = fn(backend_instance.dialect, table_name) + sql, params = create_expr.to_sql() + await backend_instance.execute(sql, params, options=opts) + self._created_tables.add(table_name) + return model_class + + async def _setup_multiple_models_async(self, models_and_tables: List[Tuple[Type[ActiveRecord], str]], scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: + result = [] + shared_backend = None + for i, (model_class, table_name) in enumerate(models_and_tables): + if i == 0: + configured_model = await self._setup_async_model(model_class, scenario_name, table_name) + shared_backend = configured_model.__backend__ + else: + configured_model = await self._setup_async_model(model_class, scenario_name, table_name, shared_backend=shared_backend) + result.append(configured_model) + return tuple(result) + + async def setup_order_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: + from rhosocial.activerecord.testsuite.feature.query.fixtures.async_models import AsyncUser, AsyncOrder, AsyncOrderItem return await self._setup_multiple_models_async([ (AsyncUser, "users"), (AsyncOrder, "orders"), (AsyncOrderItem, "order_items") ], scenario_name) - async def setup_async_blog_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for async blog-related models (AsyncUser, AsyncPost, AsyncComment) tests.""" + async def setup_blog_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: from rhosocial.activerecord.testsuite.feature.query.fixtures.async_models import AsyncUser from rhosocial.activerecord.testsuite.feature.query.fixtures.async_blog_models import AsyncPost, AsyncComment return await self._setup_multiple_models_async([ @@ -314,45 +394,34 @@ async def setup_async_blog_fixtures(self, scenario_name: str) -> Tuple[Type[Acti (AsyncComment, "comments") ], scenario_name) - async def setup_async_json_user_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: - """Sets up the database for the async JSON user model. - - Registers PostgresJSONBAdapter for JSON fields so that psycopg3's - automatic JSONB deserialization (dict/list) is converted back to str - before Pydantic validation, matching the model's Optional[str] type. - """ + async def setup_json_user_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: from rhosocial.activerecord.testsuite.feature.query.fixtures.async_json_models import AsyncJsonUser from rhosocial.activerecord.backend.impl.postgres.adapters import PostgresJSONBAdapter - jsonb_adapter = PostgresJSONBAdapter() json_fields = ["settings", "tags", "profile", "roles", "scores", "subscription", "preferences"] for field_name in json_fields: if field_name not in AsyncJsonUser.__field_adapters__: AsyncJsonUser.__field_adapters__[field_name] = (jsonb_adapter, str) - return await self._setup_multiple_models_async([ (AsyncJsonUser, "json_users"), ], scenario_name) - async def setup_async_tree_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: - """Sets up the database for the async tree structure (AsyncNode) model.""" + async def setup_tree_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: from rhosocial.activerecord.testsuite.feature.query.fixtures.async_cte_models import AsyncNode return await self._setup_multiple_models_async([ (AsyncNode, "nodes"), ], scenario_name) - async def setup_async_extended_order_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for async extended order-related models (AsyncUser, AsyncExtendedOrder, AsyncExtendedOrderItem) tests.""" # noqa: E501 - from rhosocial.activerecord.testsuite.feature.query.fixtures.async_extended_models import AsyncUser, AsyncExtendedOrder, AsyncExtendedOrderItem # noqa: E501 + async def setup_extended_order_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: + from rhosocial.activerecord.testsuite.feature.query.fixtures.async_extended_models import AsyncUser, AsyncExtendedOrder, AsyncExtendedOrderItem return await self._setup_multiple_models_async([ (AsyncUser, "users"), (AsyncExtendedOrder, "extended_orders"), (AsyncExtendedOrderItem, "extended_order_items") ], scenario_name) - async def setup_async_combined_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for async combined models (AsyncUser, AsyncOrder, AsyncOrderItem, AsyncPost, AsyncComment) tests.""" # noqa: E501 - from rhosocial.activerecord.testsuite.feature.query.fixtures.async_models import AsyncUser, AsyncOrder, AsyncOrderItem # noqa: E501 + async def setup_combined_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: + from rhosocial.activerecord.testsuite.feature.query.fixtures.async_models import AsyncUser, AsyncOrder, AsyncOrderItem from rhosocial.activerecord.testsuite.feature.query.fixtures.async_blog_models import AsyncPost, AsyncComment return await self._setup_multiple_models_async([ (AsyncUser, "users"), @@ -362,39 +431,55 @@ async def setup_async_combined_fixtures(self, scenario_name: str) -> Tuple[Type[ (AsyncComment, "comments") ], scenario_name) - async def setup_async_annotated_query_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: - """Sets up the database for the async SearchableItem model tests.""" - from rhosocial.activerecord.testsuite.feature.query.fixtures.async_annotated_adapter_models import AsyncSearchableItem # noqa: E501 + async def setup_annotated_query_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: + from rhosocial.activerecord.testsuite.feature.query.fixtures.async_annotated_adapter_models import AsyncSearchableItem return await self._setup_multiple_models_async([ (AsyncSearchableItem, "searchable_items"), ], scenario_name) - async def setup_async_mapped_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for AsyncMappedUser, AsyncMappedPost, and AsyncMappedComment models.""" - from rhosocial.activerecord.testsuite.feature.basic.fixtures.models import AsyncMappedUser, AsyncMappedPost, AsyncMappedComment # noqa: E501 + async def setup_mapped_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: + from rhosocial.activerecord.testsuite.feature.basic.fixtures.models import AsyncMappedUser, AsyncMappedPost, AsyncMappedComment return await self._setup_multiple_models_async([ (AsyncMappedUser, "users"), (AsyncMappedPost, "posts"), (AsyncMappedComment, "comments") ], scenario_name) - async def setup_async_profile_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for AsyncUser and AsyncProfile models.""" + async def setup_profile_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord]]: from rhosocial.activerecord.testsuite.feature.query.fixtures.async_models import AsyncUser, AsyncProfile return await self._setup_multiple_models_async([ (AsyncUser, "users"), (AsyncProfile, "profiles"), ], scenario_name) - async def cleanup_after_test_async(self, scenario_name: str): - """Performs async cleanup after a test.""" - tables_to_drop = [ - 'users', 'orders', 'order_items', 'posts', 'comments', 'json_users', 'nodes', - 'extended_orders', 'extended_order_items', 'searchable_items' - ] + async def setup_order_item_model(self, scenario_name: str) -> Type[AsyncActiveRecord]: + """Set up the composite-PK AsyncOrderItem model for the query feature tests.""" + from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.options import ExecutionOptions, StatementType + from providers.fixtures._common import drop_table + from providers.fixtures.basic import TABLE_EXPRESSIONS as BASIC_EXPRS + + _, config = get_scenario(scenario_name) + await AsyncCompositeOrderItemBase.configure(config, AsyncPostgresBackend) + backend_instance = AsyncCompositeOrderItemBase.__backend__ + self._track_backend(backend_instance, self._active_async_backends) + opts = ExecutionOptions(stmt_type=StatementType.DDL) + try: + sql, params = drop_table(backend_instance.dialect, "order_items").to_sql() + await backend_instance.execute(sql, params, options=opts) + except Exception: + pass + if fn := BASIC_EXPRS.get("order_items"): + create_expr = fn(backend_instance.dialect, "order_items") + sql, params = create_expr.to_sql() + await backend_instance.execute(sql, params, options=opts) + self._created_tables.add("order_items") + return AsyncCompositeOrderItemBase + + async def cleanup_after_test(self, scenario_name: str): for backend_instance in self._active_async_backends: try: - for table_name in tables_to_drop: + for table_name in list(self._created_tables): try: await backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') except Exception: @@ -402,133 +487,7 @@ async def cleanup_after_test_async(self, scenario_name: str): finally: try: await backend_instance.disconnect() - except: # noqa: E722 + except Exception: pass self._active_async_backends.clear() - - def _load_postgres_schema(self, filename: str) -> str: - """Helper to load a SQL schema file from this project's fixtures.""" - # Schemas are stored in the centralized location for query feature. - schema_dir = os.path.join(os.path.dirname(__file__), "..", "rhosocial", "activerecord_postgres_test", "feature", "query", "schema") # noqa: E501 - schema_path = os.path.join(schema_dir, filename) - - with open(schema_path, 'r', encoding='utf-8') as f: - return f.read() - - def cleanup_after_test(self, scenario_name: str): - """ - Performs cleanup after a test. This uses the same backend instance - that was used for setup to ensure proper cleanup with foreign key constraints. - """ - tables_to_drop = [ - 'users', 'orders', 'order_items', 'posts', 'comments', 'json_users', 'nodes', - 'extended_orders', 'extended_order_items', 'searchable_items' - ] - for backend_instance in self._active_backends: - try: - # Drop all tables that might have been created for query tests - # Use CASCADE to handle foreign key dependencies - for table_name in tables_to_drop: - try: - backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') - except Exception: - # Continue with other tables if one fails - pass - finally: - # Always disconnect the backend instance that was used in the test - try: - backend_instance.disconnect() - except: # noqa: E722 - # Ignore errors during disconnect - pass - - # Clear the list of active backends for the next test - self._active_backends.clear() - - # --- Implementation of WorkerTestProtocol --- - - def get_worker_connection_params(self, scenario_name: str, fixture_type: str = 'order') -> dict: - """ - Return serializable connection parameters for Worker processes. - - This method provides all information needed to recreate the database - connection in a Worker process, including the schema SQL for table creation. - - Args: - scenario_name: The test scenario name - fixture_type: Type of fixture ('order', 'blog', 'user', 'combined', - or with 'async_' prefix for async backends) - - Returns: - Dictionary with connection parameters and schema SQL - """ - from .scenarios import SCENARIO_MAP - - # Determine if async backend is needed based on fixture_type - is_async = fixture_type and fixture_type.startswith('async_') - backend_class_name = 'AsyncPostgresBackend' if is_async else 'PostgresBackend' - - # Get base fixture type (remove 'async_' prefix if present) - base_fixture_type = fixture_type.replace('async_', '') if fixture_type else 'order' - - # Build schema SQL based on fixture type - schema_sql = self._get_schema_sql_for_fixture_type(base_fixture_type) - - # Get connection config from scenario - if scenario_name not in SCENARIO_MAP: - if SCENARIO_MAP: - scenario_name = next(iter(SCENARIO_MAP)) - else: - raise ValueError("No scenarios registered") - - config_dict = SCENARIO_MAP[scenario_name] - - return { - 'backend_module': 'rhosocial.activerecord.backend.impl.postgres', - 'backend_class_name': backend_class_name, - 'config_class_module': 'rhosocial.activerecord.backend.impl.postgres.config', - 'config_class_name': 'PostgresConnectionConfig', - 'config_kwargs': config_dict, - 'schema_sql': schema_sql, - } - - def get_worker_schema_sql(self, scenario_name: str, table_name: str) -> str: - """ - Return the SQL statement to create a specific table. - - Args: - scenario_name: The test scenario name (unused for Postgres as schema is fixed) - table_name: Name of the table to create - - Returns: - CREATE TABLE SQL statement - """ - return self._load_postgres_schema(f'{table_name}.sql') - - def _get_schema_sql_for_fixture_type(self, fixture_type: str) -> dict: - """ - Get schema SQL for a specific fixture type. - - Args: - fixture_type: Type of fixture ('order', 'blog', 'user', 'combined') - - Returns: - Dictionary mapping table names to CREATE TABLE statements - """ - schemas = {} - - if fixture_type == 'order': - tables = ['users', 'orders', 'order_items'] - elif fixture_type == 'blog': - tables = ['users', 'posts', 'comments'] - elif fixture_type == 'user': - tables = ['users'] - elif fixture_type == 'combined': - tables = ['users', 'orders', 'order_items', 'posts', 'comments'] - else: - tables = ['users'] - - for table in tables: - schemas[table] = self._load_postgres_schema(f'{table}.sql') - - return schemas + self._created_tables.clear() diff --git a/tests/providers/query_connection.py b/tests/providers/query_connection.py index ab962f1e..7fab8e23 100644 --- a/tests/providers/query_connection.py +++ b/tests/providers/query_connection.py @@ -134,6 +134,10 @@ def cleanup_sync(self, scenario_name: str, pool: BackendPool): # Disconnect backends for backend in self._active_backends: + try: + backend.execute("DROP TABLE IF EXISTS test_users CASCADE") + except Exception: + pass try: backend.disconnect() except Exception: @@ -146,6 +150,10 @@ async def cleanup_async(self, scenario_name: str, pool: AsyncBackendPool): # Disconnect backends for backend in self._active_async_backends: + try: + await backend.execute("DROP TABLE IF EXISTS test_users CASCADE") + except Exception: + pass try: await backend.disconnect() except Exception: diff --git a/tests/providers/registry.py b/tests/providers/registry.py index 34984fff..32738870 100644 --- a/tests/providers/registry.py +++ b/tests/providers/registry.py @@ -6,14 +6,13 @@ interfaces for the postgres backend. """ from rhosocial.activerecord.testsuite.core.registry import ProviderRegistry -from .basic import BasicProvider -from .events import EventsProvider -from .mixins import MixinsProvider -from .query import QueryProvider -from .relation import RelationProvider +from .basic import BasicSyncProvider, BasicAsyncProvider +from .events import EventsSyncProvider, EventsAsyncProvider +from .mixins import MixinsSyncProvider, MixinsAsyncProvider +from .query import QuerySyncProvider, QueryAsyncProvider +from .relation import RelationSyncProvider, RelationAsyncProvider from .basic_connection import BasicConnectionProvider from .query_connection import QueryConnectionProvider -from .composite_pk import CompositePKProvider from .crud_benchmark import CrudBenchmarkProvider from .fastapi_benchmark import FastAPIBenchmarkProvider from .mixin_benchmark import MixinBenchmarkProvider @@ -25,11 +24,25 @@ # Register the concrete implementations as the providers for the # interfaces defined in the testsuite. -provider_registry.register("feature.basic.IBasicProvider", BasicProvider) -provider_registry.register("feature.events.IEventsProvider", EventsProvider) -provider_registry.register("feature.mixins.IMixinsProvider", MixinsProvider) -provider_registry.register("feature.query.IQueryProvider", QueryProvider) -provider_registry.register("feature.relation.IRelationProvider", RelationProvider) +provider_registry.register("feature.basic.IBasicProvider", BasicSyncProvider) +provider_registry.register("feature.basic.IBasicSyncProvider", BasicSyncProvider) +provider_registry.register("feature.basic.IBasicAsyncProvider", BasicAsyncProvider) + +provider_registry.register("feature.events.IEventsProvider", EventsSyncProvider) +provider_registry.register("feature.events.IEventsSyncProvider", EventsSyncProvider) +provider_registry.register("feature.events.IEventsAsyncProvider", EventsAsyncProvider) + +provider_registry.register("feature.mixins.IMixinsProvider", MixinsSyncProvider) +provider_registry.register("feature.mixins.IMixinsSyncProvider", MixinsSyncProvider) +provider_registry.register("feature.mixins.IMixinsAsyncProvider", MixinsAsyncProvider) + +provider_registry.register("feature.query.IQueryProvider", QuerySyncProvider) +provider_registry.register("feature.query.IQuerySyncProvider", QuerySyncProvider) +provider_registry.register("feature.query.IQueryAsyncProvider", QueryAsyncProvider) + +provider_registry.register("feature.relation.IRelationProvider", RelationSyncProvider) +provider_registry.register("feature.relation.IRelationSyncProvider", RelationSyncProvider) +provider_registry.register("feature.relation.IRelationAsyncProvider", RelationAsyncProvider) # Register connection pool context awareness providers provider_registry.register( @@ -41,8 +54,6 @@ QueryConnectionProvider, ) -provider_registry.register("feature.composite_pk.ICompositePKProvider", CompositePKProvider) - # Register benchmark providers. provider_registry.register("benchmark.crud.ICrudBenchmarkProvider", CrudBenchmarkProvider) provider_registry.register("benchmark.query.IQueryBenchmarkProvider", QueryBenchmarkProvider) diff --git a/tests/providers/relation.py b/tests/providers/relation.py index c852ad55..d556166c 100644 --- a/tests/providers/relation.py +++ b/tests/providers/relation.py @@ -1,11 +1,11 @@ # tests/providers/relation.py import asyncio -from typing import Dict, List, Tuple, Type +from typing import Dict, List, Tuple, Type, Set from rhosocial.activerecord.model import ActiveRecord, AsyncActiveRecord from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend from rhosocial.activerecord.backend.impl.postgres.adapters.json import PostgresJSONBAdapter -from rhosocial.activerecord.testsuite.feature.relation.interfaces import IRelationProvider +from rhosocial.activerecord.testsuite.feature.relation.interfaces import IRelationSyncProvider, IRelationAsyncProvider from rhosocial.activerecord.testsuite.feature.relation.fixtures.models import ( Employee, Department, Author, Book, Chapter, Profile, User, Post, Comment, @@ -104,44 +104,22 @@ """ -class RelationProvider(IRelationProvider): - +class RelationProviderBase: def __init__(self): - self._active_backends = [] - self._active_async_backends = [] self._sync_user_post_comment_setup = False self._async_user_post_comment_setup = False self._sync_relation_boundary_setup = False self._async_relation_boundary_setup = False + self._created_tables: Set[str] = set() def get_test_scenarios(self) -> List[str]: return list(get_enabled_scenarios().keys()) - def _execute_script(self, backend, sql: str): - for statement in sql.split(";"): - statement = statement.strip() - if statement: - backend.execute(statement) - - async def _execute_script_async(self, backend, sql: str): - for statement in sql.split(";"): - statement = statement.strip() - if statement: - await backend.execute(statement) - def _configure_with_shared_backend(self, model_class, config, backend_class, backend): model_class.__connection_config__ = config model_class.__backend_class__ = backend_class model_class.__backend__ = backend - def _configure_async_model_without_connection(self, model_class, config, backend=None): - if backend is None: - backend = AsyncPostgresBackend(connection_config=config) - model_class.__connection_config__ = config - model_class.__backend_class__ = AsyncPostgresBackend - model_class.__backend__ = backend - return backend - def _configure_json_field_adapters(self): adapter = PostgresJSONBAdapter() User.__field_adapters__["settings"] = (adapter, str) @@ -151,6 +129,25 @@ def _configure_json_field_adapters(self): AsyncPost.__field_adapters__["metadata"] = (adapter, str) AsyncComment.__field_adapters__["meta"] = (adapter, str) + def _reset_setup_state(self): + self._sync_user_post_comment_setup = False + self._async_user_post_comment_setup = False + self._sync_relation_boundary_setup = False + self._async_relation_boundary_setup = False + + +class RelationSyncProvider(RelationProviderBase, IRelationSyncProvider): + + def __init__(self): + super().__init__() + self._active_backends = [] + + def _execute_script(self, backend, sql: str): + for statement in sql.split(";"): + statement = statement.strip() + if statement: + backend.execute(statement) + def _setup_employee_department(self, scenario_name): backend_class, config = get_scenario(scenario_name) Employee.configure(config, backend_class) @@ -189,23 +186,6 @@ def _setup_user_post_comment_sync(self, scenario_name): self._configure_with_shared_backend(Comment, config, backend_class, backend) self._sync_user_post_comment_setup = True - def _setup_user_post_comment_async(self, scenario_name): - if not self._async_user_post_comment_setup: - self._configure_json_field_adapters() - _, config = get_scenario(scenario_name) - backend = self._configure_async_model_without_connection(AsyncUser, config) - self._configure_async_model_without_connection(AsyncPost, config, backend) - self._configure_async_model_without_connection(AsyncComment, config, backend) - self._active_async_backends.append(backend) - self._async_user_post_comment_setup = True - - async def _ensure_user_post_comment_async_schema(self): - backend = AsyncUser.backend() - if backend not in self._active_async_backends: - self._active_async_backends.append(backend) - await backend.introspect_and_adapt() - await self._execute_script_async(backend, USER_POST_COMMENT_SCHEMA) - def _setup_relation_boundary_sync(self, scenario_name): if not self._sync_relation_boundary_setup: backend_class, config = get_scenario(scenario_name) @@ -216,41 +196,13 @@ def _setup_relation_boundary_sync(self, scenario_name): self._active_backends.append(backend) self._execute_script(backend, RELATION_BOUNDARY_SCHEMA) self._configure_with_shared_backend( - BoundaryProfile, - config, - backend_class, - backend, + BoundaryProfile, config, backend_class, backend, ) self._configure_with_shared_backend( - BoundaryPost, - config, - backend_class, - backend, + BoundaryPost, config, backend_class, backend, ) self._sync_relation_boundary_setup = True - async def _setup_relation_boundary_async(self, scenario_name): - if not self._async_relation_boundary_setup: - _, config = get_scenario(scenario_name) - backend = self._configure_async_model_without_connection( - AsyncBoundaryOwner, - config, - ) - self._configure_async_model_without_connection( - AsyncBoundaryProfile, - config, - backend, - ) - self._configure_async_model_without_connection( - AsyncBoundaryPost, - config, - backend, - ) - self._active_async_backends.append(backend) - await backend.introspect_and_adapt() - await self._execute_script_async(backend, RELATION_BOUNDARY_SCHEMA) - self._async_relation_boundary_setup = True - def setup_employee_department_fixtures( self, scenario_name: str, @@ -260,12 +212,7 @@ def setup_employee_department_fixtures( def setup_author_book_fixtures( self, scenario_name: str, - ) -> Tuple[ - Type[ActiveRecord], - Type[ActiveRecord], - Type[ActiveRecord], - Type[ActiveRecord], - ]: + ) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: return self._setup_author_book(scenario_name) def setup_user_model(self, scenario_name: str) -> Type[ActiveRecord]: @@ -280,18 +227,6 @@ def setup_comment_model(self, scenario_name: str) -> Type[ActiveRecord]: self._setup_user_post_comment_sync(scenario_name) return Comment - def setup_async_user_model(self, scenario_name: str) -> Type[AsyncActiveRecord]: - self._setup_user_post_comment_async(scenario_name) - return AsyncUser - - def setup_async_post_model(self, scenario_name: str) -> Type[AsyncActiveRecord]: - self._setup_user_post_comment_async(scenario_name) - return AsyncPost - - def setup_async_comment_model(self, scenario_name: str) -> Type[AsyncActiveRecord]: - self._setup_user_post_comment_async(scenario_name) - return AsyncComment - def setup_relation_boundary_fixtures( self, scenario_name: str, @@ -299,40 +234,10 @@ def setup_relation_boundary_fixtures( self._setup_relation_boundary_sync(scenario_name) return BoundaryOwner, BoundaryProfile, BoundaryPost - def setup_async_relation_boundary_fixtures( - self, - scenario_name: str, - ) -> Tuple[ - Type[AsyncActiveRecord], - Type[AsyncActiveRecord], - Type[AsyncActiveRecord], - ]: - _, config = get_scenario(scenario_name) - self._configure_async_model_without_connection(AsyncBoundaryOwner, config) - self._configure_async_model_without_connection( - AsyncBoundaryProfile, - config, - AsyncBoundaryOwner.backend(), - ) - self._configure_async_model_without_connection( - AsyncBoundaryPost, - config, - AsyncBoundaryOwner.backend(), - ) - return AsyncBoundaryOwner, AsyncBoundaryProfile, AsyncBoundaryPost - def load_relation_boundary_dataset(self, scenario_name: str, dataset_name: str) -> Dict[str, int]: self._setup_relation_boundary_sync(scenario_name) return self._load_relation_boundary_dataset(dataset_name) - async def load_async_relation_boundary_dataset( - self, - scenario_name: str, - dataset_name: str, - ) -> Dict[str, int]: - await self._setup_relation_boundary_async(scenario_name) - return await self._load_async_relation_boundary_dataset(dataset_name) - def _load_relation_boundary_dataset(self, dataset_name): if dataset_name == "null_foreign_key": profile = BoundaryProfile(bio="No owner", owner_id=None) @@ -365,6 +270,141 @@ def _load_relation_boundary_dataset(self, dataset_name): raise ValueError(f"Unknown relation boundary dataset: {dataset_name}") + def cleanup_after_test(self, scenario_name: str) -> None: + tables_to_drop = [ + 'employees', 'departments', + 'authors', 'books', 'chapters', 'profiles', + 'users', 'posts', 'comments', + 'relation_boundary_owners', 'relation_boundary_profiles', 'relation_boundary_posts', + ] + for backend in self._active_backends: + try: + for table in tables_to_drop: + try: + backend.execute(f'DROP TABLE IF EXISTS "{table}" CASCADE') + except Exception: + pass + finally: + try: + backend.disconnect() + except Exception: + pass + self._active_backends.clear() + self._reset_setup_state() + + +class RelationAsyncProvider(RelationProviderBase, IRelationAsyncProvider): + + def __init__(self): + super().__init__() + self._active_async_backends = [] + + def _configure_async_model_without_connection(self, model_class, config, backend=None): + if backend is None: + backend = AsyncPostgresBackend(connection_config=config) + model_class.__connection_config__ = config + model_class.__backend_class__ = AsyncPostgresBackend + model_class.__backend__ = backend + return backend + + async def _execute_script_async(self, backend, sql: str): + for statement in sql.split(";"): + statement = statement.strip() + if statement: + await backend.execute(statement) + + async def _setup_employee_department_fixtures_async(self, scenario_name): + _, config = get_scenario(scenario_name) + await Employee.configure(config, AsyncPostgresBackend) + backend = Employee.backend() + await backend.connect() + await backend.introspect_and_adapt() + self._active_async_backends.append(backend) + await self._execute_script_async(backend, EMPLOYEE_DEPARTMENT_SCHEMA) + self._configure_with_shared_backend(Department, config, AsyncPostgresBackend, backend) + return Employee, Department + + async def _setup_author_book_fixtures_async(self, scenario_name): + _, config = get_scenario(scenario_name) + await Author.configure(config, AsyncPostgresBackend) + backend = Author.backend() + await backend.connect() + await backend.introspect_and_adapt() + self._active_async_backends.append(backend) + await self._execute_script_async(backend, AUTHOR_BOOK_SCHEMA) + self._configure_with_shared_backend(Book, config, AsyncPostgresBackend, backend) + self._configure_with_shared_backend(Chapter, config, AsyncPostgresBackend, backend) + self._configure_with_shared_backend(Profile, config, AsyncPostgresBackend, backend) + return Author, Book, Chapter, Profile + + async def _setup_user_post_comment_async(self, scenario_name): + if not self._async_user_post_comment_setup: + self._configure_json_field_adapters() + _, config = get_scenario(scenario_name) + backend = self._configure_async_model_without_connection(AsyncUser, config) + self._configure_async_model_without_connection(AsyncPost, config, backend) + self._configure_async_model_without_connection(AsyncComment, config, backend) + self._active_async_backends.append(backend) + await backend.introspect_and_adapt() + await self._execute_script_async(backend, USER_POST_COMMENT_SCHEMA) + self._async_user_post_comment_setup = True + + async def _setup_relation_boundary_async(self, scenario_name): + if not self._async_relation_boundary_setup: + _, config = get_scenario(scenario_name) + backend = self._configure_async_model_without_connection( + AsyncBoundaryOwner, config, + ) + self._configure_async_model_without_connection( + AsyncBoundaryProfile, config, backend, + ) + self._configure_async_model_without_connection( + AsyncBoundaryPost, config, backend, + ) + self._active_async_backends.append(backend) + await backend.introspect_and_adapt() + await self._execute_script_async(backend, RELATION_BOUNDARY_SCHEMA) + self._async_relation_boundary_setup = True + + async def setup_employee_department_fixtures( + self, + scenario_name: str, + ) -> Tuple[Type[ActiveRecord], Type[ActiveRecord]]: + return await self._setup_employee_department_fixtures_async(scenario_name) + + async def setup_author_book_fixtures( + self, + scenario_name: str, + ) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: + return await self._setup_author_book_fixtures_async(scenario_name) + + async def setup_user_model(self, scenario_name: str) -> Type[ActiveRecord]: + await self._setup_user_post_comment_async(scenario_name) + return AsyncUser + + async def setup_post_model(self, scenario_name: str) -> Type[ActiveRecord]: + await self._setup_user_post_comment_async(scenario_name) + return AsyncPost + + async def setup_comment_model(self, scenario_name: str) -> Type[ActiveRecord]: + await self._setup_user_post_comment_async(scenario_name) + return AsyncComment + + async def setup_relation_boundary_fixtures( + self, + scenario_name: str, + ) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: + await self._setup_relation_boundary_async(scenario_name) + return AsyncBoundaryOwner, AsyncBoundaryProfile, AsyncBoundaryPost + + async def load_relation_boundary_dataset( + self, + scenario_name: str, + dataset_name: str, + ) -> Dict[str, int]: + await self._setup_relation_boundary_async(scenario_name) + return await self._load_async_relation_boundary_dataset(dataset_name) + async def _load_async_relation_boundary_dataset(self, dataset_name): if dataset_name == "null_foreign_key": profile = AsyncBoundaryProfile(bio="No owner", owner_id=None) @@ -397,38 +437,24 @@ async def _load_async_relation_boundary_dataset(self, dataset_name): raise ValueError(f"Unknown relation boundary dataset: {dataset_name}") - def _reset_setup_state(self): - self._sync_user_post_comment_setup = False - self._async_user_post_comment_setup = False - self._sync_relation_boundary_setup = False - self._async_relation_boundary_setup = False - - def cleanup_after_test(self, scenario_name: str) -> None: - for backend in self._active_backends: - try: - backend.disconnect() - except Exception: - pass - self._active_backends.clear() - for backend in self._active_async_backends: - try: - asyncio.run(backend.disconnect()) - except Exception: - pass - self._active_async_backends.clear() - self._reset_setup_state() - - async def cleanup_after_test_async(self, scenario_name: str): - for backend in self._active_backends: - try: - backend.disconnect() - except Exception: - pass - self._active_backends.clear() + async def cleanup_after_test(self, scenario_name: str): + tables_to_drop = [ + 'employees', 'departments', + 'authors', 'books', 'chapters', 'profiles', + 'users', 'posts', 'comments', + 'relation_boundary_owners', 'relation_boundary_profiles', 'relation_boundary_posts', + ] for backend in self._active_async_backends: try: - await backend.disconnect() - except Exception: - pass + for table in tables_to_drop: + try: + await backend.execute(f'DROP TABLE IF EXISTS "{table}" CASCADE') + except Exception: + pass + finally: + try: + await backend.disconnect() + except Exception: + pass self._active_async_backends.clear() self._reset_setup_state() diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_drop_table_cascade.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_drop_table_cascade.py new file mode 100644 index 00000000..564abdfd --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_drop_table_cascade.py @@ -0,0 +1,53 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_drop_table_cascade.py +"""Tests for DROP TABLE ... CASCADE/RESTRICT rendering on PostgreSQL. + +PostgreSQL declares full SQL-standard CASCADE/RESTRICT support; the generic +``TableMixin.format_drop_table_statement`` helper (optimistic True/True) +emits the correct token without backend override. These tests pin that +contract so a future override that drops support is caught early. +""" + +import pytest + +from rhosocial.activerecord.backend.expression import DropTableExpression +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect + + +@pytest.fixture +def dialect(): + return PostgresDialect(version=(15, 0, 0)) + + +class TestPostgresDropTableCascade: + def test_capability_switches(self, dialect): + assert dialect.supports_drop_table_cascade() is True + assert dialect.supports_drop_table_restrict() is True + + def test_cascade_renders_standard_token(self, dialect): + expr = DropTableExpression(dialect, table="users", cascade=True) + sql, params = expr.to_sql() + assert sql.endswith(" CASCADE") + assert "CASCADE CONSTRAINTS" not in sql + assert params == () + + def test_restrict_renders_standard_token(self, dialect): + expr = DropTableExpression(dialect, table="users", cascade=False) + sql, params = expr.to_sql() + assert sql.endswith(" RESTRICT") + assert params == () + + def test_cascade_none_omits_token(self, dialect): + expr = DropTableExpression(dialect, table="users", cascade=None) + sql, params = expr.to_sql() + assert "CASCADE" not in sql + assert "RESTRICT" not in sql + assert params == () + + def test_if_exists_combined_with_cascade(self, dialect): + expr = DropTableExpression( + dialect, table="users", if_exists=True, cascade=True + ) + sql, params = expr.to_sql() + assert sql.startswith("DROP TABLE IF EXISTS") + assert sql.endswith(" CASCADE") + assert params == () diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_insert_on_conflict_clauses.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_insert_on_conflict_clauses.py new file mode 100644 index 00000000..fb83bdf1 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_insert_on_conflict_clauses.py @@ -0,0 +1,110 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_insert_on_conflict_clauses.py +"""Tests for PostgreSQL ON CONFLICT clause capability and rendering. + +Covers: +- Capability switches: single clause supported, multiple clauses rejected. +- Single ON CONFLICT DO NOTHING / DO UPDATE rendering (incl. EXCLUDED). +- Multiple ON CONFLICT clauses rejected by the generic gate. +""" + +import pytest + +from rhosocial.activerecord.backend.dialect import UnsupportedFeatureError +from rhosocial.activerecord.backend.expression import ( + Column, + InsertExpression, + Literal, + OnConflictClause, + ValuesSource, +) +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect + + +@pytest.fixture +def dialect(): + return PostgresDialect(version=(15, 0, 0)) + + +class TestPostgresOnConflictCapabilities: + """Capability switch tests.""" + + def test_supports_upsert_pg94(self): + assert PostgresDialect((9, 4, 0)).supports_upsert() is False + + def test_supports_upsert_pg95(self): + assert PostgresDialect((9, 5, 0)).supports_upsert() is True + + def test_supports_upsert_modern(self): + assert PostgresDialect((16, 0, 0)).supports_upsert() is True + + def test_supports_on_conflict_clause(self, dialect): + assert dialect.supports_on_conflict_clause() is True + + def test_supports_on_conflict_clause_old_version(self): + assert PostgresDialect((9, 4, 0)).supports_on_conflict_clause() is True + + def test_does_not_support_multiple_on_conflict_clauses(self, dialect): + assert dialect.supports_multiple_on_conflict_clauses() is False + + def test_multiple_on_conflict_clauses_rejected(self, dialect): + """PostgreSQL grammar allows only one ON CONFLICT clause per INSERT.""" + source = ValuesSource(dialect, values_list=[[Literal(dialect, 1)]]) + clause1 = OnConflictClause(dialect, conflict_target=["col_a"], do_nothing=True) + clause2 = OnConflictClause(dialect, conflict_target=["col_b"], do_nothing=True) + expr = InsertExpression(dialect, into="t", source=source, on_conflict=[clause1, clause2]) + + with pytest.raises(UnsupportedFeatureError, match="multiple ON CONFLICT clauses"): + expr.to_sql() + + +class TestPostgresOnConflictRendering: + """SQL rendering tests for a single ON CONFLICT clause.""" + + def test_do_nothing(self, dialect): + source = ValuesSource(dialect, values_list=[[Literal(dialect, 1)]]) + clause = OnConflictClause(dialect, conflict_target=["id"], do_nothing=True) + expr = InsertExpression(dialect, into="users", columns=["id"], source=source, on_conflict=clause) + sql, params = expr.to_sql() + assert sql == 'INSERT INTO "users" ("id") VALUES (%s) ON CONFLICT ("id") DO NOTHING' + assert params == (1,) + + def test_do_update_with_excluded(self, dialect): + """EXCLUDED pseudo-table must not be double-quoted in PostgreSQL.""" + source = ValuesSource( + dialect, values_list=[[Literal(dialect, 1), Literal(dialect, "new_name")]] + ) + clause = OnConflictClause( + dialect, + conflict_target=["id"], + update_assignments={"name": Column(dialect, "name", "EXCLUDED")}, + ) + expr = InsertExpression( + dialect, into="users", columns=["id", "name"], source=source, on_conflict=clause + ) + sql, params = expr.to_sql() + assert sql == ( + 'INSERT INTO "users" ("id", "name") VALUES (%s, %s) ' + 'ON CONFLICT ("id") DO UPDATE SET "name" = EXCLUDED."name"' + ) + assert params == (1, "new_name") + + def test_do_update_with_where(self, dialect): + source = ValuesSource( + dialect, values_list=[[Literal(dialect, 1), Literal(dialect, 10)]] + ) + clause = OnConflictClause( + dialect, + conflict_target=["id"], + update_assignments={"qty": Column(dialect, "qty", "EXCLUDED")}, + update_where=Column(dialect, "qty", "users") > Column(dialect, "qty", "EXCLUDED"), + ) + expr = InsertExpression( + dialect, into="users", columns=["id", "qty"], source=source, on_conflict=clause + ) + sql, params = expr.to_sql() + assert sql == ( + 'INSERT INTO "users" ("id", "qty") VALUES (%s, %s) ' + 'ON CONFLICT ("id") DO UPDATE SET "qty" = EXCLUDED."qty" ' + 'WHERE "users"."qty" > "EXCLUDED"."qty"' + ) + assert params == (1, 10) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_property_graph_query_format.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_property_graph_query_format.py index 723d351c..2318f5cf 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_property_graph_query_format.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_property_graph_query_format.py @@ -52,6 +52,34 @@ def test_graph_support_protocol_implemented_pg15(self): assert isinstance(d, GraphTableSupport) +class TestPGQLimitationGating: + """Tests that PG 19 correctly reports unsupported features.""" + + def test_quantified_path_unsupported_pg19(self): + d = PostgresDialect((19, 0, 0)) + assert d.supports_quantified_path() is False + + def test_quantified_path_unsupported_pg15(self): + d = PostgresDialect((15, 0, 0)) + assert d.supports_quantified_path() is False + + def test_comma_separated_patterns_unsupported_pg19(self): + d = PostgresDialect((19, 0, 0)) + assert d.supports_comma_separated_patterns() is False + + def test_comma_separated_patterns_unsupported_pg15(self): + d = PostgresDialect((15, 0, 0)) + assert d.supports_comma_separated_patterns() is False + + def test_quantified_path_raises_on_format(self): + d = PostgresDialect((19, 0, 0)) + from rhosocial.activerecord.backend.expression import QuantifiedPath + edge = GraphEdge(d, "e", "knows", GraphEdgeDirection.RIGHT) + qp = QuantifiedPath(d, edge) + with pytest.raises(UnsupportedFeatureError): + qp.to_sql() + + @pytest.fixture def pg19_dialect(): return PostgresDialect((19, 0, 0)) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_index_pg.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_index_pg.py index 6be4882a..40fc7ee7 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_index_pg.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_index_pg.py @@ -518,6 +518,17 @@ def test_format_fulltext_match_boolean(self): assert "plainto_tsquery" not in sql assert params == ("a & b",) + def test_format_fulltext_match_phrase(self): + sql, params = PostgresDialect().format_fulltext_match(["col"], "cat dog", mode="PHRASE") + assert "phraseto_tsquery" in sql + assert "plainto_tsquery" not in sql + assert params == ("cat dog",) + + def test_format_fulltext_match_unknown_mode_uses_plain(self): + sql, params = PostgresDialect().format_fulltext_match(["col"], "search", mode="CUSTOM") + assert "plainto_tsquery" in sql + assert params == ("search",) + def test_format_fulltext_match_multi_column(self): sql, params = PostgresDialect().format_fulltext_match(["a", "b"], "search") assert " || " in sql @@ -551,6 +562,19 @@ def test_format_create_fulltext_index_statement_if_not_exists(self): sql, _ = dialect.format_create_fulltext_index_statement(expr) assert "IF NOT EXISTS" in sql + def test_format_create_fulltext_index_statement_multi_column(self): + dialect = PostgresDialect() + expr = CreateFulltextIndexExpression( + dialect, + index_name="idx_ft", + table_name="articles", + columns=["title", "body"], + ) + sql, _ = dialect.format_create_fulltext_index_statement(expr) + assert " || " in sql + assert "to_tsvector" in sql + assert "title" in sql and "body" in sql + def test_format_drop_fulltext_index_statement(self): dialect = PostgresDialect() expr = DropFulltextIndexExpression( diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py new file mode 100644 index 00000000..b05c6fb1 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py @@ -0,0 +1,510 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py +""" +Recursive CTE graph traversal tests — PGQ quantified path alternative. + +These tests verify that recursive CTE + SetOperationExpression + UNION ALL +correctly simulates PGQ quantified path patterns (not natively supported +in PostgreSQL 19). + +Scenarios: +1. Social Network: N-degree friend recommendations +2. Anti-Money Laundering: Fund source tracing +""" +import pytest +import pytest_asyncio + +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, + InsertExpression, + ValuesSource, + DropTableExpression, + QueryExpression, + TableExpression, + CTEExpression, + WithQueryExpression, + SetOperationExpression, + FunctionCall, + ColumnDefinition, + ColumnConstraint, + ColumnConstraintType, +) +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, + VarCharType, + DecimalType, +) +from rhosocial.activerecord.backend.expression.core import Literal, Column +from rhosocial.activerecord.backend.expression.predicates import BetweenPredicate +from rhosocial.activerecord.backend.expression.query_parts import ( + WhereClause, + OrderByClause, + GroupByHavingClause, + JoinExpression, +) + + +@pytest.fixture +def social_network_data(postgres_backend): + """Fixture: create users + follows tables with test data.""" + backend = postgres_backend + dialect = backend.dialect + + for t in ("follows", "users"): + backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) + + backend.execute(*CreateTableExpression(dialect, "users", [ + ColumnDefinition("id", IntegerType(), constraints=[ + ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(100)), + ]).to_sql()) + + backend.execute(*CreateTableExpression(dialect, "follows", [ + ColumnDefinition("id", IntegerType(), constraints=[ + ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("follower_id", IntegerType()), + ColumnDefinition("followed_id", IntegerType()), + ]).to_sql()) + + backend.execute(*InsertExpression(dialect, "users", columns=["id", "name"], + source=ValuesSource(dialect, [ + [Literal(dialect, 1), Literal(dialect, "Alice")], + [Literal(dialect, 2), Literal(dialect, "Bob")], + [Literal(dialect, 3), Literal(dialect, "Charlie")], + [Literal(dialect, 4), Literal(dialect, "Diana")], + [Literal(dialect, 5), Literal(dialect, "Eve")], + ])).to_sql()) + + backend.execute(*InsertExpression(dialect, "follows", columns=["id", "follower_id", "followed_id"], + source=ValuesSource(dialect, [ + [Literal(dialect, 1), Literal(dialect, 1), Literal(dialect, 2)], + [Literal(dialect, 2), Literal(dialect, 2), Literal(dialect, 3)], + [Literal(dialect, 3), Literal(dialect, 1), Literal(dialect, 3)], + [Literal(dialect, 4), Literal(dialect, 4), Literal(dialect, 1)], + ])).to_sql()) + + yield + + for t in ("follows", "users"): + backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) + + +@pytest.fixture +def aml_data(postgres_backend): + """Fixture: create accounts + transactions tables with test data.""" + backend = postgres_backend + dialect = backend.dialect + + for t in ("transactions", "accounts"): + backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) + + backend.execute(*CreateTableExpression(dialect, "accounts", [ + ColumnDefinition("id", IntegerType(), constraints=[ + ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("account_holder", VarCharType(100)), + ColumnDefinition("account_type", VarCharType(20)), + ]).to_sql()) + + backend.execute(*CreateTableExpression(dialect, "transactions", [ + ColumnDefinition("id", IntegerType(), constraints=[ + ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("source_account_id", IntegerType()), + ColumnDefinition("target_account_id", IntegerType()), + ColumnDefinition("amount", DecimalType(12, 2)), + ]).to_sql()) + + backend.execute(*InsertExpression(dialect, "accounts", columns=["id", "account_holder", "account_type"], + source=ValuesSource(dialect, [ + [Literal(dialect, 1), Literal(dialect, "Alice Smith"), Literal(dialect, "checking")], + [Literal(dialect, 2), Literal(dialect, "Bob Corp"), Literal(dialect, "business")], + [Literal(dialect, 3), Literal(dialect, "Charlie Ltd"), Literal(dialect, "business")], + [Literal(dialect, 4), Literal(dialect, "Diana Offshore"), Literal(dialect, "offshore")], + [Literal(dialect, 5), Literal(dialect, "Eve Holding"), Literal(dialect, "offshore")], + ])).to_sql()) + + backend.execute(*InsertExpression(dialect, "transactions", columns=[ + "id", "source_account_id", "target_account_id", "amount"], + source=ValuesSource(dialect, [ + [Literal(dialect, 1), Literal(dialect, 4), Literal(dialect, 3), Literal(dialect, 500000)], + [Literal(dialect, 2), Literal(dialect, 5), Literal(dialect, 3), Literal(dialect, 300000)], + [Literal(dialect, 3), Literal(dialect, 3), Literal(dialect, 2), Literal(dialect, 600000)], + [Literal(dialect, 4), Literal(dialect, 2), Literal(dialect, 1), Literal(dialect, 450000)], + ])).to_sql()) + + yield + + for t in ("transactions", "accounts"): + backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) + + +class TestSocialNetworkTraversal: + """Recursive CTE for N-degree friend recommendations.""" + + def _build_traversal_cte(self, dialect): + """Build recursive CTE expression for social network traversal.""" + base = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, "id"), + Column(dialect, "name"), + Literal(dialect, 0).as_("depth"), + ], + from_=TableExpression(dialect, "users"), + where=WhereClause(dialect, condition=Column(dialect, "name") == Literal(dialect, "Alice")), + ) + + recursive_join = JoinExpression( + dialect=dialect, + left_table=TableExpression(dialect, "traversal", alias="t"), + right_table=TableExpression(dialect, "follows", alias="f"), + join_type="INNER JOIN", + condition=Column(dialect, "id", table="t") == Column(dialect, "follower_id", table="f"), + ).inner_join( + right_table=TableExpression(dialect, "users", alias="u"), + condition=Column(dialect, "followed_id", table="f") == Column(dialect, "id", table="u"), + ) + + recursive = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, "id", table="u"), + Column(dialect, "name", table="u"), + (Column(dialect, "depth", table="t") + Literal(dialect, 1)).as_("depth"), + ], + from_=recursive_join, + where=WhereClause(dialect, condition=Column(dialect, "depth", table="t") < Literal(dialect, 4)), + ) + + union = SetOperationExpression( + dialect=dialect, + left=base, + right=recursive, + operation="UNION ALL", + ) + + return CTEExpression( + dialect=dialect, + name="traversal", + query=union, + columns=["id", "name", "depth"], + ) + + def test_friend_recommendation_depth_1_to_3(self, postgres_backend, social_network_data): + """Q1: Friend recommendations at depths 1-3 (simulate {1,3}).""" + dialect = postgres_backend.dialect + cte = self._build_traversal_cte(dialect) + + main = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, "id"), + Column(dialect, "name"), + Column(dialect, "depth"), + ], + from_=TableExpression(dialect, "traversal"), + where=WhereClause(dialect, condition=BetweenPredicate( + dialect, Column(dialect, "depth"), Literal(dialect, 1), Literal(dialect, 3), + )), + order_by=OrderByClause(dialect, [Column(dialect, "depth"), Column(dialect, "name")]), + ) + + with_query = WithQueryExpression( + dialect=dialect, + ctes=[cte], + main_query=main, + recursive=True, + ) + + sql, params = with_query.to_sql() + rows = postgres_backend.fetch_all(sql, params) + names_at_depth = {r["name"]: r["depth"] for r in rows} + + # Alice follows Bob (depth 1) and Charlie (depth 1) + assert names_at_depth.get("Bob") == 1 + # Charlie also appears at depth 2 (via Bob), dict stores last occurrence + assert names_at_depth.get("Charlie") == 2 + + def test_friend_recommendation_depth_2(self, postgres_backend, social_network_data): + """Q2: Only depth=2 (simulate {2,2}).""" + dialect = postgres_backend.dialect + cte = self._build_traversal_cte(dialect) + + main = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, "id"), + Column(dialect, "name"), + Column(dialect, "depth"), + ], + from_=TableExpression(dialect, "traversal"), + where=WhereClause(dialect, condition=Column(dialect, "depth") == Literal(dialect, 2)), + order_by=OrderByClause(dialect, [Column(dialect, "name")]), + ) + + with_query = WithQueryExpression( + dialect=dialect, + ctes=[cte], + main_query=main, + recursive=True, + ) + + sql, params = with_query.to_sql() + rows = postgres_backend.fetch_all(sql, params) + names = [r["name"] for r in rows] + assert "Charlie" in names + assert len(names) == 1 + + def test_exclude_start_node(self, postgres_backend, social_network_data): + """Q3: Exclude Alice herself by filtering depth >= 1.""" + dialect = postgres_backend.dialect + cte = self._build_traversal_cte(dialect) + + main = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, "id"), + Column(dialect, "name"), + Column(dialect, "depth"), + ], + from_=TableExpression(dialect, "traversal"), + where=WhereClause(dialect, condition=Column(dialect, "depth") >= Literal(dialect, 1)), + order_by=OrderByClause(dialect, [Column(dialect, "depth"), Column(dialect, "name")]), + ) + + with_query = WithQueryExpression( + dialect=dialect, + ctes=[cte], + main_query=main, + recursive=True, + ) + + sql, params = with_query.to_sql() + rows = postgres_backend.fetch_all(sql, params) + names = [r["name"] for r in rows] + assert "Alice" not in names + assert len(names) == 3 # Bob, Charlie (depth=1), Charlie (depth=2) + + +class TestAMLFundTracing: + """Recursive CTE for fund source tracing (AML).""" + + def _build_aml_cte(self, dialect): + """Build recursive CTE expression for fund tracing.""" + base_join = JoinExpression( + dialect=dialect, + left_table=TableExpression(dialect, "transactions", alias="tx"), + right_table=TableExpression(dialect, "accounts", alias="a"), + join_type="INNER JOIN", + condition=Column(dialect, "source_account_id", table="tx") == Column(dialect, "id", table="a"), + ) + + base = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, "id", table="a"), + Column(dialect, "account_holder", table="a"), + Column(dialect, "account_type", table="a"), + Column(dialect, "amount", table="tx"), + Literal(dialect, 1).as_("depth"), + ], + from_=base_join, + where=WhereClause( + dialect, + condition=Column(dialect, "target_account_id", table="tx") + == Literal(dialect, 1), + ), + ) + + recursive_join = JoinExpression( + dialect=dialect, + left_table=TableExpression(dialect, "fund_trace", alias="tr"), + right_table=TableExpression(dialect, "transactions", alias="tx"), + join_type="INNER JOIN", + condition=Column(dialect, "target_account_id", table="tx") == Column(dialect, "id", table="tr"), + ).inner_join( + right_table=TableExpression(dialect, "accounts", alias="a"), + condition=Column(dialect, "source_account_id", table="tx") == Column(dialect, "id", table="a"), + ) + + recursive = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, "id", table="a"), + Column(dialect, "account_holder", table="a"), + Column(dialect, "account_type", table="a"), + Column(dialect, "amount", table="tx"), + (Column(dialect, "depth", table="tr") + Literal(dialect, 1)).as_("depth"), + ], + from_=recursive_join, + where=WhereClause(dialect, condition=Column(dialect, "depth", table="tr") < Literal(dialect, 5)), + ) + + union = SetOperationExpression( + dialect=dialect, + left=base, + right=recursive, + operation="UNION ALL", + ) + + return CTEExpression( + dialect=dialect, + name="fund_trace", + query=union, + columns=["id", "account_holder", "account_type", "amount", "depth"], + ) + + def test_fund_trace_chain(self, postgres_backend, aml_data): + """Trace fund sources for account ACC-001.""" + dialect = postgres_backend.dialect + cte = self._build_aml_cte(dialect) + + main = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, "id"), + Column(dialect, "account_holder"), + Column(dialect, "depth"), + ], + from_=TableExpression(dialect, "fund_trace"), + where=WhereClause(dialect, condition=Column(dialect, "depth") >= Literal(dialect, 1)), + order_by=OrderByClause(dialect, [Column(dialect, "depth"), Column(dialect, "id")]), + ) + + with_query = WithQueryExpression( + dialect=dialect, + ctes=[cte], + main_query=main, + recursive=True, + ) + + sql, params = with_query.to_sql() + rows = postgres_backend.fetch_all(sql, params) + account_names = [r["account_holder"] for r in rows] + # Direct source: Bob Corp (depth 1) + assert "Bob Corp" in account_names + # Indirect: Charlie Ltd (depth 2) -> Bob Corp + assert "Charlie Ltd" in account_names + # Indirect: Diana Offshore (depth 3) -> Charlie Ltd + assert "Diana Offshore" in account_names + + def test_fund_trace_depth_filter(self, postgres_backend, aml_data): + """Filter fund sources by minimum depth.""" + dialect = postgres_backend.dialect + cte = self._build_aml_cte(dialect) + + main = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, "id"), + Column(dialect, "account_holder"), + Column(dialect, "depth"), + ], + from_=TableExpression(dialect, "fund_trace"), + where=WhereClause(dialect, condition=Column(dialect, "depth") >= Literal(dialect, 2)), + order_by=OrderByClause(dialect, [Column(dialect, "depth"), Column(dialect, "id")]), + ) + + with_query = WithQueryExpression( + dialect=dialect, + ctes=[cte], + main_query=main, + recursive=True, + ) + + sql, params = with_query.to_sql() + rows = postgres_backend.fetch_all(sql, params) + depths = [r["depth"] for r in rows] + assert all(d >= 2 for d in depths) + # Depth 2: Charlie Ltd (via Bob Corp) + assert any(r["account_holder"] == "Charlie Ltd" for r in rows) + + def test_fund_trace_aggregation(self, postgres_backend, aml_data): + """Aggregate total amount by depth level.""" + dialect = postgres_backend.dialect + cte = self._build_aml_cte(dialect) + + agg_main = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, "depth"), + FunctionCall(dialect, "SUM", Column(dialect, "amount"), alias="total"), + ], + from_=TableExpression(dialect, "fund_trace"), + group_by_having=GroupByHavingClause( + dialect, group_by=[Column(dialect, "depth")] + ), + order_by=OrderByClause(dialect, [Column(dialect, "depth")]), + ) + + with_query = WithQueryExpression( + dialect=dialect, + ctes=[cte], + main_query=agg_main, + recursive=True, + ) + + sql, params = with_query.to_sql() + rows = postgres_backend.fetch_all(sql, params) + totals = {r["depth"]: float(r["total"]) for r in rows} + # Depth 1: Bob Corp sent 450000 to Alice Smith + assert totals.get(1) == 450000 + # Depth 2: Charlie Ltd sent 600000 to Bob Corp + assert totals.get(2) == 600000 + # Depth 3: Diana Offshore (500000) + Eve Holding (300000) sent to Charlie Ltd + assert totals.get(3) == 800000 + + +class TestAsyncRecursiveCTEGraph: + """Async versions of recursive CTE graph tests.""" + + @pytest_asyncio.fixture + async def async_social_network_data(self, async_postgres_backend): + backend = async_postgres_backend + dialect = backend.dialect + + for t in ("follows", "users"): + await backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) + + await backend.execute(*CreateTableExpression(dialect, "users", [ + ColumnDefinition("id", IntegerType(), constraints=[ + ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(100)), + ]).to_sql()) + + await backend.execute(*CreateTableExpression(dialect, "follows", [ + ColumnDefinition("id", IntegerType(), constraints=[ + ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("follower_id", IntegerType()), + ColumnDefinition("followed_id", IntegerType()), + ]).to_sql()) + + await backend.execute(*InsertExpression(dialect, "users", columns=["id", "name"], + source=ValuesSource(dialect, [ + [Literal(dialect, 1), Literal(dialect, "Alice")], + [Literal(dialect, 2), Literal(dialect, "Bob")], + ])).to_sql()) + + await backend.execute(*InsertExpression(dialect, "follows", columns=["id", "follower_id", "followed_id"], + source=ValuesSource(dialect, [ + [Literal(dialect, 1), Literal(dialect, 1), Literal(dialect, 2)], + ])).to_sql()) + + yield + + for t in ("follows", "users"): + await backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) + + @pytest.mark.asyncio + async def test_async_single_hop(self, async_postgres_backend, async_social_network_data): + """Async: Alice's 1-hop friends.""" + dialect = async_postgres_backend.dialect + base = QueryExpression( + dialect=dialect, + select=[Column(dialect, "id"), Column(dialect, "name"), Literal(dialect, 0).as_("depth")], + from_=TableExpression(dialect, "users"), + where=WhereClause(dialect, condition=Column(dialect, "name") == Literal(dialect, "Alice")), + ) + result = base.to_sql() + sql, params = result + rows = await async_postgres_backend.fetch_all(sql, params) + assert len(rows) == 1 + assert rows[0]["name"] == "Alice" diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cursor_pollution.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cursor_pollution.py index a2787e23..c174b2ce 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cursor_pollution.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cursor_pollution.py @@ -19,16 +19,15 @@ class TestPostgresCursorPollution: """Cursor pollution: sync PostgreSQL backend.""" - def test_get_server_version_then_query(self, pg_backend): + def test_get_server_version_then_query(self, postgres_backend): """get_server_version() then a normal query.""" - backend = pg_backend + backend = postgres_backend version = backend.get_server_version() assert version is not None cursor = backend._get_cursor() cursor.execute("SELECT 1 AS marker") rows = cursor.fetchall() - cursor.close() assert len(rows) > 0 assert cursor.description is not None @@ -36,16 +35,16 @@ def test_get_server_version_then_query(self, pg_backend): assert col_name == "marker", ( f"cursor.description polluted! expected 'marker', got: {col_name}" ) + cursor.close() - def test_introspect_and_adapt_then_query(self, pg_backend): + def test_introspect_and_adapt_then_query(self, postgres_backend): """introspect_and_adapt() then a normal query.""" - backend = pg_backend + backend = postgres_backend backend.introspect_and_adapt() cursor = backend._get_cursor() cursor.execute("SELECT 'ok' AS status") rows = cursor.fetchall() - cursor.close() assert len(rows) > 0 assert cursor.description is not None @@ -53,10 +52,11 @@ def test_introspect_and_adapt_then_query(self, pg_backend): assert col_name == "status", ( f"cursor.description polluted! expected 'status', got: {col_name}" ) + cursor.close() - def test_high_frequency_version_query_cycle(self, pg_backend): + def test_high_frequency_version_query_cycle(self, postgres_backend): """Repeated get_server_version → query cycle to expose state leaks.""" - backend = pg_backend + backend = postgres_backend for i in range(200): backend.get_server_version() @@ -64,7 +64,6 @@ def test_high_frequency_version_query_cycle(self, pg_backend): cursor = backend._get_cursor() cursor.execute(f"SELECT {i} AS cycle") rows = cursor.fetchall() - cursor.close() assert len(rows) > 0 assert cursor.description is not None @@ -73,6 +72,7 @@ def test_high_frequency_version_query_cycle(self, pg_backend): f"Iteration {i}: cursor.description polluted! " f"expected 'cycle', got: {col_name}" ) + cursor.close() logger.info("PostgreSQL high-frequency version query cycle 200 iterations passed") @@ -81,16 +81,15 @@ class TestAsyncPostgresCursorPollution: """Cursor pollution: async PostgreSQL backend.""" @pytest.mark.asyncio - async def test_get_server_version_then_query(self, async_pg_backend): + async def test_get_server_version_then_query(self, async_postgres_backend): """Async get_server_version() then a normal query.""" - backend = async_pg_backend + backend = async_postgres_backend version = await backend.get_server_version() assert version is not None cursor = await backend._get_cursor() await cursor.execute("SELECT 1 AS marker") rows = await cursor.fetchall() - await cursor.close() assert len(rows) > 0 assert cursor.description is not None @@ -98,17 +97,17 @@ async def test_get_server_version_then_query(self, async_pg_backend): assert col_name == "marker", ( f"Async cursor.description polluted! expected 'marker', got: {col_name}" ) + await cursor.close() @pytest.mark.asyncio - async def test_introspect_and_adapt_then_query(self, async_pg_backend): + async def test_introspect_and_adapt_then_query(self, async_postgres_backend): """Async introspect_and_adapt() then a normal query.""" - backend = async_pg_backend + backend = async_postgres_backend await backend.introspect_and_adapt() cursor = await backend._get_cursor() await cursor.execute("SELECT 'ok' AS status") rows = await cursor.fetchall() - await cursor.close() assert len(rows) > 0 assert cursor.description is not None @@ -117,17 +116,17 @@ async def test_introspect_and_adapt_then_query(self, async_pg_backend): f"Async cursor.description polluted after introspect! " f"expected 'status', got: {col_name}" ) + await cursor.close() @pytest.mark.asyncio - async def test_context_entry_workflow(self, async_pg_backend): + async def test_context_entry_workflow(self, async_postgres_backend): """backend.context() workflow (production pattern).""" - backend = async_pg_backend + backend = async_postgres_backend async with backend.context(): cursor = await backend._get_cursor() await cursor.execute("SELECT 'ctx_ok' AS ctx_status") rows = await cursor.fetchall() - await cursor.close() assert len(rows) > 0 assert cursor.description is not None @@ -136,11 +135,12 @@ async def test_context_entry_workflow(self, async_pg_backend): f"Async cursor.description polluted after context()! " f"expected 'ctx_status', got: {col_name}" ) + await cursor.close() @pytest.mark.asyncio - async def test_high_frequency_version_query_cycle(self, async_pg_backend): + async def test_high_frequency_version_query_cycle(self, async_postgres_backend): """Async high-frequency version query cycle to expose state leaks.""" - backend = async_pg_backend + backend = async_postgres_backend for i in range(200): await backend.get_server_version() @@ -148,7 +148,6 @@ async def test_high_frequency_version_query_cycle(self, async_pg_backend): cursor = await backend._get_cursor() await cursor.execute(f"SELECT {i} AS cycle") rows = await cursor.fetchall() - await cursor.close() assert len(rows) > 0 assert cursor.description is not None @@ -157,5 +156,6 @@ async def test_high_frequency_version_query_cycle(self, async_pg_backend): f"Iteration {i}: async cursor.description polluted! " f"expected 'cycle', got: {col_name}" ) + await cursor.close() logger.info("Async PostgreSQL high-frequency version query cycle 200 iterations passed") \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_data_type_formatting.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_data_type_formatting.py new file mode 100644 index 00000000..c7c286f9 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_data_type_formatting.py @@ -0,0 +1,435 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_data_type_formatting.py +"""Pure-function tests for PostgreSQL DataType formatting and parsing. + +These tests exercise ``PostgresTypeFormatSupportMixin`` (the +``PostgresDialect.format_data_type`` and ``PostgresDialect.parse_type`` +paths) without requiring a live PostgreSQL server. +""" + +import pytest + +from rhosocial.activerecord.backend.expression.types import ( + ArrayType, + BigIntType, + BlobType, + BooleanType, + CharType, + CustomType, + DateType, + DateTimeType, + DecimalType, + DoubleType, + FloatType, + IntegerType, + IntType, + IntervalType, + JsonBType, + JsonType, + RealType, + SmallIntType, + TextType, + TimeType, + TimeTzType, + TimestampType, + TimestampTzType, + TinyIntType, + VarCharType, +) +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + PostgresBigSerialType, + PostgresBitType, + PostgresBoxType, + PostgresByteaType, + PostgresCIDType, + PostgresCharacterVaryingType, + PostgresCidrType, + PostgresCircleType, + PostgresCitextType, + PostgresCubeType, + PostgresDateMultirangeType, + PostgresDateRangeType, + PostgresGeographyType, + PostgresGeometryType, + PostgresHstoreType, + PostgresInetType, + PostgresInt4MultirangeType, + PostgresInt4RangeType, + PostgresInt8MultirangeType, + PostgresInt8RangeType, + PostgresJsonPathType, + PostgresLineSegmentType, + PostgresLineType, + PostgresLtreeType, + PostgresMacAddr8Type, + PostgresMacAddrType, + PostgresMoneyType, + PostgresNumMultirangeType, + PostgresNumRangeType, + PostgresOIDType, + PostgresPathType, + PostgresPgLSNType, + PostgresPointType, + PostgresPolygonType, + PostgresRasterType, + PostgresRegClassType, + PostgresRegTypeType, + PostgresSerialType, + PostgresSmallSerialType, + PostgresTIDType, + PostgresTSQueryType, + PostgresTSVectorType, + PostgresTsMultirangeType, + PostgresTsRangeType, + PostgresTsTzMultirangeType, + PostgresTsTzRangeType, + PostgresUUIDType, + PostgresVarBitType, + PostgresVectorType, + PostgresXID8Type, + PostgresXIDType, + PostgresXMLType, +) + + +@pytest.fixture +def dialect(): + return PostgresDialect(version=(15, 0, 0)) + + +# --------------------------------------------------------------------------- +# format_data_type — PostgreSQL-specific types +# --------------------------------------------------------------------------- + +FORMAT_CASES = [ + (PostgresByteaType(), "BYTEA"), + (PostgresSmallSerialType(), "SMALLSERIAL"), + (PostgresSerialType(), "SERIAL"), + (PostgresBigSerialType(), "BIGSERIAL"), + (PostgresUUIDType(), "UUID"), + (PostgresXMLType(), "XML"), + (PostgresTSVectorType(), "TSVECTOR"), + (PostgresTSQueryType(), "TSQUERY"), + (PostgresJsonPathType(), "JSONPATH"), + (PostgresBitType(), "BIT"), + (PostgresBitType(n=8), "BIT(8)"), + (PostgresVarBitType(), "VARBIT"), + (PostgresVarBitType(n=16), "VARBIT(16)"), + (PostgresInetType(), "INET"), + (PostgresCidrType(), "CIDR"), + (PostgresMacAddrType(), "MACADDR"), + (PostgresMacAddr8Type(), "MACADDR8"), + (PostgresPointType(), "POINT"), + (PostgresLineType(), "LINE"), + (PostgresLineSegmentType(), "LSEG"), + (PostgresBoxType(), "BOX"), + (PostgresPathType(), "PATH"), + (PostgresPolygonType(), "POLYGON"), + (PostgresCircleType(), "CIRCLE"), + (PostgresMoneyType(), "MONEY"), + (PostgresInt4RangeType(), "INT4RANGE"), + (PostgresInt8RangeType(), "INT8RANGE"), + (PostgresNumRangeType(), "NUMRANGE"), + (PostgresTsRangeType(), "TSRANGE"), + (PostgresTsTzRangeType(), "TSTZRANGE"), + (PostgresDateRangeType(), "DATERANGE"), + (PostgresInt4MultirangeType(), "INT4MULTIRANGE"), + (PostgresInt8MultirangeType(), "INT8MULTIRANGE"), + (PostgresNumMultirangeType(), "NUMMULTIRANGE"), + (PostgresTsMultirangeType(), "TSMULTIRANGE"), + (PostgresTsTzMultirangeType(), "TSTZMULTIRANGE"), + (PostgresDateMultirangeType(), "DATEMULTIRANGE"), + (PostgresOIDType(), "OID"), + (PostgresRegClassType(), "REGCLASS"), + (PostgresRegTypeType(), "REGTYPE"), + (PostgresXIDType(), "XID"), + (PostgresXID8Type(), "XID8"), + (PostgresCIDType(), "CID"), + (PostgresTIDType(), "TID"), + (PostgresPgLSNType(), "PG_LSN"), + (PostgresHstoreType(), "HSTORE"), + (PostgresGeometryType(), "GEOMETRY"), + (PostgresGeographyType(), "GEOGRAPHY"), + (PostgresVectorType(dim=384), "VECTOR(384)"), + (PostgresCitextType(), "CITEXT"), + (PostgresCubeType(), "CUBE"), + (PostgresLtreeType(), "LTREE"), + (PostgresRasterType(), "RASTER"), + (PostgresCharacterVaryingType(), "CHARACTER VARYING"), + (PostgresCharacterVaryingType(length=100), "CHARACTER VARYING(100)"), +] + + +@pytest.mark.parametrize("data_type,expected", FORMAT_CASES) +def test_format_postgres_specific_types(dialect, data_type, expected): + sql, params = dialect.format_data_type(data_type) + assert sql == expected + assert params == () + + +# --------------------------------------------------------------------------- +# format_data_type — core types with PostgreSQL specialisation +# --------------------------------------------------------------------------- + +CORE_FORMAT_CASES = [ + (TinyIntType(), "SMALLINT"), + (SmallIntType(), "SMALLINT"), + (IntType(), "INTEGER"), + (IntegerType(), "INTEGER"), + (BigIntType(), "BIGINT"), + (FloatType(), "REAL"), + (FloatType(precision=8), "FLOAT(8)"), + (RealType(), "REAL"), + (DoubleType(), "DOUBLE PRECISION"), + (DecimalType(), "DECIMAL"), + (DecimalType(precision=10), "DECIMAL(10)"), + (DecimalType(precision=10, scale=2), "DECIMAL(10,2)"), + (CharType(), "CHAR"), + (CharType(length=5), "CHAR(5)"), + (VarCharType(), "VARCHAR"), + (VarCharType(length=50), "VARCHAR(50)"), + (TextType(), "TEXT"), + (BooleanType(), "BOOLEAN"), + (BlobType(), "BYTEA"), + (DateType(), "DATE"), + (TimeType(), "TIME"), + (TimeType(precision=6), "TIME(6)"), + (TimeTzType(), "TIME WITH TIME ZONE"), + (TimeTzType(precision=6), "TIME(6) WITH TIME ZONE"), + (DateTimeType(), "TIMESTAMP"), + (TimestampType(), "TIMESTAMP"), + (TimestampType(precision=3), "TIMESTAMP(3)"), + (TimestampTzType(), "TIMESTAMP WITH TIME ZONE"), + (TimestampTzType(precision=3), "TIMESTAMP(3) WITH TIME ZONE"), + (IntervalType(), "INTERVAL"), + (IntervalType(fields="DAY TO SECOND"), "INTERVAL DAY TO SECOND"), + (JsonType(), "JSON"), + (JsonBType(), "JSONB"), +] + + +@pytest.mark.parametrize("data_type,expected", CORE_FORMAT_CASES) +def test_format_core_types(dialect, data_type, expected): + sql, params = dialect.format_data_type(data_type) + assert sql == expected + assert params == () + + +def test_format_array_type(dialect): + sql, _ = dialect.format_data_type(ArrayType(IntegerType(), dimensions=1)) + assert sql == "INTEGER[]" + + sql, _ = dialect.format_data_type(ArrayType(VarCharType(10), dimensions=3)) + assert sql == "VARCHAR(10)[][][]" + + +def test_format_array_type_multidimensional(dialect): + sql, _ = dialect.format_data_type(ArrayType(IntegerType(), dimensions=2)) + assert sql == "INTEGER[][]" + + +def test_format_array_type_with_postgres_element(dialect): + sql, _ = dialect.format_data_type(ArrayType(PostgresUUIDType(), dimensions=1)) + assert sql == "UUID[]" + + +def test_format_array_type_unsupported_element_raises(dialect): + with pytest.raises(TypeError, match="is not supported"): + dialect.format_data_type(ArrayType(CustomType("X"), dimensions=1)) + + +def test_format_unregistered_type_raises(dialect): + with pytest.raises(TypeError, match="does not support"): + dialect.format_data_type(CustomType("X")) + + +# --------------------------------------------------------------------------- +# parse_type — round-trip families +# --------------------------------------------------------------------------- + +PARSE_CASES = [ + ("SMALLINT", SmallIntType, {}), + ("INT2", SmallIntType, {}), + ("INT", IntegerType, {}), + ("INTEGER", IntegerType, {}), + ("INT4", IntegerType, {}), + ("BIGINT", BigIntType, {}), + ("INT8", BigIntType, {}), + ("SMALLSERIAL", PostgresSmallSerialType, {}), + ("SERIAL2", PostgresSmallSerialType, {}), + ("SERIAL", PostgresSerialType, {}), + ("SERIAL4", PostgresSerialType, {}), + ("BIGSERIAL", PostgresBigSerialType, {}), + ("SERIAL8", PostgresBigSerialType, {}), + ("REAL", RealType, {}), + ("FLOAT4", RealType, {}), + ("DOUBLE", DoubleType, {}), + ("DOUBLE PRECISION", DoubleType, {}), + ("FLOAT", FloatType, {}), + ("FLOAT(8)", FloatType, {"precision": 8}), + ("DECIMAL", DecimalType, {}), + ("NUMERIC", DecimalType, {}), + ("DECIMAL(10)", DecimalType, {"precision": 10}), + ("DECIMAL(10,2)", DecimalType, {"precision": 10, "scale": 2}), + ("NUMERIC(12,4)", DecimalType, {"precision": 12, "scale": 4}), + ("CHAR", CharType, {}), + ("CHAR(5)", CharType, {"length": 5}), + ("CHARACTER", CharType, {}), + ("CHARACTER(5)", CharType, {"length": 5}), + ("VARCHAR", VarCharType, {}), + ("VARCHAR(50)", VarCharType, {"length": 50}), + ("CHARACTER VARYING", VarCharType, {}), + ("CHARACTER VARYING(100)", VarCharType, {"length": 100}), + ("TEXT", TextType, {}), + ("BYTEA", PostgresByteaType, {}), + ("BLOB", PostgresByteaType, {}), + ("DATE", DateType, {}), + ("DATETIME", DateTimeType, {}), + ("TIMESTAMP", TimestampType, {}), + ("TIMESTAMP(3)", TimestampType, {"precision": 3}), + ("TIMESTAMPTZ", TimestampTzType, {}), + ("TIMESTAMP(3) WITH TIME ZONE", TimestampTzType, {"precision": 3}), + ("TIME", TimeType, {}), + ("TIME(6)", TimeType, {"precision": 6}), + ("TIMETZ", TimeTzType, {}), + ("TIME(6) WITH TIME ZONE", TimeTzType, {"precision": 6}), + ("INTERVAL", IntervalType, {}), + ("INTERVAL DAY TO SECOND", IntervalType, {"fields": "DAY TO SECOND"}), + ("JSON", JsonType, {}), + ("JSONB", JsonBType, {}), + ("JSONPATH", PostgresJsonPathType, {}), + ("UUID", PostgresUUIDType, {}), + ("BOOLEAN", BooleanType, {}), + ("BOOL", BooleanType, {}), + ("BIT", PostgresBitType, {}), + ("BIT(8)", PostgresBitType, {"n": 8}), + ("VARBIT", PostgresVarBitType, {}), + ("VARBIT(16)", PostgresVarBitType, {"n": 16}), + ("INET", PostgresInetType, {}), + ("CIDR", PostgresCidrType, {}), + ("MACADDR", PostgresMacAddrType, {}), + ("MACADDR8", PostgresMacAddr8Type, {}), + ("POINT", PostgresPointType, {}), + ("LINE", PostgresLineType, {}), + ("LSEG", PostgresLineSegmentType, {}), + ("BOX", PostgresBoxType, {}), + ("PATH", PostgresPathType, {}), + ("POLYGON", PostgresPolygonType, {}), + ("CIRCLE", PostgresCircleType, {}), + ("INT4RANGE", PostgresInt4RangeType, {}), + ("INT8RANGE", PostgresInt8RangeType, {}), + ("NUMRANGE", PostgresNumRangeType, {}), + ("TSRANGE", PostgresTsRangeType, {}), + ("TSTZRANGE", PostgresTsTzRangeType, {}), + ("DATERANGE", PostgresDateRangeType, {}), + ("INT4MULTIRANGE", PostgresInt4MultirangeType, {}), + ("INT8MULTIRANGE", PostgresInt8MultirangeType, {}), + ("NUMMULTIRANGE", PostgresNumMultirangeType, {}), + ("TSMULTIRANGE", PostgresTsMultirangeType, {}), + ("TSTZMULTIRANGE", PostgresTsTzMultirangeType, {}), + ("DATEMULTIRANGE", PostgresDateMultirangeType, {}), + ("OID", PostgresOIDType, {}), + ("REGCLASS", PostgresRegClassType, {}), + ("REGTYPE", PostgresRegTypeType, {}), + ("XID", PostgresXIDType, {}), + ("XID8", PostgresXID8Type, {}), + ("CID", PostgresCIDType, {}), + ("TID", PostgresTIDType, {}), + ("TSVECTOR", PostgresTSVectorType, {}), + ("TSQUERY", PostgresTSQueryType, {}), + ("MONEY", PostgresMoneyType, {}), + ("XML", PostgresXMLType, {}), + ("PG_LSN", PostgresPgLSNType, {}), + ("HSTORE", PostgresHstoreType, {}), + ("GEOGRAPHY", PostgresGeographyType, {}), + ("GEOMETRY", PostgresGeometryType, {}), + ("VECTOR", PostgresVectorType, {"dim": 0}), + ("VECTOR(768)", PostgresVectorType, {"dim": 768}), +] + + +@pytest.mark.parametrize("raw,expected_cls,expected_attrs", PARSE_CASES) +def test_parse_type(dialect, raw, expected_cls, expected_attrs): + result = dialect.parse_type(raw) + assert type(result) is expected_cls + for attr, value in expected_attrs.items(): + assert getattr(result, attr) == value + + +def test_parse_type_array_bracket_suffix(dialect): + result = dialect.parse_type("INTEGER[]") + assert type(result) is PostgresArrayType + assert type(result.element_type) is IntegerType + assert result.dimensions == 1 + + +def test_parse_type_array_multidimensional_brackets(dialect): + result = dialect.parse_type("INTEGER[][]") + assert type(result) is PostgresArrayType + assert type(result.element_type) is IntegerType + + +def test_parse_type_array_with_dimension_bracket(dialect): + result = dialect.parse_type("VARCHAR(10)[3]") + assert type(result) is PostgresArrayType + assert type(result.element_type) is VarCharType + assert result.element_type.length == 10 + + +def test_parse_type_malformed_bracket_suffix_falls_back(dialect): + result = dialect.parse_type("FOO[abc]") + assert type(result) is CustomType + + +def test_parse_type_array_keyword(dialect): + result = dialect.parse_type("INTEGER ARRAY") + assert type(result) is PostgresArrayType + assert type(result.element_type) is IntegerType + + +def test_parse_type_array_keyword_with_dimension(dialect): + result = dialect.parse_type("INTEGER ARRAY[3]") + assert type(result) is PostgresArrayType + assert type(result.element_type) is IntegerType + + +def test_parse_type_array_nested_element(dialect): + result = dialect.parse_type("UUID[]") + assert type(result) is PostgresArrayType + assert type(result.element_type) is PostgresUUIDType + + +def test_parse_type_unknown_falls_back_to_custom(dialect): + result = dialect.parse_type("FOOBARBAZ") + assert type(result) is CustomType + assert result.raw == "FOOBARBAZ" + + +def test_parse_type_case_insensitive(dialect): + result = dialect.parse_type(" varchar(25) ") + assert type(result) is VarCharType + assert result.length == 25 + + +# --------------------------------------------------------------------------- +# supports_data_types +# --------------------------------------------------------------------------- + + +def test_supports_data_types_registers_all_postgres_types(dialect): + supported = dialect.supports_data_types() + assert isinstance(supported, list) + assert len(supported) > 0 + classes = {cls for cls, _ in supported} + for expected in ( + PostgresSerialType, + PostgresByteaType, + PostgresUUIDType, + PostgresVectorType, + PostgresTsRangeType, + ArrayType, + JsonBType, + IntegerType, + ): + assert expected in classes diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_expression_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_expression_types.py new file mode 100644 index 00000000..e8a96295 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_expression_types.py @@ -0,0 +1,104 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_expression_types.py +"""Tests for PostgreSQL-specific DataType subclasses (pure, no DB). + +Covers the ``synonyms()`` registrations, equality/hash semantics and +array-type equivalence used by introspection and schema comparison. +""" + +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, + VarCharType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + PostgresBigSerialType, + PostgresBitType, + PostgresByteaType, + PostgresCharacterVaryingType, + PostgresSerialType, + PostgresSmallSerialType, + PostgresVarBitType, + PostgresVectorType, +) + + +class TestPostgresTypeSynonyms: + def test_character_varying_synonyms(self): + assert PostgresCharacterVaryingType.synonyms() == {"VarCharType"} + + def test_bytea_synonyms(self): + assert PostgresByteaType.synonyms() == {"BlobType"} + + def test_small_serial_synonyms(self): + assert PostgresSmallSerialType.synonyms() == {"SmallSerialType", "SmallIntType"} + + def test_serial_synonyms(self): + assert PostgresSerialType.synonyms() == {"SerialType", "IntegerType"} + + def test_big_serial_synonyms(self): + assert PostgresBigSerialType.synonyms() == {"BigSerialType", "BigIntType"} + + +class TestPostgresBitTypeEquality: + def test_equal(self): + assert PostgresBitType(8) == PostgresBitType(8) + + def test_not_equal_values(self): + assert PostgresBitType(8) != PostgresBitType(16) + + def test_not_equal_types(self): + assert PostgresBitType(8) != PostgresVarBitType(8) + assert PostgresBitType(8) != "not a type" + + def test_none_vs_value(self): + assert PostgresBitType(None) != PostgresBitType(8) + + def test_hash(self): + assert hash(PostgresBitType(8)) == hash((type(PostgresBitType(8)), 8)) + + +class TestPostgresVarBitTypeEquality: + def test_equal(self): + assert PostgresVarBitType(16) == PostgresVarBitType(16) + + def test_not_equal_values(self): + assert PostgresVarBitType(16) != PostgresVarBitType(32) + + def test_not_equal_types(self): + assert PostgresVarBitType(16) != PostgresBitType(16) + assert PostgresVarBitType(16) != object() + + def test_hash(self): + assert hash(PostgresVarBitType(16)) == hash((type(PostgresVarBitType(16)), 16)) + + +class TestPostgresVectorTypeEquality: + def test_equal(self): + assert PostgresVectorType(384) == PostgresVectorType(384) + + def test_not_equal_values(self): + assert PostgresVectorType(384) != PostgresVectorType(768) + + def test_not_equal_types(self): + assert PostgresVectorType(384) != PostgresBitType(8) + assert PostgresVectorType(384) != "vector" + + def test_hash(self): + assert hash(PostgresVectorType(384)) == hash((type(PostgresVectorType(384)), 384)) + + +class TestPostgresArrayType: + def test_is_equivalent_matching_element(self): + arr1 = PostgresArrayType(IntegerType(), dimensions=1) + arr2 = PostgresArrayType(IntegerType(), dimensions=3) + assert arr1.is_equivalent(arr2) + + def test_is_equivalent_non_array(self): + arr = PostgresArrayType(IntegerType(), dimensions=1) + assert arr.is_equivalent(IntegerType()) is False + assert arr.is_equivalent(None) is False + + def test_is_equivalent_different_element(self): + arr1 = PostgresArrayType(IntegerType(), dimensions=1) + arr2 = PostgresArrayType(VarCharType(), dimensions=1) + assert arr1.is_equivalent(arr2) is False diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_bulk_operations.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/bulk_crud/test_bulk_operations.py similarity index 57% rename from tests/rhosocial/activerecord_postgres_test/feature/basic/test_bulk_operations.py rename to tests/rhosocial/activerecord_postgres_test/feature/basic/bulk_crud/test_bulk_operations.py index e0936eda..8e76ce45 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_bulk_operations.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/bulk_crud/test_bulk_operations.py @@ -8,4 +8,6 @@ async_bulk_user_class, # noqa: F401 ) -from rhosocial.activerecord.testsuite.feature.basic.test_bulk_operations import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.bulk_crud.test_bulk_operations import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.bulk_crud.test_bulk_operations_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/bulk_crud/test_bulk_operations_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/bulk_crud/test_bulk_operations_async.py new file mode 100644 index 00000000..2a67aa45 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/bulk_crud/test_bulk_operations_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/bulk_crud/test_bulk_operations_async.py +"""Bridge file for async feature.basic.bulk_crud.test_bulk_operations_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.bulk_crud.test_bulk_operations_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/conftest.py index 92417f78..1f6cb9a6 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/conftest.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/conftest.py @@ -3,7 +3,28 @@ Pytest configuration for basic feature tests. This file imports fixtures from the corresponding testsuite, making them -available to the tests in this directory. +available to the tests in this directory. It overrides ``type_adapter_fixtures`` +so the scenario name is passed to the provider. Without this, the sync provider +always creates the ``type_adapter_tests`` table on the first registered scenario, +which collides with the async provider when both scenarios run in parallel. """ -from rhosocial.activerecord.testsuite.feature.basic.conftest import * # noqa: F403 +import pytest + +from rhosocial.activerecord.testsuite.feature.basic.conftest import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.basic.conftest import ( # noqa: F401 + SCENARIO_PARAMS_SYNC, + PROVIDER_KEY_SYNC, +) +from rhosocial.activerecord.testsuite.core.registry import get_provider_registry + + +@pytest.fixture(scope="function", params=SCENARIO_PARAMS_SYNC) +def type_adapter_fixtures(request): + scenario = request.param + provider_registry = get_provider_registry() + provider_class = provider_registry.get_provider(PROVIDER_KEY_SYNC) + provider = provider_class() + model = provider.setup_type_adapter_model_and_schema(scenario) + yield model + provider.cleanup_after_test(scenario) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_context.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_context.py index 8f7e8cf3..b5ecbf1d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_context.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_context.py @@ -12,3 +12,5 @@ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.basic.connection.test_active_record_context import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.connection.test_active_record_context_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_context_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_context_async.py new file mode 100644 index 00000000..8d360f89 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_context_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_context_async.py +"""Bridge file for async feature.basic.connection.test_active_record_context_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.connection.test_active_record_context_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_crud.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_crud.py index eb4e162b..c9488510 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_crud.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_crud.py @@ -12,3 +12,5 @@ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.basic.connection.test_active_record_crud import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.connection.test_active_record_crud_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_crud_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_crud_async.py new file mode 100644 index 00000000..a826a9e8 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_crud_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_crud_async.py +"""Bridge file for async feature.basic.connection.test_active_record_crud_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.connection.test_active_record_crud_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_composite_pk_crud.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_composite_pk_crud.py new file mode 100644 index 00000000..1f34b0b8 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_composite_pk_crud.py @@ -0,0 +1,6 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_composite_pk_crud.py +""" +Bridge file for composite PK CRUD tests from the testsuite. +""" +from rhosocial.activerecord.testsuite.feature.basic.crud.test_composite_pk_crud import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.crud.test_composite_pk_crud_async import * # noqa: F403 \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_composite_pk_crud_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_composite_pk_crud_async.py new file mode 100644 index 00000000..5bf67d8f --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_composite_pk_crud_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_composite_pk_crud_async.py +"""Bridge file for async feature.basic.crud.test_composite_pk_crud_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.crud.test_composite_pk_crud_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_crud.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_crud.py similarity index 87% rename from tests/rhosocial/activerecord_postgres_test/feature/basic/test_crud.py rename to tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_crud.py index c434da79..ffc5439b 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_crud.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_crud.py @@ -27,4 +27,6 @@ ) # Import shared tests from testsuite package -from rhosocial.activerecord.testsuite.feature.basic.test_crud import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.crud.test_crud import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.crud.test_crud_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_crud_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_crud_async.py new file mode 100644 index 00000000..b8ffe646 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_crud_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_crud_async.py +"""Bridge file for async feature.basic.crud.test_crud_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.crud.test_crud_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_basic.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_basic.py new file mode 100644 index 00000000..cb781f9d --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_basic.py @@ -0,0 +1,5 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_basic.py +""" +Bridge file for derived field tests from the testsuite. +""" +from rhosocial.activerecord.testsuite.feature.basic.fields.test_derived_field import * # noqa: F403 \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_basic_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_basic_async.py new file mode 100644 index 00000000..e6cf4fff --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_basic_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_basic_async.py +"""Bridge file for async derived field basic tests from the testsuite.""" +from rhosocial.activerecord.testsuite.feature.basic.fields.test_derived_field_async import * # noqa: F401, F403 \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_example_basic_fixtures.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_example_basic_fixtures.py new file mode 100644 index 00000000..a35d9746 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_example_basic_fixtures.py @@ -0,0 +1,6 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_example_basic_fixtures.py +""" +Bridge file for example basic fixtures tests from the testsuite. +""" +from rhosocial.activerecord.testsuite.feature.basic.fields.test_example_basic_fixtures import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.fields.test_example_basic_fixtures_async import * # noqa: F403 \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_example_basic_fixtures_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_example_basic_fixtures_async.py new file mode 100644 index 00000000..cef55fc3 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_example_basic_fixtures_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_example_basic_fixtures_async.py +"""Bridge file for async feature.basic.fields.test_example_basic_fixtures_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.fields.test_example_basic_fixtures_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_field_column_mapping.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_field_column_mapping.py similarity index 81% rename from tests/rhosocial/activerecord_postgres_test/feature/basic/test_field_column_mapping.py rename to tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_field_column_mapping.py index e5503f88..87b920c3 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_field_column_mapping.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_field_column_mapping.py @@ -20,4 +20,6 @@ ) # The test classes contain the actual test cases. -from rhosocial.activerecord.testsuite.feature.basic.test_field_column_mapping import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.fields.test_field_column_mapping import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.fields.test_field_column_mapping_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_field_column_mapping_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_field_column_mapping_async.py new file mode 100644 index 00000000..d46911b2 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_field_column_mapping_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_field_column_mapping_async.py +"""Bridge file for async feature.basic.fields.test_field_column_mapping_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.fields.test_field_column_mapping_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_fields.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_fields.py similarity index 84% rename from tests/rhosocial/activerecord_postgres_test/feature/basic/test_fields.py rename to tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_fields.py index ed3bdd00..13b13fb8 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_fields.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_fields.py @@ -20,4 +20,6 @@ ) # Import shared tests from testsuite package -from rhosocial.activerecord.testsuite.feature.basic.test_fields import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.fields.test_fields import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.fields.test_fields_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_fields_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_fields_async.py new file mode 100644 index 00000000..2dace749 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_fields_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_fields_async.py +"""Bridge file for async feature.basic.fields.test_fields_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.fields.test_fields_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_basic_mapped_models.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_basic_mapped_models.py index 9c3e81ba..6601e6ad 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_basic_mapped_models.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_basic_mapped_models.py @@ -15,4 +15,6 @@ from rhosocial.activerecord.testsuite.feature.basic.conftest import mapped_models_fixtures # noqa: F401 # The `TestBasicMappedModels` class contains the actual test cases. -from rhosocial.activerecord.testsuite.feature.basic.test_example_basic_fixtures import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.fields.test_example_basic_fixtures import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.fields.test_example_basic_fixtures_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_type_adapter.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_type_adapter.py deleted file mode 100644 index f9841654..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_type_adapter.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -This is a "bridge" file for the basic features test group, specifically for -type adapter tests. - -Its purpose is to import the generic tests from the `rhosocial-activerecord-testsuite` -package and make them discoverable by `pytest` within this project's test run. -""" - -# Import the fixture that provides the configured model and backend for type adapter tests. -from rhosocial.activerecord.testsuite.feature.basic.conftest import ( - type_adapter_fixtures, # noqa: F401 - async_type_adapter_fixtures, # noqa: F401 -) - -# Import all tests from the generic testsuite file. -from rhosocial.activerecord.testsuite.feature.basic.test_type_adapter import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/type_adapter/test_type_adapter.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/type_adapter/test_type_adapter.py new file mode 100644 index 00000000..d6108e4f --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/type_adapter/test_type_adapter.py @@ -0,0 +1,18 @@ +""" +This is a "bridge" file for the basic features test group, specifically for +type adapter tests. + +Its purpose is to import the generic tests from the `rhosocial-activerecord-testsuite` +package and make them discoverable by `pytest` within this project's test run. +""" + +# Note: fixtures are provided by the conftest override at +# feature/basic/conftest.py (which passes the scenario name to the provider). +# Do NOT re-import type_adapter_fixtures here -- pytest would register the +# testsuite's original (scenario-ignoring) fixture with this module as the +# most specific baseid, shadowing the conftest override. + +# Import all tests from the generic testsuite file. +from rhosocial.activerecord.testsuite.feature.basic.type_adapter.test_type_adapter import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.type_adapter.test_type_adapter_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/type_adapter/test_type_adapter_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/type_adapter/test_type_adapter_async.py new file mode 100644 index 00000000..533df4fa --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/type_adapter/test_type_adapter_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/type_adapter/test_type_adapter_async.py +"""Bridge file for async feature.basic.type_adapter.test_type_adapter_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.type_adapter.test_type_adapter_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_pydantic_native_validation.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_pydantic_native_validation.py similarity index 71% rename from tests/rhosocial/activerecord_postgres_test/feature/basic/test_pydantic_native_validation.py rename to tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_pydantic_native_validation.py index 752b0a1b..03fc5eeb 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_pydantic_native_validation.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_pydantic_native_validation.py @@ -13,4 +13,6 @@ pydantic_validated_model, # noqa: F401 ) -from rhosocial.activerecord.testsuite.feature.basic.test_pydantic_native_validation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.validation.test_pydantic_native_validation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.validation.test_pydantic_native_validation_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_pydantic_native_validation_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_pydantic_native_validation_async.py new file mode 100644 index 00000000..1b5019de --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_pydantic_native_validation_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_pydantic_native_validation_async.py +"""Bridge file for async feature.basic.validation.test_pydantic_native_validation_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.validation.test_pydantic_native_validation_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_validation.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_validation.py similarity index 83% rename from tests/rhosocial/activerecord_postgres_test/feature/basic/test_validation.py rename to tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_validation.py index b522ba20..2c2cd094 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_validation.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_validation.py @@ -20,4 +20,6 @@ ) # Import shared tests from testsuite package -from rhosocial.activerecord.testsuite.feature.basic.test_validation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.validation.test_validation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.validation.test_validation_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_validation_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_validation_async.py new file mode 100644 index 00000000..df784d78 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_validation_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_validation_async.py +"""Bridge file for async feature.basic.validation.test_validation_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.validation.test_validation_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/conftest.py new file mode 100644 index 00000000..35e82e19 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/conftest.py @@ -0,0 +1,8 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/worker/conftest.py +""" +Pytest configuration for basic worker tests. + +This file imports fixtures from the corresponding testsuite, making them +available to the tests in this directory. +""" +from rhosocial.activerecord.testsuite.feature.basic.worker.conftest import * # noqa: F403 \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_connection_management.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_connection_management.py index 4486e093..a5150e06 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_connection_management.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_connection_management.py @@ -8,3 +8,5 @@ user_class_for_worker, # noqa: F401 ) from rhosocial.activerecord.testsuite.feature.basic.worker.test_connection_management import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.worker.test_connection_management_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_connection_management_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_connection_management_async.py new file mode 100644 index 00000000..3340c392 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_connection_management_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_connection_management_async.py +"""Bridge file for async feature.basic.worker.test_connection_management_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.worker.test_connection_management_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_parallel_crud.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_parallel_crud.py index c75a4e8c..287d0828 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_parallel_crud.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_parallel_crud.py @@ -9,3 +9,5 @@ async_user_class_for_worker, # noqa: F401 ) from rhosocial.activerecord.testsuite.feature.basic.worker.test_parallel_crud import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.worker.test_parallel_crud_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_parallel_crud_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_parallel_crud_async.py new file mode 100644 index 00000000..62eedf04 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_parallel_crud_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_parallel_crud_async.py +"""Bridge file for async feature.basic.worker.test_parallel_crud_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.worker.test_parallel_crud_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_worker_pool_lifecycle.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_worker_pool_lifecycle.py index 53d0a0d0..7df4d9df 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_worker_pool_lifecycle.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_worker_pool_lifecycle.py @@ -8,3 +8,5 @@ user_class_for_worker, # noqa: F401 ) from rhosocial.activerecord.testsuite.feature.basic.worker.test_worker_pool_lifecycle import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.worker.test_worker_pool_lifecycle_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_worker_pool_lifecycle_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_worker_pool_lifecycle_async.py new file mode 100644 index 00000000..15ccdd00 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_worker_pool_lifecycle_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_worker_pool_lifecycle_async.py +"""Bridge file for async feature.basic.worker.test_worker_pool_lifecycle_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.worker.test_worker_pool_lifecycle_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/conftest.py deleted file mode 100644 index 4d6a5f53..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/conftest.py +++ /dev/null @@ -1,2 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/composite_pk/conftest.py -from rhosocial.activerecord.testsuite.feature.composite_pk.conftest import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/order_items.sql b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/order_items.sql deleted file mode 100644 index 9ea4998a..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/order_items.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE TABLE "order_items" ( - "order_id" INTEGER NOT NULL, - "product_id" INTEGER NOT NULL, - "quantity" INTEGER NOT NULL DEFAULT 1, - "unit_price" DECIMAL(10,2) NOT NULL, - PRIMARY KEY ("order_id", "product_id") -); diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/orders.sql b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/orders.sql deleted file mode 100644 index b96c78fa..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/orders.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE "orders" ( - "id" SERIAL PRIMARY KEY, - "total" DECIMAL(10,2) NOT NULL, - "created_at" TIMESTAMP WITH TIME ZONE, - "updated_at" TIMESTAMP WITH TIME ZONE -); diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/store_inventory.sql b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/store_inventory.sql deleted file mode 100644 index e4bfbab9..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/store_inventory.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE TABLE "store_inventory" ( - "store_id" INTEGER NOT NULL, - "product_id" INTEGER NOT NULL, - "batch_id" VARCHAR(64) NOT NULL, - "stock" INTEGER NOT NULL DEFAULT 0, - PRIMARY KEY ("store_id", "product_id", "batch_id") -); diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_active_query.py b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_active_query.py deleted file mode 100644 index 66e9faae..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_active_query.py +++ /dev/null @@ -1,2 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_active_query.py -from rhosocial.activerecord.testsuite.feature.composite_pk.test_active_query import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_activerecord.py b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_activerecord.py deleted file mode 100644 index a4b222c9..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_activerecord.py +++ /dev/null @@ -1,2 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_activerecord.py -from rhosocial.activerecord.testsuite.feature.composite_pk.test_activerecord import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_cte_query.py b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_cte_query.py deleted file mode 100644 index 6790f16b..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_cte_query.py +++ /dev/null @@ -1,2 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_cte_query.py -from rhosocial.activerecord.testsuite.feature.composite_pk.test_cte_query import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_set_operation.py b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_set_operation.py deleted file mode 100644 index 148c1b97..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_set_operation.py +++ /dev/null @@ -1,2 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_set_operation.py -from rhosocial.activerecord.testsuite.feature.composite_pk.test_set_operation import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/events/test_handlers.py b/tests/rhosocial/activerecord_postgres_test/feature/events/test_handlers.py index 02d222d2..5964c8ba 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/events/test_handlers.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/events/test_handlers.py @@ -21,3 +21,5 @@ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.events.test_handlers import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.events.test_handlers_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/events/test_handlers_async.py b/tests/rhosocial/activerecord_postgres_test/feature/events/test_handlers_async.py new file mode 100644 index 00000000..6b946f79 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/events/test_handlers_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/events/test_handlers_async.py +"""Bridge file for async feature.events.test_handlers_async tests.""" +from rhosocial.activerecord.testsuite.feature.events.test_handlers_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/events/test_lifecycle.py b/tests/rhosocial/activerecord_postgres_test/feature/events/test_lifecycle.py index 00b29520..43bb6d83 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/events/test_lifecycle.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/events/test_lifecycle.py @@ -21,3 +21,5 @@ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.events.test_lifecycle import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.events.test_lifecycle_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/events/test_lifecycle_async.py b/tests/rhosocial/activerecord_postgres_test/feature/events/test_lifecycle_async.py new file mode 100644 index 00000000..94312c2a --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/events/test_lifecycle_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/events/test_lifecycle_async.py +"""Bridge file for async feature.events.test_lifecycle_async tests.""" +from rhosocial.activerecord.testsuite.feature.events.test_lifecycle_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/interface/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/interface/conftest.py new file mode 100644 index 00000000..52d35dc0 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/interface/conftest.py @@ -0,0 +1,8 @@ +# tests/rhosocial/activerecord_postgres_test/feature/interface/conftest.py +""" +Pytest configuration for interface tests. + +This file imports fixtures from the corresponding testsuite, making them +available to the tests in this directory. +""" +from rhosocial.activerecord.testsuite.feature.interface.conftest import * # noqa: F403 \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/interface/test_threadsafe_dict.py b/tests/rhosocial/activerecord_postgres_test/feature/interface/test_threadsafe_dict.py new file mode 100644 index 00000000..48cf1235 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/interface/test_threadsafe_dict.py @@ -0,0 +1,7 @@ +# tests/rhosocial/activerecord_postgres_test/feature/interface/test_threadsafe_dict.py +""" +Bridge file for ThreadSafeDict interface tests from the testsuite. +""" + +from rhosocial.activerecord.testsuite.feature.interface.test_threadsafe_dict import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.interface.test_threadsafe_dict_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/interface/test_threadsafe_dict_async.py b/tests/rhosocial/activerecord_postgres_test/feature/interface/test_threadsafe_dict_async.py new file mode 100644 index 00000000..b5e43962 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/interface/test_threadsafe_dict_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/interface/test_threadsafe_dict_async.py +"""Bridge file for async feature.interface.test_threadsafe_dict_async tests.""" +from rhosocial.activerecord.testsuite.feature.interface.test_threadsafe_dict_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_combined_articles.py b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_combined_articles.py index a63c2d24..c0c81379 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_combined_articles.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_combined_articles.py @@ -7,3 +7,5 @@ """ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.mixins.test_combined_articles import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.mixins.test_combined_articles_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_combined_articles_async.py b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_combined_articles_async.py new file mode 100644 index 00000000..1dc7d5c5 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_combined_articles_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/mixins/test_combined_articles_async.py +"""Bridge file for async feature.mixins.test_combined_articles_async tests.""" +from rhosocial.activerecord.testsuite.feature.mixins.test_combined_articles_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_optimistic_lock.py b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_optimistic_lock.py index 9fd9601a..be5611e5 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_optimistic_lock.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_optimistic_lock.py @@ -7,3 +7,5 @@ """ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.mixins.test_optimistic_lock import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.mixins.test_optimistic_lock_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_optimistic_lock_async.py b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_optimistic_lock_async.py new file mode 100644 index 00000000..4c233401 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_optimistic_lock_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/mixins/test_optimistic_lock_async.py +"""Bridge file for async feature.mixins.test_optimistic_lock_async tests.""" +from rhosocial.activerecord.testsuite.feature.mixins.test_optimistic_lock_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_soft_delete.py b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_soft_delete.py index e440e286..40a34336 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_soft_delete.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_soft_delete.py @@ -7,3 +7,5 @@ """ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.mixins.test_soft_delete import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.mixins.test_soft_delete_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_soft_delete_async.py b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_soft_delete_async.py new file mode 100644 index 00000000..14371fd5 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_soft_delete_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/mixins/test_soft_delete_async.py +"""Bridge file for async feature.mixins.test_soft_delete_async tests.""" +from rhosocial.activerecord.testsuite.feature.mixins.test_soft_delete_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_timestamps.py b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_timestamps.py index d776bdbf..64344372 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_timestamps.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_timestamps.py @@ -7,3 +7,5 @@ """ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.mixins.test_timestamps import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.mixins.test_timestamps_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_timestamps_async.py b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_timestamps_async.py new file mode 100644 index 00000000..0260c373 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_timestamps_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/mixins/test_timestamps_async.py +"""Bridge file for async feature.mixins.test_timestamps_async tests.""" +from rhosocial.activerecord.testsuite.feature.mixins.test_timestamps_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_aggregate.py b/tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_active_query_aggregate.py similarity index 55% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_aggregate.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_active_query_aggregate.py index 75f400d0..deae3685 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_aggregate.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_active_query_aggregate.py @@ -5,4 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_active_query_aggregate import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.aggregate.test_active_query_aggregate import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.aggregate.test_active_query_aggregate_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_active_query_aggregate_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_active_query_aggregate_async.py new file mode 100644 index 00000000..c3568677 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_active_query_aggregate_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_active_query_aggregate_async.py +"""Bridge file for async feature.query.aggregate.test_active_query_aggregate_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.aggregate.test_active_query_aggregate_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_aggregate_queries.py b/tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_aggregate_queries.py similarity index 55% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_aggregate_queries.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_aggregate_queries.py index 12c99783..525ba49a 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_aggregate_queries.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_aggregate_queries.py @@ -5,4 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_aggregate_queries import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.aggregate.test_aggregate_queries import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.aggregate.test_aggregate_queries_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_aggregate_queries_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_aggregate_queries_async.py new file mode 100644 index 00000000..df180f44 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_aggregate_queries_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_aggregate_queries_async.py +"""Bridge file for async feature.query.aggregate.test_aggregate_queries_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.aggregate.test_aggregate_queries_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_basic.py b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_active_query_basic.py similarity index 56% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_basic.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_active_query_basic.py index c8e67b7d..9baba06b 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_basic.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_active_query_basic.py @@ -5,4 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_active_query_basic import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.basic.test_active_query_basic import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.basic.test_active_query_basic_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_active_query_basic_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_active_query_basic_async.py new file mode 100644 index 00000000..4bc013b4 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_active_query_basic_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_active_query_basic_async.py +"""Bridge file for async feature.query.basic.test_active_query_basic_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.basic.test_active_query_basic_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_basic.py b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_basic.py similarity index 57% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_basic.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_basic.py index f56b9765..e2f69cef 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_basic.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_basic.py @@ -5,4 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_basic import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.basic.test_basic import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.basic.test_basic_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_basic_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_basic_async.py new file mode 100644 index 00000000..55d0570f --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_basic_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_basic_async.py +"""Bridge file for async feature.query.basic.test_basic_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.basic.test_basic_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_composite_pk_query.py b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_composite_pk_query.py new file mode 100644 index 00000000..20bd64fb --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_composite_pk_query.py @@ -0,0 +1,6 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_composite_pk_query.py +""" +Bridge file for composite PK query tests from the testsuite. +""" +from rhosocial.activerecord.testsuite.feature.query.basic.test_composite_pk_query import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.basic.test_composite_pk_query_async import * # noqa: F403 \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_composite_pk_query_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_composite_pk_query_async.py new file mode 100644 index 00000000..a4d65b87 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_composite_pk_query_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_composite_pk_query_async.py +"""Bridge file for async feature.query.basic.test_composite_pk_query_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.basic.test_composite_pk_query_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_sync_queries.py b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_queries.py similarity index 70% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_sync_queries.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_queries.py index 75205a5d..1fda7a9b 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_sync_queries.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_queries.py @@ -1,8 +1,8 @@ -# tests/rhosocial/activerecord_postgres_test/feature/query/test_sync_queries.py +# tests/rhosocial/activerecord_postgres_test/feature/query/test_queries.py """ Bridge file for synchronous query tests from the testsuite. This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_sync_queries import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.basic.test_queries import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_async_queries.py b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_queries_async.py similarity index 68% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_async_queries.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_queries_async.py index ef4556d1..8e53f6fe 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_async_queries.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_queries_async.py @@ -1,8 +1,9 @@ -# tests/rhosocial/activerecord_postgres_test/feature/query/test_async_queries.py +# tests/rhosocial/activerecord_postgres_test/feature/query/test_queries_async.py """ Bridge file for async queries tests from the testsuite. This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_async_queries import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.basic.test_queries_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_collation_expression.py b/tests/rhosocial/activerecord_postgres_test/feature/query/collation/test_collation_expression.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_collation_expression.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/collation/test_collation_expression.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_active_query_context.py b/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_active_query_context.py index 403c1a9e..9fb3d2f7 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_active_query_context.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_active_query_context.py @@ -12,3 +12,5 @@ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.query.connection.test_active_query_context import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.connection.test_active_query_context_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_active_query_context_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_active_query_context_async.py new file mode 100644 index 00000000..cd8c61c6 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_active_query_context_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_active_query_context_async.py +"""Bridge file for async feature.query.connection.test_active_query_context_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.connection.test_active_query_context_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_cte_query_context.py b/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_cte_query_context.py index 1483e319..0b1e6153 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_cte_query_context.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_cte_query_context.py @@ -12,3 +12,5 @@ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.query.connection.test_cte_query_context import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.connection.test_cte_query_context_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_cte_query_context_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_cte_query_context_async.py new file mode 100644 index 00000000..e8b21313 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_cte_query_context_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_cte_query_context_async.py +"""Bridge file for async feature.query.connection.test_cte_query_context_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.connection.test_cte_query_context_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_set_operation_context.py b/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_set_operation_context.py index f6acecbf..ef0d192a 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_set_operation_context.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_set_operation_context.py @@ -12,3 +12,5 @@ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.query.connection.test_set_operation_context import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.connection.test_set_operation_context_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_set_operation_context_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_set_operation_context_async.py new file mode 100644 index 00000000..9bc0e34e --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_set_operation_context_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_set_operation_context_async.py +"""Bridge file for async feature.query.connection.test_set_operation_context_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.connection.test_set_operation_context_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_cross_database_compatibility.py b/tests/rhosocial/activerecord_postgres_test/feature/query/cross_database/test_cross_database_compatibility.py similarity index 54% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_cross_database_compatibility.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/cross_database/test_cross_database_compatibility.py index ce5c074a..b234b681 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_cross_database_compatibility.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/cross_database/test_cross_database_compatibility.py @@ -5,4 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_cross_database_compatibility import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.cross_database.test_cross_database_compatibility import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.cross_database.test_cross_database_compatibility_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/cross_database/test_cross_database_compatibility_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/cross_database/test_cross_database_compatibility_async.py new file mode 100644 index 00000000..1c8d2108 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/cross_database/test_cross_database_compatibility_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/cross_database/test_cross_database_compatibility_async.py +"""Bridge file for async feature.query.cross_database.test_cross_database_compatibility_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.cross_database.test_cross_database_compatibility_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_composite_pk_cte.py b/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_composite_pk_cte.py new file mode 100644 index 00000000..986a6c4a --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_composite_pk_cte.py @@ -0,0 +1,6 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_composite_pk_cte.py +""" +Bridge file for composite PK CTE tests from the testsuite. +""" +from rhosocial.activerecord.testsuite.feature.query.cte.test_composite_pk_cte import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.cte.test_composite_pk_cte_async import * # noqa: F403 \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_composite_pk_cte_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_composite_pk_cte_async.py new file mode 100644 index 00000000..46972049 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_composite_pk_cte_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_composite_pk_cte_async.py +"""Bridge file for async feature.query.cte.test_composite_pk_cte_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.cte.test_composite_pk_cte_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_active_query.py b/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_active_query.py similarity index 57% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_active_query.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_active_query.py index 1ce77f68..6298688f 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_active_query.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_active_query.py @@ -5,4 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_cte_query_active_query import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.cte.test_cte_query_active_query import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.cte.test_cte_query_active_query_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_active_query_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_active_query_async.py new file mode 100644 index 00000000..3429c45a --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_active_query_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_active_query_async.py +"""Bridge file for async feature.query.cte.test_cte_query_active_query_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.cte.test_cte_query_active_query_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_set_operation.py b/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_set_operation.py similarity index 56% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_set_operation.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_set_operation.py index 2610511f..686d8fc6 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_set_operation.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_set_operation.py @@ -5,4 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_cte_query_set_operation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.cte.test_cte_query_set_operation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.cte.test_cte_query_set_operation_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_set_operation_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_set_operation_async.py new file mode 100644 index 00000000..4bf171be --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_set_operation_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_set_operation_async.py +"""Bridge file for async feature.query.cte.test_cte_query_set_operation_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.cte.test_cte_query_set_operation_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_caching.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_caching.py new file mode 100644 index 00000000..a7c1e676 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_caching.py @@ -0,0 +1,7 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_caching.py +""" +Bridge file for eager loading caching tests. +""" +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_caching import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_caching_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_caching_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_caching_async.py new file mode 100644 index 00000000..f946bcec --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_caching_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_caching_async.py +"""Bridge file for async feature.query.eager_loading.test_eager_loading_caching_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_caching_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_nested.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_nested.py new file mode 100644 index 00000000..09efb41a --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_nested.py @@ -0,0 +1,7 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_nested.py +""" +Bridge file for eager loading nested tests. +""" +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_nested import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_nested_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_nested_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_nested_async.py new file mode 100644 index 00000000..b7921aa6 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_nested_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_nested_async.py +"""Bridge file for async feature.query.eager_loading.test_eager_loading_nested_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_nested_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_scenarios.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_scenarios.py new file mode 100644 index 00000000..7b5ca3cd --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_scenarios.py @@ -0,0 +1,2 @@ +"""Bridge file for sync eager loading scenario tests.""" +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_scenarios import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_scenarios_async.py similarity index 54% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios_async.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_scenarios_async.py index 37b45c60..64cae900 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios_async.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_scenarios_async.py @@ -2,4 +2,4 @@ """ Bridge file for async eager loading scenario tests. """ -from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_scenarios_async import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_scenarios_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_all.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_all.py new file mode 100644 index 00000000..7571dafa --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_all.py @@ -0,0 +1,7 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_all.py +""" +Bridge file for eager loading with_all tests. +""" +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_with_all import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_with_all_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_all_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_all_async.py new file mode 100644 index 00000000..197ae5d6 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_all_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_all_async.py +"""Bridge file for async feature.query.eager_loading.test_eager_loading_with_all_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_with_all_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_modifier.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_modifier.py new file mode 100644 index 00000000..c55cb6f8 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_modifier.py @@ -0,0 +1,7 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_modifier.py +""" +Bridge file for eager loading with_modifier tests. +""" +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_with_modifier import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_with_modifier_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_modifier_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_modifier_async.py new file mode 100644 index 00000000..c558e117 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_modifier_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_modifier_async.py +"""Bridge file for async feature.query.eager_loading.test_eager_loading_with_modifier_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_with_modifier_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_one.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_one.py new file mode 100644 index 00000000..8dc3de43 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_one.py @@ -0,0 +1,7 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_one.py +""" +Bridge file for eager loading with_one tests. +""" +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_with_one import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_with_one_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_one_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_one_async.py new file mode 100644 index 00000000..1ad43fb7 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_one_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_one_async.py +"""Bridge file for async feature.query.eager_loading.test_eager_loading_with_one_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_with_one_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_error_handling.py b/tests/rhosocial/activerecord_postgres_test/feature/query/error_handling/test_error_handling.py similarity index 54% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_error_handling.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/error_handling/test_error_handling.py index d30ddc60..88f48f2c 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_error_handling.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/error_handling/test_error_handling.py @@ -5,4 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_error_handling import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.error_handling.test_error_handling import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.error_handling.test_error_handling_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/error_handling/test_error_handling_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/error_handling/test_error_handling_async.py new file mode 100644 index 00000000..ef91b73c --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/error_handling/test_error_handling_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/error_handling/test_error_handling_async.py +"""Bridge file for async feature.query.error_handling.test_error_handling_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.error_handling.test_error_handling_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_join.py b/tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_active_query_join.py similarity index 56% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_join.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_active_query_join.py index 4f750e5b..90eb3a08 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_join.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_active_query_join.py @@ -5,4 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_active_query_join import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.joins.test_active_query_join import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.joins.test_active_query_join_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_active_query_join_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_active_query_join_async.py new file mode 100644 index 00000000..63a0b532 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_active_query_join_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_active_query_join_async.py +"""Bridge file for async feature.query.joins.test_active_query_join_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.joins.test_active_query_join_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_joins.py b/tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_joins.py similarity index 57% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_joins.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_joins.py index 56a0a911..a775027d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_joins.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_joins.py @@ -5,4 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_joins import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.joins.test_joins import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.joins.test_joins_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_joins_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_joins_async.py new file mode 100644 index 00000000..f5710f45 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_joins_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_joins_async.py +"""Bridge file for async feature.query.joins.test_joins_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.joins.test_joins_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_logging_data_summarization.py b/tests/rhosocial/activerecord_postgres_test/feature/query/logging/test_logging_data_summarization.py similarity index 55% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_logging_data_summarization.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/logging/test_logging_data_summarization.py index fd09e85d..5087c2ed 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_logging_data_summarization.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/logging/test_logging_data_summarization.py @@ -5,4 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_logging_data_summarization import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.logging.test_logging_data_summarization import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.logging.test_logging_data_summarization_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/logging/test_logging_data_summarization_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/logging/test_logging_data_summarization_async.py new file mode 100644 index 00000000..1f9e0aa4 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/logging/test_logging_data_summarization_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/logging/test_logging_data_summarization_async.py +"""Bridge file for async feature.query.logging.test_logging_data_summarization_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.logging.test_logging_data_summarization_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_query_optimization.py b/tests/rhosocial/activerecord_postgres_test/feature/query/optimization/test_query_optimization.py similarity index 55% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_query_optimization.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/optimization/test_query_optimization.py index ad5b3b08..115d70f0 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_query_optimization.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/optimization/test_query_optimization.py @@ -5,4 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_query_optimization import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.optimization.test_query_optimization import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.optimization.test_query_optimization_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/optimization/test_query_optimization_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/optimization/test_query_optimization_async.py new file mode 100644 index 00000000..9688b94b --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/optimization/test_query_optimization_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/optimization/test_query_optimization_async.py +"""Bridge file for async feature.query.optimization.test_query_optimization_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.optimization.test_query_optimization_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_range.py b/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_active_query_range.py similarity index 54% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_range.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_active_query_range.py index af663f25..8498bb08 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_range.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_active_query_range.py @@ -5,4 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_active_query_range import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.range_queries.test_active_query_range import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.range_queries.test_active_query_range_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_active_query_range_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_active_query_range_async.py new file mode 100644 index 00000000..667a3718 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_active_query_range_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_active_query_range_async.py +"""Bridge file for async feature.query.range_queries.test_active_query_range_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.range_queries.test_active_query_range_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_range_queries.py b/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_queries.py similarity index 55% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_range_queries.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_queries.py index f6027323..fb8bc85e 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_range_queries.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_queries.py @@ -5,4 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_range_queries import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.range_queries.test_range_queries import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.range_queries.test_range_queries_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_queries_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_queries_async.py new file mode 100644 index 00000000..c6ef096e --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_queries_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_queries_async.py +"""Bridge file for async feature.query.range_queries.test_range_queries_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.range_queries.test_range_queries_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_range_query_detailed.py b/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_query_detailed.py similarity index 54% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_range_query_detailed.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_query_detailed.py index 62bd6a32..ef05c44b 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_range_query_detailed.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_query_detailed.py @@ -5,4 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_range_query_detailed import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.range_queries.test_range_query_detailed import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.range_queries.test_range_query_detailed_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_query_detailed_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_query_detailed_async.py new file mode 100644 index 00000000..df8937fe --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_query_detailed_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_query_detailed_async.py +"""Bridge file for async feature.query.range_queries.test_range_query_detailed_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.range_queries.test_range_query_detailed_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relational_validation.py b/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relational_validation.py similarity index 55% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_relational_validation.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relational_validation.py index def8f01d..abd6a3f4 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relational_validation.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relational_validation.py @@ -5,4 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_relational_validation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.relations.test_relational_validation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.relations.test_relational_validation_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relational_validation_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relational_validation_async.py new file mode 100644 index 00000000..9276ed3a --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relational_validation_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relational_validation_async.py +"""Bridge file for async feature.query.relations.test_relational_validation_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.relations.test_relational_validation_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_basic.py b/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_basic.py similarity index 56% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_basic.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_basic.py index fbf463d4..7a2a089e 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_basic.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_basic.py @@ -5,4 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_relations_basic import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.relations.test_relations_basic import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.relations.test_relations_basic_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_basic_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_basic_async.py new file mode 100644 index 00000000..6ff408c0 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_basic_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_basic_async.py +"""Bridge file for async feature.query.relations.test_relations_basic_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.relations.test_relations_basic_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_with_sync.py b/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_with.py similarity index 55% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_with_sync.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_with.py index 9d4a3e0e..8e7d2338 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_with_sync.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_with.py @@ -2,4 +2,4 @@ """ Bridge file for sync relation 'with' tests. """ -from rhosocial.activerecord.testsuite.feature.query.test_relations_with_sync import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.relations.test_relations_with import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_with_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_with_async.py similarity index 54% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_with_async.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_with_async.py index 854df473..8057becf 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_with_async.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_with_async.py @@ -2,4 +2,4 @@ """ Bridge file for async relation 'with' tests. """ -from rhosocial.activerecord.testsuite.feature.query.test_relations_with_async import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.relations.test_relations_with_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_set_operation.py b/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_active_query_set_operation.py similarity index 54% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_set_operation.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_active_query_set_operation.py index 04171825..d3981b53 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_set_operation.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_active_query_set_operation.py @@ -5,4 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_active_query_set_operation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.set_operations.test_active_query_set_operation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.set_operations.test_active_query_set_operation_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_active_query_set_operation_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_active_query_set_operation_async.py new file mode 100644 index 00000000..97e7fa6f --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_active_query_set_operation_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_active_query_set_operation_async.py +"""Bridge file for async feature.query.set_operations.test_active_query_set_operation_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.set_operations.test_active_query_set_operation_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_composite_pk_set_operation.py b/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_composite_pk_set_operation.py new file mode 100644 index 00000000..8cf2e58c --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_composite_pk_set_operation.py @@ -0,0 +1,6 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_composite_pk_set_operation.py +""" +Bridge file for composite PK set operation tests from the testsuite. +""" +from rhosocial.activerecord.testsuite.feature.query.set_operations.test_composite_pk_set_operation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.set_operations.test_composite_pk_set_operation_async import * # noqa: F403 \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_composite_pk_set_operation_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_composite_pk_set_operation_async.py new file mode 100644 index 00000000..fe46fbee --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_composite_pk_set_operation_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_composite_pk_set_operation_async.py +"""Bridge file for async feature.query.set_operations.test_composite_pk_set_operation_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.set_operations.test_composite_pk_set_operation_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_set_operation.py b/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_set_operation.py similarity index 55% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_set_operation.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_set_operation.py index fc7158fb..a23ca1b3 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_set_operation.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_set_operation.py @@ -5,4 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_set_operation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.set_operations.test_set_operation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.set_operations.test_set_operation_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_set_operation_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_set_operation_async.py new file mode 100644 index 00000000..20f605cb --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_set_operation_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_set_operation_async.py +"""Bridge file for async feature.query.set_operations.test_set_operation_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.set_operations.test_set_operation_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_example_query_fixtures.py b/tests/rhosocial/activerecord_postgres_test/feature/query/special/test_example_query_fixtures.py similarity index 56% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_example_query_fixtures.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/special/test_example_query_fixtures.py index 79aa6592..162cb400 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_example_query_fixtures.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/special/test_example_query_fixtures.py @@ -5,4 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_example_query_fixtures import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.special.test_example_query_fixtures import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.special.test_example_query_fixtures_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/special/test_example_query_fixtures_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/special/test_example_query_fixtures_async.py new file mode 100644 index 00000000..aa20fa09 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/special/test_example_query_fixtures_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/special/test_example_query_fixtures_async.py +"""Bridge file for async feature.query.special.test_example_query_fixtures_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.special.test_example_query_fixtures_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_special_queries.py b/tests/rhosocial/activerecord_postgres_test/feature/query/special/test_special_queries.py similarity index 56% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_special_queries.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/special/test_special_queries.py index b9eb5614..eb4efe6f 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_special_queries.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/special/test_special_queries.py @@ -5,4 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_special_queries import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.special.test_special_queries import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.special.test_special_queries_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/special/test_special_queries_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/special/test_special_queries_async.py new file mode 100644 index 00000000..4bba9f80 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/special/test_special_queries_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/special/test_special_queries_async.py +"""Bridge file for async feature.query.special.test_special_queries_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.special.test_special_queries_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_caching.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_caching.py deleted file mode 100644 index 2c23a774..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_caching.py +++ /dev/null @@ -1,5 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_caching.py -""" -Bridge file for eager loading caching tests. -""" -from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_caching import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_nested.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_nested.py deleted file mode 100644 index c90a39ef..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_nested.py +++ /dev/null @@ -1,5 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_nested.py -""" -Bridge file for eager loading nested tests. -""" -from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_nested import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios_sync.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios_sync.py deleted file mode 100644 index ff5df366..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios_sync.py +++ /dev/null @@ -1,5 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios_sync.py -""" -Bridge file for sync eager loading scenario tests. -""" -from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_scenarios_sync import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_all.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_all.py deleted file mode 100644 index c298cef3..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_all.py +++ /dev/null @@ -1,5 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_all.py -""" -Bridge file for eager loading with_all tests. -""" -from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_with_all import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_modifier.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_modifier.py deleted file mode 100644 index 433a7051..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_modifier.py +++ /dev/null @@ -1,5 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_modifier.py -""" -Bridge file for eager loading with_modifier tests. -""" -from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_with_modifier import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_one.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_one.py deleted file mode 100644 index df162324..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_one.py +++ /dev/null @@ -1,5 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_one.py -""" -Bridge file for eager loading with_one tests. -""" -from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_with_one import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_window_functions.py b/tests/rhosocial/activerecord_postgres_test/feature/query/window_functions/test_window_functions.py similarity index 54% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_window_functions.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/window_functions/test_window_functions.py index aa5d6cc7..54ee5de3 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_window_functions.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/window_functions/test_window_functions.py @@ -5,4 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_window_functions import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.window_functions.test_window_functions import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.window_functions.test_window_functions_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/window_functions/test_window_functions_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/window_functions/test_window_functions_async.py new file mode 100644 index 00000000..f69a0000 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/window_functions/test_window_functions_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/window_functions/test_window_functions_async.py +"""Bridge file for async feature.query.window_functions.test_window_functions_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.window_functions.test_window_functions_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/worker/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/query/worker/conftest.py new file mode 100644 index 00000000..e9cd5de9 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/worker/conftest.py @@ -0,0 +1,8 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/worker/conftest.py +""" +Pytest configuration for query worker tests. + +This file imports fixtures from the corresponding testsuite, making them +available to the tests in this directory. +""" +from rhosocial.activerecord.testsuite.feature.query.worker.conftest import * # noqa: F403 \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_parallel_queries.py b/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_parallel_queries.py index 7bfe5ecb..f7927c07 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_parallel_queries.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_parallel_queries.py @@ -9,3 +9,5 @@ async_order_fixtures_for_worker, # noqa: F401 ) from rhosocial.activerecord.testsuite.feature.query.worker.test_parallel_queries import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.worker.test_parallel_queries_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_parallel_queries_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_parallel_queries_async.py new file mode 100644 index 00000000..326c49d9 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_parallel_queries_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_parallel_queries_async.py +"""Bridge file for async feature.query.worker.test_parallel_queries_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.worker.test_parallel_queries_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_transaction_isolation_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_transaction_isolation_async.py new file mode 100644 index 00000000..f73efd1c --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_transaction_isolation_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_transaction_isolation_async.py +"""Bridge file for async feature.query.worker.test_transaction_isolation_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.worker.test_transaction_isolation_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_worker_transaction_isolation.py b/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_worker_transaction_isolation.py index c7590e84..9913196c 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_worker_transaction_isolation.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_worker_transaction_isolation.py @@ -10,3 +10,5 @@ async_order_fixtures_for_worker, # noqa: F401 ) from rhosocial.activerecord.testsuite.feature.query.worker.test_transaction_isolation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.worker.test_transaction_isolation_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/base/test_base.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/base/test_base.py new file mode 100644 index 00000000..1fb8802b --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/base/test_base.py @@ -0,0 +1,5 @@ +""" +Tests for relation base functionality. +Imports tests from the standardized testsuite. +""" +from rhosocial.activerecord.testsuite.feature.relation.base.test_base import TestRelationDescriptor # noqa: F401 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/base/test_base_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/base/test_base_async.py new file mode 100644 index 00000000..5259aa4a --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/base/test_base_async.py @@ -0,0 +1 @@ +from rhosocial.activerecord.testsuite.feature.relation.base.test_base_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/base/test_interfaces.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/base/test_interfaces.py new file mode 100644 index 00000000..dce937f9 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/base/test_interfaces.py @@ -0,0 +1,5 @@ +""" +Tests for relation interfaces. +Imports tests from the standardized testsuite. +""" +from rhosocial.activerecord.testsuite.feature.relation.base.test_interfaces import TestRelationInterfaces # noqa: F401 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/base/test_interfaces_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/base/test_interfaces_async.py new file mode 100644 index 00000000..688ea916 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/base/test_interfaces_async.py @@ -0,0 +1 @@ +from rhosocial.activerecord.testsuite.feature.relation.base.test_interfaces_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/boundary/test_boundary.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/boundary/test_boundary.py new file mode 100644 index 00000000..0efbbf89 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/boundary/test_boundary.py @@ -0,0 +1,3 @@ +from rhosocial.activerecord.testsuite.feature.relation.boundary.test_boundary import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.boundary.test_boundary_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/boundary/test_boundary_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/boundary/test_boundary_async.py new file mode 100644 index 00000000..591af194 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/boundary/test_boundary_async.py @@ -0,0 +1 @@ +from rhosocial.activerecord.testsuite.feature.relation.boundary.test_boundary_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/cache/test_cache.py similarity index 76% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/cache/test_cache.py index ccd3017a..7e97b7cd 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/cache/test_cache.py @@ -7,7 +7,7 @@ # Import the test classes from the testsuite -from rhosocial.activerecord.testsuite.feature.relation.test_cache import TestRelationCache # noqa: F401 +from rhosocial.activerecord.testsuite.feature.relation.cache.test_cache import TestRelationCache # noqa: F401 # This will cause pytest to run all the tests in the imported class diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/cache/test_cache_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/cache/test_cache_async.py new file mode 100644 index 00000000..33f8eccb --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/cache/test_cache_async.py @@ -0,0 +1 @@ +from rhosocial.activerecord.testsuite.feature.relation.cache.test_cache_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/cache/test_cache_clearing.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/cache/test_cache_clearing.py new file mode 100644 index 00000000..ae990e84 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/cache/test_cache_clearing.py @@ -0,0 +1,3 @@ +from rhosocial.activerecord.testsuite.feature.relation.cache.test_cache_clearing import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.cache.test_cache_clearing_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/cache/test_cache_clearing_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/cache/test_cache_clearing_async.py new file mode 100644 index 00000000..e5e95b6a --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/cache/test_cache_clearing_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/relation/cache/test_cache_clearing_async.py +"""Bridge file for async feature.relation.cache.test_cache_clearing_async tests.""" +from rhosocial.activerecord.testsuite.feature.relation.cache.test_cache_clearing_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/conftest.py index cb3209b8..77f688a5 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/conftest.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/conftest.py @@ -28,30 +28,29 @@ async def async_comment_class(async_user_post_comment_classes): return comment -@pytest_asyncio.fixture(scope="function", params=SCENARIO_PARAMS) # noqa: F405 +@pytest_asyncio.fixture(scope="function", params=SCENARIO_PARAMS_ASYNC) # noqa: F405 async def async_user_post_comment_classes(request): from rhosocial.activerecord.testsuite.core.registry import get_provider_registry scenario = request.param provider_registry = get_provider_registry() - provider_class = provider_registry.get_provider(PROVIDER_KEY) # noqa: F405 + provider_class = provider_registry.get_provider(PROVIDER_KEY_ASYNC) # noqa: F405 provider = provider_class() - user = provider.setup_async_user_model(scenario) - post = provider.setup_async_post_model(scenario) - comment = provider.setup_async_comment_model(scenario) - await provider._ensure_user_post_comment_async_schema() + user = await provider.setup_user_model(scenario) + post = await provider.setup_post_model(scenario) + comment = await provider.setup_comment_model(scenario) yield user, post, comment - await provider.cleanup_after_test_async(scenario) + await provider.cleanup_after_test(scenario) -@pytest_asyncio.fixture(scope="function", params=SCENARIO_PARAMS) # noqa: F405 +@pytest_asyncio.fixture(scope="function", params=SCENARIO_PARAMS_ASYNC) # noqa: F405 async def async_relation_boundary_context(request): from rhosocial.activerecord.testsuite.core.registry import get_provider_registry scenario = request.param provider_registry = get_provider_registry() - provider_class = provider_registry.get_provider(PROVIDER_KEY) # noqa: F405 + provider_class = provider_registry.get_provider(PROVIDER_KEY_ASYNC) # noqa: F405 provider = provider_class() - owner, profile, post = provider.setup_async_relation_boundary_fixtures(scenario) + owner, profile, post = await provider.setup_relation_boundary_fixtures(scenario) yield provider, scenario, owner, profile, post - await provider.cleanup_after_test_async(scenario) + await provider.cleanup_after_test(scenario) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field.py new file mode 100644 index 00000000..cd7185c5 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field.py +"""Bridge file for sync relation derived field tests.""" +from rhosocial.activerecord.testsuite.feature.relation.derived.test_derived_field import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field_async.py new file mode 100644 index 00000000..417703a6 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field_async.py @@ -0,0 +1 @@ +from rhosocial.activerecord.testsuite.feature.relation.derived.test_derived_field_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field_json.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field_json.py new file mode 100644 index 00000000..21d13321 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field_json.py @@ -0,0 +1,3 @@ +from rhosocial.activerecord.testsuite.feature.relation.derived.test_derived_field_json import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.derived.test_derived_field_json_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field_json_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field_json_async.py new file mode 100644 index 00000000..867655b5 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field_json_async.py @@ -0,0 +1 @@ +from rhosocial.activerecord.testsuite.feature.relation.derived.test_derived_field_json_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/descriptors/test_descriptor_compat.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/descriptors/test_descriptor_compat.py new file mode 100644 index 00000000..09df177d --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/descriptors/test_descriptor_compat.py @@ -0,0 +1,4 @@ +# tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptor_compat.py + +from rhosocial.activerecord.testsuite.feature.relation.descriptors.test_descriptor_compat import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.descriptors.test_descriptor_compat_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/descriptors/test_descriptor_compat_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/descriptors/test_descriptor_compat_async.py new file mode 100644 index 00000000..53052583 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/descriptors/test_descriptor_compat_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/relation/descriptors/test_descriptor_compat_async.py +"""Bridge file for async feature.relation.descriptors.test_descriptor_compat_async tests.""" +from rhosocial.activerecord.testsuite.feature.relation.descriptors.test_descriptor_compat_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/descriptors/test_descriptors.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/descriptors/test_descriptors.py new file mode 100644 index 00000000..9622ce84 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/descriptors/test_descriptors.py @@ -0,0 +1,5 @@ +""" +Tests for relation descriptor functionality. +Imports tests from the standardized testsuite. +""" +from rhosocial.activerecord.testsuite.feature.relation.descriptors.test_descriptors import TestRelationDescriptors # noqa: F401 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/descriptors/test_descriptors_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/descriptors/test_descriptors_async.py new file mode 100644 index 00000000..b10f32f3 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/descriptors/test_descriptors_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptors_async.py + +from rhosocial.activerecord.testsuite.feature.relation.descriptors.test_descriptors_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/eager_loading/test_nested_relationship_access.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/eager_loading/test_nested_relationship_access.py new file mode 100644 index 00000000..f9447d3a --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/eager_loading/test_nested_relationship_access.py @@ -0,0 +1,5 @@ +""" +Tests for nested relationship access functionality. +Imports tests from the standardized testsuite. +""" +from rhosocial.activerecord.testsuite.feature.relation.eager_loading.test_nested_relationship_access import TestNestedRelationshipAccess # noqa: E501, F401 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/eager_loading/test_nested_relationship_access_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/eager_loading/test_nested_relationship_access_async.py new file mode 100644 index 00000000..6ea7cbbe --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/eager_loading/test_nested_relationship_access_async.py @@ -0,0 +1 @@ +from rhosocial.activerecord.testsuite.feature.relation.eager_loading.test_nested_relationship_access_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/eager_loading/test_with_method.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/eager_loading/test_with_method.py new file mode 100644 index 00000000..a1ce8686 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/eager_loading/test_with_method.py @@ -0,0 +1,3 @@ +from rhosocial.activerecord.testsuite.feature.relation.eager_loading.test_with_method import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.eager_loading.test_with_method_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/eager_loading/test_with_method_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/eager_loading/test_with_method_async.py new file mode 100644 index 00000000..a2c28563 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/eager_loading/test_with_method_async.py @@ -0,0 +1 @@ +from rhosocial.activerecord.testsuite.feature.relation.eager_loading.test_with_method_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/integration/test_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/integration/test_integration.py new file mode 100644 index 00000000..eb3e57d4 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/integration/test_integration.py @@ -0,0 +1,3 @@ +from rhosocial.activerecord.testsuite.feature.relation.integration.test_integration import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.integration.test_integration_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/integration/test_integration_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/integration/test_integration_async.py new file mode 100644 index 00000000..ef9b2e02 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/integration/test_integration_async.py @@ -0,0 +1 @@ +from rhosocial.activerecord.testsuite.feature.relation.integration.test_integration_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/modifiers/test_modifier_behavior.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/modifiers/test_modifier_behavior.py new file mode 100644 index 00000000..8fc621f3 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/modifiers/test_modifier_behavior.py @@ -0,0 +1,3 @@ +from rhosocial.activerecord.testsuite.feature.relation.modifiers.test_modifier_behavior import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.modifiers.test_modifier_behavior_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/modifiers/test_modifier_behavior_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/modifiers/test_modifier_behavior_async.py new file mode 100644 index 00000000..797fa5e3 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/modifiers/test_modifier_behavior_async.py @@ -0,0 +1 @@ +from rhosocial.activerecord.testsuite.feature.relation.modifiers.test_modifier_behavior_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/modifiers/test_modifier_warning.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/modifiers/test_modifier_warning.py new file mode 100644 index 00000000..2b7b7bc8 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/modifiers/test_modifier_warning.py @@ -0,0 +1,3 @@ +from rhosocial.activerecord.testsuite.feature.relation.modifiers.test_modifier_warning import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.modifiers.test_modifier_warning_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/modifiers/test_modifier_warning_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/modifiers/test_modifier_warning_async.py new file mode 100644 index 00000000..6e884253 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/modifiers/test_modifier_warning_async.py @@ -0,0 +1 @@ +from rhosocial.activerecord.testsuite.feature.relation.modifiers.test_modifier_warning_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_base.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_base.py deleted file mode 100644 index 2b7e6eb2..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_base.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -Tests for relation base functionality. -Imports tests from the standardized testsuite. -""" -from rhosocial.activerecord.testsuite.feature.relation.test_base import TestRelationDescriptor # noqa: F401 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_boundary.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_boundary.py deleted file mode 100644 index 7e44d5a1..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_boundary.py +++ /dev/null @@ -1 +0,0 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_boundary import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_boundary_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_boundary_async.py deleted file mode 100644 index 95cbd862..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_boundary_async.py +++ /dev/null @@ -1 +0,0 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_boundary_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache_clearing.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache_clearing.py deleted file mode 100644 index 4a4b246f..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache_clearing.py +++ /dev/null @@ -1 +0,0 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_cache_clearing import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field.py deleted file mode 100644 index 21564139..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field.py +++ /dev/null @@ -1 +0,0 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_derived_field import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_async.py deleted file mode 100644 index af5beacd..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_async.py +++ /dev/null @@ -1 +0,0 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_derived_field_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_json.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_json.py deleted file mode 100644 index 67251dc4..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_json.py +++ /dev/null @@ -1 +0,0 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_derived_field_json import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_json_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_json_async.py deleted file mode 100644 index 0130588f..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_json_async.py +++ /dev/null @@ -1 +0,0 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_derived_field_json_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptor_compat.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptor_compat.py deleted file mode 100644 index 47c04e0f..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptor_compat.py +++ /dev/null @@ -1,3 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptor_compat.py - -from rhosocial.activerecord.testsuite.feature.relation.test_descriptor_compat import * # noqa: F401,F403 \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptors.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptors.py deleted file mode 100644 index 8f5b34e5..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptors.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -Tests for relation descriptor functionality. -Imports tests from the standardized testsuite. -""" -from rhosocial.activerecord.testsuite.feature.relation.test_descriptors import TestRelationDescriptors # noqa: F401 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptors_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptors_async.py deleted file mode 100644 index 8fa56f6d..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptors_async.py +++ /dev/null @@ -1,3 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptors_async.py - -from rhosocial.activerecord.testsuite.feature.relation.test_descriptors_async import * # noqa: F401,F403 \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_integration.py deleted file mode 100644 index ebd76ceb..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_integration.py +++ /dev/null @@ -1 +0,0 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_integration import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_integration_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_integration_async.py deleted file mode 100644 index 5bc443dc..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_integration_async.py +++ /dev/null @@ -1 +0,0 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_integration_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_interfaces.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_interfaces.py deleted file mode 100644 index 2e1fab2c..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_interfaces.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -Tests for relation interfaces. -Imports tests from the standardized testsuite. -""" -from rhosocial.activerecord.testsuite.feature.relation.test_interfaces import TestRelationInterfaces # noqa: F401 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_behavior.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_behavior.py deleted file mode 100644 index bbd9a806..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_behavior.py +++ /dev/null @@ -1 +0,0 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_modifier_behavior import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_behavior_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_behavior_async.py deleted file mode 100644 index 6538d2b1..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_behavior_async.py +++ /dev/null @@ -1 +0,0 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_modifier_behavior_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_warning.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_warning.py deleted file mode 100644 index eb29123d..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_warning.py +++ /dev/null @@ -1 +0,0 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_modifier_warning import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_warning_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_warning_async.py deleted file mode 100644 index b7207a9f..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_warning_async.py +++ /dev/null @@ -1 +0,0 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_modifier_warning_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_nested_relationship_access.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_nested_relationship_access.py deleted file mode 100644 index edb06654..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_nested_relationship_access.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -Tests for nested relationship access functionality. -Imports tests from the standardized testsuite. -""" -from rhosocial.activerecord.testsuite.feature.relation.test_nested_relationship_access import TestNestedRelationshipAccess # noqa: E501, F401 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_path_validation.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_path_validation.py deleted file mode 100644 index 37e7e8e9..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_path_validation.py +++ /dev/null @@ -1 +0,0 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_path_validation import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_path_validation_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_path_validation_async.py deleted file mode 100644 index 599860af..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_path_validation_async.py +++ /dev/null @@ -1 +0,0 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_path_validation_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_validator.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_validator.py deleted file mode 100644 index 0cebf814..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_validator.py +++ /dev/null @@ -1,3 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/relation/test_validator.py - -from rhosocial.activerecord.testsuite.feature.relation.test_validator import * # noqa: F401,F403 \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_with_method.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_with_method.py deleted file mode 100644 index fdb8bca6..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_with_method.py +++ /dev/null @@ -1 +0,0 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_with_method import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_with_method_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_with_method_async.py deleted file mode 100644 index de09d14a..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_with_method_async.py +++ /dev/null @@ -1 +0,0 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_with_method_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/validation/test_path_validation.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/validation/test_path_validation.py new file mode 100644 index 00000000..b2b0a150 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/validation/test_path_validation.py @@ -0,0 +1,3 @@ +from rhosocial.activerecord.testsuite.feature.relation.validation.test_path_validation import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.validation.test_path_validation_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/validation/test_path_validation_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/validation/test_path_validation_async.py new file mode 100644 index 00000000..b492478f --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/validation/test_path_validation_async.py @@ -0,0 +1 @@ +from rhosocial.activerecord.testsuite.feature.relation.validation.test_path_validation_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/validation/test_validator.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/validation/test_validator.py new file mode 100644 index 00000000..47df6d4e --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/validation/test_validator.py @@ -0,0 +1,4 @@ +# tests/rhosocial/activerecord_postgres_test/feature/relation/test_validator.py + +from rhosocial.activerecord.testsuite.feature.relation.validation.test_validator import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.validation.test_validator_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/validation/test_validator_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/validation/test_validator_async.py new file mode 100644 index 00000000..799baf9f --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/validation/test_validator_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/relation/validation/test_validator_async.py +"""Bridge file for async feature.relation.validation.test_validator_async tests.""" +from rhosocial.activerecord.testsuite.feature.relation.validation.test_validator_async import * # noqa: F401, F403