@lotics/app-sdk 0.63.0 → 0.64.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.
- package/AGENTS.md +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/new_record.d.ts +69 -0
- package/dist/src/new_record.js +117 -0
- package/docs/ai.md +3 -0
- package/docs/mutations.md +37 -0
- package/docs/queries.md +22 -0
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -16,7 +16,7 @@ signature; open the file.**
|
|
|
16
16
|
|---|---|
|
|
17
17
|
| [docs/queries.md](./docs/queries.md) | **The query engine authoring reference** — AST node kinds, per-field-type operator support, filters/params/pruning, free-text search, combining tables (join/union/link/`unnest`/`record_id`), shaping (aggregates, date buckets, windows), runtime refinement bounds, limits & the efficiency playbook. |
|
|
18
18
|
| [docs/data_fetching.md](./docs/data_fetching.md) | The three read hooks (`useQuery`/`useInfiniteQuery`/`usePaginatedQuery`), the `QueryRow` shape (projected columns `unknown`; `__source_record_id`/`__source_table_id` typed but optional), cell readers (`row.*`, `readSelect`, `readMembers`, `readLinks`, `readFiles`, `readLocked`), `useFieldOptions`, data discipline, the search-as-you-type + record-picker patterns. |
|
|
19
|
-
| [docs/mutations.md](./docs/mutations.md) | `useWorkflow` (the ONLY write path), the `WorkflowResult` resolve-never-throw contract, typed inputs, diff-before-update, locked records, `useOptimistic`, read-after-write ordering (a re-read must not overtake an in-flight write). |
|
|
19
|
+
| [docs/mutations.md](./docs/mutations.md) | `useWorkflow` (the ONLY write path), the `WorkflowResult` resolve-never-throw contract, typed inputs, diff-before-update, locked records, `useOptimistic`, `useNewRecord` (client-minted `rec_*` id so a new-record surface never remounts on its first save), read-after-write ordering (a re-read must not overtake an in-flight write). |
|
|
20
20
|
| [docs/workflows.md](./docs/workflows.md) | **The workflow-BODY authoring reference** — the JS subset a `src/workflows/<alias>.ts` body may use: the parse-at-save/never-execute model, opaque `fld_*`/`opt_*` keys, expression sources + link descent, every step form (tool call, `agent`, waits, `validate`, `return`), the accepted sugar and its canonical lowering, helpers + callback rules, record-write surfaces, the traps, the bright line, and the verify loop — `check` (the only local gate: the app's own `npm run typecheck` never sees a body) → `dry_run_workflow` (static green is not a run) → `set`. |
|
|
21
21
|
| [docs/files.md](./docs/files.md) | Files end to end — `useFileUpload`, `useAttachments`, `readFiles`/presigned URLs (**a bearer credential for the bytes** — never logged, reported, or persisted), workflow-generated files, **naming a zip's entries** (`{ id, name }` per file — a file name, never a path), preview pairing, filter operators, the server-side delivery bounds. **Uploads declare a `fidelity`** (`standard` / `high` / `original`) — the app picks how much of the image survives storage; use `high` whenever text must stay legible. |
|
|
22
22
|
| [docs/members_and_options.md](./docs/members_and_options.md) | People + select options + comments — `useMembers`, `useFieldOptions`, `useViewer`, `useComments`, and the `@lotics/ui` components they feed. |
|
package/dist/src/index.d.ts
CHANGED
|
@@ -40,6 +40,8 @@ export { row, readLinks, readFiles, readLocked } from "./row.js";
|
|
|
40
40
|
export type { ResolvedLink, AppFile } from "./row.js";
|
|
41
41
|
export { useOptimistic } from "./use_optimistic.js";
|
|
42
42
|
export type { OptimisticApi } from "./use_optimistic.js";
|
|
43
|
+
export { useNewRecord, newRecordId } from "./new_record.js";
|
|
44
|
+
export type { NewRecordApi } from "./new_record.js";
|
|
43
45
|
export { useRecents } from "./use_recents.js";
|
|
44
46
|
export type { RecentsApi, RecentsOptions } from "./use_recents.js";
|
|
45
47
|
export { useUrlState } from "./use_url_state.js";
|
package/dist/src/index.js
CHANGED
|
@@ -28,6 +28,7 @@ export { readMembers } from "./members.js";
|
|
|
28
28
|
export { readSelect } from "./select.js";
|
|
29
29
|
export { row, readLinks, readFiles, readLocked } from "./row.js";
|
|
30
30
|
export { useOptimistic } from "./use_optimistic.js";
|
|
31
|
+
export { useNewRecord, newRecordId } from "./new_record.js";
|
|
31
32
|
export { useRecents } from "./use_recents.js";
|
|
32
33
|
export { useUrlState } from "./use_url_state.js";
|
|
33
34
|
export { urlParam } from "./url_params.js";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mint a record id locally, in the shape the platform mints.
|
|
3
|
+
*
|
|
4
|
+
* The generator is restated here rather than imported because the canonical one
|
|
5
|
+
* lives in a package that is not published, and this one is. The duplication is
|
|
6
|
+
* safe by construction rather than by discipline: the server validates the shape on
|
|
7
|
+
* every write, so a client that drifted would be rejected loudly at the first call
|
|
8
|
+
* instead of quietly persisting a malformed primary key.
|
|
9
|
+
*
|
|
10
|
+
* Uses `crypto.getRandomValues` — available in every browser this SDK runs in — and
|
|
11
|
+
* rejection-samples so each character is uniformly drawn from the alphabet. A plain
|
|
12
|
+
* `% 62` over bytes would bias the first 8 characters, which is a poor property for
|
|
13
|
+
* something used as a primary key.
|
|
14
|
+
*/
|
|
15
|
+
export declare function newRecordId(): string;
|
|
16
|
+
export interface NewRecordApi<P> {
|
|
17
|
+
/**
|
|
18
|
+
* The id this surface writes under, stable from the first render — before the
|
|
19
|
+
* record exists, and unchanged by its creation.
|
|
20
|
+
*/
|
|
21
|
+
id: string;
|
|
22
|
+
/**
|
|
23
|
+
* Persist a patch. The first call creates the record, every later one updates it.
|
|
24
|
+
* Calls are serialised, so firing several before the first resolves is safe.
|
|
25
|
+
*
|
|
26
|
+
* Rejects with whatever `create`/`update` rejected with; a failed create leaves the
|
|
27
|
+
* record uncreated and the next call will try again.
|
|
28
|
+
*/
|
|
29
|
+
save: (patch: P) => Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A record that does not exist yet, named before it does.
|
|
33
|
+
*
|
|
34
|
+
* The problem this removes: when the server mints the id, a surface editing a new
|
|
35
|
+
* record has nothing to identify it by until the first write returns. Everything
|
|
36
|
+
* keyed on that id — the route, the drawer, a list selection — therefore changes
|
|
37
|
+
* identity mid-edit, which React resolves by remounting the surface the user is
|
|
38
|
+
* typing into. Minting the id locally makes it stable from the first render, so the
|
|
39
|
+
* create stops being an event the UI has to survive.
|
|
40
|
+
*
|
|
41
|
+
* Creation still happens on the first write, not on mount: a surface the user opens
|
|
42
|
+
* and abandons should leave nothing behind.
|
|
43
|
+
*
|
|
44
|
+
* The transport is the caller's. Like `useOptimistic`, this hook has no idea how the
|
|
45
|
+
* app persists anything — it takes `create` and `update` thunks and owns only the id
|
|
46
|
+
* and the ordering, which is the part that is easy to get wrong:
|
|
47
|
+
*
|
|
48
|
+
* - Two blur-saves fired before the first resolves must not both create. That is a
|
|
49
|
+
* duplicate record, and it is the failure this exists to prevent.
|
|
50
|
+
* - A save arriving mid-create must wait for it, or it updates a row that is not
|
|
51
|
+
* there yet.
|
|
52
|
+
* - A create that FAILS must not latch. Otherwise every later save updates a record
|
|
53
|
+
* that was never written, and the user's work goes nowhere while looking saved.
|
|
54
|
+
*
|
|
55
|
+
* ```tsx
|
|
56
|
+
* const { id, save } = useNewRecord({
|
|
57
|
+
* create: (id, patch) => createCustomer({ record_id: id, ...patch }),
|
|
58
|
+
* update: (id, patch) => updateCustomer({ record_id: id, ...patch }),
|
|
59
|
+
* onCreated: refetch,
|
|
60
|
+
* });
|
|
61
|
+
* <InlineText onBlur={(name) => save({ name })} />
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare function useNewRecord<P>(opts: {
|
|
65
|
+
create: (id: string, patch: P) => Promise<unknown>;
|
|
66
|
+
update: (id: string, patch: P) => Promise<unknown>;
|
|
67
|
+
/** Runs once, after the record first exists. Pass a list `refetch` to reveal it. */
|
|
68
|
+
onCreated?: (id: string) => void;
|
|
69
|
+
}): NewRecordApi<P>;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
const ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
3
|
+
const ID_LENGTH = 12;
|
|
4
|
+
/**
|
|
5
|
+
* Mint a record id locally, in the shape the platform mints.
|
|
6
|
+
*
|
|
7
|
+
* The generator is restated here rather than imported because the canonical one
|
|
8
|
+
* lives in a package that is not published, and this one is. The duplication is
|
|
9
|
+
* safe by construction rather than by discipline: the server validates the shape on
|
|
10
|
+
* every write, so a client that drifted would be rejected loudly at the first call
|
|
11
|
+
* instead of quietly persisting a malformed primary key.
|
|
12
|
+
*
|
|
13
|
+
* Uses `crypto.getRandomValues` — available in every browser this SDK runs in — and
|
|
14
|
+
* rejection-samples so each character is uniformly drawn from the alphabet. A plain
|
|
15
|
+
* `% 62` over bytes would bias the first 8 characters, which is a poor property for
|
|
16
|
+
* something used as a primary key.
|
|
17
|
+
*/
|
|
18
|
+
export function newRecordId() {
|
|
19
|
+
const max = 256 - (256 % ID_ALPHABET.length);
|
|
20
|
+
let out = "";
|
|
21
|
+
const buf = new Uint8Array(ID_LENGTH * 2);
|
|
22
|
+
while (out.length < ID_LENGTH) {
|
|
23
|
+
crypto.getRandomValues(buf);
|
|
24
|
+
for (let i = 0; i < buf.length && out.length < ID_LENGTH; i++) {
|
|
25
|
+
if (buf[i] < max)
|
|
26
|
+
out += ID_ALPHABET[buf[i] % ID_ALPHABET.length];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return `rec_${out}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A record that does not exist yet, named before it does.
|
|
33
|
+
*
|
|
34
|
+
* The problem this removes: when the server mints the id, a surface editing a new
|
|
35
|
+
* record has nothing to identify it by until the first write returns. Everything
|
|
36
|
+
* keyed on that id — the route, the drawer, a list selection — therefore changes
|
|
37
|
+
* identity mid-edit, which React resolves by remounting the surface the user is
|
|
38
|
+
* typing into. Minting the id locally makes it stable from the first render, so the
|
|
39
|
+
* create stops being an event the UI has to survive.
|
|
40
|
+
*
|
|
41
|
+
* Creation still happens on the first write, not on mount: a surface the user opens
|
|
42
|
+
* and abandons should leave nothing behind.
|
|
43
|
+
*
|
|
44
|
+
* The transport is the caller's. Like `useOptimistic`, this hook has no idea how the
|
|
45
|
+
* app persists anything — it takes `create` and `update` thunks and owns only the id
|
|
46
|
+
* and the ordering, which is the part that is easy to get wrong:
|
|
47
|
+
*
|
|
48
|
+
* - Two blur-saves fired before the first resolves must not both create. That is a
|
|
49
|
+
* duplicate record, and it is the failure this exists to prevent.
|
|
50
|
+
* - A save arriving mid-create must wait for it, or it updates a row that is not
|
|
51
|
+
* there yet.
|
|
52
|
+
* - A create that FAILS must not latch. Otherwise every later save updates a record
|
|
53
|
+
* that was never written, and the user's work goes nowhere while looking saved.
|
|
54
|
+
*
|
|
55
|
+
* ```tsx
|
|
56
|
+
* const { id, save } = useNewRecord({
|
|
57
|
+
* create: (id, patch) => createCustomer({ record_id: id, ...patch }),
|
|
58
|
+
* update: (id, patch) => updateCustomer({ record_id: id, ...patch }),
|
|
59
|
+
* onCreated: refetch,
|
|
60
|
+
* });
|
|
61
|
+
* <InlineText onBlur={(name) => save({ name })} />
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export function useNewRecord(opts) {
|
|
65
|
+
const idRef = useRef("");
|
|
66
|
+
if (idRef.current === "")
|
|
67
|
+
idRef.current = newRecordId();
|
|
68
|
+
// Callbacks are read through a ref so `save` keeps a stable identity across
|
|
69
|
+
// renders — it is typically handed to an `onBlur`, and a new function every render
|
|
70
|
+
// would re-bind every field on every keystroke.
|
|
71
|
+
//
|
|
72
|
+
// Updated in an effect rather than during render: React only sanctions writing a
|
|
73
|
+
// ref while rendering for one-time initialisation (as `idRef` above does), because
|
|
74
|
+
// a render that is discarded would otherwise leave the ref holding props that never
|
|
75
|
+
// committed. `save` runs from event handlers, which are always after commit, so the
|
|
76
|
+
// effect is early enough.
|
|
77
|
+
const optsRef = useRef(opts);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
optsRef.current = opts;
|
|
80
|
+
});
|
|
81
|
+
/** The in-flight or settled create. `null` means the record does not exist yet. */
|
|
82
|
+
const createdRef = useRef(null);
|
|
83
|
+
/** Tail of the write chain, so saves apply in the order the user made them. */
|
|
84
|
+
const queueRef = useRef(Promise.resolve());
|
|
85
|
+
const save = useCallback((patch) => {
|
|
86
|
+
const run = async () => {
|
|
87
|
+
const id = idRef.current;
|
|
88
|
+
const { create, update, onCreated } = optsRef.current;
|
|
89
|
+
if (createdRef.current === null) {
|
|
90
|
+
const attempt = (async () => {
|
|
91
|
+
await create(id, patch);
|
|
92
|
+
})();
|
|
93
|
+
createdRef.current = attempt;
|
|
94
|
+
try {
|
|
95
|
+
await attempt;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
// Unlatch. The record was not written, so the next save has to be free to
|
|
99
|
+
// create it — leaving the promise in place would send every later write to
|
|
100
|
+
// a row that does not exist.
|
|
101
|
+
createdRef.current = null;
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
onCreated?.(id);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
await createdRef.current;
|
|
108
|
+
await update(id, patch);
|
|
109
|
+
};
|
|
110
|
+
// Chained on both settle paths: one failed save must not strand the ones behind
|
|
111
|
+
// it, but they still have to run in order.
|
|
112
|
+
const result = queueRef.current.then(run, run);
|
|
113
|
+
queueRef.current = result.catch(() => undefined);
|
|
114
|
+
return result;
|
|
115
|
+
}, []);
|
|
116
|
+
return { id: idRef.current, save };
|
|
117
|
+
}
|
package/docs/ai.md
CHANGED
|
@@ -209,6 +209,8 @@ Sessions are scoped to the authenticated member who ran them: two members using
|
|
|
209
209
|
|---|---|---|
|
|
210
210
|
| **Perceived natively** | `image/*`, `application/pdf` | A vision / document part — the agent literally sees it |
|
|
211
211
|
| **Materialized** | Word (`.docx`/`.doc`), Excel (`.xlsx`/`.xls`), CSV, and text (`.txt`, `.md`, `.json`, `.eml`, `.html`, `.xml`, `.yaml`) | Read server-side by the same engines `view_files` uses and inlined into the run's message, truncated at 40,000 characters with the agent told when that happened |
|
|
212
|
+
|
|
213
|
+
**Pictures inside a Word document are delivered as images**, so a scanned page, an ID card photographed into a `.docx`, or a screenshot pasted into one is READ, not merely described. A document whose content is entirely pictures carries no text at all — it used to arrive as empty paragraphs plus a size in centimetres, and the agent correctly reported it could not read it. Any picture that is skipped or unresolvable is counted and stated in the text, so a partially-read document never reads as a complete one.
|
|
212
214
|
| **Unreadable** | Archives, audio, video | The run fails immediately, naming the file — a fact about the upload, not a gap in your configuration |
|
|
213
215
|
|
|
214
216
|
Every file in a `multi` input is materialized — nothing is collapsed to the first. Get the ids from [`useFileUpload` / `useAttachments`](./files.md).
|
|
@@ -223,6 +225,7 @@ Every file in a `multi` input is materialized — nothing is collapsed to the fi
|
|
|
223
225
|
- **Owner-billed.** Token usage is recorded against the app's organization's AI credits. The quota is enforced before the first model call (a `run()` rejection) and re-checked between steps (a mid-run exhaustion settles the run as an error).
|
|
224
226
|
- **Hard duration cap: 20 minutes per run.** A capped run settles as an error — but with whatever partial transcript and captured output it produced, recoverable from the persisted run.
|
|
225
227
|
- **Concurrency cap: 5 in-flight runs per member.** Exceeding it rejects with "Too many agent runs in progress".
|
|
228
|
+
- **One tool result may not exceed 512 KiB.** A tool's result is persisted into the run's transcript and replayed on every later step, so an oversized one is paid again per step until the run stops fitting in a request. A tool that overruns fails by name ("Tool \"x\" returned N bytes…") rather than being delivered; narrow the call, or use a tool that returns a reference instead of the bytes. Reaching this from a normal call means too much was asked for at once — read a range, not a whole sheet.
|
|
226
229
|
|
|
227
230
|
**Limitation (dev harness):** `lotics app dev` streams runs but does not forward the run id, so in the dev loop `cancel()` degrades to a local `abort()` (the run keeps executing server-side) and connection-drop recovery is unavailable. Both work fully in the embedded product.
|
|
228
231
|
|
package/docs/mutations.md
CHANGED
|
@@ -539,3 +539,40 @@ This is the full read → mutate → reconcile loop: `useQuery` reads, `row.*` c
|
|
|
539
539
|
one workflow per (table, field) you mutate — typed and narrow, never a generic
|
|
540
540
|
`setField(any_field)` that hands the client write access to every field
|
|
541
541
|
([security](./security.md)).
|
|
542
|
+
|
|
543
|
+
## Editing a record that does not exist yet: `useNewRecord`
|
|
544
|
+
|
|
545
|
+
A "new record" surface — a drawer, a detail page reached from a **New** button — has a problem
|
|
546
|
+
before its first write: it has no id. If the server mints one, then everything keyed on that id
|
|
547
|
+
(the route, the drawer, a list selection) *changes identity* the moment the first field saves,
|
|
548
|
+
and React answers an identity change by remounting the surface the user is typing into.
|
|
549
|
+
|
|
550
|
+
Mint the id on the client instead. `create_records` accepts caller-supplied `rec_*` ids, so the
|
|
551
|
+
id is known from the first render and the create stops being an event the UI has to survive:
|
|
552
|
+
|
|
553
|
+
```tsx
|
|
554
|
+
const { id, save } = useNewRecord({
|
|
555
|
+
create: (id, patch) => createCustomer({ record_id: id, ...patch }),
|
|
556
|
+
update: (id, patch) => updateCustomer({ record_id: id, ...patch }),
|
|
557
|
+
onCreated: list.refetch,
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
<InlineText onBlur={(name) => save({ name })} />
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
- **The record is still created on the first write, not on mount** — a surface the user opens
|
|
564
|
+
and abandons leaves nothing behind.
|
|
565
|
+
- **`save` serialises.** The first call creates, every later one updates, and calls made before
|
|
566
|
+
the create resolves queue behind it. Two fields blurred in quick succession therefore produce
|
|
567
|
+
one record, not two. This is the whole reason to use the hook rather than a `useRef` latch:
|
|
568
|
+
hand-rolled, the racing-blur case creates a duplicate.
|
|
569
|
+
- **A failed create does not latch.** The next `save` retries the create, rather than updating a
|
|
570
|
+
row that was never written while the UI looks like it saved.
|
|
571
|
+
- `id` and `save` are stable across renders, so `save` can be bound directly to an `onBlur`.
|
|
572
|
+
|
|
573
|
+
Your `create` workflow must pass the id through to `create_records` as `ids: [record_id]`.
|
|
574
|
+
Creation is creation: an id that already exists is a conflict, never an overwrite — so the
|
|
575
|
+
workflow cannot be used to clobber another record by guessing its id.
|
|
576
|
+
|
|
577
|
+
Use `newRecordId()` directly if you need the id outside a hook (routing to the surface before
|
|
578
|
+
mounting it, say). It mints the same `rec_*` shape the server validates.
|
package/docs/queries.md
CHANGED
|
@@ -853,6 +853,22 @@ per-table queries you merge client-side; the merge already happens in the index.
|
|
|
853
853
|
sort key to be a bare field projection — a computed/literal/type-overridden sort column falls back
|
|
854
854
|
to the whole-union sort.)
|
|
855
855
|
|
|
856
|
+
**An aggregate arm you cannot filter costs its whole table, every execution.** The shape to watch
|
|
857
|
+
is the mirror of the one above: a `join` whose right side is a `group` over one or more entire
|
|
858
|
+
tables — a lookup built by scanning everything in order to decorate a small left side. Rule 1 is
|
|
859
|
+
no help here, and that is what makes it easy to ship: the arm keys on a value the LEFT side
|
|
860
|
+
supplies at run time (a code, a serial number), so there is no static predicate to push down. The
|
|
861
|
+
left side's size is irrelevant; you pay for the arms. As a unit to budget with: a four-table arm
|
|
862
|
+
totalling ~14k rows is **seconds, not milliseconds**, on every single execution. Prefer a
|
|
863
|
+
materialized **lookup/rollup field** on the row (the platform keeps it current and it filters at
|
|
864
|
+
the source), or drop the arm entirely if the value it fetches now lives on the row already.
|
|
865
|
+
|
|
866
|
+
**Measure before you change a shape, and after.** Latency is observable per alias: each `useQuery`
|
|
867
|
+
goes out as its own RPC, so a screen's cold load attributes a duration to every query by name in
|
|
868
|
+
the browser's network panel. Time it, fix the one that dominates, time it again. Reasoning
|
|
869
|
+
alone mis-ranks these — the union above looks expensive and is fast, the join here looks ordinary
|
|
870
|
+
and is not — and a playbook rule applied to the wrong query costs effort while proving nothing.
|
|
871
|
+
|
|
856
872
|
### The authoring rules
|
|
857
873
|
|
|
858
874
|
1. **Filter at the source.** Push every static predicate into `from_table.filter`.
|
|
@@ -875,6 +891,12 @@ to the whole-union sort.)
|
|
|
875
891
|
at once (`by: [a, b, c, …]`) and fold each facet client-side by summing over the others:
|
|
876
892
|
`count` and `sum` fold exactly. `unique` does NOT fold across groups (the same value can
|
|
877
893
|
appear in many groups) — keep a separate query for each distinct-count you render.
|
|
894
|
+
11. **Re-derive a query when its tables change shape.** A `join` or `union` that exists to bridge
|
|
895
|
+
two tables becomes pure cost the moment those tables become one — and nothing fails, because
|
|
896
|
+
it keeps returning the right answer at the old price. Migrations that merge, move or back-fill
|
|
897
|
+
a table are exactly when this happens, and exactly when nobody re-reads the queries. After
|
|
898
|
+
one, open every query over the affected tables and ask what it would look like written today,
|
|
899
|
+
not what it needs to keep working.
|
|
878
900
|
|
|
879
901
|
---
|
|
880
902
|
|