@segmentstream/ontology 0.0.0-stage → 0.92.1

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 (483) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +2 -0
  3. package/README.md +1655 -2
  4. package/dist/action.d.ts +193 -0
  5. package/dist/action.js +41 -0
  6. package/dist/agent-chat.d.ts +44 -0
  7. package/dist/agent-chat.js +91 -0
  8. package/dist/agent-task-contract.d.ts +64 -0
  9. package/dist/agent-task-contract.js +15 -0
  10. package/dist/agent-task.d.ts +39 -0
  11. package/dist/agent-task.js +112 -0
  12. package/dist/analyze.d.ts +215 -0
  13. package/dist/analyze.js +2438 -0
  14. package/dist/app-feedback.d.ts +167 -0
  15. package/dist/app-feedback.js +176 -0
  16. package/dist/authoring-input.d.ts +3 -0
  17. package/dist/authoring-input.js +107 -0
  18. package/dist/authoring-names.d.ts +4 -0
  19. package/dist/authoring-names.js +13 -0
  20. package/dist/authoring-records.d.ts +35 -0
  21. package/dist/authoring-records.js +264 -0
  22. package/dist/authoring.d.ts +15 -0
  23. package/dist/authoring.js +331 -0
  24. package/dist/bin/check.d.ts +30 -0
  25. package/dist/bin/check.js +99 -0
  26. package/dist/bin/load.d.ts +48 -0
  27. package/dist/bin/load.js +139 -0
  28. package/dist/bin/main.d.ts +31 -0
  29. package/dist/bin/main.js +121 -0
  30. package/dist/bin/migrate.d.ts +57 -0
  31. package/dist/bin/migrate.js +170 -0
  32. package/dist/bin/output.d.ts +31 -0
  33. package/dist/bin/output.js +84 -0
  34. package/dist/bin.d.ts +2 -0
  35. package/dist/bin.js +31 -0
  36. package/dist/blob.d.ts +52 -0
  37. package/dist/blob.js +87 -0
  38. package/dist/channel-session.d.ts +16 -0
  39. package/dist/channel-session.js +16 -0
  40. package/dist/chat.d.ts +43 -0
  41. package/dist/chat.js +34 -0
  42. package/dist/confirmation.d.ts +46 -0
  43. package/dist/confirmation.js +41 -0
  44. package/dist/connection.d.ts +213 -0
  45. package/dist/connection.js +289 -0
  46. package/dist/deployment.d.ts +35 -0
  47. package/dist/deployment.js +76 -0
  48. package/dist/diagnostics.d.ts +27 -0
  49. package/dist/diagnostics.js +106 -0
  50. package/dist/emails.d.ts +70 -0
  51. package/dist/emails.js +221 -0
  52. package/dist/entity.d.ts +331 -0
  53. package/dist/entity.js +153 -0
  54. package/dist/errors.d.ts +154 -0
  55. package/dist/errors.js +109 -0
  56. package/dist/feedback-config.d.ts +24 -0
  57. package/dist/feedback-config.js +126 -0
  58. package/dist/files.d.ts +39 -0
  59. package/dist/files.js +4 -0
  60. package/dist/groups.d.ts +95 -0
  61. package/dist/groups.js +280 -0
  62. package/dist/index.d.ts +109 -0
  63. package/dist/index.js +70 -0
  64. package/dist/internal/diagnostics.js +594 -0
  65. package/dist/legacy.d.ts +160 -0
  66. package/dist/legacy.js +270 -0
  67. package/dist/manifest-compatibility.d.ts +8 -0
  68. package/dist/manifest-compatibility.js +87 -0
  69. package/dist/manifest.d.ts +3329 -0
  70. package/dist/manifest.js +757 -0
  71. package/dist/me.d.ts +46 -0
  72. package/dist/me.js +109 -0
  73. package/dist/metric-filters.d.ts +48 -0
  74. package/dist/metric-filters.js +252 -0
  75. package/dist/metric-routes.d.ts +132 -0
  76. package/dist/metric-routes.js +498 -0
  77. package/dist/metric.d.ts +172 -0
  78. package/dist/metric.js +132 -0
  79. package/dist/migrate/apply.d.ts +94 -0
  80. package/dist/migrate/apply.js +526 -0
  81. package/dist/migrate/errors.d.ts +46 -0
  82. package/dist/migrate/errors.js +38 -0
  83. package/dist/migrate/index.d.ts +10 -0
  84. package/dist/migrate/index.js +7 -0
  85. package/dist/migrate/model.d.ts +2311 -0
  86. package/dist/migrate/model.js +148 -0
  87. package/dist/migrate/plan.d.ts +51 -0
  88. package/dist/migrate/plan.js +1856 -0
  89. package/dist/migrate/preview.d.ts +13 -0
  90. package/dist/migrate/preview.js +65 -0
  91. package/dist/migrate/storage.d.ts +226 -0
  92. package/dist/migrate/storage.js +774 -0
  93. package/dist/model-metrics.d.ts +22 -0
  94. package/dist/model-metrics.js +545 -0
  95. package/dist/names.d.ts +49 -0
  96. package/dist/names.js +110 -0
  97. package/dist/navigation-artifact.d.ts +13 -0
  98. package/dist/navigation-artifact.js +11 -0
  99. package/dist/navigation-catalog.d.ts +55 -0
  100. package/dist/navigation-catalog.js +297 -0
  101. package/dist/nitro-navigation.d.ts +3 -0
  102. package/dist/nitro-navigation.js +64 -0
  103. package/dist/nitro-workflows.d.ts +108 -0
  104. package/dist/nitro-workflows.js +286 -0
  105. package/dist/nitro.d.ts +40 -0
  106. package/dist/nitro.js +50 -0
  107. package/dist/ontology.d.ts +41 -0
  108. package/dist/ontology.js +46 -0
  109. package/dist/permissions.d.ts +198 -0
  110. package/dist/permissions.js +272 -0
  111. package/dist/property.d.ts +188 -0
  112. package/dist/property.js +301 -0
  113. package/dist/query.d.ts +245 -0
  114. package/dist/query.js +615 -0
  115. package/dist/relationship.d.ts +99 -0
  116. package/dist/relationship.js +33 -0
  117. package/dist/render.d.ts +81 -0
  118. package/dist/render.js +512 -0
  119. package/dist/replication/baseline.d.ts +51 -0
  120. package/dist/replication/baseline.js +215 -0
  121. package/dist/replication/bigquery.d.ts +37 -0
  122. package/dist/replication/bigquery.js +211 -0
  123. package/dist/replication/coverage.d.ts +14 -0
  124. package/dist/replication/coverage.js +53 -0
  125. package/dist/replication/evolution.d.ts +25 -0
  126. package/dist/replication/evolution.js +184 -0
  127. package/dist/replication/identities.d.ts +6 -0
  128. package/dist/replication/identities.js +47 -0
  129. package/dist/replication/legacy-baseline.d.ts +27 -0
  130. package/dist/replication/legacy-baseline.js +109 -0
  131. package/dist/replication/legacy-store.d.ts +19 -0
  132. package/dist/replication/legacy-store.js +63 -0
  133. package/dist/replication/lineage.d.ts +36 -0
  134. package/dist/replication/lineage.js +72 -0
  135. package/dist/replication/migrate.d.ts +21 -0
  136. package/dist/replication/migrate.js +84 -0
  137. package/dist/replication/model.d.ts +52 -0
  138. package/dist/replication/model.js +99 -0
  139. package/dist/replication/scans.d.ts +34 -0
  140. package/dist/replication/scans.js +158 -0
  141. package/dist/replication/schema.d.ts +11 -0
  142. package/dist/replication/schema.js +226 -0
  143. package/dist/replication/shadow-schema.d.ts +4 -0
  144. package/dist/replication/shadow-schema.js +61 -0
  145. package/dist/replication/shadow.d.ts +127 -0
  146. package/dist/replication/shadow.js +614 -0
  147. package/dist/replication/source-replacement.d.ts +33 -0
  148. package/dist/replication/source-replacement.js +76 -0
  149. package/dist/replication/store.d.ts +36 -0
  150. package/dist/replication/store.js +382 -0
  151. package/dist/replication/transaction.d.ts +23 -0
  152. package/dist/replication/transaction.js +88 -0
  153. package/dist/role.d.ts +61 -0
  154. package/dist/role.js +76 -0
  155. package/dist/runtime/action-deadlocks.d.ts +21 -0
  156. package/dist/runtime/action-deadlocks.js +130 -0
  157. package/dist/runtime/action-transactions.d.ts +35 -0
  158. package/dist/runtime/action-transactions.js +54 -0
  159. package/dist/runtime/agent-counters.d.ts +26 -0
  160. package/dist/runtime/agent-counters.js +98 -0
  161. package/dist/runtime/agent-tasks.d.ts +63 -0
  162. package/dist/runtime/agent-tasks.js +120 -0
  163. package/dist/runtime/agents.d.ts +15 -0
  164. package/dist/runtime/agents.js +457 -0
  165. package/dist/runtime/batch-diagnostics.d.ts +22 -0
  166. package/dist/runtime/batch-diagnostics.js +89 -0
  167. package/dist/runtime/batch.d.ts +52 -0
  168. package/dist/runtime/batch.js +534 -0
  169. package/dist/runtime/channel-session-revocation.d.ts +3 -0
  170. package/dist/runtime/channel-session-revocation.js +5 -0
  171. package/dist/runtime/channel-sessions.d.ts +4 -0
  172. package/dist/runtime/channel-sessions.js +61 -0
  173. package/dist/runtime/confirmations.d.ts +29 -0
  174. package/dist/runtime/confirmations.js +57 -0
  175. package/dist/runtime/cursor.d.ts +23 -0
  176. package/dist/runtime/cursor.js +100 -0
  177. package/dist/runtime/deployment.d.ts +7 -0
  178. package/dist/runtime/deployment.js +4 -0
  179. package/dist/runtime/errors.d.ts +118 -0
  180. package/dist/runtime/errors.js +109 -0
  181. package/dist/runtime/federation-tables.d.ts +2 -0
  182. package/dist/runtime/federation-tables.js +20 -0
  183. package/dist/runtime/federation.d.ts +42 -0
  184. package/dist/runtime/federation.js +222 -0
  185. package/dist/runtime/files.d.ts +39 -0
  186. package/dist/runtime/files.js +44 -0
  187. package/dist/runtime/generated-actions.d.ts +37 -0
  188. package/dist/runtime/generated-actions.js +307 -0
  189. package/dist/runtime/history.d.ts +91 -0
  190. package/dist/runtime/history.js +77 -0
  191. package/dist/runtime/ids.d.ts +6 -0
  192. package/dist/runtime/ids.js +39 -0
  193. package/dist/runtime/member-tables.d.ts +8 -0
  194. package/dist/runtime/member-tables.js +33 -0
  195. package/dist/runtime/member-writes.d.ts +78 -0
  196. package/dist/runtime/member-writes.js +202 -0
  197. package/dist/runtime/metric-query.d.ts +19 -0
  198. package/dist/runtime/metric-query.js +411 -0
  199. package/dist/runtime/model.d.ts +67 -0
  200. package/dist/runtime/model.js +150 -0
  201. package/dist/runtime/new-user-hook.d.ts +6 -0
  202. package/dist/runtime/new-user-hook.js +37 -0
  203. package/dist/runtime/oauth.d.ts +9 -0
  204. package/dist/runtime/oauth.js +299 -0
  205. package/dist/runtime/pipeline.d.ts +155 -0
  206. package/dist/runtime/pipeline.js +597 -0
  207. package/dist/runtime/postgres-tls.d.ts +10 -0
  208. package/dist/runtime/postgres-tls.js +14 -0
  209. package/dist/runtime/principals.d.ts +302 -0
  210. package/dist/runtime/principals.js +1307 -0
  211. package/dist/runtime/reads.d.ts +55 -0
  212. package/dist/runtime/reads.js +313 -0
  213. package/dist/runtime/records.d.ts +42 -0
  214. package/dist/runtime/records.js +245 -0
  215. package/dist/runtime/resolve-agent-identity.d.ts +6 -0
  216. package/dist/runtime/resolve-agent-identity.js +181 -0
  217. package/dist/runtime/service-binding.d.ts +18 -0
  218. package/dist/runtime/service-binding.js +266 -0
  219. package/dist/runtime/sign-in-links.d.ts +68 -0
  220. package/dist/runtime/sign-in-links.js +147 -0
  221. package/dist/runtime/sql-runtime.d.ts +131 -0
  222. package/dist/runtime/sql-runtime.js +362 -0
  223. package/dist/runtime/statement.d.ts +18 -0
  224. package/dist/runtime/statement.js +31 -0
  225. package/dist/runtime/user-identities.d.ts +66 -0
  226. package/dist/runtime/user-identities.js +106 -0
  227. package/dist/runtime/user-identity-sql.d.ts +86 -0
  228. package/dist/runtime/user-identity-sql.js +297 -0
  229. package/dist/runtime/user-roles.d.ts +16 -0
  230. package/dist/runtime/user-roles.js +32 -0
  231. package/dist/runtime/validation.d.ts +16 -0
  232. package/dist/runtime/validation.js +26 -0
  233. package/dist/runtime/where.d.ts +57 -0
  234. package/dist/runtime/where.js +404 -0
  235. package/dist/runtime/workflow-admission.d.ts +49 -0
  236. package/dist/runtime/workflow-admission.js +92 -0
  237. package/dist/runtime/workflow-argument-codec.d.ts +10 -0
  238. package/dist/runtime/workflow-argument-codec.js +120 -0
  239. package/dist/runtime/workflow-arguments.d.ts +17 -0
  240. package/dist/runtime/workflow-arguments.js +99 -0
  241. package/dist/runtime/workflow-starts.d.ts +203 -0
  242. package/dist/runtime/workflow-starts.js +493 -0
  243. package/dist/runtime/workflow-users.d.ts +9 -0
  244. package/dist/runtime/workflow-users.js +17 -0
  245. package/dist/runtime/workload.d.ts +25 -0
  246. package/dist/runtime/workload.js +24 -0
  247. package/dist/runtime/writes.d.ts +108 -0
  248. package/dist/runtime/writes.js +491 -0
  249. package/dist/runtime-source.d.ts +22 -0
  250. package/dist/runtime-source.js +28 -0
  251. package/dist/runtime.d.ts +310 -0
  252. package/dist/runtime.js +31 -0
  253. package/dist/server.d.ts +23 -0
  254. package/dist/server.js +107 -0
  255. package/dist/service/agent/attachments.d.ts +33 -0
  256. package/dist/service/agent/attachments.js +228 -0
  257. package/dist/service/agent/authentication.d.ts +22 -0
  258. package/dist/service/agent/authentication.js +58 -0
  259. package/dist/service/agent/confirmations.d.ts +5 -0
  260. package/dist/service/agent/confirmations.js +73 -0
  261. package/dist/service/agent/conversation-routes.d.ts +14 -0
  262. package/dist/service/agent/conversation-routes.js +45 -0
  263. package/dist/service/agent/conversations.d.ts +40 -0
  264. package/dist/service/agent/conversations.js +69 -0
  265. package/dist/service/agent/files.d.ts +12 -0
  266. package/dist/service/agent/files.js +69 -0
  267. package/dist/service/agent/protocol.d.ts +90 -0
  268. package/dist/service/agent/protocol.js +114 -0
  269. package/dist/service/agent/server.d.ts +28 -0
  270. package/dist/service/agent/server.js +374 -0
  271. package/dist/service/agent/task-caller.d.ts +21 -0
  272. package/dist/service/agent/task-caller.js +35 -0
  273. package/dist/service/agent/task-start.d.ts +53 -0
  274. package/dist/service/agent/task-start.js +161 -0
  275. package/dist/service/agent/tasks.d.ts +40 -0
  276. package/dist/service/agent/tasks.js +155 -0
  277. package/dist/service/api.d.ts +50 -0
  278. package/dist/service/api.js +288 -0
  279. package/dist/service/authentication.d.ts +227 -0
  280. package/dist/service/authentication.js +767 -0
  281. package/dist/service/caller-context.d.ts +24 -0
  282. package/dist/service/caller-context.js +6 -0
  283. package/dist/service/catalog.d.ts +42 -0
  284. package/dist/service/catalog.js +66 -0
  285. package/dist/service/channel/provider.d.ts +10 -0
  286. package/dist/service/channel/provider.js +1 -0
  287. package/dist/service/channel/providers/telegram.d.ts +18 -0
  288. package/dist/service/channel/providers/telegram.js +108 -0
  289. package/dist/service/channel/sessions.d.ts +5 -0
  290. package/dist/service/channel/sessions.js +53 -0
  291. package/dist/service/channel/sign-in.d.ts +7 -0
  292. package/dist/service/channel/sign-in.js +78 -0
  293. package/dist/service/chat/archive.d.ts +64 -0
  294. package/dist/service/chat/archive.js +62 -0
  295. package/dist/service/chat/attribution.d.ts +26 -0
  296. package/dist/service/chat/attribution.js +28 -0
  297. package/dist/service/chat/delivery.d.ts +18 -0
  298. package/dist/service/chat/delivery.js +251 -0
  299. package/dist/service/chat/diagnostics.d.ts +2 -0
  300. package/dist/service/chat/diagnostics.js +35 -0
  301. package/dist/service/chat/envelope.d.ts +96 -0
  302. package/dist/service/chat/envelope.js +39 -0
  303. package/dist/service/chat/idempotency.d.ts +19 -0
  304. package/dist/service/chat/idempotency.js +37 -0
  305. package/dist/service/chat/providers/telegram.d.ts +36 -0
  306. package/dist/service/chat/providers/telegram.js +133 -0
  307. package/dist/service/chat/reads.d.ts +25 -0
  308. package/dist/service/chat/reads.js +75 -0
  309. package/dist/service/chat/resolve.d.ts +34 -0
  310. package/dist/service/chat/resolve.js +150 -0
  311. package/dist/service/chat/routes.d.ts +23 -0
  312. package/dist/service/chat/routes.js +75 -0
  313. package/dist/service/chat/schemas.d.ts +43 -0
  314. package/dist/service/chat/schemas.js +31 -0
  315. package/dist/service/config.d.ts +218 -0
  316. package/dist/service/config.js +265 -0
  317. package/dist/service/connection-stream.d.ts +10 -0
  318. package/dist/service/connection-stream.js +191 -0
  319. package/dist/service/connections.d.ts +60 -0
  320. package/dist/service/connections.js +271 -0
  321. package/dist/service/decisions.d.ts +35 -0
  322. package/dist/service/decisions.js +63 -0
  323. package/dist/service/descriptions.d.ts +8 -0
  324. package/dist/service/descriptions.js +20 -0
  325. package/dist/service/discovery-schemas.d.ts +92 -0
  326. package/dist/service/discovery-schemas.js +59 -0
  327. package/dist/service/discovery-search.d.ts +30 -0
  328. package/dist/service/discovery-search.js +95 -0
  329. package/dist/service/discovery.d.ts +37 -0
  330. package/dist/service/discovery.js +436 -0
  331. package/dist/service/docs.d.ts +9 -0
  332. package/dist/service/docs.js +197 -0
  333. package/dist/service/errors.d.ts +145 -0
  334. package/dist/service/errors.js +704 -0
  335. package/dist/service/federation-provider.d.ts +36 -0
  336. package/dist/service/federation-provider.js +110 -0
  337. package/dist/service/federation-server.d.ts +20 -0
  338. package/dist/service/federation-server.js +249 -0
  339. package/dist/service/federation-status.d.ts +40 -0
  340. package/dist/service/federation-status.js +151 -0
  341. package/dist/service/handlers.d.ts +18 -0
  342. package/dist/service/handlers.js +287 -0
  343. package/dist/service/mcp/confirmation.d.ts +68 -0
  344. package/dist/service/mcp/confirmation.js +178 -0
  345. package/dist/service/mcp/find-input.d.ts +12 -0
  346. package/dist/service/mcp/find-input.js +122 -0
  347. package/dist/service/mcp/input-schema.d.ts +6 -0
  348. package/dist/service/mcp/input-schema.js +126 -0
  349. package/dist/service/mcp/listing.d.ts +26 -0
  350. package/dist/service/mcp/listing.js +92 -0
  351. package/dist/service/mcp/metric-tools.d.ts +85 -0
  352. package/dist/service/mcp/metric-tools.js +258 -0
  353. package/dist/service/mcp/request-log.d.ts +5 -0
  354. package/dist/service/mcp/request-log.js +53 -0
  355. package/dist/service/mcp/runtime-policy.d.ts +27 -0
  356. package/dist/service/mcp/runtime-policy.js +62 -0
  357. package/dist/service/mcp/server.d.ts +52 -0
  358. package/dist/service/mcp/server.js +861 -0
  359. package/dist/service/mcp/tools.d.ts +99 -0
  360. package/dist/service/mcp/tools.js +938 -0
  361. package/dist/service/member-file-routes.d.ts +22 -0
  362. package/dist/service/member-file-routes.js +52 -0
  363. package/dist/service/member-files.d.ts +245 -0
  364. package/dist/service/member-files.js +258 -0
  365. package/dist/service/metric-query-platform.d.ts +77 -0
  366. package/dist/service/metric-query-platform.js +198 -0
  367. package/dist/service/metric-query-scopes.d.ts +43 -0
  368. package/dist/service/metric-query-scopes.js +147 -0
  369. package/dist/service/metric-query.d.ts +48 -0
  370. package/dist/service/metric-query.js +200 -0
  371. package/dist/service/name-suggestions.d.ts +3 -0
  372. package/dist/service/name-suggestions.js +38 -0
  373. package/dist/service/navigation-schemas.d.ts +80 -0
  374. package/dist/service/navigation-schemas.js +49 -0
  375. package/dist/service/navigation.d.ts +20 -0
  376. package/dist/service/navigation.js +165 -0
  377. package/dist/service/oauth/protocol.d.ts +70 -0
  378. package/dist/service/oauth/protocol.js +157 -0
  379. package/dist/service/oauth/refresh-replay.d.ts +2 -0
  380. package/dist/service/oauth/refresh-replay.js +20 -0
  381. package/dist/service/oauth/server.d.ts +28 -0
  382. package/dist/service/oauth/server.js +769 -0
  383. package/dist/service/operations.d.ts +170 -0
  384. package/dist/service/operations.js +392 -0
  385. package/dist/service/plan.d.ts +57 -0
  386. package/dist/service/plan.js +96 -0
  387. package/dist/service/rate-limit.d.ts +22 -0
  388. package/dist/service/rate-limit.js +52 -0
  389. package/dist/service/record-reads.d.ts +21 -0
  390. package/dist/service/record-reads.js +155 -0
  391. package/dist/service/replication.d.ts +5 -0
  392. package/dist/service/replication.js +39 -0
  393. package/dist/service/runtime-contract.d.ts +508 -0
  394. package/dist/service/runtime-contract.js +18 -0
  395. package/dist/service/runtime-identity.d.ts +71 -0
  396. package/dist/service/runtime-identity.js +122 -0
  397. package/dist/service/schemas.d.ts +961 -0
  398. package/dist/service/schemas.js +639 -0
  399. package/dist/service/service.d.ts +21 -0
  400. package/dist/service/service.js +526 -0
  401. package/dist/service/warehouse-platform.d.ts +17 -0
  402. package/dist/service/warehouse-platform.js +122 -0
  403. package/dist/service/warehouse-query.d.ts +24 -0
  404. package/dist/service/warehouse-query.js +280 -0
  405. package/dist/service/webhooks.d.ts +106 -0
  406. package/dist/service/webhooks.js +233 -0
  407. package/dist/service/workflow-admission-provider.d.ts +31 -0
  408. package/dist/service/workflow-admission-provider.js +205 -0
  409. package/dist/service/workflow-argument-serialization.d.ts +2 -0
  410. package/dist/service/workflow-argument-serialization.js +14 -0
  411. package/dist/service/workflow-callback.d.ts +22 -0
  412. package/dist/service/workflow-callback.js +75 -0
  413. package/dist/service/workflow-deployment.d.ts +8 -0
  414. package/dist/service/workflow-deployment.js +131 -0
  415. package/dist/service/workflow-diagnostics.d.ts +23 -0
  416. package/dist/service/workflow-diagnostics.js +263 -0
  417. package/dist/service/workflow-errors.d.ts +26 -0
  418. package/dist/service/workflow-errors.js +181 -0
  419. package/dist/service/workflow-logging.d.ts +2 -0
  420. package/dist/service/workflow-logging.js +50 -0
  421. package/dist/service/workflow-queue-fence.d.ts +6 -0
  422. package/dist/service/workflow-queue-fence.js +66 -0
  423. package/dist/service/workflows.d.ts +200 -0
  424. package/dist/service/workflows.js +1549 -0
  425. package/dist/sign-in.d.ts +58 -0
  426. package/dist/sign-in.js +57 -0
  427. package/dist/source.d.ts +30 -0
  428. package/dist/source.js +107 -0
  429. package/dist/start-recovery-contract.d.ts +49 -0
  430. package/dist/start-recovery-contract.js +26 -0
  431. package/dist/start-recovery.d.ts +27 -0
  432. package/dist/start-recovery.js +81 -0
  433. package/dist/storage/api.d.ts +4 -0
  434. package/dist/storage/api.js +40 -0
  435. package/dist/storage/attachments.d.ts +146 -0
  436. package/dist/storage/attachments.js +733 -0
  437. package/dist/storage/authorization.d.ts +38 -0
  438. package/dist/storage/authorization.js +83 -0
  439. package/dist/storage/file-access.d.ts +17 -0
  440. package/dist/storage/file-access.js +8 -0
  441. package/dist/storage/limits.d.ts +12 -0
  442. package/dist/storage/limits.js +17 -0
  443. package/dist/storage/maintenance-auth.d.ts +20 -0
  444. package/dist/storage/maintenance-auth.js +24 -0
  445. package/dist/storage/maintenance.d.ts +11 -0
  446. package/dist/storage/maintenance.js +67 -0
  447. package/dist/storage/neon.d.ts +4 -0
  448. package/dist/storage/neon.js +318 -0
  449. package/dist/storage/operator.d.ts +11 -0
  450. package/dist/storage/operator.js +30 -0
  451. package/dist/storage/provider.d.ts +93 -0
  452. package/dist/storage/provider.js +36 -0
  453. package/dist/storage/sessions.d.ts +184 -0
  454. package/dist/storage/sessions.js +688 -0
  455. package/dist/storage/tables.d.ts +54 -0
  456. package/dist/storage/tables.js +205 -0
  457. package/dist/storage/transfers.d.ts +16 -0
  458. package/dist/storage/transfers.js +153 -0
  459. package/dist/storage/writes.d.ts +7 -0
  460. package/dist/storage/writes.js +88 -0
  461. package/dist/telegram.d.ts +35 -0
  462. package/dist/telegram.js +173 -0
  463. package/dist/user-identity.d.ts +28 -0
  464. package/dist/user-identity.js +106 -0
  465. package/dist/user.d.ts +75 -0
  466. package/dist/user.js +111 -0
  467. package/dist/warehouse-backed.d.ts +6 -0
  468. package/dist/warehouse-backed.js +495 -0
  469. package/dist/webhook-token.d.ts +11 -0
  470. package/dist/webhook-token.js +18 -0
  471. package/dist/webhooks.d.ts +40 -0
  472. package/dist/webhooks.js +55 -0
  473. package/dist/wire.d.ts +10 -0
  474. package/dist/wire.js +35 -0
  475. package/dist/workflow-runtime.d.ts +116 -0
  476. package/dist/workflow-runtime.js +109 -0
  477. package/dist/workflow-topics.d.ts +28 -0
  478. package/dist/workflow-topics.js +28 -0
  479. package/dist/workflow.d.ts +284 -0
  480. package/dist/workflow.js +259 -0
  481. package/dist/workflows.d.ts +24 -0
  482. package/dist/workflows.js +18 -0
  483. package/package.json +127 -4
