Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
284 changes: 100 additions & 184 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,14 @@ members = [
"modules/sdk-test-connect-disconnect",
"modules/sdk-test-procedure",
"modules/sdk-test-view",
"modules/sdk-test-view-pk",
"modules/sdk-test-event-table",
"sdks/rust/tests/test-client",
"sdks/rust/tests/test-counter",
"sdks/rust/tests/connect_disconnect_client",
"sdks/rust/tests/procedure-client",
"sdks/rust/tests/view-client",
"sdks/rust/tests/view-pk-client",
"sdks/rust/tests/event-table-client",
"tools/ci",
"tools/upgrade-version",
Expand Down
18 changes: 15 additions & 3 deletions crates/bindings-csharp/Codegen/Module.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1119,12 +1119,15 @@ string makeConstraintFn
/// </summary>
record ViewDeclaration
{
private const string QueryViewReturnTag = "__query__";

public readonly string Name;
public readonly string? CanonicalName;
public readonly string FullName;
public readonly bool IsAnonymous;
public readonly bool IsPublic;
public readonly bool ReturnsQuery;
public readonly string? QueryRowTypeBSATNName;
public readonly TypeUse ReturnType;
public readonly EquatableArray<MemberDeclaration> Parameters;
public readonly Scope Scope;
Expand Down Expand Up @@ -1190,6 +1193,7 @@ method.ReturnType is INamedTypeSymbol
{
ReturnsQuery = true;
var rowType = TypeUse.Parse(method, queryRowType, diag);
QueryRowTypeBSATNName = rowType.BSATNName;
var optType = queryRowType.IsValueType
? "SpacetimeDB.BSATN.ValueOption"
: "SpacetimeDB.BSATN.RefOption";
Expand All @@ -1199,6 +1203,7 @@ method.ReturnType is INamedTypeSymbol
}
else
{
QueryRowTypeBSATNName = null;
ReturnType = TypeUse.Parse(method, method.ReturnType, diag);
}
Scope = new Scope(methodSyntax.Parent as MemberDeclarationSyntax);
Expand Down Expand Up @@ -1233,17 +1238,24 @@ method.ReturnType is INamedTypeSymbol
);
}

public string GenerateViewDef(uint Index) =>
$$$"""
public string GenerateViewDef(uint Index)
{
var returnTypeExpr =
ReturnsQuery && QueryRowTypeBSATNName is { } rowTypeBSATNName
? $"new global::SpacetimeDB.BSATN.AlgebraicType.Product([new global::SpacetimeDB.BSATN.AggregateElement(\"{QueryViewReturnTag}\", new {rowTypeBSATNName}().GetAlgebraicType(registrar))])"
: $"new {ReturnType.BSATNName}().GetAlgebraicType(registrar)";

return $$$"""
new global::SpacetimeDB.Internal.RawViewDefV10(
SourceName: "{{{Name}}}",
Index: {{{Index}}},
IsPublic: {{{IsPublic.ToString().ToLower()}}},
IsAnonymous: {{{IsAnonymous.ToString().ToLower()}}},
Params: [{{{MemberDeclaration.GenerateDefs(Parameters)}}}],
ReturnType: new {{{ReturnType.BSATNName}}}().GetAlgebraicType(registrar)
ReturnType: {{{returnTypeExpr}}}
);
""";
}

/// <summary>
/// Generates the class responsible for evaluating a view.
Expand Down
20 changes: 20 additions & 0 deletions crates/bindings-typescript/src/lib/type_builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1295,6 +1295,14 @@ export class ArrayBuilder<Element extends TypeBuilder<any, any>>
}
}

export const queryViewReturnMarker = Symbol('queryViewReturnMarker');

export class QueryBuilderViewReturnBuilder<
Row extends TypeBuilder<object, AlgebraicTypeVariants.Product>,
> extends ArrayBuilder<Row> {
readonly [queryViewReturnMarker] = true as const;
}

export class ByteArrayBuilder
extends TypeBuilder<
Uint8Array,
Expand Down Expand Up @@ -3871,6 +3879,18 @@ export const t = {
return new ArrayBuilder(e);
},

/**
* Declares that a view returns a query over rows of the given type.
*
* This emits a query-view return marker in the module definition while preserving
* list-valued runtime semantics for view execution.
*/
query<Row extends TypeBuilder<object, AlgebraicTypeVariants.Product>>(
rowType: Row
): QueryBuilderViewReturnBuilder<Row> {
return new QueryBuilderViewReturnBuilder(rowType);
},

enum: enumImpl,

/**
Expand Down
30 changes: 23 additions & 7 deletions crates/bindings-typescript/src/server/view.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,11 @@ const spacetime = schema({
personWithMissing,
});

const queryRetValue = t.query(person.rowType);
const arrayRetValue = t.array(person.rowType);
const optionalPerson = t.option(person.rowType);

spacetime.anonymousView({ name: 'v1', public: true }, arrayRetValue, ctx => {
spacetime.anonymousView({ name: 'v1', public: true }, queryRetValue, ctx => {
return ctx.from.person.build();
});

Expand Down Expand Up @@ -105,8 +106,23 @@ spacetime.anonymousView(
);

spacetime.anonymousView(
{ name: 'v2', public: true },
{ name: 'arrayProcedural', public: true },
arrayRetValue,
() => []
);

spacetime.anonymousView(
{ name: 'arrayCannotReturnQuery', public: true },
arrayRetValue,
// @ts-expect-error query-builder views must use t.query(...)
ctx => {
return ctx.from.person.build();
}
);

spacetime.anonymousView(
{ name: 'v2', public: true },
queryRetValue,
// @ts-expect-error returns a query of the wrong type.
ctx => {
return ctx.from.order.build();
Expand All @@ -116,7 +132,7 @@ spacetime.anonymousView(
// For queries, we can't return rows with extra fields.
spacetime.anonymousView(
{ name: 'v3', public: true },
arrayRetValue,
queryRetValue,
// @ts-expect-error returns a query of the wrong type.
ctx => {
return ctx.from.personWithExtra.build();
Expand All @@ -126,7 +142,7 @@ spacetime.anonymousView(
// Ideally this would fail, since we depend on the field ordering for serialization.
spacetime.anonymousView(
{ name: 'reorderedPerson', public: true },
arrayRetValue,
queryRetValue,
// Comment this out if we can fix the types.
// // @ts-expect-error returns a query of the wrong type.
ctx => {
Expand All @@ -137,21 +153,21 @@ spacetime.anonymousView(
// Fails because it is missing a field.
spacetime.anonymousView(
{ name: 'missingField', public: true },
arrayRetValue,
queryRetValue,
// @ts-expect-error returns a query of the wrong type.
ctx => {
return ctx.from.personWithMissing.build();
}
);

spacetime.anonymousView({ name: 'v4', public: true }, arrayRetValue, ctx => {
spacetime.anonymousView({ name: 'v4', public: true }, queryRetValue, ctx => {
// @ts-expect-error returns a query of the wrong type.
const _invalid = ctx.from.person.where(row => row.id.eq('string')).build();
const _columnEqs = ctx.from.person.where(row => row.id.eq(row.id)).build();
return ctx.from.person.where(row => row.id.eq(5)).build();
});

spacetime.anonymousView({ name: 'v5', public: true }, arrayRetValue, ctx => {
spacetime.anonymousView({ name: 'v5', public: true }, queryRetValue, ctx => {
const _nonIndexedSemijoin = ctx.from.person
.where(row => row.id.eq(5))
// @ts-expect-error person_id is not indexed.
Expand Down
52 changes: 41 additions & 11 deletions crates/bindings-typescript/src/server/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import type { OptionAlgebraicType } from '../lib/option';
import type { ParamsObj } from '../lib/reducers';
import { type UntypedSchemaDef } from '../lib/schema';
import {
QueryBuilderViewReturnBuilder,
RowBuilder,
type Infer,
type InferSpacetimeTypeOfTypeBuilder,
type InferTypeOfRow,
type TypeBuilder,
queryViewReturnMarker,
} from '../lib/type_builders';
import { bsatnBaseSize, toPascalCase } from '../lib/util';
import type { ReadonlyDbView } from './db_view';
Expand Down Expand Up @@ -99,25 +101,29 @@ export type ViewFn<
S extends UntypedSchemaDef,
Params extends ParamsObj,
Ret extends ViewReturnTypeBuilder,
> =
| ((ctx: ViewCtx<S>, params: InferTypeOfRow<Params>) => Infer<Ret>)
| ((
> = Ret extends QueryViewReturnTypeBuilder
? (
ctx: ViewCtx<S>,
params: InferTypeOfRow<Params>
) => RowTypedQuery<FlattenedArray<Infer<Ret>>, ExtractArrayProduct<Ret>>);
) => RowTypedQuery<FlattenedArray<Infer<Ret>>, ExtractArrayProduct<Ret>>
: (ctx: ViewCtx<S>, params: InferTypeOfRow<Params>) => Infer<Ret>;

export type AnonymousViewFn<
S extends UntypedSchemaDef,
Params extends ParamsObj,
Ret extends ViewReturnTypeBuilder,
> =
| ((ctx: AnonymousViewCtx<S>, params: InferTypeOfRow<Params>) => Infer<Ret>)
| ((
> = Ret extends QueryViewReturnTypeBuilder
? (
ctx: AnonymousViewCtx<S>,
params: InferTypeOfRow<Params>
) => RowTypedQuery<FlattenedArray<Infer<Ret>>, ExtractArrayProduct<Ret>>);
) => RowTypedQuery<FlattenedArray<Infer<Ret>>, ExtractArrayProduct<Ret>>
: (ctx: AnonymousViewCtx<S>, params: InferTypeOfRow<Params>) => Infer<Ret>;

export type ViewReturnTypeBuilder =
type QueryViewReturnTypeBuilder = QueryBuilderViewReturnBuilder<
TypeBuilder<object, AlgebraicTypeVariants.Product>
>;

type ProceduralViewReturnTypeBuilder =
| TypeBuilder<
readonly object[],
{ tag: 'Array'; value: AlgebraicTypeVariants.Product }
Expand All @@ -127,6 +133,10 @@ export type ViewReturnTypeBuilder =
OptionAlgebraicType<AlgebraicTypeVariants.Product>
>;

export type ViewReturnTypeBuilder =
| ProceduralViewReturnTypeBuilder
| QueryViewReturnTypeBuilder;

export function registerView<
S extends UntypedSchemaDef,
const Anonymous extends boolean,
Expand Down Expand Up @@ -154,13 +164,33 @@ export function registerView<
ctx.registerTypesRecursively(paramsBuilder)
);

const returnTypeDescriptor = returnType as AlgebraicType;
const isQueryBuilderReturn = (ret as any)[queryViewReturnMarker] === true;
const queryRowTypeIsProduct =
returnTypeDescriptor.tag === 'Array' &&
(returnTypeDescriptor.value.tag === 'Product' ||
(returnTypeDescriptor.value.tag === 'Ref' &&
typespace.types[returnTypeDescriptor.value.value]?.tag === 'Product'));

const moduleReturnType =
isQueryBuilderReturn && queryRowTypeIsProduct
? AlgebraicType.Product({
elements: [
{
name: '__query__',
algebraicType: returnTypeDescriptor.value,
},
],
})
: returnType;

ctx.moduleDef.views.push({
sourceName: exportName,
index: (anon ? ctx.anonViews : ctx.views).length,
isPublic: opts.public,
isAnonymous: anon,
params: paramType,
returnType,
returnType: moduleReturnType,
});

if (opts.name != null) {
Expand All @@ -186,7 +216,7 @@ export function registerView<
}

(anon ? ctx.anonViews : ctx.views).push({
fn,
fn: fn as any,
deserializeParams: ProductType.makeDeserializer(paramType, typespace),
serializeReturn: AlgebraicType.makeSerializer(returnType, typespace),
returnTypeBaseSize: bsatnBaseSize(typespace, returnType),
Expand Down
2 changes: 1 addition & 1 deletion crates/bindings-typescript/test-app/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ edition = "2024"
crate-type = ["cdylib"]

[dependencies]
spacetimedb = "1.2.0"
spacetimedb = { path = "../../../bindings" }
log = "0.4"
anyhow = "1.0"
47 changes: 41 additions & 6 deletions crates/bindings-typescript/test-app/server/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use spacetimedb::{reducer, table, Identity, ReducerContext, SpacetimeType, Table};
use spacetimedb::{reducer, table, view, Identity, Query, ReducerContext, SpacetimeType, Table, ViewContext};

#[table(name = player, public)]
#[table(accessor = player, public)]
pub struct Player {
#[primary_key]
#[auto_inc]
Expand All @@ -16,14 +16,14 @@ pub struct Point {
pub y: u16,
}

#[table(name = user, public)]
#[table(accessor = user, public)]
pub struct User {
#[primary_key]
pub identity: Identity,
pub username: String,
}

#[table(name = unindexed_player, public)]
#[table(accessor = unindexed_player, public)]
pub struct UnindexedPlayer {
#[primary_key]
#[auto_inc]
Expand All @@ -33,16 +33,51 @@ pub struct UnindexedPlayer {
location: Point,
}

#[table(accessor = view_pk_player, public)]
pub struct ViewPkPlayer {
#[primary_key]
id: u64,
name: String,
}

#[table(accessor = view_pk_membership, public)]
pub struct ViewPkMembership {
#[primary_key]
id: u64,
#[index(btree)]
player_id: u64,
}

#[reducer]
pub fn create_player(ctx: &ReducerContext, name: String, location: Point) {
ctx.db.user().insert(User {
identity: ctx.sender,
identity: ctx.sender(),
username: name.clone(),
});
ctx.db.player().insert(Player {
id: 0,
user_id: ctx.sender,
user_id: ctx.sender(),
name,
location,
});
}

#[reducer]
pub fn insert_view_pk_player(ctx: &ReducerContext, id: u64, name: String) {
ctx.db.view_pk_player().insert(ViewPkPlayer { id, name });
}

#[reducer]
pub fn update_view_pk_player(ctx: &ReducerContext, id: u64, name: String) {
ctx.db.view_pk_player().id().update(ViewPkPlayer { id, name });
}

#[reducer]
pub fn insert_view_pk_membership(ctx: &ReducerContext, id: u64, player_id: u64) {
ctx.db.view_pk_membership().insert(ViewPkMembership { id, player_id });
}

#[view(accessor = all_view_pk_players, public)]
pub fn all_view_pk_players(ctx: &ViewContext) -> impl Query<ViewPkPlayer> {
ctx.from.view_pk_player()
}
Loading
Loading