chore(studio): remove require-safe-sql-fragment ESLint rule (#46079)

## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Refactor / chore.

## What is the current behavior?

A custom type-aware ESLint rule (`studio/require-safe-sql-fragment`)
enforces that the `sql` argument to `executeSql` is a `SafeSqlFragment`.
It runs in a separate `eslint.type-checks.config.cjs` and a dedicated CI
ratchet step, and pulls in `@typescript-eslint/utils` as a direct dev
dependency.

## What is the new behavior?

`SafeSqlFragment` enforcement is now handled entirely by TypeScript
compilation. The ESLint rule, its dedicated config, the ratchet
baselines for it, the CI step, and the `@typescript-eslint/utils` direct
dev dependency have all been removed.

## Additional context

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Simplified development linting workflow by removing type-aware ESLint
checks and associated rule files.
* Cleaned up ESLint configuration and dependencies in the studio
application.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46079?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Charis authored and GitHub committed 2026-05-19 20:30:23 +00:00
1 parent 1e113a8a01
commit cc0b2d3d21
6 files changed
+1 -260

No files matched your search

@@ -44,8 +44,3 @@ jobs:
- name: Run ratchet script
run: pnpm --filter studio run lint:ratchet
- name: Run type-aware ratchet script
env:
NODE_OPTIONS: --max-old-space-size=4096
run: pnpm --filter studio run lint:ratchet:type-checks
+1 -144
View File
@@ -6,8 +6,7 @@
"@typescript-eslint/no-explicit-any": 990,
"no-restricted-imports": 0,
"no-restricted-exports": 251,
"react/no-unstable-nested-components": 51,
"studio/require-safe-sql-fragment": 158
"react/no-unstable-nested-components": 51
},
"ruleFiles": {
"react-hooks/exhaustive-deps": {
@@ -930,148 +929,6 @@
"components/ui/ErrorBoundary/ErrorBoundary.tsx": 1,
"components/ui/NoPermission.tsx": 1,
"pages/project/[ref]/observability/database.tsx": 1
},
"studio/require-safe-sql-fragment": {
"components/interfaces/Auth/Hooks/CreateHookSheet.tsx": 1,
"components/interfaces/Auth/Hooks/HooksListing.tsx": 1,
"components/interfaces/Docs/Description.tsx": 1,
"components/interfaces/Integrations/CronJobs/CronJobsTab.useCleanupActions.ts": 2,
"components/interfaces/QueryPerformance/IndexAdvisor/index-advisor.utils.ts": 1,
"components/interfaces/QueryPerformance/WithStatements/WithStatements.tsx": 1,
"components/interfaces/QueryPerformance/useQueryPerformanceQuery.ts": 1,
"components/interfaces/Reports/ReportBlock/ReportBlock.tsx": 1,
"components/interfaces/Settings/API/DataApiEnableSwitch.utils.ts": 1,
"components/interfaces/Storage/PublicBucketWarning.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx": 12,
"components/ui/AIAssistantPanel/DisplayBlockRenderer.tsx": 1,
"components/ui/EditorPanel/EditorPanel.tsx": 1,
"data/api-settings/create-and-expose-api-schema-mutation.ts": 1,
"data/auth/index-worker-status-query.ts": 1,
"data/auth/user-query.ts": 1,
"data/auth/user-search-indexes-query.ts": 1,
"data/auth/users-count-query.ts": 1,
"data/auth/users-infinite-query.ts": 1,
"data/config/disk-breakdown-query.ts": 1,
"data/database-columns/database-column-create-mutation.ts": 1,
"data/database-columns/database-column-delete-mutation.ts": 1,
"data/database-columns/database-column-update-mutation.ts": 1,
"data/database-cron-jobs/database-cron-job-query.ts": 1,
"data/database-cron-jobs/database-cron-job-run-mutation.ts": 1,
"data/database-cron-jobs/database-cron-jobs-count-estimate-query.ts": 1,
"data/database-cron-jobs/database-cron-jobs-count-query.ts": 1,
"data/database-cron-jobs/database-cron-jobs-create-mutation.ts": 1,
"data/database-cron-jobs/database-cron-jobs-delete-mutation.ts": 1,
"data/database-cron-jobs/database-cron-jobs-infinite-query.ts": 1,
"data/database-cron-jobs/database-cron-jobs-minimal-infinite-query.ts": 1,
"data/database-cron-jobs/database-cron-jobs-runs-infinite-query.ts": 1,
"data/database-cron-jobs/database-cron-jobs-toggle-mutation.ts": 1,
"data/database-cron-jobs/database-cron-timezone-query.ts": 1,
"data/database-cron-jobs/schedule-clean-up-mutation.ts": 1,
"data/database-event-triggers/database-event-trigger-create-mutation.ts": 1,
"data/database-event-triggers/database-event-trigger-delete-mutation.ts": 1,
"data/database-event-triggers/database-event-triggers-query.ts": 1,
"data/database-extensions/database-extension-disable-mutation.ts": 1,
"data/database-extensions/database-extension-enable-mutation.ts": 1,
"data/database-extensions/database-extensions-query.ts": 1,
"data/database-functions/database-functions-create-mutation.ts": 1,
"data/database-functions/database-functions-delete-mutation.ts": 1,
"data/database-functions/database-functions-query.ts": 1,
"data/database-functions/database-functions-update-mutation.ts": 1,
"data/database-indexes/index-create-mutation.ts": 1,
"data/database-indexes/index-delete-mutation.ts": 1,
"data/database-indexes/indexes-query.ts": 1,
"data/database-integrations/stripe/sync-state-query.ts": 1,
"data/database-policies/database-policy-create-mutation.ts": 1,
"data/database-policies/database-policy-delete-mutation.ts": 1,
"data/database-policies/database-policy-update-mutation.ts": 1,
"data/database-publications/database-publications-create-mutation.ts": 1,
"data/database-publications/database-publications-update-mutation.ts": 1,
"data/database-queues/database-queue-messages-archive-mutation.ts": 1,
"data/database-queues/database-queue-messages-delete-mutation.ts": 1,
"data/database-queues/database-queue-messages-infinite-query.ts": 1,
"data/database-queues/database-queue-messages-read-mutation.ts": 1,
"data/database-queues/database-queue-messages-send-mutation.ts": 1,
"data/database-queues/database-queues-create-mutation.ts": 1,
"data/database-queues/database-queues-delete-mutation.ts": 1,
"data/database-queues/database-queues-expose-postgrest-status-query.ts": 1,
"data/database-queues/database-queues-metrics-query.ts": 2,
"data/database-queues/database-queues-purge-mutation.ts": 1,
"data/database-queues/database-queues-query.ts": 1,
"data/database-queues/database-queues-toggle-postgrest-mutation.ts": 1,
"data/database-roles/database-role-create-mutation.ts": 1,
"data/database-roles/database-role-delete-mutation.ts": 1,
"data/database-roles/database-role-update-mutation.ts": 1,
"data/database-roles/database-roles-query.ts": 1,
"data/database-triggers/database-trigger-create-mutation.ts": 1,
"data/database-triggers/database-trigger-delete-mutation.ts": 1,
"data/database-triggers/database-trigger-update-mutation.ts": 1,
"data/database-triggers/database-trigger-update-transaction-mutation.ts": 1,
"data/database/constraints-query.ts": 1,
"data/database/database-size-query.ts": 1,
"data/database/entity-definitions-query.ts": 1,
"data/database/foreign-key-constraints-query.ts": 1,
"data/database/keywords-query.ts": 1,
"data/database/max-connections-query.ts": 1,
"data/database/migrations-query.ts": 1,
"data/database/retrieve-index-advisor-result-query.ts": 1,
"data/database/retrieve-index-from-select-query.ts": 2,
"data/database/schema-create-mutation.ts": 1,
"data/database/schemas-query.ts": 1,
"data/database/supamonitor-enabled-query.ts": 1,
"data/database/table-columns-query.ts": 1,
"data/database/table-definition-query.ts": 1,
"data/database/table-index-advisor-query.ts": 1,
"data/database/view-definition-query.ts": 1,
"data/entity-types/entity-types-infinite-query.ts": 1,
"data/enumerated-types/enumerated-type-create-mutation.ts": 1,
"data/enumerated-types/enumerated-type-delete-mutation.ts": 1,
"data/enumerated-types/enumerated-type-update-mutation.ts": 1,
"data/fdw/fdw-create-mutation.ts": 1,
"data/fdw/fdw-delete-mutation.ts": 1,
"data/fdw/fdw-drop-foreign-table-mutation.ts": 1,
"data/fdw/fdw-import-foreign-schema-mutation.ts": 1,
"data/fdw/fdw-update-mutation.ts": 1,
"data/fdw/fdws-query.ts": 1,
"data/privileges/default-privileges-query.ts": 1,
"data/privileges/exposed-function-counts-query.ts": 1,
"data/privileges/exposed-functions-infinite-query.ts": 1,
"data/privileges/exposed-table-counts-query.ts": 1,
"data/privileges/exposed-tables-infinite-query.ts": 1,
"data/privileges/table-api-access-mutation.ts": 1,
"data/privileges/table-privileges-grant-mutation.ts": 1,
"data/privileges/table-privileges-query.ts": 1,
"data/privileges/table-privileges-revoke-mutation.ts": 1,
"data/privileges/update-default-privileges-mutation.ts": 1,
"data/privileges/update-exposed-entities-mutation.ts": 1,
"data/read-replicas/replica-lag-query.ts": 1,
"data/sql/abort-query-mutation.ts": 1,
"data/sql/execute-sql-query.ts": 1,
"data/sql/ongoing-queries-query.ts": 1,
"data/storage/bucket-prefix-delete-mutation.ts": 1,
"data/storage/buckets-max-size-limit-query.ts": 2,
"data/storage/public-buckets-with-select-policies-query.ts": 1,
"data/table-editor/table-editor-query.ts": 1,
"data/table-rows/get-cell-value-mutation.ts": 1,
"data/table-rows/operation-queue-save-mutation.ts": 1,
"data/table-rows/table-row-create-mutation.ts": 1,
"data/table-rows/table-row-delete-all-mutation.ts": 1,
"data/table-rows/table-row-delete-mutation.tsx": 1,
"data/table-rows/table-row-truncate-mutation.ts": 1,
"data/table-rows/table-row-update-mutation.ts": 1,
"data/table-rows/table-rows-count-query.ts": 1,
"data/table-rows/table-rows-query.ts": 3,
"data/tables/table-names-query.ts": 1,
"data/tables/tables-roles-access-query.ts": 1,
"data/vault/vault-secret-create-mutation.ts": 1,
"data/vault/vault-secret-decrypted-value-query.ts": 2,
"data/vault/vault-secret-delete-mutation.ts": 1,
"data/vault/vault-secret-update-mutation.ts": 1,
"data/vault/vault-secrets-query.ts": 1,
"hooks/analytics/useDbQuery.tsx": 1,
"lib/ai/tools/fallback-tools.ts": 1,
"pages/api/ai/code/complete.ts": 1,
"pages/api/ai/sql/generate-v4.ts": 1,
"state/role-impersonation-state.tsx": 1
}
}
}
@@ -1,70 +0,0 @@
/**
* ESLint rule to require SafeSqlFragment (not a plain string) for the sql argument of executeSql.
*
* During migration, executeSql still accepts string (a supertype of SafeSqlFragment), but new
* call sites should use SafeSqlFragment to avoid SQL injection risks and to be ready for when
* the type is tightened.
*
* BAD:
* executeSql({ projectRef, sql: `SELECT * FROM ${table}` })
* const query = buildQuery() // returns string
* executeSql({ projectRef, sql: query })
*
* GOOD:
* executeSql({ projectRef, sql: safeSql`SELECT * FROM ${ident(table)}` })
* executeSql({ projectRef, sql: keyword('SELECT 1') })
*/
const { ESLintUtils } = require('@typescript-eslint/utils')
/** @type {import('@typescript-eslint/utils').TSESLint.RuleModule<'requireSafeSqlFragment'>} */
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'Require executeSql to be called with SafeSqlFragment, not a plain string. Use safeSql`...`, ident(), literal(), or keyword() to create a SafeSqlFragment.',
recommended: true,
},
messages: {
requireSafeSqlFragment:
'The sql argument to executeSql must be SafeSqlFragment, not a plain string. Use safeSql`...`, ident(), literal(), or keyword() to construct it safely.',
},
schema: [],
},
create(context) {
const services = ESLintUtils.getParserServices(context)
const checker = services.program.getTypeChecker()
return {
CallExpression(node) {
const callee = node.callee
const isExecuteSql =
(callee.type === 'Identifier' && callee.name === 'executeSql') ||
(callee.type === 'MemberExpression' &&
callee.property.type === 'Identifier' &&
callee.property.name === 'executeSql')
if (!isExecuteSql) return
const firstArg = node.arguments[0]
if (!firstArg || firstArg.type !== 'ObjectExpression') return
const sqlProp = firstArg.properties.find(
(prop) =>
prop.type === 'Property' && prop.key.type === 'Identifier' && prop.key.name === 'sql'
)
if (!sqlProp || sqlProp.type !== 'Property') return
const tsNode = services.esTreeNodeToTSNodeMap.get(sqlProp.value)
const type = checker.getTypeAtLocation(tsNode)
const hasBrand = checker.getPropertyOfType(type, '__safeSqlFragmentBrand') !== undefined
if (!hasBrand) {
context.report({ node: sqlProp.value, messageId: 'requireSafeSqlFragment' })
}
},
}
},
}
-36
View File
@@ -1,36 +0,0 @@
/**
* Separate ESLint config for rules that require TypeScript type information.
*
* Run via: pnpm lint:type-checks
*
* Kept separate from eslint.config.cjs because loading a full TypeScript program
* (project: true) is memory-intensive and would slow down or OOM the main lint run.
*/
const { defineConfig } = require('eslint/config')
const tsparser = require('@typescript-eslint/parser')
const requireSafeSqlFragment = require('./eslint-rules/require-safe-sql-fragment.cjs')
const studioPlugin = {
rules: {
'require-safe-sql-fragment': requireSafeSqlFragment,
},
}
module.exports = defineConfig([
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parser: tsparser,
parserOptions: {
project: true,
},
},
plugins: {
studio: studioPlugin,
},
rules: {
'studio/require-safe-sql-fragment': 'warn',
},
},
])
-2
View File
@@ -11,7 +11,6 @@
"start": "next start -p 8082",
"lint": "eslint .",
"lint:ratchet": "tsx scripts/ratchet-eslint-rules.ts --rule react-hooks/exhaustive-deps --rule import/no-anonymous-default-export --rule @tanstack/query/exhaustive-deps --rule @typescript-eslint/no-explicit-any --rule no-restricted-imports --rule no-restricted-exports --rule react/no-unstable-nested-components",
"lint:ratchet:type-checks": "tsx scripts/ratchet-eslint-rules.ts --rule studio/require-safe-sql-fragment --eslint-args \"--config eslint.type-checks.config.cjs .\"",
"clean": "rimraf node_modules tsconfig.tsbuildinfo .next .turbo",
"test": "vitest --run --coverage",
"test:watch": "vitest watch",
@@ -176,7 +175,6 @@
"@types/recharts": "^1.8.23",
"@types/sqlstring": "^2.3.0",
"@types/zxcvbn": "^4.4.1",
"@typescript-eslint/utils": "8.48.0",
"@vitejs/plugin-react": "catalog:",
"@vitest/coverage-v8": "catalog:",
"@vitest/ui": "catalog:",
-3
View File
@@ -1284,9 +1284,6 @@ importers:
'@types/zxcvbn':
specifier: ^4.4.1
version: 4.4.2
'@typescript-eslint/utils':
specifier: 8.48.0
version: 8.48.0(eslint@9.37.0(jiti@2.6.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.2)
'@vitejs/plugin-react':
specifier: 'catalog:'
version: 6.0.1(vite@8.0.8(@types/node@22.13.14)(esbuild@0.25.2)(jiti@2.6.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.3))