@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,3 @@
1
+ # Git configuration (used for commits inside the container)
2
+ GIT_USER_NAME=Your Name
3
+ GIT_USER_EMAIL=your.email@example.com
@@ -0,0 +1,36 @@
1
+ FROM alpine:latest
2
+
3
+ # Install packages: base tools, node, ruby, postgresql client
4
+ RUN apk update && apk add --no-cache \
5
+ bash \
6
+ curl \
7
+ git \
8
+ build-base \
9
+ nodejs \
10
+ npm \
11
+ vim \
12
+ sudo
13
+
14
+ # Create non-root user with sudo access
15
+ RUN addgroup -S bmg && adduser -S bmg -G bmg \
16
+ && echo "bmg ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
17
+
18
+ # Install Claude Code CLI globally
19
+ RUN npm install -g @anthropic-ai/claude-code
20
+
21
+ # Set up working directory
22
+ WORKDIR /workspace
23
+
24
+ # Copy and set up entrypoint
25
+ COPY entrypoint.sh /entrypoint.sh
26
+ RUN chmod +x /entrypoint.sh
27
+
28
+ # Create .claude directory for history persistence
29
+ RUN mkdir -p /home/bmg/.claude && chown -R bmg:bmg /home/bmg/.claude
30
+
31
+ # Switch to non-root user
32
+ USER bmg
33
+
34
+ # Use entrypoint to configure git and run command
35
+ ENTRYPOINT ["/entrypoint.sh"]
36
+ CMD ["bash"]
@@ -0,0 +1,63 @@
1
+ # Hacking on Bmg with Claude Code
2
+
3
+ This Docker setup provides a sandboxed environment with all tools needed to hack on Bmg:
4
+ - Node.js
5
+ - Claude Code CLI
6
+
7
+ ## Prerequisites
8
+
9
+ 1. Docker and Docker Compose installed
10
+ 2. A Claude Code account
11
+
12
+ ## Getting Started
13
+
14
+ ```bash
15
+ cd .claude/safe-setup
16
+
17
+ # Create your .env file from template
18
+ cp .env.example .env
19
+
20
+ # Edit .env to set your git identity
21
+ # GIT_USER_NAME=Your Name
22
+ # GIT_USER_EMAIL=your.email@example.com
23
+
24
+ # Build and start containers
25
+ make up
26
+
27
+ # Enter the dev environment
28
+ make shell
29
+ ```
30
+
31
+ ## Persistent Data
32
+
33
+ - **Claude history**: Stored in a named Docker volume (`claude-history`) that persists across container restarts
34
+ - **Git configuration**: Automatically set from `GIT_USER_NAME` and `GIT_USER_EMAIL` environment variables
35
+
36
+ ## Inside the Container
37
+
38
+ You're now user `bmg` in `/workspace` (the Bmg project root).
39
+
40
+ ```bash
41
+ # Install project dependencies
42
+ npm install
43
+
44
+ # Run tests
45
+ npm run test
46
+
47
+ # Use Claude Code
48
+ claude
49
+ ```
50
+
51
+ PostgreSQL is pre-configured via environment variables. Just run `psql` to connect.
52
+
53
+ ## Commands
54
+
55
+ | Command | Description |
56
+ |---------|-------------|
57
+ | `make up` | Build and start containers |
58
+ | `make down` | Stop containers |
59
+ | `make shell` | Enter dev container |
60
+ | `make restart` | Restart everything |
61
+ | `make logs` | Follow container logs |
62
+ | `make status` | Show container status |
63
+ | `make clean` | Remove containers and images |
@@ -0,0 +1,22 @@
1
+ .PHONY: up down shell restart clean logs status
2
+
3
+ up:
4
+ docker compose up -d --build
5
+ @echo "Containers started. Use 'make shell' to access dev environment"
6
+
7
+ down:
8
+ docker compose down
9
+
10
+ shell:
11
+ docker compose exec dev bash
12
+
13
+ restart: down up
14
+
15
+ clean: down
16
+ docker compose down --rmi all --volumes
17
+
18
+ logs:
19
+ docker compose logs -f
20
+
21
+ status:
22
+ @docker compose ps
@@ -0,0 +1,18 @@
1
+ services:
2
+
3
+ dev:
4
+ build:
5
+ context: .
6
+ dockerfile: Dockerfile.claude
7
+ container_name: claude
8
+ volumes:
9
+ - ../..:/workspace
10
+ - claude-history:/home/bmg/.claude
11
+ environment:
12
+ - GIT_USER_NAME=${GIT_USER_NAME:-}
13
+ - GIT_USER_EMAIL=${GIT_USER_EMAIL:-}
14
+ stdin_open: true
15
+ tty: true
16
+
17
+ volumes:
18
+ claude-history:
@@ -0,0 +1,13 @@
1
+ #!/bin/bash
2
+
3
+ # Configure git user/email from environment variables if provided
4
+ if [ -n "$GIT_USER_NAME" ]; then
5
+ git config --global user.name "$GIT_USER_NAME"
6
+ fi
7
+
8
+ if [ -n "$GIT_USER_EMAIL" ]; then
9
+ git config --global user.email "$GIT_USER_EMAIL"
10
+ fi
11
+
12
+ # Execute the command passed to the container
13
+ exec "$@"
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npm run test:*)",
5
+ "Bash(npx tsc:*)",
6
+ "Bash(wc:*)"
7
+ ]
8
+ }
9
+ }
@@ -0,0 +1,273 @@
1
+ # TypeScript Type Safety Implementation Plan
2
+
3
+ ## Goal
4
+ Add opt-in generic type parameters to enable:
5
+ - IDE autocomplete on tuple attributes
6
+ - Compile-time validation of attribute names
7
+ - Type transformation tracking through operator chains
8
+
9
+ ## Core Changes
10
+
11
+ ### 1. Generic Relation Interface (`src/types.ts`)
12
+
13
+ ```typescript
14
+ export interface Relation<T extends Tuple = Tuple> {
15
+ one(): T;
16
+ toArray(): T[];
17
+
18
+ // Preserve type
19
+ restrict(p: TypedPredicate<T>): Relation<T>;
20
+ where(p: TypedPredicate<T>): Relation<T>;
21
+
22
+ // Transform type
23
+ project<K extends keyof T>(attrs: K[]): Relation<Pick<T, K>>;
24
+ allbut<K extends keyof T>(attrs: K[]): Relation<Omit<T, K>>;
25
+ rename<R extends RenameMap<T>>(r: R): Relation<Renamed<T, R>>;
26
+ extend<E extends Record<string, unknown>>(e: TypedExtension<T, E>): Relation<T & E>;
27
+ constants<C extends Tuple>(c: C): Relation<T & C>;
28
+
29
+ // Binary ops - combine types
30
+ join<R extends Tuple>(right: RelationOperand<R>, keys?: JoinKeys<T, R>): Relation<Joined<T, R>>;
31
+ left_join<R extends Tuple>(right: RelationOperand<R>, keys?: JoinKeys<T, R>): Relation<LeftJoined<T, R>>;
32
+ cross_product<R extends Tuple>(right: RelationOperand<R>): Relation<T & R>;
33
+
34
+ // Nesting
35
+ group<K extends keyof T, As extends string>(attrs: K[], as: As): Relation<Grouped<T, K, As>>;
36
+ ungroup<K extends keyof T>(attr: K): Relation<Ungrouped<T, K>>;
37
+ // ...
38
+ }
39
+ ```
40
+
41
+ ### 2. Generic MemoryRelation Class (`src/Relation/Memory.ts`)
42
+
43
+ ```typescript
44
+ export class MemoryRelation<T extends Tuple = Tuple> implements Relation<T> {
45
+ constructor(private tuples: T[]) {}
46
+
47
+ project<K extends keyof T>(attrs: K[]): MemoryRelation<Pick<T, K>> {
48
+ return project(this, attrs as string[]) as MemoryRelation<Pick<T, K>>;
49
+ }
50
+ // ...
51
+ }
52
+ ```
53
+
54
+ ### 3. Generic Bmg Factory (`src/index.ts`)
55
+
56
+ ```typescript
57
+ export function Bmg<T extends Tuple>(tuples: T[]): MemoryRelation<T>;
58
+ export function Bmg(tuples: Tuple[]): MemoryRelation<Tuple>;
59
+ export function Bmg<T extends Tuple = Tuple>(tuples: T[]): MemoryRelation<T> {
60
+ return new MemoryRelation<T>(tuples);
61
+ }
62
+ ```
63
+
64
+ ### 4. Utility Types (`src/utility-types.ts` - new file)
65
+
66
+ ```typescript
67
+ // Rename: { name: 'fullName' } transforms { name, age } to { fullName, age }
68
+ export type RenameMap<T> = { [K in keyof T]?: string };
69
+ export type Renamed<T, R extends RenameMap<T>> = {
70
+ [K in keyof T as K extends keyof R ? (R[K] extends string ? R[K] : K) : K]: T[K];
71
+ };
72
+
73
+ // Join: combine left & right, remove duplicate keys from right
74
+ export type CommonKeys<L, R> = Extract<keyof L, keyof R>;
75
+ export type Joined<L, R> = L & Omit<R, CommonKeys<L, R>>;
76
+ export type LeftJoined<L, R> = L & Partial<Omit<R, CommonKeys<L, R>>>;
77
+
78
+ // Group/Ungroup
79
+ export type Grouped<T, K extends keyof T, As extends string> =
80
+ Omit<T, K> & Record<As, Relation<Pick<T, K>>>;
81
+ export type Ungrouped<T, K extends keyof T> =
82
+ T[K] extends Relation<infer N> ? Omit<T, K> & N : never;
83
+
84
+ // Wrap/Unwrap
85
+ export type Wrapped<T, K extends keyof T, As extends string> =
86
+ Omit<T, K> & Record<As, Pick<T, K>>;
87
+ export type Unwrapped<T, K extends keyof T> =
88
+ T[K] extends Record<string, unknown> ? Omit<T, K> & T[K] : never;
89
+
90
+ // Prefix/Suffix (template literal types)
91
+ export type Prefixed<T, P extends string, Ex extends keyof T = never> = {
92
+ [K in keyof T as K extends Ex ? K : `${P}${K & string}`]: T[K];
93
+ };
94
+ ```
95
+
96
+ ### 5. Typed Predicates (`src/types.ts`)
97
+
98
+ ```typescript
99
+ export type TypedPredicateFunc<T> = (t: T) => boolean;
100
+ export type TypedPredicate<T> = TypedPredicateFunc<T> | Partial<T>;
101
+ ```
102
+
103
+ ## Implementation Phases
104
+
105
+ ### Phase 1: Foundation
106
+ 1. Add `Relation<T = Tuple>` interface with generics
107
+ 2. Add `MemoryRelation<T = Tuple>` class
108
+ 3. Update `Bmg<T>()` factory
109
+ 4. Type these operators first (highest value):
110
+ - `one()`, `toArray()` - return `T` / `T[]`
111
+ - `project()` - `Pick<T, K>`
112
+ - `allbut()` - `Omit<T, K>`
113
+ - `restrict()`, `where()`, `exclude()` - preserve `T` with typed predicate
114
+
115
+ ### Phase 2: Structure-Changing Operators
116
+ - `extend()` - `T & E`
117
+ - `constants()` - `T & C`
118
+ - `rename()` - `Renamed<T, R>`
119
+ - `prefix()`, `suffix()` - template literal types
120
+
121
+ ### Phase 3: Binary Operations
122
+ - `union()`, `minus()`, `intersect()` - require same `T`
123
+ - `join()` - `Joined<T, R>`
124
+ - `left_join()` - `LeftJoined<T, R>`
125
+ - `cross_product()` - `T & R`
126
+ - `matching()`, `not_matching()` - preserve `T`
127
+
128
+ ### Phase 4: Nesting Operators
129
+ - `group()` - `Grouped<T, K, As>`
130
+ - `ungroup()` - `Ungrouped<T, K>`
131
+ - `wrap()` - `Wrapped<T, K, As>`
132
+ - `unwrap()` - `Unwrapped<T, K>`
133
+ - `image()` - adds `Relation<...>` attribute
134
+
135
+ ### Phase 5: Complex Operators
136
+ - `summarize()` - infer result from aggregators
137
+ - `transform()` - preserve structure, optional value type tracking
138
+ - `autowrap()` - return `Relation<Tuple>` (dynamic, un-typeable)
139
+
140
+ ## Backwards Compatibility
141
+
142
+ - Default type parameter `= Tuple` ensures existing untyped code compiles
143
+ - When `T = Tuple = Record<string, unknown>`, `keyof T = string`, so any string[] is accepted
144
+ - No breaking changes to existing API
145
+
146
+ ## Files to Modify
147
+
148
+ | File | Changes |
149
+ |------|---------|
150
+ | `src/types.ts` | Add generics to `Relation`, typed predicates |
151
+ | `src/utility-types.ts` (new) | Utility types: `Renamed`, `Joined`, `Grouped`, etc. |
152
+ | `src/Relation/Memory.ts` | Add `<T>` parameter, update method signatures |
153
+ | `src/index.ts` | Update `Bmg` factory with type inference |
154
+ | `src/operators/_helpers.ts` | Generic `toOperationalOperand<T>` |
155
+
156
+ ## Example Usage After Implementation
157
+
158
+ ```typescript
159
+ // Opt-in: provide type for full type safety
160
+ interface Supplier {
161
+ sid: string;
162
+ name: string;
163
+ status: number;
164
+ city: string;
165
+ }
166
+
167
+ const suppliers = Bmg<Supplier>([
168
+ { sid: 'S1', name: 'Smith', status: 20, city: 'London' },
169
+ ]);
170
+
171
+ // IDE autocomplete: suggests 'sid', 'name', 'status', 'city'
172
+ const result = suppliers
173
+ .project(['sid', 'name']) // Relation<{ sid: string; name: string }>
174
+ .rename({ name: 'fullName' }) // Relation<{ sid: string; fullName: string }>
175
+ .extend({ upper: t => t.fullName.toUpperCase() }); // t.fullName autocompletes
176
+
177
+ // Compile error for invalid attributes
178
+ suppliers.project(['invalid']); // Error: 'invalid' not in keyof Supplier
179
+
180
+ // Untyped usage still works (backwards compatible)
181
+ const r = Bmg([{ a: 1 }]); // Relation<Tuple>
182
+ r.project(['a']); // Works, no validation
183
+ ```
184
+
185
+ ## Test Plan
186
+
187
+ ### 1. Compile-Time Type Tests
188
+
189
+ Create `tests/types/relation.test-d.ts` using vitest's type testing:
190
+
191
+ ```typescript
192
+ import { Bmg } from 'src';
193
+ import { expectTypeOf } from 'vitest';
194
+
195
+ interface Person { id: number; name: string; age: number }
196
+
197
+ describe('Type Safety', () => {
198
+ const people = Bmg<Person>([{ id: 1, name: 'Alice', age: 30 }]);
199
+
200
+ it('project narrows type to Pick<T, K>', () => {
201
+ const result = people.project(['id', 'name']);
202
+ expectTypeOf(result.one()).toEqualTypeOf<{ id: number; name: string }>();
203
+ });
204
+
205
+ it('rejects invalid attribute names', () => {
206
+ // @ts-expect-error - 'invalid' is not a key of Person
207
+ people.project(['invalid']);
208
+ });
209
+
210
+ it('restrict preserves type with typed predicate', () => {
211
+ const result = people.restrict(t => t.age > 25);
212
+ expectTypeOf(result.one()).toEqualTypeOf<Person>();
213
+ });
214
+
215
+ it('predicate function receives typed tuple', () => {
216
+ people.restrict(t => {
217
+ expectTypeOf(t.id).toBeNumber();
218
+ expectTypeOf(t.name).toBeString();
219
+ return true;
220
+ });
221
+ });
222
+
223
+ it('join combines types correctly', () => {
224
+ interface Order { orderId: number; personId: number; total: number }
225
+ const orders = Bmg<Order>([]);
226
+ const joined = people.join(orders, { id: 'personId' });
227
+ expectTypeOf(joined.one()).toEqualTypeOf<{
228
+ id: number; name: string; age: number; orderId: number; total: number
229
+ }>();
230
+ });
231
+
232
+ it('rename transforms keys', () => {
233
+ const renamed = people.rename({ name: 'fullName' });
234
+ expectTypeOf(renamed.one()).toEqualTypeOf<{ id: number; fullName: string; age: number }>();
235
+ });
236
+
237
+ it('untyped usage still works', () => {
238
+ const r = Bmg([{ a: 1 }]); // Relation<Tuple>
239
+ r.project(['a']); // Should compile
240
+ r.project(['anything']); // Should compile (no validation when T=Tuple)
241
+ });
242
+ });
243
+ ```
244
+
245
+ ### 2. Runtime Tests
246
+
247
+ All 152 existing tests must continue to pass without modification - validates backwards compatibility.
248
+
249
+ ### 3. Type Test Coverage by Phase
250
+
251
+ | Phase | Type Tests |
252
+ |-------|-----------|
253
+ | Phase 1 | `project`, `allbut`, `restrict`, `one`, `toArray` |
254
+ | Phase 2 | `extend`, `constants`, `rename`, `prefix`, `suffix` |
255
+ | Phase 3 | `join`, `left_join`, `cross_product`, `union`, `minus` |
256
+ | Phase 4 | `group`, `ungroup`, `wrap`, `unwrap`, `image` |
257
+ | Phase 5 | `summarize`, `transform`, `autowrap` (escape hatch) |
258
+
259
+ ### 4. Test File Structure
260
+
261
+ ```
262
+ tests/
263
+ types/
264
+ relation.test-d.ts # Core Relation<T> type tests
265
+ operators.test-d.ts # Operator type transformation tests
266
+ utility-types.test-d.ts # Utility type unit tests
267
+ ```
268
+
269
+ ## Limitations
270
+
271
+ - Function-based `rename()` loses type tracking (only object syntax typed)
272
+ - `autowrap()` returns `Relation<Tuple>` (dynamic structure)
273
+ - Requires TypeScript 4.1+ for template literal types
@@ -0,0 +1,26 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: ['*']
6
+ pull_request:
7
+ branches: ['*']
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - uses: pnpm/action-setup@v4
17
+
18
+ - uses: actions/setup-node@v4
19
+ with:
20
+ node-version: '20'
21
+
22
+ - name: Install dependencies
23
+ run: pnpm install
24
+
25
+ - name: Run tests
26
+ run: pnpm test
package/CLAUDE.md ADDED
@@ -0,0 +1,48 @@
1
+ # What is Bmg.js ?
2
+
3
+ A Typescript implementation of BMG, a relational algebra originally implemented
4
+ in/for Ruby :
5
+
6
+ - https://github.com/enspirit/bmg
7
+ - https://www.relational-algebra.dev/
8
+
9
+ The aim of bmg.js is NOT to implement the SQL compiler, but only to provide an
10
+ implementation of main algebra operators to work on arrays of javascript objects.
11
+
12
+ ## Development flow
13
+
14
+ * List operators to support, by order of importance
15
+ * Add each operator, in order, with unit tests.
16
+ * One commit per operator, don't forget to adapt the README.
17
+ * Tests MUST succeed at all times, run them with `npm run test`
18
+
19
+ ## Theory
20
+
21
+ IMPORTANT rules:
22
+
23
+ 1. Relations NEVER have duplicates
24
+ 2. Order of tuples and order or attributes in a tuple are not important semantically
25
+ 3. Mathematically, relations are sets of tuples ; tuples are sets of (attr, value) pairs.
26
+ 4. Two relations are equal of they have the exact same set of exact same tuples.
27
+
28
+ ## About unit tests
29
+
30
+ IMPORTANT rules:
31
+
32
+ * Favor purely relational tests: compare an obtained relation with the expected relation
33
+ using `isEqual`.
34
+ * Do NEVER access the "first" tuple, since there is no such tuple.
35
+ * Instead, use `r.restrict(...predicate...).one` with a predicate that selects the
36
+ tuple you are interested in. `one` will correctly fail if your assumption is wrong.
37
+
38
+ ## Implemented operators
39
+
40
+ **Relational:** restrict, where, exclude, project, allbut, extend, rename, prefix, suffix, constants, union, minus, intersect, matching, not_matching, join, left_join, cross_product, cross_join, image, summarize, group, ungroup, wrap, unwrap, autowrap, transform
41
+
42
+ **Non-relational:** one, yByX, toArray, isRelation, isEqual
43
+
44
+ ## TODO
45
+
46
+ - [ ] sort - Order tuples by attributes
47
+ - [ ] page - Pagination (offset + limit)
48
+ - [ ] size, empty, first, exists - Utility helpers
package/Makefile ADDED
@@ -0,0 +1,2 @@
1
+ dist/bmg.cjs: src/**/*
2
+ pnpm run build
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # Bmg.js - Relational Algebra for JavaScript/TypeScript
2
+
3
+ A TypeScript/JavaScript implementation of [BMG](https://www.relational-algebra.dev/), providing relational algebra operators for working with arrays of objects.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @enspirit/bmg-js
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ Using the Relation abstraction:
14
+
15
+ ```typescript
16
+ import { Bmg } from '@enspirit/bmg-js'
17
+
18
+ const suppliers = Bmg([
19
+ { sid: 'S1', name: 'Smith', status: 20, city: 'London' },
20
+ { sid: 'S2', name: 'Jones', status: 10, city: 'Paris' },
21
+ { sid: 'S3', name: 'Blake', status: 30, city: 'Paris' },
22
+ ])
23
+
24
+ // Chain operations fluently
25
+ const parisSuppliers = suppliers
26
+ .restrict({ city: 'Paris' })
27
+ .project(['sid', 'name'])
28
+
29
+ console.log(parisSuppliers.toArray())
30
+ // => [{ sid: 'S2', name: 'Jones' }, { sid: 'S3', name: 'Blake' }]
31
+
32
+ // Extract a single tuple
33
+ const smith = suppliers.restrict({ sid: 'S1' }).one()
34
+ // => { sid: 'S1', name: 'Smith', status: 20, city: 'London' }
35
+ ```
36
+
37
+ Using standalone operators on plain arrays:
38
+
39
+ ```typescript
40
+ import { restrict, project } from '@enspirit/bmg-js'
41
+
42
+ const suppliers = [
43
+ { sid: 'S1', name: 'Smith', city: 'London' },
44
+ { sid: 'S2', name: 'Jones', city: 'Paris' },
45
+ ]
46
+
47
+ const result = project(restrict(suppliers, { city: 'Paris' }), ['name'])
48
+ // => [{ name: 'Jones' }]
49
+ ```
50
+
51
+ ## TypeScript Support
52
+
53
+ Bmg.js provides full TypeScript support with generic types:
54
+
55
+ ```typescript
56
+ import { Bmg } from '@enspirit/bmg-js'
57
+
58
+ interface Supplier {
59
+ sid: string
60
+ name: string
61
+ status: number
62
+ city: string
63
+ }
64
+
65
+ const suppliers = Bmg<Supplier>([
66
+ { sid: 'S1', name: 'Smith', status: 20, city: 'London' },
67
+ ])
68
+
69
+ // Type-safe operations with autocomplete
70
+ const projected = suppliers.project(['sid', 'name'])
71
+ // Type: Relation<{ sid: string; name: string }>
72
+
73
+ const one = suppliers.restrict({ sid: 'S1' }).one()
74
+ // Type: Supplier
75
+ ```
76
+
77
+ See the [full type-safe example](./example/index.ts) for more.
78
+
79
+ ## Available Operators
80
+
81
+ | Category | Operator | Description |
82
+ |----------|----------|-------------|
83
+ | **Filtering** | `restrict(predicate)` | Keep tuples matching predicate |
84
+ | | `where(predicate)` | Alias for restrict |
85
+ | | `exclude(predicate)` | Keep tuples NOT matching predicate |
86
+ | | `matching(other, keys?)` | Semi-join (tuples with match in other) |
87
+ | | `not_matching(other, keys?)` | Anti-join (tuples without match) |
88
+ | **Projection** | `project(attrs)` | Keep only specified attributes |
89
+ | | `allbut(attrs)` | Keep all attributes except specified |
90
+ | **Extension** | `extend(extensions)` | Add computed attributes |
91
+ | | `constants(values)` | Add constant attributes |
92
+ | **Renaming** | `rename(mapping)` | Rename attributes |
93
+ | | `prefix(pfx, options?)` | Add prefix to attribute names |
94
+ | | `suffix(sfx, options?)` | Add suffix to attribute names |
95
+ | **Set Operations** | `union(other)` | Set union of two relations |
96
+ | | `minus(other)` | Set difference (tuples in left but not right) |
97
+ | | `intersect(other)` | Set intersection (tuples in both) |
98
+ | **Join Operations** | `join(other, keys?)` | Natural join on common/specified attributes |
99
+ | | `left_join(other, keys?)` | Left outer join |
100
+ | | `cross_product(other)` | Cartesian product |
101
+ | | `cross_join(other)` | Alias for cross_product |
102
+ | **Nesting & Grouping** | `image(other, as, keys?)` | Nest matching tuples as relation attribute |
103
+ | | `group(attrs, as)` | Group attributes into nested relation |
104
+ | | `ungroup(attr)` | Flatten nested relation |
105
+ | | `wrap(attrs, as)` | Wrap attributes into tuple-valued attribute |
106
+ | | `unwrap(attr)` | Flatten tuple-valued attribute |
107
+ | | `autowrap(options?)` | Auto-wrap by separator pattern |
108
+ | **Aggregation** | `summarize(by, aggregators)` | Group and aggregate |
109
+ | **Transformation** | `transform(transformation)` | Transform attribute values |
110
+ | **Non-Relational** | `one()` | Extract single tuple (throws if not exactly one) |
111
+ | | `toArray()` | Convert relation to array |
112
+ | | `isEqual(other)` | Check set equality |
113
+ | | `yByX(y, x)` | Create `{ x-value: y-value }` mapping |
114
+ | | `Bmg.isRelation(value)` | Check if value is a Relation (static) |
115
+
116
+ Built-in aggregators for `summarize`: `count`, `sum`, `min`, `max`, `avg`, `collect`
117
+
118
+ ## Theory
119
+
120
+ Bmg.js implements relational algebra with these principles:
121
+
122
+ 1. **No duplicates** - Relations are sets; duplicate tuples are automatically removed
123
+ 2. **Order independence** - Tuple order and attribute order have no semantic meaning
124
+ 3. **Set equality** - Two relations are equal if they contain the same tuples
125
+
126
+ ## Horizon
127
+
128
+ The aim is to have a language where one can write beautiful functional expressions:
129
+
130
+ ```
131
+ suppliers
132
+ |> restrict( _ ~> _.status > 20 )
133
+ |> rename(sid: 'id', name: 'lastname')
134
+ |> restrict(city: 'Paris')
135
+ |> one
136
+ ```
137
+
138
+ This will be provided by [Elo](https://elo-lang.org).
139
+
140
+ ## Versioning
141
+
142
+ Bmg.js follows [Semantic Versioning](https://semver.org/) (SemVer):
143
+
144
+ - **MAJOR** (x.0.0) - Breaking changes to the API (renamed operators, changed signatures, removed features)
145
+ - **MINOR** (0.x.0) - New operators or features, fully backward-compatible
146
+ - **PATCH** (0.0.x) - Bug fixes and performance improvements, no API changes
147
+
148
+ For Bmg.js specifically:
149
+ - Adding a new operator (e.g., `sort`, `page`) is a **minor** release
150
+ - Fixing incorrect behavior in an existing operator is a **patch** release
151
+ - Changing an operator's signature or renaming it is a **major** release
152
+
153
+ ## Contributing
154
+
155
+ 1. Fork the repository
156
+ 2. Create a feature branch (`git checkout -b feature/new-operator`)
157
+ 3. Add your changes with tests
158
+ 4. Ensure tests pass: `npm run test`
159
+ 5. Commit following conventional commits (e.g., `feat: add sort operator`)
160
+ 6. Open a pull request
161
+
162
+ When adding a new operator:
163
+ - Add the operator implementation
164
+ - Add unit tests (use relational comparisons with `isEqual`, avoid accessing "first" tuple)
165
+ - Update the README operators table
166
+ - One commit per operator
167
+
168
+ ## License
169
+
170
+ MIT