diff --git a/apps/nest-server/src/app/app.controller.ts b/apps/nest-server/src/app/app.controller.ts index bf3d2e8..675f631 100644 --- a/apps/nest-server/src/app/app.controller.ts +++ b/apps/nest-server/src/app/app.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get } from '@nestjs/common' +import { Controller, Get, Param } from '@nestjs/common' import { AppService } from './app.service' @@ -6,6 +6,24 @@ import { AppService } from './app.service' export class AppController { constructor(private readonly appService: AppService) {} + @Get('get-schemas') + async getSchemas(@Param('catalog') catalog: string) { + try { + return await this.appService.getSchemas(catalog) + } catch (err) { + console.error(err) + } + } + + @Get('get-catalogs') + async getCatalogs() { + try { + return await this.appService.getCatalogs() + } catch (err) { + console.error(err) + } + } + @Get('query-test') async getData() { try { diff --git a/apps/nest-server/src/app/app.service.ts b/apps/nest-server/src/app/app.service.ts index 2d22229..0fa08e0 100644 --- a/apps/nest-server/src/app/app.service.ts +++ b/apps/nest-server/src/app/app.service.ts @@ -3,6 +3,38 @@ import PrestoClient, { PrestoClientConfig, PrestoError } from '@prestodb/presto- @Injectable() export class AppService { + async getCatalogs(): Promise { + const clientParams: PrestoClientConfig = { + catalog: 'tpch', + host: 'http://localhost', + port: 8080, + schema: 'sf1', + user: 'root', + } + const client = new PrestoClient(clientParams) + try { + return await client.getCatalogs() + } catch (error) { + console.error(error) + } + } + + async getSchemas(catalog: string): Promise { + const clientParams: PrestoClientConfig = { + catalog: 'tpch', + host: 'http://localhost', + port: 8080, + schema: 'sf1', + user: 'root', + } + const client = new PrestoClient(clientParams) + try { + return await client.getSchemas(catalog) + } catch (error) { + console.error(error) + } + } + async getData(): Promise { const clientParams: PrestoClientConfig = { catalog: 'tpch', diff --git a/presto-client/README.md b/presto-client/README.md index a5dbc53..8f5f0e5 100644 --- a/presto-client/README.md +++ b/presto-client/README.md @@ -99,3 +99,68 @@ Additional notes - The `query` method is asynchronous and will return a promise that resolves to a PrestoQuery object. - The `query` method will automatically retry the query if it fails due to a transient error. - The `query` method will cancel the query if the client is destroyed. + +## Query catalog, schema, table and column metadata + +### Get Catalogs + +The `getCatalogs` method retrieves all available database catalogs, returning them as an array of strings. + +#### Example usage + +```typescript +const catalogs = await prestoClient.getCatalogs() +console.log(catalogs) +``` + +### Get Schemas + +The `getSchemas` method retrieves all schemas within a given catalog. It accepts a catalog parameter, which is a string representing the name of the catalog. + +Parameters + +- `catalog`: The name of the catalog for which to retrieve schemas. + +#### Example usage + +```typescript +const schemas = await prestoClient.getSchemas('tpch') +console.log(schemas) +``` + +### Get Tables + +The `getTables` method retrieves a list of tables (of type `Table`) filtered by the given catalog and, optionally, the schema. It accepts an object containing `catalog` and optional `schema` parameters. + +Parameters + +- `catalog`: The catalog name. +- `schema` (optional): The schema name. + +#### Example usage + +```typescript +const tables: Table[] = await prestoClient.getTables({ catalog: 'tpch', schema: 'sf100' }) +console.log(tables) +``` + +### Get Columns + +The `getColumns` method retrieves a list of columns (of type `Column`) filtered for the given catalog and optional schema and table filters. It accepts an object with `catalog`, and optional `schema` and `table` parameters. + +Parameters + +- `catalog`: The catalog name. +- `schema` (optional): The schema name. +- `table` (optional): The table name. + +#### Example usage + +```typescript +const columns: Column[] = await prestoClient.getColumns({ + catalog: 'tpch', + schema: 'sf100', + table: 'orders', +}) +console.log(columns) +``` diff --git a/presto-client/src/client.ts b/presto-client/src/client.ts index 73538d6..ef8071b 100644 --- a/presto-client/src/client.ts +++ b/presto-client/src/client.ts @@ -1,4 +1,5 @@ import { PrestoClientConfig, PrestoError, PrestoQuery, PrestoResponse } from './client.types' +import { Column, Table } from './information-schema.types' export class PrestoClient { private baseUrl: string @@ -11,6 +12,18 @@ export class PrestoClient { private timezone?: string private user: string + /** + * Creates an instance of PrestoClient. + * @param {PrestoClientConfig} config - Configuration object for the PrestoClient. + * @param {string} config.catalog - The default catalog to be used. + * @param {string} config.host - The host address of the Presto server. + * @param {number} config.interval - The polling interval in milliseconds for query status checks. + * @param {number} config.port - The port number on which the Presto server is listening. + * @param {string} [config.schema] - The default schema to be used. Optional. + * @param {string} [config.source] - The name of the source making the query. Optional. + * @param {string} [config.timezone] - The timezone to be used for the session. Optional. + * @param {string} config.user - The username to be used for the Presto session. + */ constructor({ catalog, host, interval, port, schema, source, timezone, user }: PrestoClientConfig) { this.baseUrl = `${host || 'http://localhost'}:${port || 8080}/v1/statement` this.catalog = catalog @@ -36,25 +49,114 @@ export class PrestoClient { // TODO: Set up auth } - private request({ - body, - headers, - method, - url, + /** + * Retrieves all catalogs. + * @returns {Promise} An array of all the catalog names. + */ + async getCatalogs(): Promise { + return (await this.query('SHOW CATALOGS')).data?.map(([catalog]) => catalog as string) + } + + /** + * Retrieves a list of columns filtered for the given catalog and optional filters. + * @param {Object} options - The options for retrieving columns. + * @param {string} options.catalog - The catalog name. + * @param {string} [options.schema] - The schema name. Optional. + * @param {string} [options.table] - The table name. Optional. + * @returns {Promise} An array of all the columns that match the given filters. + */ + async getColumns({ + catalog, + schema, + table, }: { - body?: string - headers?: Record - method: string - url: string - }) { - return fetch(url, { - body, - headers, - method, - }) + catalog: string + schema?: string + table?: string + }): Promise { + const whereCondition = this.getWhereCondition([ + { key: 'table_schema', value: schema }, + { key: 'table_name', value: table }, + ]) + + // The order of the select expression columns is important since we destructure them in the same order below + const query = `SELECT table_catalog, table_schema, table_name, column_name, column_default, is_nullable, data_type, comment, extra_info FROM information_schema.columns ${whereCondition}` + const rawResult = ( + await this.query(query, { + catalog, + }) + ).data + return rawResult?.map( + ([ + // This destructuring names the fields properly for each row, and converts them to camelCase + tableCatalog, + tableSchema, + tableName, + columnName, + columnDefault, + isNullable, + dataType, + comment, + extraInfo, + ]) => ({ + tableCatalog, + tableSchema, + tableName, + columnName, + columnDefault, + isNullable, + dataType, + comment, + extraInfo, + }), + ) as Column[] } /** + * Retrieves all schemas within a given catalog. + * @param {string} catalog - The name of the catalog for which to retrieve schemas. + * @returns {Promise} An array of schema names within the specified catalog. + */ + async getSchemas(catalog: string): Promise { + return ( + await this.query('SHOW SCHEMAS', { + catalog, + }) + ).data?.map(([schema]) => schema as string) + } + + /** + * Retrieves a list of tables filtered by the given catalog and optional schema. + * @param {Object} options - The options for retrieving tables. + * @param {string} options.catalog - The catalog name. + * @param {string} [options.schema] - The schema name. Optional. + * @returns {Promise} An array of tables that match the given filters. + */ + async getTables({ catalog, schema }: { catalog: string; schema?: string }): Promise { + const whereCondition = this.getWhereCondition([{ key: 'table_schema', value: schema }]) + // The order of the select expression columns is important since we destructure them in the same order below + const query = `SELECT table_catalog, table_schema, table_name, table_type FROM information_schema.tables ${whereCondition}` + const rawResult = ( + await this.query(query, { + catalog, + }) + ).data + // This destructuring names the fields properly for each row, and converts them to camelCase + return rawResult?.map(([tableCatalog, tableSchema, tableName, tableType]) => ({ + tableCatalog, + tableSchema, + tableName, + tableType, + })) as Table[] + } + + /** + * Executes a given query with optional catalog and schema settings. + * @param {string} query - The SQL query string to be executed. + * @param {Object} [options] - Optional parameters for the query. + * @param {string} [options.catalog] - The catalog to be used for the query. Optional. + * @param {string} [options.schema] - The schema to be used for the query. Optional. + * @returns {Promise} A promise that resolves to the result of the query execution. * @throws {PrestoError} If the underlying Presto engine returns an error */ async query( @@ -143,6 +245,34 @@ export class PrestoClient { private delay(milliseconds: number) { return new Promise(resolve => setTimeout(resolve, milliseconds)) } + + // This builds a WHERE statement if one or more of the conditions contain non-undefined values + // Currently only works for string values (need more conditions for number and boolean) + private getWhereCondition(conditions: { key: string; value?: string }[]): string { + const filteredConditions = conditions.filter(({ value }) => Boolean(value)) + if (filteredConditions.length) { + return `WHERE ${filteredConditions.map(({ key, value }) => `${key} = '${value}'`).join(' AND ')}` + } + return '' + } + + private request({ + body, + headers, + method, + url, + }: { + body?: string + headers?: Record + method: string + url: string + }) { + return fetch(url, { + body, + headers, + method, + }) + } } export default PrestoClient diff --git a/presto-client/src/information-schema.types.ts b/presto-client/src/information-schema.types.ts new file mode 100644 index 0000000..b090363 --- /dev/null +++ b/presto-client/src/information-schema.types.ts @@ -0,0 +1,19 @@ +export interface Table { + tableCatalog: string + tableName: string + tableSchema: string + tableType: string +} + +export interface Column { + columnDefault: unknown + columnName: string + comment: string + dataType: string + extraInfo: unknown + isNullable: boolean + ordinalPosition: number + tableCatalog: string + tableName: string + tableSchema: string +}