-
Notifications
You must be signed in to change notification settings - Fork 5.1k
fix(mysql): compatibilidade da coluna lid e queries RAW #2333
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Reviewer's GuideAdds MySQL 8-compatible behavior for JSON-based raw queries and preserves the IsOnWhatsapp.lid column by aligning Prisma schema and MySQL migration; introduces DB-provider branching to support both PostgreSQL and MySQL for key JSON operations in messaging and Chatwoot integrations. Sequence diagram for fetchChats with PostgreSQL/MySQL branchingsequenceDiagram
actor Client
participant ChannelStartupService
participant ConfigService
participant PrismaRepository
participant PostgreSQL
participant MySQL
Client->>ChannelStartupService: fetchChats(query, remoteJid)
ChannelStartupService->>ConfigService: get(DATABASE)
ConfigService-->>ChannelStartupService: Database{PROVIDER}
alt provider == mysql
ChannelStartupService->>PrismaRepository: $queryRaw(MySQL JSON query)
PrismaRepository->>MySQL: execute SELECT with JSON_EXTRACT
MySQL-->>PrismaRepository: rows
else provider != mysql (PostgreSQL)
ChannelStartupService->>PrismaRepository: $queryRaw(PostgreSQL JSON query)
PrismaRepository->>PostgreSQL: execute WITH rankedMessages
PostgreSQL-->>PrismaRepository: rows
end
PrismaRepository-->>ChannelStartupService: results
ChannelStartupService-->>Client: mapped chat list
Sequence diagram for label management with DB-specific raw SQLsequenceDiagram
actor Client
participant BaileysStartupService
participant ConfigService
participant PrismaRepository
participant PostgreSQL
participant MySQL
Client->>BaileysStartupService: addLabel(labelId, instanceId, chatId)
BaileysStartupService->>ConfigService: get(DATABASE)
ConfigService-->>BaileysStartupService: Database{PROVIDER}
alt provider == mysql
BaileysStartupService->>PrismaRepository: $executeRawUnsafe(INSERT ... ON DUPLICATE KEY UPDATE, JSON_ARRAY/JSON_ARRAY_APPEND)
PrismaRepository->>MySQL: execute SQL
MySQL-->>PrismaRepository: affected rows
else provider != mysql (PostgreSQL)
BaileysStartupService->>PrismaRepository: $executeRawUnsafe(INSERT ... ON CONFLICT, to_jsonb/jsonb_array_elements_text)
PrismaRepository->>PostgreSQL: execute SQL
PostgreSQL-->>PrismaRepository: affected rows
end
PrismaRepository-->>BaileysStartupService: result
BaileysStartupService-->>Client: void
ER diagram for IsOnWhatsapp with preserved lid columnerDiagram
IsOnWhatsapp {
int id PK
string remoteJid
string lid
datetime createdAt
datetime updatedAt
}
%% The PR aligns Prisma schema with existing MySQL migration by keeping lid and adjusting timestamps
Class diagram for updated messaging and Chatwoot services with DB provider branchingclassDiagram
class ConfigService {
+get(name string) Database
}
class Database {
+PROVIDER string
}
class PrismaRepository {
+$queryRaw(query any) Promise~any[]~
+$executeRaw(query any) Promise~number~
+$executeRawUnsafe(query string, params any) Promise~number~
+chat_findFirst(where any) Promise~Chat~
}
class ChannelStartupService {
-configService ConfigService
-prismaRepository PrismaRepository
-instanceId string
+fetchChats(query any, remoteJid string) Promise~any[]~
}
class BaileysStartupService {
-configService ConfigService
-prismaRepository PrismaRepository
-instanceId string
+getMessage(key IMessageKey, full boolean) Promise~IWebMessageInfo~
+updateMessagesReadedByTimestamp(remoteJid string, timestamp number) Promise~number~
+updateChatUnreadMessages(remoteJid string) Promise~number~
+addLabel(labelId string, instanceId string, chatId string) Promise~void~
+removeLabel(labelId string, instanceId string, chatId string) Promise~void~
}
class ChatwootService {
-configService ConfigService
-prismaRepository PrismaRepository
+updateChatwootMessageId(instance InstanceDto, key IMessageKey, chatwootMessageIds any) Promise~void~
+getMessageByKeyId(instance InstanceDto, keyId string) Promise~MessageModel~
}
class IMessageKey {
+id string
}
class IWebMessageInfo
class Chat
class InstanceDto {
+instanceId string
}
class MessageModel
ChannelStartupService --> ConfigService : uses
ChannelStartupService --> PrismaRepository : uses
BaileysStartupService --> ConfigService : uses
BaileysStartupService --> PrismaRepository : uses
ChatwootService --> ConfigService : uses
ChatwootService --> PrismaRepository : uses
BaileysStartupService --> IMessageKey : parameter
ChatwootService --> IMessageKey : parameter
ChatwootService --> InstanceDto : parameter
ChannelStartupService --> Chat : reads
ChatwootService --> MessageModel : returns
BaileysStartupService --> IWebMessageInfo : returns
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey - I've found 4 issues, and left some high level feedback:
- There is a lot of repeated
const provider = this.configService.get<Database>('DATABASE').PROVIDER/ MySQL vs PostgreSQL branching logic across services; consider extracting small helper utilities (e.g.isMySQL()and JSON accessor helpers) to centralize this decision and avoid duplication and drift. - The MySQL
addLabelimplementation usesJSON_ARRAY_APPENDwithout a DISTINCT step, while the PostgreSQL version explicitly deduplicates labels witharray_agg(DISTINCT elem); if label uniqueness is important, you may want to align the MySQL behavior or enforce uniqueness at another layer. - In the MySQL
removeLabelimplementation,JSON_SEARCH(labels, 'one', ?)can return NULL, which will makeJSON_REMOVEfail with an invalid path; consider guarding for a NULL search result (e.g. conditionally applyingJSON_REMOVEonly when a path is found).
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- There is a lot of repeated `const provider = this.configService.get<Database>('DATABASE').PROVIDER` / MySQL vs PostgreSQL branching logic across services; consider extracting small helper utilities (e.g. `isMySQL()` and JSON accessor helpers) to centralize this decision and avoid duplication and drift.
- The MySQL `addLabel` implementation uses `JSON_ARRAY_APPEND` without a DISTINCT step, while the PostgreSQL version explicitly deduplicates labels with `array_agg(DISTINCT elem)`; if label uniqueness is important, you may want to align the MySQL behavior or enforce uniqueness at another layer.
- In the MySQL `removeLabel` implementation, `JSON_SEARCH(labels, 'one', ?)` can return NULL, which will make `JSON_REMOVE` fail with an invalid path; consider guarding for a NULL search result (e.g. conditionally applying `JSON_REMOVE` only when a path is found).
## Individual Comments
### Comment 1
<location> `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:529-538` </location>
<code_context>
+
+ let webMessageInfo: proto.IWebMessageInfo[];
+
+ if (provider === 'mysql') {
+ // MySQL version
+ webMessageInfo = (await this.prismaRepository.$queryRaw`
</code_context>
<issue_to_address>
**issue (bug_risk):** MySQL label insertion does not deduplicate labels like the PostgreSQL version does.
In Postgres, `labels` is built with `array_agg(DISTINCT elem)`, so each label appears at most once per chat. In MySQL, `JSON_ARRAY_APPEND(COALESCE(labels, JSON_ARRAY()), '$', ?)` can add the same `labelId` multiple times, changing the semantics from a set to a multiset across providers. This mismatch can cause subtle bugs if callers assume uniqueness. Please add a deduplication step for MySQL (e.g., via `JSON_TABLE` or a normalization function) so both backends behave consistently.
</issue_to_address>
### Comment 2
<location> `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:4782-4791` </location>
<code_context>
+
+ if (provider === 'mysql') {
+ // MySQL version - use INSERT ... ON DUPLICATE KEY UPDATE
+ await this.prismaRepository.$executeRawUnsafe(
+ `INSERT INTO Chat (id, instanceId, remoteJid, labels, createdAt, updatedAt)
+ VALUES (?, ?, ?, JSON_ARRAY(), NOW(), NOW())
+ ON DUPLICATE KEY UPDATE
+ labels = COALESCE(
+ JSON_REMOVE(
+ labels,
</code_context>
<issue_to_address>
**issue (bug_risk):** MySQL label removal can wipe the entire labels array when the label is not present.
In the MySQL branch, if `JSON_SEARCH` doesn’t find the label, it returns `NULL`. `JSON_REMOVE(labels, NULL)` then evaluates to `NULL`, so `COALESCE(NULL, JSON_ARRAY())` resets `labels` to an empty array. This turns a no-op removal into data loss.
Consider restructuring so `JSON_REMOVE` is only called when `JSON_SEARCH` finds a match (e.g., via a `CASE` or subquery), and otherwise `labels` is left unchanged.
</issue_to_address>
### Comment 3
<location> `src/api/services/channel.service.ts:740-749` </location>
<code_context>
+
+ let webMessageInfo: proto.IWebMessageInfo[];
+
+ if (provider === 'mysql') {
+ // MySQL version
+ webMessageInfo = (await this.prismaRepository.$queryRaw`
</code_context>
<issue_to_address>
**issue (bug_risk):** MySQL conversation listing uses a different "last message" selection semantics than PostgreSQL when applying timestamp filters.
The PostgreSQL query returns the last message *within* the timestamp window per `remoteJid` via `DISTINCT ON` + `ORDER BY ... messageTimestamp DESC`. In contrast, the MySQL query filters by timestamp only in the outer `WHERE`, but selects the last message per chat via a correlated subquery on `MAX(m2.messageTimestamp)` without any timestamp constraint.
This means if the most recent message for a chat is outside the window, but older messages are inside, that chat is excluded in MySQL while included in PostgreSQL (using the last in-range message). To align behavior, the correlated subquery in MySQL should also apply the timestamp filter, or the query should be restructured to emulate `DISTINCT ON` within the time window.
</issue_to_address>
### Comment 4
<location> `src/api/services/channel.service.ts:764-767` </location>
<code_context>
+ Contact.updatedAt
+ ) as updatedAt,
+ Chat.name as chatName,
+ Chat.createdAt as windowStart,
+ DATE_ADD(Chat.createdAt, INTERVAL 24 HOUR) as windowExpires,
+ Chat.unreadMessages as unreadMessages,
+ CASE WHEN DATE_ADD(Chat.createdAt, INTERVAL 24 HOUR) > NOW() THEN 1 ELSE 0 END as windowActive,
+ Message.id AS lastMessageId,
+ Message.key AS lastMessage_key,
</code_context>
<issue_to_address>
**suggestion (bug_risk):** MySQL `windowActive` is returned as 1/0 instead of boolean, which may diverge from expectations.
In Postgres this column is a boolean expression, but in MySQL it’s `1`/`0`:
```sql
CASE WHEN DATE_ADD(Chat.createdAt, INTERVAL 24 HOUR) > NOW() THEN 1 ELSE 0 END as windowActive
```
If the TypeScript layer expects a `boolean`, this mismatch can cause subtle issues (strict equality, serialization, validation). Please update the MySQL query to return `TRUE`/`FALSE` or cast to `BOOLEAN` so both backends expose the same type.
```suggestion
DATE_ADD(Chat.createdAt, INTERVAL 24 HOUR) as windowExpires,
Chat.unreadMessages as unreadMessages,
CASE WHEN DATE_ADD(Chat.createdAt, INTERVAL 24 HOUR) > NOW() THEN TRUE ELSE FALSE END as windowActive,
Message.id AS lastMessageId,
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| if (provider === 'mysql') { | ||
| // MySQL version | ||
| webMessageInfo = (await this.prismaRepository.$queryRaw` | ||
| SELECT * FROM Message | ||
| WHERE instanceId = ${this.instanceId} | ||
| AND JSON_UNQUOTE(JSON_EXTRACT(\`key\`, '$.id')) = ${key.id} | ||
| LIMIT 1 | ||
| `) as proto.IWebMessageInfo[]; | ||
| } else { | ||
| // PostgreSQL version |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): MySQL label insertion does not deduplicate labels like the PostgreSQL version does.
In Postgres, labels is built with array_agg(DISTINCT elem), so each label appears at most once per chat. In MySQL, JSON_ARRAY_APPEND(COALESCE(labels, JSON_ARRAY()), '$', ?) can add the same labelId multiple times, changing the semantics from a set to a multiset across providers. This mismatch can cause subtle bugs if callers assume uniqueness. Please add a deduplication step for MySQL (e.g., via JSON_TABLE or a normalization function) so both backends behave consistently.
| WHERE "instanceId" = ${this.instanceId} | ||
| AND "key"->>'remoteJid' = ${remoteJid} | ||
| AND ("key"->>'fromMe')::boolean = false | ||
| AND "messageTimestamp" <= ${timestamp} | ||
| AND ("status" IS NULL OR "status" = ${status[3]}) | ||
| `; | ||
| } | ||
|
|
||
| if (result) { | ||
| if (result > 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): MySQL label removal can wipe the entire labels array when the label is not present.
In the MySQL branch, if JSON_SEARCH doesn’t find the label, it returns NULL. JSON_REMOVE(labels, NULL) then evaluates to NULL, so COALESCE(NULL, JSON_ARRAY()) resets labels to an empty array. This turns a no-op removal into data loss.
Consider restructuring so JSON_REMOVE is only called when JSON_SEARCH finds a match (e.g., via a CASE or subquery), and otherwise labels is left unchanged.
| if (provider === 'mysql') { | ||
| // MySQL version | ||
| const timestampFilterMysql = | ||
| query?.where?.messageTimestamp?.gte && query?.where?.messageTimestamp?.lte | ||
| ? Prisma.sql` | ||
| AND Message.messageTimestamp >= ${Math.floor(new Date(query.where.messageTimestamp.gte).getTime() / 1000)} | ||
| AND Message.messageTimestamp <= ${Math.floor(new Date(query.where.messageTimestamp.lte).getTime() / 1000)}` | ||
| : Prisma.sql``; | ||
|
|
||
| results = await this.prismaRepository.$queryRaw` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): MySQL conversation listing uses a different "last message" selection semantics than PostgreSQL when applying timestamp filters.
The PostgreSQL query returns the last message within the timestamp window per remoteJid via DISTINCT ON + ORDER BY ... messageTimestamp DESC. In contrast, the MySQL query filters by timestamp only in the outer WHERE, but selects the last message per chat via a correlated subquery on MAX(m2.messageTimestamp) without any timestamp constraint.
This means if the most recent message for a chat is outside the window, but older messages are inside, that chat is excluded in MySQL while included in PostgreSQL (using the last in-range message). To align behavior, the correlated subquery in MySQL should also apply the timestamp filter, or the query should be restructured to emulate DISTINCT ON within the time window.
| DATE_ADD(Chat.createdAt, INTERVAL 24 HOUR) as windowExpires, | ||
| Chat.unreadMessages as unreadMessages, | ||
| CASE WHEN DATE_ADD(Chat.createdAt, INTERVAL 24 HOUR) > NOW() THEN 1 ELSE 0 END as windowActive, | ||
| Message.id AS lastMessageId, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (bug_risk): MySQL windowActive is returned as 1/0 instead of boolean, which may diverge from expectations.
In Postgres this column is a boolean expression, but in MySQL it’s 1/0:
CASE WHEN DATE_ADD(Chat.createdAt, INTERVAL 24 HOUR) > NOW() THEN 1 ELSE 0 END as windowActiveIf the TypeScript layer expects a boolean, this mismatch can cause subtle issues (strict equality, serialization, validation). Please update the MySQL query to return TRUE/FALSE or cast to BOOLEAN so both backends expose the same type.
| DATE_ADD(Chat.createdAt, INTERVAL 24 HOUR) as windowExpires, | |
| Chat.unreadMessages as unreadMessages, | |
| CASE WHEN DATE_ADD(Chat.createdAt, INTERVAL 24 HOUR) > NOW() THEN 1 ELSE 0 END as windowActive, | |
| Message.id AS lastMessageId, | |
| DATE_ADD(Chat.createdAt, INTERVAL 24 HOUR) as windowExpires, | |
| Chat.unreadMessages as unreadMessages, | |
| CASE WHEN DATE_ADD(Chat.createdAt, INTERVAL 24 HOUR) > NOW() THEN TRUE ELSE FALSE END as windowActive, | |
| Message.id AS lastMessageId, |
Creates new migration to ensure lid column exists even in databases where it was previously dropped by the Kafka integration migration. Uses prepared statement to check column existence before adding, ensuring compatibility with both fresh and existing installations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Esta PR corrige problemas de compatibilidade com MySQL 8.0 no Evolution API.
Arquivos alterados:
prisma/mysql-migrations/20250918183910_add_kafka_integration/migration.sql
prisma/mysql-schema.prisma
lid String? @db.VarChar(100)no modelo IsOnWhatsapp para sincronizar o schema com a migration existente que já cria essa coluna no banco.src/api/services/channel.service.ts
fetchChats(): Adicionada versão MySQL da query RAW, mantendo a versão PostgreSQL original.DISTINCT ON,->>'remoteJid',to_timestamp(),INTERVAL '24 hours'por equivalentes MySQL.src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
getMessage(): Versões PostgreSQL/MySQL para busca porkey->>'id'.messages.update: Versões PostgreSQL/MySQL.updateMessagesReadedByTimestamp(): Versões PostgreSQL/MySQL para operador JSON e cast::boolean.updateChatUnreadMessages(): Versões PostgreSQL/MySQL paraCOUNT(*)::inte operadores JSON.addLabel(): Versões PostgreSQL/MySQL parato_jsonb,jsonb_array_elements_text.removeLabel(): Versões PostgreSQL/MySQL parajsonb_agg,'[]'::jsonb.src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts
updateChatwootMessageId(): Versões PostgreSQL/MySQL para operadorkey->>'id'.getMessageByKeyId(): Versões PostgreSQL/MySQL para operadorkey->>'id'.Summary by Sourcery
Ensure database-agnostic behavior for WhatsApp/channel and Chatwoot integrations while preserving the IsOnWhatsapp lid column in MySQL migrations and schema.
Bug Fixes:
Enhancements: