Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <meos-idl.json> -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 <meos-idl.json>"
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,
Expand Down Expand Up @@ -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

Expand Down
26 changes: 22 additions & 4 deletions GENERATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <meos-idl.json> -o catalog_gen.go
go run ./tools/codegen -catalog <meos-idl.json> \
-o catalog_gen.go -o-meos mathops_meos_gen.go
```

## Where the facts come from
Expand Down Expand Up @@ -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:

Expand Down
47 changes: 47 additions & 0 deletions catalog_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 58 additions & 0 deletions catalog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
67 changes: 67 additions & 0 deletions mathops_meos_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 34 additions & 18 deletions stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
39 changes: 6 additions & 33 deletions stream_engine_meos.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
14 changes: 14 additions & 0 deletions stream_meos_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading