ai-query对接新版freesun-agent接口的分支
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

198 lines
6.7 KiB

'use strict';
const {
CLICKHOUSE_IDENTIFIER_PATTERN,
} = require('./constants');
const { ensureObjectPayload } = require('./helpers');
const { ensureDataSource } = require('./repository');
const {
validateStructuredQueryDslAgainstFields,
buildStructuredPreviewSql,
} = require('./structuredQuery');
const ensureClickHouseIdentifier = (value, label = 'identifier') => {
const normalized = String(value || '').trim();
if (!normalized) throw `missing param: ${label}`;
if (!CLICKHOUSE_IDENTIFIER_PATTERN.test(normalized)) throw `invalid param: ${label}`;
return normalized;
};
const parseClickHouseRows = jsonResult => {
if (Array.isArray(jsonResult)) return jsonResult.map(r => ({ ...r, id: r.name }));
if (Array.isArray(jsonResult?.data)) return jsonResult.data.map(r => ({ ...r, id: r.name }));
return [];
};
const resolveClickHouseDataSourceContextFromDataSource = async (ctx, dataSource, transaction = null) => {
if (String(dataSource?.type || '').trim().toLowerCase() !== 'clickhouse') {
throw 'data source type must be clickhouse';
}
const connectionConfig = ensureObjectPayload(dataSource.connectionConfig || {}, 'connectionConfig');
const clientKey = String(connectionConfig.clientKey || '').trim();
const database = String(connectionConfig.database || '').trim();
if (!clientKey) throw 'missing param: connectionConfig.clientKey';
if (!database) throw 'missing param: connectionConfig.database';
const client = ctx.app?.fs?.clickHouse?.[clientKey];
if (!client) throw `clickhouse client not found: ${clientKey}`;
const configuredDbs = Array.isArray(ctx.app?.fs?.config?.clickHouse?.db)
? ctx.app.fs.config.clickHouse.db
: [];
const matched = configuredDbs.find(item => String(item?.name || '').trim() === clientKey);
if (!matched) throw `clickhouse client not configured: ${clientKey}`;
if (String(matched.db || '').trim() !== database) {
throw `database mismatch for client: ${clientKey}`;
}
return {
dataSource,
clientKey,
database,
client,
transaction,
};
};
const resolveClickHouseDataSourceContext = async (ctx, dataSourceId, transaction = null) => {
const dataSource = await ensureDataSource(ctx, dataSourceId, transaction);
return resolveClickHouseDataSourceContextFromDataSource(ctx, dataSource, transaction);
};
const queryClickHouseRows = async (client, query, queryParams = {}) => {
const normalizedQuery = String(query || '').trim().replace(/;+\s*$/, '');
const finalQuery = normalizedQuery.replace(/\s+FORMAT\s+[A-Za-z0-9_]+\s*$/i, '');
if (process.env.NODE_ENV === 'development') {
console.log('finalQuery:', finalQuery);
}
const result = await client.query({
query: finalQuery,
query_params: queryParams,
format: 'JSON',
});
const json = await result.json();
return parseClickHouseRows(json);
};
const getClickHouseTableColumns = async (client, database, tableName) => {
const query = `
SELECT
name,
type,
default_kind AS "defaultKind",
default_expression AS "defaultExpression",
is_in_primary_key AS "isInPrimaryKey",
is_in_sorting_key AS "isInSortingKey",
position
FROM system.columns
WHERE database = {database:String}
AND table = {table:String}
ORDER BY position ASC
`;
return queryClickHouseRows(client, query, { database, table: tableName });
};
const validateStructuredQueryDsl = async (dataSourceContext, rawDsl) => {
const tableName = ensureClickHouseIdentifier(rawDsl?.table, 'table');
const columns = await getClickHouseTableColumns(
dataSourceContext.client,
dataSourceContext.database,
tableName
);
if (!columns.length) throw `table not found: ${tableName}`;
const fieldSet = new Set(columns.map(item => item.name));
return validateStructuredQueryDslAgainstFields(rawDsl, fieldSet, {
tableParser: ensureClickHouseIdentifier,
fieldParser: ensureClickHouseIdentifier,
aliasParser: ensureClickHouseIdentifier,
});
};
const listClickHouseTables = async (ctx, dataSource, transaction = null) => {
const dataSourceContext = await resolveClickHouseDataSourceContextFromDataSource(ctx, dataSource, transaction);
const query = `
SELECT
name,
engine
FROM system.tables
WHERE database = {database:String}
ORDER BY name ASC
`;
const rows = await queryClickHouseRows(
dataSourceContext.client,
query,
{ database: dataSourceContext.database }
);
return {
dataSourceId: dataSource.id,
clientKey: dataSourceContext.clientKey,
database: dataSourceContext.database,
rows,
};
};
const getClickHouseTableColumnsPayload = async (ctx, dataSource, tableName, transaction = null) => {
const safeTableName = ensureClickHouseIdentifier(tableName, 'tableName');
const dataSourceContext = await resolveClickHouseDataSourceContextFromDataSource(ctx, dataSource, transaction);
const rows = await getClickHouseTableColumns(
dataSourceContext.client,
dataSourceContext.database,
safeTableName
);
if (!rows.length) throw `table not found: ${safeTableName}`;
return {
dataSourceId: dataSource.id,
clientKey: dataSourceContext.clientKey,
database: dataSourceContext.database,
table: safeTableName,
rows,
};
};
const previewClickHouseQuery = async (ctx, dataSource, rawDsl, transaction = null) => {
const dataSourceContext = await resolveClickHouseDataSourceContextFromDataSource(ctx, dataSource, transaction);
const normalizedDsl = await validateStructuredQueryDsl(dataSourceContext, rawDsl);
const queryBundle = buildStructuredPreviewSql(normalizedDsl);
const rows = await queryClickHouseRows(
dataSourceContext.client,
queryBundle.query,
queryBundle.queryParams
);
let total = null;
if (!queryBundle.hasGroupBy) {
const totalSql = `SELECT count() AS total FROM ${queryBundle.tableSql}${queryBundle.whereClause} FORMAT JSON`;
const totalRows = await queryClickHouseRows(
dataSourceContext.client,
totalSql,
queryBundle.queryParams
);
if (totalRows.length && totalRows[0]?.total !== undefined) {
total = Number(totalRows[0].total);
}
}
return {
columns: queryBundle.resultColumns,
rows,
total,
sqlPreview: queryBundle.sqlPreview,
};
};
module.exports = {
ensureClickHouseIdentifier,
resolveClickHouseDataSourceContext,
resolveClickHouseDataSourceContextFromDataSource,
queryClickHouseRows,
getClickHouseTableColumns,
validateStructuredQueryDsl,
listClickHouseTables,
getClickHouseTableColumnsPayload,
previewClickHouseQuery,
};