mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-31 09:46:05 -04:00
idb: add compound keys
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const Key = @import("Key.zig");
|
||||
const Sqlite = @import("../../../../storage/sqlite/Sqlite.zig");
|
||||
|
||||
const log = lp.log;
|
||||
@@ -133,6 +134,7 @@ pub fn open(path: [:0]const u8) !Engine {
|
||||
\\ database_id integer not null references idb_databases(id) on delete cascade,
|
||||
\\ name text not null,
|
||||
\\ key_path text,
|
||||
\\ key_path_component_length integer not null default 0,
|
||||
\\ auto_increment integer not null default 0,
|
||||
\\ key_generator integer not null default 1,
|
||||
\\ unique(database_id, name)
|
||||
@@ -150,6 +152,7 @@ pub fn open(path: [:0]const u8) !Engine {
|
||||
\\ object_store_id integer not null references idb_object_stores(id) on delete cascade,
|
||||
\\ name text not null,
|
||||
\\ key_path text not null,
|
||||
\\ key_path_component_length integer not null default 0,
|
||||
\\ is_unique integer not null default 0,
|
||||
\\ multi_entry integer not null default 0,
|
||||
\\ unique(object_store_id, name)
|
||||
@@ -220,13 +223,13 @@ pub fn objectStoreId(self: *const Engine, database_id: i64, name: []const u8) !?
|
||||
|
||||
pub const StoreInfo = struct {
|
||||
id: i64,
|
||||
key_path: ?[]const u8,
|
||||
key_path: ?Key.KeyPath,
|
||||
auto_increment: bool,
|
||||
};
|
||||
|
||||
pub fn objectStoreInfo(self: *const Engine, arena: Allocator, database_id: i64, name: []const u8) !?StoreInfo {
|
||||
var row = (try self.conn.row(
|
||||
"select id, key_path, auto_increment from idb_object_stores where database_id = ?1 and name = ?2",
|
||||
"select id, key_path, key_path_component_length, auto_increment from idb_object_stores where database_id = ?1 and name = ?2",
|
||||
.{ database_id, name },
|
||||
)) orelse return null;
|
||||
defer row.deinit();
|
||||
@@ -234,8 +237,8 @@ pub fn objectStoreInfo(self: *const Engine, arena: Allocator, database_id: i64,
|
||||
const key_path = row.get(?[]const u8, 1);
|
||||
return .{
|
||||
.id = row.get(i64, 0),
|
||||
.key_path = if (key_path) |kp| try arena.dupe(u8, kp) else null,
|
||||
.auto_increment = row.get(bool, 2),
|
||||
.key_path = if (key_path) |kp| try Key.decodeKeyPathColumn(arena, kp, @intCast(row.get(i64, 2))) else null,
|
||||
.auto_increment = row.get(bool, 3),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -267,15 +270,23 @@ pub fn maybeBumpGenerator(self: *Engine, store_id: i64, key: f64) !void {
|
||||
|
||||
pub fn createObjectStore(
|
||||
self: *Engine,
|
||||
arena: Allocator,
|
||||
database_id: i64,
|
||||
name: []const u8,
|
||||
key_path: ?[]const u8,
|
||||
key_path: ?Key.KeyPath,
|
||||
auto_increment: bool,
|
||||
) !i64 {
|
||||
const column = if (key_path) |kp| try Key.encodeKeyPathColumn(arena, kp) else null;
|
||||
try self.conn.exec(
|
||||
\\ insert into idb_object_stores (database_id, name, key_path, auto_increment)
|
||||
\\ values (?1, ?2, ?3, ?4)
|
||||
, .{ database_id, name, key_path, auto_increment });
|
||||
\\ insert into idb_object_stores (database_id, name, key_path, key_path_component_length, auto_increment)
|
||||
\\ values (?1, ?2, ?3, ?4, ?5)
|
||||
, .{
|
||||
database_id,
|
||||
name,
|
||||
if (column) |c| c.text else null,
|
||||
if (column) |c| c.component_length else @as(usize, 0),
|
||||
auto_increment,
|
||||
});
|
||||
return (try self.objectStoreId(database_id, name)).?;
|
||||
}
|
||||
|
||||
@@ -320,18 +331,19 @@ pub fn clear(self: *Engine, object_store_id: i64) !void {
|
||||
|
||||
pub const IndexInfo = struct {
|
||||
id: i64,
|
||||
key_path: []const u8,
|
||||
key_path: Key.KeyPath,
|
||||
unique: bool,
|
||||
multi_entry: bool,
|
||||
};
|
||||
|
||||
pub fn createIndexRow(self: *Engine, object_store_id: i64, name: []const u8, key_path: []const u8, unique: bool, multi_entry: bool) !i64 {
|
||||
pub fn createIndexRow(self: *Engine, arena: Allocator, object_store_id: i64, name: []const u8, key_path: Key.KeyPath, unique: bool, multi_entry: bool) !i64 {
|
||||
const column = try Key.encodeKeyPathColumn(arena, key_path);
|
||||
return (try self.conn.scalar(
|
||||
i64,
|
||||
\\ insert into idb_indexes (object_store_id, name, key_path, is_unique, multi_entry)
|
||||
\\ values (?1, ?2, ?3, ?4, ?5) returning id
|
||||
\\ insert into idb_indexes (object_store_id, name, key_path, key_path_component_length, is_unique, multi_entry)
|
||||
\\ values (?1, ?2, ?3, ?4, ?5, ?6) returning id
|
||||
,
|
||||
.{ object_store_id, name, key_path, unique, multi_entry },
|
||||
.{ object_store_id, name, column.text, column.component_length, unique, multi_entry },
|
||||
)) orelse error.UnknownError;
|
||||
}
|
||||
|
||||
@@ -348,22 +360,22 @@ pub fn deleteIndexRow(self: *Engine, object_store_id: i64, name: []const u8) !vo
|
||||
|
||||
pub fn indexInfo(self: *const Engine, arena: Allocator, object_store_id: i64, name: []const u8) !?IndexInfo {
|
||||
var row = (try self.conn.row(
|
||||
"select id, key_path, is_unique, multi_entry from idb_indexes where object_store_id = ?1 and name = ?2",
|
||||
"select id, key_path, key_path_component_length, is_unique, multi_entry from idb_indexes where object_store_id = ?1 and name = ?2",
|
||||
.{ object_store_id, name },
|
||||
)) orelse return null;
|
||||
defer row.deinit();
|
||||
|
||||
return .{
|
||||
.id = row.get(i64, 0),
|
||||
.key_path = try arena.dupe(u8, row.get([]const u8, 1)),
|
||||
.unique = row.get(bool, 2),
|
||||
.multi_entry = row.get(bool, 3),
|
||||
.key_path = try Key.decodeKeyPathColumn(arena, row.get([]const u8, 1), @intCast(row.get(i64, 2))),
|
||||
.unique = row.get(bool, 3),
|
||||
.multi_entry = row.get(bool, 4),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn indexesForStore(self: *const Engine, arena: Allocator, object_store_id: i64) ![]IndexInfo {
|
||||
var rows = try self.conn.rows(
|
||||
"select id, key_path, is_unique, multi_entry from idb_indexes where object_store_id = ?1",
|
||||
"select id, key_path, key_path_component_length, is_unique, multi_entry from idb_indexes where object_store_id = ?1",
|
||||
.{object_store_id},
|
||||
);
|
||||
defer rows.deinit();
|
||||
@@ -372,9 +384,9 @@ pub fn indexesForStore(self: *const Engine, arena: Allocator, object_store_id: i
|
||||
while (try rows.next()) |row| {
|
||||
try list.append(arena, .{
|
||||
.id = row.get(i64, 0),
|
||||
.key_path = try arena.dupe(u8, row.get([]const u8, 1)),
|
||||
.unique = row.get(bool, 2),
|
||||
.multi_entry = row.get(bool, 3),
|
||||
.key_path = try Key.decodeKeyPathColumn(arena, row.get([]const u8, 1), @intCast(row.get(i64, 2))),
|
||||
.unique = row.get(bool, 3),
|
||||
.multi_entry = row.get(bool, 4),
|
||||
});
|
||||
}
|
||||
return list.items;
|
||||
@@ -769,7 +781,7 @@ test "IDB - Engine: ranged getAll/count honour open and closed bounds" {
|
||||
var engine = try Engine.open(":memory:");
|
||||
defer engine.close();
|
||||
const db_id = try engine.upsertDatabase("app", 1);
|
||||
const store_id = try engine.createObjectStore(db_id, "s", null, false);
|
||||
const store_id = try engine.createObjectStore(testing.allocator, db_id, "s", null, false);
|
||||
try seedNumbers(&engine, store_id, arena, &.{ 3, 1, 5, 2, 4 });
|
||||
|
||||
const k2 = try numKey(arena, 2);
|
||||
@@ -821,7 +833,7 @@ test "IDB - Engine: getRange/getKeyRange first-in-range and deleteRange" {
|
||||
var engine = try Engine.open(":memory:");
|
||||
defer engine.close();
|
||||
const db_id = try engine.upsertDatabase("app", 1);
|
||||
const store_id = try engine.createObjectStore(db_id, "s", null, false);
|
||||
const store_id = try engine.createObjectStore(testing.allocator, db_id, "s", null, false);
|
||||
try seedNumbers(&engine, store_id, arena, &.{ 1, 2, 3, 4, 5 });
|
||||
|
||||
const k2 = try numKey(arena, 2);
|
||||
@@ -909,7 +921,7 @@ test "IDB - Engine: database + object store + add/get round-trip" {
|
||||
const db_id = try engine.upsertDatabase("app", 1);
|
||||
try testing.expectEqual(1, (try engine.databaseVersion("app")).?);
|
||||
|
||||
const store_id = try engine.createObjectStore(db_id, "books", null, false);
|
||||
const store_id = try engine.createObjectStore(testing.allocator, db_id, "books", null, false);
|
||||
|
||||
// Value bytes deliberately include an embedded NUL and high bytes to prove
|
||||
// the BLOB path is binary-safe (serialized values are arbitrary bytes).
|
||||
@@ -928,7 +940,7 @@ test "IDB - Engine: add rejects duplicate key, put overwrites" {
|
||||
defer engine.close();
|
||||
|
||||
const db_id = try engine.upsertDatabase("app", 1);
|
||||
const store_id = try engine.createObjectStore(db_id, "s", null, false);
|
||||
const store_id = try engine.createObjectStore(testing.allocator, db_id, "s", null, false);
|
||||
|
||||
try engine.add(store_id, "k", "v1");
|
||||
try testing.expectError(error.Constraint, engine.add(store_id, "k", "v2"));
|
||||
@@ -944,8 +956,8 @@ test "IDB - Engine: createObjectStore rejects duplicate name" {
|
||||
defer engine.close();
|
||||
|
||||
const db_id = try engine.upsertDatabase("app", 1);
|
||||
_ = try engine.createObjectStore(db_id, "s", null, false);
|
||||
try testing.expectError(error.Constraint, engine.createObjectStore(db_id, "s", null, false));
|
||||
_ = try engine.createObjectStore(testing.allocator, db_id, "s", null, false);
|
||||
try testing.expectError(error.Constraint, engine.createObjectStore(testing.allocator, db_id, "s", null, false));
|
||||
}
|
||||
|
||||
test "IDB - Engine: rollback discards uncommitted writes" {
|
||||
@@ -953,7 +965,7 @@ test "IDB - Engine: rollback discards uncommitted writes" {
|
||||
defer engine.close();
|
||||
|
||||
const db_id = try engine.upsertDatabase("app", 1);
|
||||
const store_id = try engine.createObjectStore(db_id, "s", null, false);
|
||||
const store_id = try engine.createObjectStore(testing.allocator, db_id, "s", null, false);
|
||||
|
||||
try engine.begin();
|
||||
try engine.add(store_id, "k", "v");
|
||||
@@ -962,8 +974,6 @@ test "IDB - Engine: rollback discards uncommitted writes" {
|
||||
try testing.expectEqual(null, try engine.get(testing.allocator, store_id, "k"));
|
||||
}
|
||||
|
||||
const Key = @import("Key.zig");
|
||||
|
||||
// Seed a store with numeric keys n and values "v<n>", inserted out of order so
|
||||
// the range tests prove ORDER BY rather than insertion order.
|
||||
fn seedNumbers(engine: *Engine, store_id: i64, arena: Allocator, ns: []const u8) !void {
|
||||
|
||||
@@ -245,7 +245,7 @@ pub fn update(self: *IDBCursor, value: js.Value, exec: *Execution) !*IDBRequest
|
||||
|
||||
// For an in-line store, the value's own key must match the record's key.
|
||||
if (self._store._key_path) |kp| {
|
||||
const extracted = Key.evaluatePath(value, kp) orelse return error.DataError;
|
||||
const extracted = (try Key.extractKeyPath(exec.js.local.?, value, kp)) orelse return error.DataError;
|
||||
const encoded = try Key.encodeValue(exec.call_arena, extracted);
|
||||
if (!std.mem.eql(u8, encoded, current_key)) {
|
||||
return error.DataError;
|
||||
|
||||
@@ -24,6 +24,7 @@ const js = @import("../../../js/js.zig");
|
||||
const EventTarget = @import("../../EventTarget.zig");
|
||||
|
||||
const idb = @import("idb.zig");
|
||||
const Key = @import("Key.zig");
|
||||
const Engine = @import("Engine.zig");
|
||||
const IDBTransaction = @import("IDBTransaction.zig");
|
||||
const IDBObjectStore = @import("IDBObjectStore.zig");
|
||||
@@ -61,7 +62,7 @@ pub fn asEventTarget(self: *IDBDatabase) *EventTarget {
|
||||
}
|
||||
|
||||
const CreateObjectStoreOptions = struct {
|
||||
keyPath: ?[]const u8 = null,
|
||||
keyPath: ?Key.KeyPath = null,
|
||||
autoIncrement: bool = false,
|
||||
};
|
||||
|
||||
@@ -78,10 +79,24 @@ pub fn createObjectStore(
|
||||
try txn.assertActive();
|
||||
|
||||
const opts = options orelse CreateObjectStoreOptions{};
|
||||
|
||||
// Validate + copy the key path onto the transaction arena so it outlives the
|
||||
// call. autoIncrement is incompatible with an empty or compound key path.
|
||||
const key_path: ?Key.KeyPath = if (opts.keyPath) |kp| blk: {
|
||||
if (Key.isValidKeyPathSpec(kp) == false) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
if (opts.autoIncrement and keyPathBlocksAutoIncrement(kp)) {
|
||||
return error.InvalidAccessError;
|
||||
}
|
||||
break :blk try Key.dupeKeyPath(txn._arena, kp);
|
||||
} else null;
|
||||
|
||||
const store_id = self._engine.createObjectStore(
|
||||
txn._arena,
|
||||
self._database_id,
|
||||
name,
|
||||
opts.keyPath,
|
||||
key_path,
|
||||
opts.autoIncrement,
|
||||
) catch |err| switch (err) {
|
||||
error.Constraint => return error.ConstraintError,
|
||||
@@ -89,12 +104,20 @@ pub fn createObjectStore(
|
||||
};
|
||||
|
||||
const owned_name = try txn.dupe(name);
|
||||
const key_path = if (opts.keyPath) |kp| try txn.dupe(kp) else null;
|
||||
const store = try IDBObjectStore.init(txn, store_id, owned_name, key_path, opts.autoIncrement);
|
||||
try txn.cacheStore(store);
|
||||
return store;
|
||||
}
|
||||
|
||||
// autoIncrement requires an out-of-line or single-property in-line key; an empty
|
||||
// or compound key path can't carry a generated key.
|
||||
fn keyPathBlocksAutoIncrement(kp: Key.KeyPath) bool {
|
||||
return switch (kp) {
|
||||
.string => |s| s.len == 0,
|
||||
.list => true,
|
||||
};
|
||||
}
|
||||
|
||||
// Only callable while the upgrade transaction is live and active, hence the checks
|
||||
pub fn deleteObjectStore(self: *IDBDatabase, name: []const u8, _: *Execution) !void {
|
||||
const txn = self._txn orelse return error.InvalidStateError;
|
||||
|
||||
@@ -22,6 +22,7 @@ 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");
|
||||
const IDBCursor = @import("IDBCursor.zig");
|
||||
@@ -40,9 +41,12 @@ _store: *IDBObjectStore,
|
||||
_engine: *Engine,
|
||||
_index_id: i64,
|
||||
_name: []const u8,
|
||||
_key_path: []const u8,
|
||||
_key_path: Key.KeyPath,
|
||||
_unique: bool,
|
||||
_multi_entry: bool,
|
||||
// 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,
|
||||
|
||||
pub fn init(obj_store: *IDBObjectStore, info: Engine.IndexInfo, name: []const u8) !*IDBIndex {
|
||||
const self = try obj_store._txn._arena.create(IDBIndex);
|
||||
@@ -197,8 +201,8 @@ pub fn getName(self: *const IDBIndex) []const u8 {
|
||||
return self._name;
|
||||
}
|
||||
|
||||
pub fn getKeyPath(self: *const IDBIndex) []const u8 {
|
||||
return self._key_path;
|
||||
pub fn getKeyPath(self: *IDBIndex, exec: *Execution) !js.Value {
|
||||
return idb.cachedKeyPathJs(&self._key_path_js, self._store._txn, self._key_path, exec);
|
||||
}
|
||||
|
||||
pub fn getUnique(self: *const IDBIndex) bool {
|
||||
|
||||
@@ -22,6 +22,7 @@ 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");
|
||||
const IDBIndex = @import("IDBIndex.zig");
|
||||
@@ -41,18 +42,21 @@ const IDBObjectStore = @This();
|
||||
_engine: *Engine,
|
||||
_store_id: i64,
|
||||
_name: []const u8,
|
||||
_key_path: ?[]const u8,
|
||||
_key_path: ?Key.KeyPath,
|
||||
_auto_increment: bool,
|
||||
_txn: *IDBTransaction,
|
||||
_deleted: 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
|
||||
// compound key is accessed.
|
||||
_key_path_js: ?*js.Value.BareGlobal = null,
|
||||
|
||||
pub fn init(
|
||||
txn: *IDBTransaction,
|
||||
store_id: i64,
|
||||
name: []const u8,
|
||||
key_path: ?[]const u8,
|
||||
key_path: ?Key.KeyPath,
|
||||
auto_increment: bool,
|
||||
) !*IDBObjectStore {
|
||||
const self = try txn._arena.create(IDBObjectStore);
|
||||
@@ -267,8 +271,8 @@ pub fn getName(self: *const IDBObjectStore) []const u8 {
|
||||
return self._name;
|
||||
}
|
||||
|
||||
pub fn getKeyPath(self: *const IDBObjectStore) ?[]const u8 {
|
||||
return self._key_path;
|
||||
pub fn getKeyPath(self: *IDBObjectStore, exec: *Execution) !js.Value {
|
||||
return idb.cachedKeyPathJs(&self._key_path_js, self._txn, self._key_path, exec);
|
||||
}
|
||||
|
||||
pub fn getAutoIncrement(self: *const IDBObjectStore) bool {
|
||||
@@ -311,7 +315,7 @@ fn write(self: *IDBObjectStore, value: js.Value, key_arg: ?js.Value, kind: Write
|
||||
// can't have an explicit key if we're configured for in-line keys
|
||||
return error.DataError;
|
||||
}
|
||||
if (Key.evaluatePath(value, kp)) |extracted| {
|
||||
if (try Key.extractKeyPath(exec.js.local.?, value, kp)) |extracted| {
|
||||
break :blk .{ .explicit = .{
|
||||
.encoded = try Key.encodeValue(txn._arena, extracted),
|
||||
.bump = if (self._auto_increment and extracted.isNumber()) try extracted.toF64() else null,
|
||||
@@ -321,7 +325,9 @@ fn write(self: *IDBObjectStore, value: js.Value, key_arg: ?js.Value, kind: Write
|
||||
if (self._auto_increment == false) {
|
||||
return error.DataError;
|
||||
}
|
||||
if (Key.canInjectKey(value, kp) == false) {
|
||||
// A compound or empty key path can't carry a generated key, so
|
||||
// auto_increment implies a non-empty single-property path here.
|
||||
if (Key.canInjectKey(value, kp.string) == false) {
|
||||
return error.DataError;
|
||||
}
|
||||
break :blk .generate_in_line;
|
||||
@@ -382,7 +388,7 @@ fn writeInner(self: *IDBObjectStore, request: *IDBRequest, kind: WriteKind, valu
|
||||
.generate_in_line => blk: {
|
||||
const n = try self._engine.nextGeneratedKey(self._store_id);
|
||||
const k = try local.newNumber(@floatFromInt(n));
|
||||
try Key.injectKey(local, value, self._key_path.?, k);
|
||||
try Key.injectKey(local, value, self._key_path.?.string, k);
|
||||
break :blk try Key.encodeValue(exec.call_arena, k);
|
||||
},
|
||||
};
|
||||
@@ -439,12 +445,12 @@ fn reindex(self: *IDBObjectStore, value: js.Value, primary_key: []const u8, exec
|
||||
var seen: std.ArrayList([]const u8) = .empty;
|
||||
try self._engine.deleteIndexRecordsForKey(self._store_id, primary_key);
|
||||
for (indexes) |idx| {
|
||||
try self.addIndexEntries(arena, &seen, idx.id, idx.unique, idx.multi_entry, idx.key_path, value, primary_key);
|
||||
try self.addIndexEntries(exec.js.local.?, arena, &seen, idx.id, idx.unique, idx.multi_entry, idx.key_path, value, primary_key);
|
||||
}
|
||||
}
|
||||
|
||||
fn addIndexEntries(self: *IDBObjectStore, arena: Allocator, seen: *std.ArrayList([]const u8), index_id: i64, unique: bool, multi_entry: bool, key_path: []const u8, value: js.Value, primary_key: []const u8) !void {
|
||||
const extracted = Key.evaluatePath(value, key_path) orelse return;
|
||||
fn addIndexEntries(self: *IDBObjectStore, local: *const js.Local, arena: Allocator, seen: *std.ArrayList([]const u8), index_id: i64, unique: bool, multi_entry: bool, key_path: Key.KeyPath, value: js.Value, primary_key: []const u8) !void {
|
||||
const extracted = (try Key.extractKeyPath(local, value, key_path)) orelse return;
|
||||
if (multi_entry and extracted.isArray()) {
|
||||
seen.clearRetainingCapacity();
|
||||
|
||||
@@ -473,7 +479,7 @@ const CreateIndexOptions = struct {
|
||||
};
|
||||
|
||||
// Only callable during an upgrade (versionchange transaction).
|
||||
pub fn createIndex(self: *IDBObjectStore, name: []const u8, key_path: []const u8, options: ?CreateIndexOptions, exec: *Execution) !*IDBIndex {
|
||||
pub fn createIndex(self: *IDBObjectStore, name: []const u8, key_path: Key.KeyPath, options: ?CreateIndexOptions, exec: *Execution) !*IDBIndex {
|
||||
try self.assertLive();
|
||||
const txn = self._txn;
|
||||
if (txn._mode != .versionchange) {
|
||||
@@ -481,12 +487,21 @@ pub fn createIndex(self: *IDBObjectStore, name: []const u8, key_path: []const u8
|
||||
}
|
||||
// Spec order: the transaction-state check precedes the index-name check.
|
||||
try txn.assertActive();
|
||||
if (Key.isValidKeyPathSpec(key_path) == false) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
const opts = options orelse CreateIndexOptions{};
|
||||
// multiEntry is meaningless for a compound key path.
|
||||
if (opts.multiEntry and std.meta.activeTag(key_path) == .list) {
|
||||
return error.InvalidAccessError;
|
||||
}
|
||||
|
||||
const owned_key_path = try Key.dupeKeyPath(txn._arena, key_path);
|
||||
|
||||
try self._engine.savepoint();
|
||||
errdefer self._engine.rollbackSavepoint();
|
||||
|
||||
const index_id = self._engine.createIndexRow(self._store_id, name, key_path, opts.unique, opts.multiEntry) catch |err| switch (err) {
|
||||
const index_id = self._engine.createIndexRow(txn._arena, self._store_id, name, owned_key_path, opts.unique, opts.multiEntry) catch |err| switch (err) {
|
||||
error.Constraint => return error.ConstraintError, // duplicate index name
|
||||
else => return err,
|
||||
};
|
||||
@@ -503,12 +518,11 @@ pub fn createIndex(self: *IDBObjectStore, name: []const u8, key_path: []const u8
|
||||
var seen: std.ArrayList([]const u8) = .empty;
|
||||
while (try rows.next()) |row| {
|
||||
const value = try js.Value.deserialize(local, row.get([]const u8, 1));
|
||||
try self.addIndexEntries(arena, &seen, index_id, opts.unique, opts.multiEntry, key_path, value, row.get([]const u8, 0));
|
||||
try self.addIndexEntries(local, arena, &seen, index_id, opts.unique, opts.multiEntry, owned_key_path, value, row.get([]const u8, 0));
|
||||
}
|
||||
}
|
||||
|
||||
const owned_name = try txn.dupe(name);
|
||||
const owned_key_path = try txn.dupe(key_path);
|
||||
const idb_index = try IDBIndex.init(self, .{
|
||||
.id = index_id,
|
||||
.key_path = owned_key_path,
|
||||
|
||||
@@ -44,6 +44,14 @@ pub const Value = union(enum) {
|
||||
array: []const Value,
|
||||
};
|
||||
|
||||
// A key path is either a single string path or, for a compound key, a list of
|
||||
// string paths. An out-of-line store has no key path at all (represented as an
|
||||
// optional KeyPath by its holders).
|
||||
pub const KeyPath = union(enum) {
|
||||
string: []const u8,
|
||||
list: []const []const u8,
|
||||
};
|
||||
|
||||
pub fn number(n: f64) Key {
|
||||
return .{ .value = .{ .number = n } };
|
||||
}
|
||||
@@ -159,8 +167,8 @@ fn decodeBytes(allocator: Allocator, bytes: []const u8, pos: *usize) ![]u8 {
|
||||
}
|
||||
|
||||
// Build a Key.Value from a JS value, validating that it is a structurally valid
|
||||
// IDB key. Invalid keys (booleans, null/undefined, objects, NaN, ±Inf, and —
|
||||
// until the binding supports it — Date) produce error.DataError.
|
||||
// IDB key. Invalid keys (booleans, null/undefined, plain objects, NaN, and
|
||||
// invalid Dates) produce error.DataError.
|
||||
pub fn fromJs(value: js.Value, allocator: Allocator) !Value {
|
||||
return fromJsDepth(value, allocator, 0);
|
||||
}
|
||||
@@ -173,7 +181,8 @@ fn fromJsDepth(value: js.Value, allocator: Allocator, depth: usize) !Value {
|
||||
}
|
||||
if (value.isNumber()) {
|
||||
var n = try value.toF64();
|
||||
if (std.math.isNan(n) or std.math.isInf(n)) return error.DataError;
|
||||
// NaN is not a valid key; ±Infinity is (it sorts at the numeric extremes).
|
||||
if (std.math.isNan(n)) return error.DataError;
|
||||
if (n == 0) n = 0; // normalize -0 to +0
|
||||
return .{ .number = n };
|
||||
}
|
||||
@@ -219,6 +228,134 @@ pub fn toJs(value: Value, local: *const Local) !js.Value {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn isValidKeyPath(path: []const u8) bool {
|
||||
if (path.len == 0) {
|
||||
// empty is valid
|
||||
return true;
|
||||
}
|
||||
|
||||
// if not empty, must be one or more "identifiers" split by '.'
|
||||
var it = std.mem.splitScalar(u8, path, '.');
|
||||
while (it.next()) |component| {
|
||||
if (isIdentifier(component) == false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
fn isIdentifier(s: []const u8) bool {
|
||||
if (s.len == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// leading 0-9 not allowed
|
||||
const first_ok = switch (s[0]) {
|
||||
'a'...'z', 'A'...'Z', '_', '$' => true,
|
||||
else => |c| c >= 0x80,
|
||||
};
|
||||
if (first_ok == false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (s[1..]) |c| {
|
||||
const ok = switch (c) {
|
||||
'a'...'z', 'A'...'Z', '_', '$', '0' ... '9' => true,
|
||||
else => c >= 0x80,
|
||||
};
|
||||
if (ok == false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validate the spec's (DOMString or sequence<DOMString>) key path form. A
|
||||
// compound path must be a non-empty list of valid, non-empty string paths.
|
||||
pub fn isValidKeyPathSpec(kp: KeyPath) bool {
|
||||
switch (kp) {
|
||||
.string => |s| return isValidKeyPath(s),
|
||||
.list => |items| {
|
||||
if (items.len == 0) {
|
||||
return false;
|
||||
}
|
||||
for (items) |item| {
|
||||
if (item.len == 0 or isValidKeyPath(item) == false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dupeKeyPath(arena: Allocator, kp: KeyPath) !KeyPath {
|
||||
switch (kp) {
|
||||
.string => |s| return .{ .string = try arena.dupe(u8, s) },
|
||||
.list => |items| {
|
||||
const copy = try arena.alloc([]const u8, items.len);
|
||||
for (items, 0..) |item, i| {
|
||||
copy[i] = try arena.dupe(u8, item);
|
||||
}
|
||||
return .{ .list = copy };
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extractKeyPath(local: *const Local, value: js.Value, kp: KeyPath) !?js.Value {
|
||||
switch (kp) {
|
||||
.string => |s| return evaluatePath(value, s),
|
||||
.list => |items| {
|
||||
const arr = local.newArray(@intCast(items.len));
|
||||
for (items, 0..) |item, i| {
|
||||
const component = evaluatePath(value, item) orelse return null;
|
||||
_ = try arr.set(@intCast(i), component, .{});
|
||||
}
|
||||
return arr.toValue();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn keyPathToJs(local: *const Local, kp: ?KeyPath) !js.Value {
|
||||
const path = kp orelse return .{ .local = local, .handle = local.isolate.initNull() };
|
||||
switch (path) {
|
||||
.string => |s| return local.newString(s).toValue(),
|
||||
.list => |items| {
|
||||
const arr = local.newArray(@intCast(items.len));
|
||||
for (items, 0..) |item, i| {
|
||||
_ = try arr.set(@intCast(i), local.newString(item).toValue(), .{});
|
||||
}
|
||||
return arr.toValue();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Storage form of a key path: the text column value and its component count. A
|
||||
// count of 0 denotes a single string path; a positive count is a compound path
|
||||
// of that many ','-joined components (a valid component can't contain a ',').
|
||||
// The count both disambiguates a one-element list from a string and lets decode
|
||||
// allocate the exact slice.
|
||||
pub const ColumnKeyPath = struct { text: []const u8, component_length: usize };
|
||||
|
||||
pub fn encodeKeyPathColumn(arena: Allocator, kp: KeyPath) !ColumnKeyPath {
|
||||
return switch (kp) {
|
||||
.string => |s| .{ .text = s, .component_length = 0 },
|
||||
.list => |items| .{ .text = try std.mem.join(arena, ",", items), .component_length = items.len },
|
||||
};
|
||||
}
|
||||
|
||||
pub fn decodeKeyPathColumn(arena: Allocator, text: []const u8, component_length: usize) !KeyPath {
|
||||
if (component_length == 0) {
|
||||
return .{ .string = try arena.dupe(u8, text) };
|
||||
}
|
||||
const items = try arena.alloc([]const u8, component_length);
|
||||
var it = std.mem.splitScalar(u8, text, ',');
|
||||
for (items) |*item| {
|
||||
item.* = try arena.dupe(u8, it.next() orelse return error.InvalidKeyEncoding);
|
||||
}
|
||||
return .{ .list = items };
|
||||
}
|
||||
|
||||
// Given a key path , "manager.id", extract that from an object
|
||||
pub fn evaluatePath(value: js.Value, key_path: []const u8) ?js.Value {
|
||||
if (key_path.len == 0) {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
// 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 js = @import("../../../js/js.zig");
|
||||
|
||||
pub const Key = @import("Key.zig");
|
||||
@@ -50,6 +51,24 @@ pub fn registerTypes() []const type {
|
||||
};
|
||||
}
|
||||
|
||||
// the keyPath attribute need identity equality. For a compound key (an array)
|
||||
// we have to return the same v8::rray on every reach. So users of KeyPath will
|
||||
// cache it locally, and store it in the Transaction's globals list for cleanup.
|
||||
pub fn cachedKeyPathJs(cache: *?*js.Value.BareGlobal, txn: *IDBTransaction, kp: ?Key.KeyPath, exec: *js.Execution) !js.Value {
|
||||
const local = exec.js.local.?;
|
||||
if (kp) |path| {
|
||||
if (std.meta.activeTag(path) == .list) {
|
||||
if (cache.*) |slot| {
|
||||
return slot.local(local);
|
||||
}
|
||||
const value = try Key.keyPathToJs(local, path);
|
||||
cache.* = try txn.persist(value);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return Key.keyPathToJs(local, kp);
|
||||
}
|
||||
|
||||
// An on* event-handler attribute setter. The bridge can hand the setter either
|
||||
// a function (store it) or any other value (clears it).
|
||||
pub const FunctionSetter = union(enum) {
|
||||
|
||||
Reference in New Issue
Block a user