Merge branch 'postgres' into 'main'
Support Postgres See merge request soapbox-pub/ditto!167
This commit is contained in:
commit
aeaa236518
|
@ -14,7 +14,9 @@
|
||||||
"imports": {
|
"imports": {
|
||||||
"@/": "./src/",
|
"@/": "./src/",
|
||||||
"@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.9.7",
|
"@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.9.7",
|
||||||
"~/fixtures/": "./fixtures/"
|
"~/fixtures/": "./fixtures/",
|
||||||
|
"kysely": "npm:kysely@^0.26.3",
|
||||||
|
"kysely_deno_postgres": "https://deno.land/x/kysely_deno_postgres@v0.4.0/mod.ts"
|
||||||
},
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
"include": ["src/", "scripts/"],
|
"include": ["src/", "scripts/"],
|
||||||
|
|
91
src/db.ts
91
src/db.ts
|
@ -1,93 +1,10 @@
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
import { FileMigrationProvider, Kysely, Migrator, PolySqliteDialect } from '@/deps.ts';
|
import { DittoDB } from '@/db/DittoDB.ts';
|
||||||
import { Conf } from '@/config.ts';
|
import { FileMigrationProvider, Migrator } from '@/deps.ts';
|
||||||
import { setPragma } from '@/pragma.ts';
|
|
||||||
import SqliteWorker from '@/workers/sqlite.ts';
|
|
||||||
|
|
||||||
interface DittoDB {
|
const db = await DittoDB.getInstance();
|
||||||
events: EventRow;
|
|
||||||
events_fts: EventFTSRow;
|
|
||||||
tags: TagRow;
|
|
||||||
relays: RelayRow;
|
|
||||||
unattached_media: UnattachedMediaRow;
|
|
||||||
author_stats: AuthorStatsRow;
|
|
||||||
event_stats: EventStatsRow;
|
|
||||||
pubkey_domains: PubkeyDomainRow;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AuthorStatsRow {
|
|
||||||
pubkey: string;
|
|
||||||
followers_count: number;
|
|
||||||
following_count: number;
|
|
||||||
notes_count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EventStatsRow {
|
|
||||||
event_id: string;
|
|
||||||
replies_count: number;
|
|
||||||
reposts_count: number;
|
|
||||||
reactions_count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EventRow {
|
|
||||||
id: string;
|
|
||||||
kind: number;
|
|
||||||
pubkey: string;
|
|
||||||
content: string;
|
|
||||||
created_at: number;
|
|
||||||
tags: string;
|
|
||||||
sig: string;
|
|
||||||
deleted_at: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EventFTSRow {
|
|
||||||
id: string;
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TagRow {
|
|
||||||
tag: string;
|
|
||||||
value: string;
|
|
||||||
event_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RelayRow {
|
|
||||||
url: string;
|
|
||||||
domain: string;
|
|
||||||
active: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UnattachedMediaRow {
|
|
||||||
id: string;
|
|
||||||
pubkey: string;
|
|
||||||
url: string;
|
|
||||||
data: string;
|
|
||||||
uploaded_at: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PubkeyDomainRow {
|
|
||||||
pubkey: string;
|
|
||||||
domain: string;
|
|
||||||
last_updated_at: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sqliteWorker = new SqliteWorker();
|
|
||||||
await sqliteWorker.open(Conf.dbPath);
|
|
||||||
|
|
||||||
const db = new Kysely<DittoDB>({
|
|
||||||
dialect: new PolySqliteDialect({
|
|
||||||
database: sqliteWorker,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set PRAGMA values.
|
|
||||||
await Promise.all([
|
|
||||||
setPragma(db, 'synchronous', 'normal'),
|
|
||||||
setPragma(db, 'temp_store', 'memory'),
|
|
||||||
setPragma(db, 'mmap_size', Conf.sqlite.mmapSize),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const migrator = new Migrator({
|
const migrator = new Migrator({
|
||||||
db,
|
db,
|
||||||
|
@ -120,4 +37,4 @@ async function migrate() {
|
||||||
|
|
||||||
await migrate();
|
await migrate();
|
||||||
|
|
||||||
export { type AuthorStatsRow, db, type DittoDB, type EventRow, type EventStatsRow, type TagRow };
|
export { db };
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Conf } from '@/config.ts';
|
||||||
|
import { DittoPostgres } from '@/db/adapters/DittoPostgres.ts';
|
||||||
|
import { DittoSQLite } from '@/db/adapters/DittoSQLite.ts';
|
||||||
|
import { DittoTables } from '@/db/DittoTables.ts';
|
||||||
|
import { Kysely } from '@/deps.ts';
|
||||||
|
|
||||||
|
export class DittoDB {
|
||||||
|
static getInstance(): Promise<Kysely<DittoTables>> {
|
||||||
|
const { databaseUrl } = Conf;
|
||||||
|
|
||||||
|
switch (databaseUrl.protocol) {
|
||||||
|
case 'sqlite:':
|
||||||
|
return DittoSQLite.getInstance();
|
||||||
|
case 'postgres:':
|
||||||
|
return DittoPostgres.getInstance();
|
||||||
|
default:
|
||||||
|
throw new Error('Unsupported database URL.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
export interface DittoTables {
|
||||||
|
events: EventRow;
|
||||||
|
events_fts: EventFTSRow;
|
||||||
|
tags: TagRow;
|
||||||
|
relays: RelayRow;
|
||||||
|
unattached_media: UnattachedMediaRow;
|
||||||
|
author_stats: AuthorStatsRow;
|
||||||
|
event_stats: EventStatsRow;
|
||||||
|
pubkey_domains: PubkeyDomainRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthorStatsRow {
|
||||||
|
pubkey: string;
|
||||||
|
followers_count: number;
|
||||||
|
following_count: number;
|
||||||
|
notes_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventStatsRow {
|
||||||
|
event_id: string;
|
||||||
|
replies_count: number;
|
||||||
|
reposts_count: number;
|
||||||
|
reactions_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventRow {
|
||||||
|
id: string;
|
||||||
|
kind: number;
|
||||||
|
pubkey: string;
|
||||||
|
content: string;
|
||||||
|
created_at: number;
|
||||||
|
tags: string;
|
||||||
|
sig: string;
|
||||||
|
deleted_at: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventFTSRow {
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TagRow {
|
||||||
|
tag: string;
|
||||||
|
value: string;
|
||||||
|
event_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RelayRow {
|
||||||
|
url: string;
|
||||||
|
domain: string;
|
||||||
|
active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UnattachedMediaRow {
|
||||||
|
id: string;
|
||||||
|
pubkey: string;
|
||||||
|
url: string;
|
||||||
|
data: string;
|
||||||
|
uploaded_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PubkeyDomainRow {
|
||||||
|
pubkey: string;
|
||||||
|
domain: string;
|
||||||
|
last_updated_at: number;
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { Kysely, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler } from 'kysely';
|
||||||
|
import { PostgreSQLDriver } from 'kysely_deno_postgres';
|
||||||
|
|
||||||
|
import { DittoTables } from '@/db/DittoTables.ts';
|
||||||
|
|
||||||
|
export class DittoPostgres {
|
||||||
|
static db: Kysely<DittoTables> | undefined;
|
||||||
|
|
||||||
|
// deno-lint-ignore require-await
|
||||||
|
static async getInstance(): Promise<Kysely<DittoTables>> {
|
||||||
|
if (!this.db) {
|
||||||
|
this.db = new Kysely({
|
||||||
|
dialect: {
|
||||||
|
createAdapter() {
|
||||||
|
return new PostgresAdapter();
|
||||||
|
},
|
||||||
|
// @ts-ignore mismatched kysely versions probably
|
||||||
|
createDriver() {
|
||||||
|
return new PostgreSQLDriver({
|
||||||
|
connectionString: Deno.env.get('DATABASE_URL'),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
createIntrospector(db: Kysely<unknown>) {
|
||||||
|
return new PostgresIntrospector(db);
|
||||||
|
},
|
||||||
|
createQueryCompiler() {
|
||||||
|
return new PostgresQueryCompiler();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.db;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { Conf } from '@/config.ts';
|
||||||
|
import { DittoTables } from '@/db/DittoTables.ts';
|
||||||
|
import { Kysely, PolySqliteDialect, sql } from '@/deps.ts';
|
||||||
|
import SqliteWorker from '@/workers/sqlite.ts';
|
||||||
|
|
||||||
|
export class DittoSQLite {
|
||||||
|
static db: Kysely<DittoTables> | undefined;
|
||||||
|
|
||||||
|
static async getInstance(): Promise<Kysely<DittoTables>> {
|
||||||
|
if (!this.db) {
|
||||||
|
const sqliteWorker = new SqliteWorker();
|
||||||
|
await sqliteWorker.open(this.path);
|
||||||
|
|
||||||
|
this.db = new Kysely<DittoTables>({
|
||||||
|
dialect: new PolySqliteDialect({
|
||||||
|
database: sqliteWorker,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set PRAGMA values.
|
||||||
|
await Promise.all([
|
||||||
|
sql`PRAGMA synchronous = normal`.execute(this.db),
|
||||||
|
sql`PRAGMA temp_store = memory`.execute(this.db),
|
||||||
|
sql`PRAGMA foreign_keys = ON`.execute(this.db),
|
||||||
|
sql`PRAGMA auto_vacuum = FULL`.execute(this.db),
|
||||||
|
sql`PRAGMA journal_mode = WAL`.execute(this.db),
|
||||||
|
sql.raw(`PRAGMA mmap_size = ${Conf.sqlite.mmapSize}`).execute(this.db),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return this.db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the relative or absolute path based on the `DATABASE_URL`. */
|
||||||
|
static get path() {
|
||||||
|
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 '';
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { Kysely, sql } from '@/deps.ts';
|
import { Kysely } from '@/deps.ts';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
|
@ -21,13 +21,6 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
.addColumn('event_id', 'text', (col) => col.notNull())
|
.addColumn('event_id', 'text', (col) => col.notNull())
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.createTable('users')
|
|
||||||
.addColumn('pubkey', 'text', (col) => col.primaryKey())
|
|
||||||
.addColumn('username', 'text', (col) => col.notNull().unique())
|
|
||||||
.addColumn('inserted_at', 'datetime', (col) => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`))
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
await db.schema
|
||||||
.createIndex('idx_events_kind')
|
.createIndex('idx_events_kind')
|
||||||
.on('events')
|
.on('events')
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
|
import { Conf } from '@/config.ts';
|
||||||
import { Kysely, sql } from '@/deps.ts';
|
import { Kysely, sql } from '@/deps.ts';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
if (Conf.databaseUrl.protocol === 'sqlite:') {
|
||||||
await sql`CREATE VIRTUAL TABLE events_fts USING fts5(id, content)`.execute(db);
|
await sql`CREATE VIRTUAL TABLE events_fts USING fts5(id, content)`.execute(db);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
await db.schema.dropTable('events_fts').execute();
|
await db.schema.dropTable('events_fts').ifExists().execute();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import { Kysely } from '@/deps.ts';
|
import { Kysely } from '@/deps.ts';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(_db: Kysely<any>): Promise<void> {
|
||||||
await db.schema
|
|
||||||
.alterTable('users')
|
|
||||||
.addColumn('admin', 'boolean', (col) => col.defaultTo(false))
|
|
||||||
.execute();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
|
|
@ -1,17 +1,6 @@
|
||||||
import { Kysely } from '@/deps.ts';
|
import { Kysely } from '@/deps.ts';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(_db: Kysely<any>): Promise<void> {
|
||||||
await db.schema
|
|
||||||
.createIndex('idx_users_pubkey')
|
|
||||||
.on('users')
|
|
||||||
.column('pubkey')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.createIndex('idx_users_username')
|
|
||||||
.on('users')
|
|
||||||
.column('username')
|
|
||||||
.execute();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
import { Kysely, sql } from '@/deps.ts';
|
import { Kysely } from '@/deps.ts';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(_db: Kysely<any>): Promise<void> {
|
||||||
await sql`PRAGMA foreign_keys = ON`.execute(db);
|
|
||||||
await sql`PRAGMA auto_vacuum = FULL`.execute(db);
|
|
||||||
await sql`VACUUM`.execute(db);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(_db: Kysely<any>): Promise<void> {
|
||||||
await sql`PRAGMA foreign_keys = OFF`.execute(db);
|
|
||||||
await sql`PRAGMA auto_vacuum = NONE`.execute(db);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Kysely, sql } from '@/deps.ts';
|
import { Kysely } from '@/deps.ts';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
await db.schema
|
await db.schema
|
||||||
|
@ -7,7 +7,7 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
.addColumn('pubkey', 'text', (c) => c.notNull())
|
.addColumn('pubkey', 'text', (c) => c.notNull())
|
||||||
.addColumn('url', 'text', (c) => c.notNull())
|
.addColumn('url', 'text', (c) => c.notNull())
|
||||||
.addColumn('data', 'text', (c) => c.notNull())
|
.addColumn('data', 'text', (c) => c.notNull())
|
||||||
.addColumn('uploaded_at', 'datetime', (c) => c.notNull().defaultTo(sql`CURRENT_TIMESTAMP`))
|
.addColumn('uploaded_at', 'bigint', (c) => c.notNull())
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await db.schema
|
await db.schema
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { Kysely, sql } from '@/deps.ts';
|
import { Kysely } from '@/deps.ts';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(_db: Kysely<any>): Promise<void> {
|
||||||
await sql`PRAGMA journal_mode = WAL`.execute(db);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(_db: Kysely<any>): Promise<void> {
|
||||||
await sql`PRAGMA journal_mode = DELETE`.execute(db);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Kysely } from '@/deps.ts';
|
import { Kysely } from '@/deps.ts';
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
await db.schema.dropTable('users').execute();
|
await db.schema.dropTable('users').ifExists().execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(_db: Kysely<any>): Promise<void> {
|
export async function down(_db: Kysely<any>): Promise<void> {
|
||||||
|
|
|
@ -7,14 +7,14 @@ interface UnattachedMedia {
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
url: string;
|
url: string;
|
||||||
data: MediaData;
|
data: MediaData;
|
||||||
uploaded_at: Date;
|
uploaded_at: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Add unattached media into the database. */
|
/** Add unattached media into the database. */
|
||||||
async function insertUnattachedMedia(media: Omit<UnattachedMedia, 'id' | 'uploaded_at'>) {
|
async function insertUnattachedMedia(media: Omit<UnattachedMedia, 'id' | 'uploaded_at'>) {
|
||||||
const result = {
|
const result = {
|
||||||
id: uuid62.v4(),
|
id: uuid62.v4(),
|
||||||
uploaded_at: new Date(),
|
uploaded_at: Date.now(),
|
||||||
...media,
|
...media,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ function selectUnattachedMediaQuery() {
|
||||||
function getUnattachedMedia(until: Date) {
|
function getUnattachedMedia(until: Date) {
|
||||||
return selectUnattachedMediaQuery()
|
return selectUnattachedMediaQuery()
|
||||||
.leftJoin('tags', 'unattached_media.url', 'tags.value')
|
.leftJoin('tags', 'unattached_media.url', 'tags.value')
|
||||||
.where('uploaded_at', '<', until)
|
.where('uploaded_at', '<', until.getTime())
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { type Kysely, sql } from '@/deps.ts';
|
|
||||||
|
|
||||||
/** Set the PRAGMA and then read back its value to confirm. */
|
|
||||||
function setPragma(db: Kysely<any>, pragma: string, value: string | number) {
|
|
||||||
return sql.raw(`PRAGMA ${pragma} = ${value}`).execute(db);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get value of PRAGMA from the database. */
|
|
||||||
async function getPragma(db: Kysely<any>, pragma: string) {
|
|
||||||
const result = await sql.raw(`PRAGMA ${pragma}`).execute(db);
|
|
||||||
const row = result.rows[0] as Record<string, unknown> | undefined;
|
|
||||||
return row?.[pragma];
|
|
||||||
}
|
|
||||||
|
|
||||||
export { getPragma, setPragma };
|
|
17
src/stats.ts
17
src/stats.ts
|
@ -1,11 +1,12 @@
|
||||||
import { NostrEvent } from '@nostrify/nostrify';
|
import { NostrEvent } from '@nostrify/nostrify';
|
||||||
import { type AuthorStatsRow, db, type DittoDB, type EventStatsRow } from '@/db.ts';
|
import { db } from '@/db.ts';
|
||||||
|
import { DittoTables } from '@/db/DittoTables.ts';
|
||||||
import { Debug, type InsertQueryBuilder } from '@/deps.ts';
|
import { Debug, type InsertQueryBuilder } from '@/deps.ts';
|
||||||
import { eventsDB } from '@/storages.ts';
|
import { eventsDB } from '@/storages.ts';
|
||||||
import { findReplyTag } from '@/tags.ts';
|
import { findReplyTag } from '@/tags.ts';
|
||||||
|
|
||||||
type AuthorStat = keyof Omit<AuthorStatsRow, 'pubkey'>;
|
type AuthorStat = keyof Omit<DittoTables['author_stats'], 'pubkey'>;
|
||||||
type EventStat = keyof Omit<EventStatsRow, 'event_id'>;
|
type EventStat = keyof Omit<DittoTables['event_stats'], 'event_id'>;
|
||||||
|
|
||||||
type AuthorStatDiff = ['author_stats', pubkey: string, stat: AuthorStat, diff: number];
|
type AuthorStatDiff = ['author_stats', pubkey: string, stat: AuthorStat, diff: number];
|
||||||
type EventStatDiff = ['event_stats', eventId: string, stat: EventStat, diff: number];
|
type EventStatDiff = ['event_stats', eventId: string, stat: EventStat, diff: number];
|
||||||
|
@ -16,7 +17,7 @@ const debug = Debug('ditto:stats');
|
||||||
/** Store stats for the event in LMDB. */
|
/** Store stats for the event in LMDB. */
|
||||||
async function updateStats(event: NostrEvent) {
|
async function updateStats(event: NostrEvent) {
|
||||||
let prev: NostrEvent | undefined;
|
let prev: NostrEvent | undefined;
|
||||||
const queries: InsertQueryBuilder<DittoDB, any, unknown>[] = [];
|
const queries: InsertQueryBuilder<DittoTables, any, unknown>[] = [];
|
||||||
|
|
||||||
// Kind 3 is a special case - replace the count with the new list.
|
// Kind 3 is a special case - replace the count with the new list.
|
||||||
if (event.kind === 3) {
|
if (event.kind === 3) {
|
||||||
|
@ -99,8 +100,8 @@ async function getStatsDiff(event: NostrEvent, prev: NostrEvent | undefined): Pr
|
||||||
|
|
||||||
/** Create an author stats query from the list of diffs. */
|
/** Create an author stats query from the list of diffs. */
|
||||||
function authorStatsQuery(diffs: AuthorStatDiff[]) {
|
function authorStatsQuery(diffs: AuthorStatDiff[]) {
|
||||||
const values: AuthorStatsRow[] = diffs.map(([_, pubkey, stat, diff]) => {
|
const values: DittoTables['author_stats'][] = diffs.map(([_, pubkey, stat, diff]) => {
|
||||||
const row: AuthorStatsRow = {
|
const row: DittoTables['author_stats'] = {
|
||||||
pubkey,
|
pubkey,
|
||||||
followers_count: 0,
|
followers_count: 0,
|
||||||
following_count: 0,
|
following_count: 0,
|
||||||
|
@ -125,8 +126,8 @@ function authorStatsQuery(diffs: AuthorStatDiff[]) {
|
||||||
|
|
||||||
/** Create an event stats query from the list of diffs. */
|
/** Create an event stats query from the list of diffs. */
|
||||||
function eventStatsQuery(diffs: EventStatDiff[]) {
|
function eventStatsQuery(diffs: EventStatDiff[]) {
|
||||||
const values: EventStatsRow[] = diffs.map(([_, event_id, stat, diff]) => {
|
const values: DittoTables['event_stats'][] = diffs.map(([_, event_id, stat, diff]) => {
|
||||||
const row: EventStatsRow = {
|
const row: DittoTables['event_stats'] = {
|
||||||
event_id,
|
event_id,
|
||||||
replies_count: 0,
|
replies_count: 0,
|
||||||
reposts_count: 0,
|
reposts_count: 0,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { NIP50, NostrEvent, NostrFilter, NStore } from '@nostrify/nostrify';
|
import { NIP50, NostrEvent, NostrFilter, NStore } from '@nostrify/nostrify';
|
||||||
import { Conf } from '@/config.ts';
|
import { Conf } from '@/config.ts';
|
||||||
import { type DittoDB } from '@/db.ts';
|
import { DittoTables } from '@/db/DittoTables.ts';
|
||||||
import { Debug, Kysely, type SelectQueryBuilder } from '@/deps.ts';
|
import { Debug, Kysely, type SelectQueryBuilder } from '@/deps.ts';
|
||||||
import { normalizeFilters } from '@/filter.ts';
|
import { normalizeFilters } from '@/filter.ts';
|
||||||
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||||
|
@ -33,7 +33,7 @@ const tagConditions: Record<string, TagCondition> = {
|
||||||
'role': ({ event, count }) => event.kind === 30361 && count === 0,
|
'role': ({ event, count }) => event.kind === 30361 && count === 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
type EventQuery = SelectQueryBuilder<DittoDB, 'events', {
|
type EventQuery = SelectQueryBuilder<DittoTables, 'events', {
|
||||||
id: string;
|
id: string;
|
||||||
tags: string;
|
tags: string;
|
||||||
kind: number;
|
kind: number;
|
||||||
|
@ -58,10 +58,11 @@ type EventQuery = SelectQueryBuilder<DittoDB, 'events', {
|
||||||
|
|
||||||
/** SQLite database storage adapter for Nostr events. */
|
/** SQLite database storage adapter for Nostr events. */
|
||||||
class EventsDB implements NStore {
|
class EventsDB implements NStore {
|
||||||
#db: Kysely<DittoDB>;
|
#db: Kysely<DittoTables>;
|
||||||
#debug = Debug('ditto:db:events');
|
#debug = Debug('ditto:db:events');
|
||||||
|
private protocol = Conf.databaseUrl.protocol;
|
||||||
|
|
||||||
constructor(db: Kysely<DittoDB>) {
|
constructor(db: Kysely<DittoTables>) {
|
||||||
this.#db = db;
|
this.#db = db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,8 +83,10 @@ class EventsDB implements NStore {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const protocol = this.protocol;
|
||||||
/** Add search data to the FTS table. */
|
/** Add search data to the FTS table. */
|
||||||
async function indexSearch() {
|
async function indexSearch() {
|
||||||
|
if (protocol !== 'sqlite:') return;
|
||||||
const searchContent = buildSearchContent(event);
|
const searchContent = buildSearchContent(event);
|
||||||
if (!searchContent) return;
|
if (!searchContent) return;
|
||||||
await trx.insertInto('events_fts')
|
await trx.insertInto('events_fts')
|
||||||
|
@ -143,7 +146,7 @@ class EventsDB implements NStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Build the query for a filter. */
|
/** Build the query for a filter. */
|
||||||
getFilterQuery(db: Kysely<DittoDB>, filter: NostrFilter): EventQuery {
|
getFilterQuery(db: Kysely<DittoTables>, filter: NostrFilter): EventQuery {
|
||||||
let query = db
|
let query = db
|
||||||
.selectFrom('events')
|
.selectFrom('events')
|
||||||
.select([
|
.select([
|
||||||
|
@ -194,7 +197,7 @@ class EventsDB implements NStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.search) {
|
if (filter.search && this.protocol === 'sqlite:') {
|
||||||
query = query
|
query = query
|
||||||
.innerJoin('events_fts', 'events_fts.id', 'events.id')
|
.innerJoin('events_fts', 'events_fts.id', 'events.id')
|
||||||
.where('events_fts.content', 'match', JSON.stringify(filter.search));
|
.where('events_fts.content', 'match', JSON.stringify(filter.search));
|
||||||
|
@ -315,7 +318,7 @@ class EventsDB implements NStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Delete events from each table. Should be run in a transaction! */
|
/** Delete events from each table. Should be run in a transaction! */
|
||||||
async deleteEventsTrx(db: Kysely<DittoDB>, filters: NostrFilter[]) {
|
async deleteEventsTrx(db: Kysely<DittoTables>, filters: NostrFilter[]) {
|
||||||
if (!filters.length) return Promise.resolve();
|
if (!filters.length) return Promise.resolve();
|
||||||
this.#debug('DELETE', JSON.stringify(filters));
|
this.#debug('DELETE', JSON.stringify(filters));
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ export const TrendsWorker = {
|
||||||
CREATE TABLE IF NOT EXISTS tag_usages (
|
CREATE TABLE IF NOT EXISTS tag_usages (
|
||||||
tag TEXT NOT NULL COLLATE NOCASE,
|
tag TEXT NOT NULL COLLATE NOCASE,
|
||||||
pubkey8 TEXT NOT NULL,
|
pubkey8 TEXT NOT NULL,
|
||||||
inserted_at DATETIME NOT NULL
|
inserted_at INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_time_tag ON tag_usages(inserted_at, tag);
|
CREATE INDEX IF NOT EXISTS idx_time_tag ON tag_usages(inserted_at, tag);
|
||||||
|
|
Loading…
Reference in New Issue