node:sqlite で取ってきた行を assert.deepStrictEqual でオブジェクトと比べると、ダンプが同一に表示されるのにテストが落ちる、というエラーです。
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
+ actual - expected
[
+ [Object: null prototype] {
- {
name: 'a',
score: 10
},
+ [Object: null prototype] {
- {
name: 'b',
score: 20
}
]
actual: [ { name: 'a', score: 10 }, { name: 'b', score: 20 } ]
expected: [ { name: 'a', score: 10 }, { name: 'b', score: 20 } ]
operator: 'deepStrictEqual'
下の actual: と expected: の行は文字通り同じダンプです。それでも落ちます。差が出ているのは 1 か所、[Object: null prototype] という表示だけ。キーも値も一致しているのに、プロトタイプが違うためです。
node:sqlite の行は「素のオブジェクト」ではない
DatabaseSync#prepare(...).all() や .get() が返す行は、プロトタイプを持たないオブジェクト(Object.create(null) 相当)です。実際に確かめられます。
const row = db.prepare('SELECT name, score FROM scores').get();
Object.getPrototypeOf(row) === null; // true
一方、テストで書く { name: 'a', score: 10 } は普通のオブジェクトリテラルなので、プロトタイプは Object.prototype です。
assert.deepStrictEqual は、キーと値だけでなくプロトタイプも比較します(Node 公式ドキュメントの Comparison details:オブジェクトの [[Prototype]] を === で比較する)。だから、name も score も完全に一致していても、null プロトタイプの行と Object.prototype のリテラルは「厳密には等しくない」と判定されます。ダンプの下部(actual: / expected:)にはプロトタイプが出ません。差が現れるのは、上の構造差分の [Object: null prototype] だけです。
行にプロトタイプが無いということ
行に Object.prototype が付いていると、選んでいない列まで「存在するように見えます」。普通のオブジェクトは toString や hasOwnProperty、constructor を継承するので、そういう列を SELECT していなくても row.toString は継承した関数を返し、row[col] のような存在検査が誤爆します。null プロトタイプの行は、選んだ列だけを自分のプロパティとして持ちます。
toString 列が無い行 -> row.toString = undefined # 継承メソッドが漏れてこない
普通のリテラル -> typeof lit.toString = function # こちらは Object.prototype を継承する
(toString という名前の列がある行なら、値はそのまま取れます。これはプロトタイプに関わらず自前プロパティが継承メンバを上書きするからで、null プロトタイプ固有の話ではありません。)
この null プロトタイプという挙動は、node:sqlite の公式ドキュメントには書かれていません(戻り値は「オブジェクト」「行に対応するオブジェクト」としか説明されていない)。実装がそう返すという実測に基づく事実で、deepStrictEqual が落ちるのはその帰結です。行の中身が壊れているわけではありません。 ドキュメント化された保証ではないので、node:sqlite が stable になる過程で変わる可能性は残ります。
直し方:比較の前に素のオブジェクトへ写す
値だけを比べたいなら、行を普通のオブジェクトに写してから比較します。スプレッドで包むと、自分のプロパティが Object.prototype を持つ新しいオブジェクトへコピーされます。
// Before([Object: null prototype] で落ちる)
const rows = db.prepare('SELECT name, score FROM scores ORDER BY name').all();
assert.deepStrictEqual(rows, [
{ name: 'a', score: 10 },
{ name: 'b', score: 20 },
]);
// After(各行をスプレッドで写すだけ)
const rows = db
.prepare('SELECT name, score FROM scores ORDER BY name')
.all()
.map((row) => ({ ...row }));
assert.deepStrictEqual(rows, [
{ name: 'a', score: 10 },
{ name: 'b', score: 20 },
]);
差は .map((row) => ({ ...row })) の 1 か所だけです。写したオブジェクトは Object.prototype を持つので、リテラル側とプロトタイプがそろい、比較が通ります。行の値は TEXT / INTEGER / REAL のプリミティブと BLOB(Uint8Array)で、入れ子のプレーンオブジェクトは無いので、浅いコピー(スプレッド)で足ります(実測)。
Object.assign({}, row) は同じではありません。__proto__ という名前の列があると、Object.assign はその値を Object.prototype の setter に流してしまい、列を取りこぼします。スプレッドは列を自分のデータプロパティとしてコピーするので、__proto__ 列があっても安全です(Object.fromEntries(Object.entries(row)) も同じく安全。いずれも実測)。
期待値の側をわざわざ Object.create(null) で作る手もありますが、テストの読みやすさを考えると、取得側を普通のオブジェクトに写すほうが素直です。
node:sqlite の版・フラグ・警告
node:sqlite は長く実験的(Experimental)な組み込みモジュールで、Node 25.7.0 で release candidate(stability 1.2)に変わりました(Node 公式ドキュメントの Version history)。行が null プロトタイプで返る挙動は、実測した 22.13.0 / 22.23.1 / 24.13.0 / 25.9.0 / 26.5.0 のすべてで同じでした(.all() / .get() とも)。バージョンで変わるのは、フラグの要否と、実験的警告の有無です。
フラグの要否。 node:sqlite が加わったのは 22.5.0 で、当初は --experimental-sqlite の後ろにありました。22.13.0 / 23.4.0 でフラグが不要になっています(Node 公式ドキュメント)。境界の両側を実測すると、22.12.0 はフラグ必須(無しだと ERR_UNKNOWN_BUILTIN_MODULE)、22.13.0 はフラグ無しで読み込めます。
| Node | import "node:sqlite" |
|---|---|
| 22.5.0 〜 22.12.x / 23.0 〜 23.3.x | --experimental-sqlite が要る(無しだと ERR_UNKNOWN_BUILTIN_MODULE) |
| 22.13.0 以降 / 23.4.0 以降 / 24 以降 | フラグ無しで読み込める |
実験的警告。 node:sqlite を読み込むと(DatabaseSync を作らなくても、require / import の時点で)次の警告が stderr に出ます。終了コードには影響しません。
ExperimentalWarning: SQLite is an experimental feature and might change at any time
ただしこの警告は、実測した 22.5.0 / 22.13.0 / 22.23.1 / 24.13.0 では出ましたが、release candidate 昇格後の 25.9.0 / 26.5.0 では出ませんでした。25.7.0 以降でこのエラーに当たったなら、警告が無くても同じ null プロトタイプ由来の失敗です。
切り分け
- 構造差分が
[Object: null prototype]の 1 か所だけか — 構造差分がプロトタイプ表示だけで、actual:とexpected:のキーと値の表示が一致しているなら、原因はこれです。行を写せば通ります。値の差(score: 9とscore: 10など)も同時に出ているなら、行を写しても値の不一致は残ります——その場合はプロトタイプと値の両方がずれているので、先に値を合わせてください。 .get()の行を写すときは分岐を残す —.all()は必ず配列なので.map((row) => ({ ...row }))で安全ですが、.get()は行が無いとundefinedを返します。{ ...undefined }は例外にならず{}(truthy)になるので、const row = stmt.get(); const copied = row && { ...row };のように書きます(if (!row)の not-found 判定を壊さないため・実測)。.get()が返す行そのものは.all()と同じく null プロトタイプです。- BLOB 列で、行を写しても落ちる — BLOB は
Uint8Arrayで返ります。行を写せばUint8Arrayどうしは通りますが、期待値をBuffer.from(...)で書くと落ちます(Buffer.prototypeとUint8Array.prototypeが別物のため・実測)。期待値もnew Uint8Array([...])で書いてください。 - 自分で
Object.create(null)を作って比べても同じ — これはnode:sqlite固有ではなく、deepStrictEqualがプロトタイプを見ることによる挙動です。null プロトタイプのオブジェクトとリテラルは、値が同じでも厳密には等しくなりません。 assert.deepEqual(node:assert/strict以外)に替えたら通った — 非 strict のdeepEqualはプロトタイプを見ません(本ケースでは未実測、Node 公式ドキュメントの記述に基づきます)。ただしnode:assert/strictから取り込むとdeepEqualはdeepStrictEqualと同じ挙動になります(strict 版のエイリアス)。型の緩さを持ち込みたくないなら、比較方法を緩めるより行を写すほうが影響が局所的です。
検証環境
node:24.13.0-alpine、ネットワーク無し(:memory:DB と組み込みモジュールだけ)- 再現:
node:sqliteの行をnode:assert/strictのdeepStrictEqualでオブジェクトリテラルの配列と比較し、node --testが終了コード 1、AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equalが出る - 修正:同じテストで、比較の直前に各行を
.map((row) => ({ ...row }))で写すだけ。終了コード 0・シグネチャ消滅。差は 1 行だけです - null プロトタイプ挙動は 22.13.0 / 22.23.1 / 24.13.0 / 25.9.0 / 26.5.0 のすべてで同じ(
.all()/.get()ともObject.getPrototypeOf(row) === null)を、各イメージで実行して確認しました - フラグ境界:22.12.0 は
--experimental-sqlite必須(無しだとERR_UNKNOWN_BUILTIN_MODULE)、22.13.0 はフラグ無しで読み込める、を実測。Node 公式ドキュメントの「v22.13.0 / v23.4.0 でフラグ不要」と一致します - 実験的警告:
require("node:sqlite")の時点で(DatabaseSyncを作らずに)stderr に出ること、22.5.0 / 22.13.0 / 22.23.1 / 24.13.0 では出て 25.9.0 / 26.5.0 では出ないことを実測。Node 公式ドキュメントの「v25.7.0 で release candidate(stability 1.2)」に対応します - 写し方:
.get()の no-row({ ...undefined }→{}・truthy)、__proto__列でObject.assignが列を取りこぼしスプレッド/Object.fromEntriesは保持すること、BLOB はUint8Arrayで返り期待値をBuffer.from(...)で書くと落ちること({ toString: 'hi' }.toString === 'hi'の shadow 挙動も)を、node:24.13.0-alpineで確認しました - null プロトタイプが返る挙動と、非 strict
assert.deepEqualがプロトタイプ比較を外すことは Node 公式ドキュメントに基づく記述で、後者は本ケースでは実測していません