يطبق 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" }واجهة برمجة التطبيقات بسيطة ومتزامنة وسريعة. الفضل يعود إلى better-sqlite3 ومساهميها في إلهام واجهة برمجة التطبيقات لـ bun:sqlite.
تشمل الميزات:
- المعاملات
- المعلمات (مسماة وموضعية)
- العبارات المحضرة
- تحويلات نوع البيانات (
BLOBيصبحUint8Array) - تعيين نتائج الاستعلام إلى فئات بدون ORM -
query.as(MyClass) - أسرع أداء لأي محرك SQLite لـ JavaScript
- دعم
bigint - عبارات متعددة الاستعلامات (مثل
SELECT 1; SELECT 2;) في استدعاء واحد لـ database.run(query)
وحدة bun:sqlite أسرع بحوالي 3-6 مرات من better-sqlite3 و 8-9 مرات أسرع من deno.land/x/sqlite لاستعلامات القراءة. تم اختبار كل محرك مقابل مجموعة بيانات Northwind Traders. اعرض وشغل مصدر المعيار.
قاعدة البيانات
لفتح أو إنشاء قاعدة بيانات 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 ربط المعلمات لتشمل البادئة $ أو : أو @، ولا يطرح خطأ إذا كان هناك معامل مفقود.
لإطلاق خطأ بدلاً من ذلك عند فقدان معامل والسماح بالربط بدون بادئة، عيّن strict: true على منشئ Database:
import { Database } from "bun:sqlite";
const strict = new Database(":memory:", { strict: true });
// يطرح خطأ بسبب الخطأ المطبعي:
const query = strict.query("SELECT $message;").all({ messag: "Hello world" });
const notStrict = new Database(":memory:");
// لا يطرح خطأ:
notStrict.query("SELECT $message;").all({ messag: "Hello world" });التحميل عبر استيراد وحدة ES
يمكنك أيضًا استخدام سمة استيراد لتحميل قاعدة بيانات.
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);لإغلاق قاعدة البيانات وإطلاق خطأ إذا كان هناك أي استعلامات معلقة، استدعِ .close(true):
const db = new Database();
// ... قم بالأشياء
db.close(true);NOTE
يتم استدعاء `close(false)` تلقائيًا عند جمع قاعدة البيانات بواسطة GC. من الآمن استدعاؤها عدة مرات لكن ليس لها تأثير بعد المرة الأولى.جملة 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()
استخدم طريقة db.query() على مثيل Database الخاص بك لـ تحضير استعلام SQL. النتيجة هي مثيل Statement سيتم تخزينه مؤقتًا على مثيل Database. لن يتم تنفيذ الاستعلام.
const query = db.query(`select "Hello world" as message`);NOTE
**ماذا يعني "مخزن مؤقتًا"؟**يشير التخزين المؤقت إلى العبارة المحضرة المترجمة (بايت كود SQL)، وليس نتائج الاستعلام. عند استدعاء db.query() بنفس سلسلة SQL عدة مرات، يعيد Bun نفس كائن Statement المخزن مؤقتًا بدلاً من إعادة تجميع SQL.
من الآمن تمامًا إعادة استخدام عبارة مخزنة مؤقتًا بقيم معلمات مختلفة:
const query = db.query("SELECT * FROM users WHERE id = ?");
query.get(1); // ✓ يعمل
query.get(2); // ✓ يعمل أيضًا - يتم ربط المعلمات جديدة في كل مرة
query.get(3); // ✓ لا يزال يعملاستخدم .prepare() بدلاً من .query() عندما تريد مثيل Statement جديد غير مخزن مؤقت، على سبيل المثال إذا كنت تنشئ SQL ديناميكيًا ولا تريد ملء التخزين المؤقت باستعلامات لمرة واحدة.
// تجميع العبارة المحضرة بدون تخزين مؤقت
const query = db.prepare("SELECT * FROM foo WHERE bar = ?");وضع WAL
يدعم SQLite وضع سجل الكتابة المسبق (WAL) الذي يحسن الأداء بشكل كبير، خاصة في المواقف التي تحتوي على العديد من القراء المتزامنين وكاتب واحد. يُنصح بشكل عام بتمكين وضع WAL لمعظم التطبيقات النموذجية.
لتمكين وضع WAL، شغّل استعلام pragma هذا في بداية تطبيقك:
db.run("PRAGMA journal_mode = WAL;");ما هو وضع WAL؟
في وضع WAL، تُكتب الكتابات إلى قاعدة البيانات مباشرة في ملف منفصل يسمى "ملف WAL" (سجل الكتابة المسبق). سيتم دمج هذا الملف لاحقًا في ملف قاعدة البيانات الرئيسي. فكر فيه كمخزن مؤقت للكتابات المعلقة. راجع وثائق SQLite للحصول على نظرة عامة أكثر تفصيلاً.
على macOS، قد تكون ملفات WAL ثابتة افتراضيًا. هذا ليس خطأً، فهذه هي الطريقة التي قام بها macOS بتكوين إصدار النظام من SQLite.
العبارات
الـ Statement هي استعلام محضر، مما يعني أنه تم تحليله وتجميعه في شكل ثنائي فعال. يمكن تنفيذه عدة مرات بطريقة فعالة.
أنشئ عبارة باستخدام طريقة .query على مثيل Database الخاص بك.
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 يسمح لك بربط القيم بدون بادئات
افتراضيًا، تتضمن البادئات $ و : و @ عند ربط القيم بالمعلمات المسماة. للربط بدون هذه البادئات، استخدم خيار strict في منشئ Database.
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 معرف آخر صف تم إدراجه في قاعدة البيانات. خاصية changes هي عدد الصفوف المتأثرة بالاستعلام.
.as(Class) - تعيين نتائج الاستعلام إلى فئة
استخدم .as(Class) لتشغيل استعلام والحصول على النتائج كمثيلات لفئة. هذا يسمح لك بإرفاق طرق و getters/setters بالنتائج.
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كتحسين للأداء، لا يتم استدعاء منشئ الفئة، ولا يتم تشغيل المهيئات الافتراضية، ولا يمكن الوصول إلى الحقول الخاصة. هذا أشبه باستخدام Object.create من new. يتم تعيين نموذج الفئة الأولي على الكائن، وتُرفق الطرق، ويتم إعداد getters/setters، لكن لا يتم استدعاء المنشئ.
يتم تعيين أعمدة قاعدة البيانات كخصائص على مثيل الفئة.
.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 وتحرير أي موارد مرتبطة به. بمجرد الانتهاء، لا يمكن تنفيذ Statement مرة أخرى. عادةً، سيقوم GC بذلك نيابةً عنك، لكن الإنهاء الصريح قد يكون مفيدًا في التطبيقات الحساسة للأداء.
const query = db.query("SELECT title, year FROM movies");
const movies = query.all();
query.finalize();.toString()
استدعاء toString() على مثيل Statement يطبع استعلام 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، عيّن خيار safeIntegers إلى true عند إنشاء مثيل Database. يتحقق هذا أيضًا من أن bigint الممررة إلى bun:sqlite لا تتجاوز 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);9007199254741093nعندما يكون safeIntegers هو true، سيطرح bun:sqlite خطأ إذا كانت قيمة bigint في معامل مرتبط تتجاوز 64 بت:
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 '81129638414606663681390495662081' خارج النطاقsafeIntegers: 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(`تم إدراج ${count} قطط`);سيقوم المحرك تلقائيًا begin معاملة عند استدعاء insertCats و commit عندما تعود الدالة المغلفة. إذا تم طرح استثناء، فسيتم ارتداد المعاملة. سينتشر الاستثناء كالمعتاد؛ لا يتم التقاطه.
NOTE
**المعاملات المتداخلة** — يمكن استدعاء دوال المعاملات من داخل دوال معاملات أخرى. عند القيام بذلك، تصبح المعاملة الداخلية [نقطة حفظ](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، استدعِ .loadExtension(name) على مثيل Database الخاص بك
import { Database } from "bun:sqlite";
const db = new Database();
db.loadExtension("myext");NOTE
**مستخدمو MacOS** افتراضيًا، يشحن macOS مع بناء SQLite الخاص بـ Apple، الذي لا يدعم الامتدادات. لاستخدام الامتدادات، ستحتاج إلى تثبيت بناء SQLite عادي.brew install sqlite
which sqlite # الحصول على مسار الثنائيلتوجيه bun:sqlite إلى البناء الجديد، استدعِ Database.setCustomSQLite(path) قبل إنشاء أي مثيلات Database. (على أنظمة التشغيل الأخرى، هذا لا يفعل شيئًا.) مرر مسارًا إلى ملف .dylib لـ SQLite، ليس الملف التنفيذي. مع إصدارات 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، استدعِ .fileControl(cmd, value) على مثيل Database الخاص بك.
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 |