Skip to content

Commit d408398

Browse files
FROM EXPANCESTORSOF() (#7223)
* FROM EXPANCESTORSOF checkpoint * Some validation * ExpObjectDataColumn * case sensitivity fix * Experimental Feature * fix typo!
1 parent cb3a736 commit d408398

File tree

10 files changed

+425
-97
lines changed

10 files changed

+425
-97
lines changed

api/src/org/labkey/api/exp/api/ExperimentService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ public interface ExperimentService extends ExperimentRunTypeSource
127127

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

130+
String EXPERIMENTAL_FEATURE_FROM_EXPANCESTORS = "org.labkey.api.exp.api.ExperimentService#FROM_EXPANCESTORS";
131+
130132
int SIMPLE_PROTOCOL_FIRST_STEP_SEQUENCE = 1;
131133
int SIMPLE_PROTOCOL_CORE_STEP_SEQUENCE = 10;
132134
int SIMPLE_PROTOCOL_EXTRA_STEP_SEQUENCE = 15;

api/src/org/labkey/api/exp/query/ExpSchema.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.labkey.api.exp.query;
1818

19+
import org.apache.commons.lang3.Strings;
1920
import org.jetbrains.annotations.NotNull;
2021
import org.jetbrains.annotations.Nullable;
2122
import org.labkey.api.collections.CaseInsensitiveHashSet;
@@ -502,11 +503,10 @@ public QuerySchema getSchema(String name)
502503
if (_restricted)
503504
return null;
504505

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

509-
if (name.equals(NestedSchemas.data.name()))
509+
if (Strings.CI.equals(name, NestedSchemas.data.name()))
510510
return new DataClassUserSchema(getContainer(), getUser());
511511

512512
return super.getSchema(name);

api/src/org/labkey/api/exp/query/ExpTable.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
import org.labkey.api.data.Container;
2323
import org.labkey.api.data.ContainerFilter;
2424
import org.labkey.api.data.ContainerFilterable;
25+
import org.labkey.api.data.DataColumn;
2526
import org.labkey.api.data.MutableColumnInfo;
27+
import org.labkey.api.data.RenderContext;
2628
import org.labkey.api.data.SQLFragment;
2729
import org.labkey.api.data.TableInfo;
2830
import org.labkey.api.dataiterator.DataIteratorContext;
@@ -134,4 +136,37 @@ default ColumnInfo getExpObjectColumn()
134136
{
135137
return null;
136138
}
139+
140+
class ExpObjectDataColumn extends DataColumn
141+
{
142+
public ExpObjectDataColumn(ColumnInfo colInfo)
143+
{
144+
super(colInfo);
145+
}
146+
147+
@Override
148+
public Object getValue(RenderContext ctx)
149+
{
150+
return 0;
151+
}
152+
153+
@Override
154+
public Object getDisplayValue(RenderContext ctx)
155+
{
156+
var v = super.getValue(ctx);
157+
return null == v ? v : "lineage object";
158+
}
159+
160+
@Override
161+
public boolean isSortable()
162+
{
163+
return false;
164+
}
165+
166+
@Override
167+
public boolean isFilterable()
168+
{
169+
return false;
170+
}
171+
}
137172
}

experiment/src/org/labkey/experiment/ExperimentModule.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,8 @@ protected void init()
267267
}
268268
OptionalFeatureService.get().addExperimentalFeatureFlag(AppProps.QUANTITY_COLUMN_SUFFIX_TESTING, "Quantity column suffix testing",
269269
"If a column name contains a \"__<unit>\" suffix, this feature allows for testing it as a Quantity display column", false);
270+
OptionalFeatureService.get().addExperimentalFeatureFlag(ExperimentService.EXPERIMENTAL_FEATURE_FROM_EXPANCESTORS, "SQL syntax: 'FROM EXPANCESTORS()'",
271+
"Support for querying lineage of experiment objects", false);
270272

271273
RoleManager.registerPermission(new DesignVocabularyPermission(), true);
272274
RoleManager.registerRole(new SampleTypeDesignerRole());

experiment/src/org/labkey/experiment/api/ExpTableImpl.java

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -513,38 +513,11 @@ public SQLFragment getSQL(String tableAlias, DbSchema schema, SQLFragment[] argu
513513
@Override
514514
public MutableColumnInfo createColumnInfo(TableInfo parentTable, ColumnInfo[] arguments, String alias)
515515
{
516-
var objectColumn = getExpObjectColumn();
517516
if (null == _expObjectColumnName)
518517
return new NullColumnInfo(parentTable, "_exptable_object_", JdbcType.INTEGER);
519518
var ret = super.createColumnInfo(parentTable, arguments, "_exptable_object_");
520519
ret.setConceptURI(BuiltInColumnTypes.EXPOBJECTID_CONCEPT_URI);
521-
ret.setDisplayColumnFactory(colInfo -> new DataColumn(colInfo)
522-
{
523-
@Override
524-
public Object getValue(RenderContext ctx)
525-
{
526-
return 0;
527-
}
528-
529-
@Override
530-
public Object getDisplayValue(RenderContext ctx)
531-
{
532-
var v = super.getValue(ctx);
533-
return null == v ? v : "lineage object";
534-
}
535-
536-
@Override
537-
public boolean isSortable()
538-
{
539-
return false;
540-
}
541-
542-
@Override
543-
public boolean isFilterable()
544-
{
545-
return false;
546-
}
547-
});
520+
ret.setDisplayColumnFactory(ExpObjectDataColumn::new);
548521
return ret;
549522
}
550523
}

query/src/org/labkey/query/sql/QIdentifier.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@ public FieldKey getFieldKey()
7878

7979
public String getIdentifier()
8080
{
81-
if (getTokenType() == SqlBaseParser.IDENT)
82-
return getTokenText();
83-
return LabKeySql.unquoteIdentifier(getTokenText());
81+
if (getTokenType() == SqlBaseParser.QUOTED_IDENTIFIER)
82+
return LabKeySql.unquoteIdentifier(getTokenText());
83+
return getTokenText();
8484
}
8585

8686

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
package org.labkey.query.sql;
2+
3+
import org.apache.commons.lang3.Strings;
4+
import org.jetbrains.annotations.NotNull;
5+
import org.jetbrains.annotations.Nullable;
6+
import org.labkey.api.data.BaseColumnInfo;
7+
import org.labkey.api.data.ContainerFilter;
8+
import org.labkey.api.data.JdbcType;
9+
import org.labkey.api.data.SQLFragment;
10+
import org.labkey.api.data.TableInfo;
11+
import org.labkey.api.data.dialect.SqlDialect;
12+
import org.labkey.api.exp.api.ExpLineageOptions;
13+
import org.labkey.api.exp.api.ExperimentService;
14+
import org.labkey.api.exp.query.ExpTable;
15+
import org.labkey.api.query.FieldKey;
16+
import org.labkey.api.query.QueryParseException;
17+
import org.labkey.api.query.column.BuiltInColumnTypes;
18+
import org.labkey.data.xml.ColumnType;
19+
20+
import java.util.Collection;
21+
import java.util.List;
22+
import java.util.Map;
23+
import java.util.Objects;
24+
import java.util.Set;
25+
26+
public class QueryLineage extends AbstractQueryRelation
27+
{
28+
final QuerySelect sourceSelect;
29+
final boolean ancestors;
30+
31+
QueryLineage(Query query, QNode token, QuerySelect source, String alias, boolean ancestors)
32+
{
33+
super(query, query.getSchema(), Objects.toString(alias, "explineage_" + query.incrementAliasCounter()));
34+
this.sourceSelect = source;
35+
this.ancestors = ancestors;
36+
37+
sourceSelect.setSkipSuggestedColumns(true);
38+
var all = source.getAllColumns();
39+
if (1 != all.size())
40+
{
41+
query.getParseErrors().add(new QueryParseException(token.getTokenText() + " subquery must have one column", null, token.getLine(), token.getColumn()));
42+
}
43+
else
44+
{
45+
var relationColumn = all.values().iterator().next();
46+
var col = new BaseColumnInfo("?");
47+
relationColumn.copyColumnAttributesTo(col);
48+
if (!BuiltInColumnTypes.EXPOBJECTID_CONCEPT_URI.equals(col.getConceptURI()))
49+
query.getParseErrors().add(new QueryParseException(token.getTokenText() + " requires an object column", null, token.getLine(), token.getColumn()));
50+
}
51+
}
52+
53+
@Override
54+
public void declareFields()
55+
{
56+
sourceSelect.declareFields();
57+
}
58+
59+
@Override
60+
public void resolveFields()
61+
{
62+
sourceSelect.resolveFields();
63+
}
64+
65+
@Override
66+
public TableInfo getTableInfo()
67+
{
68+
throw new UnsupportedOperationException();
69+
}
70+
71+
@Override
72+
public Map<String, RelationColumn> getAllColumns()
73+
{
74+
return Map.of(
75+
"Depth", Objects.requireNonNull(getColumn("depth")),
76+
"FromObject", Objects.requireNonNull(getColumn("fromobject")),
77+
"ToObject", Objects.requireNonNull(getColumn("toobject")));
78+
}
79+
80+
@Override
81+
public @Nullable AbstractQueryRelation.RelationColumn getFirstColumn()
82+
{
83+
return getColumn("fromobject");
84+
}
85+
86+
@Override
87+
public @Nullable AbstractQueryRelation.RelationColumn getColumn(@NotNull String name)
88+
{
89+
return switch (name.toLowerCase())
90+
{
91+
case "depth", "fromobject", "toobject" -> new LineageColumn(name.toLowerCase());
92+
default -> null;
93+
};
94+
}
95+
96+
@Override
97+
public int getSelectedColumnCount()
98+
{
99+
return 3;
100+
}
101+
102+
@Override
103+
public @Nullable AbstractQueryRelation.RelationColumn getLookupColumn(@NotNull RelationColumn parent, @NotNull String name)
104+
{
105+
throw new UnsupportedOperationException();
106+
}
107+
108+
@Override
109+
public @Nullable AbstractQueryRelation.RelationColumn getLookupColumn(@NotNull RelationColumn parent, ColumnType.@NotNull Fk fk, @NotNull String name)
110+
{
111+
throw new UnsupportedOperationException();
112+
}
113+
114+
@Override
115+
public SQLFragment getSql()
116+
{
117+
throw new UnsupportedOperationException();
118+
}
119+
120+
@Override
121+
public SQLFragment getFromSql()
122+
{
123+
SqlDialect d = _query.getSchema().getDbSchema().getSqlDialect();
124+
125+
ExpLineageOptions options = new ExpLineageOptions(ancestors, !ancestors, 1000);
126+
options.setForLookup(true); // remove intermediate edges
127+
options.setUseObjectIds(true); // expObject() returns objectid not lsid
128+
129+
SQLFragment sql = new SQLFragment();
130+
sql.appendComment("<QueryLineage>", d);
131+
// CONSIDER: use CTE for sourceSelect.getSql()
132+
SQLFragment lineageSql = ExperimentService.get().generateExperimentTreeSQL(sourceSelect.getSql(), options);
133+
sql.append("(\n").append(lineageSql).append("\n) AS ").appendIdentifier(getAlias());
134+
sql.appendComment("</QueryLineage>", d);
135+
return sql;
136+
}
137+
138+
@Override
139+
public String getQueryText()
140+
{
141+
return (ancestors?"EXPANCESTORSOF":"EXPDESCENDANTSOF") + "(" + sourceSelect.getQueryText() + ")";
142+
}
143+
144+
@Override
145+
public void setContainerFilter(ContainerFilter containerFilter)
146+
{
147+
sourceSelect.setContainerFilter(containerFilter);
148+
}
149+
150+
@Override
151+
public Set<RelationColumn> getSuggestedColumns(Set<RelationColumn> selected)
152+
{
153+
return Set.of();
154+
}
155+
156+
157+
private class LineageColumn extends RelationColumn
158+
{
159+
final FieldKey _fieldKey;
160+
final String _alias;
161+
final JdbcType _jdbcType;
162+
163+
LineageColumn(String name)
164+
{
165+
String alias;
166+
FieldKey fk;
167+
JdbcType jdbcType = JdbcType.BIGINT;
168+
switch (name.toLowerCase())
169+
{
170+
case "depth":
171+
alias = "depth";
172+
fk = new FieldKey(null, "Depth");
173+
jdbcType = JdbcType.INTEGER;
174+
break;
175+
case "fromobject":
176+
alias = "self";
177+
fk = new FieldKey(null, "FromObject");
178+
break;
179+
case "toobject":
180+
alias = "objectid";
181+
fk = new FieldKey(null, "ToObject");
182+
break;
183+
default:
184+
throw new IllegalArgumentException("Unknown column name: " + name);
185+
}
186+
_fieldKey = fk;
187+
_alias = alias;
188+
_jdbcType = jdbcType;
189+
}
190+
191+
@Override
192+
SQLFragment getInternalSql()
193+
{
194+
return new SQLFragment().appendDottedIdentifiers(QueryLineage.this.getAlias(), getAlias());
195+
}
196+
197+
@Override
198+
void copyColumnAttributesTo(@NotNull BaseColumnInfo to)
199+
{
200+
to.setJdbcType(_jdbcType);
201+
if (!"depth".equals(_alias))
202+
{
203+
to.setConceptURI(BuiltInColumnTypes.EXPOBJECTID_CONCEPT_URI);
204+
to.setDisplayColumnFactory(ExpTable.ExpObjectDataColumn::new);
205+
}
206+
}
207+
208+
@Override
209+
public FieldKey getFieldKey()
210+
{
211+
return _fieldKey;
212+
}
213+
214+
@Override
215+
String getAlias()
216+
{
217+
return _alias;
218+
}
219+
220+
@Override
221+
AbstractQueryRelation getTable()
222+
{
223+
return QueryLineage.this;
224+
}
225+
226+
@Override
227+
boolean isHidden()
228+
{
229+
return false;
230+
}
231+
232+
@Override
233+
String getPrincipalConceptCode()
234+
{
235+
return "";
236+
}
237+
238+
@Override
239+
String getConceptURI()
240+
{
241+
return "";
242+
}
243+
244+
@Override
245+
public @NotNull JdbcType getJdbcType()
246+
{
247+
return _jdbcType;
248+
}
249+
250+
@Override
251+
public Collection<RelationColumn> gatherInvolvedSelectColumns(Collection<RelationColumn> collect)
252+
{
253+
return List.of();
254+
}
255+
}
256+
}

0 commit comments

Comments
 (0)