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 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)` | Get Prisma client for a given key |
44
- | `getShardById(shardId)` | Get Prisma client by shard ID |
45
- | `getRandomShard()` | Get random shard (for new records) |
46
- | `findFirst(fn)` | Search across all shards, return first result |
47
- | `runOnAll(fn)` | Execute on all shards |
48
- | `getHealth()` | Get health status of all shards |
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
- const client = sharding.getRandomShard();
58
+ ## Shard Ownership
59
59
 
60
- const user = await client.user.create({
61
- data: {
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
- ## Step 3: Access User by ID (Shard Routing)
63
+ ### Pattern A: Deterministic Ownership
69
64
 
70
- When you have a user ID, Prisma Sharding routes you to the correct shard automatically.
65
+ Generate or obtain the routing key before inserting the record, then use the same key for every
66
+ future operation:
71
67
 
72
- ```ts
73
- const userId = 'abc123';
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 user = await client.user.findUnique({
77
+ const sameUser = await sharding.getShard(userId).user.findUnique({
78
78
  where: { id: userId },
79
79
  });
80
80
  ```
81
81
 
82
- `Important rule:`
83
-
84
- Once you get the shard client using a user ID, **all future operations for that user must use this same client**.
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
- That includes:
87
+ ### Pattern B: Assigned Ownership
87
88
 
88
- - Reading user data
89
- - Updating user data
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
- Every user belongs to exactly one shard. Their entire data lives on that shard only.
93
-
94
- Do **not** switch shards or use a random shard for user related actions.
92
+ ```typescript
93
+ const { client, shardId } = sharding.getRandomShardWithInfo();
94
+ const user = await client.user.create({ data: { email, username } });
95
95
 
96
- Always do this:
96
+ await shardDirectory.create({ data: { recordId: user.id, shardId } });
97
97
 
98
- ```ts
99
- const client = sharding.getShard(userId);
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
- This guarantees all user data stays on the correct shard and avoids cross shard bugs.
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
- ### Step 4: Find User Without ID (Cross Shard Search)
111
+ ### Find Without Ownership Metadata
105
112
 
106
- If you do not have the user ID, search all shards in parallel.
107
- Use this only when necessary.
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
- ## Step 5: Run on All Shards (Admin or Analytics)
132
+ ### Run on All Shards
125
133
 
126
134
  ```typescript
127
- // Get counts from all shards
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. It generates Prisma Client types and migrates all shards.
200
- **Use this whenever you change `schema.prisma`.**
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 (Updates Databases)
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
- **With Flags:**
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
- Only pushes schema to all shards (skips type generation). Useful for production deployment pipelines.
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` | Health check frequency |
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; // Optional weight for distribution
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
- Minimizes data movement when adding/removing shards.
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
@@ -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: options.env || process.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
- resolve(result);
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
- const output = data.toString();
112
- stdout += output;
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
- const output = data.toString();
119
- stderr += output;
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
- error: error.message
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 getShardConfigs = (env = process.env) => {
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 syncShardSchemas = async (shards, extraArgs, verbose) => {
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, "Syncing", !verbose);
184
- if (verbose) {
185
- console.log(`
186
- Syncing ${shard.id}...`);
187
- console.log(`Database: ${maskShardUrl(shard.url)}
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 (verbose && result.error && !result.stderr.trim() && !result.stdout.trim()) {
198
- console.error(result.error);
336
+ if (!status.success) {
337
+ results.push({ shardId: shard.id, success: false });
338
+ loader.fail("Failed");
339
+ continue;
199
340
  }
200
- results.push({ shardId: shard.id, success: result.success });
201
- if (result.success) {
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 = getShardConfigs();
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 syncShardSchemas(shards, extraArgs, verbose);
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) {