diff --git a/codegen/src/test/java/JvmPresenceFlagTest.java b/codegen/src/test/java/JvmPresenceFlagTest.java new file mode 100644 index 000000000..7adb8f198 --- /dev/null +++ b/codegen/src/test/java/JvmPresenceFlagTest.java @@ -0,0 +1,107 @@ +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.*; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * An argument declared DEFAULT NULL that feeds a C value and a flag stating whether it is + * given, on the Flink and Spark surfaces of tools/codegen_jvm.py (#SqlModel.signatures there): + * the call stating the argument passes it to the value and the catalog's presentBinds to the + * flag, as reclass(rast, nband, reclassexpr, pixeltype, nodataval) passes its nodata value and + * has_nodata true, and the call leaving it out passes the catalog's nullDefaultBinds to both. + * The generator runs over a catalog of one function, as JvmShorterCallOrderTest runs it, so no + * jar and no libmeos. + */ +class JvmPresenceFlagTest { + + private static final Path TOOLS = Path.of("..", "tools").toAbsolutePath().normalize(); + + /** given(a int, has_v bool, v double) in C, read by the SQL signature + * given(a integer, v float DEFAULT NULL), whose wrapper passes has_v = ! PG_ARGISNULL(1) + * and v = has_v ? PG_GETARG_FLOAT8(1) : 0.0. */ + private static final String CATALOG = "{\"functions\": [" + + "{\"name\": \"given\", \"file\": \"meos.h\", \"api\": \"public\", \"sqlfn\": \"given\"," + + " \"returnType\": {\"c\": \"int64_t\", \"canonical\": \"int64_t\"}," + + " \"params\": [{\"name\": \"a\", \"cType\": \"int\", \"canonical\": \"int\"}," + + " {\"name\": \"has_v\", \"cType\": \"bool\", \"canonical\": \"bool\"}," + + " {\"name\": \"v\", \"cType\": \"double\", \"canonical\": \"double\"}]," + + " \"sqlSignatures\": [{\"args\": [\"integer\", \"float\"], \"ret\": \"bigint\"," + + " \"argDefaults\": [null, \"NULL\"]," + + " \"nullDefaultBinds\": {\"1\": {\"has_v\": \"false\", \"v\": \"0.0\"}}," + + " \"presentBinds\": {\"1\": {\"has_v\": \"true\"}}}]}]," + + " \"typeEncodings\": {}, \"structs\": []," + + " \"objectModel\": {\"classes\": {}}, \"enums\": [], \"macros\": []}"; + + private static final String JAR = "{" + + "\"given\": {\"ret\": \"long\", \"arg_types\": [\"int\", \"boolean\", \"double\"]}}"; + + /** Prints {engine: [{args, call}]}, one entry per overload of given the engine registers. */ + private static final String DRIVER = String.join("\n", + "import json, sys", + "sys.path.insert(0, sys.argv[1])", + "import codegen_jvm as g", + "from collections import defaultdict", + "cat = json.load(open(sys.argv[2]))", + "jar = json.load(open(sys.argv[3]))", + "out = {}", + "for engine, m in (('flink', g.SqlModel(cat, jar)),", + " ('spark', g.SqlModel(cat, jar, g.SPARK_SQL_PKG, 'spark'))):", + " rows = []", + " for name, sargs, sdef, ov, why, isset, arity in g._sql_overloads(", + " m, cat, jar, m.layout, defaultdict(int), lambda f: True):", + " rows.append({'args': sargs, 'why': why,", + " 'call': [] if ov is None else [l for l in ov[3] if 'given(' in l]})", + " out[engine] = rows", + "print(json.dumps(out))", + ""); + + private static JsonNode results; + + @BeforeAll + static void generate() throws Exception { + Path dir = Files.createTempDirectory("presence-flag"); + try { + Files.writeString(dir.resolve("catalog.json"), CATALOG); + Files.writeString(dir.resolve("jar.json"), JAR); + Files.writeString(dir.resolve("driver.py"), DRIVER); + Process p = new ProcessBuilder("python3", "-B", dir.resolve("driver.py").toString(), + TOOLS.toString(), dir.resolve("catalog.json").toString(), + dir.resolve("jar.json").toString()).redirectErrorStream(true).start(); + String out = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + assertTrue(p.waitFor(120, TimeUnit.SECONDS), "the generator ran"); + assertEquals(0, p.exitValue(), out); + results = new ObjectMapper().readTree(out); + } finally { + try (var stream = Files.walk(dir)) { + stream.sorted(java.util.Comparator.reverseOrder()).forEach(q -> q.toFile().delete()); + } + } + } + + @Test + void theCallStatingTheArgumentPassesItsValueAndTheFlagTheCatalogStates() { + for (String engine : new String[] {"flink", "spark"}) { + JsonNode full = results.get(engine).get(0); + assertEquals("[\"integer\",\"float\"]", full.get("args").toString(), engine); + assertTrue(full.get("call").toString().contains("GeneratedFunctions.given(a0, true, a1)"), + engine + " " + full); + } + } + + @Test + void theCallLeavingTheArgumentOutPassesWhatTheWrapperPassesForTheValueAndTheFlag() { + for (String engine : new String[] {"flink", "spark"}) { + assertEquals(2, results.get(engine).size(), engine + " " + results.get(engine)); + JsonNode shorter = results.get(engine).get(1); + assertEquals("[\"integer\"]", shorter.get("args").toString(), engine); + assertTrue(shorter.get("call").toString().contains("GeneratedFunctions.given(a0, false, 0.0)"), + engine + " " + shorter); + } + } +} diff --git a/tools/codegen_jvm.py b/tools/codegen_jvm.py index 7788eb965..90bbf91cf 100644 --- a/tools/codegen_jvm.py +++ b/tools/codegen_jvm.py @@ -741,14 +741,20 @@ def signatures(self, f): for these engines wins over the bare one, as #_catalog_aggregates takes the catalog's own `Agg` name of an aggregate. A type carries no modifier: CREATE FUNCTION discards it, so `startValue(tgeompoint)`, declared to return geometry(Point), returns a - geometry (#_deployed_type).""" + geometry (#_deployed_type). The parameter stating that an argument is given takes + the value the catalog's presentBinds states for it, `has_nodata` true beside the + nodata value `reclass` passes, so the argument pairs with its value alone; the call + leaving the argument out binds both as #_null_default_signatures reads them.""" fbound = (f.get('shape') or {}).get('boundArgs') or {} for s in f.get('sqlSignatures') or []: ret = s.get('ret') + present = {p: v for binds in (s.get('presentBinds') or {}).values() + for p, v in binds.items()} yield (s.get('altSqlName') or s.get('sqlName') or f['sqlfn'], [_deployed_type(a) for a in s.get('args') or []], _deployed_type(ret) if ret else ret, - s.get('argDefaults') or [], {**fbound, **(s.get('boundArgs') or {})}) + s.get('argDefaults') or [], + {**fbound, **(s.get('boundArgs') or {}), **present}) def _align(self): """The C base type each SQL type stands for, read off the signatures that