idb: on abort, rollback created stores/indexes

Also, better query key validation
This commit is contained in:
Karl Seguin
2026-07-03 19:09:57 +08:00
parent c50c66a21f
commit 4bebb2207d
7 changed files with 58 additions and 8 deletions

View File

@@ -8,7 +8,7 @@
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.4.9.tar.gz",
.hash = "v8-0.0.0-xddH63jnAgB8guO5jPzJD4pnX-CI5od-g4v-fIIjmQXr",
},
//.v8 = .{ .path = "../zig-v8-fork" },
// .v8 = .{ .path = "../zig-v8-fork" },
.brotli = .{
// v1.2.0
.url = "https://github.com/google/brotli/archive/028fb5a23661f123017c060daa546b55cf4bde29.tar.gz",

View File

@@ -105,6 +105,7 @@ pub fn createObjectStore(
const owned_name = try txn.dupe(name);
const store = try IDBObjectStore.init(txn, store_id, owned_name, key_path, opts.autoIncrement);
store._created = true;
try txn.cacheStore(store);
return store;
}

View File

@@ -44,6 +44,9 @@ _name: []const u8,
_key_path: Key.KeyPath,
_unique: bool,
_multi_entry: bool,
_deleted: bool = false,
// Created by this (versionchange) transaction — so an abort must delete it.
_created: bool = false,
// not just for efficiency, we must return the same v8::Array every time the
// compound key is accessed.
_key_path_js: ?*js.Value.BareGlobal = null,
@@ -70,7 +73,16 @@ pub fn releaseRef(self: *IDBIndex, page: *Page) void {
self._store._txn.releaseRef(page);
}
// The index (or its object store) may have been deleted — including by an
// aborted upgrade. That check precedes the transaction-active check per spec.
fn assertLive(self: *const IDBIndex) !void {
if (self._deleted or self._store._deleted) {
return error.InvalidStateError;
}
}
fn txn(self: *IDBIndex) !*IDBTransaction {
try self.assertLive();
const t = self._store._txn;
try t.assertActive();
return t;
@@ -78,7 +90,7 @@ fn txn(self: *IDBIndex) !*IDBTransaction {
pub fn get(self: *IDBIndex, query: js.Value, exec: *Execution) !*IDBRequest {
const t = try self.txn();
const bounds = try IDBKeyRange.resolveQuery(t._arena, query, exec);
const bounds = try IDBKeyRange.resolveKey(t._arena, query, exec);
const request = try t.newRequest();
return request.submit(.{ .index_get = .{ .index = self, .bounds = bounds } }, exec);
}
@@ -96,7 +108,7 @@ pub fn runGet(self: *IDBIndex, request: *IDBRequest, bounds: Engine.Bounds, exec
pub fn getKey(self: *IDBIndex, query: js.Value, exec: *Execution) !*IDBRequest {
const t = try self.txn();
const bounds = try IDBKeyRange.resolveQuery(t._arena, query, exec);
const bounds = try IDBKeyRange.resolveKey(t._arena, query, exec);
const request = try t.newRequest();
return request.submit(.{ .index_get_key = .{ .index = self, .bounds = bounds } }, exec);
}
@@ -188,11 +200,13 @@ pub fn runCount(self: *IDBIndex, request: *IDBRequest, bounds: Engine.Bounds, ex
}
pub fn openCursor(self: *IDBIndex, query: ?js.Value, direction: ?IDBCursor.Direction, exec: *Execution) !*IDBRequest {
try self.assertLive();
const bounds = try IDBKeyRange.resolveQuery(self._store._txn._arena, query, exec);
return IDBCursor.initIndex(self, bounds, direction orelse .next, false, exec);
}
pub fn openKeyCursor(self: *IDBIndex, query: ?js.Value, direction: ?IDBCursor.Direction, exec: *Execution) !*IDBRequest {
try self.assertLive();
const bounds = try IDBKeyRange.resolveQuery(self._store._txn._arena, query, exec);
return IDBCursor.initIndex(self, bounds, direction orelse .next, true, exec);
}

View File

@@ -123,9 +123,24 @@ pub fn toBounds(self: *const IDBKeyRange) Engine.Bounds {
};
}
// query/count/openCursor: a missing or null/undefined query means "all records".
pub fn resolveQuery(arena: Allocator, query: ?js.Value, exec: *Execution) !Engine.Bounds {
const q = query orelse return Engine.Bounds.unbounded();
return resolveQueryInner(arena, query, false, exec);
}
// get/getKey/delete: the query must be a valid key or key range. null/undefined
// is a DataError
pub fn resolveKey(arena: Allocator, query: ?js.Value, exec: *Execution) !Engine.Bounds {
return resolveQueryInner(arena, query, true, exec);
}
fn resolveQueryInner(arena: Allocator, query: ?js.Value, null_disallowed: bool, exec: *Execution) !Engine.Bounds {
const q = query orelse {
if (null_disallowed) return error.DataError;
return Engine.Bounds.unbounded();
};
if (q.isNullOrUndefined()) {
if (null_disallowed) return error.DataError;
return Engine.Bounds.unbounded();
}

View File

@@ -46,6 +46,8 @@ _key_path: ?Key.KeyPath,
_auto_increment: bool,
_txn: *IDBTransaction,
_deleted: bool = false,
// Created by this (versionchange) transaction — so an abort must delete it.
_created: bool = false,
// identity map, store.indexes('a') === store.index('a')
_indexes: std.ArrayList(*IDBIndex) = .empty,
// not just for efficiency, we must return the same v8::Array every time the
@@ -91,7 +93,7 @@ pub fn get(self: *IDBObjectStore, query: js.Value, exec: *Execution) !*IDBReques
try self.assertLive();
const txn = self._txn;
try txn.assertActive();
const bounds = try IDBKeyRange.resolveQuery(txn._arena, query, exec);
const bounds = try IDBKeyRange.resolveKey(txn._arena, query, exec);
const request = try txn.newRequest();
return request.submit(.{ .store_get = .{ .store = self, .bounds = bounds } }, exec);
}
@@ -114,7 +116,7 @@ pub fn delete(self: *IDBObjectStore, query: js.Value, exec: *Execution) !*IDBReq
return error.ReadOnlyError;
}
try txn.assertActive();
const bounds = try IDBKeyRange.resolveQuery(txn._arena, query, exec);
const bounds = try IDBKeyRange.resolveKey(txn._arena, query, exec);
const request = try txn.newRequest();
return request.submit(.{ .store_delete = .{ .store = self, .bounds = bounds } }, exec);
}
@@ -239,7 +241,7 @@ pub fn getKey(self: *IDBObjectStore, query: js.Value, exec: *Execution) !*IDBReq
try self.assertLive();
const txn = self._txn;
try txn.assertActive();
const bounds = try IDBKeyRange.resolveQuery(txn._arena, query, exec);
const bounds = try IDBKeyRange.resolveKey(txn._arena, query, exec);
const request = try txn.newRequest();
return request.submit(.{ .store_get_key = .{ .store = self, .bounds = bounds } }, exec);
}
@@ -529,6 +531,7 @@ pub fn createIndex(self: *IDBObjectStore, name: []const u8, key_path: Key.KeyPat
.unique = opts.unique,
.multi_entry = opts.multiEntry,
}, owned_name);
idb_index._created = true;
try self._engine.releaseSavepoint();
try self._indexes.append(txn._arena, idb_index);
return idb_index;
@@ -549,6 +552,8 @@ pub fn deleteIndex(self: *IDBObjectStore, name: []const u8, _: *Execution) !void
};
for (self._indexes.items, 0..) |idx, i| {
if (std.mem.eql(u8, idx._name, name)) {
// A handle the caller still holds must report itself deleted.
idx._deleted = true;
_ = self._indexes.swapRemove(i);
break;
}

View File

@@ -247,6 +247,21 @@ pub fn abortWith(self: *IDBTransaction, exec: *Execution, reason: ?anyerror) err
self._settled = true;
self._error = reason;
// An aborted upgrade reverts the schema: stores and indexes created during
// it no longer exist, so handles the caller still holds must report deleted.
if (self._mode == .versionchange) {
for (self._stores.items) |store| {
if (store._created) {
store._deleted = true;
}
for (store._indexes.items) |idx| {
if (idx._created) {
idx._deleted = true;
}
}
}
}
if (self._begun) {
self._engine.rollback();
self._begun = false;

View File

@@ -260,7 +260,7 @@ fn isIdentifier(s: []const u8) bool {
for (s[1..]) |c| {
const ok = switch (c) {
'a'...'z', 'A'...'Z', '_', '$', '0' ... '9' => true,
'a'...'z', 'A'...'Z', '_', '$', '0'...'9' => true,
else => c >= 0x80,
};
if (ok == false) {