idb: add compound keys

This commit is contained in:
Karl Seguin
2026-07-03 18:30:54 +08:00
parent b1453f12a3
commit 2961264052
7 changed files with 261 additions and 54 deletions

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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) {