diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 19a9787..fb156a9 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -60,13 +60,14 @@ jobs: # generated: a hand-kept copy goes stale in silence instead. - name: The generated table is what the catalog declares run: | - go run ./tools/codegen -catalog "${{ steps.provision.outputs.catalog-path }}" -o catalog_gen.go - if ! git diff --quiet -- catalog_gen.go; then - echo "::error::catalog_gen.go is not what the catalog declares. Regenerate it: go run ./tools/codegen -catalog -o catalog_gen.go" - git diff -- catalog_gen.go + go run ./tools/codegen -catalog "${{ steps.provision.outputs.catalog-path }}" \ + -o catalog_gen.go -o-meos mathops_meos_gen.go + if ! git diff --quiet -- catalog_gen.go mathops_meos_gen.go; then + echo "::error::the generated files are not what the catalog declares. Regenerate them: go run ./tools/codegen -catalog " + git diff -- catalog_gen.go mathops_meos_gen.go exit 1 fi - echo "the committed table is what the catalog of MobilityDB master declares" + echo "the committed tables are what the catalog of MobilityDB master declares" # EVERY TEST THIS REPOSITORY CARRIES RUNS HERE, AND NONE OF THEM SKIPS. # A test skips when a precondition is missing from the job that collects it, @@ -102,12 +103,12 @@ jobs: # The vendored OGC schema bundle is compared against what OGC publishes. MFAPI_SCHEMA_FRESHNESS: "1" # The floor is every result line `go test -v` writes, subtests included: - # 84 top-level tests and 64 subtests. Subtests belong in it because a + # 86 top-level tests and 64 subtests. Subtests belong in it because a # t.Run whose body skips is as unrun as its parent would be, and deleting # a case from a table-driven test moves nothing else. Raise it when the # suite grows. Lowering it belongs in the same commit as the removal it # accounts for, because deleting a test is what a skip refusal cannot see. - MFAPI_TEST_FLOOR: "148" + MFAPI_TEST_FLOOR: "150" steps: - uses: actions/checkout@v4 diff --git a/GENERATION.md b/GENERATION.md index f44db44..db519fd 100644 --- a/GENERATION.md +++ b/GENERATION.md @@ -7,14 +7,22 @@ MobilityDB master, regenerates against it, and refuses a difference. ## What is generated -`catalog_gen.go` is the table of MEOS temporal types: for each one, its name, -its base type, its bounding box, the token `asMFJSON` writes for it, whether it -is spatial, whether it is a number, and whether MEOS interpolates it linearly. +`catalog_gen.go` holds two tables. `temporalTypes` is the MEOS temporal types: +for each one, its name, its base type, its bounding box, the token `asMFJSON` +writes for it, whether it is spatial, whether it is a number, and whether MEOS +interpolates it linearly. `mathOps` is the MEOS functions that take a temporal +number and answer one, each with the SQL name it carries on every engine, the +MEOS C symbol behind it, and the C type of its one further parameter. + +`mathops_meos_gen.go` is the cgo call for each of those operations, behind the +`meos` build tag. It is what keeps a C symbol out of hand-written code: a MEOS +rename becomes a compile error rather than a wrong call. Run it against a catalog: ```bash -go run ./tools/codegen -catalog -o catalog_gen.go +go run ./tools/codegen -catalog \ + -o catalog_gen.go -o-meos mathops_meos_gen.go ``` ## Where the facts come from @@ -43,6 +51,16 @@ generator. ## What the tier adds, and what holds it complete +Which operations are POINTWISE is the tier's, because MEOS does not state it. +The catalog places `derivative`, `trend` and `angularDifference` in the same +group and the same category as `sin` and `abs`, since all five transform a +temporal number's values; only the first three do it by walking segments, and a +stream record is one instant with no neighbour to read. `liftedOps` in +`stream.go` declares the pointwise subset by SQL name, and `catalog_test.go` +checks every entry against `mathOps`: the name must be one the catalog states, +it must name exactly one function, and the query supplies the further parameter +exactly when the catalog gives it as a double. + Two things are the standard's rather than MEOS's, so they are stated in `main.go` next to the code that uses them: diff --git a/catalog_gen.go b/catalog_gen.go index 0b39f75..68a1f09 100644 --- a/catalog_gen.go +++ b/catalog_gen.go @@ -60,3 +60,50 @@ var temporalTypeByName = func() map[string]*TemporalType { } return m }() + +// MathOp is one MEOS function that takes a temporal number and answers one. +// SQLName is the name the operation carries on every engine, Symbol the MEOS C +// symbol behind it, and Arg the C type of its one further parameter, empty when +// it takes none. +type MathOp struct { + SQLName string + Symbol string + Arg string +} + +// mathOps is every such function the catalog states, ordered by SQL name. +var mathOps = []MathOp{ + {SQLName: "abs", Symbol: "tnumber_abs", Arg: ""}, + {SQLName: "angularDifference", Symbol: "tnumber_angular_difference", Arg: ""}, + {SQLName: "ceil", Symbol: "tfloat_ceil", Arg: ""}, + {SQLName: "cos", Symbol: "tfloat_cos", Arg: ""}, + {SQLName: "degrees", Symbol: "tfloat_degrees", Arg: "bool"}, + {SQLName: "derivative", Symbol: "temporal_derivative", Arg: ""}, + {SQLName: "exp", Symbol: "tfloat_exp", Arg: ""}, + {SQLName: "floor", Symbol: "tfloat_floor", Arg: ""}, + {SQLName: "ln", Symbol: "tfloat_ln", Arg: ""}, + {SQLName: "log10", Symbol: "tfloat_log10", Arg: ""}, + {SQLName: "radians", Symbol: "tfloat_radians", Arg: ""}, + {SQLName: "round", Symbol: "temporal_round", Arg: "int"}, + {SQLName: "scaleTime", Symbol: "temporal_scale_time", Arg: "const Interval *"}, + {SQLName: "setInterp", Symbol: "temporal_set_interp", Arg: "interpType"}, + {SQLName: "shiftTime", Symbol: "temporal_shift_time", Arg: "const Interval *"}, + {SQLName: "sin", Symbol: "tfloat_sin", Arg: ""}, + {SQLName: "tAdd", Symbol: "add_tfloat_float", Arg: "double"}, + {SQLName: "tDiv", Symbol: "div_tfloat_float", Arg: "double"}, + {SQLName: "tMul", Symbol: "mul_tfloat_float", Arg: "double"}, + {SQLName: "tSub", Symbol: "sub_tfloat_float", Arg: "double"}, + {SQLName: "tan", Symbol: "tfloat_tan", Arg: ""}, + {SQLName: "trend", Symbol: "tnumber_trend", Arg: ""}, +} + +// mathOpBySQLName indexes mathOps by the SQL name. A name several functions +// carry keeps them all, so a caller resolves it rather than reading whichever +// entry a map happened to hold. +var mathOpBySQLName = func() map[string][]*MathOp { + m := map[string][]*MathOp{} + for i := range mathOps { + m[mathOps[i].SQLName] = append(m[mathOps[i].SQLName], &mathOps[i]) + } + return m +}() diff --git a/catalog_test.go b/catalog_test.go index 7ab96e0..149bc2c 100644 --- a/catalog_test.go +++ b/catalog_test.go @@ -157,3 +157,61 @@ func TestValueColumnsAreOnePerScalarType(t *testing.T) { t.Errorf("the value columns are not in a stable order: %v", names) } } + +// Every operation the streaming tier lifts must be one the catalog states, with +// the shape the entry claims. A MEOS rename, a removal or a changed parameter +// then fails the build rather than a request at run time. +func TestEveryLiftedOpIsInTheCatalog(t *testing.T) { + for name, info := range liftedOps { + if info.sqlName == "" { + t.Errorf("the lifted operation %q names no SQL function", name) + continue + } + ops := mathOpBySQLName[info.sqlName] + if len(ops) == 0 { + t.Errorf("the lifted operation %q names the SQL function %q, which the catalog does not state over a temporal number", name, info.sqlName) + continue + } + if len(ops) > 1 { + var syms []string + for _, op := range ops { + syms = append(syms, op.Symbol) + } + sort.Strings(syms) + t.Errorf("the SQL name %q is carried by %v, so it does not name one operation", info.sqlName, syms) + continue + } + // The query supplies the further parameter exactly when the catalog + // gives it as a double. Where the catalog gives another type, as it + // does for the degrees normalisation flag, the tier passes a constant + // and the query supplies nothing. + if want := ops[0].Arg == "double"; info.needsArg != want { + t.Errorf("the lifted operation %q takes an argument from the query = %t, and the catalog gives %s a parameter of %q", + name, info.needsArg, ops[0].Symbol, ops[0].Arg) + } + } +} + +// The catalog states more operations over a temporal number than the tier +// lifts, and that is the point of holding the two apart: the ones left out are +// left out for a reason a reader can check. +func TestTheCatalogStatesMoreThanTheTierLifts(t *testing.T) { + if len(mathOps) < len(liftedOps) { + t.Fatalf("the catalog states %d operations and the tier lifts %d, so the tier lifts one the catalog does not", len(mathOps), len(liftedOps)) + } + lifted := map[string]bool{} + for _, info := range liftedOps { + lifted[info.sqlName] = true + } + // These three transform a temporal number's values by walking its segments, + // so on a stream record, which is one instant, they have no neighbour to + // read. They are in the catalog and must stay out of the lifted set. + for _, name := range []string{"derivative", "trend", "angularDifference"} { + if len(mathOpBySQLName[name]) == 0 { + t.Errorf("the catalog no longer states %q, so this test no longer measures anything", name) + } + if lifted[name] { + t.Errorf("%q is lifted, and its value at an instant is not a function of the value at that instant", name) + } + } +} diff --git a/mathops_meos_gen.go b/mathops_meos_gen.go new file mode 100644 index 0000000..b338b83 --- /dev/null +++ b/mathops_meos_gen.go @@ -0,0 +1,67 @@ +//go:build meos + +// Code generated by tools/codegen from the MEOS-API catalog. DO NOT EDIT. +// +// Regenerate with: +// +// go run ./tools/codegen -catalog -o-meos mathops_meos_gen.go +// +// One case per operation the catalog states over a temporal number, calling the +// MEOS symbol the catalog names. An operation whose further parameter cannot be +// filled from the single value a query carries gets no case. + +package main + +/* +#cgo pkg-config: meos +#include +*/ +import "C" + +// callMathOp applies the operation named by its SQL name to a temporal number, +// filling any further parameter from arg. It answers false when the operation +// is not one this dispatch serves, which the caller reports as an unknown +// operation rather than as a failed call. +func callMathOp(sqlName string, temp *C.Temporal, arg float64) (*C.Temporal, bool) { + switch sqlName { + case "abs": + return C.tnumber_abs(temp), true + case "angularDifference": + return C.tnumber_angular_difference(temp), true + case "ceil": + return C.tfloat_ceil(temp), true + case "cos": + return C.tfloat_cos(temp), true + case "degrees": + return C.tfloat_degrees(temp, arg != 0), true + case "derivative": + return C.temporal_derivative(temp), true + case "exp": + return C.tfloat_exp(temp), true + case "floor": + return C.tfloat_floor(temp), true + case "ln": + return C.tfloat_ln(temp), true + case "log10": + return C.tfloat_log10(temp), true + case "radians": + return C.tfloat_radians(temp), true + case "round": + return C.temporal_round(temp, C.int(arg)), true + case "sin": + return C.tfloat_sin(temp), true + case "tAdd": + return C.add_tfloat_float(temp, C.double(arg)), true + case "tDiv": + return C.div_tfloat_float(temp, C.double(arg)), true + case "tMul": + return C.mul_tfloat_float(temp, C.double(arg)), true + case "tSub": + return C.sub_tfloat_float(temp, C.double(arg)), true + case "tan": + return C.tfloat_tan(temp), true + case "trend": + return C.tnumber_trend(temp), true + } + return nil, false +} diff --git a/stream.go b/stream.go index cf329b2..36a26d7 100644 --- a/stream.go +++ b/stream.go @@ -200,31 +200,47 @@ func engineFor(name string) (StreamEngine, error) { return e, nil } -// opInfo describes a lifted scalar operation exposed by the streaming tier. The -// engine maps the name to the MEOS function; the control plane only validates. +// opInfo describes a lifted scalar operation exposed by the streaming tier. +// sqlName is the operation's name on every MEOS engine, which is what ties the +// entry to the catalog: catalog_gen.go states the MEOS symbol behind that name +// and the further parameter it takes. needsArg says the query supplies that +// parameter, which is so exactly when the catalog gives it as a double; where +// the catalog gives another type the tier passes a constant of its own. type opInfo struct { + sqlName string needsArg bool desc string } // liftedOps is the catalogue of scalar operations a continuous transform can -// apply to a tfloat stream. Every entry maps to a MEOS lifted temporal function. +// apply to a tfloat stream. It is the POINTWISE subset of the operations +// catalog_gen.go states: an operation whose value at an instant is a function +// of the value at that instant alone. A stream record is one instant, so an +// operation reading its neighbours has nothing to read. +// +// That property is the one thing about these operations MEOS does not state. +// The catalog places derivative, trend and angularDifference in the same group +// and the same category as sin and abs, because all five transform a temporal +// number's values; only the first three do it by walking segments. So the +// SUBSET is declared here and everything else about each entry is checked +// against the catalog by catalog_test.go: a MEOS rename, a removal, or a change +// of parameter fails the build rather than a request. var liftedOps = map[string]opInfo{ - "ln": {false, "natural logarithm"}, - "exp": {false, "exponential"}, - "log10": {false, "base-10 logarithm"}, - "ceil": {false, "ceiling"}, - "floor": {false, "floor"}, - "abs": {false, "absolute value"}, - "degrees": {false, "radians to degrees"}, - "radians": {false, "degrees to radians"}, - "sin": {false, "sine"}, - "cos": {false, "cosine"}, - "tan": {false, "tangent"}, - "add": {true, "add a scalar"}, - "sub": {true, "subtract a scalar"}, - "mul": {true, "multiply by a scalar"}, - "div": {true, "divide by a scalar"}, + "ln": {"ln", false, "natural logarithm"}, + "exp": {"exp", false, "exponential"}, + "log10": {"log10", false, "base-10 logarithm"}, + "ceil": {"ceil", false, "ceiling"}, + "floor": {"floor", false, "floor"}, + "abs": {"abs", false, "absolute value"}, + "degrees": {"degrees", false, "radians to degrees"}, + "radians": {"radians", false, "degrees to radians"}, + "sin": {"sin", false, "sine"}, + "cos": {"cos", false, "cosine"}, + "tan": {"tan", false, "tangent"}, + "add": {"tAdd", true, "add a scalar"}, + "sub": {"tSub", true, "subtract a scalar"}, + "mul": {"tMul", true, "multiply by a scalar"}, + "div": {"tDiv", true, "divide by a scalar"}, } func supportedOps() string { diff --git a/stream_engine_meos.go b/stream_engine_meos.go index fc82e35..f79b50f 100644 --- a/stream_engine_meos.go +++ b/stream_engine_meos.go @@ -291,41 +291,14 @@ func liftInstant(op string, arg float64, in Instant) (Instant, error) { } defer C.free(unsafe.Pointer(temp)) - var res *C.Temporal - switch op { - case "ln": - res = C.tfloat_ln(temp) - case "exp": - res = C.tfloat_exp(temp) - case "log10": - res = C.tfloat_log10(temp) - case "ceil": - res = C.tfloat_ceil(temp) - case "floor": - res = C.tfloat_floor(temp) - case "abs": - res = C.tnumber_abs(temp) - case "degrees": - res = C.tfloat_degrees(temp, false) - case "radians": - res = C.tfloat_radians(temp) - case "sin": - res = C.tfloat_sin(temp) - case "cos": - res = C.tfloat_cos(temp) - case "tan": - res = C.tfloat_tan(temp) - case "add": - res = C.add_tfloat_float(temp, C.double(arg)) - case "sub": - res = C.sub_tfloat_float(temp, C.double(arg)) - case "mul": - res = C.mul_tfloat_float(temp, C.double(arg)) - case "div": - res = C.div_tfloat_float(temp, C.double(arg)) - default: + info, lifted := liftedOps[op] + if !lifted { return Instant{}, fmt.Errorf("unknown operation %q", op) } + res, served := callMathOp(info.sqlName, temp, arg) + if !served { + return Instant{}, fmt.Errorf("the operation %q names the MEOS function %q, which this build does not call", op, info.sqlName) + } if res == nil { return Instant{}, fmt.Errorf("operation %q produced no result (domain error?)", op) } diff --git a/stream_meos_test.go b/stream_meos_test.go index 13d30dd..224acfc 100644 --- a/stream_meos_test.go +++ b/stream_meos_test.go @@ -40,6 +40,20 @@ func TestMeosTransform(t *testing.T) { {"cos", 0, 0, 1}, {"tan", 0, math.Pi / 4, 1}, } + // Every lifted operation is measured here, and that is what makes this an + // equivalence check rather than a sample: the dispatch it exercises names + // the MEOS symbol per operation, so an operation nobody measures could call + // the wrong one and still pass. + covered := map[string]bool{} + for _, c := range cases { + covered[c.op] = true + } + for op := range liftedOps { + if !covered[op] { + t.Errorf("the lifted operation %q is not measured here, so nothing checks which MEOS function it calls", op) + } + } + for _, c := range cases { ctx, cancel := context.WithCancel(context.Background()) src := make(chan Instant, 1) diff --git a/tools/codegen/main.go b/tools/codegen/main.go index d3b4dac..9c1ab84 100644 --- a/tools/codegen/main.go +++ b/tools/codegen/main.go @@ -28,11 +28,34 @@ import ( "go/format" "os" "sort" + "strings" ) // catalog is the part of meos-idl.json this generator reads. type catalog struct { TemporalTypes map[string]temporalType `json:"temporalTypes"` + Functions []function `json:"functions"` +} + +// function is the part of a catalog function entry the math-operation table +// reads: the C symbol, the SQL name that is the cross-engine invariant, the +// Doxygen group and category that place it, and the parameter C types. +type function struct { + Name string `json:"name"` + Group string `json:"group"` + Category string `json:"category"` + SQLName string `json:"sqlfn"` + Params []param `json:"params"` + Returns ctype `json:"returnType"` +} + +type param struct { + Name string `json:"name"` + CType string `json:"cType"` +} + +type ctype struct { + C string `json:"c"` } // temporalType is one entry of the catalog's temporalTypes registry. MFJSON is @@ -50,6 +73,7 @@ type temporalType struct { func main() { path := flag.String("catalog", "meos-idl.json", "the MEOS-API catalog") out := flag.String("o", "catalog_gen.go", "file to write") + outMeos := flag.String("o-meos", "mathops_meos_gen.go", "file to write the cgo dispatch to") flag.Parse() raw, err := os.ReadFile(*path) @@ -65,14 +89,29 @@ func main() { "MEOS-API that states one, or it is not a MEOS-API catalog", *path)) } - src, err := render(c.TemporalTypes) + ops := mathOperations(c.Functions) + if len(ops) == 0 { + fail(fmt.Errorf("%s names no temporal math operation: the groups this reads, "+ + "meos_temporal_math and meos_temporal_transf, are not where they were", *path)) + } + + src, err := render(c.TemporalTypes, ops) if err != nil { fail(err) } if err := os.WriteFile(*out, src, 0o644); err != nil { fail(err) } - fmt.Fprintf(os.Stderr, "codegen: %d temporal types written to %s\n", len(c.TemporalTypes), *out) + + dispatch, called, err := renderDispatch(ops) + if err != nil { + fail(err) + } + if err := os.WriteFile(*outMeos, dispatch, 0o644); err != nil { + fail(err) + } + fmt.Fprintf(os.Stderr, "codegen: %d temporal types and %d math operations written to %s, "+ + "%d of them callable from %s\n", len(c.TemporalTypes), len(ops), *out, called, *outMeos) } func fail(err error) { @@ -80,7 +119,145 @@ func fail(err error) { os.Exit(1) } -func render(types map[string]temporalType) ([]byte, error) { +// mathGroups are the two Doxygen groups MEOS puts a temporal number's value +// transformations in. +var mathGroups = map[string]bool{ + "meos_temporal_math": true, + "meos_temporal_transf": true, +} + +// mathPrefixes are the type classes whose functions accept a tfloat, which is +// the value a stream record carries. A MEOS function's leading token names the +// class it is generic over, so tfloat_, tnumber_ and temporal_ all admit one +// while tint_ and tbigint_ do not. +var mathPrefixes = map[string]bool{ + "tfloat": true, + "tnumber": true, + "temporal": true, +} + +// mathOperation is one MEOS function over a temporal number that the tier can +// apply to a stream record. +type mathOperation struct { + SQLName string // the SQL name, which is the same on every engine + Symbol string // the MEOS C symbol + Arg string // the C type of its one further parameter, empty when it takes none +} + +// mathOperations selects, from the catalog, the functions that take a temporal +// number and answer one. The selection is the function's own metadata and +// nothing else: +// +// - the Doxygen group places it among the value transformations; +// - the category is transformation, which is what separates a value +// transformation from a conversion (temporal_as_tinstant) and from an +// accessor (tnumber_delta_value); +// - it takes a Temporal and answers a Temporal; +// - its leading name token names a class that admits a tfloat, or it is the +// arithmetic form _tfloat_float; +// - it takes at most one further parameter, and that parameter is not itself +// a Temporal, since a stream record carries one value and not two. +func mathOperations(fns []function) []mathOperation { + var out []mathOperation + for _, f := range fns { + if !mathGroups[f.Group] || f.Category != "transformation" || f.SQLName == "" { + continue + } + if !strings.Contains(f.Returns.C, "Temporal *") { + continue + } + if len(f.Params) == 0 || !strings.Contains(f.Params[0].CType, "Temporal *") { + continue + } + rest := f.Params[1:] + if len(rest) > 1 || (len(rest) == 1 && strings.Contains(rest[0].CType, "Temporal *")) { + continue + } + if !mathPrefixes[strings.SplitN(f.Name, "_", 2)[0]] && !strings.HasSuffix(f.Name, "_tfloat_float") { + continue + } + arg := "" + if len(rest) == 1 { + arg = rest[0].CType + } + out = append(out, mathOperation{SQLName: f.SQLName, Symbol: f.Name, Arg: arg}) + } + sort.Slice(out, func(i, j int) bool { + if out[i].SQLName != out[j].SQLName { + return out[i].SQLName < out[j].SQLName + } + return out[i].Symbol < out[j].Symbol + }) + return out +} + +// argConversions turn the one float64 a continuous query carries into the C +// value a MEOS function's further parameter takes. A parameter type absent from +// this map cannot be filled from a query, so the operation carrying it gets no +// case and reaches the caller as one the dispatch does not serve. +var argConversions = map[string]string{ + "": "", + "double": "C.double(arg)", + "int": "C.int(arg)", + "bool": "arg != 0", +} + +// renderDispatch writes the cgo call for each operation, which is what keeps +// the C symbol behind an operation out of hand-written code: the symbol comes +// from the catalog, and a MEOS rename becomes a compile error here rather than +// a wrong call. It answers the source and how many operations it serves. +func renderDispatch(ops []mathOperation) ([]byte, int, error) { + var b bytes.Buffer + b.WriteString(`//go:build meos + +// Code generated by tools/codegen from the MEOS-API catalog. DO NOT EDIT. +// +// Regenerate with: +// +// go run ./tools/codegen -catalog -o-meos mathops_meos_gen.go +// +// One case per operation the catalog states over a temporal number, calling the +// MEOS symbol the catalog names. An operation whose further parameter cannot be +// filled from the single value a query carries gets no case. + +package main + +/* +#cgo pkg-config: meos +#include +*/ +import "C" + +// callMathOp applies the operation named by its SQL name to a temporal number, +// filling any further parameter from arg. It answers false when the operation +// is not one this dispatch serves, which the caller reports as an unknown +// operation rather than as a failed call. +func callMathOp(sqlName string, temp *C.Temporal, arg float64) (*C.Temporal, bool) { + switch sqlName { +`) + called := 0 + for _, op := range ops { + conv, ok := argConversions[op.Arg] + if !ok { + continue + } + called++ + fmt.Fprintf(&b, "\tcase %q:\n", op.SQLName) + if conv == "" { + fmt.Fprintf(&b, "\t\treturn C.%s(temp), true\n", op.Symbol) + } else { + fmt.Fprintf(&b, "\t\treturn C.%s(temp, %s), true\n", op.Symbol, conv) + } + } + b.WriteString(` } + return nil, false +} +`) + src, err := format.Source(b.Bytes()) + return src, called, err +} + +func render(types map[string]temporalType, ops []mathOperation) ([]byte, error) { names := make([]string, 0, len(types)) for name := range types { names = append(names, name) @@ -133,6 +310,35 @@ var temporalTypeByName = func() map[string]*TemporalType { } return m }() + +// MathOp is one MEOS function that takes a temporal number and answers one. +// SQLName is the name the operation carries on every engine, Symbol the MEOS C +// symbol behind it, and Arg the C type of its one further parameter, empty when +// it takes none. +type MathOp struct { + SQLName string + Symbol string + Arg string +} + +// mathOps is every such function the catalog states, ordered by SQL name. +var mathOps = []MathOp{ +`) + for _, op := range ops { + fmt.Fprintf(&b, "\t{SQLName: %q, Symbol: %q, Arg: %q},\n", op.SQLName, op.Symbol, op.Arg) + } + b.WriteString(`} + +// mathOpBySQLName indexes mathOps by the SQL name. A name several functions +// carry keeps them all, so a caller resolves it rather than reading whichever +// entry a map happened to hold. +var mathOpBySQLName = func() map[string][]*MathOp { + m := map[string][]*MathOp{} + for i := range mathOps { + m[mathOps[i].SQLName] = append(m[mathOps[i].SQLName], &mathOps[i]) + } + return m +}() `) return format.Source(b.Bytes()) }