Skip to content

Conversation

@samwillis
Copy link
Collaborator

@samwillis samwillis commented Jan 8, 2026

stacked on #944

PR: Public API for Custom Operators with Factory Helpers

Summary

This PR adds a clean public API (defineOperator, defineAggregate) for users to create custom operators and aggregates, along with factory helpers that reduce boilerplate for common patterns.

Changes

New Files

  • define.ts - Public API for defineOperator and defineAggregate
  • factories.ts - Factory helpers: comparison(), transform(), numeric(), pattern(), booleanOp()

Refactored Built-in Operators

All built-in operators now use the new factory helpers, significantly reducing code duplication:

  • Comparison operators (eq, gt, gte, lt, lte) - Use comparison() helper
  • Boolean operators (and, or) - Use booleanOp() helper
  • Numeric operators (add, subtract, multiply, divide) - Use numeric() helper
  • String operators (upper, lower) - Use transform() helper
  • Pattern operators (like, ilike) - Use pattern() helper
  • Simple operators (isNull, isUndefined, not) - Use defineOperator directly

Tree-Shaking Improvements

  • Added /*#__PURE__*/ annotations to all factory calls for better tree-shaking
  • Each operator remains in its own file for module-level tree-shaking

New Exports

// Public API
export { defineOperator, defineAggregate } from './define.js'

// Factory helpers for custom operators
export { comparison, transform, numeric, pattern, booleanOp, isUnknown } from './factories.js'

// Types
export type {
  OperatorConfig,
  AggregateDefinition,
  TypedCompiledExpression,
  CompiledArgsFor,
  TypedEvaluatorFactory,
  ExpressionArg,
  ExpressionArgs,
} from './define.js'

Usage Examples

Custom Operator with Factory Helper

import { defineOperator, comparison } from '@tanstack/db'

const notEquals = defineOperator<boolean, [a: unknown, b: unknown]>({
  name: 'notEquals',
  compile: comparison((a, b) => a !== b),
})

Custom Operator with Inline Compile Function

import { defineOperator, isUnknown } from '@tanstack/db'

const between = defineOperator<boolean, [value: number, min: number, max: number]>({
  name: 'between',
  compile: ([value, min, max]) => (data) => {
    const v = value(data)
    if (isUnknown(v)) return null
    return v >= min(data) && v <= max(data)
  }
})

// Use in queries
query.where(({ user }) => between(user.age, 18, 65))

Custom Aggregate

import { defineAggregate } from '@tanstack/db'

const product = defineAggregate<number, number>({
  name: 'product',
  factory: (valueExtractor) => ({
    preMap: valueExtractor,
    reduce: (values) => {
      let result = 1
      for (const [value, mult] of values) {
        for (let i = 0; i < mult; i++) result *= value
      }
      return result
    }
  }),
  valueTransform: 'numeric'
})

API Design Notes

  • compile property: Named to reflect the two-phase execution - called once during query compilation to create the per-row evaluator
  • Named tuples: TArgs supports named tuples like [value: number, min: number] for better IDE experience
  • Factory helpers: Return TypedEvaluatorFactory for type safety while remaining compatible with the internal EvaluatorFactory

Tests

  • Added comprehensive runtime tests in custom-operators.test.ts and custom-aggregates.test.ts
  • Added type-level tests in custom-operators.test-d.ts and custom-aggregates.test-d.ts
  • All 1905 tests pass

@changeset-bot
Copy link

changeset-bot bot commented Jan 8, 2026

⚠️ No Changeset found

Latest commit: 1d734fd

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes changesets to release 12 packages
Name Type
@tanstack/db Patch
@tanstack/angular-db Patch
@tanstack/electric-db-collection Patch
@tanstack/offline-transactions Patch
@tanstack/powersync-db-collection Patch
@tanstack/query-db-collection Patch
@tanstack/react-db Patch
@tanstack/rxdb-db-collection Patch
@tanstack/solid-db Patch
@tanstack/svelte-db Patch
@tanstack/trailbase-db-collection Patch
@tanstack/vue-db Patch

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@samwillis samwillis force-pushed the samwillis/operators-refactor branch from 01b917f to 98b251a Compare January 8, 2026 17:37
@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 8, 2026

More templates

@tanstack/angular-db

npm i https://pkg.pr.new/@tanstack/angular-db@1106

@tanstack/db

npm i https://pkg.pr.new/@tanstack/db@1106

@tanstack/db-ivm

npm i https://pkg.pr.new/@tanstack/db-ivm@1106

@tanstack/electric-db-collection

npm i https://pkg.pr.new/@tanstack/electric-db-collection@1106

@tanstack/offline-transactions

npm i https://pkg.pr.new/@tanstack/offline-transactions@1106

@tanstack/powersync-db-collection

npm i https://pkg.pr.new/@tanstack/powersync-db-collection@1106

@tanstack/query-db-collection

npm i https://pkg.pr.new/@tanstack/query-db-collection@1106

@tanstack/react-db

npm i https://pkg.pr.new/@tanstack/react-db@1106

@tanstack/rxdb-db-collection

npm i https://pkg.pr.new/@tanstack/rxdb-db-collection@1106

@tanstack/solid-db

npm i https://pkg.pr.new/@tanstack/solid-db@1106

@tanstack/svelte-db

npm i https://pkg.pr.new/@tanstack/svelte-db@1106

@tanstack/trailbase-db-collection

npm i https://pkg.pr.new/@tanstack/trailbase-db-collection@1106

@tanstack/vue-db

npm i https://pkg.pr.new/@tanstack/vue-db@1106

commit: 1d734fd

@github-actions
Copy link
Contributor

github-actions bot commented Jan 8, 2026

Size Change: +467 B (+0.48%)

Total Size: 97.2 kB

Filename Size Change
./packages/db/dist/esm/index.js 3.08 kB +130 B (+4.41%)
./packages/db/dist/esm/proxy.js 3.75 kB +3 B (+0.08%)
./packages/db/dist/esm/query/builder/operators/add.js 234 B -13 B (-5.26%)
./packages/db/dist/esm/query/builder/operators/and.js 312 B -24 B (-7.14%)
./packages/db/dist/esm/query/builder/operators/coalesce.js 242 B -6 B (-2.42%)
./packages/db/dist/esm/query/builder/operators/concat.js 234 B -56 B (-19.31%) 👏
./packages/db/dist/esm/query/builder/operators/define.js 325 B +325 B (new file) 🆕
./packages/db/dist/esm/query/builder/operators/divide.js 250 B -9 B (-3.47%)
./packages/db/dist/esm/query/builder/operators/eq.js 315 B +8 B (+2.61%)
./packages/db/dist/esm/query/builder/operators/factories.js 387 B +387 B (new file) 🆕
./packages/db/dist/esm/query/builder/operators/gt.js 227 B -36 B (-13.69%) 👏
./packages/db/dist/esm/query/builder/operators/gte.js 230 B -36 B (-13.53%) 👏
./packages/db/dist/esm/query/builder/operators/ilike.js 255 B -32 B (-11.15%) 👏
./packages/db/dist/esm/query/builder/operators/in.js 287 B +8 B (+2.87%)
./packages/db/dist/esm/query/builder/operators/isNull.js 197 B -14 B (-6.64%)
./packages/db/dist/esm/query/builder/operators/isUndefined.js 200 B -22 B (-9.91%) 👏
./packages/db/dist/esm/query/builder/operators/length.js 259 B +1 B (+0.39%)
./packages/db/dist/esm/query/builder/operators/like.js 404 B -27 B (-6.26%)
./packages/db/dist/esm/query/builder/operators/lower.js 254 B +4 B (+1.6%)
./packages/db/dist/esm/query/builder/operators/lt.js 228 B -34 B (-12.98%) 👏
./packages/db/dist/esm/query/builder/operators/lte.js 230 B -35 B (-13.21%) 👏
./packages/db/dist/esm/query/builder/operators/multiply.js 239 B -11 B (-4.4%)
./packages/db/dist/esm/query/builder/operators/not.js 210 B -24 B (-10.26%) 👏
./packages/db/dist/esm/query/builder/operators/or.js 309 B -23 B (-6.93%)
./packages/db/dist/esm/query/builder/operators/subtract.js 237 B -12 B (-4.82%)
./packages/db/dist/esm/query/builder/operators/upper.js 253 B +4 B (+1.61%)
./packages/db/dist/esm/query/compiler/group-by.js 1.76 kB -5 B (-0.28%)
./packages/db/dist/esm/query/live/collection-subscriber.js 1.9 kB +1 B (+0.05%)
./packages/db/dist/esm/query/live/internal.js 145 B +15 B (+11.54%) ⚠️
ℹ️ View Unchanged
Filename Size
./packages/db/dist/esm/collection/change-events.js 1.39 kB
./packages/db/dist/esm/collection/changes.js 999 B
./packages/db/dist/esm/collection/events.js 388 B
./packages/db/dist/esm/collection/index.js 3.24 kB
./packages/db/dist/esm/collection/indexes.js 1.1 kB
./packages/db/dist/esm/collection/lifecycle.js 1.67 kB
./packages/db/dist/esm/collection/mutations.js 2.34 kB
./packages/db/dist/esm/collection/state.js 3.46 kB
./packages/db/dist/esm/collection/subscription.js 3.64 kB
./packages/db/dist/esm/collection/sync.js 2.38 kB
./packages/db/dist/esm/deferred.js 207 B
./packages/db/dist/esm/errors.js 4.27 kB
./packages/db/dist/esm/event-emitter.js 748 B
./packages/db/dist/esm/indexes/auto-index.js 742 B
./packages/db/dist/esm/indexes/base-index.js 766 B
./packages/db/dist/esm/indexes/btree-index.js 1.93 kB
./packages/db/dist/esm/indexes/lazy-index.js 1.1 kB
./packages/db/dist/esm/indexes/reverse-index.js 513 B
./packages/db/dist/esm/local-only.js 837 B
./packages/db/dist/esm/local-storage.js 2.1 kB
./packages/db/dist/esm/optimistic-action.js 359 B
./packages/db/dist/esm/paced-mutations.js 496 B
./packages/db/dist/esm/query/builder/aggregates/avg.js 251 B
./packages/db/dist/esm/query/builder/aggregates/collect.js 246 B
./packages/db/dist/esm/query/builder/aggregates/count.js 244 B
./packages/db/dist/esm/query/builder/aggregates/max.js 256 B
./packages/db/dist/esm/query/builder/aggregates/maxStr.js 274 B
./packages/db/dist/esm/query/builder/aggregates/min.js 255 B
./packages/db/dist/esm/query/builder/aggregates/minStr.js 273 B
./packages/db/dist/esm/query/builder/aggregates/sum.js 251 B
./packages/db/dist/esm/query/builder/functions.js 308 B
./packages/db/dist/esm/query/builder/index.js 3.96 kB
./packages/db/dist/esm/query/builder/ref-proxy.js 917 B
./packages/db/dist/esm/query/compiler/evaluators.js 591 B
./packages/db/dist/esm/query/compiler/expressions.js 437 B
./packages/db/dist/esm/query/compiler/index.js 1.96 kB
./packages/db/dist/esm/query/compiler/joins.js 2.01 kB
./packages/db/dist/esm/query/compiler/order-by.js 1.46 kB
./packages/db/dist/esm/query/compiler/select.js 1.07 kB
./packages/db/dist/esm/query/expression-helpers.js 1.43 kB
./packages/db/dist/esm/query/ir.js 691 B
./packages/db/dist/esm/query/live-query-collection.js 360 B
./packages/db/dist/esm/query/live/collection-config-builder.js 5.33 kB
./packages/db/dist/esm/query/live/collection-registry.js 264 B
./packages/db/dist/esm/query/optimizer.js 2.57 kB
./packages/db/dist/esm/query/predicate-utils.js 3.03 kB
./packages/db/dist/esm/query/subset-dedupe.js 921 B
./packages/db/dist/esm/scheduler.js 1.3 kB
./packages/db/dist/esm/SortedMap.js 1.3 kB
./packages/db/dist/esm/strategies/debounceStrategy.js 247 B
./packages/db/dist/esm/strategies/queueStrategy.js 428 B
./packages/db/dist/esm/strategies/throttleStrategy.js 246 B
./packages/db/dist/esm/transactions.js 2.9 kB
./packages/db/dist/esm/utils.js 881 B
./packages/db/dist/esm/utils/browser-polyfills.js 304 B
./packages/db/dist/esm/utils/btree.js 5.61 kB
./packages/db/dist/esm/utils/comparison.js 852 B
./packages/db/dist/esm/utils/cursor.js 481 B
./packages/db/dist/esm/utils/index-optimization.js 1.51 kB
./packages/db/dist/esm/utils/type-guards.js 157 B

compressed-size-action::db-package-size

@github-actions
Copy link
Contributor

github-actions bot commented Jan 8, 2026

Size Change: 0 B

Total Size: 3.35 kB

ℹ️ View Unchanged
Filename Size
./packages/react-db/dist/esm/index.js 225 B
./packages/react-db/dist/esm/useLiveInfiniteQuery.js 1.17 kB
./packages/react-db/dist/esm/useLiveQuery.js 1.12 kB
./packages/react-db/dist/esm/useLiveSuspenseQuery.js 431 B
./packages/react-db/dist/esm/usePacedMutations.js 401 B

compressed-size-action::react-db-package-size

@samwillis samwillis force-pushed the samwillis/operators-refactor branch from be91eeb to dd23147 Compare January 8, 2026 17:43
Copy link
Collaborator

@KyleAMathews KyleAMathews left a comment

Choose a reason for hiding this comment

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

Looks good! Merge (double-merge) away!

@KyleAMathews
Copy link
Collaborator

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants