diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 6d35236dce0f82..c46ddaeb8f418b 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -2718,6 +2718,7 @@ BaseObjectPtr SQLTagStore::Create( .ToLocal(&obj)) { return nullptr; } + obj->SetInternalField(kDatabaseObject, database->object()); return MakeBaseObject(env, obj, std::move(database), capacity); } @@ -2728,9 +2729,8 @@ void SQLTagStore::CapacityGetter(const FunctionCallbackInfo& args) { } void SQLTagStore::DatabaseGetter(const FunctionCallbackInfo& args) { - SQLTagStore* store; - ASSIGN_OR_RETURN_UNWRAP(&store, args.This()); - args.GetReturnValue().Set(store->database_->object()); + args.GetReturnValue().Set( + args.This()->GetInternalField(kDatabaseObject).As()); } void SQLTagStore::SizeGetter(const FunctionCallbackInfo& args) { diff --git a/src/node_sqlite.h b/src/node_sqlite.h index 2641c9d4f1e8c5..f31af86aebc871 100644 --- a/src/node_sqlite.h +++ b/src/node_sqlite.h @@ -304,6 +304,11 @@ class Session : public BaseObject { class SQLTagStore : public BaseObject { public: + enum InternalFields { + kDatabaseObject = BaseObject::kInternalFieldCount, + kInternalFieldCount + }; + SQLTagStore(Environment* env, v8::Local object, BaseObjectWeakPtr database, diff --git a/test/parallel/test-sqlite-template-tag.js b/test/parallel/test-sqlite-template-tag.js index f640e70f8c399a..f9f1d9936c38b1 100644 --- a/test/parallel/test-sqlite-template-tag.js +++ b/test/parallel/test-sqlite-template-tag.js @@ -1,4 +1,6 @@ 'use strict'; +// Flags: --expose-gc + const { skipIfSQLiteMissing } = require('../common'); skipIfSQLiteMissing(); @@ -124,3 +126,35 @@ test('sql error messages are descriptive', () => { message: /no such table/i, }); }); + +test('a tag store keeps the database alive by itself', () => { + const sql = new DatabaseSync(':memory:').createTagStore(); + + sql.db.exec('CREATE TABLE test (data INTEGER)'); + + global.gc(); + + // eslint-disable-next-line no-unused-expressions + sql.run`INSERT INTO test (data) VALUES (1)`; +}); + +test('tag store prevents circular reference leaks', async () => { + const { gcUntil } = require('../common/gc'); + + const before = process.memoryUsage().heapUsed; + + // Create many SQLTagStore + DatabaseSync pairs with circular references + for (let i = 0; i < 1000; i++) { + const sql = new DatabaseSync(':memory:').createTagStore(); + sql.db.exec('CREATE TABLE test (data INTEGER)'); + // eslint-disable-next-line no-void + sql.db.setAuthorizer(() => void sql.db); + } + + // GC until memory stabilizes or give up after 20 attempts + await gcUntil('tag store leak check', () => { + const after = process.memoryUsage().heapUsed; + // Memory should not grow significantly (allow 50% margin for noise) + return after < before * 1.5; + }, 20); +});