## What
PR 6 of the SQL-editor "query source (Database vs Logs)" stack (builds
on the merged PR 5, #48414). Adds the user-facing toolbar surface for
the logs query source and consolidates the SQL-editor toolbar into a
single **source menu**.
Everything stays behind `sqlEditorLogsSource` + `otelLegacyLogs`
(dual-flag gated); with the flags off the toolbar is unchanged.
## Changes
- **Consolidated source menu** (`QuerySourceMenu`) — one `Database ▾` /
`Logs ▾` dropdown that both labels the snippet's source and hosts the
source-specific controls as flyout submenus:
- Database: database selector (`Primary` / read replicas), `Run as`
(role impersonation), and `Row limit`.
- Logs: `Time range` — the same relative presets as the Logs Explorer
plus a `Custom range…` calendar dialog.
- **Source is immutable** — the Database/Logs rows aren't a toggle. An
existing (materialized) snippet opens a *fresh* tab of the target source
(never reinterpreting a query against the wrong backend); a blank new
tab re-flavors in place. Extracted as the pure, unit-tested
`resolveSourceSwitch`.
- **New-snippet-with-source** threaded through `/sql/new?source=`, the
nav "Create a new logs query" entry, and the duplicate flow. Logs
snippets hide the (db-dialect) Export action.
- **Run-affordance guard** — the Run button is disabled + annotated for
a logs snippet on a non-ClickHouse org (sits above PR 5's execution
short-circuit).
- **Retention entitlement gating** — both preset and custom logs ranges
past `log.retention_days` surface the upgrade prompt instead of applying
silently. Prettify is disabled for logs (sql-formatter mangles
ClickHouse).
## Tests
- `querySource.test.ts` — `logDateRangesEqual` (structural
relative/absolute matching, incl. the "Last hour" vs "Last 1 hour" label
case).
- `QuerySourceMenu.utils.test.ts` — `resolveSourceSwitch`
push-vs-replace / no-op behavior.
`pnpm --filter studio typecheck` · `lint:ratchet` · Prettier · SQL
editor suite (307 tests) all green.
## For reviewers
To test manually, enable the `sqlEditorLogsSource` feature flag for
yourself on local/staging. There is no nav for Log SQL snippets
currently (that is by design, this PR is big enough as-is), so to check
an existing logs snippet, you can create one using the existing Logs
Explorer, copy its UUID, and force navigate to that snippet in the SQL
editor via URL.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added support for creating and switching between database and logs
queries.
* Added log time-range presets and custom date-range selection.
* Added database, run-as role, and row-limit controls.
* Added read-replica selection, including options to create a new
replica when available.
* **Improvements**
* Added clearer explanations when query execution is unavailable.
* Disabled SQL formatting and query export where unsupported for logs
queries.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
Stacked on #48305.
## What
PR 3 of the stacked SQL-editor query-source series (Database vs Logs).
Stacked on the PR 2 branch `charislam/log-sql-content-shape`.
Turns `SnippetWithContent` into a discriminated union on `type` and
makes all snippet writes source-aware:
- `data/content/sql-folders-query.ts`: `SnippetWithContent` is now `{
type: 'sql'; content?: SqlSnippets.Content } | { type: 'log_sql';
content?: LogSqlSnippets.Content } | { type: 'report'; content?: never
}`. `report` is kept (the content endpoints' wire type carries it) but
has no SQL content — its body is `Dashboards.Content`, loaded through
the separate `Content` union.
- `setSql` brands per type (`untrustedLogSql` vs `untrustedSql`).
- `buildUpsertPayload` persists `snippet.type` (no longer hardcoded
`'sql'`).
- `createSqlSnippetSkeletonV2({ source })` emits the matching type +
content shape with the `as any` cast removed.
- New `components/interfaces/SQLEditor/querySource.ts`:
`SqlSnippetSource` + `getSnippetSource`.
- `seedSnippet` test helper gains a `source` arg.
- New `remapWireSnippet` boundary helper in `content-remap.ts`
concentrates the single wire->domain assertion, so `content-id-query` /
`content-upsert-mutation` call sites are cast-free (no `as unknown as`).
- Collateral: query result types aligned to the union; `updateSnippet`
no longer accepts `type` (source is immutable); db-only editor read
paths narrow away `log_sql`.
## Why
Impossible-states-impossible typing: a snippet's brand follows its
content type, so logs SQL and database SQL can never cross execution
paths. No behavior change for existing database snippets.
## Testing
- \`pnpm typecheck\` — clean
- \`pnpm --filter studio run lint:ratchet\` — no new warnings
- \`pnpm test:studio\` (data/content, SQLEditor, state/sql-editor) —
passing, including new tests for \`getSnippetSource\`, source-aware
\`setSql\`, type-aware \`buildUpsertPayload\`, and both skeleton shapes.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added source-aware creation for SQL editor snippets, including
log-based SQL snippets.
* Introduced backend source mapping so log snippets are treated as
log_sql.
* **Bug Fixes**
* Improved SQL retrieval/prettification so log snippets no longer use
the wrong fallback content.
* Ensured log snippets are sanitized and preserve correct type, content,
identifiers, and statuses during save/upsert flows.
* **Tests**
* Expanded unit and integration coverage for log snippet creation,
source mapping, editing, prettification, and upsert payloads.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
Step 2 of the SQL Editor testability plan.
`SQLEditorContext` already wrapped the Monaco refs and exposed a few
semantic imperative helpers (`getEditorSql`, `clearHighlights`,
`applyErrorHighlight`, `refocusEditor`, …). This finishes that
abstraction so no hook or controller touches
`editorRef.current`/`diffEditorRef.current` directly anymore — they only
call the port. The port is what will let Step 3's test harness inject a
real in-memory editor adapter instead of mocking Monaco; production
wires it to the real Monaco refs, unchanged.
- Extends the context value with two semantic controllers, backed by the
existing refs:
- `editor: EditorController` — `isReady`, `getValue`,
`getSelectionStartLine`, `getSql` (today's `getEditorSql`), `replaceAll`
(wraps the repeated `executeEdits(...)` pattern), `focus`,
`revealLineInCenter`, `highlightErrorLine` (today's
`applyErrorHighlight`), `clearHighlights`.
- `diff: DiffController` — `isMounted`, `getModifiedValue`, `setDiff`
(the diff-sync effect body), `attach` (today's `handleDiffEditorMount`).
- Migrates every touch point off raw refs onto the port:
`useSqlEditorExecution`, `usePrettifyQuery`, `useSqlEditorShortcuts`,
`SQLEditorControllers`' `readEditorSql`, and `useSqlEditorAi`'s
`acceptAiHandler`/`drainDiffRequest`/`handleDiffEditorMount`/diff-sync
effect.
- `SQLEditorEditorPanel.tsx` is intentionally left untouched — it wires
the raw refs into the real Monaco/DiffEditor React components for
rendering, which isn't decision logic to abstract.
Behavior-preserving.
## Test plan
- [x] `pnpm --filter studio typecheck`
- [x] `pnpm test:studio -- SQLEditor` (265 tests passing)
- [x] `pnpm --filter studio run lint:ratchet`
## Summary
Continues the SQLEditor decomposition. Pulls four cohesive concerns out
of the `SQLEditorContent` composition root into co-located `use*` hooks.
## New hooks
- `useSnippetIdentity` — derives `id` / `generatedNewSnippetName` /
`isLoading` from the URL + snippet store (keeps the `[urlId]` memo dep
verbatim).
- `useEditorMount` — the editor `onMount` (scroll restore/track) + the
mount counter that lets a pre-mount diff request re-run.
- `usePrettifyQuery` — formats the editor SQL in place and writes it
back to the store.
- `useSnippetTitleGenerator` — the title-generation mutation +
`setAiTitle`.