indexeddb: Add setName to IDBObjectStore and IDBIndex

This commit is contained in:
Karl Seguin committed 2026-09-04 11:51:15 +08:00
1 parent 1d66b2fbc8
commit 17983fb8f8
5 files changed
+146 -6

No files matched your search

+82 -1
View File
@@ -1189,7 +1189,6 @@
});
}
</script>
<<<<<<< HEAD
<!-- put()/add() structured-clone the value at call time: the record is the
clone as of the call (later mutation of the argument is not stored), a
@@ -1439,4 +1438,86 @@
await state.done((got) => testing.expectEqual(true, got));
}
</script>
<!-- Object store and index renames during an upgrade: names, name lists and
data follow; same-name is a no-op; collisions are a ConstraintError;
outside an upgrade it's an InvalidStateError. -->
<script id="rename_store_and_index" type=module>
{
const state = await testing.async();
const open = indexedDB.open("rename-db", 1);
open.onupgradeneeded = (e) => {
const db = e.target.result;
const books = db.createObjectStore("books", { keyPath: "isbn" });
books.createIndex("by_author", "author");
books.put({ isbn: 1, author: "Herbert" });
db.createObjectStore("other");
books.name = "renamed_books";
books.name = "renamed_books"; // same name: no-op
testing.expectError("ConstraintError", () => books.name = "other");
testing.expectEqual("renamed_books", books.name);
testing.expectEqual(["other", "renamed_books"], [...db.objectStoreNames]);
testing.expectEqual(books, e.target.transaction.objectStore("renamed_books"));
const index = books.index("by_author");
index.name = "by_writer";
testing.expectEqual("by_writer", index.name);
testing.expectEqual(["by_writer"], [...books.indexNames]);
testing.expectEqual(index, books.index("by_writer"));
};
open.onsuccess = () => {
const db = open.result;
const tx = db.transaction("renamed_books", "readwrite");
const store = tx.objectStore("renamed_books");
testing.expectError("InvalidStateError", () => store.name = "nope");
testing.expectError("InvalidStateError", () => store.index("by_writer").name = "nope");
const req = store.index("by_writer").get("Herbert");
req.onsuccess = () => state.resolve(req.result);
};
await state.done((got) => testing.expectEqual({ isbn: 1, author: "Herbert" }, got));
}
</script>
<!-- An aborted upgrade reverts renames on the handles the caller still holds. -->
<script id="rename_reverts_on_abort" type=module>
{
const state = await testing.async();
const setup = indexedDB.open("rename-abort-db", 1);
setup.onupgradeneeded = (e) => e.target.result.createObjectStore("books").createIndex("by_author", "author");
setup.onsuccess = () => {
setup.result.close();
const open = indexedDB.open("rename-abort-db", 2);
let store = null;
let index = null;
open.onupgradeneeded = (e) => {
const tx = e.target.transaction;
store = tx.objectStore("books");
index = store.index("by_author");
store.name = "renamed";
index.name = "renamed_index";
tx.abort();
};
open.onerror = (e) => {
e.preventDefault();
const reopen = indexedDB.open("rename-abort-db", 1);
reopen.onsuccess = () => {
const db = reopen.result;
state.resolve({
store: store.name,
index: index.name,
stores: [...db.objectStoreNames],
indexes: [...db.transaction("books").objectStore("books").indexNames],
});
};
};
};
await state.done((got) => {
testing.expectEqual("books", got.store);
testing.expectEqual("by_author", got.index);
testing.expectEqual(["books"], got.stores);
testing.expectEqual(["by_author"], got.indexes);
});
}
</script>
</body>
@@ -315,6 +315,15 @@ pub fn createObjectStore(
return (try self.objectStoreId(database_id, name)).?;
}
// A duplicate name surfaces as error.Constraint.
pub fn renameObjectStore(self: *Engine, object_store_id: i64, name: []const u8) !void {
try self.conn.exec("update idb_object_stores set name = ?2 where id = ?1", .{ object_store_id, name });
}
pub fn renameIndex(self: *Engine, index_id: i64, name: []const u8) !void {
try self.conn.exec("update idb_indexes set name = ?2 where id = ?1", .{ index_id, name });
}
pub fn deleteObjectStore(self: *Engine, database_id: i64, name: []const u8) !void {
// caller has a transaction open; cascade drops records, indexes and index
// records.
+24 -2
View File
@@ -16,11 +16,12 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig");
const idb = @import("idb.zig");
const Key = @import("Key.zig");
const Engine = @import("Engine.zig");
@@ -40,6 +41,7 @@ _store: *IDBObjectStore,
_engine: *Engine,
_index_id: i64,
_name: []const u8,
_original_name: ?[]const u8 = null, // needed for restore incase of abort
_key_path: Key.KeyPath,
_unique: bool,
_multi_entry: bool,
@@ -214,6 +216,26 @@ pub fn getName(self: *const IDBIndex) []const u8 {
return self._name;
}
pub fn setName(self: *IDBIndex, name: []const u8, _: *Execution) !void {
try self.assertLive();
const t = self._store._txn;
if (t._mode != .versionchange) {
return error.InvalidStateError;
}
try t.assertActive();
if (std.mem.eql(u8, name, self._name)) {
return;
}
self._engine.renameIndex(self._index_id, name) catch |err| switch (err) {
error.Constraint => return error.ConstraintError,
else => return err,
};
if (self._original_name == null) {
self._original_name = self._name;
}
self._name = try t.dupe(name);
}
pub fn getKeyPath(self: *IDBIndex, exec: *Execution) !js.Value {
return idb.cachedKeyPathJs(&self._key_path_js, self._store._txn, self._key_path, exec);
}
@@ -239,7 +261,7 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const name = bridge.accessor(IDBIndex.getName, null, .{});
pub const name = bridge.accessor(IDBIndex.getName, IDBIndex.setName, .{});
pub const keyPath = bridge.accessor(IDBIndex.getKeyPath, null, .{});
pub const unique = bridge.accessor(IDBIndex.getUnique, null, .{});
pub const multiEntry = bridge.accessor(IDBIndex.getMultiEntry, null, .{});
@@ -20,8 +20,8 @@ const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig");
const idb = @import("idb.zig");
const Key = @import("Key.zig");
const Engine = @import("Engine.zig");
@@ -42,6 +42,7 @@ const IDBObjectStore = @This();
_engine: *Engine,
_store_id: i64,
_name: []const u8,
_original_name: ?[]const u8 = null, // needed for restore incase of abort
_key_path: ?Key.KeyPath,
_auto_increment: bool,
_txn: *IDBTransaction,
@@ -275,6 +276,27 @@ pub fn getName(self: *const IDBObjectStore) []const u8 {
return self._name;
}
// Only during an upgrade.
pub fn setName(self: *IDBObjectStore, name: []const u8, _: *Execution) !void {
try self.assertLive();
const txn = self._txn;
if (txn._mode != .versionchange) {
return error.InvalidStateError;
}
try txn.assertActive();
if (std.mem.eql(u8, name, self._name)) {
return;
}
self._engine.renameObjectStore(self._store_id, name) catch |err| switch (err) {
error.Constraint => return error.ConstraintError,
else => return err,
};
if (self._original_name == null) {
self._original_name = self._name;
}
self._name = try txn.dupe(name);
}
pub fn getKeyPath(self: *IDBObjectStore, exec: *Execution) !js.Value {
return idb.cachedKeyPathJs(&self._key_path_js, self._txn, self._key_path, exec);
}
@@ -643,7 +665,7 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const name = bridge.accessor(IDBObjectStore.getName, null, .{});
pub const name = bridge.accessor(IDBObjectStore.getName, IDBObjectStore.setName, .{});
pub const keyPath = bridge.accessor(IDBObjectStore.getKeyPath, null, .{});
pub const autoIncrement = bridge.accessor(IDBObjectStore.getAutoIncrement, null, .{});
pub const transaction = bridge.accessor(IDBObjectStore.getTransaction, null, .{ .null_as_undefined = true });
@@ -293,15 +293,21 @@ pub fn abortWith(self: *IDBTransaction, exec: *Execution, reason: ?anyerror) err
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.
// it no longer exist, so handles the caller still holds must report
// deleted; pre-existing ones that were renamed get their names back (a
// created one has no earlier name to go back to and keeps its last).
if (self._mode == .versionchange) {
for (self._stores.items) |store| {
if (store._created) {
store._deleted = true;
} else if (store._original_name) |name| {
store._name = name;
}
for (store._indexes.items) |idx| {
if (idx._created) {
idx._deleted = true;
} else if (idx._original_name) |name| {
idx._name = name;
}
}
}