@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,240 @@
1
+ import { OperationalOperand, Relation, RelationOperand, Renaming, RenamingFunc, Tuple, JoinKeys, AttrName } from "@/types";
2
+ import { MemoryRelation } from '@/Relation';
3
+ import { isRelation } from "./isRelation";
4
+
5
+ const valueKey = (value: unknown): unknown => {
6
+ if (isRelation(value)) {
7
+ // For nested relations, convert to sorted array of tuple keys for comparison
8
+ const tuples = (value as Relation).toArray();
9
+ const keys = tuples.map(t => tupleKey(t)).sort();
10
+ return keys;
11
+ }
12
+ return value;
13
+ }
14
+
15
+ /**
16
+ * Generates a unique string key for a tuple, used for equality comparison and deduplication.
17
+ * Handles nested relations by converting them to sorted tuple keys.
18
+ *
19
+ * @example
20
+ * tupleKey({ name: 'Alice', age: 30 })
21
+ * // => '[["age",30],["name","Alice"]]'
22
+ *
23
+ * @example
24
+ * tupleKey({ id: 1, items: Bmg([{ x: 1 }, { x: 2 }]) })
25
+ * // => '[["id",1],["items",[...]]]' (nested relation converted to sorted keys)
26
+ */
27
+ export const tupleKey = (tuple: Tuple): string => {
28
+ const entries = Object.entries(tuple).map(([k, v]) => [k, valueKey(v)]);
29
+ return JSON.stringify(entries.sort(([a], [b]) => (a as string).localeCompare(b as string)));
30
+ }
31
+
32
+ /**
33
+ * Removes duplicate tuples from an array, preserving order of first occurrence.
34
+ * Uses tupleKey() for equality comparison.
35
+ *
36
+ * @example
37
+ * deduplicate([
38
+ * { id: 1, name: 'Alice' },
39
+ * { id: 2, name: 'Bob' },
40
+ * { id: 1, name: 'Alice' }, // duplicate
41
+ * ])
42
+ * // => [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]
43
+ */
44
+ export const deduplicate = (tuples: Tuple[]): Tuple[] => {
45
+ const seen = new Set<string>();
46
+ const result: Tuple[] = [];
47
+ for (const tuple of tuples) {
48
+ const key = tupleKey(tuple);
49
+ if (!seen.has(key)) {
50
+ seen.add(key);
51
+ result.push(tuple);
52
+ }
53
+ }
54
+ return result;
55
+ }
56
+
57
+ /**
58
+ * Converts a RelationOperand (Relation or Tuple[]) to an OperationalOperand
59
+ * that provides a uniform interface for iteration and output.
60
+ *
61
+ * @example
62
+ * // With array input, output remains array
63
+ * const op = toOperationalOperand([{ id: 1 }]);
64
+ * [...op.tuples()]; // => [{ id: 1 }]
65
+ * op.output([{ id: 2 }]); // => [{ id: 2 }]
66
+ *
67
+ * @example
68
+ * // With Relation input, output is a new Relation
69
+ * const op = toOperationalOperand(Bmg([{ id: 1 }]));
70
+ * [...op.tuples()]; // => [{ id: 1 }]
71
+ * op.output([{ id: 2 }]); // => Bmg([{ id: 2 }])
72
+ */
73
+ export const toOperationalOperand = (operand: RelationOperand): OperationalOperand => {
74
+ if (Array.isArray(operand)) {
75
+ return {
76
+ tuples: () => operand,
77
+ output: (tuples) => tuples,
78
+ };
79
+ } else if (isRelation(operand)) {
80
+ return {
81
+ tuples: () => (operand as Relation).toArray(),
82
+ output: (tuples) => new MemoryRelation(tuples),
83
+ };
84
+ } else {
85
+ throw `Unable to iterate ${operand}`
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Converts a Renaming (object or function) to a RenamingFunc.
91
+ *
92
+ * @example
93
+ * // Object renaming
94
+ * const fn = toRenamingFunc({ name: 'fullName', age: 'years' });
95
+ * fn('name'); // => 'fullName'
96
+ * fn('age'); // => 'years'
97
+ * fn('other'); // => 'other' (unchanged)
98
+ *
99
+ * @example
100
+ * // Function renaming (passed through)
101
+ * const fn = toRenamingFunc(attr => attr.toUpperCase());
102
+ * fn('name'); // => 'NAME'
103
+ */
104
+ export const toRenamingFunc = (renaming: Renaming): RenamingFunc => {
105
+ if (typeof(renaming) === 'function') {
106
+ return renaming;
107
+ } else {
108
+ return (attr) => renaming[attr] || attr;
109
+ }
110
+ }
111
+
112
+ export const error = (msg: string) => {
113
+ throw(msg);
114
+ }
115
+
116
+ // Join helpers
117
+
118
+ /**
119
+ * Finds attribute names that exist in both left and right tuple arrays.
120
+ * Used for natural joins when no explicit keys are provided.
121
+ *
122
+ * @example
123
+ * const left = [{ id: 1, name: 'Alice', city: 'NYC' }];
124
+ * const right = [{ city: 'NYC', country: 'USA' }];
125
+ * getCommonAttrs(left, right);
126
+ * // => ['city']
127
+ *
128
+ * @example
129
+ * const left = [{ a: 1, b: 2 }];
130
+ * const right = [{ b: 2, c: 3 }];
131
+ * getCommonAttrs(left, right);
132
+ * // => ['b']
133
+ */
134
+ export const getCommonAttrs = (left: Tuple[], right: Tuple[]): AttrName[] => {
135
+ if (left.length === 0 || right.length === 0) return [];
136
+ const leftAttrs = new Set(Object.keys(left[0]));
137
+ const rightAttrs = Object.keys(right[0]);
138
+ return rightAttrs.filter(attr => leftAttrs.has(attr));
139
+ }
140
+
141
+ /**
142
+ * Normalizes JoinKeys to a Record<AttrName, AttrName> mapping left attrs to right attrs.
143
+ *
144
+ * @example
145
+ * // undefined => use common attributes
146
+ * normalizeKeys(undefined, [{ id: 1, city: 'NYC' }], [{ city: 'NYC' }]);
147
+ * // => { city: 'city' }
148
+ *
149
+ * @example
150
+ * // Array of common attribute names
151
+ * normalizeKeys(['city', 'country'], leftTuples, rightTuples);
152
+ * // => { city: 'city', country: 'country' }
153
+ *
154
+ * @example
155
+ * // Object mapping left attr to right attr
156
+ * normalizeKeys({ city: 'location' }, leftTuples, rightTuples);
157
+ * // => { city: 'location' }
158
+ */
159
+ export const normalizeKeys = (keys: JoinKeys | undefined, leftTuples: Tuple[], rightTuples: Tuple[]): Record<AttrName, AttrName> => {
160
+ if (!keys) {
161
+ const common = getCommonAttrs(leftTuples, rightTuples);
162
+ return common.reduce((acc, attr) => {
163
+ acc[attr] = attr;
164
+ return acc;
165
+ }, {} as Record<AttrName, AttrName>);
166
+ }
167
+ if (Array.isArray(keys)) {
168
+ return keys.reduce((acc, attr) => {
169
+ acc[attr] = attr;
170
+ return acc;
171
+ }, {} as Record<AttrName, AttrName>);
172
+ }
173
+ return keys;
174
+ }
175
+
176
+ /**
177
+ * Checks if two tuples match on the specified key mapping.
178
+ *
179
+ * @example
180
+ * const keyMap = { city: 'location' };
181
+ * tuplesMatch({ id: 1, city: 'NYC' }, { location: 'NYC', pop: 8 }, keyMap);
182
+ * // => true (left.city === right.location)
183
+ *
184
+ * @example
185
+ * const keyMap = { city: 'city' };
186
+ * tuplesMatch({ city: 'NYC' }, { city: 'LA' }, keyMap);
187
+ * // => false
188
+ */
189
+ export const tuplesMatch = (left: Tuple, right: Tuple, keyMap: Record<AttrName, AttrName>): boolean => {
190
+ for (const [leftAttr, rightAttr] of Object.entries(keyMap)) {
191
+ if (left[leftAttr] !== right[rightAttr]) return false;
192
+ }
193
+ return true;
194
+ }
195
+
196
+ /**
197
+ * Creates a string key from a tuple's join attributes for fast Set-based lookups.
198
+ * Used by matching/not_matching for efficient semi-join operations.
199
+ *
200
+ * @example
201
+ * const keyMap = { first: 'fname', last: 'lname' };
202
+ *
203
+ * // Left side uses left attr names (keys of keyMap)
204
+ * matchKey({ id: 1, first: 'John', last: 'Doe' }, keyMap, 'left');
205
+ * // => '"John"|"Doe"'
206
+ *
207
+ * // Right side uses right attr names (values of keyMap)
208
+ * matchKey({ fname: 'John', lname: 'Doe', age: 30 }, keyMap, 'right');
209
+ * // => '"John"|"Doe"'
210
+ */
211
+ export const matchKey = (tuple: Tuple, keyMap: Record<AttrName, AttrName>, side: 'left' | 'right'): string => {
212
+ const attrs = side === 'left' ? Object.keys(keyMap) : Object.values(keyMap);
213
+ const values = attrs.map(attr => JSON.stringify(tuple[attr]));
214
+ return values.join('|');
215
+ }
216
+
217
+ /**
218
+ * Removes join key attributes from a right tuple when merging.
219
+ * Used to avoid duplicate columns in join results.
220
+ *
221
+ * @example
222
+ * const keyMap = { city: 'location' };
223
+ * projectOutKeys({ location: 'NYC', country: 'USA', pop: 8 }, keyMap);
224
+ * // => { country: 'USA', pop: 8 } (location removed)
225
+ *
226
+ * @example
227
+ * const keyMap = { a: 'a', b: 'b' };
228
+ * projectOutKeys({ a: 1, b: 2, c: 3 }, keyMap);
229
+ * // => { c: 3 } (a and b removed)
230
+ */
231
+ export const projectOutKeys = (tuple: Tuple, keyMap: Record<AttrName, AttrName>): Tuple => {
232
+ const rightKeys = new Set(Object.values(keyMap));
233
+ const result: Tuple = {};
234
+ for (const [attr, value] of Object.entries(tuple)) {
235
+ if (!rightKeys.has(attr)) {
236
+ result[attr] = value;
237
+ }
238
+ }
239
+ return result;
240
+ }
@@ -0,0 +1,19 @@
1
+ import { RelationOperand, AttrName, Tuple } from "../types";
2
+ import { toOperationalOperand, deduplicate } from "./_helpers";
3
+
4
+ export const allbut = (operand: RelationOperand, attrs: AttrName[]): RelationOperand => {
5
+ const op = toOperationalOperand(operand);
6
+ const iterable = op.tuples();
7
+ const excluded = new Set(attrs);
8
+ const result: Tuple[] = [];
9
+ for (const tuple of iterable) {
10
+ const projected = Object.keys(tuple).reduce((memo, attr) => {
11
+ if (!excluded.has(attr)) {
12
+ memo[attr] = tuple[attr];
13
+ }
14
+ return memo;
15
+ }, {} as Tuple);
16
+ result.push(projected);
17
+ }
18
+ return op.output(deduplicate(result));
19
+ }
@@ -0,0 +1,26 @@
1
+ import { AutowrapOptions, RelationOperand, Tuple } from "../types";
2
+ import { toOperationalOperand } from "./_helpers";
3
+
4
+ export const autowrap = (operand: RelationOperand, options?: AutowrapOptions): RelationOperand => {
5
+ const sep = options?.separator ?? '_';
6
+ const op = toOperationalOperand(operand);
7
+ const iterable = op.tuples();
8
+ const result: Tuple[] = [];
9
+
10
+ for (const tuple of iterable) {
11
+ const wrapped: Tuple = {};
12
+ for (const [attr, value] of Object.entries(tuple)) {
13
+ const parts = attr.split(sep);
14
+ if (parts.length === 1) {
15
+ wrapped[attr] = value;
16
+ } else {
17
+ const [prefix, ...rest] = parts;
18
+ wrapped[prefix] = wrapped[prefix] ?? {};
19
+ (wrapped[prefix] as Tuple)[rest.join(sep)] = value;
20
+ }
21
+ }
22
+ result.push(wrapped);
23
+ }
24
+
25
+ return op.output(result);
26
+ }
@@ -0,0 +1,12 @@
1
+ import { RelationOperand, Tuple } from "../types";
2
+ import { toOperationalOperand } from "./_helpers";
3
+
4
+ export const constants = (operand: RelationOperand, consts: Tuple): RelationOperand => {
5
+ const op = toOperationalOperand(operand);
6
+ const iterable = op.tuples();
7
+ const result: Tuple[] = [];
8
+ for (const tuple of iterable) {
9
+ result.push({ ...tuple, ...consts });
10
+ }
11
+ return op.output(result)
12
+ }
@@ -0,0 +1,20 @@
1
+ import { RelationOperand, Tuple } from "../types";
2
+ import { toOperationalOperand, deduplicate } from "./_helpers";
3
+
4
+ export const cross_product = (left: RelationOperand, right: RelationOperand): RelationOperand => {
5
+ const opLeft = toOperationalOperand(left);
6
+ const opRight = toOperationalOperand(right);
7
+ const leftTuples = [...opLeft.tuples()];
8
+ const rightTuples = [...opRight.tuples()];
9
+ const result: Tuple[] = [];
10
+
11
+ for (const l of leftTuples) {
12
+ for (const r of rightTuples) {
13
+ result.push({ ...r, ...l });
14
+ }
15
+ }
16
+
17
+ return opLeft.output(deduplicate(result));
18
+ }
19
+
20
+ export { cross_product as cross_join };
@@ -0,0 +1,14 @@
1
+ import { toPredicateFunc } from "../support/toPredicateFunc";
2
+ import { RelationOperand, Predicate, Tuple } from "../types";
3
+ import { toOperationalOperand } from "./_helpers";
4
+
5
+ export const exclude = (operand: RelationOperand, p: Predicate): RelationOperand => {
6
+ const op = toOperationalOperand(operand);
7
+ const iterable = op.tuples();
8
+ const f = toPredicateFunc(p)
9
+ const kept: Tuple[] = [];
10
+ for (const tuple of iterable) {
11
+ if (!f(tuple)) kept.push(tuple);
12
+ }
13
+ return op.output(kept)
14
+ }
@@ -0,0 +1,20 @@
1
+ import { RelationOperand, Extension, Tuple } from "../types";
2
+ import { toOperationalOperand } from "./_helpers";
3
+
4
+ export const extend = (operand: RelationOperand, extension: Extension): RelationOperand => {
5
+ const op = toOperationalOperand(operand);
6
+ const iterable = op.tuples();
7
+ const result: Tuple[] = [];
8
+ for (const tuple of iterable) {
9
+ const extended = { ...tuple };
10
+ for (const [attr, spec] of Object.entries(extension)) {
11
+ if (typeof spec === 'function') {
12
+ extended[attr] = spec(tuple);
13
+ } else {
14
+ extended[attr] = tuple[spec];
15
+ }
16
+ }
17
+ result.push(extended);
18
+ }
19
+ return op.output(result);
20
+ }
@@ -0,0 +1,53 @@
1
+ import { RelationOperand, AttrName, Tuple } from "../types";
2
+ import { toOperationalOperand } from "./_helpers";
3
+ import { MemoryRelation } from "@/Relation";
4
+
5
+ const groupKey = (tuple: Tuple, byAttrs: AttrName[]): string => {
6
+ const keyParts = byAttrs.map(attr => JSON.stringify(tuple[attr]));
7
+ return keyParts.join('|');
8
+ }
9
+
10
+ const pickAttrs = (tuple: Tuple, attrs: AttrName[]): Tuple => {
11
+ return attrs.reduce((acc, attr) => {
12
+ acc[attr] = tuple[attr];
13
+ return acc;
14
+ }, {} as Tuple);
15
+ }
16
+
17
+ export const group = (operand: RelationOperand, attrs: AttrName[], as: AttrName): RelationOperand => {
18
+ const op = toOperationalOperand(operand);
19
+ const tuples = [...op.tuples()];
20
+
21
+ if (tuples.length === 0) {
22
+ return op.output([]);
23
+ }
24
+
25
+ // Determine which attributes to keep at the top level (all except grouped ones)
26
+ const allAttrs = Object.keys(tuples[0]);
27
+ const groupedSet = new Set(attrs);
28
+ const byAttrs = allAttrs.filter(a => !groupedSet.has(a));
29
+
30
+ // Group tuples
31
+ const groups = new Map<string, { base: Tuple, nested: Tuple[] }>();
32
+ for (const tuple of tuples) {
33
+ const key = groupKey(tuple, byAttrs);
34
+ if (!groups.has(key)) {
35
+ groups.set(key, {
36
+ base: pickAttrs(tuple, byAttrs),
37
+ nested: []
38
+ });
39
+ }
40
+ groups.get(key)!.nested.push(pickAttrs(tuple, attrs));
41
+ }
42
+
43
+ // Build result with nested relations
44
+ const result: Tuple[] = [];
45
+ for (const { base, nested } of groups.values()) {
46
+ result.push({
47
+ ...base,
48
+ [as]: new MemoryRelation(nested)
49
+ });
50
+ }
51
+
52
+ return op.output(result);
53
+ }
@@ -0,0 +1,27 @@
1
+ import { RelationOperand, JoinKeys, Tuple, AttrName } from "../types";
2
+ import { toOperationalOperand, normalizeKeys, tuplesMatch, projectOutKeys } from "./_helpers";
3
+ import { MemoryRelation } from "@/Relation";
4
+
5
+ export const image = (left: RelationOperand, right: RelationOperand, as: AttrName, keys?: JoinKeys): RelationOperand => {
6
+ const opLeft = toOperationalOperand(left);
7
+ const opRight = toOperationalOperand(right);
8
+ const leftTuples = [...opLeft.tuples()];
9
+ const rightTuples = [...opRight.tuples()];
10
+ const keyMap = normalizeKeys(keys, leftTuples, rightTuples);
11
+ const result: Tuple[] = [];
12
+
13
+ for (const leftTuple of leftTuples) {
14
+ const matches: Tuple[] = [];
15
+ for (const rightTuple of rightTuples) {
16
+ if (tuplesMatch(leftTuple, rightTuple, keyMap)) {
17
+ matches.push(projectOutKeys(rightTuple, keyMap));
18
+ }
19
+ }
20
+ result.push({
21
+ ...leftTuple,
22
+ [as]: new MemoryRelation(matches)
23
+ });
24
+ }
25
+
26
+ return opLeft.output(result);
27
+ }
@@ -0,0 +1,31 @@
1
+ export * from './restrict'
2
+ export * from './where'
3
+ export * from './exclude'
4
+ export * from './constants'
5
+ export * from './rename'
6
+ export * from './prefix'
7
+ export * from './suffix'
8
+ export * from './project'
9
+ export * from './allbut'
10
+ export * from './extend'
11
+ export * from './union'
12
+ export * from './minus'
13
+ export * from './intersect'
14
+ export * from './matching'
15
+ export * from './not_matching'
16
+ export * from './join'
17
+ export * from './left_join'
18
+ export * from './cross_product'
19
+ export * from './image'
20
+ export * from './summarize'
21
+ export * from './group'
22
+ export * from './ungroup'
23
+ export * from './wrap'
24
+ export * from './unwrap'
25
+ export * from './autowrap'
26
+ export * from './transform'
27
+
28
+ export * from './isRelation'
29
+ export * from './isEqual'
30
+ export * from './one'
31
+ export * from './yByX'
@@ -0,0 +1,24 @@
1
+ import { RelationOperand, Tuple } from "../types";
2
+ import { toOperationalOperand, tupleKey } from "./_helpers";
3
+
4
+ export const intersect = (left: RelationOperand, right: RelationOperand): RelationOperand => {
5
+ const opLeft = toOperationalOperand(left);
6
+ const opRight = toOperationalOperand(right);
7
+
8
+ const rightKeys = new Set<string>();
9
+ for (const tuple of opRight.tuples()) {
10
+ rightKeys.add(tupleKey(tuple));
11
+ }
12
+
13
+ const seen = new Set<string>();
14
+ const result: Tuple[] = [];
15
+ for (const tuple of opLeft.tuples()) {
16
+ const key = tupleKey(tuple);
17
+ if (rightKeys.has(key) && !seen.has(key)) {
18
+ seen.add(key);
19
+ result.push(tuple);
20
+ }
21
+ }
22
+
23
+ return opLeft.output(result);
24
+ }
@@ -0,0 +1,29 @@
1
+ import { RelationOperand } from "../types";
2
+ import { toOperationalOperand, tupleKey } from "./_helpers";
3
+
4
+ export const isEqual = (left: RelationOperand, right: RelationOperand): boolean => {
5
+ const opLeft = toOperationalOperand(left);
6
+ const opRight = toOperationalOperand(right);
7
+
8
+ const leftKeys = new Set<string>();
9
+ for (const tuple of opLeft.tuples()) {
10
+ leftKeys.add(tupleKey(tuple));
11
+ }
12
+
13
+ const rightKeys = new Set<string>();
14
+ for (const tuple of opRight.tuples()) {
15
+ rightKeys.add(tupleKey(tuple));
16
+ }
17
+
18
+ if (leftKeys.size !== rightKeys.size) {
19
+ return false;
20
+ }
21
+
22
+ for (const key of leftKeys) {
23
+ if (!rightKeys.has(key)) {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ return true;
29
+ }
@@ -0,0 +1,5 @@
1
+ import { MemoryRelation } from "@/Relation";
2
+
3
+ export const isRelation = (op) => {
4
+ return op != null && op.constructor === MemoryRelation;
5
+ }
@@ -0,0 +1,25 @@
1
+ import { RelationOperand, JoinKeys, Tuple, AttrName } from "../types";
2
+ import { toOperationalOperand, normalizeKeys, tuplesMatch, projectOutKeys } from "./_helpers";
3
+
4
+ const mergeTuples = (left: Tuple, right: Tuple, keyMap: Record<AttrName, AttrName>): Tuple => {
5
+ return { ...left, ...projectOutKeys(right, keyMap) };
6
+ }
7
+
8
+ export const join = (left: RelationOperand, right: RelationOperand, keys?: JoinKeys): RelationOperand => {
9
+ const opLeft = toOperationalOperand(left);
10
+ const opRight = toOperationalOperand(right);
11
+ const leftTuples = [...opLeft.tuples()];
12
+ const rightTuples = [...opRight.tuples()];
13
+ const keyMap = normalizeKeys(keys, leftTuples, rightTuples);
14
+ const result: Tuple[] = [];
15
+
16
+ for (const leftTuple of leftTuples) {
17
+ for (const rightTuple of rightTuples) {
18
+ if (tuplesMatch(leftTuple, rightTuple, keyMap)) {
19
+ result.push(mergeTuples(leftTuple, rightTuple, keyMap));
20
+ }
21
+ }
22
+ }
23
+
24
+ return opLeft.output(result);
25
+ }
@@ -0,0 +1,41 @@
1
+ import { RelationOperand, JoinKeys, Tuple, AttrName } from "../types";
2
+ import { toOperationalOperand, normalizeKeys, tuplesMatch } from "./_helpers";
3
+
4
+ const getRightAttrs = (rightTuples: Tuple[], keyMap: Record<AttrName, AttrName>): AttrName[] => {
5
+ if (rightTuples.length === 0) return [];
6
+ const rightKeys = new Set(Object.values(keyMap));
7
+ return Object.keys(rightTuples[0]).filter(attr => !rightKeys.has(attr));
8
+ }
9
+
10
+ const mergeTuples = (left: Tuple, right: Tuple | null, rightAttrs: AttrName[]): Tuple => {
11
+ const result = { ...left };
12
+ for (const attr of rightAttrs) {
13
+ result[attr] = right ? right[attr] : null;
14
+ }
15
+ return result;
16
+ }
17
+
18
+ export const left_join = (left: RelationOperand, right: RelationOperand, keys?: JoinKeys): RelationOperand => {
19
+ const opLeft = toOperationalOperand(left);
20
+ const opRight = toOperationalOperand(right);
21
+ const leftTuples = [...opLeft.tuples()];
22
+ const rightTuples = [...opRight.tuples()];
23
+ const keyMap = normalizeKeys(keys, leftTuples, rightTuples);
24
+ const rightAttrs = getRightAttrs(rightTuples, keyMap);
25
+ const result: Tuple[] = [];
26
+
27
+ for (const leftTuple of leftTuples) {
28
+ let matched = false;
29
+ for (const rightTuple of rightTuples) {
30
+ if (tuplesMatch(leftTuple, rightTuple, keyMap)) {
31
+ result.push(mergeTuples(leftTuple, rightTuple, rightAttrs));
32
+ matched = true;
33
+ }
34
+ }
35
+ if (!matched) {
36
+ result.push(mergeTuples(leftTuple, null, rightAttrs));
37
+ }
38
+ }
39
+
40
+ return opLeft.output(result);
41
+ }
@@ -0,0 +1,24 @@
1
+ import { RelationOperand, JoinKeys, Tuple } from "../types";
2
+ import { toOperationalOperand, normalizeKeys, matchKey } from "./_helpers";
3
+
4
+ export const matching = (left: RelationOperand, right: RelationOperand, keys?: JoinKeys): RelationOperand => {
5
+ const opLeft = toOperationalOperand(left);
6
+ const opRight = toOperationalOperand(right);
7
+ const leftTuples = [...opLeft.tuples()];
8
+ const rightTuples = [...opRight.tuples()];
9
+ const keyMap = normalizeKeys(keys, leftTuples, rightTuples);
10
+
11
+ const rightKeys = new Set<string>();
12
+ for (const tuple of rightTuples) {
13
+ rightKeys.add(matchKey(tuple, keyMap, 'right'));
14
+ }
15
+
16
+ const result: Tuple[] = [];
17
+ for (const tuple of leftTuples) {
18
+ if (rightKeys.has(matchKey(tuple, keyMap, 'left'))) {
19
+ result.push(tuple);
20
+ }
21
+ }
22
+
23
+ return opLeft.output(result);
24
+ }
@@ -0,0 +1,24 @@
1
+ import { RelationOperand, Tuple } from "../types";
2
+ import { toOperationalOperand, tupleKey } from "./_helpers";
3
+
4
+ export const minus = (left: RelationOperand, right: RelationOperand): RelationOperand => {
5
+ const opLeft = toOperationalOperand(left);
6
+ const opRight = toOperationalOperand(right);
7
+
8
+ const rightKeys = new Set<string>();
9
+ for (const tuple of opRight.tuples()) {
10
+ rightKeys.add(tupleKey(tuple));
11
+ }
12
+
13
+ const seen = new Set<string>();
14
+ const result: Tuple[] = [];
15
+ for (const tuple of opLeft.tuples()) {
16
+ const key = tupleKey(tuple);
17
+ if (!rightKeys.has(key) && !seen.has(key)) {
18
+ seen.add(key);
19
+ result.push(tuple);
20
+ }
21
+ }
22
+
23
+ return opLeft.output(result);
24
+ }
@@ -0,0 +1,24 @@
1
+ import { RelationOperand, JoinKeys, Tuple } from "../types";
2
+ import { toOperationalOperand, normalizeKeys, matchKey } from "./_helpers";
3
+
4
+ export const not_matching = (left: RelationOperand, right: RelationOperand, keys?: JoinKeys): RelationOperand => {
5
+ const opLeft = toOperationalOperand(left);
6
+ const opRight = toOperationalOperand(right);
7
+ const leftTuples = [...opLeft.tuples()];
8
+ const rightTuples = [...opRight.tuples()];
9
+ const keyMap = normalizeKeys(keys, leftTuples, rightTuples);
10
+
11
+ const rightKeys = new Set<string>();
12
+ for (const tuple of rightTuples) {
13
+ rightKeys.add(matchKey(tuple, keyMap, 'right'));
14
+ }
15
+
16
+ const result: Tuple[] = [];
17
+ for (const tuple of leftTuples) {
18
+ if (!rightKeys.has(matchKey(tuple, keyMap, 'left'))) {
19
+ result.push(tuple);
20
+ }
21
+ }
22
+
23
+ return opLeft.output(result);
24
+ }