@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.
Files changed (79) hide show
  1. package/dist/antelope.test.js +2 -2
  2. package/dist/antelope.test.js.map +1 -1
  3. package/dist/atomic.d.ts +41 -0
  4. package/dist/atomic.js +135 -0
  5. package/dist/atomic.js.map +1 -0
  6. package/dist/common.js.map +1 -1
  7. package/dist/datum.d.ts +2 -2
  8. package/dist/datum.js.map +1 -1
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.js +15 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/query.js +1 -1
  13. package/dist/query.js.map +1 -1
  14. package/dist/schema.d.ts +1 -1
  15. package/dist/schema.js +1 -1
  16. package/dist/schema.js.map +1 -1
  17. package/dist/selection.d.ts +4 -1
  18. package/dist/selection.js +7 -0
  19. package/dist/selection.js.map +1 -1
  20. package/dist/stream.d.ts +1 -1
  21. package/dist/stream.js.map +1 -1
  22. package/dist/tests/aggregation_operations.test.js +1 -1
  23. package/dist/tests/aggregation_operations.test.js.map +1 -1
  24. package/dist/tests/atomic_mutation.test.d.ts +1 -0
  25. package/dist/tests/atomic_mutation.test.js +185 -0
  26. package/dist/tests/atomic_mutation.test.js.map +1 -0
  27. package/dist/tests/basic_operations.test.js +1 -1
  28. package/dist/tests/basic_operations.test.js.map +1 -1
  29. package/dist/tests/between_operations.test.js +1 -1
  30. package/dist/tests/between_operations.test.js.map +1 -1
  31. package/dist/tests/change_feeds.test.js +1 -1
  32. package/dist/tests/change_feeds.test.js.map +1 -1
  33. package/dist/tests/datasets/codec_fields.js.map +1 -1
  34. package/dist/tests/date_operations.test.js +1 -1
  35. package/dist/tests/date_operations.test.js.map +1 -1
  36. package/dist/tests/do_operations.test.js +1 -1
  37. package/dist/tests/do_operations.test.js.map +1 -1
  38. package/dist/tests/filters.test.js +1 -1
  39. package/dist/tests/filters.test.js.map +1 -1
  40. package/dist/tests/group_operations.test.js +1 -1
  41. package/dist/tests/group_operations.test.js.map +1 -1
  42. package/dist/tests/instance_modes.test.js +1 -1
  43. package/dist/tests/instance_modes.test.js.map +1 -1
  44. package/dist/tests/joins.test.js +2 -2
  45. package/dist/tests/joins.test.js.map +1 -1
  46. package/dist/tests/lookup_operations.test.js +2 -2
  47. package/dist/tests/lookup_operations.test.js.map +1 -1
  48. package/dist/tests/merge_operations.test.js +2 -2
  49. package/dist/tests/merge_operations.test.js.map +1 -1
  50. package/dist/tests/sorting.test.js +1 -1
  51. package/dist/tests/sorting.test.js.map +1 -1
  52. package/dist/tests/union_operations.test.js +2 -2
  53. package/dist/tests/union_operations.test.js.map +1 -1
  54. package/docs/1.query_types/1.index.md +53 -0
  55. package/docs/1.query_types/2.table.md +114 -0
  56. package/docs/1.query_types/3.selection.md +92 -0
  57. package/docs/1.query_types/4.single_selection.md +121 -0
  58. package/docs/1.query_types/5.stream.md +234 -0
  59. package/docs/1.query_types/6.feed.md +82 -0
  60. package/docs/1.query_types/7.query.md +82 -0
  61. package/docs/1.query_types/8.datum.md +102 -0
  62. package/docs/1.query_types/9.valueproxy.md +232 -0
  63. package/docs/2.operations/1.index.md +34 -0
  64. package/docs/2.operations/2.database_management.md +151 -0
  65. package/docs/2.operations/3.table_operations.md +117 -0
  66. package/docs/2.operations/4.crud.md +214 -0
  67. package/docs/2.operations/5.indexes.md +110 -0
  68. package/docs/2.operations/6.filtering.md +172 -0
  69. package/docs/2.operations/7.lookup.md +135 -0
  70. package/docs/2.operations/8.atomic_mutations.md +71 -0
  71. package/docs/3.results/1.index.md +38 -0
  72. package/docs/3.results/2.write_results.md +86 -0
  73. package/docs/3.results/3.index_changes.md +82 -0
  74. package/docs/3.results/4.table_changes.md +105 -0
  75. package/docs/3.results/5.database_changes.md +96 -0
  76. package/docs/3.results/6.value_changes.md +75 -0
  77. package/docs/index.md +51 -0
  78. package/package.json +62 -42
  79. 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.