Skip to content

feat: implement schema change chat functionality with message persistence and thread management#1770

Merged
Artuomka merged 1 commit into
mainfrom
backend_ai_table_schema_context
May 14, 2026
Merged

feat: implement schema change chat functionality with message persistence and thread management#1770
Artuomka merged 1 commit into
mainfrom
backend_ai_table_schema_context

Conversation

@Artuomka
Copy link
Copy Markdown
Collaborator

@Artuomka Artuomka commented May 14, 2026

Summary by CodeRabbit

Release Notes

  • New Features
    • Added conversational threading support for schema changes, enabling users to reference and continue discussions on previous requests
    • API endpoints now provide threadId in responses for seamless continuation of schema change conversations
    • Schema change proposals are tracked through persistent conversation threads with full message history

Review Change Stack

Copilot AI review requested due to automatic review settings May 14, 2026 14:00
@Artuomka Artuomka enabled auto-merge May 14, 2026 14:00
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

📝 Walkthrough

Walkthrough

This PR adds conversational threading to schema-change generation, enabling users to refine schema proposals across multiple turns. It introduces chat entities, repositories, and persistence logic that maintains message history, automatically generates chat names, and validates thread ownership across database connections.

Changes

Schema Change Chat Feature

Layer / File(s) Summary
Entity models and DTOs
backend/src/entities/table-schema/schema-change-chat/schema-change-chat/schema-change-chat.entity.ts, backend/src/entities/table-schema/schema-change-chat/schema-change-chat-message/schema-change-chat-message.entity.ts, backend/src/entities/table-schema/application/data-transfer-objects/*, backend/src/entities/table-schema/application/data-structures/*
New SchemaChangeChatEntity and SchemaChangeChatMessageEntity model conversation threads with cascade-delete relations to users and connections. Updated GenerateSchemaChangeDto, SchemaChangeBatchResponseDto, and GenerateSchemaChangeDs to include optional threadId field for request/response contracts.
Repository interfaces and implementations
backend/src/entities/table-schema/schema-change-chat/schema-change-chat/repository/*, backend/src/entities/table-schema/schema-change-chat/schema-change-chat-message/repository/*
ISchemaChangeChatRepository and ISchemaChangeChatMessageRepository interfaces define typed CRUD and query methods. Implementations use TypeORM query builders to find chats by ID and user, list chats per connection, create chats with optional names, and manage message history with ordering and batch association.
Application context and module setup
backend/src/common/application/global-database-context.interface.ts, backend/src/common/application/global-database-context.ts, backend/src/entities/table-schema/table-schema.module.ts
Extended GlobalDatabaseContext to wire and expose schema-change chat and message repositories via getters. Updated TableSchemaModule to register both chat entities with TypeORM feature configuration.
Schema generation with threaded conversations
backend/src/entities/table-schema/use-cases/generate-schema-change.use-case.ts, backend/src/entities/table-schema/table-schema.controller.ts
GenerateSchemaChangeUseCase now resolves or creates chat threads, replays message history when continuing conversations, persists user prompts and AI proposals as separate messages, asynchronously generates and updates chat names (with Sentry error capture), and returns threadId in response. Controller endpoint updated to pass optional threadId through request and include it in response DTO.
Database schema migration
backend/src/migrations/1778767036234-AddSchemaChangeChatEntities.ts
Migration creates schema_change_chat table with cascade-delete FKs to user and connection, defines schema_change_chat_message_role_enum with user/ai/system roles, creates schema_change_chat_message table linking to chat, and removes response_id column from existing ai_chat_message table.
End-to-end tests
backend/test/ava-tests/non-saas-tests/non-saas-table-schema-postgres-e2e.test.ts
Three new serial tests validate: (1) schema generation without threadId creates a new chat and persists user/AI messages, (2) calling generate with the same threadId continues the conversation and updates last_batch_id, (3) reusing a threadId from a different connection returns HTTP 400.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • lyubov-voloshko

Poem

🐰 A thread through the schema, a chat that won't lose,
Memory preserved in each turn that we choose,
The AI learns context, refines with a smile,
Cascading relations, and names all the while! 🌿

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Security Check ⚠️ Warning Invalid threadIds silently create chats instead of rejecting. Nullable role field creates data integrity issues. Violates OWASP input validation principles. Throw NotFoundException for invalid threadIds. Make role NOT NULL. Fix name nullability.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: implementing schema change chat functionality with message persistence and thread management. It is specific, clear, and directly reflects the primary objectives of the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch backend_ai_table_schema_context

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot requested a review from lyubov-voloshko May 14, 2026 14:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Implements schema-change chat threads for the table-schema generation flow, persisting user/AI turns and allowing follow-up prompts to reuse prior conversation context.

Changes:

  • Adds schema-change chat/message entities, repositories, database context wiring, and migration.
  • Extends generate schema-change request/response DTOs and controller handling with optional threadId.
  • Updates generation use case to create/resolve threads, persist messages, update last batch, and adds e2e coverage.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
backend/test/ava-tests/non-saas-tests/non-saas-table-schema-postgres-e2e.test.ts Adds e2e tests for thread creation, continuation, and cross-connection rejection.
backend/src/migrations/1778767036234-AddSchemaChangeChatEntities.ts Creates chat/message tables and related constraints.
backend/src/entities/table-schema/use-cases/generate-schema-change.use-case.ts Adds thread resolution, history construction, message persistence, and chat naming.
backend/src/entities/table-schema/table-schema.module.ts Registers new chat/message entities with TypeORM module features.
backend/src/entities/table-schema/table-schema.controller.ts Passes optional body threadId into generation use case.
backend/src/entities/table-schema/schema-change-chat/schema-change-chat/schema-change-chat.entity.ts Defines schema-change chat thread entity.
backend/src/entities/table-schema/schema-change-chat/schema-change-chat/repository/schema-change-chat-repository.interface.ts Defines chat repository contract.
backend/src/entities/table-schema/schema-change-chat/schema-change-chat/repository/schema-change-chat-repository.extension.ts Implements chat repository methods.
backend/src/entities/table-schema/schema-change-chat/schema-change-chat-message/schema-change-chat-message.entity.ts Defines persisted chat message entity.
backend/src/entities/table-schema/schema-change-chat/schema-change-chat-message/repository/schema-change-chat-message-repository.interface.ts Defines chat message repository contract.
backend/src/entities/table-schema/schema-change-chat/schema-change-chat-message/repository/schema-change-chat-message-repository.extension.ts Implements chat message persistence/query helpers.
backend/src/entities/table-schema/application/data-transfer-objects/schema-change-batch-response.dto.ts Adds threadId to generate response DTO.
backend/src/entities/table-schema/application/data-transfer-objects/generate-schema-change.dto.ts Adds optional request body threadId validation/docs.
backend/src/entities/table-schema/application/data-structures/generate-schema-change.ds.ts Adds threadId to internal use-case input.
backend/src/common/application/global-database-context.ts Wires new repositories into global database context.
backend/src/common/application/global-database-context.interface.ts Exposes new repositories on the database context interface.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

await queryRunner.query(
`CREATE TABLE "schema_change_chat_message" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "message" text, "role" "public"."schema_change_chat_message_role_enum", "batch_id" uuid, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP DEFAULT now(), "chat_id" uuid NOT NULL, CONSTRAINT "PK_5984cdb248fa9c2f55f5a19022c" PRIMARY KEY ("id"))`,
);
await queryRunner.query(`ALTER TABLE "ai_chat_message" DROP COLUMN "response_id"`);
Comment on lines +180 to +185
if (existing) {
if (existing.connection_id !== connectionId) {
throw new BadRequestException('Provided threadId belongs to a different connection.');
}
return { chat: existing, isNewChat: false };
}
? createElasticsearchSchemaChangeTools()
: createSchemaChangeTools();

await this._dbContext.schemaChangeChatMessageRepository.saveMessage(chat.id, userPrompt, MessageRole.user);
Comment on lines +201 to +213
const previousMessages = await this._dbContext.schemaChangeChatMessageRepository.findMessagesForChat(chatId);
const builder = new MessageBuilder().system(systemPrompt);

for (const msg of previousMessages) {
if (msg.role === MessageRole.user) {
builder.human(msg.message);
} else if (msg.role === MessageRole.ai) {
builder.ai(msg.message);
}
}

builder.human(userMessage);
return builder.build();
`CREATE TYPE "public"."schema_change_chat_message_role_enum" AS ENUM('user', 'ai', 'system')`,
);
await queryRunner.query(
`CREATE TABLE "schema_change_chat_message" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "message" text, "role" "public"."schema_change_chat_message_role_enum", "batch_id" uuid, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP DEFAULT now(), "chat_id" uuid NOT NULL, CONSTRAINT "PK_5984cdb248fa9c2f55f5a19022c" PRIMARY KEY ("id"))`,

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "schema_change_chat" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP DEFAULT now(), "user_id" uuid NOT NULL, "connection_id" character varying(38) NOT NULL, "last_batch_id" uuid, CONSTRAINT "PK_60082e3e240c265fc043290381d" PRIMARY KEY ("id"))`,
required: false,
nullable: true,
description:
'Conversation thread ID. Present when the request used or created a chat thread. Pass it back as the threadId query param on subsequent generate calls to continue the conversation with full prior context.',
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (3)
backend/src/migrations/1778767036234-AddSchemaChangeChatEntities.ts (1)

6-26: ⚡ Quick win

Consider adding indexes on the new foreign-key columns.

PostgreSQL does not automatically index FK columns. The cascade-delete paths (user→chats, connection→chats, schema_change_chat→messages) and the typical query patterns ("list chats for user/connection", "list messages for chat") will benefit from indexes on schema_change_chat(user_id), schema_change_chat(connection_id), and schema_change_chat_message(chat_id). Without them, parent deletes and history loads will scan these tables.

♻️ Suggested indexes (add to up() and reverse in down())
 		await queryRunner.query(
 			`ALTER TABLE "schema_change_chat_message" ADD CONSTRAINT "FK_32825f4780664738f60fa75cd50" FOREIGN KEY ("chat_id") REFERENCES "schema_change_chat"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
 		);
+		await queryRunner.query(`CREATE INDEX "IDX_schema_change_chat_user_id" ON "schema_change_chat" ("user_id")`);
+		await queryRunner.query(`CREATE INDEX "IDX_schema_change_chat_connection_id" ON "schema_change_chat" ("connection_id")`);
+		await queryRunner.query(`CREATE INDEX "IDX_schema_change_chat_message_chat_id" ON "schema_change_chat_message" ("chat_id")`);
 	}
 	public async down(queryRunner: QueryRunner): Promise<void> {
+		await queryRunner.query(`DROP INDEX "IDX_schema_change_chat_message_chat_id"`);
+		await queryRunner.query(`DROP INDEX "IDX_schema_change_chat_connection_id"`);
+		await queryRunner.query(`DROP INDEX "IDX_schema_change_chat_user_id"`);
 		await queryRunner.query(
 			`ALTER TABLE "schema_change_chat_message" DROP CONSTRAINT "FK_32825f4780664738f60fa75cd50"`,
 		);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/migrations/1778767036234-AddSchemaChangeChatEntities.ts` around
lines 6 - 26, The migration creates schema_change_chat and
schema_change_chat_message and adds FKs but does not create indexes on the FK
columns; add CREATE INDEX statements for schema_change_chat(user_id),
schema_change_chat(connection_id) and schema_change_chat_message(chat_id) inside
the up() method (after the corresponding CREATE TABLE/ALTER TABLE statements)
and add corresponding DROP INDEX statements in the down() method to reverse the
change so deletes and common queries are not forced to do full-table scans;
reference the up() method and the tables schema_change_chat and
schema_change_chat_message and the columns user_id, connection_id, chat_id when
making the changes.
backend/src/entities/table-schema/schema-change-chat/schema-change-chat-message/repository/schema-change-chat-message-repository.extension.ts (1)

13-19: ⚡ Quick win

Consider using entity class instead of string for type safety.

Line 16 uses the string 'schema_change_chat_message' as the table reference, while the entity class SchemaChangeChatMessageEntity is available. Using the entity class provides better type safety and refactoring support.

♻️ Proposed refactor
 async deleteMessagesForChat(chatId: string): Promise<void> {
 	await this.createQueryBuilder()
 		.delete()
-		.from('schema_change_chat_message')
+		.from(SchemaChangeChatMessageEntity)
 		.where('chat_id = :chatId', { chatId })
 		.execute();
 },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/entities/table-schema/schema-change-chat/schema-change-chat-message/repository/schema-change-chat-message-repository.extension.ts`
around lines 13 - 19, The deleteMessagesForChat method uses the literal table
name 'schema_change_chat_message' which is fragile; change it to use the entity
class SchemaChangeChatMessageEntity to gain type safety and refactor resilience.
Update the query in deleteMessagesForChat to reference the entity (e.g., pass
SchemaChangeChatMessageEntity to createQueryBuilder or to .from) instead of the
string, keep the same where parameter (chatId) and .execute() call, and ensure
any import of SchemaChangeChatMessageEntity is added at the top of the file.
backend/src/entities/table-schema/use-cases/generate-schema-change.use-case.ts (1)

103-106: 💤 Low value

Redundant error handler in fire-and-forget call.

The .catch() handler at line 103-105 is redundant because generateAndUpdateChatName already has an internal try-catch that captures exceptions to Sentry (line 248-250) without re-throwing. The outer .catch() will never execute.

♻️ Simplification
 if (isNewChat) {
-	this.generateAndUpdateChatName(chat.id, userPrompt).catch((error) => {
-		Sentry.captureException(error);
-	});
+	this.generateAndUpdateChatName(chat.id, userPrompt);
 }

The internal error handling in generateAndUpdateChatName (lines 226-250) already captures exceptions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/entities/table-schema/use-cases/generate-schema-change.use-case.ts`
around lines 103 - 106, The call site uses a redundant .catch on the
fire-and-forget call to generateAndUpdateChatName(chat.id, userPrompt) even
though generateAndUpdateChatName already handles and Sentry-captures its own
errors; remove the trailing .catch((error) => { Sentry.captureException(error);
}) so the call becomes a plain fire-and-forget invocation, or if you intended
outer handling instead, change generateAndUpdateChatName to rethrow errors and
use await + .catch here—refer to generateAndUpdateChatName for the internal
try/catch and the caller where generateAndUpdateChatName(chat.id,
userPrompt).catch(...) is used.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@backend/src/entities/table-schema/application/data-transfer-objects/generate-schema-change.dto.ts`:
- Around line 19-29: The DTO's `@ApiProperty` marks threadId as nullable but the
TypeScript declaration excludes null; update the property in
generate-schema-change.dto.ts so the TypeScript type allows null (e.g.,
threadId?: string | null) and keep the existing decorators (`@ApiProperty` with
nullable: true, `@IsOptional`(), `@IsUUID`()) so validation still skips
null/undefined and UUID validation runs for non-null values.

In
`@backend/src/entities/table-schema/application/data-transfer-objects/schema-change-batch-response.dto.ts`:
- Around line 21-22: The response DTO's description for the threadId field
incorrectly tells clients to pass it as a query param; update the docstring for
the threadId property in SchemaChangeBatchResponseDto (the DTO where threadId is
declared) to state that threadId should be returned by the server and included
in subsequent requests in the request body (not as a query param), e.g.,
"Present when the request used or created a chat thread. Include this threadId
in the request body of subsequent generate calls to continue the conversation
with full prior context."

In
`@backend/src/entities/table-schema/schema-change-chat/schema-change-chat-message/schema-change-chat-message.entity.ts`:
- Around line 22-23: The entity column for role currently allows null even
though saveMessage(...) requires a role; change the Column on
schema-change-chat-message.entity (the role property of type MessageRole) to be
non-nullable (nullable: false) and remove the default null so the DB enforces
presence; additionally add a migration to backfill or reject existing null rows
(e.g., set a sensible default role or fail migration) and apply the schema
change so the database constraint matches the saveMessage(...) contract.

In
`@backend/src/entities/table-schema/schema-change-chat/schema-change-chat/schema-change-chat.entity.ts`:
- Around line 21-22: The entity property "name" is declared as type string but
the Column decorator lacks nullable: true while createChatForUser writes name:
null; update the SchemaChangeChat entity by adding nullable: true to the `@Column`
on the name property (and/or adjust the TypeScript type to string | null) so the
DB mapping and the write path (createChatForUser) are consistent; ensure you
modify the Column decorator on the name field and the property type accordingly.

In
`@backend/src/entities/table-schema/use-cases/generate-schema-change.use-case.ts`:
- Around line 102-106: The call to generateAndUpdateChatName in the isNewChat
branch is fire-and-forget and can fail because GlobalDatabaseContext is
request-scoped and may be destroyed before the async work completes; either
await the call (await this.generateAndUpdateChatName(chat.id, userPrompt)) so
the request lifecycle keeps the DB context alive and handle errors with
try/catch + Sentry.captureException, or instead delegate the task to a
singleton/background worker (e.g., a ChatNameGenerationService.enqueue(chat.id,
userPrompt) or BackgroundJobService) that performs the DB write outside the
request scope; update the code that currently calls
this.generateAndUpdateChatName(...).catch(...) accordingly.
- Around line 173-189: The resolveChat method currently creates a new chat when
a threadId is supplied but not found/owned by the user; change it so that if a
non-null threadId is provided and findChatByIdAndUserId returns no chat, you
throw a BadRequestException instead of falling through to createChatForUser.
Keep the existing connection_id check (existing.connection_id !== connectionId)
and only call createChatForUser when threadId is null; update the logic in
resolveChat to detect the "threadId supplied but missing" case and raise an
error.

---

Nitpick comments:
In
`@backend/src/entities/table-schema/schema-change-chat/schema-change-chat-message/repository/schema-change-chat-message-repository.extension.ts`:
- Around line 13-19: The deleteMessagesForChat method uses the literal table
name 'schema_change_chat_message' which is fragile; change it to use the entity
class SchemaChangeChatMessageEntity to gain type safety and refactor resilience.
Update the query in deleteMessagesForChat to reference the entity (e.g., pass
SchemaChangeChatMessageEntity to createQueryBuilder or to .from) instead of the
string, keep the same where parameter (chatId) and .execute() call, and ensure
any import of SchemaChangeChatMessageEntity is added at the top of the file.

In
`@backend/src/entities/table-schema/use-cases/generate-schema-change.use-case.ts`:
- Around line 103-106: The call site uses a redundant .catch on the
fire-and-forget call to generateAndUpdateChatName(chat.id, userPrompt) even
though generateAndUpdateChatName already handles and Sentry-captures its own
errors; remove the trailing .catch((error) => { Sentry.captureException(error);
}) so the call becomes a plain fire-and-forget invocation, or if you intended
outer handling instead, change generateAndUpdateChatName to rethrow errors and
use await + .catch here—refer to generateAndUpdateChatName for the internal
try/catch and the caller where generateAndUpdateChatName(chat.id,
userPrompt).catch(...) is used.

In `@backend/src/migrations/1778767036234-AddSchemaChangeChatEntities.ts`:
- Around line 6-26: The migration creates schema_change_chat and
schema_change_chat_message and adds FKs but does not create indexes on the FK
columns; add CREATE INDEX statements for schema_change_chat(user_id),
schema_change_chat(connection_id) and schema_change_chat_message(chat_id) inside
the up() method (after the corresponding CREATE TABLE/ALTER TABLE statements)
and add corresponding DROP INDEX statements in the down() method to reverse the
change so deletes and common queries are not forced to do full-table scans;
reference the up() method and the tables schema_change_chat and
schema_change_chat_message and the columns user_id, connection_id, chat_id when
making the changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e1bc43fe-4596-4b43-bd44-412ca53fcd3c

📥 Commits

Reviewing files that changed from the base of the PR and between e6f9a80 and 1209be1.

📒 Files selected for processing (16)
  • backend/src/common/application/global-database-context.interface.ts
  • backend/src/common/application/global-database-context.ts
  • backend/src/entities/table-schema/application/data-structures/generate-schema-change.ds.ts
  • backend/src/entities/table-schema/application/data-transfer-objects/generate-schema-change.dto.ts
  • backend/src/entities/table-schema/application/data-transfer-objects/schema-change-batch-response.dto.ts
  • backend/src/entities/table-schema/schema-change-chat/schema-change-chat-message/repository/schema-change-chat-message-repository.extension.ts
  • backend/src/entities/table-schema/schema-change-chat/schema-change-chat-message/repository/schema-change-chat-message-repository.interface.ts
  • backend/src/entities/table-schema/schema-change-chat/schema-change-chat-message/schema-change-chat-message.entity.ts
  • backend/src/entities/table-schema/schema-change-chat/schema-change-chat/repository/schema-change-chat-repository.extension.ts
  • backend/src/entities/table-schema/schema-change-chat/schema-change-chat/repository/schema-change-chat-repository.interface.ts
  • backend/src/entities/table-schema/schema-change-chat/schema-change-chat/schema-change-chat.entity.ts
  • backend/src/entities/table-schema/table-schema.controller.ts
  • backend/src/entities/table-schema/table-schema.module.ts
  • backend/src/entities/table-schema/use-cases/generate-schema-change.use-case.ts
  • backend/src/migrations/1778767036234-AddSchemaChangeChatEntities.ts
  • backend/test/ava-tests/non-saas-tests/non-saas-table-schema-postgres-e2e.test.ts

Comment on lines +19 to 29
@ApiProperty({
type: String,
required: false,
nullable: true,
description:
'Optional thread ID to continue an existing conversation. When supplied, prior turns are prepended to the AI prompt, giving the model context for iterative refinement (e.g. "now also add an index", "rename it to created_at"). Omit to start a fresh thread; the returned threadId can be passed back on the next call.',
})
@IsOptional()
@IsUUID()
threadId?: string;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align nullable API contract with TypeScript type.

@ApiProperty marks threadId as nullable, but the property type excludes null. This creates a type-level mismatch for consumers and internal mappings.

Proposed fix
-	threadId?: string;
+	threadId?: string | null;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@ApiProperty({
type: String,
required: false,
nullable: true,
description:
'Optional thread ID to continue an existing conversation. When supplied, prior turns are prepended to the AI prompt, giving the model context for iterative refinement (e.g. "now also add an index", "rename it to created_at"). Omit to start a fresh thread; the returned threadId can be passed back on the next call.',
})
@IsOptional()
@IsUUID()
threadId?: string;
}
`@ApiProperty`({
type: String,
required: false,
nullable: true,
description:
'Optional thread ID to continue an existing conversation. When supplied, prior turns are prepended to the AI prompt, giving the model context for iterative refinement (e.g. "now also add an index", "rename it to created_at"). Omit to start a fresh thread; the returned threadId can be passed back on the next call.',
})
`@IsOptional`()
`@IsUUID`()
threadId?: string | null;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/entities/table-schema/application/data-transfer-objects/generate-schema-change.dto.ts`
around lines 19 - 29, The DTO's `@ApiProperty` marks threadId as nullable but the
TypeScript declaration excludes null; update the property in
generate-schema-change.dto.ts so the TypeScript type allows null (e.g.,
threadId?: string | null) and keep the existing decorators (`@ApiProperty` with
nullable: true, `@IsOptional`(), `@IsUUID`()) so validation still skips
null/undefined and UUID validation runs for non-null values.

Comment on lines +21 to +22
'Conversation thread ID. Present when the request used or created a chat thread. Pass it back as the threadId query param on subsequent generate calls to continue the conversation with full prior context.',
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix response docs: threadId is not a query param in this contract.

The description currently instructs clients to send threadId as a query param, but the request DTO models it as a body field. This will confuse API consumers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/entities/table-schema/application/data-transfer-objects/schema-change-batch-response.dto.ts`
around lines 21 - 22, The response DTO's description for the threadId field
incorrectly tells clients to pass it as a query param; update the docstring for
the threadId property in SchemaChangeBatchResponseDto (the DTO where threadId is
declared) to state that threadId should be returned by the server and included
in subsequent requests in the request body (not as a query param), e.g.,
"Present when the request used or created a chat thread. Include this threadId
in the request body of subsequent generate calls to continue the conversation
with full prior context."

Comment on lines +22 to +23
@Column({ nullable: true, default: null, type: 'enum', enum: MessageRole })
role: MessageRole;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce non-null role at persistence level.

saveMessage(...) requires role, but this entity allows null in DB. That permits invalid rows and can break role-based prompt reconstruction later.

Proposed fix
-	`@Column`({ nullable: true, default: null, type: 'enum', enum: MessageRole })
+	`@Column`({ type: 'enum', enum: MessageRole })
 	role: MessageRole;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Column({ nullable: true, default: null, type: 'enum', enum: MessageRole })
role: MessageRole;
`@Column`({ type: 'enum', enum: MessageRole })
role: MessageRole;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/entities/table-schema/schema-change-chat/schema-change-chat-message/schema-change-chat-message.entity.ts`
around lines 22 - 23, The entity column for role currently allows null even
though saveMessage(...) requires a role; change the Column on
schema-change-chat-message.entity (the role property of type MessageRole) to be
non-nullable (nullable: false) and remove the default null so the DB enforces
presence; additionally add a migration to backfill or reject existing null rows
(e.g., set a sensible default role or fail migration) and apply the schema
change so the database constraint matches the saveMessage(...) contract.

Comment on lines +21 to +22
@Column({ default: null })
name: string;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

name nullability is inconsistent with write path.

createChatForUser stores name: null when omitted, but this entity declares name as non-null string without nullable: true. This mismatch can cause runtime insert errors and weakens type safety.

Proposed fix
-	`@Column`({ default: null })
-	name: string;
+	`@Column`({ nullable: true, default: null })
+	name: string | null;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Column({ default: null })
name: string;
`@Column`({ nullable: true, default: null })
name: string | null;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/entities/table-schema/schema-change-chat/schema-change-chat/schema-change-chat.entity.ts`
around lines 21 - 22, The entity property "name" is declared as type string but
the Column decorator lacks nullable: true while createChatForUser writes name:
null; update the SchemaChangeChat entity by adding nullable: true to the `@Column`
on the name property (and/or adjust the TypeScript type to string | null) so the
DB mapping and the write path (createChatForUser) are consistent; ensure you
modify the Column decorator on the name field and the property type accordingly.

Comment on lines +102 to +106
if (isNewChat) {
this.generateAndUpdateChatName(chat.id, userPrompt).catch((error) => {
Sentry.captureException(error);
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚖️ Poor tradeoff

Fire-and-forget async operation with request-scoped context may cause connection issues.

The generateAndUpdateChatName call at line 103 is fire-and-forget (not awaited), but GlobalDatabaseContext is request-scoped (Scope.REQUEST). If the HTTP request completes before the async operation finishes, the database context may be destroyed, causing the operation to fail with connection errors.

Potential solutions

Option 1 (simplest): Await the operation to ensure it completes before the request ends:

-	if (isNewChat) {
-		this.generateAndUpdateChatName(chat.id, userPrompt).catch((error) => {
-			Sentry.captureException(error);
-		});
-	}
+	if (isNewChat) {
+		await this.generateAndUpdateChatName(chat.id, userPrompt);
+	}

Note: This adds ~500ms-2s latency to the response. If acceptable, this is the safest fix.

Option 2 (better isolation): Queue the operation for a background worker or use a singleton-scoped service to handle the database write independently of the request lifecycle.

Option 3 (acceptable risk): Keep current behavior but add monitoring to track failures via Sentry. Document that chat name generation is best-effort and may occasionally fail.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/entities/table-schema/use-cases/generate-schema-change.use-case.ts`
around lines 102 - 106, The call to generateAndUpdateChatName in the isNewChat
branch is fire-and-forget and can fail because GlobalDatabaseContext is
request-scoped and may be destroyed before the async work completes; either
await the call (await this.generateAndUpdateChatName(chat.id, userPrompt)) so
the request lifecycle keeps the DB context alive and handle errors with
try/catch + Sentry.captureException, or instead delegate the task to a
singleton/background worker (e.g., a ChatNameGenerationService.enqueue(chat.id,
userPrompt) or BackgroundJobService) that performs the DB write outside the
request scope; update the code that currently calls
this.generateAndUpdateChatName(...).catch(...) accordingly.

Comment on lines +173 to +189
private async resolveChat(
threadId: string | null,
userId: string,
connectionId: string,
): Promise<{ chat: SchemaChangeChatEntity; isNewChat: boolean }> {
if (threadId) {
const existing = await this._dbContext.schemaChangeChatRepository.findChatByIdAndUserId(threadId, userId);
if (existing) {
if (existing.connection_id !== connectionId) {
throw new BadRequestException('Provided threadId belongs to a different connection.');
}
return { chat: existing, isNewChat: false };
}
}
const created = await this._dbContext.schemaChangeChatRepository.createChatForUser(userId, connectionId);
return { chat: created, isNewChat: true };
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Invalid threadId silently creates a new chat instead of throwing an error.

When a user provides a threadId that doesn't exist (or belongs to a different user), the code falls through to line 187 and creates a new chat instead of throwing an error. This violates the principle of least surprise—the user expects to continue an existing conversation but unknowingly starts a new one.

🔧 Proposed fix
 private async resolveChat(
 	threadId: string | null,
 	userId: string,
 	connectionId: string,
 ): Promise<{ chat: SchemaChangeChatEntity; isNewChat: boolean }> {
 	if (threadId) {
 		const existing = await this._dbContext.schemaChangeChatRepository.findChatByIdAndUserId(threadId, userId);
-		if (existing) {
-			if (existing.connection_id !== connectionId) {
-				throw new BadRequestException('Provided threadId belongs to a different connection.');
-			}
-			return { chat: existing, isNewChat: false };
+		if (!existing) {
+			throw new NotFoundException('Chat thread not found.');
+		}
+		if (existing.connection_id !== connectionId) {
+			throw new BadRequestException('Provided threadId belongs to a different connection.');
 		}
+		return { chat: existing, isNewChat: false };
 	}
 	const created = await this._dbContext.schemaChangeChatRepository.createChatForUser(userId, connectionId);
 	return { chat: created, isNewChat: true };
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private async resolveChat(
threadId: string | null,
userId: string,
connectionId: string,
): Promise<{ chat: SchemaChangeChatEntity; isNewChat: boolean }> {
if (threadId) {
const existing = await this._dbContext.schemaChangeChatRepository.findChatByIdAndUserId(threadId, userId);
if (existing) {
if (existing.connection_id !== connectionId) {
throw new BadRequestException('Provided threadId belongs to a different connection.');
}
return { chat: existing, isNewChat: false };
}
}
const created = await this._dbContext.schemaChangeChatRepository.createChatForUser(userId, connectionId);
return { chat: created, isNewChat: true };
}
private async resolveChat(
threadId: string | null,
userId: string,
connectionId: string,
): Promise<{ chat: SchemaChangeChatEntity; isNewChat: boolean }> {
if (threadId) {
const existing = await this._dbContext.schemaChangeChatRepository.findChatByIdAndUserId(threadId, userId);
if (!existing) {
throw new NotFoundException('Chat thread not found.');
}
if (existing.connection_id !== connectionId) {
throw new BadRequestException('Provided threadId belongs to a different connection.');
}
return { chat: existing, isNewChat: false };
}
const created = await this._dbContext.schemaChangeChatRepository.createChatForUser(userId, connectionId);
return { chat: created, isNewChat: true };
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/src/entities/table-schema/use-cases/generate-schema-change.use-case.ts`
around lines 173 - 189, The resolveChat method currently creates a new chat when
a threadId is supplied but not found/owned by the user; change it so that if a
non-null threadId is provided and findChatByIdAndUserId returns no chat, you
throw a BadRequestException instead of falling through to createChatForUser.
Keep the existing connection_id check (existing.connection_id !== connectionId)
and only call createChatForUser when threadId is null; update the logic in
resolveChat to detect the "threadId supplied but missing" case and raise an
error.

@Artuomka Artuomka disabled auto-merge May 14, 2026 14:06
@Artuomka Artuomka enabled auto-merge May 14, 2026 14:08
@Artuomka Artuomka merged commit 99f2a58 into main May 14, 2026
20 checks passed
@Artuomka Artuomka deleted the backend_ai_table_schema_context branch May 14, 2026 14:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants