From 7c1e57f930e394b37d5b5039bedf65db143b09b6 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Fri, 1 May 2026 15:25:20 +0800 Subject: [PATCH 01/12] chore: bump version to 1.0.0.dev13 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3628cdbf..75206926 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "rhosocial-activerecord-postgres" -version = "1.0.0.dev12" +version = "1.0.0.dev13" description = "postgres backend implementation for rhosocial-activerecord, providing a robust and optimized postgres database support." readme = "README.md" license = { file = "LICENSE" } From 6bb7f2a65a9b1bee806a33346463f59874957f2e Mon Sep 17 00:00:00 2001 From: zhao vistart <864657+vistart@users.noreply.github.com> Date: Fri, 1 May 2026 22:59:26 +0800 Subject: [PATCH 02/12] fix(security): resolve SQL injection risks in PostgreSQL backend (#35) * fix: resolve SQL injection risks in DDL/DML statements - Fix partial index where_clause to accept ToSQLProtocol for parameterized queries - Fix CREATE TYPE ENUM values to use _escape_sql_string for escaping - Fix trigger function name to use format_identifier for proper quoting - Fix stored procedure names (CREATE/DROP/CALL) to use format_identifier - Fix extended statistics names to use format_identifier Resolves: SQL injection vulnerabilities in PostgreSQL backend * fix: resolve parameter loss bug in where_clause The ToSQLProtocol path was collecting parameters but returning hardcoded empty tuple, causing WHERE clause parameters to be silently dropped. This commit fixes the return statement to include the collected parameters. * test: add SQL injection security tests for PostgreSQL fixes - Add tests for CREATE TYPE ENUM value escaping - Add tests for trigger function name format_identifier - Add tests for stored procedure names (CREATE/DROP/CALL) - Add tests for extended statistics names - Add tests for partial index WHERE clause ToSQLProtocol support Resolves: Test coverage for SQL injection fixes in PostgreSQL backend * test: fix and enhance PostgreSQL security tests - Fix method name: format_create_type_enum_statement - Fix method name: format_call_statement - Add all_params initialization in format_create_index_pg_statement - Add tests for ENUM escaping, stored procedures, and partial index WHERE clause * chore: use git source for rhosocial-activerecord at release/v1.0.0.dev26 * test: update test assertions for quoted identifiers * chore: add changelog fragment for PR #35 --- .github/workflows/test.yml | 2 +- changelog.d/35.security.md | 1 + .../backend/impl/postgres/mixins/ddl/index.py | 20 +- .../impl/postgres/mixins/ddl/trigger.py | 2 +- .../backend/impl/postgres/mixins/ddl/type.py | 2 +- .../mixins/dml/extended_statistics.py | 6 +- .../postgres/mixins/dml/stored_procedure.py | 6 +- .../postgres/test_extended_statistics.py | 22 +- .../backend/postgres/test_stored_procedure.py | 22 +- .../feature/backend/postgres/test_trigger.py | 2 +- .../backend/test_postgres_dialect_security.py | 215 +++++++++++++++++- 11 files changed, 263 insertions(+), 37 deletions(-) create mode 100644 changelog.d/35.security.md diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1af384c8..990fa173 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,7 +79,7 @@ jobs: - name: Install activerecord and testsuite from PyPI/git run: | - pip install rhosocial-activerecord==1.0.0.dev25 + pip install git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev26#egg=rhosocial-activerecord pip install rhosocial-activerecord-testsuite==1.0.0.dev12 - name: Create postgres scenario config diff --git a/changelog.d/35.security.md b/changelog.d/35.security.md new file mode 100644 index 00000000..d1efe6bc --- /dev/null +++ b/changelog.d/35.security.md @@ -0,0 +1 @@ +**SECURITY**: Resolved SQL injection risks in PostgreSQL backend \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/index.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/index.py index b00ad848..97ff0d2d 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/index.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/index.py @@ -5,7 +5,7 @@ PostgreSQL-specific index features and operations. """ -from typing import Any, Dict, Optional, Tuple, List, TYPE_CHECKING +from typing import Any, Dict, Optional, Tuple, List, Union, TYPE_CHECKING from rhosocial.activerecord.backend.expression.bases import ToSQLProtocol @@ -247,13 +247,20 @@ def format_create_index_pg_statement( include_columns: Optional[List[str]] = None, with_options: Optional[Dict[str, Any]] = None, tablespace: Optional[str] = None, - where_clause: Optional[str] = None, + where_clause: Optional[Union[str, ToSQLProtocol]] = None, ) -> Tuple[str, tuple]: """Format CREATE INDEX statement with PostgreSQL-specific options. This method provides a convenient way to create indexes with PostgreSQL-specific options without using expression objects. + + Args: + where_clause: Accepts either a string (for hardcoded conditions) + or a ToSQLProtocol expression for parameterized queries. + WARNING: When using string, never pass user input directly; + always use ToSQLProtocol expressions for untrusted input. """ + all_params = [] parts = ["CREATE"] if unique: @@ -312,6 +319,11 @@ def format_create_index_pg_statement( # WHERE clause for partial index if where_clause: - parts.append(f"WHERE {where_clause}") + if isinstance(where_clause, ToSQLProtocol): + where_sql, where_params = where_clause.to_sql() + parts.append(f"WHERE {where_sql}") + all_params = list(all_params) + list(where_params) + else: + parts.append(f"WHERE {where_clause}") - return (" ".join(parts), ()) + return (" ".join(parts), tuple(all_params)) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/trigger.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/trigger.py index 54b9e1d1..e7d16d32 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/trigger.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/trigger.py @@ -66,7 +66,7 @@ def format_create_trigger_statement(self, expr) -> Tuple[str, tuple]: all_params.extend(cond_params) parts.append("EXECUTE FUNCTION") - parts.append(expr.function_name + "()") + parts.append(f"{self.format_identifier(expr.function_name)}()") return " ".join(parts), tuple(all_params) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/type.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/type.py index 1f143af1..8d8c756c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/type.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/type.py @@ -51,7 +51,7 @@ def format_create_type_enum_statement( if schema: type_name = f"{self.format_identifier(schema)}.{type_name}" - escaped_values = [f"'{v}'" for v in values] + escaped_values = [f"'{self._escape_sql_string(str(v))}'" for v in values] values_str = ", ".join(escaped_values) sql = f"CREATE TYPE {type_name} AS ENUM ({values_str})" diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/extended_statistics.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/extended_statistics.py index e4999dd8..05f83efd 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/extended_statistics.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/extended_statistics.py @@ -46,8 +46,8 @@ def format_create_statistics_statement(self, expr: "PostgresCreateStatisticsExpr if not self.supports_create_statistics(): raise ValueError("CREATE STATISTICS requires PostgreSQL 10+") - full_name = f"{expr.schema}.{expr.name}" if expr.schema else expr.name - table_full = f"{expr.schema}.{expr.table_name}" if expr.schema else expr.table_name + full_name = f"{self.format_identifier(expr.schema)}.{self.format_identifier(expr.name)}" if expr.schema else self.format_identifier(expr.name) + table_full = f"{self.format_identifier(expr.schema)}.{self.format_identifier(expr.table_name)}" if expr.schema else self.format_identifier(expr.table_name) exists_clause = "IF NOT EXISTS " if expr.if_not_exists else "" @@ -78,7 +78,7 @@ def format_drop_statistics_statement(self, expr: "PostgresDropStatisticsExpressi Returns: Tuple of (SQL statement, parameters tuple) """ - full_name = f"{expr.schema}.{expr.name}" if expr.schema else expr.name + full_name = f"{self.format_identifier(expr.schema)}.{self.format_identifier(expr.name)}" if expr.schema else self.format_identifier(expr.name) exists_clause = "IF EXISTS " if expr.if_exists else "" sql = f"DROP STATISTICS {exists_clause}{full_name}" diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/stored_procedure.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/stored_procedure.py index 4b07beb9..3023aa17 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/stored_procedure.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/stored_procedure.py @@ -63,7 +63,7 @@ def format_create_procedure_statement( if not self.supports_call_statement(): raise ValueError("CREATE PROCEDURE requires PostgreSQL 11+") - full_name = f"{schema}.{name}" if schema else name + full_name = f"{self.format_identifier(schema)}.{self.format_identifier(name)}" if schema else self.format_identifier(name) # OR REPLACE clause replace_clause = "OR REPLACE " if or_replace else "" @@ -126,7 +126,7 @@ def format_drop_procedure_statement( Returns: Tuple of (SQL statement, parameters tuple) """ - full_name = f"{schema}.{name}" if schema else name + full_name = f"{self.format_identifier(schema)}.{self.format_identifier(name)}" if schema else self.format_identifier(name) exists_clause = "IF EXISTS " if if_exists else "" @@ -158,7 +158,7 @@ def format_call_statement( if not self.supports_call_statement(): raise ValueError("CALL statement requires PostgreSQL 11+") - full_name = f"{schema}.{name}" if schema else name + full_name = f"{self.format_identifier(schema)}.{self.format_identifier(name)}" if schema else self.format_identifier(name) if arguments: placeholders = ", ".join(["%s"] * len(arguments)) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_extended_statistics.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_extended_statistics.py index 19415202..d2160cc7 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_extended_statistics.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_extended_statistics.py @@ -84,9 +84,9 @@ def test_create_statistics_basic(self): table_name="test_table" ) sql, params = dialect.format_create_statistics_statement(expr) - assert "CREATE STATISTICS test_stats" in sql + assert 'CREATE STATISTICS "test_stats"' in sql assert "ON col1, col2" in sql - assert "FROM test_table" in sql + assert 'FROM "test_table"' in sql assert params == () def test_create_statistics_with_schema(self): @@ -100,8 +100,8 @@ def test_create_statistics_with_schema(self): schema="public" ) sql, params = dialect.format_create_statistics_statement(expr) - assert "public.test_stats" in sql - assert "FROM public.test_table" in sql + assert '"public"."test_stats"' in sql + assert 'FROM "public"."test_table"' in sql def test_create_statistics_if_not_exists(self): """Test CREATE STATISTICS IF NOT EXISTS.""" @@ -114,7 +114,7 @@ def test_create_statistics_if_not_exists(self): if_not_exists=True ) sql, params = dialect.format_create_statistics_statement(expr) - assert "CREATE STATISTICS IF NOT EXISTS test_stats" in sql + assert 'CREATE STATISTICS IF NOT EXISTS "test_stats"' in sql def test_create_statistics_with_ndistinct(self): """Test CREATE STATISTICS with ndistinct type.""" @@ -127,7 +127,7 @@ def test_create_statistics_with_ndistinct(self): statistics_type="ndistinct" ) sql, params = dialect.format_create_statistics_statement(expr) - assert "test_stats(ndistinct)" in sql + assert '"test_stats"(ndistinct)' in sql def test_create_statistics_with_dependencies(self): """Test CREATE STATISTICS with dependencies type.""" @@ -140,7 +140,7 @@ def test_create_statistics_with_dependencies(self): statistics_type="dependencies" ) sql, params = dialect.format_create_statistics_statement(expr) - assert "test_stats(dependencies)" in sql + assert '"test_stats"(dependencies)' in sql def test_create_statistics_with_mcv_pg12(self): """Test CREATE STATISTICS with MCV type on PostgreSQL 12.""" @@ -153,7 +153,7 @@ def test_create_statistics_with_mcv_pg12(self): statistics_type="mcv" ) sql, params = dialect.format_create_statistics_statement(expr) - assert "test_stats(mcv)" in sql + assert '"test_stats"(mcv)' in sql def test_create_statistics_with_mcv_pg11_raises_error(self): """Test CREATE STATISTICS with MCV type on PostgreSQL 11 raises error.""" @@ -193,7 +193,7 @@ def test_drop_statistics_basic(self): name="test_stats" ) sql, params = dialect.format_drop_statistics_statement(expr) - assert sql == "DROP STATISTICS test_stats" + assert sql == 'DROP STATISTICS "test_stats"' assert params == () def test_drop_statistics_with_schema(self): @@ -205,7 +205,7 @@ def test_drop_statistics_with_schema(self): schema="public" ) sql, params = dialect.format_drop_statistics_statement(expr) - assert "DROP STATISTICS public.test_stats" in sql + assert 'DROP STATISTICS "public"."test_stats"' in sql def test_drop_statistics_if_exists(self): """Test DROP STATISTICS IF EXISTS.""" @@ -216,4 +216,4 @@ def test_drop_statistics_if_exists(self): if_exists=True ) sql, params = dialect.format_drop_statistics_statement(expr) - assert "DROP STATISTICS IF EXISTS test_stats" in sql + assert 'DROP STATISTICS IF EXISTS "test_stats"' in sql diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_stored_procedure.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_stored_procedure.py index 8fe5dc10..a912b91e 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_stored_procedure.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_stored_procedure.py @@ -65,7 +65,7 @@ def test_create_procedure_basic(self): name="test_proc", body="BEGIN NULL; END" ) - assert "CREATE PROCEDURE test_proc" in sql + assert "CREATE PROCEDURE \"test_proc\"" in sql assert "LANGUAGE plpgsql" in sql assert "BEGIN NULL; END" in sql assert params == () @@ -78,7 +78,7 @@ def test_create_procedure_with_schema(self): schema="public", body="BEGIN NULL; END" ) - assert "public.test_proc" in sql + assert '"public"."test_proc"' in sql def test_create_procedure_with_or_replace(self): """Test CREATE OR REPLACE PROCEDURE statement.""" @@ -181,7 +181,7 @@ def test_drop_procedure_basic(self): sql, params = dialect.format_drop_procedure_statement( name="test_proc" ) - assert sql == "DROP PROCEDURE test_proc" + assert sql == "DROP PROCEDURE \"test_proc\"" assert params == () def test_drop_procedure_with_schema(self): @@ -191,7 +191,7 @@ def test_drop_procedure_with_schema(self): name="test_proc", schema="public" ) - assert "DROP PROCEDURE public.test_proc" in sql + assert 'DROP PROCEDURE "public"."test_proc"' in sql def test_drop_procedure_if_exists(self): """Test DROP PROCEDURE IF EXISTS.""" @@ -200,7 +200,7 @@ def test_drop_procedure_if_exists(self): name="test_proc", if_exists=True ) - assert "DROP PROCEDURE IF EXISTS test_proc" in sql + assert "DROP PROCEDURE IF EXISTS \"test_proc\"" in sql def test_drop_procedure_cascade(self): """Test DROP PROCEDURE CASCADE.""" @@ -209,7 +209,7 @@ def test_drop_procedure_cascade(self): name="test_proc", cascade=True ) - assert "DROP PROCEDURE test_proc CASCADE" in sql + assert "DROP PROCEDURE \"test_proc\" CASCADE" in sql def test_drop_procedure_with_parameter_types(self): """Test DROP PROCEDURE with parameter types for overload resolution.""" @@ -221,7 +221,7 @@ def test_drop_procedure_with_parameter_types(self): {"type": "varchar"} ] ) - assert "test_proc(integer, varchar)" in sql + assert '"test_proc"(integer, varchar)' in sql def test_drop_procedure_with_parameter_type_strings(self): """Test DROP PROCEDURE with parameter types as strings.""" @@ -230,7 +230,7 @@ def test_drop_procedure_with_parameter_type_strings(self): name="test_proc", parameters=["integer", "varchar"] ) - assert "test_proc(integer, varchar)" in sql + assert '"test_proc"(integer, varchar)' in sql class TestFormatCallStatement: @@ -246,7 +246,7 @@ def test_call_statement_no_args(self): """Test CALL statement without arguments.""" dialect = PostgresDialect((14, 0, 0)) sql, params = dialect.format_call_statement(name="test_proc") - assert sql == "CALL test_proc()" + assert sql == 'CALL "test_proc"()' assert params == () def test_call_statement_with_args(self): @@ -256,7 +256,7 @@ def test_call_statement_with_args(self): name="test_proc", arguments=[1, "test", None] ) - assert "CALL test_proc(%s, %s, %s)" in sql + assert 'CALL "test_proc"(%s, %s, %s)' in sql assert params == (1, "test", None) def test_call_statement_with_schema(self): @@ -266,4 +266,4 @@ def test_call_statement_with_schema(self): name="test_proc", schema="public" ) - assert "CALL public.test_proc()" in sql + assert 'CALL "public"."test_proc"()' in sql diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_trigger.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_trigger.py index 85228336..5eacdd2b 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_trigger.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_trigger.py @@ -75,7 +75,7 @@ def test_create_trigger_basic_before(self): assert 'CREATE TRIGGER "update_timestamp"' in sql assert "BEFORE UPDATE" in sql assert 'ON "users"' in sql - assert "EXECUTE FUNCTION update_updated_at_column()" in sql + assert 'EXECUTE FUNCTION "update_updated_at_column"()' in sql def test_create_trigger_basic_after(self): """Test basic CREATE TRIGGER AFTER statement.""" diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_dialect_security.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_dialect_security.py index 24edfb84..d4d6fab9 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_dialect_security.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_dialect_security.py @@ -9,6 +9,7 @@ import pytest from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.expression.bases import BaseExpression from rhosocial.activerecord.backend.expression.statements import ( ColumnDefinition, ColumnConstraint, @@ -16,6 +17,7 @@ TableConstraint, TableConstraintType, ) +from typing import Tuple, Any @pytest.fixture @@ -167,4 +169,215 @@ def test_exclude_constraint_sql_injection_prevention(dialect): assert "; DROP" not in sql assert "--" not in sql - assert "/*" not in sql \ No newline at end of file + assert "/*" not in sql + + +class TestPostgresEnumSecurity: + """Tests for CREATE TYPE ENUM value escaping (fix for SQL injection risk).""" + + def test_create_type_enum_values_escaped(self, dialect): + """Test ENUM values are properly escaped with single quote doubling.""" + sql, params = dialect.format_create_type_enum_statement( + name="test_enum", + values=["it's", "normal", "test's value"], + ) + + assert "it''s" in sql + assert "test''s value" in sql + assert "'; DROP" not in sql + + def test_create_type_enum_sql_injection_blocked(self, dialect): + """Test SQL injection in ENUM values is properly escaped (not executed).""" + sql, params = dialect.format_create_type_enum_statement( + name="test_enum", + values=["normal", "'; DROP TABLE users--"], + ) + + assert "DROP TABLE users--" in sql + assert "CREATE TYPE" in sql + assert "AS ENUM" in sql + + +class TestPostgresStoredProcedureNameSecurity: + """Tests for stored procedure names using format_identifier.""" + + def test_create_procedure_name_quoted(self, dialect): + """Test CREATE PROCEDURE name is properly quoted.""" + sql, params = dialect.format_create_procedure_statement( + name="my_procedure", + body="BEGIN END", + language="PL/pgSQL", + ) + + assert '"my_procedure"' in sql + + def test_drop_procedure_name_quoted(self, dialect): + """Test DROP PROCEDURE name is properly quoted.""" + sql, params = dialect.format_drop_procedure_statement( + name="my_procedure", + ) + + assert '"my_procedure"' in sql + + def test_call_statement_name_quoted(self, dialect): + """Test CALL statement procedure name is properly quoted.""" + sql, params = dialect.format_call_statement( + name="my_procedure", + ) + + assert '"my_procedure"' in sql + + def test_procedure_with_schema_quoted(self, dialect): + """Test procedure with schema is properly quoted.""" + sql, params = dialect.format_create_procedure_statement( + schema="my_schema", + name="my_procedure", + body="BEGIN END", + language="PL/pgSQL", + ) + + assert '"my_schema"' in sql + assert '"my_procedure"' in sql + + +class TestPostgresPartialIndexWhereClauseSecurity: + """Tests for partial index WHERE clause (ToSQLProtocol support).""" + + def test_create_index_with_string_where_clause(self, dialect): + """Test CREATE INDEX with string WHERE clause (backward compatible).""" + sql, params = dialect.format_create_index_pg_statement( + index_name="idx_test", + table_name="users", + columns=["email"], + where_clause="active = true", + ) + + assert "WHERE active = true" in sql + assert params == () + + def test_create_index_with_to_sql_protocol(self, dialect): + """Test CREATE INDEX with ToSQLProtocol expression (parameterized).""" + from rhosocial.activerecord.backend.expression.bases import BaseExpression + + class MockWhereExpr(BaseExpression): + def __init__(self): + self._sql = "age >= 18" + self._params = (18,) + + def to_sql(self): + return self._sql, self._params + + sql, params = dialect.format_create_index_pg_statement( + index_name="idx_active", + table_name="users", + columns=["email"], + where_clause=MockWhereExpr(), + ) + + assert "WHERE age >= 18" in sql + assert params == (18,) + + def test_create_index_where_clause_params_collected(self, dialect): + """Test WHERE clause parameters are properly collected and returned.""" + from rhosocial.activerecord.backend.expression.bases import BaseExpression + + class MockWhereExpr(BaseExpression): + def __init__(self): + self._sql = "status = %s" + self._params = ("active",) + + def to_sql(self): + return self._sql, self._params + + sql, params = dialect.format_create_index_pg_statement( + index_name="idx_test", + table_name="users", + columns=["status"], + where_clause=MockWhereExpr(), + ) + + assert "WHERE status = %s" in sql + assert params == ("active",) + + +class TestPostgresTriggerFunctionNameSecurity: + """Tests for trigger function name using format_identifier.""" + + def test_trigger_function_name_quoted(self, dialect): + """Test trigger function name is properly quoted with double quotes.""" + from rhosocial.activerecord.backend.expression.statements.ddl_trigger import ( + CreateTriggerExpression, + TriggerTiming, + TriggerEvent, + ) + + expr = CreateTriggerExpression( + dialect=dialect, + trigger_name="my_trigger", + table_name="users", + timing=TriggerTiming.BEFORE, + events=[TriggerEvent.INSERT], + function_name="my_function", + ) + + sql, params = dialect.format_create_trigger_statement(expr) + + assert '"my_function"' in sql + + def test_trigger_function_name_special_chars_quoted(self, dialect): + """Test trigger function name with special characters is quoted.""" + from rhosocial.activerecord.backend.expression.statements.ddl_trigger import ( + CreateTriggerExpression, + TriggerTiming, + TriggerEvent, + ) + + expr = CreateTriggerExpression( + dialect=dialect, + trigger_name="trigger", + table_name="users", + timing=TriggerTiming.BEFORE, + events=[TriggerEvent.INSERT], + function_name="Function With Spaces", + ) + + sql, params = dialect.format_create_trigger_statement(expr) + + assert '"Function With Spaces"' in sql + + +class TestPostgresExtendedStatisticsNameSecurity: + """Tests for extended statistics names using format_identifier.""" + + def test_create_statistics_name_quoted(self, dialect): + """Test CREATE STATISTICS name is properly quoted.""" + from rhosocial.activerecord.backend.impl.postgres.expression.ddl.statistics import ( + PostgresCreateStatisticsExpression, + ) + + expr = PostgresCreateStatisticsExpression( + dialect=dialect, + name="my_stats", + table_name="users", + columns=["email"], + statistics_type="ndistinct", + ) + + sql, params = dialect.format_create_statistics_statement(expr) + + assert '"my_stats"' in sql + + def test_drop_statistics_name_quoted(self, dialect): + """Test DROP STATISTICS name is properly quoted.""" + from rhosocial.activerecord.backend.impl.postgres.expression.ddl.statistics import ( + PostgresDropStatisticsExpression, + ) + + expr = PostgresDropStatisticsExpression( + dialect=dialect, + name="my_stats", + ) + + sql, params = dialect.format_drop_statistics_statement(expr) + + assert '"my_stats"' in sql \ No newline at end of file From 81c6398171d01b1c18d9a147b6c42f925b312f85 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 2 May 2026 23:51:37 +0800 Subject: [PATCH 03/12] refactor(examples): unify PostgreSQL examples to use POSTGRES_* environment variables - Add cli/bash/ directory with per-command bash scripts: - info.sh, query.sh, introspect.sh, status.sh - named_query.sh, named_procedure.sh, named_connection.sh - Update all example files to use POSTGRES_HOST/PORT/DATABASE/USER/PASSWORD instead of PG_* variables for consistency with CLI - Each bash script covers all CLI options with self-contained test data --- .../impl/postgres/examples/cli/bash/info.sh | 42 +++++++++++++++++ .../postgres/examples/cli/bash/introspect.sh | 46 ++++++++++++++++++ .../examples/cli/bash/named_connection.sh | 22 +++++++++ .../examples/cli/bash/named_procedure.sh | 21 +++++++++ .../postgres/examples/cli/bash/named_query.sh | 21 +++++++++ .../impl/postgres/examples/cli/bash/query.sh | 47 +++++++++++++++++++ .../impl/postgres/examples/cli/bash/status.sh | 25 ++++++++++ .../examples/connection/quickstart.py | 10 ++-- .../impl/postgres/examples/ddl/alter_table.py | 10 ++-- .../postgres/examples/ddl/create_index.py | 10 ++-- .../postgres/examples/ddl/create_table.py | 10 ++-- .../impl/postgres/examples/ddl/drop_table.py | 10 ++-- .../examples/ddl/materialized_view.py | 10 ++-- .../impl/postgres/examples/ddl/partition.py | 10 ++-- .../postgres/examples/ddl/unique_index.py | 10 ++-- .../impl/postgres/examples/ddl/view.py | 10 ++-- .../impl/postgres/examples/delete/basic.py | 10 ++-- .../impl/postgres/examples/insert/batch.py | 10 ++-- .../impl/postgres/examples/insert/single.py | 10 ++-- .../impl/postgres/examples/insert/upsert.py | 10 ++-- .../examples/insert/with_returning.py | 10 ++-- .../impl/postgres/examples/query/aggregate.py | 10 ++-- .../postgres/examples/query/array_func.py | 10 ++-- .../impl/postgres/examples/query/basic.py | 10 ++-- .../impl/postgres/examples/query/constants.py | 10 ++-- .../impl/postgres/examples/query/cte.py | 10 ++-- .../impl/postgres/examples/query/distinct.py | 10 ++-- .../impl/postgres/examples/query/explain.py | 10 ++-- .../impl/postgres/examples/query/join.py | 10 ++-- .../postgres/examples/query/pagination.py | 10 ++-- .../impl/postgres/examples/query/predicate.py | 10 ++-- .../impl/postgres/examples/query/subquery.py | 10 ++-- .../postgres/examples/query/text_search.py | 10 ++-- .../impl/postgres/examples/query/union.py | 10 ++-- .../impl/postgres/examples/query/window.py | 10 ++-- .../examples/query/window_functions.py | 10 ++-- .../postgres/examples/transaction/basic.py | 10 ++-- .../examples/transaction/exclusive.py | 10 ++-- .../examples/transaction/for_update.py | 10 ++-- .../examples/types/array_postgres84.py | 10 ++-- .../postgres/examples/types/json_basic.py | 10 ++-- .../impl/postgres/examples/types/uuid.py | 10 ++-- .../impl/postgres/examples/update/basic.py | 10 ++-- 43 files changed, 404 insertions(+), 180 deletions(-) create mode 100755 src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/info.sh create mode 100755 src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/introspect.sh create mode 100755 src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/named_connection.sh create mode 100755 src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/named_procedure.sh create mode 100755 src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/named_query.sh create mode 100755 src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/query.sh create mode 100755 src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/status.sh diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/info.sh b/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/info.sh new file mode 100755 index 00000000..ec5c4c6f --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/info.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# info.sh - PostgreSQL CLI info command example + +set -e + +POSTGRES_HOST="${POSTGRES_HOST:-localhost}" +POSTGRES_PORT="${POSTGRES_PORT:-5432}" +POSTGRES_DATABASE="${POSTGRES_DATABASE:-test}" +POSTGRES_USER="${POSTGRES_USER:-postgres}" +POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-}" + +export POSTGRES_HOST POSTGRES_PORT POSTGRES_DATABASE POSTGRES_USER POSTGRES_PASSWORD + +PYTHON_CMD="python -m rhosocial.activerecord.backend.impl.postgres" + +echo "==========================================" +echo "PostgreSQL CLI - info command examples" +echo "==========================================" + +echo "" +echo "--- Basic info (table output) ---" +$PYTHON_CMD info + +echo "" +echo "--- Verbose info (protocol families) ---" +$PYTHON_CMD info -v + +echo "" +echo "--- Detailed verbose (all details) ---" +$PYTHON_CMD info -vv + +echo "" +echo "--- JSON output ---" +$PYTHON_CMD info -o json + +echo "" +echo "--- Info with specific PostgreSQL version ---" +$PYTHON_CMD info --version 15.0.0 + +echo "" +echo "--- Rich ASCII output ---" +$PYTHON_CMD info --rich-ascii \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/introspect.sh b/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/introspect.sh new file mode 100755 index 00000000..62c2552d --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/introspect.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# introspect.sh - PostgreSQL CLI introspect command example + +set -e + +POSTGRES_HOST="${POSTGRES_HOST:-localhost}" +POSTGRES_PORT="${POSTGRES_PORT:-5432}" +POSTGRES_DATABASE="${POSTGRES_DATABASE:-test}" +POSTGRES_USER="${POSTGRES_USER:-postgres}" +POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-}" + +export POSTGRES_HOST POSTGRES_PORT POSTGRES_DATABASE POSTGRES_USER POSTGRES_PASSWORD + +PYTHON_CMD="python -m rhosocial.activerecord.backend.impl.postgres" + +TEST_TABLE_USERS="cli_test_users" +TEST_TABLE_ORDERS="cli_test_orders" + +cleanup() { + $PYTHON_CMD query "DROP TABLE IF EXISTS $TEST_TABLE_ORDERS CASCADE" 2>/dev/null || true + $PYTHON_CMD query "DROP TABLE IF EXISTS $TEST_TABLE_USERS CASCADE" 2>/dev/null || true +} + +setup() { + $PYTHON_CMD query "CREATE TABLE IF NOT EXISTS $TEST_TABLE_USERS (id SERIAL PRIMARY KEY, name VARCHAR(100))" + $PYTHON_CMD query "INSERT INTO $TEST_TABLE_USERS (name) VALUES ('Alice'), ('Bob')" + $PYTHON_CMD query "CREATE TABLE IF NOT EXISTS $TEST_TABLE_ORDERS (id SERIAL PRIMARY KEY, user_id INTEGER, amount DECIMAL(10,2))" + $PYTHON_CMD query "INSERT INTO $TEST_TABLE_ORDERS (user_id, amount) VALUES (1, 100.00)" +} + +trap cleanup EXIT + +echo "==========================================" +echo "PostgreSQL CLI - introspect command examples" +echo "==========================================" + +setup + +$PYTHON_CMD introspect tables +$PYTHON_CMD introspect views +$PYTHON_CMD introspect table $TEST_TABLE_USERS +$PYTHON_CMD introspect columns $TEST_TABLE_USERS +$PYTHON_CMD introspect indexes $TEST_TABLE_USERS +$PYTHON_CMD introspect foreign-keys $TEST_TABLE_ORDERS +$PYTHON_CMD introspect database +$PYTHON_CMD introspect tables -o json \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/named_connection.sh b/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/named_connection.sh new file mode 100755 index 00000000..439d8fd4 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/named_connection.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# named_connection.sh - PostgreSQL CLI named-connection command example + +set -e + +POSTGRES_HOST="${POSTGRES_HOST:-localhost}" +POSTGRES_PORT="${POSTGRES_PORT:-5432}" +POSTGRES_DATABASE="${POSTGRES_DATABASE:-test}" +POSTGRES_USER="${POSTGRES_USER:-postgres}" +POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-}" + +export POSTGRES_HOST POSTGRES_PORT POSTGRES_DATABASE POSTGRES_USER POSTGRES_PASSWORD + +PYTHON_CMD="python -m rhosocial.activerecord.backend.impl.postgres" + +echo "==========================================" +echo "PostgreSQL CLI - named-connection command examples" +echo "==========================================" + +$PYTHON_CMD named-connection --list rhosocial.activerecord.backend.impl.postgres.examples.named_connections +$PYTHON_CMD named-connection --show rhosocial.activerecord.backend.impl.postgres.examples.named_connections.local_dev +$PYTHON_CMD named-connection --describe rhosocial.activerecord.backend.impl.postgres.examples.named_connections.local_dev \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/named_procedure.sh b/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/named_procedure.sh new file mode 100755 index 00000000..fdc97289 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/named_procedure.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# named_procedure.sh - PostgreSQL CLI named-procedure command example + +set -e + +POSTGRES_HOST="${POSTGRES_HOST:-localhost}" +POSTGRES_PORT="${POSTGRES_PORT:-5432}" +POSTGRES_DATABASE="${POSTGRES_DATABASE:-test}" +POSTGRES_USER="${POSTGRES_USER:-postgres}" +POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-}" + +export POSTGRES_HOST POSTGRES_PORT POSTGRES_DATABASE POSTGRES_USER POSTGRES_PASSWORD + +PYTHON_CMD="python -m rhosocial.activerecord.backend.impl.postgres" + +echo "==========================================" +echo "PostgreSQL CLI - named-procedure command examples" +echo "==========================================" + +$PYTHON_CMD named-procedure --list rhosocial.activerecord.backend.impl.postgres.examples.named_procedures 2>/dev/null || echo "(No named procedures found)" +$PYTHON_CMD named-connection --list rhosocial.activerecord.backend.impl.postgres.examples.named_connections \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/named_query.sh b/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/named_query.sh new file mode 100755 index 00000000..833f1226 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/named_query.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# named_query.sh - PostgreSQL CLI named-query command example + +set -e + +POSTGRES_HOST="${POSTGRES_HOST:-localhost}" +POSTGRES_PORT="${POSTGRES_PORT:-5432}" +POSTGRES_DATABASE="${POSTGRES_DATABASE:-test}" +POSTGRES_USER="${POSTGRES_USER:-postgres}" +POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-}" + +export POSTGRES_HOST POSTGRES_PORT POSTGRES_DATABASE POSTGRES_USER POSTGRES_PASSWORD + +PYTHON_CMD="python -m rhosocial.activerecord.backend.impl.postgres" + +echo "==========================================" +echo "PostgreSQL CLI - named-query command examples" +echo "==========================================" + +$PYTHON_CMD named-query --list rhosocial.activerecord.backend.impl.postgres.examples.named_queries 2>/dev/null || echo "(No named queries found)" +$PYTHON_CMD named-connection --list rhosocial.activerecord.backend.impl.postgres.examples.named_connections \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/query.sh b/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/query.sh new file mode 100755 index 00000000..2e0997ce --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/query.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# query.sh - PostgreSQL CLI query command example + +set -e + +POSTGRES_HOST="${POSTGRES_HOST:-localhost}" +POSTGRES_PORT="${POSTGRES_PORT:-5432}" +POSTGRES_DATABASE="${POSTGRES_DATABASE:-test}" +POSTGRES_USER="${POSTGRES_USER:-postgres}" +POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-}" + +export POSTGRES_HOST POSTGRES_PORT POSTGRES_DATABASE POSTGRES_USER POSTGRES_PASSWORD + +PYTHON_CMD="python -m rhosocial.activerecord.backend.impl.postgres" + +TEST_TABLE_USERS="cli_test_users" +TEST_TABLE_ORDERS="cli_test_orders" + +cleanup() { + $PYTHON_CMD query "DROP TABLE IF EXISTS $TEST_TABLE_ORDERS CASCADE" 2>/dev/null || true + $PYTHON_CMD query "DROP TABLE IF EXISTS $TEST_TABLE_USERS CASCADE" 2>/dev/null || true +} + +setup() { + $PYTHON_CMD query "CREATE TABLE IF NOT EXISTS $TEST_TABLE_USERS (id SERIAL PRIMARY KEY, name VARCHAR(100), status VARCHAR(20) DEFAULT 'active')" + $PYTHON_CMD query "INSERT INTO $TEST_TABLE_USERS (name, status) VALUES ('Alice', 'active'), ('Bob', 'inactive'), ('Charlie', 'active')" + $PYTHON_CMD query "CREATE TABLE IF NOT EXISTS $TEST_TABLE_ORDERS (id SERIAL PRIMARY KEY, user_id INTEGER, amount DECIMAL(10,2))" + $PYTHON_CMD query "INSERT INTO $TEST_TABLE_ORDERS (user_id, amount) VALUES (1, 100.00), (1, 200.00), (2, 150.00)" +} + +trap cleanup EXIT + +echo "==========================================" +echo "PostgreSQL CLI - query command examples" +echo "==========================================" + +setup + +$PYTHON_CMD query "SELECT * FROM $TEST_TABLE_USERS" +$PYTHON_CMD query "SELECT * FROM $TEST_TABLE_USERS WHERE status = 'active'" +$PYTHON_CMD query "SELECT u.name, o.amount FROM $TEST_TABLE_USERS u JOIN $TEST_TABLE_ORDERS o ON u.id = o.user_id" +$PYTHON_CMD query "SELECT user_id, SUM(amount) as total FROM $TEST_TABLE_ORDERS GROUP BY user_id" +$PYTHON_CMD query "SELECT * FROM $TEST_TABLE_USERS" -o json +$PYTHON_CMD query "SELECT * FROM $TEST_TABLE_USERS LIMIT 2" -o csv +echo "SELECT * FROM $TEST_TABLE_USERS WHERE id > 0" > /tmp/test_query.sql +$PYTHON_CMD query -f /tmp/test_query.sql +rm -f /tmp/test_query.sql \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/status.sh b/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/status.sh new file mode 100755 index 00000000..1d83ca37 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/status.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# status.sh - PostgreSQL CLI status command example + +set -e + +POSTGRES_HOST="${POSTGRES_HOST:-localhost}" +POSTGRES_PORT="${POSTGRES_PORT:-5432}" +POSTGRES_DATABASE="${POSTGRES_DATABASE:-test}" +POSTGRES_USER="${POSTGRES_USER:-postgres}" +POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-}" + +export POSTGRES_HOST POSTGRES_PORT POSTGRES_DATABASE POSTGRES_USER POSTGRES_PASSWORD + +PYTHON_CMD="python -m rhosocial.activerecord.backend.impl.postgres" + +echo "==========================================" +echo "PostgreSQL CLI - status command examples" +echo "==========================================" + +$PYTHON_CMD status all +$PYTHON_CMD status config +$PYTHON_CMD status performance +$PYTHON_CMD status storage +$PYTHON_CMD status databases +$PYTHON_CMD status all -o json \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/connection/quickstart.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/connection/quickstart.py index a8dcfbda..ea715be8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/connection/quickstart.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/connection/quickstart.py @@ -36,11 +36,11 @@ # Create connection configuration config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', '5432')), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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', ''), ) # Create backend instance and connect diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/alter_table.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/alter_table.py index 8199c5c4..8971e5c7 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/alter_table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/alter_table.py @@ -26,11 +26,11 @@ from rhosocial.activerecord.backend.schema import StatementType config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', '5432')), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_index.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_index.py index 6ea5c796..9e097b89 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_index.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_index.py @@ -21,11 +21,11 @@ from rhosocial.activerecord.backend.schema import StatementType config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', '5432')), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_table.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_table.py index 24ada37c..0cb8edd7 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_table.py @@ -30,11 +30,11 @@ ) config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', '5432')), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/drop_table.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/drop_table.py index 8874239d..759c709c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/drop_table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/drop_table.py @@ -14,11 +14,11 @@ from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', 5432)), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/materialized_view.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/materialized_view.py index ca3f4438..557e2858 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/materialized_view.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/materialized_view.py @@ -38,11 +38,11 @@ from rhosocial.activerecord.backend.schema import StatementType config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', 5432)), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/partition.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/partition.py index 666e2b56..3a8fc5f4 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/partition.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/partition.py @@ -26,11 +26,11 @@ from rhosocial.activerecord.backend.schema import StatementType config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', '5432')), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/unique_index.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/unique_index.py index 37f19f65..cd371781 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/unique_index.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/unique_index.py @@ -30,11 +30,11 @@ ) config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', '5432')), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/view.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/view.py index 3ee6c89b..29ec005e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/view.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/view.py @@ -18,11 +18,11 @@ from rhosocial.activerecord.backend.schema import StatementType config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', 5432)), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/delete/basic.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/delete/basic.py index 2986187c..c34770f2 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/delete/basic.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/delete/basic.py @@ -32,11 +32,11 @@ from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', '5432')), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/batch.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/batch.py index ea7d4a6e..9ccacaa9 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/batch.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/batch.py @@ -21,11 +21,11 @@ from rhosocial.activerecord.backend.schema import StatementType config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', '5432')), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/single.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/single.py index 7b7e5835..fd3bfeb2 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/single.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/single.py @@ -14,11 +14,11 @@ from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', 5432)), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/upsert.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/upsert.py index 47d979b9..9d1c98f2 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/upsert.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/upsert.py @@ -33,11 +33,11 @@ from rhosocial.activerecord.backend.schema import StatementType config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', 5432)), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/with_returning.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/with_returning.py index 01b16e68..7353b070 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/with_returning.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/with_returning.py @@ -30,11 +30,11 @@ ) config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', '5432')), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/aggregate.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/aggregate.py index 75ca3e50..e62a3234 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/aggregate.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/aggregate.py @@ -24,11 +24,11 @@ from rhosocial.activerecord.backend.schema import StatementType config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', '5432')), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/array_func.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/array_func.py index 6096317d..06431c89 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/array_func.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/array_func.py @@ -24,11 +24,11 @@ from rhosocial.activerecord.backend.schema import StatementType config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', '5432')), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/basic.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/basic.py index b4c880d3..d68b866e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/basic.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/basic.py @@ -24,11 +24,11 @@ from rhosocial.activerecord.backend.schema import StatementType config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', '5432')), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/constants.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/constants.py index dd23957b..691982b1 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/constants.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/constants.py @@ -31,11 +31,11 @@ ) config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', '5432')), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/cte.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/cte.py index 88a47ab8..0790ff44 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/cte.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/cte.py @@ -15,11 +15,11 @@ from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', 5432)), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/distinct.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/distinct.py index 3011dd17..3b666510 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/distinct.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/distinct.py @@ -16,11 +16,11 @@ from rhosocial.activerecord.backend.schema import StatementType config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', 5432)), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/explain.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/explain.py index d501f1ee..e733712d 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/explain.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/explain.py @@ -34,11 +34,11 @@ from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', 5432)), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/join.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/join.py index 1e7e3b84..a8d68e30 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/join.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/join.py @@ -24,11 +24,11 @@ from rhosocial.activerecord.backend.schema import StatementType config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', '5432')), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/pagination.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/pagination.py index 5d5ba67f..6f087cbf 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/pagination.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/pagination.py @@ -33,11 +33,11 @@ from rhosocial.activerecord.backend.schema import StatementType config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', '5432')), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/predicate.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/predicate.py index 2ce6831b..cc2f6f33 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/predicate.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/predicate.py @@ -40,11 +40,11 @@ from rhosocial.activerecord.backend.schema import StatementType config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', '5432')), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/subquery.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/subquery.py index f74043b8..56231777 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/subquery.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/subquery.py @@ -24,11 +24,11 @@ from rhosocial.activerecord.backend.schema import StatementType config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', '5432')), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/text_search.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/text_search.py index 679765f8..a9b76948 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/text_search.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/text_search.py @@ -24,11 +24,11 @@ from rhosocial.activerecord.backend.schema import StatementType config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', '5432')), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/union.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/union.py index 9893646a..d89b00cf 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/union.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/union.py @@ -16,11 +16,11 @@ from rhosocial.activerecord.backend.schema import StatementType config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', 5432)), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window.py index a839692c..5df3faf4 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window.py @@ -24,11 +24,11 @@ from rhosocial.activerecord.backend.schema import StatementType config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', '5432')), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window_functions.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window_functions.py index dcbd3f6a..7a1ab16b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window_functions.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window_functions.py @@ -10,11 +10,11 @@ from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', 5432)), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/basic.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/basic.py index b668ba9c..6eb5c9e3 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/basic.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/basic.py @@ -32,11 +32,11 @@ from rhosocial.activerecord.backend.schema import StatementType config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', '5432')), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/exclusive.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/exclusive.py index 41a8c77f..f5bda6ef 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/exclusive.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/exclusive.py @@ -36,11 +36,11 @@ from rhosocial.activerecord.backend.transaction import IsolationLevel config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', '5432')), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/for_update.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/for_update.py index 6640be06..61f52a9f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/for_update.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/for_update.py @@ -35,11 +35,11 @@ from rhosocial.activerecord.backend.schema import StatementType config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', 5432)), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/types/array_postgres84.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/types/array_postgres84.py index dcc324c3..36ef2ff2 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/types/array_postgres84.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/types/array_postgres84.py @@ -24,11 +24,11 @@ from rhosocial.activerecord.backend.schema import StatementType config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', '5432')), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/types/json_basic.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/types/json_basic.py index 5366baf0..da478356 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/types/json_basic.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/types/json_basic.py @@ -24,11 +24,11 @@ from rhosocial.activerecord.backend.schema import StatementType config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', '5432')), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/types/uuid.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/types/uuid.py index 59d0949b..21c54946 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/types/uuid.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/types/uuid.py @@ -32,11 +32,11 @@ from rhosocial.activerecord.backend.schema import StatementType config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', '5432')), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/update/basic.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/update/basic.py index 6b52c39a..c2b32db9 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/update/basic.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/update/basic.py @@ -17,11 +17,11 @@ from rhosocial.activerecord.backend.schema import StatementType config = PostgresConnectionConfig( - host=os.getenv('PG_HOST', 'localhost'), - port=int(os.getenv('PG_PORT', 5432)), - database=os.getenv('PG_DATABASE', 'test'), - username=os.getenv('PG_USERNAME', 'postgres'), - password=os.getenv('PG_PASSWORD', ''), + 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() From 4caf05965d8283c09dc9ea824c0e8dabfda7dabd Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 2 May 2026 23:53:37 +0800 Subject: [PATCH 04/12] chore: add requires_functions marker, remove requires_capability, update test.yml - pyproject.toml: add requires_functions marker, remove deprecated requires_capability - test.yml: use git+https for testsuite at release/v1.0.0.dev13 --- .github/workflows/test.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 990fa173..a3213e56 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -80,7 +80,7 @@ jobs: - name: Install activerecord and testsuite from PyPI/git run: | pip install git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev26#egg=rhosocial-activerecord - pip install rhosocial-activerecord-testsuite==1.0.0.dev12 + pip install git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev13#egg=rhosocial-activerecord-testsuite - name: Create postgres scenario config run: | diff --git a/pyproject.toml b/pyproject.toml index 75206926..f26a0a17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,8 +83,8 @@ testpaths = [ "tests", ] markers = [ - "requires_capability: marks tests requiring specific backend capabilities", "requires_protocol: marks tests requiring specific backend protocol support", + "requires_functions: marks tests requiring specific database functions", "requires_inner_join: marks tests requiring INNER JOIN support", ] From 53cc7e645a48850a1e06636ab1fc471c413cabb7 Mon Sep 17 00:00:00 2001 From: vistart Date: Wed, 6 May 2026 06:55:06 +0800 Subject: [PATCH 05/12] refactor: add CLAUDE.md and .claude/ documentation --- .claude/architecture.md | 849 +++++++++++++++++++++++++++ .claude/backend_development.md | 577 ++++++++++++++++++ .claude/code_style.md | 140 ++++- .claude/commands/check-sync-async.md | 25 + .claude/commands/gen-migration.md | 148 +++++ .claude/commands/gen-model.md | 73 +++ .claude/commands/gen-query.md | 85 +++ .claude/commands/gen-relation.md | 154 +++++ .claude/commands/lint-fix.md | 9 + .claude/commands/lint.md | 9 + .claude/commands/new-backend.md | 44 ++ .claude/commands/new-feature.md | 33 ++ .claude/commands/test-feature.md | 12 + .claude/commands/test.md | 12 + .claude/commands/type-check.md | 8 + .claude/commands/validate-model.md | 116 ++++ .claude/testing.md | 250 ++------ AGENTS.md | 1 + CLAUDE.md | 36 ++ GEMINI.md | 1 + 20 files changed, 2367 insertions(+), 215 deletions(-) create mode 100644 .claude/architecture.md create mode 100644 .claude/backend_development.md create mode 100644 .claude/commands/check-sync-async.md create mode 100644 .claude/commands/gen-migration.md create mode 100644 .claude/commands/gen-model.md create mode 100644 .claude/commands/gen-query.md create mode 100644 .claude/commands/gen-relation.md create mode 100644 .claude/commands/lint-fix.md create mode 100644 .claude/commands/lint.md create mode 100644 .claude/commands/new-backend.md create mode 100644 .claude/commands/new-feature.md create mode 100644 .claude/commands/test-feature.md create mode 100644 .claude/commands/test.md create mode 100644 .claude/commands/type-check.md create mode 100644 .claude/commands/validate-model.md create mode 120000 AGENTS.md create mode 100644 CLAUDE.md create mode 120000 GEMINI.md diff --git a/.claude/architecture.md b/.claude/architecture.md new file mode 100644 index 00000000..b2ca7488 --- /dev/null +++ b/.claude/architecture.md @@ -0,0 +1,849 @@ +# rhosocial-activerecord Architecture Document + +## Core Architecture Components + +### 1. Expression-Dialect System + +The Expression-Dialect System is the core query building component of rhosocial-activerecord, which achieves separation between database-agnostic SQL query building and database-specific SQL generation. + +#### Architectural Principles +- **Expression classes** implement the `ToSQLProtocol` interface, defining how to generate SQL +- **Each expression class** must call its dialect's `format_*` methods instead of self-formatting +- **Dialect classes** are responsible for the actual SQL formatting and parameter handling +- **Expression classes** should never directly concatenate SQL strings; they should delegate to dialect +- This pattern ensures each dialect can customize formatting behavior while maintaining security + +#### Relationship Model +``` +Expression.to_sql() -> Dialect.format_*() -> SQL string and parameters +``` + +#### Main Modules +- `bases.py`: Abstract base classes and protocol definitions +- `core.py`: Core expression components (columns, literals, function calls, subqueries) +- `mixins.py`: Mixin classes that provide operator-overloading capabilities +- `operators.py`: SQL operations (binary, unary, arithmetic expressions) +- `predicates.py`: SQL predicate expressions (WHERE clause conditions) +- `query_parts.py`: SQL query clauses (WHERE, GROUP BY, HAVING, ORDER BY, etc.) +- `statements.py`: DML/DQL/DDL statements (SELECT, INSERT, UPDATE, DELETE, etc.) +- `functions.py`: Standalone factory functions for creating SQL expression objects +- `aggregates.py`: Expressions related to SQL aggregation and aggregate functions +- `advanced_functions.py`: Advanced SQL functions and expressions (CASE, CAST, EXISTS, window functions, etc.) +- `query_sources.py`: Data source expressions (VALUES, table functions, lateral joins, CTEs, etc.) +- `graph.py`: SQL Graph Query (MATCH) expression building blocks + +#### Important Limitations +The expression system faithfully builds SQL according to user intent, but **does not validate** whether the generated SQL complies with SQL standards or can be successfully executed in the target database. Semantic validation is the responsibility of the database engine. + +### 2. Backend System + +The backend system is responsible for database connection management and actual SQL execution. + +#### Main Components +- `StorageBackend`: Base class for storage backends +- `SQLDialectBase`: Base class for SQL dialects +- Concrete implementations: `sqlite`, `dummy`, etc. + +### 3. Model Layer + +The model layer provides the implementation of the Active Record pattern. + +#### Main Components +- `ActiveRecord`: Base class for the Active Record pattern +- `FieldProxy`: Field proxy that bridges the gap between Python objects and SQL queries +- Mixins: `UUIDMixin`, `TimestampMixin`, etc. + +### 4. Query Interface + +The query interface provides a fluent, type-safe query API. + +#### Main Components +- `ActiveQuery`: Most commonly used query object bound to ActiveRecord models +- `CTEQuery`: Query object for building Common Table Expressions (CTEs) +- `SetOperationQuery`: Set operation query object + +## Design Patterns + +### Sync-Async Parity +Synchronous and asynchronous implementations provide equivalent functionality with unified APIs. + +### Gradual ORM +Balancing strict type safety (OLTP) with raw performance (OLAP). + +### Layered Architecture +Clear distinction between Backend and ActiveRecord, avoiding tight coupling between database connection management and model definition in traditional ORMs. + +## Core Architectural Principles + +### 1. Independence from Existing ORMs +- **No ORM Dependencies**: Built from scratch without relying on SQLAlchemy, Django ORM, or other ORMs +- **Direct Driver Interaction**: Backends interact directly with database drivers +- **Lightweight Core**: Core functionality requires only Pydantic + +### 2. Open-Closed Principle +- **Open for Extension**: New backends can be added without modifying core code +- **Closed for Modification**: Core interfaces remain stable +- **Implementation**: Protocol-based design with abstract base classes + +### 3. Dependency Inversion +- High-level modules (ActiveRecord) don't depend on low-level modules (backends) +- Both depend on abstractions (interfaces) +- Dependency injection through configuration + +### 4. Single Responsibility +- Each module has one clear purpose +- Backends handle database-specific details +- Models handle business logic +- Query builders handle query construction + +### 5. Consistent SQL Identifier and Literal Formatting (Single Point of Call) + +**Purpose**: Ensure all database identifiers (table names, column names, aliases) and literal values are consistently and correctly formatted according to the specific database dialect. This is crucial for conforming to dialect-specific quoting rules (e.g., `"name"`, ``` `name` ```, `[name]`) and ensuring broad compatibility. + +**Security Note**: While proper identifier formatting is essential for SQL correctness, the primary mechanism for preventing SQL injection vulnerabilities in this project is the use of **SQL placeholders and parameter separation**, ensuring that literal values are always bound securely and never concatenated directly into the SQL query string. + +**Implementation**: The `SQLDialectBase` (and its concrete implementations like `SQLiteDialect`) provides dedicated methods for this: +- `format_identifier(self, identifier: str) -> str`: Formats and quotes identifiers. +- `format_string_literal(self, value: str) -> str`: Formats and quotes string literals. + +All query building components (e.g., mixins, `SQLExpression` subclasses) are designed to route identifier and literal formatting through these dialect methods, ensuring a single, centralized point of control for SQL syntax generation. + +```python +# In backend/dialect.py +class SQLDialectBase(ABC): + @abstractmethod + def format_identifier(self, identifier: str) -> str: + """Format identifier (table name, column name).""" + pass + + @abstractmethod + def format_string_literal(self, value: str) -> str: + """Format string literal.""" + pass + +# Usage in query builder (conceptual) +# formatted_column = self.model_class.backend().dialect.format_identifier(column_name) +# formatted_string = self.model_class.backend().dialect.format_string_literal(string_value) +``` + +## Package Architecture + +### Package Structure + +``` +rhosocial-activerecord/ # Core package +├── src/rhosocial/activerecord/ # Main package root +│ ├── __init__.py # Package initialization +│ ├── base/ # Core model functionality +│ │ ├── __init__.py +│ │ ├── base.py # Base ActiveRecord implementation +│ │ ├── column_name_mixin.py # Column name handling mixin +│ │ ├── field_adapter_mixin.py # Field adapter mixin +│ │ ├── field_proxy.py # Field proxy implementation +│ │ ├── fields.py # Field definitions +│ │ ├── metaclass.py # Model metaclass +│ │ └── query_mixin.py # Query functionality mixin +│ ├── field/ # Field mixins and types +│ │ ├── __init__.py +│ │ ├── integer_pk.py # Integer primary key mixin +│ │ ├── README.md +│ │ ├── soft_delete.py # Soft delete mixin +│ │ ├── timestamp.py # Timestamp mixin +│ │ ├── uuid.py # UUID mixin +│ │ └── version.py # Version mixin +│ ├── interface/ # Public API interfaces +│ │ ├── __init__.py +│ │ ├── base.py # Base interfaces +│ │ ├── model.py # Model interface +│ │ ├── query.py # Query interface +│ │ ├── update.py # Update interface +│ ├── query/ # Query building components +│ │ ├── __init__.py +│ │ ├── active_query.py # Main query interface +│ │ ├── aggregate.py # Aggregate query functionality +│ │ ├── async_join.py # Async join functionality +│ │ ├── base.py # Base query functionality +│ │ ├── cte_query.py # CTE query functionality +│ │ ├── join.py # Join query functionality +│ │ ├── range.py # Range query functionality +│ │ ├── relational.py # Relational query functionality +│ │ └── set_operation.py # Set operation query functionality +│ ├── relation/ # Relationship components +│ │ ├── __init__.py +│ │ ├── async_descriptors.py # Async relationship descriptors +│ │ ├── base.py # Base relationship functionality +│ │ ├── cache.py # Relationship caching +│ │ ├── descriptors.py # Relationship descriptors +│ │ └── interfaces.py # Relationship interfaces +│ ├── model.py # Main ActiveRecord class +│ └── backend/ # Backend abstraction and implementations +│ ├── __init__.py +│ ├── base/ # Backend base classes +│ │ ├── __init__.py +│ │ └── base.py # StorageBackend base class +│ ├── dialect/ # SQL dialect implementations +│ │ ├── __init__.py +│ │ └── base.py # SQLDialect base class +│ ├── expression/ # Expression system +│ │ ├── __init__.py # Expression system entry point +│ │ ├── advanced_functions.py # Advanced SQL functions (CASE, CAST, etc.) +│ │ ├── aggregates.py # Aggregate function expressions +│ │ ├── bases.py # Base expression classes and protocols +│ │ ├── core.py # Core expressions (Column, Literal, etc.) +│ │ ├── functions.py # Factory functions for expressions +│ │ ├── graph.py # Graph query expressions +│ │ ├── literals.py # Literal expressions +│ │ ├── mixins.py # Expression mixins with operator overloading +│ │ ├── operators.py # SQL operation expressions +│ │ ├── predicates.py # Predicate expressions +│ │ ├── query_parts.py # Query clause expressions +│ │ ├── query_sources.py # Query source expressions +│ │ └── statements.py # SQL statement expressions +│ ├── impl/ # Backend implementations +│ │ ├── dummy/ # Dummy backend for testing +│ │ │ ├── __init__.py +│ │ │ ├── backend.py +│ │ │ └── dialect.py +│ │ └── sqlite/ # SQLite backend implementation +│ │ ├── __init__.py +│ │ ├── __main__.py +│ │ ├── adapters.py # SQLite type adapters +│ │ ├── backend.py # SQLite backend implementation +│ │ ├── config.py # SQLite configuration +│ │ ├── dialect.py # SQLite dialect implementation +│ │ ├── transaction.py # SQLite transaction handling +│ │ └── types.py # SQLite-specific types +│ ├── config.py # Backend configuration base classes +│ ├── errors.py # Backend-specific errors +│ ├── helpers.py # Backend helper functions +│ ├── options.py # Backend options +│ ├── output_abc.py # Output abstraction +│ ├── output_rich.py # Rich output implementation +│ ├── output.py # Output utilities +│ ├── README.md # Backend documentation +│ ├── result.py # Query result handling +│ ├── schema.py # Schema management +│ ├── transaction.py # Transaction base classes +│ ├── type_adapter.py # Type adaptation system +│ └── type_registry.py # Type adapter registry +``` + +### Namespace Package Structure + +The package follows a namespace structure that allows for distributed backend implementations: + +```python +# Core package __init__.py +__path__ = __import__('pkgutil').extend_path(__path__, __name__) +``` + +This allows multiple packages to contribute to the same namespace, enabling distributed backend implementations. + +## Layer Architecture + +### 1. Interface Layer + +**Location**: `interface/` + +**Purpose**: Define contracts for all components + +```python +# interface/model.py +class IActiveRecord(BaseModel, ABC): + """Core ActiveRecord interface.""" + + @abstractmethod + def save(self) -> bool: + """Save record to database.""" + pass + + @abstractmethod + def delete(self) -> bool: + """Delete record from database.""" + pass +``` + +### 2. Model Layer + +**Location**: `base/`, main `ActiveRecord` class + +**Purpose**: Business logic and data management + +```python +# Composition through mixins +class ActiveRecord( + RelationManagementMixin, # Relationship handling + QueryMixin, # Query capabilities + FieldAdapterMixin, # Field-specific type adaptation + MetaclassMixin, # Metaclass-based feature handling + BaseActiveRecord # Core CRUD +): + pass +``` + +### 3. Backend Layer + +**Location**: `backend/` + +**Purpose**: Database abstraction and operations + +```python +# backend/base.py +class StorageBackend( + # ...composed from LoggingMixin, CapabilityMixin, TypeAdaptionMixin, SQLBuildingMixin, etc. + ABC +): + """ + Abstract storage backend, primarily composed from various functional mixins. + Its `execute` method acts as a template method orchestrating these mixins. + """ + + # The actual execute method is a template method, coordinating mixins + def execute(self, sql: str, params: Dict, **kwargs) -> QueryResult: + # ... implementation coordinates mixins for connection, parsing, + # returning clause, cursor, SQL building, execution, result processing ... + pass +``` + +### 4. Implementation Layer + +**Location**: `backend/impl/` + +**Purpose**: Concrete database implementations + +```python +# backend/impl/sqlite/backend.py +class SQLiteBackend(StorageBackend): + """SQLite-specific implementation.""" + + def execute(self, sql: str, params: Dict) -> QueryResult: + # SQLite-specific execution + pass +``` + +## Design Patterns + +### 1. Active Record Pattern + +**Implementation**: Core pattern of the library + +```python +class User(ActiveRecord): + __table_name__ = "users" + + name: str + email: str + +# Usage +user = User(name="John", email="john@example.com") +user.save() # Persists to database +``` + +**Benefits**: +- Intuitive API +- Encapsulated persistence logic +- Domain model and data access combined + +### 2. Protocol Pattern + +**Purpose**: Define interfaces without inheritance + +```python +from typing import Protocol, runtime_checkable + +# Example: SQLTypeAdapter protocol +from typing import Protocol, runtime_checkable, Any, Type, Dict, Optional + +@runtime_checkable +class SQLTypeAdapter(Protocol): + """Protocol for type conversion between Python and database values.""" + + def to_database(self, value: Any, target_type: Type, options: Optional[Dict[str, Any]] = None) -> Any: ... + def from_database(self, value: Any, target_type: Type, options: Optional[Dict[str, Any]] = None) -> Any: ... + + @property + def supported_types(self) -> Dict[Type, Set[Type]]: ... +``` + +**Benefits**: +- Duck typing with type safety +- No forced inheritance hierarchy +- Runtime type checking available + +### 3. Mixin Pattern + +**Purpose**: Composable functionality + +```python +class TimestampMixin: + """Add timestamp tracking.""" + created_at: datetime = Field(default_factory=datetime.now) + updated_at: Optional[datetime] = None + +class SoftDeleteMixin: + """Add soft delete capability.""" + deleted_at: Optional[datetime] = None + + def delete(self): + self.deleted_at = datetime.now() + return self.save() + +# Composition +class Article(TimestampMixin, SoftDeleteMixin, ActiveRecord): + __table_name__ = "articles" + title: str + content: str + +# Mixins are also extensively used for backend composition (e.g., StorageBackend from backend/base.py) +# providing modular and reusable components for database interaction. +``` + +### 4. Builder Pattern + +**Implementation**: Query construction + +```python +# The project's query builder is ActiveQuery, composed from specialized mixins. +from rhosocial.activerecord.query import ActiveQuery, BaseQueryMixin, AggregateQueryMixin, CTEQueryMixin, JoinQueryMixin, RangeQueryMixin, RelationalQueryMixin + +class ActiveQuery( + CTEQueryMixin, + JoinQueryMixin, + RelationalQueryMixin, + RangeQueryMixin, + # BaseQueryMixin and AggregateQueryMixin are inherited through CTEQueryMixin +): + """ + Complete ActiveQuery implementation, combining all query mixins. + It builds SQL by collecting conditions, orders, joins, etc., + and delegates to its mixins for specific functionalities. + """ + pass + +# Usage +users = User.query().where("age >= ?", (18,)).order_by("-created_at").limit(10).all() +``` + +### 5. Registry Pattern + +**Purpose**: Manage type converters and backends + +```python +# The project uses backend.type_registry.TypeRegistry for SQLTypeAdapter instances +from typing import Dict, Tuple, Type, Optional +from rhosocial.activerecord.backend.type_adapter import SQLTypeAdapter + +class TypeRegistry: + """Central registry for SQLTypeAdapter instances.""" + def __init__(self): + self._adapters: Dict[Tuple[Type, Type], SQLTypeAdapter] = {} + + def register(self, adapter: SQLTypeAdapter, py_type: Type, db_type: Type) -> None: + self._adapters[(py_type, db_type)] = adapter + + def get_adapter(self, py_type: Type, db_type: Type) -> Optional[SQLTypeAdapter]: + return self._adapters.get((py_type, db_type)) +``` + +### 6. Template Method Pattern + +**Purpose**: Define algorithm skeleton in base class + +```python +# The execute method in StorageBackend acts as a Template Method +class StorageBackend( + # ...composed from various functional mixins like SQLBuildingMixin, + # QueryAnalysisMixin, ReturningClauseMixin, ResultProcessingMixin, etc. + ABC +): + """ + Abstract storage backend. Its `execute` method is a Template Method + that orchestrates the query execution process by coordinating its + composed functional mixins. + """ + + # This is the actual Template Method + def execute(self, sql: str, params: Optional[Tuple] = None, **kwargs) -> QueryResult: + # 1. Start timer + # 2. Log SQL and parameters + # 3. Handle connection setup (if not already connected) + # 4. Parse statement type (via QueryAnalysisMixin) + # 5. Process RETURNING clause (via ReturningClauseMixin) + # 6. Get cursor + # 7. Prepare SQL and parameters (via SQLBuildingMixin) + # 8. Execute query (hook method `_execute_query` in concrete backend) + # 9. Process results (via TypeAdaptionMixin, ResultProcessingMixin) + # 10. Log completion + # 11. Build QueryResult + # 12. Handle auto-commit (hook method `_handle_auto_commit_if_needed`) + # 13. Handle errors (hook method `_handle_execution_error`) + pass + + # Hook methods (some are abstract in StorageBackend, others in mixins) + @abstractmethod + def _execute_query(self, cursor, sql: str, params: Optional[Tuple]): ... + # ... other abstract hook methods like connect(), disconnect(), etc. +``` + +### 7. Factory Pattern + +**Purpose**: Create backend instances + +```python +def create_backend(backend_type: str, **config) -> StorageBackend: + """Factory for backend creation.""" + backends = { + 'sqlite': SQLiteBackend, + 'mysql': MySQLBackend, + 'postgresql': PostgreSQLBackend, + } + + backend_class = backends.get(backend_type) + if not backend_class: + raise ValueError(f"Unknown backend: {backend_type}") + + return backend_class(**config) +``` + +## Class Hierarchy + +### Model Hierarchy + +``` +BaseModel (Pydantic) + └── IActiveRecord (Interface - implemented by BaseActiveRecord) + └── BaseActiveRecord (Core implementation, implements IActiveRecord) + # ActiveRecord is composed from these mixins and BaseActiveRecord + └── QueryMixin + └── RelationManagementMixin + └── FieldAdapterMixin + └── MetaclassMixin + └── ActiveRecord (Full implementation, combines BaseActiveRecord and mixins) + └── User, Post, etc. (User models) +``` + +### Backend Hierarchy + +``` +StorageBackendBase (ABC) + # Composed from mixins for sync operations + └── StorageBackend (ABC) + └── SQLiteBackend # Concrete implementation + # └── MySQLBackend (if implemented) + # └── PostgreSQLBackend (if implemented) + +StorageBackendBase (ABC) + # Composed from mixins for async operations + └── AsyncStorageBackend (ABC) + └── # AsyncSQLiteBackend (if implemented) + # └── AsyncMySQLBackend (if implemented) +``` + +## Module Interactions + +### Configuration Flow + +```python +# 1. User configures model +from rhosocial.activerecord.model import ActiveRecord +from rhosocial.activerecord.backend.impl.sqlite import SQLiteBackend + +class User(ActiveRecord): + __table_name__ = "users" + name: str + +# 2. Configure backend +config = SQLiteConnectionConfig(database="app.db") +User.configure(config, SQLiteBackend) + +# 3. Backend initialized and attached +# User.__backend__ = SQLiteBackend(config) +``` + +### Query Execution Flow + +``` +User.where("name = ?", ("John",)) + ↓ +QueryBuilder.where() + ↓ +QueryBuilder.build() + ↓ +Backend.execute() + ↓ +Database + ↓ +Backend.fetch_results() + ↓ +Model instantiation + ↓ +User instances +``` + +## Dependency Management + +### Core Dependencies + +The project maintains **minimal core dependencies** by design: + +```python +# Minimal core dependencies - Pydantic only +dependencies = [ + "pydantic>=2.0.0", # Data validation and model definition + "typing_extensions>=4.0.0", # Backported typing features for Python 3.8 +] +``` + +**Important**: This is a **standalone ActiveRecord implementation** with no dependencies on existing ORMs like SQLAlchemy, Django ORM, or others. All database interaction logic is implemented from scratch. + +### Optional Dependencies + +```python +extras_require = { + "mysql": ["mysql-connector-python>=8.0.0"], # MySQL backend + "postgresql": ["psycopg2>=2.9.0"], # PostgreSQL backend + "dev": ["pytest", "black", "mypy"], # Development tools +} +``` + +### Backend Discovery + +```python +# Dynamic backend loading - no ORM dependencies +def discover_backends(): + """Discover installed backends.""" + backends = {} + + # Check for installed backends (each uses only native drivers) + try: + from rhosocial.activerecord.backend.impl.mysql import MySQLBackend + backends['mysql'] = MySQLBackend # Uses mysql-connector-python directly + except ImportError: + pass + + try: + from rhosocial.activerecord.backend.impl.postgresql import PostgreSQLBackend + backends['postgresql'] = PostgreSQLBackend # Uses psycopg2 directly + except ImportError: + pass + + return backends +``` + +## Extension Points + +### 1. Custom Fields + +```python +class EncryptedField(Field): + """Custom encrypted field type.""" + + def __set__(self, instance, value): + encrypted = encrypt(value) + super().__set__(instance, encrypted) + + def __get__(self, instance, owner): + value = super().__get__(instance, owner) + return decrypt(value) if value else None +``` + +### 2. Custom Validators + +```python +from pydantic import field_validator + +class User(ActiveRecord): + email: str + + @field_validator('email') + def validate_email(cls, v): + if '@' not in v: + raise ValueError('Invalid email') + return v.lower() +``` + +### 3. Custom Query Methods + +```python +class UserQueryMixin: + @classmethod + def find_by_email(cls, email: str): + return cls.where("email = ?", (email,)).first() + + @classmethod + def active_users(cls): + return cls.where(is_active=True) + +class User(UserQueryMixin, ActiveRecord): + pass +``` + +### 4. Event Hooks + +```python +class User(ActiveRecord): + def before_save(self): + """Called before saving.""" + self.updated_at = datetime.now() + + def after_save(self): + """Called after saving.""" + cache.invalidate(f"user:{self.id}") +``` + +## Performance Optimization + +### 0. Lightweight Foundation + +The architecture is designed for minimal overhead: +- **No ORM layers**: Direct database driver communication +- **Single dependency**: Only Pydantic required for core functionality +- **Fast startup**: Minimal imports and initialization +- **Low memory footprint**: No heavy framework baggage + +### 1. Connection Pooling + +```python +class PooledBackend(StorageBackend): + def __init__(self, config, pool_size=10): + self.pool = ConnectionPool(config, size=pool_size) + + def get_connection(self): + return self.pool.acquire() +``` + +### 2. Relation Caching (Instance-level) + +```python +from rhosocial.activerecord.relation.cache import InstanceCache, CacheConfig + +# In RelationDescriptor (used by mixins like RelationalQueryMixin) +class RelationDescriptor: + # ... + def _load_relation(self, instance: Any) -> Optional[T]: + """ + Load relation with instance-level caching support. + """ + cached = InstanceCache.get(instance, self.name, self._cache_config) + if cached is not None: + return cached + + data = self._loader.load(instance) + InstanceCache.set(instance, self.name, data, self._cache_config) + return data + +# Cache configuration can be global or per-relation +global_config = GlobalCacheConfig() +global_config.set_config(enabled=True, ttl=300, max_size=1000) +``` + +### 3. Lazy Loading + +```python +class RelationDescriptor: + def __get__(self, instance, owner): + if not hasattr(instance, '_relation_cache'): + instance._relation_cache = {} + + if self.name not in instance._relation_cache: + # Load relation only when accessed + instance._relation_cache[self.name] = self.load(instance) + + return instance._relation_cache[self.name] +``` + +## Thread Safety + +### Key Thread Safety Mechanisms + +# The project utilizes specific mechanisms for thread safety: +# 1. ThreadSafeDict for mutable shared state (e.g., query eager loads, _eager_loads in RelationalQueryMixin) +# 2. InstanceCache for per-instance caching (thread-safe for each instance's data) +# 3. Backend connection management (e.g., SQLiteBackend uses sqlite3.connect which can be thread-safe for basic ops, +# or relies on external connection pooling for more robust solutions in other DBs). + +# Example: ThreadSafeDict (from interface/query.py) +from typing import Dict, Any, TypeVar +from threading import local + +K = TypeVar('K') +V = TypeVar('V') + +class ThreadSafeDict(Dict[K, V]): + """A thread-safe dictionary that behaves exactly like a normal dict.""" + def __init__(self, *args, **kwargs): + self._local = local() + if not hasattr(self._local, 'data'): + self._local.data = {} + if args or kwargs: + self.update(*args, **kwargs) + + def __getitem__(self, key: K) -> V: + return self._local.data[key] + +# InstanceCache for relation caching (from relation/cache.py) +# This design ensures thread-safety by storing cache data on the instance itself +# or using thread-local storage where appropriate, preventing race conditions +# on shared cache structures. + +## Error Handling Strategy + +### Exception Hierarchy + +```python +class ActiveRecordError(Exception): + """Base exception for all ActiveRecord errors.""" + +class DatabaseError(ActiveRecordError): + """Database operation errors.""" + +class ValidationError(ActiveRecordError): + """Data validation errors.""" + +class RecordNotFound(DatabaseError): + """Record not found in database.""" +``` + +### Error Propagation + +``` +User Input + ↓ +Validation (ValidationError) + ↓ +Model Operations + ↓ +Backend Operations (DatabaseError) + ↓ +Database Driver (Driver-specific errors) +``` + +## Testing Architecture + +### Test Organization + +``` +tests/ +├── unit/ # Unit tests for individual components +├── integration/ # Integration tests with real databases +├── fixtures/ # Shared test fixtures +└── benchmarks/ # Performance benchmarks +``` + +### Fixture Pattern + +```python +@pytest.fixture +def backend_matrix(): + """Test across multiple backends.""" + backends = [] + + # Always test SQLite + backends.append(SQLiteBackend) + + # Test others if available + if mysql_available(): + backends.append(MySQLBackend) + + return backends +``` \ No newline at end of file diff --git a/.claude/backend_development.md b/.claude/backend_development.md new file mode 100644 index 00000000..aa452352 --- /dev/null +++ b/.claude/backend_development.md @@ -0,0 +1,577 @@ +# A Complete Guide to Backend Development + +## Overview + +This guide provides comprehensive instructions for implementing new database backends for the rhosocial-activerecord ecosystem. A robust backend is more than just a query executor; it's a complete implementation that includes a database-specific **SQL Dialect**, a precise **Type Adaptation** system, reliable **Transaction Management**, robust **Error Handling**, clear **Feature Detection**, and **Performance Optimizations**. + +This document covers these essential pillars, outlining the required architecture, patterns, and best practices for creating a fully-featured and reliable backend. + +**Important Design Constraint**: Backend implementations must be built using **native database drivers only** (e.g., `mysql-connector-python`, `psycopg`). Do not use or depend on other ORMs like SQLAlchemy or Django ORM. This ensures the ecosystem remains lightweight and independent. + +## Reference Implementations + +For practical examples of fully-featured, production-ready backend implementations, developers can refer to the following projects: + +* **`rhosocial-activerecord-mysql`**: A mature MySQL backend implementation. +* **`rhosocial-activerecord-postgres`**: A robust PostgreSQL backend implementation. + +These repositories provide concrete examples of how to implement all the components discussed in this guide, including their dedicated test suites. Studying their codebases, especially their `backend.py`, `adapters.py`, and `tests/` directories, is highly recommended as a practical reference. + +## Backend Architecture + +### Package Structure + +The package structure is standardized to support namespace packages and maintain consistency. + +``` +rhosocial-activerecord-{backend}/ +├── src/ +│ └── rhosocial/ +│ └── activerecord/ +│ └── backend/ +│ └── impl/ +│ └── {backend}/ +│ ├── __init__.py +│ ├── backend.py # Main backend implementation +│ ├── adapters.py # Backend-specific type adapters +│ ├── config.py # Connection configuration +│ ├── dialect.py # SQL dialect handling +│ ├── transaction.py # Transaction management +│ ├── expression/ # Backend-specific expressions (directory) +│ │ └── __init__.py +│ ├── functions/ # Backend-specific SQL functions (directory) +│ │ └── __init__.py +│ └── features.py # Optional: Feature detection +``` + +#### Expression Directory + +All backend-specific expressions **must** be placed in the `expression/` directory (not `expressions.py`). This ensures consistent structure across all backends. + +```python +# expression/__init__.py +from rhosocial.activerecord.backend.expression.bases import BaseExpression +from rhosocial.activerecord.backend.dialect import SQLDialectBase + +class MyBackendReindexExpression(BaseExpression): + """MyBackend-specific REINDEX expression.""" + pass +``` + +**Important**: Use absolute imports for expressions from core or other backends to avoid deep relative import chains: + +```python +# Correct - absolute imports +from rhosocial.activerecord.backend.expression.bases import BaseExpression +from rhosocial.activerecord.backend.dialect import SQLDialectBase + +# Avoid - deep relative imports +from .....expression.bases import BaseExpression # Not recommended +``` + +## Key Backend Concepts + +Before diving into components, it is crucial to understand the **Expression-Dialect System**, which is fundamental to how SQL queries are built and executed: + +### The Expression-Dialect Architecture + +The expression-dialect system separates SQL query construction from SQL generation, enabling database-agnostic query building while allowing database-specific formatting. + +### Advantages of Our Approach + +Our expression-dialect system offers several advantages over traditional approaches: + +- **No Complex State Management**: Unlike systems that maintain complex object states, our expressions are stateless and pure +- **Direct SQL Generation**: Only 2 steps from expression to SQL, avoiding multi-layer compilation architectures +- **Standard-Based Implementation**: Using the dummy backend as a complete SQL standard reference, other dialects only need to override differences +- **Test-Friendly**: The dummy backend allows testing SQL generation without database connections +- **No Hidden Compilation Steps**: Unlike systems with complex multi-stage compilation, our approach is direct and predictable +- **Fragment Generation**: Any expression can generate SQL fragments independently, unlike systems that require complete query compilation +- **Explicit Control**: Unlike systems with automatic session management or hidden behaviors, our approach gives users complete visibility into when database operations occur +- **Simple Architecture**: No complex object lifecycle tracking, automatic caching mechanisms, or multiple state transitions + +#### Architecture Principles +- **Expression classes** implement the `ToSQLProtocol` and define how to generate SQL +- **Each expression class** must call its dialect's `format_*` methods instead of self-formatting +- **Dialect classes** are responsible for the actual SQL formatting and parameter handling +- **Expression classes** should never directly concatenate SQL strings; they should delegate to dialect +- This pattern ensures each dialect can customize formatting behavior while maintaining security + +#### Relationship Model +``` +Expression.to_sql() -> Dialect.format_*() -> SQL string and parameters +``` + +#### Key Components +1. **Expression Formatting**: Expression classes (subclasses of `BaseExpression`) build query structure and call dialect methods for formatting. The **expression system** handles query construction by calling dialect formatting methods. + +2. **Type Adaptation**: This applies to plain Python values passed as parameters (e.g., a `datetime` object in a `WHERE` clause). The **backend is responsible** for converting these values into a format the native database driver can consume. For instance, converting a Python `datetime` object into a Python `str`. This is handled by the **SQLTypeAdapter Pattern**. + +#### Expression System Modules +- `bases.py`: Abstract base classes and protocol definitions +- `core.py`: Core expression components (columns, literals, function calls, subqueries) +- `mixins.py`: Operator-overloading capabilities for expressions +- `operators.py`: SQL operations (binary, unary, arithmetic expressions) +- `predicates.py`: SQL predicate expressions (WHERE clause conditions) +- `query_parts.py`: SQL query clauses (WHERE, GROUP BY, HAVING, ORDER BY, etc.) +- `statements.py`: DML/DQL/DDL statements (SELECT, INSERT, UPDATE, DELETE, etc.) +- `functions.py`: Standalone factory functions for creating SQL expressions +- `aggregates.py`: SQL aggregation expressions and functions +- `advanced_functions.py`: Advanced SQL functions (CASE, CAST, EXISTS, window functions) +- `query_sources.py`: Data source expressions (VALUES, table functions, CTEs) +- `graph.py`: SQL Graph Query (MATCH) expressions + +#### Important Limitation +The expression system faithfully builds SQL according to user intent, but **does not validate** whether the generated SQL complies with SQL standards or can be successfully executed in the target database. Semantic validation is the responsibility of the database engine. + +#### Design Philosophy: Explicit Over Implicit +Our expression system follows the principle of explicit control over implicit behavior: +- No hidden state management or object lifecycle tracking +- No automatic query compilation or caching mechanisms +- No complex object state transitions +- Users have complete visibility and control over SQL generation +- Unlike systems with complex multi-stage compilation, our approach is direct and predictable + +## A Note on Asynchronous Backends + +The architecture supports parallel synchronous and asynchronous backends. If you choose to provide an asynchronous version of your backend, it must adhere to one fundamental principle: + +- **Functional Equivalence**: The `AsyncStorageBackend` must provide the same features and functionality as its synchronous `StorageBackend` counterpart. This is achieved by implementing asynchronous versions of all I/O-bound methods (e.g., `async def connect(...)`, `async def execute(...)`) and using async-compatible mixins where appropriate. The goal is to ensure a seamless and predictable developer experience, regardless of whether they are using the sync or async version of your backend. + +## The Core Components + +A backend is composed of several key components that work together. The following sections detail each required piece. + +### 1. The `StorageBackend` Interface + +Every backend must implement the `StorageBackend` abstract base class. This is the main entry point for the core library. It is a large interface, and a concrete backend must implement all its abstract methods. + +```python +# backend.py +from typing import Any, Dict, List, Optional, Tuple, Type +from ...base import StorageBackend, ConnectionConfig +from ...type_adapter import SQLTypeAdapter + +class MyBackend(StorageBackend): + """Implementation of MyDatabase backend using native driver.""" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._connection = None + self._transaction_manager = None # Must be initialized in connect() + self._register_my_adapters() # NEW: Call to register adapters + + # --- Methods to be Implemented by Concrete Backend --- + + def connect(self) -> None: + """Establish database connection.""" + pass + + def disconnect(self) -> None: + """Close database connection.""" + pass + + def ping(self, reconnect: bool = True) -> bool: + """Check if connection is valid.""" + pass + + def execute(self, sql: str, params: Optional[Tuple] = None, returning: Optional[Union[bool, List[str], ReturningOptions]] = None, column_adapters: Optional[Dict[str, Tuple[SQLTypeAdapter, Type]]] = None) -> QueryResult: + """Executes a SQL query.""" + pass + + def get_server_version(self) -> tuple: + """Get database server version as (major, minor, patch).""" + pass + + def introspect_and_adapt(self) -> None: + """Introspect backend and adapt to actual database server capabilities. + + This method is called during model configuration to ensure the backend + adapts to the actual database server version and capabilities. It should: + 1. Connect to the database (if not already connected) + 2. Query the actual server version + 3. Re-initialize dialect and type adapters based on actual version + + Backends that don't need version-specific adaptation (e.g., SQLite, Dummy) + can implement this as a no-op. + """ + pass + + def _initialize_capabilities(self) -> DatabaseCapabilities: + """Declare all features supported by this backend.""" + pass + + def _handle_error(self, error: Exception) -> None: + """Catch driver-specific exceptions and raise standard errors.""" + pass + + @property + def transaction_manager(self) -> 'TransactionManager': + """Return an instance of the backend's transaction manager.""" + pass + + @property + def dialect(self) -> 'SQLDialectBase': + """Return an instance of the backend's SQL dialect.""" + pass + + # --- Type Adaptation System Hooks --- + + def get_default_adapter_suggestions(self) -> Dict[Type, Tuple[SQLTypeAdapter, Type]]: + """ + [Backend Implementation] Provides this backend's preferred + type conversion strategies to the core library. + """ + # (Full implementation shown in the Type Adaptation section) + pass + + def _register_my_adapters(self): + """ + A private helper to instantiate and register all necessary type adapters. + """ + # (Full implementation shown in the Type Adaptation section) + pass +``` + +### 2. Connection Configuration + +Define an immutable configuration class for your backend. + +```python +# config.py +from dataclasses import dataclass, field +from typing import Optional, Dict, Any +from rhosocial.activerecord.backend.config import BaseConfig + +@dataclass(frozen=True) +class MyDatabaseConfig(BaseConfig): + host: str = "localhost" + port: int = 5432 + database: str + user: Optional[str] = None + password: Optional[str] = None + # ... other options like charset, ssl_mode, etc. +``` + +### 3. SQL Dialect + +The SQL Dialect is the backend's "translator" for database-specific syntax. This is where you define how your backend speaks SQL. + +#### Protocol and Mixin Architecture + +The dialect system uses a **Protocol-Mixin** architecture: + +- **Protocols** (`protocols.py`): Define the interface contract - what methods a dialect must implement +- **Mixins** (`mixins.py`): Provide default SQL standard implementations that dialects can override + +Every dialect inherits from: +1. `SQLDialectBase` - Core dialect functionality +2. Relevant Mixins - Default implementations for supported features +3. Relevant Protocols - Interface contracts for type checking + +##### Current Protocols and Mixins (Main Package) + +| Protocol | Mixin | Description | +|----------|-------|-------------| +| `WindowFunctionSupport` | `WindowFunctionMixin` | Window functions (OVER, PARTITION BY) | +| `CTESupport` | `CTEMixin` | Common Table Expressions (WITH clause) | +| `AdvancedGroupingSupport` | `AdvancedGroupingMixin` | ROLLUP, CUBE, GROUPING SETS | +| `ReturningSupport` | `ReturningMixin` | RETURNING clause | +| `UpsertSupport` | `UpsertMixin` | UPSERT operations (ON CONFLICT) | +| `LateralJoinSupport` | `LateralJoinMixin` | LATERAL joins | +| `ArraySupport` | `ArrayMixin` | Array types and operations | +| `JSONSupport` | `JSONMixin` | JSON types and operations | +| `ExplainSupport` | `ExplainMixin` | EXPLAIN statement | +| `FilterClauseSupport` | `FilterClauseMixin` | FILTER clause for aggregates | +| `OrderedSetAggregationSupport` | `OrderedSetAggregationMixin` | WITHIN GROUP (ORDER BY) | +| `MergeSupport` | `MergeMixin` | MERGE statement | +| `TemporalTableSupport` | `TemporalTableMixin` | FOR SYSTEM_TIME queries | +| `QualifyClauseSupport` | `QualifyClauseMixin` | QUALIFY clause | +| `LockingSupport` | `LockingMixin` | FOR UPDATE, SKIP LOCKED | +| `GraphSupport` | `GraphMixin` | Graph queries (MATCH) | +| `JoinSupport` | `JoinMixin` | JOIN operations | +| `SetOperationSupport` | `SetOperationMixin` | UNION, INTERSECT, EXCEPT | +| `ILIKESupport` | `ILIKEMixin` | Case-insensitive LIKE | +| `TableSupport` | `TableMixin` | CREATE/DROP/ALTER TABLE | +| `ViewSupport` | `ViewMixin` | CREATE/DROP VIEW | +| `TruncateSupport` | `TruncateMixin` | TRUNCATE TABLE | +| `SchemaSupport` | `SchemaMixin` | CREATE/DROP SCHEMA | +| `IndexSupport` | `IndexMixin` | CREATE/DROP INDEX | +| `SequenceSupport` | `SequenceMixin` | CREATE/DROP/ALTER SEQUENCE | +| `TriggerSupport` | `TriggerMixin` | CREATE/DROP TRIGGER (SQL:1999) | +| `FunctionSupport` | `FunctionMixin` | CREATE/DROP FUNCTION (SQL/PSM) | + +##### Principles for Adding New Protocols/Mixins + +**When to add to Main Package:** +1. Feature is defined in **SQL standard** (SQL:1999, SQL:2003, SQL:2008, SQL:2011, SQL:2016) +2. Feature is widely supported across multiple database systems +3. Feature is a **DDL (Data Definition Language)** or **DML (Data Manipulation Language)** construct + +**When to add to Dialect-Specific Extension:** +1. Feature is **dialect-specific** (not in SQL standard) +2. Feature is implemented differently across databases +3. Feature is proprietary to a specific database vendor + +**Example Analysis:** + +| Feature | SQL Standard? | Location | +|---------|---------------|----------| +| `CREATE TRIGGER` | Yes (SQL:1999) | Main Package (`TriggerSupport`) | +| `CREATE FUNCTION` | Yes (SQL/PSM) | Main Package (`FunctionSupport`) | +| `COMMENT ON` | No (PostgreSQL/Oracle) | PostgreSQL Extension | +| `CREATE TYPE ... AS ENUM` | No (PostgreSQL-specific) | PostgreSQL Extension | +| `AUTO_INCREMENT` | No (MySQL-specific) | MySQL Extension | +| `BIGSERIAL` | No (PostgreSQL-specific) | PostgreSQL Extension | + +##### Implementation Steps for New Protocols + +1. **Add to Main Package** (if SQL standard): + ```python + # protocols.py + @runtime_checkable + class NewFeatureSupport(Protocol): + def supports_new_feature(self) -> bool: ... + def format_new_feature_statement(self, expr) -> Tuple[str, tuple]: ... + + # mixins.py + class NewFeatureMixin: + def supports_new_feature(self) -> bool: return False + def format_new_feature_statement(self, expr) -> Tuple[str, tuple]: + # SQL standard implementation + ... + ``` + +2. **Add to Dialect Extension** (if dialect-specific): + ```python + # postgres/dialect.py + def supports_dialect_feature(self) -> bool: return True + def format_dialect_statement(self, ...): ... + ``` + +3. **Update DummyDialect** (for main package protocols): + ```python + # impl/dummy/dialect.py + class DummyDialect( + ..., + NewFeatureMixin, + NewFeatureSupport, + ): + def supports_new_feature(self) -> bool: return True + ``` + +4. **Add Tests**: + - `tests/.../dummy2/test_new_feature.py` - Expression tests + - `tests/.../dummy/test_dummy_protocol_support.py` - Protocol support verification + +```python +# dialect.py +from rhosocial.activerecord.backend.dialect import ( + SQLDialect, ReturningClauseHandler, TypeMapping +) +from typing import Dict, List, Optional + +class MyDatabaseDialect(SQLDialect): + def __init__(self): + super().__init__() + self.returning_handler = MyDatabaseReturningHandler() + + def quote_identifier(self, identifier: str) -> str: return f'"{identifier}"' + def get_placeholder(self, name: str = None) -> str: return "?" + + def format_limit_offset(self, sql: str, limit: Optional[int], offset: Optional[int]) -> str: + if limit is not None: sql += f" LIMIT {limit}" + if offset is not None: sql += f" OFFSET {offset}" + return sql + + def get_type_mappings(self) -> Dict[str, TypeMapping]: + return {"INTEGER": TypeMapping("INTEGER"), "TEXT": TypeMapping("TEXT"), ...} + +class MyDatabaseReturningHandler(ReturningClauseHandler): + @property + def is_supported(self) -> bool: return True + + def format_clause(self, columns: Optional[List[str]] = None) -> str: + cols = "*" if not columns else ', '.join(self.dialect.quote_identifier(c) for c in columns) + return f"RETURNING {cols}" +``` + +### 4. The Type Adaptation System + +This system handles the conversion of Python values to and from formats compatible with the native database driver. This is a two-part implementation: defining the adapters, and then telling the core library how to use them. + +#### a. The `SQLTypeAdapter` Pattern +Each adapter is a stateless component for converting between a source Python type and a target **driver-compatible Python type**. + +```python +# From: src/rhosocial/activerecord/backend/type_adapter.py (Core Library) +from abc import ABC, abstractmethod +from typing import Type, Set, Dict + +class BaseSQLTypeAdapter(ABC): + def __init__(self): + self._supported_types: Dict[Type, Set[Type]] = {} + + def _register_type(self, py_type: Type, driver_py_type: Type): + """Registers that this adapter can convert from py_type to driver_py_type.""" + if py_type not in self._supported_types: + self._supported_types[py_type] = set() + self._supported_types[py_type].add(driver_py_type) + + @abstractmethod + def _do_to_database(self, value, target_type, options): ... + @abstractmethod + def _do_from_database(self, value, target_type, options): ... +``` + +#### b. Registering Adapters in Your Backend +In your backend's `__init__` (or a helper), you instantiate and register all adapters your backend will use. + +```python +# backend.py (inside your MyBackend class) +from .adapters import MyCustomJSONAdapter +from ...type_adapter import DateTimeAdapter, DecimalAdapter + +def _register_my_adapters(self): + """Register all adapters the backend will use.""" + all_adapters = [ + DateTimeAdapter(), + DecimalAdapter(), + MyCustomJSONAdapter(), # Backend-specific adapter can override standard ones + ] + for adapter in all_adapters: + for py_type, driver_types in adapter.supported_types.items(): + for driver_type in driver_types: + self.adapter_registry.register( + adapter, py_type, driver_type, allow_override=True + ) +``` + +#### c. Providing Suggestions via `get_default_adapter_suggestions` +This method is the "glue". It defines your backend's preferred conversion strategy. + +```python +# backend.py (inside your MyBackend class) +def get_default_adapter_suggestions(self) -> Dict[Type, Tuple[SQLTypeAdapter, Type]]: + suggestions: Dict[Type, Tuple[SQLTypeAdapter, Type]] = {} + from datetime import date, datetime + from decimal import Decimal + # ... other imports + + # Define the desired (Source Python Type -> Target Driver Python Type) mappings + type_mappings = [ + (datetime, str), # Prefer converting datetimes to ISO strings + (Decimal, str), # Prefer converting Decimals to strings for precision + (dict, str), + ] + + for py_type, driver_type in type_mappings: + adapter = self.adapter_registry.get_adapter(py_type, driver_type) + if adapter: + suggestions[py_type] = (adapter, driver_type) + return suggestions +``` + +### 5. Transaction Management +Proper transaction management is a cornerstone of any reliable database backend, ensuring data consistency and ACID compliance. + +```python +# transaction.py +from contextlib import contextmanager + +class MyTransactionManager: + def __init__(self, backend_connection): + self.connection = backend_connection + self._savepoint_counter = 0 + + @contextmanager + def transaction(self, isolation_level: Optional[str] = None): + self.connection.begin() + try: + yield self + self.connection.commit() + except Exception: + self.connection.rollback() + raise + + @contextmanager + def savepoint(self, name: Optional[str] = None): + self._savepoint_counter += 1 + name = name or f"sp_{self._savepoint_counter}" + self.connection.execute(f"SAVEPOINT {name}") + try: + yield + self.connection.execute(f"RELEASE SAVEPOINT {name}") + except Exception: + self.connection.execute(f"ROLLBACK TO SAVEPOINT {name}") + raise +``` + +### 6. Error Handling +A robust backend must catch driver-specific exceptions and re-raise them as standardized `DatabaseError` subclasses from the core library (`IntegrityError`, `ConnectionError`, etc.). + +```python +# In backend.py, as part of the _handle_error implementation +from ...errors import IntegrityError, ConnectionError +# import native_driver + +def _handle_error(self, error: Exception) -> None: + if isinstance(error, native_driver.UniqueConstraintViolation): + raise IntegrityError("Unique constraint failed") from error + if isinstance(error, native_driver.CannotConnectNow): + raise ConnectionError("Connection failed") from error + # Fallback to a generic database error + raise DatabaseError(f"An unexpected database error occurred: {error}") from error +``` + +### 7. Other Core Components +- **Feature Detection**: A class that checks the database version to declare supported features (CTEs, Window Functions, etc.). +- **Performance Optimization**: Advanced features like connection pooling and query optimization hints. + +## Testing Requirements + +A backend is only as reliable as its tests. The test suite should be comprehensive and cover all aspects of the implementation. + +- **Connection & Configuration**: Test successful connections, disconnections, pings, and failure modes with bad configurations. +- **CRUD & Query Execution**: Verify that `execute`, `fetch_one`, `fetch_all`, etc., work correctly for all statement types. +- **Type Adaptation System**: + - Write dedicated unit tests for each custom `SQLTypeAdapter`. + - Test that `get_default_adapter_suggestions` returns the correct adapter and target type. + - Write integration tests that save and retrieve models with all supported data types to ensure end-to-end correctness. +- **Transaction Management**: Test `commit`, `rollback`, and savepoints. +- **Dialect & SQL Formatting**: Test that the dialect produces correct SQL for `LIMIT`/`OFFSET`, `RETURNING`, etc. +- **Error Handling**: Test that driver-specific errors are correctly caught and re-raised as standard exceptions. +- **Expression Formatting**: Integration tests should confirm that queries using `SQLExpression` objects (like `CurrentExpression`) execute correctly. + +## Preparing for Public Release + +Beyond implementing the core components, a production-quality backend intended for public release should meet the following standards to ensure quality, compatibility, and maintainability: + +* **Comprehensive Backend Testing**: The backend must have its own robust test suite covering all aspects mentioned in the "Testing Requirements" section. This is the first line of defense for quality. +* **Testsuite Compliance**: The backend must integrate with and pass the official `rhosocial-activerecord-testsuite`. This is a mandatory step to guarantee compatibility with the ActiveRecord core and other ecosystem components. +* **Continuous Integration (CI)**: It is highly recommended to set up a CI pipeline for automated testing across different Python versions and environments. You can refer to the workflow files in the main project's `.github/workflows/` directory for a working example of how to configure this. + +## Checklist for New Backends + +This checklist summarizes all the required and recommended steps for creating a high-quality backend. + +### Required Implementation +- [ ] Implement all `StorageBackend` abstract methods (`connect`, `disconnect`, `ping`, `get_server_version`, `introspect_and_adapt`, etc.). +- [ ] Provide a `Connection Configuration` class. +- [ ] Implement a `SQL Dialect` for syntax differences. +- [ ] Implement the full **Type Adaptation System**: + - [ ] Create specific adapters where needed (`adapters.py`). + - [ ] Register all adapters in `_register_my_adapters`. + - [ ] Implement `get_default_adapter_suggestions` to define the conversion strategy. +- [ ] Implement `Transaction Management`. +- [ ] Implement `Error Handling` to map driver exceptions. +- [ ] Implement `Feature Detection` to declare capabilities. +- [ ] If providing an `AsyncStorageBackend`, ensure it has **Functional Equivalence** with the sync version. + +### Required Tests +- [ ] Comprehensive tests for every item in the **Testing Requirements** section above. + +### Release Readiness +- [ ] All backend-specific tests are passing. +- [ ] The backend is fully compliant with the official `rhosocial-activerecord-testsuite`. +- [ ] A **Continuous Integration (CI)** pipeline is set up for automated testing. diff --git a/.claude/code_style.md b/.claude/code_style.md index fae47ac8..048b351d 100644 --- a/.claude/code_style.md +++ b/.claude/code_style.md @@ -93,6 +93,48 @@ result = ( | Protected | Single underscore | `_protected_method` | | Magic | Double underscore | `__table_name__`, `__primary_key__` | +### Sync/Async Function Signature Naming Rules + +**CRITICAL**: For functions that need to distinguish between synchronous and asynchronous versions (e.g., I/O operations), the naming convention is strictly defined: + +1. **Method names must be IDENTICAL** - No `_async` suffix or similar distinctions +2. **Only the `async`/`await` keywords differ** - Everything else in the signature must match +3. **This applies to**: + - Method names + - Parameter names and types + - Return type annotations (except `async` methods return `Coroutine`) + +```python +# CORRECT: Sync/Async methods with identical names +class StorageBackend: + def connect(self) -> None: + """Establish connection to database.""" + pass + + def execute(self, sql: str, params: Optional[Tuple] = None) -> QueryResult: + """Execute a SQL query.""" + pass + +class AsyncStorageBackend: + async def connect(self) -> None: + """Establish connection to database asynchronously.""" + pass + + async def execute(self, sql: str, params: Optional[Tuple] = None) -> QueryResult: + """Execute a SQL query asynchronously.""" + pass + +# WRONG: Do NOT use _async suffix +class BadExample: + async def connect_async(self): # ❌ WRONG + pass + + async def execute_async(self, sql: str): # ❌ WRONG + pass +``` + +**Rationale**: This convention ensures that sync and async APIs are strictly equivalent, making it easier for users to switch between them without learning different method names. The only difference users need to be aware of is the `async`/`await` keywords. + ### Special Naming Patterns ```python @@ -271,6 +313,10 @@ def utility_function(): __all__ = ['MainClass', 'utility_function'] ``` +#### Grouping Functionalities and File Splitting +- If a single file contains several functionalities and the file is not long, it is recommended to group and arrange these functionalities together, and add section comments (e.g., `# region ... # endregion`). +- If the file is too long, e.g., over 1000 lines, and a single group also exceeds 400 lines, it is recommended to split it into separate files under a directory package. + ### Class Organization ```python @@ -566,6 +612,94 @@ class TimestampMixin: self.updated_at = datetime.now() ``` +## Expression-Dialect System Coding Standards + +### Expression Class Guidelines + +1. **Proper Inheritance**: + - All expression classes must inherit from `BaseExpression` + - Value expressions inherit from `SQLValueExpression` + - Predicate expressions inherit from `SQLPredicate` + - Use appropriate mixins for functionality (e.g., `ArithmeticMixin`, `ComparisonMixin`) + +2. **Protocol Implementation**: + - Implement `ToSQLProtocol` with proper `to_sql()` method + - Return `SQLQueryAndParams` type (SQL string and parameter tuple) + - Never directly concatenate SQL strings in expression classes + +3. **Dialect Delegation**: + - Always delegate formatting to dialect methods + - Use `self.dialect.format_*()` methods for all SQL formatting + - Maintain separation between query structure and SQL generation + +4. **Explicit Control**: + - Expression classes should not maintain internal state + - No hidden behaviors or automatic operations + - Keep expressions stateless and pure + - Unlike systems with complex object state management, our expressions are simple and predictable + +### Dialect Class Guidelines + +1. **Base Class Inheritance**: + - All dialect classes must inherit from `SQLDialectBase` + - Implement all required abstract methods + +2. **Formatting Methods**: + - Implement `format_*` methods for all SQL formatting needs + - Handle identifier quoting appropriately + - Properly handle parameter binding + +3. **Database-Specific Logic**: + - Include database-specific syntax variations + - Handle version-specific features + - Implement feature detection where needed + +### Expression System Module Organization + +Follow the established module structure: +- `bases.py`: Abstract base classes and protocols +- `core.py`: Core expressions (columns, literals, functions) +- `mixins.py`: Operator overloading capabilities +- `operators.py`: SQL operations +- `predicates.py`: Predicate expressions +- `query_parts.py`: Query clauses +- `statements.py`: DML/DDL statements +- `functions.py`: Factory functions +- `aggregates.py`: Aggregation expressions +- `advanced_functions.py`: Advanced SQL features +- `query_sources.py`: Data source expressions +- `graph.py`: Graph query expressions + +### Example Expression Class + +```python +from typing import TYPE_CHECKING +from . import bases, mixins + +if TYPE_CHECKING: # pragma: no cover + from ..dialect import SQLDialectBase + +class MyExpression(mixins.ArithmeticMixin, mixins.ComparisonMixin, bases.SQLValueExpression): + """Represents a custom SQL expression.""" + def __init__(self, dialect: "SQLDialectBase", value: str): + super().__init__(dialect) + self.value = value + + def to_sql(self) -> 'bases.SQLQueryAndParams': + # Delegate formatting to dialect + formatted_value = self.dialect.format_string_literal(self.value) + return formatted_value, (self.value,) +``` + +### Example Dialect Method + +```python +def format_identifier(self, identifier: str) -> str: + """Format identifier with proper quoting.""" + # Database-specific identifier formatting + return f'"{identifier}"' # Example for standard SQL +``` + ## Code Review Checklist - [ ] Path comment at file start matches actual location @@ -577,4 +711,8 @@ class TimestampMixin: - [ ] Exception messages are informative - [ ] Test coverage for new code - [ ] Security considerations addressed -- [ ] Performance implications considered \ No newline at end of file +- [ ] Performance implications considered +- [ ] Expression classes properly delegate to dialect +- [ ] Dialect methods handle formatting correctly +- [ ] Expression-dialect separation maintained +- [ ] Expression system module organization followed \ No newline at end of file diff --git a/.claude/commands/check-sync-async.md b/.claude/commands/check-sync-async.md new file mode 100644 index 00000000..42911928 --- /dev/null +++ b/.claude/commands/check-sync-async.md @@ -0,0 +1,25 @@ +Check sync/async API parity + +Check that sync and async APIs are properly paired following the parity rules. + +## Parity Rules to Verify + +1. **Class Names**: Async version adds Async prefix + - BaseActiveRecord → AsyncBaseActiveRecord + - ActiveQuery → AsyncActiveQuery + +2. **Method Names**: Remain IDENTICAL (no _async suffix) + - def save(self) → async def save(self) + - NOT: async def save_async(self) + +3. **Docstrings**: Async version notes "asynchronously" in first sentence + +4. **Field Order**: Declaration order must match exactly + +5. **Testing Parity**: + - Fixtures: order_fixtures → async_order_fixtures + - Test classes: TestQuery → TestAsyncQuery + - Test methods: identical names + - Schema files: shared between sync/async + +Scan the codebase for violations and report any issues found. diff --git a/.claude/commands/gen-migration.md b/.claude/commands/gen-migration.md new file mode 100644 index 00000000..2f44e715 --- /dev/null +++ b/.claude/commands/gen-migration.md @@ -0,0 +1,148 @@ +--- +description: Generate database migration SQL for models +agent: build +--- + +Generate SQL migration for creating or modifying database tables. + +## Migration Configuration + +Ask the user for the following information: + +1. **Migration Type**: + - `create` - Create new table + - `alter` - Modify existing table + - `drop` - Drop table + - `index` - Add index +2. **Target Model** (e.g., `User`, `Order`) +3. **Database Type** (sqlite, postgresql, mysql - affects SQL syntax) +4. **Fields** (for create/alter: `name:type:constraints`) +5. **Foreign Keys** (optional references to other tables) + +## Generated SQL Examples + +### Create Table + +```sql +-- SQLite / PostgreSQL / MySQL +CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, -- SQLite + -- id SERIAL PRIMARY KEY, -- PostgreSQL + -- id INT AUTO_INCREMENT PRIMARY KEY, -- MySQL + name VARCHAR(100) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + age INTEGER DEFAULT 0, + status VARCHAR(20) DEFAULT 'active', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Indexes +CREATE INDEX idx_users_email ON users(email); +CREATE INDEX idx_users_status ON users(status); +``` + +### Alter Table + +```sql +-- Add column +ALTER TABLE users ADD COLUMN phone VARCHAR(20); + +-- Modify column (PostgreSQL) +ALTER TABLE users ALTER COLUMN age SET DEFAULT 18; + +-- SQLite workaround for ALTER (limited support) +-- Requires table recreation +``` + +### Foreign Keys + +```sql +CREATE TABLE orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + total DECIMAL(10,2), + status VARCHAR(20) DEFAULT 'pending', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- Foreign key constraint + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); + +-- Index for foreign key +CREATE INDEX idx_orders_user_id ON orders(user_id); +``` + +### Join Table (Many-to-Many) + +```sql +CREATE TABLE user_roles ( + user_id INTEGER NOT NULL, + role_id INTEGER NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY (user_id, role_id), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE +); + +CREATE INDEX idx_user_roles_user_id ON user_roles(user_id); +CREATE INDEX idx_user_roles_role_id ON user_roles(role_id); +``` + +## Field Type Mapping + +| Python Type | SQLite | PostgreSQL | MySQL | +|-------------|--------|------------|-------| +| `int` | INTEGER | INTEGER | INT | +| `str` | TEXT/VARCHAR | VARCHAR | VARCHAR | +| `float` | REAL | DOUBLE PRECISION | DOUBLE | +| `bool` | INTEGER | BOOLEAN | BOOLEAN | +| `datetime` | TIMESTAMP | TIMESTAMP | TIMESTAMP | +| `date` | DATE | DATE | DATE | +| `Decimal` | DECIMAL | DECIMAL | DECIMAL | +| `Optional[T]` | NULL | NULL | NULL | + +## Constraints + +```sql +-- Primary Key (auto-increment) +id INTEGER PRIMARY KEY AUTOINCREMENT -- SQLite +id SERIAL PRIMARY KEY -- PostgreSQL +id INT AUTO_INCREMENT PRIMARY KEY -- MySQL + +-- Unique +email VARCHAR(255) UNIQUE + +-- Not Null +name VARCHAR(100) NOT NULL + +-- Default +status VARCHAR(20) DEFAULT 'active' + +-- Check (PostgreSQL/MySQL) +age INTEGER CHECK (age >= 0) + +-- Foreign Key with actions +FOREIGN KEY (user_id) REFERENCES users(id) + ON DELETE CASCADE + ON UPDATE CASCADE +``` + +## Migration File Naming + +``` +migrations/ +├── 001_create_users.sql +├── 002_create_orders.sql +├── 003_add_user_indexes.sql +└── 004_add_order_status.sql +``` + +## Best Practices + +1. **Always create indexes** on foreign keys and frequently queried columns +2. **Use CASCADE carefully** - understand data deletion impact +3. **Add timestamps** (created_at, updated_at) for audit trails +4. **Use appropriate field sizes** - don't use TEXT for short strings +5. **Test migrations** on a copy of production data before deploying diff --git a/.claude/commands/gen-model.md b/.claude/commands/gen-model.md new file mode 100644 index 00000000..87631693 --- /dev/null +++ b/.claude/commands/gen-model.md @@ -0,0 +1,73 @@ +--- +description: Generate ActiveRecord model with fields and configuration +agent: build +--- + +Generate a new ActiveRecord model for rhosocial-activerecord. + +## Model Configuration + +Ask the user for the following information: + +1. **Model Name** (PascalCase, e.g., `User`, `OrderItem`) +2. **Sync or Async** (`sync` or `async`) +3. **Table Name** (snake_case, e.g., `users`, `order_items`) +4. **Primary Key** (default: `id`) +5. **Fields** (format: `name:type`, e.g., `name:str, age:int, email:str`) +6. **Mixins** (optional: `TimestampMixin`, `SoftDeleteMixin`, `VersionMixin`) + +## Generated Model Structure + +```python +from typing import ClassVar, Optional +from datetime import datetime +from pydantic import Field +from rhosocial.activerecord.model import ActiveRecord +from rhosocial.activerecord.base import FieldProxy +from rhosocial.activerecord.field import TimestampMixin # if selected + +class User(ActiveRecord): # or AsyncActiveRecord + __table_name__ = 'users' + __primary_key__ = 'id' # if not default + + # Fields + name: str = Field(..., max_length=100) + email: str + age: int = 0 + + # Required: Enable type-safe query building + c: ClassVar[FieldProxy] = FieldProxy() +``` + +## Generated SQL Migration + +```sql +CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(100) NOT NULL, + email VARCHAR(255) NOT NULL, + age INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +## After Generation + +1. Save the model to `src/models/user.py` +2. Show the SQL migration to create the table +3. Remind the user to configure the backend: + ```python + from rhosocial.activerecord.backend.impl.sqlite import SQLiteBackend + User.configure(SQLiteBackend("sqlite:///app.db")) + ``` + +## Field Type Mappings + +- `str` → VARCHAR (with optional max_length) +- `int` → INTEGER +- `float` → FLOAT/DOUBLE +- `bool` → BOOLEAN +- `datetime` → TIMESTAMP +- `date` → DATE +- `Optional[T]` → Nullable column diff --git a/.claude/commands/gen-query.md b/.claude/commands/gen-query.md new file mode 100644 index 00000000..64026677 --- /dev/null +++ b/.claude/commands/gen-query.md @@ -0,0 +1,85 @@ +--- +description: Generate preset ActiveQuery methods for a model +agent: build +--- + +Generate preset query methods for an existing ActiveRecord model. + +## Query Configuration + +Ask the user for the following information: + +1. **Target Model** (e.g., `User`, `Order`) +2. **Query Name** (e.g., `active_users`, `recent_orders`) +3. **Query Type**: + - `finder` - Single record lookup + - `list` - Multiple records + - `count` - Count records + - `exists` - Check existence +4. **Conditions** (e.g., `status == 'active'`, `created_at >= 7_days_ago`) +5. **Ordering** (optional, e.g., `created_at DESC`, `name ASC`) +6. **Limit** (optional, e.g., `10`, `100`) + +## Generated Code Examples + +### Finder Query +```python +@classmethod +def find_active_by_email(cls, email: str) -> Optional['User']: + """Find active user by email address.""" + return cls.query().where( + (cls.c.email == email) & (cls.c.status == 'active') + ).one() +``` + +### List Query +```python +@classmethod +def get_recent_orders(cls, days: int = 7) -> List['Order']: + """Get orders from the last N days.""" + from datetime import datetime, timedelta + cutoff = datetime.now() - timedelta(days=days) + return cls.query().where( + cls.c.created_at >= cutoff + ).order_by((cls.c.created_at, "DESC")).all() +``` + +### Count Query +```python +@classmethod +def count_active_users(cls) -> int: + """Count active users.""" + result = cls.query().where( + cls.c.status == 'active' + ).count() + return result +``` + +### Exists Query +```python +@classmethod +def has_admin_users(cls) -> bool: + """Check if any admin users exist.""" + return cls.query().where( + cls.c.role == 'admin' + ).exists() +``` + +## Best Practices + +1. **Use classmethod** for queries that don't require instance +2. **Return typed results** using `Optional['Model']` or `List['Model']` +3. **Use FieldProxy** (cls.c.column_name) for type-safe column references +4. **Wrap conditions in parentheses** when using & or | +5. **Add docstrings** describing the query purpose +6. **Handle edge cases** (empty results, None values) + +## Naming Conventions + +- **find_*** - Single record lookup (returns Optional[Model]) +- **get_*** - Multiple records (returns List[Model]) +- **count_*** - Count operation (returns int) +- **has_*** / **exists_*** - Existence check (returns bool) +- **active_*** - Filter by status +- **recent_*** - Time-based filtering +- **by_*** - Lookup by specific field diff --git a/.claude/commands/gen-relation.md b/.claude/commands/gen-relation.md new file mode 100644 index 00000000..08e7df45 --- /dev/null +++ b/.claude/commands/gen-relation.md @@ -0,0 +1,154 @@ +--- +description: Generate model relationships (belongs_to, has_one, has_many) +agent: build +--- + +Generate relationship definitions between ActiveRecord models. + +## Relationship Configuration + +Ask the user for the following information: + +1. **Source Model** (e.g., `Order` - the model that will have the relationship) +2. **Relationship Type**: + - `belongs_to` - Child references parent (contains foreign key) + - `has_one` - Parent has one child + - `has_many` - Parent has many children + - `has_many_through` - Many-to-many via join table +3. **Target Model** (e.g., `User` - the related model) +4. **Foreign Key** (e.g., `user_id`, defaults to `{target}_id`) +5. **Inverse Of** (optional, e.g., `orders` - the reverse relationship name) +6. **Sync or Async** (affects both models) + +## Generated Code Examples + +### belongs_to (Order belongs to User) + +```python +from typing import ClassVar +from rhosocial.activerecord.model import ActiveRecord +from rhosocial.activerecord.relation import BelongsTo + +class Order(ActiveRecord): + __table_name__ = 'orders' + + user_id: int + total: float + + # Relationship + user: ClassVar[BelongsTo['User']] = BelongsTo( + foreign_key='user_id', + inverse_of='orders' + ) + + c: ClassVar[FieldProxy] = FieldProxy() +``` + +### has_many (User has many Orders) + +```python +from typing import ClassVar +from rhosocial.activerecord.model import ActiveRecord +from rhosocial.activerecord.relation import HasMany + +class User(ActiveRecord): + __table_name__ = 'users' + + name: str + email: str + + # Relationship + orders: ClassVar[HasMany['Order']] = HasMany( + foreign_key='user_id', + inverse_of='user' + ) + + c: ClassVar[FieldProxy] = FieldProxy() +``` + +### has_one (User has one Profile) + +```python +from typing import ClassVar +from rhosocial.activerecord.model import ActiveRecord +from rhosocial.activerecord.relation import HasOne + +class User(ActiveRecord): + __table_name__ = 'users' + + name: str + + # Relationship + profile: ClassVar[HasOne['Profile']] = HasOne( + foreign_key='user_id', + inverse_of='user' + ) + + c: ClassVar[FieldProxy] = FieldProxy() +``` + +### has_many_through (User has many Roles through UserRole) + +```python +from typing import ClassVar +from rhosocial.activerecord.model import ActiveRecord +from rhosocial.activerecord.relation import HasManyThrough + +class User(ActiveRecord): + __table_name__ = 'users' + + name: str + + # Many-to-many relationship + roles: ClassVar[HasManyThrough['Role']] = HasManyThrough( + through='UserRole', # Join model + foreign_key='user_id', + target_foreign_key='role_id', + inverse_of='users' + ) + + c: ClassVar[FieldProxy] = FieldProxy() +``` + +## Relationship Matrix + +| Type | Foreign Key Location | Example | +|------|---------------------|---------| +| belongs_to | Source model | Order has `user_id` | +| has_one | Target model | Profile has `user_id` | +| has_many | Target model | Order has `user_id` | +| has_many_through | Join table | UserRole has both IDs | + +## Important Notes + +1. **Use String References** for forward declarations to avoid circular imports: + ```python + orders: ClassVar[HasMany['Order']] # Note the quotes + ``` + +2. **Define both sides** of the relationship for bidirectional access: + ```python + # In Order: + user: ClassVar[BelongsTo['User']] = BelongsTo(inverse_of='orders') + + # In User: + orders: ClassVar[HasMany['Order']] = HasMany(inverse_of='user') + ``` + +3. **Foreign key naming** follows convention `{related_model}_id` + +4. **Async models** use `AsyncBelongsTo`, `AsyncHasOne`, `AsyncHasMany` + +## Generated Migration + +Include foreign key constraints in SQL: +```sql +CREATE TABLE orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + total DECIMAL(10,2), + FOREIGN KEY (user_id) REFERENCES users(id) +); + +CREATE INDEX idx_orders_user_id ON orders(user_id); +``` diff --git a/.claude/commands/lint-fix.md b/.claude/commands/lint-fix.md new file mode 100644 index 00000000..97abb5b3 --- /dev/null +++ b/.claude/commands/lint-fix.md @@ -0,0 +1,9 @@ +Auto-fix code issues with ruff + +Auto-fix code style and formatting issues. + +Execute the following commands: +1. ruff check src/ --fix - Auto-fix code style issues +2. ruff format src/ - Format code + +Show what was fixed and any remaining issues that need manual attention. diff --git a/.claude/commands/lint.md b/.claude/commands/lint.md new file mode 100644 index 00000000..ad96bb3a --- /dev/null +++ b/.claude/commands/lint.md @@ -0,0 +1,9 @@ +Run code linting with ruff + +Run code linting and formatting checks for the project. + +Execute the following commands: +1. ruff check src/ - Check code style +2. ruff format --check src/ - Check code formatting + +Show any violations and suggest fixes. diff --git a/.claude/commands/new-backend.md b/.claude/commands/new-backend.md new file mode 100644 index 00000000..70230dc6 --- /dev/null +++ b/.claude/commands/new-backend.md @@ -0,0 +1,44 @@ +Create new database backend implementation + +Create a new database backend for rhosocial-activerecord. + +## Backend Structure + +Create directory: src/rhosocial/activerecord/backend/impl/{backend_name}/ + +Required files: +1. backend.py - StorageBackend implementation (sync and async) +2. dialect.py - SQLDialect implementation +3. config.py - Connection configuration +4. type_adapter.py - Type adapter for Python <-> DB type conversion + +## Implementation Guidelines + +### backend.py +- Implement StorageBackend for sync +- Implement AsyncStorageBackend for async +- Must support: connect, disconnect, execute, transaction methods +- Use dialect for SQL generation (don't generate SQL directly) + +### dialect.py +- Inherit from SQLDialectBase +- Implement format_* methods for SQL formatting +- Implement supports_* protocol methods for feature detection +- Handle backend-specific SQL syntax + +### config.py +- Define connection configuration dataclass +- Include: host, port, database, username, password, etc. +- Provide to_connection_string() method + +### type_adapter.py +- Register Python <-> DB type conversions +- Handle special types (datetime, decimal, json, etc.) + +## Testing +Create tests in: tests/rhosocial/activerecord_test/feature/backend/{backend_name}/ + +Ask the user: +1. Backend name (e.g., mysql, postgresql)? +2. Python driver package (e.g., pymysql, psycopg2)? +3. Any special features to support? diff --git a/.claude/commands/new-feature.md b/.claude/commands/new-feature.md new file mode 100644 index 00000000..ae781bce --- /dev/null +++ b/.claude/commands/new-feature.md @@ -0,0 +1,33 @@ +Create new feature following project standards + +Create a new feature for rhosocial-activerecord following the project's strict standards. + +## Feature Creation Checklist + +### 1. Design Phase +- Review existing features in .claude/feature_points.md +- Determine feature category: query/backend/field/relation/mixin +- Decide if async API is needed (usually yes for parity) + +### 2. Implementation Phase +- Create sync version: src/rhosocial/activerecord/{category}/{feature}.py +- Create async version: src/rhosocial/activerecord/{category}/async_{feature}.py +- Ensure method names are IDENTICAL between sync/async (only add Async prefix to class name) +- Document with "asynchronously" in async version's first docstring sentence +- Keep field/method order identical between versions + +### 3. Testing Phase +- Add tests following Testsuite architecture +- Use Provider pattern for fixtures +- Ensure sync/async test parity (fixtures, test classes, methods) +- Share SQL schema files between sync/async tests + +### 4. Documentation +- Create changelog.d/{issue}.{type}.md fragment +- Update relevant documentation + +Ask the user: +1. What is the feature name (snake_case)? +2. What category does it belong to? +3. Does it need async API? +4. What issue number should be referenced? diff --git a/.claude/commands/test-feature.md b/.claude/commands/test-feature.md new file mode 100644 index 00000000..cf37b96c --- /dev/null +++ b/.claude/commands/test-feature.md @@ -0,0 +1,12 @@ +Run tests for feature category: $ARGUMENTS + +Valid categories: basic, query, relation, events, mixins, backend + +Before running tests, ensure PYTHONPATH is set: +```bash +export PYTHONPATH=src # Linux/macOS +``` + +Then run: pytest tests/rhosocial/activerecord_test/feature/$ARGUMENTS/ -v + +Show test results and any failures. diff --git a/.claude/commands/test.md b/.claude/commands/test.md new file mode 100644 index 00000000..83545e21 --- /dev/null +++ b/.claude/commands/test.md @@ -0,0 +1,12 @@ +Run the full test suite for rhosocial-activerecord. + +Before running tests, ensure PYTHONPATH is set: +```bash +export PYTHONPATH=src # Linux/macOS +# OR +$env:PYTHONPATH="src" # PowerShell +``` + +Then run: pytest tests/ -v + +Show test results and any failures. Focus on failing tests and suggest fixes. diff --git a/.claude/commands/type-check.md b/.claude/commands/type-check.md new file mode 100644 index 00000000..051814bf --- /dev/null +++ b/.claude/commands/type-check.md @@ -0,0 +1,8 @@ +Run type checking with mypy + +Run type checking for the project using mypy. + +Execute: +mypy src/rhosocial/activerecord --ignore-missing-imports + +Show any type errors and suggest fixes. diff --git a/.claude/commands/validate-model.md b/.claude/commands/validate-model.md new file mode 100644 index 00000000..5e95c977 --- /dev/null +++ b/.claude/commands/validate-model.md @@ -0,0 +1,116 @@ +--- +description: Validate ActiveRecord model configuration +agent: build +--- + +Validate an existing ActiveRecord model for common configuration errors. + +## Validation Checklist + +Check the following aspects of the model: + +### 1. Required Configuration +- [ ] `__table_name__` is defined +- [ ] `__primary_key__` is defined (or defaults to 'id') +- [ ] Backend is configured before use + +### 2. FieldProxy Configuration +- [ ] `c: ClassVar[FieldProxy] = FieldProxy()` is present +- [ ] Used correctly in query building (not string references) + +### 3. Field Definitions +- [ ] All fields have type annotations +- [ ] Field types are supported by the backend +- [ ] Optional fields use `Optional[T]` +- [ ] Default values match field types + +### 4. Sync/Async Consistency +- [ ] Model inherits from correct base class (`ActiveRecord` vs `AsyncActiveRecord`) +- [ ] Query methods use appropriate syntax + +### 5. Relationship Configuration +- [ ] Relationships use string forward references (`'ModelName'`) +- [ ] Foreign keys match relationship definitions +- [ ] Inverse relationships are defined + +### 6. Import Statements +- [ ] Required imports are present +- [ ] No circular import issues + +## Common Issues + +### Missing FieldProxy +```python +# WRONG +class User(ActiveRecord): + __table_name__ = 'users' + name: str + + def find_by_name(self, name): + return self.query().where("name = ?", name) # Bad! + +# CORRECT +class User(ActiveRecord): + __table_name__ = 'users' + name: str + c: ClassVar[FieldProxy] = FieldProxy() + + def find_by_name(self, name): + return self.query().where(self.c.name == name) # Good! +``` + +### No Backend Configured +```python +# WRONG +user = User(name="John") # Will raise "No backend configured" +user.save() + +# CORRECT +from rhosocial.activerecord.backend.impl.sqlite import SQLiteBackend +User.configure(SQLiteBackend("sqlite:///app.db")) +user = User(name="John") +user.save() +``` + +### Incorrect Type Annotations +```python +# WRONG +class User(ActiveRecord): + name = str # Missing type annotation + age: int = "0" # Wrong default type + +# CORRECT +class User(ActiveRecord): + name: str + age: int = 0 + email: Optional[str] = None +``` + +### String References in Queries +```python +# WRONG +User.query().where("age >= 18") # Not type-safe + +# CORRECT +User.query().where(User.c.age >= 18) # Type-safe +``` + +## Validation Output + +After validation, provide: + +1. **Checklist results** - Pass/Fail for each validation point +2. **Issues found** - Specific problems with line numbers +3. **Suggested fixes** - Code examples showing corrections +4. **Best practices** - Recommendations for improvement + +## Usage + +Run validation on a model file: +```bash +# Check specific model +/validate:model src/models/user.py + +# Check all models in directory +/validate:model src/models/ +``` diff --git a/.claude/testing.md b/.claude/testing.md index 7e2d8c87..7acfc565 100644 --- a/.claude/testing.md +++ b/.claude/testing.md @@ -1,239 +1,61 @@ -> **Important**: This document is a supplement to the main **Testing Architecture and Execution Guide** ([local](../../python-activerecord/.claude/testing.md) | [github](https://github.com/Rhosocial/python-activerecord/blob/main/.claude/testing.md)). Please read the core documentation first to understand the overall testing architecture, provider patterns, and capability negotiation. This guide focuses only on details specific to the PostgreSQL backend. +# Testing Guide - python-activerecord-postgres -# PostgreSQL Backend Testing Guide +> AI Assistant Note: This document covers PostgreSQL backend-specific testing requirements. -## 1. Overview +## Project-Specific Information -This guide provides instructions for setting up and running tests for the `rhosocial-activerecord-postgres` backend, covering PostgreSQL-specific configuration, testing patterns, and troubleshooting. +| Item | Value | +|------|-------| +| **Python Version** | 3.8+ | +| **Database Driver** | psycopg (version 3) | +| **Free-Threading Support** | ❌ No | -## 2. PostgreSQL Test Environment Setup +## ⚠️ Important Limitation -### 2.1. Dependencies +**psycopg does NOT support free-threaded Python (Python 3.13t, 3.14t).** -In addition to core testing dependencies, the PostgreSQL backend requires `psycopg`. For easier installation, `psycopg-binary` is also included. +This backend CANNOT be used with free-threaded Python builds. Free-threaded tests are not included. + +## Dependencies ```toml -# pyproject.toml -[project] dependencies = [ - "psycopg>=3.2.12", - "psycopg-binary>=3.2.12" -] - -[project.optional-dependencies] -test = [ - # ... other dependencies - "rhosocial-activerecord-testsuite", + "psycopg[binary]", + "rhosocial-activerecord" ] ``` -### 2.2. Test Scenario Configuration - -All database configurations for testing are managed through **test scenarios**. The primary method for defining these scenarios is the `tests/config/postgres_scenarios.yaml` file. This file is git-ignored, so you must create it locally. - -The test framework, specifically `config_manager.py` and `providers/scenarios.py`, reads this file to understand which database environments are available for testing. - -**Example `tests/config/postgres_scenarios.yaml`:** - -This example shows how to configure connections for different versions of PostgreSQL. You should adapt the host, port, and credential details to your local environment. - -```yaml -scenarios: - # Example for a PostgreSQL 16 server - postgres_16: - host: "localhost" - port: 5432 - username: "test_user" - password: "your_password" # Use ${PG_PASSWORD} to load from env var - database: "test_activerecord_pg16" - sslmode: "prefer" - - # Example for a PostgreSQL 15 server (commented out by default) - # postgres_15: - # host: "localhost" - # port: 5433 - # username: "test_user" - # password: "your_password" - # database: "test_activerecord_pg15" -``` - -Alternatively, for CI/CD environments, you can configure scenarios using environment variables, but the YAML file is recommended for local development. - -### 2.3. Setting Up Test Databases with Docker - -For a consistent and isolated testing environment, you can use Docker to run PostgreSQL instances. - -```bash -# Start a PostgreSQL 16 container for the 'postgres_16' scenario -docker run -d \ - --name postgres-test-16 \ - -e POSTGRES_USER=test_user \ - -e POSTGRES_PASSWORD=your_password \ - -e POSTGRES_DB=test_activerecord_pg16 \ - -p 5432:5432 \ - postgres:16 -``` - -## 3. Testing Against the Shared Test Suite - -Integrating the PostgreSQL backend with `rhosocial-activerecord-testsuite` involves implementing a set of provider classes that adapt the generic tests to the PostgreSQL environment. - -### 3.1. Implement Provider Interfaces by Feature - -For each feature category defined in the test suite (e.g., `basic`, `query`), a corresponding provider must be implemented in the `tests/providers/` directory. - -Each provider is responsible for: -1. **Setting up the database connection** for its specific test scenario. -2. **Creating the necessary schema**. This involves converting SQLite-based schemas from the test suite to PostgreSQL-compatible SQL (e.g., changing `AUTOINCREMENT` to `SERIAL` or `IDENTITY`). -3. **Configuring and returning ActiveRecord models**. -4. **Handling cleanup** (teardown). - -The `providers/registry.py` file discovers and registers these provider implementations. - -### 3.2. Import and Execute Shared Tests - -With providers in place, tests are triggered by files in `tests/rhosocial/activerecord_postgres_test/feature/`. - -To ensure maximum compatibility, **it is strongly recommended to import and run the entire suite of tests without omissions.** This provides a comprehensive validation of the backend's implementation. - -These test files import test classes from `rhosocial-activerecord-testsuite`, and pytest fixtures invoke the providers to run the tests against the PostgreSQL backend. +## Quick Test Commands ```bash -# Ensure the test suite is installed -pip install rhosocial-activerecord-testsuite -``` - -## 4. Capability Declaration - -Capabilities are declared once, during the **backend's instantiation phase**, not during testing. The `PostgreSQLBackend` class's `_initialize_capabilities` method is called on creation to build a `DatabaseCapabilities` object based on the database version. - -**Example: Declaring Capabilities in `PostgreSQLBackend`:** -```python -# src/rhosocial/activerecord/backend/impl/postgres/backend.py -from rhosocial.activerecord.backend.capabilities import ( - DatabaseCapabilities, - CTECapability, - JSONCapability -) - -class PostgreSQLBackend(StorageBackend): - def _initialize_capabilities(self): - """Initializes and returns the backend's capability descriptor.""" - capabilities = DatabaseCapabilities() - version = self.get_server_version() # Returns a tuple like (16, 1, 0) - - # PostgreSQL has robust JSONB support - if version >= (9, 4, 0): - capabilities.add_json_operation([ - JSONCapability.BASIC_JSON, - JSONCapability.JSON_EXTRACT, - JSONCapability.JSONB, # PostgreSQL specific - ]) - - # RETURNING clause is well-supported - capabilities.add_dml_returning(True) - - return capabilities -``` -The test suite automatically uses this information to skip unsupported tests. For backend-specific tests, you are responsible for managing capability checks. - -## 5. Writing PostgreSQL-Specific Tests - -Dedicated tests are needed for PostgreSQL-specific features. These can be organized in a separate directory or interspersed within the `tests/rhosocial/activerecord_postgres_test/feature/` directories. - -### Example: Testing JSONB Contains Operator - -```python -# tests/feature/query/test_postgres_jsonb.py -def test_postgres_jsonb_contains_operator(pg_backend): - """Tests PostgreSQL's JSONB '@>' contains operator.""" - pg_backend.execute("CREATE TABLE users (id SERIAL, data JSONB)") - - class User(ActiveRecord): - # ... - - User(data={'tags': ['python', 'orm']}).save() - - # Use the @> operator for JSONB containment - found_user = User.where("data @> %s", [{'tags': ['python']}]).first() - - assert found_user is not None -``` - -## 6. Troubleshooting - -* **Issue**: `psycopg.OperationalError: connection failed` - * **Solution**: - 1. Check if your PostgreSQL service is running (e.g., via `docker ps`). - 2. Confirm that the details in your `postgres_scenarios.yaml` match your running database instance. - 3. Try connecting manually (e.g., with `psql`) to verify credentials and permissions. - -* **Issue**: PostgreSQL syntax errors during shared tests. - * **Solution**: Check the schema conversion logic in your providers. It may need to be updated to handle more SQLite-to-PostgreSQL syntax differences (e.g., `AUTOINCREMENT` to `SERIAL`). - -* **Issue**: Tests skipped due to unsupported capabilities. - * **Solution**: Check the `_initialize_capabilities` method in your backend implementation. Ensure it correctly declares all supported features based on the PostgreSQL version. - -## 7. Quick Command Reference - -**Crucial Prerequisite**: Before running any tests, `PYTHONPATH` **must** be set. - -```bash -# Set PYTHONPATH (Linux/macOS) +# Activate virtual environment and set PYTHONPATH +cd /mnt/i/GitHubRepositories/rhosocial/python-activerecord-postgres +source .venv/bin/activate export PYTHONPATH=src -# Set PYTHONPATH (Windows PowerShell) -$env:PYTHONPATH="src" - -# --- - -# --- Test Execution --- +# Run tests +pytest -# Run all PostgreSQL backend tests -pytest tests/ - -# Run tests for a specific feature +# Run specific test directory pytest tests/rhosocial/activerecord_postgres_test/feature/basic/ - -# Run specific tests by name -pytest -k "jsonb" - -# View coverage -pytest --cov=rhosocial.activerecord.backend.impl.postgres tests/ ``` -## 8. CRITICAL: No Parallel Test Execution - -**Tests MUST be executed serially. Do NOT use pytest-xdist or similar parallel execution tools.** - -### Why Parallel Execution Is Not Supported - -The test suite creates database tables with **fixed names** (e.g., `users`, `orders`, `posts`). Each test: - -1. Drops existing tables: `DROP TABLE IF EXISTS ... CASCADE` -2. Creates fresh tables with the same names -3. Inserts test data -4. Cleans up after completion - -If tests run in parallel, they will: -- Drop tables while other tests are using them → Errors -- Create conflicting data → Test failures -- Cleanup prematurely → Data loss +## Backend-Specific Test Markers -### What NOT To Do - -```bash -# DO NOT use parallel execution -pytest -n auto # ❌ WILL CAUSE FAILURES -pytest -n 4 # ❌ WILL CAUSE FAILURES -pytest --dist=loadfile # ❌ WILL CAUSE FAILURES +```python +markers = [ + "requires_functions: marks tests requiring specific database functions", +] ``` -### Correct Execution +## Key Differences from Core -```bash -# Always run tests serially (default behavior) -pytest # ✅ Correct - serial execution -``` +- Uses PostgreSQL-specific dialect in `src/rhosocial/activerecord/backend/impl/postgres/dialect.py` +- Schema files in `tests/rhosocial/activerecord_postgres_test/feature/basic/schema/` +- Provider implementation in `tests/providers/` +- Supports PostgreSQL-specific features: array types, JSONB, range types, PostGIS, etc. + +## Reference -For more details, see the main project's [Testing Documentation](https://github.com/Rhosocial/python-activerecord/blob/main/.claude/testing.md). +- [Core testing guide](../python-activerecord/.claude/testing.md) +- [PostgreSQL backend development](../python-activerecord/.claude/backend_development.md) \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 120000 index 00000000..681311eb --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..a97384be --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,36 @@ +# Project Overview: rhosocial-activerecord-postgres + +## Project Name +- **Repository Name**: python-activerecord-postgres +- **Python Package Name**: rhosocial-activerecord-postgres + +## Project Purpose + +This project is a PostgreSQL backend implementation for the `rhosocial-activerecord` Python package. It provides PostgreSQL database support with the elegant ActiveRecord pattern interface. + +## Key Design Principles + +1. **Backend Implementation**: Extends core ActiveRecord with PostgreSQL-specific features +2. **Driver**: Uses `psycopg` (version 3) for database connectivity +3. **Namespace Package**: Integrates with the rhosocial namespace package architecture + +## Current Status + +This project is under active development. Key features implemented: + +- Basic CRUD operations +- Connection management +- Transaction support +- Schema introspection +- PostgreSQL-specific data types +- Array types +- JSON/JSONB +- Range types +- PostGIS spatial types +- UUID types +- HStore +- Full-text search + +## Version Control and Changelog + +This project adheres to the same version control, branching, commit message, and changelog management standards as the main `python-activerecord` project. \ No newline at end of file diff --git a/GEMINI.md b/GEMINI.md new file mode 120000 index 00000000..681311eb --- /dev/null +++ b/GEMINI.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file From 324ba4411241a00ab9e6f7b5a7f6d88dda51d6ce Mon Sep 17 00:00:00 2001 From: vistart Date: Thu, 7 May 2026 15:50:50 +0800 Subject: [PATCH 06/12] fix(test): align Expression parameter names with core API - Replace table_name= with table= for CreateTableExpression, DropTableExpression - Replace table= with table_name= for CreateIndexExpression (PostgreSQL-specific) - Apply across all 20 extension integration tests and create_table_like tests --- .../extensions/test_bloom_integration.py | 24 ++--- .../extensions/test_btree_gin_integration.py | 8 +- .../extensions/test_btree_gist_integration.py | 8 +- .../extensions/test_citext_integration.py | 84 +++++++++--------- .../extensions/test_cube_integration.py | 28 +++--- .../test_earthdistance_integration.py | 16 ++-- .../test_fuzzystrmatch_integration.py | 40 ++++----- .../extensions/test_hstore_integration.py | 36 ++++---- .../extensions/test_hypopg_integration.py | 8 +- .../extensions/test_intarray_integration.py | 88 +++++++++---------- .../extensions/test_ltree_integration.py | 36 ++++---- .../extensions/test_orafce_integration.py | 8 +- .../extensions/test_pg_trgm_integration.py | 68 +++++++------- .../extensions/test_pgcrypto_integration.py | 40 ++++----- .../extensions/test_pgrouting_integration.py | 8 +- .../extensions/test_pgvector_integration.py | 64 +++++++------- .../extensions/test_postgis_integration.py | 88 +++++++++---------- .../test_postgis_raster_integration.py | 8 +- .../extensions/test_tablefunc_integration.py | 24 ++--- .../extensions/test_uuid_ossp_integration.py | 24 ++--- .../feature/backend/test_create_table_like.py | 30 +++---- 21 files changed, 369 insertions(+), 369 deletions(-) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_bloom_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_bloom_integration.py index 8905847e..814d970b 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_bloom_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_bloom_integration.py @@ -46,8 +46,8 @@ def bloom_env(postgres_backend_single): dialect = backend.dialect # Clean up residual tables from previous runs - for table_name in [TABLE_NAME]: - drop_expr = DropTableExpression(dialect=dialect, table_name=table_name, if_exists=True) + for table in [TABLE_NAME]: + drop_expr = DropTableExpression(dialect=dialect, table=table, if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) @@ -65,7 +65,7 @@ def bloom_env(postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=TABLE_NAME, + table=TABLE_NAME, columns=columns, if_not_exists=True, ) @@ -105,7 +105,7 @@ def bloom_env(postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table_name=TABLE_NAME, + table=TABLE_NAME, if_exists=True, ) sql, params = drop_expr.to_sql() @@ -168,7 +168,7 @@ def test_bloom_index_with_options(self, bloom_env): ] create_expr = CreateTableExpression( dialect=dialect, - table_name="test_bloom_opts", + table="test_bloom_opts", columns=columns, if_not_exists=True, ) @@ -208,7 +208,7 @@ def test_bloom_index_with_options(self, bloom_env): finally: drop_expr = DropTableExpression( dialect=dialect, - table_name="test_bloom_opts", + table="test_bloom_opts", if_exists=True, ) sql, params = drop_expr.to_sql() @@ -223,8 +223,8 @@ async def async_bloom_env(async_postgres_backend_single): dialect = backend.dialect # Clean up residual tables from previous runs - for table_name in [ASYNC_TABLE_NAME]: - drop_expr = DropTableExpression(dialect=dialect, table_name=table_name, if_exists=True) + for table in [ASYNC_TABLE_NAME]: + drop_expr = DropTableExpression(dialect=dialect, table=table, if_exists=True) sql, params = drop_expr.to_sql() await backend.execute(sql, params) @@ -242,7 +242,7 @@ async def async_bloom_env(async_postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=ASYNC_TABLE_NAME, + table=ASYNC_TABLE_NAME, columns=columns, if_not_exists=True, ) @@ -282,7 +282,7 @@ async def async_bloom_env(async_postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table_name=ASYNC_TABLE_NAME, + table=ASYNC_TABLE_NAME, if_exists=True, ) sql, params = drop_expr.to_sql() @@ -345,7 +345,7 @@ async def test_async_bloom_index_with_options(self, async_bloom_env): ] create_expr = CreateTableExpression( dialect=dialect, - table_name="test_bloom_opts_async", + table="test_bloom_opts_async", columns=columns, if_not_exists=True, ) @@ -383,7 +383,7 @@ async def test_async_bloom_index_with_options(self, async_bloom_env): finally: drop_expr = DropTableExpression( dialect=dialect, - table_name="test_bloom_opts_async", + table="test_bloom_opts_async", if_exists=True, ) sql, params = drop_expr.to_sql() diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gin_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gin_integration.py index b95937ba..a94ded0a 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gin_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gin_integration.py @@ -62,7 +62,7 @@ def btree_gin_env(postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table_name="test_btree_gin", + table="test_btree_gin", columns=columns, if_not_exists=True, ) @@ -89,7 +89,7 @@ def btree_gin_env(postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table_name="test_btree_gin", + table="test_btree_gin", if_exists=True, ) sql, params = drop_expr.to_sql() @@ -217,7 +217,7 @@ async def async_btree_gin_env(async_postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table_name="test_btree_gin_async", + table="test_btree_gin_async", columns=columns, if_not_exists=True, ) @@ -244,7 +244,7 @@ async def async_btree_gin_env(async_postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table_name="test_btree_gin_async", + table="test_btree_gin_async", if_exists=True, ) sql, params = drop_expr.to_sql() diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gist_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gist_integration.py index 44109c1b..39636320 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gist_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gist_integration.py @@ -64,7 +64,7 @@ def btree_gist_env(postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table_name="test_btree_gist", + table="test_btree_gist", columns=columns, if_not_exists=True, ) @@ -92,7 +92,7 @@ def btree_gist_env(postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table_name="test_btree_gist", + table="test_btree_gist", if_exists=True, ) sql, params = drop_expr.to_sql() @@ -228,7 +228,7 @@ async def async_btree_gist_env(async_postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table_name="test_btree_gist_async", + table="test_btree_gist_async", columns=columns, if_not_exists=True, ) @@ -256,7 +256,7 @@ async def async_btree_gist_env(async_postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table_name="test_btree_gist_async", + table="test_btree_gist_async", if_exists=True, ) sql, params = drop_expr.to_sql() diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_citext_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_citext_integration.py index 14c58575..c87b27d2 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_citext_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_citext_integration.py @@ -42,7 +42,7 @@ # --- Helper functions --- -def _setup_citext_table(backend, dialect, table_name): +def _setup_citext_table(backend, dialect, table): """Create and populate the basic citext test table using expressions.""" columns = [ ColumnDefinition( @@ -56,7 +56,7 @@ def _setup_citext_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -65,7 +65,7 @@ def _setup_citext_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["name"], source=ValuesSource( dialect, @@ -76,7 +76,7 @@ def _setup_citext_table(backend, dialect, table_name): backend.execute(sql, params) -def _setup_citext_unique_table(backend, dialect, table_name): +def _setup_citext_unique_table(backend, dialect, table): """Create and populate the citext unique constraint test table using expressions.""" columns = [ ColumnDefinition( @@ -96,7 +96,7 @@ def _setup_citext_unique_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -105,7 +105,7 @@ def _setup_citext_unique_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["email"], source=ValuesSource( dialect, @@ -116,7 +116,7 @@ def _setup_citext_unique_table(backend, dialect, table_name): backend.execute(sql, params) -def _setup_citext_like_table(backend, dialect, table_name): +def _setup_citext_like_table(backend, dialect, table): """Create and populate the citext LIKE test table using expressions.""" columns = [ ColumnDefinition( @@ -130,7 +130,7 @@ def _setup_citext_like_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -139,7 +139,7 @@ def _setup_citext_like_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["name"], source=ValuesSource( dialect, @@ -153,7 +153,7 @@ def _setup_citext_like_table(backend, dialect, table_name): backend.execute(sql, params) -def _setup_citext_users_table(backend, dialect, table_name): +def _setup_citext_users_table(backend, dialect, table): """Create and populate the citext users test table using expressions.""" columns = [ ColumnDefinition( @@ -167,7 +167,7 @@ def _setup_citext_users_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -176,7 +176,7 @@ def _setup_citext_users_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["username"], source=ValuesSource( dialect, @@ -187,7 +187,7 @@ def _setup_citext_users_table(backend, dialect, table_name): backend.execute(sql, params) -def _setup_citext_roles_table(backend, dialect, table_name): +def _setup_citext_roles_table(backend, dialect, table): """Create and populate the citext roles test table using expressions.""" columns = [ ColumnDefinition( @@ -202,7 +202,7 @@ def _setup_citext_roles_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -211,7 +211,7 @@ def _setup_citext_roles_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["username", "role"], source=ValuesSource( dialect, @@ -222,18 +222,18 @@ def _setup_citext_roles_table(backend, dialect, table_name): backend.execute(sql, params) -def _teardown_table(backend, dialect, table_name): +def _teardown_table(backend, dialect, table): """Drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table_name=table_name, + table=table, if_exists=True, ) sql, params = drop_expr.to_sql() backend.execute(sql, params) -async def _async_setup_citext_table(backend, dialect, table_name): +async def _async_setup_citext_table(backend, dialect, table): """Async: create and populate the basic citext test table using expressions.""" columns = [ ColumnDefinition( @@ -247,7 +247,7 @@ async def _async_setup_citext_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -256,7 +256,7 @@ async def _async_setup_citext_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["name"], source=ValuesSource( dialect, @@ -267,7 +267,7 @@ async def _async_setup_citext_table(backend, dialect, table_name): await backend.execute(sql, params) -async def _async_setup_citext_unique_table(backend, dialect, table_name): +async def _async_setup_citext_unique_table(backend, dialect, table): """Async: create and populate the citext unique constraint test table using expressions.""" columns = [ ColumnDefinition( @@ -287,7 +287,7 @@ async def _async_setup_citext_unique_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -296,7 +296,7 @@ async def _async_setup_citext_unique_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["email"], source=ValuesSource( dialect, @@ -307,7 +307,7 @@ async def _async_setup_citext_unique_table(backend, dialect, table_name): await backend.execute(sql, params) -async def _async_setup_citext_like_table(backend, dialect, table_name): +async def _async_setup_citext_like_table(backend, dialect, table): """Async: create and populate the citext LIKE test table using expressions.""" columns = [ ColumnDefinition( @@ -321,7 +321,7 @@ async def _async_setup_citext_like_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -330,7 +330,7 @@ async def _async_setup_citext_like_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["name"], source=ValuesSource( dialect, @@ -344,7 +344,7 @@ async def _async_setup_citext_like_table(backend, dialect, table_name): await backend.execute(sql, params) -async def _async_setup_citext_users_table(backend, dialect, table_name): +async def _async_setup_citext_users_table(backend, dialect, table): """Async: create and populate the citext users test table using expressions.""" columns = [ ColumnDefinition( @@ -358,7 +358,7 @@ async def _async_setup_citext_users_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -367,7 +367,7 @@ async def _async_setup_citext_users_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["username"], source=ValuesSource( dialect, @@ -378,7 +378,7 @@ async def _async_setup_citext_users_table(backend, dialect, table_name): await backend.execute(sql, params) -async def _async_setup_citext_roles_table(backend, dialect, table_name): +async def _async_setup_citext_roles_table(backend, dialect, table): """Async: create and populate the citext roles test table using expressions.""" columns = [ ColumnDefinition( @@ -393,7 +393,7 @@ async def _async_setup_citext_roles_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -402,7 +402,7 @@ async def _async_setup_citext_roles_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["username", "role"], source=ValuesSource( dialect, @@ -413,11 +413,11 @@ async def _async_setup_citext_roles_table(backend, dialect, table_name): await backend.execute(sql, params) -async def _async_teardown_table(backend, dialect, table_name): +async def _async_teardown_table(backend, dialect, table): """Async: drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table_name=table_name, + table=table, if_exists=True, ) sql, params = drop_expr.to_sql() @@ -450,9 +450,9 @@ def citext_env(postgres_backend_single): dialect = backend.dialect # Clean up residual tables from previous runs - for table_name in [T_CITEXT, T_CITEXT_UNIQUE, T_CITEXT_LIKE, + for table in [T_CITEXT, T_CITEXT_UNIQUE, T_CITEXT_LIKE, T_CITEXT_USERS, T_CITEXT_ROLES]: - _teardown_table(backend, dialect, table_name) + _teardown_table(backend, dialect, table) _setup_citext_table(backend, dialect, T_CITEXT) _setup_citext_unique_table(backend, dialect, T_CITEXT_UNIQUE) @@ -462,9 +462,9 @@ def citext_env(postgres_backend_single): yield backend, dialect - for table_name in [T_CITEXT, T_CITEXT_UNIQUE, T_CITEXT_LIKE, + for table in [T_CITEXT, T_CITEXT_UNIQUE, T_CITEXT_LIKE, T_CITEXT_USERS, T_CITEXT_ROLES]: - _teardown_table(backend, dialect, table_name) + _teardown_table(backend, dialect, table) class TestCitextIntegration: @@ -624,9 +624,9 @@ async def async_citext_env(async_postgres_backend_single): dialect = backend.dialect # Clean up residual tables from previous runs - for table_name in [T_CITEXT_ASYNC, T_CITEXT_UNIQUE_ASYNC, T_CITEXT_LIKE_ASYNC, + for table in [T_CITEXT_ASYNC, T_CITEXT_UNIQUE_ASYNC, T_CITEXT_LIKE_ASYNC, T_CITEXT_USERS_ASYNC, T_CITEXT_ROLES_ASYNC]: - await _async_teardown_table(backend, dialect, table_name) + await _async_teardown_table(backend, dialect, table) await _async_setup_citext_table(backend, dialect, T_CITEXT_ASYNC) await _async_setup_citext_unique_table(backend, dialect, T_CITEXT_UNIQUE_ASYNC) @@ -636,9 +636,9 @@ async def async_citext_env(async_postgres_backend_single): yield backend, dialect - for table_name in [T_CITEXT_ASYNC, T_CITEXT_UNIQUE_ASYNC, T_CITEXT_LIKE_ASYNC, + for table in [T_CITEXT_ASYNC, T_CITEXT_UNIQUE_ASYNC, T_CITEXT_LIKE_ASYNC, T_CITEXT_USERS_ASYNC, T_CITEXT_ROLES_ASYNC]: - await _async_teardown_table(backend, dialect, table_name) + await _async_teardown_table(backend, dialect, table) class TestAsyncCitextIntegration: diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_cube_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_cube_integration.py index 885315b7..a7be5d5f 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_cube_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_cube_integration.py @@ -50,15 +50,15 @@ def cube_env(postgres_backend_single): dialect = backend.dialect # Clean up residual tables from previous runs - for table_name in ["test_cubes", "test_cube_dist", "test_cube_idx"]: - drop_expr = DropTableExpression(dialect=dialect, table_name=table_name, if_exists=True) + for table in ["test_cubes", "test_cube_dist", "test_cube_idx"]: + drop_expr = DropTableExpression(dialect=dialect, table=table, if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) # Setup: create test_cubes table create_cubes = CreateTableExpression( dialect=dialect, - table_name="test_cubes", + table="test_cubes", columns=[ ColumnDefinition( name="id", @@ -93,7 +93,7 @@ def cube_env(postgres_backend_single): # Setup: create test_cube_dist table create_dist = CreateTableExpression( dialect=dialect, - table_name="test_cube_dist", + table="test_cube_dist", columns=[ ColumnDefinition( name="id", @@ -128,7 +128,7 @@ def cube_env(postgres_backend_single): # Setup: create test_cube_idx table create_idx_table = CreateTableExpression( dialect=dialect, - table_name="test_cube_idx", + table="test_cube_idx", columns=[ ColumnDefinition( name="id", @@ -172,10 +172,10 @@ def cube_env(postgres_backend_single): yield backend, dialect # Teardown: drop tables - for table_name in ["test_cubes", "test_cube_dist", "test_cube_idx"]: + for table in ["test_cubes", "test_cube_dist", "test_cube_idx"]: drop_expr = DropTableExpression( dialect=dialect, - table_name=table_name, + table=table, if_exists=True, ) sql, params = drop_expr.to_sql() @@ -336,15 +336,15 @@ async def async_cube_env(async_postgres_backend_single): dialect = backend.dialect # Clean up residual tables from previous runs - for table_name in ["test_cubes_async", "test_cube_dist_async", "test_cube_idx_async"]: - drop_expr = DropTableExpression(dialect=dialect, table_name=table_name, if_exists=True) + for table in ["test_cubes_async", "test_cube_dist_async", "test_cube_idx_async"]: + drop_expr = DropTableExpression(dialect=dialect, table=table, if_exists=True) sql, params = drop_expr.to_sql() await backend.execute(sql, params) # Setup: create test_cubes_async table create_cubes = CreateTableExpression( dialect=dialect, - table_name="test_cubes_async", + table="test_cubes_async", columns=[ ColumnDefinition( name="id", @@ -379,7 +379,7 @@ async def async_cube_env(async_postgres_backend_single): # Setup: create test_cube_dist_async table create_dist = CreateTableExpression( dialect=dialect, - table_name="test_cube_dist_async", + table="test_cube_dist_async", columns=[ ColumnDefinition( name="id", @@ -414,7 +414,7 @@ async def async_cube_env(async_postgres_backend_single): # Setup: create test_cube_idx_async table create_idx_table = CreateTableExpression( dialect=dialect, - table_name="test_cube_idx_async", + table="test_cube_idx_async", columns=[ ColumnDefinition( name="id", @@ -458,10 +458,10 @@ async def async_cube_env(async_postgres_backend_single): yield backend, dialect # Teardown: drop tables - for table_name in ["test_cubes_async", "test_cube_dist_async", "test_cube_idx_async"]: + for table in ["test_cubes_async", "test_cube_dist_async", "test_cube_idx_async"]: drop_expr = DropTableExpression( dialect=dialect, - table_name=table_name, + table=table, if_exists=True, ) sql, params = drop_expr.to_sql() diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_earthdistance_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_earthdistance_integration.py index 46ad6371..3af2872a 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_earthdistance_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_earthdistance_integration.py @@ -42,8 +42,8 @@ def earthdistance_env(postgres_backend_single): dialect = backend.dialect # Clean up residual tables from previous runs - for table_name in [TABLE_NAME]: - drop_expr = DropTableExpression(dialect=dialect, table_name=table_name, if_exists=True) + for table in [TABLE_NAME]: + drop_expr = DropTableExpression(dialect=dialect, table=table, if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) @@ -60,7 +60,7 @@ def earthdistance_env(postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=TABLE_NAME, + table=TABLE_NAME, columns=columns, if_not_exists=True, ) @@ -88,7 +88,7 @@ def earthdistance_env(postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table_name=TABLE_NAME, + table=TABLE_NAME, if_exists=True, ) sql, params = drop_expr.to_sql() @@ -154,8 +154,8 @@ async def async_earthdistance_env(async_postgres_backend_single): dialect = backend.dialect # Clean up residual tables from previous runs - for table_name in [ASYNC_TABLE_NAME]: - drop_expr = DropTableExpression(dialect=dialect, table_name=table_name, if_exists=True) + for table in [ASYNC_TABLE_NAME]: + drop_expr = DropTableExpression(dialect=dialect, table=table, if_exists=True) sql, params = drop_expr.to_sql() await backend.execute(sql, params) @@ -172,7 +172,7 @@ async def async_earthdistance_env(async_postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=ASYNC_TABLE_NAME, + table=ASYNC_TABLE_NAME, columns=columns, if_not_exists=True, ) @@ -200,7 +200,7 @@ async def async_earthdistance_env(async_postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table_name=ASYNC_TABLE_NAME, + table=ASYNC_TABLE_NAME, if_exists=True, ) sql, params = drop_expr.to_sql() diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_fuzzystrmatch_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_fuzzystrmatch_integration.py index 92077d41..311d54ee 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_fuzzystrmatch_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_fuzzystrmatch_integration.py @@ -44,7 +44,7 @@ TABLE_NAME_DIFF_ASYNC = "test_fuzzy_diff_async" -def _setup_lev_table(backend, dialect, table_name): +def _setup_lev_table(backend, dialect, table): """Create and populate the levenshtein test table using expressions.""" columns = [ ColumnDefinition( @@ -58,7 +58,7 @@ def _setup_lev_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -73,7 +73,7 @@ def _setup_lev_table(backend, dialect, table_name): ] insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["word"], source=ValuesSource(dialect, rows), ) @@ -81,7 +81,7 @@ def _setup_lev_table(backend, dialect, table_name): backend.execute(sql, params) -def _setup_diff_table(backend, dialect, table_name): +def _setup_diff_table(backend, dialect, table): """Create and populate the difference test table using expressions.""" columns = [ ColumnDefinition( @@ -95,7 +95,7 @@ def _setup_diff_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -109,7 +109,7 @@ def _setup_diff_table(backend, dialect, table_name): ] insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["name"], source=ValuesSource(dialect, rows), ) @@ -117,18 +117,18 @@ def _setup_diff_table(backend, dialect, table_name): backend.execute(sql, params) -def _teardown_table(backend, dialect, table_name): +def _teardown_table(backend, dialect, table): """Drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table_name=table_name, + table=table, if_exists=True, ) sql, params = drop_expr.to_sql() backend.execute(sql, params) -async def _async_setup_lev_table(backend, dialect, table_name): +async def _async_setup_lev_table(backend, dialect, table): """Async: create and populate the levenshtein test table using expressions.""" columns = [ ColumnDefinition( @@ -142,7 +142,7 @@ async def _async_setup_lev_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -157,7 +157,7 @@ async def _async_setup_lev_table(backend, dialect, table_name): ] insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["word"], source=ValuesSource(dialect, rows), ) @@ -165,7 +165,7 @@ async def _async_setup_lev_table(backend, dialect, table_name): await backend.execute(sql, params) -async def _async_setup_diff_table(backend, dialect, table_name): +async def _async_setup_diff_table(backend, dialect, table): """Async: create and populate the difference test table using expressions.""" columns = [ ColumnDefinition( @@ -179,7 +179,7 @@ async def _async_setup_diff_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -193,7 +193,7 @@ async def _async_setup_diff_table(backend, dialect, table_name): ] insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["name"], source=ValuesSource(dialect, rows), ) @@ -201,11 +201,11 @@ async def _async_setup_diff_table(backend, dialect, table_name): await backend.execute(sql, params) -async def _async_teardown_table(backend, dialect, table_name): +async def _async_teardown_table(backend, dialect, table): """Async: drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table_name=table_name, + table=table, if_exists=True, ) sql, params = drop_expr.to_sql() @@ -220,8 +220,8 @@ def fuzzystrmatch_env(postgres_backend_single): dialect = backend.dialect # Clean up residual tables from previous runs - for table_name in [TABLE_NAME, TABLE_NAME_DIFF]: - _teardown_table(backend, dialect, table_name) + for table in [TABLE_NAME, TABLE_NAME_DIFF]: + _teardown_table(backend, dialect, table) _setup_lev_table(backend, dialect, TABLE_NAME) _setup_diff_table(backend, dialect, TABLE_NAME_DIFF) @@ -464,8 +464,8 @@ async def async_fuzzystrmatch_env(async_postgres_backend_single): dialect = backend.dialect # Clean up residual tables from previous runs - for table_name in [TABLE_NAME_ASYNC, TABLE_NAME_DIFF_ASYNC]: - await _async_teardown_table(backend, dialect, table_name) + for table in [TABLE_NAME_ASYNC, TABLE_NAME_DIFF_ASYNC]: + await _async_teardown_table(backend, dialect, table) await _async_setup_lev_table(backend, dialect, TABLE_NAME_ASYNC) await _async_setup_diff_table(backend, dialect, TABLE_NAME_DIFF_ASYNC) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hstore_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hstore_integration.py index 89d1cabf..f8b99261 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hstore_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hstore_integration.py @@ -57,11 +57,11 @@ def _make_hstore_columns(): ] -def _setup_hstore_table(backend, dialect, table_name, hstore_value): +def _setup_hstore_table(backend, dialect, table, hstore_value): """Create and populate an hstore test table using expressions.""" create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=_make_hstore_columns(), if_not_exists=True, ) @@ -70,7 +70,7 @@ def _setup_hstore_table(backend, dialect, table_name, hstore_value): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["data"], source=ValuesSource( dialect, @@ -81,22 +81,22 @@ def _setup_hstore_table(backend, dialect, table_name, hstore_value): backend.execute(sql, params) -def _teardown_table(backend, dialect, table_name): +def _teardown_table(backend, dialect, table): """Drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table_name=table_name, + table=table, if_exists=True, ) sql, params = drop_expr.to_sql() backend.execute(sql, params) -async def _async_setup_hstore_table(backend, dialect, table_name, hstore_value): +async def _async_setup_hstore_table(backend, dialect, table, hstore_value): """Async: create and populate an hstore test table using expressions.""" create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=_make_hstore_columns(), if_not_exists=True, ) @@ -105,7 +105,7 @@ async def _async_setup_hstore_table(backend, dialect, table_name, hstore_value): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["data"], source=ValuesSource( dialect, @@ -116,11 +116,11 @@ async def _async_setup_hstore_table(backend, dialect, table_name, hstore_value): await backend.execute(sql, params) -async def _async_teardown_table(backend, dialect, table_name): +async def _async_teardown_table(backend, dialect, table): """Async: drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table_name=table_name, + table=table, if_exists=True, ) sql, params = drop_expr.to_sql() @@ -159,10 +159,10 @@ def hstore_env(postgres_backend_single): dialect = backend.dialect # Clean up residual tables from previous runs - for table_name in [T_HSTORE, T_HSTORE_OPS, T_HSTORE_KEY, T_HSTORE_CONCAT, + for table in [T_HSTORE, T_HSTORE_OPS, T_HSTORE_KEY, T_HSTORE_CONCAT, T_HSTORE_EACH, T_HSTORE_KEYS_VALS, T_HSTORE_UPDATE, T_HSTORE_DELETE]: - _teardown_table(backend, dialect, table_name) + _teardown_table(backend, dialect, table) _setup_hstore_table(backend, dialect, T_HSTORE, "a=>1, b=>2") _setup_hstore_table(backend, dialect, T_HSTORE_OPS, "color=>red, size=>large") @@ -175,10 +175,10 @@ def hstore_env(postgres_backend_single): yield backend, dialect - for table_name in [T_HSTORE, T_HSTORE_OPS, T_HSTORE_KEY, T_HSTORE_CONCAT, + for table in [T_HSTORE, T_HSTORE_OPS, T_HSTORE_KEY, T_HSTORE_CONCAT, T_HSTORE_EACH, T_HSTORE_KEYS_VALS, T_HSTORE_UPDATE, T_HSTORE_DELETE]: - _teardown_table(backend, dialect, table_name) + _teardown_table(backend, dialect, table) class TestHstoreIntegration: @@ -554,11 +554,11 @@ async def async_hstore_env(async_postgres_backend_single): dialect = backend.dialect # Clean up residual tables from previous runs - for table_name in [T_HSTORE_ASYNC, T_HSTORE_OPS_ASYNC, T_HSTORE_KEY_ASYNC, + for table in [T_HSTORE_ASYNC, T_HSTORE_OPS_ASYNC, T_HSTORE_KEY_ASYNC, T_HSTORE_CONCAT_ASYNC, T_HSTORE_EACH_ASYNC, T_HSTORE_KEYS_VALS_ASYNC, T_HSTORE_UPDATE_ASYNC, T_HSTORE_DELETE_ASYNC]: - await _async_teardown_table(backend, dialect, table_name) + await _async_teardown_table(backend, dialect, table) await _async_setup_hstore_table(backend, dialect, T_HSTORE_ASYNC, "a=>1, b=>2") await _async_setup_hstore_table(backend, dialect, T_HSTORE_OPS_ASYNC, "color=>red, size=>large") @@ -571,11 +571,11 @@ async def async_hstore_env(async_postgres_backend_single): yield backend, dialect - for table_name in [T_HSTORE_ASYNC, T_HSTORE_OPS_ASYNC, T_HSTORE_KEY_ASYNC, + for table in [T_HSTORE_ASYNC, T_HSTORE_OPS_ASYNC, T_HSTORE_KEY_ASYNC, T_HSTORE_CONCAT_ASYNC, T_HSTORE_EACH_ASYNC, T_HSTORE_KEYS_VALS_ASYNC, T_HSTORE_UPDATE_ASYNC, T_HSTORE_DELETE_ASYNC]: - await _async_teardown_table(backend, dialect, table_name) + await _async_teardown_table(backend, dialect, table) class TestAsyncHstoreIntegration: diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hypopg_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hypopg_integration.py index 2b754b99..d477159b 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hypopg_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hypopg_integration.py @@ -56,7 +56,7 @@ def hypopg_env(postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table_name="test_hypopg_items", + table="test_hypopg_items", columns=columns, if_not_exists=True, ) @@ -96,7 +96,7 @@ def hypopg_env(postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table_name="test_hypopg_items", + table="test_hypopg_items", if_exists=True, ) sql, params = drop_expr.to_sql() @@ -272,7 +272,7 @@ async def async_hypopg_env(async_postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table_name="test_hypopg_items_async", + table="test_hypopg_items_async", columns=columns, if_not_exists=True, ) @@ -312,7 +312,7 @@ async def async_hypopg_env(async_postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table_name="test_hypopg_items_async", + table="test_hypopg_items_async", if_exists=True, ) sql, params = drop_expr.to_sql() diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_intarray_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_intarray_integration.py index cde81bd6..8a70b4ff 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_intarray_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_intarray_integration.py @@ -44,7 +44,7 @@ # --- Helper functions --- -def _setup_intarray_table(backend, dialect, table_name): +def _setup_intarray_table(backend, dialect, table): """Create and populate the basic intarray test table using expressions.""" columns = [ ColumnDefinition( @@ -58,7 +58,7 @@ def _setup_intarray_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -67,7 +67,7 @@ def _setup_intarray_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["tags"], source=ValuesSource( dialect, @@ -82,7 +82,7 @@ def _setup_intarray_table(backend, dialect, table_name): backend.execute(sql, params) -def _setup_intarray_overlap_table(backend, dialect, table_name): +def _setup_intarray_overlap_table(backend, dialect, table): """Create and populate the intarray overlap test table using expressions.""" columns = [ ColumnDefinition( @@ -96,7 +96,7 @@ def _setup_intarray_overlap_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -105,7 +105,7 @@ def _setup_intarray_overlap_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["tags"], source=ValuesSource( dialect, @@ -120,7 +120,7 @@ def _setup_intarray_overlap_table(backend, dialect, table_name): backend.execute(sql, params) -def _setup_intarray_contained_table(backend, dialect, table_name): +def _setup_intarray_contained_table(backend, dialect, table): """Create and populate the intarray contained-by test table using expressions.""" columns = [ ColumnDefinition( @@ -134,7 +134,7 @@ def _setup_intarray_contained_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -143,7 +143,7 @@ def _setup_intarray_contained_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["tags"], source=ValuesSource( dialect, @@ -158,7 +158,7 @@ def _setup_intarray_contained_table(backend, dialect, table_name): backend.execute(sql, params) -def _setup_intarray_idx_table(backend, dialect, table_name): +def _setup_intarray_idx_table(backend, dialect, table): """Create and populate the intarray idx() test table using expressions.""" columns = [ ColumnDefinition( @@ -172,7 +172,7 @@ def _setup_intarray_idx_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -181,7 +181,7 @@ def _setup_intarray_idx_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["tags"], source=ValuesSource( dialect, @@ -192,7 +192,7 @@ def _setup_intarray_idx_table(backend, dialect, table_name): backend.execute(sql, params) -def _setup_intarray_gin_table(backend, dialect, table_name, index_name): +def _setup_intarray_gin_table(backend, dialect, table, index_name): """Create and populate the intarray GIN test table using expressions. The GIN index with gin__int_ops opclass is created using raw SQL @@ -210,7 +210,7 @@ def _setup_intarray_gin_table(backend, dialect, table_name, index_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -221,12 +221,12 @@ def _setup_intarray_gin_table(backend, dialect, table_name, index_name): # (expression system does not support opclass specifications) backend.execute( f"CREATE INDEX IF NOT EXISTS {index_name} " - f"ON {table_name} USING gin (tags gin__int_ops)" + f"ON {table} USING gin (tags gin__int_ops)" ) insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["tags"], source=ValuesSource( dialect, @@ -237,18 +237,18 @@ def _setup_intarray_gin_table(backend, dialect, table_name, index_name): backend.execute(sql, params) -def _teardown_table(backend, dialect, table_name): +def _teardown_table(backend, dialect, table): """Drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table_name=table_name, + table=table, if_exists=True, ) sql, params = drop_expr.to_sql() backend.execute(sql, params) -async def _async_setup_intarray_table(backend, dialect, table_name): +async def _async_setup_intarray_table(backend, dialect, table): """Async: create and populate the basic intarray test table using expressions.""" columns = [ ColumnDefinition( @@ -262,7 +262,7 @@ async def _async_setup_intarray_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -271,7 +271,7 @@ async def _async_setup_intarray_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["tags"], source=ValuesSource( dialect, @@ -286,7 +286,7 @@ async def _async_setup_intarray_table(backend, dialect, table_name): await backend.execute(sql, params) -async def _async_setup_intarray_overlap_table(backend, dialect, table_name): +async def _async_setup_intarray_overlap_table(backend, dialect, table): """Async: create and populate the intarray overlap test table using expressions.""" columns = [ ColumnDefinition( @@ -300,7 +300,7 @@ async def _async_setup_intarray_overlap_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -309,7 +309,7 @@ async def _async_setup_intarray_overlap_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["tags"], source=ValuesSource( dialect, @@ -324,7 +324,7 @@ async def _async_setup_intarray_overlap_table(backend, dialect, table_name): await backend.execute(sql, params) -async def _async_setup_intarray_contained_table(backend, dialect, table_name): +async def _async_setup_intarray_contained_table(backend, dialect, table): """Async: create and populate the intarray contained-by test table using expressions.""" columns = [ ColumnDefinition( @@ -338,7 +338,7 @@ async def _async_setup_intarray_contained_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -347,7 +347,7 @@ async def _async_setup_intarray_contained_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["tags"], source=ValuesSource( dialect, @@ -362,7 +362,7 @@ async def _async_setup_intarray_contained_table(backend, dialect, table_name): await backend.execute(sql, params) -async def _async_setup_intarray_idx_table(backend, dialect, table_name): +async def _async_setup_intarray_idx_table(backend, dialect, table): """Async: create and populate the intarray idx() test table using expressions.""" columns = [ ColumnDefinition( @@ -376,7 +376,7 @@ async def _async_setup_intarray_idx_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -385,7 +385,7 @@ async def _async_setup_intarray_idx_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["tags"], source=ValuesSource( dialect, @@ -396,7 +396,7 @@ async def _async_setup_intarray_idx_table(backend, dialect, table_name): await backend.execute(sql, params) -async def _async_setup_intarray_gin_table(backend, dialect, table_name, index_name): +async def _async_setup_intarray_gin_table(backend, dialect, table, index_name): """Async: create and populate the intarray GIN test table using expressions. The GIN index with gin__int_ops opclass is created using raw SQL @@ -414,7 +414,7 @@ async def _async_setup_intarray_gin_table(backend, dialect, table_name, index_na ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -424,12 +424,12 @@ async def _async_setup_intarray_gin_table(backend, dialect, table_name, index_na # GIN index with gin__int_ops opclass requires raw SQL await backend.execute( f"CREATE INDEX IF NOT EXISTS {index_name} " - f"ON {table_name} USING gin (tags gin__int_ops)" + f"ON {table} USING gin (tags gin__int_ops)" ) insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["tags"], source=ValuesSource( dialect, @@ -440,11 +440,11 @@ async def _async_setup_intarray_gin_table(backend, dialect, table_name, index_na await backend.execute(sql, params) -async def _async_teardown_table(backend, dialect, table_name): +async def _async_teardown_table(backend, dialect, table): """Async: drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table_name=table_name, + table=table, if_exists=True, ) sql, params = drop_expr.to_sql() @@ -479,9 +479,9 @@ def intarray_env(postgres_backend_single): dialect = backend.dialect # Clean up residual tables from previous runs - for table_name in [T_INTARRAY, T_INTARRAY_OVERLAP, T_INTARRAY_CONTAINED, + for table in [T_INTARRAY, T_INTARRAY_OVERLAP, T_INTARRAY_CONTAINED, T_INTARRAY_IDX, T_INTARRAY_GIN]: - _teardown_table(backend, dialect, table_name) + _teardown_table(backend, dialect, table) _setup_intarray_table(backend, dialect, T_INTARRAY) _setup_intarray_overlap_table(backend, dialect, T_INTARRAY_OVERLAP) @@ -491,9 +491,9 @@ def intarray_env(postgres_backend_single): yield backend, dialect - for table_name in [T_INTARRAY, T_INTARRAY_OVERLAP, T_INTARRAY_CONTAINED, + for table in [T_INTARRAY, T_INTARRAY_OVERLAP, T_INTARRAY_CONTAINED, T_INTARRAY_IDX, T_INTARRAY_GIN]: - _teardown_table(backend, dialect, table_name) + _teardown_table(backend, dialect, table) class TestIntarrayIntegration: @@ -686,10 +686,10 @@ async def async_intarray_env(async_postgres_backend_single): dialect = backend.dialect # Clean up residual tables from previous runs - for table_name in [T_INTARRAY_ASYNC, T_INTARRAY_OVERLAP_ASYNC, + for table in [T_INTARRAY_ASYNC, T_INTARRAY_OVERLAP_ASYNC, T_INTARRAY_CONTAINED_ASYNC, T_INTARRAY_IDX_ASYNC, T_INTARRAY_GIN_ASYNC]: - await _async_teardown_table(backend, dialect, table_name) + await _async_teardown_table(backend, dialect, table) await _async_setup_intarray_table(backend, dialect, T_INTARRAY_ASYNC) await _async_setup_intarray_overlap_table(backend, dialect, T_INTARRAY_OVERLAP_ASYNC) @@ -699,10 +699,10 @@ async def async_intarray_env(async_postgres_backend_single): yield backend, dialect - for table_name in [T_INTARRAY_ASYNC, T_INTARRAY_OVERLAP_ASYNC, + for table in [T_INTARRAY_ASYNC, T_INTARRAY_OVERLAP_ASYNC, T_INTARRAY_CONTAINED_ASYNC, T_INTARRAY_IDX_ASYNC, T_INTARRAY_GIN_ASYNC]: - await _async_teardown_table(backend, dialect, table_name) + await _async_teardown_table(backend, dialect, table) class TestAsyncIntarrayIntegration: diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_ltree_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_ltree_integration.py index 9696cd07..d0c56ecc 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_ltree_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_ltree_integration.py @@ -50,16 +50,16 @@ def ltree_env(postgres_backend_single): dialect = backend.dialect # Clean up residual tables from previous runs - for table_name in ["test_ltree", "test_ltree_tree", "test_ltree_func", + for table in ["test_ltree", "test_ltree_tree", "test_ltree_func", "test_ltree_idx", "test_ltree_txtq"]: - drop_expr = DropTableExpression(dialect=dialect, table_name=table_name, if_exists=True) + drop_expr = DropTableExpression(dialect=dialect, table=table, if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) # Setup: create test_ltree table create_ltree = CreateTableExpression( dialect=dialect, - table_name="test_ltree", + table="test_ltree", columns=[ ColumnDefinition( name="id", @@ -97,7 +97,7 @@ def ltree_env(postgres_backend_single): # Setup: create test_ltree_tree table create_tree = CreateTableExpression( dialect=dialect, - table_name="test_ltree_tree", + table="test_ltree_tree", columns=[ ColumnDefinition( name="id", @@ -136,7 +136,7 @@ def ltree_env(postgres_backend_single): # Setup: create test_ltree_func table create_func = CreateTableExpression( dialect=dialect, - table_name="test_ltree_func", + table="test_ltree_func", columns=[ ColumnDefinition( name="id", @@ -168,7 +168,7 @@ def ltree_env(postgres_backend_single): # Setup: create test_ltree_idx table create_idx_table = CreateTableExpression( dialect=dialect, - table_name="test_ltree_idx", + table="test_ltree_idx", columns=[ ColumnDefinition( name="id", @@ -216,7 +216,7 @@ def ltree_env(postgres_backend_single): # Setup: create test_ltree_txtq table create_txtq = CreateTableExpression( dialect=dialect, - table_name="test_ltree_txtq", + table="test_ltree_txtq", columns=[ ColumnDefinition( name="id", @@ -252,7 +252,7 @@ def ltree_env(postgres_backend_single): yield backend, dialect # Teardown: drop tables - for table_name in [ + for table in [ "test_ltree", "test_ltree_tree", "test_ltree_func", @@ -261,7 +261,7 @@ def ltree_env(postgres_backend_single): ]: drop_expr = DropTableExpression( dialect=dialect, - table_name=table_name, + table=table, if_exists=True, ) sql, params = drop_expr.to_sql() @@ -555,17 +555,17 @@ async def async_ltree_env(async_postgres_backend_single): dialect = backend.dialect # Clean up residual tables from previous runs - for table_name in ["test_ltree_async", "test_ltree_tree_async", + for table in ["test_ltree_async", "test_ltree_tree_async", "test_ltree_func_async", "test_ltree_idx_async", "test_ltree_txtq_async"]: - drop_expr = DropTableExpression(dialect=dialect, table_name=table_name, if_exists=True) + drop_expr = DropTableExpression(dialect=dialect, table=table, if_exists=True) sql, params = drop_expr.to_sql() await backend.execute(sql, params) # Setup: create test_ltree_async table create_ltree = CreateTableExpression( dialect=dialect, - table_name="test_ltree_async", + table="test_ltree_async", columns=[ ColumnDefinition( name="id", @@ -603,7 +603,7 @@ async def async_ltree_env(async_postgres_backend_single): # Setup: create test_ltree_tree_async table create_tree = CreateTableExpression( dialect=dialect, - table_name="test_ltree_tree_async", + table="test_ltree_tree_async", columns=[ ColumnDefinition( name="id", @@ -642,7 +642,7 @@ async def async_ltree_env(async_postgres_backend_single): # Setup: create test_ltree_func_async table create_func = CreateTableExpression( dialect=dialect, - table_name="test_ltree_func_async", + table="test_ltree_func_async", columns=[ ColumnDefinition( name="id", @@ -674,7 +674,7 @@ async def async_ltree_env(async_postgres_backend_single): # Setup: create test_ltree_idx_async table create_idx_table = CreateTableExpression( dialect=dialect, - table_name="test_ltree_idx_async", + table="test_ltree_idx_async", columns=[ ColumnDefinition( name="id", @@ -722,7 +722,7 @@ async def async_ltree_env(async_postgres_backend_single): # Setup: create test_ltree_txtq_async table create_txtq = CreateTableExpression( dialect=dialect, - table_name="test_ltree_txtq_async", + table="test_ltree_txtq_async", columns=[ ColumnDefinition( name="id", @@ -758,7 +758,7 @@ async def async_ltree_env(async_postgres_backend_single): yield backend, dialect # Teardown: drop tables - for table_name in [ + for table in [ "test_ltree_async", "test_ltree_tree_async", "test_ltree_func_async", @@ -767,7 +767,7 @@ async def async_ltree_env(async_postgres_backend_single): ]: drop_expr = DropTableExpression( dialect=dialect, - table_name=table_name, + table=table, if_exists=True, ) sql, params = drop_expr.to_sql() diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_orafce_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_orafce_integration.py index 0adf2157..59463877 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_orafce_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_orafce_integration.py @@ -68,7 +68,7 @@ def orafce_backend(postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=TABLE_NAME, + table=TABLE_NAME, columns=columns, if_not_exists=True, ) @@ -115,7 +115,7 @@ def orafce_backend(postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table_name=TABLE_NAME, + table=TABLE_NAME, if_exists=True, ) sql, params = drop_expr.to_sql() @@ -329,7 +329,7 @@ async def async_orafce_backend(async_postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=ASYNC_TABLE_NAME, + table=ASYNC_TABLE_NAME, columns=columns, if_not_exists=True, ) @@ -376,7 +376,7 @@ async def async_orafce_backend(async_postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table_name=ASYNC_TABLE_NAME, + table=ASYNC_TABLE_NAME, if_exists=True, ) sql, params = drop_expr.to_sql() diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pg_trgm_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pg_trgm_integration.py index 2c2c0c49..29c0be98 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pg_trgm_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pg_trgm_integration.py @@ -51,7 +51,7 @@ TABLE_TRGM_WORD_ASYNC = "test_trgm_word_async" -def _setup_trgm_table(backend, dialect, table_name): +def _setup_trgm_table(backend, dialect, table): """Create and populate the test_trgm table using expressions.""" columns = [ ColumnDefinition( @@ -65,7 +65,7 @@ def _setup_trgm_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -80,7 +80,7 @@ def _setup_trgm_table(backend, dialect, table_name): ] insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["name"], source=ValuesSource(dialect, rows), ) @@ -88,7 +88,7 @@ def _setup_trgm_table(backend, dialect, table_name): backend.execute(sql, params) -def _setup_trgm_ops_table(backend, dialect, table_name): +def _setup_trgm_ops_table(backend, dialect, table): """Create and populate the test_trgm_ops table using expressions.""" columns = [ ColumnDefinition( @@ -102,7 +102,7 @@ def _setup_trgm_ops_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -116,7 +116,7 @@ def _setup_trgm_ops_table(backend, dialect, table_name): ] insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["name"], source=ValuesSource(dialect, rows), ) @@ -124,7 +124,7 @@ def _setup_trgm_ops_table(backend, dialect, table_name): backend.execute(sql, params) -def _setup_trgm_idx_table(backend, dialect, table_name): +def _setup_trgm_idx_table(backend, dialect, table): """Create test_trgm_idx table, GIN index with gin_trgm_ops, and insert data. Note: GIN index with gin_trgm_ops opclass is created via raw SQL because @@ -143,7 +143,7 @@ def _setup_trgm_idx_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -153,7 +153,7 @@ def _setup_trgm_idx_table(backend, dialect, table_name): # Create GIN index with gin_trgm_ops opclass via raw SQL # (expression system lacks structural support for opclass specification) backend.execute( - f"CREATE INDEX IF NOT EXISTS idx_trgm_name ON {table_name} USING gin (name gin_trgm_ops)" + f"CREATE INDEX IF NOT EXISTS idx_trgm_name ON {table} USING gin (name gin_trgm_ops)" ) rows = [ @@ -161,7 +161,7 @@ def _setup_trgm_idx_table(backend, dialect, table_name): ] insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["name"], source=ValuesSource(dialect, rows), ) @@ -169,7 +169,7 @@ def _setup_trgm_idx_table(backend, dialect, table_name): backend.execute(sql, params) -def _setup_trgm_word_table(backend, dialect, table_name): +def _setup_trgm_word_table(backend, dialect, table): """Create and populate the test_trgm_word table using expressions.""" columns = [ ColumnDefinition( @@ -183,7 +183,7 @@ def _setup_trgm_word_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -195,7 +195,7 @@ def _setup_trgm_word_table(backend, dialect, table_name): ] insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["description"], source=ValuesSource(dialect, rows), ) @@ -203,18 +203,18 @@ def _setup_trgm_word_table(backend, dialect, table_name): backend.execute(sql, params) -def _teardown_table(backend, dialect, table_name): +def _teardown_table(backend, dialect, table): """Drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table_name=table_name, + table=table, if_exists=True, ) sql, params = drop_expr.to_sql() backend.execute(sql, params) -async def _async_setup_trgm_table(backend, dialect, table_name): +async def _async_setup_trgm_table(backend, dialect, table): """Async: create and populate the test_trgm table using expressions.""" columns = [ ColumnDefinition( @@ -228,7 +228,7 @@ async def _async_setup_trgm_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -243,7 +243,7 @@ async def _async_setup_trgm_table(backend, dialect, table_name): ] insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["name"], source=ValuesSource(dialect, rows), ) @@ -251,7 +251,7 @@ async def _async_setup_trgm_table(backend, dialect, table_name): await backend.execute(sql, params) -async def _async_setup_trgm_ops_table(backend, dialect, table_name): +async def _async_setup_trgm_ops_table(backend, dialect, table): """Async: create and populate the test_trgm_ops table using expressions.""" columns = [ ColumnDefinition( @@ -265,7 +265,7 @@ async def _async_setup_trgm_ops_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -279,7 +279,7 @@ async def _async_setup_trgm_ops_table(backend, dialect, table_name): ] insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["name"], source=ValuesSource(dialect, rows), ) @@ -287,7 +287,7 @@ async def _async_setup_trgm_ops_table(backend, dialect, table_name): await backend.execute(sql, params) -async def _async_setup_trgm_idx_table(backend, dialect, table_name): +async def _async_setup_trgm_idx_table(backend, dialect, table): """Async: create test_trgm_idx table, GIN index with gin_trgm_ops, and insert data. Note: GIN index with gin_trgm_ops opclass is created via raw SQL because @@ -306,7 +306,7 @@ async def _async_setup_trgm_idx_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -316,7 +316,7 @@ async def _async_setup_trgm_idx_table(backend, dialect, table_name): # Create GIN index with gin_trgm_ops opclass via raw SQL # (expression system lacks structural support for opclass specification) await backend.execute( - f"CREATE INDEX IF NOT EXISTS idx_trgm_name_async ON {table_name} USING gin (name gin_trgm_ops)" + f"CREATE INDEX IF NOT EXISTS idx_trgm_name_async ON {table} USING gin (name gin_trgm_ops)" ) rows = [ @@ -324,7 +324,7 @@ async def _async_setup_trgm_idx_table(backend, dialect, table_name): ] insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["name"], source=ValuesSource(dialect, rows), ) @@ -332,7 +332,7 @@ async def _async_setup_trgm_idx_table(backend, dialect, table_name): await backend.execute(sql, params) -async def _async_setup_trgm_word_table(backend, dialect, table_name): +async def _async_setup_trgm_word_table(backend, dialect, table): """Async: create and populate the test_trgm_word table using expressions.""" columns = [ ColumnDefinition( @@ -346,7 +346,7 @@ async def _async_setup_trgm_word_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -358,7 +358,7 @@ async def _async_setup_trgm_word_table(backend, dialect, table_name): ] insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["description"], source=ValuesSource(dialect, rows), ) @@ -366,11 +366,11 @@ async def _async_setup_trgm_word_table(backend, dialect, table_name): await backend.execute(sql, params) -async def _async_teardown_table(backend, dialect, table_name): +async def _async_teardown_table(backend, dialect, table): """Async: drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table_name=table_name, + table=table, if_exists=True, ) sql, params = drop_expr.to_sql() @@ -388,8 +388,8 @@ def pg_trgm_env(postgres_backend_single): dialect = backend.dialect # Clean up residual tables from previous runs - for table_name in [TABLE_TRGM, TABLE_TRGM_OPS, TABLE_TRGM_IDX, TABLE_TRGM_WORD]: - _teardown_table(backend, dialect, table_name) + for table in [TABLE_TRGM, TABLE_TRGM_OPS, TABLE_TRGM_IDX, TABLE_TRGM_WORD]: + _teardown_table(backend, dialect, table) _setup_trgm_table(backend, dialect, TABLE_TRGM) _setup_trgm_ops_table(backend, dialect, TABLE_TRGM_OPS) @@ -632,8 +632,8 @@ async def async_pg_trgm_env(async_postgres_backend_single): dialect = backend.dialect # Clean up residual tables from previous runs - for table_name in [TABLE_TRGM_ASYNC, TABLE_TRGM_OPS_ASYNC, TABLE_TRGM_IDX_ASYNC, TABLE_TRGM_WORD_ASYNC]: - await _async_teardown_table(backend, dialect, table_name) + for table in [TABLE_TRGM_ASYNC, TABLE_TRGM_OPS_ASYNC, TABLE_TRGM_IDX_ASYNC, TABLE_TRGM_WORD_ASYNC]: + await _async_teardown_table(backend, dialect, table) await _async_setup_trgm_table(backend, dialect, TABLE_TRGM_ASYNC) await _async_setup_trgm_ops_table(backend, dialect, TABLE_TRGM_OPS_ASYNC) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgcrypto_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgcrypto_integration.py index 38768005..5a6e331f 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgcrypto_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgcrypto_integration.py @@ -47,7 +47,7 @@ TABLE_ENCRYPT_ASYNC = "test_pgcrypto_encrypt_async" -def _setup_users_table(backend, dialect, table_name): +def _setup_users_table(backend, dialect, table): """Create and populate the test_pgcrypto_users table using expressions.""" columns = [ ColumnDefinition( @@ -62,7 +62,7 @@ def _setup_users_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -80,7 +80,7 @@ def _setup_users_table(backend, dialect, table_name): ] insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["username", "password_hash"], source=ValuesSource(dialect, rows), ) @@ -88,7 +88,7 @@ def _setup_users_table(backend, dialect, table_name): backend.execute(sql, params) -def _setup_encrypt_table(backend, dialect, table_name): +def _setup_encrypt_table(backend, dialect, table): """Create and populate the test_pgcrypto_encrypt table using expressions.""" columns = [ ColumnDefinition( @@ -102,7 +102,7 @@ def _setup_encrypt_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -120,7 +120,7 @@ def _setup_encrypt_table(backend, dialect, table_name): ] insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["secret_data"], source=ValuesSource(dialect, rows), ) @@ -128,18 +128,18 @@ def _setup_encrypt_table(backend, dialect, table_name): backend.execute(sql, params) -def _teardown_table(backend, dialect, table_name): +def _teardown_table(backend, dialect, table): """Drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table_name=table_name, + table=table, if_exists=True, ) sql, params = drop_expr.to_sql() backend.execute(sql, params) -async def _async_setup_users_table(backend, dialect, table_name): +async def _async_setup_users_table(backend, dialect, table): """Async: create and populate the test_pgcrypto_users table using expressions.""" columns = [ ColumnDefinition( @@ -154,7 +154,7 @@ async def _async_setup_users_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -172,7 +172,7 @@ async def _async_setup_users_table(backend, dialect, table_name): ] insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["username", "password_hash"], source=ValuesSource(dialect, rows), ) @@ -180,7 +180,7 @@ async def _async_setup_users_table(backend, dialect, table_name): await backend.execute(sql, params) -async def _async_setup_encrypt_table(backend, dialect, table_name): +async def _async_setup_encrypt_table(backend, dialect, table): """Async: create and populate the test_pgcrypto_encrypt table using expressions.""" columns = [ ColumnDefinition( @@ -194,7 +194,7 @@ async def _async_setup_encrypt_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -212,7 +212,7 @@ async def _async_setup_encrypt_table(backend, dialect, table_name): ] insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["secret_data"], source=ValuesSource(dialect, rows), ) @@ -220,11 +220,11 @@ async def _async_setup_encrypt_table(backend, dialect, table_name): await backend.execute(sql, params) -async def _async_teardown_table(backend, dialect, table_name): +async def _async_teardown_table(backend, dialect, table): """Async: drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table_name=table_name, + table=table, if_exists=True, ) sql, params = drop_expr.to_sql() @@ -242,8 +242,8 @@ def pgcrypto_env(postgres_backend_single): dialect = backend.dialect # Clean up residual tables from previous runs - for table_name in [TABLE_USERS, TABLE_ENCRYPT]: - _teardown_table(backend, dialect, table_name) + for table in [TABLE_USERS, TABLE_ENCRYPT]: + _teardown_table(backend, dialect, table) _setup_users_table(backend, dialect, TABLE_USERS) _setup_encrypt_table(backend, dialect, TABLE_ENCRYPT) @@ -504,8 +504,8 @@ async def async_pgcrypto_env(async_postgres_backend_single): dialect = backend.dialect # Clean up residual tables from previous runs - for table_name in [TABLE_USERS_ASYNC, TABLE_ENCRYPT_ASYNC]: - await _async_teardown_table(backend, dialect, table_name) + for table in [TABLE_USERS_ASYNC, TABLE_ENCRYPT_ASYNC]: + await _async_teardown_table(backend, dialect, table) await _async_setup_users_table(backend, dialect, TABLE_USERS_ASYNC) await _async_setup_encrypt_table(backend, dialect, TABLE_ENCRYPT_ASYNC) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgrouting_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgrouting_integration.py index f454cabe..4452dc02 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgrouting_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgrouting_integration.py @@ -59,7 +59,7 @@ def pgrouting_env(postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table_name="test_route_edges", + table="test_route_edges", columns=columns, if_not_exists=True, ) @@ -108,7 +108,7 @@ def pgrouting_env(postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table_name="test_route_edges", + table="test_route_edges", if_exists=True, ) sql, params = drop_expr.to_sql() @@ -188,7 +188,7 @@ async def async_pgrouting_env(async_postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table_name="test_route_edges_async", + table="test_route_edges_async", columns=columns, if_not_exists=True, ) @@ -237,7 +237,7 @@ async def async_pgrouting_env(async_postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table_name="test_route_edges_async", + table="test_route_edges_async", if_exists=True, ) sql, params = drop_expr.to_sql() diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgvector_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgvector_integration.py index 53b61de1..058b9f4b 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgvector_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgvector_integration.py @@ -72,10 +72,10 @@ def vector_env(postgres_backend_single): ] # Clean up any leftover tables from prior runs - for table_name in table_names: + for table in table_names: drop_expr = DropTableExpression( dialect=dialect, - table_name=table_name, + table=table, if_exists=True, ) sql, params = drop_expr.to_sql() @@ -84,7 +84,7 @@ def vector_env(postgres_backend_single): # --- test_vector_items --- create_items = CreateTableExpression( dialect=dialect, - table_name="test_vector_items", + table="test_vector_items", columns=[_id_column(), _embedding_column()], ) sql, params = create_items.to_sql() @@ -108,7 +108,7 @@ def vector_env(postgres_backend_single): # --- test_vector_l2 --- create_l2 = CreateTableExpression( dialect=dialect, - table_name="test_vector_l2", + table="test_vector_l2", columns=[_id_column(), _embedding_column()], ) sql, params = create_l2.to_sql() @@ -133,7 +133,7 @@ def vector_env(postgres_backend_single): # --- test_vector_cosine --- create_cosine = CreateTableExpression( dialect=dialect, - table_name="test_vector_cosine", + table="test_vector_cosine", columns=[_id_column(), _embedding_column()], ) sql, params = create_cosine.to_sql() @@ -158,7 +158,7 @@ def vector_env(postgres_backend_single): # --- test_vector_ip --- create_ip = CreateTableExpression( dialect=dialect, - table_name="test_vector_ip", + table="test_vector_ip", columns=[_id_column(), _embedding_column()], ) sql, params = create_ip.to_sql() @@ -182,7 +182,7 @@ def vector_env(postgres_backend_single): yield backend, dialect # Teardown: drop tables - for table_name in [ + for table in [ "test_vector_items", "test_vector_l2", "test_vector_cosine", @@ -190,7 +190,7 @@ def vector_env(postgres_backend_single): ]: drop_expr = DropTableExpression( dialect=dialect, - table_name=table_name, + table=table, if_exists=True, ) sql, params = drop_expr.to_sql() @@ -311,7 +311,7 @@ def test_vector_update(self, vector_env): # Create temporary table create_expr = CreateTableExpression( dialect=dialect, - table_name="test_vector_update", + table="test_vector_update", columns=[_id_column(), _embedding_column()], ) sql, params = create_expr.to_sql() @@ -356,7 +356,7 @@ def test_vector_update(self, vector_env): finally: drop_expr = DropTableExpression( dialect=dialect, - table_name="test_vector_update", + table="test_vector_update", if_exists=True, ) sql, params = drop_expr.to_sql() @@ -369,7 +369,7 @@ def test_ivfflat_index(self, vector_env): # Create table for IVFFlat index test create_expr = CreateTableExpression( dialect=dialect, - table_name="test_vector_ivfflat", + table="test_vector_ivfflat", columns=[_id_column(), _embedding_column()], ) sql, params = create_expr.to_sql() @@ -413,7 +413,7 @@ def test_ivfflat_index(self, vector_env): finally: drop_expr = DropTableExpression( dialect=dialect, - table_name="test_vector_ivfflat", + table="test_vector_ivfflat", if_exists=True, ) sql, params = drop_expr.to_sql() @@ -430,7 +430,7 @@ def test_hnsw_index(self, vector_env): # Create table for HNSW index test create_expr = CreateTableExpression( dialect=dialect, - table_name="test_vector_hnsw", + table="test_vector_hnsw", columns=[_id_column(), _embedding_column()], ) sql, params = create_expr.to_sql() @@ -474,7 +474,7 @@ def test_hnsw_index(self, vector_env): finally: drop_expr = DropTableExpression( dialect=dialect, - table_name="test_vector_hnsw", + table="test_vector_hnsw", if_exists=True, ) sql, params = drop_expr.to_sql() @@ -487,7 +487,7 @@ def test_vector_dimension_constraint(self, vector_env): # Create table with VECTOR(3) dimension constraint create_expr = CreateTableExpression( dialect=dialect, - table_name="test_vector_dim", + table="test_vector_dim", columns=[_id_column(), _embedding_column()], ) sql, params = create_expr.to_sql() @@ -510,7 +510,7 @@ def test_vector_dimension_constraint(self, vector_env): finally: drop_expr = DropTableExpression( dialect=dialect, - table_name="test_vector_dim", + table="test_vector_dim", if_exists=True, ) sql, params = drop_expr.to_sql() @@ -535,10 +535,10 @@ async def async_vector_env(async_postgres_backend_single): ] # Clean up any leftover tables from prior runs - for table_name in async_table_names: + for table in async_table_names: drop_expr = DropTableExpression( dialect=dialect, - table_name=table_name, + table=table, if_exists=True, ) sql, params = drop_expr.to_sql() @@ -547,7 +547,7 @@ async def async_vector_env(async_postgres_backend_single): # --- test_vector_items_async --- create_items = CreateTableExpression( dialect=dialect, - table_name="test_vector_items_async", + table="test_vector_items_async", columns=[_id_column(), _embedding_column()], ) sql, params = create_items.to_sql() @@ -571,7 +571,7 @@ async def async_vector_env(async_postgres_backend_single): # --- test_vector_l2_async --- create_l2 = CreateTableExpression( dialect=dialect, - table_name="test_vector_l2_async", + table="test_vector_l2_async", columns=[_id_column(), _embedding_column()], ) sql, params = create_l2.to_sql() @@ -596,7 +596,7 @@ async def async_vector_env(async_postgres_backend_single): # --- test_vector_cosine_async --- create_cosine = CreateTableExpression( dialect=dialect, - table_name="test_vector_cosine_async", + table="test_vector_cosine_async", columns=[_id_column(), _embedding_column()], ) sql, params = create_cosine.to_sql() @@ -621,7 +621,7 @@ async def async_vector_env(async_postgres_backend_single): # --- test_vector_ip_async --- create_ip = CreateTableExpression( dialect=dialect, - table_name="test_vector_ip_async", + table="test_vector_ip_async", columns=[_id_column(), _embedding_column()], ) sql, params = create_ip.to_sql() @@ -645,7 +645,7 @@ async def async_vector_env(async_postgres_backend_single): yield backend, dialect # Teardown: drop tables - for table_name in [ + for table in [ "test_vector_items_async", "test_vector_l2_async", "test_vector_cosine_async", @@ -653,7 +653,7 @@ async def async_vector_env(async_postgres_backend_single): ]: drop_expr = DropTableExpression( dialect=dialect, - table_name=table_name, + table=table, if_exists=True, ) sql, params = drop_expr.to_sql() @@ -772,7 +772,7 @@ async def test_async_vector_update(self, async_vector_env): create_expr = CreateTableExpression( dialect=dialect, - table_name="test_vector_update_async", + table="test_vector_update_async", columns=[_id_column(), _embedding_column()], ) sql, params = create_expr.to_sql() @@ -814,7 +814,7 @@ async def test_async_vector_update(self, async_vector_env): finally: drop_expr = DropTableExpression( dialect=dialect, - table_name="test_vector_update_async", + table="test_vector_update_async", if_exists=True, ) sql, params = drop_expr.to_sql() @@ -827,7 +827,7 @@ async def test_async_ivfflat_index(self, async_vector_env): create_expr = CreateTableExpression( dialect=dialect, - table_name="test_vector_ivfflat_async", + table="test_vector_ivfflat_async", columns=[_id_column(), _embedding_column()], ) sql, params = create_expr.to_sql() @@ -869,7 +869,7 @@ async def test_async_ivfflat_index(self, async_vector_env): finally: drop_expr = DropTableExpression( dialect=dialect, - table_name="test_vector_ivfflat_async", + table="test_vector_ivfflat_async", if_exists=True, ) sql, params = drop_expr.to_sql() @@ -885,7 +885,7 @@ async def test_async_hnsw_index(self, async_vector_env): create_expr = CreateTableExpression( dialect=dialect, - table_name="test_vector_hnsw_async", + table="test_vector_hnsw_async", columns=[_id_column(), _embedding_column()], ) sql, params = create_expr.to_sql() @@ -927,7 +927,7 @@ async def test_async_hnsw_index(self, async_vector_env): finally: drop_expr = DropTableExpression( dialect=dialect, - table_name="test_vector_hnsw_async", + table="test_vector_hnsw_async", if_exists=True, ) sql, params = drop_expr.to_sql() @@ -940,7 +940,7 @@ async def test_async_vector_dimension_constraint(self, async_vector_env): create_expr = CreateTableExpression( dialect=dialect, - table_name="test_vector_dim_async", + table="test_vector_dim_async", columns=[_id_column(), _embedding_column()], ) sql, params = create_expr.to_sql() @@ -962,7 +962,7 @@ async def test_async_vector_dimension_constraint(self, async_vector_env): finally: drop_expr = DropTableExpression( dialect=dialect, - table_name="test_vector_dim_async", + table="test_vector_dim_async", if_exists=True, ) sql, params = drop_expr.to_sql() diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_integration.py index a858e112..aefaf055 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_integration.py @@ -38,7 +38,7 @@ # --- Helper functions --- -def _setup_geo_points_table(backend, dialect, table_name): +def _setup_geo_points_table(backend, dialect, table): """Create and populate the test_geo_points table using expressions.""" columns = [ ColumnDefinition( @@ -52,7 +52,7 @@ def _setup_geo_points_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -61,7 +61,7 @@ def _setup_geo_points_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["geom"], source=ValuesSource( dialect, @@ -75,7 +75,7 @@ def _setup_geo_points_table(backend, dialect, table_name): backend.execute(sql, params) -def _setup_geo_distance_table(backend, dialect, table_name): +def _setup_geo_distance_table(backend, dialect, table): """Create and populate the test_geo_distance table using expressions.""" columns = [ ColumnDefinition( @@ -89,7 +89,7 @@ def _setup_geo_distance_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -98,7 +98,7 @@ def _setup_geo_distance_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["geom"], source=ValuesSource( dialect, @@ -112,7 +112,7 @@ def _setup_geo_distance_table(backend, dialect, table_name): backend.execute(sql, params) -def _setup_geo_contains_table(backend, dialect, table_name): +def _setup_geo_contains_table(backend, dialect, table): """Create and populate the test_geo_contains table using expressions.""" columns = [ ColumnDefinition( @@ -126,7 +126,7 @@ def _setup_geo_contains_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -135,7 +135,7 @@ def _setup_geo_contains_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["geom"], source=ValuesSource( dialect, @@ -155,7 +155,7 @@ def _setup_geo_contains_table(backend, dialect, table_name): backend.execute(sql, params) -def _setup_geo_index_table(backend, dialect, table_name, index_name): +def _setup_geo_index_table(backend, dialect, table, index_name): """Create and populate the test_geo_index table with GiST index using expressions.""" columns = [ ColumnDefinition( @@ -169,7 +169,7 @@ def _setup_geo_index_table(backend, dialect, table_name, index_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -178,7 +178,7 @@ def _setup_geo_index_table(backend, dialect, table_name, index_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["geom"], source=ValuesSource( dialect, @@ -193,7 +193,7 @@ def _setup_geo_index_table(backend, dialect, table_name, index_name): create_index = CreateIndexExpression( dialect=dialect, index_name=index_name, - table_name=table_name, + table_name=table, columns=["geom"], index_type="GIST", if_not_exists=True, @@ -202,7 +202,7 @@ def _setup_geo_index_table(backend, dialect, table_name, index_name): backend.execute(sql, params) -def _setup_geog_table(backend, dialect, table_name): +def _setup_geog_table(backend, dialect, table): """Create and populate the test_geog table using expressions.""" columns = [ ColumnDefinition( @@ -216,7 +216,7 @@ def _setup_geog_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -225,7 +225,7 @@ def _setup_geog_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["geog"], source=ValuesSource( dialect, @@ -238,18 +238,18 @@ def _setup_geog_table(backend, dialect, table_name): backend.execute(sql, params) -def _teardown_table(backend, dialect, table_name): +def _teardown_table(backend, dialect, table): """Drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table_name=table_name, + table=table, if_exists=True, ) sql, params = drop_expr.to_sql() backend.execute(sql, params) -async def _async_setup_geo_points_table(backend, dialect, table_name): +async def _async_setup_geo_points_table(backend, dialect, table): """Async: create and populate the test_geo_points table using expressions.""" columns = [ ColumnDefinition( @@ -263,7 +263,7 @@ async def _async_setup_geo_points_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -272,7 +272,7 @@ async def _async_setup_geo_points_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["geom"], source=ValuesSource( dialect, @@ -286,7 +286,7 @@ async def _async_setup_geo_points_table(backend, dialect, table_name): await backend.execute(sql, params) -async def _async_setup_geo_distance_table(backend, dialect, table_name): +async def _async_setup_geo_distance_table(backend, dialect, table): """Async: create and populate the test_geo_distance table using expressions.""" columns = [ ColumnDefinition( @@ -300,7 +300,7 @@ async def _async_setup_geo_distance_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -309,7 +309,7 @@ async def _async_setup_geo_distance_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["geom"], source=ValuesSource( dialect, @@ -323,7 +323,7 @@ async def _async_setup_geo_distance_table(backend, dialect, table_name): await backend.execute(sql, params) -async def _async_setup_geo_contains_table(backend, dialect, table_name): +async def _async_setup_geo_contains_table(backend, dialect, table): """Async: create and populate the test_geo_contains table using expressions.""" columns = [ ColumnDefinition( @@ -337,7 +337,7 @@ async def _async_setup_geo_contains_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -346,7 +346,7 @@ async def _async_setup_geo_contains_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["geom"], source=ValuesSource( dialect, @@ -366,7 +366,7 @@ async def _async_setup_geo_contains_table(backend, dialect, table_name): await backend.execute(sql, params) -async def _async_setup_geo_index_table(backend, dialect, table_name, index_name): +async def _async_setup_geo_index_table(backend, dialect, table, index_name): """Async: create and populate the test_geo_index table with GiST index using expressions.""" columns = [ ColumnDefinition( @@ -380,7 +380,7 @@ async def _async_setup_geo_index_table(backend, dialect, table_name, index_name) ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -389,7 +389,7 @@ async def _async_setup_geo_index_table(backend, dialect, table_name, index_name) insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["geom"], source=ValuesSource( dialect, @@ -404,7 +404,7 @@ async def _async_setup_geo_index_table(backend, dialect, table_name, index_name) create_index = CreateIndexExpression( dialect=dialect, index_name=index_name, - table_name=table_name, + table_name=table, columns=["geom"], index_type="GIST", if_not_exists=True, @@ -413,7 +413,7 @@ async def _async_setup_geo_index_table(backend, dialect, table_name, index_name) await backend.execute(sql, params) -async def _async_setup_geog_table(backend, dialect, table_name): +async def _async_setup_geog_table(backend, dialect, table): """Async: create and populate the test_geog table using expressions.""" columns = [ ColumnDefinition( @@ -427,7 +427,7 @@ async def _async_setup_geog_table(backend, dialect, table_name): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=table_name, + table=table, columns=columns, if_not_exists=True, ) @@ -436,7 +436,7 @@ async def _async_setup_geog_table(backend, dialect, table_name): insert_expr = InsertExpression( dialect=dialect, - into=table_name, + into=table, columns=["geog"], source=ValuesSource( dialect, @@ -449,11 +449,11 @@ async def _async_setup_geog_table(backend, dialect, table_name): await backend.execute(sql, params) -async def _async_teardown_table(backend, dialect, table_name): +async def _async_teardown_table(backend, dialect, table): """Async: drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table_name=table_name, + table=table, if_exists=True, ) sql, params = drop_expr.to_sql() @@ -488,8 +488,8 @@ def postgis_env(postgres_backend_single): dialect = backend.dialect # Clean up residual tables from previous runs - for table_name in [T_GEO_POINTS, T_GEO_DISTANCE, T_GEO_CONTAINS, T_GEO_INDEX, T_GEOG]: - _teardown_table(backend, dialect, table_name) + for table in [T_GEO_POINTS, T_GEO_DISTANCE, T_GEO_CONTAINS, T_GEO_INDEX, T_GEOG]: + _teardown_table(backend, dialect, table) _setup_geo_points_table(backend, dialect, T_GEO_POINTS) _setup_geo_distance_table(backend, dialect, T_GEO_DISTANCE) @@ -499,8 +499,8 @@ def postgis_env(postgres_backend_single): yield backend, dialect - for table_name in [T_GEO_POINTS, T_GEO_DISTANCE, T_GEO_CONTAINS, T_GEO_INDEX, T_GEOG]: - _teardown_table(backend, dialect, table_name) + for table in [T_GEO_POINTS, T_GEO_DISTANCE, T_GEO_CONTAINS, T_GEO_INDEX, T_GEOG]: + _teardown_table(backend, dialect, table) class TestPostGISIntegration: @@ -819,8 +819,8 @@ async def async_postgis_env(async_postgres_backend_single): dialect = backend.dialect # Clean up residual tables from previous runs - for table_name in [T_GEO_POINTS_ASYNC, T_GEO_DISTANCE_ASYNC, T_GEO_CONTAINS_ASYNC, T_GEO_INDEX_ASYNC, T_GEOG_ASYNC]: - await _async_teardown_table(backend, dialect, table_name) + for table in [T_GEO_POINTS_ASYNC, T_GEO_DISTANCE_ASYNC, T_GEO_CONTAINS_ASYNC, T_GEO_INDEX_ASYNC, T_GEOG_ASYNC]: + await _async_teardown_table(backend, dialect, table) await _async_setup_geo_points_table(backend, dialect, T_GEO_POINTS_ASYNC) await _async_setup_geo_distance_table(backend, dialect, T_GEO_DISTANCE_ASYNC) @@ -830,8 +830,8 @@ async def async_postgis_env(async_postgres_backend_single): yield backend, dialect - for table_name in [T_GEO_POINTS_ASYNC, T_GEO_DISTANCE_ASYNC, T_GEO_CONTAINS_ASYNC, T_GEO_INDEX_ASYNC, T_GEOG_ASYNC]: - await _async_teardown_table(backend, dialect, table_name) + for table in [T_GEO_POINTS_ASYNC, T_GEO_DISTANCE_ASYNC, T_GEO_CONTAINS_ASYNC, T_GEO_INDEX_ASYNC, T_GEOG_ASYNC]: + await _async_teardown_table(backend, dialect, table) class TestAsyncPostGISIntegration: diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_raster_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_raster_integration.py index 097ecda8..0f1fef5d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_raster_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_raster_integration.py @@ -93,7 +93,7 @@ def raster_env(postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=TABLE_NAME, + table=TABLE_NAME, columns=columns, if_not_exists=True, ) @@ -124,7 +124,7 @@ def raster_env(postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table_name=TABLE_NAME, + table=TABLE_NAME, if_exists=True, ) sql, params = drop_expr.to_sql() @@ -212,7 +212,7 @@ async def async_raster_env(async_postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table_name=ASYNC_TABLE_NAME, + table=ASYNC_TABLE_NAME, columns=columns, if_not_exists=True, ) @@ -243,7 +243,7 @@ async def async_raster_env(async_postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table_name=ASYNC_TABLE_NAME, + table=ASYNC_TABLE_NAME, if_exists=True, ) sql, params = drop_expr.to_sql() diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_tablefunc_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_tablefunc_integration.py index b2aa9d9d..b5e58b99 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_tablefunc_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_tablefunc_integration.py @@ -67,7 +67,7 @@ def tablefunc_env(postgres_backend_single): ] create_sales = CreateTableExpression( dialect=dialect, - table_name="test_tf_sales", + table="test_tf_sales", columns=sales_columns, if_not_exists=True, ) @@ -106,7 +106,7 @@ def tablefunc_env(postgres_backend_single): ] create_tree = CreateTableExpression( dialect=dialect, - table_name="test_tf_tree", + table="test_tf_tree", columns=tree_columns, if_not_exists=True, ) @@ -134,7 +134,7 @@ def tablefunc_env(postgres_backend_single): # Teardown: drop both tables using DropTableExpression drop_sales = DropTableExpression( dialect=dialect, - table_name="test_tf_sales", + table="test_tf_sales", if_exists=True, ) sql, params = drop_sales.to_sql() @@ -142,7 +142,7 @@ def tablefunc_env(postgres_backend_single): drop_tree = DropTableExpression( dialect=dialect, - table_name="test_tf_tree", + table="test_tf_tree", if_exists=True, ) sql, params = drop_tree.to_sql() @@ -258,7 +258,7 @@ def test_connectby_expression_generation(self, tablefunc_env): ) sql, params = connectby_func.to_sql() assert "CONNECTBY" in sql - # 5 args: table_name, key_column, parent_column, start_value, max_depth + # 5 args: table, key_column, parent_column, start_value, max_depth assert len(params) == 5 # Verify with branch_delim expression generation @@ -273,7 +273,7 @@ def test_connectby_expression_generation(self, tablefunc_env): ) sql2, params2 = connectby_func2.to_sql() assert "CONNECTBY" in sql2 - # 6 args: table_name, key_column, parent_column, start_value, max_depth, branch_delim + # 6 args: table, key_column, parent_column, start_value, max_depth, branch_delim assert len(params2) == 6 def test_connectby_execution(self, tablefunc_env): @@ -345,7 +345,7 @@ async def async_tablefunc_env(async_postgres_backend_single): ] create_sales = CreateTableExpression( dialect=dialect, - table_name="test_tf_sales_async", + table="test_tf_sales_async", columns=sales_columns, if_not_exists=True, ) @@ -384,7 +384,7 @@ async def async_tablefunc_env(async_postgres_backend_single): ] create_tree = CreateTableExpression( dialect=dialect, - table_name="test_tf_tree_async", + table="test_tf_tree_async", columns=tree_columns, if_not_exists=True, ) @@ -412,7 +412,7 @@ async def async_tablefunc_env(async_postgres_backend_single): # Teardown: drop both tables using DropTableExpression drop_sales = DropTableExpression( dialect=dialect, - table_name="test_tf_sales_async", + table="test_tf_sales_async", if_exists=True, ) sql, params = drop_sales.to_sql() @@ -420,7 +420,7 @@ async def async_tablefunc_env(async_postgres_backend_single): drop_tree = DropTableExpression( dialect=dialect, - table_name="test_tf_tree_async", + table="test_tf_tree_async", if_exists=True, ) sql, params = drop_tree.to_sql() @@ -541,7 +541,7 @@ async def test_async_connectby_expression_generation(self, async_tablefunc_env): ) sql, params = connectby_func.to_sql() assert "CONNECTBY" in sql - # 5 args: table_name, key_column, parent_column, start_value, max_depth + # 5 args: table, key_column, parent_column, start_value, max_depth assert len(params) == 5 # Verify with branch_delim expression generation @@ -556,7 +556,7 @@ async def test_async_connectby_expression_generation(self, async_tablefunc_env): ) sql2, params2 = connectby_func2.to_sql() assert "CONNECTBY" in sql2 - # 6 args: table_name, key_column, parent_column, start_value, max_depth, branch_delim + # 6 args: table, key_column, parent_column, start_value, max_depth, branch_delim assert len(params2) == 6 @pytest.mark.asyncio diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_uuid_ossp_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_uuid_ossp_integration.py index a851dcaf..9f409c00 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_uuid_ossp_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_uuid_ossp_integration.py @@ -59,15 +59,15 @@ def uuid_ossp_env(postgres_backend_single): dialect = backend.dialect # Clean up residual tables from previous runs - for table_name in ["test_uuid_ossp", "test_uuid_explicit"]: - drop_expr = DropTableExpression(dialect=dialect, table_name=table_name, if_exists=True) + for table in ["test_uuid_ossp", "test_uuid_explicit"]: + drop_expr = DropTableExpression(dialect=dialect, table=table, if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) # Setup: create test_uuid_ossp table (with DEFAULT uuid_generate_v4()) create_ossp = CreateTableExpression( dialect=dialect, - table_name="test_uuid_ossp", + table="test_uuid_ossp", columns=[ ColumnDefinition( name="id", @@ -103,7 +103,7 @@ def uuid_ossp_env(postgres_backend_single): # Setup: create test_uuid_explicit table (no DEFAULT) create_explicit = CreateTableExpression( dialect=dialect, - table_name="test_uuid_explicit", + table="test_uuid_explicit", columns=[ ColumnDefinition( name="id", @@ -145,10 +145,10 @@ def uuid_ossp_env(postgres_backend_single): yield backend, dialect # Teardown: drop tables - for table_name in ["test_uuid_ossp", "test_uuid_explicit"]: + for table in ["test_uuid_ossp", "test_uuid_explicit"]: drop_expr = DropTableExpression( dialect=dialect, - table_name=table_name, + table=table, if_exists=True, ) sql, params = drop_expr.to_sql() @@ -264,15 +264,15 @@ async def async_uuid_ossp_env(async_postgres_backend_single): dialect = backend.dialect # Clean up residual tables from previous runs - for table_name in ["test_uuid_ossp_async", "test_uuid_explicit_async"]: - drop_expr = DropTableExpression(dialect=dialect, table_name=table_name, if_exists=True) + for table in ["test_uuid_ossp_async", "test_uuid_explicit_async"]: + drop_expr = DropTableExpression(dialect=dialect, table=table, if_exists=True) sql, params = drop_expr.to_sql() await backend.execute(sql, params) # Setup: create test_uuid_ossp_async table (with DEFAULT uuid_generate_v4()) create_ossp = CreateTableExpression( dialect=dialect, - table_name="test_uuid_ossp_async", + table="test_uuid_ossp_async", columns=[ ColumnDefinition( name="id", @@ -308,7 +308,7 @@ async def async_uuid_ossp_env(async_postgres_backend_single): # Setup: create test_uuid_explicit_async table (no DEFAULT) create_explicit = CreateTableExpression( dialect=dialect, - table_name="test_uuid_explicit_async", + table="test_uuid_explicit_async", columns=[ ColumnDefinition( name="id", @@ -350,10 +350,10 @@ async def async_uuid_ossp_env(async_postgres_backend_single): yield backend, dialect # Teardown: drop tables - for table_name in ["test_uuid_ossp_async", "test_uuid_explicit_async"]: + for table in ["test_uuid_ossp_async", "test_uuid_explicit_async"]: drop_expr = DropTableExpression( dialect=dialect, - table_name=table_name, + table=table, if_exists=True, ) sql, params = drop_expr.to_sql() diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_create_table_like.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_create_table_like.py index 5841a020..2b5506c2 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_create_table_like.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_create_table_like.py @@ -19,7 +19,7 @@ def test_basic_like_syntax(self): dialect = PostgresDialect() create_expr = CreateTableExpression( dialect=dialect, - table_name="users_copy", + table="users_copy", columns=[], dialect_options={'like_table': 'users'} ) @@ -33,7 +33,7 @@ def test_like_with_including_defaults(self): dialect = PostgresDialect() create_expr = CreateTableExpression( dialect=dialect, - table_name="users_copy", + table="users_copy", columns=[], dialect_options={ 'like_table': 'users', @@ -50,7 +50,7 @@ def test_like_with_including_constraints(self): dialect = PostgresDialect() create_expr = CreateTableExpression( dialect=dialect, - table_name="users_copy", + table="users_copy", columns=[], dialect_options={ 'like_table': 'users', @@ -67,7 +67,7 @@ def test_like_with_including_indexes(self): dialect = PostgresDialect() create_expr = CreateTableExpression( dialect=dialect, - table_name="users_copy", + table="users_copy", columns=[], dialect_options={ 'like_table': 'users', @@ -84,7 +84,7 @@ def test_like_with_multiple_including_options(self): dialect = PostgresDialect() create_expr = CreateTableExpression( dialect=dialect, - table_name="users_copy", + table="users_copy", columns=[], dialect_options={ 'like_table': 'users', @@ -103,7 +103,7 @@ def test_like_with_including_and_excluding_options(self): dialect = PostgresDialect() create_expr = CreateTableExpression( dialect=dialect, - table_name="users_copy", + table="users_copy", columns=[], dialect_options={ 'like_table': 'users', @@ -123,7 +123,7 @@ def test_like_with_list_format_options(self): dialect = PostgresDialect() create_expr = CreateTableExpression( dialect=dialect, - table_name="users_copy", + table="users_copy", columns=[], dialect_options={ 'like_table': 'users', @@ -141,7 +141,7 @@ def test_like_with_tuple_format_options(self): dialect = PostgresDialect() create_expr = CreateTableExpression( dialect=dialect, - table_name="users_copy", + table="users_copy", columns=[], dialect_options={ 'like_table': 'users', @@ -161,7 +161,7 @@ def test_like_with_if_not_exists(self): dialect = PostgresDialect() create_expr = CreateTableExpression( dialect=dialect, - table_name="users_copy", + table="users_copy", columns=[], if_not_exists=True, dialect_options={'like_table': 'users'} @@ -176,7 +176,7 @@ def test_like_with_temporary(self): dialect = PostgresDialect() create_expr = CreateTableExpression( dialect=dialect, - table_name="temp_users", + table="temp_users", columns=[], temporary=True, dialect_options={'like_table': 'users'} @@ -191,7 +191,7 @@ def test_like_with_schema_qualified_table(self): dialect = PostgresDialect() create_expr = CreateTableExpression( dialect=dialect, - table_name="users_copy", + table="users_copy", columns=[], dialect_options={'like_table': ('public', 'users')} ) @@ -211,7 +211,7 @@ def test_like_ignores_columns(self): ] create_expr = CreateTableExpression( dialect=dialect, - table_name="users_copy", + table="users_copy", columns=columns, dialect_options={'like_table': 'users'} ) @@ -226,7 +226,7 @@ def test_like_with_temporary_and_if_not_exists(self): dialect = PostgresDialect() create_expr = CreateTableExpression( dialect=dialect, - table_name="temp_users_copy", + table="temp_users_copy", columns=[], temporary=True, if_not_exists=True, @@ -242,7 +242,7 @@ def test_like_with_all_option(self): dialect = PostgresDialect() create_expr = CreateTableExpression( dialect=dialect, - table_name="users_copy", + table="users_copy", columns=[], dialect_options={ 'like_table': 'users', @@ -267,7 +267,7 @@ def test_fallback_to_base_when_no_like(self): ] create_expr = CreateTableExpression( dialect=dialect, - table_name="users", + table="users", columns=columns ) sql, params = create_expr.to_sql() From fabadd16647c58514437339293d5b118d03e708c Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Fri, 15 May 2026 09:43:54 +0800 Subject: [PATCH 07/12] ci: enable scenario-parallel mode with 2 workers in test workflow --- .github/workflows/test.yml | 4 +- tests/conftest.py | 128 ++++++++++ .../feature/backend/introspection/conftest.py | 240 +++++------------- 3 files changed, 190 insertions(+), 182 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a3213e56..dd64638e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -123,14 +123,14 @@ jobs: if: matrix.postgres-version != '18' run: | export PYTHONPATH=src:tests - python -m pytest tests/ --tb=short + python -m pytest tests/ --tb=short --scenario-parallel -n 2 --dist=loadgroup - name: Run tests with coverage (only for postgres 18) if: matrix.postgres-version == '18' run: | export PYTHONPATH=src:tests pip install codecov - coverage run --source=src -m pytest tests/ --tb=short + coverage run --source=src -m pytest tests/ --tb=short --scenario-parallel -n 2 --dist=loadgroup coverage xml - name: Upload coverage to Codecov diff --git a/tests/conftest.py b/tests/conftest.py index 1c1a1c2a..6abfccdc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,8 @@ import os import sys +import pytest + def ensure_compatible_event_loop(): if sys.platform == "win32": @@ -32,3 +34,129 @@ def ensure_compatible_event_loop(): 'TESTSUITE_PROVIDER_REGISTRY', 'providers.registry:provider_registry' ) + +# ============================================================================= +# Scenario Parallel Scheduling Plugin +# +# Usage: +# pytest --scenario-parallel -n --dist=loadgroup tests/ +# +# Design: All tests for the same scenario are pinned to the same xdist worker +# (via nodeid @suffix), while tests for different scenarios run in +# parallel. Behavior is unchanged without --scenario-parallel. +# ============================================================================= + + +def _get_scenario_names(): + """Lazy import to avoid side effects during conftest loading.""" + try: + from providers.scenarios import SCENARIO_MAP + return set(SCENARIO_MAP.keys()), list(SCENARIO_MAP.keys()) + except Exception: + return set(), [] + + +def _extract_scenario_from_item(item, scenario_name_set): + """Extract scenario name from item's callspec. + + Returns: + str: scenario name when exactly one scenario param is found + None: no scenario params (not a scenario-parametrized test) + list: multiple distinct scenario names (cross-scenario test) + """ + callspec = getattr(item, 'callspec', None) + if callspec is None: + return None + scenario_values = [ + v for v in callspec.params.values() + if isinstance(v, str) and v in scenario_name_set + ] + if len(scenario_values) == 1: + return scenario_values[0] + if len(scenario_values) >= 2: + return scenario_values # cross-scenario: caller checks isinstance(result, list) + return None + + +def _add_xdist_group_marker(item, group_name): + """Append @group_name suffix to item._nodeid for loadgroup scheduling.""" + suffix = f"@{group_name}" + if suffix not in item.nodeid: + item._nodeid = item.nodeid + suffix + + +def pytest_addoption(parser): + parser.addoption( + '--scenario-parallel', + action='store_true', + default=False, + help='Scenario parallel mode: distribute scenarios across workers, keep each scenario on one worker.', + ) + + +def pytest_configure(config): + config.addinivalue_line( + "markers", + "xdist_group(name): specify the xdist group for a test (provided by pytest-xdist).", + ) + + +def pytest_collection_modifyitems(config, items): + if not config.getoption('--scenario-parallel', default=False): + return + + scenario_name_set, scenario_name_list = _get_scenario_names() + if not scenario_name_set: + return + + scenario_items = [] + cross_scenario_items = [] + normal_items = [] + + for item in items: + result = _extract_scenario_from_item(item, scenario_name_set) + if isinstance(result, list): + item.add_marker( + pytest.mark.skip( + reason="Cross-scenario test skipped in --scenario-parallel mode. " + "Run without --scenario-parallel for serial execution." + ) + ) + cross_scenario_items.append(item) + elif isinstance(result, str): + _add_xdist_group_marker(item, result) + scenario_items.append(item) + else: + normal_items.append(item) + + def sort_key(item): + result = _extract_scenario_from_item(item, scenario_name_set) + if not isinstance(result, str): + return ('~', 0) + try: + scenario_idx = scenario_name_list.index(result) + except ValueError: + scenario_idx = 0 + base = item.nodeid.split('[')[0] if '[' in item.nodeid else item.nodeid + return (base, scenario_idx) + + first_scenario = scenario_name_list[0] + for item in normal_items: + _add_xdist_group_marker(item, first_scenario) + scenario_items.extend(normal_items) + normal_items = [] + + scenario_items.sort(key=sort_key) + items[:] = scenario_items + normal_items + cross_scenario_items + + groups: dict = {} + for item in scenario_items: + sn = _extract_scenario_from_item(item, scenario_name_set) + if isinstance(sn, str): + base = item.nodeid.split('[')[0] if '[' in item.nodeid else item.nodeid + groups.setdefault(base, set()).add(sn) + + print(f"\n[ScenarioParallel] {len(items)} items: {len(scenario_items)} scenario-param + " + f"{len(normal_items)} normal + {len(cross_scenario_items)} cross-scenario (skipped)") + print(f"[ScenarioParallel] {len(groups)} test methods, {len(scenario_name_list)} scenarios in parallel") + print(f"[ScenarioParallel] Suggested: --dist=loadgroup -n {len(scenario_name_list)}") diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/introspection/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/introspection/conftest.py index aa95ee6e..d52c7846 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/introspection/conftest.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/introspection/conftest.py @@ -6,14 +6,9 @@ functionality with PostgreSQL backends. """ -import os import pytest import pytest_asyncio -import yaml -from typing import Dict, Any, Tuple, Type -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend -from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig # SQL statements for test tables _TABLES_SQL = [ @@ -151,214 +146,99 @@ ] -# --- Scenario Loading Logic (consistent with feature/backend/conftest.py) --- - -SCENARIO_MAP: Dict[str, Dict[str, Any]] = {} - - -def _load_scenarios_from_config(): - """Load scenarios from configuration file.""" - config_path = None - env_config_path = os.getenv("POSTGRES_SCENARIOS_CONFIG_PATH") - - if env_config_path and os.path.exists(env_config_path): - config_path = env_config_path - else: - default_path = os.path.join(os.path.dirname(__file__), "../../../../../config", "postgres_scenarios.yaml") - if os.path.exists(default_path): - config_path = default_path - - if not config_path: - raise FileNotFoundError( - "No PostgreSQL scenarios configuration file found. " - "Set POSTGRES_SCENARIOS_CONFIG_PATH or place postgres_scenarios.yaml in tests/config." - ) - - with open(config_path, 'r', encoding='utf-8') as f: - config_data = yaml.safe_load(f) - - if 'scenarios' not in config_data: - raise ValueError(f"Configuration file {config_path} does not contain 'scenarios' key") - - for scenario_name, config in config_data['scenarios'].items(): - if config: - SCENARIO_MAP[scenario_name] = config - - -_load_scenarios_from_config() - - -def get_scenario(name: str) -> Tuple[Type[PostgresBackend], PostgresConnectionConfig]: - """Get backend class and connection config for a scenario.""" - if name not in SCENARIO_MAP: - if SCENARIO_MAP: - name = next(iter(SCENARIO_MAP)) - else: - raise ValueError("No scenarios registered") - - config_dict = SCENARIO_MAP[name].copy() - config_dict.pop('enabled', None) - config = PostgresConnectionConfig(**config_dict) - return PostgresBackend, config - - -def get_enabled_scenarios() -> Dict[str, Any]: - """Return all enabled scenarios.""" - return SCENARIO_MAP - - def _execute_sql_list(backend, sql_list): """Execute a list of SQL statements.""" for sql in sql_list: backend.execute(sql) -@pytest.fixture(scope="function") -def postgres_backend_single(request): - """Fixture providing a single PostgreSQL backend for introspection tests.""" - # Get first available scenario - enabled_scenarios = get_enabled_scenarios() - if not enabled_scenarios: - pytest.skip("No PostgreSQL scenarios configured") - - first_scenario = list(enabled_scenarios.keys())[0] - backend_class, config = get_scenario(first_scenario) - backend = backend_class(connection_config=config) - backend.connect() - backend.introspect_and_adapt() - yield backend - backend.disconnect() - - -@pytest_asyncio.fixture(scope="function") -async def async_postgres_backend_single(request): - """Async fixture providing a single PostgreSQL backend for introspection tests.""" - # Get first available scenario - enabled_scenarios = get_enabled_scenarios() - if not enabled_scenarios: - pytest.skip("No PostgreSQL scenarios configured") - - first_scenario = list(enabled_scenarios.keys())[0] - _backend_class, config = get_scenario(first_scenario) - backend = AsyncPostgresBackend(connection_config=config) - await backend.connect() - await backend.introspect_and_adapt() - yield backend - await backend.disconnect() +async def _async_execute_sql_list(backend, sql_list): + """Execute a list of SQL statements asynchronously.""" + for sql in sql_list: + await backend.execute(sql) @pytest.fixture(scope="function") -def backend_with_tables(request): +def backend_with_tables(postgres_backend): """Fixture providing backend with test tables created.""" - # Get first available scenario - enabled_scenarios = get_enabled_scenarios() - if not enabled_scenarios: - pytest.skip("No PostgreSQL scenarios configured") - - first_scenario = list(enabled_scenarios.keys())[0] - backend_class, config = get_scenario(first_scenario) - backend = backend_class(connection_config=config) - backend.connect() - backend.introspect_and_adapt() - - # Create tables - _execute_sql_list(backend, _TABLES_SQL) - # Add foreign keys - _execute_sql_list(backend, _FK_SQL) - # Clear introspection cache - backend.introspector.clear_cache() - yield backend - + postgres_backend.introspector.clear_cache() + _execute_sql_list(postgres_backend, _TABLES_SQL) + _execute_sql_list(postgres_backend, _FK_SQL) + postgres_backend.introspector.clear_cache() + yield postgres_backend try: - backend.introspector.clear_cache() - _execute_sql_list(backend, _CLEANUP_TABLES_SQL) + postgres_backend.introspector.clear_cache() + _execute_sql_list(postgres_backend, _CLEANUP_TABLES_SQL) except Exception: pass - finally: - backend.disconnect() @pytest_asyncio.fixture(scope="function") -async def async_backend_with_tables(request): +async def async_backend_with_tables(async_postgres_backend): """Async fixture providing backend with test tables created.""" - # Get first available scenario - enabled_scenarios = get_enabled_scenarios() - if not enabled_scenarios: - pytest.skip("No PostgreSQL scenarios configured") - - first_scenario = list(enabled_scenarios.keys())[0] - _backend_class, config = get_scenario(first_scenario) - backend = AsyncPostgresBackend(connection_config=config) - await backend.connect() - await backend.introspect_and_adapt() - - # Create tables - for sql in _TABLES_SQL: - await backend.execute(sql) - # Add foreign keys - for sql in _FK_SQL: - await backend.execute(sql) - # Clear introspection cache - backend.introspector.clear_cache() - yield backend - + async_postgres_backend.introspector.clear_cache() + await _async_execute_sql_list(async_postgres_backend, _TABLES_SQL) + await _async_execute_sql_list(async_postgres_backend, _FK_SQL) + async_postgres_backend.introspector.clear_cache() + yield async_postgres_backend try: - backend.introspector.clear_cache() - for sql in _CLEANUP_TABLES_SQL: - await backend.execute(sql) + async_postgres_backend.introspector.clear_cache() + await _async_execute_sql_list(async_postgres_backend, _CLEANUP_TABLES_SQL) except Exception: pass - finally: - await backend.disconnect() @pytest.fixture(scope="function") -def backend_with_view(request): - """Fixture providing backend with test view created.""" - enabled_scenarios = get_enabled_scenarios() - if not enabled_scenarios: - pytest.skip("No PostgreSQL scenarios configured") - - first_scenario = list(enabled_scenarios.keys())[0] - backend_class, config = get_scenario(first_scenario) - backend = backend_class(connection_config=config) - backend.connect() - backend.introspect_and_adapt() +def backend_with_view(postgres_backend): + """Fixture providing backend with a test view.""" + postgres_backend.introspector.clear_cache() + _execute_sql_list(postgres_backend, _VIEW_SQL) + postgres_backend.introspector.clear_cache() + yield postgres_backend + try: + postgres_backend.introspector.clear_cache() + _execute_sql_list(postgres_backend, _CLEANUP_VIEW_SQL) + except Exception: + pass - _execute_sql_list(backend, _VIEW_SQL) - backend.introspector.clear_cache() - yield backend +@pytest_asyncio.fixture(scope="function") +async def async_backend_with_view(async_postgres_backend): + """Async fixture providing backend with a test view.""" + async_postgres_backend.introspector.clear_cache() + await _async_execute_sql_list(async_postgres_backend, _VIEW_SQL) + async_postgres_backend.introspector.clear_cache() + yield async_postgres_backend try: - backend.introspector.clear_cache() - _execute_sql_list(backend, _CLEANUP_VIEW_SQL) + async_postgres_backend.introspector.clear_cache() + await _async_execute_sql_list(async_postgres_backend, _CLEANUP_VIEW_SQL) except Exception: pass - finally: - backend.disconnect() @pytest.fixture(scope="function") -def backend_with_trigger(request): - """Fixture providing backend with test trigger created.""" - enabled_scenarios = get_enabled_scenarios() - if not enabled_scenarios: - pytest.skip("No PostgreSQL scenarios configured") - - first_scenario = list(enabled_scenarios.keys())[0] - backend_class, config = get_scenario(first_scenario) - backend = backend_class(connection_config=config) - backend.connect() - backend.introspect_and_adapt() +def backend_with_trigger(postgres_backend): + """Fixture providing backend with a test trigger.""" + postgres_backend.introspector.clear_cache() + _execute_sql_list(postgres_backend, _TRIGGER_SQL) + postgres_backend.introspector.clear_cache() + yield postgres_backend + try: + postgres_backend.introspector.clear_cache() + _execute_sql_list(postgres_backend, _CLEANUP_TRIGGER_SQL) + except Exception: + pass - _execute_sql_list(backend, _TRIGGER_SQL) - backend.introspector.clear_cache() - yield backend +@pytest_asyncio.fixture(scope="function") +async def async_backend_with_trigger(async_postgres_backend): + """Async fixture providing backend with a test trigger.""" + async_postgres_backend.introspector.clear_cache() + await _async_execute_sql_list(async_postgres_backend, _TRIGGER_SQL) + async_postgres_backend.introspector.clear_cache() + yield async_postgres_backend try: - backend.introspector.clear_cache() - _execute_sql_list(backend, _CLEANUP_TRIGGER_SQL) + async_postgres_backend.introspector.clear_cache() + await _async_execute_sql_list(async_postgres_backend, _CLEANUP_TRIGGER_SQL) except Exception: pass - finally: - backend.disconnect() From 779fd8c54658aa55c63969b3664d3e66f0e87860 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Fri, 15 May 2026 10:03:00 +0800 Subject: [PATCH 08/12] build(dev): add pytest-xdist dependency for scenario-parallel test execution Co-Authored-By: Claude Opus 4.7 --- requirements-dev.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e8ab6511..b4c88845 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,4 +4,5 @@ rhosocial-activerecord-testsuite pytest-asyncio pydantic[email] pytest-timeout -pytest-mock \ No newline at end of file +pytest-mock +pytest-xdist \ No newline at end of file From ec8c68c5b7d355689beae08d257b672e822f56f2 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Fri, 15 May 2026 10:09:34 +0800 Subject: [PATCH 09/12] fix(ci): use pytest-cov instead of coverage run for xdist compatibility coverage run does not collect data from xdist worker subprocesses. pytest-cov with --cov flag natively supports parallel worker coverage merging. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/test.yml | 3 +-- requirements-dev.txt | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dd64638e..9da5c190 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -130,8 +130,7 @@ jobs: run: | export PYTHONPATH=src:tests pip install codecov - coverage run --source=src -m pytest tests/ --tb=short --scenario-parallel -n 2 --dist=loadgroup - coverage xml + python -m pytest tests/ --tb=short --scenario-parallel -n 2 --dist=loadgroup --cov=src --cov-report=xml - name: Upload coverage to Codecov if: matrix.postgres-version == '18' diff --git a/requirements-dev.txt b/requirements-dev.txt index b4c88845..0773a3c8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,4 +5,5 @@ pytest-asyncio pydantic[email] pytest-timeout pytest-mock -pytest-xdist \ No newline at end of file +pytest-xdist +pytest-cov \ No newline at end of file From 9f0487120090e45d32c10d56275f7cfca0d153d5 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Fri, 15 May 2026 12:25:27 +0800 Subject: [PATCH 10/12] ci: switch testsuite install from git to PyPI Change rhosocial-activerecord-testsuite installation source from git+https://github.com/... to PyPI, pinned to version 1.0.0.dev13. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9da5c190..aae08e36 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -80,7 +80,7 @@ jobs: - name: Install activerecord and testsuite from PyPI/git run: | pip install git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev26#egg=rhosocial-activerecord - pip install git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev13#egg=rhosocial-activerecord-testsuite + pip install rhosocial-activerecord-testsuite==1.0.0.dev13 - name: Create postgres scenario config run: | From 530693009a43aa61436c6e6f077d426014236f85 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Fri, 15 May 2026 13:55:34 +0800 Subject: [PATCH 11/12] ci: install rhosocial-activerecord from PyPI instead of git Changed installation source from git+https to PyPI, pinned to version 1.0.0.dev26. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aae08e36..358e7e4e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -77,9 +77,9 @@ jobs: pip install -r requirements.txt pip install -r requirements-dev.txt - - name: Install activerecord and testsuite from PyPI/git + - name: Install activerecord and testsuite from PyPI run: | - pip install git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev26#egg=rhosocial-activerecord + pip install rhosocial-activerecord==1.0.0.dev26 pip install rhosocial-activerecord-testsuite==1.0.0.dev13 - name: Create postgres scenario config From a9d97627d175af742674070ceebe82e091d092ac Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Fri, 15 May 2026 14:39:22 +0800 Subject: [PATCH 12/12] docs: update CHANGELOG for v1.0.0.dev13 Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 7 +++++++ changelog.d/35.security.md | 1 - 2 files changed, 7 insertions(+), 1 deletion(-) delete mode 100644 changelog.d/35.security.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 43c2dc91..f88dfd9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [v1.0.0.dev13] - 2026-05-15 + +### Security + +- **SECURITY**: Resolved SQL injection risks in PostgreSQL backend ([#35](https://github.com/rhosocial/python-activerecord-postgres/issues/35)) + + ## [v1.0.0.dev12] - 2026-05-01 ### Added diff --git a/changelog.d/35.security.md b/changelog.d/35.security.md deleted file mode 100644 index d1efe6bc..00000000 --- a/changelog.d/35.security.md +++ /dev/null @@ -1 +0,0 @@ -**SECURITY**: Resolved SQL injection risks in PostgreSQL backend \ No newline at end of file