데이터를 AI SDK 5.0으로 마이그레이션

데이터를 AI SDK 5.0으로 마이그레이션 (Migrate Your Data to AI SDK 5.0)

AI SDK 5.0은 메시지 구조와 영속화 패턴에 변경을 가져왔어요. 코드 마이그레이션은 codemod로 자동화되는 경우가 많지만, 데이터 마이그레이션은 여러분의 구체적인 저장 방식, 데이터베이스 스키마, 애플리케이션 요구사항에 따라 달라져요. 이 가이드는 먼저 런타임 변환 계층으로 앱을 5.0에서 동작하게 만든 뒤, 여유가 될 때 스키마를 마이그레이션하는 2단계 접근을 소개해요.

출처: 문서

본문

AI SDK 5.0은 메시지 구조와 영속화 패턴에 변경을 도입합니다. codemod로 자동화할 수 있는 코드 마이그레이션과 달리, 데이터 마이그레이션은 여러분의 구체적인 저장 방식, 데이터베이스 스키마, 애플리케이션 요구사항에 따라 달라집니다.

이 가이드는 먼저 런타임 변환 계층을 사용해 애플리케이션이 AI SDK 5.0에서 동작하게 해줍니다. 이를 통해 데이터베이스 마이그레이션이 여러분을 막지 않고 앱을 즉시 업데이트할 수 있습니다. 그런 다음 데이터 스키마를 여유 있게 마이그레이션할 수 있습니다.

안전한 마이그레이션을 위해 다음의 2단계 접근을 따르세요:

1단계: 앱 동작시키기 (런타임 변환) (Phase 1: Get Your App Working (Runtime Conversion))

목표: 데이터베이스를 건드리지 않고 애플리케이션을 AI SDK 5.0으로 업데이트합니다.

  1. 의존성 업데이트 (v5 옆에 v4 타입 설치)
  2. v4와 v5 메시지 형식 사이를 변환하는 변환 함수 추가
  3. 데이터베이스에서 읽을 때 메시지를 변환하도록 데이터 페칭 로직 업데이트
  4. 나머지 애플리케이션 코드를 AI SDK 5.0으로 업데이트 (주요 마이그레이션 가이드 참고)

데이터베이스 스키마는 1단계 동안 변경되지 않습니다. 런타임에 메시지를 변환하는 변환 계층만 추가하는 것입니다.

기간: 몇 시간에서 며칠 안에 완료할 수 있습니다.

목표: 런타임 변환 오버헤드를 제거하면서 데이터를 v5 호환 스키마로 마이그레이션합니다.

1단계가 즉시 동작하게 해주지만, 1단계를 완료한 직후 스키마를 마이그레이션하세요. 이 단계는 동등한 v5 스키마로 나란히(side-by-side) 마이그레이션하는 접근을 사용합니다:

  1. 기존 messages 테이블 옆에 messages_v5 테이블 생성
  2. 두 테이블 모두에 이중 쓰기 시작 (변환 포함)
  3. 기존 메시지를 변환하는 백그라운드 마이그레이션 실행
  4. 읽기를 v5 스키마로 전환
  5. 라우트 핸들러에서 변환 제거
  6. 이중 쓰기 제거 (v5에만 쓰기)
  7. 이전 테이블 삭제

기간: 1단계 직후에 하세요.

이것이 중요한 이유:

  • 런타임 변환 오버헤드 제거
  • 기술 부채를 일찍 제거
  • v5 메시지 형식으로 타입 안전성 확보
  • 유지보수와 확장이 더 쉬움

변경 사항 이해하기 (Understanding the Changes)

시작하기 전에 AI SDK 5.0의 주요 영속화 관련 변경을 이해하세요:

AI SDK 4.0:

  • 텍스트용 content 필드
  • 최상위 속성으로서 reasoning
  • 최상위 속성으로서 toolInvocations
  • parts (선택) 순서화된 배열

