Skip to content
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
3347494
feat: adds sql query runner ai auto completion
Rudra-Sankha-Sinhamahapatra Dec 5, 2025
17d4827
Merge branch 'main' into feat/sql-auto-completions
Rudra-Sankha-Sinhamahapatra Dec 5, 2025
92aeaeb
fix: catalog monacopilot version and console.logs removed
Rudra-Sankha-Sinhamahapatra Dec 5, 2025
eb72623
fix: remove typescript error
Rudra-Sankha-Sinhamahapatra Dec 5, 2025
ab12ba8
fix: removing endpoint since fetching from orpc
Rudra-Sankha-Sinhamahapatra Dec 5, 2025
3ad7228
fix: handle mount
Rudra-Sankha-Sinhamahapatra Dec 5, 2025
3a2d975
fix: added completion cache to reduce unnecessary api calls
Rudra-Sankha-Sinhamahapatra Dec 5, 2025
c7938f9
Merge branch 'main' into feat/sql-auto-completions
Rudra-Sankha-Sinhamahapatra Dec 12, 2025
6e492be
fix: lint issue during push
Rudra-Sankha-Sinhamahapatra Dec 12, 2025
b782c7e
fix: merge conflicts
Rudra-Sankha-Sinhamahapatra Dec 16, 2025
3f9d557
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
letstri Dec 16, 2025
167a4b8
refactor: made useEffect as a separate component
Rudra-Sankha-Sinhamahapatra Dec 18, 2025
b9f2149
fix: merge conflicts
Rudra-Sankha-Sinhamahapatra Dec 18, 2025
55ec7e4
fix: merge conflicts
Rudra-Sankha-Sinhamahapatra Dec 20, 2025
13e9da7
refactor: merge with main
Rudra-Sankha-Sinhamahapatra Dec 23, 2025
a8e8de2
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
Rudra-Sankha-Sinhamahapatra Dec 24, 2025
72ba4cf
fix: using DatabaseType instead of manually defining
Rudra-Sankha-Sinhamahapatra Dec 24, 2025
c23bbbf
refactor: moved a lot of code from useEffect to outside
Rudra-Sankha-Sinhamahapatra Dec 24, 2025
37f3deb
Merge branch 'feat/sql-auto-completions' of https://github.com/wannab…
letstri Dec 25, 2025
bb01f51
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
letstri Dec 25, 2025
d94244f
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
Rudra-Sankha-Sinhamahapatra Dec 25, 2025
bf83f7e
fix: monaco console errors & added abort prev api call
Rudra-Sankha-Sinhamahapatra Dec 25, 2025
648250c
fix: eslint issue
Rudra-Sankha-Sinhamahapatra Dec 25, 2025
d45f548
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
letstri Jan 6, 2026
4880fd0
refactor: update AI tab completion logic
letstri Jan 6, 2026
4ff2090
refactor: improve context generation in database AI completion
letstri Jan 6, 2026
5ddd450
refactor: enhance prompt formatting for SQL code completion assistant
letstri Jan 6, 2026
ccd6157
refactor: replace auth middleware with subscription requirement for c…
letstri Jan 6, 2026
f7a61c3
chore: merge with main
Rudra-Sankha-Sinhamahapatra Jan 7, 2026
af5e1a4
fix: auto completion issue
Rudra-Sankha-Sinhamahapatra Jan 7, 2026
fa6e954
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
Rudra-Sankha-Sinhamahapatra Jan 8, 2026
ad7ffbb
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
Rudra-Sankha-Sinhamahapatra Jan 8, 2026
9bf0187
refactor: merge with main
Rudra-Sankha-Sinhamahapatra Jan 10, 2026
1806867
refactor: merge with main
Rudra-Sankha-Sinhamahapatra Jan 15, 2026
4d56616
refactor: merge with main
Rudra-Sankha-Sinhamahapatra Jan 21, 2026
b0f89c3
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
letstri Jan 21, 2026
dbcdb68
fix: merge conflicts
Rudra-Sankha-Sinhamahapatra Jan 23, 2026
6aea6db
chore(deps): update monacopilot to v1.2.12
Rudra-Sankha-Sinhamahapatra Jan 31, 2026
0dff40b
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
Rudra-Sankha-Sinhamahapatra Jan 31, 2026
e29d3ce
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
Rudra-Sankha-Sinhamahapatra Feb 4, 2026
fb58058
fix: lint errors
Rudra-Sankha-Sinhamahapatra Feb 8, 2026
7d994ed
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
Rudra-Sankha-Sinhamahapatra Feb 9, 2026
d4eabbd
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
Rudra-Sankha-Sinhamahapatra Feb 14, 2026
b3e4dee
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
Rudra-Sankha-Sinhamahapatra Feb 16, 2026
1e1fa70
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
letstri Feb 16, 2026
05eeb21
chore: change gemini model
letstri Feb 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"ioredis": "catalog:",
"ioredis-mock": "catalog:",
"loops": "catalog:",
"monacopilot": "catalog:",
"nanoid": "catalog:",
"pg": "catalog:",
"postgres": "catalog:",
Expand Down
79 changes: 79 additions & 0 deletions apps/api/src/orpc/routers/ai/code-completion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { google } from '@ai-sdk/google'
import { generateText } from 'ai'
import { z } from 'zod'
import { authMiddleware, orpc } from '~/orpc'

export const codeCompletion = orpc
.use(authMiddleware)
.input(z.object({
context: z.string(),
suffix: z.string().optional(),
instruction: z.string().optional(),
fileContent: z.string(),
databaseType: z.enum(['postgres', 'mysql', 'clickhouse', 'mssql']),
Comment thread
Rudra-Sankha-Sinhamahapatra marked this conversation as resolved.
Outdated
schemaContext: z.string().optional(),
}))
.output(z.object({
completion: z.string(),
}))
.handler(async ({ input }) => {
try {
const databaseType = input.databaseType || 'postgres'
const schemaContext = input.schemaContext || ''

const enhancedPrompt = `You are an expert SQL code completion assistant.

Database Type: ${databaseType}

Database Schema:
${schemaContext}

Current Code BEFORE Cursor:
${input.context || ''}

Current Code AFTER Cursor:
${input.suffix || ''}

Full File Content:
${input.fileContent || ''}

Task: ${input.instruction || 'Complete the SQL query'}

General Safety Rules:
- Prioritize SELECT queries (read-only) when the intent is ambiguous.
- If the user explicitly starts writing a data modification query (INSERT, UPDATE, etc.), complete it validly.
- Do NOT generate destructive schema changes (DROP TABLE, TRUNCATE, ALTER) unless explicitly requested in a generic comment.

Specific Instructions:
- If the context is empty (start of file), return an empty string.
- For UUID/CUID: Use generation functions (e.g. gen_random_uuid()) instead of hardcoded strings if possible.
- For Foreign Keys: Do NOT plain guess IDs. Use subqueries (e.g. "(SELECT id FROM users ... LIMIT 1)") to make the query robust without needing external tools.
- Look at "Current Code AFTER Cursor". Do NOT generate code that is already present there (like closing parenthesis or semicolons). Make your completion join smoothly with the suffix.
- Intelligent Spacing: If the code before the cursor is a keyword or identifier (e.g., 'SELECT', 'FROM', 'tableName') and your completion starts with a keyword/identifier, ensure there is a space at the beginning of your completion.

Generate ONLY the completion text that should be inserted at the cursor position.
- Do NOT repeat the code that is already present before the cursor.
- Do NOT wrap in markdown code blocks.
- If no completion is needed, return an empty string.`

const result = await generateText({
model: google('gemma-3-27b-it'),
prompt: enhancedPrompt,
})

let cleanText = result.text.trim()
if (cleanText.startsWith('```')) {
cleanText = cleanText.replace(/^```\w*\s*/, '').replace(/\s*```$/, '')
}

return {
completion: cleanText,
}
}
catch (error) {
console.error('Error in completion handler:', error instanceof Error ? error.message : 'Unknown error')
return {
completion: '',
}
}
})
2 changes: 2 additions & 0 deletions apps/api/src/orpc/routers/ai/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ask } from './ask'
import { codeCompletion } from './code-completion'
import { enhancePrompt } from './enhance-prompt'
import { filters } from './filters'
import { fixSQL } from './fix-sql'
Expand All @@ -8,6 +9,7 @@ import { updateSQL } from './update-sql'

export const ai = {
ask,
codeCompletion,
enhancePrompt,
filters,
generateTitle,
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@clickhouse/client": "catalog:",
"@todesktop/runtime": "catalog:",
"electron-store": "catalog:",
"monacopilot": "catalog:",
"mssql": "catalog:",
"mysql2": "catalog:",
"pg": "catalog:"
Expand Down
47 changes: 47 additions & 0 deletions apps/desktop/src/entities/database/utils/monaco.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,50 @@ export function databaseCompletionService(database: typeof databases.$inferSelec
return [...items, ...keywordItems]
}
}

export async function databaseAICompletionContext(database: typeof databases.$inferSelect) {
queryClient.prefetchQuery(databaseTablesAndSchemasQuery({ database }))
queryClient.prefetchQuery(databaseEnumsQuery({ database }))

const buildSchemaContext = async (): Promise<string> => {
const [tablesAndSchemas, enums] = await Promise.all([
queryClient.ensureQueryData(databaseTablesAndSchemasQuery({ database })),
queryClient.ensureQueryData(databaseEnumsQuery({ database })),
])

let context = ''

for (const schema of tablesAndSchemas.schemas) {
context += `\nSchema: ${schema.name}\n`

for (const tableName of schema.tables) {
const columns = await queryClient.ensureQueryData(
databaseTableColumnsQuery({
database,
schema: schema.name,
table: tableName,
}),
)

context += ` Table: ${tableName}\n`
columns.forEach((col) => {
context += ` - ${col.id}: ${col.type}${col.isNullable ? ' (nullable)' : ' (not null)'}\n`
})
}
}

if (enums?.length > 0) {
context += '\nEnums:\n'
enums.forEach((enumItem) => {
context += ` ${enumItem.schema}.${enumItem.name}: [${enumItem.values.join(', ')}]\n`
})
}

return context
}

return {
databaseType: database.type,
buildSchemaContext,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import type { DatabaseType } from '@conar/shared/enums/database-type'
import type { editor } from 'monaco-editor'
import type { CompletionRegistration } from 'monacopilot'
import type { RefObject } from 'react'
import * as monaco from 'monaco-editor'
import { LanguageIdEnum } from 'monaco-sql-languages'
import { registerCompletion } from 'monacopilot'
import { useEffect, useRef } from 'react'
import { databaseAICompletionContext } from '~/entities/database/utils/monaco'
import { orpc } from '~/lib/orpc'
import { Route } from '../..'

export const dialectsMap = {
postgres: LanguageIdEnum.PG,
mysql: LanguageIdEnum.MYSQL,
mssql: LanguageIdEnum.PG,
clickhouse: LanguageIdEnum.MYSQL,
} satisfies Record<DatabaseType, LanguageIdEnum>

export function useRunnerEditorAiTabCompletion(monacoRef:
RefObject<editor.IStandaloneCodeEditor | null>) {
const { database } = Route.useRouteContext()

const completionRef = useRef<CompletionRegistration | null>(null)
const completionCacheRef = useRef<{
prefix: string
completion: string
} | null>(null)

useEffect(() => {
if (!monacoRef.current)
return

let debounceTimer: ReturnType<typeof setTimeout> | null = null
let pendingResolve: ((value: { completion: string }) => void) | null = null

let isMounted = true

const setupCompletion = async () => {
if (!isMounted)
return

const aiConfig = await databaseAICompletionContext(database)
if (!isMounted)
return

const schemaContext = await aiConfig.buildSchemaContext()

if (!isMounted)
return

if (completionRef.current) {
completionRef.current.deregister()
}

if (!monacoRef.current)
return

completionRef.current = registerCompletion(
monaco,
monacoRef.current,
{
trigger: 'onTyping',
language: dialectsMap[database.type],
requestHandler: async () => {
const model = monacoRef.current?.getModel()
const position = monacoRef.current?.getPosition()

if (model && position && completionCacheRef.current) {
const offset = model.getOffsetAt(position)
const textFull = model.getValue()
const textBefore = textFull.substring(0, offset)
const cached = completionCacheRef.current

if (textBefore.startsWith(cached.prefix)) {
const addedText = textBefore.slice(cached.prefix.length)

if (cached.completion.startsWith(addedText)) {
const remainingCompletion = cached.completion.slice(addedText.length)

if (remainingCompletion.length > 0) {
return { completion: remainingCompletion }
}

return { completion: '' }
}
}
}

if (debounceTimer) {
clearTimeout(debounceTimer)
}
if (pendingResolve) {
pendingResolve({ completion: '' })
pendingResolve = null
}

return new Promise((resolve) => {
pendingResolve = resolve
debounceTimer = setTimeout(async () => {
if (!isMounted)
return

try {
const model = monacoRef.current?.getModel()
const position = monacoRef.current?.getPosition()

if (!model || !position) {
resolve({ completion: '' })
return
}

const fileContent = model.getValue()
const offset = model.getOffsetAt(position)
const context = fileContent.substring(0, offset)
const suffix = fileContent.substring(offset)

if (context.trim().length < 2) {
resolve({ completion: '' })
return
}

const transformedBody = {
context,
suffix,
instruction: 'Complete the SQL query with secure and safe optimised version',
fileContent,
databaseType: aiConfig.databaseType,
schemaContext,
}

const result = await orpc.ai.codeCompletion(transformedBody)

if (pendingResolve === resolve) {
completionCacheRef.current = {
prefix: context,
completion: result.completion,
}

resolve(result)
}
else {
resolve({ completion: '' })
}
}
catch (error) {
console.error(error)
resolve({ completion: '' })
}
finally {
if (pendingResolve === resolve)
pendingResolve = null
}
}, 500)
})
},
},
)
}

setupCompletion()

return () => {
isMounted = false
if (debounceTimer)
clearTimeout(debounceTimer)
if (pendingResolve)
pendingResolve({ completion: '' })
if (completionRef.current) {
completionRef.current.deregister()
completionRef.current = null
}
}
}, [database])
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,19 @@
import type { DatabaseType } from '@conar/shared/enums/database-type'
import type { editor, Position } from 'monaco-editor'
import type { RefObject } from 'react'
import { useStore } from '@tanstack/react-store'
import { KeyCode, KeyMod } from 'monaco-editor'
import { LanguageIdEnum, setupLanguageFeatures } from 'monaco-sql-languages'
import { setupLanguageFeatures } from 'monaco-sql-languages'
import { useEffect, useEffectEvent, useRef } from 'react'
import { Monaco } from '~/components/monaco'
import { databaseStore } from '~/entities/database'
import { databaseCompletionService } from '~/entities/database/utils/monaco'
import { Route } from '../..'
import { runnerHooks } from '../../-page'
import { dialectsMap, useRunnerEditorAiTabCompletion } from './ai-tab-completion'
import { useRunnerContext } from './runner-context'
import { useRunnerEditorAIZones } from './runner-editor-ai-zones'
import { useRunnerEditorQueryZones } from './runner-editor-query-zones'

const dialectsMap = {
postgres: LanguageIdEnum.PG,
mysql: LanguageIdEnum.MYSQL,
mssql: LanguageIdEnum.PG,
clickhouse: LanguageIdEnum.MYSQL,
} satisfies Record<DatabaseType, LanguageIdEnum>

function useRunnerEditorHooks(monacoRef: RefObject<editor.IStandaloneCodeEditor | null>) {
const { database } = Route.useRouteContext()
const store = databaseStore(database.id)
Expand Down Expand Up @@ -121,6 +114,7 @@ export function RunnerEditor() {
const sql = useStore(store, state => state.sql)
const editorQueries = useStore(store, state => state.editorQueries)
const monacoRef = useRef<editor.IStandaloneCodeEditor>(null)

const run = useRunnerContext(({ run }) => run)

const runEvent = useEffectEvent(run)
Expand All @@ -130,6 +124,8 @@ export function RunnerEditor() {
useRunnerEditorQueryZones(monacoRef)
useRunnerEditorAIZones(monacoRef)

useRunnerEditorAiTabCompletion(monacoRef)

useEffect(() => {
setupLanguageFeatures(dialectsMap[database.type], {
completionItems: {
Expand Down
Loading