Skip to content

feat(store): add ConversationItemStore trait and implementations#645

Open
leseb wants to merge 1 commit into
praxis-proxy:mainfrom
leseb:leseb/issue-631-conversation-item-store
Open

feat(store): add ConversationItemStore trait and implementations#645
leseb wants to merge 1 commit into
praxis-proxy:mainfrom
leseb:leseb/issue-631-conversation-item-store

Conversation

@leseb

@leseb leseb commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add ConversationItemStore trait with 7 item CRUD methods: create (batch upsert), list (cursor-based pagination with composite (position, item_id) ordering), get, delete single item, get position, max position, and delete all items for a conversation
  • Implement for both SQLite and PostgreSQL backends with ON CONFLICT upsert semantics
  • Cascade-delete conversation items within a transaction when delete_conversation is called and items table is configured
  • Comprehensive test coverage: 12 SQLite tests + 12 Postgres parity tests (ignored, require DATABASE_URL) covering pagination, duplicate positions, tenant isolation, upsert, field round-tripping, nonexistent cursor handling, and cascade delete

Test plan

  • cargo test -p praxis-proxy-filter -- store — 215 passed, 24 ignored
  • cargo clippy -p praxis-proxy-filter -- -D warnings — clean
  • cargo +nightly fmt — clean
  • CI green

Closes #631

🤖 Generated with Claude Code

@leseb leseb requested review from shaneutt and twghu as code owners June 22, 2026 09:29
@praxis-bot-app

Copy link
Copy Markdown

PR too large: 1341 lines added (limit: 750, excludes Cargo files, tests, docs, examples, and benchmarks). Please split into smaller PRs. Add skip/pr-conventions label to override.

@praxis-bot-app

Copy link
Copy Markdown

AI tool authorship detected:

  • 0e7e609: Co-Authored-By: Claude <noreply@anthropic.com>

Sorry, this project does not accept commits authored by tools as valid.
Commits need to be authored by and signed-off by the human(s) responsible for the PR, with their name and contact.

@leseb leseb force-pushed the leseb/issue-631-conversation-item-store branch from 0e7e609 to 506674e Compare June 22, 2026 09:31
@leseb leseb added the skip/pr-conventions Skip conventions checks for PRs label Jun 22, 2026
Add a separate ConversationItemStore trait for conversation item CRUD
operations (create, get, list, delete) with cursor-based pagination
using composite (position, item_id) ordering. Implement for both
SQLite and PostgreSQL backends with ON CONFLICT upsert semantics.

Conversation deletion now cascade-deletes associated items within a
transaction when the items table is configured.

Closes praxis-proxy#631

Signed-off-by: Sébastien Han <seb@redhat.com>
@leseb leseb force-pushed the leseb/issue-631-conversation-item-store branch from 506674e to 1bff360 Compare June 22, 2026 09:42

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review

Summary: Adds a ConversationItemStore trait with SQLite and PostgreSQL implementations for persisting OpenAI Responses API conversation items.

Overall: Clean trait design with good test coverage for the happy paths. The main concern is transactional safety in create_conversation_items and the upsert conflict key design.

Severity Count
Critical 0
Large 1
Medium 2

limit: u32,
ascending: bool,
) -> Result<Vec<ConversationItemRecord>, StoreError> {
let table = self

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Large] create_conversation_items inserts items one-by-one in a loop without wrapping the batch in a transaction. If the third of five inserts fails, the first two are already committed — leaving the store in a partial state with no way for the caller to know which items succeeded. Wrap the loop in conn.call(move |conn| { let tx = conn.transaction()?; ... tx.commit()?; }) so the batch is atomic.

.await
.map_err(|e| StoreError::Database(e.to_string()))?;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] The upsert conflict key is (item_id, tenant_id), but conversation_id is in the UPDATE SET clause. This means an item with the same item_id and tenant_id but a different conversation_id silently migrates to the new conversation on conflict, rather than being rejected. If cross-conversation item migration is not intended, add conversation_id to the conflict target or add a check constraint.

remaining.is_empty(),
"items should be cascade-deleted with conversation"
);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] No test exercises the StoreError::Unavailable error path — i.e., calling ConversationItemStore methods on a store instance where items_table was not configured. Add a negative test that constructs a store without the items table and asserts that each method returns StoreError::Unavailable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip/pr-conventions Skip conventions checks for PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Store layer: add ConversationItemStore trait and implementations

2 participants