Skip to main content

Sources

Docs: Panes, Preferences, Policies, and Execution context. Web App: Select a Pane, open Plots, and choose a named side tab. Results, History and Details appear below its SQL, and Read by lists the Pieces that read the Plot. Under the Pane’s own Plots, Its Panes lists the Plots of the Panes it declares, to read; a Page’s Plots tab lists those alone. React source is in Programming. Adding a Plot to a Pane opens a dialog that asks for its name and one sentence on what it computes. The name seeds the Plot’s handle and its catalog address; the address stays fixed after the Plot is created, even when it is renamed. Every Plot needs a description, shown in the band under its header. While you edit SQL, an advisory check runs after a short pause. Warnings & Errors are underlined in the source, marked in the gutter, and listed in the footer under the SQL, with the other checks. Hovering one shows its message and code; one with a suggested replacement shows it as a button. The footer also lists findings without a source location, such as a Plot that blocks publication. Choosing a finding with a location moves the editor to it. A Plot’s recorded warnings and errors hold a Publish, as every Piece’s do. The panel distinguishes Advisory SQL from Server validation. A pending or unavailable check is not an all-clear. Missing catalog access or an unavailable advisory check leaves the source editable and keeps the existing save controls available. Server checks and Policies still apply when you save. Saving a Plot’s SQL records server validation diagnostics. The server also plans the SQL the way a read does, without running it: SQL the server cannot plan records PV_STMT_004 with the reason, so a Plot that would fail on every read says so when you save. An aggregate in ORDER BY reads table columns, not output aliases: ORDER BY MIN(priority) where priority is only an alias records PV_REF_003 or PV_STMT_004. The save plans SQL that takes parameters with the Plot’s saved parameter values, SQL that reads another Plot against that Plot’s last output schema, and SQL that reads a Pane by name (parables.<parable>.<pane>, <pane>_by_actor or <pane>_data) against the Pane’s type, as Prepare does. So this holds in Personal mode, which has no Prepare, and after an archive import. A column name that the SQL can resolve two ways, such as ORDER BY c.actor_id beside an output column actor_id, records PV_STMT_004: order by the output column’s name. Saving new parameter values alone checks the SQL again. The save also runs the planning pass that Prepare runs for the Plot’s path. When that pass refuses the Plot, the save records PV_STMT_004 with the words Prepare will give, so the Patch shows the problem before Prepare. For example, an output column of a type that a Plot cannot hold, such as an INTERVAL, records a finding that names the column and its type, and says to cast its expression in the SELECT to a type such as VARCHAR or BIGINT. A refusal at Prepare keeps the planner’s own reason after “This SQL could not be planned:”. A parameter that has no saved value stays unbound, as it does at Prepare: the save runs Prepare’s planning pass and skips only the execution plan, which needs a value. It records no finding for a read of a Plot that has no output schema yet. When the SQL reads another Plot, a column the server cannot find there is not a finding: that Plot’s last output schema can trail its SQL. With Published Versions, Prepare and Publish also plan every Plot with its parameter defaults and the other Plots bound, and SQL the server cannot plan is a Prepare finding that blocks publication. An unavailable validator does not prevent saving: the draft and its last successful schema stay available. A save also checks how deep a Pipeline’s plan nests, as Prepare does (PARABLE-8430). query-runner runs a Pipeline from encoded plan stages, and protobuf decodes at most 100 nested levels. Each plan level costs two levels. A table scan adds three or more for its table’s schema, and window frames and CASE add their own. A view is never encoded, so the check does not apply to it.
  • PV_STMT_005: the plan is too deep to decode. This error blocks publication, and Prepare refuses the same plan with plot_plan_failed.
  • PV_STMT_006: the plan nests 80 levels or more. This advisory names the depth and the limit. It does not block publication.
To make a plan shallower, join fewer tables in one statement, or move part of the work into another Pipeline. A Plot counts a Pipeline that it reads as one table. The save also counts a view that the SQL reads as one table. Prepare and the Report run plan the view’s SQL inside the Plot. So a Pipeline that reads views can pass the save and still fail at Prepare. A Workspace copy can pull a fix before its first successful Report. Prepare plans the selected SQL and its dependencies without requiring saved Report tables. Differences only in a Plot’s derived output schema or its derivation receipt do not create a pull conflict. For a conflict between authored changes, select the version to keep, then run Prepare again to check that definition. Publishing carries the SQL’s recorded check into the Release. If Warnings & Errors finds no recorded check for a Plot’s SQL, it says the check is unavailable and asks for a re-check. It does not remain pending when nothing is running. A Plot that reads a sibling Plot is checked again once that sibling’s columns change in the same save, so a save that adds a chain of Plots (C reading B reading A) checks every link. A save shares one ten-second budget across all its Plot checks. When time runs out, the remaining checks are skipped and those Plots record PV_SYS_001 (unchecked). Your SQL draft still saves, but unchecked Plots cannot be published. Saving an unchecked Plot again checks it again, even when its SQL did not change, so an agent that cannot reach the re-check below recovers by sending the Plot once more. A save that does not send an unchecked Plot leaves it unchecked. An explicit re-check of the Parable or of a Patch checks again each Plot that is unchecked or has a stale check, with the same shared budget. It also checks again each Plot whose check waited on a Plot its SQL reads (PV_REF_007), since that Plot may have a schema now. It skips a Plot whose check is current and complete, including one with an SQL error that needs an author edit. Re-checking does not change your authored content, but it writes check results onto the Plots it checks, so a Patch re-check runs only on a Pending Patch. A Proposed or Published Patch cannot change, so its re-check is refused with PA-AU-004. Prepare checks a Proposed Patch’s Plots without changing them. A re-check needs a participation on the Parable, as a read does. A person with none gets 404 PA-AU-001, the answer a read gives them. A Patch re-check gives the same answer to a person with no participation who did not start the Patch. An upload (Upload Parable) runs the same checks before it answers. Uploads and explicit re-checks also plan Plots with saved parameters and retry Plots that were left unchecked or waiting on another Plot’s columns. A persistent SQL error or an unavailable planner still needs an author edit or a later re-check. An upload does not take Plot schemas from the archive. The web app allows up to five minutes for an upload or re-check and names the command if its request times out. An upload that was already saved keeps its content even if its later checks could not finish. An uploaded Pipeline may have checked columns before it has a planned or stored output. Planning a Plot that reads it can use those checked columns, with each column treated as nullable because the check does not prove non-null values. Reading that Pipeline still requires its planned output and data. The save’s check derives the schema of SQL without parameter defaults. A save of SQL with explicit defaults, including [], plans it right after the save with those defaults, as publication does, and records that schema, so Plots that read it in the same draft use it at once. Planning runs no SQL, so a Plot that calls a model costs no call. A Plot the planner refuses keeps its last successful schema. Publishing checks the definition, its parameters, its findings and the plan of each Plot before it publishes the Release. A Plot whose SQL takes $ inputs needs no saved defaults to publish: publishing plans it with the inputs unbound, as long as no output column takes its type from one (cast the input when one does). A Report member still needs its values, because its run binds what the definition saved. When publication is blocked, the current Prepare findings identify the Plot and retain the server’s reason, including after reloading the Patch. A Publish refused by the gate names the same findings in its own message. Publishing also judges what each Plot’s SQL reads, for the person who publishes (PARABLE-9299). A Refresh runs a published Plot’s SQL with the Parable’s authority, so the rows of every Plot that SQL reads go into the new Plot’s stored rows. A read of those rows still refuses a reader whom a Plot behind them keeps out, but the rows are copied all the same, and the new Plot’s output schema shows that Plot’s columns. So a Publish refuses with 403 when it makes a Plot read a Plot of this Parable that the person who publishes may not read, by the view.preferences Policy of that Plot’s Pane. The refusal names the person’s own Plot, never the Plot they may not read. The Publish takes the reads from each Plot’s plan, so a join, a subquery and a GRAPH_TABLE pattern count alike. A Plot that reads another Plot also reads what that Plot reads, a view and a Pipeline alike. It judges every read of a Plot that the Changes add, move to another Pane, or whose SQL they change. It judges every read of a view that the Changes make a Pipeline, because that Plot then stores what it reads. It also judges each read that another Plot gains from them. A read that the published Version already holds stays, so a person with Publishing can publish other Changes beside a Plot that the Primary published. The save’s check still plans against every Plot, so its stored result is the same for everyone with Primary or Publishing on the Changes. A Delivery that Parable applies by itself publishes SQL that a person already published, so it is not judged again. A person who publishes an update of a Workspace copy is judged. A Plot returns exactly the columns its Pane’s Perception declares for it. A column the declaration does not have, a declared column the SQL leaves out, or a column of another type (text where a number is declared, say) is an error under Warnings & Errors (PA-AU-047), judged from the schema recorded for the saved SQL. A list or object column that the save’s check cannot type, such as a list of structs, is judged once planning has typed the SQL. Prepare and Publish refuse such a Plot, naming each column, because every read of it would be refused. End a Plot’s SQL in an explicit column list, never SELECT *. The declared columns, their types and the name of the Pane’s kind are part of the Pane’s Perception. Under Warnings & Errors, a reader who can read the Plot’s SQL (view.plots) but not the Pane’s Perception (view.perception) gets the same error with no column, type or kind in it: “Plot regionChart does not return the rows its Pane declares. Every read of it is refused until its columns match.” A value that breaks the declaration when the Plot is read refuses that read with PA-AU-047. Examples are a null where the Perception requires a value, a number past the declared maximum, and text its scalar does not accept. The refusal names the Plot, the column that holds the value and what is wrong with it, never the value. The Pane shows it as the read’s error. A Plot whose SQL reads such a Plot gets the same refusal, naming the Plot whose rows broke it. Warnings & Errors, Prepare and Publish check the columns and their types, not the values. A Plot that returns such a value publishes, and its first read is refused. An outer join returns a null for a row with no match. Return a value with COALESCE, or declare the field T | null. A NaN or an infinity, such as a floating-point number divided by zero, reads as a null too. A Plot needs a contract: its Pane’s Perception declares it under plots, or its Pane’s type is an object kind, so its rows come from that type. A Plot with neither has no contract, and every read of it is refused (“Plot requires an authored Perception computation contract”). Prepare and Publish refuse it, naming the Plot and its Pane. It never stops another Plot from reading: SQL that names it is refused as not ready, and every other read goes on. A Pane’s type is the kind of the rows its Plot returns: a Plot it owns that its Perception does not declare returns rows of that type: any of the type’s fields, as columns of the same names and types. Its rows are the type picked to the columns its saved SQL returns, and it takes no input. A column the type does not have, or one of another type, is PA-AU-047, named against the type (“it returns quarter, which RegionSales does not have”). Add the field to the type or leave the column out. Until the save records the SQL’s columns, the Plot’s rows are the whole type. Return a Temporal.Date field as a DATE column and a Temporal.DateTime field as a TIMESTAMP, with a CAST where the source holds the day or time as text (Perceptions). A Lab Plot is the exception (PARABLE-7230). A Lab declares its SQL as Plots in parable.lab.json, and the import keeps them on the Lab’s data Pane (Lab Parables). A Parable has a Perception, but the Plots in a Lab have none, as they can query each other. A Lab Plot’s contract comes from the output schema that its SQL check recorded, the same schema that types the Plots that read it: no input, and rows of exactly those columns, each of which may be null. Each column takes the plain scalar of its kind: Generic.Text, Generic.Float64 or Generic.Boolean, with Generic.JSON for JSON, and lists and objects of them. A column of another kind, such as a map, leaves the Plot with no contract, as does SQL with no recorded schema. Such a Plot is refused as not ready, and Prepare and Publish name it with Lab Plot <slug> has no checked output for its current SQL. When the check did not finish (PV_SYS_001), as when a Lab is imported before the Query Layer is ready, an editor’s next read of the draft, through a Plot read or SQL in the Parable, checks its Plots again and reads once more. A Release needs no re-check: Publish plans every Plot. The data Pane owns every Lab Plot and shows no kind, so the rule that each Plot of a Pane has a typed contract (PA-AU-049) does not apply to it. Only a Lab package import changes a Lab Plot. A save from the Plot editor is refused. Structured Parables keep the Perception contracts above. To clear a Plot finding, return the Patch to Pending with Patch Proposal, fix the SQL, propose it again, and choose Prepare Patches in the Publishing options. Prepare runs every check on the current Patch, the Plot checks included. A finding the fix resolved stops blocking, and Publish Patches becomes available. Saved findings belong to the exact SQL and selected Workspace and definition. They replace advisory feedback for that saved text and clear when a completed server check clears them. Changing a Plot’s SQL starts a new check. The catalog follows the selected Workspace and draft. Earlier Releases remain read-only, and their advisory check does not use the current catalog. A Plot’s advisory check can also use the columns of other Plots’ unsaved SQL. Typing a Plot back to its saved SQL withdraws its unsaved draft, including when an earlier save is still pending. Other Plots then validate against the saved schema again.

