diff --git a/packages/cubejs-databricks-jdbc-driver/src/DatabricksQuery.ts b/packages/cubejs-databricks-jdbc-driver/src/DatabricksQuery.ts index b0da4374d9e2c..34372af6188f7 100644 --- a/packages/cubejs-databricks-jdbc-driver/src/DatabricksQuery.ts +++ b/packages/cubejs-databricks-jdbc-driver/src/DatabricksQuery.ts @@ -141,7 +141,9 @@ export class DatabricksQuery extends BaseQuery { } public escapeColumnName(name: string) { - return `\`${name}\``; + // Use ` to escape ` itself. + // https://docs.databricks.com/en/sql/language-manual/sql-ref-identifiers.html + return `\`${name.replaceAll('`', '``')}\``; } public override getFieldIndex(id: string): string | number | null { diff --git a/packages/cubejs-dremio-driver/driver/DremioQuery.js b/packages/cubejs-dremio-driver/driver/DremioQuery.js index 8e231f3d25caa..7e10824aa9bbc 100644 --- a/packages/cubejs-dremio-driver/driver/DremioQuery.js +++ b/packages/cubejs-dremio-driver/driver/DremioQuery.js @@ -96,7 +96,7 @@ class DremioQuery extends BaseQuery { } escapeColumnName(name) { - return `"${name}"`; + return `"${name.replaceAll('"', '""')}"`; } seriesSql(timeDimension) { diff --git a/packages/cubejs-ksql-driver/src/KsqlQuery.ts b/packages/cubejs-ksql-driver/src/KsqlQuery.ts index 21632a8648d99..9b29910f0c719 100644 --- a/packages/cubejs-ksql-driver/src/KsqlQuery.ts +++ b/packages/cubejs-ksql-driver/src/KsqlQuery.ts @@ -52,7 +52,9 @@ export class KsqlQuery extends BaseQuery { } public escapeColumnName(name: string) { - return `\`${name}\``; + // https://docs.confluent.io/platform/current/ksqldb/reference/sql/lexical-structure.html#ksqldb-lexical-structure-identifiers + // https://github.com/confluentinc/ksql/blob/84afdf1c2504844a15e02643f796288b8b069073/ksqldb-parser/src/main/antlr4/io/confluent/ksql/parser/SqlBase.g4#L378 + return `\`${name.replaceAll('`', '``')}\``; } public castToString(sql: string) { diff --git a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js index 43b270f34b5a9..4f35476f32c45 100644 --- a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js +++ b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js @@ -684,12 +684,16 @@ export class BaseQuery { } /** - * Wrap specified column/table name with the double quote. + * Wrap specified column/table name with the double quote and escape quotes inside. * @param {string} name * @returns {string} */ escapeColumnName(name) { - return `"${name}"`; + // Identifier is wrapped with double quotes + // https://ronsavage.github.io/SQL/sql-2003-2.bnf.html#delimited%20identifier + // Double quote inside is represented by double double quote + // https://ronsavage.github.io/SQL/sql-2003-2.bnf.html#doublequote%20symbol + return `"${name.replaceAll('"', '""')}"`; } /** diff --git a/packages/cubejs-schema-compiler/src/adapter/BigqueryQuery.ts b/packages/cubejs-schema-compiler/src/adapter/BigqueryQuery.ts index cdeb20e458613..922301a0c1e69 100644 --- a/packages/cubejs-schema-compiler/src/adapter/BigqueryQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/BigqueryQuery.ts @@ -54,7 +54,12 @@ export class BigqueryQuery extends BaseQuery { } public escapeColumnName(name) { - return `\`${name}\``; + // Quoted identifiers must be enclosed by ` + // Identifiers have the same escape sequences as string literals + // https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#quoted_identifiers + // \` is escape sequence for ` + // https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#escape_sequences + return `\`${name.replaceAll('`', '\\`')}\``; } public timeGroupedColumn(granularity, dimension) { diff --git a/packages/cubejs-schema-compiler/src/adapter/ClickHouseQuery.ts b/packages/cubejs-schema-compiler/src/adapter/ClickHouseQuery.ts index 8c3cdaedea6b1..994111d2b7365 100644 --- a/packages/cubejs-schema-compiler/src/adapter/ClickHouseQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/ClickHouseQuery.ts @@ -36,7 +36,13 @@ export class ClickHouseQuery extends BaseQuery { } public escapeColumnName(name) { - return `\`${name}\``; + // ClickHouse does not document it's exact rules regarding identifiers + // https://clickhouse.com/docs/en/sql-reference/syntax#identifiers + // But there's a bit about escaping in string + // https://clickhouse.com/docs/en/sql-reference/syntax#string + // Note that documentation does not allow to use \` sequence + // See https://github.com/ClickHouse/ClickHouse/issues/71310 + return `\`${name.replaceAll('`', '\\x60')}\``; } public convertTz(field) { @@ -260,7 +266,8 @@ export class ClickHouseQuery extends BaseQuery { delete templates.functions.PERCENTILECONT; delete templates.expressions.like_escape; templates.quotes.identifiers = '`'; - templates.quotes.escape = '\\`'; + // See escapeColumnName comment + templates.quotes.escape = '\\x60'; templates.types.boolean = 'BOOL'; templates.types.timestamp = 'DATETIME'; delete templates.types.time; diff --git a/packages/cubejs-schema-compiler/src/adapter/CubeStoreQuery.ts b/packages/cubejs-schema-compiler/src/adapter/CubeStoreQuery.ts index fd6f23a44a666..9dd3d0396d312 100644 --- a/packages/cubejs-schema-compiler/src/adapter/CubeStoreQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/CubeStoreQuery.ts @@ -150,7 +150,7 @@ export class CubeStoreQuery extends BaseQuery { } public escapeColumnName(name) { - return `\`${name}\``; + return `\`${name.replaceAll('`', '``')}\``; } public seriesSql(timeDimension) { diff --git a/packages/cubejs-schema-compiler/src/adapter/HiveQuery.ts b/packages/cubejs-schema-compiler/src/adapter/HiveQuery.ts index 5cadce3d2d905..6e9255eff2bc1 100644 --- a/packages/cubejs-schema-compiler/src/adapter/HiveQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/HiveQuery.ts @@ -55,7 +55,9 @@ export class HiveQuery extends BaseQuery { } public escapeColumnName(name) { - return `\`${name}\``; + // Within a backtick string, use double backticks (``) to represent a backtick character + // https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=27362043#LanguageManualSelect-SelectSyntax + return `\`${name.replaceAll('`', '``')}\``; } public simpleQuery() { diff --git a/packages/cubejs-schema-compiler/src/adapter/MysqlQuery.ts b/packages/cubejs-schema-compiler/src/adapter/MysqlQuery.ts index a57153fd2cb75..e3aa4eb05c42a 100644 --- a/packages/cubejs-schema-compiler/src/adapter/MysqlQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/MysqlQuery.ts @@ -133,7 +133,9 @@ export class MysqlQuery extends BaseQuery { } public escapeColumnName(name) { - return `\`${name}\``; + // If the character to be included within the identifier is the same as that used to quote the identifier itself, then you need to double the character + // https://dev.mysql.com/doc/refman/8.0/en/identifiers.html + return `\`${name.replaceAll('`', '``')}\``; } public seriesSql(timeDimension) { diff --git a/packages/cubejs-schema-compiler/test/unit/base-query.test.ts b/packages/cubejs-schema-compiler/test/unit/base-query.test.ts index 3fc1822f338c4..7697a26555de3 100644 --- a/packages/cubejs-schema-compiler/test/unit/base-query.test.ts +++ b/packages/cubejs-schema-compiler/test/unit/base-query.test.ts @@ -2375,6 +2375,16 @@ describe('Class unit tests', () => { expect(baseQuery.cubeAlias('CamelCaseCube.id')).toEqual('"camel_case_cube__id"'); expect(baseQuery.cubeAlias('CamelCaseCube.description')).toEqual('"camel_case_cube__description"'); expect(baseQuery.cubeAlias('CamelCaseCube.grant_total')).toEqual('"camel_case_cube__grant_total"'); + + // aliasName + memberToAlias + const memberAliasQuery = new BaseQuery(set, { + memberToAlias: { + 'CamelCaseCube.id': 'alias("id")' + } + }); + expect(memberAliasQuery.aliasName('CamelCaseCube.id', false)).toEqual('alias("id")'); + // Should escape quotes + expect(memberAliasQuery.newDimension('CamelCaseCube.id').aliasName()).toEqual('"alias(""id"")"'); }); it('Test BaseQuery with aliased cube', async () => { @@ -2424,6 +2434,17 @@ describe('Class unit tests', () => { expect(baseQuery.cubeAlias('CamelCaseCube.id')).toEqual('"t1__id"'); expect(baseQuery.cubeAlias('CamelCaseCube.description')).toEqual('"t1__description"'); expect(baseQuery.cubeAlias('CamelCaseCube.grant_total')).toEqual('"t1__grant_total"'); + + // aliasName + memberToAlias + // Should ignore cube alias + const memberAliasQuery = new BaseQuery(set, { + memberToAlias: { + 'CamelCaseCube.id': 'alias("id")' + } + }); + expect(memberAliasQuery.aliasName('CamelCaseCube.id', false)).toEqual('alias("id")'); + // Should escape quotes + expect(memberAliasQuery.newDimension('CamelCaseCube.id').aliasName()).toEqual('"alias(""id"")"'); }); it('Test BaseQuery columns order for the query with the sub-query', async () => {