@antelopejs/interface-database 0.1.3 → 0.1.5
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/dist/antelope.test.js +2 -2
- package/dist/antelope.test.js.map +1 -1
- package/dist/atomic.d.ts +41 -0
- package/dist/atomic.js +135 -0
- package/dist/atomic.js.map +1 -0
- package/dist/common.js.map +1 -1
- package/dist/datum.d.ts +2 -2
- package/dist/datum.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/query.js +1 -1
- package/dist/query.js.map +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +1 -1
- package/dist/schema.js.map +1 -1
- package/dist/selection.d.ts +4 -1
- package/dist/selection.js +7 -0
- package/dist/selection.js.map +1 -1
- package/dist/stream.d.ts +1 -1
- package/dist/stream.js.map +1 -1
- package/dist/tests/aggregation_operations.test.js +1 -1
- package/dist/tests/aggregation_operations.test.js.map +1 -1
- package/dist/tests/atomic_mutation.test.d.ts +1 -0
- package/dist/tests/atomic_mutation.test.js +185 -0
- package/dist/tests/atomic_mutation.test.js.map +1 -0
- package/dist/tests/basic_operations.test.js +1 -1
- package/dist/tests/basic_operations.test.js.map +1 -1
- package/dist/tests/between_operations.test.js +1 -1
- package/dist/tests/between_operations.test.js.map +1 -1
- package/dist/tests/change_feeds.test.js +1 -1
- package/dist/tests/change_feeds.test.js.map +1 -1
- package/dist/tests/datasets/codec_fields.js.map +1 -1
- package/dist/tests/date_operations.test.js +1 -1
- package/dist/tests/date_operations.test.js.map +1 -1
- package/dist/tests/do_operations.test.js +1 -1
- package/dist/tests/do_operations.test.js.map +1 -1
- package/dist/tests/filters.test.js +1 -1
- package/dist/tests/filters.test.js.map +1 -1
- package/dist/tests/group_operations.test.js +1 -1
- package/dist/tests/group_operations.test.js.map +1 -1
- package/dist/tests/instance_modes.test.js +1 -1
- package/dist/tests/instance_modes.test.js.map +1 -1
- package/dist/tests/joins.test.js +2 -2
- package/dist/tests/joins.test.js.map +1 -1
- package/dist/tests/lookup_operations.test.js +2 -2
- package/dist/tests/lookup_operations.test.js.map +1 -1
- package/dist/tests/merge_operations.test.js +2 -2
- package/dist/tests/merge_operations.test.js.map +1 -1
- package/dist/tests/sorting.test.js +1 -1
- package/dist/tests/sorting.test.js.map +1 -1
- package/dist/tests/union_operations.test.js +2 -2
- package/dist/tests/union_operations.test.js.map +1 -1
- package/docs/1.query_types/1.index.md +53 -0
- package/docs/1.query_types/2.table.md +114 -0
- package/docs/1.query_types/3.selection.md +92 -0
- package/docs/1.query_types/4.single_selection.md +121 -0
- package/docs/1.query_types/5.stream.md +234 -0
- package/docs/1.query_types/6.feed.md +82 -0
- package/docs/1.query_types/7.query.md +82 -0
- package/docs/1.query_types/8.datum.md +102 -0
- package/docs/1.query_types/9.valueproxy.md +232 -0
- package/docs/2.operations/1.index.md +34 -0
- package/docs/2.operations/2.database_management.md +151 -0
- package/docs/2.operations/3.table_operations.md +117 -0
- package/docs/2.operations/4.crud.md +214 -0
- package/docs/2.operations/5.indexes.md +110 -0
- package/docs/2.operations/6.filtering.md +172 -0
- package/docs/2.operations/7.lookup.md +135 -0
- package/docs/2.operations/8.atomic_mutations.md +71 -0
- package/docs/3.results/1.index.md +38 -0
- package/docs/3.results/2.write_results.md +86 -0
- package/docs/3.results/3.index_changes.md +82 -0
- package/docs/3.results/4.table_changes.md +105 -0
- package/docs/3.results/5.database_changes.md +96 -0
- package/docs/3.results/6.value_changes.md +75 -0
- package/docs/index.md +51 -0
- package/package.json +62 -42
- package/skills/database-interface/SKILL.md +101 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# CRUD Operations
|
|
2
|
+
|
|
3
|
+
CRUD operations (Create, Read, Update, Delete) form the foundation of document management in AQL. All examples assume a schema is already defined and an instance is available.
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { Schema } from "@antelopejs/interface-database";
|
|
7
|
+
|
|
8
|
+
const schema = Schema.get("myapp")!;
|
|
9
|
+
const users = schema.instance().table("users");
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Create Documents
|
|
13
|
+
|
|
14
|
+
### Insert a Single Document
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
const ids = await users.insert({
|
|
18
|
+
name: "Alice Johnson",
|
|
19
|
+
email: "alice@example.com",
|
|
20
|
+
status: "active",
|
|
21
|
+
createdAt: new Date(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
console.log("Inserted ID:", ids[0]);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Insert Multiple Documents
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
const ids = await users.insert([
|
|
31
|
+
{ name: "Bob", email: "bob@example.com", status: "active" },
|
|
32
|
+
{ name: "Carol", email: "carol@example.com", status: "active" },
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
console.log("Inserted IDs:", ids);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Upsert (Insert or Update)
|
|
39
|
+
|
|
40
|
+
Use the `conflict` option to handle documents that already exist:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// Update existing fields, keep unmentioned fields
|
|
44
|
+
await users.insert(
|
|
45
|
+
{ _id: "user-123", name: "Updated Name", lastSeen: new Date() },
|
|
46
|
+
{ conflict: "update" },
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Replace the entire document
|
|
50
|
+
await users.insert(
|
|
51
|
+
{ _id: "user-123", name: "Replaced", email: "new@example.com" },
|
|
52
|
+
{ conflict: "replace" },
|
|
53
|
+
);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Read Documents
|
|
57
|
+
|
|
58
|
+
### Get by Primary Key
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
const user = await users.get("user-123");
|
|
62
|
+
|
|
63
|
+
if (user) {
|
|
64
|
+
console.log("Found:", user.name);
|
|
65
|
+
} else {
|
|
66
|
+
console.log("User not found");
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Get by Secondary Index
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
const admins = await users.getAll("admin", "role");
|
|
74
|
+
console.log(`Found ${admins.length} admins`);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Get by Range
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
const recent = await users.between(
|
|
81
|
+
"createdAt",
|
|
82
|
+
new Date("2024-01-01"),
|
|
83
|
+
new Date(),
|
|
84
|
+
);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Filter Documents
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// Filter with a predicate function
|
|
91
|
+
const active = await users.filter((user) =>
|
|
92
|
+
user.key("status").eq("active").and(user.key("age").ge(18)),
|
|
93
|
+
);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Select Specific Fields
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// Keep only listed fields
|
|
100
|
+
const profiles = await users.pluck("_id", "name", "avatar");
|
|
101
|
+
|
|
102
|
+
// Remove sensitive fields
|
|
103
|
+
const safe = await users.without("password", "ssn");
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Order and Paginate
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
const page1 = await users.orderBy("createdAt", "desc").slice(0, 10);
|
|
110
|
+
const page2 = await users.orderBy("createdAt", "desc").slice(10, 10);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Stream Results
|
|
114
|
+
|
|
115
|
+
For large result sets, use async iteration to process documents one at a time:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
for await (const user of users) {
|
|
119
|
+
console.log("Processing:", user.name);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Update Documents
|
|
124
|
+
|
|
125
|
+
### Update a Single Document
|
|
126
|
+
|
|
127
|
+
The `update` method modifies specified fields while preserving all other fields.
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
const count = await users.get("user-123").update({
|
|
131
|
+
status: "active",
|
|
132
|
+
lastLogin: new Date(),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (count === 1) {
|
|
136
|
+
console.log("User updated");
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Update with Computed Values
|
|
141
|
+
|
|
142
|
+
Pass a function to compute new values based on current ones:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
await users.get("user-123").update((user) => ({
|
|
146
|
+
loginCount: user.key("loginCount").add(1),
|
|
147
|
+
lastLogin: new Date(),
|
|
148
|
+
}));
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Update Multiple Documents
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
const count = await users
|
|
155
|
+
.getAll("inactive", "status")
|
|
156
|
+
.update({ status: "active" });
|
|
157
|
+
|
|
158
|
+
console.log(`Activated ${count} users`);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Replace Documents
|
|
162
|
+
|
|
163
|
+
The `replace` method substitutes the entire document. Fields not included in the replacement are removed.
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
const count = await users.get("user-123").replace({
|
|
167
|
+
_id: "user-123",
|
|
168
|
+
name: "Alice Updated",
|
|
169
|
+
email: "alice.new@example.com",
|
|
170
|
+
status: "active",
|
|
171
|
+
updatedAt: new Date(),
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Delete Documents
|
|
176
|
+
|
|
177
|
+
### Delete a Single Document
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
const count = await users.get("user-123").delete();
|
|
181
|
+
|
|
182
|
+
if (count === 1) {
|
|
183
|
+
console.log("User deleted");
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Delete Multiple Documents
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
const count = await users
|
|
191
|
+
.filter((user) => user.key("status").eq("inactive"))
|
|
192
|
+
.delete();
|
|
193
|
+
|
|
194
|
+
console.log(`Deleted ${count} inactive users`);
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Batch Operations
|
|
198
|
+
|
|
199
|
+
For large-scale inserts, batch documents to manage memory and throughput:
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
const documents = Array.from({ length: 10000 }, (_, i) => ({
|
|
203
|
+
index: i,
|
|
204
|
+
value: `Item ${i}`,
|
|
205
|
+
createdAt: new Date(),
|
|
206
|
+
}));
|
|
207
|
+
|
|
208
|
+
const batchSize = 1000;
|
|
209
|
+
for (let i = 0; i < documents.length; i += batchSize) {
|
|
210
|
+
const batch = documents.slice(i, i + batchSize);
|
|
211
|
+
await schema.instance().table("items").insert(batch);
|
|
212
|
+
console.log(`Inserted batch ${Math.floor(i / batchSize) + 1}`);
|
|
213
|
+
}
|
|
214
|
+
```
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Index Management
|
|
2
|
+
|
|
3
|
+
Indexes improve query performance by enabling the database to locate documents quickly based on indexed fields. In AQL, indexes are defined as part of the schema definition.
|
|
4
|
+
|
|
5
|
+
## Define Indexes in a Schema
|
|
6
|
+
|
|
7
|
+
Indexes are declared in the `indexes` property of each table definition:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { Schema } from "@antelopejs/interface-database";
|
|
11
|
+
|
|
12
|
+
const schema = new Schema("myapp", {
|
|
13
|
+
users: {
|
|
14
|
+
fields: { name: "string", email: "string", role: "string", age: "number" },
|
|
15
|
+
indexes: {
|
|
16
|
+
email: {}, // Simple index on the "email" field
|
|
17
|
+
role: {}, // Simple index on the "role" field
|
|
18
|
+
name_age: { fields: ["name", "age"] }, // Compound index
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### `IndexDefinition`
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
interface IndexDefinition {
|
|
28
|
+
fields?: string[]; // Fields for compound indexes (omit for single-field)
|
|
29
|
+
multi?: boolean; // Whether this is a multi-valued index
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
When `fields` is omitted, the index name is used as the field name. For compound indexes, provide the `fields` array explicitly.
|
|
34
|
+
|
|
35
|
+
## Use Indexes for Queries
|
|
36
|
+
|
|
37
|
+
### `getAll()` with an Index
|
|
38
|
+
|
|
39
|
+
Retrieve documents by index value:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
const db = schema.instance();
|
|
43
|
+
const users = db.table("users");
|
|
44
|
+
|
|
45
|
+
// Query by the "role" index
|
|
46
|
+
const admins = await users.getAll("admin", "role");
|
|
47
|
+
|
|
48
|
+
// Query multiple values
|
|
49
|
+
const targets = await users.getAll(["admin", "moderator"], "role");
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### `between()` with an Index
|
|
53
|
+
|
|
54
|
+
Retrieve documents where the index value falls within a range:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
const results = await users.between("age", 18, 65);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### `orderBy()` with an Index
|
|
61
|
+
|
|
62
|
+
Sort results using an indexed field for better performance:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
const sorted = await users.orderBy("email", "asc");
|
|
66
|
+
const newest = await users.orderBy("createdAt", "desc");
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Compound Indexes
|
|
70
|
+
|
|
71
|
+
Compound indexes span multiple fields. Define them with the `fields` array:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
const schema = new Schema("blog", {
|
|
75
|
+
posts: {
|
|
76
|
+
fields: {
|
|
77
|
+
category: "string",
|
|
78
|
+
publishedAt: "date",
|
|
79
|
+
title: "string",
|
|
80
|
+
},
|
|
81
|
+
indexes: {
|
|
82
|
+
category_date: { fields: ["category", "publishedAt"] },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Compound indexes support prefix-based lookups. You can query by the first field alone or by both fields together.
|
|
89
|
+
|
|
90
|
+
## Multi Indexes
|
|
91
|
+
|
|
92
|
+
Set `multi: true` for fields that contain arrays. The database creates an index entry for each element in the array, enabling lookups by any single element:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
const schema = new Schema("content", {
|
|
96
|
+
articles: {
|
|
97
|
+
fields: { tags: ["string"] },
|
|
98
|
+
indexes: {
|
|
99
|
+
tags: { multi: true },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Index Design Guidelines
|
|
106
|
+
|
|
107
|
+
- **Index fields used in `getAll()` and `between()` calls** for best performance.
|
|
108
|
+
- **Compound indexes** are useful when you frequently filter or sort by multiple fields together.
|
|
109
|
+
- **Avoid over-indexing.** Each index adds storage overhead and slows down writes.
|
|
110
|
+
- **Filter early** in your query chain to reduce the number of documents processed.
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Filtering and Querying
|
|
2
|
+
|
|
3
|
+
This guide covers techniques for selecting, comparing, and transforming documents using AQL's filter and ValueProxy capabilities.
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { Schema } from "@antelopejs/interface-database";
|
|
7
|
+
|
|
8
|
+
const schema = Schema.get("myapp")!;
|
|
9
|
+
const users = schema.instance().table("users");
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Basic Filtering
|
|
13
|
+
|
|
14
|
+
Use the `filter` method with a predicate function. The callback receives a `ValueProxy<T>` representing each document.
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
const active = await users.filter((user) => user.key("status").eq("active"));
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Comparison Operators
|
|
21
|
+
|
|
22
|
+
### Equality and Inequality
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
const admins = await users.filter((u) => u.key("role").eq("admin"));
|
|
26
|
+
const nonAdmins = await users.filter((u) => u.key("role").ne("admin"));
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Numeric Comparisons
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
const adults = await users.filter((u) => u.key("age").ge(18));
|
|
33
|
+
const seniors = await users.filter((u) => u.key("age").gt(65));
|
|
34
|
+
const young = await users.filter((u) => u.key("age").lt(18));
|
|
35
|
+
const discounted = await users.filter((u) => u.key("age").le(25));
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Logical Operators
|
|
39
|
+
|
|
40
|
+
Combine conditions with `and()`, `or()`, and `not()`:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// AND
|
|
44
|
+
const verified = await users.filter((u) =>
|
|
45
|
+
u.key("status").eq("active").and(u.key("emailVerified").eq(true)),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// OR
|
|
49
|
+
const important = await users.filter((u) =>
|
|
50
|
+
u.key("role").eq("admin").or(u.key("plan").eq("premium")),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// NOT
|
|
54
|
+
const standard = await users.filter((u) => u.key("plan").eq("premium").not());
|
|
55
|
+
|
|
56
|
+
// Complex combinations
|
|
57
|
+
const target = await users.filter((u) =>
|
|
58
|
+
u
|
|
59
|
+
.key("status")
|
|
60
|
+
.eq("active")
|
|
61
|
+
.and(u.key("plan").eq("premium").or(u.key("totalPurchases").gt(500))),
|
|
62
|
+
);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## String Operations
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// Case-insensitive match
|
|
69
|
+
const bobs = await users.filter((u) => u.key("name").downcase().eq("bob"));
|
|
70
|
+
|
|
71
|
+
// Regex matching
|
|
72
|
+
const gmailUsers = await users.filter((u) =>
|
|
73
|
+
u.key("email").match("@gmail\\.com$"),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// String concatenation in filter
|
|
77
|
+
const fullNameMatch = await users.filter((u) =>
|
|
78
|
+
u.key("firstName").concat(u.key("lastName")).eq("JohnSmith"),
|
|
79
|
+
);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Date Filtering
|
|
83
|
+
|
|
84
|
+
### Date Comparisons
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
const newUsers = await users.filter((u) =>
|
|
88
|
+
u.key("createdAt").gt(new Date("2024-01-01")),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const q1Users = await users.filter((u) =>
|
|
92
|
+
u
|
|
93
|
+
.key("createdAt")
|
|
94
|
+
.ge(new Date("2024-01-01"))
|
|
95
|
+
.and(u.key("createdAt").lt(new Date("2024-04-01"))),
|
|
96
|
+
);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Date Components
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
const thisYear = await users.filter((u) => u.key("createdAt").year().eq(2024));
|
|
103
|
+
|
|
104
|
+
const morningLogins = await users.filter((u) =>
|
|
105
|
+
u.key("lastLogin").hours().lt(12),
|
|
106
|
+
);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Array Operations
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// Check array contents
|
|
113
|
+
const jsPosts = await schema
|
|
114
|
+
.instance()
|
|
115
|
+
.table("posts")
|
|
116
|
+
.filter((post) => post.key("tags").includes("javascript"));
|
|
117
|
+
|
|
118
|
+
// Check array length
|
|
119
|
+
const wellTagged = await schema
|
|
120
|
+
.instance()
|
|
121
|
+
.table("posts")
|
|
122
|
+
.filter((post) => post.key("tags").count().gt(3));
|
|
123
|
+
|
|
124
|
+
// Check if empty
|
|
125
|
+
const untagged = await schema
|
|
126
|
+
.instance()
|
|
127
|
+
.table("posts")
|
|
128
|
+
.filter((post) => post.key("tags").isempty());
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Default Values in Filters
|
|
132
|
+
|
|
133
|
+
Use `default()` to handle potentially null fields:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
const activeUsers = await users.filter((u) =>
|
|
137
|
+
u.key("active").default(false).eq(true),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const frequentVisitors = await users.filter((u) =>
|
|
141
|
+
u.key("visits").default(0).gt(10),
|
|
142
|
+
);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Ordering and Pagination
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
// Sort ascending
|
|
149
|
+
const byName = await users.orderBy("name", "asc");
|
|
150
|
+
|
|
151
|
+
// Sort descending
|
|
152
|
+
const newest = await users.orderBy("createdAt", "desc");
|
|
153
|
+
|
|
154
|
+
// Paginate
|
|
155
|
+
const page1 = await users.orderBy("createdAt").slice(0, 10);
|
|
156
|
+
const page2 = await users.orderBy("createdAt").slice(10, 10);
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Performance Tips
|
|
160
|
+
|
|
161
|
+
1. **Use `getAll()` and `between()` with indexes** instead of `filter()` when possible. Index-based lookups are significantly faster than full scans.
|
|
162
|
+
2. **Filter early** in the query chain to reduce the dataset before applying transformations or aggregations.
|
|
163
|
+
3. **Limit result sets** with `slice()` to avoid transferring unnecessary data.
|
|
164
|
+
4. **Avoid complex computations** inside filter predicates when simpler alternatives exist.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
// Slower: full table scan with filter
|
|
168
|
+
const slow = await users.filter((u) => u.key("email").eq("user@example.com"));
|
|
169
|
+
|
|
170
|
+
// Faster: index-based lookup
|
|
171
|
+
const fast = await users.getAll("user@example.com", "email");
|
|
172
|
+
```
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Lookup
|
|
2
|
+
|
|
3
|
+
The `lookup` method performs a foreign key join, replacing a field in your documents with the referenced data from another table. It is available on both `Stream<T>` and `Datum<T>`.
|
|
4
|
+
|
|
5
|
+
## Parameters
|
|
6
|
+
|
|
7
|
+
| Parameter | Type | Description |
|
|
8
|
+
| ---------- | -------------- | --------------------------------------------------------- |
|
|
9
|
+
| `other` | `Selection<U>` | The table containing the referenced documents |
|
|
10
|
+
| `localKey` | `keyof T` | The field in the current document holding the foreign key |
|
|
11
|
+
| `otherKey` | `keyof U` | The field in the other table to match against |
|
|
12
|
+
|
|
13
|
+
## Behavior
|
|
14
|
+
|
|
15
|
+
The `lookup` method replaces the local key field with the populated data:
|
|
16
|
+
|
|
17
|
+
- If the local key contains a **single value**, the field becomes a **single document** from the other table.
|
|
18
|
+
- If the local key contains an **array of values**, the field becomes an **array of documents** from the other table.
|
|
19
|
+
- TypeScript automatically tracks the type transformation.
|
|
20
|
+
|
|
21
|
+
## One-to-One Relationship
|
|
22
|
+
|
|
23
|
+
Replace a single foreign key with the referenced document:
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { Schema } from "@antelopejs/interface-database";
|
|
27
|
+
|
|
28
|
+
interface Post {
|
|
29
|
+
_id: string;
|
|
30
|
+
title: string;
|
|
31
|
+
authorId: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface User {
|
|
35
|
+
_id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
email: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const schema = Schema.get("blog")!;
|
|
41
|
+
const db = schema.instance();
|
|
42
|
+
|
|
43
|
+
const postsWithAuthors = await db
|
|
44
|
+
.table<Post>("posts")
|
|
45
|
+
.lookup(db.table<User>("users"), "authorId", "_id");
|
|
46
|
+
|
|
47
|
+
// Result type: { _id: string, title: string, authorId: User }
|
|
48
|
+
for (const post of postsWithAuthors) {
|
|
49
|
+
console.log(`${post.title} by ${post.authorId.name}`);
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## One-to-Many Relationship
|
|
54
|
+
|
|
55
|
+
Replace an array of foreign keys with an array of documents:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
interface Order {
|
|
59
|
+
_id: string;
|
|
60
|
+
customerId: string;
|
|
61
|
+
productIds: string[];
|
|
62
|
+
total: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface Product {
|
|
66
|
+
_id: string;
|
|
67
|
+
name: string;
|
|
68
|
+
price: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const ordersWithProducts = await db
|
|
72
|
+
.table<Order>("orders")
|
|
73
|
+
.lookup(db.table<Product>("products"), "productIds", "_id");
|
|
74
|
+
|
|
75
|
+
// Result type: { ..., productIds: Product[] }
|
|
76
|
+
for (const order of ordersWithProducts) {
|
|
77
|
+
for (const product of order.productIds) {
|
|
78
|
+
console.log(` - ${product.name}: $${product.price}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Combine with Other Operations
|
|
84
|
+
|
|
85
|
+
Lookup works seamlessly with filters, sorting, and other stream operations:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// Filter before lookup
|
|
89
|
+
const recentWithCustomers = await db
|
|
90
|
+
.table("orders")
|
|
91
|
+
.filter((order) => order.key("createdAt").gt(new Date("2024-01-01")))
|
|
92
|
+
.lookup(db.table("customers"), "customerId", "_id");
|
|
93
|
+
|
|
94
|
+
// Lookup on a single document
|
|
95
|
+
const post = await db
|
|
96
|
+
.table<Post>("posts")
|
|
97
|
+
.get("post-1")
|
|
98
|
+
.lookup(db.table<User>("users"), "authorId", "_id");
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Lookup vs Join
|
|
102
|
+
|
|
103
|
+
### Use `lookup` when:
|
|
104
|
+
|
|
105
|
+
- You have a straightforward foreign key relationship.
|
|
106
|
+
- You want the referenced data to replace the foreign key field.
|
|
107
|
+
- You are working with one-to-one or one-to-many relationships.
|
|
108
|
+
|
|
109
|
+
### Use `join` or `joinInner` when:
|
|
110
|
+
|
|
111
|
+
- You need custom join predicates beyond simple equality.
|
|
112
|
+
- You want to keep both original and joined data as separate fields.
|
|
113
|
+
- You need to transform the result into a custom structure.
|
|
114
|
+
|
|
115
|
+
### Comparison
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// Lookup: replaces the field
|
|
119
|
+
const withLookup = await db
|
|
120
|
+
.table("orders")
|
|
121
|
+
.lookup(db.table("customers"), "customerId", "_id");
|
|
122
|
+
// order.customerId is now the full customer object
|
|
123
|
+
|
|
124
|
+
// Join: custom output structure
|
|
125
|
+
const withJoin = await db.table("orders").joinInner(
|
|
126
|
+
db.table("customers"),
|
|
127
|
+
(order, customer) => order.key("customerId").eq(customer.key("_id")),
|
|
128
|
+
(order, customer) => ({
|
|
129
|
+
orderId: order.key("_id"),
|
|
130
|
+
orderTotal: order.key("total"),
|
|
131
|
+
customerName: customer.key("name"),
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
// Result is a custom object with selected fields from both tables
|
|
135
|
+
```
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Atomic single-record mutations
|
|
2
|
+
|
|
3
|
+
`Table.atomicMutation(id, request)` checks one immutable, instance-scoped record identity and one condition inside the database mutation. It returns `Query<AtomicMutationOutcome>`. It does not provide multi-record transactions, a lock service, receipts, or external-effect coordination.
|
|
4
|
+
|
|
5
|
+
Generic `filter(...).update(...)` and `filter(...).delete()` do **not** promise this guarantee. Use a direct `schema.instance(instanceId).table(tableId)` with one literal string identity. The default instance is supported; `CROSS_INSTANCE`, filtered selections, expression keys, cursors, and upserts are not.
|
|
6
|
+
|
|
7
|
+
## Compare a revision
|
|
8
|
+
|
|
9
|
+
Public types are exported from `@antelopejs/interface-database` and `@antelopejs/interface-database/atomic`. An update checks the current revision and installs the patch and next revision together. A delete checks the revision inside the deletion.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
const outcome = await table.atomicMutation(recordId, {
|
|
13
|
+
type: "update",
|
|
14
|
+
revisionField: "revision",
|
|
15
|
+
expectedRevision: observed.revision,
|
|
16
|
+
nextRevision: freshRevision,
|
|
17
|
+
patch: { title: "Updated title" },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const deletion = await table.atomicMutation(recordId, {
|
|
21
|
+
type: "delete",
|
|
22
|
+
revisionField: "revision",
|
|
23
|
+
expectedRevision: observed.revision,
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Revision tokens are nonempty strings. Each update must install a changed token that the caller guarantees has never been used for that identity. All writers of protected state must advance the revision; otherwise a successful comparison does not prove that the observed state is current. Initialize new records with fresh revisions and use a new identity for each incarnation, including recreation after deletion.
|
|
28
|
+
|
|
29
|
+
`expectedRevision: { kind: "missing" }` matches an **absent revision field on an existing record**. It does not match a stored `null`, an empty string, or a missing record. An update can use this expectation to bootstrap legacy data atomically; a delete can use it when the domain permits deleting an unversioned record. A backend that cannot distinguish absence from null must reject this expectation rather than weaken it. In PostgreSQL, declared SQL columns collapse this distinction; an overflow JSON field can preserve it. Review mapper serialization before choosing revision storage.
|
|
30
|
+
|
|
31
|
+
## Patch semantics
|
|
32
|
+
|
|
33
|
+
The patch replaces each supplied top-level field. Nested objects and arrays replace their entire previous values; they do not merge recursively. Unspecified fields remain unchanged, and `null` stores null rather than removing the field. There is no unset operation.
|
|
34
|
+
|
|
35
|
+
Patches contain only constant strings, booleans, finite numbers, null, valid Dates, dense arrays, and plain objects. Undefined, functions, query proxies, cycles, and operator/path field names are rejected. The patch cannot contain `id`, `_id`, the revision field, or adapter-owned scope fields. Adapters must preserve their normal Date storage representation without interpreting objects that resemble query stages as executable expressions.
|
|
36
|
+
|
|
37
|
+
## Delete by one observed scalar
|
|
38
|
+
|
|
39
|
+
`deleteIfEqual` provides a narrower condition for domains where one field equality is sufficient, such as deletion of an expired record whose observed timestamp remains unchanged.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
const outcome = await table.atomicMutation(recordId, {
|
|
43
|
+
type: "deleteIfEqual",
|
|
44
|
+
field: "lastSeenAt",
|
|
45
|
+
expectedValue: observed.lastSeenAt,
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`expectedValue` accepts a string, finite number, boolean, or valid Date. Equality is exact and type-sensitive, with Dates compared by their stored instant; scalar equality must not match an array containing that scalar. Missing fields never match. Null, objects, arrays, identity fields, and scope fields are not supported.
|
|
50
|
+
|
|
51
|
+
This variant does not check or advance a revision and does not prevent ABA: a value can change and later return to its observed value. The domain must prove that equality alone authorizes deletion. It is not a substitute for revision checks on state transitions.
|
|
52
|
+
|
|
53
|
+
## Handle all outcomes
|
|
54
|
+
|
|
55
|
+
| Outcome | Meaning |
|
|
56
|
+
| ------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
57
|
+
| `applied` | The adapter received definitive acknowledgement that the one mutation applied. |
|
|
58
|
+
| `not-applied` | The adapter received definitive acknowledgement that the target was missing or its scope or condition did not match. |
|
|
59
|
+
| `unknown` | The adapter cannot determine whether the mutation applied, including acknowledgement loss. |
|
|
60
|
+
|
|
61
|
+
Definitive validation and unsupported-operation errors throw. Neither an exception nor `unknown` means mismatch. The interface rejects invalid adapter results, including `undefined`, zero, and generic modified counts, with `AtomicMutationUnsupportedError`; adapters must reject unsupported stages explicitly. There is no fallback to generic filtered writes and no separate capability probe. Use an adapter whose implementation has been verified before relying on this API.
|
|
62
|
+
|
|
63
|
+
The interface and adapter must not automatically retry an uncertain mutation. Do not repeat external effects or infer failure from a later mismatch. Domain-specific recovery can reconcile durable evidence and receipts, but a fresh read alone cannot prove whether an earlier operation or external effect occurred. Any deliberate replay requires a domain proof and does not turn `not-applied` into evidence about the original attempt.
|
|
64
|
+
|
|
65
|
+
## Adapter contract
|
|
66
|
+
|
|
67
|
+
The canonical terminal stage is `{ stage: "atomicMutation", options: undefined, args: [id, request] }`, immediately after `schema`, `instance`, and `table`. `AtomicMutation<T>` is the union of `AtomicUpdate<T>`, `AtomicDelete<T>`, and `AtomicDeleteIfEqual<T>`.
|
|
68
|
+
|
|
69
|
+
Adapters can reuse `ValidateAtomicMutation(id, request, protectedFields)` and `ValidateAtomicMutationTable(prefixStages)`. They must additionally validate the terminal envelope and schema-specific restrictions. Scope, immutable identity, and the revision or equality condition must participate in the target-local native mutation, not only an earlier selection or identity subquery. Backend tests must prove contention, scope isolation, missing/null distinction, top-level replacement, and acknowledgement-loss behavior.
|
|
70
|
+
|
|
71
|
+
This API remains unpublished during adapter and consumer verification. Package metadata alone does not establish capability; do not infer support from the unchanged development package version.
|