@ -448,7 +448,8 @@ public extension SessionThreadViewModel {
let interactionAttachment : TypedTableAlias < InteractionAttachment > = TypedTableAlias ( )
let profile : TypedTableAlias < Profile > = TypedTableAlias ( )
let interactionTimestampMsColumnLiteral : SQL = SQL ( stringLiteral : Interaction . Columns . timestampMs . name )
let aggregateInteractionLiteral : SQL = SQL ( stringLiteral : " aggregateInteraction " )
let timestampMsColumnLiteral : SQL = SQL ( stringLiteral : Interaction . Columns . timestampMs . name )
let interactionStateInteractionIdColumnLiteral : SQL = SQL ( stringLiteral : RecipientState . Columns . interactionId . name )
let readReceiptTableLiteral : SQL = SQL ( stringLiteral : " readReceipt " )
let readReceiptReadTimestampMsColumnLiteral : SQL = SQL ( stringLiteral : RecipientState . Columns . readTimestampMs . name )
@ -459,9 +460,7 @@ public extension SessionThreadViewModel {
let interactionAttachmentAttachmentIdColumnLiteral : SQL = SQL ( stringLiteral : InteractionAttachment . Columns . attachmentId . name )
let interactionAttachmentInteractionIdColumnLiteral : SQL = SQL ( stringLiteral : InteractionAttachment . Columns . interactionId . name )
let interactionAttachmentAlbumIndexColumnLiteral : SQL = SQL ( stringLiteral : InteractionAttachment . Columns . albumIndex . name )
let groupMemberProfileIdColumnLiteral : SQL = SQL ( stringLiteral : GroupMember . Columns . profileId . name )
let groupMemberRoleColumnLiteral : SQL = SQL ( stringLiteral : GroupMember . Columns . role . name )
let groupMemberGroupIdColumnLiteral : SQL = SQL ( stringLiteral : GroupMember . Columns . groupId . name )
// / * * N o t e : * * T h e ` n u m C o l u m n s B e f o r e P r o f i l e s ` v a l u e * * M U S T * * m a t c h t h e n u m b e r o f f i e l d s b e f o r e
// / t h e ` V i e w M o d e l . c o n t a c t P r o f i l e K e y ` e n t r y b e l o w o t h e r w i s e t h e q u e r y w i l l f a i l t o
@ -470,7 +469,6 @@ public extension SessionThreadViewModel {
// / E x p l i c i t l y s e t d e f a u l t v a l u e s f o r t h e f i e l d s i g n o r e d f o r s e a r c h r e s u l t s
let numColumnsBeforeProfiles : Int = 12
let numColumnsBetweenProfilesAndAttachmentInfo : Int = 12 // T h e a t t a c h m e n t i n f o c o l u m n s w i l l b e c o m b i n e d
let request : SQLRequest < ViewModel > = " " "
SELECT
\ ( thread . alias [ Column . rowID ] ) AS \ ( ViewModel . rowIdKey ) ,
@ -485,26 +483,55 @@ public extension SessionThreadViewModel {
\ ( thread [ . onlyNotifyForMentions ] ) AS \ ( ViewModel . threadOnlyNotifyForMentionsKey ) ,
( \ ( typingIndicator [ . threadId ] ) IS NOT NULL ) AS \ ( ViewModel . threadContactIsTypingKey ) ,
\ ( Interaction. self ) . \ ( ViewModel . threadUnreadCountKey ) ,
\ ( Interaction. self ) . \ ( ViewModel . threadUnreadMentionCountKey ) ,
\ ( aggregateInteractionLiteral ) . \ ( ViewModel . threadUnreadCountKey ) ,
\ ( aggregateInteractionLiteral ) . \ ( ViewModel . threadUnreadMentionCountKey ) ,
\ ( ViewModel . contactProfileKey ) . * ,
\ ( ViewModel . closedGroupProfileFrontKey ) . * ,
\ ( ViewModel . closedGroupProfileBackKey ) . * ,
\ ( ViewModel . closedGroupProfileBackFallbackKey ) . * ,
\ ( closedGroup [ . name ] ) AS \ ( ViewModel . closedGroupNameKey ) ,
( \ ( ViewModel . currentUserIsClosedGroupMemberKey ) . profileId IS NOT NULL ) AS \ ( ViewModel . currentUserIsClosedGroupMemberKey ) ,
( \ ( ViewModel . currentUserIsClosedGroupAdminKey ) . profileId IS NOT NULL ) AS \ ( ViewModel . currentUserIsClosedGroupAdminKey ) ,
EXISTS (
SELECT 1
FROM \ ( GroupMember . self )
WHERE (
\ ( groupMember [ . groupId ] ) = \ ( closedGroup [ . threadId ] ) AND
\ ( SQL ( " \( groupMember [ . role ] ) != \( GroupMember . Role . zombie ) " ) ) AND
\ ( SQL ( " \( groupMember [ . profileId ] ) = \( userPublicKey ) " ) )
)
) AS \ ( ViewModel . currentUserIsClosedGroupMemberKey ) ,
EXISTS (
SELECT 1
FROM \ ( GroupMember . self )
WHERE (
\ ( groupMember [ . groupId ] ) = \ ( closedGroup [ . threadId ] ) AND
\ ( SQL ( " \( groupMember [ . role ] ) = \( GroupMember . Role . admin ) " ) ) AND
\ ( SQL ( " \( groupMember [ . profileId ] ) = \( userPublicKey ) " ) )
)
) AS \ ( ViewModel . currentUserIsClosedGroupAdminKey ) ,
\ ( openGroup [ . name ] ) AS \ ( ViewModel . openGroupNameKey ) ,
\ ( openGroup [ . imageData ] ) AS \ ( ViewModel . openGroupProfilePictureDataKey ) ,
\ ( Interaction . self ) . \ ( ViewModel . interactionIdKey ) ,
\ ( Interaction . self ) . \ ( ViewModel . interactionVariantKey ) ,
\ ( Interaction . self ) . \ ( interactionTimestampMsColumnLiteral ) AS \ ( ViewModel . interactionTimestampMsKey ) ,
\ ( Interaction . self ) . \ ( ViewModel . interactionBodyKey ) ,
\ ( interaction[ . id ] ) AS \ ( ViewModel . interactionIdKey ) ,
\ ( interaction[ . variant ] ) AS \ ( ViewModel . interactionVariantKey ) ,
\ ( interaction[ . timestampMs ] ) AS \ ( ViewModel . interactionTimestampMsKey ) ,
\ ( interaction[ . body ] ) AS \ ( ViewModel . interactionBodyKey ) ,
-- Default to ' sending ' assuming non - processed interaction when null
IFNULL ( MIN ( \ ( recipientState [ . state ] ) ) , \ ( SQL ( " \( RecipientState . State . sending ) " ) ) ) AS \ ( ViewModel . interactionStateKey ) ,
IFNULL ( (
SELECT \ ( recipientState [ . state ] )
FROM \ ( RecipientState . self )
WHERE (
\ ( recipientState [ . interactionId ] ) = \ ( interaction [ . id ] ) AND
-- Ignore ' skipped ' states
\ ( SQL ( " \( recipientState [ . state ] ) = \( RecipientState . State . sending ) " ) )
)
LIMIT 1
) , 0 ) AS \ ( ViewModel . interactionStateKey ) ,
( \ ( readReceiptTableLiteral ) . \ ( readReceiptReadTimestampMsColumnLiteral ) IS NOT NULL ) AS \ ( ViewModel . interactionHasAtLeastOneReadReceiptKey ) ,
( \ ( linkPreview [ . url ] ) IS NOT NULL ) AS \ ( ViewModel . interactionIsOpenGroupInvitationKey ) ,
@ -523,45 +550,39 @@ public extension SessionThreadViewModel {
FROM \ ( SessionThread . self )
LEFT JOIN \ ( Contact . self ) ON \ ( contact [ . id ] ) = \ ( thread [ . id ] )
LEFT JOIN \ ( ThreadTypingIndicator . self ) ON \ ( typingIndicator [ . threadId ] ) = \ ( thread [ . id ] )
LEFT JOIN (
-- Fetch all interaction - specific data in a subquery to be more efficient
SELECT
\ ( interaction [ . id ] ) AS \ ( ViewModel . interactionIdKey ) ,
\ ( interaction [ . threadId ] ) ,
\ ( interaction [ . variant ] ) AS \ ( ViewModel . interactionVariantKey ) ,
MAX ( \ ( interaction [ . timestampMs ] ) ) AS \ ( interactionTimestampMsColumnLiteral ) ,
\ ( interaction [ . body ] ) AS \ ( ViewModel . interactionBodyKey ) ,
\ ( interaction [ . authorId ] ) ,
\ ( interaction [ . linkPreviewUrl ] ) ,
\ ( interaction [ . threadId ] ) AS \ ( ViewModel . threadIdKey ) ,
MAX ( \ ( interaction [ . timestampMs ] ) ) AS \ ( timestampMsColumnLiteral ) ,
SUM ( \ ( interaction [ . wasRead ] ) = false ) AS \ ( ViewModel . threadUnreadCountKey ) ,
SUM ( \ ( interaction [ . wasRead ] ) = false AND \ ( interaction [ . hasMention ] ) = true ) AS \ ( ViewModel . threadUnreadMentionCountKey )
FROM \ ( Interaction . self )
WHERE \ ( SQL ( " \( interaction [ . variant ] ) != \( Interaction . Variant . standardIncomingDeleted ) " ) )
GROUP BY \ ( interaction [ . threadId ] )
) AS \ ( Interaction. self ) ON \ ( interaction [ . threadId ] ) = \ ( thread [ . id ] )
) AS \ ( aggregateInteractionLiteral) ON \ ( aggregateInteractionLiteral ) . \ ( ViewModel . threadIdKey ) = \ ( thread [ . id ] )
LEFT JOIN \ ( RecipientState . self ) ON (
-- Ignore ' skipped ' states
\ ( SQL ( " \( recipientState [ . state ] ) != \( RecipientState . State . skipped ) " ) ) AND
\ ( recipientState [ . interactionId ] ) = \ ( Interaction . self ) . \ ( ViewModel . interactionIdKey )
LEFT JOIN \ ( Interaction . self ) ON (
\ ( interaction [ . threadId ] ) = \ ( thread [ . id ] ) AND
\ ( interaction [ . id ] ) = \ ( aggregateInteractionLiteral ) . \ ( ViewModel . interactionIdKey )
)
LEFT JOIN \ ( RecipientState . self ) AS \ ( readReceiptTableLiteral ) ON (
\ ( readReceiptTableLiteral) . \ ( readReceiptReadTimestampMsColumnLiteral) IS NOT NULL AND
\ ( Interaction. self ) . \ ( ViewModel . interactionIdKey ) = \ ( readReceiptTableLiteral) . \ ( interactionStateInteractionIdColumnLiteral)
\ ( interaction[ . id ] ) = \ ( readReceiptTableLiteral) . \ ( interactionStateInteractionIdColumnLiteral) AND
\ ( readReceiptTableLiteral) . \ ( readReceiptReadTimestampMsColumnLiteral) IS NOT NULL
)
LEFT JOIN \ ( LinkPreview . self ) ON (
\ ( linkPreview [ . url ] ) = \ ( interaction [ . linkPreviewUrl ] ) AND
\ ( SQL( " \( linkPreview [ . variant ] ) = \( LinkPreview . Variant . openGroupInvitation ) " ) ) AND
\ ( Interaction. linkPreviewFilterLiteral ( timestampColumn : interactionTimestampMsColumnLiteral ) )
\ ( Interaction. linkPreviewFilterLiteral ) AND
\ ( SQL( " \( linkPreview [ . variant ] ) = \( LinkPreview . Variant . openGroupInvitation ) " ) )
)
LEFT JOIN \ ( InteractionAttachment . self ) AS \ ( firstInteractionAttachmentLiteral ) ON (
\ ( firstInteractionAttachmentLiteral ) . \ ( interactionAttachment AlbumIndexColumnLiteral) = 0 AND
\ ( firstInteractionAttachmentLiteral ) . \ ( interactionAttachment InteractionIdColumnLiteral) = \ ( Interaction . self ) . \ ( ViewModel . interactionIdKey )
\ ( firstInteractionAttachmentLiteral ) . \ ( interactionAttachment InteractionIdColumnLiteral) = \ ( interaction [ . id ] ) AND
\ ( firstInteractionAttachmentLiteral ) . \ ( interactionAttachment AlbumIndexColumnLiteral) = 0
)
LEFT JOIN \ ( Attachment . self ) ON \ ( attachment [ . id ] ) = \ ( firstInteractionAttachmentLiteral ) . \ ( interactionAttachmentAttachmentIdColumnLiteral )
LEFT JOIN \ ( InteractionAttachment . self ) ON \ ( interactionAttachment [ . interactionId ] ) = \ ( Interaction. self ) . \ ( ViewModel . interactionIdKey )
LEFT JOIN \ ( InteractionAttachment . self ) ON \ ( interactionAttachment [ . interactionId ] ) = \ ( interaction[ . id ] )
LEFT JOIN \ ( Profile . self ) ON \ ( profile [ . id ] ) = \ ( interaction [ . authorId ] )
-- Thread naming & avatar content
@ -569,16 +590,6 @@ public extension SessionThreadViewModel {
LEFT JOIN \ ( Profile . self ) AS \ ( ViewModel . contactProfileKey ) ON \ ( ViewModel . contactProfileKey ) . \ ( profileIdColumnLiteral ) = \ ( thread [ . id ] )
LEFT JOIN \ ( OpenGroup . self ) ON \ ( openGroup [ . threadId ] ) = \ ( thread [ . id ] )
LEFT JOIN \ ( ClosedGroup . self ) ON \ ( closedGroup [ . threadId ] ) = \ ( thread [ . id ] )
LEFT JOIN \ ( GroupMember . self ) AS \ ( ViewModel . currentUserIsClosedGroupMemberKey ) ON (
\ ( SQL ( " \( ViewModel . currentUserIsClosedGroupMemberKey ) . \( groupMemberRoleColumnLiteral ) != \( GroupMember . Role . zombie ) " ) ) AND
\ ( ViewModel . currentUserIsClosedGroupMemberKey ) . \ ( groupMemberGroupIdColumnLiteral ) = \ ( closedGroup [ . threadId ] ) AND
\ ( SQL ( " \( ViewModel . currentUserIsClosedGroupMemberKey ) . \( groupMemberProfileIdColumnLiteral ) = \( userPublicKey ) " ) )
)
LEFT JOIN \ ( GroupMember . self ) AS \ ( ViewModel . currentUserIsClosedGroupAdminKey ) ON (
\ ( SQL ( " \( ViewModel . currentUserIsClosedGroupAdminKey ) . \( groupMemberRoleColumnLiteral ) = \( GroupMember . Role . admin ) " ) ) AND
\ ( ViewModel . currentUserIsClosedGroupAdminKey ) . \ ( groupMemberGroupIdColumnLiteral ) = \ ( closedGroup [ . threadId ] ) AND
\ ( SQL ( " \( ViewModel . currentUserIsClosedGroupAdminKey ) . \( groupMemberProfileIdColumnLiteral ) = \( userPublicKey ) " ) )
)
LEFT JOIN \ ( Profile . self ) AS \ ( ViewModel . closedGroupProfileFrontKey ) ON (
\ ( ViewModel . closedGroupProfileFrontKey ) . \ ( profileIdColumnLiteral ) = (
@ -586,8 +597,8 @@ public extension SessionThreadViewModel {
FROM \ ( GroupMember . self )
JOIN \ ( Profile . self ) ON \ ( profile [ . id ] ) = \ ( groupMember [ . profileId ] )
WHERE (
\ ( SQL ( " \( groupMember [ . role ] ) = \( GroupMember . Role . standard ) " ) ) AND
\ ( groupMember [ . groupId ] ) = \ ( closedGroup [ . threadId ] ) AND
\ ( SQL ( " \( groupMember [ . role ] ) = \( GroupMember . Role . standard ) " ) ) AND
\ ( SQL ( " \( groupMember [ . profileId ] ) != \( userPublicKey ) " ) )
)
)
@ -599,8 +610,8 @@ public extension SessionThreadViewModel {
FROM \ ( GroupMember . self )
JOIN \ ( Profile . self ) ON \ ( profile [ . id ] ) = \ ( groupMember [ . profileId ] )
WHERE (
\ ( SQL ( " \( groupMember [ . role ] ) = \( GroupMember . Role . standard ) " ) ) AND
\ ( groupMember [ . groupId ] ) = \ ( closedGroup [ . threadId ] ) AND
\ ( SQL ( " \( groupMember [ . role ] ) = \( GroupMember . Role . standard ) " ) ) AND
\ ( SQL ( " \( groupMember [ . profileId ] ) != \( userPublicKey ) " ) )
)
)
@ -643,14 +654,14 @@ public extension SessionThreadViewModel {
let contact : TypedTableAlias < Contact > = TypedTableAlias ( )
let interaction : TypedTableAlias < Interaction > = TypedTableAlias ( )
let in teractionT imestampMsColumnLiteral: SQL = SQL ( stringLiteral : Interaction . Columns . timestampMs . name )
let timestampMsColumnLiteral: SQL = SQL ( stringLiteral : Interaction . Columns . timestampMs . name )
return " " "
LEFT JOIN \ ( Contact . self ) ON \ ( contact [ . id ] ) = \ ( thread [ . id ] )
LEFT JOIN (
SELECT
\ ( interaction [ . threadId ] ) ,
MAX ( \ ( interaction [ . timestampMs ] ) ) AS \ ( in teractionT imestampMsColumnLiteral)
MAX ( \ ( interaction [ . timestampMs ] ) ) AS \ ( timestampMsColumnLiteral)
FROM \ ( Interaction . self )
WHERE \ ( SQL ( " \( interaction [ . variant ] ) != \( Interaction . Variant . standardIncomingDeleted ) " ) )
GROUP BY \ ( interaction [ . threadId ] )
@ -701,7 +712,10 @@ public extension SessionThreadViewModel {
let thread : TypedTableAlias < SessionThread > = TypedTableAlias ( )
let interaction : TypedTableAlias < Interaction > = TypedTableAlias ( )
return SQL ( " \( thread [ . isPinned ] ) DESC, IFNULL( \( interaction [ . timestampMs ] ) , ( \( thread [ . creationDateTimestamp ] ) * 1000)) DESC " )
return SQL ( " " "
\ ( thread [ . isPinned ] ) DESC ,
CASE WHEN \ ( interaction [ . timestampMs ] ) IS NOT NULL THEN \ ( interaction [ . timestampMs ] ) ELSE ( \ ( thread [ . creationDateTimestamp ] ) * 1000 ) END DESC
" " " )
} ( )
static let messageRequetsOrderSQL : SQL = {
@ -725,6 +739,8 @@ public extension SessionThreadViewModel {
let openGroup : TypedTableAlias < OpenGroup > = TypedTableAlias ( )
let interaction : TypedTableAlias < Interaction > = TypedTableAlias ( )
let aggregateInteractionLiteral : SQL = SQL ( stringLiteral : " aggregateInteraction " )
let timestampMsColumnLiteral : SQL = SQL ( stringLiteral : Interaction . Columns . timestampMs . name )
let closedGroupUserCountTableLiteral : SQL = SQL ( stringLiteral : " \( ViewModel . closedGroupUserCountString ) _table " )
let groupMemberGroupIdColumnLiteral : SQL = SQL ( stringLiteral : GroupMember . Columns . groupId . name )
let profileIdColumnLiteral : SQL = SQL ( stringLiteral : Profile . Columns . id . name )
@ -760,12 +776,22 @@ public extension SessionThreadViewModel {
\ ( thread [ . onlyNotifyForMentions ] ) AS \ ( ViewModel . threadOnlyNotifyForMentionsKey ) ,
\ ( thread [ . messageDraft ] ) AS \ ( ViewModel . threadMessageDraftKey ) ,
\ ( Interaction. self ) . \ ( ViewModel . threadUnreadCountKey ) ,
\ ( aggregateInteractionLiteral ) . \ ( ViewModel . threadUnreadCountKey ) ,
\ ( ViewModel . contactProfileKey ) . * ,
\ ( closedGroup [ . name ] ) AS \ ( ViewModel . closedGroupNameKey ) ,
\ ( closedGroupUserCountTableLiteral ) . \ ( ViewModel . closedGroupUserCountKey ) AS \ ( ViewModel . closedGroupUserCountKey ) ,
( \ ( groupMember [ . profileId ] ) IS NOT NULL ) AS \ ( ViewModel . currentUserIsClosedGroupMemberKey ) ,
EXISTS (
SELECT 1
FROM \ ( GroupMember . self )
WHERE (
\ ( groupMember [ . groupId ] ) = \ ( closedGroup [ . threadId ] ) AND
\ ( SQL ( " \( groupMember [ . role ] ) != \( GroupMember . Role . zombie ) " ) ) AND
\ ( SQL ( " \( groupMember [ . profileId ] ) = \( userPublicKey ) " ) )
)
) AS \ ( ViewModel . currentUserIsClosedGroupMemberKey ) ,
\ ( openGroup [ . name ] ) AS \ ( ViewModel . openGroupNameKey ) ,
\ ( openGroup [ . server ] ) AS \ ( ViewModel . openGroupServerKey ) ,
\ ( openGroup [ . roomToken ] ) AS \ ( ViewModel . openGroupRoomTokenKey ) ,
@ -773,33 +799,28 @@ public extension SessionThreadViewModel {
\ ( openGroup [ . userCount ] ) AS \ ( ViewModel . openGroupUserCountKey ) ,
\ ( openGroup [ . permissions ] ) AS \ ( ViewModel . openGroupPermissionsKey ) ,
\ ( Interaction. self ) . \ ( ViewModel . interactionIdKey ) ,
\ ( aggregateInteractionLiteral ) . \ ( ViewModel . interactionIdKey ) ,
\ ( SQL ( " \( userPublicKey ) " ) ) AS \ ( ViewModel . currentUserPublicKeyKey )
FROM \ ( SessionThread . self )
LEFT JOIN \ ( Contact . self ) ON \ ( contact [ . id ] ) = \ ( thread [ . id ] )
LEFT JOIN (
-- Fetch all interaction - specific data in a subquery to be more efficient
SELECT
\ ( interaction [ . id ] ) AS \ ( ViewModel . interactionIdKey ) ,
\ ( interaction [ . threadId ] ) ,
MAX ( \ ( interaction [ . timestampMs ] ) ) ,
\ ( interaction [ . threadId ] ) AS \ ( ViewModel . threadIdKey ) ,
MAX ( \ ( interaction [ . timestampMs ] ) ) AS \ ( timestampMsColumnLiteral ) ,
SUM ( \ ( interaction [ . wasRead ] ) = false ) AS \ ( ViewModel . threadUnreadCountKey )
FROM \ ( Interaction . self )
WHERE \ ( SQL ( " \( interaction [ . threadId ] ) = \( threadId ) " ) )
) AS \ ( Interaction . self ) ON \ ( interaction [ . threadId ] ) = \ ( thread [ . id ] )
WHERE (
\ ( SQL ( " \( interaction [ . threadId ] ) = \( threadId ) " ) ) AND
\ ( SQL ( " \( interaction [ . variant ] ) != \( Interaction . Variant . standardIncomingDeleted ) " ) )
)
) AS \ ( aggregateInteractionLiteral ) ON \ ( aggregateInteractionLiteral ) . \ ( ViewModel . threadIdKey ) = \ ( thread [ . id ] )
LEFT JOIN \ ( Profile . self ) AS \ ( ViewModel . contactProfileKey ) ON \ ( ViewModel . contactProfileKey ) . \ ( profileIdColumnLiteral ) = \ ( thread [ . id ] )
LEFT JOIN \ ( OpenGroup . self ) ON \ ( openGroup [ . threadId ] ) = \ ( thread [ . id ] )
LEFT JOIN \ ( ClosedGroup . self ) ON \ ( closedGroup [ . threadId ] ) = \ ( thread [ . id ] )
LEFT JOIN \ ( GroupMember . self ) ON (
\ ( SQL ( " \( groupMember [ . role ] ) = \( GroupMember . Role . standard ) " ) ) AND
\ ( groupMember [ . groupId ] ) = \ ( closedGroup [ . threadId ] ) AND
\ ( SQL ( " \( groupMember [ . profileId ] ) = \( userPublicKey ) " ) )
)
LEFT JOIN (
SELECT
\ ( groupMember [ . groupId ] ) ,
@ -1583,7 +1604,7 @@ public extension SessionThreadViewModel {
FROM \ ( SessionThread . self )
LEFT JOIN \ ( Contact . self ) ON \ ( contact [ . id ] ) = \ ( thread [ . id ] )
LEFT JOIN (
SELECT * , MAX ( \ ( interaction [ . timestampMs ] ) )
SELECT \ ( interaction [ . threadId ] ) , MAX ( \ ( interaction [ . timestampMs ] ) )
FROM \ ( Interaction . self )
GROUP BY \ ( interaction [ . threadId ] )
) AS \ ( Interaction . self ) ON \ ( interaction [ . threadId ] ) = \ ( thread [ . id ] )