Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
1e82714a
Unverified
Commit
1e82714a
authored
Mar 07, 2026
by
memoclaw
Committed by
GitHub
Mar 07, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
perf: batch load memo relations when listing memos (#5692)
Co-authored-by:
Claude Opus 4.6
<
noreply@anthropic.com
>
parent
3d4f793f
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
456 additions
and
14 deletions
+456
-14
memo_service.go
server/router/api/v1/memo_service.go
+33
-6
memo_service_converter.go
server/router/api/v1/memo_service_converter.go
+115
-7
test_helper.go
server/router/api/v1/test/test_helper.go
+3
-1
memo_relation.go
store/db/mysql/memo_relation.go
+12
-0
memo_relation.go
store/db/postgres/memo_relation.go
+14
-0
memo_relation.go
store/db/sqlite/memo_relation.go
+13
-0
memo_relation.go
store/memo_relation.go
+2
-0
memo_relation_test.go
store/test/memo_relation_test.go
+264
-0
No files found.
server/router/api/v1/memo_service.go
View file @
1e82714a
...
@@ -126,7 +126,11 @@ func (s *APIV1Service) CreateMemo(ctx context.Context, request *v1pb.CreateMemoR
...
@@ -126,7 +126,11 @@ func (s *APIV1Service) CreateMemo(ctx context.Context, request *v1pb.CreateMemoR
}
}
}
}
memoMessage
,
err
:=
s
.
convertMemoFromStore
(
ctx
,
memo
,
nil
,
attachments
)
relations
,
err
:=
s
.
loadMemoRelations
(
ctx
,
memo
)
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to load memo relations"
)
}
memoMessage
,
err
:=
s
.
convertMemoFromStore
(
ctx
,
memo
,
nil
,
attachments
,
relations
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to convert memo"
)
return
nil
,
errors
.
Wrap
(
err
,
"failed to convert memo"
)
}
}
...
@@ -266,12 +270,19 @@ func (s *APIV1Service) ListMemos(ctx context.Context, request *v1pb.ListMemosReq
...
@@ -266,12 +270,19 @@ func (s *APIV1Service) ListMemos(ctx context.Context, request *v1pb.ListMemosReq
attachmentMap
[
*
attachment
.
MemoID
]
=
append
(
attachmentMap
[
*
attachment
.
MemoID
],
attachment
)
attachmentMap
[
*
attachment
.
MemoID
]
=
append
(
attachmentMap
[
*
attachment
.
MemoID
],
attachment
)
}
}
// RELATIONS (batch load to avoid N+1)
relationMap
,
err
:=
s
.
batchConvertMemoRelations
(
ctx
,
memos
)
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to batch load memo relations"
)
}
for
_
,
memo
:=
range
memos
{
for
_
,
memo
:=
range
memos
{
memoName
:=
fmt
.
Sprintf
(
"%s%s"
,
MemoNamePrefix
,
memo
.
UID
)
memoName
:=
fmt
.
Sprintf
(
"%s%s"
,
MemoNamePrefix
,
memo
.
UID
)
reactions
:=
reactionMap
[
memoName
]
reactions
:=
reactionMap
[
memoName
]
attachments
:=
attachmentMap
[
memo
.
ID
]
attachments
:=
attachmentMap
[
memo
.
ID
]
relations
:=
relationMap
[
memo
.
ID
]
memoMessage
,
err
:=
s
.
convertMemoFromStore
(
ctx
,
memo
,
reactions
,
attachments
)
memoMessage
,
err
:=
s
.
convertMemoFromStore
(
ctx
,
memo
,
reactions
,
attachments
,
relations
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to convert memo"
)
return
nil
,
errors
.
Wrap
(
err
,
"failed to convert memo"
)
}
}
...
@@ -327,7 +338,11 @@ func (s *APIV1Service) GetMemo(ctx context.Context, request *v1pb.GetMemoRequest
...
@@ -327,7 +338,11 @@ func (s *APIV1Service) GetMemo(ctx context.Context, request *v1pb.GetMemoRequest
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to list attachments"
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to list attachments"
)
}
}
memoMessage
,
err
:=
s
.
convertMemoFromStore
(
ctx
,
memo
,
reactions
,
attachments
)
relations
,
err
:=
s
.
loadMemoRelations
(
ctx
,
memo
)
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to load memo relations"
)
}
memoMessage
,
err
:=
s
.
convertMemoFromStore
(
ctx
,
memo
,
reactions
,
attachments
,
relations
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to convert memo"
)
return
nil
,
errors
.
Wrap
(
err
,
"failed to convert memo"
)
}
}
...
@@ -462,7 +477,11 @@ func (s *APIV1Service) UpdateMemo(ctx context.Context, request *v1pb.UpdateMemoR
...
@@ -462,7 +477,11 @@ func (s *APIV1Service) UpdateMemo(ctx context.Context, request *v1pb.UpdateMemoR
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to list attachments"
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to list attachments"
)
}
}
memoMessage
,
err
:=
s
.
convertMemoFromStore
(
ctx
,
memo
,
reactions
,
attachments
)
relations
,
err
:=
s
.
loadMemoRelations
(
ctx
,
memo
)
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to load memo relations"
)
}
memoMessage
,
err
:=
s
.
convertMemoFromStore
(
ctx
,
memo
,
reactions
,
attachments
,
relations
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to convert memo"
)
return
nil
,
errors
.
Wrap
(
err
,
"failed to convert memo"
)
}
}
...
@@ -521,7 +540,8 @@ func (s *APIV1Service) DeleteMemo(ctx context.Context, request *v1pb.DeleteMemoR
...
@@ -521,7 +540,8 @@ func (s *APIV1Service) DeleteMemo(ctx context.Context, request *v1pb.DeleteMemoR
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to list attachments"
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to list attachments"
)
}
}
if
memoMessage
,
err
:=
s
.
convertMemoFromStore
(
ctx
,
memo
,
reactions
,
attachments
);
err
==
nil
{
deleteRelations
,
_
:=
s
.
loadMemoRelations
(
ctx
,
memo
)
if
memoMessage
,
err
:=
s
.
convertMemoFromStore
(
ctx
,
memo
,
reactions
,
attachments
,
deleteRelations
);
err
==
nil
{
// Try to dispatch webhook when memo is deleted.
// Try to dispatch webhook when memo is deleted.
if
err
:=
s
.
DispatchMemoDeletedWebhook
(
ctx
,
memoMessage
);
err
!=
nil
{
if
err
:=
s
.
DispatchMemoDeletedWebhook
(
ctx
,
memoMessage
);
err
!=
nil
{
slog
.
Warn
(
"Failed to dispatch memo deleted webhook"
,
slog
.
Any
(
"err"
,
err
))
slog
.
Warn
(
"Failed to dispatch memo deleted webhook"
,
slog
.
Any
(
"err"
,
err
))
...
@@ -725,13 +745,20 @@ func (s *APIV1Service) ListMemoComments(ctx context.Context, request *v1pb.ListM
...
@@ -725,13 +745,20 @@ func (s *APIV1Service) ListMemoComments(ctx context.Context, request *v1pb.ListM
attachmentMap
[
*
attachment
.
MemoID
]
=
append
(
attachmentMap
[
*
attachment
.
MemoID
],
attachment
)
attachmentMap
[
*
attachment
.
MemoID
]
=
append
(
attachmentMap
[
*
attachment
.
MemoID
],
attachment
)
}
}
// RELATIONS (batch load to avoid N+1)
relationMap
,
err
:=
s
.
batchConvertMemoRelations
(
ctx
,
memos
)
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to batch load memo relations"
)
}
var
memosResponse
[]
*
v1pb
.
Memo
var
memosResponse
[]
*
v1pb
.
Memo
for
_
,
m
:=
range
memos
{
for
_
,
m
:=
range
memos
{
memoName
:=
memoIDToNameMap
[
m
.
ID
]
memoName
:=
memoIDToNameMap
[
m
.
ID
]
reactions
:=
memoReactionsMap
[
memoName
]
reactions
:=
memoReactionsMap
[
memoName
]
attachments
:=
attachmentMap
[
m
.
ID
]
attachments
:=
attachmentMap
[
m
.
ID
]
relations
:=
relationMap
[
m
.
ID
]
memoMessage
,
err
:=
s
.
convertMemoFromStore
(
ctx
,
m
,
reactions
,
attachments
)
memoMessage
,
err
:=
s
.
convertMemoFromStore
(
ctx
,
m
,
reactions
,
attachments
,
relations
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to convert memo"
)
return
nil
,
errors
.
Wrap
(
err
,
"failed to convert memo"
)
}
}
...
...
server/router/api/v1/memo_service_converter.go
View file @
1e82714a
...
@@ -13,7 +13,7 @@ import (
...
@@ -13,7 +13,7 @@ import (
"github.com/usememos/memos/store"
"github.com/usememos/memos/store"
)
)
func
(
s
*
APIV1Service
)
convertMemoFromStore
(
ctx
context
.
Context
,
memo
*
store
.
Memo
,
reactions
[]
*
store
.
Reaction
,
attachments
[]
*
store
.
Attachment
)
(
*
v1pb
.
Memo
,
error
)
{
func
(
s
*
APIV1Service
)
convertMemoFromStore
(
ctx
context
.
Context
,
memo
*
store
.
Memo
,
reactions
[]
*
store
.
Reaction
,
attachments
[]
*
store
.
Attachment
,
relations
[]
*
v1pb
.
MemoRelation
)
(
*
v1pb
.
Memo
,
error
)
{
displayTs
:=
memo
.
CreatedTs
displayTs
:=
memo
.
CreatedTs
instanceMemoRelatedSetting
,
err
:=
s
.
Store
.
GetInstanceMemoRelatedSetting
(
ctx
)
instanceMemoRelatedSetting
,
err
:=
s
.
Store
.
GetInstanceMemoRelatedSetting
(
ctx
)
if
err
!=
nil
{
if
err
!=
nil
{
...
@@ -47,20 +47,18 @@ func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Mem
...
@@ -47,20 +47,18 @@ func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Mem
}
}
memoMessage
.
Reactions
=
[]
*
v1pb
.
Reaction
{}
memoMessage
.
Reactions
=
[]
*
v1pb
.
Reaction
{}
for
_
,
reaction
:=
range
reactions
{
for
_
,
reaction
:=
range
reactions
{
reactionResponse
:=
convertReactionFromStore
(
reaction
)
reactionResponse
:=
convertReactionFromStore
(
reaction
)
memoMessage
.
Reactions
=
append
(
memoMessage
.
Reactions
,
reactionResponse
)
memoMessage
.
Reactions
=
append
(
memoMessage
.
Reactions
,
reactionResponse
)
}
}
listMemoRelationsResponse
,
err
:=
s
.
ListMemoRelations
(
ctx
,
&
v1pb
.
ListMemoRelationsRequest
{
Name
:
name
})
if
relations
!=
nil
{
if
err
!=
nil
{
memoMessage
.
Relations
=
relations
return
nil
,
errors
.
Wrap
(
err
,
"failed to list memo relations"
)
}
else
{
memoMessage
.
Relations
=
[]
*
v1pb
.
MemoRelation
{}
}
}
memoMessage
.
Relations
=
listMemoRelationsResponse
.
Relations
memoMessage
.
Attachments
=
[]
*
v1pb
.
Attachment
{}
memoMessage
.
Attachments
=
[]
*
v1pb
.
Attachment
{}
for
_
,
attachment
:=
range
attachments
{
for
_
,
attachment
:=
range
attachments
{
attachmentResponse
:=
convertAttachmentFromStore
(
attachment
)
attachmentResponse
:=
convertAttachmentFromStore
(
attachment
)
memoMessage
.
Attachments
=
append
(
memoMessage
.
Attachments
,
attachmentResponse
)
memoMessage
.
Attachments
=
append
(
memoMessage
.
Attachments
,
attachmentResponse
)
...
@@ -75,6 +73,116 @@ func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Mem
...
@@ -75,6 +73,116 @@ func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Mem
return
memoMessage
,
nil
return
memoMessage
,
nil
}
}
// batchConvertMemoRelations batch-loads relations for a list of memos and returns
// a map from memo ID to its converted relations. This avoids N+1 queries when listing memos.
func
(
s
*
APIV1Service
)
batchConvertMemoRelations
(
ctx
context
.
Context
,
memos
[]
*
store
.
Memo
)
(
map
[
int32
][]
*
v1pb
.
MemoRelation
,
error
)
{
if
len
(
memos
)
==
0
{
return
map
[
int32
][]
*
v1pb
.
MemoRelation
{},
nil
}
currentUser
,
err
:=
s
.
fetchCurrentUser
(
ctx
)
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to get user"
)
}
var
memoFilter
string
if
currentUser
==
nil
{
memoFilter
=
`visibility == "PUBLIC"`
}
else
{
memoFilter
=
fmt
.
Sprintf
(
`creator_id == %d || visibility in ["PUBLIC", "PROTECTED"]`
,
currentUser
.
ID
)
}
memoIDs
:=
make
([]
int32
,
len
(
memos
))
memoIDSet
:=
make
(
map
[
int32
]
bool
,
len
(
memos
))
for
i
,
m
:=
range
memos
{
memoIDs
[
i
]
=
m
.
ID
memoIDSet
[
m
.
ID
]
=
true
}
// Single batch query to get all relations involving any of these memos.
allRelations
,
err
:=
s
.
Store
.
ListMemoRelations
(
ctx
,
&
store
.
FindMemoRelation
{
MemoIDList
:
memoIDs
,
MemoFilter
:
&
memoFilter
,
})
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to batch list memo relations"
)
}
// Collect all memo IDs referenced in relations that we need to resolve.
neededIDs
:=
make
(
map
[
int32
]
bool
)
for
_
,
r
:=
range
allRelations
{
neededIDs
[
r
.
MemoID
]
=
true
neededIDs
[
r
.
RelatedMemoID
]
=
true
}
// Build ID→UID map from the memos we already have.
memoIDToUID
:=
make
(
map
[
int32
]
string
,
len
(
memos
))
memoIDToContent
:=
make
(
map
[
int32
]
string
,
len
(
memos
))
for
_
,
m
:=
range
memos
{
memoIDToUID
[
m
.
ID
]
=
m
.
UID
memoIDToContent
[
m
.
ID
]
=
m
.
Content
delete
(
neededIDs
,
m
.
ID
)
}
// Batch fetch any additional memos referenced by relations that we don't already have.
if
len
(
neededIDs
)
>
0
{
extraIDs
:=
make
([]
int32
,
0
,
len
(
neededIDs
))
for
id
:=
range
neededIDs
{
extraIDs
=
append
(
extraIDs
,
id
)
}
extraMemos
,
err
:=
s
.
Store
.
ListMemos
(
ctx
,
&
store
.
FindMemo
{
IDList
:
extraIDs
})
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to batch fetch related memos"
)
}
for
_
,
m
:=
range
extraMemos
{
memoIDToUID
[
m
.
ID
]
=
m
.
UID
memoIDToContent
[
m
.
ID
]
=
m
.
Content
}
}
// Build the result map: memo ID → its relations (both directions).
result
:=
make
(
map
[
int32
][]
*
v1pb
.
MemoRelation
,
len
(
memos
))
for
_
,
r
:=
range
allRelations
{
memoUID
,
ok1
:=
memoIDToUID
[
r
.
MemoID
]
relatedUID
,
ok2
:=
memoIDToUID
[
r
.
RelatedMemoID
]
if
!
ok1
||
!
ok2
{
continue
}
memoSnippet
,
_
:=
s
.
getMemoContentSnippet
(
memoIDToContent
[
r
.
MemoID
])
relatedSnippet
,
_
:=
s
.
getMemoContentSnippet
(
memoIDToContent
[
r
.
RelatedMemoID
])
relation
:=
&
v1pb
.
MemoRelation
{
Memo
:
&
v1pb
.
MemoRelation_Memo
{
Name
:
fmt
.
Sprintf
(
"%s%s"
,
MemoNamePrefix
,
memoUID
),
Snippet
:
memoSnippet
,
},
RelatedMemo
:
&
v1pb
.
MemoRelation_Memo
{
Name
:
fmt
.
Sprintf
(
"%s%s"
,
MemoNamePrefix
,
relatedUID
),
Snippet
:
relatedSnippet
,
},
Type
:
convertMemoRelationTypeFromStore
(
r
.
Type
),
}
// Add to the memo that owns this relation (both directions).
if
memoIDSet
[
r
.
MemoID
]
{
result
[
r
.
MemoID
]
=
append
(
result
[
r
.
MemoID
],
relation
)
}
if
memoIDSet
[
r
.
RelatedMemoID
]
{
result
[
r
.
RelatedMemoID
]
=
append
(
result
[
r
.
RelatedMemoID
],
relation
)
}
}
return
result
,
nil
}
// loadMemoRelations loads relations for a single memo and converts them to API format.
func
(
s
*
APIV1Service
)
loadMemoRelations
(
ctx
context
.
Context
,
memo
*
store
.
Memo
)
([]
*
v1pb
.
MemoRelation
,
error
)
{
relationMap
,
err
:=
s
.
batchConvertMemoRelations
(
ctx
,
[]
*
store
.
Memo
{
memo
})
if
err
!=
nil
{
return
nil
,
err
}
return
relationMap
[
memo
.
ID
],
nil
}
func
convertMemoPropertyFromStore
(
property
*
storepb
.
MemoPayload_Property
)
*
v1pb
.
Memo_Property
{
func
convertMemoPropertyFromStore
(
property
*
storepb
.
MemoPayload_Property
)
*
v1pb
.
Memo_Property
{
if
property
==
nil
{
if
property
==
nil
{
return
nil
return
nil
...
...
server/router/api/v1/test/test_helper.go
View file @
1e82714a
...
@@ -27,13 +27,15 @@ func NewTestService(t *testing.T) *TestService {
...
@@ -27,13 +27,15 @@ func NewTestService(t *testing.T) *TestService {
// Create a test store with SQLite
// Create a test store with SQLite
testStore
:=
teststore
.
NewTestingStore
(
ctx
,
t
)
testStore
:=
teststore
.
NewTestingStore
(
ctx
,
t
)
// Create a test profile
// Create a test profile with a temp directory for file storage,
// so tests that create attachments don't leave artifacts in the source tree.
testProfile
:=
&
profile
.
Profile
{
testProfile
:=
&
profile
.
Profile
{
Demo
:
true
,
Demo
:
true
,
Version
:
"test-1.0.0"
,
Version
:
"test-1.0.0"
,
InstanceURL
:
"http://localhost:8080"
,
InstanceURL
:
"http://localhost:8080"
,
Driver
:
"sqlite"
,
Driver
:
"sqlite"
,
DSN
:
":memory:"
,
DSN
:
":memory:"
,
Data
:
t
.
TempDir
(),
}
}
// Create APIV1Service with nil grpcServer since we're testing direct calls
// Create APIV1Service with nil grpcServer since we're testing direct calls
...
...
store/db/mysql/memo_relation.go
View file @
1e82714a
...
@@ -42,6 +42,18 @@ func (d *DB) ListMemoRelations(ctx context.Context, find *store.FindMemoRelation
...
@@ -42,6 +42,18 @@ func (d *DB) ListMemoRelations(ctx context.Context, find *store.FindMemoRelation
if
find
.
Type
!=
nil
{
if
find
.
Type
!=
nil
{
where
,
args
=
append
(
where
,
"`type` = ?"
),
append
(
args
,
find
.
Type
)
where
,
args
=
append
(
where
,
"`type` = ?"
),
append
(
args
,
find
.
Type
)
}
}
if
len
(
find
.
MemoIDList
)
>
0
{
placeholders
:=
make
([]
string
,
len
(
find
.
MemoIDList
))
for
i
,
id
:=
range
find
.
MemoIDList
{
placeholders
[
i
]
=
"?"
args
=
append
(
args
,
id
)
}
inClause
:=
strings
.
Join
(
placeholders
,
", "
)
for
_
,
id
:=
range
find
.
MemoIDList
{
args
=
append
(
args
,
id
)
}
where
=
append
(
where
,
fmt
.
Sprintf
(
"(`memo_id` IN (%s) OR `related_memo_id` IN (%s))"
,
inClause
,
inClause
))
}
if
find
.
MemoFilter
!=
nil
{
if
find
.
MemoFilter
!=
nil
{
engine
,
err
:=
filter
.
DefaultEngine
()
engine
,
err
:=
filter
.
DefaultEngine
()
if
err
!=
nil
{
if
err
!=
nil
{
...
...
store/db/postgres/memo_relation.go
View file @
1e82714a
...
@@ -49,6 +49,20 @@ func (d *DB) ListMemoRelations(ctx context.Context, find *store.FindMemoRelation
...
@@ -49,6 +49,20 @@ func (d *DB) ListMemoRelations(ctx context.Context, find *store.FindMemoRelation
if
find
.
Type
!=
nil
{
if
find
.
Type
!=
nil
{
where
,
args
=
append
(
where
,
"type = "
+
placeholder
(
len
(
args
)
+
1
)),
append
(
args
,
find
.
Type
)
where
,
args
=
append
(
where
,
"type = "
+
placeholder
(
len
(
args
)
+
1
)),
append
(
args
,
find
.
Type
)
}
}
if
len
(
find
.
MemoIDList
)
>
0
{
memoPlaceholders
:=
make
([]
string
,
len
(
find
.
MemoIDList
))
for
i
,
id
:=
range
find
.
MemoIDList
{
memoPlaceholders
[
i
]
=
placeholder
(
len
(
args
)
+
1
)
args
=
append
(
args
,
id
)
}
relatedPlaceholders
:=
make
([]
string
,
len
(
find
.
MemoIDList
))
for
i
,
id
:=
range
find
.
MemoIDList
{
relatedPlaceholders
[
i
]
=
placeholder
(
len
(
args
)
+
1
)
args
=
append
(
args
,
id
)
}
where
=
append
(
where
,
fmt
.
Sprintf
(
"(memo_id IN (%s) OR related_memo_id IN (%s))"
,
strings
.
Join
(
memoPlaceholders
,
", "
),
strings
.
Join
(
relatedPlaceholders
,
", "
)))
}
if
find
.
MemoFilter
!=
nil
{
if
find
.
MemoFilter
!=
nil
{
engine
,
err
:=
filter
.
DefaultEngine
()
engine
,
err
:=
filter
.
DefaultEngine
()
if
err
!=
nil
{
if
err
!=
nil
{
...
...
store/db/sqlite/memo_relation.go
View file @
1e82714a
...
@@ -49,6 +49,19 @@ func (d *DB) ListMemoRelations(ctx context.Context, find *store.FindMemoRelation
...
@@ -49,6 +49,19 @@ func (d *DB) ListMemoRelations(ctx context.Context, find *store.FindMemoRelation
if
find
.
Type
!=
nil
{
if
find
.
Type
!=
nil
{
where
,
args
=
append
(
where
,
"type = ?"
),
append
(
args
,
find
.
Type
)
where
,
args
=
append
(
where
,
"type = ?"
),
append
(
args
,
find
.
Type
)
}
}
if
len
(
find
.
MemoIDList
)
>
0
{
placeholders
:=
make
([]
string
,
len
(
find
.
MemoIDList
))
for
i
,
id
:=
range
find
.
MemoIDList
{
placeholders
[
i
]
=
"?"
args
=
append
(
args
,
id
)
}
inClause
:=
strings
.
Join
(
placeholders
,
", "
)
// Duplicate args for the second IN clause.
for
_
,
id
:=
range
find
.
MemoIDList
{
args
=
append
(
args
,
id
)
}
where
=
append
(
where
,
fmt
.
Sprintf
(
"(memo_id IN (%s) OR related_memo_id IN (%s))"
,
inClause
,
inClause
))
}
if
find
.
MemoFilter
!=
nil
{
if
find
.
MemoFilter
!=
nil
{
engine
,
err
:=
filter
.
DefaultEngine
()
engine
,
err
:=
filter
.
DefaultEngine
()
if
err
!=
nil
{
if
err
!=
nil
{
...
...
store/memo_relation.go
View file @
1e82714a
...
@@ -24,6 +24,8 @@ type FindMemoRelation struct {
...
@@ -24,6 +24,8 @@ type FindMemoRelation struct {
RelatedMemoID
*
int32
RelatedMemoID
*
int32
Type
*
MemoRelationType
Type
*
MemoRelationType
MemoFilter
*
string
MemoFilter
*
string
// MemoIDList matches relations where memo_id OR related_memo_id is in the list.
MemoIDList
[]
int32
}
}
type
DeleteMemoRelation
struct
{
type
DeleteMemoRelation
struct
{
...
...
store/test/memo_relation_test.go
View file @
1e82714a
...
@@ -638,6 +638,270 @@ func TestMemoRelationBidirectional(t *testing.T) {
...
@@ -638,6 +638,270 @@ func TestMemoRelationBidirectional(t *testing.T) {
ts
.
Close
()
ts
.
Close
()
}
}
func
TestMemoRelationListByMemoIDList
(
t
*
testing
.
T
)
{
t
.
Parallel
()
ctx
:=
context
.
Background
()
ts
:=
NewTestingStore
(
ctx
,
t
)
user
,
err
:=
createTestingHostUser
(
ctx
,
ts
)
require
.
NoError
(
t
,
err
)
// Create 3 memos.
memoA
,
err
:=
ts
.
CreateMemo
(
ctx
,
&
store
.
Memo
{
UID
:
"memo-a"
,
CreatorID
:
user
.
ID
,
Content
:
"memo A content"
,
Visibility
:
store
.
Public
,
})
require
.
NoError
(
t
,
err
)
memoB
,
err
:=
ts
.
CreateMemo
(
ctx
,
&
store
.
Memo
{
UID
:
"memo-b"
,
CreatorID
:
user
.
ID
,
Content
:
"memo B content"
,
Visibility
:
store
.
Public
,
})
require
.
NoError
(
t
,
err
)
memoC
,
err
:=
ts
.
CreateMemo
(
ctx
,
&
store
.
Memo
{
UID
:
"memo-c"
,
CreatorID
:
user
.
ID
,
Content
:
"memo C content"
,
Visibility
:
store
.
Public
,
})
require
.
NoError
(
t
,
err
)
memoD
,
err
:=
ts
.
CreateMemo
(
ctx
,
&
store
.
Memo
{
UID
:
"memo-d"
,
CreatorID
:
user
.
ID
,
Content
:
"memo D content"
,
Visibility
:
store
.
Public
,
})
require
.
NoError
(
t
,
err
)
// A -> B (reference)
_
,
err
=
ts
.
UpsertMemoRelation
(
ctx
,
&
store
.
MemoRelation
{
MemoID
:
memoA
.
ID
,
RelatedMemoID
:
memoB
.
ID
,
Type
:
store
.
MemoRelationReference
,
})
require
.
NoError
(
t
,
err
)
// A -> C (comment)
_
,
err
=
ts
.
UpsertMemoRelation
(
ctx
,
&
store
.
MemoRelation
{
MemoID
:
memoA
.
ID
,
RelatedMemoID
:
memoC
.
ID
,
Type
:
store
.
MemoRelationComment
,
})
require
.
NoError
(
t
,
err
)
// D -> B (reference) — B appears as related_memo_id
_
,
err
=
ts
.
UpsertMemoRelation
(
ctx
,
&
store
.
MemoRelation
{
MemoID
:
memoD
.
ID
,
RelatedMemoID
:
memoB
.
ID
,
Type
:
store
.
MemoRelationReference
,
})
require
.
NoError
(
t
,
err
)
// Batch query for memos A and B: should return all 3 relations
// (A->B because A is in list, A->C because A is in list, D->B because B is in list)
relations
,
err
:=
ts
.
ListMemoRelations
(
ctx
,
&
store
.
FindMemoRelation
{
MemoIDList
:
[]
int32
{
memoA
.
ID
,
memoB
.
ID
},
})
require
.
NoError
(
t
,
err
)
require
.
Len
(
t
,
relations
,
3
)
// Batch query for memo C only: should return 1 relation (A->C because C is related_memo_id)
relations
,
err
=
ts
.
ListMemoRelations
(
ctx
,
&
store
.
FindMemoRelation
{
MemoIDList
:
[]
int32
{
memoC
.
ID
},
})
require
.
NoError
(
t
,
err
)
require
.
Len
(
t
,
relations
,
1
)
require
.
Equal
(
t
,
memoA
.
ID
,
relations
[
0
]
.
MemoID
)
require
.
Equal
(
t
,
memoC
.
ID
,
relations
[
0
]
.
RelatedMemoID
)
// Batch query for memo D only: should return 1 relation (D->B because D is memo_id)
relations
,
err
=
ts
.
ListMemoRelations
(
ctx
,
&
store
.
FindMemoRelation
{
MemoIDList
:
[]
int32
{
memoD
.
ID
},
})
require
.
NoError
(
t
,
err
)
require
.
Len
(
t
,
relations
,
1
)
require
.
Equal
(
t
,
memoD
.
ID
,
relations
[
0
]
.
MemoID
)
require
.
Equal
(
t
,
memoB
.
ID
,
relations
[
0
]
.
RelatedMemoID
)
ts
.
Close
()
}
func
TestMemoRelationListByMemoIDListEmpty
(
t
*
testing
.
T
)
{
t
.
Parallel
()
ctx
:=
context
.
Background
()
ts
:=
NewTestingStore
(
ctx
,
t
)
user
,
err
:=
createTestingHostUser
(
ctx
,
ts
)
require
.
NoError
(
t
,
err
)
memo
,
err
:=
ts
.
CreateMemo
(
ctx
,
&
store
.
Memo
{
UID
:
"memo-no-relations"
,
CreatorID
:
user
.
ID
,
Content
:
"memo with no relations"
,
Visibility
:
store
.
Public
,
})
require
.
NoError
(
t
,
err
)
// Batch query with a memo that has no relations.
relations
,
err
:=
ts
.
ListMemoRelations
(
ctx
,
&
store
.
FindMemoRelation
{
MemoIDList
:
[]
int32
{
memo
.
ID
},
})
require
.
NoError
(
t
,
err
)
require
.
Len
(
t
,
relations
,
0
)
// Empty MemoIDList should not filter by MemoIDList (returns based on other filters).
relations
,
err
=
ts
.
ListMemoRelations
(
ctx
,
&
store
.
FindMemoRelation
{
MemoIDList
:
[]
int32
{},
})
require
.
NoError
(
t
,
err
)
require
.
Len
(
t
,
relations
,
0
)
ts
.
Close
()
}
func
TestMemoRelationListByMemoIDListWithTypeFilter
(
t
*
testing
.
T
)
{
t
.
Parallel
()
ctx
:=
context
.
Background
()
ts
:=
NewTestingStore
(
ctx
,
t
)
user
,
err
:=
createTestingHostUser
(
ctx
,
ts
)
require
.
NoError
(
t
,
err
)
memoA
,
err
:=
ts
.
CreateMemo
(
ctx
,
&
store
.
Memo
{
UID
:
"memo-a"
,
CreatorID
:
user
.
ID
,
Content
:
"memo A content"
,
Visibility
:
store
.
Public
,
})
require
.
NoError
(
t
,
err
)
memoB
,
err
:=
ts
.
CreateMemo
(
ctx
,
&
store
.
Memo
{
UID
:
"memo-b"
,
CreatorID
:
user
.
ID
,
Content
:
"memo B content"
,
Visibility
:
store
.
Public
,
})
require
.
NoError
(
t
,
err
)
memoC
,
err
:=
ts
.
CreateMemo
(
ctx
,
&
store
.
Memo
{
UID
:
"memo-c"
,
CreatorID
:
user
.
ID
,
Content
:
"memo C content"
,
Visibility
:
store
.
Public
,
})
require
.
NoError
(
t
,
err
)
// A -> B (reference)
_
,
err
=
ts
.
UpsertMemoRelation
(
ctx
,
&
store
.
MemoRelation
{
MemoID
:
memoA
.
ID
,
RelatedMemoID
:
memoB
.
ID
,
Type
:
store
.
MemoRelationReference
,
})
require
.
NoError
(
t
,
err
)
// A -> C (comment)
_
,
err
=
ts
.
UpsertMemoRelation
(
ctx
,
&
store
.
MemoRelation
{
MemoID
:
memoA
.
ID
,
RelatedMemoID
:
memoC
.
ID
,
Type
:
store
.
MemoRelationComment
,
})
require
.
NoError
(
t
,
err
)
// Batch query with type filter: only references
refType
:=
store
.
MemoRelationReference
relations
,
err
:=
ts
.
ListMemoRelations
(
ctx
,
&
store
.
FindMemoRelation
{
MemoIDList
:
[]
int32
{
memoA
.
ID
},
Type
:
&
refType
,
})
require
.
NoError
(
t
,
err
)
require
.
Len
(
t
,
relations
,
1
)
require
.
Equal
(
t
,
store
.
MemoRelationReference
,
relations
[
0
]
.
Type
)
// Batch query with type filter: only comments
commentType
:=
store
.
MemoRelationComment
relations
,
err
=
ts
.
ListMemoRelations
(
ctx
,
&
store
.
FindMemoRelation
{
MemoIDList
:
[]
int32
{
memoA
.
ID
},
Type
:
&
commentType
,
})
require
.
NoError
(
t
,
err
)
require
.
Len
(
t
,
relations
,
1
)
require
.
Equal
(
t
,
store
.
MemoRelationComment
,
relations
[
0
]
.
Type
)
ts
.
Close
()
}
func
TestMemoRelationListByMemoIDListBothDirections
(
t
*
testing
.
T
)
{
t
.
Parallel
()
ctx
:=
context
.
Background
()
ts
:=
NewTestingStore
(
ctx
,
t
)
user
,
err
:=
createTestingHostUser
(
ctx
,
ts
)
require
.
NoError
(
t
,
err
)
memoA
,
err
:=
ts
.
CreateMemo
(
ctx
,
&
store
.
Memo
{
UID
:
"memo-a"
,
CreatorID
:
user
.
ID
,
Content
:
"memo A content"
,
Visibility
:
store
.
Public
,
})
require
.
NoError
(
t
,
err
)
memoB
,
err
:=
ts
.
CreateMemo
(
ctx
,
&
store
.
Memo
{
UID
:
"memo-b"
,
CreatorID
:
user
.
ID
,
Content
:
"memo B content"
,
Visibility
:
store
.
Public
,
})
require
.
NoError
(
t
,
err
)
memoX
,
err
:=
ts
.
CreateMemo
(
ctx
,
&
store
.
Memo
{
UID
:
"memo-x"
,
CreatorID
:
user
.
ID
,
Content
:
"memo X content"
,
Visibility
:
store
.
Public
,
})
require
.
NoError
(
t
,
err
)
// X -> A (A appears as related_memo_id)
_
,
err
=
ts
.
UpsertMemoRelation
(
ctx
,
&
store
.
MemoRelation
{
MemoID
:
memoX
.
ID
,
RelatedMemoID
:
memoA
.
ID
,
Type
:
store
.
MemoRelationReference
,
})
require
.
NoError
(
t
,
err
)
// A -> B (A appears as memo_id)
_
,
err
=
ts
.
UpsertMemoRelation
(
ctx
,
&
store
.
MemoRelation
{
MemoID
:
memoA
.
ID
,
RelatedMemoID
:
memoB
.
ID
,
Type
:
store
.
MemoRelationReference
,
})
require
.
NoError
(
t
,
err
)
// Query with MemoIDList=[A]: should find both relations (A as source and A as target).
relations
,
err
:=
ts
.
ListMemoRelations
(
ctx
,
&
store
.
FindMemoRelation
{
MemoIDList
:
[]
int32
{
memoA
.
ID
},
})
require
.
NoError
(
t
,
err
)
require
.
Len
(
t
,
relations
,
2
)
// Verify we got both directions.
memoIDs
:=
map
[
int32
]
bool
{}
relatedIDs
:=
map
[
int32
]
bool
{}
for
_
,
r
:=
range
relations
{
memoIDs
[
r
.
MemoID
]
=
true
relatedIDs
[
r
.
RelatedMemoID
]
=
true
}
require
.
True
(
t
,
memoIDs
[
memoX
.
ID
],
"should include X->A relation"
)
require
.
True
(
t
,
memoIDs
[
memoA
.
ID
],
"should include A->B relation"
)
require
.
True
(
t
,
relatedIDs
[
memoA
.
ID
],
"should include X->A relation"
)
require
.
True
(
t
,
relatedIDs
[
memoB
.
ID
],
"should include A->B relation"
)
ts
.
Close
()
}
func
TestMemoRelationMultipleRelationsToSameMemo
(
t
*
testing
.
T
)
{
func
TestMemoRelationMultipleRelationsToSameMemo
(
t
*
testing
.
T
)
{
t
.
Parallel
()
t
.
Parallel
()
ctx
:=
context
.
Background
()
ctx
:=
context
.
Background
()
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment