From 250998405a4bce28a2b6136a244703e5c85fe351 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 24 May 2024 20:07:38 -0500 Subject: [PATCH 1/3] Rework database Conf to easily get the dialect --- src/config.ts | 40 ++++++++++++++------------------ src/db/DittoDB.ts | 9 +++---- src/db/adapters/DittoPostgres.ts | 2 +- src/db/adapters/DittoSQLite.ts | 4 ++-- src/storages/EventsDB.ts | 11 +-------- 5 files changed, 24 insertions(+), 42 deletions(-) diff --git a/src/config.ts b/src/config.ts index cc14998..d8e322d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -61,27 +61,6 @@ class Conf { static get externalDomain() { return Deno.env.get('NOSTR_EXTERNAL') || Conf.localDomain; } - /** Path to the main SQLite database which stores users, events, and more. */ - static get dbPath() { - if (Deno.env.get('DATABASE_URL') === 'sqlite://:memory:') { - return ':memory:'; - } - - const { host, pathname } = Conf.databaseUrl; - - if (!pathname) return ''; - - // Get relative path. - if (host === '') { - return pathname; - } else if (host === '.') { - return pathname; - } else if (host) { - return host + pathname; - } - - return ''; - } /** * Heroku-style database URL. This is used in production to connect to the * database. @@ -92,9 +71,24 @@ class Conf { * protocol://username:password@host:port/database_name * ``` */ - static get databaseUrl(): url.UrlWithStringQuery { - return url.parse(Deno.env.get('DATABASE_URL') ?? 'sqlite://data/db.sqlite3'); + static get databaseUrl(): string { + return Deno.env.get('DATABASE_URL') ?? 'sqlite://data/db.sqlite3'; } + static db = { + get url(): url.UrlWithStringQuery { + return url.parse(Deno.env.get('DATABASE_URL') ?? 'sqlite://data/db.sqlite3'); + }, + get dialect(): 'sqlite' | 'postgres' | undefined { + switch (Conf.db.url.protocol) { + case 'sqlite:': + return 'sqlite'; + case 'postgres:': + case 'postgresql:': + return 'postgres'; + } + return undefined; + }, + }; /** Character limit to enforce for posts made through Mastodon API. */ static get postCharLimit() { return Number(Deno.env.get('POST_CHAR_LIMIT') || 5000); diff --git a/src/db/DittoDB.ts b/src/db/DittoDB.ts index fbca18d..d06b331 100644 --- a/src/db/DittoDB.ts +++ b/src/db/DittoDB.ts @@ -19,16 +19,13 @@ export class DittoDB { } static async _getInstance(): Promise> { - const { databaseUrl } = Conf; - let kysely: Kysely; - switch (databaseUrl.protocol) { - case 'sqlite:': + switch (Conf.db.dialect) { + case 'sqlite': kysely = await DittoSQLite.getInstance(); break; - case 'postgres:': - case 'postgresql:': + case 'postgres': kysely = await DittoPostgres.getInstance(); break; default: diff --git a/src/db/adapters/DittoPostgres.ts b/src/db/adapters/DittoPostgres.ts index d0abbf9..bfecd92 100644 --- a/src/db/adapters/DittoPostgres.ts +++ b/src/db/adapters/DittoPostgres.ts @@ -19,7 +19,7 @@ export class DittoPostgres { // @ts-ignore mismatched kysely versions probably createDriver() { return new PostgreSQLDriver( - { connectionString: Deno.env.get('DATABASE_URL') }, + { connectionString: Conf.databaseUrl }, Conf.pg.poolSize, ); }, diff --git a/src/db/adapters/DittoSQLite.ts b/src/db/adapters/DittoSQLite.ts index fe225a2..d412ca3 100644 --- a/src/db/adapters/DittoSQLite.ts +++ b/src/db/adapters/DittoSQLite.ts @@ -36,11 +36,11 @@ export class DittoSQLite { /** Get the relative or absolute path based on the `DATABASE_URL`. */ static get path() { - if (Deno.env.get('DATABASE_URL') === 'sqlite://:memory:') { + if (Conf.databaseUrl === 'sqlite://:memory:') { return ':memory:'; } - const { host, pathname } = Conf.databaseUrl; + const { host, pathname } = Conf.db.url; if (!pathname) return ''; diff --git a/src/storages/EventsDB.ts b/src/storages/EventsDB.ts index a550f39..1c52f40 100644 --- a/src/storages/EventsDB.ts +++ b/src/storages/EventsDB.ts @@ -42,17 +42,8 @@ class EventsDB implements NStore { }; constructor(private kysely: Kysely) { - let fts: 'sqlite' | 'postgres' | undefined; - - if (Conf.databaseUrl.protocol === 'sqlite:') { - fts = 'sqlite'; - } - if (['postgres:', 'postgresql:'].includes(Conf.databaseUrl.protocol!)) { - fts = 'postgres'; - } - this.store = new NDatabase(kysely, { - fts, + fts: Conf.db.dialect, indexTags: EventsDB.indexTags, searchText: EventsDB.searchText, }); From 04018015c57939b0874940867a60fb8c2677454e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 24 May 2024 20:11:22 -0500 Subject: [PATCH 2/3] stats: fix race conditions (on Postgres) --- src/utils/stats.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/utils/stats.ts b/src/utils/stats.ts index 61d5e9a..0b0eb7e 100644 --- a/src/utils/stats.ts +++ b/src/utils/stats.ts @@ -6,6 +6,7 @@ import { SetRequired } from 'type-fest'; import { DittoTables } from '@/db/DittoTables.ts'; import { getTagSet } from '@/utils/tags.ts'; +import { Conf } from '@/config.ts'; interface UpdateStatsOpts { kysely: Kysely; @@ -153,8 +154,16 @@ export async function updateAuthorStats( notes_count: 0, }; - const prev = await getAuthorStats(kysely, pubkey); + let query = kysely + .selectFrom('author_stats') + .selectAll() + .where('pubkey', '=', pubkey); + if (Conf.db.dialect === 'postgres') { + query = query.forUpdate(); + } + + const prev = await query.executeTakeFirst(); const stats = fn(prev ?? empty); if (prev) { @@ -195,8 +204,16 @@ export async function updateEventStats( reactions: '{}', }; - const prev = await getEventStats(kysely, eventId); + let query = kysely + .selectFrom('event_stats') + .selectAll() + .where('event_id', '=', eventId); + if (Conf.db.dialect === 'postgres') { + query = query.forUpdate(); + } + + const prev = await query.executeTakeFirst(); const stats = fn(prev ?? empty); if (prev) { From 4330cae62610022a7d875d4c077a722a3f2c5808 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 24 May 2024 20:16:51 -0500 Subject: [PATCH 3/3] Fix Conf.db in migrations --- src/db/migrations/002_events_fts.ts | 2 +- src/db/migrations/019_ndatabase_schema.ts | 4 ++-- src/db/migrations/020_pgfts.ts | 4 ++-- src/db/migrations/021_pgfts_index.ts | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/db/migrations/002_events_fts.ts b/src/db/migrations/002_events_fts.ts index ffaf5fb..56abab5 100644 --- a/src/db/migrations/002_events_fts.ts +++ b/src/db/migrations/002_events_fts.ts @@ -3,7 +3,7 @@ import { Kysely, sql } from 'kysely'; import { Conf } from '@/config.ts'; export async function up(db: Kysely): Promise { - if (Conf.databaseUrl.protocol === 'sqlite:') { + if (Conf.db.dialect === 'sqlite') { await sql`CREATE VIRTUAL TABLE events_fts USING fts5(id, content)`.execute(db); } } diff --git a/src/db/migrations/019_ndatabase_schema.ts b/src/db/migrations/019_ndatabase_schema.ts index 94378f0..31b86cd 100644 --- a/src/db/migrations/019_ndatabase_schema.ts +++ b/src/db/migrations/019_ndatabase_schema.ts @@ -7,7 +7,7 @@ export async function up(db: Kysely): Promise { await db.schema.alterTable('tags').renameTo('nostr_tags').execute(); await db.schema.alterTable('nostr_tags').renameColumn('tag', 'name').execute(); - if (Conf.databaseUrl.protocol === 'sqlite:') { + if (Conf.db.dialect === 'sqlite') { await db.schema.dropTable('events_fts').execute(); await sql`CREATE VIRTUAL TABLE nostr_fts5 USING fts5(event_id, content)`.execute(db); } @@ -18,7 +18,7 @@ export async function down(db: Kysely): Promise { await db.schema.alterTable('nostr_tags').renameTo('tags').execute(); await db.schema.alterTable('tags').renameColumn('name', 'tag').execute(); - if (Conf.databaseUrl.protocol === 'sqlite:') { + if (Conf.db.dialect === 'sqlite') { await db.schema.dropTable('nostr_fts5').execute(); await sql`CREATE VIRTUAL TABLE events_fts USING fts5(id, content)`.execute(db); } diff --git a/src/db/migrations/020_pgfts.ts b/src/db/migrations/020_pgfts.ts index 8b3cfa0..835de11 100644 --- a/src/db/migrations/020_pgfts.ts +++ b/src/db/migrations/020_pgfts.ts @@ -3,7 +3,7 @@ import { Kysely, sql } from 'kysely'; import { Conf } from '@/config.ts'; export async function up(db: Kysely): Promise { - if (['postgres:', 'postgresql:'].includes(Conf.databaseUrl.protocol!)) { + if (Conf.db.dialect === 'postgres') { await db.schema.createTable('nostr_pgfts') .ifNotExists() .addColumn('event_id', 'text', (c) => c.primaryKey().references('nostr_events.id').onDelete('cascade')) @@ -13,7 +13,7 @@ export async function up(db: Kysely): Promise { } export async function down(db: Kysely): Promise { - if (['postgres:', 'postgresql:'].includes(Conf.databaseUrl.protocol!)) { + if (Conf.db.dialect === 'postgres') { await db.schema.dropTable('nostr_pgfts').ifExists().execute(); } } diff --git a/src/db/migrations/021_pgfts_index.ts b/src/db/migrations/021_pgfts_index.ts index d18d110..4b83499 100644 --- a/src/db/migrations/021_pgfts_index.ts +++ b/src/db/migrations/021_pgfts_index.ts @@ -3,7 +3,7 @@ import { Kysely } from 'kysely'; import { Conf } from '@/config.ts'; export async function up(db: Kysely): Promise { - if (['postgres:', 'postgresql:'].includes(Conf.databaseUrl.protocol!)) { + if (Conf.db.dialect === 'postgres') { await db.schema .createIndex('nostr_pgfts_gin_search_vec') .ifNotExists() @@ -15,7 +15,7 @@ export async function up(db: Kysely): Promise { } export async function down(db: Kysely): Promise { - if (['postgres:', 'postgresql:'].includes(Conf.databaseUrl.protocol!)) { + if (Conf.db.dialect === 'postgres') { await db.schema.dropIndex('nostr_pgfts_gin_search_vec').ifExists().execute(); } }