From 4da3dbf325de86de01ba8863accab1ecd0ee5bfa Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Sun, 11 Jan 2026 04:51:20 -0800 Subject: [PATCH 1/2] Join core.Documents rows to parent details --- .../announcements/model/AnnouncementType.java | 5 +- .../api/attachments/AttachmentParentType.java | 34 ++++++--- .../attachments/LookAndFeelResourceType.java | 7 ++ .../api/collections/LabKeyCollectors.java | 14 ++++ api/src/org/labkey/api/data/SQLFragment.java | 29 ++++--- api/src/org/labkey/api/exp/Lsid.java | 35 +++++---- .../exp/api/ExpProtocolAttachmentType.java | 5 +- .../api/exp/api/ExpRunAttachmentType.java | 5 +- .../api/files/FileSystemAttachmentType.java | 5 +- .../DefaultMigrationSchemaHandler.java | 19 ++--- api/src/org/labkey/api/query/QueryView.java | 2 +- .../labkey/api/reports/report/ReportType.java | 9 ++- .../api/security/AuthenticationLogoType.java | 11 ++- .../org/labkey/api/security/AvatarType.java | 5 +- api/src/org/labkey/api/util/GUID.java | 10 ++- core/src/org/labkey/core/CoreUpgradeCode.java | 3 +- .../attachment/AttachmentServiceImpl.java | 76 +++++++++++++------ .../org/labkey/core/query/DocumentsTable.java | 30 ++++++++ .../experiment/api/ExpDataClassType.java | 24 +++--- .../labkey/issue/model/IssueCommentType.java | 10 ++- .../org/labkey/list/view/ListItemType.java | 12 ++- .../model/SpecimenRequestEventType.java | 10 ++- .../study/model/ProtocolDocumentType.java | 5 +- wiki/src/org/labkey/wiki/model/WikiType.java | 5 +- 24 files changed, 238 insertions(+), 132 deletions(-) diff --git a/announcements/src/org/labkey/announcements/model/AnnouncementType.java b/announcements/src/org/labkey/announcements/model/AnnouncementType.java index 6b2e1808e6a..d94ae9d63d7 100644 --- a/announcements/src/org/labkey/announcements/model/AnnouncementType.java +++ b/announcements/src/org/labkey/announcements/model/AnnouncementType.java @@ -16,7 +16,6 @@ package org.labkey.announcements.model; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.labkey.api.announcements.CommSchema; import org.labkey.api.attachments.AttachmentParentType; import org.labkey.api.data.SQLFragment; @@ -41,8 +40,8 @@ public static AttachmentParentType get() } @Override - public @Nullable SQLFragment getSelectParentEntityIdsSql() + public @NotNull SQLFragment getSelectEntityIdAndDescriptionSql() { - return new SQLFragment("SELECT EntityId FROM ").append(CommSchema.getInstance().getTableInfoAnnouncements(), "ann"); + return new SQLFragment("SELECT EntityId, Title AS Description FROM ").append(CommSchema.getInstance().getTableInfoAnnouncements(), "ann"); } } diff --git a/api/src/org/labkey/api/attachments/AttachmentParentType.java b/api/src/org/labkey/api/attachments/AttachmentParentType.java index b24470cdf68..2e4c434f71d 100644 --- a/api/src/org/labkey/api/attachments/AttachmentParentType.java +++ b/api/src/org/labkey/api/attachments/AttachmentParentType.java @@ -16,7 +16,6 @@ package org.labkey.api.attachments; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.labkey.api.data.SQLFragment; /** @@ -25,7 +24,7 @@ */ public interface AttachmentParentType { - SQLFragment NO_ENTITY_IDS = new SQLFragment("SELECT NULL AS EntityId WHERE 1 = 0"); + SQLFragment NO_ROWS = new SQLFragment("SELECT NULL AS EntityId, NULL AS Description WHERE 1 = 0"); AttachmentParentType UNKNOWN = new AttachmentParentType() { @@ -41,6 +40,12 @@ public void addWhereSql(SQLFragment sql, String parentColumn, String documentNam { sql.append("0 = 1"); } + + @Override + public @NotNull SQLFragment getSelectEntityIdAndDescriptionSql() + { + return NO_ROWS; + } }; // A short, human-friendly, unique name for this attachment parent type @@ -56,20 +61,27 @@ public void addWhereSql(SQLFragment sql, String parentColumn, String documentNam default void addWhereSql(SQLFragment sql, String parentColumn, String documentNameColumn) { SQLFragment selectSql = getSelectParentEntityIdsSql(); - if (selectSql == null) - throw new IllegalStateException("Must override either addWhereSql() or getSelectParentEntityIdsSql()"); sql.append(parentColumn).append(" IN (").append(selectSql).append(")"); } /** - * Return a SQLFragment that selects all the EntityIds that might be attachment parents from the table(s) that - * provide attachments of this type, without involving the core.Documents table. For example, - * {@code SELECT EntityId FROM comm.Announcements}. Return null if this is not-yet-implemented or inappropriate. - * For example, some attachments' parents are container IDs. If the method determines that no parents exist, then - * return a valid query that selects no rows, for example, {@code NO_ENTITY_IDS}. + * Return a SQLFragment that selects just the EntityId of rows that might be attachment parents from the table(s) + * that provide attachments of this type, without involving the core.Documents table. */ - default @Nullable SQLFragment getSelectParentEntityIdsSql() + default @NotNull SQLFragment getSelectParentEntityIdsSql() { - return null; + SQLFragment selectSql = getSelectEntityIdAndDescriptionSql(); + + // The returned SQL is always used inside a subselect, so the alias doesn't have to be unique + return new SQLFragment("SELECT EntityId FROM (").append(selectSql).append(") x"); } + + /** + * Return a SQLFragment that selects the EntityId and an appropriate Description of all rows that might be + * attachment parents from the table(s) that provide attachment parents of this type, without involving the + * core.Documents table. For example, {@code SELECT EntityId, Title AS Description FROM comm.Announcements}. + * If the method determines that no parents exist, then return a valid query that selects no rows, for example, + * {@code NO_ROWS}. + */ + @NotNull SQLFragment getSelectEntityIdAndDescriptionSql(); } diff --git a/api/src/org/labkey/api/attachments/LookAndFeelResourceType.java b/api/src/org/labkey/api/attachments/LookAndFeelResourceType.java index a963da92611..6b16b819398 100644 --- a/api/src/org/labkey/api/attachments/LookAndFeelResourceType.java +++ b/api/src/org/labkey/api/attachments/LookAndFeelResourceType.java @@ -49,4 +49,11 @@ public void addWhereSql(SQLFragment sql, String parentColumn, String documentNam sql.append(documentNameColumn).append(" LIKE '" + AttachmentCache.LOGO_FILE_NAME_PREFIX + "%' OR "); sql.append(documentNameColumn).append(" LIKE '" + AttachmentCache.MOBILE_LOGO_FILE_NAME_PREFIX + "%')"); } + + @Override + public @NotNull SQLFragment getSelectEntityIdAndDescriptionSql() + { + return new SQLFragment("SELECT EntityId, CASE WHEN Name IS NULL THEN '' ELSE Name END AS Description FROM ") + .append(CoreSchema.getInstance().getTableInfoContainers()); + } } diff --git a/api/src/org/labkey/api/collections/LabKeyCollectors.java b/api/src/org/labkey/api/collections/LabKeyCollectors.java index 761ed76f649..8c8549220e0 100644 --- a/api/src/org/labkey/api/collections/LabKeyCollectors.java +++ b/api/src/org/labkey/api/collections/LabKeyCollectors.java @@ -6,6 +6,7 @@ import org.json.JSONArray; import org.junit.Assert; import org.junit.Test; +import org.labkey.api.data.SQLFragment; import org.labkey.api.util.HtmlString; import org.labkey.api.util.HtmlStringBuilder; @@ -14,6 +15,7 @@ import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -222,6 +224,18 @@ public static Collector joining(HtmlS ); } + /** + * Returns a {@link Collector} that joins {@link SQLFragment}s into a single {@link SQLFragment} separated by delimiter + */ + public static Collector, SQLFragment> joining(SQLFragment delimiter) { + return Collector.of( + LinkedList::new, + List::add, + (list1, list2) -> {list1.addAll(list2); return list1;}, + (list) -> SQLFragment.join(list, delimiter) + ); + } + public static class TestCase extends Assert { @Test diff --git a/api/src/org/labkey/api/data/SQLFragment.java b/api/src/org/labkey/api/data/SQLFragment.java index 20fe5ca1ed3..f282a38d25c 100644 --- a/api/src/org/labkey/api/data/SQLFragment.java +++ b/api/src/org/labkey/api/data/SQLFragment.java @@ -47,7 +47,6 @@ import java.util.TreeSet; import java.util.regex.Pattern; import java.util.stream.Collectors; -import java.util.stream.StreamSupport; import static org.labkey.api.query.ExprColumn.STR_TABLE_ALIAS; @@ -1339,25 +1338,23 @@ public int hashCode() * concatenation using the provided separator. The parameters are combined to form the new parameter list. * * @param fragments SQLFragments to join together - * @param separator Separator to use on the SQL portion + * @param separator Separator to use * @return A new SQLFragment that joins all the SQLFragments */ - public static SQLFragment join(Iterable fragments, String separator) + public static SQLFragment join(Iterable fragments, SQLFragment separator) { - if (separator.contains("?")) - throw new IllegalStateException("separator must not include a parameter marker"); + SQLFragment join = new SQLFragment(); + boolean first = true; - // Join all the SQL statements - String sql = StreamSupport.stream(fragments.spliterator(), false) - .map(SQLFragment::getSQL) - .collect(Collectors.joining(separator)); - - // Collect all the parameters to a single list - List params = StreamSupport.stream(fragments.spliterator(), false) - .map(SQLFragment::getParams) - .flatMap(Collection::stream) - .collect(Collectors.toList()); + for (SQLFragment fragment : fragments) + { + if (first) + first = false; + else + join.append(separator); + join.append(fragment); + } - return new SQLFragment(sql, params); + return join; } } diff --git a/api/src/org/labkey/api/exp/Lsid.java b/api/src/org/labkey/api/exp/Lsid.java index d195b1af822..e10744304dd 100644 --- a/api/src/org/labkey/api/exp/Lsid.java +++ b/api/src/org/labkey/api/exp/Lsid.java @@ -25,8 +25,10 @@ import org.junit.BeforeClass; import org.junit.Test; import org.labkey.api.data.Builder; +import org.labkey.api.data.SQLFragment; import org.labkey.api.data.dialect.SqlDialect; import org.labkey.api.settings.AppProps; +import org.labkey.api.util.GUID; import org.labkey.api.util.Pair; import java.net.URI; @@ -36,8 +38,6 @@ import java.nio.charset.StandardCharsets; import java.util.Objects; -import static org.apache.commons.lang3.StringUtils.repeat; - /** * Life-sciences identifier (LSID). A structured URI to describe things like samples, data files, assay runs, and protocols. * User: migra @@ -122,7 +122,6 @@ private Lsid(String src, String authority, String namespace, String objectId, St * * To spend less time parsing, maybe cached parsed Lsid in ExpObjectImpl? (not that straightforward) */ - @Nullable private static String[] parseLsid(@Nullable String s) { @@ -138,28 +137,32 @@ private static String[] parseLsid(@Nullable String s) return new String[] {parts[2], parts[3], parts[4], parts.length < 6 ? null : parts[5]}; } - - // Keep in sync with LSID_REGEX (above) - public static Pair getSqlExpressionToExtractObjectId(String lsidExpression, SqlDialect dialect) + // Keep in sync with LSID_REGEX (above). Note: AttachmentServiceImpl.TestCase.testLsidGuidExtraction tests this. + public static Pair getSqlExpressionToExtractObjectId(SQLFragment lsidExpression, SqlDialect dialect) { + String objectId = GUID.SQL_LIKE_GUID_PATTERN; + if (dialect.isPostgreSQL()) { - // PostgreSQL SUBSTRING supports simple regular expressions. This captures all the text from the third - // colon to the end of the string (or to the fourth colon, if present). - String expression = "SUBSTRING(" + lsidExpression + " FROM '%urn:lsid:%:#\"%#\":?%' FOR '#')"; - String where = lsidExpression + " SIMILAR TO '%urn:lsid:%:[0-9a-f\\-]{36}:?%'"; + // PostgreSQL SUBSTRING supports simple regular expressions. This captures all the text from the fourth + // colon to the end of the string (or to the fifth colon, if present). + SQLFragment expression = new SQLFragment("SUBSTRING(") + .append(lsidExpression) + .append(" FROM '%urn:lsid:%:%:#\"[0-9a-f\\-]{36}#\":?%' FOR '#')"); + SQLFragment where = new SQLFragment(lsidExpression).append(" SIMILAR TO '%urn:lsid:%:%:[0-9a-f\\-]{36}:?%'"); return new Pair<>(expression, where); } if (dialect.isSqlServer()) { - // SQL Server doesn't support regular expressions; this uses an unwieldy pattern to extract the objectid - String d = "[0-9a-f]"; // pattern for a single digit - String objectId = repeat(d, 8) + "-" + repeat(d, 4) + "-" + repeat(d, 4) + "-" + repeat(d, 4) + "-" + repeat(d, 12); - - String expression = "SUBSTRING(" + lsidExpression + ", PATINDEX('%:" + objectId + "%', " + lsidExpression + ") + 1, 36)"; - String where = lsidExpression + " LIKE '%urn:lsid:%:" + objectId + "%'"; + // SQL Server doesn't support regular expressions + SQLFragment expression = new SQLFragment("SUBSTRING(") + .append(lsidExpression) + .append(", PATINDEX('%:" + objectId + "%', ") + .append(lsidExpression) + .append(") + 1, 36)"); + SQLFragment where = new SQLFragment(lsidExpression).append(" LIKE '%urn:lsid:%:%:" + objectId + "%'"); return new Pair<>(expression, where); } diff --git a/api/src/org/labkey/api/exp/api/ExpProtocolAttachmentType.java b/api/src/org/labkey/api/exp/api/ExpProtocolAttachmentType.java index fe01000b4d1..388ad108dad 100644 --- a/api/src/org/labkey/api/exp/api/ExpProtocolAttachmentType.java +++ b/api/src/org/labkey/api/exp/api/ExpProtocolAttachmentType.java @@ -16,7 +16,6 @@ package org.labkey.api.exp.api; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.labkey.api.attachments.AttachmentParentType; import org.labkey.api.data.SQLFragment; @@ -40,8 +39,8 @@ private ExpProtocolAttachmentType() } @Override - public @Nullable SQLFragment getSelectParentEntityIdsSql() + public @NotNull SQLFragment getSelectEntityIdAndDescriptionSql() { - return new SQLFragment("SELECT EntityId FROM ").append(ExperimentService.get().getTinfoProtocol(), "ep"); + return new SQLFragment("SELECT EntityId, Name AS Description FROM ").append(ExperimentService.get().getTinfoProtocol(), "ep"); } } \ No newline at end of file diff --git a/api/src/org/labkey/api/exp/api/ExpRunAttachmentType.java b/api/src/org/labkey/api/exp/api/ExpRunAttachmentType.java index 38fa4c42319..8b50345b93f 100644 --- a/api/src/org/labkey/api/exp/api/ExpRunAttachmentType.java +++ b/api/src/org/labkey/api/exp/api/ExpRunAttachmentType.java @@ -16,7 +16,6 @@ package org.labkey.api.exp.api; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.labkey.api.attachments.AttachmentParentType; import org.labkey.api.data.SQLFragment; @@ -40,8 +39,8 @@ private ExpRunAttachmentType() } @Override - public @Nullable SQLFragment getSelectParentEntityIdsSql() + public @NotNull SQLFragment getSelectEntityIdAndDescriptionSql() { - return new SQLFragment("SELECT EntityId FROM ").append(ExperimentService.get().getTinfoExperimentRun(), "er"); + return new SQLFragment("SELECT EntityId, Name AS Description FROM ").append(ExperimentService.get().getTinfoExperimentRun(), "er"); } } \ No newline at end of file diff --git a/api/src/org/labkey/api/files/FileSystemAttachmentType.java b/api/src/org/labkey/api/files/FileSystemAttachmentType.java index e02b61a3d18..5f739764a76 100644 --- a/api/src/org/labkey/api/files/FileSystemAttachmentType.java +++ b/api/src/org/labkey/api/files/FileSystemAttachmentType.java @@ -16,7 +16,6 @@ package org.labkey.api.files; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.labkey.api.attachments.AttachmentParentType; import org.labkey.api.data.CoreSchema; import org.labkey.api.data.SQLFragment; @@ -41,8 +40,8 @@ private FileSystemAttachmentType() } @Override - public @Nullable SQLFragment getSelectParentEntityIdsSql() + public @NotNull SQLFragment getSelectEntityIdAndDescriptionSql() { - return new SQLFragment("SELECT EntityId FROM ").append(CoreSchema.getInstance().getMappedDirectories(), "md"); + return new SQLFragment("SELECT EntityId, Name AS Description FROM ").append(CoreSchema.getInstance().getMappedDirectories(), "md"); } } diff --git a/api/src/org/labkey/api/migration/DefaultMigrationSchemaHandler.java b/api/src/org/labkey/api/migration/DefaultMigrationSchemaHandler.java index bd2ffae6705..e78365dec28 100644 --- a/api/src/org/labkey/api/migration/DefaultMigrationSchemaHandler.java +++ b/api/src/org/labkey/api/migration/DefaultMigrationSchemaHandler.java @@ -26,7 +26,6 @@ import org.labkey.api.query.FieldKey; import org.labkey.api.query.SchemaKey; import org.labkey.api.query.TableSorter; -import org.labkey.api.util.ConfigurationException; import org.labkey.api.util.GUID; import org.labkey.api.util.StringUtilsLabKey; import org.labkey.api.util.logging.LogHelper; @@ -249,20 +248,14 @@ public Collection copyAttachments(DatabaseMigrationConfigu // requires querying and re-filtering the source tables instead. Collection ret = new LinkedList<>(); + // TODO: Select ParentType as well to avoid duplication getAttachmentTypes().forEach(type -> { SQLFragment sql = type.getSelectParentEntityIdsSql(); - if (sql != null) - { - Collection entityIds = new SqlSelector(targetSchema, sql).getCollection(String.class); - SQLFragment selectParents = new SQLFragment("Parent"); - // This query against the source database is likely to contain a large IN clause, so use an alternative InClauseGenerator - sourceSchema.getSqlDialect().appendInClauseSqlWithCustomInClauseGenerator(selectParents, entityIds, getTempTableInClauseGenerator(sourceSchema.getScope())); - ret.addAll(copyAttachments(configuration, new SQLClause(selectParents), type)); - } - else - { - throw new ConfigurationException("AttachmentType \"" + type.getUniqueName() + "\" is not configured to find parent EntityIds!"); - } + Collection entityIds = new SqlSelector(targetSchema, sql).getCollection(String.class); + SQLFragment selectParents = new SQLFragment("Parent"); + // This query against the source database is likely to contain a large IN clause, so use an alternative InClauseGenerator + sourceSchema.getSqlDialect().appendInClauseSqlWithCustomInClauseGenerator(selectParents, entityIds, getTempTableInClauseGenerator(sourceSchema.getScope())); + ret.addAll(copyAttachments(configuration, new SQLClause(selectParents), type)); }); return ret; diff --git a/api/src/org/labkey/api/query/QueryView.java b/api/src/org/labkey/api/query/QueryView.java index a135fc5cf39..be99e20907c 100644 --- a/api/src/org/labkey/api/query/QueryView.java +++ b/api/src/org/labkey/api/query/QueryView.java @@ -143,7 +143,7 @@ /** * View that generates the majority of standard data grids/tables in the LabKey Server UI. - * The backing query is lazily invoked when it comes times to render the QueryView. + * The backing query is lazily invoked when it comes time to render the QueryView. */ public class QueryView extends WebPartView implements ContainerUser { diff --git a/api/src/org/labkey/api/reports/report/ReportType.java b/api/src/org/labkey/api/reports/report/ReportType.java index 94ddf5e147c..135b2f6d896 100644 --- a/api/src/org/labkey/api/reports/report/ReportType.java +++ b/api/src/org/labkey/api/reports/report/ReportType.java @@ -19,6 +19,7 @@ import org.labkey.api.attachments.AttachmentParentType; import org.labkey.api.data.CoreSchema; import org.labkey.api.data.SQLFragment; +import org.labkey.api.data.TableInfo; public class ReportType implements AttachmentParentType { @@ -40,8 +41,12 @@ private ReportType() } @Override - public @NotNull SQLFragment getSelectParentEntityIdsSql() + public @NotNull SQLFragment getSelectEntityIdAndDescriptionSql() { - return new SQLFragment("SELECT EntityId FROM ").append(CoreSchema.getInstance().getTableInfoReport(), "reports"); + TableInfo table = CoreSchema.getInstance().getTableInfoReport(); + return new SQLFragment("SELECT EntityId, ") + .append(table.getSqlDialect().concatenate("ReportKey", "':'", "CAST(RowId AS VARCHAR)")) + .append(" AS Description FROM ") + .append(table, "reports"); } } diff --git a/api/src/org/labkey/api/security/AuthenticationLogoType.java b/api/src/org/labkey/api/security/AuthenticationLogoType.java index df528ad07ee..3dcaf8f5747 100644 --- a/api/src/org/labkey/api/security/AuthenticationLogoType.java +++ b/api/src/org/labkey/api/security/AuthenticationLogoType.java @@ -16,10 +16,10 @@ package org.labkey.api.security; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.labkey.api.attachments.AttachmentParentType; import org.labkey.api.data.CoreSchema; import org.labkey.api.data.SQLFragment; +import org.labkey.api.data.TableInfo; public class AuthenticationLogoType implements AttachmentParentType { @@ -41,8 +41,13 @@ private AuthenticationLogoType() } @Override - public @Nullable SQLFragment getSelectParentEntityIdsSql() + public @NotNull SQLFragment getSelectEntityIdAndDescriptionSql() { - return new SQLFragment("SELECT EntityId FROM ").append(CoreSchema.getInstance().getTableInfoAuthenticationConfigurations(), "acs"); + TableInfo table = CoreSchema.getInstance().getTableInfoAuthenticationConfigurations(); + + return new SQLFragment("SELECT EntityId, ") + .append(table.getSqlDialect().concatenate("'Configuration #'", "CAST(RowId AS VARCHAR)")) + .append(" AS Description FROM ") + .append(table, "acs"); } } diff --git a/api/src/org/labkey/api/security/AvatarType.java b/api/src/org/labkey/api/security/AvatarType.java index 4c9cf06235b..c11c6e1a648 100644 --- a/api/src/org/labkey/api/security/AvatarType.java +++ b/api/src/org/labkey/api/security/AvatarType.java @@ -16,7 +16,6 @@ package org.labkey.api.security; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.labkey.api.attachments.AttachmentParentType; import org.labkey.api.data.CoreSchema; import org.labkey.api.data.SQLFragment; @@ -44,8 +43,8 @@ private AvatarType() } @Override - public @Nullable SQLFragment getSelectParentEntityIdsSql() + public @NotNull SQLFragment getSelectEntityIdAndDescriptionSql() { - return new SQLFragment("SELECT EntityId FROM ").append(CoreSchema.getInstance().getTableInfoUsers(), "users"); + return new SQLFragment("SELECT EntityId, DisplayName AS Description FROM ").append(CoreSchema.getInstance().getTableInfoUsers(), "users"); } } diff --git a/api/src/org/labkey/api/util/GUID.java b/api/src/org/labkey/api/util/GUID.java index 287b51c5397..c245e355944 100644 --- a/api/src/org/labkey/api/util/GUID.java +++ b/api/src/org/labkey/api/util/GUID.java @@ -38,6 +38,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static org.apache.commons.lang3.StringUtils.repeat; + /** * Create GUID that looks like this f082cbda-b574-4e1e-9dba-b9e9b377f5b1. @@ -81,7 +83,13 @@ public class GUID implements Serializable, Parameter.JdbcParameterValue, SafeToR private static int nanoCounter = 0xffffffff; // Can be used to match GUID values in SQL - public static String SQL_LIKE_GUID_PATTERN = "[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]"; + public static final String SQL_LIKE_GUID_PATTERN; + + static + { + String d = "[0-9a-f]"; // pattern for a single digit + SQL_LIKE_GUID_PATTERN = repeat(d, 8) + "-" + repeat(d, 4) + "-" + repeat(d, 4) + "-" + repeat(d, 4) + "-" + repeat(d, 12); + } private static String genClockSeqAndReserved() { diff --git a/core/src/org/labkey/core/CoreUpgradeCode.java b/core/src/org/labkey/core/CoreUpgradeCode.java index fa78f0f3eaa..066f970fb7b 100644 --- a/core/src/org/labkey/core/CoreUpgradeCode.java +++ b/core/src/org/labkey/core/CoreUpgradeCode.java @@ -16,8 +16,8 @@ package org.labkey.core; import org.apache.logging.log4j.Logger; -import org.labkey.api.attachments.AttachmentService; import org.labkey.api.attachments.AttachmentParentType; +import org.labkey.api.attachments.AttachmentService; import org.labkey.api.data.CoreSchema; import org.labkey.api.data.DeferredUpgrade; import org.labkey.api.data.SQLFragment; @@ -99,6 +99,7 @@ public static void populateAttachmentParentTypeColumn(ModuleContext context) .append(" SET ParentType = ?") .add(type.getUniqueName()) .append(" WHERE "); + // TODO: This is the only caller of addWhereSql(), which can be removed when this upgrade code is deleted. type.addWhereSql(updateSql, "Parent", "DocumentName"); new SqlExecutor(CoreSchema.getInstance().getSchema()).execute(updateSql); diff --git a/core/src/org/labkey/core/attachment/AttachmentServiceImpl.java b/core/src/org/labkey/core/attachment/AttachmentServiceImpl.java index b5a3ff7198b..7eaf81a1d76 100644 --- a/core/src/org/labkey/core/attachment/AttachmentServiceImpl.java +++ b/core/src/org/labkey/core/attachment/AttachmentServiceImpl.java @@ -37,6 +37,7 @@ import org.labkey.api.audit.AuditLogService; import org.labkey.api.audit.provider.FileSystemAuditProvider; import org.labkey.api.collections.CaseInsensitiveHashSet; +import org.labkey.api.collections.LabKeyCollectors; import org.labkey.api.collections.Sets; import org.labkey.api.data.ColumnInfo; import org.labkey.api.data.ColumnRenderProperties; @@ -48,6 +49,7 @@ import org.labkey.api.data.DbSchema; import org.labkey.api.data.DbSchemaType; import org.labkey.api.data.DbScope; +import org.labkey.api.data.JdbcType; import org.labkey.api.data.Parameter; import org.labkey.api.data.ResultSetView; import org.labkey.api.data.RuntimeSQLException; @@ -128,9 +130,9 @@ import java.util.Date; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.TreeSet; @@ -280,7 +282,7 @@ public synchronized void addAttachments(AttachmentParent parent, List duplicates = findDuplicates(files); - if (duplicates.size() > 0) + if (!duplicates.isEmpty()) { throw new AttachmentService.DuplicateFilenameException(duplicates); } @@ -763,7 +765,7 @@ public Collection getAttachmentParentTypes() public WebPartView getFindAttachmentParentsView() { SQLFragment sql = new SQLFragment("SELECT RowId, CreatedBy, Created, ModifiedBy, Modified, Container, DocumentName, TableName FROM core.Documents LEFT OUTER JOIN (\n"); - addSelectAllEntityIdsSql(sql, Sets.newCaseInsensitiveHashSet("Audit")); + addSelectAllEntityIdsSql(sql, null, Sets.newCaseInsensitiveHashSet("Audit"), false); sql.append(") c ON EntityId = Parent\nORDER BY TableName, DocumentName, Container"); return getResultSetView(sql, "Probable Attachment Parents", null); @@ -773,53 +775,64 @@ public WebPartView getFindAttachmentParentsView() // - Enumerate all tables in all schemas in the labkey scope // - Enumerate columns and identify potential attachment parents (currently, EntityId columns and ObjectIds extracted from LSIDs) // - Create a UNION query that selects the candidate ids along with a constant column that lists the table name - private void addSelectAllEntityIdsSql(SQLFragment sql, Set userRequestedSchemasToIgnore) + private void addSelectAllEntityIdsSql(SQLFragment sql, @Nullable Set userRequestedSchemas, Set userRequestedSchemasToIgnore, boolean lsidsOnly) { - List selectStatements = new LinkedList<>(); Set schemasToIgnore = Sets.newCaseInsensitiveHashSet(userRequestedSchemasToIgnore); + String documentsSelectName = CoreSchema.getInstance().getTableInfoDocuments().getSelectName(); + if (documentsSelectName == null) + throw new IllegalStateException("core.Document select name is null"); // Temp schema causes problems because materialized tables disappear but stay in the cached list. This is probably a bug with // MaterializedQueryHelper... it should clear the temp DbSchema when it deletes a temp table. TODO: fix MQH & remove this workaround schemasToIgnore.add("temp"); - DbScope.getLabKeyScope().getSchemaNames().stream() + sql.append((userRequestedSchemas != null ? userRequestedSchemas.stream() : DbScope.getLabKeyScope().getSchemaNames().stream()) .filter(schemaName->!schemasToIgnore.contains(schemaName)) // Exclude unwanted schema names .map(schemaName->DbSchema.get(schemaName, DbSchemaType.Bare)) - .forEach(schema-> schema.getTableNames().stream() - .map(schema::getTable) - .filter(table->table.getTableType() == DatabaseTableType.TABLE) // We just want the underlying tables (no views or virtual tables) - .map(SchemaTableInfo::getColumns) - .flatMap(Collection::stream) - .filter(ColumnRenderProperties::isStringType) - .forEach(c->addSelectStatement(selectStatements, c)) - ); - - sql.append(StringUtils.join(selectStatements, " UNION\n")); + .flatMap(schema->schema.getTableNames().stream().map(schema::getTable)) + .filter(table->table.getTableType() == DatabaseTableType.TABLE) // We just want the underlying tables (no views or virtual tables) + .filter(table->!documentsSelectName.equals(table.getSelectName())) // Don't join to the Documents table! + .map(SchemaTableInfo::getColumns) + .flatMap(Collection::stream) + .filter(ColumnRenderProperties::isStringType) + .map(c->getSelectStatement(c, lsidsOnly)) + .filter(Objects::nonNull) + .collect(LabKeyCollectors.joining(new SQLFragment(" UNION\n")))); } - private void addSelectStatement(List selectStatements, ColumnInfo column) + private SQLFragment getSelectStatement(ColumnInfo column, boolean lsidsOnly) { - String expression; - String where = null; + SQLFragment expression; + SQLFragment where = null; - if (Strings.CI.contains(column.getName(), "EntityId")) + if (column.getJdbcType() == JdbcType.GUID && !lsidsOnly) { - // TODO convert all this to use SQLFragment - expression = column.getSelectIdentifier().getSql().getRawSQL(); + expression = column.getSelectIdentifier().getSql(); } else if (Strings.CI.endsWith(column.getName(), "LSID")) { - Pair pair = Lsid.getSqlExpressionToExtractObjectId(column.getSelectIdentifier().getSql().getRawSQL(), column.getSqlDialect()); + Pair pair = Lsid.getSqlExpressionToExtractObjectId(column.getSelectIdentifier().getSql(), column.getSqlDialect()); expression = pair.first; where = pair.second; } else { - return; + return null; } TableInfo table = column.getParentTable(); - selectStatements.add(" SELECT " + expression + " AS EntityId, " + table.getSqlDialect().quoteStringLiteral(table.getSelectName()) + " AS TableName FROM " + table.getSelectName() + (null != where ? " WHERE " + where : "") + "\n"); + SQLFragment sql = new SQLFragment(" SELECT ") + .append(expression) + .append(" AS EntityId, ? AS TableName FROM ") + .add(table.getSelectName()) + .append(table); + + if (null != where) + sql.append(" WHERE ").append(where); + + sql.append("\n"); + + return sql; } private WebPartView getResultSetView(SQLFragment sql, String title, @Nullable ActionURL linkUrl) @@ -1753,5 +1766,18 @@ private void testFileAttachmentFiles(File file1, File file2, User user) throws I attachments = service.getAttachments(root); assertEquals(originalCount, attachments.size()); } + + // Tests the ability to extract EntityIds from data class LSIDs + @Test + public void testLsidGuidExtraction() + { + SQLFragment sql = new SQLFragment(); + new AttachmentServiceImpl().addSelectAllEntityIdsSql(sql, Set.of("expdataclass"), Set.of(), true); + new SqlSelector(DbScope.getLabKeyScope(), sql).forEach(rs -> { + String entityId = rs.getString("entityid"); + if (!GUID.isGUID(entityId)) + fail(entityId + " from " + rs.getString("tablename") + " is not a valid GUID"); + }); + } } } diff --git a/core/src/org/labkey/core/query/DocumentsTable.java b/core/src/org/labkey/core/query/DocumentsTable.java index 824d2e70b5f..fab0094d6b5 100644 --- a/core/src/org/labkey/core/query/DocumentsTable.java +++ b/core/src/org/labkey/core/query/DocumentsTable.java @@ -1,12 +1,21 @@ package org.labkey.core.query; import org.jetbrains.annotations.NotNull; +import org.labkey.api.attachments.AttachmentService; +import org.labkey.api.collections.LabKeyCollectors; +import org.labkey.api.data.BaseColumnInfo; import org.labkey.api.data.ContainerFilter; import org.labkey.api.data.CoreSchema; +import org.labkey.api.data.JdbcType; import org.labkey.api.data.MutableColumnInfo; +import org.labkey.api.data.SQLFragment; +import org.labkey.api.data.dialect.SqlDialect; +import org.labkey.api.query.AliasManager; import org.labkey.api.query.FilteredTable; import org.labkey.api.query.UserIdQueryForeignKey; +import java.util.Objects; + public class DocumentsTable extends FilteredTable { public DocumentsTable(@NotNull CoreQuerySchema userSchema, ContainerFilter cf) @@ -23,5 +32,26 @@ public DocumentsTable(@NotNull CoreQuerySchema userSchema, ContainerFilter cf) getMutableColumnOrThrow("DocumentSize").setFormat("#,##0"); getMutableColumnOrThrow("Document").setHidden(true); getMutableColumnOrThrow("LastIndexed").setHidden(true); + addColumn(new BaseColumnInfo("ParentDescription", this, JdbcType.VARCHAR)); + } + + @Override + public @NotNull SQLFragment getFromSQL(String alias) + { + SqlDialect dialect = getSqlDialect(); + AliasManager am = new AliasManager(dialect); + SQLFragment parents = AttachmentService.get().getAttachmentParentTypes().stream() + .map(type -> new SQLFragment("SELECT ? AS ParentType, EntityId, Description FROM (") + .add(type.getUniqueName()) + .append(type.getSelectEntityIdAndDescriptionSql()) + .append(") ") + .appendIdentifier(am.decideAlias("x"))) + .filter(Objects::nonNull) + .collect(LabKeyCollectors.joining(new SQLFragment("\nUNION\n"))); + + return new SQLFragment("(SELECT d.*, CASE WHEN EntityId IS NULL THEN '' ELSE p.Description END AS ParentDescription FROM core.Documents d LEFT JOIN (\n") + .append(parents) + .append("\n) p ON d.Parent = p.EntityId AND d.ParentType = p.ParentType) ") + .append(alias); } } diff --git a/experiment/src/org/labkey/experiment/api/ExpDataClassType.java b/experiment/src/org/labkey/experiment/api/ExpDataClassType.java index dbfc5d2cb82..e98c7db886d 100644 --- a/experiment/src/org/labkey/experiment/api/ExpDataClassType.java +++ b/experiment/src/org/labkey/experiment/api/ExpDataClassType.java @@ -15,9 +15,7 @@ */ package org.labkey.experiment.api; -import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.labkey.api.attachments.AttachmentParentType; import org.labkey.api.data.Container; import org.labkey.api.data.ContainerManager; @@ -55,17 +53,17 @@ public static AttachmentParentType get() } @Override - public @Nullable SQLFragment getSelectParentEntityIdsSql() + public @NotNull SQLFragment getSelectEntityIdAndDescriptionSql() { TableInfo tableInfo = ExperimentService.get().getTinfoDataClass(); // Get a dialect-specific expression that can extract an ObjectId from the LSID column and a WHERE clause to // filter the rows to LSIDs containing ObjectIds - Pair pair = Lsid.getSqlExpressionToExtractObjectId("LSID", tableInfo.getSqlDialect()); - String expressionToExtractObjectId = pair.first; - String where = pair.second; + Pair pair = Lsid.getSqlExpressionToExtractObjectId(new SQLFragment("LSID"), tableInfo.getSqlDialect()); + SQLFragment expressionToExtractObjectId = pair.first; + SQLFragment where = pair.second; - List selectStatements = new LinkedList<>(); + List selectStatements = new LinkedList<>(); // Enumerate the rows in exp.DataClass new TableSelector(tableInfo, PageFlowUtil.set("Container", "LSID")).forEach(rs->{ @@ -77,12 +75,18 @@ public static AttachmentParentType get() // Add a select for the ObjectIds in this ExpDataClass if the domain includes an attachment column. ExpDataClass attachments // use the LSID's ObjectId as the attachment parent EntityId, so we need to use a SQL expression to extract it. if (null != domain && domain.getProperties().stream().anyMatch(p -> p.getPropertyType() == PropertyType.ATTACHMENT)) - selectStatements.add("\n SELECT " + expressionToExtractObjectId + " AS ID FROM expdataclass." + domain.getStorageTableName() + " WHERE " + where); + selectStatements.add( + new SQLFragment("\n SELECT ") + .append(expressionToExtractObjectId) + .append(" AS EntityId, Name AS Description FROM expdataclass.") + .append(domain.getStorageTableName()) + .append(" WHERE ").append(where) + ); }); return selectStatements.isEmpty() ? - NO_ENTITY_IDS : // No ExpDataClasses with attachment columns - new SQLFragment(StringUtils.join(selectStatements, "\n UNION")); + NO_ROWS : // No ExpDataClasses with attachment columns + SQLFragment.join(selectStatements, new SQLFragment("\n UNION")); } } diff --git a/issues/src/org/labkey/issue/model/IssueCommentType.java b/issues/src/org/labkey/issue/model/IssueCommentType.java index 80580b0fe0b..aafeabda4b9 100644 --- a/issues/src/org/labkey/issue/model/IssueCommentType.java +++ b/issues/src/org/labkey/issue/model/IssueCommentType.java @@ -18,6 +18,7 @@ import org.jetbrains.annotations.NotNull; import org.labkey.api.attachments.AttachmentParentType; import org.labkey.api.data.SQLFragment; +import org.labkey.api.data.TableInfo; import org.labkey.api.issues.IssuesSchema; public class IssueCommentType implements AttachmentParentType @@ -40,8 +41,13 @@ private IssueCommentType() } @Override - public @NotNull SQLFragment getSelectParentEntityIdsSql() + public @NotNull SQLFragment getSelectEntityIdAndDescriptionSql() { - return new SQLFragment("SELECT EntityId FROM ").append(IssuesSchema.getInstance().getTableInfoComments(), "comments"); + TableInfo table = IssuesSchema.getInstance().getTableInfoComments(); + + return new SQLFragment("SELECT EntityId, ") + .append(table.getSqlDialect().concatenate("'Issue #'", "CAST(IssueId AS VARCHAR)")) + .append(" AS Description FROM ") + .append(table, "comments"); } } diff --git a/list/src/org/labkey/list/view/ListItemType.java b/list/src/org/labkey/list/view/ListItemType.java index de4325164a6..1fa9bea2c44 100644 --- a/list/src/org/labkey/list/view/ListItemType.java +++ b/list/src/org/labkey/list/view/ListItemType.java @@ -15,9 +15,7 @@ */ package org.labkey.list.view; -import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.labkey.api.attachments.AttachmentParentType; import org.labkey.api.data.ContainerManager; import org.labkey.api.data.SQLFragment; @@ -50,24 +48,24 @@ private ListItemType() } @Override - public @Nullable SQLFragment getSelectParentEntityIdsSql() + public @NotNull SQLFragment getSelectEntityIdAndDescriptionSql() { ListService svc = ListService.get(); assert null != svc; - List selectStatements = new LinkedList<>(); + List selectStatements = new LinkedList<>(); ContainerManager.getAllChildren(ContainerManager.getRoot()).forEach(c -> { Map map = svc.getLists(c, null, false); map.forEach((k, v) -> { Domain domain = v.getDomain(); if (null != domain && domain.getProperties().stream().anyMatch(p -> p.getPropertyType() == PropertyType.ATTACHMENT)) - selectStatements.add("\n SELECT EntityId AS ID FROM list." + domain.getStorageTableName()); + selectStatements.add(new SQLFragment("\n SELECT EntityId, ? AS Description FROM list.", domain.getName()).append(domain.getStorageTableName())); }); }); return selectStatements.isEmpty() ? - NO_ENTITY_IDS : // No lists with attachment columns - new SQLFragment(StringUtils.join(selectStatements, "\n UNION")); + NO_ROWS : // No lists with attachment columns + SQLFragment.join(selectStatements, new SQLFragment("\n UNION")); } } diff --git a/specimen/src/org/labkey/specimen/model/SpecimenRequestEventType.java b/specimen/src/org/labkey/specimen/model/SpecimenRequestEventType.java index b30e6e1fa1c..ad10c6a8e7b 100644 --- a/specimen/src/org/labkey/specimen/model/SpecimenRequestEventType.java +++ b/specimen/src/org/labkey/specimen/model/SpecimenRequestEventType.java @@ -16,9 +16,9 @@ package org.labkey.specimen.model; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.labkey.api.attachments.AttachmentParentType; import org.labkey.api.data.SQLFragment; +import org.labkey.api.data.TableInfo; import org.labkey.api.specimen.SpecimenSchema; public class SpecimenRequestEventType implements AttachmentParentType @@ -41,8 +41,12 @@ private SpecimenRequestEventType() } @Override - public @Nullable SQLFragment getSelectParentEntityIdsSql() + public @NotNull SQLFragment getSelectEntityIdAndDescriptionSql() { - return new SQLFragment("SELECT EntityId FROM ").append(SpecimenSchema.get().getTableInfoSampleRequestEvent(), "sre"); + TableInfo table = SpecimenSchema.get().getTableInfoSampleRequestEvent(); + return new SQLFragment("SELECT EntityId, ") + .append(table.getSqlDialect().concatenate("'RowId:'", "CAST(RowId AS VARCHAR)")) + .append(" AS Description FROM ") + .append(table, "sre"); } } \ No newline at end of file diff --git a/study/src/org/labkey/study/model/ProtocolDocumentType.java b/study/src/org/labkey/study/model/ProtocolDocumentType.java index 56fdbd84daf..2b8452b92f6 100644 --- a/study/src/org/labkey/study/model/ProtocolDocumentType.java +++ b/study/src/org/labkey/study/model/ProtocolDocumentType.java @@ -16,7 +16,6 @@ package org.labkey.study.model; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.labkey.api.attachments.AttachmentParentType; import org.labkey.api.data.SQLFragment; import org.labkey.study.StudySchema; @@ -41,8 +40,8 @@ private ProtocolDocumentType() } @Override - public @Nullable SQLFragment getSelectParentEntityIdsSql() + public @NotNull SQLFragment getSelectEntityIdAndDescriptionSql() { - return new SQLFragment("SELECT ProtocolDocumentEntityId FROM ").append(StudySchema.getInstance().getTableInfoStudy(), "s"); + return new SQLFragment("SELECT ProtocolDocumentEntityId AS EntityId, Label AS Description FROM ").append(StudySchema.getInstance().getTableInfoStudy(), "s"); } } diff --git a/wiki/src/org/labkey/wiki/model/WikiType.java b/wiki/src/org/labkey/wiki/model/WikiType.java index 27f0350d363..a0260d30f77 100644 --- a/wiki/src/org/labkey/wiki/model/WikiType.java +++ b/wiki/src/org/labkey/wiki/model/WikiType.java @@ -16,7 +16,6 @@ package org.labkey.wiki.model; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.labkey.api.announcements.CommSchema; import org.labkey.api.attachments.AttachmentParentType; import org.labkey.api.data.SQLFragment; @@ -41,8 +40,8 @@ public static AttachmentParentType get() } @Override - public @Nullable SQLFragment getSelectParentEntityIdsSql() + public @NotNull SQLFragment getSelectEntityIdAndDescriptionSql() { - return new SQLFragment("SELECT EntityId FROM ").append(CommSchema.getInstance().getTableInfoPages(), "pages"); + return new SQLFragment("SELECT EntityId, Name AS Description FROM ").append(CommSchema.getInstance().getTableInfoPages(), "pages"); } } From c471839c83637720e7df33255a91279b833f9f63 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Sun, 11 Jan 2026 10:46:12 -0800 Subject: [PATCH 2/2] Fix container filter. Add Orphaned column. --- .../org/labkey/api/attachments/AttachmentParentType.java | 6 ------ core/src/org/labkey/core/query/DocumentsTable.java | 9 ++++++++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/api/src/org/labkey/api/attachments/AttachmentParentType.java b/api/src/org/labkey/api/attachments/AttachmentParentType.java index 2e4c434f71d..68bd8ca9e61 100644 --- a/api/src/org/labkey/api/attachments/AttachmentParentType.java +++ b/api/src/org/labkey/api/attachments/AttachmentParentType.java @@ -35,12 +35,6 @@ public String getUniqueName() return "Unknown"; } - @Override - public void addWhereSql(SQLFragment sql, String parentColumn, String documentNameColumn) - { - sql.append("0 = 1"); - } - @Override public @NotNull SQLFragment getSelectEntityIdAndDescriptionSql() { diff --git a/core/src/org/labkey/core/query/DocumentsTable.java b/core/src/org/labkey/core/query/DocumentsTable.java index fab0094d6b5..9e453d2f2fe 100644 --- a/core/src/org/labkey/core/query/DocumentsTable.java +++ b/core/src/org/labkey/core/query/DocumentsTable.java @@ -33,6 +33,9 @@ public DocumentsTable(@NotNull CoreQuerySchema userSchema, ContainerFilter cf) getMutableColumnOrThrow("Document").setHidden(true); getMutableColumnOrThrow("LastIndexed").setHidden(true); addColumn(new BaseColumnInfo("ParentDescription", this, JdbcType.VARCHAR)); + BaseColumnInfo orphaned = new BaseColumnInfo("Orphaned", this, JdbcType.BOOLEAN); + orphaned.setHidden(true); + addColumn(orphaned); } @Override @@ -49,7 +52,11 @@ public DocumentsTable(@NotNull CoreQuerySchema userSchema, ContainerFilter cf) .filter(Objects::nonNull) .collect(LabKeyCollectors.joining(new SQLFragment("\nUNION\n"))); - return new SQLFragment("(SELECT d.*, CASE WHEN EntityId IS NULL THEN '' ELSE p.Description END AS ParentDescription FROM core.Documents d LEFT JOIN (\n") + return new SQLFragment("(SELECT d.*, p.Description AS ParentDescription, ") + .append(dialect.wrapBooleanExpression(new SQLFragment("EntityId IS NULL"))) + .append(" AS Orphaned FROM ") + .append(super.getFromSQL("d")) // core.Documents with container filter applied + .append(" LEFT JOIN (\n") .append(parents) .append("\n) p ON d.Parent = p.EntityId AND d.ParentType = p.ParentType) ") .append(alias);