diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index c618ad6a85d32..242780a411355 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -434,6 +434,7 @@ jobs: yarn run smoke:questdb yarn run smoke:multidb yarn run smoke:lambda + yarn run smoke:prestodb docker-image-latest-set-tag: # At least git should be completed pushed up until this moment diff --git a/packages/cubejs-prestodb-driver/driver/PrestoDriver.js b/packages/cubejs-prestodb-driver/driver/PrestoDriver.js index e79a918d24dfe..7d5f3889eedd6 100644 --- a/packages/cubejs-prestodb-driver/driver/PrestoDriver.js +++ b/packages/cubejs-prestodb-driver/driver/PrestoDriver.js @@ -71,7 +71,10 @@ class PrestoDriver extends BaseDriver { } query(query, values) { - const queryWithParams = SqlString.format(query, values); + const queryWithParams = SqlString.format(query, (values || []).map(value => (typeof s === 'string' ? { + toSqlString: () => SqlString.escape(value).replace(/\\\\([_%])/g, '\\$1') + } : value))); + return this.queryPromised(queryWithParams); } diff --git a/packages/cubejs-testing-shared/src/db/index.ts b/packages/cubejs-testing-shared/src/db/index.ts index b5497bd0a78d4..439561de10b57 100644 --- a/packages/cubejs-testing-shared/src/db/index.ts +++ b/packages/cubejs-testing-shared/src/db/index.ts @@ -4,3 +4,4 @@ export * from './cubestore'; export * from './questdb'; export * from './materialize'; export * from './crate'; +export * from './prestodb'; diff --git a/packages/cubejs-testing-shared/src/db/prestodb.ts b/packages/cubejs-testing-shared/src/db/prestodb.ts new file mode 100644 index 0000000000000..71a9a49b4010a --- /dev/null +++ b/packages/cubejs-testing-shared/src/db/prestodb.ts @@ -0,0 +1,27 @@ +import { GenericContainer, Wait } from 'testcontainers'; + +import { DbRunnerAbstract, DBRunnerContainerOptions } from './db-runner.abstract'; + +type PrestoStartOptions = DBRunnerContainerOptions & { + version?: string, +}; + +export class PrestoDbRunner extends DbRunnerAbstract { + public static startContainer(options: PrestoStartOptions) { + const version = process.env.TEST_PRESTO_VERSION || options.version || '0.277'; + + const container = new GenericContainer(`ahanaio/prestodb-sandbox:${version}`) + .withExposedPorts(8080) + .withWaitStrategy(Wait.forLogMessage("======== SERVER STARTED ========")) + .withStartupTimeout(30 * 1000); + + if (options.volumes) { + // eslint-disable-next-line no-restricted-syntax + for (const { source, target, bindMode } of options.volumes) { + container.withBindMount(source, target, bindMode); + } + } + + return container.start(); + } +} diff --git a/packages/cubejs-testing/birdbox-fixtures/presto/schema/Orders.js b/packages/cubejs-testing/birdbox-fixtures/presto/schema/Orders.js new file mode 100644 index 0000000000000..3488eaa0768ac --- /dev/null +++ b/packages/cubejs-testing/birdbox-fixtures/presto/schema/Orders.js @@ -0,0 +1,39 @@ +cube('Orders', { + sql: ` + select 1 as id, 100 as amount, 'new' status + UNION ALL + select 2 as id, 200 as amount, 'new' status + UNION ALL + select 3 as id, 300 as amount, 'processed' status + UNION ALL + select 4 as id, 500 as amount, 'processed' status + UNION ALL + select 5 as id, 600 as amount, 'shipped' status + UNION ALL + select 6 as id, 700 as amount, 'cancelled_by_customer' status + `, + measures: { + count: { + type: 'count', + }, + totalAmount: { + sql: 'amount', + type: 'sum', + }, + }, + dimensions: { + status: { + sql: 'status', + type: 'string', + }, + }, + preAggregations: { + orderStatus: { + measures: [CUBE.count, CUBE.totalAmount], + dimensions: [CUBE.status], + refreshKey: { + every: '1 second', + } + }, + }, +}); diff --git a/packages/cubejs-testing/birdbox-fixtures/prestodb.yml b/packages/cubejs-testing/birdbox-fixtures/prestodb.yml new file mode 100644 index 0000000000000..28c7907711609 --- /dev/null +++ b/packages/cubejs-testing/birdbox-fixtures/prestodb.yml @@ -0,0 +1,22 @@ +version: "2.2" + +services: + cube: + container_name: birdbox-cube + image: ${BIRDBOX_CUBEJS_REGISTRY_PATH}cubejs/cube:${BIRDBOX_CUBEJS_VERSION:-latest} + environment: + CUBEJS_DB_TYPE: prestodb + + CUBEJS_DB_HOST: host.docker.internal + CUBEJS_DB_PORT: ${CUBEJS_DB_PORT:-8080} + CUBEJS_DB_PRESTO_CATALOG: ${CUBEJS_DB_NAME:-memory} + CUBEJS_DB_USER: ${CUBEJS_DB_USER:-presto} + + CUBEJS_DEV_MODE: "true" + CUBEJS_WEB_SOCKETS: "true" + CUBEJS_API_SECRET: mysupersecret + volumes: + - ./prestodb/schema:/cube/conf/schema + ports: + - "4000" + restart: always diff --git a/packages/cubejs-testing/package.json b/packages/cubejs-testing/package.json index d3f1ec937f727..9796b37991bc3 100644 --- a/packages/cubejs-testing/package.json +++ b/packages/cubejs-testing/package.json @@ -67,7 +67,9 @@ "smoke:redshift": "jest --verbose -i dist/test/smoke-redshift.test.js", "smoke:redshift:snapshot": "jest --verbose --updateSnapshot -i dist/test/smoke-redshift.test.js", "smoke:cubesql": "jest --verbose --forceExit -i dist/test/smoke-cubesql.test.js", - "smoke:cubesql:snapshot": "jest --verbose --forceExit --updateSnapshot -i dist/test/smoke-cubesql.test.js" + "smoke:cubesql:snapshot": "jest --verbose --forceExit --updateSnapshot -i dist/test/smoke-cubesql.test.js", + "smoke:prestodb": "jest --verbose -i dist/test/smoke-prestodb.test.js", + "smoke:prestodb:snapshot": "jest --verbose --forceExit --updateSnapshot -i dist/test/smoke-prestodb.test.js" }, "files": [ "dist/src", diff --git a/packages/cubejs-testing/src/REQUIRED_ENV_VARS.ts b/packages/cubejs-testing/src/REQUIRED_ENV_VARS.ts index a963856a8b7bf..4d2777ecf01da 100644 --- a/packages/cubejs-testing/src/REQUIRED_ENV_VARS.ts +++ b/packages/cubejs-testing/src/REQUIRED_ENV_VARS.ts @@ -44,4 +44,5 @@ export const REQUIRED_ENV_VARS: {[key: string]: string[]} = { 'CUBEJS_DB_EXPORT_BUCKET_AWS_SECRET', 'CUBEJS_DB_EXPORT_BUCKET_AWS_REGION', ], + prestodb: [] }; diff --git a/packages/cubejs-testing/src/birdbox.ts b/packages/cubejs-testing/src/birdbox.ts index 527f3a44b7390..f6b0a21038f3a 100644 --- a/packages/cubejs-testing/src/birdbox.ts +++ b/packages/cubejs-testing/src/birdbox.ts @@ -48,7 +48,7 @@ interface Args { log: Log, } -export type DriverType = 'postgresql' | 'postgres' | 'multidb' | 'materialize' | 'crate' | 'bigquery' | 'athena' | 'postgresql-cubestore' | 'firebolt' | 'questdb' | 'redshift' | 'databricks-jdbc'; +export type DriverType = 'postgresql' | 'postgres' | 'multidb' | 'materialize' | 'crate' | 'bigquery' | 'athena' | 'postgresql-cubestore' | 'firebolt' | 'questdb' | 'redshift' | 'databricks-jdbc' | 'prestodb'; export type Schemas = string[]; @@ -102,7 +102,8 @@ const driverNameToFolderNameMapper: Record = { firebolt: 'postgresql', questdb: 'postgresql', redshift: 'postgresql', - 'databricks-jdbc': 'databricks-jdbc' + 'databricks-jdbc': 'databricks-jdbc', + prestodb: 'postgresql' }; /** diff --git a/packages/cubejs-testing/test/__snapshots__/smoke-prestodb.test.ts.snap b/packages/cubejs-testing/test/__snapshots__/smoke-prestodb.test.ts.snap new file mode 100644 index 0000000000000..10de78c92be9e --- /dev/null +++ b/packages/cubejs-testing/test/__snapshots__/smoke-prestodb.test.ts.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`prestodb query dimensions with underscore filter: dimensions 1`] = ` +Array [ + Object { + "Orders.status": "cancelled_by_customer", + }, +] +`; + +exports[`prestodb query dimensions: dimensions 1`] = ` +Array [ + Object { + "Orders.status": "processed", + "Orders.totalAmount": "800", + }, + Object { + "Orders.status": "cancelled_by_customer", + "Orders.totalAmount": "700", + }, + Object { + "Orders.status": "shipped", + "Orders.totalAmount": "600", + }, + Object { + "Orders.status": "new", + "Orders.totalAmount": "300", + }, +] +`; + +exports[`prestodb query measure: query 1`] = ` +Array [ + Object { + "Orders.totalAmount": "2400", + }, +] +`; diff --git a/packages/cubejs-testing/test/smoke-prestodb.test.ts b/packages/cubejs-testing/test/smoke-prestodb.test.ts new file mode 100644 index 0000000000000..d571e156454c9 --- /dev/null +++ b/packages/cubejs-testing/test/smoke-prestodb.test.ts @@ -0,0 +1,71 @@ +import cubejs, { CubejsApi } from '@cubejs-client/core'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { afterAll, beforeAll, expect, jest } from '@jest/globals'; +import { PrestoDbRunner } from '@cubejs-backend/testing-shared'; +import { BirdBox, getBirdbox } from '../src'; +import { DEFAULT_CONFIG, testQueryMeasure } from './smoke-tests'; + +describe('prestodb', () => { + jest.setTimeout(60 * 5 * 1000); + let birdbox: BirdBox; + let client: CubejsApi; + + beforeAll(async () => { + const db = await PrestoDbRunner.startContainer({}); + birdbox = await getBirdbox( + 'prestodb', + { + CUBEJS_DB_TYPE: 'prestodb', + + CUBEJS_DB_HOST: db.getHost(), + CUBEJS_DB_PORT: `${db.getMappedPort(8080)}`, + CUBEJS_DB_PRESTO_CATALOG: 'memory', + CUBEJS_DB_USER: 'test', + + ...DEFAULT_CONFIG, + }, + { + schemaDir: 'presto/schema', + } + ); + client = cubejs(async () => 'test', { + apiUrl: birdbox.configuration.apiUrl, + }); + }); + + afterAll(async () => { + await birdbox.stop(); + }); + + test('query measure', () => testQueryMeasure(client)); + + test('query dimensions', async () => { + const response = await client.load({ + measures: [ + 'Orders.totalAmount', + ], + dimensions: [ + 'Orders.status', + ], + }); + + expect(response.rawData()).toMatchSnapshot('dimensions'); + }); + + test('query dimensions with underscore filter', async () => { + const response = await client.load({ + filters: [ + { + member: 'Orders.status', + operator: 'contains', + values: ['cancelled_'] + } + ], + dimensions: [ + 'Orders.status', + ], + }); + + expect(response.rawData()).toMatchSnapshot('dimensions'); + }); +});