mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
fix(formatter): keep ClickHouse date abbreviation alias case
This commit is contained in:
@@ -124,6 +124,42 @@ OR inside$$ as note
|
||||
expect(formatted).toContain("x -> dictGet");
|
||||
expect(formatted).not.toContain("- >");
|
||||
});
|
||||
|
||||
it("preserves the ClickHouse table alias from issue #7079", async () => {
|
||||
const formatted = await formatSqlText("SELECT *\nFROM MATERIAL m\nLIMIT 100;", sqlFormatDialectForDbType("clickhouse"));
|
||||
|
||||
expect(formatted).toBe("SELECT\n *\nFROM\n MATERIAL m\nLIMIT\n 100;");
|
||||
});
|
||||
|
||||
it.each(["d", "dd", "h", "hh", "m", "mcs", "mi", "mm", "ms", "n", "ns", "q", "qq", "s", "ss", "wk", "ww", "yy", "yyyy"])("preserves ClickHouse identifier-like date part %s while formatting keywords", async (identifier) => {
|
||||
const formatted = await formatSqlText(`select ${identifier}, t.${identifier} from material ${identifier} limit 100;`, sqlFormatDialectForDbType("clickhouse"));
|
||||
|
||||
expect(formatted).toContain(` ${identifier},`);
|
||||
expect(formatted).toContain(`material ${identifier}`);
|
||||
expect(formatted).toContain("SELECT");
|
||||
expect(formatted).toContain("FROM");
|
||||
expect(formatted).toContain("LIMIT");
|
||||
});
|
||||
|
||||
it("keeps ClickHouse identifier casing independent from keyword casing", async () => {
|
||||
const sql = "SELECT * FROM MATERIAL M LIMIT 100;";
|
||||
|
||||
const lowerKeywords = await formatSqlText(sql, sqlFormatDialectForDbType("clickhouse"), { keywordCase: "lower", identifierCase: "preserve" });
|
||||
const lowerIdentifiers = await formatSqlText(sql, sqlFormatDialectForDbType("clickhouse"), { keywordCase: "upper", identifierCase: "lower" });
|
||||
|
||||
expect(lowerKeywords).toContain("from\n MATERIAL M");
|
||||
expect(lowerKeywords).toContain("limit\n 100");
|
||||
expect(lowerIdentifiers).toContain("FROM\n material m");
|
||||
expect(lowerIdentifiers).toContain("LIMIT\n 100");
|
||||
});
|
||||
|
||||
it("still formats unambiguous ClickHouse interval keywords", async () => {
|
||||
const formatted = await formatSqlText("select now() + interval 1 minutes from source_table m;", sqlFormatDialectForDbType("clickhouse"));
|
||||
|
||||
expect(formatted).toContain("INTERVAL 1 MINUTES");
|
||||
expect(formatted).toContain("FROM\n source_table m");
|
||||
});
|
||||
|
||||
it("preserves DBX brace placeholders in generic and MySQL SQL", async () => {
|
||||
const sql = "SELECT ${x} AS shell_value, #{x} AS mybatis_value, '${date}' AS quoted_value";
|
||||
|
||||
|
||||
@@ -5,6 +5,27 @@ export type SqlFormatDialect = "mysql" | "postgres" | "sqlite" | "sqlserver" | "
|
||||
|
||||
export const MAX_SQL_FORMAT_CHARS = 1_000_000;
|
||||
|
||||
type SqlFormatterModule = typeof import("sql-formatter");
|
||||
|
||||
// sql-formatter classifies ClickHouse date-part abbreviations as reserved
|
||||
// keywords even where ClickHouse accepts them as ordinary identifiers. Keep
|
||||
// them identifier-like so keyword casing cannot rewrite aliases such as `m`.
|
||||
const CLICKHOUSE_IDENTIFIER_LIKE_DATE_PARTS = new Set(["D", "DD", "H", "HH", "M", "MCS", "MI", "MM", "MS", "N", "NS", "Q", "QQ", "S", "SS", "WK", "WW", "YY", "YYYY"]);
|
||||
let clickHouseIdentifierSafeDialect: SqlFormatterModule["clickhouse"] | null = null;
|
||||
|
||||
function resolveClickHouseIdentifierSafeDialect(sqlFormatter: SqlFormatterModule): SqlFormatterModule["clickhouse"] {
|
||||
if (clickHouseIdentifierSafeDialect) return clickHouseIdentifierSafeDialect;
|
||||
|
||||
clickHouseIdentifierSafeDialect = {
|
||||
...sqlFormatter.clickhouse,
|
||||
tokenizerOptions: {
|
||||
...sqlFormatter.clickhouse.tokenizerOptions,
|
||||
reservedKeywords: sqlFormatter.clickhouse.tokenizerOptions.reservedKeywords.filter((keyword) => !CLICKHOUSE_IDENTIFIER_LIKE_DATE_PARTS.has(keyword)),
|
||||
},
|
||||
};
|
||||
return clickHouseIdentifierSafeDialect;
|
||||
}
|
||||
|
||||
export function canFormatSqlForDatabaseType(dbType: string | null | undefined): boolean {
|
||||
return dbType !== "victoriametrics";
|
||||
}
|
||||
@@ -223,7 +244,8 @@ export async function formatSqlText(sql: string, dialect: SqlFormatDialect = "ge
|
||||
throw new UnsupportedStructuredInputError("xml");
|
||||
}
|
||||
|
||||
const { format } = await import("sql-formatter");
|
||||
const sqlFormatter = await import("sql-formatter");
|
||||
const { format, formatDialect } = sqlFormatter;
|
||||
const normalizedSettings = normalizeSqlFormatterSettings(settings);
|
||||
const options = sqlFormatterOptions(normalizedSettings);
|
||||
const language = formatterLanguage(dialect);
|
||||
@@ -244,6 +266,9 @@ export async function formatSqlText(sql: string, dialect: SqlFormatDialect = "ge
|
||||
: options;
|
||||
const formatWithFallback = (input: string): string => {
|
||||
try {
|
||||
if (dialect === "clickhouse") {
|
||||
return formatDialect(input, { ...formatterOptions, dialect: resolveClickHouseIdentifierSafeDialect(sqlFormatter) });
|
||||
}
|
||||
return format(input, { language, ...formatterOptions });
|
||||
} catch (err) {
|
||||
// The generic "sql" dialect can't parse many real-world constructs (PostgreSQL
|
||||
|
||||
Reference in New Issue
Block a user