diff --git a/xtraplatform-cql/src/main/java/de/ii/xtraplatform/cql/domain/InResultSet.java b/xtraplatform-cql/src/main/java/de/ii/xtraplatform/cql/domain/InResultSet.java index f1b04483c..7d3d4fa63 100644 --- a/xtraplatform-cql/src/main/java/de/ii/xtraplatform/cql/domain/InResultSet.java +++ b/xtraplatform-cql/src/main/java/de/ii/xtraplatform/cql/domain/InResultSet.java @@ -63,6 +63,15 @@ default String getOp() { @JsonIgnore Optional> getMaterializedValues(); + /** + * Name of a table the service has materialized the result set into (one column of member values). + * Used for sets that are too large to inline as a literal list: the predicate is encoded as + * {@code IN (SELECT FROM )} against the pre-materialized, indexed table, so + * the producing query runs once instead of being re-derived in every consumer. + */ + @JsonIgnore + Optional getMaterializedTable(); + @JsonIgnore @Value.Lazy default String getSetName() { diff --git a/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/app/FilterEncoderSql.java b/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/app/FilterEncoderSql.java index 016749522..f52c47384 100644 --- a/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/app/FilterEncoderSql.java +++ b/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/app/FilterEncoderSql.java @@ -268,7 +268,10 @@ private String reduceSelectToColumnForTemplate(String expression) { // output column alias of every result-set CTE; consumers reference it as `SELECT FROM // ` - private static final String CTE_VALUE_COL = "rs_value"; + /** Stable name of the single value column projected by a result-set producer. */ + public static final String RESULT_SET_VALUE_COLUMN = "rs_value"; + + private static final String CTE_VALUE_COL = RESULT_SET_VALUE_COLUMN; /** * Collects the result-set subqueries of one top-level {@code inResultSet} predicate as named, @@ -395,6 +398,15 @@ public String encodeResultSetProducer(InResultSet inResultSet) { return resultSetProducerSelect(inResultSet, false, null); } + /** + * Producer SELECT of a result set for materialization into a table: the value column is aliased + * to {@link #RESULT_SET_VALUE_COLUMN} so the resulting table has a stable column name to index + * and to reference from the consuming filters. + */ + public String encodeResultSetProducerAliased(InResultSet inResultSet) { + return resultSetProducerSelect(inResultSet, true, null); + } + /** * Type of the value column of a result set, used to coerce and render its materialized values. */ @@ -2085,6 +2097,17 @@ private String encodeInResultSet(InResultSet inResultSet, String mainExpression) return "1 = 0"; } + // if the result set has been materialized into a table, reference it directly so the producer + // runs once and each consumer only scans the indexed table + if (inResultSet.getMaterializedTable().isPresent()) { + return String.format( + mainExpression, + "", + String.format( + " IN (SELECT %s FROM %s)", + CTE_VALUE_COL, inResultSet.getMaterializedTable().get())); + } + // if the result set has been materialized up front, inline its values as a literal IN list if (inResultSet.getMaterializedValues().isPresent()) { List values = inResultSet.getMaterializedValues().get(); diff --git a/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/app/ResultSetMaterializer.java b/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/app/ResultSetMaterializer.java index 2424e4107..9f5b963ca 100644 --- a/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/app/ResultSetMaterializer.java +++ b/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/app/ResultSetMaterializer.java @@ -22,6 +22,7 @@ import de.ii.xtraplatform.features.domain.MultiFeatureQuery.SubQuery; import de.ii.xtraplatform.features.domain.SchemaBase; import de.ii.xtraplatform.features.sql.domain.SqlClient; +import de.ii.xtraplatform.features.sql.domain.SqlDialect; import de.ii.xtraplatform.features.sql.domain.SqlQueryOptions; import de.ii.xtraplatform.features.sql.domain.SqlRow; import java.util.ArrayList; @@ -30,9 +31,12 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Supplier; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -43,29 +47,54 @@ * run a single time (its dependencies already materialized as literal id lists), and the collected * values are attached to the {@link InResultSet} nodes of the consuming filters so that they are * encoded as a literal {@code IN} list instead of a per-statement nested subquery. A result set - * that exceeds the configured cap is left unmaterialized and falls back to the inline (CTE) - * encoding. + * that exceeds the configured cap is materialized once into an indexed table that consumers join + * (when the dialect supports it); otherwise it is left unmaterialized and falls back to the inline + * (CTE) re-evaluation. */ public class ResultSetMaterializer { private static final Logger LOGGER = LoggerFactory.getLogger(ResultSetMaterializer.class); + // prefix for the request-scoped tables that hold oversized result sets + private static final String TABLE_PREFIX = "_rs_mat_"; + // monotonic counter making each materialize() call's table names unique within this JVM + private static final AtomicLong SEQUENCE = new AtomicLong(); + private final Supplier sqlClient; private final FilterEncoderSql filterEncoder; private final int maxSetSize; + private final SqlDialect dialect; + // random per-provider-instance token so concurrent instances on the same database cannot collide + private final String instanceId; public ResultSetMaterializer( - Supplier sqlClient, FilterEncoderSql filterEncoder, int maxSetSize) { + Supplier sqlClient, + FilterEncoderSql filterEncoder, + int maxSetSize, + SqlDialect dialect) { this.sqlClient = sqlClient; this.filterEncoder = filterEncoder; this.maxSetSize = maxSetSize; + this.dialect = dialect; + this.instanceId = Integer.toHexString(ThreadLocalRandom.current().nextInt()); } /** * Returns a copy of the query with every materializable result set computed and inlined. If the - * query uses no result sets, it is returned unchanged. + * query uses no result sets, it is returned unchanged. Any tables created for oversized sets are + * named uniquely per call; their names are not tracked and must be dropped by the caller via the + * overload that collects them. */ public MultiFeatureQuery materialize(MultiFeatureQuery query) { + return materialize(query, new ArrayList<>()); + } + + /** + * As {@link #materialize(MultiFeatureQuery)}, but records the names of any tables created for + * oversized result sets into {@code createdTables}. The caller owns their lifecycle and must + * {@link #dropTables(java.util.Collection) drop} them once the query's stream has completed. + */ + public MultiFeatureQuery materialize(MultiFeatureQuery query, List createdTables) { Map sets = new LinkedHashMap<>(); for (SubQuery subQuery : query.getQueries()) { for (Cql2Expression filter : subQuery.getFilters()) { @@ -76,7 +105,12 @@ public MultiFeatureQuery materialize(MultiFeatureQuery query) { return query; } + // one sequence value per call gives every oversized set in this request a name that is unique + // across concurrent requests (and, with the instance token, across provider instances) + long sequence = SEQUENCE.incrementAndGet(); Map> materialized = new HashMap<>(); + // oversized sets materialized into a request-scoped table: set name -> table name + Map materializedTables = new HashMap<>(); int shortCircuited = 0; // materialize level by level: within a level the producers are independent and run concurrently // (bounded by the connection pool). SQL is built single-threaded between levels, so the filter @@ -85,6 +119,10 @@ public MultiFeatureQuery materialize(MultiFeatureQuery query) { for (List level : topologicalLevels(sets)) { Map>> running = new LinkedHashMap<>(); Map valueTypes = new HashMap<>(); + // keep each level's prepared nodes (dependencies already applied) for the join phase, where + // an + // oversized set is materialized into a table from the very same producer + Map prepared = new HashMap<>(); for (String name : level) { InResultSet node = sets.get(name); // if a dependency has already materialized to no members in a position that forces this @@ -96,18 +134,21 @@ && isProvablyEmpty(node.getProducerFilter().get(), materialized)) { shortCircuited++; continue; } - InResultSet prepared = + InResultSet preparedNode = node.getProducerFilter().isPresent() ? new ImmutableInResultSet.Builder() .from(node) - .producerFilter(applyMaterialized(node.getProducerFilter().get(), materialized)) + .producerFilter( + applyMaterialized( + node.getProducerFilter().get(), materialized, materializedTables)) .build() : node; + prepared.put(name, preparedNode); // bound the fetch to one past the cap so an oversized set is detected without loading it // all String producerQuery = - filterEncoder.encodeResultSetProducer(prepared) + " LIMIT " + (maxSetSize + 1); + filterEncoder.encodeResultSetProducer(preparedNode) + " LIMIT " + (maxSetSize + 1); valueTypes.put(name, filterEncoder.resultSetValueType(node)); running.put(name, sqlClient.get().run(producerQuery, SqlQueryOptions.single())); } @@ -116,7 +157,11 @@ && isProvablyEmpty(node.getProducerFilter().get(), materialized)) { String name = entry.getKey(); Collection rows = entry.getValue().join(); if (rows.size() > maxSetSize) { - if (LOGGER.isWarnEnabled()) { + if (dialect.supportsResultSetTables()) { + // too large to inline as a literal list: materialize the producer once into an indexed + // table; consumers reference it instead of re-deriving the producer each time + materializeTable(name, prepared.get(name), sequence, materializedTables, createdTables); + } else if (LOGGER.isWarnEnabled()) { LOGGER.warn( "Result set '{}' has more than the materialization cap of {} members; falling back" + " to inline evaluation for this set.", @@ -150,7 +195,10 @@ && isProvablyEmpty(node.getProducerFilter().get(), materialized)) { .from(subQuery) .filters( subQuery.getFilters().stream() - .map(filter -> applyMaterialized(filter, materialized)) + .map( + filter -> + applyMaterialized( + filter, materialized, materializedTables)) .collect(Collectors.toList())) .build()) .collect(Collectors.toList()); @@ -229,8 +277,57 @@ private static Set dependenciesOf(InResultSet node, Map> materialized) { - return (Cql2Expression) expression.accept(new ApplyMaterialized(materialized)); + Cql2Expression expression, + Map> materialized, + Map materializedTables) { + return (Cql2Expression) + expression.accept(new ApplyMaterialized(materialized, materializedTables)); + } + + /** + * Materializes an oversized producer into an indexed table that consumers reference instead of + * re-deriving the producer. The table name is unique to this materialize call (instance token + + * call sequence + set name), so concurrent requests never collide; the name is recorded in {@code + * createdTables} so the caller can drop it once the query's stream has completed. + */ + private void materializeTable( + String name, + InResultSet prepared, + long sequence, + Map materializedTables, + List createdTables) { + String suffix = name.replaceAll("[^A-Za-z0-9_]", "_").toLowerCase(Locale.ROOT); + if (suffix.length() > 24) { + suffix = suffix.substring(0, 24); + } + String table = TABLE_PREFIX + instanceId + "_" + sequence + "_" + suffix; + SqlClient client = sqlClient.get(); + client + .run( + dialect.createResultSetTable( + table, filterEncoder.encodeResultSetProducerAliased(prepared)), + SqlQueryOptions.ddl()) + .join(); + client + .run( + dialect.createResultSetTableIndex(table, FilterEncoderSql.RESULT_SET_VALUE_COLUMN), + SqlQueryOptions.ddl()) + .join(); + materializedTables.put(name, table); + createdTables.add(table); + } + + /** Drops the given result-set tables, best effort. Safe to call with an empty collection. */ + public void dropTables(java.util.Collection tables) { + for (String table : tables) { + try { + sqlClient.get().run(dialect.dropResultSetTable(table), SqlQueryOptions.ddl()).join(); + } catch (RuntimeException e) { + if (LOGGER.isWarnEnabled()) { + LOGGER.warn("Could not drop result-set table '{}': {}", table, e.getMessage()); + } + } + } } /** @@ -331,12 +428,15 @@ public CqlNode visit(BinaryScalarOperation scalarOperation, List childr } } - /** Attaches materialized values to the {@link InResultSet} nodes that have them. */ + /** Attaches materialized values (or a materialized table) to the {@link InResultSet} nodes. */ private static class ApplyMaterialized extends CqlVisitorCopy { private final Map> materialized; + private final Map materializedTables; - ApplyMaterialized(Map> materialized) { + ApplyMaterialized( + Map> materialized, Map materializedTables) { this.materialized = materialized; + this.materializedTables = materializedTables; } @Override @@ -348,6 +448,10 @@ public CqlNode visit(BinaryScalarOperation scalarOperation, List childr if (values != null) { return new ImmutableInResultSet.Builder().from(node).materializedValues(values).build(); } + String table = materializedTables.get(node.getSetName()); + if (table != null) { + return new ImmutableInResultSet.Builder().from(node).materializedTable(table).build(); + } } return copy; } diff --git a/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/FeatureProviderSql.java b/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/FeatureProviderSql.java index 2d1829a5b..bbf7e6893 100644 --- a/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/FeatureProviderSql.java +++ b/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/FeatureProviderSql.java @@ -72,12 +72,14 @@ import de.ii.xtraplatform.features.domain.ProviderData; import de.ii.xtraplatform.features.domain.ProviderExtensionRegistry; import de.ii.xtraplatform.features.domain.Query; +import de.ii.xtraplatform.features.domain.QueryRunner; import de.ii.xtraplatform.features.domain.SchemaBase; import de.ii.xtraplatform.features.domain.SchemaMapping; import de.ii.xtraplatform.features.domain.SortKey; import de.ii.xtraplatform.features.domain.SourceSchemaValidator; import de.ii.xtraplatform.features.domain.transform.OnlyQueryables; import de.ii.xtraplatform.features.domain.transform.OnlySortables; +import de.ii.xtraplatform.features.domain.transform.PropertyTransformations; import de.ii.xtraplatform.features.sql.ImmutableSqlPathSyntax; import de.ii.xtraplatform.features.sql.SqlPathSyntax; import de.ii.xtraplatform.features.sql.app.AggregateStatsQueryGenerator; @@ -113,6 +115,7 @@ import de.ii.xtraplatform.values.domain.ValueStore; import java.time.ZoneId; import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; @@ -122,6 +125,8 @@ import java.util.Objects; import java.util.Optional; import java.util.OptionalInt; +import java.util.concurrent.CompletionStage; +import java.util.function.BiFunction; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -632,7 +637,8 @@ protected boolean onStartup() throws InterruptedException { new ResultSetMaterializer( this::getSqlClient, filterEncoder, - getData().getQueryGeneration().getResultSetMaterializationMaxSize()); + getData().getQueryGeneration().getResultSetMaterializationMaxSize(), + sqlDialect); this.aggregateStatsReader = new AggregateStatsReaderSql( @@ -1398,7 +1404,13 @@ private MutationResult writeFeatures( return mutationStream.run().toCompletableFuture().join(); } + @Override protected Query preprocessQuery(Query query) { + // result-set tables created here are discarded: callers that need to drop them use the overload + return preprocessQuery(query, new ArrayList<>()); + } + + protected Query preprocessQuery(Query query, List resultSetTables) { if (query instanceof FeatureQuery && (((FeatureQuery) query).getFields().size() > 1 || !"*".equals(((FeatureQuery) query).getFields().get(0)))) { @@ -1462,7 +1474,7 @@ protected Query preprocessQuery(Query query) { // instead // of re-deriving each shared result set in every sub-query statement if (ResultSetMaterializer.hasResultSets(multiQuery)) { - multiQuery = resultSetMaterializer.materialize(multiQuery); + multiQuery = resultSetMaterializer.materialize(multiQuery, resultSetTables); } return multiQuery; @@ -1475,17 +1487,36 @@ protected Query preprocessQuery(Query query) { public FeatureStream getFeatureStream(MultiFeatureQuery query) { validateQuery(query); - Query query2 = preprocessQuery(query); + List resultSetTables = new ArrayList<>(); + Query query2 = preprocessQuery(query, resultSetTables); boolean nativeCrsIs3d = crsInfo.is3d(getData().getNativeCrs().orElse(OgcCrs.CRS84)); + // drop any tables materialized for oversized result sets once the query's stream has completed + QueryRunner runner = + resultSetTables.isEmpty() + ? this::runQuery + : new QueryRunner() { + @Override + public CompletionStage runQuery( + BiFunction, Reactive.Stream> stream, + Query runQuery, + Map propertyTransformations, + boolean passThrough) { + return FeatureProviderSql.this + .runQuery(stream, runQuery, propertyTransformations, passThrough) + .whenComplete( + (result, error) -> resultSetMaterializer.dropTables(resultSetTables)); + } + }; + return new FeatureStreamImpl( query2, getData(), crsTransformerFactory, nativeCrsIs3d, getCodelists(), - this::runQuery, + runner, !query.hitsOnly(), auditLog); } diff --git a/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/SqlDialect.java b/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/SqlDialect.java index a09ee0674..4ba6e8d2f 100644 --- a/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/SqlDialect.java +++ b/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/SqlDialect.java @@ -95,6 +95,30 @@ default String materializedCte(String name, String query) { return name + " AS (" + query + ")"; } + /** + * Whether result sets that exceed the materialization cap can be materialized into a (temporary) + * table that consumers join, instead of being re-derived inline in every consumer. Dialects that + * return {@code false} keep the inline re-evaluation fallback. + */ + default boolean supportsResultSetTables() { + return false; + } + + /** DDL to materialize a result-set producer into a session-independent table once. */ + default String createResultSetTable(String name, String producerSelect) { + throw new UnsupportedOperationException(); + } + + /** DDL to index the value column of a materialized result-set table. */ + default String createResultSetTableIndex(String name, String valueColumn) { + throw new UnsupportedOperationException(); + } + + /** DDL to drop a materialized result-set table if it exists. */ + default String dropResultSetTable(String name) { + throw new UnsupportedOperationException(); + } + String castToBigInt(int value); Optional parseExtent(String extent, EpsgCrs crs); diff --git a/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/SqlDialectPgis.java b/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/SqlDialectPgis.java index a8159237e..97a5e22cd 100644 --- a/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/SqlDialectPgis.java +++ b/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/SqlDialectPgis.java @@ -48,6 +48,29 @@ public String materializedCte(String name, String query) { return name + " AS MATERIALIZED (" + query + ")"; } + @Override + public boolean supportsResultSetTables() { + return true; + } + + @Override + public String createResultSetTable(String name, String producerSelect) { + // UNLOGGED: no WAL, much faster to populate; the table is request-scoped scratch data. It is a + // regular (not session-TEMP) table so it stays visible to the other pooled connections that run + // the consuming sub-queries concurrently. + return String.format("CREATE UNLOGGED TABLE %s AS (%s)", name, producerSelect); + } + + @Override + public String createResultSetTableIndex(String name, String valueColumn) { + return String.format("CREATE INDEX ON %s (%s)", name, valueColumn); + } + + @Override + public String dropResultSetTable(String name) { + return String.format("DROP TABLE IF EXISTS %s", name); + } + private static final Splitter BBOX_SPLITTER = Splitter.onPattern("[(), ]").omitEmptyStrings().trimResults(); private static final Map SPATIAL_OPERATORS_3D = diff --git a/xtraplatform-features-sql/src/test/groovy/de/ii/xtraplatform/features/sql/app/ResultSetMaterializerSpec.groovy b/xtraplatform-features-sql/src/test/groovy/de/ii/xtraplatform/features/sql/app/ResultSetMaterializerSpec.groovy index 567e531a0..81749d012 100644 --- a/xtraplatform-features-sql/src/test/groovy/de/ii/xtraplatform/features/sql/app/ResultSetMaterializerSpec.groovy +++ b/xtraplatform-features-sql/src/test/groovy/de/ii/xtraplatform/features/sql/app/ResultSetMaterializerSpec.groovy @@ -26,6 +26,7 @@ import de.ii.xtraplatform.features.json.app.DecoderFactoryJson import de.ii.xtraplatform.features.sql.domain.ImmutableQueryGeneratorSettings import de.ii.xtraplatform.features.sql.domain.ImmutableSqlPathDefaults import de.ii.xtraplatform.features.sql.domain.SqlClient +import de.ii.xtraplatform.features.sql.domain.SqlDialect import de.ii.xtraplatform.features.sql.domain.SqlDialectPgis import de.ii.xtraplatform.features.sql.domain.SqlPathParser import de.ii.xtraplatform.features.sql.domain.SqlQueryMapping @@ -91,7 +92,7 @@ class ResultSetMaterializerSpec extends Specification { given: def filter = resolved("s1", "simple", Eq.of(Property.of("id"), ScalarLiteral.of("foo"))) def sqlClient = Mock(SqlClient) - def materializer = new ResultSetMaterializer({ -> sqlClient } as Supplier, filterEncoder, 100000) + def materializer = new ResultSetMaterializer({ -> sqlClient } as Supplier, filterEncoder, 100000, new SqlDialectPgis()) when: def result = materializer.materialize(query(filter)) @@ -102,11 +103,48 @@ class ResultSetMaterializerSpec extends Specification { node.getMaterializedValues().get() == ["x", "y"] } - def 'a result set exceeding the cap is left unmaterialized'() { + def 'a result set exceeding the cap is materialized into a uniquely named table'() { given: def filter = resolved("s1", "simple", Eq.of(Property.of("id"), ScalarLiteral.of("foo"))) def sqlClient = Mock(SqlClient) - def materializer = new ResultSetMaterializer({ -> sqlClient } as Supplier, filterEncoder, 1) + def created = [] + def materializer = new ResultSetMaterializer({ -> sqlClient } as Supplier, filterEncoder, 1, new SqlDialectPgis()) + + when: + def result = materializer.materialize(query(filter), created) + + then: + // detection query returns more than the cap -> producer is materialized into a table once + // (no pre-drop: the name is unique per call) + 1 * sqlClient.run({ it.contains("LIMIT 2") }, _) >> CompletableFuture.completedFuture([row("x"), row("y")]) + 1 * sqlClient.run({ it.startsWith("CREATE UNLOGGED TABLE _rs_mat_") && it.contains("_s1 AS") }, _) >> CompletableFuture.completedFuture([]) + 1 * sqlClient.run({ it.startsWith("CREATE INDEX ON _rs_mat_") }, _) >> CompletableFuture.completedFuture([]) + 0 * sqlClient.run({ it.startsWith("DROP TABLE") }, _) + def node = (InResultSet) result.getQueries().get(0).getFilters().get(0) + node.getMaterializedTable().get() ==~ /_rs_mat_[0-9a-f]+_\d+_s1/ + node.getMaterializedValues().isEmpty() + created == [node.getMaterializedTable().get()] + } + + def 'dropTables drops each collected table'() { + given: + def sqlClient = Mock(SqlClient) + def materializer = new ResultSetMaterializer({ -> sqlClient } as Supplier, filterEncoder, 1, new SqlDialectPgis()) + + when: + materializer.dropTables(["_rs_mat_a_1_s1", "_rs_mat_a_1_s2"]) + + then: + 1 * sqlClient.run({ it == "DROP TABLE IF EXISTS _rs_mat_a_1_s1" }, _) >> CompletableFuture.completedFuture([]) + 1 * sqlClient.run({ it == "DROP TABLE IF EXISTS _rs_mat_a_1_s2" }, _) >> CompletableFuture.completedFuture([]) + } + + def 'a result set exceeding the cap is left unmaterialized when the dialect has no table support'() { + given: + def filter = resolved("s1", "simple", Eq.of(Property.of("id"), ScalarLiteral.of("foo"))) + def sqlClient = Mock(SqlClient) + def noTables = Stub(SqlDialect) { supportsResultSetTables() >> false } + def materializer = new ResultSetMaterializer({ -> sqlClient } as Supplier, filterEncoder, 1, noTables) when: def result = materializer.materialize(query(filter)) @@ -115,6 +153,7 @@ class ResultSetMaterializerSpec extends Specification { 1 * sqlClient.run(_, _) >> CompletableFuture.completedFuture([row("x"), row("y")]) def node = (InResultSet) result.getQueries().get(0).getFilters().get(0) node.getMaterializedValues().isEmpty() + node.getMaterializedTable().isEmpty() } def 'a producer whose dependency materialized empty is short-circuited and not run'() { @@ -122,7 +161,7 @@ class ResultSetMaterializerSpec extends Specification { def s1 = resolved("s1", "simple", Eq.of(Property.of("id"), ScalarLiteral.of("foo"))) def s2 = resolved("s2", "simple", s1) def sqlClient = Mock(SqlClient) - def materializer = new ResultSetMaterializer({ -> sqlClient } as Supplier, filterEncoder, 100000) + def materializer = new ResultSetMaterializer({ -> sqlClient } as Supplier, filterEncoder, 100000, new SqlDialectPgis()) when: def result = materializer.materialize(query(s2)) @@ -139,7 +178,7 @@ class ResultSetMaterializerSpec extends Specification { def s1 = resolved("s1", "simple", Eq.of(Property.of("id"), ScalarLiteral.of("foo"))) def s2 = resolved("s2", "simple", s1) def sqlClient = Mock(SqlClient) - def materializer = new ResultSetMaterializer({ -> sqlClient } as Supplier, filterEncoder, 100000) + def materializer = new ResultSetMaterializer({ -> sqlClient } as Supplier, filterEncoder, 100000, new SqlDialectPgis()) when: def result = materializer.materialize(query(s2)) @@ -159,7 +198,7 @@ class ResultSetMaterializerSpec extends Specification { def s2 = resolved("s2", "simple", Or.of([s1, Eq.of(Property.of("id"), ScalarLiteral.of("bar"))] as List)) def sqlClient = Mock(SqlClient) - def materializer = new ResultSetMaterializer({ -> sqlClient } as Supplier, filterEncoder, 100000) + def materializer = new ResultSetMaterializer({ -> sqlClient } as Supplier, filterEncoder, 100000, new SqlDialectPgis()) when: def result = materializer.materialize(query(s2)) @@ -178,7 +217,7 @@ class ResultSetMaterializerSpec extends Specification { given: def filter = Eq.of(Property.of("id"), ScalarLiteral.of("foo")) def sqlClient = Mock(SqlClient) - def materializer = new ResultSetMaterializer({ -> sqlClient } as Supplier, filterEncoder, 100000) + def materializer = new ResultSetMaterializer({ -> sqlClient } as Supplier, filterEncoder, 100000, new SqlDialectPgis()) when: def result = materializer.materialize(query(filter))