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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions packages/db/src/query/builder/composables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { BaseQueryBuilder } from "./index.js"
import type { InitialQueryBuilder, QueryBuilder } from "./index.js"
import type { NamespacedRow } from "../../types.js"
import type { RefProxyForNamespaceRow, SelectObject } from "./types.js"

/**
* Create a reusable query builder that can be used in multiple places
*
* @param fn - A function that receives an initial query builder and returns a configured query
* @returns A reusable query builder that can be used directly or extended further
*
* @example
* ```ts
* const activeUsersQuery = defineQuery((q) =>
* q.from({ user: usersCollection })
* .where(({ user }) => eq(user.active, true))
* )
*
* // Use directly
* const users = useLiveQuery(activeUsersQuery)
*
* // Extend further
* const activeAdults = activeUsersQuery.where(({ user }) => gt(user.age, 18))
* ```
*/
export function defineQuery<TQueryBuilder extends QueryBuilder<any>>(
fn: (builder: InitialQueryBuilder) => TQueryBuilder
): TQueryBuilder {
return fn(new BaseQueryBuilder())
}

/**
* Create reusable, type-safe query components for specific table schemas
*
* @returns An object with `callback` and `select` methods for creating reusable components
*
* @example
* ```ts
* // Create reusable predicates
* const userIsAdult = defineForRow<{ user: User }>().callback(({ user }) =>
* gt(user.age, 18)
* )
*
* // Create reusable select transformations
* const userInfo = defineForRow<{ user: User }>().select(({ user }) => ({
* id: user.id,
* name: upper(user.name)
* }))
*
* // Use in queries
* const query = useLiveQuery((q) =>
* q.from({ user: usersCollection })
* .where(userIsAdult)
* .select(userInfo)
* )
* ```
*/
export function defineForRow<TNamespaceRow extends NamespacedRow>() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The defineForRow function seems to be used only for typing purposes. I'm against introducing runtime functions for compile time concerns (like typing).

/**
* Create a reusable callback function for WHERE, HAVING, or other expression contexts
*
* @param fn - A function that receives table references and returns an expression
* @returns A reusable callback function that can be used in query builders
*
* @example
* ```ts
* const userIsActive = defineForRow<{ user: User }>().callback(({ user }) =>
* eq(user.active, true)
* )
*
* // Use in WHERE clauses
* query.where(userIsActive)
* ```
*/
const callback = <TResult>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function as well as select are confusing me. Both are basically identity functions. I don't see how this is useful. Seems like unnecessary wrapping.

Since they are identity functions, it seems that your example from the PR description is equivalent to:

type User = {
  name: string
  age: number
}

const userIsAdult = ({ user: User }) => gt(user.age, 18)

export const userNameUpper = ({ user }) => ({
  name: upper(user.name),
})

const users = useLiveQuery((q) => 
  q
    .from({ user: usersCollection }) // usersCollection is a Collection<User>
    .where(userIsAdult)
    .select(userNameUpper)
)

This seems more compact and simpler than the original code in the PR description.

fn: (refs: RefProxyForNamespaceRow<TNamespaceRow>) => TResult
) => fn

/**
* Create a reusable select transformation for projecting and transforming data
*
* @param fn - A function that receives table references and returns a select object
* @returns A reusable select transformation that can be used in query builders
*
* @example
* ```ts
* const userBasicInfo = defineForRow<{ user: User }>().select(({ user }) => ({
* id: user.id,
* name: user.name,
* displayName: upper(user.name)
* }))
*
* // Use in SELECT clauses
* query.select(userBasicInfo)
* ```
*/
const select = <TSelectObject extends SelectObject>(
fn: (refs: RefProxyForNamespaceRow<TNamespaceRow>) => TSelectObject
) => fn

return {
callback,
select,
}
}
6 changes: 6 additions & 0 deletions packages/db/src/query/builder/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { NamespacedRow } from "../../types.js"
import type { CollectionImpl } from "../../collection.js"
import type { Agg, Expression } from "../ir.js"
import type { QueryBuilder } from "./index.js"
Expand Down Expand Up @@ -88,6 +89,11 @@ export type RefProxyForContext<TContext extends Context> = {
[K in keyof TContext[`schema`]]: RefProxyFor<TContext[`schema`][K]>
}

// Type for creating RefProxy objects based on a namespace row
export type RefProxyForNamespaceRow<TNamespaceRow extends NamespacedRow> = {
[K in keyof TNamespaceRow]: RefProxyFor<TNamespaceRow[K]>
}

// Helper type to check if T is exactly undefined
type IsExactlyUndefined<T> = [T] extends [undefined] ? true : false

Expand Down
2 changes: 1 addition & 1 deletion packages/db/src/query/compiler/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type {
type QueryCache = WeakMap<Query, ResultStream>

/**
* Compiles a query2 IR into a D2 pipeline
* Compiles a query IR into a D2 pipeline
* @param query The query IR to compile
* @param inputs Mapping of collection names to input streams
* @param cache Optional cache for compiled subqueries (used internally for recursion)
Expand Down
7 changes: 2 additions & 5 deletions packages/db/src/query/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// Main exports for the new query builder system
// Main exports for the query builder system

// Query builder exports
export {
BaseQueryBuilder,
buildQuery,
type InitialQueryBuilder,
type QueryBuilder,
type Context,
type Source,
type GetResult,
} from "./builder/index.js"
export { defineQuery, defineForRow } from "./builder/composables.js"

// Expression functions exports
export {
Expand Down Expand Up @@ -40,9 +40,6 @@ export {
max,
} from "./builder/functions.js"

// Ref proxy utilities
export { val, toExpression, isRefProxy } from "./builder/ref-proxy.js"

// IR types (for advanced usage)
export type {
Query,
Expand Down
37 changes: 29 additions & 8 deletions packages/db/src/query/live-query-collection.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { D2, MultiSet, output } from "@electric-sql/d2mini"
import { createCollection } from "../collection.js"
import { compileQuery } from "./compiler/index.js"
import { buildQuery } from "./builder/index.js"
import { BaseQueryBuilder, buildQuery, getQuery } from "./builder/index.js"
import type { InitialQueryBuilder, QueryBuilder } from "./builder/index.js"
import type { Collection } from "../collection.js"
import type {
Expand Down Expand Up @@ -53,9 +53,11 @@ export interface LiveQueryCollectionConfig<
id?: string

/**
* Query builder function that defines the live query
* Query builder function or predefined query builder instance that defines the live query
*/
query: (q: InitialQueryBuilder) => QueryBuilder<TContext>
query:
| ((q: InitialQueryBuilder) => QueryBuilder<TContext>)
| QueryBuilder<TContext>

/**
* Function to extract the key from result items
Expand Down Expand Up @@ -114,8 +116,11 @@ export function liveQueryCollectionOptions<
// Generate a unique ID if not provided
const id = config.id || `live-query-${++liveQueryCollectionCounter}`

// Build the query using the provided query builder function
const query = buildQuery(config.query)
// Build the query using the provided query builder function or predefined query builder
const query =
typeof config.query === `function`
? buildQuery(config.query)
: getQuery(config.query)

// WeakMap to store the keys of the results so that we can retreve them in the
// getKey function
Expand Down Expand Up @@ -372,7 +377,13 @@ export function createLiveQueryCollection<
query: (q: InitialQueryBuilder) => QueryBuilder<TContext>
): Collection<TResult, string | number, {}>

// Overload 2: Accept full config object with optional utilities
// Overload 2: Accept just a predefined query builder
export function createLiveQueryCollection<
TContext extends Context,
TResult extends object = GetResult<TContext>,
>(query: QueryBuilder<TContext>): Collection<TResult, string | number, {}>

// Overload 3: Accept full config object with optional utilities
export function createLiveQueryCollection<
TContext extends Context,
TResult extends object = GetResult<TContext>,
Expand All @@ -390,15 +401,25 @@ export function createLiveQueryCollection<
configOrQuery:
| (LiveQueryCollectionConfig<TContext, TResult> & { utils?: TUtils })
| ((q: InitialQueryBuilder) => QueryBuilder<TContext>)
| QueryBuilder<TContext>
): Collection<TResult, string | number, TUtils> {
// Determine if the argument is a function (query) or a config object
// Determine if the argument is a function (query), a QueryBuilder, or a config object
if (typeof configOrQuery === `function`) {
// Simple query function case
const config: LiveQueryCollectionConfig<TContext, TResult> = {
query: configOrQuery,
}
const options = liveQueryCollectionOptions<TContext, TResult>(config)

// Use a bridge function that handles the type compatibility cleanly
return bridgeToCreateCollection(options)
} else if (configOrQuery instanceof BaseQueryBuilder) {
// QueryBuilder instance case (predefined query builder)
const config: LiveQueryCollectionConfig<TContext, TResult> = {
query: configOrQuery as QueryBuilder<TContext>,
}
const options = liveQueryCollectionOptions<TContext, TResult>(config)

// Use a bridge function that handles the type compatibility cleanly
return bridgeToCreateCollection(options)
} else {
Expand All @@ -418,7 +439,7 @@ export function createLiveQueryCollection<
}

/**
* Bridge function that handles the type compatibility between query2's TResult
* Bridge function that handles the type compatibility between query's TResult
* and core collection's ResolveType without exposing ugly type assertions to users
*/
function bridgeToCreateCollection<
Expand Down
Loading