From cd8804e2c81b33a8092646c4232b79e7e52d2777 Mon Sep 17 00:00:00 2001 From: Luigi Dell'Aquila Date: Thu, 23 Nov 2023 16:26:36 +0100 Subject: [PATCH 1/6] Fix LIMIT pushdown with MV_EXPAND --- .../src/main/resources/mv_expand.csv-spec | 26 ++++++ .../xpack/esql/analysis/Analyzer.java | 3 +- .../xpack/esql/io/stream/PlanNamedTypes.java | 3 +- .../esql/optimizer/LogicalPlanOptimizer.java | 46 +++++++++- .../xpack/esql/parser/LogicalPlanBuilder.java | 2 +- .../xpack/esql/plan/logical/MvExpand.java | 19 ++-- .../esql/io/stream/PlanNamedTypesTests.java | 2 +- .../optimizer/LogicalPlanOptimizerTests.java | 90 ++++++++++++++++--- 8 files changed, 168 insertions(+), 23 deletions(-) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mv_expand.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mv_expand.csv-spec index c681a1a7e977c..b583e37fb5e02 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mv_expand.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mv_expand.csv-spec @@ -306,3 +306,29 @@ a:long | b:long | c:long | gender:keyword | str:keyword | x:key 57 |57 |57 |M |"57,M" |M 0 |10 |10 |null |null |null ; + + +// see https://github.com/elastic/elasticsearch/issues/102061 +sortMvExpand#[skip:-8.11.99] +row a = 1 | sort a | mv_expand a; + +a:integer +1 +; + + +// see https://github.com/elastic/elasticsearch/issues/102061 +limitSortMvExpand#[skip:-8.11.99] +row a = 1 | limit 1 | sort a | mv_expand a; + +a:integer +1 +; + + +//see https://github.com/elastic/elasticsearch/issues/102084 +whereMvExpand#[skip:-8.11.99] +row a = 1, b = -15 | where b > 3 | mv_expand b; + +a:integer | b:integer +; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java index 557da9639a086..84afa662f8327 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java @@ -342,7 +342,8 @@ private LogicalPlan resolveMvExpand(MvExpand p, List childrenOutput) p.source(), p.child(), resolved, - new ReferenceAttribute(resolved.source(), resolved.name(), resolved.dataType(), null, resolved.nullable(), null, false) + new ReferenceAttribute(resolved.source(), resolved.name(), resolved.dataType(), null, resolved.nullable(), null, false), + p.limit() ); } return p; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypes.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypes.java index 20ec1ac410f64..460b781ff0142 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypes.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypes.java @@ -781,7 +781,8 @@ static void writeLimit(PlanStreamOutput out, Limit limit) throws IOException { } static MvExpand readMvExpand(PlanStreamInput in) throws IOException { - return new MvExpand(in.readSource(), in.readLogicalPlanNode(), in.readNamedExpression(), in.readAttribute()); + // last parameter (limit) is a local value that is needed only during the logical planning, no need to serialize/deserialize it + return new MvExpand(in.readSource(), in.readLogicalPlanNode(), in.readNamedExpression(), in.readAttribute(), -1); } static void writeMvExpand(PlanStreamOutput out, MvExpand mvExpand) throws IOException { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java index 3ae19ceef4d08..934e4d128ff60 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java @@ -155,10 +155,10 @@ protected static List> rules() { new PushDownAndCombineLimits(), new ReplaceLimitAndSortAsTopN() ); - var defaultTopN = new Batch<>("Add default TopN", new AddDefaultTopN()); + var defaultLimits = new Batch<>("Add default limits", new AddLimitToMvExpand(), new AddDefaultTopN()); var label = new Batch<>("Set as Optimized", Limiter.ONCE, new SetAsOptimized()); - return asList(substitutions, operators, skip, cleanup, defaultTopN, label); + return asList(substitutions, operators, skip, cleanup, defaultLimits, label); } // TODO: currently this rule only works for aggregate functions (AVG) @@ -438,6 +438,13 @@ protected LogicalPlan rule(Limit limit) { } else if (limit.child() instanceof UnaryPlan unary) { if (unary instanceof Eval || unary instanceof Project || unary instanceof RegexExtract || unary instanceof Enrich) { return unary.replaceChild(limit.replaceChild(unary.child())); + } else if (unary instanceof MvExpand mvx) { + var limitSource = limit.limit(); + var limitVal = (int) limitSource.fold(); + if (mvx.limit() < 0 || mvx.limit() > limitVal) { + mvx = new MvExpand(mvx.source(), mvx.child(), mvx.target(), mvx.expanded(), limitVal); + } + return mvx.replaceChild(limit.replaceChild(mvx.child())); } // check if there's a 'visible' descendant limit lower than the current one // and if so, align the current limit since it adds no value @@ -1050,6 +1057,41 @@ protected LogicalPlan rule(LogicalPlan plan, LogicalOptimizerContext context) { } } + /** + * This is not an optimization: this is last step of pushing LIMIT past MV_EXPAND, ie. + * + * 1. PushDownAndCombineLimits: "mv_expand x (unbounded) | limit y" -> "limit y | mv_expand x (with inner limit y)" + * 2. further pushdown of LIMIT (eg. needed for TopN) + * ... further rules + * 3. GO TO 1 (normal batch loop) + * 4. only once AddLimitToMvExpand: "mv_expand x (with inner limit y)" -> "mv_expand x (unbounded) | limit y" + * + * The final limit has to be added again because the implementation of MV_EXPAND operator is unbounded + * + * The reason why this cannot be a single rule + * (eg. "mv_expand x | limit y" -> "limit y | mv_expand x | limit y" ) + * is that LIMIT push-down (PushDownAndCombineLimits) is executed multiple times, and it would result in an infinite loop + * + */ + static class AddLimitToMvExpand extends OptimizerRules.OptimizerRule { + AddLimitToMvExpand() { + super(TransformDirection.UP); + } + + @Override + protected LogicalPlan rule(MvExpand plan) { + if (plan.limit() >= 0) { + return new Limit( + plan.source(), + new Literal(Source.EMPTY, plan.limit(), DataTypes.INTEGER), + new MvExpand(plan.source(), plan.child(), plan.target(), plan.expanded(), -1) + ); + } + return plan; + } + + } + public static class ReplaceRegexMatch extends OptimizerRules.ReplaceRegexMatch { protected Expression regexToEquals(RegexMatch regexMatch, Literal literal) { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/LogicalPlanBuilder.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/LogicalPlanBuilder.java index 49caf0e4618bd..429ef25268952 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/LogicalPlanBuilder.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/LogicalPlanBuilder.java @@ -151,7 +151,7 @@ public PlanFactory visitDissectCommand(EsqlBaseParser.DissectCommandContext ctx) public PlanFactory visitMvExpandCommand(EsqlBaseParser.MvExpandCommandContext ctx) { String identifier = visitSourceIdentifier(ctx.sourceIdentifier()); Source src = source(ctx); - return child -> new MvExpand(src, child, new UnresolvedAttribute(src, identifier), new UnresolvedAttribute(src, identifier)); + return child -> new MvExpand(src, child, new UnresolvedAttribute(src, identifier), new UnresolvedAttribute(src, identifier), -1); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/MvExpand.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/MvExpand.java index 17f669b5d30b3..d808385f03889 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/MvExpand.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/MvExpand.java @@ -24,11 +24,14 @@ public class MvExpand extends UnaryPlan { private final List output; - public MvExpand(Source source, LogicalPlan child, NamedExpression target, Attribute expanded) { + private final int limit; + + public MvExpand(Source source, LogicalPlan child, NamedExpression target, Attribute expanded, int limit) { super(source, child); this.target = target; this.expanded = expanded; this.output = calculateOutput(child.output(), target, expanded); + this.limit = limit; } public static List calculateOutput(List input, NamedExpression target, Attribute expanded) { @@ -58,7 +61,7 @@ public boolean expressionsResolved() { @Override public UnaryPlan replaceChild(LogicalPlan newChild) { - return new MvExpand(source(), newChild, target, expanded); + return new MvExpand(source(), newChild, target, expanded, limit); } @Override @@ -66,14 +69,18 @@ public List output() { return output; } + public int limit() { + return limit; + } + @Override protected NodeInfo info() { - return NodeInfo.create(this, MvExpand::new, child(), target, expanded); + return NodeInfo.create(this, MvExpand::new, child(), target, expanded, limit); } @Override public int hashCode() { - return Objects.hash(super.hashCode(), target, expanded); + return Objects.hash(super.hashCode(), target, expanded, limit); } @Override @@ -81,6 +88,8 @@ public boolean equals(Object obj) { if (false == super.equals(obj)) { return false; } - return Objects.equals(target, ((MvExpand) obj).target) && Objects.equals(expanded, ((MvExpand) obj).expanded); + return Objects.equals(target, ((MvExpand) obj).target) + && Objects.equals(expanded, ((MvExpand) obj).expanded) + && limit == ((MvExpand) obj).limit; } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypesTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypesTests.java index 85612427a1867..7ce8321a60d86 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypesTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypesTests.java @@ -488,7 +488,7 @@ public void testEsqlProject() throws IOException { public void testMvExpand() throws IOException { var esRelation = new EsRelation(Source.EMPTY, randomEsIndex(), List.of(randomFieldAttribute()), randomBoolean()); - var orig = new MvExpand(Source.EMPTY, esRelation, randomFieldAttribute(), randomFieldAttribute()); + var orig = new MvExpand(Source.EMPTY, esRelation, randomFieldAttribute(), randomFieldAttribute(), -1); BytesStreamOutput bso = new BytesStreamOutput(); PlanStreamOutput out = new PlanStreamOutput(bso, planNameRegistry); PlanNamedTypes.writeMvExpand(out, orig); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java index b82bb46ec103e..636f11da8bd62 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java @@ -968,17 +968,18 @@ public void testDontPushDownLimitPastMvExpand() { /** * Expected - * EsqlProject[[emp_no{f}#141, first_name{f}#142, languages{f}#143, lll{r}#132, salary{f}#147]] - * \_TopN[[Order[salary{f}#147,DESC,FIRST], Order[first_name{f}#142,ASC,LAST]],5[INTEGER]] - * \_Limit[5[INTEGER]] - * \_MvExpand[salary{f}#147] - * \_Eval[[languages{f}#143 + 5[INTEGER] AS lll]] - * \_Filter[languages{f}#143 > 1[INTEGER]] - * \_Limit[10[INTEGER]] - * \_MvExpand[first_name{f}#142] - * \_TopN[[Order[emp_no{f}#141,DESC,FIRST]],10[INTEGER]] - * \_Filter[emp_no{f}#141 < 10006[INTEGER]] - * \_EsRelation[test][emp_no{f}#141, first_name{f}#142, languages{f}#1..] + * EsqlProject[[emp_no{f}#19, first_name{r}#29, languages{f}#22, lll{r}#8, salary{r}#30]] + * \_TopN[[Order[salary{r}#30,DESC,FIRST]],5[INTEGER]] + * \_Limit[5[INTEGER]] + * \_MvExpand[salary{f}#24,salary{r}#30,2147483647] + * \_Eval[[languages{f}#22 + 5[INTEGER] AS lll]] + * \_Limit[5[INTEGER]] + * \_Filter[languages{f}#22 > 1[INTEGER]] + * \_Limit[10[INTEGER]] + * \_MvExpand[first_name{f}#20,first_name{r}#29,2147483647] + * \_TopN[[Order[emp_no{f}#19,DESC,FIRST]],10[INTEGER]] + * \_Filter[emp_no{f}#19 ≤ 10006[INTEGER]] + * \_EsRelation[test][_meta_field{f}#25, emp_no{f}#19, first_name{f}#20, ..] */ public void testMultipleMvExpandWithSortAndLimit() { LogicalPlan plan = optimizedPlan(""" @@ -1003,7 +1004,8 @@ public void testMultipleMvExpandWithSortAndLimit() { assertThat(limit.limit().fold(), equalTo(5)); var mvExp = as(limit.child(), MvExpand.class); var eval = as(mvExp.child(), Eval.class); - var filter = as(eval.child(), Filter.class); + var limit5 = as(eval.child(), Limit.class); + var filter = as(limit5.child(), Filter.class); limit = as(filter.child(), Limit.class); assertThat(limit.limit().fold(), equalTo(10)); mvExp = as(limit.child(), MvExpand.class); @@ -2454,6 +2456,70 @@ public void testMvExpandFoldable() { var row = as(expand.child(), Row.class); } + /** + * Expected: + * Limit[500[INTEGER]] + * \_MvExpand[a{r}#2,a{r}#6,2147483647] + * \_TopN[[Order[a{r}#2,ASC,LAST]],500[INTEGER]] + * \_Row[[1[INTEGER] AS a]] + */ + public void testSortMvExpand() { + LogicalPlan plan = optimizedPlan(""" + row a = 1 + | sort a + | mv_expand a"""); + + var limit = as(plan, Limit.class); + var expand = as(limit.child(), MvExpand.class); + var topN = as(expand.child(), TopN.class); + var row = as(topN.child(), Row.class); + } + + /** + * Expected: + * Limit[20[INTEGER]] + * \_MvExpand[emp_no{f}#4,emp_no{r}#14,-1] + * \_TopN[[Order[emp_no{f}#4,ASC,LAST]],20[INTEGER]] + * \_EsRelation[test][_meta_field{f}#10, emp_no{f}#4, first_name{f}#5, ge..] + */ + public void testSortMvExpandLimit() { + LogicalPlan plan = optimizedPlan(""" + from test + | sort emp_no + | mv_expand emp_no + | limit 20"""); + + var limit = as(plan, Limit.class); + assertThat(limit.limit().fold(), is(20)); + var expand = as(limit.child(), MvExpand.class); + var topN = as(expand.child(), TopN.class); + assertThat(topN.limit().fold(), is(20)); + var row = as(topN.child(), EsRelation.class); + } + + /** + * Expected: + * Limit[500[INTEGER]] + * \_MvExpand[b{r}#4,b{r}#8,-1] + * \_Limit[500[INTEGER]] + * \_Row[[1[INTEGER] AS a, -15[INTEGER] AS b]] + * + * see https://github.com/elastic/elasticsearch/issues/102084 + */ + public void testWhereMvExpand() { + LogicalPlan plan = optimizedPlan(""" + row a = 1, b = -15 + | where b < 3 + | mv_expand b"""); + + var limit = as(plan, Limit.class); + assertThat(limit.limit().fold(), is(500)); + var expand = as(limit.child(), MvExpand.class); + var limit2 = as(expand.child(), Limit.class); + assertThat(limit2.limit().fold(), is(500)); + var row = as(limit2.child(), Row.class); + } + /** * Expected * Limit[500[INTEGER]] From 791d48bef8f81e63bdf862a71d1ca76b5cc8af82 Mon Sep 17 00:00:00 2001 From: Luigi Dell'Aquila Date: Thu, 23 Nov 2023 16:37:54 +0100 Subject: [PATCH 2/6] Add one more test --- .../src/main/resources/mv_expand.csv-spec | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mv_expand.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mv_expand.csv-spec index b583e37fb5e02..89d3e6fea14f0 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mv_expand.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mv_expand.csv-spec @@ -326,6 +326,17 @@ a:integer ; +// see https://github.com/elastic/elasticsearch/issues/102061 +limitSortMultipleMvExpand#[skip:-8.11.99] +row a = [1, 2, 3, 4, 5], b = 2, c = 3 | sort a | mv_expand a | mv_expand b | mv_expand c | limit 3; + +a:integer | b:integer | c:integer +1 | 2 | 3 +2 | 2 | 3 +3 | 2 | 3 +; + + //see https://github.com/elastic/elasticsearch/issues/102084 whereMvExpand#[skip:-8.11.99] row a = 1, b = -15 | where b > 3 | mv_expand b; From 1dc90092b4900c3195378ced1457ce3d58232843 Mon Sep 17 00:00:00 2001 From: Luigi Dell'Aquila Date: Thu, 23 Nov 2023 16:43:26 +0100 Subject: [PATCH 3/6] Add more tests --- .../src/main/resources/mv_expand.csv-spec | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mv_expand.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mv_expand.csv-spec index 89d3e6fea14f0..ba3db9eb79b7c 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mv_expand.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mv_expand.csv-spec @@ -337,6 +337,24 @@ a:integer | b:integer | c:integer ; +multipleLimitSortMultipleMvExpand#[skip:-8.11.99] +row a = [1, 2, 3, 4, 5], b = 2, c = 3 | sort a | mv_expand a | limit 2 | mv_expand b | mv_expand c | limit 3; + +a:integer | b:integer | c:integer +1 | 2 | 3 +2 | 2 | 3 +; + + +multipleLimitSortMultipleMvExpand2#[skip:-8.11.99] +row a = [1, 2, 3, 4, 5], b = 2, c = 3 | sort a | mv_expand a | limit 3 | mv_expand b | mv_expand c | limit 2; + +a:integer | b:integer | c:integer +1 | 2 | 3 +2 | 2 | 3 +; + + //see https://github.com/elastic/elasticsearch/issues/102084 whereMvExpand#[skip:-8.11.99] row a = 1, b = -15 | where b > 3 | mv_expand b; From f8d69719daf08c889df45b3ae43a44562f385b6a Mon Sep 17 00:00:00 2001 From: Luigi Dell'Aquila Date: Thu, 23 Nov 2023 16:44:05 +0100 Subject: [PATCH 4/6] Update docs/changelog/102545.yaml --- docs/changelog/102545.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 docs/changelog/102545.yaml diff --git a/docs/changelog/102545.yaml b/docs/changelog/102545.yaml new file mode 100644 index 0000000000000..bd2743397d1e4 --- /dev/null +++ b/docs/changelog/102545.yaml @@ -0,0 +1,7 @@ +pr: 102545 +summary: Fix LIMIT pushdown with MV_EXPAND +area: ES|QL +type: bug +issues: + - 102084 + - 102061 From b2dbef0c0703ff420dcde5f4916248e6f67a17d0 Mon Sep 17 00:00:00 2001 From: Luigi Dell'Aquila Date: Mon, 27 Nov 2023 09:25:03 +0100 Subject: [PATCH 5/6] Add assert to avoid leaking the implicit mv_expand limit to physical level --- .../main/java/org/elasticsearch/xpack/esql/planner/Mapper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/Mapper.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/Mapper.java index 3eea84b0bd1f9..1b7745d4418d9 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/Mapper.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/Mapper.java @@ -151,6 +151,7 @@ private PhysicalPlan map(UnaryPlan p, PhysicalPlan child) { } if (p instanceof MvExpand mvExpand) { + assert mvExpand.limit() < 0; return new MvExpandExec(mvExpand.source(), map(mvExpand.child()), mvExpand.target(), mvExpand.expanded()); } From f06756c83ca5e0a0a86707beffc945472ab614e1 Mon Sep 17 00:00:00 2001 From: Luigi Dell'Aquila Date: Tue, 2 Jan 2024 13:48:22 +0100 Subject: [PATCH 6/6] Fix merge --- .../java/org/elasticsearch/xpack/esql/plan/logical/MvExpand.java | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/MvExpand.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/MvExpand.java index 327840a23817e..9a38ece8d0c4d 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/MvExpand.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/MvExpand.java @@ -30,7 +30,6 @@ public MvExpand(Source source, LogicalPlan child, NamedExpression target, Attrib super(source, child); this.target = target; this.expanded = expanded; - this.output = calculateOutput(child.output(), target, expanded); this.limit = limit; }