Refactor database accessUnverified
d81cbd6 parent: cd41abf modified
.env.example +2 -2 | @@ -2,8 +2,8 @@ GLEAN_ADDR=:8080 | ||
| 2 | 2 | GLEAN_DB=glean.db |
| 3 | 3 | GLEAN_JETSTREAM=wss://jetstream.glean.at |
| 4 | 4 | GLEAN_PLC_URL=https://didplc.glean.at |
| 5 | -GLEAN_SYNC_INTERVAL=30m | |
| 6 | -GLEAN_CLUSTER_INTERVAL=1h | |
| 5 | +GLEAN_SYNC_INTERVAL=10m | |
| 6 | +GLEAN_CLUSTER_INTERVAL=15m | |
| 7 | 7 | GLEAN_FETCH_INTERVAL=5m |
| 8 | 8 | GLEAN_COLLECTION_DIR_URL=https://lightrail.microcosm.blue/xrpc/com.atproto.sync.listReposByCollection?collection=at.glean.subscription |
| 9 | 9 | # Leave empty for localhost OAuth (development) |
| @@ -2,8 +2,8 @@ GLEAN_ADDR=:8080 | |||
| 2 | GLEAN_DB=glean.db | 2 | GLEAN_DB=glean.db |
| 3 | GLEAN_JETSTREAM=wss://jetstream.glean.at | 3 | GLEAN_JETSTREAM=wss://jetstream.glean.at |
| 4 | GLEAN_PLC_URL=https://didplc.glean.at | 4 | GLEAN_PLC_URL=https://didplc.glean.at |
| 5 | -GLEAN_SYNC_INTERVAL=30m | 5 | +GLEAN_SYNC_INTERVAL=10m |
| 6 | -GLEAN_CLUSTER_INTERVAL=1h | 6 | +GLEAN_CLUSTER_INTERVAL=15m |
| 7 | GLEAN_FETCH_INTERVAL=5m | 7 | GLEAN_FETCH_INTERVAL=5m |
| 8 | GLEAN_COLLECTION_DIR_URL=https://lightrail.microcosm.blue/xrpc/com.atproto.sync.listReposByCollection?collection=at.glean.subscription | 8 | GLEAN_COLLECTION_DIR_URL=https://lightrail.microcosm.blue/xrpc/com.atproto.sync.listReposByCollection?collection=at.glean.subscription |
| 9 | # Leave empty for localhost OAuth (development) | 9 | # Leave empty for localhost OAuth (development) |
modified
internal/atproto/stream_handler.go +3 -3 | @@ -23,12 +23,12 @@ func isSentinel(err error) bool { | ||
| 23 | 23 | } |
| 24 | 24 | |
| 25 | 25 | type StreamDBHandler struct { |
| 26 | - articles *db.DB | |
| 27 | - users *db.DB | |
| 26 | + articles *db.ArticleStore | |
| 27 | + users *db.UserStore | |
| 28 | 28 | logger *slog.Logger |
| 29 | 29 | } |
| 30 | 30 | |
| 31 | -func NewStreamDBHandler(articles, users *db.DB, logger *slog.Logger) *StreamDBHandler { | |
| 31 | +func NewStreamDBHandler(articles *db.ArticleStore, users *db.UserStore, logger *slog.Logger) *StreamDBHandler { | |
| 32 | 32 | return &StreamDBHandler{articles: articles, users: users, logger: logger} |
| 33 | 33 | } |
| 34 | 34 | |
| @@ -23,12 +23,12 @@ func isSentinel(err error) bool { | |||
| 23 | } | 23 | } |
| 24 | 24 | ||
| 25 | type StreamDBHandler struct { | 25 | type StreamDBHandler struct { |
| 26 | - articles *db.DB | 26 | + articles *db.ArticleStore |
| 27 | - users *db.DB | 27 | + users *db.UserStore |
| 28 | logger *slog.Logger | 28 | logger *slog.Logger |
| 29 | } | 29 | } |
| 30 | 30 | ||
| 31 | -func NewStreamDBHandler(articles, users *db.DB, logger *slog.Logger) *StreamDBHandler { | 31 | +func NewStreamDBHandler(articles *db.ArticleStore, users *db.UserStore, logger *slog.Logger) *StreamDBHandler { |
| 32 | return &StreamDBHandler{articles: articles, users: users, logger: logger} | 32 | return &StreamDBHandler{articles: articles, users: users, logger: logger} |
| 33 | } | 33 | } |
| 34 | 34 | ||
modified
internal/atproto/sync.go +3 -3 | @@ -20,13 +20,13 @@ import ( | ||
| 20 | 20 | ) |
| 21 | 21 | |
| 22 | 22 | type Sync struct { |
| 23 | - articles *db.DB | |
| 24 | - users *db.DB | |
| 23 | + articles *db.ArticleStore | |
| 24 | + users *db.UserStore | |
| 25 | 25 | client *Client |
| 26 | 26 | logger *slog.Logger |
| 27 | 27 | } |
| 28 | 28 | |
| 29 | -func NewSync(articles, users *db.DB, client *Client, logger *slog.Logger) *Sync { | |
| 29 | +func NewSync(articles *db.ArticleStore, users *db.UserStore, client *Client, logger *slog.Logger) *Sync { | |
| 30 | 30 | return &Sync{articles: articles, users: users, client: client, logger: logger} |
| 31 | 31 | } |
| 32 | 32 | |
| @@ -20,13 +20,13 @@ import ( | |||
| 20 | ) | 20 | ) |
| 21 | 21 | ||
| 22 | type Sync struct { | 22 | type Sync struct { |
| 23 | - articles *db.DB | 23 | + articles *db.ArticleStore |
| 24 | - users *db.DB | 24 | + users *db.UserStore |
| 25 | client *Client | 25 | client *Client |
| 26 | logger *slog.Logger | 26 | logger *slog.Logger |
| 27 | } | 27 | } |
| 28 | 28 | ||
| 29 | -func NewSync(articles, users *db.DB, client *Client, logger *slog.Logger) *Sync { | 29 | +func NewSync(articles *db.ArticleStore, users *db.UserStore, client *Client, logger *slog.Logger) *Sync { |
| 30 | return &Sync{articles: articles, users: users, client: client, logger: logger} | 30 | return &Sync{articles: articles, users: users, client: client, logger: logger} |
| 31 | } | 31 | } |
| 32 | 32 | ||
modified
internal/cluster/jaccard.go +2 -0 | @@ -24,6 +24,8 @@ func DefaultConfig() Config { | ||
| 24 | 24 | } |
| 25 | 25 | } |
| 26 | 26 | |
| 27 | +// Engine uses *sql.DB directly because it performs cross-schema transactions | |
| 28 | +// across main, articles, and recs. Typed stores would add overhead without benefit here. | |
| 27 | 29 | type Engine struct { |
| 28 | 30 | db *sql.DB |
| 29 | 31 | logger *slog.Logger |
| @@ -24,6 +24,8 @@ func DefaultConfig() Config { | |||
| 24 | } | 24 | } |
| 25 | } | 25 | } |
| 26 | 26 | ||
| 27 | +// Engine uses *sql.DB directly because it performs cross-schema transactions | ||
| 28 | +// across main, articles, and recs. Typed stores would add overhead without benefit here. | ||
| 27 | type Engine struct { | 29 | type Engine struct { |
| 28 | db *sql.DB | 30 | db *sql.DB |
| 29 | logger *slog.Logger | 31 | logger *slog.Logger |
modified
internal/cluster/jaccard_test.go +32 -32 | @@ -46,7 +46,7 @@ func seedClusterData(t *testing.T, ctx context.Context, dbs *db.Databases) { | ||
| 46 | 46 | {"did:test:carol", "carol"}, |
| 47 | 47 | } |
| 48 | 48 | for _, u := range users { |
| 49 | - _, err := dbs.Users.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, u.did, u.handle) | |
| 49 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, u.did, u.handle) | |
| 50 | 50 | assert.NilError(t, err) |
| 51 | 51 | } |
| 52 | 52 | |
| @@ -58,7 +58,7 @@ func seedClusterData(t *testing.T, ctx context.Context, dbs *db.Databases) { | ||
| 58 | 58 | {"https://e.com/feed", "Feed E"}, |
| 59 | 59 | } |
| 60 | 60 | for _, f := range feeds { |
| 61 | - _, err := dbs.Articles.ExecContext(ctx, `INSERT INTO feeds (feed_url, title, site_url, description, feed_type, subscriber_count) VALUES (?, ?, ?, '', 'rss', 2)`, f.url, f.title, f.url) | |
| 61 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO articles.feeds (feed_url, title, site_url, description, feed_type, subscriber_count) VALUES (?, ?, ?, '', 'rss', 2)`, f.url, f.title, f.url) | |
| 62 | 62 | assert.NilError(t, err) |
| 63 | 63 | } |
| 64 | 64 | |
| @@ -74,7 +74,7 @@ func seedClusterData(t *testing.T, ctx context.Context, dbs *db.Databases) { | ||
| 74 | 74 | {"did:test:carol", "https://c.com/feed"}, |
| 75 | 75 | } |
| 76 | 76 | for _, s := range subs { |
| 77 | - _, err := dbs.Articles.ExecContext(ctx, `INSERT INTO subscriptions (user_did, feed_url) VALUES (?, ?)`, s.user, s.feed) | |
| 77 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO articles.subscriptions (user_did, feed_url) VALUES (?, ?)`, s.user, s.feed) | |
| 78 | 78 | assert.NilError(t, err) |
| 79 | 79 | } |
| 80 | 80 | } |
| @@ -86,13 +86,13 @@ func seedFollowData(t *testing.T, ctx context.Context, dbs *db.Databases) { | ||
| 86 | 86 | {"did:test:bob", "did:test:carol"}, |
| 87 | 87 | } |
| 88 | 88 | for _, f := range follows { |
| 89 | - _, err := dbs.Users.ExecContext(ctx, `INSERT OR IGNORE INTO follows (user_did, target_did) VALUES (?, ?)`, f.user, f.target) | |
| 89 | + _, err := dbs.DB().ExecContext(ctx, `INSERT OR IGNORE INTO follows (user_did, target_did) VALUES (?, ?)`, f.user, f.target) | |
| 90 | 90 | assert.NilError(t, err) |
| 91 | 91 | } |
| 92 | 92 | } |
| 93 | 93 | |
| 94 | 94 | func newTestEngine(dbs *db.Databases) *Engine { |
| 95 | - return NewEngine(dbs.Users.DB, slog.Default()) | |
| 95 | + return NewEngine(dbs.DB(), slog.Default()) | |
| 96 | 96 | } |
| 97 | 97 | |
| 98 | 98 | func TestComputeFeedSimilarity(t *testing.T) { |
| @@ -105,7 +105,7 @@ func TestComputeFeedSimilarity(t *testing.T) { | ||
| 105 | 105 | assert.NilError(t, err) |
| 106 | 106 | |
| 107 | 107 | var count int |
| 108 | - err = dbs.Users.QueryRowContext(ctx, `SELECT COUNT(*) FROM recs.feed_similarity`).Scan(&count) | |
| 108 | + err = dbs.DB().QueryRowContext(ctx, `SELECT COUNT(*) FROM recs.feed_similarity`).Scan(&count) | |
| 109 | 109 | assert.NilError(t, err) |
| 110 | 110 | assert.Assert(t, count > 0, "expected feed similarity pairs") |
| 111 | 111 | } |
| @@ -120,7 +120,7 @@ func TestComputeUserSimilarity(t *testing.T) { | ||
| 120 | 120 | assert.NilError(t, err) |
| 121 | 121 | |
| 122 | 122 | var count int |
| 123 | - err = dbs.Users.QueryRowContext(ctx, `SELECT COUNT(*) FROM recs.user_similarity`).Scan(&count) | |
| 123 | + err = dbs.DB().QueryRowContext(ctx, `SELECT COUNT(*) FROM recs.user_similarity`).Scan(&count) | |
| 124 | 124 | assert.NilError(t, err) |
| 125 | 125 | assert.Assert(t, count > 0, "expected user similarity pairs") |
| 126 | 126 | } |
| @@ -222,14 +222,14 @@ func TestRecordImpressions(t *testing.T) { | ||
| 222 | 222 | assert.NilError(t, engine.RecordImpressions(ctx, "did:test:alice", impressions)) |
| 223 | 223 | |
| 224 | 224 | var count int |
| 225 | - assert.NilError(t, dbs.Users.QueryRowContext(ctx, | |
| 225 | + assert.NilError(t, dbs.DB().QueryRowContext(ctx, | |
| 226 | 226 | `SELECT COUNT(*) FROM recs.recommendation_impressions WHERE user_did = 'did:test:alice'`).Scan(&count)) |
| 227 | 227 | assert.Equal(t, count, 2) |
| 228 | 228 | |
| 229 | 229 | assert.NilError(t, engine.RecordImpressions(ctx, "did:test:alice", impressions)) |
| 230 | 230 | |
| 231 | 231 | var shownCount int |
| 232 | - assert.NilError(t, dbs.Users.QueryRowContext(ctx, | |
| 232 | + assert.NilError(t, dbs.DB().QueryRowContext(ctx, | |
| 233 | 233 | `SELECT shown_count FROM recs.recommendation_impressions WHERE user_did = 'did:test:alice' AND target_id = 'https://a.com/feed'`).Scan(&shownCount)) |
| 234 | 234 | assert.Equal(t, shownCount, 2, "shown_count should increment on repeated impression") |
| 235 | 235 | } |
| @@ -247,7 +247,7 @@ func TestMarkImpressionActed(t *testing.T) { | ||
| 247 | 247 | assert.NilError(t, engine.MarkImpressionActed(ctx, "did:test:alice", "feed", "https://a.com/feed")) |
| 248 | 248 | |
| 249 | 249 | var acted bool |
| 250 | - assert.NilError(t, dbs.Users.QueryRowContext(ctx, | |
| 250 | + assert.NilError(t, dbs.DB().QueryRowContext(ctx, | |
| 251 | 251 | `SELECT acted FROM recs.recommendation_impressions WHERE user_did = 'did:test:alice' AND target_id = 'https://a.com/feed'`).Scan(&acted)) |
| 252 | 252 | assert.Assert(t, acted, "impression should be marked as acted") |
| 253 | 253 | } |
| @@ -262,15 +262,15 @@ func TestComputeFollowDistances(t *testing.T) { | ||
| 262 | 262 | assert.NilError(t, engine.ComputeFollowDistances(ctx)) |
| 263 | 263 | |
| 264 | 264 | var d1, d2 int |
| 265 | - assert.NilError(t, dbs.Users.QueryRowContext(ctx, | |
| 265 | + assert.NilError(t, dbs.DB().QueryRowContext(ctx, | |
| 266 | 266 | `SELECT COUNT(*) FROM recs.follow_distances WHERE distance = 1`).Scan(&d1)) |
| 267 | - assert.NilError(t, dbs.Users.QueryRowContext(ctx, | |
| 267 | + assert.NilError(t, dbs.DB().QueryRowContext(ctx, | |
| 268 | 268 | `SELECT COUNT(*) FROM recs.follow_distances WHERE distance = 2`).Scan(&d2)) |
| 269 | 269 | assert.Assert(t, d1 >= 2, "expected at least 2 direct follow distances") |
| 270 | 270 | assert.Assert(t, d2 >= 1, "expected at least 1 two-hop distance (alice -> bob -> carol)") |
| 271 | 271 | |
| 272 | 272 | var dist int |
| 273 | - assert.NilError(t, dbs.Users.QueryRowContext(ctx, | |
| 273 | + assert.NilError(t, dbs.DB().QueryRowContext(ctx, | |
| 274 | 274 | `SELECT distance FROM recs.follow_distances WHERE user_a = 'did:test:alice' AND user_b = 'did:test:carol'`).Scan(&dist)) |
| 275 | 275 | assert.Equal(t, dist, 2, "alice should be 2 hops from carol") |
| 276 | 276 | } |
| @@ -282,7 +282,7 @@ func TestAutoDismissStale(t *testing.T) { | ||
| 282 | 282 | |
| 283 | 283 | engine := newTestEngine(dbs) |
| 284 | 284 | |
| 285 | - _, err := dbs.Users.ExecContext(ctx, ` | |
| 285 | + _, err := dbs.DB().ExecContext(ctx, ` | |
| 286 | 286 | INSERT INTO recs.recommendation_impressions (user_did, target_type, target_id, first_shown_at, last_shown_at, shown_count, acted) |
| 287 | 287 | VALUES ('did:test:alice', 'feed', 'https://stale.com/feed', datetime('now', '-31 days'), datetime('now'), 20, 0) |
| 288 | 288 | `) |
| @@ -302,7 +302,7 @@ func TestAutoDismissStale_DoesNotDismissRecent(t *testing.T) { | ||
| 302 | 302 | |
| 303 | 303 | engine := newTestEngine(dbs) |
| 304 | 304 | |
| 305 | - _, err := dbs.Users.ExecContext(ctx, ` | |
| 305 | + _, err := dbs.DB().ExecContext(ctx, ` | |
| 306 | 306 | INSERT INTO recs.recommendation_impressions (user_did, target_type, target_id, first_shown_at, last_shown_at, shown_count, acted) |
| 307 | 307 | VALUES ('did:test:alice', 'feed', 'https://recent.com/feed', datetime('now'), datetime('now'), 5, 0) |
| 308 | 308 | `) |
| @@ -322,7 +322,7 @@ func TestAutoDismissStale_DoesNotDismissActed(t *testing.T) { | ||
| 322 | 322 | |
| 323 | 323 | engine := newTestEngine(dbs) |
| 324 | 324 | |
| 325 | - _, err := dbs.Users.ExecContext(ctx, ` | |
| 325 | + _, err := dbs.DB().ExecContext(ctx, ` | |
| 326 | 326 | INSERT INTO recs.recommendation_impressions (user_did, target_type, target_id, first_shown_at, last_shown_at, shown_count, acted) |
| 327 | 327 | VALUES ('did:test:alice', 'feed', 'https://acted.com/feed', datetime('now', '-31 days'), datetime('now'), 20, 1) |
| 328 | 328 | `) |
| @@ -397,13 +397,13 @@ func TestSignalWeights_RewardPenalize(t *testing.T) { | ||
| 397 | 397 | |
| 398 | 398 | engine := newTestEngine(dbs) |
| 399 | 399 | |
| 400 | - _, err := dbs.Users.ExecContext(ctx, ` | |
| 400 | + _, err := dbs.DB().ExecContext(ctx, ` | |
| 401 | 401 | INSERT INTO recs.recommendation_impressions (user_did, target_type, target_id, first_shown_at, last_shown_at, shown_count, acted) |
| 402 | 402 | VALUES ('did:test:alice', 'feed', 'https://a.com/feed', datetime('now'), datetime('now'), 1, 1) |
| 403 | 403 | `) |
| 404 | 404 | assert.NilError(t, err) |
| 405 | 405 | for i := range minActionsTune { |
| 406 | - _, err = dbs.Users.ExecContext(ctx, ` | |
| 406 | + _, err = dbs.DB().ExecContext(ctx, ` | |
| 407 | 407 | INSERT INTO recs.recommendation_impressions (user_did, target_type, target_id, first_shown_at, last_shown_at, shown_count, acted) |
| 408 | 408 | VALUES ('did:test:alice', 'feed', ?, datetime('now'), datetime('now'), 1, 1) |
| 409 | 409 | `, fmt.Sprintf("https://%d.com/feed", i)) |
| @@ -425,12 +425,12 @@ func TestColdStartRecommendations(t *testing.T) { | ||
| 425 | 425 | engine := newTestEngine(dbs) |
| 426 | 426 | assert.NilError(t, engine.ComputeFollowDistances(ctx)) |
| 427 | 427 | |
| 428 | - _, err := dbs.Articles.ExecContext(ctx, `UPDATE feeds SET subscriber_count = 2 WHERE feed_url = 'https://a.com/feed'`) | |
| 428 | + _, err := dbs.DB().ExecContext(ctx, `UPDATE articles.feeds SET subscriber_count = 2 WHERE feed_url = 'https://a.com/feed'`) | |
| 429 | 429 | assert.NilError(t, err) |
| 430 | - _, err = dbs.Articles.ExecContext(ctx, `UPDATE feeds SET subscriber_count = 2 WHERE feed_url = 'https://b.com/feed'`) | |
| 430 | + _, err = dbs.DB().ExecContext(ctx, `UPDATE articles.feeds SET subscriber_count = 2 WHERE feed_url = 'https://b.com/feed'`) | |
| 431 | 431 | assert.NilError(t, err) |
| 432 | 432 | |
| 433 | - _, err = dbs.Users.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, "did:test:newuser", "newuser") | |
| 433 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, "did:test:newuser", "newuser") | |
| 434 | 434 | assert.NilError(t, err) |
| 435 | 435 | |
| 436 | 436 | recs, err := engine.ColdStartRecommendations(ctx, "did:test:newuser", 10) |
| @@ -475,7 +475,7 @@ func TestDismissArticle(t *testing.T) { | ||
| 475 | 475 | assert.NilError(t, engine.DismissArticle(ctx, "did:test:alice", "https://a.com/article1", "not_interested")) |
| 476 | 476 | |
| 477 | 477 | var count int |
| 478 | - assert.NilError(t, dbs.Users.QueryRowContext(ctx, | |
| 478 | + assert.NilError(t, dbs.DB().QueryRowContext(ctx, | |
| 479 | 479 | `SELECT COUNT(*) FROM recs.dismissed_recommendations WHERE user_did = 'did:test:alice' AND target_type = 'article'`).Scan(&count)) |
| 480 | 480 | assert.Equal(t, count, 1) |
| 481 | 481 | } |
| @@ -489,7 +489,7 @@ func TestComputeSignalProfiles(t *testing.T) { | ||
| 489 | 489 | assert.NilError(t, engine.ComputeSignalProfiles(ctx)) |
| 490 | 490 | |
| 491 | 491 | var count int |
| 492 | - assert.NilError(t, dbs.Users.QueryRowContext(ctx, `SELECT COUNT(*) FROM recs.user_signal_profiles`).Scan(&count)) | |
| 492 | + assert.NilError(t, dbs.DB().QueryRowContext(ctx, `SELECT COUNT(*) FROM recs.user_signal_profiles`).Scan(&count)) | |
| 493 | 493 | assert.Assert(t, count >= 3, "expected signal profiles for all users") |
| 494 | 494 | } |
| 495 | 495 | |
| @@ -504,7 +504,7 @@ func TestDismissFeed_Idempotent(t *testing.T) { | ||
| 504 | 504 | assert.NilError(t, engine.DismissFeed(ctx, "did:test:alice", "https://a.com/feed", "reason2")) |
| 505 | 505 | |
| 506 | 506 | var count int |
| 507 | - assert.NilError(t, dbs.Users.QueryRowContext(ctx, | |
| 507 | + assert.NilError(t, dbs.DB().QueryRowContext(ctx, | |
| 508 | 508 | `SELECT COUNT(*) FROM recs.dismissed_recommendations WHERE user_did = 'did:test:alice' AND target_type = 'feed'`).Scan(&count)) |
| 509 | 509 | assert.Equal(t, count, 1, "duplicate dismiss should not create extra rows") |
| 510 | 510 | } |
| @@ -513,33 +513,33 @@ func TestDescriptionBasedFeedSimilarity(t *testing.T) { | ||
| 513 | 513 | ctx := context.Background() |
| 514 | 514 | dbs := setupClusterTestDB(t) |
| 515 | 515 | |
| 516 | - _, err := dbs.Users.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, "did:test:alice", "alice") | |
| 516 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, "did:test:alice", "alice") | |
| 517 | 517 | assert.NilError(t, err) |
| 518 | - _, err = dbs.Users.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, "did:test:bob", "bob") | |
| 518 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, "did:test:bob", "bob") | |
| 519 | 519 | assert.NilError(t, err) |
| 520 | 520 | |
| 521 | - _, err = dbs.Articles.ExecContext(ctx, `INSERT INTO feeds (feed_url, title, site_url, description, feed_type) VALUES (?, ?, ?, ?, 'rss')`, | |
| 521 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.feeds (feed_url, title, site_url, description, feed_type) VALUES (?, ?, ?, ?, 'rss')`, | |
| 522 | 522 | "https://go.com/feed", "Go Blog", "https://go.com", "programming language golang software development") |
| 523 | 523 | assert.NilError(t, err) |
| 524 | - _, err = dbs.Articles.ExecContext(ctx, `INSERT INTO feeds (feed_url, title, site_url, description, feed_type) VALUES (?, ?, ?, ?, 'rss')`, | |
| 524 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.feeds (feed_url, title, site_url, description, feed_type) VALUES (?, ?, ?, ?, 'rss')`, | |
| 525 | 525 | "https://rust.com/feed", "Rust Blog", "https://rust.com", "programming language rust software development") |
| 526 | 526 | assert.NilError(t, err) |
| 527 | 527 | |
| 528 | - _, err = dbs.Articles.ExecContext(ctx, `INSERT INTO subscriptions (user_did, feed_url) VALUES (?, ?)`, "did:test:alice", "https://go.com/feed") | |
| 528 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.subscriptions (user_did, feed_url) VALUES (?, ?)`, "did:test:alice", "https://go.com/feed") | |
| 529 | 529 | assert.NilError(t, err) |
| 530 | - _, err = dbs.Articles.ExecContext(ctx, `INSERT INTO subscriptions (user_did, feed_url) VALUES (?, ?)`, "did:test:bob", "https://rust.com/feed") | |
| 530 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.subscriptions (user_did, feed_url) VALUES (?, ?)`, "did:test:bob", "https://rust.com/feed") | |
| 531 | 531 | assert.NilError(t, err) |
| 532 | 532 | |
| 533 | 533 | engine := newTestEngine(dbs) |
| 534 | 534 | assert.NilError(t, engine.ComputeFeedSimilarity(ctx)) |
| 535 | 535 | |
| 536 | 536 | var count int |
| 537 | - assert.NilError(t, dbs.Users.QueryRowContext(ctx, `SELECT COUNT(*) FROM recs.feed_similarity`).Scan(&count)) | |
| 537 | + assert.NilError(t, dbs.DB().QueryRowContext(ctx, `SELECT COUNT(*) FROM recs.feed_similarity`).Scan(&count)) | |
| 538 | 538 | assert.Assert(t, count >= 0, "description-based similarity should produce pairs") |
| 539 | 539 | |
| 540 | 540 | if count > 0 { |
| 541 | 541 | var jaccard float64 |
| 542 | - assert.NilError(t, dbs.Users.QueryRowContext(ctx, | |
| 542 | + assert.NilError(t, dbs.DB().QueryRowContext(ctx, | |
| 543 | 543 | `SELECT jaccard FROM recs.feed_similarity WHERE feed_a = ? AND feed_b = ?`, |
| 544 | 544 | "https://go.com/feed", "https://rust.com/feed").Scan(&jaccard)) |
| 545 | 545 | assert.Assert(t, jaccard > 0, "description word overlap should boost similarity") |
| @@ -46,7 +46,7 @@ func seedClusterData(t *testing.T, ctx context.Context, dbs *db.Databases) { | |||
| 46 | {"did:test:carol", "carol"}, | 46 | {"did:test:carol", "carol"}, |
| 47 | } | 47 | } |
| 48 | for _, u := range users { | 48 | for _, u := range users { |
| 49 | - _, err := dbs.Users.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, u.did, u.handle) | 49 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, u.did, u.handle) |
| 50 | assert.NilError(t, err) | 50 | assert.NilError(t, err) |
| 51 | } | 51 | } |
| 52 | 52 | ||
| @@ -58,7 +58,7 @@ func seedClusterData(t *testing.T, ctx context.Context, dbs *db.Databases) { | |||
| 58 | {"https://e.com/feed", "Feed E"}, | 58 | {"https://e.com/feed", "Feed E"}, |
| 59 | } | 59 | } |
| 60 | for _, f := range feeds { | 60 | for _, f := range feeds { |
| 61 | - _, err := dbs.Articles.ExecContext(ctx, `INSERT INTO feeds (feed_url, title, site_url, description, feed_type, subscriber_count) VALUES (?, ?, ?, '', 'rss', 2)`, f.url, f.title, f.url) | 61 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO articles.feeds (feed_url, title, site_url, description, feed_type, subscriber_count) VALUES (?, ?, ?, '', 'rss', 2)`, f.url, f.title, f.url) |
| 62 | assert.NilError(t, err) | 62 | assert.NilError(t, err) |
| 63 | } | 63 | } |
| 64 | 64 | ||
| @@ -74,7 +74,7 @@ func seedClusterData(t *testing.T, ctx context.Context, dbs *db.Databases) { | |||
| 74 | {"did:test:carol", "https://c.com/feed"}, | 74 | {"did:test:carol", "https://c.com/feed"}, |
| 75 | } | 75 | } |
| 76 | for _, s := range subs { | 76 | for _, s := range subs { |
| 77 | - _, err := dbs.Articles.ExecContext(ctx, `INSERT INTO subscriptions (user_did, feed_url) VALUES (?, ?)`, s.user, s.feed) | 77 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO articles.subscriptions (user_did, feed_url) VALUES (?, ?)`, s.user, s.feed) |
| 78 | assert.NilError(t, err) | 78 | assert.NilError(t, err) |
| 79 | } | 79 | } |
| 80 | } | 80 | } |
| @@ -86,13 +86,13 @@ func seedFollowData(t *testing.T, ctx context.Context, dbs *db.Databases) { | |||
| 86 | {"did:test:bob", "did:test:carol"}, | 86 | {"did:test:bob", "did:test:carol"}, |
| 87 | } | 87 | } |
| 88 | for _, f := range follows { | 88 | for _, f := range follows { |
| 89 | - _, err := dbs.Users.ExecContext(ctx, `INSERT OR IGNORE INTO follows (user_did, target_did) VALUES (?, ?)`, f.user, f.target) | 89 | + _, err := dbs.DB().ExecContext(ctx, `INSERT OR IGNORE INTO follows (user_did, target_did) VALUES (?, ?)`, f.user, f.target) |
| 90 | assert.NilError(t, err) | 90 | assert.NilError(t, err) |
| 91 | } | 91 | } |
| 92 | } | 92 | } |
| 93 | 93 | ||
| 94 | func newTestEngine(dbs *db.Databases) *Engine { | 94 | func newTestEngine(dbs *db.Databases) *Engine { |
| 95 | - return NewEngine(dbs.Users.DB, slog.Default()) | 95 | + return NewEngine(dbs.DB(), slog.Default()) |
| 96 | } | 96 | } |
| 97 | 97 | ||
| 98 | func TestComputeFeedSimilarity(t *testing.T) { | 98 | func TestComputeFeedSimilarity(t *testing.T) { |
| @@ -105,7 +105,7 @@ func TestComputeFeedSimilarity(t *testing.T) { | |||
| 105 | assert.NilError(t, err) | 105 | assert.NilError(t, err) |
| 106 | 106 | ||
| 107 | var count int | 107 | var count int |
| 108 | - err = dbs.Users.QueryRowContext(ctx, `SELECT COUNT(*) FROM recs.feed_similarity`).Scan(&count) | 108 | + err = dbs.DB().QueryRowContext(ctx, `SELECT COUNT(*) FROM recs.feed_similarity`).Scan(&count) |
| 109 | assert.NilError(t, err) | 109 | assert.NilError(t, err) |
| 110 | assert.Assert(t, count > 0, "expected feed similarity pairs") | 110 | assert.Assert(t, count > 0, "expected feed similarity pairs") |
| 111 | } | 111 | } |
| @@ -120,7 +120,7 @@ func TestComputeUserSimilarity(t *testing.T) { | |||
| 120 | assert.NilError(t, err) | 120 | assert.NilError(t, err) |
| 121 | 121 | ||
| 122 | var count int | 122 | var count int |
| 123 | - err = dbs.Users.QueryRowContext(ctx, `SELECT COUNT(*) FROM recs.user_similarity`).Scan(&count) | 123 | + err = dbs.DB().QueryRowContext(ctx, `SELECT COUNT(*) FROM recs.user_similarity`).Scan(&count) |
| 124 | assert.NilError(t, err) | 124 | assert.NilError(t, err) |
| 125 | assert.Assert(t, count > 0, "expected user similarity pairs") | 125 | assert.Assert(t, count > 0, "expected user similarity pairs") |
| 126 | } | 126 | } |
| @@ -222,14 +222,14 @@ func TestRecordImpressions(t *testing.T) { | |||
| 222 | assert.NilError(t, engine.RecordImpressions(ctx, "did:test:alice", impressions)) | 222 | assert.NilError(t, engine.RecordImpressions(ctx, "did:test:alice", impressions)) |
| 223 | 223 | ||
| 224 | var count int | 224 | var count int |
| 225 | - assert.NilError(t, dbs.Users.QueryRowContext(ctx, | 225 | + assert.NilError(t, dbs.DB().QueryRowContext(ctx, |
| 226 | `SELECT COUNT(*) FROM recs.recommendation_impressions WHERE user_did = 'did:test:alice'`).Scan(&count)) | 226 | `SELECT COUNT(*) FROM recs.recommendation_impressions WHERE user_did = 'did:test:alice'`).Scan(&count)) |
| 227 | assert.Equal(t, count, 2) | 227 | assert.Equal(t, count, 2) |
| 228 | 228 | ||
| 229 | assert.NilError(t, engine.RecordImpressions(ctx, "did:test:alice", impressions)) | 229 | assert.NilError(t, engine.RecordImpressions(ctx, "did:test:alice", impressions)) |
| 230 | 230 | ||
| 231 | var shownCount int | 231 | var shownCount int |
| 232 | - assert.NilError(t, dbs.Users.QueryRowContext(ctx, | 232 | + assert.NilError(t, dbs.DB().QueryRowContext(ctx, |
| 233 | `SELECT shown_count FROM recs.recommendation_impressions WHERE user_did = 'did:test:alice' AND target_id = 'https://a.com/feed'`).Scan(&shownCount)) | 233 | `SELECT shown_count FROM recs.recommendation_impressions WHERE user_did = 'did:test:alice' AND target_id = 'https://a.com/feed'`).Scan(&shownCount)) |
| 234 | assert.Equal(t, shownCount, 2, "shown_count should increment on repeated impression") | 234 | assert.Equal(t, shownCount, 2, "shown_count should increment on repeated impression") |
| 235 | } | 235 | } |
| @@ -247,7 +247,7 @@ func TestMarkImpressionActed(t *testing.T) { | |||
| 247 | assert.NilError(t, engine.MarkImpressionActed(ctx, "did:test:alice", "feed", "https://a.com/feed")) | 247 | assert.NilError(t, engine.MarkImpressionActed(ctx, "did:test:alice", "feed", "https://a.com/feed")) |
| 248 | 248 | ||
| 249 | var acted bool | 249 | var acted bool |
| 250 | - assert.NilError(t, dbs.Users.QueryRowContext(ctx, | 250 | + assert.NilError(t, dbs.DB().QueryRowContext(ctx, |
| 251 | `SELECT acted FROM recs.recommendation_impressions WHERE user_did = 'did:test:alice' AND target_id = 'https://a.com/feed'`).Scan(&acted)) | 251 | `SELECT acted FROM recs.recommendation_impressions WHERE user_did = 'did:test:alice' AND target_id = 'https://a.com/feed'`).Scan(&acted)) |
| 252 | assert.Assert(t, acted, "impression should be marked as acted") | 252 | assert.Assert(t, acted, "impression should be marked as acted") |
| 253 | } | 253 | } |
| @@ -262,15 +262,15 @@ func TestComputeFollowDistances(t *testing.T) { | |||
| 262 | assert.NilError(t, engine.ComputeFollowDistances(ctx)) | 262 | assert.NilError(t, engine.ComputeFollowDistances(ctx)) |
| 263 | 263 | ||
| 264 | var d1, d2 int | 264 | var d1, d2 int |
| 265 | - assert.NilError(t, dbs.Users.QueryRowContext(ctx, | 265 | + assert.NilError(t, dbs.DB().QueryRowContext(ctx, |
| 266 | `SELECT COUNT(*) FROM recs.follow_distances WHERE distance = 1`).Scan(&d1)) | 266 | `SELECT COUNT(*) FROM recs.follow_distances WHERE distance = 1`).Scan(&d1)) |
| 267 | - assert.NilError(t, dbs.Users.QueryRowContext(ctx, | 267 | + assert.NilError(t, dbs.DB().QueryRowContext(ctx, |
| 268 | `SELECT COUNT(*) FROM recs.follow_distances WHERE distance = 2`).Scan(&d2)) | 268 | `SELECT COUNT(*) FROM recs.follow_distances WHERE distance = 2`).Scan(&d2)) |
| 269 | assert.Assert(t, d1 >= 2, "expected at least 2 direct follow distances") | 269 | assert.Assert(t, d1 >= 2, "expected at least 2 direct follow distances") |
| 270 | assert.Assert(t, d2 >= 1, "expected at least 1 two-hop distance (alice -> bob -> carol)") | 270 | assert.Assert(t, d2 >= 1, "expected at least 1 two-hop distance (alice -> bob -> carol)") |
| 271 | 271 | ||
| 272 | var dist int | 272 | var dist int |
| 273 | - assert.NilError(t, dbs.Users.QueryRowContext(ctx, | 273 | + assert.NilError(t, dbs.DB().QueryRowContext(ctx, |
| 274 | `SELECT distance FROM recs.follow_distances WHERE user_a = 'did:test:alice' AND user_b = 'did:test:carol'`).Scan(&dist)) | 274 | `SELECT distance FROM recs.follow_distances WHERE user_a = 'did:test:alice' AND user_b = 'did:test:carol'`).Scan(&dist)) |
| 275 | assert.Equal(t, dist, 2, "alice should be 2 hops from carol") | 275 | assert.Equal(t, dist, 2, "alice should be 2 hops from carol") |
| 276 | } | 276 | } |
| @@ -282,7 +282,7 @@ func TestAutoDismissStale(t *testing.T) { | |||
| 282 | 282 | ||
| 283 | engine := newTestEngine(dbs) | 283 | engine := newTestEngine(dbs) |
| 284 | 284 | ||
| 285 | - _, err := dbs.Users.ExecContext(ctx, ` | 285 | + _, err := dbs.DB().ExecContext(ctx, ` |
| 286 | INSERT INTO recs.recommendation_impressions (user_did, target_type, target_id, first_shown_at, last_shown_at, shown_count, acted) | 286 | INSERT INTO recs.recommendation_impressions (user_did, target_type, target_id, first_shown_at, last_shown_at, shown_count, acted) |
| 287 | VALUES ('did:test:alice', 'feed', 'https://stale.com/feed', datetime('now', '-31 days'), datetime('now'), 20, 0) | 287 | VALUES ('did:test:alice', 'feed', 'https://stale.com/feed', datetime('now', '-31 days'), datetime('now'), 20, 0) |
| 288 | `) | 288 | `) |
| @@ -302,7 +302,7 @@ func TestAutoDismissStale_DoesNotDismissRecent(t *testing.T) { | |||
| 302 | 302 | ||
| 303 | engine := newTestEngine(dbs) | 303 | engine := newTestEngine(dbs) |
| 304 | 304 | ||
| 305 | - _, err := dbs.Users.ExecContext(ctx, ` | 305 | + _, err := dbs.DB().ExecContext(ctx, ` |
| 306 | INSERT INTO recs.recommendation_impressions (user_did, target_type, target_id, first_shown_at, last_shown_at, shown_count, acted) | 306 | INSERT INTO recs.recommendation_impressions (user_did, target_type, target_id, first_shown_at, last_shown_at, shown_count, acted) |
| 307 | VALUES ('did:test:alice', 'feed', 'https://recent.com/feed', datetime('now'), datetime('now'), 5, 0) | 307 | VALUES ('did:test:alice', 'feed', 'https://recent.com/feed', datetime('now'), datetime('now'), 5, 0) |
| 308 | `) | 308 | `) |
| @@ -322,7 +322,7 @@ func TestAutoDismissStale_DoesNotDismissActed(t *testing.T) { | |||
| 322 | 322 | ||
| 323 | engine := newTestEngine(dbs) | 323 | engine := newTestEngine(dbs) |
| 324 | 324 | ||
| 325 | - _, err := dbs.Users.ExecContext(ctx, ` | 325 | + _, err := dbs.DB().ExecContext(ctx, ` |
| 326 | INSERT INTO recs.recommendation_impressions (user_did, target_type, target_id, first_shown_at, last_shown_at, shown_count, acted) | 326 | INSERT INTO recs.recommendation_impressions (user_did, target_type, target_id, first_shown_at, last_shown_at, shown_count, acted) |
| 327 | VALUES ('did:test:alice', 'feed', 'https://acted.com/feed', datetime('now', '-31 days'), datetime('now'), 20, 1) | 327 | VALUES ('did:test:alice', 'feed', 'https://acted.com/feed', datetime('now', '-31 days'), datetime('now'), 20, 1) |
| 328 | `) | 328 | `) |
| @@ -397,13 +397,13 @@ func TestSignalWeights_RewardPenalize(t *testing.T) { | |||
| 397 | 397 | ||
| 398 | engine := newTestEngine(dbs) | 398 | engine := newTestEngine(dbs) |
| 399 | 399 | ||
| 400 | - _, err := dbs.Users.ExecContext(ctx, ` | 400 | + _, err := dbs.DB().ExecContext(ctx, ` |
| 401 | INSERT INTO recs.recommendation_impressions (user_did, target_type, target_id, first_shown_at, last_shown_at, shown_count, acted) | 401 | INSERT INTO recs.recommendation_impressions (user_did, target_type, target_id, first_shown_at, last_shown_at, shown_count, acted) |
| 402 | VALUES ('did:test:alice', 'feed', 'https://a.com/feed', datetime('now'), datetime('now'), 1, 1) | 402 | VALUES ('did:test:alice', 'feed', 'https://a.com/feed', datetime('now'), datetime('now'), 1, 1) |
| 403 | `) | 403 | `) |
| 404 | assert.NilError(t, err) | 404 | assert.NilError(t, err) |
| 405 | for i := range minActionsTune { | 405 | for i := range minActionsTune { |
| 406 | - _, err = dbs.Users.ExecContext(ctx, ` | 406 | + _, err = dbs.DB().ExecContext(ctx, ` |
| 407 | INSERT INTO recs.recommendation_impressions (user_did, target_type, target_id, first_shown_at, last_shown_at, shown_count, acted) | 407 | INSERT INTO recs.recommendation_impressions (user_did, target_type, target_id, first_shown_at, last_shown_at, shown_count, acted) |
| 408 | VALUES ('did:test:alice', 'feed', ?, datetime('now'), datetime('now'), 1, 1) | 408 | VALUES ('did:test:alice', 'feed', ?, datetime('now'), datetime('now'), 1, 1) |
| 409 | `, fmt.Sprintf("https://%d.com/feed", i)) | 409 | `, fmt.Sprintf("https://%d.com/feed", i)) |
| @@ -425,12 +425,12 @@ func TestColdStartRecommendations(t *testing.T) { | |||
| 425 | engine := newTestEngine(dbs) | 425 | engine := newTestEngine(dbs) |
| 426 | assert.NilError(t, engine.ComputeFollowDistances(ctx)) | 426 | assert.NilError(t, engine.ComputeFollowDistances(ctx)) |
| 427 | 427 | ||
| 428 | - _, err := dbs.Articles.ExecContext(ctx, `UPDATE feeds SET subscriber_count = 2 WHERE feed_url = 'https://a.com/feed'`) | 428 | + _, err := dbs.DB().ExecContext(ctx, `UPDATE articles.feeds SET subscriber_count = 2 WHERE feed_url = 'https://a.com/feed'`) |
| 429 | assert.NilError(t, err) | 429 | assert.NilError(t, err) |
| 430 | - _, err = dbs.Articles.ExecContext(ctx, `UPDATE feeds SET subscriber_count = 2 WHERE feed_url = 'https://b.com/feed'`) | 430 | + _, err = dbs.DB().ExecContext(ctx, `UPDATE articles.feeds SET subscriber_count = 2 WHERE feed_url = 'https://b.com/feed'`) |
| 431 | assert.NilError(t, err) | 431 | assert.NilError(t, err) |
| 432 | 432 | ||
| 433 | - _, err = dbs.Users.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, "did:test:newuser", "newuser") | 433 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, "did:test:newuser", "newuser") |
| 434 | assert.NilError(t, err) | 434 | assert.NilError(t, err) |
| 435 | 435 | ||
| 436 | recs, err := engine.ColdStartRecommendations(ctx, "did:test:newuser", 10) | 436 | recs, err := engine.ColdStartRecommendations(ctx, "did:test:newuser", 10) |
| @@ -475,7 +475,7 @@ func TestDismissArticle(t *testing.T) { | |||
| 475 | assert.NilError(t, engine.DismissArticle(ctx, "did:test:alice", "https://a.com/article1", "not_interested")) | 475 | assert.NilError(t, engine.DismissArticle(ctx, "did:test:alice", "https://a.com/article1", "not_interested")) |
| 476 | 476 | ||
| 477 | var count int | 477 | var count int |
| 478 | - assert.NilError(t, dbs.Users.QueryRowContext(ctx, | 478 | + assert.NilError(t, dbs.DB().QueryRowContext(ctx, |
| 479 | `SELECT COUNT(*) FROM recs.dismissed_recommendations WHERE user_did = 'did:test:alice' AND target_type = 'article'`).Scan(&count)) | 479 | `SELECT COUNT(*) FROM recs.dismissed_recommendations WHERE user_did = 'did:test:alice' AND target_type = 'article'`).Scan(&count)) |
| 480 | assert.Equal(t, count, 1) | 480 | assert.Equal(t, count, 1) |
| 481 | } | 481 | } |
| @@ -489,7 +489,7 @@ func TestComputeSignalProfiles(t *testing.T) { | |||
| 489 | assert.NilError(t, engine.ComputeSignalProfiles(ctx)) | 489 | assert.NilError(t, engine.ComputeSignalProfiles(ctx)) |
| 490 | 490 | ||
| 491 | var count int | 491 | var count int |
| 492 | - assert.NilError(t, dbs.Users.QueryRowContext(ctx, `SELECT COUNT(*) FROM recs.user_signal_profiles`).Scan(&count)) | 492 | + assert.NilError(t, dbs.DB().QueryRowContext(ctx, `SELECT COUNT(*) FROM recs.user_signal_profiles`).Scan(&count)) |
| 493 | assert.Assert(t, count >= 3, "expected signal profiles for all users") | 493 | assert.Assert(t, count >= 3, "expected signal profiles for all users") |
| 494 | } | 494 | } |
| 495 | 495 | ||
| @@ -504,7 +504,7 @@ func TestDismissFeed_Idempotent(t *testing.T) { | |||
| 504 | assert.NilError(t, engine.DismissFeed(ctx, "did:test:alice", "https://a.com/feed", "reason2")) | 504 | assert.NilError(t, engine.DismissFeed(ctx, "did:test:alice", "https://a.com/feed", "reason2")) |
| 505 | 505 | ||
| 506 | var count int | 506 | var count int |
| 507 | - assert.NilError(t, dbs.Users.QueryRowContext(ctx, | 507 | + assert.NilError(t, dbs.DB().QueryRowContext(ctx, |
| 508 | `SELECT COUNT(*) FROM recs.dismissed_recommendations WHERE user_did = 'did:test:alice' AND target_type = 'feed'`).Scan(&count)) | 508 | `SELECT COUNT(*) FROM recs.dismissed_recommendations WHERE user_did = 'did:test:alice' AND target_type = 'feed'`).Scan(&count)) |
| 509 | assert.Equal(t, count, 1, "duplicate dismiss should not create extra rows") | 509 | assert.Equal(t, count, 1, "duplicate dismiss should not create extra rows") |
| 510 | } | 510 | } |
| @@ -513,33 +513,33 @@ func TestDescriptionBasedFeedSimilarity(t *testing.T) { | |||
| 513 | ctx := context.Background() | 513 | ctx := context.Background() |
| 514 | dbs := setupClusterTestDB(t) | 514 | dbs := setupClusterTestDB(t) |
| 515 | 515 | ||
| 516 | - _, err := dbs.Users.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, "did:test:alice", "alice") | 516 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, "did:test:alice", "alice") |
| 517 | assert.NilError(t, err) | 517 | assert.NilError(t, err) |
| 518 | - _, err = dbs.Users.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, "did:test:bob", "bob") | 518 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, "did:test:bob", "bob") |
| 519 | assert.NilError(t, err) | 519 | assert.NilError(t, err) |
| 520 | 520 | ||
| 521 | - _, err = dbs.Articles.ExecContext(ctx, `INSERT INTO feeds (feed_url, title, site_url, description, feed_type) VALUES (?, ?, ?, ?, 'rss')`, | 521 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.feeds (feed_url, title, site_url, description, feed_type) VALUES (?, ?, ?, ?, 'rss')`, |
| 522 | "https://go.com/feed", "Go Blog", "https://go.com", "programming language golang software development") | 522 | "https://go.com/feed", "Go Blog", "https://go.com", "programming language golang software development") |
| 523 | assert.NilError(t, err) | 523 | assert.NilError(t, err) |
| 524 | - _, err = dbs.Articles.ExecContext(ctx, `INSERT INTO feeds (feed_url, title, site_url, description, feed_type) VALUES (?, ?, ?, ?, 'rss')`, | 524 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.feeds (feed_url, title, site_url, description, feed_type) VALUES (?, ?, ?, ?, 'rss')`, |
| 525 | "https://rust.com/feed", "Rust Blog", "https://rust.com", "programming language rust software development") | 525 | "https://rust.com/feed", "Rust Blog", "https://rust.com", "programming language rust software development") |
| 526 | assert.NilError(t, err) | 526 | assert.NilError(t, err) |
| 527 | 527 | ||
| 528 | - _, err = dbs.Articles.ExecContext(ctx, `INSERT INTO subscriptions (user_did, feed_url) VALUES (?, ?)`, "did:test:alice", "https://go.com/feed") | 528 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.subscriptions (user_did, feed_url) VALUES (?, ?)`, "did:test:alice", "https://go.com/feed") |
| 529 | assert.NilError(t, err) | 529 | assert.NilError(t, err) |
| 530 | - _, err = dbs.Articles.ExecContext(ctx, `INSERT INTO subscriptions (user_did, feed_url) VALUES (?, ?)`, "did:test:bob", "https://rust.com/feed") | 530 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.subscriptions (user_did, feed_url) VALUES (?, ?)`, "did:test:bob", "https://rust.com/feed") |
| 531 | assert.NilError(t, err) | 531 | assert.NilError(t, err) |
| 532 | 532 | ||
| 533 | engine := newTestEngine(dbs) | 533 | engine := newTestEngine(dbs) |
| 534 | assert.NilError(t, engine.ComputeFeedSimilarity(ctx)) | 534 | assert.NilError(t, engine.ComputeFeedSimilarity(ctx)) |
| 535 | 535 | ||
| 536 | var count int | 536 | var count int |
| 537 | - assert.NilError(t, dbs.Users.QueryRowContext(ctx, `SELECT COUNT(*) FROM recs.feed_similarity`).Scan(&count)) | 537 | + assert.NilError(t, dbs.DB().QueryRowContext(ctx, `SELECT COUNT(*) FROM recs.feed_similarity`).Scan(&count)) |
| 538 | assert.Assert(t, count >= 0, "description-based similarity should produce pairs") | 538 | assert.Assert(t, count >= 0, "description-based similarity should produce pairs") |
| 539 | 539 | ||
| 540 | if count > 0 { | 540 | if count > 0 { |
| 541 | var jaccard float64 | 541 | var jaccard float64 |
| 542 | - assert.NilError(t, dbs.Users.QueryRowContext(ctx, | 542 | + assert.NilError(t, dbs.DB().QueryRowContext(ctx, |
| 543 | `SELECT jaccard FROM recs.feed_similarity WHERE feed_a = ? AND feed_b = ?`, | 543 | `SELECT jaccard FROM recs.feed_similarity WHERE feed_a = ? AND feed_b = ?`, |
| 544 | "https://go.com/feed", "https://rust.com/feed").Scan(&jaccard)) | 544 | "https://go.com/feed", "https://rust.com/feed").Scan(&jaccard)) |
| 545 | assert.Assert(t, jaccard > 0, "description word overlap should boost similarity") | 545 | assert.Assert(t, jaccard > 0, "description word overlap should boost similarity") |
modified
internal/db/article.go +101 -92 | @@ -10,6 +10,14 @@ import ( | ||
| 10 | 10 | "pkg.rbrt.fr/glean/internal/feed" |
| 11 | 11 | ) |
| 12 | 12 | |
| 13 | +type ArticleStore struct { | |
| 14 | + db *DB | |
| 15 | +} | |
| 16 | + | |
| 17 | +func NewArticleStore(db *DB) *ArticleStore { | |
| 18 | + return &ArticleStore{db: db} | |
| 19 | +} | |
| 20 | + | |
| 13 | 21 | type Article struct { |
| 14 | 22 | ID int64 |
| 15 | 23 | FeedURL string |
| @@ -37,19 +45,19 @@ type ReadState struct { | ||
| 37 | 45 | ReadAt sql.NullTime |
| 38 | 46 | } |
| 39 | 47 | |
| 40 | -func (db *DB) UpsertArticlesBatch(ctx context.Context, articles []feed.Article) error { | |
| 48 | +func (s *ArticleStore) UpsertArticlesBatch(ctx context.Context, articles []feed.Article) error { | |
| 41 | 49 | if len(articles) == 0 { |
| 42 | 50 | return nil |
| 43 | 51 | } |
| 44 | 52 | |
| 45 | - tx, err := db.BeginTx(ctx, nil) | |
| 53 | + tx, err := s.db.BeginTx(ctx, nil) | |
| 46 | 54 | if err != nil { |
| 47 | 55 | return err |
| 48 | 56 | } |
| 49 | 57 | defer tx.Rollback() |
| 50 | 58 | |
| 51 | 59 | stmt, err := tx.PrepareContext(ctx, ` |
| 52 | - INSERT INTO articles (feed_url, guid, title, url, author, summary, content, published, updated) | |
| 60 | + INSERT INTO articles.articles (feed_url, guid, title, url, author, summary, content, published, updated) | |
| 53 | 61 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) |
| 54 | 62 | ON CONFLICT(feed_url, guid) DO NOTHING |
| 55 | 63 | `) |
| @@ -79,11 +87,11 @@ func (db *DB) UpsertArticlesBatch(ctx context.Context, articles []feed.Article) | ||
| 79 | 87 | return tx.Commit() |
| 80 | 88 | } |
| 81 | 89 | |
| 82 | -func (db *DB) GetArticle(ctx context.Context, id int64) (*Article, error) { | |
| 90 | +func (s *ArticleStore) GetArticle(ctx context.Context, id int64) (*Article, error) { | |
| 83 | 91 | a := &Article{} |
| 84 | - err := db.QueryRowContext(ctx, ` | |
| 92 | + err := s.db.QueryRowContext(ctx, ` | |
| 85 | 93 | SELECT id, feed_url, guid, title, url, author, summary, content, full_content, published, updated, fetched_at |
| 86 | - FROM articles WHERE id = ? | |
| 94 | + FROM articles.articles WHERE id = ? | |
| 87 | 95 | `, id).Scan(&a.ID, &a.FeedURL, &a.GUID, &a.Title, &a.URL, &a.Author, |
| 88 | 96 | &a.Summary, &a.Content, &a.FullContent, &a.Published, &a.Updated, &a.FetchedAt) |
| 89 | 97 | if err != nil { |
| @@ -92,7 +100,7 @@ func (db *DB) GetArticle(ctx context.Context, id int64) (*Article, error) { | ||
| 92 | 100 | return a, nil |
| 93 | 101 | } |
| 94 | 102 | |
| 95 | -func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit, offset int) ([]*Article, error) { | |
| 103 | +func (s *ArticleStore) ListArticles(ctx context.Context, userDID, feedURL string, limit, offset int) ([]*Article, error) { | |
| 96 | 104 | var query string |
| 97 | 105 | var args []any |
| 98 | 106 | |
| @@ -103,12 +111,12 @@ func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit, | ||
| 103 | 111 | COALESCE(r.is_read, 0), |
| 104 | 112 | COALESCE(lc.cnt, 0), |
| 105 | 113 | COALESCE(ul.liked, 0) |
| 106 | - FROM articles a | |
| 107 | - LEFT JOIN feeds f ON a.feed_url = f.feed_url | |
| 108 | - LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id | |
| 109 | - LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc | |
| 114 | + FROM articles.articles a | |
| 115 | + LEFT JOIN articles.feeds f ON a.feed_url = f.feed_url | |
| 116 | + LEFT JOIN articles.read_state r ON r.user_did = ? AND r.article_id = a.id | |
| 117 | + LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM articles.likes GROUP BY feed_url, article_url) lc | |
| 110 | 118 | ON lc.feed_url = a.feed_url AND lc.article_url = a.url |
| 111 | - LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul | |
| 119 | + LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM articles.likes WHERE author_did = ?) ul | |
| 112 | 120 | ON ul.feed_url = a.feed_url AND ul.article_url = a.url |
| 113 | 121 | WHERE a.feed_url = ? |
| 114 | 122 | ` |
| @@ -120,13 +128,13 @@ func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit, | ||
| 120 | 128 | COALESCE(r.is_read, 0), |
| 121 | 129 | COALESCE(lc.cnt, 0), |
| 122 | 130 | COALESCE(ul.liked, 0) |
| 123 | - FROM articles a | |
| 124 | - JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? | |
| 125 | - LEFT JOIN feeds f ON a.feed_url = f.feed_url | |
| 126 | - LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id | |
| 127 | - LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc | |
| 131 | + FROM articles.articles a | |
| 132 | + JOIN articles.subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? | |
| 133 | + LEFT JOIN articles.feeds f ON a.feed_url = f.feed_url | |
| 134 | + LEFT JOIN articles.read_state r ON r.user_did = ? AND r.article_id = a.id | |
| 135 | + LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM articles.likes GROUP BY feed_url, article_url) lc | |
| 128 | 136 | ON lc.feed_url = a.feed_url AND lc.article_url = a.url |
| 129 | - LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul | |
| 137 | + LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM articles.likes WHERE author_did = ?) ul | |
| 130 | 138 | ON ul.feed_url = a.feed_url AND ul.article_url = a.url |
| 131 | 139 | WHERE 1=1 |
| 132 | 140 | ` |
| @@ -137,7 +145,7 @@ func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit, | ||
| 137 | 145 | query += ` ORDER BY (CASE WHEN a.published > 'now' THEN 1 ELSE 0 END), a.published DESC LIMIT ? OFFSET ?` |
| 138 | 146 | args = append(args, limit, offset) |
| 139 | 147 | |
| 140 | - rows, err := db.QueryContext(ctx, query, args...) | |
| 148 | + rows, err := s.db.QueryContext(ctx, query, args...) | |
| 141 | 149 | if err != nil { |
| 142 | 150 | return nil, err |
| 143 | 151 | } |
| @@ -156,7 +164,7 @@ func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit, | ||
| 156 | 164 | return articles, rows.Err() |
| 157 | 165 | } |
| 158 | 166 | |
| 159 | -func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, limit, offset int) ([]*Article, error) { | |
| 167 | +func (s *ArticleStore) ListUnreadArticles(ctx context.Context, userDID, feedURL string, limit, offset int) ([]*Article, error) { | |
| 160 | 168 | var query string |
| 161 | 169 | var args []any |
| 162 | 170 | |
| @@ -167,12 +175,12 @@ func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, l | ||
| 167 | 175 | COALESCE(r.is_read, 0), |
| 168 | 176 | COALESCE(lc.cnt, 0), |
| 169 | 177 | COALESCE(ul.liked, 0) |
| 170 | - FROM articles a | |
| 171 | - LEFT JOIN feeds f ON a.feed_url = f.feed_url | |
| 172 | - LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id | |
| 173 | - LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc | |
| 178 | + FROM articles.articles a | |
| 179 | + LEFT JOIN articles.feeds f ON a.feed_url = f.feed_url | |
| 180 | + LEFT JOIN articles.read_state r ON r.user_did = ? AND r.article_id = a.id | |
| 181 | + LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM articles.likes GROUP BY feed_url, article_url) lc | |
| 174 | 182 | ON lc.feed_url = a.feed_url AND lc.article_url = a.url |
| 175 | - LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul | |
| 183 | + LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM articles.likes WHERE author_did = ?) ul | |
| 176 | 184 | ON ul.feed_url = a.feed_url AND ul.article_url = a.url |
| 177 | 185 | WHERE a.feed_url = ? AND (r.is_read = 0 OR r.is_read IS NULL) |
| 178 | 186 | ` |
| @@ -184,13 +192,13 @@ func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, l | ||
| 184 | 192 | COALESCE(r.is_read, 0), |
| 185 | 193 | COALESCE(lc.cnt, 0), |
| 186 | 194 | COALESCE(ul.liked, 0) |
| 187 | - FROM articles a | |
| 188 | - JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? | |
| 189 | - LEFT JOIN feeds f ON a.feed_url = f.feed_url | |
| 190 | - LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id | |
| 191 | - LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc | |
| 195 | + FROM articles.articles a | |
| 196 | + JOIN articles.subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? | |
| 197 | + LEFT JOIN articles.feeds f ON a.feed_url = f.feed_url | |
| 198 | + LEFT JOIN articles.read_state r ON r.user_did = ? AND r.article_id = a.id | |
| 199 | + LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM articles.likes GROUP BY feed_url, article_url) lc | |
| 192 | 200 | ON lc.feed_url = a.feed_url AND lc.article_url = a.url |
| 193 | - LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul | |
| 201 | + LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM articles.likes WHERE author_did = ?) ul | |
| 194 | 202 | ON ul.feed_url = a.feed_url AND ul.article_url = a.url |
| 195 | 203 | WHERE (r.is_read = 0 OR r.is_read IS NULL) |
| 196 | 204 | ` |
| @@ -201,7 +209,7 @@ func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, l | ||
| 201 | 209 | query += ` ORDER BY (CASE WHEN a.published > 'now' THEN 1 ELSE 0 END), a.published DESC LIMIT ? OFFSET ?` |
| 202 | 210 | args = append(args, limit, offset) |
| 203 | 211 | |
| 204 | - rows, err := db.QueryContext(ctx, query, args...) | |
| 212 | + rows, err := s.db.QueryContext(ctx, query, args...) | |
| 205 | 213 | if err != nil { |
| 206 | 214 | return nil, err |
| 207 | 215 | } |
| @@ -220,7 +228,7 @@ func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, l | ||
| 220 | 228 | return articles, rows.Err() |
| 221 | 229 | } |
| 222 | 230 | |
| 223 | -func (db *DB) ListReadArticles(ctx context.Context, userDID, feedURL string, limit, offset int) ([]*Article, error) { | |
| 231 | +func (s *ArticleStore) ListReadArticles(ctx context.Context, userDID, feedURL string, limit, offset int) ([]*Article, error) { | |
| 224 | 232 | var query string |
| 225 | 233 | var args []any |
| 226 | 234 | |
| @@ -231,12 +239,12 @@ func (db *DB) ListReadArticles(ctx context.Context, userDID, feedURL string, lim | ||
| 231 | 239 | COALESCE(r.is_read, 0), |
| 232 | 240 | COALESCE(lc.cnt, 0), |
| 233 | 241 | COALESCE(ul.liked, 0) |
| 234 | - FROM articles a | |
| 235 | - LEFT JOIN feeds f ON a.feed_url = f.feed_url | |
| 236 | - JOIN read_state r ON r.user_did = ? AND r.article_id = a.id | |
| 237 | - LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc | |
| 242 | + FROM articles.articles a | |
| 243 | + LEFT JOIN articles.feeds f ON a.feed_url = f.feed_url | |
| 244 | + JOIN articles.read_state r ON r.user_did = ? AND r.article_id = a.id | |
| 245 | + LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM articles.likes GROUP BY feed_url, article_url) lc | |
| 238 | 246 | ON lc.feed_url = a.feed_url AND lc.article_url = a.url |
| 239 | - LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul | |
| 247 | + LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM articles.likes WHERE author_did = ?) ul | |
| 240 | 248 | ON ul.feed_url = a.feed_url AND ul.article_url = a.url |
| 241 | 249 | WHERE r.is_read = 1 AND a.feed_url = ? |
| 242 | 250 | ` |
| @@ -248,13 +256,13 @@ func (db *DB) ListReadArticles(ctx context.Context, userDID, feedURL string, lim | ||
| 248 | 256 | COALESCE(r.is_read, 0), |
| 249 | 257 | COALESCE(lc.cnt, 0), |
| 250 | 258 | COALESCE(ul.liked, 0) |
| 251 | - FROM articles a | |
| 252 | - JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? | |
| 253 | - LEFT JOIN feeds f ON a.feed_url = f.feed_url | |
| 254 | - JOIN read_state r ON r.user_did = ? AND r.article_id = a.id | |
| 255 | - LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc | |
| 259 | + FROM articles.articles a | |
| 260 | + JOIN articles.subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? | |
| 261 | + LEFT JOIN articles.feeds f ON a.feed_url = f.feed_url | |
| 262 | + JOIN articles.read_state r ON r.user_did = ? AND r.article_id = a.id | |
| 263 | + LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM articles.likes GROUP BY feed_url, article_url) lc | |
| 256 | 264 | ON lc.feed_url = a.feed_url AND lc.article_url = a.url |
| 257 | - LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul | |
| 265 | + LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM articles.likes WHERE author_did = ?) ul | |
| 258 | 266 | ON ul.feed_url = a.feed_url AND ul.article_url = a.url |
| 259 | 267 | WHERE r.is_read = 1 |
| 260 | 268 | ` |
| @@ -265,7 +273,7 @@ func (db *DB) ListReadArticles(ctx context.Context, userDID, feedURL string, lim | ||
| 265 | 273 | query += ` ORDER BY (CASE WHEN a.published > 'now' THEN 1 ELSE 0 END), a.published DESC LIMIT ? OFFSET ?` |
| 266 | 274 | args = append(args, limit, offset) |
| 267 | 275 | |
| 268 | - rows, err := db.QueryContext(ctx, query, args...) | |
| 276 | + rows, err := s.db.QueryContext(ctx, query, args...) | |
| 269 | 277 | if err != nil { |
| 270 | 278 | return nil, err |
| 271 | 279 | } |
| @@ -284,9 +292,9 @@ func (db *DB) ListReadArticles(ctx context.Context, userDID, feedURL string, lim | ||
| 284 | 292 | return articles, rows.Err() |
| 285 | 293 | } |
| 286 | 294 | |
| 287 | -func (db *DB) MarkArticleRead(ctx context.Context, userDID string, articleID int64) error { | |
| 288 | - _, err := db.ExecContext(ctx, ` | |
| 289 | - INSERT INTO read_state (user_did, article_id, is_read, read_at) | |
| 295 | +func (s *ArticleStore) MarkArticleRead(ctx context.Context, userDID string, articleID int64) error { | |
| 296 | + _, err := s.db.ExecContext(ctx, ` | |
| 297 | + INSERT INTO articles.read_state (user_did, article_id, is_read, read_at) | |
| 290 | 298 | VALUES (?, ?, 1, CURRENT_TIMESTAMP) |
| 291 | 299 | ON CONFLICT(user_did, article_id) DO UPDATE SET |
| 292 | 300 | is_read = 1, read_at = CURRENT_TIMESTAMP |
| @@ -294,9 +302,9 @@ func (db *DB) MarkArticleRead(ctx context.Context, userDID string, articleID int | ||
| 294 | 302 | return err |
| 295 | 303 | } |
| 296 | 304 | |
| 297 | -func (db *DB) MarkArticleUnread(ctx context.Context, userDID string, articleID int64) error { | |
| 298 | - _, err := db.ExecContext(ctx, ` | |
| 299 | - INSERT INTO read_state (user_did, article_id, is_read) | |
| 305 | +func (s *ArticleStore) MarkArticleUnread(ctx context.Context, userDID string, articleID int64) error { | |
| 306 | + _, err := s.db.ExecContext(ctx, ` | |
| 307 | + INSERT INTO articles.read_state (user_did, article_id, is_read) | |
| 300 | 308 | VALUES (?, ?, 0) |
| 301 | 309 | ON CONFLICT(user_did, article_id) DO UPDATE SET |
| 302 | 310 | is_read = 0, read_at = NULL |
| @@ -304,11 +312,11 @@ func (db *DB) MarkArticleUnread(ctx context.Context, userDID string, articleID i | ||
| 304 | 312 | return err |
| 305 | 313 | } |
| 306 | 314 | |
| 307 | -func (db *DB) MarkAllRead(ctx context.Context, userDID, feedURL string) error { | |
| 308 | - _, err := db.ExecContext(ctx, ` | |
| 309 | - INSERT INTO read_state (user_did, article_id, is_read, read_at) | |
| 315 | +func (s *ArticleStore) MarkAllRead(ctx context.Context, userDID, feedURL string) error { | |
| 316 | + _, err := s.db.ExecContext(ctx, ` | |
| 317 | + INSERT INTO articles.read_state (user_did, article_id, is_read, read_at) | |
| 310 | 318 | SELECT ?, a.id, 1, CURRENT_TIMESTAMP |
| 311 | - FROM articles a | |
| 319 | + FROM articles.articles a | |
| 312 | 320 | WHERE a.feed_url = ? |
| 313 | 321 | ON CONFLICT(user_did, article_id) DO UPDATE SET |
| 314 | 322 | is_read = 1, read_at = CURRENT_TIMESTAMP |
| @@ -316,23 +324,23 @@ func (db *DB) MarkAllRead(ctx context.Context, userDID, feedURL string) error { | ||
| 316 | 324 | return err |
| 317 | 325 | } |
| 318 | 326 | |
| 319 | -func (db *DB) MarkAllSubscribedRead(ctx context.Context, userDID string) error { | |
| 320 | - _, err := db.ExecContext(ctx, ` | |
| 321 | - INSERT INTO read_state (user_did, article_id, is_read, read_at) | |
| 327 | +func (s *ArticleStore) MarkAllSubscribedRead(ctx context.Context, userDID string) error { | |
| 328 | + _, err := s.db.ExecContext(ctx, ` | |
| 329 | + INSERT INTO articles.read_state (user_did, article_id, is_read, read_at) | |
| 322 | 330 | SELECT ?, a.id, 1, CURRENT_TIMESTAMP |
| 323 | - FROM articles a | |
| 324 | - JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? | |
| 331 | + FROM articles.articles a | |
| 332 | + JOIN articles.subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? | |
| 325 | 333 | ON CONFLICT(user_did, article_id) DO UPDATE SET |
| 326 | 334 | is_read = 1, read_at = CURRENT_TIMESTAMP |
| 327 | 335 | `, userDID, userDID) |
| 328 | 336 | return err |
| 329 | 337 | } |
| 330 | 338 | |
| 331 | -func (db *DB) GetReadState(ctx context.Context, userDID string, articleID int64) (*ReadState, error) { | |
| 339 | +func (s *ArticleStore) GetReadState(ctx context.Context, userDID string, articleID int64) (*ReadState, error) { | |
| 332 | 340 | rs := &ReadState{} |
| 333 | - err := db.QueryRowContext(ctx, ` | |
| 341 | + err := s.db.QueryRowContext(ctx, ` | |
| 334 | 342 | SELECT user_did, article_id, is_read, read_at |
| 335 | - FROM read_state WHERE user_did = ? AND article_id = ? | |
| 343 | + FROM articles.read_state WHERE user_did = ? AND article_id = ? | |
| 336 | 344 | `, userDID, articleID).Scan(&rs.UserDID, &rs.ArticleID, &rs.IsRead, &rs.ReadAt) |
| 337 | 345 | if err == sql.ErrNoRows { |
| 338 | 346 | return &ReadState{UserDID: userDID, ArticleID: articleID}, nil |
| @@ -343,39 +351,39 @@ func (db *DB) GetReadState(ctx context.Context, userDID string, articleID int64) | ||
| 343 | 351 | return rs, nil |
| 344 | 352 | } |
| 345 | 353 | |
| 346 | -func (db *DB) GetUnreadCount(ctx context.Context, userDID, feedURL string) (int, error) { | |
| 354 | +func (s *ArticleStore) GetUnreadCount(ctx context.Context, userDID, feedURL string) (int, error) { | |
| 347 | 355 | var count int |
| 348 | 356 | if feedURL != "" { |
| 349 | - err := db.QueryRowContext(ctx, ` | |
| 357 | + err := s.db.QueryRowContext(ctx, ` | |
| 350 | 358 | SELECT COUNT(*) |
| 351 | - FROM articles a | |
| 352 | - LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id | |
| 359 | + FROM articles.articles a | |
| 360 | + LEFT JOIN articles.read_state r ON r.user_did = ? AND r.article_id = a.id | |
| 353 | 361 | WHERE a.feed_url = ? AND (r.is_read = 0 OR r.is_read IS NULL) |
| 354 | 362 | `, userDID, feedURL).Scan(&count) |
| 355 | 363 | return count, err |
| 356 | 364 | } |
| 357 | - err := db.QueryRowContext(ctx, ` | |
| 365 | + err := s.db.QueryRowContext(ctx, ` | |
| 358 | 366 | SELECT COUNT(*) |
| 359 | - FROM articles a | |
| 360 | - JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? | |
| 361 | - LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id | |
| 367 | + FROM articles.articles a | |
| 368 | + JOIN articles.subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? | |
| 369 | + LEFT JOIN articles.read_state r ON r.user_did = ? AND r.article_id = a.id | |
| 362 | 370 | WHERE r.is_read = 0 OR r.is_read IS NULL |
| 363 | 371 | `, userDID, userDID).Scan(&count) |
| 364 | 372 | return count, err |
| 365 | 373 | } |
| 366 | 374 | |
| 367 | -func (db *DB) UpdateArticleFullContent(ctx context.Context, id int64, fullContent string) error { | |
| 368 | - _, err := db.ExecContext(ctx, ` | |
| 369 | - UPDATE articles SET full_content = ? WHERE id = ? | |
| 375 | +func (s *ArticleStore) UpdateArticleFullContent(ctx context.Context, id int64, fullContent string) error { | |
| 376 | + _, err := s.db.ExecContext(ctx, ` | |
| 377 | + UPDATE articles.articles SET full_content = ? WHERE id = ? | |
| 370 | 378 | `, fullContent, id) |
| 371 | 379 | return err |
| 372 | 380 | } |
| 373 | 381 | |
| 374 | -func (db *DB) GetArticleByURL(ctx context.Context, url string) (*Article, error) { | |
| 382 | +func (s *ArticleStore) GetArticleByURL(ctx context.Context, url string) (*Article, error) { | |
| 375 | 383 | a := &Article{} |
| 376 | - err := db.QueryRowContext(ctx, ` | |
| 384 | + err := s.db.QueryRowContext(ctx, ` | |
| 377 | 385 | SELECT id, feed_url, guid, title, url, author, summary, content, full_content, published, updated, fetched_at |
| 378 | - FROM articles WHERE url = ? | |
| 386 | + FROM articles.articles WHERE url = ? | |
| 379 | 387 | LIMIT 1 |
| 380 | 388 | `, url).Scan(&a.ID, &a.FeedURL, &a.GUID, &a.Title, &a.URL, &a.Author, |
| 381 | 389 | &a.Summary, &a.Content, &a.FullContent, &a.Published, &a.Updated, &a.FetchedAt) |
| @@ -385,12 +393,12 @@ func (db *DB) GetArticleByURL(ctx context.Context, url string) (*Article, error) | ||
| 385 | 393 | return a, nil |
| 386 | 394 | } |
| 387 | 395 | |
| 388 | -func (db *DB) CountNewArticles(ctx context.Context, userDID string, since time.Time) (int, error) { | |
| 396 | +func (s *ArticleStore) CountNewArticles(ctx context.Context, userDID string, since time.Time) (int, error) { | |
| 389 | 397 | var count int |
| 390 | - err := db.QueryRowContext(ctx, ` | |
| 398 | + err := s.db.QueryRowContext(ctx, ` | |
| 391 | 399 | SELECT COUNT(*) |
| 392 | - FROM articles a | |
| 393 | - JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? | |
| 400 | + FROM articles.articles a | |
| 401 | + JOIN articles.subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? | |
| 394 | 402 | WHERE a.fetched_at > ? |
| 395 | 403 | `, userDID, since).Scan(&count) |
| 396 | 404 | return count, err |
| @@ -407,7 +415,7 @@ func escapeFTS5(query string) string { | ||
| 407 | 415 | return b.String() |
| 408 | 416 | } |
| 409 | 417 | |
| 410 | -func (db *DB) SearchArticles(ctx context.Context, userDID, query string, limit, offset int) ([]*Article, error) { | |
| 418 | +func (s *ArticleStore) SearchArticles(ctx context.Context, userDID, query string, limit, offset int) ([]*Article, error) { | |
| 411 | 419 | if strings.TrimSpace(query) == "" { |
| 412 | 420 | return nil, nil |
| 413 | 421 | } |
| @@ -419,25 +427,26 @@ func (db *DB) SearchArticles(ctx context.Context, userDID, query string, limit, | ||
| 419 | 427 | |
| 420 | 428 | columnQuery := "{title summary} : " + safeQuery |
| 421 | 429 | |
| 422 | - rows, err := db.QueryContext(ctx, ` | |
| 430 | + rows, err := s.db.QueryContext(ctx, ` | |
| 423 | 431 | SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content, |
| 424 | 432 | a.published, a.updated, a.fetched_at, |
| 425 | 433 | COALESCE(r.is_read, 0), |
| 426 | 434 | COALESCE(lc.cnt, 0), |
| 427 | 435 | COALESCE(ul.liked, 0) |
| 428 | - FROM articles_fts ft | |
| 429 | - JOIN articles a ON a.id = ft.rowid | |
| 430 | - JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? | |
| 431 | - LEFT JOIN feeds f ON a.feed_url = f.feed_url | |
| 432 | - LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id | |
| 433 | - LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc | |
| 436 | + FROM ( | |
| 437 | + SELECT rowid, rank FROM articles.articles_fts WHERE articles_fts MATCH ? | |
| 438 | + ) ft | |
| 439 | + JOIN articles.articles a ON a.id = ft.rowid | |
| 440 | + JOIN articles.subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? | |
| 441 | + LEFT JOIN articles.feeds f ON a.feed_url = f.feed_url | |
| 442 | + LEFT JOIN articles.read_state r ON r.user_did = ? AND r.article_id = a.id | |
| 443 | + LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM articles.likes GROUP BY feed_url, article_url) lc | |
| 434 | 444 | ON lc.feed_url = a.feed_url AND lc.article_url = a.url |
| 435 | - LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul | |
| 445 | + LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM articles.likes WHERE author_did = ?) ul | |
| 436 | 446 | ON ul.feed_url = a.feed_url AND ul.article_url = a.url |
| 437 | - WHERE articles_fts MATCH ? | |
| 438 | 447 | ORDER BY ft.rank |
| 439 | 448 | LIMIT ? OFFSET ? |
| 440 | - `, userDID, userDID, userDID, columnQuery, limit, offset) | |
| 449 | + `, columnQuery, userDID, userDID, userDID, limit, offset) | |
| 441 | 450 | if err != nil { |
| 442 | 451 | return nil, err |
| 443 | 452 | } |
| @@ -10,6 +10,14 @@ import ( | |||
| 10 | "pkg.rbrt.fr/glean/internal/feed" | 10 | "pkg.rbrt.fr/glean/internal/feed" |
| 11 | ) | 11 | ) |
| 12 | 12 | ||
| 13 | +type ArticleStore struct { | ||
| 14 | + db *DB | ||
| 15 | +} | ||
| 16 | + | ||
| 17 | +func NewArticleStore(db *DB) *ArticleStore { | ||
| 18 | + return &ArticleStore{db: db} | ||
| 19 | +} | ||
| 20 | + | ||
| 13 | type Article struct { | 21 | type Article struct { |
| 14 | ID int64 | 22 | ID int64 |
| 15 | FeedURL string | 23 | FeedURL string |
| @@ -37,19 +45,19 @@ type ReadState struct { | |||
| 37 | ReadAt sql.NullTime | 45 | ReadAt sql.NullTime |
| 38 | } | 46 | } |
| 39 | 47 | ||
| 40 | -func (db *DB) UpsertArticlesBatch(ctx context.Context, articles []feed.Article) error { | 48 | +func (s *ArticleStore) UpsertArticlesBatch(ctx context.Context, articles []feed.Article) error { |
| 41 | if len(articles) == 0 { | 49 | if len(articles) == 0 { |
| 42 | return nil | 50 | return nil |
| 43 | } | 51 | } |
| 44 | 52 | ||
| 45 | - tx, err := db.BeginTx(ctx, nil) | 53 | + tx, err := s.db.BeginTx(ctx, nil) |
| 46 | if err != nil { | 54 | if err != nil { |
| 47 | return err | 55 | return err |
| 48 | } | 56 | } |
| 49 | defer tx.Rollback() | 57 | defer tx.Rollback() |
| 50 | 58 | ||
| 51 | stmt, err := tx.PrepareContext(ctx, ` | 59 | stmt, err := tx.PrepareContext(ctx, ` |
| 52 | - INSERT INTO articles (feed_url, guid, title, url, author, summary, content, published, updated) | 60 | + INSERT INTO articles.articles (feed_url, guid, title, url, author, summary, content, published, updated) |
| 53 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) | 61 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) |
| 54 | ON CONFLICT(feed_url, guid) DO NOTHING | 62 | ON CONFLICT(feed_url, guid) DO NOTHING |
| 55 | `) | 63 | `) |
| @@ -79,11 +87,11 @@ func (db *DB) UpsertArticlesBatch(ctx context.Context, articles []feed.Article) | |||
| 79 | return tx.Commit() | 87 | return tx.Commit() |
| 80 | } | 88 | } |
| 81 | 89 | ||
| 82 | -func (db *DB) GetArticle(ctx context.Context, id int64) (*Article, error) { | 90 | +func (s *ArticleStore) GetArticle(ctx context.Context, id int64) (*Article, error) { |
| 83 | a := &Article{} | 91 | a := &Article{} |
| 84 | - err := db.QueryRowContext(ctx, ` | 92 | + err := s.db.QueryRowContext(ctx, ` |
| 85 | SELECT id, feed_url, guid, title, url, author, summary, content, full_content, published, updated, fetched_at | 93 | SELECT id, feed_url, guid, title, url, author, summary, content, full_content, published, updated, fetched_at |
| 86 | - FROM articles WHERE id = ? | 94 | + FROM articles.articles WHERE id = ? |
| 87 | `, id).Scan(&a.ID, &a.FeedURL, &a.GUID, &a.Title, &a.URL, &a.Author, | 95 | `, id).Scan(&a.ID, &a.FeedURL, &a.GUID, &a.Title, &a.URL, &a.Author, |
| 88 | &a.Summary, &a.Content, &a.FullContent, &a.Published, &a.Updated, &a.FetchedAt) | 96 | &a.Summary, &a.Content, &a.FullContent, &a.Published, &a.Updated, &a.FetchedAt) |
| 89 | if err != nil { | 97 | if err != nil { |
| @@ -92,7 +100,7 @@ func (db *DB) GetArticle(ctx context.Context, id int64) (*Article, error) { | |||
| 92 | return a, nil | 100 | return a, nil |
| 93 | } | 101 | } |
| 94 | 102 | ||
| 95 | -func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit, offset int) ([]*Article, error) { | 103 | +func (s *ArticleStore) ListArticles(ctx context.Context, userDID, feedURL string, limit, offset int) ([]*Article, error) { |
| 96 | var query string | 104 | var query string |
| 97 | var args []any | 105 | var args []any |
| 98 | 106 | ||
| @@ -103,12 +111,12 @@ func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit, | |||
| 103 | COALESCE(r.is_read, 0), | 111 | COALESCE(r.is_read, 0), |
| 104 | COALESCE(lc.cnt, 0), | 112 | COALESCE(lc.cnt, 0), |
| 105 | COALESCE(ul.liked, 0) | 113 | COALESCE(ul.liked, 0) |
| 106 | - FROM articles a | 114 | + FROM articles.articles a |
| 107 | - LEFT JOIN feeds f ON a.feed_url = f.feed_url | 115 | + LEFT JOIN articles.feeds f ON a.feed_url = f.feed_url |
| 108 | - LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id | 116 | + LEFT JOIN articles.read_state r ON r.user_did = ? AND r.article_id = a.id |
| 109 | - LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc | 117 | + LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM articles.likes GROUP BY feed_url, article_url) lc |
| 110 | ON lc.feed_url = a.feed_url AND lc.article_url = a.url | 118 | ON lc.feed_url = a.feed_url AND lc.article_url = a.url |
| 111 | - LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul | 119 | + LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM articles.likes WHERE author_did = ?) ul |
| 112 | ON ul.feed_url = a.feed_url AND ul.article_url = a.url | 120 | ON ul.feed_url = a.feed_url AND ul.article_url = a.url |
| 113 | WHERE a.feed_url = ? | 121 | WHERE a.feed_url = ? |
| 114 | ` | 122 | ` |
| @@ -120,13 +128,13 @@ func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit, | |||
| 120 | COALESCE(r.is_read, 0), | 128 | COALESCE(r.is_read, 0), |
| 121 | COALESCE(lc.cnt, 0), | 129 | COALESCE(lc.cnt, 0), |
| 122 | COALESCE(ul.liked, 0) | 130 | COALESCE(ul.liked, 0) |
| 123 | - FROM articles a | 131 | + FROM articles.articles a |
| 124 | - JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? | 132 | + JOIN articles.subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? |
| 125 | - LEFT JOIN feeds f ON a.feed_url = f.feed_url | 133 | + LEFT JOIN articles.feeds f ON a.feed_url = f.feed_url |
| 126 | - LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id | 134 | + LEFT JOIN articles.read_state r ON r.user_did = ? AND r.article_id = a.id |
| 127 | - LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc | 135 | + LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM articles.likes GROUP BY feed_url, article_url) lc |
| 128 | ON lc.feed_url = a.feed_url AND lc.article_url = a.url | 136 | ON lc.feed_url = a.feed_url AND lc.article_url = a.url |
| 129 | - LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul | 137 | + LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM articles.likes WHERE author_did = ?) ul |
| 130 | ON ul.feed_url = a.feed_url AND ul.article_url = a.url | 138 | ON ul.feed_url = a.feed_url AND ul.article_url = a.url |
| 131 | WHERE 1=1 | 139 | WHERE 1=1 |
| 132 | ` | 140 | ` |
| @@ -137,7 +145,7 @@ func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit, | |||
| 137 | query += ` ORDER BY (CASE WHEN a.published > 'now' THEN 1 ELSE 0 END), a.published DESC LIMIT ? OFFSET ?` | 145 | query += ` ORDER BY (CASE WHEN a.published > 'now' THEN 1 ELSE 0 END), a.published DESC LIMIT ? OFFSET ?` |
| 138 | args = append(args, limit, offset) | 146 | args = append(args, limit, offset) |
| 139 | 147 | ||
| 140 | - rows, err := db.QueryContext(ctx, query, args...) | 148 | + rows, err := s.db.QueryContext(ctx, query, args...) |
| 141 | if err != nil { | 149 | if err != nil { |
| 142 | return nil, err | 150 | return nil, err |
| 143 | } | 151 | } |
| @@ -156,7 +164,7 @@ func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit, | |||
| 156 | return articles, rows.Err() | 164 | return articles, rows.Err() |
| 157 | } | 165 | } |
| 158 | 166 | ||
| 159 | -func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, limit, offset int) ([]*Article, error) { | 167 | +func (s *ArticleStore) ListUnreadArticles(ctx context.Context, userDID, feedURL string, limit, offset int) ([]*Article, error) { |
| 160 | var query string | 168 | var query string |
| 161 | var args []any | 169 | var args []any |
| 162 | 170 | ||
| @@ -167,12 +175,12 @@ func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, l | |||
| 167 | COALESCE(r.is_read, 0), | 175 | COALESCE(r.is_read, 0), |
| 168 | COALESCE(lc.cnt, 0), | 176 | COALESCE(lc.cnt, 0), |
| 169 | COALESCE(ul.liked, 0) | 177 | COALESCE(ul.liked, 0) |
| 170 | - FROM articles a | 178 | + FROM articles.articles a |
| 171 | - LEFT JOIN feeds f ON a.feed_url = f.feed_url | 179 | + LEFT JOIN articles.feeds f ON a.feed_url = f.feed_url |
| 172 | - LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id | 180 | + LEFT JOIN articles.read_state r ON r.user_did = ? AND r.article_id = a.id |
| 173 | - LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc | 181 | + LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM articles.likes GROUP BY feed_url, article_url) lc |
| 174 | ON lc.feed_url = a.feed_url AND lc.article_url = a.url | 182 | ON lc.feed_url = a.feed_url AND lc.article_url = a.url |
| 175 | - LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul | 183 | + LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM articles.likes WHERE author_did = ?) ul |
| 176 | ON ul.feed_url = a.feed_url AND ul.article_url = a.url | 184 | ON ul.feed_url = a.feed_url AND ul.article_url = a.url |
| 177 | WHERE a.feed_url = ? AND (r.is_read = 0 OR r.is_read IS NULL) | 185 | WHERE a.feed_url = ? AND (r.is_read = 0 OR r.is_read IS NULL) |
| 178 | ` | 186 | ` |
| @@ -184,13 +192,13 @@ func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, l | |||
| 184 | COALESCE(r.is_read, 0), | 192 | COALESCE(r.is_read, 0), |
| 185 | COALESCE(lc.cnt, 0), | 193 | COALESCE(lc.cnt, 0), |
| 186 | COALESCE(ul.liked, 0) | 194 | COALESCE(ul.liked, 0) |
| 187 | - FROM articles a | 195 | + FROM articles.articles a |
| 188 | - JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? | 196 | + JOIN articles.subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? |
| 189 | - LEFT JOIN feeds f ON a.feed_url = f.feed_url | 197 | + LEFT JOIN articles.feeds f ON a.feed_url = f.feed_url |
| 190 | - LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id | 198 | + LEFT JOIN articles.read_state r ON r.user_did = ? AND r.article_id = a.id |
| 191 | - LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc | 199 | + LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM articles.likes GROUP BY feed_url, article_url) lc |
| 192 | ON lc.feed_url = a.feed_url AND lc.article_url = a.url | 200 | ON lc.feed_url = a.feed_url AND lc.article_url = a.url |
| 193 | - LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul | 201 | + LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM articles.likes WHERE author_did = ?) ul |
| 194 | ON ul.feed_url = a.feed_url AND ul.article_url = a.url | 202 | ON ul.feed_url = a.feed_url AND ul.article_url = a.url |
| 195 | WHERE (r.is_read = 0 OR r.is_read IS NULL) | 203 | WHERE (r.is_read = 0 OR r.is_read IS NULL) |
| 196 | ` | 204 | ` |
| @@ -201,7 +209,7 @@ func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, l | |||
| 201 | query += ` ORDER BY (CASE WHEN a.published > 'now' THEN 1 ELSE 0 END), a.published DESC LIMIT ? OFFSET ?` | 209 | query += ` ORDER BY (CASE WHEN a.published > 'now' THEN 1 ELSE 0 END), a.published DESC LIMIT ? OFFSET ?` |
| 202 | args = append(args, limit, offset) | 210 | args = append(args, limit, offset) |
| 203 | 211 | ||
| 204 | - rows, err := db.QueryContext(ctx, query, args...) | 212 | + rows, err := s.db.QueryContext(ctx, query, args...) |
| 205 | if err != nil { | 213 | if err != nil { |
| 206 | return nil, err | 214 | return nil, err |
| 207 | } | 215 | } |
| @@ -220,7 +228,7 @@ func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, l | |||
| 220 | return articles, rows.Err() | 228 | return articles, rows.Err() |
| 221 | } | 229 | } |
| 222 | 230 | ||
| 223 | -func (db *DB) ListReadArticles(ctx context.Context, userDID, feedURL string, limit, offset int) ([]*Article, error) { | 231 | +func (s *ArticleStore) ListReadArticles(ctx context.Context, userDID, feedURL string, limit, offset int) ([]*Article, error) { |
| 224 | var query string | 232 | var query string |
| 225 | var args []any | 233 | var args []any |
| 226 | 234 | ||
| @@ -231,12 +239,12 @@ func (db *DB) ListReadArticles(ctx context.Context, userDID, feedURL string, lim | |||
| 231 | COALESCE(r.is_read, 0), | 239 | COALESCE(r.is_read, 0), |
| 232 | COALESCE(lc.cnt, 0), | 240 | COALESCE(lc.cnt, 0), |
| 233 | COALESCE(ul.liked, 0) | 241 | COALESCE(ul.liked, 0) |
| 234 | - FROM articles a | 242 | + FROM articles.articles a |
| 235 | - LEFT JOIN feeds f ON a.feed_url = f.feed_url | 243 | + LEFT JOIN articles.feeds f ON a.feed_url = f.feed_url |
| 236 | - JOIN read_state r ON r.user_did = ? AND r.article_id = a.id | 244 | + JOIN articles.read_state r ON r.user_did = ? AND r.article_id = a.id |
| 237 | - LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc | 245 | + LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM articles.likes GROUP BY feed_url, article_url) lc |
| 238 | ON lc.feed_url = a.feed_url AND lc.article_url = a.url | 246 | ON lc.feed_url = a.feed_url AND lc.article_url = a.url |
| 239 | - LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul | 247 | + LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM articles.likes WHERE author_did = ?) ul |
| 240 | ON ul.feed_url = a.feed_url AND ul.article_url = a.url | 248 | ON ul.feed_url = a.feed_url AND ul.article_url = a.url |
| 241 | WHERE r.is_read = 1 AND a.feed_url = ? | 249 | WHERE r.is_read = 1 AND a.feed_url = ? |
| 242 | ` | 250 | ` |
| @@ -248,13 +256,13 @@ func (db *DB) ListReadArticles(ctx context.Context, userDID, feedURL string, lim | |||
| 248 | COALESCE(r.is_read, 0), | 256 | COALESCE(r.is_read, 0), |
| 249 | COALESCE(lc.cnt, 0), | 257 | COALESCE(lc.cnt, 0), |
| 250 | COALESCE(ul.liked, 0) | 258 | COALESCE(ul.liked, 0) |
| 251 | - FROM articles a | 259 | + FROM articles.articles a |
| 252 | - JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? | 260 | + JOIN articles.subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? |
| 253 | - LEFT JOIN feeds f ON a.feed_url = f.feed_url | 261 | + LEFT JOIN articles.feeds f ON a.feed_url = f.feed_url |
| 254 | - JOIN read_state r ON r.user_did = ? AND r.article_id = a.id | 262 | + JOIN articles.read_state r ON r.user_did = ? AND r.article_id = a.id |
| 255 | - LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc | 263 | + LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM articles.likes GROUP BY feed_url, article_url) lc |
| 256 | ON lc.feed_url = a.feed_url AND lc.article_url = a.url | 264 | ON lc.feed_url = a.feed_url AND lc.article_url = a.url |
| 257 | - LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul | 265 | + LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM articles.likes WHERE author_did = ?) ul |
| 258 | ON ul.feed_url = a.feed_url AND ul.article_url = a.url | 266 | ON ul.feed_url = a.feed_url AND ul.article_url = a.url |
| 259 | WHERE r.is_read = 1 | 267 | WHERE r.is_read = 1 |
| 260 | ` | 268 | ` |
| @@ -265,7 +273,7 @@ func (db *DB) ListReadArticles(ctx context.Context, userDID, feedURL string, lim | |||
| 265 | query += ` ORDER BY (CASE WHEN a.published > 'now' THEN 1 ELSE 0 END), a.published DESC LIMIT ? OFFSET ?` | 273 | query += ` ORDER BY (CASE WHEN a.published > 'now' THEN 1 ELSE 0 END), a.published DESC LIMIT ? OFFSET ?` |
| 266 | args = append(args, limit, offset) | 274 | args = append(args, limit, offset) |
| 267 | 275 | ||
| 268 | - rows, err := db.QueryContext(ctx, query, args...) | 276 | + rows, err := s.db.QueryContext(ctx, query, args...) |
| 269 | if err != nil { | 277 | if err != nil { |
| 270 | return nil, err | 278 | return nil, err |
| 271 | } | 279 | } |
| @@ -284,9 +292,9 @@ func (db *DB) ListReadArticles(ctx context.Context, userDID, feedURL string, lim | |||
| 284 | return articles, rows.Err() | 292 | return articles, rows.Err() |
| 285 | } | 293 | } |
| 286 | 294 | ||
| 287 | -func (db *DB) MarkArticleRead(ctx context.Context, userDID string, articleID int64) error { | 295 | +func (s *ArticleStore) MarkArticleRead(ctx context.Context, userDID string, articleID int64) error { |
| 288 | - _, err := db.ExecContext(ctx, ` | 296 | + _, err := s.db.ExecContext(ctx, ` |
| 289 | - INSERT INTO read_state (user_did, article_id, is_read, read_at) | 297 | + INSERT INTO articles.read_state (user_did, article_id, is_read, read_at) |
| 290 | VALUES (?, ?, 1, CURRENT_TIMESTAMP) | 298 | VALUES (?, ?, 1, CURRENT_TIMESTAMP) |
| 291 | ON CONFLICT(user_did, article_id) DO UPDATE SET | 299 | ON CONFLICT(user_did, article_id) DO UPDATE SET |
| 292 | is_read = 1, read_at = CURRENT_TIMESTAMP | 300 | is_read = 1, read_at = CURRENT_TIMESTAMP |
| @@ -294,9 +302,9 @@ func (db *DB) MarkArticleRead(ctx context.Context, userDID string, articleID int | |||
| 294 | return err | 302 | return err |
| 295 | } | 303 | } |
| 296 | 304 | ||
| 297 | -func (db *DB) MarkArticleUnread(ctx context.Context, userDID string, articleID int64) error { | 305 | +func (s *ArticleStore) MarkArticleUnread(ctx context.Context, userDID string, articleID int64) error { |
| 298 | - _, err := db.ExecContext(ctx, ` | 306 | + _, err := s.db.ExecContext(ctx, ` |
| 299 | - INSERT INTO read_state (user_did, article_id, is_read) | 307 | + INSERT INTO articles.read_state (user_did, article_id, is_read) |
| 300 | VALUES (?, ?, 0) | 308 | VALUES (?, ?, 0) |
| 301 | ON CONFLICT(user_did, article_id) DO UPDATE SET | 309 | ON CONFLICT(user_did, article_id) DO UPDATE SET |
| 302 | is_read = 0, read_at = NULL | 310 | is_read = 0, read_at = NULL |
| @@ -304,11 +312,11 @@ func (db *DB) MarkArticleUnread(ctx context.Context, userDID string, articleID i | |||
| 304 | return err | 312 | return err |
| 305 | } | 313 | } |
| 306 | 314 | ||
| 307 | -func (db *DB) MarkAllRead(ctx context.Context, userDID, feedURL string) error { | 315 | +func (s *ArticleStore) MarkAllRead(ctx context.Context, userDID, feedURL string) error { |
| 308 | - _, err := db.ExecContext(ctx, ` | 316 | + _, err := s.db.ExecContext(ctx, ` |
| 309 | - INSERT INTO read_state (user_did, article_id, is_read, read_at) | 317 | + INSERT INTO articles.read_state (user_did, article_id, is_read, read_at) |
| 310 | SELECT ?, a.id, 1, CURRENT_TIMESTAMP | 318 | SELECT ?, a.id, 1, CURRENT_TIMESTAMP |
| 311 | - FROM articles a | 319 | + FROM articles.articles a |
| 312 | WHERE a.feed_url = ? | 320 | WHERE a.feed_url = ? |
| 313 | ON CONFLICT(user_did, article_id) DO UPDATE SET | 321 | ON CONFLICT(user_did, article_id) DO UPDATE SET |
| 314 | is_read = 1, read_at = CURRENT_TIMESTAMP | 322 | is_read = 1, read_at = CURRENT_TIMESTAMP |
| @@ -316,23 +324,23 @@ func (db *DB) MarkAllRead(ctx context.Context, userDID, feedURL string) error { | |||
| 316 | return err | 324 | return err |
| 317 | } | 325 | } |
| 318 | 326 | ||
| 319 | -func (db *DB) MarkAllSubscribedRead(ctx context.Context, userDID string) error { | 327 | +func (s *ArticleStore) MarkAllSubscribedRead(ctx context.Context, userDID string) error { |
| 320 | - _, err := db.ExecContext(ctx, ` | 328 | + _, err := s.db.ExecContext(ctx, ` |
| 321 | - INSERT INTO read_state (user_did, article_id, is_read, read_at) | 329 | + INSERT INTO articles.read_state (user_did, article_id, is_read, read_at) |
| 322 | SELECT ?, a.id, 1, CURRENT_TIMESTAMP | 330 | SELECT ?, a.id, 1, CURRENT_TIMESTAMP |
| 323 | - FROM articles a | 331 | + FROM articles.articles a |
| 324 | - JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? | 332 | + JOIN articles.subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? |
| 325 | ON CONFLICT(user_did, article_id) DO UPDATE SET | 333 | ON CONFLICT(user_did, article_id) DO UPDATE SET |
| 326 | is_read = 1, read_at = CURRENT_TIMESTAMP | 334 | is_read = 1, read_at = CURRENT_TIMESTAMP |
| 327 | `, userDID, userDID) | 335 | `, userDID, userDID) |
| 328 | return err | 336 | return err |
| 329 | } | 337 | } |
| 330 | 338 | ||
| 331 | -func (db *DB) GetReadState(ctx context.Context, userDID string, articleID int64) (*ReadState, error) { | 339 | +func (s *ArticleStore) GetReadState(ctx context.Context, userDID string, articleID int64) (*ReadState, error) { |
| 332 | rs := &ReadState{} | 340 | rs := &ReadState{} |
| 333 | - err := db.QueryRowContext(ctx, ` | 341 | + err := s.db.QueryRowContext(ctx, ` |
| 334 | SELECT user_did, article_id, is_read, read_at | 342 | SELECT user_did, article_id, is_read, read_at |
| 335 | - FROM read_state WHERE user_did = ? AND article_id = ? | 343 | + FROM articles.read_state WHERE user_did = ? AND article_id = ? |
| 336 | `, userDID, articleID).Scan(&rs.UserDID, &rs.ArticleID, &rs.IsRead, &rs.ReadAt) | 344 | `, userDID, articleID).Scan(&rs.UserDID, &rs.ArticleID, &rs.IsRead, &rs.ReadAt) |
| 337 | if err == sql.ErrNoRows { | 345 | if err == sql.ErrNoRows { |
| 338 | return &ReadState{UserDID: userDID, ArticleID: articleID}, nil | 346 | return &ReadState{UserDID: userDID, ArticleID: articleID}, nil |
| @@ -343,39 +351,39 @@ func (db *DB) GetReadState(ctx context.Context, userDID string, articleID int64) | |||
| 343 | return rs, nil | 351 | return rs, nil |
| 344 | } | 352 | } |
| 345 | 353 | ||
| 346 | -func (db *DB) GetUnreadCount(ctx context.Context, userDID, feedURL string) (int, error) { | 354 | +func (s *ArticleStore) GetUnreadCount(ctx context.Context, userDID, feedURL string) (int, error) { |
| 347 | var count int | 355 | var count int |
| 348 | if feedURL != "" { | 356 | if feedURL != "" { |
| 349 | - err := db.QueryRowContext(ctx, ` | 357 | + err := s.db.QueryRowContext(ctx, ` |
| 350 | SELECT COUNT(*) | 358 | SELECT COUNT(*) |
| 351 | - FROM articles a | 359 | + FROM articles.articles a |
| 352 | - LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id | 360 | + LEFT JOIN articles.read_state r ON r.user_did = ? AND r.article_id = a.id |
| 353 | WHERE a.feed_url = ? AND (r.is_read = 0 OR r.is_read IS NULL) | 361 | WHERE a.feed_url = ? AND (r.is_read = 0 OR r.is_read IS NULL) |
| 354 | `, userDID, feedURL).Scan(&count) | 362 | `, userDID, feedURL).Scan(&count) |
| 355 | return count, err | 363 | return count, err |
| 356 | } | 364 | } |
| 357 | - err := db.QueryRowContext(ctx, ` | 365 | + err := s.db.QueryRowContext(ctx, ` |
| 358 | SELECT COUNT(*) | 366 | SELECT COUNT(*) |
| 359 | - FROM articles a | 367 | + FROM articles.articles a |
| 360 | - JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? | 368 | + JOIN articles.subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? |
| 361 | - LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id | 369 | + LEFT JOIN articles.read_state r ON r.user_did = ? AND r.article_id = a.id |
| 362 | WHERE r.is_read = 0 OR r.is_read IS NULL | 370 | WHERE r.is_read = 0 OR r.is_read IS NULL |
| 363 | `, userDID, userDID).Scan(&count) | 371 | `, userDID, userDID).Scan(&count) |
| 364 | return count, err | 372 | return count, err |
| 365 | } | 373 | } |
| 366 | 374 | ||
| 367 | -func (db *DB) UpdateArticleFullContent(ctx context.Context, id int64, fullContent string) error { | 375 | +func (s *ArticleStore) UpdateArticleFullContent(ctx context.Context, id int64, fullContent string) error { |
| 368 | - _, err := db.ExecContext(ctx, ` | 376 | + _, err := s.db.ExecContext(ctx, ` |
| 369 | - UPDATE articles SET full_content = ? WHERE id = ? | 377 | + UPDATE articles.articles SET full_content = ? WHERE id = ? |
| 370 | `, fullContent, id) | 378 | `, fullContent, id) |
| 371 | return err | 379 | return err |
| 372 | } | 380 | } |
| 373 | 381 | ||
| 374 | -func (db *DB) GetArticleByURL(ctx context.Context, url string) (*Article, error) { | 382 | +func (s *ArticleStore) GetArticleByURL(ctx context.Context, url string) (*Article, error) { |
| 375 | a := &Article{} | 383 | a := &Article{} |
| 376 | - err := db.QueryRowContext(ctx, ` | 384 | + err := s.db.QueryRowContext(ctx, ` |
| 377 | SELECT id, feed_url, guid, title, url, author, summary, content, full_content, published, updated, fetched_at | 385 | SELECT id, feed_url, guid, title, url, author, summary, content, full_content, published, updated, fetched_at |
| 378 | - FROM articles WHERE url = ? | 386 | + FROM articles.articles WHERE url = ? |
| 379 | LIMIT 1 | 387 | LIMIT 1 |
| 380 | `, url).Scan(&a.ID, &a.FeedURL, &a.GUID, &a.Title, &a.URL, &a.Author, | 388 | `, url).Scan(&a.ID, &a.FeedURL, &a.GUID, &a.Title, &a.URL, &a.Author, |
| 381 | &a.Summary, &a.Content, &a.FullContent, &a.Published, &a.Updated, &a.FetchedAt) | 389 | &a.Summary, &a.Content, &a.FullContent, &a.Published, &a.Updated, &a.FetchedAt) |
| @@ -385,12 +393,12 @@ func (db *DB) GetArticleByURL(ctx context.Context, url string) (*Article, error) | |||
| 385 | return a, nil | 393 | return a, nil |
| 386 | } | 394 | } |
| 387 | 395 | ||
| 388 | -func (db *DB) CountNewArticles(ctx context.Context, userDID string, since time.Time) (int, error) { | 396 | +func (s *ArticleStore) CountNewArticles(ctx context.Context, userDID string, since time.Time) (int, error) { |
| 389 | var count int | 397 | var count int |
| 390 | - err := db.QueryRowContext(ctx, ` | 398 | + err := s.db.QueryRowContext(ctx, ` |
| 391 | SELECT COUNT(*) | 399 | SELECT COUNT(*) |
| 392 | - FROM articles a | 400 | + FROM articles.articles a |
| 393 | - JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? | 401 | + JOIN articles.subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? |
| 394 | WHERE a.fetched_at > ? | 402 | WHERE a.fetched_at > ? |
| 395 | `, userDID, since).Scan(&count) | 403 | `, userDID, since).Scan(&count) |
| 396 | return count, err | 404 | return count, err |
| @@ -407,7 +415,7 @@ func escapeFTS5(query string) string { | |||
| 407 | return b.String() | 415 | return b.String() |
| 408 | } | 416 | } |
| 409 | 417 | ||
| 410 | -func (db *DB) SearchArticles(ctx context.Context, userDID, query string, limit, offset int) ([]*Article, error) { | 418 | +func (s *ArticleStore) SearchArticles(ctx context.Context, userDID, query string, limit, offset int) ([]*Article, error) { |
| 411 | if strings.TrimSpace(query) == "" { | 419 | if strings.TrimSpace(query) == "" { |
| 412 | return nil, nil | 420 | return nil, nil |
| 413 | } | 421 | } |
| @@ -419,25 +427,26 @@ func (db *DB) SearchArticles(ctx context.Context, userDID, query string, limit, | |||
| 419 | 427 | ||
| 420 | columnQuery := "{title summary} : " + safeQuery | 428 | columnQuery := "{title summary} : " + safeQuery |
| 421 | 429 | ||
| 422 | - rows, err := db.QueryContext(ctx, ` | 430 | + rows, err := s.db.QueryContext(ctx, ` |
| 423 | SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content, | 431 | SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content, |
| 424 | a.published, a.updated, a.fetched_at, | 432 | a.published, a.updated, a.fetched_at, |
| 425 | COALESCE(r.is_read, 0), | 433 | COALESCE(r.is_read, 0), |
| 426 | COALESCE(lc.cnt, 0), | 434 | COALESCE(lc.cnt, 0), |
| 427 | COALESCE(ul.liked, 0) | 435 | COALESCE(ul.liked, 0) |
| 428 | - FROM articles_fts ft | 436 | + FROM ( |
| 429 | - JOIN articles a ON a.id = ft.rowid | 437 | + SELECT rowid, rank FROM articles.articles_fts WHERE articles_fts MATCH ? |
| 430 | - JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? | 438 | + ) ft |
| 431 | - LEFT JOIN feeds f ON a.feed_url = f.feed_url | 439 | + JOIN articles.articles a ON a.id = ft.rowid |
| 432 | - LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id | 440 | + JOIN articles.subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? |
| 433 | - LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc | 441 | + LEFT JOIN articles.feeds f ON a.feed_url = f.feed_url |
| 442 | + LEFT JOIN articles.read_state r ON r.user_did = ? AND r.article_id = a.id | ||
| 443 | + LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM articles.likes GROUP BY feed_url, article_url) lc | ||
| 434 | ON lc.feed_url = a.feed_url AND lc.article_url = a.url | 444 | ON lc.feed_url = a.feed_url AND lc.article_url = a.url |
| 435 | - LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul | 445 | + LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM articles.likes WHERE author_did = ?) ul |
| 436 | ON ul.feed_url = a.feed_url AND ul.article_url = a.url | 446 | ON ul.feed_url = a.feed_url AND ul.article_url = a.url |
| 437 | - WHERE articles_fts MATCH ? | ||
| 438 | ORDER BY ft.rank | 447 | ORDER BY ft.rank |
| 439 | LIMIT ? OFFSET ? | 448 | LIMIT ? OFFSET ? |
| 440 | - `, userDID, userDID, userDID, columnQuery, limit, offset) | 449 | + `, columnQuery, userDID, userDID, userDID, limit, offset) |
| 441 | if err != nil { | 450 | if err != nil { |
| 442 | return nil, err | 451 | return nil, err |
| 443 | } | 452 | } |
modified
internal/db/article_test.go +103 -99 | @@ -9,46 +9,50 @@ import ( | ||
| 9 | 9 | "gotest.tools/v3/assert" |
| 10 | 10 | ) |
| 11 | 11 | |
| 12 | -func setupTestDB(t *testing.T) *DB { | |
| 12 | +func setupTestDB(t *testing.T) *Databases { | |
| 13 | 13 | t.Helper() |
| 14 | 14 | f, err := os.CreateTemp("", "glean-test-*.db") |
| 15 | 15 | assert.NilError(t, err) |
| 16 | 16 | assert.NilError(t, f.Close()) |
| 17 | 17 | path := f.Name() |
| 18 | - t.Cleanup(func() { _ = os.Remove(path) }) | |
| 18 | + t.Cleanup(func() { | |
| 19 | + for _, suffix := range []string{"", "_users", "_users-shm", "_users-wal", "_articles", "_articles-shm", "_articles-wal", "_recs", "_recs-shm", "_recs-wal"} { | |
| 20 | + _ = os.Remove(path + suffix) | |
| 21 | + } | |
| 22 | + }) | |
| 19 | 23 | |
| 20 | - db, err := Open(path) | |
| 24 | + dbs, err := OpenAll(path) | |
| 21 | 25 | assert.NilError(t, err) |
| 22 | - t.Cleanup(func() { _ = db.Close() }) | |
| 23 | - return db | |
| 26 | + t.Cleanup(func() { _ = dbs.Close() }) | |
| 27 | + return dbs | |
| 24 | 28 | } |
| 25 | 29 | |
| 26 | -func seedArticleReadState(t *testing.T, ctx context.Context, db *DB) (userDID string, feedURL string, readArticleID, unreadArticleID int64) { | |
| 30 | +func seedArticleReadState(t *testing.T, ctx context.Context, dbs *Databases) (userDID string, feedURL string, readArticleID, unreadArticleID int64) { | |
| 27 | 31 | t.Helper() |
| 28 | 32 | |
| 29 | 33 | userDID = "did:test:user1" |
| 30 | 34 | feedURL = "https://example.com/feed.xml" |
| 31 | 35 | |
| 32 | - _, err := db.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, userDID, "user1") | |
| 36 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, userDID, "user1") | |
| 33 | 37 | assert.NilError(t, err) |
| 34 | 38 | |
| 35 | - _, err = db.ExecContext(ctx, `INSERT INTO feeds (feed_url, title) VALUES (?, ?)`, feedURL, "Test Feed") | |
| 39 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.feeds (feed_url, title) VALUES (?, ?)`, feedURL, "Test Feed") | |
| 36 | 40 | assert.NilError(t, err) |
| 37 | 41 | |
| 38 | - _, err = db.ExecContext(ctx, `INSERT INTO subscriptions (user_did, feed_url) VALUES (?, ?)`, userDID, feedURL) | |
| 42 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.subscriptions (user_did, feed_url) VALUES (?, ?)`, userDID, feedURL) | |
| 39 | 43 | assert.NilError(t, err) |
| 40 | 44 | |
| 41 | - res, err := db.ExecContext(ctx, `INSERT INTO articles (feed_url, guid, title, url) VALUES (?, ?, ?, ?)`, | |
| 45 | + res, err := dbs.DB().ExecContext(ctx, `INSERT INTO articles.articles (feed_url, guid, title, url) VALUES (?, ?, ?, ?)`, | |
| 42 | 46 | feedURL, "guid-read", "Read Article", "https://example.com/read") |
| 43 | 47 | assert.NilError(t, err) |
| 44 | 48 | readArticleID, _ = res.LastInsertId() |
| 45 | 49 | |
| 46 | - res, err = db.ExecContext(ctx, `INSERT INTO articles (feed_url, guid, title, url) VALUES (?, ?, ?, ?)`, | |
| 50 | + res, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.articles (feed_url, guid, title, url) VALUES (?, ?, ?, ?)`, | |
| 47 | 51 | feedURL, "guid-unread", "Unread Article", "https://example.com/unread") |
| 48 | 52 | assert.NilError(t, err) |
| 49 | 53 | unreadArticleID, _ = res.LastInsertId() |
| 50 | 54 | |
| 51 | - err = db.MarkArticleRead(ctx, userDID, readArticleID) | |
| 55 | + err = dbs.Articles.MarkArticleRead(ctx, userDID, readArticleID) | |
| 52 | 56 | assert.NilError(t, err) |
| 53 | 57 | |
| 54 | 58 | return userDID, feedURL, readArticleID, unreadArticleID |
| @@ -56,121 +60,121 @@ func seedArticleReadState(t *testing.T, ctx context.Context, db *DB) (userDID st | ||
| 56 | 60 | |
| 57 | 61 | func TestListReadArticles_ReturnsOnlyRead(t *testing.T) { |
| 58 | 62 | ctx := context.Background() |
| 59 | - db := setupTestDB(t) | |
| 60 | - userDID, feedURL, readID, unreadID := seedArticleReadState(t, ctx, db) | |
| 63 | + dbs := setupTestDB(t) | |
| 64 | + userDID, feedURL, readID, unreadID := seedArticleReadState(t, ctx, dbs) | |
| 61 | 65 | |
| 62 | - articles, err := db.ListReadArticles(ctx, userDID, feedURL, 10, 0) | |
| 66 | + results, err := dbs.Articles.ListReadArticles(ctx, userDID, feedURL, 10, 0) | |
| 63 | 67 | assert.NilError(t, err) |
| 64 | - assert.Equal(t, len(articles), 1) | |
| 65 | - assert.Equal(t, articles[0].ID, readID) | |
| 66 | - assert.Equal(t, articles[0].IsRead, sql.NullBool{Bool: true, Valid: true}) | |
| 68 | + assert.Equal(t, len(results), 1) | |
| 69 | + assert.Equal(t, results[0].ID, readID) | |
| 70 | + assert.Equal(t, results[0].IsRead, sql.NullBool{Bool: true, Valid: true}) | |
| 67 | 71 | |
| 68 | 72 | _ = unreadID |
| 69 | 73 | } |
| 70 | 74 | |
| 71 | 75 | func TestListReadArticles_ExcludesUnread(t *testing.T) { |
| 72 | 76 | ctx := context.Background() |
| 73 | - db := setupTestDB(t) | |
| 74 | - userDID, feedURL, _, unreadID := seedArticleReadState(t, ctx, db) | |
| 77 | + dbs := setupTestDB(t) | |
| 78 | + userDID, feedURL, _, unreadID := seedArticleReadState(t, ctx, dbs) | |
| 75 | 79 | |
| 76 | - articles, err := db.ListReadArticles(ctx, userDID, feedURL, 10, 0) | |
| 80 | + results, err := dbs.Articles.ListReadArticles(ctx, userDID, feedURL, 10, 0) | |
| 77 | 81 | assert.NilError(t, err) |
| 78 | - for _, a := range articles { | |
| 82 | + for _, a := range results { | |
| 79 | 83 | assert.Assert(t, a.ID != unreadID, "unread article should not appear in read list") |
| 80 | 84 | } |
| 81 | 85 | } |
| 82 | 86 | |
| 83 | 87 | func TestListUnreadArticles_ReturnsOnlyUnread(t *testing.T) { |
| 84 | 88 | ctx := context.Background() |
| 85 | - db := setupTestDB(t) | |
| 86 | - userDID, feedURL, readID, unreadID := seedArticleReadState(t, ctx, db) | |
| 89 | + dbs := setupTestDB(t) | |
| 90 | + userDID, feedURL, readID, unreadID := seedArticleReadState(t, ctx, dbs) | |
| 87 | 91 | |
| 88 | - articles, err := db.ListUnreadArticles(ctx, userDID, feedURL, 10, 0) | |
| 92 | + results, err := dbs.Articles.ListUnreadArticles(ctx, userDID, feedURL, 10, 0) | |
| 89 | 93 | assert.NilError(t, err) |
| 90 | - assert.Equal(t, len(articles), 1) | |
| 91 | - assert.Equal(t, articles[0].ID, unreadID) | |
| 92 | - assert.Equal(t, articles[0].IsRead.Bool, false) | |
| 94 | + assert.Equal(t, len(results), 1) | |
| 95 | + assert.Equal(t, results[0].ID, unreadID) | |
| 96 | + assert.Equal(t, results[0].IsRead.Bool, false) | |
| 93 | 97 | |
| 94 | 98 | _ = readID |
| 95 | 99 | } |
| 96 | 100 | |
| 97 | 101 | func TestListArticles_ReturnsAll(t *testing.T) { |
| 98 | 102 | ctx := context.Background() |
| 99 | - db := setupTestDB(t) | |
| 100 | - userDID, feedURL, _, _ := seedArticleReadState(t, ctx, db) | |
| 103 | + dbs := setupTestDB(t) | |
| 104 | + userDID, feedURL, _, _ := seedArticleReadState(t, ctx, dbs) | |
| 101 | 105 | |
| 102 | - articles, err := db.ListArticles(ctx, userDID, feedURL, 10, 0) | |
| 106 | + results, err := dbs.Articles.ListArticles(ctx, userDID, feedURL, 10, 0) | |
| 103 | 107 | assert.NilError(t, err) |
| 104 | - assert.Equal(t, len(articles), 2) | |
| 108 | + assert.Equal(t, len(results), 2) | |
| 105 | 109 | } |
| 106 | 110 | |
| 107 | 111 | func TestMarkArticleRead_ToggleUnread(t *testing.T) { |
| 108 | 112 | ctx := context.Background() |
| 109 | - db := setupTestDB(t) | |
| 110 | - userDID, _, _, unreadID := seedArticleReadState(t, ctx, db) | |
| 113 | + dbs := setupTestDB(t) | |
| 114 | + userDID, _, _, unreadID := seedArticleReadState(t, ctx, dbs) | |
| 111 | 115 | |
| 112 | - err := db.MarkArticleRead(ctx, userDID, unreadID) | |
| 116 | + err := dbs.Articles.MarkArticleRead(ctx, userDID, unreadID) | |
| 113 | 117 | assert.NilError(t, err) |
| 114 | 118 | |
| 115 | - state, err := db.GetReadState(ctx, userDID, unreadID) | |
| 119 | + state, err := dbs.Articles.GetReadState(ctx, userDID, unreadID) | |
| 116 | 120 | assert.NilError(t, err) |
| 117 | 121 | assert.Equal(t, state.IsRead, true) |
| 118 | 122 | |
| 119 | - err = db.MarkArticleUnread(ctx, userDID, unreadID) | |
| 123 | + err = dbs.Articles.MarkArticleUnread(ctx, userDID, unreadID) | |
| 120 | 124 | assert.NilError(t, err) |
| 121 | 125 | |
| 122 | - state, err = db.GetReadState(ctx, userDID, unreadID) | |
| 126 | + state, err = dbs.Articles.GetReadState(ctx, userDID, unreadID) | |
| 123 | 127 | assert.NilError(t, err) |
| 124 | 128 | assert.Equal(t, state.IsRead, false) |
| 125 | 129 | } |
| 126 | 130 | |
| 127 | 131 | func TestListReadArticles_EmptyWhenNoneRead(t *testing.T) { |
| 128 | 132 | ctx := context.Background() |
| 129 | - db := setupTestDB(t) | |
| 130 | - userDID, feedURL, _, _ := seedArticleReadState(t, ctx, db) | |
| 133 | + dbs := setupTestDB(t) | |
| 134 | + userDID, feedURL, _, _ := seedArticleReadState(t, ctx, dbs) | |
| 131 | 135 | |
| 132 | - articles, err := db.ListUnreadArticles(ctx, userDID, feedURL, 10, 0) | |
| 136 | + results, err := dbs.Articles.ListUnreadArticles(ctx, userDID, feedURL, 10, 0) | |
| 133 | 137 | assert.NilError(t, err) |
| 134 | - assert.Equal(t, len(articles), 1) | |
| 138 | + assert.Equal(t, len(results), 1) | |
| 135 | 139 | } |
| 136 | 140 | |
| 137 | 141 | func TestListReadArticles_WithFeedURLFilter(t *testing.T) { |
| 138 | 142 | ctx := context.Background() |
| 139 | - db := setupTestDB(t) | |
| 140 | - userDID, feedURL, _, _ := seedArticleReadState(t, ctx, db) | |
| 143 | + dbs := setupTestDB(t) | |
| 144 | + userDID, feedURL, _, _ := seedArticleReadState(t, ctx, dbs) | |
| 141 | 145 | |
| 142 | - articles, err := db.ListReadArticles(ctx, userDID, feedURL, 10, 0) | |
| 146 | + results, err := dbs.Articles.ListReadArticles(ctx, userDID, feedURL, 10, 0) | |
| 143 | 147 | assert.NilError(t, err) |
| 144 | - assert.Equal(t, len(articles), 1) | |
| 148 | + assert.Equal(t, len(results), 1) | |
| 145 | 149 | |
| 146 | - articles, err = db.ListReadArticles(ctx, userDID, "https://other.com/feed", 10, 0) | |
| 150 | + results, err = dbs.Articles.ListReadArticles(ctx, userDID, "https://other.com/feed", 10, 0) | |
| 147 | 151 | assert.NilError(t, err) |
| 148 | - assert.Equal(t, len(articles), 0) | |
| 152 | + assert.Equal(t, len(results), 0) | |
| 149 | 153 | } |
| 150 | 154 | |
| 151 | 155 | func TestGetUnreadCount(t *testing.T) { |
| 152 | 156 | ctx := context.Background() |
| 153 | - db := setupTestDB(t) | |
| 154 | - userDID, feedURL, _, _ := seedArticleReadState(t, ctx, db) | |
| 157 | + dbs := setupTestDB(t) | |
| 158 | + userDID, feedURL, _, _ := seedArticleReadState(t, ctx, dbs) | |
| 155 | 159 | |
| 156 | - count, err := db.GetUnreadCount(ctx, userDID, feedURL) | |
| 160 | + count, err := dbs.Articles.GetUnreadCount(ctx, userDID, feedURL) | |
| 157 | 161 | assert.NilError(t, err) |
| 158 | 162 | assert.Equal(t, count, 1) |
| 159 | 163 | |
| 160 | - count, err = db.GetUnreadCount(ctx, userDID, "") | |
| 164 | + count, err = dbs.Articles.GetUnreadCount(ctx, userDID, "") | |
| 161 | 165 | assert.NilError(t, err) |
| 162 | 166 | assert.Equal(t, count, 1) |
| 163 | 167 | } |
| 164 | 168 | |
| 165 | 169 | func TestUpdateArticleFullContent(t *testing.T) { |
| 166 | 170 | ctx := context.Background() |
| 167 | - db := setupTestDB(t) | |
| 168 | - _, _, _, articleID := seedArticleReadState(t, ctx, db) | |
| 171 | + dbs := setupTestDB(t) | |
| 172 | + _, _, _, articleID := seedArticleReadState(t, ctx, dbs) | |
| 169 | 173 | |
| 170 | - err := db.UpdateArticleFullContent(ctx, articleID, "<p>Scraped content</p>") | |
| 174 | + err := dbs.Articles.UpdateArticleFullContent(ctx, articleID, "<p>Scraped content</p>") | |
| 171 | 175 | assert.NilError(t, err) |
| 172 | 176 | |
| 173 | - article, err := db.GetArticle(ctx, articleID) | |
| 177 | + article, err := dbs.Articles.GetArticle(ctx, articleID) | |
| 174 | 178 | assert.NilError(t, err) |
| 175 | 179 | assert.Equal(t, article.FullContent.String, "<p>Scraped content</p>") |
| 176 | 180 | assert.Assert(t, article.FullContent.Valid) |
| @@ -178,27 +182,27 @@ func TestUpdateArticleFullContent(t *testing.T) { | ||
| 178 | 182 | |
| 179 | 183 | func TestGetArticle_IncludesFullContent(t *testing.T) { |
| 180 | 184 | ctx := context.Background() |
| 181 | - db := setupTestDB(t) | |
| 182 | - _, _, _, articleID := seedArticleReadState(t, ctx, db) | |
| 185 | + dbs := setupTestDB(t) | |
| 186 | + _, _, _, articleID := seedArticleReadState(t, ctx, dbs) | |
| 183 | 187 | |
| 184 | - article, err := db.GetArticle(ctx, articleID) | |
| 188 | + article, err := dbs.Articles.GetArticle(ctx, articleID) | |
| 185 | 189 | assert.NilError(t, err) |
| 186 | 190 | assert.Assert(t, !article.FullContent.Valid) |
| 187 | 191 | } |
| 188 | 192 | |
| 189 | -func seedSearchData(t *testing.T, ctx context.Context, database *DB) (userDID, feedURL string) { | |
| 193 | +func seedSearchData(t *testing.T, ctx context.Context, dbs *Databases) (userDID, feedURL string) { | |
| 190 | 194 | t.Helper() |
| 191 | 195 | |
| 192 | 196 | userDID = "did:test:searcher" |
| 193 | 197 | feedURL = "https://search.example.com/feed.xml" |
| 194 | 198 | |
| 195 | - _, err := database.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, userDID, "searcher") | |
| 199 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, userDID, "searcher") | |
| 196 | 200 | assert.NilError(t, err) |
| 197 | 201 | |
| 198 | - _, err = database.ExecContext(ctx, `INSERT INTO feeds (feed_url, title) VALUES (?, ?)`, feedURL, "Tech Blog") | |
| 202 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.feeds (feed_url, title) VALUES (?, ?)`, feedURL, "Tech Blog") | |
| 199 | 203 | assert.NilError(t, err) |
| 200 | 204 | |
| 201 | - _, err = database.ExecContext(ctx, `INSERT INTO subscriptions (user_did, feed_url) VALUES (?, ?)`, userDID, feedURL) | |
| 205 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.subscriptions (user_did, feed_url) VALUES (?, ?)`, userDID, feedURL) | |
| 202 | 206 | assert.NilError(t, err) |
| 203 | 207 | |
| 204 | 208 | articles := []struct { |
| @@ -209,8 +213,8 @@ func seedSearchData(t *testing.T, ctx context.Context, database *DB) (userDID, f | ||
| 209 | 213 | {"g3", "Python Data Science", "NumPy and Pandas tutorial", "Python is popular for data analysis"}, |
| 210 | 214 | } |
| 211 | 215 | for _, a := range articles { |
| 212 | - _, err := database.ExecContext(ctx, ` | |
| 213 | - INSERT INTO articles (feed_url, guid, title, summary, content) VALUES (?, ?, ?, ?, ?) | |
| 216 | + _, err := dbs.DB().ExecContext(ctx, ` | |
| 217 | + INSERT INTO articles.articles (feed_url, guid, title, summary, content) VALUES (?, ?, ?, ?, ?) | |
| 214 | 218 | `, feedURL, a.guid, a.title, a.summary, a.content) |
| 215 | 219 | assert.NilError(t, err) |
| 216 | 220 | } |
| @@ -220,10 +224,10 @@ func seedSearchData(t *testing.T, ctx context.Context, database *DB) (userDID, f | ||
| 220 | 224 | |
| 221 | 225 | func TestSearchArticles_FindsByTitle(t *testing.T) { |
| 222 | 226 | ctx := context.Background() |
| 223 | - db := setupTestDB(t) | |
| 224 | - userDID, _ := seedSearchData(t, ctx, db) | |
| 227 | + dbs := setupTestDB(t) | |
| 228 | + userDID, _ := seedSearchData(t, ctx, dbs) | |
| 225 | 229 | |
| 226 | - results, err := db.SearchArticles(ctx, userDID, "Go Programming", 10, 0) | |
| 230 | + results, err := dbs.Articles.SearchArticles(ctx, userDID, "Go Programming", 10, 0) | |
| 227 | 231 | assert.NilError(t, err) |
| 228 | 232 | assert.Equal(t, len(results), 1) |
| 229 | 233 | assert.Equal(t, results[0].Title, "Go Programming Basics") |
| @@ -231,10 +235,10 @@ func TestSearchArticles_FindsByTitle(t *testing.T) { | ||
| 231 | 235 | |
| 232 | 236 | func TestSearchArticles_FindsBySummary(t *testing.T) { |
| 233 | 237 | ctx := context.Background() |
| 234 | - db := setupTestDB(t) | |
| 235 | - userDID, _ := seedSearchData(t, ctx, db) | |
| 238 | + dbs := setupTestDB(t) | |
| 239 | + userDID, _ := seedSearchData(t, ctx, dbs) | |
| 236 | 240 | |
| 237 | - results, err := db.SearchArticles(ctx, userDID, "ownership", 10, 0) | |
| 241 | + results, err := dbs.Articles.SearchArticles(ctx, userDID, "ownership", 10, 0) | |
| 238 | 242 | assert.NilError(t, err) |
| 239 | 243 | assert.Equal(t, len(results), 1) |
| 240 | 244 | assert.Equal(t, results[0].Title, "Rust Memory Safety") |
| @@ -242,30 +246,30 @@ func TestSearchArticles_FindsBySummary(t *testing.T) { | ||
| 242 | 246 | |
| 243 | 247 | func TestSearchArticles_IgnoresContentOnlyMatch(t *testing.T) { |
| 244 | 248 | ctx := context.Background() |
| 245 | - db := setupTestDB(t) | |
| 246 | - userDID, _ := seedSearchData(t, ctx, db) | |
| 249 | + dbs := setupTestDB(t) | |
| 250 | + userDID, _ := seedSearchData(t, ctx, dbs) | |
| 247 | 251 | |
| 248 | - results, err := db.SearchArticles(ctx, userDID, "garbage collection", 10, 0) | |
| 252 | + results, err := dbs.Articles.SearchArticles(ctx, userDID, "garbage collection", 10, 0) | |
| 249 | 253 | assert.NilError(t, err) |
| 250 | 254 | assert.Equal(t, len(results), 0) |
| 251 | 255 | } |
| 252 | 256 | |
| 253 | 257 | func TestSearchArticles_NoResults(t *testing.T) { |
| 254 | 258 | ctx := context.Background() |
| 255 | - db := setupTestDB(t) | |
| 256 | - userDID, _ := seedSearchData(t, ctx, db) | |
| 259 | + dbs := setupTestDB(t) | |
| 260 | + userDID, _ := seedSearchData(t, ctx, dbs) | |
| 257 | 261 | |
| 258 | - results, err := db.SearchArticles(ctx, userDID, "nonexistent_xyz", 10, 0) | |
| 262 | + results, err := dbs.Articles.SearchArticles(ctx, userDID, "nonexistent_xyz", 10, 0) | |
| 259 | 263 | assert.NilError(t, err) |
| 260 | 264 | assert.Equal(t, len(results), 0) |
| 261 | 265 | } |
| 262 | 266 | |
| 263 | 267 | func TestSearchArticles_MultipleMatches(t *testing.T) { |
| 264 | 268 | ctx := context.Background() |
| 265 | - db := setupTestDB(t) | |
| 266 | - userDID, _ := seedSearchData(t, ctx, db) | |
| 269 | + dbs := setupTestDB(t) | |
| 270 | + userDID, _ := seedSearchData(t, ctx, dbs) | |
| 267 | 271 | |
| 268 | - results, err := db.SearchArticles(ctx, userDID, "Python", 10, 0) | |
| 272 | + results, err := dbs.Articles.SearchArticles(ctx, userDID, "Python", 10, 0) | |
| 269 | 273 | assert.NilError(t, err) |
| 270 | 274 | assert.Assert(t, len(results) >= 1) |
| 271 | 275 | |
| @@ -281,19 +285,19 @@ func TestSearchArticles_MultipleMatches(t *testing.T) { | ||
| 281 | 285 | |
| 282 | 286 | func TestSearchArticles_ScopedToSubscriptions(t *testing.T) { |
| 283 | 287 | ctx := context.Background() |
| 284 | - db := setupTestDB(t) | |
| 285 | - userDID, feedURL := seedSearchData(t, ctx, db) | |
| 288 | + dbs := setupTestDB(t) | |
| 289 | + userDID, feedURL := seedSearchData(t, ctx, dbs) | |
| 286 | 290 | |
| 287 | 291 | otherFeed := "https://other.example.com/feed.xml" |
| 288 | - _, err := db.ExecContext(ctx, `INSERT INTO feeds (feed_url, title) VALUES (?, ?)`, otherFeed, "Other Feed") | |
| 292 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO articles.feeds (feed_url, title) VALUES (?, ?)`, otherFeed, "Other Feed") | |
| 289 | 293 | assert.NilError(t, err) |
| 290 | 294 | |
| 291 | - _, err = db.ExecContext(ctx, ` | |
| 292 | - INSERT INTO articles (feed_url, guid, title) VALUES (?, ?, ?) | |
| 295 | + _, err = dbs.DB().ExecContext(ctx, ` | |
| 296 | + INSERT INTO articles.articles (feed_url, guid, title) VALUES (?, ?, ?) | |
| 293 | 297 | `, otherFeed, "other-1", "Go Concurrency Tips") |
| 294 | 298 | assert.NilError(t, err) |
| 295 | 299 | |
| 296 | - results, err := db.SearchArticles(ctx, userDID, "Go", 10, 0) | |
| 300 | + results, err := dbs.Articles.SearchArticles(ctx, userDID, "Go", 10, 0) | |
| 297 | 301 | assert.NilError(t, err) |
| 298 | 302 | for _, a := range results { |
| 299 | 303 | assert.Equal(t, a.FeedURL, feedURL) |
| @@ -302,43 +306,43 @@ func TestSearchArticles_ScopedToSubscriptions(t *testing.T) { | ||
| 302 | 306 | |
| 303 | 307 | func TestSearchArticles_Pagination(t *testing.T) { |
| 304 | 308 | ctx := context.Background() |
| 305 | - db := setupTestDB(t) | |
| 306 | - userDID, _ := seedSearchData(t, ctx, db) | |
| 309 | + dbs := setupTestDB(t) | |
| 310 | + userDID, _ := seedSearchData(t, ctx, dbs) | |
| 307 | 311 | |
| 308 | - results, err := db.SearchArticles(ctx, userDID, "Go", 1, 0) | |
| 312 | + results, err := dbs.Articles.SearchArticles(ctx, userDID, "Go", 1, 0) | |
| 309 | 313 | assert.NilError(t, err) |
| 310 | 314 | assert.Equal(t, len(results), 1) |
| 311 | 315 | |
| 312 | - results2, err := db.SearchArticles(ctx, userDID, "Go", 1, 1) | |
| 316 | + results2, err := dbs.Articles.SearchArticles(ctx, userDID, "Go", 1, 1) | |
| 313 | 317 | assert.NilError(t, err) |
| 314 | 318 | assert.Assert(t, len(results2) == 0 || results2[0].ID != results[0].ID) |
| 315 | 319 | } |
| 316 | 320 | |
| 317 | 321 | func TestSearchArticles_EmptyQuery(t *testing.T) { |
| 318 | 322 | ctx := context.Background() |
| 319 | - db := setupTestDB(t) | |
| 320 | - userDID, _ := seedSearchData(t, ctx, db) | |
| 323 | + dbs := setupTestDB(t) | |
| 324 | + userDID, _ := seedSearchData(t, ctx, dbs) | |
| 321 | 325 | |
| 322 | - results, err := db.SearchArticles(ctx, userDID, "", 10, 0) | |
| 326 | + results, err := dbs.Articles.SearchArticles(ctx, userDID, "", 10, 0) | |
| 323 | 327 | assert.NilError(t, err) |
| 324 | 328 | assert.Equal(t, len(results), 0) |
| 325 | 329 | } |
| 326 | 330 | |
| 327 | 331 | func TestSearchArticles_SpecialCharactersNoError(t *testing.T) { |
| 328 | 332 | ctx := context.Background() |
| 329 | - db := setupTestDB(t) | |
| 330 | - userDID, _ := seedSearchData(t, ctx, db) | |
| 333 | + dbs := setupTestDB(t) | |
| 334 | + userDID, _ := seedSearchData(t, ctx, dbs) | |
| 331 | 335 | |
| 332 | - _, err := db.SearchArticles(ctx, userDID, "test.example.com/path?q=1&b=2", 10, 0) | |
| 336 | + _, err := dbs.Articles.SearchArticles(ctx, userDID, "test.example.com/path?q=1&b=2", 10, 0) | |
| 333 | 337 | assert.NilError(t, err) |
| 334 | 338 | } |
| 335 | 339 | |
| 336 | 340 | func TestSearchArticles_OnlySpecialCharacters(t *testing.T) { |
| 337 | 341 | ctx := context.Background() |
| 338 | - db := setupTestDB(t) | |
| 339 | - userDID, _ := seedSearchData(t, ctx, db) | |
| 342 | + dbs := setupTestDB(t) | |
| 343 | + userDID, _ := seedSearchData(t, ctx, dbs) | |
| 340 | 344 | |
| 341 | - results, err := db.SearchArticles(ctx, userDID, "...///:::!!!", 10, 0) | |
| 345 | + results, err := dbs.Articles.SearchArticles(ctx, userDID, "...///:::!!!", 10, 0) | |
| 342 | 346 | assert.NilError(t, err) |
| 343 | 347 | assert.Equal(t, len(results), 0) |
| 344 | 348 | } |
| @@ -9,46 +9,50 @@ import ( | |||
| 9 | "gotest.tools/v3/assert" | 9 | "gotest.tools/v3/assert" |
| 10 | ) | 10 | ) |
| 11 | 11 | ||
| 12 | -func setupTestDB(t *testing.T) *DB { | 12 | +func setupTestDB(t *testing.T) *Databases { |
| 13 | t.Helper() | 13 | t.Helper() |
| 14 | f, err := os.CreateTemp("", "glean-test-*.db") | 14 | f, err := os.CreateTemp("", "glean-test-*.db") |
| 15 | assert.NilError(t, err) | 15 | assert.NilError(t, err) |
| 16 | assert.NilError(t, f.Close()) | 16 | assert.NilError(t, f.Close()) |
| 17 | path := f.Name() | 17 | path := f.Name() |
| 18 | - t.Cleanup(func() { _ = os.Remove(path) }) | 18 | + t.Cleanup(func() { |
| 19 | + for _, suffix := range []string{"", "_users", "_users-shm", "_users-wal", "_articles", "_articles-shm", "_articles-wal", "_recs", "_recs-shm", "_recs-wal"} { | ||
| 20 | + _ = os.Remove(path + suffix) | ||
| 21 | + } | ||
| 22 | + }) | ||
| 19 | 23 | ||
| 20 | - db, err := Open(path) | 24 | + dbs, err := OpenAll(path) |
| 21 | assert.NilError(t, err) | 25 | assert.NilError(t, err) |
| 22 | - t.Cleanup(func() { _ = db.Close() }) | 26 | + t.Cleanup(func() { _ = dbs.Close() }) |
| 23 | - return db | 27 | + return dbs |
| 24 | } | 28 | } |
| 25 | 29 | ||
| 26 | -func seedArticleReadState(t *testing.T, ctx context.Context, db *DB) (userDID string, feedURL string, readArticleID, unreadArticleID int64) { | 30 | +func seedArticleReadState(t *testing.T, ctx context.Context, dbs *Databases) (userDID string, feedURL string, readArticleID, unreadArticleID int64) { |
| 27 | t.Helper() | 31 | t.Helper() |
| 28 | 32 | ||
| 29 | userDID = "did:test:user1" | 33 | userDID = "did:test:user1" |
| 30 | feedURL = "https://example.com/feed.xml" | 34 | feedURL = "https://example.com/feed.xml" |
| 31 | 35 | ||
| 32 | - _, err := db.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, userDID, "user1") | 36 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, userDID, "user1") |
| 33 | assert.NilError(t, err) | 37 | assert.NilError(t, err) |
| 34 | 38 | ||
| 35 | - _, err = db.ExecContext(ctx, `INSERT INTO feeds (feed_url, title) VALUES (?, ?)`, feedURL, "Test Feed") | 39 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.feeds (feed_url, title) VALUES (?, ?)`, feedURL, "Test Feed") |
| 36 | assert.NilError(t, err) | 40 | assert.NilError(t, err) |
| 37 | 41 | ||
| 38 | - _, err = db.ExecContext(ctx, `INSERT INTO subscriptions (user_did, feed_url) VALUES (?, ?)`, userDID, feedURL) | 42 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.subscriptions (user_did, feed_url) VALUES (?, ?)`, userDID, feedURL) |
| 39 | assert.NilError(t, err) | 43 | assert.NilError(t, err) |
| 40 | 44 | ||
| 41 | - res, err := db.ExecContext(ctx, `INSERT INTO articles (feed_url, guid, title, url) VALUES (?, ?, ?, ?)`, | 45 | + res, err := dbs.DB().ExecContext(ctx, `INSERT INTO articles.articles (feed_url, guid, title, url) VALUES (?, ?, ?, ?)`, |
| 42 | feedURL, "guid-read", "Read Article", "https://example.com/read") | 46 | feedURL, "guid-read", "Read Article", "https://example.com/read") |
| 43 | assert.NilError(t, err) | 47 | assert.NilError(t, err) |
| 44 | readArticleID, _ = res.LastInsertId() | 48 | readArticleID, _ = res.LastInsertId() |
| 45 | 49 | ||
| 46 | - res, err = db.ExecContext(ctx, `INSERT INTO articles (feed_url, guid, title, url) VALUES (?, ?, ?, ?)`, | 50 | + res, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.articles (feed_url, guid, title, url) VALUES (?, ?, ?, ?)`, |
| 47 | feedURL, "guid-unread", "Unread Article", "https://example.com/unread") | 51 | feedURL, "guid-unread", "Unread Article", "https://example.com/unread") |
| 48 | assert.NilError(t, err) | 52 | assert.NilError(t, err) |
| 49 | unreadArticleID, _ = res.LastInsertId() | 53 | unreadArticleID, _ = res.LastInsertId() |
| 50 | 54 | ||
| 51 | - err = db.MarkArticleRead(ctx, userDID, readArticleID) | 55 | + err = dbs.Articles.MarkArticleRead(ctx, userDID, readArticleID) |
| 52 | assert.NilError(t, err) | 56 | assert.NilError(t, err) |
| 53 | 57 | ||
| 54 | return userDID, feedURL, readArticleID, unreadArticleID | 58 | return userDID, feedURL, readArticleID, unreadArticleID |
| @@ -56,121 +60,121 @@ func seedArticleReadState(t *testing.T, ctx context.Context, db *DB) (userDID st | |||
| 56 | 60 | ||
| 57 | func TestListReadArticles_ReturnsOnlyRead(t *testing.T) { | 61 | func TestListReadArticles_ReturnsOnlyRead(t *testing.T) { |
| 58 | ctx := context.Background() | 62 | ctx := context.Background() |
| 59 | - db := setupTestDB(t) | 63 | + dbs := setupTestDB(t) |
| 60 | - userDID, feedURL, readID, unreadID := seedArticleReadState(t, ctx, db) | 64 | + userDID, feedURL, readID, unreadID := seedArticleReadState(t, ctx, dbs) |
| 61 | 65 | ||
| 62 | - articles, err := db.ListReadArticles(ctx, userDID, feedURL, 10, 0) | 66 | + results, err := dbs.Articles.ListReadArticles(ctx, userDID, feedURL, 10, 0) |
| 63 | assert.NilError(t, err) | 67 | assert.NilError(t, err) |
| 64 | - assert.Equal(t, len(articles), 1) | 68 | + assert.Equal(t, len(results), 1) |
| 65 | - assert.Equal(t, articles[0].ID, readID) | 69 | + assert.Equal(t, results[0].ID, readID) |
| 66 | - assert.Equal(t, articles[0].IsRead, sql.NullBool{Bool: true, Valid: true}) | 70 | + assert.Equal(t, results[0].IsRead, sql.NullBool{Bool: true, Valid: true}) |
| 67 | 71 | ||
| 68 | _ = unreadID | 72 | _ = unreadID |
| 69 | } | 73 | } |
| 70 | 74 | ||
| 71 | func TestListReadArticles_ExcludesUnread(t *testing.T) { | 75 | func TestListReadArticles_ExcludesUnread(t *testing.T) { |
| 72 | ctx := context.Background() | 76 | ctx := context.Background() |
| 73 | - db := setupTestDB(t) | 77 | + dbs := setupTestDB(t) |
| 74 | - userDID, feedURL, _, unreadID := seedArticleReadState(t, ctx, db) | 78 | + userDID, feedURL, _, unreadID := seedArticleReadState(t, ctx, dbs) |
| 75 | 79 | ||
| 76 | - articles, err := db.ListReadArticles(ctx, userDID, feedURL, 10, 0) | 80 | + results, err := dbs.Articles.ListReadArticles(ctx, userDID, feedURL, 10, 0) |
| 77 | assert.NilError(t, err) | 81 | assert.NilError(t, err) |
| 78 | - for _, a := range articles { | 82 | + for _, a := range results { |
| 79 | assert.Assert(t, a.ID != unreadID, "unread article should not appear in read list") | 83 | assert.Assert(t, a.ID != unreadID, "unread article should not appear in read list") |
| 80 | } | 84 | } |
| 81 | } | 85 | } |
| 82 | 86 | ||
| 83 | func TestListUnreadArticles_ReturnsOnlyUnread(t *testing.T) { | 87 | func TestListUnreadArticles_ReturnsOnlyUnread(t *testing.T) { |
| 84 | ctx := context.Background() | 88 | ctx := context.Background() |
| 85 | - db := setupTestDB(t) | 89 | + dbs := setupTestDB(t) |
| 86 | - userDID, feedURL, readID, unreadID := seedArticleReadState(t, ctx, db) | 90 | + userDID, feedURL, readID, unreadID := seedArticleReadState(t, ctx, dbs) |
| 87 | 91 | ||
| 88 | - articles, err := db.ListUnreadArticles(ctx, userDID, feedURL, 10, 0) | 92 | + results, err := dbs.Articles.ListUnreadArticles(ctx, userDID, feedURL, 10, 0) |
| 89 | assert.NilError(t, err) | 93 | assert.NilError(t, err) |
| 90 | - assert.Equal(t, len(articles), 1) | 94 | + assert.Equal(t, len(results), 1) |
| 91 | - assert.Equal(t, articles[0].ID, unreadID) | 95 | + assert.Equal(t, results[0].ID, unreadID) |
| 92 | - assert.Equal(t, articles[0].IsRead.Bool, false) | 96 | + assert.Equal(t, results[0].IsRead.Bool, false) |
| 93 | 97 | ||
| 94 | _ = readID | 98 | _ = readID |
| 95 | } | 99 | } |
| 96 | 100 | ||
| 97 | func TestListArticles_ReturnsAll(t *testing.T) { | 101 | func TestListArticles_ReturnsAll(t *testing.T) { |
| 98 | ctx := context.Background() | 102 | ctx := context.Background() |
| 99 | - db := setupTestDB(t) | 103 | + dbs := setupTestDB(t) |
| 100 | - userDID, feedURL, _, _ := seedArticleReadState(t, ctx, db) | 104 | + userDID, feedURL, _, _ := seedArticleReadState(t, ctx, dbs) |
| 101 | 105 | ||
| 102 | - articles, err := db.ListArticles(ctx, userDID, feedURL, 10, 0) | 106 | + results, err := dbs.Articles.ListArticles(ctx, userDID, feedURL, 10, 0) |
| 103 | assert.NilError(t, err) | 107 | assert.NilError(t, err) |
| 104 | - assert.Equal(t, len(articles), 2) | 108 | + assert.Equal(t, len(results), 2) |
| 105 | } | 109 | } |
| 106 | 110 | ||
| 107 | func TestMarkArticleRead_ToggleUnread(t *testing.T) { | 111 | func TestMarkArticleRead_ToggleUnread(t *testing.T) { |
| 108 | ctx := context.Background() | 112 | ctx := context.Background() |
| 109 | - db := setupTestDB(t) | 113 | + dbs := setupTestDB(t) |
| 110 | - userDID, _, _, unreadID := seedArticleReadState(t, ctx, db) | 114 | + userDID, _, _, unreadID := seedArticleReadState(t, ctx, dbs) |
| 111 | 115 | ||
| 112 | - err := db.MarkArticleRead(ctx, userDID, unreadID) | 116 | + err := dbs.Articles.MarkArticleRead(ctx, userDID, unreadID) |
| 113 | assert.NilError(t, err) | 117 | assert.NilError(t, err) |
| 114 | 118 | ||
| 115 | - state, err := db.GetReadState(ctx, userDID, unreadID) | 119 | + state, err := dbs.Articles.GetReadState(ctx, userDID, unreadID) |
| 116 | assert.NilError(t, err) | 120 | assert.NilError(t, err) |
| 117 | assert.Equal(t, state.IsRead, true) | 121 | assert.Equal(t, state.IsRead, true) |
| 118 | 122 | ||
| 119 | - err = db.MarkArticleUnread(ctx, userDID, unreadID) | 123 | + err = dbs.Articles.MarkArticleUnread(ctx, userDID, unreadID) |
| 120 | assert.NilError(t, err) | 124 | assert.NilError(t, err) |
| 121 | 125 | ||
| 122 | - state, err = db.GetReadState(ctx, userDID, unreadID) | 126 | + state, err = dbs.Articles.GetReadState(ctx, userDID, unreadID) |
| 123 | assert.NilError(t, err) | 127 | assert.NilError(t, err) |
| 124 | assert.Equal(t, state.IsRead, false) | 128 | assert.Equal(t, state.IsRead, false) |
| 125 | } | 129 | } |
| 126 | 130 | ||
| 127 | func TestListReadArticles_EmptyWhenNoneRead(t *testing.T) { | 131 | func TestListReadArticles_EmptyWhenNoneRead(t *testing.T) { |
| 128 | ctx := context.Background() | 132 | ctx := context.Background() |
| 129 | - db := setupTestDB(t) | 133 | + dbs := setupTestDB(t) |
| 130 | - userDID, feedURL, _, _ := seedArticleReadState(t, ctx, db) | 134 | + userDID, feedURL, _, _ := seedArticleReadState(t, ctx, dbs) |
| 131 | 135 | ||
| 132 | - articles, err := db.ListUnreadArticles(ctx, userDID, feedURL, 10, 0) | 136 | + results, err := dbs.Articles.ListUnreadArticles(ctx, userDID, feedURL, 10, 0) |
| 133 | assert.NilError(t, err) | 137 | assert.NilError(t, err) |
| 134 | - assert.Equal(t, len(articles), 1) | 138 | + assert.Equal(t, len(results), 1) |
| 135 | } | 139 | } |
| 136 | 140 | ||
| 137 | func TestListReadArticles_WithFeedURLFilter(t *testing.T) { | 141 | func TestListReadArticles_WithFeedURLFilter(t *testing.T) { |
| 138 | ctx := context.Background() | 142 | ctx := context.Background() |
| 139 | - db := setupTestDB(t) | 143 | + dbs := setupTestDB(t) |
| 140 | - userDID, feedURL, _, _ := seedArticleReadState(t, ctx, db) | 144 | + userDID, feedURL, _, _ := seedArticleReadState(t, ctx, dbs) |
| 141 | 145 | ||
| 142 | - articles, err := db.ListReadArticles(ctx, userDID, feedURL, 10, 0) | 146 | + results, err := dbs.Articles.ListReadArticles(ctx, userDID, feedURL, 10, 0) |
| 143 | assert.NilError(t, err) | 147 | assert.NilError(t, err) |
| 144 | - assert.Equal(t, len(articles), 1) | 148 | + assert.Equal(t, len(results), 1) |
| 145 | 149 | ||
| 146 | - articles, err = db.ListReadArticles(ctx, userDID, "https://other.com/feed", 10, 0) | 150 | + results, err = dbs.Articles.ListReadArticles(ctx, userDID, "https://other.com/feed", 10, 0) |
| 147 | assert.NilError(t, err) | 151 | assert.NilError(t, err) |
| 148 | - assert.Equal(t, len(articles), 0) | 152 | + assert.Equal(t, len(results), 0) |
| 149 | } | 153 | } |
| 150 | 154 | ||
| 151 | func TestGetUnreadCount(t *testing.T) { | 155 | func TestGetUnreadCount(t *testing.T) { |
| 152 | ctx := context.Background() | 156 | ctx := context.Background() |
| 153 | - db := setupTestDB(t) | 157 | + dbs := setupTestDB(t) |
| 154 | - userDID, feedURL, _, _ := seedArticleReadState(t, ctx, db) | 158 | + userDID, feedURL, _, _ := seedArticleReadState(t, ctx, dbs) |
| 155 | 159 | ||
| 156 | - count, err := db.GetUnreadCount(ctx, userDID, feedURL) | 160 | + count, err := dbs.Articles.GetUnreadCount(ctx, userDID, feedURL) |
| 157 | assert.NilError(t, err) | 161 | assert.NilError(t, err) |
| 158 | assert.Equal(t, count, 1) | 162 | assert.Equal(t, count, 1) |
| 159 | 163 | ||
| 160 | - count, err = db.GetUnreadCount(ctx, userDID, "") | 164 | + count, err = dbs.Articles.GetUnreadCount(ctx, userDID, "") |
| 161 | assert.NilError(t, err) | 165 | assert.NilError(t, err) |
| 162 | assert.Equal(t, count, 1) | 166 | assert.Equal(t, count, 1) |
| 163 | } | 167 | } |
| 164 | 168 | ||
| 165 | func TestUpdateArticleFullContent(t *testing.T) { | 169 | func TestUpdateArticleFullContent(t *testing.T) { |
| 166 | ctx := context.Background() | 170 | ctx := context.Background() |
| 167 | - db := setupTestDB(t) | 171 | + dbs := setupTestDB(t) |
| 168 | - _, _, _, articleID := seedArticleReadState(t, ctx, db) | 172 | + _, _, _, articleID := seedArticleReadState(t, ctx, dbs) |
| 169 | 173 | ||
| 170 | - err := db.UpdateArticleFullContent(ctx, articleID, "<p>Scraped content</p>") | 174 | + err := dbs.Articles.UpdateArticleFullContent(ctx, articleID, "<p>Scraped content</p>") |
| 171 | assert.NilError(t, err) | 175 | assert.NilError(t, err) |
| 172 | 176 | ||
| 173 | - article, err := db.GetArticle(ctx, articleID) | 177 | + article, err := dbs.Articles.GetArticle(ctx, articleID) |
| 174 | assert.NilError(t, err) | 178 | assert.NilError(t, err) |
| 175 | assert.Equal(t, article.FullContent.String, "<p>Scraped content</p>") | 179 | assert.Equal(t, article.FullContent.String, "<p>Scraped content</p>") |
| 176 | assert.Assert(t, article.FullContent.Valid) | 180 | assert.Assert(t, article.FullContent.Valid) |
| @@ -178,27 +182,27 @@ func TestUpdateArticleFullContent(t *testing.T) { | |||
| 178 | 182 | ||
| 179 | func TestGetArticle_IncludesFullContent(t *testing.T) { | 183 | func TestGetArticle_IncludesFullContent(t *testing.T) { |
| 180 | ctx := context.Background() | 184 | ctx := context.Background() |
| 181 | - db := setupTestDB(t) | 185 | + dbs := setupTestDB(t) |
| 182 | - _, _, _, articleID := seedArticleReadState(t, ctx, db) | 186 | + _, _, _, articleID := seedArticleReadState(t, ctx, dbs) |
| 183 | 187 | ||
| 184 | - article, err := db.GetArticle(ctx, articleID) | 188 | + article, err := dbs.Articles.GetArticle(ctx, articleID) |
| 185 | assert.NilError(t, err) | 189 | assert.NilError(t, err) |
| 186 | assert.Assert(t, !article.FullContent.Valid) | 190 | assert.Assert(t, !article.FullContent.Valid) |
| 187 | } | 191 | } |
| 188 | 192 | ||
| 189 | -func seedSearchData(t *testing.T, ctx context.Context, database *DB) (userDID, feedURL string) { | 193 | +func seedSearchData(t *testing.T, ctx context.Context, dbs *Databases) (userDID, feedURL string) { |
| 190 | t.Helper() | 194 | t.Helper() |
| 191 | 195 | ||
| 192 | userDID = "did:test:searcher" | 196 | userDID = "did:test:searcher" |
| 193 | feedURL = "https://search.example.com/feed.xml" | 197 | feedURL = "https://search.example.com/feed.xml" |
| 194 | 198 | ||
| 195 | - _, err := database.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, userDID, "searcher") | 199 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, userDID, "searcher") |
| 196 | assert.NilError(t, err) | 200 | assert.NilError(t, err) |
| 197 | 201 | ||
| 198 | - _, err = database.ExecContext(ctx, `INSERT INTO feeds (feed_url, title) VALUES (?, ?)`, feedURL, "Tech Blog") | 202 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.feeds (feed_url, title) VALUES (?, ?)`, feedURL, "Tech Blog") |
| 199 | assert.NilError(t, err) | 203 | assert.NilError(t, err) |
| 200 | 204 | ||
| 201 | - _, err = database.ExecContext(ctx, `INSERT INTO subscriptions (user_did, feed_url) VALUES (?, ?)`, userDID, feedURL) | 205 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.subscriptions (user_did, feed_url) VALUES (?, ?)`, userDID, feedURL) |
| 202 | assert.NilError(t, err) | 206 | assert.NilError(t, err) |
| 203 | 207 | ||
| 204 | articles := []struct { | 208 | articles := []struct { |
| @@ -209,8 +213,8 @@ func seedSearchData(t *testing.T, ctx context.Context, database *DB) (userDID, f | |||
| 209 | {"g3", "Python Data Science", "NumPy and Pandas tutorial", "Python is popular for data analysis"}, | 213 | {"g3", "Python Data Science", "NumPy and Pandas tutorial", "Python is popular for data analysis"}, |
| 210 | } | 214 | } |
| 211 | for _, a := range articles { | 215 | for _, a := range articles { |
| 212 | - _, err := database.ExecContext(ctx, ` | 216 | + _, err := dbs.DB().ExecContext(ctx, ` |
| 213 | - INSERT INTO articles (feed_url, guid, title, summary, content) VALUES (?, ?, ?, ?, ?) | 217 | + INSERT INTO articles.articles (feed_url, guid, title, summary, content) VALUES (?, ?, ?, ?, ?) |
| 214 | `, feedURL, a.guid, a.title, a.summary, a.content) | 218 | `, feedURL, a.guid, a.title, a.summary, a.content) |
| 215 | assert.NilError(t, err) | 219 | assert.NilError(t, err) |
| 216 | } | 220 | } |
| @@ -220,10 +224,10 @@ func seedSearchData(t *testing.T, ctx context.Context, database *DB) (userDID, f | |||
| 220 | 224 | ||
| 221 | func TestSearchArticles_FindsByTitle(t *testing.T) { | 225 | func TestSearchArticles_FindsByTitle(t *testing.T) { |
| 222 | ctx := context.Background() | 226 | ctx := context.Background() |
| 223 | - db := setupTestDB(t) | 227 | + dbs := setupTestDB(t) |
| 224 | - userDID, _ := seedSearchData(t, ctx, db) | 228 | + userDID, _ := seedSearchData(t, ctx, dbs) |
| 225 | 229 | ||
| 226 | - results, err := db.SearchArticles(ctx, userDID, "Go Programming", 10, 0) | 230 | + results, err := dbs.Articles.SearchArticles(ctx, userDID, "Go Programming", 10, 0) |
| 227 | assert.NilError(t, err) | 231 | assert.NilError(t, err) |
| 228 | assert.Equal(t, len(results), 1) | 232 | assert.Equal(t, len(results), 1) |
| 229 | assert.Equal(t, results[0].Title, "Go Programming Basics") | 233 | assert.Equal(t, results[0].Title, "Go Programming Basics") |
| @@ -231,10 +235,10 @@ func TestSearchArticles_FindsByTitle(t *testing.T) { | |||
| 231 | 235 | ||
| 232 | func TestSearchArticles_FindsBySummary(t *testing.T) { | 236 | func TestSearchArticles_FindsBySummary(t *testing.T) { |
| 233 | ctx := context.Background() | 237 | ctx := context.Background() |
| 234 | - db := setupTestDB(t) | 238 | + dbs := setupTestDB(t) |
| 235 | - userDID, _ := seedSearchData(t, ctx, db) | 239 | + userDID, _ := seedSearchData(t, ctx, dbs) |
| 236 | 240 | ||
| 237 | - results, err := db.SearchArticles(ctx, userDID, "ownership", 10, 0) | 241 | + results, err := dbs.Articles.SearchArticles(ctx, userDID, "ownership", 10, 0) |
| 238 | assert.NilError(t, err) | 242 | assert.NilError(t, err) |
| 239 | assert.Equal(t, len(results), 1) | 243 | assert.Equal(t, len(results), 1) |
| 240 | assert.Equal(t, results[0].Title, "Rust Memory Safety") | 244 | assert.Equal(t, results[0].Title, "Rust Memory Safety") |
| @@ -242,30 +246,30 @@ func TestSearchArticles_FindsBySummary(t *testing.T) { | |||
| 242 | 246 | ||
| 243 | func TestSearchArticles_IgnoresContentOnlyMatch(t *testing.T) { | 247 | func TestSearchArticles_IgnoresContentOnlyMatch(t *testing.T) { |
| 244 | ctx := context.Background() | 248 | ctx := context.Background() |
| 245 | - db := setupTestDB(t) | 249 | + dbs := setupTestDB(t) |
| 246 | - userDID, _ := seedSearchData(t, ctx, db) | 250 | + userDID, _ := seedSearchData(t, ctx, dbs) |
| 247 | 251 | ||
| 248 | - results, err := db.SearchArticles(ctx, userDID, "garbage collection", 10, 0) | 252 | + results, err := dbs.Articles.SearchArticles(ctx, userDID, "garbage collection", 10, 0) |
| 249 | assert.NilError(t, err) | 253 | assert.NilError(t, err) |
| 250 | assert.Equal(t, len(results), 0) | 254 | assert.Equal(t, len(results), 0) |
| 251 | } | 255 | } |
| 252 | 256 | ||
| 253 | func TestSearchArticles_NoResults(t *testing.T) { | 257 | func TestSearchArticles_NoResults(t *testing.T) { |
| 254 | ctx := context.Background() | 258 | ctx := context.Background() |
| 255 | - db := setupTestDB(t) | 259 | + dbs := setupTestDB(t) |
| 256 | - userDID, _ := seedSearchData(t, ctx, db) | 260 | + userDID, _ := seedSearchData(t, ctx, dbs) |
| 257 | 261 | ||
| 258 | - results, err := db.SearchArticles(ctx, userDID, "nonexistent_xyz", 10, 0) | 262 | + results, err := dbs.Articles.SearchArticles(ctx, userDID, "nonexistent_xyz", 10, 0) |
| 259 | assert.NilError(t, err) | 263 | assert.NilError(t, err) |
| 260 | assert.Equal(t, len(results), 0) | 264 | assert.Equal(t, len(results), 0) |
| 261 | } | 265 | } |
| 262 | 266 | ||
| 263 | func TestSearchArticles_MultipleMatches(t *testing.T) { | 267 | func TestSearchArticles_MultipleMatches(t *testing.T) { |
| 264 | ctx := context.Background() | 268 | ctx := context.Background() |
| 265 | - db := setupTestDB(t) | 269 | + dbs := setupTestDB(t) |
| 266 | - userDID, _ := seedSearchData(t, ctx, db) | 270 | + userDID, _ := seedSearchData(t, ctx, dbs) |
| 267 | 271 | ||
| 268 | - results, err := db.SearchArticles(ctx, userDID, "Python", 10, 0) | 272 | + results, err := dbs.Articles.SearchArticles(ctx, userDID, "Python", 10, 0) |
| 269 | assert.NilError(t, err) | 273 | assert.NilError(t, err) |
| 270 | assert.Assert(t, len(results) >= 1) | 274 | assert.Assert(t, len(results) >= 1) |
| 271 | 275 | ||
| @@ -281,19 +285,19 @@ func TestSearchArticles_MultipleMatches(t *testing.T) { | |||
| 281 | 285 | ||
| 282 | func TestSearchArticles_ScopedToSubscriptions(t *testing.T) { | 286 | func TestSearchArticles_ScopedToSubscriptions(t *testing.T) { |
| 283 | ctx := context.Background() | 287 | ctx := context.Background() |
| 284 | - db := setupTestDB(t) | 288 | + dbs := setupTestDB(t) |
| 285 | - userDID, feedURL := seedSearchData(t, ctx, db) | 289 | + userDID, feedURL := seedSearchData(t, ctx, dbs) |
| 286 | 290 | ||
| 287 | otherFeed := "https://other.example.com/feed.xml" | 291 | otherFeed := "https://other.example.com/feed.xml" |
| 288 | - _, err := db.ExecContext(ctx, `INSERT INTO feeds (feed_url, title) VALUES (?, ?)`, otherFeed, "Other Feed") | 292 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO articles.feeds (feed_url, title) VALUES (?, ?)`, otherFeed, "Other Feed") |
| 289 | assert.NilError(t, err) | 293 | assert.NilError(t, err) |
| 290 | 294 | ||
| 291 | - _, err = db.ExecContext(ctx, ` | 295 | + _, err = dbs.DB().ExecContext(ctx, ` |
| 292 | - INSERT INTO articles (feed_url, guid, title) VALUES (?, ?, ?) | 296 | + INSERT INTO articles.articles (feed_url, guid, title) VALUES (?, ?, ?) |
| 293 | `, otherFeed, "other-1", "Go Concurrency Tips") | 297 | `, otherFeed, "other-1", "Go Concurrency Tips") |
| 294 | assert.NilError(t, err) | 298 | assert.NilError(t, err) |
| 295 | 299 | ||
| 296 | - results, err := db.SearchArticles(ctx, userDID, "Go", 10, 0) | 300 | + results, err := dbs.Articles.SearchArticles(ctx, userDID, "Go", 10, 0) |
| 297 | assert.NilError(t, err) | 301 | assert.NilError(t, err) |
| 298 | for _, a := range results { | 302 | for _, a := range results { |
| 299 | assert.Equal(t, a.FeedURL, feedURL) | 303 | assert.Equal(t, a.FeedURL, feedURL) |
| @@ -302,43 +306,43 @@ func TestSearchArticles_ScopedToSubscriptions(t *testing.T) { | |||
| 302 | 306 | ||
| 303 | func TestSearchArticles_Pagination(t *testing.T) { | 307 | func TestSearchArticles_Pagination(t *testing.T) { |
| 304 | ctx := context.Background() | 308 | ctx := context.Background() |
| 305 | - db := setupTestDB(t) | 309 | + dbs := setupTestDB(t) |
| 306 | - userDID, _ := seedSearchData(t, ctx, db) | 310 | + userDID, _ := seedSearchData(t, ctx, dbs) |
| 307 | 311 | ||
| 308 | - results, err := db.SearchArticles(ctx, userDID, "Go", 1, 0) | 312 | + results, err := dbs.Articles.SearchArticles(ctx, userDID, "Go", 1, 0) |
| 309 | assert.NilError(t, err) | 313 | assert.NilError(t, err) |
| 310 | assert.Equal(t, len(results), 1) | 314 | assert.Equal(t, len(results), 1) |
| 311 | 315 | ||
| 312 | - results2, err := db.SearchArticles(ctx, userDID, "Go", 1, 1) | 316 | + results2, err := dbs.Articles.SearchArticles(ctx, userDID, "Go", 1, 1) |
| 313 | assert.NilError(t, err) | 317 | assert.NilError(t, err) |
| 314 | assert.Assert(t, len(results2) == 0 || results2[0].ID != results[0].ID) | 318 | assert.Assert(t, len(results2) == 0 || results2[0].ID != results[0].ID) |
| 315 | } | 319 | } |
| 316 | 320 | ||
| 317 | func TestSearchArticles_EmptyQuery(t *testing.T) { | 321 | func TestSearchArticles_EmptyQuery(t *testing.T) { |
| 318 | ctx := context.Background() | 322 | ctx := context.Background() |
| 319 | - db := setupTestDB(t) | 323 | + dbs := setupTestDB(t) |
| 320 | - userDID, _ := seedSearchData(t, ctx, db) | 324 | + userDID, _ := seedSearchData(t, ctx, dbs) |
| 321 | 325 | ||
| 322 | - results, err := db.SearchArticles(ctx, userDID, "", 10, 0) | 326 | + results, err := dbs.Articles.SearchArticles(ctx, userDID, "", 10, 0) |
| 323 | assert.NilError(t, err) | 327 | assert.NilError(t, err) |
| 324 | assert.Equal(t, len(results), 0) | 328 | assert.Equal(t, len(results), 0) |
| 325 | } | 329 | } |
| 326 | 330 | ||
| 327 | func TestSearchArticles_SpecialCharactersNoError(t *testing.T) { | 331 | func TestSearchArticles_SpecialCharactersNoError(t *testing.T) { |
| 328 | ctx := context.Background() | 332 | ctx := context.Background() |
| 329 | - db := setupTestDB(t) | 333 | + dbs := setupTestDB(t) |
| 330 | - userDID, _ := seedSearchData(t, ctx, db) | 334 | + userDID, _ := seedSearchData(t, ctx, dbs) |
| 331 | 335 | ||
| 332 | - _, err := db.SearchArticles(ctx, userDID, "test.example.com/path?q=1&b=2", 10, 0) | 336 | + _, err := dbs.Articles.SearchArticles(ctx, userDID, "test.example.com/path?q=1&b=2", 10, 0) |
| 333 | assert.NilError(t, err) | 337 | assert.NilError(t, err) |
| 334 | } | 338 | } |
| 335 | 339 | ||
| 336 | func TestSearchArticles_OnlySpecialCharacters(t *testing.T) { | 340 | func TestSearchArticles_OnlySpecialCharacters(t *testing.T) { |
| 337 | ctx := context.Background() | 341 | ctx := context.Background() |
| 338 | - db := setupTestDB(t) | 342 | + dbs := setupTestDB(t) |
| 339 | - userDID, _ := seedSearchData(t, ctx, db) | 343 | + userDID, _ := seedSearchData(t, ctx, dbs) |
| 340 | 344 | ||
| 341 | - results, err := db.SearchArticles(ctx, userDID, "...///:::!!!", 10, 0) | 345 | + results, err := dbs.Articles.SearchArticles(ctx, userDID, "...///:::!!!", 10, 0) |
| 342 | assert.NilError(t, err) | 346 | assert.NilError(t, err) |
| 343 | assert.Equal(t, len(results), 0) | 347 | assert.Equal(t, len(results), 0) |
| 344 | } | 348 | } |
modified
internal/db/batch_test.go +61 -61 | @@ -10,21 +10,21 @@ import ( | ||
| 10 | 10 | |
| 11 | 11 | func TestBatchCreateUsers_InsertsAll(t *testing.T) { |
| 12 | 12 | ctx := context.Background() |
| 13 | - db := setupTestDB(t) | |
| 13 | + dbs := setupTestDB(t) | |
| 14 | 14 | |
| 15 | - users := []UserData{ | |
| 15 | + data := []UserData{ | |
| 16 | 16 | {DID: "did:test:u1", Handle: "user1", DisplayName: "User One", AvatarURL: "https://avatar1.png"}, |
| 17 | 17 | {DID: "did:test:u2", Handle: "user2", DisplayName: "User Two"}, |
| 18 | 18 | } |
| 19 | - err := db.BatchCreateUsers(ctx, users) | |
| 19 | + err := dbs.Users.BatchCreateUsers(ctx, data) | |
| 20 | 20 | assert.NilError(t, err) |
| 21 | 21 | |
| 22 | - u1, err := db.GetUser(ctx, "did:test:u1") | |
| 22 | + u1, err := dbs.Users.GetUser(ctx, "did:test:u1") | |
| 23 | 23 | assert.NilError(t, err) |
| 24 | 24 | assert.Equal(t, u1.Handle, "user1") |
| 25 | 25 | assert.Equal(t, u1.DisplayName.String, "User One") |
| 26 | 26 | |
| 27 | - u2, err := db.GetUser(ctx, "did:test:u2") | |
| 27 | + u2, err := dbs.Users.GetUser(ctx, "did:test:u2") | |
| 28 | 28 | assert.NilError(t, err) |
| 29 | 29 | assert.Equal(t, u2.Handle, "user2") |
| 30 | 30 | assert.Equal(t, u2.DisplayName.String, "User Two") |
| @@ -32,18 +32,18 @@ func TestBatchCreateUsers_InsertsAll(t *testing.T) { | ||
| 32 | 32 | |
| 33 | 33 | func TestBatchCreateUsers_UpsertsExisting(t *testing.T) { |
| 34 | 34 | ctx := context.Background() |
| 35 | - db := setupTestDB(t) | |
| 35 | + dbs := setupTestDB(t) | |
| 36 | 36 | |
| 37 | - _, err := db.CreateUser(ctx, "did:test:u1", "old-handle", "", "") | |
| 37 | + _, err := dbs.Users.CreateUser(ctx, "did:test:u1", "old-handle", "", "") | |
| 38 | 38 | assert.NilError(t, err) |
| 39 | 39 | |
| 40 | - users := []UserData{ | |
| 40 | + data := []UserData{ | |
| 41 | 41 | {DID: "did:test:u1", Handle: "new-handle", DisplayName: "New Name"}, |
| 42 | 42 | } |
| 43 | - err = db.BatchCreateUsers(ctx, users) | |
| 43 | + err = dbs.Users.BatchCreateUsers(ctx, data) | |
| 44 | 44 | assert.NilError(t, err) |
| 45 | 45 | |
| 46 | - u, err := db.GetUser(ctx, "did:test:u1") | |
| 46 | + u, err := dbs.Users.GetUser(ctx, "did:test:u1") | |
| 47 | 47 | assert.NilError(t, err) |
| 48 | 48 | assert.Equal(t, u.Handle, "new-handle") |
| 49 | 49 | assert.Equal(t, u.DisplayName.String, "New Name") |
| @@ -51,217 +51,217 @@ func TestBatchCreateUsers_UpsertsExisting(t *testing.T) { | ||
| 51 | 51 | |
| 52 | 52 | func TestBatchCreateUsers_Empty(t *testing.T) { |
| 53 | 53 | ctx := context.Background() |
| 54 | - db := setupTestDB(t) | |
| 54 | + dbs := setupTestDB(t) | |
| 55 | 55 | |
| 56 | - err := db.BatchCreateUsers(ctx, nil) | |
| 56 | + err := dbs.Users.BatchCreateUsers(ctx, nil) | |
| 57 | 57 | assert.NilError(t, err) |
| 58 | 58 | } |
| 59 | 59 | |
| 60 | 60 | func TestBatchCreateUsers_DoesNotOverwriteWithEmpty(t *testing.T) { |
| 61 | 61 | ctx := context.Background() |
| 62 | - db := setupTestDB(t) | |
| 62 | + dbs := setupTestDB(t) | |
| 63 | 63 | |
| 64 | - _, err := db.CreateUser(ctx, "did:test:u1", "handle", "Existing Name", "https://avatar.png") | |
| 64 | + _, err := dbs.Users.CreateUser(ctx, "did:test:u1", "handle", "Existing Name", "https://avatar.png") | |
| 65 | 65 | assert.NilError(t, err) |
| 66 | 66 | |
| 67 | - users := []UserData{ | |
| 67 | + data := []UserData{ | |
| 68 | 68 | {DID: "did:test:u1", Handle: "", DisplayName: "", AvatarURL: ""}, |
| 69 | 69 | } |
| 70 | - err = db.BatchCreateUsers(ctx, users) | |
| 70 | + err = dbs.Users.BatchCreateUsers(ctx, data) | |
| 71 | 71 | assert.NilError(t, err) |
| 72 | 72 | |
| 73 | - u, err := db.GetUser(ctx, "did:test:u1") | |
| 73 | + u, err := dbs.Users.GetUser(ctx, "did:test:u1") | |
| 74 | 74 | assert.NilError(t, err) |
| 75 | 75 | assert.Equal(t, u.Handle, "handle") |
| 76 | 76 | assert.Equal(t, u.DisplayName.String, "Existing Name") |
| 77 | 77 | assert.Equal(t, u.AvatarURL.String, "https://avatar.png") |
| 78 | 78 | } |
| 79 | 79 | |
| 80 | -func seedSubscriptionData(t *testing.T, ctx context.Context, database *DB) (userDID string) { | |
| 80 | +func seedSubscriptionData(t *testing.T, ctx context.Context, dbs *Databases) (userDID string) { | |
| 81 | 81 | t.Helper() |
| 82 | 82 | userDID = "did:test:subuser" |
| 83 | - _, err := database.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, userDID, "subuser") | |
| 83 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, userDID, "subuser") | |
| 84 | 84 | assert.NilError(t, err) |
| 85 | 85 | return userDID |
| 86 | 86 | } |
| 87 | 87 | |
| 88 | 88 | func TestBatchUpsertFeeds_InsertsAll(t *testing.T) { |
| 89 | 89 | ctx := context.Background() |
| 90 | - db := setupTestDB(t) | |
| 90 | + dbs := setupTestDB(t) | |
| 91 | 91 | |
| 92 | 92 | feeds := []*Feed{ |
| 93 | 93 | {FeedURL: "https://a.com/feed.xml", Title: NullStr("Feed A")}, |
| 94 | 94 | {FeedURL: "https://b.com/feed.xml", Title: NullStr("Feed B")}, |
| 95 | 95 | } |
| 96 | - err := db.BatchUpsertFeeds(ctx, feeds) | |
| 96 | + err := dbs.Articles.BatchUpsertFeeds(ctx, feeds) | |
| 97 | 97 | assert.NilError(t, err) |
| 98 | 98 | |
| 99 | - f, err := db.GetFeed(ctx, "https://a.com/feed.xml") | |
| 99 | + f, err := dbs.Articles.GetFeed(ctx, "https://a.com/feed.xml") | |
| 100 | 100 | assert.NilError(t, err) |
| 101 | 101 | assert.Equal(t, f.Title.String, "Feed A") |
| 102 | 102 | |
| 103 | - f, err = db.GetFeed(ctx, "https://b.com/feed.xml") | |
| 103 | + f, err = dbs.Articles.GetFeed(ctx, "https://b.com/feed.xml") | |
| 104 | 104 | assert.NilError(t, err) |
| 105 | 105 | assert.Equal(t, f.Title.String, "Feed B") |
| 106 | 106 | } |
| 107 | 107 | |
| 108 | 108 | func TestBatchUpsertFeeds_UpdatesExisting(t *testing.T) { |
| 109 | 109 | ctx := context.Background() |
| 110 | - db := setupTestDB(t) | |
| 110 | + dbs := setupTestDB(t) | |
| 111 | 111 | |
| 112 | - err := db.UpsertFeed(ctx, &Feed{FeedURL: "https://a.com/feed.xml", Title: NullStr("Old Title")}) | |
| 112 | + err := dbs.Articles.UpsertFeed(ctx, &Feed{FeedURL: "https://a.com/feed.xml", Title: NullStr("Old Title")}) | |
| 113 | 113 | assert.NilError(t, err) |
| 114 | 114 | |
| 115 | 115 | feeds := []*Feed{ |
| 116 | 116 | {FeedURL: "https://a.com/feed.xml", Title: NullStr("New Title")}, |
| 117 | 117 | } |
| 118 | - err = db.BatchUpsertFeeds(ctx, feeds) | |
| 118 | + err = dbs.Articles.BatchUpsertFeeds(ctx, feeds) | |
| 119 | 119 | assert.NilError(t, err) |
| 120 | 120 | |
| 121 | - f, err := db.GetFeed(ctx, "https://a.com/feed.xml") | |
| 121 | + f, err := dbs.Articles.GetFeed(ctx, "https://a.com/feed.xml") | |
| 122 | 122 | assert.NilError(t, err) |
| 123 | 123 | assert.Equal(t, f.Title.String, "New Title") |
| 124 | 124 | } |
| 125 | 125 | |
| 126 | 126 | func TestBatchReconcileSubscriptions_CreatesNew(t *testing.T) { |
| 127 | 127 | ctx := context.Background() |
| 128 | - database := setupTestDB(t) | |
| 129 | - userDID := seedSubscriptionData(t, ctx, database) | |
| 128 | + dbs := setupTestDB(t) | |
| 129 | + userDID := seedSubscriptionData(t, ctx, dbs) | |
| 130 | 130 | |
| 131 | - _ = database.UpsertFeed(ctx, &Feed{FeedURL: "https://a.com/feed.xml", Title: NullStr("Feed A")}) | |
| 132 | - _ = database.UpsertFeed(ctx, &Feed{FeedURL: "https://b.com/feed.xml", Title: NullStr("Feed B")}) | |
| 131 | + _ = dbs.Articles.UpsertFeed(ctx, &Feed{FeedURL: "https://a.com/feed.xml", Title: NullStr("Feed A")}) | |
| 132 | + _ = dbs.Articles.UpsertFeed(ctx, &Feed{FeedURL: "https://b.com/feed.xml", Title: NullStr("Feed B")}) | |
| 133 | 133 | |
| 134 | 134 | subs := []SubData{ |
| 135 | 135 | {FeedURL: "https://a.com/feed.xml", Title: "Feed A", URI: "at://uri1", CID: "cid1"}, |
| 136 | 136 | {FeedURL: "https://b.com/feed.xml", Title: "Feed B", URI: "at://uri2", CID: "cid2"}, |
| 137 | 137 | } |
| 138 | - err := database.BatchReconcileSubscriptions(ctx, userDID, subs) | |
| 138 | + err := dbs.Articles.BatchReconcileSubscriptions(ctx, userDID, subs) | |
| 139 | 139 | assert.NilError(t, err) |
| 140 | 140 | |
| 141 | - subs2, err := database.ListSubscriptions(ctx, userDID, "", 10, 0) | |
| 141 | + subs2, err := dbs.Articles.ListSubscriptions(ctx, userDID, "", 10, 0) | |
| 142 | 142 | assert.NilError(t, err) |
| 143 | 143 | assert.Equal(t, len(subs2), 2) |
| 144 | 144 | |
| 145 | - f, err := database.GetFeed(ctx, "https://a.com/feed.xml") | |
| 145 | + f, err := dbs.Articles.GetFeed(ctx, "https://a.com/feed.xml") | |
| 146 | 146 | assert.NilError(t, err) |
| 147 | 147 | assert.Equal(t, f.SubscriberCount, 1) |
| 148 | 148 | } |
| 149 | 149 | |
| 150 | 150 | func TestBatchReconcileSubscriptions_BackfillsURI(t *testing.T) { |
| 151 | 151 | ctx := context.Background() |
| 152 | - database := setupTestDB(t) | |
| 153 | - userDID := seedSubscriptionData(t, ctx, database) | |
| 152 | + dbs := setupTestDB(t) | |
| 153 | + userDID := seedSubscriptionData(t, ctx, dbs) | |
| 154 | 154 | |
| 155 | - _ = database.UpsertFeed(ctx, &Feed{FeedURL: "https://a.com/feed.xml"}) | |
| 155 | + _ = dbs.Articles.UpsertFeed(ctx, &Feed{FeedURL: "https://a.com/feed.xml"}) | |
| 156 | 156 | |
| 157 | - err := database.CreateSubscription(ctx, userDID, "https://a.com/feed.xml", "Feed A", "", "", "") | |
| 157 | + err := dbs.Articles.CreateSubscription(ctx, userDID, "https://a.com/feed.xml", "Feed A", "", "", "") | |
| 158 | 158 | assert.NilError(t, err) |
| 159 | 159 | |
| 160 | 160 | subs := []SubData{ |
| 161 | 161 | {FeedURL: "https://a.com/feed.xml", URI: "at://new-uri", CID: "new-cid"}, |
| 162 | 162 | } |
| 163 | - err = database.BatchReconcileSubscriptions(ctx, userDID, subs) | |
| 163 | + err = dbs.Articles.BatchReconcileSubscriptions(ctx, userDID, subs) | |
| 164 | 164 | assert.NilError(t, err) |
| 165 | 165 | |
| 166 | - s, err := database.GetSubscription(ctx, userDID, "https://a.com/feed.xml") | |
| 166 | + s, err := dbs.Articles.GetSubscription(ctx, userDID, "https://a.com/feed.xml") | |
| 167 | 167 | assert.NilError(t, err) |
| 168 | 168 | assert.Equal(t, s.URI.String, "at://new-uri") |
| 169 | 169 | |
| 170 | - f, err := database.GetFeed(ctx, "https://a.com/feed.xml") | |
| 170 | + f, err := dbs.Articles.GetFeed(ctx, "https://a.com/feed.xml") | |
| 171 | 171 | assert.NilError(t, err) |
| 172 | 172 | assert.Equal(t, f.SubscriberCount, 1) |
| 173 | 173 | } |
| 174 | 174 | |
| 175 | 175 | func TestBatchReconcileSubscriptions_SkipsExistingWithURI(t *testing.T) { |
| 176 | 176 | ctx := context.Background() |
| 177 | - database := setupTestDB(t) | |
| 178 | - userDID := seedSubscriptionData(t, ctx, database) | |
| 177 | + dbs := setupTestDB(t) | |
| 178 | + userDID := seedSubscriptionData(t, ctx, dbs) | |
| 179 | 179 | |
| 180 | - _ = database.UpsertFeed(ctx, &Feed{FeedURL: "https://a.com/feed.xml"}) | |
| 181 | - err := database.CreateSubscription(ctx, userDID, "https://a.com/feed.xml", "Feed A", "", "at://existing", "cid") | |
| 180 | + _ = dbs.Articles.UpsertFeed(ctx, &Feed{FeedURL: "https://a.com/feed.xml"}) | |
| 181 | + err := dbs.Articles.CreateSubscription(ctx, userDID, "https://a.com/feed.xml", "Feed A", "", "at://existing", "cid") | |
| 182 | 182 | assert.NilError(t, err) |
| 183 | 183 | |
| 184 | 184 | subs := []SubData{ |
| 185 | 185 | {FeedURL: "https://a.com/feed.xml", URI: "at://different", CID: "cid2"}, |
| 186 | 186 | } |
| 187 | - err = database.BatchReconcileSubscriptions(ctx, userDID, subs) | |
| 187 | + err = dbs.Articles.BatchReconcileSubscriptions(ctx, userDID, subs) | |
| 188 | 188 | assert.NilError(t, err) |
| 189 | 189 | |
| 190 | - s, err := database.GetSubscription(ctx, userDID, "https://a.com/feed.xml") | |
| 190 | + s, err := dbs.Articles.GetSubscription(ctx, userDID, "https://a.com/feed.xml") | |
| 191 | 191 | assert.NilError(t, err) |
| 192 | 192 | assert.Equal(t, s.URI.String, "at://existing") |
| 193 | 193 | } |
| 194 | 194 | |
| 195 | 195 | func TestBatchCreateLikes_InsertsAll(t *testing.T) { |
| 196 | 196 | ctx := context.Background() |
| 197 | - database := setupTestDB(t) | |
| 197 | + dbs := setupTestDB(t) | |
| 198 | 198 | |
| 199 | 199 | now := NullTime(time.Now()) |
| 200 | 200 | likes := []*Like{ |
| 201 | 201 | {URI: "at://like1", AuthorDID: "did:test:u1", FeedURL: "https://a.com/feed", ArticleURL: "https://a.com/1", CreatedAt: now, CID: NullStr("cid1")}, |
| 202 | 202 | {URI: "at://like2", AuthorDID: "did:test:u1", FeedURL: "https://a.com/feed", ArticleURL: "https://a.com/2", CreatedAt: now, CID: NullStr("cid2")}, |
| 203 | 203 | } |
| 204 | - err := database.BatchCreateLikes(ctx, likes) | |
| 204 | + err := dbs.Articles.BatchCreateLikes(ctx, likes) | |
| 205 | 205 | assert.NilError(t, err) |
| 206 | 206 | |
| 207 | - exists, err := database.HasLiked(ctx, "did:test:u1", "https://a.com/feed", "https://a.com/1") | |
| 207 | + exists, err := dbs.Articles.HasLiked(ctx, "did:test:u1", "https://a.com/feed", "https://a.com/1") | |
| 208 | 208 | assert.NilError(t, err) |
| 209 | 209 | assert.Equal(t, exists, true) |
| 210 | 210 | |
| 211 | - exists, err = database.HasLiked(ctx, "did:test:u1", "https://a.com/feed", "https://a.com/2") | |
| 211 | + exists, err = dbs.Articles.HasLiked(ctx, "did:test:u1", "https://a.com/feed", "https://a.com/2") | |
| 212 | 212 | assert.NilError(t, err) |
| 213 | 213 | assert.Equal(t, exists, true) |
| 214 | 214 | } |
| 215 | 215 | |
| 216 | 216 | func TestBatchCreateLikes_IgnoresDuplicates(t *testing.T) { |
| 217 | 217 | ctx := context.Background() |
| 218 | - database := setupTestDB(t) | |
| 218 | + dbs := setupTestDB(t) | |
| 219 | 219 | |
| 220 | 220 | now := NullTime(time.Now()) |
| 221 | 221 | likes := []*Like{ |
| 222 | 222 | {URI: "at://like1", AuthorDID: "did:test:u1", FeedURL: "https://a.com/feed", ArticleURL: "https://a.com/1", CreatedAt: now}, |
| 223 | 223 | } |
| 224 | - err := database.BatchCreateLikes(ctx, likes) | |
| 224 | + err := dbs.Articles.BatchCreateLikes(ctx, likes) | |
| 225 | 225 | assert.NilError(t, err) |
| 226 | 226 | |
| 227 | 227 | likes = append(likes, &Like{URI: "at://like1", AuthorDID: "did:test:u1", FeedURL: "https://a.com/feed", ArticleURL: "https://a.com/1", CreatedAt: now}) |
| 228 | - err = database.BatchCreateLikes(ctx, likes) | |
| 228 | + err = dbs.Articles.BatchCreateLikes(ctx, likes) | |
| 229 | 229 | assert.NilError(t, err) |
| 230 | 230 | } |
| 231 | 231 | |
| 232 | 232 | func TestBatchCreateAnnotations_InsertsAll(t *testing.T) { |
| 233 | 233 | ctx := context.Background() |
| 234 | - database := setupTestDB(t) | |
| 234 | + dbs := setupTestDB(t) | |
| 235 | 235 | |
| 236 | 236 | now := NullTime(time.Now()) |
| 237 | 237 | annotations := []*Annotation{ |
| 238 | 238 | {URI: "at://ann1", AuthorDID: "did:test:u1", FeedURL: "https://a.com/feed", ArticleURL: "https://a.com/1", Note: NullStr("Great"), CreatedAt: now}, |
| 239 | 239 | {URI: "at://ann2", AuthorDID: "did:test:u1", FeedURL: "https://a.com/feed", ArticleURL: "https://a.com/2", Note: NullStr("Nice"), CreatedAt: now}, |
| 240 | 240 | } |
| 241 | - err := database.BatchCreateAnnotations(ctx, annotations) | |
| 241 | + err := dbs.Articles.BatchCreateAnnotations(ctx, annotations) | |
| 242 | 242 | assert.NilError(t, err) |
| 243 | 243 | |
| 244 | - exists, err := database.AnnotationExists(ctx, "at://ann1") | |
| 244 | + exists, err := dbs.Articles.AnnotationExists(ctx, "at://ann1") | |
| 245 | 245 | assert.NilError(t, err) |
| 246 | 246 | assert.Equal(t, exists, true) |
| 247 | 247 | |
| 248 | - exists, err = database.AnnotationExists(ctx, "at://ann2") | |
| 248 | + exists, err = dbs.Articles.AnnotationExists(ctx, "at://ann2") | |
| 249 | 249 | assert.NilError(t, err) |
| 250 | 250 | assert.Equal(t, exists, true) |
| 251 | 251 | } |
| 252 | 252 | |
| 253 | 253 | func TestBatchCreateAnnotations_IgnoresDuplicates(t *testing.T) { |
| 254 | 254 | ctx := context.Background() |
| 255 | - database := setupTestDB(t) | |
| 255 | + dbs := setupTestDB(t) | |
| 256 | 256 | |
| 257 | 257 | now := NullTime(time.Now()) |
| 258 | 258 | annotations := []*Annotation{ |
| 259 | 259 | {URI: "at://ann1", AuthorDID: "did:test:u1", FeedURL: "https://a.com/feed", ArticleURL: "https://a.com/1", CreatedAt: now}, |
| 260 | 260 | } |
| 261 | - err := database.BatchCreateAnnotations(ctx, annotations) | |
| 261 | + err := dbs.Articles.BatchCreateAnnotations(ctx, annotations) | |
| 262 | 262 | assert.NilError(t, err) |
| 263 | 263 | |
| 264 | 264 | annotations = append(annotations, &Annotation{URI: "at://ann1", AuthorDID: "did:test:u1", FeedURL: "https://a.com/feed", ArticleURL: "https://a.com/1", CreatedAt: now}) |
| 265 | - err = database.BatchCreateAnnotations(ctx, annotations) | |
| 265 | + err = dbs.Articles.BatchCreateAnnotations(ctx, annotations) | |
| 266 | 266 | assert.NilError(t, err) |
| 267 | 267 | } |
| @@ -10,21 +10,21 @@ import ( | |||
| 10 | 10 | ||
| 11 | func TestBatchCreateUsers_InsertsAll(t *testing.T) { | 11 | func TestBatchCreateUsers_InsertsAll(t *testing.T) { |
| 12 | ctx := context.Background() | 12 | ctx := context.Background() |
| 13 | - db := setupTestDB(t) | 13 | + dbs := setupTestDB(t) |
| 14 | 14 | ||
| 15 | - users := []UserData{ | 15 | + data := []UserData{ |
| 16 | {DID: "did:test:u1", Handle: "user1", DisplayName: "User One", AvatarURL: "https://avatar1.png"}, | 16 | {DID: "did:test:u1", Handle: "user1", DisplayName: "User One", AvatarURL: "https://avatar1.png"}, |
| 17 | {DID: "did:test:u2", Handle: "user2", DisplayName: "User Two"}, | 17 | {DID: "did:test:u2", Handle: "user2", DisplayName: "User Two"}, |
| 18 | } | 18 | } |
| 19 | - err := db.BatchCreateUsers(ctx, users) | 19 | + err := dbs.Users.BatchCreateUsers(ctx, data) |
| 20 | assert.NilError(t, err) | 20 | assert.NilError(t, err) |
| 21 | 21 | ||
| 22 | - u1, err := db.GetUser(ctx, "did:test:u1") | 22 | + u1, err := dbs.Users.GetUser(ctx, "did:test:u1") |
| 23 | assert.NilError(t, err) | 23 | assert.NilError(t, err) |
| 24 | assert.Equal(t, u1.Handle, "user1") | 24 | assert.Equal(t, u1.Handle, "user1") |
| 25 | assert.Equal(t, u1.DisplayName.String, "User One") | 25 | assert.Equal(t, u1.DisplayName.String, "User One") |
| 26 | 26 | ||
| 27 | - u2, err := db.GetUser(ctx, "did:test:u2") | 27 | + u2, err := dbs.Users.GetUser(ctx, "did:test:u2") |
| 28 | assert.NilError(t, err) | 28 | assert.NilError(t, err) |
| 29 | assert.Equal(t, u2.Handle, "user2") | 29 | assert.Equal(t, u2.Handle, "user2") |
| 30 | assert.Equal(t, u2.DisplayName.String, "User Two") | 30 | assert.Equal(t, u2.DisplayName.String, "User Two") |
| @@ -32,18 +32,18 @@ func TestBatchCreateUsers_InsertsAll(t *testing.T) { | |||
| 32 | 32 | ||
| 33 | func TestBatchCreateUsers_UpsertsExisting(t *testing.T) { | 33 | func TestBatchCreateUsers_UpsertsExisting(t *testing.T) { |
| 34 | ctx := context.Background() | 34 | ctx := context.Background() |
| 35 | - db := setupTestDB(t) | 35 | + dbs := setupTestDB(t) |
| 36 | 36 | ||
| 37 | - _, err := db.CreateUser(ctx, "did:test:u1", "old-handle", "", "") | 37 | + _, err := dbs.Users.CreateUser(ctx, "did:test:u1", "old-handle", "", "") |
| 38 | assert.NilError(t, err) | 38 | assert.NilError(t, err) |
| 39 | 39 | ||
| 40 | - users := []UserData{ | 40 | + data := []UserData{ |
| 41 | {DID: "did:test:u1", Handle: "new-handle", DisplayName: "New Name"}, | 41 | {DID: "did:test:u1", Handle: "new-handle", DisplayName: "New Name"}, |
| 42 | } | 42 | } |
| 43 | - err = db.BatchCreateUsers(ctx, users) | 43 | + err = dbs.Users.BatchCreateUsers(ctx, data) |
| 44 | assert.NilError(t, err) | 44 | assert.NilError(t, err) |
| 45 | 45 | ||
| 46 | - u, err := db.GetUser(ctx, "did:test:u1") | 46 | + u, err := dbs.Users.GetUser(ctx, "did:test:u1") |
| 47 | assert.NilError(t, err) | 47 | assert.NilError(t, err) |
| 48 | assert.Equal(t, u.Handle, "new-handle") | 48 | assert.Equal(t, u.Handle, "new-handle") |
| 49 | assert.Equal(t, u.DisplayName.String, "New Name") | 49 | assert.Equal(t, u.DisplayName.String, "New Name") |
| @@ -51,217 +51,217 @@ func TestBatchCreateUsers_UpsertsExisting(t *testing.T) { | |||
| 51 | 51 | ||
| 52 | func TestBatchCreateUsers_Empty(t *testing.T) { | 52 | func TestBatchCreateUsers_Empty(t *testing.T) { |
| 53 | ctx := context.Background() | 53 | ctx := context.Background() |
| 54 | - db := setupTestDB(t) | 54 | + dbs := setupTestDB(t) |
| 55 | 55 | ||
| 56 | - err := db.BatchCreateUsers(ctx, nil) | 56 | + err := dbs.Users.BatchCreateUsers(ctx, nil) |
| 57 | assert.NilError(t, err) | 57 | assert.NilError(t, err) |
| 58 | } | 58 | } |
| 59 | 59 | ||
| 60 | func TestBatchCreateUsers_DoesNotOverwriteWithEmpty(t *testing.T) { | 60 | func TestBatchCreateUsers_DoesNotOverwriteWithEmpty(t *testing.T) { |
| 61 | ctx := context.Background() | 61 | ctx := context.Background() |
| 62 | - db := setupTestDB(t) | 62 | + dbs := setupTestDB(t) |
| 63 | 63 | ||
| 64 | - _, err := db.CreateUser(ctx, "did:test:u1", "handle", "Existing Name", "https://avatar.png") | 64 | + _, err := dbs.Users.CreateUser(ctx, "did:test:u1", "handle", "Existing Name", "https://avatar.png") |
| 65 | assert.NilError(t, err) | 65 | assert.NilError(t, err) |
| 66 | 66 | ||
| 67 | - users := []UserData{ | 67 | + data := []UserData{ |
| 68 | {DID: "did:test:u1", Handle: "", DisplayName: "", AvatarURL: ""}, | 68 | {DID: "did:test:u1", Handle: "", DisplayName: "", AvatarURL: ""}, |
| 69 | } | 69 | } |
| 70 | - err = db.BatchCreateUsers(ctx, users) | 70 | + err = dbs.Users.BatchCreateUsers(ctx, data) |
| 71 | assert.NilError(t, err) | 71 | assert.NilError(t, err) |
| 72 | 72 | ||
| 73 | - u, err := db.GetUser(ctx, "did:test:u1") | 73 | + u, err := dbs.Users.GetUser(ctx, "did:test:u1") |
| 74 | assert.NilError(t, err) | 74 | assert.NilError(t, err) |
| 75 | assert.Equal(t, u.Handle, "handle") | 75 | assert.Equal(t, u.Handle, "handle") |
| 76 | assert.Equal(t, u.DisplayName.String, "Existing Name") | 76 | assert.Equal(t, u.DisplayName.String, "Existing Name") |
| 77 | assert.Equal(t, u.AvatarURL.String, "https://avatar.png") | 77 | assert.Equal(t, u.AvatarURL.String, "https://avatar.png") |
| 78 | } | 78 | } |
| 79 | 79 | ||
| 80 | -func seedSubscriptionData(t *testing.T, ctx context.Context, database *DB) (userDID string) { | 80 | +func seedSubscriptionData(t *testing.T, ctx context.Context, dbs *Databases) (userDID string) { |
| 81 | t.Helper() | 81 | t.Helper() |
| 82 | userDID = "did:test:subuser" | 82 | userDID = "did:test:subuser" |
| 83 | - _, err := database.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, userDID, "subuser") | 83 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, userDID, "subuser") |
| 84 | assert.NilError(t, err) | 84 | assert.NilError(t, err) |
| 85 | return userDID | 85 | return userDID |
| 86 | } | 86 | } |
| 87 | 87 | ||
| 88 | func TestBatchUpsertFeeds_InsertsAll(t *testing.T) { | 88 | func TestBatchUpsertFeeds_InsertsAll(t *testing.T) { |
| 89 | ctx := context.Background() | 89 | ctx := context.Background() |
| 90 | - db := setupTestDB(t) | 90 | + dbs := setupTestDB(t) |
| 91 | 91 | ||
| 92 | feeds := []*Feed{ | 92 | feeds := []*Feed{ |
| 93 | {FeedURL: "https://a.com/feed.xml", Title: NullStr("Feed A")}, | 93 | {FeedURL: "https://a.com/feed.xml", Title: NullStr("Feed A")}, |
| 94 | {FeedURL: "https://b.com/feed.xml", Title: NullStr("Feed B")}, | 94 | {FeedURL: "https://b.com/feed.xml", Title: NullStr("Feed B")}, |
| 95 | } | 95 | } |
| 96 | - err := db.BatchUpsertFeeds(ctx, feeds) | 96 | + err := dbs.Articles.BatchUpsertFeeds(ctx, feeds) |
| 97 | assert.NilError(t, err) | 97 | assert.NilError(t, err) |
| 98 | 98 | ||
| 99 | - f, err := db.GetFeed(ctx, "https://a.com/feed.xml") | 99 | + f, err := dbs.Articles.GetFeed(ctx, "https://a.com/feed.xml") |
| 100 | assert.NilError(t, err) | 100 | assert.NilError(t, err) |
| 101 | assert.Equal(t, f.Title.String, "Feed A") | 101 | assert.Equal(t, f.Title.String, "Feed A") |
| 102 | 102 | ||
| 103 | - f, err = db.GetFeed(ctx, "https://b.com/feed.xml") | 103 | + f, err = dbs.Articles.GetFeed(ctx, "https://b.com/feed.xml") |
| 104 | assert.NilError(t, err) | 104 | assert.NilError(t, err) |
| 105 | assert.Equal(t, f.Title.String, "Feed B") | 105 | assert.Equal(t, f.Title.String, "Feed B") |
| 106 | } | 106 | } |
| 107 | 107 | ||
| 108 | func TestBatchUpsertFeeds_UpdatesExisting(t *testing.T) { | 108 | func TestBatchUpsertFeeds_UpdatesExisting(t *testing.T) { |
| 109 | ctx := context.Background() | 109 | ctx := context.Background() |
| 110 | - db := setupTestDB(t) | 110 | + dbs := setupTestDB(t) |
| 111 | 111 | ||
| 112 | - err := db.UpsertFeed(ctx, &Feed{FeedURL: "https://a.com/feed.xml", Title: NullStr("Old Title")}) | 112 | + err := dbs.Articles.UpsertFeed(ctx, &Feed{FeedURL: "https://a.com/feed.xml", Title: NullStr("Old Title")}) |
| 113 | assert.NilError(t, err) | 113 | assert.NilError(t, err) |
| 114 | 114 | ||
| 115 | feeds := []*Feed{ | 115 | feeds := []*Feed{ |
| 116 | {FeedURL: "https://a.com/feed.xml", Title: NullStr("New Title")}, | 116 | {FeedURL: "https://a.com/feed.xml", Title: NullStr("New Title")}, |
| 117 | } | 117 | } |
| 118 | - err = db.BatchUpsertFeeds(ctx, feeds) | 118 | + err = dbs.Articles.BatchUpsertFeeds(ctx, feeds) |
| 119 | assert.NilError(t, err) | 119 | assert.NilError(t, err) |
| 120 | 120 | ||
| 121 | - f, err := db.GetFeed(ctx, "https://a.com/feed.xml") | 121 | + f, err := dbs.Articles.GetFeed(ctx, "https://a.com/feed.xml") |
| 122 | assert.NilError(t, err) | 122 | assert.NilError(t, err) |
| 123 | assert.Equal(t, f.Title.String, "New Title") | 123 | assert.Equal(t, f.Title.String, "New Title") |
| 124 | } | 124 | } |
| 125 | 125 | ||
| 126 | func TestBatchReconcileSubscriptions_CreatesNew(t *testing.T) { | 126 | func TestBatchReconcileSubscriptions_CreatesNew(t *testing.T) { |
| 127 | ctx := context.Background() | 127 | ctx := context.Background() |
| 128 | - database := setupTestDB(t) | 128 | + dbs := setupTestDB(t) |
| 129 | - userDID := seedSubscriptionData(t, ctx, database) | 129 | + userDID := seedSubscriptionData(t, ctx, dbs) |
| 130 | 130 | ||
| 131 | - _ = database.UpsertFeed(ctx, &Feed{FeedURL: "https://a.com/feed.xml", Title: NullStr("Feed A")}) | 131 | + _ = dbs.Articles.UpsertFeed(ctx, &Feed{FeedURL: "https://a.com/feed.xml", Title: NullStr("Feed A")}) |
| 132 | - _ = database.UpsertFeed(ctx, &Feed{FeedURL: "https://b.com/feed.xml", Title: NullStr("Feed B")}) | 132 | + _ = dbs.Articles.UpsertFeed(ctx, &Feed{FeedURL: "https://b.com/feed.xml", Title: NullStr("Feed B")}) |
| 133 | 133 | ||
| 134 | subs := []SubData{ | 134 | subs := []SubData{ |
| 135 | {FeedURL: "https://a.com/feed.xml", Title: "Feed A", URI: "at://uri1", CID: "cid1"}, | 135 | {FeedURL: "https://a.com/feed.xml", Title: "Feed A", URI: "at://uri1", CID: "cid1"}, |
| 136 | {FeedURL: "https://b.com/feed.xml", Title: "Feed B", URI: "at://uri2", CID: "cid2"}, | 136 | {FeedURL: "https://b.com/feed.xml", Title: "Feed B", URI: "at://uri2", CID: "cid2"}, |
| 137 | } | 137 | } |
| 138 | - err := database.BatchReconcileSubscriptions(ctx, userDID, subs) | 138 | + err := dbs.Articles.BatchReconcileSubscriptions(ctx, userDID, subs) |
| 139 | assert.NilError(t, err) | 139 | assert.NilError(t, err) |
| 140 | 140 | ||
| 141 | - subs2, err := database.ListSubscriptions(ctx, userDID, "", 10, 0) | 141 | + subs2, err := dbs.Articles.ListSubscriptions(ctx, userDID, "", 10, 0) |
| 142 | assert.NilError(t, err) | 142 | assert.NilError(t, err) |
| 143 | assert.Equal(t, len(subs2), 2) | 143 | assert.Equal(t, len(subs2), 2) |
| 144 | 144 | ||
| 145 | - f, err := database.GetFeed(ctx, "https://a.com/feed.xml") | 145 | + f, err := dbs.Articles.GetFeed(ctx, "https://a.com/feed.xml") |
| 146 | assert.NilError(t, err) | 146 | assert.NilError(t, err) |
| 147 | assert.Equal(t, f.SubscriberCount, 1) | 147 | assert.Equal(t, f.SubscriberCount, 1) |
| 148 | } | 148 | } |
| 149 | 149 | ||
| 150 | func TestBatchReconcileSubscriptions_BackfillsURI(t *testing.T) { | 150 | func TestBatchReconcileSubscriptions_BackfillsURI(t *testing.T) { |
| 151 | ctx := context.Background() | 151 | ctx := context.Background() |
| 152 | - database := setupTestDB(t) | 152 | + dbs := setupTestDB(t) |
| 153 | - userDID := seedSubscriptionData(t, ctx, database) | 153 | + userDID := seedSubscriptionData(t, ctx, dbs) |
| 154 | 154 | ||
| 155 | - _ = database.UpsertFeed(ctx, &Feed{FeedURL: "https://a.com/feed.xml"}) | 155 | + _ = dbs.Articles.UpsertFeed(ctx, &Feed{FeedURL: "https://a.com/feed.xml"}) |
| 156 | 156 | ||
| 157 | - err := database.CreateSubscription(ctx, userDID, "https://a.com/feed.xml", "Feed A", "", "", "") | 157 | + err := dbs.Articles.CreateSubscription(ctx, userDID, "https://a.com/feed.xml", "Feed A", "", "", "") |
| 158 | assert.NilError(t, err) | 158 | assert.NilError(t, err) |
| 159 | 159 | ||
| 160 | subs := []SubData{ | 160 | subs := []SubData{ |
| 161 | {FeedURL: "https://a.com/feed.xml", URI: "at://new-uri", CID: "new-cid"}, | 161 | {FeedURL: "https://a.com/feed.xml", URI: "at://new-uri", CID: "new-cid"}, |
| 162 | } | 162 | } |
| 163 | - err = database.BatchReconcileSubscriptions(ctx, userDID, subs) | 163 | + err = dbs.Articles.BatchReconcileSubscriptions(ctx, userDID, subs) |
| 164 | assert.NilError(t, err) | 164 | assert.NilError(t, err) |
| 165 | 165 | ||
| 166 | - s, err := database.GetSubscription(ctx, userDID, "https://a.com/feed.xml") | 166 | + s, err := dbs.Articles.GetSubscription(ctx, userDID, "https://a.com/feed.xml") |
| 167 | assert.NilError(t, err) | 167 | assert.NilError(t, err) |
| 168 | assert.Equal(t, s.URI.String, "at://new-uri") | 168 | assert.Equal(t, s.URI.String, "at://new-uri") |
| 169 | 169 | ||
| 170 | - f, err := database.GetFeed(ctx, "https://a.com/feed.xml") | 170 | + f, err := dbs.Articles.GetFeed(ctx, "https://a.com/feed.xml") |
| 171 | assert.NilError(t, err) | 171 | assert.NilError(t, err) |
| 172 | assert.Equal(t, f.SubscriberCount, 1) | 172 | assert.Equal(t, f.SubscriberCount, 1) |
| 173 | } | 173 | } |
| 174 | 174 | ||
| 175 | func TestBatchReconcileSubscriptions_SkipsExistingWithURI(t *testing.T) { | 175 | func TestBatchReconcileSubscriptions_SkipsExistingWithURI(t *testing.T) { |
| 176 | ctx := context.Background() | 176 | ctx := context.Background() |
| 177 | - database := setupTestDB(t) | 177 | + dbs := setupTestDB(t) |
| 178 | - userDID := seedSubscriptionData(t, ctx, database) | 178 | + userDID := seedSubscriptionData(t, ctx, dbs) |
| 179 | 179 | ||
| 180 | - _ = database.UpsertFeed(ctx, &Feed{FeedURL: "https://a.com/feed.xml"}) | 180 | + _ = dbs.Articles.UpsertFeed(ctx, &Feed{FeedURL: "https://a.com/feed.xml"}) |
| 181 | - err := database.CreateSubscription(ctx, userDID, "https://a.com/feed.xml", "Feed A", "", "at://existing", "cid") | 181 | + err := dbs.Articles.CreateSubscription(ctx, userDID, "https://a.com/feed.xml", "Feed A", "", "at://existing", "cid") |
| 182 | assert.NilError(t, err) | 182 | assert.NilError(t, err) |
| 183 | 183 | ||
| 184 | subs := []SubData{ | 184 | subs := []SubData{ |
| 185 | {FeedURL: "https://a.com/feed.xml", URI: "at://different", CID: "cid2"}, | 185 | {FeedURL: "https://a.com/feed.xml", URI: "at://different", CID: "cid2"}, |
| 186 | } | 186 | } |
| 187 | - err = database.BatchReconcileSubscriptions(ctx, userDID, subs) | 187 | + err = dbs.Articles.BatchReconcileSubscriptions(ctx, userDID, subs) |
| 188 | assert.NilError(t, err) | 188 | assert.NilError(t, err) |
| 189 | 189 | ||
| 190 | - s, err := database.GetSubscription(ctx, userDID, "https://a.com/feed.xml") | 190 | + s, err := dbs.Articles.GetSubscription(ctx, userDID, "https://a.com/feed.xml") |
| 191 | assert.NilError(t, err) | 191 | assert.NilError(t, err) |
| 192 | assert.Equal(t, s.URI.String, "at://existing") | 192 | assert.Equal(t, s.URI.String, "at://existing") |
| 193 | } | 193 | } |
| 194 | 194 | ||
| 195 | func TestBatchCreateLikes_InsertsAll(t *testing.T) { | 195 | func TestBatchCreateLikes_InsertsAll(t *testing.T) { |
| 196 | ctx := context.Background() | 196 | ctx := context.Background() |
| 197 | - database := setupTestDB(t) | 197 | + dbs := setupTestDB(t) |
| 198 | 198 | ||
| 199 | now := NullTime(time.Now()) | 199 | now := NullTime(time.Now()) |
| 200 | likes := []*Like{ | 200 | likes := []*Like{ |
| 201 | {URI: "at://like1", AuthorDID: "did:test:u1", FeedURL: "https://a.com/feed", ArticleURL: "https://a.com/1", CreatedAt: now, CID: NullStr("cid1")}, | 201 | {URI: "at://like1", AuthorDID: "did:test:u1", FeedURL: "https://a.com/feed", ArticleURL: "https://a.com/1", CreatedAt: now, CID: NullStr("cid1")}, |
| 202 | {URI: "at://like2", AuthorDID: "did:test:u1", FeedURL: "https://a.com/feed", ArticleURL: "https://a.com/2", CreatedAt: now, CID: NullStr("cid2")}, | 202 | {URI: "at://like2", AuthorDID: "did:test:u1", FeedURL: "https://a.com/feed", ArticleURL: "https://a.com/2", CreatedAt: now, CID: NullStr("cid2")}, |
| 203 | } | 203 | } |
| 204 | - err := database.BatchCreateLikes(ctx, likes) | 204 | + err := dbs.Articles.BatchCreateLikes(ctx, likes) |
| 205 | assert.NilError(t, err) | 205 | assert.NilError(t, err) |
| 206 | 206 | ||
| 207 | - exists, err := database.HasLiked(ctx, "did:test:u1", "https://a.com/feed", "https://a.com/1") | 207 | + exists, err := dbs.Articles.HasLiked(ctx, "did:test:u1", "https://a.com/feed", "https://a.com/1") |
| 208 | assert.NilError(t, err) | 208 | assert.NilError(t, err) |
| 209 | assert.Equal(t, exists, true) | 209 | assert.Equal(t, exists, true) |
| 210 | 210 | ||
| 211 | - exists, err = database.HasLiked(ctx, "did:test:u1", "https://a.com/feed", "https://a.com/2") | 211 | + exists, err = dbs.Articles.HasLiked(ctx, "did:test:u1", "https://a.com/feed", "https://a.com/2") |
| 212 | assert.NilError(t, err) | 212 | assert.NilError(t, err) |
| 213 | assert.Equal(t, exists, true) | 213 | assert.Equal(t, exists, true) |
| 214 | } | 214 | } |
| 215 | 215 | ||
| 216 | func TestBatchCreateLikes_IgnoresDuplicates(t *testing.T) { | 216 | func TestBatchCreateLikes_IgnoresDuplicates(t *testing.T) { |
| 217 | ctx := context.Background() | 217 | ctx := context.Background() |
| 218 | - database := setupTestDB(t) | 218 | + dbs := setupTestDB(t) |
| 219 | 219 | ||
| 220 | now := NullTime(time.Now()) | 220 | now := NullTime(time.Now()) |
| 221 | likes := []*Like{ | 221 | likes := []*Like{ |
| 222 | {URI: "at://like1", AuthorDID: "did:test:u1", FeedURL: "https://a.com/feed", ArticleURL: "https://a.com/1", CreatedAt: now}, | 222 | {URI: "at://like1", AuthorDID: "did:test:u1", FeedURL: "https://a.com/feed", ArticleURL: "https://a.com/1", CreatedAt: now}, |
| 223 | } | 223 | } |
| 224 | - err := database.BatchCreateLikes(ctx, likes) | 224 | + err := dbs.Articles.BatchCreateLikes(ctx, likes) |
| 225 | assert.NilError(t, err) | 225 | assert.NilError(t, err) |
| 226 | 226 | ||
| 227 | likes = append(likes, &Like{URI: "at://like1", AuthorDID: "did:test:u1", FeedURL: "https://a.com/feed", ArticleURL: "https://a.com/1", CreatedAt: now}) | 227 | likes = append(likes, &Like{URI: "at://like1", AuthorDID: "did:test:u1", FeedURL: "https://a.com/feed", ArticleURL: "https://a.com/1", CreatedAt: now}) |
| 228 | - err = database.BatchCreateLikes(ctx, likes) | 228 | + err = dbs.Articles.BatchCreateLikes(ctx, likes) |
| 229 | assert.NilError(t, err) | 229 | assert.NilError(t, err) |
| 230 | } | 230 | } |
| 231 | 231 | ||
| 232 | func TestBatchCreateAnnotations_InsertsAll(t *testing.T) { | 232 | func TestBatchCreateAnnotations_InsertsAll(t *testing.T) { |
| 233 | ctx := context.Background() | 233 | ctx := context.Background() |
| 234 | - database := setupTestDB(t) | 234 | + dbs := setupTestDB(t) |
| 235 | 235 | ||
| 236 | now := NullTime(time.Now()) | 236 | now := NullTime(time.Now()) |
| 237 | annotations := []*Annotation{ | 237 | annotations := []*Annotation{ |
| 238 | {URI: "at://ann1", AuthorDID: "did:test:u1", FeedURL: "https://a.com/feed", ArticleURL: "https://a.com/1", Note: NullStr("Great"), CreatedAt: now}, | 238 | {URI: "at://ann1", AuthorDID: "did:test:u1", FeedURL: "https://a.com/feed", ArticleURL: "https://a.com/1", Note: NullStr("Great"), CreatedAt: now}, |
| 239 | {URI: "at://ann2", AuthorDID: "did:test:u1", FeedURL: "https://a.com/feed", ArticleURL: "https://a.com/2", Note: NullStr("Nice"), CreatedAt: now}, | 239 | {URI: "at://ann2", AuthorDID: "did:test:u1", FeedURL: "https://a.com/feed", ArticleURL: "https://a.com/2", Note: NullStr("Nice"), CreatedAt: now}, |
| 240 | } | 240 | } |
| 241 | - err := database.BatchCreateAnnotations(ctx, annotations) | 241 | + err := dbs.Articles.BatchCreateAnnotations(ctx, annotations) |
| 242 | assert.NilError(t, err) | 242 | assert.NilError(t, err) |
| 243 | 243 | ||
| 244 | - exists, err := database.AnnotationExists(ctx, "at://ann1") | 244 | + exists, err := dbs.Articles.AnnotationExists(ctx, "at://ann1") |
| 245 | assert.NilError(t, err) | 245 | assert.NilError(t, err) |
| 246 | assert.Equal(t, exists, true) | 246 | assert.Equal(t, exists, true) |
| 247 | 247 | ||
| 248 | - exists, err = database.AnnotationExists(ctx, "at://ann2") | 248 | + exists, err = dbs.Articles.AnnotationExists(ctx, "at://ann2") |
| 249 | assert.NilError(t, err) | 249 | assert.NilError(t, err) |
| 250 | assert.Equal(t, exists, true) | 250 | assert.Equal(t, exists, true) |
| 251 | } | 251 | } |
| 252 | 252 | ||
| 253 | func TestBatchCreateAnnotations_IgnoresDuplicates(t *testing.T) { | 253 | func TestBatchCreateAnnotations_IgnoresDuplicates(t *testing.T) { |
| 254 | ctx := context.Background() | 254 | ctx := context.Background() |
| 255 | - database := setupTestDB(t) | 255 | + dbs := setupTestDB(t) |
| 256 | 256 | ||
| 257 | now := NullTime(time.Now()) | 257 | now := NullTime(time.Now()) |
| 258 | annotations := []*Annotation{ | 258 | annotations := []*Annotation{ |
| 259 | {URI: "at://ann1", AuthorDID: "did:test:u1", FeedURL: "https://a.com/feed", ArticleURL: "https://a.com/1", CreatedAt: now}, | 259 | {URI: "at://ann1", AuthorDID: "did:test:u1", FeedURL: "https://a.com/feed", ArticleURL: "https://a.com/1", CreatedAt: now}, |
| 260 | } | 260 | } |
| 261 | - err := database.BatchCreateAnnotations(ctx, annotations) | 261 | + err := dbs.Articles.BatchCreateAnnotations(ctx, annotations) |
| 262 | assert.NilError(t, err) | 262 | assert.NilError(t, err) |
| 263 | 263 | ||
| 264 | annotations = append(annotations, &Annotation{URI: "at://ann1", AuthorDID: "did:test:u1", FeedURL: "https://a.com/feed", ArticleURL: "https://a.com/1", CreatedAt: now}) | 264 | annotations = append(annotations, &Annotation{URI: "at://ann1", AuthorDID: "did:test:u1", FeedURL: "https://a.com/feed", ArticleURL: "https://a.com/1", CreatedAt: now}) |
| 265 | - err = database.BatchCreateAnnotations(ctx, annotations) | 265 | + err = dbs.Articles.BatchCreateAnnotations(ctx, annotations) |
| 266 | assert.NilError(t, err) | 266 | assert.NilError(t, err) |
| 267 | } | 267 | } |
modified
internal/db/db.go +0 -2 | @@ -110,8 +110,6 @@ var schema = []string{ | ||
| 110 | 110 | subscriber_count INTEGER NOT NULL DEFAULT 0, |
| 111 | 111 | etag TEXT, |
| 112 | 112 | last_modified TEXT, |
| 113 | - fetch_interval_minutes INTEGER NOT NULL DEFAULT 30, | |
| 114 | - next_fetch_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| 115 | 113 | consecutive_empty_fetches INTEGER NOT NULL DEFAULT 0, |
| 116 | 114 | error_count INTEGER NOT NULL DEFAULT 0, |
| 117 | 115 | favicon_url TEXT |
| @@ -110,8 +110,6 @@ var schema = []string{ | |||
| 110 | subscriber_count INTEGER NOT NULL DEFAULT 0, | 110 | subscriber_count INTEGER NOT NULL DEFAULT 0, |
| 111 | etag TEXT, | 111 | etag TEXT, |
| 112 | last_modified TEXT, | 112 | last_modified TEXT, |
| 113 | - fetch_interval_minutes INTEGER NOT NULL DEFAULT 30, | ||
| 114 | - next_fetch_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 115 | consecutive_empty_fetches INTEGER NOT NULL DEFAULT 0, | 113 | consecutive_empty_fetches INTEGER NOT NULL DEFAULT 0, |
| 116 | error_count INTEGER NOT NULL DEFAULT 0, | 114 | error_count INTEGER NOT NULL DEFAULT 0, |
| 117 | favicon_url TEXT | 115 | favicon_url TEXT |
modified
internal/db/feed.go +89 -91 | @@ -21,8 +21,6 @@ type Feed struct { | ||
| 21 | 21 | SubscriberCount int |
| 22 | 22 | Etag sql.NullString |
| 23 | 23 | LastModified sql.NullString |
| 24 | - FetchIntervalMinutes int | |
| 25 | - NextFetchAt sql.NullTime | |
| 26 | 24 | ConsecutiveEmptyFetches int |
| 27 | 25 | ErrorCount int |
| 28 | 26 | FaviconURL sql.NullString |
| @@ -41,33 +39,33 @@ type Subscription struct { | ||
| 41 | 39 | FaviconURL sql.NullString |
| 42 | 40 | } |
| 43 | 41 | |
| 44 | -func (db *DB) UpsertFeed(ctx context.Context, feed *Feed) error { | |
| 45 | - return db.BatchUpsertFeeds(ctx, []*Feed{feed}) | |
| 42 | +func (s *ArticleStore) UpsertFeed(ctx context.Context, feed *Feed) error { | |
| 43 | + return s.BatchUpsertFeeds(ctx, []*Feed{feed}) | |
| 46 | 44 | } |
| 47 | 45 | |
| 48 | -func (db *DB) GetFeed(ctx context.Context, feedURL string) (*Feed, error) { | |
| 46 | +func (s *ArticleStore) GetFeed(ctx context.Context, feedURL string) (*Feed, error) { | |
| 49 | 47 | f := &Feed{} |
| 50 | - err := db.QueryRowContext(ctx, ` | |
| 48 | + err := s.db.QueryRowContext(ctx, ` | |
| 51 | 49 | SELECT feed_url, title, site_url, description, feed_type, |
| 52 | 50 | last_fetched_at, last_error, subscriber_count, etag, last_modified, |
| 53 | - fetch_interval_minutes, next_fetch_at, consecutive_empty_fetches, error_count, favicon_url | |
| 54 | - FROM feeds WHERE feed_url = ? | |
| 51 | + consecutive_empty_fetches, error_count, favicon_url | |
| 52 | + FROM articles.feeds WHERE feed_url = ? | |
| 55 | 53 | `, feedURL).Scan(&f.FeedURL, &f.Title, &f.SiteURL, &f.Description, &f.FeedType, |
| 56 | 54 | &f.LastFetchedAt, &f.LastError, &f.SubscriberCount, &f.Etag, &f.LastModified, |
| 57 | - &f.FetchIntervalMinutes, &f.NextFetchAt, &f.ConsecutiveEmptyFetches, &f.ErrorCount, &f.FaviconURL) | |
| 55 | + &f.ConsecutiveEmptyFetches, &f.ErrorCount, &f.FaviconURL) | |
| 58 | 56 | if err != nil { |
| 59 | 57 | return nil, err |
| 60 | 58 | } |
| 61 | 59 | return f, nil |
| 62 | 60 | } |
| 63 | 61 | |
| 64 | -func (db *DB) GetFeedsToFetch(ctx context.Context, olderThan time.Duration, limit int) ([]*Feed, error) { | |
| 62 | +func (s *ArticleStore) GetFeedsToFetch(ctx context.Context, olderThan time.Duration, limit int) ([]*Feed, error) { | |
| 65 | 63 | cutoff := time.Now().Add(-olderThan) |
| 66 | - rows, err := db.QueryContext(ctx, ` | |
| 64 | + rows, err := s.db.QueryContext(ctx, ` | |
| 67 | 65 | SELECT feed_url, title, site_url, description, feed_type, |
| 68 | 66 | last_fetched_at, last_error, subscriber_count, etag, last_modified, |
| 69 | - fetch_interval_minutes, next_fetch_at, consecutive_empty_fetches, error_count, favicon_url | |
| 70 | - FROM feeds | |
| 67 | + consecutive_empty_fetches, error_count, favicon_url | |
| 68 | + FROM articles.feeds | |
| 71 | 69 | WHERE subscriber_count > 0 AND error_count < 25 AND (last_fetched_at IS NULL OR last_fetched_at <= ?) |
| 72 | 70 | ORDER BY last_fetched_at ASC NULLS FIRST |
| 73 | 71 | LIMIT ? |
| @@ -82,7 +80,7 @@ func (db *DB) GetFeedsToFetch(ctx context.Context, olderThan time.Duration, limi | ||
| 82 | 80 | f := &Feed{} |
| 83 | 81 | if err := rows.Scan(&f.FeedURL, &f.Title, &f.SiteURL, &f.Description, &f.FeedType, |
| 84 | 82 | &f.LastFetchedAt, &f.LastError, &f.SubscriberCount, &f.Etag, &f.LastModified, |
| 85 | - &f.FetchIntervalMinutes, &f.NextFetchAt, &f.ConsecutiveEmptyFetches, &f.ErrorCount, &f.FaviconURL); err != nil { | |
| 83 | + &f.ConsecutiveEmptyFetches, &f.ErrorCount, &f.FaviconURL); err != nil { | |
| 86 | 84 | return nil, err |
| 87 | 85 | } |
| 88 | 86 | feeds = append(feeds, f) |
| @@ -90,9 +88,9 @@ func (db *DB) GetFeedsToFetch(ctx context.Context, olderThan time.Duration, limi | ||
| 90 | 88 | return feeds, rows.Err() |
| 91 | 89 | } |
| 92 | 90 | |
| 93 | -func (db *DB) MarkFeedFetched(ctx context.Context, feedURL, etag, lastModified string) error { | |
| 94 | - _, err := db.ExecContext(ctx, ` | |
| 95 | - UPDATE feeds SET | |
| 91 | +func (s *ArticleStore) MarkFeedFetched(ctx context.Context, feedURL, etag, lastModified string) error { | |
| 92 | + _, err := s.db.ExecContext(ctx, ` | |
| 93 | + UPDATE articles.feeds SET | |
| 96 | 94 | etag = ?, |
| 97 | 95 | last_modified = ?, |
| 98 | 96 | error_count = 0, |
| @@ -103,9 +101,9 @@ func (db *DB) MarkFeedFetched(ctx context.Context, feedURL, etag, lastModified s | ||
| 103 | 101 | return err |
| 104 | 102 | } |
| 105 | 103 | |
| 106 | -func (db *DB) MarkFeedFetchError(ctx context.Context, feedURL, lastError string) error { | |
| 107 | - _, err := db.ExecContext(ctx, ` | |
| 108 | - UPDATE feeds SET | |
| 104 | +func (s *ArticleStore) MarkFeedFetchError(ctx context.Context, feedURL, lastError string) error { | |
| 105 | + _, err := s.db.ExecContext(ctx, ` | |
| 106 | + UPDATE articles.feeds SET | |
| 109 | 107 | error_count = error_count + 1, |
| 110 | 108 | last_error = ?, |
| 111 | 109 | last_fetched_at = CURRENT_TIMESTAMP |
| @@ -114,27 +112,27 @@ func (db *DB) MarkFeedFetchError(ctx context.Context, feedURL, lastError string) | ||
| 114 | 112 | return err |
| 115 | 113 | } |
| 116 | 114 | |
| 117 | -func (db *DB) decrementSubscriberCount(ctx context.Context, feedURL string) error { | |
| 118 | - _, err := db.ExecContext(ctx, ` | |
| 119 | - UPDATE feeds SET subscriber_count = MAX(subscriber_count - 1, 0) WHERE feed_url = ? | |
| 115 | +func (s *ArticleStore) decrementSubscriberCount(ctx context.Context, feedURL string) error { | |
| 116 | + _, err := s.db.ExecContext(ctx, ` | |
| 117 | + UPDATE articles.feeds SET subscriber_count = MAX(subscriber_count - 1, 0) WHERE feed_url = ? | |
| 120 | 118 | `, feedURL) |
| 121 | 119 | return err |
| 122 | 120 | } |
| 123 | 121 | |
| 124 | -func (db *DB) CreateSubscription(ctx context.Context, userDID, feedURL, title, category, uri, cid string) error { | |
| 125 | - existing, err := db.GetSubscription(ctx, userDID, feedURL) | |
| 122 | +func (s *ArticleStore) CreateSubscription(ctx context.Context, userDID, feedURL, title, category, uri, cid string) error { | |
| 123 | + existing, err := s.GetSubscription(ctx, userDID, feedURL) | |
| 126 | 124 | if err == nil && existing != nil { |
| 127 | 125 | if !existing.URI.Valid || existing.URI.String == "" { |
| 128 | - return db.updateSubscriptionURI(ctx, userDID, feedURL, uri, cid) | |
| 126 | + return s.updateSubscriptionURI(ctx, userDID, feedURL, uri, cid) | |
| 129 | 127 | } |
| 130 | 128 | return ErrDuplicateSubscription |
| 131 | 129 | } |
| 132 | - return db.BatchReconcileSubscriptions(ctx, userDID, []SubData{{FeedURL: feedURL, Title: title, Category: category, URI: uri, CID: cid}}) | |
| 130 | + return s.BatchReconcileSubscriptions(ctx, userDID, []SubData{{FeedURL: feedURL, Title: title, Category: category, URI: uri, CID: cid}}) | |
| 133 | 131 | } |
| 134 | 132 | |
| 135 | -func (db *DB) updateSubscriptionURI(ctx context.Context, userDID, feedURL, uri, cid string) error { | |
| 136 | - _, err := db.ExecContext(ctx, ` | |
| 137 | - UPDATE subscriptions SET uri = ?, cid = ? WHERE user_did = ? AND feed_url = ? | |
| 133 | +func (s *ArticleStore) updateSubscriptionURI(ctx context.Context, userDID, feedURL, uri, cid string) error { | |
| 134 | + _, err := s.db.ExecContext(ctx, ` | |
| 135 | + UPDATE articles.subscriptions SET uri = ?, cid = ? WHERE user_did = ? AND feed_url = ? | |
| 138 | 136 | `, uri, cid, userDID, feedURL) |
| 139 | 137 | return err |
| 140 | 138 | } |
| @@ -153,24 +151,24 @@ func nilIfEmpty(v string) any { | ||
| 153 | 151 | return v |
| 154 | 152 | } |
| 155 | 153 | |
| 156 | -func (db *DB) DeleteSubscription(ctx context.Context, userDID, feedURL string) error { | |
| 157 | - _, err := db.ExecContext(ctx, ` | |
| 158 | - DELETE FROM subscriptions WHERE user_did = ? AND feed_url = ? | |
| 154 | +func (s *ArticleStore) DeleteSubscription(ctx context.Context, userDID, feedURL string) error { | |
| 155 | + _, err := s.db.ExecContext(ctx, ` | |
| 156 | + DELETE FROM articles.subscriptions WHERE user_did = ? AND feed_url = ? | |
| 159 | 157 | `, userDID, feedURL) |
| 160 | 158 | if err != nil { |
| 161 | 159 | return err |
| 162 | 160 | } |
| 163 | - return db.decrementSubscriberCount(ctx, feedURL) | |
| 161 | + return s.decrementSubscriberCount(ctx, feedURL) | |
| 164 | 162 | } |
| 165 | 163 | |
| 166 | -func (db *DB) DeleteAllSubscriptions(ctx context.Context, userDID string) error { | |
| 167 | - tx, err := db.BeginTx(ctx, nil) | |
| 164 | +func (s *ArticleStore) DeleteAllSubscriptions(ctx context.Context, userDID string) error { | |
| 165 | + tx, err := s.db.BeginTx(ctx, nil) | |
| 168 | 166 | if err != nil { |
| 169 | 167 | return err |
| 170 | 168 | } |
| 171 | 169 | defer tx.Rollback() |
| 172 | 170 | |
| 173 | - rows, err := tx.QueryContext(ctx, `SELECT feed_url FROM subscriptions WHERE user_did = ?`, userDID) | |
| 171 | + rows, err := tx.QueryContext(ctx, `SELECT feed_url FROM articles.subscriptions WHERE user_did = ?`, userDID) | |
| 174 | 172 | if err != nil { |
| 175 | 173 | return err |
| 176 | 174 | } |
| @@ -185,7 +183,7 @@ func (db *DB) DeleteAllSubscriptions(ctx context.Context, userDID string) error | ||
| 185 | 183 | } |
| 186 | 184 | rows.Close() |
| 187 | 185 | |
| 188 | - _, err = tx.ExecContext(ctx, `DELETE FROM subscriptions WHERE user_did = ?`, userDID) | |
| 186 | + _, err = tx.ExecContext(ctx, `DELETE FROM articles.subscriptions WHERE user_did = ?`, userDID) | |
| 189 | 187 | if err != nil { |
| 190 | 188 | return err |
| 191 | 189 | } |
| @@ -198,7 +196,7 @@ func (db *DB) DeleteAllSubscriptions(ctx context.Context, userDID string) error | ||
| 198 | 196 | args[i] = u |
| 199 | 197 | } |
| 200 | 198 | _, err = tx.ExecContext(ctx, ` |
| 201 | - UPDATE feeds SET subscriber_count = MAX(subscriber_count - 1, 0) | |
| 199 | + UPDATE articles.feeds SET subscriber_count = MAX(subscriber_count - 1, 0) | |
| 202 | 200 | WHERE feed_url IN (`+strings.Join(ph, ",")+`) |
| 203 | 201 | `, args...) |
| 204 | 202 | if err != nil { |
| @@ -209,41 +207,41 @@ func (db *DB) DeleteAllSubscriptions(ctx context.Context, userDID string) error | ||
| 209 | 207 | return tx.Commit() |
| 210 | 208 | } |
| 211 | 209 | |
| 212 | -func (db *DB) GetSubscriptionByURI(ctx context.Context, userDID, uri string) (*Subscription, error) { | |
| 213 | - s := &Subscription{} | |
| 214 | - err := db.QueryRowContext(ctx, ` | |
| 210 | +func (s *ArticleStore) GetSubscriptionByURI(ctx context.Context, userDID, uri string) (*Subscription, error) { | |
| 211 | + sub := &Subscription{} | |
| 212 | + err := s.db.QueryRowContext(ctx, ` | |
| 215 | 213 | SELECT s.id, s.user_did, s.feed_url, COALESCE(s.title, f.title, ''), s.category, s.added_at, |
| 216 | 214 | s.uri, s.cid |
| 217 | - FROM subscriptions s | |
| 218 | - LEFT JOIN feeds f ON s.feed_url = f.feed_url | |
| 215 | + FROM articles.subscriptions s | |
| 216 | + LEFT JOIN articles.feeds f ON s.feed_url = f.feed_url | |
| 219 | 217 | WHERE s.user_did = ? AND s.uri = ? |
| 220 | - `, userDID, uri).Scan(&s.ID, &s.UserDID, &s.FeedURL, &s.FeedTitle, &s.Category, &s.AddedAt, &s.URI, &s.CID) | |
| 218 | + `, userDID, uri).Scan(&sub.ID, &sub.UserDID, &sub.FeedURL, &sub.FeedTitle, &sub.Category, &sub.AddedAt, &sub.URI, &sub.CID) | |
| 221 | 219 | if err != nil { |
| 222 | 220 | return nil, err |
| 223 | 221 | } |
| 224 | - return s, nil | |
| 222 | + return sub, nil | |
| 225 | 223 | } |
| 226 | 224 | |
| 227 | -func (db *DB) GetSubscription(ctx context.Context, userDID, feedURL string) (*Subscription, error) { | |
| 228 | - s := &Subscription{} | |
| 229 | - err := db.QueryRowContext(ctx, ` | |
| 225 | +func (s *ArticleStore) GetSubscription(ctx context.Context, userDID, feedURL string) (*Subscription, error) { | |
| 226 | + sub := &Subscription{} | |
| 227 | + err := s.db.QueryRowContext(ctx, ` | |
| 230 | 228 | SELECT s.id, s.user_did, s.feed_url, COALESCE(s.title, f.title, ''), s.category, s.added_at, |
| 231 | 229 | s.uri, s.cid |
| 232 | - FROM subscriptions s | |
| 233 | - LEFT JOIN feeds f ON s.feed_url = f.feed_url | |
| 230 | + FROM articles.subscriptions s | |
| 231 | + LEFT JOIN articles.feeds f ON s.feed_url = f.feed_url | |
| 234 | 232 | WHERE s.user_did = ? AND s.feed_url = ? |
| 235 | - `, userDID, feedURL).Scan(&s.ID, &s.UserDID, &s.FeedURL, &s.FeedTitle, &s.Category, &s.AddedAt, &s.URI, &s.CID) | |
| 233 | + `, userDID, feedURL).Scan(&sub.ID, &sub.UserDID, &sub.FeedURL, &sub.FeedTitle, &sub.Category, &sub.AddedAt, &sub.URI, &sub.CID) | |
| 236 | 234 | if err != nil { |
| 237 | 235 | return nil, err |
| 238 | 236 | } |
| 239 | - return s, nil | |
| 237 | + return sub, nil | |
| 240 | 238 | } |
| 241 | 239 | |
| 242 | -func (db *DB) ListSubscriptions(ctx context.Context, userDID, category string, limit, offset int) ([]*Subscription, error) { | |
| 240 | +func (s *ArticleStore) ListSubscriptions(ctx context.Context, userDID, category string, limit, offset int) ([]*Subscription, error) { | |
| 243 | 241 | query := `SELECT s.id, s.user_did, s.feed_url, COALESCE(s.title, f.title, ''), s.category, s.added_at, |
| 244 | 242 | s.uri, s.cid, f.favicon_url |
| 245 | - FROM subscriptions s | |
| 246 | - LEFT JOIN feeds f ON s.feed_url = f.feed_url | |
| 243 | + FROM articles.subscriptions s | |
| 244 | + LEFT JOIN articles.feeds f ON s.feed_url = f.feed_url | |
| 247 | 245 | WHERE s.user_did = ?` |
| 248 | 246 | args := []any{userDID} |
| 249 | 247 | |
| @@ -255,7 +253,7 @@ func (db *DB) ListSubscriptions(ctx context.Context, userDID, category string, l | ||
| 255 | 253 | query += ` ORDER BY s.added_at DESC LIMIT ? OFFSET ?` |
| 256 | 254 | args = append(args, limit, offset) |
| 257 | 255 | |
| 258 | - rows, err := db.QueryContext(ctx, query, args...) | |
| 256 | + rows, err := s.db.QueryContext(ctx, query, args...) | |
| 259 | 257 | if err != nil { |
| 260 | 258 | return nil, err |
| 261 | 259 | } |
| @@ -272,17 +270,17 @@ func (db *DB) ListSubscriptions(ctx context.Context, userDID, category string, l | ||
| 272 | 270 | return subs, rows.Err() |
| 273 | 271 | } |
| 274 | 272 | |
| 275 | -func (db *DB) GetSubscriptionCount(ctx context.Context, userDID string) (int, error) { | |
| 273 | +func (s *ArticleStore) GetSubscriptionCount(ctx context.Context, userDID string) (int, error) { | |
| 276 | 274 | var count int |
| 277 | - err := db.QueryRowContext(ctx, ` | |
| 278 | - SELECT COUNT(*) FROM subscriptions WHERE user_did = ? | |
| 275 | + err := s.db.QueryRowContext(ctx, ` | |
| 276 | + SELECT COUNT(*) FROM articles.subscriptions WHERE user_did = ? | |
| 279 | 277 | `, userDID).Scan(&count) |
| 280 | 278 | return count, err |
| 281 | 279 | } |
| 282 | 280 | |
| 283 | -func (db *DB) GetCategories(ctx context.Context, userDID string) ([]string, error) { | |
| 284 | - rows, err := db.QueryContext(ctx, ` | |
| 285 | - SELECT DISTINCT category FROM subscriptions | |
| 281 | +func (s *ArticleStore) GetCategories(ctx context.Context, userDID string) ([]string, error) { | |
| 282 | + rows, err := s.db.QueryContext(ctx, ` | |
| 283 | + SELECT DISTINCT category FROM articles.subscriptions | |
| 286 | 284 | WHERE user_did = ? AND category IS NOT NULL AND category != '' |
| 287 | 285 | ORDER BY category |
| 288 | 286 | `, userDID) |
| @@ -302,18 +300,18 @@ func (db *DB) GetCategories(ctx context.Context, userDID string) ([]string, erro | ||
| 302 | 300 | return categories, rows.Err() |
| 303 | 301 | } |
| 304 | 302 | |
| 305 | -func (db *DB) UpdateFeedFavicon(ctx context.Context, feedURL, faviconURL string) error { | |
| 306 | - _, err := db.ExecContext(ctx, `UPDATE feeds SET favicon_url = ? WHERE feed_url = ?`, faviconURL, feedURL) | |
| 303 | +func (s *ArticleStore) UpdateFeedFavicon(ctx context.Context, feedURL, faviconURL string) error { | |
| 304 | + _, err := s.db.ExecContext(ctx, `UPDATE articles.feeds SET favicon_url = ? WHERE feed_url = ?`, faviconURL, feedURL) | |
| 307 | 305 | return err |
| 308 | 306 | } |
| 309 | 307 | |
| 310 | -func (db *DB) ListDeadFeeds(ctx context.Context, userDID string, threshold int) ([]*Feed, error) { | |
| 311 | - rows, err := db.QueryContext(ctx, ` | |
| 308 | +func (s *ArticleStore) ListDeadFeeds(ctx context.Context, userDID string, threshold int) ([]*Feed, error) { | |
| 309 | + rows, err := s.db.QueryContext(ctx, ` | |
| 312 | 310 | SELECT f.feed_url, f.title, f.site_url, f.description, f.feed_type, |
| 313 | 311 | f.last_fetched_at, f.last_error, f.subscriber_count, f.etag, f.last_modified, |
| 314 | - f.fetch_interval_minutes, f.next_fetch_at, f.consecutive_empty_fetches, f.error_count, f.favicon_url | |
| 315 | - FROM feeds f | |
| 316 | - JOIN subscriptions s ON s.feed_url = f.feed_url AND s.user_did = ? | |
| 312 | + f.consecutive_empty_fetches, f.error_count, f.favicon_url | |
| 313 | + FROM articles.feeds f | |
| 314 | + JOIN articles.subscriptions s ON s.feed_url = f.feed_url AND s.user_did = ? | |
| 317 | 315 | WHERE f.error_count >= ? |
| 318 | 316 | ORDER BY f.error_count DESC |
| 319 | 317 | `, userDID, threshold) |
| @@ -327,7 +325,7 @@ func (db *DB) ListDeadFeeds(ctx context.Context, userDID string, threshold int) | ||
| 327 | 325 | f := &Feed{} |
| 328 | 326 | if err := rows.Scan(&f.FeedURL, &f.Title, &f.SiteURL, &f.Description, &f.FeedType, |
| 329 | 327 | &f.LastFetchedAt, &f.LastError, &f.SubscriberCount, &f.Etag, &f.LastModified, |
| 330 | - &f.FetchIntervalMinutes, &f.NextFetchAt, &f.ConsecutiveEmptyFetches, &f.ErrorCount, &f.FaviconURL); err != nil { | |
| 328 | + &f.ConsecutiveEmptyFetches, &f.ErrorCount, &f.FaviconURL); err != nil { | |
| 331 | 329 | return nil, err |
| 332 | 330 | } |
| 333 | 331 | feeds = append(feeds, f) |
| @@ -335,12 +333,12 @@ func (db *DB) ListDeadFeeds(ctx context.Context, userDID string, threshold int) | ||
| 335 | 333 | return feeds, rows.Err() |
| 336 | 334 | } |
| 337 | 335 | |
| 338 | -func (db *DB) ListAllFeeds(ctx context.Context, limit, offset int) ([]*Feed, error) { | |
| 339 | - rows, err := db.QueryContext(ctx, ` | |
| 336 | +func (s *ArticleStore) ListAllFeeds(ctx context.Context, limit, offset int) ([]*Feed, error) { | |
| 337 | + rows, err := s.db.QueryContext(ctx, ` | |
| 340 | 338 | SELECT feed_url, title, site_url, description, feed_type, |
| 341 | 339 | last_fetched_at, last_error, subscriber_count, etag, last_modified, |
| 342 | - fetch_interval_minutes, next_fetch_at, consecutive_empty_fetches, error_count, favicon_url | |
| 343 | - FROM feeds | |
| 340 | + consecutive_empty_fetches, error_count, favicon_url | |
| 341 | + FROM articles.feeds | |
| 344 | 342 | ORDER BY subscriber_count DESC |
| 345 | 343 | LIMIT ? OFFSET ? |
| 346 | 344 | `, limit, offset) |
| @@ -354,7 +352,7 @@ func (db *DB) ListAllFeeds(ctx context.Context, limit, offset int) ([]*Feed, err | ||
| 354 | 352 | f := &Feed{} |
| 355 | 353 | if err := rows.Scan(&f.FeedURL, &f.Title, &f.SiteURL, &f.Description, &f.FeedType, |
| 356 | 354 | &f.LastFetchedAt, &f.LastError, &f.SubscriberCount, &f.Etag, &f.LastModified, |
| 357 | - &f.FetchIntervalMinutes, &f.NextFetchAt, &f.ConsecutiveEmptyFetches, &f.ErrorCount, &f.FaviconURL); err != nil { | |
| 355 | + &f.ConsecutiveEmptyFetches, &f.ErrorCount, &f.FaviconURL); err != nil { | |
| 358 | 356 | return nil, err |
| 359 | 357 | } |
| 360 | 358 | feeds = append(feeds, f) |
| @@ -370,18 +368,18 @@ type SubData struct { | ||
| 370 | 368 | CID string |
| 371 | 369 | } |
| 372 | 370 | |
| 373 | -func (db *DB) BatchUpsertFeeds(ctx context.Context, feeds []*Feed) error { | |
| 371 | +func (s *ArticleStore) BatchUpsertFeeds(ctx context.Context, feeds []*Feed) error { | |
| 374 | 372 | if len(feeds) == 0 { |
| 375 | 373 | return nil |
| 376 | 374 | } |
| 377 | - tx, err := db.BeginTx(ctx, nil) | |
| 375 | + tx, err := s.db.BeginTx(ctx, nil) | |
| 378 | 376 | if err != nil { |
| 379 | 377 | return err |
| 380 | 378 | } |
| 381 | 379 | defer tx.Rollback() |
| 382 | 380 | |
| 383 | 381 | stmt, err := tx.PrepareContext(ctx, ` |
| 384 | - INSERT INTO feeds (feed_url, title, site_url, description, feed_type) | |
| 382 | + INSERT INTO articles.feeds (feed_url, title, site_url, description, feed_type) | |
| 385 | 383 | VALUES (?, ?, ?, ?, ?) |
| 386 | 384 | ON CONFLICT(feed_url) DO UPDATE SET |
| 387 | 385 | title = excluded.title, |
| @@ -402,17 +400,17 @@ func (db *DB) BatchUpsertFeeds(ctx context.Context, feeds []*Feed) error { | ||
| 402 | 400 | return tx.Commit() |
| 403 | 401 | } |
| 404 | 402 | |
| 405 | -func (db *DB) BatchReconcileSubscriptions(ctx context.Context, userDID string, subs []SubData) error { | |
| 403 | +func (s *ArticleStore) BatchReconcileSubscriptions(ctx context.Context, userDID string, subs []SubData) error { | |
| 406 | 404 | if len(subs) == 0 { |
| 407 | 405 | return nil |
| 408 | 406 | } |
| 409 | - tx, err := db.BeginTx(ctx, nil) | |
| 407 | + tx, err := s.db.BeginTx(ctx, nil) | |
| 410 | 408 | if err != nil { |
| 411 | 409 | return err |
| 412 | 410 | } |
| 413 | 411 | defer tx.Rollback() |
| 414 | 412 | |
| 415 | - rows, err := tx.QueryContext(ctx, `SELECT feed_url, COALESCE(uri, '') FROM subscriptions WHERE user_did = ?`, userDID) | |
| 413 | + rows, err := tx.QueryContext(ctx, `SELECT feed_url, COALESCE(uri, '') FROM articles.subscriptions WHERE user_did = ?`, userDID) | |
| 416 | 414 | if err != nil { |
| 417 | 415 | return err |
| 418 | 416 | } |
| @@ -428,7 +426,7 @@ func (db *DB) BatchReconcileSubscriptions(ctx context.Context, userDID string, s | ||
| 428 | 426 | rows.Close() |
| 429 | 427 | |
| 430 | 428 | insertStmt, err := tx.PrepareContext(ctx, ` |
| 431 | - INSERT OR IGNORE INTO subscriptions (user_did, feed_url, title, category, uri, cid) | |
| 429 | + INSERT OR IGNORE INTO articles.subscriptions (user_did, feed_url, title, category, uri, cid) | |
| 432 | 430 | VALUES (?, ?, ?, ?, ?, ?) |
| 433 | 431 | `) |
| 434 | 432 | if err != nil { |
| @@ -437,14 +435,14 @@ func (db *DB) BatchReconcileSubscriptions(ctx context.Context, userDID string, s | ||
| 437 | 435 | defer insertStmt.Close() |
| 438 | 436 | |
| 439 | 437 | updateStmt, err := tx.PrepareContext(ctx, ` |
| 440 | - UPDATE subscriptions SET uri = ?, cid = ? WHERE user_did = ? AND feed_url = ? | |
| 438 | + UPDATE articles.subscriptions SET uri = ?, cid = ? WHERE user_did = ? AND feed_url = ? | |
| 441 | 439 | `) |
| 442 | 440 | if err != nil { |
| 443 | 441 | return err |
| 444 | 442 | } |
| 445 | 443 | defer updateStmt.Close() |
| 446 | 444 | |
| 447 | - incrStmt, err := tx.PrepareContext(ctx, `UPDATE feeds SET subscriber_count = subscriber_count + 1 WHERE feed_url = ?`) | |
| 445 | + incrStmt, err := tx.PrepareContext(ctx, `UPDATE articles.feeds SET subscriber_count = subscriber_count + 1 WHERE feed_url = ?`) | |
| 448 | 446 | if err != nil { |
| 449 | 447 | return err |
| 450 | 448 | } |
| @@ -473,13 +471,13 @@ func (db *DB) BatchReconcileSubscriptions(ctx context.Context, userDID string, s | ||
| 473 | 471 | return tx.Commit() |
| 474 | 472 | } |
| 475 | 473 | |
| 476 | -func (db *DB) ListUnsubscribedFeeds(ctx context.Context, userDID string, limit, offset int) ([]*Feed, error) { | |
| 477 | - rows, err := db.QueryContext(ctx, ` | |
| 474 | +func (s *ArticleStore) ListUnsubscribedFeeds(ctx context.Context, userDID string, limit, offset int) ([]*Feed, error) { | |
| 475 | + rows, err := s.db.QueryContext(ctx, ` | |
| 478 | 476 | SELECT feed_url, title, site_url, description, feed_type, |
| 479 | 477 | last_fetched_at, last_error, subscriber_count, etag, last_modified, |
| 480 | - fetch_interval_minutes, next_fetch_at, consecutive_empty_fetches, error_count, favicon_url | |
| 481 | - FROM feeds | |
| 482 | - WHERE feed_url NOT IN (SELECT feed_url FROM subscriptions WHERE user_did = ?) | |
| 478 | + consecutive_empty_fetches, error_count, favicon_url | |
| 479 | + FROM articles.feeds | |
| 480 | + WHERE feed_url NOT IN (SELECT feed_url FROM articles.subscriptions WHERE user_did = ?) | |
| 483 | 481 | ORDER BY subscriber_count DESC |
| 484 | 482 | LIMIT ? OFFSET ? |
| 485 | 483 | `, userDID, limit, offset) |
| @@ -493,7 +491,7 @@ func (db *DB) ListUnsubscribedFeeds(ctx context.Context, userDID string, limit, | ||
| 493 | 491 | f := &Feed{} |
| 494 | 492 | if err := rows.Scan(&f.FeedURL, &f.Title, &f.SiteURL, &f.Description, &f.FeedType, |
| 495 | 493 | &f.LastFetchedAt, &f.LastError, &f.SubscriberCount, &f.Etag, &f.LastModified, |
| 496 | - &f.FetchIntervalMinutes, &f.NextFetchAt, &f.ConsecutiveEmptyFetches, &f.ErrorCount, &f.FaviconURL); err != nil { | |
| 494 | + &f.ConsecutiveEmptyFetches, &f.ErrorCount, &f.FaviconURL); err != nil { | |
| 497 | 495 | return nil, err |
| 498 | 496 | } |
| 499 | 497 | feeds = append(feeds, f) |
| @@ -21,8 +21,6 @@ type Feed struct { | |||
| 21 | SubscriberCount int | 21 | SubscriberCount int |
| 22 | Etag sql.NullString | 22 | Etag sql.NullString |
| 23 | LastModified sql.NullString | 23 | LastModified sql.NullString |
| 24 | - FetchIntervalMinutes int | ||
| 25 | - NextFetchAt sql.NullTime | ||
| 26 | ConsecutiveEmptyFetches int | 24 | ConsecutiveEmptyFetches int |
| 27 | ErrorCount int | 25 | ErrorCount int |
| 28 | FaviconURL sql.NullString | 26 | FaviconURL sql.NullString |
| @@ -41,33 +39,33 @@ type Subscription struct { | |||
| 41 | FaviconURL sql.NullString | 39 | FaviconURL sql.NullString |
| 42 | } | 40 | } |
| 43 | 41 | ||
| 44 | -func (db *DB) UpsertFeed(ctx context.Context, feed *Feed) error { | 42 | +func (s *ArticleStore) UpsertFeed(ctx context.Context, feed *Feed) error { |
| 45 | - return db.BatchUpsertFeeds(ctx, []*Feed{feed}) | 43 | + return s.BatchUpsertFeeds(ctx, []*Feed{feed}) |
| 46 | } | 44 | } |
| 47 | 45 | ||
| 48 | -func (db *DB) GetFeed(ctx context.Context, feedURL string) (*Feed, error) { | 46 | +func (s *ArticleStore) GetFeed(ctx context.Context, feedURL string) (*Feed, error) { |
| 49 | f := &Feed{} | 47 | f := &Feed{} |
| 50 | - err := db.QueryRowContext(ctx, ` | 48 | + err := s.db.QueryRowContext(ctx, ` |
| 51 | SELECT feed_url, title, site_url, description, feed_type, | 49 | SELECT feed_url, title, site_url, description, feed_type, |
| 52 | last_fetched_at, last_error, subscriber_count, etag, last_modified, | 50 | last_fetched_at, last_error, subscriber_count, etag, last_modified, |
| 53 | - fetch_interval_minutes, next_fetch_at, consecutive_empty_fetches, error_count, favicon_url | 51 | + consecutive_empty_fetches, error_count, favicon_url |
| 54 | - FROM feeds WHERE feed_url = ? | 52 | + FROM articles.feeds WHERE feed_url = ? |
| 55 | `, feedURL).Scan(&f.FeedURL, &f.Title, &f.SiteURL, &f.Description, &f.FeedType, | 53 | `, feedURL).Scan(&f.FeedURL, &f.Title, &f.SiteURL, &f.Description, &f.FeedType, |
| 56 | &f.LastFetchedAt, &f.LastError, &f.SubscriberCount, &f.Etag, &f.LastModified, | 54 | &f.LastFetchedAt, &f.LastError, &f.SubscriberCount, &f.Etag, &f.LastModified, |
| 57 | - &f.FetchIntervalMinutes, &f.NextFetchAt, &f.ConsecutiveEmptyFetches, &f.ErrorCount, &f.FaviconURL) | 55 | + &f.ConsecutiveEmptyFetches, &f.ErrorCount, &f.FaviconURL) |
| 58 | if err != nil { | 56 | if err != nil { |
| 59 | return nil, err | 57 | return nil, err |
| 60 | } | 58 | } |
| 61 | return f, nil | 59 | return f, nil |
| 62 | } | 60 | } |
| 63 | 61 | ||
| 64 | -func (db *DB) GetFeedsToFetch(ctx context.Context, olderThan time.Duration, limit int) ([]*Feed, error) { | 62 | +func (s *ArticleStore) GetFeedsToFetch(ctx context.Context, olderThan time.Duration, limit int) ([]*Feed, error) { |
| 65 | cutoff := time.Now().Add(-olderThan) | 63 | cutoff := time.Now().Add(-olderThan) |
| 66 | - rows, err := db.QueryContext(ctx, ` | 64 | + rows, err := s.db.QueryContext(ctx, ` |
| 67 | SELECT feed_url, title, site_url, description, feed_type, | 65 | SELECT feed_url, title, site_url, description, feed_type, |
| 68 | last_fetched_at, last_error, subscriber_count, etag, last_modified, | 66 | last_fetched_at, last_error, subscriber_count, etag, last_modified, |
| 69 | - fetch_interval_minutes, next_fetch_at, consecutive_empty_fetches, error_count, favicon_url | 67 | + consecutive_empty_fetches, error_count, favicon_url |
| 70 | - FROM feeds | 68 | + FROM articles.feeds |
| 71 | WHERE subscriber_count > 0 AND error_count < 25 AND (last_fetched_at IS NULL OR last_fetched_at <= ?) | 69 | WHERE subscriber_count > 0 AND error_count < 25 AND (last_fetched_at IS NULL OR last_fetched_at <= ?) |
| 72 | ORDER BY last_fetched_at ASC NULLS FIRST | 70 | ORDER BY last_fetched_at ASC NULLS FIRST |
| 73 | LIMIT ? | 71 | LIMIT ? |
| @@ -82,7 +80,7 @@ func (db *DB) GetFeedsToFetch(ctx context.Context, olderThan time.Duration, limi | |||
| 82 | f := &Feed{} | 80 | f := &Feed{} |
| 83 | if err := rows.Scan(&f.FeedURL, &f.Title, &f.SiteURL, &f.Description, &f.FeedType, | 81 | if err := rows.Scan(&f.FeedURL, &f.Title, &f.SiteURL, &f.Description, &f.FeedType, |
| 84 | &f.LastFetchedAt, &f.LastError, &f.SubscriberCount, &f.Etag, &f.LastModified, | 82 | &f.LastFetchedAt, &f.LastError, &f.SubscriberCount, &f.Etag, &f.LastModified, |
| 85 | - &f.FetchIntervalMinutes, &f.NextFetchAt, &f.ConsecutiveEmptyFetches, &f.ErrorCount, &f.FaviconURL); err != nil { | 83 | + &f.ConsecutiveEmptyFetches, &f.ErrorCount, &f.FaviconURL); err != nil { |
| 86 | return nil, err | 84 | return nil, err |
| 87 | } | 85 | } |
| 88 | feeds = append(feeds, f) | 86 | feeds = append(feeds, f) |
| @@ -90,9 +88,9 @@ func (db *DB) GetFeedsToFetch(ctx context.Context, olderThan time.Duration, limi | |||
| 90 | return feeds, rows.Err() | 88 | return feeds, rows.Err() |
| 91 | } | 89 | } |
| 92 | 90 | ||
| 93 | -func (db *DB) MarkFeedFetched(ctx context.Context, feedURL, etag, lastModified string) error { | 91 | +func (s *ArticleStore) MarkFeedFetched(ctx context.Context, feedURL, etag, lastModified string) error { |
| 94 | - _, err := db.ExecContext(ctx, ` | 92 | + _, err := s.db.ExecContext(ctx, ` |
| 95 | - UPDATE feeds SET | 93 | + UPDATE articles.feeds SET |
| 96 | etag = ?, | 94 | etag = ?, |
| 97 | last_modified = ?, | 95 | last_modified = ?, |
| 98 | error_count = 0, | 96 | error_count = 0, |
| @@ -103,9 +101,9 @@ func (db *DB) MarkFeedFetched(ctx context.Context, feedURL, etag, lastModified s | |||
| 103 | return err | 101 | return err |
| 104 | } | 102 | } |
| 105 | 103 | ||
| 106 | -func (db *DB) MarkFeedFetchError(ctx context.Context, feedURL, lastError string) error { | 104 | +func (s *ArticleStore) MarkFeedFetchError(ctx context.Context, feedURL, lastError string) error { |
| 107 | - _, err := db.ExecContext(ctx, ` | 105 | + _, err := s.db.ExecContext(ctx, ` |
| 108 | - UPDATE feeds SET | 106 | + UPDATE articles.feeds SET |
| 109 | error_count = error_count + 1, | 107 | error_count = error_count + 1, |
| 110 | last_error = ?, | 108 | last_error = ?, |
| 111 | last_fetched_at = CURRENT_TIMESTAMP | 109 | last_fetched_at = CURRENT_TIMESTAMP |
| @@ -114,27 +112,27 @@ func (db *DB) MarkFeedFetchError(ctx context.Context, feedURL, lastError string) | |||
| 114 | return err | 112 | return err |
| 115 | } | 113 | } |
| 116 | 114 | ||
| 117 | -func (db *DB) decrementSubscriberCount(ctx context.Context, feedURL string) error { | 115 | +func (s *ArticleStore) decrementSubscriberCount(ctx context.Context, feedURL string) error { |
| 118 | - _, err := db.ExecContext(ctx, ` | 116 | + _, err := s.db.ExecContext(ctx, ` |
| 119 | - UPDATE feeds SET subscriber_count = MAX(subscriber_count - 1, 0) WHERE feed_url = ? | 117 | + UPDATE articles.feeds SET subscriber_count = MAX(subscriber_count - 1, 0) WHERE feed_url = ? |
| 120 | `, feedURL) | 118 | `, feedURL) |
| 121 | return err | 119 | return err |
| 122 | } | 120 | } |
| 123 | 121 | ||
| 124 | -func (db *DB) CreateSubscription(ctx context.Context, userDID, feedURL, title, category, uri, cid string) error { | 122 | +func (s *ArticleStore) CreateSubscription(ctx context.Context, userDID, feedURL, title, category, uri, cid string) error { |
| 125 | - existing, err := db.GetSubscription(ctx, userDID, feedURL) | 123 | + existing, err := s.GetSubscription(ctx, userDID, feedURL) |
| 126 | if err == nil && existing != nil { | 124 | if err == nil && existing != nil { |
| 127 | if !existing.URI.Valid || existing.URI.String == "" { | 125 | if !existing.URI.Valid || existing.URI.String == "" { |
| 128 | - return db.updateSubscriptionURI(ctx, userDID, feedURL, uri, cid) | 126 | + return s.updateSubscriptionURI(ctx, userDID, feedURL, uri, cid) |
| 129 | } | 127 | } |
| 130 | return ErrDuplicateSubscription | 128 | return ErrDuplicateSubscription |
| 131 | } | 129 | } |
| 132 | - return db.BatchReconcileSubscriptions(ctx, userDID, []SubData{{FeedURL: feedURL, Title: title, Category: category, URI: uri, CID: cid}}) | 130 | + return s.BatchReconcileSubscriptions(ctx, userDID, []SubData{{FeedURL: feedURL, Title: title, Category: category, URI: uri, CID: cid}}) |
| 133 | } | 131 | } |
| 134 | 132 | ||
| 135 | -func (db *DB) updateSubscriptionURI(ctx context.Context, userDID, feedURL, uri, cid string) error { | 133 | +func (s *ArticleStore) updateSubscriptionURI(ctx context.Context, userDID, feedURL, uri, cid string) error { |
| 136 | - _, err := db.ExecContext(ctx, ` | 134 | + _, err := s.db.ExecContext(ctx, ` |
| 137 | - UPDATE subscriptions SET uri = ?, cid = ? WHERE user_did = ? AND feed_url = ? | 135 | + UPDATE articles.subscriptions SET uri = ?, cid = ? WHERE user_did = ? AND feed_url = ? |
| 138 | `, uri, cid, userDID, feedURL) | 136 | `, uri, cid, userDID, feedURL) |
| 139 | return err | 137 | return err |
| 140 | } | 138 | } |
| @@ -153,24 +151,24 @@ func nilIfEmpty(v string) any { | |||
| 153 | return v | 151 | return v |
| 154 | } | 152 | } |
| 155 | 153 | ||
| 156 | -func (db *DB) DeleteSubscription(ctx context.Context, userDID, feedURL string) error { | 154 | +func (s *ArticleStore) DeleteSubscription(ctx context.Context, userDID, feedURL string) error { |
| 157 | - _, err := db.ExecContext(ctx, ` | 155 | + _, err := s.db.ExecContext(ctx, ` |
| 158 | - DELETE FROM subscriptions WHERE user_did = ? AND feed_url = ? | 156 | + DELETE FROM articles.subscriptions WHERE user_did = ? AND feed_url = ? |
| 159 | `, userDID, feedURL) | 157 | `, userDID, feedURL) |
| 160 | if err != nil { | 158 | if err != nil { |
| 161 | return err | 159 | return err |
| 162 | } | 160 | } |
| 163 | - return db.decrementSubscriberCount(ctx, feedURL) | 161 | + return s.decrementSubscriberCount(ctx, feedURL) |
| 164 | } | 162 | } |
| 165 | 163 | ||
| 166 | -func (db *DB) DeleteAllSubscriptions(ctx context.Context, userDID string) error { | 164 | +func (s *ArticleStore) DeleteAllSubscriptions(ctx context.Context, userDID string) error { |
| 167 | - tx, err := db.BeginTx(ctx, nil) | 165 | + tx, err := s.db.BeginTx(ctx, nil) |
| 168 | if err != nil { | 166 | if err != nil { |
| 169 | return err | 167 | return err |
| 170 | } | 168 | } |
| 171 | defer tx.Rollback() | 169 | defer tx.Rollback() |
| 172 | 170 | ||
| 173 | - rows, err := tx.QueryContext(ctx, `SELECT feed_url FROM subscriptions WHERE user_did = ?`, userDID) | 171 | + rows, err := tx.QueryContext(ctx, `SELECT feed_url FROM articles.subscriptions WHERE user_did = ?`, userDID) |
| 174 | if err != nil { | 172 | if err != nil { |
| 175 | return err | 173 | return err |
| 176 | } | 174 | } |
| @@ -185,7 +183,7 @@ func (db *DB) DeleteAllSubscriptions(ctx context.Context, userDID string) error | |||
| 185 | } | 183 | } |
| 186 | rows.Close() | 184 | rows.Close() |
| 187 | 185 | ||
| 188 | - _, err = tx.ExecContext(ctx, `DELETE FROM subscriptions WHERE user_did = ?`, userDID) | 186 | + _, err = tx.ExecContext(ctx, `DELETE FROM articles.subscriptions WHERE user_did = ?`, userDID) |
| 189 | if err != nil { | 187 | if err != nil { |
| 190 | return err | 188 | return err |
| 191 | } | 189 | } |
| @@ -198,7 +196,7 @@ func (db *DB) DeleteAllSubscriptions(ctx context.Context, userDID string) error | |||
| 198 | args[i] = u | 196 | args[i] = u |
| 199 | } | 197 | } |
| 200 | _, err = tx.ExecContext(ctx, ` | 198 | _, err = tx.ExecContext(ctx, ` |
| 201 | - UPDATE feeds SET subscriber_count = MAX(subscriber_count - 1, 0) | 199 | + UPDATE articles.feeds SET subscriber_count = MAX(subscriber_count - 1, 0) |
| 202 | WHERE feed_url IN (`+strings.Join(ph, ",")+`) | 200 | WHERE feed_url IN (`+strings.Join(ph, ",")+`) |
| 203 | `, args...) | 201 | `, args...) |
| 204 | if err != nil { | 202 | if err != nil { |
| @@ -209,41 +207,41 @@ func (db *DB) DeleteAllSubscriptions(ctx context.Context, userDID string) error | |||
| 209 | return tx.Commit() | 207 | return tx.Commit() |
| 210 | } | 208 | } |
| 211 | 209 | ||
| 212 | -func (db *DB) GetSubscriptionByURI(ctx context.Context, userDID, uri string) (*Subscription, error) { | 210 | +func (s *ArticleStore) GetSubscriptionByURI(ctx context.Context, userDID, uri string) (*Subscription, error) { |
| 213 | - s := &Subscription{} | 211 | + sub := &Subscription{} |
| 214 | - err := db.QueryRowContext(ctx, ` | 212 | + err := s.db.QueryRowContext(ctx, ` |
| 215 | SELECT s.id, s.user_did, s.feed_url, COALESCE(s.title, f.title, ''), s.category, s.added_at, | 213 | SELECT s.id, s.user_did, s.feed_url, COALESCE(s.title, f.title, ''), s.category, s.added_at, |
| 216 | s.uri, s.cid | 214 | s.uri, s.cid |
| 217 | - FROM subscriptions s | 215 | + FROM articles.subscriptions s |
| 218 | - LEFT JOIN feeds f ON s.feed_url = f.feed_url | 216 | + LEFT JOIN articles.feeds f ON s.feed_url = f.feed_url |
| 219 | WHERE s.user_did = ? AND s.uri = ? | 217 | WHERE s.user_did = ? AND s.uri = ? |
| 220 | - `, userDID, uri).Scan(&s.ID, &s.UserDID, &s.FeedURL, &s.FeedTitle, &s.Category, &s.AddedAt, &s.URI, &s.CID) | 218 | + `, userDID, uri).Scan(&sub.ID, &sub.UserDID, &sub.FeedURL, &sub.FeedTitle, &sub.Category, &sub.AddedAt, &sub.URI, &sub.CID) |
| 221 | if err != nil { | 219 | if err != nil { |
| 222 | return nil, err | 220 | return nil, err |
| 223 | } | 221 | } |
| 224 | - return s, nil | 222 | + return sub, nil |
| 225 | } | 223 | } |
| 226 | 224 | ||
| 227 | -func (db *DB) GetSubscription(ctx context.Context, userDID, feedURL string) (*Subscription, error) { | 225 | +func (s *ArticleStore) GetSubscription(ctx context.Context, userDID, feedURL string) (*Subscription, error) { |
| 228 | - s := &Subscription{} | 226 | + sub := &Subscription{} |
| 229 | - err := db.QueryRowContext(ctx, ` | 227 | + err := s.db.QueryRowContext(ctx, ` |
| 230 | SELECT s.id, s.user_did, s.feed_url, COALESCE(s.title, f.title, ''), s.category, s.added_at, | 228 | SELECT s.id, s.user_did, s.feed_url, COALESCE(s.title, f.title, ''), s.category, s.added_at, |
| 231 | s.uri, s.cid | 229 | s.uri, s.cid |
| 232 | - FROM subscriptions s | 230 | + FROM articles.subscriptions s |
| 233 | - LEFT JOIN feeds f ON s.feed_url = f.feed_url | 231 | + LEFT JOIN articles.feeds f ON s.feed_url = f.feed_url |
| 234 | WHERE s.user_did = ? AND s.feed_url = ? | 232 | WHERE s.user_did = ? AND s.feed_url = ? |
| 235 | - `, userDID, feedURL).Scan(&s.ID, &s.UserDID, &s.FeedURL, &s.FeedTitle, &s.Category, &s.AddedAt, &s.URI, &s.CID) | 233 | + `, userDID, feedURL).Scan(&sub.ID, &sub.UserDID, &sub.FeedURL, &sub.FeedTitle, &sub.Category, &sub.AddedAt, &sub.URI, &sub.CID) |
| 236 | if err != nil { | 234 | if err != nil { |
| 237 | return nil, err | 235 | return nil, err |
| 238 | } | 236 | } |
| 239 | - return s, nil | 237 | + return sub, nil |
| 240 | } | 238 | } |
| 241 | 239 | ||
| 242 | -func (db *DB) ListSubscriptions(ctx context.Context, userDID, category string, limit, offset int) ([]*Subscription, error) { | 240 | +func (s *ArticleStore) ListSubscriptions(ctx context.Context, userDID, category string, limit, offset int) ([]*Subscription, error) { |
| 243 | query := `SELECT s.id, s.user_did, s.feed_url, COALESCE(s.title, f.title, ''), s.category, s.added_at, | 241 | query := `SELECT s.id, s.user_did, s.feed_url, COALESCE(s.title, f.title, ''), s.category, s.added_at, |
| 244 | s.uri, s.cid, f.favicon_url | 242 | s.uri, s.cid, f.favicon_url |
| 245 | - FROM subscriptions s | 243 | + FROM articles.subscriptions s |
| 246 | - LEFT JOIN feeds f ON s.feed_url = f.feed_url | 244 | + LEFT JOIN articles.feeds f ON s.feed_url = f.feed_url |
| 247 | WHERE s.user_did = ?` | 245 | WHERE s.user_did = ?` |
| 248 | args := []any{userDID} | 246 | args := []any{userDID} |
| 249 | 247 | ||
| @@ -255,7 +253,7 @@ func (db *DB) ListSubscriptions(ctx context.Context, userDID, category string, l | |||
| 255 | query += ` ORDER BY s.added_at DESC LIMIT ? OFFSET ?` | 253 | query += ` ORDER BY s.added_at DESC LIMIT ? OFFSET ?` |
| 256 | args = append(args, limit, offset) | 254 | args = append(args, limit, offset) |
| 257 | 255 | ||
| 258 | - rows, err := db.QueryContext(ctx, query, args...) | 256 | + rows, err := s.db.QueryContext(ctx, query, args...) |
| 259 | if err != nil { | 257 | if err != nil { |
| 260 | return nil, err | 258 | return nil, err |
| 261 | } | 259 | } |
| @@ -272,17 +270,17 @@ func (db *DB) ListSubscriptions(ctx context.Context, userDID, category string, l | |||
| 272 | return subs, rows.Err() | 270 | return subs, rows.Err() |
| 273 | } | 271 | } |
| 274 | 272 | ||
| 275 | -func (db *DB) GetSubscriptionCount(ctx context.Context, userDID string) (int, error) { | 273 | +func (s *ArticleStore) GetSubscriptionCount(ctx context.Context, userDID string) (int, error) { |
| 276 | var count int | 274 | var count int |
| 277 | - err := db.QueryRowContext(ctx, ` | 275 | + err := s.db.QueryRowContext(ctx, ` |
| 278 | - SELECT COUNT(*) FROM subscriptions WHERE user_did = ? | 276 | + SELECT COUNT(*) FROM articles.subscriptions WHERE user_did = ? |
| 279 | `, userDID).Scan(&count) | 277 | `, userDID).Scan(&count) |
| 280 | return count, err | 278 | return count, err |
| 281 | } | 279 | } |
| 282 | 280 | ||
| 283 | -func (db *DB) GetCategories(ctx context.Context, userDID string) ([]string, error) { | 281 | +func (s *ArticleStore) GetCategories(ctx context.Context, userDID string) ([]string, error) { |
| 284 | - rows, err := db.QueryContext(ctx, ` | 282 | + rows, err := s.db.QueryContext(ctx, ` |
| 285 | - SELECT DISTINCT category FROM subscriptions | 283 | + SELECT DISTINCT category FROM articles.subscriptions |
| 286 | WHERE user_did = ? AND category IS NOT NULL AND category != '' | 284 | WHERE user_did = ? AND category IS NOT NULL AND category != '' |
| 287 | ORDER BY category | 285 | ORDER BY category |
| 288 | `, userDID) | 286 | `, userDID) |
| @@ -302,18 +300,18 @@ func (db *DB) GetCategories(ctx context.Context, userDID string) ([]string, erro | |||
| 302 | return categories, rows.Err() | 300 | return categories, rows.Err() |
| 303 | } | 301 | } |
| 304 | 302 | ||
| 305 | -func (db *DB) UpdateFeedFavicon(ctx context.Context, feedURL, faviconURL string) error { | 303 | +func (s *ArticleStore) UpdateFeedFavicon(ctx context.Context, feedURL, faviconURL string) error { |
| 306 | - _, err := db.ExecContext(ctx, `UPDATE feeds SET favicon_url = ? WHERE feed_url = ?`, faviconURL, feedURL) | 304 | + _, err := s.db.ExecContext(ctx, `UPDATE articles.feeds SET favicon_url = ? WHERE feed_url = ?`, faviconURL, feedURL) |
| 307 | return err | 305 | return err |
| 308 | } | 306 | } |
| 309 | 307 | ||
| 310 | -func (db *DB) ListDeadFeeds(ctx context.Context, userDID string, threshold int) ([]*Feed, error) { | 308 | +func (s *ArticleStore) ListDeadFeeds(ctx context.Context, userDID string, threshold int) ([]*Feed, error) { |
| 311 | - rows, err := db.QueryContext(ctx, ` | 309 | + rows, err := s.db.QueryContext(ctx, ` |
| 312 | SELECT f.feed_url, f.title, f.site_url, f.description, f.feed_type, | 310 | SELECT f.feed_url, f.title, f.site_url, f.description, f.feed_type, |
| 313 | f.last_fetched_at, f.last_error, f.subscriber_count, f.etag, f.last_modified, | 311 | f.last_fetched_at, f.last_error, f.subscriber_count, f.etag, f.last_modified, |
| 314 | - f.fetch_interval_minutes, f.next_fetch_at, f.consecutive_empty_fetches, f.error_count, f.favicon_url | 312 | + f.consecutive_empty_fetches, f.error_count, f.favicon_url |
| 315 | - FROM feeds f | 313 | + FROM articles.feeds f |
| 316 | - JOIN subscriptions s ON s.feed_url = f.feed_url AND s.user_did = ? | 314 | + JOIN articles.subscriptions s ON s.feed_url = f.feed_url AND s.user_did = ? |
| 317 | WHERE f.error_count >= ? | 315 | WHERE f.error_count >= ? |
| 318 | ORDER BY f.error_count DESC | 316 | ORDER BY f.error_count DESC |
| 319 | `, userDID, threshold) | 317 | `, userDID, threshold) |
| @@ -327,7 +325,7 @@ func (db *DB) ListDeadFeeds(ctx context.Context, userDID string, threshold int) | |||
| 327 | f := &Feed{} | 325 | f := &Feed{} |
| 328 | if err := rows.Scan(&f.FeedURL, &f.Title, &f.SiteURL, &f.Description, &f.FeedType, | 326 | if err := rows.Scan(&f.FeedURL, &f.Title, &f.SiteURL, &f.Description, &f.FeedType, |
| 329 | &f.LastFetchedAt, &f.LastError, &f.SubscriberCount, &f.Etag, &f.LastModified, | 327 | &f.LastFetchedAt, &f.LastError, &f.SubscriberCount, &f.Etag, &f.LastModified, |
| 330 | - &f.FetchIntervalMinutes, &f.NextFetchAt, &f.ConsecutiveEmptyFetches, &f.ErrorCount, &f.FaviconURL); err != nil { | 328 | + &f.ConsecutiveEmptyFetches, &f.ErrorCount, &f.FaviconURL); err != nil { |
| 331 | return nil, err | 329 | return nil, err |
| 332 | } | 330 | } |
| 333 | feeds = append(feeds, f) | 331 | feeds = append(feeds, f) |
| @@ -335,12 +333,12 @@ func (db *DB) ListDeadFeeds(ctx context.Context, userDID string, threshold int) | |||
| 335 | return feeds, rows.Err() | 333 | return feeds, rows.Err() |
| 336 | } | 334 | } |
| 337 | 335 | ||
| 338 | -func (db *DB) ListAllFeeds(ctx context.Context, limit, offset int) ([]*Feed, error) { | 336 | +func (s *ArticleStore) ListAllFeeds(ctx context.Context, limit, offset int) ([]*Feed, error) { |
| 339 | - rows, err := db.QueryContext(ctx, ` | 337 | + rows, err := s.db.QueryContext(ctx, ` |
| 340 | SELECT feed_url, title, site_url, description, feed_type, | 338 | SELECT feed_url, title, site_url, description, feed_type, |
| 341 | last_fetched_at, last_error, subscriber_count, etag, last_modified, | 339 | last_fetched_at, last_error, subscriber_count, etag, last_modified, |
| 342 | - fetch_interval_minutes, next_fetch_at, consecutive_empty_fetches, error_count, favicon_url | 340 | + consecutive_empty_fetches, error_count, favicon_url |
| 343 | - FROM feeds | 341 | + FROM articles.feeds |
| 344 | ORDER BY subscriber_count DESC | 342 | ORDER BY subscriber_count DESC |
| 345 | LIMIT ? OFFSET ? | 343 | LIMIT ? OFFSET ? |
| 346 | `, limit, offset) | 344 | `, limit, offset) |
| @@ -354,7 +352,7 @@ func (db *DB) ListAllFeeds(ctx context.Context, limit, offset int) ([]*Feed, err | |||
| 354 | f := &Feed{} | 352 | f := &Feed{} |
| 355 | if err := rows.Scan(&f.FeedURL, &f.Title, &f.SiteURL, &f.Description, &f.FeedType, | 353 | if err := rows.Scan(&f.FeedURL, &f.Title, &f.SiteURL, &f.Description, &f.FeedType, |
| 356 | &f.LastFetchedAt, &f.LastError, &f.SubscriberCount, &f.Etag, &f.LastModified, | 354 | &f.LastFetchedAt, &f.LastError, &f.SubscriberCount, &f.Etag, &f.LastModified, |
| 357 | - &f.FetchIntervalMinutes, &f.NextFetchAt, &f.ConsecutiveEmptyFetches, &f.ErrorCount, &f.FaviconURL); err != nil { | 355 | + &f.ConsecutiveEmptyFetches, &f.ErrorCount, &f.FaviconURL); err != nil { |
| 358 | return nil, err | 356 | return nil, err |
| 359 | } | 357 | } |
| 360 | feeds = append(feeds, f) | 358 | feeds = append(feeds, f) |
| @@ -370,18 +368,18 @@ type SubData struct { | |||
| 370 | CID string | 368 | CID string |
| 371 | } | 369 | } |
| 372 | 370 | ||
| 373 | -func (db *DB) BatchUpsertFeeds(ctx context.Context, feeds []*Feed) error { | 371 | +func (s *ArticleStore) BatchUpsertFeeds(ctx context.Context, feeds []*Feed) error { |
| 374 | if len(feeds) == 0 { | 372 | if len(feeds) == 0 { |
| 375 | return nil | 373 | return nil |
| 376 | } | 374 | } |
| 377 | - tx, err := db.BeginTx(ctx, nil) | 375 | + tx, err := s.db.BeginTx(ctx, nil) |
| 378 | if err != nil { | 376 | if err != nil { |
| 379 | return err | 377 | return err |
| 380 | } | 378 | } |
| 381 | defer tx.Rollback() | 379 | defer tx.Rollback() |
| 382 | 380 | ||
| 383 | stmt, err := tx.PrepareContext(ctx, ` | 381 | stmt, err := tx.PrepareContext(ctx, ` |
| 384 | - INSERT INTO feeds (feed_url, title, site_url, description, feed_type) | 382 | + INSERT INTO articles.feeds (feed_url, title, site_url, description, feed_type) |
| 385 | VALUES (?, ?, ?, ?, ?) | 383 | VALUES (?, ?, ?, ?, ?) |
| 386 | ON CONFLICT(feed_url) DO UPDATE SET | 384 | ON CONFLICT(feed_url) DO UPDATE SET |
| 387 | title = excluded.title, | 385 | title = excluded.title, |
| @@ -402,17 +400,17 @@ func (db *DB) BatchUpsertFeeds(ctx context.Context, feeds []*Feed) error { | |||
| 402 | return tx.Commit() | 400 | return tx.Commit() |
| 403 | } | 401 | } |
| 404 | 402 | ||
| 405 | -func (db *DB) BatchReconcileSubscriptions(ctx context.Context, userDID string, subs []SubData) error { | 403 | +func (s *ArticleStore) BatchReconcileSubscriptions(ctx context.Context, userDID string, subs []SubData) error { |
| 406 | if len(subs) == 0 { | 404 | if len(subs) == 0 { |
| 407 | return nil | 405 | return nil |
| 408 | } | 406 | } |
| 409 | - tx, err := db.BeginTx(ctx, nil) | 407 | + tx, err := s.db.BeginTx(ctx, nil) |
| 410 | if err != nil { | 408 | if err != nil { |
| 411 | return err | 409 | return err |
| 412 | } | 410 | } |
| 413 | defer tx.Rollback() | 411 | defer tx.Rollback() |
| 414 | 412 | ||
| 415 | - rows, err := tx.QueryContext(ctx, `SELECT feed_url, COALESCE(uri, '') FROM subscriptions WHERE user_did = ?`, userDID) | 413 | + rows, err := tx.QueryContext(ctx, `SELECT feed_url, COALESCE(uri, '') FROM articles.subscriptions WHERE user_did = ?`, userDID) |
| 416 | if err != nil { | 414 | if err != nil { |
| 417 | return err | 415 | return err |
| 418 | } | 416 | } |
| @@ -428,7 +426,7 @@ func (db *DB) BatchReconcileSubscriptions(ctx context.Context, userDID string, s | |||
| 428 | rows.Close() | 426 | rows.Close() |
| 429 | 427 | ||
| 430 | insertStmt, err := tx.PrepareContext(ctx, ` | 428 | insertStmt, err := tx.PrepareContext(ctx, ` |
| 431 | - INSERT OR IGNORE INTO subscriptions (user_did, feed_url, title, category, uri, cid) | 429 | + INSERT OR IGNORE INTO articles.subscriptions (user_did, feed_url, title, category, uri, cid) |
| 432 | VALUES (?, ?, ?, ?, ?, ?) | 430 | VALUES (?, ?, ?, ?, ?, ?) |
| 433 | `) | 431 | `) |
| 434 | if err != nil { | 432 | if err != nil { |
| @@ -437,14 +435,14 @@ func (db *DB) BatchReconcileSubscriptions(ctx context.Context, userDID string, s | |||
| 437 | defer insertStmt.Close() | 435 | defer insertStmt.Close() |
| 438 | 436 | ||
| 439 | updateStmt, err := tx.PrepareContext(ctx, ` | 437 | updateStmt, err := tx.PrepareContext(ctx, ` |
| 440 | - UPDATE subscriptions SET uri = ?, cid = ? WHERE user_did = ? AND feed_url = ? | 438 | + UPDATE articles.subscriptions SET uri = ?, cid = ? WHERE user_did = ? AND feed_url = ? |
| 441 | `) | 439 | `) |
| 442 | if err != nil { | 440 | if err != nil { |
| 443 | return err | 441 | return err |
| 444 | } | 442 | } |
| 445 | defer updateStmt.Close() | 443 | defer updateStmt.Close() |
| 446 | 444 | ||
| 447 | - incrStmt, err := tx.PrepareContext(ctx, `UPDATE feeds SET subscriber_count = subscriber_count + 1 WHERE feed_url = ?`) | 445 | + incrStmt, err := tx.PrepareContext(ctx, `UPDATE articles.feeds SET subscriber_count = subscriber_count + 1 WHERE feed_url = ?`) |
| 448 | if err != nil { | 446 | if err != nil { |
| 449 | return err | 447 | return err |
| 450 | } | 448 | } |
| @@ -473,13 +471,13 @@ func (db *DB) BatchReconcileSubscriptions(ctx context.Context, userDID string, s | |||
| 473 | return tx.Commit() | 471 | return tx.Commit() |
| 474 | } | 472 | } |
| 475 | 473 | ||
| 476 | -func (db *DB) ListUnsubscribedFeeds(ctx context.Context, userDID string, limit, offset int) ([]*Feed, error) { | 474 | +func (s *ArticleStore) ListUnsubscribedFeeds(ctx context.Context, userDID string, limit, offset int) ([]*Feed, error) { |
| 477 | - rows, err := db.QueryContext(ctx, ` | 475 | + rows, err := s.db.QueryContext(ctx, ` |
| 478 | SELECT feed_url, title, site_url, description, feed_type, | 476 | SELECT feed_url, title, site_url, description, feed_type, |
| 479 | last_fetched_at, last_error, subscriber_count, etag, last_modified, | 477 | last_fetched_at, last_error, subscriber_count, etag, last_modified, |
| 480 | - fetch_interval_minutes, next_fetch_at, consecutive_empty_fetches, error_count, favicon_url | 478 | + consecutive_empty_fetches, error_count, favicon_url |
| 481 | - FROM feeds | 479 | + FROM articles.feeds |
| 482 | - WHERE feed_url NOT IN (SELECT feed_url FROM subscriptions WHERE user_did = ?) | 480 | + WHERE feed_url NOT IN (SELECT feed_url FROM articles.subscriptions WHERE user_did = ?) |
| 483 | ORDER BY subscriber_count DESC | 481 | ORDER BY subscriber_count DESC |
| 484 | LIMIT ? OFFSET ? | 482 | LIMIT ? OFFSET ? |
| 485 | `, userDID, limit, offset) | 483 | `, userDID, limit, offset) |
| @@ -493,7 +491,7 @@ func (db *DB) ListUnsubscribedFeeds(ctx context.Context, userDID string, limit, | |||
| 493 | f := &Feed{} | 491 | f := &Feed{} |
| 494 | if err := rows.Scan(&f.FeedURL, &f.Title, &f.SiteURL, &f.Description, &f.FeedType, | 492 | if err := rows.Scan(&f.FeedURL, &f.Title, &f.SiteURL, &f.Description, &f.FeedType, |
| 495 | &f.LastFetchedAt, &f.LastError, &f.SubscriberCount, &f.Etag, &f.LastModified, | 493 | &f.LastFetchedAt, &f.LastError, &f.SubscriberCount, &f.Etag, &f.LastModified, |
| 496 | - &f.FetchIntervalMinutes, &f.NextFetchAt, &f.ConsecutiveEmptyFetches, &f.ErrorCount, &f.FaviconURL); err != nil { | 494 | + &f.ConsecutiveEmptyFetches, &f.ErrorCount, &f.FaviconURL); err != nil { |
| 497 | return nil, err | 495 | return nil, err |
| 498 | } | 496 | } |
| 499 | feeds = append(feeds, f) | 497 | feeds = append(feeds, f) |
modified
internal/db/follow.go +16 -16 | @@ -14,8 +14,8 @@ type Follow struct { | ||
| 14 | 14 | FollowedAt sql.NullTime |
| 15 | 15 | } |
| 16 | 16 | |
| 17 | -func (db *DB) UpsertFollow(ctx context.Context, userDID, targetDID, uri, cid string) error { | |
| 18 | - _, err := db.ExecContext(ctx, ` | |
| 17 | +func (s *UserStore) UpsertFollow(ctx context.Context, userDID, targetDID, uri, cid string) error { | |
| 18 | + _, err := s.db.ExecContext(ctx, ` | |
| 19 | 19 | INSERT INTO follows (user_did, target_did, uri, cid, followed_at) |
| 20 | 20 | VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) |
| 21 | 21 | ON CONFLICT(user_did, target_did) DO UPDATE SET |
| @@ -25,18 +25,18 @@ func (db *DB) UpsertFollow(ctx context.Context, userDID, targetDID, uri, cid str | ||
| 25 | 25 | return err |
| 26 | 26 | } |
| 27 | 27 | |
| 28 | -func (db *DB) DeleteFollow(ctx context.Context, userDID, targetDID string) error { | |
| 29 | - _, err := db.ExecContext(ctx, `DELETE FROM follows WHERE user_did = ? AND target_did = ?`, userDID, targetDID) | |
| 28 | +func (s *UserStore) DeleteFollow(ctx context.Context, userDID, targetDID string) error { | |
| 29 | + _, err := s.db.ExecContext(ctx, `DELETE FROM follows WHERE user_did = ? AND target_did = ?`, userDID, targetDID) | |
| 30 | 30 | return err |
| 31 | 31 | } |
| 32 | 32 | |
| 33 | -func (db *DB) DeleteFollowByURI(ctx context.Context, uri string) error { | |
| 34 | - _, err := db.ExecContext(ctx, `DELETE FROM follows WHERE uri = ?`, uri) | |
| 33 | +func (s *UserStore) DeleteFollowByURI(ctx context.Context, uri string) error { | |
| 34 | + _, err := s.db.ExecContext(ctx, `DELETE FROM follows WHERE uri = ?`, uri) | |
| 35 | 35 | return err |
| 36 | 36 | } |
| 37 | 37 | |
| 38 | -func (db *DB) ListFollows(ctx context.Context, userDID string, limit, offset int) ([]*Follow, error) { | |
| 39 | - rows, err := db.QueryContext(ctx, ` | |
| 38 | +func (s *UserStore) ListFollows(ctx context.Context, userDID string, limit, offset int) ([]*Follow, error) { | |
| 39 | + rows, err := s.db.QueryContext(ctx, ` | |
| 40 | 40 | SELECT user_did, target_did, uri, cid, followed_at |
| 41 | 41 | FROM follows WHERE user_did = ? |
| 42 | 42 | ORDER BY followed_at DESC |
| @@ -58,8 +58,8 @@ func (db *DB) ListFollows(ctx context.Context, userDID string, limit, offset int | ||
| 58 | 58 | return follows, rows.Err() |
| 59 | 59 | } |
| 60 | 60 | |
| 61 | -func (db *DB) ListFollowers(ctx context.Context, targetDID string, limit, offset int) ([]*Follow, error) { | |
| 62 | - rows, err := db.QueryContext(ctx, ` | |
| 61 | +func (s *UserStore) ListFollowers(ctx context.Context, targetDID string, limit, offset int) ([]*Follow, error) { | |
| 62 | + rows, err := s.db.QueryContext(ctx, ` | |
| 63 | 63 | SELECT user_did, target_did, uri, cid, followed_at |
| 64 | 64 | FROM follows WHERE target_did = ? |
| 65 | 65 | ORDER BY followed_at DESC |
| @@ -81,9 +81,9 @@ func (db *DB) ListFollowers(ctx context.Context, targetDID string, limit, offset | ||
| 81 | 81 | return follows, rows.Err() |
| 82 | 82 | } |
| 83 | 83 | |
| 84 | -func (db *DB) IsFollowing(ctx context.Context, userDID, targetDID string) (bool, error) { | |
| 84 | +func (s *UserStore) IsFollowing(ctx context.Context, userDID, targetDID string) (bool, error) { | |
| 85 | 85 | var exists int |
| 86 | - err := db.QueryRowContext(ctx, ` | |
| 86 | + err := s.db.QueryRowContext(ctx, ` | |
| 87 | 87 | SELECT 1 FROM follows WHERE user_did = ? AND target_did = ? |
| 88 | 88 | `, userDID, targetDID).Scan(&exists) |
| 89 | 89 | if err == sql.ErrNoRows { |
| @@ -95,8 +95,8 @@ func (db *DB) IsFollowing(ctx context.Context, userDID, targetDID string) (bool, | ||
| 95 | 95 | return true, nil |
| 96 | 96 | } |
| 97 | 97 | |
| 98 | -func (db *DB) GetFollowDIDs(ctx context.Context, userDID string) ([]string, error) { | |
| 99 | - rows, err := db.QueryContext(ctx, ` | |
| 98 | +func (s *UserStore) GetFollowDIDs(ctx context.Context, userDID string) ([]string, error) { | |
| 99 | + rows, err := s.db.QueryContext(ctx, ` | |
| 100 | 100 | SELECT target_did FROM follows WHERE user_did = ? |
| 101 | 101 | `, userDID) |
| 102 | 102 | if err != nil { |
| @@ -115,8 +115,8 @@ func (db *DB) GetFollowDIDs(ctx context.Context, userDID string) ([]string, erro | ||
| 115 | 115 | return dids, rows.Err() |
| 116 | 116 | } |
| 117 | 117 | |
| 118 | -func (db *DB) SyncFollows(ctx context.Context, userDID string, activeFollows map[string]Follow) error { | |
| 119 | - tx, err := db.BeginTx(ctx, nil) | |
| 118 | +func (s *UserStore) SyncFollows(ctx context.Context, userDID string, activeFollows map[string]Follow) error { | |
| 119 | + tx, err := s.db.BeginTx(ctx, nil) | |
| 120 | 120 | if err != nil { |
| 121 | 121 | return err |
| 122 | 122 | } |
| @@ -14,8 +14,8 @@ type Follow struct { | |||
| 14 | FollowedAt sql.NullTime | 14 | FollowedAt sql.NullTime |
| 15 | } | 15 | } |
| 16 | 16 | ||
| 17 | -func (db *DB) UpsertFollow(ctx context.Context, userDID, targetDID, uri, cid string) error { | 17 | +func (s *UserStore) UpsertFollow(ctx context.Context, userDID, targetDID, uri, cid string) error { |
| 18 | - _, err := db.ExecContext(ctx, ` | 18 | + _, err := s.db.ExecContext(ctx, ` |
| 19 | INSERT INTO follows (user_did, target_did, uri, cid, followed_at) | 19 | INSERT INTO follows (user_did, target_did, uri, cid, followed_at) |
| 20 | VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) | 20 | VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) |
| 21 | ON CONFLICT(user_did, target_did) DO UPDATE SET | 21 | ON CONFLICT(user_did, target_did) DO UPDATE SET |
| @@ -25,18 +25,18 @@ func (db *DB) UpsertFollow(ctx context.Context, userDID, targetDID, uri, cid str | |||
| 25 | return err | 25 | return err |
| 26 | } | 26 | } |
| 27 | 27 | ||
| 28 | -func (db *DB) DeleteFollow(ctx context.Context, userDID, targetDID string) error { | 28 | +func (s *UserStore) DeleteFollow(ctx context.Context, userDID, targetDID string) error { |
| 29 | - _, err := db.ExecContext(ctx, `DELETE FROM follows WHERE user_did = ? AND target_did = ?`, userDID, targetDID) | 29 | + _, err := s.db.ExecContext(ctx, `DELETE FROM follows WHERE user_did = ? AND target_did = ?`, userDID, targetDID) |
| 30 | return err | 30 | return err |
| 31 | } | 31 | } |
| 32 | 32 | ||
| 33 | -func (db *DB) DeleteFollowByURI(ctx context.Context, uri string) error { | 33 | +func (s *UserStore) DeleteFollowByURI(ctx context.Context, uri string) error { |
| 34 | - _, err := db.ExecContext(ctx, `DELETE FROM follows WHERE uri = ?`, uri) | 34 | + _, err := s.db.ExecContext(ctx, `DELETE FROM follows WHERE uri = ?`, uri) |
| 35 | return err | 35 | return err |
| 36 | } | 36 | } |
| 37 | 37 | ||
| 38 | -func (db *DB) ListFollows(ctx context.Context, userDID string, limit, offset int) ([]*Follow, error) { | 38 | +func (s *UserStore) ListFollows(ctx context.Context, userDID string, limit, offset int) ([]*Follow, error) { |
| 39 | - rows, err := db.QueryContext(ctx, ` | 39 | + rows, err := s.db.QueryContext(ctx, ` |
| 40 | SELECT user_did, target_did, uri, cid, followed_at | 40 | SELECT user_did, target_did, uri, cid, followed_at |
| 41 | FROM follows WHERE user_did = ? | 41 | FROM follows WHERE user_did = ? |
| 42 | ORDER BY followed_at DESC | 42 | ORDER BY followed_at DESC |
| @@ -58,8 +58,8 @@ func (db *DB) ListFollows(ctx context.Context, userDID string, limit, offset int | |||
| 58 | return follows, rows.Err() | 58 | return follows, rows.Err() |
| 59 | } | 59 | } |
| 60 | 60 | ||
| 61 | -func (db *DB) ListFollowers(ctx context.Context, targetDID string, limit, offset int) ([]*Follow, error) { | 61 | +func (s *UserStore) ListFollowers(ctx context.Context, targetDID string, limit, offset int) ([]*Follow, error) { |
| 62 | - rows, err := db.QueryContext(ctx, ` | 62 | + rows, err := s.db.QueryContext(ctx, ` |
| 63 | SELECT user_did, target_did, uri, cid, followed_at | 63 | SELECT user_did, target_did, uri, cid, followed_at |
| 64 | FROM follows WHERE target_did = ? | 64 | FROM follows WHERE target_did = ? |
| 65 | ORDER BY followed_at DESC | 65 | ORDER BY followed_at DESC |
| @@ -81,9 +81,9 @@ func (db *DB) ListFollowers(ctx context.Context, targetDID string, limit, offset | |||
| 81 | return follows, rows.Err() | 81 | return follows, rows.Err() |
| 82 | } | 82 | } |
| 83 | 83 | ||
| 84 | -func (db *DB) IsFollowing(ctx context.Context, userDID, targetDID string) (bool, error) { | 84 | +func (s *UserStore) IsFollowing(ctx context.Context, userDID, targetDID string) (bool, error) { |
| 85 | var exists int | 85 | var exists int |
| 86 | - err := db.QueryRowContext(ctx, ` | 86 | + err := s.db.QueryRowContext(ctx, ` |
| 87 | SELECT 1 FROM follows WHERE user_did = ? AND target_did = ? | 87 | SELECT 1 FROM follows WHERE user_did = ? AND target_did = ? |
| 88 | `, userDID, targetDID).Scan(&exists) | 88 | `, userDID, targetDID).Scan(&exists) |
| 89 | if err == sql.ErrNoRows { | 89 | if err == sql.ErrNoRows { |
| @@ -95,8 +95,8 @@ func (db *DB) IsFollowing(ctx context.Context, userDID, targetDID string) (bool, | |||
| 95 | return true, nil | 95 | return true, nil |
| 96 | } | 96 | } |
| 97 | 97 | ||
| 98 | -func (db *DB) GetFollowDIDs(ctx context.Context, userDID string) ([]string, error) { | 98 | +func (s *UserStore) GetFollowDIDs(ctx context.Context, userDID string) ([]string, error) { |
| 99 | - rows, err := db.QueryContext(ctx, ` | 99 | + rows, err := s.db.QueryContext(ctx, ` |
| 100 | SELECT target_did FROM follows WHERE user_did = ? | 100 | SELECT target_did FROM follows WHERE user_did = ? |
| 101 | `, userDID) | 101 | `, userDID) |
| 102 | if err != nil { | 102 | if err != nil { |
| @@ -115,8 +115,8 @@ func (db *DB) GetFollowDIDs(ctx context.Context, userDID string) ([]string, erro | |||
| 115 | return dids, rows.Err() | 115 | return dids, rows.Err() |
| 116 | } | 116 | } |
| 117 | 117 | ||
| 118 | -func (db *DB) SyncFollows(ctx context.Context, userDID string, activeFollows map[string]Follow) error { | 118 | +func (s *UserStore) SyncFollows(ctx context.Context, userDID string, activeFollows map[string]Follow) error { |
| 119 | - tx, err := db.BeginTx(ctx, nil) | 119 | + tx, err := s.db.BeginTx(ctx, nil) |
| 120 | if err != nil { | 120 | if err != nil { |
| 121 | return err | 121 | return err |
| 122 | } | 122 | } |
modified
internal/db/follow_test.go +51 -51 | @@ -7,15 +7,15 @@ import ( | ||
| 7 | 7 | "gotest.tools/v3/assert" |
| 8 | 8 | ) |
| 9 | 9 | |
| 10 | -func seedFollowData(t *testing.T, ctx context.Context, db *DB) (userDID, targetDID string) { | |
| 10 | +func seedFollowData(t *testing.T, ctx context.Context, dbs *Databases) (userDID, targetDID string) { | |
| 11 | 11 | t.Helper() |
| 12 | 12 | |
| 13 | 13 | userDID = "did:test:follower" |
| 14 | 14 | targetDID = "did:test:followed" |
| 15 | 15 | |
| 16 | - _, err := db.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, userDID, "follower") | |
| 16 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, userDID, "follower") | |
| 17 | 17 | assert.NilError(t, err) |
| 18 | - _, err = db.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, targetDID, "followed") | |
| 18 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, targetDID, "followed") | |
| 19 | 19 | assert.NilError(t, err) |
| 20 | 20 | |
| 21 | 21 | return userDID, targetDID |
| @@ -23,123 +23,123 @@ func seedFollowData(t *testing.T, ctx context.Context, db *DB) (userDID, targetD | ||
| 23 | 23 | |
| 24 | 24 | func TestUpsertFollow(t *testing.T) { |
| 25 | 25 | ctx := context.Background() |
| 26 | - db := setupTestDB(t) | |
| 27 | - userDID, targetDID := seedFollowData(t, ctx, db) | |
| 26 | + dbs := setupTestDB(t) | |
| 27 | + userDID, targetDID := seedFollowData(t, ctx, dbs) | |
| 28 | 28 | |
| 29 | - err := db.UpsertFollow(ctx, userDID, targetDID, "at://did:test:follower/app.bsky.graph.follow/123", "cid123") | |
| 29 | + err := dbs.Users.UpsertFollow(ctx, userDID, targetDID, "at://did:test:follower/app.bsky.graph.follow/123", "cid123") | |
| 30 | 30 | assert.NilError(t, err) |
| 31 | 31 | |
| 32 | - following, err := db.IsFollowing(ctx, userDID, targetDID) | |
| 32 | + following, err := dbs.Users.IsFollowing(ctx, userDID, targetDID) | |
| 33 | 33 | assert.NilError(t, err) |
| 34 | 34 | assert.Equal(t, following, true) |
| 35 | 35 | } |
| 36 | 36 | |
| 37 | 37 | func TestIsFollowing_NotFollowing(t *testing.T) { |
| 38 | 38 | ctx := context.Background() |
| 39 | - db := setupTestDB(t) | |
| 40 | - userDID, targetDID := seedFollowData(t, ctx, db) | |
| 39 | + dbs := setupTestDB(t) | |
| 40 | + userDID, targetDID := seedFollowData(t, ctx, dbs) | |
| 41 | 41 | |
| 42 | - following, err := db.IsFollowing(ctx, userDID, targetDID) | |
| 42 | + following, err := dbs.Users.IsFollowing(ctx, userDID, targetDID) | |
| 43 | 43 | assert.NilError(t, err) |
| 44 | 44 | assert.Equal(t, following, false) |
| 45 | 45 | } |
| 46 | 46 | |
| 47 | 47 | func TestDeleteFollow(t *testing.T) { |
| 48 | 48 | ctx := context.Background() |
| 49 | - db := setupTestDB(t) | |
| 50 | - userDID, targetDID := seedFollowData(t, ctx, db) | |
| 49 | + dbs := setupTestDB(t) | |
| 50 | + userDID, targetDID := seedFollowData(t, ctx, dbs) | |
| 51 | 51 | |
| 52 | - err := db.UpsertFollow(ctx, userDID, targetDID, "at://uri", "cid") | |
| 52 | + err := dbs.Users.UpsertFollow(ctx, userDID, targetDID, "at://uri", "cid") | |
| 53 | 53 | assert.NilError(t, err) |
| 54 | 54 | |
| 55 | - err = db.DeleteFollow(ctx, userDID, targetDID) | |
| 55 | + err = dbs.Users.DeleteFollow(ctx, userDID, targetDID) | |
| 56 | 56 | assert.NilError(t, err) |
| 57 | 57 | |
| 58 | - following, err := db.IsFollowing(ctx, userDID, targetDID) | |
| 58 | + following, err := dbs.Users.IsFollowing(ctx, userDID, targetDID) | |
| 59 | 59 | assert.NilError(t, err) |
| 60 | 60 | assert.Equal(t, following, false) |
| 61 | 61 | } |
| 62 | 62 | |
| 63 | 63 | func TestDeleteFollowByURI(t *testing.T) { |
| 64 | 64 | ctx := context.Background() |
| 65 | - db := setupTestDB(t) | |
| 66 | - userDID, targetDID := seedFollowData(t, ctx, db) | |
| 65 | + dbs := setupTestDB(t) | |
| 66 | + userDID, targetDID := seedFollowData(t, ctx, dbs) | |
| 67 | 67 | |
| 68 | 68 | uri := "at://did:test:follower/app.bsky.graph.follow/abc" |
| 69 | - err := db.UpsertFollow(ctx, userDID, targetDID, uri, "cid") | |
| 69 | + err := dbs.Users.UpsertFollow(ctx, userDID, targetDID, uri, "cid") | |
| 70 | 70 | assert.NilError(t, err) |
| 71 | 71 | |
| 72 | - err = db.DeleteFollowByURI(ctx, uri) | |
| 72 | + err = dbs.Users.DeleteFollowByURI(ctx, uri) | |
| 73 | 73 | assert.NilError(t, err) |
| 74 | 74 | |
| 75 | - following, err := db.IsFollowing(ctx, userDID, targetDID) | |
| 75 | + following, err := dbs.Users.IsFollowing(ctx, userDID, targetDID) | |
| 76 | 76 | assert.NilError(t, err) |
| 77 | 77 | assert.Equal(t, following, false) |
| 78 | 78 | } |
| 79 | 79 | |
| 80 | 80 | func TestListFollows(t *testing.T) { |
| 81 | 81 | ctx := context.Background() |
| 82 | - db := setupTestDB(t) | |
| 83 | - userDID, _ := seedFollowData(t, ctx, db) | |
| 82 | + dbs := setupTestDB(t) | |
| 83 | + userDID, _ := seedFollowData(t, ctx, dbs) | |
| 84 | 84 | |
| 85 | 85 | target2 := "did:test:followed2" |
| 86 | - _, err := db.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, target2, "followed2") | |
| 86 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, target2, "followed2") | |
| 87 | 87 | assert.NilError(t, err) |
| 88 | 88 | |
| 89 | - err = db.UpsertFollow(ctx, userDID, "did:test:followed", "uri1", "cid1") | |
| 89 | + err = dbs.Users.UpsertFollow(ctx, userDID, "did:test:followed", "uri1", "cid1") | |
| 90 | 90 | assert.NilError(t, err) |
| 91 | - err = db.UpsertFollow(ctx, userDID, target2, "uri2", "cid2") | |
| 91 | + err = dbs.Users.UpsertFollow(ctx, userDID, target2, "uri2", "cid2") | |
| 92 | 92 | assert.NilError(t, err) |
| 93 | 93 | |
| 94 | - follows, err := db.ListFollows(ctx, userDID, 10, 0) | |
| 94 | + follows, err := dbs.Users.ListFollows(ctx, userDID, 10, 0) | |
| 95 | 95 | assert.NilError(t, err) |
| 96 | 96 | assert.Equal(t, len(follows), 2) |
| 97 | 97 | } |
| 98 | 98 | |
| 99 | 99 | func TestListFollowers(t *testing.T) { |
| 100 | 100 | ctx := context.Background() |
| 101 | - db := setupTestDB(t) | |
| 102 | - _, targetDID := seedFollowData(t, ctx, db) | |
| 101 | + dbs := setupTestDB(t) | |
| 102 | + _, targetDID := seedFollowData(t, ctx, dbs) | |
| 103 | 103 | |
| 104 | 104 | follower2 := "did:test:follower2" |
| 105 | - _, err := db.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, follower2, "follower2") | |
| 105 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, follower2, "follower2") | |
| 106 | 106 | assert.NilError(t, err) |
| 107 | 107 | |
| 108 | - err = db.UpsertFollow(ctx, "did:test:follower", targetDID, "uri1", "cid1") | |
| 108 | + err = dbs.Users.UpsertFollow(ctx, "did:test:follower", targetDID, "uri1", "cid1") | |
| 109 | 109 | assert.NilError(t, err) |
| 110 | - err = db.UpsertFollow(ctx, follower2, targetDID, "uri2", "cid2") | |
| 110 | + err = dbs.Users.UpsertFollow(ctx, follower2, targetDID, "uri2", "cid2") | |
| 111 | 111 | assert.NilError(t, err) |
| 112 | 112 | |
| 113 | - followers, err := db.ListFollowers(ctx, targetDID, 10, 0) | |
| 113 | + followers, err := dbs.Users.ListFollowers(ctx, targetDID, 10, 0) | |
| 114 | 114 | assert.NilError(t, err) |
| 115 | 115 | assert.Equal(t, len(followers), 2) |
| 116 | 116 | } |
| 117 | 117 | |
| 118 | 118 | func TestGetFollowDIDs(t *testing.T) { |
| 119 | 119 | ctx := context.Background() |
| 120 | - db := setupTestDB(t) | |
| 121 | - userDID, _ := seedFollowData(t, ctx, db) | |
| 120 | + dbs := setupTestDB(t) | |
| 121 | + userDID, _ := seedFollowData(t, ctx, dbs) | |
| 122 | 122 | |
| 123 | 123 | target2 := "did:test:followed2" |
| 124 | - _, err := db.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, target2, "followed2") | |
| 124 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, target2, "followed2") | |
| 125 | 125 | assert.NilError(t, err) |
| 126 | 126 | |
| 127 | - err = db.UpsertFollow(ctx, userDID, "did:test:followed", "uri1", "cid1") | |
| 127 | + err = dbs.Users.UpsertFollow(ctx, userDID, "did:test:followed", "uri1", "cid1") | |
| 128 | 128 | assert.NilError(t, err) |
| 129 | - err = db.UpsertFollow(ctx, userDID, target2, "uri2", "cid2") | |
| 129 | + err = dbs.Users.UpsertFollow(ctx, userDID, target2, "uri2", "cid2") | |
| 130 | 130 | assert.NilError(t, err) |
| 131 | 131 | |
| 132 | - dids, err := db.GetFollowDIDs(ctx, userDID) | |
| 132 | + dids, err := dbs.Users.GetFollowDIDs(ctx, userDID) | |
| 133 | 133 | assert.NilError(t, err) |
| 134 | 134 | assert.Equal(t, len(dids), 2) |
| 135 | 135 | } |
| 136 | 136 | |
| 137 | 137 | func TestSyncFollows_AddsNewRemovesStale(t *testing.T) { |
| 138 | 138 | ctx := context.Background() |
| 139 | - db := setupTestDB(t) | |
| 140 | - userDID, _ := seedFollowData(t, ctx, db) | |
| 139 | + dbs := setupTestDB(t) | |
| 140 | + userDID, _ := seedFollowData(t, ctx, dbs) | |
| 141 | 141 | |
| 142 | - err := db.UpsertFollow(ctx, userDID, "did:test:old", "old-uri", "old-cid") | |
| 142 | + err := dbs.Users.UpsertFollow(ctx, userDID, "did:test:old", "old-uri", "old-cid") | |
| 143 | 143 | assert.NilError(t, err) |
| 144 | 144 | |
| 145 | 145 | activeFollows := map[string]Follow{ |
| @@ -147,37 +147,37 @@ func TestSyncFollows_AddsNewRemovesStale(t *testing.T) { | ||
| 147 | 147 | "did:test:new2": {URI: NullStr("uri2"), CID: NullStr("cid2")}, |
| 148 | 148 | } |
| 149 | 149 | |
| 150 | - err = db.SyncFollows(ctx, userDID, activeFollows) | |
| 150 | + err = dbs.Users.SyncFollows(ctx, userDID, activeFollows) | |
| 151 | 151 | assert.NilError(t, err) |
| 152 | 152 | |
| 153 | - stillFollowing, err := db.IsFollowing(ctx, userDID, "did:test:old") | |
| 153 | + stillFollowing, err := dbs.Users.IsFollowing(ctx, userDID, "did:test:old") | |
| 154 | 154 | assert.NilError(t, err) |
| 155 | 155 | assert.Equal(t, stillFollowing, false) |
| 156 | 156 | |
| 157 | - following1, err := db.IsFollowing(ctx, userDID, "did:test:new1") | |
| 157 | + following1, err := dbs.Users.IsFollowing(ctx, userDID, "did:test:new1") | |
| 158 | 158 | assert.NilError(t, err) |
| 159 | 159 | assert.Equal(t, following1, true) |
| 160 | 160 | |
| 161 | - following2, err := db.IsFollowing(ctx, userDID, "did:test:new2") | |
| 161 | + following2, err := dbs.Users.IsFollowing(ctx, userDID, "did:test:new2") | |
| 162 | 162 | assert.NilError(t, err) |
| 163 | 163 | assert.Equal(t, following2, true) |
| 164 | 164 | |
| 165 | - dids, err := db.GetFollowDIDs(ctx, userDID) | |
| 165 | + dids, err := dbs.Users.GetFollowDIDs(ctx, userDID) | |
| 166 | 166 | assert.NilError(t, err) |
| 167 | 167 | assert.Equal(t, len(dids), 2) |
| 168 | 168 | } |
| 169 | 169 | |
| 170 | 170 | func TestUpsertFollow_Idempotent(t *testing.T) { |
| 171 | 171 | ctx := context.Background() |
| 172 | - db := setupTestDB(t) | |
| 173 | - userDID, targetDID := seedFollowData(t, ctx, db) | |
| 172 | + dbs := setupTestDB(t) | |
| 173 | + userDID, targetDID := seedFollowData(t, ctx, dbs) | |
| 174 | 174 | |
| 175 | - err := db.UpsertFollow(ctx, userDID, targetDID, "uri1", "cid1") | |
| 175 | + err := dbs.Users.UpsertFollow(ctx, userDID, targetDID, "uri1", "cid1") | |
| 176 | 176 | assert.NilError(t, err) |
| 177 | - err = db.UpsertFollow(ctx, userDID, targetDID, "uri2", "cid2") | |
| 177 | + err = dbs.Users.UpsertFollow(ctx, userDID, targetDID, "uri2", "cid2") | |
| 178 | 178 | assert.NilError(t, err) |
| 179 | 179 | |
| 180 | - follows, err := db.ListFollows(ctx, userDID, 10, 0) | |
| 180 | + follows, err := dbs.Users.ListFollows(ctx, userDID, 10, 0) | |
| 181 | 181 | assert.NilError(t, err) |
| 182 | 182 | assert.Equal(t, len(follows), 1) |
| 183 | 183 | assert.Equal(t, follows[0].URI.String, "uri2") |
| @@ -7,15 +7,15 @@ import ( | |||
| 7 | "gotest.tools/v3/assert" | 7 | "gotest.tools/v3/assert" |
| 8 | ) | 8 | ) |
| 9 | 9 | ||
| 10 | -func seedFollowData(t *testing.T, ctx context.Context, db *DB) (userDID, targetDID string) { | 10 | +func seedFollowData(t *testing.T, ctx context.Context, dbs *Databases) (userDID, targetDID string) { |
| 11 | t.Helper() | 11 | t.Helper() |
| 12 | 12 | ||
| 13 | userDID = "did:test:follower" | 13 | userDID = "did:test:follower" |
| 14 | targetDID = "did:test:followed" | 14 | targetDID = "did:test:followed" |
| 15 | 15 | ||
| 16 | - _, err := db.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, userDID, "follower") | 16 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, userDID, "follower") |
| 17 | assert.NilError(t, err) | 17 | assert.NilError(t, err) |
| 18 | - _, err = db.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, targetDID, "followed") | 18 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, targetDID, "followed") |
| 19 | assert.NilError(t, err) | 19 | assert.NilError(t, err) |
| 20 | 20 | ||
| 21 | return userDID, targetDID | 21 | return userDID, targetDID |
| @@ -23,123 +23,123 @@ func seedFollowData(t *testing.T, ctx context.Context, db *DB) (userDID, targetD | |||
| 23 | 23 | ||
| 24 | func TestUpsertFollow(t *testing.T) { | 24 | func TestUpsertFollow(t *testing.T) { |
| 25 | ctx := context.Background() | 25 | ctx := context.Background() |
| 26 | - db := setupTestDB(t) | 26 | + dbs := setupTestDB(t) |
| 27 | - userDID, targetDID := seedFollowData(t, ctx, db) | 27 | + userDID, targetDID := seedFollowData(t, ctx, dbs) |
| 28 | 28 | ||
| 29 | - err := db.UpsertFollow(ctx, userDID, targetDID, "at://did:test:follower/app.bsky.graph.follow/123", "cid123") | 29 | + err := dbs.Users.UpsertFollow(ctx, userDID, targetDID, "at://did:test:follower/app.bsky.graph.follow/123", "cid123") |
| 30 | assert.NilError(t, err) | 30 | assert.NilError(t, err) |
| 31 | 31 | ||
| 32 | - following, err := db.IsFollowing(ctx, userDID, targetDID) | 32 | + following, err := dbs.Users.IsFollowing(ctx, userDID, targetDID) |
| 33 | assert.NilError(t, err) | 33 | assert.NilError(t, err) |
| 34 | assert.Equal(t, following, true) | 34 | assert.Equal(t, following, true) |
| 35 | } | 35 | } |
| 36 | 36 | ||
| 37 | func TestIsFollowing_NotFollowing(t *testing.T) { | 37 | func TestIsFollowing_NotFollowing(t *testing.T) { |
| 38 | ctx := context.Background() | 38 | ctx := context.Background() |
| 39 | - db := setupTestDB(t) | 39 | + dbs := setupTestDB(t) |
| 40 | - userDID, targetDID := seedFollowData(t, ctx, db) | 40 | + userDID, targetDID := seedFollowData(t, ctx, dbs) |
| 41 | 41 | ||
| 42 | - following, err := db.IsFollowing(ctx, userDID, targetDID) | 42 | + following, err := dbs.Users.IsFollowing(ctx, userDID, targetDID) |
| 43 | assert.NilError(t, err) | 43 | assert.NilError(t, err) |
| 44 | assert.Equal(t, following, false) | 44 | assert.Equal(t, following, false) |
| 45 | } | 45 | } |
| 46 | 46 | ||
| 47 | func TestDeleteFollow(t *testing.T) { | 47 | func TestDeleteFollow(t *testing.T) { |
| 48 | ctx := context.Background() | 48 | ctx := context.Background() |
| 49 | - db := setupTestDB(t) | 49 | + dbs := setupTestDB(t) |
| 50 | - userDID, targetDID := seedFollowData(t, ctx, db) | 50 | + userDID, targetDID := seedFollowData(t, ctx, dbs) |
| 51 | 51 | ||
| 52 | - err := db.UpsertFollow(ctx, userDID, targetDID, "at://uri", "cid") | 52 | + err := dbs.Users.UpsertFollow(ctx, userDID, targetDID, "at://uri", "cid") |
| 53 | assert.NilError(t, err) | 53 | assert.NilError(t, err) |
| 54 | 54 | ||
| 55 | - err = db.DeleteFollow(ctx, userDID, targetDID) | 55 | + err = dbs.Users.DeleteFollow(ctx, userDID, targetDID) |
| 56 | assert.NilError(t, err) | 56 | assert.NilError(t, err) |
| 57 | 57 | ||
| 58 | - following, err := db.IsFollowing(ctx, userDID, targetDID) | 58 | + following, err := dbs.Users.IsFollowing(ctx, userDID, targetDID) |
| 59 | assert.NilError(t, err) | 59 | assert.NilError(t, err) |
| 60 | assert.Equal(t, following, false) | 60 | assert.Equal(t, following, false) |
| 61 | } | 61 | } |
| 62 | 62 | ||
| 63 | func TestDeleteFollowByURI(t *testing.T) { | 63 | func TestDeleteFollowByURI(t *testing.T) { |
| 64 | ctx := context.Background() | 64 | ctx := context.Background() |
| 65 | - db := setupTestDB(t) | 65 | + dbs := setupTestDB(t) |
| 66 | - userDID, targetDID := seedFollowData(t, ctx, db) | 66 | + userDID, targetDID := seedFollowData(t, ctx, dbs) |
| 67 | 67 | ||
| 68 | uri := "at://did:test:follower/app.bsky.graph.follow/abc" | 68 | uri := "at://did:test:follower/app.bsky.graph.follow/abc" |
| 69 | - err := db.UpsertFollow(ctx, userDID, targetDID, uri, "cid") | 69 | + err := dbs.Users.UpsertFollow(ctx, userDID, targetDID, uri, "cid") |
| 70 | assert.NilError(t, err) | 70 | assert.NilError(t, err) |
| 71 | 71 | ||
| 72 | - err = db.DeleteFollowByURI(ctx, uri) | 72 | + err = dbs.Users.DeleteFollowByURI(ctx, uri) |
| 73 | assert.NilError(t, err) | 73 | assert.NilError(t, err) |
| 74 | 74 | ||
| 75 | - following, err := db.IsFollowing(ctx, userDID, targetDID) | 75 | + following, err := dbs.Users.IsFollowing(ctx, userDID, targetDID) |
| 76 | assert.NilError(t, err) | 76 | assert.NilError(t, err) |
| 77 | assert.Equal(t, following, false) | 77 | assert.Equal(t, following, false) |
| 78 | } | 78 | } |
| 79 | 79 | ||
| 80 | func TestListFollows(t *testing.T) { | 80 | func TestListFollows(t *testing.T) { |
| 81 | ctx := context.Background() | 81 | ctx := context.Background() |
| 82 | - db := setupTestDB(t) | 82 | + dbs := setupTestDB(t) |
| 83 | - userDID, _ := seedFollowData(t, ctx, db) | 83 | + userDID, _ := seedFollowData(t, ctx, dbs) |
| 84 | 84 | ||
| 85 | target2 := "did:test:followed2" | 85 | target2 := "did:test:followed2" |
| 86 | - _, err := db.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, target2, "followed2") | 86 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, target2, "followed2") |
| 87 | assert.NilError(t, err) | 87 | assert.NilError(t, err) |
| 88 | 88 | ||
| 89 | - err = db.UpsertFollow(ctx, userDID, "did:test:followed", "uri1", "cid1") | 89 | + err = dbs.Users.UpsertFollow(ctx, userDID, "did:test:followed", "uri1", "cid1") |
| 90 | assert.NilError(t, err) | 90 | assert.NilError(t, err) |
| 91 | - err = db.UpsertFollow(ctx, userDID, target2, "uri2", "cid2") | 91 | + err = dbs.Users.UpsertFollow(ctx, userDID, target2, "uri2", "cid2") |
| 92 | assert.NilError(t, err) | 92 | assert.NilError(t, err) |
| 93 | 93 | ||
| 94 | - follows, err := db.ListFollows(ctx, userDID, 10, 0) | 94 | + follows, err := dbs.Users.ListFollows(ctx, userDID, 10, 0) |
| 95 | assert.NilError(t, err) | 95 | assert.NilError(t, err) |
| 96 | assert.Equal(t, len(follows), 2) | 96 | assert.Equal(t, len(follows), 2) |
| 97 | } | 97 | } |
| 98 | 98 | ||
| 99 | func TestListFollowers(t *testing.T) { | 99 | func TestListFollowers(t *testing.T) { |
| 100 | ctx := context.Background() | 100 | ctx := context.Background() |
| 101 | - db := setupTestDB(t) | 101 | + dbs := setupTestDB(t) |
| 102 | - _, targetDID := seedFollowData(t, ctx, db) | 102 | + _, targetDID := seedFollowData(t, ctx, dbs) |
| 103 | 103 | ||
| 104 | follower2 := "did:test:follower2" | 104 | follower2 := "did:test:follower2" |
| 105 | - _, err := db.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, follower2, "follower2") | 105 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, follower2, "follower2") |
| 106 | assert.NilError(t, err) | 106 | assert.NilError(t, err) |
| 107 | 107 | ||
| 108 | - err = db.UpsertFollow(ctx, "did:test:follower", targetDID, "uri1", "cid1") | 108 | + err = dbs.Users.UpsertFollow(ctx, "did:test:follower", targetDID, "uri1", "cid1") |
| 109 | assert.NilError(t, err) | 109 | assert.NilError(t, err) |
| 110 | - err = db.UpsertFollow(ctx, follower2, targetDID, "uri2", "cid2") | 110 | + err = dbs.Users.UpsertFollow(ctx, follower2, targetDID, "uri2", "cid2") |
| 111 | assert.NilError(t, err) | 111 | assert.NilError(t, err) |
| 112 | 112 | ||
| 113 | - followers, err := db.ListFollowers(ctx, targetDID, 10, 0) | 113 | + followers, err := dbs.Users.ListFollowers(ctx, targetDID, 10, 0) |
| 114 | assert.NilError(t, err) | 114 | assert.NilError(t, err) |
| 115 | assert.Equal(t, len(followers), 2) | 115 | assert.Equal(t, len(followers), 2) |
| 116 | } | 116 | } |
| 117 | 117 | ||
| 118 | func TestGetFollowDIDs(t *testing.T) { | 118 | func TestGetFollowDIDs(t *testing.T) { |
| 119 | ctx := context.Background() | 119 | ctx := context.Background() |
| 120 | - db := setupTestDB(t) | 120 | + dbs := setupTestDB(t) |
| 121 | - userDID, _ := seedFollowData(t, ctx, db) | 121 | + userDID, _ := seedFollowData(t, ctx, dbs) |
| 122 | 122 | ||
| 123 | target2 := "did:test:followed2" | 123 | target2 := "did:test:followed2" |
| 124 | - _, err := db.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, target2, "followed2") | 124 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, target2, "followed2") |
| 125 | assert.NilError(t, err) | 125 | assert.NilError(t, err) |
| 126 | 126 | ||
| 127 | - err = db.UpsertFollow(ctx, userDID, "did:test:followed", "uri1", "cid1") | 127 | + err = dbs.Users.UpsertFollow(ctx, userDID, "did:test:followed", "uri1", "cid1") |
| 128 | assert.NilError(t, err) | 128 | assert.NilError(t, err) |
| 129 | - err = db.UpsertFollow(ctx, userDID, target2, "uri2", "cid2") | 129 | + err = dbs.Users.UpsertFollow(ctx, userDID, target2, "uri2", "cid2") |
| 130 | assert.NilError(t, err) | 130 | assert.NilError(t, err) |
| 131 | 131 | ||
| 132 | - dids, err := db.GetFollowDIDs(ctx, userDID) | 132 | + dids, err := dbs.Users.GetFollowDIDs(ctx, userDID) |
| 133 | assert.NilError(t, err) | 133 | assert.NilError(t, err) |
| 134 | assert.Equal(t, len(dids), 2) | 134 | assert.Equal(t, len(dids), 2) |
| 135 | } | 135 | } |
| 136 | 136 | ||
| 137 | func TestSyncFollows_AddsNewRemovesStale(t *testing.T) { | 137 | func TestSyncFollows_AddsNewRemovesStale(t *testing.T) { |
| 138 | ctx := context.Background() | 138 | ctx := context.Background() |
| 139 | - db := setupTestDB(t) | 139 | + dbs := setupTestDB(t) |
| 140 | - userDID, _ := seedFollowData(t, ctx, db) | 140 | + userDID, _ := seedFollowData(t, ctx, dbs) |
| 141 | 141 | ||
| 142 | - err := db.UpsertFollow(ctx, userDID, "did:test:old", "old-uri", "old-cid") | 142 | + err := dbs.Users.UpsertFollow(ctx, userDID, "did:test:old", "old-uri", "old-cid") |
| 143 | assert.NilError(t, err) | 143 | assert.NilError(t, err) |
| 144 | 144 | ||
| 145 | activeFollows := map[string]Follow{ | 145 | activeFollows := map[string]Follow{ |
| @@ -147,37 +147,37 @@ func TestSyncFollows_AddsNewRemovesStale(t *testing.T) { | |||
| 147 | "did:test:new2": {URI: NullStr("uri2"), CID: NullStr("cid2")}, | 147 | "did:test:new2": {URI: NullStr("uri2"), CID: NullStr("cid2")}, |
| 148 | } | 148 | } |
| 149 | 149 | ||
| 150 | - err = db.SyncFollows(ctx, userDID, activeFollows) | 150 | + err = dbs.Users.SyncFollows(ctx, userDID, activeFollows) |
| 151 | assert.NilError(t, err) | 151 | assert.NilError(t, err) |
| 152 | 152 | ||
| 153 | - stillFollowing, err := db.IsFollowing(ctx, userDID, "did:test:old") | 153 | + stillFollowing, err := dbs.Users.IsFollowing(ctx, userDID, "did:test:old") |
| 154 | assert.NilError(t, err) | 154 | assert.NilError(t, err) |
| 155 | assert.Equal(t, stillFollowing, false) | 155 | assert.Equal(t, stillFollowing, false) |
| 156 | 156 | ||
| 157 | - following1, err := db.IsFollowing(ctx, userDID, "did:test:new1") | 157 | + following1, err := dbs.Users.IsFollowing(ctx, userDID, "did:test:new1") |
| 158 | assert.NilError(t, err) | 158 | assert.NilError(t, err) |
| 159 | assert.Equal(t, following1, true) | 159 | assert.Equal(t, following1, true) |
| 160 | 160 | ||
| 161 | - following2, err := db.IsFollowing(ctx, userDID, "did:test:new2") | 161 | + following2, err := dbs.Users.IsFollowing(ctx, userDID, "did:test:new2") |
| 162 | assert.NilError(t, err) | 162 | assert.NilError(t, err) |
| 163 | assert.Equal(t, following2, true) | 163 | assert.Equal(t, following2, true) |
| 164 | 164 | ||
| 165 | - dids, err := db.GetFollowDIDs(ctx, userDID) | 165 | + dids, err := dbs.Users.GetFollowDIDs(ctx, userDID) |
| 166 | assert.NilError(t, err) | 166 | assert.NilError(t, err) |
| 167 | assert.Equal(t, len(dids), 2) | 167 | assert.Equal(t, len(dids), 2) |
| 168 | } | 168 | } |
| 169 | 169 | ||
| 170 | func TestUpsertFollow_Idempotent(t *testing.T) { | 170 | func TestUpsertFollow_Idempotent(t *testing.T) { |
| 171 | ctx := context.Background() | 171 | ctx := context.Background() |
| 172 | - db := setupTestDB(t) | 172 | + dbs := setupTestDB(t) |
| 173 | - userDID, targetDID := seedFollowData(t, ctx, db) | 173 | + userDID, targetDID := seedFollowData(t, ctx, dbs) |
| 174 | 174 | ||
| 175 | - err := db.UpsertFollow(ctx, userDID, targetDID, "uri1", "cid1") | 175 | + err := dbs.Users.UpsertFollow(ctx, userDID, targetDID, "uri1", "cid1") |
| 176 | assert.NilError(t, err) | 176 | assert.NilError(t, err) |
| 177 | - err = db.UpsertFollow(ctx, userDID, targetDID, "uri2", "cid2") | 177 | + err = dbs.Users.UpsertFollow(ctx, userDID, targetDID, "uri2", "cid2") |
| 178 | assert.NilError(t, err) | 178 | assert.NilError(t, err) |
| 179 | 179 | ||
| 180 | - follows, err := db.ListFollows(ctx, userDID, 10, 0) | 180 | + follows, err := dbs.Users.ListFollows(ctx, userDID, 10, 0) |
| 181 | assert.NilError(t, err) | 181 | assert.NilError(t, err) |
| 182 | assert.Equal(t, len(follows), 1) | 182 | assert.Equal(t, len(follows), 1) |
| 183 | assert.Equal(t, follows[0].URI.String, "uri2") | 183 | assert.Equal(t, follows[0].URI.String, "uri2") |
modified
internal/db/multi.go +72 -86 | @@ -11,9 +11,10 @@ import ( | ||
| 11 | 11 | ) |
| 12 | 12 | |
| 13 | 13 | type Databases struct { |
| 14 | - Users *DB | |
| 15 | - Articles *DB | |
| 16 | - Recs *DB | |
| 14 | + Users *UserStore | |
| 15 | + Articles *ArticleStore | |
| 16 | + | |
| 17 | + db *DB | |
| 17 | 18 | } |
| 18 | 19 | |
| 19 | 20 | var multiDriverSeq int64 |
| @@ -22,6 +23,14 @@ func OpenAll(basePath string) (*Databases, error) { | ||
| 22 | 23 | articlesPath := basePath + "_articles" |
| 23 | 24 | recsPath := basePath + "_recs" |
| 24 | 25 | |
| 26 | + for _, p := range []string{articlesPath, recsPath} { | |
| 27 | + f, err := sql.Open("sqlite3", p+"?"+DSN) | |
| 28 | + if err != nil { | |
| 29 | + return nil, err | |
| 30 | + } | |
| 31 | + f.Close() | |
| 32 | + } | |
| 33 | + | |
| 25 | 34 | seq := atomic.AddInt64(&multiDriverSeq, 1) |
| 26 | 35 | driverName := fmt.Sprintf("sqlite3_glean_multi_%d", seq) |
| 27 | 36 | |
| @@ -52,69 +61,48 @@ func OpenAll(basePath string) (*Databases, error) { | ||
| 52 | 61 | }, |
| 53 | 62 | }) |
| 54 | 63 | |
| 55 | - usersDB, err := sql.Open(driverName, basePath+"_users?cache=shared&"+DSN) | |
| 56 | - if err != nil { | |
| 57 | - return nil, err | |
| 58 | - } | |
| 59 | - usersDB.SetMaxOpenConns(10) | |
| 60 | - usersDB.SetMaxIdleConns(5) | |
| 61 | - usersDB.SetConnMaxLifetime(30 * time.Minute) | |
| 62 | - users := &DB{usersDB} | |
| 63 | - | |
| 64 | - articles, err := Open(articlesPath) | |
| 64 | + db, err := sql.Open(driverName, basePath+"_users?cache=shared&"+DSN) | |
| 65 | 65 | if err != nil { |
| 66 | - users.Close() | |
| 67 | 66 | return nil, err |
| 68 | 67 | } |
| 68 | + db.SetMaxOpenConns(10) | |
| 69 | + db.SetMaxIdleConns(5) | |
| 70 | + db.SetConnMaxLifetime(30 * time.Minute) | |
| 71 | + d := &DB{db} | |
| 69 | 72 | |
| 70 | - recs, err := Open(recsPath) | |
| 71 | - if err != nil { | |
| 72 | - users.Close() | |
| 73 | - articles.Close() | |
| 74 | - return nil, err | |
| 75 | - } | |
| 76 | - | |
| 77 | - if err := initUsersSchema(users); err != nil { | |
| 78 | - users.Close() | |
| 79 | - articles.Close() | |
| 80 | - recs.Close() | |
| 73 | + if err := initUsersSchema(d); err != nil { | |
| 74 | + d.Close() | |
| 81 | 75 | return nil, err |
| 82 | 76 | } |
| 83 | 77 | |
| 84 | - if err := initArticlesSchema(articles); err != nil { | |
| 85 | - users.Close() | |
| 86 | - articles.Close() | |
| 87 | - recs.Close() | |
| 78 | + if err := initArticlesSchema(d); err != nil { | |
| 79 | + d.Close() | |
| 88 | 80 | return nil, err |
| 89 | 81 | } |
| 90 | 82 | |
| 91 | - if err := initRecsSchema(recs); err != nil { | |
| 92 | - users.Close() | |
| 93 | - articles.Close() | |
| 94 | - recs.Close() | |
| 83 | + if err := initRecsSchema(d); err != nil { | |
| 84 | + d.Close() | |
| 95 | 85 | return nil, err |
| 96 | 86 | } |
| 97 | 87 | |
| 98 | 88 | return &Databases{ |
| 99 | - Users: users, | |
| 100 | - Articles: articles, | |
| 101 | - Recs: recs, | |
| 89 | + Users: NewUserStore(d), | |
| 90 | + Articles: NewArticleStore(d), | |
| 91 | + db: d, | |
| 102 | 92 | }, nil |
| 103 | 93 | } |
| 104 | 94 | |
| 105 | 95 | func (d *Databases) Close() error { |
| 106 | - if d.Users != nil { | |
| 107 | - _ = d.Users.Close() | |
| 108 | - } | |
| 109 | - if d.Articles != nil { | |
| 110 | - _ = d.Articles.Close() | |
| 111 | - } | |
| 112 | - if d.Recs != nil { | |
| 113 | - _ = d.Recs.Close() | |
| 96 | + if d.db != nil { | |
| 97 | + _ = d.db.Close() | |
| 114 | 98 | } |
| 115 | 99 | return nil |
| 116 | 100 | } |
| 117 | 101 | |
| 102 | +func (d *Databases) DB() *sql.DB { | |
| 103 | + return d.db.DB | |
| 104 | +} | |
| 105 | + | |
| 118 | 106 | func initUsersSchema(db *DB) error { |
| 119 | 107 | for _, s := range usersSchema { |
| 120 | 108 | if _, err := db.Exec(s); err != nil { |
| @@ -181,7 +169,7 @@ var usersSchema = []string{ | ||
| 181 | 169 | } |
| 182 | 170 | |
| 183 | 171 | var articlesSchema = []string{ |
| 184 | - `CREATE TABLE IF NOT EXISTS feeds ( | |
| 172 | + `CREATE TABLE IF NOT EXISTS articles.feeds ( | |
| 185 | 173 | feed_url TEXT PRIMARY KEY, |
| 186 | 174 | title TEXT, |
| 187 | 175 | site_url TEXT, |
| @@ -192,14 +180,12 @@ var articlesSchema = []string{ | ||
| 192 | 180 | subscriber_count INTEGER NOT NULL DEFAULT 0, |
| 193 | 181 | etag TEXT, |
| 194 | 182 | last_modified TEXT, |
| 195 | - fetch_interval_minutes INTEGER NOT NULL DEFAULT 30, | |
| 196 | - next_fetch_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| 197 | 183 | consecutive_empty_fetches INTEGER NOT NULL DEFAULT 0, |
| 198 | 184 | error_count INTEGER NOT NULL DEFAULT 0, |
| 199 | 185 | favicon_url TEXT |
| 200 | 186 | )`, |
| 201 | 187 | |
| 202 | - `CREATE TABLE IF NOT EXISTS subscriptions ( | |
| 188 | + `CREATE TABLE IF NOT EXISTS articles.subscriptions ( | |
| 203 | 189 | id INTEGER PRIMARY KEY AUTOINCREMENT, |
| 204 | 190 | user_did TEXT NOT NULL, |
| 205 | 191 | feed_url TEXT NOT NULL, |
| @@ -211,7 +197,7 @@ var articlesSchema = []string{ | ||
| 211 | 197 | UNIQUE(user_did, feed_url) |
| 212 | 198 | )`, |
| 213 | 199 | |
| 214 | - `CREATE TABLE IF NOT EXISTS articles ( | |
| 200 | + `CREATE TABLE IF NOT EXISTS articles.articles ( | |
| 215 | 201 | id INTEGER PRIMARY KEY AUTOINCREMENT, |
| 216 | 202 | feed_url TEXT NOT NULL, |
| 217 | 203 | guid TEXT NOT NULL, |
| @@ -227,7 +213,7 @@ var articlesSchema = []string{ | ||
| 227 | 213 | UNIQUE(feed_url, guid) |
| 228 | 214 | )`, |
| 229 | 215 | |
| 230 | - `CREATE TABLE IF NOT EXISTS read_state ( | |
| 216 | + `CREATE TABLE IF NOT EXISTS articles.read_state ( | |
| 231 | 217 | user_did TEXT NOT NULL, |
| 232 | 218 | article_id INTEGER NOT NULL, |
| 233 | 219 | is_read BOOLEAN NOT NULL DEFAULT 0, |
| @@ -235,7 +221,7 @@ var articlesSchema = []string{ | ||
| 235 | 221 | PRIMARY KEY (user_did, article_id) |
| 236 | 222 | )`, |
| 237 | 223 | |
| 238 | - `CREATE TABLE IF NOT EXISTS annotations ( | |
| 224 | + `CREATE TABLE IF NOT EXISTS articles.annotations ( | |
| 239 | 225 | id INTEGER PRIMARY KEY AUTOINCREMENT, |
| 240 | 226 | uri TEXT NOT NULL UNIQUE, |
| 241 | 227 | author_did TEXT NOT NULL, |
| @@ -249,7 +235,7 @@ var articlesSchema = []string{ | ||
| 249 | 235 | cid TEXT |
| 250 | 236 | )`, |
| 251 | 237 | |
| 252 | - `CREATE TABLE IF NOT EXISTS likes ( | |
| 238 | + `CREATE TABLE IF NOT EXISTS articles.likes ( | |
| 253 | 239 | id INTEGER PRIMARY KEY AUTOINCREMENT, |
| 254 | 240 | uri TEXT NOT NULL UNIQUE, |
| 255 | 241 | author_did TEXT NOT NULL, |
| @@ -260,37 +246,37 @@ var articlesSchema = []string{ | ||
| 260 | 246 | UNIQUE(author_did, feed_url, article_url) |
| 261 | 247 | )`, |
| 262 | 248 | |
| 263 | - `CREATE INDEX IF NOT EXISTS idx_subscriptions_feed ON subscriptions(feed_url)`, | |
| 264 | - `CREATE INDEX IF NOT EXISTS idx_subscriptions_feed_user ON subscriptions(feed_url, user_did)`, | |
| 265 | - `CREATE INDEX IF NOT EXISTS idx_subscriptions_user ON subscriptions(user_did)`, | |
| 266 | - `CREATE INDEX IF NOT EXISTS idx_subscriptions_uri ON subscriptions(uri)`, | |
| 267 | - `CREATE INDEX IF NOT EXISTS idx_likes_author_feed ON likes(author_did, feed_url, created_at)`, | |
| 268 | - `CREATE INDEX IF NOT EXISTS idx_articles_feed ON articles(feed_url)`, | |
| 269 | - `CREATE INDEX IF NOT EXISTS idx_articles_published ON articles(published DESC)`, | |
| 270 | - `CREATE INDEX IF NOT EXISTS idx_articles_url ON articles(url)`, | |
| 271 | - `CREATE INDEX IF NOT EXISTS idx_read_state_unread ON read_state(user_did, is_read) WHERE is_read = 0`, | |
| 272 | - `CREATE INDEX IF NOT EXISTS idx_annotations_article ON annotations(article_url)`, | |
| 273 | - `CREATE INDEX IF NOT EXISTS idx_annotations_author ON annotations(author_did)`, | |
| 274 | - `CREATE INDEX IF NOT EXISTS idx_annotations_created_at ON annotations(created_at DESC)`, | |
| 275 | - `CREATE INDEX IF NOT EXISTS idx_likes_article ON likes(feed_url, article_url)`, | |
| 276 | - `CREATE INDEX IF NOT EXISTS idx_likes_author ON likes(author_did)`, | |
| 277 | - `CREATE INDEX IF NOT EXISTS idx_likes_created_at ON likes(created_at DESC)`, | |
| 278 | - | |
| 279 | - `CREATE VIRTUAL TABLE IF NOT EXISTS articles_fts USING fts5(title, summary, content, author, content=articles, content_rowid=id)`, | |
| 280 | - `CREATE TRIGGER IF NOT EXISTS articles_ai AFTER INSERT ON articles BEGIN | |
| 249 | + `CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_feed ON subscriptions(feed_url)`, | |
| 250 | + `CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_feed_user ON subscriptions(feed_url, user_did)`, | |
| 251 | + `CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_user ON subscriptions(user_did)`, | |
| 252 | + `CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_uri ON subscriptions(uri)`, | |
| 253 | + `CREATE INDEX IF NOT EXISTS articles.idx_likes_author_feed ON likes(author_did, feed_url, created_at)`, | |
| 254 | + `CREATE INDEX IF NOT EXISTS articles.idx_articles_feed ON articles(feed_url)`, | |
| 255 | + `CREATE INDEX IF NOT EXISTS articles.idx_articles_published ON articles(published DESC)`, | |
| 256 | + `CREATE INDEX IF NOT EXISTS articles.idx_articles_url ON articles(url)`, | |
| 257 | + `CREATE INDEX IF NOT EXISTS articles.idx_read_state_unread ON read_state(user_did, is_read) WHERE is_read = 0`, | |
| 258 | + `CREATE INDEX IF NOT EXISTS articles.idx_annotations_article ON annotations(article_url)`, | |
| 259 | + `CREATE INDEX IF NOT EXISTS articles.idx_annotations_author ON annotations(author_did)`, | |
| 260 | + `CREATE INDEX IF NOT EXISTS articles.idx_annotations_created_at ON annotations(created_at DESC)`, | |
| 261 | + `CREATE INDEX IF NOT EXISTS articles.idx_likes_article ON likes(feed_url, article_url)`, | |
| 262 | + `CREATE INDEX IF NOT EXISTS articles.idx_likes_author ON likes(author_did)`, | |
| 263 | + `CREATE INDEX IF NOT EXISTS articles.idx_likes_created_at ON likes(created_at DESC)`, | |
| 264 | + | |
| 265 | + `CREATE VIRTUAL TABLE IF NOT EXISTS articles.articles_fts USING fts5(title, summary, content, author, content=articles, content_rowid=id)`, | |
| 266 | + `CREATE TRIGGER IF NOT EXISTS articles.articles_ai AFTER INSERT ON articles BEGIN | |
| 281 | 267 | INSERT INTO articles_fts(rowid, title, summary, content, author) VALUES (new.id, new.title, new.summary, new.content, new.author); |
| 282 | 268 | END`, |
| 283 | - `CREATE TRIGGER IF NOT EXISTS articles_ad AFTER DELETE ON articles BEGIN | |
| 269 | + `CREATE TRIGGER IF NOT EXISTS articles.articles_ad AFTER DELETE ON articles BEGIN | |
| 284 | 270 | INSERT INTO articles_fts(articles_fts, rowid, title, summary, content, author) VALUES('delete', old.id, old.title, old.summary, old.content, old.author); |
| 285 | 271 | END`, |
| 286 | - `CREATE TRIGGER IF NOT EXISTS articles_au AFTER UPDATE ON articles BEGIN | |
| 272 | + `CREATE TRIGGER IF NOT EXISTS articles.articles_au AFTER UPDATE ON articles BEGIN | |
| 287 | 273 | INSERT INTO articles_fts(articles_fts, rowid, title, summary, content, author) VALUES('delete', old.id, old.title, old.summary, old.content, old.author); |
| 288 | 274 | INSERT INTO articles_fts(rowid, title, summary, content, author) VALUES (new.id, new.title, new.summary, new.content, new.author); |
| 289 | 275 | END`, |
| 290 | 276 | } |
| 291 | 277 | |
| 292 | 278 | var recsSchema = []string{ |
| 293 | - `CREATE TABLE IF NOT EXISTS feed_similarity ( | |
| 279 | + `CREATE TABLE IF NOT EXISTS recs.feed_similarity ( | |
| 294 | 280 | feed_a TEXT NOT NULL, |
| 295 | 281 | feed_b TEXT NOT NULL, |
| 296 | 282 | jaccard REAL NOT NULL, |
| @@ -299,7 +285,7 @@ var recsSchema = []string{ | ||
| 299 | 285 | CHECK(feed_a < feed_b) |
| 300 | 286 | )`, |
| 301 | 287 | |
| 302 | - `CREATE TABLE IF NOT EXISTS user_similarity ( | |
| 288 | + `CREATE TABLE IF NOT EXISTS recs.user_similarity ( | |
| 303 | 289 | user_a TEXT NOT NULL, |
| 304 | 290 | user_b TEXT NOT NULL, |
| 305 | 291 | jaccard REAL NOT NULL, |
| @@ -311,7 +297,7 @@ var recsSchema = []string{ | ||
| 311 | 297 | CHECK(user_a < user_b) |
| 312 | 298 | )`, |
| 313 | 299 | |
| 314 | - `CREATE TABLE IF NOT EXISTS dismissed_recommendations ( | |
| 300 | + `CREATE TABLE IF NOT EXISTS recs.dismissed_recommendations ( | |
| 315 | 301 | user_did TEXT NOT NULL, |
| 316 | 302 | target_type TEXT NOT NULL CHECK(target_type IN ('feed', 'article')), |
| 317 | 303 | target_id TEXT NOT NULL, |
| @@ -320,7 +306,7 @@ var recsSchema = []string{ | ||
| 320 | 306 | PRIMARY KEY (user_did, target_type, target_id) |
| 321 | 307 | )`, |
| 322 | 308 | |
| 323 | - `CREATE TABLE IF NOT EXISTS recommendation_impressions ( | |
| 309 | + `CREATE TABLE IF NOT EXISTS recs.recommendation_impressions ( | |
| 324 | 310 | user_did TEXT NOT NULL, |
| 325 | 311 | target_type TEXT NOT NULL CHECK(target_type IN ('feed', 'article')), |
| 326 | 312 | target_id TEXT NOT NULL, |
| @@ -331,14 +317,14 @@ var recsSchema = []string{ | ||
| 331 | 317 | PRIMARY KEY (user_did, target_type, target_id) |
| 332 | 318 | )`, |
| 333 | 319 | |
| 334 | - `CREATE TABLE IF NOT EXISTS follow_distances ( | |
| 320 | + `CREATE TABLE IF NOT EXISTS recs.follow_distances ( | |
| 335 | 321 | user_a TEXT NOT NULL, |
| 336 | 322 | user_b TEXT NOT NULL, |
| 337 | 323 | distance INTEGER NOT NULL CHECK(distance IN (1, 2)), |
| 338 | 324 | PRIMARY KEY (user_a, user_b) |
| 339 | 325 | )`, |
| 340 | 326 | |
| 341 | - `CREATE TABLE IF NOT EXISTS user_signal_weights ( | |
| 327 | + `CREATE TABLE IF NOT EXISTS recs.user_signal_weights ( | |
| 342 | 328 | user_did TEXT PRIMARY KEY, |
| 343 | 329 | w_sub REAL NOT NULL DEFAULT 1.0, |
| 344 | 330 | w_like REAL NOT NULL DEFAULT 0.5, |
| @@ -349,7 +335,7 @@ var recsSchema = []string{ | ||
| 349 | 335 | updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP |
| 350 | 336 | )`, |
| 351 | 337 | |
| 352 | - `CREATE TABLE IF NOT EXISTS user_signal_profiles ( | |
| 338 | + `CREATE TABLE IF NOT EXISTS recs.user_signal_profiles ( | |
| 353 | 339 | user_did TEXT PRIMARY KEY, |
| 354 | 340 | total_likes INTEGER NOT NULL DEFAULT 0, |
| 355 | 341 | total_tags INTEGER NOT NULL DEFAULT 0, |
| @@ -357,11 +343,11 @@ var recsSchema = []string{ | ||
| 357 | 343 | updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP |
| 358 | 344 | )`, |
| 359 | 345 | |
| 360 | - `CREATE INDEX IF NOT EXISTS idx_dismissed_user_type ON dismissed_recommendations(user_did, target_type)`, | |
| 361 | - `CREATE INDEX IF NOT EXISTS idx_impressions_user_unacted ON recommendation_impressions(user_did, acted, shown_count)`, | |
| 362 | - `CREATE INDEX IF NOT EXISTS idx_impressions_last_shown ON recommendation_impressions(last_shown_at)`, | |
| 363 | - `CREATE INDEX IF NOT EXISTS idx_follow_distances_b ON follow_distances(user_b)`, | |
| 364 | - `CREATE INDEX IF NOT EXISTS idx_follow_distances_a_dist ON follow_distances(user_a, distance)`, | |
| 365 | - `CREATE INDEX IF NOT EXISTS idx_user_similarity_b ON user_similarity(user_b)`, | |
| 366 | - `CREATE INDEX IF NOT EXISTS idx_user_similarity_a ON user_similarity(user_a)`, | |
| 346 | + `CREATE INDEX IF NOT EXISTS recs.idx_dismissed_user_type ON dismissed_recommendations(user_did, target_type)`, | |
| 347 | + `CREATE INDEX IF NOT EXISTS recs.idx_impressions_user_unacted ON recommendation_impressions(user_did, acted, shown_count)`, | |
| 348 | + `CREATE INDEX IF NOT EXISTS recs.idx_impressions_last_shown ON recommendation_impressions(last_shown_at)`, | |
| 349 | + `CREATE INDEX IF NOT EXISTS recs.idx_follow_distances_b ON follow_distances(user_b)`, | |
| 350 | + `CREATE INDEX IF NOT EXISTS recs.idx_follow_distances_a_dist ON follow_distances(user_a, distance)`, | |
| 351 | + `CREATE INDEX IF NOT EXISTS recs.idx_user_similarity_b ON user_similarity(user_b)`, | |
| 352 | + `CREATE INDEX IF NOT EXISTS recs.idx_user_similarity_a ON user_similarity(user_a)`, | |
| 367 | 353 | } |
| @@ -11,9 +11,10 @@ import ( | |||
| 11 | ) | 11 | ) |
| 12 | 12 | ||
| 13 | type Databases struct { | 13 | type Databases struct { |
| 14 | - Users *DB | 14 | + Users *UserStore |
| 15 | - Articles *DB | 15 | + Articles *ArticleStore |
| 16 | - Recs *DB | 16 | + |
| 17 | + db *DB | ||
| 17 | } | 18 | } |
| 18 | 19 | ||
| 19 | var multiDriverSeq int64 | 20 | var multiDriverSeq int64 |
| @@ -22,6 +23,14 @@ func OpenAll(basePath string) (*Databases, error) { | |||
| 22 | articlesPath := basePath + "_articles" | 23 | articlesPath := basePath + "_articles" |
| 23 | recsPath := basePath + "_recs" | 24 | recsPath := basePath + "_recs" |
| 24 | 25 | ||
| 26 | + for _, p := range []string{articlesPath, recsPath} { | ||
| 27 | + f, err := sql.Open("sqlite3", p+"?"+DSN) | ||
| 28 | + if err != nil { | ||
| 29 | + return nil, err | ||
| 30 | + } | ||
| 31 | + f.Close() | ||
| 32 | + } | ||
| 33 | + | ||
| 25 | seq := atomic.AddInt64(&multiDriverSeq, 1) | 34 | seq := atomic.AddInt64(&multiDriverSeq, 1) |
| 26 | driverName := fmt.Sprintf("sqlite3_glean_multi_%d", seq) | 35 | driverName := fmt.Sprintf("sqlite3_glean_multi_%d", seq) |
| 27 | 36 | ||
| @@ -52,69 +61,48 @@ func OpenAll(basePath string) (*Databases, error) { | |||
| 52 | }, | 61 | }, |
| 53 | }) | 62 | }) |
| 54 | 63 | ||
| 55 | - usersDB, err := sql.Open(driverName, basePath+"_users?cache=shared&"+DSN) | 64 | + db, err := sql.Open(driverName, basePath+"_users?cache=shared&"+DSN) |
| 56 | - if err != nil { | ||
| 57 | - return nil, err | ||
| 58 | - } | ||
| 59 | - usersDB.SetMaxOpenConns(10) | ||
| 60 | - usersDB.SetMaxIdleConns(5) | ||
| 61 | - usersDB.SetConnMaxLifetime(30 * time.Minute) | ||
| 62 | - users := &DB{usersDB} | ||
| 63 | - | ||
| 64 | - articles, err := Open(articlesPath) | ||
| 65 | if err != nil { | 65 | if err != nil { |
| 66 | - users.Close() | ||
| 67 | return nil, err | 66 | return nil, err |
| 68 | } | 67 | } |
| 68 | + db.SetMaxOpenConns(10) | ||
| 69 | + db.SetMaxIdleConns(5) | ||
| 70 | + db.SetConnMaxLifetime(30 * time.Minute) | ||
| 71 | + d := &DB{db} | ||
| 69 | 72 | ||
| 70 | - recs, err := Open(recsPath) | 73 | + if err := initUsersSchema(d); err != nil { |
| 71 | - if err != nil { | 74 | + d.Close() |
| 72 | - users.Close() | ||
| 73 | - articles.Close() | ||
| 74 | - return nil, err | ||
| 75 | - } | ||
| 76 | - | ||
| 77 | - if err := initUsersSchema(users); err != nil { | ||
| 78 | - users.Close() | ||
| 79 | - articles.Close() | ||
| 80 | - recs.Close() | ||
| 81 | return nil, err | 75 | return nil, err |
| 82 | } | 76 | } |
| 83 | 77 | ||
| 84 | - if err := initArticlesSchema(articles); err != nil { | 78 | + if err := initArticlesSchema(d); err != nil { |
| 85 | - users.Close() | 79 | + d.Close() |
| 86 | - articles.Close() | ||
| 87 | - recs.Close() | ||
| 88 | return nil, err | 80 | return nil, err |
| 89 | } | 81 | } |
| 90 | 82 | ||
| 91 | - if err := initRecsSchema(recs); err != nil { | 83 | + if err := initRecsSchema(d); err != nil { |
| 92 | - users.Close() | 84 | + d.Close() |
| 93 | - articles.Close() | ||
| 94 | - recs.Close() | ||
| 95 | return nil, err | 85 | return nil, err |
| 96 | } | 86 | } |
| 97 | 87 | ||
| 98 | return &Databases{ | 88 | return &Databases{ |
| 99 | - Users: users, | 89 | + Users: NewUserStore(d), |
| 100 | - Articles: articles, | 90 | + Articles: NewArticleStore(d), |
| 101 | - Recs: recs, | 91 | + db: d, |
| 102 | }, nil | 92 | }, nil |
| 103 | } | 93 | } |
| 104 | 94 | ||
| 105 | func (d *Databases) Close() error { | 95 | func (d *Databases) Close() error { |
| 106 | - if d.Users != nil { | 96 | + if d.db != nil { |
| 107 | - _ = d.Users.Close() | 97 | + _ = d.db.Close() |
| 108 | - } | ||
| 109 | - if d.Articles != nil { | ||
| 110 | - _ = d.Articles.Close() | ||
| 111 | - } | ||
| 112 | - if d.Recs != nil { | ||
| 113 | - _ = d.Recs.Close() | ||
| 114 | } | 98 | } |
| 115 | return nil | 99 | return nil |
| 116 | } | 100 | } |
| 117 | 101 | ||
| 102 | +func (d *Databases) DB() *sql.DB { | ||
| 103 | + return d.db.DB | ||
| 104 | +} | ||
| 105 | + | ||
| 118 | func initUsersSchema(db *DB) error { | 106 | func initUsersSchema(db *DB) error { |
| 119 | for _, s := range usersSchema { | 107 | for _, s := range usersSchema { |
| 120 | if _, err := db.Exec(s); err != nil { | 108 | if _, err := db.Exec(s); err != nil { |
| @@ -181,7 +169,7 @@ var usersSchema = []string{ | |||
| 181 | } | 169 | } |
| 182 | 170 | ||
| 183 | var articlesSchema = []string{ | 171 | var articlesSchema = []string{ |
| 184 | - `CREATE TABLE IF NOT EXISTS feeds ( | 172 | + `CREATE TABLE IF NOT EXISTS articles.feeds ( |
| 185 | feed_url TEXT PRIMARY KEY, | 173 | feed_url TEXT PRIMARY KEY, |
| 186 | title TEXT, | 174 | title TEXT, |
| 187 | site_url TEXT, | 175 | site_url TEXT, |
| @@ -192,14 +180,12 @@ var articlesSchema = []string{ | |||
| 192 | subscriber_count INTEGER NOT NULL DEFAULT 0, | 180 | subscriber_count INTEGER NOT NULL DEFAULT 0, |
| 193 | etag TEXT, | 181 | etag TEXT, |
| 194 | last_modified TEXT, | 182 | last_modified TEXT, |
| 195 | - fetch_interval_minutes INTEGER NOT NULL DEFAULT 30, | ||
| 196 | - next_fetch_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 197 | consecutive_empty_fetches INTEGER NOT NULL DEFAULT 0, | 183 | consecutive_empty_fetches INTEGER NOT NULL DEFAULT 0, |
| 198 | error_count INTEGER NOT NULL DEFAULT 0, | 184 | error_count INTEGER NOT NULL DEFAULT 0, |
| 199 | favicon_url TEXT | 185 | favicon_url TEXT |
| 200 | )`, | 186 | )`, |
| 201 | 187 | ||
| 202 | - `CREATE TABLE IF NOT EXISTS subscriptions ( | 188 | + `CREATE TABLE IF NOT EXISTS articles.subscriptions ( |
| 203 | id INTEGER PRIMARY KEY AUTOINCREMENT, | 189 | id INTEGER PRIMARY KEY AUTOINCREMENT, |
| 204 | user_did TEXT NOT NULL, | 190 | user_did TEXT NOT NULL, |
| 205 | feed_url TEXT NOT NULL, | 191 | feed_url TEXT NOT NULL, |
| @@ -211,7 +197,7 @@ var articlesSchema = []string{ | |||
| 211 | UNIQUE(user_did, feed_url) | 197 | UNIQUE(user_did, feed_url) |
| 212 | )`, | 198 | )`, |
| 213 | 199 | ||
| 214 | - `CREATE TABLE IF NOT EXISTS articles ( | 200 | + `CREATE TABLE IF NOT EXISTS articles.articles ( |
| 215 | id INTEGER PRIMARY KEY AUTOINCREMENT, | 201 | id INTEGER PRIMARY KEY AUTOINCREMENT, |
| 216 | feed_url TEXT NOT NULL, | 202 | feed_url TEXT NOT NULL, |
| 217 | guid TEXT NOT NULL, | 203 | guid TEXT NOT NULL, |
| @@ -227,7 +213,7 @@ var articlesSchema = []string{ | |||
| 227 | UNIQUE(feed_url, guid) | 213 | UNIQUE(feed_url, guid) |
| 228 | )`, | 214 | )`, |
| 229 | 215 | ||
| 230 | - `CREATE TABLE IF NOT EXISTS read_state ( | 216 | + `CREATE TABLE IF NOT EXISTS articles.read_state ( |
| 231 | user_did TEXT NOT NULL, | 217 | user_did TEXT NOT NULL, |
| 232 | article_id INTEGER NOT NULL, | 218 | article_id INTEGER NOT NULL, |
| 233 | is_read BOOLEAN NOT NULL DEFAULT 0, | 219 | is_read BOOLEAN NOT NULL DEFAULT 0, |
| @@ -235,7 +221,7 @@ var articlesSchema = []string{ | |||
| 235 | PRIMARY KEY (user_did, article_id) | 221 | PRIMARY KEY (user_did, article_id) |
| 236 | )`, | 222 | )`, |
| 237 | 223 | ||
| 238 | - `CREATE TABLE IF NOT EXISTS annotations ( | 224 | + `CREATE TABLE IF NOT EXISTS articles.annotations ( |
| 239 | id INTEGER PRIMARY KEY AUTOINCREMENT, | 225 | id INTEGER PRIMARY KEY AUTOINCREMENT, |
| 240 | uri TEXT NOT NULL UNIQUE, | 226 | uri TEXT NOT NULL UNIQUE, |
| 241 | author_did TEXT NOT NULL, | 227 | author_did TEXT NOT NULL, |
| @@ -249,7 +235,7 @@ var articlesSchema = []string{ | |||
| 249 | cid TEXT | 235 | cid TEXT |
| 250 | )`, | 236 | )`, |
| 251 | 237 | ||
| 252 | - `CREATE TABLE IF NOT EXISTS likes ( | 238 | + `CREATE TABLE IF NOT EXISTS articles.likes ( |
| 253 | id INTEGER PRIMARY KEY AUTOINCREMENT, | 239 | id INTEGER PRIMARY KEY AUTOINCREMENT, |
| 254 | uri TEXT NOT NULL UNIQUE, | 240 | uri TEXT NOT NULL UNIQUE, |
| 255 | author_did TEXT NOT NULL, | 241 | author_did TEXT NOT NULL, |
| @@ -260,37 +246,37 @@ var articlesSchema = []string{ | |||
| 260 | UNIQUE(author_did, feed_url, article_url) | 246 | UNIQUE(author_did, feed_url, article_url) |
| 261 | )`, | 247 | )`, |
| 262 | 248 | ||
| 263 | - `CREATE INDEX IF NOT EXISTS idx_subscriptions_feed ON subscriptions(feed_url)`, | 249 | + `CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_feed ON subscriptions(feed_url)`, |
| 264 | - `CREATE INDEX IF NOT EXISTS idx_subscriptions_feed_user ON subscriptions(feed_url, user_did)`, | 250 | + `CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_feed_user ON subscriptions(feed_url, user_did)`, |
| 265 | - `CREATE INDEX IF NOT EXISTS idx_subscriptions_user ON subscriptions(user_did)`, | 251 | + `CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_user ON subscriptions(user_did)`, |
| 266 | - `CREATE INDEX IF NOT EXISTS idx_subscriptions_uri ON subscriptions(uri)`, | 252 | + `CREATE INDEX IF NOT EXISTS articles.idx_subscriptions_uri ON subscriptions(uri)`, |
| 267 | - `CREATE INDEX IF NOT EXISTS idx_likes_author_feed ON likes(author_did, feed_url, created_at)`, | 253 | + `CREATE INDEX IF NOT EXISTS articles.idx_likes_author_feed ON likes(author_did, feed_url, created_at)`, |
| 268 | - `CREATE INDEX IF NOT EXISTS idx_articles_feed ON articles(feed_url)`, | 254 | + `CREATE INDEX IF NOT EXISTS articles.idx_articles_feed ON articles(feed_url)`, |
| 269 | - `CREATE INDEX IF NOT EXISTS idx_articles_published ON articles(published DESC)`, | 255 | + `CREATE INDEX IF NOT EXISTS articles.idx_articles_published ON articles(published DESC)`, |
| 270 | - `CREATE INDEX IF NOT EXISTS idx_articles_url ON articles(url)`, | 256 | + `CREATE INDEX IF NOT EXISTS articles.idx_articles_url ON articles(url)`, |
| 271 | - `CREATE INDEX IF NOT EXISTS idx_read_state_unread ON read_state(user_did, is_read) WHERE is_read = 0`, | 257 | + `CREATE INDEX IF NOT EXISTS articles.idx_read_state_unread ON read_state(user_did, is_read) WHERE is_read = 0`, |
| 272 | - `CREATE INDEX IF NOT EXISTS idx_annotations_article ON annotations(article_url)`, | 258 | + `CREATE INDEX IF NOT EXISTS articles.idx_annotations_article ON annotations(article_url)`, |
| 273 | - `CREATE INDEX IF NOT EXISTS idx_annotations_author ON annotations(author_did)`, | 259 | + `CREATE INDEX IF NOT EXISTS articles.idx_annotations_author ON annotations(author_did)`, |
| 274 | - `CREATE INDEX IF NOT EXISTS idx_annotations_created_at ON annotations(created_at DESC)`, | 260 | + `CREATE INDEX IF NOT EXISTS articles.idx_annotations_created_at ON annotations(created_at DESC)`, |
| 275 | - `CREATE INDEX IF NOT EXISTS idx_likes_article ON likes(feed_url, article_url)`, | 261 | + `CREATE INDEX IF NOT EXISTS articles.idx_likes_article ON likes(feed_url, article_url)`, |
| 276 | - `CREATE INDEX IF NOT EXISTS idx_likes_author ON likes(author_did)`, | 262 | + `CREATE INDEX IF NOT EXISTS articles.idx_likes_author ON likes(author_did)`, |
| 277 | - `CREATE INDEX IF NOT EXISTS idx_likes_created_at ON likes(created_at DESC)`, | 263 | + `CREATE INDEX IF NOT EXISTS articles.idx_likes_created_at ON likes(created_at DESC)`, |
| 278 | - | 264 | + |
| 279 | - `CREATE VIRTUAL TABLE IF NOT EXISTS articles_fts USING fts5(title, summary, content, author, content=articles, content_rowid=id)`, | 265 | + `CREATE VIRTUAL TABLE IF NOT EXISTS articles.articles_fts USING fts5(title, summary, content, author, content=articles, content_rowid=id)`, |
| 280 | - `CREATE TRIGGER IF NOT EXISTS articles_ai AFTER INSERT ON articles BEGIN | 266 | + `CREATE TRIGGER IF NOT EXISTS articles.articles_ai AFTER INSERT ON articles BEGIN |
| 281 | INSERT INTO articles_fts(rowid, title, summary, content, author) VALUES (new.id, new.title, new.summary, new.content, new.author); | 267 | INSERT INTO articles_fts(rowid, title, summary, content, author) VALUES (new.id, new.title, new.summary, new.content, new.author); |
| 282 | END`, | 268 | END`, |
| 283 | - `CREATE TRIGGER IF NOT EXISTS articles_ad AFTER DELETE ON articles BEGIN | 269 | + `CREATE TRIGGER IF NOT EXISTS articles.articles_ad AFTER DELETE ON articles BEGIN |
| 284 | INSERT INTO articles_fts(articles_fts, rowid, title, summary, content, author) VALUES('delete', old.id, old.title, old.summary, old.content, old.author); | 270 | INSERT INTO articles_fts(articles_fts, rowid, title, summary, content, author) VALUES('delete', old.id, old.title, old.summary, old.content, old.author); |
| 285 | END`, | 271 | END`, |
| 286 | - `CREATE TRIGGER IF NOT EXISTS articles_au AFTER UPDATE ON articles BEGIN | 272 | + `CREATE TRIGGER IF NOT EXISTS articles.articles_au AFTER UPDATE ON articles BEGIN |
| 287 | INSERT INTO articles_fts(articles_fts, rowid, title, summary, content, author) VALUES('delete', old.id, old.title, old.summary, old.content, old.author); | 273 | INSERT INTO articles_fts(articles_fts, rowid, title, summary, content, author) VALUES('delete', old.id, old.title, old.summary, old.content, old.author); |
| 288 | INSERT INTO articles_fts(rowid, title, summary, content, author) VALUES (new.id, new.title, new.summary, new.content, new.author); | 274 | INSERT INTO articles_fts(rowid, title, summary, content, author) VALUES (new.id, new.title, new.summary, new.content, new.author); |
| 289 | END`, | 275 | END`, |
| 290 | } | 276 | } |
| 291 | 277 | ||
| 292 | var recsSchema = []string{ | 278 | var recsSchema = []string{ |
| 293 | - `CREATE TABLE IF NOT EXISTS feed_similarity ( | 279 | + `CREATE TABLE IF NOT EXISTS recs.feed_similarity ( |
| 294 | feed_a TEXT NOT NULL, | 280 | feed_a TEXT NOT NULL, |
| 295 | feed_b TEXT NOT NULL, | 281 | feed_b TEXT NOT NULL, |
| 296 | jaccard REAL NOT NULL, | 282 | jaccard REAL NOT NULL, |
| @@ -299,7 +285,7 @@ var recsSchema = []string{ | |||
| 299 | CHECK(feed_a < feed_b) | 285 | CHECK(feed_a < feed_b) |
| 300 | )`, | 286 | )`, |
| 301 | 287 | ||
| 302 | - `CREATE TABLE IF NOT EXISTS user_similarity ( | 288 | + `CREATE TABLE IF NOT EXISTS recs.user_similarity ( |
| 303 | user_a TEXT NOT NULL, | 289 | user_a TEXT NOT NULL, |
| 304 | user_b TEXT NOT NULL, | 290 | user_b TEXT NOT NULL, |
| 305 | jaccard REAL NOT NULL, | 291 | jaccard REAL NOT NULL, |
| @@ -311,7 +297,7 @@ var recsSchema = []string{ | |||
| 311 | CHECK(user_a < user_b) | 297 | CHECK(user_a < user_b) |
| 312 | )`, | 298 | )`, |
| 313 | 299 | ||
| 314 | - `CREATE TABLE IF NOT EXISTS dismissed_recommendations ( | 300 | + `CREATE TABLE IF NOT EXISTS recs.dismissed_recommendations ( |
| 315 | user_did TEXT NOT NULL, | 301 | user_did TEXT NOT NULL, |
| 316 | target_type TEXT NOT NULL CHECK(target_type IN ('feed', 'article')), | 302 | target_type TEXT NOT NULL CHECK(target_type IN ('feed', 'article')), |
| 317 | target_id TEXT NOT NULL, | 303 | target_id TEXT NOT NULL, |
| @@ -320,7 +306,7 @@ var recsSchema = []string{ | |||
| 320 | PRIMARY KEY (user_did, target_type, target_id) | 306 | PRIMARY KEY (user_did, target_type, target_id) |
| 321 | )`, | 307 | )`, |
| 322 | 308 | ||
| 323 | - `CREATE TABLE IF NOT EXISTS recommendation_impressions ( | 309 | + `CREATE TABLE IF NOT EXISTS recs.recommendation_impressions ( |
| 324 | user_did TEXT NOT NULL, | 310 | user_did TEXT NOT NULL, |
| 325 | target_type TEXT NOT NULL CHECK(target_type IN ('feed', 'article')), | 311 | target_type TEXT NOT NULL CHECK(target_type IN ('feed', 'article')), |
| 326 | target_id TEXT NOT NULL, | 312 | target_id TEXT NOT NULL, |
| @@ -331,14 +317,14 @@ var recsSchema = []string{ | |||
| 331 | PRIMARY KEY (user_did, target_type, target_id) | 317 | PRIMARY KEY (user_did, target_type, target_id) |
| 332 | )`, | 318 | )`, |
| 333 | 319 | ||
| 334 | - `CREATE TABLE IF NOT EXISTS follow_distances ( | 320 | + `CREATE TABLE IF NOT EXISTS recs.follow_distances ( |
| 335 | user_a TEXT NOT NULL, | 321 | user_a TEXT NOT NULL, |
| 336 | user_b TEXT NOT NULL, | 322 | user_b TEXT NOT NULL, |
| 337 | distance INTEGER NOT NULL CHECK(distance IN (1, 2)), | 323 | distance INTEGER NOT NULL CHECK(distance IN (1, 2)), |
| 338 | PRIMARY KEY (user_a, user_b) | 324 | PRIMARY KEY (user_a, user_b) |
| 339 | )`, | 325 | )`, |
| 340 | 326 | ||
| 341 | - `CREATE TABLE IF NOT EXISTS user_signal_weights ( | 327 | + `CREATE TABLE IF NOT EXISTS recs.user_signal_weights ( |
| 342 | user_did TEXT PRIMARY KEY, | 328 | user_did TEXT PRIMARY KEY, |
| 343 | w_sub REAL NOT NULL DEFAULT 1.0, | 329 | w_sub REAL NOT NULL DEFAULT 1.0, |
| 344 | w_like REAL NOT NULL DEFAULT 0.5, | 330 | w_like REAL NOT NULL DEFAULT 0.5, |
| @@ -349,7 +335,7 @@ var recsSchema = []string{ | |||
| 349 | updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | 335 | updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP |
| 350 | )`, | 336 | )`, |
| 351 | 337 | ||
| 352 | - `CREATE TABLE IF NOT EXISTS user_signal_profiles ( | 338 | + `CREATE TABLE IF NOT EXISTS recs.user_signal_profiles ( |
| 353 | user_did TEXT PRIMARY KEY, | 339 | user_did TEXT PRIMARY KEY, |
| 354 | total_likes INTEGER NOT NULL DEFAULT 0, | 340 | total_likes INTEGER NOT NULL DEFAULT 0, |
| 355 | total_tags INTEGER NOT NULL DEFAULT 0, | 341 | total_tags INTEGER NOT NULL DEFAULT 0, |
| @@ -357,11 +343,11 @@ var recsSchema = []string{ | |||
| 357 | updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | 343 | updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP |
| 358 | )`, | 344 | )`, |
| 359 | 345 | ||
| 360 | - `CREATE INDEX IF NOT EXISTS idx_dismissed_user_type ON dismissed_recommendations(user_did, target_type)`, | 346 | + `CREATE INDEX IF NOT EXISTS recs.idx_dismissed_user_type ON dismissed_recommendations(user_did, target_type)`, |
| 361 | - `CREATE INDEX IF NOT EXISTS idx_impressions_user_unacted ON recommendation_impressions(user_did, acted, shown_count)`, | 347 | + `CREATE INDEX IF NOT EXISTS recs.idx_impressions_user_unacted ON recommendation_impressions(user_did, acted, shown_count)`, |
| 362 | - `CREATE INDEX IF NOT EXISTS idx_impressions_last_shown ON recommendation_impressions(last_shown_at)`, | 348 | + `CREATE INDEX IF NOT EXISTS recs.idx_impressions_last_shown ON recommendation_impressions(last_shown_at)`, |
| 363 | - `CREATE INDEX IF NOT EXISTS idx_follow_distances_b ON follow_distances(user_b)`, | 349 | + `CREATE INDEX IF NOT EXISTS recs.idx_follow_distances_b ON follow_distances(user_b)`, |
| 364 | - `CREATE INDEX IF NOT EXISTS idx_follow_distances_a_dist ON follow_distances(user_a, distance)`, | 350 | + `CREATE INDEX IF NOT EXISTS recs.idx_follow_distances_a_dist ON follow_distances(user_a, distance)`, |
| 365 | - `CREATE INDEX IF NOT EXISTS idx_user_similarity_b ON user_similarity(user_b)`, | 351 | + `CREATE INDEX IF NOT EXISTS recs.idx_user_similarity_b ON user_similarity(user_b)`, |
| 366 | - `CREATE INDEX IF NOT EXISTS idx_user_similarity_a ON user_similarity(user_a)`, | 352 | + `CREATE INDEX IF NOT EXISTS recs.idx_user_similarity_a ON user_similarity(user_a)`, |
| 367 | } | 353 | } |
modified
internal/db/oauth_store.go +2 -2 | @@ -14,8 +14,8 @@ type OAuthStore struct { | ||
| 14 | 14 | db *DB |
| 15 | 15 | } |
| 16 | 16 | |
| 17 | -func NewOAuthStore(db *DB) *OAuthStore { | |
| 18 | - return &OAuthStore{db: db} | |
| 17 | +func NewOAuthStore(dbs *Databases) *OAuthStore { | |
| 18 | + return &OAuthStore{db: dbs.db} | |
| 19 | 19 | } |
| 20 | 20 | |
| 21 | 21 | func (s *OAuthStore) GetSession(ctx context.Context, did syntax.DID, sessionID string) (*oauth.ClientSessionData, error) { |
| @@ -14,8 +14,8 @@ type OAuthStore struct { | |||
| 14 | db *DB | 14 | db *DB |
| 15 | } | 15 | } |
| 16 | 16 | ||
| 17 | -func NewOAuthStore(db *DB) *OAuthStore { | 17 | +func NewOAuthStore(dbs *Databases) *OAuthStore { |
| 18 | - return &OAuthStore{db: db} | 18 | + return &OAuthStore{db: dbs.db} |
| 19 | } | 19 | } |
| 20 | 20 | ||
| 21 | func (s *OAuthStore) GetSession(ctx context.Context, did syntax.DID, sessionID string) (*oauth.ClientSessionData, error) { | 21 | func (s *OAuthStore) GetSession(ctx context.Context, did syntax.DID, sessionID string) (*oauth.ClientSessionData, error) { |
modified
internal/db/social.go +60 -60 | @@ -35,17 +35,17 @@ type Like struct { | ||
| 35 | 35 | CID sql.NullString |
| 36 | 36 | } |
| 37 | 37 | |
| 38 | -func (db *DB) CreateAnnotation(ctx context.Context, a *Annotation) error { | |
| 39 | - return db.BatchCreateAnnotations(ctx, []*Annotation{a}) | |
| 38 | +func (s *ArticleStore) CreateAnnotation(ctx context.Context, a *Annotation) error { | |
| 39 | + return s.BatchCreateAnnotations(ctx, []*Annotation{a}) | |
| 40 | 40 | } |
| 41 | 41 | |
| 42 | -func (db *DB) GetAnnotation(ctx context.Context, id int64) (*Annotation, error) { | |
| 42 | +func (s *ArticleStore) GetAnnotation(ctx context.Context, id int64) (*Annotation, error) { | |
| 43 | 43 | a := &Annotation{} |
| 44 | - err := db.QueryRowContext(ctx, ` | |
| 44 | + err := s.db.QueryRowContext(ctx, ` | |
| 45 | 45 | SELECT a.id, a.uri, a.author_did, COALESCE(u.handle, ''), a.feed_url, a.article_url, ar.id, a.quote, a.note, a.tags, a.rating, a.created_at, a.cid |
| 46 | - FROM annotations a | |
| 46 | + FROM articles.annotations a | |
| 47 | 47 | LEFT JOIN users u ON a.author_did = u.did |
| 48 | - LEFT JOIN articles ar ON ar.url = a.article_url AND ar.feed_url = a.feed_url | |
| 48 | + LEFT JOIN articles.articles ar ON ar.url = a.article_url AND ar.feed_url = a.feed_url | |
| 49 | 49 | WHERE a.id = ? |
| 50 | 50 | `, id).Scan(&a.ID, &a.URI, &a.AuthorDID, &a.AuthorHandle, &a.FeedURL, &a.ArticleURL, &a.ArticleID, |
| 51 | 51 | &a.Quote, &a.Note, &a.Tags, &a.Rating, &a.CreatedAt, &a.CID) |
| @@ -55,14 +55,14 @@ func (db *DB) GetAnnotation(ctx context.Context, id int64) (*Annotation, error) | ||
| 55 | 55 | return a, nil |
| 56 | 56 | } |
| 57 | 57 | |
| 58 | -func (db *DB) DeleteAnnotation(ctx context.Context, uri string) error { | |
| 59 | - _, err := db.ExecContext(ctx, `DELETE FROM annotations WHERE uri = ?`, uri) | |
| 58 | +func (s *ArticleStore) DeleteAnnotation(ctx context.Context, uri string) error { | |
| 59 | + _, err := s.db.ExecContext(ctx, `DELETE FROM articles.annotations WHERE uri = ?`, uri) | |
| 60 | 60 | return err |
| 61 | 61 | } |
| 62 | 62 | |
| 63 | -func (db *DB) AnnotationExists(ctx context.Context, uri string) (bool, error) { | |
| 63 | +func (s *ArticleStore) AnnotationExists(ctx context.Context, uri string) (bool, error) { | |
| 64 | 64 | var exists int |
| 65 | - err := db.QueryRowContext(ctx, `SELECT 1 FROM annotations WHERE uri = ?`, uri).Scan(&exists) | |
| 65 | + err := s.db.QueryRowContext(ctx, `SELECT 1 FROM articles.annotations WHERE uri = ?`, uri).Scan(&exists) | |
| 66 | 66 | if err == sql.ErrNoRows { |
| 67 | 67 | return false, nil |
| 68 | 68 | } |
| @@ -72,7 +72,7 @@ func (db *DB) AnnotationExists(ctx context.Context, uri string) (bool, error) { | ||
| 72 | 72 | return true, nil |
| 73 | 73 | } |
| 74 | 74 | |
| 75 | -func (db *DB) ListAnnotations(ctx context.Context, feedURL, articleURL, authorDID string, limit, offset int) ([]*Annotation, error) { | |
| 75 | +func (s *ArticleStore) ListAnnotations(ctx context.Context, feedURL, articleURL, authorDID string, limit, offset int) ([]*Annotation, error) { | |
| 76 | 76 | var conds []string |
| 77 | 77 | var args []any |
| 78 | 78 | |
| @@ -90,16 +90,16 @@ func (db *DB) ListAnnotations(ctx context.Context, feedURL, articleURL, authorDI | ||
| 90 | 90 | } |
| 91 | 91 | |
| 92 | 92 | query := `SELECT a.id, a.uri, a.author_did, COALESCE(u.handle, ''), a.feed_url, a.article_url, ar.id, a.quote, a.note, a.tags, a.rating, a.created_at, a.cid |
| 93 | - FROM annotations a | |
| 93 | + FROM articles.annotations a | |
| 94 | 94 | LEFT JOIN users u ON a.author_did = u.did |
| 95 | - LEFT JOIN articles ar ON ar.url = a.article_url AND ar.feed_url = a.feed_url` | |
| 95 | + LEFT JOIN articles.articles ar ON ar.url = a.article_url AND ar.feed_url = a.feed_url` | |
| 96 | 96 | if len(conds) > 0 { |
| 97 | 97 | query += ` WHERE ` + strings.Join(conds, " AND ") |
| 98 | 98 | } |
| 99 | 99 | query += ` ORDER BY a.created_at DESC LIMIT ? OFFSET ?` |
| 100 | 100 | args = append(args, limit, offset) |
| 101 | 101 | |
| 102 | - rows, err := db.QueryContext(ctx, query, args...) | |
| 102 | + rows, err := s.db.QueryContext(ctx, query, args...) | |
| 103 | 103 | if err != nil { |
| 104 | 104 | return nil, err |
| 105 | 105 | } |
| @@ -117,18 +117,18 @@ func (db *DB) ListAnnotations(ctx context.Context, feedURL, articleURL, authorDI | ||
| 117 | 117 | return annotations, rows.Err() |
| 118 | 118 | } |
| 119 | 119 | |
| 120 | -func (db *DB) BatchCreateLikes(ctx context.Context, likes []*Like) error { | |
| 120 | +func (s *ArticleStore) BatchCreateLikes(ctx context.Context, likes []*Like) error { | |
| 121 | 121 | if len(likes) == 0 { |
| 122 | 122 | return nil |
| 123 | 123 | } |
| 124 | - tx, err := db.BeginTx(ctx, nil) | |
| 124 | + tx, err := s.db.BeginTx(ctx, nil) | |
| 125 | 125 | if err != nil { |
| 126 | 126 | return err |
| 127 | 127 | } |
| 128 | 128 | defer tx.Rollback() |
| 129 | 129 | |
| 130 | 130 | stmt, err := tx.PrepareContext(ctx, ` |
| 131 | - INSERT OR IGNORE INTO likes (uri, author_did, feed_url, article_url, created_at, cid) | |
| 131 | + INSERT OR IGNORE INTO articles.likes (uri, author_did, feed_url, article_url, created_at, cid) | |
| 132 | 132 | VALUES (?, ?, ?, ?, ?, ?) |
| 133 | 133 | `) |
| 134 | 134 | if err != nil { |
| @@ -144,18 +144,18 @@ func (db *DB) BatchCreateLikes(ctx context.Context, likes []*Like) error { | ||
| 144 | 144 | return tx.Commit() |
| 145 | 145 | } |
| 146 | 146 | |
| 147 | -func (db *DB) BatchCreateAnnotations(ctx context.Context, annotations []*Annotation) error { | |
| 147 | +func (s *ArticleStore) BatchCreateAnnotations(ctx context.Context, annotations []*Annotation) error { | |
| 148 | 148 | if len(annotations) == 0 { |
| 149 | 149 | return nil |
| 150 | 150 | } |
| 151 | - tx, err := db.BeginTx(ctx, nil) | |
| 151 | + tx, err := s.db.BeginTx(ctx, nil) | |
| 152 | 152 | if err != nil { |
| 153 | 153 | return err |
| 154 | 154 | } |
| 155 | 155 | defer tx.Rollback() |
| 156 | 156 | |
| 157 | 157 | stmt, err := tx.PrepareContext(ctx, ` |
| 158 | - INSERT OR IGNORE INTO annotations (uri, author_did, feed_url, article_url, quote, note, tags, rating, created_at, cid) | |
| 158 | + INSERT OR IGNORE INTO articles.annotations (uri, author_did, feed_url, article_url, quote, note, tags, rating, created_at, cid) | |
| 159 | 159 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
| 160 | 160 | `) |
| 161 | 161 | if err != nil { |
| @@ -171,30 +171,30 @@ func (db *DB) BatchCreateAnnotations(ctx context.Context, annotations []*Annotat | ||
| 171 | 171 | return tx.Commit() |
| 172 | 172 | } |
| 173 | 173 | |
| 174 | -func (db *DB) CreateLike(ctx context.Context, l *Like) error { | |
| 175 | - exists, err := db.HasLiked(ctx, l.AuthorDID, l.FeedURL, l.ArticleURL) | |
| 174 | +func (s *ArticleStore) CreateLike(ctx context.Context, l *Like) error { | |
| 175 | + exists, err := s.HasLiked(ctx, l.AuthorDID, l.FeedURL, l.ArticleURL) | |
| 176 | 176 | if err != nil { |
| 177 | 177 | return err |
| 178 | 178 | } |
| 179 | 179 | if exists { |
| 180 | 180 | return ErrDuplicateLike |
| 181 | 181 | } |
| 182 | - return db.BatchCreateLikes(ctx, []*Like{l}) | |
| 182 | + return s.BatchCreateLikes(ctx, []*Like{l}) | |
| 183 | 183 | } |
| 184 | 184 | |
| 185 | -func (db *DB) DeleteLike(ctx context.Context, uri string) error { | |
| 186 | - _, err := db.ExecContext(ctx, `DELETE FROM likes WHERE uri = ?`, uri) | |
| 185 | +func (s *ArticleStore) DeleteLike(ctx context.Context, uri string) error { | |
| 186 | + _, err := s.db.ExecContext(ctx, `DELETE FROM articles.likes WHERE uri = ?`, uri) | |
| 187 | 187 | return err |
| 188 | 188 | } |
| 189 | 189 | |
| 190 | -func (db *DB) DeleteLikeByUserArticle(ctx context.Context, authorDID, feedURL, articleURL string) error { | |
| 191 | - _, err := db.ExecContext(ctx, ` | |
| 192 | - DELETE FROM likes WHERE author_did = ? AND feed_url = ? AND article_url = ? | |
| 190 | +func (s *ArticleStore) DeleteLikeByUserArticle(ctx context.Context, authorDID, feedURL, articleURL string) error { | |
| 191 | + _, err := s.db.ExecContext(ctx, ` | |
| 192 | + DELETE FROM articles.likes WHERE author_did = ? AND feed_url = ? AND article_url = ? | |
| 193 | 193 | `, authorDID, feedURL, articleURL) |
| 194 | 194 | return err |
| 195 | 195 | } |
| 196 | 196 | |
| 197 | -func (db *DB) ListLikes(ctx context.Context, authorDID, feedURL string, limit, offset int) ([]*Like, error) { | |
| 197 | +func (s *ArticleStore) ListLikes(ctx context.Context, authorDID, feedURL string, limit, offset int) ([]*Like, error) { | |
| 198 | 198 | var conds []string |
| 199 | 199 | var args []any |
| 200 | 200 | |
| @@ -207,14 +207,14 @@ func (db *DB) ListLikes(ctx context.Context, authorDID, feedURL string, limit, o | ||
| 207 | 207 | args = append(args, feedURL) |
| 208 | 208 | } |
| 209 | 209 | |
| 210 | - query := `SELECT id, uri, author_did, feed_url, article_url, created_at, cid FROM likes` | |
| 210 | + query := `SELECT id, uri, author_did, feed_url, article_url, created_at, cid FROM articles.likes` | |
| 211 | 211 | if len(conds) > 0 { |
| 212 | 212 | query += ` WHERE ` + strings.Join(conds, " AND ") |
| 213 | 213 | } |
| 214 | 214 | query += ` ORDER BY created_at DESC LIMIT ? OFFSET ?` |
| 215 | 215 | args = append(args, limit, offset) |
| 216 | 216 | |
| 217 | - rows, err := db.QueryContext(ctx, query, args...) | |
| 217 | + rows, err := s.db.QueryContext(ctx, query, args...) | |
| 218 | 218 | if err != nil { |
| 219 | 219 | return nil, err |
| 220 | 220 | } |
| @@ -231,18 +231,18 @@ func (db *DB) ListLikes(ctx context.Context, authorDID, feedURL string, limit, o | ||
| 231 | 231 | return likes, rows.Err() |
| 232 | 232 | } |
| 233 | 233 | |
| 234 | -func (db *DB) GetLikeCount(ctx context.Context, feedURL, articleURL string) (int, error) { | |
| 234 | +func (s *ArticleStore) GetLikeCount(ctx context.Context, feedURL, articleURL string) (int, error) { | |
| 235 | 235 | var count int |
| 236 | - err := db.QueryRowContext(ctx, ` | |
| 237 | - SELECT COUNT(*) FROM likes WHERE feed_url = ? AND article_url = ? | |
| 236 | + err := s.db.QueryRowContext(ctx, ` | |
| 237 | + SELECT COUNT(*) FROM articles.likes WHERE feed_url = ? AND article_url = ? | |
| 238 | 238 | `, feedURL, articleURL).Scan(&count) |
| 239 | 239 | return count, err |
| 240 | 240 | } |
| 241 | 241 | |
| 242 | -func (db *DB) GetLike(ctx context.Context, authorDID, feedURL, articleURL string) (*Like, error) { | |
| 242 | +func (s *ArticleStore) GetLike(ctx context.Context, authorDID, feedURL, articleURL string) (*Like, error) { | |
| 243 | 243 | l := &Like{} |
| 244 | - err := db.QueryRowContext(ctx, ` | |
| 245 | - SELECT id, uri, author_did, feed_url, article_url, created_at, cid FROM likes | |
| 244 | + err := s.db.QueryRowContext(ctx, ` | |
| 245 | + SELECT id, uri, author_did, feed_url, article_url, created_at, cid FROM articles.likes | |
| 246 | 246 | WHERE author_did = ? AND feed_url = ? AND article_url = ? |
| 247 | 247 | `, authorDID, feedURL, articleURL).Scan(&l.ID, &l.URI, &l.AuthorDID, &l.FeedURL, &l.ArticleURL, &l.CreatedAt, &l.CID) |
| 248 | 248 | if err != nil { |
| @@ -251,10 +251,10 @@ func (db *DB) GetLike(ctx context.Context, authorDID, feedURL, articleURL string | ||
| 251 | 251 | return l, nil |
| 252 | 252 | } |
| 253 | 253 | |
| 254 | -func (db *DB) HasLiked(ctx context.Context, authorDID, feedURL, articleURL string) (bool, error) { | |
| 254 | +func (s *ArticleStore) HasLiked(ctx context.Context, authorDID, feedURL, articleURL string) (bool, error) { | |
| 255 | 255 | var exists int |
| 256 | - err := db.QueryRowContext(ctx, ` | |
| 257 | - SELECT 1 FROM likes WHERE author_did = ? AND feed_url = ? AND article_url = ? | |
| 256 | + err := s.db.QueryRowContext(ctx, ` | |
| 257 | + SELECT 1 FROM articles.likes WHERE author_did = ? AND feed_url = ? AND article_url = ? | |
| 258 | 258 | `, authorDID, feedURL, articleURL).Scan(&exists) |
| 259 | 259 | if err == sql.ErrNoRows { |
| 260 | 260 | return false, nil |
| @@ -279,19 +279,19 @@ type TrendingItem struct { | ||
| 279 | 279 | HasLiked bool |
| 280 | 280 | } |
| 281 | 281 | |
| 282 | -func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since string, limit, offset int) ([]*TrendingItem, error) { | |
| 283 | - rows, err := db.QueryContext(ctx, ` | |
| 282 | +func (s *ArticleStore) ListTrendingArticlesForUser(ctx context.Context, userDID, since string, limit, offset int) ([]*TrendingItem, error) { | |
| 283 | + rows, err := s.db.QueryContext(ctx, ` | |
| 284 | 284 | SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''), |
| 285 | 285 | COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''), |
| 286 | 286 | COALESCE(f.favicon_url, ''), |
| 287 | 287 | COUNT(DISTINCT l.id) AS like_count, |
| 288 | 288 | COUNT(DISTINCT a.id) AS annotation_count, |
| 289 | 289 | COALESCE(MAX(CASE WHEN ul.id IS NOT NULL THEN 1 ELSE 0 END), 0) |
| 290 | - FROM likes l | |
| 291 | - JOIN articles ar ON ar.url = l.article_url AND ar.feed_url = l.feed_url | |
| 292 | - LEFT JOIN feeds f ON f.feed_url = l.feed_url | |
| 293 | - LEFT JOIN annotations a ON a.feed_url = l.feed_url AND a.article_url = l.article_url AND a.created_at >= ? | |
| 294 | - LEFT JOIN likes ul ON ul.feed_url = l.feed_url AND ul.article_url = l.article_url AND ul.author_did = ? | |
| 290 | + FROM articles.likes l | |
| 291 | + JOIN articles.articles ar ON ar.url = l.article_url AND ar.feed_url = l.feed_url | |
| 292 | + LEFT JOIN articles.feeds f ON f.feed_url = l.feed_url | |
| 293 | + LEFT JOIN articles.annotations a ON a.feed_url = l.feed_url AND a.article_url = l.article_url AND a.created_at >= ? | |
| 294 | + LEFT JOIN articles.likes ul ON ul.feed_url = l.feed_url AND ul.article_url = l.article_url AND ul.author_did = ? | |
| 295 | 295 | WHERE l.created_at >= ? |
| 296 | 296 | AND l.author_did IN ( |
| 297 | 297 | SELECT CASE WHEN us.user_a = ? THEN us.user_b ELSE us.user_a END |
| @@ -323,19 +323,19 @@ func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since st | ||
| 323 | 323 | return results, rows.Err() |
| 324 | 324 | } |
| 325 | 325 | |
| 326 | -func (db *DB) ListTrendingArticles(ctx context.Context, userDID, since string, limit, offset int) ([]*TrendingItem, error) { | |
| 327 | - rows, err := db.QueryContext(ctx, ` | |
| 326 | +func (s *ArticleStore) ListTrendingArticles(ctx context.Context, userDID, since string, limit, offset int) ([]*TrendingItem, error) { | |
| 327 | + rows, err := s.db.QueryContext(ctx, ` | |
| 328 | 328 | SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''), |
| 329 | 329 | COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''), |
| 330 | 330 | COALESCE(f.favicon_url, ''), |
| 331 | 331 | COUNT(DISTINCT l.id) AS like_count, |
| 332 | 332 | COUNT(DISTINCT a.id) AS annotation_count, |
| 333 | 333 | COALESCE(MAX(CASE WHEN ul.id IS NOT NULL THEN 1 ELSE 0 END), 0) |
| 334 | - FROM likes l | |
| 335 | - JOIN articles ar ON ar.url = l.article_url AND ar.feed_url = l.feed_url | |
| 336 | - LEFT JOIN feeds f ON f.feed_url = l.feed_url | |
| 337 | - LEFT JOIN annotations a ON a.feed_url = l.feed_url AND a.article_url = l.article_url AND a.created_at >= ? | |
| 338 | - LEFT JOIN likes ul ON ul.feed_url = l.feed_url AND ul.article_url = l.article_url AND ul.author_did = ? | |
| 334 | + FROM articles.likes l | |
| 335 | + JOIN articles.articles ar ON ar.url = l.article_url AND ar.feed_url = l.feed_url | |
| 336 | + LEFT JOIN articles.feeds f ON f.feed_url = l.feed_url | |
| 337 | + LEFT JOIN articles.annotations a ON a.feed_url = l.feed_url AND a.article_url = l.article_url AND a.created_at >= ? | |
| 338 | + LEFT JOIN articles.likes ul ON ul.feed_url = l.feed_url AND ul.article_url = l.article_url AND ul.author_did = ? | |
| 339 | 339 | WHERE l.created_at >= ? |
| 340 | 340 | GROUP BY ar.id |
| 341 | 341 | -- Future-published articles (e.g., scheduled) sort last |
| @@ -360,19 +360,19 @@ func (db *DB) ListTrendingArticles(ctx context.Context, userDID, since string, l | ||
| 360 | 360 | return results, rows.Err() |
| 361 | 361 | } |
| 362 | 362 | |
| 363 | -func (db *DB) ListLikedArticles(ctx context.Context, userDID string, limit, offset int) ([]*Article, error) { | |
| 364 | - rows, err := db.QueryContext(ctx, ` | |
| 363 | +func (s *ArticleStore) ListLikedArticles(ctx context.Context, userDID string, limit, offset int) ([]*Article, error) { | |
| 364 | + rows, err := s.db.QueryContext(ctx, ` | |
| 365 | 365 | SELECT DISTINCT a.id, a.feed_url, a.guid, a.title, a.url, a.author, a.summary, a.content, |
| 366 | 366 | a.published, a.updated, a.fetched_at, |
| 367 | 367 | COALESCE(f.title, ''), |
| 368 | 368 | COALESCE(r.is_read, 0), |
| 369 | 369 | COALESCE(lc.cnt, 0), |
| 370 | 370 | 1 |
| 371 | - FROM likes l | |
| 372 | - JOIN articles a ON a.url = l.article_url AND a.feed_url = l.feed_url | |
| 373 | - LEFT JOIN feeds f ON f.feed_url = a.feed_url | |
| 371 | + FROM articles.likes l | |
| 372 | + JOIN articles.articles a ON a.url = l.article_url AND a.feed_url = l.feed_url | |
| 373 | + LEFT JOIN articles.feeds f ON f.feed_url = a.feed_url | |
| 374 | 374 | LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id |
| 375 | - LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc | |
| 375 | + LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM articles.likes GROUP BY feed_url, article_url) lc | |
| 376 | 376 | ON lc.feed_url = a.feed_url AND lc.article_url = a.url |
| 377 | 377 | WHERE l.author_did = ? |
| 378 | 378 | ORDER BY l.created_at DESC |
| @@ -35,17 +35,17 @@ type Like struct { | |||
| 35 | CID sql.NullString | 35 | CID sql.NullString |
| 36 | } | 36 | } |
| 37 | 37 | ||
| 38 | -func (db *DB) CreateAnnotation(ctx context.Context, a *Annotation) error { | 38 | +func (s *ArticleStore) CreateAnnotation(ctx context.Context, a *Annotation) error { |
| 39 | - return db.BatchCreateAnnotations(ctx, []*Annotation{a}) | 39 | + return s.BatchCreateAnnotations(ctx, []*Annotation{a}) |
| 40 | } | 40 | } |
| 41 | 41 | ||
| 42 | -func (db *DB) GetAnnotation(ctx context.Context, id int64) (*Annotation, error) { | 42 | +func (s *ArticleStore) GetAnnotation(ctx context.Context, id int64) (*Annotation, error) { |
| 43 | a := &Annotation{} | 43 | a := &Annotation{} |
| 44 | - err := db.QueryRowContext(ctx, ` | 44 | + err := s.db.QueryRowContext(ctx, ` |
| 45 | SELECT a.id, a.uri, a.author_did, COALESCE(u.handle, ''), a.feed_url, a.article_url, ar.id, a.quote, a.note, a.tags, a.rating, a.created_at, a.cid | 45 | SELECT a.id, a.uri, a.author_did, COALESCE(u.handle, ''), a.feed_url, a.article_url, ar.id, a.quote, a.note, a.tags, a.rating, a.created_at, a.cid |
| 46 | - FROM annotations a | 46 | + FROM articles.annotations a |
| 47 | LEFT JOIN users u ON a.author_did = u.did | 47 | LEFT JOIN users u ON a.author_did = u.did |
| 48 | - LEFT JOIN articles ar ON ar.url = a.article_url AND ar.feed_url = a.feed_url | 48 | + LEFT JOIN articles.articles ar ON ar.url = a.article_url AND ar.feed_url = a.feed_url |
| 49 | WHERE a.id = ? | 49 | WHERE a.id = ? |
| 50 | `, id).Scan(&a.ID, &a.URI, &a.AuthorDID, &a.AuthorHandle, &a.FeedURL, &a.ArticleURL, &a.ArticleID, | 50 | `, id).Scan(&a.ID, &a.URI, &a.AuthorDID, &a.AuthorHandle, &a.FeedURL, &a.ArticleURL, &a.ArticleID, |
| 51 | &a.Quote, &a.Note, &a.Tags, &a.Rating, &a.CreatedAt, &a.CID) | 51 | &a.Quote, &a.Note, &a.Tags, &a.Rating, &a.CreatedAt, &a.CID) |
| @@ -55,14 +55,14 @@ func (db *DB) GetAnnotation(ctx context.Context, id int64) (*Annotation, error) | |||
| 55 | return a, nil | 55 | return a, nil |
| 56 | } | 56 | } |
| 57 | 57 | ||
| 58 | -func (db *DB) DeleteAnnotation(ctx context.Context, uri string) error { | 58 | +func (s *ArticleStore) DeleteAnnotation(ctx context.Context, uri string) error { |
| 59 | - _, err := db.ExecContext(ctx, `DELETE FROM annotations WHERE uri = ?`, uri) | 59 | + _, err := s.db.ExecContext(ctx, `DELETE FROM articles.annotations WHERE uri = ?`, uri) |
| 60 | return err | 60 | return err |
| 61 | } | 61 | } |
| 62 | 62 | ||
| 63 | -func (db *DB) AnnotationExists(ctx context.Context, uri string) (bool, error) { | 63 | +func (s *ArticleStore) AnnotationExists(ctx context.Context, uri string) (bool, error) { |
| 64 | var exists int | 64 | var exists int |
| 65 | - err := db.QueryRowContext(ctx, `SELECT 1 FROM annotations WHERE uri = ?`, uri).Scan(&exists) | 65 | + err := s.db.QueryRowContext(ctx, `SELECT 1 FROM articles.annotations WHERE uri = ?`, uri).Scan(&exists) |
| 66 | if err == sql.ErrNoRows { | 66 | if err == sql.ErrNoRows { |
| 67 | return false, nil | 67 | return false, nil |
| 68 | } | 68 | } |
| @@ -72,7 +72,7 @@ func (db *DB) AnnotationExists(ctx context.Context, uri string) (bool, error) { | |||
| 72 | return true, nil | 72 | return true, nil |
| 73 | } | 73 | } |
| 74 | 74 | ||
| 75 | -func (db *DB) ListAnnotations(ctx context.Context, feedURL, articleURL, authorDID string, limit, offset int) ([]*Annotation, error) { | 75 | +func (s *ArticleStore) ListAnnotations(ctx context.Context, feedURL, articleURL, authorDID string, limit, offset int) ([]*Annotation, error) { |
| 76 | var conds []string | 76 | var conds []string |
| 77 | var args []any | 77 | var args []any |
| 78 | 78 | ||
| @@ -90,16 +90,16 @@ func (db *DB) ListAnnotations(ctx context.Context, feedURL, articleURL, authorDI | |||
| 90 | } | 90 | } |
| 91 | 91 | ||
| 92 | query := `SELECT a.id, a.uri, a.author_did, COALESCE(u.handle, ''), a.feed_url, a.article_url, ar.id, a.quote, a.note, a.tags, a.rating, a.created_at, a.cid | 92 | query := `SELECT a.id, a.uri, a.author_did, COALESCE(u.handle, ''), a.feed_url, a.article_url, ar.id, a.quote, a.note, a.tags, a.rating, a.created_at, a.cid |
| 93 | - FROM annotations a | 93 | + FROM articles.annotations a |
| 94 | LEFT JOIN users u ON a.author_did = u.did | 94 | LEFT JOIN users u ON a.author_did = u.did |
| 95 | - LEFT JOIN articles ar ON ar.url = a.article_url AND ar.feed_url = a.feed_url` | 95 | + LEFT JOIN articles.articles ar ON ar.url = a.article_url AND ar.feed_url = a.feed_url` |
| 96 | if len(conds) > 0 { | 96 | if len(conds) > 0 { |
| 97 | query += ` WHERE ` + strings.Join(conds, " AND ") | 97 | query += ` WHERE ` + strings.Join(conds, " AND ") |
| 98 | } | 98 | } |
| 99 | query += ` ORDER BY a.created_at DESC LIMIT ? OFFSET ?` | 99 | query += ` ORDER BY a.created_at DESC LIMIT ? OFFSET ?` |
| 100 | args = append(args, limit, offset) | 100 | args = append(args, limit, offset) |
| 101 | 101 | ||
| 102 | - rows, err := db.QueryContext(ctx, query, args...) | 102 | + rows, err := s.db.QueryContext(ctx, query, args...) |
| 103 | if err != nil { | 103 | if err != nil { |
| 104 | return nil, err | 104 | return nil, err |
| 105 | } | 105 | } |
| @@ -117,18 +117,18 @@ func (db *DB) ListAnnotations(ctx context.Context, feedURL, articleURL, authorDI | |||
| 117 | return annotations, rows.Err() | 117 | return annotations, rows.Err() |
| 118 | } | 118 | } |
| 119 | 119 | ||
| 120 | -func (db *DB) BatchCreateLikes(ctx context.Context, likes []*Like) error { | 120 | +func (s *ArticleStore) BatchCreateLikes(ctx context.Context, likes []*Like) error { |
| 121 | if len(likes) == 0 { | 121 | if len(likes) == 0 { |
| 122 | return nil | 122 | return nil |
| 123 | } | 123 | } |
| 124 | - tx, err := db.BeginTx(ctx, nil) | 124 | + tx, err := s.db.BeginTx(ctx, nil) |
| 125 | if err != nil { | 125 | if err != nil { |
| 126 | return err | 126 | return err |
| 127 | } | 127 | } |
| 128 | defer tx.Rollback() | 128 | defer tx.Rollback() |
| 129 | 129 | ||
| 130 | stmt, err := tx.PrepareContext(ctx, ` | 130 | stmt, err := tx.PrepareContext(ctx, ` |
| 131 | - INSERT OR IGNORE INTO likes (uri, author_did, feed_url, article_url, created_at, cid) | 131 | + INSERT OR IGNORE INTO articles.likes (uri, author_did, feed_url, article_url, created_at, cid) |
| 132 | VALUES (?, ?, ?, ?, ?, ?) | 132 | VALUES (?, ?, ?, ?, ?, ?) |
| 133 | `) | 133 | `) |
| 134 | if err != nil { | 134 | if err != nil { |
| @@ -144,18 +144,18 @@ func (db *DB) BatchCreateLikes(ctx context.Context, likes []*Like) error { | |||
| 144 | return tx.Commit() | 144 | return tx.Commit() |
| 145 | } | 145 | } |
| 146 | 146 | ||
| 147 | -func (db *DB) BatchCreateAnnotations(ctx context.Context, annotations []*Annotation) error { | 147 | +func (s *ArticleStore) BatchCreateAnnotations(ctx context.Context, annotations []*Annotation) error { |
| 148 | if len(annotations) == 0 { | 148 | if len(annotations) == 0 { |
| 149 | return nil | 149 | return nil |
| 150 | } | 150 | } |
| 151 | - tx, err := db.BeginTx(ctx, nil) | 151 | + tx, err := s.db.BeginTx(ctx, nil) |
| 152 | if err != nil { | 152 | if err != nil { |
| 153 | return err | 153 | return err |
| 154 | } | 154 | } |
| 155 | defer tx.Rollback() | 155 | defer tx.Rollback() |
| 156 | 156 | ||
| 157 | stmt, err := tx.PrepareContext(ctx, ` | 157 | stmt, err := tx.PrepareContext(ctx, ` |
| 158 | - INSERT OR IGNORE INTO annotations (uri, author_did, feed_url, article_url, quote, note, tags, rating, created_at, cid) | 158 | + INSERT OR IGNORE INTO articles.annotations (uri, author_did, feed_url, article_url, quote, note, tags, rating, created_at, cid) |
| 159 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | 159 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
| 160 | `) | 160 | `) |
| 161 | if err != nil { | 161 | if err != nil { |
| @@ -171,30 +171,30 @@ func (db *DB) BatchCreateAnnotations(ctx context.Context, annotations []*Annotat | |||
| 171 | return tx.Commit() | 171 | return tx.Commit() |
| 172 | } | 172 | } |
| 173 | 173 | ||
| 174 | -func (db *DB) CreateLike(ctx context.Context, l *Like) error { | 174 | +func (s *ArticleStore) CreateLike(ctx context.Context, l *Like) error { |
| 175 | - exists, err := db.HasLiked(ctx, l.AuthorDID, l.FeedURL, l.ArticleURL) | 175 | + exists, err := s.HasLiked(ctx, l.AuthorDID, l.FeedURL, l.ArticleURL) |
| 176 | if err != nil { | 176 | if err != nil { |
| 177 | return err | 177 | return err |
| 178 | } | 178 | } |
| 179 | if exists { | 179 | if exists { |
| 180 | return ErrDuplicateLike | 180 | return ErrDuplicateLike |
| 181 | } | 181 | } |
| 182 | - return db.BatchCreateLikes(ctx, []*Like{l}) | 182 | + return s.BatchCreateLikes(ctx, []*Like{l}) |
| 183 | } | 183 | } |
| 184 | 184 | ||
| 185 | -func (db *DB) DeleteLike(ctx context.Context, uri string) error { | 185 | +func (s *ArticleStore) DeleteLike(ctx context.Context, uri string) error { |
| 186 | - _, err := db.ExecContext(ctx, `DELETE FROM likes WHERE uri = ?`, uri) | 186 | + _, err := s.db.ExecContext(ctx, `DELETE FROM articles.likes WHERE uri = ?`, uri) |
| 187 | return err | 187 | return err |
| 188 | } | 188 | } |
| 189 | 189 | ||
| 190 | -func (db *DB) DeleteLikeByUserArticle(ctx context.Context, authorDID, feedURL, articleURL string) error { | 190 | +func (s *ArticleStore) DeleteLikeByUserArticle(ctx context.Context, authorDID, feedURL, articleURL string) error { |
| 191 | - _, err := db.ExecContext(ctx, ` | 191 | + _, err := s.db.ExecContext(ctx, ` |
| 192 | - DELETE FROM likes WHERE author_did = ? AND feed_url = ? AND article_url = ? | 192 | + DELETE FROM articles.likes WHERE author_did = ? AND feed_url = ? AND article_url = ? |
| 193 | `, authorDID, feedURL, articleURL) | 193 | `, authorDID, feedURL, articleURL) |
| 194 | return err | 194 | return err |
| 195 | } | 195 | } |
| 196 | 196 | ||
| 197 | -func (db *DB) ListLikes(ctx context.Context, authorDID, feedURL string, limit, offset int) ([]*Like, error) { | 197 | +func (s *ArticleStore) ListLikes(ctx context.Context, authorDID, feedURL string, limit, offset int) ([]*Like, error) { |
| 198 | var conds []string | 198 | var conds []string |
| 199 | var args []any | 199 | var args []any |
| 200 | 200 | ||
| @@ -207,14 +207,14 @@ func (db *DB) ListLikes(ctx context.Context, authorDID, feedURL string, limit, o | |||
| 207 | args = append(args, feedURL) | 207 | args = append(args, feedURL) |
| 208 | } | 208 | } |
| 209 | 209 | ||
| 210 | - query := `SELECT id, uri, author_did, feed_url, article_url, created_at, cid FROM likes` | 210 | + query := `SELECT id, uri, author_did, feed_url, article_url, created_at, cid FROM articles.likes` |
| 211 | if len(conds) > 0 { | 211 | if len(conds) > 0 { |
| 212 | query += ` WHERE ` + strings.Join(conds, " AND ") | 212 | query += ` WHERE ` + strings.Join(conds, " AND ") |
| 213 | } | 213 | } |
| 214 | query += ` ORDER BY created_at DESC LIMIT ? OFFSET ?` | 214 | query += ` ORDER BY created_at DESC LIMIT ? OFFSET ?` |
| 215 | args = append(args, limit, offset) | 215 | args = append(args, limit, offset) |
| 216 | 216 | ||
| 217 | - rows, err := db.QueryContext(ctx, query, args...) | 217 | + rows, err := s.db.QueryContext(ctx, query, args...) |
| 218 | if err != nil { | 218 | if err != nil { |
| 219 | return nil, err | 219 | return nil, err |
| 220 | } | 220 | } |
| @@ -231,18 +231,18 @@ func (db *DB) ListLikes(ctx context.Context, authorDID, feedURL string, limit, o | |||
| 231 | return likes, rows.Err() | 231 | return likes, rows.Err() |
| 232 | } | 232 | } |
| 233 | 233 | ||
| 234 | -func (db *DB) GetLikeCount(ctx context.Context, feedURL, articleURL string) (int, error) { | 234 | +func (s *ArticleStore) GetLikeCount(ctx context.Context, feedURL, articleURL string) (int, error) { |
| 235 | var count int | 235 | var count int |
| 236 | - err := db.QueryRowContext(ctx, ` | 236 | + err := s.db.QueryRowContext(ctx, ` |
| 237 | - SELECT COUNT(*) FROM likes WHERE feed_url = ? AND article_url = ? | 237 | + SELECT COUNT(*) FROM articles.likes WHERE feed_url = ? AND article_url = ? |
| 238 | `, feedURL, articleURL).Scan(&count) | 238 | `, feedURL, articleURL).Scan(&count) |
| 239 | return count, err | 239 | return count, err |
| 240 | } | 240 | } |
| 241 | 241 | ||
| 242 | -func (db *DB) GetLike(ctx context.Context, authorDID, feedURL, articleURL string) (*Like, error) { | 242 | +func (s *ArticleStore) GetLike(ctx context.Context, authorDID, feedURL, articleURL string) (*Like, error) { |
| 243 | l := &Like{} | 243 | l := &Like{} |
| 244 | - err := db.QueryRowContext(ctx, ` | 244 | + err := s.db.QueryRowContext(ctx, ` |
| 245 | - SELECT id, uri, author_did, feed_url, article_url, created_at, cid FROM likes | 245 | + SELECT id, uri, author_did, feed_url, article_url, created_at, cid FROM articles.likes |
| 246 | WHERE author_did = ? AND feed_url = ? AND article_url = ? | 246 | WHERE author_did = ? AND feed_url = ? AND article_url = ? |
| 247 | `, authorDID, feedURL, articleURL).Scan(&l.ID, &l.URI, &l.AuthorDID, &l.FeedURL, &l.ArticleURL, &l.CreatedAt, &l.CID) | 247 | `, authorDID, feedURL, articleURL).Scan(&l.ID, &l.URI, &l.AuthorDID, &l.FeedURL, &l.ArticleURL, &l.CreatedAt, &l.CID) |
| 248 | if err != nil { | 248 | if err != nil { |
| @@ -251,10 +251,10 @@ func (db *DB) GetLike(ctx context.Context, authorDID, feedURL, articleURL string | |||
| 251 | return l, nil | 251 | return l, nil |
| 252 | } | 252 | } |
| 253 | 253 | ||
| 254 | -func (db *DB) HasLiked(ctx context.Context, authorDID, feedURL, articleURL string) (bool, error) { | 254 | +func (s *ArticleStore) HasLiked(ctx context.Context, authorDID, feedURL, articleURL string) (bool, error) { |
| 255 | var exists int | 255 | var exists int |
| 256 | - err := db.QueryRowContext(ctx, ` | 256 | + err := s.db.QueryRowContext(ctx, ` |
| 257 | - SELECT 1 FROM likes WHERE author_did = ? AND feed_url = ? AND article_url = ? | 257 | + SELECT 1 FROM articles.likes WHERE author_did = ? AND feed_url = ? AND article_url = ? |
| 258 | `, authorDID, feedURL, articleURL).Scan(&exists) | 258 | `, authorDID, feedURL, articleURL).Scan(&exists) |
| 259 | if err == sql.ErrNoRows { | 259 | if err == sql.ErrNoRows { |
| 260 | return false, nil | 260 | return false, nil |
| @@ -279,19 +279,19 @@ type TrendingItem struct { | |||
| 279 | HasLiked bool | 279 | HasLiked bool |
| 280 | } | 280 | } |
| 281 | 281 | ||
| 282 | -func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since string, limit, offset int) ([]*TrendingItem, error) { | 282 | +func (s *ArticleStore) ListTrendingArticlesForUser(ctx context.Context, userDID, since string, limit, offset int) ([]*TrendingItem, error) { |
| 283 | - rows, err := db.QueryContext(ctx, ` | 283 | + rows, err := s.db.QueryContext(ctx, ` |
| 284 | SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''), | 284 | SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''), |
| 285 | COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''), | 285 | COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''), |
| 286 | COALESCE(f.favicon_url, ''), | 286 | COALESCE(f.favicon_url, ''), |
| 287 | COUNT(DISTINCT l.id) AS like_count, | 287 | COUNT(DISTINCT l.id) AS like_count, |
| 288 | COUNT(DISTINCT a.id) AS annotation_count, | 288 | COUNT(DISTINCT a.id) AS annotation_count, |
| 289 | COALESCE(MAX(CASE WHEN ul.id IS NOT NULL THEN 1 ELSE 0 END), 0) | 289 | COALESCE(MAX(CASE WHEN ul.id IS NOT NULL THEN 1 ELSE 0 END), 0) |
| 290 | - FROM likes l | 290 | + FROM articles.likes l |
| 291 | - JOIN articles ar ON ar.url = l.article_url AND ar.feed_url = l.feed_url | 291 | + JOIN articles.articles ar ON ar.url = l.article_url AND ar.feed_url = l.feed_url |
| 292 | - LEFT JOIN feeds f ON f.feed_url = l.feed_url | 292 | + LEFT JOIN articles.feeds f ON f.feed_url = l.feed_url |
| 293 | - LEFT JOIN annotations a ON a.feed_url = l.feed_url AND a.article_url = l.article_url AND a.created_at >= ? | 293 | + LEFT JOIN articles.annotations a ON a.feed_url = l.feed_url AND a.article_url = l.article_url AND a.created_at >= ? |
| 294 | - LEFT JOIN likes ul ON ul.feed_url = l.feed_url AND ul.article_url = l.article_url AND ul.author_did = ? | 294 | + LEFT JOIN articles.likes ul ON ul.feed_url = l.feed_url AND ul.article_url = l.article_url AND ul.author_did = ? |
| 295 | WHERE l.created_at >= ? | 295 | WHERE l.created_at >= ? |
| 296 | AND l.author_did IN ( | 296 | AND l.author_did IN ( |
| 297 | SELECT CASE WHEN us.user_a = ? THEN us.user_b ELSE us.user_a END | 297 | SELECT CASE WHEN us.user_a = ? THEN us.user_b ELSE us.user_a END |
| @@ -323,19 +323,19 @@ func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since st | |||
| 323 | return results, rows.Err() | 323 | return results, rows.Err() |
| 324 | } | 324 | } |
| 325 | 325 | ||
| 326 | -func (db *DB) ListTrendingArticles(ctx context.Context, userDID, since string, limit, offset int) ([]*TrendingItem, error) { | 326 | +func (s *ArticleStore) ListTrendingArticles(ctx context.Context, userDID, since string, limit, offset int) ([]*TrendingItem, error) { |
| 327 | - rows, err := db.QueryContext(ctx, ` | 327 | + rows, err := s.db.QueryContext(ctx, ` |
| 328 | SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''), | 328 | SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''), |
| 329 | COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''), | 329 | COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''), |
| 330 | COALESCE(f.favicon_url, ''), | 330 | COALESCE(f.favicon_url, ''), |
| 331 | COUNT(DISTINCT l.id) AS like_count, | 331 | COUNT(DISTINCT l.id) AS like_count, |
| 332 | COUNT(DISTINCT a.id) AS annotation_count, | 332 | COUNT(DISTINCT a.id) AS annotation_count, |
| 333 | COALESCE(MAX(CASE WHEN ul.id IS NOT NULL THEN 1 ELSE 0 END), 0) | 333 | COALESCE(MAX(CASE WHEN ul.id IS NOT NULL THEN 1 ELSE 0 END), 0) |
| 334 | - FROM likes l | 334 | + FROM articles.likes l |
| 335 | - JOIN articles ar ON ar.url = l.article_url AND ar.feed_url = l.feed_url | 335 | + JOIN articles.articles ar ON ar.url = l.article_url AND ar.feed_url = l.feed_url |
| 336 | - LEFT JOIN feeds f ON f.feed_url = l.feed_url | 336 | + LEFT JOIN articles.feeds f ON f.feed_url = l.feed_url |
| 337 | - LEFT JOIN annotations a ON a.feed_url = l.feed_url AND a.article_url = l.article_url AND a.created_at >= ? | 337 | + LEFT JOIN articles.annotations a ON a.feed_url = l.feed_url AND a.article_url = l.article_url AND a.created_at >= ? |
| 338 | - LEFT JOIN likes ul ON ul.feed_url = l.feed_url AND ul.article_url = l.article_url AND ul.author_did = ? | 338 | + LEFT JOIN articles.likes ul ON ul.feed_url = l.feed_url AND ul.article_url = l.article_url AND ul.author_did = ? |
| 339 | WHERE l.created_at >= ? | 339 | WHERE l.created_at >= ? |
| 340 | GROUP BY ar.id | 340 | GROUP BY ar.id |
| 341 | -- Future-published articles (e.g., scheduled) sort last | 341 | -- Future-published articles (e.g., scheduled) sort last |
| @@ -360,19 +360,19 @@ func (db *DB) ListTrendingArticles(ctx context.Context, userDID, since string, l | |||
| 360 | return results, rows.Err() | 360 | return results, rows.Err() |
| 361 | } | 361 | } |
| 362 | 362 | ||
| 363 | -func (db *DB) ListLikedArticles(ctx context.Context, userDID string, limit, offset int) ([]*Article, error) { | 363 | +func (s *ArticleStore) ListLikedArticles(ctx context.Context, userDID string, limit, offset int) ([]*Article, error) { |
| 364 | - rows, err := db.QueryContext(ctx, ` | 364 | + rows, err := s.db.QueryContext(ctx, ` |
| 365 | SELECT DISTINCT a.id, a.feed_url, a.guid, a.title, a.url, a.author, a.summary, a.content, | 365 | SELECT DISTINCT a.id, a.feed_url, a.guid, a.title, a.url, a.author, a.summary, a.content, |
| 366 | a.published, a.updated, a.fetched_at, | 366 | a.published, a.updated, a.fetched_at, |
| 367 | COALESCE(f.title, ''), | 367 | COALESCE(f.title, ''), |
| 368 | COALESCE(r.is_read, 0), | 368 | COALESCE(r.is_read, 0), |
| 369 | COALESCE(lc.cnt, 0), | 369 | COALESCE(lc.cnt, 0), |
| 370 | 1 | 370 | 1 |
| 371 | - FROM likes l | 371 | + FROM articles.likes l |
| 372 | - JOIN articles a ON a.url = l.article_url AND a.feed_url = l.feed_url | 372 | + JOIN articles.articles a ON a.url = l.article_url AND a.feed_url = l.feed_url |
| 373 | - LEFT JOIN feeds f ON f.feed_url = a.feed_url | 373 | + LEFT JOIN articles.feeds f ON f.feed_url = a.feed_url |
| 374 | LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id | 374 | LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id |
| 375 | - LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc | 375 | + LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM articles.likes GROUP BY feed_url, article_url) lc |
| 376 | ON lc.feed_url = a.feed_url AND lc.article_url = a.url | 376 | ON lc.feed_url = a.feed_url AND lc.article_url = a.url |
| 377 | WHERE l.author_did = ? | 377 | WHERE l.author_did = ? |
| 378 | ORDER BY l.created_at DESC | 378 | ORDER BY l.created_at DESC |
modified
internal/db/store.go +8 -8 | @@ -8,15 +8,15 @@ import ( | ||
| 8 | 8 | ) |
| 9 | 9 | |
| 10 | 10 | type FeedStoreAdapter struct { |
| 11 | - db *DB | |
| 11 | + store *ArticleStore | |
| 12 | 12 | } |
| 13 | 13 | |
| 14 | -func NewFeedStoreAdapter(db *DB) *FeedStoreAdapter { | |
| 15 | - return &FeedStoreAdapter{db: db} | |
| 14 | +func NewFeedStoreAdapter(store *ArticleStore) *FeedStoreAdapter { | |
| 15 | + return &FeedStoreAdapter{store: store} | |
| 16 | 16 | } |
| 17 | 17 | |
| 18 | 18 | func (a *FeedStoreAdapter) GetFeedsToFetch(ctx context.Context, olderThan time.Duration, limit int) ([]*feed.Feed, error) { |
| 19 | - dbFeeds, err := a.db.GetFeedsToFetch(ctx, olderThan, limit) | |
| 19 | + dbFeeds, err := a.store.GetFeedsToFetch(ctx, olderThan, limit) | |
| 20 | 20 | if err != nil { |
| 21 | 21 | return nil, err |
| 22 | 22 | } |
| @@ -37,20 +37,20 @@ func (a *FeedStoreAdapter) GetFeedsToFetch(ctx context.Context, olderThan time.D | ||
| 37 | 37 | } |
| 38 | 38 | |
| 39 | 39 | func (a *FeedStoreAdapter) RecordFetchError(ctx context.Context, feedURL, lastError string) error { |
| 40 | - return a.db.MarkFeedFetchError(ctx, feedURL, lastError) | |
| 40 | + return a.store.MarkFeedFetchError(ctx, feedURL, lastError) | |
| 41 | 41 | } |
| 42 | 42 | |
| 43 | 43 | func (a *FeedStoreAdapter) StoreFetchResult(ctx context.Context, feedURL, etag, lastModified string, articles []feed.Article, faviconURL string) error { |
| 44 | - if err := a.db.MarkFeedFetched(ctx, feedURL, etag, lastModified); err != nil { | |
| 44 | + if err := a.store.MarkFeedFetched(ctx, feedURL, etag, lastModified); err != nil { | |
| 45 | 45 | return err |
| 46 | 46 | } |
| 47 | 47 | if len(articles) > 0 { |
| 48 | - if err := a.db.UpsertArticlesBatch(ctx, articles); err != nil { | |
| 48 | + if err := a.store.UpsertArticlesBatch(ctx, articles); err != nil { | |
| 49 | 49 | return err |
| 50 | 50 | } |
| 51 | 51 | } |
| 52 | 52 | if faviconURL != "" { |
| 53 | - if err := a.db.UpdateFeedFavicon(ctx, feedURL, faviconURL); err != nil { | |
| 53 | + if err := a.store.UpdateFeedFavicon(ctx, feedURL, faviconURL); err != nil { | |
| 54 | 54 | return err |
| 55 | 55 | } |
| 56 | 56 | } |
| @@ -8,15 +8,15 @@ import ( | |||
| 8 | ) | 8 | ) |
| 9 | 9 | ||
| 10 | type FeedStoreAdapter struct { | 10 | type FeedStoreAdapter struct { |
| 11 | - db *DB | 11 | + store *ArticleStore |
| 12 | } | 12 | } |
| 13 | 13 | ||
| 14 | -func NewFeedStoreAdapter(db *DB) *FeedStoreAdapter { | 14 | +func NewFeedStoreAdapter(store *ArticleStore) *FeedStoreAdapter { |
| 15 | - return &FeedStoreAdapter{db: db} | 15 | + return &FeedStoreAdapter{store: store} |
| 16 | } | 16 | } |
| 17 | 17 | ||
| 18 | func (a *FeedStoreAdapter) GetFeedsToFetch(ctx context.Context, olderThan time.Duration, limit int) ([]*feed.Feed, error) { | 18 | func (a *FeedStoreAdapter) GetFeedsToFetch(ctx context.Context, olderThan time.Duration, limit int) ([]*feed.Feed, error) { |
| 19 | - dbFeeds, err := a.db.GetFeedsToFetch(ctx, olderThan, limit) | 19 | + dbFeeds, err := a.store.GetFeedsToFetch(ctx, olderThan, limit) |
| 20 | if err != nil { | 20 | if err != nil { |
| 21 | return nil, err | 21 | return nil, err |
| 22 | } | 22 | } |
| @@ -37,20 +37,20 @@ func (a *FeedStoreAdapter) GetFeedsToFetch(ctx context.Context, olderThan time.D | |||
| 37 | } | 37 | } |
| 38 | 38 | ||
| 39 | func (a *FeedStoreAdapter) RecordFetchError(ctx context.Context, feedURL, lastError string) error { | 39 | func (a *FeedStoreAdapter) RecordFetchError(ctx context.Context, feedURL, lastError string) error { |
| 40 | - return a.db.MarkFeedFetchError(ctx, feedURL, lastError) | 40 | + return a.store.MarkFeedFetchError(ctx, feedURL, lastError) |
| 41 | } | 41 | } |
| 42 | 42 | ||
| 43 | func (a *FeedStoreAdapter) StoreFetchResult(ctx context.Context, feedURL, etag, lastModified string, articles []feed.Article, faviconURL string) error { | 43 | func (a *FeedStoreAdapter) StoreFetchResult(ctx context.Context, feedURL, etag, lastModified string, articles []feed.Article, faviconURL string) error { |
| 44 | - if err := a.db.MarkFeedFetched(ctx, feedURL, etag, lastModified); err != nil { | 44 | + if err := a.store.MarkFeedFetched(ctx, feedURL, etag, lastModified); err != nil { |
| 45 | return err | 45 | return err |
| 46 | } | 46 | } |
| 47 | if len(articles) > 0 { | 47 | if len(articles) > 0 { |
| 48 | - if err := a.db.UpsertArticlesBatch(ctx, articles); err != nil { | 48 | + if err := a.store.UpsertArticlesBatch(ctx, articles); err != nil { |
| 49 | return err | 49 | return err |
| 50 | } | 50 | } |
| 51 | } | 51 | } |
| 52 | if faviconURL != "" { | 52 | if faviconURL != "" { |
| 53 | - if err := a.db.UpdateFeedFavicon(ctx, feedURL, faviconURL); err != nil { | 53 | + if err := a.store.UpdateFeedFavicon(ctx, feedURL, faviconURL); err != nil { |
| 54 | return err | 54 | return err |
| 55 | } | 55 | } |
| 56 | } | 56 | } |
modified
internal/db/user.go +23 -15 | @@ -21,11 +21,19 @@ type UserData struct { | ||
| 21 | 21 | AvatarURL string |
| 22 | 22 | } |
| 23 | 23 | |
| 24 | -func (db *DB) BatchCreateUsers(ctx context.Context, users []UserData) error { | |
| 24 | +type UserStore struct { | |
| 25 | + db *DB | |
| 26 | +} | |
| 27 | + | |
| 28 | +func NewUserStore(db *DB) *UserStore { | |
| 29 | + return &UserStore{db: db} | |
| 30 | +} | |
| 31 | + | |
| 32 | +func (s *UserStore) BatchCreateUsers(ctx context.Context, users []UserData) error { | |
| 25 | 33 | if len(users) == 0 { |
| 26 | 34 | return nil |
| 27 | 35 | } |
| 28 | - tx, err := db.BeginTx(ctx, nil) | |
| 36 | + tx, err := s.db.BeginTx(ctx, nil) | |
| 29 | 37 | if err != nil { |
| 30 | 38 | return err |
| 31 | 39 | } |
| @@ -53,17 +61,17 @@ func (db *DB) BatchCreateUsers(ctx context.Context, users []UserData) error { | ||
| 53 | 61 | return tx.Commit() |
| 54 | 62 | } |
| 55 | 63 | |
| 56 | -func (db *DB) CreateUser(ctx context.Context, did, handle, displayName, avatarURL string) (*User, error) { | |
| 57 | - err := db.BatchCreateUsers(ctx, []UserData{{DID: did, Handle: handle, DisplayName: displayName, AvatarURL: avatarURL}}) | |
| 64 | +func (s *UserStore) CreateUser(ctx context.Context, did, handle, displayName, avatarURL string) (*User, error) { | |
| 65 | + err := s.BatchCreateUsers(ctx, []UserData{{DID: did, Handle: handle, DisplayName: displayName, AvatarURL: avatarURL}}) | |
| 58 | 66 | if err != nil { |
| 59 | 67 | return nil, err |
| 60 | 68 | } |
| 61 | - return db.GetUser(ctx, did) | |
| 69 | + return s.GetUser(ctx, did) | |
| 62 | 70 | } |
| 63 | 71 | |
| 64 | -func (db *DB) GetUser(ctx context.Context, did string) (*User, error) { | |
| 72 | +func (s *UserStore) GetUser(ctx context.Context, did string) (*User, error) { | |
| 65 | 73 | u := &User{} |
| 66 | - err := db.QueryRowContext(ctx, ` | |
| 74 | + err := s.db.QueryRowContext(ctx, ` | |
| 67 | 75 | SELECT did, handle, display_name, avatar_url, indexed_at, updated_at |
| 68 | 76 | FROM users WHERE did = ? |
| 69 | 77 | `, did).Scan(&u.DID, &u.Handle, &u.DisplayName, &u.AvatarURL, &u.IndexedAt, &u.UpdatedAt) |
| @@ -73,9 +81,9 @@ func (db *DB) GetUser(ctx context.Context, did string) (*User, error) { | ||
| 73 | 81 | return u, nil |
| 74 | 82 | } |
| 75 | 83 | |
| 76 | -func (db *DB) GetUserByHandle(ctx context.Context, handle string) (*User, error) { | |
| 84 | +func (s *UserStore) GetUserByHandle(ctx context.Context, handle string) (*User, error) { | |
| 77 | 85 | u := &User{} |
| 78 | - err := db.QueryRowContext(ctx, ` | |
| 86 | + err := s.db.QueryRowContext(ctx, ` | |
| 79 | 87 | SELECT did, handle, display_name, avatar_url, indexed_at, updated_at |
| 80 | 88 | FROM users WHERE handle = ? |
| 81 | 89 | `, handle).Scan(&u.DID, &u.Handle, &u.DisplayName, &u.AvatarURL, &u.IndexedAt, &u.UpdatedAt) |
| @@ -85,8 +93,8 @@ func (db *DB) GetUserByHandle(ctx context.Context, handle string) (*User, error) | ||
| 85 | 93 | return u, nil |
| 86 | 94 | } |
| 87 | 95 | |
| 88 | -func (db *DB) ListUserDIDs(ctx context.Context) (map[string]bool, error) { | |
| 89 | - rows, err := db.QueryContext(ctx, `SELECT did FROM users`) | |
| 96 | +func (s *UserStore) ListUserDIDs(ctx context.Context) (map[string]bool, error) { | |
| 97 | + rows, err := s.db.QueryContext(ctx, `SELECT did FROM users`) | |
| 90 | 98 | if err != nil { |
| 91 | 99 | return nil, err |
| 92 | 100 | } |
| @@ -103,8 +111,8 @@ func (db *DB) ListUserDIDs(ctx context.Context) (map[string]bool, error) { | ||
| 103 | 111 | return dids, rows.Err() |
| 104 | 112 | } |
| 105 | 113 | |
| 106 | -func (db *DB) UpdateUserProfile(ctx context.Context, did, displayName, avatarURL string) error { | |
| 107 | - _, err := db.ExecContext(ctx, ` | |
| 114 | +func (s *UserStore) UpdateUserProfile(ctx context.Context, did, displayName, avatarURL string) error { | |
| 115 | + _, err := s.db.ExecContext(ctx, ` | |
| 108 | 116 | UPDATE users SET |
| 109 | 117 | display_name = COALESCE(NULLIF(?, ''), display_name), |
| 110 | 118 | avatar_url = COALESCE(NULLIF(?, ''), avatar_url), |
| @@ -114,8 +122,8 @@ func (db *DB) UpdateUserProfile(ctx context.Context, did, displayName, avatarURL | ||
| 114 | 122 | return err |
| 115 | 123 | } |
| 116 | 124 | |
| 117 | -func (db *DB) ListUsers(ctx context.Context) ([]*User, error) { | |
| 118 | - rows, err := db.QueryContext(ctx, ` | |
| 125 | +func (s *UserStore) ListUsers(ctx context.Context) ([]*User, error) { | |
| 126 | + rows, err := s.db.QueryContext(ctx, ` | |
| 119 | 127 | SELECT did, handle, display_name, avatar_url, indexed_at, updated_at |
| 120 | 128 | FROM users ORDER BY updated_at DESC |
| 121 | 129 | `) |
| @@ -21,11 +21,19 @@ type UserData struct { | |||
| 21 | AvatarURL string | 21 | AvatarURL string |
| 22 | } | 22 | } |
| 23 | 23 | ||
| 24 | -func (db *DB) BatchCreateUsers(ctx context.Context, users []UserData) error { | 24 | +type UserStore struct { |
| 25 | + db *DB | ||
| 26 | +} | ||
| 27 | + | ||
| 28 | +func NewUserStore(db *DB) *UserStore { | ||
| 29 | + return &UserStore{db: db} | ||
| 30 | +} | ||
| 31 | + | ||
| 32 | +func (s *UserStore) BatchCreateUsers(ctx context.Context, users []UserData) error { | ||
| 25 | if len(users) == 0 { | 33 | if len(users) == 0 { |
| 26 | return nil | 34 | return nil |
| 27 | } | 35 | } |
| 28 | - tx, err := db.BeginTx(ctx, nil) | 36 | + tx, err := s.db.BeginTx(ctx, nil) |
| 29 | if err != nil { | 37 | if err != nil { |
| 30 | return err | 38 | return err |
| 31 | } | 39 | } |
| @@ -53,17 +61,17 @@ func (db *DB) BatchCreateUsers(ctx context.Context, users []UserData) error { | |||
| 53 | return tx.Commit() | 61 | return tx.Commit() |
| 54 | } | 62 | } |
| 55 | 63 | ||
| 56 | -func (db *DB) CreateUser(ctx context.Context, did, handle, displayName, avatarURL string) (*User, error) { | 64 | +func (s *UserStore) CreateUser(ctx context.Context, did, handle, displayName, avatarURL string) (*User, error) { |
| 57 | - err := db.BatchCreateUsers(ctx, []UserData{{DID: did, Handle: handle, DisplayName: displayName, AvatarURL: avatarURL}}) | 65 | + err := s.BatchCreateUsers(ctx, []UserData{{DID: did, Handle: handle, DisplayName: displayName, AvatarURL: avatarURL}}) |
| 58 | if err != nil { | 66 | if err != nil { |
| 59 | return nil, err | 67 | return nil, err |
| 60 | } | 68 | } |
| 61 | - return db.GetUser(ctx, did) | 69 | + return s.GetUser(ctx, did) |
| 62 | } | 70 | } |
| 63 | 71 | ||
| 64 | -func (db *DB) GetUser(ctx context.Context, did string) (*User, error) { | 72 | +func (s *UserStore) GetUser(ctx context.Context, did string) (*User, error) { |
| 65 | u := &User{} | 73 | u := &User{} |
| 66 | - err := db.QueryRowContext(ctx, ` | 74 | + err := s.db.QueryRowContext(ctx, ` |
| 67 | SELECT did, handle, display_name, avatar_url, indexed_at, updated_at | 75 | SELECT did, handle, display_name, avatar_url, indexed_at, updated_at |
| 68 | FROM users WHERE did = ? | 76 | FROM users WHERE did = ? |
| 69 | `, did).Scan(&u.DID, &u.Handle, &u.DisplayName, &u.AvatarURL, &u.IndexedAt, &u.UpdatedAt) | 77 | `, did).Scan(&u.DID, &u.Handle, &u.DisplayName, &u.AvatarURL, &u.IndexedAt, &u.UpdatedAt) |
| @@ -73,9 +81,9 @@ func (db *DB) GetUser(ctx context.Context, did string) (*User, error) { | |||
| 73 | return u, nil | 81 | return u, nil |
| 74 | } | 82 | } |
| 75 | 83 | ||
| 76 | -func (db *DB) GetUserByHandle(ctx context.Context, handle string) (*User, error) { | 84 | +func (s *UserStore) GetUserByHandle(ctx context.Context, handle string) (*User, error) { |
| 77 | u := &User{} | 85 | u := &User{} |
| 78 | - err := db.QueryRowContext(ctx, ` | 86 | + err := s.db.QueryRowContext(ctx, ` |
| 79 | SELECT did, handle, display_name, avatar_url, indexed_at, updated_at | 87 | SELECT did, handle, display_name, avatar_url, indexed_at, updated_at |
| 80 | FROM users WHERE handle = ? | 88 | FROM users WHERE handle = ? |
| 81 | `, handle).Scan(&u.DID, &u.Handle, &u.DisplayName, &u.AvatarURL, &u.IndexedAt, &u.UpdatedAt) | 89 | `, handle).Scan(&u.DID, &u.Handle, &u.DisplayName, &u.AvatarURL, &u.IndexedAt, &u.UpdatedAt) |
| @@ -85,8 +93,8 @@ func (db *DB) GetUserByHandle(ctx context.Context, handle string) (*User, error) | |||
| 85 | return u, nil | 93 | return u, nil |
| 86 | } | 94 | } |
| 87 | 95 | ||
| 88 | -func (db *DB) ListUserDIDs(ctx context.Context) (map[string]bool, error) { | 96 | +func (s *UserStore) ListUserDIDs(ctx context.Context) (map[string]bool, error) { |
| 89 | - rows, err := db.QueryContext(ctx, `SELECT did FROM users`) | 97 | + rows, err := s.db.QueryContext(ctx, `SELECT did FROM users`) |
| 90 | if err != nil { | 98 | if err != nil { |
| 91 | return nil, err | 99 | return nil, err |
| 92 | } | 100 | } |
| @@ -103,8 +111,8 @@ func (db *DB) ListUserDIDs(ctx context.Context) (map[string]bool, error) { | |||
| 103 | return dids, rows.Err() | 111 | return dids, rows.Err() |
| 104 | } | 112 | } |
| 105 | 113 | ||
| 106 | -func (db *DB) UpdateUserProfile(ctx context.Context, did, displayName, avatarURL string) error { | 114 | +func (s *UserStore) UpdateUserProfile(ctx context.Context, did, displayName, avatarURL string) error { |
| 107 | - _, err := db.ExecContext(ctx, ` | 115 | + _, err := s.db.ExecContext(ctx, ` |
| 108 | UPDATE users SET | 116 | UPDATE users SET |
| 109 | display_name = COALESCE(NULLIF(?, ''), display_name), | 117 | display_name = COALESCE(NULLIF(?, ''), display_name), |
| 110 | avatar_url = COALESCE(NULLIF(?, ''), avatar_url), | 118 | avatar_url = COALESCE(NULLIF(?, ''), avatar_url), |
| @@ -114,8 +122,8 @@ func (db *DB) UpdateUserProfile(ctx context.Context, did, displayName, avatarURL | |||
| 114 | return err | 122 | return err |
| 115 | } | 123 | } |
| 116 | 124 | ||
| 117 | -func (db *DB) ListUsers(ctx context.Context) ([]*User, error) { | 125 | +func (s *UserStore) ListUsers(ctx context.Context) ([]*User, error) { |
| 118 | - rows, err := db.QueryContext(ctx, ` | 126 | + rows, err := s.db.QueryContext(ctx, ` |
| 119 | SELECT did, handle, display_name, avatar_url, indexed_at, updated_at | 127 | SELECT did, handle, display_name, avatar_url, indexed_at, updated_at |
| 120 | FROM users ORDER BY updated_at DESC | 128 | FROM users ORDER BY updated_at DESC |
| 121 | `) | 129 | `) |
modified
internal/db/user_test.go +8 -8 | @@ -9,15 +9,15 @@ import ( | ||
| 9 | 9 | |
| 10 | 10 | func TestUpdateUserProfile_SetsFields(t *testing.T) { |
| 11 | 11 | ctx := context.Background() |
| 12 | - db := setupTestDB(t) | |
| 12 | + dbs := setupTestDB(t) | |
| 13 | 13 | |
| 14 | - _, err := db.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, "did:test:profile", "tester") | |
| 14 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, "did:test:profile", "tester") | |
| 15 | 15 | assert.NilError(t, err) |
| 16 | 16 | |
| 17 | - err = db.UpdateUserProfile(ctx, "did:test:profile", "Display Name", "https://cdn.bsky.app/img/avatar.png") | |
| 17 | + err = dbs.Users.UpdateUserProfile(ctx, "did:test:profile", "Display Name", "https://cdn.bsky.app/img/avatar.png") | |
| 18 | 18 | assert.NilError(t, err) |
| 19 | 19 | |
| 20 | - u, err := db.GetUser(ctx, "did:test:profile") | |
| 20 | + u, err := dbs.Users.GetUser(ctx, "did:test:profile") | |
| 21 | 21 | assert.NilError(t, err) |
| 22 | 22 | assert.Equal(t, u.DisplayName.String, "Display Name") |
| 23 | 23 | assert.Equal(t, u.AvatarURL.String, "https://cdn.bsky.app/img/avatar.png") |
| @@ -25,16 +25,16 @@ func TestUpdateUserProfile_SetsFields(t *testing.T) { | ||
| 25 | 25 | |
| 26 | 26 | func TestUpdateUserProfile_DoesNotOverwriteWithEmpty(t *testing.T) { |
| 27 | 27 | ctx := context.Background() |
| 28 | - db := setupTestDB(t) | |
| 28 | + dbs := setupTestDB(t) | |
| 29 | 29 | |
| 30 | - _, err := db.ExecContext(ctx, `INSERT INTO users (did, handle, display_name, avatar_url) VALUES (?, ?, ?, ?)`, | |
| 30 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle, display_name, avatar_url) VALUES (?, ?, ?, ?)`, | |
| 31 | 31 | "did:test:profile2", "tester2", "Existing Name", "https://old.avatar/url") |
| 32 | 32 | assert.NilError(t, err) |
| 33 | 33 | |
| 34 | - err = db.UpdateUserProfile(ctx, "did:test:profile2", "", "") | |
| 34 | + err = dbs.Users.UpdateUserProfile(ctx, "did:test:profile2", "", "") | |
| 35 | 35 | assert.NilError(t, err) |
| 36 | 36 | |
| 37 | - u, err := db.GetUser(ctx, "did:test:profile2") | |
| 37 | + u, err := dbs.Users.GetUser(ctx, "did:test:profile2") | |
| 38 | 38 | assert.NilError(t, err) |
| 39 | 39 | assert.Equal(t, u.DisplayName.String, "Existing Name") |
| 40 | 40 | assert.Equal(t, u.AvatarURL.String, "https://old.avatar/url") |
| @@ -9,15 +9,15 @@ import ( | |||
| 9 | 9 | ||
| 10 | func TestUpdateUserProfile_SetsFields(t *testing.T) { | 10 | func TestUpdateUserProfile_SetsFields(t *testing.T) { |
| 11 | ctx := context.Background() | 11 | ctx := context.Background() |
| 12 | - db := setupTestDB(t) | 12 | + dbs := setupTestDB(t) |
| 13 | 13 | ||
| 14 | - _, err := db.ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, "did:test:profile", "tester") | 14 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, "did:test:profile", "tester") |
| 15 | assert.NilError(t, err) | 15 | assert.NilError(t, err) |
| 16 | 16 | ||
| 17 | - err = db.UpdateUserProfile(ctx, "did:test:profile", "Display Name", "https://cdn.bsky.app/img/avatar.png") | 17 | + err = dbs.Users.UpdateUserProfile(ctx, "did:test:profile", "Display Name", "https://cdn.bsky.app/img/avatar.png") |
| 18 | assert.NilError(t, err) | 18 | assert.NilError(t, err) |
| 19 | 19 | ||
| 20 | - u, err := db.GetUser(ctx, "did:test:profile") | 20 | + u, err := dbs.Users.GetUser(ctx, "did:test:profile") |
| 21 | assert.NilError(t, err) | 21 | assert.NilError(t, err) |
| 22 | assert.Equal(t, u.DisplayName.String, "Display Name") | 22 | assert.Equal(t, u.DisplayName.String, "Display Name") |
| 23 | assert.Equal(t, u.AvatarURL.String, "https://cdn.bsky.app/img/avatar.png") | 23 | assert.Equal(t, u.AvatarURL.String, "https://cdn.bsky.app/img/avatar.png") |
| @@ -25,16 +25,16 @@ func TestUpdateUserProfile_SetsFields(t *testing.T) { | |||
| 25 | 25 | ||
| 26 | func TestUpdateUserProfile_DoesNotOverwriteWithEmpty(t *testing.T) { | 26 | func TestUpdateUserProfile_DoesNotOverwriteWithEmpty(t *testing.T) { |
| 27 | ctx := context.Background() | 27 | ctx := context.Background() |
| 28 | - db := setupTestDB(t) | 28 | + dbs := setupTestDB(t) |
| 29 | 29 | ||
| 30 | - _, err := db.ExecContext(ctx, `INSERT INTO users (did, handle, display_name, avatar_url) VALUES (?, ?, ?, ?)`, | 30 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle, display_name, avatar_url) VALUES (?, ?, ?, ?)`, |
| 31 | "did:test:profile2", "tester2", "Existing Name", "https://old.avatar/url") | 31 | "did:test:profile2", "tester2", "Existing Name", "https://old.avatar/url") |
| 32 | assert.NilError(t, err) | 32 | assert.NilError(t, err) |
| 33 | 33 | ||
| 34 | - err = db.UpdateUserProfile(ctx, "did:test:profile2", "", "") | 34 | + err = dbs.Users.UpdateUserProfile(ctx, "did:test:profile2", "", "") |
| 35 | assert.NilError(t, err) | 35 | assert.NilError(t, err) |
| 36 | 36 | ||
| 37 | - u, err := db.GetUser(ctx, "did:test:profile2") | 37 | + u, err := dbs.Users.GetUser(ctx, "did:test:profile2") |
| 38 | assert.NilError(t, err) | 38 | assert.NilError(t, err) |
| 39 | assert.Equal(t, u.DisplayName.String, "Existing Name") | 39 | assert.Equal(t, u.DisplayName.String, "Existing Name") |
| 40 | assert.Equal(t, u.AvatarURL.String, "https://old.avatar/url") | 40 | assert.Equal(t, u.AvatarURL.String, "https://old.avatar/url") |
modified
internal/server/server.go +2 -2 | @@ -73,7 +73,7 @@ type Server struct { | ||
| 73 | 73 | } |
| 74 | 74 | |
| 75 | 75 | func New(dbs *db.Databases, clientID, callbackURL, addr string, scheduler *feed.Scheduler, engine *cluster.Engine, logger *slog.Logger) *Server { |
| 76 | - oauthStore := db.NewOAuthStore(dbs.Users) | |
| 76 | + oauthStore := db.NewOAuthStore(dbs) | |
| 77 | 77 | |
| 78 | 78 | var config oauth.ClientConfig |
| 79 | 79 | if clientID == "" { |
| @@ -202,7 +202,7 @@ func (s *Server) setupRoutes() { | ||
| 202 | 202 | s.router.Post("/auth/logout", s.handleAuthLogout) |
| 203 | 203 | s.router.Get("/oauth/client-metadata", s.handleOAuthClientMetadata) |
| 204 | 204 | |
| 205 | - xrpc := atproto.NewXRPCHandler(s.dbs.Articles.DB, s.engine) | |
| 205 | + xrpc := atproto.NewXRPCHandler(s.dbs.DB(), s.engine) | |
| 206 | 206 | s.router.Get("/xrpc/at.glean.listSubscriptions", xrpc.ListSubscriptions) |
| 207 | 207 | s.router.Get("/xrpc/at.glean.listAnnotations", xrpc.ListAnnotations) |
| 208 | 208 | s.router.Get("/xrpc/at.glean.listLikes", xrpc.ListLikes) |
| @@ -73,7 +73,7 @@ type Server struct { | |||
| 73 | } | 73 | } |
| 74 | 74 | ||
| 75 | func New(dbs *db.Databases, clientID, callbackURL, addr string, scheduler *feed.Scheduler, engine *cluster.Engine, logger *slog.Logger) *Server { | 75 | func New(dbs *db.Databases, clientID, callbackURL, addr string, scheduler *feed.Scheduler, engine *cluster.Engine, logger *slog.Logger) *Server { |
| 76 | - oauthStore := db.NewOAuthStore(dbs.Users) | 76 | + oauthStore := db.NewOAuthStore(dbs) |
| 77 | 77 | ||
| 78 | var config oauth.ClientConfig | 78 | var config oauth.ClientConfig |
| 79 | if clientID == "" { | 79 | if clientID == "" { |
| @@ -202,7 +202,7 @@ func (s *Server) setupRoutes() { | |||
| 202 | s.router.Post("/auth/logout", s.handleAuthLogout) | 202 | s.router.Post("/auth/logout", s.handleAuthLogout) |
| 203 | s.router.Get("/oauth/client-metadata", s.handleOAuthClientMetadata) | 203 | s.router.Get("/oauth/client-metadata", s.handleOAuthClientMetadata) |
| 204 | 204 | ||
| 205 | - xrpc := atproto.NewXRPCHandler(s.dbs.Articles.DB, s.engine) | 205 | + xrpc := atproto.NewXRPCHandler(s.dbs.DB(), s.engine) |
| 206 | s.router.Get("/xrpc/at.glean.listSubscriptions", xrpc.ListSubscriptions) | 206 | s.router.Get("/xrpc/at.glean.listSubscriptions", xrpc.ListSubscriptions) |
| 207 | s.router.Get("/xrpc/at.glean.listAnnotations", xrpc.ListAnnotations) | 207 | s.router.Get("/xrpc/at.glean.listAnnotations", xrpc.ListAnnotations) |
| 208 | s.router.Get("/xrpc/at.glean.listLikes", xrpc.ListLikes) | 208 | s.router.Get("/xrpc/at.glean.listLikes", xrpc.ListLikes) |
modified
main.go +1 -1 | @@ -47,7 +47,7 @@ func main() { | ||
| 47 | 47 | storeAdapter := db.NewFeedStoreAdapter(dbs.Articles) |
| 48 | 48 | scheduler := feed.NewScheduler(storeAdapter, logger, *fetchInterval, 30*time.Minute) |
| 49 | 49 | |
| 50 | - engine := cluster.NewEngine(dbs.Users.DB, logger) | |
| 50 | + engine := cluster.NewEngine(dbs.DB(), logger) | |
| 51 | 51 | |
| 52 | 52 | srv := server.New(dbs, clientID, callbackURL, *addr, scheduler, engine, logger) |
| 53 | 53 | |
| @@ -47,7 +47,7 @@ func main() { | |||
| 47 | storeAdapter := db.NewFeedStoreAdapter(dbs.Articles) | 47 | storeAdapter := db.NewFeedStoreAdapter(dbs.Articles) |
| 48 | scheduler := feed.NewScheduler(storeAdapter, logger, *fetchInterval, 30*time.Minute) | 48 | scheduler := feed.NewScheduler(storeAdapter, logger, *fetchInterval, 30*time.Minute) |
| 49 | 49 | ||
| 50 | - engine := cluster.NewEngine(dbs.Users.DB, logger) | 50 | + engine := cluster.NewEngine(dbs.DB(), logger) |
| 51 | 51 | ||
| 52 | srv := server.New(dbs, clientID, callbackURL, *addr, scheduler, engine, logger) | 52 | srv := server.New(dbs, clientID, callbackURL, *addr, scheduler, engine, logger) |
| 53 | 53 | ||