diff --git a/.changeset/clean-query-ownership.md b/.changeset/clean-query-ownership.md new file mode 100644 index 0000000000..b86854f275 --- /dev/null +++ b/.changeset/clean-query-ownership.md @@ -0,0 +1,5 @@ +--- +'@tanstack/query-db-collection': patch +--- + +Clean up empty query ownership state while preserving authoritative empty results and retained-row lifecycle behavior. diff --git a/.changeset/curly-planets-lead.md b/.changeset/curly-planets-lead.md new file mode 100644 index 0000000000..891ffbb522 --- /dev/null +++ b/.changeset/curly-planets-lead.md @@ -0,0 +1,6 @@ +--- +'@tanstack/powersync-db-collection': patch +--- + +Add attachments support via `TanStackDBAttachmentQueue`. This extends the PowerSync SDK's `AttachmentQueue` and backs it with +a TanStack DB collection, so file uploads/deletes are managed atomically alongside the relational data. diff --git a/.changeset/young-cats-initialize.md b/.changeset/young-cats-initialize.md new file mode 100644 index 0000000000..483c4fb569 --- /dev/null +++ b/.changeset/young-cats-initialize.md @@ -0,0 +1,7 @@ +--- +'@tanstack/query-db-collection': minor +--- + +Add eager collection support for TanStack Query `initialData` and `initialDataUpdatedAt`, including wrapped response projection and collection-local initialization on shared QueryClient instances. + +QueryClient-default `placeholderData` no longer materializes as collection rows, and QueryClient-default `initialData` no longer seeds on-demand subset observers. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..cc6f65a9e7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,146 @@ +# Contributing + +## Questions + +If you have questions about implementation details, help, or support, please use our dedicated community forum at [GitHub Discussions](https://github.com/TanStack/db/discussions). **PLEASE NOTE:** If you choose to open an issue for your question instead, your issue may be closed and redirected to the forum. + +## Reporting issues + +If you have found what you think is a bug, please [file an issue](https://github.com/TanStack/db/issues/new/choose). **PLEASE NOTE:** Issues that are identified as implementation questions or non-issues may be closed and redirected to [GitHub Discussions](https://github.com/TanStack/db/discussions). + +## Suggesting new features + +If you are here to suggest a feature, first create an issue if it does not already exist. From there, we can discuss use cases for the feature and how it could be implemented. + +## Development + +If you have been assigned to fix an issue or develop a new feature, please follow these steps to get started: + +- Fork this repository. +- Use the Node.js version mentioned in `.nvmrc`. + + ```bash + nvm use + ``` + +- Enable [Corepack](https://nodejs.org/api/corepack.html) so the [pnpm](https://pnpm.io/) version mentioned in `package.json` is used. + + ```bash + corepack enable + ``` + +- Install dependencies. + + ```bash + pnpm install + ``` + +- Build all packages. + + ```bash + pnpm build + ``` + +- Run tests. + + ```bash + pnpm test + ``` + +- Run linting. + + ```bash + pnpm lint + ``` + +- Implement your changes and tests in the relevant package or example. +- Document your changes in the appropriate doc page. +- Git stage your required changes and commit them. +- Submit a PR for review. + +### Editing the docs locally and previewing changes + +The documentation for all TanStack projects is hosted on [tanstack.com](https://tanstack.com), which is a TanStack Start application (https://github.com/TanStack/tanstack.com). You need to run this app locally to preview your changes in the `TanStack/db` docs. + +> [!NOTE] +> The website fetches doc pages from GitHub in production, and searches for them at `../db/docs` in development. Your local clone of `TanStack/db` needs to be in the same directory as the local clone of `TanStack/tanstack.com`. + +You can follow these steps to set up the docs for local development: + +1. Make a new directory called `tanstack`. + +```sh +mkdir tanstack +``` + +2. Enter that directory and clone the [`TanStack/db`](https://github.com/TanStack/db) and [`TanStack/tanstack.com`](https://github.com/TanStack/tanstack.com) repos. + +```sh +cd tanstack +git clone git@github.com:TanStack/db.git +# We probably don't need all the branches and commit history +# from the `tanstack.com` repo, so let's just create a shallow +# clone of the latest version of the `main` branch. +# Read more about shallow clones here: +# https://github.blog/2020-12-21-get-up-to-speed-with-partial-clone-and-shallow-clone/#user-content-shallow-clones +git clone git@github.com:TanStack/tanstack.com.git --depth=1 --single-branch --branch=main +``` + +> [!NOTE] +> Your `tanstack` directory should look like this: +> +> ```text +> tanstack/ +> | +> +-- db/ (<-- this directory cannot be called anything else!) +> | +> +-- tanstack.com/ +> ``` + +3. Enter the `tanstack/tanstack.com` directory, install the dependencies, and run the app in dev mode. + +```sh +cd tanstack.com +pnpm i +# The app will run on http://localhost:3000 by default +pnpm dev +``` + +4. Visit http://localhost:3000/db/latest/docs/overview in the browser and see the changes you make in `tanstack/db/docs` there. + +> [!WARNING] +> You will need to update `docs/config.json` if you add a new documentation page. + +### Running examples + +- Make sure you've installed dependencies in the repo's root directory. + + ```bash + pnpm install + ``` + +- If you want to run an example against your local changes, run the relevant package build/watch command from the repo root if needed. Otherwise, examples may run against the latest published TanStack DB release. + +- Run the example from the selected example directory. + + ```bash + pnpm dev + ``` + +#### Note on standalone execution + +If you want to run an example without installing dependencies for the whole repo, follow the instructions from the example's README.md file. It will then run against the latest TanStack DB release. + +## Changesets + +This repo uses [Changesets](https://github.com/changesets/changesets) to automate releases. If your PR should release a new package version (patch, minor, or major), please run `pnpm changeset` and commit the generated file. If your PR affects docs, examples, styles, etc., you probably don't need to generate a changeset. + +## Pull requests + +Maintainers merge pull requests by squashing all commits and editing the commit message if necessary using the GitHub user interface. + +Use an appropriate commit type. Be especially careful with breaking changes. + +## Releases + +For each new commit added to `main`, a GitHub Workflow is triggered which runs the [Changesets Action](https://github.com/changesets/action). This generates a preview PR showing the impact of all changesets. When this PR is merged, the package will be published to npm. diff --git a/docs/collections/powersync-collection.md b/docs/collections/powersync-collection.md index c8ddbabbbe..8799f96aa8 100644 --- a/docs/collections/powersync-collection.md +++ b/docs/collections/powersync-collection.md @@ -1099,4 +1099,171 @@ const liveQuery = createLiveQueryCollection({ completed: todo.completed, })), }) +``` + +## Attachments + +`@tanstack/powersync-db-collection` ships `TanStackDBAttachmentQueue`, an [`AttachmentQueue`](https://docs.powersync.com/usage/use-case-examples/attachments-files) whose file operations commit inside a TanStack DB collection transaction. This lets you create (or delete) an attachment and mutate a related collection row (for example, setting `lists.photo_id`) atomically in a single transaction, instead of issuing two independent writes. + +The queue extends PowerSync's `AttachmentQueue`, so the generic concepts are unchanged and documented once in the SDK. + +> This section only covers what is specific to the TanStack DB integration. For storage adapters (local and remote), the `AttachmentTable` schema primitive, error-handling/retry semantics, and the `startSync()` / `stopSync()` lifecycle, see the [PowerSync attachments documentation](https://docs.powersync.com/usage/use-case-examples/attachments-files). + +### Prerequisites + +These are standard PowerSync attachment requirements. See the SDK attachments docs for details. + +- An `AttachmentTable` in your schema: + + ```ts + import { AttachmentTable, Schema } from "@powersync/web" + + const APP_SCHEMA = new Schema({ + // ...your tables + attachments: new AttachmentTable(), + }) + ``` + +- A local storage adapter (such as `IndexDBFileSystemStorageAdapter` on web) and a remote storage adapter (an implementation of the SDK's `RemoteStorageAdapter`, for example backed by Supabase Storage). Both are generic to all attachment users. See the SDK docs for the available adapters and the remote-adapter contract. + +### 1. Create the attachments collection + +This is the piece that makes the integration TanStack-aware: a normal PowerSync collection over the attachments table. The queue reads and writes attachment records through it. + +```ts +import { createCollection } from "@tanstack/react-db" +import { powerSyncCollectionOptions } from "@tanstack/powersync-db-collection" + +const attachmentsCollection = createCollection( + powerSyncCollectionOptions({ + database: db, + table: APP_SCHEMA.props.attachments, + }) +) +``` + +### 2. Construct the queue + +Pass your collection as `attachmentsCollection` alongside the standard `AttachmentQueue` options. Only `attachmentsCollection` and `watchAttachments` (below) are specific to this package; `db`, `localStorage`, `remoteStorage`, and `errorHandler` are the usual SDK options. + +```ts +import { TanStackDBAttachmentQueue } from "@tanstack/powersync-db-collection" + +const attachmentQueue = new TanStackDBAttachmentQueue({ + db, + attachmentsCollection, // TanStack DB collection over your AttachmentTable + localStorage, // SDK local storage adapter + remoteStorage, // your RemoteStorageAdapter (see SDK docs) + watchAttachments, // see step 3 + errorHandler, // standard AttachmentQueue error handler (see SDK docs) +}) +``` + +Start and stop syncing with the standard `attachmentQueue.startSync()` / `attachmentQueue.stopSync()` lifecycle (see SDK docs), typically inside a React effect or provider. + +### 3. Tell the queue which attachments exist (`watchAttachments`) + +`watchAttachments` reports the set of attachment IDs your data currently references, so the queue knows what to download and what to archive. With TanStack DB you drive it from a live query: emit the initial state, then re-emit the complete set on every change, and clean up on abort. + +```ts +import { + createCollection, + isNull, + liveQueryCollectionOptions, + not, +} from "@tanstack/db" +import { WatchedAttachmentItem } from "@powersync/web" + +const watchAttachments = async (onUpdate, abortSignal) => { + // Every row in your data model that references an attachment. + const livePhotoIds = createCollection( + liveQueryCollectionOptions({ + query: (q) => + q + .from({ document: listsCollection }) + .where(({ document }) => not(isNull(document.photo_id))) + .select(({ document }) => ({ photo_id: document.photo_id })), + }) + ) + + const mapper = (item) => + ({ + id: item.photo_id, + fileExtension: "jpg", + }) satisfies WatchedAttachmentItem + + // 1. Report the initial set of referenced attachment IDs. + const initialState = await livePhotoIds.stateWhenReady() + onUpdate(Array.from(initialState.values()).map(mapper)) + + // 2. Re-emit the whole set on every change (the queue expects the holistic state). + livePhotoIds.subscribeChanges(() => { + onUpdate(livePhotoIds.map(mapper)) + }) + + // 3. Clean up when sync stops. + abortSignal.addEventListener("abort", () => livePhotoIds.cleanup(), { + once: true, + }) +} +``` + +> A `watchAttachmentsFromQuery(...)` convenience helper that collapses this boilerplate into a single call is planned. Until then, use the pattern above. + +### 4. Save an attachment atomically with related data + +`save` writes the file, inserts the attachment record into your collection, and runs your `updateHook` mutations in the same transaction. Use the hook to insert or update the row that references the new attachment, so both land together or not at all. + +```ts +await attachmentQueue.save({ + data, // file bytes (ArrayBuffer / base64, per your local adapter) + fileExtension: "jpg", + updateHook: async (attachmentRecord) => { + // Runs in the same transaction as the attachment insert. + listsCollection.insert({ + id: crypto.randomUUID(), + name, + created_at: new Date(), + owner_id: userID, + photo_id: attachmentRecord.id, // associate the row with the attachment + }) + }, +}) +``` + +### 5. Delete an attachment and detach it from the row + +`delete` queues the file for deletion and runs your `updateHook` in the same transaction. Clear the foreign key so the row and the attachment stay consistent. + +```ts +await attachmentQueue.delete({ + id: photo_id, + updateHook: async () => { + listsCollection.update(listId, (draft) => { + draft.photo_id = null + }) + }, +}) +``` + +### 6. Display attachments via a live-query join + +Join your attachments collection into a live query to read the local URI (the locally cached file path) alongside your domain rows: + +```ts +import { eq } from "@tanstack/db" + +const { data } = useLiveQuery((q) => + q + .from({ lists: listsCollection }) + .leftJoin({ attachment: attachmentsCollection }, ({ lists, attachment }) => + eq(lists.photo_id, attachment.id) + ) + .select(({ lists, attachment }) => ({ + id: lists.id, + name: lists.name, + photo_id: lists.photo_id, + attachment_local_uri: attachment?.local_uri, + })) +) ``` \ No newline at end of file diff --git a/docs/collections/query-collection.md b/docs/collections/query-collection.md index 73f3511edc..e55fb94138 100644 --- a/docs/collections/query-collection.md +++ b/docs/collections/query-collection.md @@ -54,19 +54,196 @@ The `queryCollectionOptions` function accepts the following options: - `queryClient`: TanStack Query client instance - `getKey`: Function to extract the unique key from an item +### Creating Collection Options from a Runtime QueryClient + +`queryCollectionOptions` needs a `queryClient` when the collection options are created. In SSR, TanStack Start, tests, or multi-tenant apps, that `QueryClient` is often request-local or route-local rather than module-global. + +Keep shared collection configuration in a factory function that accepts the runtime `QueryClient`: + +```typescript +import { QueryClient } from "@tanstack/query-core" +import { createCollection } from "@tanstack/db" +import { queryCollectionOptions } from "@tanstack/query-db-collection" + +interface Todo { + id: string + title: string +} + +export function todoCollectionOptions(queryClient: QueryClient) { + return queryCollectionOptions({ + queryKey: ["todos"], + queryFn: async () => { + const response = await fetch("/api/todos") + return response.json() as Promise> + }, + queryClient, + getKey: (todo) => todo.id, + }) +} + +function createTodosCollection(queryClient: QueryClient) { + return createCollection(todoCollectionOptions(queryClient)) +} +``` + +Create the collection once for each scoped `QueryClient` and parameter set, then reuse that `Collection` instance. Creating multiple collections with the same `QueryClient` and `queryKey` gives each collection its own materialized state, lifecycle, subscriptions, and optimistic mutations. + +In request-scoped environments, store the collection in request or router context. For client-side scopes, memoize by `QueryClient`: + +```typescript +type TodosCollection = ReturnType + +const collectionsByClient = new WeakMap() + +export function getTodosCollection( + queryClient: QueryClient, +): TodosCollection { + let collection = collectionsByClient.get(queryClient) + + if (!collection) { + collection = createTodosCollection(queryClient) + collectionsByClient.set(queryClient, collection) + } + + return collection +} +``` + +Avoid calling `createCollection(todoCollectionOptions(queryClient))` independently during render or in each consumer. Share the stable collection instance for the lifetime of that `QueryClient` scope. + +This keeps SSR and request-scoped code from sharing a global `QueryClient` while keeping each collection instance stable within its scope. + +### Business-Scoped Collection Factories + +A tenant, project, account, or route parameter can define a **business scope**: the server resource that a collection represents. Include the scope in both the Query key and `queryFn`. This extends the [runtime `QueryClient` factory pattern](#creating-collection-options-from-a-runtime-queryclient) with an explicit scope parameter: + +```typescript +interface Todo { + id: string + title: string + projectId: string +} + +async function fetchProjectTodos(projectId: string): Promise> { + const response = await fetch(`/api/projects/${projectId}/todos`) + return response.json() +} + +export function createProjectTodosCollection( + queryClient: QueryClient, + projectId: string, +) { + return createCollection( + queryCollectionOptions({ + queryKey: ["projects", projectId, "todos"], + queryFn: () => fetchProjectTodos(projectId), + queryClient, + getKey: (todo) => todo.id, + }) + ) +} +``` + +The scope is part of the collection's identity. Memoize by both the `QueryClient` and a stable scope key so consumers of the same project share one collection: + +```typescript +type ProjectTodosCollection = ReturnType + +const projectCollections = new WeakMap< + QueryClient, + Map +>() + +export function getProjectTodosCollection( + queryClient: QueryClient, + projectId: string, +): ProjectTodosCollection { + let collectionsByProject = projectCollections.get(queryClient) + + if (!collectionsByProject) { + collectionsByProject = new Map() + projectCollections.set(queryClient, collectionsByProject) + } + + let collection = collectionsByProject.get(projectId) + + if (!collection) { + collection = createProjectTodosCollection(queryClient, projectId) + collectionsByProject.set(projectId, collection) + } + + return collection +} +``` + +For multiple scope values, use nested maps or a collision-safe stable key that includes every value. Do not call the factory on each render. In a long-lived client, user-selected scopes can make the map grow without bound. Remove unused entries and call `await collection.cleanup()` when your application owns their lifecycle. Request-local maps can be discarded with the request. + +A business scope is separate from a **relational subset** requested by a live query. With `syncMode: "on-demand"`, `LoadSubsetOptions` describes predicates, ordering, limits, and offsets within one business-scoped collection. These options reach `queryFn` through `ctx.meta.loadSubsetOptions` and determine the subset Query keys. See [QueryFn and Predicate Push-Down](#queryfn-and-predicate-push-down). + +Do not create a collection for each `where`, `orderBy`, or `limit`. Reuse the business-scoped collection and let on-demand loading represent those subsets. Create separate collections only for distinct server resources. + ### Query Options -- `select`: Function that lets extract array items when they're wrapped with metadata +Query Collections use TanStack Query internally and expose supported Query observer options as top-level `queryCollectionOptions` fields. + +The following top-level Query Collection options are forwarded to the underlying Query observer: + +- `select`: Function that extracts the row array TanStack DB materializes from a wrapped Query response - `enabled`: Whether the query should automatically run (default: `true`) -- `refetchInterval`: Refetch interval in milliseconds (default: 0 — set an interval to enable polling refetching) +- `refetchInterval`: Refetch interval in milliseconds - `retry`: Retry configuration for failed queries - `retryDelay`: Delay between retries - `staleTime`: How long data is considered fresh -- `meta`: Optional metadata that will be passed to the query function context +- `gcTime`: How long unused query data stays in the Query cache +- `refetchOnWindowFocus`: Whether to refetch when the window regains focus +- `refetchOnReconnect`: Whether to refetch when the network reconnects +- `refetchOnMount`: Whether to refetch when the observer mounts +- `networkMode`: Query network mode +- `initialData`: Initial Query response for eager collections +- `initialDataUpdatedAt`: Timestamp used by TanStack Query to determine initial data freshness +- `meta`: Metadata passed to the query function context. Query Collections may add `loadSubsetOptions` for on-demand queries. + +```ts +const todosCollection = createCollection( + queryCollectionOptions({ + queryKey: ["todos"], + queryFn: fetchTodos, + queryClient, + getKey: (todo) => todo.id, + refetchOnWindowFocus: true, + refetchOnReconnect: true, + refetchOnMount: "always", + networkMode: "online", + }) +) +``` + +Top-level `meta` is always merged by Query Collection so it can add on-demand `loadSubsetOptions`. Other supported top-level Query options are only passed to TanStack Query when you define them. If you omit them, `QueryClient.defaultOptions` can still apply. + +Some fields are owned or reinterpreted by the collection adapter rather than treated as ordinary Query option pass-through: + +- `queryKey`: Identifies the Query cache entry and, in on-demand mode, may be built from load-subset options. +- `queryFn`: Fetches the complete collection state or the requested on-demand subset. +- `select`: Extracts array rows from wrapped responses before they are stored in the collection. This is not the same contract as TanStack Query's `select` option. +- `queryClient`: Supplies the Query client instance used by the collection. +- `syncMode`: Controls whether the collection syncs eagerly or on demand. +- `getKey`: Extracts each row's stable TanStack DB key. +- Mutation handlers such as `onInsert`, `onUpdate`, and `onDelete`. + +Some TanStack Query fields are owned or reinterpreted by Query Collection and are intentionally not exposed as ordinary Query observer options: + +- `queryKey`, `queryFn`, and `queryClient` +- `select` (Query Collection uses this for row extraction, not TanStack Query's observer-level `select` contract) +- `meta` (merged by Query Collection so on-demand `loadSubsetOptions` can be included) +- `subscribed` (Query Collection owns the observer subscription lifecycle) +- `structuralSharing` and `notifyOnChangeProps` (managed by Query Collection synchronization) + +`placeholderData` is intentionally unsupported. TanStack Query treats placeholder data as observer-local presentation state rather than cached Query data. Materializing it would expose temporary UI data as collection-wide normalized rows. Render placeholders in the consuming UI instead. ### Using with `queryOptions(...)` -If your app already uses TanStack Query's `queryOptions` helper (e.g. from `@tanstack/react-query`), you can spread those options into `queryCollectionOptions`. Note that `queryFn` must be explicitly provided since query collections require it both in types and at runtime: +If your app already uses TanStack Query's `queryOptions` helper (e.g. from `@tanstack/react-query`), you can spread compatible top-level options into `queryCollectionOptions`. Note that `queryFn` must be explicitly provided since query collections require it both in types and at runtime, and Query Collection's `select` option is for row extraction rather than TanStack Query observer-level selection: ```typescript import { QueryClient } from "@tanstack/query-core" @@ -96,6 +273,99 @@ const todosCollection = createCollection( If `queryFn` is missing at runtime, `queryCollectionOptions` throws `QueryFnRequiredError`. +### Initial Data + +Eager Query Collections support TanStack Query's `initialData` and +`initialDataUpdatedAt` options. Initial data has the original Query response +shape, is stored in the Query cache, and is immediately materialized as +normalized collection rows. TanStack Query uses `initialDataUpdatedAt` together +with `staleTime` to decide whether to fetch. + +```typescript +const serverRenderedAt = Date.now() +const initialTodos = [ + { id: "1", title: "Write documentation" }, + { id: "2", title: "Ship initial data support" }, +] + +const todosCollection = createCollection( + queryCollectionOptions({ + queryKey: ["todos"], + queryFn: fetchTodos, + queryClient, + getKey: (todo) => todo.id, + initialData: initialTodos, + initialDataUpdatedAt: serverRenderedAt, + staleTime: 60_000, + }), +) +``` + +An existing cached or hydrated Query response takes precedence over +`initialData`. Query keys remain the cache identity: two collections using the +same QueryClient and exact Query key observe one shared Query document, and a +later collection's initializer does not replace it. Use distinct Query keys for +independent documents. + +Initial data is supported only for eager collections. A collection-wide value +cannot establish row membership for arbitrary on-demand predicates, ordering, +limits, and offsets. For `syncMode: "on-demand"`, seed or hydrate the exact +derived Query cache entries instead. + +If a stale initial response triggers a fetch, the initial rows remain available +while it is in flight. A successful response reconciles them through the normal +row ownership pipeline; an error retains the initial rows. Direct writes use the +same Query cache-patching rules as fetched data, and a later successful server +response may reconcile or replace those writes. + +### Selecting Rows from Wrapped Responses + +Many APIs return rows inside a response envelope that also contains metadata such as pagination cursors, totals, or request information. Use `select` to extract the row array that TanStack DB should materialize: + +```typescript +interface TodosResponse { + items: Array<{ id: string; title: string }> + nextCursor?: string + total: number +} + +const todosCollection = createCollection( + queryCollectionOptions({ + queryKey: ["todos"], + queryFn: async (): Promise => { + const response = await fetch("/api/todos") + return response.json() + }, + initialData: { + items: [{ id: "1", title: "Initial todo" }], + nextCursor: undefined, + total: 1, + }, + select: (response) => response.items, + queryClient, + getKey: (item) => item.id, + }), +) +``` + +`select` is a query-db-collection row extraction hook. It tells TanStack DB which rows to materialize while the TanStack Query cache keeps the original query response shape. In the example above, `queryClient.getQueryData(["todos"])` still returns the full `TodosResponse`, including `nextCursor` and `total`. + +The same projection applies to `initialData`: provide the complete response +envelope, and Query Collection materializes the rows returned by `select` while +preserving the envelope in the Query cache. + +This differs from TanStack Query's observer-level `select`: query-db-collection uses this option to bridge Query's response object into DB's normalized row store. + +Direct write utilities such as `writeInsert`, `writeUpdate`, and `writeDelete` make a best-effort attempt to update the matching row array inside wrapped Query cache entries while preserving wrapper metadata. + +This works automatically for simple wrappers such as: + +- `{ data: [...] }` +- `{ items: [...] }` +- `{ results: [...] }` + +Derived projections, such as `select: (response) => response.edges.map((edge) => edge.node)`, are read-side row extraction only. query-db-collection cannot generally reconstruct the original response envelope from updated rows. Refetch or invalidate the query if the wrapped cache must exactly reflect direct writes for a derived projection. + ### Collection Options - `id`: Unique identifier for the collection diff --git a/docs/collections/query-initial-placeholder-data-design.md b/docs/collections/query-initial-placeholder-data-design.md new file mode 100644 index 0000000000..9ee866f325 --- /dev/null +++ b/docs/collections/query-initial-placeholder-data-design.md @@ -0,0 +1,249 @@ +# Query Collection initial and placeholder data semantics + +## Status and scope + +This document defines the semantics for TanStack Query `initialData` and +`placeholderData` at the `@tanstack/query-db-collection` boundary. It is the +design follow-up for [RFC #1643](https://github.com/TanStack/db/issues/1643) +and [issue #346](https://github.com/TanStack/db/issues/346). The accompanying +implementation adds the approved eager `initialData` behavior without changing +the persistence format. + +The adapter connects two different models: + +- TanStack Query owns a document cache and remote-query lifecycle. +- TanStack DB owns normalized rows, local queries, and optimistic writes. +- Query Collection projects a Query response into rows and records which Query + key owns each materialized row. + +`initialData` and `placeholderData` must not be treated as equivalent ways to +provide an array. In Query Core 5.90.20, `initialData` initializes Query cache +state with `status: "success"` and a `dataUpdatedAt` timestamp. By contrast, +`placeholderData` is computed per observer only while Query state is pending; +it produces an observer result with `isPlaceholderData: true` but is not stored +in Query state. + +## Decision + +Support Query-owned `initialData` as an additive, eager-mode option. Materialize +it immediately through the existing row extraction and ownership pipeline. +Do not expose or materialize `placeholderData` in the first implementation. + +Query Collection can already materialize data that was seeded or hydrated into +the QueryClient before the observer is created, and QueryClient defaults can +already provide `initialData` indirectly. That is useful existing behavior, but +it does not close the configuration gap. Applications commonly share one +QueryClient across many Query Collections: a client-wide default is too broad, +while imperative `setQueryData` requires coordinating collection construction, +exact Query keys, and initialization elsewhere. The additive field supplies a +collection-local declaration while leaving Query as the cache authority. + +The minimal additive API is: + +```ts +initialData?: TQueryData | (() => TQueryData) +initialDataUpdatedAt?: number | (() => number | undefined) +``` + +These remain flat top-level fields. Their types describe the original Query +response, not the extracted row array. Consequently, wrapped responses use the +same adapter `select` as network responses: + +```ts +queryCollectionOptions({ + queryKey: ["todos"], + queryFn: fetchTodos, + initialData: { items: serverTodos, nextCursor: null }, + select: (response) => response.items, + // ... +}) +``` + +Query keys still define cache identity. If two Query Collections on the same +QueryClient use the same exact key, they observe one shared Query document; +`initialData` initializes that document only when it does not already exist. +Collection-local configuration does not create collection-local cache data, and +later observers must not replace an existing document with their initializer. +Collections that require independent initial documents must use distinct keys. + +This initial API is limited to `syncMode: "eager"`. A single configuration-level +value cannot lawfully initialize an open-ended family of on-demand subset keys, +and Query's `initialData` function receives no query key or subset context. +Applications that already know data for an exact on-demand key should seed or +hydrate that Query cache entry instead. A future subset-aware initializer would +need an explicit key/subset argument and a separate design. + +`placeholderData` remains Query UI vocabulary. A DB collection has no +observer-local result surface: materializing a placeholder would make it visible +to every DB query and mutation, assign it row ownership, and potentially persist +it. Callers should render placeholders in the consuming UI. A future opt-in +temporary-row feature, if needed, should be a DB feature with explicit provenance +and lifecycle rather than Query's `placeholderData` option. + +## Authority model + +"Authoritative" has two dimensions here. Query owns the authoritative document +for a Query key, while DB owns the current normalized row state. A server result +is the newest remote snapshot, but local optimistic transactions can temporarily +overlay its rows. + +| Phase | Query document authority | Materialized row authority | Consequence | +| --- | --- | --- | --- | +| No cached data | None | Existing DB/persisted rows, if any | The collection waits for Query; absence is not an empty result. | +| Initial data | Query cache `initialData` | Its projected rows, subject to normal local overlays | It is a real cached snapshot, not temporary presentation data. | +| Fetch/refetch in flight | Existing Query document | Existing rows | Loading does not clear rows or ownership. | +| Server success | Returned response replaces the Query document | Projected server rows reconcile the owning Query key | Missing rows lose this Query owner's lease; shared rows remain. | +| Fetch/refetch error | Last successful Query document | Existing rows | An error does not retract initial or previously fetched rows. | +| Placeholder presentation | No Query document | No rows | Placeholder data is never passed to DB. | + +`initialDataUpdatedAt` and `staleTime` remain Query-owned. They decide whether a +fetch starts; the adapter does not reproduce their freshness calculation. An +initial value is therefore a seed snapshot with ordinary Query authority, not a +weaker class of row waiting to be promoted. The first successful server result +reconciles it through the same path as any later refetch. + +## Behavior matrix + +| Concern | `initialData` | `placeholderData` | +| --- | --- | --- | +| Query cache | Stored as successful Query data | Not stored; observer-only | +| Existing cache entry | Existing cached/hydrated data wins; `initialData` is not reapplied | Not applicable | +| DB materialization | Immediate in eager mode | Never | +| Wrapped response | Adapter `select(initialData)` extracts rows; original envelope stays cached | Unsupported | +| Function value | Evaluated by Query once when the Query is created | Not forwarded or evaluated by the adapter | +| Ownership | The Query key owns projected rows exactly like a server success | No ownership | +| Overlapping subsets | Not applicable to the initial eager-only API | Not applicable | +| Ready state | Initial successful result can make the collection ready synchronously | Cannot make the collection ready | +| Refetch success | Reconciles additions, updates, and removals normally | N/A | +| Refetch error | Initial rows and ownership remain; error state is reported | N/A | +| Cancellation/cleanup | Existing ownership cleanup rules apply; a cancelled fetch does not retract the cached seed | No rows to clean up | +| Query cache GC/unload | Existing Query-to-row ownership and persisted-retention rules apply | No effect | +| Query dehydration | Query owns persistence of the initial response | Never persisted | +| DB persistence/hydration | Rows and owner metadata use the existing format; no provenance tag is added | Never persisted or hydrated | +| Direct writes before server success | Allowed under the existing write rules below | No target rows exist | +| QueryClient defaults | Supported for eager `initialData`; see compatibility guard below | Must be suppressed at this adapter boundary | + +## Select and writes + +Adapter `select` remains a one-way row extractor. It is applied identically to +initial and network responses, and TanStack Query retains the original response +shape. Query observer-level `select` remains unsupported. + +Direct writes before the first server result follow the current authority rule: +they update DB immediately and may patch Query cache only when the reverse update +is lawful. A raw array can be replaced. For a wrapped response, the existing +best-effort patch is lawful only when `select` returns an array property of the +cached object by reference, allowing the wrapper to be preserved. A derived +projection such as `response.edges.map(...)` has no general reverse projection; +the adapter must leave that Query document unchanged and rely on invalidate or +refetch. It must never fabricate an envelope around rows. + +The next successful remote response remains authoritative for that Query key and +may overwrite a direct cache patch or normalized row value. Mutation handlers and +optimistic transaction barriers retain their existing semantics. + +## Ownership, persistence, and transitions + +Initial rows use the existing `queryToRows` and `rowToQueries` relationship. No +`seed`, `temporary`, or `placeholder` bit is added to a row. This keeps these +invariants intact: + +1. A successful result, whether initial or fetched, is a complete snapshot for + its Query key. +2. A row is deleted when a snapshot omits it only if no other Query key owns it. +3. Unload, cache GC, and collection cleanup remove only the relevant ownership. +4. A failed or cancelled fetch cannot turn the last successful snapshot into an + empty snapshot. +5. Persistence records only Query data and the existing row ownership metadata; + observer-only presentation state is never persisted. + +The expected transitions are: + +- **Initial, fresh:** materialize and become ready; do not fetch until Query's + normal freshness triggers say to do so. +- **Initial, stale:** materialize and become ready while Query fetches; success + reconciles the same ownership, and error retains the seed. +- **Loading without data:** keep the collection's prior independently owned or + hydrated rows; do not infer an empty result. +- **Placeholder to loading/success/error:** the placeholder is UI-only. DB sees + no transition until success; error leaves DB unchanged. +- **Cleanup before network completion:** existing cancellation and readiness + listener cleanup apply. A late result must not mutate a cleaned-up collection. + +## Defaults and compatibility guard + +Query Collection currently constructs a `QueryObserver`, so QueryClient defaults +can contain semantic fields even when Query Collection does not expose them. +Implementation must explicitly enforce this design after Query defaults are +resolved: + +- reject an explicitly configured `initialData` in on-demand mode; +- prevent default `initialData` from initializing on-demand subset observers; +- prevent explicit or default `placeholderData` from reaching all Query + Collection observers; +- continue to let omitted eager `initialData` and `initialDataUpdatedAt` inherit + QueryClient defaults; +- never copy a function-valued initializer into adapter metadata or persisted + ownership metadata. Query Core may own it as an option and stores only its + evaluated data in Query state. + +Silently materializing default placeholder data would violate the public +compatibility table even before a top-level field is added. The guard is therefore +a correctness fix, not support for placeholder semantics. + +Other Query-owned options keep their current classification. Query key creation, +adapter `select`, subscriptions, `notifyOnChangeProps`, and structural sharing +remain adapter-owned or reinterpreted. No nested `queryOptions`, runtime binding +API, subset deduplication, or lease manager is introduced by this design. + +## Rejected designs + +- **Forward both options mechanically.** `result.isSuccess` is true for a + placeholder observer result, so the current success handler would normalize + presentation-only data and give it durable-looking ownership. +- **Tag placeholder rows and later promote or delete them.** Tags would have to + survive collisions with real rows, overlapping observers, local writes, + unload, GC, persistence, and hydration. Query's observer-local placeholder has + no collection-wide lifetime that can drive those transitions safely. +- **Treat initial rows as unowned temporary rows.** Query considers initial data + real cached data. Bypassing normal ownership would leak rows or allow cleanup + of one Query to remove rows still represented by another. +- **Seed every on-demand key from one value.** A collection-level initializer + cannot prove membership in arbitrary predicate/order/limit subsets. +- **Create a wrapped response from selected rows.** A read projection is not an + inverse. Fabricating metadata, cursors, or edges corrupts Query cache meaning. +- **Persist initializer functions.** Functions are not structured-clone safe and + are runtime configuration, not data. + +## Implementation and test sequence + +Each behavior PR should begin with the named failing or characterization tests. + +1. **Guard the existing boundary.** Add focused tests proving that explicit and + QueryClient-default `placeholderData` never materialize, never mark the + collection ready, and never survive dehydration as data. Characterize current + default `initialData` behavior in eager and on-demand modes. Then suppress + placeholder and on-demand initialization when constructing observers. +2. **Add eager initial data.** Add the two flat typed fields and forward only + defined values so QueryClient defaults remain intact. Test static and function + values, fresh versus stale timestamps, synchronous readiness, fetch error, + cancellation, cleanup, an explicit on-demand configuration error, multiple + collections on one QueryClient, and same-key first-initializer-wins behavior. +3. **Lock projection and writes.** Test raw arrays, direct-property wrapped + responses, and derived projections. Verify the full initial envelope remains + in Query cache, lawful direct writes preserve it, unlawful reverse projection + does not fabricate one, and server success reconciles rows. +4. **Lock ownership.** Test initial-to-server row removal, overlapping ownership + with externally hydrated/persisted rows, GC/unload, remount within `gcTime`, + and late notification after cleanup. Reuse the existing ownership machinery; + do not add a second seed ownership map. +5. **Lock persistence.** Dehydrate and `structuredClone` initial raw and wrapped + data; hydrate Query and DB state; verify ownership reconciliation and that no + function appears in Query metadata or adapter persistence metadata. +6. **Document the shipped API.** Move the settled behavior into the Query Options, + row extraction, direct writes, on-demand, and persistence sections of the user + guide. Update RFC #1643 and close or narrow #346 only after these contracts ship. + +Placeholder materialization, subset-aware initialization, and a lawful general +reverse projection are separate future proposals. None is a prerequisite for the +minimal eager `initialData` API. diff --git a/docs/framework/angular/reference/functions/injectLiveQuery.md b/docs/framework/angular/reference/functions/injectLiveQuery.md index 5eb790a543..efe20157a4 100644 --- a/docs/framework/angular/reference/functions/injectLiveQuery.md +++ b/docs/framework/angular/reference/functions/injectLiveQuery.md @@ -11,7 +11,7 @@ title: injectLiveQuery function injectLiveQuery(options): InjectLiveQueryResult; ``` -Defined in: [index.ts:89](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L89) +Defined in: [index.ts:93](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L93) ### Type Parameters @@ -45,7 +45,7 @@ Defined in: [index.ts:89](https://github.com/TanStack/db/blob/main/packages/angu function injectLiveQuery(options): InjectLiveQueryResult; ``` -Defined in: [index.ts:99](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L99) +Defined in: [index.ts:103](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L103) ### Type Parameters @@ -79,7 +79,7 @@ Defined in: [index.ts:99](https://github.com/TanStack/db/blob/main/packages/angu function injectLiveQuery(queryFn): InjectLiveQueryResult; ``` -Defined in: [index.ts:109](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L109) +Defined in: [index.ts:113](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L113) ### Type Parameters @@ -103,7 +103,7 @@ Defined in: [index.ts:109](https://github.com/TanStack/db/blob/main/packages/ang function injectLiveQuery(queryFn): InjectLiveQueryResult; ``` -Defined in: [index.ts:112](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L112) +Defined in: [index.ts:116](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L116) ### Type Parameters @@ -127,7 +127,7 @@ Defined in: [index.ts:112](https://github.com/TanStack/db/blob/main/packages/ang function injectLiveQuery(config): InjectLiveQueryResult; ``` -Defined in: [index.ts:117](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L117) +Defined in: [index.ts:121](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L121) ### Type Parameters @@ -151,7 +151,7 @@ Defined in: [index.ts:117](https://github.com/TanStack/db/blob/main/packages/ang function injectLiveQuery(liveQueryCollection): InjectLiveQueryResultWithCollection; ``` -Defined in: [index.ts:121](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L121) +Defined in: [index.ts:125](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L125) ### Type Parameters @@ -183,7 +183,7 @@ Defined in: [index.ts:121](https://github.com/TanStack/db/blob/main/packages/ang function injectLiveQuery(liveQueryCollection): InjectLiveQueryResultWithSingleResultCollection; ``` -Defined in: [index.ts:129](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L129) +Defined in: [index.ts:133](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L133) ### Type Parameters diff --git a/docs/framework/angular/reference/interfaces/InjectLiveQueryResult.md b/docs/framework/angular/reference/interfaces/InjectLiveQueryResult.md index 066d6b5bab..cf4d8f3cee 100644 --- a/docs/framework/angular/reference/interfaces/InjectLiveQueryResult.md +++ b/docs/framework/angular/reference/interfaces/InjectLiveQueryResult.md @@ -5,7 +5,7 @@ title: InjectLiveQueryResult # Interface: InjectLiveQueryResult\ -Defined in: [index.ts:32](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L32) +Defined in: [index.ts:36](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L36) The result of calling `injectLiveQuery`. Contains reactive signals for the query state and data. @@ -27,7 +27,7 @@ collection: Signal< | null>; ``` -Defined in: [index.ts:38](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L38) +Defined in: [index.ts:42](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L42) A signal containing the underlying collection instance (null for disabled queries) @@ -39,7 +39,7 @@ A signal containing the underlying collection instance (null for disabled querie data: Signal>; ``` -Defined in: [index.ts:36](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L36) +Defined in: [index.ts:40](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L40) A signal containing the results as an array, or single result for findOne queries @@ -51,7 +51,7 @@ A signal containing the results as an array, or single result for findOne querie isCleanedUp: Signal; ``` -Defined in: [index.ts:54](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L54) +Defined in: [index.ts:58](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L58) A signal indicating whether the collection has been cleaned up @@ -63,7 +63,7 @@ A signal indicating whether the collection has been cleaned up isError: Signal; ``` -Defined in: [index.ts:52](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L52) +Defined in: [index.ts:56](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L56) A signal indicating whether the collection has an error @@ -75,7 +75,7 @@ A signal indicating whether the collection has an error isIdle: Signal; ``` -Defined in: [index.ts:50](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L50) +Defined in: [index.ts:54](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L54) A signal indicating whether the collection is idle @@ -87,7 +87,7 @@ A signal indicating whether the collection is idle isLoading: Signal; ``` -Defined in: [index.ts:46](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L46) +Defined in: [index.ts:50](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L50) A signal indicating whether the collection is currently loading @@ -99,7 +99,7 @@ A signal indicating whether the collection is currently loading isReady: Signal; ``` -Defined in: [index.ts:48](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L48) +Defined in: [index.ts:52](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L52) A signal indicating whether the collection is ready @@ -111,7 +111,7 @@ A signal indicating whether the collection is ready state: Signal[K] }>>; ``` -Defined in: [index.ts:34](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L34) +Defined in: [index.ts:38](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L38) A signal containing the complete state map of results keyed by their ID @@ -123,6 +123,6 @@ A signal containing the complete state map of results keyed by their ID status: Signal; ``` -Defined in: [index.ts:44](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L44) +Defined in: [index.ts:48](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L48) A signal containing the current status of the collection diff --git a/docs/framework/angular/reference/interfaces/InjectLiveQueryResultWithCollection.md b/docs/framework/angular/reference/interfaces/InjectLiveQueryResultWithCollection.md index c6f1c6c3bc..06cefc8bc6 100644 --- a/docs/framework/angular/reference/interfaces/InjectLiveQueryResultWithCollection.md +++ b/docs/framework/angular/reference/interfaces/InjectLiveQueryResultWithCollection.md @@ -5,7 +5,7 @@ title: InjectLiveQueryResultWithCollection # Interface: InjectLiveQueryResultWithCollection\ -Defined in: [index.ts:57](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L57) +Defined in: [index.ts:61](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L61) ## Type Parameters @@ -32,7 +32,7 @@ collection: Signal< | null>; ``` -Defined in: [index.ts:64](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L64) +Defined in: [index.ts:68](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L68) *** @@ -42,7 +42,7 @@ Defined in: [index.ts:64](https://github.com/TanStack/db/blob/main/packages/angu data: Signal; ``` -Defined in: [index.ts:63](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L63) +Defined in: [index.ts:67](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L67) *** @@ -52,7 +52,7 @@ Defined in: [index.ts:63](https://github.com/TanStack/db/blob/main/packages/angu isCleanedUp: Signal; ``` -Defined in: [index.ts:70](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L70) +Defined in: [index.ts:74](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L74) *** @@ -62,7 +62,7 @@ Defined in: [index.ts:70](https://github.com/TanStack/db/blob/main/packages/angu isError: Signal; ``` -Defined in: [index.ts:69](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L69) +Defined in: [index.ts:73](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L73) *** @@ -72,7 +72,7 @@ Defined in: [index.ts:69](https://github.com/TanStack/db/blob/main/packages/angu isIdle: Signal; ``` -Defined in: [index.ts:68](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L68) +Defined in: [index.ts:72](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L72) *** @@ -82,7 +82,7 @@ Defined in: [index.ts:68](https://github.com/TanStack/db/blob/main/packages/angu isLoading: Signal; ``` -Defined in: [index.ts:66](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L66) +Defined in: [index.ts:70](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L70) *** @@ -92,7 +92,7 @@ Defined in: [index.ts:66](https://github.com/TanStack/db/blob/main/packages/angu isReady: Signal; ``` -Defined in: [index.ts:67](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L67) +Defined in: [index.ts:71](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L71) *** @@ -102,7 +102,7 @@ Defined in: [index.ts:67](https://github.com/TanStack/db/blob/main/packages/angu state: Signal>; ``` -Defined in: [index.ts:62](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L62) +Defined in: [index.ts:66](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L66) *** @@ -112,4 +112,4 @@ Defined in: [index.ts:62](https://github.com/TanStack/db/blob/main/packages/angu status: Signal; ``` -Defined in: [index.ts:65](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L65) +Defined in: [index.ts:69](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L69) diff --git a/docs/framework/angular/reference/interfaces/InjectLiveQueryResultWithSingleResultCollection.md b/docs/framework/angular/reference/interfaces/InjectLiveQueryResultWithSingleResultCollection.md index 2cf88f2856..29daff2673 100644 --- a/docs/framework/angular/reference/interfaces/InjectLiveQueryResultWithSingleResultCollection.md +++ b/docs/framework/angular/reference/interfaces/InjectLiveQueryResultWithSingleResultCollection.md @@ -5,7 +5,7 @@ title: InjectLiveQueryResultWithSingleResultCollection # Interface: InjectLiveQueryResultWithSingleResultCollection\ -Defined in: [index.ts:73](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L73) +Defined in: [index.ts:77](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L77) ## Type Parameters @@ -32,7 +32,7 @@ collection: Signal< | null>; ``` -Defined in: [index.ts:80](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L80) +Defined in: [index.ts:84](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L84) *** @@ -42,7 +42,7 @@ Defined in: [index.ts:80](https://github.com/TanStack/db/blob/main/packages/angu data: Signal; ``` -Defined in: [index.ts:79](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L79) +Defined in: [index.ts:83](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L83) *** @@ -52,7 +52,7 @@ Defined in: [index.ts:79](https://github.com/TanStack/db/blob/main/packages/angu isCleanedUp: Signal; ``` -Defined in: [index.ts:86](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L86) +Defined in: [index.ts:90](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L90) *** @@ -62,7 +62,7 @@ Defined in: [index.ts:86](https://github.com/TanStack/db/blob/main/packages/angu isError: Signal; ``` -Defined in: [index.ts:85](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L85) +Defined in: [index.ts:89](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L89) *** @@ -72,7 +72,7 @@ Defined in: [index.ts:85](https://github.com/TanStack/db/blob/main/packages/angu isIdle: Signal; ``` -Defined in: [index.ts:84](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L84) +Defined in: [index.ts:88](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L88) *** @@ -82,7 +82,7 @@ Defined in: [index.ts:84](https://github.com/TanStack/db/blob/main/packages/angu isLoading: Signal; ``` -Defined in: [index.ts:82](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L82) +Defined in: [index.ts:86](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L86) *** @@ -92,7 +92,7 @@ Defined in: [index.ts:82](https://github.com/TanStack/db/blob/main/packages/angu isReady: Signal; ``` -Defined in: [index.ts:83](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L83) +Defined in: [index.ts:87](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L87) *** @@ -102,7 +102,7 @@ Defined in: [index.ts:83](https://github.com/TanStack/db/blob/main/packages/angu state: Signal>; ``` -Defined in: [index.ts:78](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L78) +Defined in: [index.ts:82](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L82) *** @@ -112,4 +112,4 @@ Defined in: [index.ts:78](https://github.com/TanStack/db/blob/main/packages/angu status: Signal; ``` -Defined in: [index.ts:81](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L81) +Defined in: [index.ts:85](https://github.com/TanStack/db/blob/main/packages/angular-db/src/index.ts#L85) diff --git a/docs/framework/react/reference/functions/useLiveQuery.md b/docs/framework/react/reference/functions/useLiveQuery.md index accc95e0fe..15c4a155b3 100644 --- a/docs/framework/react/reference/functions/useLiveQuery.md +++ b/docs/framework/react/reference/functions/useLiveQuery.md @@ -11,7 +11,7 @@ title: useLiveQuery function useLiveQuery(queryFn, deps?): object; ``` -Defined in: [useLiveQuery.ts:84](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L84) +Defined in: [useLiveQuery.ts:85](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L85) Create a live query using a query function @@ -168,7 +168,7 @@ return ( function useLiveQuery(queryFn, deps?): object; ``` -Defined in: [useLiveQuery.ts:101](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L101) +Defined in: [useLiveQuery.ts:102](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L102) Create a live query using a query function @@ -329,7 +329,7 @@ return ( function useLiveQuery(queryFn, deps?): object; ``` -Defined in: [useLiveQuery.ts:120](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L120) +Defined in: [useLiveQuery.ts:121](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L121) Create a live query using a query function @@ -493,7 +493,7 @@ return ( function useLiveQuery(queryFn, deps?): object; ``` -Defined in: [useLiveQuery.ts:139](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L139) +Defined in: [useLiveQuery.ts:140](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L140) Create a live query using a query function @@ -662,7 +662,7 @@ return ( function useLiveQuery(queryFn, deps?): object; ``` -Defined in: [useLiveQuery.ts:162](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L162) +Defined in: [useLiveQuery.ts:163](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L163) Create a live query using a query function @@ -842,7 +842,7 @@ return ( function useLiveQuery(config, deps?): object; ``` -Defined in: [useLiveQuery.ts:230](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L230) +Defined in: [useLiveQuery.ts:231](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L231) Create a live query using configuration object @@ -972,7 +972,7 @@ return
{data.length} items loaded
function useLiveQuery(liveQueryCollection): object; ``` -Defined in: [useLiveQuery.ts:276](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L276) +Defined in: [useLiveQuery.ts:277](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L277) Subscribe to an existing live query collection @@ -1100,7 +1100,7 @@ return
{data.map(item => )}
function useLiveQuery(liveQueryCollection): object; ``` -Defined in: [useLiveQuery.ts:296](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L296) +Defined in: [useLiveQuery.ts:297](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L297) Create a live query using a query function diff --git a/docs/framework/react/reference/type-aliases/UseLiveQueryStatus.md b/docs/framework/react/reference/type-aliases/UseLiveQueryStatus.md index 93bbcd634b..24c41b5bb1 100644 --- a/docs/framework/react/reference/type-aliases/UseLiveQueryStatus.md +++ b/docs/framework/react/reference/type-aliases/UseLiveQueryStatus.md @@ -9,4 +9,4 @@ title: UseLiveQueryStatus type UseLiveQueryStatus = CollectionStatus | "disabled"; ``` -Defined in: [useLiveQuery.ts:23](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L23) +Defined in: [useLiveQuery.ts:24](https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts#L24) diff --git a/docs/framework/svelte/reference/functions/useLiveQuery.md b/docs/framework/svelte/reference/functions/useLiveQuery.md index 1ad0b46d1b..d251a36b7c 100644 --- a/docs/framework/svelte/reference/functions/useLiveQuery.md +++ b/docs/framework/svelte/reference/functions/useLiveQuery.md @@ -11,7 +11,7 @@ title: useLiveQuery function useLiveQuery(queryFn, deps?): UseLiveQueryReturn<{ [K in string | number | symbol]: ResultValue[K] }, InferResultType>; ``` -Defined in: [useLiveQuery.svelte.ts:160](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L160) +Defined in: [useLiveQuery.svelte.ts:164](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L164) Create a live query using a query function @@ -137,7 +137,7 @@ const todosQuery = useLiveQuery((q) => function useLiveQuery(queryFn, deps?): UseLiveQueryReturn<{ [K in string | number | symbol]: ResultValue[K] }, InferResultType | undefined>; ``` -Defined in: [useLiveQuery.svelte.ts:166](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L166) +Defined in: [useLiveQuery.svelte.ts:170](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L170) Create a live query using a query function @@ -263,7 +263,7 @@ const todosQuery = useLiveQuery((q) => function useLiveQuery(config, deps?): UseLiveQueryReturn<{ [K in string | number | symbol]: ResultValue[K] }, InferResultType>; ``` -Defined in: [useLiveQuery.svelte.ts:214](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L214) +Defined in: [useLiveQuery.svelte.ts:218](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L218) Create a live query using configuration object @@ -336,7 +336,7 @@ const itemsQuery = useLiveQuery({ function useLiveQuery(liveQueryCollection): UseLiveQueryReturnWithCollection; ``` -Defined in: [useLiveQuery.svelte.ts:263](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L263) +Defined in: [useLiveQuery.svelte.ts:267](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L267) Subscribe to an existing query collection (can be reactive) @@ -419,7 +419,7 @@ const queryResult = useLiveQuery(sharedQuery) function useLiveQuery(liveQueryCollection): UseLiveQueryReturnWithCollection; ``` -Defined in: [useLiveQuery.svelte.ts:274](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L274) +Defined in: [useLiveQuery.svelte.ts:278](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L278) Create a live query using a query function diff --git a/docs/framework/svelte/reference/interfaces/UseLiveQueryReturn.md b/docs/framework/svelte/reference/interfaces/UseLiveQueryReturn.md index 714736f4bf..741971a118 100644 --- a/docs/framework/svelte/reference/interfaces/UseLiveQueryReturn.md +++ b/docs/framework/svelte/reference/interfaces/UseLiveQueryReturn.md @@ -5,7 +5,7 @@ title: UseLiveQueryReturn # Interface: UseLiveQueryReturn\ -Defined in: [useLiveQuery.svelte.ts:33](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L33) +Defined in: [useLiveQuery.svelte.ts:37](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L37) Return type for useLiveQuery hook @@ -28,7 +28,7 @@ collection: Collection; ``` -Defined in: [useLiveQuery.svelte.ts:36](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L36) +Defined in: [useLiveQuery.svelte.ts:40](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L40) The underlying query collection instance @@ -40,7 +40,7 @@ The underlying query collection instance data: TData; ``` -Defined in: [useLiveQuery.svelte.ts:35](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L35) +Defined in: [useLiveQuery.svelte.ts:39](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L39) Reactive array of query results in order, or single item when using findOne() @@ -52,7 +52,7 @@ Reactive array of query results in order, or single item when using findOne() isCleanedUp: boolean; ``` -Defined in: [useLiveQuery.svelte.ts:42](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L42) +Defined in: [useLiveQuery.svelte.ts:46](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L46) True when query has been cleaned up @@ -64,7 +64,7 @@ True when query has been cleaned up isError: boolean; ``` -Defined in: [useLiveQuery.svelte.ts:41](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L41) +Defined in: [useLiveQuery.svelte.ts:45](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L45) True when query encountered an error @@ -76,7 +76,7 @@ True when query encountered an error isIdle: boolean; ``` -Defined in: [useLiveQuery.svelte.ts:40](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L40) +Defined in: [useLiveQuery.svelte.ts:44](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L44) True when query hasn't started yet @@ -88,7 +88,7 @@ True when query hasn't started yet isLoading: boolean; ``` -Defined in: [useLiveQuery.svelte.ts:38](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L38) +Defined in: [useLiveQuery.svelte.ts:42](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L42) True while initial query data is loading @@ -100,7 +100,7 @@ True while initial query data is loading isReady: boolean; ``` -Defined in: [useLiveQuery.svelte.ts:39](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L39) +Defined in: [useLiveQuery.svelte.ts:43](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L43) True when query has received first data and is ready @@ -112,7 +112,7 @@ True when query has received first data and is ready state: Map; ``` -Defined in: [useLiveQuery.svelte.ts:34](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L34) +Defined in: [useLiveQuery.svelte.ts:38](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L38) Reactive Map of query results (key → item) @@ -124,6 +124,6 @@ Reactive Map of query results (key → item) status: CollectionStatus; ``` -Defined in: [useLiveQuery.svelte.ts:37](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L37) +Defined in: [useLiveQuery.svelte.ts:41](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L41) Current query status diff --git a/docs/framework/svelte/reference/interfaces/UseLiveQueryReturnWithCollection.md b/docs/framework/svelte/reference/interfaces/UseLiveQueryReturnWithCollection.md index 0d62c7ae82..916174515c 100644 --- a/docs/framework/svelte/reference/interfaces/UseLiveQueryReturnWithCollection.md +++ b/docs/framework/svelte/reference/interfaces/UseLiveQueryReturnWithCollection.md @@ -5,7 +5,7 @@ title: UseLiveQueryReturnWithCollection # Interface: UseLiveQueryReturnWithCollection\ -Defined in: [useLiveQuery.svelte.ts:45](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L45) +Defined in: [useLiveQuery.svelte.ts:49](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L49) ## Type Parameters @@ -33,7 +33,7 @@ Defined in: [useLiveQuery.svelte.ts:45](https://github.com/TanStack/db/blob/main collection: Collection; ``` -Defined in: [useLiveQuery.svelte.ts:53](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L53) +Defined in: [useLiveQuery.svelte.ts:57](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L57) *** @@ -43,7 +43,7 @@ Defined in: [useLiveQuery.svelte.ts:53](https://github.com/TanStack/db/blob/main data: TData; ``` -Defined in: [useLiveQuery.svelte.ts:52](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L52) +Defined in: [useLiveQuery.svelte.ts:56](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L56) *** @@ -53,7 +53,7 @@ Defined in: [useLiveQuery.svelte.ts:52](https://github.com/TanStack/db/blob/main isCleanedUp: boolean; ``` -Defined in: [useLiveQuery.svelte.ts:59](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L59) +Defined in: [useLiveQuery.svelte.ts:63](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L63) *** @@ -63,7 +63,7 @@ Defined in: [useLiveQuery.svelte.ts:59](https://github.com/TanStack/db/blob/main isError: boolean; ``` -Defined in: [useLiveQuery.svelte.ts:58](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L58) +Defined in: [useLiveQuery.svelte.ts:62](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L62) *** @@ -73,7 +73,7 @@ Defined in: [useLiveQuery.svelte.ts:58](https://github.com/TanStack/db/blob/main isIdle: boolean; ``` -Defined in: [useLiveQuery.svelte.ts:57](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L57) +Defined in: [useLiveQuery.svelte.ts:61](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L61) *** @@ -83,7 +83,7 @@ Defined in: [useLiveQuery.svelte.ts:57](https://github.com/TanStack/db/blob/main isLoading: boolean; ``` -Defined in: [useLiveQuery.svelte.ts:55](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L55) +Defined in: [useLiveQuery.svelte.ts:59](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L59) *** @@ -93,7 +93,7 @@ Defined in: [useLiveQuery.svelte.ts:55](https://github.com/TanStack/db/blob/main isReady: boolean; ``` -Defined in: [useLiveQuery.svelte.ts:56](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L56) +Defined in: [useLiveQuery.svelte.ts:60](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L60) *** @@ -103,7 +103,7 @@ Defined in: [useLiveQuery.svelte.ts:56](https://github.com/TanStack/db/blob/main state: Map; ``` -Defined in: [useLiveQuery.svelte.ts:51](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L51) +Defined in: [useLiveQuery.svelte.ts:55](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L55) *** @@ -113,4 +113,4 @@ Defined in: [useLiveQuery.svelte.ts:51](https://github.com/TanStack/db/blob/main status: CollectionStatus; ``` -Defined in: [useLiveQuery.svelte.ts:54](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L54) +Defined in: [useLiveQuery.svelte.ts:58](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L58) diff --git a/docs/framework/vue/reference/functions/useLiveQuery.md b/docs/framework/vue/reference/functions/useLiveQuery.md index 71c717e542..97b3f0bfee 100644 --- a/docs/framework/vue/reference/functions/useLiveQuery.md +++ b/docs/framework/vue/reference/functions/useLiveQuery.md @@ -11,7 +11,7 @@ title: useLiveQuery function useLiveQuery(queryFn, deps?): UseLiveQueryReturn; ``` -Defined in: [useLiveQuery.ts:134](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L134) +Defined in: [useLiveQuery.ts:137](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L137) Create a live query using a query function @@ -97,7 +97,7 @@ const { data, isLoading, isError, status } = useLiveQuery((q) => function useLiveQuery(queryFn, deps?): UseLiveQueryReturn; ``` -Defined in: [useLiveQuery.ts:140](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L140) +Defined in: [useLiveQuery.ts:143](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L143) Create a live query using a query function @@ -183,7 +183,7 @@ const { data, isLoading, isError, status } = useLiveQuery((q) => function useLiveQuery(config, deps?): UseLiveQueryReturn; ``` -Defined in: [useLiveQuery.ts:180](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L180) +Defined in: [useLiveQuery.ts:183](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L183) Create a live query using configuration object @@ -251,7 +251,7 @@ const { data, isLoading, isReady, isError } = useLiveQuery({ function useLiveQuery(liveQueryCollection): UseLiveQueryReturnWithCollection; ``` -Defined in: [useLiveQuery.ts:225](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L225) +Defined in: [useLiveQuery.ts:228](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L228) Subscribe to an existing query collection (can be reactive) @@ -330,7 +330,7 @@ const { data, isLoading, isError } = useLiveQuery(sharedQuery) function useLiveQuery(liveQueryCollection): UseLiveQueryReturnWithSingleResultCollection; ``` -Defined in: [useLiveQuery.ts:236](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L236) +Defined in: [useLiveQuery.ts:239](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L239) Create a live query using a query function diff --git a/docs/framework/vue/reference/interfaces/UseLiveQueryReturn.md b/docs/framework/vue/reference/interfaces/UseLiveQueryReturn.md index d434f46cd7..927376b10d 100644 --- a/docs/framework/vue/reference/interfaces/UseLiveQueryReturn.md +++ b/docs/framework/vue/reference/interfaces/UseLiveQueryReturn.md @@ -5,7 +5,7 @@ title: UseLiveQueryReturn # Interface: UseLiveQueryReturn\ -Defined in: [useLiveQuery.ts:40](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L40) +Defined in: [useLiveQuery.ts:43](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L43) Return type for useLiveQuery hook @@ -24,7 +24,7 @@ collection: ComputedRef, { [K in string | number | symbol]: ResultValue[K] }>>; ``` -Defined in: [useLiveQuery.ts:43](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L43) +Defined in: [useLiveQuery.ts:46](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L46) The underlying query collection instance @@ -36,7 +36,7 @@ The underlying query collection instance data: ComputedRef>; ``` -Defined in: [useLiveQuery.ts:42](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L42) +Defined in: [useLiveQuery.ts:45](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L45) Reactive array of query results in order, or single result for findOne queries @@ -48,7 +48,7 @@ Reactive array of query results in order, or single result for findOne queries isCleanedUp: ComputedRef; ``` -Defined in: [useLiveQuery.ts:49](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L49) +Defined in: [useLiveQuery.ts:52](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L52) True when query has been cleaned up @@ -60,7 +60,7 @@ True when query has been cleaned up isError: ComputedRef; ``` -Defined in: [useLiveQuery.ts:48](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L48) +Defined in: [useLiveQuery.ts:51](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L51) True when query encountered an error @@ -72,7 +72,7 @@ True when query encountered an error isIdle: ComputedRef; ``` -Defined in: [useLiveQuery.ts:47](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L47) +Defined in: [useLiveQuery.ts:50](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L50) True when query hasn't started yet @@ -84,7 +84,7 @@ True when query hasn't started yet isLoading: ComputedRef; ``` -Defined in: [useLiveQuery.ts:45](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L45) +Defined in: [useLiveQuery.ts:48](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L48) True while initial query data is loading @@ -96,7 +96,7 @@ True while initial query data is loading isReady: ComputedRef; ``` -Defined in: [useLiveQuery.ts:46](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L46) +Defined in: [useLiveQuery.ts:49](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L49) True when query has received first data and is ready @@ -108,7 +108,7 @@ True when query has received first data and is ready state: ComputedRef[K] }>>; ``` -Defined in: [useLiveQuery.ts:41](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L41) +Defined in: [useLiveQuery.ts:44](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L44) Reactive Map of query results (key → item) @@ -120,6 +120,6 @@ Reactive Map of query results (key → item) status: ComputedRef; ``` -Defined in: [useLiveQuery.ts:44](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L44) +Defined in: [useLiveQuery.ts:47](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L47) Current query status diff --git a/docs/framework/vue/reference/interfaces/UseLiveQueryReturnWithCollection.md b/docs/framework/vue/reference/interfaces/UseLiveQueryReturnWithCollection.md index 8f63d93141..b0a79b7d68 100644 --- a/docs/framework/vue/reference/interfaces/UseLiveQueryReturnWithCollection.md +++ b/docs/framework/vue/reference/interfaces/UseLiveQueryReturnWithCollection.md @@ -5,7 +5,7 @@ title: UseLiveQueryReturnWithCollection # Interface: UseLiveQueryReturnWithCollection\ -Defined in: [useLiveQuery.ts:52](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L52) +Defined in: [useLiveQuery.ts:55](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L55) ## Type Parameters @@ -29,7 +29,7 @@ Defined in: [useLiveQuery.ts:52](https://github.com/TanStack/db/blob/main/packag collection: ComputedRef, T>>; ``` -Defined in: [useLiveQuery.ts:59](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L59) +Defined in: [useLiveQuery.ts:62](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L62) *** @@ -39,7 +39,7 @@ Defined in: [useLiveQuery.ts:59](https://github.com/TanStack/db/blob/main/packag data: ComputedRef; ``` -Defined in: [useLiveQuery.ts:58](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L58) +Defined in: [useLiveQuery.ts:61](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L61) *** @@ -49,7 +49,7 @@ Defined in: [useLiveQuery.ts:58](https://github.com/TanStack/db/blob/main/packag isCleanedUp: ComputedRef; ``` -Defined in: [useLiveQuery.ts:65](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L65) +Defined in: [useLiveQuery.ts:68](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L68) *** @@ -59,7 +59,7 @@ Defined in: [useLiveQuery.ts:65](https://github.com/TanStack/db/blob/main/packag isError: ComputedRef; ``` -Defined in: [useLiveQuery.ts:64](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L64) +Defined in: [useLiveQuery.ts:67](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L67) *** @@ -69,7 +69,7 @@ Defined in: [useLiveQuery.ts:64](https://github.com/TanStack/db/blob/main/packag isIdle: ComputedRef; ``` -Defined in: [useLiveQuery.ts:63](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L63) +Defined in: [useLiveQuery.ts:66](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L66) *** @@ -79,7 +79,7 @@ Defined in: [useLiveQuery.ts:63](https://github.com/TanStack/db/blob/main/packag isLoading: ComputedRef; ``` -Defined in: [useLiveQuery.ts:61](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L61) +Defined in: [useLiveQuery.ts:64](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L64) *** @@ -89,7 +89,7 @@ Defined in: [useLiveQuery.ts:61](https://github.com/TanStack/db/blob/main/packag isReady: ComputedRef; ``` -Defined in: [useLiveQuery.ts:62](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L62) +Defined in: [useLiveQuery.ts:65](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L65) *** @@ -99,7 +99,7 @@ Defined in: [useLiveQuery.ts:62](https://github.com/TanStack/db/blob/main/packag state: ComputedRef>; ``` -Defined in: [useLiveQuery.ts:57](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L57) +Defined in: [useLiveQuery.ts:60](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L60) *** @@ -109,4 +109,4 @@ Defined in: [useLiveQuery.ts:57](https://github.com/TanStack/db/blob/main/packag status: ComputedRef; ``` -Defined in: [useLiveQuery.ts:60](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L60) +Defined in: [useLiveQuery.ts:63](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L63) diff --git a/docs/framework/vue/reference/interfaces/UseLiveQueryReturnWithSingleResultCollection.md b/docs/framework/vue/reference/interfaces/UseLiveQueryReturnWithSingleResultCollection.md index 5b9502a979..98fb29f150 100644 --- a/docs/framework/vue/reference/interfaces/UseLiveQueryReturnWithSingleResultCollection.md +++ b/docs/framework/vue/reference/interfaces/UseLiveQueryReturnWithSingleResultCollection.md @@ -5,7 +5,7 @@ title: UseLiveQueryReturnWithSingleResultCollection # Interface: UseLiveQueryReturnWithSingleResultCollection\ -Defined in: [useLiveQuery.ts:68](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L68) +Defined in: [useLiveQuery.ts:71](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L71) ## Type Parameters @@ -29,7 +29,7 @@ Defined in: [useLiveQuery.ts:68](https://github.com/TanStack/db/blob/main/packag collection: ComputedRef, T> & SingleResult>; ``` -Defined in: [useLiveQuery.ts:75](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L75) +Defined in: [useLiveQuery.ts:78](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L78) *** @@ -39,7 +39,7 @@ Defined in: [useLiveQuery.ts:75](https://github.com/TanStack/db/blob/main/packag data: ComputedRef; ``` -Defined in: [useLiveQuery.ts:74](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L74) +Defined in: [useLiveQuery.ts:77](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L77) *** @@ -49,7 +49,7 @@ Defined in: [useLiveQuery.ts:74](https://github.com/TanStack/db/blob/main/packag isCleanedUp: ComputedRef; ``` -Defined in: [useLiveQuery.ts:81](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L81) +Defined in: [useLiveQuery.ts:84](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L84) *** @@ -59,7 +59,7 @@ Defined in: [useLiveQuery.ts:81](https://github.com/TanStack/db/blob/main/packag isError: ComputedRef; ``` -Defined in: [useLiveQuery.ts:80](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L80) +Defined in: [useLiveQuery.ts:83](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L83) *** @@ -69,7 +69,7 @@ Defined in: [useLiveQuery.ts:80](https://github.com/TanStack/db/blob/main/packag isIdle: ComputedRef; ``` -Defined in: [useLiveQuery.ts:79](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L79) +Defined in: [useLiveQuery.ts:82](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L82) *** @@ -79,7 +79,7 @@ Defined in: [useLiveQuery.ts:79](https://github.com/TanStack/db/blob/main/packag isLoading: ComputedRef; ``` -Defined in: [useLiveQuery.ts:77](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L77) +Defined in: [useLiveQuery.ts:80](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L80) *** @@ -89,7 +89,7 @@ Defined in: [useLiveQuery.ts:77](https://github.com/TanStack/db/blob/main/packag isReady: ComputedRef; ``` -Defined in: [useLiveQuery.ts:78](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L78) +Defined in: [useLiveQuery.ts:81](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L81) *** @@ -99,7 +99,7 @@ Defined in: [useLiveQuery.ts:78](https://github.com/TanStack/db/blob/main/packag state: ComputedRef>; ``` -Defined in: [useLiveQuery.ts:73](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L73) +Defined in: [useLiveQuery.ts:76](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L76) *** @@ -109,4 +109,4 @@ Defined in: [useLiveQuery.ts:73](https://github.com/TanStack/db/blob/main/packag status: ComputedRef; ``` -Defined in: [useLiveQuery.ts:76](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L76) +Defined in: [useLiveQuery.ts:79](https://github.com/TanStack/db/blob/main/packages/vue-db/src/useLiveQuery.ts#L79) diff --git a/docs/guides/live-queries.md b/docs/guides/live-queries.md index 0780871a3b..9b6ee18b06 100644 --- a/docs/guides/live-queries.md +++ b/docs/guides/live-queries.md @@ -728,6 +728,13 @@ not(condition) For a complete reference of all available functions, see the [Expression Functions Reference](#expression-functions-reference) section. +### Comparison semantics + +Comparisons follow SQL/PostgreSQL conventions rather than raw JavaScript: + +- **`null` / `undefined` use three-valued logic.** Any comparison involving `null` or `undefined` evaluates to `UNKNOWN`, so the row is not matched. For example `eq(user.score, null)` matches nothing — use a dedicated null check (e.g. `isUndefined`) to match missing values. +- **`NaN` follows PostgreSQL float semantics.** `NaN` is treated as equal to itself and greater than every other (non-null) value. So `eq(row.value, NaN)` matches `NaN` rows, `gt(row.value, x)` includes `NaN`, and ordering by such a field places `NaN` last. (Invalid `Date` values, whose timestamp is `NaN`, behave the same way.) This differs from JavaScript, where `NaN === NaN` is `false`, and matches how PostgreSQL orders and indexes floating-point values. + ## Select Use `select` to specify which fields to include in your results and transform your data. Without `select`, you get the full schema. @@ -1252,6 +1259,48 @@ const projectsWithIssues = createLiveQueryCollection((q) => With `toArray()`, the project row is re-emitted whenever its issues change. Without it, the child `Collection` updates independently. +### materialize + +`materialize()` is a single helper that covers both multi-row and single-row includes: + +- When the wrapped subquery returns multiple rows, the parent receives `Array` — same shape as `toArray()`. +- When the wrapped subquery ends in `.findOne()`, the parent receives `T | undefined` — a single object, or `undefined` when no child matches. + +This spares callers from unwrapping a singleton array whenever they know the child query yields at most one row. Reactive semantics match `toArray()`: the parent row is re-emitted whenever the underlying children change, including insert / update / delete transitions and rows moving in or out of a match. + +```ts +import { createLiveQueryCollection, eq, materialize } from '@tanstack/db' + +// Multi-row → issues: Array +const projectsWithIssues = createLiveQueryCollection((q) => + q.from({ p: projectsCollection }).select(({ p }) => ({ + ...p, + issues: materialize( + q + .from({ i: issuesCollection }) + .where(({ i }) => eq(i.projectId, p.id)), + ), + })), +) + +// Singleton → project: Project | undefined +const issuesWithProject = createLiveQueryCollection((q) => + q.from({ i: issuesCollection }).select(({ i }) => ({ + ...i, + project: materialize( + q + .from({ p: projectsCollection }) + .where(({ p }) => eq(p.id, i.projectId)) + .findOne(), + ), + })), +) +``` + +The singleton vs. array result type is inferred from whether the wrapped query ends in `.findOne()` — no extra type annotation is required. + +Like `toArray()`, `materialize()` is only valid as a top-level value in `.select()` — it cannot be nested inside expression helpers such as `coalesce()` or `eq()`. + ### Aggregates You can use aggregate functions in child queries. Aggregates are computed per parent: @@ -2551,6 +2600,54 @@ Add two numbers: add(user.salary, user.bonus) ``` +#### `subtract(left, right)` +Subtract two numbers: +```ts +subtract(user.salary, user.deductions) +``` + +#### `multiply(left, right)` +Multiply two numbers: +```ts +multiply(item.price, item.quantity) +``` + +#### `divide(left, right)` +Divide two numbers (returns `null` on divide-by-zero): +```ts +divide(order.total, order.itemCount) +``` + +#### Computed Columns in orderBy + +You can use math functions directly in `orderBy` to sort by computed values. This is useful for ranking algorithms that combine multiple factors: + +```ts +import { subtract, multiply, divide } from '@tanstack/db' + +// HN-style ranking: balance rating with recency +// Date.now() is captured when this query is created. Recreate the query if +// you need the recency score to advance as time passes. +const rankedRecipes = createLiveQueryCollection((q) => + q + .from({ r: recipesCollection }) + .orderBy( + ({ r }) => + subtract( + multiply(r.rating, r.timesMade), // weighted rating + divide( + subtract(Date.now(), r.lastMadeAt), // time since last made + 3600000 * 24 // convert ms to days + ) + ), + 'desc' + ) + .limit(20) +) +``` + +> **Note:** When using computed expressions in `orderBy` with `limit()`, lazy loading optimization is skipped (all matching data is loaded first, then sorted). For large collections where this matters, consider pre-computing the ranking score as a stored field. + ### Utility Functions #### `coalesce(...values)` diff --git a/docs/reference/@tanstack/namespaces/IR/classes/Aggregate.md b/docs/reference/@tanstack/namespaces/IR/classes/Aggregate.md index 19066baac0..9cbcc566c8 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/Aggregate.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/Aggregate.md @@ -5,7 +5,7 @@ title: Aggregate # Class: Aggregate\ -Defined in: [packages/db/src/query/ir.ts:129](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L129) +Defined in: [packages/db/src/query/ir.ts:166](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L166) ## Extends @@ -25,7 +25,7 @@ Defined in: [packages/db/src/query/ir.ts:129](https://github.com/TanStack/db/blo new Aggregate(name, args): Aggregate; ``` -Defined in: [packages/db/src/query/ir.ts:131](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L131) +Defined in: [packages/db/src/query/ir.ts:168](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L168) #### Parameters @@ -55,7 +55,7 @@ BaseExpression.constructor readonly __returnType: T; ``` -Defined in: [packages/db/src/query/ir.ts:73](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L73) +Defined in: [packages/db/src/query/ir.ts:82](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L82) **`Internal`** @@ -75,7 +75,7 @@ BaseExpression.__returnType args: BasicExpression[]; ``` -Defined in: [packages/db/src/query/ir.ts:133](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L133) +Defined in: [packages/db/src/query/ir.ts:170](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L170) *** @@ -85,7 +85,7 @@ Defined in: [packages/db/src/query/ir.ts:133](https://github.com/TanStack/db/blo name: string; ``` -Defined in: [packages/db/src/query/ir.ts:132](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L132) +Defined in: [packages/db/src/query/ir.ts:169](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L169) *** @@ -95,7 +95,7 @@ Defined in: [packages/db/src/query/ir.ts:132](https://github.com/TanStack/db/blo type: "agg"; ``` -Defined in: [packages/db/src/query/ir.ts:130](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L130) +Defined in: [packages/db/src/query/ir.ts:167](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L167) #### Overrides diff --git a/docs/reference/@tanstack/namespaces/IR/classes/CollectionRef.md b/docs/reference/@tanstack/namespaces/IR/classes/CollectionRef.md index 416a1c83e7..99c1b5ee85 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/CollectionRef.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/CollectionRef.md @@ -5,7 +5,7 @@ title: CollectionRef # Class: CollectionRef -Defined in: [packages/db/src/query/ir.ts:76](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L76) +Defined in: [packages/db/src/query/ir.ts:85](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L85) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/query/ir.ts:76](https://github.com/TanStack/db/blob new CollectionRef(collection, alias): CollectionRef; ``` -Defined in: [packages/db/src/query/ir.ts:78](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L78) +Defined in: [packages/db/src/query/ir.ts:87](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L87) #### Parameters @@ -49,7 +49,7 @@ BaseExpression.constructor readonly __returnType: any; ``` -Defined in: [packages/db/src/query/ir.ts:73](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L73) +Defined in: [packages/db/src/query/ir.ts:82](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L82) **`Internal`** @@ -69,7 +69,7 @@ BaseExpression.__returnType alias: string; ``` -Defined in: [packages/db/src/query/ir.ts:80](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L80) +Defined in: [packages/db/src/query/ir.ts:89](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L89) *** @@ -79,7 +79,7 @@ Defined in: [packages/db/src/query/ir.ts:80](https://github.com/TanStack/db/blob collection: CollectionImpl; ``` -Defined in: [packages/db/src/query/ir.ts:79](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L79) +Defined in: [packages/db/src/query/ir.ts:88](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L88) *** @@ -89,7 +89,7 @@ Defined in: [packages/db/src/query/ir.ts:79](https://github.com/TanStack/db/blob type: "collectionRef"; ``` -Defined in: [packages/db/src/query/ir.ts:77](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L77) +Defined in: [packages/db/src/query/ir.ts:86](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L86) #### Overrides diff --git a/docs/reference/@tanstack/namespaces/IR/classes/ConditionalSelect.md b/docs/reference/@tanstack/namespaces/IR/classes/ConditionalSelect.md new file mode 100644 index 0000000000..278abc6aac --- /dev/null +++ b/docs/reference/@tanstack/namespaces/IR/classes/ConditionalSelect.md @@ -0,0 +1,98 @@ +--- +id: ConditionalSelect +title: ConditionalSelect +--- + +# Class: ConditionalSelect + +Defined in: [packages/db/src/query/ir.ts:204](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L204) + +## Extends + +- `BaseExpression` + +## Constructors + +### Constructor + +```ts +new ConditionalSelect(branches, defaultValue?): ConditionalSelect; +``` + +Defined in: [packages/db/src/query/ir.ts:206](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L206) + +#### Parameters + +##### branches + +[`ConditionalSelectBranch`](../type-aliases/ConditionalSelectBranch.md)[] + +##### defaultValue? + +[`SelectValueExpression`](../type-aliases/SelectValueExpression.md) + +#### Returns + +`ConditionalSelect` + +#### Overrides + +```ts +BaseExpression.constructor +``` + +## Properties + +### \_\_returnType + +```ts +readonly __returnType: any; +``` + +Defined in: [packages/db/src/query/ir.ts:82](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L82) + +**`Internal`** + +- Type brand for TypeScript inference + +#### Inherited from + +```ts +BaseExpression.__returnType +``` + +*** + +### branches + +```ts +branches: ConditionalSelectBranch[]; +``` + +Defined in: [packages/db/src/query/ir.ts:207](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L207) + +*** + +### defaultValue? + +```ts +optional defaultValue: SelectValueExpression; +``` + +Defined in: [packages/db/src/query/ir.ts:208](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L208) + +*** + +### type + +```ts +type: "conditionalSelect"; +``` + +Defined in: [packages/db/src/query/ir.ts:205](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L205) + +#### Overrides + +```ts +BaseExpression.type +``` diff --git a/docs/reference/@tanstack/namespaces/IR/classes/Func.md b/docs/reference/@tanstack/namespaces/IR/classes/Func.md index 4d6fb79848..f18f5015fb 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/Func.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/Func.md @@ -5,7 +5,7 @@ title: Func # Class: Func\ -Defined in: [packages/db/src/query/ir.ts:114](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L114) +Defined in: [packages/db/src/query/ir.ts:151](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L151) ## Extends @@ -25,7 +25,7 @@ Defined in: [packages/db/src/query/ir.ts:114](https://github.com/TanStack/db/blo new Func(name, args): Func; ``` -Defined in: [packages/db/src/query/ir.ts:116](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L116) +Defined in: [packages/db/src/query/ir.ts:153](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L153) #### Parameters @@ -55,7 +55,7 @@ BaseExpression.constructor readonly __returnType: T; ``` -Defined in: [packages/db/src/query/ir.ts:73](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L73) +Defined in: [packages/db/src/query/ir.ts:82](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L82) **`Internal`** @@ -75,7 +75,7 @@ BaseExpression.__returnType args: BasicExpression[]; ``` -Defined in: [packages/db/src/query/ir.ts:118](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L118) +Defined in: [packages/db/src/query/ir.ts:155](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L155) *** @@ -85,7 +85,7 @@ Defined in: [packages/db/src/query/ir.ts:118](https://github.com/TanStack/db/blo name: string; ``` -Defined in: [packages/db/src/query/ir.ts:117](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L117) +Defined in: [packages/db/src/query/ir.ts:154](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L154) *** @@ -95,7 +95,7 @@ Defined in: [packages/db/src/query/ir.ts:117](https://github.com/TanStack/db/blo type: "func"; ``` -Defined in: [packages/db/src/query/ir.ts:115](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L115) +Defined in: [packages/db/src/query/ir.ts:152](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L152) #### Overrides diff --git a/docs/reference/@tanstack/namespaces/IR/classes/IncludesSubquery.md b/docs/reference/@tanstack/namespaces/IR/classes/IncludesSubquery.md index 8d932c9ead..4da4ae8a77 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/IncludesSubquery.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/IncludesSubquery.md @@ -5,7 +5,7 @@ title: IncludesSubquery # Class: IncludesSubquery -Defined in: [packages/db/src/query/ir.ts:139](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L139) +Defined in: [packages/db/src/query/ir.ts:176](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L176) ## Extends @@ -27,7 +27,7 @@ new IncludesSubquery( scalarField?): IncludesSubquery; ``` -Defined in: [packages/db/src/query/ir.ts:141](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L141) +Defined in: [packages/db/src/query/ir.ts:178](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L178) #### Parameters @@ -81,7 +81,7 @@ BaseExpression.constructor readonly __returnType: any; ``` -Defined in: [packages/db/src/query/ir.ts:73](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L73) +Defined in: [packages/db/src/query/ir.ts:82](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L82) **`Internal`** @@ -101,7 +101,7 @@ BaseExpression.__returnType childCorrelationField: PropRef; ``` -Defined in: [packages/db/src/query/ir.ts:144](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L144) +Defined in: [packages/db/src/query/ir.ts:181](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L181) *** @@ -111,7 +111,7 @@ Defined in: [packages/db/src/query/ir.ts:144](https://github.com/TanStack/db/blo correlationField: PropRef; ``` -Defined in: [packages/db/src/query/ir.ts:143](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L143) +Defined in: [packages/db/src/query/ir.ts:180](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L180) *** @@ -121,7 +121,7 @@ Defined in: [packages/db/src/query/ir.ts:143](https://github.com/TanStack/db/blo fieldName: string; ``` -Defined in: [packages/db/src/query/ir.ts:145](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L145) +Defined in: [packages/db/src/query/ir.ts:182](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L182) *** @@ -131,7 +131,7 @@ Defined in: [packages/db/src/query/ir.ts:145](https://github.com/TanStack/db/blo materialization: IncludesMaterialization; ``` -Defined in: [packages/db/src/query/ir.ts:148](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L148) +Defined in: [packages/db/src/query/ir.ts:185](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L185) *** @@ -141,7 +141,7 @@ Defined in: [packages/db/src/query/ir.ts:148](https://github.com/TanStack/db/blo optional parentFilters: Where[]; ``` -Defined in: [packages/db/src/query/ir.ts:146](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L146) +Defined in: [packages/db/src/query/ir.ts:183](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L183) *** @@ -151,7 +151,7 @@ Defined in: [packages/db/src/query/ir.ts:146](https://github.com/TanStack/db/blo optional parentProjection: PropRef[]; ``` -Defined in: [packages/db/src/query/ir.ts:147](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L147) +Defined in: [packages/db/src/query/ir.ts:184](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L184) *** @@ -161,7 +161,7 @@ Defined in: [packages/db/src/query/ir.ts:147](https://github.com/TanStack/db/blo query: QueryIR; ``` -Defined in: [packages/db/src/query/ir.ts:142](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L142) +Defined in: [packages/db/src/query/ir.ts:179](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L179) *** @@ -171,7 +171,7 @@ Defined in: [packages/db/src/query/ir.ts:142](https://github.com/TanStack/db/blo optional scalarField: string; ``` -Defined in: [packages/db/src/query/ir.ts:149](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L149) +Defined in: [packages/db/src/query/ir.ts:186](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L186) *** @@ -181,7 +181,7 @@ Defined in: [packages/db/src/query/ir.ts:149](https://github.com/TanStack/db/blo type: "includesSubquery"; ``` -Defined in: [packages/db/src/query/ir.ts:140](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L140) +Defined in: [packages/db/src/query/ir.ts:177](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L177) #### Overrides diff --git a/docs/reference/@tanstack/namespaces/IR/classes/PropRef.md b/docs/reference/@tanstack/namespaces/IR/classes/PropRef.md index f9dd66cf12..734b812c81 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/PropRef.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/PropRef.md @@ -5,7 +5,7 @@ title: PropRef # Class: PropRef\ -Defined in: [packages/db/src/query/ir.ts:96](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L96) +Defined in: [packages/db/src/query/ir.ts:133](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L133) ## Extends @@ -25,7 +25,7 @@ Defined in: [packages/db/src/query/ir.ts:96](https://github.com/TanStack/db/blob new PropRef(path): PropRef; ``` -Defined in: [packages/db/src/query/ir.ts:98](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L98) +Defined in: [packages/db/src/query/ir.ts:135](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L135) #### Parameters @@ -51,7 +51,7 @@ BaseExpression.constructor readonly __returnType: T; ``` -Defined in: [packages/db/src/query/ir.ts:73](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L73) +Defined in: [packages/db/src/query/ir.ts:82](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L82) **`Internal`** @@ -71,7 +71,7 @@ BaseExpression.__returnType path: string[]; ``` -Defined in: [packages/db/src/query/ir.ts:99](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L99) +Defined in: [packages/db/src/query/ir.ts:136](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L136) *** @@ -81,7 +81,7 @@ Defined in: [packages/db/src/query/ir.ts:99](https://github.com/TanStack/db/blob type: "ref"; ``` -Defined in: [packages/db/src/query/ir.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L97) +Defined in: [packages/db/src/query/ir.ts:134](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L134) #### Overrides diff --git a/docs/reference/@tanstack/namespaces/IR/classes/QueryRef.md b/docs/reference/@tanstack/namespaces/IR/classes/QueryRef.md index 1dbc0357e9..7f52841e65 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/QueryRef.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/QueryRef.md @@ -5,7 +5,7 @@ title: QueryRef # Class: QueryRef -Defined in: [packages/db/src/query/ir.ts:86](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L86) +Defined in: [packages/db/src/query/ir.ts:95](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L95) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/query/ir.ts:86](https://github.com/TanStack/db/blob new QueryRef(query, alias): QueryRef; ``` -Defined in: [packages/db/src/query/ir.ts:88](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L88) +Defined in: [packages/db/src/query/ir.ts:97](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L97) #### Parameters @@ -49,7 +49,7 @@ BaseExpression.constructor readonly __returnType: any; ``` -Defined in: [packages/db/src/query/ir.ts:73](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L73) +Defined in: [packages/db/src/query/ir.ts:82](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L82) **`Internal`** @@ -69,7 +69,7 @@ BaseExpression.__returnType alias: string; ``` -Defined in: [packages/db/src/query/ir.ts:90](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L90) +Defined in: [packages/db/src/query/ir.ts:99](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L99) *** @@ -79,7 +79,7 @@ Defined in: [packages/db/src/query/ir.ts:90](https://github.com/TanStack/db/blob query: QueryIR; ``` -Defined in: [packages/db/src/query/ir.ts:89](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L89) +Defined in: [packages/db/src/query/ir.ts:98](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L98) *** @@ -89,7 +89,7 @@ Defined in: [packages/db/src/query/ir.ts:89](https://github.com/TanStack/db/blob type: "queryRef"; ``` -Defined in: [packages/db/src/query/ir.ts:87](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L87) +Defined in: [packages/db/src/query/ir.ts:96](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L96) #### Overrides diff --git a/docs/reference/@tanstack/namespaces/IR/classes/UnionAll.md b/docs/reference/@tanstack/namespaces/IR/classes/UnionAll.md new file mode 100644 index 0000000000..8e119b7c0d --- /dev/null +++ b/docs/reference/@tanstack/namespaces/IR/classes/UnionAll.md @@ -0,0 +1,105 @@ +--- +id: UnionAll +title: UnionAll +--- + +# Class: UnionAll + +Defined in: [packages/db/src/query/ir.ts:116](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L116) + +## Extends + +- `BaseExpression` + +## Constructors + +### Constructor + +```ts +new UnionAll(queries): UnionAll; +``` + +Defined in: [packages/db/src/query/ir.ts:124](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L124) + +Result-level UNION ALL. Downstream query clauses see the union result row +shape, not the branch source aliases. Optimizers may push safe operations +into branches, but compiler phases should treat this as a derived relation +unless they are explicitly handling branch lowering. + +#### Parameters + +##### queries + +[`QueryIR`](../interfaces/QueryIR.md)[] + +#### Returns + +`UnionAll` + +#### Overrides + +```ts +BaseExpression.constructor +``` + +## Properties + +### \_\_returnType + +```ts +readonly __returnType: any; +``` + +Defined in: [packages/db/src/query/ir.ts:82](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L82) + +**`Internal`** + +- Type brand for TypeScript inference + +#### Inherited from + +```ts +BaseExpression.__returnType +``` + +*** + +### queries + +```ts +queries: QueryIR[]; +``` + +Defined in: [packages/db/src/query/ir.ts:124](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L124) + +*** + +### type + +```ts +type: "unionAll"; +``` + +Defined in: [packages/db/src/query/ir.ts:117](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L117) + +#### Overrides + +```ts +BaseExpression.type +``` + +## Accessors + +### alias + +#### Get Signature + +```ts +get alias(): string; +``` + +Defined in: [packages/db/src/query/ir.ts:128](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L128) + +##### Returns + +`string` diff --git a/docs/reference/@tanstack/namespaces/IR/classes/UnionFrom.md b/docs/reference/@tanstack/namespaces/IR/classes/UnionFrom.md new file mode 100644 index 0000000000..f15085397a --- /dev/null +++ b/docs/reference/@tanstack/namespaces/IR/classes/UnionFrom.md @@ -0,0 +1,100 @@ +--- +id: UnionFrom +title: UnionFrom +--- + +# Class: UnionFrom + +Defined in: [packages/db/src/query/ir.ts:105](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L105) + +## Extends + +- `BaseExpression` + +## Constructors + +### Constructor + +```ts +new UnionFrom(sources): UnionFrom; +``` + +Defined in: [packages/db/src/query/ir.ts:107](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L107) + +#### Parameters + +##### sources + +([`CollectionRef`](CollectionRef.md) \| [`QueryRef`](QueryRef.md))[] + +#### Returns + +`UnionFrom` + +#### Overrides + +```ts +BaseExpression.constructor +``` + +## Properties + +### \_\_returnType + +```ts +readonly __returnType: any; +``` + +Defined in: [packages/db/src/query/ir.ts:82](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L82) + +**`Internal`** + +- Type brand for TypeScript inference + +#### Inherited from + +```ts +BaseExpression.__returnType +``` + +*** + +### sources + +```ts +sources: (CollectionRef | QueryRef)[]; +``` + +Defined in: [packages/db/src/query/ir.ts:107](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L107) + +*** + +### type + +```ts +type: "unionFrom"; +``` + +Defined in: [packages/db/src/query/ir.ts:106](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L106) + +#### Overrides + +```ts +BaseExpression.type +``` + +## Accessors + +### alias + +#### Get Signature + +```ts +get alias(): string; +``` + +Defined in: [packages/db/src/query/ir.ts:111](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L111) + +##### Returns + +`string` diff --git a/docs/reference/@tanstack/namespaces/IR/classes/Value.md b/docs/reference/@tanstack/namespaces/IR/classes/Value.md index fd1970ef7a..e5adbed4b5 100644 --- a/docs/reference/@tanstack/namespaces/IR/classes/Value.md +++ b/docs/reference/@tanstack/namespaces/IR/classes/Value.md @@ -5,7 +5,7 @@ title: Value # Class: Value\ -Defined in: [packages/db/src/query/ir.ts:105](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L105) +Defined in: [packages/db/src/query/ir.ts:142](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L142) ## Extends @@ -25,7 +25,7 @@ Defined in: [packages/db/src/query/ir.ts:105](https://github.com/TanStack/db/blo new Value(value): Value; ``` -Defined in: [packages/db/src/query/ir.ts:107](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L107) +Defined in: [packages/db/src/query/ir.ts:144](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L144) #### Parameters @@ -51,7 +51,7 @@ BaseExpression.constructor readonly __returnType: T; ``` -Defined in: [packages/db/src/query/ir.ts:73](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L73) +Defined in: [packages/db/src/query/ir.ts:82](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L82) **`Internal`** @@ -71,7 +71,7 @@ BaseExpression.__returnType type: "val"; ``` -Defined in: [packages/db/src/query/ir.ts:106](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L106) +Defined in: [packages/db/src/query/ir.ts:143](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L143) #### Overrides @@ -87,4 +87,4 @@ BaseExpression.type value: T; ``` -Defined in: [packages/db/src/query/ir.ts:108](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L108) +Defined in: [packages/db/src/query/ir.ts:145](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L145) diff --git a/docs/reference/@tanstack/namespaces/IR/functions/createResidualWhere.md b/docs/reference/@tanstack/namespaces/IR/functions/createResidualWhere.md index e4c71d86f1..95c6d17f41 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/createResidualWhere.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/createResidualWhere.md @@ -9,7 +9,7 @@ title: createResidualWhere function createResidualWhere(expression): Where; ``` -Defined in: [packages/db/src/query/ir.ts:208](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L208) +Defined in: [packages/db/src/query/ir.ts:296](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L296) Create a residual Where clause from an expression diff --git a/docs/reference/@tanstack/namespaces/IR/functions/followRef.md b/docs/reference/@tanstack/namespaces/IR/functions/followRef.md index 5d2636f8c3..ed666f655a 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/followRef.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/followRef.md @@ -12,12 +12,13 @@ function followRef( collection): | void | { + alias?: string; collection: Collection; path: string[]; }; ``` -Defined in: [packages/db/src/query/ir.ts:234](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L234) +Defined in: [packages/db/src/query/ir.ts:332](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L332) Follows the given reference in a query until its finds the root field the reference points to. @@ -40,8 +41,13 @@ until its finds the root field the reference points to. \| `void` \| \{ + `alias?`: `string`; `collection`: [`Collection`](../../../../interfaces/Collection.md); `path`: `string`[]; \} -The collection, its alias, and the path to the root field in this collection +The collection, its alias, and the path to the root field in this collection. +`alias` is the alias under which the resolved collection is referenced in the +query it was reached from (when the ref crosses into a joined source). It is +left undefined when the ref simply resolves to a field on the passed-in +`collection`, in which case the caller already knows the alias. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/getHavingExpression.md b/docs/reference/@tanstack/namespaces/IR/functions/getHavingExpression.md index 1ce31a66a9..5ec8e22c4c 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/getHavingExpression.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/getHavingExpression.md @@ -11,7 +11,7 @@ function getHavingExpression(having): | Aggregate; ``` -Defined in: [packages/db/src/query/ir.ts:186](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L186) +Defined in: [packages/db/src/query/ir.ts:274](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L274) Extract the expression from a HAVING clause HAVING clauses can contain aggregates, unlike regular WHERE clauses diff --git a/docs/reference/@tanstack/namespaces/IR/functions/getWhereExpression.md b/docs/reference/@tanstack/namespaces/IR/functions/getWhereExpression.md index fbf8d49943..1d4e6ad699 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/getWhereExpression.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/getWhereExpression.md @@ -9,7 +9,7 @@ title: getWhereExpression function getWhereExpression(where): BasicExpression; ``` -Defined in: [packages/db/src/query/ir.ts:176](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L176) +Defined in: [packages/db/src/query/ir.ts:264](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L264) Extract the expression from a Where clause diff --git a/docs/reference/@tanstack/namespaces/IR/functions/isExpressionLike.md b/docs/reference/@tanstack/namespaces/IR/functions/isExpressionLike.md index 07961d0d13..5f158b4abc 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/isExpressionLike.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/isExpressionLike.md @@ -9,7 +9,7 @@ title: isExpressionLike function isExpressionLike(value): boolean; ``` -Defined in: [packages/db/src/query/ir.ts:159](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L159) +Defined in: [packages/db/src/query/ir.ts:218](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L218) Runtime helper to detect IR expression-like objects. Prefer this over ad-hoc local implementations to keep behavior consistent. diff --git a/docs/reference/@tanstack/namespaces/IR/functions/isResidualWhere.md b/docs/reference/@tanstack/namespaces/IR/functions/isResidualWhere.md index ff3b6b22f8..b8f61883eb 100644 --- a/docs/reference/@tanstack/namespaces/IR/functions/isResidualWhere.md +++ b/docs/reference/@tanstack/namespaces/IR/functions/isResidualWhere.md @@ -9,7 +9,7 @@ title: isResidualWhere function isResidualWhere(where): boolean; ``` -Defined in: [packages/db/src/query/ir.ts:197](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L197) +Defined in: [packages/db/src/query/ir.ts:285](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L285) Check if a Where clause is marked as residual diff --git a/docs/reference/@tanstack/namespaces/IR/index.md b/docs/reference/@tanstack/namespaces/IR/index.md index 415a365542..add67e808e 100644 --- a/docs/reference/@tanstack/namespaces/IR/index.md +++ b/docs/reference/@tanstack/namespaces/IR/index.md @@ -9,10 +9,13 @@ title: IR - [Aggregate](classes/Aggregate.md) - [CollectionRef](classes/CollectionRef.md) +- [ConditionalSelect](classes/ConditionalSelect.md) - [Func](classes/Func.md) - [IncludesSubquery](classes/IncludesSubquery.md) - [PropRef](classes/PropRef.md) - [QueryRef](classes/QueryRef.md) +- [UnionAll](classes/UnionAll.md) +- [UnionFrom](classes/UnionFrom.md) - [Value](classes/Value.md) ## Interfaces @@ -23,6 +26,7 @@ title: IR ## Type Aliases - [BasicExpression](type-aliases/BasicExpression.md) +- [ConditionalSelectBranch](type-aliases/ConditionalSelectBranch.md) - [From](type-aliases/From.md) - [GroupBy](type-aliases/GroupBy.md) - [Having](type-aliases/Having.md) @@ -34,6 +38,7 @@ title: IR - [OrderByClause](type-aliases/OrderByClause.md) - [OrderByDirection](type-aliases/OrderByDirection.md) - [Select](type-aliases/Select.md) +- [SelectValueExpression](type-aliases/SelectValueExpression.md) - [Where](type-aliases/Where.md) ## Variables diff --git a/docs/reference/@tanstack/namespaces/IR/interfaces/JoinClause.md b/docs/reference/@tanstack/namespaces/IR/interfaces/JoinClause.md index e41a4e7153..d41b72cace 100644 --- a/docs/reference/@tanstack/namespaces/IR/interfaces/JoinClause.md +++ b/docs/reference/@tanstack/namespaces/IR/interfaces/JoinClause.md @@ -5,7 +5,7 @@ title: JoinClause # Interface: JoinClause -Defined in: [packages/db/src/query/ir.ts:40](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L40) +Defined in: [packages/db/src/query/ir.ts:49](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L49) ## Properties @@ -17,7 +17,7 @@ from: | QueryRef; ``` -Defined in: [packages/db/src/query/ir.ts:41](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L41) +Defined in: [packages/db/src/query/ir.ts:50](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L50) *** @@ -27,7 +27,7 @@ Defined in: [packages/db/src/query/ir.ts:41](https://github.com/TanStack/db/blob left: BasicExpression; ``` -Defined in: [packages/db/src/query/ir.ts:43](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L43) +Defined in: [packages/db/src/query/ir.ts:52](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L52) *** @@ -37,7 +37,7 @@ Defined in: [packages/db/src/query/ir.ts:43](https://github.com/TanStack/db/blob right: BasicExpression; ``` -Defined in: [packages/db/src/query/ir.ts:44](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L44) +Defined in: [packages/db/src/query/ir.ts:53](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L53) *** @@ -47,4 +47,4 @@ Defined in: [packages/db/src/query/ir.ts:44](https://github.com/TanStack/db/blob type: "inner" | "left" | "right" | "full" | "outer" | "cross"; ``` -Defined in: [packages/db/src/query/ir.ts:42](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L42) +Defined in: [packages/db/src/query/ir.ts:51](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L51) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/BasicExpression.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/BasicExpression.md index de4676e5b7..8d15913c05 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/BasicExpression.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/BasicExpression.md @@ -12,7 +12,7 @@ type BasicExpression = | Func; ``` -Defined in: [packages/db/src/query/ir.ts:127](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L127) +Defined in: [packages/db/src/query/ir.ts:164](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L164) ## Type Parameters diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/ConditionalSelectBranch.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/ConditionalSelectBranch.md new file mode 100644 index 0000000000..73c4f155d0 --- /dev/null +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/ConditionalSelectBranch.md @@ -0,0 +1,32 @@ +--- +id: ConditionalSelectBranch +title: ConditionalSelectBranch +--- + +# Type Alias: ConditionalSelectBranch + +```ts +type ConditionalSelectBranch = object; +``` + +Defined in: [packages/db/src/query/ir.ts:192](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L192) + +## Properties + +### condition + +```ts +condition: BasicExpression; +``` + +Defined in: [packages/db/src/query/ir.ts:193](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L193) + +*** + +### value + +```ts +value: SelectValueExpression; +``` + +Defined in: [packages/db/src/query/ir.ts:194](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L194) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/From.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/From.md index f5dc25e149..4b863b7a95 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/From.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/From.md @@ -8,7 +8,9 @@ title: From ```ts type From = | CollectionRef - | QueryRef; + | QueryRef + | UnionFrom + | UnionAll; ``` -Defined in: [packages/db/src/query/ir.ts:32](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L32) +Defined in: [packages/db/src/query/ir.ts:36](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L36) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/GroupBy.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/GroupBy.md index 1ecd399968..e17b6a1c8d 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/GroupBy.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/GroupBy.md @@ -9,4 +9,4 @@ title: GroupBy type GroupBy = BasicExpression[]; ``` -Defined in: [packages/db/src/query/ir.ts:51](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L51) +Defined in: [packages/db/src/query/ir.ts:60](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L60) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/Having.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/Having.md index 7654798e3a..2f042ce4dd 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/Having.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/Having.md @@ -9,4 +9,4 @@ title: Having type Having = Where; ``` -Defined in: [packages/db/src/query/ir.ts:53](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L53) +Defined in: [packages/db/src/query/ir.ts:62](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L62) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/IncludesMaterialization.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/IncludesMaterialization.md index 02afe1663a..578786d243 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/IncludesMaterialization.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/IncludesMaterialization.md @@ -6,7 +6,7 @@ title: IncludesMaterialization # Type Alias: IncludesMaterialization ```ts -type IncludesMaterialization = "collection" | "array" | "concat"; +type IncludesMaterialization = "collection" | "array" | "singleton" | "concat"; ``` Defined in: [packages/db/src/query/ir.ts:28](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L28) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/Join.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/Join.md index 53b84b8b49..0e6c1d1b90 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/Join.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/Join.md @@ -9,4 +9,4 @@ title: Join type Join = JoinClause[]; ``` -Defined in: [packages/db/src/query/ir.ts:38](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L38) +Defined in: [packages/db/src/query/ir.ts:47](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L47) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/Limit.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/Limit.md index 38be20e012..b571ea0440 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/Limit.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/Limit.md @@ -9,4 +9,4 @@ title: Limit type Limit = number; ``` -Defined in: [packages/db/src/query/ir.ts:64](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L64) +Defined in: [packages/db/src/query/ir.ts:73](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L73) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/Offset.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/Offset.md index 771c53d41a..a95aa02514 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/Offset.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/Offset.md @@ -9,4 +9,4 @@ title: Offset type Offset = number; ``` -Defined in: [packages/db/src/query/ir.ts:66](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L66) +Defined in: [packages/db/src/query/ir.ts:75](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L75) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderBy.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderBy.md index 2d5a450cdf..14ce76dc14 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderBy.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderBy.md @@ -9,4 +9,4 @@ title: OrderBy type OrderBy = OrderByClause[]; ``` -Defined in: [packages/db/src/query/ir.ts:55](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L55) +Defined in: [packages/db/src/query/ir.ts:64](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L64) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByClause.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByClause.md index 2770aed38b..987cbf522b 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByClause.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByClause.md @@ -9,7 +9,7 @@ title: OrderByClause type OrderByClause = object; ``` -Defined in: [packages/db/src/query/ir.ts:57](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L57) +Defined in: [packages/db/src/query/ir.ts:66](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L66) ## Properties @@ -19,7 +19,7 @@ Defined in: [packages/db/src/query/ir.ts:57](https://github.com/TanStack/db/blob compareOptions: CompareOptions; ``` -Defined in: [packages/db/src/query/ir.ts:59](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L59) +Defined in: [packages/db/src/query/ir.ts:68](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L68) *** @@ -29,4 +29,4 @@ Defined in: [packages/db/src/query/ir.ts:59](https://github.com/TanStack/db/blob expression: BasicExpression; ``` -Defined in: [packages/db/src/query/ir.ts:58](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L58) +Defined in: [packages/db/src/query/ir.ts:67](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L67) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByDirection.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByDirection.md index 58c632a106..7466f4b263 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByDirection.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/OrderByDirection.md @@ -9,4 +9,4 @@ title: OrderByDirection type OrderByDirection = "asc" | "desc"; ``` -Defined in: [packages/db/src/query/ir.ts:62](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L62) +Defined in: [packages/db/src/query/ir.ts:71](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L71) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/Select.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/Select.md index 4016ef6f4d..3833cf0ed8 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/Select.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/Select.md @@ -9,7 +9,7 @@ title: Select type Select = object; ``` -Defined in: [packages/db/src/query/ir.ts:34](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L34) +Defined in: [packages/db/src/query/ir.ts:38](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L38) ## Index Signature @@ -19,4 +19,5 @@ Defined in: [packages/db/src/query/ir.ts:34](https://github.com/TanStack/db/blob | Select | Aggregate | IncludesSubquery + | ConditionalSelect ``` diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/SelectValueExpression.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/SelectValueExpression.md new file mode 100644 index 0000000000..c8f003863a --- /dev/null +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/SelectValueExpression.md @@ -0,0 +1,17 @@ +--- +id: SelectValueExpression +title: SelectValueExpression +--- + +# Type Alias: SelectValueExpression + +```ts +type SelectValueExpression = + | BasicExpression + | Aggregate + | Select + | IncludesSubquery + | ConditionalSelect; +``` + +Defined in: [packages/db/src/query/ir.ts:197](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L197) diff --git a/docs/reference/@tanstack/namespaces/IR/type-aliases/Where.md b/docs/reference/@tanstack/namespaces/IR/type-aliases/Where.md index fdbb878874..f80a99c4e7 100644 --- a/docs/reference/@tanstack/namespaces/IR/type-aliases/Where.md +++ b/docs/reference/@tanstack/namespaces/IR/type-aliases/Where.md @@ -14,4 +14,4 @@ type Where = }; ``` -Defined in: [packages/db/src/query/ir.ts:47](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L47) +Defined in: [packages/db/src/query/ir.ts:56](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L56) diff --git a/docs/reference/@tanstack/namespaces/IR/variables/INCLUDES_SCALAR_FIELD.md b/docs/reference/@tanstack/namespaces/IR/variables/INCLUDES_SCALAR_FIELD.md index c68802f241..ea124c83e6 100644 --- a/docs/reference/@tanstack/namespaces/IR/variables/INCLUDES_SCALAR_FIELD.md +++ b/docs/reference/@tanstack/namespaces/IR/variables/INCLUDES_SCALAR_FIELD.md @@ -9,4 +9,4 @@ title: INCLUDES_SCALAR_FIELD const INCLUDES_SCALAR_FIELD: "__includes_scalar__"; ``` -Defined in: [packages/db/src/query/ir.ts:30](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L30) +Defined in: [packages/db/src/query/ir.ts:34](https://github.com/TanStack/db/blob/main/packages/db/src/query/ir.ts#L34) diff --git a/docs/reference/classes/AggregateFunctionNotInSelectError.md b/docs/reference/classes/AggregateFunctionNotInSelectError.md index 19e08fb682..506a12bf7a 100644 --- a/docs/reference/classes/AggregateFunctionNotInSelectError.md +++ b/docs/reference/classes/AggregateFunctionNotInSelectError.md @@ -5,7 +5,7 @@ title: AggregateFunctionNotInSelectError # Class: AggregateFunctionNotInSelectError -Defined in: [packages/db/src/errors.ts:614](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L614) +Defined in: [packages/db/src/errors.ts:639](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L639) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:614](https://github.com/TanStack/db/blob/ new AggregateFunctionNotInSelectError(functionName): AggregateFunctionNotInSelectError; ``` -Defined in: [packages/db/src/errors.ts:615](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L615) +Defined in: [packages/db/src/errors.ts:640](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L640) #### Parameters diff --git a/docs/reference/classes/AggregateNotSupportedError.md b/docs/reference/classes/AggregateNotSupportedError.md index 7cdcf25e56..b5807bbe10 100644 --- a/docs/reference/classes/AggregateNotSupportedError.md +++ b/docs/reference/classes/AggregateNotSupportedError.md @@ -5,7 +5,7 @@ title: AggregateNotSupportedError # Class: AggregateNotSupportedError -Defined in: [packages/db/src/errors.ts:730](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L730) +Defined in: [packages/db/src/errors.ts:755](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L755) Error thrown when aggregate expressions are used outside of a GROUP BY context. @@ -21,7 +21,7 @@ Error thrown when aggregate expressions are used outside of a GROUP BY context. new AggregateNotSupportedError(): AggregateNotSupportedError; ``` -Defined in: [packages/db/src/errors.ts:731](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L731) +Defined in: [packages/db/src/errors.ts:756](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L756) #### Returns diff --git a/docs/reference/classes/BTreeIndex.md b/docs/reference/classes/BTreeIndex.md index 3072cef868..bd7bdd6a96 100644 --- a/docs/reference/classes/BTreeIndex.md +++ b/docs/reference/classes/BTreeIndex.md @@ -68,7 +68,7 @@ Defined in: [packages/db/src/indexes/btree-index.ts:55](https://github.com/TanSt protected compareOptions: CompareOptions; ``` -Defined in: [packages/db/src/indexes/base-index.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L92) +Defined in: [packages/db/src/indexes/base-index.ts:101](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L101) #### Inherited from @@ -82,7 +82,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:92](https://github.com/TanSta readonly expression: BasicExpression; ``` -Defined in: [packages/db/src/indexes/base-index.ts:86](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L86) +Defined in: [packages/db/src/indexes/base-index.ts:95](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L95) #### Inherited from @@ -90,13 +90,30 @@ Defined in: [packages/db/src/indexes/base-index.ts:86](https://github.com/TanSta *** +### hasCustomComparator + +```ts +protected hasCustomComparator: boolean = false; +``` + +Defined in: [packages/db/src/indexes/base-index.ts:106](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L106) + +Set by subclasses when constructed with a user-supplied comparator, whose +ordering may not match the WHERE evaluator's relational operators. + +#### Inherited from + +[`BaseIndex`](BaseIndex.md).[`hasCustomComparator`](BaseIndex.md#hascustomcomparator) + +*** + ### id ```ts readonly id: number; ``` -Defined in: [packages/db/src/indexes/base-index.ts:84](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L84) +Defined in: [packages/db/src/indexes/base-index.ts:93](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L93) #### Inherited from @@ -110,7 +127,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:84](https://github.com/TanSta protected lastUpdated: Date; ``` -Defined in: [packages/db/src/indexes/base-index.ts:91](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L91) +Defined in: [packages/db/src/indexes/base-index.ts:100](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L100) #### Inherited from @@ -124,7 +141,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:91](https://github.com/TanSta protected lookupCount: number = 0; ``` -Defined in: [packages/db/src/indexes/base-index.ts:89](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L89) +Defined in: [packages/db/src/indexes/base-index.ts:98](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L98) #### Inherited from @@ -138,7 +155,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:89](https://github.com/TanSta readonly optional name: string; ``` -Defined in: [packages/db/src/indexes/base-index.ts:85](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L85) +Defined in: [packages/db/src/indexes/base-index.ts:94](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L94) #### Inherited from @@ -166,7 +183,7 @@ Defined in: [packages/db/src/indexes/btree-index.ts:38](https://github.com/TanSt protected totalLookupTime: number = 0; ``` -Defined in: [packages/db/src/indexes/base-index.ts:90](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L90) +Defined in: [packages/db/src/indexes/base-index.ts:99](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L99) #### Inherited from @@ -182,7 +199,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:90](https://github.com/TanSta get indexedKeysSet(): Set; ``` -Defined in: [packages/db/src/indexes/btree-index.ts:400](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L400) +Defined in: [packages/db/src/indexes/btree-index.ts:410](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L410) ##### Returns @@ -202,7 +219,7 @@ Defined in: [packages/db/src/indexes/btree-index.ts:400](https://github.com/TanS get keyCount(): number; ``` -Defined in: [packages/db/src/indexes/btree-index.ts:213](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L213) +Defined in: [packages/db/src/indexes/btree-index.ts:214](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L214) Gets the number of indexed keys @@ -224,7 +241,7 @@ Gets the number of indexed keys get orderedEntriesArray(): [any, Set][]; ``` -Defined in: [packages/db/src/indexes/btree-index.ts:404](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L404) +Defined in: [packages/db/src/indexes/btree-index.ts:414](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L414) ##### Returns @@ -244,7 +261,7 @@ Defined in: [packages/db/src/indexes/btree-index.ts:404](https://github.com/TanS get orderedEntriesArrayReversed(): [any, Set][]; ``` -Defined in: [packages/db/src/indexes/btree-index.ts:413](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L413) +Defined in: [packages/db/src/indexes/btree-index.ts:423](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L423) ##### Returns @@ -256,6 +273,32 @@ Defined in: [packages/db/src/indexes/btree-index.ts:413](https://github.com/TanS *** +### supportsRangeOptimization + +#### Get Signature + +```ts +get supportsRangeOptimization(): boolean; +``` + +Defined in: [packages/db/src/indexes/base-index.ts:161](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L161) + +Whether range lookups (gt/gte/lt/lte) on this index can be trusted to +return every matching key. Range traversal relies on the index ordering, so +it is unsafe when the index uses a custom comparator, whose order may not +match the WHERE evaluator's relational operators. Callers must fall back to +a full scan when this is `false`. + +##### Returns + +`boolean` + +#### Inherited from + +[`BaseIndex`](BaseIndex.md).[`supportsRangeOptimization`](BaseIndex.md#supportsrangeoptimization) + +*** + ### valueMapData #### Get Signature @@ -264,7 +307,7 @@ Defined in: [packages/db/src/indexes/btree-index.ts:413](https://github.com/TanS get valueMapData(): Map>; ``` -Defined in: [packages/db/src/indexes/btree-index.ts:420](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L420) +Defined in: [packages/db/src/indexes/btree-index.ts:430](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L430) ##### Returns @@ -282,7 +325,7 @@ Defined in: [packages/db/src/indexes/btree-index.ts:420](https://github.com/TanS add(key, item): void; ``` -Defined in: [packages/db/src/indexes/btree-index.ts:83](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L83) +Defined in: [packages/db/src/indexes/btree-index.ts:84](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L84) Adds a value to the index @@ -312,7 +355,7 @@ Adds a value to the index build(entries): void; ``` -Defined in: [packages/db/src/indexes/btree-index.ts:157](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L157) +Defined in: [packages/db/src/indexes/btree-index.ts:158](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L158) Builds the index from a collection of entries @@ -338,7 +381,7 @@ Builds the index from a collection of entries clear(): void; ``` -Defined in: [packages/db/src/indexes/btree-index.ts:168](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L168) +Defined in: [packages/db/src/indexes/btree-index.ts:169](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L169) Clears all data from the index @@ -358,7 +401,7 @@ Clears all data from the index equalityLookup(value): Set; ``` -Defined in: [packages/db/src/indexes/btree-index.ts:222](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L222) +Defined in: [packages/db/src/indexes/btree-index.ts:223](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L223) Performs an equality lookup @@ -384,7 +427,7 @@ Performs an equality lookup protected evaluateIndexExpression(item): any; ``` -Defined in: [packages/db/src/indexes/base-index.ts:194](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L194) +Defined in: [packages/db/src/indexes/base-index.ts:212](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L212) #### Parameters @@ -408,7 +451,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:194](https://github.com/TanSt getStats(): IndexStats; ``` -Defined in: [packages/db/src/indexes/base-index.ts:182](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L182) +Defined in: [packages/db/src/indexes/base-index.ts:200](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L200) #### Returns @@ -426,7 +469,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:182](https://github.com/TanSt inArrayLookup(values): Set; ``` -Defined in: [packages/db/src/indexes/btree-index.ts:385](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L385) +Defined in: [packages/db/src/indexes/btree-index.ts:395](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L395) Performs an IN array lookup @@ -452,7 +495,7 @@ Performs an IN array lookup protected initialize(_options?): void; ``` -Defined in: [packages/db/src/indexes/btree-index.ts:78](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L78) +Defined in: [packages/db/src/indexes/btree-index.ts:79](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L79) #### Parameters @@ -476,7 +519,7 @@ Defined in: [packages/db/src/indexes/btree-index.ts:78](https://github.com/TanSt lookup(operation, value): Set; ``` -Defined in: [packages/db/src/indexes/btree-index.ts:178](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L178) +Defined in: [packages/db/src/indexes/btree-index.ts:179](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L179) Performs a lookup operation @@ -506,7 +549,7 @@ Performs a lookup operation matchesCompareOptions(compareOptions): boolean; ``` -Defined in: [packages/db/src/indexes/base-index.ts:159](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L159) +Defined in: [packages/db/src/indexes/base-index.ts:177](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L177) Checks if the compare options match the index's compare options. The direction is ignored because the index can be reversed if the direction is different. @@ -533,7 +576,7 @@ The direction is ignored because the index can be reversed if the direction is d matchesDirection(direction): boolean; ``` -Defined in: [packages/db/src/indexes/base-index.ts:178](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L178) +Defined in: [packages/db/src/indexes/base-index.ts:196](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L196) Checks if the index matches the provided direction. @@ -559,7 +602,7 @@ Checks if the index matches the provided direction. matchesField(fieldPath): boolean; ``` -Defined in: [packages/db/src/indexes/base-index.ts:147](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L147) +Defined in: [packages/db/src/indexes/base-index.ts:165](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L165) #### Parameters @@ -583,7 +626,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:147](https://github.com/TanSt rangeQuery(options): Set; ``` -Defined in: [packages/db/src/indexes/btree-index.ts:231](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L231) +Defined in: [packages/db/src/indexes/btree-index.ts:232](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L232) Performs a range query with options This is more efficient for compound queries like "WHERE a > 5 AND a < 10" @@ -610,7 +653,7 @@ This is more efficient for compound queries like "WHERE a > 5 AND a < 10" rangeQueryReversed(options): Set; ``` -Defined in: [packages/db/src/indexes/btree-index.ts:269](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L269) +Defined in: [packages/db/src/indexes/btree-index.ts:279](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L279) Performs a reversed range query @@ -636,7 +679,7 @@ Performs a reversed range query remove(key, item): void; ``` -Defined in: [packages/db/src/indexes/btree-index.ts:114](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L114) +Defined in: [packages/db/src/indexes/btree-index.ts:115](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L115) Removes a value from the index @@ -666,7 +709,7 @@ Removes a value from the index supports(operation): boolean; ``` -Defined in: [packages/db/src/indexes/base-index.ts:143](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L143) +Defined in: [packages/db/src/indexes/base-index.ts:157](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L157) #### Parameters @@ -693,7 +736,7 @@ take( filterFn?): TKey[]; ``` -Defined in: [packages/db/src/indexes/btree-index.ts:331](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L331) +Defined in: [packages/db/src/indexes/btree-index.ts:341](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L341) Returns the next n items after the provided item. @@ -733,7 +776,7 @@ The next n items after the provided key. takeFromStart(n, filterFn?): TKey[]; ``` -Defined in: [packages/db/src/indexes/btree-index.ts:344](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L344) +Defined in: [packages/db/src/indexes/btree-index.ts:354](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L354) Returns the first n items from the beginning. @@ -772,7 +815,7 @@ takeReversed( filterFn?): TKey[]; ``` -Defined in: [packages/db/src/indexes/btree-index.ts:356](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L356) +Defined in: [packages/db/src/indexes/btree-index.ts:366](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L366) Returns the next n items **before** the provided item (in descending order). @@ -812,7 +855,7 @@ The next n items **before** the provided key. takeReversedFromEnd(n, filterFn?): TKey[]; ``` -Defined in: [packages/db/src/indexes/btree-index.ts:373](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L373) +Defined in: [packages/db/src/indexes/btree-index.ts:383](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L383) Returns the last n items from the end. @@ -848,7 +891,7 @@ The last n items protected trackLookup(startTime): void; ``` -Defined in: [packages/db/src/indexes/base-index.ts:199](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L199) +Defined in: [packages/db/src/indexes/base-index.ts:217](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L217) #### Parameters @@ -875,7 +918,7 @@ update( newItem): void; ``` -Defined in: [packages/db/src/indexes/btree-index.ts:149](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L149) +Defined in: [packages/db/src/indexes/btree-index.ts:150](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/btree-index.ts#L150) Updates a value in the index @@ -909,7 +952,7 @@ Updates a value in the index protected updateTimestamp(): void; ``` -Defined in: [packages/db/src/indexes/base-index.ts:205](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L205) +Defined in: [packages/db/src/indexes/base-index.ts:223](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L223) #### Returns diff --git a/docs/reference/classes/BaseIndex.md b/docs/reference/classes/BaseIndex.md index bd512fdbea..90f82ced5b 100644 --- a/docs/reference/classes/BaseIndex.md +++ b/docs/reference/classes/BaseIndex.md @@ -5,7 +5,7 @@ title: BaseIndex # Abstract Class: BaseIndex\ -Defined in: [packages/db/src/indexes/base-index.ts:81](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L81) +Defined in: [packages/db/src/indexes/base-index.ts:90](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L90) Base abstract class that all index types extend @@ -36,7 +36,7 @@ new BaseIndex( options?): BaseIndex; ``` -Defined in: [packages/db/src/indexes/base-index.ts:94](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L94) +Defined in: [packages/db/src/indexes/base-index.ts:108](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L108) #### Parameters @@ -68,7 +68,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:94](https://github.com/TanSta protected compareOptions: CompareOptions; ``` -Defined in: [packages/db/src/indexes/base-index.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L92) +Defined in: [packages/db/src/indexes/base-index.ts:101](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L101) *** @@ -78,7 +78,20 @@ Defined in: [packages/db/src/indexes/base-index.ts:92](https://github.com/TanSta readonly expression: BasicExpression; ``` -Defined in: [packages/db/src/indexes/base-index.ts:86](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L86) +Defined in: [packages/db/src/indexes/base-index.ts:95](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L95) + +*** + +### hasCustomComparator + +```ts +protected hasCustomComparator: boolean = false; +``` + +Defined in: [packages/db/src/indexes/base-index.ts:106](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L106) + +Set by subclasses when constructed with a user-supplied comparator, whose +ordering may not match the WHERE evaluator's relational operators. *** @@ -88,7 +101,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:86](https://github.com/TanSta readonly id: number; ``` -Defined in: [packages/db/src/indexes/base-index.ts:84](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L84) +Defined in: [packages/db/src/indexes/base-index.ts:93](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L93) *** @@ -98,7 +111,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:84](https://github.com/TanSta protected lastUpdated: Date; ``` -Defined in: [packages/db/src/indexes/base-index.ts:91](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L91) +Defined in: [packages/db/src/indexes/base-index.ts:100](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L100) *** @@ -108,7 +121,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:91](https://github.com/TanSta protected lookupCount: number = 0; ``` -Defined in: [packages/db/src/indexes/base-index.ts:89](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L89) +Defined in: [packages/db/src/indexes/base-index.ts:98](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L98) *** @@ -118,7 +131,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:89](https://github.com/TanSta readonly optional name: string; ``` -Defined in: [packages/db/src/indexes/base-index.ts:85](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L85) +Defined in: [packages/db/src/indexes/base-index.ts:94](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L94) *** @@ -128,7 +141,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:85](https://github.com/TanSta abstract readonly supportedOperations: Set<"eq" | "gt" | "gte" | "lt" | "lte" | "in" | "like" | "ilike">; ``` -Defined in: [packages/db/src/indexes/base-index.ts:87](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L87) +Defined in: [packages/db/src/indexes/base-index.ts:96](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L96) *** @@ -138,7 +151,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:87](https://github.com/TanSta protected totalLookupTime: number = 0; ``` -Defined in: [packages/db/src/indexes/base-index.ts:90](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L90) +Defined in: [packages/db/src/indexes/base-index.ts:99](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L99) ## Accessors @@ -150,7 +163,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:90](https://github.com/TanSta get abstract indexedKeysSet(): Set; ``` -Defined in: [packages/db/src/indexes/base-index.ts:139](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L139) +Defined in: [packages/db/src/indexes/base-index.ts:153](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L153) ##### Returns @@ -170,7 +183,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:139](https://github.com/TanSt get abstract keyCount(): number; ``` -Defined in: [packages/db/src/indexes/base-index.ts:132](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L132) +Defined in: [packages/db/src/indexes/base-index.ts:146](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L146) ##### Returns @@ -190,7 +203,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:132](https://github.com/TanSt get abstract orderedEntriesArray(): [any, Set][]; ``` -Defined in: [packages/db/src/indexes/base-index.ts:137](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L137) +Defined in: [packages/db/src/indexes/base-index.ts:151](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L151) ##### Returns @@ -210,7 +223,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:137](https://github.com/TanSt get abstract orderedEntriesArrayReversed(): [any, Set][]; ``` -Defined in: [packages/db/src/indexes/base-index.ts:138](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L138) +Defined in: [packages/db/src/indexes/base-index.ts:152](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L152) ##### Returns @@ -222,6 +235,32 @@ Defined in: [packages/db/src/indexes/base-index.ts:138](https://github.com/TanSt *** +### supportsRangeOptimization + +#### Get Signature + +```ts +get supportsRangeOptimization(): boolean; +``` + +Defined in: [packages/db/src/indexes/base-index.ts:161](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L161) + +Whether range lookups (gt/gte/lt/lte) on this index can be trusted to +return every matching key. Range traversal relies on the index ordering, so +it is unsafe when the index uses a custom comparator, whose order may not +match the WHERE evaluator's relational operators. Callers must fall back to +a full scan when this is `false`. + +##### Returns + +`boolean` + +#### Implementation of + +[`IndexInterface`](../interfaces/IndexInterface.md).[`supportsRangeOptimization`](../interfaces/IndexInterface.md#supportsrangeoptimization) + +*** + ### valueMapData #### Get Signature @@ -230,7 +269,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:138](https://github.com/TanSt get abstract valueMapData(): Map>; ``` -Defined in: [packages/db/src/indexes/base-index.ts:140](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L140) +Defined in: [packages/db/src/indexes/base-index.ts:154](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L154) ##### Returns @@ -248,7 +287,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:140](https://github.com/TanSt abstract add(key, item): void; ``` -Defined in: [packages/db/src/indexes/base-index.ts:108](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L108) +Defined in: [packages/db/src/indexes/base-index.ts:122](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L122) #### Parameters @@ -276,7 +315,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:108](https://github.com/TanSt abstract build(entries): void; ``` -Defined in: [packages/db/src/indexes/base-index.ts:111](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L111) +Defined in: [packages/db/src/indexes/base-index.ts:125](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L125) #### Parameters @@ -300,7 +339,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:111](https://github.com/TanSt abstract clear(): void; ``` -Defined in: [packages/db/src/indexes/base-index.ts:112](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L112) +Defined in: [packages/db/src/indexes/base-index.ts:126](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L126) #### Returns @@ -318,7 +357,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:112](https://github.com/TanSt abstract equalityLookup(value): Set; ``` -Defined in: [packages/db/src/indexes/base-index.ts:133](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L133) +Defined in: [packages/db/src/indexes/base-index.ts:147](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L147) #### Parameters @@ -342,7 +381,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:133](https://github.com/TanSt protected evaluateIndexExpression(item): any; ``` -Defined in: [packages/db/src/indexes/base-index.ts:194](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L194) +Defined in: [packages/db/src/indexes/base-index.ts:212](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L212) #### Parameters @@ -362,7 +401,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:194](https://github.com/TanSt getStats(): IndexStats; ``` -Defined in: [packages/db/src/indexes/base-index.ts:182](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L182) +Defined in: [packages/db/src/indexes/base-index.ts:200](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L200) #### Returns @@ -380,7 +419,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:182](https://github.com/TanSt abstract inArrayLookup(values): Set; ``` -Defined in: [packages/db/src/indexes/base-index.ts:134](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L134) +Defined in: [packages/db/src/indexes/base-index.ts:148](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L148) #### Parameters @@ -404,7 +443,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:134](https://github.com/TanSt abstract protected initialize(options?): void; ``` -Defined in: [packages/db/src/indexes/base-index.ts:192](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L192) +Defined in: [packages/db/src/indexes/base-index.ts:210](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L210) #### Parameters @@ -424,7 +463,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:192](https://github.com/TanSt abstract lookup(operation, value): Set; ``` -Defined in: [packages/db/src/indexes/base-index.ts:113](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L113) +Defined in: [packages/db/src/indexes/base-index.ts:127](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L127) #### Parameters @@ -452,7 +491,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:113](https://github.com/TanSt matchesCompareOptions(compareOptions): boolean; ``` -Defined in: [packages/db/src/indexes/base-index.ts:159](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L159) +Defined in: [packages/db/src/indexes/base-index.ts:177](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L177) Checks if the compare options match the index's compare options. The direction is ignored because the index can be reversed if the direction is different. @@ -479,7 +518,7 @@ The direction is ignored because the index can be reversed if the direction is d matchesDirection(direction): boolean; ``` -Defined in: [packages/db/src/indexes/base-index.ts:178](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L178) +Defined in: [packages/db/src/indexes/base-index.ts:196](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L196) Checks if the index matches the provided direction. @@ -505,7 +544,7 @@ Checks if the index matches the provided direction. matchesField(fieldPath): boolean; ``` -Defined in: [packages/db/src/indexes/base-index.ts:147](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L147) +Defined in: [packages/db/src/indexes/base-index.ts:165](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L165) #### Parameters @@ -529,7 +568,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:147](https://github.com/TanSt abstract rangeQuery(options): Set; ``` -Defined in: [packages/db/src/indexes/base-index.ts:135](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L135) +Defined in: [packages/db/src/indexes/base-index.ts:149](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L149) #### Parameters @@ -553,7 +592,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:135](https://github.com/TanSt abstract rangeQueryReversed(options): Set; ``` -Defined in: [packages/db/src/indexes/base-index.ts:136](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L136) +Defined in: [packages/db/src/indexes/base-index.ts:150](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L150) #### Parameters @@ -577,7 +616,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:136](https://github.com/TanSt abstract remove(key, item): void; ``` -Defined in: [packages/db/src/indexes/base-index.ts:109](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L109) +Defined in: [packages/db/src/indexes/base-index.ts:123](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L123) #### Parameters @@ -605,7 +644,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:109](https://github.com/TanSt supports(operation): boolean; ``` -Defined in: [packages/db/src/indexes/base-index.ts:143](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L143) +Defined in: [packages/db/src/indexes/base-index.ts:157](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L157) #### Parameters @@ -632,7 +671,7 @@ abstract take( filterFn?): TKey[]; ``` -Defined in: [packages/db/src/indexes/base-index.ts:114](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L114) +Defined in: [packages/db/src/indexes/base-index.ts:128](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L128) #### Parameters @@ -664,7 +703,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:114](https://github.com/TanSt abstract takeFromStart(n, filterFn?): TKey[]; ``` -Defined in: [packages/db/src/indexes/base-index.ts:119](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L119) +Defined in: [packages/db/src/indexes/base-index.ts:133](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L133) #### Parameters @@ -695,7 +734,7 @@ abstract takeReversed( filterFn?): TKey[]; ``` -Defined in: [packages/db/src/indexes/base-index.ts:123](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L123) +Defined in: [packages/db/src/indexes/base-index.ts:137](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L137) #### Parameters @@ -727,7 +766,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:123](https://github.com/TanSt abstract takeReversedFromEnd(n, filterFn?): TKey[]; ``` -Defined in: [packages/db/src/indexes/base-index.ts:128](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L128) +Defined in: [packages/db/src/indexes/base-index.ts:142](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L142) #### Parameters @@ -755,7 +794,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:128](https://github.com/TanSt protected trackLookup(startTime): void; ``` -Defined in: [packages/db/src/indexes/base-index.ts:199](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L199) +Defined in: [packages/db/src/indexes/base-index.ts:217](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L217) #### Parameters @@ -778,7 +817,7 @@ abstract update( newItem): void; ``` -Defined in: [packages/db/src/indexes/base-index.ts:110](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L110) +Defined in: [packages/db/src/indexes/base-index.ts:124](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L124) #### Parameters @@ -810,7 +849,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:110](https://github.com/TanSt protected updateTimestamp(): void; ``` -Defined in: [packages/db/src/indexes/base-index.ts:205](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L205) +Defined in: [packages/db/src/indexes/base-index.ts:223](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L223) #### Returns diff --git a/docs/reference/classes/BaseQueryBuilder.md b/docs/reference/classes/BaseQueryBuilder.md index 224e68885d..05dfc08521 100644 --- a/docs/reference/classes/BaseQueryBuilder.md +++ b/docs/reference/classes/BaseQueryBuilder.md @@ -5,7 +5,7 @@ title: BaseQueryBuilder # Class: BaseQueryBuilder\ -Defined in: [packages/db/src/query/builder/index.ts:63](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L63) +Defined in: [packages/db/src/query/builder/index.ts:78](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L78) ## Type Parameters @@ -21,7 +21,7 @@ Defined in: [packages/db/src/query/builder/index.ts:63](https://github.com/TanSt new BaseQueryBuilder(query): BaseQueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:66](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L66) +Defined in: [packages/db/src/query/builder/index.ts:81](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L81) #### Parameters @@ -43,7 +43,7 @@ Defined in: [packages/db/src/query/builder/index.ts:66](https://github.com/TanSt get fn(): object; ``` -Defined in: [packages/db/src/query/builder/index.ts:770](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L770) +Defined in: [packages/db/src/query/builder/index.ts:850](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L850) Functional variants of the query builder These are imperative function that are called for ery row. @@ -176,7 +176,7 @@ query _getQuery(): QueryIR; ``` -Defined in: [packages/db/src/query/builder/index.ts:857](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L857) +Defined in: [packages/db/src/query/builder/index.ts:937](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L937) #### Returns @@ -190,7 +190,7 @@ Defined in: [packages/db/src/query/builder/index.ts:857](https://github.com/TanS distinct(): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:709](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L709) +Defined in: [packages/db/src/query/builder/index.ts:783](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L783) Specify that the query should return distinct rows. Deduplicates rows based on the selected columns. @@ -219,7 +219,7 @@ query findOne(): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:729](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L729) +Defined in: [packages/db/src/query/builder/index.ts:803](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L803) Specify that the query should return a single result @@ -244,15 +244,10 @@ query ### from() ```ts -from(source): QueryBuilder<{ - baseSchema: SchemaFromSource; - fromSourceName: keyof TSource & string; - hasJoins: false; - schema: SchemaFromSource; -}>; +from(source): QueryBuilder>; ``` -Defined in: [packages/db/src/query/builder/index.ts:145](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L145) +Defined in: [packages/db/src/query/builder/index.ts:175](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L175) Specify the source table or subquery for the query @@ -266,18 +261,13 @@ Specify the source table or subquery for the query ##### source -`TSource` +[`SingleSource`](../type-aliases/SingleSource.md)\<`TSource`\> An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery #### Returns -[`QueryBuilder`](../type-aliases/QueryBuilder.md)\<\{ - `baseSchema`: [`SchemaFromSource`](../type-aliases/SchemaFromSource.md)\<`TSource`\>; - `fromSourceName`: keyof `TSource` & `string`; - `hasJoins`: `false`; - `schema`: [`SchemaFromSource`](../type-aliases/SchemaFromSource.md)\<`TSource`\>; -\}\> +[`QueryBuilder`](../type-aliases/QueryBuilder.md)\<[`ContextFromSource`](../type-aliases/ContextFromSource.md)\<`TSource`\>\> A QueryBuilder with the specified source @@ -300,7 +290,7 @@ query.from({ activeUsers }) fullJoin(source, onCallback): QueryBuilder, "full">>; ``` -Defined in: [packages/db/src/query/builder/index.ts:336](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L336) +Defined in: [packages/db/src/query/builder/index.ts:410](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L410) Perform a FULL JOIN with another table or subquery @@ -320,7 +310,7 @@ An object with a single key-value pair where the key is the table alias and the ##### onCallback -[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? \{ \[K in string \| number \| symbol\]: ResultValue\\[K\] \} : never \}\[K\] \}\>\> +[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> A function that receives table references and returns the join condition @@ -347,7 +337,7 @@ query groupBy(callback): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:631](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L631) +Defined in: [packages/db/src/query/builder/index.ts:705](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L705) Group rows by one or more columns for aggregation @@ -396,7 +386,7 @@ query having(callback): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:430](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L430) +Defined in: [packages/db/src/query/builder/index.ts:504](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L504) Filter grouped rows based on aggregate conditions @@ -445,7 +435,7 @@ query innerJoin(source, onCallback): QueryBuilder, "inner">>; ``` -Defined in: [packages/db/src/query/builder/index.ts:310](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L310) +Defined in: [packages/db/src/query/builder/index.ts:384](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L384) Perform an INNER JOIN with another table or subquery @@ -465,7 +455,7 @@ An object with a single key-value pair where the key is the table alias and the ##### onCallback -[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? \{ \[K in string \| number \| symbol\]: ResultValue\\[K\] \} : never \}\[K\] \}\>\> +[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> A function that receives table references and returns the join condition @@ -495,7 +485,7 @@ join( type): QueryBuilder, TJoinType>>; ``` -Defined in: [packages/db/src/query/builder/index.ts:188](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L188) +Defined in: [packages/db/src/query/builder/index.ts:262](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L262) Join another table or subquery to the current query @@ -519,7 +509,7 @@ An object with a single key-value pair where the key is the table alias and the ##### onCallback -[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? \{ \[K in string \| number \| symbol\]: ResultValue\\[K\] \} : never \}\[K\] \}\>\> +[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> A function that receives table references and returns the join condition @@ -563,7 +553,7 @@ query leftJoin(source, onCallback): QueryBuilder, "left">>; ``` -Defined in: [packages/db/src/query/builder/index.ts:258](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L258) +Defined in: [packages/db/src/query/builder/index.ts:332](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L332) Perform a LEFT JOIN with another table or subquery @@ -583,7 +573,7 @@ An object with a single key-value pair where the key is the table alias and the ##### onCallback -[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? \{ \[K in string \| number \| symbol\]: ResultValue\\[K\] \} : never \}\[K\] \}\>\> +[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> A function that receives table references and returns the join condition @@ -610,7 +600,7 @@ query limit(count): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:664](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L664) +Defined in: [packages/db/src/query/builder/index.ts:738](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L738) Limit the number of rows returned by the query `orderBy` is required for `limit` @@ -647,7 +637,7 @@ query offset(count): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:688](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L688) +Defined in: [packages/db/src/query/builder/index.ts:762](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L762) Skip a number of rows before returning results `orderBy` is required for `offset` @@ -685,7 +675,7 @@ query orderBy(callback, options): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:555](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L555) +Defined in: [packages/db/src/query/builder/index.ts:629](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L629) Sort the query results by one or more columns @@ -735,7 +725,7 @@ query rightJoin(source, onCallback): QueryBuilder, "right">>; ``` -Defined in: [packages/db/src/query/builder/index.ts:284](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L284) +Defined in: [packages/db/src/query/builder/index.ts:358](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L358) Perform a RIGHT JOIN with another table or subquery @@ -755,7 +745,7 @@ An object with a single key-value pair where the key is the table alias and the ##### onCallback -[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? \{ \[K in string \| number \| symbol\]: ResultValue\\[K\] \} : never \}\[K\] \}\>\> +[`JoinOnCallback`](../type-aliases/JoinOnCallback.md)\<[`MergeContextForJoinCallback`](../type-aliases/MergeContextForJoinCallback.md)\<`TContext`, \{ \[K in string \| number \| symbol\]: \{ \[K in string \| number \| symbol\]: TSource\[K\] extends CollectionImpl\ ? InferCollectionType\ : TSource\[K\] extends QueryBuilder\ ? ResultValue\ : never \}\[K\] \}\>\> A function that receives table references and returns the join condition @@ -784,7 +774,7 @@ query select(callback): QueryBuilder>>; ``` -Defined in: [packages/db/src/query/builder/index.ts:496](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L496) +Defined in: [packages/db/src/query/builder/index.ts:570](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L570) Select specific columns or computed values from the query @@ -843,7 +833,7 @@ query select(callback): QueryBuilder>>; ``` -Defined in: [packages/db/src/query/builder/index.ts:501](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L501) +Defined in: [packages/db/src/query/builder/index.ts:575](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L575) Select specific columns or computed values from the query @@ -898,13 +888,95 @@ query *** +### unionAll() + +#### Call Signature + +```ts +unionAll(source): QueryBuilder>; +``` + +Defined in: [packages/db/src/query/builder/index.ts:201](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L201) + +Union multiple independent source streams in one query. + +##### Type Parameters + +###### TSource + +`TSource` *extends* [`Source`](../type-aliases/Source.md) + +##### Parameters + +###### source + +`TSource` + +An object with one or more aliases mapped to collections or subqueries + +##### Returns + +[`QueryBuilder`](../type-aliases/QueryBuilder.md)\<[`ContextFromUnionSource`](../type-aliases/ContextFromUnionSource.md)\<`TSource`\>\> + +A QueryBuilder with the unioned sources available + +##### Example + +```ts +query + .unionAll({ message: messagesCollection, toolCall: toolCallsCollection }) + .orderBy(({ message, toolCall }) => + coalesce(message.timestamp, toolCall.timestamp) + ) +``` + +#### Call Signature + +```ts +unionAll(...branches): QueryBuilder>; +``` + +Defined in: [packages/db/src/query/builder/index.ts:204](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L204) + +Union multiple independent source streams in one query. + +##### Type Parameters + +###### TBranches + +`TBranches` *extends* readonly \[[`QueryBuilder`](../type-aliases/QueryBuilder.md)\<`any`\>, [`QueryBuilder`](../type-aliases/QueryBuilder.md)\<`any`\>\] + +##### Parameters + +###### branches + +...`TBranches` + +##### Returns + +[`QueryBuilder`](../type-aliases/QueryBuilder.md)\<[`ContextFromUnionBranches`](../type-aliases/ContextFromUnionBranches.md)\<`TBranches`\>\> + +A QueryBuilder with the unioned sources available + +##### Example + +```ts +query + .unionAll({ message: messagesCollection, toolCall: toolCallsCollection }) + .orderBy(({ message, toolCall }) => + coalesce(message.timestamp, toolCall.timestamp) + ) +``` + +*** + ### where() ```ts where(callback): QueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:375](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L375) +Defined in: [packages/db/src/query/builder/index.ts:449](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L449) Filter rows based on a condition diff --git a/docs/reference/classes/BasicIndex.md b/docs/reference/classes/BasicIndex.md index 35df050713..7e6ab4cec6 100644 --- a/docs/reference/classes/BasicIndex.md +++ b/docs/reference/classes/BasicIndex.md @@ -74,7 +74,7 @@ Defined in: [packages/db/src/indexes/basic-index.ts:60](https://github.com/TanSt protected compareOptions: CompareOptions; ``` -Defined in: [packages/db/src/indexes/base-index.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L92) +Defined in: [packages/db/src/indexes/base-index.ts:101](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L101) #### Inherited from @@ -88,7 +88,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:92](https://github.com/TanSta readonly expression: BasicExpression; ``` -Defined in: [packages/db/src/indexes/base-index.ts:86](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L86) +Defined in: [packages/db/src/indexes/base-index.ts:95](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L95) #### Inherited from @@ -96,13 +96,30 @@ Defined in: [packages/db/src/indexes/base-index.ts:86](https://github.com/TanSta *** +### hasCustomComparator + +```ts +protected hasCustomComparator: boolean = false; +``` + +Defined in: [packages/db/src/indexes/base-index.ts:106](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L106) + +Set by subclasses when constructed with a user-supplied comparator, whose +ordering may not match the WHERE evaluator's relational operators. + +#### Inherited from + +[`BaseIndex`](BaseIndex.md).[`hasCustomComparator`](BaseIndex.md#hascustomcomparator) + +*** + ### id ```ts readonly id: number; ``` -Defined in: [packages/db/src/indexes/base-index.ts:84](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L84) +Defined in: [packages/db/src/indexes/base-index.ts:93](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L93) #### Inherited from @@ -116,7 +133,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:84](https://github.com/TanSta protected lastUpdated: Date; ``` -Defined in: [packages/db/src/indexes/base-index.ts:91](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L91) +Defined in: [packages/db/src/indexes/base-index.ts:100](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L100) #### Inherited from @@ -130,7 +147,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:91](https://github.com/TanSta protected lookupCount: number = 0; ``` -Defined in: [packages/db/src/indexes/base-index.ts:89](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L89) +Defined in: [packages/db/src/indexes/base-index.ts:98](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L98) #### Inherited from @@ -144,7 +161,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:89](https://github.com/TanSta readonly optional name: string; ``` -Defined in: [packages/db/src/indexes/base-index.ts:85](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L85) +Defined in: [packages/db/src/indexes/base-index.ts:94](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L94) #### Inherited from @@ -172,7 +189,7 @@ Defined in: [packages/db/src/indexes/basic-index.ts:42](https://github.com/TanSt protected totalLookupTime: number = 0; ``` -Defined in: [packages/db/src/indexes/base-index.ts:90](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L90) +Defined in: [packages/db/src/indexes/base-index.ts:99](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L99) #### Inherited from @@ -188,7 +205,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:90](https://github.com/TanSta get indexedKeysSet(): Set; ``` -Defined in: [packages/db/src/indexes/basic-index.ts:484](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L484) +Defined in: [packages/db/src/indexes/basic-index.ts:485](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L485) ##### Returns @@ -208,7 +225,7 @@ Defined in: [packages/db/src/indexes/basic-index.ts:484](https://github.com/TanS get keyCount(): number; ``` -Defined in: [packages/db/src/indexes/basic-index.ts:238](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L238) +Defined in: [packages/db/src/indexes/basic-index.ts:239](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L239) Gets the number of indexed keys @@ -230,7 +247,7 @@ Gets the number of indexed keys get orderedEntriesArray(): [any, Set][]; ``` -Defined in: [packages/db/src/indexes/basic-index.ts:488](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L488) +Defined in: [packages/db/src/indexes/basic-index.ts:489](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L489) ##### Returns @@ -250,7 +267,7 @@ Defined in: [packages/db/src/indexes/basic-index.ts:488](https://github.com/TanS get orderedEntriesArrayReversed(): [any, Set][]; ``` -Defined in: [packages/db/src/indexes/basic-index.ts:495](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L495) +Defined in: [packages/db/src/indexes/basic-index.ts:496](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L496) ##### Returns @@ -262,6 +279,32 @@ Defined in: [packages/db/src/indexes/basic-index.ts:495](https://github.com/TanS *** +### supportsRangeOptimization + +#### Get Signature + +```ts +get supportsRangeOptimization(): boolean; +``` + +Defined in: [packages/db/src/indexes/base-index.ts:161](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L161) + +Whether range lookups (gt/gte/lt/lte) on this index can be trusted to +return every matching key. Range traversal relies on the index ordering, so +it is unsafe when the index uses a custom comparator, whose order may not +match the WHERE evaluator's relational operators. Callers must fall back to +a full scan when this is `false`. + +##### Returns + +`boolean` + +#### Inherited from + +[`BaseIndex`](BaseIndex.md).[`supportsRangeOptimization`](BaseIndex.md#supportsrangeoptimization) + +*** + ### valueMapData #### Get Signature @@ -270,7 +313,7 @@ Defined in: [packages/db/src/indexes/basic-index.ts:495](https://github.com/TanS get valueMapData(): Map>; ``` -Defined in: [packages/db/src/indexes/basic-index.ts:504](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L504) +Defined in: [packages/db/src/indexes/basic-index.ts:505](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L505) ##### Returns @@ -288,7 +331,7 @@ Defined in: [packages/db/src/indexes/basic-index.ts:504](https://github.com/TanS add(key, item): void; ``` -Defined in: [packages/db/src/indexes/basic-index.ts:78](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L78) +Defined in: [packages/db/src/indexes/basic-index.ts:79](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L79) Adds a value to the index @@ -318,7 +361,7 @@ Adds a value to the index build(entries): void; ``` -Defined in: [packages/db/src/indexes/basic-index.ts:156](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L156) +Defined in: [packages/db/src/indexes/basic-index.ts:157](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L157) Builds the index from a collection of entries @@ -344,7 +387,7 @@ Builds the index from a collection of entries clear(): void; ``` -Defined in: [packages/db/src/indexes/basic-index.ts:193](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L193) +Defined in: [packages/db/src/indexes/basic-index.ts:194](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L194) Clears all data from the index @@ -364,7 +407,7 @@ Clears all data from the index equalityLookup(value): Set; ``` -Defined in: [packages/db/src/indexes/basic-index.ts:245](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L245) +Defined in: [packages/db/src/indexes/basic-index.ts:246](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L246) Performs an equality lookup - O(1) @@ -390,7 +433,7 @@ Performs an equality lookup - O(1) protected evaluateIndexExpression(item): any; ``` -Defined in: [packages/db/src/indexes/base-index.ts:194](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L194) +Defined in: [packages/db/src/indexes/base-index.ts:212](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L212) #### Parameters @@ -414,7 +457,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:194](https://github.com/TanSt getStats(): IndexStats; ``` -Defined in: [packages/db/src/indexes/base-index.ts:182](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L182) +Defined in: [packages/db/src/indexes/base-index.ts:200](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L200) #### Returns @@ -432,7 +475,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:182](https://github.com/TanSt inArrayLookup(values): Set; ``` -Defined in: [packages/db/src/indexes/basic-index.ts:469](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L469) +Defined in: [packages/db/src/indexes/basic-index.ts:470](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L470) Performs an IN array lookup - O(k) where k is values.length @@ -458,7 +501,7 @@ Performs an IN array lookup - O(k) where k is values.length protected initialize(_options?): void; ``` -Defined in: [packages/db/src/indexes/basic-index.ts:73](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L73) +Defined in: [packages/db/src/indexes/basic-index.ts:74](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L74) #### Parameters @@ -482,7 +525,7 @@ Defined in: [packages/db/src/indexes/basic-index.ts:73](https://github.com/TanSt lookup(operation, value): Set; ``` -Defined in: [packages/db/src/indexes/basic-index.ts:203](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L203) +Defined in: [packages/db/src/indexes/basic-index.ts:204](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L204) Performs a lookup operation @@ -512,7 +555,7 @@ Performs a lookup operation matchesCompareOptions(compareOptions): boolean; ``` -Defined in: [packages/db/src/indexes/base-index.ts:159](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L159) +Defined in: [packages/db/src/indexes/base-index.ts:177](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L177) Checks if the compare options match the index's compare options. The direction is ignored because the index can be reversed if the direction is different. @@ -539,7 +582,7 @@ The direction is ignored because the index can be reversed if the direction is d matchesDirection(direction): boolean; ``` -Defined in: [packages/db/src/indexes/base-index.ts:178](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L178) +Defined in: [packages/db/src/indexes/base-index.ts:196](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L196) Checks if the index matches the provided direction. @@ -565,7 +608,7 @@ Checks if the index matches the provided direction. matchesField(fieldPath): boolean; ``` -Defined in: [packages/db/src/indexes/base-index.ts:147](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L147) +Defined in: [packages/db/src/indexes/base-index.ts:165](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L165) #### Parameters @@ -589,7 +632,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:147](https://github.com/TanSt rangeQuery(options): Set; ``` -Defined in: [packages/db/src/indexes/basic-index.ts:253](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L253) +Defined in: [packages/db/src/indexes/basic-index.ts:254](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L254) Performs a range query using binary search - O(log n + m) @@ -615,7 +658,7 @@ Performs a range query using binary search - O(log n + m) rangeQueryReversed(options): Set; ``` -Defined in: [packages/db/src/indexes/basic-index.ts:314](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L314) +Defined in: [packages/db/src/indexes/basic-index.ts:315](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L315) Performs a reversed range query @@ -641,7 +684,7 @@ Performs a reversed range query remove(key, item): void; ``` -Defined in: [packages/db/src/indexes/basic-index.ts:114](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L114) +Defined in: [packages/db/src/indexes/basic-index.ts:115](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L115) Removes a value from the index @@ -671,7 +714,7 @@ Removes a value from the index supports(operation): boolean; ``` -Defined in: [packages/db/src/indexes/base-index.ts:143](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L143) +Defined in: [packages/db/src/indexes/base-index.ts:157](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L157) #### Parameters @@ -698,7 +741,7 @@ take( filterFn?): TKey[]; ``` -Defined in: [packages/db/src/indexes/basic-index.ts:339](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L339) +Defined in: [packages/db/src/indexes/basic-index.ts:340](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L340) Returns the next n items in sorted order @@ -732,7 +775,7 @@ Returns the next n items in sorted order takeFromStart(n, filterFn?): TKey[]; ``` -Defined in: [packages/db/src/indexes/basic-index.ts:424](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L424) +Defined in: [packages/db/src/indexes/basic-index.ts:425](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L425) Returns the first n items in sorted order (from the start) @@ -765,7 +808,7 @@ takeReversed( filterFn?): TKey[]; ``` -Defined in: [packages/db/src/indexes/basic-index.ts:381](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L381) +Defined in: [packages/db/src/indexes/basic-index.ts:382](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L382) Returns the next n items in reverse sorted order @@ -799,7 +842,7 @@ Returns the next n items in reverse sorted order takeReversedFromEnd(n, filterFn?): TKey[]; ``` -Defined in: [packages/db/src/indexes/basic-index.ts:443](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L443) +Defined in: [packages/db/src/indexes/basic-index.ts:444](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L444) Returns the first n items in reverse sorted order (from the end) @@ -829,7 +872,7 @@ Returns the first n items in reverse sorted order (from the end) protected trackLookup(startTime): void; ``` -Defined in: [packages/db/src/indexes/base-index.ts:199](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L199) +Defined in: [packages/db/src/indexes/base-index.ts:217](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L217) #### Parameters @@ -856,7 +899,7 @@ update( newItem): void; ``` -Defined in: [packages/db/src/indexes/basic-index.ts:148](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L148) +Defined in: [packages/db/src/indexes/basic-index.ts:149](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/basic-index.ts#L149) Updates a value in the index @@ -890,7 +933,7 @@ Updates a value in the index protected updateTimestamp(): void; ``` -Defined in: [packages/db/src/indexes/base-index.ts:205](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L205) +Defined in: [packages/db/src/indexes/base-index.ts:223](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L223) #### Returns diff --git a/docs/reference/classes/CannotCombineEmptyExpressionListError.md b/docs/reference/classes/CannotCombineEmptyExpressionListError.md index 121f7481d0..caa115e143 100644 --- a/docs/reference/classes/CannotCombineEmptyExpressionListError.md +++ b/docs/reference/classes/CannotCombineEmptyExpressionListError.md @@ -5,7 +5,7 @@ title: CannotCombineEmptyExpressionListError # Class: CannotCombineEmptyExpressionListError -Defined in: [packages/db/src/errors.ts:693](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L693) +Defined in: [packages/db/src/errors.ts:718](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L718) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:693](https://github.com/TanStack/db/blob/ new CannotCombineEmptyExpressionListError(): CannotCombineEmptyExpressionListError; ``` -Defined in: [packages/db/src/errors.ts:694](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L694) +Defined in: [packages/db/src/errors.ts:719](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L719) #### Returns diff --git a/docs/reference/classes/CollectionImpl.md b/docs/reference/classes/CollectionImpl.md index 18c8c74d69..a287af2c24 100644 --- a/docs/reference/classes/CollectionImpl.md +++ b/docs/reference/classes/CollectionImpl.md @@ -5,7 +5,7 @@ title: CollectionImpl # Class: CollectionImpl\ -Defined in: [packages/db/src/collection/index.ts:272](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L272) +Defined in: [packages/db/src/collection/index.ts:273](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L273) ## Extended by @@ -42,7 +42,7 @@ Defined in: [packages/db/src/collection/index.ts:272](https://github.com/TanStac new CollectionImpl(config): CollectionImpl; ``` -Defined in: [packages/db/src/collection/index.ts:318](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L318) +Defined in: [packages/db/src/collection/index.ts:319](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L319) Creates a new Collection instance @@ -70,7 +70,7 @@ Error if sync config is missing _lifecycle: CollectionLifecycleManager; ``` -Defined in: [packages/db/src/collection/index.ts:289](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L289) +Defined in: [packages/db/src/collection/index.ts:290](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L290) *** @@ -80,7 +80,7 @@ Defined in: [packages/db/src/collection/index.ts:289](https://github.com/TanStac _state: CollectionStateManager; ``` -Defined in: [packages/db/src/collection/index.ts:301](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L301) +Defined in: [packages/db/src/collection/index.ts:302](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L302) *** @@ -90,7 +90,7 @@ Defined in: [packages/db/src/collection/index.ts:301](https://github.com/TanStac _sync: CollectionSyncManager; ``` -Defined in: [packages/db/src/collection/index.ts:290](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L290) +Defined in: [packages/db/src/collection/index.ts:291](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L291) *** @@ -100,7 +100,7 @@ Defined in: [packages/db/src/collection/index.ts:290](https://github.com/TanStac config: CollectionConfig; ``` -Defined in: [packages/db/src/collection/index.ts:280](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L280) +Defined in: [packages/db/src/collection/index.ts:281](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L281) *** @@ -110,7 +110,7 @@ Defined in: [packages/db/src/collection/index.ts:280](https://github.com/TanStac deferDataRefresh: Promise | null = null; ``` -Defined in: [packages/db/src/collection/index.ts:308](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L308) +Defined in: [packages/db/src/collection/index.ts:309](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L309) When set, collection consumers should defer processing incoming data refreshes until this promise resolves. This prevents stale data from @@ -124,7 +124,7 @@ overwriting optimistic state while pending writes are being applied. id: string; ``` -Defined in: [packages/db/src/collection/index.ts:279](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L279) +Defined in: [packages/db/src/collection/index.ts:280](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L280) *** @@ -134,7 +134,7 @@ Defined in: [packages/db/src/collection/index.ts:279](https://github.com/TanStac utils: Record = {}; ``` -Defined in: [packages/db/src/collection/index.ts:284](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L284) +Defined in: [packages/db/src/collection/index.ts:285](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L285) ## Accessors @@ -146,7 +146,7 @@ Defined in: [packages/db/src/collection/index.ts:284](https://github.com/TanStac get compareOptions(): StringCollationConfig; ``` -Defined in: [packages/db/src/collection/index.ts:643](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L643) +Defined in: [packages/db/src/collection/index.ts:644](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L644) ##### Returns @@ -162,7 +162,7 @@ Defined in: [packages/db/src/collection/index.ts:643](https://github.com/TanStac get indexes(): Map>; ``` -Defined in: [packages/db/src/collection/index.ts:628](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L628) +Defined in: [packages/db/src/collection/index.ts:629](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L629) Get resolved indexes for query optimization @@ -180,7 +180,7 @@ Get resolved indexes for query optimization get isLoadingSubset(): boolean; ``` -Defined in: [packages/db/src/collection/index.ts:456](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L456) +Defined in: [packages/db/src/collection/index.ts:457](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L457) Check if the collection is currently loading more data @@ -200,7 +200,7 @@ true if the collection has pending load more operations, false otherwise get size(): number; ``` -Defined in: [packages/db/src/collection/index.ts:493](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L493) +Defined in: [packages/db/src/collection/index.ts:494](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L494) Get the current size of the collection (cached) @@ -218,7 +218,7 @@ Get the current size of the collection (cached) get state(): Map>; ``` -Defined in: [packages/db/src/collection/index.ts:820](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L820) +Defined in: [packages/db/src/collection/index.ts:821](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L821) Gets the current state of the collection as a Map @@ -254,7 +254,7 @@ Map containing all items in the collection, with keys as identifiers get status(): CollectionStatus; ``` -Defined in: [packages/db/src/collection/index.ts:411](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L411) +Defined in: [packages/db/src/collection/index.ts:412](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L412) Gets the current status of the collection @@ -272,7 +272,7 @@ Gets the current status of the collection get subscriberCount(): number; ``` -Defined in: [packages/db/src/collection/index.ts:418](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L418) +Defined in: [packages/db/src/collection/index.ts:419](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L419) Get the number of subscribers to the collection @@ -290,7 +290,7 @@ Get the number of subscribers to the collection get toArray(): WithVirtualProps[]; ``` -Defined in: [packages/db/src/collection/index.ts:849](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L849) +Defined in: [packages/db/src/collection/index.ts:850](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L850) Gets the current state of the collection as an Array @@ -308,7 +308,7 @@ An Array containing all items in the collection iterator: IterableIterator<[TKey, WithVirtualProps]>; ``` -Defined in: [packages/db/src/collection/index.ts:531](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L531) +Defined in: [packages/db/src/collection/index.ts:532](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L532) Get all entries (virtual derived state) @@ -324,7 +324,7 @@ Get all entries (virtual derived state) cleanup(): Promise; ``` -Defined in: [packages/db/src/collection/index.ts:988](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L988) +Defined in: [packages/db/src/collection/index.ts:989](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L989) Clean up the collection by stopping sync and clearing data This can be called manually or automatically by garbage collection @@ -341,7 +341,7 @@ This can be called manually or automatically by garbage collection createIndex(indexCallback, config): BaseIndex; ``` -Defined in: [packages/db/src/collection/index.ts:597](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L597) +Defined in: [packages/db/src/collection/index.ts:598](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L598) Creates an index on a collection for faster queries. Indexes significantly improve query performance by allowing constant time lookups @@ -397,7 +397,7 @@ currentStateAsChanges(options): | ChangeMessage, string | number>[]; ``` -Defined in: [packages/db/src/collection/index.ts:887](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L887) +Defined in: [packages/db/src/collection/index.ts:888](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L888) Returns the current state of the collection as an array of changes @@ -441,7 +441,7 @@ const activeChanges = collection.currentStateAsChanges({ delete(keys, config?): Transaction; ``` -Defined in: [packages/db/src/collection/index.ts:797](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L797) +Defined in: [packages/db/src/collection/index.ts:798](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L798) Deletes one or more items from the collection @@ -504,7 +504,7 @@ try { entries(): IterableIterator<[TKey, WithVirtualProps]>; ``` -Defined in: [packages/db/src/collection/index.ts:519](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L519) +Defined in: [packages/db/src/collection/index.ts:520](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L520) Get all entries (virtual derived state) @@ -520,7 +520,7 @@ Get all entries (virtual derived state) forEach(callbackfn): void; ``` -Defined in: [packages/db/src/collection/index.ts:540](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L540) +Defined in: [packages/db/src/collection/index.ts:541](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L541) Execute a callback for each entry in the collection @@ -544,7 +544,7 @@ get(key): | undefined; ``` -Defined in: [packages/db/src/collection/index.ts:479](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L479) +Defined in: [packages/db/src/collection/index.ts:480](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L480) Get the current value for a key (virtual derived state) @@ -567,7 +567,7 @@ Get the current value for a key (virtual derived state) getIndexMetadata(): CollectionIndexMetadata[]; ``` -Defined in: [packages/db/src/collection/index.ts:621](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L621) +Defined in: [packages/db/src/collection/index.ts:622](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L622) Returns a snapshot of current index metadata sorted by indexId. Persistence wrappers can use this to bootstrap index state if indexes were @@ -585,7 +585,7 @@ created before event listeners were attached. getKeyFromItem(item): TKey; ``` -Defined in: [packages/db/src/collection/index.ts:571](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L571) +Defined in: [packages/db/src/collection/index.ts:572](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L572) #### Parameters @@ -605,7 +605,7 @@ Defined in: [packages/db/src/collection/index.ts:571](https://github.com/TanStac has(key): boolean; ``` -Defined in: [packages/db/src/collection/index.ts:486](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L486) +Defined in: [packages/db/src/collection/index.ts:487](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L487) Check if a key exists in the collection (virtual derived state) @@ -627,7 +627,7 @@ Check if a key exists in the collection (virtual derived state) insert(data, config?): Transaction>; ``` -Defined in: [packages/db/src/collection/index.ts:684](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L684) +Defined in: [packages/db/src/collection/index.ts:685](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L685) Inserts one or more items into the collection @@ -697,7 +697,7 @@ try { isReady(): boolean; ``` -Defined in: [packages/db/src/collection/index.ts:448](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L448) +Defined in: [packages/db/src/collection/index.ts:449](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L449) Check if the collection is ready for use Returns true if the collection has been marked as ready by its sync implementation @@ -727,7 +727,7 @@ if (collection.isReady()) { keys(): IterableIterator; ``` -Defined in: [packages/db/src/collection/index.ts:500](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L500) +Defined in: [packages/db/src/collection/index.ts:501](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L501) Get all keys (virtual derived state) @@ -743,7 +743,7 @@ Get all keys (virtual derived state) map(callbackfn): U[]; ``` -Defined in: [packages/db/src/collection/index.ts:556](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L556) +Defined in: [packages/db/src/collection/index.ts:557](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L557) Create a new array with the results of calling a function for each entry in the collection @@ -771,7 +771,7 @@ Create a new array with the results of calling a function for each entry in the off(event, callback): void; ``` -Defined in: [packages/db/src/collection/index.ts:967](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L967) +Defined in: [packages/db/src/collection/index.ts:968](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L968) Unsubscribe from a collection event @@ -814,7 +814,7 @@ Unsubscribe from a collection event on(event, callback): () => void; ``` -Defined in: [packages/db/src/collection/index.ts:947](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L947) +Defined in: [packages/db/src/collection/index.ts:948](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L948) Subscribe to a collection event @@ -863,7 +863,7 @@ Subscribe to a collection event once(event, callback): () => void; ``` -Defined in: [packages/db/src/collection/index.ts:957](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L957) +Defined in: [packages/db/src/collection/index.ts:958](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L958) Subscribe to a collection event once @@ -912,7 +912,7 @@ Subscribe to a collection event once onFirstReady(callback): void; ``` -Defined in: [packages/db/src/collection/index.ts:432](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L432) +Defined in: [packages/db/src/collection/index.ts:433](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L433) Register a callback to be executed when the collection first becomes ready Useful for preloading collections @@ -946,7 +946,7 @@ collection.onFirstReady(() => { preload(): Promise; ``` -Defined in: [packages/db/src/collection/index.ts:472](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L472) +Defined in: [packages/db/src/collection/index.ts:473](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L473) Preload the collection data by starting sync if not already started Multiple concurrent calls will share the same promise @@ -963,7 +963,7 @@ Multiple concurrent calls will share the same promise removeIndex(indexOrId): boolean; ``` -Defined in: [packages/db/src/collection/index.ts:612](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L612) +Defined in: [packages/db/src/collection/index.ts:613](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L613) Removes an index created with createIndex. Returns true when an index existed and was removed. @@ -990,7 +990,7 @@ as invalid after removal. startSyncImmediate(): void; ``` -Defined in: [packages/db/src/collection/index.ts:464](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L464) +Defined in: [packages/db/src/collection/index.ts:465](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L465) Start sync immediately - internal method for compiled queries This bypasses lazy loading for special cases like live query results @@ -1007,7 +1007,7 @@ This bypasses lazy loading for special cases like live query results stateWhenReady(): Promise>>; ``` -Defined in: [packages/db/src/collection/index.ts:834](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L834) +Defined in: [packages/db/src/collection/index.ts:835](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L835) Gets the current state of the collection as a Map, but only resolves when data is available Waits for the first sync commit to complete before resolving @@ -1026,7 +1026,7 @@ Promise that resolves to a Map containing all items in the collection subscribeChanges(callback, options): CollectionSubscription; ``` -Defined in: [packages/db/src/collection/index.ts:935](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L935) +Defined in: [packages/db/src/collection/index.ts:936](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L936) Subscribe to changes in the collection @@ -1101,7 +1101,7 @@ const subscription = collection.subscribeChanges((changes) => { toArrayWhenReady(): Promise[]>; ``` -Defined in: [packages/db/src/collection/index.ts:859](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L859) +Defined in: [packages/db/src/collection/index.ts:860](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L860) Gets the current state of the collection as an Array, but only resolves when data is available Waits for the first sync commit to complete before resolving @@ -1122,7 +1122,7 @@ Promise that resolves to an Array containing all items in the collection update(key, callback): Transaction; ``` -Defined in: [packages/db/src/collection/index.ts:729](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L729) +Defined in: [packages/db/src/collection/index.ts:730](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L730) Updates one or more items in the collection using a callback function @@ -1193,7 +1193,7 @@ update( callback): Transaction; ``` -Defined in: [packages/db/src/collection/index.ts:735](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L735) +Defined in: [packages/db/src/collection/index.ts:736](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L736) Updates one or more items in the collection using a callback function @@ -1267,7 +1267,7 @@ try { update(id, callback): Transaction; ``` -Defined in: [packages/db/src/collection/index.ts:742](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L742) +Defined in: [packages/db/src/collection/index.ts:743](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L743) Updates one or more items in the collection using a callback function @@ -1338,7 +1338,7 @@ update( callback): Transaction; ``` -Defined in: [packages/db/src/collection/index.ts:748](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L748) +Defined in: [packages/db/src/collection/index.ts:749](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L749) Updates one or more items in the collection using a callback function @@ -1415,7 +1415,7 @@ validateData( key?): TOutput; ``` -Defined in: [packages/db/src/collection/index.ts:635](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L635) +Defined in: [packages/db/src/collection/index.ts:636](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L636) Validates the data against the schema @@ -1445,7 +1445,7 @@ Validates the data against the schema values(): IterableIterator>; ``` -Defined in: [packages/db/src/collection/index.ts:507](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L507) +Defined in: [packages/db/src/collection/index.ts:508](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L508) Get all values (virtual derived state) @@ -1461,7 +1461,7 @@ Get all values (virtual derived state) waitFor(event, timeout?): Promise; ``` -Defined in: [packages/db/src/collection/index.ts:977](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L977) +Defined in: [packages/db/src/collection/index.ts:978](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L978) Wait for a collection event diff --git a/docs/reference/classes/CollectionInputNotFoundError.md b/docs/reference/classes/CollectionInputNotFoundError.md index b2d75b3f58..0dff36d276 100644 --- a/docs/reference/classes/CollectionInputNotFoundError.md +++ b/docs/reference/classes/CollectionInputNotFoundError.md @@ -5,7 +5,7 @@ title: CollectionInputNotFoundError # Class: CollectionInputNotFoundError -Defined in: [packages/db/src/errors.ts:474](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L474) +Defined in: [packages/db/src/errors.ts:499](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L499) Error thrown when a collection input stream is not found during query compilation. In self-joins, each alias (e.g., 'employee', 'manager') requires its own input stream. @@ -25,7 +25,7 @@ new CollectionInputNotFoundError( availableKeys?): CollectionInputNotFoundError; ``` -Defined in: [packages/db/src/errors.ts:475](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L475) +Defined in: [packages/db/src/errors.ts:500](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L500) #### Parameters diff --git a/docs/reference/classes/DistinctRequiresSelectError.md b/docs/reference/classes/DistinctRequiresSelectError.md index c72c004535..15b7fcd2fe 100644 --- a/docs/reference/classes/DistinctRequiresSelectError.md +++ b/docs/reference/classes/DistinctRequiresSelectError.md @@ -5,7 +5,7 @@ title: DistinctRequiresSelectError # Class: DistinctRequiresSelectError -Defined in: [packages/db/src/errors.ts:430](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L430) +Defined in: [packages/db/src/errors.ts:455](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L455) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:430](https://github.com/TanStack/db/blob/ new DistinctRequiresSelectError(): DistinctRequiresSelectError; ``` -Defined in: [packages/db/src/errors.ts:431](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L431) +Defined in: [packages/db/src/errors.ts:456](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L456) #### Returns diff --git a/docs/reference/classes/DuplicateAliasInSubqueryError.md b/docs/reference/classes/DuplicateAliasInSubqueryError.md index d32ef272f9..3caada57de 100644 --- a/docs/reference/classes/DuplicateAliasInSubqueryError.md +++ b/docs/reference/classes/DuplicateAliasInSubqueryError.md @@ -5,7 +5,7 @@ title: DuplicateAliasInSubqueryError # Class: DuplicateAliasInSubqueryError -Defined in: [packages/db/src/errors.ts:495](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L495) +Defined in: [packages/db/src/errors.ts:520](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L520) Error thrown when a subquery uses the same alias as its parent query. This causes issues because parent and subquery would share the same input streams, @@ -23,7 +23,7 @@ leading to empty results or incorrect data (aggregation cross-leaking). new DuplicateAliasInSubqueryError(alias, parentAliases): DuplicateAliasInSubqueryError; ``` -Defined in: [packages/db/src/errors.ts:496](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L496) +Defined in: [packages/db/src/errors.ts:521](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L521) #### Parameters diff --git a/docs/reference/classes/EmptyReferencePathError.md b/docs/reference/classes/EmptyReferencePathError.md index 2b5583daa0..7881e98bcf 100644 --- a/docs/reference/classes/EmptyReferencePathError.md +++ b/docs/reference/classes/EmptyReferencePathError.md @@ -5,7 +5,7 @@ title: EmptyReferencePathError # Class: EmptyReferencePathError -Defined in: [packages/db/src/errors.ts:518](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L518) +Defined in: [packages/db/src/errors.ts:543](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L543) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:518](https://github.com/TanStack/db/blob/ new EmptyReferencePathError(): EmptyReferencePathError; ``` -Defined in: [packages/db/src/errors.ts:519](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L519) +Defined in: [packages/db/src/errors.ts:544](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L544) #### Returns diff --git a/docs/reference/classes/FnSelectWithGroupByError.md b/docs/reference/classes/FnSelectWithGroupByError.md index 4886f6e568..83f4fd0e33 100644 --- a/docs/reference/classes/FnSelectWithGroupByError.md +++ b/docs/reference/classes/FnSelectWithGroupByError.md @@ -5,7 +5,7 @@ title: FnSelectWithGroupByError # Class: FnSelectWithGroupByError -Defined in: [packages/db/src/errors.ts:436](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L436) +Defined in: [packages/db/src/errors.ts:461](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L461) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:436](https://github.com/TanStack/db/blob/ new FnSelectWithGroupByError(): FnSelectWithGroupByError; ``` -Defined in: [packages/db/src/errors.ts:437](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L437) +Defined in: [packages/db/src/errors.ts:462](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L462) #### Returns diff --git a/docs/reference/classes/GroupByError.md b/docs/reference/classes/GroupByError.md index c010300fba..96ecd611be 100644 --- a/docs/reference/classes/GroupByError.md +++ b/docs/reference/classes/GroupByError.md @@ -5,7 +5,7 @@ title: GroupByError # Class: GroupByError -Defined in: [packages/db/src/errors.ts:593](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L593) +Defined in: [packages/db/src/errors.ts:618](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L618) ## Extends @@ -26,7 +26,7 @@ Defined in: [packages/db/src/errors.ts:593](https://github.com/TanStack/db/blob/ new GroupByError(message): GroupByError; ``` -Defined in: [packages/db/src/errors.ts:594](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L594) +Defined in: [packages/db/src/errors.ts:619](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L619) #### Parameters diff --git a/docs/reference/classes/HavingRequiresGroupByError.md b/docs/reference/classes/HavingRequiresGroupByError.md index bbf7cdb8fe..c4a7af348a 100644 --- a/docs/reference/classes/HavingRequiresGroupByError.md +++ b/docs/reference/classes/HavingRequiresGroupByError.md @@ -5,7 +5,7 @@ title: HavingRequiresGroupByError # Class: HavingRequiresGroupByError -Defined in: [packages/db/src/errors.ts:456](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L456) +Defined in: [packages/db/src/errors.ts:481](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L481) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:456](https://github.com/TanStack/db/blob/ new HavingRequiresGroupByError(): HavingRequiresGroupByError; ``` -Defined in: [packages/db/src/errors.ts:457](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L457) +Defined in: [packages/db/src/errors.ts:482](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L482) #### Returns diff --git a/docs/reference/classes/InvalidJoinCondition.md b/docs/reference/classes/InvalidJoinCondition.md index 777521bdc4..2079d31c5c 100644 --- a/docs/reference/classes/InvalidJoinCondition.md +++ b/docs/reference/classes/InvalidJoinCondition.md @@ -5,7 +5,7 @@ title: InvalidJoinCondition # Class: InvalidJoinCondition -Defined in: [packages/db/src/errors.ts:580](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L580) +Defined in: [packages/db/src/errors.ts:605](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L605) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:580](https://github.com/TanStack/db/blob/ new InvalidJoinCondition(): InvalidJoinCondition; ``` -Defined in: [packages/db/src/errors.ts:581](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L581) +Defined in: [packages/db/src/errors.ts:606](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L606) #### Returns diff --git a/docs/reference/classes/InvalidJoinConditionLeftSourceError.md b/docs/reference/classes/InvalidJoinConditionLeftSourceError.md index 2c6c6257df..f0abb3daa6 100644 --- a/docs/reference/classes/InvalidJoinConditionLeftSourceError.md +++ b/docs/reference/classes/InvalidJoinConditionLeftSourceError.md @@ -5,7 +5,7 @@ title: InvalidJoinConditionLeftSourceError # Class: InvalidJoinConditionLeftSourceError -Defined in: [packages/db/src/errors.ts:564](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L564) +Defined in: [packages/db/src/errors.ts:589](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L589) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:564](https://github.com/TanStack/db/blob/ new InvalidJoinConditionLeftSourceError(sourceAlias): InvalidJoinConditionLeftSourceError; ``` -Defined in: [packages/db/src/errors.ts:565](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L565) +Defined in: [packages/db/src/errors.ts:590](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L590) #### Parameters diff --git a/docs/reference/classes/InvalidJoinConditionRightSourceError.md b/docs/reference/classes/InvalidJoinConditionRightSourceError.md index 92f28a9565..7d559c3e48 100644 --- a/docs/reference/classes/InvalidJoinConditionRightSourceError.md +++ b/docs/reference/classes/InvalidJoinConditionRightSourceError.md @@ -5,7 +5,7 @@ title: InvalidJoinConditionRightSourceError # Class: InvalidJoinConditionRightSourceError -Defined in: [packages/db/src/errors.ts:572](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L572) +Defined in: [packages/db/src/errors.ts:597](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L597) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:572](https://github.com/TanStack/db/blob/ new InvalidJoinConditionRightSourceError(sourceAlias): InvalidJoinConditionRightSourceError; ``` -Defined in: [packages/db/src/errors.ts:573](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L573) +Defined in: [packages/db/src/errors.ts:598](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L598) #### Parameters diff --git a/docs/reference/classes/InvalidJoinConditionSameSourceError.md b/docs/reference/classes/InvalidJoinConditionSameSourceError.md index 8b20d62b36..f6fa518a41 100644 --- a/docs/reference/classes/InvalidJoinConditionSameSourceError.md +++ b/docs/reference/classes/InvalidJoinConditionSameSourceError.md @@ -5,7 +5,7 @@ title: InvalidJoinConditionSameSourceError # Class: InvalidJoinConditionSameSourceError -Defined in: [packages/db/src/errors.ts:550](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L550) +Defined in: [packages/db/src/errors.ts:575](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L575) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:550](https://github.com/TanStack/db/blob/ new InvalidJoinConditionSameSourceError(sourceAlias): InvalidJoinConditionSameSourceError; ``` -Defined in: [packages/db/src/errors.ts:551](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L551) +Defined in: [packages/db/src/errors.ts:576](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L576) #### Parameters diff --git a/docs/reference/classes/InvalidJoinConditionSourceMismatchError.md b/docs/reference/classes/InvalidJoinConditionSourceMismatchError.md index 3978012ae8..5e7f0db7da 100644 --- a/docs/reference/classes/InvalidJoinConditionSourceMismatchError.md +++ b/docs/reference/classes/InvalidJoinConditionSourceMismatchError.md @@ -5,7 +5,7 @@ title: InvalidJoinConditionSourceMismatchError # Class: InvalidJoinConditionSourceMismatchError -Defined in: [packages/db/src/errors.ts:558](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L558) +Defined in: [packages/db/src/errors.ts:583](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L583) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:558](https://github.com/TanStack/db/blob/ new InvalidJoinConditionSourceMismatchError(): InvalidJoinConditionSourceMismatchError; ``` -Defined in: [packages/db/src/errors.ts:559](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L559) +Defined in: [packages/db/src/errors.ts:584](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L584) #### Returns diff --git a/docs/reference/classes/InvalidSourceTypeError.md b/docs/reference/classes/InvalidSourceTypeError.md index 7128f7249f..b7479a9efd 100644 --- a/docs/reference/classes/InvalidSourceTypeError.md +++ b/docs/reference/classes/InvalidSourceTypeError.md @@ -5,7 +5,7 @@ title: InvalidSourceTypeError # Class: InvalidSourceTypeError -Defined in: [packages/db/src/errors.ts:388](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L388) +Defined in: [packages/db/src/errors.ts:393](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L393) ## Extends @@ -19,13 +19,13 @@ Defined in: [packages/db/src/errors.ts:388](https://github.com/TanStack/db/blob/ new InvalidSourceTypeError(context, type): InvalidSourceTypeError; ``` -Defined in: [packages/db/src/errors.ts:389](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L389) +Defined in: [packages/db/src/errors.ts:394](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L394) #### Parameters ##### context -`string` +[`SourceClauseContext`](../type-aliases/SourceClauseContext.md) ##### type diff --git a/docs/reference/classes/InvalidStorageDataFormatError.md b/docs/reference/classes/InvalidStorageDataFormatError.md index c7ec40856c..12a18460c6 100644 --- a/docs/reference/classes/InvalidStorageDataFormatError.md +++ b/docs/reference/classes/InvalidStorageDataFormatError.md @@ -5,7 +5,7 @@ title: InvalidStorageDataFormatError # Class: InvalidStorageDataFormatError -Defined in: [packages/db/src/errors.ts:658](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L658) +Defined in: [packages/db/src/errors.ts:683](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L683) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:658](https://github.com/TanStack/db/blob/ new InvalidStorageDataFormatError(storageKey, key): InvalidStorageDataFormatError; ``` -Defined in: [packages/db/src/errors.ts:659](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L659) +Defined in: [packages/db/src/errors.ts:684](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L684) #### Parameters diff --git a/docs/reference/classes/InvalidStorageObjectFormatError.md b/docs/reference/classes/InvalidStorageObjectFormatError.md index d2d3cec6e8..92161868e9 100644 --- a/docs/reference/classes/InvalidStorageObjectFormatError.md +++ b/docs/reference/classes/InvalidStorageObjectFormatError.md @@ -5,7 +5,7 @@ title: InvalidStorageObjectFormatError # Class: InvalidStorageObjectFormatError -Defined in: [packages/db/src/errors.ts:666](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L666) +Defined in: [packages/db/src/errors.ts:691](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L691) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:666](https://github.com/TanStack/db/blob/ new InvalidStorageObjectFormatError(storageKey): InvalidStorageObjectFormatError; ``` -Defined in: [packages/db/src/errors.ts:667](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L667) +Defined in: [packages/db/src/errors.ts:692](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L692) #### Parameters diff --git a/docs/reference/classes/InvalidWhereExpressionError.md b/docs/reference/classes/InvalidWhereExpressionError.md index 26649e0ce7..cc1f4e394f 100644 --- a/docs/reference/classes/InvalidWhereExpressionError.md +++ b/docs/reference/classes/InvalidWhereExpressionError.md @@ -5,7 +5,7 @@ title: InvalidWhereExpressionError # Class: InvalidWhereExpressionError -Defined in: [packages/db/src/errors.ts:409](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L409) +Defined in: [packages/db/src/errors.ts:424](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L424) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:409](https://github.com/TanStack/db/blob/ new InvalidWhereExpressionError(valueType): InvalidWhereExpressionError; ``` -Defined in: [packages/db/src/errors.ts:410](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L410) +Defined in: [packages/db/src/errors.ts:425](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L425) #### Parameters diff --git a/docs/reference/classes/JoinCollectionNotFoundError.md b/docs/reference/classes/JoinCollectionNotFoundError.md index 1268a7f58e..fa6a1349b1 100644 --- a/docs/reference/classes/JoinCollectionNotFoundError.md +++ b/docs/reference/classes/JoinCollectionNotFoundError.md @@ -5,7 +5,7 @@ title: JoinCollectionNotFoundError # Class: JoinCollectionNotFoundError -Defined in: [packages/db/src/errors.ts:530](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L530) +Defined in: [packages/db/src/errors.ts:555](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L555) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:530](https://github.com/TanStack/db/blob/ new JoinCollectionNotFoundError(collectionId): JoinCollectionNotFoundError; ``` -Defined in: [packages/db/src/errors.ts:531](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L531) +Defined in: [packages/db/src/errors.ts:556](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L556) #### Parameters diff --git a/docs/reference/classes/JoinConditionMustBeEqualityError.md b/docs/reference/classes/JoinConditionMustBeEqualityError.md index fea19bb6b5..f8c95d2dff 100644 --- a/docs/reference/classes/JoinConditionMustBeEqualityError.md +++ b/docs/reference/classes/JoinConditionMustBeEqualityError.md @@ -5,7 +5,7 @@ title: JoinConditionMustBeEqualityError # Class: JoinConditionMustBeEqualityError -Defined in: [packages/db/src/errors.ts:397](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L397) +Defined in: [packages/db/src/errors.ts:412](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L412) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:397](https://github.com/TanStack/db/blob/ new JoinConditionMustBeEqualityError(): JoinConditionMustBeEqualityError; ``` -Defined in: [packages/db/src/errors.ts:398](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L398) +Defined in: [packages/db/src/errors.ts:413](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L413) #### Returns diff --git a/docs/reference/classes/JoinError.md b/docs/reference/classes/JoinError.md index c1154a4667..126de410b5 100644 --- a/docs/reference/classes/JoinError.md +++ b/docs/reference/classes/JoinError.md @@ -5,7 +5,7 @@ title: JoinError # Class: JoinError -Defined in: [packages/db/src/errors.ts:537](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L537) +Defined in: [packages/db/src/errors.ts:562](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L562) ## Extends @@ -29,7 +29,7 @@ Defined in: [packages/db/src/errors.ts:537](https://github.com/TanStack/db/blob/ new JoinError(message): JoinError; ``` -Defined in: [packages/db/src/errors.ts:538](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L538) +Defined in: [packages/db/src/errors.ts:563](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L563) #### Parameters diff --git a/docs/reference/classes/LimitOffsetRequireOrderByError.md b/docs/reference/classes/LimitOffsetRequireOrderByError.md index 6a41edba30..2d19124468 100644 --- a/docs/reference/classes/LimitOffsetRequireOrderByError.md +++ b/docs/reference/classes/LimitOffsetRequireOrderByError.md @@ -5,7 +5,7 @@ title: LimitOffsetRequireOrderByError # Class: LimitOffsetRequireOrderByError -Defined in: [packages/db/src/errors.ts:462](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L462) +Defined in: [packages/db/src/errors.ts:487](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L487) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:462](https://github.com/TanStack/db/blob/ new LimitOffsetRequireOrderByError(): LimitOffsetRequireOrderByError; ``` -Defined in: [packages/db/src/errors.ts:463](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L463) +Defined in: [packages/db/src/errors.ts:488](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L488) #### Returns diff --git a/docs/reference/classes/LocalStorageCollectionError.md b/docs/reference/classes/LocalStorageCollectionError.md index 78725fca5a..488fc1b2b4 100644 --- a/docs/reference/classes/LocalStorageCollectionError.md +++ b/docs/reference/classes/LocalStorageCollectionError.md @@ -5,7 +5,7 @@ title: LocalStorageCollectionError # Class: LocalStorageCollectionError -Defined in: [packages/db/src/errors.ts:645](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L645) +Defined in: [packages/db/src/errors.ts:670](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L670) ## Extends @@ -25,7 +25,7 @@ Defined in: [packages/db/src/errors.ts:645](https://github.com/TanStack/db/blob/ new LocalStorageCollectionError(message): LocalStorageCollectionError; ``` -Defined in: [packages/db/src/errors.ts:646](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L646) +Defined in: [packages/db/src/errors.ts:671](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L671) #### Parameters diff --git a/docs/reference/classes/MissingAliasInputsError.md b/docs/reference/classes/MissingAliasInputsError.md index 8e13aedf05..4b53a6660d 100644 --- a/docs/reference/classes/MissingAliasInputsError.md +++ b/docs/reference/classes/MissingAliasInputsError.md @@ -5,7 +5,7 @@ title: MissingAliasInputsError # Class: MissingAliasInputsError -Defined in: [packages/db/src/errors.ts:742](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L742) +Defined in: [packages/db/src/errors.ts:767](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L767) Internal error when the compiler returns aliases that don't have corresponding input streams. This should never happen since all aliases come from user declarations. @@ -22,7 +22,7 @@ This should never happen since all aliases come from user declarations. new MissingAliasInputsError(missingAliases): MissingAliasInputsError; ``` -Defined in: [packages/db/src/errors.ts:743](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L743) +Defined in: [packages/db/src/errors.ts:768](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L768) #### Parameters diff --git a/docs/reference/classes/NonAggregateExpressionNotInGroupByError.md b/docs/reference/classes/NonAggregateExpressionNotInGroupByError.md index b34fd77838..1b5ceb8f79 100644 --- a/docs/reference/classes/NonAggregateExpressionNotInGroupByError.md +++ b/docs/reference/classes/NonAggregateExpressionNotInGroupByError.md @@ -5,7 +5,7 @@ title: NonAggregateExpressionNotInGroupByError # Class: NonAggregateExpressionNotInGroupByError -Defined in: [packages/db/src/errors.ts:600](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L600) +Defined in: [packages/db/src/errors.ts:625](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L625) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:600](https://github.com/TanStack/db/blob/ new NonAggregateExpressionNotInGroupByError(alias): NonAggregateExpressionNotInGroupByError; ``` -Defined in: [packages/db/src/errors.ts:601](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L601) +Defined in: [packages/db/src/errors.ts:626](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L626) #### Parameters diff --git a/docs/reference/classes/QueryCompilationError.md b/docs/reference/classes/QueryCompilationError.md index abd2054dea..cab9dcd58c 100644 --- a/docs/reference/classes/QueryCompilationError.md +++ b/docs/reference/classes/QueryCompilationError.md @@ -5,7 +5,7 @@ title: QueryCompilationError # Class: QueryCompilationError -Defined in: [packages/db/src/errors.ts:423](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L423) +Defined in: [packages/db/src/errors.ts:438](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L438) ## Extends @@ -13,6 +13,7 @@ Defined in: [packages/db/src/errors.ts:423](https://github.com/TanStack/db/blob/ ## Extended by +- [`UnsafeAliasPathError`](UnsafeAliasPathError.md) - [`DistinctRequiresSelectError`](DistinctRequiresSelectError.md) - [`FnSelectWithGroupByError`](FnSelectWithGroupByError.md) - [`UnsupportedRootScalarSelectError`](UnsupportedRootScalarSelectError.md) @@ -38,7 +39,7 @@ Defined in: [packages/db/src/errors.ts:423](https://github.com/TanStack/db/blob/ new QueryCompilationError(message): QueryCompilationError; ``` -Defined in: [packages/db/src/errors.ts:424](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L424) +Defined in: [packages/db/src/errors.ts:439](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L439) #### Parameters diff --git a/docs/reference/classes/QueryMustHaveFromClauseError.md b/docs/reference/classes/QueryMustHaveFromClauseError.md index de0baea892..c0ce14ddf8 100644 --- a/docs/reference/classes/QueryMustHaveFromClauseError.md +++ b/docs/reference/classes/QueryMustHaveFromClauseError.md @@ -5,7 +5,7 @@ title: QueryMustHaveFromClauseError # Class: QueryMustHaveFromClauseError -Defined in: [packages/db/src/errors.ts:403](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L403) +Defined in: [packages/db/src/errors.ts:418](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L418) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:403](https://github.com/TanStack/db/blob/ new QueryMustHaveFromClauseError(): QueryMustHaveFromClauseError; ``` -Defined in: [packages/db/src/errors.ts:404](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L404) +Defined in: [packages/db/src/errors.ts:419](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L419) #### Returns diff --git a/docs/reference/classes/QueryOptimizerError.md b/docs/reference/classes/QueryOptimizerError.md index f519315f23..4c47eda4dc 100644 --- a/docs/reference/classes/QueryOptimizerError.md +++ b/docs/reference/classes/QueryOptimizerError.md @@ -5,7 +5,7 @@ title: QueryOptimizerError # Class: QueryOptimizerError -Defined in: [packages/db/src/errors.ts:686](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L686) +Defined in: [packages/db/src/errors.ts:711](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L711) ## Extends @@ -24,7 +24,7 @@ Defined in: [packages/db/src/errors.ts:686](https://github.com/TanStack/db/blob/ new QueryOptimizerError(message): QueryOptimizerError; ``` -Defined in: [packages/db/src/errors.ts:687](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L687) +Defined in: [packages/db/src/errors.ts:712](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L712) #### Parameters diff --git a/docs/reference/classes/ReverseIndex.md b/docs/reference/classes/ReverseIndex.md index cf4860b028..162c421317 100644 --- a/docs/reference/classes/ReverseIndex.md +++ b/docs/reference/classes/ReverseIndex.md @@ -47,7 +47,7 @@ Defined in: [packages/db/src/indexes/reverse-index.ts:11](https://github.com/Tan get indexedKeysSet(): Set; ``` -Defined in: [packages/db/src/indexes/reverse-index.ts:124](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L124) +Defined in: [packages/db/src/indexes/reverse-index.ts:128](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L128) ##### Returns @@ -67,7 +67,7 @@ Defined in: [packages/db/src/indexes/reverse-index.ts:124](https://github.com/Ta get keyCount(): number; ``` -Defined in: [packages/db/src/indexes/reverse-index.ts:112](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L112) +Defined in: [packages/db/src/indexes/reverse-index.ts:116](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L116) ##### Returns @@ -119,6 +119,32 @@ Defined in: [packages/db/src/indexes/reverse-index.ts:66](https://github.com/Tan *** +### supportsRangeOptimization + +#### Get Signature + +```ts +get supportsRangeOptimization(): boolean; +``` + +Defined in: [packages/db/src/indexes/reverse-index.ts:76](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L76) + +Whether range lookups (gt/gte/lt/lte) on this index can be trusted to +return every matching key. Range traversal relies on the index ordering, so +it is unsafe when the index uses a custom comparator, whose order may not +match the WHERE evaluator's relational operators. Callers must fall back to +a full scan when this is `false`. + +##### Returns + +`boolean` + +#### Implementation of + +[`IndexInterface`](../interfaces/IndexInterface.md).[`supportsRangeOptimization`](../interfaces/IndexInterface.md#supportsrangeoptimization) + +*** + ### valueMapData #### Get Signature @@ -127,7 +153,7 @@ Defined in: [packages/db/src/indexes/reverse-index.ts:66](https://github.com/Tan get valueMapData(): Map>; ``` -Defined in: [packages/db/src/indexes/reverse-index.ts:128](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L128) +Defined in: [packages/db/src/indexes/reverse-index.ts:132](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L132) ##### Returns @@ -145,7 +171,7 @@ Defined in: [packages/db/src/indexes/reverse-index.ts:128](https://github.com/Ta add(key, item): void; ``` -Defined in: [packages/db/src/indexes/reverse-index.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L92) +Defined in: [packages/db/src/indexes/reverse-index.ts:96](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L96) #### Parameters @@ -173,7 +199,7 @@ Defined in: [packages/db/src/indexes/reverse-index.ts:92](https://github.com/Tan build(entries): void; ``` -Defined in: [packages/db/src/indexes/reverse-index.ts:104](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L104) +Defined in: [packages/db/src/indexes/reverse-index.ts:108](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L108) #### Parameters @@ -197,7 +223,7 @@ Defined in: [packages/db/src/indexes/reverse-index.ts:104](https://github.com/Ta clear(): void; ``` -Defined in: [packages/db/src/indexes/reverse-index.ts:108](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L108) +Defined in: [packages/db/src/indexes/reverse-index.ts:112](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L112) #### Returns @@ -215,7 +241,7 @@ Defined in: [packages/db/src/indexes/reverse-index.ts:108](https://github.com/Ta equalityLookup(value): Set; ``` -Defined in: [packages/db/src/indexes/reverse-index.ts:116](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L116) +Defined in: [packages/db/src/indexes/reverse-index.ts:120](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L120) #### Parameters @@ -239,7 +265,7 @@ Defined in: [packages/db/src/indexes/reverse-index.ts:116](https://github.com/Ta getStats(): IndexStats; ``` -Defined in: [packages/db/src/indexes/reverse-index.ts:88](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L88) +Defined in: [packages/db/src/indexes/reverse-index.ts:92](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L92) #### Returns @@ -257,7 +283,7 @@ Defined in: [packages/db/src/indexes/reverse-index.ts:88](https://github.com/Tan inArrayLookup(values): Set; ``` -Defined in: [packages/db/src/indexes/reverse-index.ts:120](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L120) +Defined in: [packages/db/src/indexes/reverse-index.ts:124](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L124) #### Parameters @@ -309,7 +335,7 @@ Defined in: [packages/db/src/indexes/reverse-index.ts:17](https://github.com/Tan matchesCompareOptions(compareOptions): boolean; ``` -Defined in: [packages/db/src/indexes/reverse-index.ts:80](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L80) +Defined in: [packages/db/src/indexes/reverse-index.ts:84](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L84) #### Parameters @@ -333,7 +359,7 @@ Defined in: [packages/db/src/indexes/reverse-index.ts:80](https://github.com/Tan matchesDirection(direction): boolean; ``` -Defined in: [packages/db/src/indexes/reverse-index.ts:84](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L84) +Defined in: [packages/db/src/indexes/reverse-index.ts:88](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L88) #### Parameters @@ -357,7 +383,7 @@ Defined in: [packages/db/src/indexes/reverse-index.ts:84](https://github.com/Tan matchesField(fieldPath): boolean; ``` -Defined in: [packages/db/src/indexes/reverse-index.ts:76](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L76) +Defined in: [packages/db/src/indexes/reverse-index.ts:80](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L80) #### Parameters @@ -429,7 +455,7 @@ Defined in: [packages/db/src/indexes/reverse-index.ts:35](https://github.com/Tan remove(key, item): void; ``` -Defined in: [packages/db/src/indexes/reverse-index.ts:96](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L96) +Defined in: [packages/db/src/indexes/reverse-index.ts:100](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L100) #### Parameters @@ -610,7 +636,7 @@ update( newItem): void; ``` -Defined in: [packages/db/src/indexes/reverse-index.ts:100](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L100) +Defined in: [packages/db/src/indexes/reverse-index.ts:104](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/reverse-index.ts#L104) #### Parameters diff --git a/docs/reference/classes/SerializationError.md b/docs/reference/classes/SerializationError.md index 57d07991cd..2c209926c5 100644 --- a/docs/reference/classes/SerializationError.md +++ b/docs/reference/classes/SerializationError.md @@ -5,7 +5,7 @@ title: SerializationError # Class: SerializationError -Defined in: [packages/db/src/errors.ts:636](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L636) +Defined in: [packages/db/src/errors.ts:661](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L661) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:636](https://github.com/TanStack/db/blob/ new SerializationError(operation, originalError): SerializationError; ``` -Defined in: [packages/db/src/errors.ts:637](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L637) +Defined in: [packages/db/src/errors.ts:662](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L662) #### Parameters diff --git a/docs/reference/classes/SetWindowRequiresOrderByError.md b/docs/reference/classes/SetWindowRequiresOrderByError.md index 54e14e7ca9..3f82d3e76c 100644 --- a/docs/reference/classes/SetWindowRequiresOrderByError.md +++ b/docs/reference/classes/SetWindowRequiresOrderByError.md @@ -5,7 +5,7 @@ title: SetWindowRequiresOrderByError # Class: SetWindowRequiresOrderByError -Defined in: [packages/db/src/errors.ts:754](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L754) +Defined in: [packages/db/src/errors.ts:779](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L779) Error thrown when setWindow is called on a collection without an ORDER BY clause. @@ -21,7 +21,7 @@ Error thrown when setWindow is called on a collection without an ORDER BY clause new SetWindowRequiresOrderByError(): SetWindowRequiresOrderByError; ``` -Defined in: [packages/db/src/errors.ts:755](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L755) +Defined in: [packages/db/src/errors.ts:780](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L780) #### Returns diff --git a/docs/reference/classes/StorageError.md b/docs/reference/classes/StorageError.md index 503edc0846..52e3869adf 100644 --- a/docs/reference/classes/StorageError.md +++ b/docs/reference/classes/StorageError.md @@ -5,7 +5,7 @@ title: StorageError # Class: StorageError -Defined in: [packages/db/src/errors.ts:629](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L629) +Defined in: [packages/db/src/errors.ts:654](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L654) ## Extends @@ -24,7 +24,7 @@ Defined in: [packages/db/src/errors.ts:629](https://github.com/TanStack/db/blob/ new StorageError(message): StorageError; ``` -Defined in: [packages/db/src/errors.ts:630](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L630) +Defined in: [packages/db/src/errors.ts:655](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L655) #### Parameters diff --git a/docs/reference/classes/StorageKeyRequiredError.md b/docs/reference/classes/StorageKeyRequiredError.md index 43c7b92c1b..5cf4e72cdd 100644 --- a/docs/reference/classes/StorageKeyRequiredError.md +++ b/docs/reference/classes/StorageKeyRequiredError.md @@ -5,7 +5,7 @@ title: StorageKeyRequiredError # Class: StorageKeyRequiredError -Defined in: [packages/db/src/errors.ts:652](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L652) +Defined in: [packages/db/src/errors.ts:677](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L677) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:652](https://github.com/TanStack/db/blob/ new StorageKeyRequiredError(): StorageKeyRequiredError; ``` -Defined in: [packages/db/src/errors.ts:653](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L653) +Defined in: [packages/db/src/errors.ts:678](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L678) #### Returns diff --git a/docs/reference/classes/SubscriptionNotFoundError.md b/docs/reference/classes/SubscriptionNotFoundError.md index 29f745156e..de9841e958 100644 --- a/docs/reference/classes/SubscriptionNotFoundError.md +++ b/docs/reference/classes/SubscriptionNotFoundError.md @@ -5,7 +5,7 @@ title: SubscriptionNotFoundError # Class: SubscriptionNotFoundError -Defined in: [packages/db/src/errors.ts:714](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L714) +Defined in: [packages/db/src/errors.ts:739](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L739) Error when a subscription cannot be found during lazy join processing. For subqueries, aliases may be remapped (e.g., 'activeUser' → 'user'). @@ -26,7 +26,7 @@ new SubscriptionNotFoundError( availableAliases): SubscriptionNotFoundError; ``` -Defined in: [packages/db/src/errors.ts:715](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L715) +Defined in: [packages/db/src/errors.ts:740](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L740) #### Parameters diff --git a/docs/reference/classes/SyncCleanupError.md b/docs/reference/classes/SyncCleanupError.md index f36d0e1d45..6b9031a33f 100644 --- a/docs/reference/classes/SyncCleanupError.md +++ b/docs/reference/classes/SyncCleanupError.md @@ -5,7 +5,7 @@ title: SyncCleanupError # Class: SyncCleanupError -Defined in: [packages/db/src/errors.ts:675](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L675) +Defined in: [packages/db/src/errors.ts:700](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L700) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:675](https://github.com/TanStack/db/blob/ new SyncCleanupError(collectionId, error): SyncCleanupError; ``` -Defined in: [packages/db/src/errors.ts:676](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L676) +Defined in: [packages/db/src/errors.ts:701](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L701) #### Parameters diff --git a/docs/reference/classes/UnknownExpressionTypeError.md b/docs/reference/classes/UnknownExpressionTypeError.md index 0aadc0abbc..d223de2501 100644 --- a/docs/reference/classes/UnknownExpressionTypeError.md +++ b/docs/reference/classes/UnknownExpressionTypeError.md @@ -5,7 +5,7 @@ title: UnknownExpressionTypeError # Class: UnknownExpressionTypeError -Defined in: [packages/db/src/errors.ts:512](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L512) +Defined in: [packages/db/src/errors.ts:537](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L537) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:512](https://github.com/TanStack/db/blob/ new UnknownExpressionTypeError(type): UnknownExpressionTypeError; ``` -Defined in: [packages/db/src/errors.ts:513](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L513) +Defined in: [packages/db/src/errors.ts:538](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L538) #### Parameters diff --git a/docs/reference/classes/UnknownFunctionError.md b/docs/reference/classes/UnknownFunctionError.md index fda586a8fb..3cb73f5f4b 100644 --- a/docs/reference/classes/UnknownFunctionError.md +++ b/docs/reference/classes/UnknownFunctionError.md @@ -5,7 +5,7 @@ title: UnknownFunctionError # Class: UnknownFunctionError -Defined in: [packages/db/src/errors.ts:524](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L524) +Defined in: [packages/db/src/errors.ts:549](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L549) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:524](https://github.com/TanStack/db/blob/ new UnknownFunctionError(functionName): UnknownFunctionError; ``` -Defined in: [packages/db/src/errors.ts:525](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L525) +Defined in: [packages/db/src/errors.ts:550](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L550) #### Parameters diff --git a/docs/reference/classes/UnknownHavingExpressionTypeError.md b/docs/reference/classes/UnknownHavingExpressionTypeError.md index 6f4806558a..d2c03792e0 100644 --- a/docs/reference/classes/UnknownHavingExpressionTypeError.md +++ b/docs/reference/classes/UnknownHavingExpressionTypeError.md @@ -5,7 +5,7 @@ title: UnknownHavingExpressionTypeError # Class: UnknownHavingExpressionTypeError -Defined in: [packages/db/src/errors.ts:622](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L622) +Defined in: [packages/db/src/errors.ts:647](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L647) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:622](https://github.com/TanStack/db/blob/ new UnknownHavingExpressionTypeError(type): UnknownHavingExpressionTypeError; ``` -Defined in: [packages/db/src/errors.ts:623](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L623) +Defined in: [packages/db/src/errors.ts:648](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L648) #### Parameters diff --git a/docs/reference/classes/UnsafeAliasPathError.md b/docs/reference/classes/UnsafeAliasPathError.md new file mode 100644 index 0000000000..3140c9f7e1 --- /dev/null +++ b/docs/reference/classes/UnsafeAliasPathError.md @@ -0,0 +1,220 @@ +--- +id: UnsafeAliasPathError +title: UnsafeAliasPathError +--- + +# Class: UnsafeAliasPathError + +Defined in: [packages/db/src/errors.ts:445](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L445) + +## Extends + +- [`QueryCompilationError`](QueryCompilationError.md) + +## Constructors + +### Constructor + +```ts +new UnsafeAliasPathError(segment): UnsafeAliasPathError; +``` + +Defined in: [packages/db/src/errors.ts:446](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L446) + +#### Parameters + +##### segment + +`string` + +#### Returns + +`UnsafeAliasPathError` + +#### Overrides + +[`QueryCompilationError`](QueryCompilationError.md).[`constructor`](QueryCompilationError.md#constructor) + +## Properties + +### cause? + +```ts +optional cause: unknown; +``` + +Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es2022.error.d.ts:26 + +#### Inherited from + +[`QueryCompilationError`](QueryCompilationError.md).[`cause`](QueryCompilationError.md#cause) + +*** + +### message + +```ts +message: string; +``` + +Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1077 + +#### Inherited from + +[`QueryCompilationError`](QueryCompilationError.md).[`message`](QueryCompilationError.md#message) + +*** + +### name + +```ts +name: string; +``` + +Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1076 + +#### Inherited from + +[`QueryCompilationError`](QueryCompilationError.md).[`name`](QueryCompilationError.md#name) + +*** + +### stack? + +```ts +optional stack: string; +``` + +Defined in: node\_modules/.pnpm/typescript@5.9.3/node\_modules/typescript/lib/lib.es5.d.ts:1078 + +#### Inherited from + +[`QueryCompilationError`](QueryCompilationError.md).[`stack`](QueryCompilationError.md#stack) + +*** + +### stackTraceLimit + +```ts +static stackTraceLimit: number; +``` + +Defined in: node\_modules/.pnpm/@types+node@25.2.2/node\_modules/@types/node/globals.d.ts:67 + +The `Error.stackTraceLimit` property specifies the number of stack frames +collected by a stack trace (whether generated by `new Error().stack` or +`Error.captureStackTrace(obj)`). + +The default value is `10` but may be set to any valid JavaScript number. Changes +will affect any stack trace captured _after_ the value has been changed. + +If set to a non-number value, or set to a negative number, stack traces will +not capture any frames. + +#### Inherited from + +[`QueryCompilationError`](QueryCompilationError.md).[`stackTraceLimit`](QueryCompilationError.md#stacktracelimit) + +## Methods + +### captureStackTrace() + +```ts +static captureStackTrace(targetObject, constructorOpt?): void; +``` + +Defined in: node\_modules/.pnpm/@types+node@25.2.2/node\_modules/@types/node/globals.d.ts:51 + +Creates a `.stack` property on `targetObject`, which when accessed returns +a string representing the location in the code at which +`Error.captureStackTrace()` was called. + +```js +const myObject = {}; +Error.captureStackTrace(myObject); +myObject.stack; // Similar to `new Error().stack` +``` + +The first line of the trace will be prefixed with +`${myObject.name}: ${myObject.message}`. + +The optional `constructorOpt` argument accepts a function. If given, all frames +above `constructorOpt`, including `constructorOpt`, will be omitted from the +generated stack trace. + +The `constructorOpt` argument is useful for hiding implementation +details of error generation from the user. For instance: + +```js +function a() { + b(); +} + +function b() { + c(); +} + +function c() { + // Create an error without stack trace to avoid calculating the stack trace twice. + const { stackTraceLimit } = Error; + Error.stackTraceLimit = 0; + const error = new Error(); + Error.stackTraceLimit = stackTraceLimit; + + // Capture the stack trace above function b + Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace + throw error; +} + +a(); +``` + +#### Parameters + +##### targetObject + +`object` + +##### constructorOpt? + +`Function` + +#### Returns + +`void` + +#### Inherited from + +[`QueryCompilationError`](QueryCompilationError.md).[`captureStackTrace`](QueryCompilationError.md#capturestacktrace) + +*** + +### prepareStackTrace() + +```ts +static prepareStackTrace(err, stackTraces): any; +``` + +Defined in: node\_modules/.pnpm/@types+node@25.2.2/node\_modules/@types/node/globals.d.ts:55 + +#### Parameters + +##### err + +`Error` + +##### stackTraces + +`CallSite`[] + +#### Returns + +`any` + +#### See + +https://v8.dev/docs/stack-trace-api#customizing-stack-traces + +#### Inherited from + +[`QueryCompilationError`](QueryCompilationError.md).[`prepareStackTrace`](QueryCompilationError.md#preparestacktrace) diff --git a/docs/reference/classes/UnsupportedAggregateFunctionError.md b/docs/reference/classes/UnsupportedAggregateFunctionError.md index b2eaefe29b..6f34017e09 100644 --- a/docs/reference/classes/UnsupportedAggregateFunctionError.md +++ b/docs/reference/classes/UnsupportedAggregateFunctionError.md @@ -5,7 +5,7 @@ title: UnsupportedAggregateFunctionError # Class: UnsupportedAggregateFunctionError -Defined in: [packages/db/src/errors.ts:608](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L608) +Defined in: [packages/db/src/errors.ts:633](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L633) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:608](https://github.com/TanStack/db/blob/ new UnsupportedAggregateFunctionError(functionName): UnsupportedAggregateFunctionError; ``` -Defined in: [packages/db/src/errors.ts:609](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L609) +Defined in: [packages/db/src/errors.ts:634](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L634) #### Parameters diff --git a/docs/reference/classes/UnsupportedFromTypeError.md b/docs/reference/classes/UnsupportedFromTypeError.md index a2a98b89da..b0c6499504 100644 --- a/docs/reference/classes/UnsupportedFromTypeError.md +++ b/docs/reference/classes/UnsupportedFromTypeError.md @@ -5,7 +5,7 @@ title: UnsupportedFromTypeError # Class: UnsupportedFromTypeError -Defined in: [packages/db/src/errors.ts:506](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L506) +Defined in: [packages/db/src/errors.ts:531](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L531) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:506](https://github.com/TanStack/db/blob/ new UnsupportedFromTypeError(type): UnsupportedFromTypeError; ``` -Defined in: [packages/db/src/errors.ts:507](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L507) +Defined in: [packages/db/src/errors.ts:532](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L532) #### Parameters diff --git a/docs/reference/classes/UnsupportedJoinSourceTypeError.md b/docs/reference/classes/UnsupportedJoinSourceTypeError.md index f355eee52c..085343980e 100644 --- a/docs/reference/classes/UnsupportedJoinSourceTypeError.md +++ b/docs/reference/classes/UnsupportedJoinSourceTypeError.md @@ -5,7 +5,7 @@ title: UnsupportedJoinSourceTypeError # Class: UnsupportedJoinSourceTypeError -Defined in: [packages/db/src/errors.ts:586](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L586) +Defined in: [packages/db/src/errors.ts:611](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L611) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:586](https://github.com/TanStack/db/blob/ new UnsupportedJoinSourceTypeError(type): UnsupportedJoinSourceTypeError; ``` -Defined in: [packages/db/src/errors.ts:587](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L587) +Defined in: [packages/db/src/errors.ts:612](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L612) #### Parameters diff --git a/docs/reference/classes/UnsupportedJoinTypeError.md b/docs/reference/classes/UnsupportedJoinTypeError.md index 23ba03b1b1..c15081ab54 100644 --- a/docs/reference/classes/UnsupportedJoinTypeError.md +++ b/docs/reference/classes/UnsupportedJoinTypeError.md @@ -5,7 +5,7 @@ title: UnsupportedJoinTypeError # Class: UnsupportedJoinTypeError -Defined in: [packages/db/src/errors.ts:544](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L544) +Defined in: [packages/db/src/errors.ts:569](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L569) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:544](https://github.com/TanStack/db/blob/ new UnsupportedJoinTypeError(joinType): UnsupportedJoinTypeError; ``` -Defined in: [packages/db/src/errors.ts:545](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L545) +Defined in: [packages/db/src/errors.ts:570](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L570) #### Parameters diff --git a/docs/reference/classes/UnsupportedRootScalarSelectError.md b/docs/reference/classes/UnsupportedRootScalarSelectError.md index a3996fdf17..9b7a87b665 100644 --- a/docs/reference/classes/UnsupportedRootScalarSelectError.md +++ b/docs/reference/classes/UnsupportedRootScalarSelectError.md @@ -5,7 +5,7 @@ title: UnsupportedRootScalarSelectError # Class: UnsupportedRootScalarSelectError -Defined in: [packages/db/src/errors.ts:447](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L447) +Defined in: [packages/db/src/errors.ts:472](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L472) ## Extends @@ -19,7 +19,7 @@ Defined in: [packages/db/src/errors.ts:447](https://github.com/TanStack/db/blob/ new UnsupportedRootScalarSelectError(): UnsupportedRootScalarSelectError; ``` -Defined in: [packages/db/src/errors.ts:448](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L448) +Defined in: [packages/db/src/errors.ts:473](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L473) #### Returns diff --git a/docs/reference/classes/WhereClauseConversionError.md b/docs/reference/classes/WhereClauseConversionError.md index 21d94f4594..f6d98ac001 100644 --- a/docs/reference/classes/WhereClauseConversionError.md +++ b/docs/reference/classes/WhereClauseConversionError.md @@ -5,7 +5,7 @@ title: WhereClauseConversionError # Class: WhereClauseConversionError -Defined in: [packages/db/src/errors.ts:702](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L702) +Defined in: [packages/db/src/errors.ts:727](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L727) Internal error when the query optimizer fails to convert a WHERE clause to a collection filter. @@ -21,7 +21,7 @@ Internal error when the query optimizer fails to convert a WHERE clause to a col new WhereClauseConversionError(collectionId, alias): WhereClauseConversionError; ``` -Defined in: [packages/db/src/errors.ts:703](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L703) +Defined in: [packages/db/src/errors.ts:728](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L728) #### Parameters diff --git a/docs/reference/electric-db-collection/functions/electricCollectionOptions.md b/docs/reference/electric-db-collection/functions/electricCollectionOptions.md index 8e80aeed8e..a89acd634d 100644 --- a/docs/reference/electric-db-collection/functions/electricCollectionOptions.md +++ b/docs/reference/electric-db-collection/functions/electricCollectionOptions.md @@ -11,7 +11,7 @@ title: electricCollectionOptions function electricCollectionOptions(config): Omit, string | number, T, UtilsRecord>, "utils" | "onInsert" | "onUpdate" | "onDelete"> & Pick, T>, "onInsert" | "onUpdate" | "onDelete"> & object; ``` -Defined in: [packages/electric-db-collection/src/electric.ts:576](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L576) +Defined in: [packages/electric-db-collection/src/electric.ts:592](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L592) Creates Electric collection options for use with a standard Collection @@ -43,7 +43,7 @@ Collection options with utilities function electricCollectionOptions(config): Omit, "utils" | "onInsert" | "onUpdate" | "onDelete"> & Pick, "onInsert" | "onUpdate" | "onDelete"> & object; ``` -Defined in: [packages/electric-db-collection/src/electric.ts:594](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L594) +Defined in: [packages/electric-db-collection/src/electric.ts:610](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L610) Creates Electric collection options for use with a standard Collection diff --git a/docs/reference/electric-db-collection/functions/isChangeMessage.md b/docs/reference/electric-db-collection/functions/isChangeMessage.md index 0e9732cd2a..81de9bff39 100644 --- a/docs/reference/electric-db-collection/functions/isChangeMessage.md +++ b/docs/reference/electric-db-collection/functions/isChangeMessage.md @@ -9,7 +9,7 @@ title: isChangeMessage function isChangeMessage(message): message is ChangeMessage; ``` -Defined in: node\_modules/.pnpm/@electric-sql+client@1.5.14/node\_modules/@electric-sql/client/dist/index.d.ts:889 +Defined in: node\_modules/.pnpm/@electric-sql+client@1.5.15/node\_modules/@electric-sql/client/dist/index.d.ts:922 Type guard for checking Message is ChangeMessage. diff --git a/docs/reference/electric-db-collection/functions/isControlMessage.md b/docs/reference/electric-db-collection/functions/isControlMessage.md index 66ea4b70f3..6cd6b1582a 100644 --- a/docs/reference/electric-db-collection/functions/isControlMessage.md +++ b/docs/reference/electric-db-collection/functions/isControlMessage.md @@ -9,7 +9,7 @@ title: isControlMessage function isControlMessage(message): message is ControlMessage; ``` -Defined in: node\_modules/.pnpm/@electric-sql+client@1.5.14/node\_modules/@electric-sql/client/dist/index.d.ts:907 +Defined in: node\_modules/.pnpm/@electric-sql+client@1.5.15/node\_modules/@electric-sql/client/dist/index.d.ts:940 Type guard for checking Message is ControlMessage. diff --git a/docs/reference/electric-db-collection/interfaces/ElectricCollectionConfig.md b/docs/reference/electric-db-collection/interfaces/ElectricCollectionConfig.md index 95c0ec9f3c..32b7c1935d 100644 --- a/docs/reference/electric-db-collection/interfaces/ElectricCollectionConfig.md +++ b/docs/reference/electric-db-collection/interfaces/ElectricCollectionConfig.md @@ -5,7 +5,7 @@ title: ElectricCollectionConfig # Interface: ElectricCollectionConfig\ -Defined in: [packages/electric-db-collection/src/electric.ts:148](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L148) +Defined in: [packages/electric-db-collection/src/electric.ts:150](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L150) Configuration interface for Electric collection options @@ -35,7 +35,7 @@ The schema type for validation optional [ELECTRIC_TEST_HOOKS]: ElectricTestHooks; ``` -Defined in: [packages/electric-db-collection/src/electric.ts:171](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L171) +Defined in: [packages/electric-db-collection/src/electric.ts:173](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L173) Internal test hooks (for testing only) Hidden via Symbol to prevent accidental usage in production @@ -48,7 +48,7 @@ Hidden via Symbol to prevent accidental usage in production optional onDelete: (params) => Promise; ``` -Defined in: [packages/electric-db-collection/src/electric.ts:288](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L288) +Defined in: [packages/electric-db-collection/src/electric.ts:290](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L290) Optional asynchronous handler function called before a delete operation @@ -100,7 +100,7 @@ onDelete: async ({ transaction, collection }) => { optional onInsert: (params) => Promise; ``` -Defined in: [packages/electric-db-collection/src/electric.ts:219](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L219) +Defined in: [packages/electric-db-collection/src/electric.ts:221](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L221) Optional asynchronous handler function called before an insert operation @@ -174,7 +174,7 @@ onInsert: async ({ transaction, collection }) => { optional onUpdate: (params) => Promise; ``` -Defined in: [packages/electric-db-collection/src/electric.ts:254](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L254) +Defined in: [packages/electric-db-collection/src/electric.ts:256](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L256) Optional asynchronous handler function called before an update operation @@ -227,7 +227,7 @@ onUpdate: async ({ transaction, collection }) => { shapeOptions: ShapeStreamOptions>; ``` -Defined in: [packages/electric-db-collection/src/electric.ts:164](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L164) +Defined in: [packages/electric-db-collection/src/electric.ts:166](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L166) Configuration options for the ElectricSQL ShapeStream @@ -239,4 +239,4 @@ Configuration options for the ElectricSQL ShapeStream optional syncMode: ElectricSyncMode; ``` -Defined in: [packages/electric-db-collection/src/electric.ts:165](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L165) +Defined in: [packages/electric-db-collection/src/electric.ts:167](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L167) diff --git a/docs/reference/electric-db-collection/interfaces/ElectricCollectionUtils.md b/docs/reference/electric-db-collection/interfaces/ElectricCollectionUtils.md index b7ce5f0c9c..7ef911d08f 100644 --- a/docs/reference/electric-db-collection/interfaces/ElectricCollectionUtils.md +++ b/docs/reference/electric-db-collection/interfaces/ElectricCollectionUtils.md @@ -5,7 +5,7 @@ title: ElectricCollectionUtils # Interface: ElectricCollectionUtils\ -Defined in: [packages/electric-db-collection/src/electric.ts:558](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L558) +Defined in: [packages/electric-db-collection/src/electric.ts:574](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L574) Electric collection utilities type @@ -33,7 +33,7 @@ Electric collection utilities type awaitMatch: AwaitMatchFn; ``` -Defined in: [packages/electric-db-collection/src/electric.ts:562](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L562) +Defined in: [packages/electric-db-collection/src/electric.ts:578](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L578) *** @@ -43,4 +43,4 @@ Defined in: [packages/electric-db-collection/src/electric.ts:562](https://github awaitTxId: AwaitTxIdFn; ``` -Defined in: [packages/electric-db-collection/src/electric.ts:561](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L561) +Defined in: [packages/electric-db-collection/src/electric.ts:577](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L577) diff --git a/docs/reference/electric-db-collection/type-aliases/AwaitTxIdFn.md b/docs/reference/electric-db-collection/type-aliases/AwaitTxIdFn.md index b03f81a358..f4d5ca8d58 100644 --- a/docs/reference/electric-db-collection/type-aliases/AwaitTxIdFn.md +++ b/docs/reference/electric-db-collection/type-aliases/AwaitTxIdFn.md @@ -9,7 +9,7 @@ title: AwaitTxIdFn type AwaitTxIdFn = (txId, timeout?) => Promise; ``` -Defined in: [packages/electric-db-collection/src/electric.ts:545](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L545) +Defined in: [packages/electric-db-collection/src/electric.ts:561](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L561) Type for the awaitTxId utility function diff --git a/docs/reference/electric-db-collection/type-aliases/Txid.md b/docs/reference/electric-db-collection/type-aliases/Txid.md index f16e8e37ad..02dc695ec1 100644 --- a/docs/reference/electric-db-collection/type-aliases/Txid.md +++ b/docs/reference/electric-db-collection/type-aliases/Txid.md @@ -9,6 +9,6 @@ title: Txid type Txid = number; ``` -Defined in: [packages/electric-db-collection/src/electric.ts:86](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L86) +Defined in: [packages/electric-db-collection/src/electric.ts:88](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/src/electric.ts#L88) Type representing a transaction ID in ElectricSQL diff --git a/docs/reference/functions/add.md b/docs/reference/functions/add.md index 1c3ceb9246..c7dddb77cf 100644 --- a/docs/reference/functions/add.md +++ b/docs/reference/functions/add.md @@ -6,10 +6,10 @@ title: add # Function: add() ```ts -function add(left, right): BinaryNumericReturnType; +function add(left, right): BinaryNumericReturnType; ``` -Defined in: [packages/db/src/query/builder/functions.ts:354](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L354) +Defined in: [packages/db/src/query/builder/functions.ts:601](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L601) ## Type Parameters @@ -33,4 +33,4 @@ Defined in: [packages/db/src/query/builder/functions.ts:354](https://github.com/ ## Returns -`BinaryNumericReturnType`\<`T1`, `T2`\> +`BinaryNumericReturnType` diff --git a/docs/reference/functions/and.md b/docs/reference/functions/and.md index b99e1c2326..b463b2ec2f 100644 --- a/docs/reference/functions/and.md +++ b/docs/reference/functions/and.md @@ -11,7 +11,7 @@ title: and function and(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:199](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L199) +Defined in: [packages/db/src/query/builder/functions.ts:203](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L203) ### Parameters @@ -36,7 +36,7 @@ function and( rest): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:203](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L203) +Defined in: [packages/db/src/query/builder/functions.ts:207](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L207) ### Parameters diff --git a/docs/reference/functions/avg.md b/docs/reference/functions/avg.md index acf474d673..2613e64655 100644 --- a/docs/reference/functions/avg.md +++ b/docs/reference/functions/avg.md @@ -9,7 +9,7 @@ title: avg function avg(arg): AggregateReturnType; ``` -Defined in: [packages/db/src/query/builder/functions.ts:370](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L370) +Defined in: [packages/db/src/query/builder/functions.ts:647](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L647) ## Type Parameters diff --git a/docs/reference/functions/caseWhen.md b/docs/reference/functions/caseWhen.md new file mode 100644 index 0000000000..17559636a0 --- /dev/null +++ b/docs/reference/functions/caseWhen.md @@ -0,0 +1,1299 @@ +--- +id: caseWhen +title: caseWhen +--- + +# Function: caseWhen() + +## Call Signature + +```ts +function caseWhen(condition1, value1): CaseWhenResult<[V1], false>; +``` + +Defined in: [packages/db/src/query/builder/functions.ts:399](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L399) + +Returns the value for the first matching condition, similar to SQL +`CASE WHEN`. + +Arguments are evaluated as condition/value pairs followed by an optional +default value. Scalar branch values return a query expression and can be used +in expression contexts like `select`, `where`, `orderBy`, `groupBy`, +`having`, and equality join operands. If no scalar branch matches and no +default is provided, the result is `null`. + +When a branch value is a projection object, `caseWhen` becomes a select-only +projection value. Projection branches can include nested fields, ref spreads, +and includes. If no projection branch matches and no default is provided, the +result is `undefined`. + +### Type Parameters + +#### C1 + +`C1` *extends* `ExpressionLike` + +#### V1 + +`V1` *extends* `CaseWhenValue` + +### Parameters + +#### condition1 + +`C1` + +#### value1 + +`V1` + +### Returns + +`CaseWhenResult`\<\[`V1`\], `false`\> + +### Examples + +```ts +caseWhen(gt(user.age, 18), `adult`, `minor`) +``` + +```ts +caseWhen( + gt(user.age, 65), + `senior`, + gt(user.age, 18), + `adult`, + `minor`, +) +``` + +```ts +caseWhen(gt(user.age, 18), { + ...user, + posts: q + .from({ post: postsCollection }) + .where(({ post }) => eq(post.userId, user.id)), +}) +``` + +## Call Signature + +```ts +function caseWhen( + condition1, + value1, +defaultValue): CaseWhenResult<[V1, D], true>; +``` + +Defined in: [packages/db/src/query/builder/functions.ts:403](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L403) + +Returns the value for the first matching condition, similar to SQL +`CASE WHEN`. + +Arguments are evaluated as condition/value pairs followed by an optional +default value. Scalar branch values return a query expression and can be used +in expression contexts like `select`, `where`, `orderBy`, `groupBy`, +`having`, and equality join operands. If no scalar branch matches and no +default is provided, the result is `null`. + +When a branch value is a projection object, `caseWhen` becomes a select-only +projection value. Projection branches can include nested fields, ref spreads, +and includes. If no projection branch matches and no default is provided, the +result is `undefined`. + +### Type Parameters + +#### C1 + +`C1` *extends* `ExpressionLike` + +#### V1 + +`V1` *extends* `CaseWhenValue` + +#### D + +`D` *extends* `CaseWhenValue` + +### Parameters + +#### condition1 + +`C1` + +#### value1 + +`V1` + +#### defaultValue + +`D` + +### Returns + +`CaseWhenResult`\<\[`V1`, `D`\], `true`\> + +### Examples + +```ts +caseWhen(gt(user.age, 18), `adult`, `minor`) +``` + +```ts +caseWhen( + gt(user.age, 65), + `senior`, + gt(user.age, 18), + `adult`, + `minor`, +) +``` + +```ts +caseWhen(gt(user.age, 18), { + ...user, + posts: q + .from({ post: postsCollection }) + .where(({ post }) => eq(post.userId, user.id)), +}) +``` + +## Call Signature + +```ts +function caseWhen( + condition1, + value1, + condition2, +value2): CaseWhenResult<[V1, V2], false>; +``` + +Defined in: [packages/db/src/query/builder/functions.ts:408](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L408) + +Returns the value for the first matching condition, similar to SQL +`CASE WHEN`. + +Arguments are evaluated as condition/value pairs followed by an optional +default value. Scalar branch values return a query expression and can be used +in expression contexts like `select`, `where`, `orderBy`, `groupBy`, +`having`, and equality join operands. If no scalar branch matches and no +default is provided, the result is `null`. + +When a branch value is a projection object, `caseWhen` becomes a select-only +projection value. Projection branches can include nested fields, ref spreads, +and includes. If no projection branch matches and no default is provided, the +result is `undefined`. + +### Type Parameters + +#### C1 + +`C1` *extends* `ExpressionLike` + +#### V1 + +`V1` *extends* `CaseWhenValue` + +#### C2 + +`C2` *extends* `ExpressionLike` + +#### V2 + +`V2` *extends* `CaseWhenValue` + +### Parameters + +#### condition1 + +`C1` + +#### value1 + +`V1` + +#### condition2 + +`C2` + +#### value2 + +`V2` + +### Returns + +`CaseWhenResult`\<\[`V1`, `V2`\], `false`\> + +### Examples + +```ts +caseWhen(gt(user.age, 18), `adult`, `minor`) +``` + +```ts +caseWhen( + gt(user.age, 65), + `senior`, + gt(user.age, 18), + `adult`, + `minor`, +) +``` + +```ts +caseWhen(gt(user.age, 18), { + ...user, + posts: q + .from({ post: postsCollection }) + .where(({ post }) => eq(post.userId, user.id)), +}) +``` + +## Call Signature + +```ts +function caseWhen( + condition1, + value1, + condition2, + value2, +defaultValue): CaseWhenResult<[V1, V2, D], true>; +``` + +Defined in: [packages/db/src/query/builder/functions.ts:419](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L419) + +Returns the value for the first matching condition, similar to SQL +`CASE WHEN`. + +Arguments are evaluated as condition/value pairs followed by an optional +default value. Scalar branch values return a query expression and can be used +in expression contexts like `select`, `where`, `orderBy`, `groupBy`, +`having`, and equality join operands. If no scalar branch matches and no +default is provided, the result is `null`. + +When a branch value is a projection object, `caseWhen` becomes a select-only +projection value. Projection branches can include nested fields, ref spreads, +and includes. If no projection branch matches and no default is provided, the +result is `undefined`. + +### Type Parameters + +#### C1 + +`C1` *extends* `ExpressionLike` + +#### V1 + +`V1` *extends* `CaseWhenValue` + +#### C2 + +`C2` *extends* `ExpressionLike` + +#### V2 + +`V2` *extends* `CaseWhenValue` + +#### D + +`D` *extends* `CaseWhenValue` + +### Parameters + +#### condition1 + +`C1` + +#### value1 + +`V1` + +#### condition2 + +`C2` + +#### value2 + +`V2` + +#### defaultValue + +`D` + +### Returns + +`CaseWhenResult`\<\[`V1`, `V2`, `D`\], `true`\> + +### Examples + +```ts +caseWhen(gt(user.age, 18), `adult`, `minor`) +``` + +```ts +caseWhen( + gt(user.age, 65), + `senior`, + gt(user.age, 18), + `adult`, + `minor`, +) +``` + +```ts +caseWhen(gt(user.age, 18), { + ...user, + posts: q + .from({ post: postsCollection }) + .where(({ post }) => eq(post.userId, user.id)), +}) +``` + +## Call Signature + +```ts +function caseWhen( + condition1, + value1, + condition2, + value2, + condition3, +value3): CaseWhenResult<[V1, V2, V3], false>; +``` + +Defined in: [packages/db/src/query/builder/functions.ts:432](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L432) + +Returns the value for the first matching condition, similar to SQL +`CASE WHEN`. + +Arguments are evaluated as condition/value pairs followed by an optional +default value. Scalar branch values return a query expression and can be used +in expression contexts like `select`, `where`, `orderBy`, `groupBy`, +`having`, and equality join operands. If no scalar branch matches and no +default is provided, the result is `null`. + +When a branch value is a projection object, `caseWhen` becomes a select-only +projection value. Projection branches can include nested fields, ref spreads, +and includes. If no projection branch matches and no default is provided, the +result is `undefined`. + +### Type Parameters + +#### C1 + +`C1` *extends* `ExpressionLike` + +#### V1 + +`V1` *extends* `CaseWhenValue` + +#### C2 + +`C2` *extends* `ExpressionLike` + +#### V2 + +`V2` *extends* `CaseWhenValue` + +#### C3 + +`C3` *extends* `ExpressionLike` + +#### V3 + +`V3` *extends* `CaseWhenValue` + +### Parameters + +#### condition1 + +`C1` + +#### value1 + +`V1` + +#### condition2 + +`C2` + +#### value2 + +`V2` + +#### condition3 + +`C3` + +#### value3 + +`V3` + +### Returns + +`CaseWhenResult`\<\[`V1`, `V2`, `V3`\], `false`\> + +### Examples + +```ts +caseWhen(gt(user.age, 18), `adult`, `minor`) +``` + +```ts +caseWhen( + gt(user.age, 65), + `senior`, + gt(user.age, 18), + `adult`, + `minor`, +) +``` + +```ts +caseWhen(gt(user.age, 18), { + ...user, + posts: q + .from({ post: postsCollection }) + .where(({ post }) => eq(post.userId, user.id)), +}) +``` + +## Call Signature + +```ts +function caseWhen( + condition1, + value1, + condition2, + value2, + condition3, + value3, +defaultValue): CaseWhenResult<[V1, V2, V3, D], true>; +``` + +Defined in: [packages/db/src/query/builder/functions.ts:447](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L447) + +Returns the value for the first matching condition, similar to SQL +`CASE WHEN`. + +Arguments are evaluated as condition/value pairs followed by an optional +default value. Scalar branch values return a query expression and can be used +in expression contexts like `select`, `where`, `orderBy`, `groupBy`, +`having`, and equality join operands. If no scalar branch matches and no +default is provided, the result is `null`. + +When a branch value is a projection object, `caseWhen` becomes a select-only +projection value. Projection branches can include nested fields, ref spreads, +and includes. If no projection branch matches and no default is provided, the +result is `undefined`. + +### Type Parameters + +#### C1 + +`C1` *extends* `ExpressionLike` + +#### V1 + +`V1` *extends* `CaseWhenValue` + +#### C2 + +`C2` *extends* `ExpressionLike` + +#### V2 + +`V2` *extends* `CaseWhenValue` + +#### C3 + +`C3` *extends* `ExpressionLike` + +#### V3 + +`V3` *extends* `CaseWhenValue` + +#### D + +`D` *extends* `CaseWhenValue` + +### Parameters + +#### condition1 + +`C1` + +#### value1 + +`V1` + +#### condition2 + +`C2` + +#### value2 + +`V2` + +#### condition3 + +`C3` + +#### value3 + +`V3` + +#### defaultValue + +`D` + +### Returns + +`CaseWhenResult`\<\[`V1`, `V2`, `V3`, `D`\], `true`\> + +### Examples + +```ts +caseWhen(gt(user.age, 18), `adult`, `minor`) +``` + +```ts +caseWhen( + gt(user.age, 65), + `senior`, + gt(user.age, 18), + `adult`, + `minor`, +) +``` + +```ts +caseWhen(gt(user.age, 18), { + ...user, + posts: q + .from({ post: postsCollection }) + .where(({ post }) => eq(post.userId, user.id)), +}) +``` + +## Call Signature + +```ts +function caseWhen( + condition1, + value1, + condition2, + value2, + condition3, + value3, + condition4, +value4): CaseWhenResult<[V1, V2, V3, V4], false>; +``` + +Defined in: [packages/db/src/query/builder/functions.ts:464](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L464) + +Returns the value for the first matching condition, similar to SQL +`CASE WHEN`. + +Arguments are evaluated as condition/value pairs followed by an optional +default value. Scalar branch values return a query expression and can be used +in expression contexts like `select`, `where`, `orderBy`, `groupBy`, +`having`, and equality join operands. If no scalar branch matches and no +default is provided, the result is `null`. + +When a branch value is a projection object, `caseWhen` becomes a select-only +projection value. Projection branches can include nested fields, ref spreads, +and includes. If no projection branch matches and no default is provided, the +result is `undefined`. + +### Type Parameters + +#### C1 + +`C1` *extends* `ExpressionLike` + +#### V1 + +`V1` *extends* `CaseWhenValue` + +#### C2 + +`C2` *extends* `ExpressionLike` + +#### V2 + +`V2` *extends* `CaseWhenValue` + +#### C3 + +`C3` *extends* `ExpressionLike` + +#### V3 + +`V3` *extends* `CaseWhenValue` + +#### C4 + +`C4` *extends* `ExpressionLike` + +#### V4 + +`V4` *extends* `CaseWhenValue` + +### Parameters + +#### condition1 + +`C1` + +#### value1 + +`V1` + +#### condition2 + +`C2` + +#### value2 + +`V2` + +#### condition3 + +`C3` + +#### value3 + +`V3` + +#### condition4 + +`C4` + +#### value4 + +`V4` + +### Returns + +`CaseWhenResult`\<\[`V1`, `V2`, `V3`, `V4`\], `false`\> + +### Examples + +```ts +caseWhen(gt(user.age, 18), `adult`, `minor`) +``` + +```ts +caseWhen( + gt(user.age, 65), + `senior`, + gt(user.age, 18), + `adult`, + `minor`, +) +``` + +```ts +caseWhen(gt(user.age, 18), { + ...user, + posts: q + .from({ post: postsCollection }) + .where(({ post }) => eq(post.userId, user.id)), +}) +``` + +## Call Signature + +```ts +function caseWhen( + condition1, + value1, + condition2, + value2, + condition3, + value3, + condition4, + value4, +defaultValue): CaseWhenResult<[V1, V2, V3, V4, D], true>; +``` + +Defined in: [packages/db/src/query/builder/functions.ts:483](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L483) + +Returns the value for the first matching condition, similar to SQL +`CASE WHEN`. + +Arguments are evaluated as condition/value pairs followed by an optional +default value. Scalar branch values return a query expression and can be used +in expression contexts like `select`, `where`, `orderBy`, `groupBy`, +`having`, and equality join operands. If no scalar branch matches and no +default is provided, the result is `null`. + +When a branch value is a projection object, `caseWhen` becomes a select-only +projection value. Projection branches can include nested fields, ref spreads, +and includes. If no projection branch matches and no default is provided, the +result is `undefined`. + +### Type Parameters + +#### C1 + +`C1` *extends* `ExpressionLike` + +#### V1 + +`V1` *extends* `CaseWhenValue` + +#### C2 + +`C2` *extends* `ExpressionLike` + +#### V2 + +`V2` *extends* `CaseWhenValue` + +#### C3 + +`C3` *extends* `ExpressionLike` + +#### V3 + +`V3` *extends* `CaseWhenValue` + +#### C4 + +`C4` *extends* `ExpressionLike` + +#### V4 + +`V4` *extends* `CaseWhenValue` + +#### D + +`D` *extends* `CaseWhenValue` + +### Parameters + +#### condition1 + +`C1` + +#### value1 + +`V1` + +#### condition2 + +`C2` + +#### value2 + +`V2` + +#### condition3 + +`C3` + +#### value3 + +`V3` + +#### condition4 + +`C4` + +#### value4 + +`V4` + +#### defaultValue + +`D` + +### Returns + +`CaseWhenResult`\<\[`V1`, `V2`, `V3`, `V4`, `D`\], `true`\> + +### Examples + +```ts +caseWhen(gt(user.age, 18), `adult`, `minor`) +``` + +```ts +caseWhen( + gt(user.age, 65), + `senior`, + gt(user.age, 18), + `adult`, + `minor`, +) +``` + +```ts +caseWhen(gt(user.age, 18), { + ...user, + posts: q + .from({ post: postsCollection }) + .where(({ post }) => eq(post.userId, user.id)), +}) +``` + +## Call Signature + +```ts +function caseWhen( + condition1, + value1, + condition2, + value2, + condition3, + value3, + condition4, + value4, + condition5, +value5): CaseWhenResult<[V1, V2, V3, V4, V5], false>; +``` + +Defined in: [packages/db/src/query/builder/functions.ts:504](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L504) + +Returns the value for the first matching condition, similar to SQL +`CASE WHEN`. + +Arguments are evaluated as condition/value pairs followed by an optional +default value. Scalar branch values return a query expression and can be used +in expression contexts like `select`, `where`, `orderBy`, `groupBy`, +`having`, and equality join operands. If no scalar branch matches and no +default is provided, the result is `null`. + +When a branch value is a projection object, `caseWhen` becomes a select-only +projection value. Projection branches can include nested fields, ref spreads, +and includes. If no projection branch matches and no default is provided, the +result is `undefined`. + +### Type Parameters + +#### C1 + +`C1` *extends* `ExpressionLike` + +#### V1 + +`V1` *extends* `CaseWhenValue` + +#### C2 + +`C2` *extends* `ExpressionLike` + +#### V2 + +`V2` *extends* `CaseWhenValue` + +#### C3 + +`C3` *extends* `ExpressionLike` + +#### V3 + +`V3` *extends* `CaseWhenValue` + +#### C4 + +`C4` *extends* `ExpressionLike` + +#### V4 + +`V4` *extends* `CaseWhenValue` + +#### C5 + +`C5` *extends* `ExpressionLike` + +#### V5 + +`V5` *extends* `CaseWhenValue` + +### Parameters + +#### condition1 + +`C1` + +#### value1 + +`V1` + +#### condition2 + +`C2` + +#### value2 + +`V2` + +#### condition3 + +`C3` + +#### value3 + +`V3` + +#### condition4 + +`C4` + +#### value4 + +`V4` + +#### condition5 + +`C5` + +#### value5 + +`V5` + +### Returns + +`CaseWhenResult`\<\[`V1`, `V2`, `V3`, `V4`, `V5`\], `false`\> + +### Examples + +```ts +caseWhen(gt(user.age, 18), `adult`, `minor`) +``` + +```ts +caseWhen( + gt(user.age, 65), + `senior`, + gt(user.age, 18), + `adult`, + `minor`, +) +``` + +```ts +caseWhen(gt(user.age, 18), { + ...user, + posts: q + .from({ post: postsCollection }) + .where(({ post }) => eq(post.userId, user.id)), +}) +``` + +## Call Signature + +```ts +function caseWhen( + condition1, + value1, + condition2, + value2, + condition3, + value3, + condition4, + value4, + condition5, + value5, +defaultValue): CaseWhenResult<[V1, V2, V3, V4, V5, D], true>; +``` + +Defined in: [packages/db/src/query/builder/functions.ts:527](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L527) + +Returns the value for the first matching condition, similar to SQL +`CASE WHEN`. + +Arguments are evaluated as condition/value pairs followed by an optional +default value. Scalar branch values return a query expression and can be used +in expression contexts like `select`, `where`, `orderBy`, `groupBy`, +`having`, and equality join operands. If no scalar branch matches and no +default is provided, the result is `null`. + +When a branch value is a projection object, `caseWhen` becomes a select-only +projection value. Projection branches can include nested fields, ref spreads, +and includes. If no projection branch matches and no default is provided, the +result is `undefined`. + +### Type Parameters + +#### C1 + +`C1` *extends* `ExpressionLike` + +#### V1 + +`V1` *extends* `CaseWhenValue` + +#### C2 + +`C2` *extends* `ExpressionLike` + +#### V2 + +`V2` *extends* `CaseWhenValue` + +#### C3 + +`C3` *extends* `ExpressionLike` + +#### V3 + +`V3` *extends* `CaseWhenValue` + +#### C4 + +`C4` *extends* `ExpressionLike` + +#### V4 + +`V4` *extends* `CaseWhenValue` + +#### C5 + +`C5` *extends* `ExpressionLike` + +#### V5 + +`V5` *extends* `CaseWhenValue` + +#### D + +`D` *extends* `CaseWhenValue` + +### Parameters + +#### condition1 + +`C1` + +#### value1 + +`V1` + +#### condition2 + +`C2` + +#### value2 + +`V2` + +#### condition3 + +`C3` + +#### value3 + +`V3` + +#### condition4 + +`C4` + +#### value4 + +`V4` + +#### condition5 + +`C5` + +#### value5 + +`V5` + +#### defaultValue + +`D` + +### Returns + +`CaseWhenResult`\<\[`V1`, `V2`, `V3`, `V4`, `V5`, `D`\], `true`\> + +### Examples + +```ts +caseWhen(gt(user.age, 18), `adult`, `minor`) +``` + +```ts +caseWhen( + gt(user.age, 65), + `senior`, + gt(user.age, 18), + `adult`, + `minor`, +) +``` + +```ts +caseWhen(gt(user.age, 18), { + ...user, + posts: q + .from({ post: postsCollection }) + .where(({ post }) => eq(post.userId, user.id)), +}) +``` + +## Call Signature + +```ts +function caseWhen( + condition1, + value1, + condition2, + value2, + condition3, + value3, + condition4, + value4, + condition5, + value5, + condition6, + value6, ... + rest): any; +``` + +Defined in: [packages/db/src/query/builder/functions.ts:552](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L552) + +Returns the value for the first matching condition, similar to SQL +`CASE WHEN`. + +Arguments are evaluated as condition/value pairs followed by an optional +default value. Scalar branch values return a query expression and can be used +in expression contexts like `select`, `where`, `orderBy`, `groupBy`, +`having`, and equality join operands. If no scalar branch matches and no +default is provided, the result is `null`. + +When a branch value is a projection object, `caseWhen` becomes a select-only +projection value. Projection branches can include nested fields, ref spreads, +and includes. If no projection branch matches and no default is provided, the +result is `undefined`. + +### Type Parameters + +#### C1 + +`C1` *extends* `ExpressionLike` + +#### V1 + +`V1` *extends* `CaseWhenValue` + +#### C2 + +`C2` *extends* `ExpressionLike` + +#### V2 + +`V2` *extends* `CaseWhenValue` + +#### C3 + +`C3` *extends* `ExpressionLike` + +#### V3 + +`V3` *extends* `CaseWhenValue` + +#### C4 + +`C4` *extends* `ExpressionLike` + +#### V4 + +`V4` *extends* `CaseWhenValue` + +#### C5 + +`C5` *extends* `ExpressionLike` + +#### V5 + +`V5` *extends* `CaseWhenValue` + +### Parameters + +#### condition1 + +`C1` + +#### value1 + +`V1` + +#### condition2 + +`C2` + +#### value2 + +`V2` + +#### condition3 + +`C3` + +#### value3 + +`V3` + +#### condition4 + +`C4` + +#### value4 + +`V4` + +#### condition5 + +`C5` + +#### value5 + +`V5` + +#### condition6 + +`ExpressionLike` + +#### value6 + +`CaseWhenValue` + +#### rest + +...`CaseWhenValue`[] + +### Returns + +`any` + +### Examples + +```ts +caseWhen(gt(user.age, 18), `adult`, `minor`) +``` + +```ts +caseWhen( + gt(user.age, 65), + `senior`, + gt(user.age, 18), + `adult`, + `minor`, +) +``` + +```ts +caseWhen(gt(user.age, 18), { + ...user, + posts: q + .from({ post: postsCollection }) + .where(({ post }) => eq(post.userId, user.id)), +}) +``` diff --git a/docs/reference/functions/coalesce.md b/docs/reference/functions/coalesce.md index d78f0bbc8c..6d4e95ecb2 100644 --- a/docs/reference/functions/coalesce.md +++ b/docs/reference/functions/coalesce.md @@ -9,7 +9,7 @@ title: coalesce function coalesce(...args): CoalesceReturnType; ``` -Defined in: [packages/db/src/query/builder/functions.ts:345](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L345) +Defined in: [packages/db/src/query/builder/functions.ts:349](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L349) ## Type Parameters diff --git a/docs/reference/functions/compileExpression.md b/docs/reference/functions/compileExpression.md index 1908f7522a..90f18bc152 100644 --- a/docs/reference/functions/compileExpression.md +++ b/docs/reference/functions/compileExpression.md @@ -9,7 +9,7 @@ title: compileExpression function compileExpression(expr, isSingleRow): CompiledSingleRowExpression | CompiledExpression; ``` -Defined in: [packages/db/src/query/compiler/evaluators.ts:72](https://github.com/TanStack/db/blob/main/packages/db/src/query/compiler/evaluators.ts#L72) +Defined in: [packages/db/src/query/compiler/evaluators.ts:89](https://github.com/TanStack/db/blob/main/packages/db/src/query/compiler/evaluators.ts#L89) Compiles an expression into an optimized evaluator function. This eliminates branching during evaluation by pre-compiling the expression structure. diff --git a/docs/reference/functions/compileQuery.md b/docs/reference/functions/compileQuery.md index 4f6db2f575..049b05d593 100644 --- a/docs/reference/functions/compileQuery.md +++ b/docs/reference/functions/compileQuery.md @@ -21,7 +21,7 @@ function compileQuery( childCorrelationField?): CompilationResult; ``` -Defined in: [packages/db/src/query/compiler/index.ts:130](https://github.com/TanStack/db/blob/main/packages/db/src/query/compiler/index.ts#L130) +Defined in: [packages/db/src/query/compiler/index.ts:162](https://github.com/TanStack/db/blob/main/packages/db/src/query/compiler/index.ts#L162) Compiles a query IR into a D2 pipeline diff --git a/docs/reference/functions/compileSingleRowExpression.md b/docs/reference/functions/compileSingleRowExpression.md index 19ab88aa27..7fe2fc727b 100644 --- a/docs/reference/functions/compileSingleRowExpression.md +++ b/docs/reference/functions/compileSingleRowExpression.md @@ -9,7 +9,7 @@ title: compileSingleRowExpression function compileSingleRowExpression(expr): CompiledSingleRowExpression; ``` -Defined in: [packages/db/src/query/compiler/evaluators.ts:83](https://github.com/TanStack/db/blob/main/packages/db/src/query/compiler/evaluators.ts#L83) +Defined in: [packages/db/src/query/compiler/evaluators.ts:100](https://github.com/TanStack/db/blob/main/packages/db/src/query/compiler/evaluators.ts#L100) Compiles a single-row expression into an optimized evaluator function. diff --git a/docs/reference/functions/concat.md b/docs/reference/functions/concat.md index ddfaf560e5..8ac8228207 100644 --- a/docs/reference/functions/concat.md +++ b/docs/reference/functions/concat.md @@ -11,7 +11,7 @@ title: concat function concat(arg): ConcatToArrayWrapper; ``` -Defined in: [packages/db/src/query/builder/functions.ts:297](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L297) +Defined in: [packages/db/src/query/builder/functions.ts:301](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L301) ### Type Parameters @@ -35,7 +35,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:297](https://github.com/ function concat(...args): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:300](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L300) +Defined in: [packages/db/src/query/builder/functions.ts:304](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L304) ### Parameters diff --git a/docs/reference/functions/count.md b/docs/reference/functions/count.md index c1a8436e97..f09ebb4134 100644 --- a/docs/reference/functions/count.md +++ b/docs/reference/functions/count.md @@ -9,7 +9,7 @@ title: count function count(arg): Aggregate; ``` -Defined in: [packages/db/src/query/builder/functions.ts:366](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L366) +Defined in: [packages/db/src/query/builder/functions.ts:643](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L643) ## Parameters diff --git a/docs/reference/functions/createCollection.md b/docs/reference/functions/createCollection.md index c36372d477..69d77e7a10 100644 --- a/docs/reference/functions/createCollection.md +++ b/docs/reference/functions/createCollection.md @@ -11,7 +11,7 @@ title: createCollection function createCollection(options): Collection, TKey, TUtils, T, InferSchemaInput> & NonSingleResult; ``` -Defined in: [packages/db/src/collection/index.ts:140](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L140) +Defined in: [packages/db/src/collection/index.ts:141](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L141) Creates a new Collection instance with the given configuration @@ -120,7 +120,7 @@ const todos = createCollection({ function createCollection(options): Collection, TKey, Exclude, T, InferSchemaInput> & NonSingleResult; ``` -Defined in: [packages/db/src/collection/index.ts:157](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L157) +Defined in: [packages/db/src/collection/index.ts:158](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L158) Creates a new Collection instance with the given configuration @@ -229,7 +229,7 @@ const todos = createCollection({ function createCollection(options): Collection, TKey, TUtils, T, InferSchemaInput> & SingleResult; ``` -Defined in: [packages/db/src/collection/index.ts:175](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L175) +Defined in: [packages/db/src/collection/index.ts:176](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L176) Creates a new Collection instance with the given configuration @@ -338,7 +338,7 @@ const todos = createCollection({ function createCollection(options): Collection, TKey, TUtils, T, InferSchemaInput> & SingleResult; ``` -Defined in: [packages/db/src/collection/index.ts:191](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L191) +Defined in: [packages/db/src/collection/index.ts:192](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L192) Creates a new Collection instance with the given configuration @@ -447,7 +447,7 @@ const todos = createCollection({ function createCollection(options): Collection & NonSingleResult; ``` -Defined in: [packages/db/src/collection/index.ts:204](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L204) +Defined in: [packages/db/src/collection/index.ts:205](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L205) Creates a new Collection instance with the given configuration @@ -556,7 +556,7 @@ const todos = createCollection({ function createCollection(options): Collection & NonSingleResult; ``` -Defined in: [packages/db/src/collection/index.ts:217](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L217) +Defined in: [packages/db/src/collection/index.ts:218](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L218) Creates a new Collection instance with the given configuration @@ -665,7 +665,7 @@ const todos = createCollection({ function createCollection(options): Collection & SingleResult; ``` -Defined in: [packages/db/src/collection/index.ts:229](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L229) +Defined in: [packages/db/src/collection/index.ts:230](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L230) Creates a new Collection instance with the given configuration @@ -774,7 +774,7 @@ const todos = createCollection({ function createCollection(options): Collection & SingleResult; ``` -Defined in: [packages/db/src/collection/index.ts:242](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L242) +Defined in: [packages/db/src/collection/index.ts:243](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L243) Creates a new Collection instance with the given configuration diff --git a/docs/reference/functions/createTransaction.md b/docs/reference/functions/createTransaction.md index e1207af829..596bd96331 100644 --- a/docs/reference/functions/createTransaction.md +++ b/docs/reference/functions/createTransaction.md @@ -9,7 +9,7 @@ title: createTransaction function createTransaction(config): Transaction; ``` -Defined in: [packages/db/src/transactions.ts:156](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L156) +Defined in: [packages/db/src/transactions.ts:157](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L157) Creates a new transaction for grouping multiple collection operations diff --git a/docs/reference/functions/divide.md b/docs/reference/functions/divide.md new file mode 100644 index 0000000000..b8424ec323 --- /dev/null +++ b/docs/reference/functions/divide.md @@ -0,0 +1,36 @@ +--- +id: divide +title: divide +--- + +# Function: divide() + +```ts +function divide(left, right): DivideReturnType; +``` + +Defined in: [packages/db/src/query/builder/functions.ts:631](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L631) + +## Type Parameters + +### T1 + +`T1` *extends* `ExpressionLike` + +### T2 + +`T2` *extends* `ExpressionLike` + +## Parameters + +### left + +`T1` + +### right + +`T2` + +## Returns + +`DivideReturnType` diff --git a/docs/reference/functions/eq.md b/docs/reference/functions/eq.md index 794ec946c3..bda6e68399 100644 --- a/docs/reference/functions/eq.md +++ b/docs/reference/functions/eq.md @@ -11,7 +11,7 @@ title: eq function eq(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:133](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L133) +Defined in: [packages/db/src/query/builder/functions.ts:137](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L137) ### Type Parameters @@ -39,7 +39,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:133](https://github.com/ function eq(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:137](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L137) +Defined in: [packages/db/src/query/builder/functions.ts:141](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L141) ### Type Parameters @@ -67,7 +67,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:137](https://github.com/ function eq(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:141](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L141) +Defined in: [packages/db/src/query/builder/functions.ts:145](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L145) ### Type Parameters diff --git a/docs/reference/functions/findIndexForField.md b/docs/reference/functions/findIndexForField.md index aaab37d8ef..9ff4274941 100644 --- a/docs/reference/functions/findIndexForField.md +++ b/docs/reference/functions/findIndexForField.md @@ -14,7 +14,7 @@ function findIndexForField( | undefined; ``` -Defined in: [packages/db/src/utils/index-optimization.ts:37](https://github.com/TanStack/db/blob/main/packages/db/src/utils/index-optimization.ts#L37) +Defined in: [packages/db/src/utils/index-optimization.ts:45](https://github.com/TanStack/db/blob/main/packages/db/src/utils/index-optimization.ts#L45) Finds an index that matches a given field path diff --git a/docs/reference/functions/getActiveTransaction.md b/docs/reference/functions/getActiveTransaction.md index b1488c8d08..45a5bd2514 100644 --- a/docs/reference/functions/getActiveTransaction.md +++ b/docs/reference/functions/getActiveTransaction.md @@ -11,7 +11,7 @@ function getActiveTransaction(): | undefined; ``` -Defined in: [packages/db/src/transactions.ts:175](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L175) +Defined in: [packages/db/src/transactions.ts:176](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L176) Gets the currently active ambient transaction, if any Used internally by collection operations to join existing transactions diff --git a/docs/reference/functions/getLiveQueryStatusFlags.md b/docs/reference/functions/getLiveQueryStatusFlags.md new file mode 100644 index 0000000000..4dedb0d47f --- /dev/null +++ b/docs/reference/functions/getLiveQueryStatusFlags.md @@ -0,0 +1,26 @@ +--- +id: getLiveQueryStatusFlags +title: getLiveQueryStatusFlags +--- + +# Function: getLiveQueryStatusFlags() + +```ts +function getLiveQueryStatusFlags(status): LiveQueryStatusFlags; +``` + +Defined in: [packages/db/src/live-query-adapter.ts:58](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-adapter.ts#L58) + +Derive the boolean status flags from a collection status. Adapters represent +a disabled query separately (with `isReady: true`); this covers the real +`CollectionStatus` values. + +## Parameters + +### status + +[`CollectionStatus`](../type-aliases/CollectionStatus.md) + +## Returns + +[`LiveQueryStatusFlags`](../interfaces/LiveQueryStatusFlags.md) diff --git a/docs/reference/functions/gt.md b/docs/reference/functions/gt.md index 42f0ff1883..5e7dd5cc89 100644 --- a/docs/reference/functions/gt.md +++ b/docs/reference/functions/gt.md @@ -11,7 +11,7 @@ title: gt function gt(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:146](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L146) +Defined in: [packages/db/src/query/builder/functions.ts:150](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L150) ### Type Parameters @@ -39,7 +39,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:146](https://github.com/ function gt(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:150](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L150) +Defined in: [packages/db/src/query/builder/functions.ts:154](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L154) ### Type Parameters @@ -67,7 +67,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:150](https://github.com/ function gt(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:154](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L154) +Defined in: [packages/db/src/query/builder/functions.ts:158](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L158) ### Type Parameters diff --git a/docs/reference/functions/gte.md b/docs/reference/functions/gte.md index 18aecd8c67..efcba19155 100644 --- a/docs/reference/functions/gte.md +++ b/docs/reference/functions/gte.md @@ -11,7 +11,7 @@ title: gte function gte(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:159](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L159) +Defined in: [packages/db/src/query/builder/functions.ts:163](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L163) ### Type Parameters @@ -39,7 +39,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:159](https://github.com/ function gte(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:163](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L163) +Defined in: [packages/db/src/query/builder/functions.ts:167](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L167) ### Type Parameters @@ -67,7 +67,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:163](https://github.com/ function gte(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:167](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L167) +Defined in: [packages/db/src/query/builder/functions.ts:171](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L171) ### Type Parameters diff --git a/docs/reference/functions/hasVirtualProps.md b/docs/reference/functions/hasVirtualProps.md index 57a2b9c614..19013aac4e 100644 --- a/docs/reference/functions/hasVirtualProps.md +++ b/docs/reference/functions/hasVirtualProps.md @@ -9,7 +9,7 @@ title: hasVirtualProps function hasVirtualProps(value): value is VirtualRowProps; ``` -Defined in: [packages/db/src/virtual-props.ts:145](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L145) +Defined in: [packages/db/src/virtual-props.ts:150](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L150) Checks if a value has virtual properties attached. diff --git a/docs/reference/functions/ilike.md b/docs/reference/functions/ilike.md index faf4f82371..4d51513275 100644 --- a/docs/reference/functions/ilike.md +++ b/docs/reference/functions/ilike.md @@ -9,7 +9,7 @@ title: ilike function ilike(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:270](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L270) +Defined in: [packages/db/src/query/builder/functions.ts:274](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L274) ## Parameters diff --git a/docs/reference/functions/inArray.md b/docs/reference/functions/inArray.md index 6a8d545296..c6ed1471b9 100644 --- a/docs/reference/functions/inArray.md +++ b/docs/reference/functions/inArray.md @@ -9,7 +9,7 @@ title: inArray function inArray(value, array): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:255](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L255) +Defined in: [packages/db/src/query/builder/functions.ts:259](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L259) ## Parameters diff --git a/docs/reference/functions/isCollection.md b/docs/reference/functions/isCollection.md new file mode 100644 index 0000000000..a846ecfa33 --- /dev/null +++ b/docs/reference/functions/isCollection.md @@ -0,0 +1,29 @@ +--- +id: isCollection +title: isCollection +--- + +# Function: isCollection() + +```ts +function isCollection(value): value is Collection, any>; +``` + +Defined in: [packages/db/src/live-query-adapter.ts:22](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-adapter.ts#L22) + +Structural check for a live-query/`Collection` instance. + +Uses duck typing rather than `instanceof CollectionImpl` on purpose: adapters +and core can resolve to different copies of `@tanstack/db` (dual-package / +multi-realm), where `instanceof` gives false negatives. The three methods +below uniquely identify a Collection. + +## Parameters + +### value + +`unknown` + +## Returns + +`value is Collection, any>` diff --git a/docs/reference/functions/isNull.md b/docs/reference/functions/isNull.md index cf303fc635..fbd75223ca 100644 --- a/docs/reference/functions/isNull.md +++ b/docs/reference/functions/isNull.md @@ -9,7 +9,7 @@ title: isNull function isNull(value): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:251](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L251) +Defined in: [packages/db/src/query/builder/functions.ts:255](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L255) ## Parameters diff --git a/docs/reference/functions/isSingleResultCollection.md b/docs/reference/functions/isSingleResultCollection.md new file mode 100644 index 0000000000..1ec948a6d7 --- /dev/null +++ b/docs/reference/functions/isSingleResultCollection.md @@ -0,0 +1,24 @@ +--- +id: isSingleResultCollection +title: isSingleResultCollection +--- + +# Function: isSingleResultCollection() + +```ts +function isSingleResultCollection(collection): boolean; +``` + +Defined in: [packages/db/src/live-query-adapter.ts:35](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-adapter.ts#L35) + +Whether a collection yields a single result (`findOne`) rather than an array. + +## Parameters + +### collection + +[`Collection`](../interfaces/Collection.md)\<`any`, `any`, `any`\> + +## Returns + +`boolean` diff --git a/docs/reference/functions/isUndefined.md b/docs/reference/functions/isUndefined.md index 49d99d535b..5444e21830 100644 --- a/docs/reference/functions/isUndefined.md +++ b/docs/reference/functions/isUndefined.md @@ -9,7 +9,7 @@ title: isUndefined function isUndefined(value): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:247](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L247) +Defined in: [packages/db/src/query/builder/functions.ts:251](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L251) ## Parameters diff --git a/docs/reference/functions/length.md b/docs/reference/functions/length.md index d452d3a863..1586545934 100644 --- a/docs/reference/functions/length.md +++ b/docs/reference/functions/length.md @@ -9,7 +9,7 @@ title: length function length(arg): NumericFunctionReturnType; ``` -Defined in: [packages/db/src/query/builder/functions.ts:291](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L291) +Defined in: [packages/db/src/query/builder/functions.ts:295](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L295) ## Type Parameters diff --git a/docs/reference/functions/like.md b/docs/reference/functions/like.md index a05fa13226..ee6f72f03e 100644 --- a/docs/reference/functions/like.md +++ b/docs/reference/functions/like.md @@ -9,7 +9,7 @@ title: like function like(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:262](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L262) +Defined in: [packages/db/src/query/builder/functions.ts:266](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L266) ## Parameters diff --git a/docs/reference/functions/localOnlyCollectionOptions.md b/docs/reference/functions/localOnlyCollectionOptions.md index 41c22862b3..3f034ae8d3 100644 --- a/docs/reference/functions/localOnlyCollectionOptions.md +++ b/docs/reference/functions/localOnlyCollectionOptions.md @@ -11,7 +11,7 @@ title: localOnlyCollectionOptions function localOnlyCollectionOptions(config): CollectionConfig, TKey, T, LocalOnlyCollectionUtils> & object & object; ``` -Defined in: [packages/db/src/local-only.ts:149](https://github.com/TanStack/db/blob/main/packages/db/src/local-only.ts#L149) +Defined in: [packages/db/src/local-only.ts:150](https://github.com/TanStack/db/blob/main/packages/db/src/local-only.ts#L150) Creates Local-only collection options for use with a standard Collection @@ -123,7 +123,7 @@ await tx.commit() function localOnlyCollectionOptions(config): CollectionConfig & object & object; ``` -Defined in: [packages/db/src/local-only.ts:162](https://github.com/TanStack/db/blob/main/packages/db/src/local-only.ts#L162) +Defined in: [packages/db/src/local-only.ts:163](https://github.com/TanStack/db/blob/main/packages/db/src/local-only.ts#L163) Creates Local-only collection options for use with a standard Collection diff --git a/docs/reference/functions/localStorageCollectionOptions.md b/docs/reference/functions/localStorageCollectionOptions.md index 6ea5ecad08..4516ac84a8 100644 --- a/docs/reference/functions/localStorageCollectionOptions.md +++ b/docs/reference/functions/localStorageCollectionOptions.md @@ -11,7 +11,7 @@ title: localStorageCollectionOptions function localStorageCollectionOptions(config): CollectionConfig, TKey, T, LocalStorageCollectionUtils> & object; ``` -Defined in: [packages/db/src/local-storage.ts:316](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L316) +Defined in: [packages/db/src/local-storage.ts:317](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L317) Creates localStorage collection options for use with a standard Collection @@ -126,7 +126,7 @@ await tx.commit() function localStorageCollectionOptions(config): CollectionConfig & object; ``` -Defined in: [packages/db/src/local-storage.ts:336](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L336) +Defined in: [packages/db/src/local-storage.ts:337](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L337) Creates localStorage collection options for use with a standard Collection diff --git a/docs/reference/functions/lower.md b/docs/reference/functions/lower.md index fff4fc0830..f9739693e7 100644 --- a/docs/reference/functions/lower.md +++ b/docs/reference/functions/lower.md @@ -9,7 +9,7 @@ title: lower function lower(arg): StringFunctionReturnType; ``` -Defined in: [packages/db/src/query/builder/functions.ts:285](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L285) +Defined in: [packages/db/src/query/builder/functions.ts:289](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L289) ## Type Parameters diff --git a/docs/reference/functions/lt.md b/docs/reference/functions/lt.md index 7c204f4a93..e103058e7d 100644 --- a/docs/reference/functions/lt.md +++ b/docs/reference/functions/lt.md @@ -11,7 +11,7 @@ title: lt function lt(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:172](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L172) +Defined in: [packages/db/src/query/builder/functions.ts:176](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L176) ### Type Parameters @@ -39,7 +39,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:172](https://github.com/ function lt(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:176](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L176) +Defined in: [packages/db/src/query/builder/functions.ts:180](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L180) ### Type Parameters @@ -67,7 +67,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:176](https://github.com/ function lt(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:180](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L180) +Defined in: [packages/db/src/query/builder/functions.ts:184](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L184) ### Type Parameters diff --git a/docs/reference/functions/lte.md b/docs/reference/functions/lte.md index 2b2b5423cc..01fc872a0e 100644 --- a/docs/reference/functions/lte.md +++ b/docs/reference/functions/lte.md @@ -11,7 +11,7 @@ title: lte function lte(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:185](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L185) +Defined in: [packages/db/src/query/builder/functions.ts:189](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L189) ### Type Parameters @@ -39,7 +39,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:185](https://github.com/ function lte(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:189](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L189) +Defined in: [packages/db/src/query/builder/functions.ts:193](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L193) ### Type Parameters @@ -67,7 +67,7 @@ Defined in: [packages/db/src/query/builder/functions.ts:189](https://github.com/ function lte(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:193](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L193) +Defined in: [packages/db/src/query/builder/functions.ts:197](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L197) ### Type Parameters diff --git a/docs/reference/functions/materialize.md b/docs/reference/functions/materialize.md new file mode 100644 index 0000000000..8f80a39eac --- /dev/null +++ b/docs/reference/functions/materialize.md @@ -0,0 +1,58 @@ +--- +id: materialize +title: materialize +--- + +# Function: materialize() + +```ts +function materialize(query): MaterializeWrapper, TContext extends SingleResult ? true : false>; +``` + +Defined in: [packages/db/src/query/builder/functions.ts:848](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L848) + +Materialize an includes subquery into a plain value on the parent row. + +- For multi-row subqueries, the parent receives an `Array` snapshot + (equivalent to `toArray()`). +- For `findOne()` subqueries, the parent receives a single `T | undefined` + value — `undefined` when no child matches. + +The snapshot updates reactively: parent rows re-emit when the underlying +children change. + +## Type Parameters + +### TContext + +`TContext` *extends* [`Context`](../interfaces/Context.md) + +## Parameters + +### query + +[`QueryBuilder`](../type-aliases/QueryBuilder.md)\<`TContext`\> + +## Returns + +`MaterializeWrapper`\<`GetRawResult`\<`TContext`\>, `TContext` *extends* [`SingleResult`](../type-aliases/SingleResult.md) ? `true` : `false`\> + +## Example + +```ts +// Multi-row: produces Array on each project +select(({ p }) => ({ + ...p, + issues: materialize( + q.from({ i: issues }).where(({ i }) => eq(i.projectId, p.id)), + ), +})) + +// Singleton: produces Author | undefined on each post +select(({ p }) => ({ + ...p, + author: materialize( + q.from({ a: authors }).where(({ a }) => eq(a.id, p.authorId)).findOne(), + ), +})) +``` diff --git a/docs/reference/functions/max.md b/docs/reference/functions/max.md index 008439a7d1..6d04ccf084 100644 --- a/docs/reference/functions/max.md +++ b/docs/reference/functions/max.md @@ -9,7 +9,7 @@ title: max function max(arg): AggregateReturnType; ``` -Defined in: [packages/db/src/query/builder/functions.ts:382](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L382) +Defined in: [packages/db/src/query/builder/functions.ts:659](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L659) ## Type Parameters diff --git a/docs/reference/functions/min.md b/docs/reference/functions/min.md index 8c5788295b..f470eef662 100644 --- a/docs/reference/functions/min.md +++ b/docs/reference/functions/min.md @@ -9,7 +9,7 @@ title: min function min(arg): AggregateReturnType; ``` -Defined in: [packages/db/src/query/builder/functions.ts:378](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L378) +Defined in: [packages/db/src/query/builder/functions.ts:655](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L655) ## Type Parameters diff --git a/docs/reference/functions/multiply.md b/docs/reference/functions/multiply.md new file mode 100644 index 0000000000..868c04b5da --- /dev/null +++ b/docs/reference/functions/multiply.md @@ -0,0 +1,36 @@ +--- +id: multiply +title: multiply +--- + +# Function: multiply() + +```ts +function multiply(left, right): BinaryNumericReturnType; +``` + +Defined in: [packages/db/src/query/builder/functions.ts:621](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L621) + +## Type Parameters + +### T1 + +`T1` *extends* `ExpressionLike` + +### T2 + +`T2` *extends* `ExpressionLike` + +## Parameters + +### left + +`T1` + +### right + +`T2` + +## Returns + +`BinaryNumericReturnType` diff --git a/docs/reference/functions/not.md b/docs/reference/functions/not.md index e6020ac2de..9b0c6b2164 100644 --- a/docs/reference/functions/not.md +++ b/docs/reference/functions/not.md @@ -9,7 +9,7 @@ title: not function not(value): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:242](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L242) +Defined in: [packages/db/src/query/builder/functions.ts:246](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L246) ## Parameters diff --git a/docs/reference/functions/optimizeExpressionWithIndexes.md b/docs/reference/functions/optimizeExpressionWithIndexes.md index 31d06210a8..4f0db98160 100644 --- a/docs/reference/functions/optimizeExpressionWithIndexes.md +++ b/docs/reference/functions/optimizeExpressionWithIndexes.md @@ -9,7 +9,7 @@ title: optimizeExpressionWithIndexes function optimizeExpressionWithIndexes(expression, collection): OptimizationResult; ``` -Defined in: [packages/db/src/utils/index-optimization.ts:100](https://github.com/TanStack/db/blob/main/packages/db/src/utils/index-optimization.ts#L100) +Defined in: [packages/db/src/utils/index-optimization.ts:196](https://github.com/TanStack/db/blob/main/packages/db/src/utils/index-optimization.ts#L196) Optimizes a query expression using available indexes to find matching keys diff --git a/docs/reference/functions/or.md b/docs/reference/functions/or.md index 95adf2ff2e..73b058e122 100644 --- a/docs/reference/functions/or.md +++ b/docs/reference/functions/or.md @@ -11,7 +11,7 @@ title: or function or(left, right): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:221](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L221) +Defined in: [packages/db/src/query/builder/functions.ts:225](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L225) ### Parameters @@ -36,7 +36,7 @@ function or( rest): BasicExpression; ``` -Defined in: [packages/db/src/query/builder/functions.ts:225](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L225) +Defined in: [packages/db/src/query/builder/functions.ts:229](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L229) ### Parameters diff --git a/docs/reference/functions/safeRandomUUID.md b/docs/reference/functions/safeRandomUUID.md new file mode 100644 index 0000000000..145a79caaf --- /dev/null +++ b/docs/reference/functions/safeRandomUUID.md @@ -0,0 +1,25 @@ +--- +id: safeRandomUUID +title: safeRandomUUID +--- + +# Function: safeRandomUUID() + +```ts +function safeRandomUUID(): string; +``` + +Defined in: [packages/db/src/utils/uuid.ts:11](https://github.com/TanStack/db/blob/main/packages/db/src/utils/uuid.ts#L11) + +Returns a RFC 4122 version 4 UUID. + +Prefers `crypto.randomUUID()` when available. In non-secure browser contexts +(e.g. a dev server accessed via a LAN IP over HTTP) `crypto.randomUUID` is +`undefined`, so this falls back to building a UUIDv4 from +`crypto.getRandomValues`. Throws if neither API is available. + +See https://github.com/TanStack/db/issues/1541. + +## Returns + +`string` diff --git a/docs/reference/functions/subtract.md b/docs/reference/functions/subtract.md new file mode 100644 index 0000000000..3ae204d17e --- /dev/null +++ b/docs/reference/functions/subtract.md @@ -0,0 +1,36 @@ +--- +id: subtract +title: subtract +--- + +# Function: subtract() + +```ts +function subtract(left, right): BinaryNumericReturnType; +``` + +Defined in: [packages/db/src/query/builder/functions.ts:611](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L611) + +## Type Parameters + +### T1 + +`T1` *extends* `ExpressionLike` + +### T2 + +`T2` *extends* `ExpressionLike` + +## Parameters + +### left + +`T1` + +### right + +`T2` + +## Returns + +`BinaryNumericReturnType` diff --git a/docs/reference/functions/sum.md b/docs/reference/functions/sum.md index 3dcf0b62d7..2b6850a87c 100644 --- a/docs/reference/functions/sum.md +++ b/docs/reference/functions/sum.md @@ -9,7 +9,7 @@ title: sum function sum(arg): AggregateReturnType; ``` -Defined in: [packages/db/src/query/builder/functions.ts:374](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L374) +Defined in: [packages/db/src/query/builder/functions.ts:651](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L651) ## Type Parameters diff --git a/docs/reference/functions/toArray.md b/docs/reference/functions/toArray.md index edcf651bf0..f4dbfcf302 100644 --- a/docs/reference/functions/toArray.md +++ b/docs/reference/functions/toArray.md @@ -9,7 +9,7 @@ title: toArray function toArray(query): ToArrayWrapper>; ``` -Defined in: [packages/db/src/query/builder/functions.ts:453](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L453) +Defined in: [packages/db/src/query/builder/functions.ts:752](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L752) ## Type Parameters diff --git a/docs/reference/functions/toBooleanPredicate.md b/docs/reference/functions/toBooleanPredicate.md index e1ed96757b..88121e0c23 100644 --- a/docs/reference/functions/toBooleanPredicate.md +++ b/docs/reference/functions/toBooleanPredicate.md @@ -9,7 +9,7 @@ title: toBooleanPredicate function toBooleanPredicate(result): boolean; ``` -Defined in: [packages/db/src/query/compiler/evaluators.ts:54](https://github.com/TanStack/db/blob/main/packages/db/src/query/compiler/evaluators.ts#L54) +Defined in: [packages/db/src/query/compiler/evaluators.ts:71](https://github.com/TanStack/db/blob/main/packages/db/src/query/compiler/evaluators.ts#L71) Converts a 3-valued logic result to a boolean for use in WHERE/HAVING filters. In SQL, UNKNOWN (null) values in WHERE clauses exclude rows, matching false behavior. diff --git a/docs/reference/functions/upper.md b/docs/reference/functions/upper.md index 390fc9fb31..bb33cb7976 100644 --- a/docs/reference/functions/upper.md +++ b/docs/reference/functions/upper.md @@ -9,7 +9,7 @@ title: upper function upper(arg): StringFunctionReturnType; ``` -Defined in: [packages/db/src/query/builder/functions.ts:279](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L279) +Defined in: [packages/db/src/query/builder/functions.ts:283](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L283) ## Type Parameters diff --git a/docs/reference/index.md b/docs/reference/index.md index ce09d71916..7030d02310 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -99,6 +99,7 @@ title: "@tanstack/db" - [UnknownExpressionTypeError](classes/UnknownExpressionTypeError.md) - [UnknownFunctionError](classes/UnknownFunctionError.md) - [UnknownHavingExpressionTypeError](classes/UnknownHavingExpressionTypeError.md) +- [UnsafeAliasPathError](classes/UnsafeAliasPathError.md) - [UnsupportedAggregateFunctionError](classes/UnsupportedAggregateFunctionError.md) - [UnsupportedFromTypeError](classes/UnsupportedFromTypeError.md) - [UnsupportedJoinSourceTypeError](classes/UnsupportedJoinSourceTypeError.md) @@ -133,6 +134,7 @@ title: "@tanstack/db" - [IndexSuggestion](interfaces/IndexSuggestion.md) - [InsertConfig](interfaces/InsertConfig.md) - [LiveQueryCollectionConfig](interfaces/LiveQueryCollectionConfig.md) +- [LiveQueryStatusFlags](interfaces/LiveQueryStatusFlags.md) - [LocalOnlyCollectionConfig](interfaces/LocalOnlyCollectionConfig.md) - [LocalOnlyCollectionUtils](interfaces/LocalOnlyCollectionUtils.md) - [LocalStorageCollectionConfig](interfaces/LocalStorageCollectionConfig.md) @@ -172,6 +174,9 @@ title: "@tanstack/db" - [ClearStorageFn](type-aliases/ClearStorageFn.md) - [CollectionConfigSingleRowOption](type-aliases/CollectionConfigSingleRowOption.md) - [CollectionStatus](type-aliases/CollectionStatus.md) +- [ContextFromSource](type-aliases/ContextFromSource.md) +- [ContextFromUnionBranches](type-aliases/ContextFromUnionBranches.md) +- [ContextFromUnionSource](type-aliases/ContextFromUnionSource.md) - [ContextSchema](type-aliases/ContextSchema.md) - [CursorExpressions](type-aliases/CursorExpressions.md) - [DeleteKeyMessage](type-aliases/DeleteKeyMessage.md) @@ -230,7 +235,9 @@ title: "@tanstack/db" - [SchemaFromSource](type-aliases/SchemaFromSource.md) - [SelectObject](type-aliases/SelectObject.md) - [SingleResult](type-aliases/SingleResult.md) +- [SingleSource](type-aliases/SingleSource.md) - [Source](type-aliases/Source.md) +- [SourceClauseContext](type-aliases/SourceClauseContext.md) - [StandardSchema](type-aliases/StandardSchema.md) - [StandardSchemaAlias](type-aliases/StandardSchemaAlias.md) - [StorageApi](type-aliases/StorageApi.md) @@ -284,6 +291,7 @@ title: "@tanstack/db" - [createTransaction](functions/createTransaction.md) - [debounceStrategy](functions/debounceStrategy.md) - [deepEquals](functions/deepEquals.md) +- [divide](functions/divide.md) - [eq](functions/eq.md) - [extractFieldPath](functions/extractFieldPath.md) - [extractSimpleComparisons](functions/extractSimpleComparisons.md) @@ -291,18 +299,21 @@ title: "@tanstack/db" - [findIndexForField](functions/findIndexForField.md) - [getActiveTransaction](functions/getActiveTransaction.md) - [getIndexDevModeConfig](functions/getIndexDevModeConfig.md) +- [getLiveQueryStatusFlags](functions/getLiveQueryStatusFlags.md) - [getQueryPatterns](functions/getQueryPatterns.md) - [gt](functions/gt.md) - [gte](functions/gte.md) - [hasVirtualProps](functions/hasVirtualProps.md) - [ilike](functions/ilike.md) - [inArray](functions/inArray.md) +- [isCollection](functions/isCollection.md) - [isDevModeEnabled](functions/isDevModeEnabled.md) - [isLimitSubset](functions/isLimitSubset.md) - [isNull](functions/isNull.md) - [isOffsetLimitSubset](functions/isOffsetLimitSubset.md) - [isOrderBySubset](functions/isOrderBySubset.md) - [isPredicateSubset](functions/isPredicateSubset.md) +- [isSingleResultCollection](functions/isSingleResultCollection.md) - [isUndefined](functions/isUndefined.md) - [isWhereSubset](functions/isWhereSubset.md) - [length](functions/length.md) @@ -313,9 +324,11 @@ title: "@tanstack/db" - [lower](functions/lower.md) - [lt](functions/lt.md) - [lte](functions/lte.md) +- [materialize](functions/materialize.md) - [max](functions/max.md) - [min](functions/min.md) - [minusWherePredicates](functions/minusWherePredicates.md) +- [multiply](functions/multiply.md) - [not](functions/not.md) - [optimizeExpressionWithIndexes](functions/optimizeExpressionWithIndexes.md) - [or](functions/or.md) @@ -324,6 +337,8 @@ title: "@tanstack/db" - [parseWhereExpression](functions/parseWhereExpression.md) - [queryOnce](functions/queryOnce.md) - [queueStrategy](functions/queueStrategy.md) +- [safeRandomUUID](functions/safeRandomUUID.md) +- [subtract](functions/subtract.md) - [sum](functions/sum.md) - [throttleStrategy](functions/throttleStrategy.md) - [toArray](functions/toArray.md) diff --git a/docs/reference/interfaces/Collection.md b/docs/reference/interfaces/Collection.md index dfc0e6aab8..ce124438c4 100644 --- a/docs/reference/interfaces/Collection.md +++ b/docs/reference/interfaces/Collection.md @@ -5,7 +5,7 @@ title: Collection # Interface: Collection\ -Defined in: [packages/db/src/collection/index.ts:54](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L54) +Defined in: [packages/db/src/collection/index.ts:55](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L55) Enhanced Collection interface that includes both data type T and utilities TUtils @@ -51,7 +51,7 @@ The type for insert operations (can be different from T for schemas with default _lifecycle: CollectionLifecycleManager; ``` -Defined in: [packages/db/src/collection/index.ts:289](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L289) +Defined in: [packages/db/src/collection/index.ts:290](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L290) #### Inherited from @@ -65,7 +65,7 @@ Defined in: [packages/db/src/collection/index.ts:289](https://github.com/TanStac _state: CollectionStateManager; ``` -Defined in: [packages/db/src/collection/index.ts:301](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L301) +Defined in: [packages/db/src/collection/index.ts:302](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L302) #### Inherited from @@ -79,7 +79,7 @@ Defined in: [packages/db/src/collection/index.ts:301](https://github.com/TanStac _sync: CollectionSyncManager; ``` -Defined in: [packages/db/src/collection/index.ts:290](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L290) +Defined in: [packages/db/src/collection/index.ts:291](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L291) #### Inherited from @@ -93,7 +93,7 @@ Defined in: [packages/db/src/collection/index.ts:290](https://github.com/TanStac config: CollectionConfig; ``` -Defined in: [packages/db/src/collection/index.ts:280](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L280) +Defined in: [packages/db/src/collection/index.ts:281](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L281) #### Inherited from @@ -107,7 +107,7 @@ Defined in: [packages/db/src/collection/index.ts:280](https://github.com/TanStac deferDataRefresh: Promise | null = null; ``` -Defined in: [packages/db/src/collection/index.ts:308](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L308) +Defined in: [packages/db/src/collection/index.ts:309](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L309) When set, collection consumers should defer processing incoming data refreshes until this promise resolves. This prevents stale data from @@ -125,7 +125,7 @@ overwriting optimistic state while pending writes are being applied. id: string; ``` -Defined in: [packages/db/src/collection/index.ts:279](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L279) +Defined in: [packages/db/src/collection/index.ts:280](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L280) #### Inherited from @@ -139,7 +139,7 @@ Defined in: [packages/db/src/collection/index.ts:279](https://github.com/TanStac readonly optional singleResult: true; ``` -Defined in: [packages/db/src/collection/index.ts:62](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L62) +Defined in: [packages/db/src/collection/index.ts:63](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L63) *** @@ -149,7 +149,7 @@ Defined in: [packages/db/src/collection/index.ts:62](https://github.com/TanStack readonly utils: TUtils; ``` -Defined in: [packages/db/src/collection/index.ts:61](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L61) +Defined in: [packages/db/src/collection/index.ts:62](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L62) #### Overrides @@ -165,7 +165,7 @@ Defined in: [packages/db/src/collection/index.ts:61](https://github.com/TanStack get compareOptions(): StringCollationConfig; ``` -Defined in: [packages/db/src/collection/index.ts:643](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L643) +Defined in: [packages/db/src/collection/index.ts:644](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L644) ##### Returns @@ -185,7 +185,7 @@ Defined in: [packages/db/src/collection/index.ts:643](https://github.com/TanStac get indexes(): Map>; ``` -Defined in: [packages/db/src/collection/index.ts:628](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L628) +Defined in: [packages/db/src/collection/index.ts:629](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L629) Get resolved indexes for query optimization @@ -207,7 +207,7 @@ Get resolved indexes for query optimization get isLoadingSubset(): boolean; ``` -Defined in: [packages/db/src/collection/index.ts:456](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L456) +Defined in: [packages/db/src/collection/index.ts:457](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L457) Check if the collection is currently loading more data @@ -231,7 +231,7 @@ true if the collection has pending load more operations, false otherwise get size(): number; ``` -Defined in: [packages/db/src/collection/index.ts:493](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L493) +Defined in: [packages/db/src/collection/index.ts:494](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L494) Get the current size of the collection (cached) @@ -253,7 +253,7 @@ Get the current size of the collection (cached) get state(): Map>; ``` -Defined in: [packages/db/src/collection/index.ts:820](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L820) +Defined in: [packages/db/src/collection/index.ts:821](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L821) Gets the current state of the collection as a Map @@ -293,7 +293,7 @@ Map containing all items in the collection, with keys as identifiers get status(): CollectionStatus; ``` -Defined in: [packages/db/src/collection/index.ts:411](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L411) +Defined in: [packages/db/src/collection/index.ts:412](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L412) Gets the current status of the collection @@ -315,7 +315,7 @@ Gets the current status of the collection get subscriberCount(): number; ``` -Defined in: [packages/db/src/collection/index.ts:418](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L418) +Defined in: [packages/db/src/collection/index.ts:419](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L419) Get the number of subscribers to the collection @@ -337,7 +337,7 @@ Get the number of subscribers to the collection get toArray(): WithVirtualProps[]; ``` -Defined in: [packages/db/src/collection/index.ts:849](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L849) +Defined in: [packages/db/src/collection/index.ts:850](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L850) Gets the current state of the collection as an Array @@ -359,7 +359,7 @@ An Array containing all items in the collection iterator: IterableIterator<[TKey, WithVirtualProps]>; ``` -Defined in: [packages/db/src/collection/index.ts:531](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L531) +Defined in: [packages/db/src/collection/index.ts:532](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L532) Get all entries (virtual derived state) @@ -379,7 +379,7 @@ Get all entries (virtual derived state) cleanup(): Promise; ``` -Defined in: [packages/db/src/collection/index.ts:988](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L988) +Defined in: [packages/db/src/collection/index.ts:989](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L989) Clean up the collection by stopping sync and clearing data This can be called manually or automatically by garbage collection @@ -400,7 +400,7 @@ This can be called manually or automatically by garbage collection createIndex(indexCallback, config): BaseIndex; ``` -Defined in: [packages/db/src/collection/index.ts:597](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L597) +Defined in: [packages/db/src/collection/index.ts:598](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L598) Creates an index on a collection for faster queries. Indexes significantly improve query performance by allowing constant time lookups @@ -460,7 +460,7 @@ currentStateAsChanges(options): | ChangeMessage, string | number>[]; ``` -Defined in: [packages/db/src/collection/index.ts:887](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L887) +Defined in: [packages/db/src/collection/index.ts:888](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L888) Returns the current state of the collection as an array of changes @@ -508,7 +508,7 @@ const activeChanges = collection.currentStateAsChanges({ delete(keys, config?): Transaction; ``` -Defined in: [packages/db/src/collection/index.ts:797](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L797) +Defined in: [packages/db/src/collection/index.ts:798](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L798) Deletes one or more items from the collection @@ -575,7 +575,7 @@ try { entries(): IterableIterator<[TKey, WithVirtualProps]>; ``` -Defined in: [packages/db/src/collection/index.ts:519](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L519) +Defined in: [packages/db/src/collection/index.ts:520](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L520) Get all entries (virtual derived state) @@ -595,7 +595,7 @@ Get all entries (virtual derived state) forEach(callbackfn): void; ``` -Defined in: [packages/db/src/collection/index.ts:540](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L540) +Defined in: [packages/db/src/collection/index.ts:541](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L541) Execute a callback for each entry in the collection @@ -623,7 +623,7 @@ get(key): | undefined; ``` -Defined in: [packages/db/src/collection/index.ts:479](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L479) +Defined in: [packages/db/src/collection/index.ts:480](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L480) Get the current value for a key (virtual derived state) @@ -650,7 +650,7 @@ Get the current value for a key (virtual derived state) getIndexMetadata(): CollectionIndexMetadata[]; ``` -Defined in: [packages/db/src/collection/index.ts:621](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L621) +Defined in: [packages/db/src/collection/index.ts:622](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L622) Returns a snapshot of current index metadata sorted by indexId. Persistence wrappers can use this to bootstrap index state if indexes were @@ -672,7 +672,7 @@ created before event listeners were attached. getKeyFromItem(item): TKey; ``` -Defined in: [packages/db/src/collection/index.ts:571](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L571) +Defined in: [packages/db/src/collection/index.ts:572](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L572) #### Parameters @@ -696,7 +696,7 @@ Defined in: [packages/db/src/collection/index.ts:571](https://github.com/TanStac has(key): boolean; ``` -Defined in: [packages/db/src/collection/index.ts:486](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L486) +Defined in: [packages/db/src/collection/index.ts:487](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L487) Check if a key exists in the collection (virtual derived state) @@ -722,7 +722,7 @@ Check if a key exists in the collection (virtual derived state) insert(data, config?): Transaction>; ``` -Defined in: [packages/db/src/collection/index.ts:684](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L684) +Defined in: [packages/db/src/collection/index.ts:685](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L685) Inserts one or more items into the collection @@ -796,7 +796,7 @@ try { isReady(): boolean; ``` -Defined in: [packages/db/src/collection/index.ts:448](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L448) +Defined in: [packages/db/src/collection/index.ts:449](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L449) Check if the collection is ready for use Returns true if the collection has been marked as ready by its sync implementation @@ -830,7 +830,7 @@ if (collection.isReady()) { keys(): IterableIterator; ``` -Defined in: [packages/db/src/collection/index.ts:500](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L500) +Defined in: [packages/db/src/collection/index.ts:501](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L501) Get all keys (virtual derived state) @@ -850,7 +850,7 @@ Get all keys (virtual derived state) map(callbackfn): U[]; ``` -Defined in: [packages/db/src/collection/index.ts:556](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L556) +Defined in: [packages/db/src/collection/index.ts:557](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L557) Create a new array with the results of calling a function for each entry in the collection @@ -882,7 +882,7 @@ Create a new array with the results of calling a function for each entry in the off(event, callback): void; ``` -Defined in: [packages/db/src/collection/index.ts:967](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L967) +Defined in: [packages/db/src/collection/index.ts:968](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L968) Unsubscribe from a collection event @@ -929,7 +929,7 @@ Unsubscribe from a collection event on(event, callback): () => void; ``` -Defined in: [packages/db/src/collection/index.ts:947](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L947) +Defined in: [packages/db/src/collection/index.ts:948](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L948) Subscribe to a collection event @@ -982,7 +982,7 @@ Subscribe to a collection event once(event, callback): () => void; ``` -Defined in: [packages/db/src/collection/index.ts:957](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L957) +Defined in: [packages/db/src/collection/index.ts:958](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L958) Subscribe to a collection event once @@ -1035,7 +1035,7 @@ Subscribe to a collection event once onFirstReady(callback): void; ``` -Defined in: [packages/db/src/collection/index.ts:432](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L432) +Defined in: [packages/db/src/collection/index.ts:433](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L433) Register a callback to be executed when the collection first becomes ready Useful for preloading collections @@ -1073,7 +1073,7 @@ collection.onFirstReady(() => { preload(): Promise; ``` -Defined in: [packages/db/src/collection/index.ts:472](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L472) +Defined in: [packages/db/src/collection/index.ts:473](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L473) Preload the collection data by starting sync if not already started Multiple concurrent calls will share the same promise @@ -1094,7 +1094,7 @@ Multiple concurrent calls will share the same promise removeIndex(indexOrId): boolean; ``` -Defined in: [packages/db/src/collection/index.ts:612](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L612) +Defined in: [packages/db/src/collection/index.ts:613](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L613) Removes an index created with createIndex. Returns true when an index existed and was removed. @@ -1125,7 +1125,7 @@ as invalid after removal. startSyncImmediate(): void; ``` -Defined in: [packages/db/src/collection/index.ts:464](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L464) +Defined in: [packages/db/src/collection/index.ts:465](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L465) Start sync immediately - internal method for compiled queries This bypasses lazy loading for special cases like live query results @@ -1146,7 +1146,7 @@ This bypasses lazy loading for special cases like live query results stateWhenReady(): Promise>>; ``` -Defined in: [packages/db/src/collection/index.ts:834](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L834) +Defined in: [packages/db/src/collection/index.ts:835](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L835) Gets the current state of the collection as a Map, but only resolves when data is available Waits for the first sync commit to complete before resolving @@ -1169,7 +1169,7 @@ Promise that resolves to a Map containing all items in the collection subscribeChanges(callback, options): CollectionSubscription; ``` -Defined in: [packages/db/src/collection/index.ts:935](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L935) +Defined in: [packages/db/src/collection/index.ts:936](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L936) Subscribe to changes in the collection @@ -1248,7 +1248,7 @@ const subscription = collection.subscribeChanges((changes) => { toArrayWhenReady(): Promise[]>; ``` -Defined in: [packages/db/src/collection/index.ts:859](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L859) +Defined in: [packages/db/src/collection/index.ts:860](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L860) Gets the current state of the collection as an Array, but only resolves when data is available Waits for the first sync commit to complete before resolving @@ -1273,7 +1273,7 @@ Promise that resolves to an Array containing all items in the collection update(key, callback): Transaction; ``` -Defined in: [packages/db/src/collection/index.ts:729](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L729) +Defined in: [packages/db/src/collection/index.ts:730](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L730) Updates one or more items in the collection using a callback function @@ -1348,7 +1348,7 @@ update( callback): Transaction; ``` -Defined in: [packages/db/src/collection/index.ts:735](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L735) +Defined in: [packages/db/src/collection/index.ts:736](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L736) Updates one or more items in the collection using a callback function @@ -1426,7 +1426,7 @@ try { update(id, callback): Transaction; ``` -Defined in: [packages/db/src/collection/index.ts:742](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L742) +Defined in: [packages/db/src/collection/index.ts:743](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L743) Updates one or more items in the collection using a callback function @@ -1501,7 +1501,7 @@ update( callback): Transaction; ``` -Defined in: [packages/db/src/collection/index.ts:748](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L748) +Defined in: [packages/db/src/collection/index.ts:749](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L749) Updates one or more items in the collection using a callback function @@ -1582,7 +1582,7 @@ validateData( key?): T; ``` -Defined in: [packages/db/src/collection/index.ts:635](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L635) +Defined in: [packages/db/src/collection/index.ts:636](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L636) Validates the data against the schema @@ -1616,7 +1616,7 @@ Validates the data against the schema values(): IterableIterator>; ``` -Defined in: [packages/db/src/collection/index.ts:507](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L507) +Defined in: [packages/db/src/collection/index.ts:508](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L508) Get all values (virtual derived state) @@ -1636,7 +1636,7 @@ Get all values (virtual derived state) waitFor(event, timeout?): Promise; ``` -Defined in: [packages/db/src/collection/index.ts:977](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L977) +Defined in: [packages/db/src/collection/index.ts:978](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L978) Wait for a collection event diff --git a/docs/reference/interfaces/CollectionLike.md b/docs/reference/interfaces/CollectionLike.md index cc5593c1d1..aa91a8b88b 100644 --- a/docs/reference/interfaces/CollectionLike.md +++ b/docs/reference/interfaces/CollectionLike.md @@ -32,7 +32,7 @@ for the change events system to work compareOptions: StringCollationConfig; ``` -Defined in: [packages/db/src/collection/index.ts:643](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L643) +Defined in: [packages/db/src/collection/index.ts:644](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L644) #### Inherited from @@ -46,7 +46,7 @@ Defined in: [packages/db/src/collection/index.ts:643](https://github.com/TanStac id: string; ``` -Defined in: [packages/db/src/collection/index.ts:279](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L279) +Defined in: [packages/db/src/collection/index.ts:280](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L280) #### Inherited from @@ -60,7 +60,7 @@ Defined in: [packages/db/src/collection/index.ts:279](https://github.com/TanStac indexes: Map>; ``` -Defined in: [packages/db/src/collection/index.ts:628](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L628) +Defined in: [packages/db/src/collection/index.ts:629](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L629) #### Inherited from @@ -76,7 +76,7 @@ Pick.indexes entries(): IterableIterator<[TKey, WithVirtualProps]>; ``` -Defined in: [packages/db/src/collection/index.ts:519](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L519) +Defined in: [packages/db/src/collection/index.ts:520](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L520) Get all entries (virtual derived state) @@ -100,7 +100,7 @@ get(key): | undefined; ``` -Defined in: [packages/db/src/collection/index.ts:479](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L479) +Defined in: [packages/db/src/collection/index.ts:480](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L480) Get the current value for a key (virtual derived state) @@ -129,7 +129,7 @@ Pick.get has(key): boolean; ``` -Defined in: [packages/db/src/collection/index.ts:486](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L486) +Defined in: [packages/db/src/collection/index.ts:487](https://github.com/TanStack/db/blob/main/packages/db/src/collection/index.ts#L487) Check if a key exists in the collection (virtual derived state) diff --git a/docs/reference/interfaces/Context.md b/docs/reference/interfaces/Context.md index 2caa57dab1..bf497cf24e 100644 --- a/docs/reference/interfaces/Context.md +++ b/docs/reference/interfaces/Context.md @@ -5,7 +5,7 @@ title: Context # Interface: Context -Defined in: [packages/db/src/query/builder/types.ts:37](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L37) +Defined in: [packages/db/src/query/builder/types.ts:43](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L43) Context - The central state container for query builder operations @@ -16,7 +16,8 @@ This interface tracks all the information needed to build and type-check queries - `schema`: Current available tables (expands with joins, contracts with subqueries) **Query State**: -- `fromSourceName`: Which table was used in `from()` - needed for optionality logic +- `fromSourceName`: Which table was used in `from()` or the first + `unionAll()` source - needed for optionality logic - `hasJoins`: Whether any joins have been added (affects result type inference) - `joinTypes`: Maps table aliases to their join types for optionality calculations @@ -36,7 +37,7 @@ The context evolves through the query builder chain: baseSchema: ContextSchema; ``` -Defined in: [packages/db/src/query/builder/types.ts:39](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L39) +Defined in: [packages/db/src/query/builder/types.ts:45](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L45) *** @@ -46,7 +47,17 @@ Defined in: [packages/db/src/query/builder/types.ts:39](https://github.com/TanSt fromSourceName: string; ``` -Defined in: [packages/db/src/query/builder/types.ts:43](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L43) +Defined in: [packages/db/src/query/builder/types.ts:51](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L51) + +*** + +### fromSourceNames? + +```ts +optional fromSourceNames: readonly string[]; +``` + +Defined in: [packages/db/src/query/builder/types.ts:53](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L53) *** @@ -56,7 +67,7 @@ Defined in: [packages/db/src/query/builder/types.ts:43](https://github.com/TanSt optional hasJoins: boolean; ``` -Defined in: [packages/db/src/query/builder/types.ts:45](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L45) +Defined in: [packages/db/src/query/builder/types.ts:57](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L57) *** @@ -66,7 +77,17 @@ Defined in: [packages/db/src/query/builder/types.ts:45](https://github.com/TanSt optional hasResult: true; ``` -Defined in: [packages/db/src/query/builder/types.ts:54](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L54) +Defined in: [packages/db/src/query/builder/types.ts:66](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L66) + +*** + +### hasUnionFrom? + +```ts +optional hasUnionFrom: true; +``` + +Defined in: [packages/db/src/query/builder/types.ts:55](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L55) *** @@ -76,7 +97,17 @@ Defined in: [packages/db/src/query/builder/types.ts:54](https://github.com/TanSt optional joinTypes: Record; ``` -Defined in: [packages/db/src/query/builder/types.ts:47](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L47) +Defined in: [packages/db/src/query/builder/types.ts:59](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L59) + +*** + +### refsSchema? + +```ts +optional refsSchema: ContextSchema; +``` + +Defined in: [packages/db/src/query/builder/types.ts:49](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L49) *** @@ -86,7 +117,7 @@ Defined in: [packages/db/src/query/builder/types.ts:47](https://github.com/TanSt optional result: any; ``` -Defined in: [packages/db/src/query/builder/types.ts:52](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L52) +Defined in: [packages/db/src/query/builder/types.ts:64](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L64) *** @@ -96,7 +127,7 @@ Defined in: [packages/db/src/query/builder/types.ts:52](https://github.com/TanSt schema: ContextSchema; ``` -Defined in: [packages/db/src/query/builder/types.ts:41](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L41) +Defined in: [packages/db/src/query/builder/types.ts:47](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L47) *** @@ -106,4 +137,4 @@ Defined in: [packages/db/src/query/builder/types.ts:41](https://github.com/TanSt optional singleResult: boolean; ``` -Defined in: [packages/db/src/query/builder/types.ts:56](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L56) +Defined in: [packages/db/src/query/builder/types.ts:68](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L68) diff --git a/docs/reference/interfaces/IndexInterface.md b/docs/reference/interfaces/IndexInterface.md index 43400d877c..a27709eae3 100644 --- a/docs/reference/interfaces/IndexInterface.md +++ b/docs/reference/interfaces/IndexInterface.md @@ -99,7 +99,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:40](https://github.com/TanSta getStats: () => IndexStats; ``` -Defined in: [packages/db/src/indexes/base-index.ts:75](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L75) +Defined in: [packages/db/src/indexes/base-index.ts:84](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L84) #### Returns @@ -157,7 +157,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:38](https://github.com/TanSta matchesCompareOptions: (compareOptions) => boolean; ``` -Defined in: [packages/db/src/indexes/base-index.ts:72](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L72) +Defined in: [packages/db/src/indexes/base-index.ts:81](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L81) #### Parameters @@ -177,7 +177,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:72](https://github.com/TanSta matchesDirection: (direction) => boolean; ``` -Defined in: [packages/db/src/indexes/base-index.ts:73](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L73) +Defined in: [packages/db/src/indexes/base-index.ts:82](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L82) #### Parameters @@ -197,7 +197,7 @@ Defined in: [packages/db/src/indexes/base-index.ts:73](https://github.com/TanSta matchesField: (fieldPath) => boolean; ``` -Defined in: [packages/db/src/indexes/base-index.ts:71](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L71) +Defined in: [packages/db/src/indexes/base-index.ts:80](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L80) #### Parameters @@ -491,6 +491,28 @@ Defined in: [packages/db/src/indexes/base-index.ts:64](https://github.com/TanSta *** +### supportsRangeOptimization + +#### Get Signature + +```ts +get supportsRangeOptimization(): boolean; +``` + +Defined in: [packages/db/src/indexes/base-index.ts:78](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L78) + +Whether range lookups (gt/gte/lt/lte) on this index can be trusted to +return every matching key. Range traversal relies on the index ordering, so +it is unsafe when the index uses a custom comparator, whose order may not +match the WHERE evaluator's relational operators. Callers must fall back to +a full scan when this is `false`. + +##### Returns + +`boolean` + +*** + ### valueMapData #### Get Signature diff --git a/docs/reference/interfaces/LiveQueryStatusFlags.md b/docs/reference/interfaces/LiveQueryStatusFlags.md new file mode 100644 index 0000000000..e687c972b0 --- /dev/null +++ b/docs/reference/interfaces/LiveQueryStatusFlags.md @@ -0,0 +1,60 @@ +--- +id: LiveQueryStatusFlags +title: LiveQueryStatusFlags +--- + +# Interface: LiveQueryStatusFlags + +Defined in: [packages/db/src/live-query-adapter.ts:45](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-adapter.ts#L45) + +The derived boolean status flags every adapter exposes for a query. + +## Properties + +### isCleanedUp + +```ts +isCleanedUp: boolean; +``` + +Defined in: [packages/db/src/live-query-adapter.ts:50](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-adapter.ts#L50) + +*** + +### isError + +```ts +isError: boolean; +``` + +Defined in: [packages/db/src/live-query-adapter.ts:49](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-adapter.ts#L49) + +*** + +### isIdle + +```ts +isIdle: boolean; +``` + +Defined in: [packages/db/src/live-query-adapter.ts:48](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-adapter.ts#L48) + +*** + +### isLoading + +```ts +isLoading: boolean; +``` + +Defined in: [packages/db/src/live-query-adapter.ts:46](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-adapter.ts#L46) + +*** + +### isReady + +```ts +isReady: boolean; +``` + +Defined in: [packages/db/src/live-query-adapter.ts:47](https://github.com/TanStack/db/blob/main/packages/db/src/live-query-adapter.ts#L47) diff --git a/docs/reference/interfaces/LocalOnlyCollectionConfig.md b/docs/reference/interfaces/LocalOnlyCollectionConfig.md index e948a015e3..1cfeb8e17e 100644 --- a/docs/reference/interfaces/LocalOnlyCollectionConfig.md +++ b/docs/reference/interfaces/LocalOnlyCollectionConfig.md @@ -5,7 +5,7 @@ title: LocalOnlyCollectionConfig # Interface: LocalOnlyCollectionConfig\ -Defined in: [packages/db/src/local-only.ts:22](https://github.com/TanStack/db/blob/main/packages/db/src/local-only.ts#L22) +Defined in: [packages/db/src/local-only.ts:23](https://github.com/TanStack/db/blob/main/packages/db/src/local-only.ts#L23) Configuration interface for Local-only collection options @@ -221,7 +221,7 @@ Defined in: [packages/db/src/types.ts:534](https://github.com/TanStack/db/blob/m optional initialData: T[]; ``` -Defined in: [packages/db/src/local-only.ts:34](https://github.com/TanStack/db/blob/main/packages/db/src/local-only.ts#L34) +Defined in: [packages/db/src/local-only.ts:35](https://github.com/TanStack/db/blob/main/packages/db/src/local-only.ts#L35) Optional initial data to populate the collection with on creation This data will be applied during the initial sync process diff --git a/docs/reference/interfaces/LocalOnlyCollectionUtils.md b/docs/reference/interfaces/LocalOnlyCollectionUtils.md index c86fbc07b0..8065738560 100644 --- a/docs/reference/interfaces/LocalOnlyCollectionUtils.md +++ b/docs/reference/interfaces/LocalOnlyCollectionUtils.md @@ -5,7 +5,7 @@ title: LocalOnlyCollectionUtils # Interface: LocalOnlyCollectionUtils -Defined in: [packages/db/src/local-only.ts:40](https://github.com/TanStack/db/blob/main/packages/db/src/local-only.ts#L40) +Defined in: [packages/db/src/local-only.ts:41](https://github.com/TanStack/db/blob/main/packages/db/src/local-only.ts#L41) Local-only collection utilities type @@ -27,7 +27,7 @@ Local-only collection utilities type acceptMutations: (transaction) => void; ``` -Defined in: [packages/db/src/local-only.ts:58](https://github.com/TanStack/db/blob/main/packages/db/src/local-only.ts#L58) +Defined in: [packages/db/src/local-only.ts:59](https://github.com/TanStack/db/blob/main/packages/db/src/local-only.ts#L59) Accepts mutations from a transaction that belong to this collection and persists them. This should be called in your transaction's mutationFn to persist local-only data. diff --git a/docs/reference/interfaces/LocalStorageCollectionConfig.md b/docs/reference/interfaces/LocalStorageCollectionConfig.md index f6884ecefb..46dd151a73 100644 --- a/docs/reference/interfaces/LocalStorageCollectionConfig.md +++ b/docs/reference/interfaces/LocalStorageCollectionConfig.md @@ -5,7 +5,7 @@ title: LocalStorageCollectionConfig # Interface: LocalStorageCollectionConfig\ -Defined in: [packages/db/src/local-storage.ts:58](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L58) +Defined in: [packages/db/src/local-storage.ts:59](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L59) Configuration interface for localStorage collection options @@ -433,7 +433,7 @@ onUpdate: async ({ transaction, collection }) => { optional parser: Parser; ``` -Defined in: [packages/db/src/local-storage.ts:84](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L84) +Defined in: [packages/db/src/local-storage.ts:85](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L85) Parser to use for serializing and deserializing data to and from storage Defaults to JSON @@ -487,7 +487,7 @@ false optional storage: StorageApi; ``` -Defined in: [packages/db/src/local-storage.ts:72](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L72) +Defined in: [packages/db/src/local-storage.ts:73](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L73) Storage API to use (defaults to window.localStorage) Can be any object that implements the Storage interface (e.g., sessionStorage) @@ -500,7 +500,7 @@ Can be any object that implements the Storage interface (e.g., sessionStorage) optional storageEventApi: StorageEventApi; ``` -Defined in: [packages/db/src/local-storage.ts:78](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L78) +Defined in: [packages/db/src/local-storage.ts:79](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L79) Storage event API to use for cross-tab synchronization (defaults to window) Can be any object that implements addEventListener/removeEventListener for storage events @@ -513,7 +513,7 @@ Can be any object that implements addEventListener/removeEventListener for stora storageKey: string; ``` -Defined in: [packages/db/src/local-storage.ts:66](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L66) +Defined in: [packages/db/src/local-storage.ts:67](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L67) The key to use for storing the collection data in localStorage/sessionStorage diff --git a/docs/reference/interfaces/LocalStorageCollectionUtils.md b/docs/reference/interfaces/LocalStorageCollectionUtils.md index 9472f18f60..92a08f99fa 100644 --- a/docs/reference/interfaces/LocalStorageCollectionUtils.md +++ b/docs/reference/interfaces/LocalStorageCollectionUtils.md @@ -5,7 +5,7 @@ title: LocalStorageCollectionUtils # Interface: LocalStorageCollectionUtils -Defined in: [packages/db/src/local-storage.ts:100](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L100) +Defined in: [packages/db/src/local-storage.ts:101](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L101) LocalStorage collection utilities type @@ -27,7 +27,7 @@ LocalStorage collection utilities type acceptMutations: (transaction) => void; ``` -Defined in: [packages/db/src/local-storage.ts:120](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L120) +Defined in: [packages/db/src/local-storage.ts:121](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L121) Accepts mutations from a transaction that belong to this collection and persists them to localStorage. This should be called in your transaction's mutationFn to persist local-storage data. @@ -69,7 +69,7 @@ const tx = createTransaction({ clearStorage: ClearStorageFn; ``` -Defined in: [packages/db/src/local-storage.ts:101](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L101) +Defined in: [packages/db/src/local-storage.ts:102](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L102) *** @@ -79,4 +79,4 @@ Defined in: [packages/db/src/local-storage.ts:101](https://github.com/TanStack/d getStorageSize: GetStorageSizeFn; ``` -Defined in: [packages/db/src/local-storage.ts:102](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L102) +Defined in: [packages/db/src/local-storage.ts:103](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L103) diff --git a/docs/reference/interfaces/ParseWhereOptions.md b/docs/reference/interfaces/ParseWhereOptions.md index 8a90e90142..5fee3544ef 100644 --- a/docs/reference/interfaces/ParseWhereOptions.md +++ b/docs/reference/interfaces/ParseWhereOptions.md @@ -87,6 +87,22 @@ optional avg: (...args) => T; `T` +##### caseWhen()? + +```ts +optional caseWhen: (...args) => T; +``` + +###### Parameters + +###### args + +...`any`[] + +###### Returns + +`T` + ##### coalesce()? ```ts @@ -135,6 +151,22 @@ optional count: (...args) => T; `T` +##### divide()? + +```ts +optional divide: (...args) => T; +``` + +###### Parameters + +###### args + +...`any`[] + +###### Returns + +`T` + ##### eq()? ```ts @@ -359,6 +391,22 @@ optional min: (...args) => T; `T` +##### multiply()? + +```ts +optional multiply: (...args) => T; +``` + +###### Parameters + +###### args + +...`any`[] + +###### Returns + +`T` + ##### not()? ```ts @@ -391,6 +439,22 @@ optional or: (...args) => T; `T` +##### subtract()? + +```ts +optional subtract: (...args) => T; +``` + +###### Parameters + +###### args + +...`any`[] + +###### Returns + +`T` + ##### sum()? ```ts diff --git a/docs/reference/interfaces/Parser.md b/docs/reference/interfaces/Parser.md index d1eafc24c7..1db679a980 100644 --- a/docs/reference/interfaces/Parser.md +++ b/docs/reference/interfaces/Parser.md @@ -5,7 +5,7 @@ title: Parser # Interface: Parser -Defined in: [packages/db/src/local-storage.ts:47](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L47) +Defined in: [packages/db/src/local-storage.ts:48](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L48) ## Properties @@ -15,7 +15,7 @@ Defined in: [packages/db/src/local-storage.ts:47](https://github.com/TanStack/db parse: (data) => unknown; ``` -Defined in: [packages/db/src/local-storage.ts:48](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L48) +Defined in: [packages/db/src/local-storage.ts:49](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L49) #### Parameters @@ -35,7 +35,7 @@ Defined in: [packages/db/src/local-storage.ts:48](https://github.com/TanStack/db stringify: (data) => string; ``` -Defined in: [packages/db/src/local-storage.ts:49](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L49) +Defined in: [packages/db/src/local-storage.ts:50](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L50) #### Parameters diff --git a/docs/reference/interfaces/Transaction.md b/docs/reference/interfaces/Transaction.md index cffa506996..6621aa3c91 100644 --- a/docs/reference/interfaces/Transaction.md +++ b/docs/reference/interfaces/Transaction.md @@ -5,7 +5,7 @@ title: Transaction # Interface: Transaction\ -Defined in: [packages/db/src/transactions.ts:208](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L208) +Defined in: [packages/db/src/transactions.ts:209](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L209) ## Type Parameters @@ -21,7 +21,7 @@ Defined in: [packages/db/src/transactions.ts:208](https://github.com/TanStack/db autoCommit: boolean; ``` -Defined in: [packages/db/src/transactions.ts:214](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L214) +Defined in: [packages/db/src/transactions.ts:227](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L227) *** @@ -31,7 +31,7 @@ Defined in: [packages/db/src/transactions.ts:214](https://github.com/TanStack/db createdAt: Date; ``` -Defined in: [packages/db/src/transactions.ts:215](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L215) +Defined in: [packages/db/src/transactions.ts:228](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L228) *** @@ -41,7 +41,7 @@ Defined in: [packages/db/src/transactions.ts:215](https://github.com/TanStack/db optional error: object; ``` -Defined in: [packages/db/src/transactions.ts:218](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L218) +Defined in: [packages/db/src/transactions.ts:231](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L231) #### error @@ -63,7 +63,7 @@ message: string; id: string; ``` -Defined in: [packages/db/src/transactions.ts:209](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L209) +Defined in: [packages/db/src/transactions.ts:210](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L210) *** @@ -73,7 +73,18 @@ Defined in: [packages/db/src/transactions.ts:209](https://github.com/TanStack/db isPersisted: Deferred>; ``` -Defined in: [packages/db/src/transactions.ts:213](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L213) +Defined in: [packages/db/src/transactions.ts:226](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L226) + +Deferred that settles when this transaction settles. + +Await `isPersisted.promise`, not `isPersisted` itself. The promise resolves +when the transaction completes successfully and rejects if the transaction +fails or is rolled back. + +For non-empty commits, the mutation function is the normal settlement +boundary. This does not inherently prove that a backend has uploaded, +confirmed, or read back the write unless the mutation function waits for +that backend observation before returning. *** @@ -83,7 +94,7 @@ Defined in: [packages/db/src/transactions.ts:213](https://github.com/TanStack/db metadata: Record; ``` -Defined in: [packages/db/src/transactions.ts:217](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L217) +Defined in: [packages/db/src/transactions.ts:230](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L230) *** @@ -93,7 +104,7 @@ Defined in: [packages/db/src/transactions.ts:217](https://github.com/TanStack/db mutationFn: MutationFn; ``` -Defined in: [packages/db/src/transactions.ts:211](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L211) +Defined in: [packages/db/src/transactions.ts:212](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L212) *** @@ -103,7 +114,7 @@ Defined in: [packages/db/src/transactions.ts:211](https://github.com/TanStack/db mutations: PendingMutation>[]; ``` -Defined in: [packages/db/src/transactions.ts:212](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L212) +Defined in: [packages/db/src/transactions.ts:213](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L213) *** @@ -113,7 +124,7 @@ Defined in: [packages/db/src/transactions.ts:212](https://github.com/TanStack/db sequenceNumber: number; ``` -Defined in: [packages/db/src/transactions.ts:216](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L216) +Defined in: [packages/db/src/transactions.ts:229](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L229) *** @@ -123,7 +134,7 @@ Defined in: [packages/db/src/transactions.ts:216](https://github.com/TanStack/db state: TransactionState; ``` -Defined in: [packages/db/src/transactions.ts:210](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L210) +Defined in: [packages/db/src/transactions.ts:211](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L211) ## Methods @@ -133,7 +144,7 @@ Defined in: [packages/db/src/transactions.ts:210](https://github.com/TanStack/db applyMutations(mutations): void; ``` -Defined in: [packages/db/src/transactions.ts:327](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L327) +Defined in: [packages/db/src/transactions.ts:348](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L348) Apply new mutations to this transaction, intelligently merging with existing mutations @@ -169,7 +180,7 @@ Array of new mutations to apply commit(): Promise>; ``` -Defined in: [packages/db/src/transactions.ts:472](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L472) +Defined in: [packages/db/src/transactions.ts:505](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L505) Commit the transaction and execute the mutation function @@ -228,7 +239,7 @@ console.log(tx.state) // "completed" or "failed" compareCreatedAt(other): number; ``` -Defined in: [packages/db/src/transactions.ts:526](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L526) +Defined in: [packages/db/src/transactions.ts:559](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L559) Compare two transactions by their createdAt time and sequence number in order to sort them in the order they were created. @@ -255,7 +266,7 @@ The other transaction to compare to mutate(callback): Transaction; ``` -Defined in: [packages/db/src/transactions.ts:287](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L287) +Defined in: [packages/db/src/transactions.ts:308](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L308) Execute collection operations within this transaction @@ -265,9 +276,12 @@ Execute collection operations within this transaction () => `void` -Function containing collection operations to group together. If the -callback returns a Promise, the transaction context will remain active until the promise -settles, allowing optimistic writes after `await` boundaries. +Synchronous function containing collection operations to group together. +The transaction context is active only for the synchronous duration of this callback. +Async work should happen in `mutationFn`; collection operations after `await` boundaries +inside this callback will not be part of this transaction. For manual transactions, call +`mutate` multiple times before committing to add more synchronous operations to the same +transaction. #### Returns @@ -311,6 +325,11 @@ tx.mutate(() => { collection.insert({ id: "1", text: "Item" }) }) +// Add more synchronous mutations to the same transaction +tx.mutate(() => { + collection.update("1", draft => { draft.text = "Updated item" }) +}) + // Commit later when ready await tx.commit() ``` @@ -323,7 +342,7 @@ await tx.commit() rollback(config?): Transaction; ``` -Defined in: [packages/db/src/transactions.ts:389](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L389) +Defined in: [packages/db/src/transactions.ts:422](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L422) Rollback the transaction and any conflicting transactions @@ -390,7 +409,7 @@ try { setState(newState): void; ``` -Defined in: [packages/db/src/transactions.ts:238](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L238) +Defined in: [packages/db/src/transactions.ts:251](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L251) #### Parameters @@ -410,7 +429,7 @@ Defined in: [packages/db/src/transactions.ts:238](https://github.com/TanStack/db touchCollection(): void; ``` -Defined in: [packages/db/src/transactions.ts:417](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L417) +Defined in: [packages/db/src/transactions.ts:450](https://github.com/TanStack/db/blob/main/packages/db/src/transactions.ts#L450) #### Returns diff --git a/docs/reference/interfaces/VirtualRowProps.md b/docs/reference/interfaces/VirtualRowProps.md index c4719d188e..63d2ca95c5 100644 --- a/docs/reference/interfaces/VirtualRowProps.md +++ b/docs/reference/interfaces/VirtualRowProps.md @@ -21,7 +21,7 @@ These properties are: // Accessing virtual properties on a row const user = collection.get('user-1') if (user.$synced) { - console.log('Confirmed by backend') + console.log('No pending local optimistic writes for this row') } if (user.$origin === 'local') { console.log('Created/modified locally') @@ -30,7 +30,7 @@ if (user.$origin === 'local') { ```typescript // Using virtual properties in queries -const confirmedOrders = createLiveQueryCollection({ +const ordersWithoutLocalWrites = createLiveQueryCollection({ query: (q) => q .from({ order: orders }) .where(({ order }) => eq(order.$synced, true)) @@ -53,7 +53,7 @@ The type of the row's key (string or number) readonly $collectionId: string; ``` -Defined in: [packages/db/src/virtual-props.ts:96](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L96) +Defined in: [packages/db/src/virtual-props.ts:101](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L101) The ID of the source collection this row originated from. @@ -68,7 +68,7 @@ For live query collections, this is the ID of the upstream collection. readonly $key: TKey; ``` -Defined in: [packages/db/src/virtual-props.ts:88](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L88) +Defined in: [packages/db/src/virtual-props.ts:93](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L93) The row's key (primary identifier). @@ -83,7 +83,7 @@ Useful when you need the key in projections or computations. readonly $origin: VirtualOrigin; ``` -Defined in: [packages/db/src/virtual-props.ts:80](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L80) +Defined in: [packages/db/src/virtual-props.ts:85](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L85) Origin of the last confirmed change to this row, from the current client's perspective. @@ -101,12 +101,17 @@ For live query collections, this is passed through from the source collection. readonly $synced: boolean; ``` -Defined in: [packages/db/src/virtual-props.ts:69](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L69) +Defined in: [packages/db/src/virtual-props.ts:74](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L74) -Whether this row reflects confirmed state from the backend. +Whether this row currently has no pending local optimistic writes. -- `true`: Row is confirmed by the backend (no pending optimistic mutations) -- `false`: Row has pending optimistic mutations that haven't been confirmed +- `true`: No pending local optimistic mutation currently affects this row +- `false`: One or more pending local optimistic mutations currently affect this row + +This is local mutation status. It does not prove that a backend has uploaded, +confirmed, or read back the row. If you need backend-confirmed status, keep +your mutation function pending until that backend observation has happened, +or expose adapter-specific status. For local-only collections (no sync), this is always `true`. For live query collections, this is passed through from the source collection. diff --git a/docs/reference/query-db-collection/functions/queryCollectionOptions.md b/docs/reference/query-db-collection/functions/queryCollectionOptions.md index 360652b593..5da712517b 100644 --- a/docs/reference/query-db-collection/functions/queryCollectionOptions.md +++ b/docs/reference/query-db-collection/functions/queryCollectionOptions.md @@ -11,7 +11,7 @@ title: queryCollectionOptions function queryCollectionOptions(config): CollectionConfig, TKey, T, QueryCollectionUtils, TKey, InferSchemaInput, TError>> & object; ``` -Defined in: [packages/query-db-collection/src/query.ts:431](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L431) +Defined in: [packages/query-db-collection/src/query.ts:510](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L510) Creates query collection options for use with a standard Collection. This integrates TanStack Query with TanStack DB for automatic synchronization. @@ -151,7 +151,7 @@ const todosCollection = createCollection( function queryCollectionOptions(config): CollectionConfig> & object; ``` -Defined in: [packages/query-db-collection/src/query.ts:466](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L466) +Defined in: [packages/query-db-collection/src/query.ts:545](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L545) Creates query collection options for use with a standard Collection. This integrates TanStack Query with TanStack DB for automatic synchronization. @@ -291,7 +291,7 @@ const todosCollection = createCollection( function queryCollectionOptions(config): CollectionConfig, TKey, T, QueryCollectionUtils, TKey, InferSchemaInput, TError>> & object; ``` -Defined in: [packages/query-db-collection/src/query.ts:499](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L499) +Defined in: [packages/query-db-collection/src/query.ts:578](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L578) Creates query collection options for use with a standard Collection. This integrates TanStack Query with TanStack DB for automatic synchronization. @@ -423,7 +423,7 @@ const todosCollection = createCollection( function queryCollectionOptions(config): CollectionConfig> & object; ``` -Defined in: [packages/query-db-collection/src/query.ts:533](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L533) +Defined in: [packages/query-db-collection/src/query.ts:612](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L612) Creates query collection options for use with a standard Collection. This integrates TanStack Query with TanStack DB for automatic synchronization. diff --git a/docs/reference/query-db-collection/interfaces/QueryCollectionConfig.md b/docs/reference/query-db-collection/interfaces/QueryCollectionConfig.md index a53cae230d..bbe7bf8f69 100644 --- a/docs/reference/query-db-collection/interfaces/QueryCollectionConfig.md +++ b/docs/reference/query-db-collection/interfaces/QueryCollectionConfig.md @@ -5,7 +5,7 @@ title: QueryCollectionConfig # Interface: QueryCollectionConfig\ -Defined in: [packages/query-db-collection/src/query.ts:61](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L61) +Defined in: [packages/query-db-collection/src/query.ts:95](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L95) Configuration options for creating a Query Collection @@ -63,19 +63,38 @@ The schema type for validation optional enabled: Enabled; ``` -Defined in: [packages/query-db-collection/src/query.ts:87](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L87) +Defined in: [packages/query-db-collection/src/query.ts:124](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L124) Whether the query should automatically run (default: true) *** +### gcTime? + +```ts +optional gcTime: number; +``` + +Defined in: [packages/query-db-collection/src/query.ts:159](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L159) + +Time in milliseconds after which the collection will be garbage collected +when it has no active subscribers. Defaults to 5 minutes (300000ms). + +#### Overrides + +```ts +BaseCollectionConfig.gcTime +``` + +*** + ### meta? ```ts optional meta: Record; ``` -Defined in: [packages/query-db-collection/src/query.ts:144](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L144) +Defined in: [packages/query-db-collection/src/query.ts:216](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L216) Metadata to pass to the query. Available in queryFn via context.meta @@ -101,13 +120,23 @@ meta: { *** +### networkMode? + +```ts +optional networkMode: NetworkMode; +``` + +Defined in: [packages/query-db-collection/src/query.ts:187](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L187) + +*** + ### persistedGcTime? ```ts optional persistedGcTime: number; ``` -Defined in: [packages/query-db-collection/src/query.ts:122](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L122) +Defined in: [packages/query-db-collection/src/query.ts:194](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L194) *** @@ -117,7 +146,7 @@ Defined in: [packages/query-db-collection/src/query.ts:122](https://github.com/T queryClient: QueryClient; ``` -Defined in: [packages/query-db-collection/src/query.ts:83](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L83) +Defined in: [packages/query-db-collection/src/query.ts:120](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L120) The TanStack Query client instance @@ -129,7 +158,7 @@ The TanStack Query client instance queryFn: TQueryFn extends (context) => any[] | Promise ? (context) => T[] | Promise : TQueryFn; ``` -Defined in: [packages/query-db-collection/src/query.ts:75](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L75) +Defined in: [packages/query-db-collection/src/query.ts:109](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L109) Function that fetches data from the server. Must return the complete collection state @@ -141,7 +170,7 @@ Function that fetches data from the server. Must return the complete collection queryKey: TQueryKey | TQueryKeyBuilder; ``` -Defined in: [packages/query-db-collection/src/query.ts:73](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L73) +Defined in: [packages/query-db-collection/src/query.ts:107](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L107) The query key used by TanStack Query to identify this query @@ -153,7 +182,37 @@ The query key used by TanStack Query to identify this query optional refetchInterval: number | false | (query) => number | false | undefined; ``` -Defined in: [packages/query-db-collection/src/query.ts:94](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L94) +Defined in: [packages/query-db-collection/src/query.ts:131](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L131) + +*** + +### refetchOnMount? + +```ts +optional refetchOnMount: boolean | "always" | (query) => boolean | "always"; +``` + +Defined in: [packages/query-db-collection/src/query.ts:180](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L180) + +*** + +### refetchOnReconnect? + +```ts +optional refetchOnReconnect: boolean | "always" | (query) => boolean | "always"; +``` + +Defined in: [packages/query-db-collection/src/query.ts:173](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L173) + +*** + +### refetchOnWindowFocus? + +```ts +optional refetchOnWindowFocus: boolean | "always" | (query) => boolean | "always"; +``` + +Defined in: [packages/query-db-collection/src/query.ts:166](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L166) *** @@ -163,7 +222,7 @@ Defined in: [packages/query-db-collection/src/query.ts:94](https://github.com/Ta optional retry: RetryValue; ``` -Defined in: [packages/query-db-collection/src/query.ts:101](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L101) +Defined in: [packages/query-db-collection/src/query.ts:138](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L138) *** @@ -173,7 +232,7 @@ Defined in: [packages/query-db-collection/src/query.ts:101](https://github.com/T optional retryDelay: RetryDelayValue; ``` -Defined in: [packages/query-db-collection/src/query.ts:108](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L108) +Defined in: [packages/query-db-collection/src/query.ts:145](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L145) *** @@ -183,7 +242,10 @@ Defined in: [packages/query-db-collection/src/query.ts:108](https://github.com/T optional select: (data) => T[]; ``` -Defined in: [packages/query-db-collection/src/query.ts:81](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L81) +Defined in: [packages/query-db-collection/src/query.ts:118](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L118) + +Extracts the row array TanStack DB materializes from the Query response. +The Query cache keeps the original response shape. #### Parameters @@ -203,4 +265,4 @@ Defined in: [packages/query-db-collection/src/query.ts:81](https://github.com/Ta optional staleTime: StaleTimeFunction; ``` -Defined in: [packages/query-db-collection/src/query.ts:115](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L115) +Defined in: [packages/query-db-collection/src/query.ts:152](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L152) diff --git a/docs/reference/query-db-collection/interfaces/QueryCollectionUtils.md b/docs/reference/query-db-collection/interfaces/QueryCollectionUtils.md index d30be795f5..b2ab5a7c36 100644 --- a/docs/reference/query-db-collection/interfaces/QueryCollectionUtils.md +++ b/docs/reference/query-db-collection/interfaces/QueryCollectionUtils.md @@ -5,7 +5,7 @@ title: QueryCollectionUtils # Interface: QueryCollectionUtils\ -Defined in: [packages/query-db-collection/src/query.ts:163](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L163) +Defined in: [packages/query-db-collection/src/query.ts:235](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L235) Utility methods available on Query Collections for direct writes and manual operations. Direct writes bypass the normal query/mutation flow and write directly to the synced data store. @@ -54,7 +54,7 @@ The type of errors that can occur during queries clearError: () => Promise; ``` -Defined in: [packages/query-db-collection/src/query.ts:208](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L208) +Defined in: [packages/query-db-collection/src/query.ts:280](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L280) Clear the error state and trigger a refetch of the query @@ -76,7 +76,7 @@ Error if the refetch fails dataUpdatedAt: number; ``` -Defined in: [packages/query-db-collection/src/query.ts:199](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L199) +Defined in: [packages/query-db-collection/src/query.ts:271](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L271) Get timestamp of last successful data update (in milliseconds) @@ -88,7 +88,7 @@ Get timestamp of last successful data update (in milliseconds) errorCount: number; ``` -Defined in: [packages/query-db-collection/src/query.ts:191](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L191) +Defined in: [packages/query-db-collection/src/query.ts:263](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L263) Get the number of consecutive sync failures. Incremented only when query fails completely (not per retry attempt); reset on success. @@ -101,7 +101,7 @@ Incremented only when query fails completely (not per retry attempt); reset on s fetchStatus: "idle" | "fetching" | "paused"; ``` -Defined in: [packages/query-db-collection/src/query.ts:201](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L201) +Defined in: [packages/query-db-collection/src/query.ts:273](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L273) Get current fetch status @@ -113,7 +113,7 @@ Get current fetch status isError: boolean; ``` -Defined in: [packages/query-db-collection/src/query.ts:186](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L186) +Defined in: [packages/query-db-collection/src/query.ts:258](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L258) Check if the collection is in an error state @@ -125,7 +125,7 @@ Check if the collection is in an error state isFetching: boolean; ``` -Defined in: [packages/query-db-collection/src/query.ts:193](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L193) +Defined in: [packages/query-db-collection/src/query.ts:265](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L265) Check if query is currently fetching (initial or background) @@ -137,7 +137,7 @@ Check if query is currently fetching (initial or background) isLoading: boolean; ``` -Defined in: [packages/query-db-collection/src/query.ts:197](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L197) +Defined in: [packages/query-db-collection/src/query.ts:269](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L269) Check if query is loading for the first time (no data yet) @@ -149,7 +149,7 @@ Check if query is loading for the first time (no data yet) isRefetching: boolean; ``` -Defined in: [packages/query-db-collection/src/query.ts:195](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L195) +Defined in: [packages/query-db-collection/src/query.ts:267](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L267) Check if query is refetching in background (not initial fetch) @@ -161,7 +161,7 @@ Check if query is refetching in background (not initial fetch) lastError: TError | undefined; ``` -Defined in: [packages/query-db-collection/src/query.ts:184](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L184) +Defined in: [packages/query-db-collection/src/query.ts:256](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L256) Get the last error encountered by the query (if any); reset on success @@ -173,7 +173,7 @@ Get the last error encountered by the query (if any); reset on success refetch: RefetchFn; ``` -Defined in: [packages/query-db-collection/src/query.ts:170](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L170) +Defined in: [packages/query-db-collection/src/query.ts:242](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L242) Manually trigger a refetch of the query @@ -185,7 +185,7 @@ Manually trigger a refetch of the query writeBatch: (callback) => void; ``` -Defined in: [packages/query-db-collection/src/query.ts:180](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L180) +Defined in: [packages/query-db-collection/src/query.ts:252](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L252) Execute multiple write operations as a single atomic batch to the synced data store @@ -207,7 +207,7 @@ Execute multiple write operations as a single atomic batch to the synced data st writeDelete: (keys) => void; ``` -Defined in: [packages/query-db-collection/src/query.ts:176](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L176) +Defined in: [packages/query-db-collection/src/query.ts:248](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L248) Delete one or more items directly from the synced data store without triggering a query refetch or optimistic update @@ -229,7 +229,7 @@ Delete one or more items directly from the synced data store without triggering writeInsert: (data) => void; ``` -Defined in: [packages/query-db-collection/src/query.ts:172](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L172) +Defined in: [packages/query-db-collection/src/query.ts:244](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L244) Insert one or more items directly into the synced data store without triggering a query refetch or optimistic update @@ -251,7 +251,7 @@ Insert one or more items directly into the synced data store without triggering writeUpdate: (updates) => void; ``` -Defined in: [packages/query-db-collection/src/query.ts:174](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L174) +Defined in: [packages/query-db-collection/src/query.ts:246](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L246) Update one or more items directly in the synced data store without triggering a query refetch or optimistic update @@ -273,7 +273,7 @@ Update one or more items directly in the synced data store without triggering a writeUpsert: (data) => void; ``` -Defined in: [packages/query-db-collection/src/query.ts:178](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L178) +Defined in: [packages/query-db-collection/src/query.ts:250](https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/query.ts#L250) Insert or update one or more items directly in the synced data store without triggering a query refetch or optimistic update diff --git a/docs/reference/type-aliases/ApplyJoinOptionalityToMergedSchema.md b/docs/reference/type-aliases/ApplyJoinOptionalityToMergedSchema.md index de18e0b6d1..5ceb7cea55 100644 --- a/docs/reference/type-aliases/ApplyJoinOptionalityToMergedSchema.md +++ b/docs/reference/type-aliases/ApplyJoinOptionalityToMergedSchema.md @@ -3,13 +3,13 @@ id: ApplyJoinOptionalityToMergedSchema title: ApplyJoinOptionalityToMergedSchema --- -# Type Alias: ApplyJoinOptionalityToMergedSchema\ +# Type Alias: ApplyJoinOptionalityToMergedSchema\ ```ts -type ApplyJoinOptionalityToMergedSchema = { [K in keyof TExistingSchema]: K extends TFromSourceName ? TJoinType extends "right" | "full" ? TExistingSchema[K] | undefined : TExistingSchema[K] : TExistingSchema[K] } & { [K in keyof TNewSchema]: TJoinType extends "left" | "full" ? TNewSchema[K] | undefined : TNewSchema[K] }; +type ApplyJoinOptionalityToMergedSchema = { [K in keyof TExistingSchema]: K extends TFromSourceNames ? TJoinType extends "right" | "full" ? TExistingSchema[K] | undefined : TExistingSchema[K] : TExistingSchema[K] } & { [K in keyof TNewSchema]: TJoinType extends "left" | "full" ? TNewSchema[K] | undefined : TNewSchema[K] }; ``` -Defined in: [packages/db/src/query/builder/types.ts:779](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L779) +Defined in: [packages/db/src/query/builder/types.ts:987](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L987) ApplyJoinOptionalityToMergedSchema - Applies optionality rules when merging schemas @@ -49,6 +49,6 @@ into a single type while preserving all table references. `TJoinType` *extends* `"inner"` \| `"left"` \| `"right"` \| `"full"` \| `"outer"` \| `"cross"` -### TFromSourceName +### TFromSourceNames -`TFromSourceName` *extends* `string` +`TFromSourceNames` *extends* `string` diff --git a/docs/reference/type-aliases/ClearStorageFn.md b/docs/reference/type-aliases/ClearStorageFn.md index 05751d0912..699e453215 100644 --- a/docs/reference/type-aliases/ClearStorageFn.md +++ b/docs/reference/type-aliases/ClearStorageFn.md @@ -9,7 +9,7 @@ title: ClearStorageFn type ClearStorageFn = () => void; ``` -Defined in: [packages/db/src/local-storage.ts:90](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L90) +Defined in: [packages/db/src/local-storage.ts:91](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L91) Type for the clear utility function diff --git a/docs/reference/type-aliases/ContextFromSource.md b/docs/reference/type-aliases/ContextFromSource.md new file mode 100644 index 0000000000..73642c5c0f --- /dev/null +++ b/docs/reference/type-aliases/ContextFromSource.md @@ -0,0 +1,58 @@ +--- +id: ContextFromSource +title: ContextFromSource +--- + +# Type Alias: ContextFromSource\ + +```ts +type ContextFromSource = object; +``` + +Defined in: [packages/db/src/query/builder/types.ts:137](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L137) + +## Type Parameters + +### TSource + +`TSource` *extends* [`Source`](Source.md) + +## Properties + +### baseSchema + +```ts +baseSchema: SchemaFromSource; +``` + +Defined in: [packages/db/src/query/builder/types.ts:138](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L138) + +*** + +### fromSourceName + +```ts +fromSourceName: keyof TSource & string; +``` + +Defined in: [packages/db/src/query/builder/types.ts:140](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L140) + +*** + +### hasJoins + +```ts +hasJoins: false; +``` + +Defined in: [packages/db/src/query/builder/types.ts:141](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L141) + +*** + +### schema + +```ts +schema: SchemaFromSource; +``` + +Defined in: [packages/db/src/query/builder/types.ts:139](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L139) diff --git a/docs/reference/type-aliases/ContextFromUnionBranches.md b/docs/reference/type-aliases/ContextFromUnionBranches.md new file mode 100644 index 0000000000..929bb1b444 --- /dev/null +++ b/docs/reference/type-aliases/ContextFromUnionBranches.md @@ -0,0 +1,88 @@ +--- +id: ContextFromUnionBranches +title: ContextFromUnionBranches +--- + +# Type Alias: ContextFromUnionBranches\ + +```ts +type ContextFromUnionBranches = object; +``` + +Defined in: [packages/db/src/query/builder/types.ts:169](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L169) + +## Type Parameters + +### TBranches + +`TBranches` *extends* readonly \[[`QueryBuilder`](QueryBuilder.md)\<`any`\>, `...QueryBuilder[]`\] + +## Properties + +### baseSchema + +```ts +baseSchema: UnionBranchSchema & ContextSchema; +``` + +Defined in: [packages/db/src/query/builder/types.ts:172](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L172) + +*** + +### fromSourceName + +```ts +fromSourceName: keyof UnionBranchSchema & string; +``` + +Defined in: [packages/db/src/query/builder/types.ts:175](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L175) + +*** + +### hasJoins + +```ts +hasJoins: false; +``` + +Defined in: [packages/db/src/query/builder/types.ts:176](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L176) + +*** + +### hasResult + +```ts +hasResult: true; +``` + +Defined in: [packages/db/src/query/builder/types.ts:178](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L178) + +*** + +### refsSchema + +```ts +refsSchema: UnionBranchSchema; +``` + +Defined in: [packages/db/src/query/builder/types.ts:174](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L174) + +*** + +### result + +```ts +result: PrettifyIfPlainObject>; +``` + +Defined in: [packages/db/src/query/builder/types.ts:177](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L177) + +*** + +### schema + +```ts +schema: UnionBranchSchema & ContextSchema; +``` + +Defined in: [packages/db/src/query/builder/types.ts:173](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L173) diff --git a/docs/reference/type-aliases/ContextFromUnionSource.md b/docs/reference/type-aliases/ContextFromUnionSource.md new file mode 100644 index 0000000000..79930c1f53 --- /dev/null +++ b/docs/reference/type-aliases/ContextFromUnionSource.md @@ -0,0 +1,18 @@ +--- +id: ContextFromUnionSource +title: ContextFromUnionSource +--- + +# Type Alias: ContextFromUnionSource\ + +```ts +type ContextFromUnionSource = IsUnion extends true ? object : ContextFromSource; +``` + +Defined in: [packages/db/src/query/builder/types.ts:144](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L144) + +## Type Parameters + +### TSource + +`TSource` *extends* [`Source`](Source.md) diff --git a/docs/reference/type-aliases/ContextSchema.md b/docs/reference/type-aliases/ContextSchema.md index 09d96d6a38..ef7c6ed1f0 100644 --- a/docs/reference/type-aliases/ContextSchema.md +++ b/docs/reference/type-aliases/ContextSchema.md @@ -9,7 +9,7 @@ title: ContextSchema type ContextSchema = Record; ``` -Defined in: [packages/db/src/query/builder/types.ts:68](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L68) +Defined in: [packages/db/src/query/builder/types.ts:80](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L80) ContextSchema - The shape of available tables/collections in a query context diff --git a/docs/reference/type-aliases/ExtractContext.md b/docs/reference/type-aliases/ExtractContext.md index 4bbdc55025..d781fe1b57 100644 --- a/docs/reference/type-aliases/ExtractContext.md +++ b/docs/reference/type-aliases/ExtractContext.md @@ -9,7 +9,7 @@ title: ExtractContext type ExtractContext = T extends BaseQueryBuilder ? TContext : T extends QueryBuilder ? TContext : never; ``` -Defined in: [packages/db/src/query/builder/index.ts:1235](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1235) +Defined in: [packages/db/src/query/builder/index.ts:1411](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1411) ## Type Parameters diff --git a/docs/reference/type-aliases/FunctionalHavingRow.md b/docs/reference/type-aliases/FunctionalHavingRow.md index 039184e3c1..3b720db9aa 100644 --- a/docs/reference/type-aliases/FunctionalHavingRow.md +++ b/docs/reference/type-aliases/FunctionalHavingRow.md @@ -9,7 +9,7 @@ title: FunctionalHavingRow type FunctionalHavingRow = TContext["schema"] & TContext["hasResult"] extends true ? object : object; ``` -Defined in: [packages/db/src/query/builder/types.ts:483](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L483) +Defined in: [packages/db/src/query/builder/types.ts:640](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L640) FunctionalHavingRow - Type for the row parameter in functional having callbacks diff --git a/docs/reference/type-aliases/GetResult.md b/docs/reference/type-aliases/GetResult.md index 36f2043108..3b10139c7c 100644 --- a/docs/reference/type-aliases/GetResult.md +++ b/docs/reference/type-aliases/GetResult.md @@ -9,7 +9,7 @@ title: GetResult type GetResult = Prettify>; ``` -Defined in: [packages/db/src/query/builder/types.ts:848](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L848) +Defined in: [packages/db/src/query/builder/types.ts:1101](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1101) ## Type Parameters diff --git a/docs/reference/type-aliases/GetStorageSizeFn.md b/docs/reference/type-aliases/GetStorageSizeFn.md index 2008fbbd6c..b57ac423b2 100644 --- a/docs/reference/type-aliases/GetStorageSizeFn.md +++ b/docs/reference/type-aliases/GetStorageSizeFn.md @@ -9,7 +9,7 @@ title: GetStorageSizeFn type GetStorageSizeFn = () => number; ``` -Defined in: [packages/db/src/local-storage.ts:95](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L95) +Defined in: [packages/db/src/local-storage.ts:96](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L96) Type for the getStorageSize utility function diff --git a/docs/reference/type-aliases/GroupByCallback.md b/docs/reference/type-aliases/GroupByCallback.md index d5f6e0d5e9..077618195c 100644 --- a/docs/reference/type-aliases/GroupByCallback.md +++ b/docs/reference/type-aliases/GroupByCallback.md @@ -9,7 +9,7 @@ title: GroupByCallback type GroupByCallback = (refs) => any; ``` -Defined in: [packages/db/src/query/builder/types.ts:446](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L446) +Defined in: [packages/db/src/query/builder/types.ts:603](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L603) GroupByCallback - Type for groupBy clause callback functions diff --git a/docs/reference/type-aliases/IndexConstructor.md b/docs/reference/type-aliases/IndexConstructor.md index 0a64c46ab2..304437506f 100644 --- a/docs/reference/type-aliases/IndexConstructor.md +++ b/docs/reference/type-aliases/IndexConstructor.md @@ -9,7 +9,7 @@ title: IndexConstructor type IndexConstructor = (id, expression, name?, options?) => BaseIndex; ``` -Defined in: [packages/db/src/indexes/base-index.ts:213](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L213) +Defined in: [packages/db/src/indexes/base-index.ts:231](https://github.com/TanStack/db/blob/main/packages/db/src/indexes/base-index.ts#L231) Type for index constructor diff --git a/docs/reference/type-aliases/InferCollectionType.md b/docs/reference/type-aliases/InferCollectionType.md index 466fb9aeba..36b19a5fd8 100644 --- a/docs/reference/type-aliases/InferCollectionType.md +++ b/docs/reference/type-aliases/InferCollectionType.md @@ -9,7 +9,7 @@ title: InferCollectionType type InferCollectionType = T extends CollectionImpl ? WithVirtualProps : never; ``` -Defined in: [packages/db/src/query/builder/types.ts:89](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L89) +Defined in: [packages/db/src/query/builder/types.ts:101](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L101) InferCollectionType - Extracts the TypeScript type from a CollectionImpl diff --git a/docs/reference/type-aliases/InferResultType.md b/docs/reference/type-aliases/InferResultType.md index 15b1f23375..6a33c3baba 100644 --- a/docs/reference/type-aliases/InferResultType.md +++ b/docs/reference/type-aliases/InferResultType.md @@ -9,7 +9,7 @@ title: InferResultType type InferResultType = TContext extends SingleResult ? GetResult | undefined : GetResult[]; ``` -Defined in: [packages/db/src/query/builder/types.ts:805](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L805) +Defined in: [packages/db/src/query/builder/types.ts:1013](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1013) Utility type to infer the query result size (single row or an array) diff --git a/docs/reference/type-aliases/InitialQueryBuilder.md b/docs/reference/type-aliases/InitialQueryBuilder.md index 1308b07fc3..a4de915831 100644 --- a/docs/reference/type-aliases/InitialQueryBuilder.md +++ b/docs/reference/type-aliases/InitialQueryBuilder.md @@ -6,7 +6,7 @@ title: InitialQueryBuilder # Type Alias: InitialQueryBuilder ```ts -type InitialQueryBuilder = Pick, "from">; +type InitialQueryBuilder = Pick, "from" | "unionAll">; ``` -Defined in: [packages/db/src/query/builder/index.ts:1221](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1221) +Defined in: [packages/db/src/query/builder/index.ts:1394](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1394) diff --git a/docs/reference/type-aliases/JoinOnCallback.md b/docs/reference/type-aliases/JoinOnCallback.md index 27ba29f3ad..922b693ec0 100644 --- a/docs/reference/type-aliases/JoinOnCallback.md +++ b/docs/reference/type-aliases/JoinOnCallback.md @@ -9,7 +9,7 @@ title: JoinOnCallback type JoinOnCallback = (refs) => any; ``` -Defined in: [packages/db/src/query/builder/types.ts:462](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L462) +Defined in: [packages/db/src/query/builder/types.ts:619](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L619) JoinOnCallback - Type for join condition callback functions diff --git a/docs/reference/type-aliases/LiveQueryCollectionUtils.md b/docs/reference/type-aliases/LiveQueryCollectionUtils.md index ecb1be4892..bd12ab1281 100644 --- a/docs/reference/type-aliases/LiveQueryCollectionUtils.md +++ b/docs/reference/type-aliases/LiveQueryCollectionUtils.md @@ -9,7 +9,7 @@ title: LiveQueryCollectionUtils type LiveQueryCollectionUtils = UtilsRecord & object; ``` -Defined in: [packages/db/src/query/live/collection-config-builder.ts:54](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/collection-config-builder.ts#L54) +Defined in: [packages/db/src/query/live/collection-config-builder.ts:58](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/collection-config-builder.ts#L58) ## Type Declaration diff --git a/docs/reference/type-aliases/MergeContextForJoinCallback.md b/docs/reference/type-aliases/MergeContextForJoinCallback.md index b675c42d15..1873f80f38 100644 --- a/docs/reference/type-aliases/MergeContextForJoinCallback.md +++ b/docs/reference/type-aliases/MergeContextForJoinCallback.md @@ -6,10 +6,10 @@ title: MergeContextForJoinCallback # Type Alias: MergeContextForJoinCallback\ ```ts -type MergeContextForJoinCallback = object & PreserveHasResultFlag; +type MergeContextForJoinCallback = object & PreserveHasResultFlag & PreserveUnionFromFlag & PreserveFromSourceNames; ``` -Defined in: [packages/db/src/query/builder/types.ts:1005](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1005) +Defined in: [packages/db/src/query/builder/types.ts:1258](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1258) MergeContextForJoinCallback - Special context for join condition callbacks @@ -63,6 +63,12 @@ hasJoins: true; joinTypes: TContext["joinTypes"] extends Record ? TContext["joinTypes"] : object; ``` +### refsSchema + +```ts +refsSchema: RefsSchemaForContext & TNewSchema; +``` + ### result ```ts diff --git a/docs/reference/type-aliases/MergeContextWithJoinType.md b/docs/reference/type-aliases/MergeContextWithJoinType.md index 983e847180..43c439caca 100644 --- a/docs/reference/type-aliases/MergeContextWithJoinType.md +++ b/docs/reference/type-aliases/MergeContextWithJoinType.md @@ -6,10 +6,10 @@ title: MergeContextWithJoinType # Type Alias: MergeContextWithJoinType\ ```ts -type MergeContextWithJoinType = object & PreserveSingleResultFlag & PreserveHasResultFlag; +type MergeContextWithJoinType = object & PreserveSingleResultFlag & PreserveHasResultFlag & PreserveUnionFromFlag & PreserveFromSourceNames; ``` -Defined in: [packages/db/src/query/builder/types.ts:729](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L729) +Defined in: [packages/db/src/query/builder/types.ts:929](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L929) MergeContextWithJoinType - Creates a new context after a join operation @@ -59,6 +59,12 @@ hasJoins: true; joinTypes: TContext["joinTypes"] extends Record ? TContext["joinTypes"] : object & { [K in keyof TNewSchema & string]: TJoinType }; ``` +### refsSchema + +```ts +refsSchema: ApplyJoinOptionalityToMergedSchema, TNewSchema, TJoinType, FromSourceNamesForOptionality>; +``` + ### result ```ts @@ -68,7 +74,7 @@ result: TContext["result"]; ### schema ```ts -schema: ApplyJoinOptionalityToMergedSchema; +schema: ApplyJoinOptionalityToMergedSchema>; ``` ## Type Parameters diff --git a/docs/reference/type-aliases/OperatorName.md b/docs/reference/type-aliases/OperatorName.md index 92c072d006..695ed994ab 100644 --- a/docs/reference/type-aliases/OperatorName.md +++ b/docs/reference/type-aliases/OperatorName.md @@ -9,4 +9,4 @@ title: OperatorName type OperatorName = typeof operators[number]; ``` -Defined in: [packages/db/src/query/builder/functions.ts:437](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L437) +Defined in: [packages/db/src/query/builder/functions.ts:718](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L718) diff --git a/docs/reference/type-aliases/OrderByCallback.md b/docs/reference/type-aliases/OrderByCallback.md index b58fde0d12..783862a6f4 100644 --- a/docs/reference/type-aliases/OrderByCallback.md +++ b/docs/reference/type-aliases/OrderByCallback.md @@ -9,7 +9,7 @@ title: OrderByCallback type OrderByCallback = (refs) => any; ``` -Defined in: [packages/db/src/query/builder/types.ts:410](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L410) +Defined in: [packages/db/src/query/builder/types.ts:567](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L567) OrderByCallback - Type for orderBy clause callback functions diff --git a/docs/reference/type-aliases/Prettify.md b/docs/reference/type-aliases/Prettify.md index 673257fd9a..ac4189e446 100644 --- a/docs/reference/type-aliases/Prettify.md +++ b/docs/reference/type-aliases/Prettify.md @@ -9,7 +9,7 @@ title: Prettify type Prettify = { [K in keyof T]: T[K] } & object; ``` -Defined in: [packages/db/src/query/builder/types.ts:1044](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1044) +Defined in: [packages/db/src/query/builder/types.ts:1300](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1300) Prettify - Utility type for clean IDE display diff --git a/docs/reference/type-aliases/QueryBuilder.md b/docs/reference/type-aliases/QueryBuilder.md index 060999bb6c..790ca0702c 100644 --- a/docs/reference/type-aliases/QueryBuilder.md +++ b/docs/reference/type-aliases/QueryBuilder.md @@ -6,10 +6,10 @@ title: QueryBuilder # Type Alias: QueryBuilder\ ```ts -type QueryBuilder = Omit, "from" | "_getQuery">; +type QueryBuilder = Omit, "from" | "unionAll" | "_getQuery">; ``` -Defined in: [packages/db/src/query/builder/index.ts:1225](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1225) +Defined in: [packages/db/src/query/builder/index.ts:1401](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1401) ## Type Parameters diff --git a/docs/reference/type-aliases/QueryResult.md b/docs/reference/type-aliases/QueryResult.md index 26cecaf634..6d86f7a989 100644 --- a/docs/reference/type-aliases/QueryResult.md +++ b/docs/reference/type-aliases/QueryResult.md @@ -9,7 +9,7 @@ title: QueryResult type QueryResult = GetResult>; ``` -Defined in: [packages/db/src/query/builder/index.ts:1243](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1243) +Defined in: [packages/db/src/query/builder/index.ts:1419](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1419) ## Type Parameters diff --git a/docs/reference/type-aliases/Ref.md b/docs/reference/type-aliases/Ref.md index fce38d02f4..79aca3b7ee 100644 --- a/docs/reference/type-aliases/Ref.md +++ b/docs/reference/type-aliases/Ref.md @@ -6,10 +6,10 @@ title: Ref # Type Alias: Ref\ ```ts -type Ref = { [K in keyof T]: IsNonExactOptional extends true ? IsNonExactNullable extends true ? IsPlainObject> extends true ? Ref, Nullable> | undefined : RefLeaf, Nullable> | undefined : IsPlainObject> extends true ? Ref, Nullable> | undefined : RefLeaf, Nullable> | undefined : IsNonExactNullable extends true ? IsPlainObject> extends true ? Ref, Nullable> | null : RefLeaf, Nullable> | null : IsPlainObject extends true ? Ref : RefLeaf } & RefLeaf & VirtualPropsRef; +type Ref = T extends unknown ? RefBranch : never; ``` -Defined in: [packages/db/src/query/builder/types.ts:639](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L639) +Defined in: [packages/db/src/query/builder/types.ts:827](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L827) Ref - The user-facing ref interface for the query builder diff --git a/docs/reference/type-aliases/RefsForContext.md b/docs/reference/type-aliases/RefsForContext.md index 3476398e73..2634c6774b 100644 --- a/docs/reference/type-aliases/RefsForContext.md +++ b/docs/reference/type-aliases/RefsForContext.md @@ -6,25 +6,10 @@ title: RefsForContext # Type Alias: RefsForContext\ ```ts -type RefsForContext = { [K in keyof TContext["schema"]]: IsNonExactOptional extends true ? IsNonExactNullable extends true ? Ref, true> : Ref, true> : IsNonExactNullable extends true ? Ref, true> : Ref } & TContext["hasResult"] extends true ? object : object; +type RefsForContext = { [K in KeysOfUnion>]: IsNonExactOptional, K>> extends true ? IsNonExactNullable, K>> extends true ? RefForContextValue, K>>, true> : RefForContextValue, K>>, true> : IsNonExactNullable, K>> extends true ? RefForContextValue, K>>, true> : RefForContextValue, K>> } & TContext["hasResult"] extends true ? object : object; ``` -Defined in: [packages/db/src/query/builder/types.ts:502](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L502) - -RefsForContext - Creates ref proxies for all tables/collections in a query context - -This is the main entry point for creating ref objects in query builder callbacks. -For nullable join sides (left/right/full joins), it produces `Ref` instead -of `Ref | undefined`. This accurately reflects that the proxy object is always -present at build time (it's a truthy proxy that records property access paths), -while the `Nullable` flag ensures the result type correctly includes `| undefined`. - -Examples: -- Required field: `Ref` → user.name works, result is T -- Nullable join side: `Ref` → user.name works, result is T | undefined - -After `select()` is called, this type also includes `$selected` which provides access -to the SELECT result fields via `$selected.fieldName` syntax. +Defined in: [packages/db/src/query/builder/types.ts:677](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L677) ## Type Parameters diff --git a/docs/reference/type-aliases/ResultTypeFromSelect.md b/docs/reference/type-aliases/ResultTypeFromSelect.md index dd7e568aad..ab73347331 100644 --- a/docs/reference/type-aliases/ResultTypeFromSelect.md +++ b/docs/reference/type-aliases/ResultTypeFromSelect.md @@ -6,10 +6,10 @@ title: ResultTypeFromSelect # Type Alias: ResultTypeFromSelect\ ```ts -type ResultTypeFromSelect = IsAny extends true ? any : WithoutRefBrand extends true ? ExtractExpressionType : TSelectObject[K] extends ToArrayWrapper ? T[] : TSelectObject[K] extends ConcatToArrayWrapper ? string : TSelectObject[K] extends QueryBuilder ? Collection> : TSelectObject[K] extends Ref ? ExtractRef<(...)[(...)]> : (...)[(...)] extends RefLeaf<(...)> ? (...) extends (...) ? (...) : (...) : (...) extends (...) ? (...) : (...) }>>; +type ResultTypeFromSelect = IsAny extends true ? any : WithoutRefBrand extends true ? ExtractExpressionType : TSelectObject[K] extends ToArrayWrapper ? T[] : TSelectObject[K] extends ConcatToArrayWrapper ? string : TSelectObject[K] extends MaterializeWrapper ? IsSingle extends true ? T | undefined : T[] : TSelectObject[K] extends { __brand: "CaseWhenWrapper"; _result?: infer T } ? ResultTypeFromCaseWhen : (...)[(...)] extends QueryBuilder<(...)> ? Collection<(...)> : (...) extends (...) ? (...) : (...) }>>; ``` -Defined in: [packages/db/src/query/builder/types.ts:309](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L309) +Defined in: [packages/db/src/query/builder/types.ts:389](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L389) ResultTypeFromSelect - Infers the result type from a select object diff --git a/docs/reference/type-aliases/SchemaFromSource.md b/docs/reference/type-aliases/SchemaFromSource.md index 35569f09df..c065f6c540 100644 --- a/docs/reference/type-aliases/SchemaFromSource.md +++ b/docs/reference/type-aliases/SchemaFromSource.md @@ -6,10 +6,10 @@ title: SchemaFromSource # Type Alias: SchemaFromSource\ ```ts -type SchemaFromSource = Prettify<{ [K in keyof T]: T[K] extends CollectionImpl ? InferCollectionType : T[K] extends QueryBuilder ? GetResult : never }>; +type SchemaFromSource = Prettify<{ [K in keyof T]: T[K] extends CollectionImpl ? InferCollectionType : T[K] extends QueryBuilder ? GetRawResult : never }>; ``` -Defined in: [packages/db/src/query/builder/types.ts:104](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L104) +Defined in: [packages/db/src/query/builder/types.ts:116](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L116) SchemaFromSource - Converts a Source definition into a ContextSchema diff --git a/docs/reference/type-aliases/SelectObject.md b/docs/reference/type-aliases/SelectObject.md index 61ebb31249..27a738fe56 100644 --- a/docs/reference/type-aliases/SelectObject.md +++ b/docs/reference/type-aliases/SelectObject.md @@ -9,7 +9,7 @@ title: SelectObject type SelectObject = T; ``` -Defined in: [packages/db/src/query/builder/types.ts:208](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L208) +Defined in: [packages/db/src/query/builder/types.ts:279](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L279) SelectObject - Wrapper type for select clause objects diff --git a/docs/reference/type-aliases/SingleSource.md b/docs/reference/type-aliases/SingleSource.md new file mode 100644 index 0000000000..6ebceecfdb --- /dev/null +++ b/docs/reference/type-aliases/SingleSource.md @@ -0,0 +1,18 @@ +--- +id: SingleSource +title: SingleSource +--- + +# Type Alias: SingleSource\ + +```ts +type SingleSource = IsUnion extends true ? never : TSource; +``` + +Defined in: [packages/db/src/query/builder/types.ts:134](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L134) + +## Type Parameters + +### TSource + +`TSource` *extends* [`Source`](Source.md) diff --git a/docs/reference/type-aliases/Source.md b/docs/reference/type-aliases/Source.md index 9c797516a3..fe3c543014 100644 --- a/docs/reference/type-aliases/Source.md +++ b/docs/reference/type-aliases/Source.md @@ -9,15 +9,15 @@ title: Source type Source = object; ``` -Defined in: [packages/db/src/query/builder/types.ts:79](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L79) +Defined in: [packages/db/src/query/builder/types.ts:91](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L91) -Source - Input definition for query builder `from()` clause +Source - Input definition for query builder `from()` and `unionAll()` clauses Maps table aliases to either: - `CollectionImpl`: A database collection/table - `QueryBuilder`: A subquery that can be used as a table -Example: `{ users: usersCollection, orders: ordersCollection }` +Example: `{ users: usersCollection }` ## Index Signature diff --git a/docs/reference/type-aliases/SourceClauseContext.md b/docs/reference/type-aliases/SourceClauseContext.md new file mode 100644 index 0000000000..a9a4622033 --- /dev/null +++ b/docs/reference/type-aliases/SourceClauseContext.md @@ -0,0 +1,12 @@ +--- +id: SourceClauseContext +title: SourceClauseContext +--- + +# Type Alias: SourceClauseContext + +```ts +type SourceClauseContext = "from clause" | "unionAll clause" | "join clause"; +``` + +Defined in: [packages/db/src/errors.ts:388](https://github.com/TanStack/db/blob/main/packages/db/src/errors.ts#L388) diff --git a/docs/reference/type-aliases/StorageApi.md b/docs/reference/type-aliases/StorageApi.md index fde4dcad56..7a79766b22 100644 --- a/docs/reference/type-aliases/StorageApi.md +++ b/docs/reference/type-aliases/StorageApi.md @@ -9,6 +9,6 @@ title: StorageApi type StorageApi = Pick; ``` -Defined in: [packages/db/src/local-storage.ts:23](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L23) +Defined in: [packages/db/src/local-storage.ts:24](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L24) Storage API interface - subset of DOM Storage that we need diff --git a/docs/reference/type-aliases/StorageEventApi.md b/docs/reference/type-aliases/StorageEventApi.md index 4e3d8f10ab..de6e668685 100644 --- a/docs/reference/type-aliases/StorageEventApi.md +++ b/docs/reference/type-aliases/StorageEventApi.md @@ -9,7 +9,7 @@ title: StorageEventApi type StorageEventApi = object; ``` -Defined in: [packages/db/src/local-storage.ts:28](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L28) +Defined in: [packages/db/src/local-storage.ts:29](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L29) Storage event API - subset of Window for 'storage' events only @@ -21,7 +21,7 @@ Storage event API - subset of Window for 'storage' events only addEventListener: (type, listener) => void; ``` -Defined in: [packages/db/src/local-storage.ts:29](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L29) +Defined in: [packages/db/src/local-storage.ts:30](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L30) #### Parameters @@ -45,7 +45,7 @@ Defined in: [packages/db/src/local-storage.ts:29](https://github.com/TanStack/db removeEventListener: (type, listener) => void; ``` -Defined in: [packages/db/src/local-storage.ts:33](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L33) +Defined in: [packages/db/src/local-storage.ts:34](https://github.com/TanStack/db/blob/main/packages/db/src/local-storage.ts#L34) #### Parameters diff --git a/docs/reference/type-aliases/WhereCallback.md b/docs/reference/type-aliases/WhereCallback.md index 2f556c7215..61b99b8281 100644 --- a/docs/reference/type-aliases/WhereCallback.md +++ b/docs/reference/type-aliases/WhereCallback.md @@ -9,7 +9,7 @@ title: WhereCallback type WhereCallback = (refs) => any; ``` -Defined in: [packages/db/src/query/builder/types.ts:129](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L129) +Defined in: [packages/db/src/query/builder/types.ts:198](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L198) WhereCallback - Type for where/having clause callback functions diff --git a/docs/reference/type-aliases/WithResult.md b/docs/reference/type-aliases/WithResult.md index 862a84cf22..680631622b 100644 --- a/docs/reference/type-aliases/WithResult.md +++ b/docs/reference/type-aliases/WithResult.md @@ -9,7 +9,7 @@ title: WithResult type WithResult = Prettify & object>; ``` -Defined in: [packages/db/src/query/builder/types.ts:1034](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1034) +Defined in: [packages/db/src/query/builder/types.ts:1290](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/types.ts#L1290) WithResult - Updates a context with a new result type after select() diff --git a/docs/reference/type-aliases/WithVirtualProps.md b/docs/reference/type-aliases/WithVirtualProps.md index 7a8b21aeaf..d5cec665fe 100644 --- a/docs/reference/type-aliases/WithVirtualProps.md +++ b/docs/reference/type-aliases/WithVirtualProps.md @@ -9,7 +9,7 @@ title: WithVirtualProps type WithVirtualProps = T & VirtualRowProps; ``` -Defined in: [packages/db/src/virtual-props.ts:112](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L112) +Defined in: [packages/db/src/virtual-props.ts:117](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L117) Adds virtual properties to a row type. diff --git a/docs/reference/type-aliases/WithoutVirtualProps.md b/docs/reference/type-aliases/WithoutVirtualProps.md index f7c3d864e2..e314776ce2 100644 --- a/docs/reference/type-aliases/WithoutVirtualProps.md +++ b/docs/reference/type-aliases/WithoutVirtualProps.md @@ -9,7 +9,7 @@ title: WithoutVirtualProps type WithoutVirtualProps = Omit; ``` -Defined in: [packages/db/src/virtual-props.ts:130](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L130) +Defined in: [packages/db/src/virtual-props.ts:135](https://github.com/TanStack/db/blob/main/packages/db/src/virtual-props.ts#L135) Extracts the base type from a type that may have virtual properties. Useful when you need to work with the raw data without virtual properties. diff --git a/docs/reference/variables/Query.md b/docs/reference/variables/Query.md index 30e7d7ba37..4eae382bc9 100644 --- a/docs/reference/variables/Query.md +++ b/docs/reference/variables/Query.md @@ -9,4 +9,4 @@ title: Query const Query: InitialQueryBuilderConstructor = BaseQueryBuilder; ``` -Defined in: [packages/db/src/query/builder/index.ts:1232](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1232) +Defined in: [packages/db/src/query/builder/index.ts:1408](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/index.ts#L1408) diff --git a/docs/reference/variables/operators.md b/docs/reference/variables/operators.md index 84e9a00189..8338611985 100644 --- a/docs/reference/variables/operators.md +++ b/docs/reference/variables/operators.md @@ -6,9 +6,9 @@ title: operators # Variable: operators ```ts -const operators: readonly ["eq", "gt", "gte", "lt", "lte", "in", "like", "ilike", "and", "or", "not", "isNull", "isUndefined", "upper", "lower", "length", "concat", "add", "coalesce", "caseWhen", "count", "avg", "sum", "min", "max"]; +const operators: readonly ["eq", "gt", "gte", "lt", "lte", "in", "like", "ilike", "and", "or", "not", "isNull", "isUndefined", "upper", "lower", "length", "concat", "add", "subtract", "multiply", "divide", "coalesce", "caseWhen", "count", "avg", "sum", "min", "max"]; ``` -Defined in: [packages/db/src/query/builder/functions.ts](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts) +Defined in: [packages/db/src/query/builder/functions.ts:680](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L680) All supported operator names in TanStack DB expressions diff --git a/examples/angular/todos/package.json b/examples/angular/todos/package.json index fd4d3ac679..26d55fd858 100644 --- a/examples/angular/todos/package.json +++ b/examples/angular/todos/package.json @@ -28,8 +28,8 @@ "@angular/forms": "^20.3.16", "@angular/platform-browser": "^20.3.16", "@angular/router": "^20.3.16", - "@tanstack/angular-db": "^0.1.68", - "@tanstack/db": "^0.6.8", + "@tanstack/angular-db": "^0.1.76", + "@tanstack/db": "^0.6.16", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "~0.15.0" diff --git a/examples/electron/offline-first/package.json b/examples/electron/offline-first/package.json index b15ad88932..42131dd1c9 100644 --- a/examples/electron/offline-first/package.json +++ b/examples/electron/offline-first/package.json @@ -13,11 +13,11 @@ "postinstall": "prebuild-install --runtime electron --target 40.2.1 --arch arm64 || echo 'prebuild-install failed, try: npx @electron/rebuild'" }, "dependencies": { - "@tanstack/electron-db-sqlite-persistence": "^0.1.12", - "@tanstack/node-db-sqlite-persistence": "^0.2.0", - "@tanstack/offline-transactions": "^1.0.33", - "@tanstack/query-db-collection": "^1.0.40", - "@tanstack/react-db": "^0.1.86", + "@tanstack/electron-db-sqlite-persistence": "^0.1.20", + "@tanstack/node-db-sqlite-persistence": "^0.2.8", + "@tanstack/offline-transactions": "^1.0.41", + "@tanstack/query-db-collection": "^1.1.0", + "@tanstack/react-db": "^0.1.94", "@tanstack/react-query": "^5.90.20", "better-sqlite3": "^12.6.2", "react": "^19.2.4", diff --git a/examples/react-native/offline-transactions/package.json b/examples/react-native/offline-transactions/package.json index 6274e7353c..6289136338 100644 --- a/examples/react-native/offline-transactions/package.json +++ b/examples/react-native/offline-transactions/package.json @@ -15,11 +15,11 @@ "@op-engineering/op-sqlite": "^15.2.5", "@react-native-async-storage/async-storage": "2.1.2", "@react-native-community/netinfo": "11.4.1", - "@tanstack/db": "^0.6.8", - "@tanstack/offline-transactions": "^1.0.33", - "@tanstack/query-db-collection": "^1.0.40", - "@tanstack/react-db": "^0.1.86", - "@tanstack/react-native-db-sqlite-persistence": "^0.2.0", + "@tanstack/db": "^0.6.16", + "@tanstack/offline-transactions": "^1.0.41", + "@tanstack/query-db-collection": "^1.1.0", + "@tanstack/react-db": "^0.1.94", + "@tanstack/react-native-db-sqlite-persistence": "^0.2.8", "@tanstack/react-query": "^5.90.20", "expo": "~53.0.26", "expo-constants": "~17.1.0", diff --git a/examples/react-native/shopping-list/package.json b/examples/react-native/shopping-list/package.json index be14aed669..a63a641093 100644 --- a/examples/react-native/shopping-list/package.json +++ b/examples/react-native/shopping-list/package.json @@ -18,11 +18,11 @@ "@op-engineering/op-sqlite": "^15.2.5", "@react-native-async-storage/async-storage": "2.1.2", "@react-native-community/netinfo": "11.4.1", - "@tanstack/db": "^0.6.8", - "@tanstack/electric-db-collection": "^0.3.6", - "@tanstack/offline-transactions": "^1.0.33", - "@tanstack/react-db": "^0.1.86", - "@tanstack/react-native-db-sqlite-persistence": "^0.2.0", + "@tanstack/db": "^0.6.16", + "@tanstack/electric-db-collection": "^0.3.14", + "@tanstack/offline-transactions": "^1.0.41", + "@tanstack/react-db": "^0.1.94", + "@tanstack/react-native-db-sqlite-persistence": "^0.2.8", "@tanstack/react-query": "^5.90.20", "expo": "~53.0.26", "expo-constants": "~17.1.0", diff --git a/examples/react/offline-transactions/package.json b/examples/react/offline-transactions/package.json index 05ce1caea1..31ffdc09f4 100644 --- a/examples/react/offline-transactions/package.json +++ b/examples/react/offline-transactions/package.json @@ -8,11 +8,11 @@ "build": "vite build && tsc --noEmit" }, "dependencies": { - "@tanstack/browser-db-sqlite-persistence": "^0.2.0", - "@tanstack/db": "^0.6.8", - "@tanstack/offline-transactions": "^1.0.33", - "@tanstack/query-db-collection": "^1.0.40", - "@tanstack/react-db": "^0.1.86", + "@tanstack/browser-db-sqlite-persistence": "^0.2.8", + "@tanstack/db": "^0.6.16", + "@tanstack/offline-transactions": "^1.0.41", + "@tanstack/query-db-collection": "^1.1.0", + "@tanstack/react-db": "^0.1.94", "@tanstack/react-query": "^5.90.20", "@tanstack/react-router": "^1.159.5", "@tanstack/react-router-devtools": "^1.159.5", diff --git a/examples/react/paced-mutations-demo/package.json b/examples/react/paced-mutations-demo/package.json index 02a929269c..6441cd1d3b 100644 --- a/examples/react/paced-mutations-demo/package.json +++ b/examples/react/paced-mutations-demo/package.json @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/db": "^0.6.8", - "@tanstack/react-db": "^0.1.86", + "@tanstack/db": "^0.6.16", + "@tanstack/react-db": "^0.1.94", "mitt": "^3.0.1", "react": "^19.2.4", "react-dom": "^19.2.4" diff --git a/examples/react/projects/package.json b/examples/react/projects/package.json index 1f0178c52e..2d69ccbb5a 100644 --- a/examples/react/projects/package.json +++ b/examples/react/projects/package.json @@ -17,8 +17,8 @@ "dependencies": { "@tailwindcss/vite": "^4.1.18", "@tanstack/query-core": "^5.90.20", - "@tanstack/query-db-collection": "^1.0.40", - "@tanstack/react-db": "^0.1.86", + "@tanstack/query-db-collection": "^1.1.0", + "@tanstack/react-db": "^0.1.94", "@tanstack/react-router": "^1.159.5", "@tanstack/react-router-devtools": "^1.159.5", "@tanstack/react-router-with-query": "^1.130.17", diff --git a/examples/react/todo/package.json b/examples/react/todo/package.json index a656ae38ed..2935606209 100644 --- a/examples/react/todo/package.json +++ b/examples/react/todo/package.json @@ -3,13 +3,13 @@ "private": true, "version": "0.1.25", "dependencies": { - "@tanstack/electric-db-collection": "^0.3.6", + "@tanstack/electric-db-collection": "^0.3.14", "@tanstack/query-core": "^5.90.20", - "@tanstack/query-db-collection": "^1.0.40", - "@tanstack/react-db": "^0.1.86", + "@tanstack/query-db-collection": "^1.1.0", + "@tanstack/react-db": "^0.1.94", "@tanstack/react-router": "^1.159.5", "@tanstack/react-start": "^1.159.5", - "@tanstack/trailbase-db-collection": "^0.1.86", + "@tanstack/trailbase-db-collection": "^0.1.94", "cors": "^2.8.6", "drizzle-orm": "^0.45.1", "drizzle-zod": "^0.8.3", diff --git a/examples/solid/todo/package.json b/examples/solid/todo/package.json index 215c98e075..95e0616db0 100644 --- a/examples/solid/todo/package.json +++ b/examples/solid/todo/package.json @@ -3,13 +3,13 @@ "private": true, "version": "0.0.35", "dependencies": { - "@tanstack/electric-db-collection": "^0.3.6", + "@tanstack/electric-db-collection": "^0.3.14", "@tanstack/query-core": "^5.90.20", - "@tanstack/query-db-collection": "^1.0.40", - "@tanstack/solid-db": "^0.2.22", + "@tanstack/query-db-collection": "^1.1.0", + "@tanstack/solid-db": "^0.2.30", "@tanstack/solid-router": "^1.159.5", "@tanstack/solid-start": "^1.159.5", - "@tanstack/trailbase-db-collection": "^0.1.86", + "@tanstack/trailbase-db-collection": "^0.1.94", "cors": "^2.8.6", "drizzle-orm": "^0.45.1", "drizzle-zod": "^0.8.3", diff --git a/packages/angular-db/CHANGELOG.md b/packages/angular-db/CHANGELOG.md index 10a322326a..e92e0c88f1 100644 --- a/packages/angular-db/CHANGELOG.md +++ b/packages/angular-db/CHANGELOG.md @@ -1,5 +1,69 @@ # @tanstack/angular-db +## 0.1.76 + +### Patch Changes + +- Updated dependencies [[`8258d09`](https://github.com/TanStack/db/commit/8258d0955ab47c8510bd49ea59bcdbefd2ae054d), [`286964d`](https://github.com/TanStack/db/commit/286964d72612b59e3e427baabd9870f5a71a4281)]: + - @tanstack/db@0.6.16 + +## 0.1.75 + +### Patch Changes + +- fix(angular-db): a `{ query }` config-object passed to `injectLiveQuery` now syncs ([#1638](https://github.com/TanStack/db/pull/1638)) + + The config-object branch called `createLiveQueryCollection(opts)` without defaulting `startSync`, unlike the query-function and reactive-options branches (which force `startSync: true`), so a bare `{ query }` never started syncing and produced no data. It now defaults `startSync: true` and `gcTime: 0` while still honoring any explicit values in the config. + +- Extract shared live-query adapter helpers into `@tanstack/db` ([#1641](https://github.com/TanStack/db/pull/1641)) + + Adds `isCollection`, `isSingleResultCollection`, and `getLiveQueryStatusFlags` to `@tanstack/db` and migrates all five framework adapters to use them. `isCollection` replaces the per-adapter duck-typing and Solid's `instanceof CollectionImpl` with one structural, multi-realm-safe guard (the `instanceof` form gave false negatives across dual-package boundaries). No behavior change; internal deduplication only. + +- Updated dependencies [[`eabcea7`](https://github.com/TanStack/db/commit/eabcea743fdfa045a2db01e12bef87403613102a), [`6d4c096`](https://github.com/TanStack/db/commit/6d4c096395b7ff3f428122ea8842bbead551a8c9)]: + - @tanstack/db@0.6.15 + +## 0.1.74 + +### Patch Changes + +- Updated dependencies [[`397e12a`](https://github.com/TanStack/db/commit/397e12a1224ad563e20a331eebcbe904cd4af948)]: + - @tanstack/db@0.6.14 + +## 0.1.73 + +### Patch Changes + +- Updated dependencies [[`99e9afe`](https://github.com/TanStack/db/commit/99e9afed46ab4083d66609a3e37ee44103c2177f), [`816b667`](https://github.com/TanStack/db/commit/816b6671c2cc9806715f6e6ed4410b3f4efb5afb)]: + - @tanstack/db@0.6.13 + +## 0.1.72 + +### Patch Changes + +- Updated dependencies [[`2b27dd1`](https://github.com/TanStack/db/commit/2b27dd1448da71c78a48e2390cb71b0ada1b1488)]: + - @tanstack/db@0.6.12 + +## 0.1.71 + +### Patch Changes + +- Updated dependencies [[`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`36fb29a`](https://github.com/TanStack/db/commit/36fb29ad7e906d39b6afdba2fd31e369c601bbb0), [`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`ac09b11`](https://github.com/TanStack/db/commit/ac09b1177a100eafa85cba3cd09dd1f53f933ded)]: + - @tanstack/db@0.6.11 + +## 0.1.70 + +### Patch Changes + +- Updated dependencies [[`307fdf8`](https://github.com/TanStack/db/commit/307fdf80f522a39a50e316316b3b75ba27fd5e84)]: + - @tanstack/db@0.6.10 + +## 0.1.69 + +### Patch Changes + +- Updated dependencies [[`2147345`](https://github.com/TanStack/db/commit/2147345236ceee6e73d9fc6c0cdc2385833199fc), [`00389a4`](https://github.com/TanStack/db/commit/00389a47b258ad58fc3a03c5cc6f66957b9bd2d1)]: + - @tanstack/db@0.6.9 + ## 0.1.68 ### Patch Changes diff --git a/packages/angular-db/package.json b/packages/angular-db/package.json index e8ee4b5620..eda106fc25 100644 --- a/packages/angular-db/package.json +++ b/packages/angular-db/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/angular-db", - "version": "0.1.68", + "version": "0.1.76", "description": "Angular integration for @tanstack/db", "author": "Ethan McDaniel", "license": "MIT", diff --git a/packages/angular-db/src/index.ts b/packages/angular-db/src/index.ts index 1bce5c6843..f8cac4086b 100644 --- a/packages/angular-db/src/index.ts +++ b/packages/angular-db/src/index.ts @@ -6,11 +6,15 @@ import { inject, signal, } from '@angular/core' -import { BaseQueryBuilder, createLiveQueryCollection } from '@tanstack/db' +import { + BaseQueryBuilder, + createLiveQueryCollection, + isCollection, + isSingleResultCollection, +} from '@tanstack/db' import type { ChangeMessage, Collection, - CollectionConfigSingleRowOption, CollectionStatus, Context, GetResult, @@ -139,14 +143,7 @@ export function injectLiveQuery(opts: any) { const collection = computed(() => { // Check if it's an existing collection - const isExistingCollection = - opts && - typeof opts === `object` && - typeof opts.subscribeChanges === `function` && - typeof opts.startSyncImmediate === `function` && - typeof opts.id === `string` - - if (isExistingCollection) { + if (isCollection(opts)) { return opts } @@ -194,9 +191,11 @@ export function injectLiveQuery(opts: any) { }) } - // Handle LiveQueryCollectionConfig objects + // Handle LiveQueryCollectionConfig objects. Default startSync/gcTime to + // match the query-fn and reactive-options paths, but let an explicit value + // in the config win — otherwise a bare `{ query }` never syncs. if (opts && typeof opts === `object` && typeof opts.query === `function`) { - return createLiveQueryCollection(opts) + return createLiveQueryCollection({ startSync: true, gcTime: 0, ...opts }) } throw new Error(`Invalid options provided to injectLiveQuery`) @@ -214,10 +213,9 @@ export function injectLiveQuery(opts: any) { if (!currentCollection) { return internalData() } - const config = currentCollection.config as - | CollectionConfigSingleRowOption - | undefined - return config?.singleResult ? internalData()[0] : internalData() + return isSingleResultCollection(currentCollection) + ? internalData()[0] + : internalData() }) const syncDataFromCollection = ( @@ -282,7 +280,9 @@ export function injectLiveQuery(opts: any) { return { state, data, - collection, + // Loosely typed so the impl return stays compatible with every overload + // (the shared `isCollection` guard narrows the computed to `Collection | null`). + collection: collection as Signal, status, isLoading: computed(() => status() === `loading`), isReady: computed(() => status() === `ready` || status() === `disabled`), diff --git a/packages/angular-db/tests/conformance.test.ts b/packages/angular-db/tests/conformance.test.ts new file mode 100644 index 0000000000..a8cc7ec803 --- /dev/null +++ b/packages/angular-db/tests/conformance.test.ts @@ -0,0 +1,221 @@ +/** + * Angular driver for the shared live-query conformance suite. + * + * `injectLiveQuery` needs an injection context, so each mount runs inside a + * child `EnvironmentInjector` created off TestBed's; `unmount` calls + * `injector.destroy()`, firing the `DestroyRef` cleanup. Result signals are read + * after settling. Controllable inputs use Angular's reactive `{ params, query }` + * form driven by a signal. + * + * `knownGaps` is populated empirically from the run below. + */ +import { + EnvironmentInjector, + createEnvironmentInjector, + runInInjectionContext, + signal, +} from '@angular/core' +import { TestBed } from '@angular/core/testing' +import { + coalesce, + count, + createCollection, + createLiveQueryCollection, + createOptimisticAction, + eq, + gt, + sum, +} from '@tanstack/db' +import { + mockSyncCollectionOptions, + mockSyncCollectionOptionsNoInitialState, +} from '../../db/tests/utils' +import { injectLiveQuery } from '../src/index' +import { runSuite } from '../../db/tests/conformance/suite' +import type { + ConformanceResult, + ControllableHandle, + DeferredSourceHandle, + LiveQueryDriver, + LiveQueryHandle, + QueryBuild, + SourceHandle, +} from '../../db/tests/conformance/contract' + +let sourceSeq = 0 + +function writer(collection: any) { + return (type: `insert` | `update` | `delete`, value: T) => { + collection.utils.begin() + collection.utils.write({ type, value }) + collection.utils.commit() + } +} + +function makeSource( + initialData: ReadonlyArray, +): SourceHandle { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `conformance-angular-${sourceSeq++}`, + getKey: (r) => r.id, + initialData: [...initialData], + }), + ) + const write = writer(collection) + return { + collection, + insert: (row) => write(`insert`, row), + update: (row) => write(`update`, row), + remove: (row) => write(`delete`, row), + } +} + +function makeDeferredSource< + T extends { id: string }, +>(): DeferredSourceHandle { + const collection = createCollection( + mockSyncCollectionOptionsNoInitialState({ + id: `conformance-angular-${sourceSeq++}`, + getKey: (r) => r.id, + }), + ) + collection.startSyncImmediate() + const write = writer(collection) + return { + collection, + insert: (row) => write(`insert`, row), + update: (row) => write(`update`, row), + remove: (row) => write(`delete`, row), + emit: (rows) => { + collection.utils.begin() + rows.forEach((value) => collection.utils.write({ type: `insert`, value })) + collection.utils.commit() + }, + markReady: () => collection.utils.markReady(), + } +} + +function makePrecreated(build: QueryBuild, opts?: { startSync?: boolean }) { + const collection = createLiveQueryCollection({ + query: build as any, + startSync: opts?.startSync ?? true, + }) + return { collection } +} + +function makeErrorSource() { + const collection = createCollection<{ id: string }>({ + id: `conformance-angular-err-${sourceSeq++}`, + getKey: (r) => r.id, + startSync: false, + sync: { + sync: () => { + throw new Error(`conformance: sync failure`) + }, + }, + }) + try { + collection.startSyncImmediate() + } catch { + // expected: engine catches the sync error and sets status to `error` + } + return { collection } +} + +async function settle() { + await new Promise((resolve) => setTimeout(resolve, 0)) + await new Promise((resolve) => setTimeout(resolve, 50)) +} + +function makeHandle(result: any, destroy: () => void): LiveQueryHandle { + return { + current(): ConformanceResult { + return { + data: result.data(), + status: result.status(), + isReady: Boolean(result.isReady()), + isError: Boolean(result.isError()), + // angular-db exposes no `isEnabled`; derive it from status (status-derived). + isEnabled: result.status() !== `disabled`, + } + }, + flush: settle, + async apply(fn: () => void) { + fn() + await settle() + }, + unmount() { + destroy() + }, + } +} + +function inCtx(fn: () => any): { result: any; destroy: () => void } { + const parent = TestBed.inject(EnvironmentInjector) + const injector = createEnvironmentInjector([], parent) + let result: any + runInInjectionContext(injector, () => { + result = fn() + }) + return { result, destroy: () => injector.destroy() } +} + +function mount(build: QueryBuild) { + const { result, destroy } = inCtx(() => injectLiveQuery(build as any)) + return makeHandle(result, destroy) +} + +function mountCollection(collection: any) { + const { result, destroy } = inCtx(() => injectLiveQuery(collection)) + return makeHandle(result, destroy) +} + +function mountConfig(build: QueryBuild) { + const { result, destroy } = inCtx(() => injectLiveQuery({ query: build })) + return makeHandle(result, destroy) +} + +function mountDisabled() { + const { result, destroy } = inCtx(() => injectLiveQuery(() => null)) + return makeHandle(result, destroy) +} + +function mountControllable

( + build: (q: any, param: P) => any, + initial: P, +): ControllableHandle

{ + const param = signal

(initial) + const { result, destroy } = inCtx(() => + injectLiveQuery({ + params: () => ({ value: param() }), + query: ({ params, q }: any) => build(q, params.value), + }), + ) + const handle = makeHandle(result, destroy) + return { + ...handle, + async setParam(next: P) { + param.set(next) + await settle() + }, + } +} + +const angularDriver: LiveQueryDriver = { + name: `angular`, + ops: { eq, gt, count, sum, coalesce, createOptimisticAction }, + makeSource, + makeDeferredSource, + makePrecreated, + makeErrorSource, + mount, + mountControllable, + mountCollection, + mountConfig, + mountDisabled, + knownGaps: [], + features: { serverSnapshot: false, suspense: false }, +} + +runSuite(angularDriver) diff --git a/packages/browser-db-sqlite-persistence/CHANGELOG.md b/packages/browser-db-sqlite-persistence/CHANGELOG.md index fafcb0bb1f..dcebd33ec9 100644 --- a/packages/browser-db-sqlite-persistence/CHANGELOG.md +++ b/packages/browser-db-sqlite-persistence/CHANGELOG.md @@ -1,5 +1,63 @@ # @tanstack/browser-db-sqlite-persistence +## 0.2.8 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.8 + +## 0.2.7 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.7 + +## 0.2.6 + +### Patch Changes + +- Updated dependencies [[`f7da776`](https://github.com/TanStack/db/commit/f7da77660b16cbfe30817fb5c938267d696c8d1c)]: + - @tanstack/db-sqlite-persistence-core@0.2.6 + +## 0.2.5 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.5 + +## 0.2.4 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.4 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.3 + +## 0.2.2 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.2 + +## 0.2.1 + +### Patch Changes + +- Use a safe `randomUUID` helper that falls back to `crypto.getRandomValues` when `crypto.randomUUID` is unavailable (non-secure browser contexts such as dev servers reached via a LAN IP over HTTP). Fixes #1541. ([#1593](https://github.com/TanStack/db/pull/1593)) + +- Updated dependencies [[`00389a4`](https://github.com/TanStack/db/commit/00389a47b258ad58fc3a03c5cc6f66957b9bd2d1)]: + - @tanstack/db-sqlite-persistence-core@0.2.1 + ## 0.2.0 ### Minor Changes diff --git a/packages/browser-db-sqlite-persistence/README.md b/packages/browser-db-sqlite-persistence/README.md index b14c83b32f..84175bf7cd 100644 --- a/packages/browser-db-sqlite-persistence/README.md +++ b/packages/browser-db-sqlite-persistence/README.md @@ -1,6 +1,9 @@ # @tanstack/browser-db-sqlite-persistence -Thin browser SQLite persistence for TanStack DB using `wa-sqlite` + OPFS. +Browser SQLite persistence for TanStack DB using `wa-sqlite` + OPFS. + +Supports both single-tab (default) and multi-tab usage. Multi-tab coordination +is opt-in by passing a `BrowserCollectionCoordinator`. ## Public API @@ -8,7 +11,12 @@ Thin browser SQLite persistence for TanStack DB using `wa-sqlite` + OPFS. - `openBrowserWASQLiteOPFSDatabase(...)` - `persistedCollectionOptions(...)` (re-exported from core) -## Quick start +## Quick start (single-tab) + +By default, `createBrowserWASQLitePersistence` uses `SingleProcessCoordinator` +semantics — no leader election, no `BroadcastChannel`, no Web Locks. This is +the right choice when your app is only ever open in one tab at a time, or when +each tab uses its own database. ```ts import { createCollection } from '@tanstack/db' @@ -42,13 +50,60 @@ export const todosCollection = createCollection( ) ``` +## Multi-tab usage + +To safely share a single OPFS database across multiple tabs of the same +origin, pass a `BrowserCollectionCoordinator` via the `coordinator` option. +The coordinator uses the Web Locks API to elect a leader tab, and +`BroadcastChannel` to fan out committed transactions to follower tabs. +Follower tabs forward writes to the leader via RPC over the channel. + +```ts +import { createCollection } from '@tanstack/db' +import { + BrowserCollectionCoordinator, + createBrowserWASQLitePersistence, + openBrowserWASQLiteOPFSDatabase, + persistedCollectionOptions, +} from '@tanstack/browser-db-sqlite-persistence' + +const database = await openBrowserWASQLiteOPFSDatabase({ + databaseName: `tanstack-db.sqlite`, +}) + +const coordinator = new BrowserCollectionCoordinator({ + dbName: `tanstack-db`, +}) + +const persistence = createBrowserWASQLitePersistence({ + database, + coordinator, +}) + +export const todosCollection = createCollection( + persistedCollectionOptions({ + id: `todos`, + getKey: (todo) => todo.id, + persistence, + schemaVersion: 1, + }), +) + +// On teardown: +// coordinator.dispose() +// await database.close?.() +``` + +See [`examples/react/offline-transactions`](../../examples/react/offline-transactions/src/db/persisted-todos.ts) +for a full multi-tab example. + ## Notes -- This package is Phase 7 single-tab browser wiring: it uses - `SingleProcessCoordinator` semantics by default. - `openBrowserWASQLiteOPFSDatabase(...)` starts a dedicated Web Worker and routes SQL operations through it. OPFS sync access handle APIs are used in that worker context. -- Single-tab mode does not require BroadcastChannel or Web Locks for +- Single-tab mode does not require `BroadcastChannel` or Web Locks for correctness. +- Multi-tab mode requires `BroadcastChannel` and the Web Locks API; both are + available in all modern browsers. - OPFS capability failures are surfaced as `PersistenceUnavailableError`. diff --git a/packages/browser-db-sqlite-persistence/package.json b/packages/browser-db-sqlite-persistence/package.json index 8e94ebedf9..de64e8fccb 100644 --- a/packages/browser-db-sqlite-persistence/package.json +++ b/packages/browser-db-sqlite-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/browser-db-sqlite-persistence", - "version": "0.2.0", + "version": "0.2.8", "description": "Browser wa-sqlite persisted collection adapter for TanStack DB", "author": "TanStack Team", "license": "MIT", diff --git a/packages/browser-db-sqlite-persistence/src/browser-coordinator.ts b/packages/browser-db-sqlite-persistence/src/browser-coordinator.ts index 5b518ea387..1babddc5a7 100644 --- a/packages/browser-db-sqlite-persistence/src/browser-coordinator.ts +++ b/packages/browser-db-sqlite-persistence/src/browser-coordinator.ts @@ -1,3 +1,4 @@ +import { safeRandomUUID } from '@tanstack/db-sqlite-persistence-core' import type { ApplyLocalMutationsResponse, PersistedCollectionCoordinator, @@ -118,7 +119,7 @@ export type BrowserCollectionCoordinatorOptions = { // --------------------------------------------------------------------------- export class BrowserCollectionCoordinator implements PersistedCollectionCoordinator { - private readonly nodeId = crypto.randomUUID() + private readonly nodeId = safeRandomUUID() private readonly dbName: string private adapter: AdapterWithPullSince | null private readonly channel: BroadcastChannel @@ -205,7 +206,7 @@ export class BrowserCollectionCoordinator implements PersistedCollectionCoordina error?: string }>(collectionId, { type: `rpc:ensureRemoteSubset:req`, - rpcId: crypto.randomUUID(), + rpcId: safeRandomUUID(), options, }) @@ -233,7 +234,7 @@ export class BrowserCollectionCoordinator implements PersistedCollectionCoordina error?: string }>(collectionId, { type: `rpc:ensurePersistedIndex:req`, - rpcId: crypto.randomUUID(), + rpcId: safeRandomUUID(), signature, spec, }) @@ -252,16 +253,16 @@ export class BrowserCollectionCoordinator implements PersistedCollectionCoordina if (this.isLeader(collectionId)) { return this.handleApplyLocalMutations(collectionId, { type: `rpc:applyLocalMutations:req`, - rpcId: crypto.randomUUID(), - envelopeId: crypto.randomUUID(), + rpcId: safeRandomUUID(), + envelopeId: safeRandomUUID(), mutations, }) } return this.sendRPC(collectionId, { type: `rpc:applyLocalMutations:req`, - rpcId: crypto.randomUUID(), - envelopeId: crypto.randomUUID(), + rpcId: safeRandomUUID(), + envelopeId: safeRandomUUID(), mutations, }) } @@ -273,14 +274,14 @@ export class BrowserCollectionCoordinator implements PersistedCollectionCoordina if (this.isLeader(collectionId)) { return this.handlePullSince(collectionId, { type: `rpc:pullSince:req`, - rpcId: crypto.randomUUID(), + rpcId: safeRandomUUID(), fromRowVersion, }) } return this.sendRPC(collectionId, { type: `rpc:pullSince:req`, - rpcId: crypto.randomUUID(), + rpcId: safeRandomUUID(), fromRowVersion, }) } @@ -663,7 +664,7 @@ export class BrowserCollectionCoordinator implements PersistedCollectionCoordina // Build and apply the persisted transaction const tx = { - txId: crypto.randomUUID(), + txId: safeRandomUUID(), term, seq, rowVersion, diff --git a/packages/capacitor-db-sqlite-persistence/CHANGELOG.md b/packages/capacitor-db-sqlite-persistence/CHANGELOG.md index 127512e3aa..aaf56287f3 100644 --- a/packages/capacitor-db-sqlite-persistence/CHANGELOG.md +++ b/packages/capacitor-db-sqlite-persistence/CHANGELOG.md @@ -1,5 +1,61 @@ # @tanstack/capacitor-db-sqlite-persistence +## 0.2.8 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.8 + +## 0.2.7 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.7 + +## 0.2.6 + +### Patch Changes + +- Updated dependencies [[`f7da776`](https://github.com/TanStack/db/commit/f7da77660b16cbfe30817fb5c938267d696c8d1c)]: + - @tanstack/db-sqlite-persistence-core@0.2.6 + +## 0.2.5 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.5 + +## 0.2.4 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.4 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.3 + +## 0.2.2 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.2 + +## 0.2.1 + +### Patch Changes + +- Updated dependencies [[`00389a4`](https://github.com/TanStack/db/commit/00389a47b258ad58fc3a03c5cc6f66957b9bd2d1)]: + - @tanstack/db-sqlite-persistence-core@0.2.1 + ## 0.2.0 ### Minor Changes diff --git a/packages/capacitor-db-sqlite-persistence/e2e/app/CHANGELOG.md b/packages/capacitor-db-sqlite-persistence/e2e/app/CHANGELOG.md index 118201f804..5feabbd34f 100644 --- a/packages/capacitor-db-sqlite-persistence/e2e/app/CHANGELOG.md +++ b/packages/capacitor-db-sqlite-persistence/e2e/app/CHANGELOG.md @@ -1,5 +1,69 @@ # @tanstack/capacitor-db-sqlite-persistence-e2e-app +## 0.0.20 + +### Patch Changes + +- Updated dependencies [[`8258d09`](https://github.com/TanStack/db/commit/8258d0955ab47c8510bd49ea59bcdbefd2ae054d), [`286964d`](https://github.com/TanStack/db/commit/286964d72612b59e3e427baabd9870f5a71a4281)]: + - @tanstack/db@0.6.16 + - @tanstack/capacitor-db-sqlite-persistence@0.2.8 + +## 0.0.19 + +### Patch Changes + +- Updated dependencies [[`eabcea7`](https://github.com/TanStack/db/commit/eabcea743fdfa045a2db01e12bef87403613102a), [`6d4c096`](https://github.com/TanStack/db/commit/6d4c096395b7ff3f428122ea8842bbead551a8c9)]: + - @tanstack/db@0.6.15 + - @tanstack/capacitor-db-sqlite-persistence@0.2.7 + +## 0.0.18 + +### Patch Changes + +- Updated dependencies [[`397e12a`](https://github.com/TanStack/db/commit/397e12a1224ad563e20a331eebcbe904cd4af948)]: + - @tanstack/db@0.6.14 + - @tanstack/capacitor-db-sqlite-persistence@0.2.6 + +## 0.0.17 + +### Patch Changes + +- Updated dependencies [[`99e9afe`](https://github.com/TanStack/db/commit/99e9afed46ab4083d66609a3e37ee44103c2177f), [`816b667`](https://github.com/TanStack/db/commit/816b6671c2cc9806715f6e6ed4410b3f4efb5afb)]: + - @tanstack/db@0.6.13 + - @tanstack/capacitor-db-sqlite-persistence@0.2.5 + +## 0.0.16 + +### Patch Changes + +- Updated dependencies [[`2b27dd1`](https://github.com/TanStack/db/commit/2b27dd1448da71c78a48e2390cb71b0ada1b1488)]: + - @tanstack/db@0.6.12 + - @tanstack/capacitor-db-sqlite-persistence@0.2.4 + +## 0.0.15 + +### Patch Changes + +- Updated dependencies [[`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`36fb29a`](https://github.com/TanStack/db/commit/36fb29ad7e906d39b6afdba2fd31e369c601bbb0), [`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`ac09b11`](https://github.com/TanStack/db/commit/ac09b1177a100eafa85cba3cd09dd1f53f933ded)]: + - @tanstack/db@0.6.11 + - @tanstack/capacitor-db-sqlite-persistence@0.2.3 + +## 0.0.14 + +### Patch Changes + +- Updated dependencies [[`307fdf8`](https://github.com/TanStack/db/commit/307fdf80f522a39a50e316316b3b75ba27fd5e84)]: + - @tanstack/db@0.6.10 + - @tanstack/capacitor-db-sqlite-persistence@0.2.2 + +## 0.0.13 + +### Patch Changes + +- Updated dependencies [[`2147345`](https://github.com/TanStack/db/commit/2147345236ceee6e73d9fc6c0cdc2385833199fc), [`00389a4`](https://github.com/TanStack/db/commit/00389a47b258ad58fc3a03c5cc6f66957b9bd2d1)]: + - @tanstack/db@0.6.9 + - @tanstack/capacitor-db-sqlite-persistence@0.2.1 + ## 0.0.12 ### Patch Changes diff --git a/packages/capacitor-db-sqlite-persistence/e2e/app/package.json b/packages/capacitor-db-sqlite-persistence/e2e/app/package.json index 7c8cacdf76..f769604a1e 100644 --- a/packages/capacitor-db-sqlite-persistence/e2e/app/package.json +++ b/packages/capacitor-db-sqlite-persistence/e2e/app/package.json @@ -1,7 +1,7 @@ { "name": "@tanstack/capacitor-db-sqlite-persistence-e2e-app", "private": true, - "version": "0.0.12", + "version": "0.0.20", "type": "module", "scripts": { "build": "vite build", diff --git a/packages/capacitor-db-sqlite-persistence/package.json b/packages/capacitor-db-sqlite-persistence/package.json index 8ea6d338f6..48707c77e4 100644 --- a/packages/capacitor-db-sqlite-persistence/package.json +++ b/packages/capacitor-db-sqlite-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/capacitor-db-sqlite-persistence", - "version": "0.2.0", + "version": "0.2.8", "description": "Capacitor SQLite persisted collection adapter for TanStack DB", "author": "TanStack Team", "license": "MIT", diff --git a/packages/cloudflare-durable-objects-db-sqlite-persistence/CHANGELOG.md b/packages/cloudflare-durable-objects-db-sqlite-persistence/CHANGELOG.md index 87f4d18c83..1c0203475c 100644 --- a/packages/cloudflare-durable-objects-db-sqlite-persistence/CHANGELOG.md +++ b/packages/cloudflare-durable-objects-db-sqlite-persistence/CHANGELOG.md @@ -1,5 +1,61 @@ # @tanstack/cloudflare-durable-objects-db-sqlite-persistence +## 0.2.8 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.8 + +## 0.2.7 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.7 + +## 0.2.6 + +### Patch Changes + +- Updated dependencies [[`f7da776`](https://github.com/TanStack/db/commit/f7da77660b16cbfe30817fb5c938267d696c8d1c)]: + - @tanstack/db-sqlite-persistence-core@0.2.6 + +## 0.2.5 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.5 + +## 0.2.4 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.4 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.3 + +## 0.2.2 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.2 + +## 0.2.1 + +### Patch Changes + +- Updated dependencies [[`00389a4`](https://github.com/TanStack/db/commit/00389a47b258ad58fc3a03c5cc6f66957b9bd2d1)]: + - @tanstack/db-sqlite-persistence-core@0.2.1 + ## 0.2.0 ### Minor Changes diff --git a/packages/cloudflare-durable-objects-db-sqlite-persistence/package.json b/packages/cloudflare-durable-objects-db-sqlite-persistence/package.json index 257183d06e..728f6fce01 100644 --- a/packages/cloudflare-durable-objects-db-sqlite-persistence/package.json +++ b/packages/cloudflare-durable-objects-db-sqlite-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/cloudflare-durable-objects-db-sqlite-persistence", - "version": "0.2.0", + "version": "0.2.8", "description": "Cloudflare Durable Object SQLite persisted collection adapter for TanStack DB", "author": "TanStack Team", "license": "MIT", diff --git a/packages/db-sqlite-persistence-core/CHANGELOG.md b/packages/db-sqlite-persistence-core/CHANGELOG.md index e1721d2e3a..3e89e46a1f 100644 --- a/packages/db-sqlite-persistence-core/CHANGELOG.md +++ b/packages/db-sqlite-persistence-core/CHANGELOG.md @@ -1,5 +1,65 @@ # @tanstack/db-sqlite-persistence-core +## 0.2.8 + +### Patch Changes + +- Updated dependencies [[`8258d09`](https://github.com/TanStack/db/commit/8258d0955ab47c8510bd49ea59bcdbefd2ae054d), [`286964d`](https://github.com/TanStack/db/commit/286964d72612b59e3e427baabd9870f5a71a4281)]: + - @tanstack/db@0.6.16 + +## 0.2.7 + +### Patch Changes + +- Updated dependencies [[`eabcea7`](https://github.com/TanStack/db/commit/eabcea743fdfa045a2db01e12bef87403613102a), [`6d4c096`](https://github.com/TanStack/db/commit/6d4c096395b7ff3f428122ea8842bbead551a8c9)]: + - @tanstack/db@0.6.15 + +## 0.2.6 + +### Patch Changes + +- Fixed bug where internal metadata would get reset on insert causing stale items to remain ([#1626](https://github.com/TanStack/db/pull/1626)) + +- Updated dependencies [[`397e12a`](https://github.com/TanStack/db/commit/397e12a1224ad563e20a331eebcbe904cd4af948)]: + - @tanstack/db@0.6.14 + +## 0.2.5 + +### Patch Changes + +- Updated dependencies [[`99e9afe`](https://github.com/TanStack/db/commit/99e9afed46ab4083d66609a3e37ee44103c2177f), [`816b667`](https://github.com/TanStack/db/commit/816b6671c2cc9806715f6e6ed4410b3f4efb5afb)]: + - @tanstack/db@0.6.13 + +## 0.2.4 + +### Patch Changes + +- Updated dependencies [[`2b27dd1`](https://github.com/TanStack/db/commit/2b27dd1448da71c78a48e2390cb71b0ada1b1488)]: + - @tanstack/db@0.6.12 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies [[`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`36fb29a`](https://github.com/TanStack/db/commit/36fb29ad7e906d39b6afdba2fd31e369c601bbb0), [`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`ac09b11`](https://github.com/TanStack/db/commit/ac09b1177a100eafa85cba3cd09dd1f53f933ded)]: + - @tanstack/db@0.6.11 + +## 0.2.2 + +### Patch Changes + +- Updated dependencies [[`307fdf8`](https://github.com/TanStack/db/commit/307fdf80f522a39a50e316316b3b75ba27fd5e84)]: + - @tanstack/db@0.6.10 + +## 0.2.1 + +### Patch Changes + +- Use a safe `randomUUID` helper that falls back to `crypto.getRandomValues` when `crypto.randomUUID` is unavailable (non-secure browser contexts such as dev servers reached via a LAN IP over HTTP). Fixes #1541. ([#1593](https://github.com/TanStack/db/pull/1593)) + +- Updated dependencies [[`2147345`](https://github.com/TanStack/db/commit/2147345236ceee6e73d9fc6c0cdc2385833199fc), [`00389a4`](https://github.com/TanStack/db/commit/00389a47b258ad58fc3a03c5cc6f66957b9bd2d1)]: + - @tanstack/db@0.6.9 + ## 0.2.0 ### Minor Changes diff --git a/packages/db-sqlite-persistence-core/package.json b/packages/db-sqlite-persistence-core/package.json index ddcd5847d0..be2341c0e2 100644 --- a/packages/db-sqlite-persistence-core/package.json +++ b/packages/db-sqlite-persistence-core/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/db-sqlite-persistence-core", - "version": "0.2.0", + "version": "0.2.8", "description": "SQLite persisted collection core for TanStack DB", "author": "TanStack Team", "license": "MIT", diff --git a/packages/db-sqlite-persistence-core/src/index.ts b/packages/db-sqlite-persistence-core/src/index.ts index 921b92f3b0..9e2bb9faae 100644 --- a/packages/db-sqlite-persistence-core/src/index.ts +++ b/packages/db-sqlite-persistence-core/src/index.ts @@ -1,3 +1,5 @@ export * from './persisted' export * from './errors' export * from './sqlite-core-adapter' +// Re-export for use in non-secure browser contexts (see #1541) +export { safeRandomUUID } from '@tanstack/db' diff --git a/packages/db-sqlite-persistence-core/src/persisted.ts b/packages/db-sqlite-persistence-core/src/persisted.ts index 7c20e96883..ca60a050ab 100644 --- a/packages/db-sqlite-persistence-core/src/persisted.ts +++ b/packages/db-sqlite-persistence-core/src/persisted.ts @@ -1,4 +1,8 @@ -import { compileSingleRowExpression, toBooleanPredicate } from '@tanstack/db' +import { + compileSingleRowExpression, + safeRandomUUID, + toBooleanPredicate, +} from '@tanstack/db' import { InvalidPersistedCollectionConfigError, InvalidPersistedCollectionCoordinatorError, @@ -440,7 +444,7 @@ type SyncControlFns = { export class SingleProcessCoordinator implements PersistedCollectionCoordinator { private readonly nodeId: string - constructor(nodeId: string = crypto.randomUUID()) { + constructor(nodeId: string = safeRandomUUID()) { this.nodeId = nodeId } @@ -467,7 +471,7 @@ export class SingleProcessCoordinator implements PersistedCollectionCoordinator public pullSince(): Promise { return Promise.resolve({ type: `rpc:pullSince:res`, - rpcId: crypto.randomUUID(), + rpcId: safeRandomUUID(), ok: true, latestTerm: 1, latestSeq: 0, @@ -1387,7 +1391,7 @@ class PersistedCollectionRuntime< this.createTxCommittedPayload({ term: streamPosition.term, seq: streamPosition.seq, - txId: crypto.randomUUID(), + txId: safeRandomUUID(), latestRowVersion: streamPosition.rowVersion, changedRows: [], deletedKeys: [], @@ -1427,7 +1431,7 @@ class PersistedCollectionRuntime< streamPosition: { term: number; seq: number; rowVersion: number }, ): PersistedTx { return { - txId: crypto.randomUUID(), + txId: safeRandomUUID(), term: streamPosition.term, seq: streamPosition.seq, rowVersion: streamPosition.rowVersion, @@ -1471,7 +1475,7 @@ class PersistedCollectionRuntime< streamPosition: { term: number; seq: number; rowVersion: number }, ): PersistedTx { return { - txId: crypto.randomUUID(), + txId: safeRandomUUID(), term: streamPosition.term, seq: streamPosition.seq, rowVersion: streamPosition.rowVersion, @@ -2286,9 +2290,21 @@ function createWrappedSyncConfig< message.type === `insert` && normalization.operation.metadata === undefined ) { - openTransaction.rowMetadataWrites.set(normalization.operation.key, { - type: `delete`, - }) + // Reset stale metadata for a fresh insert, but don't clobber an + // explicit metadata write already queued for this key in the same + // transaction (e.g. query reconcile stamps owners, then inserts). + if ( + !openTransaction.rowMetadataWrites.has( + normalization.operation.key, + ) + ) { + openTransaction.rowMetadataWrites.set( + normalization.operation.key, + { + type: `delete`, + }, + ) + } } else if (normalization.operation.metadata !== undefined) { openTransaction.rowMetadataWrites.set(normalization.operation.key, { type: `set`, @@ -2607,7 +2623,7 @@ export function persistedCollectionOptions< const { schemaVersion, ...syncOptions } = options const collectionId = - syncOptions.id ?? `persisted-collection:${crypto.randomUUID()}` + syncOptions.id ?? `persisted-collection:${safeRandomUUID()}` const persistence = resolvePersistenceForCollection( syncOptions.persistence, { @@ -2635,7 +2651,7 @@ export function persistedCollectionOptions< const { schemaVersion, ...localOnlyOptions } = options const collectionId = - localOnlyOptions.id ?? `persisted-collection:${crypto.randomUUID()}` + localOnlyOptions.id ?? `persisted-collection:${safeRandomUUID()}` const persistence = resolvePersistenceForCollection( localOnlyOptions.persistence, { diff --git a/packages/db-sqlite-persistence-core/tests/persisted.test.ts b/packages/db-sqlite-persistence-core/tests/persisted.test.ts index 57c11d8442..42bbcf5633 100644 --- a/packages/db-sqlite-persistence-core/tests/persisted.test.ts +++ b/packages/db-sqlite-persistence-core/tests/persisted.test.ts @@ -816,6 +816,78 @@ describe(`persistedCollectionOptions`, () => { ) }) + it(`preserves row metadata set before a metadata-less insert in the same sync transaction`, async () => { + const adapter = createRecordingAdapter() + const ownership = { queryCollection: { owners: [`gc:q1`] } } + const sync: SyncConfig = { + sync: ({ begin, write, commit, markReady, metadata }) => { + begin() + metadata?.row.set(`remote-1`, ownership) + write({ + type: `insert`, + value: { + id: `remote-1`, + title: `From remote`, + }, + }) + commit() + markReady() + }, + } + + const collection = createCollection( + persistedCollectionOptions({ + id: `sync-present`, + getKey: (item: Todo) => item.id, + sync, + persistence: { + adapter, + }, + }), + ) + + await collection.stateWhenReady() + await flushAsyncWork() + + expect(adapter.rowMetadata.get(`remote-1`)).toEqual(ownership) + expect(collection._state.syncedMetadata.get(`remote-1`)).toEqual(ownership) + }) + + it(`resets stale row metadata for a metadata-less insert with no queued metadata`, async () => { + const adapter = createRecordingAdapter() + adapter.rowMetadata.set(`remote-1`, { stale: true }) + const sync: SyncConfig = { + sync: ({ begin, write, commit, markReady }) => { + begin() + write({ + type: `insert`, + value: { + id: `remote-1`, + title: `From remote`, + }, + }) + commit() + markReady() + }, + } + + const collection = createCollection( + persistedCollectionOptions({ + id: `sync-present`, + getKey: (item: Todo) => item.id, + sync, + persistence: { + adapter, + }, + }), + ) + + await collection.stateWhenReady() + await flushAsyncWork() + + expect(adapter.rowMetadata.has(`remote-1`)).toBe(false) + }) + it(`uses a stable generated collection id in sync-present mode when id is omitted`, async () => { const adapter = createRecordingAdapter() const options = persistedCollectionOptions({ diff --git a/packages/db/CHANGELOG.md b/packages/db/CHANGELOG.md index d59f3991e6..57ca69702c 100644 --- a/packages/db/CHANGELOG.md +++ b/packages/db/CHANGELOG.md @@ -1,5 +1,121 @@ # @tanstack/db +## 0.6.16 + +### Patch Changes + +- Fix queries failing to typecheck when the collection's row type is a generic type parameter. Refs inside where/join/select callbacks now expose the properties guaranteed by the type parameter's constraint, and subqueries over generic collections can be used as join sources again (regression introduced in 0.6.6). ([#1678](https://github.com/TanStack/db/pull/1678)) + +- Preserve an explicit `gcTime: 0` on live query collections. The live query config builder used `this.config.gcTime || 5000`, so a `gcTime` of `0` (which disables garbage collection) was treated as unset and silently replaced by the 5s default, causing the collection to be garbage collected instead of kept alive. Use `??` so only `undefined` falls back to the default. ([#1660](https://github.com/TanStack/db/pull/1660)) + +## 0.6.15 + +### Patch Changes + +- Clarify local write status documentation for `$synced` and `isPersisted.promise`, and add core coverage for queued ambiguous server-key sync while optimistic temp-key inserts are pending. ([#1652](https://github.com/TanStack/db/pull/1652)) + +- Extract shared live-query adapter helpers into `@tanstack/db` ([#1641](https://github.com/TanStack/db/pull/1641)) + + Adds `isCollection`, `isSingleResultCollection`, and `getLiveQueryStatusFlags` to `@tanstack/db` and migrates all five framework adapters to use them. `isCollection` replaces the per-adapter duck-typing and Solid's `instanceof CollectionImpl` with one structural, multi-realm-safe guard (the `instanceof` form gave false negatives across dual-package boundaries). No behavior change; internal deduplication only. + +## 0.6.14 + +### Patch Changes + +- Avoid full row origin snapshots during incremental collection updates and make bulk mutation merging linear. ([#1640](https://github.com/TanStack/db/pull/1640)) + +## 0.6.13 + +### Patch Changes + +- Fix `.select()` collapsing discriminated-union fields to the intersection of common keys (#1511). `Ref` now distributes over `T` so `keyof (A | B | C)` no longer reduces the union to its common keys, and `ExtractRef` now distinguishes a real branded `Ref` (where the underlying user type `U` can be returned directly) from a spread-produced inline object (which still needs to be projected through `ResultTypeFromSelect`). This preserves discriminated unions both when the field is selected at the top level and when the field is nested inside another selected object. The real-`Ref` detection uses a strict structural equivalence against the canonical `Ref` shape, so spread-derived objects that keep the same keys but change a field's type (e.g. `{ ...u, code: u.slug }`) or drop an optional key (e.g. `const { nickname, ...rest } = u`) are projected through `ResultTypeFromSelect` instead of being collapsed back to `U`. ([#1597](https://github.com/TanStack/db/pull/1597)) + +- fix(db): keep deeply nested includes in sync when sibling groups share nested correlation keys ([#1607](https://github.com/TanStack/db/pull/1607)) + + Deeply nested includes could drop or stop updating nested rows when sibling parent groups shared the same nested correlation key, especially when one sibling group was inserted after the initial load. Shared nested pipeline buffers were being drained through route state that was scoped too narrowly, so one branch could consume a buffered update before other branches that referenced the same nested row received it. + + Nested route state is now shared at the same scope as the nested buffer and routes updates to every concrete destination branch before clearing the buffer. Snapshot replay still seeds late-arriving sibling groups with already-materialized rows, and recursive pending-change detection ensures deeper routed updates are flushed back up through the result tree. + +## 0.6.12 + +### Patch Changes + +- Fix live query includes reconciliation so updates that re-emit existing child rows update internal child collections instead of attempting duplicate inserts, and ensure duplicate-key sync errors handle collection configs without live query internals. ([#1600](https://github.com/TanStack/db/pull/1600)) + +## 0.6.11 + +### Patch Changes + +- Fix incorrect results from index-optimized `where` clauses that combine indexed and non-indexed conditions. ([#1582](https://github.com/TanStack/db/pull/1582)) + - `OR` expressions are now only served from indexes when every disjunct can use an index; otherwise the query falls back to a full scan. Previously, rows matched only by a non-indexed disjunct were missing from the result. + - `AND` expressions still use indexes for the conditions that have them, but the remaining conditions are now enforced by re-checking each candidate row against the full expression. Previously, non-indexed conditions were silently dropped, returning rows that did not match the query. + - Compound range conditions (e.g. `age > 5 AND age < 10`) combined with conditions on other fields no longer ignore those other conditions. + - Compound range conditions sharing the same boundary value (e.g. `age >= 5 AND age > 5`) now apply the strictest bound regardless of the order the conditions appear in, using the same value comparison semantics as the indexes (dates, locale strings, ...). + - Compound range conditions that only bound one side (e.g. `age > 5 AND age >= 8`) no longer return an empty result. + - Strict range comparisons (`gt`/`lt`) on BTree-indexed fields holding normalized values such as dates now correctly exclude the boundary value. + - Compound range conditions with a `null`/`undefined` bound (e.g. `gt(score, undefined)`) now re-filter against the full expression instead of returning index-ordered rows, matching the semantics of a full scan (a comparison against `null`/`undefined` is never true). + - Index-optimized `eq`, `IN`, and range queries on a field that has rows with `null`/`undefined` values no longer leak those rows into results. BTree indexes store and return such rows (they sort as the smallest key), but a comparison against `null`/`undefined` is never true, so these results are now re-filtered against the full expression to stay equivalent to a full scan. + - String range conditions (`gt`/`gte`/`lt`/`lte`) on a collection using locale string collation (the default) are no longer served by the index. The index orders strings with `localeCompare` while the `where` evaluator compares them with standard relational operators, so an index range lookup could omit matching rows; these conditions now fall back to a full scan. + - Range conditions whose operand is not ordered the same way by the index and the `where` evaluator (arrays, plain objects, Temporal values) now fall back to a full scan instead of using the index, which could otherwise omit matching rows. + - Range conditions on an index created with a custom comparator now fall back to a full scan, since the comparator's ordering may not match the `where` evaluator's relational operators. + +- fix(query): drive lazy-join loading through the collection the join key resolves to ([#1614](https://github.com/TanStack/db/pull/1614)) + + When a subquery used in a JOIN clause selects its join key from a _joined_ source rather than from its own `from` clause, the lazy-join loader subscribed to the wrong inner source: it used the subquery's `from` alias while computing the index requirement against the collection the key actually resolves to. This produced a misleading `Join requires an index` warning naming an already-indexed collection and an unnecessary full-load fallback. `followRef` now reports the resolved source alias, so lazy loading subscribes to the correct collection and loads through its index. + +- Adopt PostgreSQL float semantics for `NaN` in `where` clauses and ordering. ([#1582](https://github.com/TanStack/db/pull/1582)) + + `NaN` (and invalid `Date` values, whose timestamp is `NaN`) previously had no consistent order — `NaN === NaN` is `false` in JavaScript, so `NaN` compared unequal to everything and could not be sorted or indexed deterministically. Following PostgreSQL, `NaN` is now treated as **equal to itself** and **greater than every other non-null value**: + - `eq(row.value, NaN)` matches rows whose value is `NaN`; `inArray(row.value, [NaN, ...])` matches them too. + - Range comparisons treat `NaN` as the greatest value: `gt`/`gte` include it, `lt`/`lte` exclude it. + - Ordering by a field containing `NaN` is now deterministic, with `NaN` sorting last (and `null` still ordered by `NULLS FIRST`/`NULLS LAST`). + + `null`/`undefined` are unaffected: they continue to use three-valued logic (a comparison with `null` yields `UNKNOWN`). + + This makes results independent of whether a query is served from an index or a full scan. + +- Fix prototype pollution via `select()` alias paths. Aliases were split on `.` and walked into the result object without sanitization, so a query like `select(() => ({ ['__proto__.polluted']: ... }))` (or any segment matching `__proto__`, `prototype`, or `constructor`) could mutate `Object.prototype`. The select compiler now rejects unsafe alias path segments with a new `UnsafeAliasPathError`. Fixes #1584. ([#1595](https://github.com/TanStack/db/pull/1595)) + +## 0.6.10 + +### Patch Changes + +- Fix live query `preload()` hanging forever after a source collection was cleaned up (#1576) ([#1606](https://github.com/TanStack/db/pull/1606)) + + When a source collection is cleaned up while a live query depends on it, the live query transitions to an error state and latches an internal `isInErrorState` flag. That flag was never reset, so restarting sync (e.g. calling `preload()` again after cleanup when switching profiles) left the live query unable to become ready and the returned promise never resolved. The flag is now cleared at the start of each sync session so the live query can recover. + +## 0.6.9 + +### Patch Changes + +- Add `subtract`, `multiply`, and `divide` math functions for computed columns ([#1151](https://github.com/TanStack/db/pull/1151)) + + These functions enable complex calculations in `select` and `orderBy` clauses, such as ranking algorithms that combine multiple factors (e.g., HN-style scoring that balances recency and rating). + + ```ts + import { subtract, multiply, divide } from '@tanstack/db' + + // Example: Sort by computed ranking score + const ranked = createLiveQueryCollection((q) => + q + .from({ r: recipesCollection }) + .orderBy( + ({ r }) => + subtract( + multiply(r.rating, r.timesMade), + divide(r.ageInMs, 86400000), + ), + 'desc', + ), + ) + ``` + + - `subtract(a, b)` - Subtraction + - `multiply(a, b)` - Multiplication + - `divide(a, b)` - Division (returns `null` on divide-by-zero) + +- Use a safe `randomUUID` helper that falls back to `crypto.getRandomValues` when `crypto.randomUUID` is unavailable (non-secure browser contexts such as dev servers reached via a LAN IP over HTTP). Fixes #1541. ([#1593](https://github.com/TanStack/db/pull/1593)) + ## 0.6.8 ### Patch Changes diff --git a/packages/db/package.json b/packages/db/package.json index 218beb8072..f99b6fe435 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/db", - "version": "0.6.8", + "version": "0.6.16", "description": "A reactive client store for building super fast apps on sync", "author": "Kyle Mathews", "license": "MIT", diff --git a/packages/db/src/collection/change-events.ts b/packages/db/src/collection/change-events.ts index 6afac412ce..3f4977b7bd 100644 --- a/packages/db/src/collection/change-events.ts +++ b/packages/db/src/collection/change-events.ts @@ -138,11 +138,16 @@ export function currentStateAsChanges< ) if (optimizationResult.canOptimize) { - // Use index optimization + // Use index optimization. When the index lookup is inexact, the keys + // are a superset of the true result (some conditions could not be + // served by an index), so re-check each row against the full expression. + const filterFn = optimizationResult.isExact + ? undefined + : createFilterFunctionFromExpression(expression) const result: Array, TKey>> = [] for (const key of optimizationResult.matchingKeys) { const value = collection.get(key) - if (value !== undefined) { + if (value !== undefined && (filterFn?.(value) ?? true)) { result.push({ type: `insert`, key, diff --git a/packages/db/src/collection/index.ts b/packages/db/src/collection/index.ts index e51eb998d6..137fd5f595 100644 --- a/packages/db/src/collection/index.ts +++ b/packages/db/src/collection/index.ts @@ -1,3 +1,4 @@ +import { safeRandomUUID } from '../utils/uuid' import { CollectionConfigurationError, CollectionRequiresConfigError, @@ -329,7 +330,7 @@ export class CollectionImpl< if (config.id) { this.id = config.id } else { - this.id = crypto.randomUUID() + this.id = safeRandomUUID() } // Set default values for optional config properties diff --git a/packages/db/src/collection/mutations.ts b/packages/db/src/collection/mutations.ts index 765e409ef6..abfb6693eb 100644 --- a/packages/db/src/collection/mutations.ts +++ b/packages/db/src/collection/mutations.ts @@ -1,4 +1,5 @@ import { withArrayChangeTracking, withChangeTracking } from '../proxy' +import { safeRandomUUID } from '../utils/uuid' import { createTransaction, getActiveTransaction } from '../transactions' import { DeleteKeyNotFoundError, @@ -193,7 +194,7 @@ export class CollectionMutationsManager< const globalKey = this.generateGlobalKey(key, item) const mutation: PendingMutation = { - mutationId: crypto.randomUUID(), + mutationId: safeRandomUUID(), original: {}, modified: validatedData, // Pick the values from validatedData based on what's passed in - this is for cases @@ -366,7 +367,7 @@ export class CollectionMutationsManager< const globalKey = this.generateGlobalKey(modifiedItemId, modifiedItem) return { - mutationId: crypto.randomUUID(), + mutationId: safeRandomUUID(), original: originalItem, modified: modifiedItem, // Pick the values from modifiedItem based on what's passed in - this is for cases @@ -497,7 +498,7 @@ export class CollectionMutationsManager< `delete`, CollectionImpl > = { - mutationId: crypto.randomUUID(), + mutationId: safeRandomUUID(), original: this.state.get(key)!, modified: this.state.get(key)!, changes: this.state.get(key)!, diff --git a/packages/db/src/collection/state.ts b/packages/db/src/collection/state.ts index 9cbdebb234..0f7b3b868c 100644 --- a/packages/db/src/collection/state.ts +++ b/packages/db/src/collection/state.ts @@ -154,7 +154,12 @@ export class CollectionStateManager< } /** - * Checks if a row has pending optimistic mutations (not yet confirmed by sync). + * Checks whether this row currently has no pending local optimistic writes. + * + * This is local mutation status, not backend confirmation: `true` means the + * row is not currently affected by an optimistic transaction in this + * collection's visible state. + * * Used to compute the $synced virtual property. */ public isRowSynced(key: TKey): boolean { @@ -226,6 +231,21 @@ export class CollectionStateManager< }) } + private snapshotRowOriginsForKeys( + keys: Iterable, + ): Map { + const rowOrigins = new Map() + + for (const key of keys) { + const origin = this.rowOrigins.get(key) + if (origin !== undefined) { + rowOrigins.set(key, origin) + } + } + + return rowOrigins + } + private enrichWithVirtualPropsSnapshot( row: TOutput, virtualProps: VirtualRowProps, @@ -476,7 +496,7 @@ export class CollectionStateManager< const previousState = new Map(this.optimisticUpserts) const previousDeletes = new Set(this.optimisticDeletes) - const previousRowOrigins = new Map(this.rowOrigins) + const previousRowOrigins = this.rowOrigins // Update pending optimistic state for completed/failed transactions for (const transaction of this.transactions.values()) { @@ -857,10 +877,6 @@ export class CollectionStateManager< // Set flag to prevent redundant optimistic state recalculations this.isCommittingSyncTransactions = true - const previousRowOrigins = new Map(this.rowOrigins) - const previousOptimisticUpserts = new Map(this.optimisticUpserts) - const previousOptimisticDeletes = new Set(this.optimisticDeletes) - // Get the optimistic snapshot from the truncate transaction (captured when truncate() was called) const truncateOptimisticSnapshot = hasTruncateSync ? committedSyncedTransactions.find((t) => t.truncate) @@ -880,6 +896,18 @@ export class CollectionStateManager< } } + const virtualSnapshotKeys = new Set(changedKeys) + for (const key of this.pendingOptimisticDirectUpserts) { + virtualSnapshotKeys.add(key) + } + for (const key of this.pendingOptimisticDirectDeletes) { + virtualSnapshotKeys.add(key) + } + const previousRowOrigins = + this.snapshotRowOriginsForKeys(virtualSnapshotKeys) + const previousOptimisticUpserts = new Map(this.optimisticUpserts) + const previousOptimisticDeletes = new Set(this.optimisticDeletes) + // Use pre-captured state if available (from optimistic scenarios), // otherwise capture current state (for pure sync scenarios) let currentVisibleState = this.preSyncVisibleState diff --git a/packages/db/src/collection/sync.ts b/packages/db/src/collection/sync.ts index 82fffb7728..af89ed2cf3 100644 --- a/packages/db/src/collection/sync.ts +++ b/packages/db/src/collection/sync.ts @@ -151,9 +151,10 @@ export class CollectionSyncManager< // throwing a duplicate-key error during reconciliation. messageType = `update` } else { - const utils = this.config - .utils as Partial - const internal = utils[LIVE_QUERY_INTERNAL] + const utils = this.config.utils as + | Partial + | undefined + const internal = utils?.[LIVE_QUERY_INTERNAL] throw new DuplicateKeySyncError(key, this.id, { hasCustomGetKey: internal?.hasCustomGetKey ?? false, hasJoins: internal?.hasJoins ?? false, diff --git a/packages/db/src/errors.ts b/packages/db/src/errors.ts index 472fcabd12..0bfd2f9969 100644 --- a/packages/db/src/errors.ts +++ b/packages/db/src/errors.ts @@ -442,6 +442,16 @@ export class QueryCompilationError extends TanStackDBError { } } +export class UnsafeAliasPathError extends QueryCompilationError { + constructor(segment: string) { + super( + `Unsafe alias path segment "${segment}" is not allowed in .select(). ` + + `Aliases must not contain "__proto__", "prototype", or "constructor".`, + ) + this.name = `UnsafeAliasPathError` + } +} + export class DistinctRequiresSelectError extends QueryCompilationError { constructor() { super(`DISTINCT requires a SELECT clause.`) diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index ec1e229665..71e264d712 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -10,6 +10,7 @@ export * from './types' export * from './proxy' export * from './query/index.js' export * from './optimistic-action' +export * from './live-query-adapter' export * from './local-only' export * from './local-storage' export * from './errors' @@ -80,6 +81,9 @@ export { type EffectQueryInput, } from './query/effect.js' +// UUID helper (safe in non-secure browser contexts, see #1541) +export { safeRandomUUID } from './utils/uuid.js' + // Re-export some stuff explicitly to ensure the type & value is exported export type { Collection } from './collection/index.js' export { IR } diff --git a/packages/db/src/indexes/base-index.ts b/packages/db/src/indexes/base-index.ts index 4346450f90..945221e6fa 100644 --- a/packages/db/src/indexes/base-index.ts +++ b/packages/db/src/indexes/base-index.ts @@ -68,6 +68,15 @@ export interface IndexInterface< supports: (operation: IndexOperation) => boolean + /** + * Whether range lookups (gt/gte/lt/lte) on this index can be trusted to + * return every matching key. Range traversal relies on the index ordering, so + * it is unsafe when the index uses a custom comparator, whose order may not + * match the WHERE evaluator's relational operators. Callers must fall back to + * a full scan when this is `false`. + */ + get supportsRangeOptimization(): boolean + matchesField: (fieldPath: Array) => boolean matchesCompareOptions: (compareOptions: CompareOptions) => boolean matchesDirection: (direction: OrderByDirection) => boolean @@ -90,6 +99,11 @@ export abstract class BaseIndex< protected totalLookupTime = 0 protected lastUpdated = new Date() protected compareOptions: CompareOptions + /** + * Set by subclasses when constructed with a user-supplied comparator, whose + * ordering may not match the WHERE evaluator's relational operators. + */ + protected hasCustomComparator = false constructor( id: number, @@ -144,6 +158,10 @@ export abstract class BaseIndex< return this.supportedOperations.has(operation) } + get supportsRangeOptimization(): boolean { + return !this.hasCustomComparator + } + matchesField(fieldPath: Array): boolean { return ( this.expression.type === `ref` && diff --git a/packages/db/src/indexes/basic-index.ts b/packages/db/src/indexes/basic-index.ts index b80f7fb439..b9b06d1925 100644 --- a/packages/db/src/indexes/basic-index.ts +++ b/packages/db/src/indexes/basic-index.ts @@ -65,6 +65,7 @@ export class BasicIndex< ) { super(id, expression, name, options) this.compareFn = options?.compareFn ?? defaultComparator + this.hasCustomComparator = options?.compareFn != null if (options?.compareOptions) { this.compareOptions = options!.compareOptions } diff --git a/packages/db/src/indexes/btree-index.ts b/packages/db/src/indexes/btree-index.ts index 17608950a7..8b92095f01 100644 --- a/packages/db/src/indexes/btree-index.ts +++ b/packages/db/src/indexes/btree-index.ts @@ -62,6 +62,7 @@ export class BTreeIndex< // Get the base compare function const baseCompareFn = options?.compareFn ?? defaultComparator + this.hasCustomComparator = options?.compareFn != null // Wrap it to denormalize sentinels before comparison // This ensures UNDEFINED_SENTINEL is converted back to undefined @@ -247,7 +248,16 @@ export class BTreeIndex< toKey, toInclusive, (indexedValue, _) => { - if (!fromInclusive && this.compareFn(indexedValue, from) === 0) { + // Only exclude the boundary when an exclusive lower bound was + // actually provided. Without a `from` bound, `fromKey` defaults to + // the minimum key and must not be dropped. Compare against the + // normalized key since indexed values are stored normalized + // (e.g. dates as timestamps), so the raw `from` would never match. + if ( + hasFrom && + !fromInclusive && + this.compareFn(indexedValue, fromKey) === 0 + ) { // the B+ tree `forRange` method does not support exclusive lower bounds // so we need to exclude it manually return diff --git a/packages/db/src/indexes/reverse-index.ts b/packages/db/src/indexes/reverse-index.ts index 8999b2801c..6ca61636e1 100644 --- a/packages/db/src/indexes/reverse-index.ts +++ b/packages/db/src/indexes/reverse-index.ts @@ -73,6 +73,10 @@ export class ReverseIndex< return this.originalIndex.supports(operation) } + get supportsRangeOptimization(): boolean { + return this.originalIndex.supportsRangeOptimization + } + matchesField(fieldPath: Array): boolean { return this.originalIndex.matchesField(fieldPath) } diff --git a/packages/db/src/live-query-adapter.ts b/packages/db/src/live-query-adapter.ts new file mode 100644 index 0000000000..b13001d0a8 --- /dev/null +++ b/packages/db/src/live-query-adapter.ts @@ -0,0 +1,68 @@ +import type { Collection } from './collection/index.js' +import type { CollectionStatus } from './types.js' + +/** + * Shared helpers for the first-party framework adapters (`@tanstack/react-db`, + * `@tanstack/vue-db`, `@tanstack/svelte-db`, `@tanstack/solid-db`, + * `@tanstack/angular-db`). + * + * These centralize small pieces of logic every adapter used to duplicate, so + * they stay consistent across frameworks. They are intended for the official + * adapters; treat them as unstable for external use. + */ + +/** + * Structural check for a live-query/`Collection` instance. + * + * Uses duck typing rather than `instanceof CollectionImpl` on purpose: adapters + * and core can resolve to different copies of `@tanstack/db` (dual-package / + * multi-realm), where `instanceof` gives false negatives. The three methods + * below uniquely identify a Collection. + */ +export function isCollection( + value: unknown, +): value is Collection { + return ( + typeof value === `object` && + value !== null && + typeof (value as any).subscribeChanges === `function` && + typeof (value as any).startSyncImmediate === `function` && + typeof (value as any).id === `string` + ) +} + +/** Whether a collection yields a single result (`findOne`) rather than an array. */ +export function isSingleResultCollection( + collection: Collection, +): boolean { + return ( + (collection.config as { singleResult?: boolean } | undefined) + ?.singleResult === true + ) +} + +/** The derived boolean status flags every adapter exposes for a query. */ +export interface LiveQueryStatusFlags { + isLoading: boolean + isReady: boolean + isIdle: boolean + isError: boolean + isCleanedUp: boolean +} + +/** + * Derive the boolean status flags from a collection status. Adapters represent + * a disabled query separately (with `isReady: true`); this covers the real + * `CollectionStatus` values. + */ +export function getLiveQueryStatusFlags( + status: CollectionStatus, +): LiveQueryStatusFlags { + return { + isLoading: status === `loading`, + isReady: status === `ready`, + isIdle: status === `idle`, + isError: status === `error`, + isCleanedUp: status === `cleaned-up`, + } +} diff --git a/packages/db/src/local-only.ts b/packages/db/src/local-only.ts index d3a0a7f2ca..afcf3c9a76 100644 --- a/packages/db/src/local-only.ts +++ b/packages/db/src/local-only.ts @@ -1,3 +1,4 @@ +import { safeRandomUUID } from './utils/uuid' import type { BaseCollectionConfig, CollectionConfig, @@ -182,7 +183,7 @@ export function localOnlyCollectionOptions< const { initialData, onInsert, onUpdate, onDelete, id, ...restConfig } = config - const collectionId = id ?? crypto.randomUUID() + const collectionId = id ?? safeRandomUUID() // Create the sync configuration with transaction confirmation capability const syncResult = createLocalOnlySync(initialData) diff --git a/packages/db/src/local-storage.ts b/packages/db/src/local-storage.ts index 3060b7ec61..05ad388d7c 100644 --- a/packages/db/src/local-storage.ts +++ b/packages/db/src/local-storage.ts @@ -1,3 +1,4 @@ +import { safeRandomUUID } from './utils/uuid' import { InvalidStorageDataFormatError, InvalidStorageObjectFormatError, @@ -149,7 +150,7 @@ function validateJsonSerializable( * @returns A unique identifier string for tracking data versions */ function generateUuid(): string { - return crypto.randomUUID() + return safeRandomUUID() } /** diff --git a/packages/db/src/query/builder/functions.ts b/packages/db/src/query/builder/functions.ts index 47e190162a..84c26e5ca4 100644 --- a/packages/db/src/query/builder/functions.ts +++ b/packages/db/src/query/builder/functions.ts @@ -125,31 +125,12 @@ type MapToNumber = T extends string | Array ? null : T -// Helper type for binary numeric operations (combines nullability of both operands) -type BinaryNumericReturnType = - ExtractType extends infer U1 - ? ExtractType extends infer U2 - ? U1 extends number - ? U2 extends number - ? BasicExpression - : U2 extends number | undefined - ? BasicExpression - : U2 extends number | null - ? BasicExpression - : BasicExpression - : U1 extends number | undefined - ? U2 extends number - ? BasicExpression - : U2 extends number | undefined - ? BasicExpression - : BasicExpression - : U1 extends number | null - ? U2 extends number - ? BasicExpression - : BasicExpression - : BasicExpression - : BasicExpression - : BasicExpression +// Helper type for binary numeric operations. +// Runtime coalesces nullish operands to 0 for these operations, so nullable +// operands don't make the result nullable. +type BinaryNumericReturnType = BasicExpression + +type DivideReturnType = BasicExpression // Operators @@ -620,11 +601,41 @@ export function caseWhen(...args: Array): any { export function add( left: T1, right: T2, -): BinaryNumericReturnType { +): BinaryNumericReturnType { return new Func(`add`, [ toExpression(left), toExpression(right), - ]) as BinaryNumericReturnType + ]) as BinaryNumericReturnType +} + +export function subtract( + left: T1, + right: T2, +): BinaryNumericReturnType { + return new Func(`subtract`, [ + toExpression(left), + toExpression(right), + ]) as BinaryNumericReturnType +} + +export function multiply( + left: T1, + right: T2, +): BinaryNumericReturnType { + return new Func(`multiply`, [ + toExpression(left), + toExpression(right), + ]) as BinaryNumericReturnType +} + +export function divide( + left: T1, + right: T2, +): DivideReturnType { + return new Func(`divide`, [ + toExpression(left), + toExpression(right), + ]) as DivideReturnType } // Aggregates @@ -690,6 +701,9 @@ export const operators = [ `concat`, // Numeric functions `add`, + `subtract`, + `multiply`, + `divide`, // Utility functions `coalesce`, `caseWhen`, diff --git a/packages/db/src/query/builder/types.ts b/packages/db/src/query/builder/types.ts index ac6a95dac4..f8cdbcabe4 100644 --- a/packages/db/src/query/builder/types.ts +++ b/packages/db/src/query/builder/types.ts @@ -117,7 +117,7 @@ export type SchemaFromSource = Prettify<{ [K in keyof T]: T[K] extends CollectionImpl ? InferCollectionType : T[K] extends QueryBuilder - ? GetResult + ? GetRawResult : never }> @@ -478,8 +478,59 @@ type ResultTypeFromCaseWhen = T extends unknown ? ResultTypeFromSelectValue : never -// Extract Ref or subobject with a spread or a Ref -type ExtractRef = Prettify>> +// Extract Ref or subobject with a spread or a Ref. +type ExtractRef = T extends unknown + ? IsTrueRef extends true + ? T extends RefLeaf + ? IsNullableRef extends true + ? DeepNullable + : U + : never + : Prettify>> + : never + +// A "true" Ref is one that is structurally equivalent to the canonical +// `Ref` shape the query builder produces for its underlying user type +// `U` (taking the ref's own nullability into account). When `T` is a true +// ref, `ExtractRef` can safely return `U` directly; otherwise it must fall +// through to the recursive projection. +// +// Checking only that `T` has "no extra keys" beyond `keyof U` (plus the +// brand/virtual props) is not sufficient. A spread-derived object can keep +// exactly the keys of `U` while: +// - changing a field's type, e.g. `{ ...u, code: u.slug }`, or +// - dropping an optional key, e.g. `const { nickname, ...rest } = u`. +// Both must be recursively projected, not collapsed back to `U`. We +// therefore require strict structural equivalence against the canonical ref +// shape rather than a one-directional key-subset check. +type IsTrueRef = + T extends RefLeaf + ? RefShapeMatches>> extends true + ? true + : false + : false + +// Strict structural equivalence between two ref shapes. Unlike plain +// bidirectional assignability, this is sensitive to *key presence* — an +// object that drops an optional key (e.g. `const { nickname, ...rest } = u`) +// is not considered equal to one that keeps `nickname?`, even though the two +// remain mutually assignable. A direct ref (`u.document`, a union member, +// etc.) is exactly the canonical `Ref` shape and matches here, so it returns +// `U` via the fast path; any spread-derived object differs (changed field +// types, dropped keys, or stripped `readonly` modifiers) and instead falls +// through to the recursive projection, which reconstructs the correct type. +type RefShapeMatches = + (() => G extends A ? 1 : 2) extends () => G extends B ? 1 : 2 + ? true + : false + +// Propagate nullable-join semantics into the user-data shape. +type DeepNullable = + T extends Record + ? IsPlainObject extends true + ? { [K in keyof T]: DeepNullable } + : T | undefined + : T | undefined // Helper type to extract the underlying type from various expression types type ExtractExpressionType = @@ -611,8 +662,11 @@ type ValueOfUnion = T extends unknown ? T[K] : never : never -type RefForContextValue = - IsPlainObject extends true ? Ref : RefLeaf +type RefForContextValue = T extends unknown + ? IsPlainObject extends true + ? Ref + : RefLeaf + : never type RefsSchemaForContext = IsExactlyUndefined extends true ? TContext[`schema`] @@ -770,7 +824,11 @@ type VirtualPropsRef = { * select(({ user }) => ({ ...user })) // Returns User type, not Ref types * ``` */ -export type Ref = { +export type Ref = T extends unknown + ? RefBranch + : never + +type RefBranch = { [K in keyof T]: IsNonExactOptional extends true ? IsNonExactNullable extends true ? // Both optional and nullable diff --git a/packages/db/src/query/compiler/evaluators.ts b/packages/db/src/query/compiler/evaluators.ts index 929ac56dfb..fa2e90725d 100644 --- a/packages/db/src/query/compiler/evaluators.ts +++ b/packages/db/src/query/compiler/evaluators.ts @@ -3,7 +3,11 @@ import { UnknownExpressionTypeError, UnknownFunctionError, } from '../../errors.js' -import { areValuesEqual, normalizeValue } from '../../utils/comparison.js' +import { + areValuesEqual, + isUnorderable, + normalizeValue, +} from '../../utils/comparison.js' import type { BasicExpression, Func, PropRef } from '../ir.js' import type { NamespacedRow } from '../../types.js' @@ -14,6 +18,19 @@ function isUnknown(value: any): boolean { return value === null || value === undefined } +/** + * Equality that follows PostgreSQL float semantics for `NaN`/invalid Dates: + * such values are equal to one another and unequal to anything else. For all + * other values it defers to {@link areValuesEqual}. Operands must not be + * null/undefined (callers handle UNKNOWN first). + */ +function valuesEqual(a: any, b: any): boolean { + if (isUnorderable(a) || isUnorderable(b)) { + return isUnorderable(a) && isUnorderable(b) + } + return areValuesEqual(a, b) +} + function toDateValue(value: any): Date | null { if (value instanceof Date) { return Number.isNaN(value.getTime()) ? null : value @@ -233,8 +250,9 @@ function compileFunction(func: Func, isSingleRow: boolean): (data: any) => any { if (isUnknown(a) || isUnknown(b)) { return null } - // Use areValuesEqual for proper Uint8Array/Buffer comparison - return areValuesEqual(a, b) + // NaN/invalid Dates are equal to one another (PostgreSQL semantics); + // otherwise use areValuesEqual for proper Uint8Array/Buffer comparison + return valuesEqual(a, b) } } case `gt`: { @@ -247,6 +265,11 @@ function compileFunction(func: Func, isSingleRow: boolean): (data: any) => any { if (isUnknown(a) || isUnknown(b)) { return null } + // NaN/invalid Dates sort greater than every other value, and are equal + // to one another (PostgreSQL semantics) + if (isUnorderable(a) || isUnorderable(b)) { + return isUnorderable(a) && !isUnorderable(b) + } return a > b } } @@ -260,6 +283,9 @@ function compileFunction(func: Func, isSingleRow: boolean): (data: any) => any { if (isUnknown(a) || isUnknown(b)) { return null } + if (isUnorderable(a) || isUnorderable(b)) { + return isUnorderable(a) + } return a >= b } } @@ -273,6 +299,9 @@ function compileFunction(func: Func, isSingleRow: boolean): (data: any) => any { if (isUnknown(a) || isUnknown(b)) { return null } + if (isUnorderable(a) || isUnorderable(b)) { + return isUnorderable(b) && !isUnorderable(a) + } return a < b } } @@ -286,6 +315,9 @@ function compileFunction(func: Func, isSingleRow: boolean): (data: any) => any { if (isUnknown(a) || isUnknown(b)) { return null } + if (isUnorderable(a) || isUnorderable(b)) { + return isUnorderable(b) + } return a <= b } } @@ -370,7 +402,7 @@ function compileFunction(func: Func, isSingleRow: boolean): (data: any) => any { if (!Array.isArray(array)) { return false } - return array.some((item) => normalizeValue(item) === value) + return array.some((item) => valuesEqual(normalizeValue(item), value)) } } diff --git a/packages/db/src/query/compiler/lazy-targets.ts b/packages/db/src/query/compiler/lazy-targets.ts index ee37c144f8..f8affd1e63 100644 --- a/packages/db/src/query/compiler/lazy-targets.ts +++ b/packages/db/src/query/compiler/lazy-targets.ts @@ -49,9 +49,15 @@ export function getLazyLoadTargets( return [] } + // The subscription we drive lazy loading through must be the one for the + // collection the join key actually resolves to. When the key traces through a + // subquery's select into a *joined* source, that collection differs from the + // subquery's from clause (which is what `aliasRemapping[lazyAlias]` yields), + // so prefer the alias reported by `followRef`. Fall back to the from-clause + // remapping when the key resolves directly to the from source. return [ { - alias: aliasRemapping[lazyAlias] || lazyAlias, + alias: followRefResult.alias || aliasRemapping[lazyAlias] || lazyAlias, collection: followRefResult.collection, path: followRefResult.path, }, diff --git a/packages/db/src/query/compiler/select.ts b/packages/db/src/query/compiler/select.ts index e257b09ca4..88ec842915 100644 --- a/packages/db/src/query/compiler/select.ts +++ b/packages/db/src/query/compiler/select.ts @@ -5,7 +5,10 @@ import { Value as ValClass, isExpressionLike, } from '../ir.js' -import { AggregateNotSupportedError } from '../../errors.js' +import { + AggregateNotSupportedError, + UnsafeAliasPathError, +} from '../../errors.js' import { compileExpression, isCaseWhenConditionTrue } from './evaluators.js' import { containsAggregate } from './group-by.js' import type { @@ -39,6 +42,16 @@ function unwrapVal(input: any): any { return input } +const UNSAFE_ALIAS_SEGMENTS = new Set([`__proto__`, `prototype`, `constructor`]) + +function assertSafeAliasSegments(segments: ReadonlyArray): void { + for (const seg of segments) { + if (UNSAFE_ALIAS_SEGMENTS.has(seg)) { + throw new UnsafeAliasPathError(seg) + } + } +} + /** * Processes a merge operation by merging source values into the target path */ @@ -47,6 +60,7 @@ function processMerge( namespacedRow: NamespacedRow, selectResults: Record, ): void { + assertSafeAliasSegments(op.targetPath) const value = op.source(namespacedRow) if (value && typeof value === `object`) { // Ensure target object exists @@ -89,6 +103,7 @@ function processNonMergeOp( ): void { // Support nested alias paths like "meta.author.name" const path = op.alias.split(`.`) + assertSafeAliasSegments(path) if (path.length === 1) { selectResults[op.alias] = op.compiled(namespacedRow) } else { @@ -283,6 +298,9 @@ function addFromObject( ops: Array, ) { for (const [key, value] of Object.entries(obj)) { + if (!key.startsWith(`__SPREAD_SENTINEL__`)) { + assertSafeAliasSegments(key.split(`.`)) + } if (key.startsWith(`__SPREAD_SENTINEL__`)) { const rest = key.slice(`__SPREAD_SENTINEL__`.length) const splitIndex = rest.lastIndexOf(`__`) diff --git a/packages/db/src/query/index.ts b/packages/db/src/query/index.ts index e23475bc25..8cda812b3f 100644 --- a/packages/db/src/query/index.ts +++ b/packages/db/src/query/index.ts @@ -59,6 +59,9 @@ export { coalesce, caseWhen, add, + subtract, + multiply, + divide, // Aggregates count, avg, diff --git a/packages/db/src/query/ir.ts b/packages/db/src/query/ir.ts index 48019a23b2..0cfb15ea50 100644 --- a/packages/db/src/query/ir.ts +++ b/packages/db/src/query/ir.ts @@ -323,13 +323,17 @@ function getRefFromAlias( /** * Follows the given reference in a query * until its finds the root field the reference points to. - * @returns The collection, its alias, and the path to the root field in this collection + * @returns The collection, its alias, and the path to the root field in this collection. + * `alias` is the alias under which the resolved collection is referenced in the + * query it was reached from (when the ref crosses into a joined source). It is + * left undefined when the ref simply resolves to a field on the passed-in + * `collection`, in which case the caller already knows the alias. */ export function followRef( query: QueryIR, ref: PropRef, collection: Collection, -): { collection: Collection; path: Array } | void { +): { collection: Collection; path: Array; alias?: string } | void { if (ref.path.length === 0) { return } @@ -365,8 +369,10 @@ export function followRef( } else { // This is a reference to a collection // we can't follow it further - // so the field must be on the collection itself - return { collection: aliasRef.collection, path: rest } + // so the field must be on the collection itself. + // Report the alias too: when the ref crossed a join, this is the source + // that actually holds the field (which may differ from the from clause). + return { collection: aliasRef.collection, path: rest, alias } } } } diff --git a/packages/db/src/query/live/collection-config-builder.ts b/packages/db/src/query/live/collection-config-builder.ts index b40e6e431a..4d80b3fe17 100644 --- a/packages/db/src/query/live/collection-config-builder.ts +++ b/packages/db/src/query/live/collection-config-builder.ts @@ -248,7 +248,7 @@ export class CollectionConfigBuilder< sync: this.getSyncConfig(), compare: this.compare, defaultStringCollation: this.compareOptions, - gcTime: this.config.gcTime || 5000, // 5 seconds by default for live queries + gcTime: this.config.gcTime ?? 5000, // 5 seconds by default for live queries schema: this.config.schema, onInsert: this.config.onInsert, onUpdate: this.config.onUpdate, @@ -596,6 +596,8 @@ export class CollectionConfigBuilder< private syncFn(config: SyncMethods) { // Store reference to the live query collection for error state transitions this.liveQueryCollection = config.collection + // Reset error state from any previous sync session so a restarted sync can become ready again. + this.isInErrorState = false // Store config and syncState as instance properties for the duration of this sync session this.currentSyncConfig = config @@ -907,8 +909,6 @@ export class CollectionConfigBuilder< entry.childCompilationResult.includes, syncState, ) - state.nestedRoutingIndex = new Map() - state.nestedRoutingReverseIndex = new Map() } return state @@ -1150,6 +1150,28 @@ function createOrderByComparator( } } +type SnapshotRow = { + value: any + orderByIndex: string | undefined + /** Net multiplicity (inserts − deletes) currently materialized for this row */ + count: number +} + +type NestedRouteIndex = Map< + unknown, + Map>> +> + +type NestedRouteReverseIndex = Map< + IncludesOutputState, + Map> +> + +type NestedRouteChildToNested = Map< + IncludesOutputState, + Map> +> + /** * Shared buffer setup for a single nested includes level. * Pipeline output writes into the buffer; during flush the buffer is drained @@ -1159,6 +1181,23 @@ type NestedIncludesSetup = { compilationResult: IncludesCompilationResult /** Shared buffer: nestedCorrelationKey → Map */ buffer: Map>> + /** + * Cumulative net-present grandchild rows per nested correlation key. The + * buffer holds only deltas since the last drain and is cleared once drained, + * so a parent group that starts referencing an existing correlation key + * *after* the rows were already drained (the pipeline does not re-emit them) + * would otherwise see nothing. The snapshot lets such late-arriving parent + * groups be seeded with the rows their siblings already received. + */ + snapshot: Map> + /** + * Shared route store for this shared buffer. Routes target concrete + * IncludesOutputState instances so one emitted child row can fan out across + * every per-entry state that references the same nested correlation key. + */ + routingIndex: NestedRouteIndex + routingReverseIndex: NestedRouteReverseIndex + routingChildToNested: NestedRouteChildToNested /** For 3+ levels of nesting */ nestedSetups?: Array } @@ -1184,10 +1223,6 @@ type IncludesOutputState = { correlationToParentKeys: Map> /** Shared nested pipeline setups (one per nested includes level) */ nestedSetups?: Array - /** nestedCorrelationKey → parentCorrelationKey */ - nestedRoutingIndex?: Map - /** parentCorrelationKey → Set */ - nestedRoutingReverseIndex?: Map> } type ChildCollectionEntry = { @@ -1298,6 +1333,10 @@ function setupNestedPipelines( const setup: NestedIncludesSetup = { compilationResult: entry, buffer, + snapshot: new Map(), + routingIndex: new Map(), + routingReverseIndex: new Map(), + routingChildToNested: new Map(), } // Recursively set up deeper levels @@ -1334,14 +1373,99 @@ function createPerEntryIncludesStates( if (setup.nestedSetups) { state.nestedSetups = setup.nestedSetups - state.nestedRoutingIndex = new Map() - state.nestedRoutingReverseIndex = new Map() } return state }) } +function cloneSnapshotValue(value: T): T { + if (value == null || typeof value !== `object`) { + return value + } + + return (Array.isArray(value) ? [...value] : { ...value }) as T +} + +/** + * Folds a drained delta into a nested setup's cumulative snapshot, tracking the + * net multiplicity per child row and dropping rows (and empty keys) once their + * net count reaches zero. + */ +function accumulateSnapshot( + setup: NestedIncludesSetup, + nestedCorrelationKey: unknown, + childChanges: Map>, +): void { + let snap = setup.snapshot.get(nestedCorrelationKey) + if (!snap) { + snap = new Map() + setup.snapshot.set(nestedCorrelationKey, snap) + } + + for (const [childKey, changes] of childChanges) { + let row = snap.get(childKey) + if (!row) { + row = { + value: cloneSnapshotValue(changes.value), + orderByIndex: changes.orderByIndex, + count: 0, + } + snap.set(childKey, row) + } + row.count += changes.inserts - changes.deletes + if (changes.inserts > 0) { + row.value = cloneSnapshotValue(changes.value) + if (changes.orderByIndex !== undefined) { + row.orderByIndex = changes.orderByIndex + } + } + if (row.count <= 0) { + snap.delete(childKey) + } + } + + if (snap.size === 0) { + setup.snapshot.delete(nestedCorrelationKey) + } +} + +/** + * Seeds a parent group's per-entry state with the rows already materialized for + * a nested correlation key. Used when a parent group starts referencing a key + * whose rows were drained (and cleared from the buffer) in an earlier flush, so + * the pipeline will not re-emit them. + */ +function seedParentFromSnapshot( + state: IncludesOutputState, + setupIndex: number, + parentCorrelationKey: unknown, + nestedCorrelationKey: unknown, +): void { + const setup = state.nestedSetups![setupIndex]! + const snap = setup.snapshot.get(nestedCorrelationKey) + if (!snap || snap.size === 0) return + + const entry = state.childRegistry.get(parentCorrelationKey) + if (!entry || !entry.includesStates) return + + const entryState = entry.includesStates[setupIndex]! + let byChild = entryState.pendingChildChanges.get(nestedCorrelationKey) + if (!byChild) { + byChild = new Map() + entryState.pendingChildChanges.set(nestedCorrelationKey, byChild) + } + for (const [childKey, row] of snap) { + if (byChild.has(childKey)) continue + byChild.set(childKey, { + deletes: 0, + inserts: row.count, + value: cloneSnapshotValue(row.value), + orderByIndex: row.orderByIndex, + }) + } +} + /** * Drains shared buffers into per-entry states using the routing index. * Returns the set of parent correlation keys that had changes routed to them. @@ -1351,48 +1475,68 @@ function drainNestedBuffers(state: IncludesOutputState): Set { if (!state.nestedSetups) return dirtyCorrelationKeys - for (let i = 0; i < state.nestedSetups.length; i++) { - const setup = state.nestedSetups[i]! + for (const setup of state.nestedSetups) { const toDelete: Array = [] for (const [nestedCorrelationKey, childChanges] of setup.buffer) { - const parentCorrelationKey = - state.nestedRoutingIndex!.get(nestedCorrelationKey) - if (parentCorrelationKey === undefined) { + const stateRoutes = setup.routingIndex.get(nestedCorrelationKey) + if (stateRoutes === undefined || stateRoutes.size === 0) { // Unroutable — parent not yet seen; keep in buffer continue } - const entry = state.childRegistry.get(parentCorrelationKey) - if (!entry || !entry.includesStates) { - continue - } + // A single nested correlation key can map to multiple parent groups when + // sibling parents share the same correlation value, and at depth 4+ those + // parents may live in different per-entry states. Fan the buffered changes + // out to every ready target before clearing the shared buffer entry. + let routedToAny = false + for (const [targetState, parentRoutes] of stateRoutes) { + const targetSetupIndex = targetState.nestedSetups?.indexOf(setup) ?? -1 + if (targetSetupIndex < 0) continue + + for (const parentCorrelationKey of parentRoutes.keys()) { + const entry = targetState.childRegistry.get(parentCorrelationKey) + if (!entry || !entry.includesStates) { + continue + } - // Route changes into this entry's per-entry state at position i - const entryState = entry.includesStates[i]! - for (const [childKey, changes] of childChanges) { - let byChild = entryState.pendingChildChanges.get(nestedCorrelationKey) - if (!byChild) { - byChild = new Map() - entryState.pendingChildChanges.set(nestedCorrelationKey, byChild) - } - const existing = byChild.get(childKey) - if (existing) { - existing.inserts += changes.inserts - existing.deletes += changes.deletes - if (changes.inserts > 0) { - existing.value = changes.value - if (changes.orderByIndex !== undefined) { - existing.orderByIndex = changes.orderByIndex + // Route changes into this entry's per-entry state at the same setup. + const entryState = entry.includesStates[targetSetupIndex]! + for (const [childKey, changes] of childChanges) { + let byChild = + entryState.pendingChildChanges.get(nestedCorrelationKey) + if (!byChild) { + byChild = new Map() + entryState.pendingChildChanges.set(nestedCorrelationKey, byChild) + } + const existing = byChild.get(childKey) + if (existing) { + existing.inserts += changes.inserts + existing.deletes += changes.deletes + if (changes.inserts > 0) { + existing.value = changes.value + if (changes.orderByIndex !== undefined) { + existing.orderByIndex = changes.orderByIndex + } + } + } else { + byChild.set(childKey, { ...changes }) } } - } else { - byChild.set(childKey, { ...changes }) + + if (targetState === state) { + dirtyCorrelationKeys.add(parentCorrelationKey) + } + routedToAny = true } } - dirtyCorrelationKeys.add(parentCorrelationKey) - toDelete.push(nestedCorrelationKey) + if (routedToAny) { + // Fold the drained delta into the cumulative snapshot so a parent group + // that starts referencing this nested key later can be seeded with it. + accumulateSnapshot(setup, nestedCorrelationKey, childChanges) + toDelete.push(nestedCorrelationKey) + } } for (const key of toDelete) { @@ -1408,6 +1552,52 @@ function drainNestedBuffers(state: IncludesOutputState): Set { * Maps nested correlation keys to parent correlation keys so that * grandchild changes can be routed to the correct per-entry state. */ +/** + * Removes a single child row's reference to a nested routing key from a parent + * group's route, dropping the parent (and the nested key, and the reverse-index + * entry) once no child row in the group references the key anymore. + */ +function removeChildKeyFromRoute( + setup: NestedIncludesSetup, + state: IncludesOutputState, + correlationKey: unknown, + nestedRoutingKey: unknown, + childKey: unknown, +): void { + const stateRoutes = setup.routingIndex.get(nestedRoutingKey) + const parents = stateRoutes?.get(state) + const childKeys = parents?.get(correlationKey) + if (!parents || !childKeys) return + + childKeys.delete(childKey) + // Only drop the parent group from the route once its last child row + // referencing this nested key is gone — a surviving sibling in the same + // parent group must keep receiving grandchild changes. + if (childKeys.size === 0) { + parents.delete(correlationKey) + if (parents.size === 0) { + stateRoutes!.delete(state) + if (stateRoutes!.size === 0) { + setup.routingIndex.delete(nestedRoutingKey) + } + } + // The reverse index tracks parent → nested keys at group granularity, so + // only drop the entry when no child row in this parent group references the + // nested key anymore. + const reverse = setup.routingReverseIndex.get(state) + const reverseSet = reverse?.get(correlationKey) + if (reverseSet) { + reverseSet.delete(nestedRoutingKey) + if (reverseSet.size === 0) { + reverse!.delete(correlationKey) + if (reverse!.size === 0) { + setup.routingReverseIndex.delete(state) + } + } + } + } +} + function updateRoutingIndex( state: IncludesOutputState, correlationKey: unknown, @@ -1415,8 +1605,14 @@ function updateRoutingIndex( ): void { if (!state.nestedSetups) return - for (const setup of state.nestedSetups) { - for (const [, change] of childChanges) { + for (let i = 0; i < state.nestedSetups.length; i++) { + const setup = state.nestedSetups[i]! + let childToNested = setup.routingChildToNested.get(state) + if (!childToNested) { + childToNested = new Map() + setup.routingChildToNested.set(state, childToNested) + } + for (const [childKey, change] of childChanges) { if (change.inserts > 0) { // Read the nested routing key from the INCLUDES_ROUTING stamp. // Must use the composite routing key (not raw correlationKey) to match @@ -1430,14 +1626,83 @@ function updateRoutingIndex( nestedParentContext, ) + // An update (inserts > 0 && deletes > 0) can change a child row's nested + // correlation key (e.g. a price range's regionId changes). The change + // only carries the NEW key, so drop the row's previous reference for + // THIS setup using the recorded mapping before re-routing it. + // + // This relies on the compiler stamping the FULL INCLUDES_ROUTING map on + // every emitted row (one entry per nested include field), so for an + // unrelated nested include the recomputed nestedRoutingKey equals the + // recorded one and the guard below is a no-op — a change to one nested + // include never disturbs the recorded key of another on the same row. + const perParent = childToNested.get(correlationKey) + const prevNestedKey = perParent?.get(childKey) + if (prevNestedKey !== undefined && prevNestedKey !== nestedRoutingKey) { + removeChildKeyFromRoute( + setup, + state, + correlationKey, + prevNestedKey, + childKey, + ) + perParent!.delete(childKey) + } + if (nestedCorrelationKey != null) { - state.nestedRoutingIndex!.set(nestedRoutingKey, correlationKey) - let reverseSet = state.nestedRoutingReverseIndex!.get(correlationKey) + let stateRoutes = setup.routingIndex.get(nestedRoutingKey) + if (!stateRoutes) { + stateRoutes = new Map() + setup.routingIndex.set(nestedRoutingKey, stateRoutes) + } + let parents = stateRoutes.get(state) + if (!parents) { + parents = new Map() + stateRoutes.set(state, parents) + } + let childKeys = parents.get(correlationKey) + // The parent group is "new" for this nested key only when no child row + // in it referenced the key before; that's the case that needs seeding. + const isNewParent = !childKeys || childKeys.size === 0 + if (!childKeys) { + childKeys = new Set() + parents.set(correlationKey, childKeys) + } + childKeys.add(childKey) + let reverse = setup.routingReverseIndex.get(state) + if (!reverse) { + reverse = new Map() + setup.routingReverseIndex.set(state, reverse) + } + let reverseSet = reverse.get(correlationKey) if (!reverseSet) { reverseSet = new Set() - state.nestedRoutingReverseIndex!.set(correlationKey, reverseSet) + reverse.set(correlationKey, reverseSet) } reverseSet.add(nestedRoutingKey) + + // Record the row's current nested key for this setup so a later update + // that changes it can release the old reference. Reuse perParent when + // it already exists to avoid a second lookup. + let recorded = perParent + if (!recorded) { + recorded = new Map() + childToNested.set(correlationKey, recorded) + } + recorded.set(childKey, nestedRoutingKey) + + // If this parent group is newly associated with a nested key whose + // rows were already drained (and cleared from the buffer) in an + // earlier flush, the pipeline will not re-emit them. Seed this parent + // from the cumulative snapshot so it receives the same rows its + // siblings already have. + if (isNewParent) { + seedParentFromSnapshot(state, i, correlationKey, nestedRoutingKey) + } + } else if (perParent && perParent.size === 0) { + // The row no longer has a nested key (cleared via update) and held no + // others — drop the now-empty per-parent record. + childToNested.delete(correlationKey) } } else if (change.deletes > 0 && change.inserts === 0) { // Remove from routing index @@ -1451,15 +1716,18 @@ function updateRoutingIndex( ) if (nestedCorrelationKey != null) { - state.nestedRoutingIndex!.delete(nestedRoutingKey) - const reverseSet = - state.nestedRoutingReverseIndex!.get(correlationKey) - if (reverseSet) { - reverseSet.delete(nestedRoutingKey) - if (reverseSet.size === 0) { - state.nestedRoutingReverseIndex!.delete(correlationKey) - } - } + removeChildKeyFromRoute( + setup, + state, + correlationKey, + nestedRoutingKey, + childKey, + ) + } + const perParent = childToNested.get(correlationKey) + if (perParent) { + perParent.delete(childKey) + if (perParent.size === 0) childToNested.delete(correlationKey) } } } @@ -1474,14 +1742,40 @@ function cleanRoutingIndexOnDelete( state: IncludesOutputState, correlationKey: unknown, ): void { - if (!state.nestedRoutingReverseIndex) return + if (!state.nestedSetups) return - const nestedKeys = state.nestedRoutingReverseIndex.get(correlationKey) - if (nestedKeys) { + // The whole parent group is gone, so drop it from every nested setup's route + // (along with all the child keys it tracked); other sibling parent groups may + // still reference the same nested correlation key. + for (const setup of state.nestedSetups) { + const reverseIndex = setup.routingReverseIndex.get(state) + const nestedKeys = reverseIndex?.get(correlationKey) + if (!nestedKeys) continue for (const nestedKey of nestedKeys) { - state.nestedRoutingIndex!.delete(nestedKey) + const stateRoutes = setup.routingIndex.get(nestedKey) + const parents = stateRoutes?.get(state) + if (parents) { + parents.delete(correlationKey) + if (parents.size === 0) { + stateRoutes!.delete(state) + if (stateRoutes!.size === 0) { + setup.routingIndex.delete(nestedKey) + } + } + } + } + reverseIndex!.delete(correlationKey) + if (reverseIndex!.size === 0) { + setup.routingReverseIndex.delete(state) + } + + const childToNested = setup.routingChildToNested.get(state) + if (childToNested) { + childToNested.delete(correlationKey) + if (childToNested.size === 0) { + setup.routingChildToNested.delete(state) + } } - state.nestedRoutingReverseIndex.delete(correlationKey) } } @@ -1671,14 +1965,19 @@ function flushIncludesState( if (entry.orderByIndices && change.orderByIndex !== undefined) { entry.orderByIndices.set(change.value, change.orderByIndex) } + const key = entry.syncMethods.collection.getKeyFromItem( + change.value, + ) + const childAlreadyExists = entry.syncMethods.collection.has(key) + if (change.inserts > 0 && change.deletes === 0) { - entry.syncMethods.write({ value: change.value, type: `insert` }) + entry.syncMethods.write({ + value: change.value, + type: childAlreadyExists ? `update` : `insert`, + }) } else if ( change.inserts > change.deletes || - (change.inserts === change.deletes && - entry.syncMethods.collection.has( - entry.syncMethods.collection.getKeyFromItem(change.value), - )) + (change.inserts === change.deletes && childAlreadyExists) ) { entry.syncMethods.write({ value: change.value, type: `update` }) } else if (change.deletes > 0) { @@ -1848,6 +2147,13 @@ function hasPendingIncludesChanges( if (state.pendingChildChanges.size > 0) return true if (state.nestedSetups && hasNestedBufferChanges(state.nestedSetups)) return true + for (const entry of state.childRegistry.values()) { + if ( + entry.includesStates && + hasPendingIncludesChanges(entry.includesStates) + ) + return true + } } return false } diff --git a/packages/db/src/transactions.ts b/packages/db/src/transactions.ts index 84e2bb0d5d..4a3e2f80f2 100644 --- a/packages/db/src/transactions.ts +++ b/packages/db/src/transactions.ts @@ -1,4 +1,5 @@ import { createDeferred } from './deferred' +import { safeRandomUUID } from './utils/uuid' import './duplicate-instance-check' import { MissingMutationFunctionError, @@ -210,6 +211,18 @@ class Transaction> { public state: TransactionState public mutationFn: MutationFn public mutations: Array> + /** + * Deferred that settles when this transaction settles. + * + * Await `isPersisted.promise`, not `isPersisted` itself. The promise resolves + * when the transaction completes successfully and rejects if the transaction + * fails or is rolled back. + * + * For non-empty commits, the mutation function is the normal settlement + * boundary. This does not inherently prove that a backend has uploaded, + * confirmed, or read back the write unless the mutation function waits for + * that backend observation before returning. + */ public isPersisted: Deferred> public autoCommit: boolean public createdAt: Date @@ -224,7 +237,7 @@ class Transaction> { if (typeof config.mutationFn === `undefined`) { throw new MissingMutationFunctionError() } - this.id = config.id ?? crypto.randomUUID() + this.id = config.id ?? safeRandomUUID() this.mutationFn = config.mutationFn this.state = `pending` this.mutations = [] @@ -333,27 +346,39 @@ class Transaction> { * @param mutations - Array of new mutations to apply */ applyMutations(mutations: Array>): void { + // Merge via a globalKey-keyed map rather than a findIndex scan per + // mutation, which is O(n²) for bulk operations (e.g. inserting many rows + // in one call). Map preserves insertion order, matching the previous + // replace-in-place / remove / append semantics. + const merged = new Map>() + for (const mutation of this.mutations) { + merged.set(mutation.globalKey, mutation) + } + for (const newMutation of mutations) { - const existingIndex = this.mutations.findIndex( - (m) => m.globalKey === newMutation.globalKey, - ) + const existingMutation = merged.get(newMutation.globalKey) - if (existingIndex >= 0) { - const existingMutation = this.mutations[existingIndex]! + if (existingMutation) { const mergeResult = mergePendingMutations(existingMutation, newMutation) if (mergeResult === null) { // Remove the mutation (e.g., delete after insert cancels both) - this.mutations.splice(existingIndex, 1) + merged.delete(newMutation.globalKey) } else { // Replace with merged mutation - this.mutations[existingIndex] = mergeResult + merged.set(newMutation.globalKey, mergeResult) } } else { // Insert new mutation - this.mutations.push(newMutation) + merged.set(newMutation.globalKey, newMutation) } } + + // Rebuild in place to preserve the array's identity for external holders + this.mutations.length = 0 + for (const mutation of merged.values()) { + this.mutations.push(mutation) + } } /** diff --git a/packages/db/src/utils/comparison.ts b/packages/db/src/utils/comparison.ts index bf5ac1a913..992f0098c5 100644 --- a/packages/db/src/utils/comparison.ts +++ b/packages/db/src/utils/comparison.ts @@ -17,6 +17,21 @@ function getObjectId(obj: object): number { return id } +/** + * Whether a value has no IEEE-754 natural order: `NaN`, or an invalid Date + * (whose timestamp is `NaN`). The query engine follows PostgreSQL float + * semantics for these values — they are all equal to one another and greater + * than every other (non-null) value — so the comparator and the WHERE + * evaluator treat them explicitly instead of letting `NaN` compare unequal to + * everything (which has no consistent order and cannot be indexed or sorted). + */ +export function isUnorderable(value: any): boolean { + return ( + (typeof value === `number` && Number.isNaN(value)) || + (value instanceof Date && Number.isNaN(value.getTime())) + ) +} + /** * Universal comparison function for all data types * Handles null/undefined, strings, arrays, dates, objects, and primitives @@ -30,6 +45,16 @@ export const ascComparator = (a: any, b: any, opts: CompareOptions): number => { if (a == null) return nulls === `first` ? -1 : 1 if (b == null) return nulls === `first` ? 1 : -1 + // Handle NaN / invalid Dates. Following PostgreSQL float semantics, they are + // all equal and sort greater than every other non-null value. This keeps the + // order total (NaN would otherwise compare equal to everything), so such + // values can be sorted and stored in tree-based indexes. + const aUnordered = isUnorderable(a) + const bUnordered = isUnorderable(b) + if (aUnordered && bUnordered) return 0 + if (aUnordered) return 1 + if (bUnordered) return -1 + // if a and b are both strings, compare them based on locale if (typeof a === `string` && typeof b === `string`) { if (opts.stringSort === `locale`) { diff --git a/packages/db/src/utils/index-optimization.ts b/packages/db/src/utils/index-optimization.ts index 81b111af56..5a52a5ec54 100644 --- a/packages/db/src/utils/index-optimization.ts +++ b/packages/db/src/utils/index-optimization.ts @@ -18,6 +18,7 @@ import { DEFAULT_COMPARE_OPTIONS } from '../utils.js' import { ReverseIndex } from '../indexes/reverse-index.js' import { hasVirtualPropPath } from '../virtual-props.js' +import { makeComparator } from './comparison.js' import type { CompareOptions } from '../query/builder/types.js' import type { IndexInterface, IndexOperation } from '../indexes/base-index.js' import type { BasicExpression } from '../query/ir.js' @@ -29,6 +30,13 @@ import type { CollectionLike } from '../types.js' export interface OptimizationResult { canOptimize: boolean matchingKeys: Set + /** + * Whether `matchingKeys` is exactly the set of keys matching the expression. + * When `false`, the keys are a superset of the true result (some conditions + * could not be served by an index) and each row must be re-checked against + * the full expression before being included in the result. + */ + isExact: boolean } /** @@ -94,6 +102,94 @@ export function unionSets(sets: Array>): Set { return result } +/** + * Whether a value can be matched exactly by an index lookup, i.e. the index + * result for it is not a superset that the caller must re-filter. + * + * Only `null`/`undefined` are inexact: the WHERE evaluator's three-valued logic + * makes any comparison against them UNKNOWN, yet a BTree index stores and + * returns rows with nullish keys (they sort to the nulls end), so a result that + * could include such rows must be re-filtered. + * + * `NaN` and invalid Dates are exact: under the engine's PostgreSQL float + * semantics they are equal to themselves and ordered (greatest non-null value), + * so the evaluator and the index agree on them and no re-filtering is needed. + */ +function isExactComparisonValue(value: unknown): boolean { + return value != null +} + +/** + * Whether the collection orders strings using locale collation. + * + * Under `stringSort: 'locale'` a BTree string index orders values with + * `localeCompare`, but the WHERE evaluator compares strings with JS relational + * operators (code-point order). For range predicates these orders disagree + * (e.g. `'ö' > 'z'` is true in JS but `'ö'` sorts before `'z'` under locale + * `en`), so an index range lookup can omit matching rows. Such omissions cannot + * be recovered by re-filtering, so locale-backed string range predicates must + * not be index-optimized. + */ +function usesLocaleStringSort(collection: CollectionLike): boolean { + const opts = { ...DEFAULT_COMPARE_OPTIONS, ...collection.compareOptions } + return opts.stringSort === `locale` +} + +/** + * Whether a range predicate on this operand would use an index ordering that + * differs from the WHERE evaluator's relational operators, so an index range + * lookup could omit genuine matches that re-filtering cannot recover. + * + * The evaluator compares with JS relational operators (extended with + * PostgreSQL float semantics for `NaN`/invalid Dates). That order matches the + * index comparator for numbers, booleans, bigints, lexically-sorted strings, + * Dates (valid, ordered by time; invalid, ordered as the greatest value) and + * `NaN`. It diverges for locale-sorted strings (localeCompare vs code-point + * order) and for arrays, plain objects, Temporal values and typed arrays + * (recursive/identity ordering vs string coercion). + * + * Note: `null`/`undefined` operands are not handled here — those are superset + * cases handled by re-filtering ({@link isExactComparisonValue}). + */ +function isRangeOrderingDivergent( + value: unknown, + collection: CollectionLike, +): boolean { + switch (typeof value) { + case `number`: + case `bigint`: + case `boolean`: + return false + case `string`: + return usesLocaleStringSort(collection) + case `object`: { + if (value === null) return false + // Dates order consistently with the evaluator: valid Dates by time, and + // invalid Dates as the greatest value under PostgreSQL float semantics. + return !(value instanceof Date) + } + default: + return false + } +} + +/** + * Whether a range predicate (gt/gte/lt/lte) on this operand can be safely + * served by the given index: the operand's domain must order the same way the + * index does, and the index itself must support trustworthy range traversal + * (no custom comparator). + */ +function canRangeOptimize( + value: unknown, + index: IndexInterface, + collection: CollectionLike, +): boolean { + return ( + !isRangeOrderingDivergent(value, collection) && + index.supportsRangeOptimization + ) +} + /** * Optimizes a query expression using available indexes to find matching keys */ @@ -134,7 +230,7 @@ function optimizeQueryRecursive( } } - return { canOptimize: false, matchingKeys: new Set() } + return { canOptimize: false, matchingKeys: new Set(), isExact: false } } /** @@ -167,6 +263,14 @@ export function canOptimizeExpression< return false } +/** + * Result of compound range optimization, including which AND arguments + * were covered by the range query so the caller can process the rest. + */ +interface CompoundRangeResult extends OptimizationResult { + coveredArgIndices: Set +} + /** * Optimizes compound range queries on the same field * Example: WHERE age > 5 AND age < 10 @@ -177,9 +281,14 @@ function optimizeCompoundRangeQuery< >( expression: BasicExpression, collection: CollectionLike, -): OptimizationResult { +): CompoundRangeResult { if (expression.type !== `func` || expression.args.length < 2) { - return { canOptimize: false, matchingKeys: new Set() } + return { + canOptimize: false, + matchingKeys: new Set(), + isExact: false, + coveredArgIndices: new Set(), + } } // Group range operations by field @@ -188,11 +297,12 @@ function optimizeCompoundRangeQuery< Array<{ operation: `gt` | `gte` | `lt` | `lte` value: any + argIndex: number }> >() // Collect all range operations from AND arguments - for (const arg of expression.args) { + for (const [argIndex, arg] of expression.args.entries()) { if (arg.type === `func` && [`gt`, `gte`, `lt`, `lte`].includes(arg.name)) { const rangeOp = arg as any if (rangeOp.args.length === 2) { @@ -238,7 +348,7 @@ function optimizeCompoundRangeQuery< if (!fieldOperations.has(fieldKey)) { fieldOperations.set(fieldKey, []) } - fieldOperations.get(fieldKey)!.push({ operation, value }) + fieldOperations.get(fieldKey)!.push({ operation, value, argIndex }) } } } @@ -250,55 +360,114 @@ function optimizeCompoundRangeQuery< const fieldPath = fieldKey.split(`.`) const index = findIndexForField(collection, fieldPath) + // Only collapse this field into a range query when every bound's domain + // orders the same way the index does and the index supports trustworthy + // range traversal. Otherwise the index may omit matching rows that + // re-filtering cannot recover, so leave the field for a full scan. + if ( + index && + operations.some((op) => !canRangeOptimize(op.value, index, collection)) + ) { + continue + } + if (index && index.supports(`gt`) && index.supports(`lt`)) { - // Build range query options + // Compare values with the same semantics the index uses (dates, + // locale strings, ...), in ascending order since bounds are about + // value order regardless of the index direction + const compare = makeComparator({ + ...DEFAULT_COMPARE_OPTIONS, + ...collection.compareOptions, + direction: `asc`, + }) + + // Build range query options, keeping the strictest bound on each + // side: a larger lower bound (or smaller upper bound) wins, and at + // equal values the exclusive operation wins over the inclusive one. + // `hasFromBound`/`hasToBound` track whether a bound was selected, + // separately from the bound value (which may legitimately be falsy). let from: any = undefined let to: any = undefined + let hasFromBound = false + let hasToBound = false let fromInclusive = true let toInclusive = true + // A comparison against null/undefined is never true, but in an index + // nullish values sort to the nulls end, so a range query cannot + // represent such a bound. Track it and force a re-filter instead of + // claiming the result is exact. (NaN/invalid Dates are ordered and + // comparable under PostgreSQL semantics, so they are real bounds.) + let hasNonComparableBound = false for (const { operation, value } of operations) { + if (!isExactComparisonValue(value)) { + hasNonComparableBound = true + continue + } switch (operation) { case `gt`: - if (from === undefined || value > from) { + case `gte`: { + const cmp = hasFromBound ? compare(value, from) : 1 + if (cmp > 0) { from = value + hasFromBound = true + fromInclusive = operation === `gte` + } else if (cmp === 0 && operation === `gt`) { fromInclusive = false } break - case `gte`: - if (from === undefined || value > from) { - from = value - fromInclusive = true - } - break + } case `lt`: - if (to === undefined || value < to) { + case `lte`: { + const cmp = hasToBound ? compare(value, to) : -1 + if (cmp < 0) { to = value + hasToBound = true + toInclusive = operation === `lte` + } else if (cmp === 0 && operation === `lt`) { toInclusive = false } break - case `lte`: - if (to === undefined || value < to) { - to = value - toInclusive = true - } - break + } } } - const matchingKeys = (index as any).rangeQuery({ - from, - to, - fromInclusive, - toInclusive, - }) - - return { canOptimize: true, matchingKeys } + // Only pass the bounds that were selected: rangeQuery distinguishes + // an absent bound (open-ended) from an explicitly provided one + const rangeOptions: Record = {} + if (hasFromBound) { + rangeOptions.from = from + rangeOptions.fromInclusive = fromInclusive + } + if (hasToBound) { + rangeOptions.to = to + rangeOptions.toInclusive = toInclusive + } + const matchingKeys = (index as any).rangeQuery(rangeOptions) + + return { + canOptimize: true, + matchingKeys, + // The range result is exact only when it cannot include rows with a + // nullish indexed value (which a comparison would reject but the + // index returns, as they sort as the smallest key). That requires a + // non-nullish lower bound to exclude them: without `hasFromBound` + // the range is open at the bottom and captures those rows, and a + // non-comparable bound value (`hasNonComparableBound`) can never + // bound them out. + isExact: hasFromBound && !hasNonComparableBound, + coveredArgIndices: new Set(operations.map((op) => op.argIndex)), + } } } } - return { canOptimize: false, matchingKeys: new Set() } + return { + canOptimize: false, + matchingKeys: new Set(), + isExact: false, + coveredArgIndices: new Set(), + } } /** @@ -312,7 +481,7 @@ function optimizeSimpleComparison< collection: CollectionLike, ): OptimizationResult { if (expression.type !== `func` || expression.args.length !== 2) { - return { canOptimize: false, matchingKeys: new Set() } + return { canOptimize: false, matchingKeys: new Set(), isExact: false } } const leftArg = expression.args[0]! @@ -362,15 +531,46 @@ function optimizeSimpleComparison< // Check if the index supports this operation if (!index.supports(indexOperation)) { - return { canOptimize: false, matchingKeys: new Set() } + return { canOptimize: false, matchingKeys: new Set(), isExact: false } + } + + // A range op can only use the index when the operand's domain orders the + // same way the index does and the index supports trustworthy traversal. + // Otherwise the index may omit matching rows, which re-filtering cannot + // recover, so fall back to a full scan. + if ( + (operation === `gt` || + operation === `gte` || + operation === `lt` || + operation === `lte`) && + !canRangeOptimize(queryValue, index, collection) + ) { + return { canOptimize: false, matchingKeys: new Set(), isExact: false } } const matchingKeys = index.lookup(indexOperation, queryValue) - return { canOptimize: true, matchingKeys } + + // A comparison against a nullish value is never true, but BTree indexes + // store and return rows with nullish keys (they sort to the nulls end). + // Determine whether the index result is exact or a superset that the + // caller must re-filter: + // - eq/gt/gte: a nullish query value matches nothing while the index + // still returns nullish-keyed rows -> inexact. A non-nullish lower + // bound (gt/gte) excludes those bottom-sorted rows, so they stay exact. + // - lt/lte: the open lower bound always includes nullish-keyed rows, + // so the result is conservatively inexact. + // NaN/invalid Dates are exact here: under PostgreSQL float semantics the + // evaluator and the index agree on them (equal to self, greatest). + const isExact = + operation === `lt` || operation === `lte` + ? false + : isExactComparisonValue(queryValue) + + return { canOptimize: true, matchingKeys, isExact } } } - return { canOptimize: false, matchingKeys: new Set() } + return { canOptimize: false, matchingKeys: new Set(), isExact: false } } /** @@ -412,22 +612,41 @@ function optimizeAndExpression( collection: CollectionLike, ): OptimizationResult { if (expression.type !== `func` || expression.args.length < 2) { - return { canOptimize: false, matchingKeys: new Set() } + return { canOptimize: false, matchingKeys: new Set(), isExact: false } } // First, try to optimize compound range queries on the same field + // (e.g. age > 5 AND age < 10 becomes a single range query) const compoundRangeResult = optimizeCompoundRangeQuery(expression, collection) - if (compoundRangeResult.canOptimize) { - return compoundRangeResult - } + const coveredArgIndices = compoundRangeResult.canOptimize + ? compoundRangeResult.coveredArgIndices + : new Set() const results: Array> = [] + if (compoundRangeResult.canOptimize) { + results.push(compoundRangeResult) + } - // Try to optimize each part, keep the optimizable ones - for (const arg of expression.args) { + // Try to optimize the remaining conjuncts, keep the optimizable ones. + // Conjuncts that cannot use an index make the result inexact: the + // intersection is then a superset of the true result and must be + // re-filtered against the full expression by the caller. The compound + // range result may itself be inexact (e.g. a null/undefined bound). + let allConjunctsExact = !compoundRangeResult.canOptimize + ? true + : compoundRangeResult.isExact + for (const [argIndex, arg] of expression.args.entries()) { + if (coveredArgIndices.has(argIndex)) { + continue + } const result = optimizeQueryRecursive(arg, collection) if (result.canOptimize) { results.push(result) + if (!result.isExact) { + allConjunctsExact = false + } + } else { + allConjunctsExact = false } } @@ -435,10 +654,14 @@ function optimizeAndExpression( // Use intersectSets utility for AND logic const allMatchingSets = results.map((r) => r.matchingKeys) const intersectedKeys = intersectSets(allMatchingSets) - return { canOptimize: true, matchingKeys: intersectedKeys } + return { + canOptimize: true, + matchingKeys: intersectedKeys, + isExact: allConjunctsExact, + } } - return { canOptimize: false, matchingKeys: new Set() } + return { canOptimize: false, matchingKeys: new Set(), isExact: false } } /** @@ -464,27 +687,31 @@ function optimizeOrExpression( collection: CollectionLike, ): OptimizationResult { if (expression.type !== `func` || expression.args.length < 2) { - return { canOptimize: false, matchingKeys: new Set() } + return { canOptimize: false, matchingKeys: new Set(), isExact: false } } const results: Array> = [] - // Try to optimize each part, keep the optimizable ones + // Every disjunct must be optimizable: rows matched only by a disjunct + // that cannot use an index would be missing from the union, and no + // post-filtering can recover them. In that case fall back to a full scan. for (const arg of expression.args) { const result = optimizeQueryRecursive(arg, collection) - if (result.canOptimize) { - results.push(result) + if (!result.canOptimize) { + return { canOptimize: false, matchingKeys: new Set(), isExact: false } } + results.push(result) } - if (results.length > 0) { - // Use unionSets utility for OR logic - const allMatchingSets = results.map((r) => r.matchingKeys) - const unionedKeys = unionSets(allMatchingSets) - return { canOptimize: true, matchingKeys: unionedKeys } + // Use unionSets utility for OR logic + const allMatchingSets = results.map((r) => r.matchingKeys) + const unionedKeys = unionSets(allMatchingSets) + return { + canOptimize: true, + matchingKeys: unionedKeys, + // An inexact (superset) disjunct makes the union a superset as well + isExact: results.every((r) => r.isExact), } - - return { canOptimize: false, matchingKeys: new Set() } } /** @@ -498,8 +725,9 @@ function canOptimizeOrExpression< return false } - // If any argument can be optimized, we can gain some speedup - return expression.args.some((arg) => canOptimizeExpression(arg, collection)) + // Every disjunct must be optimizable, otherwise the union would miss + // rows matched only by the non-optimizable disjuncts + return expression.args.every((arg) => canOptimizeExpression(arg, collection)) } /** @@ -513,7 +741,7 @@ function optimizeInArrayExpression< collection: CollectionLike, ): OptimizationResult { if (expression.type !== `func` || expression.args.length !== 2) { - return { canOptimize: false, matchingKeys: new Set() } + return { canOptimize: false, matchingKeys: new Set(), isExact: false } } const fieldArg = expression.args[0]! @@ -528,11 +756,17 @@ function optimizeInArrayExpression< const values = (arrayArg as any).value const index = findIndexForField(collection, fieldPath) + // A nullish or NaN member can never be matched by `IN` (a comparison + // against null/undefined/NaN is never true), but the index would still + // return rows with such an indexed value. When the list contains one of + // those the result is a superset that the caller must re-filter. + const isExact = values.every((value: any) => isExactComparisonValue(value)) + if (index) { // Check if the index supports IN operation if (index.supports(`in`)) { const matchingKeys = index.lookup(`in`, values) - return { canOptimize: true, matchingKeys } + return { canOptimize: true, matchingKeys, isExact } } else if (index.supports(`eq`)) { // Fallback to multiple equality lookups const matchingKeys = new Set() @@ -542,12 +776,12 @@ function optimizeInArrayExpression< matchingKeys.add(key) } } - return { canOptimize: true, matchingKeys } + return { canOptimize: true, matchingKeys, isExact } } } } - return { canOptimize: false, matchingKeys: new Set() } + return { canOptimize: false, matchingKeys: new Set(), isExact: false } } /** diff --git a/packages/db/src/utils/uuid.ts b/packages/db/src/utils/uuid.ts new file mode 100644 index 0000000000..45875a459f --- /dev/null +++ b/packages/db/src/utils/uuid.ts @@ -0,0 +1,45 @@ +/** + * Returns a RFC 4122 version 4 UUID. + * + * Prefers `crypto.randomUUID()` when available. In non-secure browser contexts + * (e.g. a dev server accessed via a LAN IP over HTTP) `crypto.randomUUID` is + * `undefined`, so this falls back to building a UUIDv4 from + * `crypto.getRandomValues`. Throws if neither API is available. + * + * See https://github.com/TanStack/db/issues/1541. + */ +export function safeRandomUUID(): string { + const c: Crypto | undefined = + typeof globalThis !== `undefined` ? (globalThis as any).crypto : undefined + + if (c && typeof c.randomUUID === `function`) { + return c.randomUUID() + } + + if (c && typeof c.getRandomValues === `function`) { + const bytes = c.getRandomValues(new Uint8Array(16)) + // Per RFC 4122 §4.4: set version (4) and variant (10xx) bits. + bytes[6] = (bytes[6]! & 0x0f) | 0x40 + bytes[8] = (bytes[8]! & 0x3f) | 0x80 + + const hex: Array = [] + for (let i = 0; i < 16; i++) { + hex.push(bytes[i]!.toString(16).padStart(2, `0`)) + } + return ( + hex.slice(0, 4).join(``) + + `-` + + hex.slice(4, 6).join(``) + + `-` + + hex.slice(6, 8).join(``) + + `-` + + hex.slice(8, 10).join(``) + + `-` + + hex.slice(10, 16).join(``) + ) + } + + throw new Error( + `No secure random number generator available: neither crypto.randomUUID nor crypto.getRandomValues is defined in this environment.`, + ) +} diff --git a/packages/db/src/virtual-props.ts b/packages/db/src/virtual-props.ts index 3205d31c2d..ef285821fa 100644 --- a/packages/db/src/virtual-props.ts +++ b/packages/db/src/virtual-props.ts @@ -37,7 +37,7 @@ export type VirtualOrigin = 'local' | 'remote' * // Accessing virtual properties on a row * const user = collection.get('user-1') * if (user.$synced) { - * console.log('Confirmed by backend') + * console.log('No pending local optimistic writes for this row') * } * if (user.$origin === 'local') { * console.log('Created/modified locally') @@ -47,7 +47,7 @@ export type VirtualOrigin = 'local' | 'remote' * @example * ```typescript * // Using virtual properties in queries - * const confirmedOrders = createLiveQueryCollection({ + * const ordersWithoutLocalWrites = createLiveQueryCollection({ * query: (q) => q * .from({ order: orders }) * .where(({ order }) => eq(order.$synced, true)) @@ -58,10 +58,15 @@ export interface VirtualRowProps< TKey extends string | number = string | number, > { /** - * Whether this row reflects confirmed state from the backend. + * Whether this row currently has no pending local optimistic writes. * - * - `true`: Row is confirmed by the backend (no pending optimistic mutations) - * - `false`: Row has pending optimistic mutations that haven't been confirmed + * - `true`: No pending local optimistic mutation currently affects this row + * - `false`: One or more pending local optimistic mutations currently affect this row + * + * This is local mutation status. It does not prove that a backend has uploaded, + * confirmed, or read back the row. If you need backend-confirmed status, keep + * your mutation function pending until that backend observation has happened, + * or expose adapter-specific status. * * For local-only collections (no sync), this is always `true`. * For live query collections, this is passed through from the source collection. diff --git a/packages/db/tests/btree-index-undefined-values.test.ts b/packages/db/tests/btree-index-undefined-values.test.ts index 11e29690c6..1510c02e3c 100644 --- a/packages/db/tests/btree-index-undefined-values.test.ts +++ b/packages/db/tests/btree-index-undefined-values.test.ts @@ -248,6 +248,24 @@ describe(`BTreeIndex - undefined value handling`, () => { expect(withoutFrom.size).toBe(3) }) + it(`should not drop the minimum key when an upper-only range is exclusive on the (absent) lower bound`, () => { + // When no `from` bound is provided, `fromInclusive` must not cause the + // smallest key to be excluded: there is no lower bound to exclude + // against. Only an explicitly provided exclusive lower bound should + // drop its boundary value. + const index = createIndex(`value`) + index.add(`a`, { value: 1 }) + index.add(`b`, { value: 5 }) + index.add(`c`, { value: 10 }) + + const result = index.rangeQuery({ to: 10, fromInclusive: false }) + + expect(result.size).toBe(3) + expect(result).toContain(`a`) + expect(result).toContain(`b`) + expect(result).toContain(`c`) + }) + it(`should handle range query from undefined to undefined`, () => { const index = createIndex(`value`) index.add(`a`, { value: undefined }) diff --git a/packages/db/tests/collection-indexes.test.ts b/packages/db/tests/collection-indexes.test.ts index a441a5520d..bd5f4868c7 100644 --- a/packages/db/tests/collection-indexes.test.ts +++ b/packages/db/tests/collection-indexes.test.ts @@ -625,6 +625,19 @@ describe(`Collection Indexes`, () => { }) }) + it(`should exclude the boundary value from greater than queries on dates`, () => { + // gt must be strict for date fields: Bob was created exactly on + // 2023-01-02, so only rows created strictly later may be returned. + collection.createIndex((row) => row.createdAt) + + const result = collection.currentStateAsChanges({ + where: gt(new PropRef([`createdAt`]), new Date(`2023-01-02`)), + })! + + const names = result.map((r) => r.value.name).sort() + expect(names).toEqual([`Charlie`, `Diana`, `Eve`]) + }) + it(`should perform greater than or equal queries`, () => { withIndexTracking(collection, (tracker) => { const result = collection.currentStateAsChanges({ @@ -1179,6 +1192,538 @@ describe(`Collection Indexes`, () => { }) }) }) + + it(`should include rows matched by any OR condition when conditions mix indexed and non-indexed expressions`, () => { + // An OR query must return the union of rows matching each condition: + // eq(age, 25) matches Alice (age 25) + // gt(length(name), 6) matches Charlie (name length 7) + // `age` has an index while `length(name)` is a computed expression + // without one, but the chosen execution strategy must not change the + // result: both Alice and Charlie satisfy the OR and must be returned. + const result = collection.currentStateAsChanges({ + where: or( + eq(new PropRef([`age`]), 25), + gt(length(new PropRef([`name`])), 6), + ), + })! + + const names = result.map((r) => r.value.name).sort() + expect(names).toEqual([`Alice`, `Charlie`]) + }) + + it(`should only return rows matching every AND condition when conditions mix indexed and non-indexed expressions`, () => { + // An AND query must return only the rows matching all conditions: + // eq(status, 'active') matches Alice, Charlie and Eve + // gt(length(name), 6) matches only Charlie (name length 7) + // `status` has an index while `length(name)` is a computed expression + // without one, but every condition must still be enforced: only + // Charlie satisfies both. + const result = collection.currentStateAsChanges({ + where: and( + eq(new PropRef([`status`]), `active`), + gt(length(new PropRef([`name`])), 6), + ), + })! + + const names = result.map((r) => r.value.name).sort() + expect(names).toEqual([`Charlie`]) + }) + + it(`should apply the strictest lower bound when range conditions share the same value`, () => { + // gte(age, 25) AND gt(age, 25) reduces to age > 25: the strict + // comparison wins at the shared boundary, so Alice (age 25) must be + // excluded regardless of the order the conditions appear in. + const result = collection.currentStateAsChanges({ + where: and(gte(new PropRef([`age`]), 25), gt(new PropRef([`age`]), 25)), + })! + + const names = result.map((r) => r.value.name).sort() + expect(names).toEqual([`Bob`, `Charlie`, `Diana`]) + }) + + it(`should apply the strictest upper bound when range conditions share the same value`, () => { + // lte(age, 30) AND lt(age, 30) reduces to age < 30: the strict + // comparison wins at the shared boundary, so Bob (age 30) must be + // excluded. + const result = collection.currentStateAsChanges({ + where: and(lte(new PropRef([`age`]), 30), lt(new PropRef([`age`]), 30)), + })! + + const names = result.map((r) => r.value.name).sort() + expect(names).toEqual([`Alice`, `Diana`, `Eve`]) + }) + + it(`should apply the strictest bound for date ranges sharing the same value`, () => { + // Distinct Date instances representing the same point in time must be + // treated as equal values: gte(createdAt, jan2) AND gt(createdAt, jan2) + // reduces to createdAt > jan2, so Bob (created 2023-01-02) must be + // excluded. + collection.createIndex((row) => row.createdAt) + + const result = collection.currentStateAsChanges({ + where: and( + gte(new PropRef([`createdAt`]), new Date(`2023-01-02`)), + gt(new PropRef([`createdAt`]), new Date(`2023-01-02`)), + ), + })! + + const names = result.map((r) => r.value.name).sort() + expect(names).toEqual([`Charlie`, `Diana`, `Eve`]) + }) + + it(`should enforce every AND condition when a range on one field is combined with conditions on other fields`, () => { + // An AND query that contains a compound range on one field plus a + // condition on another field must enforce all of them: + // gt(age, 24) AND lt(age, 36) matches Alice (25), Bob (30), + // Charlie (35) and Diana (28) + // eq(status, 'active') matches Alice, Charlie and Eve + // Only Alice and Charlie satisfy the full conjunction. + const result = collection.currentStateAsChanges({ + where: and( + gt(new PropRef([`age`]), 24), + lt(new PropRef([`age`]), 36), + eq(new PropRef([`status`]), `active`), + ), + })! + + const names = result.map((r) => r.value.name).sort() + expect(names).toEqual([`Alice`, `Charlie`]) + }) + + it(`should match a full scan when a range condition uses an undefined bound`, () => { + // A comparison against `undefined` matches no rows (a comparison with + // null/undefined is never true), so `gt(score, undefined)` excludes + // every row and the whole AND must return nothing. The index-optimized + // path must agree with a plain full scan and not leak rows. + collection.createIndex((row) => row.score) + + const result = collection.currentStateAsChanges({ + where: and( + gt(new PropRef([`score`]), undefined), + lt(new PropRef([`score`]), 90), + ), + })! + + expect(result).toEqual([]) + }) + + it(`should not match rows with a missing value for an equality on undefined`, () => { + // An equality comparison against `undefined` is never true, so + // `eq(score, undefined)` must return no rows even though Eve has an + // undefined score. The index-optimized path must agree with a full + // predicate scan. + collection.createIndex((row) => row.score) + + const result = collection.currentStateAsChanges({ + where: eq(new PropRef([`score`]), undefined), + })! + + expect(result).toEqual([]) + }) + + it(`should ignore an undefined member when matching an IN list`, () => { + // A row only matches `IN` when its value equals one of the listed + // values; a comparison with `undefined` is never true. So + // `inArray(score, [undefined, 80])` must match only Bob (score 80) + // and must not match Eve (undefined score). + collection.createIndex((row) => row.score) + + const result = collection.currentStateAsChanges({ + where: inArray(new PropRef([`score`]), [undefined, 80]), + })! + + const names = result.map((r) => r.value.name).sort() + expect(names).toEqual([`Bob`]) + }) + + it(`should not match rows with a missing value for a range comparison`, () => { + // A range comparison against a row with an undefined value is never + // true, so `lt(score, 85)` must match only Bob (score 80) and must + // not match Eve (undefined score). + collection.createIndex((row) => row.score) + + const result = collection.currentStateAsChanges({ + where: lt(new PropRef([`score`]), 85), + })! + + const names = result.map((r) => r.value.name).sort() + expect(names).toEqual([`Bob`]) + }) + + it(`should not match rows with a missing value for an upper-bounded compound range`, () => { + // A compound range with only upper bounds (e.g. score <= 90) must not + // match a row with an undefined value, since a comparison against + // undefined is never true. Only Bob (80), Charlie (90) and Diana (85) + // satisfy `score <= 90`; Eve (undefined) must be excluded. + collection.createIndex((row) => row.score) + + const result = collection.currentStateAsChanges({ + where: and( + lte(new PropRef([`score`]), 90), + lte(new PropRef([`score`]), 95), + ), + })! + + const names = result.map((r) => r.value.name).sort() + expect(names).toEqual([`Bob`, `Charlie`, `Diana`]) + }) + + it(`should match a string range predicate using the same ordering as a full scan`, async () => { + // String comparisons in the WHERE evaluator use JS relational operators + // (code-point order), where `'ö' > 'z'` is true. A row named `ö` must + // therefore be returned by `name > 'z'`, even though a locale-collated + // index orders `ö` before `z`. The index-optimized result must agree + // with a full predicate scan. + const stringCollection = createCollection< + { id: string; name: string }, + string + >({ + getKey: (row) => row.id, + startSync: true, + autoIndex: `eager`, + defaultIndexType: BTreeIndex, + sync: { + sync: ({ begin, write, commit, markReady }) => { + begin() + write({ type: `insert`, value: { id: `1`, name: `apple` } }) + write({ type: `insert`, value: { id: `2`, name: `ö` } }) + commit() + markReady() + }, + }, + }) + await stringCollection.stateWhenReady() + stringCollection.createIndex((row) => row.name) + + const result = stringCollection.currentStateAsChanges({ + where: gt(new PropRef([`name`]), `z`), + })! + + const names = result.map((r) => r.value.name).sort() + expect(names).toEqual([`ö`]) + }) + + it(`should match a row with a NaN value for an equality on NaN`, async () => { + // Under PostgreSQL float semantics NaN is equal to itself, so + // `eq(score, NaN)` matches the NaN-valued row (and the index, which + // stores and returns it, agrees with a full scan). + const nanCollection = createCollection< + { id: string; score: number }, + string + >({ + getKey: (row) => row.id, + startSync: true, + autoIndex: `eager`, + defaultIndexType: BTreeIndex, + sync: { + sync: ({ begin, write, commit, markReady }) => { + begin() + write({ type: `insert`, value: { id: `1`, score: 5 } }) + write({ type: `insert`, value: { id: `2`, score: NaN } }) + commit() + markReady() + }, + }, + }) + await nanCollection.stateWhenReady() + nanCollection.createIndex((row) => row.score) + + const result = nanCollection.currentStateAsChanges({ + where: eq(new PropRef([`score`]), NaN), + })! + + const ids = result.map((r) => r.value.id).sort() + expect(ids).toEqual([`2`]) + }) + + it(`should match a row with a NaN value for an IN list containing NaN`, async () => { + // A row matches `IN` when its value equals a listed value. Under + // PostgreSQL float semantics NaN is equal to itself, so + // `inArray(score, [NaN, 5])` matches both the score-5 row and the + // NaN-valued row. + const nanCollection = createCollection< + { id: string; score: number }, + string + >({ + getKey: (row) => row.id, + startSync: true, + autoIndex: `eager`, + defaultIndexType: BTreeIndex, + sync: { + sync: ({ begin, write, commit, markReady }) => { + begin() + write({ type: `insert`, value: { id: `1`, score: 5 } }) + write({ type: `insert`, value: { id: `2`, score: NaN } }) + commit() + markReady() + }, + }, + }) + await nanCollection.stateWhenReady() + nanCollection.createIndex((row) => row.score) + + const result = nanCollection.currentStateAsChanges({ + where: inArray(new PropRef([`score`]), [NaN, 5]), + })! + + const ids = result.map((r) => r.value.id).sort() + expect(ids).toEqual([`1`, `2`]) + }) + + it(`should return array-valued rows for a range predicate consistently with a full scan`, async () => { + // Range predicates are evaluated with standard relational comparison, + // under which `[2] > [10]` is true (arrays compare as their string + // form). An index on an array-valued field must return the same rows as + // a full scan and must not drop this match. + const arrayCollection = createCollection< + { id: string; value: Array }, + string + >({ + getKey: (row) => row.id, + startSync: true, + autoIndex: `off`, + defaultIndexType: BTreeIndex, + sync: { + sync: ({ begin, write, commit, markReady }) => { + begin() + write({ type: `insert`, value: { id: `1`, value: [2] } }) + commit() + markReady() + }, + }, + }) + await arrayCollection.stateWhenReady() + arrayCollection.createIndex((row) => row.value) + + const result = arrayCollection.currentStateAsChanges({ + where: gt(new PropRef([`value`]), [10]), + })! + + const ids = result.map((r) => r.value.id).sort() + expect(ids).toEqual([`1`]) + }) + + it(`should return all matching rows for a range predicate on a custom-comparator index`, async () => { + // A range predicate must return every row that satisfies it regardless + // of the comparator the index was created with. With scores 5 and 20, + // `score > 10` matches only the row with score 20. + const customCollection = createCollection< + { id: string; score: number }, + string + >({ + getKey: (row) => row.id, + startSync: true, + autoIndex: `off`, + defaultIndexType: BTreeIndex, + sync: { + sync: ({ begin, write, commit, markReady }) => { + begin() + write({ type: `insert`, value: { id: `low`, score: 5 } }) + write({ type: `insert`, value: { id: `high`, score: 20 } }) + commit() + markReady() + }, + }, + }) + await customCollection.stateWhenReady() + customCollection.createIndex((row) => row.score, { + options: { compareFn: (a: number, b: number) => b - a }, + }) + + const result = customCollection.currentStateAsChanges({ + where: gt(new PropRef([`score`]), 10), + })! + + const ids = result.map((r) => r.value.id).sort() + expect(ids).toEqual([`high`]) + }) + + it(`should return all matching rows for a range predicate when the field also contains NaN`, async () => { + // A range predicate must return every matching row even when other rows + // hold a NaN value for the field. Under PostgreSQL float semantics NaN is + // the greatest value, so with scores NaN, 1, 3, 5 and 7, `score > 2` + // matches the rows with scores 3, 5, 7 and NaN. + const nanCollection = createCollection< + { id: string; score: number }, + string + >({ + getKey: (row) => row.id, + startSync: true, + autoIndex: `off`, + defaultIndexType: BTreeIndex, + sync: { + sync: ({ begin, write, commit, markReady }) => { + begin() + write({ type: `insert`, value: { id: `nan`, score: NaN } }) + write({ type: `insert`, value: { id: `one`, score: 1 } }) + write({ type: `insert`, value: { id: `three`, score: 3 } }) + write({ type: `insert`, value: { id: `five`, score: 5 } }) + write({ type: `insert`, value: { id: `seven`, score: 7 } }) + commit() + markReady() + }, + }, + }) + await nanCollection.stateWhenReady() + nanCollection.createIndex((row) => row.score) + + const result = nanCollection.currentStateAsChanges({ + where: gt(new PropRef([`score`]), 2), + })! + + const ids = result.map((r) => r.value.id).sort() + expect(ids).toEqual([`five`, `nan`, `seven`, `three`]) + }) + + it(`should use the index for a range query on a field that also contains NaN`, async () => { + // A NaN value has a well-defined sort position (greatest, under + // PostgreSQL float semantics), so a range query on the field can still be + // served by the index and does not need to fall back to a full scan. + const nanCollection = createCollection< + { id: string; score: number }, + string + >({ + getKey: (row) => row.id, + startSync: true, + autoIndex: `off`, + defaultIndexType: BTreeIndex, + sync: { + sync: ({ begin, write, commit, markReady }) => { + begin() + write({ type: `insert`, value: { id: `nan`, score: NaN } }) + write({ type: `insert`, value: { id: `one`, score: 1 } }) + write({ type: `insert`, value: { id: `three`, score: 3 } }) + write({ type: `insert`, value: { id: `five`, score: 5 } }) + write({ type: `insert`, value: { id: `seven`, score: 7 } }) + commit() + markReady() + }, + }, + }) + await nanCollection.stateWhenReady() + nanCollection.createIndex((row) => row.score) + + withIndexTracking(nanCollection, (tracker) => { + const result = nanCollection.currentStateAsChanges({ + where: gt(new PropRef([`score`]), 2), + })! + + const ids = result.map((r) => r.value.id).sort() + expect(ids).toEqual([`five`, `nan`, `seven`, `three`]) + + expectIndexUsage(tracker.stats, { + shouldUseIndex: true, + shouldUseFullScan: false, + }) + }) + }) + + it(`should exclude NaN from a less-than range query`, async () => { + // Under PostgreSQL float semantics NaN is the greatest value, so + // `score < 4` matches the rows with scores 1 and 3 but never the + // NaN-valued row. + const nanCollection = createCollection< + { id: string; score: number }, + string + >({ + getKey: (row) => row.id, + startSync: true, + autoIndex: `off`, + defaultIndexType: BTreeIndex, + sync: { + sync: ({ begin, write, commit, markReady }) => { + begin() + write({ type: `insert`, value: { id: `nan`, score: NaN } }) + write({ type: `insert`, value: { id: `one`, score: 1 } }) + write({ type: `insert`, value: { id: `three`, score: 3 } }) + write({ type: `insert`, value: { id: `five`, score: 5 } }) + write({ type: `insert`, value: { id: `seven`, score: 7 } }) + commit() + markReady() + }, + }, + }) + await nanCollection.stateWhenReady() + nanCollection.createIndex((row) => row.score) + + const result = nanCollection.currentStateAsChanges({ + where: lt(new PropRef([`score`]), 4), + })! + + const ids = result.map((r) => r.value.id).sort() + expect(ids).toEqual([`one`, `three`]) + }) + + // Invalid Dates have a NaN timestamp, so they follow the same PostgreSQL + // float semantics as NaN: equal to one another and greater than every valid + // Date. The index-served and full-scan results must agree. + const makeInvalidDateCollection = async () => { + const dateCollection = createCollection< + { id: string; createdAt: Date }, + string + >({ + getKey: (row) => row.id, + startSync: true, + autoIndex: `off`, + defaultIndexType: BTreeIndex, + sync: { + sync: ({ begin, write, commit, markReady }) => { + begin() + write({ + type: `insert`, + value: { id: `invalid`, createdAt: new Date(`not a date`) }, + }) + write({ + type: `insert`, + value: { id: `valid`, createdAt: new Date(`2023-01-01`) }, + }) + commit() + markReady() + }, + }, + }) + await dateCollection.stateWhenReady() + dateCollection.createIndex((row) => row.createdAt) + return dateCollection + } + + it(`should match an invalid-Date row for an equality on an invalid Date`, async () => { + const dateCollection = await makeInvalidDateCollection() + + const result = dateCollection.currentStateAsChanges({ + where: eq(new PropRef([`createdAt`]), new Date(`not a date`)), + })! + + const ids = result.map((r) => r.value.id).sort() + expect(ids).toEqual([`invalid`]) + }) + + it(`should match an invalid-Date member of an IN list`, async () => { + const dateCollection = await makeInvalidDateCollection() + + const result = dateCollection.currentStateAsChanges({ + where: inArray(new PropRef([`createdAt`]), [ + new Date(`not a date`), + new Date(`2023-01-01`), + ]), + })! + + const ids = result.map((r) => r.value.id).sort() + expect(ids).toEqual([`invalid`, `valid`]) + }) + + it(`should treat an invalid Date as greater than valid Dates in a range query`, async () => { + // `createdAt > 2022` matches the valid Date and the invalid Date (which + // is the greatest value under PostgreSQL float semantics). + const dateCollection = await makeInvalidDateCollection() + + const result = dateCollection.currentStateAsChanges({ + where: gt(new PropRef([`createdAt`]), new Date(`2022-01-01`)), + })! + + const ids = result.map((r) => r.value.id).sort() + expect(ids).toEqual([`invalid`, `valid`]) + }) }) describe(`Index Usage Verification`, () => { diff --git a/packages/db/tests/collection.test.ts b/packages/db/tests/collection.test.ts index 7fc5d67b46..e96994db64 100644 --- a/packages/db/tests/collection.test.ts +++ b/packages/db/tests/collection.test.ts @@ -4,6 +4,7 @@ import { createCollection } from '../src/collection/index.js' import { CollectionRequiresConfigError, DuplicateKeyError, + DuplicateKeySyncError, InvalidKeyError, KeyUpdateNotAllowedError, MissingDeleteHandlerError, @@ -18,7 +19,12 @@ import { stripVirtualProps, withExpectedRejection, } from './utils' -import type { ChangeMessage, MutationFn, PendingMutation } from '../src/types' +import type { + ChangeMessage, + MutationFn, + PendingMutation, + SyncConfig, +} from '../src/types' const getStateValue = ( collection: { state: Map }, @@ -42,7 +48,36 @@ describe(`Collection`, () => { expect(() => createCollection()).toThrow(CollectionRequiresConfigError) }) - it(`removes optimistic insert when sync confirms with a different server-generated key`, async () => { + it(`throws DuplicateKeySyncError instead of TypeError when config has no utils`, async () => { + let begin!: () => void + let write!: Parameters< + SyncConfig<{ id: number; text: string }, number>[`sync`] + >[0][`write`] + + const collection = createCollection<{ id: number; text: string }, number>({ + id: `duplicate-key-no-utils-test`, + getKey: (item) => item.id, + sync: { + sync: (params) => { + begin = params.begin + write = params.write + params.begin() + params.write({ type: `insert`, value: { id: 1, text: `one` } }) + params.commit() + params.markReady() + }, + }, + }) + + await collection.stateWhenReady() + + begin() + expect(() => + write({ type: `insert`, value: { id: 1, text: `changed` } }), + ).toThrow(DuplicateKeySyncError) + }) + + it(`keeps ambiguous server-key sync queued while a temp-key optimistic insert is pending`, async () => { const options = mockSyncCollectionOptionsNoInitialState<{ id: number text: string @@ -67,6 +102,11 @@ describe(`Collection`, () => { options.utils.commit() // The sync commit is held while the local insert transaction is persisting. + // Without an explicit temp-key -> server-key mapping, core cannot know + // whether key 24 is this optimistic insert's server echo or an unrelated + // row, so it must not expose both rows at the same time. + expect(tx.isPersisted.isPending()).toBe(true) + expect(collection.has(24)).toBe(false) expect(getStateEntries(collection)).toEqual([ [4733, { id: 4733, text: `two` }], ]) diff --git a/packages/db/tests/comparison.test.ts b/packages/db/tests/comparison.test.ts new file mode 100644 index 0000000000..ae40488f43 --- /dev/null +++ b/packages/db/tests/comparison.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest' +import { ascComparator, defaultComparator } from '../src/utils/comparison' +import { DEFAULT_COMPARE_OPTIONS } from '../src/utils' + +describe(`ascComparator - PostgreSQL float semantics for NaN`, () => { + const opts = DEFAULT_COMPARE_OPTIONS // nulls: `first` + + it(`orders NaN greater than every number`, () => { + expect(ascComparator(NaN, 5, opts)).toBeGreaterThan(0) + expect(ascComparator(5, NaN, opts)).toBeLessThan(0) + }) + + it(`treats NaN as equal to NaN`, () => { + expect(ascComparator(NaN, NaN, opts)).toBe(0) + }) + + it(`produces a stable total order with NaN sorting last`, () => { + const sorted = [3, NaN, 1, 5, NaN].sort((a, b) => defaultComparator(a, b)) + + expect(sorted.slice(0, 3)).toEqual([1, 3, 5]) + expect(sorted.slice(3).every((v) => Number.isNaN(v))).toBe(true) + }) + + it(`keeps null before non-null values regardless of NaN`, () => { + // nulls still sort first by default; NaN sorts last (greatest non-null) + const sorted = [5, NaN, null, 1].sort((a, b) => defaultComparator(a, b)) + + expect(sorted[0]).toBe(null) + expect(sorted[1]).toBe(1) + expect(sorted[2]).toBe(5) + expect(Number.isNaN(sorted[3])).toBe(true) + }) + + it(`orders an invalid Date greater than valid Dates`, () => { + const invalid = new Date(`not a date`) + const valid = new Date(`2023-01-01`) + + expect(ascComparator(invalid, valid, opts)).toBeGreaterThan(0) + expect(ascComparator(valid, invalid, opts)).toBeLessThan(0) + }) +}) diff --git a/packages/db/tests/conformance/contract.ts b/packages/db/tests/conformance/contract.ts new file mode 100644 index 0000000000..486fb4920d --- /dev/null +++ b/packages/db/tests/conformance/contract.ts @@ -0,0 +1,156 @@ +/** + * Cross-adapter live-query conformance harness — shared contract. + * + * ONE behavioral spec for `useLiveQuery`, run against every framework adapter. + * Each adapter provides a thin `LiveQueryDriver` and the shared suite in + * `suite.ts` does the rest. + * + * Realm safety: the driver — not the scenarios — creates source collections and + * supplies query operators, both imported from the *adapter's* copy of + * `@tanstack/db`. This keeps collection instances and expression nodes in the + * same module realm as the adapter's hook, avoiding the dual-package + * `instanceof CollectionImpl` mismatch. Scenarios never import `@tanstack/db`. + * + * Expected-fail policy: `knownGaps` lists scenario KEYS this adapter does not + * yet satisfy. Populate it EMPIRICALLY — port a behavior, run it, and only add + * the key if it actually fails. The behavior matrix tells you where to look; + * the test run tells you what's broken. When a gap closes, `it.fails` errors + * ("expected to fail but passed") prompting you to delete the key. + */ +import type { Collection } from '@tanstack/db' + +/** Default row shape used by the base scenarios (a "person"). */ +export interface Row { + id: string + name: string + age: number + team: string +} + +/** + * A realm-correct source collection plus sync-driven mutators. Generic over the + * row type so relational scenarios (join, includes) can build differently-shaped + * related collections; keyed by `id` in every case. + */ +export interface SourceHandle { + collection: Collection + insert: (row: T) => void + update: (row: T) => void + remove: (row: T) => void +} + +/** + * A source whose readiness the scenario controls, for loading/eager/ready + * transitions. Starts in `loading` (synced, not ready) so scenarios can `emit` + * rows while still loading and then `markReady`. + */ +export interface DeferredSourceHandle< + T extends { id: string } = Row, +> extends SourceHandle { + /** Write rows without marking ready — exercises the eager (visible-while-loading) path. */ + emit: (rows: ReadonlyArray) => void + /** Transition the source from `loading` to `ready`. */ + markReady: () => void +} + +/** + * The subset of `@tanstack/db` query operators scenarios need, supplied by the + * driver from the adapter's realm. Grows as engine scenarios are ported. + */ +export interface DbOps { + eq: (a: any, b: any) => any + gt: (a: any, b: any) => any + count: (a: any) => any + sum: (a: any) => any + coalesce: (...args: Array) => any + /** Build an optimistic action: onMutate applies optimistic state, mutationFn confirms. */ + createOptimisticAction: (config: { + onMutate: (variables: any) => void + mutationFn: (variables: any) => Promise + }) => (variables?: any) => { isPersisted: { promise: Promise } } +} + +/** Normalized, adapter-agnostic view of a live query's current result. */ +export interface ConformanceResult { + /** Array for list queries; a single row (or undefined) for `findOne`. */ + data: any + status: string + isReady: boolean + isError: boolean + isEnabled: boolean +} + +/** A query-builder callback, e.g. `(q) => q.from({ items: source.collection })`. */ +export type QueryBuild = (q: any) => any + +/** A mounted live query under test. */ +export interface LiveQueryHandle { + current: () => ConformanceResult + /** Let the framework scheduler + core sync settle, then resolve. */ + flush: () => Promise + /** + * Run a state-mutating callback inside the framework's update scope, then + * settle (React `act`, Vue `nextTick`, Svelte `flushSync`, Solid `batch`). + * Needed when a mutation notifies synchronously, e.g. optimistic actions. + */ + apply: (fn: () => void) => Promise + unmount: () => void +} + +/** + * A mounted query whose input parameter can change after mount, for + * recompilation and disabled/enabled transitions. `setParam` re-renders with the + * new value and settles. + */ +export interface ControllableHandle

extends LiveQueryHandle { + setParam: (param: P) => Promise +} + +/** What each adapter package implements and hands to `runSuite`. */ +export interface LiveQueryDriver { + name: string + /** Operators from the adapter's `@tanstack/db` realm. */ + ops: DbOps + /** Create a realm-correct source collection + mutators, keyed by `id`. */ + makeSource: ( + initialData: ReadonlyArray, + ) => SourceHandle + /** Create a source that starts `loading` and readies on demand (keyed by `id`). */ + makeDeferredSource: () => DeferredSourceHandle + /** + * Create a pre-built live-query collection to pass straight to the hook. + * `startSync: false` yields a not-yet-syncing collection (isReady false). + */ + makePrecreated: ( + build: QueryBuild, + opts?: { startSync?: boolean }, + ) => { collection: Collection } + /** Create a source whose sync fails, driving it into `error` status. */ + makeErrorSource: () => { collection: Collection } + /** Mount a live query from a query-builder callback. */ + mount: (build: QueryBuild) => LiveQueryHandle + /** + * Mount a live query whose input depends on a parameter that can change after + * mount. `build` returns a query, or `null`/`undefined` to represent disabled. + */ + mountControllable:

( + build: (q: any, param: P) => any, + initial: P, + ) => ControllableHandle

+ /** Mount a pre-created collection passed directly to the hook. */ + mountCollection: (collection: Collection) => LiveQueryHandle + /** Mount via the config-object input form (`{ query: build }`). */ + mountConfig: (build: QueryBuild) => LiveQueryHandle + /** Mount an explicitly-disabled query (adapter's own null/undefined form). */ + mountDisabled: () => LiveQueryHandle + /** Scenario keys this adapter is empirically known NOT to satisfy yet. */ + knownGaps?: ReadonlyArray + /** + * How the adapter surfaces a query error (see the `error-status` scenario): + * - `flag` (default): a readable `isError`/`status === 'error'` on the result. + * - `throw`: reading the errored result throws, for a framework error boundary + * to catch (e.g. Solid's `createResource`/`` model). + */ + errorSurface?: `flag` | `throw` + features?: { serverSnapshot?: boolean; suspense?: boolean } +} diff --git a/packages/db/tests/conformance/suite.ts b/packages/db/tests/conformance/suite.ts new file mode 100644 index 0000000000..8e02911c69 --- /dev/null +++ b/packages/db/tests/conformance/suite.ts @@ -0,0 +1,700 @@ +/** + * Shared live-query conformance suite. + * + * Sourced bottom-up from the union of the five adapters' existing test suites + * (the "spine" + framework-agnostic "gap-closers"), plus a small tail of + * behaviors no adapter tests yet but all should (encoded as expected-fail). + * + * Each scenario has a stable KEY. An adapter marks a key in `driver.knownGaps` + * (populated empirically by running, not from the coverage matrix) to assert it + * as expected-fail. `UNIVERSAL_EXPECTED_FAIL` keys fail on every adapter until + * the underlying core gap is fixed. + * + * Coverage: query/where/select, live insert/update/delete, orderBy, join, + * groupBy/aggregate, nested aggregates, `.includes` subqueries, findOne + * cardinality, disabled + transitions, deferred readiness / eager / ready-with- + * no-data, param recompilation, optimistic mutation, pre-created & config-object + * inputs, error status, and the order-only-move tail (expected-fail). + */ +import { describe, expect, it } from 'vitest' +import type { LiveQueryDriver, LiveQueryHandle, Row } from './contract' + +const SEED: Array = [ + { id: `1`, name: `John Doe`, age: 30, team: `a` }, + { id: `2`, name: `Jane Doe`, age: 25, team: `b` }, + { id: `3`, name: `John Smith`, age: 35, team: `a` }, +] + +interface Issue { + id: string + title: string + userId: string +} + +// Issues reference SEED people: John(1) has 2, Jane(2) has 1, John Smith(3) has 0. +const ISSUES: Array = [ + { id: `i1`, title: `Issue 1`, userId: `1` }, + { id: `i2`, title: `Issue 2`, userId: `2` }, + { id: `i3`, title: `Issue 3`, userId: `1` }, +] + +/** Keys that are expected to fail on ALL adapters (core gaps, not adapter drift). */ +const UNIVERSAL_EXPECTED_FAIL = new Set([`order-only-move`]) + +export function runSuite(rawDriver: LiveQueryDriver) { + const { ops } = rawDriver + const gaps = new Set(rawDriver.knownGaps ?? []) + + // Every scenario key registered below, used to validate `knownGaps` / + // `UNIVERSAL_EXPECTED_FAIL` don't reference a stale or misspelled key. + const registeredKeys = new Set() + + // Track every handle mounted during the current scenario so it is always torn + // down, even when an (expected-fail) scenario throws before its own + // `h.unmount()`. Wrapping the driver's `mount*` methods records handles + // automatically, so scenario bodies need no `try/finally` of their own. + let mounted: Array | null = null + const track = (handle: H): H => { + mounted?.push(handle) + return handle + } + const driver: LiveQueryDriver = { + ...rawDriver, + mount: (build) => track(rawDriver.mount(build)), + mountControllable: (build, initial) => + track(rawDriver.mountControllable(build, initial)), + mountCollection: (collection) => + track(rawDriver.mountCollection(collection)), + mountConfig: (build) => track(rawDriver.mountConfig(build)), + mountDisabled: () => track(rawDriver.mountDisabled()), + } + + /** Register a scenario as `it` or `it.fails` based on known gaps. */ + const scenario = ( + key: string, + name: string, + fn: () => Promise | void, + ) => { + registeredKeys.add(key) + const expectFail = gaps.has(key) || UNIVERSAL_EXPECTED_FAIL.has(key) + const label = `[${key}] ${name}${expectFail ? ` (expected-fail)` : ``}` + const run = async () => { + const handles: Array = [] + mounted = handles + try { + await fn() + } finally { + mounted = null + for (const handle of handles) { + try { + handle.unmount() + } catch { + // teardown is best-effort / idempotent + } + } + } + } + if (expectFail) it.fails(label, run) + else it(label, run) + } + + describe(`live-query conformance :: ${driver.name}`, () => { + // ---- spine: query + liveness ---------------------------------------- + + scenario( + `basic-select`, + `from + where + select returns matching rows`, + async () => { + const source = driver.makeSource(SEED) + const h = driver.mount((q) => + q + .from({ items: source.collection }) + .where(({ items }: any) => ops.gt(items.age, 30)) + .select(({ items }: any) => ({ id: items.id, name: items.name })), + ) + await h.flush() + + expect(h.current().data).toHaveLength(1) + expect(h.current().data[0]).toMatchObject({ + id: `3`, + name: `John Smith`, + }) + h.unmount() + }, + ) + + scenario(`live-insert`, `a sync insert appears in the result`, async () => { + const source = driver.makeSource(SEED) + const h = driver.mount((q) => + q + .from({ items: source.collection }) + .select(({ items }: any) => ({ id: items.id })), + ) + await h.flush() + expect(h.current().data).toHaveLength(SEED.length) + + source.insert({ id: `4`, name: `Dave`, age: 40, team: `b` }) + await h.flush() + + expect(h.current().data).toHaveLength(SEED.length + 1) + h.unmount() + }) + + scenario( + `live-delete`, + `a sync delete removes from the result`, + async () => { + const source = driver.makeSource(SEED) + const h = driver.mount((q) => + q + .from({ items: source.collection }) + .select(({ items }: any) => ({ id: items.id })), + ) + await h.flush() + + source.remove(SEED[0]!) + await h.flush() + + expect(h.current().data).toHaveLength(SEED.length - 1) + h.unmount() + }, + ) + + scenario(`orderby`, `orderBy yields rows in sorted order`, async () => { + const source = driver.makeSource(SEED) + const h = driver.mount((q) => + q + .from({ items: source.collection }) + .orderBy(({ items }: any) => items.age) + .select(({ items }: any) => ({ id: items.id })), + ) + await h.flush() + + expect(h.current().data.map((r: any) => r.id)).toEqual([`2`, `1`, `3`]) + h.unmount() + }) + + // ---- gap-closer: cardinality (matrix: Vue tests this 0 times) -------- + + scenario( + `findone-cardinality`, + `findOne returns a single row, not an array`, + async () => { + const source = driver.makeSource(SEED) + const h = driver.mount((q) => + q + .from({ items: source.collection }) + .where(({ items }: any) => ops.eq(items.id, `3`)) + .findOne(), + ) + await h.flush() + + expect(Array.isArray(h.current().data)).toBe(false) + expect(h.current().data).toMatchObject({ id: `3`, name: `John Smith` }) + h.unmount() + }, + ) + + // ---- gap-closer: disabled (matrix: Svelte tests this 0 times) -------- + + scenario( + `disabled-explicit`, + `a disabled query reports isEnabled=false with no data`, + async () => { + const h = driver.mountDisabled() + await h.flush() + + expect(h.current().isEnabled).toBe(false) + expect(h.current().data ?? []).toHaveLength(0) + h.unmount() + }, + ) + + // ---- spine: lifecycle invariant -------------------------------------- + + scenario( + `no-updates-after-unmount`, + `no result mutation after unmount`, + async () => { + const source = driver.makeSource(SEED) + const h = driver.mount((q) => + q + .from({ items: source.collection }) + .select(({ items }: any) => ({ id: items.id })), + ) + await h.flush() + const before = h.current().data.length + + h.unmount() + source.insert({ id: `99`, name: `Zed`, age: 1, team: `b` }) + await h.flush() + + expect(h.current().data.length).toBe(before) + }, + ) + + // ---- spine: relational + aggregate queries --------------------------- + + scenario(`join`, `join across two collections`, async () => { + const people = driver.makeSource(SEED) + const issues = driver.makeSource(ISSUES) + const h = driver.mount((q) => + q + .from({ issues: issues.collection }) + .join({ persons: people.collection }, ({ issues: i, persons }: any) => + ops.eq(i.userId, persons.id), + ) + .select(({ issues: i, persons }: any) => ({ + id: i.id, + title: i.title, + name: persons.name, + })), + ) + await h.flush() + + expect(h.current().data).toHaveLength(ISSUES.length) + expect(h.current().data.find((r: any) => r.id === `i1`)).toMatchObject({ + title: `Issue 1`, + name: `John Doe`, + }) + h.unmount() + }) + + scenario( + `groupby-aggregate`, + `groupBy + count aggregates per group`, + async () => { + const people = driver.makeSource(SEED) + const h = driver.mount((q) => + q + .from({ items: people.collection }) + .groupBy(({ items }: any) => items.team) + .select(({ items }: any) => ({ + team: items.team, + count: ops.count(items.id), + })), + ) + await h.flush() + + const byTeam = new Map( + h.current().data.map((r: any) => [r.team, r.count]), + ) + expect(byTeam.get(`a`)).toBe(2) + expect(byTeam.get(`b`)).toBe(1) + h.unmount() + }, + ) + + // ---- gap-closers: engine features tested only by React today --------- + + scenario( + `nested-aggregates`, + `coalesce(count(...), 0) in a joined subquery`, + async () => { + const people = driver.makeSource(SEED) + const issues = driver.makeSource(ISSUES) + const h = driver.mount((q) => { + const issueCounts = q + .from({ issues: issues.collection }) + .groupBy(({ issues: i }: any) => i.userId) + .select(({ issues: i }: any) => ({ + userId: i.userId, + issueCount: ops.coalesce(ops.count(i.id), 0), + })) + return q + .from({ persons: people.collection }) + .leftJoin({ ic: issueCounts }, ({ persons, ic }: any) => + ops.eq(persons.id, ic.userId), + ) + .select(({ persons, ic }: any) => ({ + name: persons.name, + issueCount: ic.issueCount, + })) + }) + await h.flush() + + const byName = new Map( + h.current().data.map((r: any) => [r.name, r.issueCount]), + ) + expect(byName.get(`John Doe`)).toBe(2) + expect(byName.get(`Jane Doe`)).toBe(1) + h.unmount() + }, + ) + + scenario( + `includes-subquery`, + `select with a nested subquery produces child collections`, + async () => { + const people = driver.makeSource(SEED) + const issues = driver.makeSource(ISSUES) + const h = driver.mount((q) => + q.from({ persons: people.collection }).select(({ persons }: any) => ({ + id: persons.id, + name: persons.name, + issues: q + .from({ issues: issues.collection }) + .where(({ issues: i }: any) => ops.eq(i.userId, persons.id)) + .select(({ issues: i }: any) => ({ id: i.id, title: i.title })), + })), + ) + await h.flush() + + expect(h.current().data).toHaveLength(SEED.length) + const john = h.current().data.find((r: any) => r.id === `1`) + // `john.issues` is a child collection; read its contents through the + // collection API. John (id 1) has issues i1 and i3. + expect(john.issues).toBeDefined() + const johnIssueIds = Array.from(john.issues.values()) + .map((i: any) => i.id) + .sort() + expect(johnIssueIds).toEqual([`i1`, `i3`]) + h.unmount() + }, + ) + + // ---- free ports (no new capability needed) --------------------------- + + scenario(`live-update`, `a sync update is reflected in place`, async () => { + const source = driver.makeSource(SEED) + const h = driver.mount((q) => + q + .from({ items: source.collection }) + .select(({ items }: any) => ({ id: items.id, name: items.name })), + ) + await h.flush() + + source.update({ id: `1`, name: `Johnny Doe`, age: 30, team: `a` }) + await h.flush() + + expect(h.current().data.find((r: any) => r.id === `1`).name).toBe( + `Johnny Doe`, + ) + h.unmount() + }) + + scenario( + `findone-reactive`, + `findOne updates in place and becomes undefined on delete`, + async () => { + const source = driver.makeSource(SEED) + const h = driver.mount((q) => + q + .from({ items: source.collection }) + .where(({ items }: any) => ops.eq(items.id, `3`)) + .findOne(), + ) + await h.flush() + expect(h.current().data).toMatchObject({ name: `John Smith` }) + + source.update({ id: `3`, name: `Johnny Smith`, age: 35, team: `a` }) + await h.flush() + expect(h.current().data).toMatchObject({ name: `Johnny Smith` }) + + source.remove({ id: `3`, name: `Johnny Smith`, age: 35, team: `a` }) + await h.flush() + expect(h.current().data ?? undefined).toBeUndefined() + h.unmount() + }, + ) + + // ---- Tier 2: deferred readiness -------------------------------------- + + scenario( + `isready-transition`, + `isReady flips from false to true when the source readies`, + async () => { + const source = driver.makeDeferredSource() + const h = driver.mount((q) => + q + .from({ items: source.collection }) + .select(({ items }: any) => ({ id: items.id })), + ) + await h.flush() + expect(h.current().isReady).toBe(false) + + source.markReady() + await h.flush() + expect(h.current().isReady).toBe(true) + h.unmount() + }, + ) + + scenario( + `eager-visible-while-loading`, + `rows emitted before ready are visible while still loading`, + async () => { + const source = driver.makeDeferredSource() + const h = driver.mount((q) => + q + .from({ items: source.collection }) + .select(({ items }: any) => ({ id: items.id })), + ) + await h.flush() + + source.emit(SEED) + await h.flush() + + expect(h.current().isReady).toBe(false) + expect(h.current().data).toHaveLength(SEED.length) + + source.markReady() + await h.flush() + expect(h.current().isReady).toBe(true) + h.unmount() + }, + ) + + scenario( + `isready-no-data`, + `isReady becomes true even when the source readies with no rows`, + async () => { + const source = driver.makeDeferredSource() + const h = driver.mount((q) => + q + .from({ items: source.collection }) + .select(({ items }: any) => ({ id: items.id })), + ) + await h.flush() + + source.markReady() + await h.flush() + + expect(h.current().isReady).toBe(true) + expect(h.current().data ?? []).toHaveLength(0) + h.unmount() + }, + ) + + // ---- Tier 2: controllable input -------------------------------------- + + scenario( + `param-recompile`, + `changing a query parameter recompiles the result`, + async () => { + const source = driver.makeSource(SEED) + const h = driver.mountControllable( + (q, minAge) => + q + .from({ items: source.collection }) + .where(({ items }: any) => ops.gt(items.age, minAge)) + .select(({ items }: any) => ({ id: items.id })), + 30, + ) + await h.flush() + expect(h.current().data).toHaveLength(1) // age > 30 → John Smith + + await h.setParam(20) + expect(h.current().data).toHaveLength(3) // all + + await h.setParam(50) + expect(h.current().data).toHaveLength(0) // none + h.unmount() + }, + ) + + scenario( + `disabled-transition`, + `disabled -> enabled -> disabled toggles correctly`, + async () => { + const source = driver.makeSource(SEED) + const h = driver.mountControllable( + (q, enabled) => + enabled + ? q + .from({ items: source.collection }) + .select(({ items }: any) => ({ id: items.id })) + : null, + false, + ) + await h.flush() + expect(h.current().isEnabled).toBe(false) + + await h.setParam(true) + expect(h.current().isEnabled).toBe(true) + expect(h.current().data).toHaveLength(SEED.length) + + await h.setParam(false) + expect(h.current().isEnabled).toBe(false) + h.unmount() + }, + ) + + // ---- Tier 2: optimistic mutation ------------------------------------- + + scenario( + `optimistic-insert`, + `optimistic insert is visible immediately, then reconciles to the server key`, + async () => { + const source = driver.makeSource(SEED) + const h = driver.mount((q) => + q + .from({ items: source.collection }) + .select(({ items }: any) => ({ id: items.id })), + ) + await h.flush() + + const temp: Row = { id: `temp`, name: `New`, age: 20, team: `c` } + const perm: Row = { id: `p9`, name: `New`, age: 20, team: `c` } + // The "server" confirms only when we release it, so the optimistic + // window is deterministic rather than racing the settle. + let confirmServer!: () => void + const serverConfirmed = new Promise((resolve) => { + confirmServer = resolve + }) + const add = ops.createOptimisticAction({ + onMutate: () => source.collection.insert(temp), + mutationFn: async () => { + await serverConfirmed + source.remove(temp) + source.insert(perm) + }, + }) + + let tx!: { isPersisted: { promise: Promise } } + await h.apply(() => { + tx = add() + }) + // Optimistic row is visible before the server confirms. + expect(h.current().data.find((r: any) => r.id === `temp`)).toBeDefined() + + confirmServer() + await tx.isPersisted.promise + await h.flush() + // Reconciled: temp replaced by the permanent key. + expect( + h.current().data.find((r: any) => r.id === `temp`), + ).toBeUndefined() + expect(h.current().data.find((r: any) => r.id === `p9`)).toBeDefined() + h.unmount() + }, + ) + + // ---- Tier 3: input variants + error status --------------------------- + + scenario( + `precreated-collection-ready`, + `accepts a pre-created (syncing) live-query collection`, + async () => { + const source = driver.makeSource(SEED) + const pre = driver.makePrecreated( + (q) => + q + .from({ items: source.collection }) + .select(({ items }: any) => ({ id: items.id })), + { startSync: true }, + ) + const h = driver.mountCollection(pre.collection) + await h.flush() + + expect(h.current().isReady).toBe(true) + expect(h.current().data).toHaveLength(SEED.length) + h.unmount() + }, + ) + + scenario( + `precreated-not-syncing-isready-false`, + `a pre-created collection over a not-ready source reports isReady=false`, + async () => { + // Both the live query (startSync: false) and its source are not ready. + // Even if the adapter eagerly starts the collection on mount, it cannot + // become ready because the source never readies — so isReady stays false. + const source = driver.makeDeferredSource() + const pre = driver.makePrecreated( + (q) => + q + .from({ items: source.collection }) + .select(({ items }: any) => ({ id: items.id })), + { startSync: false }, + ) + const h = driver.mountCollection(pre.collection) + await h.flush() + expect(h.current().isReady).toBe(false) + h.unmount() + }, + ) + + scenario( + `config-object-input`, + `accepts the { query } config-object input form`, + async () => { + const source = driver.makeSource(SEED) + const h = driver.mountConfig((q) => + q + .from({ items: source.collection }) + .where(({ items }: any) => ops.eq(items.id, `3`)) + .select(({ items }: any) => ({ id: items.id, name: items.name })), + ) + await h.flush() + + expect(h.current().data).toHaveLength(1) + expect(h.current().data[0]).toMatchObject({ id: `3` }) + h.unmount() + }, + ) + + scenario( + `error-status`, + `a failing source surfaces an error (flag or boundary)`, + async () => { + const source = driver.makeErrorSource() + const h = driver.mountCollection(source.collection) + await h.flush() + + if (driver.errorSurface === `throw`) { + // Boundary model: reading the errored result throws (for an error + // boundary to catch), rather than exposing a readable flag. + expect(() => h.current()).toThrow() + } else { + expect(h.current().status).toBe(`error`) + expect(h.current().isError).toBe(true) + } + h.unmount() + }, + ) + + // ---- tail: universal expected-fail --------------------------- + + scenario( + `order-only-move`, + `an order-only move republishes the ordered result`, + async () => { + const source = driver.makeSource(SEED) + // Project only id+name; sort by age. Changing age reorders the result + // WITHOUT changing any projected row value. + const h = driver.mount((q) => + q + .from({ items: source.collection }) + .orderBy(({ items }: any) => items.age) + .select(({ items }: any) => ({ id: items.id, name: items.name })), + ) + await h.flush() + const first = h.current().data.map((r: any) => r.id) // ['2','1','3'] + + source.update({ id: `2`, name: `Jane Doe`, age: 99, team: `b` }) + await h.flush() + + expect(h.current().data.map((r: any) => r.id)).not.toEqual(first) + h.unmount() + }, + ) + + // ---- meta: guard against stale/misspelled expected-fail keys --------- + + it(`every knownGap / universal expected-fail references a real scenario`, () => { + for (const key of rawDriver.knownGaps ?? []) { + expect( + registeredKeys.has(key), + `${driver.name} knownGaps has "${key}", which is not a scenario key`, + ).toBe(true) + } + for (const key of UNIVERSAL_EXPECTED_FAIL) { + expect( + registeredKeys.has(key), + `UNIVERSAL_EXPECTED_FAIL has "${key}", which is not a scenario key`, + ).toBe(true) + } + }) + }) +} diff --git a/packages/db/tests/deterministic-ordering.test.ts b/packages/db/tests/deterministic-ordering.test.ts index 9ce9a326d6..160d03284b 100644 --- a/packages/db/tests/deterministic-ordering.test.ts +++ b/packages/db/tests/deterministic-ordering.test.ts @@ -489,5 +489,38 @@ describe(`Deterministic Ordering`, () => { const keys = changes?.map((c) => c.key) expect(keys).toEqual([`a`, `b`, `c`]) }) + + it(`should place NaN values consistently when ordering`, () => { + type Item = { id: string; score: number } + + const options = mockSyncCollectionOptions({ + id: `test-collection-changes-nan`, + getKey: (item) => item.id, + initialData: [], + }) + + const collection = createCollection(options) + + options.utils.begin() + options.utils.write({ type: `insert`, value: { id: `a`, score: 5 } }) + options.utils.write({ type: `insert`, value: { id: `nan`, score: NaN } }) + options.utils.write({ type: `insert`, value: { id: `b`, score: 1 } }) + options.utils.write({ type: `insert`, value: { id: `c`, score: 3 } }) + options.utils.commit() + + const changes = collection.currentStateAsChanges({ + orderBy: [ + { + expression: new PropRef([`score`]), + compareOptions: { direction: `asc`, nulls: `first` }, + }, + ], + }) + + // Under PostgreSQL float semantics NaN is the greatest value, so the + // numbers sort ascending first and NaN sorts last. + const keys = changes?.map((c) => c.key) + expect(keys).toEqual([`b`, `c`, `a`, `nan`]) + }) }) }) diff --git a/packages/db/tests/query/builder/functions.test.ts b/packages/db/tests/query/builder/functions.test.ts index cd40ce2e4c..e4d284960b 100644 --- a/packages/db/tests/query/builder/functions.test.ts +++ b/packages/db/tests/query/builder/functions.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, expectTypeOf, it } from 'vitest' import { CollectionImpl } from '../../../src/collection/index.js' import { Query, getQueryIR } from '../../../src/query/builder/index.js' import { @@ -12,6 +12,7 @@ import { coalesce, concat, count, + divide, eq, gt, gte, @@ -23,12 +24,16 @@ import { lte, max, min, + multiply, not, or, + subtract, sum, toArray, upper, } from '../../../src/query/builder/functions.js' +import { compileSingleRowExpression } from '../../../src/query/compiler/evaluators.js' +import type { BasicExpression } from '../../../src/query/ir.js' // Test schema interface Employee { @@ -324,5 +329,92 @@ describe(`QueryBuilder Functions`, () => { const select = builtQuery.select! expect((select.salary_plus_bonus as any).name).toBe(`add`) }) + + it(`subtract function works`, () => { + const query = new Query() + .from({ employees: employeesCollection }) + .select(({ employees }) => ({ + id: employees.id, + salary_minus_tax: subtract(employees.salary, 5000), + })) + + const builtQuery = getQueryIR(query) + const select = builtQuery.select! + expect((select.salary_minus_tax as any).name).toBe(`subtract`) + }) + + it(`multiply function works`, () => { + const query = new Query() + .from({ employees: employeesCollection }) + .select(({ employees }) => ({ + id: employees.id, + double_salary: multiply(employees.salary, 2), + })) + + const builtQuery = getQueryIR(query) + const select = builtQuery.select! + expect((select.double_salary as any).name).toBe(`multiply`) + }) + + it(`divide function works`, () => { + const query = new Query() + .from({ employees: employeesCollection }) + .select(({ employees }) => ({ + id: employees.id, + monthly_salary: divide(employees.salary, 12), + })) + + const builtQuery = getQueryIR(query) + const select = builtQuery.select! + expect((select.monthly_salary as any).name).toBe(`divide`) + }) + + it(`math functions can be combined for complex calculations`, () => { + const query = new Query() + .from({ employees: employeesCollection }) + .select(({ employees }) => ({ + id: employees.id, + // (salary * 1.1) - 500 = 10% raise minus deductions + adjusted_salary: subtract(multiply(employees.salary, 1.1), 500), + })) + + const builtQuery = getQueryIR(query) + const select = builtQuery.select! + expect((select.adjusted_salary as any).name).toBe(`subtract`) + }) + + it(`RED review: nullish operands are coalesced to 0 at runtime but widen types`, () => { + const subtractExpression = subtract(10, null) + expectTypeOf(subtractExpression).toEqualTypeOf>() + + const subtractResult = compileSingleRowExpression(subtractExpression)({}) + expect(subtractResult).toBe(10) + }) + + it(`RED review: divide can return null for non-null operand types`, () => { + const divideExpression = divide(10, 0) + expectTypeOf(divideExpression).toEqualTypeOf< + BasicExpression + >() + + const divideResult = compileSingleRowExpression(divideExpression)({}) + expect(divideResult).toBeNull() + }) + + it(`math functions can be used in orderBy`, () => { + const query = new Query() + .from({ employees: employeesCollection }) + .orderBy(({ employees }) => multiply(employees.salary, 2), `desc`) + .select(({ employees }) => ({ + id: employees.id, + salary: employees.salary, + })) + + const builtQuery = getQueryIR(query) + expect(builtQuery.orderBy).toBeDefined() + expect(builtQuery.orderBy).toHaveLength(1) + expect((builtQuery.orderBy![0]!.expression as any).name).toBe(`multiply`) + expect(builtQuery.orderBy![0]!.compareOptions.direction).toBe(`desc`) + }) }) }) diff --git a/packages/db/tests/query/compiler/evaluators.test.ts b/packages/db/tests/query/compiler/evaluators.test.ts index 69969de18a..4c5acb78c1 100644 --- a/packages/db/tests/query/compiler/evaluators.test.ts +++ b/packages/db/tests/query/compiler/evaluators.test.ts @@ -730,6 +730,87 @@ describe(`evaluators`, () => { expect(compiled({})).toBe(null) }) }) + + describe(`NaN (PostgreSQL float semantics)`, () => { + // Following PostgreSQL, NaN is equal to itself and greater than every + // other (non-null) value, so it has a well-defined order. + it(`treats NaN as equal to NaN`, () => { + const func = new Func(`eq`, [new Value(NaN), new Value(NaN)]) + expect(compileExpression(func)({})).toBe(true) + }) + + it(`treats NaN as not equal to a number`, () => { + const func = new Func(`eq`, [new Value(NaN), new Value(5)]) + expect(compileExpression(func)({})).toBe(false) + }) + + it(`still returns UNKNOWN when comparing NaN with null`, () => { + const func = new Func(`eq`, [new Value(NaN), new Value(null)]) + expect(compileExpression(func)({})).toBe(null) + }) + + it(`treats NaN as greater than every number`, () => { + expect( + compileExpression(new Func(`gt`, [new Value(NaN), new Value(5)]))( + {}, + ), + ).toBe(true) + expect( + compileExpression(new Func(`gt`, [new Value(5), new Value(NaN)]))( + {}, + ), + ).toBe(false) + expect( + compileExpression( + new Func(`gt`, [new Value(NaN), new Value(NaN)]), + )({}), + ).toBe(false) + }) + + it(`orders NaN with gte/lt/lte consistently`, () => { + // NaN >= anything (including NaN); nothing finite >= NaN + expect( + compileExpression( + new Func(`gte`, [new Value(NaN), new Value(5)]), + )({}), + ).toBe(true) + expect( + compileExpression( + new Func(`gte`, [new Value(NaN), new Value(NaN)]), + )({}), + ).toBe(true) + // NaN < nothing; a finite value < NaN + expect( + compileExpression(new Func(`lt`, [new Value(NaN), new Value(5)]))( + {}, + ), + ).toBe(false) + expect( + compileExpression(new Func(`lt`, [new Value(5), new Value(NaN)]))( + {}, + ), + ).toBe(true) + // NaN <= NaN; a finite value <= NaN + expect( + compileExpression( + new Func(`lte`, [new Value(NaN), new Value(NaN)]), + )({}), + ).toBe(true) + expect( + compileExpression( + new Func(`lte`, [new Value(5), new Value(NaN)]), + )({}), + ).toBe(true) + }) + + it(`matches NaN inside an IN list`, () => { + const func = new Func(`in`, [ + new Value(NaN), + new Value([NaN, 1, 2]), + ]) + expect(compileExpression(func)({})).toBe(true) + }) + }) }) describe(`boolean operators`, () => { diff --git a/packages/db/tests/query/generic-collection.test-d.ts b/packages/db/tests/query/generic-collection.test-d.ts new file mode 100644 index 0000000000..553b5f975b --- /dev/null +++ b/packages/db/tests/query/generic-collection.test-d.ts @@ -0,0 +1,53 @@ +import { describe, test } from 'vitest' +import { createLiveQueryCollection, eq } from '../../src/query/index.js' +import type { Collection } from '../../src/collection/index.js' + +// Regression tests for https://github.com/TanStack/db/issues/1677 +// Queries over a Collection where T is an unresolved generic type parameter +// must still expose the properties guaranteed by T's constraint inside +// where/join/select callbacks. This worked in 0.6.5 and broke in 0.6.6. + +describe(`queries over generic collection row types`, () => { + test(`where callback can access properties guaranteed by the type constraint`, () => { + function findById( + items: Collection, + id: string, + ) { + return createLiveQueryCollection((q) => + q.from({ items }).where(({ items: itemsRef }) => eq(itemsRef.id, id)), + ) + } + + void findById + }) + + test(`select callback can access properties guaranteed by the type constraint`, () => { + function selectIds(items: Collection) { + return createLiveQueryCollection((q) => + q.from({ items }).select(({ items: itemsRef }) => ({ + id: itemsRef.id, + })), + ) + } + + void selectIds + }) + + test(`subquery over a generic collection can be used as a join source`, () => { + function withDiff( + a: Collection, + b: Collection<{ id: string }, string>, + ) { + return createLiveQueryCollection((q) => { + const sub = q.from({ a }) + return q + .from({ b }) + .leftJoin({ sub }, ({ b: bRef, sub: subRef }) => + eq(bRef.id, subRef.id), + ) + }) + } + + void withDiff + }) +}) diff --git a/packages/db/tests/query/includes.test.ts b/packages/db/tests/query/includes.test.ts index 0124ebbeac..2eba6f122a 100644 --- a/packages/db/tests/query/includes.test.ts +++ b/packages/db/tests/query/includes.test.ts @@ -12,7 +12,11 @@ import { import { createCollection } from '../../src/collection/index.js' import { CleanupQueue } from '../../src/collection/cleanup-queue.js' import { localOnlyCollectionOptions } from '../../src/local-only.js' -import { mockSyncCollectionOptions, stripVirtualProps } from '../utils.js' +import { + flushPromises, + mockSyncCollectionOptions, + stripVirtualProps, +} from '../utils.js' import type { SyncConfig } from '../../src/types.js' type Project = { @@ -567,6 +571,60 @@ describe(`includes subqueries`, () => { }) describe(`change propagation`, () => { + it(`Collection includes: joined child update does not duplicate insert into child collection`, async () => { + type LineItem = { id: number; productId: number; qty: number } + type Product = { id: number; categoryId: number; name: string } + + const lineItems = createCollection( + mockSyncCollectionOptions({ + id: `includes-line-items`, + getKey: (lineItem) => lineItem.id, + initialData: [{ id: 1, productId: 10, qty: 1 }], + }), + ) + const products = createCollection( + mockSyncCollectionOptions({ + id: `includes-products`, + getKey: (product) => product.id, + initialData: [{ id: 10, categoryId: 1, name: `Widget` }], + }), + ) + + const collection = createLiveQueryCollection((q) => + q.from({ lineItem: lineItems }).select(({ lineItem }) => ({ + id: lineItem.id, + product: q + .from({ product: products }) + .where(({ product }) => eq(product.id, lineItem.productId)) + .select(({ product }) => ({ + id: product.id, + categoryId: product.categoryId, + name: product.name, + })), + })), + ) + await collection.preload() + + lineItems.utils.begin() + expect(() => { + lineItems.utils.write({ + type: `delete`, + value: { id: 1, productId: 10, qty: 1 }, + }) + lineItems.utils.write({ + type: `insert`, + value: { id: 1, productId: 10, qty: 2 }, + }) + }).not.toThrow() + lineItems.utils.commit() + + await vi.waitFor(() => { + expect(childItems((collection.get(1) as any).product)).toEqual([ + { id: 10, categoryId: 1, name: `Widget` }, + ]) + }) + }) + it(`Collection includes: child change does not re-emit the parent row`, async () => { const collection = buildIncludesQuery() await collection.preload() @@ -4902,6 +4960,1339 @@ describe(`includes subqueries`, () => { expect(data().runs[0].texts).toBe(run1TextsBefore) }) + + // Three collection levels (products -> priceRanges -> region). When two + // price ranges in different parent groups point at the same deepest + // correlation key (regionId 1, one under each product), each must still + // resolve its own copy of the nested `region` array. + it(`resolves nested grandchildren for sibling groups sharing a correlation key`, async () => { + type Product = { id: number; title: string } + type PriceRange = { id: number; productId: number; regionId: number } + type Region = { id: number; name: string } + + const products = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-products`, + getKey: (p) => p.id, + initialData: [ + { id: 1, title: `T-Shirt` }, + { id: 2, title: `Hoodie` }, + ], + }), + ) + + const priceRanges = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-price-ranges`, + getKey: (r) => r.id, + initialData: [ + { id: 1, productId: 1, regionId: 1 }, + { id: 2, productId: 1, regionId: 2 }, + { id: 3, productId: 2, regionId: 1 }, // same regionId as priceRange 1 + ], + }), + ) + + const regions = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-regions`, + getKey: (r) => r.id, + initialData: [ + { id: 1, name: `Europe` }, + { id: 2, name: `North America` }, + ], + }), + ) + + await Promise.all([ + products.preload(), + priceRanges.preload(), + regions.preload(), + ]) + + const collection = createLiveQueryCollection({ + id: `shared-corr-live`, + query: (q) => + q.from({ p: products }).select(({ p }) => ({ + id: p.id, + title: p.title, + priceRanges: toArray( + q + .from({ pr: priceRanges }) + .where(({ pr }) => eq(pr.productId, p.id)) + .select(({ pr }) => ({ + id: pr.id, + regionId: pr.regionId, + region: toArray( + q + .from({ r: regions }) + .where(({ r }) => eq(r.id, pr.regionId)) + .select(({ r }) => ({ id: r.id, name: r.name })), + ), + })), + ), + })), + }) + + await collection.preload() + + expect(toTree(collection)).toEqual([ + { + id: 1, + title: `T-Shirt`, + priceRanges: [ + { + id: 1, + regionId: 1, + region: [{ id: 1, name: `Europe` }], + }, + { + id: 2, + regionId: 2, + region: [{ id: 2, name: `North America` }], + }, + ], + }, + { + id: 2, + title: `Hoodie`, + priceRanges: [ + { + id: 3, + regionId: 1, + region: [{ id: 1, name: `Europe` }], + }, + ], + }, + ]) + }) + + // When a second parent group starts referencing a deepest correlation key + // that another group already resolved (the sibling price range is inserted + // after the initial load), the newly inserted group must also receive the + // nested grandchildren. + it(`fans nested grandchildren out to a sibling group inserted after load`, async () => { + type Product = { id: number; title: string } + type PriceRange = { id: number; productId: number; regionId: number } + type Region = { id: number; name: string } + + const products = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-incremental-products`, + getKey: (p) => p.id, + initialData: [ + { id: 1, title: `T-Shirt` }, + { id: 2, title: `Hoodie` }, + ], + }), + ) + const priceRanges = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-incremental-price-ranges`, + getKey: (r) => r.id, + initialData: [{ id: 1, productId: 1, regionId: 1 }], + }), + ) + const regions = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-incremental-regions`, + getKey: (r) => r.id, + initialData: [{ id: 1, name: `Europe` }], + }), + ) + + await Promise.all([ + products.preload(), + priceRanges.preload(), + regions.preload(), + ]) + + const collection = createLiveQueryCollection({ + id: `shared-corr-incremental-live`, + query: (q) => + q.from({ p: products }).select(({ p }) => ({ + id: p.id, + title: p.title, + priceRanges: toArray( + q + .from({ pr: priceRanges }) + .where(({ pr }) => eq(pr.productId, p.id)) + .select(({ pr }) => ({ + id: pr.id, + regionId: pr.regionId, + region: toArray( + q + .from({ r: regions }) + .where(({ r }) => eq(r.id, pr.regionId)) + .select(({ r }) => ({ id: r.id, name: r.name })), + ), + })), + ), + })), + }) + await collection.preload() + + // Insert a second price range under a different product, sharing regionId 1. + priceRanges.insert({ id: 3, productId: 2, regionId: 1 }) + await new Promise((r) => setTimeout(r, 50)) + + const tree = toTree(collection) + const tshirt = tree.find((p: any) => p.title === `T-Shirt`) + const hoodie = tree.find((p: any) => p.title === `Hoodie`) + expect(tshirt.priceRanges.find((pr: any) => pr.id === 1).region).toEqual([ + { id: 1, name: `Europe` }, + ]) + expect(hoodie.priceRanges.find((pr: any) => pr.id === 3).region).toEqual([ + { id: 1, name: `Europe` }, + ]) + }) + + it(`keeps deeper nested includes reactive for a sibling group added after load`, async () => { + type Product = { id: number; title: string } + type PriceRange = { id: number; productId: number; regionId: number } + type Region = { id: number; name: string; countryId: number } + type Country = { id: number; name: string } + + const products = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-late-sibling-products`, + getKey: (p) => p.id, + initialData: [ + { id: 1, title: `T-Shirt` }, + { id: 2, title: `Hoodie` }, + ], + }), + ) + const priceRanges = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-late-sibling-price-ranges`, + getKey: (r) => r.id, + initialData: [{ id: 1, productId: 1, regionId: 1 }], + }), + ) + const regions = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-late-sibling-regions`, + getKey: (r) => r.id, + initialData: [{ id: 1, name: `Europe`, countryId: 1 }], + }), + ) + const countries = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-late-sibling-countries`, + getKey: (c) => c.id, + initialData: [{ id: 1, name: `France` }], + }), + ) + + await Promise.all([ + products.preload(), + priceRanges.preload(), + regions.preload(), + countries.preload(), + ]) + + const collection = createLiveQueryCollection({ + id: `shared-corr-late-sibling-live`, + query: (q) => + q.from({ p: products }).select(({ p }) => ({ + id: p.id, + title: p.title, + priceRanges: toArray( + q + .from({ pr: priceRanges }) + .where(({ pr }) => eq(pr.productId, p.id)) + .select(({ pr }) => ({ + id: pr.id, + regionId: pr.regionId, + region: toArray( + q + .from({ r: regions }) + .where(({ r }) => eq(r.id, pr.regionId)) + .select(({ r }) => ({ + id: r.id, + name: r.name, + country: toArray( + q + .from({ c: countries }) + .where(({ c }) => eq(c.id, r.countryId)) + .select(({ c }) => ({ id: c.id, name: c.name })), + ), + })), + ), + })), + ), + })), + }) + await collection.preload() + + priceRanges.insert({ id: 2, productId: 2, regionId: 1 }) + await flushPromises() + + priceRanges.delete(1) + await flushPromises() + + countries.update(1, (draft) => { + draft.name = `Renamed France` + }) + await flushPromises() + + expect(toTree(collection)).toEqual([ + { + id: 1, + title: `T-Shirt`, + priceRanges: [], + }, + { + id: 2, + title: `Hoodie`, + priceRanges: [ + { + id: 2, + regionId: 1, + region: [ + { + id: 1, + name: `Europe`, + country: [{ id: 1, name: `Renamed France` }], + }, + ], + }, + ], + }, + ]) + }) + + it(`keeps shared nested includes reactive for sibling groups added after load at depth 3`, async () => { + type Product = { id: number; title: string } + type PriceRange = { id: number; productId: number; regionId: number } + type Region = { id: number; name: string } + + const products = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-depth-3-products`, + getKey: (p) => p.id, + initialData: [ + { id: 1, title: `T-Shirt` }, + { id: 2, title: `Hoodie` }, + ], + }), + ) + const priceRanges = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-depth-3-price-ranges`, + getKey: (r) => r.id, + initialData: [{ id: 1, productId: 1, regionId: 1 }], + }), + ) + const regions = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-depth-3-regions`, + getKey: (r) => r.id, + initialData: [{ id: 1, name: `Europe` }], + }), + ) + + await Promise.all([ + products.preload(), + priceRanges.preload(), + regions.preload(), + ]) + + const collection = createLiveQueryCollection({ + id: `shared-corr-depth-3-live`, + query: (q) => + q.from({ p: products }).select(({ p }) => ({ + id: p.id, + title: p.title, + priceRanges: toArray( + q + .from({ pr: priceRanges }) + .where(({ pr }) => eq(pr.productId, p.id)) + .select(({ pr }) => ({ + id: pr.id, + region: toArray( + q + .from({ r: regions }) + .where(({ r }) => eq(r.id, pr.regionId)) + .select(({ r }) => ({ id: r.id, name: r.name })), + ), + })), + ), + })), + }) + await collection.preload() + + priceRanges.insert({ id: 2, productId: 2, regionId: 1 }) + await flushPromises() + + regions.update(1, (draft) => { + draft.name = `Renamed Europe` + }) + await flushPromises() + + expect(toTree(collection)).toEqual([ + { + id: 1, + title: `T-Shirt`, + priceRanges: [{ id: 1, region: [{ id: 1, name: `Renamed Europe` }] }], + }, + { + id: 2, + title: `Hoodie`, + priceRanges: [{ id: 2, region: [{ id: 1, name: `Renamed Europe` }] }], + }, + ]) + }) + + it(`keeps shared nested includes reactive for sibling groups added after load at depth 4`, async () => { + type Product = { id: number; title: string } + type PriceRange = { id: number; productId: number; regionId: number } + type Region = { id: number; name: string; countryId: number } + type Country = { id: number; name: string } + + const products = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-depth-4-products`, + getKey: (p) => p.id, + initialData: [ + { id: 1, title: `T-Shirt` }, + { id: 2, title: `Hoodie` }, + ], + }), + ) + const priceRanges = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-depth-4-price-ranges`, + getKey: (r) => r.id, + initialData: [{ id: 1, productId: 1, regionId: 1 }], + }), + ) + const regions = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-depth-4-regions`, + getKey: (r) => r.id, + initialData: [{ id: 1, name: `Europe`, countryId: 1 }], + }), + ) + const countries = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-depth-4-countries`, + getKey: (c) => c.id, + initialData: [{ id: 1, name: `France` }], + }), + ) + + await Promise.all([ + products.preload(), + priceRanges.preload(), + regions.preload(), + countries.preload(), + ]) + + const collection = createLiveQueryCollection({ + id: `shared-corr-depth-4-live`, + query: (q) => + q.from({ p: products }).select(({ p }) => ({ + id: p.id, + title: p.title, + priceRanges: toArray( + q + .from({ pr: priceRanges }) + .where(({ pr }) => eq(pr.productId, p.id)) + .select(({ pr }) => ({ + id: pr.id, + region: toArray( + q + .from({ r: regions }) + .where(({ r }) => eq(r.id, pr.regionId)) + .select(({ r }) => ({ + id: r.id, + name: r.name, + country: toArray( + q + .from({ c: countries }) + .where(({ c }) => eq(c.id, r.countryId)) + .select(({ c }) => ({ id: c.id, name: c.name })), + ), + })), + ), + })), + ), + })), + }) + await collection.preload() + + priceRanges.insert({ id: 2, productId: 2, regionId: 1 }) + await flushPromises() + + countries.update(1, (draft) => { + draft.name = `Renamed France` + }) + await flushPromises() + + expect(toTree(collection)).toEqual([ + { + id: 1, + title: `T-Shirt`, + priceRanges: [ + { + id: 1, + region: [ + { + id: 1, + name: `Europe`, + country: [{ id: 1, name: `Renamed France` }], + }, + ], + }, + ], + }, + { + id: 2, + title: `Hoodie`, + priceRanges: [ + { + id: 2, + region: [ + { + id: 1, + name: `Europe`, + country: [{ id: 1, name: `Renamed France` }], + }, + ], + }, + ], + }, + ]) + }) + + it(`keeps shared nested includes reactive for sibling groups added after load at depth 5`, async () => { + type Product = { id: number; title: string } + type PriceRange = { id: number; productId: number; regionId: number } + type Region = { id: number; name: string; countryId: number } + type Country = { id: number; name: string; zoneId: number } + type Zone = { id: number; name: string } + + const products = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-depth-5-products`, + getKey: (p) => p.id, + initialData: [ + { id: 1, title: `T-Shirt` }, + { id: 2, title: `Hoodie` }, + ], + }), + ) + const priceRanges = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-depth-5-price-ranges`, + getKey: (r) => r.id, + initialData: [{ id: 1, productId: 1, regionId: 1 }], + }), + ) + const regions = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-depth-5-regions`, + getKey: (r) => r.id, + initialData: [{ id: 1, name: `Europe`, countryId: 1 }], + }), + ) + const countries = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-depth-5-countries`, + getKey: (c) => c.id, + initialData: [{ id: 1, name: `France`, zoneId: 1 }], + }), + ) + const zones = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-depth-5-zones`, + getKey: (z) => z.id, + initialData: [{ id: 1, name: `Eurozone` }], + }), + ) + + await Promise.all([ + products.preload(), + priceRanges.preload(), + regions.preload(), + countries.preload(), + zones.preload(), + ]) + + const collection = createLiveQueryCollection({ + id: `shared-corr-depth-5-live`, + query: (q) => + q.from({ p: products }).select(({ p }) => ({ + id: p.id, + title: p.title, + priceRanges: toArray( + q + .from({ pr: priceRanges }) + .where(({ pr }) => eq(pr.productId, p.id)) + .select(({ pr }) => ({ + id: pr.id, + region: toArray( + q + .from({ r: regions }) + .where(({ r }) => eq(r.id, pr.regionId)) + .select(({ r }) => ({ + id: r.id, + name: r.name, + country: toArray( + q + .from({ c: countries }) + .where(({ c }) => eq(c.id, r.countryId)) + .select(({ c }) => ({ + id: c.id, + name: c.name, + zone: toArray( + q + .from({ z: zones }) + .where(({ z }) => eq(z.id, c.zoneId)) + .select(({ z }) => ({ + id: z.id, + name: z.name, + })), + ), + })), + ), + })), + ), + })), + ), + })), + }) + await collection.preload() + + priceRanges.insert({ id: 2, productId: 2, regionId: 1 }) + await flushPromises() + + zones.update(1, (draft) => { + draft.name = `Renamed Eurozone` + }) + await flushPromises() + + expect(toTree(collection)).toEqual([ + { + id: 1, + title: `T-Shirt`, + priceRanges: [ + { + id: 1, + region: [ + { + id: 1, + name: `Europe`, + country: [ + { + id: 1, + name: `France`, + zone: [{ id: 1, name: `Renamed Eurozone` }], + }, + ], + }, + ], + }, + ], + }, + { + id: 2, + title: `Hoodie`, + priceRanges: [ + { + id: 2, + region: [ + { + id: 1, + name: `Europe`, + country: [ + { + id: 1, + name: `France`, + zone: [{ id: 1, name: `Renamed Eurozone` }], + }, + ], + }, + ], + }, + ], + }, + ]) + }) + + // When two parent groups share a deepest correlation key and one of them is + // deleted, the surviving group must keep its nested grandchildren. + it(`resolves two nested includes on the same child independently when they share a correlation value`, async () => { + type Product = { id: number; title: string } + type PriceRange = { + id: number + productId: number + regionId: number + currencyId: number + } + type Region = { id: number; name: string } + type Currency = { id: number; code: string } + + const products = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-value-products`, + getKey: (p) => p.id, + initialData: [{ id: 1, title: `T-Shirt` }], + }), + ) + // The price range points at region 1 and currency 1: both nested includes + // correlate on the same value. + const priceRanges = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-value-price-ranges`, + getKey: (r) => r.id, + initialData: [{ id: 1, productId: 1, regionId: 1, currencyId: 1 }], + }), + ) + const regions = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-value-regions`, + getKey: (r) => r.id, + initialData: [ + { id: 1, name: `Europe` }, + { id: 2, name: `North America` }, + ], + }), + ) + const currencies = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-value-currencies`, + getKey: (c) => c.id, + initialData: [{ id: 1, code: `EUR` }], + }), + ) + + await Promise.all([ + products.preload(), + priceRanges.preload(), + regions.preload(), + currencies.preload(), + ]) + + const collection = createLiveQueryCollection({ + id: `shared-corr-value-live`, + query: (q) => + q.from({ p: products }).select(({ p }) => ({ + id: p.id, + title: p.title, + priceRanges: toArray( + q + .from({ pr: priceRanges }) + .where(({ pr }) => eq(pr.productId, p.id)) + .select(({ pr }) => ({ + id: pr.id, + currency: toArray( + q + .from({ c: currencies }) + .where(({ c }) => eq(c.id, pr.currencyId)) + .select(({ c }) => ({ id: c.id, code: c.code })), + ), + region: toArray( + q + .from({ r: regions }) + .where(({ r }) => eq(r.id, pr.regionId)) + .select(({ r }) => ({ id: r.id, name: r.name })), + ), + })), + ), + })), + }) + await collection.preload() + + // Re-point only the region include; the currency include still resolves 1. + priceRanges.update(1, (draft) => { + draft.regionId = 2 + }) + await new Promise((r) => setTimeout(r, 50)) + + // A later currency change must still reach the currency include. + currencies.update(1, (draft) => { + draft.code = `USD` + }) + await new Promise((r) => setTimeout(r, 50)) + + expect(toTree(collection)).toEqual([ + { + id: 1, + title: `T-Shirt`, + priceRanges: [ + { + id: 1, + currency: [{ id: 1, code: `USD` }], + region: [{ id: 2, name: `North America` }], + }, + ], + }, + ]) + }) + + it(`isolates a nested correlation-key update from a second nested include on the same child`, async () => { + type Product = { id: number; title: string } + type PriceRange = { + id: number + productId: number + regionId: number + currencyId: number + } + type Region = { id: number; name: string } + type Currency = { id: number; code: string } + + const products = createCollection( + localOnlyCollectionOptions({ + id: `temp2-products`, + getKey: (p) => p.id, + initialData: [{ id: 1, title: `T-Shirt` }], + }), + ) + const priceRanges = createCollection( + localOnlyCollectionOptions({ + id: `temp2-price-ranges`, + getKey: (r) => r.id, + initialData: [{ id: 1, productId: 1, regionId: 1, currencyId: 9 }], + }), + ) + const regions = createCollection( + localOnlyCollectionOptions({ + id: `temp2-regions`, + getKey: (r) => r.id, + initialData: [ + { id: 1, name: `Europe` }, + { id: 2, name: `North America` }, + ], + }), + ) + const currencies = createCollection( + localOnlyCollectionOptions({ + id: `temp2-currencies`, + getKey: (c) => c.id, + initialData: [{ id: 9, code: `EUR` }], + }), + ) + + await Promise.all([ + products.preload(), + priceRanges.preload(), + regions.preload(), + currencies.preload(), + ]) + + const collection = createLiveQueryCollection({ + id: `temp2-live`, + query: (q) => + q.from({ p: products }).select(({ p }) => ({ + id: p.id, + title: p.title, + priceRanges: toArray( + q + .from({ pr: priceRanges }) + .where(({ pr }) => eq(pr.productId, p.id)) + .select(({ pr }) => ({ + id: pr.id, + region: toArray( + q + .from({ r: regions }) + .where(({ r }) => eq(r.id, pr.regionId)) + .select(({ r }) => ({ id: r.id, name: r.name })), + ), + currency: toArray( + q + .from({ c: currencies }) + .where(({ c }) => eq(c.id, pr.currencyId)) + .select(({ c }) => ({ id: c.id, code: c.code })), + ), + })), + ), + })), + }) + await collection.preload() + + // Change ONLY regionId; currency must still resolve, and a later currency + // rename must still reach this price range. + priceRanges.update(1, (draft) => { + draft.regionId = 2 + }) + await new Promise((r) => setTimeout(r, 50)) + + currencies.update(9, (draft) => { + draft.code = `USD` + }) + await new Promise((r) => setTimeout(r, 50)) + + expect(toTree(collection)).toEqual([ + { + id: 1, + title: `T-Shirt`, + priceRanges: [ + { + id: 1, + region: [{ id: 2, name: `North America` }], + currency: [{ id: 9, code: `USD` }], + }, + ], + }, + ]) + }) + + it(`keeps the survivor's data when a child changes its nested key then a sibling sharing the old key is deleted`, async () => { + type Product = { id: number; title: string } + type PriceRange = { id: number; productId: number; regionId: number } + type Region = { id: number; name: string } + + const products = createCollection( + localOnlyCollectionOptions({ + id: `temp-upd-products`, + getKey: (p) => p.id, + initialData: [{ id: 1, title: `T-Shirt` }], + }), + ) + const priceRanges = createCollection( + localOnlyCollectionOptions({ + id: `temp-upd-price-ranges`, + getKey: (r) => r.id, + initialData: [ + { id: 1, productId: 1, regionId: 1 }, + { id: 2, productId: 1, regionId: 1 }, + ], + }), + ) + const regions = createCollection( + localOnlyCollectionOptions({ + id: `temp-upd-regions`, + getKey: (r) => r.id, + initialData: [ + { id: 1, name: `Europe` }, + { id: 2, name: `North America` }, + ], + }), + ) + + await Promise.all([ + products.preload(), + priceRanges.preload(), + regions.preload(), + ]) + + const collection = createLiveQueryCollection({ + id: `temp-upd-live`, + query: (q) => + q.from({ p: products }).select(({ p }) => ({ + id: p.id, + title: p.title, + priceRanges: toArray( + q + .from({ pr: priceRanges }) + .where(({ pr }) => eq(pr.productId, p.id)) + .select(({ pr }) => ({ + id: pr.id, + regionId: pr.regionId, + region: toArray( + q + .from({ r: regions }) + .where(({ r }) => eq(r.id, pr.regionId)) + .select(({ r }) => ({ id: r.id, name: r.name })), + ), + })), + ), + })), + }) + await collection.preload() + + // pr_1 moves from region 1 to region 2 (both pr_1, pr_2 started at region 1) + priceRanges.update(1, (draft) => { + draft.regionId = 2 + }) + await new Promise((r) => setTimeout(r, 50)) + + // delete pr_2 (the remaining referencer of region 1) + priceRanges.delete(2) + await new Promise((r) => setTimeout(r, 50)) + + // rename region 1 — nothing references it anymore, must NOT affect pr_1 + regions.update(1, (draft) => { + draft.name = `Renamed Europe` + }) + await new Promise((r) => setTimeout(r, 50)) + + expect(toTree(collection)).toEqual([ + { + id: 1, + title: `T-Shirt`, + priceRanges: [ + { id: 1, regionId: 2, region: [{ id: 2, name: `North America` }] }, + ], + }, + ]) + }) + + it(`keeps grandchildren on the surviving sibling after the other is deleted`, async () => { + type Product = { id: number; title: string } + type PriceRange = { id: number; productId: number; regionId: number } + type Region = { id: number; name: string } + + const products = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-delete-products`, + getKey: (p) => p.id, + initialData: [ + { id: 1, title: `T-Shirt` }, + { id: 2, title: `Hoodie` }, + ], + }), + ) + const priceRanges = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-delete-price-ranges`, + getKey: (r) => r.id, + initialData: [ + { id: 1, productId: 1, regionId: 1 }, + { id: 3, productId: 2, regionId: 1 }, + ], + }), + ) + const regions = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-delete-regions`, + getKey: (r) => r.id, + initialData: [{ id: 1, name: `Europe` }], + }), + ) + + await Promise.all([ + products.preload(), + priceRanges.preload(), + regions.preload(), + ]) + + const collection = createLiveQueryCollection({ + id: `shared-corr-delete-live`, + query: (q) => + q.from({ p: products }).select(({ p }) => ({ + id: p.id, + title: p.title, + priceRanges: toArray( + q + .from({ pr: priceRanges }) + .where(({ pr }) => eq(pr.productId, p.id)) + .select(({ pr }) => ({ + id: pr.id, + regionId: pr.regionId, + region: toArray( + q + .from({ r: regions }) + .where(({ r }) => eq(r.id, pr.regionId)) + .select(({ r }) => ({ id: r.id, name: r.name })), + ), + })), + ), + })), + }) + await collection.preload() + + // Delete the Hoodie's price range (the sibling sharing regionId 1). + priceRanges.delete(3) + await new Promise((r) => setTimeout(r, 50)) + + const tree = toTree(collection) + const tshirt = tree.find((p: any) => p.title === `T-Shirt`) + const hoodie = tree.find((p: any) => p.title === `Hoodie`) + expect(tshirt.priceRanges.find((pr: any) => pr.id === 1).region).toEqual([ + { id: 1, name: `Europe` }, + ]) + expect(hoodie.priceRanges).toEqual([]) + }) + + it(`keeps routing when one of multiple same-parent siblings sharing a nested key is deleted`, async () => { + type Product = { id: number; title: string } + type PriceRange = { id: number; productId: number; regionId: number } + type Region = { id: number; name: string } + + const products = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-same-parent-products`, + getKey: (p) => p.id, + initialData: [{ id: 1, title: `T-Shirt` }], + }), + ) + const priceRanges = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-same-parent-price-ranges`, + getKey: (r) => r.id, + initialData: [ + { id: 1, productId: 1, regionId: 1 }, + { id: 2, productId: 1, regionId: 1 }, + ], + }), + ) + const regions = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-same-parent-regions`, + getKey: (r) => r.id, + initialData: [{ id: 1, name: `Europe` }], + }), + ) + + await Promise.all([ + products.preload(), + priceRanges.preload(), + regions.preload(), + ]) + + const collection = createLiveQueryCollection({ + id: `shared-corr-same-parent-live`, + query: (q) => + q.from({ p: products }).select(({ p }) => ({ + id: p.id, + title: p.title, + priceRanges: toArray( + q + .from({ pr: priceRanges }) + .where(({ pr }) => eq(pr.productId, p.id)) + .select(({ pr }) => ({ + id: pr.id, + regionId: pr.regionId, + region: toArray( + q + .from({ r: regions }) + .where(({ r }) => eq(r.id, pr.regionId)) + .select(({ r }) => ({ id: r.id, name: r.name })), + ), + })), + ), + })), + }) + await collection.preload() + + priceRanges.delete(1) + await new Promise((r) => setTimeout(r, 50)) + + regions.update(1, (draft) => { + draft.name = `Renamed Europe` + }) + await new Promise((r) => setTimeout(r, 50)) + + expect(toTree(collection)).toEqual([ + { + id: 1, + title: `T-Shirt`, + priceRanges: [ + { + id: 2, + regionId: 1, + region: [{ id: 1, name: `Renamed Europe` }], + }, + ], + }, + ]) + }) + + // The shared-correlation-key routing is independent of how each level is + // materialized, so the same guarantee must hold when the nested levels are + // left as live Collections (no toArray/materialize wrapper). + it(`resolves nested grandchildren for sibling groups when levels stay Collections`, async () => { + type Product = { id: number; title: string } + type PriceRange = { id: number; productId: number; regionId: number } + type Region = { id: number; name: string } + + const products = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-collection-products`, + getKey: (p) => p.id, + initialData: [ + { id: 1, title: `T-Shirt` }, + { id: 2, title: `Hoodie` }, + ], + }), + ) + const priceRanges = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-collection-price-ranges`, + getKey: (r) => r.id, + initialData: [ + { id: 1, productId: 1, regionId: 1 }, + { id: 2, productId: 1, regionId: 2 }, + { id: 3, productId: 2, regionId: 1 }, + ], + }), + ) + const regions = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-collection-regions`, + getKey: (r) => r.id, + initialData: [ + { id: 1, name: `Europe` }, + { id: 2, name: `North America` }, + ], + }), + ) + + await Promise.all([ + products.preload(), + priceRanges.preload(), + regions.preload(), + ]) + + const collection = createLiveQueryCollection({ + id: `shared-corr-collection-live`, + query: (q) => + q.from({ p: products }).select(({ p }) => ({ + id: p.id, + title: p.title, + priceRanges: q + .from({ pr: priceRanges }) + .where(({ pr }) => eq(pr.productId, p.id)) + .select(({ pr }) => ({ + id: pr.id, + regionId: pr.regionId, + region: q + .from({ r: regions }) + .where(({ r }) => eq(r.id, pr.regionId)) + .select(({ r }) => ({ id: r.id, name: r.name })), + })), + })), + }) + await collection.preload() + + // toTree recursively unwraps the nested live Collections into arrays. + expect(toTree(collection)).toEqual([ + { + id: 1, + title: `T-Shirt`, + priceRanges: [ + { id: 1, regionId: 1, region: [{ id: 1, name: `Europe` }] }, + { + id: 2, + regionId: 2, + region: [{ id: 2, name: `North America` }], + }, + ], + }, + { + id: 2, + title: `Hoodie`, + priceRanges: [ + { id: 3, regionId: 1, region: [{ id: 1, name: `Europe` }] }, + ], + }, + ]) + }) + + // Same guarantee for materialize(), which produces array/singleton + // snapshots through the same nested-includes routing. + it(`resolves nested grandchildren for sibling groups with materialize()`, async () => { + type Product = { id: number; title: string } + type PriceRange = { id: number; productId: number; regionId: number } + type Region = { id: number; name: string } + + const products = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-materialize-products`, + getKey: (p) => p.id, + initialData: [ + { id: 1, title: `T-Shirt` }, + { id: 2, title: `Hoodie` }, + ], + }), + ) + const priceRanges = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-materialize-price-ranges`, + getKey: (r) => r.id, + initialData: [ + { id: 1, productId: 1, regionId: 1 }, + { id: 2, productId: 1, regionId: 2 }, + { id: 3, productId: 2, regionId: 1 }, + ], + }), + ) + const regions = createCollection( + localOnlyCollectionOptions({ + id: `shared-corr-materialize-regions`, + getKey: (r) => r.id, + initialData: [ + { id: 1, name: `Europe` }, + { id: 2, name: `North America` }, + ], + }), + ) + + await Promise.all([ + products.preload(), + priceRanges.preload(), + regions.preload(), + ]) + + const collection = createLiveQueryCollection({ + id: `shared-corr-materialize-live`, + query: (q) => + q.from({ p: products }).select(({ p }) => ({ + id: p.id, + title: p.title, + priceRanges: materialize( + q + .from({ pr: priceRanges }) + .where(({ pr }) => eq(pr.productId, p.id)) + .select(({ pr }) => ({ + id: pr.id, + regionId: pr.regionId, + region: materialize( + q + .from({ r: regions }) + .where(({ r }) => eq(r.id, pr.regionId)) + .select(({ r }) => ({ id: r.id, name: r.name })), + ), + })), + ), + })), + }) + await collection.preload() + + expect(toTree(collection)).toEqual([ + { + id: 1, + title: `T-Shirt`, + priceRanges: [ + { id: 1, regionId: 1, region: [{ id: 1, name: `Europe` }] }, + { + id: 2, + regionId: 2, + region: [{ id: 2, name: `North America` }], + }, + ], + }, + { + id: 2, + title: `Hoodie`, + priceRanges: [ + { id: 3, regionId: 1, region: [{ id: 1, name: `Europe` }] }, + ], + }, + ]) + + // Post-load: insert a price range under Hoodie that references regionId 2, + // a correlation key already materialized for T-Shirt at load. This drives + // the late-arrival snapshot re-emit path through materialize() — the new + // sibling group must be seeded with the already-drained North America row + // without disturbing T-Shirt's existing nested rows. + priceRanges.insert({ id: 4, productId: 2, regionId: 2 }) + await new Promise((r) => setTimeout(r, 50)) + + const tree = toTree(collection) + const tshirt = tree.find((p: any) => p.title === `T-Shirt`) + const hoodie = tree.find((p: any) => p.title === `Hoodie`) + expect(tshirt.priceRanges.find((pr: any) => pr.id === 2).region).toEqual([ + { id: 2, name: `North America` }, + ]) + expect(hoodie.priceRanges.find((pr: any) => pr.id === 4).region).toEqual([ + { id: 2, name: `North America` }, + ]) + }) }) describe(`many sibling toArray includes with chained derived collections`, () => { diff --git a/packages/db/tests/query/join-subquery.test.ts b/packages/db/tests/query/join-subquery.test.ts index 093d6e62ae..eac49d6491 100644 --- a/packages/db/tests/query/join-subquery.test.ts +++ b/packages/db/tests/query/join-subquery.test.ts @@ -1,6 +1,7 @@ -import { beforeEach, describe, expect, test } from 'vitest' +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { createLiveQueryCollection, eq, gt } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' +import { BTreeIndex } from '../../src/indexes/btree-index.js' import { mockSyncCollectionOptions, stripVirtualProps } from '../utils.js' // Sample data types for join-subquery testing @@ -873,3 +874,82 @@ describe(`Join with Subqueries`, () => { createJoinSubqueryTests(`off`) createJoinSubqueryTests(`eager`) }) + +describe(`Lazy join: subquery whose join key resolves to an indexed collection`, () => { + type Team = { id: string } + type Member = { id: string; teamId: string } + + // `teams.id` is indexed; `members` has no index. + const makeTeamsCollection = () => + createCollection( + mockSyncCollectionOptions({ + id: `lazy-join-teams`, + getKey: (r) => r.id, + autoIndex: `eager`, + defaultIndexType: BTreeIndex, + initialData: [{ id: `t1` }], + }), + ) + const makeMembersCollection = () => + createCollection( + mockSyncCollectionOptions({ + id: `lazy-join-members`, + getKey: (r) => r.id, + autoIndex: `off`, + initialData: [{ id: `m1`, teamId: `t1` }], + }), + ) + + let teams: ReturnType + let members: ReturnType + let warnSpy: ReturnType + + beforeEach(() => { + teams = makeTeamsCollection() + members = makeMembersCollection() + warnSpy = vi.spyOn(console, `warn`).mockImplementation(() => {}) + }) + + afterEach(() => { + warnSpy.mockRestore() + }) + + // When a subquery used in a JOIN clause selects its join key from the + // *joined* side of the subquery (here `team.id`) rather than from its own + // FROM side (`member`), the outer join key resolves to `teams.id`, which is + // indexed. The lazy-join loader should therefore load through that index and + // must not emit a "Join requires an index" warning that points at the + // already-indexed `teams` collection. + test(`does not warn about an index that the resolved collection already has`, () => { + const joinQuery = createLiveQueryCollection({ + startSync: true, + query: (q) => { + const teamByMember = q + .from({ member: members }) + .leftJoin({ team: teams }, ({ team, member }) => + eq(team.id, member.teamId), + ) + .select(({ team }) => ({ teamId: team.id })) + + return q + .from({ m: members }) + .leftJoin({ memberTeam: teamByMember }, ({ m, memberTeam }) => + eq(memberTeam.teamId, m.teamId), + ) + .select(({ m }) => ({ id: m.id })) + }, + }) + + // Data flows correctly regardless (via fallback full-load today). + expect(joinQuery.toArray.map((r) => r.id)).toEqual([`m1`]) + + const indexWarnings = warnSpy.mock.calls + .map((c) => String(c[0])) + .filter((m) => m.includes(`Join requires an index`)) + + // `teams.id` is already indexed, so no warning should advise indexing it. + expect(indexWarnings.filter((m) => m.includes(`lazy-join-teams`))).toEqual( + [], + ) + }) +}) diff --git a/packages/db/tests/query/live-query-collection.test.ts b/packages/db/tests/query/live-query-collection.test.ts index 8b2b609016..a2613c0104 100644 --- a/packages/db/tests/query/live-query-collection.test.ts +++ b/packages/db/tests/query/live-query-collection.test.ts @@ -449,6 +449,21 @@ describe(`createLiveQueryCollection`, () => { }) }) + it(`should forward an explicit gcTime of 0 (disable GC) instead of coercing it to the default`, () => { + const options = liveQueryCollectionOptions({ + query: (q) => + q + .from({ user: usersCollection }) + .where(({ user }) => eq(user.active, true)), + gcTime: 0, + }) + + // gcTime: 0 disables garbage collection. A `|| 5000` fallback treats the + // explicit 0 as unset and silently replaces it with the 5s default, so the + // collection is garbage collected instead of being kept alive. + expect(options.gcTime).toBe(0) + }) + it(`should not reuse finalized graph after GC cleanup (resubscribe is safe)`, async () => { const liveQuery = createLiveQueryCollection({ query: (q) => @@ -594,6 +609,33 @@ describe(`createLiveQueryCollection`, () => { finalSubscription.unsubscribe() }) + it(`loads its data again when preloaded after the live query and its source collection were cleaned up`, async () => { + const activeUsers = createLiveQueryCollection({ + query: (q) => + q + .from({ user: usersCollection }) + .where(({ user }) => eq(user.active, true)), + }) + + await activeUsers.preload() + expect(activeUsers.status).toBe(`ready`) + expect(activeUsers.size).toBe(2) + + // Tear down the source collection and the live query, e.g. when switching + // to a different data set at runtime. Cleaning up a source collection puts + // the dependent live query into an error state. + await usersCollection.cleanup() + expect(activeUsers.status).toBe(`error`) + + await activeUsers.cleanup() + expect(activeUsers.status).toBe(`cleaned-up`) + + // Preloading again restarts sync and resolves once the data is loaded. + await activeUsers.preload() + expect(activeUsers.status).toBe(`ready`) + expect(activeUsers.size).toBe(2) + }) + it(`should handle temporal values correctly in live queries`, async () => { // Define a type with temporal values type Task = { diff --git a/packages/db/tests/query/select.test-d.ts b/packages/db/tests/query/select.test-d.ts index 225decdfb3..f4ec842922 100644 --- a/packages/db/tests/query/select.test-d.ts +++ b/packages/db/tests/query/select.test-d.ts @@ -1,6 +1,6 @@ import { describe, expectTypeOf, test } from 'vitest' import { createCollection } from '../../src/collection/index.js' -import { createLiveQueryCollection } from '../../src/query/index.js' +import { createLiveQueryCollection, eq } from '../../src/query/index.js' import { mockSyncCollectionOptions } from '../utils.js' import { upper } from '../../src/query/builder/functions.js' import type { OutputWithVirtual } from '../utils.js' @@ -109,6 +109,126 @@ describe(`select types`, () => { expectTypeOf(results).toMatchTypeOf>() }) + test(`select preserves union types and where works on common keys`, () => { + type ItemDocument = + | { type: 'pdf'; url: string; pages: number } + | { type: 'image'; url: string; width: number; height: number } + | { type: 'legacy'; path: string } + + type Item = { id: number; name: string; document: ItemDocument } + + const items = createCollection( + mockSyncCollectionOptions({ + id: `union-field-items`, + getKey: (i) => i.id, + initialData: [], + }), + ) + + // Filtering by a common key of the union should compile, + // and the result should preserve the full discriminated union + const col = createLiveQueryCollection((q) => + q + .from({ i: items }) + .where(({ i }) => eq(i.document.type, `pdf`)) + .select(({ i }) => ({ + id: i.id, + document: i.document, + })), + ) + + const result = col.toArray[0]! + expectTypeOf(result.document).toEqualTypeOf() + }) + + test(`select preserves union when nested under another field`, () => { + type Payload = + | { kind: 'text'; body: string } + | { kind: 'binary'; bytes: number; mime: string } + + type Envelope = { id: number; payload: { inner: Payload } } + + const envelopes = createCollection( + mockSyncCollectionOptions({ + id: `nested-union-envelopes`, + getKey: (e) => e.id, + initialData: [], + }), + ) + + // Selecting a nested object whose field is a discriminated union + // must preserve the union (not collapse to the intersection of keys). + const col = createLiveQueryCollection((q) => + q.from({ e: envelopes }).select(({ e }) => ({ + id: e.id, + payload: e.payload, + })), + ) + const r = col.toArray[0]! + expectTypeOf(r.payload).toEqualTypeOf<{ inner: Payload }>() + expectTypeOf(r.payload.inner).toEqualTypeOf() + }) + + test(`spread with a same-key narrower override projects the override type`, () => { + type SpreadUser = { + id: number + code: string | number + slug: string + nickname?: string + } + + const spreadUsers = createCollection( + mockSyncCollectionOptions({ + id: `spread-override-users`, + getKey: (u) => u.id, + initialData: [], + }), + ) + + const col = createLiveQueryCollection((q) => + q.from({ u: spreadUsers }).select(({ u }) => ({ + narrowed: { ...u, code: u.slug }, + })), + ) + + const result = col.toArray[0]! + // `code` was overridden with `u.slug` (string), so the projected + // field must be `string`, not the original `string | number`. + expectTypeOf(result.narrowed.code).toEqualTypeOf() + }) + + test(`spread that omits an optional property drops the key`, () => { + type SpreadUser = { + id: number + code: string | number + slug: string + nickname?: string + } + + const spreadUsers = createCollection( + mockSyncCollectionOptions({ + id: `spread-omit-users`, + getKey: (u) => u.id, + initialData: [], + }), + ) + + const col = createLiveQueryCollection((q) => + q.from({ u: spreadUsers }).select(({ u }) => { + const { nickname: _nickname, ...withoutNickname } = u + return { trimmed: withoutNickname } + }), + ) + + const _result = col.toArray[0]! + // `nickname` was destructured out, so the projected object must + // not reintroduce the key. + type HasNickname = `nickname` extends keyof typeof _result.trimmed + ? true + : false + expectTypeOf().toEqualTypeOf() + }) + test(`nested spread preserves object structure types`, () => { const users = createUsers() const col = createLiveQueryCollection((q) => { diff --git a/packages/db/tests/query/select.test.ts b/packages/db/tests/query/select.test.ts index 79dd481bb8..448936885f 100644 --- a/packages/db/tests/query/select.test.ts +++ b/packages/db/tests/query/select.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it } from 'vitest' import { createCollection } from '../../src/collection/index.js' -import { createLiveQueryCollection } from '../../src/query/index.js' +import { createLiveQueryCollection, queryOnce } from '../../src/query/index.js' +import { UnsafeAliasPathError } from '../../src/errors.js' import { mockSyncCollectionOptions } from '../utils.js' import { upper } from '../../src/query/builder/functions.js' @@ -129,3 +130,45 @@ describe(`nested select projections`, () => { }) }) }) + +function prototypeHasOwn(prop: string): boolean { + return Object.prototype.hasOwnProperty.call(Object.prototype, prop) +} + +describe(`select() alias prototype pollution`, () => { + let users: ReturnType + + beforeEach(() => { + users = createUsers() + }) + + it(`should reject __proto__ in alias path and not pollute Object.prototype`, async () => { + const hadBefore = prototypeHasOwn(`polluted`) + + await expect( + queryOnce((q) => + q.from({ user: users }).select(({ user }) => ({ + [`__proto__.polluted`]: user.name, + })), + ), + ).rejects.toThrow(UnsafeAliasPathError) + + expect(prototypeHasOwn(`polluted`)).toBe(hadBefore) + expect(prototypeHasOwn(`polluted`)).toBe(false) + }) + + it(`should reject constructor in alias path and not pollute Object.prototype`, async () => { + const hadBefore = prototypeHasOwn(`polluted`) + + await expect( + queryOnce((q) => + q.from({ user: users }).select(({ user }) => ({ + [`constructor.prototype.polluted`]: user.name, + })), + ), + ).rejects.toThrow(UnsafeAliasPathError) + + expect(prototypeHasOwn(`polluted`)).toBe(hadBefore) + expect(prototypeHasOwn(`polluted`)).toBe(false) + }) +}) diff --git a/packages/db/tests/uuid.test.ts b/packages/db/tests/uuid.test.ts new file mode 100644 index 0000000000..f6cf577e70 --- /dev/null +++ b/packages/db/tests/uuid.test.ts @@ -0,0 +1,76 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { safeRandomUUID } from '../src/utils/uuid' + +const UUID_V4_REGEX = + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/ + +describe(`safeRandomUUID helper`, () => { + afterEach(() => { + vi.restoreAllMocks() + vi.unstubAllGlobals() + }) + + it(`delegates to crypto.randomUUID when available`, () => { + const spy = vi + .spyOn(globalThis.crypto, `randomUUID`) + .mockReturnValue(`11111111-2222-4333-8444-555555555555`) + const id = safeRandomUUID() + expect(spy).toHaveBeenCalledTimes(1) + expect(id).toBe(`11111111-2222-4333-8444-555555555555`) + }) + + it(`falls back to getRandomValues when crypto.randomUUID is undefined (non-secure context)`, () => { + // Simulate a non-secure browser context where crypto.randomUUID is unavailable + // but getRandomValues remains. + vi.stubGlobal(`crypto`, { + randomUUID: undefined, + getRandomValues: (arr: Uint8Array) => { + // Deterministic-ish fill so we can verify version/variant bits land + // exactly where they should. + for (let i = 0; i < arr.length; i++) arr[i] = 0xff + return arr + }, + }) + + const id = safeRandomUUID() + expect(id).toMatch(UUID_V4_REGEX) + + // Verify version nibble == 4 and variant nibble in [8,9,a,b] + const versionChar = id[14] + const variantChar = id[19] + expect(versionChar).toBe(`4`) + expect([`8`, `9`, `a`, `b`]).toContain(variantChar) + + // With all bytes 0xff, expect ffffffff-ffff-4fff-bfff-ffffffffffff + expect(id).toBe(`ffffffff-ffff-4fff-bfff-ffffffffffff`) + }) + + it(`produces unique, well-formed UUIDs via the fallback path across many calls`, () => { + vi.stubGlobal(`crypto`, { + randomUUID: undefined, + getRandomValues: (arr: Uint8Array) => { + for (let i = 0; i < arr.length; i++) + arr[i] = Math.floor(Math.random() * 256) + return arr + }, + }) + + const seen = new Set() + for (let i = 0; i < 200; i++) { + const id = safeRandomUUID() + expect(id).toMatch(UUID_V4_REGEX) + seen.add(id) + } + expect(seen.size).toBe(200) + }) + + it(`throws when neither crypto.randomUUID nor crypto.getRandomValues is available`, () => { + vi.stubGlobal(`crypto`, {}) + expect(() => safeRandomUUID()).toThrow(/No secure random number generator/) + }) + + it(`throws when globalThis.crypto is undefined`, () => { + vi.stubGlobal(`crypto`, undefined) + expect(() => safeRandomUUID()).toThrow(/No secure random number generator/) + }) +}) diff --git a/packages/electric-db-collection/CHANGELOG.md b/packages/electric-db-collection/CHANGELOG.md index d8e0ee03db..b698868549 100644 --- a/packages/electric-db-collection/CHANGELOG.md +++ b/packages/electric-db-collection/CHANGELOG.md @@ -1,5 +1,65 @@ # @tanstack/electric-db-collection +## 0.3.14 + +### Patch Changes + +- Prevent progressive Electric collections from truncating persisted rows when resuming from saved Electric shape metadata. ([#1493](https://github.com/TanStack/db/pull/1493)) + +- Bound the wait for Electric stream refreshes before loading on-demand subsets so native fetch implementations that do not promptly abort long polls do not keep live queries loading until the poll times out. ([#1575](https://github.com/TanStack/db/pull/1575)) + +- Updated dependencies [[`8258d09`](https://github.com/TanStack/db/commit/8258d0955ab47c8510bd49ea59bcdbefd2ae054d), [`286964d`](https://github.com/TanStack/db/commit/286964d72612b59e3e427baabd9870f5a71a4281)]: + - @tanstack/db@0.6.16 + +## 0.3.13 + +### Patch Changes + +- Updated dependencies [[`eabcea7`](https://github.com/TanStack/db/commit/eabcea743fdfa045a2db01e12bef87403613102a), [`6d4c096`](https://github.com/TanStack/db/commit/6d4c096395b7ff3f428122ea8842bbead551a8c9)]: + - @tanstack/db@0.6.15 + +## 0.3.12 + +### Patch Changes + +- Updated dependencies [[`397e12a`](https://github.com/TanStack/db/commit/397e12a1224ad563e20a331eebcbe904cd4af948)]: + - @tanstack/db@0.6.14 + +## 0.3.11 + +### Patch Changes + +- Updated dependencies [[`99e9afe`](https://github.com/TanStack/db/commit/99e9afed46ab4083d66609a3e37ee44103c2177f), [`816b667`](https://github.com/TanStack/db/commit/816b6671c2cc9806715f6e6ed4410b3f4efb5afb)]: + - @tanstack/db@0.6.13 + +## 0.3.10 + +### Patch Changes + +- Updated dependencies [[`2b27dd1`](https://github.com/TanStack/db/commit/2b27dd1448da71c78a48e2390cb71b0ada1b1488)]: + - @tanstack/db@0.6.12 + +## 0.3.9 + +### Patch Changes + +- Updated dependencies [[`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`36fb29a`](https://github.com/TanStack/db/commit/36fb29ad7e906d39b6afdba2fd31e369c601bbb0), [`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`ac09b11`](https://github.com/TanStack/db/commit/ac09b1177a100eafa85cba3cd09dd1f53f933ded)]: + - @tanstack/db@0.6.11 + +## 0.3.8 + +### Patch Changes + +- Updated dependencies [[`307fdf8`](https://github.com/TanStack/db/commit/307fdf80f522a39a50e316316b3b75ba27fd5e84)]: + - @tanstack/db@0.6.10 + +## 0.3.7 + +### Patch Changes + +- Updated dependencies [[`2147345`](https://github.com/TanStack/db/commit/2147345236ceee6e73d9fc6c0cdc2385833199fc), [`00389a4`](https://github.com/TanStack/db/commit/00389a47b258ad58fc3a03c5cc6f66957b9bd2d1)]: + - @tanstack/db@0.6.9 + ## 0.3.6 ### Patch Changes diff --git a/packages/electric-db-collection/package.json b/packages/electric-db-collection/package.json index a265310672..23da8ac76f 100644 --- a/packages/electric-db-collection/package.json +++ b/packages/electric-db-collection/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/electric-db-collection", - "version": "0.3.6", + "version": "0.3.14", "description": "ElectricSQL collection for TanStack DB", "author": "Kyle Mathews", "license": "MIT", diff --git a/packages/electric-db-collection/src/electric.ts b/packages/electric-db-collection/src/electric.ts index 72a9a072e0..1b63237cff 100644 --- a/packages/electric-db-collection/src/electric.ts +++ b/packages/electric-db-collection/src/electric.ts @@ -64,6 +64,8 @@ export { isChangeMessage, isControlMessage } from '@electric-sql/client' const debug = DebugModule.debug(`ts/db:electric`) +const FORCE_DISCONNECT_AND_REFRESH_TIMEOUT_MS = 250 + /** * Symbol for internal test hooks (hidden from public API) */ @@ -481,11 +483,23 @@ function createLoadSubsetDedupe>({ // When the stream is already up-to-date, it may be in a long-poll wait. // Forcing a disconnect-and-refresh ensures requestSnapshot gets a response // from a fresh server round-trip rather than waiting for the current poll to end. - // If the refresh fails (e.g., PauseLock held during subscriber processing in - // join pipelines), we fall through to requestSnapshot which still works. + // Some native fetch implementations (notably React Native/Expo) may not abort + // long-poll requests promptly. Bound the wait so on-demand live queries don't + // remain loading until the long-poll naturally times out. + // If the refresh fails or times out, we fall through to requestSnapshot which + // still works. if (stream.isUpToDate) { + let timeoutId: ReturnType | undefined try { - await stream.forceDisconnectAndRefresh() + await Promise.race([ + stream.forceDisconnectAndRefresh(), + new Promise((resolve) => { + timeoutId = setTimeout( + resolve, + FORCE_DISCONNECT_AND_REFRESH_TIMEOUT_MS, + ) + }), + ]) } catch (error) { if (handleSnapshotError(error, `forceDisconnectAndRefresh`)) { return @@ -494,6 +508,8 @@ function createLoadSubsetDedupe>({ `${logPrefix}forceDisconnectAndRefresh failed, proceeding to requestSnapshot: %o`, error, ) + } finally { + clearTimeout(timeoutId) } } @@ -1441,7 +1457,11 @@ function createElectricSync>( let transactionStarted = false const newTxids = new Set() const newSnapshots: Array = [] - let hasReceivedUpToDate = false // Track if we've completed initial sync in progressive mode + // Track if we've completed initial sync in progressive mode. A persisted + // resume starts from an already-committed stream offset, so the next + // up-to-date message must not run the initial atomic swap again. + let hasReceivedUpToDate = + syncMode === `progressive` && canUsePersistedResume // Progressive mode state // Helper to determine if we're buffering the initial sync diff --git a/packages/electric-db-collection/tests/electric.test.ts b/packages/electric-db-collection/tests/electric.test.ts index 868c085213..c48121a9c1 100644 --- a/packages/electric-db-collection/tests/electric.test.ts +++ b/packages/electric-db-collection/tests/electric.test.ts @@ -95,16 +95,22 @@ describe(`Electric Integration`, () => { const createPersistedAdapter = ( collectionMetadata?: Map, + rows: Map = new Map(), ) => ({ - loadSubset: async () => [], - loadCollectionMetadata: async () => - Array.from((collectionMetadata ?? new Map()).entries()).map( - ([key, value]) => ({ - key, - value, - }), + loadSubset: () => + Promise.resolve( + Array.from(rows.entries()).map(([key, value]) => ({ key, value })), + ), + loadCollectionMetadata: () => + Promise.resolve( + Array.from((collectionMetadata ?? new Map()).entries()).map( + ([key, value]) => ({ + key, + value, + }), + ), ), - applyCommittedTx: async (_collectionId: string, tx: any) => { + applyCommittedTx: (_collectionId: string, tx: any) => { for (const mutation of tx.collectionMetadataMutations ?? []) { if (mutation.type === `delete`) { collectionMetadata?.delete(mutation.key) @@ -112,8 +118,19 @@ describe(`Electric Integration`, () => { collectionMetadata?.set(mutation.key, mutation.value) } } + if (tx.truncate) { + rows.clear() + } + for (const mutation of tx.mutations ?? []) { + if (mutation.type === `delete`) { + rows.delete(mutation.key) + } else { + rows.set(mutation.key, mutation.value) + } + } + return Promise.resolve() }, - ensureIndex: async () => {}, + ensureIndex: () => Promise.resolve(), }) beforeEach(() => { @@ -2565,6 +2582,122 @@ describe(`Electric Integration`, () => { expect(mockRequestSnapshot).toHaveBeenCalledTimes(1) }) + it(`should request the snapshot after the refresh timeout and ignore late fulfillment`, async () => { + vi.useFakeTimers() + try { + let resolveRefresh: () => void = () => {} + const refresh = new Promise((resolve) => { + resolveRefresh = resolve + }) + mockStream.isUpToDate = true + mockForceDisconnectAndRefresh.mockReturnValueOnce(refresh) + + const testCollection = createCollection( + electricCollectionOptions({ + id: `on-demand-refresh-timeout-fulfillment-test`, + shapeOptions: { + url: `http://test-url`, + params: { table: `test_table` }, + }, + syncMode: `on-demand`, + getKey: (item: Row) => item.id as number, + startSync: true, + }), + ) + + let loadSettled = false + const load = Promise.resolve( + testCollection._sync.loadSubset({ limit: 10 }), + ).then(() => { + loadSettled = true + }) + + await vi.advanceTimersByTimeAsync(249) + expect(mockRequestSnapshot).not.toHaveBeenCalled() + expect(loadSettled).toBe(false) + + await vi.advanceTimersByTimeAsync(1) + await load + expect(mockRequestSnapshot).toHaveBeenCalledTimes(1) + expect(loadSettled).toBe(true) + await testCollection.cleanup() + expect(vi.getTimerCount()).toBe(0) + + resolveRefresh() + await refresh + await Promise.resolve() + expect(mockRequestSnapshot).toHaveBeenCalledTimes(1) + expect(vi.getTimerCount()).toBe(0) + } finally { + vi.useRealTimers() + } + }) + + it(`should handle late refresh rejection after requesting the snapshot`, async () => { + vi.useFakeTimers() + try { + let rejectRefresh: (error: Error) => void = () => {} + const refresh = new Promise((_resolve, reject) => { + rejectRefresh = reject + }) + mockStream.isUpToDate = true + mockForceDisconnectAndRefresh.mockReturnValueOnce(refresh) + + const testCollection = createCollection( + electricCollectionOptions({ + id: `on-demand-refresh-timeout-rejection-test`, + shapeOptions: { + url: `http://test-url`, + params: { table: `test_table` }, + }, + syncMode: `on-demand`, + getKey: (item: Row) => item.id as number, + startSync: true, + }), + ) + + const load = testCollection._sync.loadSubset({ limit: 10 }) + await vi.advanceTimersByTimeAsync(250) + await load + + rejectRefresh(new Error(`late refresh failure`)) + await expect(refresh).rejects.toThrow(`late refresh failure`) + await Promise.resolve() + expect(mockRequestSnapshot).toHaveBeenCalledTimes(1) + expect(vi.getTimerCount()).toBe(0) + } finally { + vi.useRealTimers() + } + }) + + it(`should clear the refresh timeout when refresh settles early`, async () => { + vi.useFakeTimers() + try { + mockStream.isUpToDate = true + mockForceDisconnectAndRefresh.mockResolvedValueOnce(undefined) + + const testCollection = createCollection( + electricCollectionOptions({ + id: `on-demand-refresh-clears-timeout-test`, + shapeOptions: { + url: `http://test-url`, + params: { table: `test_table` }, + }, + syncMode: `on-demand`, + getKey: (item: Row) => item.id as number, + startSync: true, + }), + ) + + await testCollection._sync.loadSubset({ limit: 10 }) + + expect(mockRequestSnapshot).toHaveBeenCalledTimes(1) + expect(vi.getTimerCount()).toBe(0) + } finally { + vi.useRealTimers() + } + }) + it(`should fetch snapshots in progressive mode when loadSubset is called before sync completes`, async () => { vi.clearAllMocks() @@ -3179,6 +3312,96 @@ describe(`Electric Integration`, () => { ) }) + it(`should preserve hydrated rows and resumed changes received before up-to-date`, async () => { + vi.clearAllMocks() + + const { ShapeStream } = await import(`@electric-sql/client`) + mockFetchSnapshot.mockResolvedValue({ + metadata: {}, + data: [], + }) + + const collectionMetadata = new Map([ + [ + `electric:resume`, + { + kind: `resume`, + offset: `10_0`, + handle: `handle-1`, + shapeId: `{"params":{"table":"test_table"},"url":"http://test-url"}`, + updatedAt: 1, + }, + ], + ]) + const persistedRows = new Map([ + [1, { id: 1, name: `Persisted User` }], + ]) + + const persistedCollection = createCollection( + persistedCollectionOptions({ + ...(electricCollectionOptions({ + id: `persisted-progressive-resume-test`, + shapeOptions: { + url: `http://test-url`, + params: { + table: `test_table`, + }, + }, + syncMode: `progressive` as const, + getKey: (item: Row) => item.id as number, + startSync: true, + }) as any), + persistence: { + adapter: createPersistedAdapter(collectionMetadata, persistedRows), + }, + }) as any, + ) + + persistedCollection.startSyncImmediate() + await persistedCollection._sync.loadSubset({ limit: 10 }) + + expect(ShapeStream).toHaveBeenCalledWith( + expect.objectContaining({ + offset: `10_0`, + handle: `handle-1`, + }), + ) + expect(stripVirtualProps(persistedCollection.get(1))).toEqual({ + id: 1, + name: `Persisted User`, + }) + + subscriber([ + { + key: `2`, + value: { id: 2, name: `Resumed User` }, + headers: { operation: `insert` }, + }, + ]) + subscriber([ + { + headers: { control: `up-to-date` }, + }, + ]) + + expect( + [persistedCollection.get(1), persistedCollection.get(2)].map( + stripVirtualProps, + ), + ).toEqual([ + { id: 1, name: `Persisted User` }, + { id: 2, name: `Resumed User` }, + ]) + await vi.waitFor(() => { + expect( + [persistedRows.get(1), persistedRows.get(2)].map(stripVirtualProps), + ).toEqual([ + { id: 1, name: `Persisted User` }, + { id: 2, name: `Resumed User` }, + ]) + }) + }) + it(`should not mix explicit handle with persisted offset`, async () => { vi.clearAllMocks() diff --git a/packages/electron-db-sqlite-persistence/CHANGELOG.md b/packages/electron-db-sqlite-persistence/CHANGELOG.md index 9a74f0ce27..9662079f5a 100644 --- a/packages/electron-db-sqlite-persistence/CHANGELOG.md +++ b/packages/electron-db-sqlite-persistence/CHANGELOG.md @@ -1,5 +1,63 @@ # @tanstack/electron-db-sqlite-persistence +## 0.1.20 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.8 + +## 0.1.19 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.7 + +## 0.1.18 + +### Patch Changes + +- Updated dependencies [[`f7da776`](https://github.com/TanStack/db/commit/f7da77660b16cbfe30817fb5c938267d696c8d1c)]: + - @tanstack/db-sqlite-persistence-core@0.2.6 + +## 0.1.17 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.5 + +## 0.1.16 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.4 + +## 0.1.15 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.3 + +## 0.1.14 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.2 + +## 0.1.13 + +### Patch Changes + +- Use a safe `randomUUID` helper that falls back to `crypto.getRandomValues` when `crypto.randomUUID` is unavailable (non-secure browser contexts such as dev servers reached via a LAN IP over HTTP). Fixes #1541. ([#1593](https://github.com/TanStack/db/pull/1593)) + +- Updated dependencies [[`00389a4`](https://github.com/TanStack/db/commit/00389a47b258ad58fc3a03c5cc6f66957b9bd2d1)]: + - @tanstack/db-sqlite-persistence-core@0.2.1 + ## 0.1.12 ### Patch Changes diff --git a/packages/electron-db-sqlite-persistence/package.json b/packages/electron-db-sqlite-persistence/package.json index 18f38372f7..345d506fdd 100644 --- a/packages/electron-db-sqlite-persistence/package.json +++ b/packages/electron-db-sqlite-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/electron-db-sqlite-persistence", - "version": "0.1.12", + "version": "0.1.20", "description": "Electron SQLite persisted collection bridge for TanStack DB", "author": "TanStack Team", "license": "MIT", diff --git a/packages/electron-db-sqlite-persistence/src/electron-coordinator.ts b/packages/electron-db-sqlite-persistence/src/electron-coordinator.ts index ea9735e709..a4c6bb7fe8 100644 --- a/packages/electron-db-sqlite-persistence/src/electron-coordinator.ts +++ b/packages/electron-db-sqlite-persistence/src/electron-coordinator.ts @@ -1,3 +1,4 @@ +import { safeRandomUUID } from '@tanstack/db-sqlite-persistence-core' import type { ApplyLocalMutationsResponse, PersistedCollectionCoordinator, @@ -118,7 +119,7 @@ export type ElectronCollectionCoordinatorOptions = { // --------------------------------------------------------------------------- export class ElectronCollectionCoordinator implements PersistedCollectionCoordinator { - private readonly nodeId = crypto.randomUUID() + private readonly nodeId = safeRandomUUID() private readonly dbName: string private adapter: AdapterWithPullSince | null private readonly channel: BroadcastChannel @@ -205,7 +206,7 @@ export class ElectronCollectionCoordinator implements PersistedCollectionCoordin error?: string }>(collectionId, { type: `rpc:ensureRemoteSubset:req`, - rpcId: crypto.randomUUID(), + rpcId: safeRandomUUID(), options, }) @@ -233,7 +234,7 @@ export class ElectronCollectionCoordinator implements PersistedCollectionCoordin error?: string }>(collectionId, { type: `rpc:ensurePersistedIndex:req`, - rpcId: crypto.randomUUID(), + rpcId: safeRandomUUID(), signature, spec, }) @@ -252,16 +253,16 @@ export class ElectronCollectionCoordinator implements PersistedCollectionCoordin if (this.isLeader(collectionId)) { return this.handleApplyLocalMutations(collectionId, { type: `rpc:applyLocalMutations:req`, - rpcId: crypto.randomUUID(), - envelopeId: crypto.randomUUID(), + rpcId: safeRandomUUID(), + envelopeId: safeRandomUUID(), mutations, }) } return this.sendRPC(collectionId, { type: `rpc:applyLocalMutations:req`, - rpcId: crypto.randomUUID(), - envelopeId: crypto.randomUUID(), + rpcId: safeRandomUUID(), + envelopeId: safeRandomUUID(), mutations, }) } @@ -273,14 +274,14 @@ export class ElectronCollectionCoordinator implements PersistedCollectionCoordin if (this.isLeader(collectionId)) { return this.handlePullSince(collectionId, { type: `rpc:pullSince:req`, - rpcId: crypto.randomUUID(), + rpcId: safeRandomUUID(), fromRowVersion, }) } return this.sendRPC(collectionId, { type: `rpc:pullSince:req`, - rpcId: crypto.randomUUID(), + rpcId: safeRandomUUID(), fromRowVersion, }) } @@ -663,7 +664,7 @@ export class ElectronCollectionCoordinator implements PersistedCollectionCoordin // Build and apply the persisted transaction const tx = { - txId: crypto.randomUUID(), + txId: safeRandomUUID(), term, seq, rowVersion, diff --git a/packages/expo-db-sqlite-persistence/CHANGELOG.md b/packages/expo-db-sqlite-persistence/CHANGELOG.md index b1cded9a2c..fb65632a3f 100644 --- a/packages/expo-db-sqlite-persistence/CHANGELOG.md +++ b/packages/expo-db-sqlite-persistence/CHANGELOG.md @@ -1,5 +1,61 @@ # @tanstack/expo-db-sqlite-persistence +## 0.2.8 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.8 + +## 0.2.7 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.7 + +## 0.2.6 + +### Patch Changes + +- Updated dependencies [[`f7da776`](https://github.com/TanStack/db/commit/f7da77660b16cbfe30817fb5c938267d696c8d1c)]: + - @tanstack/db-sqlite-persistence-core@0.2.6 + +## 0.2.5 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.5 + +## 0.2.4 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.4 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.3 + +## 0.2.2 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.2 + +## 0.2.1 + +### Patch Changes + +- Updated dependencies [[`00389a4`](https://github.com/TanStack/db/commit/00389a47b258ad58fc3a03c5cc6f66957b9bd2d1)]: + - @tanstack/db-sqlite-persistence-core@0.2.1 + ## 0.2.0 ### Minor Changes diff --git a/packages/expo-db-sqlite-persistence/e2e/expo-runtime-app/CHANGELOG.md b/packages/expo-db-sqlite-persistence/e2e/expo-runtime-app/CHANGELOG.md index 935e723540..92509453f5 100644 --- a/packages/expo-db-sqlite-persistence/e2e/expo-runtime-app/CHANGELOG.md +++ b/packages/expo-db-sqlite-persistence/e2e/expo-runtime-app/CHANGELOG.md @@ -1,5 +1,69 @@ # @tanstack/expo-db-sqlite-persistence-e2e-app +## 0.0.20 + +### Patch Changes + +- Updated dependencies [[`8258d09`](https://github.com/TanStack/db/commit/8258d0955ab47c8510bd49ea59bcdbefd2ae054d), [`286964d`](https://github.com/TanStack/db/commit/286964d72612b59e3e427baabd9870f5a71a4281)]: + - @tanstack/db@0.6.16 + - @tanstack/expo-db-sqlite-persistence@0.2.8 + +## 0.0.19 + +### Patch Changes + +- Updated dependencies [[`eabcea7`](https://github.com/TanStack/db/commit/eabcea743fdfa045a2db01e12bef87403613102a), [`6d4c096`](https://github.com/TanStack/db/commit/6d4c096395b7ff3f428122ea8842bbead551a8c9)]: + - @tanstack/db@0.6.15 + - @tanstack/expo-db-sqlite-persistence@0.2.7 + +## 0.0.18 + +### Patch Changes + +- Updated dependencies [[`397e12a`](https://github.com/TanStack/db/commit/397e12a1224ad563e20a331eebcbe904cd4af948)]: + - @tanstack/db@0.6.14 + - @tanstack/expo-db-sqlite-persistence@0.2.6 + +## 0.0.17 + +### Patch Changes + +- Updated dependencies [[`99e9afe`](https://github.com/TanStack/db/commit/99e9afed46ab4083d66609a3e37ee44103c2177f), [`816b667`](https://github.com/TanStack/db/commit/816b6671c2cc9806715f6e6ed4410b3f4efb5afb)]: + - @tanstack/db@0.6.13 + - @tanstack/expo-db-sqlite-persistence@0.2.5 + +## 0.0.16 + +### Patch Changes + +- Updated dependencies [[`2b27dd1`](https://github.com/TanStack/db/commit/2b27dd1448da71c78a48e2390cb71b0ada1b1488)]: + - @tanstack/db@0.6.12 + - @tanstack/expo-db-sqlite-persistence@0.2.4 + +## 0.0.15 + +### Patch Changes + +- Updated dependencies [[`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`36fb29a`](https://github.com/TanStack/db/commit/36fb29ad7e906d39b6afdba2fd31e369c601bbb0), [`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`ac09b11`](https://github.com/TanStack/db/commit/ac09b1177a100eafa85cba3cd09dd1f53f933ded)]: + - @tanstack/db@0.6.11 + - @tanstack/expo-db-sqlite-persistence@0.2.3 + +## 0.0.14 + +### Patch Changes + +- Updated dependencies [[`307fdf8`](https://github.com/TanStack/db/commit/307fdf80f522a39a50e316316b3b75ba27fd5e84)]: + - @tanstack/db@0.6.10 + - @tanstack/expo-db-sqlite-persistence@0.2.2 + +## 0.0.13 + +### Patch Changes + +- Updated dependencies [[`2147345`](https://github.com/TanStack/db/commit/2147345236ceee6e73d9fc6c0cdc2385833199fc), [`00389a4`](https://github.com/TanStack/db/commit/00389a47b258ad58fc3a03c5cc6f66957b9bd2d1)]: + - @tanstack/db@0.6.9 + - @tanstack/expo-db-sqlite-persistence@0.2.1 + ## 0.0.12 ### Patch Changes diff --git a/packages/expo-db-sqlite-persistence/e2e/expo-runtime-app/package.json b/packages/expo-db-sqlite-persistence/e2e/expo-runtime-app/package.json index 8922e1a5c6..351f644dfc 100644 --- a/packages/expo-db-sqlite-persistence/e2e/expo-runtime-app/package.json +++ b/packages/expo-db-sqlite-persistence/e2e/expo-runtime-app/package.json @@ -1,7 +1,7 @@ { "name": "@tanstack/expo-db-sqlite-persistence-e2e-app", "private": true, - "version": "0.0.12", + "version": "0.0.20", "main": "index.js", "scripts": { "start": "expo start", diff --git a/packages/expo-db-sqlite-persistence/package.json b/packages/expo-db-sqlite-persistence/package.json index 7ff8f3554c..2cc04d03b5 100644 --- a/packages/expo-db-sqlite-persistence/package.json +++ b/packages/expo-db-sqlite-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/expo-db-sqlite-persistence", - "version": "0.2.0", + "version": "0.2.8", "description": "Expo SQLite persisted collection adapter for TanStack DB", "author": "TanStack Team", "license": "MIT", diff --git a/packages/node-db-sqlite-persistence/CHANGELOG.md b/packages/node-db-sqlite-persistence/CHANGELOG.md index b427f31a5e..f9d57554ca 100644 --- a/packages/node-db-sqlite-persistence/CHANGELOG.md +++ b/packages/node-db-sqlite-persistence/CHANGELOG.md @@ -1,5 +1,61 @@ # @tanstack/node-db-sqlite-persistence +## 0.2.8 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.8 + +## 0.2.7 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.7 + +## 0.2.6 + +### Patch Changes + +- Updated dependencies [[`f7da776`](https://github.com/TanStack/db/commit/f7da77660b16cbfe30817fb5c938267d696c8d1c)]: + - @tanstack/db-sqlite-persistence-core@0.2.6 + +## 0.2.5 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.5 + +## 0.2.4 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.4 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.3 + +## 0.2.2 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.2 + +## 0.2.1 + +### Patch Changes + +- Updated dependencies [[`00389a4`](https://github.com/TanStack/db/commit/00389a47b258ad58fc3a03c5cc6f66957b9bd2d1)]: + - @tanstack/db-sqlite-persistence-core@0.2.1 + ## 0.2.0 ### Minor Changes diff --git a/packages/node-db-sqlite-persistence/package.json b/packages/node-db-sqlite-persistence/package.json index 4391f7b071..48b856e1ed 100644 --- a/packages/node-db-sqlite-persistence/package.json +++ b/packages/node-db-sqlite-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/node-db-sqlite-persistence", - "version": "0.2.0", + "version": "0.2.8", "description": "Node SQLite persisted collection adapter for TanStack DB", "author": "TanStack Team", "license": "MIT", diff --git a/packages/offline-transactions/CHANGELOG.md b/packages/offline-transactions/CHANGELOG.md index 2057952ea4..dc66c1b59b 100644 --- a/packages/offline-transactions/CHANGELOG.md +++ b/packages/offline-transactions/CHANGELOG.md @@ -1,5 +1,63 @@ # @tanstack/offline-transactions +## 1.0.41 + +### Patch Changes + +- Updated dependencies [[`8258d09`](https://github.com/TanStack/db/commit/8258d0955ab47c8510bd49ea59bcdbefd2ae054d), [`286964d`](https://github.com/TanStack/db/commit/286964d72612b59e3e427baabd9870f5a71a4281)]: + - @tanstack/db@0.6.16 + +## 1.0.40 + +### Patch Changes + +- Updated dependencies [[`eabcea7`](https://github.com/TanStack/db/commit/eabcea743fdfa045a2db01e12bef87403613102a), [`6d4c096`](https://github.com/TanStack/db/commit/6d4c096395b7ff3f428122ea8842bbead551a8c9)]: + - @tanstack/db@0.6.15 + +## 1.0.39 + +### Patch Changes + +- Updated dependencies [[`397e12a`](https://github.com/TanStack/db/commit/397e12a1224ad563e20a331eebcbe904cd4af948)]: + - @tanstack/db@0.6.14 + +## 1.0.38 + +### Patch Changes + +- Updated dependencies [[`99e9afe`](https://github.com/TanStack/db/commit/99e9afed46ab4083d66609a3e37ee44103c2177f), [`816b667`](https://github.com/TanStack/db/commit/816b6671c2cc9806715f6e6ed4410b3f4efb5afb)]: + - @tanstack/db@0.6.13 + +## 1.0.37 + +### Patch Changes + +- Updated dependencies [[`2b27dd1`](https://github.com/TanStack/db/commit/2b27dd1448da71c78a48e2390cb71b0ada1b1488)]: + - @tanstack/db@0.6.12 + +## 1.0.36 + +### Patch Changes + +- Updated dependencies [[`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`36fb29a`](https://github.com/TanStack/db/commit/36fb29ad7e906d39b6afdba2fd31e369c601bbb0), [`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`ac09b11`](https://github.com/TanStack/db/commit/ac09b1177a100eafa85cba3cd09dd1f53f933ded)]: + - @tanstack/db@0.6.11 + +## 1.0.35 + +### Patch Changes + +- Updated dependencies [[`307fdf8`](https://github.com/TanStack/db/commit/307fdf80f522a39a50e316316b3b75ba27fd5e84)]: + - @tanstack/db@0.6.10 + +## 1.0.34 + +### Patch Changes + +- Use a safe `randomUUID` helper that falls back to `crypto.getRandomValues` when `crypto.randomUUID` is unavailable (non-secure browser contexts such as dev servers reached via a LAN IP over HTTP). Fixes #1541. ([#1593](https://github.com/TanStack/db/pull/1593)) + +- Updated dependencies [[`2147345`](https://github.com/TanStack/db/commit/2147345236ceee6e73d9fc6c0cdc2385833199fc), [`00389a4`](https://github.com/TanStack/db/commit/00389a47b258ad58fc3a03c5cc6f66957b9bd2d1)]: + - @tanstack/db@0.6.9 + ## 1.0.33 ### Patch Changes diff --git a/packages/offline-transactions/package.json b/packages/offline-transactions/package.json index 872a47b924..bc8294f67f 100644 --- a/packages/offline-transactions/package.json +++ b/packages/offline-transactions/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/offline-transactions", - "version": "1.0.33", + "version": "1.0.41", "description": "Offline-first transaction capabilities for TanStack DB", "author": "TanStack", "license": "MIT", diff --git a/packages/offline-transactions/src/OfflineExecutor.ts b/packages/offline-transactions/src/OfflineExecutor.ts index 8f443277c2..a6140cfebc 100644 --- a/packages/offline-transactions/src/OfflineExecutor.ts +++ b/packages/offline-transactions/src/OfflineExecutor.ts @@ -1,5 +1,9 @@ // Storage adapters -import { createOptimisticAction, createTransaction } from '@tanstack/db' +import { + createOptimisticAction, + createTransaction, + safeRandomUUID, +} from '@tanstack/db' import { IndexedDBAdapter } from './storage/IndexedDBAdapter' import { LocalStorageAdapter } from './storage/LocalStorageAdapter' @@ -367,7 +371,7 @@ export class OfflineExecutor { mutationFn: (params) => mutationFn({ ...params, - idempotencyKey: options.idempotencyKey || crypto.randomUUID(), + idempotencyKey: options.idempotencyKey || safeRandomUUID(), }), metadata: options.metadata, }) @@ -399,7 +403,7 @@ export class OfflineExecutor { mutationFn({ ...vars, ...params, - idempotencyKey: crypto.randomUUID(), + idempotencyKey: safeRandomUUID(), }), onMutate: options.onMutate, }) diff --git a/packages/offline-transactions/src/api/OfflineTransaction.ts b/packages/offline-transactions/src/api/OfflineTransaction.ts index 32c96fd26a..af13484a75 100644 --- a/packages/offline-transactions/src/api/OfflineTransaction.ts +++ b/packages/offline-transactions/src/api/OfflineTransaction.ts @@ -1,4 +1,4 @@ -import { createTransaction } from '@tanstack/db' +import { createTransaction, safeRandomUUID } from '@tanstack/db' import { NonRetriableError } from '../types' import type { PendingMutation, Transaction } from '@tanstack/db' import type { @@ -23,10 +23,10 @@ export class OfflineTransaction { persistTransaction: (tx: OfflineTransactionType) => Promise, executor: any, ) { - this.offlineId = crypto.randomUUID() + this.offlineId = safeRandomUUID() this.mutationFnName = options.mutationFnName this.autoCommit = options.autoCommit ?? true - this.idempotencyKey = options.idempotencyKey ?? crypto.randomUUID() + this.idempotencyKey = options.idempotencyKey ?? safeRandomUUID() this.metadata = options.metadata ?? {} this.persistTransaction = persistTransaction this.executor = executor diff --git a/packages/offline-transactions/src/coordination/BroadcastChannelLeader.ts b/packages/offline-transactions/src/coordination/BroadcastChannelLeader.ts index ce11a8abc3..7f50d06459 100644 --- a/packages/offline-transactions/src/coordination/BroadcastChannelLeader.ts +++ b/packages/offline-transactions/src/coordination/BroadcastChannelLeader.ts @@ -1,3 +1,4 @@ +import { safeRandomUUID } from '@tanstack/db' import { BaseLeaderElection } from './LeaderElection' interface LeaderMessage { @@ -19,7 +20,7 @@ export class BroadcastChannelLeader extends BaseLeaderElection { constructor(channelName = `offline-executor-leader`) { super() this.channelName = channelName - this.tabId = crypto.randomUUID() + this.tabId = safeRandomUUID() this.setupChannel() } diff --git a/packages/powersync-db-collection/CHANGELOG.md b/packages/powersync-db-collection/CHANGELOG.md index b5447bdea9..365bf47b4e 100644 --- a/packages/powersync-db-collection/CHANGELOG.md +++ b/packages/powersync-db-collection/CHANGELOG.md @@ -1,5 +1,61 @@ # @tanstack/powersync-db-collection +## 0.1.54 + +### Patch Changes + +- Updated dependencies [[`8258d09`](https://github.com/TanStack/db/commit/8258d0955ab47c8510bd49ea59bcdbefd2ae054d), [`286964d`](https://github.com/TanStack/db/commit/286964d72612b59e3e427baabd9870f5a71a4281)]: + - @tanstack/db@0.6.16 + +## 0.1.53 + +### Patch Changes + +- Updated dependencies [[`eabcea7`](https://github.com/TanStack/db/commit/eabcea743fdfa045a2db01e12bef87403613102a), [`6d4c096`](https://github.com/TanStack/db/commit/6d4c096395b7ff3f428122ea8842bbead551a8c9)]: + - @tanstack/db@0.6.15 + +## 0.1.52 + +### Patch Changes + +- Updated dependencies [[`397e12a`](https://github.com/TanStack/db/commit/397e12a1224ad563e20a331eebcbe904cd4af948)]: + - @tanstack/db@0.6.14 + +## 0.1.51 + +### Patch Changes + +- Updated dependencies [[`99e9afe`](https://github.com/TanStack/db/commit/99e9afed46ab4083d66609a3e37ee44103c2177f), [`816b667`](https://github.com/TanStack/db/commit/816b6671c2cc9806715f6e6ed4410b3f4efb5afb)]: + - @tanstack/db@0.6.13 + +## 0.1.50 + +### Patch Changes + +- Updated dependencies [[`2b27dd1`](https://github.com/TanStack/db/commit/2b27dd1448da71c78a48e2390cb71b0ada1b1488)]: + - @tanstack/db@0.6.12 + +## 0.1.49 + +### Patch Changes + +- Updated dependencies [[`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`36fb29a`](https://github.com/TanStack/db/commit/36fb29ad7e906d39b6afdba2fd31e369c601bbb0), [`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`ac09b11`](https://github.com/TanStack/db/commit/ac09b1177a100eafa85cba3cd09dd1f53f933ded)]: + - @tanstack/db@0.6.11 + +## 0.1.48 + +### Patch Changes + +- Updated dependencies [[`307fdf8`](https://github.com/TanStack/db/commit/307fdf80f522a39a50e316316b3b75ba27fd5e84)]: + - @tanstack/db@0.6.10 + +## 0.1.47 + +### Patch Changes + +- Updated dependencies [[`2147345`](https://github.com/TanStack/db/commit/2147345236ceee6e73d9fc6c0cdc2385833199fc), [`00389a4`](https://github.com/TanStack/db/commit/00389a47b258ad58fc3a03c5cc6f66957b9bd2d1)]: + - @tanstack/db@0.6.9 + ## 0.1.46 ### Patch Changes diff --git a/packages/powersync-db-collection/package.json b/packages/powersync-db-collection/package.json index 93444c4f26..5f41c617f7 100644 --- a/packages/powersync-db-collection/package.json +++ b/packages/powersync-db-collection/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/powersync-db-collection", - "version": "0.1.46", + "version": "0.1.54", "description": "PowerSync collection for TanStack DB", "author": "POWERSYNC", "license": "MIT", @@ -59,11 +59,11 @@ "p-defer": "^4.0.1" }, "peerDependencies": { - "@powersync/common": "^1.41.0" + "@powersync/common": "^1.57.0" }, "devDependencies": { - "@powersync/common": "1.49.0", - "@powersync/node": "0.18.1", + "@powersync/common": "1.57.0", + "@powersync/node": "0.19.2", "@types/debug": "^4.1.12", "@vitest/coverage-istanbul": "^3.2.4", "better-sqlite3": "^12.6.2" diff --git a/packages/powersync-db-collection/src/attachments.ts b/packages/powersync-db-collection/src/attachments.ts new file mode 100644 index 0000000000..d3c48fb1d8 --- /dev/null +++ b/packages/powersync-db-collection/src/attachments.ts @@ -0,0 +1,156 @@ +import { + AttachmentQueue, + AttachmentState +} from '@powersync/common' +import { createTransaction } from '@tanstack/db' +import { PowerSyncTransactor } from './PowerSyncTransactor' + +import type { + AbstractPowerSyncDatabase, + AttachmentData, + AttachmentQueueOptions, + + AttachmentTable} from '@powersync/common' +import type { Collection } from '@tanstack/db' +import type { OptionalExtractedTable } from './helpers' + +export type TanStackDBAttachmentQueueOptions = AttachmentQueueOptions & { + /** + * For TanStack, we want access to the synced TanStackDB collection. + * In order to have the same relational data be set in a single transaction. + * This also allows for joining both TanStackDB collections. + */ + attachmentsCollection: Collection +} + +export interface SaveOptions { + data: AttachmentData + fileExtension: string + mediaType?: string + metaData?: string + id?: string + /** + * Called within the same TanStackDB transaction as the attachment write, + * so any mutations made to other collections are committed atomically with it. + */ + updateHook?: (attachment: AttachmentQueueRow) => void +} + +export interface DeleteOptions { + id: string + /** + * Called within the same TanStackDB transaction as the attachment write, + * so any mutations made to other collections are committed atomically with it. + */ + updateHook?: (attachment: AttachmentQueueRow) => void +} + +export type AttachmentQueueRow = OptionalExtractedTable + +/** + * A custom extension of the PowerSyncAttachmentQueue for TanStackDB. + */ +export class TanStackDBAttachmentQueue extends AttachmentQueue { + readonly powersync: AbstractPowerSyncDatabase + readonly collection: Collection + + constructor(params: TanStackDBAttachmentQueueOptions) { + super(params) + this.powersync = params.db + this.collection = params.attachmentsCollection + } + + /** + * Saves a file to local storage and queues it for upload to remote storage. + * + * Exposes an `updateHook` option which is called inside a TanStackDB transaction, + * relational associations with the provided attachment ID should be made in this hook. + */ + async save({ + data, + fileExtension, + mediaType, + metaData, + id, + updateHook, + }: SaveOptions): Promise { + const resolvedId = id ?? (await this.generateAttachmentId()) + const filename = `${resolvedId}.${fileExtension}` + const localUri = this.localStorage.getLocalUri(filename) + const size = await this.localStorage.saveFile(localUri, data) + + const attachment: AttachmentQueueRow = { + id: resolvedId, + filename, + media_type: mediaType ?? null, + local_uri: localUri, + state: AttachmentState.QUEUED_UPLOAD, + has_synced: 0, + size, + timestamp: new Date().getTime(), + meta_data: metaData ?? null, + } + + /** + * We use the attachmentService lock to prevent attachment queue race conditions — specifically, + * it stops the watcher from treating a newly inserted attachment record as one that needs + * to be downloaded. + * */ + await this.withAttachmentContext(async (ctx) => { + const tanStackDBTransaction = createTransaction({ + autoCommit: false, + mutationFn: async ({ transaction }) => { + await new PowerSyncTransactor({ + database: ctx.db, + }).applyTransaction(transaction) + }, + }) + + tanStackDBTransaction.mutate(() => { + this.collection.insert(attachment) + // allow the user to associate values in this transaction + updateHook?.(attachment) + }) + + await tanStackDBTransaction.commit() + }) + + return attachment + } + + /** + * Queues a file for deletion from local and remote storage. + * + * Exposes an `updateHook` option which is called inside a TanStackDB transaction, + * relational associations with the provided attachment ID should be cleaned up in this hook. + */ + async delete({ id, updateHook }: DeleteOptions): Promise { + await this.withAttachmentContext(async (ctx) => { + const tanStackDBTransaction = createTransaction({ + autoCommit: false, + mutationFn: async ({ transaction }) => { + await new PowerSyncTransactor({ + database: ctx.db, + }).applyTransaction(transaction) + }, + }) + + tanStackDBTransaction.mutate(() => { + const attachment = this.collection.get(id) + if (!attachment) { + throw new Error(`Attachment with id ${id} not found`) + } + + this.collection.update(id, (draft) => { + draft.state = AttachmentState.QUEUED_DELETE + draft.has_synced = 0 + }) + + // allow the user to associate values in this transaction + updateHook?.(attachment) + }) + + await tanStackDBTransaction.commit() + }) + } +} diff --git a/packages/powersync-db-collection/src/index.ts b/packages/powersync-db-collection/src/index.ts index f8d0928056..f96a7a0ee4 100644 --- a/packages/powersync-db-collection/src/index.ts +++ b/packages/powersync-db-collection/src/index.ts @@ -1,3 +1,4 @@ +export * from './attachments' export * from './definitions' export * from './powersync' export * from './PowerSyncTransactor' diff --git a/packages/powersync-db-collection/tests/attachments.test.ts b/packages/powersync-db-collection/tests/attachments.test.ts new file mode 100644 index 0000000000..c5925e234f --- /dev/null +++ b/packages/powersync-db-collection/tests/attachments.test.ts @@ -0,0 +1,401 @@ +import { randomUUID } from 'node:crypto' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { + AttachmentState, + AttachmentTable, + Schema, + Table, + column, +} from '@powersync/common' +import { NodeFileSystemAdapter, PowerSyncDatabase } from '@powersync/node' +import { + createCollection, + isNull, + liveQueryCollectionOptions, + not, +} from '@tanstack/db' +import { describe, expect, it, onTestFinished, vi } from 'vitest' +import { powerSyncCollectionOptions } from '../src' +import { TanStackDBAttachmentQueue } from '../src/attachments' +import { TEST_DATABASE_IMPLEMENTATION } from './test-db-implementation' +import type { + AttachmentErrorHandler, + RemoteStorageAdapter, + WatchedAttachmentItem, +} from '@powersync/common' + +// A minimal valid 1x1 pixel JPEG used as the remote payload for downloads. +const MOCK_JPEG_U8A = [ + 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, + 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xff, 0xd9, +] +const createMockJpegBuffer = (): ArrayBuffer => + new Uint8Array(MOCK_JPEG_U8A).buffer + +const SYNC_INTERVAL_MS = 300 +const WAIT_TIMEOUT = 8000 + +const APP_SCHEMA = new Schema({ + users: new Table({ + name: column.text, + email: column.text, + photo_id: column.text, + }), + attachments: new AttachmentTable(), +}) + +type WatchAttachments = ( + onUpdate: (attachments: Array) => Promise, + signal: AbortSignal, +) => void + +const describePowerSync = TEST_DATABASE_IMPLEMENTATION + ? describe + : describe.skip + +describePowerSync(`PowerSync AttachmentQueue (TanStackDB)`, () => { + async function setup() { + const db = new PowerSyncDatabase({ + database: { + dbFilename: `attachments-test-${randomUUID()}.sqlite`, + dbLocation: tmpdir(), + implementation: TEST_DATABASE_IMPLEMENTATION, + }, + schema: APP_SCHEMA, + }) + await db.disconnectAndClear() + + const localStorage = new NodeFileSystemAdapter( + join(tmpdir(), `ps-attachments-${randomUUID()}`), + ) + await localStorage.initialize() + + const uploadFile = vi.fn(() => + Promise.resolve(), + ) + const downloadFile = vi.fn(() => + Promise.resolve(createMockJpegBuffer()), + ) + const deleteFile = vi.fn(() => + Promise.resolve(), + ) + const remoteStorage: RemoteStorageAdapter = { + uploadFile, + downloadFile, + deleteFile, + } + + const attachmentsCollection = createCollection( + powerSyncCollectionOptions({ + database: db, + table: APP_SCHEMA.props.attachments, + }), + ) + const usersCollection = createCollection( + powerSyncCollectionOptions({ + database: db, + table: APP_SCHEMA.props.users, + }), + ) + await Promise.all([ + attachmentsCollection.stateWhenReady(), + usersCollection.stateWhenReady(), + ]) + + onTestFinished(async () => { + attachmentsCollection.cleanup() + usersCollection.cleanup() + await db.disconnectAndClear() + await db.close() + await localStorage.clear().catch(() => {}) + }) + + function createQueue( + overrides: { + watchAttachments?: WatchAttachments + archivedCacheLimit?: number + errorHandler?: AttachmentErrorHandler + remoteStorage?: RemoteStorageAdapter + } = {}, + ) { + const queue = new TanStackDBAttachmentQueue({ + db, + attachmentsCollection, + remoteStorage: overrides.remoteStorage ?? remoteStorage, + localStorage, + watchAttachments: overrides.watchAttachments ?? watchPhotoIds, + syncIntervalMs: SYNC_INTERVAL_MS, + archivedCacheLimit: overrides.archivedCacheLimit ?? 0, + errorHandler: overrides.errorHandler, + }) + onTestFinished(() => queue.stopSync()) + return queue + } + + // Reports every photo_id referenced by the users collection as a watched + // attachment. This mirrors how an application links its domain model to the + // attachment queue using a TanStack DB live query rather than a raw SQL + // watch: the `photo_id IS NOT NULL` filter lives in the query, and each + // change re-emits the full set of referenced ids. + const watchPhotoIdsWith = ( + toItem: (photoId: string) => WatchedAttachmentItem, + ): WatchAttachments => { + return async (onUpdate, signal) => { + const livePhotoIds = createCollection( + liveQueryCollectionOptions({ + query: (q) => + q + .from({ user: usersCollection }) + .where(({ user }) => not(isNull(user.photo_id))) + .select(({ user }) => ({ photo_id: user.photo_id })), + }), + ) + + const emit = () => + void onUpdate( + livePhotoIds.toArray + .map((row) => row.photo_id) + .filter((photoId): photoId is string => photoId != null) + .map(toItem), + ) + + // Emit the current snapshot once ready, then on every change. + await livePhotoIds.stateWhenReady() + emit() + const subscription = livePhotoIds.subscribeChanges(() => emit()) + + signal.addEventListener(`abort`, () => { + subscription.unsubscribe() + livePhotoIds.cleanup() + }) + } + } + + const watchPhotoIds = watchPhotoIdsWith((id) => ({ + id, + fileExtension: `jpg`, + })) + + return { + db, + localStorage, + remoteStorage, + uploadFile, + downloadFile, + deleteFile, + attachmentsCollection, + usersCollection, + createQueue, + watchPhotoIds, + watchPhotoIdsWith, + } + } + + /** Waits until the attachment with `id` reaches the expected state. */ + function waitForState( + collection: { get: (id: string) => TRow | undefined }, + id: string, + state: AttachmentState, + ): Promise { + return vi.waitFor( + () => { + const attachment = collection.get(id) + expect( + (attachment as { state?: AttachmentState } | undefined)?.state, + ).toBe(state) + return attachment! + }, + { timeout: WAIT_TIMEOUT, interval: 50 }, + ) + } + + describe(`save`, () => { + it(`writes the local file and inserts a QUEUED_UPLOAD row into the collection`, async () => { + const { createQueue, attachmentsCollection, localStorage } = await setup() + const queue = createQueue() + + const data = new Uint8Array(123).fill(42).buffer + const record = await queue.save({ + data, + fileExtension: `jpg`, + mediaType: `image/jpeg`, + }) + + expect(record.size).toBe(123) + expect(record.state).toBe(AttachmentState.QUEUED_UPLOAD) + expect(record.media_type).toBe(`image/jpeg`) + expect(record.filename).toBe(`${record.id}.jpg`) + expect(record.has_synced).toBe(0) + + // The file should exist on disk at the returned local_uri. + expect(await localStorage.fileExists(record.local_uri!)).toBe(true) + + // The row should be reflected in the collection once it syncs back. + await waitForState( + attachmentsCollection, + record.id, + AttachmentState.QUEUED_UPLOAD, + ) + }) + + it(`commits the updateHook mutation atomically with the attachment row`, async () => { + const { createQueue, attachmentsCollection, usersCollection } = + await setup() + const queue = createQueue() + + const userId = randomUUID() + const record = await queue.save({ + data: createMockJpegBuffer(), + fileExtension: `jpg`, + updateHook: async (attachment) => { + usersCollection.insert({ + id: userId, + name: `steven`, + email: `steven@journeyapps.com`, + photo_id: attachment.id, + }) + }, + }) + + // Both the attachment and the linked user row should appear together. + await waitForState( + attachmentsCollection, + record.id, + AttachmentState.QUEUED_UPLOAD, + ) + await vi.waitFor( + () => { + const user = usersCollection.get(userId) + expect(user?.photo_id).toBe(record.id) + }, + { timeout: WAIT_TIMEOUT, interval: 50 }, + ) + }) + + it(`uploads the saved file and transitions it to SYNCED`, async () => { + const { + createQueue, + attachmentsCollection, + usersCollection, + uploadFile, + } = await setup() + const queue = createQueue() + await queue.startSync() + + const userId = randomUUID() + const record = await queue.save({ + data: createMockJpegBuffer(), + fileExtension: `jpg`, + updateHook: async (attachment) => { + usersCollection.insert({ + id: userId, + name: `steven`, + email: `steven@journeyapps.com`, + photo_id: attachment.id, + }) + }, + }) + + await waitForState( + attachmentsCollection, + record.id, + AttachmentState.SYNCED, + ) + + expect(uploadFile).toHaveBeenCalled() + const [, uploadedAttachment] = uploadFile.mock.calls[0]! + expect(uploadedAttachment.id).toBe(record.id) + }) + + it(`honours a caller-supplied id`, async () => { + const { createQueue } = await setup() + const queue = createQueue() + + const id = `my-custom-id` + const record = await queue.save({ + id, + data: createMockJpegBuffer(), + fileExtension: `png`, + }) + + expect(record.id).toBe(id) + expect(record.filename).toBe(`${id}.png`) + }) + }) + + describe(`delete file`, () => { + it(`queues an existing attachment for deletion and removes the local file`, async () => { + const { + createQueue, + attachmentsCollection, + usersCollection, + localStorage, + } = await setup() + const queue = createQueue() + await queue.startSync() + + const userId = randomUUID() + const record = await queue.save({ + data: createMockJpegBuffer(), + fileExtension: `jpg`, + updateHook: async (attachment) => { + usersCollection.insert({ + id: userId, + name: `steven`, + email: `steven@journeyapps.com`, + photo_id: attachment.id, + }) + }, + }) + + await waitForState( + attachmentsCollection, + record.id, + AttachmentState.SYNCED, + ) + + await queue.delete({ + id: record.id, + updateHook: async (attachment) => { + usersCollection.update(userId, (draft) => { + if (draft.photo_id === attachment.id) { + draft.photo_id = null + } + }) + }, + }) + + // It should immediately be marked for deletion (and no longer synced). + const queued = attachmentsCollection.get(record.id) + expect(queued?.state).toBe(AttachmentState.QUEUED_DELETE) + expect(queued?.has_synced).toBe(0) + + // The user reference should have been cleared in the same transaction. + expect(usersCollection.get(userId)?.photo_id).toBeNull() + + // Eventually the row and the local file are removed. + await vi.waitFor( + () => expect(attachmentsCollection.get(record.id)).toBeUndefined(), + { timeout: WAIT_TIMEOUT, interval: 50 }, + ) + expect(await localStorage.fileExists(record.local_uri!)).toBe(false) + }) + + it(`throws for an unknown id and commits nothing`, async () => { + const { createQueue, attachmentsCollection, usersCollection } = + await setup() + const queue = createQueue() + + const hook = vi.fn() + await expect( + queue.delete({ id: `does-not-exist`, updateHook: hook }), + ).rejects.toThrow(/not found/i) + + // The failing transaction must not have run the hook or touched state. + expect(hook).not.toHaveBeenCalled() + expect(attachmentsCollection.get(`does-not-exist`)).toBeUndefined() + expect(usersCollection.size).toBe(0) + }) + }) +}) diff --git a/packages/query-db-collection/CHANGELOG.md b/packages/query-db-collection/CHANGELOG.md index c64cc6fe15..d8c2573a0c 100644 --- a/packages/query-db-collection/CHANGELOG.md +++ b/packages/query-db-collection/CHANGELOG.md @@ -1,5 +1,81 @@ # @tanstack/query-db-collection +## 1.1.0 + +### Minor Changes + +- Add top-level Query Collection support for additional Query observer options while preserving QueryClient defaultOptions behavior. ([#1665](https://github.com/TanStack/db/pull/1665)) + +### Patch Changes + +- Fix temporary query readiness listeners so subset unload and collection cleanup release them correctly during in-flight requests. ([#1673](https://github.com/TanStack/db/pull/1673)) + +- Extract internal query row ownership helpers to make lifecycle cleanup paths easier to reason about while preserving existing behavior. ([#1664](https://github.com/TanStack/db/pull/1664)) + +- Updated dependencies [[`8258d09`](https://github.com/TanStack/db/commit/8258d0955ab47c8510bd49ea59bcdbefd2ae054d), [`286964d`](https://github.com/TanStack/db/commit/286964d72612b59e3e427baabd9870f5a71a4281)]: + - @tanstack/db@0.6.16 + +## 1.0.48 + +### Patch Changes + +- Clarify that `select` extracts rows for DB materialization while preserving the wrapped TanStack Query cache response. ([#1654](https://github.com/TanStack/db/pull/1654)) + +- Document the current TanStack Query option compatibility surface for Query Collections, including forwarded options, QueryClient defaults, adapter-owned fields, and common options that are not currently exposed. ([#1653](https://github.com/TanStack/db/pull/1653)) + +- Add coverage for query invalidation behavior across eager and on-demand query collections. ([#1655](https://github.com/TanStack/db/pull/1655)) + +- Updated dependencies [[`eabcea7`](https://github.com/TanStack/db/commit/eabcea743fdfa045a2db01e12bef87403613102a), [`6d4c096`](https://github.com/TanStack/db/commit/6d4c096395b7ff3f428122ea8842bbead551a8c9)]: + - @tanstack/db@0.6.15 + +## 1.0.47 + +### Patch Changes + +- Keep on-demand load subset subscription state out of TanStack Query metadata so dehydrated query state remains safe to persist with structured-clone based persisters. ([#1644](https://github.com/TanStack/db/pull/1644)) + +## 1.0.46 + +### Patch Changes + +- Updated dependencies [[`397e12a`](https://github.com/TanStack/db/commit/397e12a1224ad563e20a331eebcbe904cd4af948)]: + - @tanstack/db@0.6.14 + +## 1.0.45 + +### Patch Changes + +- Updated dependencies [[`99e9afe`](https://github.com/TanStack/db/commit/99e9afed46ab4083d66609a3e37ee44103c2177f), [`816b667`](https://github.com/TanStack/db/commit/816b6671c2cc9806715f6e6ed4410b3f4efb5afb)]: + - @tanstack/db@0.6.13 + +## 1.0.44 + +### Patch Changes + +- Updated dependencies [[`2b27dd1`](https://github.com/TanStack/db/commit/2b27dd1448da71c78a48e2390cb71b0ada1b1488)]: + - @tanstack/db@0.6.12 + +## 1.0.43 + +### Patch Changes + +- Updated dependencies [[`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`36fb29a`](https://github.com/TanStack/db/commit/36fb29ad7e906d39b6afdba2fd31e369c601bbb0), [`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`ac09b11`](https://github.com/TanStack/db/commit/ac09b1177a100eafa85cba3cd09dd1f53f933ded)]: + - @tanstack/db@0.6.11 + +## 1.0.42 + +### Patch Changes + +- Updated dependencies [[`307fdf8`](https://github.com/TanStack/db/commit/307fdf80f522a39a50e316316b3b75ba27fd5e84)]: + - @tanstack/db@0.6.10 + +## 1.0.41 + +### Patch Changes + +- Updated dependencies [[`2147345`](https://github.com/TanStack/db/commit/2147345236ceee6e73d9fc6c0cdc2385833199fc), [`00389a4`](https://github.com/TanStack/db/commit/00389a47b258ad58fc3a03c5cc6f66957b9bd2d1)]: + - @tanstack/db@0.6.9 + ## 1.0.40 ### Patch Changes diff --git a/packages/query-db-collection/package.json b/packages/query-db-collection/package.json index f3599c7a5e..349070bb8f 100644 --- a/packages/query-db-collection/package.json +++ b/packages/query-db-collection/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/query-db-collection", - "version": "1.0.40", + "version": "1.1.0", "description": "TanStack Query collection for TanStack DB", "author": "Kyle Mathews", "license": "MIT", diff --git a/packages/query-db-collection/src/errors.ts b/packages/query-db-collection/src/errors.ts index 7224745255..2a226181bc 100644 --- a/packages/query-db-collection/src/errors.ts +++ b/packages/query-db-collection/src/errors.ts @@ -36,6 +36,15 @@ export class GetKeyRequiredError extends QueryCollectionError { } } +export class InitialDataInOnDemandModeError extends QueryCollectionError { + constructor() { + super( + `[QueryCollection] initialData and initialDataUpdatedAt are only supported when syncMode is 'eager'. Seed or hydrate the exact Query cache key for on-demand subsets instead.`, + ) + this.name = `InitialDataInOnDemandModeError` + } +} + export class SyncNotInitializedError extends QueryCollectionError { constructor() { super( diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index 5012036127..82f2a8dde5 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -2,6 +2,7 @@ import { QueryObserver, hashKey } from '@tanstack/query-core' import { deepEquals } from '@tanstack/db' import { GetKeyRequiredError, + InitialDataInOnDemandModeError, QueryClientRequiredError, QueryFnRequiredError, QueryKeyRequiredError, @@ -49,6 +50,40 @@ type InferSchemaInput = T extends StandardSchemaV1 type TQueryKeyBuilder = (opts: LoadSubsetOptions) => TQueryKey +const queryObserverOptionKeys = [ + `enabled`, + `refetchInterval`, + `retry`, + `retryDelay`, + `staleTime`, + `gcTime`, + `refetchOnWindowFocus`, + `refetchOnReconnect`, + `refetchOnMount`, + `networkMode`, +] as const + +type QueryObserverOptionKey = (typeof queryObserverOptionKeys)[number] + +type QueryObserverOptionValues = Pick< + QueryObserverOptions, any, Array, Array, any>, + QueryObserverOptionKey +> + +function pickDefinedQueryObserverOptions( + config: Partial, +): Partial { + const options: Partial = {} + + for (const key of queryObserverOptionKeys) { + if (config[key] !== undefined) { + ;(options as Record)[key] = config[key] + } + } + + return options +} + /** * Configuration options for creating a Query Collection * @template T - The explicit type of items stored in the collection @@ -77,7 +112,10 @@ export interface QueryCollectionConfig< ) => Promise> | Array ? (context: QueryFunctionContext) => Promise> | Array : TQueryFn - /* Function that extracts array items from wrapped API responses (e.g metadata, pagination) */ + /** + * Extracts the row array TanStack DB materializes from the Query response. + * The Query cache keeps the original response shape. + */ select?: (data: TQueryData) => Array /** The TanStack Query client instance */ queryClient: QueryClient @@ -126,6 +164,54 @@ export interface QueryCollectionConfig< TQueryData, TQueryKey >[`gcTime`] + refetchOnWindowFocus?: QueryObserverOptions< + TQueryData, + TError, + Array, + TQueryData, + TQueryKey + >[`refetchOnWindowFocus`] + refetchOnReconnect?: QueryObserverOptions< + TQueryData, + TError, + Array, + TQueryData, + TQueryKey + >[`refetchOnReconnect`] + refetchOnMount?: QueryObserverOptions< + TQueryData, + TError, + Array, + TQueryData, + TQueryKey + >[`refetchOnMount`] + networkMode?: QueryObserverOptions< + TQueryData, + TError, + Array, + TQueryData, + TQueryKey + >[`networkMode`] + /** + * Data used to initialize the TanStack Query cache for an eager collection. + * The value has the original Query response shape and is projected through + * the collection's select option before rows are materialized. + */ + initialData?: QueryObserverOptions< + TQueryData, + TError, + Array, + TQueryData, + TQueryKey + >[`initialData`] + /** The timestamp TanStack Query uses to determine initialData freshness. */ + initialDataUpdatedAt?: QueryObserverOptions< + TQueryData, + TError, + Array, + TQueryData, + TQueryKey + >[`initialDataUpdatedAt`] persistedGcTime?: number /** @@ -349,6 +435,13 @@ class QueryCollectionUtilsImpl { } } +function getLoadSubsetOptionsForMeta( + opts: LoadSubsetOptions, +): Omit { + const { subscription: _subscription, ...serializableOptions } = opts + return serializableOptions +} + /** * Creates query collection options for use with a standard Collection. * This integrates TanStack Query with TanStack DB for automatic synchronization. @@ -586,6 +679,12 @@ export function queryCollectionOptions( retryDelay, staleTime, gcTime, + refetchOnWindowFocus, + refetchOnReconnect, + refetchOnMount, + networkMode, + initialData, + initialDataUpdatedAt, persistedGcTime, getKey, onInsert, @@ -598,6 +697,35 @@ export function queryCollectionOptions( // Default to eager sync mode if not provided const syncMode = baseCollectionConfig.syncMode ?? `eager` + if ( + syncMode === `on-demand` && + (initialData !== undefined || initialDataUpdatedAt !== undefined) + ) { + throw new InitialDataInOnDemandModeError() + } + + const initialDataObserverOptions = + syncMode === `eager` + ? { + ...(initialData !== undefined && { initialData }), + ...(initialDataUpdatedAt !== undefined && { + initialDataUpdatedAt, + }), + // Placeholder data is observer-local UI state in TanStack Query. It + // must not be exposed as collection-wide normalized rows, including + // when supplied through QueryClient defaults. + placeholderData: undefined, + } + : { + // A collection-wide initializer cannot establish membership for + // arbitrary on-demand subsets. A defined initializer is needed to + // override a QueryClient default because Query Core can apply + // defaults again while constructing each Query. + initialData: () => undefined, + initialDataUpdatedAt: undefined, + placeholderData: undefined, + } + // Compute the base query key once for cache lookups. // All derived keys (from on-demand predicates or function-based queryKey) must // share this prefix so that queryCache.findAll({ queryKey: baseKey }) can find them. @@ -661,7 +789,8 @@ export function queryCollectionOptions( // hashedQueryKey → queryKey const hashToQueryKey = new Map() - // queryKey → Set + // queryKey → Set. Entry presence means ownership is resolved; + // an empty set represents a resolved query that currently owns no rows. const queryToRows = new Map>() // RowKey → Set @@ -669,6 +798,7 @@ export function queryCollectionOptions( // queryKey → QueryObserver's unsubscribe function const unsubscribes = new Map void>() + const pendingReadyUnsubscribes = new Map void>>() // queryKey → reference count (how many loadSubset calls are active) // Reference counting for QueryObserver lifecycle management @@ -687,28 +817,63 @@ export function queryCollectionOptions( // 3. Decrements refcount and GCs rows where count reaches 0 const queryRefCounts = new Map() - // Helper function to add a row to the internal state - const addRow = (rowKey: string | number, hashedQueryKey: string) => { - const rowToQueriesSet = rowToQueries.get(rowKey) || new Set() - rowToQueriesSet.add(hashedQueryKey) - rowToQueries.set(rowKey, rowToQueriesSet) + const addRowOwner = (rowKey: string | number, hashedQueryKey: string) => { + const owners = rowToQueries.get(rowKey) || new Set() + owners.add(hashedQueryKey) + rowToQueries.set(rowKey, owners) - const queryToRowsSet = queryToRows.get(hashedQueryKey) || new Set() - queryToRowsSet.add(rowKey) - queryToRows.set(hashedQueryKey, queryToRowsSet) + const ownedRows = + queryToRows.get(hashedQueryKey) || new Set() + ownedRows.add(rowKey) + queryToRows.set(hashedQueryKey, ownedRows) } - // Helper function to remove a row from the internal state - const removeRow = (rowKey: string | number, hashedQuerKey: string) => { - const rowToQueriesSet = rowToQueries.get(rowKey) || new Set() - rowToQueriesSet.delete(hashedQuerKey) - rowToQueries.set(rowKey, rowToQueriesSet) + const addRowOwners = (rowKey: string | number, owners: Set) => { + if (owners.size === 0) { + rowToQueries.delete(rowKey) + return + } + + rowToQueries.set(rowKey, new Set(owners)) + owners.forEach((owner) => { + const ownedRows = queryToRows.get(owner) || new Set() + ownedRows.add(rowKey) + queryToRows.set(owner, ownedRows) + }) + } - const queryToRowsSet = queryToRows.get(hashedQuerKey) || new Set() - queryToRowsSet.delete(rowKey) - queryToRows.set(hashedQuerKey, queryToRowsSet) + const removeRowOwner = (rowKey: string | number, hashedQueryKey: string) => { + const owners = rowToQueries.get(rowKey) + owners?.delete(hashedQueryKey) + if (!owners?.size) { + rowToQueries.delete(rowKey) + } + + const ownedRows = queryToRows.get(hashedQueryKey) + ownedRows?.delete(rowKey) - return rowToQueriesSet.size === 0 + return !owners?.size + } + + const removeQueryOwnership = (hashedQueryKey: string) => { + const nextOwnersByRow = new Map>() + + const rowKeys = + queryToRows.get(hashedQueryKey) ?? new Set() + + rowKeys.forEach((rowKey) => { + const owners = rowToQueries.get(rowKey) + + if (!owners) { + return + } + + const nextOwners = new Set(owners) + nextOwners.delete(hashedQueryKey) + nextOwnersByRow.set(rowKey, nextOwners) + }) + + return nextOwnersByRow } const internalSync: SyncConfig[`sync`] = (params) => { @@ -843,12 +1008,7 @@ export function queryCollectionOptions( continue } - rowToQueries.set(rowKey, new Set(owners)) - owners.forEach((owner) => { - const queryToRowsSet = queryToRows.get(owner) || new Set() - queryToRowsSet.add(rowKey) - queryToRows.set(owner, queryToRowsSet) - }) + addRowOwners(rowKey, owners) if (owners.has(hashedQueryKey)) { ownedRows.add(rowKey) @@ -934,12 +1094,7 @@ export function queryCollectionOptions( return } - rowToQueries.set(row.key, new Set(ownerSet)) - ownerSet.forEach((owner) => { - const queryToRowsSet = queryToRows.get(owner) || new Set() - queryToRowsSet.add(row.key) - queryToRows.set(owner, queryToRowsSet) - }) + addRowOwners(row.key, ownerSet) if (ownerSet.has(hashedQueryKey)) { baseline.set(row.key, { @@ -965,7 +1120,7 @@ export function queryCollectionOptions( baseline.forEach(({ value: oldItem, owners }, rowKey) => { owners.delete(hashedQueryKey) setPersistedOwners(rowKey, owners) - const needToRemove = removeRow(rowKey, hashedQueryKey) + const needToRemove = removeRowOwner(rowKey, hashedQueryKey) if (needToRemove) { rowsToDelete.push(oldItem) } @@ -979,6 +1134,7 @@ export function queryCollectionOptions( `${QUERY_COLLECTION_GC_PREFIX}${hashedQueryKey}`, ) commit() + queryToRows.delete(hashedQueryKey) } const schedulePersistedRetentionExpiry = ( @@ -1078,6 +1234,36 @@ export function queryCollectionOptions( } }) + const waitForQueryReady = ( + observer: QueryObserver, any, Array, Array, any>, + hashedQueryKey: string, + ): Promise => + new Promise((resolve, reject) => { + const unsubscribe = observer.subscribe((result) => { + // Use a microtask in case `subscribe` is called synchronously, before `unsubscribe` is initialized + queueMicrotask(() => { + if (result.isSuccess || result.isError) { + unsubscribe() + const pending = pendingReadyUnsubscribes.get(hashedQueryKey) + pending?.delete(unsubscribe) + if (pending?.size === 0) { + pendingReadyUnsubscribes.delete(hashedQueryKey) + } + + if (result.isSuccess) { + resolve() + } else { + reject(result.error) + } + } + }) + }) + const pending = + pendingReadyUnsubscribes.get(hashedQueryKey) ?? new Set() + pending.add(unsubscribe) + pendingReadyUnsubscribes.set(hashedQueryKey, pending) + }) + const createQueryFromOpts = ( opts: LoadSubsetOptions = {}, queryFunction: typeof queryFn = queryFn, @@ -1092,7 +1278,10 @@ export function queryCollectionOptions( // Generate key using common function const key = generateQueryKeyFromOptions(opts) const hashedQueryKey = hashKey(key) - const extendedMeta = { ...meta, loadSubsetOptions: opts } + const extendedMeta = { + ...meta, + loadSubsetOptions: getLoadSubsetOptionsForMeta(opts), + } const retainedEntry = metadata?.collection.get( `${QUERY_COLLECTION_GC_PREFIX}${hashedQueryKey}`, ) @@ -1135,20 +1324,7 @@ export function queryCollectionOptions( } // Query is still loading, wait for the first result - return new Promise((resolve, reject) => { - const unsubscribe = observer.subscribe((result) => { - // Use a microtask in case `subscribe` is called synchronously, before `unsubscribe` is initialized - queueMicrotask(() => { - if (result.isSuccess) { - unsubscribe() - resolve() - } else if (result.isError) { - unsubscribe() - reject(result.error) - } - }) - }) - }) + return waitForQueryReady(observer, hashedQueryKey) } } @@ -1159,19 +1335,24 @@ export function queryCollectionOptions( Array, any > = { + ...pickDefinedQueryObserverOptions({ + enabled, + refetchInterval, + retry, + retryDelay, + staleTime, + gcTime, + refetchOnWindowFocus, + refetchOnReconnect, + refetchOnMount, + networkMode, + }), + ...initialDataObserverOptions, queryKey: key, queryFn: queryFunction, meta: extendedMeta, structuralSharing: true, notifyOnChangeProps: `all`, - - // Only include options that are explicitly defined to allow QueryClient defaultOptions to be used - ...(enabled !== undefined && { enabled }), - ...(refetchInterval !== undefined && { refetchInterval }), - ...(retry !== undefined && { retry }), - ...(retryDelay !== undefined && { retryDelay }), - ...(staleTime !== undefined && { staleTime }), - ...(gcTime !== undefined && { gcTime }), } const localObserver = new QueryObserver< @@ -1214,20 +1395,7 @@ export function queryCollectionOptions( } // Create a promise that resolves when the query result is first available - const readyPromise = new Promise((resolve, reject) => { - const unsubscribe = localObserver.subscribe((result) => { - // Use a microtask in case `subscribe` is called synchronously, before `unsubscribe` is initialized - queueMicrotask(() => { - if (result.isSuccess) { - unsubscribe() - resolve() - } else if (result.isError) { - unsubscribe() - reject(result.error) - } - }) - }) - }) + const readyPromise = waitForQueryReady(localObserver, hashedQueryKey) // If sync has started or there are subscribers to the collection, subscribe to the query straight away // This creates the main subscription that handles data updates @@ -1283,6 +1451,13 @@ export function queryCollectionOptions( const previouslyOwnedRows = shouldUsePersistedBaseline ? new Set(persistedBaseline.keys()) : getHydratedOwnedRowsForQueryBaseline(hashedQueryKey) + // From this point onward the result, including an empty result, is the + // authoritative ownership baseline until this query is cleaned up. + queryToRows.set( + hashedQueryKey, + queryToRows.get(hashedQueryKey) ?? new Set(), + ) + const newItemsMap = new Map() newItemsArray.forEach((item) => { const key = getKey(item) @@ -1308,7 +1483,7 @@ export function queryCollectionOptions( const owners = getPersistedOwners(key) owners.delete(hashedQueryKey) setPersistedOwners(key, owners) - const needToRemove = removeRow(key, hashedQueryKey) + const needToRemove = removeRowOwner(key, hashedQueryKey) if (needToRemove) { write({ type: `delete`, value: oldItem }) } @@ -1323,7 +1498,7 @@ export function queryCollectionOptions( owners.add(hashedQueryKey) setPersistedOwners(key, owners) } - addRow(key, hashedQueryKey) + addRowOwner(key, hashedQueryKey) if (!currentSyncedItems.has(key)) { write({ type: `insert`, value: newItem }) } @@ -1371,6 +1546,24 @@ export function queryCollectionOptions( } if (retainedQueriesPendingRevalidation.has(hashedQueryKey)) { + const query = queryClient.getQueryCache().find({ + queryKey, + exact: true, + }) + if (query?.state.dataUpdateCount === 0) { + // Initial data seeds Query state without a fetch. It must not + // satisfy persistence retention that lasts until revalidation. + if (!result.isFetching) { + state.observers + .get(hashedQueryKey) + ?.refetch() + .catch(() => { + // Errors handled by the next handleQueryResult invocation + }) + } + return + } + void reconcileSuccessfulResult(queryKey, result).catch((error) => { console.error( `[QueryCollection] Error reconciling query ${String(queryKey)}:`, @@ -1487,27 +1680,24 @@ export function queryCollectionOptions( * Perform row-level cleanup and remove all tracking for a query. * Callers are responsible for ensuring the query is safe to cleanup. */ + const unsubscribePendingReadyListeners = (hashedQueryKey: string) => { + pendingReadyUnsubscribes.get(hashedQueryKey)?.forEach((unsubscribe) => { + unsubscribe() + }) + pendingReadyUnsubscribes.delete(hashedQueryKey) + } + const cleanupQueryInternal = (hashedQueryKey: string) => { unsubscribes.get(hashedQueryKey)?.() unsubscribes.delete(hashedQueryKey) + unsubscribePendingReadyListeners(hashedQueryKey) cancelPersistedRetentionExpiry(hashedQueryKey) retainedQueriesPendingRevalidation.delete(hashedQueryKey) - const rowKeys = queryToRows.get(hashedQueryKey) ?? new Set() - const nextOwnersByRow = new Map>() + const nextOwnersByRow = removeQueryOwnership(hashedQueryKey) const rowsToDelete: Array = [] - rowKeys.forEach((rowKey) => { - const queries = rowToQueries.get(rowKey) - - if (!queries) { - return - } - - const nextOwners = new Set(queries) - nextOwners.delete(hashedQueryKey) - nextOwnersByRow.set(rowKey, nextOwners) - + nextOwnersByRow.forEach((nextOwners, rowKey) => { if (nextOwners.size === 0 && collection.has(rowKey)) { rowsToDelete.push(collection.get(rowKey)) } @@ -1563,6 +1753,7 @@ export function queryCollectionOptions( // Drop our subscription so hasListeners reflects only active consumers unsubscribes.get(hashedQueryKey)?.() unsubscribes.delete(hashedQueryKey) + unsubscribePendingReadyListeners(hashedQueryKey) } const hasListeners = observer?.hasListeners() ?? false @@ -1654,7 +1845,10 @@ export function queryCollectionOptions( persistedRetentionTimers.clear() const allQueryKeys = [...hashToQueryKey.values()] - const allHashedKeys = [...state.observers.keys()] + const allHashedKeys = new Set([ + ...state.observers.keys(), + ...queryToRows.keys(), + ]) // Force cleanup all queries (explicit cleanup path) // This ignores hasListeners and always cleans up @@ -1863,6 +2057,23 @@ export function queryCollectionOptions( // Enhanced internalSync that captures write functions for manual use const enhancedInternalSync: SyncConfig[`sync`] = (params) => { const { begin, write, commit, collection } = params + let queryClientMounted = false + + const mountQueryClient = () => { + if (!queryClientMounted) { + queryClient.mount() + queryClientMounted = true + } + } + + const unmountQueryClient = () => { + if (queryClientMounted) { + queryClient.unmount() + queryClientMounted = false + } + } + + mountQueryClient() // Get the base query key for the context (handle both static and function-based keys) const contextQueryKey = @@ -1882,8 +2093,36 @@ export function queryCollectionOptions( updateCacheData, } - // Call the original internalSync logic - return internalSync(params) + // Call the original internalSync logic, pairing QueryClient mount with the + // collection sync lifecycle so focus/reconnect managers dispatch events for + // standalone QueryClient usage. + const syncResult = internalSync(params) + const sync = + typeof syncResult === `function` + ? { cleanup: syncResult } + : typeof syncResult === `object` + ? syncResult + : {} + + return { + ...sync, + cleanup: () => { + try { + sync.cleanup?.() + } finally { + unmountQueryClient() + } + }, + } + } + + if (typeof process !== `undefined` && process.env.NODE_ENV === `test`) { + Object.defineProperty(enhancedInternalSync, `__getOwnershipMapsForTests`, { + value: () => ({ + rowToQueries, + queryToRows, + }), + }) } // Create write utils using the manual-sync module diff --git a/packages/query-db-collection/tests/query.test-d.ts b/packages/query-db-collection/tests/query.test-d.ts index c526398f8a..30e8f64c9c 100644 --- a/packages/query-db-collection/tests/query.test-d.ts +++ b/packages/query-db-collection/tests/query.test-d.ts @@ -32,6 +32,30 @@ describe(`Query collection type resolution tests`, () => { // Create a mock QueryClient for tests const queryClient = new QueryClient() + it(`should type supported top-level Query observer options and reject adapter-owned fields`, () => { + queryCollectionOptions({ + id: `query-options-types`, + queryClient, + queryKey: [`query-options-types`], + queryFn: () => Promise.resolve([]), + getKey: (item) => item.id, + refetchOnWindowFocus: true, + refetchOnReconnect: true, + refetchOnMount: `always`, + networkMode: `online`, + }) + + queryCollectionOptions({ + id: `query-options-subscribed-owned`, + queryClient, + queryKey: [`query-options-subscribed-owned`], + queryFn: () => Promise.resolve([]), + getKey: (item) => item.id, + // @ts-expect-error Query Collection owns observer subscription lifecycle. + subscribed: false, + }) + }) + it(`should prioritize explicit type in QueryCollectionConfig`, () => { const options = queryCollectionOptions({ id: `test`, diff --git a/packages/query-db-collection/tests/query.test.ts b/packages/query-db-collection/tests/query.test.ts index 0c501ab027..a4e4350d05 100644 --- a/packages/query-db-collection/tests/query.test.ts +++ b/packages/query-db-collection/tests/query.test.ts @@ -1,5 +1,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { QueryClient, hashKey } from '@tanstack/query-core' +import { + QueryClient, + dehydrate, + focusManager, + hashKey, + onlineManager, +} from '@tanstack/query-core' import { BTreeIndex, createCollection, @@ -9,7 +15,10 @@ import { inArray, or, } from '@tanstack/db' -import { stripVirtualProps } from '../../db/tests/utils' +import { + mockSyncCollectionOptions, + stripVirtualProps, +} from '../../db/tests/utils' import { persistedCollectionOptions } from '../../db-sqlite-persistence-core/src' import { queryCollectionOptions } from '../src/query' import type { QueryFunctionContext } from '@tanstack/query-core' @@ -17,6 +26,7 @@ import type { Collection, DeleteMutationFnParams, InsertMutationFnParams, + LoadSubsetOptions, SyncMetadataApi, TransactionWithMutations, UpdateMutationFnParams, @@ -37,9 +47,41 @@ interface CategorisedItem { const getKey = (item: TestItem) => item.id +type OwnershipMaps = { + rowToQueries: Map> + queryToRows: Map> +} + +function inspectOwnershipMaps(options: { + sync: { sync: unknown } +}): OwnershipMaps { + const sync = options.sync.sync as { + __getOwnershipMapsForTests?: () => OwnershipMaps + } + const maps = sync.__getOwnershipMapsForTests?.() + if (!maps) { + throw new Error(`Ownership-map test inspection is unavailable`) + } + return maps +} + +function expectNoEmptyRowOwnershipSets(maps: OwnershipMaps): void { + maps.rowToQueries.forEach((owners) => expect(owners.size).toBeGreaterThan(0)) +} + // Helper to advance timers and allow microtasks to flush const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0)) +function createDeferred() { + let resolve!: (value: T) => void + let reject!: (reason?: unknown) => void + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise + reject = rejectPromise + }) + return { promise, resolve, reject } +} + function createInMemorySyncMetadataApi< TKey extends string | number = string | number, TItem extends object = Record, @@ -181,6 +223,527 @@ describe(`QueryCollection`, () => { queryClient.clear() }) + it(`should pass through additional top-level Query observer options`, async () => { + const queryKey = [`query-options-pass-through`] + const queryFn = vi.fn().mockResolvedValue([{ id: `1`, name: `Item 1` }]) + + const collection = createCollection( + queryCollectionOptions({ + id: `query-options-pass-through`, + queryClient, + queryKey, + queryFn, + getKey, + startSync: true, + refetchOnWindowFocus: true, + refetchOnReconnect: true, + refetchOnMount: `always`, + networkMode: `online`, + }), + ) + + await vi.waitFor(() => { + expect(collection.size).toBe(1) + }) + + const query = queryClient.getQueryCache().find({ queryKey, exact: true }) + const options = query?.options as any + expect(options.refetchOnWindowFocus).toBe(true) + expect(options.refetchOnReconnect).toBe(true) + expect(options.refetchOnMount).toBe(`always`) + expect(options.networkMode).toBe(`online`) + }) + + describe(`initialData`, () => { + it(`materializes eager initial data without fetching while it is fresh`, async () => { + const queryKey = [`initial-data-eager`] + const initialData: Array = [{ id: `1`, name: `Initial item` }] + const queryFn = vi + .fn<() => Promise>>() + .mockResolvedValue([{ id: `1`, name: `Fetched item` }]) + + const collection = createCollection( + queryCollectionOptions({ + id: `initial-data-eager`, + queryClient, + queryKey, + queryFn, + getKey, + startSync: true, + initialData, + staleTime: Infinity, + }), + ) + + try { + await vi.waitFor(() => { + expect(collection.status).toBe(`ready`) + expect(stripVirtualProps(collection.get(`1`))).toEqual(initialData[0]) + }) + + expect(queryFn).not.toHaveBeenCalled() + expect(queryClient.getQueryData(queryKey)).toEqual(initialData) + } finally { + await collection.cleanup() + } + }) + + it(`evaluates a function initializer once for a missing Query cache entry`, async () => { + const initialData = vi.fn(() => [{ id: `1`, name: `Initial item` }]) + const collection = createCollection( + queryCollectionOptions({ + id: `initial-data-function`, + queryClient, + queryKey: [`initial-data-function`], + queryFn: vi.fn().mockResolvedValue([]), + getKey, + startSync: true, + initialData, + staleTime: Infinity, + }), + ) + + try { + await vi.waitFor(() => { + expect(collection.get(`1`)?.name).toBe(`Initial item`) + }) + expect(initialData).toHaveBeenCalledTimes(1) + } finally { + await collection.cleanup() + } + }) + + it(`keeps stale initial rows while fetching and reconciles the server result`, async () => { + let resolveQuery: ((items: Array) => void) | undefined + const queryFn = vi.fn( + () => + new Promise>((resolve) => { + resolveQuery = resolve + }), + ) + const collection = createCollection( + queryCollectionOptions({ + id: `initial-data-stale`, + queryClient, + queryKey: [`initial-data-stale`], + queryFn, + getKey, + startSync: true, + initialData: [{ id: `initial`, name: `Initial` }], + initialDataUpdatedAt: 1, + staleTime: 0, + }), + ) + + try { + await vi.waitFor(() => { + expect(queryFn).toHaveBeenCalledTimes(1) + expect(collection.get(`initial`)?.name).toBe(`Initial`) + }) + + resolveQuery?.([{ id: `server`, name: `Server` }]) + await vi.waitFor(() => { + expect(collection.has(`initial`)).toBe(false) + expect(collection.get(`server`)?.name).toBe(`Server`) + }) + } finally { + await collection.cleanup() + } + }) + + it(`retains initial rows when a refetch fails`, async () => { + const initialRow = { id: `initial`, name: `Initial` } + const error = new Error(`Refetch failed`) + const queryFn = vi.fn().mockRejectedValue(error) + const consoleErrorSpy = vi + .spyOn(console, `error`) + .mockImplementation(() => {}) + const collection = createCollection( + queryCollectionOptions({ + id: `initial-data-refetch-error`, + queryClient, + queryKey: [`initial-data-refetch-error`], + queryFn, + getKey, + startSync: true, + initialData: [initialRow], + initialDataUpdatedAt: 1, + staleTime: 0, + retry: false, + }), + ) + + try { + await vi.waitFor(() => { + expect(queryFn).toHaveBeenCalledTimes(1) + expect(collection.utils.lastError).toBe(error) + }) + expect(stripVirtualProps(collection.get(initialRow.id))).toEqual( + initialRow, + ) + } finally { + consoleErrorSpy.mockRestore() + await collection.cleanup() + } + }) + + it(`ignores a late refetch result after initial data is cleaned up`, async () => { + const serverResult = createDeferred>() + const collection = createCollection( + queryCollectionOptions({ + id: `initial-data-late-cleanup`, + queryClient, + queryKey: [`initial-data-late-cleanup`], + queryFn: () => serverResult.promise, + getKey, + startSync: true, + initialData: [{ id: `initial`, name: `Initial` }], + initialDataUpdatedAt: 1, + staleTime: 0, + }), + ) + + await vi.waitFor(() => { + expect(collection.get(`initial`)?.name).toBe(`Initial`) + expect(queryClient.isFetching()).toBe(1) + }) + await collection.cleanup() + + serverResult.resolve([{ id: `server`, name: `Server` }]) + await vi.waitFor(() => expect(queryClient.isFetching()).toBe(0)) + expect(collection.status).toBe(`cleaned-up`) + expect(collection.size).toBe(0) + }) + + it(`projects a wrapped initial response while preserving its Query cache shape`, async () => { + const queryKey = [`initial-data-wrapped`] + const initialResponse = { + items: [{ id: `1`, name: `Initial item` }], + nextCursor: `next`, + } + const queryFn = vi.fn().mockResolvedValue(initialResponse) + + const collection = createCollection( + queryCollectionOptions({ + id: `initial-data-wrapped`, + queryClient, + queryKey, + queryFn, + select: (response: typeof initialResponse) => response.items, + getKey, + startSync: true, + initialData: initialResponse, + staleTime: Infinity, + }), + ) + + try { + await vi.waitFor(() => { + expect(stripVirtualProps(collection.get(`1`))).toEqual( + initialResponse.items[0], + ) + }) + + expect(queryFn).not.toHaveBeenCalled() + expect(queryClient.getQueryData(queryKey)).toEqual(initialResponse) + } finally { + await collection.cleanup() + } + }) + + it(`preserves a wrapped initial response when writing rows directly`, async () => { + const queryKey = [`initial-data-wrapped-writes`] + const initialResponse = { + items: [{ id: `1`, name: `Initial item` }], + nextCursor: `next`, + } + const collection = createCollection( + queryCollectionOptions({ + id: `initial-data-wrapped-writes`, + queryClient, + queryKey, + queryFn: vi.fn().mockResolvedValue(initialResponse), + select: (response: typeof initialResponse) => response.items, + getKey, + startSync: true, + initialData: initialResponse, + staleTime: Infinity, + }), + ) + + try { + await vi.waitFor(() => { + expect(collection.get(`1`)?.name).toBe(`Initial item`) + }) + + collection.utils.writeInsert({ id: `2`, name: `Inserted item` }) + collection.utils.writeUpdate({ id: `1`, name: `Updated item` }) + + expect(queryClient.getQueryData(queryKey)).toEqual({ + items: [ + { id: `1`, name: `Updated item` }, + { id: `2`, name: `Inserted item` }, + ], + nextCursor: `next`, + }) + } finally { + await collection.cleanup() + } + }) + + it(`keeps initial data scoped to each collection on a shared QueryClient`, async () => { + const first = createCollection( + queryCollectionOptions({ + id: `initial-data-shared-client-first`, + queryClient, + queryKey: [`initial-data-shared-client`, `first`], + queryFn: vi.fn().mockResolvedValue([]), + getKey, + startSync: true, + initialData: [{ id: `1`, name: `First` }], + staleTime: Infinity, + }), + ) + const second = createCollection( + queryCollectionOptions({ + id: `initial-data-shared-client-second`, + queryClient, + queryKey: [`initial-data-shared-client`, `second`], + queryFn: vi.fn().mockResolvedValue([]), + getKey, + startSync: true, + initialData: [{ id: `2`, name: `Second` }], + staleTime: Infinity, + }), + ) + + try { + await vi.waitFor(() => { + expect(first.get(`1`)?.name).toBe(`First`) + expect(second.get(`2`)?.name).toBe(`Second`) + }) + expect(first.has(`2`)).toBe(false) + expect(second.has(`1`)).toBe(false) + } finally { + await Promise.all([first.cleanup(), second.cleanup()]) + } + }) + + it(`does not replace existing Query data with a later collection initializer`, async () => { + const queryKey = [`initial-data-shared-key`] + queryClient.setQueryData(queryKey, [{ id: `cached`, name: `Cached` }]) + + const collection = createCollection( + queryCollectionOptions({ + id: `initial-data-shared-key`, + queryClient, + queryKey, + queryFn: vi.fn().mockResolvedValue([]), + getKey, + startSync: true, + initialData: [{ id: `initial`, name: `Initial` }], + staleTime: Infinity, + }), + ) + + try { + await vi.waitFor(() => { + expect(collection.get(`cached`)?.name).toBe(`Cached`) + }) + expect(collection.has(`initial`)).toBe(false) + } finally { + await collection.cleanup() + } + }) + + it(`rejects collection-level initial data in on-demand mode`, () => { + expect(() => + queryCollectionOptions({ + id: `initial-data-on-demand`, + queryClient, + queryKey: [`initial-data-on-demand`], + queryFn: vi.fn().mockResolvedValue([]), + getKey, + syncMode: `on-demand`, + initialData: [{ id: `1`, name: `Initial` }], + }), + ).toThrow( + `initialData and initialDataUpdatedAt are only supported when syncMode is 'eager'`, + ) + }) + + it(`does not apply QueryClient initial data defaults to on-demand subsets`, async () => { + const defaultInitialQueryClient = new QueryClient({ + defaultOptions: { + queries: { + initialData: [{ id: `default`, name: `Default` }], + staleTime: Infinity, + retry: false, + }, + }, + }) + const queryFn = vi + .fn() + .mockResolvedValue([{ id: `server`, name: `Server` }]) + const collection = createCollection( + queryCollectionOptions({ + id: `initial-data-default-on-demand`, + queryClient: defaultInitialQueryClient, + queryKey: [`initial-data-default-on-demand`], + queryFn, + getKey, + syncMode: `on-demand`, + startSync: true, + }), + ) + + try { + await collection._sync.loadSubset({}) + await vi.waitFor(() => { + expect(collection.get(`server`)?.name).toBe(`Server`) + }) + expect(collection.has(`default`)).toBe(false) + expect(queryFn).toHaveBeenCalledTimes(1) + } finally { + await collection.cleanup() + defaultInitialQueryClient.clear() + } + }) + + it(`does not materialize QueryClient placeholder defaults`, async () => { + const placeholderQueryClient = new QueryClient({ + defaultOptions: { + queries: { + placeholderData: [{ id: `placeholder`, name: `Placeholder` }], + retry: false, + }, + }, + }) + let resolveQuery: ((items: Array) => void) | undefined + const queryFn = vi.fn( + () => + new Promise>((resolve) => { + resolveQuery = resolve + }), + ) + const collection = createCollection( + queryCollectionOptions({ + id: `placeholder-default`, + queryClient: placeholderQueryClient, + queryKey: [`placeholder-default`], + queryFn, + getKey, + startSync: true, + }), + ) + + try { + await vi.waitFor(() => expect(queryFn).toHaveBeenCalledTimes(1)) + expect(collection.has(`placeholder`)).toBe(false) + + resolveQuery?.([{ id: `server`, name: `Server` }]) + await vi.waitFor(() => { + expect(collection.get(`server`)?.name).toBe(`Server`) + }) + } finally { + await collection.cleanup() + placeholderQueryClient.clear() + } + }) + }) + + it(`should refetch on focus and reconnect with standalone QueryClient`, async () => { + const queryKey = [`query-options-event-refetch`] + const queryFn = vi + .fn() + .mockResolvedValueOnce([{ id: `1`, name: `Initial` }]) + .mockResolvedValueOnce([{ id: `1`, name: `Focused` }]) + .mockResolvedValueOnce([{ id: `1`, name: `Reconnected` }]) + + const collection = createCollection( + queryCollectionOptions({ + id: `query-options-event-refetch`, + queryClient, + queryKey, + queryFn, + getKey, + startSync: true, + staleTime: 0, + refetchOnWindowFocus: true, + refetchOnReconnect: true, + }), + ) + + try { + await vi.waitFor(() => { + expect(collection.size).toBe(1) + expect(queryFn).toHaveBeenCalledTimes(1) + }) + + focusManager.setFocused(false) + focusManager.setFocused(true) + + await vi.waitFor(() => { + expect(queryFn).toHaveBeenCalledTimes(2) + }) + + onlineManager.setOnline(false) + onlineManager.setOnline(true) + + await vi.waitFor(() => { + expect(queryFn).toHaveBeenCalledTimes(3) + }) + } finally { + await collection.cleanup() + focusManager.setFocused(undefined) + onlineManager.setOnline(true) + } + }) + + it(`should omit undefined Query observer options to preserve defaults`, async () => { + const queryKey = [`query-options-default-preservation`] + const queryFn = vi.fn().mockResolvedValue([{ id: `1`, name: `Item 1` }]) + + const clientWithDefaults = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1234, + retry: 3, + refetchOnWindowFocus: false, + }, + }, + }) + + const collection = createCollection( + queryCollectionOptions({ + id: `query-options-default-preservation`, + queryClient: clientWithDefaults, + queryKey, + queryFn, + getKey, + startSync: true, + staleTime: 5678, + retry: undefined, + refetchOnWindowFocus: true, + }), + ) + + await vi.waitFor(() => { + expect(collection.size).toBe(1) + }) + + const query = clientWithDefaults.getQueryCache().find({ + queryKey, + exact: true, + }) + const options = query?.options as any + expect(options.staleTime).toBe(5678) + expect(options.retry).toBe(3) + expect(options.refetchOnWindowFocus).toBe(true) + + clientWithDefaults.clear() + }) + it(`should initialize and fetch initial data`, async () => { const queryKey = [`testItems`] const initialItems: Array = [ @@ -228,6 +791,75 @@ describe(`QueryCollection`, () => { expect(collection._state.syncedData.get(`2`)).toEqual(initialItems[1]) }) + it(`should not duplicate insert into includes child collection after update refetch`, async () => { + type LineItem = { id: string; productId: string } + type Product = { id: string; categoryId: number; name: string } + + const lineItems = createCollection( + mockSyncCollectionOptions({ + id: `query-collection-line-items`, + getKey: (lineItem) => lineItem.id, + initialData: [{ id: `line-1`, productId: `product-1` }], + }), + ) + + let productsData: Array = [ + { id: `product-1`, categoryId: 1, name: `Widget` }, + ] + + const products = createCollection( + queryCollectionOptions({ + id: `query-collection-products`, + queryClient, + queryKey: [`products`], + queryFn: vi + .fn() + .mockImplementation(() => Promise.resolve(productsData)), + getKey: (product) => product.id, + startSync: true, + onUpdate: async ({ transaction }) => { + for (const mutation of transaction.mutations) { + productsData = productsData.map((product) => + product.id === mutation.key ? mutation.modified : product, + ) + } + }, + }), + ) + + const collection = createLiveQueryCollection((q) => + q.from({ lineItem: lineItems }).select(({ lineItem }) => ({ + id: lineItem.id, + product: q + .from({ product: products }) + .where(({ product }) => eq(product.id, lineItem.productId)) + .select(({ product }) => ({ + id: product.id, + categoryId: product.categoryId, + name: product.name, + })), + })), + ) + + await collection.preload() + + expect(() => { + products.update(`product-1`, (draft) => { + draft.categoryId = 2 + }) + }).not.toThrow() + + await vi.waitFor(() => { + expect( + stripVirtualProps((collection.get(`line-1`) as any).product.toArray[0]), + ).toEqual({ + id: `product-1`, + categoryId: 2, + name: `Widget`, + }) + }) + }) + it(`should update collection when query data changes`, async () => { const queryKey = [`testItems`] const initialItems: Array = [ @@ -798,11 +1430,49 @@ describe(`QueryCollection`, () => { it(`Whole response is cached in QueryClient when used with select option`, async () => { const queryKey = [`select-test`] - const queryFn = vi.fn().mockResolvedValue(initialMetaData) - const select = vi.fn().mockReturnValue(initialMetaData.data) + const queryFn = vi.fn().mockResolvedValue(initialMetaData) + const select = vi.fn().mockReturnValue(initialMetaData.data) + + const options = queryCollectionOptions({ + id: `test`, + queryClient, + queryKey, + queryFn, + select, + getKey, + startSync: true, + }) + const collection = createCollection(options) + + await vi.waitFor(() => { + expect(queryFn).toHaveBeenCalledTimes(1) + expect(select).toHaveBeenCalledTimes(1) + expect(collection.size).toBe(2) + }) + + // Verify that the query cache state exists along with its metadata + const initialCache = queryClient.getQueryData( + queryKey, + ) as MetaDataType + expect(initialCache).toEqual(initialMetaData) + }) + + it(`materializes selected rows while preserving wrapped Query cache response`, async () => { + const queryKey = [`select-row-extraction-test`] + const wrappedResponse = { + items: initialMetaData.data, + meta: { page: 1, total: initialMetaData.data.length }, + } + const expectedCacheResponse = { + items: initialMetaData.data.map((item) => ({ ...item })), + meta: { page: 1, total: initialMetaData.data.length }, + } + + const queryFn = vi.fn().mockResolvedValue(wrappedResponse) + const select = vi.fn((data: typeof wrappedResponse) => data.items) const options = queryCollectionOptions({ - id: `test`, + id: `select-row-extraction-test`, queryClient, queryKey, queryFn, @@ -815,14 +1485,16 @@ describe(`QueryCollection`, () => { await vi.waitFor(() => { expect(queryFn).toHaveBeenCalledTimes(1) expect(select).toHaveBeenCalledTimes(1) - expect(collection.size).toBe(2) + expect(collection.size).toBe(wrappedResponse.items.length) }) - // Verify that the query cache state exists along with its metadata - const initialCache = queryClient.getQueryData( - queryKey, - ) as MetaDataType - expect(initialCache).toEqual(initialMetaData) + expect(stripVirtualProps(collection.get(`1`))).toEqual( + wrappedResponse.items[0], + ) + expect(stripVirtualProps(collection.get(`2`))).toEqual( + wrappedResponse.items[1], + ) + expect(queryClient.getQueryData(queryKey)).toEqual(expectedCacheResponse) }) it(`should not throw error when using writeInsert with select option`, async () => { @@ -1533,6 +2205,417 @@ describe(`QueryCollection`, () => { }) }) + describe(`invalidation behavior`, () => { + it(`rematerializes an active eager query after exact invalidation`, async () => { + const queryKey = [`invalidation-exact-eager-test`] + let items: Array = [{ id: `1`, name: `Item 1` }] + const queryFn = vi.fn().mockImplementation(() => Promise.resolve(items)) + + const collection = createCollection( + queryCollectionOptions({ + id: `invalidation-exact-eager-test`, + queryClient, + queryKey, + queryFn, + getKey, + startSync: true, + }), + ) + + try { + await vi.waitFor(() => { + expect(queryFn).toHaveBeenCalledTimes(1) + expect(stripVirtualProps(collection.get(`1`))).toEqual({ + id: `1`, + name: `Item 1`, + }) + }) + + items = [{ id: `1`, name: `Updated Item 1` }] + await queryClient.invalidateQueries({ queryKey, exact: true }) + + await vi.waitFor(() => { + expect(queryFn).toHaveBeenCalledTimes(2) + expect(stripVirtualProps(collection.get(`1`))).toEqual({ + id: `1`, + name: `Updated Item 1`, + }) + }) + } finally { + await collection.cleanup() + } + + expect( + queryClient.getQueryCache().find({ queryKey })?.getObserversCount(), + ).toBe(0) + }) + + it(`rematerializes an active eager query after prefix invalidation`, async () => { + const rootQueryKey = [`invalidation-prefix-eager-test`] + const queryKey = [...rootQueryKey, `child`] + let items: Array = [{ id: `1`, name: `Item 1` }] + const queryFn = vi.fn().mockImplementation(() => Promise.resolve(items)) + + const collection = createCollection( + queryCollectionOptions({ + id: `invalidation-prefix-eager-test`, + queryClient, + queryKey, + queryFn, + getKey, + startSync: true, + }), + ) + + try { + await vi.waitFor(() => { + expect(queryFn).toHaveBeenCalledTimes(1) + expect(stripVirtualProps(collection.get(`1`))).toEqual({ + id: `1`, + name: `Item 1`, + }) + }) + + items = [{ id: `1`, name: `Updated Item 1` }] + await queryClient.invalidateQueries({ queryKey: rootQueryKey }) + + await vi.waitFor(() => { + expect(queryFn).toHaveBeenCalledTimes(2) + expect(stripVirtualProps(collection.get(`1`))).toEqual({ + id: `1`, + name: `Updated Item 1`, + }) + }) + } finally { + await collection.cleanup() + } + }) + + it(`rematerializes an active on-demand subset after exact invalidation`, async () => { + const queryKey = [`invalidation-exact-on-demand-test`] + let items: Array = [{ id: `1`, name: `Item 1` }] + const queryFn = vi.fn().mockImplementation(() => Promise.resolve(items)) + + const collection = createCollection( + queryCollectionOptions({ + id: `invalidation-exact-on-demand-test`, + queryClient, + queryKey, + queryFn, + getKey, + syncMode: `on-demand`, + }), + ) + const liveQuery = createLiveQueryCollection({ + query: (q) => + q + .from({ item: collection }) + .select(({ item }) => ({ id: item.id, name: item.name })), + }) + + try { + await liveQuery.preload() + + await vi.waitFor(() => { + expect(queryFn).toHaveBeenCalledTimes(1) + expect(stripVirtualProps(collection.get(`1`))).toEqual({ + id: `1`, + name: `Item 1`, + }) + }) + + items = [{ id: `1`, name: `Updated Item 1` }] + await queryClient.invalidateQueries({ queryKey, exact: true }) + + await vi.waitFor(() => { + expect(queryFn).toHaveBeenCalledTimes(2) + expect(stripVirtualProps(collection.get(`1`))).toEqual({ + id: `1`, + name: `Updated Item 1`, + }) + }) + } finally { + await liveQuery.cleanup() + } + }) + + it(`rematerializes an active on-demand subset after root prefix invalidation`, async () => { + const queryKey = [`invalidation-prefix-on-demand-test`] + let items: Array = [{ id: `1`, name: `Item 1` }] + const observedQueryKeys: Array> = [] + const queryFn = vi + .fn() + .mockImplementation( + (ctx: QueryFunctionContext>) => { + observedQueryKeys.push(ctx.queryKey) + return Promise.resolve(items) + }, + ) + + const collection = createCollection( + queryCollectionOptions({ + id: `invalidation-prefix-on-demand-test`, + queryClient, + queryKey, + queryFn, + getKey, + syncMode: `on-demand`, + }), + ) + const liveQuery = createLiveQueryCollection({ + query: (q) => + q + .from({ item: collection }) + .where(({ item }) => eq(item.id, `1`)) + .select(({ item }) => ({ id: item.id, name: item.name })), + }) + + try { + await liveQuery.preload() + + await vi.waitFor(() => { + expect(queryFn).toHaveBeenCalledTimes(1) + expect(observedQueryKeys[0]?.length).toBeGreaterThan( + queryKey.length, + ) + expect(stripVirtualProps(collection.get(`1`))).toEqual({ + id: `1`, + name: `Item 1`, + }) + }) + + items = [{ id: `1`, name: `Updated Item 1` }] + await queryClient.invalidateQueries({ queryKey }) + + await vi.waitFor(() => { + expect(queryFn).toHaveBeenCalledTimes(2) + expect(stripVirtualProps(collection.get(`1`))).toEqual({ + id: `1`, + name: `Updated Item 1`, + }) + }) + } finally { + await liveQuery.cleanup() + } + }) + + it(`keeps overlapping on-demand subset rows materialized when one subset is invalidated`, async () => { + const queryKey = [`invalidation-overlap-on-demand-test`] + let firstSubset: Array = [ + { id: `1`, name: `Item 1` }, + { id: `2`, name: `Shared Item` }, + ] + const secondSubset: Array = [ + { id: `2`, name: `Shared Item` }, + { id: `3`, name: `Item 3` }, + ] + const observedQueryKeys: Array> = [] + const queryFn = vi + .fn() + .mockImplementation( + (ctx: QueryFunctionContext>) => { + const firstObservedKey = observedQueryKeys[0] + observedQueryKeys.push(ctx.queryKey) + return Promise.resolve( + firstObservedKey === undefined || + hashKey(ctx.queryKey) === hashKey(firstObservedKey) + ? firstSubset + : secondSubset, + ) + }, + ) + + const collection = createCollection( + queryCollectionOptions({ + id: `invalidation-overlap-on-demand-test`, + queryClient, + queryKey, + queryFn, + getKey, + syncMode: `on-demand`, + }), + ) + const firstLiveQuery = createLiveQueryCollection({ + query: (q) => + q + .from({ item: collection }) + .where(({ item }) => inArray(item.id, [`1`, `2`])) + .select(({ item }) => ({ id: item.id, name: item.name })), + }) + const secondLiveQuery = createLiveQueryCollection({ + query: (q) => + q + .from({ item: collection }) + .where(({ item }) => inArray(item.id, [`2`, `3`])) + .select(({ item }) => ({ id: item.id, name: item.name })), + }) + + try { + await firstLiveQuery.preload() + await secondLiveQuery.preload() + + await vi.waitFor(() => { + expect(queryFn).toHaveBeenCalledTimes(2) + expect(collection.size).toBe(3) + }) + + firstSubset = [ + { id: `1`, name: `Updated Item 1` }, + { id: `2`, name: `Updated Shared Item` }, + ] + await queryClient.invalidateQueries({ + queryKey: observedQueryKeys[0], + exact: true, + }) + + await vi.waitFor(() => { + expect(queryFn).toHaveBeenCalledTimes(3) + expect(stripVirtualProps(collection.get(`1`))).toEqual({ + id: `1`, + name: `Updated Item 1`, + }) + expect(stripVirtualProps(collection.get(`2`))).toEqual({ + id: `2`, + name: `Updated Shared Item`, + }) + }) + expect(stripVirtualProps(collection.get(`3`))).toEqual({ + id: `3`, + name: `Item 3`, + }) + } finally { + await firstLiveQuery.cleanup() + await secondLiveQuery.cleanup() + } + }) + + it(`retains existing rows when an invalidation refetch fails`, async () => { + const queryKey = [`invalidation-failed-refetch-test`] + const initialItems: Array = [{ id: `1`, name: `Item 1` }] + const refetchError = new Error(`refetch failed`) + const queryFn = vi + .fn() + .mockResolvedValueOnce(initialItems) + .mockRejectedValueOnce(refetchError) + + const collection = createCollection( + queryCollectionOptions({ + id: `invalidation-failed-refetch-test`, + queryClient, + queryKey, + queryFn, + getKey, + startSync: true, + retry: false, + }), + ) + + try { + await vi.waitFor(() => { + expect(queryFn).toHaveBeenCalledTimes(1) + expect(stripVirtualProps(collection.get(`1`))).toEqual({ + id: `1`, + name: `Item 1`, + }) + }) + + await queryClient.invalidateQueries({ queryKey, exact: true }) + + await vi.waitFor(() => { + expect(queryFn).toHaveBeenCalledTimes(2) + expect(collection.utils.lastError).toBe(refetchError) + }) + expect(collection.size).toBe(1) + expect(stripVirtualProps(collection.get(`1`))).toEqual({ + id: `1`, + name: `Item 1`, + }) + } finally { + await collection.cleanup() + } + }) + + // Retained/persisted invalidation is intentionally not covered here: the + // existing unit fixtures do not exercise the full persisted retention path + // without introducing broader persistence setup. This PR characterizes active, + // inactive, removed, overlapping, and failed-refetch behavior first. + it(`does not rematerialize inactive cached query rows after invalidation`, async () => { + const retainedQueryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 0, + gcTime: 60_000, + retry: false, + }, + }, + }) + const queryKey = [`invalidation-inactive-cached-test`] + let items: Array = [{ id: `1`, name: `Item 1` }] + const queryFn = vi.fn().mockImplementation(() => Promise.resolve(items)) + + const collection = createCollection( + queryCollectionOptions({ + id: `invalidation-inactive-cached-test`, + queryClient: retainedQueryClient, + queryKey, + queryFn, + getKey, + startSync: true, + }), + ) + + await vi.waitFor(() => { + expect(queryFn).toHaveBeenCalledTimes(1) + expect(collection.size).toBe(1) + }) + + await collection.cleanup() + expect(collection.status).toBe(`cleaned-up`) + expect( + retainedQueryClient.getQueryCache().find({ queryKey }), + ).toBeDefined() + + items = [{ id: `1`, name: `Updated Item 1` }] + await retainedQueryClient.invalidateQueries({ queryKey, exact: true }) + + expect(queryFn).toHaveBeenCalledTimes(1) + expect(collection.size).toBe(0) + retainedQueryClient.clear() + }) + + it(`does not refetch a removed query after invalidation`, async () => { + const queryKey = [`invalidation-removed-query-test`] + let items: Array = [{ id: `1`, name: `Item 1` }] + const queryFn = vi.fn().mockImplementation(() => Promise.resolve(items)) + + const collection = createCollection( + queryCollectionOptions({ + id: `invalidation-removed-query-test`, + queryClient, + queryKey, + queryFn, + getKey, + startSync: true, + }), + ) + + await vi.waitFor(() => { + expect(queryFn).toHaveBeenCalledTimes(1) + expect(collection.size).toBe(1) + }) + + await collection.cleanup() + queryClient.removeQueries({ queryKey, exact: true }) + expect(queryClient.getQueryCache().find({ queryKey })).toBeUndefined() + + items = [{ id: `1`, name: `Updated Item 1` }] + await queryClient.invalidateQueries({ queryKey, exact: true }) + + expect(queryFn).toHaveBeenCalledTimes(1) + expect(collection.size).toBe(0) + }) + }) + it(`should handle concurrent query operations`, async () => { const queryKey = [`concurrent-test`] const items = [{ id: `1`, name: `Item 1` }] @@ -1714,9 +2797,254 @@ describe(`QueryCollection`, () => { // Wait a bit to ensure any async operations complete await flushPromises() - // Collection should be cleaned up and not have processed the data - expect(collection.status).toBe(`cleaned-up`) - expect(collection.size).toBe(0) + // Collection should be cleaned up and not have processed the data + expect(collection.status).toBe(`cleaned-up`) + expect(collection.size).toBe(0) + }) + + describe(`query cancellation and subset cleanup lifecycle`, () => { + const createSubset = (collection: Collection) => + createLiveQueryCollection({ + query: (q) => + q + .from({ item: collection }) + .where(({ item }) => eq(item.id, `1`)) + .select(({ item }) => ({ id: item.id, name: item.name })), + }) + + it(`forwards Query Core's signal and aborts an in-flight eager query on collection cleanup`, async () => { + const deferred = createDeferred>() + let signal: AbortSignal | undefined + const collection = createCollection( + queryCollectionOptions({ + id: `signal-forwarding-cleanup-test`, + queryClient, + queryKey: [`signal-forwarding-cleanup-test`], + queryFn: (context) => { + signal = context.signal + // Reading the signal makes Query Core treat the request as cancellable. + void context.signal.aborted + return deferred.promise + }, + getKey, + startSync: true, + }), + ) + + await vi.waitFor(() => expect(signal).toBeDefined()) + expect(signal?.aborted).toBe(false) + + await collection.cleanup() + + expect(signal?.aborted).toBe(true) + expect(collection.size).toBe(0) + // Query Core may retain the cancelled cache entry, but cleanup releases every observer. + expect( + queryClient + .getQueryCache() + .find({ queryKey: [`signal-forwarding-cleanup-test`] }) + ?.getObserversCount() ?? 0, + ).toBe(0) + }) + + it(`cleans listeners immediately and resolves the unloaded preload before its request settles`, async () => { + const deferred = createDeferred>() + const collection = createCollection( + queryCollectionOptions({ + id: `late-subset-result-test`, + queryClient, + queryKey: [`late-subset-result-test`], + queryFn: () => deferred.promise, + getKey, + syncMode: `on-demand`, + }), + ) + const liveQuery = createSubset(collection) + let preloadResolved = false + void liveQuery.preload().then(() => { + preloadResolved = true + }) + + await vi.waitFor(() => expect(queryClient.isFetching()).toBe(1)) + await liveQuery.cleanup() + + const subsetQuery = queryClient.getQueryCache().findAll({ + queryKey: [`late-subset-result-test`], + })[0] + // This assertion runs while the request is unresolved and directly guards the + // ready-listener bookkeeping bug: unload must synchronously detach its observer. + expect(subsetQuery?.getObserversCount() ?? 0).toBe(0) + // Live-query cleanup resolves its preload even though Query Core is still fetching. + expect(preloadResolved).toBe(true) + + deferred.resolve([{ id: `1`, name: `Late item` }]) + await vi.waitFor(() => expect(queryClient.isFetching()).toBe(0)) + + expect(collection.size).toBe(0) + expect(preloadResolved).toBe(true) + expect(subsetQuery?.getObserversCount() ?? 0).toBe(0) + await collection.cleanup() + }) + + it(`keeps an unloaded preload resolved when its pending request later rejects`, async () => { + const deferred = createDeferred>() + const collection = createCollection( + queryCollectionOptions({ + id: `late-subset-rejection-test`, + queryClient, + queryKey: [`late-subset-rejection-test`], + queryFn: () => deferred.promise, + getKey, + syncMode: `on-demand`, + }), + ) + const liveQuery = createSubset(collection) + const preloadPromise = liveQuery.preload() + + await vi.waitFor(() => expect(queryClient.isFetching()).toBe(1)) + await liveQuery.cleanup() + + const subsetQuery = queryClient.getQueryCache().findAll({ + queryKey: [`late-subset-rejection-test`], + })[0] + expect(subsetQuery?.getObserversCount() ?? 0).toBe(0) + await expect(preloadPromise).resolves.toBeUndefined() + + deferred.reject(new Error(`Late query failure`)) + await vi.waitFor(() => expect(queryClient.isFetching()).toBe(0)) + + expect(collection.size).toBe(0) + expect(subsetQuery?.getObserversCount() ?? 0).toBe(0) + await collection.cleanup() + }) + + it(`preserves an active subset across cache removal and accepts a late notification from its detached observer`, async () => { + const queryKey = [`cache-removal-late-notification-test`] + const collection = createCollection( + queryCollectionOptions({ + id: `cache-removal-late-notification-test`, + queryClient, + queryKey, + queryFn: () => Promise.resolve([{ id: `1`, name: `Initial item` }]), + getKey, + syncMode: `on-demand`, + }), + ) + const liveQuery = createSubset(collection) + await liveQuery.preload() + const subsetQuery = queryClient.getQueryCache().findAll({ queryKey })[0] + expect(subsetQuery).toBeDefined() + + // A Query Core `removed` event can arrive before this collection's observer + // is detached. Existing semantics retain the active rows and observer. + queryClient.getQueryCache().remove(subsetQuery!) + expect(queryClient.getQueryCache().findAll({ queryKey })).toHaveLength( + 0, + ) + expect(collection.get(`1`)?.name).toBe(`Initial item`) + expect(subsetQuery!.getObserversCount()).toBe(1) + + // The retained observer can still notify after its query left the cache. + subsetQuery!.setData([{ id: `1`, name: `Late notification` }]) + await vi.waitFor(() => + expect(collection.get(`1`)?.name).toBe(`Late notification`), + ) + + await liveQuery.cleanup() + expect(subsetQuery!.getObserversCount()).toBe(0) + expect(collection.size).toBe(0) + await collection.cleanup() + }) + + it(`deterministically materializes a shared in-flight result after fast subset unmount and remount`, async () => { + const deferred = createDeferred>() + const queryFn = vi.fn(() => deferred.promise) + const collection = createCollection( + queryCollectionOptions({ + id: `fast-subset-remount-test`, + queryClient, + queryKey: [`fast-subset-remount-test`], + queryFn, + getKey, + syncMode: `on-demand`, + }), + ) + const firstLiveQuery = createSubset(collection) + void firstLiveQuery.preload().catch(() => undefined) + await vi.waitFor(() => expect(queryFn).toHaveBeenCalledTimes(1)) + + await firstLiveQuery.cleanup() + const secondLiveQuery = createSubset(collection) + const secondPreload = secondLiveQuery.preload() + deferred.resolve([{ id: `1`, name: `Remounted item` }]) + await secondPreload + + expect(queryFn).toHaveBeenCalledTimes(1) + expect(stripVirtualProps(collection.get(`1`))).toEqual({ + id: `1`, + name: `Remounted item`, + }) + await secondLiveQuery.cleanup() + expect(collection.size).toBe(0) + await collection.cleanup() + }) + + it(`keeps invalidate-unsubscribe-resubscribe compatible while removing stale subset rows and observers`, async () => { + let items: Array = [{ id: `1`, name: `Initial item` }] + const queryFn = vi.fn(() => Promise.resolve(items)) + const collection = createCollection( + queryCollectionOptions({ + id: `subset-invalidation-remount-test`, + queryClient, + queryKey: [`subset-invalidation-remount-test`], + queryFn, + getKey, + syncMode: `on-demand`, + }), + ) + const firstLiveQuery = createSubset(collection) + await firstLiveQuery.preload() + const subsetQuery = queryClient.getQueryCache().findAll({ + queryKey: [`subset-invalidation-remount-test`], + })[0] + expect(subsetQuery).toBeDefined() + + items = [{ id: `1`, name: `Invalidated item` }] + await queryClient.invalidateQueries({ + queryKey: subsetQuery!.queryKey, + exact: true, + }) + await vi.waitFor(() => + expect(collection.get(`1`)?.name).toBe(`Invalidated item`), + ) + + await firstLiveQuery.cleanup() + expect(collection.size).toBe(0) + expect(subsetQuery!.getObserversCount()).toBe(0) + + items = [{ id: `1`, name: `Remounted item` }] + const secondLiveQuery = createSubset(collection) + await secondLiveQuery.preload() + await queryClient.invalidateQueries({ + queryKey: subsetQuery!.queryKey, + exact: true, + }) + await vi.waitFor(() => + expect(collection.get(`1`)?.name).toBe(`Remounted item`), + ) + + await secondLiveQuery.cleanup() + expect(collection.size).toBe(0) + expect( + queryClient + .getQueryCache() + .findAll({ + queryKey: [`subset-invalidation-remount-test`], + })[0] + ?.getObserversCount() ?? 0, + ).toBe(0) + await collection.cleanup() + }) }) it(`should maintain data consistency during rapid updates`, async () => { @@ -4421,6 +5749,193 @@ describe(`QueryCollection`, () => { }) }) + describe(`ownership lifecycle characterization`, () => { + it(`removes only rows whose final subset owner is unloaded`, async () => { + const queryFn = vi + .fn() + .mockResolvedValueOnce([ + { id: `1`, name: `First only` }, + { id: `2`, name: `Shared` }, + ]) + .mockResolvedValueOnce([ + { id: `2`, name: `Shared` }, + { id: `3`, name: `Second only` }, + ]) + const options = queryCollectionOptions({ + id: `ownership-overlapping-subsets-test`, + queryClient, + queryKey: [`ownership-overlapping-subsets-test`], + queryFn, + getKey, + syncMode: `on-demand`, + }) + const ownershipMaps = inspectOwnershipMaps(options) + const collection = createCollection(options) + const firstSubset = createLiveQueryCollection({ + query: (q) => + q + .from({ item: collection }) + .where(({ item }) => inArray(item.id, [`1`, `2`])), + }) + const secondSubset = createLiveQueryCollection({ + query: (q) => + q + .from({ item: collection }) + .where(({ item }) => inArray(item.id, [`2`, `3`])), + }) + + await firstSubset.preload() + await secondSubset.preload() + expect(collection.size).toBe(3) + + await firstSubset.cleanup() + await vi.waitFor(() => { + expect(collection.has(`1`)).toBe(false) + expect(collection.has(`2`)).toBe(true) + expect(collection.has(`3`)).toBe(true) + }) + expectNoEmptyRowOwnershipSets(ownershipMaps) + + await secondSubset.cleanup() + await vi.waitFor(() => { + expect(collection.size).toBe(0) + }) + expectNoEmptyRowOwnershipSets(ownershipMaps) + expect(ownershipMaps.rowToQueries.size).toBe(0) + expect(ownershipMaps.queryToRows.size).toBe(0) + }) + + it(`expires the Query cache entry after unload without restoring deleted rows`, async () => { + const expiringQueryClient = new QueryClient({ + defaultOptions: { + queries: { gcTime: 100, retry: false, staleTime: Infinity }, + }, + }) + const queryKey = [`ownership-query-cache-expiry-test`] + const collection = createCollection( + queryCollectionOptions({ + id: `ownership-query-cache-expiry-test`, + queryClient: expiringQueryClient, + queryKey, + queryFn: async () => [{ id: `1`, name: `Only row` }], + getKey, + syncMode: `on-demand`, + }), + ) + const liveQuery = createLiveQueryCollection({ + query: (q) => q.from({ item: collection }), + }) + + await liveQuery.preload() + expect(collection.has(`1`)).toBe(true) + + vi.useFakeTimers() + try { + await liveQuery.cleanup() + expect(collection.size).toBe(0) + expect( + expiringQueryClient.getQueryCache().find({ queryKey }), + ).toBeDefined() + + await vi.advanceTimersByTimeAsync(101) + + expect( + expiringQueryClient.getQueryCache().find({ queryKey }), + ).toBeUndefined() + expect(collection.size).toBe(0) + } finally { + vi.useRealTimers() + expiringQueryClient.clear() + } + }) + + it(`hydrates a retained row and deletes it when its query revalidates empty`, async () => { + const queryKey = [`ownership-retained-hydration-test`] + const queryHash = hashKey(queryKey) + const retainedRow: CategorisedItem = { + id: `1`, + name: `Retained row`, + category: `A`, + } + let releaseRevalidation!: () => void + const revalidationReleased = new Promise((resolve) => { + releaseRevalidation = resolve + }) + const queryFn = vi.fn(async () => { + await revalidationReleased + return [] + }) + const baseOptions = queryCollectionOptions({ + id: `ownership-retained-hydration-test`, + queryClient, + queryKey, + queryFn, + getKey: (item) => item.id, + startSync: true, + }) + const originalSync = baseOptions.sync + const ownershipMaps = inspectOwnershipMaps(baseOptions) + const metadataHarness = createInMemorySyncMetadataApi< + string | number, + CategorisedItem + >({ + persistedRows: new Map([[retainedRow.id, retainedRow]]), + rowMetadata: new Map([ + [ + retainedRow.id, + { + queryCollection: { owners: { [queryHash]: true } }, + }, + ], + ]), + collectionMetadata: new Map([ + [ + `queryCollection:gc:${queryHash}`, + { queryHash, mode: `until-revalidated` }, + ], + ]), + }) + const collection = createCollection({ + ...baseOptions, + sync: { + sync: (params: Parameters[0]) => { + params.begin({ immediate: true }) + params.write({ type: `insert`, value: retainedRow }) + params.commit() + return originalSync.sync({ + ...params, + metadata: metadataHarness.api, + }) + }, + }, + }) + + expect(collection.has(retainedRow.id)).toBe(true) + expect( + metadataHarness.collectionMetadata.has( + `queryCollection:gc:${queryHash}`, + ), + ).toBe(true) + + releaseRevalidation() + await vi.waitFor(() => { + expect(queryFn).toHaveBeenCalledTimes(1) + expect(collection.has(retainedRow.id)).toBe(false) + }) + expect(metadataHarness.rowMetadata.get(retainedRow.id)).toBeUndefined() + expectNoEmptyRowOwnershipSets(ownershipMaps) + expect(ownershipMaps.rowToQueries.size).toBe(0) + expect(ownershipMaps.queryToRows.get(queryHash)).toEqual(new Set()) + expect( + metadataHarness.collectionMetadata.has( + `queryCollection:gc:${queryHash}`, + ), + ).toBe(false) + + await collection.cleanup() + }) + }) + it(`should handle duplicate subset loads correctly (refcount bug)`, async () => { // This test catches Bug 1: missing refcount increment when reusing existing observer // When two subscriptions load the same subset, unloading one should NOT destroy @@ -4501,6 +6016,89 @@ describe(`QueryCollection`, () => { }) }) + it(`should not let initial data satisfy persisted revalidation`, async () => { + const queryKey = [`persisted-initial-data-revalidation`] + const queryHash = hashKey(queryKey) + const retainedRow = { + id: `retained`, + name: `Retained`, + category: `A`, + } + const initialRow = { + id: `initial`, + name: `Initial`, + category: `A`, + } + const serverRow = { id: `server`, name: `Server`, category: `A` } + const serverResult = createDeferred>() + const queryFn = vi.fn(() => serverResult.promise) + const adapter = createPersistedQueryAdapter({ + rows: new Map([[retainedRow.id, retainedRow]]), + rowMetadata: new Map([ + [ + retainedRow.id, + { + queryCollection: { + owners: { + [queryHash]: true, + }, + }, + }, + ], + ]), + collectionMetadata: new Map([ + [ + `queryCollection:gc:${queryHash}`, + { + queryHash, + mode: `until-revalidated`, + }, + ], + ]), + }) + + const collection = createCollection( + persistedCollectionOptions({ + ...(queryCollectionOptions({ + id: `persisted-initial-data-revalidation`, + queryClient, + queryKey, + queryFn, + getKey: (item) => item.id, + initialData: [initialRow], + staleTime: Infinity, + syncMode: `eager`, + startSync: true, + }) as any), + persistence: { + adapter, + }, + }) as any, + ) + + try { + await vi.waitFor(() => { + expect(queryFn).toHaveBeenCalledTimes(1) + }) + expect(adapter.rows.has(retainedRow.id)).toBe(true) + expect(adapter.rows.has(initialRow.id)).toBe(false) + expect( + adapter.collectionMetadata.has(`queryCollection:gc:${queryHash}`), + ).toBe(true) + + serverResult.resolve([serverRow]) + await vi.waitFor(() => { + expect(adapter.rows.has(retainedRow.id)).toBe(false) + expect(adapter.rows.get(serverRow.id)).toEqual(serverRow) + expect( + adapter.collectionMetadata.has(`queryCollection:gc:${queryHash}`), + ).toBe(false) + }) + } finally { + await collection.cleanup() + } + }) + it(`should diff against retained query-owned rows on warm start`, async () => { const baseQueryKey = [`persisted-baseline-test`] const queryFn = vi.fn().mockResolvedValue([]) @@ -4888,6 +6486,113 @@ describe(`QueryCollection`, () => { ).toBe(false) }) + it(`should clean up an inserted row dropped by the query after a reload`, async () => { + // A row inserted while the collection is mounted is persisted. After the + // app reloads, if the query no longer returns that row, it must be removed + // from both the live collection and the persisted store. + const queryKey = [`reload-insert-cleanup`] + const makeQueryClient = () => + new QueryClient({ + defaultOptions: { + queries: { staleTime: 0, gcTime: 5 * 60 * 1000, retry: false }, + }, + }) + + // The shared on-device store, surviving across the two sessions. + const adapter = createPersistedQueryAdapter({}) + + // ---- First session: insert an item that the server then returns ---- + let serverRows: Array = [] + const firstQueryClient = makeQueryClient() + const collection1 = createCollection( + persistedCollectionOptions({ + ...(queryCollectionOptions({ + id: `reload-insert-cleanup`, + queryClient: firstQueryClient, + queryKey, + queryFn: async () => serverRows, + getKey: (item: CategorisedItem): string => item.id, + syncMode: `eager`, + startSync: true, + onInsert: async ({ transaction }) => { + // The mutation reaches the server: the item now appears in the + // API response for subsequent fetches. + for (const mutation of transaction.mutations) { + serverRows = [...serverRows, mutation.modified] + } + }, + }) as any), + persistence: { adapter }, + }) as any, + ) + + await collection1.stateWhenReady() + await flushPromises() + + await collection1.insert({ id: `1`, name: `Buy milk`, category: `A` }) + // The query refetches and sees the now-synced row. + await firstQueryClient.invalidateQueries({ queryKey }) + await flushPromises() + await flushPromises() + + expect(adapter.rows.has(`1`)).toBe(true) + + // ---- App closes; the row is removed on the server out of band ---- + serverRows = [] + + // ---- Second session: reload from the persisted store ---- + const secondQueryClient = makeQueryClient() + const adapter2 = createPersistedQueryAdapter({ + rows: adapter.rows, + rowMetadata: adapter.rowMetadata, + collectionMetadata: adapter.collectionMetadata, + }) + let releaseSecondFetch!: () => void + const secondFetchReleased = new Promise((resolve) => { + releaseSecondFetch = resolve + }) + const collection2 = createCollection( + persistedCollectionOptions({ + ...(queryCollectionOptions({ + id: `reload-insert-cleanup`, + queryClient: secondQueryClient, + queryKey, + queryFn: async () => { + await secondFetchReleased + return serverRows + }, + getKey: (item: CategorisedItem): string => item.id, + syncMode: `eager`, + startSync: true, + }) as any), + persistence: { adapter: adapter2 }, + }) as any, + ) + + await vi.waitFor(() => { + expect(collection2.has(`1`)).toBe(true) + expect(adapter2.rows.has(`1`)).toBe(true) + }) + + const liveQuery = createLiveQueryCollection({ + query: (q) => q.from({ item: collection2 }), + }) + releaseSecondFetch() + await liveQuery.preload() + // The query responds on launch (after persisted rows have hydrated). + await flushPromises() + await flushPromises() + + await vi.waitFor(() => { + expect(collection2.has(`1`)).toBe(false) + expect(adapter2.rows.has(`1`)).toBe(false) + expect(liveQuery.size).toBe(0) + }) + + firstQueryClient.clear() + secondQueryClient.clear() + }) + it(`should expire retained ttl placeholders while the app stays open`, async () => { vi.useFakeTimers() try { @@ -4911,6 +6616,7 @@ describe(`QueryCollection`, () => { const baseOptions = queryCollectionOptions(config) const originalSync = baseOptions.sync + const ownershipMaps = inspectOwnershipMaps(baseOptions) const metadataHarness = createInMemorySyncMetadataApi< string | number, CategorisedItem @@ -4943,6 +6649,13 @@ describe(`QueryCollection`, () => { await liveQuery.cleanup() + expect(ownershipMaps.queryToRows.has(retainedQueryHash)).toBe(true) + expect(ownershipMaps.queryToRows.get(retainedQueryHash)).toEqual( + new Set([`1`]), + ) + expect(ownershipMaps.rowToQueries.get(`1`)).toEqual( + new Set([retainedQueryHash]), + ) expect( metadataHarness.collectionMetadata.get( `queryCollection:gc:${retainedQueryHash}`, @@ -4962,11 +6675,60 @@ describe(`QueryCollection`, () => { ), ).toBeUndefined() expect(collection.has(`1`)).toBe(false) + expect(ownershipMaps.queryToRows.has(retainedQueryHash)).toBe(false) + expect(ownershipMaps.rowToQueries.has(`1`)).toBe(false) } finally { vi.useRealTimers() } }) + it(`should clear retained ownership during explicit collection cleanup`, async () => { + const baseQueryKey = [`explicit-retained-cleanup-test`] + const retainedQueryHash = hashKey(baseQueryKey) + const items: Array = [ + { id: `1`, name: `Retained`, category: `A` }, + ] + const config: QueryCollectionConfig = { + id: `explicit-retained-cleanup-test`, + queryClient, + queryKey: () => baseQueryKey, + queryFn: async () => items, + getKey: (item) => item.id, + syncMode: `on-demand`, + startSync: true, + persistedGcTime: 60_000, + } + const baseOptions = queryCollectionOptions(config) + const originalSync = baseOptions.sync + const ownershipMaps = inspectOwnershipMaps(baseOptions) + const metadataHarness = createInMemorySyncMetadataApi< + string | number, + CategorisedItem + >({ persistedRows: new Map(items.map((item) => [item.id, item])) }) + const collection = createCollection({ + ...baseOptions, + sync: { + sync: (params: Parameters[0]) => + originalSync.sync({ ...params, metadata: metadataHarness.api }), + }, + }) + const liveQuery = createLiveQueryCollection({ + query: (q) => + q + .from({ item: collection }) + .where(({ item }) => eq(item.category, `A`)), + }) + + await liveQuery.preload() + await liveQuery.cleanup() + expect(ownershipMaps.queryToRows.has(retainedQueryHash)).toBe(true) + + await collection.cleanup() + + expect(ownershipMaps.queryToRows.size).toBe(0) + expect(ownershipMaps.rowToQueries.size).toBe(0) + }) + it(`should default persisted retention ttl to query gcTime when persistedGcTime is undefined`, async () => { vi.useFakeTimers() const gcTime = 120 @@ -5692,6 +7454,76 @@ describe(`QueryCollection`, () => { }) }) + describe(`On-demand query persistence`, () => { + const containsFunction = (value: unknown): boolean => { + if (typeof value === `function`) { + return true + } + + if (Array.isArray(value)) { + return value.some(containsFunction) + } + + if (value && typeof value === `object`) { + return Object.values(value as Record).some( + containsFunction, + ) + } + + return false + } + + it(`should keep dehydrated query state structured-clone safe after loading an on-demand subset with subscription state`, async () => { + const queryClient = new QueryClient() + const items: Array = [ + { id: `1`, name: `Item 1`, category: `A` }, + { id: `2`, name: `Item 2`, category: `B` }, + ] + + const collection = createCollection( + queryCollectionOptions({ + id: `on-demand-persistence-clone-safe-test`, + queryClient, + queryKey: [`on-demand-persistence-clone-safe-test`], + queryFn: vi.fn().mockResolvedValue(items), + getKey: (item) => item.id, + syncMode: `on-demand`, + startSync: true, + }), + ) + + try { + await collection._sync.loadSubset({ + where: eq(`category`, `A`), + subscription: { + options: { + onUnsubscribe: () => {}, + }, + } as unknown as NonNullable, + }) + + const cachedQuery = queryClient.getQueryCache().findAll()[0] + expect(cachedQuery?.meta?.loadSubsetOptions).toBeDefined() + expect( + cachedQuery?.meta?.loadSubsetOptions?.subscription, + ).toBeUndefined() + + const dehydrated = dehydrate(queryClient) + expect(dehydrated.queries.length).toBeGreaterThan(0) + expect(() => structuredClone(dehydrated)).not.toThrow() + + for (const query of dehydrated.queries) { + expect(containsFunction(query.meta)).toBe(false) + expect( + containsFunction(query.state.data as Record), + ).toBe(false) + } + } finally { + queryClient.clear() + } + }) + }) + describe(`Static queryKey with on-demand mode`, () => { it(`should automatically append serialized predicates to static queryKey in on-demand mode`, async () => { const items: Array = [ diff --git a/packages/react-db/CHANGELOG.md b/packages/react-db/CHANGELOG.md index d8d077ea50..339fab2c02 100644 --- a/packages/react-db/CHANGELOG.md +++ b/packages/react-db/CHANGELOG.md @@ -1,5 +1,65 @@ # @tanstack/react-db +## 0.1.94 + +### Patch Changes + +- Updated dependencies [[`8258d09`](https://github.com/TanStack/db/commit/8258d0955ab47c8510bd49ea59bcdbefd2ae054d), [`286964d`](https://github.com/TanStack/db/commit/286964d72612b59e3e427baabd9870f5a71a4281)]: + - @tanstack/db@0.6.16 + +## 0.1.93 + +### Patch Changes + +- Extract shared live-query adapter helpers into `@tanstack/db` ([#1641](https://github.com/TanStack/db/pull/1641)) + + Adds `isCollection`, `isSingleResultCollection`, and `getLiveQueryStatusFlags` to `@tanstack/db` and migrates all five framework adapters to use them. `isCollection` replaces the per-adapter duck-typing and Solid's `instanceof CollectionImpl` with one structural, multi-realm-safe guard (the `instanceof` form gave false negatives across dual-package boundaries). No behavior change; internal deduplication only. + +- Updated dependencies [[`eabcea7`](https://github.com/TanStack/db/commit/eabcea743fdfa045a2db01e12bef87403613102a), [`6d4c096`](https://github.com/TanStack/db/commit/6d4c096395b7ff3f428122ea8842bbead551a8c9)]: + - @tanstack/db@0.6.15 + +## 0.1.92 + +### Patch Changes + +- Updated dependencies [[`397e12a`](https://github.com/TanStack/db/commit/397e12a1224ad563e20a331eebcbe904cd4af948)]: + - @tanstack/db@0.6.14 + +## 0.1.91 + +### Patch Changes + +- Updated dependencies [[`99e9afe`](https://github.com/TanStack/db/commit/99e9afed46ab4083d66609a3e37ee44103c2177f), [`816b667`](https://github.com/TanStack/db/commit/816b6671c2cc9806715f6e6ed4410b3f4efb5afb)]: + - @tanstack/db@0.6.13 + +## 0.1.90 + +### Patch Changes + +- Updated dependencies [[`2b27dd1`](https://github.com/TanStack/db/commit/2b27dd1448da71c78a48e2390cb71b0ada1b1488)]: + - @tanstack/db@0.6.12 + +## 0.1.89 + +### Patch Changes + +- Updated dependencies [[`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`36fb29a`](https://github.com/TanStack/db/commit/36fb29ad7e906d39b6afdba2fd31e369c601bbb0), [`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`ac09b11`](https://github.com/TanStack/db/commit/ac09b1177a100eafa85cba3cd09dd1f53f933ded)]: + - @tanstack/db@0.6.11 + +## 0.1.88 + +### Patch Changes + +- Updated dependencies [[`307fdf8`](https://github.com/TanStack/db/commit/307fdf80f522a39a50e316316b3b75ba27fd5e84)]: + - @tanstack/db@0.6.10 + +## 0.1.87 + +### Patch Changes + +- Updated dependencies [[`2147345`](https://github.com/TanStack/db/commit/2147345236ceee6e73d9fc6c0cdc2385833199fc), [`00389a4`](https://github.com/TanStack/db/commit/00389a47b258ad58fc3a03c5cc6f66957b9bd2d1)]: + - @tanstack/db@0.6.9 + ## 0.1.86 ### Patch Changes diff --git a/packages/react-db/package.json b/packages/react-db/package.json index 65f6f2f315..8906a0112b 100644 --- a/packages/react-db/package.json +++ b/packages/react-db/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/react-db", - "version": "0.1.86", + "version": "0.1.94", "description": "React integration for @tanstack/db", "author": "Kyle Mathews", "license": "MIT", diff --git a/packages/react-db/src/useLiveQuery.ts b/packages/react-db/src/useLiveQuery.ts index 331ff3a279..8dc3d0a31b 100644 --- a/packages/react-db/src/useLiveQuery.ts +++ b/packages/react-db/src/useLiveQuery.ts @@ -1,12 +1,13 @@ import { useRef, useSyncExternalStore } from 'react' import { BaseQueryBuilder, - CollectionImpl, createLiveQueryCollection, + getLiveQueryStatusFlags, + isCollection, + isSingleResultCollection, } from '@tanstack/db' import type { Collection, - CollectionConfigSingleRowOption, CollectionStatus, Context, GetResult, @@ -317,13 +318,8 @@ export function useLiveQuery( configOrQueryOrCollection: any, deps: Array = [], ) { - // Check if it's already a collection by checking for specific collection methods - const isCollection = - configOrQueryOrCollection && - typeof configOrQueryOrCollection === `object` && - typeof configOrQueryOrCollection.subscribeChanges === `function` && - typeof configOrQueryOrCollection.startSyncImmediate === `function` && - typeof configOrQueryOrCollection.id === `string` + // Check if it's already a collection + const inputIsCollection = isCollection(configOrQueryOrCollection) // Use refs to cache collection and track dependencies const collectionRef = useRef | null>( @@ -342,14 +338,14 @@ export function useLiveQuery( // Check if we need to create/recreate the collection const needsNewCollection = !collectionRef.current || - (isCollection && configRef.current !== configOrQueryOrCollection) || - (!isCollection && + (inputIsCollection && configRef.current !== configOrQueryOrCollection) || + (!inputIsCollection && (depsRef.current === null || depsRef.current.length !== deps.length || depsRef.current.some((dep, i) => dep !== deps[i]))) if (needsNewCollection) { - if (isCollection) { + if (inputIsCollection) { // Warn when passing a collection directly with on-demand sync mode // In on-demand mode, data is only loaded when queries with predicates request it // Passing the collection directly doesn't provide any predicates, so no data loads @@ -379,7 +375,7 @@ export function useLiveQuery( if (result === undefined || result === null) { // Callback returned undefined/null - disabled query collectionRef.current = null - } else if (result instanceof CollectionImpl) { + } else if (isCollection(result)) { // Callback returned a Collection instance - use it directly result.startSyncImmediate() collectionRef.current = result @@ -434,17 +430,28 @@ export function useLiveQuery( return () => {} } + let unsubscribed = false + const subscription = collectionRef.current.subscribeChanges(() => { + // Drop late notifies that race with unsubscribe. + if (unsubscribed) return // Bump version on any change; getSnapshot will rebuild next time versionRef.current += 1 onStoreChange() }) - // Collection may be ready and will not receive initial `subscribeChanges()` + // Already-ready collections won't emit an initial change. Notify React + // ourselves, but defer to a microtask — calling onStoreChange synchronously + // here lands during the render-to-commit window and trips React's + // "state update on a component that hasn't mounted yet" warning. if (collectionRef.current.status === `ready`) { - versionRef.current += 1 - onStoreChange() + queueMicrotask(() => { + if (unsubscribed) return + versionRef.current += 1 + onStoreChange() + }) } return () => { + unsubscribed = true subscription.unsubscribe() } } @@ -516,9 +523,7 @@ export function useLiveQuery( } else { // Capture a stable view of entries for this snapshot to avoid tearing const entries = Array.from(snapshot.collection.entries()) - const config: CollectionConfigSingleRowOption = - snapshot.collection.config - const singleResult = config.singleResult + const singleResult = isSingleResultCollection(snapshot.collection) let stateCache: Map | null = null let dataCache: Array | null = null @@ -537,11 +542,7 @@ export function useLiveQuery( }, collection: snapshot.collection, status: snapshot.collection.status, - isLoading: snapshot.collection.status === `loading`, - isReady: snapshot.collection.status === `ready`, - isIdle: snapshot.collection.status === `idle`, - isError: snapshot.collection.status === `error`, - isCleanedUp: snapshot.collection.status === `cleaned-up`, + ...getLiveQueryStatusFlags(snapshot.collection.status), isEnabled: true, } } diff --git a/packages/react-db/tests/conformance.test.tsx b/packages/react-db/tests/conformance.test.tsx new file mode 100644 index 0000000000..303bbd89e6 --- /dev/null +++ b/packages/react-db/tests/conformance.test.tsx @@ -0,0 +1,213 @@ +/** + * React reference driver for the shared live-query conformance suite. + * + * Everything realm-sensitive — collection creation and query operators — is + * imported here (React package's `@tanstack/db`) and handed to the shared + * scenarios, so instances match what this package's `useLiveQuery` expects. + * + * `knownGaps` is populated empirically from the run below, NOT from the coverage + * matrix: only keys that actually fail belong here. Today that's just the + * universal order-only-move case (handled by the shared suite), so the list is empty. + */ +import { act, renderHook } from '@testing-library/react' +import { + coalesce, + count, + createCollection, + createLiveQueryCollection, + createOptimisticAction, + eq, + gt, + sum, +} from '@tanstack/db' +import { + mockSyncCollectionOptions, + mockSyncCollectionOptionsNoInitialState, +} from '../../db/tests/utils' +import { useLiveQuery } from '../src/useLiveQuery' +import { runSuite } from '../../db/tests/conformance/suite' +import type { RenderHookResult } from '@testing-library/react' +import type { + ConformanceResult, + ControllableHandle, + DeferredSourceHandle, + LiveQueryDriver, + QueryBuild, + SourceHandle, +} from '../../db/tests/conformance/contract' + +let sourceSeq = 0 + +function writer(collection: any) { + return (type: `insert` | `update` | `delete`, value: T) => { + collection.utils.begin() + collection.utils.write({ type, value }) + collection.utils.commit() + } +} + +function makeSource( + initialData: ReadonlyArray, +): SourceHandle { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `conformance-react-${sourceSeq++}`, + getKey: (r) => r.id, + initialData: [...initialData], + }), + ) + const write = writer(collection) + return { + collection, + insert: (row) => write(`insert`, row), + update: (row) => write(`update`, row), + remove: (row) => write(`delete`, row), + } +} + +function makeDeferredSource< + T extends { id: string }, +>(): DeferredSourceHandle { + const collection = createCollection( + mockSyncCollectionOptionsNoInitialState({ + id: `conformance-react-${sourceSeq++}`, + getKey: (r) => r.id, + }), + ) + // Start sync so the sync fn binds utils and the collection sits in `loading` + // (NoInitialState never calls markReady on its own). + collection.startSyncImmediate() + const write = writer(collection) + return { + collection, + insert: (row) => write(`insert`, row), + update: (row) => write(`update`, row), + remove: (row) => write(`delete`, row), + emit: (rows) => { + collection.utils.begin() + rows.forEach((value) => collection.utils.write({ type: `insert`, value })) + collection.utils.commit() + }, + markReady: () => collection.utils.markReady(), + } +} + +function makePrecreated(build: QueryBuild, opts?: { startSync?: boolean }) { + const collection = createLiveQueryCollection({ + query: build as any, + startSync: opts?.startSync ?? true, + }) + return { collection } +} + +function makeErrorSource() { + const collection = createCollection<{ id: string }>({ + id: `conformance-react-err-${sourceSeq++}`, + getKey: (r) => r.id, + startSync: false, + sync: { + sync: () => { + throw new Error(`conformance: sync failure`) + }, + }, + }) + // Starting sync throws → engine catches and sets status to `error`. + try { + collection.startSyncImmediate() + } catch { + // expected: the rethrown sync error; status is already `error` + } + return { collection } +} + +function mount(build: QueryBuild) { + const hook = renderHook(() => useLiveQuery(build as any)) + return makeHandle(hook) +} + +function mountCollection(collection: any) { + const hook = renderHook(() => useLiveQuery(collection)) + return makeHandle(hook) +} + +function mountConfig(build: QueryBuild) { + const hook = renderHook(() => useLiveQuery({ query: build as any })) + return makeHandle(hook) +} + +function mountDisabled() { + // React's disabled convention: the query callback returns null. + const hook = renderHook(() => useLiveQuery(() => null as any)) + return makeHandle(hook) +} + +function mountControllable

( + build: (q: any, param: P) => any, + initial: P, +): ControllableHandle

{ + const hook = renderHook( + ({ param }: { param: P }) => + // Param goes in the dependency list so the hook recompiles when it changes. + useLiveQuery((q: any) => build(q, param), [param]), + { initialProps: { param: initial } }, + ) + const handle = makeHandle(hook) + return { + ...handle, + async setParam(param: P) { + await act(async () => { + hook.rerender({ param }) + }) + await handle.flush() + }, + } +} + +function makeHandle(hook: RenderHookResult) { + return { + current(): ConformanceResult { + const r: any = hook.result.current + return { + data: r?.data, + status: r?.status ?? `idle`, + isReady: Boolean(r?.isReady), + isError: Boolean(r?.isError), + // Read react-db's real `isEnabled` field so the suite catches a broken + // one (deriving from status would mask it). + isEnabled: Boolean(r?.isEnabled), + } + }, + async flush() { + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)) + }) + }, + async apply(fn: () => void) { + await act(async () => { + fn() + await new Promise((resolve) => setTimeout(resolve, 0)) + }) + }, + unmount() { + hook.unmount() + }, + } +} + +const reactDriver: LiveQueryDriver = { + name: `react`, + ops: { eq, gt, count, sum, coalesce, createOptimisticAction }, + makeSource, + makeDeferredSource, + makePrecreated, + makeErrorSource, + mount, + mountControllable, + mountCollection, + mountConfig, + mountDisabled, + knownGaps: [], + features: { serverSnapshot: true, suspense: true }, +} + +runSuite(reactDriver) diff --git a/packages/react-db/tests/useLiveQuery.eager-onstorechange.test.tsx b/packages/react-db/tests/useLiveQuery.eager-onstorechange.test.tsx new file mode 100644 index 0000000000..207f93156f --- /dev/null +++ b/packages/react-db/tests/useLiveQuery.eager-onstorechange.test.tsx @@ -0,0 +1,66 @@ +import { describe, expect, it, vi } from 'vitest' + +import { renderHook } from '@testing-library/react' +import { createCollection, createLiveQueryCollection } from '@tanstack/db' +import { useLiveQuery } from '../src/useLiveQuery' +import { mockSyncCollectionOptions } from '../../db/tests/utils' +import type * as ReactNS from 'react' + +// Intercept React.useSyncExternalStore so we can capture the `subscribe` +// callback that `useLiveQuery` registers and assert that it does not invoke +// `onStoreChange` synchronously when the collection is already ready. +let capturedSubscribe: ((cb: () => void) => () => void) | null = null + +vi.mock('react', async () => { + const actual = await vi.importActual('react') + return { + ...actual, + default: (actual as any).default ?? actual, + useSyncExternalStore: (subscribe: any, getSnapshot: any) => { + capturedSubscribe = subscribe + return getSnapshot() + }, + } +}) + +type Person = { id: string; name: string; age: number } + +const initialPersons: Array = [ + { id: `1`, name: `A`, age: 10 }, + { id: `2`, name: `B`, age: 20 }, +] + +describe(`useLiveQuery: eager onStoreChange must not fire synchronously during subscribe`, () => { + it(`defers the initial ready-state onStoreChange to a microtask`, async () => { + const base = createCollection( + mockSyncCollectionOptions({ + id: `eager-onstorechange-persons`, + getKey: (p) => p.id, + initialData: initialPersons, + }), + ) + + const lqc = createLiveQueryCollection({ + startSync: true, + query: (q) => q.from({ persons: base }), + }) + await lqc.preload() + expect(lqc.status).toBe(`ready`) + + capturedSubscribe = null + renderHook(() => useLiveQuery(lqc)) + expect(capturedSubscribe).toBeTypeOf(`function`) + + const onStoreChange = vi.fn() + const unsub = capturedSubscribe!(onStoreChange) + + // onStoreChange must not be invoked synchronously inside subscribe; + // it should be deferred to a microtask so it lands after React commits. + expect(onStoreChange).not.toHaveBeenCalled() + + await Promise.resolve() + expect(onStoreChange).toHaveBeenCalledTimes(1) + + unsub() + }) +}) diff --git a/packages/react-native-db-sqlite-persistence/CHANGELOG.md b/packages/react-native-db-sqlite-persistence/CHANGELOG.md index 4f72cb892d..d6f2259fe9 100644 --- a/packages/react-native-db-sqlite-persistence/CHANGELOG.md +++ b/packages/react-native-db-sqlite-persistence/CHANGELOG.md @@ -1,5 +1,61 @@ # @tanstack/react-native-db-sqlite-persistence +## 0.2.8 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.8 + +## 0.2.7 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.7 + +## 0.2.6 + +### Patch Changes + +- Updated dependencies [[`f7da776`](https://github.com/TanStack/db/commit/f7da77660b16cbfe30817fb5c938267d696c8d1c)]: + - @tanstack/db-sqlite-persistence-core@0.2.6 + +## 0.2.5 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.5 + +## 0.2.4 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.4 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.3 + +## 0.2.2 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.2 + +## 0.2.1 + +### Patch Changes + +- Updated dependencies [[`00389a4`](https://github.com/TanStack/db/commit/00389a47b258ad58fc3a03c5cc6f66957b9bd2d1)]: + - @tanstack/db-sqlite-persistence-core@0.2.1 + ## 0.2.0 ### Minor Changes diff --git a/packages/react-native-db-sqlite-persistence/package.json b/packages/react-native-db-sqlite-persistence/package.json index 36fb14352c..e73a9a128d 100644 --- a/packages/react-native-db-sqlite-persistence/package.json +++ b/packages/react-native-db-sqlite-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/react-native-db-sqlite-persistence", - "version": "0.2.0", + "version": "0.2.8", "description": "React Native and Expo SQLite persisted collection adapter for TanStack DB", "author": "TanStack Team", "license": "MIT", diff --git a/packages/rxdb-db-collection/CHANGELOG.md b/packages/rxdb-db-collection/CHANGELOG.md index 2e6f39e24f..5080f21b69 100644 --- a/packages/rxdb-db-collection/CHANGELOG.md +++ b/packages/rxdb-db-collection/CHANGELOG.md @@ -1,5 +1,61 @@ # @tanstack/rxdb-db-collection +## 0.1.82 + +### Patch Changes + +- Updated dependencies [[`8258d09`](https://github.com/TanStack/db/commit/8258d0955ab47c8510bd49ea59bcdbefd2ae054d), [`286964d`](https://github.com/TanStack/db/commit/286964d72612b59e3e427baabd9870f5a71a4281)]: + - @tanstack/db@0.6.16 + +## 0.1.81 + +### Patch Changes + +- Updated dependencies [[`eabcea7`](https://github.com/TanStack/db/commit/eabcea743fdfa045a2db01e12bef87403613102a), [`6d4c096`](https://github.com/TanStack/db/commit/6d4c096395b7ff3f428122ea8842bbead551a8c9)]: + - @tanstack/db@0.6.15 + +## 0.1.80 + +### Patch Changes + +- Updated dependencies [[`397e12a`](https://github.com/TanStack/db/commit/397e12a1224ad563e20a331eebcbe904cd4af948)]: + - @tanstack/db@0.6.14 + +## 0.1.79 + +### Patch Changes + +- Updated dependencies [[`99e9afe`](https://github.com/TanStack/db/commit/99e9afed46ab4083d66609a3e37ee44103c2177f), [`816b667`](https://github.com/TanStack/db/commit/816b6671c2cc9806715f6e6ed4410b3f4efb5afb)]: + - @tanstack/db@0.6.13 + +## 0.1.78 + +### Patch Changes + +- Updated dependencies [[`2b27dd1`](https://github.com/TanStack/db/commit/2b27dd1448da71c78a48e2390cb71b0ada1b1488)]: + - @tanstack/db@0.6.12 + +## 0.1.77 + +### Patch Changes + +- Updated dependencies [[`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`36fb29a`](https://github.com/TanStack/db/commit/36fb29ad7e906d39b6afdba2fd31e369c601bbb0), [`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`ac09b11`](https://github.com/TanStack/db/commit/ac09b1177a100eafa85cba3cd09dd1f53f933ded)]: + - @tanstack/db@0.6.11 + +## 0.1.76 + +### Patch Changes + +- Updated dependencies [[`307fdf8`](https://github.com/TanStack/db/commit/307fdf80f522a39a50e316316b3b75ba27fd5e84)]: + - @tanstack/db@0.6.10 + +## 0.1.75 + +### Patch Changes + +- Updated dependencies [[`2147345`](https://github.com/TanStack/db/commit/2147345236ceee6e73d9fc6c0cdc2385833199fc), [`00389a4`](https://github.com/TanStack/db/commit/00389a47b258ad58fc3a03c5cc6f66957b9bd2d1)]: + - @tanstack/db@0.6.9 + ## 0.1.74 ### Patch Changes diff --git a/packages/rxdb-db-collection/package.json b/packages/rxdb-db-collection/package.json index edd6a3474b..ff8904e2ce 100644 --- a/packages/rxdb-db-collection/package.json +++ b/packages/rxdb-db-collection/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/rxdb-db-collection", - "version": "0.1.74", + "version": "0.1.82", "description": "Reactive, Offline-First adapter for TanStack DB using RxDB. Sync, Replication and Local-First support.", "author": "Kyle Mathews", "license": "MIT", diff --git a/packages/solid-db/CHANGELOG.md b/packages/solid-db/CHANGELOG.md index 28f10b3eff..ec19fb8fcc 100644 --- a/packages/solid-db/CHANGELOG.md +++ b/packages/solid-db/CHANGELOG.md @@ -1,5 +1,65 @@ # @tanstack/react-db +## 0.2.30 + +### Patch Changes + +- Updated dependencies [[`8258d09`](https://github.com/TanStack/db/commit/8258d0955ab47c8510bd49ea59bcdbefd2ae054d), [`286964d`](https://github.com/TanStack/db/commit/286964d72612b59e3e427baabd9870f5a71a4281)]: + - @tanstack/db@0.6.16 + +## 0.2.29 + +### Patch Changes + +- Extract shared live-query adapter helpers into `@tanstack/db` ([#1641](https://github.com/TanStack/db/pull/1641)) + + Adds `isCollection`, `isSingleResultCollection`, and `getLiveQueryStatusFlags` to `@tanstack/db` and migrates all five framework adapters to use them. `isCollection` replaces the per-adapter duck-typing and Solid's `instanceof CollectionImpl` with one structural, multi-realm-safe guard (the `instanceof` form gave false negatives across dual-package boundaries). No behavior change; internal deduplication only. + +- Updated dependencies [[`eabcea7`](https://github.com/TanStack/db/commit/eabcea743fdfa045a2db01e12bef87403613102a), [`6d4c096`](https://github.com/TanStack/db/commit/6d4c096395b7ff3f428122ea8842bbead551a8c9)]: + - @tanstack/db@0.6.15 + +## 0.2.28 + +### Patch Changes + +- Updated dependencies [[`397e12a`](https://github.com/TanStack/db/commit/397e12a1224ad563e20a331eebcbe904cd4af948)]: + - @tanstack/db@0.6.14 + +## 0.2.27 + +### Patch Changes + +- Updated dependencies [[`99e9afe`](https://github.com/TanStack/db/commit/99e9afed46ab4083d66609a3e37ee44103c2177f), [`816b667`](https://github.com/TanStack/db/commit/816b6671c2cc9806715f6e6ed4410b3f4efb5afb)]: + - @tanstack/db@0.6.13 + +## 0.2.26 + +### Patch Changes + +- Updated dependencies [[`2b27dd1`](https://github.com/TanStack/db/commit/2b27dd1448da71c78a48e2390cb71b0ada1b1488)]: + - @tanstack/db@0.6.12 + +## 0.2.25 + +### Patch Changes + +- Updated dependencies [[`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`36fb29a`](https://github.com/TanStack/db/commit/36fb29ad7e906d39b6afdba2fd31e369c601bbb0), [`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`ac09b11`](https://github.com/TanStack/db/commit/ac09b1177a100eafa85cba3cd09dd1f53f933ded)]: + - @tanstack/db@0.6.11 + +## 0.2.24 + +### Patch Changes + +- Updated dependencies [[`307fdf8`](https://github.com/TanStack/db/commit/307fdf80f522a39a50e316316b3b75ba27fd5e84)]: + - @tanstack/db@0.6.10 + +## 0.2.23 + +### Patch Changes + +- Updated dependencies [[`2147345`](https://github.com/TanStack/db/commit/2147345236ceee6e73d9fc6c0cdc2385833199fc), [`00389a4`](https://github.com/TanStack/db/commit/00389a47b258ad58fc3a03c5cc6f66957b9bd2d1)]: + - @tanstack/db@0.6.9 + ## 0.2.22 ### Patch Changes diff --git a/packages/solid-db/package.json b/packages/solid-db/package.json index 50debdc474..55a4ef472b 100644 --- a/packages/solid-db/package.json +++ b/packages/solid-db/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/solid-db", - "version": "0.2.22", + "version": "0.2.30", "description": "Solid integration for @tanstack/db", "author": "Kyle Mathews", "license": "MIT", diff --git a/packages/solid-db/src/useLiveQuery.ts b/packages/solid-db/src/useLiveQuery.ts index 5380bae00d..7759915f32 100644 --- a/packages/solid-db/src/useLiveQuery.ts +++ b/packages/solid-db/src/useLiveQuery.ts @@ -9,15 +9,15 @@ import { import { ReactiveMap } from '@solid-primitives/map' import { BaseQueryBuilder, - CollectionImpl, createLiveQueryCollection, + isCollection, + isSingleResultCollection, } from '@tanstack/db' import { createStore, reconcile } from 'solid-js/store' import type { Accessor } from 'solid-js' import type { ChangeMessage, Collection, - CollectionConfigSingleRowOption, CollectionStatus, Context, GetResult, @@ -311,7 +311,7 @@ export function useLiveQuery( return null } - if (innerCollection instanceof CollectionImpl) { + if (isCollection(innerCollection)) { innerCollection.startSyncImmediate() return innerCollection as Collection } @@ -426,9 +426,7 @@ export function useLiveQuery( function getData() { const currentCollection = collection() if (currentCollection) { - const config: CollectionConfigSingleRowOption = - currentCollection.config - if (config.singleResult) { + if (isSingleResultCollection(currentCollection)) { // Force resource tracking so Suspense works getDataResource() return data[0] diff --git a/packages/solid-db/tests/conformance.test.tsx b/packages/solid-db/tests/conformance.test.tsx new file mode 100644 index 0000000000..2dcea6ab03 --- /dev/null +++ b/packages/solid-db/tests/conformance.test.tsx @@ -0,0 +1,226 @@ +/** + * Solid driver for the shared live-query conformance suite. + * + * Each mount runs inside a `createRoot` so `unmount` disposes via the captured + * dispose fn; the root stays alive between mount and reads so Solid's reactive + * getters stay current. Solid auto-tracks signals, so controllable inputs use a + * signal read inside the query fn (no deps array). Collection/config inputs are + * passed as accessors, per Solid's arity-based input detection. + * + * `knownGaps` is populated empirically from the run below. + */ +import { + coalesce, + count, + createCollection, + createLiveQueryCollection, + createOptimisticAction, + eq, + gt, + sum, +} from '@tanstack/db' +import { createRoot, createSignal } from 'solid-js' +import { + mockSyncCollectionOptions, + mockSyncCollectionOptionsNoInitialState, +} from '../../db/tests/utils' +import { useLiveQuery } from '../src/useLiveQuery' +import { runSuite } from '../../db/tests/conformance/suite' +import type { + ConformanceResult, + ControllableHandle, + DeferredSourceHandle, + LiveQueryDriver, + LiveQueryHandle, + QueryBuild, + SourceHandle, +} from '../../db/tests/conformance/contract' + +let sourceSeq = 0 + +function writer(collection: any) { + return (type: `insert` | `update` | `delete`, value: T) => { + collection.utils.begin() + collection.utils.write({ type, value }) + collection.utils.commit() + } +} + +function makeSource( + initialData: ReadonlyArray, +): SourceHandle { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `conformance-solid-${sourceSeq++}`, + getKey: (r) => r.id, + initialData: [...initialData], + }), + ) + const write = writer(collection) + return { + collection, + insert: (row) => write(`insert`, row), + update: (row) => write(`update`, row), + remove: (row) => write(`delete`, row), + } +} + +function makeDeferredSource< + T extends { id: string }, +>(): DeferredSourceHandle { + const collection = createCollection( + mockSyncCollectionOptionsNoInitialState({ + id: `conformance-solid-${sourceSeq++}`, + getKey: (r) => r.id, + }), + ) + collection.startSyncImmediate() + const write = writer(collection) + return { + collection, + insert: (row) => write(`insert`, row), + update: (row) => write(`update`, row), + remove: (row) => write(`delete`, row), + emit: (rows) => { + collection.utils.begin() + rows.forEach((value) => collection.utils.write({ type: `insert`, value })) + collection.utils.commit() + }, + markReady: () => collection.utils.markReady(), + } +} + +function makePrecreated(build: QueryBuild, opts?: { startSync?: boolean }) { + const collection = createLiveQueryCollection({ + query: build as any, + startSync: opts?.startSync ?? true, + }) + return { collection } +} + +function makeErrorSource() { + const collection = createCollection<{ id: string }>({ + id: `conformance-solid-err-${sourceSeq++}`, + getKey: (r) => r.id, + startSync: false, + sync: { + sync: () => { + throw new Error(`conformance: sync failure`) + }, + }, + }) + try { + collection.startSyncImmediate() + } catch { + // expected: engine catches the sync error and sets status to `error` + } + return { collection } +} + +async function settle() { + await new Promise((resolve) => setTimeout(resolve, 10)) +} + +function makeHandle( + getResult: () => any, + dispose: () => void, +): LiveQueryHandle { + return { + current(): ConformanceResult { + const result = getResult() + return { + data: result?.data, + status: result?.status ?? `idle`, + isReady: Boolean(result?.isReady), + isError: Boolean(result?.isError), + // solid-db exposes no `isEnabled`; derive it from status (status-derived). + isEnabled: result?.status !== `disabled`, + } + }, + flush: settle, + async apply(fn: () => void) { + fn() + await settle() + }, + unmount() { + dispose() + }, + } +} + +function inRoot(fn: () => any): { getResult: () => any; dispose: () => void } { + let result: any + let dispose!: () => void + createRoot((d) => { + dispose = d + result = fn() + }) + return { getResult: () => result, dispose } +} + +function mount(build: QueryBuild) { + const { getResult, dispose } = inRoot(() => useLiveQuery(build as any)) + return makeHandle(getResult, dispose) +} + +function mountCollection(collection: any) { + // Solid accepts a pre-created collection via an accessor. + const { getResult, dispose } = inRoot(() => useLiveQuery(() => collection)) + return makeHandle(getResult, dispose) +} + +function mountConfig(build: QueryBuild) { + // Solid accepts the config-object form via an accessor. + const { getResult, dispose } = inRoot(() => + useLiveQuery(() => ({ query: build })), + ) + return makeHandle(getResult, dispose) +} + +function mountDisabled() { + // Disabled: an accessor returning null. + const { getResult, dispose } = inRoot(() => useLiveQuery(() => null)) + return makeHandle(getResult, dispose) +} + +function mountControllable

( + build: (q: any, param: P) => any, + initial: P, +): ControllableHandle

{ + const [param, setParam] = createSignal

(initial) + const { getResult, dispose } = inRoot(() => + // Reading param() inside the query fn makes Solid recompute on change. + useLiveQuery((q: any) => build(q, param())), + ) + const handle = makeHandle(getResult, dispose) + return { + ...handle, + async setParam(next: P) { + setParam(() => next) + await settle() + }, + } +} + +const solidDriver: LiveQueryDriver = { + name: `solid`, + ops: { eq, gt, count, sum, coalesce, createOptimisticAction }, + makeSource, + makeDeferredSource, + makePrecreated, + makeErrorSource, + mount, + mountControllable, + mountCollection, + mountConfig, + mountDisabled, + // solid-db routes errors through its createResource/Suspense path: reading an + // errored query throws (CollectionStateError) for an to catch, + // rather than exposing a readable isError flag. That's a framework idiom, not a + // gap — the error-status scenario is parametrized to assert it via the boundary. + errorSurface: `throw`, + knownGaps: [], + features: { serverSnapshot: false, suspense: true }, +} + +runSuite(solidDriver) diff --git a/packages/svelte-db/CHANGELOG.md b/packages/svelte-db/CHANGELOG.md index a740729e17..9efd9d8ff1 100644 --- a/packages/svelte-db/CHANGELOG.md +++ b/packages/svelte-db/CHANGELOG.md @@ -1,5 +1,69 @@ # @tanstack/svelte-db +## 0.1.93 + +### Patch Changes + +- Updated dependencies [[`8258d09`](https://github.com/TanStack/db/commit/8258d0955ab47c8510bd49ea59bcdbefd2ae054d), [`286964d`](https://github.com/TanStack/db/commit/286964d72612b59e3e427baabd9870f5a71a4281)]: + - @tanstack/db@0.6.16 + +## 0.1.92 + +### Patch Changes + +- Extract shared live-query adapter helpers into `@tanstack/db` ([#1641](https://github.com/TanStack/db/pull/1641)) + + Adds `isCollection`, `isSingleResultCollection`, and `getLiveQueryStatusFlags` to `@tanstack/db` and migrates all five framework adapters to use them. `isCollection` replaces the per-adapter duck-typing and Solid's `instanceof CollectionImpl` with one structural, multi-realm-safe guard (the `instanceof` form gave false negatives across dual-package boundaries). No behavior change; internal deduplication only. + +- fix(svelte-db): a disabled `useLiveQuery` (query callback returning `null`/`undefined`) no longer crashes ([#1637](https://github.com/TanStack/db/pull/1637)) + + The reactive-getter unwrapping (`toValue`) called the query callback and, when it returned `null`/`undefined` to signal a disabled query, passed the unwrapped `null` into `createLiveQueryCollection`, throwing in `getQueryIR`. A `null`/`undefined` resolved value is now treated as a disabled query (returns the `disabled` state) as it is in the other adapters. + +- Updated dependencies [[`eabcea7`](https://github.com/TanStack/db/commit/eabcea743fdfa045a2db01e12bef87403613102a), [`6d4c096`](https://github.com/TanStack/db/commit/6d4c096395b7ff3f428122ea8842bbead551a8c9)]: + - @tanstack/db@0.6.15 + +## 0.1.91 + +### Patch Changes + +- Updated dependencies [[`397e12a`](https://github.com/TanStack/db/commit/397e12a1224ad563e20a331eebcbe904cd4af948)]: + - @tanstack/db@0.6.14 + +## 0.1.90 + +### Patch Changes + +- Updated dependencies [[`99e9afe`](https://github.com/TanStack/db/commit/99e9afed46ab4083d66609a3e37ee44103c2177f), [`816b667`](https://github.com/TanStack/db/commit/816b6671c2cc9806715f6e6ed4410b3f4efb5afb)]: + - @tanstack/db@0.6.13 + +## 0.1.89 + +### Patch Changes + +- Updated dependencies [[`2b27dd1`](https://github.com/TanStack/db/commit/2b27dd1448da71c78a48e2390cb71b0ada1b1488)]: + - @tanstack/db@0.6.12 + +## 0.1.88 + +### Patch Changes + +- Updated dependencies [[`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`36fb29a`](https://github.com/TanStack/db/commit/36fb29ad7e906d39b6afdba2fd31e369c601bbb0), [`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`ac09b11`](https://github.com/TanStack/db/commit/ac09b1177a100eafa85cba3cd09dd1f53f933ded)]: + - @tanstack/db@0.6.11 + +## 0.1.87 + +### Patch Changes + +- Updated dependencies [[`307fdf8`](https://github.com/TanStack/db/commit/307fdf80f522a39a50e316316b3b75ba27fd5e84)]: + - @tanstack/db@0.6.10 + +## 0.1.86 + +### Patch Changes + +- Updated dependencies [[`2147345`](https://github.com/TanStack/db/commit/2147345236ceee6e73d9fc6c0cdc2385833199fc), [`00389a4`](https://github.com/TanStack/db/commit/00389a47b258ad58fc3a03c5cc6f66957b9bd2d1)]: + - @tanstack/db@0.6.9 + ## 0.1.85 ### Patch Changes diff --git a/packages/svelte-db/package.json b/packages/svelte-db/package.json index 79cfab6a16..577b794daa 100644 --- a/packages/svelte-db/package.json +++ b/packages/svelte-db/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/svelte-db", - "version": "0.1.85", + "version": "0.1.93", "description": "Svelte integration for @tanstack/db", "author": "Kyle Mathews", "license": "MIT", diff --git a/packages/svelte-db/src/useLiveQuery.svelte.ts b/packages/svelte-db/src/useLiveQuery.svelte.ts index 76dedd3c78..0d9dd9d7fe 100644 --- a/packages/svelte-db/src/useLiveQuery.svelte.ts +++ b/packages/svelte-db/src/useLiveQuery.svelte.ts @@ -2,11 +2,15 @@ import { untrack } from 'svelte' // eslint-disable-next-line import/no-duplicates -- See https://github.com/un-ts/eslint-plugin-import-x/issues/308 import { SvelteMap } from 'svelte/reactivity' -import { BaseQueryBuilder, createLiveQueryCollection } from '@tanstack/db' +import { + BaseQueryBuilder, + createLiveQueryCollection, + isCollection, + isSingleResultCollection, +} from '@tanstack/db' import type { ChangeMessage, Collection, - CollectionConfigSingleRowOption, CollectionStatus, Context, GetResult, @@ -301,14 +305,9 @@ export function useLiveQuery( } // Check if it's already a collection by checking for specific collection methods - const isCollection = - unwrappedParam && - typeof unwrappedParam === `object` && - typeof unwrappedParam.subscribeChanges === `function` && - typeof unwrappedParam.startSyncImmediate === `function` && - typeof unwrappedParam.id === `string` - - if (isCollection) { + const inputIsCollection = isCollection(unwrappedParam) + + if (inputIsCollection) { // Warn when passing a collection directly with on-demand sync mode // In on-demand mode, data is only loaded when queries with predicates request it // Passing the collection directly doesn't provide any predicates, so no data loads @@ -350,6 +349,13 @@ export function useLiveQuery( startSync: true, }) } else { + // A reactive getter (or param-driven query fn) can resolve to null/undefined + // to mean "disabled". `toValue` above already called it, so guard here — + // otherwise `{ ...null }` reaches createLiveQueryCollection and throws. + if (unwrappedParam === undefined || unwrappedParam === null) { + return null + } + return createLiveQueryCollection({ ...unwrappedParam, startSync: true, @@ -474,16 +480,8 @@ export function useLiveQuery( }, get data() { const currentCollection = collection - if (currentCollection) { - const config = - currentCollection.config as CollectionConfigSingleRowOption< - any, - any, - any - > - if (config.singleResult) { - return internalData[0] - } + if (currentCollection && isSingleResultCollection(currentCollection)) { + return internalData[0] } return internalData }, diff --git a/packages/svelte-db/tests/conformance.svelte.test.ts b/packages/svelte-db/tests/conformance.svelte.test.ts new file mode 100644 index 0000000000..5b502b02c7 --- /dev/null +++ b/packages/svelte-db/tests/conformance.svelte.test.ts @@ -0,0 +1,217 @@ +/** + * Svelte driver for the shared live-query conformance suite. + * + * Svelte 5 runes: each mount runs inside a persistent `$effect.root` so the + * internal `$effect` keeps updating rune state after mount; `unmount` disposes + * the root. Reads happen after `flushSync()`. Realm-sensitive pieces come from + * Svelte's `@tanstack/db`. + * + * `knownGaps` is populated empirically from the run below. + */ +import { + coalesce, + count, + createCollection, + createLiveQueryCollection, + createOptimisticAction, + eq, + gt, + sum, +} from '@tanstack/db' +import { flushSync } from 'svelte' +import { + mockSyncCollectionOptions, + mockSyncCollectionOptionsNoInitialState, +} from '../../db/tests/utils' +import { useLiveQuery } from '../src/useLiveQuery.svelte.js' +import { runSuite } from '../../db/tests/conformance/suite' +import type { + ConformanceResult, + ControllableHandle, + DeferredSourceHandle, + LiveQueryDriver, + LiveQueryHandle, + QueryBuild, + SourceHandle, +} from '../../db/tests/conformance/contract' + +let sourceSeq = 0 + +function writer(collection: any) { + return (type: `insert` | `update` | `delete`, value: T) => { + collection.utils.begin() + collection.utils.write({ type, value }) + collection.utils.commit() + } +} + +function makeSource( + initialData: ReadonlyArray, +): SourceHandle { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `conformance-svelte-${sourceSeq++}`, + getKey: (r) => r.id, + initialData: [...initialData], + }), + ) + const write = writer(collection) + return { + collection, + insert: (row) => write(`insert`, row), + update: (row) => write(`update`, row), + remove: (row) => write(`delete`, row), + } +} + +function makeDeferredSource< + T extends { id: string }, +>(): DeferredSourceHandle { + const collection = createCollection( + mockSyncCollectionOptionsNoInitialState({ + id: `conformance-svelte-${sourceSeq++}`, + getKey: (r) => r.id, + }), + ) + collection.startSyncImmediate() + const write = writer(collection) + return { + collection, + insert: (row) => write(`insert`, row), + update: (row) => write(`update`, row), + remove: (row) => write(`delete`, row), + emit: (rows) => { + collection.utils.begin() + rows.forEach((value) => collection.utils.write({ type: `insert`, value })) + collection.utils.commit() + }, + markReady: () => collection.utils.markReady(), + } +} + +function makePrecreated(build: QueryBuild, opts?: { startSync?: boolean }) { + const collection = createLiveQueryCollection({ + query: build as any, + startSync: opts?.startSync ?? true, + }) + return { collection } +} + +function makeErrorSource() { + const collection = createCollection<{ id: string }>({ + id: `conformance-svelte-err-${sourceSeq++}`, + getKey: (r) => r.id, + startSync: false, + sync: { + sync: () => { + throw new Error(`conformance: sync failure`) + }, + }, + }) + try { + collection.startSyncImmediate() + } catch { + // expected: engine catches the sync error and sets status to `error` + } + return { collection } +} + +async function settle() { + flushSync() + await new Promise((resolve) => setTimeout(resolve, 10)) + flushSync() +} + +function makeHandle(getQuery: () => any, dispose: () => void): LiveQueryHandle { + return { + current(): ConformanceResult { + const query = getQuery() + return { + data: query?.data, + status: query?.status ?? `idle`, + isReady: Boolean(query?.isReady), + isError: Boolean(query?.isError), + // svelte-db exposes no `isEnabled`; derive it from status (status-derived). + isEnabled: query?.status !== `disabled`, + } + }, + flush: settle, + async apply(fn: () => void) { + fn() + await settle() + }, + unmount() { + dispose() + }, + } +} + +function mount(build: QueryBuild) { + let query: any + const dispose = $effect.root(() => { + query = useLiveQuery(build as any) + }) + return makeHandle(() => query, dispose) +} + +function mountCollection(collection: any) { + let query: any + const dispose = $effect.root(() => { + query = useLiveQuery(collection) + }) + return makeHandle(() => query, dispose) +} + +function mountConfig(build: QueryBuild) { + let query: any + const dispose = $effect.root(() => { + query = useLiveQuery({ query: build } as any) + }) + return makeHandle(() => query, dispose) +} + +function mountDisabled() { + // Svelte's disabled convention: the query callback returns null. + let query: any + const dispose = $effect.root(() => { + query = useLiveQuery(() => null as any) + }) + return makeHandle(() => query, dispose) +} + +function mountControllable

( + build: (q: any, param: P) => any, + initial: P, +): ControllableHandle

{ + let param = $state(initial) + let query: any + const dispose = $effect.root(() => { + query = useLiveQuery((q: any) => build(q, param), [() => param]) + }) + const handle = makeHandle(() => query, dispose) + return { + ...handle, + async setParam(next: P) { + param = next + await settle() + }, + } +} + +const svelteDriver: LiveQueryDriver = { + name: `svelte`, + ops: { eq, gt, count, sum, coalesce, createOptimisticAction }, + makeSource, + makeDeferredSource, + makePrecreated, + makeErrorSource, + mount, + mountControllable, + mountCollection, + mountConfig, + mountDisabled, + knownGaps: [], + features: { serverSnapshot: false, suspense: false }, +} + +runSuite(svelteDriver) diff --git a/packages/tauri-db-sqlite-persistence/CHANGELOG.md b/packages/tauri-db-sqlite-persistence/CHANGELOG.md index 25f620e40b..df41cc8def 100644 --- a/packages/tauri-db-sqlite-persistence/CHANGELOG.md +++ b/packages/tauri-db-sqlite-persistence/CHANGELOG.md @@ -1,5 +1,61 @@ # @tanstack/tauri-db-sqlite-persistence +## 0.2.8 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.8 + +## 0.2.7 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.7 + +## 0.2.6 + +### Patch Changes + +- Updated dependencies [[`f7da776`](https://github.com/TanStack/db/commit/f7da77660b16cbfe30817fb5c938267d696c8d1c)]: + - @tanstack/db-sqlite-persistence-core@0.2.6 + +## 0.2.5 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.5 + +## 0.2.4 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.4 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.3 + +## 0.2.2 + +### Patch Changes + +- Updated dependencies []: + - @tanstack/db-sqlite-persistence-core@0.2.2 + +## 0.2.1 + +### Patch Changes + +- Updated dependencies [[`00389a4`](https://github.com/TanStack/db/commit/00389a47b258ad58fc3a03c5cc6f66957b9bd2d1)]: + - @tanstack/db-sqlite-persistence-core@0.2.1 + ## 0.2.0 ### Minor Changes diff --git a/packages/tauri-db-sqlite-persistence/e2e/app/CHANGELOG.md b/packages/tauri-db-sqlite-persistence/e2e/app/CHANGELOG.md index 254ae4ded8..40aad5d512 100644 --- a/packages/tauri-db-sqlite-persistence/e2e/app/CHANGELOG.md +++ b/packages/tauri-db-sqlite-persistence/e2e/app/CHANGELOG.md @@ -1,5 +1,69 @@ # @tanstack/tauri-db-sqlite-persistence-e2e-app +## 0.0.20 + +### Patch Changes + +- Updated dependencies [[`8258d09`](https://github.com/TanStack/db/commit/8258d0955ab47c8510bd49ea59bcdbefd2ae054d), [`286964d`](https://github.com/TanStack/db/commit/286964d72612b59e3e427baabd9870f5a71a4281)]: + - @tanstack/db@0.6.16 + - @tanstack/tauri-db-sqlite-persistence@0.2.8 + +## 0.0.19 + +### Patch Changes + +- Updated dependencies [[`eabcea7`](https://github.com/TanStack/db/commit/eabcea743fdfa045a2db01e12bef87403613102a), [`6d4c096`](https://github.com/TanStack/db/commit/6d4c096395b7ff3f428122ea8842bbead551a8c9)]: + - @tanstack/db@0.6.15 + - @tanstack/tauri-db-sqlite-persistence@0.2.7 + +## 0.0.18 + +### Patch Changes + +- Updated dependencies [[`397e12a`](https://github.com/TanStack/db/commit/397e12a1224ad563e20a331eebcbe904cd4af948)]: + - @tanstack/db@0.6.14 + - @tanstack/tauri-db-sqlite-persistence@0.2.6 + +## 0.0.17 + +### Patch Changes + +- Updated dependencies [[`99e9afe`](https://github.com/TanStack/db/commit/99e9afed46ab4083d66609a3e37ee44103c2177f), [`816b667`](https://github.com/TanStack/db/commit/816b6671c2cc9806715f6e6ed4410b3f4efb5afb)]: + - @tanstack/db@0.6.13 + - @tanstack/tauri-db-sqlite-persistence@0.2.5 + +## 0.0.16 + +### Patch Changes + +- Updated dependencies [[`2b27dd1`](https://github.com/TanStack/db/commit/2b27dd1448da71c78a48e2390cb71b0ada1b1488)]: + - @tanstack/db@0.6.12 + - @tanstack/tauri-db-sqlite-persistence@0.2.4 + +## 0.0.15 + +### Patch Changes + +- Updated dependencies [[`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`36fb29a`](https://github.com/TanStack/db/commit/36fb29ad7e906d39b6afdba2fd31e369c601bbb0), [`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`ac09b11`](https://github.com/TanStack/db/commit/ac09b1177a100eafa85cba3cd09dd1f53f933ded)]: + - @tanstack/db@0.6.11 + - @tanstack/tauri-db-sqlite-persistence@0.2.3 + +## 0.0.14 + +### Patch Changes + +- Updated dependencies [[`307fdf8`](https://github.com/TanStack/db/commit/307fdf80f522a39a50e316316b3b75ba27fd5e84)]: + - @tanstack/db@0.6.10 + - @tanstack/tauri-db-sqlite-persistence@0.2.2 + +## 0.0.13 + +### Patch Changes + +- Updated dependencies [[`2147345`](https://github.com/TanStack/db/commit/2147345236ceee6e73d9fc6c0cdc2385833199fc), [`00389a4`](https://github.com/TanStack/db/commit/00389a47b258ad58fc3a03c5cc6f66957b9bd2d1)]: + - @tanstack/db@0.6.9 + - @tanstack/tauri-db-sqlite-persistence@0.2.1 + ## 0.0.12 ### Patch Changes diff --git a/packages/tauri-db-sqlite-persistence/e2e/app/package.json b/packages/tauri-db-sqlite-persistence/e2e/app/package.json index 795d1406cd..8bb9140d75 100644 --- a/packages/tauri-db-sqlite-persistence/e2e/app/package.json +++ b/packages/tauri-db-sqlite-persistence/e2e/app/package.json @@ -1,7 +1,7 @@ { "name": "@tanstack/tauri-db-sqlite-persistence-e2e-app", "private": true, - "version": "0.0.12", + "version": "0.0.20", "type": "module", "scripts": { "build": "vite build", diff --git a/packages/tauri-db-sqlite-persistence/package.json b/packages/tauri-db-sqlite-persistence/package.json index 796c62efa3..9fe14ff63d 100644 --- a/packages/tauri-db-sqlite-persistence/package.json +++ b/packages/tauri-db-sqlite-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/tauri-db-sqlite-persistence", - "version": "0.2.0", + "version": "0.2.8", "description": "Tauri SQLite persisted collection adapter for TanStack DB", "author": "TanStack Team", "license": "MIT", diff --git a/packages/trailbase-db-collection/CHANGELOG.md b/packages/trailbase-db-collection/CHANGELOG.md index da6baa3e67..e88f3a7b0f 100644 --- a/packages/trailbase-db-collection/CHANGELOG.md +++ b/packages/trailbase-db-collection/CHANGELOG.md @@ -1,5 +1,61 @@ # @tanstack/trailbase-db-collection +## 0.1.94 + +### Patch Changes + +- Updated dependencies [[`8258d09`](https://github.com/TanStack/db/commit/8258d0955ab47c8510bd49ea59bcdbefd2ae054d), [`286964d`](https://github.com/TanStack/db/commit/286964d72612b59e3e427baabd9870f5a71a4281)]: + - @tanstack/db@0.6.16 + +## 0.1.93 + +### Patch Changes + +- Updated dependencies [[`eabcea7`](https://github.com/TanStack/db/commit/eabcea743fdfa045a2db01e12bef87403613102a), [`6d4c096`](https://github.com/TanStack/db/commit/6d4c096395b7ff3f428122ea8842bbead551a8c9)]: + - @tanstack/db@0.6.15 + +## 0.1.92 + +### Patch Changes + +- Updated dependencies [[`397e12a`](https://github.com/TanStack/db/commit/397e12a1224ad563e20a331eebcbe904cd4af948)]: + - @tanstack/db@0.6.14 + +## 0.1.91 + +### Patch Changes + +- Updated dependencies [[`99e9afe`](https://github.com/TanStack/db/commit/99e9afed46ab4083d66609a3e37ee44103c2177f), [`816b667`](https://github.com/TanStack/db/commit/816b6671c2cc9806715f6e6ed4410b3f4efb5afb)]: + - @tanstack/db@0.6.13 + +## 0.1.90 + +### Patch Changes + +- Updated dependencies [[`2b27dd1`](https://github.com/TanStack/db/commit/2b27dd1448da71c78a48e2390cb71b0ada1b1488)]: + - @tanstack/db@0.6.12 + +## 0.1.89 + +### Patch Changes + +- Updated dependencies [[`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`36fb29a`](https://github.com/TanStack/db/commit/36fb29ad7e906d39b6afdba2fd31e369c601bbb0), [`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`ac09b11`](https://github.com/TanStack/db/commit/ac09b1177a100eafa85cba3cd09dd1f53f933ded)]: + - @tanstack/db@0.6.11 + +## 0.1.88 + +### Patch Changes + +- Updated dependencies [[`307fdf8`](https://github.com/TanStack/db/commit/307fdf80f522a39a50e316316b3b75ba27fd5e84)]: + - @tanstack/db@0.6.10 + +## 0.1.87 + +### Patch Changes + +- Updated dependencies [[`2147345`](https://github.com/TanStack/db/commit/2147345236ceee6e73d9fc6c0cdc2385833199fc), [`00389a4`](https://github.com/TanStack/db/commit/00389a47b258ad58fc3a03c5cc6f66957b9bd2d1)]: + - @tanstack/db@0.6.9 + ## 0.1.86 ### Patch Changes diff --git a/packages/trailbase-db-collection/package.json b/packages/trailbase-db-collection/package.json index 01ac435b6b..79f0e23456 100644 --- a/packages/trailbase-db-collection/package.json +++ b/packages/trailbase-db-collection/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/trailbase-db-collection", - "version": "0.1.86", + "version": "0.1.94", "description": "TrailBase collection for TanStack DB", "author": "Sebastian Jeltsch", "license": "MIT", diff --git a/packages/vue-db/CHANGELOG.md b/packages/vue-db/CHANGELOG.md index 7a4bebe4eb..db2efd02eb 100644 --- a/packages/vue-db/CHANGELOG.md +++ b/packages/vue-db/CHANGELOG.md @@ -1,5 +1,65 @@ # @tanstack/vue-db +## 0.0.127 + +### Patch Changes + +- Updated dependencies [[`8258d09`](https://github.com/TanStack/db/commit/8258d0955ab47c8510bd49ea59bcdbefd2ae054d), [`286964d`](https://github.com/TanStack/db/commit/286964d72612b59e3e427baabd9870f5a71a4281)]: + - @tanstack/db@0.6.16 + +## 0.0.126 + +### Patch Changes + +- Extract shared live-query adapter helpers into `@tanstack/db` ([#1641](https://github.com/TanStack/db/pull/1641)) + + Adds `isCollection`, `isSingleResultCollection`, and `getLiveQueryStatusFlags` to `@tanstack/db` and migrates all five framework adapters to use them. `isCollection` replaces the per-adapter duck-typing and Solid's `instanceof CollectionImpl` with one structural, multi-realm-safe guard (the `instanceof` form gave false negatives across dual-package boundaries). No behavior change; internal deduplication only. + +- Updated dependencies [[`eabcea7`](https://github.com/TanStack/db/commit/eabcea743fdfa045a2db01e12bef87403613102a), [`6d4c096`](https://github.com/TanStack/db/commit/6d4c096395b7ff3f428122ea8842bbead551a8c9)]: + - @tanstack/db@0.6.15 + +## 0.0.125 + +### Patch Changes + +- Updated dependencies [[`397e12a`](https://github.com/TanStack/db/commit/397e12a1224ad563e20a331eebcbe904cd4af948)]: + - @tanstack/db@0.6.14 + +## 0.0.124 + +### Patch Changes + +- Updated dependencies [[`99e9afe`](https://github.com/TanStack/db/commit/99e9afed46ab4083d66609a3e37ee44103c2177f), [`816b667`](https://github.com/TanStack/db/commit/816b6671c2cc9806715f6e6ed4410b3f4efb5afb)]: + - @tanstack/db@0.6.13 + +## 0.0.123 + +### Patch Changes + +- Updated dependencies [[`2b27dd1`](https://github.com/TanStack/db/commit/2b27dd1448da71c78a48e2390cb71b0ada1b1488)]: + - @tanstack/db@0.6.12 + +## 0.0.122 + +### Patch Changes + +- Updated dependencies [[`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`36fb29a`](https://github.com/TanStack/db/commit/36fb29ad7e906d39b6afdba2fd31e369c601bbb0), [`d79b0cd`](https://github.com/TanStack/db/commit/d79b0cd3fd20c1f7e2525e90121752fb6bee314c), [`ac09b11`](https://github.com/TanStack/db/commit/ac09b1177a100eafa85cba3cd09dd1f53f933ded)]: + - @tanstack/db@0.6.11 + +## 0.0.121 + +### Patch Changes + +- Updated dependencies [[`307fdf8`](https://github.com/TanStack/db/commit/307fdf80f522a39a50e316316b3b75ba27fd5e84)]: + - @tanstack/db@0.6.10 + +## 0.0.120 + +### Patch Changes + +- Updated dependencies [[`2147345`](https://github.com/TanStack/db/commit/2147345236ceee6e73d9fc6c0cdc2385833199fc), [`00389a4`](https://github.com/TanStack/db/commit/00389a47b258ad58fc3a03c5cc6f66957b9bd2d1)]: + - @tanstack/db@0.6.9 + ## 0.0.119 ### Patch Changes diff --git a/packages/vue-db/package.json b/packages/vue-db/package.json index f35759d05d..e0e9be53e1 100644 --- a/packages/vue-db/package.json +++ b/packages/vue-db/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/vue-db", - "version": "0.0.119", + "version": "0.0.127", "description": "Vue integration for @tanstack/db", "author": "Kyle Mathews", "license": "MIT", diff --git a/packages/vue-db/src/useLiveQuery.ts b/packages/vue-db/src/useLiveQuery.ts index 479c92b2b0..762cbda0a6 100644 --- a/packages/vue-db/src/useLiveQuery.ts +++ b/packages/vue-db/src/useLiveQuery.ts @@ -8,11 +8,14 @@ import { toValue, watchEffect, } from 'vue' -import { createLiveQueryCollection } from '@tanstack/db' +import { + createLiveQueryCollection, + isCollection, + isSingleResultCollection, +} from '@tanstack/db' import type { ChangeMessage, Collection, - CollectionConfigSingleRowOption, CollectionStatus, Context, GetResult, @@ -265,15 +268,10 @@ export function useLiveQuery( } } - // Check if it's already a collection by checking for specific collection methods - const isCollection = - unwrappedParam && - typeof unwrappedParam === `object` && - typeof unwrappedParam.subscribeChanges === `function` && - typeof unwrappedParam.startSyncImmediate === `function` && - typeof unwrappedParam.id === `string` + // Check if it's already a collection + const inputIsCollection = isCollection(unwrappedParam) - if (isCollection) { + if (inputIsCollection) { // Warn when passing a collection directly with on-demand sync mode // In on-demand mode, data is only loaded when queries with predicates request it // Passing the collection directly doesn't provide any predicates, so no data loads @@ -347,9 +345,9 @@ export function useLiveQuery( if (!currentCollection) { return internalData } - const config: CollectionConfigSingleRowOption = - currentCollection.config - return config.singleResult ? internalData[0] : internalData + return isSingleResultCollection(currentCollection) + ? internalData[0] + : internalData }) // Track collection status reactively diff --git a/packages/vue-db/tests/conformance.test.ts b/packages/vue-db/tests/conformance.test.ts new file mode 100644 index 0000000000..432876f4f5 --- /dev/null +++ b/packages/vue-db/tests/conformance.test.ts @@ -0,0 +1,219 @@ +/** + * Vue driver for the shared live-query conformance suite. + * + * Realm-sensitive pieces (collection factories, query operators) are imported + * from Vue's `@tanstack/db` and handed to the shared scenarios. Vue composables + * run inside an `effectScope` so `unmount` can dispose them via `scope.stop()`, + * which triggers the `watchEffect` `onInvalidate` cleanup. + * + * `knownGaps` is populated empirically from the run below. + */ +import { + coalesce, + count, + createCollection, + createLiveQueryCollection, + createOptimisticAction, + eq, + gt, + sum, +} from '@tanstack/db' +import { effectScope, nextTick, ref } from 'vue' +import { + mockSyncCollectionOptions, + mockSyncCollectionOptionsNoInitialState, +} from '../../db/tests/utils' +import { useLiveQuery } from '../src/useLiveQuery' +import { runSuite } from '../../db/tests/conformance/suite' +import type { + ConformanceResult, + ControllableHandle, + DeferredSourceHandle, + LiveQueryDriver, + LiveQueryHandle, + QueryBuild, + SourceHandle, +} from '../../db/tests/conformance/contract' + +let sourceSeq = 0 + +function writer(collection: any) { + return (type: `insert` | `update` | `delete`, value: T) => { + collection.utils.begin() + collection.utils.write({ type, value }) + collection.utils.commit() + } +} + +function makeSource( + initialData: ReadonlyArray, +): SourceHandle { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `conformance-vue-${sourceSeq++}`, + getKey: (r) => r.id, + initialData: [...initialData], + }), + ) + const write = writer(collection) + return { + collection, + insert: (row) => write(`insert`, row), + update: (row) => write(`update`, row), + remove: (row) => write(`delete`, row), + } +} + +function makeDeferredSource< + T extends { id: string }, +>(): DeferredSourceHandle { + const collection = createCollection( + mockSyncCollectionOptionsNoInitialState({ + id: `conformance-vue-${sourceSeq++}`, + getKey: (r) => r.id, + }), + ) + collection.startSyncImmediate() + const write = writer(collection) + return { + collection, + insert: (row) => write(`insert`, row), + update: (row) => write(`update`, row), + remove: (row) => write(`delete`, row), + emit: (rows) => { + collection.utils.begin() + rows.forEach((value) => collection.utils.write({ type: `insert`, value })) + collection.utils.commit() + }, + markReady: () => collection.utils.markReady(), + } +} + +function makePrecreated(build: QueryBuild, opts?: { startSync?: boolean }) { + const collection = createLiveQueryCollection({ + query: build as any, + startSync: opts?.startSync ?? true, + }) + return { collection } +} + +function makeErrorSource() { + const collection = createCollection<{ id: string }>({ + id: `conformance-vue-err-${sourceSeq++}`, + getKey: (r) => r.id, + startSync: false, + sync: { + sync: () => { + throw new Error(`conformance: sync failure`) + }, + }, + }) + try { + collection.startSyncImmediate() + } catch { + // expected: engine catches the sync error and sets status to `error` + } + return { collection } +} + +async function settle() { + await nextTick() + await new Promise((resolve) => setTimeout(resolve, 10)) +} + +function makeHandle(result: any, scope: ReturnType) { + const handle: LiveQueryHandle = { + current(): ConformanceResult { + return { + data: result.data?.value, + status: result.status?.value ?? `idle`, + isReady: Boolean(result.isReady?.value), + isError: Boolean(result.isError?.value), + // vue-db exposes no `isEnabled`; derive it from status (status-derived). + isEnabled: result.status?.value !== `disabled`, + } + }, + flush: settle, + async apply(fn: () => void) { + fn() + await settle() + }, + unmount() { + scope.stop() + }, + } + return handle +} + +function runInScope(fn: () => R): { + result: R + scope: ReturnType +} { + const scope = effectScope() + let result!: R + scope.run(() => { + result = fn() + }) + return { result, scope } +} + +function mount(build: QueryBuild) { + const { result, scope } = runInScope(() => useLiveQuery(build as any)) + return makeHandle(result, scope) +} + +function mountCollection(collection: any) { + const { result, scope } = runInScope(() => useLiveQuery(collection)) + return makeHandle(result, scope) +} + +function mountConfig(build: QueryBuild) { + const { result, scope } = runInScope(() => + useLiveQuery({ query: build } as any), + ) + return makeHandle(result, scope) +} + +function mountDisabled() { + // Vue's disabled convention: the query callback returns undefined. + const { result, scope } = runInScope(() => + useLiveQuery(() => undefined as any), + ) + return makeHandle(result, scope) +} + +function mountControllable

( + build: (q: any, param: P) => any, + initial: P, +): ControllableHandle

{ + const param = ref(initial) as { value: P } + const { result, scope } = runInScope(() => + useLiveQuery((q: any) => build(q, param.value), [() => param.value]), + ) + const handle = makeHandle(result, scope) + return { + ...handle, + async setParam(next: P) { + param.value = next + await settle() + }, + } +} + +const vueDriver: LiveQueryDriver = { + name: `vue`, + ops: { eq, gt, count, sum, coalesce, createOptimisticAction }, + makeSource, + makeDeferredSource, + makePrecreated, + makeErrorSource, + mount, + mountControllable, + mountCollection, + mountConfig, + mountDisabled, + knownGaps: [], + features: { serverSnapshot: false, suspense: false }, +} + +runSuite(vueDriver) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad174b46b5..ee9dc7fd91 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -148,10 +148,10 @@ importers: specifier: ^20.3.16 version: 20.3.16(@angular/common@20.3.16(@angular/core@20.3.17(@angular/compiler@20.3.16)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.16)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.16(@angular/common@20.3.16(@angular/core@20.3.17(@angular/compiler@20.3.16)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.17(@angular/compiler@20.3.16)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) '@tanstack/angular-db': - specifier: ^0.1.68 + specifier: ^0.1.76 version: link:../../../packages/angular-db '@tanstack/db': - specifier: ^0.6.8 + specifier: ^0.6.16 version: link:../../../packages/db rxjs: specifier: ^7.8.2 @@ -209,19 +209,19 @@ importers: examples/electron/offline-first: dependencies: '@tanstack/electron-db-sqlite-persistence': - specifier: ^0.1.12 + specifier: ^0.1.20 version: link:../../../packages/electron-db-sqlite-persistence '@tanstack/node-db-sqlite-persistence': - specifier: ^0.2.0 + specifier: ^0.2.8 version: link:../../../packages/node-db-sqlite-persistence '@tanstack/offline-transactions': - specifier: ^1.0.33 + specifier: ^1.0.41 version: link:../../../packages/offline-transactions '@tanstack/query-db-collection': - specifier: ^1.0.40 + specifier: ^1.1.0 version: link:../../../packages/query-db-collection '@tanstack/react-db': - specifier: ^0.1.86 + specifier: ^0.1.94 version: link:../../../packages/react-db '@tanstack/react-query': specifier: ^5.90.20 @@ -300,19 +300,19 @@ importers: specifier: 11.4.1 version: 11.4.1(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.0.0)) '@tanstack/db': - specifier: ^0.6.8 + specifier: ^0.6.16 version: link:../../../packages/db '@tanstack/offline-transactions': - specifier: ^1.0.33 + specifier: ^1.0.41 version: link:../../../packages/offline-transactions '@tanstack/query-db-collection': - specifier: ^1.0.40 + specifier: ^1.1.0 version: link:../../../packages/query-db-collection '@tanstack/react-db': - specifier: ^0.1.86 + specifier: ^0.1.94 version: link:../../../packages/react-db '@tanstack/react-native-db-sqlite-persistence': - specifier: ^0.2.0 + specifier: ^0.2.8 version: link:../../../packages/react-native-db-sqlite-persistence '@tanstack/react-query': specifier: ^5.90.20 @@ -397,19 +397,19 @@ importers: specifier: 11.4.1 version: 11.4.1(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.2.13)(react@19.0.0)) '@tanstack/db': - specifier: ^0.6.8 + specifier: ^0.6.16 version: link:../../../packages/db '@tanstack/electric-db-collection': - specifier: ^0.3.6 + specifier: ^0.3.14 version: link:../../../packages/electric-db-collection '@tanstack/offline-transactions': - specifier: ^1.0.33 + specifier: ^1.0.41 version: link:../../../packages/offline-transactions '@tanstack/react-db': - specifier: ^0.1.86 + specifier: ^0.1.94 version: link:../../../packages/react-db '@tanstack/react-native-db-sqlite-persistence': - specifier: ^0.2.0 + specifier: ^0.2.8 version: link:../../../packages/react-native-db-sqlite-persistence '@tanstack/react-query': specifier: ^5.90.20 @@ -482,19 +482,19 @@ importers: examples/react/offline-transactions: dependencies: '@tanstack/browser-db-sqlite-persistence': - specifier: ^0.2.0 + specifier: ^0.2.8 version: link:../../../packages/browser-db-sqlite-persistence '@tanstack/db': - specifier: ^0.6.8 + specifier: ^0.6.16 version: link:../../../packages/db '@tanstack/offline-transactions': - specifier: ^1.0.33 + specifier: ^1.0.41 version: link:../../../packages/offline-transactions '@tanstack/query-db-collection': - specifier: ^1.0.40 + specifier: ^1.1.0 version: link:../../../packages/query-db-collection '@tanstack/react-db': - specifier: ^0.1.86 + specifier: ^0.1.94 version: link:../../../packages/react-db '@tanstack/react-query': specifier: ^5.90.20 @@ -552,10 +552,10 @@ importers: examples/react/paced-mutations-demo: dependencies: '@tanstack/db': - specifier: ^0.6.8 + specifier: ^0.6.16 version: link:../../../packages/db '@tanstack/react-db': - specifier: ^0.1.86 + specifier: ^0.1.94 version: link:../../../packages/react-db mitt: specifier: ^3.0.1 @@ -592,10 +592,10 @@ importers: specifier: ^5.90.20 version: 5.90.20 '@tanstack/query-db-collection': - specifier: ^1.0.40 + specifier: ^1.1.0 version: link:../../../packages/query-db-collection '@tanstack/react-db': - specifier: ^0.1.86 + specifier: ^0.1.94 version: link:../../../packages/react-db '@tanstack/react-router': specifier: ^1.159.5 @@ -725,16 +725,16 @@ importers: examples/react/todo: dependencies: '@tanstack/electric-db-collection': - specifier: ^0.3.6 + specifier: ^0.3.14 version: link:../../../packages/electric-db-collection '@tanstack/query-core': specifier: ^5.90.20 version: 5.90.20 '@tanstack/query-db-collection': - specifier: ^1.0.40 + specifier: ^1.1.0 version: link:../../../packages/query-db-collection '@tanstack/react-db': - specifier: ^0.1.86 + specifier: ^0.1.94 version: link:../../../packages/react-db '@tanstack/react-router': specifier: ^1.159.5 @@ -743,7 +743,7 @@ importers: specifier: ^1.159.5 version: 1.159.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@7.3.2(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)))(vite@7.3.2(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)) '@tanstack/trailbase-db-collection': - specifier: ^0.1.86 + specifier: ^0.1.94 version: link:../../../packages/trailbase-db-collection cors: specifier: ^2.8.6 @@ -846,16 +846,16 @@ importers: examples/solid/todo: dependencies: '@tanstack/electric-db-collection': - specifier: ^0.3.6 + specifier: ^0.3.14 version: link:../../../packages/electric-db-collection '@tanstack/query-core': specifier: ^5.90.20 version: 5.90.20 '@tanstack/query-db-collection': - specifier: ^1.0.40 + specifier: ^1.1.0 version: link:../../../packages/query-db-collection '@tanstack/solid-db': - specifier: ^0.2.22 + specifier: ^0.2.30 version: link:../../../packages/solid-db '@tanstack/solid-router': specifier: ^1.159.5 @@ -864,7 +864,7 @@ importers: specifier: ^1.159.5 version: 1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(solid-js@1.9.11)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@7.3.2(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)))(vite@7.3.2(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)) '@tanstack/trailbase-db-collection': - specifier: ^0.1.86 + specifier: ^0.1.94 version: link:../../../packages/trailbase-db-collection cors: specifier: ^2.8.6 @@ -1338,11 +1338,11 @@ importers: version: 4.0.1 devDependencies: '@powersync/common': - specifier: 1.49.0 - version: 1.49.0 + specifier: 1.57.0 + version: 1.57.0 '@powersync/node': - specifier: 0.18.1 - version: 0.18.1(@powersync/common@1.49.0)(better-sqlite3@12.8.0) + specifier: 0.19.2 + version: 0.19.2(@powersync/common@1.57.0)(better-sqlite3@12.8.0) '@types/debug': specifier: ^4.1.12 version: 4.1.12 @@ -4831,13 +4831,13 @@ packages: '@poppinss/exception@1.2.3': resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} - '@powersync/common@1.49.0': - resolution: {integrity: sha512-g6uonubvtmtyx8hS/G5trg9LsBvzHY3tAKHiV7SIQV3Xyz9ONM6NNnjDMP2vcLZVmsOSi8x/QJZmy/ig1YtBMg==} + '@powersync/common@1.57.0': + resolution: {integrity: sha512-uYccCxK5mwahELRouY3YY584TZgjFU8wPPKZQQ6sAOUoMikV8D/+v+UYsNI280MKMnhFqLkxk4TPZIG7ArIzTQ==} - '@powersync/node@0.18.1': - resolution: {integrity: sha512-fcTICgs61CAEb39xiC7pedYsPgbjUInJ/47dr7RIdnEHpAgjWH8bW95/b70qK1fQUANy9lKBBF3PcmfswVgfCw==} + '@powersync/node@0.19.2': + resolution: {integrity: sha512-lF7v/rkiLujAojn7Vjgvs1AibhL5zlEQVYO0iCUGoE1S1Hw7lxfUvAa1mTneKWCEmj0EC9yQBHkPUyBDZXVdLA==} peerDependencies: - '@powersync/common': ^1.49.0 + '@powersync/common': ^1.57.0 better-sqlite3: 12.x peerDependenciesMeta: better-sqlite3: @@ -6799,9 +6799,6 @@ packages: async-limiter@1.0.1: resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==} - async-mutex@0.5.0: - resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} - asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -8173,9 +8170,6 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - event-iterator@2.0.0: - resolution: {integrity: sha512-KGft0ldl31BZVV//jj+IAIGCxkvvUkkON+ScH6zfoX+l+omX6001ggyRSpI0Io2Hlro0ThXotswCtfzS8UkIiQ==} - event-reduce-js@5.2.7: resolution: {integrity: sha512-Vi6aIiAmakzx81JAwhw8L988aSX5a3ZqqVjHyZa9xFU6P4oT1IotoDreWtjNlS+fvEnASvyIQT565nmkOtns/Q==} engines: {node: '>=16'} @@ -9372,6 +9366,9 @@ packages: js-base64@3.7.8: resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} + js-logger@1.6.1: + resolution: {integrity: sha512-yTgMCPXVjhmg28CuUH8CKjU+cIKL/G+zTu4Fn4lQxs8mRFH/03QTNvEFngcxfg/gRDiQAOoyCKmMTOm9ayOzXA==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -16480,16 +16477,13 @@ snapshots: '@poppinss/exception@1.2.3': {} - '@powersync/common@1.49.0': + '@powersync/common@1.57.0': dependencies: - async-mutex: 0.5.0 - event-iterator: 2.0.0 + js-logger: 1.6.1 - '@powersync/node@0.18.1(@powersync/common@1.49.0)(better-sqlite3@12.8.0)': + '@powersync/node@0.19.2(@powersync/common@1.57.0)(better-sqlite3@12.8.0)': dependencies: - '@powersync/common': 1.49.0 - async-mutex: 0.5.0 - bson: 6.10.4 + '@powersync/common': 1.57.0 comlink: 4.4.2 undici: 7.24.4 optionalDependencies: @@ -18957,10 +18951,6 @@ snapshots: async-limiter@1.0.1: {} - async-mutex@0.5.0: - dependencies: - tslib: 2.8.1 - asynckit@0.4.0: {} at-least-node@1.0.0: {} @@ -20577,8 +20567,6 @@ snapshots: etag@1.8.1: {} - event-iterator@2.0.0: {} - event-reduce-js@5.2.7: dependencies: array-push-at-sort-position: 4.0.1 @@ -22133,6 +22121,8 @@ snapshots: js-base64@3.7.8: {} + js-logger@1.6.1: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {}