mirror of
https://github.com/supabase/supabase.git
synced 2026-09-08 19:08:44 +08:00
## Context Related to Explorer/Notebook - builds on the ExplorerQuerySourceMenu by adding an option for row limit in both Notebook Query cell + Query Tab ## Side note RE persistence of the selected row limit value Note that for QueryTab - its intentional that for now that the row limit isn't scoped to the query draft atm as I wanna avoid making changes to `explorer-query` atm as there's a couple of PRs in flux that touches that file. So will handle that separately ^ This means that switching between query tabs will not change nor persist the row limit <img width="931" height="335" alt="image" src="https://github.com/user-attachments/assets/d1d52ee7-7c1d-42aa-a6ae-1d7d99ab95c9" /> <img width="1381" height="486" alt="image" src="https://github.com/user-attachments/assets/689368c9-c0fc-4000-a09e-f59bfa7afa97" /> ## To test - [ ] Verify that row limit behaviour works in notebooks - [ ] Verify that row limit behaviour works in explorer query tab
152 lines
5.2 KiB
TypeScript
152 lines
5.2 KiB
TypeScript
import { untrustedSql } from '@supabase/pg-meta'
|
|
import { type Snapshot } from 'valtio'
|
|
|
|
import { type ExplorerQueryModel } from '../QueryEditor'
|
|
import { type QueryDisplay } from '../types'
|
|
import { type ChartConfig, type QueryCell } from '@/data/content/notebooks/notebook-schema'
|
|
import { untrustedLogSql } from '@/data/logs/safe-analytics-sql'
|
|
import {
|
|
getQuerySourceBinding,
|
|
type QuerySourceBinding,
|
|
} from '@/data/query-sources/query-source-registry'
|
|
|
|
/** Row limit a database cell starts with when it has no saved one to carry over. */
|
|
export const DEFAULT_CELL_ROW_LIMIT = 100
|
|
|
|
/**
|
|
* Valtio snapshots are deep-readonly. Readonly properties assign to mutable ones, so only
|
|
* the array needs rebuilding to turn a snapshot's chart back into a writable config.
|
|
*/
|
|
type ReadonlyChartConfig = Omit<ChartConfig, 'y_columns'> & {
|
|
readonly y_columns: readonly string[]
|
|
}
|
|
|
|
export const cloneChartConfig = (
|
|
chart: ReadonlyChartConfig | undefined
|
|
): ChartConfig | undefined => (chart ? { ...chart, y_columns: [...chart.y_columns] } : undefined)
|
|
|
|
/** The display state a query cell hands the shared editor. */
|
|
// `view` is already defaulted to 'table' by the domain transform, so there is nothing to
|
|
// fall back to here — only the chart needs copying out of the snapshot.
|
|
export const getCellDisplay = (cell: Snapshot<QueryCell>): QueryDisplay => ({
|
|
view: cell.view,
|
|
chart: cloneChartConfig(cell.chart),
|
|
})
|
|
|
|
/** Fields every query cell carries, copied out of a snapshot so the result is writable. */
|
|
const copyQueryCellBase = (cell: Snapshot<QueryCell>) => ({
|
|
id: cell.id,
|
|
title: cell.title,
|
|
view: cell.view,
|
|
chart: cloneChartConfig(cell.chart),
|
|
})
|
|
|
|
/** A writable copy of a query cell, preserving its backend and every backend-specific field. */
|
|
export const cloneQueryCell = (cell: Snapshot<QueryCell>): QueryCell =>
|
|
cell._tag === 'log_cell'
|
|
? {
|
|
...copyQueryCellBase(cell),
|
|
_tag: 'log_cell',
|
|
unchecked_sql: cell.unchecked_sql,
|
|
time_range: cell.time_range,
|
|
}
|
|
: {
|
|
...copyQueryCellBase(cell),
|
|
_tag: 'database_cell',
|
|
unchecked_sql: cell.unchecked_sql,
|
|
row_limit: cell.row_limit,
|
|
database_identifier: cell.database_identifier,
|
|
}
|
|
|
|
/**
|
|
* Applies a source binding to a query cell, carrying the query text across unchanged and
|
|
* rebranding it for the new backend's dialect.
|
|
*
|
|
* NOTE — carrying the text over is very likely not what a user wants when the backend
|
|
* actually changes. Postgres SQL and logs SQL are separate dialects over separate schemas,
|
|
* so a carried-over query will almost always fail to run, and the rebrand asserts a
|
|
* dialect the text was never written in. We keep it for now because it is the
|
|
* least-destructive option and needs no confirmation prompt; revisit once we know whether
|
|
* people switch source to port an existing query or to start a fresh one, at which point
|
|
* clearing the body (behind a confirmation) is the likely answer.
|
|
*/
|
|
export function changeCellSource(cell: Snapshot<QueryCell>, source: QuerySourceBinding): QueryCell {
|
|
const base = copyQueryCellBase(cell)
|
|
|
|
if (source._tag === 'logs') {
|
|
return {
|
|
...base,
|
|
_tag: 'log_cell',
|
|
unchecked_sql: untrustedLogSql(cell.unchecked_sql),
|
|
time_range: source.time_range,
|
|
}
|
|
}
|
|
|
|
return {
|
|
...base,
|
|
_tag: 'database_cell',
|
|
unchecked_sql: untrustedSql(cell.unchecked_sql),
|
|
row_limit: cell._tag === 'database_cell' ? cell.row_limit : DEFAULT_CELL_ROW_LIMIT,
|
|
database_identifier: source.database_identifier,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Writes the editor's text back onto a cell, branded for that cell's dialect. Separate
|
|
* from `cloneQueryCell` so the brand stays correlated with the cell tag in one narrowing
|
|
* rather than being re-derived at each call site.
|
|
*/
|
|
export function setCellSql(cell: Snapshot<QueryCell>, sql: string): QueryCell {
|
|
const base = copyQueryCellBase(cell)
|
|
|
|
if (cell._tag === 'log_cell') {
|
|
return {
|
|
...base,
|
|
_tag: 'log_cell',
|
|
unchecked_sql: untrustedLogSql(sql),
|
|
time_range: cell.time_range,
|
|
}
|
|
}
|
|
|
|
return {
|
|
...base,
|
|
_tag: 'database_cell',
|
|
unchecked_sql: untrustedSql(sql),
|
|
row_limit: cell.row_limit,
|
|
database_identifier: cell.database_identifier,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Writes a new row limit onto a database cell. A log cell has no row limit concept, so it
|
|
* passes through unchanged.
|
|
*/
|
|
export function setCellRowLimit(cell: Snapshot<QueryCell>, rowLimit: number): QueryCell {
|
|
if (cell._tag === 'log_cell') return cloneQueryCell(cell)
|
|
|
|
return {
|
|
...copyQueryCellBase(cell),
|
|
_tag: 'database_cell',
|
|
unchecked_sql: cell.unchecked_sql,
|
|
row_limit: rowLimit,
|
|
database_identifier: cell.database_identifier,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds the editor's query model from a cell and the editor's live text buffer. Branding
|
|
* the buffer is the editor boundary the safe-SQL model expects; which brand applies is
|
|
* decided by the cell's tag, so the dialect can't drift from the cell it belongs to.
|
|
*/
|
|
export function toQueryModel(cell: Snapshot<QueryCell>, sql: string): ExplorerQueryModel {
|
|
if (cell._tag === 'log_cell') {
|
|
return { ...getQuerySourceBinding(cell), uncheckedSql: untrustedLogSql(sql) }
|
|
}
|
|
|
|
return {
|
|
...getQuerySourceBinding(cell),
|
|
uncheckedSql: untrustedSql(sql),
|
|
rowLimit: cell.row_limit,
|
|
}
|
|
}
|