Перейти к содержимому

D1 Database

D1 Database — это serverless SQL база данных, доступная для ваших приложений. Храните структурированные данные, используйте JOIN, транзакции и индексы.

D1 предоставляет:

  • SQLite совместимость — стандартный SQL, известный синтаксис
  • Edge-ready — низкая задержка запросов с edge-нод
  • Автоматическое масштабирование — не нужно думать о серверах
  • Изоляция по проектам — каждый проект имеет свою базу

D1 доступна через REST API с использованием API токена.

https://api.onreza.ru/api/d1/databases/{databaseId}

databaseId — UUID вашей базы, доступен в настройках проекта.

Authorization: Bearer {ONREZA_API_TOKEN}

Токен можно создать в Settings → API Keys.

Выполняет SQL запрос и возвращает результат в объектном формате (рекомендуется для большинства случаев).

Через JavaScript/TypeScript:

const response = await fetch(
`https://api.onreza.ru/api/d1/databases/${databaseId}/query`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
sql: "SELECT * FROM users WHERE email = ?",
params: ["user@example.com"]
}),
}
);
const data = await response.json();

Ответ:

{
"success": true,
"result": {
"results": [
{ "id": 1, "name": "Alice", "email": "alice@example.com" }
],
"success": true,
"meta": {
"duration": 0.5,
"rows_read": 1,
"rows_written": 0
}
},
"errors": [],
"messages": []
}

Выполняет SQL запрос и возвращает результат в колоночном формате (оптимально для больших наборов данных).

Через JavaScript/TypeScript:

const response = await fetch(
`https://api.onreza.ru/api/d1/databases/${databaseId}/raw`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
sql: "SELECT * FROM users",
params: []
}),
}
);

Ответ:

{
"success": true,
"result": {
"results": {
"columns": ["id", "name", "email"],
"rows": [
[1, "Alice", "alice@example.com"],
[2, "Bob", "bob@example.com"]
]
}
}
}

Выполняет несколько SQL запросов в одной транзакции. Если один запрос не удаётся, откатываются все изменения.

Через JavaScript/TypeScript:

const response = await fetch(
`https://api.onreza.ru/api/d1/databases/${databaseId}/batch`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify([
{
sql: "INSERT INTO users (name, email) VALUES (?, ?)",
params: ["Alice", "alice@example.com"]
},
{
sql: "INSERT INTO logs (action) VALUES (?)",
params: ["user_created"]
}
]),
}
);

Ответ:

{
"success": true,
"result": [
{
"results": [],
"success": true,
"meta": { "duration": 0.3, "rows_read": 0, "rows_written": 1 }
},
{
"results": [],
"success": true,
"meta": { "duration": 0.2, "rows_read": 0, "rows_written": 1 }
}
],
"errors": [],
"messages": []
}

Для доступа к D1 из Edge Functions и Compute Apps используйте Runtime SDK:

Окно терминала
npm install @onreza/runtime
# или: pnpm add / yarn add / bun add
import { db } from '@onreza/runtime/db';
// Prepare → bind → execute
const { results } = await db.prepare('SELECT * FROM users WHERE email = ?')
.bind('alice@example.com')
.all();
// Первая строка
const user = await db.prepare('SELECT * FROM users WHERE id = ?')
.bind(1)
.first();
// Insert
await db.prepare('INSERT INTO users (name, email) VALUES (?, ?)')
.bind('Charlie', 'charlie@example.com')
.run();
// Batch (атомарная транзакция)
await db.batch([
db.prepare('INSERT INTO orders (user_id, total) VALUES (?, ?)').bind(1, 100),
db.prepare('UPDATE users SET order_count = order_count + 1 WHERE id = ?').bind(1),
]);
// DDL
await db.exec('CREATE TABLE IF NOT EXISTS logs (id INTEGER PRIMARY KEY, message TEXT)');
// app/api/users/route.ts (Next.js App Router)
import { db } from '@onreza/runtime/db';
export async function GET(request: Request) {
const url = new URL(request.url);
const email = url.searchParams.get('email');
if (email) {
const user = await db.prepare('SELECT * FROM users WHERE email = ?')
.bind(email)
.first();
return Response.json(user ?? { error: 'Not found' });
}
const { results } = await db.prepare('SELECT * FROM users LIMIT 50').all();
return Response.json(results);
}
export async function POST(request: Request) {
const { name, email } = await request.json();
await db.prepare('INSERT INTO users (name, email) VALUES (?, ?)')
.bind(name, email)
.run();
return Response.json({ success: true }, { status: 201 });
}

Используйте KV Store как кэш перед D1 для снижения нагрузки:

import { db } from '@onreza/runtime/db';
import { kv, isKVAvailable } from '@onreza/runtime/kv';
async function getUserById(id: number) {
const cacheKey = `cache:user:${id}`;
// 1. Проверяем кэш
if (isKVAvailable()) {
const cached = await kv.get(cacheKey, { type: 'json' });
if (cached !== null) return cached;
}
// 2. Запрос в D1
const user = await db.prepare('SELECT * FROM users WHERE id = ?')
.bind(id)
.first();
// 3. Сохраняем в кэш (5 минут)
if (user && isKVAvailable()) {
await kv.set(cacheKey, user, { ttl: 300 });
}
return user;
}
// Инвалидация при обновлении
async function updateUser(id: number, name: string) {
await db.prepare('UPDATE users SET name = ? WHERE id = ?')
.bind(name, id)
.run();
// Удаляем устаревший кэш
if (isKVAvailable()) {
await kv.delete(`cache:user:${id}`);
}
}

Для доступа к D1 вне ONREZA (CI/CD, внешние сервисы, локальная разработка) используйте REST API.

// Клиент для D1
class D1Client {
constructor(
private databaseId: string,
private token: string
) {}
async query(sql: string, params: unknown[] = []) {
const response = await fetch(
`https://api.onreza.ru/api/d1/databases/${this.databaseId}/query`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ sql, params }),
}
);
if (!response.ok) {
throw new Error(`D1 error: ${response.status}`);
}
const data = await response.json();
if (!data.success) {
throw new Error(data.errors?.[0]?.message || 'Unknown error');
}
return data.result;
}
async batch(statements: { sql: string; params: unknown[] }[]) {
const response = await fetch(
`https://api.onreza.ru/api/d1/databases/${this.databaseId}/batch`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(statements),
}
);
return response.json();
}
}
// Использование
const db = new D1Client(process.env.D1_DATABASE_ID!, process.env.ONREZA_API_TOKEN!);
// SELECT
const users = await db.query(
"SELECT * FROM users WHERE created_at > ?",
["2024-01-01"]
);
// INSERT
await db.query(
"INSERT INTO users (name, email) VALUES (?, ?)",
["Charlie", "charlie@example.com"]
);
// Batch (транзакция)
await db.batch([
{ sql: "INSERT INTO orders (user_id, total) VALUES (?, ?)", params: [1, 100] },
{ sql: "UPDATE users SET order_count = order_count + 1 WHERE id = ?", params: [1] },
]);
SQL типJavaScriptПримечание
INTEGERnumber64-bit signed integer
REALnumber64-bit float
TEXTstringUTF-8 строка
BLOBUint8Array или { $blob: base64 }Бинарные данные
NULLnull

D1 поддерживает работу с бинарными данными через механизм $blob JSON marker. Это позволяет сохранять и извлекать бинарные данные (изображения, файлы, сериализованные данные) без потери формата.

При отправке запроса используйте $blob с base64-кодированными данными:

// Запись бинарных данных через $blob marker
const imageBuffer = await fs.readFile('./avatar.png');
const base64 = imageBuffer.toString('base64');
await db.query(
"INSERT INTO attachments (id, data) VALUES (?, ?)",
[1, { $blob: base64 }]
);
// Или с использованием Uint8Array (автоматически сериализуется)
const binaryData = new Uint8Array([0x89, 0x50, 0x4E, 0x47]); // PNG header
await db.query(
"INSERT INTO files (id, content) VALUES (?, ?)",
[2, binaryData]
);

При чтении BLOB-данных возвращается объект с $blob маркером:

