Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
2941d64
fix(oracle): drop index by name without ON <table>
vistart Sep 19, 2026
d9bbd4b
refactor(oracle): derive partition definitions from generic base decl…
vistart Sep 20, 2026
2dabbb3
feat(oracle): add OracleColumnDefinition and typed column options
vistart Sep 20, 2026
0134a76
refactor(oracle): read typed purge flag on DROP TABLE
vistart Sep 20, 2026
5b4677a
refactor(oracle): replace legacy partition bag paths with typed dispatch
vistart Sep 20, 2026
8cd7239
refactor(oracle): drop reserved dialect_options bag from DDL expressions
vistart Sep 20, 2026
e3a28c5
refactor(oracle): drop dialect_options pass-through from OracleForUpd…
vistart Sep 20, 2026
b03772b
refactor(oracle): drop dialect_options from data-type value objects
vistart Sep 20, 2026
b7f85c1
fix(oracle): declare supports_temporal_tables False
vistart Sep 20, 2026
020d9d3
fix(oracle): gate CREATE VIEW WITH CHECK OPTION on supports_view_chec…
vistart Sep 20, 2026
4b87f95
refactor(oracle): drop unused dialect_options params from insert/merg…
vistart Sep 20, 2026
6a405bf
refactor(ddl): drop legacy identity fields from column classes
vistart Sep 21, 2026
159acef
feat(ddl): render column attributes; backend identity syntax
vistart Sep 21, 2026
a12dea1
test(ddl): classify ColumnAttributeSupport; use deriver.creation_plan()
vistart Sep 21, 2026
356c96f
feat(oracle): render DROP TABLE ... PURGE independently of cascade
vistart Sep 21, 2026
0ea389b
fix(oracle): rename supports_comment to supports_comment_on; stop sil…
vistart Sep 23, 2026
c4b1d6f
test(ddl): classify CommentSupport for Oracle conformance
vistart Sep 23, 2026
977cab7
test: drop redundant __primary_key__ = "id" from fixtures
vistart Sep 23, 2026
34045f0
fix(ddl): stop silently dropping Oracle DDL options
vistart Sep 23, 2026
6b0ecb0
feat(oracle): add type DDL support
vistart Sep 24, 2026
9792bf0
test(ddl): cover table inheritance and tablespace
vistart Sep 24, 2026
bb8b8ae
test(ddl): use adapted Oracle dialect
vistart Sep 24, 2026
dd772e3
test(oracle): align type DDL result contract
vistart Sep 24, 2026
5b70262
fix(oracle): avoid default VALIDATE on core DROP TYPE
vistart Sep 24, 2026
b9c282d
feat(oracle): add partition lifecycle provider
vistart Sep 25, 2026
683b602
fix(oracle): update DDL imports for namespace split
vistart Sep 25, 2026
a3e139d
fix(oracle): declare partition lifecycle provider protocol
vistart Sep 25, 2026
7cd4c60
refactor(oracle): remove generated DDL lifecycle
vistart Sep 25, 2026
d1eb71f
docs: document repository search scope
vistart Sep 25, 2026
f997075
test(ddl): cover DDLSource collection
vistart Sep 25, 2026
e23a1cc
feat(view): support Oracle materialized view refresh via DBMS_MVIEW.R…
vistart Sep 26, 2026
9fcef61
fix(backend): repair unresolvable TYPE_CHECKING imports
vistart Sep 27, 2026
dee82d2
chore(test): register the pytest markers the suite already uses
vistart Sep 27, 2026
6e6b15f
fix(scripts): stop the pre-commit hook from blocking commits on older…
vistart Sep 27, 2026
0870faf
ci: install core and testsuite from release branches
vistart Sep 27, 2026
393a813
chore: add changelog fragment for PR #5
vistart Sep 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
- name: Install activerecord and testsuite from git
run: |
pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30"
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/activerecord-derived-ddl"
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17"

Check warning on line 56 in .github/workflows/test.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Using dependencies without locking resolved versions is security-sensitive.

See more on https://sonarcloud.io/project/issues?id=rhosocial_python-activerecord-oracle&issues=AaDipXpNHGdXYMwaG6to&open=AaDipXpNHGdXYMwaG6to&pullRequest=5

Check warning on line 56 in .github/workflows/test.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Omitting "--only-binary :all:" can lead to the execution of setup scripts. Make sure it is safe here.

See more on https://sonarcloud.io/project/issues?id=rhosocial_python-activerecord-oracle&issues=AaDipXpNHGdXYMwaG6tn&open=AaDipXpNHGdXYMwaG6tn&pullRequest=5

- name: Wait for Oracle and test connectivity
run: |
Expand Down Expand Up @@ -181,7 +181,7 @@
- name: Install activerecord and testsuite from git
run: |
pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30"
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/activerecord-derived-ddl"
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17"

Check warning on line 184 in .github/workflows/test.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Using dependencies without locking resolved versions is security-sensitive.

See more on https://sonarcloud.io/project/issues?id=rhosocial_python-activerecord-oracle&issues=AaDipXpNHGdXYMwaG6tp&open=AaDipXpNHGdXYMwaG6tp&pullRequest=5

- name: Wait for Oracle and test connectivity
run: |
Expand Down Expand Up @@ -309,7 +309,7 @@
- name: Install activerecord and testsuite from git
run: |
pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30"
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/activerecord-derived-ddl"
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17"

Check warning on line 312 in .github/workflows/test.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Using dependencies without locking resolved versions is security-sensitive.

See more on https://sonarcloud.io/project/issues?id=rhosocial_python-activerecord-oracle&issues=AaDipXpNHGdXYMwaG6tr&open=AaDipXpNHGdXYMwaG6tr&pullRequest=5

Check warning on line 312 in .github/workflows/test.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Omitting "--only-binary :all:" can lead to the execution of setup scripts. Make sure it is safe here.

See more on https://sonarcloud.io/project/issues?id=rhosocial_python-activerecord-oracle&issues=AaDipXpNHGdXYMwaG6tq&open=AaDipXpNHGdXYMwaG6tq&pullRequest=5

- name: Wait for Oracle and test connectivity
run: |
Expand Down
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ This project is in early development. Key features being implemented:
## 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.

## Search Scope

- Use `rg` for repository searches; never use `grep`.
- Limit searches to this `python-activerecord` repository and its related testsuite/backend directories.
- Exclude `.git/`, `.venv*/`, `__pycache__/`, `build/`, `dist/`, and other cache directories.
1 change: 1 addition & 0 deletions changelog.d/20.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add Oracle materialized view refresh via `OracleRefreshMaterializedViewExpression`, which renders a `DBMS_MVIEW.REFRESH` PL/SQL block. Oracle has no standalone `REFRESH MATERIALIZED VIEW` statement, so refresh was previously unreachable. Supports the procedure's refresh method codes (`F`/`C`/`?`/`P`/`A`, which differ from the `REFRESH FAST`/`COMPLETE` DDL keywords) plus `atomic_refresh`, `out_of_place`, `nested`, `parallelism`, `purge_option`, `refresh_after_errors` and `push_deferred_rpc`. `supports_refresh_materialized_view()` now reports `True` and the generic `RefreshMaterializedViewExpression` routes to the same formatter.
1 change: 1 addition & 0 deletions changelog.d/5.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Adapt the Oracle backend to derived DDL: `OracleColumnDefinition` with typed column options, a partition lifecycle provider with the provider protocol declared, partition definitions derived from the generic base declarations, type DDL support, `DROP TABLE ... PURGE` rendering, `CommentSupport` classification, index drop by bare name without `ON <table>`, `OracleDatabaseMixin` wiring, and materialized view refresh via `DBMS_MVIEW.REFRESH`.
1 change: 1 addition & 0 deletions changelog.d/5.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Stop the Oracle backend from discarding or over-claiming declared DDL: `supports_temporal_tables` is declared `False` instead of left unstated, unrenderable `ALTER SCHEMA` capabilities are no longer advertised, `supports_comment` is renamed to `supports_comment_on` and declared comments are no longer dropped, `CREATE VIEW ... WITH CHECK OPTION` is gated on `supports_view_check_option`, `DROP TYPE` no longer applies a default `VALIDATE`, and `DROP TABLE ... PURGE` renders independently of `CASCADE`.
1 change: 1 addition & 0 deletions changelog.d/5.internal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Install core and testsuite from release branches in CI instead of the derived-DDL feature branch, repair TYPE_CHECKING imports broken by the expression namespace split, register the pytest markers the suite already uses, and stop the pre-commit hook from blocking commits on revisions that predate `scripts/preflight.sh`.
1 change: 1 addition & 0 deletions changelog.d/5.removed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Remove the reserved `dialect_options` bag from data-type value objects, DDL expressions, the insert/merge formatters and `OracleForUpdateClause`, replace the legacy partition bag paths with typed dispatch, drop the legacy identity fields from column classes, and remove the generated DDL lifecycle.
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ markers = [
"requires_protocol: marks tests requiring specific backend protocol support",
"requires_inner_join: marks tests requiring INNER JOIN support",
"backend: marks tests for backend-specific features",
"feature: category label for feature-area tests; changes collection grouping only, not ordering",
"benchmark_relation: marks relation benchmarks; changes collection grouping only, not ordering",

]

[tool.coverage.run]
Expand Down
31 changes: 26 additions & 5 deletions scripts/install-git-hooks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,42 @@
# Installs an optional pre-commit hook that runs scripts/preflight.sh so a
# broken import/lint is caught before the commit (and before a CI round-trip).
# Bypass a single commit with: git commit --no-verify
#
# Re-run this after pulling, and re-run it from the repository root. To remove
# the hook, delete .git/hooks/pre-commit (or the core.hooksPath directory).
set -euo pipefail

cd "$(dirname "$0")/.."

HOOK=".git/hooks/pre-commit"
if [ ! -d .git/hooks ]; then
echo "install-git-hooks: .git/hooks not found (not a git repo?)" >&2
exit 2
# git ignores .git/hooks entirely once core.hooksPath is set, so a hook written
# there would never run while this script still reported success.
HOOKS_PATH="$(git config core.hooksPath || true)"
if [ -n "$HOOKS_PATH" ]; then

Check failure on line 17 in scripts/install-git-hooks.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=rhosocial_python-activerecord-oracle&issues=AaDipXqSHGdXYMwaG6ts&open=AaDipXqSHGdXYMwaG6ts&pullRequest=5
echo "install-git-hooks: core.hooksPath is set to '$HOOKS_PATH'." >&2
echo "install-git-hooks: .git/hooks is ignored by git; writing the hook to" >&2
echo " '$HOOKS_PATH/pre-commit' instead." >&2
HOOK="$HOOKS_PATH/pre-commit"
mkdir -p "$HOOKS_PATH"
else
HOOK=".git/hooks/pre-commit"
if [ ! -d .git/hooks ]; then
echo "install-git-hooks: .git/hooks not found (not a git repo?)" >&2
exit 2
fi
fi

cat > "$HOOK" <<'HOOK_BODY'
#!/usr/bin/env bash
# Installed by scripts/install-git-hooks.sh — runs the local pre-flight.
# Skip with: git commit --no-verify
exec "$(git rev-parse --show-toplevel)/scripts/preflight.sh"
#
# The guard below matters on older revisions: this hook is a local, untracked
# artifact, so it survives a checkout of a commit that predates
# scripts/preflight.sh. Without the guard, exec fails with a bare
# "No such file or directory" and blocks the commit with nothing to act on.
PREFLIGHT="$(git rev-parse --show-toplevel)/scripts/preflight.sh"
[ -x "$PREFLIGHT" ] || exit 0
exec "$PREFLIGHT"
HOOK_BODY
chmod +x "$HOOK"

