Skip to main content
The Provider Pool contains current data from every Provider connected to a Workspace. Each Provider Plugin contributes one or more queryable tables. You can query those tables with ordinary SQL and join tables from different Providers on the same connection.

Storage and repeatability

A Provider Plugin declares each stream’s storage with the tap field store. 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.users
  • providers.github.pull_requests
  • providers.linear.issues
References are fully qualified. There is no 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_providers names each Provider the Workspace has not connected, as comma-separated connector slugs.
  • parable.warning.not_enabled_streams names each stream it has not enabled, as comma-separated <connector>.<stream> names, such as slack.replies.
A tap that declares 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:
Connected tables keep their captured field values. The same rule applies to every tap that declares unknown-field capture. Each such table carries 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, in providers 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:
Open Linear issues, ordered by their latest update:
Open GitHub pull requests in one repository:

Understand projected names

A Provider stream can project one or more tables:
  • Field names are lowercased: primaryEmail becomes primaryemail.
  • Flattened object paths use __: assignee.email becomes assignee__email.
  • Arrays can become child tables named {table}__{path}.
For example, GitHub issue labels can appear at 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:
A floating-point key of magnitude 1e16 or more does not round-trip, so its children do not join by either path; see Keys and relationships. Some streams also expose convenience views that join child rows to their parent, such as 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:
A view can fail to initialize, for example while storage is temporarily unavailable. A query that names the view then fails, but never as an unknown relation (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 the Contact.Email semantic scalar represent compatible email values. They can be joined across Providers and to workspace.users.email.
The same Provider data can join live Workspace membership:
Do not join unrelated Provider-native id columns and expect a person match.

Resolve accounts when emails differ

Directory streams can contribute to the providers.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 the providers.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 a data.unrestricted_read audit event.
  • Parable staff who have permission to query the Workspace’s data. Each read is logged.
Other Workspace members cannot read them, Data Access members included. For them, the tables are not in the catalog, in 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.

Join work, not only people

Use domain keys when they represent the same work item. A Linear issue branch can join a GitHub pull request head branch without using identity data:
See keys and relationships before relying on a logical key, and use Provider quality to inspect the health of Provider tables.