@@ -0,0 +1,1856 @@
1
+ import { isValueMatched, } from "../manifest.js";
2
+ import { FEDERATION_TABLES, FEDERATION_TABLES_DDL } from "../runtime/federation-tables.js";
3
+ import { EMAIL_LINK_FENCE_SQL, SEGMENTSTREAM_LINK_FENCE_SQL, USER_IDENTITY_ENTITY_ID, USER_IDENTITY_SYNC_SQL, } from "../runtime/user-identity-sql.js";
4
+ import { AGENT_TASKS_DDL, AGENT_TASKS_TABLE, SIGN_IN_EMAILS_DDL, SIGN_IN_EMAILS_TABLE, ATTACHMENTS_TABLE, ATTACHMENT_TABLES_DDL, BLOB_TABLES_DDL, WORKFLOW_STARTS_DDL, WORKFLOW_STARTS_TABLE, } from "../storage/tables.js";
5
+ import { SNAKE_CASE_NAME, isSystemProperty, toSnakeCase } from "../names.js";
6
+ import { NO_RETAINED_STORAGE, retainedEntityManifest, } from "./model.js";
7
+ import { ACTION_ATTRIBUTION_TABLE, ACTION_ATTRIBUTION_DDL, AGENT_TABLES, AGENT_TABLES_DDL, CATALOG_TABLE_DDL, MAX_IDENTIFIER_LENGTH, OAUTH_TABLES, OAUTH_TABLES_DDL, USER_TABLE, addConstraintStatement, columnName, columnDefinition, constraintDefinition, createEnumStatement, createIndexStatement, createTableStatement, defaultExpression, entityStorage, historyActivityIndex, enumTypeName, isStoredProperty, fieldColumnName, joinTable, propertyCheck, blobForeignKey, propertyColumn, propertyEnum, quoteIdentifier, quoteLiteral, relationshipColumn, relationshipColumnName, RUNTIME_TABLES_DDL, relationshipForeignKey, SYSTEM_COLUMNS, storedEntity, tableName, } from "./storage.js";
8
+ const NO_MEMBERS = { properties: new Set(), relationships: new Set() };
9
+ const GLOBAL_KINDS = new Set(["table", "type", "index"]);
10
+ function step(input) {
11
+ return {
12
+ kind: input.kind,
13
+ classification: input.classification,
14
+ transactional: input.transactional ?? true,
15
+ validatesData: input.validatesData ?? false,
16
+ description: input.description,
17
+ sql: [...input.sql],
18
+ };
19
+ }
20
+ function refusal(code, path, message) {
21
+ return { code, path, message };
22
+ }
23
+ function alterTable(table, action) {
24
+ return `ALTER TABLE ${quoteIdentifier(table)} ${action}`;
25
+ }
26
+ /**
27
+ * Plans the migration from the applied manifest, the storage the catalog keeps and the observed
28
+ * storage to the target. It is pure: the same inputs give the same plan. An unchanged manifest can
29
+ * still need runtime storage upgrades.
30
+ */
31
+ export function planMigration(input) {
32
+ const { current, target } = input;
33
+ const allowDrops = input.options?.allowDrops === true;
34
+ const draft = {
35
+ before: [],
36
+ catalog: [],
37
+ drops: [],
38
+ renames: [],
39
+ indexDrops: [],
40
+ changes: [],
41
+ creates: [],
42
+ additions: [],
43
+ joins: [],
44
+ runtimeTables: [],
45
+ foreignKeys: [],
46
+ indexes: [],
47
+ refusals: [],
48
+ rebuilds: 0,
49
+ };
50
+ const header = current === undefined ? {} : { currentHash: current.hash };
51
+ const retainedCurrent = input.retained ?? NO_RETAINED_STORAGE;
52
+ const retainedField = (next) => input.retained === undefined ? {} : { retained: { current: retainedCurrent, target: next } };
53
+ validateManifest(target, draft.refusals);
54
+ if (draft.refusals.length > 0) {
55
+ return {
56
+ ...header,
57
+ target,
58
+ steps: [],
59
+ refusals: draft.refusals,
60
+ ...retainedField(retainedCurrent),
61
+ };
62
+ }
63
+ for (const next of target.entities) {
64
+ const previous = current?.entities.find((entity) => entity.id === next.id);
65
+ if (previous && (previous.source !== undefined) !== (next.source !== undefined)) {
66
+ draft.refusals.push(refusal("type-change", next.name, "Stored and warehouse-backed entities cannot be converted in place."));
67
+ }
68
+ }
69
+ // A list has no column, so storage is planned from the properties that have one.
70
+ const storedTargets = target.entities
71
+ .filter((entity) => entity.source === undefined)
72
+ .map(storedEntity);
73
+ checkStorageNames(storedTargets, draft.refusals);
74
+ const currentEntities = (current?.entities ?? [])
75
+ .filter((entity) => entity.source === undefined)
76
+ .map(storedEntity);
77
+ const records = reconcileRetained({
78
+ retained: retainedCurrent,
79
+ current: currentEntities,
80
+ existing: input.existingStorage,
81
+ });
82
+ const pairs = matchEntities({
83
+ currentEntities,
84
+ records,
85
+ targetEntities: storedTargets,
86
+ refusals: draft.refusals,
87
+ });
88
+ // @legacy ontology.pre-upgrade-database-tolerance
89
+ // Upgrade the applied names before renaming anything, so old SDK databases can rename safely.
90
+ for (const pair of pairs) {
91
+ const index = historyActivityIndex(pair.current.name);
92
+ if (input.existingHistoryActivityIndexes?.has(index.name) === false) {
93
+ draft.before.push(step({
94
+ kind: "addIndex",
95
+ classification: "additive",
96
+ transactional: false,
97
+ description: `Add index ${index.name} (${index.origin}).`,
98
+ sql: [
99
+ `DROP INDEX CONCURRENTLY IF EXISTS ${quoteIdentifier(index.name)}`,
100
+ createIndexStatement(index, { concurrently: true }),
101
+ ],
102
+ }));
103
+ }
104
+ }
105
+ const pairedIds = new Set(pairs.map((pair) => pair.target.id));
106
+ const addedEntities = storedTargets.filter((entity) => !pairedIds.has(entity.id));
107
+ const removedEntities = currentEntities
108
+ .filter((entity) => !pairedIds.has(entity.id))
109
+ .map((entity) => declaredModel(entity, records));
110
+ const keptEntities = records
111
+ .filter((record) => record.removed && !pairedIds.has(record.id))
112
+ .map(keptModel);
113
+ if (current === undefined) {
114
+ draft.catalog.push(step({
115
+ kind: "createCatalog",
116
+ classification: "additive",
117
+ description: "Create the _ontology catalog table.",
118
+ sql: [CATALOG_TABLE_DDL],
119
+ }));
120
+ draft.runtimeTables.push(step({
121
+ kind: "createCatalog",
122
+ classification: "additive",
123
+ description: "Create the runtime's role assignment, API key, OAuth and agent tables.",
124
+ sql: RUNTIME_TABLES_DDL,
125
+ }));
126
+ draft.runtimeTables.push(step({
127
+ kind: "createCatalog",
128
+ classification: "additive",
129
+ description: "Create the table of the workflow starts action triggers decide.",
130
+ sql: WORKFLOW_STARTS_DDL,
131
+ }));
132
+ draft.runtimeTables.push(step({
133
+ kind: "createCatalog",
134
+ classification: "additive",
135
+ description: "Create the table of the tasks workflows hand the app's agent.",
136
+ sql: AGENT_TASKS_DDL,
137
+ }));
138
+ draft.runtimeTables.push(step({
139
+ kind: "createCatalog",
140
+ classification: "additive",
141
+ description: "Create the table of the sign-in links actions send.",
142
+ sql: SIGN_IN_EMAILS_DDL,
143
+ }));
144
+ }
145
+ else {
146
+ const methods = input.target.signIn?.browser?.methods;
147
+ if (current?.signIn?.browser && (!methods || target.version < 13)) {
148
+ draft.refusals.push(refusal("invalid-manifest", "signIn.browser", "browser_auth_declaration_required_after_adoption"));
149
+ }
150
+ // @legacy ontology.pre-upgrade-database-tolerance
151
+ // A database an earlier SDK migrated lacks the tables added since; each set arrives on its own.
152
+ const lacks = (tables) => tables.some((table) => input.existingTables?.has(table) === false);
153
+ if (lacks([ACTION_ATTRIBUTION_TABLE])) {
154
+ draft.runtimeTables.push(step({
155
+ kind: "createCatalog",
156
+ classification: "additive",
157
+ description: "Create the runtime's verified pipeline action attribution table.",
158
+ sql: ACTION_ATTRIBUTION_DDL,
159
+ }));
160
+ }
161
+ if (lacks(FEDERATION_TABLES) ||
162
+ (input.existingTables !== undefined &&
163
+ (input.current?.version ?? 13) < 13 &&
164
+ target.version >= 13)) {
165
+ draft.runtimeTables.push(step({
166
+ kind: "createCatalog",
167
+ classification: "additive",
168
+ description: "Create workspace identity links and federation sessions.",
169
+ sql: FEDERATION_TABLES_DDL,
170
+ }));
171
+ }
172
+ if (lacks(OAUTH_TABLES)) {
173
+ draft.runtimeTables.push(step({
174
+ kind: "createCatalog",
175
+ classification: "additive",
176
+ description: "Create the runtime's OAuth tables, which connect agents to the MCP server.",
177
+ sql: OAUTH_TABLES_DDL,
178
+ }));
179
+ }
180
+ if (lacks([WORKFLOW_STARTS_TABLE, "_workflow_admission"])) {
181
+ draft.runtimeTables.push(step({
182
+ kind: "createCatalog",
183
+ classification: "additive",
184
+ description: "Create the table of the workflow starts action triggers decide.",
185
+ sql: WORKFLOW_STARTS_DDL,
186
+ }));
187
+ }
188
+ if (lacks([AGENT_TASKS_TABLE])) {
189
+ draft.runtimeTables.push(step({
190
+ kind: "createCatalog",
191
+ classification: "additive",
192
+ description: "Create the table of the tasks workflows hand the app's agent.",
193
+ sql: AGENT_TASKS_DDL,
194
+ }));
195
+ }
196
+ if (lacks([SIGN_IN_EMAILS_TABLE])) {
197
+ draft.runtimeTables.push(step({
198
+ kind: "createCatalog",
199
+ classification: "additive",
200
+ description: "Create the table of the sign-in links actions send.",
201
+ sql: SIGN_IN_EMAILS_DDL,
202
+ }));
203
+ }
204
+ if (lacks(AGENT_TABLES)) {
205
+ draft.runtimeTables.push(step({
206
+ kind: "createCatalog",
207
+ classification: "additive",
208
+ description: "Create the runtime's agent tables, which pair channel accounts with members.",
209
+ sql: AGENT_TABLES_DDL,
210
+ }));
211
+ }
212
+ planBrowserMethodFences(target, methods, draft);
213
+ }
214
+ const storesFiles = storedTargets.some((entity) => entity.properties.some((property) => property.type === "blob"));
215
+ if (storesFiles &&
216
+ !current?.entities.some((entity) => entity.properties.some((property) => property.type === "blob"))) {
217
+ draft.runtimeTables.push(step({
218
+ kind: "createCatalog",
219
+ classification: "additive",
220
+ description: "Create the ontology's file revision, upload and agent attachment tables.",
221
+ sql: [...BLOB_TABLES_DDL, ...ATTACHMENT_TABLES_DDL],
222
+ }));
223
+ }
224
+ else if (storesFiles && input.existingTables?.has(ATTACHMENTS_TABLE) === false) {
225
+ // File storage an earlier SDK created lacks the agent's attachments, or their newer sources.
226
+ draft.runtimeTables.push(step({
227
+ kind: "createCatalog",
228
+ classification: "additive",
229
+ description: "Create the agent's temporary attachment table beside the file tables, or widen the " +
230
+ "sources it accepts.",
231
+ sql: ATTACHMENT_TABLES_DDL,
232
+ }));
233
+ }
234
+ planDrops({ pairs, removedEntities, addedEntities, allowDrops, draft });
235
+ // Everything the database stores once the drops ran, under the applied names.
236
+ const model = [
237
+ ...pairs.map((pair) => pairModel(pair, allowDrops)),
238
+ ...keptEntities,
239
+ ...(allowDrops
240
+ ? []
241
+ : removedEntities.map((removed) => ({ ...removed, detached: true, removed: true }))),
242
+ ];
243
+ const renamed = planRenames({ pairs, model, draft });
244
+ // Everything the database stores before the plan, which steps outside the transaction meet.
245
+ const accounted = [
246
+ ...currentEntities.map((entity) => declaredModel(entity, records)),
247
+ ...records.filter((record) => record.removed).map(keptModel),
248
+ ];
249
+ const appliedNames = new Set(storageObjects(accounted)
250
+ .filter((object) => GLOBAL_KINDS.has(object.kind))
251
+ .map((object) => object.name));
252
+ for (const pair of pairs) {
253
+ planEntityChanges({ pair, renamed, appliedNames, allowDrops, draft });
254
+ }
255
+ planNewEntities({ addedEntities, pairs, draft });
256
+ planJoinTables({ pairs, addedEntities, draft });
257
+ // The storage after the plan: the target's, beside the storage that stays kept.
258
+ const final = [
259
+ ...storedTargets.map((entity) => finalModel(entity, renamed)),
260
+ ...renamed.filter((stored) => stored.removed),
261
+ ];
262
+ checkKeptNames(final, draft.refusals);
263
+ if (input.existingStorage !== undefined) {
264
+ checkUnrecordedNames({
265
+ accounted,
266
+ final,
267
+ existing: input.existingStorage,
268
+ refusals: draft.refusals,
269
+ });
270
+ }
271
+ return {
272
+ ...header,
273
+ target,
274
+ steps: [
275
+ ...draft.before,
276
+ ...draft.catalog,
277
+ ...draft.drops,
278
+ ...draft.renames,
279
+ ...draft.indexDrops,
280
+ ...draft.changes,
281
+ ...draft.creates,
282
+ ...draft.additions,
283
+ ...draft.joins,
284
+ ...draft.runtimeTables,
285
+ ...draft.foreignKeys,
286
+ ...draft.indexes,
287
+ ],
288
+ refusals: draft.refusals,
289
+ ...retainedField(retainedAfter(renamed)),
290
+ };
291
+ }
292
+ /**
293
+ * A deploy that drops a browser sign-in method is refused while an active member would lose access:
294
+ * every established email member needs a SegmentStream identity to drop email, and every member with
295
+ * a SegmentStream identity an email sign-in to drop SegmentStream. They follow the runtime tables,
296
+ * which an earlier SDK's database may lack. A target with `user_identity` reads it, once the step
297
+ * before copied what the legacy stores hold; another reads the legacy stores, as SDKs before it did.
298
+ */
299
+ function planBrowserMethodFences(target, methods, draft) {
300
+ const fences = [
301
+ ...(methods && !methods.includes("email")
302
+ ? [
303
+ {
304
+ description: "Verify every established email member has a SegmentStream identity.",
305
+ identities: SEGMENTSTREAM_LINK_FENCE_SQL,
306
+ legacy: `DO $$ BEGIN
307
+ IF to_regclass('_user_identities') IS NULL THEN
308
+ IF EXISTS (SELECT 1 FROM "user" u WHERE u.kind = 'member' AND u.active = true
309
+ AND u.identity_provider = 'neon-auth' AND u.identity_subject IS NOT NULL)
310
+ THEN RAISE EXCEPTION 'browser_auth_segmentstream_link_required'; END IF;
311
+ ELSIF EXISTS (SELECT 1 FROM "user" u WHERE u.kind = 'member' AND u.active = true
312
+ AND u.identity_provider = 'neon-auth' AND u.identity_subject IS NOT NULL
313
+ AND NOT EXISTS (SELECT 1 FROM "_user_identities" i WHERE i.user_id = u.id))
314
+ THEN RAISE EXCEPTION 'browser_auth_segmentstream_link_required';
315
+ END IF;
316
+ END $$`,
317
+ },
318
+ ]
319
+ : []),
320
+ ...(methods && !methods.includes("segmentstream")
321
+ ? [
322
+ {
323
+ description: "Verify every admitted OIDC member retains email authentication.",
324
+ identities: EMAIL_LINK_FENCE_SQL,
325
+ legacy: `DO $$ BEGIN
326
+ IF to_regclass('_user_identities') IS NOT NULL THEN
327
+ IF EXISTS (SELECT 1 FROM "user" u WHERE u.kind = 'member' AND u.active = true
328
+ AND EXISTS (SELECT 1 FROM "_user_identities" i WHERE i.user_id = u.id)
329
+ AND (u.identity_provider IS DISTINCT FROM 'neon-auth' OR u.identity_subject IS NULL))
330
+ THEN RAISE EXCEPTION 'browser_auth_email_link_required'; END IF;
331
+ END IF;
332
+ END $$`,
333
+ },
334
+ ]
335
+ : []),
336
+ ];
337
+ if (fences.length === 0) {
338
+ return;
339
+ }
340
+ const identities = target.entities.some((entity) => entity.id === USER_IDENTITY_ENTITY_ID);
341
+ if (identities) {
342
+ draft.runtimeTables.push(step({
343
+ kind: "createCatalog",
344
+ classification: "additive",
345
+ description: "Copy members' sign-in identities the legacy identity stores hold.",
346
+ sql: [USER_IDENTITY_SYNC_SQL],
347
+ }));
348
+ }
349
+ for (const fence of fences) {
350
+ draft.runtimeTables.push(step({
351
+ kind: "createCatalog",
352
+ classification: "additive",
353
+ validatesData: true,
354
+ description: fence.description,
355
+ sql: [identities ? fence.identities : fence.legacy],
356
+ }));
357
+ }
358
+ }
359
+ // Validation -------------------------------------------------------------------------------------
360
+ function isValidDefault(property) {
361
+ if (!("default" in property) || property.default === undefined) {
362
+ return true;
363
+ }
364
+ const value = property.default;
365
+ switch (property.type) {
366
+ case "json":
367
+ return true;
368
+ case "integer":
369
+ return typeof value === "number" && Number.isInteger(value);
370
+ case "number":
371
+ return typeof value === "number" && Number.isFinite(value);
372
+ case "boolean":
373
+ return typeof value === "boolean";
374
+ case "select":
375
+ return (property.options ?? []).some((option) => option.value === value);
376
+ default:
377
+ return typeof value === "string";
378
+ }
379
+ }
380
+ /**
381
+ * The manifest was validated by `check` and decoded with the manifest schema; this only guards what
382
+ * the storage mapping relies on, so that a hand-built or damaged manifest is refused instead of
383
+ * producing broken SQL.
384
+ */
385
+ function validateManifest(manifest, refusals) {
386
+ const invalid = (path, message) => {
387
+ refusals.push(refusal("invalid-manifest", path, message));
388
+ };
389
+ const entitiesByName = new Map();
390
+ const ids = new Set();
391
+ for (const entity of manifest.entities) {
392
+ if (!SNAKE_CASE_NAME.test(entity.name)) {
393
+ invalid(entity.name, "entity names are lowercase snake_case.");
394
+ }
395
+ if (entitiesByName.has(entity.name)) {
396
+ invalid(entity.name, `two entities are named ${entity.name}.`);
397
+ }
398
+ else {
399
+ entitiesByName.set(entity.name, entity);
400
+ }
401
+ if (ids.has(entity.id)) {
402
+ invalid(entity.name, `two entities have the id ${entity.id}.`);
403
+ }
404
+ ids.add(entity.id);
405
+ }
406
+ if (!entitiesByName.has(USER_TABLE)) {
407
+ invalid(USER_TABLE, "the built-in user entity is missing; every ontology includes it.");
408
+ }
409
+ for (const entity of manifest.entities) {
410
+ validateEntity({ entity, entitiesByName, invalid });
411
+ }
412
+ }
413
+ function validateEntity(input) {
414
+ const { entity, entitiesByName, invalid } = input;
415
+ for (const property of entity.properties) {
416
+ const path = `${entity.name}.properties.${property.name}`;
417
+ const values = (property.options ?? []).map((option) => option.value);
418
+ if (!SNAKE_CASE_NAME.test(property.name) || isSystemProperty(property.name)) {
419
+ invalid(path, "property names are lowercase snake_case and cannot be system properties.");
420
+ }
421
+ if (property.type === "select" &&
422
+ (values.length === 0 || values.some((value) => !SNAKE_CASE_NAME.test(value)))) {
423
+ invalid(path, "a select property needs at least one non-empty option.");
424
+ }
425
+ if (new Set(values).size !== values.length) {
426
+ invalid(path, "an option value is listed twice.");
427
+ }
428
+ if (!isValidDefault(property)) {
429
+ invalid(path, `the default ${JSON.stringify(property.default)} is not a ${property.type}.`);
430
+ }
431
+ }
432
+ for (const relationship of entity.relationships) {
433
+ const path = `${entity.name}.relationships.${relationship.name}`;
434
+ if (!SNAKE_CASE_NAME.test(relationship.name) || isSystemProperty(relationship.name)) {
435
+ invalid(path, "relationship names are lowercase snake_case and cannot be system properties.");
436
+ }
437
+ const target = entitiesByName.get(relationship.to);
438
+ if (target === undefined) {
439
+ invalid(path, `it points at ${relationship.to}, which is not an entity.`);
440
+ }
441
+ else if (isValueMatched(relationship) !== (entity.source !== undefined || target.source !== undefined)) {
442
+ // `on` is what keeps a relationship with a warehouse-backed end out of storage.
443
+ invalid(path, isValueMatched(relationship)
444
+ ? "a relationship between stored entities uses a key column and cannot declare on."
445
+ : "a relationship with a warehouse-backed end is matched by value and needs on.");
446
+ }
447
+ // A value-matched relationship has no foreign key, so no delete rule.
448
+ if (relationship.cardinality === "one" &&
449
+ !isValueMatched(relationship) &&
450
+ relationship.onDelete === undefined) {
451
+ invalid(path, "a rel.one relationship between stored entities needs onDelete.");
452
+ }
453
+ if (relationship.onDelete === "setNull" && relationship.optional !== true) {
454
+ invalid(path, 'onDelete "setNull" needs an optional relationship.');
455
+ }
456
+ }
457
+ entity.indexes.forEach((index, position) => {
458
+ const path = `${entity.name}.indexes[${position}]`;
459
+ if (index.properties.length === 0) {
460
+ invalid(path, "an index covers at least one field.");
461
+ }
462
+ for (const field of index.properties) {
463
+ if (fieldColumnName(entity, field) === undefined) {
464
+ invalid(path, `"${field}" is not a property, system property or rel.one of ${entity.name}.`);
465
+ }
466
+ }
467
+ });
468
+ }
469
+ // Storage names ----------------------------------------------------------------------------------
470
+ function tableObjects(table, owner, path) {
471
+ return [
472
+ { kind: "table", name: table.name, table: table.name, owner, path },
473
+ {
474
+ kind: "index",
475
+ name: table.primaryKey.name,
476
+ table: table.name,
477
+ owner: `the primary key of ${table.name}`,
478
+ path,
479
+ },
480
+ ...table.indexes.map((index) => ({
481
+ kind: "index",
482
+ name: index.name,
483
+ table: table.name,
484
+ owner: `index ${index.name} (${index.origin})`,
485
+ path,
486
+ })),
487
+ ...table.columns.map((column) => ({
488
+ kind: "column",
489
+ name: column.name,
490
+ table: table.name,
491
+ owner: `column ${table.name}.${column.name}`,
492
+ path,
493
+ })),
494
+ ...table.constraints.map((constraint) => ({
495
+ kind: "constraint",
496
+ name: constraint.name,
497
+ table: table.name,
498
+ owner: `constraint ${constraint.name} of ${table.name}`,
499
+ path,
500
+ })),
501
+ ];
502
+ }
503
+ function declared(entity) {
504
+ return { entity, detached: false, kept: NO_MEMBERS, restoring: NO_MEMBERS, removed: false };
505
+ }
506
+ function keptProperties(stored) {
507
+ return stored.entity.properties.filter((property) => stored.kept.properties.has(property.name));
508
+ }
509
+ function keptRelationships(stored) {
510
+ return stored.entity.relationships.filter((relationship) => stored.kept.relationships.has(relationship.name));
511
+ }
512
+ /**
513
+ * What the database holds for an entity: its storage less the foreign keys of a detached entity,
514
+ * and less the foreign keys and indexes of its kept fields.
515
+ */
516
+ function physicalStorage(stored) {
517
+ const { entity } = stored;
518
+ const storage = entityStorage(entity);
519
+ const properties = keptProperties(stored).filter(isStoredProperty);
520
+ const ones = keptRelationships(stored).filter((relationship) => relationship.cardinality === "one" && !isValueMatched(relationship));
521
+ const keptColumns = new Set([
522
+ ...properties.map((property) => columnName(property.name)),
523
+ ...ones.map((relationship) => relationshipColumnName(relationship.name)),
524
+ ]);
525
+ const keptKeys = new Set([
526
+ ...properties
527
+ .filter((property) => property.type === "blob")
528
+ .map((property) => blobForeignKey(entity.name, property).name),
529
+ ...ones.map((relationship) => relationshipForeignKey(entity.name, relationship).name),
530
+ ]);
531
+ const manys = entity.relationships.filter((relationship) => relationship.cardinality === "many");
532
+ return {
533
+ ...storage,
534
+ table: {
535
+ ...storage.table,
536
+ constraints: storage.table.constraints.filter((constraint) => constraint.kind !== "foreignKey" || (!stored.detached && !keptKeys.has(constraint.name))),
537
+ indexes: storage.table.indexes.filter((index) => !index.columns.some((column) => keptColumns.has(column))),
538
+ },
539
+ joins: storage.joins.map((join, position) => stored.detached || stored.kept.relationships.has(manys[position]?.name ?? "")
540
+ ? {
541
+ ...join,
542
+ constraints: join.constraints.filter((constraint) => constraint.kind !== "foreignKey"),
543
+ }
544
+ : join),
545
+ };
546
+ }
547
+ function keptEntityNote(entity) {
548
+ return (`The database keeps it for the removed entity ${entity.name}: entity ids never change, so ` +
549
+ `restore ${entity.id} to keep its records, or choose another name.`);
550
+ }
551
+ function keptFieldNote(entity, field) {
552
+ return (`The database keeps it for the removed field ${entity}.${field}: declare ${field} on ` +
553
+ `${entity} again to restore it, or choose another name.`);
554
+ }
555
+ /** Who each object of a declared entity's kept fields is kept for, by kind and name. */
556
+ function keptFieldNotes(stored) {
557
+ const { entity, restoring } = stored;
558
+ const notes = new Map();
559
+ for (const property of keptProperties(stored).filter(isStoredProperty)) {
560
+ if (restoring.properties.has(property.name)) {
561
+ continue;
562
+ }
563
+ const note = keptFieldNote(entity.name, property.name);
564
+ const check = propertyCheck(entity.name, property);
565
+ notes.set(`column:${columnName(property.name)}`, note);
566
+ if (property.type === "select") {
567
+ notes.set(`type:${enumTypeName(entity.name, property.name)}`, note);
568
+ }
569
+ if (check !== undefined) {
570
+ notes.set(`constraint:${check.name}`, note);
571
+ }
572
+ }
573
+ for (const relationship of keptRelationships(stored)) {
574
+ if (restoring.relationships.has(relationship.name)) {
575
+ continue;
576
+ }
577
+ const note = keptFieldNote(entity.name, relationship.name);
578
+ notes.set(relationship.cardinality === "one"
579
+ ? `column:${relationshipColumnName(relationship.name)}`
580
+ : `join:${joinTable(entity.name, relationship).name}`, note);
581
+ }
582
+ return notes;
583
+ }
584
+ /** Every named object the entities store, in a deterministic order. */
585
+ function storageObjects(models) {
586
+ return models.flatMap((stored) => {
587
+ const { entity } = stored;
588
+ const storage = physicalStorage(stored);
589
+ const manys = entity.relationships.filter((relationship) => relationship.cardinality === "many");
590
+ const entityNote = stored.removed ? keptEntityNote(entity) : undefined;
591
+ const fieldNotes = entityNote === undefined ? keptFieldNotes(stored) : new Map();
592
+ const objects = [
593
+ ...tableObjects(storage.table, `the table of entity ${entity.name}`, entity.name),
594
+ ...tableObjects(storage.history, `the history table of ${entity.name}`, entity.name),
595
+ ...storage.joins.flatMap((join, position) => tableObjects(join, `the join table of ${entity.name}.${manys[position]?.name ?? join.name}`, `${entity.name}.relationships.${manys[position]?.name ?? join.name}`)),
596
+ ...entity.properties
597
+ .filter((property) => property.type === "select")
598
+ .map((property) => ({
599
+ kind: "type",
600
+ name: enumTypeName(entity.name, property.name),
601
+ table: "",
602
+ owner: `the enum type of ${entity.name}.${property.name}`,
603
+ path: `${entity.name}.properties.${property.name}`,
604
+ })),
605
+ ];
606
+ return objects.map((object) => {
607
+ const note = entityNote ??
608
+ fieldNotes.get(`${object.kind}:${object.name}`) ??
609
+ fieldNotes.get(`join:${object.table}`);
610
+ return note === undefined ? object : { ...object, kept: note };
611
+ });
612
+ });
613
+ }
614
+ /** Tables, types and indexes share one namespace; columns and constraints are per table. */
615
+ function scopeOf(object) {
616
+ return GLOBAL_KINDS.has(object.kind) ? "global" : `${object.kind}:${object.table}`;
617
+ }
618
+ function collisions(objects) {
619
+ const seen = new Map();
620
+ const found = [];
621
+ for (const object of objects) {
622
+ const key = `${scopeOf(object)}\u0000${object.name}`;
623
+ const first = seen.get(key);
624
+ if (first === undefined) {
625
+ seen.set(key, object);
626
+ continue;
627
+ }
628
+ found.push({ first, second: object });
629
+ }
630
+ return found;
631
+ }
632
+ function findCollisions(objects) {
633
+ return collisions(objects).map(({ first, second }) => refusal("name-collision", second.path, `"${second.name}" would name both ${first.owner} and ${second.owner}; rename one of them.`));
634
+ }
635
+ /** Collisions with kept storage: one refusal per kept entity or field, at its first collision. */
636
+ function keptCollisionRefusals(found) {
637
+ const reported = new Set();
638
+ const refusals = [];
639
+ for (const { first, second } of found) {
640
+ const kept = first.kept === undefined ? second : first;
641
+ const other = kept === first ? second : first;
642
+ if (kept.kept === undefined || reported.has(kept.kept)) {
643
+ continue;
644
+ }
645
+ reported.add(kept.kept);
646
+ refusals.push(refusal("name-collision", other.path, `"${kept.name}" would name both ${other.owner} and ${kept.owner}. ${kept.kept}`));
647
+ }
648
+ return refusals;
649
+ }
650
+ function checkStorageNames(entities, refusals) {
651
+ const objects = storageObjects(entities.map(declared));
652
+ const reported = new Set();
653
+ for (const object of objects) {
654
+ if (object.name.length <= MAX_IDENTIFIER_LENGTH || reported.has(object.name)) {
655
+ continue;
656
+ }
657
+ reported.add(object.name);
658
+ refusals.push(refusal("identifier-too-long", object.path, `"${object.name}" (${object.owner}) is longer than Postgres' ${MAX_IDENTIFIER_LENGTH} ` +
659
+ "characters; shorten the entity or property name."));
660
+ }
661
+ refusals.push(...findCollisions(objects));
662
+ }
663
+ /** The target's storage cannot take a name its kept storage holds. */
664
+ function checkKeptNames(final, refusals) {
665
+ // A refused rename already reported the collisions it would cause.
666
+ const reported = new Set(refusals.map((refusal) => `${refusal.path}\u0000${refusal.message}`));
667
+ refusals.push(...keptCollisionRefusals(collisions(storageObjects(final))).filter((refusal) => !reported.has(`${refusal.path}\u0000${refusal.message}`)));
668
+ }
669
+ const UNRECORDED = "already exists in the database, but neither the applied ontology nor the storage its catalog " +
670
+ "keeps accounts for it: storage an earlier SDK kept when something was removed, for example. Its " +
671
+ "definition is unknown, so it cannot be restored. Drop it from the database, or choose another name.";
672
+ /** The member of an entity whose column has this name, for refusal paths. */
673
+ function columnPath(entity, column) {
674
+ const property = entity.properties.find((candidate) => columnName(candidate.name) === column);
675
+ const relationship = entity.relationships.find((candidate) => candidate.cardinality === "one" && relationshipColumnName(candidate.name) === column);
676
+ return property !== undefined
677
+ ? `${entity.name}.properties.${property.name}`
678
+ : relationship !== undefined
679
+ ? `${entity.name}.relationships.${relationship.name}`
680
+ : entity.name;
681
+ }
682
+ function existingColumns(existing, table) {
683
+ return new Set(Object.hasOwn(existing.columns, table) ? existing.columns[table] : []);
684
+ }
685
+ /**
686
+ * The plan creates or renames into names the database already holds only where the applied
687
+ * ontology or its kept storage accounts for them: anything else would fail in SQL.
688
+ */
689
+ function checkUnrecordedNames(input) {
690
+ const { accounted, final, existing, refusals } = input;
691
+ const physical = new Set(existing.names);
692
+ const known = new Set(storageObjects(accounted)
693
+ .filter((object) => GLOBAL_KINDS.has(object.kind))
694
+ .map((object) => object.name));
695
+ const reported = new Set();
696
+ // Indexes are left out: a failed concurrent build leaves an index the catalog does not record,
697
+ // which the next build drops first. Kept tables and columns are what carry unrecorded data.
698
+ for (const object of storageObjects(final)) {
699
+ if ((object.kind !== "table" && object.kind !== "type") ||
700
+ known.has(object.name) ||
701
+ !physical.has(object.name) ||
702
+ reported.has(object.name)) {
703
+ continue;
704
+ }
705
+ reported.add(object.name);
706
+ refusals.push(refusal("name-collision", object.path, `"${object.name}" (${object.owner}) ${UNRECORDED}`));
707
+ }
708
+ for (const stored of final) {
709
+ const before = accounted.find((candidate) => candidate.entity.id === stored.entity.id);
710
+ if (before === undefined) {
711
+ continue;
712
+ }
713
+ const table = tableName(before.entity.name);
714
+ const columns = new Set(physicalStorage(before).table.columns.map((column) => column.name));
715
+ const present = existingColumns(existing, table);
716
+ for (const column of physicalStorage(stored).table.columns) {
717
+ if (!columns.has(column.name) && present.has(column.name)) {
718
+ refusals.push(refusal("name-collision", columnPath(stored.entity, column.name), `Column "${column.name}" of ${table} ${UNRECORDED}`));
719
+ }
720
+ }
721
+ }
722
+ }
723
+ // Kept storage -----------------------------------------------------------------------------------
724
+ /** A removed property as its column is kept: nullable, without a default or index. */
725
+ function keptProperty(property) {
726
+ const kept = {
727
+ ...property,
728
+ optional: true,
729
+ unique: false,
730
+ };
731
+ delete kept.default;
732
+ return kept;
733
+ }
734
+ /** A removed relationship as its key column or join table is kept. */
735
+ function keptRelationship(relationship) {
736
+ return relationship.cardinality === "one" ? { ...relationship, optional: true } : relationship;
737
+ }
738
+ function names(members) {
739
+ return new Set(members.map((member) => member.name));
740
+ }
741
+ /** A declared entity with the kept fields the catalog records for it. */
742
+ function declaredModel(entity, records) {
743
+ const record = records.find((candidate) => candidate.id === entity.id && !candidate.removed);
744
+ if (record === undefined) {
745
+ return declared(entity);
746
+ }
747
+ return {
748
+ entity: {
749
+ ...entity,
750
+ properties: [...entity.properties, ...record.fields.properties],
751
+ relationships: [...entity.relationships, ...record.fields.relationships],
752
+ },
753
+ detached: false,
754
+ kept: {
755
+ properties: names(record.fields.properties),
756
+ relationships: names(record.fields.relationships),
757
+ },
758
+ restoring: NO_MEMBERS,
759
+ removed: false,
760
+ };
761
+ }
762
+ /** A removed entity whose storage is kept. */
763
+ function keptModel(record) {
764
+ const entity = retainedEntityManifest(record);
765
+ return {
766
+ entity: {
767
+ ...entity,
768
+ properties: [...entity.properties, ...record.fields.properties],
769
+ relationships: [...entity.relationships, ...record.fields.relationships],
770
+ },
771
+ detached: true,
772
+ kept: {
773
+ properties: names(record.fields.properties),
774
+ relationships: names(record.fields.relationships),
775
+ },
776
+ restoring: NO_MEMBERS,
777
+ removed: true,
778
+ };
779
+ }
780
+ /**
781
+ * A matched entity's storage once the drops ran: its matched members, then the members this plan
782
+ * removes and keeps, then the kept fields that stay kept. Restored fields are still in the kept
783
+ * state; the plan declares them later.
784
+ */
785
+ function pairModel(pair, allowDrops) {
786
+ const matched = {
787
+ ...pair.current,
788
+ properties: pair.properties.map(({ current }) => current),
789
+ relationships: pair.relationships.map(({ current }) => current),
790
+ };
791
+ const removedProperties = allowDrops ? [] : pair.removedProperties.map(keptProperty);
792
+ const removedRelationships = allowDrops ? [] : pair.removedRelationships.map(keptRelationship);
793
+ return {
794
+ entity: {
795
+ ...matched,
796
+ properties: [...matched.properties, ...removedProperties, ...pair.keptProperties],
797
+ relationships: [...matched.relationships, ...removedRelationships, ...pair.keptRelationships],
798
+ indexes: pair.current.indexes.filter((index) => index.properties.every((field) => fieldColumnName(pair.current, field) !== undefined)),
799
+ },
800
+ detached: pair.restored,
801
+ kept: {
802
+ properties: new Set([
803
+ ...pair.restoredFields.properties,
804
+ ...names(removedProperties),
805
+ ...names(pair.keptProperties),
806
+ ]),
807
+ relationships: new Set([
808
+ ...pair.restoredFields.relationships,
809
+ ...names(removedRelationships),
810
+ ...names(pair.keptRelationships),
811
+ ]),
812
+ },
813
+ restoring: pair.restoredFields,
814
+ removed: false,
815
+ };
816
+ }
817
+ /** A target entity with the kept fields that stay kept, under the target's names. */
818
+ function finalModel(entity, renamed) {
819
+ const stored = renamed.find((candidate) => candidate.entity.id === entity.id && !candidate.removed);
820
+ if (stored === undefined) {
821
+ return declared(entity);
822
+ }
823
+ const properties = keptProperties(stored).filter((property) => !stored.restoring.properties.has(property.name));
824
+ const relationships = keptRelationships(stored).filter((relationship) => !stored.restoring.relationships.has(relationship.name));
825
+ return {
826
+ entity: {
827
+ ...entity,
828
+ properties: [...entity.properties, ...properties],
829
+ relationships: [...entity.relationships, ...relationships],
830
+ },
831
+ detached: false,
832
+ kept: { properties: names(properties), relationships: names(relationships) },
833
+ restoring: NO_MEMBERS,
834
+ removed: false,
835
+ };
836
+ }
837
+ /** What the catalog records after the plan: the renamed model's kept storage, by entity id. */
838
+ function retainedAfter(model) {
839
+ const entities = model.flatMap((stored) => {
840
+ const { entity, kept, restoring } = stored;
841
+ const fields = {
842
+ properties: keptProperties(stored).filter((property) => !restoring.properties.has(property.name)),
843
+ relationships: keptRelationships(stored).filter((relationship) => !restoring.relationships.has(relationship.name)),
844
+ };
845
+ if (stored.removed) {
846
+ return [
847
+ {
848
+ id: entity.id,
849
+ name: entity.name,
850
+ removed: true,
851
+ properties: entity.properties.filter((property) => !kept.properties.has(property.name)),
852
+ relationships: entity.relationships.filter((relationship) => !kept.relationships.has(relationship.name) && !isValueMatched(relationship)),
853
+ indexes: entity.indexes,
854
+ fields,
855
+ },
856
+ ];
857
+ }
858
+ return fields.properties.length + fields.relationships.length === 0
859
+ ? []
860
+ : [
861
+ {
862
+ id: entity.id,
863
+ name: entity.name,
864
+ removed: false,
865
+ properties: [],
866
+ relationships: [],
867
+ indexes: [],
868
+ fields,
869
+ },
870
+ ];
871
+ });
872
+ return {
873
+ entities: entities.sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0),
874
+ };
875
+ }
876
+ /**
877
+ * The catalog's kept storage that is still what it records. Without observed storage the record is
878
+ * trusted where it agrees with the applied manifest. With it, kept storage whose objects are gone,
879
+ * dropped by hand or renamed by an earlier SDK, is forgotten; what remains of it is unrecorded, so
880
+ * the plan cannot take its names.
881
+ */
882
+ function reconcileRetained(input) {
883
+ const { current, existing } = input;
884
+ const declaredById = new Map(current.map((entity) => [entity.id, entity]));
885
+ const declaredNames = new Set(current.map((entity) => entity.name));
886
+ const physical = new Set(existing?.names ?? []);
887
+ const holds = (table) => physical.has(table.name) &&
888
+ table.columns.every((column) => existingColumns(existing, table.name).has(column.name));
889
+ const seen = new Set();
890
+ return input.retained.entities.flatMap((record) => {
891
+ if (seen.has(record.id)) {
892
+ return [];
893
+ }
894
+ seen.add(record.id);
895
+ if (record.removed) {
896
+ if (declaredById.has(record.id) || declaredNames.has(record.name)) {
897
+ return [];
898
+ }
899
+ if (existing === undefined) {
900
+ return [record];
901
+ }
902
+ const storage = physicalStorage(keptModel(record));
903
+ return [storage.table, storage.history, ...storage.joins].every(holds) &&
904
+ storage.enums.every((storage) => physical.has(storage.name))
905
+ ? [record]
906
+ : [];
907
+ }
908
+ const entity = declaredById.get(record.id);
909
+ if (entity === undefined || entity.name !== record.name) {
910
+ return [];
911
+ }
912
+ const table = tableName(entity.name);
913
+ const columns = existing === undefined ? undefined : existingColumns(existing, table);
914
+ const properties = record.fields.properties.filter((property) => !entity.properties.some((candidate) => candidate.name === property.name) &&
915
+ (columns === undefined ||
916
+ (columns.has(columnName(property.name)) &&
917
+ (property.type !== "select" ||
918
+ physical.has(enumTypeName(entity.name, property.name))))));
919
+ const relationships = record.fields.relationships.filter((relationship) => !entity.relationships.some((candidate) => candidate.name === relationship.name) &&
920
+ (columns === undefined ||
921
+ (relationship.cardinality === "one"
922
+ ? columns.has(relationshipColumnName(relationship.name))
923
+ : holds(joinTable(entity.name, relationship)))));
924
+ return properties.length + relationships.length === 0
925
+ ? []
926
+ : [{ ...record, fields: { properties, relationships } }];
927
+ });
928
+ }
929
+ // Matching ---------------------------------------------------------------------------------------
930
+ function entityIdByName(entities, name) {
931
+ return entities.find((entity) => entity.name === name)?.id;
932
+ }
933
+ function matchProperties(current, target, kept) {
934
+ const byName = new Map(current.properties.map((property) => [property.name, property]));
935
+ const keptByName = new Map(kept.map((property) => [property.name, property]));
936
+ const claimed = new Set(target.properties.filter((property) => byName.has(property.name)).map(({ name }) => name));
937
+ const properties = [];
938
+ const addedProperties = [];
939
+ const restoredProperties = new Set();
940
+ for (const property of target.properties) {
941
+ const sameName = byName.get(property.name);
942
+ const source = property.renamedFrom === undefined
943
+ ? undefined
944
+ : (byName.get(property.renamedFrom) ?? byName.get(toSnakeCase(property.renamedFrom)));
945
+ const previous = source !== undefined && !claimed.has(source.name) ? source : undefined;
946
+ const match = sameName ?? previous;
947
+ if (match === undefined) {
948
+ const keptProperty = keptByName.get(property.name);
949
+ // A kept field is declared again under its own name.
950
+ if (keptProperty === undefined) {
951
+ addedProperties.push(property);
952
+ }
953
+ else {
954
+ restoredProperties.add(property.name);
955
+ properties.push({ current: keptProperty, target: property });
956
+ }
957
+ continue;
958
+ }
959
+ claimed.add(match.name);
960
+ properties.push({ current: match, target: property });
961
+ }
962
+ const removedProperties = current.properties.filter((property) => !claimed.has(property.name));
963
+ return {
964
+ properties,
965
+ addedProperties,
966
+ removedProperties,
967
+ restoredProperties,
968
+ keptProperties: kept.filter((property) => !restoredProperties.has(property.name)),
969
+ };
970
+ }
971
+ function matchEntities(input) {
972
+ const { currentEntities, records, targetEntities, refusals } = input;
973
+ const currentById = new Map(currentEntities.map((entity) => [entity.id, entity]));
974
+ const recordById = new Map(records.map((record) => [record.id, record]));
975
+ // Relationship targets resolve among everything the database stores, declared or kept.
976
+ const storedEntities = [
977
+ ...currentEntities,
978
+ ...records.filter((record) => record.removed).map(retainedEntityManifest),
979
+ ];
980
+ const pairs = [];
981
+ for (const target of targetEntities) {
982
+ const record = recordById.get(target.id);
983
+ const restored = !currentById.has(target.id) && record?.removed === true;
984
+ const current = currentById.get(target.id) ??
985
+ (restored && record !== undefined ? retainedEntityManifest(record) : undefined);
986
+ if (current === undefined) {
987
+ continue;
988
+ }
989
+ const keptRelationshipsByName = new Map((record?.fields.relationships ?? []).map((relationship) => [relationship.name, relationship]));
990
+ const relationships = [];
991
+ const addedRelationships = [];
992
+ const restoredRelationships = new Set();
993
+ const matched = new Set();
994
+ const changed = new Set();
995
+ for (const relationship of target.relationships) {
996
+ const declaredPrevious = current.relationships.find(({ name }) => name === relationship.name);
997
+ const keptPrevious = isValueMatched(relationship)
998
+ ? undefined
999
+ : keptRelationshipsByName.get(relationship.name);
1000
+ const previous = declaredPrevious ?? keptPrevious;
1001
+ if (previous === undefined) {
1002
+ if (!isValueMatched(relationship)) {
1003
+ addedRelationships.push(relationship);
1004
+ }
1005
+ continue;
1006
+ }
1007
+ // Both lists hold stored entities only, so a warehouse-backed target has no id here.
1008
+ const previousTarget = entityIdByName(storedEntities, previous.to);
1009
+ const nextTarget = entityIdByName(targetEntities, relationship.to);
1010
+ if (previous.cardinality !== relationship.cardinality ||
1011
+ previousTarget !== nextTarget ||
1012
+ isValueMatched(previous) !== isValueMatched(relationship)) {
1013
+ changed.add(relationship.name);
1014
+ refusals.push(refusal("relationship-change", `${target.name}.relationships.${relationship.name}`, declaredPrevious === undefined
1015
+ ? `was rel.${previous.cardinality} to ${previous.to} when it was removed, and the ` +
1016
+ "database keeps its links; it cannot come back as " +
1017
+ `rel.${relationship.cardinality} to ${relationship.to}. Declare it as it was to ` +
1018
+ "restore it, or choose another name."
1019
+ : `was rel.${previous.cardinality} to ${previous.to} and is now ` +
1020
+ `rel.${relationship.cardinality} to ${relationship.to}; a relationship keeps its ` +
1021
+ "target and cardinality. Declare a new relationship and drop this one instead."));
1022
+ continue;
1023
+ }
1024
+ if (declaredPrevious === undefined) {
1025
+ restoredRelationships.add(relationship.name);
1026
+ }
1027
+ else {
1028
+ matched.add(relationship.name);
1029
+ }
1030
+ if (!isValueMatched(relationship)) {
1031
+ relationships.push({ current: previous, target: relationship });
1032
+ }
1033
+ }
1034
+ // A value-matched relationship is stored as the property its `on` names: adding, keeping or
1035
+ // removing one plans no statement and drops nothing.
1036
+ const removedRelationships = current.relationships.filter((relationship) => !matched.has(relationship.name) &&
1037
+ !changed.has(relationship.name) &&
1038
+ !isValueMatched(relationship));
1039
+ const { restoredProperties, ...properties } = matchProperties(current, target, record?.fields.properties ?? []);
1040
+ pairs.push({
1041
+ current,
1042
+ target,
1043
+ restored,
1044
+ ...properties,
1045
+ relationships,
1046
+ addedRelationships,
1047
+ removedRelationships,
1048
+ restoredFields: { properties: restoredProperties, relationships: restoredRelationships },
1049
+ keptRelationships: [...keptRelationshipsByName.values()].filter((relationship) => !restoredRelationships.has(relationship.name)),
1050
+ });
1051
+ }
1052
+ return pairs;
1053
+ }
1054
+ // Drops ------------------------------------------------------------------------------------------
1055
+ function planDrops(input) {
1056
+ const { pairs, removedEntities, addedEntities, allowDrops, draft } = input;
1057
+ for (const pair of pairs) {
1058
+ const table = tableName(pair.current.name);
1059
+ // A restored entity's foreign keys were detached when it was removed.
1060
+ const attached = !pair.restored;
1061
+ for (const relationship of pair.removedRelationships) {
1062
+ if (!allowDrops) {
1063
+ const actions = relationship.cardinality === "one"
1064
+ ? [
1065
+ ...(relationship.optional === true
1066
+ ? []
1067
+ : [
1068
+ `ALTER COLUMN ${quoteIdentifier(relationshipColumn(relationship).name)} DROP NOT NULL`,
1069
+ ]),
1070
+ ...(attached
1071
+ ? [
1072
+ `DROP CONSTRAINT ${quoteIdentifier(relationshipForeignKey(pair.current.name, relationship).name)}`,
1073
+ ]
1074
+ : []),
1075
+ ]
1076
+ : [];
1077
+ draft.drops.push(step({
1078
+ kind: "removeRelationship",
1079
+ classification: "change",
1080
+ description: `Remove relationship ${pair.current.name}.${relationship.name}, retaining its links.`,
1081
+ sql: relationship.cardinality === "one"
1082
+ ? actions.length === 0
1083
+ ? []
1084
+ : [alterTable(table, actions.join(", "))]
1085
+ : attached
1086
+ ? detachForeignKeys(joinTable(pair.current.name, relationship))
1087
+ : [],
1088
+ }));
1089
+ continue;
1090
+ }
1091
+ const statement = relationship.cardinality === "one"
1092
+ ? alterTable(table, `DROP COLUMN ${quoteIdentifier(relationshipColumn(relationship).name)}`)
1093
+ : `DROP TABLE ${quoteIdentifier(joinTable(pair.current.name, relationship).name)}`;
1094
+ draft.drops.push(step({
1095
+ kind: "dropRelationship",
1096
+ classification: "destructive",
1097
+ description: `Drop relationship ${pair.current.name}.${relationship.name} and its links.`,
1098
+ sql: [statement],
1099
+ }));
1100
+ }
1101
+ for (const property of pair.removedProperties) {
1102
+ if (!allowDrops) {
1103
+ const column = propertyColumn(pair.current.name, property);
1104
+ // Kept storage holds no foreign keys, so a kept file revision never blocks maintenance.
1105
+ const actions = [
1106
+ ...(column.notNull ? [`ALTER COLUMN ${quoteIdentifier(column.name)} DROP NOT NULL`] : []),
1107
+ ...(column.default === undefined
1108
+ ? []
1109
+ : [`ALTER COLUMN ${quoteIdentifier(column.name)} DROP DEFAULT`]),
1110
+ ...(property.type === "blob" && attached
1111
+ ? [
1112
+ `DROP CONSTRAINT ${quoteIdentifier(blobForeignKey(pair.current.name, property).name)}`,
1113
+ ]
1114
+ : []),
1115
+ ];
1116
+ draft.drops.push(step({
1117
+ kind: "removeProperty",
1118
+ classification: "change",
1119
+ description: `Remove property ${pair.current.name}.${property.name}, retaining its values.`,
1120
+ sql: actions.length === 0 ? [] : [alterTable(table, actions.join(", "))],
1121
+ }));
1122
+ continue;
1123
+ }
1124
+ const enumStorage = propertyEnum(pair.current.name, property);
1125
+ draft.drops.push(step({
1126
+ kind: "dropProperty",
1127
+ classification: "destructive",
1128
+ description: `Drop property ${pair.current.name}.${property.name} and its values.`,
1129
+ sql: [
1130
+ alterTable(table, `DROP COLUMN ${quoteIdentifier(propertyColumn(pair.current.name, property).name)}`),
1131
+ ...(enumStorage === undefined
1132
+ ? []
1133
+ : [`DROP TYPE ${quoteIdentifier(enumStorage.name)}`]),
1134
+ ],
1135
+ }));
1136
+ }
1137
+ }
1138
+ if (!allowDrops) {
1139
+ for (const pair of pairs) {
1140
+ // Dropped columns used to drop these indexes implicitly. Retained columns need explicit
1141
+ // cleanup so removed unique properties and compound indexes cannot constrain active writes.
1142
+ const removedColumns = new Set([
1143
+ ...pair.removedProperties.map((property) => propertyColumn(pair.current.name, property).name),
1144
+ ...pair.removedRelationships
1145
+ .filter((relationship) => relationship.cardinality === "one")
1146
+ .map((relationship) => relationshipColumn(relationship).name),
1147
+ ]);
1148
+ for (const index of entityStorage(pair.current).table.indexes) {
1149
+ if (index.columns.some((column) => removedColumns.has(column))) {
1150
+ draft.drops.push(step({
1151
+ kind: "dropIndex",
1152
+ classification: "change",
1153
+ description: `Drop index ${index.name} (${index.origin}).`,
1154
+ sql: [`DROP INDEX ${quoteIdentifier(index.name)}`],
1155
+ }));
1156
+ }
1157
+ }
1158
+ }
1159
+ }
1160
+ if (removedEntities.length === 0) {
1161
+ return;
1162
+ }
1163
+ const names = removedEntities.map(({ entity }) => entity.name);
1164
+ if (!allowDrops) {
1165
+ for (const { entity } of removedEntities) {
1166
+ // A successor under the removed name collides with the kept storage, refused with the
1167
+ // other kept names; one that only claims to be its rename has a new id.
1168
+ const successor = addedEntities.find((candidate) => candidate.renamedFrom === entity.name && candidate.name !== entity.name);
1169
+ if (successor !== undefined) {
1170
+ draft.refusals.push(refusal("name-collision", entity.name, `${entity.name}'s storage is retained. ${successor.name} has the new id ${successor.id}; ` +
1171
+ `entity ids never change, so restore ${entity.id} to keep ${entity.name}'s records.`));
1172
+ }
1173
+ }
1174
+ draft.drops.push(step({
1175
+ kind: "removeEntities",
1176
+ classification: "change",
1177
+ description: `Remove entities ${names.join(", ")}, retaining their records and history.`,
1178
+ sql: removedEntities.flatMap((removed) => {
1179
+ const storage = physicalStorage(removed);
1180
+ return [storage.table, ...storage.joins].flatMap(detachForeignKeys);
1181
+ }),
1182
+ }));
1183
+ return;
1184
+ }
1185
+ // One statement per object kind: Postgres resolves the references between the dropped tables.
1186
+ // An entity's kept fields go with it: their join tables and enum types are its storage too.
1187
+ const storages = removedEntities.map(physicalStorage);
1188
+ const quotedList = (names) => names.map(quoteIdentifier).join(", ");
1189
+ const joins = storages.flatMap((storage) => storage.joins.map((join) => join.name));
1190
+ const enums = storages.flatMap((storage) => storage.enums.map((storage) => storage.name));
1191
+ const noun = names.length === 1 ? "entity" : "entities";
1192
+ draft.drops.push(step({
1193
+ kind: "dropEntities",
1194
+ classification: "destructive",
1195
+ description: `Drop ${noun} ${names.join(", ")} with their records and history.`,
1196
+ sql: [
1197
+ ...(joins.length === 0 ? [] : [`DROP TABLE ${quotedList(joins)}`]),
1198
+ `DROP TABLE ${quotedList(storages.map((storage) => storage.history.name))}`,
1199
+ `DROP TABLE ${quotedList(storages.map((storage) => storage.table.name))}`,
1200
+ ...(enums.length === 0 ? [] : [`DROP TYPE ${quotedList(enums)}`]),
1201
+ ],
1202
+ }));
1203
+ }
1204
+ /** Retired tables must neither block active record deletion nor lose retained rows to cascades. */
1205
+ function detachForeignKeys(table) {
1206
+ return table.constraints
1207
+ .filter((constraint) => constraint.kind === "foreignKey")
1208
+ .map((constraint) => alterTable(table.name, `DROP CONSTRAINT ${quoteIdentifier(constraint.name)}`));
1209
+ }
1210
+ // Renames ----------------------------------------------------------------------------------------
1211
+ function renameEntity(model, from, to) {
1212
+ // Kept storage follows too: a kept join table's key column is named after its target.
1213
+ return model.map((stored) => ({
1214
+ ...stored,
1215
+ entity: {
1216
+ ...stored.entity,
1217
+ name: stored.entity.name === from ? to : stored.entity.name,
1218
+ relationships: stored.entity.relationships.map((relationship) => relationship.to === from ? { ...relationship, to } : relationship),
1219
+ },
1220
+ }));
1221
+ }
1222
+ function renameProperty(model, entityId, from, to) {
1223
+ return model.map((stored) => {
1224
+ if (stored.entity.id !== entityId || stored.removed) {
1225
+ return stored;
1226
+ }
1227
+ const { entity } = stored;
1228
+ return {
1229
+ ...stored,
1230
+ entity: {
1231
+ ...entity,
1232
+ properties: entity.properties.map((property) => property.name === from && !stored.kept.properties.has(from)
1233
+ ? { ...property, name: to }
1234
+ : property),
1235
+ indexes: entity.indexes.map((index) => ({
1236
+ ...index,
1237
+ properties: index.properties.map((field) => (field === from ? to : field)),
1238
+ })),
1239
+ },
1240
+ };
1241
+ });
1242
+ }
1243
+ function renameStatement(before, after) {
1244
+ const from = quoteIdentifier(before.name);
1245
+ const to = quoteIdentifier(after.name);
1246
+ switch (before.kind) {
1247
+ case "table":
1248
+ return `ALTER TABLE ${from} RENAME TO ${to}`;
1249
+ case "type":
1250
+ return `ALTER TYPE ${from} RENAME TO ${to}`;
1251
+ case "index":
1252
+ // Renaming a primary key's index renames the constraint too.
1253
+ return `ALTER INDEX ${from} RENAME TO ${to}`;
1254
+ case "column":
1255
+ return alterTable(after.table, `RENAME COLUMN ${from} TO ${to}`);
1256
+ case "constraint":
1257
+ return alterTable(after.table, `RENAME CONSTRAINT ${from} TO ${to}`);
1258
+ }
1259
+ }
1260
+ /**
1261
+ * The statements that take the objects of one model to the objects of the same model renamed. Within a
1262
+ * namespace, a rename waits until its new name is free; a cycle cannot be ordered and is undefined.
1263
+ */
1264
+ function renameStatements(before, after) {
1265
+ if (before.length !== after.length) {
1266
+ throw new Error("A rename changed the shape of the storage model.");
1267
+ }
1268
+ const occupied = new Map();
1269
+ for (const object of before) {
1270
+ const scope = scopeOf(object);
1271
+ const names = occupied.get(scope) ?? new Set();
1272
+ names.add(object.name);
1273
+ occupied.set(scope, names);
1274
+ }
1275
+ const renames = before
1276
+ .map((object, position) => ({ before: object, after: after[position] }))
1277
+ .filter((pair) => pair.before.name !== pair.after.name);
1278
+ // Tables first, so that column and constraint renames address tables by their new names.
1279
+ const groups = [
1280
+ renames.filter((pair) => GLOBAL_KINDS.has(pair.before.kind)),
1281
+ renames.filter((pair) => pair.before.kind === "column"),
1282
+ renames.filter((pair) => pair.before.kind === "constraint"),
1283
+ ];
1284
+ const statements = [];
1285
+ for (const group of groups) {
1286
+ const pending = [...group];
1287
+ while (pending.length > 0) {
1288
+ const ready = pending.findIndex((pair) => !occupied.get(scopeOf(pair.before))?.has(pair.after.name));
1289
+ if (ready === -1) {
1290
+ return undefined;
1291
+ }
1292
+ const [pair] = pending.splice(ready, 1);
1293
+ if (pair === undefined) {
1294
+ break;
1295
+ }
1296
+ const names = occupied.get(scopeOf(pair.before));
1297
+ names.delete(pair.before.name);
1298
+ names.add(pair.after.name);
1299
+ statements.push(renameStatement(pair.before, pair.after));
1300
+ }
1301
+ }
1302
+ return statements;
1303
+ }
1304
+ /** Orders renames so that each new name is free when its rename runs; undefined on a cycle. */
1305
+ function orderRenames(renames, names) {
1306
+ const occupied = new Set(names);
1307
+ const pending = [...renames];
1308
+ const ordered = [];
1309
+ while (pending.length > 0) {
1310
+ const ready = pending.findIndex((rename) => !occupied.has(rename.to));
1311
+ if (ready === -1) {
1312
+ return undefined;
1313
+ }
1314
+ const [rename] = pending.splice(ready, 1);
1315
+ occupied.delete(rename.from);
1316
+ occupied.add(rename.to);
1317
+ ordered.push(rename);
1318
+ }
1319
+ return ordered;
1320
+ }
1321
+ function logicalRenames(pairs, refusals) {
1322
+ const entityRenames = orderRenames(pairs
1323
+ .filter((pair) => pair.current.name !== pair.target.name)
1324
+ .map((pair) => ({ from: pair.current.name, to: pair.target.name })), pairs.map((pair) => pair.current.name));
1325
+ if (entityRenames === undefined) {
1326
+ refusals.push(refusal("rename-conflict", pairs.map((pair) => pair.target.name).join(", "), "entity renames swap names in a cycle; deploy them in separate steps."));
1327
+ return [];
1328
+ }
1329
+ const renames = entityRenames.map(({ from, to }) => ({
1330
+ kind: "renameEntity",
1331
+ path: to,
1332
+ description: `Rename entity ${from} to ${to}, with its history, enum types, indexes and join tables.`,
1333
+ apply: (model) => renameEntity(model, from, to),
1334
+ }));
1335
+ for (const pair of pairs) {
1336
+ const propertyRenames = orderRenames(pair.properties
1337
+ .filter((property) => property.current.name !== property.target.name)
1338
+ .map((property) => ({ from: property.current.name, to: property.target.name })), pair.properties.map((property) => property.current.name));
1339
+ if (propertyRenames === undefined) {
1340
+ refusals.push(refusal("rename-conflict", pair.target.name, "property renames swap names in a cycle; deploy them in separate steps."));
1341
+ continue;
1342
+ }
1343
+ for (const { from, to } of propertyRenames) {
1344
+ renames.push({
1345
+ kind: "renameProperty",
1346
+ path: `${pair.target.name}.properties.${to}`,
1347
+ description: `Rename property ${pair.target.name}.${from} to ${to}.`,
1348
+ apply: (model) => renameProperty(model, pair.target.id, from, to),
1349
+ ...(pair.current.properties.some((property) => property.name === from && property.type === "blob")
1350
+ ? {
1351
+ metadataSql: ["_blobs", "_blob_uploads", "_blob_references"].map((table) => `UPDATE ${quoteIdentifier(table)} SET property_id = ${quoteLiteral(to)} ` +
1352
+ `WHERE entity_id = ${quoteLiteral(pair.target.id)} AND property_id = ${quoteLiteral(from)}`),
1353
+ }
1354
+ : {}),
1355
+ });
1356
+ }
1357
+ }
1358
+ return renames;
1359
+ }
1360
+ /**
1361
+ * Plans the renames over everything the database stores once the drops ran, kept storage included,
1362
+ * and returns that model with every target name in place.
1363
+ */
1364
+ function planRenames(input) {
1365
+ const { pairs, draft } = input;
1366
+ let model = [...input.model];
1367
+ for (const rename of logicalRenames(pairs, draft.refusals)) {
1368
+ const next = rename.apply(model);
1369
+ const found = collisions(storageObjects(next));
1370
+ // A name kept storage holds changes which objects exist, so pair them only without collisions.
1371
+ const statements = found.length > 0 ? undefined : renameStatements(storageObjects(model), storageObjects(next));
1372
+ if (found.length > 0 || statements === undefined) {
1373
+ const kept = keptCollisionRefusals(found);
1374
+ draft.refusals.push(...(kept.length > 0
1375
+ ? kept
1376
+ : [
1377
+ refusal("rename-conflict", rename.path, `${rename.description.replace(/\.$/, "")} cannot run in this deploy: a name it ` +
1378
+ "takes is still in use. Deploy the renames in separate steps."),
1379
+ ]));
1380
+ return model;
1381
+ }
1382
+ draft.renames.push(step({
1383
+ kind: rename.kind,
1384
+ classification: "rename",
1385
+ description: rename.description,
1386
+ sql: [...statements, ...(rename.metadataSql ?? [])],
1387
+ }));
1388
+ model = next;
1389
+ }
1390
+ return model;
1391
+ }
1392
+ // Changes to surviving entities ------------------------------------------------------------------
1393
+ function isSubsequence(values, of) {
1394
+ let position = 0;
1395
+ for (const value of of) {
1396
+ if (value === values[position]) {
1397
+ position += 1;
1398
+ }
1399
+ }
1400
+ return position === values.length;
1401
+ }
1402
+ function sameIndex(left, right) {
1403
+ return (left.table === right.table &&
1404
+ left.unique === right.unique &&
1405
+ left.columns.length === right.columns.length &&
1406
+ left.columns.every((column, position) => column === right.columns[position]));
1407
+ }
1408
+ function optionValues(property) {
1409
+ return (property.options ?? []).map((option) => option.value);
1410
+ }
1411
+ /** Plans option changes; returns true when the column was rebuilt with the target default. */
1412
+ function planSelectChange(input) {
1413
+ const { entity, before, after, draft } = input;
1414
+ const path = `${entity}.properties.${after.name}`;
1415
+ const currentValues = optionValues(before);
1416
+ const targetValues = optionValues(after);
1417
+ if (currentValues.length === targetValues.length &&
1418
+ currentValues.every((value, position) => value === targetValues[position])) {
1419
+ return false;
1420
+ }
1421
+ const removed = currentValues.filter((value) => !targetValues.includes(value));
1422
+ if (removed.length === 0 && isSubsequence(currentValues, targetValues)) {
1423
+ // ALTER TYPE … ADD VALUE commits before the transaction, which may then use the new values.
1424
+ const type = quoteIdentifier(enumTypeName(input.appliedEntity, input.appliedProperty));
1425
+ const added = targetValues.filter((value) => !currentValues.includes(value));
1426
+ const firstKept = targetValues.find((value) => currentValues.includes(value));
1427
+ const statements = targetValues.flatMap((value, position) => {
1428
+ if (currentValues.includes(value)) {
1429
+ return [];
1430
+ }
1431
+ const placement = position === 0
1432
+ ? `BEFORE ${quoteLiteral(firstKept)}`
1433
+ : `AFTER ${quoteLiteral(targetValues[position - 1])}`;
1434
+ return [`ALTER TYPE ${type} ADD VALUE IF NOT EXISTS ${quoteLiteral(value)} ${placement}`];
1435
+ });
1436
+ draft.before.push(step({
1437
+ kind: "addSelectOption",
1438
+ classification: "additive",
1439
+ transactional: false,
1440
+ description: `Add ${added.map((value) => `"${value}"`).join(", ")} to the options of ${entity}.${after.name}.`,
1441
+ sql: statements,
1442
+ }));
1443
+ return false;
1444
+ }
1445
+ // Postgres cannot reorder or drop enum values: build the type again and convert the column.
1446
+ const table = quoteIdentifier(tableName(entity));
1447
+ const column = quoteIdentifier(propertyColumn(entity, after).name);
1448
+ const type = quoteIdentifier(enumTypeName(entity, after.name));
1449
+ const temporary = quoteIdentifier(`_ontology_rebuild_${draft.rebuilds}`);
1450
+ const currentDefault = defaultExpression(before);
1451
+ const targetDefault = defaultExpression(after);
1452
+ const reason = removed.length > 0
1453
+ ? `drops ${removed.map((value) => `"${value}"`).join(", ")}`
1454
+ : "changes their order";
1455
+ draft.rebuilds += 1;
1456
+ draft.changes.push(step({
1457
+ kind: "rebuildSelect",
1458
+ classification: removed.length > 0 && input.allowDrops ? "destructive" : "change",
1459
+ validatesData: removed.length > 0,
1460
+ description: `Rebuild the options of ${entity}.${after.name} (${reason}).`,
1461
+ sql: [
1462
+ `CREATE TYPE ${temporary} AS ENUM (${targetValues.map(quoteLiteral).join(", ")})`,
1463
+ ...(currentDefault === undefined
1464
+ ? []
1465
+ : [`ALTER TABLE ${table} ALTER COLUMN ${column} DROP DEFAULT`]),
1466
+ `ALTER TABLE ${table} ALTER COLUMN ${column} TYPE ${temporary} USING ${column}::text::${temporary}`,
1467
+ `DROP TYPE ${type}`,
1468
+ `ALTER TYPE ${temporary} RENAME TO ${type}`,
1469
+ ...(targetDefault === undefined
1470
+ ? []
1471
+ : [`ALTER TABLE ${table} ALTER COLUMN ${column} SET DEFAULT ${targetDefault}`]),
1472
+ ],
1473
+ }));
1474
+ return true;
1475
+ }
1476
+ function planPropertyChange(input) {
1477
+ const { entity, before, after, draft } = input;
1478
+ const path = `${entity}.properties.${after.name}`;
1479
+ const table = tableName(entity);
1480
+ const column = quoteIdentifier(propertyColumn(entity, after).name);
1481
+ if (before.type !== after.type) {
1482
+ draft.refusals.push(refusal("type-change", path, input.restored
1483
+ ? `was a ${before.type} when it was removed, and the database keeps its values; they ` +
1484
+ `are not converted to ${after.type}. Declare it as a ${before.type} to restore it, ` +
1485
+ "or choose another name."
1486
+ : `changes type from ${before.type} to ${after.type}. Stored values are not converted; ` +
1487
+ "add a property with the new type and move the values with an action instead."));
1488
+ return;
1489
+ }
1490
+ const rebuilt = after.type === "select" ? planSelectChange(input) : false;
1491
+ if (before.optional !== after.optional) {
1492
+ draft.changes.push(step({
1493
+ kind: "changeOptional",
1494
+ classification: "change",
1495
+ validatesData: !after.optional,
1496
+ description: after.optional
1497
+ ? `Make ${entity}.${after.name} optional.`
1498
+ : `Make ${entity}.${after.name} required; fails while any record has no value.`,
1499
+ sql: [
1500
+ alterTable(table, `ALTER COLUMN ${column} ${after.optional ? "DROP" : "SET"} NOT NULL`),
1501
+ ],
1502
+ }));
1503
+ }
1504
+ const currentDefault = defaultExpression(before);
1505
+ const targetDefault = defaultExpression(after);
1506
+ if (!rebuilt && currentDefault !== targetDefault) {
1507
+ draft.changes.push(step({
1508
+ kind: "changeDefault",
1509
+ classification: "change",
1510
+ description: targetDefault === undefined
1511
+ ? `Remove the default of ${entity}.${after.name}.`
1512
+ : `Change the default of ${entity}.${after.name} to ${targetDefault}.`,
1513
+ sql: [
1514
+ alterTable(table, targetDefault === undefined
1515
+ ? `ALTER COLUMN ${column} DROP DEFAULT`
1516
+ : `ALTER COLUMN ${column} SET DEFAULT ${targetDefault}`),
1517
+ ],
1518
+ }));
1519
+ }
1520
+ const currentCheck = propertyCheck(entity, before);
1521
+ const targetCheck = propertyCheck(entity, after);
1522
+ if (currentCheck?.definition !== targetCheck?.definition) {
1523
+ draft.changes.push(step({
1524
+ kind: "changeCheck",
1525
+ classification: "change",
1526
+ validatesData: targetCheck !== undefined,
1527
+ description: targetCheck === undefined
1528
+ ? `Remove the bounds of ${entity}.${after.name}.`
1529
+ : `Check the bounds of ${entity}.${after.name}; fails while any record is out of bounds.`,
1530
+ sql: [
1531
+ ...(currentCheck === undefined
1532
+ ? []
1533
+ : [alterTable(table, `DROP CONSTRAINT ${quoteIdentifier(currentCheck.name)}`)]),
1534
+ ...(targetCheck === undefined ? [] : [addConstraintStatement(table, targetCheck)]),
1535
+ ],
1536
+ }));
1537
+ }
1538
+ }
1539
+ function planRelationshipChange(input) {
1540
+ const { entity, before, after, draft } = input;
1541
+ const table = tableName(entity);
1542
+ if (after.cardinality !== "one") {
1543
+ return;
1544
+ }
1545
+ if ((before.optional === true) !== (after.optional === true)) {
1546
+ const column = quoteIdentifier(relationshipColumn(after).name);
1547
+ draft.changes.push(step({
1548
+ kind: "changeOptional",
1549
+ classification: "change",
1550
+ validatesData: after.optional !== true,
1551
+ description: after.optional === true
1552
+ ? `Make relationship ${entity}.${after.name} optional.`
1553
+ : `Make relationship ${entity}.${after.name} required; fails while any record has none.`,
1554
+ sql: [
1555
+ alterTable(table, `ALTER COLUMN ${column} ${after.optional === true ? "DROP" : "SET"} NOT NULL`),
1556
+ ],
1557
+ }));
1558
+ }
1559
+ const currentKey = relationshipForeignKey(entity, before);
1560
+ const targetKey = relationshipForeignKey(entity, after);
1561
+ if (input.attached && currentKey.definition !== targetKey.definition) {
1562
+ draft.changes.push(step({
1563
+ kind: "changeOnDelete",
1564
+ classification: "change",
1565
+ description: `Change onDelete of ${entity}.${after.name} to ${after.onDelete}.`,
1566
+ sql: [
1567
+ alterTable(table, `DROP CONSTRAINT ${quoteIdentifier(currentKey.name)}`),
1568
+ addConstraintStatement(table, targetKey),
1569
+ ],
1570
+ }));
1571
+ }
1572
+ }
1573
+ /** Columns that exist before the migration with the same name and meaning: safe to index concurrently. */
1574
+ function stableColumns(pair) {
1575
+ if (pair.current.name !== pair.target.name) {
1576
+ return new Set();
1577
+ }
1578
+ return new Set([
1579
+ ...Object.values(SYSTEM_COLUMNS),
1580
+ ...pair.properties
1581
+ .filter((property) => property.current.name === property.target.name)
1582
+ .map((property) => propertyColumn(pair.target.name, property.target).name),
1583
+ ...pair.relationships
1584
+ .filter((relationship) => relationship.target.cardinality === "one")
1585
+ .map((relationship) => relationshipColumn(relationship.target).name),
1586
+ ]);
1587
+ }
1588
+ function planIndexChanges(input) {
1589
+ const { pair, before, appliedNames, draft } = input;
1590
+ // Kept fields have no indexes: a restored field's are built again.
1591
+ const currentStorage = physicalStorage(before).table;
1592
+ const currentIndexes = currentStorage.indexes;
1593
+ // Indexes over a refused member (a relationship whose target changed) wait for that refusal.
1594
+ const known = new Set([
1595
+ ...currentStorage.columns.map((column) => column.name),
1596
+ ...pair.addedProperties.map((property) => propertyColumn(pair.target.name, property).name),
1597
+ ...pair.addedRelationships
1598
+ .filter((relationship) => relationship.cardinality === "one")
1599
+ .map((relationship) => relationshipColumn(relationship).name),
1600
+ ]);
1601
+ const targetIndexes = entityStorage(pair.target).table.indexes.filter((index) => index.columns.every((column) => known.has(column)));
1602
+ const stable = stableColumns(pair);
1603
+ for (const index of currentIndexes) {
1604
+ const replacement = targetIndexes.find((candidate) => candidate.name === index.name);
1605
+ if (replacement !== undefined && sameIndex(index, replacement)) {
1606
+ continue;
1607
+ }
1608
+ draft.indexDrops.push(step({
1609
+ kind: "dropIndex",
1610
+ classification: "change",
1611
+ description: `Drop index ${index.name} (${index.origin}).`,
1612
+ sql: [`DROP INDEX ${quoteIdentifier(index.name)}`],
1613
+ }));
1614
+ }
1615
+ for (const index of targetIndexes) {
1616
+ const existing = currentIndexes.find((candidate) => candidate.name === index.name);
1617
+ if (existing !== undefined && sameIndex(existing, index)) {
1618
+ continue;
1619
+ }
1620
+ const concurrent = existing === undefined &&
1621
+ !appliedNames.has(index.name) &&
1622
+ index.columns.every((column) => stable.has(column));
1623
+ const description = `Add ${index.unique ? "unique " : ""}index ${index.name} (${index.origin}).`;
1624
+ if (concurrent) {
1625
+ // A failed concurrent build leaves an invalid index behind; dropping it first makes a rerun work.
1626
+ draft.before.push(step({
1627
+ kind: "addIndex",
1628
+ classification: "additive",
1629
+ transactional: false,
1630
+ validatesData: index.unique,
1631
+ description,
1632
+ sql: [
1633
+ `DROP INDEX CONCURRENTLY IF EXISTS ${quoteIdentifier(index.name)}`,
1634
+ createIndexStatement(index, { concurrently: true }),
1635
+ ],
1636
+ }));
1637
+ continue;
1638
+ }
1639
+ draft.indexes.push(step({
1640
+ kind: "addIndex",
1641
+ classification: "additive",
1642
+ validatesData: index.unique,
1643
+ description,
1644
+ sql: [createIndexStatement(index)],
1645
+ }));
1646
+ }
1647
+ }
1648
+ /**
1649
+ * Restores the foreign keys a restored entity or field lost when it was kept, under the target's
1650
+ * names. Each validates the kept rows: they may refer to records deleted meanwhile.
1651
+ */
1652
+ function planRestoredForeignKeys(input) {
1653
+ const { pair, before, draft } = input;
1654
+ const declaredStorage = entityStorage({
1655
+ ...pair.target,
1656
+ properties: pair.properties.map(({ target }) => target),
1657
+ relationships: pair.relationships.map(({ target }) => target),
1658
+ indexes: [],
1659
+ });
1660
+ const physical = physicalStorage(before);
1661
+ const present = new Set([physical.table, ...physical.joins].flatMap((table) => table.constraints.map((constraint) => constraint.name)));
1662
+ for (const table of [declaredStorage.table, ...declaredStorage.joins]) {
1663
+ for (const constraint of table.constraints) {
1664
+ if (constraint.kind !== "foreignKey" || present.has(constraint.name)) {
1665
+ continue;
1666
+ }
1667
+ draft.foreignKeys.push(step({
1668
+ kind: "addForeignKey",
1669
+ classification: "additive",
1670
+ validatesData: true,
1671
+ description: `Restore foreign key ${constraint.name} of ${table.name}; fails while a kept row ` +
1672
+ `refers to a ${constraint.references === USER_TABLE ? "user" : "record"} deleted since.`,
1673
+ sql: [addConstraintStatement(table.name, constraint)],
1674
+ }));
1675
+ }
1676
+ }
1677
+ }
1678
+ function planEntityChanges(input) {
1679
+ const { pair, renamed, appliedNames, allowDrops, draft } = input;
1680
+ const stored = renamed.find((entity) => entity.entity.id === pair.target.id && !entity.removed);
1681
+ // A refused rename leaves the entity under its applied name; the plan cannot be applied anyway.
1682
+ if (stored === undefined || stored.entity.name !== pair.target.name) {
1683
+ return;
1684
+ }
1685
+ const before = stored.entity;
1686
+ const entity = pair.target.name;
1687
+ const table = tableName(entity);
1688
+ if (pair.restored) {
1689
+ draft.changes.push(step({
1690
+ kind: "restoreEntity",
1691
+ classification: "change",
1692
+ description: `Restore entity ${entity} with the records and history its storage kept.`,
1693
+ sql: [],
1694
+ }));
1695
+ }
1696
+ // Kept fields that stay kept share the model; a refused rename must not meet one of them.
1697
+ const active = (kind, name) => !stored.kept[kind].has(name) || stored.restoring[kind].has(name);
1698
+ for (const property of pair.properties) {
1699
+ const renamedProperty = before.properties.find(({ name }) => name === property.target.name && active("properties", name));
1700
+ if (renamedProperty === undefined) {
1701
+ continue;
1702
+ }
1703
+ const restored = pair.restoredFields.properties.has(property.target.name);
1704
+ if (restored) {
1705
+ draft.changes.push(step({
1706
+ kind: "restoreProperty",
1707
+ classification: "change",
1708
+ description: `Restore property ${entity}.${property.target.name} with the values its column kept.`,
1709
+ sql: [],
1710
+ }));
1711
+ }
1712
+ planPropertyChange({
1713
+ entity,
1714
+ appliedEntity: pair.current.name,
1715
+ appliedProperty: property.current.name,
1716
+ before: renamedProperty,
1717
+ after: property.target,
1718
+ restored,
1719
+ allowDrops,
1720
+ draft,
1721
+ });
1722
+ }
1723
+ for (const relationship of pair.relationships) {
1724
+ const renamedRelationship = before.relationships.find(({ name }) => name === relationship.target.name && active("relationships", name));
1725
+ if (renamedRelationship === undefined) {
1726
+ continue;
1727
+ }
1728
+ const restored = pair.restoredFields.relationships.has(relationship.target.name);
1729
+ if (restored) {
1730
+ draft.changes.push(step({
1731
+ kind: "restoreRelationship",
1732
+ classification: "change",
1733
+ description: `Restore relationship ${entity}.${relationship.target.name} with the links its storage kept.`,
1734
+ sql: [],
1735
+ }));
1736
+ }
1737
+ planRelationshipChange({
1738
+ entity,
1739
+ before: renamedRelationship,
1740
+ after: relationship.target,
1741
+ attached: !pair.restored && !restored,
1742
+ draft,
1743
+ });
1744
+ }
1745
+ for (const property of pair.addedProperties) {
1746
+ if (property.type === "blob") {
1747
+ draft.foreignKeys.push(step({
1748
+ kind: "addProperty",
1749
+ classification: "additive",
1750
+ description: `Reference the file revision for ${entity}.${property.name}.`,
1751
+ sql: [addConstraintStatement(table, blobForeignKey(entity, property))],
1752
+ }));
1753
+ }
1754
+ const enumStorage = propertyEnum(entity, property);
1755
+ const check = propertyCheck(entity, property);
1756
+ const column = propertyColumn(entity, property);
1757
+ const actions = [
1758
+ `ADD COLUMN ${columnDefinition(column)}`,
1759
+ ...(check === undefined ? [] : [`ADD ${constraintDefinition(check)}`]),
1760
+ ];
1761
+ draft.additions.push(step({
1762
+ kind: "addProperty",
1763
+ classification: "additive",
1764
+ validatesData: column.notNull && column.default === undefined,
1765
+ description: `Add property ${entity}.${property.name}.`,
1766
+ sql: [
1767
+ ...(enumStorage === undefined ? [] : [createEnumStatement(enumStorage)]),
1768
+ alterTable(table, actions.join(", ")),
1769
+ ],
1770
+ }));
1771
+ }
1772
+ for (const relationship of pair.addedRelationships) {
1773
+ if (relationship.cardinality !== "one") {
1774
+ continue;
1775
+ }
1776
+ const column = relationshipColumn(relationship);
1777
+ const key = relationshipForeignKey(entity, relationship);
1778
+ draft.additions.push(step({
1779
+ kind: "addRelationship",
1780
+ classification: "additive",
1781
+ validatesData: column.notNull,
1782
+ description: `Add relationship ${entity}.${relationship.name} to ${relationship.to}.`,
1783
+ sql: [
1784
+ alterTable(table, `ADD COLUMN ${columnDefinition(column)}, ADD ${constraintDefinition(key)}`),
1785
+ ],
1786
+ }));
1787
+ }
1788
+ planIndexChanges({ pair, before: stored, appliedNames, draft });
1789
+ if (pair.restored ||
1790
+ pair.restoredFields.properties.size + pair.restoredFields.relationships.size > 0) {
1791
+ planRestoredForeignKeys({ pair, before: stored, draft });
1792
+ }
1793
+ }
1794
+ // New entities and join tables -------------------------------------------------------------------
1795
+ function planNewEntities(input) {
1796
+ const { draft } = input;
1797
+ const available = new Set(input.pairs.map((pair) => tableName(pair.target.name)));
1798
+ const remaining = [...input.addedEntities];
1799
+ while (remaining.length > 0) {
1800
+ // Create an entity once every table it references exists; break a cycle at the first entity.
1801
+ const ready = remaining.findIndex((candidate) => entityStorage(candidate).table.constraints.every((constraint) => constraint.references === undefined ||
1802
+ constraint.references === tableName(candidate.name) ||
1803
+ available.has(constraint.references)));
1804
+ const [entity] = remaining.splice(ready === -1 ? 0 : ready, 1);
1805
+ const storage = entityStorage(entity);
1806
+ const deferred = storage.table.constraints.filter((constraint) => constraint.references !== undefined &&
1807
+ constraint.references !== storage.table.name &&
1808
+ !available.has(constraint.references));
1809
+ const inline = storage.table.constraints.filter((constraint) => !deferred.includes(constraint));
1810
+ draft.creates.push(step({
1811
+ kind: "createEntity",
1812
+ classification: "additive",
1813
+ description: `Create entity ${entity.name}: table ${storage.table.name} and history ${storage.history.name}.`,
1814
+ sql: [
1815
+ ...storage.enums.map(createEnumStatement),
1816
+ createTableStatement(storage.table, inline),
1817
+ createTableStatement(storage.history),
1818
+ ...storage.table.indexes.map((index) => createIndexStatement(index)),
1819
+ ...storage.history.indexes.map((index) => createIndexStatement(index)),
1820
+ ],
1821
+ }));
1822
+ for (const constraint of deferred) {
1823
+ draft.foreignKeys.push(step({
1824
+ kind: "addForeignKey",
1825
+ classification: "additive",
1826
+ description: `Add foreign key ${constraint.name} once ${constraint.references} exists.`,
1827
+ sql: [addConstraintStatement(storage.table.name, constraint)],
1828
+ }));
1829
+ }
1830
+ available.add(storage.table.name);
1831
+ }
1832
+ }
1833
+ function planJoinTables(input) {
1834
+ const added = [
1835
+ ...input.pairs.map((pair) => ({ entity: pair.target, relationships: pair.addedRelationships })),
1836
+ ...input.addedEntities.map((entity) => ({ entity, relationships: entity.relationships })),
1837
+ ];
1838
+ for (const { entity, relationships } of added) {
1839
+ for (const relationship of relationships) {
1840
+ if (relationship.cardinality !== "many") {
1841
+ continue;
1842
+ }
1843
+ const join = joinTable(entity.name, relationship);
1844
+ const name = `${entity.name}.${relationship.name}`;
1845
+ input.draft.joins.push(step({
1846
+ kind: "addRelationship",
1847
+ classification: "additive",
1848
+ description: `Add relationship ${name} to ${relationship.to}: join table ${join.name}.`,
1849
+ sql: [
1850
+ createTableStatement(join),
1851
+ ...join.indexes.map((index) => createIndexStatement(index)),
1852
+ ],
1853
+ }));
1854
+ }
1855
+ }
1856
+ }