Structure

A Plot is a typed computation, and it always lives on a Pane. The Pane’s Perception declares PlotContract<Input, Output> under a local Plot name, or the Plot returns rows of the Pane’s type under its handle. SQL is the implementation language; React rendering belongs to the Pane. The SQL is the Plot’s implementation: it is saved with the Parable’s draft and versioned with it. A Plot has no Policies of its own; its Pane’s Policies govern it.
The Pane’s type is its kind, the rows of the one Plot its Perception does not declare, in scalar-lib types. A Pane may own several Plots: at most one is undeclared, and then the Pane’s type is not null; each other Plot is declared. A save that adds a second undeclared Plot, or an undeclared Plot on a Pane whose type is null, is refused with PA-AU-049. The name matches the Plot’s handle when first saved, and the Plot must belong to the Pane. Until the Perception declares it, the Pane’s code has no plots.activityFacts and Preview reports that the Plot’s contract is unavailable. Perceptions lists the type to declare for each kind of SQL column.

Optional Artifact inputs

A Plot can run in a Workspace that has not uploaded an Artifact. Declare the Artifact’s canonical name and row type as the third PlotContract argument:
The SQL remains SELECT email, department FROM artifacts.census. When the Artifact is absent, the Plot reads an empty table with the declared columns. A present Artifact keeps its real rows. Missing columns, incompatible column types, an unavailable publication, and denied access fail the read. A missing or incompatible column fails with QL-TB-005, and an unavailable publication fails with QL-TB-006. Optional inputs do not bypass those errors. Use T | null for nullable columns, and declare every column as a required property. The Plot save checks each declared row. A row type that is not an object of columns fails the check with PV_STMT_004, and the message names the declaration. Each Plot owns its declaration. A nested Plot uses its own declaration, and one Plot cannot authorize another Plot’s missing input. An unused declaration produces no warning. The declaration does not add the empty table to the Workspace’s catalog listings. The Pane reads plots.census().meta?.absentOptionalArtifacts. A nonempty list names known absent inputs, including inputs of nested Plots or stored Reports. It makes no claim about unlisted inputs. An empty list means the producer recorded no absent optional inputs. A missing or null value means unknown, as with older stored outputs. Check a canonical name in the list before showing an upload prompt. Do not treat unknown provenance as a present upload. A stored Report retains the input state from its production run. Uploading or deleting an Artifact later does not change that Report’s warning. Existing two-argument contracts keep their current behavior. A Parable’s own data file follows the same declaration. Its table is at artifacts.parable_<parable>.<table> (Artifact Pool). Files do not travel with a Release or a Deploy. A copy of the Parable in another Workspace has no file until someone adds one there. Declare every Parable table that a Plot reads. This Perception declares the table of actors.csv, artifacts.parable_perceptions.actors:
The declared names are the file’s column names, and the SQL quotes them:
In a Workspace where the Parable has no actors file, the Plot reads an empty table with the declared columns, and absentOptionalArtifacts names artifacts.parable_perceptions.actors. The Pane can then offer an upload with files.upload (Panes). Other Pieces read a Plot by address. A Pane that declares the Plot’s Pane in its Perception reaches the Plot in code as panes['<pane>'].plots['<plot>']() or panes['<pane>'].plots.run('<plot>'). A Page holds no data, so a Page places the Pane that owns the Plot, and that Pane renders the rows. Page code that reads a Plot gets the note page-plot-read. Read by on the Plots tab lists each Page and Pane whose code names the Plot this way. Deleting that Plot, or its Pane, is refused, and the refusal names the readers. The Plot’s own Pane is a reader too while its Perception still declares the Plot in plots. To delete the Plot, remove that declaration first or in the same save. The Pane’s own declaration does not refuse a save that deletes the Pane with its Plots. Deleting a Pane that another Perception declares is refused the same way. A Plot’s SQL reads a Pane’s saved rows by name, as typed rows of the Pane’s type (Preferences): A field keeps its scalar, and its column carries it: a Generic.Boolean field is a boolean column, Finance.Money an integer, Temporal.Date a date, and Contact.Email text. A field that may be unset keeps its scalar too: a negated?: Generic.Boolean | null field is a boolean column. The value reads in the scalar’s canonical form, as a stored column of that scalar holds it. So a field compares as its scalar does (semantic types): a Generic.Text field compared with a Contact.Email column is refused with QL-PL-007 unless one side is cast. A Ref<T> field is its item’s id, so it joins on the other Pane’s id. <pane> is the snake_case of the Pane’s handle, the rule a new Plot’s slug follows: the Pane timeSpendScope is time_spend_scope, and readerAIUsage is reader_ai_usage. Handles are unique ignoring case, so no two Panes share a name. A Plot whose slug is a Pane’s name keeps it, and that Pane cannot be read by name until one of them is renamed. A field that no layer sets is null, and the Plot supplies its default with COALESCE. The value the Pane’s code holds on the reader’s screen (preferences.current(value)) comes before the saved rows. A filter on id reads only that item. The SQL editor’s catalog lists each Pane it can read this way. A Pane is never a Plot dependency: a change to its rows reads again only the Plots that read it by name, and the Plots that read those. A Report run keeps the moment it started, and its Plots read the rows as they were then; reading a chosen Refresh reads the rows as of that Refresh. A Plot never reads workspace.preferences (typed-read). Each column has the type of its field. A record, such as an interface the Perception declares, is a struct column. A list of records is a list of structs, also when the list may be null. SQL reads them as it reads other structs and lists: unnest(concepts) gives one row for each element, cardinality(concepts) counts the elements, and concept['name'] reads one field of a record. A record’s fields have their own types, and each field can be null. A value that a struct cannot hold is its JSON text: a map, a tuple, a mix of types, a list of lists, or a record that holds itself, directly or through other records. The JSON functions read that text, as in json_get_array(column). unnest or cardinality given text, an aggregate such as bool_or given a type it does not take, or a JSON function given a list or a record, is refused with QL-PL-019, and the refusal names the function. Panes that read one Plot with the same parameters, the same Pane values, the same reading and the same View as while a read of it is still running share that read: the browser sends one request and each Pane gets the same rows.

Collect nested values before sorting

When a Plot collects lists or records, keep ORDER BY outside array_agg. The current engine can reserve excessive memory when it merges an ordered aggregate of nested values. Small groups can trigger this too. A larger memory or spill budget does not correct the estimate. Choose the order that the result needs. For Provider references whose order does not carry meaning, collect the lists, flatten them, then sort the values:
Reference Values
This returns references in value order. It preserves duplicate references. Use array_distinct before array_sort only when the result is a set. flatten removes the list boundaries, including empty or null input lists. Null elements inside a list remain and sort last. This changes the order from an aggregate that used Provider priority or a record key. When a separate priority determines the order, number the source records first. Collect a record containing that number and its original list, then sort the collected records:
Reference Records By Priority
Use a unique, non-null row_key within each group to break ties. Put position first in the record because array_sort compares fields in their declared order. This preserves each record’s list, including null lists, empty lists, null elements, and duplicates. It returns a list of records with position and provider_refs, so declare that shape in the Perception. A Pane that needs only the references can read those records in order and flatten their lists. Do not substitute this result for a bare list without updating its consumers. Build a group’s list once with GROUP BY. A window that copies the full list onto every source record can consume memory in proportion to the square of the group size. The example’s window carries only a row number. An ORDER BY in a subquery does not guarantee the order of an unordered aggregate after parallel execution. Keep the final array_sort. These patterns still retain the result of each group. Split or limit an inherently large group instead of assuming that any list fits in memory.

Plot results in the browser

Once a read answers, the browser keeps its result in two places: in the tab’s memory, and in a store on the reader’s device (IndexedDB) for 14 days after its last use. The next read of that Plot with the same parameters, the same values of the Panes the Plot reads, the same reading, the same Release or draft and the same View as answers from one of them at once. This holds after a reload, in a new tab, and on a return to a Parable. Parable authors build none of this: every Pane’s Plot hook reads this way. The store is the first copy of Provider data that Parable keeps at rest in a browser. It follows these rules:
  • One reader, one access state. Each time a Page resolves its Panes, the server returns a key that it derives from the reader’s whole access state on the Parable: the reader, their seat, the Policies of the Parable’s current Release, the switch rows of its filter Policies, the reader’s Workspace Roles and their permissions, and the Workspace’s Provider Policies and source permissions switches. The browser encrypts each stored result under this key and holds the key in page memory only. When any of these inputs changes, the key changes, and a result stored under the old key can neither answer nor be decrypted.
  • What is never stored. A View as preview, an author’s SQL preview, a read that failed or was refused, and a read through a Platform Parable stay in tab memory only. A read through a Platform Parable answers there only by the catch-up rule below: the key covers this Parable’s access, not the access to the Parables it reads. A reader with All data access, a staff member, an impersonation, a Workspace that enforces source permissions, and a Parable whose filter Policy reads data through in_plot get no key, so nothing of theirs is stored.
  • A chosen Refresh. Its rows never change, so its result never reads again. Each Refresh in the menu keeps its own results: a return to yesterday’s Refresh after today’s arrived is instant. When a Refresh leaves the menu, its results go.
  • Latest, and a Plot that reads a stored output. A Pipeline, or a Plot that reads one, answers from the store until Latest moves to a newer Refresh. Then its results go and it reads from the server.
  • Latest, and a live Plot. It answers at once. When its rows are older than a minute, one read goes to the server in the background, after the Page is drawn. When that read finds other rows, the Pane takes them in place: no reload, no remount, and the chart’s marks move to their new values. The same rows in another order are no change.
  • A slow store. On Latest, a read waits for the store while the store looks for the result. When the store has not found it after 60 ms, as on a page that is still starting, the read also goes to the server, and the first answer shows. When the store found the result, the read waits for the store and sends nothing to the server. A chosen Refresh never reads from the server beside the store.
  • A Page that opens again. While the Page resolves its Panes, the browser reads the results that the Page used the last time. When the key arrives, it decrypts them at once, so the Page’s first reads find them ready.
  • Sign-out. A logout deletes the store on that Workspace’s host: the logout page opens on that host and deletes the store before it goes to the sign-in page. A session that only expired keeps the store locked: no key, so nothing answers, until the same person signs in again.
  • Another person in the same browser. When a page of another person opens on the host, on any route, every open tab of the host forgets the keys and closes the store. The store empties itself before its next first read.
  • A tab that stays open. The server checks the reader’s access state again when the tab becomes visible, and in the background on a read when the last check is a minute old. When the last check is five minutes old, the read waits for a new check before it answers. A background read that the server refuses as signed out (401) locks the store in every tab and clears the Pages. A background read that the server refuses for access (403) deletes that result, and its Pane reads again. A Pane resolution that the server refuses deletes every result kept under that access state and clears the Page.
Results are dropped when they could be out of date:
  • When Latest moves to a newer Refresh, every result that read a stored output on Latest is dropped, whether the move shows while the Pages read Latest, while they read a chosen Refresh, or on a return to a Parable that was closed when it moved. A live Plot’s result stays and reads again in the background.
  • When anyone saves or deletes a row of a Pane, the reader included, in another tab, another session or through Ponder, the open Pages update that Pane’s rows in place, and only the results of the Plots that read the Pane by name, or read such a Plot, are dropped and read again, in tab memory and in the store. A chosen Refresh’s results of live Plots stay: they read rows as of their run. Where the platform cannot tell which Panes a Plot reads, every result is dropped.
  • When a seat on the Parable, a row that a Policy reads, a Provider Policy or a source permissions switch changes, the reader’s access is decided again: the tab drops what it kept in memory, and every open Page resolves its Panes again before it reads. A narrowed Policy returns fewer rows rather than refusing the read. The stored results are not deleted: the change gives a new key, so they never answer again.
  • When a Plot read is refused, the Page resolves its Panes again. The Panes read again with no rows only when the reader’s access changed. Any other refusal drops every result the tab kept in memory.
  • A tab hears row, seat, Provider Policy and source permissions changes for a Parable only while that Parable is open in it. A result with a key answers at once on a return, because the Page’s Pane resolution confirmed the reader’s access state again. A result with no key answers only once the tab has heard every change it missed meanwhile. A Pane that asks before then waits for it, up to two seconds, and then reads from the server. When too much changed to catch up on, every result kept in that Workspace is dropped, and the stored Latest results of that Workspace too.
A new Release, a saved draft or another View as is another reading, so its first reads go to the server. The memory is bounded by size (256 MiB by an estimate), and the least recently used result goes first, so a few permutations of a 10 MB result stay in memory. The store sets no size limit of its own: when the browser refuses a write, the least recently used results go. A result larger than everything the store holds, or than the browser’s quota leaves, stays in tab memory only, and nothing goes for it. A large result is written in small steps after the Page is drawn, so the write never blocks the reader. A result read from memory answers in less than a millisecond. A result read from the store answers in a few milliseconds up to about 1 MB. Above about 3 MB, a read from the store takes more than 10 ms: about 20 to 30 ms at 10 MB. The store asks the browser to keep its data, except in Firefox, which would ask the reader. A private window keeps its store only while the window is open. Safari deletes the data of a site that a person has not used for seven days of Safari use, so a reader who returns after that reads from the server. A Pane that needs newer rows than a kept result calls its hook’s breakCache(): that read goes to the server and its answer replaces the result in memory and in the store. rerun() runs the hook again and is answered from a kept result when there is one. Read by does not list another Plot whose SQL reads this Plot by its catalog address, and deleting a Plot that only SQL reads is not refused.

Plots read as views

A Plot nothing stores is read as a view of its SQL. A stored Plot is a Pipeline (its Pipeline switch, requiresReportingRun, is on). Report plans are the only schedule, so a Plot runs on a schedule only as part of its Report. Every other Plot whose SQL binds no parameters is a view. SQL that names parables.{parable}.{plot} for a view, in another Plot, a Pane’s query or a Report run, runs the view’s SQL as part of its own, so small Plots compose the way SQL views do: a Plot can union one mapping Plot per Provider, each of which can read further views. Views compose; they do not make reads fast. A view’s SQL runs inside every read that names it, once for each place it is named, so a Plot that unions many mapping Plots is computed again for every Pane that reads it. Parables are product experiences, and a Page must open fast: make any expensive query a Pipeline, judged by what it scans, joins, aggregates or calls, and let other Plots and Panes read its stored output. Work down from what each Page shows first. Readers see how old stored data is and can refresh it; see Reports. To measure instead of judging, record in the editor drawer’s Performance tab and open the Page a few times: each Plot read shows its time, rows and size, whether it was live or stored, and where its server time went. A live read that holds the Page back on every open is a Pipeline, and the tab’s hints say so. Ponder reads the same recording while you record.
  • Columns. A view serves its output schema, the columns a stored table of the Plot would serve: the schema publication planned for its SQL, or, for SQL a draft added or changed, the schema the draft’s last save derived, with every column nullable. If its SQL no longer returns those columns with those types, the read refuses until the Plot is planned again, or for a draft, until a save derives its schema again. A value of the declared type in another storage is served as declared: text that SQL functions return as Utf8View or LargeUtf8 (CAST(… AS VARCHAR), string_agg) reads as Utf8, and a timestamp in another unit in the declared unit. A finer timestamp, such as the nanoseconds CAST(x AS TIMESTAMP) returns, is rounded down, so an instant never moves into the next second or day; inside a list or struct it refuses the read.
  • Definition. A Pane read and a Plot preview read each view from the definition they show: a draft reads the draft’s SQL, so an author who previews, in a Patch, a Plot that unions mapping Plots sees their draft mapping Plots, including Plots only the draft has, and a Release reads that Release’s SQL. SQL outside a Parable reads the Release the Workspace serves.
  • Views of views. A view can read another view. Views that read each other are refused: reading one answers Plot views form a cycle: … reads …. Publication refuses them with a 409 that names the cycle, such as Plot a reads Plot b reads Plot a. Change one Plot so these Plots do not read each other. Publish and automatic Delivery find the cycle after planning fails: they resolve the current SQL and name only the Plots in a closed cycle. A Plot that only reads a cycle is not named. Old saved schemas, reference pins, SQL comments, and string values cannot prove a cycle. Successful publication adds no validation request for this diagnosis.
  • Report runs. A Pipeline may read views of its own Parable. The run computes each view inside its own plan, so the run’s pinned inputs are the Provider tables and stored Plots under the views. A view of the run’s Parable must come from the Release the run captured; a run captured before a later publication of the view is refused, and running the Report again uses the new Release. A run whose view the Workspace catalog serves from an earlier Release is refused with that cause instead: the catalog has not caught up with the captured Release, or that Release was retracted. A run a few minutes later reads the Release the Workspace serves.
The input describes positional parameters and the output describes rows. A fixed tuple has exact arity. A dynamic query builder can declare an array of supported scalar types. Both paths validate parameter values and the SQL’s placeholder count. The contract exists even when no SQL has been saved. React imports other application components through declared Pane references. It accesses owned logic through its Pane SDK. For a Plot declared with no parameters:
  • plots.activityFacts() is a hook exposing status, rows, refreshing, rerun and breakCache. Pane code draws three states: reading (status is running with no rows yet), empty (ready with no rows) and rows. A read that fails never reaches it: the host shows the failure in the Pane’s place, with Retry (below).
  • plotApi.run('activityFacts') runs from an action handler.
  • result.meta identifies the data that answered the request, including a resolved Report when relevant.
A Pane can also call the Plots of a Pane it declares, through that Pane’s component (PaneComponent.plots), without rendering it. status is running while a read with no rows on screen is in flight, the first one included, then ready. rows is empty until the first answer arrives, so empty rows mean no data only when status is ready. Render the states in this order, with no error branch:
A saved row, a seat change, or a change to a row that a Policy reads in another session reaches open Pages through its live connection. A row change updates the Pane’s rows in place, and only the Plots that read the Pane run again; an access change resolves the Panes again under the current access. After a save or publish, Pages that show the draft read its new revision. Pages that show an unchanged Release do not read it again. Ponder messages and run activity do not drop kept Plot results. A hook already showing an access refusal keeps that error until the context or the access changes, or the Pane calls rerun. Returning to the window does not ask again. A refusal asks every other Pane that shows rows to read again under the current access; a Pane still reading keeps its read, and refusals of reads that began before that refresh ask for no other. A refused Plot read of a Page whose Pane resolution returned a store key resolves the Page’s Panes again instead, and the other Panes keep their rows unless the reader’s access changed. A Page whose Plots the reader’s Policies refuse sends one read per Plot. A failed read’s error has a message and a code. The code is the server’s error code when it supplies one, such as QL-TO-002; a bridge code such as TIMEOUT when the host does not answer; or PLOT_FAILED when the failure has no code of its own. A server error that names no cause carries only its service’s general code, such as web-api’s WA-SY-001, and a Pane gets PLOT_FAILED for it too. The host’s error for a hook shows the message, and run, preview and readReport reject with the error, for the action that called them to handle. A read a filter Policy refuses has the code QL-PL-015, and error.details.filter names the filter: the Plot does not output a column the filter reads, so none of its rows can be shown to this reader. That is not a failure to retry. For a hook the host shows it as hidden (below); an action that gets it from run shows it the same way, such as “Hidden by” and the filter’s name. The hook reads again when a Preference changes, when the reader, the Workspace, the definition, or the data reading changes, when the window regains focus, and on rerun() and breakCache(). On the window regaining focus only a hook that shows an answer reads again; one that failed, waits for a Refresh or is still reading is not asked again. A kept result answers it when there is one, except for breakCache(). When it reads again, the same Plot or the same Plot with new parameters, it keeps the rows it shows on screen until the new ones arrive: status stays ready and refreshing is true meanwhile, so a Pane never blanks while it reads again, and its chart can move from the old values to the new. A Pane can use refreshing to show that newer rows are coming. The platform dims the Pane after 150 milliseconds while the rows on screen are no longer what the reader asked for: rows of another parameter set, or rows read before a Pane value the Plot reads, a Preference, a row it bound or the context changed. A date range in a saved-data Pane is such a Pane value. The same inputs read again, as when the window regains focus, on rerun(), or when a background read finds newer rows, do not dim the Pane. An answer from the browser arrives before the dim. The dim draws each top box of the Pane at 55% opacity, also a box that sets its own opacity or holds one from an entrance animation. It fades in, but a box whose entrance animation holds its opacity dims with no fade. The dim ends at once when the new rows arrive. For a reader who asks for reduced motion it shows with no fade. A first read, a read after a failure, or a read after the reader’s access changed starts with no rows and status running, so no row read under the old access stays on screen. A Pane that shows its empty state without checking status reports no data during those reads. When a background read finds newer rows for the result a hook shows, the hook takes them in place: status stays ready, refreshing stays false, and the Pane’s component is not mounted again. The platform moves the SVG marks that stay on screen, and the widths and positions of HTML bars, from their old values to the new ones. New marks appear and removed marks go at once. A mark moves best when the Pane keys it by its category, not by its index. A Pane that draws on a canvas, or that sets data-plot-tween="off" on an element, draws its new rows at once, and so does every Pane for a reader who asks for reduced motion. The hook waits 70 seconds for a read. The application ends a Plot data request at 55 seconds and the read then fails with that error, so a slow read stays running until its rows or its failure arrive. A first read with no answer after 70 seconds fails with the code TIMEOUT. A read of the same parameters again with no answer keeps the rows it left on screen, still ready, and refreshing returns to false. A read of new parameters with no answer fails: another parameter set’s rows never stand as its answer. A Pane resolution waits up to 60 seconds. A save of Preference rows never publishes or proposes, so it answers like any other host call. Other host calls wait 10 seconds, except SQL that a Lab runs with query, which waits 70 seconds. A Plot hook retries a short server failure (HTTP 500, 502 or 503) twice: after one second, then after two seconds. It stays running during those pauses. When a read fails for good, the host replaces the Pane with an error that names the Plot, shows the failure’s message, and offers Retry. Retry reloads only that Pane and reads its Plots from the server, not from kept results; the other Panes keep their state. A change to a Pane the Plot reads, a new context such as another reading or View as, or a change to the Pane’s input also reads the failed Pane again. Returning to the window does not. Leaving a Pane cancels its scheduled retries and the reads it still has in flight, so the server stops them; leaving a Parable cancels every read of its Pages. Refusals, invalid requests and expired time limits are not retried: the Pane shows their error at once. A read the server refuses the reader (HTTP 403), such as one a Provider Policy refuses, shows “You don’t have access to this” with “Your current access doesn’t include this Pane’s data”, and offers no Retry, since the same read is refused again. It reads again when the context changes, such as the reading or View as; returning to the window does not ask again. A read a filter Policy refuses (QL-PL-015) is not a failure: the Pane shows “Hidden by” and the filter’s name, says the data can’t be narrowed to the rows the reader may see, and offers no Retry. It reads again when the context changes, such as the reading or View as. A preview that draws Panes without running them, such as the picture Ponder takes of a Page, reads no Plot. A Pane that reads one shows a calm “Live data isn’t drawn in previews” in the picture, not “Could not read” with Retry. A Plot that a chosen older Refresh did not capture (PD-RESULT-001, or PD-RESULT-002 for a Plot it reads) is not a failure: the Pane shows Not in this Refresh with Read Latest, which moves the reading to Latest, and no Retry. A Plot that the chosen Refresh’s Release computes as a view, rather than storing it, reads under that Refresh as its view over what the Refresh stored, with the saved rows as of its start and the directories it captured. Only a view that needs other data, such as the Pool’s current schema, answers Not in this Refresh. On Latest, a reader is served the newest Release whose stored Plots can be served (see Reports). Its readers stay on the previous Release while a new Release’s first Report runs, so they never wait on a gap. A reader who opens the new Release early, such as its publisher, sees No data yet where its Pages go, with Power on Parable and Preview for a seat that may run them, until a compatible Refresh completes. A Patch on a line with no such Refresh shows the same state with Preview, and Read Preview once a Preview of it completes (Reports, PARABLE-6285). Their Panes do not read there. The state follows the line’s Latest (currentDisabledReason), so a Patch on a line that has a Refresh does not show it, even when the Patch edits a stored Plot. A Pane that reads a stored Plot with no data yet anywhere else, such as in that Patch, on an older Release or in a Personal Parable, gets PD-RESULT-002. While a Refresh of the Parable is queued or running, as the Refresh menu shows it, that read waits: the hook stays loading, so the Pane draws its own loading state and draws its rows when the Refresh completes. A Pane that runs its Plot itself (plotApi.run) waits for the Refresh the same way. When no Refresh is queued or running, nothing will answer the read: a Patch that plans no output for the Plot, an older Release, a Personal Parable, or a draft that changed its contract. The Pane then says No data yet with the server’s sentence, such as “This Plot has no data in these Changes until they are published and the Report runs.” or “This Plot has no data in this Release.”, and offers no Retry, and plotApi.run rejects with that sentence. A Refresh that starts reads it again and waits; a Refresh that ends without data says why. A Refresh that ends, or Latest moving to a newer one, also reads again every Pane whose read failed: one that says No data yet, Refreshing…, or Could not read. Returning to the window does not. Other not-ready reads on Latest draw Refreshing… over a skeleton. A Pane may catch a Plot hook’s failure in its own error boundary and keep the rest of itself drawn, as Team Time Spend’s Home does when its teams can’t be read. The host still knows a read below failed: when the reading, a Preference or the context changes, it draws that Pane afresh, so its own error state does not outlive the reading that caused it. A slow read ends with the code QL-TO-002 and a message that names the limit that ended it, which the host’s error shows. The web app’s server ends the read at 55 seconds and cancels it, which stops the query: “This Plot read took longer than 55 s, the web app’s limit, and was stopped.” Ponder stops waiting for a Plot read it runs as a tool after 30 seconds. The host remembers a read that failed. Asked again before its hold ends, it answers with the same failure and sends nothing. The hold starts at one second, doubles with each failure in a row and stops growing at five minutes, so Pane code that runs a failing Plot in a loop sends at most one request per hold. The first two holds leave room for the hook’s own retries. Anything that could change the answer ends every hold: a refused read, a row saved here or elsewhere, a Provider Policy saved, a Refresh that starts or ends, or Latest moving to a newer Refresh. A refused read (HTTP 401 or 403) is not held. A Pane’s frame may send at most 60 requests a second; past that the host answers RATE_LIMITED, “This preview may send at most 60 requests per second.” A 429 with no code of its own is a rate limit on the reader: Parable allows 500 requests a minute from one address, counting every tab of the app. Every Plot read and Pane resolution then pauses for 30 seconds, or for as long as the answer’s Retry-After says, doubling to two minutes while 429s keep coming. During the pause nothing is sent: a read answers with the 429, and the Pane shows “Rate limit exceeded” with Retry. When too many queries are running, Parable does not start the read. The read fails with the code QL-IN-008 and the message “Too many queries are running right now. Try again in a moment.” The host shows it in the Pane’s place, and Retry reads it again. Generated types come from the Perception’s declared inputs and outputs. A Plot whose rows come from its Pane’s type is typed as rows of Pick<Type, columns>, with the columns its saved SQL returns. SQL planning and execution check that contract, including columns, nullability, and scalar representations. The editor’s Preview sends its edited SQL under the selected Plot’s same contract and its Pane’s current Policies, so it works only for someone who may save that SQL (Security). A read of a Plot with no saved SQL reports that it has none, and creates no SQL or data. SQL Output contracts support explicit object fields and homogeneous arrays, including nested objects and arrays of objects. Dictionary maps, tuple columns, multidimensional arrays, and mixed unions refuse at compilation. These SQL restrictions do not limit the ordinary types of Panes and their saved rows. Mounted Plot actions and hooks retain the contract they were compiled against, including hooks reached through a referenced Pane. If the selected contract changes, existing callers refuse until refreshed; they cannot silently inherit the new input or output type. Changing current access still takes effect on each request.

Plots of a Platform Parable’s tab read the Parable beside it

A Platform Parable’s tab sits beside the Parable on screen, its subject. The tab’s Plots read that Parable through five relations: subject.parable (its name, handle and the people who hold Primary on its Deployment), subject.revision (the Release, Patch or Refresh on screen), subject.patch (the Patch, its stage, its Problem, the person who started it and each person who changed it), subject.pieces (the Pieces of the revision) and subject.changes (each Piece the Patch adds, changes or removes). The Parable Pool lists their columns.
The server checks the subject on each read and builds the rows from the tree Ponder reads, kept to what the reader’s participation lists in the Pieces tree: a Peeking reader gets Pages only. A read with no subject gets the same relations with no rows, so the Plot saves, checks at Prepare and Publish, and runs on a schedule like any other. The relations hold names and metadata, never Piece source.

Incremental Plots

A Plot can declare that its output is a set of time intervals, so that a change to a source runs again only the intervals the change touched. Parable marks the intervals that each source change touched and runs each one into the Plot’s stable table. A Report Refresh pins that table instead of running the Plot (Incremental Plots, PARABLE-8076, PARABLE-8078, PARABLE-8142). A publish that changes the Plot builds a new table beside the current one (Epochs, PARABLE-8080). The declaration is the Plot’s incremental field. A Plot without it is not incremental. Only a Pipeline, a Plot that a Report runs, can declare it. Saving the declaration on a Plot that a Pane runs is refused. Saving it in a Parable whose Home Page declares a react contribution is refused too, because a Refresh about a Patch cannot use an incremental Pipeline. A source in sources can be an incremental Plot of the same Parable, named as the SQL names it: parables.{parable}.{plot} (PARABLE-8079). Each interval of it that completes then marks the intervals of this Plot that its range reaches. The grain must be the same as that Plot’s grain, or coarser, also when this Plot reads it through a view of the Parable. Publish refuses a finer one (MANIFEST_INVALID, path /incremental/grain, PARABLE-8246) (Incremental Plots). The stable table starts empty. Parable marks an interval only when a change lands in its time range after the line publishes the declaration. Until the Plot commits its first interval, a Refresh runs it in full, as it runs any Pipeline. After that, the Refresh pins the table, and the table holds only the intervals that changed since the publish. History from before the publish needs a backfill. A person with the budget override permission and the Primary seat starts one from the Pipeline’s History, with Backfill under Intervals (PARABLE-8691, Run history and status). So a Pipeline that becomes incremental shows only new intervals until someone backfills its history. Saving, Preview and Prepare check the declaration against the Plot’s SQL, and the editor shows the same findings while you edit. Each finding holds Publish. PV_INC_001 to PV_INC_010, PV_INC_012 and PV_INC_013 are errors, and PV_INC_011 is a warning: The AI functions (ponder(), ask(), choice(), noul(), score()) are allowed. The inference log keeps each answer by its inputs, so a second run of an interval reads the same answers. An interval run also runs the SQL of each Plot that the Plot reads as a view. So PV_INC_006 also reports a call in such a view, or in a view that the view reads. Saving reports it on the reference to the view, and the message names the function and the view that calls it. The editor shows it while you edit the incremental Plot, before a save, when your Policies let you view the SQL of every Plot view in the Parable (view.plots). It judges each view by the view’s saved SQL, so a view with unsaved edits gets no finding in the editor until you save it. If you edit the view later, or set it back, the save checks again each incremental Plot that reads it, also through other views. This includes a Plot that your Patch does not change: the editor shows its finding, and the Patch does not list the Plot. On a Workspace copy, this includes a Plot that the copy reads from its upstream Release. Prepare checks every Plot. Preview and Prepare also report it on the reference to the view, but their message names neither the view nor the function. The editor and the answer to a save or a re-check name them only when your Policies let you view the SQL of every Plot view in the Parable (view.plots). Each interval run also checks its whole plan, views included. This catches a call that the save cannot see, such as a call in a Plot of a Platform Parable. A run whose plan calls one of these functions fails with QL-PL-024, and the message names the function and the view. Intervals, in the Pipeline’s History, shows the message and the code (Run history and status). The interval runs again after its wait and fails the same way until you remove the call from the view. When the check cannot derive the type of a time column, it does not let the column pass. It reports PV_INC_011. An expression such as date_trunc('hour', created_at) has no derived type, so cast it: CAST(date_trunc('hour', created_at) AS TIMESTAMP) AS event_time. A timeColumn with no scalar, or a source Plot with no derived schema yet, gets the same warning.

