fix(db): store assistant messages as mediumtext on mysql

MySQL caps `text` at 65,535 bytes. A single base64 image attachment is
allowed up to ~1.34MB by the chat route, so appending such a message fails
with ER_DATA_TOO_LONG under the default strict mode. SQLite and PostgreSQL
are unaffected because their text types are effectively unbounded.

`mediumtext` holds 16MB, comfortably above the 2MB request cap.

The migration keeps the parenthesised default expression that every other
text default in this folder uses, because MySQL rejects a literal default
on TEXT columns.
This commit is contained in:
Thomas Camlong committed 2026-08-07 17:29:55 +02:00
1 parent da93adac14
commit 4acd6548b5
3 files changed
+23 -3

No files matched your search

@@ -26,7 +26,7 @@ CREATE TABLE `assistant_message` (
`thread_id` varchar(64) NOT NULL,
`parent_id` varchar(128),
`format` varchar(64) DEFAULT 'ai-sdk/v6' NOT NULL,
`content` text DEFAULT ('{"json": {}}') NOT NULL,
`content` mediumtext DEFAULT ('{"json": {}}') NOT NULL,
`created_at` timestamp DEFAULT (now()) NOT NULL,
CONSTRAINT `assistant_message_id` PRIMARY KEY(`id`)
);
+4 -1
View File
@@ -8,6 +8,7 @@ import {
customType,
index,
int,
mediumtext,
mysqlTable,
primaryKey,
smallint,
@@ -536,7 +537,9 @@ export const assistantMessages = mysqlTable(
.references(() => assistantThreads.id, { onDelete: "cascade" }),
parentId: varchar({ length: 128 }),
format: varchar({ length: 64 }).notNull().default("ai-sdk/v6"),
content: text().default(emptySuperJSON).notNull(),
// `text` only holds 64KB on MySQL, which a single base64 image attachment already exceeds.
// `mediumtext` holds 16MB, comfortably above the 2MB request cap enforced by the chat route.
content: mediumtext().default(emptySuperJSON).notNull(),
createdAt: timestamp().notNull().defaultNow(),
},
(message) => ({
+18 -1
View File
@@ -1,9 +1,10 @@
import path from "path";
import { MySqlContainer } from "@testcontainers/mysql";
import { eq } from "drizzle-orm";
import { drizzle } from "drizzle-orm/mysql2";
import { migrate } from "drizzle-orm/mysql2/migrator";
import mysql from "mysql2";
import { describe, test } from "vitest";
import { describe, expect, test } from "vitest";
import { DB_CASING } from "@homarr/core/infrastructure/db/constants";
@@ -35,6 +36,22 @@ describe("Mysql Migration", () => {
// Check if users table exists
await database.query.users.findMany();
// A single base64 image attachment already exceeds the 64KB that `text` holds on MySQL, so
// `assistant_message.content` has to be `mediumtext`. Everything below 64KB passes either way.
const largeContent = "x".repeat(200_000);
await database.insert(mysqlSchema.users).values({ id: "size-check-user", name: "size-check" });
await database.insert(mysqlSchema.assistantThreads).values({ id: "size-check-thread", userId: "size-check-user" });
await database.insert(mysqlSchema.assistantMessages).values({
id: "size-check-message",
threadId: "size-check-thread",
parentId: null,
content: largeContent,
});
const [storedMessage] = await database.query.assistantMessages.findMany({
where: eq(mysqlSchema.assistantMessages.id, "size-check-message"),
});
expect(storedMessage?.content).toHaveLength(largeContent.length);
connection.end();
await mysqlContainer.stop();
}, 120_000);