prisma-sharding 0.0.5 → 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.
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 |
54
57
 
55
- ```ts
56
- import { sharding } from '@/config/prisma';
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**.
85
-
86
- That includes:
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`.
87
86
 
88
- - Reading user data
89
- - Updating user data
90
- - Creating related records (profiles, posts, settings, etc)
87
+ ### Pattern B: Assigned Ownership
91
88
 
92
- Every user belongs to exactly one shard. Their entire data lives on that shard only.
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:
93
91
 
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
  }
@@ -184,48 +200,155 @@ SHARD_2_URL=postgresql://user:pass@host:5432/db2
184
200
  SHARD_3_URL=postgresql://user:pass@host:5432/db3
185
201
  SHARD_ROUTING_STRATEGY=modulo # or consistent-hash
186
202
  SHARD_STUDIO_BASE_PORT=51212 # optional, for studio
203
+ SHARD_STUDIO_REUSE_EXISTING=true # optional, defaults to true
204
+ SHARD_STUDIO_STRICT_PORT_CHECK=false # optional, defaults to false
205
+ SHARD_STUDIO_START_TIMEOUT_MS=15000 # optional, defaults to 15000
206
+ SHARD_STUDIO_VERBOSE=false # optional, defaults to false
207
+ SHARD_CLI_VERBOSE=false # optional, verbose update/migrate output
208
+ PRISMA_SHARDING_VERBOSE=false # optional, library lifecycle logs
187
209
  ```
188
210
 
189
211
  ### Commands
190
212
 
191
213
  #### `prisma-sharding-update` (Recommended)
192
214
 
193
- The "All-in-One" command. It generates Prisma Client types and migrates all shards.
194
- **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.
195
218
 
196
219
  1. Runs `prisma generate` (Updates TypeScript types)
197
- 2. Runs `prisma db push` on all shards (Updates Databases)
220
+ 2. Runs `prisma db push` on all shards (Synchronizes development databases)
198
221
 
199
222
  ```bash
200
223
  yarn db:update
201
224
 
202
225
  ```
203
226
 
204
- **With Flags:**
205
- You can pass flags like `--force-reset` if you need to wipe data due to schema conflicts.
227
+ Default output stays compact:
228
+
229
+ ```text
230
+ 🔄 Prisma Sharding Update
231
+
232
+ ✅ client Generated
233
+ ✅ shard_1 Synced
234
+ ✅ shard_2 Synced
235
+ ✅ shard_3 Synced
236
+ ```
237
+
238
+ Interactive terminals show a single inline loader while Prisma Client generation and each shard
239
+ sync are running. The loader is replaced by the completed row and is disabled for piped or CI logs.
240
+
241
+ Set `SHARD_CLI_VERBOSE=true` or `SHARD_UPDATE_VERBOSE=true` to include Prisma command
242
+ output, masked database URLs, and detailed diagnostics.
243
+
244
+ Existing flags remain unchanged and are forwarded as provided. For example:
206
245
 
207
246
  ```bash
208
247
  yarn db:update --force-reset
209
248
 
210
249
  ```
211
250
 
251
+ The command does not inject `--accept-data-loss` automatically.
252
+
212
253
  #### `prisma-sharding-migrate`
213
254
 
214
- 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`.
215
258
 
216
259
  ```bash
217
260
  yarn migrate:shards
218
261
 
219
262
  ```
220
263
 
264
+ Any failed shard makes the command exit non-zero and remains visible in the compact results.
265
+ This command uses the same compact shard rows as `db:update`. Set
266
+ `SHARD_CLI_VERBOSE=true` or `SHARD_MIGRATE_VERBOSE=true` for Prisma command output.
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
+
221
272
  #### `prisma-sharding-studio`
222
273
 
223
274
  Start Prisma Studio for all shards on sequential ports.
224
275
 
225
276
  ```bash
226
277
  yarn db:studio
227
- # Opens shard_1 on :51212, shard_2 on :51213, etc.
278
+ ```
228
279
 
280
+ By default, ports are assigned from `SHARD_STUDIO_BASE_PORT`:
281
+
282
+ ```text
283
+ shard_1 -> http://localhost:51212
284
+ shard_2 -> http://localhost:51213
285
+ shard_3 -> http://localhost:51214
286
+ ```
287
+
288
+ Set `SHARD_STUDIO_BASE_PORT` to move the whole range:
289
+
290
+ ```bash
291
+ SHARD_STUDIO_BASE_PORT=52000 yarn db:studio
292
+ # shard_1 -> :52000, shard_2 -> :52001, etc.
293
+ ```
294
+
295
+ Studio startup is safe to run from multiple local APIs. Before starting a shard Studio,
296
+ the CLI checks whether the target port is already active. If it finds an existing Prisma
297
+ Studio on that port, it reuses it instead of spawning another process:
298
+
299
+ ```text
300
+ 🗄️ Prisma Sharding Studio
301
+
302
+ ♻️ shard_1 http://localhost:51212
303
+ ♻️ shard_2 http://localhost:51213
304
+ ♻️ shard_3 http://localhost:51214
305
+ ```
306
+
307
+ If a port is occupied by another process that does not look like Prisma Studio, the shard
308
+ is marked with a warning and the CLI continues with the remaining shards. It will not kill,
309
+ restart, or claim ownership of processes it did not start.
310
+
311
+ Default output is intentionally compact:
312
+
313
+ ```text
314
+ 🗄️ Prisma Sharding Studio
315
+
316
+ ✅ shard_1 http://localhost:51212
317
+ ✅ shard_2 http://localhost:51213
318
+ ✅ shard_3 http://localhost:51214
319
+ ```
320
+
321
+ Run with `SHARD_STUDIO_VERBOSE=true` to print port checks, masked database URLs, Prisma
322
+ Studio child-process output, startup timings, and detailed failure diagnostics.
323
+
324
+ Useful Studio environment variables:
325
+
326
+ - `SHARD_STUDIO_BASE_PORT`: first port in the shard Studio range. Defaults to `51212`.
327
+ - `SHARD_STUDIO_REUSE_EXISTING`: reuse already-running Prisma Studio ports. Defaults to `true`.
328
+ - `SHARD_STUDIO_STRICT_PORT_CHECK`: when `true`, any failed shard makes the command exit
329
+ non-zero after stopping Studio processes started by that run. Defaults to `false`.
330
+ - `SHARD_STUDIO_START_TIMEOUT_MS`: maximum time to wait for a newly spawned Studio to become
331
+ reachable. Defaults to `15000`.
332
+ - `SHARD_STUDIO_STABILITY_MS`: short window a newly-ready Studio process must survive before
333
+ it is reported as started. Defaults to `500`.
334
+ - `SHARD_STUDIO_SHUTDOWN_TIMEOUT_MS`: time to wait for owned Studio processes to close during
335
+ shutdown before sending a force-stop signal. Defaults to `5000`.
336
+ - `SHARD_STUDIO_VERBOSE`: print detailed Studio startup diagnostics. Defaults to `false`.
337
+ - `SHARD_STUDIO_DEBUG`: alias for `SHARD_STUDIO_VERBOSE`.
338
+
339
+ When multiple APIs use the same shard configuration locally, the first API starts the Studio
340
+ processes and later APIs reuse the existing Studio ports. Reused-only commands stay quietly
341
+ attached, preventing process supervisors from printing a normal child-exit message. Pressing
342
+ Ctrl+C only stops Studio processes started by the current CLI run; reused processes are left running.
343
+
344
+ If you run Studio beside `nodemon`, prefer an explicit watch scope for the API process. Prisma
345
+ Studio does not need to write to your app source, but broad nodemon defaults can restart on
346
+ generated TypeScript or JSON files produced by other dev tooling:
347
+
348
+ ```bash
349
+ nodemon --watch src --ext ts,json \
350
+ --ignore 'src/types/*.generated.ts' \
351
+ --exec tsx --env-file=.env --no-warnings src/server.ts
229
352
  ```
230
353
 
231
354
  #### `prisma-sharding-test`
@@ -289,7 +412,7 @@ Verified 5/5 users on correct shards
289
412
  | `shards` | `ShardConfig[]` | Required | Array of shard configurations |
290
413
  | `strategy` | `'modulo' \| 'consistent-hash'` | `'modulo'` | Routing algorithm |
291
414
  | `createClient` | `(url, shardId) => TClient` | Required | Factory to create Prisma clients |
292
- | `healthCheckIntervalMs` | `number` | `30000` | Health check frequency |
415
+ | `healthCheckIntervalMs` | `number` | `30000` | Positive health check frequency |
293
416
  | `circuitBreakerThreshold` | `number` | `3` | Failures before marking unhealthy |
294
417
 
295
418
  ### Shard Config
@@ -298,7 +421,7 @@ Verified 5/5 users on correct shards
298
421
  interface ShardConfig {
299
422
  id: string; // Unique identifier (e.g., 'shard_1')
300
423
  url: string; // PostgreSQL connection string
301
- weight?: number; // Optional weight for distribution
424
+ weight?: number; // Positive random-assignment weight only
302
425
  isReadReplica?: boolean;
303
426
  }
304
427
  ```
@@ -315,12 +438,64 @@ strategy: 'modulo';
315
438
 
316
439
  ### Consistent Hash
317
440
 
318
- 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.
319
444
 
320
445
  ```typescript
321
446
  strategy: 'consistent-hash';
322
447
  ```
323
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
+
324
499
  ## Error Handling
325
500
 
326
501
  ```typescript
@@ -337,6 +512,9 @@ try {
337
512
 
338
513
  ## Custom Logger
339
514
 
515
+ The default logger prints warnings and errors only. Set `PRISMA_SHARDING_VERBOSE=true` to include
516
+ initialization, shard connection, and shutdown lifecycle messages.
517
+
340
518
  ```typescript
341
519
  const sharding = new PrismaSharding({
342
520
  // ...config,