@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.
- package/.claude/safe-setup/.env.example +3 -0
- package/.claude/safe-setup/Dockerfile.claude +36 -0
- package/.claude/safe-setup/HACKING.md +63 -0
- package/.claude/safe-setup/Makefile +22 -0
- package/.claude/safe-setup/docker-compose.yml +18 -0
- package/.claude/safe-setup/entrypoint.sh +13 -0
- package/.claude/settings.local.json +9 -0
- package/.claude/typescript-annotations.md +273 -0
- package/.github/workflows/test.yml +26 -0
- package/CLAUDE.md +48 -0
- package/Makefile +2 -0
- package/README.md +170 -0
- package/example/README.md +22 -0
- package/example/index.ts +316 -0
- package/example/package.json +16 -0
- package/example/tsconfig.json +11 -0
- package/package.json +34 -0
- package/src/Relation/Memory.ts +213 -0
- package/src/Relation/index.ts +1 -0
- package/src/index.ts +31 -0
- package/src/operators/_helpers.ts +240 -0
- package/src/operators/allbut.ts +19 -0
- package/src/operators/autowrap.ts +26 -0
- package/src/operators/constants.ts +12 -0
- package/src/operators/cross_product.ts +20 -0
- package/src/operators/exclude.ts +14 -0
- package/src/operators/extend.ts +20 -0
- package/src/operators/group.ts +53 -0
- package/src/operators/image.ts +27 -0
- package/src/operators/index.ts +31 -0
- package/src/operators/intersect.ts +24 -0
- package/src/operators/isEqual.ts +29 -0
- package/src/operators/isRelation.ts +5 -0
- package/src/operators/join.ts +25 -0
- package/src/operators/left_join.ts +41 -0
- package/src/operators/matching.ts +24 -0
- package/src/operators/minus.ts +24 -0
- package/src/operators/not_matching.ts +24 -0
- package/src/operators/one.ts +17 -0
- package/src/operators/prefix.ts +7 -0
- package/src/operators/project.ts +18 -0
- package/src/operators/rename.ts +17 -0
- package/src/operators/restrict.ts +14 -0
- package/src/operators/suffix.ts +7 -0
- package/src/operators/summarize.ts +85 -0
- package/src/operators/transform.ts +40 -0
- package/src/operators/ungroup.ts +41 -0
- package/src/operators/union.ts +27 -0
- package/src/operators/unwrap.ts +29 -0
- package/src/operators/where.ts +1 -0
- package/src/operators/wrap.ts +29 -0
- package/src/operators/yByX.ts +12 -0
- package/src/support/toPredicateFunc.ts +12 -0
- package/src/types.ts +178 -0
- package/src/utility-types.ts +77 -0
- package/tests/bmg.test.ts +16 -0
- package/tests/fixtures.ts +9 -0
- package/tests/operators/allbut.test.ts +51 -0
- package/tests/operators/autowrap.test.ts +82 -0
- package/tests/operators/constants.test.ts +37 -0
- package/tests/operators/cross_product.test.ts +90 -0
- package/tests/operators/exclude.test.ts +43 -0
- package/tests/operators/extend.test.ts +45 -0
- package/tests/operators/group.test.ts +69 -0
- package/tests/operators/image.test.ts +152 -0
- package/tests/operators/intersect.test.ts +53 -0
- package/tests/operators/isEqual.test.ts +111 -0
- package/tests/operators/join.test.ts +116 -0
- package/tests/operators/left_join.test.ts +116 -0
- package/tests/operators/matching.test.ts +91 -0
- package/tests/operators/minus.test.ts +47 -0
- package/tests/operators/not_matching.test.ts +104 -0
- package/tests/operators/one.test.ts +19 -0
- package/tests/operators/prefix.test.ts +37 -0
- package/tests/operators/project.test.ts +48 -0
- package/tests/operators/rename.test.ts +39 -0
- package/tests/operators/restrict.test.ts +27 -0
- package/tests/operators/suffix.test.ts +37 -0
- package/tests/operators/summarize.test.ts +109 -0
- package/tests/operators/transform.test.ts +94 -0
- package/tests/operators/ungroup.test.ts +67 -0
- package/tests/operators/union.test.ts +51 -0
- package/tests/operators/unwrap.test.ts +50 -0
- package/tests/operators/where.test.ts +33 -0
- package/tests/operators/wrap.test.ts +54 -0
- package/tests/operators/yByX.test.ts +32 -0
- package/tests/types/relation.test.ts +296 -0
- package/tsconfig.json +37 -0
- package/tsconfig.node.json +9 -0
- 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,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
|
+
}
|