@enspirit/bmg-js 1.0.0

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 (90) hide show
  1. package/.claude/safe-setup/.env.example +3 -0
  2. package/.claude/safe-setup/Dockerfile.claude +36 -0
  3. package/.claude/safe-setup/HACKING.md +63 -0
  4. package/.claude/safe-setup/Makefile +22 -0
  5. package/.claude/safe-setup/docker-compose.yml +18 -0
  6. package/.claude/safe-setup/entrypoint.sh +13 -0
  7. package/.claude/settings.local.json +9 -0
  8. package/.claude/typescript-annotations.md +273 -0
  9. package/.github/workflows/test.yml +26 -0
  10. package/CLAUDE.md +48 -0
  11. package/Makefile +2 -0
  12. package/README.md +170 -0
  13. package/example/README.md +22 -0
  14. package/example/index.ts +316 -0
  15. package/example/package.json +16 -0
  16. package/example/tsconfig.json +11 -0
  17. package/package.json +34 -0
  18. package/src/Relation/Memory.ts +213 -0
  19. package/src/Relation/index.ts +1 -0
  20. package/src/index.ts +31 -0
  21. package/src/operators/_helpers.ts +240 -0
  22. package/src/operators/allbut.ts +19 -0
  23. package/src/operators/autowrap.ts +26 -0
  24. package/src/operators/constants.ts +12 -0
  25. package/src/operators/cross_product.ts +20 -0
  26. package/src/operators/exclude.ts +14 -0
  27. package/src/operators/extend.ts +20 -0
  28. package/src/operators/group.ts +53 -0
  29. package/src/operators/image.ts +27 -0
  30. package/src/operators/index.ts +31 -0
  31. package/src/operators/intersect.ts +24 -0
  32. package/src/operators/isEqual.ts +29 -0
  33. package/src/operators/isRelation.ts +5 -0
  34. package/src/operators/join.ts +25 -0
  35. package/src/operators/left_join.ts +41 -0
  36. package/src/operators/matching.ts +24 -0
  37. package/src/operators/minus.ts +24 -0
  38. package/src/operators/not_matching.ts +24 -0
  39. package/src/operators/one.ts +17 -0
  40. package/src/operators/prefix.ts +7 -0
  41. package/src/operators/project.ts +18 -0
  42. package/src/operators/rename.ts +17 -0
  43. package/src/operators/restrict.ts +14 -0
  44. package/src/operators/suffix.ts +7 -0
  45. package/src/operators/summarize.ts +85 -0
  46. package/src/operators/transform.ts +40 -0
  47. package/src/operators/ungroup.ts +41 -0
  48. package/src/operators/union.ts +27 -0
  49. package/src/operators/unwrap.ts +29 -0
  50. package/src/operators/where.ts +1 -0
  51. package/src/operators/wrap.ts +29 -0
  52. package/src/operators/yByX.ts +12 -0
  53. package/src/support/toPredicateFunc.ts +12 -0
  54. package/src/types.ts +178 -0
  55. package/src/utility-types.ts +77 -0
  56. package/tests/bmg.test.ts +16 -0
  57. package/tests/fixtures.ts +9 -0
  58. package/tests/operators/allbut.test.ts +51 -0
  59. package/tests/operators/autowrap.test.ts +82 -0
  60. package/tests/operators/constants.test.ts +37 -0
  61. package/tests/operators/cross_product.test.ts +90 -0
  62. package/tests/operators/exclude.test.ts +43 -0
  63. package/tests/operators/extend.test.ts +45 -0
  64. package/tests/operators/group.test.ts +69 -0
  65. package/tests/operators/image.test.ts +152 -0
  66. package/tests/operators/intersect.test.ts +53 -0
  67. package/tests/operators/isEqual.test.ts +111 -0
  68. package/tests/operators/join.test.ts +116 -0
  69. package/tests/operators/left_join.test.ts +116 -0
  70. package/tests/operators/matching.test.ts +91 -0
  71. package/tests/operators/minus.test.ts +47 -0
  72. package/tests/operators/not_matching.test.ts +104 -0
  73. package/tests/operators/one.test.ts +19 -0
  74. package/tests/operators/prefix.test.ts +37 -0
  75. package/tests/operators/project.test.ts +48 -0
  76. package/tests/operators/rename.test.ts +39 -0
  77. package/tests/operators/restrict.test.ts +27 -0
  78. package/tests/operators/suffix.test.ts +37 -0
  79. package/tests/operators/summarize.test.ts +109 -0
  80. package/tests/operators/transform.test.ts +94 -0
  81. package/tests/operators/ungroup.test.ts +67 -0
  82. package/tests/operators/union.test.ts +51 -0
  83. package/tests/operators/unwrap.test.ts +50 -0
  84. package/tests/operators/where.test.ts +33 -0
  85. package/tests/operators/wrap.test.ts +54 -0
  86. package/tests/operators/yByX.test.ts +32 -0
  87. package/tests/types/relation.test.ts +296 -0
  88. package/tsconfig.json +37 -0
  89. package/tsconfig.node.json +9 -0
  90. package/vitest.config.ts +15 -0
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Bmg } from 'src';
3
+ import { SUPPLIERS } from 'tests/fixtures';
4
+ import { allbut, isEqual } from 'src/operators';
5
+
6
+ describe('.allbut', () => {
7
+
8
+ it('removes specified attributes', () => {
9
+ const result = SUPPLIERS.allbut(['status', 'city']);
10
+ const expected = Bmg([
11
+ { sid: 'S1', name: 'Smith' },
12
+ { sid: 'S2', name: 'Jones' },
13
+ { sid: 'S3', name: 'Blake' },
14
+ { sid: 'S4', name: 'Clark' },
15
+ { sid: 'S5', name: 'Adams' },
16
+ ]);
17
+ expect(result.isEqual(expected)).to.be.true;
18
+ })
19
+
20
+ it('removes duplicates (set semantics)', () => {
21
+ // Keeping only status should yield 3 unique values, not 5 tuples
22
+ const result = SUPPLIERS.allbut(['sid', 'name', 'city']);
23
+ const expected = Bmg([
24
+ { status: 20 },
25
+ { status: 10 },
26
+ { status: 30 },
27
+ ]);
28
+ expect(result.isEqual(expected)).to.be.true;
29
+ })
30
+
31
+ it('keeps all attributes when excluding none', () => {
32
+ const result = SUPPLIERS.allbut([]);
33
+ expect(result.isEqual(SUPPLIERS)).to.be.true;
34
+ })
35
+
36
+ it('ignores non-existent attributes', () => {
37
+ // @ts-expect-error - testing runtime behavior with invalid attribute
38
+ const result = SUPPLIERS.allbut(['nonexistent']);
39
+ expect(result.isEqual(SUPPLIERS)).to.be.true;
40
+ })
41
+
42
+ ///
43
+
44
+ it('can be used standalone', () => {
45
+ const input = SUPPLIERS.toArray();
46
+ const res = allbut(input, ['status', 'city']);
47
+ const expected = SUPPLIERS.allbut(['status', 'city']);
48
+ expect(isEqual(res, expected)).to.be.true;
49
+ })
50
+
51
+ });
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Bmg } from 'src';
3
+ import { autowrap , isEqual } from 'src/operators';
4
+
5
+ describe('.autowrap', () => {
6
+
7
+ it('wraps attributes based on underscore separator by default', () => {
8
+ const flat = Bmg([
9
+ { id: 1, address_street: '123 Main', address_city: 'NYC' },
10
+ ]);
11
+ const result = flat.autowrap();
12
+ const tuple = result.one();
13
+ expect(tuple.id).to.eql(1);
14
+ expect(tuple.address).to.eql({ street: '123 Main', city: 'NYC' });
15
+ })
16
+
17
+ it('allows custom separator', () => {
18
+ const flat = Bmg([
19
+ { id: 1, 'address.street': '123 Main', 'address.city': 'NYC' },
20
+ ]);
21
+ const result = flat.autowrap({ separator: '.' });
22
+ const tuple = result.one();
23
+ expect(tuple.id).to.eql(1);
24
+ expect(tuple.address).to.eql({ street: '123 Main', city: 'NYC' });
25
+ })
26
+
27
+ it('handles attributes without separator', () => {
28
+ const flat = Bmg([
29
+ { id: 1, name: 'John' },
30
+ ]);
31
+ const result = flat.autowrap();
32
+ const tuple = result.one();
33
+ expect(tuple).to.eql({ id: 1, name: 'John' });
34
+ })
35
+
36
+ it('handles multiple prefixes', () => {
37
+ const flat = Bmg([
38
+ { id: 1, home_city: 'NYC', work_city: 'Boston' },
39
+ ]);
40
+ const result = flat.autowrap();
41
+ const tuple = result.one();
42
+ expect(tuple.id).to.eql(1);
43
+ expect(tuple.home).to.eql({ city: 'NYC' });
44
+ expect(tuple.work).to.eql({ city: 'Boston' });
45
+ })
46
+
47
+ it('handles nested separators (one level only)', () => {
48
+ const flat = Bmg([
49
+ { id: 1, address_home_city: 'NYC' },
50
+ ]);
51
+ const result = flat.autowrap();
52
+ const tuple = result.one();
53
+ expect(tuple.id).to.eql(1);
54
+ // Only first level split: address -> { home_city: 'NYC' }
55
+ expect(tuple.address).to.eql({ home_city: 'NYC' });
56
+ })
57
+
58
+ it('autowraps multiple tuples correctly', () => {
59
+ const flat = Bmg([
60
+ { id: 1, address_street: '123 Main', address_city: 'NYC' },
61
+ { id: 2, address_street: '456 Oak', address_city: 'Boston' },
62
+ { id: 3, address_street: '789 Pine', address_city: 'Chicago' },
63
+ ]);
64
+ const result = flat.autowrap();
65
+ const expected = Bmg([
66
+ { id: 1, address: { street: '123 Main', city: 'NYC' } },
67
+ { id: 2, address: { street: '456 Oak', city: 'Boston' } },
68
+ { id: 3, address: { street: '789 Pine', city: 'Chicago' } },
69
+ ]);
70
+ expect(result.isEqual(expected)).to.be.true;
71
+ })
72
+
73
+ ///
74
+
75
+ it('can be used standalone', () => {
76
+ const input = Bmg([{ id: 1, addr_city: 'NYC' }]);
77
+ const res = autowrap(input.toArray());
78
+ const expected = input.autowrap();
79
+ expect(isEqual(res, expected)).to.be.true;
80
+ })
81
+
82
+ });
@@ -0,0 +1,37 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ import { SUPPLIERS } from 'tests/fixtures';
4
+ import { constants , isEqual } from 'src/operators';
5
+
6
+ describe('.constants', () => {
7
+
8
+ it('adds constant attributes to all tuples', () => {
9
+ const withType = SUPPLIERS.constants({type: 'supplier'});
10
+ const smith = withType.restrict({sid: 'S1'}).one();
11
+ expect(smith.type).to.eql('supplier');
12
+ expect(smith.name).to.eql('Smith');
13
+ })
14
+
15
+ it('can add multiple constants', () => {
16
+ const result = SUPPLIERS.constants({type: 'supplier', active: true});
17
+ const jones = result.restrict({sid: 'S2'}).one();
18
+ expect(jones.type).to.eql('supplier');
19
+ expect(jones.active).to.eql(true);
20
+ })
21
+
22
+ it('overwrites existing attributes', () => {
23
+ const result = SUPPLIERS.constants({city: 'Unknown'});
24
+ const smith = result.restrict({sid: 'S1'}).one();
25
+ expect(smith.city).to.eql('Unknown');
26
+ })
27
+
28
+ ///
29
+
30
+ it('can be used standalone', () => {
31
+ const input = SUPPLIERS.toArray();
32
+ const res = constants(input, { type: 'supplier' });
33
+ const expected = SUPPLIERS.constants({ type: 'supplier' });
34
+ expect(isEqual(res, expected)).to.be.true;
35
+ })
36
+
37
+ });
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Bmg } from 'src';
3
+ import { cross_product, cross_join , isEqual } from 'src/operators';
4
+
5
+ describe('.cross_product', () => {
6
+
7
+ const colors = Bmg([
8
+ { color: 'red' },
9
+ { color: 'blue' },
10
+ ]);
11
+
12
+ const sizes = Bmg([
13
+ { size: 'S' },
14
+ { size: 'M' },
15
+ { size: 'L' },
16
+ ]);
17
+
18
+ it('computes cartesian product of two relations', () => {
19
+ const result = colors.cross_product(sizes);
20
+ const expected = Bmg([
21
+ { color: 'red', size: 'S' },
22
+ { color: 'red', size: 'M' },
23
+ { color: 'red', size: 'L' },
24
+ { color: 'blue', size: 'S' },
25
+ { color: 'blue', size: 'M' },
26
+ { color: 'blue', size: 'L' },
27
+ ]);
28
+ expect(result.isEqual(expected)).to.be.true;
29
+ })
30
+
31
+ it('cross_join is an alias for cross_product', () => {
32
+ const withCrossProduct = colors.cross_product(sizes);
33
+ const withCrossJoin = colors.cross_join(sizes);
34
+ expect(withCrossProduct.isEqual(withCrossJoin)).to.be.true;
35
+ })
36
+
37
+ it('handles DUM', () => {
38
+ const empty = Bmg([]);
39
+ const result = colors.cross_product(empty);
40
+ expect(result.toArray()).to.eql([]);
41
+ })
42
+
43
+ it('handles DEE', () => {
44
+ const empty = Bmg([{}]);
45
+ const result = colors.cross_product(empty);
46
+ expect(result.isEqual(colors)).to.be.true;
47
+ })
48
+
49
+ it('clashing right attributes are ignored', () => {
50
+ const left = Bmg([{ x: 1, y: 2 }]);
51
+ const right = Bmg([{ y: 100, z: 3 }]);
52
+ const result = left.cross_product(right);
53
+ const expected = Bmg([{ x: 1, y: 2, z: 3 }]);
54
+ expect(result.isEqual(expected)).to.be.true;
55
+ })
56
+
57
+ it('clashing right attributes are ignored with multiple tuples', () => {
58
+ const left = Bmg([
59
+ { x: 1, y: 'a' },
60
+ { x: 2, y: 'b' },
61
+ ]);
62
+ const right = Bmg([
63
+ { y: 'ignored', z: 10 },
64
+ { y: 'also_ignored', z: 20 },
65
+ ]);
66
+ const result = left.cross_product(right);
67
+ const expected = Bmg([
68
+ { x: 1, y: 'a', z: 10 },
69
+ { x: 1, y: 'a', z: 20 },
70
+ { x: 2, y: 'b', z: 10 },
71
+ { x: 2, y: 'b', z: 20 },
72
+ ]);
73
+ expect(result.isEqual(expected)).to.be.true;
74
+ })
75
+
76
+ ///
77
+
78
+ it('can be used standalone', () => {
79
+ const res = cross_product(colors.toArray(), sizes.toArray());
80
+ const expected = colors.cross_product(sizes);
81
+ expect(isEqual(res, expected)).to.be.true;
82
+ })
83
+
84
+ it('cross_join standalone is also available', () => {
85
+ const res = cross_join(colors.toArray(), sizes.toArray());
86
+ const expected = colors.cross_join(sizes);
87
+ expect(isEqual(res, expected)).to.be.true;
88
+ })
89
+
90
+ });
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Bmg } from 'src';
3
+ import { SUPPLIERS } from 'tests/fixtures';
4
+ import { exclude , isEqual } from 'src/operators';
5
+
6
+ describe('.exclude', () => {
7
+
8
+ it('filters out tuples matching the predicate', () => {
9
+ const nonParis = SUPPLIERS.exclude({city: 'Paris'});
10
+ const expected = Bmg([
11
+ {sid: 'S1', name: 'Smith', status: 20, city: 'London' },
12
+ {sid: 'S4', name: 'Clark', status: 20, city: 'London' },
13
+ {sid: 'S5', name: 'Adams', status: 30, city: 'Athens' },
14
+ ]);
15
+ expect(nonParis.isEqual(expected)).to.be.true;
16
+ })
17
+
18
+ it('works with a function predicate', () => {
19
+ const lowStatus = SUPPLIERS.exclude((t) => (t.status as number) >= 20);
20
+ const expected = Bmg([
21
+ {sid: 'S2', name: 'Jones', status: 10, city: 'Paris' },
22
+ ]);
23
+ expect(lowStatus.isEqual(expected)).to.be.true;
24
+ })
25
+
26
+ it('is the inverse of restrict', () => {
27
+ const restricted = SUPPLIERS.restrict({city: 'Paris'});
28
+ const excluded = SUPPLIERS.exclude({city: 'Paris'});
29
+ // Union of restrict and exclude should give the original relation
30
+ const reunion = restricted.union(excluded);
31
+ expect(reunion.isEqual(SUPPLIERS)).to.be.true;
32
+ })
33
+
34
+ ///
35
+
36
+ it('can be used standalone', () => {
37
+ const input = SUPPLIERS.toArray();
38
+ const res = exclude(input, { city: 'Paris' });
39
+ const expected = SUPPLIERS.exclude({ city: 'Paris' });
40
+ expect(isEqual(res, expected)).to.be.true;
41
+ })
42
+
43
+ });
@@ -0,0 +1,45 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ import { SUPPLIERS } from 'tests/fixtures';
4
+ import { extend , isEqual } from 'src/operators';
5
+
6
+ describe('.extend', () => {
7
+
8
+ it('adds computed attributes', () => {
9
+ const result = SUPPLIERS.extend({
10
+ statusLabel: (t) => `Status: ${t.status}`
11
+ });
12
+ const smith = result.restrict({ sid: 'S1' }).one();
13
+ expect(smith.statusLabel).to.eql('Status: 20');
14
+ expect(smith.name).to.eql('Smith');
15
+ })
16
+
17
+ it('copies attributes with string shortcut', () => {
18
+ const result = SUPPLIERS.extend({
19
+ location: 'city'
20
+ });
21
+ const smith = result.restrict({ sid: 'S1' }).one();
22
+ expect(smith.location).to.eql('London');
23
+ expect(smith.city).to.eql('London');
24
+ })
25
+
26
+ it('supports multiple extensions', () => {
27
+ const result = SUPPLIERS.extend({
28
+ location: 'city',
29
+ doubled: (t) => (t.status as number) * 2
30
+ });
31
+ const smith = result.restrict({ sid: 'S1' }).one();
32
+ expect(smith.location).to.eql('London');
33
+ expect(smith.doubled).to.eql(40);
34
+ })
35
+
36
+ ///
37
+
38
+ it('can be used standalone', () => {
39
+ const input = SUPPLIERS.toArray();
40
+ const res = extend(input, { location: 'city' });
41
+ const expected = SUPPLIERS.extend({ location: 'city' });
42
+ expect(isEqual(res, expected)).to.be.true;
43
+ })
44
+
45
+ });
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Bmg } from 'src';
3
+ import { group , isEqual } from 'src/operators';
4
+
5
+ describe('.group', () => {
6
+
7
+ const orders = Bmg([
8
+ { order_id: 1, customer: 'Alice', item: 'Apple', qty: 2 },
9
+ { order_id: 1, customer: 'Alice', item: 'Banana', qty: 3 },
10
+ { order_id: 2, customer: 'Bob', item: 'Cherry', qty: 1 },
11
+ ]);
12
+
13
+ it('groups specified attributes into nested relation', () => {
14
+ const result = orders.group(['item', 'qty'], 'items');
15
+ const expected = Bmg([
16
+ {
17
+ order_id: 1,
18
+ customer: 'Alice',
19
+ items: Bmg([
20
+ { item: 'Apple', qty: 2 },
21
+ { item: 'Banana', qty: 3 },
22
+ ])
23
+ },
24
+ {
25
+ order_id: 2,
26
+ customer: 'Bob',
27
+ items: Bmg([
28
+ { item: 'Cherry', qty: 1 },
29
+ ])
30
+ },
31
+ ]);
32
+ expect(result.isEqual(expected)).to.be.true;
33
+ })
34
+
35
+ it('preserves non-grouped attributes', () => {
36
+ const result = orders.group(['item', 'qty'], 'items');
37
+ const bob = result.restrict({ customer: 'Bob' }).one();
38
+ expect(bob).to.have.property('order_id', 2);
39
+ expect(bob).to.have.property('customer', 'Bob');
40
+ expect(bob).to.have.property('items');
41
+ expect(Object.keys(bob).sort()).to.eql(['customer', 'items', 'order_id']);
42
+ })
43
+
44
+ it('handles single-item groups', () => {
45
+ const result = orders.group(['item', 'qty'], 'items');
46
+ const bobOnly = result.restrict({ customer: 'Bob' });
47
+ const expected = Bmg([{
48
+ order_id: 2,
49
+ customer: 'Bob',
50
+ items: Bmg([{ item: 'Cherry', qty: 1 }])
51
+ }]);
52
+ expect(bobOnly.isEqual(expected)).to.be.true;
53
+ })
54
+
55
+ it('handles empty relation', () => {
56
+ const empty = Bmg([]);
57
+ const result = empty.group(['x'], 'grouped');
58
+ expect(result.isEqual(Bmg([]))).to.be.true;
59
+ })
60
+
61
+ ///
62
+
63
+ it('can be used standalone', () => {
64
+ const res = group(orders.toArray(), ['item', 'qty'], 'items');
65
+ const expected = orders.group(['item', 'qty'], 'items');
66
+ expect(isEqual(res, expected)).to.be.true;
67
+ })
68
+
69
+ });
@@ -0,0 +1,152 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Bmg } from 'src';
3
+ import { image , isEqual } from 'src/operators';
4
+
5
+ describe('.image', () => {
6
+
7
+ const suppliers = Bmg([
8
+ { sid: 'S1', name: 'Smith', city: 'London' },
9
+ { sid: 'S2', name: 'Jones', city: 'Paris' },
10
+ { sid: 'S3', name: 'Blake', city: 'Paris' },
11
+ ]);
12
+
13
+ const shipments = Bmg([
14
+ { sid: 'S1', pid: 'P1', qty: 100 },
15
+ { sid: 'S1', pid: 'P2', qty: 200 },
16
+ { sid: 'S2', pid: 'P1', qty: 300 },
17
+ ]);
18
+
19
+ it('adds relation-valued attribute with matching tuples', () => {
20
+ const result = suppliers.image(shipments, 'shipments');
21
+ const expected = Bmg([
22
+ {
23
+ sid: 'S1', name: 'Smith', city: 'London',
24
+ shipments: Bmg([
25
+ { pid: 'P1', qty: 100 },
26
+ { pid: 'P2', qty: 200 },
27
+ ])
28
+ },
29
+ {
30
+ sid: 'S2', name: 'Jones', city: 'Paris',
31
+ shipments: Bmg([
32
+ { pid: 'P1', qty: 300 },
33
+ ])
34
+ },
35
+ {
36
+ sid: 'S3', name: 'Blake', city: 'Paris',
37
+ shipments: Bmg([])
38
+ },
39
+ ]);
40
+ expect(result.isEqual(expected)).to.be.true;
41
+ })
42
+
43
+ it('supports explicit keys as { left: right }', () => {
44
+ const cities = Bmg([
45
+ { location: 'London', country: 'UK' },
46
+ { location: 'Paris', country: 'France' },
47
+ ]);
48
+ const result = suppliers.image(cities, 'city_info', { city: 'location' });
49
+ const expected = Bmg([
50
+ { sid: 'S1', name: 'Smith', city: 'London', city_info: Bmg([{ country: 'UK' }]) },
51
+ { sid: 'S2', name: 'Jones', city: 'Paris', city_info: Bmg([{ country: 'France' }]) },
52
+ { sid: 'S3', name: 'Blake', city: 'Paris', city_info: Bmg([{ country: 'France' }]) },
53
+ ]);
54
+ expect(result.isEqual(expected)).to.be.true;
55
+ })
56
+
57
+ it('supports explicit keys as [common_attr]', () => {
58
+ const result = suppliers.image(shipments, 'shipments', ['sid']);
59
+ const expected = Bmg([
60
+ {
61
+ sid: 'S1', name: 'Smith', city: 'London',
62
+ shipments: Bmg([
63
+ { pid: 'P1', qty: 100 },
64
+ { pid: 'P2', qty: 200 },
65
+ ])
66
+ },
67
+ {
68
+ sid: 'S2', name: 'Jones', city: 'Paris',
69
+ shipments: Bmg([
70
+ { pid: 'P1', qty: 300 },
71
+ ])
72
+ },
73
+ {
74
+ sid: 'S3', name: 'Blake', city: 'Paris',
75
+ shipments: Bmg([])
76
+ },
77
+ ]);
78
+ expect(result.isEqual(expected)).to.be.true;
79
+ })
80
+
81
+ it('supports multiple attributes as join key', () => {
82
+ const inventory = Bmg([
83
+ { warehouse: 'W1', city: 'London', stock: 100 },
84
+ { warehouse: 'W2', city: 'London', stock: 200 },
85
+ { warehouse: 'W3', city: 'Paris', stock: 150 },
86
+ ]);
87
+ const orders = Bmg([
88
+ { warehouse: 'W1', city: 'London', item: 'A', qty: 10 },
89
+ { warehouse: 'W1', city: 'London', item: 'B', qty: 20 },
90
+ { warehouse: 'W3', city: 'Paris', item: 'C', qty: 30 },
91
+ ]);
92
+ const result = inventory.image(orders, 'orders', ['warehouse', 'city']);
93
+ const expected = Bmg([
94
+ {
95
+ warehouse: 'W1', city: 'London', stock: 100,
96
+ orders: Bmg([
97
+ { item: 'A', qty: 10 },
98
+ { item: 'B', qty: 20 },
99
+ ])
100
+ },
101
+ {
102
+ warehouse: 'W2', city: 'London', stock: 200,
103
+ orders: Bmg([])
104
+ },
105
+ {
106
+ warehouse: 'W3', city: 'Paris', stock: 150,
107
+ orders: Bmg([
108
+ { item: 'C', qty: 30 },
109
+ ])
110
+ },
111
+ ]);
112
+ expect(result.isEqual(expected)).to.be.true;
113
+ })
114
+
115
+ it('supports multiple attributes as { left: right } object', () => {
116
+ const people = Bmg([
117
+ { id: 1, first: 'John', last: 'Doe' },
118
+ { id: 2, first: 'Jane', last: 'Smith' },
119
+ ]);
120
+ const records = Bmg([
121
+ { fname: 'John', lname: 'Doe', year: 2020, score: 85 },
122
+ { fname: 'John', lname: 'Doe', year: 2021, score: 90 },
123
+ { fname: 'Jane', lname: 'Smith', year: 2020, score: 95 },
124
+ ]);
125
+ const result = people.image(records, 'scores', { first: 'fname', last: 'lname' });
126
+ const expected = Bmg([
127
+ {
128
+ id: 1, first: 'John', last: 'Doe',
129
+ scores: Bmg([
130
+ { year: 2020, score: 85 },
131
+ { year: 2021, score: 90 },
132
+ ])
133
+ },
134
+ {
135
+ id: 2, first: 'Jane', last: 'Smith',
136
+ scores: Bmg([
137
+ { year: 2020, score: 95 },
138
+ ])
139
+ },
140
+ ]);
141
+ expect(result.isEqual(expected)).to.be.true;
142
+ })
143
+
144
+ ///
145
+
146
+ it('can be used standalone', () => {
147
+ const res = image(suppliers.toArray(), shipments.toArray(), 'shipments');
148
+ const expected = suppliers.image(shipments, 'shipments');
149
+ expect(isEqual(res, expected)).to.be.true;
150
+ })
151
+
152
+ });
@@ -0,0 +1,53 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Bmg } from 'src';
3
+ import { intersect , isEqual } from 'src/operators';
4
+
5
+ describe('.intersect', () => {
6
+
7
+ const left = Bmg([
8
+ { id: 1, name: 'Alice' },
9
+ { id: 2, name: 'Bob' },
10
+ { id: 3, name: 'Charlie' },
11
+ ]);
12
+
13
+ const right = Bmg([
14
+ { id: 2, name: 'Bob' },
15
+ { id: 3, name: 'Charlie' },
16
+ { id: 4, name: 'Diana' },
17
+ ]);
18
+
19
+ it('returns tuples present in both relations', () => {
20
+ const result = left.intersect(right);
21
+ const expected = Bmg([
22
+ { id: 2, name: 'Bob' },
23
+ { id: 3, name: 'Charlie' },
24
+ ]);
25
+ expect(result.isEqual(expected)).to.be.true;
26
+ })
27
+
28
+ it('returns empty when no overlap', () => {
29
+ const other = Bmg([{ id: 99, name: 'Nobody' }]);
30
+ const result = left.intersect(other);
31
+ expect(result.isEqual(Bmg([]))).to.be.true;
32
+ })
33
+
34
+ it('returns all when identical', () => {
35
+ const result = left.intersect(left);
36
+ expect(result.isEqual(left)).to.be.true;
37
+ })
38
+
39
+ it('is commutative (set semantics)', () => {
40
+ const lr = left.intersect(right);
41
+ const rl = right.intersect(left);
42
+ expect(lr.isEqual(rl)).to.be.true;
43
+ })
44
+
45
+ ///
46
+
47
+ it('can be used standalone', () => {
48
+ const res = intersect(left.toArray(), right.toArray());
49
+ const expected = left.intersect(right);
50
+ expect(isEqual(res, expected)).to.be.true;
51
+ })
52
+
53
+ });