alchemy 0.87.0 → 0.90.0

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.
Files changed (79) hide show
  1. package/bin/alchemy.js +485 -543
  2. package/lib/cloudflare/compatibility-date.d.ts +1 -1
  3. package/lib/cloudflare/compatibility-date.d.ts.map +1 -1
  4. package/lib/cloudflare/dns-records.d.ts +6 -0
  5. package/lib/cloudflare/dns-records.d.ts.map +1 -1
  6. package/lib/cloudflare/dns-records.js +14 -12
  7. package/lib/cloudflare/dns-records.js.map +1 -1
  8. package/lib/cloudflare/index.d.ts +1 -0
  9. package/lib/cloudflare/index.d.ts.map +1 -1
  10. package/lib/cloudflare/index.js +1 -0
  11. package/lib/cloudflare/index.js.map +1 -1
  12. package/lib/cloudflare/r2-bucket-notification.d.ts +312 -0
  13. package/lib/cloudflare/r2-bucket-notification.d.ts.map +1 -0
  14. package/lib/cloudflare/r2-bucket-notification.js +387 -0
  15. package/lib/cloudflare/r2-bucket-notification.js.map +1 -0
  16. package/lib/cloudflare/worker.d.ts.map +1 -1
  17. package/lib/cloudflare/worker.js +8 -1
  18. package/lib/cloudflare/worker.js.map +1 -1
  19. package/lib/cloudflare/workflow.d.ts +17 -0
  20. package/lib/cloudflare/workflow.d.ts.map +1 -1
  21. package/lib/cloudflare/workflow.js +1 -0
  22. package/lib/cloudflare/workflow.js.map +1 -1
  23. package/lib/cloudflare/wrangler.json.js +1 -0
  24. package/lib/cloudflare/wrangler.json.js.map +1 -1
  25. package/lib/docker/api.d.ts +40 -0
  26. package/lib/docker/api.d.ts.map +1 -1
  27. package/lib/docker/api.js +30 -0
  28. package/lib/docker/api.js.map +1 -1
  29. package/lib/docker/network.d.ts.map +1 -1
  30. package/lib/docker/network.js +24 -7
  31. package/lib/docker/network.js.map +1 -1
  32. package/lib/encrypt.d.ts.map +1 -1
  33. package/lib/encrypt.js +2 -2
  34. package/lib/encrypt.js.map +1 -1
  35. package/lib/planetscale/api.d.ts +1 -0
  36. package/lib/planetscale/api.d.ts.map +1 -1
  37. package/lib/planetscale/api.js +1 -1
  38. package/lib/planetscale/api.js.map +1 -1
  39. package/lib/planetscale/branch.d.ts +28 -0
  40. package/lib/planetscale/branch.d.ts.map +1 -1
  41. package/lib/planetscale/branch.js +12 -0
  42. package/lib/planetscale/branch.js.map +1 -1
  43. package/lib/planetscale/database-extensions.d.ts +515 -0
  44. package/lib/planetscale/database-extensions.d.ts.map +1 -0
  45. package/lib/planetscale/database-extensions.js +389 -0
  46. package/lib/planetscale/database-extensions.js.map +1 -0
  47. package/lib/planetscale/database.d.ts +40 -2
  48. package/lib/planetscale/database.d.ts.map +1 -1
  49. package/lib/planetscale/database.js +76 -1
  50. package/lib/planetscale/database.js.map +1 -1
  51. package/lib/scope.d.ts.map +1 -1
  52. package/lib/scope.js +1 -2
  53. package/lib/scope.js.map +1 -1
  54. package/lib/util/telemetry.d.ts +1 -6
  55. package/lib/util/telemetry.d.ts.map +1 -1
  56. package/lib/util/telemetry.js.map +1 -1
  57. package/package.json +6 -6
  58. package/src/cloudflare/dns-records.ts +25 -16
  59. package/src/cloudflare/index.ts +1 -0
  60. package/src/cloudflare/r2-bucket-notification.ts +809 -0
  61. package/src/cloudflare/worker.ts +8 -1
  62. package/src/cloudflare/workflow.ts +18 -0
  63. package/src/cloudflare/wrangler.json.ts +1 -0
  64. package/src/docker/api.ts +61 -0
  65. package/src/docker/network.ts +27 -7
  66. package/src/encrypt.ts +2 -0
  67. package/src/planetscale/api.ts +1 -1
  68. package/src/planetscale/branch.ts +45 -0
  69. package/src/planetscale/database-extensions.ts +947 -0
  70. package/src/planetscale/database.ts +141 -2
  71. package/src/scope.ts +1 -2
  72. package/src/util/telemetry.ts +1 -20
  73. package/workers/cloudflare-state-store.js +57 -57
  74. package/workers/tunnel-proxy.js +1 -1
  75. package/lib/state/instrumented-state-store.d.ts +0 -19
  76. package/lib/state/instrumented-state-store.d.ts.map +0 -1
  77. package/lib/state/instrumented-state-store.js +0 -68
  78. package/lib/state/instrumented-state-store.js.map +0 -1
  79. package/src/state/instrumented-state-store.ts +0 -108
@@ -0,0 +1,947 @@
1
+ import { poll } from "../util/poll.ts";
2
+ import { extractToken, type PlanetScaleProps } from "./api.ts";
3
+ import type { Branch } from "./branch.ts";
4
+ import type { Database } from "./database.ts";
5
+
6
+ /**
7
+ * Base libraries that PlanetScale always includes in shared_preload_libraries.
8
+ * These are managed internally and should not be toggled by users.
9
+ * @internal
10
+ */
11
+ const BASE_LIBRARIES = ["pg_pscale_utils", "pg_readonly", "pgextwlist"];
12
+
13
+ /**
14
+ * Configuration for the pg_cron extension.
15
+ * Schedules and runs PostgreSQL commands inside the database.
16
+ */
17
+ export interface PgCronConfig {
18
+ /**
19
+ * The database in which pg_cron metadata is kept.
20
+ * @default "postgres"
21
+ */
22
+ databaseName?: string;
23
+ /**
24
+ * Whether to launch active jobs when pg_cron starts.
25
+ * @default "on"
26
+ */
27
+ launchActiveJobs?: "on" | "off";
28
+ /**
29
+ * Minimum message level for pg_cron log messages.
30
+ * @default "warning"
31
+ */
32
+ logMinMessages?: "error" | "warning" | "notice" | "info" | "log" | "debug";
33
+ /**
34
+ * Whether to log each job run into the job_run_details table.
35
+ * @default "on"
36
+ */
37
+ logRun?: "on" | "off";
38
+ /**
39
+ * Whether to log the SQL statement of each job.
40
+ * @default "on"
41
+ */
42
+ logStatement?: "on" | "off";
43
+ /**
44
+ * Maximum number of concurrently running jobs.
45
+ * @default 1
46
+ */
47
+ maxRunningJobs?: number;
48
+ }
49
+
50
+ /**
51
+ * Configuration for the pg_duckdb extension.
52
+ * Embeds DuckDB inside PostgreSQL for analytical queries.
53
+ */
54
+ export interface PgDuckdbConfig {
55
+ /**
56
+ * The PostgreSQL role used by DuckDB.
57
+ * @default "pscale_superuser"
58
+ */
59
+ postgresRole?: string;
60
+ /**
61
+ * Memory limit for DuckDB. 0 means unlimited.
62
+ * @default "0"
63
+ */
64
+ memoryLimit?: string;
65
+ }
66
+
67
+ /**
68
+ * Configuration for the pg_hint_plan extension.
69
+ * Controls query execution plans using hints in SQL comments.
70
+ */
71
+ export interface PgHintPlanConfig {
72
+ /**
73
+ * Whether to enable hint processing.
74
+ * @default "on"
75
+ */
76
+ enableHint?: "on" | "off";
77
+ /**
78
+ * Whether to enable the hint table feature.
79
+ * @default "off"
80
+ */
81
+ enableHintTable?: "on" | "off";
82
+ /**
83
+ * Message level for hint parsing messages.
84
+ * @default "info"
85
+ */
86
+ parseMessages?: "error" | "warning" | "notice" | "info" | "log" | "debug";
87
+ /**
88
+ * Controls debug output of query plans.
89
+ * @default "off"
90
+ */
91
+ debugPrint?: "off" | "on" | "detailed" | "verbose";
92
+ /**
93
+ * Message level for debug output.
94
+ * @default "info"
95
+ */
96
+ messageLevel?: "error" | "warning" | "notice" | "info" | "log" | "debug";
97
+ }
98
+
99
+ /**
100
+ * Configuration for the pg_partman background worker extension.
101
+ * Automates partition management for time-based and serial-based partitioning.
102
+ */
103
+ export interface PgPartmanBgwConfig {
104
+ /**
105
+ * How often (in seconds) the background worker runs maintenance.
106
+ * @default 3600
107
+ */
108
+ interval?: number;
109
+ /**
110
+ * The database to connect to for maintenance.
111
+ * @default "postgres"
112
+ */
113
+ dbname?: string;
114
+ /**
115
+ * The role to use when connecting.
116
+ * @default "postgres"
117
+ */
118
+ role?: string;
119
+ /**
120
+ * Whether to run ANALYZE on partitions after maintenance.
121
+ * @default "off"
122
+ */
123
+ analyze?: "on" | "off";
124
+ /**
125
+ * Whether to use pg_jobmon for logging.
126
+ * @default "on"
127
+ */
128
+ jobmon?: "on" | "off";
129
+ }
130
+
131
+ /**
132
+ * Configuration for the pg_squeeze extension.
133
+ * Removes dead tuples from tables with minimal locking.
134
+ */
135
+ export interface PgSqueezeConfig {
136
+ /**
137
+ * Maximum time (in milliseconds) to hold an exclusive lock during processing. 0 means no limit.
138
+ * @default 0
139
+ */
140
+ maxXlockTime?: number;
141
+ /**
142
+ * Databases in which the squeeze worker auto-starts, comma-separated.
143
+ * @default "postgres"
144
+ */
145
+ workerAutostart?: string;
146
+ /**
147
+ * Number of squeeze workers per database.
148
+ * @default 1
149
+ */
150
+ workersPerDatabase?: number;
151
+ }
152
+
153
+ /**
154
+ * Configuration for the pg_stat_statements extension.
155
+ * Tracks planning and execution statistics of all SQL statements.
156
+ */
157
+ export interface PgStatStatementsConfig {
158
+ /**
159
+ * Maximum number of statements tracked.
160
+ * @default 5000
161
+ */
162
+ max?: number;
163
+ /**
164
+ * Which statements to track.
165
+ * @default "top"
166
+ */
167
+ track?: "top" | "all";
168
+ /**
169
+ * Whether to track utility commands.
170
+ * @default "on"
171
+ */
172
+ trackUtility?: "on" | "off";
173
+ /**
174
+ * Whether to track planning statistics.
175
+ * @default "off"
176
+ */
177
+ trackPlanning?: "on" | "off";
178
+ /**
179
+ * Whether to save statistics across server restarts.
180
+ * @default "on"
181
+ */
182
+ save?: "on" | "off";
183
+ }
184
+
185
+ /**
186
+ * Configuration for the timescaledb extension.
187
+ * Provides time-series data support for PostgreSQL.
188
+ */
189
+ export interface TimescaledbConfig {
190
+ /**
191
+ * Log level for the background worker.
192
+ * @default "warning"
193
+ */
194
+ bgwLogLevel?: "error" | "warning" | "notice" | "info" | "log" | "debug";
195
+ /**
196
+ * Enable chunk append optimization.
197
+ * @default "on"
198
+ */
199
+ enableChunkAppend?: "on" | "off";
200
+ /**
201
+ * Enable chunk skipping optimization.
202
+ * @default "off"
203
+ */
204
+ enableChunkSkipping?: "on" | "off";
205
+ /**
206
+ * Enable constraint-aware append optimization.
207
+ * @default "on"
208
+ */
209
+ enableConstraintAwareAppend?: "on" | "off";
210
+ /**
211
+ * Enable constraint exclusion optimization.
212
+ * @default "on"
213
+ */
214
+ enableConstraintExclusion?: "on" | "off";
215
+ /**
216
+ * Enable custom hash aggregate optimization.
217
+ * @default "off"
218
+ */
219
+ enableCustomHashagg?: "on" | "off";
220
+ /**
221
+ * Enable deprecation warnings.
222
+ * @default "on"
223
+ */
224
+ enableDeprecationWarnings?: "on" | "off";
225
+ /**
226
+ * Enable event triggers.
227
+ * @default "off"
228
+ */
229
+ enableEventTriggers?: "on" | "off";
230
+ /**
231
+ * Enable foreign key propagation.
232
+ * @default "on"
233
+ */
234
+ enableForeignKeyPropagation?: "on" | "off";
235
+ /**
236
+ * Enable logging of job execution.
237
+ * @default "off"
238
+ */
239
+ enableJobExecutionLogging?: "on" | "off";
240
+ /**
241
+ * Enable now() constification.
242
+ * @default "on"
243
+ */
244
+ enableNowConstify?: "on" | "off";
245
+ /**
246
+ * Enable query optimizations.
247
+ * @default "on"
248
+ */
249
+ enableOptimizations?: "on" | "off";
250
+ /**
251
+ * Enable ordered append optimization.
252
+ * @default "on"
253
+ */
254
+ enableOrderedAppend?: "on" | "off";
255
+ /**
256
+ * Enable parallel chunk append optimization.
257
+ * @default "on"
258
+ */
259
+ enableParallelChunkAppend?: "on" | "off";
260
+ /**
261
+ * Enable qual propagation optimization.
262
+ * @default "on"
263
+ */
264
+ enableQualPropagation?: "on" | "off";
265
+ /**
266
+ * Enable runtime exclusion optimization.
267
+ * @default "on"
268
+ */
269
+ enableRuntimeExclusion?: "on" | "off";
270
+ /**
271
+ * Enable tiered reads.
272
+ * @default "on"
273
+ */
274
+ enableTieredReads?: "on" | "off";
275
+ /**
276
+ * Enable TSS callbacks.
277
+ * @default "on"
278
+ */
279
+ enableTssCallbacks?: "on" | "off";
280
+ /**
281
+ * Maximum number of cached chunks per hypertable.
282
+ * @default 1024
283
+ */
284
+ maxCachedChunksPerHypertable?: number;
285
+ /**
286
+ * Maximum number of open chunks per insert.
287
+ * @default 1024
288
+ */
289
+ maxOpenChunksPerInsert?: number;
290
+ /**
291
+ * Whether the database is being restored from a backup.
292
+ * @default "off"
293
+ */
294
+ restoring?: "on" | "off";
295
+ }
296
+
297
+ /**
298
+ * Configuration for the pgvector extension.
299
+ * Provides vector similarity search for PostgreSQL.
300
+ */
301
+ export interface PgvectorConfig {
302
+ /**
303
+ * Size of the dynamic candidate list for HNSW search.
304
+ * @default 40
305
+ */
306
+ hnswEfSearch?: number;
307
+ /**
308
+ * Whether to enable iterative scan for HNSW indexes.
309
+ * @default "off"
310
+ */
311
+ hnswIterativeScan?: "off" | "relaxed_order" | "strict_order";
312
+ /**
313
+ * Maximum number of tuples to scan with HNSW iterative scan.
314
+ * @default 20000
315
+ */
316
+ hnswMaxScanTuples?: number;
317
+ /**
318
+ * Memory multiplier for HNSW scans.
319
+ * @default 1
320
+ */
321
+ hnswScanMemMultiplier?: number;
322
+ /**
323
+ * Number of probes for IVFFlat index search.
324
+ * @default 1
325
+ */
326
+ ivfflatProbes?: number;
327
+ /**
328
+ * Whether to enable iterative scan for IVFFlat indexes.
329
+ * @default "off"
330
+ */
331
+ ivfflatIterativeScan?: "off" | "relaxed_order" | "strict_order";
332
+ /**
333
+ * Maximum number of probes for IVFFlat iterative scan.
334
+ * @default 32768
335
+ */
336
+ ivfflatMaxProbes?: number;
337
+ }
338
+
339
+ /**
340
+ * Configuration for the pginsights extension.
341
+ * Provides query performance insights.
342
+ */
343
+ export interface PgInsightsConfig {
344
+ /**
345
+ * Whether to collect raw (un-normalized) queries.
346
+ * @default "off"
347
+ */
348
+ rawQueries?: "on" | "off";
349
+ /**
350
+ * Whether to normalize schema names in queries.
351
+ * @default "off"
352
+ */
353
+ normalizeSchemaNames?: "on" | "off";
354
+ }
355
+
356
+ /**
357
+ * PostgreSQL database extensions configuration.
358
+ *
359
+ * Each property represents an extension. If present (even as `{}`), the extension
360
+ * is enabled with the given config (or defaults). If absent/undefined, the extension
361
+ * is disabled.
362
+ */
363
+ export interface DatabaseExtensions {
364
+ /**
365
+ * pg_cron - Job scheduler for PostgreSQL.
366
+ * Schedules and runs PostgreSQL commands on a recurring basis.
367
+ */
368
+ pgCron?: PgCronConfig;
369
+ /**
370
+ * pg_duckdb - Embedded DuckDB for analytical queries.
371
+ */
372
+ pgDuckdb?: PgDuckdbConfig;
373
+ /**
374
+ * pg_hint_plan - Query plan hints.
375
+ * Controls execution plans using hints in SQL comments.
376
+ */
377
+ pgHintPlan?: PgHintPlanConfig;
378
+ /**
379
+ * pg_partman (background worker) - Partition management automation.
380
+ */
381
+ pgPartmanBgw?: PgPartmanBgwConfig;
382
+ /**
383
+ * pg_squeeze - Dead tuple removal with minimal locking.
384
+ */
385
+ pgSqueeze?: PgSqueezeConfig;
386
+ /**
387
+ * pg_stat_statements - Query execution statistics tracking.
388
+ */
389
+ pgStatStatements?: PgStatStatementsConfig;
390
+ /**
391
+ * pg_strict - Stricter SQL behavior for PostgreSQL.
392
+ * This extension has no configurable parameters.
393
+ */
394
+ pgStrict?: Record<string, never>;
395
+ /**
396
+ * timescaledb - Time-series data support.
397
+ */
398
+ timescaledb?: TimescaledbConfig;
399
+ /**
400
+ * pgvector - Vector similarity search.
401
+ */
402
+ vector?: PgvectorConfig;
403
+ /**
404
+ * pginsights - Query performance insights.
405
+ */
406
+ pgInsights?: PgInsightsConfig;
407
+ }
408
+
409
+ /**
410
+ * A single change within an extensions diff: add, remove, or update.
411
+ * @internal
412
+ */
413
+ export interface ExtensionDiffEntry {
414
+ /**
415
+ * The shared_preload_libraries name (e.g. "pg_cron", "vector").
416
+ */
417
+ library: string;
418
+ /**
419
+ * "add" = extension is being enabled,
420
+ * "remove" = extension is being disabled,
421
+ * "update" = extension params changed (stays enabled).
422
+ */
423
+ action: "add" | "remove" | "update";
424
+ /**
425
+ * The pgconf parameter key-value pairs to send for this extension.
426
+ * Empty for "remove" actions.
427
+ */
428
+ params: Record<string, string>;
429
+ }
430
+
431
+ /**
432
+ * Result of diffing two DatabaseExtensions objects.
433
+ * @internal
434
+ */
435
+ export interface ExtensionsDiff {
436
+ /**
437
+ * Individual changes per extension.
438
+ */
439
+ changes: ExtensionDiffEntry[];
440
+ /**
441
+ * True if there are any changes at all.
442
+ */
443
+ hasChanges: boolean;
444
+ }
445
+
446
+ // ---------------------------------------------------------------------------
447
+ // Mapping from our camelCase config keys to the pgconf parameter names
448
+ // ---------------------------------------------------------------------------
449
+
450
+ /** @internal */
451
+ interface ParamMapping {
452
+ library: string;
453
+ params: Record<string, string>;
454
+ }
455
+
456
+ function mapPgCron(config: PgCronConfig): ParamMapping {
457
+ return {
458
+ library: "pg_cron",
459
+ params: {
460
+ "cron.database_name": config.databaseName ?? "postgres",
461
+ "cron.launch_active_jobs": config.launchActiveJobs ?? "on",
462
+ "cron.log_min_messages": config.logMinMessages ?? "warning",
463
+ "cron.log_run": config.logRun ?? "on",
464
+ "cron.log_statement": config.logStatement ?? "on",
465
+ "cron.max_running_jobs": String(config.maxRunningJobs ?? 1),
466
+ },
467
+ };
468
+ }
469
+
470
+ function mapPgDuckdb(config: PgDuckdbConfig): ParamMapping {
471
+ return {
472
+ library: "pg_duckdb",
473
+ params: {
474
+ "duckdb.postgres_role": config.postgresRole ?? "pscale_superuser",
475
+ "duckdb.memory_limit": config.memoryLimit ?? "0",
476
+ },
477
+ };
478
+ }
479
+
480
+ function mapPgHintPlan(config: PgHintPlanConfig): ParamMapping {
481
+ return {
482
+ library: "pg_hint_plan",
483
+ params: {
484
+ "pg_hint_plan.enable_hint": config.enableHint ?? "on",
485
+ "pg_hint_plan.enable_hint_table": config.enableHintTable ?? "off",
486
+ "pg_hint_plan.parse_messages": config.parseMessages ?? "info",
487
+ "pg_hint_plan.debug_print": config.debugPrint ?? "off",
488
+ "pg_hint_plan.message_level": config.messageLevel ?? "info",
489
+ },
490
+ };
491
+ }
492
+
493
+ function mapPgPartmanBgw(config: PgPartmanBgwConfig): ParamMapping {
494
+ return {
495
+ library: "pg_partman_bgw",
496
+ params: {
497
+ "pg_partman_bgw.interval": String(config.interval ?? 3600),
498
+ "pg_partman_bgw.dbname": config.dbname ?? "postgres",
499
+ "pg_partman_bgw.role": config.role ?? "postgres",
500
+ "pg_partman_bgw.analyze": config.analyze ?? "off",
501
+ "pg_partman_bgw.jobmon": config.jobmon ?? "on",
502
+ },
503
+ };
504
+ }
505
+
506
+ function mapPgSqueeze(config: PgSqueezeConfig): ParamMapping {
507
+ return {
508
+ library: "pg_squeeze",
509
+ params: {
510
+ "squeeze.max_xlock_time": String(config.maxXlockTime ?? 0),
511
+ "squeeze.worker_autostart": config.workerAutostart ?? "postgres",
512
+ "squeeze.workers_per_database": String(config.workersPerDatabase ?? 1),
513
+ },
514
+ };
515
+ }
516
+
517
+ function mapPgStatStatements(config: PgStatStatementsConfig): ParamMapping {
518
+ return {
519
+ library: "pg_stat_statements",
520
+ params: {
521
+ "pg_stat_statements.max": String(config.max ?? 5000),
522
+ "pg_stat_statements.track": config.track ?? "top",
523
+ "pg_stat_statements.track_utility": config.trackUtility ?? "on",
524
+ "pg_stat_statements.track_planning": config.trackPlanning ?? "off",
525
+ "pg_stat_statements.save": config.save ?? "on",
526
+ },
527
+ };
528
+ }
529
+
530
+ function mapTimescaledb(config: TimescaledbConfig): ParamMapping {
531
+ return {
532
+ library: "timescaledb",
533
+ params: {
534
+ "timescaledb.bgw_log_level": config.bgwLogLevel ?? "warning",
535
+ "timescaledb.enable_chunk_append": config.enableChunkAppend ?? "on",
536
+ "timescaledb.enable_chunk_skipping": config.enableChunkSkipping ?? "off",
537
+ "timescaledb.enable_constraint_aware_append":
538
+ config.enableConstraintAwareAppend ?? "on",
539
+ "timescaledb.enable_constraint_exclusion":
540
+ config.enableConstraintExclusion ?? "on",
541
+ "timescaledb.enable_custom_hashagg": config.enableCustomHashagg ?? "off",
542
+ "timescaledb.enable_deprecation_warnings":
543
+ config.enableDeprecationWarnings ?? "on",
544
+ "timescaledb.enable_event_triggers": config.enableEventTriggers ?? "off",
545
+ "timescaledb.enable_foreign_key_propagation":
546
+ config.enableForeignKeyPropagation ?? "on",
547
+ "timescaledb.enable_job_execution_logging":
548
+ config.enableJobExecutionLogging ?? "off",
549
+ "timescaledb.enable_now_constify": config.enableNowConstify ?? "on",
550
+ "timescaledb.enable_optimizations": config.enableOptimizations ?? "on",
551
+ "timescaledb.enable_ordered_append": config.enableOrderedAppend ?? "on",
552
+ "timescaledb.enable_parallel_chunk_append":
553
+ config.enableParallelChunkAppend ?? "on",
554
+ "timescaledb.enable_qual_propagation":
555
+ config.enableQualPropagation ?? "on",
556
+ "timescaledb.enable_runtime_exclusion":
557
+ config.enableRuntimeExclusion ?? "on",
558
+ "timescaledb.enable_tiered_reads": config.enableTieredReads ?? "on",
559
+ "timescaledb.enable_tss_callbacks": config.enableTssCallbacks ?? "on",
560
+ "timescaledb.max_cached_chunks_per_hypertable": String(
561
+ config.maxCachedChunksPerHypertable ?? 1024,
562
+ ),
563
+ "timescaledb.max_open_chunks_per_insert": String(
564
+ config.maxOpenChunksPerInsert ?? 1024,
565
+ ),
566
+ "timescaledb.restoring": config.restoring ?? "off",
567
+ },
568
+ };
569
+ }
570
+
571
+ function mapVector(config: PgvectorConfig): ParamMapping {
572
+ return {
573
+ library: "vector",
574
+ params: {
575
+ "hnsw.ef_search": String(config.hnswEfSearch ?? 40),
576
+ "hnsw.iterative_scan": config.hnswIterativeScan ?? "off",
577
+ "hnsw.max_scan_tuples": String(config.hnswMaxScanTuples ?? 20000),
578
+ "hnsw.scan_mem_multiplier": String(config.hnswScanMemMultiplier ?? 1),
579
+ "ivfflat.probes": String(config.ivfflatProbes ?? 1),
580
+ "ivfflat.iterative_scan": config.ivfflatIterativeScan ?? "off",
581
+ "ivfflat.max_probes": String(config.ivfflatMaxProbes ?? 32768),
582
+ },
583
+ };
584
+ }
585
+
586
+ function mapPgInsights(config: PgInsightsConfig): ParamMapping {
587
+ return {
588
+ library: "pginsights",
589
+ params: {
590
+ "pginsights.raw_queries": config.rawQueries ?? "off",
591
+ "pginsights.normalize_schema_names": config.normalizeSchemaNames ?? "off",
592
+ },
593
+ };
594
+ }
595
+
596
+ /**
597
+ * Converts a DatabaseExtensions object into a map of library name -> pgconf params.
598
+ * Only includes extensions that are present (enabled).
599
+ * @internal
600
+ */
601
+ export function extensionsToParamMappings(
602
+ extensions: DatabaseExtensions,
603
+ ): Map<string, Record<string, string>> {
604
+ const mappings = new Map<string, Record<string, string>>();
605
+
606
+ if (extensions.pgCron !== undefined) {
607
+ const m = mapPgCron(extensions.pgCron);
608
+ mappings.set(m.library, m.params);
609
+ }
610
+ if (extensions.pgDuckdb !== undefined) {
611
+ const m = mapPgDuckdb(extensions.pgDuckdb);
612
+ mappings.set(m.library, m.params);
613
+ }
614
+ if (extensions.pgHintPlan !== undefined) {
615
+ const m = mapPgHintPlan(extensions.pgHintPlan);
616
+ mappings.set(m.library, m.params);
617
+ }
618
+ if (extensions.pgPartmanBgw !== undefined) {
619
+ const m = mapPgPartmanBgw(extensions.pgPartmanBgw);
620
+ mappings.set(m.library, m.params);
621
+ }
622
+ if (extensions.pgSqueeze !== undefined) {
623
+ const m = mapPgSqueeze(extensions.pgSqueeze);
624
+ mappings.set(m.library, m.params);
625
+ }
626
+ if (extensions.pgStatStatements !== undefined) {
627
+ const m = mapPgStatStatements(extensions.pgStatStatements);
628
+ mappings.set(m.library, m.params);
629
+ }
630
+ if (extensions.pgStrict !== undefined) {
631
+ mappings.set("pg_strict", {});
632
+ }
633
+ if (extensions.timescaledb !== undefined) {
634
+ const m = mapTimescaledb(extensions.timescaledb);
635
+ mappings.set(m.library, m.params);
636
+ }
637
+ if (extensions.vector !== undefined) {
638
+ const m = mapVector(extensions.vector);
639
+ mappings.set(m.library, m.params);
640
+ }
641
+ if (extensions.pgInsights !== undefined) {
642
+ const m = mapPgInsights(extensions.pgInsights);
643
+ mappings.set(m.library, m.params);
644
+ }
645
+
646
+ return mappings;
647
+ }
648
+
649
+ /**
650
+ * Computes the diff between two extension configurations.
651
+ *
652
+ * @param current - The currently active extensions (or `undefined`/`{}` if none)
653
+ * @param desired - The desired extensions configuration
654
+ * @returns An ExtensionsDiff describing what needs to change
655
+ *
656
+ * @example
657
+ * ```ts
658
+ * const diff = diffExtensions(
659
+ * { pgCron: { maxRunningJobs: 1 } },
660
+ * { pgCron: { maxRunningJobs: 5 }, vector: {} },
661
+ * );
662
+ * // diff.changes = [
663
+ * // { library: "pg_cron", action: "update", params: { "cron.max_running_jobs": "5" } },
664
+ * // { library: "vector", action: "add", params: {} },
665
+ * // ]
666
+ * ```
667
+ */
668
+ export function diffExtensions(
669
+ current: DatabaseExtensions | undefined,
670
+ desired: DatabaseExtensions | undefined,
671
+ ): ExtensionsDiff {
672
+ const currentMappings = extensionsToParamMappings(current ?? {});
673
+ const desiredMappings = extensionsToParamMappings(desired ?? {});
674
+
675
+ const changes: ExtensionDiffEntry[] = [];
676
+
677
+ // Find additions and updates
678
+ for (const [library, desiredParams] of desiredMappings) {
679
+ const currentParams = currentMappings.get(library);
680
+ if (!currentParams) {
681
+ // Extension is being added
682
+ changes.push({ library, action: "add", params: desiredParams });
683
+ } else {
684
+ // Extension exists — check if params changed
685
+ const paramsChanged =
686
+ JSON.stringify(sortedEntries(currentParams)) !==
687
+ JSON.stringify(sortedEntries(desiredParams));
688
+ if (paramsChanged) {
689
+ changes.push({ library, action: "update", params: desiredParams });
690
+ }
691
+ }
692
+ }
693
+
694
+ // Find removals
695
+ for (const [library] of currentMappings) {
696
+ if (!desiredMappings.has(library)) {
697
+ changes.push({ library, action: "remove", params: {} });
698
+ }
699
+ }
700
+
701
+ return {
702
+ changes,
703
+ hasChanges: changes.length > 0,
704
+ };
705
+ }
706
+
707
+ function sortedEntries(obj: Record<string, string>): [string, string][] {
708
+ return Object.entries(obj).sort(([a], [b]) => a.localeCompare(b));
709
+ }
710
+
711
+ // ---------------------------------------------------------------------------
712
+ // API interaction — reverse-engineered from PlanetScale dashboard HAR captures
713
+ // ---------------------------------------------------------------------------
714
+
715
+ /**
716
+ * Options for updating database extensions.
717
+ */
718
+ export interface UpdateExtensionsOptions extends PlanetScaleProps {
719
+ /**
720
+ * The organization name.
721
+ * @default process.env.PLANETSCALE_ORGANIZATION
722
+ */
723
+ organization?: string;
724
+ /**
725
+ * The database name or Database resource.
726
+ */
727
+ database: string | Database;
728
+ /**
729
+ * The branch name or Branch resource.
730
+ * @default "main"
731
+ */
732
+ branch?: string | Branch;
733
+ /**
734
+ * The base URL of the PlanetScale API.
735
+ * @default "https://api.planetscale.com/v1"
736
+ */
737
+ baseUrl?: string;
738
+ }
739
+
740
+ function resolveOptions(opts: UpdateExtensionsOptions): {
741
+ organization: string;
742
+ database: string;
743
+ branch: string;
744
+ baseUrl: string;
745
+ token: string;
746
+ } {
747
+ const organization =
748
+ opts.organization ??
749
+ (typeof opts.database !== "string"
750
+ ? opts.database.organization
751
+ : undefined) ??
752
+ process.env.PLANETSCALE_ORGANIZATION ??
753
+ process.env.PLANETSCALE_ORG_ID;
754
+
755
+ if (!organization) {
756
+ throw new Error(
757
+ "PlanetScale organization is required. Please set the `organization` property or the `PLANETSCALE_ORGANIZATION` environment variable.",
758
+ );
759
+ }
760
+
761
+ const database =
762
+ typeof opts.database === "string" ? opts.database : opts.database.name;
763
+ const branch = !opts.branch
764
+ ? "main"
765
+ : typeof opts.branch === "string"
766
+ ? opts.branch
767
+ : opts.branch.name;
768
+ const baseUrl = opts.baseUrl ?? "https://api.planetscale.com/v1";
769
+ const token = extractToken(opts);
770
+
771
+ return { organization, database, branch, baseUrl, token };
772
+ }
773
+
774
+ /**
775
+ * Builds the multipart/form-data body for a change request that sets extensions.
776
+ *
777
+ * The API expects:
778
+ * - `_method=PATCH`
779
+ * - `queued=1` (for queue phase) or `update=1` (for apply phase)
780
+ * - `parameters[pgconf][shared_preload_libraries][]` repeated for each library
781
+ * - `parameters[pgconf][<param.name>]=<param.value>` for each config param
782
+ *
783
+ * @internal
784
+ */
785
+ function buildExtensionFormData(
786
+ desired: DatabaseExtensions,
787
+ phase: "queue" | "apply",
788
+ ): FormData {
789
+ const form = new FormData();
790
+ form.append("_method", "PATCH");
791
+
792
+ if (phase === "apply") {
793
+ form.append("update", "1");
794
+ return form;
795
+ }
796
+
797
+ // Queue phase — include all libraries and params
798
+ form.append("queued", "1");
799
+
800
+ const mappings = extensionsToParamMappings(desired);
801
+
802
+ // Always include base libraries
803
+ for (const lib of BASE_LIBRARIES) {
804
+ form.append("parameters[pgconf][shared_preload_libraries][]", lib);
805
+ }
806
+
807
+ // Include pginsights in base libraries if it has config, otherwise include as base
808
+ const hasPgInsights = mappings.has("pginsights");
809
+ if (!hasPgInsights) {
810
+ // pginsights is always loaded; if user didn't explicitly configure it, include with defaults off
811
+ form.append("parameters[pgconf][shared_preload_libraries][]", "pginsights");
812
+ form.append("parameters[pgconf][pginsights.raw_queries]", "off");
813
+ form.append("parameters[pgconf][pginsights.normalize_schema_names]", "off");
814
+ }
815
+
816
+ // Add user-requested extensions
817
+ for (const [library, params] of mappings) {
818
+ form.append("parameters[pgconf][shared_preload_libraries][]", library);
819
+
820
+ for (const [key, value] of Object.entries(params)) {
821
+ form.append(`parameters[pgconf][${key}]`, value);
822
+ }
823
+ }
824
+
825
+ return form;
826
+ }
827
+
828
+ /**
829
+ * Updates the PostgreSQL extensions on a PlanetScale database branch.
830
+ *
831
+ * Uses the reverse-engineered two-phase change request API:
832
+ * 1. **Queue** — submits a change request with the desired extension config
833
+ * 2. **Apply** — triggers execution of the queued change
834
+ * 3. **Poll** — waits for the change to complete
835
+ *
836
+ * @param desired - The desired extension configuration. Extensions present are enabled;
837
+ * extensions absent are disabled.
838
+ * @param options - Connection and targeting options.
839
+ *
840
+ * @example
841
+ * ## Enable pgvector and pg_cron
842
+ *
843
+ * ```ts
844
+ * await updateExtensions(
845
+ * {
846
+ * vector: { hnswEfSearch: 100 },
847
+ * pgCron: {},
848
+ * },
849
+ * {
850
+ * organization: "my-org",
851
+ * database: "my-db",
852
+ * branch: "main",
853
+ * },
854
+ * );
855
+ * ```
856
+ *
857
+ * @example
858
+ * ## Disable all user extensions
859
+ *
860
+ * ```ts
861
+ * await updateExtensions({}, {
862
+ * organization: "my-org",
863
+ * database: "my-db",
864
+ * });
865
+ * ```
866
+ */
867
+ export async function updateExtensions(
868
+ desired: DatabaseExtensions,
869
+ options: UpdateExtensionsOptions,
870
+ ): Promise<void> {
871
+ const { organization, database, branch, baseUrl, token } =
872
+ resolveOptions(options);
873
+
874
+ const changesUrl = `${baseUrl}/organizations/${organization}/databases/${database}/branches/${branch}/changes`;
875
+
876
+ // Wait for any in-flight changes to complete before submitting ours
877
+ await waitForPendingChanges(changesUrl, token);
878
+
879
+ // Phase 1: Queue the change
880
+ const queueForm = buildExtensionFormData(desired, "queue");
881
+ const queueResponse = await fetch(changesUrl, {
882
+ method: "POST",
883
+ headers: {
884
+ Authorization: token,
885
+ },
886
+ body: queueForm,
887
+ });
888
+
889
+ if (!queueResponse.ok) {
890
+ const errorBody = await queueResponse.text();
891
+ throw new Error(
892
+ `Failed to queue extension change (${queueResponse.status}): ${errorBody}`,
893
+ );
894
+ }
895
+
896
+ // Phase 2: Apply the change
897
+ const applyForm = buildExtensionFormData(desired, "apply");
898
+ const applyResponse = await fetch(changesUrl, {
899
+ method: "POST",
900
+ headers: {
901
+ Authorization: token,
902
+ },
903
+ body: applyForm,
904
+ });
905
+
906
+ if (!applyResponse.ok) {
907
+ const errorBody = await applyResponse.text();
908
+ throw new Error(
909
+ `Failed to apply extension change (${applyResponse.status}): ${errorBody}`,
910
+ );
911
+ }
912
+
913
+ // Phase 3: Poll until the change is completed
914
+ await waitForPendingChanges(changesUrl, token);
915
+ }
916
+
917
+ /**
918
+ * Polls the changes endpoint until all changes are completed or canceled.
919
+ * @internal
920
+ */
921
+ async function waitForPendingChanges(
922
+ changesUrl: string,
923
+ token: string,
924
+ ): Promise<void> {
925
+ await poll({
926
+ description: "extension changes to complete",
927
+ fn: async () => {
928
+ const response = await fetch(`${changesUrl}?per_page=1`, {
929
+ headers: { Authorization: token },
930
+ });
931
+ if (!response.ok) {
932
+ throw new Error(
933
+ `Failed to poll change status (${response.status}): ${await response.text()}`,
934
+ );
935
+ }
936
+ const data = (await response.json()) as {
937
+ data: Array<{ state: string; id: string }>;
938
+ };
939
+ return data;
940
+ },
941
+ predicate: (result) => {
942
+ if (result.data.length === 0) return true;
943
+ const latest = result.data[0];
944
+ return latest.state === "completed" || latest.state === "canceled";
945
+ },
946
+ });
947
+ }