Query every tenant of a storage id with *.table - #8
Merged
Merged
Conversation
`select _tenant, count(*) from customer.*.orders group by _tenant` reads every tenant of the storage id, and `*.orders` does the same for the session's storage id (in the TUI, the one being browsed). Tenants of one storage id share a schema, so one query over all of them makes sense. Every row gets a `_tenant` field with its tenant's name, usable anywhere a field is. Conditions on `_tenant` never reach the database. They pick which tenants to read from the storage id's tenant list, so `where _tenant = 'acme'` touches no other tenant. `_tenant` outside a query across tenants is refused. Planner.split/1, factored out of the GROUP BY planner, separates what each tenant reads (the filtering) from what runs over all the rows afterwards (grouping, ordering, limit, projection). Efsql.Fanout plans each tenant's read separately, because a tenant not opened since a deploy can lack a newly migrated index. The executor reads every tenant in a single FoundationDB transaction on the database, which ecto_foundationdb now supports: each tenant's async reads are started and then awaited together, so they are pipelined and see one consistent snapshot. The retry limit is 2, so a read that can't finish within the 5-second transaction limit fails with a message saying how to narrow it instead of retrying forever. At most 100 tenants are read by default, adjustable with `:max_tenants`. The TUI session also accepts tenant-qualified queries when no tenant is active. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PFBZUrerge6bbWVCHpS5Ko
transaction_too_old is retryable, so erlfdb's transactional loop retries it. A read too big for one transaction would run again, just as slowly, and fail the same way. The retry limit of 2 still meant three full reads before the error, and it also capped retries for genuinely transient errors. The reads now run in a function that rescues transaction_too_old (and transaction_timed_out) inside the transaction and re-raises it as Unsupported. erlfdb's loop only retries erlfdb errors, so the query fails on its first attempt, and other errors keep FoundationDB's normal retries. The retry limit is gone. A new test opens a transaction with an ancient read version, which forces transaction_too_old, runs a cross-tenant query inside it, and checks that the transaction function ran only once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PFBZUrerge6bbWVCHpS5Ko
A query across tenants reads them all in one FoundationDB transaction,
which has to finish within five seconds. With `\set tenant_batch 25` in
the CLI or TUI, or `tenant_batch: 25` for Efsql.qall/3, the tenants are
read 25 at a time, one transaction per batch, and the tenant limit no
longer applies. The plan's access node becomes {:batches, [fan_out]}, and
the executor runs the batches in turn. Grouping, ordering and the limit
still apply to all the rows together.
Batching is opt-in because the result is no longer one snapshot: each
batch sees the database at a different moment. The CLI and TUI footers
say how many transactions a result came from (Fanout.transactions/1).
The too-many-tenants and too-big-for-one-transaction errors now point to
the setting.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PFBZUrerge6bbWVCHpS5Ko
The count already says the result spans several snapshots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PFBZUrerge6bbWVCHpS5Ko
With tenant_batch set, up to four batches are now read at once. Each
batch runs in its own process and transaction, and results come back in
batch order. The number is configurable with `batch_concurrency:` or the
`:efsql, :batch_concurrency` setting. The batches node carries it:
{:batches, batches, concurrency}.
Executor.map_concurrently/3 runs the batches. The first failure stops
the remaining work and is re-raised in the caller as the original
exception, throw or exit. A plain Task.async_stream would turn it into a
linked exit, which the CLI's rescue can't catch, so a "too big" batch
would have crashed the session instead of printing its message. With a
concurrency of 1, everything runs in the calling process as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PFBZUrerge6bbWVCHpS5Ko
The executor adds _tenant to every row of a query across tenants, and
nothing removed it when the select list didn't name it. So
`select name from *.users` returned _tenant as well. Fanout now appends
a projection for ungrouped queries that don't select it. Grouped queries
only keep _tenant as a group field, which split/1 already projects away.
Also matches %Logical.Select{} on three struct updates that Elixir 1.19's
type checker warned about: Planner.rows/2, Fanout.plan/3 and
Parser.grouped/2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PFBZUrerge6bbWVCHpS5Ko
Elixir 1.19 no longer compiles .yrl files by default and warns that the project must list :yecc. It still compiled the grammar, but with a warning on every build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PFBZUrerge6bbWVCHpS5Ko
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
select _tenant, count(*) from customer.*.orders group by _tenantreadsevery tenant of the storage id, and
*.ordersdoes the same for thesession's storage id (in the TUI, the one being browsed). Tenants of one
storage id share a schema, so one query over all of them makes sense.
Every row gets a
_tenantfield with its tenant's name, usable anywherea field is. Conditions on
_tenantnever reach the database. They pickwhich tenants to read from the storage id's tenant list, so
where _tenant = 'acme'touches no other tenant._tenantoutside aquery across tenants is refused.
Planner.split/1, factored out of the GROUP BY planner, separates what
each tenant reads (the filtering) from what runs over all the rows
afterwards (grouping, ordering, limit, projection). Efsql.Fanout plans
each tenant's read separately, because a tenant not opened since a deploy
can lack a newly migrated index. The executor reads every tenant in a
single FoundationDB transaction on the database, which ecto_foundationdb
now supports: each tenant's async reads are started and then awaited
together, so they are pipelined and see one consistent snapshot. The
retry limit is 2, so a read that can't finish within the 5-second
transaction limit fails with a message saying how to narrow it instead
of retrying forever. At most 100 tenants are read by default, adjustable
with
:max_tenants.The TUI session also accepts tenant-qualified queries when no tenant is
active.
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01PFBZUrerge6bbWVCHpS5Ko