-
Notifications
You must be signed in to change notification settings - Fork 7
Add InventoryFilter. Clear orphaned issues. #7165
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
2fbcb73
Add InventoryFilter
labkey-adam 2871d4b
Share more code across provisioned and extensible table filtering
labkey-adam 85fd135
Delete orphaned issues rows
labkey-adam 8c9b76c
Thousands separators in data class & sample type counts
labkey-adam b95845e
Delete RelatedIssues based on RelatedIssueId as well
labkey-adam File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,10 +9,12 @@ | |
| import org.labkey.api.data.SimpleFilter.FilterClause; | ||
| import org.labkey.api.data.SimpleFilter.InClause; | ||
| import org.labkey.api.data.SimpleFilter.OrClause; | ||
| import org.labkey.api.data.SimpleFilter.SQLClause; | ||
| import org.labkey.api.query.FieldKey; | ||
| import org.labkey.api.query.SchemaKey; | ||
| import org.labkey.api.query.TableSorter; | ||
| import org.labkey.api.services.ServiceRegistry; | ||
| import org.labkey.api.util.ConfigurationException; | ||
| import org.labkey.api.util.GUID; | ||
| import org.labkey.api.util.logging.LogHelper; | ||
| import org.labkey.vfs.FileLike; | ||
|
|
@@ -30,7 +32,7 @@ public interface DatabaseMigrationService | |
| { | ||
| Logger LOG = LogHelper.getLogger(DatabaseMigrationService.class, "Information about database migration"); | ||
|
|
||
| record DomainFilter(Set<GUID> containers, String column, FilterClause condition) {} | ||
| record DataFilter(Set<GUID> containers, String column, FilterClause condition) {} | ||
|
|
||
| static @NotNull DatabaseMigrationService get() | ||
| { | ||
|
|
@@ -84,15 +86,25 @@ record Sequence(String schemaName, String tableName, String columnName, long las | |
|
|
||
| List<TableInfo> getTablesToCopy(); | ||
|
|
||
| // Create a filter clause that selects from all specified containers and (in some overrides) applies table-specific filters | ||
| FilterClause getTableFilter(TableInfo sourceTable, FieldKey containerFieldKey, Set<GUID> containers); | ||
|
|
||
| // Create a filter clause that selects from all specified containers | ||
| FilterClause getContainerClause(TableInfo sourceTable, FieldKey containerFieldKey, Set<GUID> containers); | ||
|
|
||
| // Return the FieldKey that can be used to filter this table by container. Special values SITE_WIDE_TABLE and | ||
| // DUMMY_FIELD_KEY can be returned for special behaviors. DUMMY_FIELD_KEY ensures that the handler's custom | ||
| // getContainerClause() is always called. SITE_WIDE_TABLE is used to select all rows. | ||
| @Nullable FieldKey getContainerFieldKey(TableInfo sourceTable); | ||
|
|
||
| void addDomainDataFilter(OrClause orClause, DomainFilter filter, TableInfo sourceTable, FieldKey fKey, Set<String> selectColumnNames); | ||
| // Create a filter clause that selects all rows from unfiltered containers plus filtered rows from the filtered containers | ||
| FilterClause getDomainDataFilter(Set<GUID> copyContainers, Set<GUID> filteredContainers, List<DataFilter> domainFilters, TableInfo sourceTable, FieldKey containerFieldKey, Set<String> selectColumnNames); | ||
|
|
||
| void addDomainDataFilter(OrClause orClause, DataFilter filter, TableInfo sourceTable, FieldKey fKey, Set<String> selectColumnNames); | ||
|
|
||
| // Do any necessary clean up after the target table has been populated. notCopiedFilter selects all rows in the | ||
| // source table that were NOT copied to the target table. (For example, they were filtered out due to container | ||
| // and/or domain data filtering.) | ||
| // source table that were NOT copied to the target table. (For example, rows in a global table not copied due to | ||
| // container filtering or rows in a provisioned table not copied due to domain data filtering.) | ||
| void afterTable(TableInfo sourceTable, TableInfo targetTable, SimpleFilter notCopiedFilter); | ||
|
|
||
| void afterSchema(DatabaseMigrationConfiguration configuration, DbSchema sourceSchema, DbSchema targetSchema, Map<String, Map<String, Sequence>> sequenceMap); | ||
|
|
@@ -144,9 +156,18 @@ public List<TableInfo> getTablesToCopy() | |
| .collect(Collectors.toCollection(ArrayList::new)); // Ensure mutable | ||
| } | ||
|
|
||
| @Override | ||
| public FilterClause getTableFilter(TableInfo sourceTable, FieldKey containerFieldKey, Set<GUID> containers) | ||
| { | ||
| return getContainerClause(sourceTable, containerFieldKey, containers); | ||
| } | ||
|
|
||
| @Override | ||
| public FilterClause getContainerClause(TableInfo sourceTable, FieldKey containerFieldKey, Set<GUID> containers) | ||
| { | ||
| if (containerFieldKey == SITE_WIDE_TABLE || containerFieldKey == DUMMY_FIELD_KEY) | ||
| throw new IllegalStateException("Should not be supplying " + containerFieldKey + " to the default getContainerClause() method"); | ||
|
|
||
| return new InClause(containerFieldKey, containers); | ||
| } | ||
|
|
||
|
|
@@ -192,14 +213,39 @@ public FilterClause getContainerClause(TableInfo sourceTable, FieldKey container | |
| } | ||
|
|
||
| @Override | ||
| public void addDomainDataFilter(OrClause orClause, DomainFilter filter, TableInfo sourceTable, FieldKey fKey, Set<String> selectColumnNames) | ||
| public FilterClause getDomainDataFilter(Set<GUID> copyContainers, Set<GUID> filteredContainers, List<DataFilter> domainFilters, TableInfo sourceTable, FieldKey fKey, Set<String> selectColumnNames) | ||
| { | ||
| addDomainDataStandardFilter(orClause, filter, sourceTable, fKey, selectColumnNames); | ||
| // Filtered case: remove the filtered containers from the unconditional container set | ||
| Set<GUID> otherContainers = new HashSet<>(copyContainers); | ||
| otherContainers.removeAll(filteredContainers); | ||
| FilterClause ret = getContainerClause(sourceTable, fKey, otherContainers); | ||
|
|
||
| OrClause orClause = new OrClause(); | ||
|
|
||
| // Delegate to the MigrationSchemaHandler to add domain-filtered containers back with their special filter applied | ||
| domainFilters.forEach(filter -> addDomainDataFilter(orClause, filter, sourceTable, fKey, selectColumnNames)); | ||
|
|
||
| if (!orClause.getClauses().isEmpty()) | ||
| { | ||
| orClause.addClause(ret); | ||
| ret = orClause; | ||
| } | ||
|
|
||
| return ret; | ||
| } | ||
|
|
||
| @Override | ||
| public void addDomainDataFilter(OrClause orClause, DataFilter filter, TableInfo sourceTable, FieldKey fKey, Set<String> selectColumnNames) | ||
| { | ||
| addDataFilter(orClause, filter, sourceTable, fKey, selectColumnNames); | ||
| } | ||
|
|
||
| protected void addDomainDataStandardFilter(OrClause orClause, DomainFilter filter, TableInfo sourceTable, FieldKey fKey, Set<String> selectColumnNames) | ||
| // Add a filter and return true if the column exists directly on the table | ||
| protected boolean addDataFilter(OrClause orClause, DataFilter filter, TableInfo sourceTable, FieldKey fKey, Set<String> selectColumnNames) | ||
| { | ||
| if (selectColumnNames.contains(filter.column())) | ||
| boolean columnExists = selectColumnNames.contains(filter.column()); | ||
|
|
||
| if (columnExists) | ||
| { | ||
| // Select all rows in this domain-filtered container that meet its criteria | ||
| orClause.addClause( | ||
|
|
@@ -209,37 +255,44 @@ protected void addDomainDataStandardFilter(OrClause orClause, DomainFilter filte | |
| ) | ||
| ); | ||
| } | ||
|
|
||
| return columnExists; | ||
| } | ||
|
|
||
| // Special domain data filter method for provisioned tables that have a built-in Flag field | ||
| protected void addDomainDataFlagFilter(OrClause orClause, DomainFilter filter, TableInfo sourceTable, FieldKey fKey, Set<String> selectColumnNames) | ||
| // Add a filter to select all rows where the object property with <propertyId> equals the filter value | ||
| protected void addObjectPropertyFilter(OrClause orClause, DataFilter filter, TableInfo sourceTable, FieldKey fKey, int propertyId) | ||
| { | ||
| SQLFragment flagWhere = new SQLFragment("lsid IN (SELECT ObjectURI FROM exp.Object WHERE ObjectId IN (SELECT ObjectId FROM exp.ObjectProperty WHERE StringValue = ? AND PropertyId = ?))", filter.condition().getParamVals()[0], propertyId); | ||
|
|
||
| orClause.addClause( | ||
| new AndClause( | ||
| getContainerClause(sourceTable, fKey, filter.containers()), | ||
| new SQLClause(flagWhere) | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| // Special domain data filter method for provisioned tables that have a built-in Flag field (currently used by data classes) | ||
| protected void addDomainDataFlagFilter(OrClause orClause, DataFilter filter, TableInfo sourceTable, FieldKey fKey, Set<String> selectColumnNames) | ||
| { | ||
| if (filter.column().equalsIgnoreCase("Flag")) | ||
| { | ||
| SQLFragment flagWhere = new SQLFragment("lsid IN (SELECT ObjectURI FROM exp.Object WHERE ObjectId IN (SELECT ObjectId FROM exp.ObjectProperty WHERE StringValue = ? AND PropertyId = ?))", filter.condition().getParamVals()[0], getCommentPropertyId(sourceTable)); | ||
|
|
||
| // Select all rows where the built-in flag column equals the filter value | ||
| orClause.addClause( | ||
| new AndClause( | ||
| getContainerClause(sourceTable, fKey, filter.containers()), | ||
| new SimpleFilter.SQLClause(flagWhere) | ||
| ) | ||
| ); | ||
| addObjectPropertyFilter(orClause, filter, sourceTable, fKey, getCommentPropertyId(sourceTable.getSchema().getScope())); | ||
| } | ||
| else | ||
| { | ||
| addDomainDataStandardFilter(orClause, filter, sourceTable, fKey, selectColumnNames); | ||
| addDataFilter(orClause, filter, sourceTable, fKey, selectColumnNames); | ||
| } | ||
| } | ||
|
|
||
| private Integer _commentPropertyId = null; | ||
|
|
||
| protected synchronized int getCommentPropertyId(TableInfo sourceTable) | ||
| protected synchronized int getCommentPropertyId(DbScope scope) | ||
| { | ||
| if (_commentPropertyId == null) | ||
| { | ||
| // Get the exp.PropertyDescriptor table from the source scope | ||
| TableInfo propertyDescriptor = sourceTable.getSchema().getScope().getSchema("exp", DbSchemaType.Migration).getTable("PropertyDescriptor"); | ||
| TableInfo propertyDescriptor = scope.getSchema("exp", DbSchemaType.Migration).getTable("PropertyDescriptor"); | ||
| // Select the PropertyId associated with built-in Flag fields ("urn:exp.labkey.org/#Comment") | ||
| Integer propertyId = new TableSelector(propertyDescriptor, Collections.singleton("PropertyId"), new SimpleFilter(FieldKey.fromParts("PropertyURI"), "urn:exp.labkey.org/#Comment"), null).getObject(Integer.class); | ||
| if (propertyId == null) | ||
|
|
@@ -274,22 +327,6 @@ interface MigrationTableHandler | |
| FilterClause getAdditionalFilterClause(Set<GUID> containers); | ||
| } | ||
|
|
||
| abstract class DefaultMigrationTableHandler implements MigrationTableHandler | ||
| { | ||
| private final TableInfo _tableInfo; | ||
|
|
||
| public DefaultMigrationTableHandler(TableInfo tableInfo) | ||
| { | ||
| _tableInfo = tableInfo; | ||
| } | ||
|
|
||
| @Override | ||
| public TableInfo getTableInfo() | ||
| { | ||
| return _tableInfo; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * A MigrationFilter adds support for the named filter property in the migration configuration file. If present, | ||
| * saveFilter() is called with the container guid and property value. Modules can register these to present | ||
|
|
@@ -301,4 +338,26 @@ interface MigrationFilter | |
| // Implementations should validate guid nullity | ||
| void saveFilter(@Nullable GUID guid, String value); | ||
| } | ||
|
|
||
| // Helper method that parses a data filter then adds it and its container to the provided collections, coalescing | ||
| // cases where multiple containers specify the same filter | ||
| static void addDataFilter(String filterName, List<DataFilter> dataFilters, Set<GUID> filteredContainers, GUID guid, String filter) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Too bad filter isn't a FilterClause (but I don't know how we get to this point) |
||
| { | ||
| String[] filterParts = filter.split("="); | ||
| if (filterParts.length != 2) | ||
| throw new ConfigurationException("Bad " + filterName + " value; expected <columnName>=<value>: " + filter); | ||
|
|
||
| if (!filteredContainers.add(guid)) | ||
| throw new ConfigurationException("Duplicate " + filterName + " entry for container " + guid); | ||
|
|
||
| String column = filterParts[0]; | ||
| String value = filterParts[1]; | ||
| FilterClause clause = CompareType.EQUAL.createFilterClause(new FieldKey(null, column), value); | ||
| // If another container is already using this filter clause, then simply add this guid to that domain filter. | ||
| // Otherwise, add a new domain filter to the list. | ||
| dataFilters.stream() | ||
| .filter(df -> df.column().equals(column) && df.condition().equals(clause)) | ||
| .findFirst() | ||
| .ifPresentOrElse(df -> df.containers().add(guid), () -> dataFilters.add(new DataFilter(new HashSet<>(Set.of(guid)), filterParts[0], clause))); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since most exp tables have an object it, it might be worth having a parameter that indicates whether to use "lsid" or "objectid"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This runs the risk of the database actually evaluating ((SELECT ObjectURI FROM exp.Object WHERE ObjectId IN (SELECT ObjectId FROM exp.ObjectProperty WHERE StringValue = ? AND PropertyId = ?)) in a somewhat expensive way. It's index are optimized for objectid=? and propertyid=? joins.
Iff there is a performance issue this might be worth trying
lsid:: WHERE ? = (SELECT StringValue FROM (O WHERE objecturi=lsid) JOIN OP ON objectid WHERE PropertyId=?)
or
objectid:: WHERE ? = (SELECT StringValue FROM OP WHERE OP.ObjectId=.objectid PropertyId=?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is called to filter provisioned tables, none of which have ObjectId (that I've seen). Sample Type tables do have RowId (a material RowId) and that handler generates a custom join that takes advantage. I don't think we can generalize this method to handle all the ways we join into exp.ObjectProperty... in my current FB, I've improved the comments to make it clear this is for cases where LSID is the only option.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@labkey-matthewb Here's how I've rewritten the full data class provisioned table SELECT statement (with hard-coded values instead of placeholders). Any further suggestions? (Note that because we're constructing and passing around
FilterClauses, the first join has to be a sub-select.):There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. That makes sense. Simpler is usually better (or not worse) so the rewrite “looks” better to me (does that count as vibe coding)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or maybe you're saying this is better:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That’s how I would have written it by hand as a first guess (that kinda mirrors what we do for PropertyColumn), but if you have to stuff it into a filter clause, I can see why your rewrite makes sense. I would just keep these ideas in your back pocket if the performance is a problem.