AI SDK 5.0:

  • parts 배열이 단일 진실 원천 (single source of truth)
  • content가 제거(지원 중단)되고 text 파츠를 통해 접근
  • reasoning이 제거되고 reasoning 파츠로 대체
  • toolInvocations가 제거되고 input/output(args/result에서 이름 변경)을 가진 tool-${toolName} 파츠로 대체
  • data 역할 제거 (대신 data 파츠 사용)

1단계: 런타임 변환 패턴 (Phase 1: Runtime Conversion Pattern)

이것은 데이터베이스 스키마를 변경하지 않고 변환 계층을 만듭니다.

1단계: 의존성 업데이트 (Step 1: Update Dependencies)

v4 메시지에 대한 올바른 TypeScript 타입을 얻으려면, npm 별칭을 사용해 v5 옆에 v4 패키지를 설치하세요:

{
  "dependencies": {
    "ai": "^5.0.0",
    "ai-legacy": "npm:ai@^4.3.2"
  }
}

실행:

pnpm install

올바른 타입 안전성을 위해 v4 타입을 import하세요:

import type { Message as V4Message } from 'ai-legacy';
import type { UIMessage } from 'ai';

2단계: 변환 함수 추가 (Step 2: Add Conversion Functions)

작업 중인 메시지 형식을 감지하는 타입 가드와, 모든 v4 메시지 타입을 처리하는 변환 함수를 만드세요:

import type {
  ToolInvocation,
  Message as V4Message,
  UIMessage as LegacyUIMessage,
} from 'ai-legacy';
import type { ToolUIPart, UIMessage, UITools } from 'ai';

export type MyUIMessage = UIMessage<unknown, { custom: any }, UITools>;

type V4Part = NonNullable<V4Message['parts']>[number];
type V5Part = MyUIMessage['parts'][number];

// Type definitions for V4 parts
type V4ToolInvocationPart = Extract<V4Part, { type: 'tool-invocation' }>;

type V4ReasoningPart = Extract<V4Part, { type: 'reasoning' }>;

type V4SourcePart = Extract<V4Part, { type: 'source' }>;

type V4FilePart = Extract<V4Part, { type: 'file' }>;

// Type guards
function isV4Message(msg: V4Message | MyUIMessage): msg is V4Message {
  return (
    'toolInvocations' in msg ||
    (msg?.parts?.some(p => p.type === 'tool-invocation') ?? false) ||
    msg?.role === 'data' ||
    ('reasoning' in msg && typeof msg.reasoning === 'string') ||
    (msg?.parts?.some(p => 'args' in p || 'result' in p) ?? false) ||
    (msg?.parts?.some(p => 'reasoning' in p && 'details' in p) ?? false) ||
    (msg?.parts?.some(
      p => p.type === 'file' && 'mimeType' in p && 'data' in p,
    ) ??
      false)
  );
}

function isV4ToolInvocationPart(part: unknown): part is V4ToolInvocationPart {
  return (
    typeof part === 'object' &&
    part !== null &&
    'type' in part &&
    part.type === 'tool-invocation' &&
    'toolInvocation' in part
  );
}

function isV4ReasoningPart(part: unknown): part is V4ReasoningPart {
  return (
    typeof part === 'object' &&
    part !== null &&
    'type' in part &&
    part.type === 'reasoning' &&
    'reasoning' in part
  );
}

function isV4SourcePart(part: unknown): part is V4SourcePart {
  return (
    typeof part === 'object' &&
    part !== null &&
    'type' in part &&
    part.type === 'source' &&
    'source' in part
  );
}

function isV4FilePart(part: unknown): part is V4FilePart {
  return (
    typeof part === 'object' &&
    part !== null &&
    'type' in part &&
    part.type === 'file' &&
    'mimeType' in part &&
    'data' in part
  );
}

// State mapping
const V4_TO_V5_STATE_MAP = {
  'partial-call': 'input-streaming',
  call: 'input-available',
  result: 'output-available',
} as const;

function convertToolInvocationState(
  v4State: ToolInvocation['state'],
): 'input-streaming' | 'input-available' | 'output-available' {
  return V4_TO_V5_STATE_MAP[v4State] ?? 'output-available';
}

// Tool conversion
function convertV4ToolInvocationToV5ToolUIPart(
  toolInvocation: ToolInvocation,
): ToolUIPart {
  return {
    type: `tool-${toolInvocation.toolName}`,
    toolCallId: toolInvocation.toolCallId,
    input: toolInvocation.args,
    output:
      toolInvocation.state === 'result' ? toolInvocation.result : undefined,
    state: convertToolInvocationState(toolInvocation.state),
  };
}

// Part converters
function convertV4ToolInvocationPart(part: V4ToolInvocationPart): V5Part {
  return convertV4ToolInvocationToV5ToolUIPart(part.toolInvocation);
}

function convertV4ReasoningPart(part: V4ReasoningPart): V5Part {
  return { type: 'reasoning', text: part.reasoning };
}

function convertV4SourcePart(part: V4SourcePart): V5Part {
  return {
    type: 'source-url',
    url: part.source.url,
    sourceId: part.source.id,
    title: part.source.title,
  };
}

function convertV4FilePart(part: V4FilePart): V5Part {
  return {
    type: 'file',
    mediaType: part.mimeType,
    url: part.data,
  };
}

function convertPart(part: V4Part | V5Part): V5Part {
  if (isV4ToolInvocationPart(part)) {
    return convertV4ToolInvocationPart(part);
  }
  if (isV4ReasoningPart(part)) {
    return convertV4ReasoningPart(part);
  }
  if (isV4SourcePart(part)) {
    return convertV4SourcePart(part);
  }
  if (isV4FilePart(part)) {
    return convertV4FilePart(part);
  }
  // Already V5 format
  return part;
}

// Message conversion
function createBaseMessage(
  msg: V4Message | MyUIMessage,
  index: number,
): Pick<MyUIMessage, 'id' | 'role'> {
  return {
    id: msg.id || `msg-${index}`,
    role: msg.role === 'data' ? 'assistant' : msg.role,
  };
}

function convertDataMessage(msg: V4Message, index: number): MyUIMessage {
  return {
    ...createBaseMessage(msg, index),
    parts: [
      {
        type: 'data-custom',
        data: msg.data || msg.content,
      },
    ],
  };
}

function buildPartsFromTopLevelFields(msg: V4Message): MyUIMessage['parts'] {
  const parts: MyUIMessage['parts'] = [];

  if (msg.reasoning) {
    parts.push({ type: 'reasoning', text: msg.reasoning });
  }

  if (msg.toolInvocations) {
    parts.push(
      ...msg.toolInvocations.map(convertV4ToolInvocationToV5ToolUIPart),
    );
  }

  if (msg.content && typeof msg.content === 'string') {
    parts.push({ type: 'text', text: msg.content });
  }

  return parts;
}

function convertPartsArray(parts: V4Part[]): MyUIMessage['parts'] {
  return parts.map(convertPart);
}

export function convertV4MessageToV5(
  msg: V4Message | MyUIMessage,
  index: number,
): MyUIMessage {
  if (!isV4Message(msg)) {
    return msg as MyUIMessage;
  }

  if (msg.role === 'data') {
    return convertDataMessage(msg, index);
  }

  const base = createBaseMessage(msg, index);
  const parts = msg.parts
    ? convertPartsArray(msg.parts)
    : buildPartsFromTopLevelFields(msg);

  return { ...base, parts };
}

// V5 to V4 conversion
function convertV5ToolUIPartToV4ToolInvocation(
  part: ToolUIPart,
): ToolInvocation {
  const state =
    part.state === 'input-streaming'
      ? 'partial-call'
      : part.state === 'input-available'
        ? 'call'
        : 'result';

  const toolName = part.type.startsWith('tool-')
    ? part.type.slice(5)
    : part.type;

  const base = {
    toolCallId: part.toolCallId,
    toolName,
    args: part.input,
    state,
  };

  if (state === 'result' && part.output !== undefined) {
    return { ...base, state: 'result' as const, result: part.output };
  }

  return base as ToolInvocation;
}

