mirror of
https://github.com/ipnet-mesh/meshcore-hub.git
synced 2026-08-07 01:13:11 +02:00
Phase 1: make ORM/migrations Postgres-compatible
Dialect-neutral fixes that keep SQLite behaviour unchanged: - event_observer: choose INSERT construct by bind dialect so the on_conflict upsert works on both SQLite and Postgres - database: map postgresql:// -> postgresql+asyncpg:// for async sessions (was sqlite-only), via a _to_async_url helper - models: import JSON from sqlalchemy (generic) not the sqlite dialect - alembic/env: render_as_batch only for SQLite (its ALTER TABLE workaround) Full SQLite test suite green (1045 passed) and fresh db upgrade builds all tables; black/flake8/mypy clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+3
-1
@@ -58,7 +58,9 @@ def run_migrations_offline() -> None:
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
render_as_batch=True, # SQLite batch mode for ALTER TABLE
|
||||
# Batch mode is a SQLite-only workaround for its limited ALTER TABLE;
|
||||
# Postgres performs ALTERs directly.
|
||||
render_as_batch=url.startswith("sqlite"),
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
|
||||
@@ -11,6 +11,22 @@ from sqlalchemy.orm import Session, sessionmaker
|
||||
from meshcore_hub.common.models.base import Base
|
||||
|
||||
|
||||
def _to_async_url(database_url: str) -> str:
|
||||
"""Map a sync database URL to its async-driver equivalent.
|
||||
|
||||
Leaves an already driver-qualified URL (``dialect+driver://``) untouched so an
|
||||
explicit driver choice is respected.
|
||||
"""
|
||||
scheme = database_url.split("://", 1)[0]
|
||||
if "+" in scheme:
|
||||
return database_url
|
||||
if scheme == "sqlite":
|
||||
return database_url.replace("sqlite://", "sqlite+aiosqlite://", 1)
|
||||
if scheme in ("postgresql", "postgres"):
|
||||
return database_url.replace(f"{scheme}://", "postgresql+asyncpg://", 1)
|
||||
return database_url
|
||||
|
||||
|
||||
def create_database_engine(
|
||||
database_url: str,
|
||||
echo: bool = False,
|
||||
@@ -142,7 +158,7 @@ class DatabaseManager:
|
||||
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||
|
||||
async_url = self.database_url.replace("sqlite://", "sqlite+aiosqlite://")
|
||||
async_url = _to_async_url(self.database_url)
|
||||
self._async_engine = create_async_engine(async_url, echo=self._echo)
|
||||
|
||||
# Apply the same SQLite pragmas as the sync engine (see
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, String
|
||||
from sqlalchemy.dialects.sqlite import JSON
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, JSON, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from meshcore_hub.common.models.base import Base, TimestampMixin, UUIDMixin, utc_now
|
||||
|
||||
@@ -8,13 +8,13 @@ from sqlalchemy import (
|
||||
DateTime,
|
||||
Float,
|
||||
ForeignKey,
|
||||
Insert,
|
||||
Integer,
|
||||
Index,
|
||||
String,
|
||||
UniqueConstraint,
|
||||
update,
|
||||
)
|
||||
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column, relationship
|
||||
|
||||
from meshcore_hub.common.models.base import Base, TimestampMixin, UUIDMixin, utc_now
|
||||
@@ -122,21 +122,40 @@ def add_event_observer(
|
||||
|
||||
now = observed_at or datetime.now(timezone.utc)
|
||||
|
||||
stmt = (
|
||||
sqlite_insert(EventObserver)
|
||||
.values(
|
||||
id=str(uuid4()),
|
||||
event_type=event_type,
|
||||
event_hash=event_hash,
|
||||
observer_node_id=observer_node_id,
|
||||
snr=snr,
|
||||
path_len=path_len,
|
||||
observed_at=now,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
# Both SQLite and Postgres expose on_conflict_do_nothing() with the same signature,
|
||||
# but the INSERT construct must come from the matching dialect or it emits SQL for
|
||||
# the wrong backend. Build the statement in each branch (rather than aliasing the
|
||||
# insert() function) so the two dialect-specific Insert types stay distinct.
|
||||
values = {
|
||||
"id": str(uuid4()),
|
||||
"event_type": event_type,
|
||||
"event_hash": event_hash,
|
||||
"observer_node_id": observer_node_id,
|
||||
"snr": snr,
|
||||
"path_len": path_len,
|
||||
"observed_at": now,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
conflict_cols = ["event_hash", "observer_node_id"]
|
||||
stmt: Insert
|
||||
|
||||
if session.get_bind().dialect.name == "postgresql":
|
||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
|
||||
stmt = (
|
||||
pg_insert(EventObserver)
|
||||
.values(**values)
|
||||
.on_conflict_do_nothing(index_elements=conflict_cols)
|
||||
)
|
||||
else:
|
||||
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
|
||||
|
||||
stmt = (
|
||||
sqlite_insert(EventObserver)
|
||||
.values(**values)
|
||||
.on_conflict_do_nothing(index_elements=conflict_cols)
|
||||
)
|
||||
.on_conflict_do_nothing(index_elements=["event_hash", "observer_node_id"])
|
||||
)
|
||||
result = session.execute(stmt)
|
||||
rowcount = getattr(result, "rowcount", 0)
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional
|
||||
|
||||
from sqlalchemy import DateTime, Float, ForeignKey, Index, Integer, String, Text
|
||||
from sqlalchemy.dialects.sqlite import JSON
|
||||
from sqlalchemy import DateTime, Float, ForeignKey, Index, Integer, JSON, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from meshcore_hub.common.models.base import Base, TimestampMixin, UUIDMixin, utc_now
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, LargeBinary, String
|
||||
from sqlalchemy.dialects.sqlite import JSON
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, JSON, LargeBinary, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from meshcore_hub.common.models.base import Base, TimestampMixin, UUIDMixin, utc_now
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import BigInteger, DateTime, ForeignKey, Index, Integer, String
|
||||
from sqlalchemy.dialects.sqlite import JSON
|
||||
from sqlalchemy import BigInteger, DateTime, ForeignKey, Index, Integer, JSON, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from meshcore_hub.common.models.base import Base, TimestampMixin, UUIDMixin, utc_now
|
||||
|
||||
Reference in New Issue
Block a user