From 2d2cc95caa9e9e200521425faad7abc63aa715d3 Mon Sep 17 00:00:00 2001
From: Charis <26616127+charislam@users.noreply.github.com>
Date: Wed, 13 May 2026 14:57:08 -0400
Subject: [PATCH] studio: add SafeSql foundation utilities (1/7) (#45897)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
First in a stack of seven PRs migrating the remaining `executeSql` call
sites to `SafeSqlFragment`. This PR lands pure additions — shared types
and helpers that later PRs import.
- `apps/studio/lib/postgres-types.ts` — `SafePostgresColumn` /
`SafePostgresTable` wrappers over `PGColumn` / `PGTable` that brand
`check` as `SafeSqlFragment` and add the optional `format_schema` field
- `apps/studio/lib/sql.ts` — `trimSafeSqlFragment` overload set that
preserves the brand through `.trim()`
- `apps/studio/lib/type-helpers.ts` — refine `DeepReadonly` so
primitives pass through unchanged
- `apps/studio/components/ui/SafeSqlInput.tsx` — `` wrapper that
emits `SafeSqlFragment` via `rawSql`
No consumers in this PR. The next PRs in the stack pick these up.
## Stack
This is PR 1 of 7. The full sequence:
1. **This PR** — Foundation utilities
2. `charis/safe-sql-last/2` — pg-meta columns + ColumnTypeRef cascade
3. `charis/safe-sql-last/3` — pg-meta non-column SafeSql
(functions/policies/triggers)
4. `charis/safe-sql-last/4` — Studio reports / query performance /
privileges
5. `charis/safe-sql-last/6` — Stragglers + remaining tests
6. `charis/safe-sql-last/7` — Flip `executeSql` signature from `string`
to `SafeSqlFragment`
## Test plan
- [x] `pnpm typecheck` passes for the Studio target on this branch
- [x] No runtime behavior changes — only type-level additions and a new
unused component
## Summary by CodeRabbit
## Release Notes
* **New Features**
* Introduced SafeSqlInput component providing secure and validated SQL
query input handling with built-in constraints and error prevention.
* **Refactor**
* Enhanced internal type definitions and utilities for Postgres metadata
handling and SQL operations to improve code reliability and
maintainability.
[](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45897)
---
apps/studio/components/ui/SafeSqlInput.tsx | 15 +++++++++++++++
apps/studio/lib/postgres-types.ts | 12 ++++++++++++
apps/studio/lib/sql.ts | 13 +++++++++++++
apps/studio/lib/type-helpers.ts | 14 +++++++++-----
4 files changed, 49 insertions(+), 5 deletions(-)
create mode 100644 apps/studio/components/ui/SafeSqlInput.tsx
create mode 100644 apps/studio/lib/postgres-types.ts
create mode 100644 apps/studio/lib/sql.ts
diff --git a/apps/studio/components/ui/SafeSqlInput.tsx b/apps/studio/components/ui/SafeSqlInput.tsx
new file mode 100644
index 00000000000..1dbb6631051
--- /dev/null
+++ b/apps/studio/components/ui/SafeSqlInput.tsx
@@ -0,0 +1,15 @@
+import { rawSql, type SafeSqlFragment } from '@supabase/pg-meta'
+import type { ChangeEvent, ComponentProps } from 'react'
+import { Input } from 'ui-patterns/DataInputs/Input'
+
+type InputProps = ComponentProps
+
+export type SafeSqlInputProps = Omit & {
+ placeholder?: SafeSqlFragment
+ value: SafeSqlFragment
+ onChange?: (event: ChangeEvent, value: SafeSqlFragment) => void
+}
+
+export const SafeSqlInput = ({ onChange, ...props }: SafeSqlInputProps) => (
+ onChange?.(event, rawSql(event.target.value))} />
+)
diff --git a/apps/studio/lib/postgres-types.ts b/apps/studio/lib/postgres-types.ts
new file mode 100644
index 00000000000..50a5c01572a
--- /dev/null
+++ b/apps/studio/lib/postgres-types.ts
@@ -0,0 +1,12 @@
+import type { PGColumn, PGTable, SafeSqlFragment } from '@supabase/pg-meta'
+
+export type SafePostgresColumn = PGColumn & {
+ check: SafeSqlFragment | null
+ // Present when sourced from pg-meta's SQL queries (e.g. `pgMeta.tables.retrieve`), absent
+ // from the legacy `/platform/pg-meta/{ref}/tables` REST endpoint.
+ format_schema?: string
+}
+
+export type SafePostgresTable = Omit & {
+ columns?: SafePostgresColumn[] | undefined
+}
diff --git a/apps/studio/lib/sql.ts b/apps/studio/lib/sql.ts
new file mode 100644
index 00000000000..45a76ac9778
--- /dev/null
+++ b/apps/studio/lib/sql.ts
@@ -0,0 +1,13 @@
+import type { SafeSqlFragment } from '@supabase/pg-meta'
+
+export function trimSafeSqlFragment(fragment: SafeSqlFragment): SafeSqlFragment
+export function trimSafeSqlFragment(fragment: SafeSqlFragment | null): SafeSqlFragment | null
+export function trimSafeSqlFragment(
+ fragment: SafeSqlFragment | undefined
+): SafeSqlFragment | undefined
+export function trimSafeSqlFragment(
+ fragment: SafeSqlFragment | null | undefined
+): SafeSqlFragment | null | undefined {
+ if (fragment == null) return fragment
+ return fragment.trim() as SafeSqlFragment
+}
diff --git a/apps/studio/lib/type-helpers.ts b/apps/studio/lib/type-helpers.ts
index 02b486eac21..a7310684875 100644
--- a/apps/studio/lib/type-helpers.ts
+++ b/apps/studio/lib/type-helpers.ts
@@ -2,8 +2,12 @@ export type PlainObject = Record = { [K in keyof T]: T[K] } & {}
-export type DeepReadonly = T extends (infer R)[]
- ? ReadonlyArray>
- : T extends Object
- ? { readonly [K in keyof T]: DeepReadonly }
- : T
+type Primitive = string | number | bigint | boolean | symbol | null | undefined
+
+export type DeepReadonly = T extends Primitive
+ ? T
+ : T extends (infer R)[]
+ ? ReadonlyArray>
+ : T extends object
+ ? { readonly [K in keyof T]: DeepReadonly }
+ : T