@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,17 @@
1
+ import { RelationOperand, Tuple } from "../types";
2
+ import { toOperationalOperand, error } from "./_helpers";
3
+
4
+ export const one = (operand: RelationOperand): Tuple => {
5
+ const op = toOperationalOperand(operand);
6
+ const iterable = op.tuples();
7
+ let tuple;
8
+ for (const t of iterable) {
9
+ if (tuple) {
10
+ return error('More than one tuple found');
11
+ } else {
12
+ tuple = t;
13
+ }
14
+ }
15
+ if (tuple) return tuple;
16
+ return error('Relation is empty');
17
+ }
@@ -0,0 +1,7 @@
1
+ import { PrefixOptions, RelationOperand } from "../types";
2
+ import { rename } from "./rename";
3
+
4
+ export const prefix = (operand: RelationOperand, pfx: string, options?: PrefixOptions): RelationOperand => {
5
+ const except = new Set(options?.except ?? []);
6
+ return rename(operand, (attr) => except.has(attr) ? attr : `${pfx}${attr}`);
7
+ }
@@ -0,0 +1,18 @@
1
+ import { RelationOperand, AttrName, Tuple } from "../types";
2
+ import { toOperationalOperand, deduplicate } from "./_helpers";
3
+
4
+ export const project = (operand: RelationOperand, attrs: AttrName[]): RelationOperand => {
5
+ const op = toOperationalOperand(operand);
6
+ const iterable = op.tuples();
7
+ const result: Tuple[] = [];
8
+ for (const tuple of iterable) {
9
+ const projected = attrs.reduce((memo, attr) => {
10
+ if (attr in tuple) {
11
+ memo[attr] = tuple[attr];
12
+ }
13
+ return memo;
14
+ }, {} as Tuple);
15
+ result.push(projected);
16
+ }
17
+ return op.output(deduplicate(result));
18
+ }
@@ -0,0 +1,17 @@
1
+ import { RelationOperand, Renaming, Tuple } from "../types";
2
+ import { toOperationalOperand, toRenamingFunc } from "./_helpers";
3
+
4
+ export const rename = (operand: RelationOperand, renaming: Renaming): RelationOperand => {
5
+ const op = toOperationalOperand(operand);
6
+ const iterable = op.tuples();
7
+ const renamingFunc = toRenamingFunc(renaming)
8
+ const result: Tuple[] = [];
9
+ for (const tuple of iterable) {
10
+ const renamed = Object.keys(tuple).reduce((memo, attr) => {
11
+ memo[renamingFunc(attr)] = tuple[attr];
12
+ return memo;
13
+ }, {})
14
+ result.push(renamed);
15
+ }
16
+ return op.output(result)
17
+ }
@@ -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 restrict = (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,7 @@
1
+ import { RelationOperand, SuffixOptions } from "../types";
2
+ import { rename } from "./rename";
3
+
4
+ export const suffix = (operand: RelationOperand, sfx: string, options?: SuffixOptions): RelationOperand => {
5
+ const except = new Set(options?.except ?? []);
6
+ return rename(operand, (attr) => except.has(attr) ? attr : `${attr}${sfx}`);
7
+ }
@@ -0,0 +1,85 @@
1
+ import { RelationOperand, AttrName, Tuple, Aggregator, Aggregators } from "../types";
2
+ import { toOperationalOperand } from "./_helpers";
3
+
4
+ const groupKey = (tuple: Tuple, by: AttrName[]): string => {
5
+ const keyParts = by.map(attr => JSON.stringify(tuple[attr]));
6
+ return keyParts.join('|');
7
+ }
8
+
9
+ const pickAttrs = (tuple: Tuple, attrs: AttrName[]): Tuple => {
10
+ return attrs.reduce((acc, attr) => {
11
+ acc[attr] = tuple[attr];
12
+ return acc;
13
+ }, {} as Tuple);
14
+ }
15
+
16
+ const applyAggregator = (tuples: Tuple[], agg: Aggregator): unknown => {
17
+ if (typeof agg === 'function') {
18
+ return agg(tuples);
19
+ }
20
+
21
+ const spec = typeof agg === 'string' ? { op: agg, attr: '' } : agg;
22
+ const { op, attr } = spec;
23
+
24
+ switch (op) {
25
+ case 'count':
26
+ return tuples.length;
27
+
28
+ case 'sum': {
29
+ return tuples.reduce((sum, t) => sum + (Number(t[attr]) || 0), 0);
30
+ }
31
+
32
+ case 'min': {
33
+ const values = tuples.map(t => t[attr]).filter(v => v !== undefined && v !== null);
34
+ return values.length > 0 ? Math.min(...values.map(Number)) : null;
35
+ }
36
+
37
+ case 'max': {
38
+ const values = tuples.map(t => t[attr]).filter(v => v !== undefined && v !== null);
39
+ return values.length > 0 ? Math.max(...values.map(Number)) : null;
40
+ }
41
+
42
+ case 'avg': {
43
+ const values = tuples.map(t => Number(t[attr])).filter(v => !isNaN(v));
44
+ return values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : null;
45
+ }
46
+
47
+ case 'collect': {
48
+ return tuples.map(t => t[attr]);
49
+ }
50
+
51
+ default:
52
+ throw new Error(`Unknown aggregator: ${op}`);
53
+ }
54
+ }
55
+
56
+ export const summarize = (
57
+ operand: RelationOperand,
58
+ by: AttrName[],
59
+ aggs: Aggregators
60
+ ): RelationOperand => {
61
+ const op = toOperationalOperand(operand);
62
+ const tuples = [...op.tuples()];
63
+
64
+ // Group tuples
65
+ const groups = new Map<string, Tuple[]>();
66
+ for (const tuple of tuples) {
67
+ const key = groupKey(tuple, by);
68
+ if (!groups.has(key)) {
69
+ groups.set(key, []);
70
+ }
71
+ groups.get(key)!.push(tuple);
72
+ }
73
+
74
+ // Apply aggregators to each group
75
+ const result: Tuple[] = [];
76
+ for (const groupTuples of groups.values()) {
77
+ const row: Tuple = pickAttrs(groupTuples[0], by);
78
+ for (const [resultAttr, agg] of Object.entries(aggs)) {
79
+ row[resultAttr] = applyAggregator(groupTuples, agg);
80
+ }
81
+ result.push(row);
82
+ }
83
+
84
+ return op.output(result);
85
+ }
@@ -0,0 +1,40 @@
1
+ import { RelationOperand, Transformation, Tuple } from "../types";
2
+ import { toOperationalOperand } from "./_helpers";
3
+
4
+ export const transform = (operand: RelationOperand, transformation: Transformation): RelationOperand => {
5
+ const op = toOperationalOperand(operand);
6
+ const iterable = op.tuples();
7
+ const result: Tuple[] = [];
8
+
9
+ for (const tuple of iterable) {
10
+ const transformed: Tuple = {};
11
+
12
+ for (const [attr, value] of Object.entries(tuple)) {
13
+ transformed[attr] = applyTransformation(value, attr, transformation);
14
+ }
15
+
16
+ result.push(transformed);
17
+ }
18
+
19
+ return op.output(result);
20
+ }
21
+
22
+ const applyTransformation = (value: unknown, attr: string, transformation: Transformation): unknown => {
23
+ if (typeof transformation === 'function') {
24
+ // Single function - apply to all values
25
+ return transformation(value);
26
+ } else if (Array.isArray(transformation)) {
27
+ // Array of functions - chain them
28
+ return transformation.reduce((v, fn) => fn(v), value);
29
+ } else {
30
+ // Object with attr-specific transformers
31
+ const fn = transformation[attr];
32
+ if (fn) {
33
+ if (Array.isArray(fn)) {
34
+ return fn.reduce((v, f) => f(v), value);
35
+ }
36
+ return fn(value);
37
+ }
38
+ return value;
39
+ }
40
+ }
@@ -0,0 +1,41 @@
1
+ import { RelationOperand, AttrName, Tuple, Relation } from "../types";
2
+ import { toOperationalOperand } from "./_helpers";
3
+ import { isRelation } from "./isRelation";
4
+
5
+ const toTupleArray = (value: unknown): Tuple[] => {
6
+ if (isRelation(value)) {
7
+ return (value as Relation).toArray();
8
+ }
9
+ if (Array.isArray(value)) {
10
+ return value;
11
+ }
12
+ throw new Error(`Value is not a relation or array`);
13
+ }
14
+
15
+ export const ungroup = (operand: RelationOperand, attr: AttrName): RelationOperand => {
16
+ const op = toOperationalOperand(operand);
17
+ const tuples = [...op.tuples()];
18
+ const result: Tuple[] = [];
19
+
20
+ for (const tuple of tuples) {
21
+ const nested = toTupleArray(tuple[attr]);
22
+
23
+ // Get base attributes (all except the grouped one)
24
+ const base: Tuple = {};
25
+ for (const [key, value] of Object.entries(tuple)) {
26
+ if (key !== attr) {
27
+ base[key] = value;
28
+ }
29
+ }
30
+
31
+ // Flatten each nested tuple
32
+ for (const nestedTuple of nested) {
33
+ result.push({
34
+ ...base,
35
+ ...nestedTuple
36
+ });
37
+ }
38
+ }
39
+
40
+ return op.output(result);
41
+ }
@@ -0,0 +1,27 @@
1
+ import { RelationOperand, Tuple } from "../types";
2
+ import { toOperationalOperand, tupleKey } from "./_helpers";
3
+
4
+ export const union = (left: RelationOperand, right: RelationOperand): RelationOperand => {
5
+ const opLeft = toOperationalOperand(left);
6
+ const opRight = toOperationalOperand(right);
7
+ const seen = new Set<string>();
8
+ const result: Tuple[] = [];
9
+
10
+ for (const tuple of opLeft.tuples()) {
11
+ const key = tupleKey(tuple);
12
+ if (!seen.has(key)) {
13
+ seen.add(key);
14
+ result.push(tuple);
15
+ }
16
+ }
17
+
18
+ for (const tuple of opRight.tuples()) {
19
+ const key = tupleKey(tuple);
20
+ if (!seen.has(key)) {
21
+ seen.add(key);
22
+ result.push(tuple);
23
+ }
24
+ }
25
+
26
+ return opLeft.output(result);
27
+ }
@@ -0,0 +1,29 @@
1
+ import { RelationOperand, AttrName, Tuple } from "../types";
2
+ import { toOperationalOperand } from "./_helpers";
3
+
4
+ export const unwrap = (operand: RelationOperand, attr: AttrName): RelationOperand => {
5
+ const op = toOperationalOperand(operand);
6
+ const iterable = op.tuples();
7
+ const result: Tuple[] = [];
8
+
9
+ for (const tuple of iterable) {
10
+ const wrapped = tuple[attr] as Tuple;
11
+ if (typeof wrapped !== 'object' || wrapped === null || Array.isArray(wrapped)) {
12
+ throw new Error(`Attribute '${attr}' is not a tuple (object)`);
13
+ }
14
+
15
+ const unwrapped: Tuple = {};
16
+ for (const [key, value] of Object.entries(tuple)) {
17
+ if (key !== attr) {
18
+ unwrapped[key] = value;
19
+ }
20
+ }
21
+
22
+ result.push({
23
+ ...unwrapped,
24
+ ...wrapped
25
+ });
26
+ }
27
+
28
+ return op.output(result);
29
+ }
@@ -0,0 +1 @@
1
+ export { restrict as where } from './restrict';
@@ -0,0 +1,29 @@
1
+ import { RelationOperand, AttrName, Tuple } from "../types";
2
+ import { toOperationalOperand } from "./_helpers";
3
+
4
+ export const wrap = (operand: RelationOperand, attrs: AttrName[], as: AttrName): RelationOperand => {
5
+ const op = toOperationalOperand(operand);
6
+ const iterable = op.tuples();
7
+ const wrappedSet = new Set(attrs);
8
+ const result: Tuple[] = [];
9
+
10
+ for (const tuple of iterable) {
11
+ const wrapped: Tuple = {};
12
+ const remaining: Tuple = {};
13
+
14
+ for (const [key, value] of Object.entries(tuple)) {
15
+ if (wrappedSet.has(key)) {
16
+ wrapped[key] = value;
17
+ } else {
18
+ remaining[key] = value;
19
+ }
20
+ }
21
+
22
+ result.push({
23
+ ...remaining,
24
+ [as]: wrapped
25
+ });
26
+ }
27
+
28
+ return op.output(result);
29
+ }
@@ -0,0 +1,12 @@
1
+ import { AttrName, RelationOperand, Tuple } from "../types";
2
+ import { toOperationalOperand } from "./_helpers";
3
+
4
+ export const yByX = (operand: RelationOperand, y: AttrName, x: AttrName): Tuple => {
5
+ const op = toOperationalOperand(operand);
6
+ const iterable = op.tuples();
7
+ const hash = {};
8
+ for (const tuple of iterable) {
9
+ hash[`${tuple[x]}`] = tuple[y];
10
+ }
11
+ return hash;
12
+ }
@@ -0,0 +1,12 @@
1
+ import { Predicate, PredicateFunc, Tuple } from '../types';
2
+
3
+ export const toPredicateFunc = (p: Predicate): PredicateFunc => {
4
+ if (typeof(p) === 'function') {
5
+ return p as PredicateFunc;
6
+ } else {
7
+ const expected = p as Tuple;
8
+ return (t: Tuple) => {
9
+ return Object.keys(expected).every(k => t[k] === expected[k])
10
+ }
11
+ }
12
+ }
package/src/types.ts ADDED
@@ -0,0 +1,178 @@
1
+ import type {
2
+ RenameMap, Renamed,
3
+ Prefixed, Suffixed,
4
+ Joined, LeftJoined,
5
+ Wrapped, Unwrapped,
6
+ AggregatorResults
7
+ } from './utility-types';
8
+
9
+ // ============================================================================
10
+ // Group/Ungroup Types (defined here due to Relation dependency)
11
+ // ============================================================================
12
+
13
+ /** Result of ungroup: remove relation attr, flatten its tuple type */
14
+ export type Ungrouped<T, K extends keyof T> =
15
+ T[K] extends Relation<infer N> ? Omit<T, K> & N : Omit<T, K>;
16
+
17
+ export type AttrName = string
18
+ export type Tuple = Record<AttrName, unknown>
19
+
20
+ export interface PrefixOptions {
21
+ except?: AttrName[]
22
+ }
23
+
24
+ export interface SuffixOptions {
25
+ except?: AttrName[]
26
+ }
27
+
28
+ export interface AutowrapOptions {
29
+ separator?: string
30
+ }
31
+
32
+ // ============================================================================
33
+ // Typed Predicates
34
+ // ============================================================================
35
+
36
+ /** Predicate function that receives a typed tuple */
37
+ export type TypedPredicateFunc<T> = (t: T) => boolean
38
+
39
+ /** Predicate: either a partial tuple for equality matching, or a function */
40
+ export type TypedPredicate<T> = Partial<T> | TypedPredicateFunc<T>
41
+
42
+ // ============================================================================
43
+ // Typed Extensions
44
+ // ============================================================================
45
+
46
+ /** Extension function that receives a typed tuple */
47
+ export type TypedExtensionFunc<T, R> = (tuple: T) => R
48
+
49
+ /** Extension definition: function returning value, or attribute name to copy */
50
+ export type TypedExtension<T, E extends Record<string, unknown>> = {
51
+ [K in keyof E]: TypedExtensionFunc<T, E[K]> | keyof T;
52
+ }
53
+
54
+ // ============================================================================
55
+ // Generic Relation Interface
56
+ // ============================================================================
57
+
58
+ /**
59
+ * Relation interface with generic type parameter for tuple type.
60
+ * Default parameter `Tuple` ensures backwards compatibility.
61
+ *
62
+ * @typeParam T - The tuple type for this relation. Defaults to `Tuple` (Record<string, unknown>).
63
+ *
64
+ * @example
65
+ * // Untyped usage (backwards compatible)
66
+ * const r = Bmg([{ id: 1 }]); // Relation<Tuple>
67
+ *
68
+ * @example
69
+ * // Typed usage with full type safety
70
+ * interface Person { id: number; name: string }
71
+ * const r = Bmg<Person>([{ id: 1, name: 'Alice' }]);
72
+ * r.project(['id']); // Relation<{ id: number }>
73
+ */
74
+ export interface Relation<T = Tuple> {
75
+ // === Type-preserving operators ===
76
+
77
+ restrict(p: TypedPredicate<T>): Relation<T>
78
+ where(p: TypedPredicate<T>): Relation<T>
79
+ exclude(p: TypedPredicate<T>): Relation<T>
80
+
81
+ // === Projection operators ===
82
+
83
+ project<K extends keyof T>(attrs: K[]): Relation<Pick<T, K>>
84
+ allbut<K extends keyof T>(attrs: K[]): Relation<Omit<T, K>>
85
+
86
+ // === Extension operators ===
87
+
88
+ extend<E extends Record<string, unknown>>(e: TypedExtension<T, E>): Relation<T & E>
89
+ constants<C extends Tuple>(consts: C): Relation<T & C>
90
+
91
+ // === Rename operators ===
92
+
93
+ rename<R extends RenameMap<T>>(r: R): Relation<Renamed<T, R>>
94
+ prefix<P extends string, Ex extends keyof T = never>(pfx: P, options?: { except?: Ex[] }): Relation<Prefixed<T, P, Ex>>
95
+ suffix<S extends string, Ex extends keyof T = never>(sfx: S, options?: { except?: Ex[] }): Relation<Suffixed<T, S, Ex>>
96
+
97
+ // === Set operators (require same type) ===
98
+
99
+ union(right: RelationOperand<T>): Relation<T>
100
+ minus(right: RelationOperand<T>): Relation<T>
101
+ intersect(right: RelationOperand<T>): Relation<T>
102
+
103
+ // === Semi-join operators (preserve left type) ===
104
+
105
+ matching<R>(right: RelationOperand<R>, keys?: JoinKeys): Relation<T>
106
+ not_matching<R>(right: RelationOperand<R>, keys?: JoinKeys): Relation<T>
107
+
108
+ // === Join operators ===
109
+
110
+ join<R>(right: RelationOperand<R>, keys?: JoinKeys): Relation<Joined<T, R>>
111
+ left_join<R>(right: RelationOperand<R>, keys?: JoinKeys): Relation<LeftJoined<T, R>>
112
+ cross_product<R>(right: RelationOperand<R>): Relation<T & R>
113
+ cross_join<R>(right: RelationOperand<R>): Relation<T & R>
114
+
115
+ // === Nesting operators ===
116
+
117
+ image<R, As extends string>(right: RelationOperand<R>, as: As, keys?: JoinKeys): Relation<T & Record<As, Relation<Omit<R, keyof T & keyof R>>>>
118
+ group<K extends keyof T, As extends string>(attrs: K[], as: As): Relation<Omit<T, K> & Record<As, Relation<Pick<T, K>>>>
119
+ ungroup<K extends keyof T>(attr: K): Relation<Ungrouped<T, K>>
120
+ wrap<K extends keyof T, As extends string>(attrs: K[], as: As): Relation<Wrapped<T, K, As>>
121
+ unwrap<K extends keyof T>(attr: K): Relation<Unwrapped<T, K>>
122
+
123
+ // === Aggregation ===
124
+
125
+ summarize<By extends keyof T, Aggs extends Aggregators>(by: By[], aggs: Aggs): Relation<Pick<T, By> & AggregatorResults<Aggs>>
126
+
127
+ // === Transform ===
128
+
129
+ transform(t: Transformation): Relation<T>
130
+
131
+ // === Dynamic (loses type precision) ===
132
+
133
+ autowrap(options?: AutowrapOptions): Relation<Tuple>
134
+
135
+ // === Non-relational ===
136
+
137
+ one(): T
138
+ yByX<Y extends keyof T, X extends keyof T>(y: Y, x: X): Record<T[X] & PropertyKey, T[Y]>
139
+ toArray(): T[]
140
+ isEqual(right: any): boolean
141
+ }
142
+
143
+ // ============================================================================
144
+ // Operands and Helpers
145
+ // ============================================================================
146
+
147
+ export type RelationOperand<T = Tuple> = Relation<T> | T[]
148
+
149
+ export interface OperationalOperand<T = Tuple> {
150
+ tuples(): Iterable<T>
151
+ output(tuples: T[]): RelationOperand<T>
152
+ }
153
+
154
+ // Legacy predicate types (for backwards compatibility with standalone operators)
155
+ export type PredicateFunc = ((t: Tuple) => any)
156
+ export type Predicate = Tuple | PredicateFunc
157
+
158
+ export type Renaming = RenamingObj | RenamingFunc
159
+ export type RenamingFunc = (attr: AttrName) => AttrName
160
+ export type RenamingObj = Record<AttrName, AttrName>
161
+
162
+ export type ExtensionFunc = (tuple: Tuple) => unknown
163
+ export type Extension = Record<AttrName, ExtensionFunc | AttrName>
164
+
165
+ export type JoinKeys = AttrName[] | Record<AttrName, AttrName>
166
+
167
+ export type AggregatorName = 'count' | 'sum' | 'min' | 'max' | 'avg' | 'collect'
168
+ export type AggregatorSpec = { op: AggregatorName, attr: AttrName }
169
+ export type AggregatorFunc = (tuples: Tuple[]) => unknown
170
+ export type Aggregator = AggregatorName | AggregatorSpec | AggregatorFunc
171
+ export type Aggregators = Record<AttrName, Aggregator>
172
+
173
+ export type TransformFunc = (value: unknown) => unknown
174
+ export type Transformation = TransformFunc | TransformFunc[] | Record<AttrName, TransformFunc | TransformFunc[]>
175
+
176
+ // Re-export utility types for convenience
177
+ export type { RenameMap, Renamed, Prefixed, Suffixed, Joined, LeftJoined, Wrapped, Unwrapped } from './utility-types';
178
+ // Ungrouped is defined in this file (not utility-types) due to Relation dependency
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Utility types for type transformations in relational operators.
3
+ * These enable compile-time tracking of how operators transform tuple types.
4
+ */
5
+
6
+ import type { Tuple } from './types';
7
+
8
+ // ============================================================================
9
+ // Rename Types
10
+ // ============================================================================
11
+
12
+ /** Map from old attribute names to new attribute names */
13
+ export type RenameMap<T> = { [K in keyof T]?: string };
14
+
15
+ /** Transform tuple type by renaming keys according to RenameMap */
16
+ export type Renamed<T, R extends RenameMap<T>> = {
17
+ [K in keyof T as K extends keyof R ? (R[K] extends string ? R[K] : K) : K]: T[K];
18
+ };
19
+
20
+ // ============================================================================
21
+ // Prefix/Suffix Types
22
+ // ============================================================================
23
+
24
+ /** Prefix all keys except those in Except */
25
+ export type Prefixed<T, P extends string, Except extends keyof T = never> = {
26
+ [K in keyof T as K extends Except ? K : `${P}${K & string}`]: T[K];
27
+ };
28
+
29
+ /** Suffix all keys except those in Except */
30
+ export type Suffixed<T, S extends string, Except extends keyof T = never> = {
31
+ [K in keyof T as K extends Except ? K : `${K & string}${S}`]: T[K];
32
+ };
33
+
34
+ // ============================================================================
35
+ // Join Types
36
+ // ============================================================================
37
+
38
+ /** Extract common keys between two tuple types */
39
+ export type CommonKeys<L, R> = Extract<keyof L, keyof R>;
40
+
41
+ /** Result of inner join: L & R with R's common keys removed */
42
+ export type Joined<L, R> = L & Omit<R, CommonKeys<L, R>>;
43
+
44
+ /** Result of left join: L & optional R attributes (common keys removed) */
45
+ export type LeftJoined<L, R> = L & Partial<Omit<R, CommonKeys<L, R>>>;
46
+
47
+ // ============================================================================
48
+ // Wrap/Unwrap Types
49
+ // ============================================================================
50
+
51
+ /** Result of wrap: remove wrapped attrs, add nested object */
52
+ export type Wrapped<T, K extends keyof T, As extends string> =
53
+ Omit<T, K> & Record<As, Pick<T, K>>;
54
+
55
+ /** Result of unwrap: remove object attr, spread its properties */
56
+ export type Unwrapped<T, K extends keyof T> =
57
+ T[K] extends Record<string, unknown> ? Omit<T, K> & T[K] : Omit<T, K>;
58
+
59
+ // Note: Ungrouped is defined in types.ts since it references Relation
60
+
61
+ // ============================================================================
62
+ // Aggregator Result Types
63
+ // ============================================================================
64
+
65
+ /** Infer result type from aggregator specification */
66
+ export type AggregatorResult<A> =
67
+ A extends 'count' ? number :
68
+ A extends { op: 'count' } ? number :
69
+ A extends { op: 'sum' | 'avg' | 'min' | 'max' } ? number | null :
70
+ A extends { op: 'collect' } ? unknown[] :
71
+ A extends (tuples: Tuple[]) => infer R ? R :
72
+ unknown;
73
+
74
+ /** Map aggregator definitions to their result types */
75
+ export type AggregatorResults<Aggs extends Record<string, unknown>> = {
76
+ [K in keyof Aggs]: AggregatorResult<Aggs[K]>;
77
+ };
@@ -0,0 +1,16 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Bmg } from 'src';
3
+
4
+ describe('BMG.js', () => {
5
+ it('exists', () => {
6
+ expect(Bmg).not.toBe(undefined);
7
+ });
8
+
9
+ it('allows building relations', () => {
10
+ const r = Bmg([
11
+ {sid: 'S1', name: 'Smith'},
12
+ {sid: 'S2', name: 'Jones'},
13
+ ])
14
+ expect(Bmg.isRelation(r)).toBeTruthy()
15
+ })
16
+ });
@@ -0,0 +1,9 @@
1
+ import { Bmg } from 'src'
2
+
3
+ export const SUPPLIERS = Bmg([
4
+ {sid: 'S1', name: 'Smith', status: 20, city: 'London' },
5
+ {sid: 'S2', name: 'Jones', status: 10, city: 'Paris' },
6
+ {sid: 'S3', name: 'Blake', status: 30, city: 'Paris' },
7
+ {sid: 'S4', name: 'Clark', status: 20, city: 'London' },
8
+ {sid: 'S5', name: 'Adams', status: 30, city: 'Athens' },
9
+ ])