Expand Down
70 changes: 70 additions & 0 deletions src/rhosocial/activerecord/backend/impl/oracle/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,40 @@
OracleVarChar2Type,
OracleXmlType,
)
from .expression.ddl.type import (
DropTypeBodyExpression,
OracleAlterTypeAddAttributeAction,
OracleAlterTypeAddMethodAction,
OracleAlterTypeCompileAction,
OracleAlterTypeDependentHandling,
OracleTypeDependentHandling,
OracleAlterTypeAttributeAction,
OracleAlterTypeMethodAction,
OracleAlterTypeModifyLimitAction,
OracleCompileTypeAction,
OracleSetTypeFinalAction,
OracleSetTypeInstantiableAction,
OracleAlterTypeDropAttributeAction,
OracleAlterTypeDropMethodAction,
OracleAlterTypeElementTypeAction,
OracleAlterTypeFinalAction,
OracleAlterTypeInstantiableAction,
OracleAlterTypeLimitAction,
OracleAlterTypeModifyAttributeAction,
OracleAlterTypeResetAction,
OracleCreateTypeBodyExpression,
OracleDropTypeBodyExpression,
OracleDropTypeExpression,
OracleIncompleteTypeDefinition,
OracleNestedTableTypeDefinition,
OracleObjectTypeDefinition,
OracleSqljTypeDefinition,
OracleTypeAlterAction,
OracleTypeAttribute,
OracleTypeMethod,
OracleVarrayTypeDefinition,
)
from .version import VERSION_FULL_QUERY, VERSION_QUERY, ru_from_version_full

from .schema import OracleSchemaDiffer

Expand Down Expand Up @@ -143,6 +177,42 @@
"OracleLongRawType",
"OracleBlobType",

# User-defined TYPE DDL
"OracleTypeAttribute",
"OracleTypeMethod",
"OracleObjectTypeDefinition",
"OracleSqljTypeDefinition",
"OracleVarrayTypeDefinition",
"OracleNestedTableTypeDefinition",
"OracleIncompleteTypeDefinition",
"OracleAlterTypeDependentHandling",
"OracleTypeDependentHandling",
"OracleAlterTypeAttributeAction",
"OracleAlterTypeMethodAction",
"OracleAlterTypeModifyLimitAction",
"OracleCompileTypeAction",
"OracleSetTypeFinalAction",
"OracleSetTypeInstantiableAction",
"OracleTypeAlterAction",
"OracleAlterTypeAddAttributeAction",
"OracleAlterTypeModifyAttributeAction",
"OracleAlterTypeDropAttributeAction",
"OracleAlterTypeAddMethodAction",
"OracleAlterTypeDropMethodAction",
"OracleAlterTypeLimitAction",
"OracleAlterTypeElementTypeAction",
"OracleAlterTypeCompileAction",
"OracleAlterTypeFinalAction",
"OracleAlterTypeInstantiableAction",
"OracleAlterTypeResetAction",
"OracleCreateTypeBodyExpression",
"OracleDropTypeExpression",
"DropTypeBodyExpression",
"OracleDropTypeBodyExpression",
"VERSION_FULL_QUERY",
"VERSION_QUERY",
"ru_from_version_full",

# Schema differ
"OracleSchemaDiffer",

Expand Down
87 changes: 73 additions & 14 deletions src/rhosocial/activerecord/backend/impl/oracle/async_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@
from .dialect import OracleDialect
from .async_transaction import AsyncOracleTransactionManager
from .mixins import OracleBackendMixin
from .version import (
VERSION_FULL_QUERY,
VERSION_QUERY,
normalize_version,
parse_version_row,
ru_from_version_full,
)
from .backend import OracleBackend, _is_numeric_python_type


