Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
cbd3cf2
feat(postgres): adapt to node-based expression system and declarative…
vistart Sep 10, 2026
f0be87f
ci: test against Python 3.15.0-rc.2
vistart Sep 10, 2026
95abef2
fix: Rename JoinExpression→JoinClause and format_grouping_expression→…
vistart Sep 11, 2026
2fae3ae
feat(dialect-protocol): formalize PostgreSQL type support and suggest…
vistart Sep 12, 2026
9df24de
fix(test): VarCharType dialect-first call sites in fixtures and examples
vistart Sep 12, 2026
15dfa99
fix(test): ArrayType/DecimalType/TimestampType dialect-first call sites
vistart Sep 12, 2026
cfbfcb0
fix(test): TimestampType precision keyword, ArrayType element_type ke…
vistart Sep 12, 2026
3829218
fix(test): hash tests use (type, _type_params) tuple
vistart Sep 12, 2026
381ac53
fix(test): ArrayType dimensions keyword in test_core_type_mappings
vistart Sep 12, 2026
b820f74
fix(examples): adapt example code to dialect-first ColumnDefinition/D…
vistart Sep 12, 2026
7da14ea
test(protocol): add negative protocol list + partition completeness
vistart Sep 12, 2026
31ee182
feat(dialect): declare AutoIncrement + GeneratedColumn protocol support
vistart Sep 12, 2026
400ee4c
fix(dialect): adapt format_fulltext_match/array_expression to express…
vistart Sep 13, 2026
f0245a2
feat(identifier): add reserved words and opt-out identifier quoting
vistart Sep 13, 2026
c505226
refactor(identifier): remove IdentifierMixin and fix format_column wi…
vistart Sep 13, 2026
d7d78ce
refactor(postgres): remove 11 duplicate dialect methods
vistart Sep 13, 2026
dcd433e
fix(mixins): restore Postgres EXPLAIN and TRUNCATE formatters
vistart Sep 13, 2026
4f11a48
fix(protocols): declare Postgres EXPLAIN/TRUNCATE formatters
vistart Sep 13, 2026
391712e
refactor(postgres): split dialect methods into feature mixins
vistart Sep 13, 2026
efbcd3d
feat(postgres): format_* signature compliance - ILIKE + Enum + Multir…
vistart Sep 14, 2026
25c8fae
chore: resolve merge conflict in expression types
vistart Sep 14, 2026
711ee23
fix(postgres): restore types.py accidentally emptied during conflict …
vistart Sep 14, 2026
3eb1a16
fix(postgres): fix expression format_method to match mixin method names
vistart Sep 14, 2026
d1d8965
fix(postgres): fix format_method for EnumTypeNameExpression and EnumV…
vistart Sep 14, 2026
534f511
fix(postgres): add missing methods to protocols for expression compli…
vistart Sep 14, 2026
5aedb22
fix(postgres): declare mixin formatters on protocols
vistart Sep 14, 2026
1900118
fix(postgres): convert format_ilike_expression to (self, expr)
vistart Sep 14, 2026
550ff20
refactor(postgres): drop str fallback in ILIKE column
vistart Sep 14, 2026
1bbe5e6
test(postgres): build ILIKE expressions with Column nodes
vistart Sep 14, 2026
aaf6edf
test(postgres): exercise formatter tests via expression to_sql()
vistart Sep 14, 2026
f87aa4f
test(postgres): build ILIKE expression tests with Column nodes
vistart Sep 14, 2026
1023871
refactor(postgres): emit bind markers via dialect placeholder
vistart Sep 14, 2026
5fdc694
refactor: unify format_* return types to Tuple[str, tuple] and add ex…
vistart Sep 14, 2026
5f97347
chore: fix .gitignore for .claude/plan*/
vistart Sep 14, 2026
74249f6
fix: update storage_options test for StorageOptionsExpression signatu…
vistart Sep 14, 2026
18c5e82
refactor(ddl)!: migrate CREATE TABLE ... LIKE to CreateTableLikeExpre…
vistart Sep 15, 2026
2fc869f
refactor(ddl)!: UNLOGGED via typed CreateTableOptions
vistart Sep 15, 2026
b9ddf71
feat(ddl): prefer typed identity field over dialect_options
vistart Sep 15, 2026
a52249a
refactor(ddl): rename table.py → ddl_table.py in mixins
vistart Sep 15, 2026
a81d096
feat: add PostgreSQL DDL improvements with capability gating
vistart Sep 15, 2026
68ece13
fix: B01 TRUNCATE RESTART IDENTITY now raises UnsupportedFeatureError…
vistart Sep 15, 2026
2e5852b
refactor: remove ReturningMixin import (merged into DMLMixin in core)
vistart Sep 15, 2026
cc17128
feat: add supports_returning_old_new for PostgreSQL 17+ OLD/NEW refs
vistart Sep 15, 2026
b9d8b59
fix: declare new RETURNING/view capability methods in Postgres protocols
vistart Sep 15, 2026
8d98372
refactor(dql): typed lock strength; lift supports_fetch_with_ties to …
vistart Sep 15, 2026
f2d7fde
refactor: remove core DQL/Expression mixin references (merged into DQ…
vistart Sep 16, 2026
0ceb8f7
feat(ddl): render generated columns via core GeneratedColumnExpression
vistart Sep 16, 2026
df867a2
chore: add scripts/preflight.sh and optional git hook installer
vistart Sep 16, 2026
6c1f62a
chore(ci): update rhosocial-activerecord install source to release/v1…
vistart Sep 16, 2026
cad7681
chore: add changelog fragment for PR #57
vistart Sep 16, 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
12 changes: 6 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
- name: Install activerecord and testsuite
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@release/v1.0.0.dev17"
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/activerecord-derived-ddl"

- name: Create postgres scenario config
run: |
Expand Down Expand Up @@ -231,8 +231,8 @@ jobs:
fail-fast: false
matrix:
include:
- { python-version: '3.15.0-rc.1', postgres-version: '18', freethreaded: false }
- { python-version: '3.15.0-rc.1', postgres-version: '18', freethreaded: true }
- { python-version: '3.15.0-rc.2', postgres-version: '18', freethreaded: false }
- { python-version: '3.15.0-rc.2', postgres-version: '18', freethreaded: true }

services:
postgres:
Expand Down Expand Up @@ -280,7 +280,7 @@ jobs:
- name: Install activerecord and testsuite
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@release/v1.0.0.dev17"
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/activerecord-derived-ddl"

- name: Create postgres scenario config
run: |
Expand Down Expand Up @@ -392,7 +392,7 @@ jobs:
- name: Install activerecord and testsuite
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@release/v1.0.0.dev17"
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/activerecord-derived-ddl"

- name: Create postgres scenario config
run: |
Expand Down Expand Up @@ -491,7 +491,7 @@ jobs:
- name: Install activerecord and testsuite
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@release/v1.0.0.dev17"
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/activerecord-derived-ddl"

- name: Create postgres scenario config
run: |
Expand Down
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -181,5 +181,4 @@ htmlcov/

# Claude Code temporary files (local only)
.claude/tmp/
.claude/plans/
.claude/plan/
.claude/plan*/
1 change: 1 addition & 0 deletions changelog.d/57.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Comprehensive PostgreSQL backend overhaul: expression-based formatter system, DDL improvements, protocol compliance, and identifier quoting. Breaking changes: CREATE TABLE LIKE migrated to CreateTableLikeExpression, UNLOGGED via typed CreateTableOptions, dialect methods renamed to Postgres-prefixed convention.
76 changes: 35 additions & 41 deletions docs/en_US/postgres_specific_features/dialect.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,66 +139,60 @@ sql, params = expr.to_sql()

PostgreSQL supports copying table structure using the `LIKE` clause with fine-grained control over what gets copied via INCLUDING/EXCLUDING options.

PostgreSQL renders the clause form `CREATE TABLE t (LIKE src [INCLUDING ...])`,
modelled by `CreateTableLikeExpression`.

```python
from rhosocial.activerecord.backend.expression import CreateTableExpression
from rhosocial.activerecord.backend.expression import CreateTableLikeExpression
from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect

# Basic usage - copy table structure
create_expr = CreateTableExpression(
dialect=PostgresDialect(),
table_name="users_copy",
columns=[],
dialect_options={'like_table': 'users'}
expr = CreateTableLikeExpression(
PostgresDialect(),
table="users_copy",
like_table="users",
)
# Generates: CREATE TABLE "users_copy" (LIKE "users")

# With INCLUDING options (dictionary format - recommended)
create_expr = CreateTableExpression(
dialect=PostgresDialect(),
table_name="users_copy",
columns=[],
dialect_options={
'like_table': 'users',
'like_options': {
'including': ['DEFAULTS', 'CONSTRAINTS', 'INDEXES'],
'excluding': ['COMMENTS']
}
}
expr = CreateTableLikeExpression(
PostgresDialect(),
table="users_copy",
like_table="users",
like_options={
'including': ['DEFAULTS', 'CONSTRAINTS', 'INDEXES'],
'excluding': ['COMMENTS'],
},
)
# Generates: CREATE TABLE "users_copy" (LIKE "users", INCLUDING DEFAULTS,
# INCLUDING CONSTRAINTS, INCLUDING INDEXES, EXCLUDING COMMENTS)
# Generates: CREATE TABLE "users_copy" (LIKE "users" INCLUDING DEFAULTS
# INCLUDING CONSTRAINTS INCLUDING INDEXES EXCLUDING COMMENTS)

# With schema-qualified source table
create_expr = CreateTableExpression(
dialect=PostgresDialect(),
table_name="users_copy",
columns=[],
dialect_options={'like_table': ('public', 'users')}
expr = CreateTableLikeExpression(
PostgresDialect(),
table="users_copy",
like_table=('public', 'users'),
)
# Generates: CREATE TABLE "users_copy" (LIKE "public"."users")

# With TEMPORARY and IF NOT EXISTS
create_expr = CreateTableExpression(
dialect=PostgresDialect(),
table_name="temp_users",
columns=[],
expr = CreateTableLikeExpression(
PostgresDialect(),
table="temp_users",
like_table="users",
temporary=True,
if_not_exists=True,
dialect_options={'like_table': 'users'}
)
# Generates: CREATE TEMPORARY TABLE IF NOT EXISTS "temp_users" (LIKE "users")

# Including everything
create_expr = CreateTableExpression(
dialect=PostgresDialect(),
table_name="users_copy",
columns=[],
dialect_options={
'like_table': 'users',
'like_options': {'including': ['ALL']}
}
expr = CreateTableLikeExpression(
PostgresDialect(),
table="users_copy",
like_table="users",
like_options={'including': ['ALL']},
)
# Generates: CREATE TABLE "users_copy" (LIKE "users", INCLUDING ALL)
# Generates: CREATE TABLE "users_copy" (LIKE "users" INCLUDING ALL)
```

**Available INCLUDING/EXCLUDING Options:**
Expand All @@ -213,10 +207,10 @@ create_expr = CreateTableExpression(
- `ALL` - Copy everything

**Important Notes:**
- When `like_table` is specified in `dialect_options`, it takes highest priority
- All other parameters (columns, indexes, constraints, etc.) are IGNORED
- `CreateTableLikeExpression` carries only the target table and source `like_table`
- `like_options` accepts a dict (`including`/`excluding`), a list of feature
strings (treated as `INCLUDING`), or a list of `(action, feature)` tuples
- Only `temporary` and `if_not_exists` flags are considered
- The `like_options` key supports both dictionary and list formats

## RETURNING Clause

Expand Down
76 changes: 34 additions & 42 deletions docs/zh_CN/postgres_specific_features/dialect.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,68 +137,60 @@ sql, params = expr.to_sql()

### CREATE TABLE ... LIKE

PostgreSQL 支持使用 `LIKE` 子句复制表结构,并通过 INCLUDING/EXCLUDING 选项精细控制要复制的内容。
PostgreSQL 渲染为子句形式 `CREATE TABLE t (LIKE src [INCLUDING ...])`,
由 `CreateTableLikeExpression` 建模。

```python
from rhosocial.activerecord.backend.expression import CreateTableExpression
from rhosocial.activerecord.backend.expression import CreateTableLikeExpression
from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect

# 基本用法 - 复制表结构
create_expr = CreateTableExpression(
dialect=PostgresDialect(),
table_name="users_copy",
columns=[],
dialect_options={'like_table': 'users'}
expr = CreateTableLikeExpression(
PostgresDialect(),
table="users_copy",
like_table="users",
)
# 生成: CREATE TABLE "users_copy" (LIKE "users")

# 带 INCLUDING 选项(字典格式 - 推荐)
create_expr = CreateTableExpression(
dialect=PostgresDialect(),
table_name="users_copy",
columns=[],
dialect_options={
'like_table': 'users',
'like_options': {
'including': ['DEFAULTS', 'CONSTRAINTS', 'INDEXES'],
'excluding': ['COMMENTS']
}
}
expr = CreateTableLikeExpression(
PostgresDialect(),
table="users_copy",
like_table="users",
like_options={
'including': ['DEFAULTS', 'CONSTRAINTS', 'INDEXES'],
'excluding': ['COMMENTS'],
},
)
# 生成: CREATE TABLE "users_copy" (LIKE "users", INCLUDING DEFAULTS,
# INCLUDING CONSTRAINTS, INCLUDING INDEXES, EXCLUDING COMMENTS)
# 生成: CREATE TABLE "users_copy" (LIKE "users" INCLUDING DEFAULTS
# INCLUDING CONSTRAINTS INCLUDING INDEXES EXCLUDING COMMENTS)

# 带模式限定的源表
create_expr = CreateTableExpression(
dialect=PostgresDialect(),
table_name="users_copy",
columns=[],
dialect_options={'like_table': ('public', 'users')}
expr = CreateTableLikeExpression(
PostgresDialect(),
table="users_copy",
like_table=('public', 'users'),
)
# 生成: CREATE TABLE "users_copy" (LIKE "public"."users")

# 带临时表和 IF NOT EXISTS
create_expr = CreateTableExpression(
dialect=PostgresDialect(),
table_name="temp_users",
columns=[],
expr = CreateTableLikeExpression(
PostgresDialect(),
table="temp_users",
like_table="users",
temporary=True,
if_not_exists=True,
dialect_options={'like_table': 'users'}
)
# 生成: CREATE TEMPORARY TABLE IF NOT EXISTS "temp_users" (LIKE "users")

# 包含所有内容
create_expr = CreateTableExpression(
dialect=PostgresDialect(),
table_name="users_copy",
columns=[],
dialect_options={
'like_table': 'users',
'like_options': {'including': ['ALL']}
}
expr = CreateTableLikeExpression(
PostgresDialect(),
table="users_copy",
like_table="users",
like_options={'including': ['ALL']},
)
# 生成: CREATE TABLE "users_copy" (LIKE "users", INCLUDING ALL)
# 生成: CREATE TABLE "users_copy" (LIKE "users" INCLUDING ALL)
```

**可用的 INCLUDING/EXCLUDING 选项:**
Expand All @@ -213,10 +205,10 @@ create_expr = CreateTableExpression(
- `ALL` - 复制所有内容

**重要说明:**
- 当 `dialect_options` 中指定 `like_table` 时,具有最高优先级
- 所有其他参数(columns、indexes、constraints 等)都会被忽略
- `CreateTableLikeExpression` 只携带目标表和源 `like_table`
- `like_options` 支持字典(`including`/`excluding`)、字符串列表(视为
`INCLUDING`)以及 `(action, feature)` 元组列表
- 只有 `temporary` 和 `if_not_exists` 标志会被考虑
- `like_options` 键支持字典和列表两种格式

## RETURNING 子句

Expand Down
25 changes: 25 additions & 0 deletions scripts/install-git-hooks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# scripts/install-git-hooks.sh
#
# 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
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
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"
HOOK_BODY
chmod +x "$HOOK"

echo "install-git-hooks: installed $HOOK -> scripts/preflight.sh"
86 changes: 86 additions & 0 deletions scripts/preflight.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
# scripts/preflight.sh
#
# Fast local pre-flight so a trivial ImportError/lint break is caught in
# seconds-to-a-minute instead of after a full CI round-trip.
#
# Steps:
# 1. ruff check <RUFF_TARGET> report-only by default (STRICT_LINT=1 to gate)
# 2. import smoke FATAL: catches broken src imports
# 3. pytest --collect-only FATAL: imports every test module -> ImportError
#
# `--collect-only` does NOT execute tests, but it does import conftest.py, so a
# missing local DB may print connection warnings. Collection still succeeds.
#
# Overridable env vars:
# VENV=<path> venv dir (default: .venv3.14-ubuntu26.04)
# IMPORT_MODULE=<module> package to import (repo-specific default below)
# PYTEST_TARGET=<path> collection target (default: tests)
# RUFF_TARGET=<path> lint target (default: src)
# STRICT_LINT=1 make ruff failures fatal (default: report-only)
# SKIP_LINT=1 / SKIP_IMPORT=1 / SKIP_COLLECT=1
set -uo pipefail

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

VENV="${VENV:-.venv3.14-ubuntu26.04}"
PY="$VENV/bin/python"
RUFF="$VENV/bin/ruff"
IMPORT_MODULE="${IMPORT_MODULE:-rhosocial.activerecord.backend.impl.postgres}"
PYTEST_TARGET="${PYTEST_TARGET:-tests}"
RUFF_TARGET="${RUFF_TARGET:-src}"

if [ ! -x "$PY" ]; then
echo "preflight: venv python not found at $VENV (set VENV=...)" >&2
exit 2
fi

fail=0

if [ "${SKIP_LINT:-0}" != "1" ]; then
echo "==> [1/3] ruff check $RUFF_TARGET"
if [ -x "$RUFF" ]; then lint_cmd=("$RUFF"); else lint_cmd=("$PY" -m ruff); fi
if ! "${lint_cmd[@]}" check "$RUFF_TARGET"; then
if [ "${STRICT_LINT:-0}" = "1" ]; then
echo "preflight: lint failed (STRICT_LINT=1)" >&2
fail=1
else
echo "preflight: lint issues found (non-fatal; set STRICT_LINT=1 to gate)"
fi
fi
else
echo "==> [1/3] ruff check (skipped)"
fi

if [ "${SKIP_IMPORT:-0}" != "1" ]; then
echo "==> [2/3] import smoke: $IMPORT_MODULE"
if ! "$PY" -c "import $IMPORT_MODULE"; then
echo "preflight: import of '$IMPORT_MODULE' failed" >&2
fail=1
fi
else
echo "==> [2/3] import smoke (skipped)"
fi

if [ "${SKIP_COLLECT:-0}" != "1" ]; then
echo "==> [3/3] pytest --collect-only $PYTEST_TARGET (no tests executed)"
log="$(mktemp)"
if ! PYTHONPATH=tests "$PY" -m pytest "$PYTEST_TARGET" \
--collect-only -q -p no:cacheprovider > "$log" 2>&1; then
echo "preflight: test collection failed (ImportError?):" >&2
grep -E "ImportError|ModuleNotFoundError|NameError|ERROR collecting" "$log" | head -30 >&2
fail=1
fi
rm -f "$log"
else
echo "==> [3/3] pytest --collect-only (skipped)"
fi

if [ "$fail" -ne 0 ]; then
echo ""
echo "preflight: FAILED — fix the above before committing/pushing."
exit 1
fi

echo ""
echo "preflight: OK"
Loading
Loading