Split an interval

A Plot that declares split runs each interval as parts units (PARABLE-8081). Part p keeps the rows whose parable_interval_split_part(key, parts) is p. The planner puts that filter on the output, and moves the same filter down to the scan of each source in sources that the key comes from. So each part reads only its own keys, and needs less memory than the whole interval. The planner moves the filter only where the Plot’s answer stays the same:
  • through a GROUP BY that includes the key,
  • through a window whose PARTITION BY includes the key,
  • through a join, to each side whose rows the join does not fill with NULL.
So the parts together hold exactly the rows of the unsplit interval. An interval run is refused when the filter reaches no source in sources, with the step that stopped it, such as a GROUP BY without the key. A key that the Plot computes, such as lower(email) AS email, is moved as the same expression. A Plot that groups by ROLLUP, CUBE or GROUPING SETS cannot split. The hash is fixed: a key is in the same part in every release. A NULL key is in part 0. A key can be text, a number, a boolean, binary or a time. Each part has its own row in the interval ledger, runs and retries on its own, and replaces only its own rows in the stable table. A save that changes split on a Plot that the line has published as incremental is refused. Another key, another parts, and a split that is added or removed are all changes. The Plot’s stable table holds the rows of the published split, and a run with another split would lose rows or keep them twice. A change of split waits for epochs (PARABLE-8080). An archive import is refused the same way.

Foreign keys and graph edges

Every Parable is a graph. Its nodes are its Plots, and a Plot’s output columns are its properties. Two facts on a Plot give the graph its edges (PARABLE-9104). GRAPH_TABLE reads them to turn a graph pattern into joins. Both are null on a Plot without them. A Plot without graphEdge is a node, also when it has foreign keys. View Plots can be nodes and edges. These rules hold for every published Plot:
  1. A column has one foreign key or none.
  2. references splits at its first dot. A catalog slug never holds a dot, and a column name can. The part before the dot is the catalog slug of a Plot of the same Parable. The part after it is an output column of that Plot.
  3. The foreign-key column and the column it references have the same type: the same primitive and the same scalar. Nullability does not count.
  4. graphEdge.from and graphEdge.to are two different columns, and each one has a foreign key.
  5. Names compare exactly as the catalog spells them: the catalog slug, and the column name as the Plot’s output schema holds it.
A save checks rules 1, 2 and 4. For rule 2 it checks only that the Plot exists in the Parable. A Plot new in the same save is found by the catalog slug that its handle gives it. A save refuses only a fact that it adds or changes, so a Parable that already holds a broken fact keeps saving its other changes. The editor save, Ponder’s edit_pieces and an archive upload check the same rules. Publish checks all five rules against the output schemas that it plans, after the Plot SQL gate is clear. Every publish path runs the same check: Publish & Deploy, a Workspace Publish and a Platform Parable’s automatic apply. Prepare does not run it, so a broken fact shows at Publish. Each refusal is MANIFEST_INVALID, with entityKey, the Plot, and path, a JSON pointer such as /foreignKeys/0/references or /graphEdge/from. The sentence names the Plot and what is wrong, for example “Plot reports_to has a foreign key on manager_id to Plot people, but this Parable has no Plot people.” The facts travel with the Plot. The catalog snapshot that the editor, the save and the publish gate validate against carries them on each Plot’s entry. The published Plots read that Query Layer makes carries them from the published Plot, so a query reads the graph of the Release it reads. A Parable archive carries them in each Plot’s run settings. A Refresh writes an edge Plot’s output for fast hops. The rows are sorted by the from column, in row groups of at most 8,192 rows, with a bloom filter on the to column. A hop from one row of the from Plot then reads about 1% of the edge Plot, and a hop into one row of the to Plot about 18%, measured on 1M edges. The cost is a larger file than the same rows sorted with the default settings: 10.1 MB against 4.65 MB for 1M edges. A Plot that is not an edge keeps the default layout. An incremental edge Plot keeps the default layout too.

Where a Pipeline keeps its output

A Pipeline can name the store that keeps its output, with the Plot’s materialize field: { "store": "clickhouse" } keeps it in the Workspace’s own ClickHouse instance, and { "store": "delta" } keeps it in Delta. A Plot without the field keeps its output in Delta, as every Plot did before the field (PARABLE-9101). An archive carries the field with the Plot’s other run settings. Save, publish and an archive import refuse a store the output cannot use, with MANIFEST_INVALID, path /materialize/store, and a sentence that names the Plot and the fix: ClickHouse storage is not live yet. Until Parable writes a Pipeline’s output to ClickHouse and reads it from there (the next steps of PARABLE-9101), a Pipeline that names ClickHouse keeps its output in Delta, and every reader reads it as before. A deployed Workspace also needs its ClickHouse instance, whose rollout waits on PARABLE-9102, and that instance’s reader and writer credentials (PARABLE-7556).

Services

Kysely is available by default. It compiles query structure into SQL and a separate parameters array. Preview the result through:
Previewing runs the compiled query under the declared Plot’s contract and its Pane’s Policies, and saves nothing. A Pane can call preview in the Piece editor’s canvas and on a Page in Ponder’s chat. Both send the SQL to the server, which runs it only for someone who may save it, at their own seat whatever their participation mode, and refuses anyone else with HTTP 403, PA-AU-019 and the reason before any query starts (Security). A refused preview is a refused read, so the Page’s Panes resolve again, and where the Page has no store key the kept results drop and the Panes read again: offer a preview only where the Pane’s Policies allow edit.plots or create.plots. The picture Ponder takes of a Page and a Lab Page refuse it. run, the hooks and readReport send no SQL and work for every reader of the Plot. SQL is saved only as a Plot’s implementation: through a save of the Plot in the editor or by Ponder, or by a proposal from a Pane’s code, propose.change(plot, { sql }, { expectedRevision }), when its Perception lists "plot" in proposes. A proposal goes into the reader’s Patch after the reader confirms it, needs what the same edit needs in the editor (edit.plots included), held by the reader and the Pane’s author alike, and reaches readers when the Patch is published. A query built in the app any other way is not stored. A Pane that shows SQL rather than running it, such as a mapping table or a drill-down, reads a Plot’s saved SQL with plotApi.source('meetings'), or a declared Pane’s with panes.meetingZoom.plotSource('zoomMeetings'). Either returns the SQL of the definition being viewed: the Release, or the draft open in the editor. It returns null when the reader’s Policies do not let them view the Plot’s SQL (view.plots); the Plot still runs for them. A surface that does not hold the Parable’s definition, such as a Pane preview in chat, refuses the call. A Pane can check a SQL condition or value against a Plot’s columns with plotApi.checkSql('meetings', { kind: 'condition', sql }). Use kind: 'value' for a value expression. The check validates the expression against one Plot’s columns without running the query. It requires the reader’s access to the Plot’s output. Its validation catalog contains only that Plot’s columns. References to other Provider Pool, Workspace, artifact, or Plot relations are refused without looking up their columns. It checks parsing, column references, and scalar rules. It does not use the query planner, so it can miss unknown functions and conditions that are not Boolean. checked: true means the validator ran. It does not guarantee that the SQL will execute. checked: false means validation was unavailable. SqlEditor accepts this function through its check prop and shows the diagnostics beside the text. A declared Pane exposes the same check through panes.meetingZoom.checkSql('zoomMeetings', { kind: 'condition', sql }). A Plot’s SQL source can also keep parameter defaults beside its code. Those defaults belong to the definition, survive publication, download and upload, and must match the Perception’s declared Input. Older Releases keep their own defaults after a later edit changes or clears them. React and CSS sources cannot carry SQL parameters. An explicit empty array stays distinct from omitted defaults. Use a declared tuple or scalar array for nonempty parameters. Native Dates serialize as timestamps; strings, finite numbers, booleans, and null retain their values. Parameters remain bound values and are not interpolated into SQL. A read that passes no parameters uses the saved parameters. An explicit array replaces them, including an empty array. Use explicit casts when needed to keep the planned output type stable. Native numeric values must be finite. Integer-valued numbers must fit JavaScript’s safe range, from -9007199254740991 through 9007199254740991. This also applies inside nested values, parameters, saved defaults, and Report results. Unsupported values refuse before a write or result completes. Stored values and Report results are never rounded or changed when they are read. Finite fractions remain supported. Use an explicitly declared string contract when an application needs to retain larger integral identifiers as text. A Plot edit uses the revision last read. A refused save preserves the local draft; reload explicitly before replacing it. plotApi.key('activityFacts') returns the declared Plot’s stable identity; the identity grants no access by itself.

Save a query for the Workspace

Save a query for the Workspace (save_query_for_workspace, PARABLE-5378) keeps one SQL query’s rows where everyone in the Workspace can open them, in one call. It is for a person or an agent who has a query and wants no Page of their own design, such as “save the count of open pull requests per repository so everyone in my Workspace can see it”. It shares with the whole Workspace, not with one team. It takes a name, a one-sentence description, one statement over the Provider Pools with no parameters, and an idempotencyKey. It needs the permission to manage Parables and the permission to query the Provider Pools, and it asks for a person’s approval in Ponder. Its MCP tool says destructiveHint: false, because it only adds a Parable. The server runs five stages, each through the operation that a person or Ponder would call for it, so each stage keeps that operation’s checks. Every check that can refuse runs in the check stage. The checks that need only the SQL run before the Parable exists. Plot planning runs on the new Parable, before it is shared or holds a Patch, because the Query Layer plans a Plot only in the trusted context of a Parable that exists, as for a save in the app. A refusal there removes the new Parable: Each field takes the type that Plot planning names for its column, such as Temporal.DateTime, Generic.Int64 or Contact.Email. A column of plain text, a plain number or yes or no is Generic.Text, Generic.Float64 or Generic.Boolean, as a Plot’s rows read it. The rows are a plain Plot, not a Pipeline: each reader reads them live, under their own Provider Policies. To read them, read the Plot’s data with the rootRefId and the plotKey that the answer gives. A refusal that the caller can correct is an answer with saved: false, the stage that stopped, and a refusal with its code, its message, a correction, and retry. With retry: true the same call can pass later, for example while the Plot check cannot finish. When a stage after create fails, the save removes the Parable it made, as the product deletes a Parable: it pockets it, then deletes it. The Parable leaves every list, and its handle is free again. A Parable with a Release is never removed, and neither is one that another call is writing. A failed cleanup or a stopped process can leave a draft. The same call continues that draft, or starts again after its removal. A save binds its normalized input to the create receipt before it writes the Patch. A replay with different input cannot reach cleanup of the earlier save, even if the first process stopped just after create. Calls with the same Workspace and key run one at a time across replicas. A concurrent call answers PA-AU-008 (command in progress) and can retry after the first ends. The cleanup checks for Releases on the primary while it holds the same Parable lock as Publish. It refuses cleanup if its save-lock connection stopped. A save runs for at most five minutes, or the request’s shorter deadline. Cleanup has a further 30 seconds. Each API replica admits at most four saves and uses at most a quarter of its primary connection pool for their locks. This reserves connections for each save’s commands. A full admission limit answers command in progress. A pool with fewer than four connections admits no save. A call with the same key continues a save that stopped, for example after a timeout: it skips every stage whose effect is already there and makes no second Parable or Release. After SQL preflight passes, the same key with another name, description or SQL is refused with PA-AU-006 at create. Preflight can first refuse invalid SQL or a caller without Pool access. In Ponder, Parable sets idempotencyKey for each Message, so the save makes its key from the person, the Workspace, the name and the SQL: a call in a later Message with the same input continues the same save. Inside a Ponder turn the Patch still starts in the new Parable’s own Problem, because the turn’s Problem belongs to another Parable. Each Workspace makes at most 120 saves in one hour, counted on every replica. A save past the limit is refused with HTTP 429, PA-AU-090 and a Retry-After header, before anything is created. Each caller also makes at most 10 calls a minute. A save counts when it reaches create, so a save that Plot planning refuses on the new Parable counts too. While that Parable exists, for the time of one plan, it holds the handle of its name: another save with the same name is then refused with PA-AU-024, and the same call passes after the removal. A Parable that exists for a moment holds no other place in the limit.

Nullable Conditions

When a condition can be NULL, write FILTER (WHERE enabled IS TRUE) or FILTER (WHERE coalesce(enabled, false)). A comparison on a column that can be NULL is such a condition: write FILTER (WHERE (amount > 0) IS TRUE). Use the same explicit condition in CASE WHEN enabled IS TRUE THEN ... ELSE ... END. The pinned DataFusion 53.1 engine can count NULL filter values as true in a grouped count. This example gives both input rows a NULL condition. The correct count is zero. The bare filter can return two, while IS TRUE returns zero:
The engine’s filter path can read a Boolean’s value bit without checking its NULL bit. The existing upstream report describes this defect, with a comparison on a NULL column as its example. The upstream fix ships in DataFusion 54.0.0, not in 53.1. Explicit NULL handling avoids the defect in the Plot SQL. Server validation records PV_AGG_001 for a grouped, non-distinct COUNT with one argument whose filter is a bare nullable Boolean catalog column. This advisory does not block publication. It does not infer nullability for computed expressions, derived tables, or unknown columns, so a check with no advisory does not replace the authoring rule. The CASE form above is the same explicit-NULL convention, not a separate known CASE defect.

Large results

A Pane’s Plot read returns at most 10,000 rows. Read only what the Pane draws, in this order of preference: aggregate in SQL, store heavy work, then page through rows a person is browsing. The examples use Team Time Spend’s action_hours (one row per person, Action and day) and time_spend_facts (hours per person, category and day), both stored by Pipelines. 1. Read what you draw. Aggregate in SQL to the grain the Pane shows, and never send raw rows to the browser to add up there. Apply filters, the scope and corrections before the GROUP BY. A Home chart of hours per category reads a few dozen rows, not every fact:
Totals come from an aggregate, never from adding up pages. A reader’s filter Policies remove rows before COUNT(*) or SUM sees them, so the total is exact for every reader and under View as:
2. Store heavy work. Make an expensive query a Pipeline and read its stored output (see Plots read as views and Reports). time_spend_facts is a Pipeline: the categorizing and corrections run once per Report run, and every Pane reads the result. 3. Page through rows only for a person browsing them, such as a table, a list or a drill-in, and never to assemble a total. Every page is a full Plot read: the server plans it, binds its inputs, runs it and sorts the whole result again. Prefer keyset pages over a stored output. The Pane passes the last row it shows as parameters and asks for the rows after it, ordered by columns that end in a unique key:
The Plot declares those parameters in its contract, and the Pane reads the next page when the person asks for it, for example from a Show more button that calls the hook with the last row’s values. A result the browser kept answers a page read again at once. A value several Panes share, such as a chosen person, can come from a Pane the SQL reads by name instead of a parameter. OFFSET is for shallow pages only: the server still sorts every row and skips the ones before the page, so a deeper page costs more.
Pages follow the Page’s reading. When the reader has chosen a Refresh, every page reads it. When the Page reads Latest, nothing keeps a later page on the Refresh an earlier page read: if a new Refresh lands between them, rows can move between pages. A read of a stored Plot names the Refresh that answered in its result’s meta.reading.reportRunId, but a paging Plot like the one above reads stored Plots by name and its result does not say which Refresh they came from. 4. The 10,000-row limit is a guard, not a target. When a Plot has more rows, the Pane gets the first 10,000 and its hook’s truncated is true. The host shows no notice of its own, so a Pane that could be cut off says so, and the editor drawer’s Performance tab flags the read (“Stopped at the 10,000-row page, so its Pane drew part of the answer”). A Pane that is cut off is a bug: aggregate, store or page instead. 5. Ponder and other agents follow the same rules. Ask the aggregate question in SQL instead of paging through rows to count or add them up. The Plot read operation an agent calls takes up to 10,000 rows per page and answers with nextCursor while more rows remain; each page is again a full read.

Storage

A definition version identifies code and schema. A Report identifies produced data. For a Pipeline, Latest (the API’s Current) reads one Refresh: the newest completed Refresh whose Release is in the same major line as the selected Release. A Release that keeps its major number reads the Refreshes of earlier Releases in its major line. A Release that starts a new major line needs a Refresh of that line. Every stored Plot reads its output from that one Refresh, and a Plot it did not store, or stored under another output shape, has no data until the next Refresh. Publishing does not run a Report or change its schedule. A Report run stores every Plot’s output as a table of its own, written once and never overwritten. An incremental Plot is the exception: a Report run pins its one table instead (Incremental Plot tables). A later run cannot change an earlier one, so choosing an older Report returns the rows it held when it completed, in the order the run wrote them. A Report run that fails or is cancelled stores no readable output, even for Plots that finished. SQL that names parables.{parable}.{plot} for a Pipeline reads the output of the newest completed Report that fits the published definition, and an empty table until one completes. A completed run’s rows show on the next read. For a Plot nothing stores, the same address runs the Plot’s SQL (Plots read as views). A Pipeline’s output was produced under its run’s principal, so it is served only to a reader who still has that run’s access to its inputs (Security). A run that completes reaches parables.{parable}.{plot} within moments. In rare cases, a read can keep answering with the earlier output, or the empty table, until Parable loads its catalog again, which it does three minutes after the last load. Each Report keeps its newest 30 completed runs and its recent manual Refreshes (Reports). An older run leaves the Refresh menu, Latest and parables.{parable}.{plot} before its tables are deleted, so no read ever sees a table that is partly deleted. Choosing a Refresh pins that run; an unavailable or incompatible result does not fall back to Latest. Other Plots and previews can read completed Pipeline outputs with their own SQL and parameters. A direct read of a stored output keeps the inputs its run captured and refuses other parameters. Use plotApi.readReport('activityFacts') to read completed output with its captured inputs. It returns the Perception’s typed rows without requiring today’s input tuple. It uses the selected Refresh, or the Refresh Latest reads, and refuses when no materialized result is available. run and preview keep their declared input requirements. A named read runs the Plot’s SQL from its implementation in the selected ref or Release. The result names that implementation row and its version (sourceImplementationId and sourceImplementationVersion), and a Report records the same pair for each Plot it ran. A stored result keeps the contract and source that produced it. consumerContractHash names the contract of the selected definition that accepted the rows. materialized is true only when the rows come from the selected run’s stored output, and false for a query that runs under a Report reading. SQL catalog addresses use the stored Parable and Plot slugs. Both segments begin with a lowercase letter and contain lowercase letters, digits, and underscores between groups. Hyphens and case-only replacements are refused. A new Plot’s slug is the snake_case of its handle (revenueForecast is revenue_forecast) unless its author sets one, the same rule a Pane’s name follows. Renaming a handle does not move those addresses. A save that asks for a different slug for an existing Plot is refused, and nothing in that save is kept. This is also true for the first save of that Plot in a new Patch. A Plot deleted on a ref frees its address on that ref, as a deleted Piece frees its handle, so a Plot that names that slug, as an upload from another Parable does, can take it there; a new Plot that names none never takes a deleted Plot’s address. Publication plans Plot dependencies from the schemas of the definition it publishes. A missing dependency or a cycle refuses publication. The cycle check judges the Plots as their dependencies will stand once the Release is published: two Plots in one Release that read each other are refused, and a Release that turns a dependency around (A read B; now B reads A) is not. A Plot can depend on a Plot of a Platform Parable, and of no other Parable: parables.<parable>.<plot> resolves against the line’s own Plots and the Plots of the Platform Parables the author may read, so a reference to any other Parable’s Plot gets PV_REF_002, and Prepare and Publish refuse it. Each Release pins the Release of each Platform Parable it reads, the newest one in the Workspace when it is published (Parable Pool). Planning creates no rows and needs no earlier Report. Publishing plans each Plot before it publishes the Release. The Release carries every Plot’s published SQL and planned output schema. List published Plots and Read a published Plot return the SQL and schema from that Release. Planning adds no change to a Plot the publish did not edit. Publish and Prepare plan every Plot before they answer, which takes minutes on a large Parable. The web app gives either 300 seconds, where other API calls get 60. When that limit ends a command, the web app cancels its request and answers 504. A publish stopped while it plans has written nothing. A publish or Prepare that Ponder runs as a tool has its own limit: Parable ends a Ponder tool’s operation after 90 seconds for a Message’s call and 2 minutes for a Run’s. A command stopped there answers 504 and says it ran past the time limit of the call that sent it, because sending it again through Ponder would stop at the same limit. A command whose request was cancelled some other way answers that sending it again with the same idempotencyKey returns its result if it finished and runs it once if it did not.

SQL checks and failures

Anyone with Primary or Publishing can save Plot SQL and read its findings. Save keeps the SQL when a check finds an error. The finding explains what needs to change. Prepare and the Plot’s Preview use the same SQL diagnosis when their inputs match. A Prepare finding names its Plot, so an agent can act without the Plot open beside the message. A function call with incompatible argument types is a SQL refusal, not a planner outage. For example, TRIM takes text. If its argument is a timestamp, remove TRIM, or cast the argument to VARCHAR first. A wrong argument count needs a different correction. Change the call to use the number of arguments in the message. Neither failure needs a service restart. Prepare keeps the result for each Plot. An invalid Plot does not make an independent Plot look broken. A Plot that needs the failed Plot’s output says which Plot it waits for. A circular read names the Plots in that cycle. Remove one circular read before Prepare. A Plot that only reads a cycle is not a member of that cycle. Only a connection failure, a timeout, or a known shared capacity limit advises a retry. An unexpected planner failure asks Parable staff to investigate. It does not claim that the SQL is wrong or that the planner is unavailable. SELECT * is valid when the source has known columns. A known disconnected Provider can still supply its declared columns for a check. Save, Prepare, and Preview accept that source. A source the platform does not know is a finding in all three checks. Replacing * with column names does not make an unknown source available. Save does not replace Prepare. Save can use an earlier checked result for a Plot that this Plot reads. The extra execution check also has a time limit. Prepare checks every Plot against the Patch it will publish. Neither check reads all Provider rows. Preview can still find a value that CAST cannot convert. Use TRY_CAST if values that do not fit should become NULL. A run of a Plot stores its output with the column types of the Plot’s plan. A UNION column keeps a scalar type such as Identity.Name only when every branch has it: when one Provider’s titles are names and another’s are plain text, the merged title is text. A Plot that reads a Pipeline’s table, parables.{parable}.{plot}, plans against the columns of that Pipeline’s plan. A run changes neither the Plot nor its Release. The table’s freshness is the time the Workspace’s last successful run of that Plot pinned its inputs. Each Workspace keeps its own, including a Workspace that received the Plot in a Delivery.

Incremental Plot tables

An incremental Plot keeps one Delta table for each epoch, at parables/{parable}/_incremental/{plot}/{epoch} under the Workspace’s provider root. The table is partitioned by the start of each interval, and also by the part when the Plot declares split. Each interval run replaces only the rows of its own interval, so a new interval adds rows and a restated interval replaces only its own. A source row that a sync deletes leaves the interval when the interval runs again. The interval ledger, query.plot_intervals, records each interval’s last commit: its Delta version, its row count, the digest of its rows and the hash of the contract that its run checked every row against. The table holds three columns that a read never serves: A full read and a paged read return the rows in the order of (_plot_interval_start, _plot_split_part, _plot_ordinal). A Report run’s own output orders by _plot_ordinal alone, which is unique in its table. In an incremental table only the three columns together are unique. A Report run does not run an incremental Plot. It pins the table at the version of the newest interval commit (Reports). Query Layer reads the pinned version, and checks the commit digest and the row count as it does for any stored output. It trusts the rows without a row check only when the ledger still describes the pin and every interval carries the Plot’s contract hash. Otherwise it checks each row against the Plot’s contract as it reads. An interval that commits after the pin does not change what the pinned run reads. The ledger cannot vouch for a commit it never recorded. So an interval run clears its interval’s row count in the ledger before it commits to Delta, and records the new commit after it. A run that stops between the two leaves the row count cleared, and every read of a pin of that epoch checks each row until a later run records its commit.

Epochs

A publish that changes what the table’s rows depend on starts a new epoch: the Plot’s SQL or parameters, the Policies of its Pane, its output columns, or the declaration’s by, grain, sources or split. An edit of the SQL, parameters or Pane Policies of a Plot view of the same Parable that the Plot reads, directly or through other views, starts one too: the view is planned inside the Plot. A change of quietMinutes, maxDelayMinutes or onSourceChange keeps the epoch. Every interval of the current epoch runs again into the new epoch’s table, at the new grain and parts. Reports keep reading the current epoch while the new one builds, also while one of its intervals failed and waits to run again. When every interval of the new epoch is complete, the new epoch becomes the one that Reports read. An edit back to the current Plot before then drops the new epoch. An incremental Pipeline that reads this Pipeline reads its current epoch only. When the new epoch becomes current, the reader’s intervals over the new epoch’s range run again against it. The reader keeps its own table. Before you publish, the change preview lists the Pipeline with the number of intervals the new epoch runs and their estimated cost, and calls the cost out when it is above the Workspace’s Refresh cost threshold.

Maintenance

Each interval run that commits to the table keeps its epoch in Plot maintenance. Compaction merges the small files of one interval and part, and never the files of two intervals, so the table keeps at least one file for each interval and part. VACUUM removes the files that no version of the last 7 days reads. It keeps the files of each version that a running Report run pinned, also when that version is older. A Report does not stay reproducible: a Report run pins the table only while it runs. Report run output GC never deletes an incremental table. When a Report run that pinned the table leaves retention, its result goes and the table stays. An old epoch’s table goes 15 minutes or more after a newer epoch replaced it, once no interval run writes it and no running Report run pins it. The current epoch’s table never goes.

Dependency graph

A Plot dependency says that one Plot reads another. A Plot is its Parable and its key, so Plots with the same key in two Parables are two Plots. Publication checks the dependencies by the rules below. Report capture uses them to find what each member reads, and a Report run orders its members by them. A Plot that reads a Plot of a Platform Parable publishes a dependency between two Parables. Publish refuses any other dependency between Parables with PV_REF_002. Saving a Plot checks for PV_CYC_001: a cycle through the Plot being saved, judged from the SQL you save and what the Parable’s other Plots read. For each Plot that reads a Platform Parable’s Plot, publication also records the columns it reads. This includes columns used only in a filter, a join, or a grouping. The record comes before optimization. An unused column in a CTE still counts when the SQL resolves it. A CTE that selects * records every upstream column, even when its outer query selects only one. SELECT * records every column of the upstream Plot at that Release. SELECT count(*) FROM ... with no other column references records an empty list. Republishing records the columns that the new SQL reads. Reads within the same composed Parable have no such list. Older dependencies have an unknown list. A wholly unused CTE or a wildcard with options such as EXCEPT, EXCLUDE, REPLACE, or RENAME also gets an unknown list for the upstream Plots it reads. These forms can still require columns that no longer appear in the returned plan. If their inputs cannot be identified precisely, every upstream input gets an unknown list. Other queries keep their exact lists. Unsupported SQL remains refused. An unknown list means that every column may be read. An empty list means that no column is read. Publication does not backfill older dependencies. This record does not change what a Page shows.
  • Cycles. The check finds a Plot that reads itself and Plots that read each other in a cycle. Publication refuses either: parable: plot dependency cycle: Plot … depends on itself, or parable: plot dependency cycle through Plot … in Parable ….
  • Report members. For each member, capture lists the members it reads, directly or through views. A member that reads itself, directly or through a view, is refused first, then members that read each other in a cycle. The refusal names the members: parable: plot dependency cycle: Hours by team reads Logged hours reads Hours by team, or parable: plot dependency cycle: Hours by team reads itself. A scheduled run refused this way records the refusal and its reason on the plan, and the occurrence is not tried again.
  • Report runs. A Report run runs a member after every member it reads.
  • Downstream. The Plots downstream of a set of Plots are all the Plots that read one of them, directly or through other Plots. A Plot of the set is among them only when it reads another Plot of the set.

Output schema diff

When a run publishes a Plot’s output, it compares the new output schema with the one the Plot held and records the columns added, removed and retyped, and whether the change is breaking, additive or changes nothing. A removed or retyped column is breaking. Added columns alone are additive.

Its Panes’ Plots

A Page’s or Pane’s Plots tab also lists the Plots of the Panes it declares, under Its Panes: each such Pane once, as a closed group with the number of Plots it owns, read only when it opens. Choosing one shows its SQL read-only, the reader’s draft included, with its Pane, its name and Read by. Open goes to that Plot in its own Pane’s Plots tab, where it changes. A Page owns no Plots, so its Plots tab lists its Panes’ Plots alone.

Run history and status

A Plot’s editor has three tabs below its SQL. The Refresh control at the top right of the editor, Results and History share one selected reading. When the SQL cannot be changed, such as in a released Version, a Proposed set or an archived Parable, the band of the tabs shows a read-only badge with the reason in its tooltip, whether the tabs are folded or open. In Peeking and Pinging the SQL stays on screen and the badge names the mode, such as Peeking, not Publishing.
  • Results shows the selected reading. On Latest it holds Preview’s rows with their count and time. Above them it lists what Prepare would block this Plot for, if anything: Preview runs Prepare’s own checks on the edited SQL, judged by how the Plot runs (a Pane or a Report), and still shows the rows. Preview in the Refresh menu is a different thing: a stored Report run on a sample of the data (Reports), read like any other run. On a stored Report run it holds that run’s own rows, read without running the Plot. A run that is queued, running, paused at its budget, failed, cancelled or skipped shows that status instead of rows, and a failed run shows its message. A Plot that finished inside a run that is still moving, or that did not complete, has no readable rows either: only a complete run is readable. When the selected run does not include the Plot, because the Plot is not a Pipeline or the run has no result for it, Results says so and reads nothing. Preview runs on Latest only, so it is unavailable while a stored run is selected.
  • History lists every Refresh (a stored Report run), newest first. Each row is named by when the run happened, in your time zone and clock: 12:31pm ET today, Sep 29, 9:00am ET before, with the whole moment on hover. Results names a stored run the same way, as in Refresh Sep 29, 9:00am ET · 1,204 rows. Each row shows this Plot’s status, whether the run was Manual, Scheduled, fired after its sources (After Okta synced) or a Refresh by Parable (Parable started it to repair data after a platform update), Refresh everything when it was forced, and for which Report, Started by (the person who pressed Refresh, or Schedule, Sources or Parable), when the Plot started and last changed, how long it ran, how long it waited to start, its row count, its spend in US dollars and paid calls against the caps captured when the run started, and the catalog address with the run’s short id. A row count the reader may not see reads Hidden, never 0. Choosing a row selects that Refresh in the Refresh menu and the address (refresh); choosing one in the menu selects its row. See Run a Report now.
  • On a Workspace’s line, the History of an incremental Pipeline starts with Intervals (PARABLE-8221). Each epoch of its stable table shows how many of its intervals are Pending, Running, Complete and Failed. A Building epoch also shows how many are complete, because Reports read the Current epoch until every interval of the new one is complete (Epochs). Below the epochs, the newest 200 intervals each show their state, when the last run started and how long it took, the rows it wrote, when a waiting interval runs next, the attempt, and the message and code of a failed run. The list reads again every 15 seconds while an interval waits or runs or an epoch builds. A row count reads Hidden when a filter Policy or a Provider Policy narrows what you read, because the interval runs read as the line’s Accountable seat.
  • When every interval of an incremental Pipeline waits to run again (a restate, PARABLE-8260), Every interval runs again shows above the epochs. It says why: a source the Pipeline does not read by time changed, a shared setting or Pane row it reads changed, or both. It shows the estimated cost of the paid AI calls against the Workspace’s Refresh cost threshold, and when the restate was held. Estimating means the estimate still runs. Held means the estimate is above the threshold, or it ended without an answer. Approve shows only to a person with the budget override permission and the Primary seat, as Raise budget does. It asks you to confirm the estimated cost first. Approved shows until the next sweep, within about a minute, runs every interval again. If the restate changed before your approval, the list reads again and shows it as it stands.
  • Backfill, beside Intervals, runs every interval of a range of days again, such as the history from before the Pipeline became incremental (PARABLE-8691). It spends the Refresh budget, so it shows only to a person with the budget override permission and the Primary seat, as Raise budget does. You choose From and To in your own time zone, and the dialog counts the intervals before you confirm. It also estimates the paid AI calls of those intervals and shows the cost against the Workspace’s Refresh cost threshold (PARABLE-8741). Backfill waits for the estimate while it counts. No answer means that the estimate ended without an answer, and Try again starts it again. The real cost can be a little higher than the estimate, because intervals that run at the same time can each pay for one call. The intervals run in parallel, up to half of the Workspace’s query-run slots, and their commits take turns, so readers see part of the history until every interval completes. A range ends by now, and one backfill marks at most 100,000 interval runs. One Pipeline takes at most 20 backfills in 24 hours (PARABLE-8871). After that, the dialog says when you can backfill it again, before you confirm. Ponder suggests the range and points you to Backfill. It does not start one.
  • Details lists the catalog address parables.{parable}.{plot}, whether Report runs include the Plot, its published caps and its output columns.
The same runs in History, read in New York (en-US) and in London (en-GB) after midnight there: A History row for a run that is still moving offers Cancel Refresh, and a row where this Plot is paused at its budget offers Raise budget. Raise budget opens a dialog with the Plot’s spend against its caps and takes new cost, call and runtime caps for that run only. A control the reader may not use stays visible, disabled, with the reason. See Cancel a run or raise a paused Plot’s budget. The Refresh trigger marks the newest run while it is queued, running, paused at its budget, failed or cancelled, and while that run is moving the menu offers Cancel Refresh. Opening the menu reads the runs again. While a run is still moving and the page is visible, the page reads the runs again every 5 seconds, then every 30 seconds once the newest moving run is five minutes old. When every run has ended, a page on Latest reads the runs every 30 seconds, so it sees a Refresh that another person or the schedule starts. A page that waits for the first Refresh of a newer Release, or that shows Power on Parable, reads them every 10 seconds. A page pinned to one stored Refresh stops reading. A hidden page sends no reads. It reads the runs again at once when it is shown again. A read the server refuses the reader (HTTP 403) stops the polling and is not retried until the reading changes or the reader acts. Other reads in the app keep their one retry on any failure. When that retry also fails and no run moves, a page on Latest reads the runs every 5 minutes after a server error or a lost connection, for example during a deploy, or when the server asks it to wait (HTTP 408 or 429). It reads them every 30 seconds again after a read succeeds. After any other failure, the page stops reading until it is shown again or the reader acts.

Security

A Plot is governed by its Pane’s Policies for the same Release or Patch. Source inspection uses view.plots; source edits use edit.plots. The rows follow view.preferences, and the filter Policies that cover it narrow them. Permission to read data need not disclose its query. List published Plots and Read a published Plot return each Plot’s SQL. A person sees a Plot there only with an active Workspace membership, a seat on the Parable, and the Pane’s view.preferences and view.plots grants. Neither returns the settings of the Panes that a Plot’s SQL reads by name. Read the Plot’s rows to get them applied, or read a Pane’s settings, which follow that Pane’s view.preferences. Using a React renderer requires access to the Pane and its values. It can be delivered without granting its source-editor view, but executable JavaScript necessarily reaches the browser. CSS has the same limitation. These restrictions cannot promise secrecy for delivered code or styles. A Plot read may carry SQL of its own, a constructed preview, which runs in place of the Plot’s saved SQL. The server runs it only for someone who could save that SQL, by the three checks a save makes: the Parable edit permission (tenant.parables.manage), a seat that edits Pieces (Publishing or Primary), and edit.plots or create.plots from the Plot’s Pane Policies. Anyone else, a Viewer included, is refused with HTTP 403 and PA-AU-019 before any query starts, whether the request comes from the web app, the API or Ponder. The reason says which check failed: that only people who can edit the Parable’s Plots can run their own SQL on them, or that it needs edit.plots or create.plots on the Plot’s Pane. Policies cannot admit a seat that edits no Pieces: a grant that names a Peeking or Pinging person, or a Role they hold, shows them edit.plots in the Pane’s capabilities, but the server still refuses their SQL. The checks use the reader’s own seat, as every Plot read does, so a participation mode below it changes nothing. A Pane’s preview reaches them from the Piece editor’s canvas and from a Page in Ponder’s chat, and the Pane gets the server’s code and reason; the picture Ponder takes of a Page and a Lab Page refuse it without asking the server. Code in a Page can send a preview for any Pane’s Plot in the Parable, as run can, and these checks limit what it runs. Under View as, the seat and grants of the person being previewed decide, and the permission is the viewer’s own. A read without SQL runs the saved SQL and needs only view.preferences. SQL you run inside a Parable without saving it, from a Lab Page or with Ponder’s SQL tool, works for anyone who can read the Parable: a seat on it and a role that reads Parables. It reads that Parable’s Plots by their catalog addresses, parables.<parable>.<plot>, each under its Pane’s Policies, so a reader gets the rows the Panes would show them and nothing more, and a Pane whose Policies refuse the reader refuses the SQL. generate_series and range work there too. A Plot read as a view still reads its own sources. A Plot that takes inputs or runs as a Pipeline is read from a stored Refresh that matches its current output. With none, or when your Patch edited its SQL, that Plot has no data there yet: the SQL, and a Pane whose Plot reads it, is refused with HTTP 409 and PD-RESULT-002, which names the Plot and says it has data once its Report runs, after the Patch is published. It is not an access refusal. Only someone who could edit the Parable’s Plots, with the Parable edit permission and Publishing or Primary on it, can read anything else from there, such as Provider Pools. A Plot of a Platform Parable the Parable reads is read as one of its own Plots, at the Release and the Refresh the read reads. Anyone else’s SQL that names something other than the Parable’s Plots is refused with HTTP 403 and PA-AU-019 before it runs. If your role reads Provider Pools, as Provider Pools Reader, Data Access and Workspace Administrator do, run the SQL outside the Parable instead. Under View as, SQL reads only the Parable’s Plots, as the person being previewed, whoever may edit. Under View as on the Parable beside a Platform Parable’s tab, the tab’s SQL runs as you, not as the person being previewed. It still reads only the tab’s Plots and the subject.* relations, and the server builds those relations as the person being previewed. A refusal for your role, your seat or a Policy, a Pane’s included, is HTTP 403 with PA-AU-019. SQL that names a Plot whose Refresh a Report run stored is refused with HTTP 403 and QL-TN-007 when source permissions or a Provider Policy narrow what you read of a Provider table behind it: the Refresh holds rows you cannot read, and other SQL over that Plot gets the same answer. A commitId that names a Patch nobody has published yet is refused with HTTP 409 and PA-AU-028. SQL sent with a Plot read stays limited to people who could save it, as above, because it replaces the Plot’s SQL. A constructed preview and a named read both apply their Pane’s Policies and their actual inputs’ restrictions. Restrictions that must apply to every read of a Provider table belong in that table’s Provider Policy. Invalid or unsupported filters refuse execution rather than returning unfiltered rows. For a Provider table no Provider Policy narrows for the reader, simple conditions reach storage: a comparison of a column with a value, a column IN a list of values, and AND and OR of these. A condition on a partition column, such as the dt load date, lets Parable skip partitions that cannot match. A condition on any other column, such as an event time or a key, lets Parable skip files and blocks of rows whose value ranges cannot match. How much that saves depends on how the data was written. The query still applies the complete filter to the rows it reads. For a table a Provider Policy narrows for the reader, the query’s own conditions and limits do not run ahead of the Policy’s conditions, which Parable applies as it reads the table and can use to skip data. Only a direct condition qualifies: a column inside a cast or function, or a NOT IN list, skips nothing. A source read can run in several parts at once. While it runs, the source’s access is rechecked at least once a second and again as each part finishes. A part’s final check is always one that began after the part read its last rows. A revoked grant or a changed Policy fails the query before it returns a complete result; a query that streams rows may already have delivered up to a second of rows read after the change. SQL that reads another Plot’s stored rows checks them against that Plot’s output contract before any Policy, filter, limit, or aggregate applies. Where the check runs depends on what wrote the rows:
  • A Report run writes a Pipeline’s output. The run holds the Plot’s contract and checks every row against it before it writes. A run whose rows break the contract writes nothing and fails with QR-QU-011, and the Plot keeps the output of its last successful run.
  • The read of those rows makes the same check, whoever wrote them. It checks every value in each column the query reads whose contract says more than the column’s type, such as a literal, a scalar format, a bound, a required value, or a Date’s precision. This includes columns used by Policies, filters, joins, grouping and aggregation. A column the query never reads is not read from storage or checked, so COUNT(*) reads no column.
A read of a Plot’s own table or a Report run’s output skips the row check when the commit that wrote the table’s rows replaced the whole table and records the contract the read expects. It then reads only the columns the query selects, and COUNT(*) reads none. A table with no record, with a record for another contract, or whose last write kept earlier rows is checked as above. So is every read of a Report output that keeps embed() vectors beside its table: the record does not cover them. Rows written before a scalar’s rule became stricter are served unchecked until the Plot runs again. A view’s rows cross the same two boundaries as stored rows: the values of each column a query reads are checked against the view’s output contract, and the view’s Pane Policies (view.preferences) apply for the reader before any predicate of the reading query, in every Plot or Pane that reads it, directly or through other views. The view’s filter Policies apply where it is read inside a Plot they do not also cover, and its own filters wherever it is read. A filter that covers both applies to the outer Plot’s rows. A view computes only the columns a query reads. The reading query’s own filters run on the checked rows, so a filter never hides a value that breaks the contract. The reader runs the view’s SQL under their own access to its sources, as for any Plot. Reading a view’s rows does not need view.plots: its SQL never reaches the reader, and a view that fails to plan is named without its SQL. A view that so far exists only in a Patch follows its Pane’s Policies as that draft sees them. In a Report run, the run’s principal applies each view’s Policies when the run is planned, and a reader of the run’s output must still pass each view’s Policies. A Report run reads its inputs as its principal: the person who chose Refresh, or the Workspace for a scheduled run. SQL outside a Report reading that names a Pipeline reads that run’s output, so the reader must still have the run’s access to every source it read, the same check a Report reading makes. The run’s artifacts.* and workspace.* tables have no Policies, so they add nothing to this check. Before the first row, each source’s current Policies for the reader must cover the filters the run was produced under. A Policy that adds or changes a filter the run did not have, or an input the reader can no longer see, refuses the read with Report data or its current input authority is unavailable. A source filter that uses request_principal() admits only the run’s own principal. While the read runs, the check runs again at least once a second and as each part of the read ends, the same cadence as a source read. A part’s final check is always one that began after the part read its last rows. A Policy that changes during the read stops it before the query returns a complete result; a query that streams rows may already have delivered up to a second of rows read after the change. A count or an empty result is checked the same way.

Stability

A Report run is a graph of its member Plots. Capture records which members each member reads, from the Plot dependencies planned when the Parable was published, by the rules in Dependency graph. A member runs only after every member it reads succeeded, and it reads the table that member wrote in this run, at the version it committed; every other input is read as of the run’s AS OF time. One query plan of a run holds at most 50 levels, and a view counts inside the plan that reads it, so a Plot that composes many views stays under that limit by storing the steps it builds on as members. A Plot only read live is not held to the limit. Members that read each other in a cycle are refused. A member may read a view of its own Parable, which the run computes inside its own plan, and a Plot of its own Parable outside the Report. A member can also read a Pipeline of a Platform Parable, directly or through a view. Capture pins it to the newest complete Refresh of that Parable that completed by the run’s AS OF time and fits the Platform Parable’s Release that this run’s Release pinned, so a Refresh of that Parable that completes while this run waits to plan does not change what it reads. The member reads that Refresh’s table as it completed, on every plan and restart, and its output names that Refresh (Reports). Publish refuses a member that reads a Plot of any other Parable, directly or through a view (PV_REF_002). A Report run plans with the input schemas of the definition it captured. Only retention deletes a stored Report output. Each output is written once, and a read refuses an output that no longer matches what its run recorded. A historical read does not replay Provider input. A historical result whose Provider filter uses request_principal() requires the captured principal, including email. Source filters using now(), current_timestamp, current_date, full parable_context(), or another execution-dependent function refuse both Report materialization and historical reads. Use Current data for those filters. A Release never changes. A read of a stored output also checks the output against the Release whose Report run stored it. Every read still checks the reader’s seat, membership and the Pane’s Policies, before the read runs and again after it answers, with the run the reply came from. A read that expects another contract than its Plot’s is refused with a conflict.