prisma-sharding 0.0.6 → 0.0.7
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 +135 -58
- package/dist/cli/migrate.js +198 -47
- package/dist/cli/studio.js +152 -84
- package/dist/cli/test.js +145 -41
- package/dist/cli/update.js +195 -46
- package/dist/cli/utils/command.js +232 -0
- package/dist/cli/utils/postgres.js +55 -0
- package/dist/index.js +395 -89
- package/dist/index.mjs +395 -89
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Lightweight database sharding library for Prisma with connection pooling, health monitoring, and CLI tools.
|
|
4
4
|
|
|
5
|
+
## Backward Compatibility Policy
|
|
6
|
+
|
|
7
|
+
This package treats its existing public API as stable. Production-hardening releases do not rename
|
|
8
|
+
commands or public methods, change return shapes, move the package import path, remove exported
|
|
9
|
+
errors, or add required fields to public outputs. Internal routing, execution, health, and CLI
|
|
10
|
+
safety fixes preserve existing application code and the compact default CLI format.
|
|
11
|
+
|
|
5
12
|
## Installation
|
|
6
13
|
|
|
7
14
|
```bash
|
|
@@ -38,76 +45,77 @@ await sharding.connect();
|
|
|
38
45
|
|
|
39
46
|
## API
|
|
40
47
|
|
|
41
|
-
| Method | Description
|
|
42
|
-
| ---------------------------- |
|
|
43
|
-
| `getShard(key)` |
|
|
44
|
-
| `getShardById(shardId)` |
|
|
45
|
-
| `getRandomShard()` |
|
|
46
|
-
| `findFirst(fn)` |
|
|
47
|
-
| `runOnAll(fn)` |
|
|
48
|
-
| `getHealth()` |
|
|
49
|
-
| `connect()` / `disconnect()` | Lifecycle methods
|
|
50
|
-
|
|
51
|
-
### Step 2: Create a User (Assign to a Shard)
|
|
52
|
-
|
|
53
|
-
New records should be created on a random shard for even distribution.
|
|
54
|
-
|
|
55
|
-
```ts
|
|
56
|
-
import { sharding } from '@/config/prisma';
|
|
48
|
+
| Method | Description |
|
|
49
|
+
| ---------------------------- | ---------------------------------------------- |
|
|
50
|
+
| `getShard(key)` | Deterministic client for a routing key |
|
|
51
|
+
| `getShardById(shardId)` | Client for a persisted shard owner |
|
|
52
|
+
| `getRandomShard()` | Random assignment; ownership must be recorded |
|
|
53
|
+
| `findFirst(fn)` | Bounded exception-path search across shards |
|
|
54
|
+
| `runOnAll(fn)` | Bounded admin/analytics execution |
|
|
55
|
+
| `getHealth()` | Health status using the existing output shape |
|
|
56
|
+
| `connect()` / `disconnect()` | Lifecycle methods |
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
## Shard Ownership
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
email: 'user@example.com',
|
|
63
|
-
username: 'new_user',
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
```
|
|
60
|
+
Every record needs one authoritative shard owner. Choose one of these patterns and use it
|
|
61
|
+
consistently. Cross-shard search is a recovery path, not an ownership strategy.
|
|
67
62
|
|
|
68
|
-
|
|
63
|
+
### Pattern A: Deterministic Ownership
|
|
69
64
|
|
|
70
|
-
|
|
65
|
+
Generate or obtain the routing key before inserting the record, then use the same key for every
|
|
66
|
+
future operation:
|
|
71
67
|
|
|
72
|
-
```
|
|
73
|
-
|
|
68
|
+
```typescript
|
|
69
|
+
import { sharding } from '@/config/prisma';
|
|
74
70
|
|
|
71
|
+
const userId = crypto.randomUUID();
|
|
75
72
|
const client = sharding.getShard(userId);
|
|
73
|
+
const user = await client.user.create({
|
|
74
|
+
data: { id: userId, email: 'user@example.com', username: 'new_user' },
|
|
75
|
+
});
|
|
76
76
|
|
|
77
|
-
const
|
|
77
|
+
const sameUser = await sharding.getShard(userId).user.findUnique({
|
|
78
78
|
where: { id: userId },
|
|
79
79
|
});
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
`
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
Modulo routing uses the existing `hashString(key) % shardCount` placement. The hash function and
|
|
83
|
+
configured shard order are data-placement contracts: changing either can move existing records and
|
|
84
|
+
requires an explicit migration or dual-read plan. Consistent hashing also preserves configured
|
|
85
|
+
shard IDs and supports custom IDs such as `tenant-east`.
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
### Pattern B: Assigned Ownership
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
- Creating related records (profiles, posts, settings, etc)
|
|
89
|
+
Random assignment can distribute new records, but the application must persist the assigned shard
|
|
90
|
+
ID in a directory table, tenant registry, or equivalent ownership metadata:
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
```typescript
|
|
93
|
+
const { client, shardId } = sharding.getRandomShardWithInfo();
|
|
94
|
+
const user = await client.user.create({ data: { email, username } });
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
await shardDirectory.create({ data: { recordId: user.id, shardId } });
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
const ownership = await shardDirectory.findUniqueOrThrow({
|
|
99
|
+
where: { recordId: user.id },
|
|
100
|
+
});
|
|
101
|
+
const sameUser = await sharding
|
|
102
|
+
.getShardById(ownership.shardId)
|
|
103
|
+
.user.findUnique({ where: { id: user.id } });
|
|
100
104
|
```
|
|
101
105
|
|
|
102
|
-
|
|
106
|
+
The existing `getRandomShard()` method still returns only a client. Calling it for a write and
|
|
107
|
+
later calling `getShard(record.id)` is **not guaranteed to select the same shard**. If you use
|
|
108
|
+
`getRandomShard()`, your application needs another reliable way to record which shard was selected.
|
|
109
|
+
`weight` affects random assignment only; it never changes deterministic `getShard(key)` placement.
|
|
103
110
|
|
|
104
|
-
###
|
|
111
|
+
### Find Without Ownership Metadata
|
|
105
112
|
|
|
106
|
-
|
|
107
|
-
|
|
113
|
+
`findFirst()` is bounded, timed, health-aware, and returns when the first non-null result arrives.
|
|
114
|
+
Even so, one call can create work on multiple databases. Treat it as an exception, recovery, or
|
|
115
|
+
administrative path. At high traffic it should not be the normal login, email lookup, user lookup,
|
|
116
|
+
or tenant lookup path; maintain shard ownership metadata instead.
|
|
108
117
|
|
|
109
118
|
```typescript
|
|
110
|
-
// Find user by email across ALL shards (parallel execution)
|
|
111
119
|
const { result: user, client } = await sharding.findFirst(async (c) =>
|
|
112
120
|
c.user.findFirst({ where: { email } })
|
|
113
121
|
);
|
|
@@ -121,10 +129,10 @@ if (user && client) {
|
|
|
121
129
|
}
|
|
122
130
|
```
|
|
123
131
|
|
|
124
|
-
|
|
132
|
+
### Run on All Shards
|
|
125
133
|
|
|
126
134
|
```typescript
|
|
127
|
-
//
|
|
135
|
+
// Appropriate for bounded admin or analytics work, not a normal request path.
|
|
128
136
|
const counts = await sharding.runOnAll(async (client) => client.user.count());
|
|
129
137
|
const totalUsers = counts.reduce((sum, count) => sum + count, 0);
|
|
130
138
|
|
|
@@ -136,6 +144,13 @@ const results = await sharding.runOnAllWithDetails(async (client, shardId) => {
|
|
|
136
144
|
|
|
137
145
|
### Health Monitoring
|
|
138
146
|
|
|
147
|
+
`connect()` initializes all clients and starts background warmup for clients that implement
|
|
148
|
+
`$connect()`, followed by an initial `SELECT 1` when `$queryRaw` is available. Warmup does not delay
|
|
149
|
+
client availability, preserving existing startup behavior. Periodic checks have a deadline, cannot
|
|
150
|
+
overlap, and update the existing `ShardHealth` shape. Deterministic routing still returns the
|
|
151
|
+
record's owner when it is marked unhealthy; cross-shard work schedules healthy, lower-latency
|
|
152
|
+
shards first.
|
|
153
|
+
|
|
139
154
|
```typescript
|
|
140
155
|
// Get health of all shards
|
|
141
156
|
const health = sharding.getHealth();
|
|
@@ -169,6 +184,7 @@ Add to your `package.json`:
|
|
|
169
184
|
{
|
|
170
185
|
"scripts": {
|
|
171
186
|
"db:update": "prisma-sharding-update",
|
|
187
|
+
"migrate:shards": "prisma-sharding-migrate",
|
|
172
188
|
"db:studio": "prisma-sharding-studio",
|
|
173
189
|
"test:shards": "prisma-sharding-test"
|
|
174
190
|
}
|
|
@@ -196,11 +212,12 @@ PRISMA_SHARDING_VERBOSE=false # optional, library lifecycle logs
|
|
|
196
212
|
|
|
197
213
|
#### `prisma-sharding-update` (Recommended)
|
|
198
214
|
|
|
199
|
-
The "All-in-One" command
|
|
200
|
-
|
|
215
|
+
The "All-in-One" development command generates Prisma Client types and synchronizes local/dev
|
|
216
|
+
shards. Use this whenever you change `schema.prisma` during development. It is not a production
|
|
217
|
+
migration workflow.
|
|
201
218
|
|
|
202
219
|
1. Runs `prisma generate` (Updates TypeScript types)
|
|
203
|
-
2. Runs `prisma db push` on all shards (
|
|
220
|
+
2. Runs `prisma db push` on all shards (Synchronizes development databases)
|
|
204
221
|
|
|
205
222
|
```bash
|
|
206
223
|
yarn db:update
|
|
@@ -224,26 +241,34 @@ sync are running. The loader is replaced by the completed row and is disabled fo
|
|
|
224
241
|
Set `SHARD_CLI_VERBOSE=true` or `SHARD_UPDATE_VERBOSE=true` to include Prisma command
|
|
225
242
|
output, masked database URLs, and detailed diagnostics.
|
|
226
243
|
|
|
227
|
-
|
|
228
|
-
You can pass flags like `--force-reset` if you need to wipe data due to schema conflicts.
|
|
244
|
+
Existing flags remain unchanged and are forwarded as provided. For example:
|
|
229
245
|
|
|
230
246
|
```bash
|
|
231
247
|
yarn db:update --force-reset
|
|
232
248
|
|
|
233
249
|
```
|
|
234
250
|
|
|
251
|
+
The command does not inject `--accept-data-loss` automatically.
|
|
252
|
+
|
|
235
253
|
#### `prisma-sharding-migrate`
|
|
236
254
|
|
|
237
|
-
|
|
255
|
+
This is the production/staging path. For each shard it checks migration status and runs
|
|
256
|
+
`prisma migrate deploy`, applying committed migration artifacts without generating the client,
|
|
257
|
+
resetting the database, or using `db push`.
|
|
238
258
|
|
|
239
259
|
```bash
|
|
240
260
|
yarn migrate:shards
|
|
241
261
|
|
|
242
262
|
```
|
|
243
263
|
|
|
264
|
+
Any failed shard makes the command exit non-zero and remains visible in the compact results.
|
|
244
265
|
This command uses the same compact shard rows as `db:update`. Set
|
|
245
266
|
`SHARD_CLI_VERBOSE=true` or `SHARD_MIGRATE_VERBOSE=true` for Prisma command output.
|
|
246
267
|
|
|
268
|
+
Do not use `prisma db push` as a production migration strategy. Commit and review the Prisma
|
|
269
|
+
migration directory, then run `prisma-sharding-migrate` during deployment. Verbose mode includes
|
|
270
|
+
sanitized status/deploy commands, masked database URLs, exit codes, shard IDs, and next-step hints.
|
|
271
|
+
|
|
247
272
|
#### `prisma-sharding-studio`
|
|
248
273
|
|
|
249
274
|
Start Prisma Studio for all shards on sequential ports.
|
|
@@ -387,7 +412,7 @@ Verified 5/5 users on correct shards
|
|
|
387
412
|
| `shards` | `ShardConfig[]` | Required | Array of shard configurations |
|
|
388
413
|
| `strategy` | `'modulo' \| 'consistent-hash'` | `'modulo'` | Routing algorithm |
|
|
389
414
|
| `createClient` | `(url, shardId) => TClient` | Required | Factory to create Prisma clients |
|
|
390
|
-
| `healthCheckIntervalMs` | `number` | `30000` |
|
|
415
|
+
| `healthCheckIntervalMs` | `number` | `30000` | Positive health check frequency |
|
|
391
416
|
| `circuitBreakerThreshold` | `number` | `3` | Failures before marking unhealthy |
|
|
392
417
|
|
|
393
418
|
### Shard Config
|
|
@@ -396,7 +421,7 @@ Verified 5/5 users on correct shards
|
|
|
396
421
|
interface ShardConfig {
|
|
397
422
|
id: string; // Unique identifier (e.g., 'shard_1')
|
|
398
423
|
url: string; // PostgreSQL connection string
|
|
399
|
-
weight?: number; //
|
|
424
|
+
weight?: number; // Positive random-assignment weight only
|
|
400
425
|
isReadReplica?: boolean;
|
|
401
426
|
}
|
|
402
427
|
```
|
|
@@ -413,12 +438,64 @@ strategy: 'modulo';
|
|
|
413
438
|
|
|
414
439
|
### Consistent Hash
|
|
415
440
|
|
|
416
|
-
|
|
441
|
+
Uses a precomputed virtual-node ring and binary search. Custom and non-sequential shard IDs are
|
|
442
|
+
supported. Adding or removing shards still changes ownership for part of the keyspace, so plan data
|
|
443
|
+
movement before changing a production shard list.
|
|
417
444
|
|
|
418
445
|
```typescript
|
|
419
446
|
strategy: 'consistent-hash';
|
|
420
447
|
```
|
|
421
448
|
|
|
449
|
+
## Architecture and Scaling
|
|
450
|
+
|
|
451
|
+
The public `PrismaSharding` layer validates and delegates without changing its established surface.
|
|
452
|
+
Internally, the router owns key placement, the manager owns clients and health state, and one
|
|
453
|
+
cross-shard executor owns concurrency, deadlines, health-aware scheduling, stable result ordering,
|
|
454
|
+
and failure isolation. CLI commands share one shard parser and one sanitized child-process runner.
|
|
455
|
+
|
|
456
|
+
| Layer | Responsibility |
|
|
457
|
+
| --- | --- |
|
|
458
|
+
| Public API | Validate, delegate, and preserve existing result shapes |
|
|
459
|
+
| Router | Stable deterministic placement and weighted random assignment |
|
|
460
|
+
| Shard manager | Client lifecycle, initial verification, health, and shutdown |
|
|
461
|
+
| Cross-shard executor | Shared concurrency, deadlines, ordering, and failure isolation |
|
|
462
|
+
| CLI | Safe migration/update/test/Studio orchestration with compact output |
|
|
463
|
+
|
|
464
|
+
Low-level execution behavior is intentionally internal: fan-out concurrency and deadlines are
|
|
465
|
+
central defaults, the hash function is unchanged, health checks use typed Prisma-like capability
|
|
466
|
+
guards, successful `runOnAll()` results retain configured shard order, and errors stay isolated in
|
|
467
|
+
the existing detailed result shape.
|
|
468
|
+
|
|
469
|
+
Normal request flow should be:
|
|
470
|
+
|
|
471
|
+
```text
|
|
472
|
+
routing key or directory lookup -> one shard -> one Prisma operation
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
`findFirst()` and `runOnAll()` use bounded concurrency and per-shard deadlines, but they still
|
|
476
|
+
multiply database work and tail-latency exposure. Reserve them for recovery, administration, and
|
|
477
|
+
analytics. Pending Prisma queries may not be cancellable after an early `findFirst()` result, so
|
|
478
|
+
the caller can resolve before all already-started database work has physically stopped.
|
|
479
|
+
|
|
480
|
+
The executor deadline limits how long the package waits; it does **not** cancel the underlying
|
|
481
|
+
Prisma or PostgreSQL query. Configure a database-level deadline as well, such as PostgreSQL
|
|
482
|
+
`statement_timeout` or the equivalent adapter/provider query timeout, so timed-out work cannot
|
|
483
|
+
continue consuming database resources indefinitely.
|
|
484
|
+
|
|
485
|
+
### Connection Pool Budgeting
|
|
486
|
+
|
|
487
|
+
Each shard client owns or uses a connection pool. Budget the fleet-wide maximum as:
|
|
488
|
+
|
|
489
|
+
```text
|
|
490
|
+
application instances × shards per instance × connections per shard pool
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
For example, 20 application instances × 8 shards × 10 connections can attempt 1,600 database
|
|
494
|
+
connections. Set the adapter's pool limit and connection timeout deliberately per application
|
|
495
|
+
instance and per shard. At larger fleet sizes, use PgBouncer or provider-managed pooling and verify
|
|
496
|
+
that the database's total connection budget includes migrations, administration, and failover
|
|
497
|
+
headroom. The sharding package does not create hidden extra Prisma clients.
|
|
498
|
+
|
|
422
499
|
## Error Handling
|
|
423
500
|
|
|
424
501
|
```typescript
|
package/dist/cli/migrate.js
CHANGED
|
@@ -89,36 +89,152 @@ var createCliLoader = (label, message, enabled = true) => {
|
|
|
89
89
|
// src/cli/utils/command.ts
|
|
90
90
|
var import_child_process = require("child_process");
|
|
91
91
|
var import_path = __toESM(require("path"));
|
|
92
|
+
|
|
93
|
+
// src/constants/internal.ts
|
|
94
|
+
var INTERNAL_DEFAULTS = {
|
|
95
|
+
HEALTH_CHECK_TIMEOUT_MS: 5e3,
|
|
96
|
+
CROSS_SHARD_CONCURRENCY: 4,
|
|
97
|
+
CROSS_SHARD_TIMEOUT_MS: 1e4,
|
|
98
|
+
CLI_COMMAND_TIMEOUT_MS: 12e4,
|
|
99
|
+
CLI_FORCE_KILL_GRACE_MS: 5e3,
|
|
100
|
+
CLI_MAX_OUTPUT_LENGTH: 1e6,
|
|
101
|
+
CLI_TEST_TCP_TIMEOUT_MS: 5e3,
|
|
102
|
+
CLI_TEST_COMMAND_TIMEOUT_MS: 15e3,
|
|
103
|
+
STUDIO_BASE_PORT: 51212,
|
|
104
|
+
STUDIO_REUSE_EXISTING: true,
|
|
105
|
+
STUDIO_STRICT_PORT_CHECK: false,
|
|
106
|
+
STUDIO_START_TIMEOUT_MS: 15e3,
|
|
107
|
+
STUDIO_STABILITY_MS: 500,
|
|
108
|
+
STUDIO_SHUTDOWN_TIMEOUT_MS: 5e3
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// src/utils/sanitize.ts
|
|
112
|
+
var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
113
|
+
var maskDatabaseUrl = (url) => {
|
|
114
|
+
try {
|
|
115
|
+
const parsed = new URL(url);
|
|
116
|
+
if (parsed.password) {
|
|
117
|
+
parsed.password = "***";
|
|
118
|
+
}
|
|
119
|
+
return parsed.toString();
|
|
120
|
+
} catch {
|
|
121
|
+
return url.replace(/:[^:@]+@/, ":***@");
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
var sanitizeDatabaseText = (output, databaseUrls) => {
|
|
125
|
+
let sanitized = output;
|
|
126
|
+
for (const url of databaseUrls) {
|
|
127
|
+
sanitized = sanitized.replace(new RegExp(escapeRegExp(url), "g"), maskDatabaseUrl(url));
|
|
128
|
+
try {
|
|
129
|
+
const password = new URL(url).password;
|
|
130
|
+
if (password.length >= 4) {
|
|
131
|
+
sanitized = sanitized.replace(
|
|
132
|
+
new RegExp(escapeRegExp(decodeURIComponent(password)), "g"),
|
|
133
|
+
"***"
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return sanitized.replace(
|
|
140
|
+
/(postgres(?:ql)?:\/\/[^:/\s]+:)[^@\s]+(@)/gi,
|
|
141
|
+
"$1***$2"
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// src/cli/utils/process.ts
|
|
146
|
+
var isChildProcessRunning = (childProcess) => {
|
|
147
|
+
return childProcess.exitCode === null && childProcess.signalCode === null;
|
|
148
|
+
};
|
|
149
|
+
var terminateChildProcess = (childProcess, options = {}) => {
|
|
150
|
+
if (!isChildProcessRunning(childProcess)) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
const signal = options.signal || "SIGTERM";
|
|
154
|
+
try {
|
|
155
|
+
if (options.processGroup && childProcess.pid && process.platform !== "win32") {
|
|
156
|
+
try {
|
|
157
|
+
process.kill(-childProcess.pid, signal);
|
|
158
|
+
return true;
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return childProcess.kill(signal);
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// src/cli/utils/command.ts
|
|
92
169
|
var getNpxCommand = () => process.platform === "win32" ? "npx.cmd" : "npx";
|
|
170
|
+
var OUTPUT_TRUNCATED_MARKER = "\n[output truncated]\n";
|
|
171
|
+
var appendBoundedOutput = (current, chunk, limit) => {
|
|
172
|
+
const combined = current + chunk;
|
|
173
|
+
if (combined.length <= limit) {
|
|
174
|
+
return combined;
|
|
175
|
+
}
|
|
176
|
+
if (limit <= OUTPUT_TRUNCATED_MARKER.length) {
|
|
177
|
+
return combined.slice(-limit);
|
|
178
|
+
}
|
|
179
|
+
return OUTPUT_TRUNCATED_MARKER + combined.slice(-(limit - OUTPUT_TRUNCATED_MARKER.length));
|
|
180
|
+
};
|
|
181
|
+
var sanitizeCommandOutput = (output, env = process.env) => {
|
|
182
|
+
const databaseUrls = Object.entries(env).filter(
|
|
183
|
+
([name, value]) => Boolean(value) && (name === "DATABASE_URL" || /^SHARD_\d+_URL$/.test(name))
|
|
184
|
+
).map(([, value]) => value);
|
|
185
|
+
return sanitizeDatabaseText(output, databaseUrls).replace(/(\bpassword\s*[=:]\s*)[^\s,;]+/gi, "$1***");
|
|
186
|
+
};
|
|
93
187
|
var runCommand = (command, args, options = {}) => {
|
|
94
188
|
return new Promise((resolve) => {
|
|
95
189
|
let stdout = "";
|
|
96
190
|
let stderr = "";
|
|
97
191
|
let settled = false;
|
|
192
|
+
let timedOut = false;
|
|
193
|
+
let forceKillTimeout;
|
|
194
|
+
const env = options.env || process.env;
|
|
195
|
+
const timeoutMs = options.timeoutMs ?? INTERNAL_DEFAULTS.CLI_COMMAND_TIMEOUT_MS;
|
|
196
|
+
const forceKillGraceMs = options.forceKillGraceMs ?? INTERNAL_DEFAULTS.CLI_FORCE_KILL_GRACE_MS;
|
|
197
|
+
const maxOutputLength = options.maxOutputLength ?? INTERNAL_DEFAULTS.CLI_MAX_OUTPUT_LENGTH;
|
|
198
|
+
const processGroup = process.platform !== "win32";
|
|
98
199
|
const child = (0, import_child_process.spawn)(command, args, {
|
|
99
|
-
env
|
|
200
|
+
env,
|
|
100
201
|
cwd: import_path.default.resolve(options.cwd || process.cwd()),
|
|
202
|
+
detached: processGroup,
|
|
101
203
|
shell: false,
|
|
102
204
|
stdio: ["ignore", "pipe", "pipe"]
|
|
103
205
|
});
|
|
104
206
|
const settle = (result) => {
|
|
105
207
|
if (!settled) {
|
|
106
208
|
settled = true;
|
|
107
|
-
|
|
209
|
+
clearTimeout(timeout);
|
|
210
|
+
if (forceKillTimeout) {
|
|
211
|
+
clearTimeout(forceKillTimeout);
|
|
212
|
+
}
|
|
213
|
+
const sanitizedResult = {
|
|
214
|
+
...result,
|
|
215
|
+
stdout: sanitizeCommandOutput(result.stdout, env),
|
|
216
|
+
stderr: sanitizeCommandOutput(result.stderr, env),
|
|
217
|
+
error: result.error ? sanitizeCommandOutput(result.error, env) : void 0
|
|
218
|
+
};
|
|
219
|
+
if (options.verbose) {
|
|
220
|
+
if (sanitizedResult.stdout) {
|
|
221
|
+
process.stdout.write(sanitizedResult.stdout);
|
|
222
|
+
}
|
|
223
|
+
if (sanitizedResult.stderr) {
|
|
224
|
+
process.stderr.write(sanitizedResult.stderr);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
resolve(sanitizedResult);
|
|
108
228
|
}
|
|
109
229
|
};
|
|
110
230
|
child.stdout?.on("data", (data) => {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (options.verbose) {
|
|
114
|
-
process.stdout.write(output);
|
|
231
|
+
if (!settled) {
|
|
232
|
+
stdout = appendBoundedOutput(stdout, data.toString(), maxOutputLength);
|
|
115
233
|
}
|
|
116
234
|
});
|
|
117
235
|
child.stderr?.on("data", (data) => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (options.verbose) {
|
|
121
|
-
process.stderr.write(output);
|
|
236
|
+
if (!settled) {
|
|
237
|
+
stderr = appendBoundedOutput(stderr, data.toString(), maxOutputLength);
|
|
122
238
|
}
|
|
123
239
|
});
|
|
124
240
|
child.once("error", (error) => {
|
|
@@ -126,18 +242,35 @@ var runCommand = (command, args, options = {}) => {
|
|
|
126
242
|
success: false,
|
|
127
243
|
stdout,
|
|
128
244
|
stderr,
|
|
129
|
-
|
|
245
|
+
exitCode: null,
|
|
246
|
+
error: timedOut ? `Command timed out after ${timeoutMs}ms` : error.message
|
|
130
247
|
});
|
|
131
248
|
});
|
|
132
249
|
child.once("close", (exitCode) => {
|
|
133
250
|
settle({
|
|
134
|
-
success: exitCode === 0,
|
|
251
|
+
success: !timedOut && exitCode === 0,
|
|
135
252
|
stdout,
|
|
136
253
|
stderr,
|
|
137
254
|
exitCode,
|
|
138
|
-
error: exitCode === 0 ? void 0 : stderr.trim() || stdout.trim() || `Command exited with code ${exitCode}`
|
|
255
|
+
error: timedOut ? `Command timed out after ${timeoutMs}ms` : exitCode === 0 ? void 0 : stderr.trim() || stdout.trim() || `Command exited with code ${exitCode}`
|
|
139
256
|
});
|
|
140
257
|
});
|
|
258
|
+
const timeout = setTimeout(() => {
|
|
259
|
+
timedOut = true;
|
|
260
|
+
terminateChildProcess(child, { signal: "SIGTERM", processGroup });
|
|
261
|
+
forceKillTimeout = setTimeout(() => {
|
|
262
|
+
terminateChildProcess(child, { signal: "SIGKILL", processGroup });
|
|
263
|
+
settle({
|
|
264
|
+
success: false,
|
|
265
|
+
stdout,
|
|
266
|
+
stderr,
|
|
267
|
+
exitCode: null,
|
|
268
|
+
error: `Command timed out after ${timeoutMs}ms`
|
|
269
|
+
});
|
|
270
|
+
}, forceKillGraceMs);
|
|
271
|
+
forceKillTimeout.unref?.();
|
|
272
|
+
}, timeoutMs);
|
|
273
|
+
timeout.unref?.();
|
|
141
274
|
});
|
|
142
275
|
};
|
|
143
276
|
var runPrismaCommand = (args, options = {}) => runCommand(getNpxCommand(), ["prisma", ...args], options);
|
|
@@ -161,44 +294,58 @@ var getShardConfigResult = (env = process.env) => {
|
|
|
161
294
|
}
|
|
162
295
|
return { shards, missingShardIds };
|
|
163
296
|
};
|
|
164
|
-
var
|
|
165
|
-
return getShardConfigResult(env).shards;
|
|
166
|
-
};
|
|
167
|
-
var maskShardUrl = (url) => {
|
|
168
|
-
try {
|
|
169
|
-
const parsed = new URL(url);
|
|
170
|
-
if (parsed.password) {
|
|
171
|
-
parsed.password = "***";
|
|
172
|
-
}
|
|
173
|
-
return parsed.toString();
|
|
174
|
-
} catch {
|
|
175
|
-
return url.replace(/:[^:@]+@/, ":***@");
|
|
176
|
-
}
|
|
177
|
-
};
|
|
297
|
+
var maskShardUrl = maskDatabaseUrl;
|
|
178
298
|
|
|
179
299
|
// src/cli/utils/prisma.ts
|
|
180
|
-
var
|
|
300
|
+
var executeShardCommand = async (shard, commandArgs, verbose, action) => {
|
|
301
|
+
const commandEnv = { ...process.env, DATABASE_URL: shard.url };
|
|
302
|
+
if (verbose) {
|
|
303
|
+
console.log(`
|
|
304
|
+
${action} ${shard.id}...`);
|
|
305
|
+
console.log(`Database: ${maskShardUrl(shard.url)}
|
|
306
|
+
`);
|
|
307
|
+
console.log(
|
|
308
|
+
`Command: ${sanitizeCommandOutput(`prisma ${commandArgs.join(" ")}`, commandEnv)}`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
const result = await runPrismaCommand(commandArgs, {
|
|
312
|
+
env: commandEnv,
|
|
313
|
+
verbose
|
|
314
|
+
});
|
|
315
|
+
if (verbose) {
|
|
316
|
+
console.log(`Exit code: ${result.exitCode ?? "unavailable"}`);
|
|
317
|
+
}
|
|
318
|
+
if (verbose && result.error && !result.stderr.trim() && !result.stdout.trim()) {
|
|
319
|
+
console.error(result.error);
|
|
320
|
+
}
|
|
321
|
+
if (verbose && !result.success) {
|
|
322
|
+
console.error(`Next: verify ${shard.id} connectivity and migration state, then retry.`);
|
|
323
|
+
}
|
|
324
|
+
return result;
|
|
325
|
+
};
|
|
326
|
+
var deployShardMigrations = async (shards, extraArgs, verbose) => {
|
|
181
327
|
const results = [];
|
|
182
328
|
for (const shard of shards) {
|
|
183
|
-
const loader = createCliLoader(shard.id, "
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
const result = await runPrismaCommand(
|
|
191
|
-
["db", "push", "--accept-data-loss", ...extraArgs],
|
|
192
|
-
{
|
|
193
|
-
env: { ...process.env, DATABASE_URL: shard.url },
|
|
194
|
-
verbose
|
|
195
|
-
}
|
|
329
|
+
const loader = createCliLoader(shard.id, "Migrating", !verbose);
|
|
330
|
+
const status = await executeShardCommand(
|
|
331
|
+
shard,
|
|
332
|
+
["migrate", "status"],
|
|
333
|
+
verbose,
|
|
334
|
+
"Checking migration status for"
|
|
196
335
|
);
|
|
197
|
-
if (
|
|
198
|
-
|
|
336
|
+
if (!status.success) {
|
|
337
|
+
results.push({ shardId: shard.id, success: false });
|
|
338
|
+
loader.fail("Failed");
|
|
339
|
+
continue;
|
|
199
340
|
}
|
|
200
|
-
|
|
201
|
-
|
|
341
|
+
const deploy = await executeShardCommand(
|
|
342
|
+
shard,
|
|
343
|
+
["migrate", "deploy", ...extraArgs],
|
|
344
|
+
verbose,
|
|
345
|
+
"Migrating"
|
|
346
|
+
);
|
|
347
|
+
results.push({ shardId: shard.id, success: deploy.success });
|
|
348
|
+
if (deploy.success) {
|
|
202
349
|
loader.succeed("Synced");
|
|
203
350
|
} else {
|
|
204
351
|
loader.fail("Failed");
|
|
@@ -210,14 +357,18 @@ Syncing ${shard.id}...`);
|
|
|
210
357
|
// src/cli/migrate.ts
|
|
211
358
|
var migrateAllShards = async () => {
|
|
212
359
|
const verbose = isVerboseEnv(["SHARD_MIGRATE_VERBOSE", "SHARD_CLI_VERBOSE"]);
|
|
213
|
-
const shards =
|
|
360
|
+
const { shards, missingShardIds } = getShardConfigResult();
|
|
214
361
|
const extraArgs = process.argv.slice(2);
|
|
215
362
|
printCliHeader("\u{1F504}", "Prisma Sharding Migrate");
|
|
363
|
+
if (missingShardIds.length > 0) {
|
|
364
|
+
printCliRow("\u274C", "config", `Missing shard URLs: ${missingShardIds.join(", ")}`);
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
216
367
|
if (shards.length === 0) {
|
|
217
368
|
printCliRow("\u274C", "config", NO_SHARDS_CONFIGURED_MESSAGE);
|
|
218
369
|
process.exit(1);
|
|
219
370
|
}
|
|
220
|
-
const results = await
|
|
371
|
+
const results = await deployShardMigrations(shards, extraArgs, verbose);
|
|
221
372
|
const successful = results.filter((result) => result.success).length;
|
|
222
373
|
const failed = results.length - successful;
|
|
223
374
|
if (failed > 0) {
|