@tanstack/db 0.0.23 → 0.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/cjs/collection.cjs +60 -19
  2. package/dist/cjs/collection.cjs.map +1 -1
  3. package/dist/cjs/collection.d.cts +27 -6
  4. package/dist/cjs/local-only.cjs +2 -1
  5. package/dist/cjs/local-only.cjs.map +1 -1
  6. package/dist/cjs/local-storage.cjs +2 -1
  7. package/dist/cjs/local-storage.cjs.map +1 -1
  8. package/dist/cjs/proxy.cjs +105 -11
  9. package/dist/cjs/proxy.cjs.map +1 -1
  10. package/dist/cjs/proxy.d.cts +8 -0
  11. package/dist/cjs/query/builder/index.cjs +72 -0
  12. package/dist/cjs/query/builder/index.cjs.map +1 -1
  13. package/dist/cjs/query/builder/index.d.cts +64 -0
  14. package/dist/cjs/query/compiler/index.cjs +44 -8
  15. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  16. package/dist/cjs/query/compiler/index.d.cts +4 -7
  17. package/dist/cjs/query/compiler/joins.cjs +14 -6
  18. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  19. package/dist/cjs/query/compiler/joins.d.cts +4 -8
  20. package/dist/cjs/query/compiler/types.d.cts +10 -0
  21. package/dist/cjs/query/live-query-collection.cjs +2 -1
  22. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  23. package/dist/cjs/query/optimizer.cjs +283 -0
  24. package/dist/cjs/query/optimizer.cjs.map +1 -0
  25. package/dist/cjs/query/optimizer.d.cts +42 -0
  26. package/dist/cjs/types.d.cts +1 -0
  27. package/dist/cjs/utils.cjs +42 -0
  28. package/dist/cjs/utils.cjs.map +1 -0
  29. package/dist/cjs/utils.d.cts +18 -0
  30. package/dist/esm/collection.d.ts +27 -6
  31. package/dist/esm/collection.js +60 -19
  32. package/dist/esm/collection.js.map +1 -1
  33. package/dist/esm/local-only.js +2 -1
  34. package/dist/esm/local-only.js.map +1 -1
  35. package/dist/esm/local-storage.js +2 -1
  36. package/dist/esm/local-storage.js.map +1 -1
  37. package/dist/esm/proxy.d.ts +8 -0
  38. package/dist/esm/proxy.js +105 -11
  39. package/dist/esm/proxy.js.map +1 -1
  40. package/dist/esm/query/builder/index.d.ts +64 -0
  41. package/dist/esm/query/builder/index.js +72 -0
  42. package/dist/esm/query/builder/index.js.map +1 -1
  43. package/dist/esm/query/compiler/index.d.ts +4 -7
  44. package/dist/esm/query/compiler/index.js +44 -8
  45. package/dist/esm/query/compiler/index.js.map +1 -1
  46. package/dist/esm/query/compiler/joins.d.ts +4 -8
  47. package/dist/esm/query/compiler/joins.js +14 -6
  48. package/dist/esm/query/compiler/joins.js.map +1 -1
  49. package/dist/esm/query/compiler/types.d.ts +10 -0
  50. package/dist/esm/query/live-query-collection.js +2 -1
  51. package/dist/esm/query/live-query-collection.js.map +1 -1
  52. package/dist/esm/query/optimizer.d.ts +42 -0
  53. package/dist/esm/query/optimizer.js +283 -0
  54. package/dist/esm/query/optimizer.js.map +1 -0
  55. package/dist/esm/types.d.ts +1 -0
  56. package/dist/esm/utils.d.ts +18 -0
  57. package/dist/esm/utils.js +42 -0
  58. package/dist/esm/utils.js.map +1 -0
  59. package/package.json +1 -1
  60. package/src/collection.ts +75 -26
  61. package/src/local-only.ts +4 -1
  62. package/src/local-storage.ts +4 -1
  63. package/src/proxy.ts +152 -24
  64. package/src/query/builder/index.ts +104 -0
  65. package/src/query/compiler/index.ts +85 -18
  66. package/src/query/compiler/joins.ts +21 -13
  67. package/src/query/compiler/types.ts +12 -0
  68. package/src/query/live-query-collection.ts +3 -1
  69. package/src/query/optimizer.ts +738 -0
  70. package/src/types.ts +1 -0
  71. package/src/utils.ts +86 -0
package/src/collection.ts CHANGED
@@ -229,8 +229,9 @@ export class CollectionImpl<
229
229
  private hasReceivedFirstCommit = false
230
230
  private isCommittingSyncTransactions = false
231
231
 
232
- // Array to store one-time commit listeners
233
- private onFirstCommitCallbacks: Array<() => void> = []
232
+ // Array to store one-time ready listeners
233
+ private onFirstReadyCallbacks: Array<() => void> = []
234
+ private hasBeenReady = false
234
235
 
235
236
  // Event batching for preventing duplicate emissions during transaction flows
236
237
  private batchedEvents: Array<ChangeMessage<T, TKey>> = []
@@ -244,17 +245,66 @@ export class CollectionImpl<
244
245
  private syncCleanupFn: (() => void) | null = null
245
246
 
246
247
  /**
247
- * Register a callback to be executed on the next commit
248
+ * Register a callback to be executed when the collection first becomes ready
248
249
  * Useful for preloading collections
249
- * @param callback Function to call after the next commit
250
+ * @param callback Function to call when the collection first becomes ready
250
251
  * @example
251
- * collection.onFirstCommit(() => {
252
- * console.log('Collection has received first data')
252
+ * collection.onFirstReady(() => {
253
+ * console.log('Collection is ready for the first time')
253
254
  * // Safe to access collection.state now
254
255
  * })
255
256
  */
256
- public onFirstCommit(callback: () => void): void {
257
- this.onFirstCommitCallbacks.push(callback)
257
+ public onFirstReady(callback: () => void): void {
258
+ // If already ready, call immediately
259
+ if (this.hasBeenReady) {
260
+ callback()
261
+ return
262
+ }
263
+
264
+ this.onFirstReadyCallbacks.push(callback)
265
+ }
266
+
267
+ /**
268
+ * Check if the collection is ready for use
269
+ * Returns true if the collection has been marked as ready by its sync implementation
270
+ * @returns true if the collection is ready, false otherwise
271
+ * @example
272
+ * if (collection.isReady()) {
273
+ * console.log('Collection is ready, data is available')
274
+ * // Safe to access collection.state
275
+ * } else {
276
+ * console.log('Collection is still loading')
277
+ * }
278
+ */
279
+ public isReady(): boolean {
280
+ return this._status === `ready`
281
+ }
282
+
283
+ /**
284
+ * Mark the collection as ready for use
285
+ * This is called by sync implementations to explicitly signal that the collection is ready,
286
+ * providing a more intuitive alternative to using commits for readiness signaling
287
+ * @private - Should only be called by sync implementations
288
+ */
289
+ private markReady(): void {
290
+ // Can transition to ready from loading or initialCommit states
291
+ if (this._status === `loading` || this._status === `initialCommit`) {
292
+ this.setStatus(`ready`)
293
+
294
+ // Call any registered first ready callbacks (only on first time becoming ready)
295
+ if (!this.hasBeenReady) {
296
+ this.hasBeenReady = true
297
+
298
+ // Also mark as having received first commit for backwards compatibility
299
+ if (!this.hasReceivedFirstCommit) {
300
+ this.hasReceivedFirstCommit = true
301
+ }
302
+
303
+ const callbacks = [...this.onFirstReadyCallbacks]
304
+ this.onFirstReadyCallbacks = []
305
+ callbacks.forEach((callback) => callback())
306
+ }
307
+ }
258
308
  }
259
309
 
260
310
  public id = ``
@@ -302,7 +352,7 @@ export class CollectionImpl<
302
352
  Array<CollectionStatus>
303
353
  > = {
304
354
  idle: [`loading`, `error`, `cleaned-up`],
305
- loading: [`initialCommit`, `error`, `cleaned-up`],
355
+ loading: [`initialCommit`, `ready`, `error`, `cleaned-up`],
306
356
  initialCommit: [`ready`, `error`, `cleaned-up`],
307
357
  ready: [`cleaned-up`, `error`],
308
358
  error: [`cleaned-up`, `idle`],
@@ -455,11 +505,9 @@ export class CollectionImpl<
455
505
  }
456
506
 
457
507
  this.commitPendingTransactions()
458
-
459
- // Transition from initialCommit to ready after the first commit is complete
460
- if (this._status === `initialCommit`) {
461
- this.setStatus(`ready`)
462
- }
508
+ },
509
+ markReady: () => {
510
+ this.markReady()
463
511
  },
464
512
  })
465
513
 
@@ -492,7 +540,7 @@ export class CollectionImpl<
492
540
  }
493
541
 
