diff --git a/components/webui/client/src/api/presto-search/index.ts b/components/webui/client/src/api/presto-search/index.ts new file mode 100644 index 0000000000..fdd34b6aa1 --- /dev/null +++ b/components/webui/client/src/api/presto-search/index.ts @@ -0,0 +1,36 @@ +import axios, {AxiosResponse} from "axios"; + + +// eslint-disable-next-line no-warning-comments +// TODO: Replace with shared type from the `@common` directory once refactoring is completed. +// Currently, server schema types require typebox dependency so they cannot be moved to the +// `@common` directory with current implementation. +type PrestoQueryJobCreationSchema = { + queryString: string; +}; + +type PrestoQueryJobSchema = { + searchJobId: string; +}; + + +/** + * Sends post request to server to submit presto query. + * + * @param payload + * @return + */ +const submitQuery = async ( + payload: PrestoQueryJobCreationSchema +): Promise> => { + console.log("Submitting query:", JSON.stringify(payload)); + + return axios.post("/api/presto-search/query", payload); +}; + +export type { + PrestoQueryJobCreationSchema, + PrestoQueryJobSchema, +}; + +export {submitQuery}; diff --git a/components/webui/client/src/pages/SearchPage/SearchControls/Presto/RunButton/index.tsx b/components/webui/client/src/pages/SearchPage/SearchControls/Presto/RunButton/index.tsx index 839d96209f..e0f5c26d48 100644 --- a/components/webui/client/src/pages/SearchPage/SearchControls/Presto/RunButton/index.tsx +++ b/components/webui/client/src/pages/SearchPage/SearchControls/Presto/RunButton/index.tsx @@ -1,3 +1,5 @@ +import {useCallback} from "react"; + import {CaretRightOutlined} from "@ant-design/icons"; import { Button, @@ -5,6 +7,7 @@ import { } from "antd"; import useSearchStore from "../../../SearchState/index"; +import {handlePrestoQuerySubmit} from "../presto-search-requests"; /** @@ -20,6 +23,10 @@ const RunButton = () => { "Enter SQL query to run" : ""; + const handleClick = useCallback(() => { + handlePrestoQuerySubmit({queryString}); + }, [queryString]); + return ( diff --git a/components/webui/client/src/pages/SearchPage/SearchControls/Presto/presto-search-requests.ts b/components/webui/client/src/pages/SearchPage/SearchControls/Presto/presto-search-requests.ts new file mode 100644 index 0000000000..a42e4110d1 --- /dev/null +++ b/components/webui/client/src/pages/SearchPage/SearchControls/Presto/presto-search-requests.ts @@ -0,0 +1,27 @@ +import { + type PrestoQueryJobCreationSchema, + submitQuery, +} from "../../../../api/presto-search"; + + +/** + * Submits a new Presto query to server. + * + * @param payload + */ +const handlePrestoQuerySubmit = (payload: PrestoQueryJobCreationSchema) => { + submitQuery(payload) + .then((result) => { + const {searchJobId} = result.data; + console.debug( + "Presto search job created - ", + "Search job ID:", + searchJobId + ); + }) + .catch((err: unknown) => { + console.error("Failed to submit query:", err); + }); +}; + +export {handlePrestoQuerySubmit}; diff --git a/components/webui/server/package-lock.json b/components/webui/server/package-lock.json index 976f56548c..97a013757b 100644 --- a/components/webui/server/package-lock.json +++ b/components/webui/server/package-lock.json @@ -29,10 +29,12 @@ "fastify-plugin": "^5.0.1", "http-status-codes": "^2.3.0", "pino-pretty": "^13.0.0", + "presto-client": "^1.1.0", "socket.io": "^4.8.1", "typescript": "~5.7.3" }, "devDependencies": { + "@types/presto-client": "^1.0.2", "concurrently": "^9.1.2", "eslint-config-yscope": "latest", "fastify-cli": "^7.4.0", @@ -3674,6 +3676,13 @@ "undici-types": "~6.20.0" } }, + "node_modules/@types/presto-client": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/presto-client/-/presto-client-1.0.2.tgz", + "integrity": "sha512-1q7QtX+ykoX5zjWTJpntIzxyhr20ZHrEySk2p2PQCOwPI7DJtGsL62KKFZMsTqPjikpjLL71BWtVA0cPMyYdfA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -7041,6 +7050,26 @@ "license": "ISC", "peer": true }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -10065,6 +10094,15 @@ "node": ">= 0.8.0" } }, + "node_modules/presto-client": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/presto-client/-/presto-client-1.1.0.tgz", + "integrity": "sha512-DOWEKp0eHP/x6Fupk5673vZND7OUxFtV9VUO9HMvf4DFzoWKTLMRAJ3o5/7Mgs5z9w5BEUKU88IZaume6LMelw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.3" + } + }, "node_modules/prismjs": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", diff --git a/components/webui/server/package.json b/components/webui/server/package.json index b2bd7bafa6..ebe68d80ec 100644 --- a/components/webui/server/package.json +++ b/components/webui/server/package.json @@ -22,10 +22,10 @@ "@aws-sdk/s3-request-presigner": "^3.758.0", "@fastify/autoload": "^6.3.0", "@fastify/env": "^5.0.2", + "@fastify/http-proxy": "^11.3.0", "@fastify/mongodb": "^9.0.2", "@fastify/mysql": "^5.0.2", "@fastify/rate-limit": "^10.2.2", - "@fastify/http-proxy": "^11.3.0", "@fastify/sensible": "^6.0.3", "@fastify/static": "^8.1.1", "@fastify/type-provider-typebox": "^5.1.0", @@ -38,10 +38,12 @@ "fastify-plugin": "^5.0.1", "http-status-codes": "^2.3.0", "pino-pretty": "^13.0.0", + "presto-client": "^1.1.0", "socket.io": "^4.8.1", "typescript": "~5.7.3" }, "devDependencies": { + "@types/presto-client": "^1.0.2", "concurrently": "^9.1.2", "eslint-config-yscope": "latest", "fastify-cli": "^7.4.0", diff --git a/components/webui/server/settings.json b/components/webui/server/settings.json index 5f7f789364..4a5990b32f 100644 --- a/components/webui/server/settings.json +++ b/components/webui/server/settings.json @@ -16,5 +16,9 @@ "StreamTargetUncompressedSize": 134217728, "StreamFilesS3Region": null, "StreamFilesS3PathPrefix": null, - "StreamFilesS3Profile": null + "StreamFilesS3Profile": null, + + "ClpQueryEngine": "native", + "PrestoHost": "localhost", + "PrestoPort": 8889 } diff --git a/components/webui/server/src/plugins/app/Presto.ts b/components/webui/server/src/plugins/app/Presto.ts new file mode 100644 index 0000000000..dd6ccf31be --- /dev/null +++ b/components/webui/server/src/plugins/app/Presto.ts @@ -0,0 +1,47 @@ +import fp from "fastify-plugin"; +import { + Client, + ClientOptions, +} from "presto-client"; + +import settings from "../../../settings.json" with {type: "json"}; + + +/** + * Class to manage Presto client connections. + */ +class Presto { + readonly client; + + /** + * @param clientOptions + */ + constructor (clientOptions: ClientOptions) { + this.client = new Client(clientOptions); + } +} + +declare module "fastify" { + interface FastifyInstance { + Presto?: Presto; + } +} + +export default fp( + (fastify) => { + if ("presto" !== settings.ClpQueryEngine) { + return; + } + + const clientOptions: ClientOptions = { + host: settings.PrestoHost, + port: settings.PrestoPort, + }; + + fastify.log.info( + clientOptions, + "Initializing Presto" + ); + fastify.decorate("Presto", new Presto(clientOptions)); + }, +); diff --git a/components/webui/server/src/routes/api/presto-search/index.ts b/components/webui/server/src/routes/api/presto-search/index.ts new file mode 100644 index 0000000000..2c8e8acc23 --- /dev/null +++ b/components/webui/server/src/routes/api/presto-search/index.ts @@ -0,0 +1,92 @@ +import {FastifyPluginAsyncTypebox} from "@fastify/type-provider-typebox"; +import {StatusCodes} from "http-status-codes"; + +import {ErrorSchema} from "../../../schemas/error.js"; +import { + PrestoQueryJobCreationSchema, + PrestoQueryJobSchema, +} from "../../../schemas/presto-search.js"; + + +/** + * Presto search API routes. + * + * @param fastify + */ +const plugin: FastifyPluginAsyncTypebox = async (fastify) => { + const {Presto} = fastify; + + if ("undefined" === typeof Presto) { + // If Presto client is not available, skip the plugin registration. + return; + } + + /** + * Submits a search query. + */ + fastify.post( + "/query", + { + schema: { + body: PrestoQueryJobCreationSchema, + response: { + [StatusCodes.CREATED]: PrestoQueryJobSchema, + [StatusCodes.INTERNAL_SERVER_ERROR]: ErrorSchema, + }, + tags: ["Presto Search"], + }, + }, + + async (request, reply) => { + const {queryString} = request.body; + + let searchJobId: string; + + try { + searchJobId = await new Promise((resolve, reject) => { + let isResolved = false; + + Presto.client.execute({ + // eslint-disable-next-line no-warning-comments + // TODO: Data, error, and success handlers are dummy implementations + // and will be replaced with proper implementations. + data: (_, data, columns) => { + request.log.info({columns, data}, "Presto data"); + }, + error: (error) => { + request.log.info(error, "Presto search failed"); + if (false === isResolved) { + isResolved = true; + reject(new Error("Presto search failed")); + } + }, + query: queryString, + state: (_, queryId, stats) => { + request.log.info({ + searchJobId: queryId, + state: stats.state, + }, "Presto search state updated"); + + if (false === isResolved) { + isResolved = true; + resolve(queryId); + } + }, + success: () => { + request.log.info("Presto search succeeded"); + }, + }); + }); + } catch (error) { + request.log.error(error, "Failed to submit Presto query"); + throw error; + } + + reply.code(StatusCodes.CREATED); + + return {searchJobId}; + } + ); +}; + +export default plugin; diff --git a/components/webui/server/src/schemas/presto-search.ts b/components/webui/server/src/schemas/presto-search.ts new file mode 100644 index 0000000000..ec4a92002d --- /dev/null +++ b/components/webui/server/src/schemas/presto-search.ts @@ -0,0 +1,23 @@ +import {Type} from "@sinclair/typebox"; + +import {StringSchema} from "./common.js"; + + +/** + * Schema for request to create a new Presto query job. + */ +const PrestoQueryJobCreationSchema = Type.Object({ + queryString: StringSchema, +}); + +/** + * Schema to identify a Presto query job. + */ +const PrestoQueryJobSchema = Type.Object({ + searchJobId: StringSchema, +}); + +export { + PrestoQueryJobCreationSchema, + PrestoQueryJobSchema, +};