Assistant V2 (#30523)

* start

* added panels

* remove stuff

* fixes and refinements

* clean up

* remove old assistant panel

* resizable assistant kinda

* use icon

* Add missing package

* remove canvas

* add suggestions

* updated empty state if no tables exist

* fix table condition

* Implement diffing if using assistant in sql editor

* Reinstate old assistant in SQL editor if feature preview is off

* pane size adjustment

* assistant button corners

* Add SQL snippet content to assistant if opening assistant in sql editor

* Add the necessary checks for opt in and hipaa

* revert adding snippet to assistant when opening assistant in sql editor

* Add cmd i shortcut

* Add admonitions for when disablePrompt is toggled on, and if no api key is set. Add footer note RE rate limitation

* Bump ai package in packages

* some fixes for backwards compability depending on feature preview toggled

* Rename feature preview property for new assistant

* Smol fix

* Prevent SQL snippet from running until message is finished

* only loading last message

* fix z-index

* save chat state to global state

* add debug to failed ai queries

* Add basic contextual invalidation

* Add explain code action to SQL editor

* Add link to abort ongoing queries from SqlSnippet

* Update feature preview content

* Fix

* Fix

* Fix

* Te4st

* Fix tests

* ONly show ai button within a project

* Fix PH tracking

* Beef up a bit more event tracking

* Rough fix to padding when assistant is open

* A bit more telemetry stuff

* Update prompts

* fix rls editing via assistant

* Update generate-v3.ts

prompt to get auth schema too

* Add policy satement to assistant when editing

* Address all comments

* fixc

* Fix SqlSnippet not taking full width on larger viewports

* Adjust max width

---------

Co-authored-by: Saxon Fletcher <saxonafletcher@gmail.com>
This commit is contained in:
Joshen Lim
2024-11-25 18:50:56 +08:00
committed by GitHub
parent 847453bf78
commit 12d92aed99
67 changed files with 3093 additions and 1852 deletions

View File

@@ -0,0 +1,55 @@
import Image from 'next/image'
import { useParams } from 'common'
import { Markdown } from 'components/interfaces/Markdown'
import { BASE_PATH } from 'lib/constants'
import { detectOS } from 'lib/helpers'
export const AssistantV2Preview = () => {
const os = detectOS()
const { ref } = useParams()
return (
<div className="flex flex-col gap-y-2">
<p className="text-foreground-light text-sm mb-4">
We're changing the way our AI Assistant integrates with the dashboard by making it shared
and accessible universally across the whole dashboard. This hopes to make using the
Assistant as a supporting tool more seamless while you build your project.
</p>
<Image
src={`${BASE_PATH}/img/previews/assistant-v2.png`}
width={1860}
height={970}
alt="api-docs-side-panel-preview"
className="rounded border"
/>
<p className="text-foreground-light text-sm mb-4">
The Assistant will also be automatically provided with contexts depending on where you are
in the dashboard to generate more relevant and higher quality outputs. You may also ask for
insights on your own data apart from help with SQL and Postgres!
</p>
<p className="text-foreground-light text-sm mb-4">
We believe that this should further lower the barrier of working with databases especially
if you're not well acquainted with Postgres (yet!), so please do feel free to let us know
what you think in the attached GitHub discussion above!
</p>
<div className="flex flex-col gap-y-2">
<p className="text-sm">Enabling this preview will:</p>
<ul className="list-disc pl-6 text-sm text-foreground-light space-y-1">
<li>
<Markdown
className="text-foreground-light"
content={`Add a button in the top navigation bar where you can access the AI Assistant from anywhere in the dashboard`}
/>
</li>
<li>
<Markdown
className="text-foreground-light"
content={`Add a keyboard shortcut (${os === 'macos' ? 'Cmd' : 'Ctrl'} + I) that can also open the Assistant from anywhere in the dashboard`}
/>
</li>
</ul>
</div>
</div>
)
}

View File

@@ -5,13 +5,13 @@ import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { EMPTY_OBJ } from 'lib/void'
import { APISidePanelPreview } from './APISidePanelPreview'
import { CLSPreview } from './CLSPreview'
import { FunctionsAssistantPreview } from './FunctionsAssistantPreview'
import { AssistantV2Preview } from './AssistantV2Preview'
export const FEATURE_PREVIEWS = [
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_FUNCTIONS_ASSISTANT,
name: 'Database Functions Assistant',
content: <FunctionsAssistantPreview />,
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_ASSISTANT_V2,
name: 'Supabase AI Assistant V2',
content: <AssistantV2Preview />,
discussionsUrl: undefined,
isNew: true,
},
@@ -86,7 +86,7 @@ export const useIsColumnLevelPrivilegesEnabled = () => {
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS]
}
export const useIsDatabaseFunctionsAssistantEnabled = () => {
export const useIsAssistantV2Enabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_FUNCTIONS_ASSISTANT]
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_ASSISTANT_V2]
}

View File

