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:
- We want plugins to register custom SQL functions ( the extension point ), or
- 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
Summary
Operands\Funcvalidates functions against a singleprivate const ALLOWEDdescriptor 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: aFunc\Registry( likeOperators\Comparisons\Registry, extensible via aberlindb_database_*filter ) would let a plugin register its own allow-listed SQL function - which the closedconstcannot 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 ). SoALLOWEDis almost pure DATA; the only per-function "behavior" is the derived return pattern, and even that is a sharedreturn_pattern => nullbranch, 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.Lower,Year,Coalesce,DateSub,Greatest, ... ), each declaring its descriptor via properties; per-function behavior ( e.g.Coalesce/Greatest/Leastderiving their return pattern ) can move onto the class instead of a centralnull-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.Trigger ( do it when, not now )
Gate on a real driver, not symmetry:
resolve_func_operand()accumulates special-cases ( it is clean today ).Until then the dense
ALLOWEDconst works, is well-tested, and is easy to scan.Context
Operators\Comparisons\+Operators\Arithmetic\families, each a class with a co-locatedRegistry.