Repository: rocicorp/mono
Pull request #3960: feat(zero-client): Expose some internal information
Author: login: arv
Created: 2025-03-11T16:25:36Z | Merged: 2025-03-20T11:45:28Z

This adds a new `Inspector` class to the Zero client that can be used to inspect the internal state of the client. This is useful for debugging purposes.

The inspector is lazily loaded so that the client does not need to pay the downloading cost if it is not used.

The inspector only exposes a few things at the moment:

- All the known clients in this IDB store. There can be multiple clients groups in the IDB instance and these groups can have multiple clients.
- The desired queries for a client and whether they are "got".
- The AST of the query and a ZQL and SQL representation of the query.

```ts
const inspector = await z.inspect();
for (const query of await inspector.client.queries()) {
  console.log(query.id);
  console.log(query.got);
  console.log(query.zql);
  console.log(query.sql);
}
```

This is a first step towards exposing more internal information.


Diff:
diff --git a/packages/zero-client/src/client/inspector/inspector.test.ts b/packages/zero-client/src/client/inspector/inspector.test.ts
new file mode 100644
index 0000000000..c900403444
--- /dev/null
+++ b/packages/zero-client/src/client/inspector/inspector.test.ts
@@ -0,0 +1,286 @@
+import {beforeEach, expect, test, vi} from 'vitest';
+import type {AST} from '../../../../zero-protocol/src/ast.ts';
+import {schema} from '../../../../zql/src/query/test/test-schemas.ts';
+import {nanoid} from '../../util/nanoid.ts';
+import {MockSocket, zeroForTest} from '../test-utils.ts';
+
+const rafMock = vi.fn<typeof requestAnimationFrame>();
+
+async function nextRaf() {
+  expect(rafMock).toHaveBeenCalled();
+  const f = rafMock.mock.calls[0][0];
+  rafMock.mock.calls.shift();
+  f(1);
+  await Promise.resolve(1);
+}
+
+beforeEach(() => {
+  vi.spyOn(globalThis, 'WebSocket').mockImplementation(
+    () => new MockSocket('ws://localhost:1234') as unknown as WebSocket,
+  );
+  vi.spyOn(globalThis, 'requestAnimationFrame').mockImplementation(rafMock);
+  return () => {
+    vi.restoreAllMocks();
+  };
+});
+
+test('basics', async () => {
+  const z = zeroForTest();
+  const inspector = await z.inspect();
+
+  expect(inspector.client).toEqual({
+    clientGroup: {
+      id: await z.clientGroupID,
+    },
+    id: z.clientID,
+  });
+  expect(inspector.clientGroup).toEqual({
+    id: await z.clientGroupID,
+  });
+
+  await z.close();
+});
+
+test('basics 2 clients', async () => {
+  const userID = nanoid();
+  const z1 = zeroForTest({userID, kvStore: 'idb'});
+  const z2 = zeroForTest({userID, kvStore: 'idb'});
+
+  const inspector = await z1.inspect();
+
+  expect(await inspector.clients()).toEqual([
+    {
+      clientGroup: {
+        id: await z1.clientGroupID,
+      },
+      id: z1.clientID,
+    },
+    {
+      clientGroup: {
+        id: await z2.clientGroupID,
+      },
+      id: z2.clientID,
+    },
+  ]);
+
+  await z1.close();
+  await z2.close();
+});
+
+test('queries', async () => {
+  const userID = nanoid();
+  const z1 = zeroForTest({userID, schema, kvStore: 'idb'});
+  const z2 = zeroForTest({userID, schema, kvStore: 'idb'});
+  await z1.triggerConnected();
+  await z2.triggerConnected();
+
+  await z1.triggerPoke(null, '1', {
+    desiredQueriesPatches: {
+      [z1.clientID]: [
+        {
+          op: 'put',
+          hash: 'hash1',
+          ast: {
+            table: 'issues',
+          },
+          ttl: 1000,
+        },
+      ],
+    },
+  });
+
+  await z2.triggerPoke(null, '1', {
+    desiredQueriesPatches: {
+      [z2.clientID]: [
+        {
+          op: 'put',
+          hash: 'hash2',
+          ast: {
+            table: 'users',
+          },
+          ttl: 2000,
+        },
+      ],
+    },
+  });
+
+  await nextRaf();
+  await nextRaf();
+
+  const inspector = await z1.inspect();
+  expect(await inspector.clients()).toEqual([
+    {
+      clientGroup: {
+        id: await z1.clientGroupID,
+      },
+      id: z1.clientID,
+    },
+    {
+      clientGroup: {
+        id: await z2.clientGroupID,
+      },
+      id: z2.clientID,
+    },
+  ]);
+  expect(await inspector.clientsWithQueries()).toEqual([
+    {
+      clientGroup: {
+        id: await z1.clientGroupID,
+      },
+      id: z1.clientID,
+    },
+  ]);
+  expect(await inspector.client.queries()).toEqual([
+    {
+      ast: {
+        table: 'issue',
+      },
+      got: false,
+      id: 'hash1',
+    },
+  ]);
+
+  await z1.triggerPoke('1', '2', {
+    gotQueriesPatch: [
+      {
+        hash: 'hash1',
+        op: 'put',
+        ast: {
+          table: 'issues',
+        },
+        ttl: 2000,
+      },
+    ],
+  });
+
+  await nextRaf();
+
+  expect(await inspector.client.queries()).toEqual([
+    {
+      ast: {
+        table: 'issue',
+      },
+      got: true,
+      id: 'hash1',
+    },
+  ]);
+
+  await z1.close();
+  await z2.close();
+});
+
+test('sql getter', async () => {
+  const ast: AST = {
+    table: 'issue',
+    where: {
+      type: 'or',
+      conditions: [
+        {
+          type: 'simple',
+          op: '=',
+          left: {type: 'column', name: 'id'},
+          right: {type: 'literal', value: '1'},
+        },
+        {
+          type: 'simple',
+          op: '!=',
+          left: {type: 'column', name: 'id'},
+          right: {type: 'literal', value: '2'},
+        },
+      ],
+    },
+    alias: undefined,
+    limit: undefined,
+    orderBy: undefined,
+    related: undefined,
+    schema: undefined,
+    start: undefined,
+  };
+  const z1 = zeroForTest({schema});
+  await z1.triggerConnected();
+  await z1.triggerPoke(null, '1', {
+    desiredQueriesPatches: {
+      [z1.clientID]: [
+        {
+          op: 'put',
+          hash: 'hash1',
+          ast: {
+            ...ast,
+            table: 'issues',
+          },
+          ttl: 1000,
+        },
+      ],
+    },
+  });
+
+  await nextRaf();
+
+  const inspector = await z1.inspect();
+  const queries = await inspector.client.queries();
+  expect(queries).toEqual([
+    {
+      ast,
+      got: false,
+      id: 'hash1',
+    },
+  ]);
+  expect(queries[0].sql).toMatchInlineSnapshot(
+    `"SELECT "issue"."id","issue"."title","issue"."description","issue"."closed","issue"."owner_id" as "ownerId" FROM "issues" as "issue" WHERE ("id" = $1 OR "id" != $2)"`,
+  );
+});
+
+test('zql getter', async () => {
+  const ast: AST = {
+    table: 'issue',
+    where: {
+      type: 'or',
+      conditions: [
+        {
+          type: 'simple',
+          op: '=',
+          left: {type: 'column', name: 'id'},
+          right: {type: 'literal', value: '1'},
+        },
+        {
+          type: 'simple',
+          op: '!=',
+          left: {type: 'column', name: 'id'},
+          right: {type: 'literal', value: '2'},
+        },
+      ],
+    },
+  };
+  const z1 = zeroForTest({schema});
+  await z1.triggerConnected();
+  await z1.triggerPoke(null, '1', {
+    desiredQueriesPatches: {
+      [z1.clientID]: [
+        {
+          op: 'put',
+          hash: 'hash1',
+          ast: {
+            ...ast,
+            table: 'issues',
+          },
+          ttl: 1000,
+        },
+      ],
+    },
+  });
+
+  await nextRaf();
+
+  const inspector = await z1.inspect();
+  const queries = await inspector.client.queries();
+  expect(queries).toEqual([
+    {
+      ast,
+      got: false,
+      id: 'hash1',
+    },
+  ]);
+  expect(queries[0].zql).toBe(
+    "issue.where(({cmp, or}) => or(cmp('id', '1'), cmp('id', '!=', '2')))",
+  );
+});
diff --git a/packages/zero-client/src/client/inspector/inspector.ts b/packages/zero-client/src/client/inspector/inspector.ts
new file mode 100644
index 0000000000..cb8ea31633
--- /dev/null
+++ b/packages/zero-client/src/client/inspector/inspector.ts
@@ -0,0 +1,293 @@
+import type {BTreeRead} from '../../../../replicache/src/btree/read.ts';
+import {type Read} from '../../../../replicache/src/dag/store.ts';
+import {readFromHash} from '../../../../replicache/src/db/read.ts';
+import * as FormatVersion from '../../../../replicache/src/format-version-enum.ts';
+import {
+  getClientGroup,
+  getClientGroups,
+} from '../../../../replicache/src/persist/client-groups.ts';
+import {
+  getClient,
+  getClients,
+  type ClientMap,
+} from '../../../../replicache/src/persist/clients.ts';
+import type {ReplicacheImpl} from '../../../../replicache/src/replicache-impl.ts';
+import {withRead} from '../../../../replicache/src/with-transactions.ts';
+import {assert} from '../../../../shared/src/asserts.ts';
+import type {ReadonlyJSONValue} from '../../../../shared/src/json.ts';
+import * as valita from '../../../../shared/src/valita.ts';
+import {compile} from '../../../../z2s/src/compiler.ts';
+import {formatPg} from '../../../../z2s/src/sql.ts';
+import {astSchema, type AST} from '../../../../zero-protocol/src/ast.ts';
+import type {Row} from '../../../../zero-protocol/src/data.ts';
+import type {Schema} from '../../../../zero-schema/src/builder/schema-builder.ts';
+import type {Format} from '../../../../zql/src/ivm/view.ts';
+import {astToZQL} from '../../../../zql/src/query/ast-to-zql.ts';
+import {
+  desiredQueriesPrefixForClient,
+  ENTITIES_KEY_PREFIX,
+  toGotQueriesKey,
+} from '../keys.ts';
+import type {MutatorDefs} from '../replicache-types.ts';
+import type {
+  ClientGroup as ClientGroupInterface,
+  Client as ClientInterface,
+  Inspector as InspectorInterface,
+  Query as QueryInterface,
+} from './types.ts';
+
+type Rep = ReplicacheImpl<MutatorDefs>;
+
+export async function newInspector(
+  rep: Rep,
+  schema: Schema,
+): Promise<InspectorInterface> {
+  const clientGroupID = await rep.clientGroupID;
+  return new Inspector(rep, schema, rep.clientID, clientGroupID);
+}
+
+class Inspector implements InspectorInterface {
+  readonly #rep: Rep;
+  readonly client: Client;
+  readonly clientGroup: ClientGroup;
+  readonly #schema: Schema;
+
+  constructor(
+    rep: ReplicacheImpl,
+    schema: Schema,
+    clientID: string,
+    clientGroupID: string,
+  ) {
+    this.#rep = rep;
+    this.#schema = schema;
+    this.client = new Client(rep, schema, clientID, clientGroupID);
+    this.clientGroup = this.client.clientGroup;
+  }
+
+  clients(): Promise<ClientInterface[]> {
+    return withDagRead(this.#rep, dagRead =>
+      clients(this.#rep, this.#schema, dagRead),
+    );
+  }
+
+  clientsWithQueries(): Promise<ClientInterface[]> {
+    return withDagRead(this.#rep, dagRead =>
+      clientsWithQueries(this.#rep, this.#schema, dagRead),
+    );
+  }
+
+  clientGroups(): Promise<ClientGroup[]> {
+    return withDagRead(this.#rep, async dagRead => {
+      const clientGroups = await getClientGroups(dagRead);
+      return [...clientGroups.keys()].map(
+        clientGroupID =>
+          new ClientGroup(this.#rep, this.#schema, clientGroupID),
+      );
+    });
+  }
+}
+
+class Client implements ClientInterface {
+  readonly #rep: Rep;
+  readonly id: string;
+  readonly clientGroup: ClientGroup;
+  readonly #schema: Schema;
+
+  constructor(rep: Rep, schema: Schema, id: string, clientGroupID: string) {
+    this.#rep = rep;
+    this.#schema = schema;
+    this.id = id;
+    this.clientGroup = new ClientGroup(rep, schema, clientGroupID);
+  }
+
+  queries(): Promise<QueryInterface[]> {
+    return withDagRead(this.#rep, async dagRead => {
+      const prefix = desiredQueriesPrefixForClient(this.id);
+      const tree = await getBTree(dagRead, this.id);
+      const qs: QueryInterface[] = [];
+      for await (const [key, value] of tree.scan(prefix)) {
+        if (!key.startsWith(prefix)) {
+          break;
+        }
+
+        const hash = key.substring(prefix.length);
+        const got = await tree.has(toGotQueriesKey(hash));
+        const q = new Query(
+          hash,
+          valita.parse(value, astSchema),
+          got,
+          this.#schema,
+        );
+        qs.push(q);
+      }
+      return qs;
+    });
+  }
+
+  map(): Promise<Map<string, ReadonlyJSONValue>> {
+    return withDagRead(this.#rep, async dagRead => {
+      const tree = await getBTree(dagRead, this.id);
+      const map = new Map<string, ReadonlyJSONValue>();
+      for await (const [key, value] of tree.scan('')) {
+        map.set(key, value);
+      }
+      return map;
+    });
+  }
+
+  rows(tableName: string): Promise<Row[]> {
+    return withDagRead(this.#rep, async dagRead => {
+      const prefix = ENTITIES_KEY_PREFIX + tableName;
+      const tree = await getBTree(dagRead, this.id);
+      const rows: Row[] = [];
+      for await (const [key, value] of tree.scan(prefix)) {
+        if (!key.startsWith(prefix)) {
+          break;
+        }
+        rows.push(value as Row);
+      }
+      return rows;
+    });
+  }
+}
+
+class ClientGroup implements ClientGroupInterface {
+  readonly #rep: Rep;
+  readonly id: string;
+  readonly #schema: Schema;
+
+  constructor(rep: Rep, schema: Schema, id: string) {
+    this.#rep = rep;
+    this.#schema = schema;
+    this.id = id;
+  }
+
+  clients(): Promise<ClientInterface[]> {
+    return withDagRead(this.#rep, dagRead =>
+      clients(
+        this.#rep,
+        this.#schema,
+        dagRead,
+        ([_, v]) => v.clientGroupID === this.id,
+      ),
+    );
+  }
+
+  clientsWithQueries(): Promise<ClientInterface[]> {
+    return withDagRead(this.#rep, dagRead =>
+      clientsWithQueries(
+        this.#rep,
+        this.#schema,
+        dagRead,
+        ([_, v]) => v.clientGroupID === this.id,
+      ),
+    );
+  }
+
+  queries(): Promise<QueryInterface[]> {
+    return withDagRead(this.#rep, async dagRead => {
+      const cs = await clients(
+        this.#rep,
+        this.#schema,
+        dagRead,
+        ([_, v]) => v.clientGroupID === this.id,
+      );
+      const qs: Map<string, QueryInterface> = new Map();
+      await Promise.all(
+        cs.map(async client => {
+          const clientQueries = await client.queries();
+          clientQueries.forEach(q => qs.set(q.id, q));
+        }),
+      );
+      return [...qs.values()];
+    });
+  }
+}
+
+async function withDagRead<T>(
+  rep: Rep,
+  f: (dagRead: Read) => Promise<T>,
+): Promise<T> {
+  await rep.refresh();
+  await rep.persist();
+  return withRead(rep.perdag, f);
+}
+
+async function getBTree(dagRead: Read, clientID: string): Promise<BTreeRead> {
+  const client = await getClient(clientID, dagRead);
+  assert(client, `Client not found: ${clientID}`);
+  const {clientGroupID} = client;
+  const clientGroup = await getClientGroup(clientGroupID, dagRead);
+  assert(clientGroup, `Client group not found: ${clientGroupID}`);
+  const dbRead = await readFromHash(
+    clientGroup.headHash,
+    dagRead,
+    FormatVersion.Latest,
+  );
+  return dbRead.map;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type MapEntry<T extends ReadonlyMap<any, any>> =
+  T extends ReadonlyMap<infer K, infer V> ? readonly [K, V] : never;
+
+async function clients(
+  rep: Rep,
+  schema: Schema,
+  dagRead: Read,
+  predicate: (entry: MapEntry<ClientMap>) => boolean = () => true,
+): Promise<ClientInterface[]> {
+  const clients = await getClients(dagRead);
+  return [...clients.entries()]
+    .filter(predicate)
+    .map(
+      ([clientID, {clientGroupID}]) =>
+        new Client(rep, schema, clientID, clientGroupID),
+    );
+}
+
+async function clientsWithQueries(
+  rep: Rep,
+  schema: Schema,
+  dagRead: Read,
+  predicate: (entry: MapEntry<ClientMap>) => boolean = () => true,
+): Promise<ClientInterface[]> {
+  const allClients = await clients(rep, schema, dagRead, predicate);
+  const clientsWithQueries: ClientInterface[] = [];
+  await Promise.all(
+    allClients.map(async client => {
+      const queries = await client.queries();
+      if (queries.length > 0) {
+        clientsWithQueries.push(client);
+      }
+    }),
+  );
+  return clientsWithQueries;
+}
+
+class Query implements QueryInterface {
+  readonly id: string;
+  readonly ast: AST;
+  readonly got: boolean;
+  #schema: Schema;
+
+  constructor(id: string, ast: AST, got: boolean, schema: Schema) {
+    this.id = id;
+    this.ast = ast;
+    this.got = got;
+    this.#schema = schema;
+  }
+
+  get sql(): string {
+    const format: Format = {
+      singular: false,
+      relationships: {},
+    };
+    const sqlQuery = formatPg(compile(this.ast, this.#schema.tables, format));
+    return sqlQuery.text;
+  }
+
+  get zql(): string {
+    return this.ast.table + astToZQL(this.ast);
+  }
+}
diff --git a/packages/zero-client/src/client/inspector/types.ts b/packages/zero-client/src/client/inspector/types.ts
new file mode 100644
index 0000000000..60086a66d6
--- /dev/null
+++ b/packages/zero-client/src/client/inspector/types.ts
@@ -0,0 +1,38 @@
+import type {ReadonlyJSONValue} from '../../../../shared/src/json.ts';
+import type {AST} from '../../../../zero-protocol/src/ast.ts';
+import type {Row} from '../../../../zero-protocol/src/data.ts';
+
+export interface GetInspector {
+  inspect(): Promise<Inspector>;
+}
+
+export interface Inspector {
+  readonly client: Client;
+  readonly clientGroup: ClientGroup;
+  clients(): Promise<Client[]>;
+  clientsWithQueries(): Promise<Client[]>;
+  clientGroups(): Promise<ClientGroup[]>;
+}
+
+export interface Client {
+  readonly id: string;
+  readonly clientGroup: ClientGroup;
+  queries(): Promise<Query[]>;
+  map(): Promise<Map<string, ReadonlyJSONValue>>;
+  rows(tableName: string): Promise<Row[]>;
+}
+
+export interface ClientGroup {
+  readonly id: string;
+  clients(): Promise<Client[]>;
+  clientsWithQueries(): Promise<Client[]>;
+  queries(): Promise<Query[]>;
+}
+
+export interface Query {
+  readonly id: string;
+  readonly ast: AST;
+  readonly got: boolean;
+  readonly sql: string;
+  readonly zql: string;
+}
diff --git a/packages/zero-client/src/client/keys.ts b/packages/zero-client/src/client/keys.ts
index 85fca4e34f..d7fbed72b1 100644
--- a/packages/zero-client/src/client/keys.ts
+++ b/packages/zero-client/src/client/keys.ts
@@ -4,7 +4,7 @@ import type {CompoundKey} from '../../../zero-protocol/src/ast.ts';
 import type {Row} from '../../../zero-protocol/src/data.ts';
 import {primaryKeyValueSchema} from '../../../zero-protocol/src/primary-key.ts';
 
-const DESIRED_QUERIES_KEY_PREFIX = 'd/';
+export const DESIRED_QUERIES_KEY_PREFIX = 'd/';
 export const GOT_QUERIES_KEY_PREFIX = 'g/';
 export const ENTITIES_KEY_PREFIX = 'e/';
 
diff --git a/packages/zero-client/src/client/test-utils.ts b/packages/zero-client/src/client/test-utils.ts
index c907aa9114..96c4143326 100644
--- a/packages/zero-client/src/client/test-utils.ts
+++ b/packages/zero-client/src/client/test-utils.ts
@@ -76,6 +76,8 @@ export class TestZero<
   const S extends Schema,
   MD extends CustomMutatorDefs<S> | undefined = undefined,
 > extends Zero<S, MD> {
+  pokeIDCounter = 0;
+
   #connectionStateResolvers: Set<{
     state: ConnectionState;
     resolve: (state: ConnectionState) => void;
@@ -179,6 +181,26 @@ export class TestZero<
     return this.triggerMessage(msg);
   }
 
+  async triggerPoke(
+    cookieStart: string | null,
+    cookieEnd: string,
+    pokePart: Omit<PokePartBody, 'pokeID'>,
+  ): Promise<void> {
+    const id = `${this.pokeIDCounter++}`;
+    await this.triggerPokeStart({
+      pokeID: id,
+      baseCookie: cookieStart,
+    });
+    await this.triggerPokePart({
+      ...pokePart,
+      pokeID: id,
+    });
+    await this.triggerPokeEnd({
+      pokeID: id,
+      cookie: cookieEnd,
+    });
+  }
+
   triggerPullResponse(pullResponseBody: PullResponseBody): Promise<void> {
     const msg: PullResponseMessage = ['pull', pullResponseBody];
     return this.triggerMessage(msg);
@@ -275,6 +297,7 @@ export async function waitForUpstreamMessage(
     }
   }
 }
+
 export function storageMock(storage: Record<string, string>): Storage {
   return {
     setItem: (key, value) => {
diff --git a/packages/zero-client/src/client/zero.ts b/packages/zero-client/src/client/zero.ts
index 61acb61e1a..770c3dc6a7 100644
--- a/packages/zero-client/src/client/zero.ts
+++ b/packages/zero-client/src/client/zero.ts
@@ -101,6 +101,7 @@ import {
   appendPath,
   toWSString,
 } from './http-string.ts';
+import type {Inspector} from './inspector/types.ts';
 import {IVMSourceBranch} from './ivm-branch.ts';
 import {type LogOptions, createLogOptions} from './log-options.ts';
 import {
@@ -281,6 +282,7 @@ export class Zero<
   readonly #lc: LogContext;
   readonly #logOptions: LogOptions;
   readonly #enableAnalytics: boolean;
+  readonly #schema: S;
   readonly #clientSchema: ClientSchema;
 
   readonly #pokeHandler: PokeHandler;
@@ -500,6 +502,7 @@ export class Zero<
 
     this.storageKey = storageKey ?? '';
 
+    this.#schema = schema;
     const {clientSchema, hash} = clientSchemaFrom(schema);
     this.#clientSchema = clientSchema;
 
@@ -1814,6 +1817,22 @@ export class Zero<
 
     return rv as MakeEntityQueriesFromSchema<S>;
   }
+
+  /**
+   * `inspect` returns an object that can be used to inspect the state of the
+   * queries a Zero instance uses. It is intended for debugging purposes.
+   */
+  async inspect(): Promise<Inspector> {
+    // We use esbuild dropLabels to strip this code when we build the code for the bundle size dashboard.
+    // https://esbuild.github.io/api/#ignore-annotations
+    // /packages/zero/tool/build.ts
+
+    // eslint-disable-next-line no-unused-labels
+    BUNDLE_SIZE: {
+      const m = await import('./inspector/inspector.ts');
+      return m.newInspector(this.#rep, this.#schema);
+    }
+  }
 }
 
 export async function createSocket(
diff --git a/packages/zero/tool/build.ts b/packages/zero/tool/build.ts
index 0c430fa80e..7fb496e7b0 100644
--- a/packages/zero/tool/build.ts
+++ b/packages/zero/tool/build.ts
@@ -92,6 +92,8 @@ async function verifyDependencies(external: Iterable<string>) {
   );
 }
 
+const dropLabels = forBundleSizeDashboard ? ['BUNDLE_SIZE'] : [];
+
 async function buildZeroClient() {
   const define = makeDefine('unknown');
   define['process.env.DISABLE_MUTATION_RECOVERY'] = 'true';
@@ -111,6 +113,7 @@ async function buildZeroClient() {
     // Use neutral to remove the automatic define for process.env.NODE_ENV
     platform: 'neutral',
     define,
+    dropLabels,
     outdir: basePath('out'),
     entryPoints,
   });


Reviews:
user: login: tantaman
body: > The AST of the query and an SQL representation of the query.

It'd be ideal if we can get a ZQL representation of the query. Granted it will not match their original query builder use but at least they won't have to think in two languages.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T17:19:57Z
user: login: arv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T17:30:09Z
user: login: arv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T17:31:28Z
user: login: arv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T17:36:15Z
user: login: arv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T17:38:40Z
user: login: arv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T17:39:59Z
user: login: aboodman
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T17:47:01Z

Review comments:
diff_hunk: @@ -0,0 +1,35 @@
+import type {AST} from '../../../zero-protocol/src/ast.ts';
+
+export interface GetInspector {
+  inspect(): Promise<Inspector>;
+}
+
+export interface Inspector {
+  readonly clientID: string;
+  readonly clientGroupID: string;
path: packages/zero-client/src/client/inspector-types.ts
user: login: tantaman
body: Wdyt about having the user call `inspector.client.id` and `inspector.clientGroup.id` rather than surfacing the `clientID` and `clientGroupID` here?
created_at: 2025-03-11T17:10:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1780,6 +1781,11 @@ export class Zero<
 
     return rv as MakeEntityQueriesFromSchema<S>;
   }
+
+  async inspect(): Promise<Inspector> {
+    const m = await import('./inspector.ts');
path: packages/zero-client/src/client/zero.ts
user: login: tantaman
body: How about putting `inspector.ts` and anything else that should only be dynamically imported into a specific folder (src/client/lazy) and adding an eslint rule to prevent accidental inclusion of these assets through anything besides a dynamic import?
created_at: 2025-03-11T17:13:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,233 @@
+import type {BTreeRead} from '../../../replicache/src/btree/read.ts';
+import {type Read} from '../../../replicache/src/dag/store.ts';
+import {readFromHash} from '../../../replicache/src/db/read.ts';
+import * as FormatVersion from '../../../replicache/src/format-version-enum.ts';
+import {
+  getClientGroup,
+  getClientGroups,
+} from '../../../replicache/src/persist/client-groups.ts';
+import {
+  getClient,
+  getClients,
+  type ClientMap,
+} from '../../../replicache/src/persist/clients.ts';
+import type {ReplicacheImpl} from '../../../replicache/src/replicache-impl.ts';
+import {withRead} from '../../../replicache/src/with-transactions.ts';
+import {assert} from '../../../shared/src/asserts.ts';
+import type {ReadonlyJSONValue} from '../../../shared/src/json.ts';
+import * as valita from '../../../shared/src/valita.ts';
+import {compile} from '../../../z2s/src/compiler.ts';
+import {formatPg} from '../../../z2s/src/sql.ts';
+import {astSchema, type AST} from '../../../zero-protocol/src/ast.ts';
+import type {Row} from '../../../zero-protocol/src/data.ts';
+import type {Format} from '../../../zql/src/ivm/view.ts';
+import type {
+  ClientGroup as ClientGroupInterface,
+  Client as ClientInterface,
+  Inspector as InspectorInterface,
+  Query as QueryInterface,
+} from './inspector-types.ts';
+import {
+  desiredQueriesPrefixForClient,
+  ENTITIES_KEY_PREFIX,
+  toGotQueriesKey,
+} from './keys.ts';
+import type {MutatorDefs} from './replicache-types.ts';
+
+type Rep = ReplicacheImpl<MutatorDefs>;
+
+export async function newInspector(rep: Rep): Promise<InspectorInterface> {
+  const clientGroupID = await rep.clientGroupID;
+  return new Inspector(rep, rep.clientID, clientGroupID);
+}
+
+class Inspector implements InspectorInterface {
+  readonly clientID: string;
+  readonly clientGroupID: string;
+  readonly #rep: Rep;
+  readonly client: Client;
+  readonly clientGroup: ClientGroup;
+
+  constructor(rep: ReplicacheImpl, clientID: string, clientGroupID: string) {
+    this.#rep = rep;
+    this.clientID = clientID;
+    this.clientGroupID = clientGroupID;
+    this.client = new Client(rep, clientID, clientGroupID);
+    this.clientGroup = this.client.clientGroup;
+  }
+
+  clients(): Promise<ClientInterface[]> {
+    return withDagRead(this.#rep, dagRead => clients(this.#rep, dagRead));
+  }
+
+  clientsWithQueries(): Promise<ClientInterface[]> {
+    return withDagRead(this.#rep, async dagRead => {
+      const allClients = await clients(this.#rep, dagRead);
+      const clientsWithQueries: ClientInterface[] = [];
+      for (const client of allClients) {
+        const queries = await client.queries();
+        if (queries.length > 0) {
+          clientsWithQueries.push(client);
+        }
+      }
path: packages/zero-client/src/client/inspector.ts
user: login: tantaman
body: Can you run many DAG reads concurrently? If so --

```ts
await Promise.all(allClients.map((client) => {
  const queries = await client.queries();
  if (queries.length > 0) {
    clientsWithQueries.push(client);
  }
}))
```
created_at: 2025-03-11T17:16:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,35 @@
+import type {AST} from '../../../zero-protocol/src/ast.ts';
+
+export interface GetInspector {
+  inspect(): Promise<Inspector>;
+}
+
+export interface Inspector {
+  readonly clientID: string;
+  readonly clientGroupID: string;
+  readonly client: Client;
+  readonly clientGroup: ClientGroup;
+  clients(): Promise<Client[]>;
+  clientsWithQueries(): Promise<Client[]>;
+  clientGroups(): Promise<ClientGroup[]>;
+}
+
+export interface Client {
+  readonly clientID: string;
+  readonly clientGroupID: string;
+  readonly clientGroup: ClientGroup;
+  queries(): Promise<Query[]>;
+}
+
+export interface ClientGroup {
+  readonly clientGroupID: string;
+  clients(): Promise<Client[]>;
+  queries(): Promise<Query[]>;
+}
+
+export interface Query {
+  readonly id: string;
+  readonly ast: AST;
+  readonly got: boolean;
path: packages/zero-client/src/client/inspector-types.ts
user: login: tantaman
body: will the inspector eventually be reactive to changes in `got` state? Would be cool to see how active queries change while navigating through the app...
created_at: 2025-03-11T17:18:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,35 @@
+import type {AST} from '../../../zero-protocol/src/ast.ts';
+
+export interface GetInspector {
+  inspect(): Promise<Inspector>;
+}
+
+export interface Inspector {
+  readonly clientID: string;
+  readonly clientGroupID: string;
+  readonly client: Client;
+  readonly clientGroup: ClientGroup;
+  clients(): Promise<Client[]>;
+  clientsWithQueries(): Promise<Client[]>;
+  clientGroups(): Promise<ClientGroup[]>;
+}
+
+export interface Client {
+  readonly clientID: string;
+  readonly clientGroupID: string;
+  readonly clientGroup: ClientGroup;
+  queries(): Promise<Query[]>;
+}
+
+export interface ClientGroup {
+  readonly clientGroupID: string;
+  clients(): Promise<Client[]>;
+  queries(): Promise<Query[]>;
+}
+
+export interface Query {
path: packages/zero-client/src/client/inspector-types.ts
user: login: tantaman
body: no ttl property?
created_at: 2025-03-11T17:18:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,35 @@
+import type {AST} from '../../../zero-protocol/src/ast.ts';
+
+export interface GetInspector {
+  inspect(): Promise<Inspector>;
+}
+
+export interface Inspector {
+  readonly clientID: string;
+  readonly clientGroupID: string;
+  readonly client: Client;
+  readonly clientGroup: ClientGroup;
+  clients(): Promise<Client[]>;
+  clientsWithQueries(): Promise<Client[]>;
+  clientGroups(): Promise<ClientGroup[]>;
+}
+
+export interface Client {
+  readonly clientID: string;
+  readonly clientGroupID: string;
+  readonly clientGroup: ClientGroup;
+  queries(): Promise<Query[]>;
+}
+
+export interface ClientGroup {
+  readonly clientGroupID: string;
+  clients(): Promise<Client[]>;
+  queries(): Promise<Query[]>;
+}
+
+export interface Query {
path: packages/zero-client/src/client/inspector-types.ts
user: login: arv
body: It is not synced to the client at the moment
created_at: 2025-03-11T17:30:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1780,6 +1781,11 @@ export class Zero<
 
     return rv as MakeEntityQueriesFromSchema<S>;
   }
+
+  async inspect(): Promise<Inspector> {
+    const m = await import('./inspector.ts');
path: packages/zero-client/src/client/zero.ts
user: login: arv
body: I was a bit worried about relative paths after bundling but it should not really matter.
created_at: 2025-03-11T17:31:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,233 @@
+import type {BTreeRead} from '../../../replicache/src/btree/read.ts';
+import {type Read} from '../../../replicache/src/dag/store.ts';
+import {readFromHash} from '../../../replicache/src/db/read.ts';
+import * as FormatVersion from '../../../replicache/src/format-version-enum.ts';
+import {
+  getClientGroup,
+  getClientGroups,
+} from '../../../replicache/src/persist/client-groups.ts';
+import {
+  getClient,
+  getClients,
+  type ClientMap,
+} from '../../../replicache/src/persist/clients.ts';
+import type {ReplicacheImpl} from '../../../replicache/src/replicache-impl.ts';
+import {withRead} from '../../../replicache/src/with-transactions.ts';
+import {assert} from '../../../shared/src/asserts.ts';
+import type {ReadonlyJSONValue} from '../../../shared/src/json.ts';
+import * as valita from '../../../shared/src/valita.ts';
+import {compile} from '../../../z2s/src/compiler.ts';
+import {formatPg} from '../../../z2s/src/sql.ts';
+import {astSchema, type AST} from '../../../zero-protocol/src/ast.ts';
+import type {Row} from '../../../zero-protocol/src/data.ts';
+import type {Format} from '../../../zql/src/ivm/view.ts';
+import type {
+  ClientGroup as ClientGroupInterface,
+  Client as ClientInterface,
+  Inspector as InspectorInterface,
+  Query as QueryInterface,
+} from './inspector-types.ts';
+import {
+  desiredQueriesPrefixForClient,
+  ENTITIES_KEY_PREFIX,
+  toGotQueriesKey,
+} from './keys.ts';
+import type {MutatorDefs} from './replicache-types.ts';
+
+type Rep = ReplicacheImpl<MutatorDefs>;
+
+export async function newInspector(rep: Rep): Promise<InspectorInterface> {
+  const clientGroupID = await rep.clientGroupID;
+  return new Inspector(rep, rep.clientID, clientGroupID);
+}
+
+class Inspector implements InspectorInterface {
+  readonly clientID: string;
+  readonly clientGroupID: string;
+  readonly #rep: Rep;
+  readonly client: Client;
+  readonly clientGroup: ClientGroup;
+
+  constructor(rep: ReplicacheImpl, clientID: string, clientGroupID: string) {
+    this.#rep = rep;
+    this.clientID = clientID;
+    this.clientGroupID = clientGroupID;
+    this.client = new Client(rep, clientID, clientGroupID);
+    this.clientGroup = this.client.clientGroup;
+  }
+
+  clients(): Promise<ClientInterface[]> {
+    return withDagRead(this.#rep, dagRead => clients(this.#rep, dagRead));
+  }
+
+  clientsWithQueries(): Promise<ClientInterface[]> {
+    return withDagRead(this.#rep, async dagRead => {
+      const allClients = await clients(this.#rep, dagRead);
+      const clientsWithQueries: ClientInterface[] = [];
+      for (const client of allClients) {
+        const queries = await client.queries();
+        if (queries.length > 0) {
+          clientsWithQueries.push(client);
+        }
+      }
path: packages/zero-client/src/client/inspector.ts
user: login: arv
body: Yes, reading can be done concurrently. I didn't use Promise.all here because it is less readable but ok, I'll change.
created_at: 2025-03-11T17:36:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,35 @@
+import type {AST} from '../../../zero-protocol/src/ast.ts';
+
+export interface GetInspector {
+  inspect(): Promise<Inspector>;
+}
+
+export interface Inspector {
+  readonly clientID: string;
+  readonly clientGroupID: string;
+  readonly client: Client;
+  readonly clientGroup: ClientGroup;
+  clients(): Promise<Client[]>;
+  clientsWithQueries(): Promise<Client[]>;
+  clientGroups(): Promise<ClientGroup[]>;
+}
+
+export interface Client {
+  readonly clientID: string;
+  readonly clientGroupID: string;
+  readonly clientGroup: ClientGroup;
+  queries(): Promise<Query[]>;
+}
+
+export interface ClientGroup {
+  readonly clientGroupID: string;
+  clients(): Promise<Client[]>;
+  queries(): Promise<Query[]>;
+}
+
+export interface Query {
+  readonly id: string;
+  readonly ast: AST;
+  readonly got: boolean;
path: packages/zero-client/src/client/inspector-types.ts
user: login: arv
body: That would be nice. Right now it can read btrees that are not from the head so I cannot use subscribe as is.

I definitely think this should become a chrome dev tools extension and then it must be reactive.
created_at: 2025-03-11T17:38:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,35 @@
+import type {AST} from '../../../zero-protocol/src/ast.ts';
+
+export interface GetInspector {
+  inspect(): Promise<Inspector>;
+}
+
+export interface Inspector {
+  readonly clientID: string;
+  readonly clientGroupID: string;
+  readonly client: Client;
+  readonly clientGroup: ClientGroup;
+  clients(): Promise<Client[]>;
+  clientsWithQueries(): Promise<Client[]>;
+  clientGroups(): Promise<ClientGroup[]>;
+}
+
+export interface Client {
+  readonly clientID: string;
+  readonly clientGroupID: string;
+  readonly clientGroup: ClientGroup;
+  queries(): Promise<Query[]>;
+}
+
+export interface ClientGroup {
+  readonly clientGroupID: string;
+  clients(): Promise<Client[]>;
+  queries(): Promise<Query[]>;
+}
+
+export interface Query {
path: packages/zero-client/src/client/inspector-types.ts
user: login: arv
body: It is kept in memory for the current client but I'll sync it down instead so that we can set other clients too.
created_at: 2025-03-11T17:39:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,35 @@
+import type {AST} from '../../../zero-protocol/src/ast.ts';
+
+export interface GetInspector {
+  inspect(): Promise<Inspector>;
+}
+
+export interface Inspector {
+  readonly clientID: string;
+  readonly clientGroupID: string;
+  readonly client: Client;
+  readonly clientGroup: ClientGroup;
+  clients(): Promise<Client[]>;
+  clientsWithQueries(): Promise<Client[]>;
+  clientGroups(): Promise<ClientGroup[]>;
+}
+
+export interface Client {
+  readonly clientID: string;
+  readonly clientGroupID: string;
+  readonly clientGroup: ClientGroup;
+  queries(): Promise<Query[]>;
+}
+
+export interface ClientGroup {
+  readonly clientGroupID: string;
+  clients(): Promise<Client[]>;
+  queries(): Promise<Query[]>;
+}
+
+export interface Query {
+  readonly id: string;
+  readonly ast: AST;
+  readonly got: boolean;
path: packages/zero-client/src/client/inspector-types.ts
user: login: aboodman
body: Love the idea of making it reactive. The obvious API that springs to mind is `from("system.clients")` and then using `useQuery` as-is in the inspector.
created_at: 2025-03-11T17:47:01Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-03-11T16:25:41Z
body: [vc]: #oyTUYDIA8TGBZv/mXxQElH5j6F2ggVWCXWyRDdS4guM=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJyZXBsaWNhY2hlLWRvY3MiLCJyb290RGlyZWN0b3J5IjpudWxsLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vcm9jaWNvcnAvcmVwbGljYWNoZS1kb2NzL0FueHV4a0hKaUZ3eGlhOTJZZ3NQVnhUR0R4YlMiLCJwcmV2aWV3VXJsIjoicmVwbGljYWNoZS1kb2NzLWdpdC1hcnYtemVyby1pbnNwZWN0LXJvY2ljb3JwLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoicmVwbGljYWNoZS1kb2NzLWdpdC1hcnYtemVyby1pbnNwZWN0LXJvY2ljb3JwLnZlcmNlbC5hcHAifX0seyJuYW1lIjoiemJ1Z3MiLCJyb290RGlyZWN0b3J5IjoiYXBwcy96YnVncyIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9yb2NpY29ycC96YnVncy82Y1NvdFhEQjhEZDgzMzR6RGZxeEFSejZTWHNwIiwicHJldmlld1VybCI6InpidWdzLWdpdC1hcnYtemVyby1pbnNwZWN0LXJvY2ljb3JwLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiemJ1Z3MtZ2l0LWFydi16ZXJvLWluc3BlY3Qtcm9jaWNvcnAudmVyY2VsLmFwcCJ9fV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **replicache-docs** | ✅ Ready ([Inspect](https://vercel.com/rocicorp/replicache-docs/AnxuxkHJiFwxia92YgsPVxTGDxbS)) | [Visit Preview](https://replicache-docs-git-arv-zero-inspect-rocicorp.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/replicache-docs-git-arv-zero-inspect-rocicorp.vercel.app?via=pr-comment-feedback-link) | Mar 20, 2025 11:41am |
| **zbugs** | ✅ Ready ([Inspect](https://vercel.com/rocicorp/zbugs/6cSotXDB8Dd8334zDfqxARz6SXsp)) | [Visit Preview](https://zbugs-git-arv-zero-inspect-rocicorp.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/zbugs-git-arv-zero-inspect-rocicorp.vercel.app?via=pr-comment-feedback-link) | Mar 20, 2025 11:41am |



author_association: NONE
user: login: arv
created_at: 2025-03-11T16:25:54Z
body: The API could use some polishing
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-11T16:27:32Z
body: <h2><a href="https://bencher.dev/perf/zero/reports/851c942a-6087-4bd8-9e6c-0db223ec92fe?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero"><img src="https://bencher.dev/favicon.svg" width="24" height="24" alt="🐰" /> Bencher Report</a></h2><table><tr><td>Branch</td><td><a href="https://bencher.dev/perf/zero/branches/arv-zero-inspect?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">arv/zero-inspect</a></td></tr><tr><td>Testbed</td><td><a href="https://bencher.dev/perf/zero/testbeds/localhost?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">localhost</a></td></tr></table><details><summary>Click to view all benchmark results</summary><br /><table><thead><tr><th>Benchmark</th><th><a href="https://bencher.dev/perf/zero/measures/file-size?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">File Size</a></th><th>Benchmark Result<br />kilobytes (KB)<br />(Result Δ%)</th><th>Upper Boundary<br />kilobytes (KB)<br />(Limit %)</th></tr></thead><tbody><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/zero-package-tgz?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">zero-package.tgz</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=99740f90-30ab-4fbf-bcda-156b6dbda6a4&heads=bb6ceb56-b61b-404d-980f-f2a0482a1db3&testbeds=50a21deb-8296-4657-b7ad-5a4002b77d68&benchmarks=2bcdd867-a170-440b-8c18-ee15b6476b90&measures=8d8dbe98-8c7a-4838-b351-cd9d621320fe&start_time=1739879146000&end_time=1742471146000&report=851c942a-6087-4bd8-9e6c-0db223ec92fe&upper_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/7995ff56-8672-4e7f-bdbb-76a684d5ec08?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>1,058.06 KB<br /><details><summary>(+1.09%)</summary>Baseline: 1,046.62 KB</details></td><td>1,067.56 KB<br />(99.11%)</td></tr><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/zero-js?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">zero.js</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=99740f90-30ab-4fbf-bcda-156b6dbda6a4&heads=bb6ceb56-b61b-404d-980f-f2a0482a1db3&testbeds=50a21deb-8296-4657-b7ad-5a4002b77d68&benchmarks=4f172dbf-1d31-4809-9c2d-12c79ed4c9d4&measures=8d8dbe98-8c7a-4838-b351-cd9d621320fe&start_time=1739879146000&end_time=1742471146000&report=851c942a-6087-4bd8-9e6c-0db223ec92fe&upper_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/7995ff56-8672-4e7f-bdbb-76a684d5ec08?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>188.18 KB<br /><details><summary>(+0.01%)</summary>Baseline: 188.16 KB</details></td><td>191.93 KB<br />(98.05%)</td></tr><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/zero-js-br?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">zero.js.br</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=99740f90-30ab-4fbf-bcda-156b6dbda6a4&heads=bb6ceb56-b61b-404d-980f-f2a0482a1db3&testbeds=50a21deb-8296-4657-b7ad-5a4002b77d68&benchmarks=f9d12b7c-f48c-41fc-a662-5019d902c9f9&measures=8d8dbe98-8c7a-4838-b351-cd9d621320fe&start_time=1739879146000&end_time=1742471146000&report=851c942a-6087-4bd8-9e6c-0db223ec92fe&upper_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/7995ff56-8672-4e7f-bdbb-76a684d5ec08?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>52.81 KB<br /><details><summary>(+0.00%)</summary>Baseline: 52.81 KB</details></td><td>53.86 KB<br />(98.04%)</td></tr></tbody></table></details><a href="https://bencher.dev/perf/zero/reports/851c942a-6087-4bd8-9e6c-0db223ec92fe?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">🐰 View full continuous benchmarking report in Bencher</a><div id="bencher.dev/projects/zero/id/size"></div>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-11T16:28:20Z
body: <h2><a href="https://bencher.dev/perf/zero/reports/99875378-8bfb-482a-98ed-a43c4d0e6d13?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero"><img src="https://bencher.dev/favicon.svg" width="24" height="24" alt="🐰" /> Bencher Report</a></h2><table><tr><td>Branch</td><td><a href="https://bencher.dev/perf/zero/branches/arv-zero-inspect?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">arv/zero-inspect</a></td></tr><tr><td>Testbed</td><td><a href="https://bencher.dev/perf/zero/testbeds/localhost?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">localhost</a></td></tr></table><details><summary>Click to view all benchmark results</summary><br /><table><thead><tr><th>Benchmark</th><th><a href="https://bencher.dev/perf/zero/measures/throughput?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">Throughput</a></th><th>Benchmark Result<br />operations / second (ops/s)<br />(Result Δ%)</th><th>Lower Boundary<br />operations / second (ops/s)<br />(Limit %)</th></tr></thead><tbody><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/src-client-custom-bench-ts-big-schema?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">src/client/custom.bench.ts > big schema</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=99740f90-30ab-4fbf-bcda-156b6dbda6a4&heads=f940ebd7-afe9-4b12-a7a9-51ed80c397e3&testbeds=50a21deb-8296-4657-b7ad-5a4002b77d68&benchmarks=da25d28a-0b6b-4baa-8550-fb143072856d&measures=fc802276-c2ed-462d-9fa5-0c1fcf2deb8e&start_time=1739879222000&end_time=1742471222000&report=99875378-8bfb-482a-98ed-a43c4d0e6d13&lower_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/b73009cf-5664-4204-8fcd-9824d18aebd0?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>0.00 ops/s<br /><details><summary>(-12.38%)</summary>Baseline: 0.00 ops/s</details></td><td>0.00 ops/s<br />(71.01%)</td></tr><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/src-client-zero-bench-ts-basics-all-1000-rows-x-10-colu-rypc9f4e?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">src/client/zero.bench.ts > basics > All 1000 rows x 10 columns (numbers)</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=99740f90-30ab-4fbf-bcda-156b6dbda6a4&heads=f940ebd7-afe9-4b12-a7a9-51ed80c397e3&testbeds=50a21deb-8296-4657-b7ad-5a4002b77d68&benchmarks=8ff168e6-e598-4cc2-a9c7-e5d09c4aefec&measures=fc802276-c2ed-462d-9fa5-0c1fcf2deb8e&start_time=1739879222000&end_time=1742471222000&report=99875378-8bfb-482a-98ed-a43c4d0e6d13&lower_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/b73009cf-5664-4204-8fcd-9824d18aebd0?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>43.13 ops/s<br /><details><summary>(+0.97%)</summary>Baseline: 42.72 ops/s</details></td><td>39.57 ops/s<br />(91.74%)</td></tr><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/src-client-zero-bench-ts-with-filter-lower-rows-500-x-1-d9xjs9n1?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">src/client/zero.bench.ts > with filter > Lower rows 500 x 10 columns (numbers)</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=99740f90-30ab-4fbf-bcda-156b6dbda6a4&heads=f940ebd7-afe9-4b12-a7a9-51ed80c397e3&testbeds=50a21deb-8296-4657-b7ad-5a4002b77d68&benchmarks=ce024352-2a47-400d-b489-b9951bf428ac&measures=fc802276-c2ed-462d-9fa5-0c1fcf2deb8e&start_time=1739879222000&end_time=1742471222000&report=99875378-8bfb-482a-98ed-a43c4d0e6d13&lower_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/b73009cf-5664-4204-8fcd-9824d18aebd0?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>41.62 ops/s<br /><details><summary>(-14.90%)</summary>Baseline: 48.90 ops/s</details></td><td>38.19 ops/s<br />(91.77%)</td></tr></tbody></table></details><a href="https://bencher.dev/perf/zero/reports/99875378-8bfb-482a-98ed-a43c4d0e6d13?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">🐰 View full continuous benchmarking report in Bencher</a><div id="bencher.dev/projects/zero/id/perf"></div>
author_association: NONE
user: login: aboodman
created_at: 2025-03-11T17:44:13Z
body: > It'd be ideal if we can get a ZQL representation of the query. Granted it will not match their original query builder use but at least they won't have to think in two languages.

I noticed this too for various logging things. It'd be nice to have a astToZQL function somewhere.
author_association: CONTRIBUTOR
user: login: arv
created_at: 2025-03-13T16:09:59Z
body: <img width="1416" alt="image" src="https://github.com/user-attachments/assets/6508ddf9-0269-4343-a158-412aff434f83" />

author_association: CONTRIBUTOR