@@ -3,10 +3,12 @@ import Link from 'next/link'
import { useEffect, useState } from 'react'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useFlag } from 'hooks/ui/useFlag'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { TELEMETRY_EVENTS } from 'lib/constants/telemetry'
import { useAppStateSnapshot } from 'state/app-state'
import { Badge, Button, Modal, ScrollArea, cn } from 'ui'
import { FEATURE_PREVIEWS, useFeaturePreviewContext } from './FeaturePreviewContext'
import { useFlag } from 'hooks/ui/useFlag'
const FeaturePreviewModal = () => {
const snap = useAppStateSnapshot()
@@ -36,10 +38,17 @@ const FeaturePreviewModal = () => {
const toggleFeature = () => {
onUpdateFlag(selectedFeatureKey, !isSelectedFeatureEnabled)
sendEvent({
category: 'ui_feature_previews',
action: isSelectedFeatureEnabled ? 'disabled' : 'enabled',
action: TELEMETRY_EVENTS.FEATURE_PREVIEWS,
label: selectedFeatureKey,
value: isSelectedFeatureEnabled ? 'disabled' : 'enabled',
})
if (
selectedFeatureKey === LOCAL_STORAGE_KEYS.UI_PREVIEW_ASSISTANT_V2 &&
isSelectedFeatureEnabled
) {
snap.setAiAssistantPanel({ open: false })
}
}
function handleCloseFeaturePreviewModal() {

View File

@@ -1,55 +0,0 @@
import Image from 'next/image'
import { useParams } from 'common'
import { Markdown } from 'components/interfaces/Markdown'
import { BASE_PATH } from 'lib/constants'
import { detectOS } from 'lib/helpers'
export const FunctionsAssistantPreview = () => {
const os = detectOS()
const { ref } = useParams()
return (
<div className="flex flex-col gap-y-2">
<p className="text-foreground-light text-sm mb-4">
We're providing an additional alternative UX to creating database functions through the
dashboard with the integration of our AI Assistant that you might have seen in the Auth
Policies section.
</p>
<Image
src={`${BASE_PATH}/img/previews/functions-assistant.png`}
width={1860}
height={970}
alt="api-docs-side-panel-preview"
className="rounded border"
/>
<p className="text-foreground-light text-sm mb-4">
This preview also shares an improved Assistant interface where you may provide the Assistant
with contexts in hopes to generate more relevant and higher quality outputs. Contexts that
you may provide include specific schemas, and / or specific tables from your database.
</p>
<p className="text-foreground-light text-sm mb-4">
We'd hope to use this as a consistent pattern throughout the dashboard eventually if this
feature preview proves itself to benefit most of our users, so as usual please do feel free
to let us know what you think if the attached GitHub discussion above!
</p>
<div className="flex flex-col gap-y-2">
<p className="text-sm">Enabling this preview will:</p>
<ul className="list-disc pl-6 text-sm text-foreground-light space-y-1">
<li>
<Markdown
className="text-foreground-light"
content={`Add a button beside the "Create new function" button on the [Database Functions page](/project/${ref}/database/functions) that will open up a code editor paired with a contextualized AI assistant in a side panel.`}
/>
</li>
<li>
<Markdown
className="text-foreground-light"
content={`Add a keyboard shortcut (${os === 'macos' ? 'Cmd' : 'Ctrl'} + I) that will open the Assistant in an uncontextualized mode, along with a quick SQL Editor that you can run queries with from anywhere in the dashboard`}
/>
</li>
</ul>
</div>
</div>
)
}

View File

@@ -21,8 +21,9 @@ import {
TooltipContent_Shadcn_,
TooltipTrigger_Shadcn_,
} from 'ui'
import { useIsDatabaseFunctionsAssistantEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useAppStateSnapshot } from 'state/app-state'
import { generatePolicyCreateSQL } from './PolicyTableRow.utils'
interface PolicyRowProps {
policy: PostgresPolicy
@@ -38,7 +39,7 @@ const PolicyRow = ({
onSelectDeletePolicy = noop,
}: PolicyRowProps) => {
const { setAiAssistantPanel } = useAppStateSnapshot()
const enableAssistantV2 = useIsDatabaseFunctionsAssistantEnabled()
const isAssistantV2Enabled = useIsAssistantV2Enabled()
const canUpdatePolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'policies')
const { project } = useProjectContext()
@@ -102,21 +103,21 @@ const PolicyRow = ({
<DropdownMenuContent
side="bottom"
align="end"
className={cn(enableAssistantV2 ? 'w-52' : 'w-40')}
className={cn(isAssistantV2Enabled ? 'w-52' : 'w-40')}
>
<DropdownMenuItem className="gap-x-2" onClick={() => onSelectEditPolicy(policy)}>
<Edit size={14} />
<p>Edit policy</p>
</DropdownMenuItem>
{enableAssistantV2 && (
{isAssistantV2Enabled && (
<DropdownMenuItem
className="space-x-2"
onClick={() => {
const sql = generatePolicyCreateSQL(policy)
setAiAssistantPanel({
open: true,
editor: 'rls-policies',
entity: policy,
tables: [{ schema: policy.schema, name: policy.table }],
sqlSnippets: [sql],
initialInput: `Update the policy with name "${policy.name}" in the ${policy.schema} schema on the ${policy.table} table. It should...`,
})
}}
>

View File

@@ -0,0 +1,22 @@
import { PostgresPolicy } from '@supabase/postgres-meta'
export const generatePolicyCreateSQL = (policy: PostgresPolicy) => {
let expression = ''
if (policy.definition !== null && policy.definition !== undefined) {
expression += `USING (${policy.definition})${
policy.check === null || policy.check === undefined ? ';' : ''
}\n`
}
if (policy.check !== null && policy.check !== undefined) {
expression += `WITH CHECK (${policy.check});\n`
}
return `
CREATE POLICY "${policy.name}"
ON "${policy.schema}"."${policy.table}"
AS ${policy.action}
FOR ${policy.command}
TO ${policy.roles.join(', ')}
${expression}
`.trim()
}

View File

@@ -4,7 +4,7 @@ import { Lock, Unlock } from 'lucide-react'
import { useQueryState } from 'nuqs'
import { useParams } from 'common'
import { useIsDatabaseFunctionsAssistantEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
@@ -37,7 +37,8 @@ const PolicyTableRowHeader = ({
const { ref } = useParams()
const { setAiAssistantPanel } = useAppStateSnapshot()
const enableAssistantV2 = useIsDatabaseFunctionsAssistantEnabled()
const enableAssistantV2 = useIsAssistantV2Enabled()
const canCreatePolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'policies')
const canToggleRLS = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
const isRealtimeSchema = table.schema === 'realtime'
@@ -96,13 +97,13 @@ const PolicyTableRowHeader = ({
)}
<ButtonTooltip
type="default"
disabled={!canToggleRLS}
disabled={!canToggleRLS || !canCreatePolicies}
onClick={() => onSelectCreatePolicy()}
tooltip={{
content: {
side: 'bottom',
text: !canToggleRLS
? !canToggleRLS
? !canToggleRLS || !canCreatePolicies
? 'You need additional permissions to create RLS policies'
: undefined
: undefined,
@@ -119,9 +120,7 @@ const PolicyTableRowHeader = ({
if (enableAssistantV2) {
setAiAssistantPanel({
open: true,
editor: 'rls-policies',
entity: undefined,
tables: [{ schema: table.schema, name: table.name }],
initialInput: `Create a new policy for the ${table.schema} schema on the ${table.name} table that ...`,
})
} else {
onSelectCreatePolicy()
@@ -131,9 +130,10 @@ const PolicyTableRowHeader = ({
tooltip={{
content: {
side: 'bottom',
text: !canToggleRLS
? 'You need additional permissions to create RLS policies'
: 'Create with Supabase Assistant',
text:
!canToggleRLS || !canCreatePolicies
? 'You need additional permissions to create RLS policies'
: 'Create with Supabase Assistant',
},
}}
>

View File

@@ -63,16 +63,16 @@ const EnumeratedTypes = () => {
<div className="flex items-center justify-between">
<div className="flex items-center gap-x-2">
<SchemaSelector
className="w-[260px]"
size="small"
className="w-[180px]"
size="tiny"
showError={false}
selectedSchemaName={selectedSchema}
onSelectSchema={setSelectedSchema}
/>
<Input
size="small"
size="tiny"
value={search}
className="w-64"
className="w-52"
onChange={(e) => setSearch(e.target.value)}
placeholder="Search for a type"
icon={<Search size={14} />}

View File

@@ -57,11 +57,11 @@ const Extensions = () => {
<div className="mb-4">
<div className="flex items-center justify-between">
<Input
size="small"
size="tiny"
placeholder="Search for an extension"
value={filterString}
onChange={(e) => setFilterString(e.target.value)}
className="w-64"
className="w-52"
icon={<Search size={14} />}
/>
<DocsButton href="https://supabase.com/docs/guides/database/extensions" />

View File

@@ -18,7 +18,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from 'ui'
import { useIsDatabaseFunctionsAssistantEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
interface FunctionListProps {
schema: string
@@ -37,8 +37,8 @@ const FunctionList = ({
}: FunctionListProps) => {
const router = useRouter()
const { project: selectedProject } = useProjectContext()
const enableAssistantV2 = useIsDatabaseFunctionsAssistantEnabled()
const { setAiAssistantPanel } = useAppStateSnapshot()
const isAssistantV2Enabled = useIsAssistantV2Enabled()
const { data: functions } = useDatabaseFunctionsQuery({
projectRef: selectedProject?.ref,
@@ -113,7 +113,7 @@ const FunctionList = ({
</DropdownMenuTrigger>
<DropdownMenuContent
side="left"
className={cn(enableAssistantV2 ? 'w-52' : 'w-40')}
className={cn(isAssistantV2Enabled ? 'w-52' : 'w-40')}
>
{isApiDocumentAvailable && (
<DropdownMenuItem
@@ -128,11 +128,24 @@ const FunctionList = ({
<Edit2 size={14} />
<p>Edit function</p>
</DropdownMenuItem>
{enableAssistantV2 && (
{isAssistantV2Enabled && (
<DropdownMenuItem
className="space-x-2"
onClick={() => {
setAiAssistantPanel({ open: true, editor: 'functions', entity: x })
setAiAssistantPanel({
open: true,
initialInput: 'Update this function to do...',
suggestions: {
title:
'I can help you make a change to this function, here are a few example prompts to get you started:',
prompts: [
'Rename this function to ...',
'Modify this function so that it ...',
'Add a trigger for this function that calls it when ...',
],
},
sqlSnippets: [x.complete_statement],
})
}}
>
<Edit size={14} />

View File

@@ -5,7 +5,7 @@ import { Search } from 'lucide-react'
import { useRouter } from 'next/router'
import { useParams } from 'common'
import { useIsDatabaseFunctionsAssistantEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
import Table from 'components/to-be-cleaned/Table'
@@ -38,7 +38,7 @@ const FunctionsList = ({
const { search } = useParams()
const { project } = useProjectContext()
const { setAiAssistantPanel } = useAppStateSnapshot()
const enableFunctionsAssistant = useIsDatabaseFunctionsAssistantEnabled()
const isAssistantV2Enabled = useIsAssistantV2Enabled()
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
const filterString = search ?? ''
@@ -104,10 +104,10 @@ const FunctionsList = ({
) : (
<div className="w-full space-y-4">
<div className="flex items-center justify-between gap-2 flex-wrap">
<div className="flex items-center space-x-4">
<div className="flex items-center gap-x-2">
<SchemaSelector
className="w-[260px]"
size="small"
className="w-[180px]"
size="tiny"
showError={false}
selectedSchemaName={selectedSchema}
onSelectSchema={(schema) => {
@@ -119,10 +119,10 @@ const FunctionsList = ({
/>
<Input
placeholder="Search for a function"
size="small"
size="tiny"
icon={<Search size={14} />}
value={filterString}
className="w-64"
className="w-52"
onChange={(e) => setFilterString(e.target.value)}
/>
</div>
@@ -144,7 +144,7 @@ const FunctionsList = ({
>
Create a new function
</ButtonTooltip>
{enableFunctionsAssistant && (
{isAssistantV2Enabled && (
<ButtonTooltip
type="default"
disabled={!canCreateFunctions}
@@ -155,8 +155,7 @@ const FunctionsList = ({
onClick={() =>
setAiAssistantPanel({
open: true,
editor: 'functions',
entity: undefined,
initialInput: `Create a new function for the schema ${selectedSchema} that does ...`,
})
}
tooltip={{

View File

@@ -49,10 +49,10 @@ const HooksList = ({ createHook = noop, editHook = noop, deleteHook = noop }: Ho
<div className="flex items-center justify-between">
<Input
placeholder="Search for a webhook"
size="small"
size="tiny"
icon={<Search size="14" />}
value={filterString}
className="w-64"
className="w-52"
onChange={(e) => setFilterString(e.target.value)}
/>
<div className="flex items-center gap-x-2">

View File

@@ -109,17 +109,17 @@ const Indexes = () => {
)}
{isSuccessSchemas && (
<SchemaSelector
className="w-[260px]"
size="small"
className="w-[180px]"
size="tiny"
showError={false}
selectedSchemaName={selectedSchema}
onSelectSchema={setSelectedSchema}
/>
)}
<Input
size="small"
size="tiny"
value={search}
className="w-64"
className="w-52"
onChange={(e) => setSearch(e.target.value)}
placeholder="Search for an index"
icon={<Search size={14} />}

View File

@@ -43,23 +43,15 @@ const PrivilegesHead = ({
}: PrivilegesHeadProps) => {
return (
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="flex items-center gap-x-2">
<SchemaSelector
className="bg-control rounded-md w-[200px] [&>button]:py-[5px]"
className="bg-control rounded-md w-[180px] [&>button]:py-[5px]"
selectedSchemaName={selectedSchema}
onSelectSchema={onChangeSchema}
/>
<div className="w-[200px]">
<TablesSelect
selectedTable={selectedTable}
tables={tables}
onChangeTable={onChangeTable}
/>
</div>
<TablesSelect selectedTable={selectedTable} tables={tables} onChangeTable={onChangeTable} />
<div className="h-[20px] w-px border-r border-scale-600"></div>
<div className="w-[200px]">
<RolesSelect selectedRole={selectedRole} roles={roles} onChangeRole={onChangeRole} />
</div>
<RolesSelect selectedRole={selectedRole} roles={roles} onChangeRole={onChangeRole} />
</div>
<div className="flex items-center gap-2">
@@ -96,14 +88,14 @@ const RolesSelect = ({
}) => {
return (
<Select_Shadcn_ value={selectedRole} onValueChange={onChangeRole}>
<SelectTrigger_Shadcn_>
<SelectTrigger_Shadcn_ size="tiny" className="w-40">
<SelectValue_Shadcn_ placeholder="Select a role" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
{roles.map((role) => (
<SelectItem_Shadcn_ key={role} value={role}>
<span className="text-foreground-light">role</span> {role}
<SelectItem_Shadcn_ key={role} value={role} className="text-xs">
<span className="text-foreground-light mr-1">role</span> {role}
</SelectItem_Shadcn_>
))}
</SelectGroup_Shadcn_>
@@ -125,7 +117,7 @@ const TablesSelect = ({
}) => {
return (
<Select_Shadcn_ value={selectedTable?.name} onValueChange={onChangeTable}>
<SelectTrigger_Shadcn_>
<SelectTrigger_Shadcn_ size="tiny" className="w-44">
<SelectValue_Shadcn_ placeholder="Select a table" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
@@ -136,8 +128,8 @@ const TablesSelect = ({
</div>
) : null}
{tables.map((table) => (
<SelectItem_Shadcn_ key={table} value={table}>
<span className="text-foreground-light">table</span> {table}
<SelectItem_Shadcn_ key={table} value={table} className="text-xs">
<span className="text-foreground-light mr-1">table</span> {table}
</SelectItem_Shadcn_>
))}
</SelectGroup_Shadcn_>

View File

@@ -81,7 +81,7 @@ const PublicationsList = ({ onSelectPublication = noop }: PublicationsListProps)
<div className="flex items-center justify-between">
<div className="flex items-center">
<Input
size="small"
size="tiny"
icon={<Search size="14" />}
placeholder={'Filter'}
value={filterString}

View File

@@ -63,7 +63,8 @@ const RolesList = () => {
<div className="mb-4 flex items-center justify-between gap-2 flex-wrap">
<div className="flex items-center space-x-4">
<Input
size="small"
size="tiny"
className="w-52"
placeholder="Search for a role"
icon={<Search size={12} />}
value={filterString}
@@ -81,10 +82,10 @@ const RolesList = () => {
)
}
/>
<div className="flex items-center border border-strong rounded-full w-min h-[34px]">
<div className="flex items-center border border-strong rounded-full w-min h-[26px]">
<button
className={[
'text-xs w-[90px] h-full text-center rounded-l-full flex items-center justify-center transition',
'text-xs w-[80px] h-full text-center rounded-l-full flex items-center justify-center transition',
filterType === 'all'
? 'bg-overlay-hover text-foreground'
: 'hover:bg-surface-200 text-foreground-light',
@@ -96,7 +97,7 @@ const RolesList = () => {
<div className="h-full w-[1px] border-r border-strong"></div>
<button
className={[
'text-xs w-[90px] h-full text-center rounded-r-full flex items-center justify-center transition',
'text-xs w-[80px] h-full text-center rounded-r-full flex items-center justify-center transition',
filterType === 'active'
? 'bg-overlay-hover text-foreground'
: 'hover:bg-surface-200 text-foreground-light',
@@ -131,7 +132,9 @@ const RolesList = () => {
? `${totalActiveConnections}/${maxConnectionLimit}`
: `${totalActiveConnections}`
}
labelTopClass="text-xs"
labelBottom="Active connections"
labelBottomClass="text-xs"
/>
</div>
</Tooltip.Trigger>

View File

@@ -8,12 +8,12 @@ import 'reactflow/dist/style.css'
import { useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import AlertError from 'components/ui/AlertError'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import SchemaSelector from 'components/ui/SchemaSelector'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useTablesQuery } from 'data/tables/tables-query'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { Button, Tooltip_Shadcn_, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_ } from 'ui'
import { SchemaGraphLegend } from './SchemaGraphLegend'
import { getGraphDataFromTables, getLayoutedElementsViaDagre } from './Schemas.utils'
import { TableNode } from './SchemaTableNode'
@@ -117,22 +117,21 @@ export const SchemaGraph = () => {
{isSuccessSchemas && (
<>
<SchemaSelector
className="w-[260px]"
size="small"
className="w-[180px]"
size="tiny"
showError={false}
selectedSchemaName={selectedSchema}
onSelectSchema={setSelectedSchema}
/>
<Tooltip_Shadcn_>
<TooltipTrigger_Shadcn_ asChild>
<Button type="default" onClick={resetLayout}>
Auto layout
</Button>
</TooltipTrigger_Shadcn_>
<TooltipContent_Shadcn_ side="bottom">
Automatically arrange the layout of all nodes
</TooltipContent_Shadcn_>
</Tooltip_Shadcn_>
<ButtonTooltip
type="default"
onClick={resetLayout}
tooltip={{
content: { side: 'bottom', text: 'Automatically arrange the layout of all nodes' },
}}
>
Auto layout
</ButtonTooltip>
</>
)}
</div>

View File

@@ -203,11 +203,11 @@ const TableList = ({
}
return (
<div className="space-y-4">
<div className="flex items-center gap-2 flex-wrap">
<div className="flex flex-col gap-y-4">
<div className="flex items-center gap-x-2 flex-wrap">
<SchemaSelector
className="w-[260px]"
size="small"
className="w-[180px]"
size="tiny"
showError={false}
selectedSchemaName={selectedSchema}
onSelectSchema={setSelectedSchema}
@@ -215,8 +215,9 @@ const TableList = ({
<Popover_Shadcn_>
<PopoverTrigger_Shadcn_ asChild>
<Button
size="tiny"
type={visibleTypes.length !== 5 ? 'default' : 'dashed'}
className="py-4 px-2"
className="px-1"
icon={<Filter />}
/>
</PopoverTrigger_Shadcn_>
@@ -259,8 +260,8 @@ const TableList = ({
</Popover_Shadcn_>
<Input
size="small"
className="w-64"
size="tiny"
className="w-52"
placeholder="Search for a table"
value={filterString}
onChange={(e) => setFilterString(e.target.value)}
@@ -294,7 +295,7 @@ const TableList = ({
{isError && <AlertError error={error} subject="Failed to retrieve tables" />}
{isSuccess && (
<div className="my-4 w-full">
<div className="w-full">
<Table
head={[
<Table.th key="icon" className="!px-0" />,

View File

@@ -89,18 +89,18 @@ const TriggersList = ({
<div className="space-y-4">
<div className="flex items-center gap-2 flex-wrap">
<SchemaSelector
className="w-[260px]"
size="small"
className="w-[180px]"
size="tiny"
showError={false}
selectedSchemaName={selectedSchema}
onSelectSchema={setSelectedSchema}
/>
<Input
placeholder="Search for a trigger"
size="small"
size="tiny"
icon={<Search size="14" />}
value={filterString}
className="w-64"
className="w-52"
onChange={(e) => setFilterString(e.target.value)}
/>
{!isLocked && (

View File

@@ -1,5 +1,7 @@
import Link from 'next/link'
import { ReactMarkdown, ReactMarkdownOptions } from 'react-markdown/lib/react-markdown'
import remarkGfm from 'remark-gfm'
import { cn } from 'ui'
interface Props extends Omit<ReactMarkdownOptions, 'children' | 'node'> {
@@ -14,11 +16,17 @@ const Markdown = ({ className, content = '', extLinks = false, ...props }: Props
remarkPlugins={[remarkGfm]}
components={{
h3: ({ children }) => <h3 className="mb-1">{children}</h3>,
a: ({ href, children }) => (
<a href={href} target={extLinks ? '_blank' : ''} rel={extLinks ? 'noreferrer' : ''}>
{children}
</a>
),
a: ({ href, children }) => {
if (extLinks) {
return (
<a href={href} target="_blank" rel="noreferrer noopener">
{children}
</a>
)
} else {
return <Link href={href ?? '/'}>{children}</Link>
}
},
}}
{...props}
className={cn('prose text-sm', className)}

View File

@@ -1,11 +1,23 @@
import { PropsWithChildren } from 'react'
import { useAppStateSnapshot } from 'state/app-state'
import { cn } from 'ui'
/**
* Standardized padding and width layout for non-custom reports
*/
const ReportPadding = ({ children }: PropsWithChildren<{}>) => (
<div className="flex flex-col gap-4 px-5 py-6 mx-auto 1xl:px-28 lg:px-16 xl:px-24 2xl:px-32 w-full">
{children}
</div>
)
const ReportPadding = ({ children }: PropsWithChildren<{}>) => {
const { aiAssistantPanel } = useAppStateSnapshot()
const { open } = aiAssistantPanel
return (
<div
className={cn(
'flex flex-col gap-4 px-5 py-6 mx-auto 1xl:px-28 lg:px-16 2xl:px-32 w-full',
open ? 'xl:px-6' : 'xl:px-24'
)}
>
{children}
</div>
)
}
export default ReportPadding

View File

@@ -94,13 +94,13 @@ export const AiAssistantPanel = ({
// Use chat id because useChat doesn't have a reset function to clear all messages
const {
messages: chatMessages,
append,
isLoading,
append,
} = useChat({
id: chatId,
api: `${BASE_PATH}/api/ai/sql/generate-v2`,
body: {
existingSql: existingSql,
existingSql,
entityDefinitions: entityDefinitions,
},
})

View File

@@ -13,6 +13,8 @@ import { cn } from 'ui'
import { untitledSnippetTitle } from './SQLEditor.constants'
import type { IStandaloneCodeEditor } from './SQLEditor.types'
import { createSqlSnippetSkeletonV2 } from './SQLEditor.utils'
import { useIsAssistantV2Enabled } from '../App/FeaturePreview/FeaturePreviewContext'
import { useAppStateSnapshot } from 'state/app-state'
export type MonacoEditorProps = {
id: string
@@ -39,6 +41,9 @@ const MonacoEditor = ({
const project = useSelectedProject()
const snapV2 = useSqlEditorV2StateSnapshot()
const isAssistantV2Enabled = useIsAssistantV2Enabled()
const { setAiAssistantPanel } = useAppStateSnapshot()
const [intellisenseEnabled] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.SQL_EDITOR_INTELLISENSE,
true
@@ -69,6 +74,25 @@ const MonacoEditor = ({
},
})
if (isAssistantV2Enabled) {
editor.addAction({
id: 'explain-code',
label: 'Explain Code',
contextMenuGroupId: 'operation',
contextMenuOrder: 1,
run: () => {
const selectedValue = (editorRef?.current as any)
.getModel()
.getValueInRange((editorRef?.current as any)?.getSelection())
setAiAssistantPanel({
open: true,
sqlSnippets: [selectedValue],
initialInput: 'Can you explain this section to me in more detail?',
})
},
})
}
editor.onDidChangeCursorSelection(({ selection }) => {
const noSelection =
selection.startLineNumber === selection.endLineNumber &&

View File

@@ -8,6 +8,7 @@ import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
import { useQueryAbortMutation } from 'data/sql/abort-query-mutation'
import { useOngoingQueriesQuery } from 'data/sql/ongoing-queries-query'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { useUrlState } from 'hooks/ui/useUrlState'
import { IS_PLATFORM } from 'lib/constants'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import { ResponseError } from 'types'
@@ -33,6 +34,7 @@ interface OngoingQueriesPanel {
}
export const OngoingQueriesPanel = ({ visible, onClose }: OngoingQueriesPanel) => {
const [_, setParams] = useUrlState({ replace: true })
const project = useSelectedProject()
const state = useDatabaseSelectorStateSnapshot()
const [selectedId, setSelectedId] = useState<number>()
@@ -66,9 +68,14 @@ export const OngoingQueriesPanel = ({ visible, onClose }: OngoingQueriesPanel) =
},
})
const closePanel = () => {
setParams({ viewOngoingQueries: undefined })
onClose()
}
return (
<>
<Sheet open={visible} onOpenChange={() => onClose()}>
<Sheet open={visible} onOpenChange={() => closePanel()}>
<SheetContent size="lg">
<SheetHeader>
<SheetTitle className="flex items-center gap-x-2">

View File

@@ -52,9 +52,10 @@ import {
cn,
} from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { useIsAssistantV2Enabled } from '../App/FeaturePreview/FeaturePreviewContext'
import { subscriptionHasHipaaAddon } from '../Billing/Subscription/Subscription.utils'
import AISchemaSuggestionPopover from './AISchemaSuggestionPopover'
import { AiAssistantPanel } from './AiAssistantPanel'
import AISchemaSuggestionPopover from './AISchemaSuggestionPopover'
import { DiffActionBar } from './DiffActionBar'
import {
ROWS_PER_PAGE_OPTIONS,
@@ -87,8 +88,8 @@ const DiffEditor = dynamic(
)
const SQLEditor = () => {
const { ref, id: urlId } = useParams()
const router = useRouter()
const { ref, id: urlId } = useParams()
// generate an id to be used for new snippets. The dependency on urlId is to avoid a bug which
// shows up when clicking on the SQL Editor while being in the SQL editor on a random snippet.
@@ -105,11 +106,13 @@ const SQLEditor = () => {
const databaseSelectorState = useDatabaseSelectorStateSnapshot()
const queryClient = useQueryClient()
const isAssistantV2Enabled = useIsAssistantV2Enabled()
const { open } = appSnap.aiAssistantPanel
const { mutate: formatQuery } = useFormatQueryMutation()
const { mutateAsync: generateSqlTitle } = useSqlTitleGenerateMutation()
const { mutateAsync: debugSql, isLoading: isDebugSqlLoading } = useSqlDebugMutation()
const [selectedMessage, setSelectedMessage] = useState<string>()
const [debugSolution, setDebugSolution] = useState<string>()
const [sourceSqlDiff, setSourceSqlDiff] = useState<ContentDiff>()
const [pendingTitle, setPendingTitle] = useState<string>()
@@ -118,6 +121,7 @@ const SQLEditor = () => {
const editorRef = useRef<IStandaloneCodeEditor | null>(null)
const monacoRef = useRef<Monaco | null>(null)
const diffEditorRef = useRef<IStandaloneDiffEditor | null>(null)
const aiPanelRef = useRef<ImperativePanelHandle>(null)
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug })
const { data: databases, isSuccess: isSuccessReadReplicas } = useReadReplicasQuery({
@@ -127,7 +131,9 @@ const SQLEditor = () => {
// Customers on HIPAA plans should not have access to Supabase AI
const hasHipaaAddon = subscriptionHasHipaaAddon(subscription)
// [Joshen] This eventually needs to be in the app state
const [isAiOpen, setIsAiOpen] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.SQL_EDITOR_AI_OPEN, true)
const isAssistantOpen = (!isAssistantV2Enabled && isAiOpen) || (isAssistantV2Enabled && open)
const [showPotentialIssuesModal, setShowPotentialIssuesModal] = useState(false)
const [queryHasDestructiveOperations, setQueryHasDestructiveOperations] = useState(false)
@@ -376,7 +382,7 @@ const SQLEditor = () => {
)
const updateEditorWithCheckForDiff = useCallback(
({ id, diffType, sql }: { id: string; diffType: DiffType; sql: string }) => {
({ diffType, sql }: { diffType: DiffType; sql: string }) => {
const editorModel = editorRef.current?.getModel()
if (!editorModel) return
@@ -392,7 +398,6 @@ const SQLEditor = () => {
},
])
} else {
setSelectedMessage(id)
const currentSql = editorRef.current?.getValue()
const diff = { original: currentSql || '', modified: sql }
setSourceSqlDiff(diff)
@@ -406,26 +411,33 @@ const SQLEditor = () => {
try {
const snippet = snapV2.snippets[id]
const result = snapV2.results[id]?.[0]
const { solution, sql } = await debugSql({
sql: snippet.snippet.content.sql.replace(sqlAiDisclaimerComment, '').trim(),
errorMessage: result.error.message,
entityDefinitions,
})
const formattedSql =
sqlAiDisclaimerComment +
'\n\n' +
format(sql, {
language: 'postgresql',
keywordCase: 'lower',
if (isAssistantV2Enabled) {
appSnap.setAiAssistantPanel({
open: true,
sqlSnippets: [snippet.snippet.content.sql.replace(sqlAiDisclaimerComment, '').trim()],
initialInput: `Help me to debug the attached sql snippet which gives the following error: \n\n${result.error.message}`,
})
setDebugSolution(solution)
setSourceSqlDiff({
original: snippet.snippet.content.sql,
modified: formattedSql,
})
setSelectedDiffType(DiffType.Modification)
} else {
const { solution, sql } = await debugSql({
sql: snippet.snippet.content.sql.replace(sqlAiDisclaimerComment, '').trim(),
errorMessage: result.error.message,
entityDefinitions,
})
const formattedSql =
sqlAiDisclaimerComment +
'\n\n' +
format(sql, {
language: 'postgresql',
keywordCase: 'lower',
})
setDebugSolution(solution)
setSourceSqlDiff({
original: snippet.snippet.content.sql,
modified: formattedSql,
})
setSelectedDiffType(DiffType.Modification)
}
} catch (error: unknown) {
// [Joshen] There's a tendency for the SQL debug to chuck a lengthy error message
// that's not relevant for the user - so we prettify it here by avoiding to return the
@@ -482,7 +494,6 @@ const SQLEditor = () => {
label: debugSolution ? 'debug_snippet' : 'edit_snippet',
})
setSelectedMessage(undefined)
setSelectedDiffType(DiffType.Modification)
setDebugSolution(undefined)
setSourceSqlDiff(undefined)
@@ -509,7 +520,6 @@ const SQLEditor = () => {
label: debugSolution ? 'debug_snippet' : 'edit_snippet',
})
setSelectedMessage(undefined)
setDebugSolution(undefined)
setSourceSqlDiff(undefined)
setPendingTitle(undefined)
@@ -588,6 +598,12 @@ const SQLEditor = () => {
}
}, [isSuccessReadReplicas, databases, ref])
useEffect(() => {
if (snapV2.diffContent !== undefined) {
updateEditorWithCheckForDiff(snapV2.diffContent)
}
}, [snapV2.diffContent])
const defaultSqlDiff = useMemo(() => {
if (!sourceSqlDiff) {
return { original: '', modified: '' }
@@ -610,8 +626,6 @@ const SQLEditor = () => {
}
}, [selectedDiffType, sourceSqlDiff])
const aiPanelRef = useRef<ImperativePanelHandle>(null)
return (
<>
<ConfirmationModal
@@ -679,7 +693,7 @@ const SQLEditor = () => {
direction="vertical"
autoSaveId={LOCAL_STORAGE_KEYS.SQL_EDITOR_SPLIT_SIZE}
>
{(isAiOpen || isDiffOpen) && !hasHipaaAddon && (
{(isAssistantOpen || isDiffOpen) && !hasHipaaAddon && (
<AISchemaSuggestionPopover
onClickSettings={() => {
appSnap.setShowAiSettingsModal(true)
@@ -715,14 +729,36 @@ const SQLEditor = () => {
)}
<ResizablePanel maxSize={70}>
<div className="flex-grow overflow-y-auto border-b h-full">
{!isAiOpen && (
{!isAssistantV2Enabled && !isAiOpen && (
<motion.button
layoutId="ask-ai-input-icon"
transition={{ duration: 0.1 }}
onClick={() => aiPanelRef.current?.expand()}
className={cn(
'group absolute z-10 rounded-lg right-[24px] top-4 transition-all duration-200 ease-out'
)}
className="group absolute z-10 rounded-lg right-[24px] top-4 transition-all duration-200 ease-out"
onClick={() => {
if (isAssistantV2Enabled) {
const state = getSqlEditorV2StateSnapshot()
const snippet = state.snippets[id]
const editor = editorRef.current
const selection = editor?.getSelection()
const selectedValue = selection
? editor?.getModel()?.getValueInRange(selection)
: undefined
const sql = snippet
? (selectedValue || editorRef.current?.getValue()) ??
snippet.snippet.content.sql
: selectedValue || editorRef.current?.getValue()
appSnap.setAiAssistantPanel({
open: true,
sqlSnippets: sql ? [sql] : [],
initialInput: sql
? `Help me make a change to the attached sql snippet`
: '',
})
} else {
aiPanelRef.current?.expand()
}
}}
>
<AiIconAnimation loading={false} allowHoverEffect />
</motion.button>
@@ -860,25 +896,27 @@ const SQLEditor = () => {
</ResizablePanelGroup>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
ref={aiPanelRef}
collapsible
collapsedSize={0}
minSize={31}
maxSize={40}
onCollapse={() => setIsAiOpen(false)}
onExpand={() => setIsAiOpen(true)}
>
<AiAssistantPanel
selectedMessage={selectedMessage}
existingSql={editorRef.current?.getValue() || ''}
includeSchemaMetadata={includeSchemaMetadata}
onDiff={updateEditorWithCheckForDiff}
onClose={() => aiPanelRef.current?.collapse()}
/>
</ResizablePanel>
{!isAssistantV2Enabled && (
<>
<ResizableHandle withHandle />
<ResizablePanel
ref={aiPanelRef}
collapsible
collapsedSize={0}
minSize={31}
maxSize={40}
onCollapse={() => setIsAiOpen(false)}
onExpand={() => setIsAiOpen(true)}
>
<AiAssistantPanel
existingSql={editorRef.current?.getValue() || ''}
includeSchemaMetadata={includeSchemaMetadata}
onDiff={updateEditorWithCheckForDiff}
onClose={() => aiPanelRef.current?.collapse()}
/>
</ResizablePanel>
</>
)}
</ResizablePanelGroup>
</>
)

View File

@@ -0,0 +1,53 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useRouter } from 'next/router'
import { toast } from 'sonner'
import { useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { uuidv4 } from 'lib/helpers'
import { useProfile } from 'lib/profile'
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
import { createSqlSnippetSkeletonV2 } from './SQLEditor.utils'
export const useNewQuery = () => {
const router = useRouter()
const { ref } = useParams()
const { profile } = useProfile()
const { project } = useProjectContext()
const snapV2 = useSqlEditorV2StateSnapshot()
const canCreateSQLSnippet = useCheckPermissions(PermissionAction.CREATE, 'user_content', {
resource: { type: 'sql', owner_id: profile?.id },
subject: { id: profile?.id },
})
const newQuery = async (sql: string, name: string) => {
if (!ref) return console.error('Project ref is required')
if (!project) return console.error('Project is required')
if (!profile) return console.error('Profile is required')
if (!canCreateSQLSnippet) {
return toast('Your queries will not be saved as you do not have sufficient permissions')
}
try {
const snippet = createSqlSnippetSkeletonV2({
id: uuidv4(),
name,
sql,
owner_id: profile?.id,
project_id: project?.id,
})
snapV2.addSnippet({ projectRef: ref, snippet })
snapV2.addNeedsSaving(snippet.id)
router.push(`/project/${ref}/sql/${snippet.id}`)
} catch (error: any) {
toast.error(`Failed to create new query: ${error.message}`)
}
}
return { newQuery }
}
export default useNewQuery

View File

@@ -94,8 +94,8 @@ const EncryptionKeysManagement = () => {
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Input
className="w-64 input-clear"
size="small"
className="w-52 input-clear"
size="tiny"
placeholder="Search by name or ID"
value={searchValue}
onChange={(event) => setSearchValue(event.target.value)}
@@ -116,7 +116,7 @@ const EncryptionKeysManagement = () => {
}
/>
<div className="w-44">
<Listbox size="small" value={selectedSort} onChange={setSelectedSort}>
<Listbox size="tiny" value={selectedSort} onChange={setSelectedSort}>
<Listbox.Option
id="created"
className="max-w-[180px]"

View File

@@ -60,8 +60,8 @@ const SecretsManagement = () => {
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Input
className="w-64"
size="small"
className="w-52"
size="tiny"
placeholder="Search by name or key ID"
value={searchValue}
onChange={(event) => setSearchValue(event.target.value)}
@@ -82,7 +82,7 @@ const SecretsManagement = () => {
}
/>
<div className="w-44">
<Listbox size="small" value={selectedSort} onChange={setSelectedSort}>
<Listbox size="tiny" value={selectedSort} onChange={setSelectedSort}>
<Listbox.Option
id="updated_at"
className="max-w-[180px]"

View File

@@ -13,6 +13,7 @@ import OrganizationDropdown from './OrganizationDropdown'
import ProjectDropdown from './ProjectDropdown'
import SettingsButton from './SettingsButton'
import UserSettingsDropdown from './UserSettingsDropdown'
import AssistantButton from './AssistantButton'
// [Joshen] Just FYI this is only for Nav V2 which is still going through design iteration
// Component is not currently in use
@@ -53,6 +54,7 @@ const AppHeader = () => {
<FeedbackDropdown />
<NotificationsPopoverV2 />
<HelpPopover />
<AssistantButton />
<SettingsButton />
</div>
<div className="flex items-center gap-3">

View File

@@ -0,0 +1,20 @@
import { useAppStateSnapshot } from 'state/app-state'
import { AiIconAnimation, Button } from 'ui'
const AssistantButton = () => {
const { setAiAssistantPanel, aiAssistantPanel } = useAppStateSnapshot()
return (
<Button
type="text"
id="assistant-trigger"
className="w-6 h-6"
onClick={() => {
setAiAssistantPanel({ open: !aiAssistantPanel.open })
}}
icon={<AiIconAnimation allowHoverEffect className="w-4 h-4 text-foreground-light" />}
/>
)
}
export default AssistantButton

View File

@@ -17,11 +17,14 @@ import BreadcrumbsView from './BreadcrumbsView'
import { FeedbackDropdown } from './FeedbackDropdown'
import HelpPopover from './HelpPopover'
import NotificationsPopoverV2 from './NotificationsPopoverV2/NotificationsPopover'
import AssistantButton from 'components/layouts/AppLayout/AssistantButton'
import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = true }: any) => {
const { ref: projectRef } = useParams()
const selectedProject = useSelectedProject()
const selectedOrganization = useSelectedOrganization()
const isAssistantV2Enabled = useIsAssistantV2Enabled()
const isBranchingEnabled = selectedProject?.is_branch_enabled === true
const { data: subscription } = useOrgSubscriptionQuery({
@@ -117,6 +120,7 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
<FeedbackDropdown />
<NotificationsPopoverV2 />
<HelpPopover />
{isAssistantV2Enabled && !!projectRef && <AssistantButton />}
</>
)}
</div>

View File

@@ -9,7 +9,7 @@ import { useState } from 'react'
import {
useIsAPIDocsSidePanelEnabled,
useIsDatabaseFunctionsAssistantEnabled,
useIsAssistantV2Enabled,
} from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useProjectLintsQuery } from 'data/lint/lint-query'
import { ProjectIndexPageLink } from 'data/prefetchers/project.$ref'
@@ -73,7 +73,7 @@ const NavigationBar = () => {
const navLayoutV2 = useFlag('navigationLayoutV2')
const isNewAPIDocsEnabled = useIsAPIDocsSidePanelEnabled()
const isFunctionsAssistantEnabled = useIsDatabaseFunctionsAssistantEnabled()
const isFunctionsAssistantEnabled = useIsAssistantV2Enabled()
const [userDropdownOpen, setUserDropdownOpenState] = useState(false)
const [allowNavPanelToExpand] = useLocalStorageQuery(
@@ -181,7 +181,9 @@ const NavigationBar = () => {
}
label="Assistant"
shortcut="I"
onClick={() => snap.setAiAssistantPanel({ open: true, editor: null })}
onClick={() => {
snap.setAiAssistantPanel({ open: !snap.aiAssistantPanel.open })
}}
/>
</HoverCardContent_Shadcn_>
</HoverCard_Shadcn_>

View File

@@ -31,6 +31,9 @@ import RestoreFailedState from './RestoreFailedState'
import RestoringState from './RestoringState'
import { UpgradingState } from './UpgradingState'
import { ResizingState } from './ResizingState'
import { AiAssistantPanel } from 'components/ui/AIAssistantPanel/AIAssistantPanel'
import { useAppStateSnapshot } from 'state/app-state'
import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
// [Joshen] This is temporary while we unblock users from managing their project
// if their project is not responding well for any reason. Eventually needs a bit of an overhaul
@@ -85,10 +88,14 @@ const ProjectLayout = ({
const { ref: projectRef } = useParams()
const selectedOrganization = useSelectedOrganization()
const selectedProject = useSelectedProject()
const projectName = selectedProject?.name
const organizationName = selectedOrganization?.name
const { aiAssistantPanel, setAiAssistantPanel } = useAppStateSnapshot()
const { open } = aiAssistantPanel
const navLayoutV2 = useFlag('navigationLayoutV2')
const isAssistantV2Enabled = useIsAssistantV2Enabled()
const projectName = selectedProject?.name
const organizationName = selectedOrganization?.name
const isPaused = selectedProject?.status === PROJECT_STATUS.INACTIVE
const showProductMenu = selectedProject
@@ -101,6 +108,15 @@ const ProjectLayout = ({
router.pathname === '/project/[ref]' || router.pathname.includes('/project/[ref]/settings')
const showPausedState = isPaused && !ignorePausedState
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && e.code === 'KeyI') setAiAssistantPanel({ open: !open })
}
if (isAssistantV2Enabled) window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isAssistantV2Enabled, open])
return (
<AppLayout>
<ProjectContextProvider projectRef={projectRef}>
@@ -149,20 +165,39 @@ const ProjectLayout = ({
/>
<ResizablePanel id="panel-right" className="h-full flex flex-col">
{!navLayoutV2 && !hideHeader && IS_PLATFORM && <LayoutHeader />}
<main className="h-full flex flex-col flex-1 w-full overflow-x-hidden">
{showPausedState ? (
<div className="mx-auto my-16 w-full h-full max-w-7xl flex items-center">
<div className="w-full">
<ProjectPausedState product={product} />
</div>
</div>
) : (
<ContentWrapper isLoading={isLoading} isBlocking={isBlocking}>
<ResourceExhaustionWarningBanner />
{children}
</ContentWrapper>
<ResizablePanelGroup
className="h-full w-full overflow-x-hidden flex-1"
direction="horizontal"
autoSaveId="project-layout-content"
>
<ResizablePanel id="panel-content" className=" w-full min-w-[600px]">
<main className="h-full flex flex-col flex-1 w-full overflow-x-hidden">
{showPausedState ? (
<div className="mx-auto my-16 w-full h-full max-w-7xl flex items-center">
<div className="w-full">
<ProjectPausedState product={product} />
</div>
</div>
) : (
<ContentWrapper isLoading={isLoading} isBlocking={isBlocking}>
<ResourceExhaustionWarningBanner />
{children}
</ContentWrapper>
)}
</main>
</ResizablePanel>
{aiAssistantPanel.open && (
<>
<ResizableHandle />
<ResizablePanel
id="panel-assistant"
className="min-w-[400px] max-w-[500px] bg 2xl:max-w-[600px] xl:relative xl:top-0 absolute right-0 top-[48px] bottom-0"
>
<AiAssistantPanel />
</ResizablePanel>
</>
)}
</main>
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>
</div>

View File

@@ -1,8 +1,10 @@
import { ReactNode, useEffect, useMemo, useState } from 'react'
import { OngoingQueriesPanel } from 'components/interfaces/SQLEditor/OngoingQueriesPanel'
import { withAuth } from 'hooks/misc/withAuth'
import { ReactNode, useMemo, useState } from 'react'
import ProjectLayout from '../ProjectLayout/ProjectLayout'
import { SQLEditorMenu } from './SQLEditorMenu'
import { useParams } from 'common'
export interface SQLEditorLayoutProps {
title: string
@@ -10,7 +12,9 @@ export interface SQLEditorLayoutProps {
}
const SQLEditorLayout = ({ title, children }: SQLEditorLayoutProps) => {
const { viewOngoingQueries } = useParams()
const [showOngoingQueries, setShowOngoingQueries] = useState(false)
const productMenu = useMemo(
() => (
<SQLEditorMenu
@@ -21,6 +25,10 @@ const SQLEditorLayout = ({ title, children }: SQLEditorLayoutProps) => {
[]
)
useEffect(() => {
if (viewOngoingQueries === 'true') setShowOngoingQueries(true)
}, [viewOngoingQueries])
return (
<ProjectLayout
title={title || 'SQL'}

View File

@@ -1,4 +1,5 @@
import React from 'react'
import { useAppStateSnapshot } from 'state/app-state'
import { cn } from 'ui'
export const MAX_WIDTH_CLASSES = 'mx-auto w-full max-w-[1200px]'
@@ -35,11 +36,19 @@ const ScaffoldContainer = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & { bottomPadding?: boolean }
>(({ className, bottomPadding, ...props }, ref) => {
const { aiAssistantPanel } = useAppStateSnapshot()
const { open } = aiAssistantPanel
return (
<div
ref={ref}
{...props}
className={cn(MAX_WIDTH_CLASSES, PADDING_CLASSES, bottomPadding && 'pb-16', className)}
className={cn(
MAX_WIDTH_CLASSES,
PADDING_CLASSES,
bottomPadding && 'pb-16',
open ? 'xl:px-6' : '',
className
)}
/>
)
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +1,17 @@
import { authKeys } from 'data/auth/keys'
import { databaseExtensionsKeys } from 'data/database-extensions/keys'
import { databasePoliciesKeys } from 'data/database-policies/keys'
import { databaseTriggerKeys } from 'data/database-triggers/keys'
import { databaseKeys } from 'data/database/keys'
import { enumeratedTypesKeys } from 'data/enumerated-types/keys'
import { tableKeys } from 'data/tables/keys'
import { CommonDatabaseEntity } from 'state/app-state'
import { SupportedAssistantEntities, SupportedAssistantQuickPromptTypes } from './AIAssistant.types'
import { SupportedAssistantEntities } from './AIAssistant.types'
const PLACEHOLDER_PREFIX = `-- Press tab to use this code
\n&nbsp;\n`
const PLACEHOLDER_LIMIT = `Just three examples will do.`
export const generateTitle = (
editor?: SupportedAssistantEntities | null,
entity?: CommonDatabaseEntity
) => {
switch (editor) {
case 'functions':
if (entity === undefined) return 'Create a new function'
else return `Edit function: ${entity.name}`
case 'rls-policies':
if (entity === undefined) return 'Create a new RLS policy'
else return `Edit RLS policy: ${entity.name}`
default:
return 'SQL Scratch Pad'
}
}
export const generateCTA = (editor?: SupportedAssistantEntities | null) => {
switch (editor) {
case 'functions':
return 'Save function'
case 'rls-policies':
return 'Save policy'
default:
return 'Run query'
}
}
// [Joshen] Not used but keeping this for now in case we do an inline editor
export const generatePlaceholder = (
editor?: SupportedAssistantEntities | null,
entity?: CommonDatabaseEntity,
@@ -117,68 +96,91 @@ COMMIT;
}
}
export const retrieveDocsUrl = (editor?: SupportedAssistantEntities | null) => {
switch (editor) {
case 'functions':
return 'https://supabase.com/docs/guides/database/functions'
case 'rls-policies':
return 'https://supabase.com/docs/guides/database/postgres/row-level-security'
default:
return undefined
}
}
// [Joshen] This is just very basic validation, but possible can extend perhaps
export const validateQuery = (editor: SupportedAssistantEntities | null, query: string) => {
// [Joshen] This is just very basic identification, but possible can extend perhaps
export const identifyQueryType = (query: string) => {
const formattedQuery = query.toLowerCase().replaceAll('\n', ' ')
switch (editor) {
case 'functions':
return (
formattedQuery.includes('create function') ||
formattedQuery.includes('create or replace function')
)
case 'rls-policies':
return formattedQuery.includes('create policy') || formattedQuery.includes('alter policy')
default:
return true
if (
formattedQuery.includes('create function') ||
formattedQuery.includes('create or replace function')
) {
return 'functions'
} else if (formattedQuery.includes('create policy') || formattedQuery.includes('alter policy')) {
return 'rls-policies'
}
}
export const generatePrompt = ({
type,
context,
schemas,
tables,
}: {
type: SupportedAssistantQuickPromptTypes
context: SupportedAssistantEntities
schemas: string[]
tables: readonly { schema: string; name: string }[]
}) => {
if (type === 'examples') {
return `What are some common examples of user-defined database ${context}? ${PLACEHOLDER_LIMIT}`
} else if (type === 'ask') {
return `Could you explain to me what are used-defined database ${context}?`
} else if (type === 'suggest') {
const output =
context === 'functions'
? 'user-defined database functions'
: context === 'rls-policies'
? 'RLS policies'
: ''
export const isReadOnlySelect = (query: string): boolean => {
const normalizedQuery = query.trim().toLowerCase()
const suffix =
context === 'functions' ? 'Let me know for which tables each function will be useful' : ''
// Check if it starts with SELECT
if (!normalizedQuery.startsWith('select')) {
return false
}
const basePrompt = `Suggest some ${output} that might be useful`
// List of keywords that indicate write operations or function calls
const disallowedPatterns = [
// Write operations
'insert',
'update',
'delete',
'alter',
'drop',
'create',
'truncate',
'replace',
'with',
if (tables.length > 0 && schemas.length > 0) {
return `${basePrompt} for the following tables within this database: ${tables.map((x) => `${x.schema}.${x.name}`)}. ${PLACEHOLDER_LIMIT} ${suffix}`.trim()
} else if (schemas.length > 0) {
return `${basePrompt} for the tables in the following schemas within this database: ${schemas.join(', ')}. ${suffix}`.trim()
// Function patterns
'function',
'procedure',
]
const allowedPatterns = ['created', 'inserted', 'updated', 'deleted']
// Check if query contains any disallowed patterns, but allow if part of allowedPatterns
return !disallowedPatterns.some((pattern) => {
// Check if the found disallowed pattern is actually part of an allowed pattern
const isPartOfAllowedPattern = allowedPatterns.some(
(allowed) => normalizedQuery.includes(allowed) && allowed.includes(pattern)
)
if (isPartOfAllowedPattern) {
return false
}
return basePrompt
}
return normalizedQuery.includes(pattern)
})
}
const getContextKey = (pathname: string) => {
const [_, __, ___, ...rest] = pathname.split('/')
const key = rest.join('/')
return key
}
export const getContextualInvalidationKeys = ({
ref,
pathname,
schema = 'public',
}: {
ref: string
pathname: string
schema?: string
}) => {
const key = getContextKey(pathname)
return (
(
{
'auth/users': [authKeys.usersInfinite(ref)],
'auth/policies': [databasePoliciesKeys.list(ref)],
'database/functions': [databaseKeys.databaseFunctions(ref)],
'database/tables': [tableKeys.list(ref, schema, true), tableKeys.list(ref, schema, false)],
'database/triggers': [databaseTriggerKeys.list(ref)],
'database/types': [enumeratedTypesKeys.list(ref)],
'database/extensions': [databaseExtensionsKeys.list(ref)],
'database/indexes': [databaseKeys.indexes(ref, schema)],
} as const
)[key] ?? []
)
}

View File

@@ -1,464 +1,43 @@
import { useQueryClient } from '@tanstack/react-query'
import { uniqBy } from 'lodash'
import { Command, CornerDownLeft, Loader2, X } from 'lucide-react'
import { useRouter } from 'next/router'
import { useCallback, useEffect, useRef, useState } from 'react'
import { toast } from 'sonner'
import { useChat } from 'ai/react'
import { useParams } from 'common'
import { useIsDatabaseFunctionsAssistantEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { generateThreadMessage } from 'components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyEditorPanel.utils'
import { MessageWithDebug } from 'components/interfaces/SQLEditor/AiAssistantPanel'
import { sqlAiDisclaimerComment } from 'components/interfaces/SQLEditor/SQLEditor.constants'
import { DiffType, IStandaloneCodeEditor } from 'components/interfaces/SQLEditor/SQLEditor.types'
import { suffixWithLimit } from 'components/interfaces/SQLEditor/SQLEditor.utils'
import Results from 'components/interfaces/SQLEditor/UtilityPanel/Results'
import { useSqlDebugMutation } from 'data/ai/sql-debug-mutation'
import { databasePoliciesKeys } from 'data/database-policies/keys'
import { useEntityDefinitionQuery } from 'data/database/entity-definition-query'
import { databaseKeys } from 'data/database/keys'
import { QueryResponseError, useExecuteSqlMutation } from 'data/sql/execute-sql-mutation'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { usePrevious } from 'hooks/deprecated'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { SqlEditor } from 'icons'
import {
BASE_PATH,
LOCAL_STORAGE_KEYS,
TELEMETRY_ACTIONS,
TELEMETRY_CATEGORIES,
TELEMETRY_LABELS,
} from 'lib/constants'
import { detectOS, uuidv4 } from 'lib/helpers'
import { uuidv4 } from 'lib/helpers'
import { useState, useEffect } from 'react'
import { useAppStateSnapshot } from 'state/app-state'
import {
Button,
cn,
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
Tooltip_Shadcn_,
TooltipContent_Shadcn_,
TooltipTrigger_Shadcn_,
} from 'ui'
import { Admonition } from 'ui-patterns'
import CodeEditor from '../CodeEditor/CodeEditor'
import { cn } from 'ui'
import { AIAssistant } from './AIAssistant'
import { generateCTA, generatePlaceholder, generateTitle, validateQuery } from './AIAssistant.utils'
import { ASSISTANT_SUPPORT_ENTITIES } from './AiAssistant.constants'
import type { Message as MessageType } from 'ai/react'
export const AiAssistantPanel = () => {
const os = detectOS()
const router = useRouter()
const { ref } = useParams()
const project = useSelectedProject()
const queryClient = useQueryClient()
const { aiAssistantPanel, setAiAssistantPanel } = useAppStateSnapshot()
const isEnabled = useIsDatabaseFunctionsAssistantEnabled()
const { aiAssistantPanel, resetAiAssistantPanel } = useAppStateSnapshot()
const [initialMessages, setInitialMessages] = useState<MessageType[] | undefined>(undefined)
const { open, editor, content, entity } = aiAssistantPanel
const previousEditor = usePrevious(editor)
const previousEntity = usePrevious(entity)
const [isAcknowledged, setIsAcknowledged] = useLocalStorage(
LOCAL_STORAGE_KEYS.SQL_SCRATCH_PAD_BANNER_ACKNOWLEDGED,
false
)
useEffect(() => {
// set initial state of local messages to the global state if it exists
if (aiAssistantPanel.messages) {
const messagesCopy = aiAssistantPanel.messages.map((msg) => ({
content: msg.content,
createdAt: msg.createdAt,
role: msg.role,
id: msg.id,
}))
setInitialMessages(messagesCopy)
}
}, [])
const { open } = aiAssistantPanel
const [chatId, setChatId] = useState(() => uuidv4())
const [error, setError] = useState<QueryResponseError>()
const [results, setResults] = useState<undefined | any[]>(undefined)
const [showResults, setShowResults] = useState(false)
const [showWarning, setShowWarning] = useState<boolean>(false)
const [debugThread, setDebugThread] = useState<MessageWithDebug[]>([])
const { data: existingDefinition } = useEntityDefinitionQuery({
id: entity?.id,
type: editor,
projectRef: project?.ref,
connectionString: project?.connectionString,
})
// [Joshen] JFYI I'm opting to just have the assistant always show and not toggle-able
// I don't really see any negatives of keeping it open (or benefits from hiding) tbh
// const [showAssistant, setShowAssistant] = useState(true)
const title = generateTitle(editor, entity)
const placeholder = generatePlaceholder(editor, entity, existingDefinition)
const ctaText = generateCTA(editor)
const editorRef = useRef<IStandaloneCodeEditor | undefined>()
const editorModel = editorRef.current?.getModel()
const numResults = (results ?? []).length
const [errorHeader, ...errorContent] =
(error?.formattedError?.split('\n') ?? [])?.filter((x: string) => x.length > 0) ?? []
const entityContext = ASSISTANT_SUPPORT_ENTITIES.find((x) => x.id === editor)
const { mutate: sendEvent } = useSendEventMutation()
const sendTelemetryEvent = (action: string) => {
sendEvent({
action,
category: TELEMETRY_CATEGORIES.AI_ASSISTANT,
label: TELEMETRY_LABELS.QUICK_SQL_EDITOR,
})
const handleReset = () => {
// reset local and global state
setChatId(uuidv4())
setInitialMessages(undefined)
resetAiAssistantPanel()
}
const { mutate: executeSql, isLoading: isExecuting } = useExecuteSqlMutation({
onSuccess: async (res) => {
// [Joshen] If in a specific editor context mode, assume that intent was to create/update
// a database entity - so close it once success. Otherwise it's in Quick SQL mode and we
// show the results.
if (editor !== null) {
switch (editor) {
case 'functions':
await queryClient.invalidateQueries(databaseKeys.databaseFunctions(ref))
break
case 'rls-policies':
await queryClient.invalidateQueries(databasePoliciesKeys.list(ref))
break
}
toast.success(
`Successfully ${entity === undefined ? 'created' : 'updated'} ${entityContext?.name}!`
)
setAiAssistantPanel({ open: false })
} else {
setShowResults(true)
setResults(res.result)
}
},
onError: (error) => {
setError(error)
setResults([])
},
})
const { append } = useChat({
id: chatId,
api: `${BASE_PATH}/api/ai/sql/generate-v2`,
// [Joshen] Don't need entity definitions if calling here cause support action
// via AI here is just to explain code segment, no need for entity definitions context
body: {},
})
const { mutateAsync: debugSql } = useSqlDebugMutation({ onError: () => {} })
const onExecuteSql = (skipValidation = false) => {
setError(undefined)
setShowWarning(false)
const query = editorRef.current?.getValue() ?? ''
if (query.length === 0) return
if (editor !== undefined && !skipValidation) {
// [Joshen] Some basic validation logic
const validated = validateQuery(editor, query)
if (!validated) {
return setShowWarning(true)
}
}
executeSql({
sql: suffixWithLimit(query, 100),
projectRef: project?.ref,
connectionString: project?.connectionString,
handleError: (error) => {
throw error
},
})
}
const onFixWithAssistant = async () => {
if (!error) return
const messageId = uuidv4()
const query = editorRef.current?.getValue() ?? ''
const assistantMessageBefore = generateThreadMessage({
id: messageId,
content: 'Thinking...',
isDebug: true,
})
setDebugThread([...debugThread, assistantMessageBefore])
sendTelemetryEvent(TELEMETRY_ACTIONS.FIX_WITH_ASSISTANT)
try {
const { solution, sql } = await debugSql({
sql: query,
errorMessage: error.message,
})
const assistantMessageAfter = generateThreadMessage({
id: messageId,
content: `${solution}\n\`\`\`sql\n${sql}\n\`\`\``,
isDebug: true,
})
const cleanedMessages = uniqBy([...debugThread, assistantMessageAfter], (m) => m.id)
setDebugThread(cleanedMessages)
} catch (error) {
const assistantMessageAfter = generateThreadMessage({
id: messageId,
content: `Hmm, Sorry but I'm unable to find a solution for the error that you're facing`,
isDebug: true,
})
const cleanedMessages = uniqBy([...debugThread, assistantMessageAfter], (m) => m.id)
setDebugThread(cleanedMessages)
}
}
const onExplainSql = (value: string) => {
append({
role: 'user',
createdAt: new Date(),
content: `
Can you explain this section to me in more detail?\n
${value}
`.trim(),
})
sendTelemetryEvent(TELEMETRY_ACTIONS.EXPLAIN_CODE)
}
const updateEditorWithCheckForDiff = useCallback(
({ id, diffType, sql }: { id: string; diffType: DiffType; sql: string }) => {
if (!editorModel) return
const existingValue = editorRef.current?.getValue() ?? ''
if (existingValue.length === 0) {
// if the editor is empty, just copy over the code
editorRef.current?.executeEdits('apply-ai-message', [
{
text: `${sqlAiDisclaimerComment}\n\n${sql}`,
range: editorModel.getFullModelRange(),
},
])
} else {
const currentSql = editorRef.current?.getValue()
editorRef.current?.executeEdits('apply-ai-message', [
{
text: `${currentSql}\n\n${sqlAiDisclaimerComment}\n\n${sql}`,
range: editorModel.getFullModelRange(),
},
])
}
},
[editorModel]
)
const onTogglePanel = () => {
if (open) {
// [Joshen] Save the code content when closing - should allow users to continue from
// where they left off IF they are opening the assistant again in the same "editor" context
const existingValue = editorRef.current?.getValue() ?? ''
setAiAssistantPanel({ open: false, content: existingValue })
} else {
setAiAssistantPanel({ open: true, editor: null, entity: undefined })
}
}
useEffect(() => {
if (open) {
// [Joshen] Only reset the assistant if the editor changed or if the provided entity has changed
if (previousEditor !== editor || previousEntity !== entity) {
setChatId(uuidv4())
setError(undefined)
setShowWarning(false)
setDebugThread([])
setResults(undefined)
setShowResults(false)
} else {
// [Joshen] Repopulate the code editor with the content from where the user left off
// setTimeout is just to give time for the editorRef to get associated
setTimeout(() => {
const editorModel = editorRef.current?.getModel()
if (content && editorModel) {
editorRef.current?.executeEdits('apply-ai-message', [
{ text: content, range: editorModel.getFullModelRange() },
])
}
}, 100)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open])
// [Joshen] Just to test the concept of a universal assistant of sorts
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && e.code === 'KeyI') onTogglePanel()
}
if (project !== undefined && isEnabled) window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [project, isEnabled, open])
// [Joshen] Whenever the deps change recalculate the height of the editor
useEffect(() => {
editorRef.current?.layout({ width: 0, height: 0 })
window.requestAnimationFrame(() => editorRef.current?.layout())
}, [error, showWarning, isExecuting, numResults, showResults])
return (
<Sheet open={open} onOpenChange={() => onTogglePanel()}>
<SheetContent showClose={true} className={cn('flex gap-0 w-[1200px]')}>
{/* Assistant */}
<AIAssistant
id={chatId}
className="border-r w-1/2"
debugThread={debugThread}
onDiff={updateEditorWithCheckForDiff}
onResetConversation={() => setChatId(uuidv4())}
/>
{/* Editor */}
<div className={cn('flex flex-col grow w-1/2')}>
<SheetHeader className="flex items-center justify-between py-3 pr-12">
{title}
<Tooltip_Shadcn_>
<TooltipTrigger_Shadcn_ asChild>
<Button
type="outline"
icon={<SqlEditor />}
className="h-[24px] w-[24px] px-1"
onClick={() => {
const content = editorRef.current?.getValue() ?? ''
router.push(`/project/${ref}/sql/new?content=${encodeURIComponent(content)}`)
setAiAssistantPanel({ open: false })
}}
/>
</TooltipTrigger_Shadcn_>
<TooltipContent_Shadcn_ side="bottom">Open in SQL Editor</TooltipContent_Shadcn_>
</Tooltip_Shadcn_>
</SheetHeader>
{editor === null && !isAcknowledged && (
<Admonition
showIcon={false}
type="default"
title="This is a quick access SQL editor to run queries on your database"
className="relative m-0 rounded-none border-x-0 border-t-0 [&>div]:m-0"
>
<span>
Queries written here will not be saved and results are limited up to only 100 rows
</span>
<Button
type="text"
icon={<X />}
className="px-1.5 absolute top-2 right-2"
onClick={() => setIsAcknowledged(true)}
/>
</Admonition>
)}
<div className="flex flex-col h-full justify-between">
<div className="relative flex-grow block">
<CodeEditor
id="assistant-code-editor"
language="pgsql"
editorRef={editorRef}
placeholder={placeholder}
actions={{
runQuery: { enabled: true, callback: () => onExecuteSql() },
explainCode: { enabled: true, callback: onExplainSql },
closeAssistant: { enabled: true, callback: () => onTogglePanel() },
}}
/>
</div>
<div className="flex flex-col">
{error !== undefined && (
<Admonition
type="warning"
className="m-0 rounded-none border-x-0 border-b-0 [&>div>div>pre]:text-sm [&>div]:flex [&>div]:flex-col [&>div]:gap-y-2"
title={errorHeader || 'Error running SQL query'}
description={
<>
<div>
{errorContent.length > 0 ? (
errorContent.map((errorText: string, i: number) => (
<pre key={`err-${i}`} className="font-mono text-xs whitespace-pre-wrap">
{errorText}
</pre>
))
) : (
<p className="font-mono text-xs">{error.error}</p>
)}
</div>
<Button type="default" className="w-min" onClick={() => onFixWithAssistant()}>
Fix with Assistant
</Button>
</>
}
/>
)}
{showWarning && (
<Admonition
type="default"
className="m-0 rounded-none border-x-0 border-b-0 [&>div>pre]:text-sm [&>div]:flex [&>div]:flex-col [&>div]:gap-y-2"
title={`Your query doesn't seem to be relevant to ${
entityContext?.id === 'rls-policies'
? entityContext?.label
: `Database ${entityContext?.label}`
}`}
description={
<>
<p>Are you sure you want to run this query?</p>
<Button type="default" className="w-min" onClick={() => onExecuteSql(true)}>
Confirm run query
</Button>
</>
}
/>
)}
{results !== undefined && results.length > 0 && (
<>
<div className={cn(showResults ? 'h-72 border-t' : 'h-0')}>
<Results rows={results} />
</div>
<div className="flex items-center justify-between border-t bg-surface-100 py-2 pl-2 pr-5">
<p className="text-xs text-foreground-light">
{results.length} rows
{results.length >= 100 && ` (Limited to only 100 rows)`}
</p>
<Button size="tiny" type="default" onClick={() => setShowResults(!showResults)}>
{showResults ? 'Hide' : 'Show'} results
</Button>
</div>
</>
)}
{results !== undefined && results.length === 0 && (
<div className="flex items-center justify-between border-t bg-surface-100 h-[43px] pl-2 pr-5">
<p className="text-xs text-foreground-light">Success. No rows returned.</p>
</div>
)}
<SheetFooter className="bg-surface-100 flex items-center !justify-end px-5 py-4 w-full border-t">
<Button type="default" disabled={isExecuting} onClick={() => onTogglePanel()}>
Cancel
</Button>
<Button
loading={isExecuting}
onClick={() => onExecuteSql()}
iconRight={
isExecuting ? (
<Loader2 className="animate-spin" size={10} strokeWidth={1.5} />
) : (
<div className="flex items-center space-x-1">
{os === 'macos' ? (
<Command size={10} strokeWidth={1.5} />
) : (
<p className="text-xs text-foreground-light">CTRL</p>
)}
<CornerDownLeft size={10} strokeWidth={1.5} />
</div>
)
}
>
{ctaText}
</Button>
</SheetFooter>
</div>
</div>
</div>
</SheetContent>
</Sheet>
return !open ? null : (
<AIAssistant
initialMessages={initialMessages}
id={chatId}
className={cn('w-full h-full')}
onResetConversation={handleReset}
/>
)
}

View File

@@ -0,0 +1,245 @@
import { motion } from 'framer-motion'
import { Button } from 'ui'
import { WandSparkles, FileText, MessageCircle, MessageCircleMore } from 'lucide-react'
interface AIOnboardingProps {
setMessages: (messages: any[]) => void
onSendMessage: (message: string) => void
}
export default function AIOnboarding({ setMessages, onSendMessage }: AIOnboardingProps) {
const sendMessageToAssistant = (message: string) => {
onSendMessage(message)
}
return (
<div className="w-full px-8 py-content flex flex-col flex-1 h-full">
<div className="shrink-0 h-64 mb-5 w-auto overflow-hidden -mx-8 -mt-8 relative">
<motion.div
initial={{ height: '800%', bottom: 0 }}
animate={{ height: '100%', bottom: 0, transition: { duration: 8 } }}
className="absolute bottom-0 left-0 right-0 z-20 bg-gradient-to-b from-transparent to-background"
/>
<div className="h-full w-full relative">
<motion.div
initial={{ x: 350, rotate: -45 }}
animate={{
x: 400,
rotate: -45,
transition: { duration: 5, ease: 'easeInOut' },
}}
className="absolute -inset-full bg-gradient-to-b from-black/[0.05] dark:from-white/[0.08] to-transparent "
/>
<motion.div
initial={{ x: 380, rotate: -45 }}
animate={{
x: 500,
rotate: -45,
transition: { duration: 5, ease: 'easeInOut' },
}}
className="absolute -inset-full bg-gradient-to-b from-black/[0.05] dark:from-white/[0.08] to-transparent "
/>
<motion.div
initial={{ x: 410, rotate: -45 }}
animate={{
x: 600,
rotate: -45,
transition: { duration: 5, ease: 'easeInOut' },
}}
className="absolute -inset-full bg-gradient-to-b from-black/[0.05] dark:from-white/[0.08] to-transparent "
/>
</div>
</div>
<motion.div
initial={{ x: -10, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0 }}
>
<p className="text-base mb-8">How can I help you today?</p>
</motion.div>
<motion.div className="space-y-6 pb-16">
<motion.section
initial={{ x: -10, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0 }}
>
<h3 className="text-foreground-light font-mono text-sm uppercase mb-3">Tables</h3>
<div className="-mx-3">
<Button
size="small"
icon={<WandSparkles strokeWidth={1.5} size={16} />}
type="text"
className="w-full justify-start py-1 h-auto"
onClick={() =>
sendMessageToAssistant(
"Create a table of countries and a table of cities. The cities table should have a country column that's a foreign key to the countries table."
)
}
>
Create a new table
</Button>
<Button
size="small"
icon={<FileText strokeWidth={1.5} size={16} />}
type="text"
className="w-full justify-start py-1 h-auto"
onClick={() =>
sendMessageToAssistant(
'Give me a list of new users from the auth.users table who signed up in the past week'
)
}
>
Query your data
</Button>
<Button
size="small"
icon={<MessageCircleMore strokeWidth={1.5} size={16} />}
type="text"
className="w-full justify-start py-1 h-auto"
onClick={() =>
sendMessageToAssistant(
'Give me a chart showing the number of new sign ups in the auth.users table per day over the last week'
)
}
>
Chart your data
</Button>
</div>
</motion.section>
<motion.section
initial={{ x: -10, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.1 }}
>
<h3 className="text-foreground-light font-mono text-sm uppercase mb-3">RLS Policies</h3>
<div className="-mx-3">
<Button
size="small"
icon={<WandSparkles strokeWidth={1.5} size={16} />}
type="text"
className="w-full justify-start py-1 h-auto"
onClick={() =>
sendMessageToAssistant(
'Suggest some database RLS policies I can add to my public schema'
)
}
>
Suggest RLS policies
</Button>
<Button
size="small"
icon={<FileText strokeWidth={1.5} size={16} />}
type="text"
className="w-full justify-start py-1 h-auto"
onClick={() =>
sendMessageToAssistant('Generate some examples of database RLS policies')
}
>
Examples of RLS policies
</Button>
<Button
size="small"
icon={<MessageCircleMore strokeWidth={1.5} size={16} />}
type="text"
className="w-full justify-start py-1 h-auto"
onClick={() => sendMessageToAssistant(`What are database RLS policies`)}
>
What are RLS policies?
</Button>
</div>
</motion.section>
<motion.section
initial={{ x: -10, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.2 }}
>
<h3 className="text-foreground-light font-mono text-sm uppercase mb-3">Functions</h3>
<div className="-mx-3">
<Button
size="small"
icon={<WandSparkles strokeWidth={1.5} size={16} />}
type="text"
className="w-full justify-start py-1 h-auto"
onClick={() =>
sendMessageToAssistant(
'Suggest some database functions I can add to my public schema'
)
}
>
Suggest database functions
</Button>
<Button
size="small"
icon={<FileText strokeWidth={1.5} size={16} />}
type="text"
className="w-full justify-start py-1 h-auto"
onClick={() => sendMessageToAssistant('Generate some examples of database functions')}
>
Examples of database functions
</Button>
<Button
size="small"
icon={<MessageCircleMore strokeWidth={1.5} size={16} />}
type="text"
className="w-full justify-start py-1 h-auto"
onClick={() => sendMessageToAssistant('What are database functions')}
>
What are database functions?
</Button>
</div>
</motion.section>
<motion.section
initial={{ x: -10, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.3 }}
>
<h3 className="text-foreground-light font-mono text-sm uppercase mb-3">Triggers</h3>
<div className="-mx-3">
<Button
size="small"
icon={<WandSparkles strokeWidth={1.5} size={16} />}
type="text"
className="w-full justify-start py-1 h-auto"
onClick={() =>
sendMessageToAssistant(
'Suggest some database triggers I can add to my public schema'
)
}
>
Suggest database Triggers
</Button>
<Button
size="small"
icon={<FileText strokeWidth={1.5} size={16} />}
type="text"
className="w-full justify-start py-1 h-auto"
onClick={() => sendMessageToAssistant(`Generate some examples of database triggers`)}
>
Examples of database triggers
</Button>
<Button
size="small"
icon={<MessageCircleMore strokeWidth={1.5} size={16} />}
type="text"
className="w-full justify-start py-1 h-auto"
onClick={() => sendMessageToAssistant('What are database triggers')}
>
What are database triggers?
</Button>
</div>
</motion.section>
</motion.div>
</div>
)
}

View File

@@ -0,0 +1,61 @@
import { ChevronDown, ChevronUp, X } from 'lucide-react'
import { useState } from 'react'
import { Button, CodeBlock, CodeBlockProps, cn } from 'ui'
interface CollapsibleCodeBlockProps extends CodeBlockProps {
onRemove?: () => void
}
const CollapsibleCodeBlock = ({ onRemove, ...props }: CollapsibleCodeBlockProps) => {
const [isExpanded, setIsExpanded] = useState(false)
const codeString = (props.value || props.children) as string
const firstLine = isExpanded
? codeString
: codeString?.substring(0, codeString.indexOf('\n')) || codeString
return (
<div className="relative">
<div
className={cn(
'flex items-center gap-1 p-1 bg-surface-100 border border-default w-full overflow-hidden',
'rounded-md'
)}
>
<Button
type="text"
size="tiny"
className="w-6 h-6"
onClick={() => setIsExpanded(!isExpanded)}
icon={isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
/>
<div className="flex-1 shrink-1 overflow-auto [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
<CodeBlock
{...props}
value={firstLine}
hideCopy
className={cn(
'block !bg-transparent max-h-32 max-w-full !py-0 !px-0 !border-t-0 prose dark:prose-dark border-0 text-foreground !rounded-none w-full text-wrap whitespace-pre-wrap',
// change the look of the code block. The flex hack is so that the code is wrapping since
// every word is a separate span
'[&>code]:m-0 [&>code>span]:flex border-t-0 [&>code>span]:flex-wrap [&>code]:block [&>code>span]:text-foreground text-wrap whitespace-pre-wrap',
props.className
)}
/>
</div>
{onRemove && (
<Button
type="text"
size="tiny"
className="shrink-0 w-6 h-6"
onClick={onRemove}
icon={<X size={14} />}
/>
)}
</div>
</div>
)
}
export default CollapsibleCodeBlock

View File

@@ -1,146 +1,94 @@
import dayjs from 'dayjs'
import { noop } from 'lodash'
import Image from 'next/image'
import { PropsWithChildren, memo, useMemo } from 'react'
import { motion } from 'framer-motion'
import { PropsWithChildren } from 'react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { AiIconAnimation, Badge, cn, markdownComponents, WarningIcon } from 'ui'
import { DiffType } from 'components/interfaces/SQLEditor/SQLEditor.types'
import { useProfile } from 'lib/profile'
import { MessagePre } from './MessagePre'
import { cn, markdownComponents, WarningIcon } from 'ui'
import CollapsibleCodeBlock from './CollapsibleCodeBlock'
import { SqlSnippet } from './SqlSnippet'
interface MessageProps {
name?: string
id: string
role: 'function' | 'system' | 'user' | 'assistant' | 'data' | 'tool'
content?: string
createdAt?: number
isDebug?: boolean
isSelected?: boolean
isLoading: boolean
readOnly?: boolean
action?: React.ReactNode
context?: { entity: string; schemas: string[]; tables: string[] }
onDiff?: (type: DiffType, s: string) => void
variant?: 'default' | 'warning'
}
export const Message = memo(function Message({
name,
export const Message = function Message({
id,
role,
content,
createdAt,
isDebug,
isSelected = false,
context,
isLoading,
readOnly,
children,
action = null,
variant = 'default',
onDiff = noop,
}: PropsWithChildren<MessageProps>) {
const { profile } = useProfile()
const isUser = role === 'user'
const icon = useMemo(() => {
return role === 'assistant' ? (
<AiIconAnimation
loading={content === 'Thinking...'}
className="[&>div>div]:border-black dark:[&>div>div]:border-white"
/>
) : (
<div className="relative border shadow-lg w-8 h-8 rounded-full overflow-hidden">
<Image
src={`https://github.com/${profile?.username}.png` || ''}
width={30}
height={30}
alt="avatar"
className="relative"
/>
</div>
)
}, [content, profile?.username, role])
const formattedContext =
context !== undefined
? Object.entries(context)
.filter(([_, value]) => value.length > 0)
.map(([key, value]) => {
return `${key.charAt(0).toUpperCase() + key.slice(1)}: ${Array.isArray(value) ? value.join(', ') : value}`
})
.join(' • ')
: undefined
if (!content) return null
return (
<div
className={cn(
'flex flex-col py-4 gap-4 px-5 text-foreground-light text-sm border-t first:border-0',
variant === 'warning' && 'bg-warning-200',
isUser && 'bg-default'
)}
<motion.div
layout="position"
initial={{ y: 5, opacity: 0, scale: 0.99 }}
animate={{ y: 0, opacity: 1, scale: 1 }}
className="w-full flex flex-col"
>
<div className="flex justify-between items-center">
<div className="flex gap-x-3 items-center">
{variant === 'warning' ? <WarningIcon className="w-6 h-6" /> : icon}
<div className="flex flex-col -gap-y-1">
<div className="flex items-center gap-x-3">
<span className="text-sm">{!isUser ? 'Assistant' : name ? name : 'You'}</span>
{createdAt && (
<span
className={cn(
'text-xs text-foreground-muted',
variant === 'warning' && 'text-warning-500'
)}
>
{dayjs(createdAt).fromNow()}
</span>
)}
</div>
{role === 'user' && context !== undefined && (
<span className="text-xs text-foreground-lighter">{formattedContext}</span>
)}
</div>
{isDebug && <Badge variant="warning">Debug request</Badge>}
</div>{' '}
{action}
</div>
<ReactMarkdown
className="gap-x-2.5 gap-y-4 flex flex-col [&>*>code]:text-xs [&>*>*>code]:text-xs"
remarkPlugins={[remarkGfm]}
components={{
...markdownComponents,
pre: (props: any) => {
return (
<MessagePre
onDiff={onDiff}
className={cn(
'transition [&>div>pre]:max-w-full',
isSelected ? '[&>div>pre]:!border-stronger [&>div>pre]:!bg-surface-200' : ''
)}
>
{props.children[0].props.children}
</MessagePre>
)
},
ol: (props: any) => {
return <ol className="flex flex-col gap-y-4">{props.children}</ol>
},
li: (props: any) => {
return <li className="[&>pre]:mt-2">{props.children}</li>
},
h3: (props: any) => {
return <h3 className="underline">{props.children}</h3>
},
code: (props: any) => {
return <code className={cn('text-xs', props.className)}>{props.children}</code>
},
}}
>
{content}
</ReactMarkdown>
{children}
</div>
<div
className={cn(
'text-foreground-light text-sm mb max-w-full mb-6',
variant === 'warning' && 'bg-warning-200',
isUser ? 'px-5 py-3 rounded-lg bg-background-muted w-fit self-end' : 'mb-6'
)}
>
{variant === 'warning' && <WarningIcon className="w-6 h-6" />}
{action}
<ReactMarkdown
className="gap-x-2.5 gap-y-4 flex flex-col [&>*>code]:text-xs [&>*>*>code]:text-xs"
remarkPlugins={[remarkGfm]}
components={{
...markdownComponents,
pre: (props: any) => {
return readOnly ? (
<div className="mb-1 -mt-2">
<CollapsibleCodeBlock
value={props.children[0].props.children[0]}
language="sql"
hideLineNumbers
/>
</div>
) : (
<SqlSnippet
readOnly={readOnly}
isLoading={isLoading}
sql={props.children[0].props.children}
/>
)
},
ol: (props: any) => {
return <ol className="flex flex-col gap-y-4">{props.children}</ol>
},
li: (props: any) => {
return <li className="[&>pre]:mt-2">{props.children}</li>
},
h3: (props: any) => {
return <h3 className="underline">{props.children}</h3>
},
code: (props: any) => {
return <code className={cn('text-xs', props.className)}>{props.children}</code>
},
}}
>
{content}
</ReactMarkdown>
</div>
</motion.div>
)
})
}

View File

@@ -1,5 +1,5 @@
import { InsertCode, ReplaceCode } from 'icons'
import { Check, Copy } from 'lucide-react'
import { InsertCode } from 'icons'
import { Check, Copy, Edit } from 'lucide-react'
import { useEffect, useState } from 'react'
import { format } from 'sql-formatter'
import {
@@ -74,11 +74,11 @@ export const MessagePre = ({ onDiff, children, className }: MessagePreProps) =>
})
}}
>
<InsertCode className="h-4 w-4 text-foreground-light" strokeWidth={1.5} />
<Edit className="h-4 w-4 text-foreground-light" strokeWidth={1.5} />
</Button>
</TooltipTrigger_Shadcn_>
<TooltipContent_Shadcn_ side="bottom" className="font-sans">
Insert code
Edit
</TooltipContent_Shadcn_>
</Tooltip_Shadcn_>

View File

@@ -0,0 +1,424 @@
import { Code, DatabaseIcon, Edit, Play } from 'lucide-react'
import { useRouter } from 'next/router'
import { useCallback, useEffect, useState } from 'react'
import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts'
import useNewQuery from 'components/interfaces/SQLEditor/hooks'
import { DiffType } from 'components/interfaces/SQLEditor/SQLEditor.types'
import { suffixWithLimit } from 'components/interfaces/SQLEditor/SQLEditor.utils'
import Results from 'components/interfaces/SQLEditor/UtilityPanel/Results'
import { QueryResponseError, useExecuteSqlMutation } from 'data/sql/execute-sql-mutation'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { useAppStateSnapshot } from 'state/app-state'
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
import {
Button,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
CodeBlock,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
cn,
} from 'ui'
import { Admonition } from 'ui-patterns'
import { ButtonTooltip } from '../ButtonTooltip'
import {
getContextualInvalidationKeys,
identifyQueryType,
isReadOnlySelect,
} from './AIAssistant.utils'
import { useParams } from 'common'
import { useQueryClient } from '@tanstack/react-query'
import { Markdown } from 'components/interfaces/Markdown'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { TELEMETRY_EVENTS, TELEMETRY_VALUES } from 'lib/constants/telemetry'
interface SqlSnippetWrapperProps {
sql: string
isLoading?: boolean
readOnly?: boolean
}
const SqlSnippetWrapper = ({
sql,
isLoading = false,
readOnly = false,
}: SqlSnippetWrapperProps) => {
const formatted = (sql || [''])[0]
const propsMatch = formatted.match(/--\s*props:\s*(\{[^}]+\})/)
const props = propsMatch ? JSON.parse(propsMatch[1]) : {}
const title = props.title || 'SQL Query'
const updatedFormatted = formatted?.replace(/--\s*props:\s*\{[^}]+\}/, '').trim()
return (
<div className="-mx-8 my-3 mt-2 border-b overflow-hidden">
<SqlCard
sql={updatedFormatted}
isChart={props.isChart === 'true'}
xAxis={props.xAxis}
yAxis={props.yAxis}
title={title}
readOnly={readOnly}
isLoading={isLoading}
/>
</div>
)
}
interface ParsedSqlProps {
sql: string
title: string
isLoading?: boolean
readOnly?: boolean
isChart: boolean
xAxis: string
yAxis: string
}
export const SqlCard = ({
sql,
isChart,
xAxis,
yAxis,
title,
readOnly = false,
isLoading = false,
}: ParsedSqlProps) => {
const router = useRouter()
const { ref } = useParams()
const queryClient = useQueryClient()
const project = useSelectedProject()
const snapV2 = useSqlEditorV2StateSnapshot()
const { setAiAssistantPanel } = useAppStateSnapshot()
const { newQuery } = useNewQuery()
const isInSQLEditor = router.pathname.includes('/sql')
const isInNewSnippet = router.pathname.endsWith('/sql')
const [showCode, setShowCode] = useState(readOnly || !isReadOnlySelect(sql))
const [showResults, setShowResults] = useState(false)
const [results, setResults] = useState<any[]>()
const [error, setError] = useState<QueryResponseError>()
const [showWarning, setShowWarning] = useState(false)
const { mutate: sendEvent } = useSendEventMutation()
const { mutate: executeSql, isLoading: isExecuting } = useExecuteSqlMutation({
onSuccess: async (res) => {
// [Joshen] Only do contextual invalidation within a project context
if (!!ref) {
const invalidationKeys = getContextualInvalidationKeys({ ref, pathname: router.pathname })
await Promise.all(invalidationKeys?.map((x) => queryClient.invalidateQueries(x)))
}
setShowResults(true)
setResults(res.result)
setShowWarning(false)
},
onError: (error) => {
setError(error)
setResults([])
setShowWarning(false)
},
})
const handleExecute = useCallback(() => {
if (!project?.ref || !sql || readOnly) return
if (!isReadOnlySelect(sql)) {
setShowCode(true)
setShowWarning(true)
return
}
setError(undefined)
executeSql({
sql: suffixWithLimit(sql, 100),
projectRef: project?.ref,
connectionString: project?.connectionString,
handleError: (error: any) => {
setError(error)
setResults([])
return { result: [] }
},
})
}, [project?.ref, project?.connectionString, sql, executeSql, readOnly])
const handleEditInSQLEditor = () => {
if (isInSQLEditor) {
snapV2.setDiffContent(sql, DiffType.Addition)
} else {
newQuery(sql, title)
}
}
const [errorHeader, ...errorContent] =
(error?.formattedError?.split('\n') ?? [])?.filter((x: string) => x.length > 0) ?? []
useEffect(() => {
if (isReadOnlySelect(sql) && !results && !readOnly && !isLoading) {
handleExecute()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sql, readOnly, isLoading])
return (
<div className="overflow-hidden">
<div className={cn('flex items-center gap-2 border-t', showWarning ? '' : 'px-5 py-2')}>
{showWarning ? (
<Admonition type="warning" className="mb-0 rounded-none border-0">
<p>This query contains write operations. Are you sure you want to execute it?</p>
<div className="flex justify-stretch mt-2 gap-2">
<Button
type="outline"
size="tiny"
className="w-full flex-1"
onClick={() => setShowWarning(false)}
>
Cancel
</Button>
<Button
type="outline"
size="tiny"
className="w-full flex-1"
onClick={() => {
setShowWarning(false)
executeSql({
sql: suffixWithLimit(sql, 100),
projectRef: project?.ref,
connectionString: project?.connectionString,
handleError: (error: any) => {
setError(error)
setResults([])
return { result: [] }
},
})
sendEvent({
action: TELEMETRY_EVENTS.AI_ASSISTANT_V2,
value: TELEMETRY_VALUES.RAN_SQL_SUGGESTION,
label: 'mutation',
category: identifyQueryType(sql) ?? 'unknown',
})
}}
>
Run
</Button>
</div>
</Admonition>
) : (
<>
<DatabaseIcon size={16} strokeWidth={1.5} />
<h3 className="text-sm font-medium flex-1">{title}</h3>
{!readOnly && (
<div className="flex">
<ButtonTooltip
type="text"
size="tiny"
className="w-7 h-7"
icon={<Code size={14} />}
onClick={() => setShowCode(!showCode)}
tooltip={{ content: { side: 'bottom', text: 'Show query' } }}
/>
{!isInSQLEditor || isInNewSnippet ? (
<ButtonTooltip
type="text"
size="tiny"
className="w-7 h-7"
icon={<Edit size={14} />}
onClick={() => {
handleEditInSQLEditor()
sendEvent({
action: TELEMETRY_EVENTS.AI_ASSISTANT_V2,
value: TELEMETRY_VALUES.EDIT_IN_SQL_EDITOR,
})
}}
tooltip={{ content: { side: 'bottom', text: 'Edit in SQL Editor' } }}
/>
) : (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ButtonTooltip
type="text"
size="tiny"
className="w-7 h-7"
icon={<Edit size={14} />}
tooltip={{ content: { side: 'bottom', text: 'Edit in SQL Editor' } }}
/>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-36">
<DropdownMenuItem
onClick={() => snapV2.setDiffContent(sql, DiffType.Addition)}
>
Insert code
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => snapV2.setDiffContent(sql, DiffType.Modification)}
>
Replace code
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => snapV2.setDiffContent(sql, DiffType.NewSnippet)}
>
Create new snippet
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
<ButtonTooltip
type="text"
size="tiny"
className="w-7 h-7"
icon={<Play size={14} />}
loading={isExecuting}
onClick={() => {
handleExecute()
if (isReadOnlySelect(sql)) {
sendEvent({
action: TELEMETRY_EVENTS.AI_ASSISTANT_V2,
value: TELEMETRY_VALUES.RAN_SQL_SUGGESTION,
label: 'select',
})
}
}}
tooltip={{
content: {
side: 'bottom',
className: 'max-w-56 text-center',
text: isExecuting ? (
<Markdown
className="[&>p]:text-xs text-foreground"
content={`Query is running. You may cancel ongoing queries via the [SQL Editor](/project/${ref}/sql?viewOngoingQueries=true).`}
/>
) : (
'Run query'
),
},
}}
/>
</div>
)}
</>
)}
</div>
{showCode && (
<CodeBlock
hideLineNumbers
value={sql}
language="sql"
className={cn(
'max-w-full max-h-96 block !bg-transparent !py-3 !px-3.5 prose dark:prose-dark border-0 border-t text-foreground !rounded-none w-full',
'[&>code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap [&>code]:block [&>code>span]:text-foreground'
)}
/>
)}
{/* Results Section */}
{results !== undefined && results.length > 0 && isChart && xAxis && yAxis ? (
<div className="p-5 border-t">
<ChartContainer config={{}} className="aspect-auto h-[250px] w-full">
<BarChart
data={results}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey={xAxis}
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
/>
<ChartTooltip content={<ChartTooltipContent className="w-[150px]" />} />
<Bar dataKey={yAxis} fill="hsl(var(--chart-2))" />
</BarChart>
</ChartContainer>
</div>
) : (
<div>
{error !== undefined ? (
<Admonition
type="warning"
className="m-0 rounded-none border-x-0 border-b-0 [&>div>div>pre]:text-sm [&>div]:flex [&>div]:flex-col [&>div]:gap-y-2"
title={errorHeader || 'Error running SQL query'}
description={
<>
<div>
{errorContent.length > 0 ? (
<div>
{errorContent.map((errorText: string, i: number) => (
<pre key={`err-${i}`} className="font-mono text-xs whitespace-pre-wrap">
{errorText}
</pre>
))}
<Button
type="default"
className="mt-2"
onClick={() => {
setAiAssistantPanel({
sqlSnippets: [sql],
initialInput: `Help me to debug the attached sql snippet which gives the following error: \n\n${errorHeader}\n${errorContent.join('\n')}`,
})
}}
>
Debug
</Button>
</div>
) : (
<>
<p className="font-mono text-xs">{error.error}</p>
<Button
type="default"
className="mt-2"
onClick={() => {
setAiAssistantPanel({
sqlSnippets: [sql],
initialInput: `Help me to debug the attached sql snippet which gives the following error: \n\n${error.error}`,
})
}}
>
Debug
</Button>
</>
)}
</div>
</>
}
/>
) : results !== undefined && results.length > 0 ? (
<>
<div className={cn(showResults ? 'h-auto max-h-64 overflow-auto border-t' : 'h-0')}>
<Results rows={results} />
</div>
<div className="flex items-center justify-between border-t bg-background-muted py-2 pl-2 pr-5">
<p className="text-xs text-foreground-light">
{results.length} rows
{results.length >= 100 && ` (Limited to only 100 rows)`}
</p>
</div>
</>
) : (
results !== undefined &&
results.length === 0 && (
<div className="flex items-center justify-between border-t bg-surface-100 h-[43px] pl-2 pr-5">
<p className="text-xs text-foreground-light">Success. No rows returned.</p>
</div>
)
)}
</div>
)}
</div>
)
}
export { SqlSnippetWrapper as SqlSnippet }

View File

@@ -14,11 +14,12 @@ export type DatabasePoliciesVariables = {
export async function getDatabasePolicies(
{ projectRef, connectionString, schema }: DatabasePoliciesVariables,
signal?: AbortSignal
signal?: AbortSignal,
headersInit?: HeadersInit
) {
if (!projectRef) throw new Error('projectRef is required')
let headers = new Headers()
let headers = new Headers(headersInit)
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const { data, error } = await get('/platform/pg-meta/{ref}/policies', {

View File

@@ -13,7 +13,7 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
/* SQL */ `
DROP TYPE IF EXISTS pg_temp.tabledefs CASCADE;
CREATE TYPE pg_temp.tabledefs AS ENUM ('PKEY_INTERNAL','PKEY_EXTERNAL','FKEYS_INTERNAL', 'FKEYS_EXTERNAL', 'COMMENTS', 'FKEYS_NONE', 'INCLUDE_TRIGGERS', 'NO_TRIGGERS');
-- SELECT * FROM pg_temp.pg_get_coldef('sample','orders','id');
-- DROP FUNCTION pg_temp.pg_get_coldef(text,text,text,boolean);
CREATE OR REPLACE FUNCTION pg_temp.pg_get_coldef(
@@ -32,39 +32,39 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
v_dt2 text;
v_dt3 text;
v_nullable boolean;
v_position int;
v_identity text;
v_generated text;
v_hasdflt boolean;
v_position int;
v_identity text;
v_generated text;
v_hasdflt boolean;
v_dfltexpr text;
BEGIN
IF oldway THEN
SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) INTO v_coldef FROM pg_namespace n, pg_class c, pg_attribute a, pg_type t
IF oldway THEN
SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) INTO v_coldef FROM pg_namespace n, pg_class c, pg_attribute a, pg_type t
WHERE n.nspname = in_schema AND n.oid = c.relnamespace AND c.relname = in_table AND a.attname = in_column and a.attnum > 0 AND a.attrelid = c.oid AND a.atttypid = t.oid ORDER BY a.attnum;
-- RAISE NOTICE 'DEBUG: oldway=%',v_coldef;
ELSE
-- a.attrelid::regclass::text, a.attname
SELECT CASE WHEN a.atttypid = ANY ('{int,int8,int2}'::regtype[]) AND EXISTS (SELECT FROM pg_attrdef ad WHERE ad.adrelid = a.attrelid AND ad.adnum = a.attnum AND
pg_get_expr(ad.adbin, ad.adrelid) = 'nextval(''' || (pg_get_serial_sequence (a.attrelid::regclass::text, a.attname))::regclass || '''::regclass)') THEN CASE a.atttypid
WHEN 'int'::regtype THEN 'serial' WHEN 'int8'::regtype THEN 'bigserial' WHEN 'int2'::regtype THEN 'smallserial' END ELSE format_type(a.atttypid, a.atttypmod) END AS data_type
INTO v_coldef FROM pg_namespace n, pg_class c, pg_attribute a, pg_type t
SELECT CASE WHEN a.atttypid = ANY ('{int,int8,int2}'::regtype[]) AND EXISTS (SELECT FROM pg_attrdef ad WHERE ad.adrelid = a.attrelid AND ad.adnum = a.attnum AND
pg_get_expr(ad.adbin, ad.adrelid) = 'nextval(''' || (pg_get_serial_sequence (a.attrelid::regclass::text, a.attname))::regclass || '''::regclass)') THEN CASE a.atttypid
WHEN 'int'::regtype THEN 'serial' WHEN 'int8'::regtype THEN 'bigserial' WHEN 'int2'::regtype THEN 'smallserial' END ELSE format_type(a.atttypid, a.atttypmod) END AS data_type
INTO v_coldef FROM pg_namespace n, pg_class c, pg_attribute a, pg_type t
WHERE n.nspname = in_schema AND n.oid = c.relnamespace AND c.relname = in_table AND a.attname = in_column and a.attnum > 0 AND a.attrelid = c.oid AND a.atttypid = t.oid ORDER BY a.attnum;
-- RAISE NOTICE 'DEBUG: newway=%',v_coldef;
-- Issue#24: not implemented yet
-- Issue#24: not implemented yet
-- might replace with this below to do more detailed parsing...
-- SELECT a.atttypid::regtype AS dt1, format_type(a.atttypid, a.atttypmod) as dt2, t.typname as dt3, CASE WHEN not(a.attnotnull) THEN True ELSE False END AS nullable,
-- a.attnum, a.attidentity, a.attgenerated, a.atthasdef, pg_get_expr(ad.adbin, ad.adrelid) dfltexpr
-- INTO v_dt1, v_dt2, v_dt3, v_nullable, v_position, v_identity, v_generated, v_hasdflt, v_dfltexpr
-- FROM pg_attribute a JOIN pg_class c ON (a.attrelid = c.oid) JOIN pg_type t ON (a.atttypid = t.oid) LEFT JOIN pg_attrdef ad ON (a.attrelid = ad.adrelid AND a.attnum = ad.adnum)
-- SELECT a.atttypid::regtype AS dt1, format_type(a.atttypid, a.atttypmod) as dt2, t.typname as dt3, CASE WHEN not(a.attnotnull) THEN True ELSE False END AS nullable,
-- a.attnum, a.attidentity, a.attgenerated, a.atthasdef, pg_get_expr(ad.adbin, ad.adrelid) dfltexpr
-- INTO v_dt1, v_dt2, v_dt3, v_nullable, v_position, v_identity, v_generated, v_hasdflt, v_dfltexpr
-- FROM pg_attribute a JOIN pg_class c ON (a.attrelid = c.oid) JOIN pg_type t ON (a.atttypid = t.oid) LEFT JOIN pg_attrdef ad ON (a.attrelid = ad.adrelid AND a.attnum = ad.adnum)
-- WHERE c.relkind in ('r','p') AND a.attnum > 0 AND NOT a.attisdropped AND c.relnamespace::regnamespace::text = in_schema AND c.relname = in_table AND a.attname = in_column;
-- RAISE NOTICE 'schema=% table=% column=% dt1=% dt2=% dt3=% nullable=% pos=% identity=% generated=% HasDefault=% DeftExpr=%', in_schema, in_table, in_column, v_dt1,v_dt2,v_dt3,v_nullable,v_position,v_identity,v_generated,v_hasdflt,v_dfltexpr;
END IF;
RETURN v_coldef;
END;
$$;
-- SELECT * FROM pg_temp.pg_get_tabledef('sample', 'address', false);
DROP FUNCTION IF EXISTS pg_temp.pg_get_tabledef(character varying,character varying,boolean,tabledefs[]);
CREATE OR REPLACE FUNCTION pg_temp.pg_get_tabledef(
@@ -97,7 +97,7 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
v_parent text;
v_parent_schema text;
v_persist text;
v_temp text := '';
v_temp text := '';
v_temp2 text;
v_relopts text;
v_tablespace text;
@@ -116,8 +116,8 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
search_path_new text := '';
v_partial boolean;
v_pos integer;
-- assume defaults for ENUMs at the getgo
-- assume defaults for ENUMs at the getgo
pkcnt int := 0;
fkcnt int := 0;
trigcnt int := 0;
@@ -128,7 +128,7 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
arglen integer;
vargs text;
avarg pg_temp.tabledefs;
-- exception variables
v_ret text;
v_diag1 text;
@@ -137,14 +137,14 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
v_diag4 text;
v_diag5 text;
v_diag6 text;
BEGIN
SET client_min_messages = 'notice';
IF _verbose THEN bVerbose = True; END IF;
-- v17 fix: handle case-sensitive
-- v17 fix: handle case-sensitive
-- v_qualified = in_schema || '.' || in_table;
arglen := array_length($4, 1);
IF arglen IS NULL THEN
-- nothing to do, so assume defaults
@@ -164,44 +164,44 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
trigtype = avarg;
ELSEIF avarg = 'PKEY_EXTERNAL' THEN
pkcnt = pkcnt + 1;
pktype = avarg;
pktype = avarg;
ELSEIF avarg = 'COMMENTS' THEN
cmtcnt = cmtcnt + 1;
END IF;
END LOOP;
IF fkcnt > 1 THEN
IF fkcnt > 1 THEN
RAISE WARNING 'Only one foreign key option can be provided. You provided %', fkcnt;
RETURN '';
ELSEIF trigcnt > 1 THEN
ELSEIF trigcnt > 1 THEN
RAISE WARNING 'Only one trigger option can be provided. You provided %', trigcnt;
RETURN '';
ELSEIF pkcnt > 1 THEN
ELSEIF pkcnt > 1 THEN
RAISE WARNING 'Only one pkey option can be provided. You provided %', pkcnt;
RETURN '';
ELSEIF cmtcnt > 1 THEN
RETURN '';
ELSEIF cmtcnt > 1 THEN
RAISE WARNING 'Only one comments option can be provided. You provided %', cmtcnt;
RETURN '';
END IF;
RETURN '';
END IF;
END IF;
SELECT c.oid, (select setting from pg_settings where name = 'server_version_num') INTO v_table_oid, v_pgversion FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind in ('r','p') AND c.relname = in_table AND n.nspname = in_schema;
-- set search_path = public before we do anything to force explicit schema qualification but dont forget to set it back before exiting...
SELECT setting INTO search_path_old FROM pg_settings WHERE name = 'search_path';
-- RAISE NOTICE 'DEBUG tableddl: saving old search_path: ***%***', search_path_old;
EXECUTE 'SET search_path = "public"';
SELECT setting INTO search_path_new FROM pg_settings WHERE name = 'search_path';
-- RAISE NOTICE 'DEBUG tableddl: using new search path=***%***', search_path_new;
-- throw an error if table was not found
IF (v_table_oid IS NULL) THEN
RAISE EXCEPTION 'table does not exist';
END IF;
-- get user-defined tablespaces if applicable
SELECT tablespace INTO v_temp FROM pg_tables WHERE schemaname = in_schema and tablename = in_table and tablespace IS NOT NULL;
IF v_temp IS NULL THEN
@@ -209,16 +209,16 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
ELSE
v_tablespace := 'TABLESPACE ' || v_temp;
END IF;
-- also see if there are any SET commands for this table, ie, autovacuum_enabled=off, fillfactor=70
WITH relopts AS (SELECT unnest(c.reloptions) relopts FROM pg_class c, pg_namespace n WHERE n.nspname = in_schema and n.oid = c.relnamespace and c.relname = in_table)
WITH relopts AS (SELECT unnest(c.reloptions) relopts FROM pg_class c, pg_namespace n WHERE n.nspname = in_schema and n.oid = c.relnamespace and c.relname = in_table)
SELECT string_agg(r.relopts, ', ') as relopts INTO v_temp from relopts r;
IF v_temp IS NULL THEN
v_relopts := '';
ELSE
v_relopts := ' WITH (' || v_temp || ')';
END IF;
-- -----------------------------------------------------------------------------------
-- Create table defs for partitions/children using inheritance or declarative methods.
-- inheritance: pg_class.relkind = 'r' pg_class.relispartition=false pg_class.relpartbound is NULL
@@ -230,7 +230,7 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
IF v_pgversion < 100000 THEN
-- Issue#11: handle parent schema
SELECT c2.relname parent, c2.relnamespace::regnamespace INTO v_parent, v_parent_schema from pg_class c1, pg_namespace n, pg_inherits i, pg_class c2
WHERE n.nspname = in_schema and n.oid = c1.relnamespace and c1.relname = in_table and c1.oid = i.inhrelid and i.inhparent = c2.oid and c1.relkind = 'r';
WHERE n.nspname = in_schema and n.oid = c1.relnamespace and c1.relname = in_table and c1.oid = i.inhrelid and i.inhparent = c2.oid and c1.relkind = 'r';
IF (v_parent IS NOT NULL) THEN
bPartition := True;
bInheritance := True;
@@ -250,24 +250,24 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
END IF;
IF bPartition THEN
--Issue#17 fix for case-sensitive tables
-- SELECT count(*) INTO v_cnt1 FROM information_schema.tables t WHERE EXISTS (SELECT REGEXP_MATCHES(s.table_name, '([A-Z]+)','g') FROM information_schema.tables s
-- WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = quote_ident(in_schema) AND t.table_name = quote_ident(in_table) AND t.table_type = 'BASE TABLE');
SELECT count(*) INTO v_cnt1 FROM information_schema.tables t WHERE EXISTS (SELECT REGEXP_MATCHES(s.table_name, '([A-Z]+)','g') FROM information_schema.tables s
WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = in_schema AND t.table_name = in_table AND t.table_type = 'BASE TABLE');
-- SELECT count(*) INTO v_cnt1 FROM information_schema.tables t WHERE EXISTS (SELECT REGEXP_MATCHES(s.table_name, '([A-Z]+)','g') FROM information_schema.tables s
-- WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = quote_ident(in_schema) AND t.table_name = quote_ident(in_table) AND t.table_type = 'BASE TABLE');
SELECT count(*) INTO v_cnt1 FROM information_schema.tables t WHERE EXISTS (SELECT REGEXP_MATCHES(s.table_name, '([A-Z]+)','g') FROM information_schema.tables s
WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = in_schema AND t.table_name = in_table AND t.table_type = 'BASE TABLE');
--Issue#19 put double-quotes around SQL keyword column names
-- Issue#121: fix keyword lookup for table name not column name that does not apply here
-- SELECT COUNT(*) INTO v_cnt2 FROM pg_get_keywords() WHERE word = v_colrec.column_name AND catcode = 'R';
SELECT COUNT(*) INTO v_cnt2 FROM pg_get_keywords() WHERE word = in_table AND catcode = 'R';
IF bInheritance THEN
-- inheritance-based
IF v_cnt1 > 0 OR v_cnt2 > 0 THEN
v_table_ddl := 'CREATE TABLE ' || in_schema || '."' || in_table || '"( '|| E'\\n';
v_table_ddl := 'CREATE TABLE ' || in_schema || '."' || in_table || '"( '|| E'\\n';
ELSE
v_table_ddl := 'CREATE TABLE ' || in_schema || '.' || in_table || '( '|| E'\\n';
v_table_ddl := 'CREATE TABLE ' || in_schema || '.' || in_table || '( '|| E'\\n';
END IF;
-- Jump to constraints section to add the check constraints
ELSE
-- declarative-based
@@ -288,7 +288,7 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
END IF;
END IF;
IF bVerbose THEN RAISE NOTICE '(1)tabledef so far: %', v_table_ddl; END IF;
IF NOT bPartition THEN
-- see if this is unlogged or temporary table
select c.relpersistence into v_persist from pg_class c, pg_namespace n where n.nspname = in_schema and n.oid = c.relnamespace and c.relname = in_table and c.relkind = 'r';
@@ -300,29 +300,29 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
v_temp := '';
END IF;
END IF;
-- start the create definition for regular tables unless we are in progress creating an inheritance-based child table
IF NOT bPartition THEN
--Issue#17 fix for case-sensitive tables
-- SELECT count(*) INTO v_cnt1 FROM information_schema.tables t WHERE EXISTS (SELECT REGEXP_MATCHES(s.table_name, '([A-Z]+)','g') FROM information_schema.tables s
-- WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = quote_ident(in_schema) AND t.table_name = quote_ident(in_table) AND t.table_type = 'BASE TABLE');
SELECT count(*) INTO v_cnt1 FROM information_schema.tables t WHERE EXISTS (SELECT REGEXP_MATCHES(s.table_name, '([A-Z]+)','g') FROM information_schema.tables s
WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = in_schema AND t.table_name = in_table AND t.table_type = 'BASE TABLE');
-- SELECT count(*) INTO v_cnt1 FROM information_schema.tables t WHERE EXISTS (SELECT REGEXP_MATCHES(s.table_name, '([A-Z]+)','g') FROM information_schema.tables s
-- WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = quote_ident(in_schema) AND t.table_name = quote_ident(in_table) AND t.table_type = 'BASE TABLE');
SELECT count(*) INTO v_cnt1 FROM information_schema.tables t WHERE EXISTS (SELECT REGEXP_MATCHES(s.table_name, '([A-Z]+)','g') FROM information_schema.tables s
WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = in_schema AND t.table_name = in_table AND t.table_type = 'BASE TABLE');
IF v_cnt1 > 0 THEN
v_table_ddl := 'CREATE ' || v_temp || ' TABLE ' || in_schema || '."' || in_table || '" (' || E'\\n';
ELSE
v_table_ddl := 'CREATE ' || v_temp || ' TABLE ' || in_schema || '.' || in_table || ' (' || E'\\n';
END IF;
END IF;
-- RAISE NOTICE 'DEBUG2: tabledef so far: %', v_table_ddl;
-- RAISE NOTICE 'DEBUG2: tabledef so far: %', v_table_ddl;
-- define all of the columns in the table unless we are in progress creating an inheritance-based child table
IF NOT bPartition THEN
FOR v_colrec IN
SELECT c.column_name, c.data_type, c.udt_name, c.udt_schema, c.character_maximum_length, c.is_nullable, c.column_default, c.numeric_precision, c.numeric_scale, c.is_identity, c.identity_generation, c.is_generated, c.generation_expression
SELECT c.column_name, c.data_type, c.udt_name, c.udt_schema, c.character_maximum_length, c.is_nullable, c.column_default, c.numeric_precision, c.numeric_scale, c.is_identity, c.identity_generation, c.is_generated, c.generation_expression
FROM information_schema.columns c WHERE (table_schema, table_name) = (in_schema, in_table) ORDER BY ordinal_position
LOOP
IF bVerbose THEN RAISE NOTICE '(col loop) name=% type=% udt_name=% default=% is_generated=% gen_expr=%', v_colrec.column_name, v_colrec.data_type, v_colrec.udt_name, v_colrec.column_default, v_colrec.is_generated, v_colrec.generation_expression; END IF;
IF bVerbose THEN RAISE NOTICE '(col loop) name=% type=% udt_name=% default=% is_generated=% gen_expr=%', v_colrec.column_name, v_colrec.data_type, v_colrec.udt_name, v_colrec.column_default, v_colrec.is_generated, v_colrec.generation_expression; END IF;
-- v17 fix: handle case-sensitive for pg_get_serial_sequence that requires SQL Identifier handling
-- SELECT CASE WHEN pg_get_serial_sequence(v_qualified, v_colrec.column_name) IS NOT NULL THEN True ELSE False END into bSerial;
SELECT CASE WHEN pg_get_serial_sequence(quote_ident(in_schema) || '.' || quote_ident(in_table), v_colrec.column_name) IS NOT NULL THEN True ELSE False END into bSerial;
@@ -335,20 +335,20 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
RAISE NOTICE 'DEBUG table: % Column: % datatype: % Serial=% serialval=% coldef=%', v_qualified, v_colrec.column_name, v_colrec.data_type, bSerial, v_temp, v_diag1;
RAISE NOTICE 'DEBUG tabledef: %', v_table_ddl;
END IF;
--Issue#17 put double-quotes around case-sensitive column names
SELECT COUNT(*) INTO v_cnt1 FROM information_schema.columns t WHERE EXISTS (SELECT REGEXP_MATCHES(s.column_name, '([A-Z]+)','g') FROM information_schema.columns s
WHERE t.table_schema=s.table_schema and t.table_name=s.table_name and t.column_name=s.column_name AND t.table_schema = quote_ident(in_schema) AND column_name = v_colrec.column_name);
--Issue#19 put double-quotes around SQL keyword column names
SELECT COUNT(*) INTO v_cnt1 FROM information_schema.columns t WHERE EXISTS (SELECT REGEXP_MATCHES(s.column_name, '([A-Z]+)','g') FROM information_schema.columns s
WHERE t.table_schema=s.table_schema and t.table_name=s.table_name and t.column_name=s.column_name AND t.table_schema = quote_ident(in_schema) AND column_name = v_colrec.column_name);
--Issue#19 put double-quotes around SQL keyword column names
SELECT COUNT(*) INTO v_cnt2 FROM pg_get_keywords() WHERE word = v_colrec.column_name AND catcode = 'R';
IF v_cnt1 > 0 OR v_cnt2 > 0 THEN
v_table_ddl := v_table_ddl || ' "' || v_colrec.column_name || '" ';
ELSE
v_table_ddl := v_table_ddl || ' ' || v_colrec.column_name || ' ';
END IF;
-- Issue#23: Handle autogenerated columns and rewrite as a simpler IF THEN ELSE branch instead of a much more complex embedded CASE STATEMENT
IF v_colrec.is_generated = 'ALWAYS' and v_colrec.generation_expression IS NOT NULL THEN
-- searchable tsvector GENERATED ALWAYS AS (to_tsvector('simple'::regconfig, COALESCE(translate(email, '@.-'::citext, ' '::text), ''::text)) ) STORED
@@ -361,7 +361,7 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
-- Issue#6 fix: handle arrays
v_temp = pg_temp.pg_get_coldef(in_schema, in_table,v_colrec.column_name);
-- v17 fix: handle case-sensitive for pg_get_serial_sequence that requires SQL Identifier handling
-- WHEN pg_get_serial_sequence(v_qualified, v_colrec.column_name) IS NOT NULL
-- WHEN pg_get_serial_sequence(v_qualified, v_colrec.column_name) IS NOT NULL
ELSEIF pg_get_serial_sequence(quote_ident(in_schema) || '.' || quote_ident(in_table), v_colrec.column_name) IS NOT NULL THEN
-- Issue#8 fix: handle serial. Note: NOT NULL is implied so no need to declare it explicitly
v_temp = pg_temp.pg_get_coldef(in_schema, in_table,v_colrec.column_name);
@@ -369,31 +369,31 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
v_temp = v_colrec.data_type;
END IF;
-- RAISE NOTICE 'column def1=%', v_temp;
-- handle IDENTITY columns
IF v_colrec.is_identity = 'YES' THEN
IF v_colrec.identity_generation = 'ALWAYS' THEN
IF v_colrec.identity_generation = 'ALWAYS' THEN
v_temp = v_temp || ' GENERATED ALWAYS AS IDENTITY';
ELSE
v_temp = v_temp || ' GENERATED BY DEFAULT AS IDENTITY';
END IF;
ELSEIF v_colrec.character_maximum_length IS NOT NULL THEN
ELSEIF v_colrec.character_maximum_length IS NOT NULL THEN
v_temp = v_temp || ('(' || v_colrec.character_maximum_length || ')');
ELSEIF v_colrec.numeric_precision > 0 AND v_colrec.numeric_scale > 0 THEN
ELSEIF v_colrec.numeric_precision > 0 AND v_colrec.numeric_scale > 0 THEN
v_temp = v_temp || '(' || v_colrec.numeric_precision || ',' || v_colrec.numeric_scale || ')';
END IF;
-- Handle NULL/NOT NULL
IF bSerial THEN
IF bSerial THEN
v_temp = v_temp || ' NOT NULL';
ELSEIF v_colrec.is_nullable = 'NO' THEN
ELSEIF v_colrec.is_nullable = 'NO' THEN
v_temp = v_temp || ' NOT NULL';
ELSEIF v_colrec.is_nullable = 'YES' THEN
v_temp = v_temp || ' NULL';
END IF;
-- Handle defaults
IF v_colrec.column_default IS NOT null AND NOT bSerial THEN
IF v_colrec.column_default IS NOT null AND NOT bSerial THEN
-- RAISE NOTICE 'Setting default for column, %', v_colrec.column_name;
v_temp = v_temp || (' DEFAULT ' || v_colrec.column_default);
END IF;
@@ -401,11 +401,11 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
-- RAISE NOTICE 'column def2=%', v_temp;
v_table_ddl := v_table_ddl || v_temp;
-- RAISE NOTICE 'tabledef=%', v_table_ddl;
END LOOP;
END IF;
IF bVerbose THEN RAISE NOTICE '(2)tabledef so far: %', v_table_ddl; END IF;
-- define all the constraints: conparentid does not exist pre PGv11
IF v_pgversion < 110000 THEN
FOR v_constraintrec IN
@@ -435,8 +435,8 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|| ',' || E'\\n';
ELSE
-- Issue#16 handle external PG def
SELECT 'ALTER TABLE ONLY ' || in_schema || '.' || c.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_pkey_def
FROM pg_catalog.pg_constraint r, pg_class c, pg_namespace n where r.conrelid = c.oid and r.contype = 'p' and n.oid = r.connamespace and n.nspname = in_schema AND c.relname = in_table and r.conname = v_constraint_name;
SELECT 'ALTER TABLE ONLY ' || in_schema || '.' || c.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_pkey_def
FROM pg_catalog.pg_constraint r, pg_class c, pg_namespace n where r.conrelid = c.oid and r.contype = 'p' and n.oid = r.connamespace and n.nspname = in_schema AND c.relname = in_table and r.conname = v_constraint_name;
END IF;
IF bPartition THEN
continue;
@@ -453,10 +453,10 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|| 'CONSTRAINT' || ' '
|| v_constraint_name || ' '
|| v_constraint_def
|| ',' || E'\\n';
|| ',' || E'\\n';
ELSE
-- external def
SELECT 'ALTER TABLE ONLY ' || n.nspname || '.' || c2.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_fkey_def
SELECT 'ALTER TABLE ONLY ' || n.nspname || '.' || c2.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_fkey_def
FROM pg_constraint r, pg_class c1, pg_namespace n, pg_class c2 where r.conrelid = c1.oid and r.contype = 'f' and n.nspname = in_schema and n.oid = r.connamespace and r.conrelid = c2.oid and c2.relname = in_table;
v_fkey_defs = v_fkey_defs || v_fkey_def || E'\\n';
END IF;
@@ -466,11 +466,11 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|| 'CONSTRAINT' || ' '
|| v_constraint_name || ' '
|| v_constraint_def
|| ',' || E'\\n';
|| ',' || E'\\n';
END IF;
if bVerbose THEN RAISE NOTICE 'DEBUG4: constraint name=% constraint_def=%', v_constraint_name,v_constraint_def; END IF;
constraintarr := constraintarr || v_constraintrec.constraint_name:: text;
END LOOP;
ELSE
-- handle PG versions 11 and up
@@ -486,9 +486,9 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
END as type_rank,
pg_get_constraintdef(con.oid) as constraint_definition
FROM pg_catalog.pg_constraint con JOIN pg_catalog.pg_class rel ON rel.oid = con.conrelid JOIN pg_catalog.pg_namespace nsp ON nsp.oid = connamespace
WHERE nsp.nspname = in_schema AND rel.relname = in_table
WHERE nsp.nspname = in_schema AND rel.relname = in_table
--Issue#13 added this condition:
AND con.conparentid = 0
AND con.conparentid = 0
ORDER BY type_rank
LOOP
v_constraint_name := v_constraintrec.constraint_name;
@@ -505,8 +505,8 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|| ',' || E'\\n';
ELSE
-- Issue#16 handle external PG def
SELECT 'ALTER TABLE ONLY ' || in_schema || '.' || c.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_pkey_def
FROM pg_catalog.pg_constraint r, pg_class c, pg_namespace n where r.conrelid = c.oid and r.contype = 'p' and n.oid = r.connamespace and n.nspname = in_schema AND c.relname = in_table;
SELECT 'ALTER TABLE ONLY ' || in_schema || '.' || c.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_pkey_def
FROM pg_catalog.pg_constraint r, pg_class c, pg_namespace n where r.conrelid = c.oid and r.contype = 'p' and n.oid = r.connamespace and n.nspname = in_schema AND c.relname = in_table;
END IF;
IF bPartition THEN
continue;
@@ -516,18 +516,18 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
--Issue#22 fix: added FKEY_NONE check
IF fktype = 'FKEYS_NONE' THEN
-- skip
continue;
continue;
ELSIF fkcnt = 0 OR fktype = 'FKEYS_INTERNAL' THEN
-- internal def
v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column
|| 'CONSTRAINT' || ' '
|| v_constraint_name || ' '
|| v_constraint_def
|| ',' || E'\\n';
|| ',' || E'\\n';
ELSE
-- external def
SELECT 'ALTER TABLE ONLY ' || n.nspname || '.' || c2.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_fkey_def
FROM pg_constraint r, pg_class c1, pg_namespace n, pg_class c2 where r.conrelid = c1.oid and r.contype = 'f' and n.nspname = in_schema and n.oid = r.connamespace and r.conrelid = c2.oid and c2.relname = in_table and
SELECT 'ALTER TABLE ONLY ' || n.nspname || '.' || c2.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_fkey_def
FROM pg_constraint r, pg_class c1, pg_namespace n, pg_class c2 where r.conrelid = c1.oid and r.contype = 'f' and n.nspname = in_schema and n.oid = r.connamespace and r.conrelid = c2.oid and c2.relname = in_table and
r.conname = v_constraint_name and r.conparentid = 0;
v_fkey_defs = v_fkey_defs || v_fkey_def || E'\\n';
END IF;
@@ -537,14 +537,14 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|| 'CONSTRAINT' || ' '
|| v_constraint_name || ' '
|| v_constraint_def
|| ',' || E'\\n';
|| ',' || E'\\n';
END IF;
if bVerbose THEN RAISE NOTICE 'DEBUG4: constraint name=% constraint_def=%', v_constraint_name,v_constraint_def; END IF;
constraintarr := constraintarr || v_constraintrec.constraint_name:: text;
END LOOP;
END IF;
END IF;
-- drop the last comma before ending the create statement, which should be right before the carriage return character
-- Issue#24: make sure the comma is there before removing it
select substring(v_table_ddl, length(v_table_ddl) - 1, 1) INTO v_temp;
@@ -552,12 +552,12 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
v_table_ddl = substr(v_table_ddl, 0, length(v_table_ddl) - 1) || E'\\n';
END IF;
IF bVerbose THEN RAISE NOTICE '(3)tabledef so far: %', trim(v_table_ddl); END IF;
-- ---------------------------------------------------------------------------
-- at this point we have everything up to the last table-enclosing parenthesis
-- ---------------------------------------------------------------------------
IF bVerbose THEN RAISE NOTICE '(4)tabledef so far: %', v_table_ddl; END IF;
-- See if this is an inheritance-based child table and finish up the table create.
IF bPartition and bInheritance THEN
-- Issue#11: handle parent schema
@@ -565,132 +565,132 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
IF v_parent_schema = '' OR v_parent_schema IS NULL THEN v_parent_schema = in_schema; END IF;
v_table_ddl := v_table_ddl || ') INHERITS (' || v_parent_schema || '.' || v_parent || ') ' || E'\\n' || v_relopts || ' ' || v_tablespace || ';' || E'\\n';
END IF;
IF v_pgversion >= 100000 AND NOT bPartition and NOT bInheritance THEN
-- See if this is a partitioned table (pg_class.relkind = 'p') and add the partitioned key
SELECT pg_get_partkeydef(c1.oid) as partition_key INTO v_partition_key FROM pg_class c1 JOIN pg_namespace n ON (n.oid = c1.relnamespace) LEFT JOIN pg_partitioned_table p ON (c1.oid = p.partrelid)
-- See if this is a partitioned table (pg_class.relkind = 'p') and add the partitioned key
SELECT pg_get_partkeydef(c1.oid) as partition_key INTO v_partition_key FROM pg_class c1 JOIN pg_namespace n ON (n.oid = c1.relnamespace) LEFT JOIN pg_partitioned_table p ON (c1.oid = p.partrelid)
WHERE n.nspname = in_schema and n.oid = c1.relnamespace and c1.relname = in_table and c1.relkind = 'p';
IF v_partition_key IS NOT NULL AND v_partition_key <> '' THEN
-- add partition clause
-- NOTE: cannot specify default tablespace for partitioned relations
-- v_table_ddl := v_table_ddl || ') PARTITION BY ' || v_partition_key || ' ' || v_tablespace || ';' || E'\\n';
v_table_ddl := v_table_ddl || ') PARTITION BY ' || v_partition_key || ';' || E'\\n';
-- v_table_ddl := v_table_ddl || ') PARTITION BY ' || v_partition_key || ' ' || v_tablespace || ';' || E'\\n';
v_table_ddl := v_table_ddl || ') PARTITION BY ' || v_partition_key || ';' || E'\\n';
ELSEIF v_relopts <> '' THEN
v_table_ddl := v_table_ddl || ') ' || v_relopts || ' ' || v_tablespace || ';' || E'\\n';
v_table_ddl := v_table_ddl || ') ' || v_relopts || ' ' || v_tablespace || ';' || E'\\n';
ELSE
-- end the create definition
v_table_ddl := v_table_ddl || ') ' || v_tablespace || ';' || E'\\n';
END IF;
v_table_ddl := v_table_ddl || ') ' || v_tablespace || ';' || E'\\n';
END IF;
END IF;
IF bVerbose THEN RAISE NOTICE '(5)tabledef so far: %', v_table_ddl; END IF;
-- Add closing paren for regular tables
-- IF NOT bPartition THEN
-- v_table_ddl := v_table_ddl || ') ' || v_relopts || ' ' || v_tablespace || E';\\n';
-- v_table_ddl := v_table_ddl || ') ' || v_relopts || ' ' || v_tablespace || E';\\n';
-- END IF;
-- RAISE NOTICE 'ddlsofar3: %', v_table_ddl;
-- Issue#16 create the external PKEY def if indicated
IF v_pkey_def <> '' THEN
v_table_ddl := v_table_ddl || v_pkey_def || E'\\n';
v_table_ddl := v_table_ddl || v_pkey_def || E'\\n';
END IF;
-- Issue#20
IF v_fkey_defs <> '' THEN
v_table_ddl := v_table_ddl || v_fkey_defs || E'\\n';
v_table_ddl := v_table_ddl || v_fkey_defs || E'\\n';
END IF;
IF bVerbose THEN RAISE NOTICE '(6)tabledef so far: %', v_table_ddl; END IF;
-- create indexes
FOR v_indexrec IN
SELECT indexdef, COALESCE(tablespace, 'pg_default') as tablespace, indexname FROM pg_indexes WHERE (schemaname, tablename) = (in_schema, in_table)
LOOP
-- RAISE NOTICE 'DEBUG6: indexname=% indexdef=%', v_indexrec.indexname, v_indexrec.indexdef;
-- RAISE NOTICE 'DEBUG6: indexname=% indexdef=%', v_indexrec.indexname, v_indexrec.indexdef;
-- loop through constraints and skip ones already defined
bSkip = False;
FOREACH constraintelement IN ARRAY constraintarr
LOOP
LOOP
IF constraintelement = v_indexrec.indexname THEN
-- RAISE NOTICE 'DEBUG7: skipping index, %', v_indexrec.indexname;
bSkip = True;
EXIT;
END IF;
END LOOP;
END LOOP;
if bSkip THEN CONTINUE; END IF;
-- Add IF NOT EXISTS clause so partition index additions will not be created if declarative partition in effect and index already created on parent
v_indexrec.indexdef := REPLACE(v_indexrec.indexdef, 'CREATE INDEX', 'CREATE INDEX IF NOT EXISTS');
-- Fix Issue#26: do it for unique/primary key indexes as well
v_indexrec.indexdef := REPLACE(v_indexrec.indexdef, 'CREATE UNIQUE INDEX', 'CREATE UNIQUE INDEX IF NOT EXISTS');
-- RAISE NOTICE 'DEBUG8: adding index, %', v_indexrec.indexname;
-- NOTE: cannot specify default tablespace for partitioned relations
IF v_partition_key IS NOT NULL AND v_partition_key <> '' THEN
v_table_ddl := v_table_ddl || v_indexrec.indexdef || ';' || E'\\n';
ELSE
-- Issue#25: see if partial index or not
select CASE WHEN i.indpred IS NOT NULL THEN True ELSE False END INTO v_partial
FROM pg_index i JOIN pg_class c1 ON (i.indexrelid = c1.oid) JOIN pg_class c2 ON (i.indrelid = c2.oid)
WHERE c1.relnamespace::regnamespace::text = in_schema AND c2.relnamespace::regnamespace::text = in_schema AND c2.relname = in_table AND c1.relname = v_indexrec.indexname;
select CASE WHEN i.indpred IS NOT NULL THEN True ELSE False END INTO v_partial
FROM pg_index i JOIN pg_class c1 ON (i.indexrelid = c1.oid) JOIN pg_class c2 ON (i.indrelid = c2.oid)
WHERE c1.relnamespace::regnamespace::text = in_schema AND c2.relnamespace::regnamespace::text = in_schema AND c2.relname = in_table AND c1.relname = v_indexrec.indexname;
IF v_partial THEN
-- Put tablespace def before WHERE CLAUSE
v_temp = v_indexrec.indexdef;
v_pos = POSITION(' WHERE ' IN v_temp);
v_temp2 = SUBSTRING(v_temp, v_pos);
v_temp = SUBSTRING(v_temp, 1, v_pos);
v_table_ddl := v_table_ddl || v_temp || ' TABLESPACE ' || v_indexrec.tablespace || v_temp2 || ';' || E'\\n';
v_table_ddl := v_table_ddl || v_temp || ' TABLESPACE ' || v_indexrec.tablespace || v_temp2 || ';' || E'\\n';
ELSE
v_table_ddl := v_table_ddl || v_indexrec.indexdef || ' TABLESPACE ' || v_indexrec.tablespace || ';' || E'\\n';
END IF;
END IF;
END LOOP;
IF bVerbose THEN RAISE NOTICE '(7)tabledef so far: %', v_table_ddl; END IF;
-- Issue#20: added logic for table and column comments
IF cmtcnt > 0 THEN
IF cmtcnt > 0 THEN
FOR v_rec IN
SELECT c.relname, 'COMMENT ON ' || CASE WHEN c.relkind in ('r','p') AND a.attname IS NULL THEN 'TABLE ' WHEN c.relkind in ('r','p') AND a.attname IS NOT NULL THEN 'COLUMN ' WHEN c.relkind = 'f' THEN 'FOREIGN TABLE '
WHEN c.relkind = 'm' THEN 'MATERIALIZED VIEW ' WHEN c.relkind = 'v' THEN 'VIEW ' WHEN c.relkind = 'i' THEN 'INDEX ' WHEN c.relkind = 'S' THEN 'SEQUENCE ' ELSE 'XX' END || n.nspname || '.' ||
SELECT c.relname, 'COMMENT ON ' || CASE WHEN c.relkind in ('r','p') AND a.attname IS NULL THEN 'TABLE ' WHEN c.relkind in ('r','p') AND a.attname IS NOT NULL THEN 'COLUMN ' WHEN c.relkind = 'f' THEN 'FOREIGN TABLE '
WHEN c.relkind = 'm' THEN 'MATERIALIZED VIEW ' WHEN c.relkind = 'v' THEN 'VIEW ' WHEN c.relkind = 'i' THEN 'INDEX ' WHEN c.relkind = 'S' THEN 'SEQUENCE ' ELSE 'XX' END || n.nspname || '.' ||
CASE WHEN c.relkind in ('r','p') AND a.attname IS NOT NULL THEN quote_ident(c.relname) || '.' || a.attname ELSE quote_ident(c.relname) END || ' IS ' || quote_literal(d.description) || ';' as ddl
FROM pg_class c JOIN pg_namespace n ON (n.oid = c.relnamespace) LEFT JOIN pg_description d ON (c.oid = d.objoid) LEFT JOIN pg_attribute a ON (c.oid = a.attrelid AND a.attnum > 0 and a.attnum = d.objsubid)
WHERE d.description IS NOT NULL AND n.nspname = in_schema AND c.relname = in_table ORDER BY 2 desc, ddl
LOOP
--RAISE NOTICE 'comments:%', v_rec.ddl;
v_table_ddl = v_table_ddl || v_rec.ddl || E'\\n';
END LOOP;
END LOOP;
END IF;
IF bVerbose THEN RAISE NOTICE '(8)tabledef so far: %', v_table_ddl; END IF;
IF trigtype = 'INCLUDE_TRIGGERS' THEN
-- Issue#14: handle multiple triggers for a table
FOR v_trigrec IN
select pg_get_triggerdef(t.oid, True) || ';' as triggerdef FROM pg_trigger t, pg_class c, pg_namespace n
select pg_get_triggerdef(t.oid, True) || ';' as triggerdef FROM pg_trigger t, pg_class c, pg_namespace n
WHERE n.nspname = in_schema and n.oid = c.relnamespace and c.relname = in_table and c.relkind = 'r' and t.tgrelid = c.oid and NOT t.tgisinternal
LOOP
v_table_ddl := v_table_ddl || v_trigrec.triggerdef;
v_table_ddl := v_table_ddl || E'\\n';
v_table_ddl := v_table_ddl || E'\\n';
IF bVerbose THEN RAISE NOTICE 'triggerdef = %', v_trigrec.triggerdef; END IF;
END LOOP;
END LOOP;
END IF;
IF bVerbose THEN RAISE NOTICE '(9)tabledef so far: %', v_table_ddl; END IF;
-- add empty line
v_table_ddl := v_table_ddl || E'\\n';
IF bVerbose THEN RAISE NOTICE '(10)tabledef so far: %', v_table_ddl; END IF;
-- reset search_path back to what it was
IF search_path_old = '' THEN
SELECT set_config('search_path', '', false) into v_temp;
ELSE
EXECUTE 'SET search_path = ' || search_path_old;
END IF;
RETURN v_table_ddl;
EXCEPTION
WHEN others THEN
BEGIN
@@ -701,7 +701,7 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
-- put additional coding here if necessarY
RETURN '';
END;
END;
$$;`,
{ compress: true, removeAll: true }

View File

@@ -25,7 +25,7 @@ with records as (
'NO_TRIGGERS'
)
when 'v' then concat(
'create view ', concat(nc.nspname, '.', c.relname), ' as',
'create view ', concat(nc.nspname, '.', c.relname), ' as',
pg_get_viewdef(concat(nc.nspname, '.', c.relname), true)
)
when 'm' then concat(

View File

@@ -37,7 +37,8 @@ export async function executeSql(
| 'handleError'
| 'isRoleImpersonationEnabled'
>,
signal?: AbortSignal
signal?: AbortSignal,
headersInit?: HeadersInit
): Promise<{ result: any }> {
if (!projectRef) throw new Error('projectRef is required')
@@ -47,7 +48,7 @@ export async function executeSql(
throw new Error('Query is too large to be run via the SQL Editor')
}
let headers = new Headers()
let headers = new Headers(headersInit)
if (connectionString) headers.set('x-connection-encrypted', connectionString)
let { data, error } = await post('/platform/pg-meta/{ref}/query', {

View File

@@ -10,10 +10,15 @@ import type { ResponseError } from 'types'
type SendEvent = components['schemas']['TelemetryEventBodyV2']
export type SendEventVariables = {
/** Defines the name of the event, refer to TELEMETRY_EVENTS in lib/constants */
action: string
category: string
label: string
/** These are all under the event's properties (customizable on the FE) */
/** value: refer to TELEMETRY_VALUES in lib/constants */
value?: string
/** label: secondary tag to the event for further identification */
label?: string
/** To deprecate - seems unnecessary */
category?: string
}
type SendEventPayload = any

View File

@@ -2,6 +2,11 @@ import { useDisallowHipaa } from 'hooks/misc/useDisallowHipaa'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { OPT_IN_TAGS } from 'lib/constants'
/**
* Checks if the organization has opted into sending anonymous data to OpenAI.
* Also considers if the organization has the HIPAA addon.
* @returns boolean (false if either not opted in or has the HIPAA addon)
*/
export function useOrgOptedIntoAi() {
const selectedOrganization = useSelectedOrganization()
const optInTags = selectedOrganization?.opt_in_tags

View File

@@ -33,7 +33,7 @@ export const LOCAL_STORAGE_KEYS = {
UI_PREVIEW_NAVIGATION_LAYOUT: 'supabase-ui-preview-nav-layout',
UI_PREVIEW_API_SIDE_PANEL: 'supabase-ui-api-side-panel',
UI_PREVIEW_CLS: 'supabase-ui-cls',
UI_PREVIEW_FUNCTIONS_ASSISTANT: 'supabase-ui-functions-assistant',
UI_PREVIEW_ASSISTANT_V2: 'supabase-ui-assistant-v2',
UI_ONBOARDING_NEW_PAGE_SHOWN: 'supabase-ui-onboarding-new-page-shown',
SQL_SCRATCH_PAD_BANNER_ACKNOWLEDGED: 'supabase-sql-scratch-pad-banner-acknowledged',
@@ -83,22 +83,3 @@ export const OPT_IN_TAGS = {
export const GB = 1024 * 1024 * 1024
export const MB = 1024 * 1024
export const KB = 1024
// [Joshen] Just adding these to start consolidating our telemetry configs
// may change depending on how we choose to standardize across all apps
export enum TELEMETRY_CATEGORIES {
AI_ASSISTANT = 'ai-assistant',
}
export enum TELEMETRY_LABELS {
QUICK_SQL_EDITOR = 'quick-sql-editor',
}
export const TELEMETRY_ACTIONS = {
PROMPT_SUBMITTED: 'prompt-submitted',
QUICK_PROMPT_SELECTED: (type: string) => `quick-prompt-selected-${type}`,
SCHEMA_CONTEXT_ADDED: 'schema-context-added',
TABLE_CONTEXT_ADDED: 'table-context-added',
FIX_WITH_ASSISTANT: 'fix-with-assistant',
EXPLAIN_CODE: 'explain-code',
}

View File

@@ -0,0 +1,38 @@
// [Joshen] Just adding these to start consolidating our telemetry configs
// may change depending on how we choose to standardize across all apps
// Events define the name of the event and it'll be used as the primary identification
export enum TELEMETRY_EVENTS {
FEATURE_PREVIEWS = 'Dashboard UI Feature Previews',
AI_ASSISTANT_V2 = 'AI Assistant V2',
}
// [Joshen] Values refer to the "action" of the "event"
// e.g prompt submitted (action) through the AI assistant (event)
// e.g enabled feature x (action) via the feature preview (event)
export enum TELEMETRY_VALUES {
/**
* Track whenever a prompt is submitted to the AI (excluding debug prompts)
* @context AI Assistant V2
* @purpose Indication of engagement with the feature, aid in prioritizing efforts into the assistant itself
*/
PROMPT_SUBMITTED = 'prompt-submitted',
/**
* Track whenever a debug prompt is submitted to the AI
* @context AI Assistant V2
* @purpose TBD
*/
DEBUG_SUBMITTED = 'debug-submitted',
/**
* Track running a SQL suggestion from AI Assistant
* @context AI Assistant V2
* @purpose Indication of usefulness of AI assistant response, aid in prioritizing tweak of Assistant prompts to adjust output quality
* @details Broken down into "select" or "mutation", and for the latter further broken down to the type of query (e.g "functions" or "rls-policies", default to unknown otherwise)
* */
RAN_SQL_SUGGESTION = 'ran-sql-suggestion',
/**
* Track editing a SQL suggestion:
* @context AI Assistant V2
* @purpose Indication of interest for wanting to expand from a SQL suggestion, aid in deciding the priority for an inline editor
* */
EDIT_IN_SQL_EDITOR = 'edit-in-sql-editor',
}

View File

@@ -9,6 +9,7 @@ export const config = {
const HOSTED_SUPPORTED_API_URLS = [
'/ai/sql/suggest',
'/ai/sql/generate-v2',
'/ai/sql/generate-v3',
'/ai/sql/title',
'/ai/sql/debug',
'/ai/sql/cron',

View File

@@ -22,6 +22,7 @@
"prettier:write": "prettier --write ."
},
"dependencies": {
"@ai-sdk/openai": "^0.0.72",
"@dagrejs/dagre": "^1.0.4",
"@graphiql/react": "^0.19.4",
"@graphiql/toolkit": "^0.9.1",
@@ -45,10 +46,11 @@
"@tanstack/react-query": "4.35.7",
"@tanstack/react-query-devtools": "4.35.7",
"@uidotdev/usehooks": "^2.4.1",
"@use-gesture/react": "^10.3.1",
"@vercel/flags": "^2.6.0",
"@vitejs/plugin-react": "^4.2.1",
"@zip.js/zip.js": "^2.7.29",
"ai": "^2.2.31",
"ai": "^3.4.33",
"ai-commands": "*",
"awesome-debounce-promise": "^2.1.0",
"common": "*",
@@ -69,6 +71,7 @@
"lodash": "^4.17.21",
"lucide-react": "^0.436.0",
"markdown-table": "^3.0.3",
"markdown-to-jsx": "^7.5.0",
"memoize-one": "^5.0.1",
"mime-db": "^1.53.0",
"mobx": "^6.10.2",
@@ -124,6 +127,7 @@
"yup": "^1.4.0",
"yup-password": "^0.3.0",
"zod": "^3.22.4",
"zustand": "^5.0.1",
"zxcvbn": "^4.4.2"
},
"devDependencies": {

View File

@@ -156,7 +156,6 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
<StudioCommandMenu />
<GenerateSql />
<FeaturePreviewModal />
<AiAssistantPanel />
</FeaturePreviewContextProvider>
</AppBannerWrapper>
<SonnerToaster position="top-right" />

View File

@@ -0,0 +1,120 @@
import { openai } from '@ai-sdk/openai'
import { streamText } from 'ai'
import { getTools } from './tools'
import pgMeta from '@supabase/pg-meta'
import { executeSql } from 'data/sql/execute-sql-query'
import { NextApiRequest, NextApiResponse } from 'next'
export const maxDuration = 30
const openAiKey = process.env.OPENAI_API_KEY
const pgMetaSchemasList = pgMeta.schemas.list()
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!openAiKey) {
return new Response(
JSON.stringify({
error: 'No OPENAI_API_KEY set. Create this environment variable to use AI features.',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
)
}
const { method } = req
switch (method) {
case 'POST':
return handlePost(req, res)
default:
return new Response(
JSON.stringify({ data: null, error: { message: `Method ${method} Not Allowed` } }),
{
status: 405,
headers: { 'Content-Type': 'application/json', Allow: 'POST' },
}
)
}
}
async function handlePost(req: NextApiRequest, res: NextApiResponse) {
const { messages, projectRef, connectionString, includeSchemaMetadata } = req.body
if (!projectRef) {
return res.status(400).json({
error: 'Missing project_ref in query parameters',
})
}
const authorization = req.headers.authorization
const { result: schemas } = includeSchemaMetadata
? await executeSql(
{
projectRef,
connectionString,
sql: pgMetaSchemasList.sql,
},
undefined,
{
'Content-Type': 'application/json',
...(authorization && { Authorization: authorization }),
}
)
: { result: [] }
const result = await streamText({
model: openai('gpt-4o-mini'),
maxSteps: 5,
system: `
You are a Supabase Postgres expert who can do three things.
# You generate and debug SQL
The generated SQL (must be valid SQL), and must adhere to the following:
- Always use double apostrophe in SQL strings (eg. 'Night''s watch')
- Always use semicolons
- Output as markdown
- Always include code snippets if available
- If a code snippet is SQL, the first line of the snippet should always be -- props: {"title": "Query title", "isChart": "true", "xAxis": "columnName", "yAxis": "columnName"}
- Explain what the snippet does in a sentence or two before showing it
- Use vector(384) data type for any embedding/vector related query
- When debugging, retrieve sql schema details to ensure sql is correct
When generating tables, do the following:
- For primary keys, always use "id bigint primary key generated always as identity" (not serial)
- Prefer creating foreign key references in the create statement
- Prefer 'text' over 'varchar'
- Prefer 'timestamp with time zone' over 'date'
Feel free to suggest corrections for suspected typos.
# You write row level security policies.
Your purpose is to generate a policy with the constraints given by the user.
- First, use getSchema to retrieve more information about a schema or schemas that will contain policies, usually the public schema.
- Then retrieve existing RLS policies and guidelines on how to write policies using the getRlsKnowledge tool .
- Then write new policies or update existing policies based on the prompt
- When asked to suggest policies, either alter existing policies or add new ones to the public schema.
# You write database functions
Your purpose is to generate a database function with the constraints given by the user. The output may also include a database trigger
if the function returns a type of trigger. When generating functions, do the following:
- If the function returns a trigger type, ensure that it uses security definer, otherwise default to security invoker. Include this in the create functions SQL statement.
- Ensure to set the search_path configuration parameter as '', include this in the create functions SQL statement.
- Default to create or replace whenever possible for updating an existing function, otherwise use the alter function statement
Please make sure that all queries are valid Postgres SQL queries
Follow these instructions:
- First look at the list of provided schemas and if needed, get more information about a schema. You will almost always need to retrieve information about the public schema before answering a question. If the question is about users, also retrieve the auth schema.
Here are the existing database schema names you can retrieve: ${schemas}
`,
messages,
tools: getTools({ projectRef, connectionString, authorization, includeSchemaMetadata }),
})
// write the data stream to the response
// Note: this is sent as a single response, not a stream
result.pipeDataStreamToResponse(res)
}

View File

@@ -0,0 +1,326 @@
import { tool } from 'ai'
import { stripIndent } from 'common-tags'
import { z } from 'zod'
import { getDatabasePolicies } from 'data/database-policies/database-policies-query'
import { getEntityDefinitionsSql } from 'data/database/entity-definitions-query'
import { executeSql } from 'data/sql/execute-sql-query'
export const getTools = ({
projectRef,
connectionString,
authorization,
includeSchemaMetadata,
}: {
projectRef: string
connectionString: string
authorization?: string
includeSchemaMetadata: boolean
}) => {
return {
getSchema: tool({
description: 'Get more information about one or more schemas',
parameters: z.object({
schemas: z.array(z.string()).describe('The schema names to get the definitions for'),
}),
execute: async ({ schemas }) => {
try {
const result = includeSchemaMetadata
? await executeSql(
{
projectRef,
connectionString,
sql: getEntityDefinitionsSql({ schemas }),
},
undefined,
{
'Content-Type': 'application/json',
...(authorization && { Authorization: authorization }),
}
)
: { result: [] }
return result
} catch (error) {
console.error('Failed to execute SQL:', error)
return `Failed to fetch schema: ${error}`
}
},
}),
getRlsKnowledge: tool({
description:
'Get existing policies and examples and instructions on how to write RLS policies',
parameters: z.object({
schemas: z.array(z.string()).describe('The schema names to get the policies for'),
}),
execute: async ({ schemas }) => {
const data = includeSchemaMetadata
? await getDatabasePolicies(
{
projectRef,
connectionString,
},
undefined,
{
'Content-Type': 'application/json',
...(authorization && { Authorization: authorization }),
}
)
: []
const formattedPolicies = data
.map(
(policy) => `
Policy Name: "${policy.name}"
Action: ${policy.action}
Roles: ${policy.roles.join(', ')}
Command: ${policy.command}
Definition: ${policy.definition}
${policy.check ? `Check: ${policy.check}` : ''}
`
)
.join('\n')
return stripIndent`
You're a Supabase Postgres expert in writing row level security policies. Your purpose is to
generate a policy with the constraints given by the user. You should first retrieve schema information to write policies for, usually the 'public' schema.
The output should use the following instructions:
- The generated SQL must be valid SQL.
- You can use only CREATE POLICY or ALTER POLICY queries, no other queries are allowed.
- Always use double apostrophe in SQL strings (eg. 'Night''s watch')
- You can add short explanations to your messages.
- The result should be a valid markdown. The SQL code should be wrapped in \`\`\` (including sql language tag).
- Always use "auth.uid()" instead of "current_user".
- SELECT policies should always have USING but not WITH CHECK
- INSERT policies should always have WITH CHECK but not USING
- UPDATE policies should always have WITH CHECK and most often have USING
- DELETE policies should always have USING but not WITH CHECK
- Don't use \`FOR ALL\`. Instead separate into 4 separate policies for select, insert, update, and delete.
- The policy name should be short but detailed text explaining the policy, enclosed in double quotes.
- Always put explanations as separate text. Never use inline SQL comments.
- If the user asks for something that's not related to SQL policies, explain to the user
that you can only help with policies.
- Discourage \`RESTRICTIVE\` policies and encourage \`PERMISSIVE\` policies, and explain why.
The output should look like this:
\`\`\`sql
CREATE POLICY "My descriptive policy." ON books FOR INSERT to authenticated USING ( (select auth.uid()) = author_id ) WITH ( true );
\`\`\`
Since you are running in a Supabase environment, take note of these Supabase-specific additions:
## Authenticated and unauthenticated roles
Supabase maps every request to one of the roles:
- \`anon\`: an unauthenticated request (the user is not logged in)
- \`authenticated\`: an authenticated request (the user is logged in)
These are actually [Postgres Roles](/docs/guides/database/postgres/roles). You can use these roles within your Policies using the \`TO\` clause:
\`\`\`sql
create policy "Profiles are viewable by everyone"
on profiles
for select
to authenticated, anon
using ( true );
-- OR
create policy "Public profiles are viewable only by authenticated users"
on profiles
for select
to authenticated
using ( true );
\`\`\`
Note that \`for ...\` must be added after the table but before the roles. \`to ...\` must be added after \`for ...\`:
### Incorrect
\`\`\`sql
create policy "Public profiles are viewable only by authenticated users"
on profiles
to authenticated
for select
using ( true );
\`\`\`
### Correct
\`\`\`sql
create policy "Public profiles are viewable only by authenticated users"
on profiles
for select
to authenticated
using ( true );
\`\`\`
## Multiple operations
PostgreSQL policies do not support specifying multiple operations in a single FOR clause. You need to create separate policies for each operation.
### Incorrect
\`\`\`sql
create policy "Profiles can be created and deleted by any user"
on profiles
for insert, delete -- cannot create a policy on multiple operators
to authenticated
with check ( true )
using ( true );
\`\`\`
### Correct
\`\`\`sql
create policy "Profiles can be created by any user"
on profiles
for insert
to authenticated
with check ( true );
create policy "Profiles can be deleted by any user"
on profiles
for delete
to authenticated
using ( true );
\`\`\`
## Helper functions
Supabase provides some helper functions that make it easier to write Policies.
### \`auth.uid()\`
Returns the ID of the user making the request.
### \`auth.jwt()\`
Returns the JWT of the user making the request. Anything that you store in the user's \`raw_app_meta_data\` column or the \`raw_user_meta_data\` column will be accessible using this function. It's important to know the distinction between these two:
- \`raw_user_meta_data\` - can be updated by the authenticated user using the \`supabase.auth.update()\` function. It is not a good place to store authorization data.
- \`raw_app_meta_data\` - cannot be updated by the user, so it's a good place to store authorization data.
The \`auth.jwt()\` function is extremely versatile. For example, if you store some team data inside \`app_metadata\`, you can use it to determine whether a particular user belongs to a team. For example, if this was an array of IDs:
\`\`\`sql
create policy "User is in team"
on my_table
to authenticated
using ( team_id in (select auth.jwt() -> 'app_metadata' -> 'teams'));
\`\`\`
### MFA
The \`auth.jwt()\` function can be used to check for [Multi-Factor Authentication](/docs/guides/auth/auth-mfa#enforce-rules-for-mfa-logins). For example, you could restrict a user from updating their profile unless they have at least 2 levels of authentication (Assurance Level 2):
\`\`\`sql
create policy "Restrict updates."
on profiles
as restrictive
for update
to authenticated using (
(select auth.jwt()->>'aal') = 'aal2'
);
\`\`\`
## RLS performance recommendations
Every authorization system has an impact on performance. While row level security is powerful, the performance impact is important to keep in mind. This is especially true for queries that scan every row in a table - like many \`select\` operations, including those using limit, offset, and ordering.
Based on a series of [tests](https://github.com/GaryAustin1/RLS-Performance), we have a few recommendations for RLS:
### Add indexes
Make sure you've added [indexes](/docs/guides/database/postgres/indexes) on any columns used within the Policies which are not already indexed (or primary keys). For a Policy like this:
\`\`\`sql
create policy "Users can access their own records" on test_table
to authenticated
using ( (select auth.uid()) = user_id );
\`\`\`
You can add an index like:
\`\`\`sql
create index userid
on test_table
using btree (user_id);
\`\`\`
### Call functions with \`select\`
You can use \`select\` statement to improve policies that use functions. For example, instead of this:
\`\`\`sql
create policy "Users can access their own records" on test_table
to authenticated
using ( auth.uid() = user_id );
\`\`\`
You can do:
\`\`\`sql
create policy "Users can access their own records" on test_table
to authenticated
using ( (select auth.uid()) = user_id );
\`\`\`
This method works well for JWT functions like \`auth.uid()\` and \`auth.jwt()\` as well as \`security definer\` Functions. Wrapping the function causes an \`initPlan\` to be run by the Postgres optimizer, which allows it to "cache" the results per-statement, rather than calling the function on each row.
Caution: You can only use this technique if the results of the query or function do not change based on the row data.
### Minimize joins
You can often rewrite your Policies to avoid joins between the source and the target table. Instead, try to organize your policy to fetch all the relevant data from the target table into an array or set, then you can use an \`IN\` or \`ANY\` operation in your filter.
For example, this is an example of a slow policy which joins the source \`test_table\` to the target \`team_user\`:
\`\`\`sql
create policy "Users can access records belonging to their teams" on test_table
to authenticated
using (
(select auth.uid()) in (
select user_id
from team_user
where team_user.team_id = team_id -- joins to the source "test_table.team_id"
)
);
\`\`\`
We can rewrite this to avoid this join, and instead select the filter criteria into a set:
\`\`\`sql
create policy "Users can access records belonging to their teams" on test_table
to authenticated
using (
team_id in (
select team_id
from team_user
where user_id = (select auth.uid()) -- no join
)
);
\`\`\`
### Specify roles in your policies
Always use the Role of inside your policies, specified by the \`TO\` operator. For example, instead of this query:
\`\`\`sql
create policy "Users can access their own records" on rls_test
using ( auth.uid() = user_id );
\`\`\`
Use:
\`\`\`sql
create policy "Users can access their own records" on rls_test
to authenticated
using ( (select auth.uid()) = user_id );
\`\`\`
This prevents the policy \`( (select auth.uid()) = user_id )\` from running for any \`anon\` users, since the execution stops at the \`to authenticated\` step.
${data.length > 0 ? `Here are my existing policies: ${formattedPolicies}` : ''}
`
},
}),
}
}

View File

@@ -1,14 +1,16 @@
import type { PostgresPolicy, PostgresTable } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { partition } from 'lodash'
import { ExternalLink, Search } from 'lucide-react'
import { Search } from 'lucide-react'
import { useState } from 'react'
import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { AIPolicyEditorPanel } from 'components/interfaces/Auth/Policies/AIPolicyEditorPanel'
import Policies from 'components/interfaces/Auth/Policies/Policies'
import AuthLayout from 'components/layouts/AuthLayout/AuthLayout'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import AlertError from 'components/ui/AlertError'
import { DocsButton } from 'components/ui/DocsButton'
import NoPermission from 'components/ui/NoPermission'
import SchemaSelector from 'components/ui/SchemaSelector'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
@@ -18,9 +20,9 @@ import { useTablesQuery } from 'data/tables/tables-query'
import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions'
import { useUrlState } from 'hooks/ui/useUrlState'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import { useAppStateSnapshot } from 'state/app-state'
import type { NextPageWithLayout } from 'types'
import { Button, Input } from 'ui'
import { DocsButton } from 'components/ui/DocsButton'
import { Input } from 'ui'
/**
* Filter tables by table name and policy name
@@ -64,6 +66,8 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
}>()
const { schema = 'public', search: searchString = '' } = params
const { project } = useProjectContext()
const isAssistantV2Enabled = useIsAssistantV2Enabled()
const { setAiAssistantPanel } = useAppStateSnapshot()
const [selectedTable, setSelectedTable] = useState<string>()
const [showPolicyAiEditor, setShowPolicyAiEditor] = useState(false)
@@ -99,9 +103,7 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
const filteredTables = onFilterTables(tables ?? [], policies ?? [], searchString)
const canReadPolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'policies')
const canCreatePolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'policies')
const isPermissionsLoaded = usePermissionsLoaded()
const schemaHasNoTables = (tables ?? []).length === 0
if (isPermissionsLoaded && !canReadPolicies) {
return <NoPermission isFullPage resourceText="view this project's RLS policies" />
@@ -113,8 +115,8 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<SchemaSelector
className="w-[260px]"
size="small"
className="w-[180px]"
size="tiny"
showError={false}
selectedSchemaName={schema}
onSelectSchema={(schema) => {
@@ -122,9 +124,9 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
}}
/>
<Input
size="small"
size="tiny"
placeholder="Filter tables and policies"
className="block w-64 text-sm placeholder-border-muted"
className="block w-52 text-sm placeholder-border-muted"
value={searchString || ''}
onChange={(e) => {
const str = e.target.value
@@ -148,8 +150,8 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
hasTables={tables.length > 0}
isLocked={isLocked}
onSelectCreatePolicy={(table: string) => {
setShowPolicyAiEditor(true)
setSelectedTable(table)
setShowPolicyAiEditor(true)
}}
onSelectEditPolicy={(policy) => {
setSelectedPolicyToEdit(policy)

View File

@@ -36,7 +36,7 @@ const IndexesPage: NextPageWithLayout = () => {
className="no-underline"
href="https://supabase.com/docs/guides/database/extensions/index_advisor"
>
Optimization with index_advisor
Index Advisor
</a>
</Button>
</ScaffoldSectionDetail>

View File

@@ -17,7 +17,7 @@ const DatabaseEnumeratedTypes: NextPageWithLayout = () => {
return (
<ScaffoldContainer>
<ScaffoldSection>
<ScaffoldSectionContent>
<ScaffoldSectionContent className="!col-span-12">
<FormHeader
className="!mb-0"
title="Database Enumerated Types"

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 269 KiB

View File

@@ -3,6 +3,7 @@ import { proxy, snapshot, useSnapshot } from 'valtio'
import { SupportedAssistantEntities } from 'components/ui/AIAssistantPanel/AIAssistant.types'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { LOCAL_STORAGE_KEYS as COMMON_LOCAL_STORAGE_KEYS } from 'common'
import type { Message as MessageType } from 'ai/react'
const EMPTY_DASHBOARD_HISTORY: {
sql?: string
@@ -19,8 +20,17 @@ export type CommonDatabaseEntity = {
[key: string]: any
}
export type SuggestionsType = {
title: string
prompts: string[]
}
type AiAssistantPanelType = {
open: boolean
messages?: MessageType[] | undefined
initialInput: string
sqlSnippets?: string[]
suggestions?: SuggestionsType
editor?: SupportedAssistantEntities | null
// Raw string content for the monaco editor, currently used to retain where the user left off when toggling off the panel
content?: string
@@ -29,6 +39,18 @@ type AiAssistantPanelType = {
tables: { schema: string; name: string }[]
}
const INITIAL_AI_ASSISTANT: AiAssistantPanelType = {
open: false,
messages: undefined,
sqlSnippets: undefined,
initialInput: '',
suggestions: undefined,
editor: null,
content: '',
entity: undefined,
tables: [],
}
export const appState = proxy({
// [Joshen] Last visited "entity" for any page that we wanna track
dashboardHistory: EMPTY_DASHBOARD_HISTORY,
@@ -108,13 +130,14 @@ export const appState = proxy({
appState.navigationPanelJustClosed = value
},
aiAssistantPanel: {
open: false,
editor: null,
content: '',
entity: undefined,
tables: [],
} as AiAssistantPanelType,
resetAiAssistantPanel: () => {
appState.aiAssistantPanel = {
...INITIAL_AI_ASSISTANT,
open: appState.aiAssistantPanel.open,
}
},
aiAssistantPanel: INITIAL_AI_ASSISTANT as AiAssistantPanelType,
setAiAssistantPanel: (value: Partial<AiAssistantPanelType>) => {
const hasEntityChanged = value.entity?.id !== appState.aiAssistantPanel.entity?.id

View File

@@ -11,6 +11,7 @@ import { Snippet, SnippetFolder, SnippetFolderResponse } from 'data/content/sql-
import { SqlSnippet } from 'data/content/sql-snippets-query'
import { getQueryClient } from 'data/query-client'
import { getContentById } from 'data/content/content-id-query'
import { DiffType } from 'components/interfaces/SQLEditor/SQLEditor.types'
export type StateSnippetFolder = {
projectRef: string
@@ -76,6 +77,9 @@ export const sqlEditorState = proxy({
// For handling renaming folder failed
lastUpdatedFolderName: '',
// For Assistant to render diffing into the editor
diffContent: undefined as undefined | { sql: string; diffType: DiffType },
get allFolderNames() {
return Object.values(sqlEditorState.folders).map((x) => x.folder.name)
},
@@ -121,6 +125,9 @@ export const sqlEditorState = proxy({
})
},
setDiffContent: (sql: string, diffType: DiffType) =>
(sqlEditorState.diffContent = { sql, diffType }),
setOrder: (value: 'name' | 'inserted_at') => (sqlEditorState.order = value),
setPrivateSnippetCount: ({ projectRef, value }: { projectRef: string; value: number }) => {

686
package-lock.json generated
View File

@@ -1782,6 +1782,7 @@
"apps/studio": {
"version": "0.0.9",
"dependencies": {
"@ai-sdk/openai": "^0.0.72",
"@dagrejs/dagre": "^1.0.4",
"@graphiql/react": "^0.19.4",
"@graphiql/toolkit": "^0.9.1",
@@ -1805,10 +1806,11 @@
"@tanstack/react-query": "4.35.7",
"@tanstack/react-query-devtools": "4.35.7",
"@uidotdev/usehooks": "^2.4.1",
"@use-gesture/react": "^10.3.1",
"@vercel/flags": "^2.6.0",
"@vitejs/plugin-react": "^4.2.1",
"@zip.js/zip.js": "^2.7.29",
"ai": "^2.2.31",
"ai": "^3.4.33",
"ai-commands": "*",
"awesome-debounce-promise": "^2.1.0",
"common": "*",
@@ -1829,6 +1831,7 @@
"lodash": "^4.17.21",
"lucide-react": "^0.436.0",
"markdown-table": "^3.0.3",
"markdown-to-jsx": "^7.5.0",
"memoize-one": "^5.0.1",
"mime-db": "^1.53.0",
"mobx": "^6.10.2",
@@ -1884,6 +1887,7 @@
"yup": "^1.4.0",
"yup-password": "^0.3.0",
"zod": "^3.22.4",
"zustand": "^5.0.1",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
@@ -1960,6 +1964,15 @@
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
}
},
"apps/studio/node_modules/@opentelemetry/api": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"engines": {
"node": ">=8.0.0"
}
},
"apps/studio/node_modules/@storybook/addon-backgrounds": {
"version": "7.6.17",
"resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-7.6.17.tgz",
@@ -2810,6 +2823,76 @@
"node": ">=0.4.0"
}
},
"apps/studio/node_modules/ai": {
"version": "3.4.33",
"resolved": "https://registry.npmjs.org/ai/-/ai-3.4.33.tgz",
"integrity": "sha512-plBlrVZKwPoRTmM8+D1sJac9Bq8eaa2jiZlHLZIWekKWI1yMWYZvCCEezY9ASPwRhULYDJB2VhKOBUUeg3S5JQ==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "0.0.26",
"@ai-sdk/provider-utils": "1.0.22",
"@ai-sdk/react": "0.0.70",
"@ai-sdk/solid": "0.0.54",
"@ai-sdk/svelte": "0.0.57",
"@ai-sdk/ui-utils": "0.0.50",
"@ai-sdk/vue": "0.0.59",
"@opentelemetry/api": "1.9.0",
"eventsource-parser": "1.1.2",
"json-schema": "^0.4.0",
"jsondiffpatch": "0.6.0",
"secure-json-parse": "^2.7.0",
"zod-to-json-schema": "^3.23.3"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"openai": "^4.42.0",
"react": "^18 || ^19 || ^19.0.0-rc",
"sswr": "^2.1.0",
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0",
"zod": "^3.0.0"
},
"peerDependenciesMeta": {
"openai": {
"optional": true
},
"react": {
"optional": true
},
"sswr": {
"optional": true
},
"svelte": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"apps/studio/node_modules/ai/node_modules/@ai-sdk/vue": {
"version": "0.0.59",
"resolved": "https://registry.npmjs.org/@ai-sdk/vue/-/vue-0.0.59.tgz",
"integrity": "sha512-+ofYlnqdc8c4F6tM0IKF0+7NagZRAiqBJpGDJ+6EYhDW8FHLUP/JFBgu32SjxSxC6IKFZxEnl68ZoP/Z38EMlw==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider-utils": "1.0.22",
"@ai-sdk/ui-utils": "0.0.50",
"swrv": "^1.0.4"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"vue": "^3.3.4"
},
"peerDependenciesMeta": {
"vue": {
"optional": true
}
}
},
"apps/studio/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -2845,6 +2928,15 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"apps/studio/node_modules/eventsource-parser": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz",
"integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==",
"license": "MIT",
"engines": {
"node": ">=14.18"
}
},
"apps/studio/node_modules/framer-motion": {
"version": "11.3.31",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.3.31.tgz",
@@ -2947,6 +3039,20 @@
"sql-formatter": "bin/sql-formatter-cli.cjs"
}
},
"apps/studio/node_modules/sswr": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/sswr/-/sswr-2.1.0.tgz",
"integrity": "sha512-Cqc355SYlTAaUt8iDPaC/4DPPXK925PePLMxyBKuWd5kKc5mwsG3nT9+Mq2tyguL5s7b4Jg+IRMpTRsNTAfpSQ==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"swrev": "^4.0.0"
},
"peerDependencies": {
"svelte": "^4.0.0 || ^5.0.0-next.0"
}
},
"apps/studio/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -2965,6 +3071,35 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"apps/studio/node_modules/zustand": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.1.tgz",
"integrity": "sha512-pRET7Lao2z+n5R/HduXMio35TncTlSW68WsYBq2Lg1ASspsNGjpwLAsij3RpouyV6+kHMwwwzP0bZPD70/Jx/w==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=18.0.0",
"immer": ">=9.0.6",
"react": ">=18.0.0",
"use-sync-external-store": ">=1.2.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
},
"use-sync-external-store": {
"optional": true
}
}
},
"apps/www": {
"version": "0.0.3",
"license": "MIT",
@@ -3625,6 +3760,202 @@
"integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==",
"dev": true
},
"node_modules/@ai-sdk/openai": {
"version": "0.0.72",
"resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-0.0.72.tgz",
"integrity": "sha512-IKsgxIt6KJGkEHyMp975xW5VPmetwhI8g9H6dDmwvemBB41IRQa78YMNttiJqPcgmrZX2QfErOICv1gQvZ1gZg==",
"dependencies": {
"@ai-sdk/provider": "0.0.26",
"@ai-sdk/provider-utils": "1.0.22"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.0.0"
}
},
"node_modules/@ai-sdk/provider": {
"version": "0.0.26",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-0.0.26.tgz",
"integrity": "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg==",
"license": "Apache-2.0",
"dependencies": {
"json-schema": "^0.4.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@ai-sdk/provider-utils": {
"version": "1.0.22",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-1.0.22.tgz",
"integrity": "sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "0.0.26",
"eventsource-parser": "^1.1.2",
"nanoid": "^3.3.7",
"secure-json-parse": "^2.7.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.0.0"
},
"peerDependenciesMeta": {
"zod": {
"optional": true
}
}
},
"node_modules/@ai-sdk/provider-utils/node_modules/eventsource-parser": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz",
"integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==",
"license": "MIT",
"engines": {
"node": ">=14.18"
}
},
"node_modules/@ai-sdk/provider-utils/node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/@ai-sdk/react": {
"version": "0.0.70",
"resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-0.0.70.tgz",
"integrity": "sha512-GnwbtjW4/4z7MleLiW+TOZC2M29eCg1tOUpuEiYFMmFNZK8mkrqM0PFZMo6UsYeUYMWqEOOcPOU9OQVJMJh7IQ==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider-utils": "1.0.22",
"@ai-sdk/ui-utils": "0.0.50",
"swr": "^2.2.5",
"throttleit": "2.1.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"react": "^18 || ^19 || ^19.0.0-rc",
"zod": "^3.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/@ai-sdk/react/node_modules/swr": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/swr/-/swr-2.2.5.tgz",
"integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==",
"license": "MIT",
"dependencies": {
"client-only": "^0.0.1",
"use-sync-external-store": "^1.2.0"
},
"peerDependencies": {
"react": "^16.11.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@ai-sdk/solid": {
"version": "0.0.54",
"resolved": "https://registry.npmjs.org/@ai-sdk/solid/-/solid-0.0.54.tgz",
"integrity": "sha512-96KWTVK+opdFeRubqrgaJXoNiDP89gNxFRWUp0PJOotZW816AbhUf4EnDjBjXTLjXL1n0h8tGSE9sZsRkj9wQQ==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider-utils": "1.0.22",
"@ai-sdk/ui-utils": "0.0.50"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"solid-js": "^1.7.7"
},
"peerDependenciesMeta": {
"solid-js": {
"optional": true
}
}
},
"node_modules/@ai-sdk/svelte": {
"version": "0.0.57",
"resolved": "https://registry.npmjs.org/@ai-sdk/svelte/-/svelte-0.0.57.tgz",
"integrity": "sha512-SyF9ItIR9ALP9yDNAD+2/5Vl1IT6kchgyDH8xkmhysfJI6WrvJbtO1wdQ0nylvPLcsPoYu+cAlz1krU4lFHcYw==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider-utils": "1.0.22",
"@ai-sdk/ui-utils": "0.0.50",
"sswr": "^2.1.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0"
},
"peerDependenciesMeta": {
"svelte": {
"optional": true
}
}
},
"node_modules/@ai-sdk/svelte/node_modules/sswr": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/sswr/-/sswr-2.1.0.tgz",
"integrity": "sha512-Cqc355SYlTAaUt8iDPaC/4DPPXK925PePLMxyBKuWd5kKc5mwsG3nT9+Mq2tyguL5s7b4Jg+IRMpTRsNTAfpSQ==",
"license": "MIT",
"dependencies": {
"swrev": "^4.0.0"
},
"peerDependencies": {
"svelte": "^4.0.0 || ^5.0.0-next.0"
}
},
"node_modules/@ai-sdk/ui-utils": {
"version": "0.0.50",
"resolved": "https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-0.0.50.tgz",
"integrity": "sha512-Z5QYJVW+5XpSaJ4jYCCAVG7zIAuKOOdikhgpksneNmKvx61ACFaf98pmOd+xnjahl0pIlc/QIe6O4yVaJ1sEaw==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "0.0.26",
"@ai-sdk/provider-utils": "1.0.22",
"json-schema": "^0.4.0",
"secure-json-parse": "^2.7.0",
"zod-to-json-schema": "^3.23.3"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.0.0"
},
"peerDependenciesMeta": {
"zod": {
"optional": true
}
}
},
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"license": "MIT",
@@ -16718,6 +17049,12 @@
"@types/ms": "*"
}
},
"node_modules/@types/diff-match-patch": {
"version": "1.0.36",
"resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz",
"integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==",
"license": "MIT"
},
"node_modules/@types/doctrine": {
"version": "0.0.3",
"dev": true,
@@ -17522,6 +17859,24 @@
"version": "1.2.0",
"license": "ISC"
},
"node_modules/@use-gesture/core": {
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz",
"integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==",
"license": "MIT"
},
"node_modules/@use-gesture/react": {
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz",
"integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==",
"license": "MIT",
"dependencies": {
"@use-gesture/core": "10.3.1"
},
"peerDependencies": {
"react": ">= 16.8.0"
}
},
"node_modules/@vercel/flags": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@vercel/flags/-/flags-2.6.0.tgz",
@@ -18201,43 +18556,6 @@
"node": ">=8"
}
},
"node_modules/ai": {
"version": "2.2.31",
"resolved": "https://registry.npmjs.org/ai/-/ai-2.2.31.tgz",
"integrity": "sha512-WQH13RxP+RYo9IE/FX8foNQh9gcKO/dhl9OGy5JL2bHJVBlnugPmH2CYJWaRt+mvjXHaU8txB+jzGo/fbtH2HA==",
"dependencies": {
"eventsource-parser": "1.0.0",
"nanoid": "3.3.6",
"solid-swr-store": "0.10.7",
"sswr": "2.0.0",
"swr": "2.2.0",
"swr-store": "0.10.6",
"swrv": "1.0.4"
},
"engines": {
"node": ">=14.6"
},
"peerDependencies": {
"react": "^18.2.0",
"solid-js": "^1.7.7",
"svelte": "^3.0.0 || ^4.0.0",
"vue": "^3.3.4"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"solid-js": {
"optional": true
},
"svelte": {
"optional": true
},
"vue": {
"optional": true
}
}
},
"node_modules/ai-commands": {
"resolved": "packages/ai-commands",
"link": true
@@ -19066,9 +19384,6 @@
"version": "1.0.2",
"license": "MIT"
},
"node_modules/base-64": {
"version": "0.1.0"
},
"node_modules/base64-js": {
"version": "1.5.1",
"funding": [
@@ -19524,13 +19839,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/charenc": {
"version": "0.0.2",
"license": "BSD-3-Clause",
"engines": {
"node": "*"
}
},
"node_modules/check-error": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
@@ -20336,13 +20644,6 @@
"node": ">= 8"
}
},
"node_modules/crypt": {
"version": "0.0.2",
"license": "BSD-3-Clause",
"engines": {
"node": "*"
}
},
"node_modules/crypto-random-string": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
@@ -21099,6 +21400,12 @@
"node": ">=0.3.1"
}
},
"node_modules/diff-match-patch": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
"integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==",
"license": "Apache-2.0"
},
"node_modules/diff-sequences": {
"version": "29.6.3",
"license": "MIT",
@@ -21106,14 +21413,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/digest-fetch": {
"version": "1.3.0",
"license": "ISC",
"dependencies": {
"base-64": "^0.1.0",
"md5": "^2.3.0"
}
},
"node_modules/dir-glob": {
"version": "3.0.1",
"license": "MIT",
@@ -22873,14 +23172,6 @@
"node": ">=0.8.x"
}
},
"node_modules/eventsource-parser": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.0.0.tgz",
"integrity": "sha512-9jgfSCa3dmEme2ES3mPByGXfgZ87VbP97tng1G2nWwWx6bV2nYxm2AWCrbQjXToSe+yYlqaZNtxffR9IeQr95g==",
"engines": {
"node": ">=14.18"
}
},
"node_modules/execa": {
"version": "5.1.1",
"dev": true,
@@ -28148,6 +28439,12 @@
"foreach": "^2.0.4"
}
},
"node_modules/json-schema": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
"license": "(AFL-2.1 OR BSD-3-Clause)"
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"license": "MIT"
@@ -28183,6 +28480,35 @@
"dev": true,
"license": "MIT"
},
"node_modules/jsondiffpatch": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz",
"integrity": "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==",
"license": "MIT",
"dependencies": {
"@types/diff-match-patch": "^1.0.36",
"chalk": "^5.3.0",
"diff-match-patch": "^1.0.5"
},
"bin": {
"jsondiffpatch": "bin/jsondiffpatch.js"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
}
},
"node_modules/jsondiffpatch/node_modules/chalk": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/jsonfile": {
"version": "6.1.0",
"dev": true,
@@ -28911,10 +29237,10 @@
}
},
"node_modules/markdown-to-jsx": {
"version": "7.4.7",
"resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.4.7.tgz",
"integrity": "sha512-0+ls1IQZdU6cwM1yu0ZjjiVWYtkbExSyUIFU2ZeDIFuZM1W42Mh4OlJ4nb4apX4H8smxDHRdFaoIVJGwfv5hkg==",
"dev": true,
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.5.0.tgz",
"integrity": "sha512-RrBNcMHiFPcz/iqIj0n3wclzHXjwS7mzjBNWecKKVhNTIxQepIix6Il/wZCn2Cg5Y1ow2Qi84+eJrryFRWBEWw==",
"license": "MIT",
"engines": {
"node": ">= 10"
},
@@ -28981,15 +29307,6 @@
"version": "1.0.4",
"license": "MIT"
},
"node_modules/md5": {
"version": "2.3.0",
"license": "BSD-3-Clause",
"dependencies": {
"charenc": "0.0.2",
"crypt": "0.0.2",
"is-buffer": "~1.1.6"
}
},
"node_modules/mdast": {
"version": "3.0.0",
"license": "MIT"
@@ -34419,22 +34736,29 @@
}
},
"node_modules/openai": {
"version": "4.26.1",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.26.1.tgz",
"integrity": "sha512-DvWbjhWbappsFRatOWmu4Dp1/Q4RG9oOz6CfOSjy0/Drb8G+5iAiqWAO4PfpGIkhOOKtvvNfQri2SItl+U7LhQ==",
"version": "4.71.1",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.71.1.tgz",
"integrity": "sha512-C6JNMaQ1eijM0lrjiRUL3MgThVP5RdwNAghpbJFdW0t11LzmyqON8Eh8MuUuEZ+CeD6bgYl2Fkn2BoptVxv9Ug==",
"license": "Apache-2.0",
"dependencies": {
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
"abort-controller": "^3.0.0",
"agentkeepalive": "^4.2.1",
"digest-fetch": "^1.3.0",
"form-data-encoder": "1.7.2",
"formdata-node": "^4.3.2",
"node-fetch": "^2.6.7",
"web-streams-polyfill": "^3.2.1"
"node-fetch": "^2.6.7"
},
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"zod": "^3.23.8"
},
"peerDependenciesMeta": {
"zod": {
"optional": true
}
}
},
"node_modules/openai/node_modules/@types/node": {
@@ -39240,6 +39564,12 @@
"node": ">=4"
}
},
"node_modules/secure-json-parse": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
"integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==",
"license": "BSD-3-Clause"
},
"node_modules/semver": {
"version": "6.3.1",
"license": "ISC",
@@ -39270,6 +39600,7 @@
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-0.14.1.tgz",
"integrity": "sha512-ZlC9y1KVDhZFdEHLYZup1RjKDutyX1tt3ffOauqRbRURa2vRr2NU/bHuVEuNEqR3zE2uCU3WM6LqH6Oinc3tWg==",
"optional": true,
"peer": true,
"engines": {
"node": ">=10"
@@ -39713,24 +40044,13 @@
"version": "1.8.6",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.6.tgz",
"integrity": "sha512-yiH6ZfBBZ3xj/aU/PBpVKB+8r8WWp100NGF7k/Z0IrK9Y8Lv0jwvFiJY1cHdc6Tj7GqXArKnMBabM0m1k+LzkA==",
"optional": true,
"peer": true,
"dependencies": {
"csstype": "^3.1.0",
"seroval": "^0.14.1"
}
},
"node_modules/solid-swr-store": {
"version": "0.10.7",
"resolved": "https://registry.npmjs.org/solid-swr-store/-/solid-swr-store-0.10.7.tgz",
"integrity": "sha512-A6d68aJmRP471aWqKKPE2tpgOiR5fH4qXQNfKIec+Vap+MGQm3tvXlT8n0I8UgJSlNAsSAUuw2VTviH2h3Vv5g==",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"solid-js": "^1.2",
"swr-store": "^0.10"
}
},
"node_modules/sonner": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-1.5.0.tgz",
@@ -39886,17 +40206,6 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/sswr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/sswr/-/sswr-2.0.0.tgz",
"integrity": "sha512-mV0kkeBHcjcb0M5NqKtKVg/uTIYNlIIniyDfSGrSfxpEdM9C365jK0z55pl9K0xAkNTJi2OAOVFQpgMPUk+V0w==",
"dependencies": {
"swrev": "^4.0.0"
},
"peerDependencies": {
"svelte": "^4.0.0"
}
},
"node_modules/stack-generator": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz",
@@ -40932,28 +41241,6 @@
"node": ">= 4.7.0"
}
},
"node_modules/swr": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/swr/-/swr-2.2.0.tgz",
"integrity": "sha512-AjqHOv2lAhkuUdIiBu9xbuettzAzWXmCEcLONNKJRba87WAefz8Ca9d6ds/SzrPc235n1IxWYdhJ2zF3MNUaoQ==",
"dependencies": {
"use-sync-external-store": "^1.2.0"
},
"peerDependencies": {
"react": "^16.11.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/swr-store": {
"version": "0.10.6",
"resolved": "https://registry.npmjs.org/swr-store/-/swr-store-0.10.6.tgz",
"integrity": "sha512-xPjB1hARSiRaNNlUQvWSVrG5SirCjk2TmaUyzzvk69SZQan9hCJqw/5rG9iL7xElHU784GxRPISClq4488/XVw==",
"dependencies": {
"dequal": "^2.0.3"
},
"engines": {
"node": ">=10"
}
},
"node_modules/swrev": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/swrev/-/swrev-4.0.0.tgz",
@@ -41304,6 +41591,18 @@
"node": ">=10"
}
},
"node_modules/throttleit": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz",
"integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/through2": {
"version": "2.0.5",
"license": "MIT",
@@ -44055,6 +44354,7 @@
},
"node_modules/web-streams-polyfill": {
"version": "3.2.1",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
@@ -44718,13 +45018,23 @@
}
},
"node_modules/zod": {
"version": "3.22.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
"integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zod-to-json-schema": {
"version": "3.23.5",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.23.5.tgz",
"integrity": "sha512-5wlSS0bXfF/BrL4jPAbz9da5hDlDptdEppYfe+x4eIJ7jioqKG9uUxOwPzqof09u/XeVdrgFu29lZi+8XNDJtA==",
"license": "ISC",
"peerDependencies": {
"zod": "^3.23.3"
}
},
"node_modules/zustand": {
"version": "3.7.2",
"license": "MIT",
@@ -44758,7 +45068,7 @@
"dependencies": {
"@serafin/schema-builder": "^0.18.5",
"@supabase/supabase-js": "*",
"ai": "^2.2.29",
"ai": "^3.4.33",
"common-tags": "^1.8.2",
"config": "*",
"js-tiktoken": "^1.0.10",
@@ -44766,6 +45076,7 @@
"openai": "^4.26.1"
},
"devDependencies": {
"@ai-sdk/openai": "^0.0.72",
"@babel/core": "^7.23.6",
"@babel/preset-env": "^7.23.6",
"@jest/globals": "^29.7.0",
@@ -44784,6 +45095,82 @@
"typescript": "~5.5.0"
}
},
"packages/ai-commands/node_modules/@opentelemetry/api": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"engines": {
"node": ">=8.0.0"
}
},
"packages/ai-commands/node_modules/ai": {
"version": "3.4.33",
"resolved": "https://registry.npmjs.org/ai/-/ai-3.4.33.tgz",
"integrity": "sha512-plBlrVZKwPoRTmM8+D1sJac9Bq8eaa2jiZlHLZIWekKWI1yMWYZvCCEezY9ASPwRhULYDJB2VhKOBUUeg3S5JQ==",
"dependencies": {
"@ai-sdk/provider": "0.0.26",
"@ai-sdk/provider-utils": "1.0.22",
"@ai-sdk/react": "0.0.70",
"@ai-sdk/solid": "0.0.54",
"@ai-sdk/svelte": "0.0.57",
"@ai-sdk/ui-utils": "0.0.50",
"@ai-sdk/vue": "0.0.59",
"@opentelemetry/api": "1.9.0",
"eventsource-parser": "1.1.2",
"json-schema": "^0.4.0",
"jsondiffpatch": "0.6.0",
"secure-json-parse": "^2.7.0",
"zod-to-json-schema": "^3.23.3"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"openai": "^4.42.0",
"react": "^18 || ^19 || ^19.0.0-rc",
"sswr": "^2.1.0",
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0",
"zod": "^3.0.0"
},
"peerDependenciesMeta": {
"openai": {
"optional": true
},
"react": {
"optional": true
},
"sswr": {
"optional": true
},
"svelte": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"packages/ai-commands/node_modules/ai/node_modules/@ai-sdk/vue": {
"version": "0.0.59",
"resolved": "https://registry.npmjs.org/@ai-sdk/vue/-/vue-0.0.59.tgz",
"integrity": "sha512-+ofYlnqdc8c4F6tM0IKF0+7NagZRAiqBJpGDJ+6EYhDW8FHLUP/JFBgu32SjxSxC6IKFZxEnl68ZoP/Z38EMlw==",
"dependencies": {
"@ai-sdk/provider-utils": "1.0.22",
"@ai-sdk/ui-utils": "0.0.50",
"swrv": "^1.0.4"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"vue": "^3.3.4"
},
"peerDependenciesMeta": {
"vue": {
"optional": true
}
}
},
"packages/ai-commands/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -44802,6 +45189,14 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"packages/ai-commands/node_modules/eventsource-parser": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz",
"integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==",
"engines": {
"node": ">=14.18"
}
},
"packages/ai-commands/node_modules/sql-formatter": {
"version": "15.0.2",
"resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.0.2.tgz",
@@ -44816,6 +45211,19 @@
"sql-formatter": "bin/sql-formatter-cli.cjs"
}
},
"packages/ai-commands/node_modules/sswr": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/sswr/-/sswr-2.1.0.tgz",
"integrity": "sha512-Cqc355SYlTAaUt8iDPaC/4DPPXK925PePLMxyBKuWd5kKc5mwsG3nT9+Mq2tyguL5s7b4Jg+IRMpTRsNTAfpSQ==",
"optional": true,
"peer": true,
"dependencies": {
"swrev": "^4.0.0"
},
"peerDependencies": {
"svelte": "^4.0.0 || ^5.0.0-next.0"
}
},
"packages/api-types": {
"version": "0.0.0",
"license": "MIT",

View File

@@ -13,7 +13,7 @@
"dependencies": {
"@serafin/schema-builder": "^0.18.5",
"@supabase/supabase-js": "*",
"ai": "^2.2.29",
"ai": "^3.4.33",
"common-tags": "^1.8.2",
"config": "*",
"js-tiktoken": "^1.0.10",
@@ -22,6 +22,7 @@
},
"devDependencies": {
"api-types": "*",
"@ai-sdk/openai": "^0.0.72",
"@babel/core": "^7.23.6",
"@babel/preset-env": "^7.23.6",
"@jest/globals": "^29.7.0",

View File

@@ -28,10 +28,11 @@ export async function collectStream<R extends BufferSource>(stream: ReadableStre
let content = ''
for await (const chunk of stream.pipeThrough(textDecoderStream)) {
content += chunk
const text = chunk.split('0:')[1]
content += text.slice(1, text.length - 2)
}
return content
return content.replaceAll('\\n', '\n').replaceAll('\\"', '"')
}
/**

View File

@@ -174,7 +174,7 @@ function Input({
ref={inputRef}
type={type}
value={reveal && hidden ? HIDDEN_PLACEHOLDER : value}
className={cn(inputClasses)}
className={cn(inputClasses, size === 'tiny' && 'pl-8')}
{...props}
/>
{icon && <InputIconContainer size={size} icon={icon} className={iconContainerClassName} />}