Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
130ecd7
Update 007 ideation: revise for 2.0 API, add route options for 3 feat…
claude Mar 10, 2026
0a8f851
Add plan for .minus() and .delete().where()
claude Mar 10, 2026
e8f8989
Record Feature 1 decision: Route A for .minus() with SPARQL MINUS
claude Mar 10, 2026
1b67553
Record Feature 2 decision: schema-aware bulk delete with blank node c…
claude Mar 10, 2026
fedee4d
Record Feature 3 decision: .update().where() + .forAll() on UpdateBui…
claude Mar 11, 2026
87803f1
Resolve cross-cutting questions: defer named graphs, order TBD in pla…
claude Mar 11, 2026
291e4dc
Add implementation plan for advanced query patterns (MINUS, bulk dele…
claude Mar 11, 2026
a39f138
Phase 1: Add IR types for MINUS, bulk delete, and conditional update
claude Mar 11, 2026
c5e5afb
Phase 2: Implement .minus() on QueryBuilder
claude Mar 11, 2026
15d3182
feat: implement bulk delete — .deleteAll(), .delete().where(), Shape.…
claude Mar 11, 2026
ac395a8
feat: implement conditional update — .update().where() and .update().…
claude Mar 11, 2026
cd93175
docs: mark all phases as completed in plan doc
claude Mar 11, 2026
08f3335
fix: include IRDesugar and IRLower changes missed from Phase 2 commit
claude Mar 11, 2026
8d8f5e3
refactor: address review gaps — shared helper, void returns, equivale…
claude Mar 11, 2026
15c0c38
docs: add Phase 6 plan for MINUS multi-property existence support
claude Mar 11, 2026
5a0121e
docs: revise Phase 6 plan to support nested paths in MINUS multi-prop…
claude Mar 11, 2026
ec99875
feat: implement MINUS multi-property with nested path support
claude Mar 11, 2026
669df80
refactor: cleanup — deprecate sortBy, require update data, simplify d…
claude Mar 11, 2026
d3c1e91
docs: add wrapup report, changeset, and review section for advanced q…
claude Mar 11, 2026
b3691a8
docs: update README for current API, remove ideation and plan docs
claude Mar 11, 2026
c6f5a36
Merge pull request #27
flyon Mar 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .changeset/advanced-query-patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"@_linked/core": patch
---

Add MINUS support on QueryBuilder with multiple call styles:
- `.minus(Shape)` — exclude by shape type
- `.minus(p => p.prop.equals(val))` — exclude by condition
- `.minus(p => p.prop)` — exclude by property existence
- `.minus(p => [p.prop1, p.nested.prop2])` — exclude by multi-property existence with nested path support

Add bulk delete operations:
- `Shape.deleteAll()` / `DeleteBuilder.from(Shape).all()` — delete all instances with schema-aware blank node cleanup
- `Shape.deleteWhere(fn)` / `DeleteBuilder.from(Shape).where(fn)` — conditional delete

Add conditional update operations:
- `.update(data).where(fn)` — update matching instances
- `.update(data).forAll()` — update all instances

API cleanup:
- Deprecate `sortBy()` in favor of `orderBy()`
- Remove `DeleteBuilder.for()` — use `DeleteBuilder.from(shape, ids)` instead
- Require `data` parameter in `Shape.update(data)`
74 changes: 50 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,9 +246,7 @@ const allFriends = await Person.select((p) => p.knows.selectAll());

**3) Apply a simple mutation**
```typescript
const updated = await Person.update({
name: 'Alicia',
}).for({id: 'https://my.app/node1'});
const updated = await Person.update({name: 'Alicia'}).for({id: 'https://my.app/node1'});
/* updated: {id: string} & UpdatePartial<Person> */
```

Expand Down Expand Up @@ -291,10 +289,11 @@ The query DSL is schema-parameterized: you define your own SHACL shapes, and Lin
- Counting with `.size()`
- Custom result formats (object mapping)
- Type casting with `.as(Shape)`
- MINUS exclusion (by shape, property, condition, multi-property, nested path)
- Sorting, limiting, and `.one()`
- Query context variables
- Preloading (`preloadFor`) for component-like queries
- Create / Update / Delete mutations
- Create / Update / Delete mutations (including bulk and conditional)
- Dynamic query building with `QueryBuilder`
- Composable field sets with `FieldSet`
- Mutation builders (`CreateBuilder`, `UpdateBuilder`, `DeleteBuilder`)
Expand Down Expand Up @@ -410,9 +409,27 @@ And you want to select properties of those pets that are dogs:
const guards = await Person.select((p) => p.pets.as(Dog).guardDogLevel);
```

#### MINUS (exclusion)
```typescript
// Exclude by shape — all Persons that are NOT also Employees
const nonEmployees = await Person.select((p) => p.name).minus(Employee);

