Bun 은 고성능 SQLite3 드라이버를 네이티브로 구현합니다. 사용하려면 내장 bun:sqlite 모듈에서 가져오세요.
import { Database } from "bun:sqlite";
const db = new Database(":memory:");
const query = db.query("select 'Hello world' as message;");
query.get();{ message: "Hello world" }이 API 는 간단하고 동기적이며 빠릅니다. bun:sqlite 의 API 에 영감을 준 better-sqlite3 와 그 기여자들에게 감사를 드립니다.
기능은 다음과 같습니다:
- 트랜잭션
- 파라미터 (이름付き 및 위치)
- 준비된 문 (prepared statements)
- 데이터 타입 변환 (
BLOB은Uint8Array가 됨) - ORM 없이 쿼리 결과를 클래스에 매핑 -
query.as(MyClass) - JavaScript 의 모든 SQLite 드라이버 중 가장 빠른 성능
bigint지원- 단일 database.run(query) 호출에서 여러 쿼리 문 (예:
SELECT 1; SELECT 2;)
bun:sqlite 모듈은 읽기 쿼리에서 better-sqlite3 보다 약 3-6 배, deno.land/x/sqlite 보다 8-9 배 더 빠릅니다. 각 드라이버는 Northwind Traders 데이터셋을 사용하여 벤치마크되었습니다. 벤치마크 소스 를 확인하고 실행하세요.
Database
SQLite3 데이터베이스를 열거나 생성하려면:
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite");인메모리 데이터베이스를 열려면:
import { Database } from "bun:sqlite";
// 이 모두는 동일한 작업을 수행합니다
const db = new Database(":memory:");
const db = new Database();
const db = new Database("");readonly 모드로 열려면:
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite", { readonly: true });파일이 없으면 데이터베이스를 생성하려면:
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite", { create: true });엄격 모드
기본적으로 bun:sqlite 는 바인딩 파라미터에 $, :, 또는 @ 접두사를 포함해야 하며, 파라미터가 누락되더라도 오류를 throw 하지 않습니다.
파라미터가 누락되었을 때 오류를 throw 하고 접두사 없이 바인딩하려면 Database 생성자에서 strict: true 를 설정하세요:
import { Database } from "bun:sqlite";
const strict = new Database(":memory:", { strict: true });
// 오타로 인해 오류 throw:
const query = strict.query("SELECT $message;").all({ messag: "Hello world" });
const notStrict = new Database(":memory:");
// 오류 throw 하지 않음:
notStrict.query("SELECT $message;").all({ messag: "Hello world" });ES 모듈 가져오기를 통해 로드
import attribute 를 사용하여 데이터베이스를 로드할 수도 있습니다.
import db from "./mydb.sqlite" with { type: "sqlite" };
console.log(db.query("select * from users LIMIT 1").get());이는 다음과 동일합니다:
import { Database } from "bun:sqlite";
const db = new Database("./mydb.sqlite");.close(throwOnError: boolean = false)
기존 쿼리가 완료될 수 있도록 하면서 데이터베이스 연결을 닫으려면 .close(false) 를 호출하세요:
const db = new Database();
// ... 작업 수행
db.close(false);데이터베이스를 닫고 보류 중인 쿼리가 있으면 오류를 throw 하려면 .close(true) 를 호출하세요:
const db = new Database();
// ... 작업 수행
db.close(true);NOTE
`close(false)` 는 데이터베이스가 가비지 컬렉션될 때 자동으로 호출됩니다. 여러 번 호출해도 안전하지만 첫 번째 호출 이후에는 효과가 없습니다.using 문
using 문을 사용하여 using 블록이 종료될 때 데이터베이스 연결이 닫히도록 할 수 있습니다.
import { Database } from "bun:sqlite";
{
using db = new Database("mydb.sqlite");
using query = db.query("select 'Hello world' as message;");
console.log(query.get());
}{ message: "Hello world" }.serialize()
bun:sqlite 는 SQLite 의 내장 직렬화 및 역직렬화 메커니즘을 지원합니다.
const olddb = new Database("mydb.sqlite");
const contents = olddb.serialize(); // => Uint8Array
const newdb = Database.deserialize(contents);내부적으로 .serialize() 는 sqlite3_serialize 를 호출합니다.
.query()
Database 인스턴스에서 db.query() 메서드를 사용하여 SQL 쿼리를 준비하세요. 결과는 Database 인스턴스에 캐시되는 Statement 인스턴스입니다. 쿼리는 실행되지 않습니다.
const query = db.query(`select "Hello world" as message`);NOTE
**"캐시됨"이란 무엇을 의미하나요?**캐싱은 쿼리 결과가 아닌 컴파일된 준비된 문(SQL 바이트코드) 을 의미합니다. 동일한 SQL 문자열로 db.query() 를 여러 번 호출하면 Bun 은 SQL 을 다시 컴파일하는 대신 동일한 캐시된 Statement 객체를 반환합니다.
다른 파라미터 값으로 캐시된 문을 안전하게 재사용할 수 있습니다:
const query = db.query("SELECT * FROM users WHERE id = ?");
query.get(1); // ✓ 작동
query.get(2); // ✓ 또한 작동 - 파라미터는 매번 새로 바인딩됨
query.get(3); // ✓ 여전히 작동일회성 쿼리로 캐시를 채우고 싶지 않을 때와 같이 캐시되지 않은 새 Statement 인스턴스가 필요할 때는 .query() 대신 .prepare() 를 사용하세요.
// 캐시 없이 준비된 문 컴파일
const query = db.prepare("SELECT * FROM foo WHERE bar = ?");WAL 모드
SQLite 는 write-ahead log 모드(WAL) 를 지원합니다. 이는 특히 많은 동시 리더와 단일 라이터가 있는 상황에서 성능을 극적으로 향상시킵니다. 대부분의 일반적인 애플리케이션에서 WAL 모드를 활성화하는 것이 널리 권장됩니다.
WAL 모드를 활성화하려면 애플리케이션 시작 시 이 프라그마 쿼리를 실행하세요:
db.run("PRAGMA journal_mode = WAL;");WAL 모드란 무엇인가요?">
WAL 모드에서 데이터베이스에 대한 쓰기는 "WAL 파일"(write-ahead log) 이라고 하는 별도의 파일에 직접 기록됩니다. 이 파일은 나중에 메인 데이터베이스 파일에 통합됩니다. 보류 중인 쓰기를 위한 버퍼로 생각하세요. 자세한 개요는 SQLite 문서 를 참조하세요.
macOS 에서 WAL 파일은 기본적으로 지속적일 수 있습니다. 이는 버그가 아니며 macOS 가 시스템 버전의 SQLite 를 구성하는 방식입니다.
Statements
Statement 는 준비된 쿼리 로, 효율적인 바이너리 형태로 파싱되고 컴파일되었음을 의미합니다. 이는 performant 한 방식으로 여러 번 실행될 수 있습니다.
Database 인스턴스에서 .query 메서드로 문을 생성하세요.
const query = db.query(`select "Hello world" as message`);쿼리는 숫자 (?1) 또는 이름付き ($param 또는 :param 또는 @param) 파라미터를 포함할 수 있습니다.
const query = db.query(`SELECT ?1, ?2;`);
const query = db.query(`SELECT $param1, $param2;`);쿼리가 실행될 때 이 파라미터에 값이 바인딩됩니다. Statement 는 여러 다른 메서드로 실행될 수 있으며, 각 메서드는 결과를 다른 형태로 반환합니다.
값 바인딩
문에 값을 바인딩하려면 .all(), .get(), .run() 또는 .values() 메서드에 객체를 전달하세요.
const query = db.query(`select $message;`);
query.all({ $message: "Hello world" });위치 파라미터를 사용하여 바인딩할 수도 있습니다:
const query = db.query(`select ?1;`);
query.all("Hello world");strict: true 를 사용하면 접두사 없이 값을 바인딩할 수 있습니다
기본적으로 $, :, @ 접두사는 이름付き 파라미터에 값을 바인딩할 때 포함됩니다. 이러한 접두사 없이 바인딩하려면 Database 생성자에서 strict 옵션을 사용하세요.
import { Database } from "bun:sqlite";
const db = new Database(":memory:", {
// 접두사 없이 값 바인딩
strict: true,
});
const query = db.query(`select $message;`);
// strict: true
query.all({ message: "Hello world" });
// strict: false
// query.all({ $message: "Hello world" });.all()
.all() 을 사용하여 쿼리를 실행하고 결과를 객체 배열로 가져오세요.
const query = db.query(`select $message;`);
query.all({ $message: "Hello world" });[{ message: "Hello world" }]내부적으로 이는 sqlite3_reset 을 호출하고 sqlite3_step 을 SQLITE_DONE 을 반환할 때까지 반복적으로 호출합니다.
.get()
.get() 을 사용하여 쿼리를 실행하고 첫 번째 결과를 객체로 가져오세요.
const query = db.query(`select $message;`);
query.get({ $message: "Hello world" });{ $message: "Hello world" }내부적으로 이는 sqlite3_reset 을 호출한 후 sqlite3_step 을 더 이상 SQLITE_ROW 을 반환하지 않을 때까지 호출합니다. 쿼리가 행을 반환하지 않으면 undefined 가 반환됩니다.
.run()
.run() 을 사용하여 쿼리를 실행하고 undefined 를 가져오세요. 이는 스키마 수정 쿼리 (예: CREATE TABLE) 또는 대량 쓰기 작업에 유용합니다.
const query = db.query(`create table foo;`);
query.run();{
lastInsertRowid: 0,
changes: 0,
}내부적으로 이는 sqlite3_reset 을 호출하고 sqlite3_step 을 한 번 호출합니다. 결과가 필요하지 않을 때는 모든 행을 스텝하는 것이 필요하지 않습니다.
lastInsertRowid 속성은 데이터베이스에 마지막으로 삽입된 행의 ID 를 반환합니다. changes 속성은 쿼리에 의해 영향을 받은 행의 수입니다.
.as(Class) - 쿼리 결과를 클래스에 매핑
.as(Class) 를 사용하여 쿼리를 실행하고 결과를 클래스의 인스턴스로 가져오세요. 이를 통해 결과에 메서드 및 getter/setter 를 첨부할 수 있습니다.
class Movie {
title: string;
year: number;
get isMarvel() {
return this.title.includes("Marvel");
}
}
const query = db.query("SELECT title, year FROM movies").as(Movie);
const movies = query.all();
const first = query.get();
console.log(movies[0].isMarvel);
console.log(first.isMarvel);true
true성능 최적화로 인해 클래스 생성자는 호출되지 않고, 기본 초기화는 실행되지 않으며, private 필드는 액세스할 수 없습니다. 이는 new 보다 Object.create 를 사용하는 것과 더 유사합니다. 클래스의 프로토타입이 객체에 할당되고, 메서드가 첨부되며, getter/setter 가 설정되지만 생성자는 호출되지 않습니다.
데이터베이스 열은 클래스 인스턴스의 속성으로 설정됩니다.
.iterate() (@@iterator)
.iterate() 를 사용하여 쿼리를 실행하고 결과를 점진적으로 반환하세요. 이는 모든 결과를 메모리에 로드하지 않고 한 번에 한 행씩 처리하려는 큰 결과 집합에 유용합니다.
const query = db.query("SELECT * FROM foo");
for (const row of query.iterate()) {
console.log(row);
}@@iterator 프로토콜을 사용할 수도 있습니다:
const query = db.query("SELECT * FROM foo");
for (const row of query) {
console.log(row);
}.values()
values() 를 사용하여 쿼리를 실행하고 모든 결과를 배열의 배열로 가져오세요.
const query = db.query(`select $message;`);
query.values({ $message: "Hello world" });
query.values(2);[
[ "Iron Man", 2008 ],
[ "The Avengers", 2012 ],
[ "Ant-Man: Quantumania", 2023 ],
]내부적으로 이는 sqlite3_reset 을 호출하고 sqlite3_step 을 SQLITE_DONE 을 반환할 때까지 반복적으로 호출합니다.
.finalize()
.finalize() 를 사용하여 Statement 를 파괴하고 이와 관련된 리소스를 해제하세요. 한 번 finalize 된 Statement 는 다시 실행할 수 없습니다. 일반적으로 가비지 컬렉터가 이를 수행하지만 명시적인 finalize 는 성능에 민감한 애플리케이션에서 유용할 수 있습니다.
const query = db.query("SELECT title, year FROM movies");
const movies = query.all();
query.finalize();.toString()
Statement 인스턴스에서 toString() 을 호출하면 확장된 SQL 쿼리가 출력됩니다. 이는 디버깅에 유용합니다.
import { Database } from "bun:sqlite";
// 설정
const query = db.query("SELECT $param;");
console.log(query.toString()); // => "SELECT NULL"
query.run(42);
console.log(query.toString()); // => "SELECT 42"
query.run(365);
console.log(query.toString()); // => "SELECT 365"내부적으로 이는 sqlite3_expanded_sql 을 호출합니다. 파라미터는 가장 최근에 바인딩된 값을 사용하여 확장됩니다.
파라미터
쿼리는 파라미터를 포함할 수 있습니다. 이는 숫자 (?1) 또는 이름付き ($param 또는 :param 또는 @param) 일 수 있습니다. 쿼리를 실행할 때 이 파라미터에 값을 바인딩하세요:
const query = db.query("SELECT * FROM foo WHERE bar = $bar");
const results = query.all({
$bar: "bar",
});[{ "$bar": "bar" }]번호付き (위치) 파라미터도 작동합니다:
const query = db.query("SELECT ?1, ?2");
const results = query.all("hello", "goodbye");[
{
"?1": "hello",
"?2": "goodbye",
},
];정수
sqlite 는 부호 있는 64 비트 정수를 지원하지만 JavaScript 는 부호 있는 52 비트 정수 또는 bigint 를 사용한 임의 정밀도 정수만 지원합니다.
bigint 입력은 어디서나 지원되지만 기본적으로 bun:sqlite 는 정수를 number 타입으로 반환합니다. 2^53 보다 큰 정수를 처리해야 한다면 Database 인스턴스를 생성할 때 safeIntegers 옵션을 true 로 설정하세요. 이는 또한 bun:sqlite 에 전달된 bigint 가 64 비트를 초과하지 않는지 검증합니다.
기본적으로 bun:sqlite 는 정수를 number 타입으로 반환합니다. 2^53 보다 큰 정수를 처리해야 한다면 bigint 타입을 사용할 수 있습니다.
safeIntegers: true
safeIntegers 가 true 일 때 bun:sqlite 는 정수를 bigint 타입으로 반환합니다:
import { Database } from "bun:sqlite";
const db = new Database(":memory:", { safeIntegers: true });
const query = db.query(`SELECT ${BigInt(Number.MAX_SAFE_INTEGER) + 102n} as max_int`);
const result = query.get();
console.log(result.max_int);9007199254741093nsafeIntegers 가 true 일 때 바인딩된 파라미터의 bigint 값이 64 비트를 초과하면 bun:sqlite 는 오류를 throw 합니다:
import { Database } from "bun:sqlite";
const db = new Database(":memory:", { safeIntegers: true });
db.run("CREATE TABLE test (id INTEGER PRIMARY KEY, value INTEGER)");
const query = db.query("INSERT INTO test (value) VALUES ($value)");
try {
query.run({ $value: BigInt(Number.MAX_SAFE_INTEGER) ** 2n });
} catch (e) {
console.log(e.message);
}BigInt value '81129638414606663681390495662081' is out of rangesafeIntegers: false (기본값)
safeIntegers 가 false 일 때 bun:sqlite 는 정수를 number 타입으로 반환하고 53 비트를 초과하는 비트를 잘라냅니다:
import { Database } from "bun:sqlite";
const db = new Database(":memory:", { safeIntegers: false });
const query = db.query(`SELECT ${BigInt(Number.MAX_SAFE_INTEGER) + 102n} as max_int`);
const result = query.get();
console.log(result.max_int);9007199254741092트랜잭션
트랜잭션은 여러 쿼리를 원자적 으로 실행하는 메커니즘입니다. 즉, 모든 쿼리가 성공하거나 아무것도 성공하지 않습니다. db.transaction() 메서드로 트랜잭션을 생성하세요:
const insertCat = db.prepare("INSERT INTO cats (name) VALUES ($name)");
const insertCats = db.transaction(cats => {
for (const cat of cats) insertCat.run(cat);
});이 시점에서 아직 고양이를 삽입하지 않았습니다! db.transaction() 호출은 쿼리를 실행하는 함수를 래핑 하는 새 함수 (insertCats) 를 반환합니다.
트랜잭션을 실행하려면 이 함수를 호출하세요. 모든 인수는 래핑된 함수에 전달되며, 래핑된 함수의 반환 값은 트랜잭션 함수에 의해 반환됩니다. 래핑된 함수는 트랜잭션이 실행되는 곳에서 정의된 this 컨텍스트에도 액세스할 수 있습니다.
const insert = db.prepare("INSERT INTO cats (name) VALUES ($name)");
const insertCats = db.transaction(cats => {
for (const cat of cats) insert.run(cat);
return cats.length;
});
const count = insertCats([{ $name: "Keanu" }, { $name: "Salem" }, { $name: "Crookshanks" }]);
console.log(`Inserted ${count} cats`);드라이버는 insertCats 가 호출될 때 자동으로 begin 트랜잭션을 시작하고 래핑된 함수가 반환될 때 commit 합니다. 예외가 throw 되면 트랜잭션은 롤백됩니다. 예외는 평소와 같이 전파되며 catch 되지 않습니다.
NOTE
**중첩 트랜잭션** — 트랜잭션 함수는 다른 트랜잭션 함수 내부에서 호출될 수 있습니다. 그렇게 할 때 내부 트랜잭션은 [savepoint](https://www.sqlite.org/lang_savepoint.html) 가 됩니다.중첩 트랜잭션 예제 보기">
// 설정
import { Database } from "bun:sqlite";
const db = Database.open(":memory:");
db.run("CREATE TABLE expenses (id INTEGER PRIMARY KEY AUTOINCREMENT, note TEXT, dollars INTEGER);");
db.run("CREATE TABLE cats (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, age INTEGER)");
const insertExpense = db.prepare("INSERT INTO expenses (note, dollars) VALUES (?, ?)");
const insert = db.prepare("INSERT INTO cats (name, age) VALUES ($name, $age)");
const insertCats = db.transaction(cats => {
for (const cat of cats) insert.run(cat);
});
const adopt = db.transaction(cats => {
insertExpense.run("adoption fees", 20);
insertCats(cats); // 중첩 트랜잭션
});
adopt([
{ $name: "Joey", $age: 2 },
{ $name: "Sally", $age: 4 },
{ $name: "Junior", $age: 1 },
]);트랜잭션에는 deferred, immediate, exclusive 버전도 있습니다.
insertCats(cats); // "BEGIN" 사용
insertCats.deferred(cats); // "BEGIN DEFERRED" 사용
insertCats.immediate(cats); // "BEGIN IMMEDIATE" 사용
insertCats.exclusive(cats); // "BEGIN EXCLUSIVE" 사용.loadExtension()
SQLite 확장 을 로드하려면 Database 인스턴스에서 .loadExtension(name) 을 호출하세요.
import { Database } from "bun:sqlite";
const db = new Database();
db.loadExtension("myext");NOTE
**MacOS 사용자** 기본적으로 macOS 는 확장을 지원하지 않는 Apple 의 독점 SQLite 빌드와 함께 제공됩니다. 확장을 사용하려면 일반 SQLite 빌드를 설치해야 합니다.brew install sqlite
which sqlite # 바이너리 경로 가져오기bun:sqlite 를 새 빌드로 지정하려면 Database 인스턴스를 생성하기 전에 Database.setCustomSQLite(path) 를 호출하세요. (다른 운영체제에서는 no-op 입니다.) SQLite .dylib 파일 경로 (실행 파일이 아님) 를 전달하세요. 최근 버전의 Homebrew 를 사용하면 /opt/homebrew/Cellar/sqlite/<version>/libsqlite3.dylib 와 같습니다.
import { Database } from "bun:sqlite";
Database.setCustomSQLite("/path/to/libsqlite.dylib");
const db = new Database();
db.loadExtension("myext");.fileControl(cmd: number, value: any)
고급 sqlite3_file_control API 를 사용하려면 Database 인스턴스에서 .fileControl(cmd, value) 를 호출하세요.
import { Database, constants } from "bun:sqlite";
const db = new Database();
// WAL 모드가 지속되지 않도록 함
// 이는 데이터베이스가 닫힌 후 wal 파일이 남지 않도록 방지
db.fileControl(constants.SQLITE_FCNTL_PERSIST_WAL, 0);value 는 다음이 될 수 있습니다:
numberTypedArrayundefined또는null
참조
class Database {
constructor(
filename: string,
options?:
| number
| {
readonly?: boolean;
create?: boolean;
readwrite?: boolean;
safeIntegers?: boolean;
strict?: boolean;
},
);
query<ReturnType, ParamsType>(sql: string): Statement<ReturnType, ParamsType>;
prepare<ReturnType, ParamsType>(sql: string): Statement<ReturnType, ParamsType>;
run(sql: string, params?: SQLQueryBindings): { lastInsertRowid: number; changes: number };
exec = this.run;
transaction(insideTransaction: (...args: any) => void): CallableFunction & {
deferred: (...args: any) => void;
immediate: (...args: any) => void;
exclusive: (...args: any) => void;
};
close(throwOnError?: boolean): void;
}
class Statement<ReturnType, ParamsType> {
all(...params: ParamsType[]): ReturnType[];
get(...params: ParamsType[]): ReturnType | null;
run(...params: ParamsType[]): {
lastInsertRowid: number;
changes: number;
};
values(...params: ParamsType[]): unknown[][];
finalize(): void; // 문 파괴 및 리소스 정리
toString(): string; // SQL 로 직렬화
columnNames: string[]; // 결과 집합의 열 이름
columnTypes: string[]; // 첫 번째 행의 실제 값을 기반으로 한 타입 (.get()/.all() 먼저 호출)
declaredTypes: (string | null)[]; // CREATE TABLE 스키마의 타입 (.get()/.all() 먼저 호출)
paramsCount: number; // 문에서 예상하는 파라미터 수
native: any; // 문을 나타내는 네이티브 객체
as<T>(Class: new (...args: any[]) => T): Statement<T, ParamsType>;
}
type SQLQueryBindings =
| string
| bigint
| TypedArray
| number
| boolean
| null
| Record<string, string | bigint | TypedArray | number | boolean | null>;데이터 타입
| JavaScript 타입 | SQLite 타입 |
|---|---|
string | TEXT |
number | INTEGER 또는 DECIMAL |
boolean | INTEGER (1 또는 0) |
Uint8Array | BLOB |
Buffer | BLOB |
bigint | INTEGER |
null | NULL |