/** * Copyright (C) 2009-2013 FoundationDB, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.foundationdb.server.test.it.qp; import com.foundationdb.ais.model.Index; import com.foundationdb.ais.model.TableName; import com.foundationdb.qp.expression.IndexBound; import com.foundationdb.qp.expression.IndexKeyRange; import com.foundationdb.qp.operator.API; import com.foundationdb.qp.operator.Cursor; import com.foundationdb.qp.operator.Operator; import com.foundationdb.qp.row.Row; import com.foundationdb.qp.rowtype.IndexRowType; import com.foundationdb.qp.rowtype.RowType; import com.foundationdb.qp.rowtype.Schema; import com.foundationdb.qp.rowtype.TableRowType; import com.foundationdb.server.api.dml.SetColumnSelector; import com.foundationdb.server.spatial.BoxLatLon; import com.foundationdb.server.spatial.Spatial; import com.foundationdb.server.types.value.ValueSources; import com.geophile.z.Space; import com.geophile.z.SpatialObject; import org.junit.Test; import java.math.BigDecimal; import java.util.*; import static com.foundationdb.qp.operator.API.cursor; import static com.foundationdb.qp.operator.API.indexScan_Default; import static java.lang.Math.abs; import static org.junit.Assert.*; public class SpatialLatLonGroupIndexScanIT extends OperatorITBase { @Override protected boolean doAutoTransaction() { return false; } @Override public void setupCreateSchema() { parent = createTable( "schema", "parent", "pid int not null", // 10x, x >= 0 "pbefore int not null", // pid mod 3 "plat decimal(11, 7)", // [-90, 90) in steps of 10 "plon decimal(11, 7)", // [-180, 180) in steps of 10 "primary key(pid)"); child = createTable( "schema", "child", "cid int not null", // pid + x, x in [1, 9] "pid int", "cafter int not null", // cid mod 5 "clat decimal(11, 7)", // plat + cid % 10 "clon decimal(11, 7)", // plon + cid % 10 "primary key(cid)", "grouping foreign key(pid) references parent(pid)"); groupName = new TableName("schema", "parent"); createSpatialGroupIndex(groupName, "pbefore_clat_clon_cafter", 1, Spatial.LAT_LON_DIMENSIONS, Index.JoinType.LEFT, "parent.pbefore", "child.clat", "child.clon", "child.cafter"); createSpatialGroupIndex(groupName, "pbefore_plat_plon_cafter", 1, Spatial.LAT_LON_DIMENSIONS, Index.JoinType.LEFT, "parent.pbefore", "parent.plat", "parent.plon", "child.cafter"); } @Override protected void setupPostCreateSchema() { parentRowType = schema.tableRowType(table(parent)); childRowType = schema.tableRowType(table(child)); parentOrdinal = parentRowType.table().getOrdinal(); childOrdinal = childRowType.table().getOrdinal(); cSpatialIndexRowType = groupIndexType(groupName, "parent.pbefore", "child.clat", "child.clon", "child.cafter"); pSpatialIndexRowType = groupIndexType(groupName, "parent.pbefore", "parent.plat", "parent.plon", "child.cafter"); space = Spatial.createLatLonSpace(); queryContext = queryContext(adapter); queryBindings = queryContext.createBindings(); } protected int lookaheadQuantum() { return 1; } @Test public void testLoad() { loadDB(); try (TransactionContext t = new TransactionContext()) { Operator plan = indexScan_Default(cSpatialIndexRowType); long[][] expected = new long[childZToCid.size()][]; int r = 0; for (Map.Entry<Long, Integer> entry : childZToCid.entrySet()) { long z = entry.getKey(); int cid = entry.getValue(); int pid = pid(cid); expected[r++] = new long[]{before(pid), z, after(cid), pid, cid}; } compareRows(rows(cSpatialIndexRowType.physicalRowType(), sort(expected)), cursor(plan, queryContext, queryBindings)); } try (TransactionContext t = new TransactionContext()) { Operator plan = indexScan_Default(pSpatialIndexRowType); long[][] expected = new long[parentZToPid.size() * CHILDREN_PER_PARENT][]; int r = 0; for (Map.Entry<Long, Integer> entry : parentZToPid.entrySet()) { long z = entry.getKey(); int pid = entry.getValue(); for (int c = 1; c <= CHILDREN_PER_PARENT; c++) { int cid = pid + c; expected[r++] = new long[]{before(pid), z, after(cid), pid, cid}; } } compareRows(rows(pSpatialIndexRowType.physicalRowType(), sort(expected)), cursor(plan, queryContext, queryBindings)); } } @Test public void testLoadAndRemove() { loadDB(); List<Row> remainingRows = new ArrayList<>(); try (TransactionContext t = new TransactionContext()) { // Delete the first (1 + (pid % CHILDREN_PER_PARENT)) children of parent pid, and // keep track of the remaining rows. for (Integer pid : parentZToPid.values()) { int nChildrenToDelete = 1 + pid % CHILDREN_PER_PARENT; for (int c = 1; c <= CHILDREN_PER_PARENT; c++) { int cid = pid + c; Row row = row(child, cid, pid, after(cid), clats.get(cid), clons.get(cid)); if (c <= nChildrenToDelete) { deleteRow(row); } else { remainingRows.add(row); } } } } try (TransactionContext t = new TransactionContext()) { Operator plan = indexScan_Default(cSpatialIndexRowType); long[][] expected = new long[remainingRows.size()][]; int r = 0; for (Row row : remainingRows) { int cid = (Integer) ValueSources.toObject(row.value(0)); int pid = (Integer) ValueSources.toObject(row.value(1)); int after = (Integer) ValueSources.toObject(row.value(2)); BigDecimal clat = (BigDecimal) ValueSources.toObject(row.value(3)); BigDecimal clon = (BigDecimal) ValueSources.toObject(row.value(4)); long z = Spatial.shuffle(space, clat.doubleValue(), clon.doubleValue()); expected[r++] = new long[]{before(pid), z, after, pid, cid}; } compareRows(rows(cSpatialIndexRowType.physicalRowType(), sort(expected)), cursor(plan, queryContext, queryBindings)); } try (TransactionContext t = new TransactionContext()) { Operator plan = indexScan_Default(pSpatialIndexRowType); long[][] expected = new long[remainingRows.size()][]; int r = 0; for (Row row : remainingRows) { int cid = (Integer) ValueSources.toObject(row.value(0)); int pid = (Integer) ValueSources.toObject(row.value(1)); int after = (Integer) ValueSources.toObject(row.value(2)); BigDecimal plat = plats.get(pid); BigDecimal plon = plons.get(pid); long z = Spatial.shuffle(space, plat.doubleValue(), plon.doubleValue()); expected[r++] = new long[]{before(pid), z, after, pid, cid}; } compareRows(rows(pSpatialIndexRowType.physicalRowType(), sort(expected)), cursor(plan, queryContext, queryBindings)); } } @Test public void testLoadAndUpdate() { loadDB(); List<Integer> pids = new ArrayList<>(parentZToPid.values()); parentZToPid.clear(); childZToCid.clear(); try (TransactionContext t = new TransactionContext()) { // Increment plon and clon values for (Integer pid : pids) { BigDecimal plat = plats.get(pid); BigDecimal plon = plons.get(pid); Row original = row(parent, pid, before(pid), plat, plon); Row updated = row(parent, pid, before(pid), plat, plon.add(BigDecimal.ONE)); long z = Spatial.shuffle(space, plat.doubleValue(), plon.doubleValue() + 1); parentZToPid.put(z, pid); updateRow(original, updated); for (int c = 1; c <= CHILDREN_PER_PARENT; c++) { Integer cid = pid + c; BigDecimal clat = clats.get(cid); BigDecimal clon = clons.get(cid); original = row(child, cid, pid, after(cid), clat, clon); updated = row(child, cid, pid, after(cid), clat, clon.add(BigDecimal.ONE)); updateRow(original, updated); z = Spatial.shuffle(space, clat.doubleValue(), clon.doubleValue() + 1); childZToCid.put(z, cid); } } } try (TransactionContext t = new TransactionContext()) { Operator plan = indexScan_Default(cSpatialIndexRowType); long[][] expected = new long[childZToCid.size()][]; int r = 0; for (Map.Entry<Long, Integer> entry : childZToCid.entrySet()) { long z = entry.getKey(); int cid = entry.getValue(); int pid = pid(cid); expected[r++] = new long[]{before(pid), z, after(cid), pid, cid}; } compareRows(rows(cSpatialIndexRowType.physicalRowType(), sort(expected)), cursor(plan, queryContext, queryBindings)); } try (TransactionContext t = new TransactionContext()) { Operator plan = indexScan_Default(pSpatialIndexRowType); long[][] expected = new long[parentZToPid.size() * CHILDREN_PER_PARENT][]; int r = 0; for (Map.Entry<Long, Integer> entry : parentZToPid.entrySet()) { int pid = entry.getValue(); long z = entry.getKey(); for (int c = 1; c <= CHILDREN_PER_PARENT; c++) { int cid = pid + c; expected[r++] = new long[]{before(pid), z, after(cid), pid, cid}; } } compareRows(rows(pSpatialIndexRowType.physicalRowType(), sort(expected)), cursor(plan, queryContext, queryBindings)); } } @Test public void testSpatialQueryPLatPLon() { // Find rows with random parent.before value and (plat, plon) inside random box. loadDB(); final int N = 100; int beforeEQ; BigDecimal latLo; BigDecimal latHi; BigDecimal lonLo; BigDecimal lonHi; int nEmpty = 0; for (int i = 0; i < N; i++) { try (TransactionContext t = new TransactionContext()) { beforeEQ = randomBefore(); latLo = randomLat(); latHi = randomLat(); if (latLo.compareTo(latHi) > 0) { BigDecimal swap = latLo; latLo = latHi; latHi = swap; } lonLo = randomLon(); lonHi = randomLon(); if (lonLo.compareTo(lonHi) > 0) { BigDecimal swap = lonLo; lonLo = lonHi; lonHi = swap; } // Get the right answer Set<Integer> expectedCids = new HashSet<>(); for (int pid : parentZToPid.values()) { int before = before(pid); BigDecimal lat = plats.get(pid); BigDecimal lon = plons.get(pid); if (before == beforeEQ && latLo.compareTo(lat) <= 0 && lat.compareTo(latHi) <= 0 && lonLo.compareTo(lon) <= 0 && lon.compareTo(lonHi) <= 0) { for (int c = 1; c <= CHILDREN_PER_PARENT; c++) { int cid = pid + c; expectedCids.add(cid); } } } if (expectedCids.isEmpty()) { nEmpty++; } // Get the query result using an index Set<Integer> actual = new HashSet<>(); SpatialObject queryBox = BoxLatLon.newBox(latLo.doubleValue(), latHi.doubleValue(), lonLo.doubleValue(), lonHi.doubleValue()); IndexBound boxBound = new IndexBound(row(pSpatialIndexRowType, beforeEQ, queryBox), new SetColumnSelector(0, 1)); IndexKeyRange box = IndexKeyRange.spatialObject(pSpatialIndexRowType, boxBound); Operator plan = indexScan_Default(pSpatialIndexRowType, box, lookaheadQuantum()); Cursor cursor = API.cursor(plan, queryContext, queryBindings); cursor.openTopLevel(); Row row; while ((row = cursor.next()) != null) { assertSame(pSpatialIndexRowType.physicalRowType(), row.rowType()); // Get row state int before = getLong(row, 0).intValue(); long z = getLong(row, 1); int pid = getLong(row, 3).intValue(); int cid = getLong(row, 4).intValue(); // Check against expected assertEquals(beforeEQ, before); Integer expectedPid = parentZToPid.get(z); assertNotNull(expectedPid); assertEquals(expectedPid.intValue(), pid); assertTrue(cid >= expectedPid + 1 && cid <= expectedPid + CHILDREN_PER_PARENT); assertEquals(expectedHKey(pid, cid), row.hKey().toString()); actual.add(cid); } // There should be no false negatives assertTrue(actual.containsAll(expectedCids)); } } // If there are too many empty results, we need to know about it, and try less restrictive queries. assertTrue(nEmpty < N * 0.2); } @Test public void testSpatialQueryCLatCLon() { // Find rows with random parent.before value and (clat, clon) inside random box. loadDB(); final int N = 100; int beforeEQ; BigDecimal latLo; BigDecimal latHi; BigDecimal lonLo; BigDecimal lonHi; int nEmpty = 0; for (int i = 0; i < N; i++) { try (TransactionContext t = new TransactionContext()) { beforeEQ = randomBefore(); latLo = randomLat(); latHi = randomLat(); if (latLo.compareTo(latHi) > 0) { BigDecimal swap = latLo; latLo = latHi; latHi = swap; } lonLo = randomLon(); lonHi = randomLon(); if (lonLo.compareTo(lonHi) > 0) { BigDecimal swap = lonLo; lonLo = lonHi; lonHi = swap; } // Get the right answer Set<Integer> expectedCids = new HashSet<>(); for (int cid : childZToCid.values()) { int before = before(pid(cid)); BigDecimal lat = clats.get(cid); BigDecimal lon = clons.get(cid); if (before == beforeEQ && latLo.compareTo(lat) <= 0 && lat.compareTo(latHi) <= 0 && lonLo.compareTo(lon) <= 0 && lon.compareTo(lonHi) <= 0) { expectedCids.add(cid); } } if (expectedCids.isEmpty()) { nEmpty++; } // Get the query result using an index Set<Integer> actual = new HashSet<>(); SpatialObject queryBox = BoxLatLon.newBox(latLo.doubleValue(), latHi.doubleValue(), lonLo.doubleValue(), lonHi.doubleValue()); IndexBound boxBound = new IndexBound(row(cSpatialIndexRowType, beforeEQ, queryBox), new SetColumnSelector(0, 1)); IndexKeyRange box = IndexKeyRange.spatialObject(cSpatialIndexRowType, boxBound); Operator plan = indexScan_Default(cSpatialIndexRowType, box, lookaheadQuantum()); Cursor cursor = API.cursor(plan, queryContext, queryBindings); cursor.openTopLevel(); Row row; while ((row = cursor.next()) != null) { assertSame(cSpatialIndexRowType.physicalRowType(), row.rowType()); // Get row state int before = getLong(row, 0).intValue(); long z = getLong(row, 1); int pid = getLong(row, 3).intValue(); int cid = getLong(row, 4).intValue(); // Check against expected assertEquals(beforeEQ, before); Integer expectedCid = childZToCid.get(z); assertNotNull(expectedCid); assertEquals(expectedCid.intValue(), cid); assertEquals(pid(expectedCid), pid); assertEquals(expectedHKey(pid, cid), row.hKey().toString()); actual.add(cid); } // There should be no false negatives assertTrue(actual.containsAll(expectedCids)); } } // If there are too many empty results, we need to know about it, and try less restrictive queries. assertTrue(nEmpty < N * 0.2); } @Test public void testNearPoint() { loadDB(); final int N = 100; for (int i = 0; i < N; i++) { try (TransactionContext t = new TransactionContext()) { BigDecimal queryLat = randomLat(); BigDecimal queryLon = randomLon(); long zStart = Spatial.shuffle(space, queryLat.doubleValue(), queryLon.doubleValue()); for (int beforeEQ = 0; beforeEQ <= 2; beforeEQ++) { // Expected SortedMap<Long, Integer> distanceToId = new TreeMap<>(); for (Map.Entry<Long, Integer> entry : childZToCid.entrySet()) { long z = entry.getKey(); int cid = entry.getValue(); if (before(pid(cid)) == beforeEQ) { long distance = abs(z - zStart); Integer replaced = distanceToId.put(distance, cid); // TODO: Duplicate distances are possible assertNull(replaced); } } Collection<Integer> expectedIdByDistance = distanceToId.values(); // Actual IndexBound zStartBound = new IndexBound(row(cSpatialIndexRowType, beforeEQ, queryLat, queryLon), new SetColumnSelector(0, 1, 2)); IndexKeyRange zStartRange = IndexKeyRange.around(cSpatialIndexRowType, zStartBound); Operator plan = indexScan_Default(cSpatialIndexRowType, zStartRange, lookaheadQuantum()); Cursor cursor = API.cursor(plan, queryContext, queryBindings); cursor.openTopLevel(); Row row; long previousDistance = Long.MIN_VALUE; Collection<Integer> actualIdByDistance = new ArrayList<>(); while ((row = cursor.next()) != null) { // Get row state int before = getLong(row, 0).intValue(); long z = getLong(row, 1); int pid = getLong(row, 3).intValue(); int cid = getLong(row, 4).intValue(); // Check against expected assertSame(cSpatialIndexRowType.physicalRowType(), row.rowType()); assertEquals(beforeEQ, before); BigDecimal clat = clats.get(cid); BigDecimal clon = clons.get(cid); long zExpected = Spatial.shuffle(space, clat.doubleValue(), clon.doubleValue()); assertEquals(zExpected, z); Integer expectedCid = childZToCid.get(z); assertNotNull(expectedCid); assertEquals(expectedCid.intValue(), cid); assertEquals(pid(expectedCid), pid); assertEquals(expectedHKey(pid, cid), row.hKey().toString()); long distance = abs(zExpected - zStart); assertTrue(distance >= previousDistance); previousDistance = distance; actualIdByDistance.add(cid); } assertEquals(new ArrayList<>(expectedIdByDistance), new ArrayList<>(actualIdByDistance)); } } } } private void loadDB() { try (TransactionContext t = new TransactionContext()) { int pid = 0; for (long y = LAT_LO; y < LAT_HI; y += DLAT) { for (long x = LON_LO; x < LON_HI; x += DLON) { BigDecimal plat = new BigDecimal(y); BigDecimal plon = new BigDecimal(x); writeRow(parent, pid, before(pid), plat, plon); long parentZ = Spatial.shuffle(space, plat.doubleValue(), plon.doubleValue()); parentZToPid.put(parentZ, pid); plats.put(pid, plat); plons.put(pid, plon); parentZs.add(parentZ); // System.out.println(String.format("parent %016x -> %s", parentZ, pid)); for (int cid = pid + 1; cid <= pid + CHILDREN_PER_PARENT; cid++) { BigDecimal clat = clat(plat, cid); BigDecimal clon = clon(plon, cid); clats.put(cid, clat); clons.put(cid, clon); long childZ = Spatial.shuffle(space, clat.doubleValue(), clon.doubleValue()); childZToCid.put(childZ, cid); // System.out.println(String.format(" child %016x -> %s", childZ, cid)); writeRow(child, cid, pid, after(cid), clat, clon); } pid += 10; } } } } private int randomBefore() { return random.nextInt(3); } private BigDecimal randomLat() { return new BigDecimal(random.nextDouble() * LAT_RANGE + LAT_LO); } private BigDecimal randomLon() { return new BigDecimal(random.nextDouble() * LON_RANGE + LON_LO); } private int before(int id) { return id % 3; } private int after(int id) { return id % 5; } private BigDecimal clat(BigDecimal plat, long cid) { return plat.add(new BigDecimal(cid % 10)); } private BigDecimal clon(BigDecimal plon, long cid) { return plon.add(new BigDecimal(cid % 10)); } private int pid(int cid) { return 10 * (cid / 10); } private Row[] rows(RowType rowType, long[][] x) { Row[] rows = new Row[x.length]; for (int i = 0; i < x.length; i++) { long[] a = x[i]; Object[] oa = new Object[a.length]; for (int j = 0; j < a.length; j++) { oa[j] = a[j]; } rows[i] = row(rowType, oa); } return rows; } private String expectedHKey(int pid, int cid) { return String.format("{%s,(long)%s,%s,(long)%s}", parentOrdinal, pid, childOrdinal, cid); } private long[][] sort(long[][] a) { Arrays.sort(a, new Comparator<long[]>() { @Override public int compare(long[] x, long[] y) { for (int i = 0; i < x.length; i++) { if (x[i] < y[i]) { return -1; } if (x[i] > y[i]) { return 1; } } return 0; } }); return a; } private static final int LAT_LO = -90; private static final int LAT_HI = 90; private static final int LON_LO = -180; private static final int LON_HI = 180; private static final int LAT_RANGE = LAT_HI - LAT_LO; private static final int LON_RANGE = LON_HI - LON_LO; private static final int DLAT = 10; private static final int DLON = 10; private static final int CHILDREN_PER_PARENT = 3; private int parent; private int child; private TableName groupName; private TableRowType parentRowType; private TableRowType childRowType; private int parentOrdinal; private int childOrdinal; private IndexRowType cSpatialIndexRowType; private IndexRowType pSpatialIndexRowType; private Space space; private Map<Long, Integer> childZToCid = new TreeMap<>(); private Map<Long, Integer> parentZToPid = new TreeMap<>(); Map<Integer, BigDecimal> plats = new HashMap<>(); // indexed by pid Map<Integer, BigDecimal> plons = new HashMap<>(); // indexed by pid Map<Integer, BigDecimal> clats = new HashMap<>(); // indexed by cid Map<Integer, BigDecimal> clons = new HashMap<>(); // indexed by cid List<Long> parentZs = new ArrayList<>(); // indexed by id Random random = new Random(123456); }