@verdant-web/server 3.0.5 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/ServerLibrary.test.js +10 -0
- package/dist/esm/ServerLibrary.test.js.map +1 -1
- package/dist/esm/storage/index.d.ts +1 -0
- package/dist/esm/storage/index.js +1 -0
- package/dist/esm/storage/index.js.map +1 -1
- package/dist/esm/storage/sql/database.d.ts +9 -0
- package/dist/esm/storage/sql/database.js +24 -0
- package/dist/esm/storage/sql/database.js.map +1 -0
- package/dist/esm/storage/sql/sqlStorage.d.ts +2 -1
- package/dist/esm/storage/sql/sqlStorage.js +5 -13
- package/dist/esm/storage/sql/sqlStorage.js.map +1 -1
- package/dist/esm/storage/sql/tables.d.ts +2 -0
- package/dist/esm/storage/sqlShard/Databases.d.ts +21 -0
- package/dist/esm/storage/sqlShard/Databases.js +64 -0
- package/dist/esm/storage/sqlShard/Databases.js.map +1 -0
- package/dist/esm/storage/sqlShard/SqlBaselines.d.ts +23 -0
- package/dist/esm/storage/sqlShard/SqlBaselines.js +98 -0
- package/dist/esm/storage/sqlShard/SqlBaselines.js.map +1 -0
- package/dist/esm/storage/sqlShard/SqlFileMetadata.d.ts +18 -0
- package/dist/esm/storage/sqlShard/SqlFileMetadata.js +70 -0
- package/dist/esm/storage/sqlShard/SqlFileMetadata.js.map +1 -0
- package/dist/esm/storage/sqlShard/SqlOperations.d.ts +18 -0
- package/dist/esm/storage/sqlShard/SqlOperations.js +104 -0
- package/dist/esm/storage/sqlShard/SqlOperations.js.map +1 -0
- package/dist/esm/storage/sqlShard/SqlReplicas.d.ts +32 -0
- package/dist/esm/storage/sqlShard/SqlReplicas.js +164 -0
- package/dist/esm/storage/sqlShard/SqlReplicas.js.map +1 -0
- package/dist/esm/storage/sqlShard/_testData/unifiedData.d.ts +30 -0
- package/dist/esm/storage/sqlShard/_testData/unifiedData.js +42 -0
- package/dist/esm/storage/sqlShard/_testData/unifiedData.js.map +1 -0
- package/dist/esm/storage/sqlShard/database.d.ts +6 -0
- package/dist/esm/storage/sqlShard/database.js +28 -0
- package/dist/esm/storage/sqlShard/database.js.map +1 -0
- package/dist/esm/storage/sqlShard/migrations/v1.d.ts +3 -0
- package/dist/esm/storage/sqlShard/migrations/v1.js +48 -0
- package/dist/esm/storage/sqlShard/migrations/v1.js.map +1 -0
- package/dist/esm/storage/sqlShard/migrations.d.ts +5 -0
- package/dist/esm/storage/sqlShard/migrations.js +3 -0
- package/dist/esm/storage/sqlShard/migrations.js.map +1 -0
- package/dist/esm/storage/sqlShard/sqlShardStorage.d.ts +7 -0
- package/dist/esm/storage/sqlShard/sqlShardStorage.js +55 -0
- package/dist/esm/storage/sqlShard/sqlShardStorage.js.map +1 -0
- package/dist/esm/storage/sqlShard/tables.d.ts +38 -0
- package/dist/esm/storage/sqlShard/tables.js +2 -0
- package/dist/esm/storage/sqlShard/tables.js.map +1 -0
- package/dist/esm/storage/sqlShard/transfer.d.ts +7 -0
- package/dist/esm/storage/sqlShard/transfer.js +90 -0
- package/dist/esm/storage/sqlShard/transfer.js.map +1 -0
- package/dist/esm/storage/sqlShard/transfer.test.d.ts +1 -0
- package/dist/esm/storage/sqlShard/transfer.test.js +109 -0
- package/dist/esm/storage/sqlShard/transfer.test.js.map +1 -0
- package/package.json +2 -1
- package/src/ServerLibrary.test.ts +10 -0
- package/src/storage/index.ts +1 -0
- package/src/storage/sql/database.ts +32 -0
- package/src/storage/sql/sqlStorage.ts +6 -8
- package/src/storage/sql/tables.ts +4 -0
- package/src/storage/sqlShard/Databases.ts +80 -0
- package/src/storage/sqlShard/SqlBaselines.ts +140 -0
- package/src/storage/sqlShard/SqlFileMetadata.ts +98 -0
- package/src/storage/sqlShard/SqlOperations.ts +146 -0
- package/src/storage/sqlShard/SqlReplicas.ts +237 -0
- package/src/storage/sqlShard/_testData/unifiedData.ts +45 -0
- package/src/storage/sqlShard/database.ts +37 -0
- package/src/storage/sqlShard/migrations/v1.ts +53 -0
- package/src/storage/sqlShard/migrations.ts +3 -0
- package/src/storage/sqlShard/sqlShardStorage.ts +81 -0
- package/src/storage/sqlShard/tables.ts +45 -0
- package/src/storage/sqlShard/transfer.test.ts +149 -0
- package/src/storage/sqlShard/transfer.ts +123 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { openDatabase as openUnifiedDatabase } from '../sql/database.js';
|
|
3
|
+
import {
|
|
4
|
+
randomBaselines,
|
|
5
|
+
randomFileMetadata,
|
|
6
|
+
randomOperations,
|
|
7
|
+
randomReplicaInfo,
|
|
8
|
+
} from './_testData/unifiedData.js';
|
|
9
|
+
import { join, dirname } from 'path';
|
|
10
|
+
import { transferToShards } from './transfer.js';
|
|
11
|
+
import { rm, mkdir } from 'fs/promises';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import {
|
|
14
|
+
DocumentBaselineRow,
|
|
15
|
+
FileMetadataRow,
|
|
16
|
+
OperationHistoryRow,
|
|
17
|
+
ReplicaInfoRow,
|
|
18
|
+
} from '../sql/tables.js';
|
|
19
|
+
import { Kysely } from 'kysely';
|
|
20
|
+
|
|
21
|
+
const testTempDir = join(
|
|
22
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
23
|
+
'_testData',
|
|
24
|
+
'tmp',
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
describe('sql-shard storage transfer utility', () => {
|
|
28
|
+
const databases = new Array<Kysely<any>>();
|
|
29
|
+
beforeAll(async () => {
|
|
30
|
+
try {
|
|
31
|
+
await rm(testTempDir, { recursive: true });
|
|
32
|
+
} catch (err) {}
|
|
33
|
+
// create a temp dir
|
|
34
|
+
await mkdir(testTempDir, { recursive: true });
|
|
35
|
+
});
|
|
36
|
+
afterAll(async () => {
|
|
37
|
+
try {
|
|
38
|
+
await Promise.all(databases.map((db) => db.destroy()));
|
|
39
|
+
// empty the temp dir
|
|
40
|
+
await rm(testTempDir, { recursive: true });
|
|
41
|
+
} catch (err) {
|
|
42
|
+
// what can you do...
|
|
43
|
+
console.error(err);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should transfer data from unified database to shards', async () => {
|
|
48
|
+
const unifiedFile = join(testTempDir, 'unified.sqlite');
|
|
49
|
+
const { db: unifiedDb, ready } = openUnifiedDatabase(unifiedFile);
|
|
50
|
+
databases.push(unifiedDb);
|
|
51
|
+
await ready;
|
|
52
|
+
const libraryIds = ['library-1', 'library-2', 'library-3'];
|
|
53
|
+
const randomData = {} as Record<
|
|
54
|
+
string,
|
|
55
|
+
{
|
|
56
|
+
operations: OperationHistoryRow[];
|
|
57
|
+
baselines: DocumentBaselineRow[];
|
|
58
|
+
replicas: ReplicaInfoRow[];
|
|
59
|
+
fileMetadata: FileMetadataRow[];
|
|
60
|
+
}
|
|
61
|
+
>;
|
|
62
|
+
for (const libraryId of libraryIds) {
|
|
63
|
+
randomData[libraryId] = {
|
|
64
|
+
// test a lot of them - more than max sqlite var count.
|
|
65
|
+
// only doing operations here to reduce test time.
|
|
66
|
+
operations: randomOperations(libraryId, 6000),
|
|
67
|
+
baselines: randomBaselines(libraryId),
|
|
68
|
+
replicas: randomReplicaInfo(libraryId),
|
|
69
|
+
fileMetadata: randomFileMetadata(libraryId),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// have to batch these...
|
|
73
|
+
for (let i = 0; i < randomData[libraryId].operations.length; i += 1000) {
|
|
74
|
+
const toInsert = randomData[libraryId].operations.slice(i, i + 1000);
|
|
75
|
+
await unifiedDb
|
|
76
|
+
.insertInto('OperationHistory')
|
|
77
|
+
.values(toInsert)
|
|
78
|
+
.execute();
|
|
79
|
+
}
|
|
80
|
+
for (let i = 0; i < randomData[libraryId].baselines.length; i += 1000) {
|
|
81
|
+
const toInsert = randomData[libraryId].baselines.slice(i, i + 1000);
|
|
82
|
+
await unifiedDb
|
|
83
|
+
.insertInto('DocumentBaseline')
|
|
84
|
+
.values(toInsert)
|
|
85
|
+
.execute();
|
|
86
|
+
}
|
|
87
|
+
for (
|
|
88
|
+
let i = 0;
|
|
89
|
+
i < randomData[libraryId].fileMetadata.length;
|
|
90
|
+
i += 1000
|
|
91
|
+
) {
|
|
92
|
+
const toInsert = randomData[libraryId].fileMetadata.slice(i, i + 1000);
|
|
93
|
+
await unifiedDb.insertInto('FileMetadata').values(toInsert).execute();
|
|
94
|
+
}
|
|
95
|
+
for (let i = 0; i < randomData[libraryId].replicas.length; i += 1000) {
|
|
96
|
+
const toInsert = randomData[libraryId].replicas.slice(i, i + 1000);
|
|
97
|
+
await unifiedDb.insertInto('ReplicaInfo').values(toInsert).execute();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const shards = await transferToShards({
|
|
102
|
+
directory: testTempDir,
|
|
103
|
+
file: unifiedFile,
|
|
104
|
+
});
|
|
105
|
+
Object.values(shards).map((db) => databases.push(db));
|
|
106
|
+
|
|
107
|
+
expect(Object.keys(shards).sort()).toEqual(libraryIds.sort());
|
|
108
|
+
|
|
109
|
+
for (const [libraryId, db] of Object.entries(shards)) {
|
|
110
|
+
const operations = await db
|
|
111
|
+
.selectFrom('OperationHistory')
|
|
112
|
+
.selectAll()
|
|
113
|
+
.execute();
|
|
114
|
+
const baselines = await db
|
|
115
|
+
.selectFrom('DocumentBaseline')
|
|
116
|
+
.selectAll()
|
|
117
|
+
.execute();
|
|
118
|
+
const replicas = await db.selectFrom('ReplicaInfo').selectAll().execute();
|
|
119
|
+
const fileMetadata = await db
|
|
120
|
+
.selectFrom('FileMetadata')
|
|
121
|
+
.selectAll()
|
|
122
|
+
.execute();
|
|
123
|
+
|
|
124
|
+
expect(operations.length).toBe(randomData[libraryId].operations.length);
|
|
125
|
+
expect(baselines.length).toBe(randomData[libraryId].baselines.length);
|
|
126
|
+
expect(replicas.length).toBe(randomData[libraryId].replicas.length);
|
|
127
|
+
expect(fileMetadata.length).toBe(
|
|
128
|
+
randomData[libraryId].fileMetadata.length,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
for (const { oid } of randomData[libraryId].operations) {
|
|
132
|
+
expect(operations.find((op) => op.oid === oid)).toBeDefined();
|
|
133
|
+
}
|
|
134
|
+
for (const { oid } of randomData[libraryId].baselines) {
|
|
135
|
+
expect(
|
|
136
|
+
baselines.find((baseline) => baseline.oid === oid),
|
|
137
|
+
).toBeDefined();
|
|
138
|
+
}
|
|
139
|
+
for (const { id } of randomData[libraryId].replicas) {
|
|
140
|
+
expect(replicas.find((replica) => replica.id === id)).toBeDefined();
|
|
141
|
+
}
|
|
142
|
+
for (const { fileId } of randomData[libraryId].fileMetadata) {
|
|
143
|
+
expect(
|
|
144
|
+
fileMetadata.find((file) => file.fileId === fileId),
|
|
145
|
+
).toBeDefined();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Kysely } from 'kysely';
|
|
2
|
+
import { openDatabase as openUnifiedDatabase } from '../sql/database.js';
|
|
3
|
+
import { Database as UnifiedDatabase } from '../sql/tables.js';
|
|
4
|
+
import { Database } from './tables.js';
|
|
5
|
+
import { openDatabase } from './database.js';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_BATCH_SIZE = 1000;
|
|
8
|
+
|
|
9
|
+
export async function transferToShards({
|
|
10
|
+
file,
|
|
11
|
+
directory,
|
|
12
|
+
batchSize = DEFAULT_BATCH_SIZE,
|
|
13
|
+
}: {
|
|
14
|
+
file: string;
|
|
15
|
+
directory: string;
|
|
16
|
+
batchSize?: number;
|
|
17
|
+
}) {
|
|
18
|
+
console.info(
|
|
19
|
+
`Transferring from unified database (${file}) to shards (${directory})...`,
|
|
20
|
+
);
|
|
21
|
+
const { db: unifiedDb, ready } = openUnifiedDatabase(file);
|
|
22
|
+
await ready;
|
|
23
|
+
const libraries = await unifiedDb
|
|
24
|
+
.selectFrom('ReplicaInfo')
|
|
25
|
+
.select('ReplicaInfo.libraryId')
|
|
26
|
+
.distinct()
|
|
27
|
+
.execute();
|
|
28
|
+
|
|
29
|
+
const dbs = await Promise.all(
|
|
30
|
+
libraries.map(async ({ libraryId }) => {
|
|
31
|
+
const db = await openDatabase(directory, libraryId);
|
|
32
|
+
await copyLibrary(libraryId, unifiedDb, db, batchSize);
|
|
33
|
+
return db;
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return libraries.reduce(
|
|
38
|
+
(acc, { libraryId }, i) => {
|
|
39
|
+
acc[libraryId] = dbs[i];
|
|
40
|
+
return acc;
|
|
41
|
+
},
|
|
42
|
+
{} as Record<string, Kysely<Database>>,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function copyLibrary(
|
|
47
|
+
libraryId: string,
|
|
48
|
+
source: Kysely<UnifiedDatabase>,
|
|
49
|
+
dest: Kysely<Database>,
|
|
50
|
+
batchSize: number,
|
|
51
|
+
) {
|
|
52
|
+
const operations = await source
|
|
53
|
+
.selectFrom('OperationHistory')
|
|
54
|
+
.where('libraryId', '=', libraryId)
|
|
55
|
+
.selectAll()
|
|
56
|
+
.execute();
|
|
57
|
+
const baselines = await source
|
|
58
|
+
.selectFrom('DocumentBaseline')
|
|
59
|
+
.where('libraryId', '=', libraryId)
|
|
60
|
+
.selectAll()
|
|
61
|
+
.execute();
|
|
62
|
+
const replicas = await source
|
|
63
|
+
.selectFrom('ReplicaInfo')
|
|
64
|
+
.where('libraryId', '=', libraryId)
|
|
65
|
+
.selectAll()
|
|
66
|
+
.execute();
|
|
67
|
+
const fileMetadata = await source
|
|
68
|
+
.selectFrom('FileMetadata')
|
|
69
|
+
.where('libraryId', '=', libraryId)
|
|
70
|
+
.selectAll()
|
|
71
|
+
.execute();
|
|
72
|
+
|
|
73
|
+
for (const operation of operations) {
|
|
74
|
+
// @ts-expect-error
|
|
75
|
+
delete operation.libraryId;
|
|
76
|
+
}
|
|
77
|
+
for (const baseline of baselines) {
|
|
78
|
+
// @ts-expect-error
|
|
79
|
+
delete baseline.libraryId;
|
|
80
|
+
}
|
|
81
|
+
for (const replica of replicas) {
|
|
82
|
+
// @ts-expect-error
|
|
83
|
+
delete replica.libraryId;
|
|
84
|
+
}
|
|
85
|
+
for (const metadata of fileMetadata) {
|
|
86
|
+
// @ts-expect-error
|
|
87
|
+
delete metadata.libraryId;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await dest.transaction().execute(async (trx) => {
|
|
91
|
+
const actions: Promise<any>[] = [];
|
|
92
|
+
// batch insert data to avoid maximum vars limits
|
|
93
|
+
if (operations.length) {
|
|
94
|
+
for (let i = 0; i < operations.length; i += batchSize) {
|
|
95
|
+
const toInsert = operations.slice(i, i + batchSize);
|
|
96
|
+
actions.push(
|
|
97
|
+
trx.insertInto('OperationHistory').values(toInsert).execute(),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (baselines.length) {
|
|
102
|
+
for (let i = 0; i < baselines.length; i += batchSize) {
|
|
103
|
+
const toInsert = baselines.slice(i, i + batchSize);
|
|
104
|
+
actions.push(
|
|
105
|
+
trx.insertInto('DocumentBaseline').values(toInsert).execute(),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (replicas.length) {
|
|
110
|
+
for (let i = 0; i < replicas.length; i += batchSize) {
|
|
111
|
+
const toInsert = replicas.slice(i, i + batchSize);
|
|
112
|
+
actions.push(trx.insertInto('ReplicaInfo').values(toInsert).execute());
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (fileMetadata.length) {
|
|
116
|
+
for (let i = 0; i < fileMetadata.length; i += batchSize) {
|
|
117
|
+
const toInsert = fileMetadata.slice(i, i + batchSize);
|
|
118
|
+
actions.push(trx.insertInto('FileMetadata').values(toInsert).execute());
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
await Promise.all(actions);
|
|
122
|
+
});
|
|
123
|
+
}
|