From 96f625f2cd49a3e1415fd1a9529baa6ad9ea62fc Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 6 Jul 2022 13:08:37 +0800 Subject: [PATCH] Add truncate method to grid to support deleting all rows from a table if no filter provided --- studio/components/grid/SupabaseGrid.tsx | 8 +- .../components/grid/components/grid/Grid.tsx | 1 + .../grid/components/header/Header.tsx | 5 +- studio/components/grid/query/Query.utils.ts | 169 +++++++++--------- studio/components/grid/query/QueryAction.ts | 38 ++-- studio/components/grid/query/QueryFilter.ts | 2 +- studio/components/grid/query/QueryModifier.ts | 52 +++--- .../grid/services/row/SqlRowService.ts | 10 +- studio/components/grid/services/row/index.tsx | 2 + .../TableEditorLayout/TableEditorMenu.tsx | 2 +- 10 files changed, 155 insertions(+), 134 deletions(-) diff --git a/studio/components/grid/SupabaseGrid.tsx b/studio/components/grid/SupabaseGrid.tsx index 4ce0310a3a4..cc0f3a40cf4 100644 --- a/studio/components/grid/SupabaseGrid.tsx +++ b/studio/components/grid/SupabaseGrid.tsx @@ -147,10 +147,10 @@ const SupabaseGridLayout = forwardRef((props if ( !state.table || - (typeof props.table == 'string' && - state.table!.name != props.table && - state.table!.schema != props.schema) || - (typeof props.table != 'string' && + (typeof props.table === 'string' && + state.table!.name !== props.table && + state.table!.schema !== props.schema) || + (typeof props.table !== 'string' && JSON.stringify(props.table) !== JSON.stringify(state.table)) ) { const { savedState } = initTable(props, state, dispatch, sort as string[], filter as string[]) diff --git a/studio/components/grid/components/grid/Grid.tsx b/studio/components/grid/components/grid/Grid.tsx index 7934075e5a6..5f27e85ceaa 100644 --- a/studio/components/grid/components/grid/Grid.tsx +++ b/studio/components/grid/components/grid/Grid.tsx @@ -15,6 +15,7 @@ interface IGrid extends GridProps { rows: any[] } +// [Joshen] Just for visibility this is causing some hook errors in the browser export const Grid = memo( forwardRef( ( diff --git a/studio/components/grid/components/header/Header.tsx b/studio/components/grid/components/header/Header.tsx index b284c6a875d..40edf05122a 100644 --- a/studio/components/grid/components/header/Header.tsx +++ b/studio/components/grid/components/header/Header.tsx @@ -110,7 +110,10 @@ const RowHeader: FC = ({ sorts, filters }) => { message: 'Are you sure you want to delete the selected rows? This action cannot be undone.', onAsyncConfirm: async () => { if (allRowsSelected) { - const { error } = await state.rowService!.deleteAll(filters) + const { error } = + filters.length === 0 + ? await state.rowService!.truncate() + : await state.rowService!.deleteAll(filters) if (error) { if (state.onError) state.onError(error) } else { diff --git a/studio/components/grid/query/Query.utils.ts b/studio/components/grid/query/Query.utils.ts index cca3a2dbaf2..e838093167e 100644 --- a/studio/components/grid/query/Query.utils.ts +++ b/studio/components/grid/query/Query.utils.ts @@ -1,137 +1,146 @@ -import { ident, literal, format } from '@scaleleap/pg-format'; -import { - Dictionary, - Filter, - QueryPagination, - QueryTable, - Sort, -} from '../types'; +import { ident, literal, format } from '@scaleleap/pg-format' +import { Dictionary, Filter, QueryPagination, QueryTable, Sort } from '../types' export function countQuery( table: QueryTable, options?: { - filters?: Filter[]; + filters?: Filter[] } ) { - let query = `select count(*) from ${queryTable(table)}`; - const { filters } = options ?? {}; + let query = `select count(*) from ${queryTable(table)}` + const { filters } = options ?? {} if (filters) { - query = applyFilters(query, filters); + query = applyFilters(query, filters) } - return query + ';'; + return query + ';' +} + +export function truncateQuery( + table: QueryTable, + options?: { + // [Joshen] yet to implement cascade from UI, just adding first + cascade?: boolean + } +) { + let query = `truncate ${queryTable(table)}` + const { cascade } = options ?? {} + if (cascade) { + query += ' cascade' + } + return query + ';' } export function deleteQuery( table: QueryTable, filters?: Filter[], options?: { - returning?: boolean; + returning?: boolean } ) { - if (!filters || filters.length == 0) { - throw { message: 'no filters for this delete query' }; + if (!filters || filters.length === 0) { + throw { message: 'no filters for this delete query' } } - let query = `delete from ${queryTable(table)}`; - const { returning } = options ?? {}; + let query = `delete from ${queryTable(table)}` + const { returning } = options ?? {} if (filters) { - query = applyFilters(query, filters); + query = applyFilters(query, filters) } if (returning) { - query += ' returning *'; + query += ' returning *' } - return query + ';'; + return query + ';' } export function insertQuery( table: QueryTable, values: Dictionary[], options?: { - returning?: boolean; + returning?: boolean } ) { - if (!values || values.length == 0) { - throw { message: 'no value to insert' }; + if (!values || values.length === 0) { + throw { message: 'no value to insert' } } - const { returning } = options ?? {}; + const { returning } = options ?? {} const queryColumns = Object.keys(values[0]) .map((x) => ident(x)) - .join(','); - let query = ''; + .join(',') + let query = '' if (queryColumns.length == 0) { query = format( 'insert into %1$s select from jsonb_populate_recordset(null::%1$s, %2$s)', queryTable(table), literal(JSON.stringify(values)) - ); + ) } else { query = format( 'insert into %1$s (%2$s) select %2$s from jsonb_populate_recordset(null::%1$s, %3$s)', queryTable(table), queryColumns, literal(JSON.stringify(values)) - ); + ) } if (returning) { - query += ' returning *'; + query += ' returning *' } - return query + ';'; + return query + ';' } export function selectQuery( table: QueryTable, columns?: string[], options?: { - filters?: Filter[]; - pagination?: QueryPagination; - sorts?: Sort[]; + filters?: Filter[] + pagination?: QueryPagination + sorts?: Sort[] } ) { - let query = ''; - const queryColumn = columns?.map((x) => ident(x)).join(', ') ?? '*'; - query += `select ${queryColumn} from ${queryTable(table)}`; + let query = '' + const queryColumn = columns?.map((x) => ident(x)).join(', ') ?? '*' + query += `select ${queryColumn} from ${queryTable(table)}` - const { filters, pagination, sorts } = options ?? {}; + const { filters, pagination, sorts } = options ?? {} if (filters) { - query = applyFilters(query, filters); + query = applyFilters(query, filters) } if (sorts) { - query = applySorts(query, sorts); + query = applySorts(query, sorts) } if (pagination) { - const { limit, offset } = pagination ?? {}; - query += ` limit ${literal(limit)} offset ${literal(offset)}`; + const { limit, offset } = pagination ?? {} + query += ` limit ${literal(limit)} offset ${literal(offset)}` } - return query + ';'; + return query + ';' } export function updateQuery( table: QueryTable, value: Dictionary, options?: { - filters?: Filter[]; - returning?: boolean; + filters?: Filter[] + returning?: boolean } ) { - const { filters, returning } = options ?? {}; - if (!filters || filters.length == 0) { - throw { message: 'no filters for this update query' }; + const { filters, returning } = options ?? {} + if (!filters || filters.length === 0) { + throw { message: 'no filters for this update query' } } const queryColumns = Object.keys(value) .map((x) => ident(x)) - .join(','); + .join(',') let query = format( 'update %1$s set (%2$s) = (select %2$s from json_populate_record(null::%1$s, %3$s))', queryTable(table), queryColumns, literal(JSON.stringify(value)) - ); + ) if (filters) { - query = applyFilters(query, filters); + query = applyFilters(query, filters) } if (returning) { - query += ' returning *'; + query += ' returning *' } - return query + ';'; + return query + ';' } //============================================================ @@ -139,60 +148,56 @@ export function updateQuery( //============================================================ function applyFilters(query: string, filters: Filter[]) { - if (filters.length == 0) return query; + if (filters.length === 0) return query query += ` where ${filters .map((filter) => { switch (filter.operator) { case 'in': - return inFilterSql(filter); + return inFilterSql(filter) case 'is': - return isFilterSql(filter); + return isFilterSql(filter) default: - return `${ident(filter.column)} ${filter.operator} ${filterLiteral( - filter.value - )}`; + return `${ident(filter.column)} ${filter.operator} ${filterLiteral(filter.value)}` } }) - .join(' and ')}`; - return query; + .join(' and ')}` + return query } function inFilterSql(filter: Filter) { - let values; + let values if (Array.isArray(filter.value)) { - values = filter.value.map((x: any) => filterLiteral(x)); + values = filter.value.map((x: any) => filterLiteral(x)) } else { - const filterValueTxt = String(filter.value); - values = filterValueTxt.split(',').map((x: any) => filterLiteral(x)); + const filterValueTxt = String(filter.value) + values = filterValueTxt.split(',').map((x: any) => filterLiteral(x)) } - return `${ident(filter.column)} ${filter.operator} (${values.join(',')})`; + return `${ident(filter.column)} ${filter.operator} (${values.join(',')})` } function isFilterSql(filter: Filter) { - const filterValueTxt = String(filter.value); + const filterValueTxt = String(filter.value) switch (filterValueTxt) { case 'null': case 'false': case 'true': case 'not null': - return `${ident(filter.column)} ${filter.operator} ${filterValueTxt}`; + return `${ident(filter.column)} ${filter.operator} ${filterValueTxt}` default: - return `${ident(filter.column)} ${filter.operator} ${filterLiteral( - filter.value - )}`; + return `${ident(filter.column)} ${filter.operator} ${filterLiteral(filter.value)}` } } function filterLiteral(value: any) { if (typeof value === 'string') { - const temp = value.trim(); + const temp = value.trim() if (temp?.startsWith('ARRAY[') && temp?.endsWith(']')) { - return temp; + return temp } else { - return literal(temp); + return literal(temp) } } - return value; + return value } //============================================================ @@ -200,15 +205,15 @@ function filterLiteral(value: any) { //============================================================ function applySorts(query: string, sorts: Sort[]) { - if (sorts.length == 0) return query; + if (sorts.length === 0) return query query += ` order by ${sorts .map((x) => { - const order = x.ascending ? 'asc' : 'desc'; - const nullOrder = x.nullsFirst ? 'nulls first' : 'nulls last'; - return `${ident(x.column)} ${order} ${nullOrder}`; + const order = x.ascending ? 'asc' : 'desc' + const nullOrder = x.nullsFirst ? 'nulls first' : 'nulls last' + return `${ident(x.column)} ${order} ${nullOrder}` }) - .join(', ')}`; - return query; + .join(', ')}` + return query } //============================================================ @@ -216,5 +221,5 @@ function applySorts(query: string, sorts: Sort[]) { //============================================================ function queryTable(table: QueryTable) { - return `${ident(table.schema)}.${ident(table.name)}`; + return `${ident(table.schema)}.${ident(table.name)}` } diff --git a/studio/components/grid/query/QueryAction.ts b/studio/components/grid/query/QueryAction.ts index 663a0346323..5523c37bd33 100644 --- a/studio/components/grid/query/QueryAction.ts +++ b/studio/components/grid/query/QueryAction.ts @@ -1,18 +1,13 @@ -import { Dictionary, QueryTable } from '../types'; -import { IQueryFilter, QueryFilter } from './QueryFilter'; +import { Dictionary, QueryTable } from '../types' +import { IQueryFilter, QueryFilter } from './QueryFilter' export interface IQueryAction { - count: () => IQueryFilter; - delete: (options?: { returning: boolean }) => IQueryFilter; - insert: ( - values: Dictionary[], - options?: { returning: boolean } - ) => IQueryFilter; - select: (columns?: string[]) => IQueryFilter; - update: ( - value: Dictionary, - options?: { returning: boolean } - ) => IQueryFilter; + count: () => IQueryFilter + delete: (options?: { returning: boolean }) => IQueryFilter + insert: (values: Dictionary[], options?: { returning: boolean }) => IQueryFilter + select: (columns?: string[]) => IQueryFilter + update: (value: Dictionary, options?: { returning: boolean }) => IQueryFilter + truncate: (options?: { returning: boolean }) => IQueryFilter } export class QueryAction implements IQueryAction { @@ -22,7 +17,7 @@ export class QueryAction implements IQueryAction { * Performs a COUNT on the table. */ count() { - return new QueryFilter(this.table, 'count'); + return new QueryFilter(this.table, 'count') } /** @@ -31,7 +26,7 @@ export class QueryAction implements IQueryAction { * @param options.returning If `true`, return the deleted row(s) in the response. */ delete(options?: { returning: boolean }) { - return new QueryFilter(this.table, 'delete', undefined, options); + return new QueryFilter(this.table, 'delete', undefined, options) } /** @@ -41,7 +36,7 @@ export class QueryAction implements IQueryAction { * @param options.returning If `true`, return the inserted row(s) in the response. */ insert(values: Dictionary[], options?: { returning: boolean }) { - return new QueryFilter(this.table, 'insert', values, options); + return new QueryFilter(this.table, 'insert', values, options) } /** @@ -50,7 +45,7 @@ export class QueryAction implements IQueryAction { * @param columns the query columns, by default set to '*'. */ select(columns?: string[]) { - return new QueryFilter(this.table, 'select', columns); + return new QueryFilter(this.table, 'select', columns) } /** @@ -60,6 +55,13 @@ export class QueryAction implements IQueryAction { * @param options.returning If `true`, return the updated row(s) in the response. */ update(value: Dictionary, options?: { returning: boolean }) { - return new QueryFilter(this.table, 'update', value, options); + return new QueryFilter(this.table, 'update', value, options) + } + + /** + * Performs a TRUNCATE on the table + */ + truncate(options?: { returning: boolean }) { + return new QueryFilter(this.table, 'truncate', undefined, options) } } diff --git a/studio/components/grid/query/QueryFilter.ts b/studio/components/grid/query/QueryFilter.ts index 27557599615..30eb6289110 100644 --- a/studio/components/grid/query/QueryFilter.ts +++ b/studio/components/grid/query/QueryFilter.ts @@ -13,7 +13,7 @@ export class QueryFilter implements IQueryFilter, IQueryModifier { constructor( protected table: QueryTable, - protected action: 'count' | 'delete' | 'insert' | 'select' | 'update', + protected action: 'count' | 'delete' | 'insert' | 'select' | 'update' | 'truncate', protected actionValue?: string[] | Dictionary | Dictionary[], protected actionOptions?: { returning: boolean } ) {} diff --git a/studio/components/grid/query/QueryModifier.ts b/studio/components/grid/query/QueryModifier.ts index bec0a75c43b..a9aafe0744f 100644 --- a/studio/components/grid/query/QueryModifier.ts +++ b/studio/components/grid/query/QueryModifier.ts @@ -1,34 +1,29 @@ -import { - Dictionary, - Filter, - QueryPagination, - QueryTable, - Sort, -} from '../types'; +import { Dictionary, Filter, QueryPagination, QueryTable, Sort } from '../types' import { countQuery, deleteQuery, insertQuery, selectQuery, + truncateQuery, updateQuery, -} from './Query.utils'; +} from './Query.utils' export interface IQueryModifier { - range: (from: number, to: number) => QueryModifier; - toSql: () => string; + range: (from: number, to: number) => QueryModifier + toSql: () => string } export class QueryModifier implements IQueryModifier { - protected pagination?: QueryPagination; + protected pagination?: QueryPagination constructor( protected table: QueryTable, - protected action: 'count' | 'delete' | 'insert' | 'select' | 'update', + protected action: 'count' | 'delete' | 'insert' | 'select' | 'update' | 'truncate', protected options?: { - actionValue?: string[] | Dictionary | Dictionary[]; - actionOptions?: { returning: boolean }; - filters?: Filter[]; - sorts?: Sort[]; + actionValue?: string[] | Dictionary | Dictionary[] + actionOptions?: { returning?: boolean; cascade?: boolean } + filters?: Filter[] + sorts?: Sort[] } ) {} @@ -39,8 +34,8 @@ export class QueryModifier implements IQueryModifier { * @param to The last index to which to limit the result, inclusive. */ range(from: number, to: number) { - this.pagination = { offset: from, limit: to - from + 1 }; - return this; + this.pagination = { offset: from, limit: to - from + 1 } + return this } /** @@ -48,40 +43,45 @@ export class QueryModifier implements IQueryModifier { */ toSql() { try { - const { actionValue, actionOptions, filters, sorts } = this.options ?? {}; + const { actionValue, actionOptions, filters, sorts } = this.options ?? {} switch (this.action) { case 'count': { - return countQuery(this.table, { filters }); + return countQuery(this.table, { filters }) } case 'delete': { return deleteQuery(this.table, filters, { returning: actionOptions?.returning, - }); + }) } case 'insert': { return insertQuery(this.table, actionValue as Dictionary[], { returning: actionOptions?.returning, - }); + }) } case 'select': { return selectQuery(this.table, actionValue as string[] | undefined, { filters, pagination: this.pagination, sorts, - }); + }) } case 'update': { return updateQuery(this.table, actionValue as Dictionary, { filters, returning: actionOptions?.returning, - }); + }) + } + case 'truncate': { + return truncateQuery(this.table, { + cascade: actionOptions?.cascade, + }) } default: { - return ''; + return '' } } } catch (error) { - throw error; + throw error } } } diff --git a/studio/components/grid/services/row/SqlRowService.ts b/studio/components/grid/services/row/SqlRowService.ts index 1ab0b9f4846..77af8ea4410 100644 --- a/studio/components/grid/services/row/SqlRowService.ts +++ b/studio/components/grid/services/row/SqlRowService.ts @@ -63,11 +63,12 @@ export class SqlRowService implements IRowService { return {} } + // For deleting all rows based on a given filter async deleteAll(filters: Filter[]) { let queryChains = this.query.from(this.table.name, this.table.schema ?? undefined).delete() filters - .filter((x) => x.value && x.value != '') + .filter((x) => x.value && x.value !== '') .forEach((x) => { const value = this.formatFilterValue(x) queryChains = queryChains.filter(x.column, x.operator, value) @@ -77,6 +78,13 @@ export class SqlRowService implements IRowService { return await this.onSqlQuery(query) } + // For deleting all rows without any filter (clear entire table) + async truncate() { + let queryChains = this.query.from(this.table.name, this.table.schema ?? undefined).truncate() + const query = queryChains.toSql() + return await this.onSqlQuery(query) + } + async fetchPage(page: number, rowsPerPage: number, filters: Filter[], sorts: Sort[]) { const pageFromZero = page > 0 ? page - 1 : page const from = pageFromZero * rowsPerPage diff --git a/studio/components/grid/services/row/index.tsx b/studio/components/grid/services/row/index.tsx index c438ea7bdc5..e9d1eddec87 100644 --- a/studio/components/grid/services/row/index.tsx +++ b/studio/components/grid/services/row/index.tsx @@ -33,6 +33,8 @@ export interface IRowService { changedColumn?: string, onRowUpdate?: (value: any) => void ) => { row?: SupaRow; error?: ServiceError } + + truncate: () => Promise<{ error?: ServiceError }> } export * from './SqlRowService' diff --git a/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx b/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx index cd3d055809d..537268647ee 100644 --- a/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx +++ b/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx @@ -246,7 +246,7 @@ const TableEditorMenu: FC = ({ > Duplicate Table , - , + , }