Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BRU-1053 Add autocomplete to SQL query editor #47

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 8 additions & 2 deletions backend/src/databases/databases.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import { OrgGuard } from "@/auth/organizations.guard";
import { TrackingInterceptor } from "@/interceptors/tracking/tracking.interceptor";
import { ZodSerializerDto } from "nestjs-zod";
import { ApiOkResponse } from "@nestjs/swagger";
import { CleanDatabase, CleanDatabaseSchema } from "@/definitions";
import {
CleanDatabase,
CleanDatabaseSchema,
ExternalColumn,
} from "@/definitions";
import {
GetDatabaseResponseDto,
GetAllDatabasesResponseDto,
Expand Down Expand Up @@ -74,7 +78,9 @@ export class DatabasesController {

@Get("/:id/schemas")
@UseGuards(OrgGuard("database"))
findAllSchemas(@Param("id") id: string): Promise<string[]> {
findAllSchemas(
@Param("id") id: string,
): Promise<Record<string, Record<string, Record<string, ExternalColumn>>>> {
return this.databasesService.findAllSchemas(id);
}
}
17 changes: 11 additions & 6 deletions backend/src/databases/databases.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import {
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { HttpRequestContextService } from "@/shared/http-request-context/http-request-context.service";
import { CreateDatabase, Database, UpdateDatabase } from "@/definitions";
import {
CreateDatabase,
Database,
ExternalColumn,
UpdateDatabase,
} from "@/definitions";
import { AES } from "crypto-js";
import * as CryptoJS from "crypto-js";
import * as assert from "assert";
Expand Down Expand Up @@ -123,19 +128,19 @@ export class DatabasesService {
return database;
}

async findAllSchemas(databaseId: string): Promise<string[]> {
async findAllSchemas(
databaseId: string,
): Promise<Record<string, Record<string, Record<string, ExternalColumn>>>> {
const database = await this.findOne(databaseId);

if (!database) {
throw new NotFoundException("Database not found");
}

const schemas =
await this.postgresAdapterService.getAllDatabaseSchema(databaseId);

const systemSchemas = ["information_schema", "pg_catalog", "pg_toast"];
await this.postgresAdapterService.getAllTableSchema(databaseId);

return schemas.filter((schema) => !systemSchemas.includes(schema));
return schemas;
}

async updateDatabase(
Expand Down
7 changes: 6 additions & 1 deletion backend/src/postgres-adapter/postgres-adapter.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Inject, Injectable, forwardRef } from "@nestjs/common";
import { Pool } from "pg";
import { ExternalColumn } from "@/definitions/postgres-adapter";
import * as assert from "assert";
import { SYSTEM_SCHEMAS } from "@/shared/constant/database";

interface CreateDatabaseQueryDto {
databaseId: string;
Expand Down Expand Up @@ -105,6 +106,8 @@ export class PostgresAdapterService {
const columns = result.rows;
const columnMap = {};
for (const column of columns) {
if (SYSTEM_SCHEMAS.includes(column.table_schema)) continue;

if (!columnMap[column.table_schema]) {
columnMap[column.table_schema] = {};
}
Expand Down Expand Up @@ -136,7 +139,9 @@ export class PostgresAdapterService {
`SELECT schema_name FROM information_schema.schemata`,
);
const columns = result.rows;
const schemata = columns.map((column) => column.schema_name);
const schemata = columns
.map((column) => column.schema_name)
.filter((schema) => !SYSTEM_SCHEMAS.includes(schema));

return schemata;
} catch (e) {
Expand Down
1 change: 1 addition & 0 deletions backend/src/shared/constant/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const SYSTEM_SCHEMAS = ["information_schema", "pg_catalog", "pg_toast"];
4 changes: 3 additions & 1 deletion frontend/src/app/databases/[databaseId]/database-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ const DatabaseDetails = ({ database }: { database: CleanDatabase }) => {
<Divider />
<div className="flex flex-row justify-between">
<Text>Schema count</Text>
<Text className="ml-2 bp5-text-muted">{schemas!.length}</Text>
<Text className="ml-2 bp5-text-muted">
{_.keys(schemas).length}
</Text>
</div>
<div className="flex flex-row justify-between">
<Text>Table count</Text>
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/app/databases/database-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import AddDatabase from "@/app/databases/add-database";
import React, { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import _ from "lodash";

const DatabaseSelector = ({
selectedDatabase,
Expand Down Expand Up @@ -88,7 +89,7 @@ const DatabaseSelector = ({
}
popoverProps={{ usePortal: true }}
>
{databaseSchemas?.map((schema) => (
{_.keys(databaseSchemas).map((schema) => (
<MenuItem
key={schema}
text={schema}
Expand Down
80 changes: 68 additions & 12 deletions frontend/src/components/query/query-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,72 @@ import {
MonacoThemeLight,
} from "@blueprintjs/monaco-editor-theme";
import { useDarkModeContext } from "../context/dark-mode-context";
import { LanguageIdEnum, setupLanguageFeatures } from "monaco-sql-languages";
import { useSelectedDatabase } from "@/stores";
import { useDatabaseSchemas } from "@/data/use-database";
import _ from "lodash";
import { ExternalColumn } from "@/definitions";

interface SuggestionProps {
label: string;
kind: monaco.languages.CompletionItemKind;
documentation?: string;
insertText: string;
range: monaco.IRange;
}

function createDependencyProposals(
range: monaco.IRange,
monacoInstance: typeof monaco,
schemas?: Record<string, Record<string, Record<string, ExternalColumn>>>,
) {
// returning a static list of proposals, not even looking at the prefix (filtering is done by the Monaco editor),
// here you could do a server side lookup
return [
{
label: "table_name",
const suggestions: SuggestionProps[] = [];
const tables = new Set<string>();
const columns = new Set<string>();

_.keys(schemas).forEach((schema: string) => {
if (!schemas) return;

_.keys(schemas[schema]).forEach((table) => {
_.keys(schemas[schema][table]).forEach((column) => {
columns.add(column);
});

tables.add(table);
});

suggestions.push({
label: schema,
kind: monacoInstance.languages.CompletionItemKind.Module,
documentation: "Schema name",
insertText: schema,
range: range,
});
});

tables.forEach((table) => {
suggestions.push({
label: table,
kind: monacoInstance.languages.CompletionItemKind.Field,
documentation: "The Lodash library exported as Node.js modules.",
insertText: "table_name",
documentation: "Table name",
insertText: table,
range: range,
},
];
});
});

columns.forEach((column) => {
suggestions.push({
label: column,
kind: monacoInstance.languages.CompletionItemKind.Variable,
documentation: "Column name",
insertText: column,
range: range,
});
});

return suggestions;
}

interface QueryEditorProps {
Expand All @@ -33,14 +83,16 @@ interface QueryEditorProps {
function QueryEditor({ value, onChange }: QueryEditorProps) {
const monacoInstance = useMonaco();
const { darkMode, setDarkMode } = useDarkModeContext();
const [selectedDatabase, setSelectedDatabase] = useSelectedDatabase();
const {
data: schemas,
isLoading: isLoadingSchemas,
error: schemasError,
} = useDatabaseSchemas(selectedDatabase?.id);

useEffect(() => {
if (!monacoInstance || !window) return;
// const monaco = require("monaco-editor");
const {
setupLanguageFeatures,
LanguageIdEnum,
} = require("monaco-sql-languages");
// const require("monaco-sql-languages/out/esm/pgsql/pgsql.ts");
// console.log(typeof setupLanguageFeatures);
// monacoInstance.languages.setLanguageConfiguration("pgsql", conf);
Expand All @@ -59,7 +111,11 @@ function QueryEditor({ value, onChange }: QueryEditorProps) {
endColumn: word.endColumn,
};
return {
suggestions: createDependencyProposals(range, monacoInstance),
suggestions: createDependencyProposals(
range,
monacoInstance,
schemas,
),
};
},
});
Expand Down
14 changes: 9 additions & 5 deletions frontend/src/data/use-database.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import useSWR, { useSWRConfig } from "swr";
import { CleanDatabase, CreateDatabase, UpdateDatabase } from "@/definitions";
import {
CleanDatabase,
CreateDatabase,
ExternalColumn,
UpdateDatabase,
} from "@/definitions";
import {
backendCreate,
backendGet,
Expand Down Expand Up @@ -36,10 +41,9 @@ export const useCreateDatabase = () => {
};

export const useDatabaseSchemas = (id?: string) => {
const { data, error, isLoading, isValidating, mutate } = useSWR<string[]>(
id ? `/databases/${id}/schemas` : null,
backendGet,
);
const { data, error, isLoading, isValidating, mutate } = useSWR<
Record<string, Record<string, Record<string, ExternalColumn>>>
>(id ? `/databases/${id}/schemas` : null, backendGet);
return { data, error, isLoading, isValidating, mutate };
};

Expand Down
Loading