export function convertV5MessageToV4(msg: MyUIMessage): LegacyUIMessage {
  const parts: V4Part[] = [];

  const base: LegacyUIMessage = {
    id: msg.id,
    role: msg.role,
    content: '',
    parts,
  };

  let textContent = '';
  let reasoning: string | undefined;
  const toolInvocations: ToolInvocation[] = [];

  for (const part of msg.parts) {
    if (part.type === 'text') {
      textContent = part.text;
      parts.push({ type: 'text', text: part.text });
    } else if (part.type === 'reasoning') {
      reasoning = part.text;
      parts.push({
        type: 'reasoning',
        reasoning: part.text,
        details: [{ type: 'text', text: part.text }],
      });
    } else if (part.type.startsWith('tool-')) {
      const toolInvocation = convertV5ToolUIPartToV4ToolInvocation(
        part as ToolUIPart,
      );
      parts.push({ type: 'tool-invocation', toolInvocation: toolInvocation });
      toolInvocations.push(toolInvocation);
    } else if (part.type === 'source-url') {
      parts.push({
        type: 'source',
        source: {
          id: part.sourceId,
          url: part.url,
          title: part.title,
          sourceType: 'url',
        },
      });
    } else if (part.type === 'file') {
      parts.push({
        type: 'file',
        mimeType: part.mediaType,
        data: part.url,
      });
    } else if (part.type === 'data-custom') {
      base.data = part.data;
    }
  }

  if (textContent) {
    base.content = textContent;
  }

  if (reasoning) {
    base.reasoning = reasoning;
  }

  if (toolInvocations.length > 0) {
    base.toolInvocations = toolInvocations;
  }

  if (parts.length > 0) {
    base.parts = parts;
  }
  return base;
}

3단계: 읽을 때 메시지 변환 (Step 3: Convert Messages When Reading)

데이터베이스에서 메시지를 로드할 때 변환을 적용하세요:

경고: 이 코드를 여러분의 구체적인 데이터베이스와 ORM에 맞게 조정하세요.

import { convertV4MessageToV5, type MyUIMessage } from './conversion';

export async function loadChat(chatId: string): Promise<MyUIMessage[]> {
  // Fetch messages from your database (pseudocode - update based on your data access layer)
  const rawMessages = await db
    .select()
    .from(messages)
    .where(eq(messages.chatId, chatId))
    .orderBy(messages.createdAt);

  // Convert on read
  return rawMessages.map((msg, index) => convertV4MessageToV5(msg, index));
}

4단계: 저장할 때 메시지 변환 (Step 4: Convert Messages When Saving)

1단계에서는 애플리케이션이 v5에서 실행되지만 데이터베이스는 v4 형식을 저장합니다. 라우트 핸들러에서 데이터베이스 함수에 전달하기 전에 메시지를 인라인으로 변환하세요:

import {
  convertV5MessageToV4,
  convertV4MessageToV5,
  type MyUIMessage,
} from './conversion';
import { upsertMessage, loadChat } from './db/actions';
import { streamText, generateId, convertToModelMessages } from 'ai';
__PROVIDER_IMPORT__;

export async function POST(req: Request) {
  const { message, chatId }: { message: MyUIMessage; chatId: string } =
    await req.json();

  // Convert and save incoming user message (v5 to v4 inline)
  await upsertMessage({
    chatId,
    id: message.id,
    message: convertV5MessageToV4(message), // convert to v4
  });

  // Load previous messages (already in v5 format)
  const previousMessages = await loadChat(chatId);
  const messages = [...previousMessages, message];

  const result = streamText({
    model: __MODEL__,
    messages: convertToModelMessages(messages),
    tools: {
      // Your tools here
    },
  });

  return result.toUIMessageStreamResponse({
    generateMessageId: generateId,
    originalMessages: messages,
    onFinish: async ({ responseMessage }) => {
      // Convert and save assistant response (v5 to v4 inline)
      await upsertMessage({
        chatId,
        id: responseMessage.id,
        message: convertV5MessageToV4(responseMessage),
      });
    },
  });
}

upsertMessage(또는 동등한) 함수는 v4 메시지와 계속 작업하도록 변경하지 않고 두세요.

3단계와 4단계를 완료하면 양방향 변환 계층이 생깁니다:

  • 읽기 (Reading): v4 (데이터베이스) → v5 (애플리케이션)
  • 쓰기 (Writing): v5 (애플리케이션) → v4 (데이터베이스)

데이터베이스 스키마는 변경되지 않지만, 애플리케이션은 이제 v5 형식으로 작업합니다.

다음 단계: 주요 마이그레이션 가이드를 따라 API 라우트, 컴포넌트, AI SDK를 사용하는 다른 코드를 포함해 나머지 애플리케이션 코드를 AI SDK 5.0으로 업데이트하세요. 그런 다음 2단계로 진행하세요.

자세한 내용은 주요 마이그레이션 가이드를 참고하세요.

2단계: 나란히 스키마 마이그레이션 (Phase 2: Side-by-Side Schema Migration)

이제 애플리케이션이 AI SDK 5.0으로 업데이트되고 1단계의 런타임 변환 계층으로 동작하므로, 완전히 작동하는 시스템이 갖춰졌습니다. 하지만 변환 함수는 임시 해결책일 뿐입니다. 데이터베이스가 여전히 v4 형식으로 메시지를 저장하므로 다음을 의미합니다:

  • 모든 읽기 작업에 런타임 변환 오버헤드 발생
  • 하위 호환성 코드를 무기한 유지
  • 향후 기능이 레거시 스키마와 작업해야 함

2단계는 메시지 기록을 v5 스키마로 마이그레이션하여 변환 계층을 제거하고 더 나은 성능과 장기적인 유지보수성을 가능하게 합니다.

이 단계는 단순화된 접근을 사용합니다: 현재 messages 테이블과 동일한 구조이지만 v5 형식 메시지 파츠를 저장하는 새 messages_v5 테이블을 만듭니다.

2단계 예제를 여러분의 설정에 맞게 조정하세요

이 코드 예제는 마이그레이션 패턴을 보여줍니다. 여러분의 구현은 데이터베이스(Postgres, MySQL, SQLite), ORM(Drizzle, Prisma, raw SQL), 스키마 설계, 데이터 영속화 패턴에 따라 달라집니다.

이 예제들을 가이드로 사용하고, 여러분의 구체적인 설정에 맞게 조정하세요.

개요: 마이그레이션 전략 (Overview: Migration Strategy)

  1. 기존 messages 테이블 옆에 messages_v5 테이블 생성
  2. 새 메시지를 두 스키마 모두에 이중 쓰기 (변환 포함)
  3. 기존 메시지를 변환하는 백그라운드 마이그레이션
  4. 데이터 무결성 검증
  5. 읽기 함수 업데이트해서 messages_v5 스키마 사용
  6. 라우트 핸들러에서 변환 제거
  7. 이중 쓰기 제거 (messages_v5에만 쓰기)
  8. 이전 테이블 정리

이렇게 하면 마이그레이션 내내 데이터 손실 위험 없이 애플리케이션이 계속 실행됩니다.

1단계: V4 옆에 V5 스키마 생성 (Step 1: Create V5 Schema Alongside V4)

기존 테이블과 동일한 구조이지만 v5 메시지 파츠를 저장하도록 설계된 새 messages_v5 테이블을 만드세요:

기존 v4 스키마 (계속 실행):

import { UIMessage } from 'ai-legacy';

export const messages = pgTable('messages', {
  id: varchar()
    .primaryKey()
    .$defaultFn(() => nanoid()),
  chatId: varchar()
    .references(() => chats.id, { onDelete: 'cascade' })
    .notNull(),
  createdAt: timestamp().defaultNow().notNull(),
  parts: jsonb().$type<UIMessage['parts']>().notNull(),
  role: text().$type<UIMessage['role']>().notNull(),
});

새 v5 스키마 (옆에 생성):

import { MyUIMessage } from './conversion';

export const messages_v5 = pgTable('messages_v5', {
  id: varchar()
    .primaryKey()
    .$defaultFn(() => nanoid()),
  chatId: varchar()
    .references(() => chats.id, { onDelete: 'cascade' })
    .notNull(),
  createdAt: timestamp().defaultNow().notNull(),
  parts: jsonb().$type<MyUIMessage['parts']>().notNull(),
  role: text().$type<MyUIMessage['role']>().notNull(),
});

