Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/src/org/labkey/api/exp/api/ExperimentService.java
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ public interface ExperimentService extends ExperimentRunTypeSource

String ILLEGAL_PARENT_ALIAS_CHARSET = "/:<>$[]{};,`\"~!@#$%^*=|?\\";

String EXPERIMENTAL_FEATURE_FROM_EXPANCESTORS = "org.labkey.api.exp.api.ExperimentService#FROM_EXPANCESTORS";

int SIMPLE_PROTOCOL_FIRST_STEP_SEQUENCE = 1;
int SIMPLE_PROTOCOL_CORE_STEP_SEQUENCE = 10;
int SIMPLE_PROTOCOL_EXTRA_STEP_SEQUENCE = 15;
Expand Down
6 changes: 3 additions & 3 deletions api/src/org/labkey/api/exp/query/ExpSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package org.labkey.api.exp.query;

import org.apache.commons.lang3.Strings;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.labkey.api.collections.CaseInsensitiveHashSet;
Expand Down Expand Up @@ -502,11 +503,10 @@ public QuerySchema getSchema(String name)
if (_restricted)
return null;

// CONSIDER: also support hidden "samples" schema ?
if (name.equals(NestedSchemas.materials.name()))
if (Strings.CI.equals(name, NestedSchemas.materials.name()))
return new SamplesSchema(SchemaKey.fromParts(getName(), NestedSchemas.materials.name()), getUser(), getContainer());

if (name.equals(NestedSchemas.data.name()))
if (Strings.CI.equals(name, NestedSchemas.data.name()))
return new DataClassUserSchema(getContainer(), getUser());

return super.getSchema(name);
Expand Down
35 changes: 35 additions & 0 deletions api/src/org/labkey/api/exp/query/ExpTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
import org.labkey.api.data.Container;
import org.labkey.api.data.ContainerFilter;
import org.labkey.api.data.ContainerFilterable;
import org.labkey.api.data.DataColumn;
import org.labkey.api.data.MutableColumnInfo;
import org.labkey.api.data.RenderContext;
import org.labkey.api.data.SQLFragment;
import org.labkey.api.data.TableInfo;
import org.labkey.api.dataiterator.DataIteratorContext;
Expand Down Expand Up @@ -134,4 +136,37 @@ default ColumnInfo getExpObjectColumn()
{
return null;
}

class ExpObjectDataColumn extends DataColumn
{
public ExpObjectDataColumn(ColumnInfo colInfo)
{
super(colInfo);
}

@Override
public Object getValue(RenderContext ctx)
{
return 0;
}

@Override
public Object getDisplayValue(RenderContext ctx)
{
var v = super.getValue(ctx);
return null == v ? v : "lineage object";
}

@Override
public boolean isSortable()
{
return false;
}

@Override
public boolean isFilterable()
{
return false;
}
}
}
2 changes: 2 additions & 0 deletions experiment/src/org/labkey/experiment/ExperimentModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,8 @@ protected void init()
}
OptionalFeatureService.get().addExperimentalFeatureFlag(AppProps.QUANTITY_COLUMN_SUFFIX_TESTING, "Quantity column suffix testing",
"If a column name contains a \"__<unit>\" suffix, this feature allows for testing it as a Quantity display column", false);
OptionalFeatureService.get().addExperimentalFeatureFlag(ExperimentService.EXPERIMENTAL_FEATURE_FROM_EXPANCESTORS, "SQL syntax: 'FROM EXPANCESTORS()'",
"Support for querying lineage of experiment objects", false);

RoleManager.registerPermission(new DesignVocabularyPermission(), true);
RoleManager.registerRole(new SampleTypeDesignerRole());
Expand Down
29 changes: 1 addition & 28 deletions experiment/src/org/labkey/experiment/api/ExpTableImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -513,38 +513,11 @@ public SQLFragment getSQL(String tableAlias, DbSchema schema, SQLFragment[] argu
@Override
public MutableColumnInfo createColumnInfo(TableInfo parentTable, ColumnInfo[] arguments, String alias)
{
var objectColumn = getExpObjectColumn();
if (null == _expObjectColumnName)
return new NullColumnInfo(parentTable, "_exptable_object_", JdbcType.INTEGER);
var ret = super.createColumnInfo(parentTable, arguments, "_exptable_object_");
ret.setConceptURI(BuiltInColumnTypes.EXPOBJECTID_CONCEPT_URI);
ret.setDisplayColumnFactory(colInfo -> new DataColumn(colInfo)
{
@Override
public Object getValue(RenderContext ctx)
{
return 0;
}

@Override
public Object getDisplayValue(RenderContext ctx)
{
var v = super.getValue(ctx);
return null == v ? v : "lineage object";
}

@Override
public boolean isSortable()
{
return false;
}

@Override
public boolean isFilterable()
{
return false;
}
});
ret.setDisplayColumnFactory(ExpObjectDataColumn::new);
return ret;
}
}
Expand Down
6 changes: 3 additions & 3 deletions query/src/org/labkey/query/sql/QIdentifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ public FieldKey getFieldKey()

public String getIdentifier()
{
if (getTokenType() == SqlBaseParser.IDENT)
return getTokenText();
return LabKeySql.unquoteIdentifier(getTokenText());
if (getTokenType() == SqlBaseParser.QUOTED_IDENTIFIER)
return LabKeySql.unquoteIdentifier(getTokenText());
return getTokenText();
}


Expand Down
256 changes: 256 additions & 0 deletions query/src/org/labkey/query/sql/QueryLineage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
package org.labkey.query.sql;

import org.apache.commons.lang3.Strings;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.labkey.api.data.BaseColumnInfo;
import org.labkey.api.data.ContainerFilter;
import org.labkey.api.data.JdbcType;
import org.labkey.api.data.SQLFragment;
import org.labkey.api.data.TableInfo;
import org.labkey.api.data.dialect.SqlDialect;
import org.labkey.api.exp.api.ExpLineageOptions;
import org.labkey.api.exp.api.ExperimentService;
import org.labkey.api.exp.query.ExpTable;
import org.labkey.api.query.FieldKey;
import org.labkey.api.query.QueryParseException;
import org.labkey.api.query.column.BuiltInColumnTypes;
import org.labkey.data.xml.ColumnType;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

public class QueryLineage extends AbstractQueryRelation
{
final QuerySelect sourceSelect;
final boolean ancestors;

QueryLineage(Query query, QNode token, QuerySelect source, String alias, boolean ancestors)
{
super(query, query.getSchema(), Objects.toString(alias, "explineage_" + query.incrementAliasCounter()));
this.sourceSelect = source;
this.ancestors = ancestors;

sourceSelect.setSkipSuggestedColumns(true);
var all = source.getAllColumns();
if (1 != all.size())
{
query.getParseErrors().add(new QueryParseException(token.getTokenText() + " subquery must have one column", null, token.getLine(), token.getColumn()));
}
else
{
var relationColumn = all.values().iterator().next();
var col = new BaseColumnInfo("?");
relationColumn.copyColumnAttributesTo(col);
if (!BuiltInColumnTypes.EXPOBJECTID_CONCEPT_URI.equals(col.getConceptURI()))
query.getParseErrors().add(new QueryParseException(token.getTokenText() + " requires an object column", null, token.getLine(), token.getColumn()));
}
}

@Override
public void declareFields()
{
sourceSelect.declareFields();
}

@Override
public void resolveFields()
{
sourceSelect.resolveFields();
}

@Override
public TableInfo getTableInfo()
{
throw new UnsupportedOperationException();
}

@Override
public Map<String, RelationColumn> getAllColumns()
{
return Map.of(
"Depth", Objects.requireNonNull(getColumn("depth")),
"FromObject", Objects.requireNonNull(getColumn("fromobject")),
"ToObject", Objects.requireNonNull(getColumn("toobject")));
}

@Override
public @Nullable AbstractQueryRelation.RelationColumn getFirstColumn()
{
return getColumn("fromobject");
}

@Override
public @Nullable AbstractQueryRelation.RelationColumn getColumn(@NotNull String name)
{
return switch (name.toLowerCase())
{
case "depth", "fromobject", "toobject" -> new LineageColumn(name.toLowerCase());
default -> null;
};
}

@Override
public int getSelectedColumnCount()
{
return 3;
}

@Override
public @Nullable AbstractQueryRelation.RelationColumn getLookupColumn(@NotNull RelationColumn parent, @NotNull String name)
{
throw new UnsupportedOperationException();
}

@Override
public @Nullable AbstractQueryRelation.RelationColumn getLookupColumn(@NotNull RelationColumn parent, ColumnType.@NotNull Fk fk, @NotNull String name)
{
throw new UnsupportedOperationException();
}

@Override
public SQLFragment getSql()
{
throw new UnsupportedOperationException();
}

@Override
public SQLFragment getFromSql()
{
SqlDialect d = _query.getSchema().getDbSchema().getSqlDialect();

ExpLineageOptions options = new ExpLineageOptions(ancestors, !ancestors, 1000);
options.setForLookup(true); // remove intermediate edges
options.setUseObjectIds(true); // expObject() returns objectid not lsid

SQLFragment sql = new SQLFragment();
sql.appendComment("<QueryLineage>", d);
// CONSIDER: use CTE for sourceSelect.getSql()
SQLFragment lineageSql = ExperimentService.get().generateExperimentTreeSQL(sourceSelect.getSql(), options);
sql.append("(\n").append(lineageSql).append("\n) AS ").appendIdentifier(getAlias());
sql.appendComment("</QueryLineage>", d);
return sql;
}

@Override
public String getQueryText()
{
return (ancestors?"EXPANCESTORSOF":"EXPDESCENDANTSOF") + "(" + sourceSelect.getQueryText() + ")";
}

@Override
public void setContainerFilter(ContainerFilter containerFilter)
{
sourceSelect.setContainerFilter(containerFilter);
}

@Override
public Set<RelationColumn> getSuggestedColumns(Set<RelationColumn> selected)
{
return Set.of();
}


private class LineageColumn extends RelationColumn
{
final FieldKey _fieldKey;
final String _alias;
final JdbcType _jdbcType;

LineageColumn(String name)
{
String alias;
FieldKey fk;
JdbcType jdbcType = JdbcType.BIGINT;
switch (name.toLowerCase())
{
case "depth":
alias = "depth";
fk = new FieldKey(null, "Depth");
jdbcType = JdbcType.INTEGER;
break;
case "fromobject":
alias = "self";
fk = new FieldKey(null, "FromObject");
break;
case "toobject":
alias = "objectid";
fk = new FieldKey(null, "ToObject");
break;
default:
throw new IllegalArgumentException("Unknown column name: " + name);
}
_fieldKey = fk;
_alias = alias;
_jdbcType = jdbcType;
}

@Override
SQLFragment getInternalSql()
{
return new SQLFragment().appendDottedIdentifiers(QueryLineage.this.getAlias(), getAlias());
}

@Override
void copyColumnAttributesTo(@NotNull BaseColumnInfo to)
{
to.setJdbcType(_jdbcType);
if (!"depth".equals(_alias))
{
to.setConceptURI(BuiltInColumnTypes.EXPOBJECTID_CONCEPT_URI);
to.setDisplayColumnFactory(ExpTable.ExpObjectDataColumn::new);
}
}

@Override
public FieldKey getFieldKey()
{
return _fieldKey;
}

@Override
String getAlias()
{
return _alias;
}

@Override
AbstractQueryRelation getTable()
{
return QueryLineage.this;
}

@Override
boolean isHidden()
{
return false;
}

@Override
String getPrincipalConceptCode()
{
return "";
}

@Override
String getConceptURI()
{
return "";
}

@Override
public @NotNull JdbcType getJdbcType()
{
return _jdbcType;
}

@Override
public Collection<RelationColumn> gatherInvolvedSelectColumns(Collection<RelationColumn> collect)
{
return List.of();
}
}
}
Loading