prisma-sharding 0.0.3 → 0.0.4
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/README.md +46 -0
- package/dist/cli/test.js +321 -42
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -213,6 +213,52 @@ Test connections to all shards.
|
|
|
213
213
|
yarn test:shards
|
|
214
214
|
```
|
|
215
215
|
|
|
216
|
+
```
|
|
217
|
+
================================
|
|
218
|
+
📋 User Distribution Test
|
|
219
|
+
================================
|
|
220
|
+
Creating 24 test users across 3 shards...
|
|
221
|
+
|
|
222
|
+
User 1/24: "testuser_0" → shard_3
|
|
223
|
+
User 2/24: "testuser_1" → shard_1
|
|
224
|
+
User 3/24: "testuser_2" → shard_2
|
|
225
|
+
User 4/24: "testuser_3" → shard_3
|
|
226
|
+
User 5/24: "testuser_4" → shard_1
|
|
227
|
+
User 6/24: "testuser_5" → shard_2
|
|
228
|
+
User 7/24: "testuser_6" → shard_3
|
|
229
|
+
User 8/24: "testuser_7" → shard_1
|
|
230
|
+
User 9/24: "testuser_8" → shard_2
|
|
231
|
+
User 10/24: "testuser_9" → shard_3
|
|
232
|
+
User 11/24: "testuser_10" → shard_2
|
|
233
|
+
User 12/24: "testuser_11" → shard_1
|
|
234
|
+
User 13/24: "testuser_12" → shard_3
|
|
235
|
+
User 14/24: "testuser_13" → shard_2
|
|
236
|
+
User 15/24: "testuser_14" → shard_1
|
|
237
|
+
User 16/24: "testuser_15" → shard_3
|
|
238
|
+
User 17/24: "testuser_16" → shard_2
|
|
239
|
+
User 18/24: "testuser_17" → shard_1
|
|
240
|
+
User 19/24: "testuser_18" → shard_3
|
|
241
|
+
User 20/24: "testuser_19" → shard_2
|
|
242
|
+
User 21/24: "testuser_20" → shard_1
|
|
243
|
+
User 22/24: "testuser_21" → shard_3
|
|
244
|
+
User 23/24: "testuser_22" → shard_2
|
|
245
|
+
User 24/24: "testuser_23" → shard_1
|
|
246
|
+
✅ Created 24/24 test users
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
================================
|
|
251
|
+
📋 Read Verification
|
|
252
|
+
================================
|
|
253
|
+
✓ User "test_user_1770289330292_0" found on shard_3
|
|
254
|
+
✓ User "test_user_1770289330292_1" found on shard_1
|
|
255
|
+
✓ User "test_user_1770289330292_2" found on shard_2
|
|
256
|
+
✓ User "test_user_1770289330292_3" found on shard_3
|
|
257
|
+
✓ User "test_user_1770289330292_4" found on shard_1
|
|
258
|
+
Verified 5/5 users on correct shards
|
|
259
|
+
✅ Verify users exist on correct shards (136ms)
|
|
260
|
+
```
|
|
261
|
+
|
|
216
262
|
## Configuration
|
|
217
263
|
|
|
218
264
|
| Option | Type | Default | Description |
|
package/dist/cli/test.js
CHANGED
|
@@ -1,19 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
3
25
|
|
|
4
26
|
// src/cli/test.ts
|
|
5
27
|
var import_config = require("dotenv/config");
|
|
6
|
-
var import_child_process = require("child_process");
|
|
7
28
|
var results = [];
|
|
29
|
+
var testUsers = [];
|
|
30
|
+
var TEST_USER_COUNT = 24;
|
|
8
31
|
var log = {
|
|
9
32
|
info: (msg) => console.log(`\u2139\uFE0F ${msg}`),
|
|
10
33
|
success: (msg) => console.log(`\u2705 ${msg}`),
|
|
11
34
|
error: (msg) => console.log(`\u274C ${msg}`),
|
|
12
35
|
warn: (msg) => console.log(`\u26A0\uFE0F ${msg}`),
|
|
13
36
|
section: (msg) => console.log(`
|
|
14
|
-
${"=".repeat(
|
|
37
|
+
${"=".repeat(60)}
|
|
15
38
|
\u{1F4CB} ${msg}
|
|
16
|
-
${"=".repeat(
|
|
39
|
+
${"=".repeat(60)}`),
|
|
40
|
+
detail: (msg) => console.log(` ${msg}`)
|
|
17
41
|
};
|
|
18
42
|
var getShardConfigs = () => {
|
|
19
43
|
const shards = [];
|
|
@@ -43,78 +67,333 @@ var runTest = async (name, testFn) => {
|
|
|
43
67
|
log.error(`${name}: ${message}`);
|
|
44
68
|
}
|
|
45
69
|
};
|
|
46
|
-
var
|
|
70
|
+
var parsePostgresUrl = (url) => {
|
|
71
|
+
try {
|
|
72
|
+
const match = url.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/([^?]+)/);
|
|
73
|
+
if (match) {
|
|
74
|
+
return {
|
|
75
|
+
user: match[1],
|
|
76
|
+
password: match[2],
|
|
77
|
+
host: match[3],
|
|
78
|
+
port: parseInt(match[4], 10),
|
|
79
|
+
database: match[5]
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var testTcpConnection = (host, port) => {
|
|
47
88
|
return new Promise((resolve) => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
89
|
+
import("net").then(({ default: net }) => {
|
|
90
|
+
const socket = new net.Socket();
|
|
91
|
+
socket.setTimeout(5e3);
|
|
92
|
+
socket.on("connect", () => {
|
|
93
|
+
socket.destroy();
|
|
94
|
+
resolve(true);
|
|
95
|
+
});
|
|
96
|
+
socket.on("timeout", () => {
|
|
97
|
+
socket.destroy();
|
|
98
|
+
resolve(false);
|
|
99
|
+
});
|
|
100
|
+
socket.on("error", () => {
|
|
101
|
+
socket.destroy();
|
|
102
|
+
resolve(false);
|
|
103
|
+
});
|
|
104
|
+
socket.connect(port, host);
|
|
105
|
+
}).catch(() => resolve(false));
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
var getPgClient = async () => {
|
|
109
|
+
try {
|
|
110
|
+
const pg = await import("pg");
|
|
111
|
+
return pg.default?.Client || pg.Client;
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
var executeSql = async (url, sql) => {
|
|
117
|
+
const Client = await getPgClient();
|
|
118
|
+
if (!Client) {
|
|
119
|
+
return executeSqlWithPsql(url, sql);
|
|
120
|
+
}
|
|
121
|
+
const client = new Client({ connectionString: url });
|
|
122
|
+
try {
|
|
123
|
+
await client.connect();
|
|
124
|
+
const result = await client.query(sql);
|
|
125
|
+
await client.end();
|
|
126
|
+
return { success: true, rows: result.rows };
|
|
127
|
+
} catch (error) {
|
|
128
|
+
try {
|
|
129
|
+
await client.end();
|
|
130
|
+
} catch {
|
|
131
|
+
}
|
|
132
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
var executeSqlWithPsql = (url, sql) => {
|
|
136
|
+
return new Promise((resolve) => {
|
|
137
|
+
import("child_process").then(({ spawn }) => {
|
|
138
|
+
const psql = spawn("psql", [url, "-c", sql], {
|
|
139
|
+
shell: true,
|
|
140
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
141
|
+
});
|
|
142
|
+
let stdout = "";
|
|
143
|
+
let stderr = "";
|
|
144
|
+
psql.stdout?.on("data", (data) => {
|
|
145
|
+
stdout += data.toString();
|
|
146
|
+
});
|
|
147
|
+
psql.stderr?.on("data", (data) => {
|
|
148
|
+
stderr += data.toString();
|
|
149
|
+
});
|
|
150
|
+
const timeout = setTimeout(() => {
|
|
151
|
+
psql.kill();
|
|
152
|
+
resolve({ success: false, error: "Command timeout" });
|
|
153
|
+
}, 15e3);
|
|
154
|
+
psql.on("close", (code) => {
|
|
155
|
+
clearTimeout(timeout);
|
|
156
|
+
resolve({ success: code === 0, error: code !== 0 ? stderr : void 0 });
|
|
157
|
+
});
|
|
158
|
+
psql.on("error", (err) => {
|
|
159
|
+
clearTimeout(timeout);
|
|
160
|
+
resolve({ success: false, error: err.message });
|
|
161
|
+
});
|
|
59
162
|
});
|
|
60
163
|
});
|
|
61
164
|
};
|
|
165
|
+
var hashString = (str) => {
|
|
166
|
+
let hash = 0;
|
|
167
|
+
for (let i = 0; i < str.length; i++) {
|
|
168
|
+
const char = str.charCodeAt(i);
|
|
169
|
+
hash = (hash << 5) - hash + char;
|
|
170
|
+
hash = hash & hash;
|
|
171
|
+
}
|
|
172
|
+
return Math.abs(hash);
|
|
173
|
+
};
|
|
174
|
+
var getShardIndex = (key, shardCount) => {
|
|
175
|
+
return hashString(key) % shardCount;
|
|
176
|
+
};
|
|
62
177
|
var runTests = async () => {
|
|
63
178
|
const shards = getShardConfigs();
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
console.log(
|
|
67
|
-
console.log(
|
|
179
|
+
const timestamp = Date.now();
|
|
180
|
+
const testTableName = `_prisma_sharding_test_${timestamp}`;
|
|
181
|
+
console.log("\n" + "\u2550".repeat(60));
|
|
182
|
+
console.log("\u{1F9EA} PRISMA SHARDING - Comprehensive Test Suite");
|
|
183
|
+
console.log("\u2550".repeat(60));
|
|
184
|
+
console.log(`
|
|
185
|
+
\u{1F4CA} Configuration:`);
|
|
186
|
+
console.log(` \u2022 Shard Count: ${shards.length}`);
|
|
187
|
+
console.log(` \u2022 Routing Strategy: ${process.env.SHARD_ROUTING_STRATEGY || "modulo"}`);
|
|
188
|
+
console.log(` \u2022 Test Users: ${TEST_USER_COUNT}`);
|
|
189
|
+
console.log(` \u2022 Test Table: ${testTableName}`);
|
|
68
190
|
if (shards.length === 0) {
|
|
69
191
|
console.error(
|
|
70
192
|
"\n\u274C No shards configured. Set SHARD_COUNT and SHARD_N_URL environment variables."
|
|
71
193
|
);
|
|
72
194
|
process.exit(1);
|
|
73
195
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
throw new Error(`Failed to connect to ${shard.id}`);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
196
|
+
const pgAvailable = await getPgClient();
|
|
197
|
+
if (pgAvailable) {
|
|
198
|
+
console.log(` \u2022 Database Client: pg (native)`);
|
|
199
|
+
} else {
|
|
200
|
+
console.log(` \u2022 Database Client: psql (fallback)`);
|
|
82
201
|
}
|
|
83
|
-
log.section("Test
|
|
202
|
+
log.section("Test 1: Configuration Validation");
|
|
84
203
|
await runTest("Verify environment variables", async () => {
|
|
85
204
|
const shardCount = parseInt(process.env.SHARD_COUNT || "0", 10);
|
|
86
205
|
if (shardCount === 0 && !process.env.DATABASE_URL) {
|
|
87
206
|
throw new Error("SHARD_COUNT or DATABASE_URL must be set");
|
|
88
207
|
}
|
|
89
|
-
log.
|
|
90
|
-
log.
|
|
208
|
+
log.detail(`SHARD_COUNT = ${shardCount}`);
|
|
209
|
+
log.detail(`SHARD_ROUTING_STRATEGY = ${process.env.SHARD_ROUTING_STRATEGY || "modulo"}`);
|
|
91
210
|
});
|
|
92
|
-
await runTest("Verify
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
211
|
+
await runTest("Verify shard URL formats", async () => {
|
|
212
|
+
for (const shard of shards) {
|
|
213
|
+
const urlInfo = parsePostgresUrl(shard.url);
|
|
214
|
+
if (!urlInfo) {
|
|
215
|
+
throw new Error(`Invalid URL format for ${shard.id}`);
|
|
216
|
+
}
|
|
217
|
+
log.detail(`${shard.id} \u2192 ${urlInfo.host}:${urlInfo.port}/${urlInfo.database}`);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
log.section("Test 2: Network Connectivity");
|
|
221
|
+
for (const shard of shards) {
|
|
222
|
+
await runTest(`TCP connection to ${shard.id}`, async () => {
|
|
223
|
+
const urlInfo = parsePostgresUrl(shard.url);
|
|
224
|
+
if (!urlInfo) throw new Error("Invalid URL");
|
|
225
|
+
const connected = await testTcpConnection(urlInfo.host, urlInfo.port);
|
|
226
|
+
if (!connected) {
|
|
227
|
+
throw new Error(`Cannot reach ${urlInfo.host}:${urlInfo.port}`);
|
|
228
|
+
}
|
|
229
|
+
log.detail(`${urlInfo.host}:${urlInfo.port} is reachable`);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
log.section("Test 3: Database Connection");
|
|
233
|
+
for (const shard of shards) {
|
|
234
|
+
await runTest(`SQL execution on ${shard.id}`, async () => {
|
|
235
|
+
const result = await executeSql(shard.url, "SELECT 1 as test;");
|
|
236
|
+
if (!result.success) {
|
|
237
|
+
throw new Error(result.error || "SQL execution failed");
|
|
238
|
+
}
|
|
239
|
+
log.detail(`${shard.id} accepts SQL commands`);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
log.section("Test 4: Create Test Table");
|
|
243
|
+
const createTableSql = `
|
|
244
|
+
CREATE TABLE IF NOT EXISTS "${testTableName}" (
|
|
245
|
+
id VARCHAR(50) PRIMARY KEY,
|
|
246
|
+
email VARCHAR(255) NOT NULL,
|
|
247
|
+
username VARCHAR(100) NOT NULL,
|
|
248
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
249
|
+
);
|
|
250
|
+
`;
|
|
251
|
+
for (const shard of shards) {
|
|
252
|
+
await runTest(`Create test table on ${shard.id}`, async () => {
|
|
253
|
+
const result = await executeSql(shard.url, createTableSql);
|
|
254
|
+
if (!result.success) {
|
|
255
|
+
throw new Error(result.error || "Failed to create table");
|
|
256
|
+
}
|
|
257
|
+
log.detail(`Table "${testTableName}" created on ${shard.id}`);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
log.section("Test 5: User Distribution Test");
|
|
261
|
+
console.log(`
|
|
262
|
+
Creating ${TEST_USER_COUNT} test users across ${shards.length} shards...
|
|
263
|
+
`);
|
|
264
|
+
const distribution = /* @__PURE__ */ new Map();
|
|
265
|
+
shards.forEach((s) => distribution.set(s.id, 0));
|
|
266
|
+
for (let i = 0; i < TEST_USER_COUNT; i++) {
|
|
267
|
+
const userId = `test_user_${timestamp}_${i}`;
|
|
268
|
+
const email = `test_${timestamp}_${i}@example.com`;
|
|
269
|
+
const username = `testuser_${i}`;
|
|
270
|
+
const shardIndex = getShardIndex(userId, shards.length);
|
|
271
|
+
const targetShard = shards[shardIndex];
|
|
272
|
+
const insertSql = `
|
|
273
|
+
INSERT INTO "${testTableName}" (id, email, username)
|
|
274
|
+
VALUES ('${userId}', '${email}', '${username}');
|
|
275
|
+
`;
|
|
276
|
+
const result = await executeSql(targetShard.url, insertSql);
|
|
277
|
+
if (result.success) {
|
|
278
|
+
testUsers.push({ id: userId, email, shardId: targetShard.id });
|
|
279
|
+
distribution.set(targetShard.id, (distribution.get(targetShard.id) || 0) + 1);
|
|
280
|
+
log.detail(`User ${i + 1}/${TEST_USER_COUNT}: "${username}" \u2192 ${targetShard.id}`);
|
|
281
|
+
} else {
|
|
282
|
+
log.warn(`Failed to create user ${i + 1}: ${result.error}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
log.success(`Created ${testUsers.length}/${TEST_USER_COUNT} test users`);
|
|
286
|
+
log.section("Test 6: Distribution Analysis");
|
|
287
|
+
await runTest("Analyze shard distribution", async () => {
|
|
288
|
+
console.log("\n \u{1F4CA} User Distribution Across Shards:\n");
|
|
289
|
+
console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
290
|
+
console.log(" \u2502 Shard \u2502 Users \u2502 Percentage \u2502 Visual \u2502");
|
|
291
|
+
console.log(" \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
|
|
292
|
+
let minCount = Infinity;
|
|
293
|
+
let maxCount = 0;
|
|
294
|
+
distribution.forEach((count, shardId) => {
|
|
295
|
+
const percentage = testUsers.length > 0 ? (count / testUsers.length * 100).toFixed(1) : "0.0";
|
|
296
|
+
const barLength = testUsers.length > 0 ? Math.round(count / testUsers.length * 15) : 0;
|
|
297
|
+
const bar = "\u2588".repeat(barLength) + "\u2591".repeat(15 - barLength);
|
|
298
|
+
console.log(
|
|
299
|
+
` \u2502 ${shardId.padEnd(11)} \u2502 ${count.toString().padStart(9)} \u2502 ${percentage.padStart(6)}% \u2502 ${bar} \u2502`
|
|
300
|
+
);
|
|
301
|
+
minCount = Math.min(minCount, count);
|
|
302
|
+
maxCount = Math.max(maxCount, count);
|
|
303
|
+
});
|
|
304
|
+
console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n");
|
|
305
|
+
if (testUsers.length > 0) {
|
|
306
|
+
const minPercentage = minCount / testUsers.length * 100;
|
|
307
|
+
const maxPercentage = maxCount / testUsers.length * 100;
|
|
308
|
+
if (minPercentage < 10) {
|
|
309
|
+
log.warn(`Some shards have very few users (${minPercentage.toFixed(1)}%)`);
|
|
310
|
+
}
|
|
311
|
+
if (maxPercentage > 60) {
|
|
312
|
+
log.warn(`Some shards are overloaded (${maxPercentage.toFixed(1)}%)`);
|
|
98
313
|
}
|
|
314
|
+
log.detail(`Distribution range: ${minPercentage.toFixed(1)}% - ${maxPercentage.toFixed(1)}%`);
|
|
315
|
+
log.detail(`Average per shard: ${(testUsers.length / shards.length).toFixed(1)} users`);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
log.section("Test 7: Read Verification");
|
|
319
|
+
await runTest("Verify users exist on correct shards", async () => {
|
|
320
|
+
let verified = 0;
|
|
321
|
+
const sampleSize = Math.min(5, testUsers.length);
|
|
322
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
323
|
+
const user = testUsers[i];
|
|
324
|
+
const shard = shards.find((s) => s.id === user.shardId);
|
|
325
|
+
if (!shard) continue;
|
|
326
|
+
const selectSql = `SELECT id, email FROM "${testTableName}" WHERE id = '${user.id}';`;
|
|
327
|
+
const result = await executeSql(shard.url, selectSql);
|
|
328
|
+
if (result.success && result.rows && result.rows.length > 0) {
|
|
329
|
+
verified++;
|
|
330
|
+
log.detail(`\u2713 User "${user.id}" found on ${user.shardId}`);
|
|
331
|
+
} else {
|
|
332
|
+
log.warn(`\u2717 User "${user.id}" NOT found on ${user.shardId}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (verified < sampleSize) {
|
|
336
|
+
throw new Error(`Only ${verified}/${sampleSize} users verified`);
|
|
99
337
|
}
|
|
338
|
+
log.detail(`Verified ${verified}/${sampleSize} users on correct shards`);
|
|
100
339
|
});
|
|
101
|
-
log.section("Test
|
|
340
|
+
log.section("Test 8: Cross-Shard Count");
|
|
341
|
+
await runTest("Count users across all shards", async () => {
|
|
342
|
+
let totalCount = 0;
|
|
343
|
+
for (const shard of shards) {
|
|
344
|
+
const countSql = `SELECT COUNT(*) as count FROM "${testTableName}";`;
|
|
345
|
+
const result = await executeSql(shard.url, countSql);
|
|
346
|
+
if (result.success && result.rows && result.rows.length > 0) {
|
|
347
|
+
const count = parseInt(result.rows[0].count, 10);
|
|
348
|
+
totalCount += count;
|
|
349
|
+
log.detail(`${shard.id}: ${count} users`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
log.detail(`Total users across all shards: ${totalCount}`);
|
|
353
|
+
if (totalCount !== testUsers.length) {
|
|
354
|
+
log.warn(`Expected ${testUsers.length} users, found ${totalCount}`);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
log.section("Test 9: Cleanup");
|
|
358
|
+
let cleanedUp = 0;
|
|
359
|
+
for (const shard of shards) {
|
|
360
|
+
const dropSql = `DROP TABLE IF EXISTS "${testTableName}";`;
|
|
361
|
+
const result = await executeSql(shard.url, dropSql);
|
|
362
|
+
if (result.success) {
|
|
363
|
+
cleanedUp++;
|
|
364
|
+
log.detail(`Test table removed from ${shard.id}`);
|
|
365
|
+
} else {
|
|
366
|
+
log.warn(`Could not drop table on ${shard.id}: ${result.error}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
log.success(`Cleanup complete (${cleanedUp}/${shards.length} shards)`);
|
|
370
|
+
console.log("\n" + "\u2550".repeat(60));
|
|
371
|
+
console.log("\u{1F4CB} TEST RESULTS SUMMARY");
|
|
372
|
+
console.log("\u2550".repeat(60));
|
|
102
373
|
const passed = results.filter((r) => r.passed).length;
|
|
103
374
|
const failed = results.filter((r) => !r.passed).length;
|
|
104
375
|
const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
|
|
105
376
|
console.log(`
|
|
106
377
|
Total Tests: ${results.length}`);
|
|
107
|
-
console.log(` Passed: ${passed}`);
|
|
108
|
-
console.log(` Failed: ${failed}`);
|
|
109
|
-
console.log(` Duration: ${totalDuration}
|
|
378
|
+
console.log(` \u2705 Passed: ${passed}`);
|
|
379
|
+
console.log(` \u274C Failed: ${failed}`);
|
|
380
|
+
console.log(` \u23F1\uFE0F Duration: ${(totalDuration / 1e3).toFixed(2)}s`);
|
|
381
|
+
console.log(` \u{1F4CA} Users Created: ${testUsers.length}`);
|
|
110
382
|
if (failed > 0) {
|
|
111
383
|
console.log("\n Failed Tests:");
|
|
112
384
|
results.filter((r) => !r.passed).forEach((r) => {
|
|
113
|
-
console.log(`
|
|
385
|
+
console.log(` \u2022 ${r.name}: ${r.message}`);
|
|
114
386
|
});
|
|
387
|
+
console.log("\n\u{1F4A1} Troubleshooting Tips:");
|
|
388
|
+
console.log(" \u2022 Ensure PostgreSQL is running");
|
|
389
|
+
console.log(" \u2022 Verify shard databases exist");
|
|
390
|
+
console.log(" \u2022 Check credentials in SHARD_N_URL");
|
|
391
|
+
console.log(" \u2022 Run: yarn migrate:shards\n");
|
|
115
392
|
process.exit(1);
|
|
116
393
|
}
|
|
117
|
-
console.log("\n
|
|
394
|
+
console.log("\n" + "\u2550".repeat(60));
|
|
395
|
+
console.log("\u2705 ALL TESTS PASSED SUCCESSFULLY!");
|
|
396
|
+
console.log("\u2550".repeat(60) + "\n");
|
|
118
397
|
};
|
|
119
398
|
runTests().catch((error) => {
|
|
120
399
|
console.error("\n\u{1F4A5} Test suite crashed:", error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prisma-sharding",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Lightweight database sharding library for Prisma with connection pooling and health monitoring",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -50,10 +50,12 @@
|
|
|
50
50
|
"@prisma/client": ">=5.0.0"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"dotenv": "^16.3.1"
|
|
53
|
+
"dotenv": "^16.3.1",
|
|
54
|
+
"pg": "^8.18.0"
|
|
54
55
|
},
|
|
55
56
|
"devDependencies": {
|
|
56
57
|
"@types/node": "^20.10.0",
|
|
58
|
+
"@types/pg": "^8.16.0",
|
|
57
59
|
"@typescript-eslint/eslint-plugin": "^6.13.0",
|
|
58
60
|
"@typescript-eslint/parser": "^6.13.0",
|
|
59
61
|
"eslint": "^8.55.0",
|