Skip to content
This repository was archived by the owner on May 25, 2025. It is now read-only.

Commit 48d0d3f

Browse files
feat: MySQLKeyValueDB
1 parent cf84910 commit 48d0d3f

File tree

4 files changed

+203
-2
lines changed

4 files changed

+203
-2
lines changed

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { getDBAdapter } from './dbAdapter'
22
import { MysqlDB, MysqlDBCfg, MysqlDBOptions, MysqlDBSaveOptions } from './mysql.db'
3+
import { MySQLKeyValueDB } from './mysqlKeyValueDB'
34
import { jsonSchemaToMySQLDDL } from './schema/mysql.schema.util'
45

56
export type { MysqlDBCfg, MysqlDBOptions, MysqlDBSaveOptions }
67

7-
export { MysqlDB, jsonSchemaToMySQLDDL, getDBAdapter }
8+
export { MysqlDB, MySQLKeyValueDB, jsonSchemaToMySQLDDL, getDBAdapter }

src/mysql.db.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export interface MysqlDBCfg extends PoolConfig {
6060
const BOOLEAN_TYPES = new Set(['TINY', 'TINYINT', 'INT'])
6161
const BOOLEAN_BIT_TYPES = new Set(['BIT'])
6262

63-
const typeCast: TypeCast = (field, next) => {
63+
export const typeCast: TypeCast = (field, next) => {
6464
// cast to boolean
6565
if (field.length === 1) {
6666
if (BOOLEAN_BIT_TYPES.has(field.type)) {

src/mysqlKeyValueDB.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { Transform } from 'stream'
2+
import { CommonDBCreateOptions, CommonKeyValueDB, KeyValueDBTuple } from '@naturalcycles/db-lib'
3+
import { pMap } from '@naturalcycles/js-lib'
4+
import { ReadableTyped } from '@naturalcycles/nodejs-lib'
5+
import { QueryOptions } from 'mysql'
6+
import { MysqlDB, MysqlDBCfg, typeCast } from './mysql.db'
7+
8+
interface KeyValueObject {
9+
id: string
10+
v: Buffer
11+
}
12+
13+
export class MySQLKeyValueDB implements CommonKeyValueDB {
14+
constructor(cfg: MysqlDBCfg = {}) {
15+
this.cfg = {
16+
typeCast,
17+
...cfg,
18+
}
19+
20+
this.db = new MysqlDB(this.cfg)
21+
}
22+
23+
cfg: MysqlDBCfg
24+
25+
db: MysqlDB
26+
27+
async ping(): Promise<void> {
28+
await this.db.ping()
29+
}
30+
31+
async close(): Promise<void> {
32+
await this.db.close()
33+
}
34+
35+
async createTable(table: string, opt: CommonDBCreateOptions = {}): Promise<void> {
36+
if (opt.dropIfExists) await this.dropTable(table)
37+
38+
// On blob sizes: https://tableplus.com/blog/2019/10/tinyblob-blob-mediumblob-longblob.html
39+
// LONGBLOB supports up to 4gb
40+
// MEDIUMBLOB up to 16Mb
41+
const sql = `create table ${table} (id VARCHAR(64) PRIMARY KEY, v LONGBLOB NOT NULL)`
42+
this.db.cfg.logger.log(sql)
43+
await this.db.runSQL({ sql })
44+
}
45+
46+
async getByIds(table: string, ids: string[]): Promise<KeyValueDBTuple[]> {
47+
if (!ids.length) return []
48+
49+
const sql = `SELECT id,v FROM ${table} where id in (${ids.map(id => `"${id}"`).join(',')})`
50+
51+
const rows = await this.db.runSQL<KeyValueObject[]>({ sql })
52+
53+
return rows.map(({ id, v }) => [id, v])
54+
}
55+
56+
/**
57+
* Use with caution!
58+
*/
59+
async dropTable(table: string): Promise<void> {
60+
await this.db.runSQL({ sql: `DROP TABLE IF EXISTS ${table}` })
61+
}
62+
63+
async deleteByIds(table: string, ids: string[]): Promise<void> {
64+
const sql = `DELETE FROM ${table} WHERE id in (${ids.map(id => `"${id}"`).join(',')})`
65+
if (this.cfg.logSQL) this.db.cfg.logger.log(sql)
66+
await this.db.runSQL({ sql })
67+
}
68+
69+
async saveBatch(table: string, entries: KeyValueDBTuple[]): Promise<void> {
70+
const statements: QueryOptions[] = entries.map(([id, buf]) => {
71+
return {
72+
sql: `INSERT INTO ${table} (id, v) VALUES (?, ?)`,
73+
values: [id, buf],
74+
}
75+
})
76+
77+
await pMap(statements, async statement => {
78+
if (this.cfg.debug) this.db.cfg.logger.log(statement.sql)
79+
await this.db.runSQL(statement)
80+
})
81+
}
82+
83+
streamIds(table: string, limit?: number): ReadableTyped<string> {
84+
let sql = `SELECT id FROM ${table}`
85+
if (limit) sql += ` LIMIT ${limit}`
86+
if (this.cfg.logSQL) this.db.cfg.logger.log(`stream: ${sql}`)
87+
88+
return this.db
89+
.pool()
90+
.query(sql)
91+
.stream()
92+
.pipe(
93+
new Transform({
94+
objectMode: true,
95+
transform(row, _, cb) {
96+
cb(null, row.id)
97+
},
98+
}),
99+
)
100+
}
101+
102+
streamValues(table: string, limit?: number): ReadableTyped<Buffer> {
103+
let sql = `SELECT v FROM ${table}`
104+
if (limit) sql += ` LIMIT ${limit}`
105+
if (this.cfg.logSQL) this.db.cfg.logger.log(`stream: ${sql}`)
106+
107+
return this.db
108+
.pool()
109+
.query(sql)
110+
.stream()
111+
.pipe(
112+
new Transform({
113+
objectMode: true,
114+
transform(row, _, cb) {
115+
cb(null, row.v)
116+
},
117+
}),
118+
)
119+
}
120+
121+
streamEntries(table: string, limit?: number): ReadableTyped<KeyValueDBTuple> {
122+
let sql = `SELECT id,v FROM ${table}`
123+
if (limit) sql += ` LIMIT ${limit}`
124+
if (this.cfg.logSQL) this.db.cfg.logger.log(`stream: ${sql}`)
125+
126+
return this.db
127+
.pool()
128+
.query(sql)
129+
.stream()
130+
.pipe(
131+
new Transform({
132+
objectMode: true,
133+
transform(row, _, cb) {
134+
cb(null, [row.id, row.v])
135+
},
136+
}),
137+
)
138+
}
139+
140+
async beginTransaction(): Promise<void> {
141+
await this.db.runSQL({ sql: `BEGIN TRANSACTION` })
142+
}
143+
144+
async endTransaction(): Promise<void> {
145+
await this.db.runSQL({ sql: `END TRANSACTION` })
146+
}
147+
148+
async count(table: string): Promise<number> {
149+
const sql = `SELECT count(*) as cnt FROM ${table}`
150+
if (this.cfg.logSQL) this.db.cfg.logger.log(sql)
151+
152+
const rows = await this.db.runSQL<{ cnt: number }[]>({ sql })
153+
return rows[0]!.cnt
154+
}
155+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { runCommonKeyValueDBTest, TEST_TABLE } from '@naturalcycles/db-lib/dist/testing'
2+
import { requireEnvKeys } from '@naturalcycles/nodejs-lib'
3+
import { MySQLKeyValueDB } from '../mysqlKeyValueDB'
4+
5+
require('dotenv').config()
6+
7+
const { MYSQL_HOST, MYSQL_USER, MYSQL_PW, MYSQL_DB } = requireEnvKeys(
8+
'MYSQL_HOST',
9+
'MYSQL_USER',
10+
'MYSQL_PW',
11+
'MYSQL_DB',
12+
)
13+
14+
const db = new MySQLKeyValueDB({
15+
host: MYSQL_HOST,
16+
user: MYSQL_USER,
17+
password: MYSQL_PW,
18+
database: MYSQL_DB,
19+
charset: 'utf8mb4',
20+
logSQL: true,
21+
// debug: true,
22+
})
23+
24+
beforeAll(async () => {
25+
await db.createTable(TEST_TABLE, { dropIfExists: true })
26+
})
27+
28+
afterAll(async () => {
29+
await db.close()
30+
})
31+
32+
describe('runCommonKeyValueDBTest', () => runCommonKeyValueDBTest(db))
33+
34+
test('count', async () => {
35+
const count = await db.count(TEST_TABLE)
36+
expect(count).toBe(0)
37+
})
38+
39+
// test('test1', async () => {
40+
// await db.deleteByIds(TEST_TABLE, ['id1', 'id2'])
41+
// await db.saveBatch(TEST_TABLE, {
42+
// k1: Buffer.from('hello1'),
43+
// k2: Buffer.from('hello2'),
44+
// })
45+
// })

0 commit comments

Comments
 (0)