Storage and repeatability
A Provider Plugin declares each stream’s storage with the tap fieldstore.
The default, lake, stores Provider tables in Delta. Reports and scheduled
Plots pin those inputs to a Delta version.
The event_store value declares event data for a dedicated Workspace
ClickHouse instance. Its read path is under implementation. When enabled,
Reports and scheduled Plots read these tables live. A repeated run can see
new events, late arrivals, or fewer rows after retention expires.
There is no Delta copy or historical input snapshot for these streams.
Lake inputs in a mixed query remain pinned.
A storage declaration alone does not make a new table available. Use
catalog discovery
to find the tables that the Workspace can query.
Address a Provider table
{connector} is the technical connector slug exposed by the Provider Plugin,
such as google, github, or linear. {table} is the projected stream name.
Examples include:
providers.google.usersproviders.github.pull_requestsproviders.linear.issues
USE statement or search_path.
Catalog discovery lists a table only when its Provider is configured for the
Workspace and its stream is enabled and has registered. It can be listed
before the first sync produces rows. Use catalog discovery
instead of assuming that an example table exists.
Providers and streams a Workspace has not connected
A query may name a table the Workspace has no data for: a table of a Provider it has not connected, or of a stream it has not enabled on a Provider it has connected. Either reads as an empty table with the columns the Provider Plugin declares, so SQL written for several Providers and streams runs in every Workspace. The Provider’s convenience views (below) read the same way, over those empty tables. The statement’s result schema says what was missing, so a reader can show “not connected” or “not enabled” rather than zero:parable.warning.not_connected_providersnames each Provider the Workspace has not connected, as comma-separated connector slugs.parable.warning.not_enabled_streamsnames each stream it has not enabled, as comma-separated<connector>.<stream>names, such asslack.replies.
captureUnknownFields: true keeps its
raw_unknown_fields Map column in these empty tables. For example, this
query returns 0, NULL when the Workspace has not connected Workday RaaS:
parable.provider.connection: not_connected for a
Provider that is not connected, not_enabled for a stream that is off. Its
health state is not_connected either way (see Types and metadata).
A table with no rows and none of these markers is one the Workspace serves:
its stream has not synced yet, or synced no rows. Catalog discovery still
lists only the tables the Workspace serves.
A Provider counts as connected once the Workspace has added it, even before
any of its streams serves a table. A misspelled table, or a name none of the
Provider’s streams serves, still fails as an unknown table.
Only providers resolves a Provider or stream the Workspace has not connected.
In providers_quarantine and providers_quality, such a table fails as an
unknown table. Those pools hold the records a connected stream’s syncs set
aside and the quality history of its tables, so SQL meant to run in every
Workspace reads them only for tables that catalog discovery lists.
A sync that produces no new records is still a valid result, and the table
stays queryable. An empty first run returns zero rows. An empty incremental run
keeps rows from earlier runs. Rows are removed only when the stream reports a
deletion or an exhaustive result proves that the rows are absent. An empty
result does not mean that the sync failed.
Streams whose tables cannot be built
Parable builds a stream’s tables from its schema. When a stream’s schema cannot give tables, for example because the stream’s key names a field that the schema does not declare, the Workspace’s catalog leaves that stream out and serves every other table. Catalog discovery does not list the stream’s tables, child tables or views. A query that names one of those tables, inproviders or
providers_quarantine, fails with QL-TB-004. It never reads as an unknown
table or as an empty table, so a filter such as NOT IN over it cannot return
rows it should not. The message names the table, the stream, the Provider and
why its tables cannot be built. A query that does not name the stream runs as
usual. After the stream’s schema is fixed, the next catalog update serves its
tables again.
The catalog also leaves a stream out when it cannot read the stored layout
that tells it where the stream’s tables are. A query that names one of those
tables fails with QL-TB-009 (UNAVAILABLE), which names the table, the
stream and the Provider. Nothing is wrong with the stream: the catalog reads
the layout again soon, so a retry can succeed. Every other table
keeps answering.
Query one Provider
Active Google Workspace directory users:Understand projected names
A Provider stream can project one or more tables:- Field names are lowercased:
primaryEmailbecomesprimaryemail. - Flattened object paths use
__:assignee.emailbecomesassignee__email. - Arrays can become child tables named
{table}__{path}.
providers.github.issues__labels. The parent ID repeats across child rows, and
the child table has its own logical key. Do not use SELECT DISTINCT on the
parent ID to remove expected child records.
A child row’s parent_pk column holds its parent’s key as text. A string key
is copied as it is. A number or boolean key is the text CAST(key AS VARCHAR)
returns. A date key is its day count since 1970-01-01, a time key its
microseconds since midnight, and a timestamp key its microseconds since the
Unix epoch, which CAST(CAST(key AS BIGINT) AS VARCHAR) returns. The
convenience views below compare keys this way. A join you write yourself must
do the same:
providers.github.issues__labels_with_parent, and a
{table}_nested view that re-nests each child table into its parent row as an
array. A _with_parent view returns one row for each child row whose parent is
stored.
Most tables keep one row per record, and these views join on the parent ID.
Some event streams store an event again each time a load reads it, for example
when two loads’ time windows overlap. Such a stream keeps one parent row and one
set of child rows per load. Its views pair each child row with the parent row
written by the same load, matched on dt and _parable_seal_id as well as the
parent ID, so an event read by three loads returns three pairs, not nine. To
read one row per event from such a table, keep one load’s rows per parent ID:
QL-PL-002): a view whose own definition cannot be planned returns
QL-TB-003 with a message naming the view, and other causes keep their own
codes. The next query retries without a catalog refresh. GetTables, the
semantic catalog, and information_schema leave out a view whose latest
initialization failed and keep listing every other table. GetTables and the
semantic catalog try every view on each request; information_schema lists the
view again after one of those, or a query that names it, succeeds. After a
catalog update or service restart, the first information_schema.columns or
information_schema.views query can fail on a view that nothing has tried yet.
Run it again.
The Provider Plugin owns this projection. See the
Provider Plugins primer for how a custom Plugin
defines streams, schemas, and projection behavior.
Read row state
Every Provider table carries state columns beside the Provider’s own fields. The catalog marks them as system columns:
A table that holds the current state of each record, such as
providers.google.users, writes a row only when a load changes it: the load
contains the record, the record goes missing for the first time, or it is
first marked deleted. A record missing from later loads keeps the dt and
_parable_seal_id of the day it went missing. WHERE dt = '<latest day>'
therefore returns the rows that changed that day, not the whole table. Read
current state from the whole table and filter on the state columns:
Join people by semantic email
Columns marked with theContact.Email semantic scalar represent compatible
email values. They can be joined across Providers and to
workspace.users.email.
id columns and expect a person match.
Resolve accounts when emails differ
Directory streams can contribute to theproviders.identity schema. It
appears after at least one identity directory stream has synced. Until the
Workspace has enabled one, a query that names one of these tables reads it as
an empty table. In a Workspace that has added no Provider with an identity
directory stream, the result schema names identity in
parable.warning.not_connected_providers. In one that has added such a
Provider with its directory stream off, the result schema names that stream in
parable.warning.not_enabled_streams, such as google.users, since enabling
it serves the identity tables.
Associations preserve the Provider each account came from
(
connector_slug), the Provider-native account ID, its type, the match
method, match confidence, and the account’s email, name, and
photo_url. Identity
resolution is best-effort; service accounts and shared mailboxes can receive
identities too.
email and name are the account’s email and name, trimmed and
lowercased. email is a Contact.Email column: it is null when the account
has no email or its email is not valid, and an equality literal compares in
canonical form. name is null when the account has no name. Read an
identity’s emails and names here instead of joining back to each directory
table.
The Identity.UUID columns (id, identity_account_id, connector_id,
connector_tap_id, and survivor_id and merged_id of account_merges)
hold the scalar’s spelling, base62 (YQJpYwUwvbaLOwTUr4thA), as every
Provider table does. An id typed into a comparison with one of these columns
matches in either spelling, hyphenated or base62. Earlier builds wrote
hyphenated ids. A historical Refresh that reads the identity tables as of
such a build reads hyphenated ids, and a typed id does not match those rows.
A Provider table stores an Identity.UUID or Contact.Email field inside a
struct, list or map column in the same canonical form, so
owner['id'] = '<id>' matches in either spelling too. Rows that loaded
before this rule can hold such a field as the Provider sent it, until Parable
writes those rows again.
An account joins an identity only through an exact link: the same
Provider account ID, the same email, or an external user ID that names it.
A similar email, the same part
before @ on another domain, or an equal name is not a link, so two people
are never counted as one. The cost is the other way round: one person whose
accounts share no exact link is counted as two identities.
An exact Provider account ID links rows from multiple directory taps only
within the same connector and account ID type. Across Providers, accounts
join on a shared email, or through an external user ID: a directory field
that names the person’s account in another Provider.
A GitHub organization member row carries only an ID and a login, so it joins
a Linear user through the Linear user’s gitHubUserId, which equals the
member’s ID.
External user IDs join identities after every other match. Two identities
that each hold a canonical person source’s account are never joined this way.
Parable keeps both identities. An
external user ID that names no account the directories hold joins nothing.
Every method is exact, so
match_confidence is always 1.0. Earlier builds
also joined accounts by similar emails or names and recorded
email_local_part, email_fuzzy or name_fuzzy, with the similarity score
in match_confidence. A historical Refresh that reads the identity tables as
of such a build can still show those values; current builds never write them.
Identities are recomputed each time identity resolution runs. When a run
splits an identity, the part holding most of its accounts keeps its id, the
other parts get new ids, and account_merges records nothing. A result that
stored an identity id before a split can therefore name a different person
afterwards. Where a stored result needs a key that never moves, keep the
account’s connector_tap_id and account_id.
providers.identity.account_merges.run_id names the identity resolution run
that logged the merge. Rows with the same run_id come from the same run. It
is a Generic.Text column, not an ID column. A queued run writes a name such
as 0d1f7c68-8b3f-5f8f-a8e1-111111111111-3. Earlier runs and local
development runs wrote a hyphenated UUID. A literal compared with run_id
matches as plain text.
providers.identity.account.photo_url is a Network.Url column holding the
person’s profile photo as the Provider reported it. It comes from the Provider’s profile photo field (Slack profile.image_512,
Google Workspace thumbnailPhotoUrl). An identity takes its photo the way it
takes its name: from the canonical person source first, then from the
earliest-connected stream that has one. It is null when no linked account has
a photo. A Provider photo URL changes when the person changes their photo, and
each identity run reads the current one.
providers.identity.account_associations.photo_url holds each linked
account’s own photo, by the same rules. Only a field whose URL loads as an
image for a signed-out browser declares the photo role. So a Provider whose
photos need its sign-in, such as Workday, BambooHR, Salesforce or Jira Data
Center, gives no photo. To rank sources another way, or to skip a default
avatar, read every association’s photo_url instead of the identity’s.
Parable shows members their identity’s photo_url. After each identity run,
provider-transformations sends web-api one (email, photo_url) pair per email
an identity with a photo is known by. web-api stores the URL on each matching
member’s Workspace membership and clears it from members the run did not send,
so a person who leaves the directory, or a Workspace that disconnects it, loses
the photo with the next run. Parable links to the URL and never copies the
image or reads the Provider Pool to show it. In that Workspace, a photo the
person uploaded comes first, then this directory photo, then the photo from
their Google sign-in.
connector_slug is a Connector.Slug column holding the definition slug of
the Provider each account came from (google, workday);
connector_id holds the same Provider’s id. Its Provider Pool schema is that
slug in catalog form, with any - written as _, so google accounts come
from providers.google. The Provider’s display name lives in the Workspace
Pool: join workspace.providers, whose connector_slug holds the same slug.
That table has one row per connection, so take each slug once.
See who can open what
A Provider whose streams describe access, such as file sharing or channel membership, adds theproviders.access schema. Parable computes these tables
from those streams. They describe who can open each record, including records
that the person who reads the tables cannot open.
Only these people can read these tables, in SQL that names no Parable:
- People who hold
All data access
(
tenant.data.unrestricted), such as Workspace Administrators. Each read is recorded as adata.unrestricted_readaudit event. - Parable staff who have permission to query the Workspace’s data. Each read is logged.
information_schema or in the
table lists a SQL client reads, and a query that names one fails as an
unknown table. In a Parable, and under View as, nobody reads them.