@@ -2,6 +2,9 @@ import {describe, expect, test} from 'vitest';
22import {
33 generateWithOverlayUnordered ,
44 generateWithOverlayNoYieldUnordered ,
5+ rowEqualsForCompoundKey ,
6+ isJoinMatch ,
7+ buildJoinConstraint ,
58} from './join-utils.ts' ;
69import type { Node } from './data.ts' ;
710import type { SourceSchema } from './schema.ts' ;
@@ -280,3 +283,100 @@ describe('generateWithOverlayNoYieldUnordered', () => {
280283 expect ( result . map ( n => n . row ) ) . toEqual ( [ { id : 1 } , { id : 3 } ] ) ;
281284 } ) ;
282285} ) ;
286+
287+ describe ( 'rowEqualsForCompoundKey' , ( ) => {
288+ test ( 'single key match' , ( ) => {
289+ expect ( rowEqualsForCompoundKey ( { id : 1 } , { id : 1 } , [ 'id' ] ) ) . toBe ( true ) ;
290+ } ) ;
291+
292+ test ( 'single key mismatch' , ( ) => {
293+ expect ( rowEqualsForCompoundKey ( { id : 1 } , { id : 2 } , [ 'id' ] ) ) . toBe ( false ) ;
294+ } ) ;
295+
296+ test ( 'compound key all match' , ( ) => {
297+ expect (
298+ rowEqualsForCompoundKey ( { a : 1 , b : 'x' } , { a : 1 , b : 'x' } , [ 'a' , 'b' ] ) ,
299+ ) . toBe ( true ) ;
300+ } ) ;
301+
302+ test ( 'compound key partial mismatch' , ( ) => {
303+ expect (
304+ rowEqualsForCompoundKey ( { a : 1 , b : 'x' } , { a : 1 , b : 'y' } , [ 'a' , 'b' ] ) ,
305+ ) . toBe ( false ) ;
306+ } ) ;
307+
308+ test ( 'null equals null (compareValues treats null as a real value)' , ( ) => {
309+ expect ( rowEqualsForCompoundKey ( { id : null } , { id : null } , [ 'id' ] ) ) . toBe ( true ) ;
310+ } ) ;
311+
312+ test ( 'extra columns ignored' , ( ) => {
313+ expect (
314+ rowEqualsForCompoundKey (
315+ { id : 1 , val : 'a' } ,
316+ { id : 1 , val : 'b' } ,
317+ [ 'id' ] ,
318+ ) ,
319+ ) . toBe ( true ) ;
320+ } ) ;
321+ } ) ;
322+
323+ describe ( 'isJoinMatch' , ( ) => {
324+ test ( 'single key match' , ( ) => {
325+ expect ( isJoinMatch ( { id : 1 } , [ 'id' ] , { id : 1 } , [ 'id' ] ) ) . toBe ( true ) ;
326+ } ) ;
327+
328+ test ( 'single key mismatch' , ( ) => {
329+ expect ( isJoinMatch ( { id : 1 } , [ 'id' ] , { id : 2 } , [ 'id' ] ) ) . toBe ( false ) ;
330+ } ) ;
331+
332+ test ( 'compound key match with different column names' , ( ) => {
333+ expect (
334+ isJoinMatch (
335+ { a : 1 , b : 'x' } ,
336+ [ 'a' , 'b' ] ,
337+ { x : 1 , y : 'x' } ,
338+ [ 'x' , 'y' ] ,
339+ ) ,
340+ ) . toBe ( true ) ;
341+ } ) ;
342+
343+ test ( 'null parent value returns false (SQL NULL semantics)' , ( ) => {
344+ expect ( isJoinMatch ( { id : null } , [ 'id' ] , { id : 1 } , [ 'id' ] ) ) . toBe ( false ) ;
345+ } ) ;
346+
347+ test ( 'null child value returns false' , ( ) => {
348+ expect ( isJoinMatch ( { id : 1 } , [ 'id' ] , { id : null } , [ 'id' ] ) ) . toBe ( false ) ;
349+ } ) ;
350+
351+ test ( 'both null returns false (unlike rowEqualsForCompoundKey)' , ( ) => {
352+ expect ( isJoinMatch ( { id : null } , [ 'id' ] , { id : null } , [ 'id' ] ) ) . toBe ( false ) ;
353+ } ) ;
354+ } ) ;
355+
356+ describe ( 'buildJoinConstraint' , ( ) => {
357+ test ( 'single key maps value correctly' , ( ) => {
358+ expect ( buildJoinConstraint ( { id : 1 } , [ 'id' ] , [ 'id' ] ) ) . toEqual ( { id : 1 } ) ;
359+ } ) ;
360+
361+ test ( 'compound key maps all values' , ( ) => {
362+ expect (
363+ buildJoinConstraint ( { a : 1 , b : 'x' } , [ 'a' , 'b' ] , [ 'a' , 'b' ] ) ,
364+ ) . toEqual ( { a : 1 , b : 'x' } ) ;
365+ } ) ;
366+
367+ test ( 'null value returns undefined' , ( ) => {
368+ expect ( buildJoinConstraint ( { id : null } , [ 'id' ] , [ 'id' ] ) ) . toBeUndefined ( ) ;
369+ } ) ;
370+
371+ test ( 'null in second position returns undefined' , ( ) => {
372+ expect (
373+ buildJoinConstraint ( { a : 1 , b : null } , [ 'a' , 'b' ] , [ 'x' , 'y' ] ) ,
374+ ) . toBeUndefined ( ) ;
375+ } ) ;
376+
377+ test ( 'different source/target key names' , ( ) => {
378+ expect (
379+ buildJoinConstraint ( { userId : 5 , orgId : 10 } , [ 'userId' , 'orgId' ] , [ 'id' , 'org' ] ) ,
380+ ) . toEqual ( { id : 5 , org : 10 } ) ;
381+ } ) ;
382+ } ) ;
0 commit comments