diff --git a/pom.xml b/pom.xml index 3c227f4..fad61bc 100644 --- a/pom.xml +++ b/pom.xml @@ -24,6 +24,7 @@ parser linter + rewriter diff --git a/rewriter/pom.xml b/rewriter/pom.xml new file mode 100644 index 0000000..879629b --- /dev/null +++ b/rewriter/pom.xml @@ -0,0 +1,175 @@ + + + 4.0.0 + + + com.facebook.presto + presto-coresql + 0.2-SNAPSHOT + + + presto-coresql-rewriter + presto-coresql-rewriter + + + ${project.parent.basedir} + 1.6 + 1.6 + + + + + junit + junit + 4.12 + test + + + + org.testng + testng + test + + + + com.facebook.presto + presto-coresql-parser + 0.2-SNAPSHOT + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + generate-sources + + add-source + + + + ${project.build.directory}/generated-sources + + + + + + + + com.facebook.presto + presto-maven-plugin + 0.3 + true + + + + org.apache.maven.plugins + maven-shade-plugin + 3.1.1 + + + + org.skife.maven + really-executable-jar-maven-plugin + 1.0.5 + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.8 + + + + io.airlift.maven.plugins + sphinx-maven-plugin + 2.1 + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + + + + org.alluxio:alluxio-shaded-client + org.codehaus.plexus:plexus-utils + com.google.guava:guava + + + + + + + + org.apache.maven.plugins + maven-release-plugin + + clean verify -DskipTests + + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + + -verbose + -J-Xss100M + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*.java + target/**/*.java + **/Benchmark*.java + + + **/*jmhTest*.java + **/*jmhType*.java + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + true + + + true + true + false + + + + + + + + + org.gaul + modernizer-maven-plugin + 2.1.0 + + 1.8 + + + + + + diff --git a/rewriter/src/main/java/com/facebook/coresql/rewriter/PatternMatcher.java b/rewriter/src/main/java/com/facebook/coresql/rewriter/PatternMatcher.java new file mode 100644 index 0000000..a4a4c35 --- /dev/null +++ b/rewriter/src/main/java/com/facebook/coresql/rewriter/PatternMatcher.java @@ -0,0 +1,30 @@ +/* + * Licensed 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. + */ + +package com.facebook.coresql.rewriter; + +import com.facebook.coresql.parser.AstNode; + +import java.util.Set; + +public interface PatternMatcher +{ + /** + * Traverses an AST, finding the set of nodes that match this pattern matcher's pattern + * + * @param node root of the AST + * @return The set of nodes that match this pattern matcher's pattern + */ + Set findMatchedNodes(AstNode node); +} diff --git a/rewriter/src/main/java/com/facebook/coresql/rewriter/RewriteResult.java b/rewriter/src/main/java/com/facebook/coresql/rewriter/RewriteResult.java new file mode 100644 index 0000000..7bef822 --- /dev/null +++ b/rewriter/src/main/java/com/facebook/coresql/rewriter/RewriteResult.java @@ -0,0 +1,46 @@ +/* + * Licensed 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. + */ + +package com.facebook.coresql.rewriter; + +import static java.util.Objects.requireNonNull; + +public class RewriteResult +{ + private String nameOfRewrite; + private String originalSql; + private String rewrittenSql; + + public RewriteResult(String nameOfRewrite, String originalSql, String rewrittenSql) + { + this.nameOfRewrite = requireNonNull(nameOfRewrite, "name of rewrite is null"); + this.originalSql = requireNonNull(originalSql, "original sql statement is null"); + this.rewrittenSql = requireNonNull(rewrittenSql, "rewritten sql statement is null"); + } + + public String getNameOfRewrite() + { + return nameOfRewrite; + } + + public String getOriginalSql() + { + return originalSql; + } + + public String getRewrittenSql() + { + return rewrittenSql; + } +} diff --git a/rewriter/src/main/java/com/facebook/coresql/rewriter/Rewriter.java b/rewriter/src/main/java/com/facebook/coresql/rewriter/Rewriter.java new file mode 100644 index 0000000..e40a7b2 --- /dev/null +++ b/rewriter/src/main/java/com/facebook/coresql/rewriter/Rewriter.java @@ -0,0 +1,23 @@ +/* + * Licensed 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. + */ + +package com.facebook.coresql.rewriter; + +import com.facebook.coresql.parser.Unparser; + +public abstract class Rewriter + extends Unparser +{ + public abstract RewriteResult rewrite(String originalSql); +} diff --git a/rewriter/src/main/java/com/facebook/coresql/rewriter/approxpercentile/ApproxPercentilePatternMatcher.java b/rewriter/src/main/java/com/facebook/coresql/rewriter/approxpercentile/ApproxPercentilePatternMatcher.java new file mode 100644 index 0000000..32246c9 --- /dev/null +++ b/rewriter/src/main/java/com/facebook/coresql/rewriter/approxpercentile/ApproxPercentilePatternMatcher.java @@ -0,0 +1,122 @@ +/* + * Licensed 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. + */ + +package com.facebook.coresql.rewriter.approxpercentile; + +import com.facebook.coresql.parser.AstNode; +import com.facebook.coresql.parser.Select; +import com.facebook.coresql.parser.SqlParserDefaultVisitor; +import com.facebook.coresql.rewriter.PatternMatcher; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.facebook.coresql.parser.SqlParserTreeConstants.JJTARGUMENTLIST; +import static com.facebook.coresql.parser.SqlParserTreeConstants.JJTFUNCTIONCALL; +import static com.facebook.coresql.parser.SqlParserTreeConstants.JJTIDENTIFIER; +import static com.facebook.coresql.parser.SqlParserTreeConstants.JJTSELECTLIST; +import static com.facebook.coresql.parser.SqlParserTreeConstants.JJTUNSIGNEDNUMERICLITERAL; +import static java.util.Objects.requireNonNull; + +public class ApproxPercentilePatternMatcher + extends SqlParserDefaultVisitor + implements PatternMatcher +{ + private final Map>> selectMap; // A map of SelectNode to a map of each unique first arg that has duplicate APPROX_PERCENTILE nodes + private boolean nonLiteralSecondArgFound; + + public ApproxPercentilePatternMatcher() + { + selectMap = new HashMap<>(); + } + + private boolean isLiteral(AstNode node) + { + return node.getId() == JJTUNSIGNEDNUMERICLITERAL; + } + + private void processSelectItem(AstNode node, Map> map) + { + AstNode functionCall = node.GetFirstChildOfKind(JJTFUNCTIONCALL); + if (functionCall == null) { + return; + } + AstNode identifier = functionCall.GetFirstChildOfKind(JJTIDENTIFIER); + if (identifier == null || !identifier.GetImage().equalsIgnoreCase("APPROX_PERCENTILE")) { + return; + } + AstNode argList = functionCall.GetFirstChildOfKind(JJTARGUMENTLIST); + AstNode firstArg = (AstNode) argList.jjtGetChild(0); + AstNode secondArg = (AstNode) argList.jjtGetChild(1); + if (!isLiteral(secondArg)) { + nonLiteralSecondArgFound = true; + return; + } + map.putIfAbsent(firstArg.GetImage(), new LinkedList<>()); + map.get(firstArg.GetImage()).add(node); + } + + private boolean hasMultipleApproxPercentilesWithSameArgs(Map> map) + { + if (nonLiteralSecondArgFound) { + nonLiteralSecondArgFound = false; + return false; + } + return map.keySet().size() == 1 && map.get(map.keySet().iterator().next()).size() > 1; + } + + private Map> processSelectList(AstNode node) + { + Map> map = new HashMap<>(); + for (int i = 0; i < node.NumChildren(); i++) { + processSelectItem(node.GetChild(i), map); + } + if (hasMultipleApproxPercentilesWithSameArgs(map)) { + return map; + } + return null; + } + + public void processSelectNode(AstNode node) + { + AstNode selectList = node.GetFirstChildOfKind(JJTSELECTLIST); + Map> map = processSelectList(selectList); + if (map != null) { + selectMap.put(node, map); + } + } + + @Override + public void visit(Select node, Void data) + { + processSelectNode(node); + defaultVisit(node, data); + } + + @Override + public Set findMatchedNodes(AstNode node) + { + return null; + } + + public Map>> getSelectMap(AstNode root) + { + requireNonNull(root, "AST passed to pattern matcher was null"); + root.jjtAccept(this, null); + return selectMap; + } +} diff --git a/rewriter/src/main/java/com/facebook/coresql/rewriter/approxpercentile/ApproxPercentileRewriter.java b/rewriter/src/main/java/com/facebook/coresql/rewriter/approxpercentile/ApproxPercentileRewriter.java new file mode 100644 index 0000000..418bdec --- /dev/null +++ b/rewriter/src/main/java/com/facebook/coresql/rewriter/approxpercentile/ApproxPercentileRewriter.java @@ -0,0 +1,152 @@ +/* + * Licensed 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. + */ + +package com.facebook.coresql.rewriter.approxpercentile; + +import com.facebook.coresql.parser.AstNode; +import com.facebook.coresql.parser.Select; +import com.facebook.coresql.parser.Unparser; +import com.facebook.coresql.rewriter.RewriteResult; +import com.facebook.coresql.rewriter.Rewriter; + +import java.util.Formatter; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import static com.facebook.coresql.parser.ParserHelper.parseStatement; +import static com.facebook.coresql.parser.SqlParserTreeConstants.JJTALIAS; +import static com.facebook.coresql.parser.SqlParserTreeConstants.JJTARGUMENTLIST; +import static com.facebook.coresql.parser.SqlParserTreeConstants.JJTFROMCLAUSE; +import static com.facebook.coresql.parser.SqlParserTreeConstants.JJTFUNCTIONCALL; +import static com.facebook.coresql.parser.SqlParserTreeConstants.JJTIDENTIFIER; +import static com.facebook.coresql.parser.SqlParserTreeConstants.JJTSELECTLIST; +import static com.facebook.coresql.parser.SqlParserTreeConstants.JJTTABLEEXPRESSION; +import static com.facebook.coresql.parser.SqlParserTreeConstants.JJTTABLENAME; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; + +public class ApproxPercentileRewriter + extends Rewriter +{ + private static final String REPLACEMENT = "SELECT %s FROM (SELECT %sAPPROX_PERCENTILE(%s, ARRAY[%s]) AS percentiles FROM %s)"; + private Map>> selectMap; // A map of SelectNode to a map of each unique first arg that has duplicate APPROX_PERCENTILEs + private final List nonRewrittenSelectListItems; // Items in the select list that should not be rewritten + private static final String REWRITE_NAME = "Multiple APPROX PERCENTILE with same first arg and literal second arg"; + + public ApproxPercentileRewriter() + { + this.nonRewrittenSelectListItems = new LinkedList<>(); + this.selectMap = new HashMap<>(); + } + + private String getRewrittenSelectList(List approxPercentiles, AstNode selectList) + { + StringBuilder sb = new StringBuilder(); + int approxPercentileIdx = 1; + int nonRewrittenAliasNum = 1; + for (int i = 0; i < selectList.jjtGetNumChildren(); i++) { + AstNode selectListItem = selectList.GetChild(i); + if (approxPercentiles.contains(selectListItem)) { + sb.append(format("percentiles[%d]", approxPercentileIdx)); + AstNode aliasNode = selectListItem.GetFirstChildOfKind(JJTALIAS); + if (aliasNode != null) { + sb.append(format(" AS %s", aliasNode.GetFirstChildOfKind(JJTIDENTIFIER).GetImage())); + } + approxPercentileIdx++; + } + else { + sb.append(format("_alias%d", nonRewrittenAliasNum)); + nonRewrittenSelectListItems.add(selectListItem); + nonRewrittenAliasNum++; + } + sb.append(", "); + } + sb.replace(sb.length() - 2, sb.length(), ""); // Remove trailing comma and whitespace + return sb.toString().trim(); + } + + private String getAliasesForNonRewrittens() + { + StringBuilder sb = new StringBuilder(); + int nonRewrittenAliasNum = 1; + for (AstNode selectItem : nonRewrittenSelectListItems) { + sb.append(format("%s AS _alias%d", Unparser.unparse(selectItem).trim(), nonRewrittenAliasNum)); + sb.append(", "); + nonRewrittenAliasNum++; + } + return sb.toString(); + } + + private String getRewrittenApproxPercentileClause(List approxPercentiles) + { + StringBuilder sb = new StringBuilder(); + for (AstNode selectItem : approxPercentiles) { + AstNode secondArg = getSecondArg(selectItem); + sb.append(secondArg.GetImage()); + sb.append(", "); + } + sb.replace(sb.length() - 2, sb.length(), ""); // Remove trailing comma and whitespace + return sb.toString().trim(); + } + + private AstNode getSecondArg(AstNode selectItem) + { + AstNode functionCall = selectItem.GetFirstChildOfKind(JJTFUNCTIONCALL); + AstNode argList = functionCall.GetFirstChildOfKind(JJTARGUMENTLIST); + return (AstNode) argList.jjtGetChild(1); + } + + /** + * Generates a rewritten version of the SELECT clause given, places that version in the Unparser, then skips the original SELECT. + * + * @param select The SELECT node we're rewriting + */ + private void applyRewriting(AstNode select) + { + String tableName = select.GetFirstChildOfKind(JJTTABLEEXPRESSION).GetFirstChildOfKind(JJTFROMCLAUSE).GetFirstChildOfKind(JJTTABLENAME).GetFirstChildOfKind(JJTIDENTIFIER).GetImage(); + Map> map = selectMap.get(select); // > + String firstArg = map.keySet().iterator().next(); + List approxPercentileNodes = map.get(firstArg); + AstNode selectList = select.GetFirstChildOfKind(JJTSELECTLIST); + + Formatter formatter = new Formatter(stringBuilder); + formatter.format(REPLACEMENT, getRewrittenSelectList(approxPercentileNodes, selectList), getAliasesForNonRewrittens(), firstArg, getRewrittenApproxPercentileClause(approxPercentileNodes), tableName); + + // Move to end of this SELECT node -- we've already put in a rewritten version of it + unparseUpto(select); + moveToEndOfNode(select); + } + + @Override + public void visit(Select node, Void data) + { + if (selectMap.containsKey(node)) { + applyRewriting(node); + } + else { + defaultVisit(node, data); + } + } + + @Override + public RewriteResult rewrite(String originalSql) + { + AstNode sqlAsAst = requireNonNull(parseStatement(originalSql)); + this.selectMap = new ApproxPercentilePatternMatcher().getSelectMap(sqlAsAst); + String rewrittenSql = Unparser.unparse(sqlAsAst, this); + return new RewriteResult(REWRITE_NAME, originalSql, rewrittenSql); + } +} diff --git a/rewriter/src/test/java/com/facebook/coresql/rewriter/TestApproxPercentileRewriter.java b/rewriter/src/test/java/com/facebook/coresql/rewriter/TestApproxPercentileRewriter.java new file mode 100644 index 0000000..a965f37 --- /dev/null +++ b/rewriter/src/test/java/com/facebook/coresql/rewriter/TestApproxPercentileRewriter.java @@ -0,0 +1,116 @@ +/* + * Licensed 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. + */ + +package com.facebook.coresql.rewriter; + +import com.facebook.coresql.parser.AstNode; +import com.facebook.coresql.rewriter.approxpercentile.ApproxPercentilePatternMatcher; +import com.facebook.coresql.rewriter.approxpercentile.ApproxPercentileRewriter; +import org.testng.annotations.Test; + +import static com.facebook.coresql.parser.ParserHelper.parseStatement; +import static java.lang.String.format; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +public class TestApproxPercentileRewriter +{ + private static final String[] statementsThatDontNeedAnyRewrite = new String[] { + // False Positive + "CREATE TABLE blah AS SELECT * FROM (SELECT * FROM (SELECT foo FROM T ORDER BY x LIMIT 10) ORDER BY y LIMIT 10) ORDER BY z LIMIT 10;", + "SELECT dealer_id, sales OVER (PARTITION BY dealer_id ORDER BY sales);", + "INSERT INTO blah SELECT * FROM (SELECT t.date, t.code, t.qty FROM sales AS t ORDER BY t.date LIMIT 100);", + "SELECT (true or false) and false;", + // True Negative + "SELECT * FROM T ORDER BY y;", + "SELECT * FROM T ORDER BY y LIMIT 10;", + "use a.b;", + " SELECT 1;", + "SELECT a FROM T;", + "SELECT a FROM T WHERE p1 > p2;", + "SELECT a, b, c FROM T WHERE c1 < c2 and c3 < c4;", + "SELECT CASE a WHEN IN ( 1 ) THEN b ELSE c END AS x, b, c FROM T WHERE c1 < c2 and c3 < c4;", + "SELECT T.* FROM T JOIN W ON T.x = W.x;", + "SELECT NULL;", + "SELECT ARRAY[x] FROM T;", + "SELECT TRANSFORM(ARRAY[x], x -> x + 2) AS arra FROM T;", + "CREATE TABLE T AS SELECT TRANSFORM(ARRAY[x], x -> x + 2) AS arra FROM T;", + "INSERT INTO T SELECT TRANSFORM(ARRAY[x], x -> x + 2) AS arra FROM T;", + "SELECT ROW_NUMBER() OVER(PARTITION BY x) FROM T;", + "SELECT x, SUM(y) OVER (PARTITION BY y ORDER BY 1) AS min\n" + + "FROM (values ('b',10), ('a', 10)) AS T(x, y)\n;", + "SELECT\n" + + " CAST(MAP() AS map>) AS \"bool_tensor_features\";", + "SELECT f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f())))))))))))))))))))))))))))));", + "SELECT abs, 2 as abs;", + }; + + private static final String[] statementsThatNeedApproxPercentileRewrite = new String[] { + // True Positive + "SELECT APPROX_PERCENTILE(x, 0.1) AS percentile_10, APPROX_PERCENTILE(x, 0.2) AS percentile_20, APPROX_PERCENTILE(x, 0.3) AS percentile_30 FROM T;", + "SELECT APPROX_PERCENTILE(x, 0.1), APPROX_PERCENTILE(x, 0.2) AS percentile_20, APPROX_PERCENTILE(x, 0.3) FROM T;", + "SELECT approx_percentile(y, 0.2), x + 1, approx_percentile(y, 0.1) from T group by 2;", + // Subquery + "SELECT x FROM (SELECT APPROX_PERCENTILE(x, 0.1) AS percentile_10, APPROX_PERCENTILE(x, 0.2) AS percentile_20, APPROX_PERCENTILE(x, 0.3) AS percentile_30 FROM T);", + "CREATE TABLE blah AS SELECT * FROM (SELECT * FROM (SELECT approx_percentile(y, 0.2), x + 1, approx_percentile(y, 0.1) from T group by 2));" + }; + + private static final String[] statementsThatDontNeedApproxPercentileRewrite = new String[] { + // False Positive + "SELECT APPROX_PERCENTILE(x, 0.1) AS percentile_10, APPROX_PERCENTILE(y, 0.2) AS percentile_20, APPROX_PERCENTILE(z, 0.3) AS percentile_30 FROM T;", + "SELECT APPROX_PERCENTILE(x, 0.1) AS percentile_10, APPROX_PERCENTILE(x, 0.2) AS percentile_20, APPROX_PERCENTILE(z, 0.3) AS percentile_30 FROM T;", + "SELECT APPROX_PERCENTILE(x, ARRAY[0.1, 0.2, 0.3]), APPROX_PERCENTILE(x, 0.4), APPROX_PERCENTILE(x, 0.5) FROM T;", + "SELECT APPROX_PERCENTILE(x, ARRAY[0.1, 0.2, 0.3]) FROM (SELECT APPROX_PERCENTILE(x, 0.1) from T);", + "SELECT APPROX_PERCENTILE(x, 0.1) FROM (SELECT APPROX_PERCENTILE(x, 0.2) FROM T);", + "SELECT APPROX_PERCENTILE(x, 0.1) FROM (SELECT * FROM (SELECT approx_percentile(x, 0.1), approx_percentile(y, 0.1) from T group by 2));" + }; + + private void assertApproxPercentilePatternIsMatchedOrUnmatched(String sql, boolean isMatched) + { + AstNode ast = parseStatement(sql); + assertNotNull(ast); + if (isMatched) { + assertTrue(new ApproxPercentilePatternMatcher().getSelectMap(ast).size() >= 1); + } + else { + assertEquals(new ApproxPercentilePatternMatcher().getSelectMap(ast).size(), 0); + } + } + + private void rewriteThenPrint(String sql) + { + String rewritten = new ApproxPercentileRewriter().rewrite(sql).getRewrittenSql(); + System.out.println(format("Before --> %s", sql)); + System.out.println(format("AFTER --> %s", rewritten)); + System.out.println(); + } + + @Test + public void approxPercentilePatternDetectionTest() + { + for (String sql : statementsThatNeedApproxPercentileRewrite) { + assertApproxPercentilePatternIsMatchedOrUnmatched(sql, true); + rewriteThenPrint(sql); + } + + for (String sql : statementsThatDontNeedApproxPercentileRewrite) { + assertApproxPercentilePatternIsMatchedOrUnmatched(sql, false); + } + + for (String sql : statementsThatDontNeedAnyRewrite) { + assertApproxPercentilePatternIsMatchedOrUnmatched(sql, false); + } + } +} diff --git a/rewriter/~$pom.xml b/rewriter/~$pom.xml new file mode 100644 index 0000000..0499b88 Binary files /dev/null and b/rewriter/~$pom.xml differ