chore: import upstream snapshot with attribution
Backend Tests / Tests (other) (push) Failing after 1s
Backend Tests / Static Checks (push) Failing after 2s
Backend Tests / Tests (internal) (push) Failing after 2s
Backend Tests / Tests (server) (push) Failing after 1s
Backend Tests / Tests (store) (push) Failing after 1s
Build Canary Image / build-frontend (push) Failing after 1s
Frontend Tests / Lint (push) Failing after 1s
Build Canary Image / build-push (linux/amd64) (push) Has been skipped
Build Canary Image / build-push (linux/arm64) (push) Has been skipped
Build Canary Image / merge (push) Has been skipped
Frontend Tests / Build (push) Failing after 1s
Release Please / release-please (push) Failing after 0s
Proto Linter / Lint Protos (push) Failing after 2s

This commit is contained in:
wehub-resource-sync
2026-07-13 12:02:24 +08:00
commit 4cddfcf2f3
1040 changed files with 200957 additions and 0 deletions
+254
View File
@@ -0,0 +1,254 @@
package sqlite
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/pkg/errors"
"google.golang.org/protobuf/encoding/protojson"
"github.com/usememos/memos/internal/filter"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
)
func (d *DB) CreateAttachment(ctx context.Context, create *store.Attachment) (*store.Attachment, error) {
fields := []string{"`uid`", "`filename`", "`blob`", "`type`", "`size`", "`creator_id`", "`memo_id`", "`storage_type`", "`reference`", "`payload`"}
placeholder := []string{"?", "?", "?", "?", "?", "?", "?", "?", "?", "?"}
storageType := ""
if create.StorageType != storepb.AttachmentStorageType_ATTACHMENT_STORAGE_TYPE_UNSPECIFIED {
storageType = create.StorageType.String()
}
payloadString := "{}"
if create.Payload != nil {
bytes, err := protojson.Marshal(create.Payload)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal attachment payload")
}
payloadString = string(bytes)
}
args := []any{create.UID, create.Filename, create.Blob, create.Type, create.Size, create.CreatorID, create.MemoID, storageType, create.Reference, payloadString}
stmt := "INSERT INTO `attachment` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ") RETURNING `id`, `created_ts`, `updated_ts`"
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(&create.ID, &create.CreatedTs, &create.UpdatedTs); err != nil {
return nil, err
}
return create, nil
}
func (d *DB) ListAttachments(ctx context.Context, find *store.FindAttachment) ([]*store.Attachment, error) {
where, args := []string{"1 = 1"}, []any{}
if v := find.ID; v != nil {
where, args = append(where, "`attachment`.`id` = ?"), append(args, *v)
}
if v := find.UID; v != nil {
where, args = append(where, "`attachment`.`uid` = ?"), append(args, *v)
}
if v := find.CreatorID; v != nil {
where, args = append(where, "`attachment`.`creator_id` = ?"), append(args, *v)
}
if v := find.Filename; v != nil {
where, args = append(where, "`attachment`.`filename` = ?"), append(args, *v)
}
if v := find.FilenameSearch; v != nil {
where, args = append(where, "`attachment`.`filename` LIKE ?"), append(args, fmt.Sprintf("%%%s%%", *v))
}
if v := find.MemoID; v != nil {
where, args = append(where, "`attachment`.`memo_id` = ?"), append(args, *v)
}
if len(find.MemoIDList) > 0 {
placeholders := make([]string, 0, len(find.MemoIDList))
for range find.MemoIDList {
placeholders = append(placeholders, "?")
}
where = append(where, "`attachment`.`memo_id` IN ("+strings.Join(placeholders, ",")+")")
for _, id := range find.MemoIDList {
args = append(args, id)
}
}
if find.HasRelatedMemo {
where = append(where, "`attachment`.`memo_id` IS NOT NULL")
}
if find.StorageType != nil {
where, args = append(where, "`attachment`.`storage_type` = ?"), append(args, find.StorageType.String())
}
if len(find.Filters) > 0 {
engine, err := filter.DefaultAttachmentEngine()
if err != nil {
return nil, errors.Wrap(err, "failed to get filter engine")
}
if err := filter.AppendConditions(ctx, engine, find.Filters, filter.DialectSQLite, &where, &args); err != nil {
return nil, errors.Wrap(err, "failed to append filter conditions")
}
}
fields := []string{
"`attachment`.`id` AS `id`",
"`attachment`.`uid` AS `uid`",
"`attachment`.`filename` AS `filename`",
"`attachment`.`type` AS `type`",
"`attachment`.`size` AS `size`",
"`attachment`.`creator_id` AS `creator_id`",
"`attachment`.`created_ts` AS `created_ts`",
"`attachment`.`updated_ts` AS `updated_ts`",
"`attachment`.`memo_id` AS `memo_id`",
"`attachment`.`storage_type` AS `storage_type`",
"`attachment`.`reference` AS `reference`",
"`attachment`.`payload` AS `payload`",
"CASE WHEN `memo`.`uid` IS NOT NULL THEN `memo`.`uid` ELSE NULL END AS `memo_uid`",
}
if find.GetBlob {
fields = append(fields, "`attachment`.`blob` AS `blob`")
}
query := "SELECT " + strings.Join(fields, ", ") + " FROM `attachment`" + " " +
"LEFT JOIN `memo` ON `attachment`.`memo_id` = `memo`.`id`" + " " +
"WHERE " + strings.Join(where, " AND ") + " " +
"ORDER BY `attachment`.`updated_ts` DESC"
if find.Limit != nil {
query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit)
if find.Offset != nil {
query = fmt.Sprintf("%s OFFSET %d", query, *find.Offset)
}
}
rows, err := d.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
list := make([]*store.Attachment, 0)
for rows.Next() {
attachment := store.Attachment{}
var memoID sql.NullInt32
var storageType string
var payloadBytes []byte
dests := []any{
&attachment.ID,
&attachment.UID,
&attachment.Filename,
&attachment.Type,
&attachment.Size,
&attachment.CreatorID,
&attachment.CreatedTs,
&attachment.UpdatedTs,
&memoID,
&storageType,
&attachment.Reference,
&payloadBytes,
&attachment.MemoUID,
}
if find.GetBlob {
dests = append(dests, &attachment.Blob)
}
if err := rows.Scan(dests...); err != nil {
return nil, err
}
if memoID.Valid {
attachment.MemoID = &memoID.Int32
}
attachment.StorageType = storepb.AttachmentStorageType(storepb.AttachmentStorageType_value[storageType])
payload := &storepb.AttachmentPayload{}
if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil {
return nil, err
}
attachment.Payload = payload
list = append(list, &attachment)
}
if err := rows.Err(); err != nil {
return nil, err
}
return list, nil
}
func (d *DB) UpdateAttachment(ctx context.Context, update *store.UpdateAttachment) error {
set, args := []string{}, []any{}
if v := update.UID; v != nil {
set, args = append(set, "`uid` = ?"), append(args, *v)
}
if v := update.UpdatedTs; v != nil {
set, args = append(set, "`updated_ts` = ?"), append(args, *v)
}
if v := update.Filename; v != nil {
set, args = append(set, "`filename` = ?"), append(args, *v)
}
if v := update.MemoID; v != nil {
set, args = append(set, "`memo_id` = ?"), append(args, *v)
}
if v := update.Reference; v != nil {
set, args = append(set, "`reference` = ?"), append(args, *v)
}
if v := update.Payload; v != nil {
bytes, err := protojson.Marshal(v)
if err != nil {
return errors.Wrap(err, "failed to marshal attachment payload")
}
set, args = append(set, "`payload` = ?"), append(args, string(bytes))
}
args = append(args, update.ID)
stmt := "UPDATE `attachment` SET " + strings.Join(set, ", ") + " WHERE `id` = ?"
result, err := d.db.ExecContext(ctx, stmt, args...)
if err != nil {
return errors.Wrap(err, "failed to update attachment")
}
if _, err := result.RowsAffected(); err != nil {
return err
}
return nil
}
func (d *DB) DeleteAttachment(ctx context.Context, delete *store.DeleteAttachment) error {
stmt := "DELETE FROM `attachment` WHERE `id` = ?"
result, err := d.db.ExecContext(ctx, stmt, delete.ID)
if err != nil {
return err
}
if _, err := result.RowsAffected(); err != nil {
return err
}
return nil
}
func (d *DB) DeleteAttachments(ctx context.Context, deletes []*store.DeleteAttachment) error {
if len(deletes) == 0 {
return nil
}
tx, err := d.db.BeginTx(ctx, nil)
if err != nil {
return errors.Wrap(err, "failed to start attachment delete transaction")
}
defer func() {
if tx != nil {
_ = tx.Rollback()
}
}()
stmt := "DELETE FROM `attachment` WHERE `id` = ?"
for _, delete := range deletes {
result, err := tx.ExecContext(ctx, stmt, delete.ID)
if err != nil {
return err
}
if _, err := result.RowsAffected(); err != nil {
return err
}
}
if err := tx.Commit(); err != nil {
return err
}
tx = nil
return nil
}
+9
View File
@@ -0,0 +1,9 @@
package sqlite
import "google.golang.org/protobuf/encoding/protojson"
var (
protojsonUnmarshaler = protojson.UnmarshalOptions{
DiscardUnknown: true,
}
)
+104
View File
@@ -0,0 +1,104 @@
// Package sqlite provides SQLite driver implementation with custom functions.
// Custom functions are registered globally on first use to extend SQLite's
// limited ASCII-only text operations with proper Unicode support.
package sqlite
import (
"database/sql/driver"
"errors"
"regexp"
"sync"
"golang.org/x/text/cases"
msqlite "modernc.org/sqlite"
)
var (
registerUnicodeLowerOnce sync.Once
registerUnicodeLowerErr error
// unicodeFold provides Unicode case folding for case-insensitive comparisons.
// It's safe to use concurrently and reused across all function calls.
unicodeFold = cases.Fold()
)
// ensureUnicodeLowerRegistered registers the memos_unicode_lower custom function
// with SQLite. This function provides proper Unicode case folding for case-insensitive
// text comparisons, overcoming modernc.org/sqlite's lack of ICU extension.
//
// The function is registered once globally and is safe to call multiple times.
func ensureUnicodeLowerRegistered() error {
registerUnicodeLowerOnce.Do(func() {
registerUnicodeLowerErr = msqlite.RegisterScalarFunction("memos_unicode_lower", 1, func(_ *msqlite.FunctionContext, args []driver.Value) (driver.Value, error) {
if len(args) == 0 || args[0] == nil {
return nil, nil
}
switch v := args[0].(type) {
case string:
return unicodeFold.String(v), nil
case []byte:
return unicodeFold.String(string(v)), nil
default:
return v, nil
}
})
})
return registerUnicodeLowerErr
}
var (
registerRegexpOnce sync.Once
registerRegexpErr error
// regexpCache memoizes compiled patterns; keys are pattern strings.
regexpCache sync.Map
)
// ensureRegexpRegistered registers a Go-backed `regexp(pattern, value)` scalar
// function so SQLite's `value REGEXP pattern` operator works (modernc.org/sqlite
// has no built-in implementation). Patterns use Go's RE2 syntax. Registered once
// globally; safe to call multiple times.
func ensureRegexpRegistered() error {
registerRegexpOnce.Do(func() {
registerRegexpErr = msqlite.RegisterScalarFunction("regexp", 2, func(_ *msqlite.FunctionContext, args []driver.Value) (driver.Value, error) {
if len(args) != 2 || args[0] == nil || args[1] == nil {
return int64(0), nil
}
pattern, ok := args[0].(string)
if !ok {
return nil, errors.New("regexp pattern must be a string")
}
var value string
switch v := args[1].(type) {
case string:
value = v
case []byte:
value = string(v)
default:
return int64(0), nil
}
re, err := compileRegexp(pattern)
if err != nil {
return nil, err
}
if re.MatchString(value) {
return int64(1), nil
}
return int64(0), nil
})
})
return registerRegexpErr
}
// compileRegexp compiles and caches a RE2 pattern.
func compileRegexp(pattern string) (*regexp.Regexp, error) {
if cached, ok := regexpCache.Load(pattern); ok {
if re, ok := cached.(*regexp.Regexp); ok {
return re, nil
}
}
re, err := regexp.Compile(pattern)
if err != nil {
return nil, err
}
regexpCache.Store(pattern, re)
return re, nil
}
+24
View File
@@ -0,0 +1,24 @@
package sqlite
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestRegexpFunctionMatches(t *testing.T) {
require.NoError(t, ensureRegexpRegistered())
re, err := compileRegexp(`^v\d+$`)
require.NoError(t, err)
require.True(t, re.MatchString("v12"))
require.False(t, re.MatchString("version"))
// Caching returns the same compiled instance.
re2, err := compileRegexp(`^v\d+$`)
require.NoError(t, err)
require.Same(t, re, re2)
_, err = compileRegexp(`(`)
require.Error(t, err)
}
+123
View File
@@ -0,0 +1,123 @@
package sqlite
import (
"context"
"fmt"
"strings"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
)
func (d *DB) CreateIdentityProvider(ctx context.Context, create *store.IdentityProvider) (*store.IdentityProvider, error) {
placeholders := []string{"?", "?", "?", "?", "?"}
fields := []string{"`uid`", "`name`", "`type`", "`identifier_filter`", "`config`"}
args := []any{create.UID, create.Name, create.Type.String(), create.IdentifierFilter, create.Config}
stmt := "INSERT INTO `idp` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholders, ", ") + ") RETURNING `id`"
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(&create.ID); err != nil {
return nil, err
}
identityProvider := create
return identityProvider, nil
}
func (d *DB) ListIdentityProviders(ctx context.Context, find *store.FindIdentityProvider) ([]*store.IdentityProvider, error) {
where, args := []string{"1 = 1"}, []any{}
if v := find.ID; v != nil {
where, args = append(where, fmt.Sprintf("id = $%d", len(args)+1)), append(args, *v)
}
if v := find.UID; v != nil {
where, args = append(where, fmt.Sprintf("uid = $%d", len(args)+1)), append(args, *v)
}
rows, err := d.db.QueryContext(ctx, `
SELECT
id,
uid,
name,
type,
identifier_filter,
config
FROM idp
WHERE `+strings.Join(where, " AND ")+` ORDER BY id ASC`,
args...,
)
if err != nil {
return nil, err
}
defer rows.Close()
var identityProviders []*store.IdentityProvider
for rows.Next() {
var identityProvider store.IdentityProvider
var typeString string
if err := rows.Scan(
&identityProvider.ID,
&identityProvider.UID,
&identityProvider.Name,
&typeString,
&identityProvider.IdentifierFilter,
&identityProvider.Config,
); err != nil {
return nil, err
}
identityProvider.Type = storepb.IdentityProvider_Type(storepb.IdentityProvider_Type_value[typeString])
identityProviders = append(identityProviders, &identityProvider)
}
if err := rows.Err(); err != nil {
return nil, err
}
return identityProviders, nil
}
func (d *DB) UpdateIdentityProvider(ctx context.Context, update *store.UpdateIdentityProvider) (*store.IdentityProvider, error) {
set, args := []string{}, []any{}
if v := update.Name; v != nil {
set, args = append(set, "name = ?"), append(args, *v)
}
if v := update.IdentifierFilter; v != nil {
set, args = append(set, "identifier_filter = ?"), append(args, *v)
}
if v := update.Config; v != nil {
set, args = append(set, "config = ?"), append(args, *v)
}
args = append(args, update.ID)
stmt := `
UPDATE idp
SET ` + strings.Join(set, ", ") + `
WHERE id = ?
RETURNING id, uid, name, type, identifier_filter, config
`
var identityProvider store.IdentityProvider
var typeString string
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(
&identityProvider.ID,
&identityProvider.UID,
&identityProvider.Name,
&typeString,
&identityProvider.IdentifierFilter,
&identityProvider.Config,
); err != nil {
return nil, err
}
identityProvider.Type = storepb.IdentityProvider_Type(storepb.IdentityProvider_Type_value[typeString])
return &identityProvider, nil
}
func (d *DB) DeleteIdentityProvider(ctx context.Context, delete *store.DeleteIdentityProvider) error {
where, args := []string{"id = ?"}, []any{delete.ID}
stmt := `DELETE FROM idp WHERE ` + strings.Join(where, " AND ")
result, err := d.db.ExecContext(ctx, stmt, args...)
if err != nil {
return err
}
if _, err = result.RowsAffected(); err != nil {
return err
}
return nil
}
+141
View File
@@ -0,0 +1,141 @@
package sqlite
import (
"context"
"fmt"
"strings"
"github.com/pkg/errors"
"google.golang.org/protobuf/encoding/protojson"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
)
func (d *DB) CreateInbox(ctx context.Context, create *store.Inbox) (*store.Inbox, error) {
messageString := "{}"
if create.Message != nil {
bytes, err := protojson.Marshal(create.Message)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal inbox message")
}
messageString = string(bytes)
}
fields := []string{"`sender_id`", "`receiver_id`", "`status`", "`message`"}
placeholder := []string{"?", "?", "?", "?"}
args := []any{create.SenderID, create.ReceiverID, create.Status, messageString}
stmt := "INSERT INTO `inbox` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ") RETURNING `id`, `created_ts`"
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(
&create.ID,
&create.CreatedTs,
); err != nil {
return nil, err
}
return create, nil
}
func (d *DB) ListInboxes(ctx context.Context, find *store.FindInbox) ([]*store.Inbox, error) {
where, args := []string{"1 = 1"}, []any{}
if find.ID != nil {
where, args = append(where, "`id` = ?"), append(args, *find.ID)
}
if find.SenderID != nil {
where, args = append(where, "`sender_id` = ?"), append(args, *find.SenderID)
}
if find.ReceiverID != nil {
where, args = append(where, "`receiver_id` = ?"), append(args, *find.ReceiverID)
}
if find.Status != nil {
where, args = append(where, "`status` = ?"), append(args, *find.Status)
}
if find.MessageType != nil {
// Filter by message type using JSON extraction
// Note: The type field in JSON is stored as string representation of the enum name
if *find.MessageType == storepb.InboxMessage_TYPE_UNSPECIFIED {
where, args = append(where, "(JSON_EXTRACT(`message`, '$.type') IS NULL OR JSON_EXTRACT(`message`, '$.type') = ?)"), append(args, find.MessageType.String())
} else {
where, args = append(where, "JSON_EXTRACT(`message`, '$.type') = ?"), append(args, find.MessageType.String())
}
}
query := "SELECT `id`, `created_ts`, `sender_id`, `receiver_id`, `status`, `message` FROM `inbox` WHERE " + strings.Join(where, " AND ") + " ORDER BY `created_ts` DESC"
if find.Limit != nil {
query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit)
if find.Offset != nil {
query = fmt.Sprintf("%s OFFSET %d", query, *find.Offset)
}
}
rows, err := d.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
list := []*store.Inbox{}
for rows.Next() {
inbox := &store.Inbox{}
var messageBytes []byte
if err := rows.Scan(
&inbox.ID,
&inbox.CreatedTs,
&inbox.SenderID,
&inbox.ReceiverID,
&inbox.Status,
&messageBytes,
); err != nil {
return nil, err
}
message := &storepb.InboxMessage{}
if err := protojsonUnmarshaler.Unmarshal(messageBytes, message); err != nil {
return nil, err
}
inbox.Message = message
list = append(list, inbox)
}
if err := rows.Err(); err != nil {
return nil, err
}
return list, nil
}
func (d *DB) UpdateInbox(ctx context.Context, update *store.UpdateInbox) (*store.Inbox, error) {
set, args := []string{"`status` = ?"}, []any{update.Status.String()}
args = append(args, update.ID)
query := "UPDATE `inbox` SET " + strings.Join(set, ", ") + " WHERE `id` = ? RETURNING `id`, `created_ts`, `sender_id`, `receiver_id`, `status`, `message`"
inbox := &store.Inbox{}
var messageBytes []byte
if err := d.db.QueryRowContext(ctx, query, args...).Scan(
&inbox.ID,
&inbox.CreatedTs,
&inbox.SenderID,
&inbox.ReceiverID,
&inbox.Status,
&messageBytes,
); err != nil {
return nil, err
}
message := &storepb.InboxMessage{}
if err := protojsonUnmarshaler.Unmarshal(messageBytes, message); err != nil {
return nil, err
}
inbox.Message = message
return inbox, nil
}
func (d *DB) DeleteInbox(ctx context.Context, delete *store.DeleteInbox) error {
result, err := d.db.ExecContext(ctx, "DELETE FROM `inbox` WHERE `id` = ?", delete.ID)
if err != nil {
return err
}
if _, err := result.RowsAffected(); err != nil {
return err
}
return nil
}
+72
View File
@@ -0,0 +1,72 @@
package sqlite
import (
"context"
"strings"
"github.com/usememos/memos/store"
)
func (d *DB) UpsertInstanceSetting(ctx context.Context, upsert *store.InstanceSetting) (*store.InstanceSetting, error) {
stmt := `
INSERT INTO system_setting (
name, value, description
)
VALUES (?, ?, ?)
ON CONFLICT(name) DO UPDATE
SET
value = EXCLUDED.value,
description = EXCLUDED.description
`
if _, err := d.db.ExecContext(ctx, stmt, upsert.Name, upsert.Value, upsert.Description); err != nil {
return nil, err
}
return upsert, nil
}
func (d *DB) ListInstanceSettings(ctx context.Context, find *store.FindInstanceSetting) ([]*store.InstanceSetting, error) {
where, args := []string{"1 = 1"}, []any{}
if find.Name != "" {
where, args = append(where, "name = ?"), append(args, find.Name)
}
query := `
SELECT
name,
value,
description
FROM system_setting
WHERE ` + strings.Join(where, " AND ")
rows, err := d.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
list := []*store.InstanceSetting{}
for rows.Next() {
systemSettingMessage := &store.InstanceSetting{}
if err := rows.Scan(
&systemSettingMessage.Name,
&systemSettingMessage.Value,
&systemSettingMessage.Description,
); err != nil {
return nil, err
}
list = append(list, systemSettingMessage)
}
if err := rows.Err(); err != nil {
return nil, err
}
return list, nil
}
func (d *DB) DeleteInstanceSetting(ctx context.Context, delete *store.DeleteInstanceSetting) error {
stmt := "DELETE FROM system_setting WHERE name = ?"
_, err := d.db.ExecContext(ctx, stmt, delete.Name)
return err
}
+248
View File
@@ -0,0 +1,248 @@
package sqlite
import (
"context"
"fmt"
"strings"
"github.com/pkg/errors"
"google.golang.org/protobuf/encoding/protojson"
"github.com/usememos/memos/internal/filter"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
)
func (d *DB) CreateMemo(ctx context.Context, create *store.Memo) (*store.Memo, error) {
fields := []string{"`uid`", "`creator_id`", "`content`", "`visibility`", "`payload`"}
placeholder := []string{"?", "?", "?", "?", "?"}
payload := "{}"
if create.Payload != nil {
payloadBytes, err := protojson.Marshal(create.Payload)
if err != nil {
return nil, err
}
payload = string(payloadBytes)
}
args := []any{create.UID, create.CreatorID, create.Content, create.Visibility, payload}
// Add custom timestamps if provided
if create.CreatedTs != 0 {
fields = append(fields, "`created_ts`")
placeholder = append(placeholder, "?")
args = append(args, create.CreatedTs)
}
if create.UpdatedTs != 0 {
fields = append(fields, "`updated_ts`")
placeholder = append(placeholder, "?")
args = append(args, create.UpdatedTs)
}
stmt := "INSERT INTO `memo` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ") RETURNING `id`, `created_ts`, `updated_ts`, `row_status`"
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(
&create.ID,
&create.CreatedTs,
&create.UpdatedTs,
&create.RowStatus,
); err != nil {
return nil, err
}
return create, nil
}
func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo, error) {
where, args := []string{"1 = 1"}, []any{}
engine, err := filter.DefaultEngine()
if err != nil {
return nil, err
}
if err := filter.AppendConditions(ctx, engine, find.Filters, filter.DialectSQLite, &where, &args); err != nil {
return nil, err
}
if v := find.ID; v != nil {
where, args = append(where, "`memo`.`id` = ?"), append(args, *v)
}
if len(find.IDList) > 0 {
placeholders := make([]string, 0, len(find.IDList))
for range find.IDList {
placeholders = append(placeholders, "?")
}
where = append(where, "`memo`.`id` IN ("+strings.Join(placeholders, ",")+")")
for _, id := range find.IDList {
args = append(args, id)
}
}
if v := find.UID; v != nil {
where, args = append(where, "`memo`.`uid` = ?"), append(args, *v)
}
if len(find.UIDList) > 0 {
placeholders := make([]string, 0, len(find.UIDList))
for range find.UIDList {
placeholders = append(placeholders, "?")
}
where = append(where, "`memo`.`uid` IN ("+strings.Join(placeholders, ",")+")")
for _, uid := range find.UIDList {
args = append(args, uid)
}
}
if v := find.CreatorID; v != nil {
where, args = append(where, "`memo`.`creator_id` = ?"), append(args, *v)
}
if v := find.RowStatus; v != nil {
where, args = append(where, "`memo`.`row_status` = ?"), append(args, *v)
}
if v := find.VisibilityList; len(v) != 0 {
placeholder := []string{}
for _, visibility := range v {
placeholder = append(placeholder, "?")
args = append(args, visibility.String())
}
where = append(where, fmt.Sprintf("`memo`.`visibility` IN (%s)", strings.Join(placeholder, ",")))
}
if find.ExcludeComments {
where = append(where, "`parent_uid` IS NULL")
}
order := "DESC"
if find.OrderByTimeAsc {
order = "ASC"
}
orderBy := []string{}
if find.OrderByPinned {
orderBy = append(orderBy, "`pinned` DESC")
}
if find.OrderByUpdatedTs {
orderBy = append(orderBy, "`updated_ts` "+order)
} else {
orderBy = append(orderBy, "`created_ts` "+order)
}
// Add id as final tie-breaker
orderBy = append(orderBy, "`id` DESC")
fields := []string{
"`memo`.`id` AS `id`",
"`memo`.`uid` AS `uid`",
"`memo`.`creator_id` AS `creator_id`",
"`memo`.`created_ts` AS `created_ts`",
"`memo`.`updated_ts` AS `updated_ts`",
"`memo`.`row_status` AS `row_status`",
"`memo`.`visibility` AS `visibility`",
"`memo`.`pinned` AS `pinned`",
"`memo`.`payload` AS `payload`",
"CASE WHEN `parent_memo`.`uid` IS NOT NULL THEN `parent_memo`.`uid` ELSE NULL END AS `parent_uid`",
}
if !find.ExcludeContent {
fields = append(fields, "`memo`.`content` AS `content`")
}
query := "SELECT " + strings.Join(fields, ", ") + "FROM `memo` " +
"LEFT JOIN `user` AS `memo_creator` ON `memo`.`creator_id` = `memo_creator`.`id` " +
"LEFT JOIN `memo_relation` ON `memo`.`id` = `memo_relation`.`memo_id` AND `memo_relation`.`type` = \"COMMENT\" " +
"LEFT JOIN `memo` AS `parent_memo` ON `memo_relation`.`related_memo_id` = `parent_memo`.`id` " +
"WHERE " + strings.Join(where, " AND ") + " " +
"ORDER BY " + strings.Join(orderBy, ", ")
if find.Limit != nil {
query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit)
if find.Offset != nil {
query = fmt.Sprintf("%s OFFSET %d", query, *find.Offset)
}
}
rows, err := d.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
list := make([]*store.Memo, 0)
for rows.Next() {
var memo store.Memo
var payloadBytes []byte
dests := []any{
&memo.ID,
&memo.UID,
&memo.CreatorID,
&memo.CreatedTs,
&memo.UpdatedTs,
&memo.RowStatus,
&memo.Visibility,
&memo.Pinned,
&payloadBytes,
&memo.ParentUID,
}
if !find.ExcludeContent {
dests = append(dests, &memo.Content)
}
if err := rows.Scan(dests...); err != nil {
return nil, err
}
payload := &storepb.MemoPayload{}
if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal payload")
}
memo.Payload = payload
list = append(list, &memo)
}
if err := rows.Err(); err != nil {
return nil, err
}
return list, nil
}
func (d *DB) UpdateMemo(ctx context.Context, update *store.UpdateMemo) error {
set, args := []string{}, []any{}
if v := update.UID; v != nil {
set, args = append(set, "`uid` = ?"), append(args, *v)
}
if v := update.CreatedTs; v != nil {
set, args = append(set, "`created_ts` = ?"), append(args, *v)
}
if v := update.UpdatedTs; v != nil {
set, args = append(set, "`updated_ts` = ?"), append(args, *v)
}
if v := update.RowStatus; v != nil {
set, args = append(set, "`row_status` = ?"), append(args, *v)
}
if v := update.Content; v != nil {
set, args = append(set, "`content` = ?"), append(args, *v)
}
if v := update.Visibility; v != nil {
set, args = append(set, "`visibility` = ?"), append(args, *v)
}
if v := update.Pinned; v != nil {
set, args = append(set, "`pinned` = ?"), append(args, *v)
}
if v := update.Payload; v != nil {
payloadBytes, err := protojson.Marshal(v)
if err != nil {
return err
}
set, args = append(set, "`payload` = ?"), append(args, string(payloadBytes))
}
if len(set) == 0 {
return nil
}
args = append(args, update.ID)
stmt := "UPDATE `memo` SET " + strings.Join(set, ", ") + " WHERE `id` = ?"
if _, err := d.db.ExecContext(ctx, stmt, args...); err != nil {
return err
}
return nil
}
func (d *DB) DeleteMemo(ctx context.Context, delete *store.DeleteMemo) error {
where, args := []string{"`id` = ?"}, []any{delete.ID}
stmt := "DELETE FROM `memo` WHERE " + strings.Join(where, " AND ")
result, err := d.db.ExecContext(ctx, stmt, args...)
if err != nil {
return err
}
if _, err := result.RowsAffected(); err != nil {
return err
}
return nil
}
+159
View File
@@ -0,0 +1,159 @@
package sqlite
import (
"context"
"fmt"
"strings"
"github.com/usememos/memos/internal/filter"
"github.com/usememos/memos/store"
)
func (d *DB) UpsertMemoRelation(ctx context.Context, create *store.MemoRelation) (*store.MemoRelation, error) {
stmt := `
INSERT INTO memo_relation (
memo_id,
related_memo_id,
type
)
VALUES (?, ?, ?)
ON CONFLICT(memo_id, related_memo_id, type) DO UPDATE SET type = excluded.type
RETURNING memo_id, related_memo_id, type
`
memoRelation := &store.MemoRelation{}
if err := d.db.QueryRowContext(
ctx,
stmt,
create.MemoID,
create.RelatedMemoID,
create.Type,
).Scan(
&memoRelation.MemoID,
&memoRelation.RelatedMemoID,
&memoRelation.Type,
); err != nil {
return nil, err
}
return memoRelation, nil
}
func (d *DB) ListMemoRelations(ctx context.Context, find *store.FindMemoRelation) ([]*store.MemoRelation, error) {
where, args := []string{"TRUE"}, []any{}
if find.MemoID != nil {
where, args = append(where, "memo_id = ?"), append(args, *find.MemoID)
}
if find.RelatedMemoID != nil {
where, args = append(where, "related_memo_id = ?"), append(args, *find.RelatedMemoID)
}
if find.Type != nil {
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 len(find.SourceMemoIDList) > 0 {
placeholders := make([]string, len(find.SourceMemoIDList))
for i, id := range find.SourceMemoIDList {
placeholders[i] = "?"
args = append(args, id)
}
where = append(where, fmt.Sprintf("memo_id IN (%s)", strings.Join(placeholders, ", ")))
}
if len(find.RelatedMemoIDList) > 0 {
placeholders := make([]string, len(find.RelatedMemoIDList))
for i, id := range find.RelatedMemoIDList {
placeholders[i] = "?"
args = append(args, id)
}
where = append(where, fmt.Sprintf("related_memo_id IN (%s)", strings.Join(placeholders, ", ")))
}
if find.MemoFilter != nil {
engine, err := filter.DefaultEngine()
if err != nil {
return nil, err
}
stmt, err := engine.CompileToStatement(ctx, *find.MemoFilter, filter.RenderOptions{Dialect: filter.DialectSQLite})
if err != nil {
return nil, err
}
if stmt.SQL != "" {
where = append(where, fmt.Sprintf("memo_id IN (SELECT id FROM memo WHERE %s)", stmt.SQL))
where = append(where, fmt.Sprintf("related_memo_id IN (SELECT id FROM memo WHERE %s)", stmt.SQL))
args = append(args, append(stmt.Args, stmt.Args...)...)
}
}
query := `
SELECT
memo_id,
related_memo_id,
type
FROM memo_relation
WHERE ` + strings.Join(where, " AND ") + `
ORDER BY memo_id DESC`
if find.Limit != nil {
query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit)
if find.Offset != nil {
query = fmt.Sprintf("%s OFFSET %d", query, *find.Offset)
}
}
rows, err := d.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
list := []*store.MemoRelation{}
for rows.Next() {
memoRelation := &store.MemoRelation{}
if err := rows.Scan(
&memoRelation.MemoID,
&memoRelation.RelatedMemoID,
&memoRelation.Type,
); err != nil {
return nil, err
}
list = append(list, memoRelation)
}
if err := rows.Err(); err != nil {
return nil, err
}
return list, nil
}
func (d *DB) DeleteMemoRelation(ctx context.Context, delete *store.DeleteMemoRelation) error {
where, args := []string{"TRUE"}, []any{}
if delete.MemoID != nil {
where, args = append(where, "memo_id = ?"), append(args, *delete.MemoID)
}
if delete.RelatedMemoID != nil {
where, args = append(where, "related_memo_id = ?"), append(args, *delete.RelatedMemoID)
}
if delete.Type != nil {
where, args = append(where, "type = ?"), append(args, *delete.Type)
}
stmt := `
DELETE FROM memo_relation
WHERE ` + strings.Join(where, " AND ")
result, err := d.db.ExecContext(ctx, stmt, args...)
if err != nil {
return err
}
if _, err = result.RowsAffected(); err != nil {
return err
}
return nil
}
+144
View File
@@ -0,0 +1,144 @@
package sqlite
import (
"context"
"database/sql"
"errors"
"strings"
"github.com/usememos/memos/store"
)
func (d *DB) CreateMemoShare(ctx context.Context, create *store.MemoShare) (*store.MemoShare, error) {
fields := []string{"`uid`", "`memo_id`", "`creator_id`"}
placeholders := []string{"?", "?", "?"}
args := []any{create.UID, create.MemoID, create.CreatorID}
if create.ExpiresTs != nil {
fields = append(fields, "`expires_ts`")
placeholders = append(placeholders, "?")
args = append(args, *create.ExpiresTs)
}
stmt := "INSERT INTO `memo_share` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholders, ", ") + ") RETURNING `id`, `created_ts`"
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(
&create.ID,
&create.CreatedTs,
); err != nil {
return nil, err
}
return create, nil
}
func (d *DB) ListMemoShares(ctx context.Context, find *store.FindMemoShare) ([]*store.MemoShare, error) {
where, args := []string{"1 = 1"}, []any{}
if find.ID != nil {
where, args = append(where, "`id` = ?"), append(args, *find.ID)
}
if find.UID != nil {
where, args = append(where, "`uid` = ?"), append(args, *find.UID)
}
if find.MemoID != nil {
where, args = append(where, "`memo_id` = ?"), append(args, *find.MemoID)
}
if find.CreatorID != nil {
where, args = append(where, "`creator_id` = ?"), append(args, *find.CreatorID)
}
rows, err := d.db.QueryContext(ctx, `
SELECT
id,
uid,
memo_id,
creator_id,
created_ts,
expires_ts
FROM memo_share
WHERE `+strings.Join(where, " AND ")+`
ORDER BY id ASC`,
args...,
)
if err != nil {
return nil, err
}
defer rows.Close()
list := []*store.MemoShare{}
for rows.Next() {
ms := &store.MemoShare{}
if err := rows.Scan(
&ms.ID,
&ms.UID,
&ms.MemoID,
&ms.CreatorID,
&ms.CreatedTs,
&ms.ExpiresTs,
); err != nil {
return nil, err
}
list = append(list, ms)
}
if err := rows.Err(); err != nil {
return nil, err
}
return list, nil
}
func (d *DB) GetMemoShare(ctx context.Context, find *store.FindMemoShare) (*store.MemoShare, error) {
where, args := []string{"1 = 1"}, []any{}
if find.ID != nil {
where, args = append(where, "`id` = ?"), append(args, *find.ID)
}
if find.UID != nil {
where, args = append(where, "`uid` = ?"), append(args, *find.UID)
}
if find.MemoID != nil {
where, args = append(where, "`memo_id` = ?"), append(args, *find.MemoID)
}
if find.CreatorID != nil {
where, args = append(where, "`creator_id` = ?"), append(args, *find.CreatorID)
}
ms := &store.MemoShare{}
if err := d.db.QueryRowContext(ctx, `
SELECT
id,
uid,
memo_id,
creator_id,
created_ts,
expires_ts
FROM memo_share
WHERE `+strings.Join(where, " AND ")+`
LIMIT 1`,
args...,
).Scan(
&ms.ID,
&ms.UID,
&ms.MemoID,
&ms.CreatorID,
&ms.CreatedTs,
&ms.ExpiresTs,
); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, err
}
return ms, nil
}
func (d *DB) DeleteMemoShare(ctx context.Context, delete *store.DeleteMemoShare) error {
where, args := []string{"1 = 1"}, []any{}
if delete.ID != nil {
where, args = append(where, "`id` = ?"), append(args, *delete.ID)
}
if delete.UID != nil {
where, args = append(where, "`uid` = ?"), append(args, *delete.UID)
}
_, err := d.db.ExecContext(ctx, "DELETE FROM `memo_share` WHERE "+strings.Join(where, " AND "), args...)
return err
}
+136
View File
@@ -0,0 +1,136 @@
package sqlite
import (
"context"
"database/sql"
"errors"
"strings"
"github.com/usememos/memos/store"
)
func (d *DB) UpsertReaction(ctx context.Context, upsert *store.Reaction) (*store.Reaction, error) {
fields := []string{"`creator_id`", "`content_id`", "`reaction_type`"}
placeholder := []string{"?", "?", "?"}
args := []interface{}{upsert.CreatorID, upsert.ContentID, upsert.ReactionType}
stmt := "INSERT INTO `reaction` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ") RETURNING `id`, `created_ts`"
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(
&upsert.ID,
&upsert.CreatedTs,
); err != nil {
return nil, err
}
reaction := upsert
return reaction, nil
}
func (d *DB) ListReactions(ctx context.Context, find *store.FindReaction) ([]*store.Reaction, error) {
where, args := []string{"1 = 1"}, []any{}
if find.ID != nil {
where, args = append(where, "id = ?"), append(args, *find.ID)
}
if find.CreatorID != nil {
where, args = append(where, "creator_id = ?"), append(args, *find.CreatorID)
}
if find.ContentID != nil {
where, args = append(where, "content_id = ?"), append(args, *find.ContentID)
}
if len(find.ContentIDList) > 0 {
placeholders := make([]string, 0, len(find.ContentIDList))
for range find.ContentIDList {
placeholders = append(placeholders, "?")
}
if len(placeholders) > 0 {
where = append(where, "content_id IN ("+strings.Join(placeholders, ",")+")")
for _, id := range find.ContentIDList {
args = append(args, id)
}
}
}
rows, err := d.db.QueryContext(ctx, `
SELECT
id,
created_ts,
creator_id,
content_id,
reaction_type
FROM reaction
WHERE `+strings.Join(where, " AND ")+`
ORDER BY id ASC`,
args...,
)
if err != nil {
return nil, err
}
defer rows.Close()
list := []*store.Reaction{}
for rows.Next() {
reaction := &store.Reaction{}
if err := rows.Scan(
&reaction.ID,
&reaction.CreatedTs,
&reaction.CreatorID,
&reaction.ContentID,
&reaction.ReactionType,
); err != nil {
return nil, err
}
list = append(list, reaction)
}
if err := rows.Err(); err != nil {
return nil, err
}
return list, nil
}
func (d *DB) GetReaction(ctx context.Context, find *store.FindReaction) (*store.Reaction, error) {
where, args := []string{"1 = 1"}, []any{}
if find.ID != nil {
where, args = append(where, "id = ?"), append(args, *find.ID)
}
if find.CreatorID != nil {
where, args = append(where, "creator_id = ?"), append(args, *find.CreatorID)
}
if find.ContentID != nil {
where, args = append(where, "content_id = ?"), append(args, *find.ContentID)
}
reaction := &store.Reaction{}
if err := d.db.QueryRowContext(ctx, `
SELECT
id,
created_ts,
creator_id,
content_id,
reaction_type
FROM reaction
WHERE `+strings.Join(where, " AND ")+`
LIMIT 1`,
args...,
).Scan(
&reaction.ID,
&reaction.CreatedTs,
&reaction.CreatorID,
&reaction.ContentID,
&reaction.ReactionType,
); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, err
}
return reaction, nil
}
func (d *DB) DeleteReaction(ctx context.Context, delete *store.DeleteReaction) error {
_, err := d.db.ExecContext(ctx, "DELETE FROM `reaction` WHERE `id` = ?", delete.ID)
return err
}
+91
View File
@@ -0,0 +1,91 @@
package sqlite
import (
"context"
"database/sql"
"github.com/pkg/errors"
// Note: modernc.org/sqlite driver is imported in functions.go where
// RegisterScalarFunction is used. No blank import needed here.
"github.com/usememos/memos/internal/profile"
"github.com/usememos/memos/store"
)
type DB struct {
db *sql.DB
profile *profile.Profile
}
// NewDB opens a database specified by its database driver name and a
// driver-specific data source name, usually consisting of at least a
// database name and connection information.
func NewDB(profile *profile.Profile) (store.Driver, error) {
// Ensure a DSN is set before attempting to open the database.
if profile.DSN == "" {
return nil, errors.New("dsn required")
}
if err := ensureUnicodeLowerRegistered(); err != nil {
return nil, errors.Wrap(err, "failed to register sqlite unicode lower function")
}
if err := ensureRegexpRegistered(); err != nil {
return nil, errors.Wrap(err, "failed to register sqlite regexp function")
}
// Connect to the database with some sane settings:
// - No shared-cache: it's obsolete; WAL journal mode is a better solution.
// - No foreign key constraints: it's currently disabled by default, but it's a
// good practice to be explicit and prevent future surprises on SQLite upgrades.
// - Journal mode set to WAL: it's the recommended journal mode for most applications
// as it prevents locking issues.
// - mmap size set to 0: it disables memory mapping, which can cause OOM errors on some systems.
//
// Notes:
// - When using the `modernc.org/sqlite` driver, each pragma must be prefixed with `_pragma=`.
//
// References:
// - https://pkg.go.dev/modernc.org/sqlite#Driver.Open
// - https://www.sqlite.org/sharedcache.html
// - https://www.sqlite.org/pragma.html
sqliteDB, err := sql.Open("sqlite", profile.DSN+"?_pragma=foreign_keys(0)&_pragma=busy_timeout(10000)&_pragma=journal_mode(WAL)&_pragma=mmap_size(0)")
if err != nil {
return nil, errors.Wrapf(err, "failed to open db with dsn: %s", profile.DSN)
}
driver := DB{db: sqliteDB, profile: profile}
return &driver, nil
}
func (d *DB) GetDB() *sql.DB {
return d.db
}
func (d *DB) Close() error {
return d.db.Close()
}
func (d *DB) IsInitialized(ctx context.Context) (bool, error) {
// Check if the database is initialized by checking if the memo table exists.
var exists bool
err := d.db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='table' AND name='memo')").Scan(&exists)
if err != nil {
return false, errors.Wrap(err, "failed to check if database is initialized")
}
return exists, nil
}
// GetDatabaseSize returns the database size in bytes, or -1 if unavailable.
func (d *DB) GetDatabaseSize(ctx context.Context) (int64, error) {
var pageCount, pageSize int64
if err := d.db.QueryRowContext(ctx, "PRAGMA page_count").Scan(&pageCount); err != nil {
return -1, errors.Wrap(err, "failed to read page_count")
}
if err := d.db.QueryRowContext(ctx, "PRAGMA page_size").Scan(&pageSize); err != nil {
return -1, errors.Wrap(err, "failed to read page_size")
}
return pageCount * pageSize, nil
}
+205
View File
@@ -0,0 +1,205 @@
package sqlite
import (
"context"
"fmt"
"strings"
"github.com/pkg/errors"
"github.com/usememos/memos/store"
)
func (d *DB) CreateUser(ctx context.Context, create *store.User) (*store.User, error) {
fields := []string{"`username`", "`role`", "`email`", "`nickname`", "`password_hash`, `avatar_url`"}
placeholder := []string{"?", "?", "?", "?", "?", "?"}
args := []any{create.Username, create.Role, create.Email, create.Nickname, create.PasswordHash, create.AvatarURL}
stmt := "INSERT INTO user (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ") RETURNING id, description, created_ts, updated_ts, row_status"
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(
&create.ID,
&create.Description,
&create.CreatedTs,
&create.UpdatedTs,
&create.RowStatus,
); err != nil {
return nil, err
}
return create, nil
}
func (d *DB) UpdateUser(ctx context.Context, update *store.UpdateUser) (*store.User, error) {
set, args := []string{}, []any{}
if v := update.UpdatedTs; v != nil {
set, args = append(set, "updated_ts = ?"), append(args, *v)
}
if v := update.RowStatus; v != nil {
set, args = append(set, "row_status = ?"), append(args, *v)
}
if v := update.Username; v != nil {
set, args = append(set, "username = ?"), append(args, *v)
}
if v := update.Email; v != nil {
set, args = append(set, "email = ?"), append(args, *v)
}
if v := update.Nickname; v != nil {
set, args = append(set, "nickname = ?"), append(args, *v)
}
if v := update.AvatarURL; v != nil {
set, args = append(set, "avatar_url = ?"), append(args, *v)
}
if v := update.PasswordHash; v != nil {
set, args = append(set, "password_hash = ?"), append(args, *v)
}
if v := update.Description; v != nil {
set, args = append(set, "description = ?"), append(args, *v)
}
if v := update.Role; v != nil {
set, args = append(set, "role = ?"), append(args, *v)
}
args = append(args, update.ID)
query := `
UPDATE user
SET ` + strings.Join(set, ", ") + `
WHERE id = ?
RETURNING id, username, role, email, nickname, password_hash, avatar_url, description, created_ts, updated_ts, row_status
`
user := &store.User{}
if err := d.db.QueryRowContext(ctx, query, args...).Scan(
&user.ID,
&user.Username,
&user.Role,
&user.Email,
&user.Nickname,
&user.PasswordHash,
&user.AvatarURL,
&user.Description,
&user.CreatedTs,
&user.UpdatedTs,
&user.RowStatus,
); err != nil {
return nil, err
}
return user, nil
}
func (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User, error) {
where, args := []string{"1 = 1"}, []any{}
orderBy := []string{"created_ts DESC", "row_status DESC", "id DESC"}
if len(find.Filters) > 0 {
return nil, errors.Errorf("user filters are not supported")
}
if v := find.ID; v != nil {
where, args = append(where, "id = ?"), append(args, *v)
}
if len(find.IDList) > 0 {
placeholders := make([]string, 0, len(find.IDList))
for range find.IDList {
placeholders = append(placeholders, "?")
}
where, args = append(where, fmt.Sprintf("id IN (%s)", strings.Join(placeholders, ", "))), append(args, func() []any {
list := make([]any, 0, len(find.IDList))
for _, id := range find.IDList {
list = append(list, id)
}
return list
}()...)
}
if len(find.UsernameList) > 0 {
placeholders := make([]string, 0, len(find.UsernameList))
for range find.UsernameList {
placeholders = append(placeholders, "?")
}
where, args = append(where, fmt.Sprintf("username IN (%s)", strings.Join(placeholders, ", "))), append(args, func() []any {
list := make([]any, 0, len(find.UsernameList))
for _, username := range find.UsernameList {
list = append(list, username)
}
return list
}()...)
}
if v := find.RowStatus; v != nil {
where, args = append(where, "row_status = ?"), append(args, *v)
}
if v := find.Username; v != nil {
where, args = append(where, "username = ?"), append(args, *v)
}
if v := find.Role; v != nil {
where, args = append(where, "role = ?"), append(args, *v)
}
if v := find.Email; v != nil {
where, args = append(where, "email = ?"), append(args, *v)
}
if v := find.Nickname; v != nil {
where, args = append(where, "nickname = ?"), append(args, *v)
}
if v := find.Search; v != nil && strings.TrimSpace(*v) != "" {
query := strings.ToLower(strings.TrimSpace(*v))
where, args = append(where, "(LOWER(username) LIKE ? OR LOWER(nickname) LIKE ?)"), append(args, "%"+query+"%", "%"+query+"%")
orderBy = []string{
"CASE WHEN LOWER(username) = ? THEN 0 WHEN LOWER(username) LIKE ? THEN 1 WHEN LOWER(nickname) LIKE ? THEN 2 ELSE 3 END",
"LENGTH(username) ASC",
"created_ts DESC",
"row_status DESC",
}
args = append(args, query, query+"%", query+"%")
}
query := `
SELECT
id,
username,
role,
email,
nickname,
password_hash,
avatar_url,
description,
created_ts,
updated_ts,
row_status
FROM user
WHERE ` + strings.Join(where, " AND ") + ` ORDER BY ` + strings.Join(orderBy, ", ")
if v := find.Limit; v != nil {
query += fmt.Sprintf(" LIMIT %d", *v)
if v := find.Offset; v != nil {
query += fmt.Sprintf(" OFFSET %d", *v)
}
}
rows, err := d.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
list := make([]*store.User, 0)
for rows.Next() {
var user store.User
if err := rows.Scan(
&user.ID,
&user.Username,
&user.Role,
&user.Email,
&user.Nickname,
&user.PasswordHash,
&user.AvatarURL,
&user.Description,
&user.CreatedTs,
&user.UpdatedTs,
&user.RowStatus,
); err != nil {
return nil, err
}
list = append(list, &user)
}
if err := rows.Err(); err != nil {
return nil, err
}
return list, nil
}
+529
View File
@@ -0,0 +1,529 @@
package sqlite
import (
"context"
"database/sql"
"strings"
"github.com/pkg/errors"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
)
const deleteUserBatchSize = 500
type deleteUserMemoRef struct {
ID int32
UID string
}
type deleteUserTargetSet struct {
memos []deleteUserMemoRef
attachments []*store.Attachment
attachmentIDs []int32
userSettingKeys []storepb.UserSetting_Key
inboxIDs []int32
}
func (d *DB) DeleteUser(ctx context.Context, delete *store.DeleteUser) (*store.DeleteUserResult, error) {
tx, err := d.db.BeginTx(ctx, nil)
if err != nil {
return nil, errors.Wrap(err, "failed to begin delete user transaction")
}
defer func() {
_ = tx.Rollback()
}()
targets, err := collectDeleteUserTargets(ctx, tx, delete.ID)
if err != nil {
return nil, errors.Wrap(err, "failed to collect delete user targets")
}
if err := deleteUserTargetsTx(ctx, tx, delete.ID, targets); err != nil {
return nil, errors.Wrap(err, "failed to delete user targets")
}
if store.GetDeleteUserFailpoint(ctx) == store.DeleteUserFailpointBeforeCommit {
return nil, errors.New("delete user failpoint before commit")
}
if err := tx.Commit(); err != nil {
return nil, errors.Wrap(err, "failed to commit delete user transaction")
}
return &store.DeleteUserResult{
Attachments: targets.attachments,
UserSettingKeys: targets.userSettingKeys,
}, nil
}
func collectDeleteUserTargets(ctx context.Context, tx *sql.Tx, userID int32) (*deleteUserTargetSet, error) {
targets := &deleteUserTargetSet{}
memos, err := listDeleteUserMemoTree(ctx, tx, userID)
if err != nil {
return nil, err
}
targets.memos = memos
attachments, err := listDeleteUserAttachments(ctx, tx, userID, memoIDsFromRefs(memos))
if err != nil {
return nil, err
}
targets.attachments = attachments
targets.attachmentIDs = attachmentIDsFromList(attachments)
userSettingKeys, err := listDeleteUserSettingKeys(ctx, tx, userID)
if err != nil {
return nil, err
}
targets.userSettingKeys = userSettingKeys
inboxIDs, err := listDeleteUserInboxIDs(ctx, tx, userID, memoIDSetFromRefs(memos))
if err != nil {
return nil, err
}
targets.inboxIDs = inboxIDs
return targets, nil
}
func deleteUserTargetsTx(ctx context.Context, tx *sql.Tx, userID int32, targets *deleteUserTargetSet) error {
memoIDs := memoIDsFromRefs(targets.memos)
contentIDs := memoContentIDsFromRefs(targets.memos)
if err := deleteReactionsByContentIDsTx(ctx, tx, contentIDs); err != nil {
return err
}
if err := deleteAttachmentsByIDsTx(ctx, tx, targets.attachmentIDs); err != nil {
return err
}
if err := deleteReactionsByCreatorTx(ctx, tx, userID); err != nil {
return err
}
if err := deleteMemoSharesTx(ctx, tx, userID, memoIDs); err != nil {
return err
}
if err := deleteInboxesByIDsTx(ctx, tx, targets.inboxIDs); err != nil {
return err
}
if err := deleteUserIdentitiesTx(ctx, tx, userID); err != nil {
return err
}
if err := deleteUserSettingsTx(ctx, tx, userID); err != nil {
return err
}
if err := deleteMemoRelationsTx(ctx, tx, memoIDs); err != nil {
return err
}
if err := deleteMemosTx(ctx, tx, memoIDs); err != nil {
return err
}
if err := deleteUserRowTx(ctx, tx, userID); err != nil {
return err
}
return nil
}
func listDeleteUserMemoTree(ctx context.Context, tx *sql.Tx, userID int32) ([]deleteUserMemoRef, error) {
return listDeleteUserMemoTreeRecursive(ctx, tx, userID)
}
func listDeleteUserMemoTreeRecursive(ctx context.Context, tx *sql.Tx, userID int32) ([]deleteUserMemoRef, error) {
rows, err := tx.QueryContext(ctx, `
WITH RECURSIVE memo_tree(id, uid) AS (
SELECT id, uid
FROM memo
WHERE creator_id = `+deleteUserPlaceholder(1)+`
UNION
SELECT child.id, child.uid
FROM memo child
JOIN memo_relation rel ON rel.memo_id = child.id AND rel.type = 'COMMENT'
JOIN memo_tree parent ON rel.related_memo_id = parent.id
)
SELECT id, uid
FROM memo_tree
`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
memos := make([]deleteUserMemoRef, 0)
for rows.Next() {
var memo deleteUserMemoRef
if err := rows.Scan(&memo.ID, &memo.UID); err != nil {
return nil, err
}
memos = append(memos, memo)
}
if err := rows.Err(); err != nil {
return nil, err
}
return memos, nil
}
func listDeleteUserAttachments(ctx context.Context, tx *sql.Tx, userID int32, memoIDs []int32) ([]*store.Attachment, error) {
attachments := make([]*store.Attachment, 0)
seen := make(map[int32]struct{})
if err := appendDeleteUserAttachments(ctx, tx, `
SELECT
id,
uid,
creator_id,
memo_id,
storage_type,
reference,
payload
FROM attachment
WHERE creator_id = `+deleteUserPlaceholder(1), []any{userID}, seen, &attachments); err != nil {
return nil, err
}
for _, batch := range deleteUserBatches(memoIDs, deleteUserBatchSize) {
clause, args := deleteUserInClause(1, batch)
if err := appendDeleteUserAttachments(ctx, tx, `
SELECT
id,
uid,
creator_id,
memo_id,
storage_type,
reference,
payload
FROM attachment
WHERE memo_id IN `+clause, args, seen, &attachments); err != nil {
return nil, err
}
}
return attachments, nil
}
func appendDeleteUserAttachments(ctx context.Context, tx *sql.Tx, query string, args []any, seen map[int32]struct{}, attachments *[]*store.Attachment) error {
rows, err := tx.QueryContext(ctx, query, args...)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
attachment := &store.Attachment{}
var memoID sql.NullInt32
var storageType string
var payloadBytes []byte
if err := rows.Scan(&attachment.ID, &attachment.UID, &attachment.CreatorID, &memoID, &storageType, &attachment.Reference, &payloadBytes); err != nil {
return err
}
if _, exists := seen[attachment.ID]; exists {
continue
}
seen[attachment.ID] = struct{}{}
if memoID.Valid {
attachment.MemoID = &memoID.Int32
}
attachment.StorageType = storepb.AttachmentStorageType(storepb.AttachmentStorageType_value[storageType])
payload := &storepb.AttachmentPayload{}
if len(payloadBytes) > 0 {
if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil {
return err
}
}
attachment.Payload = payload
*attachments = append(*attachments, attachment)
}
return rows.Err()
}
func listDeleteUserSettingKeys(ctx context.Context, tx *sql.Tx, userID int32) ([]storepb.UserSetting_Key, error) {
rows, err := tx.QueryContext(ctx, deleteUserSettingKeysQuery(), userID)
if err != nil {
return nil, err
}
defer rows.Close()
keys := make([]storepb.UserSetting_Key, 0)
for rows.Next() {
var keyString string
if err := rows.Scan(&keyString); err != nil {
return nil, err
}
key := storepb.UserSetting_Key(storepb.UserSetting_Key_value[keyString])
keys = append(keys, key)
}
if err := rows.Err(); err != nil {
return nil, err
}
return keys, nil
}
func deleteUserSettingKeysQuery() string {
return `SELECT key FROM user_setting WHERE user_id = ` + deleteUserPlaceholder(1)
}
func listDeleteUserInboxIDs(ctx context.Context, tx *sql.Tx, userID int32, memoIDSet map[int32]struct{}) ([]int32, error) {
directIDs, err := listDeleteUserDirectInboxIDs(ctx, tx, userID)
if err != nil {
return nil, err
}
inboxIDs := append([]int32{}, directIDs...)
if len(memoIDSet) == 0 {
return inboxIDs, nil
}
memoIDs, err := listDeleteUserMemoReferencedInboxIDs(ctx, tx, userID, memoIDSet)
if err != nil {
return nil, err
}
return append(inboxIDs, memoIDs...), nil
}
func listDeleteUserDirectInboxIDs(ctx context.Context, tx *sql.Tx, userID int32) ([]int32, error) {
rows, err := tx.QueryContext(ctx, `
SELECT id
FROM inbox
WHERE sender_id = `+deleteUserPlaceholder(1)+`
OR receiver_id = `+deleteUserPlaceholder(2), userID, userID)
if err != nil {
return nil, err
}
defer rows.Close()
inboxIDs := make([]int32, 0)
for rows.Next() {
var inboxID int32
if err := rows.Scan(&inboxID); err != nil {
return nil, err
}
inboxIDs = append(inboxIDs, inboxID)
}
if err := rows.Err(); err != nil {
return nil, err
}
return inboxIDs, nil
}
func listDeleteUserMemoReferencedInboxIDs(ctx context.Context, tx *sql.Tx, userID int32, memoIDSet map[int32]struct{}) ([]int32, error) {
rows, err := tx.QueryContext(ctx, `
SELECT id, message
FROM inbox
WHERE sender_id <> `+deleteUserPlaceholder(1)+`
AND receiver_id <> `+deleteUserPlaceholder(2), userID, userID)
if err != nil {
return nil, err
}
defer rows.Close()
inboxIDs := make([]int32, 0)
for rows.Next() {
var (
inboxID int32
messageRaw []byte
)
if err := rows.Scan(&inboxID, &messageRaw); err != nil {
return nil, err
}
if len(messageRaw) == 0 {
continue
}
message := &storepb.InboxMessage{}
if err := protojsonUnmarshaler.Unmarshal(messageRaw, message); err != nil {
return nil, err
}
if inboxMessageTouchesMemoSet(message, memoIDSet) {
inboxIDs = append(inboxIDs, inboxID)
}
}
if err := rows.Err(); err != nil {
return nil, err
}
return inboxIDs, nil
}
func inboxMessageTouchesMemoSet(message *storepb.InboxMessage, memoIDSet map[int32]struct{}) bool {
if message == nil {
return false
}
switch message.Type {
case storepb.InboxMessage_MEMO_COMMENT:
payload := message.GetMemoComment()
if payload == nil {
return false
}
return memoIDInSet(payload.MemoId, memoIDSet) || memoIDInSet(payload.RelatedMemoId, memoIDSet)
case storepb.InboxMessage_MEMO_MENTION:
payload := message.GetMemoMention()
if payload == nil {
return false
}
return memoIDInSet(payload.MemoId, memoIDSet) || memoIDInSet(payload.RelatedMemoId, memoIDSet)
default:
return false
}
}
func memoIDInSet(id int32, memoIDSet map[int32]struct{}) bool {
if id == 0 {
return false
}
_, exists := memoIDSet[id]
return exists
}
func deleteReactionsByContentIDsTx(ctx context.Context, tx *sql.Tx, contentIDs []string) error {
for _, batch := range deleteUserBatches(contentIDs, deleteUserBatchSize) {
clause, args := deleteUserInClause(1, batch)
if _, err := tx.ExecContext(ctx, `DELETE FROM reaction WHERE content_id IN `+clause, args...); err != nil {
return err
}
}
return nil
}
func deleteAttachmentsByIDsTx(ctx context.Context, tx *sql.Tx, attachmentIDs []int32) error {
for _, batch := range deleteUserBatches(attachmentIDs, deleteUserBatchSize) {
clause, args := deleteUserInClause(1, batch)
if _, err := tx.ExecContext(ctx, `DELETE FROM attachment WHERE id IN `+clause, args...); err != nil {
return err
}
}
return nil
}
func deleteReactionsByCreatorTx(ctx context.Context, tx *sql.Tx, userID int32) error {
_, err := tx.ExecContext(ctx, `DELETE FROM reaction WHERE creator_id = `+deleteUserPlaceholder(1), userID)
return err
}
func deleteMemoSharesTx(ctx context.Context, tx *sql.Tx, userID int32, memoIDs []int32) error {
if _, err := tx.ExecContext(ctx, `DELETE FROM memo_share WHERE creator_id = `+deleteUserPlaceholder(1), userID); err != nil {
return err
}
for _, batch := range deleteUserBatches(memoIDs, deleteUserBatchSize) {
clause, args := deleteUserInClause(1, batch)
if _, err := tx.ExecContext(ctx, `DELETE FROM memo_share WHERE memo_id IN `+clause, args...); err != nil {
return err
}
}
return nil
}
func deleteInboxesByIDsTx(ctx context.Context, tx *sql.Tx, inboxIDs []int32) error {
for _, batch := range deleteUserBatches(inboxIDs, deleteUserBatchSize) {
clause, args := deleteUserInClause(1, batch)
if _, err := tx.ExecContext(ctx, `DELETE FROM inbox WHERE id IN `+clause, args...); err != nil {
return err
}
}
return nil
}
func deleteUserIdentitiesTx(ctx context.Context, tx *sql.Tx, userID int32) error {
_, err := tx.ExecContext(ctx, `DELETE FROM user_identity WHERE user_id = `+deleteUserPlaceholder(1), userID)
return err
}
func deleteUserSettingsTx(ctx context.Context, tx *sql.Tx, userID int32) error {
_, err := tx.ExecContext(ctx, `DELETE FROM user_setting WHERE user_id = `+deleteUserPlaceholder(1), userID)
return err
}
func deleteMemoRelationsTx(ctx context.Context, tx *sql.Tx, memoIDs []int32) error {
for _, batch := range deleteUserBatches(memoIDs, deleteUserBatchSize) {
memoClause, args := deleteUserInClause(1, batch)
relatedClause, relatedArgs := deleteUserInClause(len(args)+1, batch)
query := `DELETE FROM memo_relation WHERE memo_id IN ` + memoClause + ` OR related_memo_id IN ` + relatedClause
args = append(args, relatedArgs...)
if _, err := tx.ExecContext(ctx, query, args...); err != nil {
return err
}
}
return nil
}
func deleteMemosTx(ctx context.Context, tx *sql.Tx, memoIDs []int32) error {
for _, batch := range deleteUserBatches(memoIDs, deleteUserBatchSize) {
clause, args := deleteUserInClause(1, batch)
if _, err := tx.ExecContext(ctx, `DELETE FROM memo WHERE id IN `+clause, args...); err != nil {
return err
}
}
return nil
}
func deleteUserRowTx(ctx context.Context, tx *sql.Tx, userID int32) error {
_, err := tx.ExecContext(ctx, `DELETE FROM user WHERE id = `+deleteUserPlaceholder(1), userID)
return err
}
func deleteUserPlaceholder(_ int) string {
return "?"
}
func deleteUserInClause[T any](start int, values []T) (string, []any) {
placeholders := make([]string, 0, len(values))
args := make([]any, 0, len(values))
for i, value := range values {
placeholders = append(placeholders, deleteUserPlaceholder(start+i))
args = append(args, value)
}
return "(" + strings.Join(placeholders, ", ") + ")", args
}
func deleteUserBatches[T any](values []T, size int) [][]T {
if len(values) == 0 {
return nil
}
if size <= 0 {
size = len(values)
}
batches := make([][]T, 0, (len(values)+size-1)/size)
for start := 0; start < len(values); start += size {
end := min(start+size, len(values))
batches = append(batches, values[start:end])
}
return batches
}
func memoIDsFromRefs(memos []deleteUserMemoRef) []int32 {
ids := make([]int32, 0, len(memos))
for _, memo := range memos {
ids = append(ids, memo.ID)
}
return ids
}
func memoIDSetFromRefs(memos []deleteUserMemoRef) map[int32]struct{} {
idSet := make(map[int32]struct{}, len(memos))
for _, memo := range memos {
idSet[memo.ID] = struct{}{}
}
return idSet
}
func memoContentIDsFromRefs(memos []deleteUserMemoRef) []string {
contentIDs := make([]string, 0, len(memos))
for _, memo := range memos {
contentIDs = append(contentIDs, "memos/"+memo.UID)
}
return contentIDs
}
func attachmentIDsFromList(attachments []*store.Attachment) []int32 {
ids := make([]int32, 0, len(attachments))
for _, attachment := range attachments {
if attachment == nil {
continue
}
ids = append(ids, attachment.ID)
}
return ids
}
+95
View File
@@ -0,0 +1,95 @@
package sqlite
import (
"context"
"strings"
"github.com/usememos/memos/store"
)
func (d *DB) CreateUserIdentity(ctx context.Context, create *store.UserIdentity) (*store.UserIdentity, error) {
stmt := "INSERT INTO `user_identity` (`user_id`, `provider`, `extern_uid`) VALUES (?, ?, ?) RETURNING `id`, `created_ts`, `updated_ts`"
if err := d.db.QueryRowContext(ctx, stmt, create.UserID, create.Provider, create.ExternUID).Scan(
&create.ID,
&create.CreatedTs,
&create.UpdatedTs,
); err != nil {
return nil, err
}
return create, nil
}
func (d *DB) ListUserIdentities(ctx context.Context, find *store.FindUserIdentity) ([]*store.UserIdentity, error) {
where, args := []string{"1 = 1"}, []any{}
if find.ID != nil {
where, args = append(where, "`id` = ?"), append(args, *find.ID)
}
if find.UserID != nil {
where, args = append(where, "`user_id` = ?"), append(args, *find.UserID)
}
if find.Provider != nil {
where, args = append(where, "`provider` = ?"), append(args, *find.Provider)
}
if find.ExternUID != nil {
where, args = append(where, "`extern_uid` = ?"), append(args, *find.ExternUID)
}
rows, err := d.db.QueryContext(ctx, `
SELECT
id,
user_id,
provider,
extern_uid,
created_ts,
updated_ts
FROM user_identity
WHERE `+strings.Join(where, " AND ")+`
ORDER BY id ASC`,
args...,
)
if err != nil {
return nil, err
}
defer rows.Close()
list := []*store.UserIdentity{}
for rows.Next() {
ui := &store.UserIdentity{}
if err := rows.Scan(
&ui.ID,
&ui.UserID,
&ui.Provider,
&ui.ExternUID,
&ui.CreatedTs,
&ui.UpdatedTs,
); err != nil {
return nil, err
}
list = append(list, ui)
}
if err := rows.Err(); err != nil {
return nil, err
}
return list, nil
}
func (d *DB) DeleteUserIdentities(ctx context.Context, delete *store.DeleteUserIdentity) error {
where, args := []string{"1 = 1"}, []any{}
if delete.ID != nil {
where, args = append(where, "`id` = ?"), append(args, *delete.ID)
}
if delete.UserID != nil {
where, args = append(where, "`user_id` = ?"), append(args, *delete.UserID)
}
if delete.Provider != nil {
where, args = append(where, "`provider` = ?"), append(args, *delete.Provider)
}
if _, err := d.db.ExecContext(ctx, "DELETE FROM `user_identity` WHERE "+strings.Join(where, " AND "), args...); err != nil {
return err
}
return nil
}
+125
View File
@@ -0,0 +1,125 @@
package sqlite
import (
"context"
"strings"
"github.com/pkg/errors"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
)
func (d *DB) UpsertUserSetting(ctx context.Context, upsert *store.UserSetting) (*store.UserSetting, error) {
stmt := `
INSERT INTO user_setting (
user_id, key, value
)
VALUES (?, ?, ?)
ON CONFLICT(user_id, key) DO UPDATE
SET value = EXCLUDED.value
`
if _, err := d.db.ExecContext(ctx, stmt, upsert.UserID, upsert.Key.String(), upsert.Value); err != nil {
return nil, err
}
return upsert, nil
}
func (d *DB) ListUserSettings(ctx context.Context, find *store.FindUserSetting) ([]*store.UserSetting, error) {
where, args := []string{"1 = 1"}, []any{}
if v := find.Key; v != storepb.UserSetting_KEY_UNSPECIFIED {
where, args = append(where, "key = ?"), append(args, v.String())
}
if v := find.UserID; v != nil {
where, args = append(where, "user_id = ?"), append(args, *find.UserID)
}
query := `
SELECT
user_id,
key,
value
FROM user_setting
WHERE ` + strings.Join(where, " AND ")
rows, err := d.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
userSettingList := make([]*store.UserSetting, 0)
for rows.Next() {
userSetting := &store.UserSetting{}
var keyString string
if err := rows.Scan(
&userSetting.UserID,
&keyString,
&userSetting.Value,
); err != nil {
return nil, err
}
userSetting.Key = storepb.UserSetting_Key(storepb.UserSetting_Key_value[keyString])
userSettingList = append(userSettingList, userSetting)
}
if err := rows.Err(); err != nil {
return nil, err
}
return userSettingList, nil
}
func (d *DB) DeleteUserSettings(ctx context.Context, delete *store.DeleteUserSetting) error {
where, args := []string{"1 = 1"}, []any{}
if v := delete.Key; v != storepb.UserSetting_KEY_UNSPECIFIED {
where, args = append(where, "key = ?"), append(args, v.String())
}
if v := delete.UserID; v != nil {
where, args = append(where, "user_id = ?"), append(args, *v)
}
if _, err := d.db.ExecContext(ctx, "DELETE FROM user_setting WHERE "+strings.Join(where, " AND "), args...); err != nil {
return err
}
return nil
}
func (d *DB) GetUserByPATHash(ctx context.Context, tokenHash string) (*store.PATQueryResult, error) {
query := `
SELECT
user_setting.user_id,
user_setting.value
FROM user_setting
WHERE user_setting.key = 'PERSONAL_ACCESS_TOKENS'
AND EXISTS (
SELECT 1
FROM json_each(json_extract(user_setting.value, '$.tokens')) AS token
WHERE json_extract(token.value, '$.tokenHash') = ?
)
`
var userID int32
var tokensJSON string
err := d.db.QueryRowContext(ctx, query, tokenHash).Scan(&userID, &tokensJSON)
if err != nil {
return nil, err
}
patsUserSetting := &storepb.PersonalAccessTokensUserSetting{}
if err := protojsonUnmarshaler.Unmarshal([]byte(tokensJSON), patsUserSetting); err != nil {
return nil, err
}
for _, pat := range patsUserSetting.Tokens {
if pat.TokenHash == tokenHash {
return &store.PATQueryResult{
UserID: userID,
PAT: pat,
}, nil
}
}
return nil, errors.New("PAT not found")
}