OIiveirra commented on code in PR #69032: URL: https://github.com/apache/doris/pull/69032#discussion_r4440241453
########## regression-test/suites/external_table_p0/iceberg/test_iceberg_spatial_v3.groovy: ########## @@ -0,0 +1,1945 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import com.amazonaws.auth.AWSStaticCredentialsProvider +import com.amazonaws.auth.BasicAWSCredentials +import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration +import com.amazonaws.services.s3.AmazonS3ClientBuilder +import com.amazonaws.services.s3.model.ListObjectsV2Request + +suite("test_iceberg_spatial_v3", "p0,external,iceberg,external_docker,external_docker_iceberg,nonConcurrent") { + String enabled = context.config.otherConfigs.get("enableIcebergTest") + if (enabled == null || !enabled.equalsIgnoreCase("true")) { + logger.info("Iceberg test is disabled") + return + } + + String catalogName = "test_iceberg_spatial_v3" + String dbName = "iceberg_spatial_v3_db" + String restPort = context.config.otherConfigs.get("iceberg_rest_uri_port") + String minioPort = context.config.otherConfigs.get("iceberg_minio_port") + String externalEnvIp = context.config.otherConfigs.get("externalEnvIp") + + def executeCommand = { String command, int timeoutSeconds = 300 -> + StringBuilder stdout = new StringBuilder() + StringBuilder stderr = new StringBuilder() + def process = new ProcessBuilder("/bin/bash", "-c", command).start() + process.consumeProcessOutput(stdout, stderr) + process.waitForOrKill(timeoutSeconds * 1000) + assertEquals(0, process.exitValue(), + "Command failed\nstdout:\n${stdout}\nstderr:\n${stderr}") + return stdout.toString() + } + + String dockerCommand = context.config.otherConfigs.get("externalDockerCommand") ?: "docker" + String sparkContainer = context.config.otherConfigs.get("icebergSparkContainer") + if (sparkContainer == null || sparkContainer.isEmpty()) { + String containers = executeCommand( + "${dockerCommand} ps --format '{{.ID}}\t{{.Names}}'", 30) + def matches = [] + containers.readLines().each { String line -> + String containerId = line.split(/\t/, 2)[0] + String probe = "${dockerCommand} exec ${containerId} bash -lc " + + "'test -f /mnt/SUCCESS && command -v spark-sql >/dev/null'" + try { + executeCommand(probe, 30) + matches.add(containerId) + } catch (Throwable ignored) { + // Only the Spark service has the Iceberg API used to create this V3 schema. + } + } + assertEquals(1, matches.size(), "Expected exactly one usable Spark Iceberg container") + sparkContainer = matches[0] + } + + String javaSource = ''' +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.UUID; +import java.util.Map; +import org.apache.iceberg.CatalogUtil; +import org.apache.iceberg.DataFiles; +import org.apache.iceberg.FileFormat; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.Schema; +import org.apache.iceberg.Table; +import org.apache.iceberg.io.OutputFile; +import org.apache.iceberg.catalog.Catalog; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.SupportsNamespaces; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.types.Types; +import org.apache.hadoop.fs.Path; +import org.apache.parquet.example.data.Group; +import org.apache.parquet.example.data.simple.SimpleGroupFactory; +import org.apache.parquet.hadoop.ParquetWriter; +import org.apache.parquet.hadoop.example.ExampleParquetWriter; +import org.apache.parquet.io.api.Binary; +import org.apache.parquet.schema.MessageType; +import org.apache.parquet.schema.MessageTypeParser; + +public class CreateIcebergSpatialV3Table { + private static void appendParquetRow(Table table, MessageType message, Group row) throws Exception { + String localFile = "/tmp/spatial-fixture-" + UUID.randomUUID() + ".parquet"; + try (ParquetWriter<Group> writer = ExampleParquetWriter.builder(new Path(localFile)).withType(message).build()) { + writer.write(row); + } + OutputFile output = table.io().newOutputFile(table.location() + + "/data/spatial-fixture-" + UUID.randomUUID() + ".parquet"); + long size = Files.size(Paths.get(localFile)); + try (OutputStream stream = output.create()) { + Files.copy(Paths.get(localFile), stream); + } + Files.delete(Paths.get(localFile)); + table.newAppend().appendFile(DataFiles.builder(table.spec()) + .withPath(output.location()).withFormat(FileFormat.PARQUET) + .withFileSizeInBytes(size).withRecordCount(1).build()).commit(); + } + + private static String parquetField(Types.NestedField field) { + String head = (field.isRequired() ? "required " : "optional "); + String tail = " = " + field.fieldId(); + switch (field.type().typeId()) { + case INTEGER: return head + "int32 " + field.name() + tail + "; "; + case STRING: return head + "binary " + field.name() + " (UTF8)" + tail + "; "; + case GEOMETRY: + case GEOGRAPHY: return head + "binary " + field.name() + tail + "; "; + case LIST: return head + "group " + field.name() + " (LIST)" + tail + + " { repeated group list { " + parquetField(field.type().asListType().fields().get(0)) + "} } "; + case MAP: return head + "group " + field.name() + " (MAP)" + tail + + " { repeated group key_value { " + parquetField(field.type().asMapType().fields().get(0)) + + parquetField(field.type().asMapType().fields().get(1)) + "} } "; + case STRUCT: + StringBuilder structure = new StringBuilder(head + "group " + field.name() + tail + " { "); + for (Types.NestedField child : field.type().asStructType().fields()) { + structure.append(parquetField(child)); + } + return structure.append("} ").toString(); + default: throw new IllegalArgumentException("Unexpected fixture type: " + field.type()); + } + } + + private static Binary binary(String hex) { + byte[] bytes = new byte[hex.length() / 2]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16); + } + return Binary.fromConstantByteArray(bytes); + } + + private static void createNestedSource(Catalog catalog, Namespace namespace, + Map<String, String> properties) throws Exception { + java.util.List<Types.NestedField> fields = new java.util.ArrayList<>(); + fields.add(Types.NestedField.required(1, "id", Types.IntegerType.get())); + int nextId = 2; + for (boolean geography : new boolean[] {false, true}) { + String prefix = geography ? "gg_" : "g_"; + org.apache.iceberg.types.Type geo = geography ? Types.GeographyType.crs84() : Types.GeometryType.crs84(); + fields.add(Types.NestedField.optional(nextId++, prefix + "array", + Types.ListType.ofOptional(nextId++, geo))); + fields.add(Types.NestedField.optional(nextId++, prefix + "map", + Types.MapType.ofOptional(nextId++, nextId++, Types.StringType.get(), geo))); + fields.add(Types.NestedField.optional(nextId++, prefix + "struct", Types.StructType.of( + Types.NestedField.optional(nextId++, "shape", geo), + Types.NestedField.optional(nextId++, "label", Types.StringType.get())))); + fields.add(Types.NestedField.optional(nextId++, prefix + "deep", + Types.MapType.ofOptional(nextId++, nextId++, Types.StringType.get(), + Types.ListType.ofOptional(nextId++, Types.StructType.of( + Types.NestedField.optional(nextId++, "shape", geo)))))); + } + TableIdentifier name = TableIdentifier.of(namespace, "spatial_nested_source"); + if (catalog.tableExists(name)) { + catalog.dropTable(name); + } + Table table = catalog.createTable(name, new Schema(fields), PartitionSpec.unpartitioned(), properties); + StringBuilder schema = new StringBuilder("message nested_spatial { "); + for (Types.NestedField field : table.schema().columns()) { + schema.append(parquetField(field)); + } + MessageType message = MessageTypeParser.parseMessageType(schema.append("}").toString()); + String[] payloads = {"0101000000000000000000F03F0000000000000040", + "00000000013FF00000000000004000000000000000", + "01b90b0000000000000000f03f000000000000004000000000000008400000000000001040", + "010200000000000000"}; + for (int id = 1; id <= 6; id++) { + Group row = new SimpleGroupFactory(message).newGroup().append("id", id); + for (String prefix : new String[] {"g_", "gg_"}) { + if (id == 5) { + continue; // SQL NULL parents. + } + Group array = row.addGroup(prefix + "array"); + Group map = row.addGroup(prefix + "map"); + Group structure = row.addGroup(prefix + "struct").append("label", "row" + id); + Group deep = row.addGroup(prefix + "deep"); + if (id == 6) { + continue; // Empty containers and a present struct with a NULL spatial field. + } + Binary point = binary(payloads[id - 1]); + array.addGroup("list").append("element", point); + array.addGroup("list"); + map.addGroup("key_value").append("key", "shape").append("value", point); + map.addGroup("key_value").append("key", "absent"); + structure.append("shape", point); + Group list = deep.addGroup("key_value").append("key", "shapes").addGroup("value"); + list.addGroup("list").addGroup("element").append("shape", point); + list.addGroup("list").addGroup("element"); + list.addGroup("list"); + } + appendParquetRow(table, message, row); + } + } + + private static void createOrcWriteTargets(Catalog catalog, Namespace namespace, + Map<String, String> properties) { + Map<String, String> orcProperties = new HashMap<>(properties); + orcProperties.put("write.format.default", "orc"); + orcProperties.put("write-format", "orc"); + for (boolean geography : new boolean[] {false, true}) { + for (boolean nested : new boolean[] {false, true}) { + TableIdentifier name = TableIdentifier.of(namespace, "spatial_existing_orc_" + + (geography ? "geog" : "geom") + (nested ? "_nested" : "")); + if (catalog.tableExists(name)) { + catalog.dropTable(name); + } + org.apache.iceberg.types.Type type = geography + ? Types.GeographyType.crs84() : Types.GeometryType.crs84(); + catalog.createTable(name, new Schema( + Types.NestedField.required(1, "id", Types.IntegerType.get()), + Types.NestedField.optional(2, "shape", nested + ? Types.ListType.ofOptional(3, type) : type)), + PartitionSpec.unpartitioned(), orcProperties); + } + } + } + + private static void createEmbeddedNullCrsSources(Catalog catalog, Namespace namespace, + Map<String, String> properties) throws Exception { + for (boolean geography : new boolean[] {false, true}) { + String suffix = geography ? "geog" : "geom"; + // Geography's ')' survives the SDK parser. A C-string truncation would turn + // its serialized prefix into the valid, different geography(custom) type. + String crs = (geography ? "custom)" : "custom:") + (char) 0 + "zone"; + org.apache.iceberg.types.Type type = geography + ? Types.GeographyType.of(crs, org.apache.iceberg.types.EdgeAlgorithm.SPHERICAL) + : Types.GeometryType.of(crs); + Schema schema = new Schema(Types.NestedField.required(1, "id", Types.IntegerType.get()), + Types.NestedField.optional(2, "shape", type)); + for (boolean copy : new boolean[] {false, true}) { + TableIdentifier name = TableIdentifier.of(namespace, + "spatial_nul_crs_" + suffix + (copy ? "_copy" : "")); + if (catalog.tableExists(name)) { + catalog.dropTable(name); + } + Table table = catalog.createTable(name, schema, PartitionSpec.unpartitioned(), properties); + if (!copy) { + MessageType message = MessageTypeParser.parseMessageType( + "message spatial_nul_crs { required int32 id = 1; optional binary shape = 2; }"); + appendParquetRow(table, message, new SimpleGroupFactory(message).newGroup().append("id", 1) + .append("shape", binary("0101000000000000000000F03F0000000000000040"))); + } + } + } + } + + public static void main(String[] args) throws Exception { + Map<String, String> properties = new HashMap<>(); + properties.put("type", "rest"); + properties.put("uri", "http://rest:8181"); + properties.put("warehouse", "s3://warehouse/wh/"); + properties.put("io-impl", "org.apache.iceberg.aws.s3.S3FileIO"); + properties.put("s3.endpoint", "http://minio:9000"); + properties.put("s3.path-style-access", "true"); + properties.put("s3.region", "us-east-1"); + Catalog catalog = CatalogUtil.buildIcebergCatalog("demo", properties, null); + SupportsNamespaces namespaceCatalog = (SupportsNamespaces) catalog; + Namespace namespace = Namespace.of(args[0]); + if (!namespaceCatalog.namespaceExists(namespace)) { + namespaceCatalog.createNamespace(namespace); + } + TableIdentifier identifier = TableIdentifier.of(namespace, args[1]); + if (catalog.tableExists(identifier)) { + catalog.dropTable(identifier); + } + Schema schema = new Schema( + Types.NestedField.required(1, "id", Types.IntegerType.get()), + Types.NestedField.optional(2, "geom", Types.GeometryType.crs84()), + Types.NestedField.optional(3, "geog", Types.GeographyType.crs84())); + Map<String, String> tableProperties = new HashMap<>(); + tableProperties.put("format-version", "3"); + tableProperties.put("write.format.default", "parquet"); + catalog.createTable(identifier, schema, PartitionSpec.unpartitioned(), tableProperties); + TableIdentifier nested = TableIdentifier.of(namespace, "nested_spatial_values"); + if (catalog.tableExists(nested)) { + catalog.dropTable(nested); + } + Table nestedTable = catalog.createTable(nested, new Schema( + Types.NestedField.optional(1, "shapes", + Types.ListType.ofOptional(2, Types.GeometryType.crs84()))), + PartitionSpec.unpartitioned(), tableProperties); + nestedTable.updateProperties().set("write.merge.mode", "merge-on-read").commit(); + TableIdentifier projected = TableIdentifier.of(namespace, "projected_values"); + if (catalog.tableExists(projected)) { + catalog.dropTable(projected); + } + Table projectedTable = catalog.createTable(projected, new Schema( + Types.NestedField.required(1, "id", Types.IntegerType.get()), + Types.NestedField.optional(2, "geom", Types.GeometryType.of("EPSG:3857")), + Types.NestedField.optional(3, "geog", Types.GeographyType.of("OGC:CRS84", + org.apache.iceberg.types.EdgeAlgorithm.VINCENTY))), + PartitionSpec.unpartitioned(), tableProperties); + // A table property resolves the Iceberg CRS reference; it is unavailable to the BE file writer. + Map<String, String> referencedProperties = new HashMap<>(tableProperties); + referencedProperties.put("my_crs", "{\\"type\\":\\"GeographicCRS\\",\\"name\\":\\"WGS 84\\",\\"datum\\":{\\"type\\":\\"GeodeticReferenceFrame\\",\\"name\\":\\"World Geodetic System 1984\\",\\"ellipsoid\\":{\\"name\\":\\"WGS 84\\",\\"semi_major_axis\\":6378137,\\"inverse_flattening\\":298.257223563}},\\"coordinate_system\\":{\\"subtype\\":\\"ellipsoidal\\",\\"axis\\":[{\\"name\\":\\"Geodetic longitude\\",\\"abbreviation\\":\\"Lon\\",\\"direction\\":\\"east\\",\\"unit\\":\\"degree\\"},{\\"name\\":\\"Geodetic latitude\\",\\"abbreviation\\":\\"Lat\\",\\"direction\\":\\"north\\",\\"unit\\":\\"degree\\"}]}}"); + TableIdentifier referenced = TableIdentifier.of(namespace, "referenced_crs_values"); + if (catalog.tableExists(referenced)) { + catalog.dropTable(referenced); + } + Table referencedTable = catalog.createTable(referenced, new Schema( + Types.NestedField.required(1, "id", Types.IntegerType.get()), + Types.NestedField.optional(2, "geom", Types.GeometryType.of("projjson:my_crs")), + Types.NestedField.optional(3, "geog", Types.GeographyType.of("projjson:my_crs", + org.apache.iceberg.types.EdgeAlgorithm.SPHERICAL))), + PartitionSpec.unpartitioned(), referencedProperties); + for (boolean isGeography : new boolean[] {false, true}) { + String column = isGeography ? "geog" : "geom"; + Schema copySchema = new Schema( + Types.NestedField.required(1, "id", Types.IntegerType.get()), + Types.NestedField.optional(2, column, isGeography + ? Types.GeographyType.of("projjson:my_crs", + org.apache.iceberg.types.EdgeAlgorithm.SPHERICAL) + : Types.GeometryType.of("projjson:my_crs"))); + for (boolean partitioned : new boolean[] {false, true}) { + String name = (isGeography ? "referenced_geography_copy" : "referenced_geometry_copy") + + (partitioned ? "_partitioned" : ""); + TableIdentifier copy = TableIdentifier.of(namespace, name); + if (catalog.tableExists(copy)) { + catalog.dropTable(copy); + } + Table copyTable = catalog.createTable(copy, copySchema, partitioned + ? PartitionSpec.builderFor(copySchema).identity("id").build() + : PartitionSpec.unpartitioned(), referencedProperties); + System.out.println("SPATIAL_DATA_LOCATION\\t" + name + "\\t" + + copyTable.properties().getOrDefault("write.data.path", copyTable.location() + "/data")); + } + } + // Iceberg 1.10 exposes spatial schema types but its generic Parquet writer does not implement them. + // Store standard WKB BYTE_ARRAY values with field ids; the Iceberg schema carries CRS/algorithm. + String parquetSchema = "message spatial { required int32 id = " + + projectedTable.schema().findField("id").fieldId() + "; optional binary geom = " + + projectedTable.schema().findField("geom").fieldId() + "; optional binary geog = " + + projectedTable.schema().findField("geog").fieldId() + "; }"; + MessageType message = MessageTypeParser.parseMessageType(parquetSchema); + Group row = new SimpleGroupFactory(message).newGroup().append("id", 1); + ByteBuffer point = ByteBuffer.allocate(21).order(ByteOrder.LITTLE_ENDIAN); + point.put((byte) 1).putInt(1).putDouble(1000).putDouble(2000).flip(); + row.append("geom", Binary.fromConstantByteBuffer(point)); + ByteBuffer geography = ByteBuffer.allocate(21).order(ByteOrder.LITTLE_ENDIAN); + geography.put((byte) 1).putInt(1).putDouble(1).putDouble(2).flip(); + row.append("geog", Binary.fromConstantByteBuffer(geography)); + appendParquetRow(projectedTable, message, row); + Group referencedRow = new SimpleGroupFactory(message).newGroup().append("id", 1); + referencedRow.append("geom", row.getBinary("geog", 0)); + referencedRow.append("geog", row.getBinary("geog", 0)); + appendParquetRow(referencedTable, message, referencedRow); + MessageType nestedMessage = MessageTypeParser.parseMessageType( + "message spatial_nested { optional group shapes (LIST) = 1 { " + + "repeated group list { optional binary element = 2; } } }"); + Group nestedRow = new SimpleGroupFactory(nestedMessage).newGroup(); + nestedRow.addGroup("shapes").addGroup("list").append("element", row.getBinary("geog", 0)); + appendParquetRow(nestedTable, nestedMessage, nestedRow); + createNestedSource(catalog, namespace, tableProperties); + createOrcWriteTargets(catalog, namespace, tableProperties); + createEmbeddedNullCrsSources(catalog, namespace, tableProperties); + } +} +''' + String encodedJavaSource = javaSource.getBytes("UTF-8").encodeBase64().toString() + String fixtureOutput = executeCommand("${dockerCommand} exec ${sparkContainer} bash -lc '" + + "task_java_dir=\$(mktemp -d /tmp/iceberg-spatial-v3.XXXXXX) && " + + "echo ${encodedJavaSource} | base64 -d >\"\$task_java_dir/CreateIcebergSpatialV3Table.java\" && " + + "javac -cp \"/opt/spark/jars/*\" \"\$task_java_dir/CreateIcebergSpatialV3Table.java\" && " + + "java -cp \"\$task_java_dir:/opt/spark/jars/*\" " + + "CreateIcebergSpatialV3Table ${dbName} geometry_values'") + + def dataLocations = [:] + fixtureOutput.readLines().findAll { it.startsWith("SPATIAL_DATA_LOCATION\t") }.each { line -> + def fields = line.split("\t", 3) + dataLocations[fields[1]] = fields[2] + } + def minioClient = AmazonS3ClientBuilder.standard() + .withEndpointConfiguration(new EndpointConfiguration("http://${externalEnvIp}:${minioPort}", "us-east-1")) + .withPathStyleAccessEnabled(true) + .withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials("admin", "password"))) + .build() + def listDataObjects = { String table -> + String location = dataLocations[table] + assertNotNull(location, "Missing data location for ${table}") + def uri = new URI(location) + def objects = new TreeMap<String, Long>() + String token = null + while (true) { + def listing = minioClient.listObjectsV2(new ListObjectsV2Request() + .withBucketName(uri.host).withPrefix(uri.path.substring(1).replaceAll('/+$', '') + '/') + .withContinuationToken(token)) + listing.objectSummaries.each { objects[it.key] = it.size } + if (!listing.isTruncated()) { + break + } + token = listing.nextContinuationToken + } + return objects + } + + def createCatalog = { + sql """drop catalog if exists ${catalogName}""" + sql """ + create catalog ${catalogName} properties ( + 'type' = 'iceberg', + 'iceberg.catalog.type' = 'rest', + 'uri' = 'http://${externalEnvIp}:${restPort}', + 's3.access_key' = 'admin', + 's3.secret_key' = 'password', + 's3.endpoint' = 'http://${externalEnvIp}:${minioPort}', + 's3.region' = 'us-east-1' + ) + """ + sql """switch ${catalogName}""" + sql """use ${dbName}""" + } + createCatalog() + test { + sql """create table spatial_invalid_geometry_crs (geom geometry('')) + engine=iceberg properties ('format-version' = '3')""" + exception "Spatial CRS must not be empty" + } + test { + sql """create table spatial_invalid_geography_crs (geog geography('', 'spherical')) + engine=iceberg properties ('format-version' = '3')""" + exception "Spatial CRS must not be empty" + } + test { + sql """create table spatial_invalid_algorithm (geog geography('OGC:CRS84', 'unknown')) + engine=iceberg properties ('format-version' = '3')""" + exception "Unsupported Geography algorithm" + } + sql """ + insert into geometry_values values + (1, ST_GeometryFromWKBTyped('0101000000000000000000F03F0000000000000040'), + ST_GeogFromWKB('0101000000000000000000F03F0000000000000040')), + (2, NULL, NULL), + (3, ST_GeometryFromWKBTyped('010100000000000000000008400000000000001040'), NULL), + (4, NULL, ST_GeogFromWKB('010100000000000000000008400000000000001040')) + """ + // Doris writes OPTIONAL GEOMETRY/GEOGRAPHY annotations; count each leaf separately + // to exercise definition-level COUNT pushdown, including SQL NULL rows. + for (boolean scanV2 : [false, true]) { + for (boolean pushdown : [false, true]) { + "qt_optional_geometry_count_${scanV2}_${pushdown}"("""select /*+ SET_VAR( + enable_file_scanner_v2=${scanV2}, enable_push_down_no_group_agg=${pushdown}) */ + count(geom) from geometry_values""") + "qt_optional_geography_count_${scanV2}_${pushdown}"("""select /*+ SET_VAR( + enable_file_scanner_v2=${scanV2}, enable_push_down_no_group_agg=${pushdown}) */ + count(geog) from geometry_values""") + } + } + qt_spatial_schema """desc geometry_values""" + // These factories do not implement scalar spatial values. Fail in FE, including + // analytic DISTINCT, which is rewritten to multi_distinct_count after binding. + for (String column : ["geom", "geog"]) { + for (String query : [ + "select ${column}, count(*) from geometry_values group by ${column}", + "select id, ${column}, count(*) from geometry_values group by id, ${column}", + "select ${column}, count(*) from geometry_values group by grouping sets ((${column}), ())", + "select distinct ${column} from geometry_values", + "select distinct id, ${column} from geometry_values"]) { + test { + sql query + exception "GROUP BY and DISTINCT do not support spatial keys" + } + } + for (String expression : ["min(${column})", "max(${column})", + "min(${column}) over ()", "max(${column}) over ()", + "count(distinct ${column})", "count(distinct id, ${column})"]) { + test { + sql "select ${expression} from geometry_values" + exception "does not support spatial arguments" + } + } + // Compare only implemented types; MAP_CONTAINS_KEY must not inspect spatial values. + for (String expression : [ + "map_contains_value(map(id, ${column}), ${column})", + "map_contains_value(map(id, ${column}), NULL)", + "map_contains_value(map(id, id), ${column})", + "map_contains_value(map(id, array(${column})), array(${column}))", + "map_contains_key(map(${column}, id), ${column})", + "map_contains_key(map(${column}, id), NULL)", + "map_contains_key(map(id, ${column}), ${column})", + "map_contains_entry(map(id, ${column}), id, ${column})", + "map_contains_entry(map(id, ${column}), id, NULL)", + "map_contains_entry(map(id, id), id, ${column})", + "map_contains_entry(map(id, id), ${column}, id)", + "map_contains_entry(map(${column}, id), NULL, id)", + "map_contains_entry(map(id, array(${column})), id, array(${column}))"]) { + test { + sql """select ${expression} from geometry_values""" + exception "does not support spatial comparison" + } + } + "order_qt_spatial_map_key_lookup_${column}"("""select id, + map_contains_key(map(id, ${column}), id), + map_contains_key(map(id, ${column}), id + 1), + map_contains_key(map(id, ${column}), CAST(NULL AS INT)), + hex(ST_AsBinary(element_at(map(id, ${column}), id))) from geometry_values""") + // Distinct imported slots must fail in FE, before BE's unsupported scalar dispatch. + for (String function : ["greatest", "least"]) { + for (String arguments : ["l.${column}, r.${column}", "NULL, r.${column}", + "l.${column}, NULL", "l.id, r.${column}"]) { + test { + sql """select ${function}(${arguments}) from geometry_values l + cross join geometry_values r""" + exception "${function} does not support spatial arguments" + } + } + } + for (String function : ["min_by", "max_by"]) { + for (String arguments : ["${column}, id", "id, ${column}", + "id, array(${column})", "id, array(array(${column}))", + "id, map(id, ${column})", "id, named_struct('shape', ${column})"]) { + test { + sql """select ${function}(${arguments}) from geometry_values""" + exception "${function} does not support spatial arguments" + } + } + } + for (String arguments : ["${column}, 2", "${column}, 2, 4", "array(${column}), 2"]) { + test { + sql """select topn_array(${arguments}) from geometry_values""" + exception "topn_array does not support spatial arguments" + } + } + for (String expression : ["multi_distinct_count(${column})", "count(distinct ${column}) over ()"]) { + test { + sql """select ${expression} from geometry_values""" + exception "does not support spatial arguments" + } + } + for (String function : ["group_array_union", "group_array_intersect"]) { + test { + sql """select ${function}(array(${column})) from geometry_values""" + exception "${function} does not support spatial arguments" + } + } + for (String suffix : ["", "_state"]) { + for (String arguments : ["${column}, 1.0", "${column}, 1.0, 0.0"]) { + test { + sql """select linear_histogram${suffix}(${arguments}) from geometry_values""" + exception "linear_histogram does not support spatial arguments" + } + } + for (String function : ["map_agg", "map_agg_v2"]) { + test { + sql """select ${function}${suffix}(${column}, id) from geometry_values""" + exception "map_agg does not support spatial keys" + } + } + for (String function : ["count_map", "sum_map", "avg_map", "min_map", "max_map"]) { + test { + sql """select ${function}${suffix}(map(${column}, id)) from geometry_values""" + exception "${function} does not support spatial MAP keys" + } + } + } + // Ordinary keys retain raw nullable WKB values through MAP_AGG and its alias. + "order_qt_spatial_map_agg_values_${column}"("""select id, + hex(ST_AsBinary(element_at(primary_values, id))), hex(ST_AsBinary(element_at(alias_values, id))) + from geometry_values cross join + (select map_agg(id, ${column}) primary_values, map_agg_v2(id, ${column}) alias_values + from geometry_values) aggregates""") + // Repeated ordinary keys exercise value updates and nullable aggregate states. + "qt_spatial_map_combinator_values_${column}"("""select element_at(count_map(map(1, ${column})), 1) + from geometry_values""") + "qt_spatial_map_state_values_${column}"("""select /*+ SET_VAR(enable_agg_state=true) */ + hex(ST_AsBinary(element_at(map_agg_merge(primary_state), 1))), + hex(ST_AsBinary(element_at(map_agg_v2_merge(alias_state), 1))), + element_at(count_map_merge(count_state), 1) + from (select map_agg_state(id, ${column}) primary_state, map_agg_v2_state(id, ${column}) alias_state, + count_map_state(map(1, ${column})) count_state from geometry_values) states""") + } + // A typed NULL has no aggregate producer: validate its state descriptor before BE initialization. + for (String spatial : ["GEOMETRY", "GEOGRAPHY"]) { + for (String function : ["count_map", "sum_map", "avg_map", "min_map", "max_map"]) { + String stateType = "AGG_STATE<${function}(MAP<${spatial}, INT>)>" + String state = "CAST(NULL AS ${stateType})" + for (String query : [ + "select ${state} from numbers('number'='3')", + "select TRY_CAST(NULL AS ${stateType}) from numbers('number'='3')", + "select CONVERT(NULL, ${stateType}) from numbers('number'='3')", + "select s from (select ${state} s from numbers('number'='3')) states", + "select ${function}_merge(s) from (select ${state} s from numbers('number'='3')) states"]) { + test { + sql query.replaceFirst("select", "select /*+ SET_VAR(enable_agg_state=true) */") + exception "${function} does not support spatial MAP keys" + } + } + } + for (String signature : ["min(${spatial})", "max(${spatial})", + "map_agg(${spatial}, INT)", "map_agg_v2(${spatial}, INT)", + "linear_histogram(${spatial}, DOUBLE)", "linear_histogram(${spatial}, DOUBLE, DOUBLE)"]) { + test { + sql """select /*+ SET_VAR(enable_agg_state=true) */ CAST(NULL AS AGG_STATE<${signature}>) + from numbers('number'='3')""" + exception "does not support spatial" + } + } + List<String> supportedStates = ["map_agg(INT, ${spatial})", "map_agg_v2(INT, ${spatial})", + "count_map(MAP<INT, ${spatial}>)", "any_value(${spatial})", + "count(${spatial})", "array_agg(${spatial})", "collect_list(${spatial})", + "collect_list(${spatial}, INT)"] + String projections = supportedStates.collect { "CAST(NULL AS AGG_STATE<${it}>)" }.join(", ") + "order_qt_spatial_declared_state_nulls_${spatial.toLowerCase()}"("""select + /*+ SET_VAR(enable_agg_state=true) */ number, ${projections} + from numbers('number'='3') order by number""") + } + order_qt_ordinary_declared_state_nulls """select /*+ SET_VAR(enable_agg_state=true) */ number, + CAST(NULL AS AGG_STATE<linear_histogram(INT, DOUBLE)>), CAST(NULL AS AGG_STATE<map_agg(INT, INT)>), + CAST(NULL AS AGG_STATE<count_map(MAP<INT, INT>)>), CAST(NULL AS AGG_STATE<sum_map(MAP<INT, INT>)>), + CAST(NULL AS AGG_STATE<avg_map(MAP<INT, INT>)>), CAST(NULL AS AGG_STATE<min_map(MAP<INT, INT>)>), + CAST(NULL AS AGG_STATE<max_map(MAP<INT, INT>)>) from numbers('number'='3') order by number""" + // Aggregate keys must reject spatial leaves through actual scan slots and state entry points. + for (String column : ["g_array", "gg_array", "g_map", "gg_map", "g_struct", "gg_struct"]) { + for (String suffix : ["", "_state"]) { + test { + sql """select /*+ SET_VAR(enable_agg_state=true) */ map_agg_v2${suffix}(${column}, id) + from spatial_nested_source""" + exception "map_agg does not support spatial keys" + } + test { + sql """select /*+ SET_VAR(enable_agg_state=true) */ ndv${suffix}(${column}) + from spatial_nested_source""" + exception "must use with specific function" + } + } + } + for (String spatial : ["GEOMETRY", "GEOGRAPHY"]) { + for (String nested : ["ARRAY<${spatial}>", "MAP<INT, ${spatial}>", "STRUCT<shape:${spatial}>"]) { + test { + sql """select /*+ SET_VAR(enable_agg_state=true) */ + CAST(NULL AS AGG_STATE<map_agg_v2(${nested}, INT)>)""" + exception "map_agg does not support spatial keys" + } + test { + sql """select /*+ SET_VAR(enable_agg_state=true) */ + CAST(NULL AS AGG_STATE<ndv(${nested})>)""" + exception "must use with specific function" + } + } + } + // Imported nested columns must obey the same group-array admission rule. + for (String function : ["group_array_union", "group_array_intersect"]) { + test { + sql """select ${function}(shapes) from nested_spatial_values""" + exception "${function} does not support spatial arguments" + } + } + qt_ordinary_parallel_aggregates """select min_by(id, id), max_by(id, id), + array_sort(topn_array(if(id > 1, 1, 2), 2)), + array_sort(group_array_union(array(id))), + array_sort(group_array_intersect(array(id, 1))), multi_distinct_count(id) + from geometry_values""" + order_qt_ordinary_analytic_distinct """select id, count(distinct id) over () + from geometry_values""" + qt_ordinary_histogram_and_map_aggregates """select linear_histogram(id, 1.0), + linear_histogram(id, 1.0, 0.0), element_at(map_agg(id, id), 1), + element_at(count_map(map(1, id)), 1), element_at(sum_map(map(1, id)), 1), + element_at(avg_map(map(1, id)), 1), element_at(min_map(map(1, id)), 1), + element_at(max_map(map(1, id)), 1) from geometry_values""" + order_qt_ordinary_map_contains """select id, + map_contains_key(map(id, id), id), map_contains_value(map(id, id), id), + map_contains_entry(map(id, id), id, id), map_contains_value(map(id, id), id + 1), + map_contains_entry(map(id, id), id, id + 1) from geometry_values""" + for (String spatialColumn : ["geom", "geog"]) { + "qt_spatial_multikey_${spatialColumn}"("""select id from geometry_values + order by ${spatialColumn} asc nulls last, id""") + "qt_spatial_multikey_topn_${spatialColumn}"("""select id from geometry_values + order by ${spatialColumn} desc nulls first, id limit 3""") + "qt_spatial_multikey_nonnull_${spatialColumn}"("""select id from geometry_values + where ${spatialColumn} is not null order by ${spatialColumn}, id""") + } + order_qt_ordinary_greatest_least """select id, greatest(id, id + 1), least(id, id + 1), + greatest(id, if(id = 1, NULL, id + 1)), least(id, if(id = 1, NULL, id + 1)) + from geometry_values""" + // All pairs below are real nullable Iceberg columns, including line/polygon boundaries. + String meridian = "0102000000020000000000000000002440000000000080464000000000000024400000000000004740" + String overlap = "01020000000200000000000000000024406666666666a6464000000000000024403333333333f34640" + String reverseOverlap = "01020000000200000000000000000024403333333333f3464000000000000024406666666666a64640" + String nearby = "010200000002000000e7db0000000024406666666666a64640e7db0000000024403333333333f34640" + String outside = "0102000000020000001924ffffffff23406666666666a646401924ffffffff23403333333333f34640" + String beyond = "0102000000020000000000000000002440cdcccccccc0c474000000000000024409a99999999194740" + String multiEdge = "010200000003000000000000000000224000000000000046400000000000002440000000000080464000000000000024400000000000004740" + String square = "010300000001000000050000000000000000002440000000000080464000000000000026400000000000804640000000000000264000000000000047400000000000002440000000000000474000000000000024400000000000804640" + String neighbor = "0103000000010000000500000000000000000024406666666666a6464000000000000024403333333333f3464000000000000022403333333333f3464000000000000022406666666666a6464000000000000024406666666666a64640" + String nearbyPolygon = "010300000001000000050000001924ffffffff23406666666666a646401924ffffffff23403333333333f3464000000000000022403333333333f3464000000000000022406666666666a646401924ffffffff23406666666666a64640" + sql """drop table if exists spatial_relation_values""" + sql """create table spatial_relation_values (id int, lhs geography, rhs geography) + engine=iceberg properties ('format-version' = '3')""" + List<String> relationRows = [] + [[meridian, overlap], [meridian, reverseOverlap], [meridian, nearby], [meridian, beyond], + [multiEdge, overlap], [square, overlap], [square, nearby], [square, outside], + [square, neighbor], [square, nearbyPolygon]].eachWithIndex { pair, index -> + relationRows.add("(${index}, ST_GeogFromWKB('${pair[0]}'), ST_GeogFromWKB('${pair[1]}'))") + } + relationRows.add("(10, NULL, ST_GeogFromWKB('${overlap}'))") + relationRows.add("(11, ST_GeogFromWKB('${meridian}'), NULL)") + relationRows.add("(12, NULL, NULL)") + sql """insert into spatial_relation_values values ${relationRows.join(',')}""" + order_qt_spatial_nonpoint_relations """select id, ST_Intersects(lhs, rhs), ST_Intersects(rhs, lhs), + ST_Disjoint(lhs, rhs), ST_Disjoint(rhs, lhs) from spatial_relation_values""" + order_qt_spatial_nonpoint_constant """select id, + ST_Intersects(lhs, ST_GeogFromWKB('${overlap}')), ST_Intersects(ST_GeogFromWKB('${overlap}'), lhs), + ST_Disjoint(lhs, ST_GeogFromWKB('${overlap}')), ST_Disjoint(ST_GeogFromWKB('${overlap}'), lhs) + from spatial_relation_values""" + // Normalized calculation rings choose the smaller region; raw WKB winding is retained. + String smallRing = "0103000000010000000500000000000000000024c000000000000024c0000000000000244000000000000024c00000000000002440000000000000244000000000000024c0000000000000244000000000000024c000000000000024c0" + String reverseRing = "0103000000010000000500000000000000000024c000000000000024c000000000000024c0000000000000244000000000000024400000000000002440000000000000244000000000000024c000000000000024c000000000000024c0" + sql """insert into spatial_relation_values values + (13, ST_GeogFromWKB('${smallRing}'), ST_GeogFromWKB('${reverseRing}'))""" + qt_spatial_polygon_winding """select ST_Contains(lhs, + ST_GeogFromWKB('010100000000000000000000000000000000000000')), + ST_Contains(rhs, ST_GeogFromWKB('010100000000000000000000000000000000000000')), + ST_Contains(lhs, ST_GeogFromWKB('010100000000000000008066400000000000000000')), + ST_Contains(rhs, ST_GeogFromWKB('010100000000000000008066400000000000000000')), + hex(ST_AsBinary(lhs)) = upper('${smallRing}'), hex(ST_AsBinary(rhs)) = upper('${reverseRing}') + from spatial_relation_values where id = 13""" + order_qt_spatial_values """ + select id, ST_AsText(geom), ST_X(geom), ST_Y(geom), ST_GeometryType(geom), + hex(ST_AsBinary(geom)), ST_AsText(geog), ST_Distance(geog, geog) + from geometry_values + """ + order_qt_spatial_literal_null_arguments """ + select id, ST_Distance(geog, NULL), ST_Distance(NULL, geog), + ST_Within(geog, NULL), ST_Azimuth(NULL, geog), + ST_Angle(NULL, geog, geog), ST_Angle(geog, NULL, geog), ST_Angle(geog, geog, NULL) + from geometry_values + """ + order_qt_spatial_additional_functions """ + select id, ST_Azimuth(geog, ST_GeogFromWKB('010100000000000000000008400000000000001040')), + ST_Within(geog, geog), + ST_Angle(geog, ST_GeogFromWKB('010100000000000000000008400000000000001040'), + ST_GeogFromWKB('010100000000000000000014400000000000001840')) + from geometry_values + """ + test { + sql """insert into geometry_values values (5, 'not WKB', NULL)""" + exception "Spatial connector write cannot convert" + } + test { + sql """insert into geometry_values values + (5, NULL, ST_GeometryFromWKBTyped('0101000000000000000000F03F0000000000000040'))""" + exception "Spatial connector write type mismatch" + } + sql """insert into geometry_values select id + 10, geom, geog + from geometry_values where id = 1""" + order_qt_spatial_values_ordinary_grouping """ + select id, ST_AsText(any_value(geom)), ST_AsText(any_value(geog)), count(*) + from geometry_values group by id + """ + // Bare Iceberg spatial slots must not generate unsupported IN/Bloom runtime predicates. + [geom: "ST_GeometryFromWKBTyped", geog: "ST_GeogFromWKB"].each { column, constructor -> + for (int filterType : [1, 2, 8]) { + String joinSql = """select /*+ SET_VAR(enable_runtime_filter_prune=false, + runtime_filter_type=${filterType}) */ count(*) from geometry_values l + join [broadcast] geometry_values r on l.${column} = r.${column}""" + "qt_spatial_scan_join_${column}_${filterType}"(joinSql) + explain { + sql joinSql + notContains "RF" + } + } + for (String operator : ["in", "not in"]) { + test { + sql """select id from geometry_values where ${column} ${operator} ( + ${constructor}('0101000000000000000000F03F0000000000000040'), + ${constructor}('010100000000000000000008400000000000001040'))""" + exception "in predicate does not support spatial arguments" + } + } + } + order_qt_referenced_crs_source """select id, ST_AsText(geom), ST_AsText(geog) + from referenced_crs_values""" + [geom: "referenced_geometry_copy", geog: "referenced_geography_copy"].each { column, target -> + for (String table : [target, target + "_partitioned"]) { + def before = listDataObjects(table) + for (int attempt = 0; attempt < 2; attempt++) { + test { + // Two source partition keys exercise dynamic writer creation, not only the + // unpartitioned writer that the sink registers before opening. + sql """insert into ${table} select s.id + cast(n.number as int), s.${column} + from referenced_crs_values s cross join numbers('number' = '2') n""" + exception "Referenced spatial CRS" + } + assertEquals(before, listDataObjects(table), + "Rejected ${column} write left physical objects in ${table}, attempt ${attempt}") + } + String tag = table == target ? column : table + "qt_referenced_crs_no_commit_${tag}"("""select + (select count(*) from ${table}), (select count(*) from `${table}\$files`)""") + } + } + minioClient.shutdown() + sql """drop table if exists spatial_partitioned_copy""" + sql """create table spatial_partitioned_copy (id int, geom geometry, geog geography) + partition by list(id) () properties ('format-version' = '3')""" + sql """insert into spatial_partitioned_copy select id, geom, geog from geometry_values""" + order_qt_spatial_partitioned_copy """select id, ST_AsText(geom), ST_AsText(geog) + from spatial_partitioned_copy""" + for (String column : ["geom", "geog"]) { + test { + sql """alter table spatial_partitioned_copy add partition key ${column}""" + exception "Unsupported type for identity" + } + test { + sql """alter table spatial_partitioned_copy replace partition key id with ${column}""" + exception "Unsupported type for identity" + } + } + // Rejected changes keep the ordinary identity partition usable with spatial values. + sql """insert into spatial_partitioned_copy select id + 100, geom, geog + from geometry_values where id = 1""" + order_qt_spatial_partition_after_rejection """select id, ST_AsText(geom), ST_AsText(geog) + from spatial_partitioned_copy where id > 100""" + + // FULL collection should still analyze ordinary columns, while excluding every spatial + // metadata identity. Drop prior stats so an old cache cannot make this check pass. + for (String table : ["geometry_values", "projected_values", "referenced_crs_values"]) { + sql """drop stats ${table}""" + sql """analyze table ${table} with full with sync""" + quickRunTest("spatial_analyze_${table}", "show column stats ${table}", true) { row -> row.take(5) } + } + for (String column : ["geom", "geog"]) { + test { + sql """select ndv(${column}) from geometry_values""" + exception "does not support spatial" + } + } + for (String expression : ["case when id = 1 then geom else NULL end", + "if(id = 1, geom, NULL)", "coalesce(geom, NULL)"]) { + test { + sql """insert into geometry_values select id, ${expression}, NULL from projected_values""" + exception "Spatial connector write CRS mismatch" + } + } + test { + sql """explain insert into geometry_values + select id, cast(geom as geometry), NULL from projected_values""" + exception "CRS mismatch" + } + for (def branch : [ + ["case when id = 1 then ST_GeometryFromWKBTyped('0101000000000000000000F03F0000000000000040') else geom end", + "Cannot find common type for case when"], + ["if(id = 1, ST_GeometryFromWKBTyped('0101000000000000000000F03F0000000000000040'), geom)", + "Can not find the compatibility function signature"], + ["coalesce(ST_GeometryFromWKBTyped('0101000000000000000000F03F0000000000000040'), geom)", + "Can not find the compatibility function signature"]]) { + test { + sql """explain insert into geometry_values + select id, ${branch[0]}, NULL from projected_values""" + exception branch[1] + } + } + test { + sql """insert into geometry_values select id, geom, NULL from projected_values""" + exception "Spatial connector write CRS mismatch" + } + test { + sql """insert into geometry_values select id, NULL, geog from projected_values""" + exception "Spatial connector write algorithm mismatch" + } + order_qt_projected_values """ + select id, ST_AsText(geom), ST_X(geom), ST_Y(geom), ST_GeometryType(geom), + hex(ST_AsBinary(geom)), ST_AsText(geog) + from projected_values + """ + test { + sql """select ST_Distance(geog, geog) from projected_values""" + exception "Spatial calculation does not support typed GEOGRAPHY" + } + for (String expression : ["ST_Azimuth(geog, geog)", "ST_Within(geog, geog)", + "ST_Angle(geog, geog, geog)"]) { + test { + sql """select ${expression} from projected_values""" + exception "Spatial calculation does not support typed GEOGRAPHY" + } + } + String spatialSavedSqlMode = sql("select @@sql_mode")[0][0].toString() + try { + for (boolean noBackslashEscapes : [false, true]) { + sql "set sql_mode = '${noBackslashEscapes ? 'NO_BACKSLASH_ESCAPES' : ''}'" + String crsLiteral = noBackslashEscapes ? 'custom\\crs' : 'custom\\\\crs' + sql "drop table if exists spatial_crs_mode" + sql "drop table if exists spatial_crs_mode_copy" + sql """create table spatial_crs_mode ( + id int, geom geometry('${crsLiteral}'), geog geography('${crsLiteral}', 'spherical'), + payload struct<stamp:datetimev2(3), shape:array<geometry('${crsLiteral}')>>) + engine=iceberg properties ('format-version'='3')""" + String recreate = sql("show create table spatial_crs_mode")[0][1].toString() + .replace('`spatial_crs_mode`', '`spatial_crs_mode_copy`') + // Reuse the emitted schema: LOCATION is informational and V3 is stored in Iceberg metadata. + recreate = recreate.substring(0, recreate.indexOf("\nLOCATION ")) + + "\nPROPERTIES ('format-version'='3')" + sql recreate + sql "set sql_mode = ''" + if (noBackslashEscapes) { + order_qt_spatial_crs_no_backslash "desc spatial_crs_mode_copy" + } else { + order_qt_spatial_crs_default_mode "desc spatial_crs_mode_copy" + } + } + } finally { + sql "set sql_mode = '${spatialSavedSqlMode}'" + } + sql """drop table if exists spatial_ddl_values""" + sql """create table spatial_ddl_values (id int, geom geometry) engine=iceberg + properties ('format-version' = '3')""" + sql """alter table spatial_ddl_values add column geog geography""" + sql """insert into spatial_ddl_values select id, geom, geog from geometry_values""" + order_qt_spatial_ddl_values """ + select id, ST_AsText(geom), ST_AsText(geog) from spatial_ddl_values + """ + sql """drop table if exists spatial_ddl_v2""" + sql """create table spatial_ddl_v2 (id int) engine=iceberg + properties ('format-version' = '2')""" + test { + sql """alter table spatial_ddl_v2 add column geom geometry""" + exception "require format-version >= 3" + } + sql """drop table if exists spatial_ddl_orc""" + sql """create table spatial_ddl_orc (id int) engine=iceberg + properties ('format-version' = '3', 'write-format' = 'orc', 'write.format.default' = 'orc')""" + test { + sql """alter table spatial_ddl_orc add column geom geometry""" + exception "require Parquet" + } + sql """drop table if exists spatial_create_rejected""" + for (String kind : ["geometry", "geography"]) { + test { + sql """create table spatial_create_rejected (id int, shape ${kind}) engine=iceberg + partition by list(shape) () properties ('format-version' = '3')""" + exception "for transform: identity" + } + String suffix = kind == "geometry" ? "geom" : "geog" + for (String nesting : ["", "_nested"]) { + test { + // An existing external schema is validated even when only its ordinary id is assigned. + sql """insert into spatial_existing_orc_${suffix}${nesting} (id) values (1)""" + exception "Iceberg spatial columns require Parquet" + } + test { + sql """alter table spatial_existing_orc_${suffix}${nesting} + set ('write.format.default' = 'parquet')""" + exception "Iceberg spatial columns require Parquet" + } + // Repair the winning nickname, then keep it authoritative over the standard key. + sql """alter table spatial_existing_orc_${suffix}${nesting} + set ('write-format' = 'parquet')""" + sql """alter table spatial_existing_orc_${suffix}${nesting} + set ('write.format.default' = 'orc')""" + String shape = (kind == "geometry" ? "ST_GeometryFromWKBTyped" : "ST_GeogFromWKB") + + "('0101000000000000000000F03F0000000000000040')" + sql """insert into spatial_existing_orc_${suffix}${nesting} + values (1, ${nesting ? "array(${shape})" : shape})""" + "order_qt_spatial_format_alias_${suffix}${nesting}"("""select id, + ST_AsText(${nesting ? 'shape[1]' : 'shape'}) + from spatial_existing_orc_${suffix}${nesting} order by id""") + } + } + test { + sql """create table spatial_create_rejected (geom geometry) engine=iceberg + properties ('format-version' = '2')""" + exception "require format-version >= 3" + } + test { + sql """create table spatial_create_rejected (geom geometry) engine=iceberg + properties ('format-version' = '3', 'write.format.default' = 'orc')""" + exception "require Parquet" + } + test { + sql """create table spatial_create_rejected (geom array<geometry>) engine=iceberg + properties ('format-version' = '2')""" + exception "require format-version >= 3" + } + // Spatial validation must preserve ordinary connector column-count diagnostics. + test { + sql """explain insert into spatial_ddl_v2 select 1, 2""" + exception "Expected 1 columns but got 2" + } + // Exercise actual SQL analysis before changelog coercion, as well as nullable DML writeback. + sql """drop table if exists spatial_dml_values""" + sql """create table spatial_dml_values (id int, geom geometry, geog geography) + engine=iceberg properties ('format-version' = '3', + 'write.update.mode' = 'merge-on-read', 'write.merge.mode' = 'merge-on-read')""" + sql """insert into spatial_dml_values select id, geom, geog from geometry_values""" + // UPDATE validates spatial identity before coercion, while proven NULLs can be retagged. + test { + sql """explain update spatial_dml_values set geog = + ST_GeometryFromWKBTyped('0101000000000000000000F03F0000000000000040') where id = 1""" + exception "Spatial connector write type mismatch" + } + test { + sql """explain update spatial_dml_values set geom = 'not WKB' where id = 1""" + exception "Spatial connector write cannot convert input column" + } + test { + sql """explain merge into spatial_dml_values t using projected_values s on t.id = s.id + when matched then update set geom = s.geom""" + exception "Spatial connector write CRS mismatch" + } + test { + sql """explain merge into spatial_dml_values t using projected_values s on t.id = s.id + when matched then update set geog = s.geog""" + exception "Spatial connector write algorithm mismatch" + } + test { + sql """explain merge into spatial_dml_values t using geometry_values s on t.id = s.id + when matched then update set geog = s.geom""" + exception "Spatial connector write type mismatch" + } + test { + sql """explain merge into spatial_dml_values t + using (select id, geom from projected_values) s on t.id = s.id + when matched then update set geom = s.geom""" + exception "Spatial connector write CRS mismatch" + } + test { + sql """explain with spatial_source as (select id, geom from projected_values) + merge into spatial_dml_values t using spatial_source s on t.id = s.id + when matched then update set geom = s.geom""" + exception "Spatial connector write CRS mismatch" + } + sql """with spatial_source as (select id, geom from geometry_values) + merge into spatial_dml_values t using spatial_source s on t.id = s.id + when matched and s.id = 2 then update set geom = s.geom""" + sql """update spatial_dml_values set geog = NULL where id = 1""" + sql """merge into spatial_dml_values t using geometry_values s on t.id = s.id + when matched and s.id = 2 then update set geom = s.geom, geog = s.geog + when not matched then insert (id, geom, geog) values (s.id, s.geom, s.geog)""" + order_qt_spatial_dml_values """ + select id, ST_AsText(geom), hex(ST_AsBinary(geom)), ST_AsText(geog) + from spatial_dml_values + """ + // A remote schema created by another engine must obey the same write restrictions. + // Partial aggregate text states cannot preserve delimiter-bearing nested WKB. + // Reject before execution, including the two-phase plan and both spatial kinds. + for (String constructor : ["ST_GeometryFromWKBTyped", "ST_GeogFromWKB"]) { + for (String limit : ["", ", 2"]) { + test { + sql """select /*+ SET_VAR(agg_phase=2) */ collect_list(array( + ${constructor}('01010000000000000000002c400000000000000040'))${limit}) + from geometry_values""" + exception "collect_list does not support nested spatial arguments" + } + } + } + test { + sql """select /*+ SET_VAR(agg_phase=2) */ collect_list(shapes) from nested_spatial_values""" + exception "collect_list does not support nested spatial arguments" + } + // ARRAY_AGG has a separate factory; its nonnullable branch shares the unsafe text state. + for (String constructor : ["ST_GeometryFromWKBTyped", "ST_GeogFromWKB"]) { + String point = "${constructor}('01010000000000000000002c400000000000000040')" + for (String argument : ["array(${point})", "if(id = 1, array(${point}), NULL)"]) { + for (String expression : ["array_agg(${argument})", "array_agg(distinct ${argument})", + "array_agg_state(${argument})", "array_agg_combine(${argument})", + "array_agg_merge(array_agg_state(${argument}))"]) { + test { + sql """select /*+ SET_VAR(agg_phase=2, enable_agg_state=true) */ + ${expression} from geometry_values""" + exception "array_agg does not support nested spatial arguments" + } + } + } + } + test { + sql """select /*+ SET_VAR(agg_phase=2) */ array_agg(shapes) from nested_spatial_values""" + exception "array_agg does not support nested spatial arguments" + } + qt_scalar_spatial_array_agg """select /*+ SET_VAR(agg_phase=2) */ + array_sort(array_map(shape -> hex(ST_AsBinary(shape)), + array_agg(ST_GeometryFromWKBTyped('01010000000000000000002c400000000000000040')))), + array_sort(array_map(shape -> hex(ST_AsBinary(shape)), + array_agg(ST_GeogFromWKB('01010000000000000000002c400000000000000040')))) + from geometry_values where id = 1""" + order_qt_nested_spatial_values """ + select size(shapes), ST_AsText(shapes[1]), hex(ST_AsBinary(shapes[1])) + from nested_spatial_values + """ + sql """insert into nested_spatial_values select * from nested_spatial_values""" + sql """merge into nested_spatial_values t using (select 1 id) s on s.id = 1 + when matched then update set shapes = t.shapes""" + order_qt_nested_array_writeback """select size(shapes), hex(ST_AsBinary(shapes[1])) + from nested_spatial_values""" + sql """merge into nested_spatial_values t using (select 1 id) s on s.id = 1 + when matched then delete""" + qt_nested_spatial_delete_only_merge """select count(*) from nested_spatial_values""" + // Spatial ordering keys are unsupported regardless of WKB byte order or state wrapping. + for (String constructor : ["ST_GeometryFromWKBTyped", "ST_GeogFromWKB"]) { + for (String first : ["0101000000000000000000f03f0000000000000040", + "00000000013ff00000000000004000000000000000"]) { + for (String function : ["min_by", "max_by"]) { + for (String suffix : ["", "_state"]) { + test { + sql """select ${function}${suffix}(id, array(shape)) from ( + select number id, if(number = 0, ${constructor}('${first}'), + ${constructor}('010100000000000000000008400000000000001040')) shape + from numbers('number' = '2')) values_with_byte_order""" + exception "${function} does not support spatial arguments" + } + } + } + } + } + qt_ordinary_array_minmax_by """select min_by(number, array(number)), max_by(number, array(number)) + from numbers('number' = '3')""" + // Actual external nested columns, not constructor-only values, exercise reader and writer recursion. + sql """drop table if exists spatial_nested_ddl""" + sql """create table spatial_nested_ddl (id int, payload struct<label:string>) + engine=iceberg properties ('format-version'='3')""" + sql """alter table spatial_nested_ddl add column shapes array<geometry>""" + sql """alter table spatial_nested_ddl add column payload.shapes array<geography>""" + sql """alter table spatial_nested_ddl modify column shapes array<geometry>""" + sql """alter table spatial_nested_ddl modify column payload.shapes array<geography>""" + sql """insert into spatial_nested_ddl + select id, named_struct('label', 'row', 'shapes', array(geog)), array(geom) from geometry_values""" + order_qt_static_nested_ddl """select id, hex(ST_AsBinary(shapes[1])), + hex(ST_AsBinary(struct_element(payload, 'shapes')[1])) from spatial_nested_ddl""" + test { + sql """alter table spatial_ddl_v2 add column shapes array<geometry>""" + exception "Iceberg spatial columns require format-version >= 3" + } + test { + sql """alter table spatial_ddl_orc add column payload struct<shape:geography>""" + exception "Iceberg spatial columns require Parquet" + } + order_qt_static_nested_source """select id, size(g_array), + array_map(x -> hex(ST_AsBinary(x)), g_array), + array_map(x -> hex(ST_AsBinary(x)), gg_array), + hex(ST_AsBinary(g_map['shape'])), hex(ST_AsBinary(gg_map['shape'])), + hex(ST_AsBinary(struct_element(g_struct, 'shape'))), + hex(ST_AsBinary(struct_element(gg_struct, 'shape'))), + array_map(x -> hex(ST_AsBinary(struct_element(x, 'shape'))), g_deep['shapes']), + array_map(x -> hex(ST_AsBinary(struct_element(x, 'shape'))), gg_deep['shapes']) + from spatial_nested_source""" + // Array equality is admitted; real scan slots must bypass scalar runtime-filter factories. + for (String column : ["g_array", "gg_array"]) { + for (int filterType : [1, 2, 8]) { + String joinSql = """select /*+ SET_VAR(enable_runtime_filter_prune=false, + runtime_filter_type=${filterType}) */ count(*) from spatial_nested_source l + join [broadcast] spatial_nested_source r on l.${column} = r.${column}""" + "qt_spatial_nested_scan_join_${column}_${filterType}"(joinSql) + explain { + sql joinSql + notContains "RF" + } + } + } + // MAP/STRUCT equality is rejected by existing analysis, independently of runtime filters. + for (String column : ["g_map", "gg_map", "g_struct", "gg_struct", "g_deep", "gg_deep"]) { + test { + sql """explain select count(*) from spatial_nested_source l + join spatial_nested_source r on l.${column} = r.${column}""" + exception "comparison predicate could not contains complex type" + } + } + for (String function : ["min_by", "max_by"]) { + for (String key : ["g_array", "gg_array"]) { + for (String suffix : ["", "_state"]) { + test { + sql """select ${function}${suffix}(id, ${key}) from spatial_nested_source""" + exception "${function} does not support spatial arguments" + } + } + } + "qt_spatial_${function}_nested_values"("""select + hex(ST_AsBinary(element_at(${function}(g_array, id), 1))), + hex(ST_AsBinary(element_at(${function}(g_map, id), 'shape'))), + hex(ST_AsBinary(struct_element(${function}(g_struct, id), 'shape'))), + hex(ST_AsBinary(element_at(${function}(gg_array, id), 1))), + hex(ST_AsBinary(element_at(${function}(gg_map, id), 'shape'))), + hex(ST_AsBinary(struct_element(${function}(gg_struct, id), 'shape'))) + from spatial_nested_source where id in (1, 2)""") + "qt_spatial_${function}_nested_state_values"("""select + hex(ST_AsBinary(element_at(${function}_merge(a), 1))), + hex(ST_AsBinary(element_at(${function}_merge(b), 'shape'))), + hex(ST_AsBinary(struct_element(${function}_merge(c), 'shape'))), + hex(ST_AsBinary(element_at(${function}_merge(d), 1))), + hex(ST_AsBinary(element_at(${function}_merge(e), 'shape'))), + hex(ST_AsBinary(struct_element(${function}_merge(f), 'shape'))) + from (select ${function}_state(g_array, id) a, ${function}_state(g_map, id) b, + ${function}_state(g_struct, id) c, ${function}_state(gg_array, id) d, + ${function}_state(gg_map, id) e, ${function}_state(gg_struct, id) f + from spatial_nested_source where id in (1, 2)) partial_values""") + } + order_qt_spatial_new_functions_scan """select id, + ST_Equals(geog, geog), ST_Overlaps(geog, geog), ST_Contains(geog, geog), + ST_Touches(geog, geog), ST_DWithin(geog, geog, id) + from geometry_values order by id""" + order_qt_spatial_new_functions_nested_scan """select id, + ST_Equals(gg_array[1], gg_map['shape']), + ST_DWithin(struct_element(gg_struct, 'shape'), gg_array[1], id) + from spatial_nested_source where id in (1,2,5,6,7) order by id""" + sql """drop table if exists spatial_nested_copy""" + sql """create table spatial_nested_copy ( + id int, g_array array<geometry>, g_map map<string, geometry>, + g_struct struct<shape:geometry,label:string>, g_deep map<string,array<struct<shape:geometry>>>, + gg_array array<geography>, gg_map map<string, geography>, + gg_struct struct<shape:geography,label:string>, gg_deep map<string,array<struct<shape:geography>>>) + properties ('format-version'='3')""" + sql """insert into spatial_nested_copy select * from spatial_nested_source""" + sql """refresh table spatial_nested_copy""" + order_qt_static_nested_copy """select id, size(g_array), + array_map(x -> hex(ST_AsBinary(x)), g_array), + array_map(x -> hex(ST_AsBinary(x)), gg_array), + hex(ST_AsBinary(g_map['shape'])), hex(ST_AsBinary(gg_map['shape'])), + hex(ST_AsBinary(struct_element(g_struct, 'shape'))), + hex(ST_AsBinary(struct_element(gg_struct, 'shape'))), + array_map(x -> hex(ST_AsBinary(struct_element(x, 'shape'))), g_deep['shapes']), + array_map(x -> hex(ST_AsBinary(struct_element(x, 'shape'))), gg_deep['shapes']) + from spatial_nested_copy""" + sql """drop table if exists spatial_nested_ctas""" + sql """create table spatial_nested_ctas properties ('format-version'='3') as + select * from spatial_nested_source""" + order_qt_static_nested_ctas """select id, hex(ST_AsBinary(g_map['shape'])), + hex(ST_AsBinary(struct_element(gg_struct, 'shape'))) from spatial_nested_ctas""" + // Normalization folds these typed NULLs before target identity checks. + sql """insert into spatial_nested_copy (id, g_array, gg_map, g_struct) values + (7, array(CAST(NULL AS GEOMETRY('EPSG:3857'))), map('shape', CAST(NULL AS INT)), + named_struct('shape', CAST(NULL AS INT), 'label', 'null'))""" + qt_static_nested_typed_nulls """select id, size(g_array), g_array[1] is null, + gg_map['shape'] is null, struct_element(g_struct, 'shape') is null + from spatial_nested_copy where id = 7""" + for (String expression : ["array(geom)", + "if(id = 1, array(geom), cast(NULL as array<geometry('EPSG:3857')>))"]) { + test { + sql """insert into spatial_nested_copy (id, g_array) select id + 10, + ${expression} from projected_values""" + exception "CRS mismatch" + } + } + test { + sql """insert into spatial_nested_copy (id, g_array) + select id + 10, cast(array(geom) as array<geometry>) from projected_values""" + exception "can not cast" + } + test { + sql """insert into spatial_nested_copy (id, g_map) + select id + 20, map('shape', geog) from geometry_values""" + exception "type mismatch" + } + test { + sql """insert into spatial_nested_copy (id, gg_struct) select id + 30, + named_struct('shape', geog, 'label', 'wrong') from projected_values""" + exception "algorithm mismatch" + } + // Partition keys require semantic equality; ordinary keys may still carry spatial window values. + for (String column : ["geom", "geog"]) { + for (String key : [column, "array(${column})", "map(id, ${column})", + "named_struct('shape', ${column})"]) { + test { + sql """select id, count(*) over (partition by ${key}) from geometry_values""" + exception "PARTITION BY does not support spatial keys" + } + } + for (String function : ["count_at_min", "count_at_max"]) { + for (String expression : ["${function}(${column})", "${function}(array(${column}))", + "${function}(${column}) over ()", "${function}_state(${column})"]) { + test { + sql """select /*+ SET_VAR(enable_agg_state=true) */ ${expression} from geometry_values""" + exception "does not support spatial arguments" + } + } + } + for (String value : [column, "array(${column})", "map('shape', ${column})", + "named_struct('shape', ${column})"]) { + test { + sql """select cast(${value} as variant) from geometry_values""" + exception (value == column ? "cannot cast" : "can not cast") + } + } + } + order_qt_static_spatial_window_values """select id, + hex(ST_AsBinary(first_value(geom) over (partition by id order by id))), + hex(ST_AsBinary(last_value(geog) over (partition by id order by id))) + from geometry_values""" + qt_static_ordinary_extremes_variant """select count_at_min(id), count_at_max(id), + count(cast(array(id) as variant)) from geometry_values""" + sql """drop table if exists spatial_variant_neighbors""" + sql """create table spatial_variant_neighbors (id int, geom geometry, geog geography, payload variant) + engine=iceberg properties ('format-version'='3')""" + sql """insert into spatial_variant_neighbors + select id, geom, geog, cast(array(id) as variant) from geometry_values""" + sql """refresh table spatial_variant_neighbors""" + order_qt_static_variant_neighbors """select id, hex(ST_AsBinary(geom)), hex(ST_AsBinary(geog)), + cast(payload as string) from spatial_variant_neighbors""" + sql """drop table if exists spatial_wkb_values""" + sql """create table spatial_wkb_values (id int, geom geometry, geog geography) + engine=iceberg properties ('format-version' = '3')""" + sql """insert into spatial_wkb_values values + (1, ST_GeometryFromWKBTyped('0104000000020000000101000000000000000000F03F0000000000000040000000000140080000000000004010000000000000'), + ST_GeogFromWKB('0104000000020000000101000000000000000000F03F0000000000000040000000000140080000000000004010000000000000')), + (2, ST_GeometryFromWKBTyped('010500000001000000010200000000000000'), + ST_GeogFromWKB('010500000001000000010200000000000000')), + (3, ST_GeometryFromWKBTyped('010600000001000000010300000000000000'), + ST_GeogFromWKB('010600000001000000010300000000000000')), + (4, ST_GeometryFromWKBTyped('010700000001000000010700000000000000'), + ST_GeogFromWKB('010700000001000000010700000000000000')), + (5, ST_GeometryFromWKBTyped('0101000000000000000000F87F000000000000F87F'), + ST_GeogFromWKB('0101000000000000000000F87F000000000000F87F')), + (6, ST_GeometryFromWKBTyped('010200000000000000'), ST_GeogFromWKB('010200000000000000')), + (7, ST_GeometryFromWKBTyped('010300000000000000'), ST_GeogFromWKB('010300000000000000')), + (8, NULL, NULL), + (9, ST_GeometryFromWKBTyped('0101000000000000000000F03F000000000000F87F'), + ST_GeogFromWKB('0101000000000000000000F03F000000000000F87F')), + (10, ST_GeometryFromWKBTyped('010200000002000000000000000000F03F000000000000004000000000000008400000000000001040'), + ST_GeogFromWKB('010200000002000000000000000000F03F000000000000004000000000000008400000000000001040')), + (11, ST_GeometryFromWKBTyped('0103000000010000000400000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000004000000000000000000000000000000000'), + ST_GeogFromWKB('0103000000010000000400000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000004000000000000000000000000000000000')), + (12, ST_GeometryFromWKBTyped('010500000001000000010200000002000000000000000000F03F000000000000004000000000000008400000000000001040'), + ST_GeogFromWKB('010500000001000000010200000002000000000000000000F03F000000000000004000000000000008400000000000001040')), + (13, ST_GeometryFromWKBTyped('0106000000010000000103000000010000000400000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000004000000000000000000000000000000000'), + ST_GeogFromWKB('0106000000010000000103000000010000000400000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000004000000000000000000000000000000000')), + (14, ST_GeometryFromWKBTyped('0107000000030000000101000000000000000000F03F0000000000000040010200000002000000000000000000F03F0000000000000040000000000000084000000000000010400103000000010000000400000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000004000000000000000000000000000000000'), + ST_GeogFromWKB('0107000000030000000101000000000000000000F03F0000000000000040010200000002000000000000000000F03F0000000000000040000000000000084000000000000010400103000000010000000400000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000004000000000000000000000000000000000'))""" + order_qt_spatial_wkb_values """ + select id, geom is null, geog is null, ST_AsText(geom), ST_AsText(geog), + ST_GeometryType(geom), ST_X(geom), ST_Y(geog), hex(ST_AsBinary(geom)) + from spatial_wkb_values + """ + // Exercise FE binding on scanned Geography columns, not only direct BE function calls. + order_qt_spatial_area_columns """select id, + round(ST_Area_Square_Meters(geog), 3), round(ST_Area_Square_Km(geog), 6), + abs(ST_Area_Square_Meters(geog) / 1000000 - ST_Area_Square_Km(geog)) < 0.000001 + from spatial_wkb_values where id in (8, 11)""" + qt_spatial_area_constants """select + ST_Area_Square_Meters(ST_GeogFromWKB('0101000000000000000000F03F0000000000000040')), + ST_Area_Square_Km(ST_GeogFromWKB('0101000000000000000000F03F0000000000000040')), + ST_Area_Square_Meters(cast(NULL as geography)), + ST_Area_Square_Km(cast(NULL as geography))""" + qt_spatial_area_legacy """select + round(ST_Area_Square_Meters(ST_Polygon('POLYGON ((0 0, 2 0, 0 2, 0 0))')), 3), + round(ST_Area_Square_Km(ST_Polygon('POLYGON ((0 0, 2 0, 0 2, 0 0))')), 6), + ST_Area_Square_Meters(NULL), ST_Area_Square_Km(NULL)""" + for (String function : ["ST_Area_Square_Meters", "ST_Area_Square_Km"]) { + for (String source : ["spatial_wkb_values", "projected_values"]) { + test { + sql """select ${function}(geom) from ${source} where id = 1""" + exception "Spatial calculation does not support typed GEOMETRY" + } + } + for (String source : ["projected_values", "referenced_crs_values"]) { + test { + sql """select ${function}(geog) from ${source} where id = 1""" + exception "Spatial calculation does not support typed GEOGRAPHY" + } + } + } + test { + sql """select ST_Length(geog) from spatial_wkb_values where id = 1""" + exception "does not support this Geography WKB shape" + } + sql """drop table if exists projected_copy""" + sql """create table projected_copy ( + id int, geom geometry('EPSG:3857'), geog geography('OGC:CRS84', 'vincenty') + ) engine=iceberg properties ('format-version' = '3')""" + sql """insert into projected_copy select id, geom, geog from projected_values""" + // A generic file TVF must not infer spatial identity outside the Iceberg catalog contract. + def spatialFile = (sql """select file_path from `projected_copy\$files` where content = 0""")[0][0] + test { + sql """select * from S3( + 'uri' = '${spatialFile}', 'format' = 'parquet', + 's3.endpoint' = 'http://${externalEnvIp}:${minioPort}', + 's3.access_key' = 'admin', 's3.secret_key' = 'password', + 's3.region' = 'us-east-1')""" + exception "Spatial Parquet columns are only supported through an Iceberg catalog" + } + // An unannotated BYTE_ARRAY remains binary when read without Iceberg schema metadata. + def rawFile = (sql """select file_path from `projected_values\$files` where content = 0""")[0][0] + order_qt_raw_file_bytes """select id, from_binary(geom), from_binary(geog) from S3( + 'uri' = '${rawFile}', 'format' = 'parquet', + 's3.endpoint' = 'http://${externalEnvIp}:${minioPort}', + 's3.access_key' = 'admin', 's3.secret_key' = 'password', + 's3.region' = 'us-east-1')""" + // Materialize payloads in both readers: COUNT pushdown alone can bypass WKB conversion. + // geometry_values has spatial Parquet annotations; projected_values imports unannotated WKB. + for (boolean scanV2 : [false, true]) { + "qt_annotated_spatial_payload_${scanV2}"("""select /*+ SET_VAR( + enable_file_scanner_v2=${scanV2}) */ id, + geom is null, ST_AsText(geom), hex(ST_AsBinary(geom)), + geog is null, ST_AsText(geog), hex(ST_AsBinary(geog)) + from geometry_values order by id""") + "qt_unannotated_spatial_payload_${scanV2}"("""select /*+ SET_VAR( + enable_file_scanner_v2=${scanV2}) */ id, + geom is null, ST_AsText(geom), hex(ST_AsBinary(geom)), + geog is null, ST_AsText(geog), hex(ST_AsBinary(geog)) + from projected_values order by id""") + } + sql """drop table if exists spatial_zm_values""" + sql """create table spatial_zm_values (id int, geom geometry, geog geography) + engine=iceberg properties ('format-version' = '3')""" + sql """insert into spatial_zm_values values + (1, ST_GeometryFromWKBTyped('01E9030000000000000000F03F00000000000000400000000000000840'), + ST_GeogFromWKB('01E9030000000000000000F03F00000000000000400000000000000840')), + (2, ST_GeometryFromWKBTyped('01D1070000000000000000F03F00000000000000400000000000000840'), + ST_GeogFromWKB('01D1070000000000000000F03F00000000000000400000000000000840')), + (3, ST_GeometryFromWKBTyped('01B90B0000000000000000F03F000000000000004000000000000008400000000000001040'), + ST_GeogFromWKB('01B90B0000000000000000F03F000000000000004000000000000008400000000000001040')), + (4, NULL, NULL), + (5, ST_GeometryFromWKBTyped('01EB03000000000000'), ST_GeogFromWKB('01EB03000000000000'))""" + sql """drop table if exists spatial_zm_copy""" + sql """create table spatial_zm_copy (id int, geom geometry, geog geography) + engine=iceberg properties ('format-version' = '3')""" + sql """insert into spatial_zm_copy select id, geom, geog from spatial_zm_values""" + for (boolean scanV2 : [false, true]) { + "qt_spatial_zm_payload_${scanV2}"("""select /*+ SET_VAR(enable_file_scanner_v2=${scanV2}) */ + id, hex(ST_AsBinary(geom)), hex(ST_AsBinary(geog)) Review Comment: Fixed in 88a45076af2. Regenerated spatial_zm_payload_false, spatial_zm_payload_true, and spatial_zm_reread to match their current three-column projections, including the NULL row. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
