UNPKG

@naturalcycles/mysql-lib

Version:

MySQL client implementing CommonDB interface

146 lines (118 loc) 4.72 kB
import { Transform } from 'node:stream' import type { CommonDBCreateOptions } from '@naturalcycles/db-lib' import type { CommonKeyValueDB, IncrementTuple, KeyValueDBTuple } from '@naturalcycles/db-lib/kv' import { commonKeyValueDBFullSupport } from '@naturalcycles/db-lib/kv' import { AppError } from '@naturalcycles/js-lib/error/error.util.js' import { pMap } from '@naturalcycles/js-lib/promise/pMap.js' import type { ObjectWithId } from '@naturalcycles/js-lib/types' import type { ReadableTyped } from '@naturalcycles/nodejs-lib/stream' import type { QueryOptions } from 'mysql' import type { MysqlDBCfg } from './mysql.db.js' import { MysqlDB } from './mysql.db.js' interface KeyValueObject { id: string v: Buffer } export class MySQLKeyValueDB implements CommonKeyValueDB { constructor(public cfg: MysqlDBCfg) { this.db = new MysqlDB(this.cfg) } db: MysqlDB support = { ...commonKeyValueDBFullSupport, increment: false, } async ping(): Promise<void> { await this.db.ping() } async close(): Promise<void> { await this.db.close() } async createTable(table: string, opt: CommonDBCreateOptions = {}): Promise<void> { if (opt.dropIfExists) await this.dropTable(table) // On blob sizes: https://tableplus.com/blog/2019/10/tinyblob-blob-mediumblob-longblob.html // LONGBLOB supports up to 4gb // MEDIUMBLOB up to 16Mb const sql = `create table ${table} (id VARCHAR(64) PRIMARY KEY, v LONGBLOB NOT NULL)` this.db.cfg.logger.log(sql) await this.db.runSQL({ sql }) } async getByIds(table: string, ids: string[]): Promise<KeyValueDBTuple[]> { if (!ids.length) return [] const sql = `SELECT id,v FROM ${table} where id in (${ids.map(id => `"${id}"`).join(',')})` const rows = await this.db.runSQL<KeyValueObject[]>({ sql }) return rows.map(({ id, v }) => [id, v]) } /** * Use with caution! */ async dropTable(table: string): Promise<void> { await this.db.runSQL({ sql: `DROP TABLE IF EXISTS ${table}` }) } async deleteByIds(table: string, ids: string[]): Promise<void> { const sql = `DELETE FROM ${table} WHERE id in (${ids.map(id => `"${id}"`).join(',')})` if (this.cfg.logSQL) this.db.cfg.logger.log(sql) await this.db.runSQL({ sql }) } async saveBatch(table: string, entries: KeyValueDBTuple[]): Promise<void> { const statements: QueryOptions[] = entries.map(([id, buf]) => { return { sql: `INSERT INTO ${table} (id, v) VALUES (?, ?)`, values: [id, buf], } }) await pMap(statements, async statement => { if (this.cfg.debug) this.db.cfg.logger.log(statement.sql) await this.db.runSQL(statement) }) } streamIds(table: string, limit?: number): ReadableTyped<string> { let sql = `SELECT id FROM ${table}` if (limit) sql += ` LIMIT ${limit}` if (this.cfg.logSQL) this.db.cfg.logger.log(`stream: ${sql}`) // todo: this is nice, but `mysql` package uses `readable-stream@2` which is not compatible with `node:stream` iterable helpers // return (this.db.pool().query(sql).stream() as ReadableTyped<ObjectWithId>).map(row => row.id) return this.db .pool() .query(sql) .stream() .pipe( new Transform({ objectMode: true, transform(row: ObjectWithId, _encoding, cb) { cb(null, row.id) }, }), ) } streamValues(table: string, limit?: number): ReadableTyped<Buffer> { let sql = `SELECT v FROM ${table}` if (limit) sql += ` LIMIT ${limit}` if (this.cfg.logSQL) this.db.cfg.logger.log(`stream: ${sql}`) return (this.db.pool().query(sql).stream() as ReadableTyped<{ v: Buffer }>).map(row => row.v) } streamEntries(table: string, limit?: number): ReadableTyped<KeyValueDBTuple> { let sql = `SELECT id,v FROM ${table}` if (limit) sql += ` LIMIT ${limit}` if (this.cfg.logSQL) this.db.cfg.logger.log(`stream: ${sql}`) return (this.db.pool().query(sql).stream() as ReadableTyped<KeyValueObject>).map(row => [ row.id, row.v, ]) } async beginTransaction(): Promise<void> { await this.db.runSQL({ sql: `BEGIN TRANSACTION` }) } async endTransaction(): Promise<void> { await this.db.runSQL({ sql: `END TRANSACTION` }) } async count(table: string): Promise<number> { const sql = `SELECT count(*) as cnt FROM ${table}` if (this.cfg.logSQL) this.db.cfg.logger.log(sql) const rows = await this.db.runSQL<{ cnt: number }[]>({ sql }) return rows[0]!.cnt } async incrementBatch(_table: string, _entries: IncrementTuple[]): Promise<IncrementTuple[]> { throw new AppError('MySQLKeyValueDB.incrementBatch() is not implemented') } }