Expand All @@ -42,6 +49,8 @@ class AsyncOracleBackend(OracleBackendMixin, IntrospectorBackendMixin, AsyncStor
def __init__(self, **kwargs):
"""Initialize async Oracle backend."""
version = kwargs.pop('version', None) or (19, 0, 0)
version_full = kwargs.pop('version_full', None)
ru_version = kwargs.pop('ru_version', None)

connection_config = kwargs.get('connection_config')

Expand Down Expand Up @@ -72,6 +81,10 @@ def __init__(self, **kwargs):
super().__init__(**kwargs)

self._version = version or (19, 0, 0)
self._version_full = normalize_version(version_full, minimum_length=2) or None
self._ru_version = (
ru_version if ru_version is not None else ru_from_version_full(self._version_full)
)
self._current_column_types = None
self._dialect = None
self._transaction_manager = AsyncOracleTransactionManager(None, self.logger)
Expand All @@ -94,7 +107,11 @@ def transaction_manager(self) -> AsyncOracleTransactionManager:
def dialect(self) -> OracleDialect:
"""Get Oracle SQL dialect."""
if self._dialect is None:
self._dialect = OracleDialect(self._version)
self._dialect = OracleDialect(
self._version,
version_full=self._version_full,
ru_version=self._ru_version,
)
return self._dialect

async def _handle_auto_commit(self) -> None:
Expand All @@ -112,10 +129,37 @@ async def introspect_and_adapt(self) -> None:
"""Introspect backend and adapt to actual server capabilities."""
if not self._connection:
await self.connect()
previous_version = getattr(self, "_version", None)
previous_version_full = getattr(self, "_version_full", None)
previous_ru_version = getattr(self, "_ru_version", None)
previous_dialect = getattr(self, "_dialect", None)
actual_version = await self.get_server_version()
if self._version != actual_version:
actual_version_full = getattr(self, "_version_full", None)
if actual_version_full is None:
actual_version_full = await self.get_server_version_full()
actual_ru_version = getattr(self, "_ru_version", None)
if actual_ru_version is None and actual_version_full is not None:
actual_ru_version = ru_from_version_full(actual_version_full)
dialect_version = getattr(previous_dialect, "_version", None)
dialect_version_full = getattr(previous_dialect, "version_full", None)
dialect_ru_version = getattr(previous_dialect, "ru_version", None)
if (
previous_version != actual_version
or previous_version_full != actual_version_full
or previous_ru_version != actual_ru_version
or previous_dialect is None
or dialect_version != actual_version
or dialect_version_full != actual_version_full
or dialect_ru_version != actual_ru_version
):
self._version = actual_version
self._dialect = OracleDialect(actual_version)
self._version_full = actual_version_full
self._ru_version = actual_ru_version
self._dialect = OracleDialect(
actual_version,
version_full=actual_version_full,
ru_version=actual_ru_version,
)
self._register_oracle_adapters()
self.log(logging.INFO, f"Adapted to Oracle server version {actual_version}")

Expand Down Expand Up @@ -562,29 +606,44 @@ async def execute_many(self, sql: str, params_list: List[Tuple]) -> QueryResult:
cursor.close()

async def get_server_version(self) -> tuple:
"""Get Oracle server version asynchronously."""
"""Get the Oracle base and full release versions asynchronously."""
if not self._connection:
await self.connect()

cursor = None
try:
cursor = await self._get_cursor()
await cursor.execute("SELECT VERSION FROM PRODUCT_COMPONENT_VERSION WHERE PRODUCT LIKE 'Oracle%'")
row = await cursor.fetchone()
version_str = row[0]

version_parts = version_str.split('.')
major = int(version_parts[0]) if len(version_parts) > 0 else 0
minor = int(version_parts[1]) if len(version_parts) > 1 else 0
patch = int(version_parts[2]) if len(version_parts) > 2 else 0

return (major, minor, patch)
try:
await cursor.execute(VERSION_FULL_QUERY)
row = await cursor.fetchone()
base, version_full = parse_version_row(row)
except Exception:
cursor.close()
cursor = await self._get_cursor()
await cursor.execute(VERSION_QUERY)
row = await cursor.fetchone()
base, version_full = parse_version_row(row)
base = normalize_version(base)[:3]
if not base:
raise ValueError("Oracle PRODUCT_COMPONENT_VERSION returned no version")
self._version_full = version_full
self._ru_version = ru_from_version_full(version_full)
return base
except Exception:
self._version_full = None
self._ru_version = None
return (19, 0, 0)
finally:
if cursor:
cursor.close()

async def get_server_version_full(self) -> Optional[Tuple[int, ...]]:
"""Return the full Oracle version, including the RU when available."""
if self._version_full is not None:
return self._version_full
await self.get_server_version()
return self._version_full

async def ping(self, reconnect: bool = True) -> bool:
"""Ping the Oracle server asynchronously."""
try:
Expand Down
Loading
Loading