feat(12-01): durable owner-scoped cloud metadata schema (migration 0006 + models)
- Migration 0006: cloud_items, cloud_item_topics, cloud_folder_states tables - cloud_connections: add display_name_override column for same-provider disambiguation - CloudItem, CloudItemTopic, CloudFolderState ORM models with owner/connection indexes - Unique (connection_id, provider_item_id) boundary; no MinIO object_key field - Root folder state representable as parent_ref='' without CloudItem parent row - services/cloud_items.py: resolve_owned_connection, upsert, list, reconcile, folder state - 17 unit/integration tests covering model fields, isolation, quota invariant, idempotency
This commit is contained in:
@@ -318,6 +318,144 @@ class CloudConnection(Base):
|
||||
__table_args__ = (Index("ix_cloud_connections_user", "user_id"),)
|
||||
|
||||
|
||||
class CloudItem(Base):
|
||||
"""Durable per-item cloud metadata indexed by (connection_id, provider_item_id).
|
||||
|
||||
Phase 12: metadata browsing only — no MinIO object_key field.
|
||||
Phase 14: extracted_text, analysis_status, semantic_index_status, semantic_index_data
|
||||
are reserved for byte analysis and semantic search.
|
||||
|
||||
D-18: provider_size never flows to quotas.used_bytes.
|
||||
"""
|
||||
|
||||
__tablename__ = "cloud_items"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("users.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
connection_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("cloud_connections.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
provider_item_id: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
parent_ref: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
path_snapshot: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
name: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
kind: Mapped[str] = mapped_column(String(8), nullable=False) # "file" | "folder"
|
||||
content_type: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
# Provider-reported size — never used to update quotas.used_bytes (D-18)
|
||||
provider_size: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True)
|
||||
etag: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
version: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
modified_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
TIMESTAMP(timezone=True), nullable=True
|
||||
)
|
||||
last_seen_at: Mapped[datetime] = mapped_column(
|
||||
TIMESTAMP(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
deleted_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
TIMESTAMP(timezone=True), nullable=True
|
||||
)
|
||||
# Phase 14 reserved fields
|
||||
extracted_text: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
analysis_status: Mapped[str] = mapped_column(
|
||||
String(16), nullable=False, default="pending"
|
||||
)
|
||||
semantic_index_status: Mapped[str] = mapped_column(
|
||||
String(16), nullable=False, default="none"
|
||||
)
|
||||
semantic_index_data: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
TIMESTAMP(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
TIMESTAMP(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"connection_id",
|
||||
"provider_item_id",
|
||||
name="uq_cloud_items_connection_provider_item",
|
||||
),
|
||||
Index("ix_cloud_items_user_id", "user_id"),
|
||||
Index("ix_cloud_items_connection_id", "connection_id"),
|
||||
Index("ix_cloud_items_connection_parent", "connection_id", "parent_ref"),
|
||||
)
|
||||
|
||||
|
||||
class CloudItemTopic(Base):
|
||||
"""Association between CloudItem and Topic — no Document row required."""
|
||||
|
||||
__tablename__ = "cloud_item_topics"
|
||||
|
||||
cloud_item_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("cloud_items.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
)
|
||||
topic_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("topics.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
)
|
||||
|
||||
|
||||
class CloudFolderState(Base):
|
||||
"""Per-connection folder freshness row for cached-first navigation.
|
||||
|
||||
parent_ref = '' represents the connection root (allows unique constraint).
|
||||
refresh_state: "refreshing" | "fresh" | "warning"
|
||||
"""
|
||||
|
||||
__tablename__ = "cloud_folder_states"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("users.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
connection_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("cloud_connections.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
# '' for root, provider ref for non-root
|
||||
parent_ref: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
||||
refresh_state: Mapped[str] = mapped_column(
|
||||
String(16), nullable=False, default="fresh"
|
||||
)
|
||||
last_refreshed_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
TIMESTAMP(timezone=True), nullable=True
|
||||
)
|
||||
error_code: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)
|
||||
error_message: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
TIMESTAMP(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
TIMESTAMP(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"connection_id",
|
||||
"parent_ref",
|
||||
name="uq_cloud_folder_states_connection_parent",
|
||||
),
|
||||
Index("ix_cloud_folder_states_connection", "connection_id"),
|
||||
)
|
||||
|
||||
|
||||
class Group(Base):
|
||||
"""v2 stub — empty table, seeded for schema completeness (PROJECT.md D-02).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user