diff --git a/experiment/src/org/labkey/experiment/ExperimentUpgradeCode.java b/experiment/src/org/labkey/experiment/ExperimentUpgradeCode.java index cbfed9e3bb7..d67dc76503c 100644 --- a/experiment/src/org/labkey/experiment/ExperimentUpgradeCode.java +++ b/experiment/src/org/labkey/experiment/ExperimentUpgradeCode.java @@ -63,14 +63,12 @@ import org.labkey.api.security.roles.SiteAdminRole; import org.labkey.api.settings.AppProps; import org.labkey.api.util.logging.LogHelper; -import org.labkey.experiment.api.ClosureQueryHelper; import org.labkey.experiment.api.ExpSampleTypeImpl; import org.labkey.experiment.api.ExperimentServiceImpl; import org.labkey.experiment.api.MaterialSource; import org.labkey.experiment.api.property.DomainImpl; import org.labkey.experiment.api.property.DomainPropertyImpl; import org.labkey.experiment.api.property.StorageProvisionerImpl; -import org.labkey.experiment.samples.SampleTimelineAuditProvider; import java.sql.Connection; import java.sql.SQLException; diff --git a/query/src/org/labkey/query/sql/QInLineage.java b/query/src/org/labkey/query/sql/QInLineage.java index 88a926e96a4..3d1c8552247 100644 --- a/query/src/org/labkey/query/sql/QInLineage.java +++ b/query/src/org/labkey/query/sql/QInLineage.java @@ -28,42 +28,71 @@ import org.labkey.api.query.column.BuiltInColumnTypes; import org.labkey.query.QueryServiceImpl; +import java.util.Objects; + +import static org.labkey.query.sql.antlr.SqlBaseParser.EXPANCESTORSOF; +import static org.labkey.query.sql.antlr.SqlBaseParser.EXPDESCENDANTSOF; +import static org.labkey.query.sql.antlr.SqlBaseParser.EXPLINEAGEOF; final public class QInLineage extends QExpr { final boolean _in; + final boolean _children; final boolean _parents; + final String _method; - public QInLineage(boolean in, boolean parents) + public QInLineage(boolean in, int methodTokenType) { - this._in = in; - this._parents = parents; + super(QNode.class); + + _in = in; + _method = switch (methodTokenType) + { + case EXPANCESTORSOF -> { + _children = false; + _parents = true; + yield "EXPANCESTORSOF"; + } + case EXPDESCENDANTSOF -> { + _children = true; + _parents = false; + yield "EXPDESCENDANTSOF"; + } + case EXPLINEAGEOF -> { + _children = true; + _parents = true; + yield "EXPLINEAGEOF"; + } + default -> throw new IllegalArgumentException("Invalid QInLineage method token type: " + methodTokenType); + }; } String operator() { - return (_in ? " IN " : " NOT IN ") + (_parents ? "EXPANCESTORSOF " : "EXPDESCENDANTSOF " ); + return (_in ? " IN " : " NOT IN ") + _method + " "; } @Override public void appendSql(SqlBuilder builder, Query query) { - SQLTableInfo sqlti = new SQLTableInfo(query.getSchema().getDbSchema(), "_"); var children = childList(); - var LHS = ((QExpr) getFirstChild()); - var RHS = ((QQuery) getLastChild()); + var LHS = (QExpr) firstOrThrow(children); + var RHS = (QQuery) secondOrThrow(children); - // LHS should be a 'lineage object', e.g. the result of calling {ExtTable}.expObject() + // LHS should be a 'lineage object', e.g., the result of calling {ExtTable}.expObject() ColumnInfo lhsCol = null; if (LHS instanceof QueryServiceImpl.QColumnInfo || LHS instanceof QMethodCall) + { + SQLTableInfo sqlti = new SQLTableInfo(query.getSchema().getDbSchema(), "_"); lhsCol = LHS.createColumnInfo(sqlti, "_", query); + } if (lhsCol == null || !Strings.CS.equals(lhsCol.getConceptURI(), BuiltInColumnTypes.EXPOBJECTID_CONCEPT_URI)) { - query.getParseErrors().add(new QueryParseException(operator() + " requires argument to be a lineage object", null, getLine(), getColumn())); + query.getParseErrors().add(new QueryParseException(_method + " requires argument to be a lineage object", null, getLine(), getColumn())); return; } - // RHS should be SELECT with one column of 'lineage object', e.g. the result of calling {ExtTable}.expObject() + // RHS should be SELECT with one column of 'lineage object', e.g., the result of calling {ExtTable}.expObject() QueryRelation r = RHS._select; var map = r.getAllColumns(); var col = map.size() != 1 ? null : map.values().iterator().next(); @@ -72,7 +101,7 @@ public void appendSql(SqlBuilder builder, Query query) col.copyColumnAttributesTo(rhsCol); if (!Strings.CS.equals(rhsCol.getConceptURI(), BuiltInColumnTypes.EXPOBJECTID_CONCEPT_URI)) { - query.getParseErrors().add(new QueryParseException(operator() + " requires argument to be a lineage object", null, getLine(), getColumn())); + query.getParseErrors().add(new QueryParseException(_method + " requires argument to be a lineage object", null, getLine(), getColumn())); return; } @@ -80,7 +109,23 @@ public void appendSql(SqlBuilder builder, Query query) RHS.appendSql(subquery, query); // subquery will have surrounding parens, but the double parens don't cause a problem - ExpLineageOptions options = new ExpLineageOptions(_parents, !_parents, 1000); + // Parse depth argument + int depth = 1_000; + { + QNode depthExpr = child(children, 2); + if (depthExpr != null) + { + if (!(depthExpr instanceof QNumber n)) + { + query.getParseErrors().add(new QueryParseException(_method + " requires second argument to be an integer", null, getLine(), getColumn())); + return; + } + + depth = n.getValue().intValue(); + } + } + + ExpLineageOptions options = new ExpLineageOptions(_parents, _children, depth); options.setUseObjectIds(true); // expObject() returns objectid not lsid options.setOnlySelectObjectId(true); // generate one column SELECT, also don't join to material/data/protocolapplication SQLFragment lineage = ExperimentService.get().generateExperimentTreeSQL(subquery, options); @@ -93,7 +138,6 @@ public void appendSql(SqlBuilder builder, Query query) builder.append("))"); } - @Override public void appendSource(SourceBuilder builder) { @@ -109,18 +153,16 @@ public void appendSource(SourceBuilder builder) builder.popPrefix(")"); } - @Override @NotNull public JdbcType getJdbcType() { return JdbcType.BOOLEAN; } - @Override public boolean equalsNode(QNode other) { - return (other instanceof QInLineage o) && _in == o._in && _parents == o._parents; + return other instanceof QInLineage o && _in == o._in && Objects.equals(_method, o._method); } @Override diff --git a/query/src/org/labkey/query/sql/QNode.java b/query/src/org/labkey/query/sql/QNode.java index 4e61134765f..2a5e272f526 100644 --- a/query/src/org/labkey/query/sql/QNode.java +++ b/query/src/org/labkey/query/sql/QNode.java @@ -18,6 +18,7 @@ import org.antlr.runtime.CommonToken; import org.antlr.runtime.tree.CommonTree; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.junit.Assert; import org.junit.Test; @@ -34,6 +35,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import static org.labkey.query.sql.antlr.SqlBaseParser.FALSE; import static org.labkey.query.sql.antlr.SqlBaseParser.IDENT; @@ -109,7 +111,7 @@ public Iterable children() return _children; } - public List childList() + public LinkedList childList() { return _children; } @@ -373,15 +375,12 @@ protected void dump(PrintWriter out, String nl, IdentityHashMap d c.dump(out, nl + " |", dumped); } - - public void addFieldRefs(Object referant) { for (QNode child : childList()) child.addFieldRefs(referant); } - public void releaseFieldRefs(Object referant) { for (QNode child : childList()) @@ -398,6 +397,35 @@ public void setHasTransformableAggregate(boolean hasTransformableAggregate) _hasTransformableAggregate = hasTransformableAggregate; } + static @Nullable QNode child(LinkedList children, int index) + { + return children.size() > index ? children.get(index) : null; + } + + static @NotNull QNode childOrThrow(LinkedList children, int index) + { + return Objects.requireNonNull(child(children, index)); + } + + static QNode first(LinkedList children) + { + return child(children, 0); + } + + static @NotNull QNode firstOrThrow(LinkedList children) + { + return childOrThrow(children, 0); + } + + static QNode second(LinkedList children) + { + return child(children, 1); + } + + static @NotNull QNode secondOrThrow(LinkedList children) + { + return childOrThrow(children, 1); + } public static class TestCase extends Assert { diff --git a/query/src/org/labkey/query/sql/SqlBase.g b/query/src/org/labkey/query/sql/SqlBase.g index f3a9aa7f71c..28ad379df35 100644 --- a/query/src/org/labkey/query/sql/SqlBase.g +++ b/query/src/org/labkey/query/sql/SqlBase.g @@ -191,6 +191,7 @@ EXCEPT : 'except'; EXISTS : 'exists'; EXPDESCENDANTSOF : 'expdescendantsof'; EXPANCESTORSOF : 'expancestorsof'; +EXPLINEAGEOF : 'explineageof'; FALSE : 'false'; FROM : 'from'; FULL : 'full'; @@ -423,7 +424,7 @@ fromRange tableMethod - : (EXPANCESTORSOF | EXPDESCENDANTSOF) op=OPEN^ {$op.setType(METHOD_CALL);} subQuery CLOSE! + : (EXPANCESTORSOF | EXPDESCENDANTSOF | EXPLINEAGEOF) op=OPEN^ {$op.setType(METHOD_CALL);} subQuery (COMMA! expression)? CLOSE! ; @@ -679,7 +680,7 @@ likeEscape inList - : (EXPANCESTORSOF | EXPDESCENDANTSOF) op=OPEN^ {$op.setType(METHOD_CALL);} subQuery CLOSE! + : (EXPANCESTORSOF | EXPDESCENDANTSOF | EXPLINEAGEOF) op=OPEN^ {$op.setType(METHOD_CALL);} subQuery (COMMA! expression)? CLOSE! | compoundExpr -> ^(IN_LIST compoundExpr) ; diff --git a/query/src/org/labkey/query/sql/SqlParser.java b/query/src/org/labkey/query/sql/SqlParser.java index 9174a6e2207..f32830ec8a8 100644 --- a/query/src/org/labkey/query/sql/SqlParser.java +++ b/query/src/org/labkey/query/sql/SqlParser.java @@ -76,9 +76,9 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; +import static org.labkey.query.sql.QNode.*; import static org.labkey.query.sql.antlr.SqlBaseParser.*; @@ -885,13 +885,27 @@ else if (divisorType==NUM_DOUBLE || divisorType==NUM_FLOAT || divisorType==NUM_I { // rewrite "IN EXPANCESTORS" "IN EXPDESCENDANTS" var method = rhs.getFirstChild(); - if (method.getTokenType() != EXPANCESTORSOF && method.getTokenType() != EXPDESCENDANTSOF) + if (method.getTokenType() != EXPANCESTORSOF && method.getTokenType() != EXPDESCENDANTSOF && method.getTokenType() != EXPLINEAGEOF) { _parseErrors.add(new QueryParseException("Illegal syntax near 'IN'", null, node.getLine(), node.getCharPositionInLine())); return null; } - var qInLineage = new QInLineage(node.getType()==IN, method.getTokenType() == EXPANCESTORSOF ); - qInLineage._replaceChildren(new LinkedList<>(List.of(lhs, rhs.childList().get(1)))); + + var rhsChildren = rhs.childList(); + if (rhsChildren.size() > 3) + { + _parseErrors.add(new QueryParseException(method.getTokenText().toUpperCase() + " supports at most 2 arguments", null, node.getLine(), node.getCharPositionInLine())); + return null; + } + + var qInLineage = new QInLineage(node.getType() == IN, method.getTokenType()); + var qInLineageChildren = new LinkedList(); + qInLineageChildren.add(lhs); + qInLineageChildren.add(secondOrThrow(rhsChildren)); + if (rhsChildren.size() > 2) + qInLineageChildren.add(childOrThrow(rhsChildren, 2)); + + qInLineage._replaceChildren(qInLineageChildren); return qInLineage; } } @@ -967,7 +981,7 @@ else if (name.equals("age")) } // special case for table returning method - var isTableResultMethod = id.getTokenType() == EXPANCESTORSOF || id.getTokenType() == EXPDESCENDANTSOF; + var isTableResultMethod = id.getTokenType() == EXPANCESTORSOF || id.getTokenType() == EXPDESCENDANTSOF || id.getTokenType() == EXPLINEAGEOF; if (!isTableResultMethod) { try @@ -1512,27 +1526,6 @@ private boolean validateTimestampConstant(QNode n) } } - - private static QNode first(LinkedList children) - { - return !children.isEmpty() ? children.get(0) : null; - } - - private static @NotNull QNode firstOrThrow(LinkedList children) - { - return Objects.requireNonNull(first(children)); - } - - private static QNode second(LinkedList children) - { - return children.size() > 1 ? children.get(1) : null; - } - - private static @NotNull QNode secondOrThrow(LinkedList children) - { - return Objects.requireNonNull(second(children)); - } - private QNode constantToStringNode(QNode node) { if (node instanceof QString) @@ -1658,6 +1651,7 @@ QNode qnode(CommonTree node, boolean constExpr) break; case EXPANCESTORSOF: case EXPDESCENDANTSOF: + case EXPLINEAGEOF: case IDENT: case QUOTED_IDENTIFIER: return QIdentifier.create(node); @@ -1892,7 +1886,6 @@ class delete elements fetch indices insert into limit new set update versioned b "SELECT CASE R.a WHEN 1 THEN 'one' WHEN 2 THEN 'two' ELSE 'few' END FROM R", "SELECT R.a FROM R WHERE R.a LIKE 'a%'", -// "SELECT R.a FROM R WHERE R.a LIKE 'a%' AND R.b LIKE 'a/%' ESCAPE '/'", "SELECT MS2SearchRuns.Flag,MS2SearchRuns.Links,MS2SearchRuns.Name,MS2SearchRuns.Created,MS2SearchRuns.RunGroups FROM MS2SearchRuns", @@ -1909,12 +1902,6 @@ class delete elements fetch indices insert into limit new set update versioned b "SELECT R.value AS V FROM R WHERE R.y > (SELECT MAX(S.y) FROM S WHERE S.x=R.x)", "SELECT R.value, T.a, T.b FROM R INNER JOIN (SELECT S.a, S.b FROM S) T ON R.z=T.z", -// "SELECT R.a FROM R WHERE EXISTS (SELECT S.b FROM S WHERE S.x=R.x)", -// "SELECT R.a FROM R WHERE NOT EXISTS (SELECT S.b FROM S WHERE S.x=R.x)", -// "SELECT R.a FROM R WHERE R.value > ALL (SELECT value from S WHERE S.x=R.x)", -// "SELECT R.a FROM R WHERE R.value > ANY (SELECT value from S WHERE S.x=R.x)", -// "SELECT R.a FROM R WHERE R.value > SOME (SELECT value from S WHERE S.x=R.x)", - "SELECT a FROM R WHERE a=b AND b<>c AND b!=c AND c>d AND d=g AND g IS NULL AND h IS NOT NULL " + " AND i BETWEEN 1 AND 2 AND j+k-l=-1 AND m/n=o AND p||q=r AND (NOT s OR t) AND u LIKE '%x%' AND u NOT LIKE '%xx%' " + " AND v IN (1,2) AND v NOT IN (3,4) AND x&y=1 AND x|y=1 AND x^y=1", @@ -1942,8 +1929,6 @@ class delete elements fetch indices insert into limit new set update versioned b "SELECT a, GROUP_CONCAT(DISTINCT b, CHR(10)) FROM R GROUP BY a", "SELECT GROUP_CONCAT(b) FROM R GROUP BY a", - "BROKEN", - // nested JOINS "SELECT R.a, \"S\".b FROM R LEFT OUTER JOIN (S RIGHT OUTER JOIN T ON S.y = T.y) ON R.x = S.x", // .* @@ -1952,10 +1937,24 @@ class delete elements fetch indices insert into limit new set update versioned b // PIVOT "SELECT R.a, R.b, SUM(x) sumX FROM R GROUP BY R.a, R.b PIVOT sumX BY b", "SELECT R.a, R.b, SUM(x) sumX FROM R GROUP BY R.a, R.b PIVOT sumX BY b IN (0,1,2)", - "SELECT R.a, R.b, SUM(x) sumX FROM R GROUP BY R.a, R.b PIVOT sumX BY b IN (0 AS Zero,1 ONE,2 TWO)" + "SELECT R.a, R.b, SUM(x) sumX FROM R GROUP BY R.a, R.b PIVOT sumX BY b IN (0 AS Zero,1 ONE,2 TWO)", + + // EXPANCESTORSOF + "SELECT M.RowId, M.Name FROM exp.Materials M WHERE M.expObject() IN EXPANCESTORSOF (SELECT DD.expObject() FROM exp.Data DD WHERE DD.RowId > 0)", + "SELECT M.RowId, M.Name FROM exp.Materials M WHERE M.expObject() IN EXPANCESTORSOF (SELECT DD.expObject() FROM exp.Data DD WHERE DD.RowId > 0, 2)", + "SELECT M.RowId, M.Name FROM exp.Materials M WHERE M.expObject() IN EXPANCESTORSOF (SELECT DD.expObject() FROM exp.Data DD WHERE DD.RowId > 0, 2000)", + + // EXPDESCENDANTSOF + "SELECT M.RowId, M.Name FROM exp.Materials M WHERE M.expObject() IN EXPDESCENDANTSOF (SELECT DD.expObject() FROM exp.Data DD WHERE DD.RowId > 0)", + "SELECT M.RowId, M.Name FROM exp.Materials M WHERE M.expObject() IN EXPDESCENDANTSOF (SELECT DD.expObject() FROM exp.Data DD WHERE DD.RowId > 0, -2)", + "SELECT M.RowId, M.Name FROM exp.Materials M WHERE M.expObject() IN EXPDESCENDANTSOF (SELECT DD.expObject() FROM exp.Data DD WHERE DD.RowId > 0, 2000)", + + // EXPLINEAGEOF + "SELECT M.RowId, M.Name FROM exp.Materials M WHERE M.expObject() IN EXPLINEAGEOF (SELECT DD.expObject() FROM exp.Data DD WHERE DD.RowId > 0)", + "SELECT M.RowId, M.Name FROM exp.Materials M WHERE M.expObject() IN EXPLINEAGEOF (SELECT DD.expObject() FROM exp.Data DD WHERE DD.RowId > 0, 2)", + "SELECT M.RowId, M.Name FROM exp.Materials M WHERE M.expObject() IN EXPLINEAGEOF (SELECT DD.expObject() FROM exp.Data DD WHERE DD.RowId > 0, 2000)" }; - static String[] failSql = new String[] { "", @@ -1980,8 +1979,6 @@ class delete elements fetch indices insert into limit new set update versioned b "SELECT * FROM (WITH peeps AS (SELECT * FROM study.participant) SELECT * FROM peeps)" }; - - @SuppressWarnings("JUnitMalformedDeclaration") public static class SqlParserTestCase extends Assert {