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

Commit f00a88a

Browse files
feat: experimental BetterSQLiteKeyValueDBCfg
Based on https://github.com/WiseLibs/better-sqlite3
1 parent 365966d commit f00a88a

File tree

10 files changed

+422
-35
lines changed

10 files changed

+422
-35
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
"@naturalcycles/db-lib": "^8.7.0",
88
"@naturalcycles/js-lib": "^14.27.0",
99
"@naturalcycles/nodejs-lib": "^12.14.5",
10+
"@types/better-sqlite3": "^7.6.0",
11+
"better-sqlite3": "^7.6.2",
1012
"sql-template-strings": "^2.2.2",
11-
"sqlite": "^4.0.23",
13+
"sqlite": "^4.1.2",
1214
"sqlite3": "^5.1.1"
1315
},
1416
"devDependencies": {

scripts/generateTestTable.script.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ import { tmpDir } from '../src/test/paths.cnst'
1818
import { TestItem } from '../src/test/test.model'
1919

2020
runScript(async () => {
21-
const db = new SqliteKeyValueDB({
22-
filename: `${tmpDir}/test.sqlite`,
23-
// debug: true,
24-
})
21+
const filename = `${tmpDir}/test.sqlite`
22+
const db = new SqliteKeyValueDB({ filename })
23+
// "Better" is 6 seconds vs 14 seconds before
24+
// const db = new BetterSqliteKeyValueDB({ filename })
25+
2526
await db.ping()
2627
await db.createTable(TEST_TABLE, { dropIfExists: true })
2728
await db.beginTransaction()

scripts/streamingTest.script.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,18 @@ import { SqliteKeyValueDB } from '../src'
1111
import { tmpDir } from '../src/test/paths.cnst'
1212

1313
runScript(async () => {
14-
const db = new SqliteKeyValueDB({
15-
filename: `${tmpDir}/test.sqlite`,
16-
})
14+
const filename = `${tmpDir}/test.sqlite`
15+
const db = new SqliteKeyValueDB({ filename })
16+
// "Better" is 6 seconds vs 14 seconds before
17+
// const db = new BetterSqliteKeyValueDB({ filename })
18+
1719
await db.open()
1820

1921
const count = await db.count(TEST_TABLE)
2022
console.log({ count })
2123

2224
await _pipeline([
23-
db.streamIds(TEST_TABLE, 50_000),
25+
db.streamIds(TEST_TABLE, 5_000_000),
2426
// db.streamValues(TEST_TABLE, 50_000),
2527
// db.streamEntries(TEST_TABLE, 50_000),
2628
transformLogProgress({ logEvery: 10_000 }),

scripts/testscript.ts

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/betterSqliteKeyValueDB.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { runCommonKeyValueDBTest, TEST_TABLE } from '@naturalcycles/db-lib/dist/testing'
2+
import { BetterSqliteKeyValueDB } from './betterSqliteKeyValueDB'
3+
4+
const db = new BetterSqliteKeyValueDB({
5+
filename: ':memory:',
6+
})
7+
8+
beforeAll(async () => {
9+
// await db.ping()
10+
11+
// await db.getTables()
12+
await db.createTable(TEST_TABLE, { dropIfExists: true })
13+
})
14+
15+
afterAll(() => db.close())
16+
17+
describe('runCommonKeyValueDBTest', () => runCommonKeyValueDBTest(db))
18+
19+
test('count', async () => {
20+
const count = await db.count(TEST_TABLE)
21+
expect(count).toBe(0)
22+
})

src/betterSqliteKeyValueDB.ts

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { CommonDBCreateOptions, CommonKeyValueDB, KeyValueDBTuple } from '@naturalcycles/db-lib'
2+
import { CommonLogger } from '@naturalcycles/js-lib'
3+
import { readableCreate, ReadableTyped } from '@naturalcycles/nodejs-lib'
4+
import { boldWhite } from '@naturalcycles/nodejs-lib/dist/colors'
5+
import type { Options, Database } from 'better-sqlite3'
6+
import * as BetterSqlite3 from 'better-sqlite3'
7+
8+
export interface BetterSQLiteKeyValueDBCfg extends Options {
9+
filename: string
10+
11+
/**
12+
* Will log all sql queries executed.
13+
*
14+
* @default false
15+
*/
16+
debug?: boolean
17+
18+
/**
19+
* Defaults to `console`
20+
*/
21+
logger?: CommonLogger
22+
}
23+
24+
interface KeyValueObject {
25+
id: string
26+
v: Buffer
27+
}
28+
29+
/**
30+
* @experimental
31+
*/
32+
export class BetterSqliteKeyValueDB implements CommonKeyValueDB {
33+
constructor(cfg: BetterSQLiteKeyValueDBCfg) {
34+
this.cfg = {
35+
logger: console,
36+
...cfg,
37+
}
38+
}
39+
40+
cfg: BetterSQLiteKeyValueDBCfg & { logger: CommonLogger }
41+
42+
_db?: Database
43+
44+
get db(): Database {
45+
if (!this._db) {
46+
this.open()
47+
}
48+
49+
return this._db!
50+
}
51+
52+
open(): void {
53+
if (this._db) return
54+
55+
this._db = new BetterSqlite3(this.cfg.filename, {
56+
verbose: this.cfg.debug ? this.cfg.logger.log : undefined,
57+
...this.cfg,
58+
})
59+
60+
this.cfg.logger.log(`${boldWhite(this.cfg.filename)} opened`)
61+
}
62+
63+
close(): void {
64+
if (!this._db) return
65+
this.db.close()
66+
this.cfg.logger.log(`${boldWhite(this.cfg.filename)} closed`)
67+
}
68+
69+
async ping(): Promise<void> {
70+
this.open()
71+
}
72+
73+
async createTable(table: string, opt: CommonDBCreateOptions = {}): Promise<void> {
74+
if (opt.dropIfExists) this.dropTable(table)
75+
76+
const sql = `create table ${table} (id TEXT PRIMARY KEY, v BLOB NOT NULL)`
77+
this.cfg.logger.log(sql)
78+
this.db.prepare(sql).run()
79+
}
80+
81+
/**
82+
* Use with caution!
83+
*/
84+
dropTable(table: string): void {
85+
this.db.prepare(`DROP TABLE IF EXISTS ${table}`).run()
86+
}
87+
88+
async deleteByIds(table: string, ids: string[]): Promise<void> {
89+
const sql = `DELETE FROM ${table} WHERE id in (${ids.map(id => `'${id}'`).join(',')})`
90+
if (this.cfg.debug) this.cfg.logger.log(sql)
91+
this.db.prepare(sql).run()
92+
}
93+
94+
/**
95+
* API design note:
96+
* Here in the array of rows we have no way to map row to id (it's an opaque Buffer).
97+
*/
98+
async getByIds(table: string, ids: string[]): Promise<KeyValueDBTuple[]> {
99+
const sql = `SELECT id,v FROM ${table} where id in (${ids.map(id => `'${id}'`).join(',')})`
100+
if (this.cfg.debug) this.cfg.logger.log(sql)
101+
const rows = this.db.prepare(sql).all() as KeyValueObject[]
102+
// console.log(rows)
103+
return rows.map(r => [r.id, r.v])
104+
}
105+
106+
async saveBatch(table: string, entries: KeyValueDBTuple[]): Promise<void> {
107+
const sql = `INSERT INTO ${table} (id, v) VALUES (?, ?)`
108+
if (this.cfg.debug) this.cfg.logger.log(sql)
109+
110+
const stmt = this.db.prepare(sql)
111+
112+
entries.forEach(([id, buf]) => {
113+
stmt.run(id, buf)
114+
})
115+
}
116+
117+
async beginTransaction(): Promise<void> {
118+
this.db.exec(`BEGIN TRANSACTION`)
119+
}
120+
121+
async endTransaction(): Promise<void> {
122+
this.db.exec(`END TRANSACTION`)
123+
}
124+
125+
streamIds(table: string, limit?: number): ReadableTyped<string> {
126+
const readable = readableCreate<string>()
127+
128+
let sql = `SELECT id FROM ${table}`
129+
if (limit) {
130+
sql += ` LIMIT ${limit}`
131+
}
132+
133+
void (async () => {
134+
for (const row of this.db.prepare(sql).iterate()) {
135+
readable.push((row as { id: string }).id)
136+
}
137+
138+
// Now we're done
139+
readable.push(null)
140+
})()
141+
142+
return readable
143+
}
144+
145+
streamValues(table: string, limit?: number): ReadableTyped<Buffer> {
146+
const readable = readableCreate<Buffer>()
147+
148+
let sql = `SELECT v FROM ${table}`
149+
if (limit) {
150+
sql += ` LIMIT ${limit}`
151+
}
152+
153+
void (async () => {
154+
for (const row of this.db.prepare(sql).iterate()) {
155+
readable.push((row as { v: Buffer }).v)
156+
}
157+
158+
// Now we're done
159+
readable.push(null)
160+
})()
161+
162+
return readable
163+
}
164+
165+
streamEntries(table: string, limit?: number): ReadableTyped<KeyValueDBTuple> {
166+
const readable = readableCreate<KeyValueDBTuple>()
167+
168+
let sql = `SELECT id,v FROM ${table}`
169+
if (limit) {
170+
sql += ` LIMIT ${limit}`
171+
}
172+
173+
void (async () => {
174+
for (const row of this.db.prepare(sql).iterate()) {
175+
readable.push([row.id, row.v])
176+
}
177+
178+
// Now we're done
179+
readable.push(null)
180+
})()
181+
182+
return readable
183+
}
184+
185+
/**
186+
* Count rows in the given table.
187+
*/
188+
async count(table: string): Promise<number> {
189+
const sql = `SELECT count(*) as cnt FROM ${table}`
190+
191+
if (this.cfg.debug) this.cfg.logger.log(sql)
192+
193+
const { cnt } = this.db.prepare(sql).get() as { cnt: number }
194+
return cnt
195+
}
196+
}

src/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { SQLiteDB, SQLiteDBCfg } from './sqlite.db'
2-
import { SqliteKeyValueDB, SQLiteKeyValueDBCfg } from './sqliteKeyValueDB'
2+
export * from './sqliteKeyValueDB'
3+
export * from './betterSqliteKeyValueDB'
34

4-
export type { SQLiteDBCfg, SQLiteKeyValueDBCfg }
5+
export type { SQLiteDBCfg }
56

6-
export { SQLiteDB, SqliteKeyValueDB }
7+
export { SQLiteDB }

src/sqlite.db.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export class SQLiteDB extends BaseCommonDB implements CommonDB {
5252
this._db = await open({
5353
driver: sqlite3.Database,
5454
// eslint-disable-next-line no-bitwise
55-
mode: OPEN_READWRITE | OPEN_CREATE, // tslint:disable-line
55+
mode: OPEN_READWRITE | OPEN_CREATE,
5656
...this.cfg,
5757
})
5858
this.cfg.logger.log(`${boldWhite(this.cfg.filename)} opened`)

src/sqliteKeyValueDB.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ export class SqliteKeyValueDB implements CommonKeyValueDB {
5252
_db?: Database
5353

5454
get db(): Database {
55-
if (!this._db) throw new Error('await SQLiteDB.open() should be called before using the DB')
55+
if (!this._db)
56+
throw new Error('await SqliteKeyValueDB.open() should be called before using the DB')
5657
return this._db
5758
}
5859

@@ -62,7 +63,7 @@ export class SqliteKeyValueDB implements CommonKeyValueDB {
6263
this._db = await open({
6364
driver: sqlite3.Database,
6465
// eslint-disable-next-line no-bitwise
65-
mode: OPEN_READWRITE | OPEN_CREATE, // tslint:disable-line
66+
mode: OPEN_READWRITE | OPEN_CREATE,
6667
...this.cfg,
6768
})
6869
this.cfg.logger.log(`${boldWhite(this.cfg.filename)} opened`)

0 commit comments

Comments
 (0)