@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,111 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Bmg } from 'src';
3
+ import { isEqual } from 'src/operators';
4
+
5
+ describe('.isEqual', () => {
6
+
7
+ it('returns true for identical relations', () => {
8
+ const r = Bmg([
9
+ { id: 1, name: 'Alice' },
10
+ { id: 2, name: 'Bob' },
11
+ ]);
12
+ expect(r.isEqual(r)).to.be.true;
13
+ })
14
+
15
+ it('returns true for relations with same tuples', () => {
16
+ const r1 = Bmg([
17
+ { id: 1, name: 'Alice' },
18
+ { id: 2, name: 'Bob' },
19
+ ]);
20
+ const r2 = Bmg([
21
+ { id: 1, name: 'Alice' },
22
+ { id: 2, name: 'Bob' },
23
+ ]);
24
+ expect(r1.isEqual(r2)).to.be.true;
25
+ })
26
+
27
+ it('returns true regardless of tuple order', () => {
28
+ const r1 = Bmg([
29
+ { id: 1, name: 'Alice' },
30
+ { id: 2, name: 'Bob' },
31
+ ]);
32
+ const r2 = Bmg([
33
+ { id: 2, name: 'Bob' },
34
+ { id: 1, name: 'Alice' },
35
+ ]);
36
+ expect(r1.isEqual(r2)).to.be.true;
37
+ })
38
+
39
+ it('returns true regardless of attribute order', () => {
40
+ const r1 = Bmg([
41
+ { id: 1, name: 'Alice' },
42
+ ]);
43
+ const r2 = Bmg([
44
+ { name: 'Alice', id: 1 },
45
+ ]);
46
+ expect(r1.isEqual(r2)).to.be.true;
47
+ })
48
+
49
+ it('ignores duplicates (set semantics)', () => {
50
+ const r1 = Bmg([
51
+ { id: 1, name: 'Alice' },
52
+ { id: 1, name: 'Alice' },
53
+ ]);
54
+ const r2 = Bmg([
55
+ { id: 1, name: 'Alice' },
56
+ ]);
57
+ expect(r1.isEqual(r2)).to.be.true;
58
+ })
59
+
60
+ it('returns false for different tuple count', () => {
61
+ const r1 = Bmg([
62
+ { id: 1, name: 'Alice' },
63
+ { id: 2, name: 'Bob' },
64
+ ]);
65
+ const r2 = Bmg([
66
+ { id: 1, name: 'Alice' },
67
+ ]);
68
+ expect(r1.isEqual(r2)).to.be.false;
69
+ })
70
+
71
+ it('returns false for different tuple values', () => {
72
+ const r1 = Bmg([
73
+ { id: 1, name: 'Alice' },
74
+ ]);
75
+ const r2 = Bmg([
76
+ { id: 1, name: 'Bob' },
77
+ ]);
78
+ expect(r1.isEqual(r2)).to.be.false;
79
+ })
80
+
81
+ it('returns false for different attributes', () => {
82
+ const r1 = Bmg([
83
+ { id: 1, name: 'Alice' },
84
+ ]);
85
+ const r2 = Bmg([
86
+ { id: 1, city: 'Paris' },
87
+ ]);
88
+ expect(r1.isEqual(r2)).to.be.false;
89
+ })
90
+
91
+ it('returns true for empty relations', () => {
92
+ const r1 = Bmg([]);
93
+ const r2 = Bmg([]);
94
+ expect(r1.isEqual(r2)).to.be.true;
95
+ })
96
+
97
+ ///
98
+
99
+ it('can be used standalone', () => {
100
+ const left = [{ id: 1, name: 'Alice' }];
101
+ const right = [{ name: 'Alice', id: 1 }];
102
+ expect(isEqual(left, right)).to.be.true;
103
+ })
104
+
105
+ it('works with array operand', () => {
106
+ const r = Bmg([{ id: 1, name: 'Alice' }]);
107
+ const arr = [{ id: 1, name: 'Alice' }];
108
+ expect(r.isEqual(arr)).to.be.true;
109
+ })
110
+
111
+ });
@@ -0,0 +1,116 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Bmg } from 'src';
3
+ import { join , isEqual } from 'src/operators';
4
+
5
+ describe('.join', () => {
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 parts = Bmg([
14
+ { pid: 'P1', pname: 'Nut', city: 'London' },
15
+ { pid: 'P2', pname: 'Bolt', city: 'Paris' },
16
+ { pid: 'P3', pname: 'Screw', city: 'Athens' },
17
+ ]);
18
+
19
+ it('performs natural join on common attributes', () => {
20
+ const result = suppliers.join(parts);
21
+ const expected = Bmg([
22
+ { sid: 'S1', name: 'Smith', city: 'London', pid: 'P1', pname: 'Nut' },
23
+ { sid: 'S2', name: 'Jones', city: 'Paris', pid: 'P2', pname: 'Bolt' },
24
+ { sid: 'S3', name: 'Blake', city: 'Paris', pid: 'P2', pname: 'Bolt' },
25
+ ]);
26
+ expect(result.isEqual(expected)).to.be.true;
27
+ })
28
+
29
+ it('joins on specified attributes as [common_attr]', () => {
30
+ const result = suppliers.join(parts, ['city']);
31
+ const expected = Bmg([
32
+ { sid: 'S1', name: 'Smith', city: 'London', pid: 'P1', pname: 'Nut' },
33
+ { sid: 'S2', name: 'Jones', city: 'Paris', pid: 'P2', pname: 'Bolt' },
34
+ { sid: 'S3', name: 'Blake', city: 'Paris', pid: 'P2', pname: 'Bolt' },
35
+ ]);
36
+ expect(result.isEqual(expected)).to.be.true;
37
+ })
38
+
39
+ it('joins on multiple attributes as [attr1, attr2]', () => {
40
+ const inventory = Bmg([
41
+ { warehouse: 'W1', city: 'London', stock: 100 },
42
+ { warehouse: 'W2', city: 'London', stock: 200 },
43
+ { warehouse: 'W1', city: 'Paris', stock: 150 },
44
+ ]);
45
+ const shipments = Bmg([
46
+ { warehouse: 'W1', city: 'London', qty: 10 },
47
+ { warehouse: 'W1', city: 'Paris', qty: 20 },
48
+ ]);
49
+ const result = inventory.join(shipments, ['warehouse', 'city']);
50
+ const expected = Bmg([
51
+ { warehouse: 'W1', city: 'London', stock: 100, qty: 10 },
52
+ { warehouse: 'W1', city: 'Paris', stock: 150, qty: 20 },
53
+ ]);
54
+ expect(result.isEqual(expected)).to.be.true;
55
+ })
56
+
57
+ it('joins on different attribute names as { left: right }', () => {
58
+ const locations = Bmg([
59
+ { location: 'London', country: 'UK' },
60
+ { location: 'Paris', country: 'France' },
61
+ ]);
62
+ const result = suppliers.join(locations, { city: 'location' });
63
+ const expected = Bmg([
64
+ { sid: 'S1', name: 'Smith', city: 'London', country: 'UK' },
65
+ { sid: 'S2', name: 'Jones', city: 'Paris', country: 'France' },
66
+ { sid: 'S3', name: 'Blake', city: 'Paris', country: 'France' },
67
+ ]);
68
+ expect(result.isEqual(expected)).to.be.true;
69
+ })
70
+
71
+ it('joins on multiple different attribute names as { left1: right1, left2: right2 }', () => {
72
+ const people = Bmg([
73
+ { id: 1, first: 'John', last: 'Doe' },
74
+ { id: 2, first: 'Jane', last: 'Smith' },
75
+ { id: 3, first: 'John', last: 'Smith' },
76
+ ]);
77
+ const records = Bmg([
78
+ { fname: 'John', lname: 'Doe', score: 85 },
79
+ { fname: 'Jane', lname: 'Smith', score: 90 },
80
+ ]);
81
+ const result = people.join(records, { first: 'fname', last: 'lname' });
82
+ const expected = Bmg([
83
+ { id: 1, first: 'John', last: 'Doe', score: 85 },
84
+ { id: 2, first: 'Jane', last: 'Smith', score: 90 },
85
+ ]);
86
+ expect(result.isEqual(expected)).to.be.true;
87
+ })
88
+
89
+ it('returns empty for no matches', () => {
90
+ const other = Bmg([{ city: 'Tokyo' }]);
91
+ const result = suppliers.join(other);
92
+ expect(result.isEqual(Bmg([]))).to.be.true;
93
+ })
94
+
95
+ it('handles cartesian product when no common attrs', () => {
96
+ const colors = Bmg([{ color: 'red' }, { color: 'blue' }]);
97
+ const sizes = Bmg([{ size: 'S' }, { size: 'M' }]);
98
+ const result = colors.join(sizes);
99
+ const expected = Bmg([
100
+ { color: 'red', size: 'S' },
101
+ { color: 'red', size: 'M' },
102
+ { color: 'blue', size: 'S' },
103
+ { color: 'blue', size: 'M' },
104
+ ]);
105
+ expect(result.isEqual(expected)).to.be.true;
106
+ })
107
+
108
+ ///
109
+
110
+ it('can be used standalone', () => {
111
+ const res = join(suppliers.toArray(), parts.toArray(), ['city']);
112
+ const expected = suppliers.join(parts, ['city']);
113
+ expect(isEqual(res, expected)).to.be.true;
114
+ })
115
+
116
+ });
@@ -0,0 +1,116 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Bmg } from 'src';
3
+ import { left_join , isEqual } from 'src/operators';
4
+
5
+ describe('.left_join', () => {
6
+
7
+ const orders = Bmg([
8
+ { oid: 1, customer_id: 'C1', amount: 100 },
9
+ { oid: 2, customer_id: 'C2', amount: 200 },
10
+ { oid: 3, customer_id: 'C3', amount: 150 },
11
+ ]);
12
+
13
+ const customers = Bmg([
14
+ { customer_id: 'C1', name: 'Alice' },
15
+ { customer_id: 'C2', name: 'Bob' },
16
+ ]);
17
+
18
+ it('joins matching tuples and keeps non-matching with null', () => {
19
+ const result = orders.left_join(customers);
20
+ const expected = Bmg([
21
+ { oid: 1, customer_id: 'C1', amount: 100, name: 'Alice' },
22
+ { oid: 2, customer_id: 'C2', amount: 200, name: 'Bob' },
23
+ { oid: 3, customer_id: 'C3', amount: 150, name: null },
24
+ ]);
25
+ expect(result.isEqual(expected)).to.be.true;
26
+ })
27
+
28
+ it('supports explicit keys as { left: right }', () => {
29
+ const suppliers = Bmg([
30
+ { sid: 'S1', city: 'London' },
31
+ { sid: 'S2', city: 'Paris' },
32
+ { sid: 'S3', city: 'Athens' },
33
+ ]);
34
+ const cities = Bmg([
35
+ { location: 'London', country: 'UK' },
36
+ { location: 'Paris', country: 'France' },
37
+ ]);
38
+ const result = suppliers.left_join(cities, { city: 'location' });
39
+ const expected = Bmg([
40
+ { sid: 'S1', city: 'London', country: 'UK' },
41
+ { sid: 'S2', city: 'Paris', country: 'France' },
42
+ { sid: 'S3', city: 'Athens', country: null },
43
+ ]);
44
+ expect(result.isEqual(expected)).to.be.true;
45
+ })
46
+
47
+ it('supports explicit keys as [common_attr]', () => {
48
+ const result = orders.left_join(customers, ['customer_id']);
49
+ const expected = Bmg([
50
+ { oid: 1, customer_id: 'C1', amount: 100, name: 'Alice' },
51
+ { oid: 2, customer_id: 'C2', amount: 200, name: 'Bob' },
52
+ { oid: 3, customer_id: 'C3', amount: 150, name: null },
53
+ ]);
54
+ expect(result.isEqual(expected)).to.be.true;
55
+ })
56
+
57
+ it('supports multiple keys as [attr1, attr2]', () => {
58
+ const inventory = Bmg([
59
+ { warehouse: 'W1', city: 'London', stock: 100 },
60
+ { warehouse: 'W2', city: 'London', stock: 200 },
61
+ { warehouse: 'W1', city: 'Paris', stock: 150 },
62
+ ]);
63
+ const shipments = Bmg([
64
+ { warehouse: 'W1', city: 'London', shipped: 50 },
65
+ ]);
66
+ const result = inventory.left_join(shipments, ['warehouse', 'city']);
67
+ const expected = Bmg([
68
+ { warehouse: 'W1', city: 'London', stock: 100, shipped: 50 },
69
+ { warehouse: 'W2', city: 'London', stock: 200, shipped: null },
70
+ { warehouse: 'W1', city: 'Paris', stock: 150, shipped: null },
71
+ ]);
72
+ expect(result.isEqual(expected)).to.be.true;
73
+ })
74
+
75
+ it('supports multiple keys as { left1: right1, left2: right2 }', () => {
76
+ const people = Bmg([
77
+ { id: 1, first: 'John', last: 'Doe' },
78
+ { id: 2, first: 'Jane', last: 'Smith' },
79
+ { id: 3, first: 'John', last: 'Smith' },
80
+ ]);
81
+ const records = Bmg([
82
+ { fname: 'John', lname: 'Doe', score: 85 },
83
+ { fname: 'Jane', lname: 'Smith', score: 90 },
84
+ ]);
85
+ const result = people.left_join(records, { first: 'fname', last: 'lname' });
86
+ const expected = Bmg([
87
+ { id: 1, first: 'John', last: 'Doe', score: 85 },
88
+ { id: 2, first: 'Jane', last: 'Smith', score: 90 },
89
+ { id: 3, first: 'John', last: 'Smith', score: null },
90
+ ]);
91
+ expect(result.isEqual(expected)).to.be.true;
92
+ })
93
+
94
+ it('handles multiple matches', () => {
95
+ const left = Bmg([{ id: 1, city: 'Paris' }]);
96
+ const right = Bmg([
97
+ { city: 'Paris', name: 'A' },
98
+ { city: 'Paris', name: 'B' },
99
+ ]);
100
+ const result = left.left_join(right);
101
+ const expected = Bmg([
102
+ { id: 1, city: 'Paris', name: 'A' },
103
+ { id: 1, city: 'Paris', name: 'B' },
104
+ ]);
105
+ expect(result.isEqual(expected)).to.be.true;
106
+ })
107
+
108
+ ///
109
+
110
+ it('can be used standalone', () => {
111
+ const res = left_join(orders.toArray(), customers.toArray());
112
+ const expected = orders.left_join(customers);
113
+ expect(isEqual(res, expected)).to.be.true;
114
+ })
115
+
116
+ });
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Bmg } from 'src';
3
+ import { matching , isEqual } from 'src/operators';
4
+
5
+ describe('.matching', () => {
6
+
7
+ const orders = Bmg([
8
+ { oid: 1, customer_id: 'C1', amount: 100 },
9
+ { oid: 2, customer_id: 'C2', amount: 200 },
10
+ { oid: 3, customer_id: 'C3', amount: 150 },
11
+ ]);
12
+
13
+ const customers = Bmg([
14
+ { customer_id: 'C1', name: 'Alice' },
15
+ { customer_id: 'C2', name: 'Bob' },
16
+ ]);
17
+
18
+ it('returns tuples from left that match right on common attrs', () => {
19
+ const result = orders.matching(customers);
20
+ const expected = Bmg([
21
+ { oid: 1, customer_id: 'C1', amount: 100 },
22
+ { oid: 2, customer_id: 'C2', amount: 200 },
23
+ ]);
24
+ expect(result.isEqual(expected)).to.be.true;
25
+ })
26
+
27
+ it('supports explicit keys as [common_attr]', () => {
28
+ const result = orders.matching(customers, ['customer_id']);
29
+ const expected = Bmg([
30
+ { oid: 1, customer_id: 'C1', amount: 100 },
31
+ { oid: 2, customer_id: 'C2', amount: 200 },
32
+ ]);
33
+ expect(result.isEqual(expected)).to.be.true;
34
+ })
35
+
36
+ it('supports multiple keys as [attr1, attr2]', () => {
37
+ const inventory = Bmg([
38
+ { warehouse: 'W1', city: 'London', stock: 100 },
39
+ { warehouse: 'W2', city: 'London', stock: 200 },
40
+ { warehouse: 'W1', city: 'Paris', stock: 150 },
41
+ ]);
42
+ const shipments = Bmg([
43
+ { warehouse: 'W1', city: 'London' },
44
+ { warehouse: 'W1', city: 'Paris' },
45
+ ]);
46
+ const result = inventory.matching(shipments, ['warehouse', 'city']);
47
+ const expected = Bmg([
48
+ { warehouse: 'W1', city: 'London', stock: 100 },
49
+ { warehouse: 'W1', city: 'Paris', stock: 150 },
50
+ ]);
51
+ expect(result.isEqual(expected)).to.be.true;
52
+ })
53
+
54
+ it('supports explicit keys as { left: right }', () => {
55
+ const suppliers = Bmg([
56
+ { sid: 'S1', city: 'London' },
57
+ { sid: 'S2', city: 'Paris' },
58
+ ]);
59
+ const cities = Bmg([{ location: 'London' }]);
60
+ const result = suppliers.matching(cities, { city: 'location' });
61
+ const expected = Bmg([{ sid: 'S1', city: 'London' }]);
62
+ expect(result.isEqual(expected)).to.be.true;
63
+ })
64
+
65
+ it('supports multiple keys as { left1: right1, left2: right2 }', () => {
66
+ const people = Bmg([
67
+ { id: 1, first: 'John', last: 'Doe' },
68
+ { id: 2, first: 'Jane', last: 'Smith' },
69
+ { id: 3, first: 'John', last: 'Smith' },
70
+ ]);
71
+ const records = Bmg([
72
+ { fname: 'John', lname: 'Doe' },
73
+ { fname: 'Jane', lname: 'Smith' },
74
+ ]);
75
+ const result = people.matching(records, { first: 'fname', last: 'lname' });
76
+ const expected = Bmg([
77
+ { id: 1, first: 'John', last: 'Doe' },
78
+ { id: 2, first: 'Jane', last: 'Smith' },
79
+ ]);
80
+ expect(result.isEqual(expected)).to.be.true;
81
+ })
82
+
83
+ ///
84
+
85
+ it('can be used standalone', () => {
86
+ const res = matching(orders.toArray(), customers.toArray());
87
+ const expected = orders.matching(customers);
88
+ expect(isEqual(res, expected)).to.be.true;
89
+ })
90
+
91
+ });
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Bmg } from 'src';
3
+ import { minus , isEqual } from 'src/operators';
4
+
5
+ describe('.minus', () => {
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: 4, name: 'Diana' },
16
+ ]);
17
+
18
+ it('returns tuples in left but not in right', () => {
19
+ const result = left.minus(right);
20
+ const expected = Bmg([
21
+ { id: 1, name: 'Alice' },
22
+ { id: 3, name: 'Charlie' },
23
+ ]);
24
+ expect(result.isEqual(expected)).to.be.true;
25
+ })
26
+
27
+ it('returns empty when left is subset of right', () => {
28
+ const small = Bmg([{ id: 2, name: 'Bob' }]);
29
+ const result = small.minus(right);
30
+ expect(result.isEqual(Bmg([]))).to.be.true;
31
+ })
32
+
33
+ it('returns all when no overlap', () => {
34
+ const other = Bmg([{ id: 99, name: 'Nobody' }]);
35
+ const result = left.minus(other);
36
+ expect(result.isEqual(left)).to.be.true;
37
+ })
38
+
39
+ ///
40
+
41
+ it('can be used standalone', () => {
42
+ const res = minus(left.toArray(), right.toArray());
43
+ const expected = left.minus(right);
44
+ expect(isEqual(res, expected)).to.be.true;
45
+ })
46
+
47
+ });
@@ -0,0 +1,104 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Bmg } from 'src';
3
+ import { not_matching , isEqual } from 'src/operators';
4
+
5
+ describe('.not_matching', () => {
6
+
7
+ const orders = Bmg([
8
+ { oid: 1, customer_id: 'C1', amount: 100 },
9
+ { oid: 2, customer_id: 'C2', amount: 200 },
10
+ { oid: 3, customer_id: 'C3', amount: 150 },
11
+ ]);
12
+
13
+ const customers = Bmg([
14
+ { customer_id: 'C1', name: 'Alice' },
15
+ { customer_id: 'C2', name: 'Bob' },
16
+ ]);
17
+
18
+ it('returns tuples from left that do NOT match right', () => {
19
+ const result = orders.not_matching(customers);
20
+ const expected = Bmg([
21
+ { oid: 3, customer_id: 'C3', amount: 150 },
22
+ ]);
23
+ expect(result.isEqual(expected)).to.be.true;
24
+ })
25
+
26
+ it('returns all when no matches', () => {
27
+ const empty = Bmg([{ customer_id: 'C99' }]);
28
+ const result = orders.not_matching(empty);
29
+ expect(result.isEqual(orders)).to.be.true;
30
+ })
31
+
32
+ it('returns empty when all match', () => {
33
+ const all = Bmg([
34
+ { customer_id: 'C1' },
35
+ { customer_id: 'C2' },
36
+ { customer_id: 'C3' },
37
+ ]);
38
+ const result = orders.not_matching(all);
39
+ expect(result.isEqual(Bmg([]))).to.be.true;
40
+ })
41
+
42
+ it('supports explicit keys as [common_attr]', () => {
43
+ const result = orders.not_matching(customers, ['customer_id']);
44
+ const expected = Bmg([
45
+ { oid: 3, customer_id: 'C3', amount: 150 },
46
+ ]);
47
+ expect(result.isEqual(expected)).to.be.true;
48
+ })
49
+
50
+ it('supports multiple keys as [attr1, attr2]', () => {
51
+ const inventory = Bmg([
52
+ { warehouse: 'W1', city: 'London', stock: 100 },
53
+ { warehouse: 'W2', city: 'London', stock: 200 },
54
+ { warehouse: 'W1', city: 'Paris', stock: 150 },
55
+ ]);
56
+ const shipments = Bmg([
57
+ { warehouse: 'W1', city: 'London' },
58
+ ]);
59
+ const result = inventory.not_matching(shipments, ['warehouse', 'city']);
60
+ const expected = Bmg([
61
+ { warehouse: 'W2', city: 'London', stock: 200 },
62
+ { warehouse: 'W1', city: 'Paris', stock: 150 },
63
+ ]);
64
+ expect(result.isEqual(expected)).to.be.true;
65
+ })
66
+
67
+ it('supports explicit keys as { left: right }', () => {
68
+ const suppliers = Bmg([
69
+ { sid: 'S1', city: 'London' },
70
+ { sid: 'S2', city: 'Paris' },
71
+ { sid: 'S3', city: 'Athens' },
72
+ ]);
73
+ const cities = Bmg([{ location: 'London' }, { location: 'Paris' }]);
74
+ const result = suppliers.not_matching(cities, { city: 'location' });
75
+ const expected = Bmg([{ sid: 'S3', city: 'Athens' }]);
76
+ expect(result.isEqual(expected)).to.be.true;
77
+ })
78
+
79
+ it('supports multiple keys as { left1: right1, left2: right2 }', () => {
80
+ const people = Bmg([
81
+ { id: 1, first: 'John', last: 'Doe' },
82
+ { id: 2, first: 'Jane', last: 'Smith' },
83
+ { id: 3, first: 'John', last: 'Smith' },
84
+ ]);
85
+ const records = Bmg([
86
+ { fname: 'John', lname: 'Doe' },
87
+ { fname: 'Jane', lname: 'Smith' },
88
+ ]);
89
+ const result = people.not_matching(records, { first: 'fname', last: 'lname' });
90
+ const expected = Bmg([
91
+ { id: 3, first: 'John', last: 'Smith' },
92
+ ]);
93
+ expect(result.isEqual(expected)).to.be.true;
94
+ })
95
+
96
+ ///
97
+
98
+ it('can be used standalone', () => {
99
+ const res = not_matching(orders.toArray(), customers.toArray());
100
+ const expected = orders.not_matching(customers);
101
+ expect(isEqual(res, expected)).to.be.true;
102
+ })
103
+
104
+ });
@@ -0,0 +1,19 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { SUPPLIERS } from 'tests/fixtures';
3
+ import { one } from 'src/operators';
4
+
5
+ describe('.one', () => {
6
+
7
+ it('is available on relations', () => {
8
+ expect(typeof SUPPLIERS.one).to.eql('function')
9
+ })
10
+
11
+ ///
12
+
13
+ it('can be used standalone', () => {
14
+ const tuple = { sid: 'S1' }
15
+ const tuples = [tuple]
16
+ expect(one(tuples)).to.eql(tuple)
17
+ })
18
+
19
+ });
@@ -0,0 +1,37 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Bmg } from 'src';
3
+ import { SUPPLIERS } from 'tests/fixtures';
4
+ import { prefix , isEqual } from 'src/operators';
5
+
6
+ describe('.prefix', () => {
7
+
8
+ it('prefixes all attribute names', () => {
9
+ const prefixed = SUPPLIERS.prefix('supplier_');
10
+ const expected = Bmg([
11
+ {supplier_sid: 'S1', supplier_name: 'Smith', supplier_status: 20, supplier_city: 'London' },
12
+ {supplier_sid: 'S2', supplier_name: 'Jones', supplier_status: 10, supplier_city: 'Paris' },
13
+ {supplier_sid: 'S3', supplier_name: 'Blake', supplier_status: 30, supplier_city: 'Paris' },
14
+ {supplier_sid: 'S4', supplier_name: 'Clark', supplier_status: 20, supplier_city: 'London' },
15
+ {supplier_sid: 'S5', supplier_name: 'Adams', supplier_status: 30, supplier_city: 'Athens' },
16
+ ]);
17
+ expect(prefixed.isEqual(expected)).to.be.true;
18
+ })
19
+
20
+ it('can exclude specific attributes', () => {
21
+ const prefixed = SUPPLIERS.prefix('s_', { except: ['sid'] });
22
+ const smith = prefixed.restrict({sid: 'S1'}).one();
23
+ expect(smith).to.have.property('sid', 'S1');
24
+ expect(smith).to.have.property('s_name', 'Smith');
25
+ expect(smith).to.have.property('s_city', 'London');
26
+ })
27
+
28
+ ///
29
+
30
+ it('can be used standalone', () => {
31
+ const input = SUPPLIERS.toArray();
32
+ const res = prefix(input, 'x_');
33
+ const expected = SUPPLIERS.prefix('x_');
34
+ expect(isEqual(res, expected)).to.be.true;
35
+ })
36
+
37
+ });
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Bmg } from 'src';
3
+ import { SUPPLIERS } from 'tests/fixtures';
4
+ import { project , isEqual } from 'src/operators';
5
+
6
+ describe('.project', () => {
7
+
8
+ it('keeps only specified attributes', () => {
9
+ const result = SUPPLIERS.project(['name', 'city']);
10
+ const expected = Bmg([
11
+ { name: 'Smith', city: 'London' },
12
+ { name: 'Jones', city: 'Paris' },
13
+ { name: 'Blake', city: 'Paris' },
14
+ { name: 'Clark', city: 'London' },
15
+ { name: 'Adams', city: 'Athens' },
16
+ ]);
17
+ expect(result.isEqual(expected)).to.be.true;
18
+ })
19
+
20
+ it('removes duplicates (set semantics)', () => {
21
+ // Projecting to 'city' only should yield 3 unique cities, not 5 tuples
22
+ const result = SUPPLIERS.project(['city']);
23
+ const expected = Bmg([
24
+ { city: 'London' },
25
+ { city: 'Paris' },
26
+ { city: 'Athens' },
27
+ ]);
28
+ expect(result.isEqual(expected)).to.be.true;
29
+ })
30
+
31
+ it('ignores missing attributes', () => {
32
+ // @ts-expect-error - testing runtime behavior with invalid attribute
33
+ const result = SUPPLIERS.project(['name', 'nonexistent']);
34
+ const smith = result.restrict({ name: 'Smith' }).one();
35
+ expect(smith).to.have.property('name');
36
+ expect(smith).to.not.have.property('nonexistent');
37
+ })
38
+
39
+ ///
40
+
41
+ it('can be used standalone', () => {
42
+ const input = SUPPLIERS.toArray();
43
+ const res = project(input, ['name', 'city']);
44
+ const expected = SUPPLIERS.project(['name', 'city']);
45
+ expect(isEqual(res, expected)).to.be.true;
46
+ })
47
+
48
+ });