feat(db-playground): schema visualizer

This commit is contained in:
Greg Richardson committed 2024-06-12 15:43:30 -06:00
1 parent a58fcbfcfa
commit cf94fc85c0
10 files changed
+1660 -245

No files matched your search

@@ -55,7 +55,6 @@ export async function POST(req: Request) {
description: codeBlock`
Generates a chart using Chart.js for a given SQL query.
- Label both axises
- Line charts should default to \`cubicInterpolationMode: 'monotone'\`
- Plugins are not available
Call \`executeSql\` first.
@@ -68,11 +67,6 @@ export async function POST(req: Request) {
),
}),
},
appendSqlToMigration: {
description:
'Appends schema changes to a SQL migration file. Always call this after modifying database structure.',
parameters: z.object({ sql: z.string() }),
},
},
})
+4 -1
View File
@@ -4,6 +4,7 @@ import './globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import { cn } from 'ui'
import Providers from '~/components/providers'
const inter = Inter({ subsets: ['latin'] })
@@ -19,7 +20,9 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body className={cn(inter.className, 'light')}>{children}</body>
<body className={cn(inter.className, 'light')}>
<Providers>{children}</Providers>
</body>
</html>
)
}
+29 -23
View File
@@ -4,6 +4,7 @@ import 'chartjs-adapter-date-fns'
import { PGlite } from '@electric-sql/pglite'
import { Editor } from '@monaco-editor/react'
import { useQueryClient } from '@tanstack/react-query'
import { nanoid } from 'ai'
import { useChat } from 'ai/react'
import Chart from 'chart.js/auto'
@@ -20,13 +21,14 @@ import remarkMath from 'remark-math'
import { format } from 'sql-formatter'
import { AiIconAnimation, markdownComponents } from 'ui'
import { Button } from 'ui/src/components/shadcn/ui/button'
import SchemaGraph from '~/components/schema/graph'
import { Report } from './api/chat/route'
const loadFramerFeatures = () => import('./framer-features').then((res) => res.default)
// React's double-rendering in dev mode causes pglite errors
// Temp: storing single instance in module scope
let db: PGlite = new PGlite('idb://local')
export let db: PGlite = new PGlite('idb://local')
function useReportSuggestions(db: PGlite) {
const [schema, setSchema] = useState<any[]>()
@@ -87,10 +89,12 @@ export default function Page() {
const [isEditorVisible, setIsEditorVisible] = useState(false)
const { reports } = useReportSuggestions(db)
const queryClient = useQueryClient()
const { messages, input, setInput, handleInputChange, handleSubmit, append, stop, isLoading } =
useChat({
api: 'api/chat',
maxToolRoundtrips: 5,
maxToolRoundtrips: 10,
async onToolCall({ toolCall }) {
console.log('tool call', toolCall)
switch (toolCall.toolName) {
@@ -112,6 +116,22 @@ export default function Page() {
const { sql } = toolCall.args as any
console.log(sql)
const results = await db.exec(sql)
setSql((s) => {
const newSql = (s + '\n' + sql).trim()
return format(newSql, {
language: 'postgresql',
keywordCase: 'lower',
identifierCase: 'lower',
dataTypeCase: 'lower',
functionCase: 'lower',
})
})
queryClient.invalidateQueries({
queryKey: ['tables'],
})
console.log(results)
return results
} catch (err) {
@@ -147,24 +167,6 @@ export default function Page() {
canvas.remove()
}
}
case 'appendSqlToMigration': {
const { sql } = toolCall.args as any
setSql((s) => {
const newSql = (s + '\n' + sql).trim()
return format(newSql, {
language: 'postgresql',
keywordCase: 'lower',
identifierCase: 'lower',
dataTypeCase: 'lower',
functionCase: 'lower',
})
})
setIsEditorVisible(true)
return {
success: true,
message: 'SQL has successfully been appended to the migration file.',
}
}
}
},
})
@@ -186,7 +188,10 @@ export default function Page() {
return (
<LazyMotion features={loadFramerFeatures}>
<div className="w-full h-full flex p-6">
<div className="w-full h-full flex p-6 gap-8">
<div className="h-full w-[40rem] py-4 rounded-md bg-[#1e1e1e]">
<SchemaGraph schema="public" />
</div>
{isEditorVisible && (
<div className="h-full w-[50rem] py-4 rounded-md bg-[#1e1e1e]">
<Editor
@@ -506,13 +511,14 @@ export default function Page() {
</m.div>
)}
<input
className="flex-grow border-none focus-visible:ring-0 text-base bg-inherit placeholder:text-neutral-400"
id="input"
name="prompt"
autoComplete="off"
className="flex-grow border-none focus-visible:ring-0 text-base bg-inherit placeholder:text-neutral-400"
value={input}
onChange={handleInputChange}
placeholder="Message Supabase AI"
autoFocus
id="input"
/>
<Button
className="rounded-full w-8 h-8 p-1.5 text-white bg-neutral-800"
@@ -0,0 +1,10 @@
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { PropsWithChildren } from 'react'
const queryClient = new QueryClient()
export default function Providers({ children }: PropsWithChildren) {
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
}
@@ -0,0 +1,291 @@
import dagre from '@dagrejs/dagre'
import type { PostgresTable } from '@supabase/postgres-meta'
import { uniqBy } from 'lodash'
import { useTheme } from 'next-themes'
import { useEffect, useMemo } from 'react'
import ReactFlow, {
Background,
BackgroundVariant,
Edge,
Node,
Position,
ReactFlowProvider,
useReactFlow,
} from 'reactflow'
import 'reactflow/dist/style.css'
import { IconLoader } from 'ui'
import { TABLE_NODE_ROW_HEIGHT, TABLE_NODE_WIDTH, TableNode } from 'ui-patterns/SchemaTableNode'
import { useTablesQuery } from '~/data/tables/tables-query'
import SchemaGraphLegend from './legend'
type TableNodeData = {
name: string
isForeign: boolean
columns: {
id: string
isPrimary: boolean
isNullable: boolean
isUnique: boolean
isUpdateable: boolean
isIdentity: boolean
name: string
format: string
}[]
}
async function getGraphDataFromTables(tables: PostgresTable[]): Promise<{
nodes: Node<TableNodeData>[]
edges: Edge[]
}> {
if (!tables.length) {
return { nodes: [], edges: [] }
}
const nodes = tables.map((table) => {
const columns = (table.columns || []).map((column) => {
return {
id: column.id,
isPrimary: table.primary_keys.some((pk) => pk.name === column.name),
name: column.name,
format: column.format,
isNullable: column.is_nullable,
isUnique: column.is_unique,
isUpdateable: column.is_updatable,
isIdentity: column.is_identity,
}
})
return {
id: `${table.id}`,
type: 'table',
data: {
name: table.name,
isForeign: false,
columns,
},
position: { x: 0, y: 0 },
}
})
const edges: Edge[] = []
const currentSchema = tables[0].schema
const uniqueRelationships = uniqBy(
tables.flatMap((t) => t.relationships),
'id'
)
for (const rel of uniqueRelationships) {
// TODO: Support [external->this] relationship?
if (rel.source_schema !== currentSchema) {
continue
}
// Create additional [this->foreign] node that we can point to on the graph.
if (rel.target_table_schema !== currentSchema) {
nodes.push({
id: rel.constraint_name,
type: 'table',
data: {
name: `${rel.target_table_schema}.${rel.target_table_name}.${rel.target_column_name}`,
isForeign: true,
columns: [],
},
position: { x: 0, y: 0 },
})
const [source, sourceHandle] = findTablesHandleIds(
tables,
rel.source_table_name,
rel.source_column_name
)
if (source) {
edges.push({
id: String(rel.id),
source,
sourceHandle,
target: rel.constraint_name,
targetHandle: rel.constraint_name,
})
}
continue
}
const [source, sourceHandle] = findTablesHandleIds(
tables,
rel.source_table_name,
rel.source_column_name
)
const [target, targetHandle] = findTablesHandleIds(
tables,
rel.target_table_name,
rel.target_column_name
)
// We do not support [external->this] flow currently.
if (source && target) {
edges.push({
id: String(rel.id),
source,
sourceHandle,
target,
targetHandle,
})
}
}
return getLayoutedElements(nodes, edges)
}
function findTablesHandleIds(
tables: PostgresTable[],
table_name: string,
column_name: string
): [string?, string?] {
for (const table of tables) {
if (table_name !== table.name) continue
for (const column of table.columns || []) {
if (column_name !== column.name) continue
return [String(table.id), column.id]
}
}
return []
}
const getLayoutedElements = (nodes: Node[], edges: Edge[]) => {
const dagreGraph = new dagre.graphlib.Graph()
dagreGraph.setDefaultEdgeLabel(() => ({}))
dagreGraph.setGraph({
rankdir: 'LR',
align: 'UR',
nodesep: 25,
ranksep: 50,
})
nodes.forEach((node) => {
dagreGraph.setNode(node.id, {
width: TABLE_NODE_WIDTH / 2,
height: (TABLE_NODE_ROW_HEIGHT / 2) * (node.data.columns.length + 1), // columns + header
})
})
edges.forEach((edge) => {
dagreGraph.setEdge(edge.source, edge.target)
})
dagre.layout(dagreGraph)
nodes.forEach((node) => {
const nodeWithPosition = dagreGraph.node(node.id)
node.targetPosition = Position.Left
node.sourcePosition = Position.Right
// We are shifting the dagre node position (anchor=center center) to the top left
// so it matches the React Flow node anchor point (top left).
node.position = {
x: nodeWithPosition.x - nodeWithPosition.width / 2,
y: nodeWithPosition.y - nodeWithPosition.height / 2,
}
return node
})
return { nodes, edges }
}
const TablesGraph = ({ tables }: { tables: PostgresTable[] }) => {
const { resolvedTheme } = useTheme()
const reactFlowInstance = useReactFlow()
const nodeTypes = useMemo(
() => ({
table: TableNode,
}),
[]
)
useEffect(() => {
getGraphDataFromTables(tables).then(({ nodes, edges }) => {
reactFlowInstance.setNodes(nodes)
reactFlowInstance.setEdges(edges)
setTimeout(() => reactFlowInstance.fitView({})) // it needs to happen during next event tick
})
}, [tables, resolvedTheme])
return (
<>
<div className="w-full h-full">
<ReactFlow
defaultNodes={[]}
defaultEdges={[]}
defaultEdgeOptions={{
type: 'smoothstep',
animated: true,
deletable: false,
style: {
stroke: 'hsl(var(--border-stronger))',
strokeWidth: 0.5,
},
}}
nodeTypes={nodeTypes}
minZoom={0.8}
maxZoom={1.8}
proOptions={{ hideAttribution: true }}
onInit={(flow) => {
// Call `fitView` imperatively in order to set padding
flow.fitView({
padding: 0.4,
})
}}
>
<Background
gap={16}
className="[&>*]:stroke-foreground-muted opacity-[25%]"
variant={BackgroundVariant.Dots}
color={'inherit'}
/>
<SchemaGraphLegend />
</ReactFlow>
</div>
</>
)
}
const SchemaGraph = ({ schema }: { schema: string }) => {
const {
data: tables,
error,
isError,
isLoading,
} = useTablesQuery({ schemas: [schema], includeColumns: true })
if (isLoading) {
return (
<div className="flex h-full w-full items-center justify-center space-x-2">
<IconLoader className="animate-spin" size={14} />
<p className="text-sm text-foreground-light">Loading table...</p>
</div>
)
}
if (isError) {
return (
<div className="px-6 py-4 text-foreground-light">
<p>Error connecting to API</p>
<p>{`${error?.message ?? 'Unknown error'}`}</p>
</div>
)
}
return (
<ReactFlowProvider>
<TablesGraph tables={tables!} />
</ReactFlowProvider>
)
}
export default SchemaGraph
@@ -0,0 +1,37 @@
import { DiamondIcon, Fingerprint, Hash, Key } from 'lucide-react'
const SchemaGraphLegend = () => {
return (
<div className="absolute bottom-0 left-0 border-t flex justify-center px-1 py-2 shadow-md bg-surface-100 w-full z-10">
<ul className="flex flex-wrap items-center justify-center gap-4">
<li className="flex items-center text-xs font-mono gap-1">
<Key size={15} strokeWidth={1.5} className="flex-shrink-0 text-light" />
Primary key
</li>
<li className="flex items-center text-xs font-mono gap-1">
<Hash size={15} strokeWidth={1.5} className="flex-shrink-0 text-light" />
Identity
</li>
<li className="flex items-center text-xs font-mono gap-1">
<Fingerprint size={15} strokeWidth={1.5} className="flex-shrink-0 text-light" />
Unique
</li>
<li className="flex items-center text-xs font-mono gap-1">
<DiamondIcon size={15} strokeWidth={1.5} className="flex-shrink-0 text-light" />
Nullable
</li>
<li className="flex items-center text-xs font-mono gap-1">
<DiamondIcon
size={15}
strokeWidth={1.5}
fill="currentColor"
className="flex-shrink-0 text-light"
/>
Non-Nullable
</li>
</ul>
</div>
)
}
export default SchemaGraphLegend
@@ -0,0 +1,36 @@
import { PostgresTable } from '@supabase/postgres-meta'
import { UseQueryOptions, useQuery } from '@tanstack/react-query'
import { db } from '~/app/page'
import { PostgresMetaErr, getTables } from '~/lib/pg-meta'
export type TablesVariables = {
schemas?: string[]
includeColumns?: boolean
}
export type TablesData = PostgresTable[]
export type TablesError = PostgresMetaErr['error']
export async function getTablesForQuery({ schemas, includeColumns = false }: TablesVariables) {
const { data, error } = await getTables(db, { includedSchemas: schemas, includeColumns })
if (error) {
throw error
}
return data
}
export const useTablesQuery = <TData = TablesData>(
{ schemas, includeColumns }: TablesVariables,
options: Omit<UseQueryOptions<TablesData, TablesError, TData>, 'queryKey' | 'queryFn'> = {}
) =>
useQuery<TablesData, TablesError, TData>({
...options,
queryKey: getTablesQueryKey({ schemas, includeColumns }),
queryFn: () => getTablesForQuery({ schemas, includeColumns }),
})
export const getTablesQueryKey = ({ schemas, includeColumns }: TablesVariables) => [
'tables',
{ schemas, includeColumns },
]
+474
View File
@@ -0,0 +1,474 @@
/**
* This contains cherry-picked `@supabase/postgres-meta` functions (like listing tables)
* that can be used browser-side with PGlite.
*
* https://github.com/supabase/postgres-meta
*
* TODO: Upstream ability to use `@supabase/postgres-meta` in the browser and remove this.
*/
import { PGlite } from '@electric-sql/pglite'
import { PostgresTable } from '@supabase/postgres-meta'
interface NoticeOrError {
message: string | undefined
severity: string | undefined
code: string | undefined
detail: string | undefined
hint: string | undefined
position: string | undefined
internalPosition: string | undefined
internalQuery: string | undefined
where: string | undefined
schema: string | undefined
table: string | undefined
column: string | undefined
dataType: string | undefined
constraint: string | undefined
file: string | undefined
line: string | undefined
routine: string | undefined
}
export declare type MessageName =
| 'parseComplete'
| 'bindComplete'
| 'closeComplete'
| 'noData'
| 'portalSuspended'
| 'replicationStart'
| 'emptyQuery'
| 'copyDone'
| 'copyData'
| 'rowDescription'
| 'parameterDescription'
| 'parameterStatus'
| 'backendKeyData'
| 'notification'
| 'readyForQuery'
| 'commandComplete'
| 'dataRow'
| 'copyInResponse'
| 'copyOutResponse'
| 'authenticationOk'
| 'authenticationMD5Password'
| 'authenticationCleartextPassword'
| 'authenticationSASL'
| 'authenticationSASLContinue'
| 'authenticationSASLFinal'
| 'error'
| 'notice'
export declare class DatabaseError extends Error implements NoticeOrError {
readonly length: number
readonly name: MessageName
severity: string | undefined
code: string | undefined
detail: string | undefined
hint: string | undefined
position: string | undefined
internalPosition: string | undefined
internalQuery: string | undefined
where: string | undefined
schema: string | undefined
table: string | undefined
column: string | undefined
dataType: string | undefined
constraint: string | undefined
file: string | undefined
line: string | undefined
routine: string | undefined
constructor(message: string, length: number, name: MessageName)
}
export interface PostgresMetaOk<T> {
data: T
error: null
}
export interface PostgresMetaErr {
data: null
error: Partial<DatabaseError> & { message: string; formattedError?: string }
}
export type PostgresMetaResult<T> = PostgresMetaOk<T> | PostgresMetaErr
const tablesSql = `
SELECT
c.oid :: int8 AS id,
nc.nspname AS schema,
c.relname AS name,
c.relrowsecurity AS rls_enabled,
c.relforcerowsecurity AS rls_forced,
CASE
WHEN c.relreplident = 'd' THEN 'DEFAULT'
WHEN c.relreplident = 'i' THEN 'INDEX'
WHEN c.relreplident = 'f' THEN 'FULL'
ELSE 'NOTHING'
END AS replica_identity,
pg_total_relation_size(format('%I.%I', nc.nspname, c.relname)) :: int8 AS bytes,
pg_size_pretty(
pg_total_relation_size(format('%I.%I', nc.nspname, c.relname))
) AS size,
pg_stat_get_live_tuples(c.oid) AS live_rows_estimate,
pg_stat_get_dead_tuples(c.oid) AS dead_rows_estimate,
obj_description(c.oid) AS comment,
coalesce(pk.primary_keys, '[]') as primary_keys,
coalesce(
jsonb_agg(relationships) filter (where relationships is not null),
'[]'
) as relationships
FROM
pg_namespace nc
JOIN pg_class c ON nc.oid = c.relnamespace
left join (
select
table_id,
jsonb_agg(_pk.*) as primary_keys
from (
select
n.nspname as schema,
c.relname as table_name,
a.attname as name,
c.oid :: int8 as table_id
from
pg_index i,
pg_class c,
pg_attribute a,
pg_namespace n
where
i.indrelid = c.oid
and c.relnamespace = n.oid
and a.attrelid = c.oid
and a.attnum = any (i.indkey)
and i.indisprimary
) as _pk
group by table_id
) as pk
on pk.table_id = c.oid
left join (
select
c.oid :: int8 as id,
c.conname as constraint_name,
nsa.nspname as source_schema,
csa.relname as source_table_name,
sa.attname as source_column_name,
nta.nspname as target_table_schema,
cta.relname as target_table_name,
ta.attname as target_column_name
from
pg_constraint c
join (
pg_attribute sa
join pg_class csa on sa.attrelid = csa.oid
join pg_namespace nsa on csa.relnamespace = nsa.oid
) on sa.attrelid = c.conrelid and sa.attnum = any (c.conkey)
join (
pg_attribute ta
join pg_class cta on ta.attrelid = cta.oid
join pg_namespace nta on cta.relnamespace = nta.oid
) on ta.attrelid = c.confrelid and ta.attnum = any (c.confkey)
where
c.contype = 'f'
) as relationships
on (relationships.source_schema = nc.nspname and relationships.source_table_name = c.relname)
or (relationships.target_table_schema = nc.nspname and relationships.target_table_name = c.relname)
WHERE
c.relkind IN ('r', 'p')
AND NOT pg_is_other_temp_schema(nc.oid)
AND (
pg_has_role(c.relowner, 'USAGE')
OR has_table_privilege(
c.oid,
'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'
)
OR has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES')
)
group by
c.oid,
c.relname,
c.relrowsecurity,
c.relforcerowsecurity,
c.relreplident,
nc.nspname,
pk.primary_keys
`
const columnsSql = `
-- Adapted from information_schema.columns
SELECT
c.oid :: int8 AS table_id,
nc.nspname AS schema,
c.relname AS table,
(c.oid || '.' || a.attnum) AS id,
a.attnum AS ordinal_position,
a.attname AS name,
CASE
WHEN a.atthasdef THEN pg_get_expr(ad.adbin, ad.adrelid)
ELSE NULL
END AS default_value,
CASE
WHEN t.typtype = 'd' THEN CASE
WHEN bt.typelem <> 0 :: oid
AND bt.typlen = -1 THEN 'ARRAY'
WHEN nbt.nspname = 'pg_catalog' THEN format_type(t.typbasetype, NULL)
ELSE 'USER-DEFINED'
END
ELSE CASE
WHEN t.typelem <> 0 :: oid
AND t.typlen = -1 THEN 'ARRAY'
WHEN nt.nspname = 'pg_catalog' THEN format_type(a.atttypid, NULL)
ELSE 'USER-DEFINED'
END
END AS data_type,
COALESCE(bt.typname, t.typname) AS format,
a.attidentity IN ('a', 'd') AS is_identity,
CASE
a.attidentity
WHEN 'a' THEN 'ALWAYS'
WHEN 'd' THEN 'BY DEFAULT'
ELSE NULL
END AS identity_generation,
a.attgenerated IN ('s') AS is_generated,
NOT (
a.attnotnull
OR t.typtype = 'd' AND t.typnotnull
) AS is_nullable,
(
c.relkind IN ('r', 'p')
OR c.relkind IN ('v', 'f') AND pg_column_is_updatable(c.oid, a.attnum, FALSE)
) AS is_updatable,
uniques.table_id IS NOT NULL AS is_unique,
check_constraints.definition AS "check",
array_to_json(
array(
SELECT
enumlabel
FROM
pg_catalog.pg_enum enums
WHERE
enums.enumtypid = coalesce(bt.oid, t.oid)
OR enums.enumtypid = coalesce(bt.typelem, t.typelem)
ORDER BY
enums.enumsortorder
)
) AS enums,
col_description(c.oid, a.attnum) AS comment
FROM
pg_attribute a
LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid
AND a.attnum = ad.adnum
JOIN (
pg_class c
JOIN pg_namespace nc ON c.relnamespace = nc.oid
) ON a.attrelid = c.oid
JOIN (
pg_type t
JOIN pg_namespace nt ON t.typnamespace = nt.oid
) ON a.atttypid = t.oid
LEFT JOIN (
pg_type bt
JOIN pg_namespace nbt ON bt.typnamespace = nbt.oid
) ON t.typtype = 'd'
AND t.typbasetype = bt.oid
LEFT JOIN (
SELECT DISTINCT ON (table_id, ordinal_position)
conrelid AS table_id,
conkey[1] AS ordinal_position
FROM pg_catalog.pg_constraint
WHERE contype = 'u' AND cardinality(conkey) = 1
) AS uniques ON uniques.table_id = c.oid AND uniques.ordinal_position = a.attnum
LEFT JOIN (
-- We only select the first column check
SELECT DISTINCT ON (table_id, ordinal_position)
conrelid AS table_id,
conkey[1] AS ordinal_position,
substring(
pg_get_constraintdef(pg_constraint.oid, true),
8,
length(pg_get_constraintdef(pg_constraint.oid, true)) - 8
) AS "definition"
FROM pg_constraint
WHERE contype = 'c' AND cardinality(conkey) = 1
ORDER BY table_id, ordinal_position, oid asc
) AS check_constraints ON check_constraints.table_id = c.oid AND check_constraints.ordinal_position = a.attnum
WHERE
NOT pg_is_other_temp_schema(nc.oid)
AND a.attnum > 0
AND NOT a.attisdropped
AND (c.relkind IN ('r', 'v', 'm', 'f', 'p'))
AND (
pg_has_role(c.relowner, 'USAGE')
OR has_column_privilege(
c.oid,
a.attnum,
'SELECT, INSERT, UPDATE, REFERENCES'
)
)
`
const coalesceRowsToArray = (source: string, filter: string) => {
return `
COALESCE(
(
SELECT
array_agg(row_to_json(${source})) FILTER (WHERE ${filter})
FROM
${source}
),
'{}'
) AS ${source}`
}
function arrayToList<T>(useSpace: boolean, array: T[], formatter: (value: T) => string) {
let sql = ''
sql += useSpace ? ' (' : '('
for (var i = 0; i < array.length; i++) {
sql += (i === 0 ? '' : ', ') + formatter(array[i])
}
sql += ')'
return sql
}
function formatDate(date: string) {
date = date.replace('T', ' ')
date = date.replace('Z', '+00')
return date
}
function quoteLiteral(value: any): string {
let literal = null
let explicitCast = null
if (value === undefined || value === null) {
return 'NULL'
} else if (value === false) {
return "'f'"
} else if (value === true) {
return "'t'"
} else if (value instanceof Date) {
return "'" + formatDate(value.toISOString()) + "'"
} else if (value instanceof Buffer) {
return "E'\\\\x" + value.toString('hex') + "'"
} else if (Array.isArray(value) === true) {
const temp = []
for (let i = 0; i < value.length; i++) {
if (Array.isArray(value[i]) === true) {
temp.push(arrayToList(i !== 0, value[i], quoteLiteral))
} else {
temp.push(quoteLiteral(value[i]))
}
}
return temp.toString()
} else if (value === Object(value)) {
explicitCast = 'jsonb'
literal = JSON.stringify(value)
} else {
literal = value.toString().slice(0) // create copy
}
let hasBackslash = false
let quoted = "'"
for (let i = 0; i < literal.length; i++) {
const c = literal[i]
if (c === "'") {
quoted += c + c
} else if (c === '\\') {
quoted += c + c
hasBackslash = true
} else {
quoted += c
}
}
quoted += "'"
if (hasBackslash === true) {
quoted = 'E' + quoted
}
if (explicitCast) {
quoted += '::' + explicitCast
}
return quoted
}
export const filterByList = (include?: string[], exclude?: string[], defaultExclude?: string[]) => {
if (defaultExclude) {
exclude = defaultExclude.concat(exclude ?? [])
}
if (include?.length) {
return `IN (${include.map(quoteLiteral).join(',')})`
} else if (exclude?.length) {
return `NOT IN (${exclude.map(quoteLiteral).join(',')})`
}
return ''
}
const generateEnrichedTablesSql = ({ includeColumns }: { includeColumns: boolean }) => `
with tables as (${tablesSql})
${includeColumns ? `, columns as (${columnsSql})` : ''}
select
*
${includeColumns ? `, ${coalesceRowsToArray('columns', 'columns.table_id = tables.id')}` : ''}
from tables`
const DEFAULT_SYSTEM_SCHEMAS = ['information_schema', 'pg_catalog', 'pg_toast']
type GetTablesOptions = {
includedSchemas?: string[]
excludedSchemas?: string[]
includeSystemSchemas?: boolean
includeColumns?: boolean
}
export async function getTables(
db: PGlite,
options?: GetTablesOptions
): Promise<PostgresMetaResult<PostgresTable[]>> {
const {
includedSchemas,
excludedSchemas,
includeSystemSchemas = false,
includeColumns = true,
} = options ?? {}
let sql = generateEnrichedTablesSql({ includeColumns })
const filter = filterByList(
includedSchemas,
excludedSchemas,
!includeSystemSchemas ? DEFAULT_SYSTEM_SCHEMAS : undefined
)
if (filter) {
sql += ` where schema ${filter}`
}
try {
const res = await db.query<PostgresTable>(sql)
return { data: res.rows, error: null }
} catch (error: any) {
if (error instanceof Error) {
const databaseError: DatabaseError = Object.assign(
new DatabaseError(error.message, 0, 'error'),
error
)
return {
data: null,
error: {
...databaseError,
// error.message is non-enumerable
message: error.message,
formattedError: undefined,
},
}
}
return { data: null, error: { message: error.message } }
}
}
+4
View File
@@ -10,8 +10,11 @@
},
"dependencies": {
"@ai-sdk/openai": "^0.0.21",
"@dagrejs/dagre": "^1.1.2",
"@electric-sql/pglite": "^0.1.5",
"@monaco-editor/react": "^4.6.0",
"@supabase/postgres-meta": "^0.81.2",
"@tanstack/react-query": "^5.45.0",
"ai": "^3.1.27",
"ai-sandbox": "*",
"chart.js": "^4.4.3",
@@ -27,6 +30,7 @@
"react-dom": "^18",
"react-error-boundary": "^4.0.13",
"react-markdown": "^9.0.1",
"reactflow": "^11.11.3",
"rehype-katex": "^7.0.0",
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0",
+775 -215
View File
File diff suppressed because it is too large. Load diff