// Чтение — вернётся как объект с $blob
const result = await db.query(
"SELECT data FROM attachments WHERE id = ?",
[1]
);
// result.results[0].data будет:
// { $blob: "iVBORw0KGgoAAAANSUhEUgAA..." }
// Декодирование base64 в Uint8Array
function decodeBlob(blobObj: { $blob: string }): Uint8Array {
const binaryString = atob(blobObj.$blob);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
const imageData = decodeBlob(result.results[0].data);
await db.batch([
{
sql: "INSERT INTO documents (id, pdf) VALUES (?, ?)",
params: [1, { $blob: pdfBase64 }]
},
{
sql: "INSERT INTO thumbnails (doc_id, image) VALUES (?, ?)",
params: [1, { $blob: thumbnailBase64 }]
}
]);

D1 реализует многоуровневую систему безопасности для защиты ваших данных и инфраструктуры.

D1 автоматически блокирует потенциально опасные SQL-операции:

Заблокированная командаПричина
ATTACH DATABASEПредотвращает доступ к другим базам данных
DETACH DATABASEЗащита от отключения основной базы
PRAGMA (кроме разрешённых)Блокировка изменения настроек SQLite

При попытке выполнить заблокированную команду вы получите ошибку:

{
"success": false,
"errors": [
{
"message": "SQL command 'ATTACH' is not allowed"
}
]
}
ПараметрЛимит
Максимальная длина SQL1 MB
Строк в ответе5,000
Параметров в запросе100
Запросов в batch100
Таймаут запроса10 сек
Таймаут batch30 сек
Максимальный размер ответа10 MB

Всегда используйте параметризованные запросы для предотвращения SQL-инъекций:

// Неправильно — SQL injection риск
await db.query(`SELECT * FROM users WHERE email = '${email}'`);
// Правильно — параметризованный запрос
await db.query("SELECT * FROM users WHERE email = ?", [email]);
МетрикаStarterPro
Storage500 MB5 GB
Rows Read/месяц5M50M
Rows Written/месяц1M10M

При превышении на Starter — операции блокируются. На Pro — начисляется оверадж. Подробнее: Лимиты и квоты.

ПараметрЛимит
Максимальная длина SQL1 MB
Максимальное количество строк в результате5,000
Максимум параметров100
Максимум запросов в batch100
Таймаут одиночного запроса10 сек
Таймаут batch30 сек
Максимальный размер ответа10 MB
// Перевод денег — обе операции или ни одна
await db.batch([
{
sql: "UPDATE accounts SET balance = balance - ? WHERE id = ?",
params: [100, fromAccountId]
},
{
sql: "UPDATE accounts SET balance = balance + ? WHERE id = ?",
params: [100, toAccountId]
},
{
sql: "INSERT INTO transactions (from_id, to_id, amount) VALUES (?, ?, ?)",
params: [fromAccountId, toAccountId, 100]
},
]);
-- Создавайте индексы для часто используемых полей
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_created ON orders(created_at);

D1 автоматически управляет соединениями. Просто делайте запросы — не нужно заботиться о пулах.

// Результат ограничен 5,000 строками — используйте LIMIT/OFFSET
async function* paginateUsers(batchSize = 1000) {
let offset = 0;
while (true) {
const rows = await db.query(
"SELECT * FROM users LIMIT ? OFFSET ?",
[batchSize, offset]
);
if (rows.results.length === 0) break;
yield rows.results;
offset += batchSize;
}
}
// Использование
for await (const batch of paginateUsers()) {
for (const user of batch) {
console.log(user);
}
}
await db.query(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
await db.query(`
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT,
published BOOLEAN DEFAULT FALSE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
)
`);

Рекомендуется хранить миграции в виде SQL-файлов и применять при деплое:

migrations/001_init.sql
// migrations/002_add_indexes.sql
async function migrate() {
const files = await fs.readdir('./migrations');
const sqlFiles = files.filter(f => f.endsWith('.sql')).sort();
for (const file of sqlFiles) {
const sql = await fs.readFile(`./migrations/${file}`, 'utf-8');
await db.query(sql);
console.log(`Applied: ${file}`);
}
}

Таблица не существует. Проверьте что миграции применены.

Нарушение уникального ограничения. Проверьте данные перед INSERT.

Запрос выполняется дольше 10 секунд. Добавьте индексы или оптимизируйте запрос.

В batch больше 100 запросов. Разбейте на несколько batch-ей.

Результат содержит более 5000 строк. Используйте LIMIT и OFFSET для пагинации.

Вы пытаетесь выполнить заблокированную команду (ATTACH, DETACH, PRAGMA). Эти команды запрещены для безопасности.