Skip to content

Func operands: split the ALLOWED allow-list into classes + a registry (function extension point) #227

Description

@JJJ

Summary

Operands\Func validates functions against a single private const ALLOWED descriptor array ( ~25 entries after #211/#226, each { sql, min_args, max_args|variadic, arg_kinds, return_pattern, accepts } ). Consider splitting it into a class-per-function family + a registry, mirroring the operator reorg ( Operators\Comparisons\ / Operators\Arithmetic\, f80952a ). The marquee payoff is an extension point: a Func\Registry ( like Operators\Comparisons\Registry, extensible via a berlindb_database_* filter ) would let a plugin register its own allow-listed SQL function - which the closed const cannot offer today.

The nuance ( why this is NOT just "do what the operators did" )

The operator split captured a real BEHAVIORAL divergence: a comparison renders a predicate ( {a} = {b} ), arithmetic an expression ( {a} + {b} ) - different machinery. Functions do not have that - every function renders the same way, NAME( args ). So ALLOWED is almost pure DATA; the only per-function "behavior" is the derived return pattern, and even that is a shared return_pattern => null branch, not per-function code.

So the split trades one dense, scannable table ( all functions + their arity/types visible at once ) for ~25 mostly-property-only files, for little behavioral gain on its own. Extensibility is what justifies it.

Proposed shape ( when a driver arrives )

  • Operands\Func\Base - the shared renderer ( NAME( args ), DISTINCT prefix ) + descriptor accessors.
  • One small class per function ( Lower, Year, Coalesce, DateSub, Greatest, ... ), each declaring its descriptor via properties; per-function behavior ( e.g. Coalesce/Greatest/Least deriving their return pattern ) can move onto the class instead of a central null-pattern check.
  • Operands\Func\Registry - holds the classes, resolves by name, and exposes a filter so third parties register custom functions. resolve_func_operand() resolves via the registry instead of reading the const.
  • Lighter middle ground if 25 classes feels heavy: keep descriptor DATA but move it into a filterable registry ( extension point without a class per function ). Less consistent with the operator pattern, but far fewer files.

Trigger ( do it when, not now )

Gate on a real driver, not symmetry:

  1. We want plugins to register custom SQL functions ( the extension point ), or
  2. Per-function behavior grows enough that resolve_func_operand() accumulates special-cases ( it is clean today ).

Until then the dense ALLOWED const works, is well-tested, and is easy to scan.

Context

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: Database/OperatorsSQL operator objects and operator behavior.object: OperandOperand value objects and operand resolution.object: OperatorOperator classes (comparison/arithmetic/logical) and their registries.object: QueryQuery object behavior, CRUD, filtering, caching, and SQL generation.scope: ExtensionThird-party extension integration or compatibility.type: FeatureNew capability or behavior.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions