插件与扩展

扩展 API 遵循当前 LogicForm V2 契约。

LogicForm V2 的扩展对象都由调用方创建、注册并传给执行入口。它们不是全局单例,不会自动影响其他请求、租户或进程。

扩展边界怎么选

需求使用方式
新的业务字段类型Semantic Type
新的单列计算Operator
Schema 元数据 + Operator + 实体规则等跨层能力SemanticDB Plugin
同一逻辑 Schema 映射到新表/预聚合表Schema Mapping
新数据库协议或 SQL 方言Database Provider
复用查询结果Query Cache

自定义 Semantic Type

定义契约

interface SemanticTypeDefinition {
  name: string;
  primalType: 'string' | 'number' | 'boolean' | 'date' | 'object';
  validateProperty?: (property: ResolvedSemanticProperty) => void;
  normalizeQuery?: (
    value: unknown,
    context: SemanticTypeNormalizeContext,
  ) => unknown;
  requiresPerPeriodEvaluation?: (
    context: SemanticPeriodEvaluationContext,
  ) => boolean;
  hydrateResult?: (
    value: unknown,
    context: SemanticTypeResultContext,
  ) => unknown;
}
Hook用途
validateProperty校验该类型专属的 Property 配置
normalizeQuery在基础 Primal Type 处理后规范化该类型的 Query 值
requiresPerPeriodEvaluation声明该字段是否需要逐时间桶计算
hydrateResult数据库返回后转换结果值;可批量请求关联实体

primalType 描述数组元素,数组容器仍由 property.isArray 表示。所有 primalType: 'date' 的类型自动获得统一日期 Query 能力,不需要在自定义类型中重复实现。

注册示例

import {
  SemanticDBError,
  createSemanticTypeRegistry,
  type SemanticTypeDefinition,
} from 'semanticdb-v2';

const ratingType: SemanticTypeDefinition = {
  name: 'rating',
  primalType: 'number',
  validateProperty(property) {
    const values = property.constraints?.enum;
    if (!Array.isArray(values) || values.some((value) =>
      typeof value !== 'number' || value < 1 || value > 5)) {
      throw new SemanticDBError(
        'INVALID_RATING_CONFIG',
        `Property ${property.name} must declare ratings from 1 to 5`,
      );
    }
  },
};

const semanticTypes = createSemanticTypeRegistry()
  .register(ratingType);

await Logicform.execute(logicform, {
  schemas,
  schemaMappings,
  connections,
  semanticTypes,
});

createSemanticTypeRegistry() 已包含所有内置类型。注册名必须非空且唯一,不能覆盖内置定义。Registry 应在应用启动时创建并跨执行复用。

自定义 Operator

Operator 类别

type OperatorKind = 'scalar' | 'aggregate' | 'window' | 'logical' | 'component';
kind公开语义
scalar对每行输入计算一个标量;可生成 SQL 时可作为 groupby 键
aggregate聚合输入行;唯一允许 pred.query 的类别
window对分组结果计算排名、序号等窗口值
logical用公开逻辑表达式描述复杂计算或重写
component拆分为一个或多个完整 LogicForm,执行后合并为一个结果列

核心定义

interface OperatorDefinitionBase {
  name: string;
  kind: OperatorKind;
  defaultName(context: OperatorDefaultNameContext): string;
  requiresProperty?: boolean;
  dependencies?: string[];
  getOutputProperty?(context: OperatorOutputPropertyContext): ResultProperty;
  normalizePred?(context: OperatorNormalizePredContext): PredItemType;
  planInput?(context: OperatorPlanInputContext): OperatorPlannedInput;
  planExpression?(context: OperatorPlanExpressionContext): Expression;
  validatePlan?(context: OperatorValidatePlanContext): void;
  rewriteLogicform?(context: OperatorRewriteLogicformContext): LogicformType;
  requiresPerPeriodEvaluation?(context: OperatorPeriodEvaluationContext): boolean;
}

根据 kind 还必须提供对应执行能力:

  • SQL scalar/aggregate:toSQL(context)
  • JavaScript scalar:evaluate(context)
  • window:windowFunction
  • logical:planExpression(context)
  • component:createComponent(context)

每个 Operator 必须只产生一个结果列,并通过 getOutputProperty 准确描述 name、Semantic Type、Primal Type、isArrayref、日期粒度等结果元数据。

简单示例

import { createOperatorRegistry } from 'semanticdb-v2';

const operators = createOperatorRegistry().register({
  name: '$double',
  kind: 'scalar',
  defaultName: ({ operandName, locale }) =>
    locale.startsWith('en') ? `Double ${operandName}` : `${operandName}的两倍`,
  toSQL({ expression }) {
    if (!expression) throw new Error('$double requires pred');
    return `(${expression} * 2)`;
  },
  getOutputProperty({ name, inputProperty }) {
    if (!inputProperty) throw new Error('$double requires input');
    return { ...inputProperty, name, isArray: false };
  },
});

传入 ExecuteOptions.operators

await Logicform.execute(logicform, {
  schemas,
  schemaMappings,
  connections,
  operators,
});

createOperatorRegistry() 已包含核心 Operator 和内置插件 Operator。重复名称会失败,不会静默覆盖。

SQL 能力

自定义 SQL Operator 应通过 context 能力生成方言无关表达式:

interface OperatorSqlCapabilities {
  aggregateFunction(name: string, expression?: string): string;
  quoteIdentifier(identifier: string): string;
  concat(expressions: string[]): string;
  substring(expression: string, start: number, length: number): string;
  dateBucket(expression: string, granularity: TimeGranularity): string;
  value(value: unknown): string;
  condition(expression: string, condition: unknown): string;
}

value() 返回绑定参数占位符;不要自行把用户值拼进 SQL。

SemanticDB Plugin

当一个能力横跨多种扩展边界时,使用 Plugin:

interface SemanticDBPlugin {
  name: string;
  schemaMetadata?: {
    keys: string[];
    resolve(
      schema: SemanticSchemaDraft,
      properties: ResolvedSemanticProperty[],
    ): Record<string, unknown>;
  };
  legacySchemaKeys?: string[];
  normalizeLegacySchema?(context: {
    source: Record<string, unknown>;
    properties: SemanticPropertyDraft[];
  }): Record<string, unknown>;
  resolveEntityFilterProperty?(
    schema: ResolvedSemanticSchema,
  ): ResolvedSemanticProperty | undefined;
  operators?: OperatorDefinition[];
}

字段含义

字段/Hook用途
name稳定且非空的插件标识
schemaMetadata.keys插件拥有的 V2 Schema 元数据字段
schemaMetadata.resolve校验并规范化插件元数据
legacySchemaKeys只存在于旧 Schema、转换后应删除的字段
normalizeLegacySchema将旧元数据转换为 V2 元数据
resolveEntityFilterProperty选择 entity_id 应过滤的 Property
operators插件提供的 Operator

Plugin 应是冻结、无状态的定义。请求状态放在调用方或执行 context 中。

使用

await Logicform.execute(logicform, {
  schemas,
  schemaMappings,
  connections,
  plugins: [organizationPlugin],
});

插件仅作用于传入它的执行。碰撞检查包括:

  • 重复插件名称;
  • 多个插件声明相同 Schema 元数据 key;
  • 重复 Operator 名;
  • 插件 Operator 与核心 Operator 冲突。

所有冲突都会明确失败。

内置 hierarchy Plugin

系统默认启用 hierarchy 插件,它拥有:

  • Schema 的 hierarchy 元数据;
  • hierarchy_property 的兼容转换;
  • 层级实体的 entity_id 字段选择;
  • $hierarchyLevel Operator。

调用方不需要在 plugins 中重复传入它。

Database Provider

当前内置 MySQL、Doris、StarRocks、PostgreSQL、ClickHouse、Snowflake 和 Oracle Provider。调用方通常只需提供 DatabaseConnection

新的 Provider 需要实现数据库连接生命周期、参数绑定、标识符引用、Query 条件、数组关系、递归查询以及所有已启用 Operator 所需的 SQL 能力。Provider 只负责物理数据库协议和方言,不应自行改变 LogicForm 语义。具体 Provider 包的发布与注册入口将在 V2 正式发布前确定。

Query Cache

缓存通过 ExecuteOptions.cache 注入:

cache: {
  store: cache,
  namespace: 'tenant-a:analytics',
}

Cache 身份至少包含 namespace、Provider、参数化 SQL 和参数。Mapping 的 freshness 决定是否缓存及何时失效:

  • ttl:固定秒数;
  • daily:每天指定时区和时间过期;
  • manual:不过期,必须按物理 source 主动失效;
  • 未配置:该 source 参与的查询不跨执行缓存。

缓存由调用方持有并关闭。缓存读写失败时执行降级到数据库,数据库错误仍正常返回。