Skip to content

Queries

A query selects entities from one or more sources and keeps its results current as the data changes, so an app reads data the same way whether it was written here or by another peer.

A filter is a predicate: it says which entities match, by type, field values, text or time. A query is the shape around filters: which entities to start from, which references or relations to follow, and how to order, limit and group the results. Learn to query builds queries for the contacts app.

import {
import Filter
Filter
,
import Order
Order
,
import Query
Query
} from '@dxos/echo';
import {
class Organization
Organization
,
class Person
Person
} from './schema.ts';
// A filter matches entities.
const
const adas: Filter.Filter<Person & OfKind<EntityKind.Object>>
adas
=
import Filter
Filter
.
const type: <typeof Person>(type: typeof Person, props?: Filter.Props<Person & OfKind<EntityKind.Object>> | undefined) => Filter.Filter<Person & OfKind<EntityKind.Object>> (+4 overloads)
type
(
class Person
Person
, {
name?: string | Filter.Filter<string> | undefined
name
: 'Ada' });
// A query is the shape around filters: what to select, follow and order.
const
const employers: Query.Query<Organization & OfKind<EntityKind.Object> & Unknown>
employers
=
import Query
Query
.
const select: <Filter.Filter<Person & OfKind<EntityKind.Object>>>(filter: Filter.Filter<Person & OfKind<EntityKind.Object>>) => Query.Query<Person & OfKind<EntityKind.Object>>

Select objects based on a filter.

@param ― filter - Filter to select the objects.

@returns ― Query for the selected objects.

@performance ― O(1); wraps the current AST in a new immutable node without executing anything.

select
(
const adas: Filter.Filter<Person & OfKind<EntityKind.Object>>
adas
).
Query<Person & OfKind<EntityKind.Object>>.reference<"employer">(key: "employer"): Query.Query<Organization & OfKind<EntityKind.Object> & Unknown>

Traverse an outgoing reference.

@param ― key - Property path inside T that is a reference or optional reference.

@returns ― Query for the target of the reference.

@performance ― O(1); wraps the current AST in a new immutable node without executing anything.

reference
('employer');
const
const staff: Query.Query<Person & OfKind<EntityKind.Object>>
staff
=
import Query
Query
.
const select: <Filter.Filter<Organization & OfKind<EntityKind.Object>>>(filter: Filter.Filter<Organization & OfKind<EntityKind.Object>>) => Query.Query<Organization & OfKind<EntityKind.Object>>

Select objects based on a filter.

@param ― filter - Filter to select the objects.

@returns ― Query for the selected objects.

@performance ― O(1); wraps the current AST in a new immutable node without executing anything.

select
(
import Filter
Filter
.
const type: <typeof Organization>(type: typeof Organization, props?: Filter.Props<Organization & OfKind<EntityKind.Object>> | undefined) => Filter.Filter<Organization & OfKind<EntityKind.Object>> (+4 overloads)
type
(
class Organization
Organization
, {
name?: string | Filter.Filter<string> | undefined
name
: 'Acme' }))
.
Query<Organization & OfKind<EntityKind.Object>>.referencedBy<typeof Person>(target: URI | typeof Person, key: RefPropKey<Person & OfKind<EntityKind.Object>>): Query.Query<Person & OfKind<EntityKind.Object>> (+2 overloads)

Find objects referencing this object.

@param ― target - Schema of the referencing object. If not provided, matches any type.

@param ― key - Property path inside the referencing object that is a reference. If not provided, matches any property.

@returns ― Query for the referencing objects.

@performance ― O(1); wraps the current AST in a new immutable node without executing anything.

referencedBy
(
class Person
Person
, 'employer')
.
Query<Person & OfKind<EntityKind.Object>>.orderBy(order_0: Order.Order<Person & OfKind<EntityKind.Object>>, ...order: Order.Order<Person & OfKind<EntityKind.Object>>[]): Query.Query<Person & OfKind<EntityKind.Object>>

Order the query results. Orders are specified in priority order. The first order will be applied first, etc.

Order.property orders by the current result shape's fields, so it works both before and after an

aggregate

: before, by member (row) properties; after, by the flat record's fields (any group or aggregate field — e.g. Order.property('lastMessageAt') reorders the groups by that aggregate).

@param ― order - Order to sort the results.

@returns ― Query for the ordered results.

@performance ― O(k) in the number of orders; builds the AST without executing anything.

orderBy
(
import Order
Order
.
const property: <Person>(property: "name" | "~@dxos/echo/Kind" | "email" | "employer" | "id", direction: OrderDirection) => Order.Order<Person>

Order by a property of the current result shape.

@performance ― O(1); builds an order AST node without sorting anything.

property
<
class Person
Person
>('name', 'asc'))
.
Query<Person & OfKind<EntityKind.Object>>.limit(limit: number): Query.Query<Person & OfKind<EntityKind.Object>>

Limit the number of results.

@param ― limit - Maximum number of results to return.

@returns ― Query for the limited results.

@performance ― O(1); wraps the current AST in a new immutable node without executing anything.

limit
(20);

A query can follow references forwards with reference and backwards with referencedBy, and relations with sourceOf and targetOf. One query answers “the people who work at Acme” without loading each person to check.

A query can also aggregate: count, sum, find the minimum or maximum, or group the results by a field.

import {
import Aggregate
Aggregate
,
import Query
Query
} from '@dxos/echo';
import {
class Person
Person
} from './schema.ts';
const [{
const people: number
people
}] = await
const db: Database.Database
db
.
Database.query: Database.QueryFn
<Query.Query<{
readonly '~@dxos/echo/Query.RecordResult': true;
readonly people: number;
}>>(query: Query.Query<{
readonly '~@dxos/echo/Query.RecordResult': true;
readonly people: number;
}>) => QueryResult<{
readonly '~@dxos/echo/Query.RecordResult': true;
readonly people: number;
}> (+1 overload)
query
(
import Query
Query
.
const type: <typeof Person>(type: typeof Person, predicates?: Filter.Props<Person & OfKind<EntityKind.Object>> | undefined) => Query.Query<Person & OfKind<EntityKind.Object>> (+3 overloads)
type
(
class Person
Person
).
Query<Person & OfKind<EntityKind.Object>>.aggregate<{
readonly people: Aggregate.Aggregate<Person & OfKind<EntityKind.Object>, number>;
}>(aggregates: {
readonly people: Aggregate.Aggregate<Person & OfKind<EntityKind.Object>, number>;
}): Query.Query<{
readonly '~@dxos/echo/Query.RecordResult': true;
readonly people: number;
}>

Aggregate the query results into flat records.

Aggregate.group

entries partition the results into contiguous groups (one record each), keyed by the record field the group is named after; with no group entries the entire input aggregates into a single record. Each declared aggregate becomes a top-level field and can be ordered by with a following

orderBy

using

Order.property

.

Groups are ordered by the first occurrence of their key in the incoming stream, so a preceding orderBy controls group order in the absence of a following one.

Aggregate.items

's own order option is a separate, explicit per-group member ordering — pass it there rather than relying on a preceding orderBy, whose only well-defined job once aggregate follows is establishing initial group order. For example, message threads ordered by their most recent message, each retaining up to 20 members newest-first:

Query.type(Message)
.aggregate({
threadId: Aggregate.group('threadId'),
lastMessageAt: Aggregate.max('created'),
items: Aggregate.items({ limit: 20, order: [Order.property('created', 'desc')] }),
})
.orderBy(Order.property('lastMessageAt', 'desc'));

Must be the last data-selecting clause in the chain — only from/options/orderBy/limit/ skip may follow.

@param ― aggregates - Record of aggregate declarations keyed by result field name.

@returns ― Query whose flat result records carry the named aggregates as fields.

@performance ― O(k) in the number of aggregates; builds the AST without executing anything.

aggregate
({
people: Aggregate.Aggregate<Person & OfKind<EntityKind.Object>, number>
people
:
import Aggregate
Aggregate
.
const count: <Person & OfKind<EntityKind.Object>>() => Aggregate.Aggregate<Person & OfKind<EntityKind.Object>, number>

Count the group's members. Opt-in — groups carry no count unless this aggregate is declared.

@performance ― O(1); builds an aggregate spec without evaluating anything.

count
() }))
.
QueryResult<{ readonly '~@dxos/echo/Query.RecordResult': true; readonly people: number; }>.run(opts?: RunOptions): Promise<{
readonly '~@dxos/echo/Query.RecordResult': true;
readonly people: number;
}[]>

Returns all known results.

@performance ― Async; executes the full query (index plus working set) on every call, not cached; O(n) in the data the executor examines, not in the result count.

run
();

A query reads from sources, chosen with Scope: this space, the code-shipped registry of types, or a feed. Without from, a query reads the database it runs on.

import {
import Filter
Filter
,
import Query
Query
,
import Scope
Scope
,
import Type
Type
} from '@dxos/echo';
// Every type this space stores, and every type registered in code.
const
const allTypes: ({
readonly jsonSchema: JsonSchemaType;
readonly name?: string | undefined;
} & {
readonly id: string;
readonly "~@dxos/echo/Kind": EntityKind.Type;
readonly "~@dxos/echo/SchemaKind": EntityKind.Type;
readonly "~@dxos/echo/Type.StaticSchema": Codec<any, any>;
} & OfKind<EntityKind.Type>)[]
allTypes
= await
const db: Database.Database
db
.
Database.query: Database.QueryFn
<Query.Query<{
readonly jsonSchema: JsonSchemaType;
readonly name?: string | undefined;
} & {
readonly id: string;
readonly "~@dxos/echo/Kind": EntityKind.Type;
readonly "~@dxos/echo/SchemaKind": EntityKind.Type;
readonly "~@dxos/echo/Type.StaticSchema": Codec<any, any>;
} & OfKind<EntityKind.Type>>>(query: Query.Query<{
readonly jsonSchema: JsonSchemaType;
readonly name?: string | undefined;
} & {
readonly id: string;
readonly "~@dxos/echo/Kind": EntityKind.Type;
readonly "~@dxos/echo/SchemaKind": EntityKind.Type;
readonly "~@dxos/echo/Type.StaticSchema": Codec<any, any>;
} & OfKind<EntityKind.Type>>) => QueryResult<...> (+1 overload)
query
(
import Query
Query
.
const select: <Filter.Filter<{
readonly jsonSchema: JsonSchemaType;
readonly name?: string | undefined;
} & {
readonly id: string;
readonly "~@dxos/echo/Kind": EntityKind.Type;
readonly "~@dxos/echo/SchemaKind": EntityKind.Type;
readonly "~@dxos/echo/Type.StaticSchema": Codec<any, any>;
} & OfKind<EntityKind.Type>>>(filter: Filter.Filter<{
readonly jsonSchema: JsonSchemaType;
readonly name?: string | undefined;
} & {
readonly id: string;
readonly "~@dxos/echo/Kind": EntityKind.Type;
readonly "~@dxos/echo/SchemaKind": EntityKind.Type;
readonly "~@dxos/echo/Type.StaticSchema": Codec<any, any>;
} & OfKind<EntityKind.Type>>) => Query.Query<...>

Select objects based on a filter.

@param ― filter - Filter to select the objects.

@returns ― Query for the selected objects.

@performance ― O(1); wraps the current AST in a new immutable node without executing anything.

select
(
import Filter
Filter
.
const type: <Type.Type<TypeSchema>>(type: Type.Type<TypeSchema>, props?: Filter.Props<{
readonly jsonSchema: JsonSchemaType;
readonly name?: string | undefined;
} & {
readonly id: string;
readonly "~@dxos/echo/Kind": EntityKind.Type;
readonly "~@dxos/echo/SchemaKind": EntityKind.Type;
readonly "~@dxos/echo/Type.StaticSchema": Codec<any, any>;
} & OfKind<EntityKind.Type>> | undefined) => Filter.Filter<{
readonly jsonSchema: JsonSchemaType;
readonly name?: string | undefined;
} & ... 1 more ... & OfKind<EntityKind.Type>> (+4 overloads)
type
(
import Type
Type
.
const Type: Type.Type<TypeSchema>

ECHO meta-schema entity — stores { name?, typename, version, jsonSchema }. Type-kind sibling of Type.makeObject(...) / Type.makeRelation(...) outputs. Stored types live under this entity; filter via Filter.type(Type.Type).

Sibling of

Obj

/

Relation

for the third ECHO entity kind: type-kind entities (meta-schemas). The singleton

Type

const is the canonical example — it describes stored type definitions themselves.

Not a Schema.Schema. Use Type.getSchema(value) to obtain the underlying Effect Schema and Type.update(value, draft => ...) to mutate.

A is the instance-type phantom — what Obj.make(value, ...) would produce. Merged with the Type const value via TypeScript declaration merging.

Type
)).
Query<{ readonly jsonSchema: JsonSchemaType; readonly name?: string | undefined; } & { readonly id: string; readonly "~@dxos/echo/Kind": EntityKind.Type; readonly "~@dxos/echo/SchemaKind": EntityKind.Type; readonly "~@dxos/echo/Type.StaticSchema": Codec<any, any>; } & OfKind<...>>.from(...scopes: Scope[]): Query.Query<{
readonly jsonSchema: JsonSchemaType;
readonly name?: string | undefined;
} & {
readonly id: string;
readonly "~@dxos/echo/Kind": EntityKind.Type;
readonly "~@dxos/echo/SchemaKind": EntityKind.Type;
readonly "~@dxos/echo/Type.StaticSchema": Codec<any, any>;
} & OfKind<EntityKind.Type>> (+6 overloads)

Query from one or more raw scopes.

Use the

Scope

constructors rather than raw tagged objects:

Query.select(Filter.type(Type.Type)).from(Scope.space(), Scope.registry());

@performance ― O(k) in the number of scopes; builds the AST without executing anything.

from
(
import Scope
Scope
.
const space: (options?: {
id?: string;
includeAllFeeds?: boolean;
}) => SpaceScope

Scope targeting a space's automerge documents.

With no spaceId, targets the owning space — i.e. the space of whichever database executes the query — so callers can reference "this space" without looking up its id.

@example

db.query(Filter.type(Person).from(Scope.space())); // owning space
db.query(Filter.type(Person).from(Scope.space({ id: otherSpaceId }))); // a specific space

@performance ― O(1); builds a scope node.

space
(),
import Scope
Scope
.
const registry: (location?: "local" | "remote") => RegistryScope

Scope targeting a code-shipped object/type registry.

  • 'local' — the in-process registry attached to the hypergraph (default).
  • 'remote' — a remote registry service (not yet implemented).

@example

// Discover all types — persisted in the space and code-shipped in the registry.
db.query(Filter.type(Type.Type).from(Scope.space(), Scope.registry()));

@performance ― O(1); builds a scope node.

registry
()),
)
.
QueryResult<{ readonly jsonSchema: JsonSchemaType; readonly name?: string | undefined; } & { readonly id: string; readonly "~@dxos/echo/Kind": EntityKind.Type; readonly "~@dxos/echo/SchemaKind": EntityKind.Type; readonly "~@dxos/echo/Type.StaticSchema": Codec<any, any>; } & OfKind<...>>.run(opts?: RunOptions): Promise<({
readonly jsonSchema: JsonSchemaType;
readonly name?: string | undefined;
} & {
readonly id: string;
readonly "~@dxos/echo/Kind": EntityKind.Type;
readonly "~@dxos/echo/SchemaKind": EntityKind.Type;
readonly "~@dxos/echo/Type.StaticSchema": Codec<any, any>;
} & OfKind<EntityKind.Type>)[]>

Returns all known results.

@performance ― Async; executes the full query (index plus working set) on every call, not cached; O(n) in the data the executor examines, not in the result count.

run
();
  • A query is live. Its results change when matching data changes, here or on another peer, and subscribers are notified only when the results change. org.dxos.echo.query#F-2.3

    import {
    import Query
    Query
    } from '@dxos/echo';
    import {
    class Event
    Event
    } from './schema.ts';
    const
    const result: QueryResult<Event & OfKind<EntityKind.Object>>
    result
    =
    const db: Database.Database
    db
    .
    Database.query: Database.QueryFn
    <Query.Query<Event & OfKind<EntityKind.Object>>>(query: Query.Query<Event & OfKind<EntityKind.Object>>) => QueryResult<Event & OfKind<EntityKind.Object>> (+1 overload)
    query
    (
    import Query
    Query
    .
    const type: <typeof Event>(type: typeof Event, predicates?: Filter.Props<Event & OfKind<EntityKind.Object>> | undefined) => Query.Query<Event & OfKind<EntityKind.Object>> (+3 overloads)
    type
    (
    class Event
    Event
    ));
    const
    const unsubscribe: CleanupFn
    unsubscribe
    =
    const result: QueryResult<Event & OfKind<EntityKind.Object>>
    result
    .
    QueryResult<Event & OfKind<EntityKind.Object>>.subscribe(callback?: ((query: QueryResult<Event & OfKind<EntityKind.Object>>) => void) | undefined, opts?: SubscriptionOptions): CleanupFn

    Subscribes to changes in query results.

    @performance ― O(1) registration; the first subscriber starts the live query, whose updates cost an O(n) recompute.

    subscribe
    (
    (
    result: QueryResult<Event & OfKind<EntityKind.Object>>
    result
    ) =>
    var console: Console

    The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

    The module exports two specific components:

    • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
    • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

    Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

    Example using the global console:

    console.log('hello world');
    // Prints: hello world, to stdout
    console.log('hello %s', 'world');
    // Prints: hello world, to stdout
    console.error(new Error('Whoops, something bad happened'));
    // Prints error message and stack trace to stderr:
    // Error: Whoops, something bad happened
    // at [eval]:5:15
    // at Script.runInThisContext (node:vm:132:18)
    // at Object.runInThisContext (node:vm:309:38)
    // at node:internal/process/execution:77:19
    // at [eval]-wrapper:6:22
    // at evalScript (node:internal/process/execution:76:60)
    // at node:internal/main/eval_string:23:3
    const name = 'Will Robinson';
    console.warn(`Danger ${name}! Danger!`);
    // Prints: Danger Will Robinson! Danger!, to stderr

    Example using the Console class:

    const out = getStreamSomehow();
    const err = getStreamSomehow();
    const myConsole = new console.Console(out, err);
    myConsole.log('hello world');
    // Prints: hello world, to out
    myConsole.log('hello %s', 'world');
    // Prints: hello world, to out
    myConsole.error(new Error('Whoops, something bad happened'));
    // Prints: [Error: Whoops, something bad happened], to err
    const name = 'Will Robinson';
    myConsole.warn(`Danger ${name}! Danger!`);
    // Prints: Danger Will Robinson! Danger!, to err

    @see ― source

    console
    .
    Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

    Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

    const count = 5;
    console.log('count: %d', count);
    // Prints: count: 5, to stdout
    console.log('count:', count);
    // Prints: count: 5, to stdout

    See util.format() for more information.

    @since ― v0.1.100

    log
    (
    result: QueryResult<Event & OfKind<EntityKind.Object>>
    result
    .
    QueryResult<Event & OfKind<EntityKind.Object>>.results: (Event & OfKind<EntityKind.Object>)[]

    Currently available results.

    @performance ― Synchronous; O(n) recompute from the working set on first read after a change, cached otherwise.

    results
    .
    Array<T>.length: number

    Gets or sets the length of the array. This is a number one higher than the highest index in the array.

    length
    ),
    {
    fire?: boolean | undefined

    Fire the callback immediately.

    fire
    : true },
    );
    // Each source of the answer, with its state: pending, answered or failed.
    const
    const sources: readonly SourceState[]
    sources
    =
    const result: QueryResult<Event & OfKind<EntityKind.Object>>
    result
    .
    QueryResult<Event & OfKind<EntityKind.Object>>.sources: readonly SourceState[]

    Per-source progress of the live query.

    @performance ― O(sources); allocates.

    sources
    ;
    const unsubscribe: () => void
    unsubscribe
    ();
  • A local write shows in the results, and reaches subscribers, before the writing call returns.

    org.dxos.echo.query#F-2.1
  • Results combine what this tab holds with answers from the index and from EDGE. Each source has its own state, pending until it first answers, and the results grow as sources answer. A live query never ends: there is no point after which its results stop changing. org.dxos.echo.query#F-3.3

  • A query without an order returns entities by id. org.dxos.echo.query#F-2.4

  • A traversal checks each step against the objects this tab holds, so a hit whose reference has since changed here is dropped. org.dxos.echo.query#F-7.2

  • An entry from the index or EDGE is checked against the object only if this tab holds the object. Otherwise it is shown on the source’s word until the object loads. org.dxos.echo.query#F-5.2
  • A page read with skip is cut by the source that answers it. Local writes can drop entries from the page, but they do not re-page it. A window with a property order and only a limit also admits local matches that sort inside it. org.dxos.echo.query#F-9.2 org.dxos.echo.query#F-9.3
  • An aggregate from a remote source is adjusted for local changes, but a minimum or maximum that a local removal would change is marked unverified until the source answers again. org.dxos.echo.query#F-8.3