// Exclude by property existence — Persons that do NOT have a hobby
const noHobby = await Person.select((p) => p.name).minus((p) => p.hobby);

// Exclude by multiple properties — Persons missing BOTH hobby AND nickNames
const sparse = await Person.select((p) => p.name).minus((p) => [p.hobby, p.nickNames]);

// Exclude by nested path — Persons whose bestFriend does NOT have a name
const unnamed = await Person.select((p) => p.name).minus((p) => [p.bestFriend.name]);

// Exclude by condition — Persons whose hobby is NOT 'Chess'
const noChess = await Person.select((p) => p.name).minus((p) => p.hobby.equals('Chess'));
```

#### Sorting, limiting, one
```typescript
const sorted = await Person.select((p) => p.name).sortBy((p) => p.name, 'ASC');
const sorted = await Person.select((p) => p.name).orderBy((p) => p.name, 'ASC');
const limited = await Person.select((p) => p.name).limit(1);
const single = await Person.select((p) => p.name).one();
```
Expand Down Expand Up @@ -447,8 +464,10 @@ Where UpdatePartial<Shape> reflects the created properties.

#### Update

Update will patch any property that you send as payload and leave the rest untouched. Chain `.for(id)` to target the entity:
Update will patch any property that you send as payload and leave the rest untouched. The data to update is required:

```typescript
// Target a specific entity with .for(id)
/* Result: {id: string} & UpdatePartial<Person> */
const updated = await Person.update({name: 'Alicia'}).for({id: 'https://my.app/node1'});
```
Expand All @@ -460,6 +479,15 @@ Returns:
}
```

**Conditional and bulk updates:**
```typescript
// Update all matching entities
const archived = await Person.update({status: 'archived'}).where(p => p.status.equals('inactive'));

// Update all instances of a shape
await Person.update({verified: true}).forAll();
```

**Updating multi-value properties**
When updating a property that holds multiple values (one that returns an array in the results), you can either overwrite all the values with a new explicit array of values, or delete from/add to the current values.

Expand Down Expand Up @@ -503,27 +531,19 @@ This returns an object with the added and removed items


#### Delete
To delete a node entirely:

```typescript
/* Result: {deleted: Array<{id: string}>, count: number} */
// Delete a single node
const deleted = await Person.delete({id: 'https://my.app/node1'});
```
Returns
```json
{
deleted:[
{id:"https://my.app/node1"}
],
count:1
}
```

To delete multiple nodes pass an array:
// Delete multiple nodes
const deleted = await Person.delete([{id: 'https://my.app/node1'}, {id: 'https://my.app/node2'}]);

```typescript
/* Result: {deleted: Array<{id: string}>, count: number} */
const deleted = await Person.delete([{id: 'https://my.app/node1'},{id: 'https://my.app/node2'}]);
// Delete all instances of a shape (with blank node cleanup)
await Person.deleteAll();

// Conditional delete
await Person.deleteWhere(p => p.status.equals('inactive'));
```


Expand Down Expand Up @@ -716,8 +736,14 @@ const updated = await UpdateBuilder.from(Person)
.for({id: 'https://my.app/alice'})
.set({name: 'Alicia'});

// Delete — equivalent to Person.delete({id: '...'})
const deleted = await DeleteBuilder.from(Person).for({id: 'https://my.app/alice'});
// Delete by ID — equivalent to Person.delete({id: '...'})
const deleted = await DeleteBuilder.from(Person, {id: 'https://my.app/alice'});

// Delete all — equivalent to Person.deleteAll()
await DeleteBuilder.from(Person).all();

// Conditional update — equivalent to Person.update({...}).where(fn)
await UpdateBuilder.from(Person).set({verified: true}).forAll();

// All builders are PromiseLike — await them or call .build() for the IR
const ir = CreateBuilder.from(Person).set({name: 'Alice'}).build();
Expand Down
174 changes: 0 additions & 174 deletions docs/ideas/007-advanced-query-patterns.md

This file was deleted.

Loading
Loading