From 0eaf4a2c79044693afba6367fd22156b3e6de433 Mon Sep 17 00:00:00 2001 From: Noa Date: Fri, 19 Dec 2025 13:36:53 -0600 Subject: [PATCH] [TS] Implement and use point scan ABI --- .../bindings-typescript/src/server/runtime.ts | 165 ++++++++++++------ .../bindings-typescript/src/server/sys.d.ts | 12 ++ crates/core/src/host/v8/syscall/mod.rs | 1 + crates/core/src/host/v8/syscall/v1.rs | 49 ++++++ 4 files changed, 178 insertions(+), 49 deletions(-) diff --git a/crates/bindings-typescript/src/server/runtime.ts b/crates/bindings-typescript/src/server/runtime.ts index 70e501b5e8e..4b77fe36c82 100644 --- a/crates/bindings-typescript/src/server/runtime.ts +++ b/crates/bindings-typescript/src/server/runtime.ts @@ -1,5 +1,6 @@ import * as _syscalls1_0 from 'spacetime:sys@1.0'; import * as _syscalls1_2 from 'spacetime:sys@1.2'; +import * as _syscalls1_3 from 'spacetime:sys@1.3'; import type { ModuleHooks, u16, u32 } from 'spacetime:sys@1.0'; import { AlgebraicType, ProductType } from '../lib/algebraic_type'; @@ -48,7 +49,9 @@ import ViewResultHeader from '../lib/autogen/view_result_header_type'; const { freeze } = Object; -export const sys = freeze(wrapSyscalls(_syscalls1_0, _syscalls1_2)); +export const sys = freeze( + wrapSyscalls(_syscalls1_0, _syscalls1_2, _syscalls1_3) +); export function parseJsonObject(json: string): JsonObject { let value: unknown; @@ -220,10 +223,8 @@ export const hooks: ModuleHooks = { return writer.getBuffer(); }, __call_reducer__(reducerId, sender, connId, timestamp, argsBuf) { - const argsType = AlgebraicType.Product( - MODULE_DEF.reducers[reducerId].params - ); - const args = AlgebraicType.deserializeValue( + const argsType = MODULE_DEF.reducers[reducerId].params; + const args = ProductType.deserializeValue( new BinaryReader(argsBuf), argsType, MODULE_DEF.typespace @@ -499,8 +500,6 @@ function makeTableView( prefix: any[], prefix_elems: number ) => { - if (prefix_elems > numColumns - 1) - throw new TypeError('too many elements in prefix'); for (let i = 0; i < prefix_elems; i++) { const elemType = indexType.value.elements[i].algebraicType; AlgebraicType.serializeValue(writer, elemType, prefix[i], typespace); @@ -508,6 +507,23 @@ function makeTableView( return writer; }; + const serializePoint = (colVal: any[]): Uint8Array => { + const writer = new BinaryWriter(baseSize); + serializePrefix(writer, colVal, numColumns); + return writer.getBuffer(); + }; + + const singleElement = + numColumns === 1 ? indexType.value.elements[0].algebraicType : null; + + const serializeSinglePoint = + singleElement && + ((colVal: any): Uint8Array => { + const writer = new BinaryWriter(baseSize); + AlgebraicType.serializeValue(writer, singleElement, colVal, typespace); + return writer.getBuffer(); + }); + type IndexScanArgs = [ prefix: Uint8Array, prefix_elems: u16, @@ -516,33 +532,51 @@ function makeTableView( ]; let index: Index; - if (isUnique) { - const serializeBound = (colVal: any[]): IndexScanArgs => { - if (colVal.length !== numColumns) - throw new TypeError('wrong number of elements'); - - const writer = new BinaryWriter(baseSize + 1); - const prefix_elems = numColumns - 1; - serializePrefix(writer, colVal, prefix_elems); - const rstartOffset = writer.offset; - writer.writeU8(0); - AlgebraicType.serializeValue( - writer, - indexType.value.elements[numColumns - 1].algebraicType, - colVal[numColumns - 1], - typespace - ); - const buffer = writer.getBuffer(); - const prefix = buffer.slice(0, rstartOffset); - const rstart = buffer.slice(rstartOffset); - return [prefix, prefix_elems, rstart, rstart]; - }; + if (isUnique && serializeSinglePoint) { + index = { + find: (colVal: IndexVal): RowType | null => { + const point = serializeSinglePoint(colVal); + const iter = tableIterator( + sys.datastore_index_scan_point_bsatn(index_id, point), + rowType + ); + const { value, done } = iter.next(); + if (done) return null; + if (!iter.next().done) + throw new Error( + '`datastore_index_scan_range_bsatn` on unique field cannot return >1 rows' + ); + return value; + }, + delete: (colVal: IndexVal): boolean => { + const point = serializeSinglePoint(colVal); + const num = sys.datastore_delete_by_index_scan_point_bsatn( + index_id, + point + ); + return num > 0; + }, + update: (row: RowType): RowType => { + const writer = new BinaryWriter(baseSize); + AlgebraicType.serializeValue(writer, rowType, row, typespace); + const ret_buf = sys.datastore_update_bsatn( + table_id, + index_id, + writer.getBuffer() + ); + integrateGeneratedColumns?.(row, ret_buf); + return row; + }, + } as UniqueIndex; + } else if (isUnique) { index = { find: (colVal: IndexVal): RowType | null => { - if (numColumns === 1) colVal = [colVal]; - const args = serializeBound(colVal); + if (colVal.length !== numColumns) + throw new TypeError('wrong number of elements'); + + const point = serializePoint(colVal); const iter = tableIterator( - sys.datastore_index_scan_range_bsatn(index_id, ...args), + sys.datastore_index_scan_point_bsatn(index_id, point), rowType ); const { value, done } = iter.next(); @@ -554,11 +588,13 @@ function makeTableView( return value; }, delete: (colVal: IndexVal): boolean => { - if (numColumns === 1) colVal = [colVal]; - const args = serializeBound(colVal); - const num = sys.datastore_delete_by_index_scan_range_bsatn( + if (colVal.length !== numColumns) + throw new TypeError('wrong number of elements'); + + const point = serializePoint(colVal); + const num = sys.datastore_delete_by_index_scan_point_bsatn( index_id, - ...args + point ); return num > 0; }, @@ -574,6 +610,23 @@ function makeTableView( return row; }, } as UniqueIndex; + } else if (serializeSinglePoint) { + index = { + filter: (range: any): IteratorObject> => { + const point = serializeSinglePoint(range); + return tableIterator( + sys.datastore_index_scan_point_bsatn(index_id, point), + rowType + ); + }, + delete: (range: any): u32 => { + const point = serializeSinglePoint(range); + return sys.datastore_delete_by_index_scan_point_bsatn( + index_id, + point + ); + }, + } as RangedIndex; } else { const serializeRange = (range: any[]): IndexScanArgs => { if (range.length > numColumns) throw new TypeError('too many elements'); @@ -613,21 +666,35 @@ function makeTableView( return [prefix, prefix_elems, rstart, rend]; }; index = { - filter: (range: any): IteratorObject> => { - if (numColumns === 1) range = [range]; - const args = serializeRange(range); - return tableIterator( - sys.datastore_index_scan_range_bsatn(index_id, ...args), - rowType - ); + filter: (range: any[]): IteratorObject> => { + if (range.length === numColumns) { + const point = serializePoint(range); + return tableIterator( + sys.datastore_index_scan_point_bsatn(index_id, point), + rowType + ); + } else { + const args = serializeRange(range); + return tableIterator( + sys.datastore_index_scan_range_bsatn(index_id, ...args), + rowType + ); + } }, - delete: (range: any): u32 => { - if (numColumns === 1) range = [range]; - const args = serializeRange(range); - return sys.datastore_delete_by_index_scan_range_bsatn( - index_id, - ...args - ); + delete: (range: any[]): u32 => { + if (range.length === numColumns) { + const point = serializePoint(range); + return sys.datastore_delete_by_index_scan_point_bsatn( + index_id, + point + ); + } else { + const args = serializeRange(range); + return sys.datastore_delete_by_index_scan_range_bsatn( + index_id, + ...args + ); + } }, } as RangedIndex; } diff --git a/crates/bindings-typescript/src/server/sys.d.ts b/crates/bindings-typescript/src/server/sys.d.ts index 07c5c7ac609..f3480b5d186 100644 --- a/crates/bindings-typescript/src/server/sys.d.ts +++ b/crates/bindings-typescript/src/server/sys.d.ts @@ -100,3 +100,15 @@ declare module 'spacetime:sys@1.2' { export function procedure_abort_mut_tx(); } + +declare module 'spacetime:sys@1.3' { + export function datastore_index_scan_point_bsatn( + index_id: u32, + point: Uint8Array + ): u32; + + export function datastore_delete_by_index_scan_point_bsatn( + index_id: u32, + point: Uint8Array + ): u32; +} diff --git a/crates/core/src/host/v8/syscall/mod.rs b/crates/core/src/host/v8/syscall/mod.rs index 1719f3ffe66..ed7fa8f4cb2 100644 --- a/crates/core/src/host/v8/syscall/mod.rs +++ b/crates/core/src/host/v8/syscall/mod.rs @@ -57,6 +57,7 @@ fn resolve_sys_module_inner<'scope>( (1, 0) => Ok(v1::sys_v1_0(scope)), (1, 1) => Ok(v1::sys_v1_1(scope)), (1, 2) => Ok(v1::sys_v1_2(scope)), + (1, 3) => Ok(v1::sys_v1_3(scope)), _ => Err(TypeError(format!( "Could not import {spec:?}, likely because this module was built for a newer version of SpacetimeDB.\n\ It requires sys module v{major}.{minor}, but that version is not supported by the database." diff --git a/crates/core/src/host/v8/syscall/v1.rs b/crates/core/src/host/v8/syscall/v1.rs index 588fd16a86e..1022f452501 100644 --- a/crates/core/src/host/v8/syscall/v1.rs +++ b/crates/core/src/host/v8/syscall/v1.rs @@ -150,6 +150,23 @@ pub(super) fn sys_v1_2<'scope>(scope: &mut PinScope<'scope, '_>) -> Local<'scope ) } +pub(super) fn sys_v1_3<'scope>(scope: &mut PinScope<'scope, '_>) -> Local<'scope, Module> { + create_synthetic_module!( + scope, + "spacetime:sys@1.2", + ( + with_sys_result_ret, + AbiCall::DatastoreIndexScanPointBsatn, + datastore_index_scan_point_bsatn + ), + ( + with_sys_result_ret, + AbiCall::DatastoreDeleteByIndexScanPointBsatn, + datastore_delete_by_index_scan_point_bsatn + ), + ) +} + /// Registers a function in `module` /// where the function has `name` and does `body`. fn register_module_fun( @@ -1694,3 +1711,35 @@ fn procedure_commit_mut_tx(scope: &mut PinScope<'_, '_>, _args: FunctionCallback Ok(()) } + +fn datastore_index_scan_point_bsatn( + scope: &mut PinScope<'_, '_>, + args: FunctionCallbackArguments<'_>, +) -> SysCallResult { + let index_id: IndexId = deserialize_js(scope, args.get(0))?; + let point: &[u8] = deserialize_js(scope, args.get(1))?; + + let env = get_env(scope)?; + + // Find the relevant rows. + let chunks = env + .instance_env + .datastore_index_scan_point_bsatn_chunks(&mut env.chunk_pool, index_id, point)?; + + // Insert the encoded + concatenated rows into a new buffer and return its id. + Ok(env.iters.insert(chunks.into_iter()).0) +} + +fn datastore_delete_by_index_scan_point_bsatn( + scope: &mut PinScope<'_, '_>, + args: FunctionCallbackArguments<'_>, +) -> SysCallResult { + let index_id: IndexId = deserialize_js(scope, args.get(0))?; + let point: &[u8] = deserialize_js(scope, args.get(1))?; + + // Delete the relevant rows. + let count = get_env(scope)? + .instance_env + .datastore_delete_by_index_scan_point_bsatn(index_id, point)?; + Ok(count) +}