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

feat(presto-client): add get catalog, schema, table & column utility methods #18

Merged
merged 9 commits into from
Jan 18, 2024
20 changes: 19 additions & 1 deletion apps/nest-server/src/app/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
import { Controller, Get } from '@nestjs/common'
import { Controller, Get, Param } from '@nestjs/common'

import { AppService } from './app.service'

@Controller()
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 {
Expand Down
32 changes: 32 additions & 0 deletions apps/nest-server/src/app/app.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,38 @@ import PrestoClient, { PrestoClientConfig } from '@prestodb/presto-js-client'

@Injectable()
export class AppService {
async getCatalogs(): Promise<string[] | undefined> {
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<string[] | undefined> {
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<unknown> {
const clientParams: PrestoClientConfig = {
catalog: 'tpch',
Expand Down
65 changes: 65 additions & 0 deletions presto-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,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 filtered by the given catalog and, optionally, the schema. It accepts an object containing `catalog` and optional `schema` parameters.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also the Table data type here. Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done for both cases 👍


Parameters

- `catalog`: The catalog name.
- `schema` (optional): The schema name.

#### Example usage

```typescript
const tables = await prestoClient.getTables({ catalog: 'tpch', schema: 'sf100' })
console.log(tables)
```

### Get Columns

The `getColumns` method retrieves a list of columns filtered for the given catalog and optional schema and table filters. It accepts an object with `catalog`, and optional `schema` and `table` parameters.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good if you could mention the Column data type here.


Parameters

- `catalog`: The catalog name.
- `schema` (optional): The schema name.
- `table` (optional): The table name.

#### Example usage

```typescript
const columns = await prestoClient.getColumns({
catalog: 'tpch',
schema: 'sf100',
table: 'orders',
})
console.log(columns)
```
162 changes: 147 additions & 15 deletions presto-client/src/client.ts
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved all private methods to the bottom, and left the public ones alphabetically sorted at the top

Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PrestoClientConfig, PrestoQuery, PrestoResponse } from './client.types'
import { Column, Table } from './information-schema.types'

export class PrestoClient {
private baseUrl: string
Expand All @@ -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
Expand All @@ -36,24 +49,115 @@ export class PrestoClient {
// TODO: Set up auth
}

private request({
body,
headers,
method,
url,
/**
* Retrieves all catalogs.
* @returns {Promise<string[] | undefined>} An array of all the catalog names.
*/
async getCatalogs(): Promise<string[] | undefined> {
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<Column[] | undefined>} An array of all the columns that match the given filters.
*/
async getColumns({
catalog,
schema,
table,
}: {
body?: string
headers?: Record<string, string>
method: string
url: string
}) {
return fetch(url, {
body,
headers,
method,
})
catalog: string
schema?: string
table?: string
}): Promise<Column[] | undefined> {
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<string[] | undefined>} An array of schema names within the specified catalog.
*/
async getSchemas(catalog: string): Promise<string[] | undefined> {
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<Table[] | undefined>} An array of tables that match the given filters.
*/
async getTables({ catalog, schema }: { catalog: string; schema?: string }): Promise<Table[] | undefined> {
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<PrestoQuery>} A promise that resolves to the result of the query execution.
*/
async query(
query: string,
options?: {
Expand Down Expand Up @@ -136,9 +240,37 @@ export class PrestoClient {
}
}

private request({
body,
headers,
method,
url,
}: {
body?: string
headers?: Record<string, string>
method: string
url: string
}) {
return fetch(url, {
body,
headers,
method,
})
}

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 ''
}
}

export default PrestoClient
19 changes: 19 additions & 0 deletions presto-client/src/information-schema.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export interface Table {
alonsovb marked this conversation as resolved.
Show resolved Hide resolved
tableCatalog: string
tableSchema: string
tableName: string
tableType: string
}

export interface Column {
tableCatalog: string
tableSchema: string
tableName: string
columnName: string
ordinalPosition: number
columnDefault: unknown
isNullable: boolean
dataType: string
comment: string
extraInfo: unknown
}
Loading