새 테이블을 만들려면 마이그레이션을 실행하세요:

pnpm drizzle-kit generate
pnpm drizzle-kit migrate

2단계: 새 메시지 이중 쓰기 구현 (Step 2: Implement Dual-Write for New Messages)

마이그레이션 기간 동안 두 스키마 모두에 쓰도록 저장 함수를 업데이트합니다. 이렇게 하면 새 메시지가 두 형식으로 모두 제공됩니다:

import { convertV4MessageToV5 } from './conversion';
import { messages, messages_v5 } from './schema';
import type { UIMessage } from 'ai-legacy';

export const upsertMessage = async ({
  chatId,
  message,
  id,
}: {
  id: string;
  chatId: string;
  message: UIMessage; // Still accepts v4 format
}) => {
  return await db.transaction(async tx => {
    // Write to v4 schema (existing)
    const [result] = await tx
      .insert(messages)
      .values({
        chatId,
        parts: message.parts ?? [],
        role: message.role,
        id,
      })
      .onConflictDoUpdate({
        target: messages.id,
        set: {
          parts: message.parts ?? [],
          chatId,
        },
      })
      .returning();

    // Convert and write to v5 schema (new)
    const v5Message = convertV4MessageToV5(
      {
        ...message,
        content: '',
      },
      0,
    );

    await tx
      .insert(messages_v5)
      .values({
        chatId,
        parts: v5Message.parts ?? [],
        role: v5Message.role,
        id,
      })
      .onConflictDoUpdate({
        target: messages_v5.id,
        set: {
          parts: v5Message.parts ?? [],
          chatId,
        },
      });

    return result;
  });
};

3단계: 기존 메시지 마이그레이션 (Step 3: Migrate Existing Messages)

기존 메시지를 v4에서 v5 스키마로 마이그레이션하는 스크립트를 만드세요:

import { convertV4MessageToV5 } from './conversion';
import { db } from './db';
import { messages, messages_v5 } from './db/schema';

async function migrateExistingMessages() {
  console.log('Starting migration of existing messages...');

  // Get all v4 messages that haven't been migrated yet
  const migratedIds = await db.select({ id: messages_v5.id }).from(messages_v5);

  const migratedIdSet = new Set(migratedIds.map(m => m.id));

  const allMessages = await db.select().from(messages);
  const unmigrated = allMessages.filter(msg => !migratedIdSet.has(msg.id));

  console.log(`Found ${unmigrated.length} messages to migrate`);

  let migrated = 0;
  let errors = 0;
  const batchSize = 100;

  for (let i = 0; i < unmigrated.length; i += batchSize) {
    const batch = unmigrated.slice(i, i + batchSize);

    await db.transaction(async tx => {
      for (const msg of batch) {
        try {
          // Convert message to v5 format
          const v5Message = convertV4MessageToV5(
            {
              id: msg.id,
              content: '',
              role: msg.role,
              parts: msg.parts,
              createdAt: msg.createdAt,
            },
            0,
          );

          // Insert into v5 messages table
          await tx.insert(messages_v5).values({
            id: v5Message.id,
            chatId: msg.chatId,
            role: v5Message.role,
            parts: v5Message.parts,
            createdAt: msg.createdAt,
          });

          migrated++;
        } catch (error) {
          console.error(`Error migrating message ${msg.id}:`, error);
          errors++;
        }
      }
    });

    console.log(`Progress: ${migrated}/${unmigrated.length} messages migrated`);
  }

  console.log(`Migration complete: ${migrated} migrated, ${errors} errors`);
}

// Run migration
migrateExistingMessages().catch(console.error);

이 스크립트는:

  • 아직 마이그레이션되지 않은 메시지만 마이그레이션
  • 더 나은 성능을 위해 배칭 사용
  • 여러 번 안전하게 실행 가능
  • 중지 후 재개 가능

4단계: 마이그레이션 검증 (Step 4: Verify Migration)

데이터 무결성을 보장하는 검증 스크립트를 만드세요:

import { count } from 'drizzle-orm';
import { db } from './db';
import { messages, messages_v5 } from './db/schema';

async function verifyMigration() {
  // Count messages in both schemas
  const v4Count = await db.select({ count: count() }).from(messages);
  const v5Count = await db.select({ count: count() }).from(messages_v5);

  console.log('Migration Status:');
  console.log(`V4 Messages: ${v4Count[0].count}`);
  console.log(`V5 Messages: ${v5Count[0].count}`);
  console.log(
    `Migration progress: ${((v5Count[0].count / v4Count[0].count) * 100).toFixed(2)}%`,
  );
}

verifyMigration().catch(console.error);

5단계: V5 스키마에서 읽기 (Step 5: Read from V5 Schema)

마이그레이션이 완료되면 새 v5 스키마를 사용하도록 읽기 함수를 업데이트하세요. 데이터가 이제 v5 형식이므로 변환이 필요 없습니다:

import type { MyUIMessage } from './conversion';

export const loadChat = async (chatId: string): Promise<MyUIMessage[]> => {
  // Load from v5 schema - no conversion needed
  const messages = await db
    .select()
    .from(messages_v5)
    .where(eq(messages_v5.chatId, chatId))
    .orderBy(messages_v5.createdAt);

  return messages;
};

6단계: V5 스키마에만 쓰기 (Step 6: Write to V5 Schema Only)

읽기 함수가 v5에서 작동하고 백그라운드 마이그레이션이 완료되면 이중 쓰기를 중지하고 v5에만 쓰세요:

import type { MyUIMessage } from './conversion';

export const upsertMessage = async ({
  chatId,
  message,
  id,
}: {
  id: string;
  chatId: string;
  message: MyUIMessage; // Now accepts v5 format
}) => {
  // Write to v5 schema only
  const [result] = await db
    .insert(messages_v5)
    .values({
      chatId,
      parts: message.parts ?? [],
      role: message.role,
      id,
    })
    .onConflictDoUpdate({
      target: messages_v5.id,
      set: {
        parts: message.parts ?? [],
        chatId,
      },
    })
    .returning();

  return result;
};

v5 메시지를 직접 전달하도록 라우트 핸들러를 업데이트하세요:

export async function POST(req: Request) {
  const { message, chatId }: { message: MyUIMessage; chatId: string } =
    await req.json();

  // Pass v5 message directly - no conversion needed
  await upsertMessage({
    chatId,
    id: message.id,
    message,
  });

  const previousMessages = await loadChat(chatId);
  const messages = [...previousMessages, message];

  const result = streamText({
    model: __MODEL__,
    messages: convertToModelMessages(messages),
    tools: {
      // Your tools here
    },
  });

  return result.toUIMessageStreamResponse({
    generateMessageId: generateId,
    originalMessages: messages,
    onFinish: async ({ responseMessage }) => {
      await upsertMessage({
        chatId,
        id: responseMessage.id,
        message: responseMessage, // No conversion needed
      });
    },
  });
}

7단계: 전환 완료 (Step 7: Complete the Switch)

검증이 통과하고 마이그레이션에 자신이 생기면:

  1. 변환 함수 제거: v4↔v5 변환 유틸리티 삭제
  2. ai-legacy 의존성 제거: v4 타입 패키지 제거
  3. 철저히 테스트: v5 스키마로 애플리케이션이 올바르게 동작하는지 확인
  4. 모니터링: 프로덕션에서 문제 감시
  5. 정리: 안전한 기간(1-2주) 후 이전 테이블 삭제
-- After confirming everything works
DROP TABLE messages;

-- Optionally rename v5 table to standard name
ALTER TABLE messages_v5 RENAME TO messages;

2단계가 이제 완료되었습니다. 런타임 변환 오버헤드 없이 애플리케이션이 v5 스키마로 완전히 마이그레이션되었습니다.

커뮤니티 자료 (Community Resources)

다음 커뮤니티 구성원들이 마이그레이션 경험을 공유했습니다:

API 변경에 대한 자세한 내용은 주요 마이그레이션 가이드를 참고하세요.

더 알아보기 (Learn more)

전체 사이트맵