494
542
  // Register callback BEFORE starting sync to avoid race condition
495
- this.onFirstCommit(() => {
543
+ this.onFirstReady(() => {
496
544
  resolve()
497
545
  })
498
546
 
@@ -555,7 +603,8 @@ export class CollectionImpl<
555
603
  this.pendingSyncedTransactions = []
556
604
  this.syncedKeys.clear()
557
605
  this.hasReceivedFirstCommit = false
558
- this.onFirstCommitCallbacks = []
606
+ this.hasBeenReady = false
607
+ this.onFirstReadyCallbacks = []
559
608
  this.preloadPromise = null
560
609
  this.batchedEvents = []
561
610
  this.shouldBatchEvents = false
@@ -1184,8 +1233,8 @@ export class CollectionImpl<
1184
1233
  // Call any registered one-time commit listeners
1185
1234
  if (!this.hasReceivedFirstCommit) {
1186
1235
  this.hasReceivedFirstCommit = true
1187
- const callbacks = [...this.onFirstCommitCallbacks]
1188
- this.onFirstCommitCallbacks = []
1236
+ const callbacks = [...this.onFirstReadyCallbacks]
1237
+ this.onFirstReadyCallbacks = []
1189
1238
  callbacks.forEach((callback) => callback())
1190
1239
  }
1191
1240
  }
@@ -1812,14 +1861,14 @@ export class CollectionImpl<
1812
1861
  * @returns Promise that resolves to a Map containing all items in the collection
1813
1862
  */
1814
1863
  stateWhenReady(): Promise<Map<TKey, T>> {
1815
- // If we already have data or there are no loading collections, resolve immediately
1816
- if (this.size > 0 || this.hasReceivedFirstCommit === true) {
1864
+ // If we already have data or collection is ready, resolve immediately
1865
+ if (this.size > 0 || this.isReady()) {
1817
1866
  return Promise.resolve(this.state)
1818
1867
  }
1819
1868
 
1820
- // Otherwise, wait for the first commit
1869
+ // Otherwise, wait for the collection to be ready
1821
1870
  return new Promise<Map<TKey, T>>((resolve) => {
1822
- this.onFirstCommit(() => {
1871
+ this.onFirstReady(() => {
1823
1872
  resolve(this.state)
1824
1873
  })
1825
1874
  })
@@ -1841,14 +1890,14 @@ export class CollectionImpl<
1841
1890
  * @returns Promise that resolves to an Array containing all items in the collection
1842
1891
  */
1843
1892
  toArrayWhenReady(): Promise<Array<T>> {
1844
- // If we already have data or there are no loading collections, resolve immediately
1845
- if (this.size > 0 || this.hasReceivedFirstCommit === true) {
1893
+ // If we already have data or collection is ready, resolve immediately
1894
+ if (this.size > 0 || this.isReady()) {
1846
1895
  return Promise.resolve(this.toArray)
1847
1896
  }
1848
1897
 
1849
- // Otherwise, wait for the first commit
1898
+ // Otherwise, wait for the collection to be ready
1850
1899
  return new Promise<Array<T>>((resolve) => {
1851
- this.onFirstCommit(() => {
1900
+ this.onFirstReady(() => {
1852
1901
  resolve(this.toArray)
1853
1902
  })
1854
1903
  })
package/src/local-only.ts CHANGED
@@ -240,7 +240,7 @@ function createLocalOnlySync<T extends object, TKey extends string | number>(
240
240
  * @returns Unsubscribe function (empty since no ongoing sync is needed)
241
241
  */
242
242
  sync: (params) => {
243
- const { begin, write, commit } = params
243
+ const { begin, write, commit, markReady } = params
244
244
 
245
245
  // Capture sync functions for later use by confirmOperationsSync
246
246
  syncBegin = begin
@@ -259,6 +259,9 @@ function createLocalOnlySync<T extends object, TKey extends string | number>(
259
259
  commit()
260
260
  }
261
261
 
262
+ // Mark collection as ready since local-only collections are immediately ready
263
+ markReady()
264
+
262
265
  // Return empty unsubscribe function - no ongoing sync needed
263
266
  return () => {}
264
267
  },
@@ -586,7 +586,7 @@ function createLocalStorageSync<T extends object>(
586
586
 
587
587
  const syncConfig: SyncConfig<T> & { manualTrigger?: () => void } = {
588
588
  sync: (params: Parameters<SyncConfig<T>[`sync`]>[0]) => {
589
- const { begin, write, commit } = params
589
+ const { begin, write, commit, markReady } = params
590
590
 
591
591
  // Store sync params for later use
592
592
  syncParams = params
@@ -608,6 +608,9 @@ function createLocalStorageSync<T extends object>(
608
608
  lastKnownData.set(key, storedItem)
609
609
  })
610
610
 
611
+ // Mark collection as ready after initial load
612
+ markReady()
613
+
611
614
  // Listen for storage events from other tabs
612
615
  const handleStorageEvent = (event: StorageEvent) => {
613
616
  // Only respond to changes to our specific key and from our storage
package/src/proxy.ts CHANGED
@@ -40,10 +40,21 @@ interface ChangeTracker<T extends object> {
40
40
  copy_: T
41
41
  proxyCount: number
42
42
  assigned_: Record<string | symbol, boolean>
43
- parent?: {
44
- tracker: ChangeTracker<Record<string | symbol, unknown>>
45
- prop: string | symbol
46
- }
43
+ parent?:
44
+ | {
45
+ tracker: ChangeTracker<Record<string | symbol, unknown>>
46
+ prop: string | symbol
47
+ }
48
+ | {
49
+ tracker: ChangeTracker<Record<string | symbol, unknown>>
50
+ prop: string | symbol
51
+ updateMap: (newValue: unknown) => void
52
+ }
53
+ | {
54
+ tracker: ChangeTracker<Record<string | symbol, unknown>>
55
+ prop: unknown
56
+ updateSet: (newValue: unknown) => void
57
+ }
47
58
  target: T
48
59
  }
49
60
 
@@ -330,9 +341,18 @@ export function createChangeProxy<
330
341
  if (state.parent) {
331
342
  debugLog(`propagating change to parent`)
332
343
 
333
- // Update parent's copy with this object's current state
334
- state.parent.tracker.copy_[state.parent.prop] = state.copy_
335
- state.parent.tracker.assigned_[state.parent.prop] = true
344
+ // Check if this is a special Map parent with updateMap function
345
+ if (`updateMap` in state.parent) {
346
+ // Use the special updateMap function for Maps
347
+ state.parent.updateMap(state.copy_)
348
+ } else if (`updateSet` in state.parent) {
349
+ // Use the special updateSet function for Sets
350
+ state.parent.updateSet(state.copy_)
351
+ } else {
352
+ // Update parent's copy with this object's current state
353
+ state.parent.tracker.copy_[state.parent.prop] = state.copy_
354
+ state.parent.tracker.assigned_[state.parent.prop] = true
355
+ }
336
356
 
337
357
  // Mark parent as changed
338
358
  markChanged(state.parent.tracker)
@@ -410,7 +430,7 @@ export function createChangeProxy<
410
430
  // Update parent status based on child changes
411
431
  function checkParentStatus(
412
432
  parentState: ChangeTracker<Record<string | symbol, unknown>>,
413
- childProp: string | symbol
433
+ childProp: string | symbol | unknown
414
434
  ) {
415
435
  debugLog(`checkParentStatus called for child prop:`, childProp)
416
436
 
@@ -461,6 +481,30 @@ export function createChangeProxy<
461
481
 
462
482
  // If the value is a function, bind it to the ptarget
463
483
  if (typeof value === `function`) {
484
+ // For Array methods that modify the array
485
+ if (Array.isArray(ptarget)) {
486
+ const methodName = prop.toString()
487
+ const modifyingMethods = new Set([
488
+ `pop`,
489
+ `push`,
490
+ `shift`,
491
+ `unshift`,
492
+ `splice`,
493
+ `sort`,
494
+ `reverse`,
495
+ `fill`,
496
+ `copyWithin`,
497
+ ])
498
+
499
+ if (modifyingMethods.has(methodName)) {
500
+ return function (...args: Array<unknown>) {
501
+ const result = value.apply(changeTracker.copy_, args)
502
+ markChanged(changeTracker)
503
+ return result
504
+ }
505
+ }
506
+ }
507
+
464
508
  // For Map and Set methods that modify the collection
465
509
  if (ptarget instanceof Map || ptarget instanceof Set) {
466
510
  const methodName = prop.toString()
@@ -541,6 +585,28 @@ export function createChangeProxy<
541
585
  // to track changes when the values are accessed and potentially modified
542
586
  const originalIterator = result
543
587
 
588
+ // For values() iterator on Maps, we need to create a value-to-key mapping
589
+ const valueToKeyMap = new Map()
590
+ if (methodName === `values` && ptarget instanceof Map) {
591
+ // Build a mapping from value to key for reverse lookup
592
+ // Use the copy_ (which is the current state) to build the mapping
593
+ for (const [
594
+ key,
595
+ mapValue,
596
+ ] of changeTracker.copy_.entries()) {
597
+ valueToKeyMap.set(mapValue, key)
598
+ }
599
+ }
600
+
601
+ // For Set iterators, we need to create an original-to-modified mapping
602
+ const originalToModifiedMap = new Map()
603
+ if (ptarget instanceof Set) {
604
+ // Initialize with original values
605
+ for (const setValue of changeTracker.copy_.values()) {
606
+ originalToModifiedMap.set(setValue, setValue)
607
+ }
608
+ }
609
+
544
610
  // Create a proxy for the iterator that will mark changes when next() is called
545
611
  return {
546
612
  next() {
@@ -563,15 +629,25 @@ export function createChangeProxy<
563
629
  nextResult.value[1] &&
564
630
  typeof nextResult.value[1] === `object`
565
631
  ) {
632
+ const mapKey = nextResult.value[0]
633
+ // Create a special parent tracker that knows how to update the Map
634
+ const mapParent = {
635
+ tracker: changeTracker,
636
+ prop: mapKey,
637
+ updateMap: (newValue: unknown) => {
638
+ // Update the Map in the copy
639
+ if (changeTracker.copy_ instanceof Map) {
640
+ changeTracker.copy_.set(mapKey, newValue)
641
+ }
642
+ },
643
+ }
644
+
566
645
  // Create a proxy for the value and replace it in the result
567
646
  const { proxy: valueProxy } =
568
- memoizedCreateChangeProxy(nextResult.value[1], {
569
- tracker: changeTracker,
570
- prop:
571
- typeof nextResult.value[0] === `symbol`
572
- ? nextResult.value[0]
573
- : String(nextResult.value[0]),
574
- })
647
+ memoizedCreateChangeProxy(
648
+ nextResult.value[1],
649
+ mapParent
650
+ )
575
651
  nextResult.value[1] = valueProxy
576
652
  }
577
653
  } else if (
@@ -584,16 +660,68 @@ export function createChangeProxy<
584
660
  typeof nextResult.value === `object` &&
585
661
  nextResult.value !== null
586
662
  ) {
587
- // For Set, we need to track the whole object
588
- // For Map, we would need the key, but we don't have it here
589
- // So we'll use a symbol as a placeholder
590
- const tempKey = Symbol(`iterator-value`)
591
- const { proxy: valueProxy } =
592
- memoizedCreateChangeProxy(nextResult.value, {
663
+ // For Map values(), try to find the key using our mapping
664
+ if (
665
+ methodName === `values` &&
666
+ ptarget instanceof Map
667
+ ) {
668
+ const mapKey = valueToKeyMap.get(nextResult.value)
669
+ if (mapKey !== undefined) {
670
+ // Create a special parent tracker for this Map value
671
+ const mapParent = {
672
+ tracker: changeTracker,
673
+ prop: mapKey,
674
+ updateMap: (newValue: unknown) => {
675
+ // Update the Map in the copy
676
+ if (changeTracker.copy_ instanceof Map) {
677
+ changeTracker.copy_.set(mapKey, newValue)
678
+ }
679
+ },
680
+ }
681
+
682
+ const { proxy: valueProxy } =
683
+ memoizedCreateChangeProxy(
684
+ nextResult.value,
685
+ mapParent
686
+ )
687
+ nextResult.value = valueProxy
688
+ }
689
+ } else if (ptarget instanceof Set) {
690
+ // For Set, we need to track modifications and update the Set accordingly
691
+ const setOriginalValue = nextResult.value
692
+ const setParent = {
593
693
  tracker: changeTracker,
594
- prop: tempKey,
595
- })
596
- nextResult.value = valueProxy
694
+ prop: setOriginalValue, // Use the original value as the prop
695
+ updateSet: (newValue: unknown) => {
696
+ // Update the Set in the copy by removing old value and adding new one
697
+ if (changeTracker.copy_ instanceof Set) {
698
+ changeTracker.copy_.delete(setOriginalValue)
699
+ changeTracker.copy_.add(newValue)
700
+ // Update our mapping for future iterations
701
+ originalToModifiedMap.set(
702
+ setOriginalValue,
703
+ newValue
704
+ )
705
+ }
706
+ },
707
+ }
708
+
709
+ const { proxy: valueProxy } =
710
+ memoizedCreateChangeProxy(
711
+ nextResult.value,
712
+ setParent
713
+ )
714
+ nextResult.value = valueProxy
715
+ } else {
716
+ // For other cases, use a symbol as a placeholder
717
+ const tempKey = Symbol(`iterator-value`)
718
+ const { proxy: valueProxy } =
719
+ memoizedCreateChangeProxy(nextResult.value, {
720
+ tracker: changeTracker,
721
+ prop: tempKey,
722
+ })
723
+ nextResult.value = valueProxy
724
+ }
597
725
  }
598
726
  }
599
727
  }
@@ -184,6 +184,110 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
184
184
  }) as any
185
185
  }
186
186
 
187
+ /**
188
+ * Perform a LEFT JOIN with another table or subquery
189
+ *
190
+ * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery
191
+ * @param onCallback - A function that receives table references and returns the join condition
192
+ * @returns A QueryBuilder with the left joined table available
193
+ *
194
+ * @example
195
+ * ```ts
196
+ * // Left join users with posts
197
+ * query
198
+ * .from({ users: usersCollection })
199
+ * .leftJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId))
200
+ * ```
201
+ */
202
+ leftJoin<TSource extends Source>(
203
+ source: TSource,
204
+ onCallback: JoinOnCallback<
205
+ MergeContext<TContext, SchemaFromSource<TSource>>
206
+ >
207
+ ): QueryBuilder<
208
+ MergeContextWithJoinType<TContext, SchemaFromSource<TSource>, `left`>
209
+ > {
210
+ return this.join(source, onCallback, `left`)
211
+ }
212
+
213
+ /**
214
+ * Perform a RIGHT JOIN with another table or subquery
215
+ *
216
+ * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery
217
+ * @param onCallback - A function that receives table references and returns the join condition
218
+ * @returns A QueryBuilder with the right joined table available
219
+ *
220
+ * @example
221
+ * ```ts
222
+ * // Right join users with posts
223
+ * query
224
+ * .from({ users: usersCollection })
225
+ * .rightJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId))
226
+ * ```
227
+ */
228
+ rightJoin<TSource extends Source>(
229
+ source: TSource,
230
+ onCallback: JoinOnCallback<
231
+ MergeContext<TContext, SchemaFromSource<TSource>>
232
+ >
233
+ ): QueryBuilder<
234
+ MergeContextWithJoinType<TContext, SchemaFromSource<TSource>, `right`>
235
+ > {
236
+ return this.join(source, onCallback, `right`)
237
+ }
238
+
239
+ /**
240
+ * Perform an INNER JOIN with another table or subquery
241
+ *
242
+ * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery
243
+ * @param onCallback - A function that receives table references and returns the join condition
244
+ * @returns A QueryBuilder with the inner joined table available
245
+ *
246
+ * @example
247
+ * ```ts
248
+ * // Inner join users with posts
249
+ * query
250
+ * .from({ users: usersCollection })
251
+ * .innerJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId))
252
+ * ```
253
+ */
254
+ innerJoin<TSource extends Source>(
255
+ source: TSource,
256
+ onCallback: JoinOnCallback<
257
+ MergeContext<TContext, SchemaFromSource<TSource>>
258
+ >
259
+ ): QueryBuilder<
260
+ MergeContextWithJoinType<TContext, SchemaFromSource<TSource>, `inner`>
261
+ > {
262
+ return this.join(source, onCallback, `inner`)
263
+ }
264
+
265
+ /**
266
+ * Perform a FULL JOIN with another table or subquery
267
+ *
268
+ * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery
269
+ * @param onCallback - A function that receives table references and returns the join condition
270
+ * @returns A QueryBuilder with the full joined table available
271
+ *
272
+ * @example
273
+ * ```ts
274
+ * // Full join users with posts
275
+ * query
276
+ * .from({ users: usersCollection })
277
+ * .fullJoin({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId))
278
+ * ```
279
+ */
280
+ fullJoin<TSource extends Source>(
281
+ source: TSource,
282
+ onCallback: JoinOnCallback<
283
+ MergeContext<TContext, SchemaFromSource<TSource>>
284
+ >
285
+ ): QueryBuilder<
286
+ MergeContextWithJoinType<TContext, SchemaFromSource<TSource>, `full`>
287
+ > {
288
+ return this.join(source, onCallback, `full`)
289
+ }
290
+
187
291
  /**
188
292
  * Filter rows based on a condition
189
293
  *