@harperfast/harper 5.2.13 → 5.3.0-beta.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 (655) hide show
  1. package/agent/mcpTools.ts +1 -1
  2. package/agent/session.ts +25 -14
  3. package/bin/cliOperations.ts +58 -9
  4. package/bin/copyDb.ts +282 -59
  5. package/bin/deploySetup.ts +16 -5
  6. package/bin/harper.ts +1 -1
  7. package/bin/help.ts +4 -1
  8. package/bin/lite.ts +4 -1
  9. package/bin/restart.ts +120 -4
  10. package/bin/run.ts +6 -11
  11. package/bin/upgrade.js +7 -3
  12. package/bin/workloadIdentity.ts +119 -0
  13. package/components/Application.ts +3436 -242
  14. package/components/ApplicationScope.ts +8 -0
  15. package/components/EntryHandler.ts +59 -39
  16. package/components/OptionsWatcher.ts +440 -98
  17. package/components/RuntimeModuleTracker.ts +38 -7
  18. package/components/Scope.ts +56 -15
  19. package/components/awaitRestart.ts +84 -0
  20. package/components/componentLoader.ts +381 -32
  21. package/components/componentPreparationLock.ts +16 -5
  22. package/components/deploymentOperations.ts +4 -1
  23. package/components/deploymentRecorder.ts +9 -2
  24. package/components/mcp/adapters/harperHttp.ts +4 -0
  25. package/components/mcp/listChanged.ts +4 -0
  26. package/components/mcp/toolRegistry.ts +2 -0
  27. package/components/mcp/tools/operations.ts +9 -0
  28. package/components/mcp/tools/schemas/operationDescriptions.ts +2 -2
  29. package/components/operations.js +537 -113
  30. package/components/operationsValidation.js +98 -3
  31. package/components/packageComponent.ts +25 -1
  32. package/components/requestRestart.ts +11 -0
  33. package/components/status/ComponentStatusRegistry.ts +59 -0
  34. package/config/RootConfigWatcher.ts +240 -40
  35. package/config/configReadRetry.ts +62 -0
  36. package/config/configUtils.ts +357 -48
  37. package/config/harperConfigEnvVars.ts +170 -27
  38. package/config/parseConfigFile.ts +34 -0
  39. package/config/readConfigFileSync.ts +44 -0
  40. package/config/watcherArming.ts +59 -0
  41. package/config-root.schema.json +33 -0
  42. package/dataLayer/blobBackup.ts +160 -50
  43. package/dataLayer/delete.ts +6 -1
  44. package/dataLayer/harperBridge/ResourceBridge.ts +80 -10
  45. package/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js +3 -1
  46. package/dataLayer/hdbInfoController.ts +34 -1
  47. package/dataLayer/insert.ts +44 -1
  48. package/dataLayer/rocksdbBackup.ts +53 -10
  49. package/dataLayer/schema.ts +11 -1
  50. package/dataLayer/schemaDescribe.ts +8 -1
  51. package/dist/agent/mcpTools.js +1 -1
  52. package/dist/agent/mcpTools.js.map +1 -1
  53. package/dist/agent/session.d.ts +22 -0
  54. package/dist/agent/session.js +26 -15
  55. package/dist/agent/session.js.map +1 -1
  56. package/dist/bin/cliOperations.js +61 -9
  57. package/dist/bin/cliOperations.js.map +1 -1
  58. package/dist/bin/copyDb.d.ts +12 -1
  59. package/dist/bin/copyDb.js +248 -60
  60. package/dist/bin/copyDb.js.map +1 -1
  61. package/dist/bin/deploySetup.d.ts +2 -0
  62. package/dist/bin/deploySetup.js +11 -3
  63. package/dist/bin/deploySetup.js.map +1 -1
  64. package/dist/bin/harper.js +1 -1
  65. package/dist/bin/harper.js.map +1 -1
  66. package/dist/bin/help.js +4 -1
  67. package/dist/bin/help.js.map +1 -1
  68. package/dist/bin/lite.js +4 -1
  69. package/dist/bin/lite.js.map +1 -1
  70. package/dist/bin/restart.js +91 -6
  71. package/dist/bin/restart.js.map +1 -1
  72. package/dist/bin/run.js +4 -10
  73. package/dist/bin/run.js.map +1 -1
  74. package/dist/bin/upgrade.js +4 -3
  75. package/dist/bin/upgrade.js.map +1 -1
  76. package/dist/bin/workloadIdentity.d.ts +18 -0
  77. package/dist/bin/workloadIdentity.js +100 -0
  78. package/dist/bin/workloadIdentity.js.map +1 -0
  79. package/dist/components/Application.d.ts +235 -17
  80. package/dist/components/Application.js +3038 -238
  81. package/dist/components/Application.js.map +1 -1
  82. package/dist/components/ApplicationScope.d.ts +8 -0
  83. package/dist/components/ApplicationScope.js +7 -0
  84. package/dist/components/ApplicationScope.js.map +1 -1
  85. package/dist/components/EntryHandler.js +26 -10
  86. package/dist/components/EntryHandler.js.map +1 -1
  87. package/dist/components/OptionsWatcher.d.ts +4 -0
  88. package/dist/components/OptionsWatcher.js +440 -99
  89. package/dist/components/OptionsWatcher.js.map +1 -1
  90. package/dist/components/RuntimeModuleTracker.js +40 -6
  91. package/dist/components/RuntimeModuleTracker.js.map +1 -1
  92. package/dist/components/Scope.js +52 -13
  93. package/dist/components/Scope.js.map +1 -1
  94. package/dist/components/awaitRestart.d.ts +33 -0
  95. package/dist/components/awaitRestart.js +61 -0
  96. package/dist/components/awaitRestart.js.map +1 -0
  97. package/dist/components/componentLoader.d.ts +38 -1
  98. package/dist/components/componentLoader.js +313 -24
  99. package/dist/components/componentLoader.js.map +1 -1
  100. package/dist/components/componentPreparationLock.d.ts +5 -0
  101. package/dist/components/componentPreparationLock.js +14 -6
  102. package/dist/components/componentPreparationLock.js.map +1 -1
  103. package/dist/components/deploymentOperations.js +4 -1
  104. package/dist/components/deploymentOperations.js.map +1 -1
  105. package/dist/components/deploymentRecorder.d.ts +4 -2
  106. package/dist/components/deploymentRecorder.js +1 -0
  107. package/dist/components/deploymentRecorder.js.map +1 -1
  108. package/dist/components/mcp/adapters/harperHttp.js +4 -0
  109. package/dist/components/mcp/adapters/harperHttp.js.map +1 -1
  110. package/dist/components/mcp/listChanged.js +5 -0
  111. package/dist/components/mcp/listChanged.js.map +1 -1
  112. package/dist/components/mcp/toolRegistry.d.ts +1 -0
  113. package/dist/components/mcp/toolRegistry.js.map +1 -1
  114. package/dist/components/mcp/tools/operations.d.ts +5 -0
  115. package/dist/components/mcp/tools/operations.js +9 -0
  116. package/dist/components/mcp/tools/operations.js.map +1 -1
  117. package/dist/components/mcp/tools/schemas/operationDescriptions.js +2 -2
  118. package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
  119. package/dist/components/operations.d.ts +28 -0
  120. package/dist/components/operations.js +476 -113
  121. package/dist/components/operations.js.map +1 -1
  122. package/dist/components/operationsValidation.js +97 -3
  123. package/dist/components/operationsValidation.js.map +1 -1
  124. package/dist/components/packageComponent.js +24 -0
  125. package/dist/components/packageComponent.js.map +1 -1
  126. package/dist/components/requestRestart.d.ts +1 -0
  127. package/dist/components/requestRestart.js +7 -0
  128. package/dist/components/requestRestart.js.map +1 -1
  129. package/dist/components/status/ComponentStatusRegistry.d.ts +0 -4
  130. package/dist/components/status/ComponentStatusRegistry.js +63 -0
  131. package/dist/components/status/ComponentStatusRegistry.js.map +1 -1
  132. package/dist/config/RootConfigWatcher.d.ts +9 -1
  133. package/dist/config/RootConfigWatcher.js +228 -37
  134. package/dist/config/RootConfigWatcher.js.map +1 -1
  135. package/dist/config/configReadRetry.d.ts +8 -0
  136. package/dist/config/configReadRetry.js +62 -0
  137. package/dist/config/configReadRetry.js.map +1 -0
  138. package/dist/config/configUtils.d.ts +11 -2
  139. package/dist/config/configUtils.js +304 -47
  140. package/dist/config/configUtils.js.map +1 -1
  141. package/dist/config/harperConfigEnvVars.d.ts +16 -0
  142. package/dist/config/harperConfigEnvVars.js +162 -25
  143. package/dist/config/harperConfigEnvVars.js.map +1 -1
  144. package/dist/config/parseConfigFile.d.ts +4 -0
  145. package/dist/config/parseConfigFile.js +35 -0
  146. package/dist/config/parseConfigFile.js.map +1 -0
  147. package/dist/config/readConfigFileSync.d.ts +1 -0
  148. package/dist/config/readConfigFileSync.js +47 -0
  149. package/dist/config/readConfigFileSync.js.map +1 -0
  150. package/dist/config/watcherArming.d.ts +15 -0
  151. package/dist/config/watcherArming.js +59 -0
  152. package/dist/config/watcherArming.js.map +1 -0
  153. package/dist/dataLayer/blobBackup.d.ts +49 -20
  154. package/dist/dataLayer/blobBackup.js +139 -50
  155. package/dist/dataLayer/blobBackup.js.map +1 -1
  156. package/dist/dataLayer/delete.js +1 -1
  157. package/dist/dataLayer/delete.js.map +1 -1
  158. package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +14 -1
  159. package/dist/dataLayer/harperBridge/ResourceBridge.js +72 -12
  160. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  161. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.d.ts +3 -1
  162. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js +3 -1
  163. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js.map +1 -1
  164. package/dist/dataLayer/hdbInfoController.d.ts +10 -0
  165. package/dist/dataLayer/hdbInfoController.js +30 -1
  166. package/dist/dataLayer/hdbInfoController.js.map +1 -1
  167. package/dist/dataLayer/insert.d.ts +9 -1
  168. package/dist/dataLayer/insert.js +30 -0
  169. package/dist/dataLayer/insert.js.map +1 -1
  170. package/dist/dataLayer/rocksdbBackup.d.ts +2 -2
  171. package/dist/dataLayer/rocksdbBackup.js +45 -8
  172. package/dist/dataLayer/rocksdbBackup.js.map +1 -1
  173. package/dist/dataLayer/schema.js +8 -0
  174. package/dist/dataLayer/schema.js.map +1 -1
  175. package/dist/dataLayer/schemaDescribe.js +8 -1
  176. package/dist/dataLayer/schemaDescribe.js.map +1 -1
  177. package/dist/index.d.ts +2 -1
  178. package/dist/index.js +4 -1
  179. package/dist/index.js.map +1 -1
  180. package/dist/json/systemSchema.json +55 -0
  181. package/dist/resources/DatabaseTransaction.d.ts +77 -0
  182. package/dist/resources/DatabaseTransaction.js +586 -54
  183. package/dist/resources/DatabaseTransaction.js.map +1 -1
  184. package/dist/resources/LMDBTransaction.d.ts +2 -1
  185. package/dist/resources/LMDBTransaction.js +43 -6
  186. package/dist/resources/LMDBTransaction.js.map +1 -1
  187. package/dist/resources/PrimaryRocksDatabase.d.ts +1 -0
  188. package/dist/resources/PrimaryRocksDatabase.js +49 -5
  189. package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
  190. package/dist/resources/RecordEncoder.d.ts +20 -0
  191. package/dist/resources/RecordEncoder.js +110 -9
  192. package/dist/resources/RecordEncoder.js.map +1 -1
  193. package/dist/resources/RequestTarget.d.ts +2 -0
  194. package/dist/resources/RequestTarget.js.map +1 -1
  195. package/dist/resources/Resource.js +117 -24
  196. package/dist/resources/Resource.js.map +1 -1
  197. package/dist/resources/ResourceInterface.d.ts +28 -1
  198. package/dist/resources/ResourceInterface.js.map +1 -1
  199. package/dist/resources/RocksIndexStore.d.ts +6 -1
  200. package/dist/resources/RocksIndexStore.js +24 -9
  201. package/dist/resources/RocksIndexStore.js.map +1 -1
  202. package/dist/resources/RocksTransactionLogStore.d.ts +24 -1
  203. package/dist/resources/RocksTransactionLogStore.js +160 -49
  204. package/dist/resources/RocksTransactionLogStore.js.map +1 -1
  205. package/dist/resources/Table.d.ts +165 -10
  206. package/dist/resources/Table.js +1805 -303
  207. package/dist/resources/Table.js.map +1 -1
  208. package/dist/resources/analytics/write.js +10 -3
  209. package/dist/resources/analytics/write.js.map +1 -1
  210. package/dist/resources/auditStore.d.ts +181 -2
  211. package/dist/resources/auditStore.js +640 -29
  212. package/dist/resources/auditStore.js.map +1 -1
  213. package/dist/resources/blob.d.ts +129 -9
  214. package/dist/resources/blob.js +992 -114
  215. package/dist/resources/blob.js.map +1 -1
  216. package/dist/resources/branchDatabase.d.ts +48 -0
  217. package/dist/resources/branchDatabase.js +892 -0
  218. package/dist/resources/branchDatabase.js.map +1 -0
  219. package/dist/resources/crdt.js +50 -12
  220. package/dist/resources/crdt.js.map +1 -1
  221. package/dist/resources/dataLoader.js +3 -4
  222. package/dist/resources/dataLoader.js.map +1 -1
  223. package/dist/resources/databases.d.ts +157 -14
  224. package/dist/resources/databases.js +1462 -325
  225. package/dist/resources/databases.js.map +1 -1
  226. package/dist/resources/defineTable.d.ts +10 -2
  227. package/dist/resources/defineTable.js +9 -1
  228. package/dist/resources/defineTable.js.map +1 -1
  229. package/dist/resources/derivedIndexRegistry.d.ts +5 -0
  230. package/dist/resources/derivedIndexRegistry.js +68 -0
  231. package/dist/resources/derivedIndexRegistry.js.map +1 -0
  232. package/dist/resources/derivedIndexRuntime.d.ts +215 -0
  233. package/dist/resources/derivedIndexRuntime.js +2027 -0
  234. package/dist/resources/derivedIndexRuntime.js.map +1 -0
  235. package/dist/resources/graphql.d.ts +1 -1
  236. package/dist/resources/graphql.js +52 -16
  237. package/dist/resources/graphql.js.map +1 -1
  238. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +103 -9
  239. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +854 -41
  240. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
  241. package/dist/resources/indexes/hnswDerivedIndex.d.ts +67 -0
  242. package/dist/resources/indexes/hnswDerivedIndex.js +464 -0
  243. package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -0
  244. package/dist/resources/indexes/hnswPlaneBinding.d.ts +65 -0
  245. package/dist/resources/indexes/hnswPlaneBinding.js +91 -0
  246. package/dist/resources/indexes/hnswPlaneBinding.js.map +1 -0
  247. package/dist/resources/longLivedTransactions.d.ts +71 -0
  248. package/dist/resources/longLivedTransactions.js +358 -0
  249. package/dist/resources/longLivedTransactions.js.map +1 -0
  250. package/dist/resources/models/backendRegistry.d.ts +26 -0
  251. package/dist/resources/models/backendRegistry.js +60 -2
  252. package/dist/resources/models/backendRegistry.js.map +1 -1
  253. package/dist/resources/models/bootstrap.d.ts +33 -1
  254. package/dist/resources/models/bootstrap.js +416 -31
  255. package/dist/resources/models/bootstrap.js.map +1 -1
  256. package/dist/resources/nodeIdMapping.d.ts +5 -0
  257. package/dist/resources/nodeIdMapping.js +49 -0
  258. package/dist/resources/nodeIdMapping.js.map +1 -1
  259. package/dist/resources/recordLock.d.ts +123 -0
  260. package/dist/resources/recordLock.js +315 -0
  261. package/dist/resources/recordLock.js.map +1 -0
  262. package/dist/resources/recordLockCoordinator.d.ts +557 -0
  263. package/dist/resources/recordLockCoordinator.js +2565 -0
  264. package/dist/resources/recordLockCoordinator.js.map +1 -0
  265. package/dist/resources/replayLogs.d.ts +14 -1
  266. package/dist/resources/replayLogs.js +157 -24
  267. package/dist/resources/replayLogs.js.map +1 -1
  268. package/dist/resources/replayLogsGuards.d.ts +94 -7
  269. package/dist/resources/replayLogsGuards.js +111 -7
  270. package/dist/resources/replayLogsGuards.js.map +1 -1
  271. package/dist/resources/replicatedApplyFailure.d.ts +16 -0
  272. package/dist/resources/replicatedApplyFailure.js +63 -0
  273. package/dist/resources/replicatedApplyFailure.js.map +1 -0
  274. package/dist/resources/scheduler/scheduler.js +3 -3
  275. package/dist/resources/scheduler/scheduler.js.map +1 -1
  276. package/dist/resources/search.d.ts +10 -4
  277. package/dist/resources/search.js +283 -37
  278. package/dist/resources/search.js.map +1 -1
  279. package/dist/resources/tracked.d.ts +5 -1
  280. package/dist/resources/tracked.js +74 -23
  281. package/dist/resources/tracked.js.map +1 -1
  282. package/dist/resources/transactionBroadcast.d.ts +1 -1
  283. package/dist/resources/transactionBroadcast.js +2 -2
  284. package/dist/resources/transactionBroadcast.js.map +1 -1
  285. package/dist/security/auth.js +61 -30
  286. package/dist/security/auth.js.map +1 -1
  287. package/dist/security/authn/oidc/claims.d.ts +22 -0
  288. package/dist/security/authn/oidc/claims.js +71 -0
  289. package/dist/security/authn/oidc/claims.js.map +1 -0
  290. package/dist/security/authn/oidc/identityToken.d.ts +27 -0
  291. package/dist/security/authn/oidc/identityToken.js +111 -0
  292. package/dist/security/authn/oidc/identityToken.js.map +1 -0
  293. package/dist/security/authn/oidc/jwks.d.ts +25 -0
  294. package/dist/security/authn/oidc/jwks.js +261 -0
  295. package/dist/security/authn/oidc/jwks.js.map +1 -0
  296. package/dist/security/authn/oidc/providers/generic.d.ts +13 -0
  297. package/dist/security/authn/oidc/providers/generic.js +34 -0
  298. package/dist/security/authn/oidc/providers/generic.js.map +1 -0
  299. package/dist/security/authn/oidc/providers/githubActions.d.ts +11 -0
  300. package/dist/security/authn/oidc/providers/githubActions.js +129 -0
  301. package/dist/security/authn/oidc/providers/githubActions.js.map +1 -0
  302. package/dist/security/authn/oidc/providers/index.d.ts +37 -0
  303. package/dist/security/authn/oidc/providers/index.js +24 -0
  304. package/dist/security/authn/oidc/providers/index.js.map +1 -0
  305. package/dist/security/authn/oidc/tokenExchange.d.ts +12 -0
  306. package/dist/security/authn/oidc/tokenExchange.js +306 -0
  307. package/dist/security/authn/oidc/tokenExchange.js.map +1 -0
  308. package/dist/security/authn/oidc/trustPolicyOperations.d.ts +49 -0
  309. package/dist/security/authn/oidc/trustPolicyOperations.js +358 -0
  310. package/dist/security/authn/oidc/trustPolicyOperations.js.map +1 -0
  311. package/dist/security/authn/oidc/types.d.ts +38 -0
  312. package/dist/security/authn/oidc/types.js +6 -0
  313. package/dist/security/authn/oidc/types.js.map +1 -0
  314. package/dist/security/certificateVerification/index.js +40 -11
  315. package/dist/security/certificateVerification/index.js.map +1 -1
  316. package/dist/security/certificateVerification/trustedIssuers.d.ts +24 -0
  317. package/dist/security/certificateVerification/trustedIssuers.js +79 -0
  318. package/dist/security/certificateVerification/trustedIssuers.js.map +1 -0
  319. package/dist/security/certificateVerification/types.d.ts +1 -0
  320. package/dist/security/credentialProvenance.d.ts +35 -0
  321. package/dist/security/credentialProvenance.js +51 -0
  322. package/dist/security/credentialProvenance.js.map +1 -0
  323. package/dist/security/credentialRejection.d.ts +4 -0
  324. package/dist/security/credentialRejection.js +24 -0
  325. package/dist/security/credentialRejection.js.map +1 -0
  326. package/dist/security/deferredAuthentication.d.ts +36 -0
  327. package/dist/security/deferredAuthentication.js +70 -0
  328. package/dist/security/deferredAuthentication.js.map +1 -0
  329. package/dist/security/impersonation.d.ts +21 -0
  330. package/dist/security/impersonation.js +108 -9
  331. package/dist/security/impersonation.js.map +1 -1
  332. package/dist/security/jsLoader.d.ts +6 -0
  333. package/dist/security/jsLoader.js +77 -15
  334. package/dist/security/jsLoader.js.map +1 -1
  335. package/dist/security/keys.js +301 -71
  336. package/dist/security/keys.js.map +1 -1
  337. package/dist/security/operationScope.d.ts +21 -0
  338. package/dist/security/operationScope.js +36 -0
  339. package/dist/security/operationScope.js.map +1 -0
  340. package/dist/security/permissionsTranslator.js +21 -0
  341. package/dist/security/permissionsTranslator.js.map +1 -1
  342. package/dist/security/tokenAuthentication.d.ts +19 -1
  343. package/dist/security/tokenAuthentication.js +191 -10
  344. package/dist/security/tokenAuthentication.js.map +1 -1
  345. package/dist/security/user.js +4 -3
  346. package/dist/security/user.js.map +1 -1
  347. package/dist/server/DurableSubscriptionsSession.d.ts +2 -2
  348. package/dist/server/DurableSubscriptionsSession.js +67 -10
  349. package/dist/server/DurableSubscriptionsSession.js.map +1 -1
  350. package/dist/server/REST.js +106 -2
  351. package/dist/server/REST.js.map +1 -1
  352. package/dist/server/graphqlQuerying.js +4 -0
  353. package/dist/server/graphqlQuerying.js.map +1 -1
  354. package/dist/server/http.d.ts +16 -1
  355. package/dist/server/http.js +122 -17
  356. package/dist/server/http.js.map +1 -1
  357. package/dist/server/itc/serverHandlers.js +8 -1
  358. package/dist/server/itc/serverHandlers.js.map +1 -1
  359. package/dist/server/jobs/jobProcess.js +6 -2
  360. package/dist/server/jobs/jobProcess.js.map +1 -1
  361. package/dist/server/jobs/jobs.js +4 -1
  362. package/dist/server/jobs/jobs.js.map +1 -1
  363. package/dist/server/liveSubscriptionAuth.d.ts +26 -4
  364. package/dist/server/liveSubscriptionAuth.js +105 -39
  365. package/dist/server/liveSubscriptionAuth.js.map +1 -1
  366. package/dist/server/loadRootComponents.js +49 -10
  367. package/dist/server/loadRootComponents.js.map +1 -1
  368. package/dist/server/mqtt.d.ts +2 -0
  369. package/dist/server/mqtt.js +165 -30
  370. package/dist/server/mqtt.js.map +1 -1
  371. package/dist/server/nodeName.d.ts +2 -0
  372. package/dist/server/nodeName.js +107 -23
  373. package/dist/server/nodeName.js.map +1 -1
  374. package/dist/server/serverHelpers/Headers.d.ts +27 -0
  375. package/dist/server/serverHelpers/Headers.js +145 -1
  376. package/dist/server/serverHelpers/Headers.js.map +1 -1
  377. package/dist/server/serverHelpers/NodeAdapterResponse.d.ts +48 -0
  378. package/dist/server/serverHelpers/NodeAdapterResponse.js +220 -0
  379. package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -0
  380. package/dist/server/serverHelpers/Request.d.ts +5 -10
  381. package/dist/server/serverHelpers/Request.js +38 -136
  382. package/dist/server/serverHelpers/Request.js.map +1 -1
  383. package/dist/server/serverHelpers/contentTypes.d.ts +11 -0
  384. package/dist/server/serverHelpers/contentTypes.js +221 -40
  385. package/dist/server/serverHelpers/contentTypes.js.map +1 -1
  386. package/dist/server/serverHelpers/deployValidationState.d.ts +3 -0
  387. package/dist/server/serverHelpers/deployValidationState.js +9 -19
  388. package/dist/server/serverHelpers/deployValidationState.js.map +1 -1
  389. package/dist/server/serverHelpers/operationAuthorizationState.d.ts +12 -0
  390. package/dist/server/serverHelpers/operationAuthorizationState.js +24 -2
  391. package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -1
  392. package/dist/server/serverHelpers/registeredOperations.d.ts +5 -4
  393. package/dist/server/serverHelpers/registeredOperations.js +74 -21
  394. package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
  395. package/dist/server/serverHelpers/requestSanitization.d.ts +11 -0
  396. package/dist/server/serverHelpers/requestSanitization.js +20 -0
  397. package/dist/server/serverHelpers/requestSanitization.js.map +1 -0
  398. package/dist/server/serverHelpers/serverHandlers.js +6 -3
  399. package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
  400. package/dist/server/serverHelpers/serverUtilities.d.ts +18 -0
  401. package/dist/server/serverHelpers/serverUtilities.js +178 -29
  402. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  403. package/dist/server/serverHelpers/sharedMessageEncoding.d.ts +67 -0
  404. package/dist/server/serverHelpers/sharedMessageEncoding.js +280 -0
  405. package/dist/server/serverHelpers/sharedMessageEncoding.js.map +1 -0
  406. package/dist/server/serverHelpers/uwsServer.js +19 -1
  407. package/dist/server/serverHelpers/uwsServer.js.map +1 -1
  408. package/dist/server/static.js +24 -28
  409. package/dist/server/static.js.map +1 -1
  410. package/dist/server/storageReclamation.d.ts +5 -0
  411. package/dist/server/storageReclamation.js +17 -1
  412. package/dist/server/storageReclamation.js.map +1 -1
  413. package/dist/server/threads/isolatedApplications.d.ts +47 -0
  414. package/dist/server/threads/isolatedApplications.js +171 -0
  415. package/dist/server/threads/isolatedApplications.js.map +1 -0
  416. package/dist/server/threads/itc.d.ts +7 -2
  417. package/dist/server/threads/itc.js +5 -1
  418. package/dist/server/threads/itc.js.map +1 -1
  419. package/dist/server/threads/logRotationTransport.d.ts +1 -0
  420. package/dist/server/threads/logRotationTransport.js +33 -0
  421. package/dist/server/threads/logRotationTransport.js.map +1 -0
  422. package/dist/server/threads/manageThreads.d.ts +83 -3
  423. package/dist/server/threads/manageThreads.js +769 -75
  424. package/dist/server/threads/manageThreads.js.map +1 -1
  425. package/dist/server/threads/socketRouter.d.ts +1 -0
  426. package/dist/server/threads/socketRouter.js +277 -31
  427. package/dist/server/threads/socketRouter.js.map +1 -1
  428. package/dist/server/threads/threadHeapMemory.d.ts +2 -0
  429. package/dist/server/threads/threadHeapMemory.js +31 -0
  430. package/dist/server/threads/threadHeapMemory.js.map +1 -0
  431. package/dist/server/threads/threadServer.js +76 -22
  432. package/dist/server/threads/threadServer.js.map +1 -1
  433. package/dist/sqlEngine/config.d.ts +1 -3
  434. package/dist/sqlEngine/config.js +19 -16
  435. package/dist/sqlEngine/config.js.map +1 -1
  436. package/dist/sqlTranslator/index.d.ts +1 -1
  437. package/dist/sqlTranslator/index.js +30 -7
  438. package/dist/sqlTranslator/index.js.map +1 -1
  439. package/dist/upgrade/directives/5-3-0.d.ts +7 -0
  440. package/dist/upgrade/directives/5-3-0.js +148 -0
  441. package/dist/upgrade/directives/5-3-0.js.map +1 -0
  442. package/dist/upgrade/directives/directivesController.js +2 -1
  443. package/dist/upgrade/directives/directivesController.js.map +1 -1
  444. package/dist/utility/OperationFunctionCaller.js +2 -1
  445. package/dist/utility/OperationFunctionCaller.js.map +1 -1
  446. package/dist/utility/common_utils.d.ts +16 -0
  447. package/dist/utility/common_utils.js +32 -6
  448. package/dist/utility/common_utils.js.map +1 -1
  449. package/dist/utility/componentNames.d.ts +8 -0
  450. package/dist/utility/componentNames.js +12 -1
  451. package/dist/utility/componentNames.js.map +1 -1
  452. package/dist/utility/environment/environmentManager.js +3 -6
  453. package/dist/utility/environment/environmentManager.js.map +1 -1
  454. package/dist/utility/environment/systemInformation.d.ts +1 -0
  455. package/dist/utility/environment/systemInformation.js +1 -0
  456. package/dist/utility/environment/systemInformation.js.map +1 -1
  457. package/dist/utility/errors/commonErrors.d.ts +2 -0
  458. package/dist/utility/errors/commonErrors.js +2 -0
  459. package/dist/utility/errors/commonErrors.js.map +1 -1
  460. package/dist/utility/errors/hdbError.d.ts +33 -0
  461. package/dist/utility/errors/hdbError.js +58 -1
  462. package/dist/utility/errors/hdbError.js.map +1 -1
  463. package/dist/utility/globalSchema.d.ts +18 -0
  464. package/dist/utility/hdbTerms.d.ts +18 -0
  465. package/dist/utility/hdbTerms.js +20 -2
  466. package/dist/utility/hdbTerms.js.map +1 -1
  467. package/dist/utility/logging/harper_logger.d.ts +2 -0
  468. package/dist/utility/logging/harper_logger.js +286 -30
  469. package/dist/utility/logging/harper_logger.js.map +1 -1
  470. package/dist/utility/logging/logGenerationCoordinator.d.ts +35 -0
  471. package/dist/utility/logging/logGenerationCoordinator.js +184 -0
  472. package/dist/utility/logging/logGenerationCoordinator.js.map +1 -0
  473. package/dist/utility/logging/logRotation.d.ts +46 -0
  474. package/dist/utility/logging/logRotation.js +365 -0
  475. package/dist/utility/logging/logRotation.js.map +1 -0
  476. package/dist/utility/logging/logRotator.d.ts +1 -1
  477. package/dist/utility/logging/logRotator.js +192 -85
  478. package/dist/utility/logging/logRotator.js.map +1 -1
  479. package/dist/utility/nodeIdentity.d.ts +9 -0
  480. package/dist/utility/nodeIdentity.js +58 -0
  481. package/dist/utility/nodeIdentity.js.map +1 -0
  482. package/dist/utility/npmUtilities.js +11 -7
  483. package/dist/utility/npmUtilities.js.map +1 -1
  484. package/dist/utility/operationPermissions.d.ts +3 -1
  485. package/dist/utility/operationPermissions.js +16 -1
  486. package/dist/utility/operationPermissions.js.map +1 -1
  487. package/dist/utility/operation_authorization.d.ts +10 -7
  488. package/dist/utility/operation_authorization.js +212 -41
  489. package/dist/utility/operation_authorization.js.map +1 -1
  490. package/dist/utility/watchPath.d.ts +29 -0
  491. package/dist/utility/watchPath.js +68 -0
  492. package/dist/utility/watchPath.js.map +1 -0
  493. package/dist/utility/watcherFallback.d.ts +41 -0
  494. package/dist/utility/watcherFallback.js +153 -0
  495. package/dist/utility/watcherFallback.js.map +1 -1
  496. package/dist/validation/configValidator.d.ts +12 -0
  497. package/dist/validation/configValidator.js +205 -75
  498. package/dist/validation/configValidator.js.map +1 -1
  499. package/dist/validation/installValidator.js +12 -0
  500. package/dist/validation/installValidator.js.map +1 -1
  501. package/dist/validation/validationWrapper.d.ts +11 -0
  502. package/dist/validation/validationWrapper.js +16 -3
  503. package/dist/validation/validationWrapper.js.map +1 -1
  504. package/index.ts +8 -0
  505. package/json/systemSchema.json +55 -0
  506. package/npm-shrinkwrap.json +286 -194
  507. package/package.json +15 -7
  508. package/resources/DESIGN.md +219 -52
  509. package/resources/DatabaseTransaction.ts +657 -52
  510. package/resources/LMDBTransaction.ts +46 -6
  511. package/resources/PrimaryRocksDatabase.ts +46 -7
  512. package/resources/RecordEncoder.ts +124 -9
  513. package/resources/RequestTarget.ts +2 -0
  514. package/resources/Resource.ts +114 -22
  515. package/resources/ResourceInterface.ts +31 -0
  516. package/resources/RocksIndexStore.ts +30 -9
  517. package/resources/RocksTransactionLogStore.ts +188 -51
  518. package/resources/Table.ts +1960 -301
  519. package/resources/analytics/write.ts +10 -3
  520. package/resources/auditStore.ts +647 -32
  521. package/resources/blob.ts +1029 -110
  522. package/resources/branchDatabase.ts +941 -0
  523. package/resources/crdt.ts +76 -12
  524. package/resources/dataLoader.ts +3 -4
  525. package/resources/databases.ts +1584 -320
  526. package/resources/defineTable.ts +18 -2
  527. package/resources/derivedIndexRegistry.ts +56 -0
  528. package/resources/derivedIndexRuntime.ts +2292 -0
  529. package/resources/graphql.ts +72 -17
  530. package/resources/indexes/HierarchicalNavigableSmallWorld.ts +911 -48
  531. package/resources/indexes/hnswDerivedIndex.ts +531 -0
  532. package/resources/indexes/hnswPlaneBinding.ts +174 -0
  533. package/resources/longLivedTransactions.ts +360 -0
  534. package/resources/models/backendRegistry.ts +84 -2
  535. package/resources/models/bootstrap.ts +473 -28
  536. package/resources/nodeIdMapping.ts +50 -0
  537. package/resources/recordLock.ts +419 -0
  538. package/resources/recordLockCoordinator.ts +3043 -0
  539. package/resources/replayLogs.ts +152 -26
  540. package/resources/replayLogsGuards.ts +171 -8
  541. package/resources/replicatedApplyFailure.ts +77 -0
  542. package/resources/scheduler/scheduler.ts +4 -4
  543. package/resources/search.ts +288 -46
  544. package/resources/tracked.ts +73 -22
  545. package/resources/transactionBroadcast.ts +3 -3
  546. package/security/auth.ts +68 -29
  547. package/security/authn/oidc/claims.ts +72 -0
  548. package/security/authn/oidc/identityToken.ts +129 -0
  549. package/security/authn/oidc/jwks.ts +260 -0
  550. package/security/authn/oidc/providers/generic.ts +40 -0
  551. package/security/authn/oidc/providers/githubActions.ts +137 -0
  552. package/security/authn/oidc/providers/index.ts +52 -0
  553. package/security/authn/oidc/tokenExchange.ts +300 -0
  554. package/security/authn/oidc/trustPolicyOperations.ts +343 -0
  555. package/security/authn/oidc/types.ts +41 -0
  556. package/security/certificateVerification/index.ts +54 -13
  557. package/security/certificateVerification/trustedIssuers.ts +76 -0
  558. package/security/certificateVerification/types.ts +1 -0
  559. package/security/credentialProvenance.ts +47 -0
  560. package/security/credentialRejection.ts +22 -0
  561. package/security/deferredAuthentication.ts +71 -0
  562. package/security/impersonation.ts +117 -12
  563. package/security/jsLoader.ts +83 -18
  564. package/security/keys.ts +298 -72
  565. package/security/operationScope.ts +33 -0
  566. package/security/permissionsTranslator.js +23 -0
  567. package/security/tokenAuthentication.ts +233 -12
  568. package/security/user.ts +4 -3
  569. package/server/DESIGN.md +194 -16
  570. package/server/DurableSubscriptionsSession.ts +71 -11
  571. package/server/REST.ts +115 -4
  572. package/server/graphqlQuerying.ts +4 -0
  573. package/server/http.ts +133 -20
  574. package/server/itc/serverHandlers.js +8 -1
  575. package/server/jobs/jobProcess.ts +8 -2
  576. package/server/jobs/jobs.ts +4 -1
  577. package/server/liveSubscriptionAuth.ts +129 -46
  578. package/server/loadRootComponents.js +50 -8
  579. package/server/mqtt.ts +179 -38
  580. package/server/nodeName.ts +103 -21
  581. package/server/serverHelpers/Headers.ts +135 -0
  582. package/server/serverHelpers/NodeAdapterResponse.ts +221 -0
  583. package/server/serverHelpers/Request.ts +33 -131
  584. package/server/serverHelpers/contentTypes.ts +217 -36
  585. package/server/serverHelpers/deployValidationState.ts +24 -13
  586. package/server/serverHelpers/operationAuthorizationState.ts +34 -3
  587. package/server/serverHelpers/registeredOperations.ts +79 -22
  588. package/server/serverHelpers/requestSanitization.ts +15 -0
  589. package/server/serverHelpers/serverHandlers.js +6 -3
  590. package/server/serverHelpers/serverUtilities.ts +232 -40
  591. package/server/serverHelpers/sharedMessageEncoding.ts +307 -0
  592. package/server/serverHelpers/uwsServer.ts +17 -2
  593. package/server/static.ts +23 -29
  594. package/server/storageReclamation.ts +15 -2
  595. package/server/threads/isolatedApplications.ts +157 -0
  596. package/server/threads/itc.js +11 -1
  597. package/server/threads/logRotationTransport.ts +40 -0
  598. package/server/threads/manageThreads.js +771 -66
  599. package/server/threads/socketRouter.ts +291 -30
  600. package/server/threads/threadHeapMemory.ts +26 -0
  601. package/server/threads/threadServer.js +73 -22
  602. package/sqlTranslator/index.ts +31 -8
  603. package/studio/web/assets/{Chat-4RrB5134.js → Chat-D3j-1yY1.js} +1 -1
  604. package/studio/web/assets/{FloatingChat-omlNMDcJ.js → FloatingChat-BxJGYcfB.js} +3 -3
  605. package/studio/web/assets/{apiToken-Bke3wvfZ.js → apiToken-CT55oWOe.js} +1 -1
  606. package/studio/web/assets/{applications-DvFDYJgK.js → applications-D9Ct9_vm.js} +1 -1
  607. package/studio/web/assets/{cssMode-C1vRa7zh.js → cssMode-DV8H7VwA.js} +1 -1
  608. package/studio/web/assets/{editor-3XRWEDWX.js → editor-uatc0unt.js} +1 -1
  609. package/studio/web/assets/{html-CTY5tdMr.js → html-Bm6D6paN.js} +1 -1
  610. package/studio/web/assets/{htmlMode-CG1vSD9t.js → htmlMode-CEn7tpLG.js} +1 -1
  611. package/studio/web/assets/{index-Dfpeofdu.js → index-BIXW6Pu4.js} +5 -5
  612. package/studio/web/assets/{index.lazy-B1VOIv-t.js → index.lazy-UI7L-Vrk.js} +1 -1
  613. package/studio/web/assets/{javascript-CJeJzGnI.js → javascript-CJ0G3AFZ.js} +1 -1
  614. package/studio/web/assets/{jsonMode-CYPBwM82.js → jsonMode-DQADAYEa.js} +1 -1
  615. package/studio/web/assets/{languageServices-IKH4GUHl.js → languageServices-CAQJXWcI.js} +1 -1
  616. package/studio/web/assets/{lspLanguageFeatures-BC8_gxKG.js → lspLanguageFeatures-CCQ8P5sY.js} +1 -1
  617. package/studio/web/assets/{notifications-BfKBpYcq.js → notifications-BbxTU6Aw.js} +1 -1
  618. package/studio/web/assets/{notifications-DFbArTfC.js → notifications-Cvb3P1lB.js} +1 -1
  619. package/studio/web/assets/{profile-Q4-T6c-S.js → profile-Yyb7gsvL.js} +1 -1
  620. package/studio/web/assets/{regions-CtkV0xje.js → regions-OgjGHlU5.js} +1 -1
  621. package/studio/web/assets/{register-1ZZuMsiA.js → register-6qwNEOY3.js} +2 -2
  622. package/studio/web/assets/{setComponentFile-ZWLcWv5X.js → setComponentFile-BilDMtgB.js} +1 -1
  623. package/studio/web/assets/{setup-DWprJyJy.js → setup-J6qJ7OIU.js} +2 -2
  624. package/studio/web/assets/{status--aNm8isn.js → status-0RWGcfyD.js} +1 -1
  625. package/studio/web/assets/{toggleHighContrast-Cq_lt3XD.js → toggleHighContrast-BIn-vErT.js} +1 -1
  626. package/studio/web/assets/{tsMode-pjgytARx.js → tsMode-DgUXku4d.js} +1 -1
  627. package/studio/web/assets/{typescript-Cdg0mqUh.js → typescript-C9orXcsM.js} +1 -1
  628. package/studio/web/assets/{useEntityRestURL-RZhaY8Rn.js → useEntityRestURL-BEoXXbUB.js} +1 -1
  629. package/studio/web/assets/{workers-BgoXIqQe.js → workers-JVzSDmgx.js} +1 -1
  630. package/studio/web/assets/{xml-BwSeDMiP.js → xml-Cq-S8S4X.js} +1 -1
  631. package/studio/web/assets/{yaml-DotCUG5l.js → yaml-sfoRdh1M.js} +1 -1
  632. package/studio/web/index.html +1 -1
  633. package/upgrade/directives/5-3-0.ts +132 -0
  634. package/upgrade/directives/directivesController.ts +2 -1
  635. package/utility/OperationFunctionCaller.ts +2 -1
  636. package/utility/common_utils.ts +30 -5
  637. package/utility/componentNames.ts +12 -0
  638. package/utility/environment/environmentManager.ts +3 -7
  639. package/utility/environment/systemInformation.ts +7 -0
  640. package/utility/errors/commonErrors.ts +4 -0
  641. package/utility/errors/hdbError.ts +57 -0
  642. package/utility/hdbTerms.ts +19 -0
  643. package/utility/logging/harper_logger.ts +278 -26
  644. package/utility/logging/logGenerationCoordinator.ts +196 -0
  645. package/utility/logging/logRotation.ts +367 -0
  646. package/utility/logging/logRotator.ts +213 -81
  647. package/utility/nodeIdentity.ts +45 -0
  648. package/utility/npmUtilities.ts +12 -8
  649. package/utility/operationPermissions.ts +18 -1
  650. package/utility/operation_authorization.ts +231 -42
  651. package/utility/watchPath.ts +63 -0
  652. package/utility/watcherFallback.ts +148 -0
  653. package/validation/configValidator.ts +215 -75
  654. package/validation/installValidator.ts +15 -0
  655. package/validation/validationWrapper.ts +18 -4
@@ -36,14 +36,24 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.ABANDONED_MARK_LOCK_TIMEOUT = exports.CHECKPOINT_ALGORITHM = exports.databases = exports.tables = exports.databaseEventsEmitter = exports.NON_REPLICATING_SYSTEM_TABLES = void 0;
39
+ exports.CHECKPOINT_ALGORITHM = exports.BRANCH_REMOVING_SUFFIX = exports.BRANCH_ROOT_DIR = exports.databases = exports.tables = exports.databaseEventsEmitter = exports.NON_REPLICATING_SYSTEM_TABLES = void 0;
40
40
  exports.isReadOnlyMode = isReadOnlyMode;
41
41
  exports.getRocksCompression = getRocksCompression;
42
42
  exports.resetRocksCompression = resetRocksCompression;
43
43
  exports.toRocksCompression = toRocksCompression;
44
44
  exports.getTables = getTables;
45
45
  exports.getDatabases = getDatabases;
46
+ exports.hydrateBranchRelationships = hydrateBranchRelationships;
46
47
  exports.readMetaDb = readMetaDb;
48
+ exports.resolveBranchPath = resolveBranchPath;
49
+ exports.quarantineBranchIdentity = quarantineBranchIdentity;
50
+ exports.assertBranchIdentityAvailable = assertBranchIdentityAvailable;
51
+ exports.reserveBranchIdentity = reserveBranchIdentity;
52
+ exports.retakeBranchIdentity = retakeBranchIdentity;
53
+ exports.releaseBranchIdentity = releaseBranchIdentity;
54
+ exports.isBranchIdentity = isBranchIdentity;
55
+ exports.openBranchDatabase = openBranchDatabase;
56
+ exports.closeBranchDatabases = closeBranchDatabases;
47
57
  exports.resetDatabases = resetDatabases;
48
58
  exports.resolveDatabaseStorageRoot = resolveDatabaseStorageRoot;
49
59
  exports.resolveDatabasePath = resolveDatabasePath;
@@ -52,10 +62,11 @@ exports.dropDatabase = dropDatabase;
52
62
  exports.closeDatabase = closeDatabase;
53
63
  exports.closeLoadedDatabases = closeLoadedDatabases;
54
64
  exports.table = table;
65
+ exports.scopedTableFactory = scopedTableFactory;
66
+ exports.reloadBranchAt = reloadBranchAt;
55
67
  exports.canonicalizeIndexOptions = canonicalizeIndexOptions;
56
68
  exports.setIndexingCheckpointPeriod = setIndexingCheckpointPeriod;
57
69
  exports.resumeStartKey = resumeStartKey;
58
- exports.tryAcquireUpdateAttributesLock = tryAcquireUpdateAttributesLock;
59
70
  exports.dropTableMeta = dropTableMeta;
60
71
  exports.onUpdatedTable = onUpdatedTable;
61
72
  exports.onRemovedTable = onRemovedTable;
@@ -67,7 +78,7 @@ const node_crypto_1 = require("node:crypto");
67
78
  const environmentManager_ts_1 = require("../utility/environment/environmentManager.js");
68
79
  const terms_ts_1 = require("../utility/lmdb/terms.js");
69
80
  const lmdb_1 = require("lmdb");
70
- const path_1 = require("path");
81
+ const node_path_1 = require("node:path");
71
82
  const node_fs_1 = require("node:fs");
72
83
  const promises_1 = require("node:fs/promises");
73
84
  const initializePaths_js_1 = require("../dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js");
@@ -85,14 +96,17 @@ const harper_logger_ts_1 = __importDefault(require("../utility/logging/harper_lo
85
96
  const { forComponent } = harper_logger_ts_1.default;
86
97
  const manageThreads = __importStar(require("../server/threads/manageThreads.js"));
87
98
  const auditStore_ts_1 = require("./auditStore.js");
88
- const storageReclamation_ts_1 = require("../server/storageReclamation.js");
89
99
  const RecordEncoder_ts_1 = require("./RecordEncoder.js");
90
100
  const blob_ts_1 = require("./blob.js");
101
+ const storageReclamation_ts_1 = require("../server/storageReclamation.js");
102
+ const common_validators_ts_1 = require("../validation/common_validators.js");
91
103
  const customIndexes_ts_1 = require("./indexes/customIndexes.js");
104
+ const hnswPlaneBinding_ts_1 = require("./indexes/hnswPlaneBinding.js");
92
105
  const OpenDBIObject_ts_1 = require("../utility/lmdb/OpenDBIObject.js");
93
106
  const rocksdb_js_1 = require("@harperfast/rocksdb-js");
94
107
  const PrimaryRocksDatabase_ts_1 = require("./PrimaryRocksDatabase.js");
95
108
  const replayLogs_ts_1 = require("./replayLogs.js");
109
+ const hnswDerivedIndex_ts_1 = require("./indexes/hnswDerivedIndex.js");
96
110
  const node_os_1 = require("node:os");
97
111
  const RocksIndexStore_ts_1 = require("./RocksIndexStore.js");
98
112
  const rocksMemoryConfig_ts_1 = require("../utility/rocksMemoryConfig.js");
@@ -143,8 +157,60 @@ function markInternalDbiNonVersioned(dbisDb) {
143
157
  const logger = forComponent('storage');
144
158
  const DEFAULT_DATABASE_NAME = 'data';
145
159
  const DEFINED_TABLES = Symbol('defined-tables');
160
+ const CATALOG_RELATIONSHIP = Symbol('catalog-relationship');
146
161
  const DEFAULT_COMPRESSION_THRESHOLD = ((0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.STORAGE_PAGESIZE) || 4096) - 60; // larger than this requires multiple pages
147
162
  (0, environmentManager_ts_1.initSync)();
163
+ let relationshipsToHydrate = [];
164
+ const reportedRelationshipErrors = new Set();
165
+ // an interrupted create is reported once per table and thread, not on every rescan
166
+ const reportedIncompleteCatalogs = new Set();
167
+ function normalizeRelationships(attributes) {
168
+ const relationships = [];
169
+ for (const attribute of attributes) {
170
+ const target = attribute.relationshipReference;
171
+ if (!attribute.relationship || !target)
172
+ continue;
173
+ const relationship = {};
174
+ if (typeof attribute.relationship.from === 'string')
175
+ relationship.from = attribute.relationship.from;
176
+ if (typeof attribute.relationship.to === 'string')
177
+ relationship.to = attribute.relationship.to;
178
+ // the GraphQL parser hands every directive argument over as a string, and the resolver reads
179
+ // filterMissing for truthiness, so persist what the resolver would see rather than the literal
180
+ if (attribute.relationship.filterMissing !== undefined)
181
+ relationship.filterMissing = Boolean(attribute.relationship.filterMissing);
182
+ if (!relationship.from && !relationship.to)
183
+ continue;
184
+ const definition = {
185
+ name: attribute.name,
186
+ type: attribute.type,
187
+ relationship,
188
+ target: { database: target.database, table: target.table },
189
+ };
190
+ if (attribute.type === 'array')
191
+ definition.elements = { type: attribute.elements?.type };
192
+ relationships.push(definition);
193
+ }
194
+ return relationships;
195
+ }
196
+ function relationshipEquals(left, right) {
197
+ return (left?.name === right?.name &&
198
+ left?.type === right?.type &&
199
+ left?.elements?.type === right?.elements?.type &&
200
+ left?.relationship?.from === right?.relationship?.from &&
201
+ left?.relationship?.to === right?.relationship?.to &&
202
+ left?.relationship?.filterMissing === right?.relationship?.filterMissing &&
203
+ left?.target?.database === right?.target?.database &&
204
+ left?.target?.table === right?.target?.table);
205
+ }
206
+ function relationshipListsEqual(left, right) {
207
+ if (!Array.isArray(left) || left.length !== right.length)
208
+ return false;
209
+ for (let index = 0; index < right.length; index++)
210
+ if (!relationshipEquals(left[index], right[index]))
211
+ return false;
212
+ return true;
213
+ }
148
214
  /**
149
215
  * The RocksDB block/blob codec for every column family this process opens (`storage.rocks.compression`),
150
216
  * or `undefined` to leave rocksdb-js on its own default (lz4 wherever the native build has it).
@@ -311,9 +377,12 @@ function openRocksDatabase(path, options) {
311
377
  else {
312
378
  db = new PrimaryRocksDatabase_ts_1.PrimaryRocksDatabase(path, options).open();
313
379
  // the RocksDB put and remove return promises, which masks thrown errors in non-awaiting calls to put/remove,
314
- // making them unsafe to replace LMDB methods, which will synchronously throw errors if there is a problem
380
+ // making them unsafe to replace LMDB methods, which will synchronously throw errors if there is a problem.
381
+ // The versioned remove is necessarily async and its callers must await or otherwise track its promise.
315
382
  db.put = db.putSync;
316
- db.remove = db.removeSync;
383
+ db.remove = ((id, removeOptions) => typeof removeOptions === 'number'
384
+ ? db.removeIfVersion(id, removeOptions)
385
+ : db.removeSync(id, removeOptions));
317
386
  db.encoder.name = options.name;
318
387
  }
319
388
  db.env = {};
@@ -325,6 +394,33 @@ const rocksdbDatabaseEnvs = new Map();
325
394
  (0, globals_js_1._assignPackageExport)('databases', exports.databases);
326
395
  (0, globals_js_1._assignPackageExport)('tables', exports.tables);
327
396
  const NEXT_TABLE_ID = Symbol.for('next-table-id');
397
+ // Restore every field used by `commonChanged`, plus `indexed` and `indexNulls`,
398
+ // from the durable descriptor. In particular, preserve `indexNulls: false` so
399
+ // an index that excludes nulls is not reopened as though it contains them.
400
+ const PEER_REDEFINABLE_FIELDS = [
401
+ 'type',
402
+ 'indexed',
403
+ 'indexNulls',
404
+ 'nullable',
405
+ 'enumerable',
406
+ 'version',
407
+ 'elements',
408
+ 'properties',
409
+ 'embed',
410
+ ];
411
+ // `indexNulls` is derived from the durable descriptor, never sent by a peer, so naming it in the
412
+ // discard warn would blame the peer for a field it did not write.
413
+ const PEER_DECLARABLE_FIELDS = PEER_REDEFINABLE_FIELDS.filter((field) => field !== 'indexNulls');
414
+ // A cluster-origin caller's list can predate a declaration another thread has already committed, so on
415
+ // that path the descriptor — not the caller — decides what the attribute is, in both directions.
416
+ function applyDurableDeclaration(attribute, descriptor) {
417
+ for (const field of PEER_REDEFINABLE_FIELDS) {
418
+ if (field in descriptor)
419
+ attribute[field] = descriptor[field];
420
+ else
421
+ delete attribute[field];
422
+ }
423
+ }
328
424
  /**
329
425
  * True when a descriptor claims an index build no live operation in this process can own. The PID and
330
426
  * worker generation cannot answer that alone: a container reuses PID 1 and starts the in-memory
@@ -415,8 +511,9 @@ function getDatabases() {
415
511
  }
416
512
  loadedDatabases = true;
417
513
  definedDatabases = new Map();
514
+ relationshipsToHydrate = [];
418
515
  const hdbBasePath = (0, environmentManager_ts_1.getHdbBasePath)();
419
- let databasePath = hdbBasePath && (0, path_1.join)(hdbBasePath, hdbTerms_ts_1.DATABASES_DIR_NAME);
516
+ let databasePath = hdbBasePath && (0, node_path_1.join)(hdbBasePath, hdbTerms_ts_1.DATABASES_DIR_NAME);
420
517
  const schemaConfigs = (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.DATABASES) || {};
421
518
  // not sure why this doesn't work with the environmemt manager
422
519
  if (process.env.SCHEMAS_DATA_PATH)
@@ -424,7 +521,7 @@ function getDatabases() {
424
521
  databasePath =
425
522
  process.env.STORAGE_PATH ||
426
523
  (0, configUtils_ts_1.getConfigPath)(hdbTerms_ts_1.CONFIG_PARAMS.STORAGE_PATH) ||
427
- (databasePath && ((0, node_fs_1.existsSync)(databasePath) ? databasePath : (0, path_1.join)((0, environmentManager_ts_1.getHdbBasePath)(), hdbTerms_ts_1.LEGACY_DATABASES_DIR_NAME)));
524
+ (databasePath && ((0, node_fs_1.existsSync)(databasePath) ? databasePath : (0, node_path_1.join)((0, environmentManager_ts_1.getHdbBasePath)(), hdbTerms_ts_1.LEGACY_DATABASES_DIR_NAME)));
428
525
  if (databasePath && (0, node_fs_1.existsSync)(databasePath)) {
429
526
  // First load all the databases from our main database folder
430
527
  // TODO: Load any databases defined with explicit storage paths from the config
@@ -439,12 +536,17 @@ function getDatabases() {
439
536
  // create it (schemaRegex forbids the backtick), but the scan opens any CURRENT+MANIFEST dir
440
537
  if (databaseEntry.name === restoreMarker_ts_1.RESTORE_META_DIR)
441
538
  continue;
442
- const dbName = (0, path_1.basename)(databaseEntry.name, '.mdb');
443
- const dbPath = (0, path_1.join)(databasePath, databaseEntry.name);
539
+ // branch directories are process-local derivatives, never databases in their own right
540
+ if (databaseEntry.name === exports.BRANCH_ROOT_DIR)
541
+ continue;
542
+ const dbName = (0, node_path_1.basename)(databaseEntry.name, '.mdb');
543
+ const dbPath = (0, node_path_1.join)(databasePath, databaseEntry.name);
444
544
  if (blockedByRestore.has(dbName))
445
545
  continue;
546
+ if (isOpenBranchPath(dbPath))
547
+ continue;
446
548
  if (databaseEntry.isFile() &&
447
- (0, path_1.extname)(databaseEntry.name).toLowerCase() === '.mdb' &&
549
+ (0, node_path_1.extname)(databaseEntry.name).toLowerCase() === '.mdb' &&
448
550
  !schemaConfigs[dbName]?.path) {
449
551
  logger.trace(`loading lmdb database: ${dbPath}`);
450
552
  readMetaDb(dbPath, null, dbName);
@@ -471,12 +573,12 @@ function getDatabases() {
471
573
  if ((0, node_fs_1.existsSync)(baseSchemaPath)) {
472
574
  for (const schemaEntry of (0, node_fs_1.readdirSync)(baseSchemaPath, { withFileTypes: true })) {
473
575
  if (!schemaEntry.isFile()) {
474
- const schemaPath = (0, path_1.join)(baseSchemaPath, schemaEntry.name);
475
- const schemaAuditPath = (0, path_1.join)((0, initializePaths_js_1.getTransactionAuditStoreBasePath)(), schemaEntry.name);
576
+ const schemaPath = (0, node_path_1.join)(baseSchemaPath, schemaEntry.name);
577
+ const schemaAuditPath = (0, node_path_1.join)((0, initializePaths_js_1.getTransactionAuditStoreBasePath)(), schemaEntry.name);
476
578
  for (const tableEntry of (0, node_fs_1.readdirSync)(schemaPath, { withFileTypes: true })) {
477
- if (tableEntry.isFile() && (0, path_1.extname)(tableEntry.name).toLowerCase() === '.mdb') {
478
- const auditPath = (0, path_1.join)(schemaAuditPath, tableEntry.name);
479
- readMetaDb((0, path_1.join)(schemaPath, tableEntry.name), (0, path_1.basename)(tableEntry.name, '.mdb'), schemaEntry.name, auditPath, true);
579
+ if (tableEntry.isFile() && (0, node_path_1.extname)(tableEntry.name).toLowerCase() === '.mdb') {
580
+ const auditPath = (0, node_path_1.join)(schemaAuditPath, tableEntry.name);
581
+ readMetaDb((0, node_path_1.join)(schemaPath, tableEntry.name), (0, node_path_1.basename)(tableEntry.name, '.mdb'), schemaEntry.name, auditPath, true);
480
582
  }
481
583
  }
482
584
  }
@@ -494,14 +596,18 @@ function getDatabases() {
494
596
  continue; // migration staging dir
495
597
  if (databaseEntry.name === restoreMarker_ts_1.RESTORE_META_DIR)
496
598
  continue; // reserved restore-metadata dir
497
- if (blockedByRestore.has((0, path_1.basename)(databaseEntry.name, '.mdb')))
599
+ if (databaseEntry.name === exports.BRANCH_ROOT_DIR)
600
+ continue; // reserved branch root
601
+ if (blockedByRestore.has((0, node_path_1.basename)(databaseEntry.name, '.mdb')))
498
602
  continue;
499
- if (databaseEntry.isFile() && (0, path_1.extname)(databaseEntry.name).toLowerCase() === '.mdb') {
500
- readMetaDb((0, path_1.join)(databasePath, databaseEntry.name), (0, path_1.basename)(databaseEntry.name, '.mdb'), dbName);
603
+ if (isOpenBranchPath((0, node_path_1.join)(databasePath, databaseEntry.name)))
604
+ continue;
605
+ if (databaseEntry.isFile() && (0, node_path_1.extname)(databaseEntry.name).toLowerCase() === '.mdb') {
606
+ readMetaDb((0, node_path_1.join)(databasePath, databaseEntry.name), (0, node_path_1.basename)(databaseEntry.name, '.mdb'), dbName);
501
607
  }
502
608
  else {
503
609
  try {
504
- const dbPath = (0, path_1.join)(databasePath, databaseEntry.name);
610
+ const dbPath = (0, node_path_1.join)(databasePath, databaseEntry.name);
505
611
  const files = (0, node_fs_1.readdirSync)(dbPath, { withFileTypes: true });
506
612
  if (files.find((file) => file.name === 'CURRENT')?.isFile() &&
507
613
  files.some((file) => file.name.startsWith('MANIFEST-'))) {
@@ -521,7 +627,7 @@ function getDatabases() {
521
627
  if (tableConfigs) {
522
628
  for (const tableName in tableConfigs) {
523
629
  const tableConfig = tableConfigs[tableName];
524
- const tablePath = (0, path_1.join)(tableConfig.path, (0, path_1.basename)(tableName + '.mdb'));
630
+ const tablePath = (0, node_path_1.join)(tableConfig.path, (0, node_path_1.basename)(tableName + '.mdb'));
525
631
  if ((0, node_fs_1.existsSync)(tablePath)) {
526
632
  readMetaDb(tablePath, tableName, dbName, null, true);
527
633
  }
@@ -540,11 +646,15 @@ function getDatabases() {
540
646
  for (const tableName in tables) {
541
647
  if (!definedTables.has(tableName)) {
542
648
  logger.trace(`delete table class ${tableName}`);
649
+ tables[tableName]?.cleanup?.();
543
650
  delete tables[tableName];
544
651
  }
545
652
  }
546
653
  }
547
654
  else {
655
+ const removedTables = exports.databases[dbName];
656
+ for (const tableName in removedTables)
657
+ removedTables[tableName]?.cleanup?.();
548
658
  delete exports.databases[dbName];
549
659
  if (dbName === 'data') {
550
660
  for (const tableName in exports.tables) {
@@ -554,6 +664,7 @@ function getDatabases() {
554
664
  }
555
665
  }
556
666
  }
667
+ hydrateCatalogRelationships();
557
668
  if ((0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.ANALYTICS_REPLICATE) === false) {
558
669
  if (!exports.NON_REPLICATING_SYSTEM_TABLES.includes('hdb_analytics'))
559
670
  exports.NON_REPLICATING_SYSTEM_TABLES.push('hdb_analytics');
@@ -572,6 +683,148 @@ function getDatabases() {
572
683
  }
573
684
  return exports.databases;
574
685
  }
686
+ /**
687
+ * Hydrate one branch's relationships, resolving each target against the application's own branches
688
+ * first and only then against the real databases: a target the application also branched must be its
689
+ * branch's table, and a target it did not branch is legitimately the shared one.
690
+ */
691
+ function hydrateBranchRelationships(branch, branches) {
692
+ const resolveTarget = (target) => {
693
+ const targetBranch = branches.get(target.database);
694
+ // A branched target resolves ONLY within that branch. A durable branch is a checkpoint frozen
695
+ // at creation while the base keeps evolving, so falling through to the base for a table the
696
+ // branch's own copy lacks would point a branched application's relationship reads at live base
697
+ // data -- the fallback belongs to a database the application did not branch, never to one it did.
698
+ return targetBranch ? targetBranch.tables?.[target.table] : exports.databases[target.database]?.[target.table];
699
+ };
700
+ // Kept, not drained, like the global list: a target declared later (on this or another thread) is
701
+ // picked up by the next pass, and `hydrateTableRelationships` is a no-op once everything resolves.
702
+ for (const hydration of branch.pendingRelationships) {
703
+ try {
704
+ hydrateTableRelationships(hydration, resolveTarget, false);
705
+ }
706
+ catch (error) {
707
+ logger.error(`Unable to hydrate persisted relationships for branch table ${hydration.databaseName}.${hydration.tableName}`, error);
708
+ }
709
+ }
710
+ }
711
+ function hydrateCatalogRelationships() {
712
+ for (const hydration of relationshipsToHydrate) {
713
+ try {
714
+ hydrateTableRelationships(hydration);
715
+ }
716
+ catch (error) {
717
+ const key = `${hydration.databaseName}.${hydration.tableName}:hydrate`;
718
+ if (!reportedRelationshipErrors.has(key)) {
719
+ reportedRelationshipErrors.add(key);
720
+ logger.error(`Unable to hydrate persisted relationships for ${hydration.databaseName}.${hydration.tableName}`, error);
721
+ }
722
+ }
723
+ }
724
+ }
725
+ const resolveTargetGlobally = (target) => exports.databases[target.database]?.[target.table];
726
+ function hydrateTableRelationships({ table, databaseName, tableName, definitions }, resolveTarget = resolveTargetGlobally, announce = true) {
727
+ const hydratable = [];
728
+ for (let index = 0; index < definitions.length; index++) {
729
+ const definition = definitions[index];
730
+ // Keyed by name rather than list position, so a reordered list cannot inherit the previous
731
+ // occupant's reported state and swallow a different relationship's failure — and by reason, so
732
+ // hydrating one entry does not clear the report of a same-named invalid duplicate.
733
+ const errorKey = `${databaseName}.${tableName}:${definition?.name || `#${index}`}`;
734
+ if (!validRelationshipDefinition(definition, definitions, index)) {
735
+ reportRelationshipError(`${errorKey}:invalid`, `Ignoring invalid persisted relationship ${databaseName}.${tableName}[${index}]`);
736
+ continue;
737
+ }
738
+ // a live schema attribute of the same name owns the name; the catalog copy is only a stand-in
739
+ // for threads that never loaded the schema
740
+ if (table.attributes.some((attribute) => attribute.name === definition.name && !attribute[CATALOG_RELATIONSHIP]))
741
+ continue;
742
+ const targetTable = resolveTarget(definition.target);
743
+ if (!targetTable || !relationshipFieldsExist(table, targetTable, definition)) {
744
+ reportRelationshipError(`${errorKey}:unavailable`, `Unable to hydrate persisted relationship ${databaseName}.${tableName}.${definition.name}: target or foreign key is unavailable`);
745
+ continue;
746
+ }
747
+ reportedRelationshipErrors.delete(`${errorKey}:unavailable`);
748
+ hydratable.push({ definition, targetTable });
749
+ }
750
+ const installed = table.attributes.filter((attribute) => attribute[CATALOG_RELATIONSHIP]);
751
+ if (installed.length === hydratable.length &&
752
+ hydratable.every(({ definition, targetTable }, index) => relationshipEquals(installed[index], definition) &&
753
+ (installed[index].definition || installed[index].elements?.definition)?.tableClass === targetTable))
754
+ return;
755
+ const attributes = table.attributes.filter((attribute) => !attribute[CATALOG_RELATIONSHIP]);
756
+ for (const { definition, targetTable } of hydratable)
757
+ attributes.push(createCatalogRelationship(definition, targetTable));
758
+ table.attributes.splice(0, table.attributes.length, ...attributes);
759
+ table.schemaVersion++;
760
+ table.updatedAttributes();
761
+ if (announce)
762
+ exports.databaseEventsEmitter.emit('updateTable', table);
763
+ }
764
+ function validRelationshipDefinition(definition, definitions, index) {
765
+ if (!definition || typeof definition !== 'object')
766
+ return false;
767
+ const validName = (value) => typeof value === 'string' && value.length > 0 && !/[`/]/.test(value);
768
+ if (!validName(definition.name) || !validName(definition.type))
769
+ return false;
770
+ if (!validName(definition.target?.database) || !validName(definition.target?.table))
771
+ return false;
772
+ if (!definition.relationship || typeof definition.relationship !== 'object')
773
+ return false;
774
+ const { from, to, filterMissing } = definition.relationship;
775
+ if (from !== undefined && !validName(from))
776
+ return false;
777
+ if (to !== undefined && !validName(to))
778
+ return false;
779
+ if (!from && !to)
780
+ return false;
781
+ if (filterMissing !== undefined && typeof filterMissing !== 'boolean')
782
+ return false;
783
+ if (definition.type === 'array' ? !validName(definition.elements?.type) : definition.elements !== undefined)
784
+ return false;
785
+ for (let earlier = 0; earlier < index; earlier++)
786
+ if (definitions[earlier]?.name === definition.name)
787
+ return false;
788
+ return true;
789
+ }
790
+ function relationshipFieldsExist(sourceTable, targetTable, definition) {
791
+ if (definition.relationship.from &&
792
+ !sourceTable.attributes.some((attribute) => attribute.name === definition.relationship.from))
793
+ return false;
794
+ if (definition.relationship.to &&
795
+ !targetTable.attributes.some((attribute) => attribute.name === definition.relationship.to))
796
+ return false;
797
+ return true;
798
+ }
799
+ function createCatalogRelationship(definition, targetTable) {
800
+ const attribute = {
801
+ name: definition.name,
802
+ attribute: definition.name,
803
+ type: definition.type,
804
+ relationship: { ...definition.relationship },
805
+ target: { ...definition.target },
806
+ };
807
+ const targetDefinition = {
808
+ tableClass: targetTable,
809
+ type: targetTable.tableName,
810
+ attributes: targetTable.attributes,
811
+ };
812
+ if (definition.elements) {
813
+ attribute.elements = { type: definition.elements.type };
814
+ Object.defineProperty(attribute.elements, 'definition', { value: targetDefinition, configurable: true });
815
+ }
816
+ else {
817
+ Object.defineProperty(attribute, 'definition', { value: targetDefinition, configurable: true });
818
+ }
819
+ Object.defineProperty(attribute, CATALOG_RELATIONSHIP, { value: true });
820
+ return attribute;
821
+ }
822
+ function reportRelationshipError(key, message) {
823
+ if (reportedRelationshipErrors.has(key))
824
+ return;
825
+ reportedRelationshipErrors.add(key);
826
+ logger.error(message);
827
+ }
575
828
  /**
576
829
  * Scan a databases directory's entries for restore lock/marker files and return the names of
577
830
  * databases that must not be loaded: a held restore lock means a restore is in progress in some
@@ -612,14 +865,14 @@ function readMetaDb(path, defaultTable, databaseName = DEFAULT_DATABASE_NAME, au
612
865
  lmdbDatabaseEnvs.set(path, rootStore);
613
866
  }
614
867
  rootStore.dbisDb?.resetReadTxn();
615
- return initStores(path, rootStore, databaseName, defaultTable, auditPath, isLegacy);
868
+ return initStores(path, rootStore, databaseName, { defaultTable, auditPath, isLegacy });
616
869
  }
617
870
  catch (error) {
618
871
  error.message += ` opening database ${path}`;
619
872
  throw error;
620
873
  }
621
874
  }
622
- function readRocksMetaDb(path, defaultTable, databaseName = DEFAULT_DATABASE_NAME) {
875
+ function readRocksMetaDb(path, defaultTable, databaseName = DEFAULT_DATABASE_NAME, { destination, storeName, openedStores } = {}) {
623
876
  try {
624
877
  logger.trace(`loading rocksdb database: ${path}`);
625
878
  if (process.env.HARPER_PARENT_PROCESS_PID) {
@@ -630,14 +883,18 @@ function readRocksMetaDb(path, defaultTable, databaseName = DEFAULT_DATABASE_NAM
630
883
  }
631
884
  let rootStore = rocksdbDatabaseEnvs.get(path);
632
885
  if (rootStore) {
633
- initStores(path, rootStore, databaseName, defaultTable);
886
+ initStores(path, rootStore, databaseName, { defaultTable, destination, storeName, openedStores });
634
887
  }
635
888
  else {
636
889
  rootStore = openRocksDatabase(path, { disableWAL: false, enableStats: true });
637
890
  rocksdbDatabaseEnvs.set(path, rootStore);
638
- initStores(path, rootStore, databaseName, defaultTable);
639
- // Skip transaction log replay in read-only mode
640
- if (!isReadOnlyMode()) {
891
+ initStores(path, rootStore, databaseName, { defaultTable, destination, storeName, openedStores });
892
+ // A branch (`destination`) recovers its transaction-log tail in `openOrCreate`
893
+ // (branchDatabase.ts), not here: the branch claim elects exactly one replaying thread —
894
+ // applications load on workers, where this call would be a no-op — and awaits the replay
895
+ // before the branch is published to any reader. See the contract note on
896
+ // `openBranchDatabase` (harper#643).
897
+ if (!isReadOnlyMode() && !destination) {
641
898
  (0, replayLogs_ts_1.replayLogs)(rootStore, exports.databases[databaseName]);
642
899
  }
643
900
  }
@@ -648,7 +905,9 @@ function readRocksMetaDb(path, defaultTable, databaseName = DEFAULT_DATABASE_NAM
648
905
  throw error;
649
906
  }
650
907
  }
651
- function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLegacy) {
908
+ function initStores(path, rootStore, databaseName, { defaultTable, auditPath, isLegacy, destination, storeName, openedStores } = {}) {
909
+ // a store with no tables never reaches the per-table loop below, and blob roots resolve from this
910
+ rootStore.databaseName = storeName ?? databaseName;
652
911
  const envInit = new OpenEnvironmentObject_ts_1.default(path, isReadOnlyMode());
653
912
  const internalDbiInit = createOpenDBIObject(false);
654
913
  let attributesDbi = rootStore.dbisDb;
@@ -663,6 +922,7 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
663
922
  else {
664
923
  attributesDbi = rootStore.openDB(terms_ts_1.INTERNAL_DBIS_NAME, internalDbiInit);
665
924
  }
925
+ openedStores?.push(attributesDbi);
666
926
  rootStore.dbisDb = markInternalDbiNonVersioned(attributesDbi);
667
927
  }
668
928
  let auditStore = rootStore.auditStore;
@@ -683,13 +943,18 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
683
943
  });
684
944
  }
685
945
  auditStore.isLegacy = true;
946
+ // A legacy standalone audit root skips openAuditStore, so give it a floor here or it
947
+ // reports its retention horizon as permanently unknown.
948
+ (0, auditStore_ts_1.establishAuditFloor)(auditStore);
686
949
  }
687
950
  }
688
951
  else {
689
952
  auditStore = (0, auditStore_ts_1.openAuditStore)(rootStore);
690
953
  }
691
954
  }
692
- const tables = ensureDB(databaseName);
955
+ const tables = destination ?? ensureDB(databaseName);
956
+ if (destination && !destination[DEFINED_TABLES])
957
+ destination[DEFINED_TABLES] = new Set();
693
958
  const definedTables = tables[DEFINED_TABLES];
694
959
  definedTables.rootStore = rootStore;
695
960
  const tablesToLoad = new Map();
@@ -788,14 +1053,26 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
788
1053
  }
789
1054
  }
790
1055
  if (!primaryAttribute) {
791
- logger.warn(`Unable to find a primary key attribute on table ${tableName}, with attributes: ${JSON.stringify(attributes)}`);
1056
+ const tableKey = `${databaseName}/${tableName}`;
1057
+ if (reportedIncompleteCatalogs.has(tableKey))
1058
+ logger.debug(`Skipping table ${databaseName}.${tableName}: still no primary key row`);
1059
+ else {
1060
+ reportedIncompleteCatalogs.add(tableKey);
1061
+ logger.warn(`Skipping table ${databaseName}.${tableName}: its catalog has attribute rows (${attributes.map((attribute) => attribute.name).join(', ')}) but no primary key row - a create in progress on another thread, or an interrupted one that re-running create_table repairs`);
1062
+ }
1063
+ // not defined until it loads, so the cleanup pass evicts a class left from a dropped same-name table
1064
+ definedTables?.delete(tableName);
792
1065
  continue;
793
1066
  }
794
1067
  }
1068
+ if (reportedIncompleteCatalogs.size)
1069
+ reportedIncompleteCatalogs.delete(`${databaseName}/${tableName}`);
795
1070
  // if the table has already been defined, use that class, don't create a new one
796
1071
  let table = tables[tableName];
797
1072
  // unless its store was migrated to a different engine (e.g. LMDB to RocksDB on startup)
798
1073
  const recreateForEngineChange = !!table && table.primaryStore?.rootStore instanceof rocksdb_js_1.RocksDatabase !== rootStore instanceof rocksdb_js_1.RocksDatabase;
1074
+ const recreateForTableIdChange = !!table && primaryAttribute.tableId != null && table.tableId !== primaryAttribute.tableId;
1075
+ const recreateTable = recreateForEngineChange || recreateForTableIdChange;
799
1076
  let indices = {}, existingAttributes = [];
800
1077
  let tableId;
801
1078
  let primaryStore;
@@ -807,7 +1084,9 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
807
1084
  const cacheControl = primaryAttribute.cacheControl;
808
1085
  const splitSegments = primaryAttribute.splitSegments;
809
1086
  const replicate = primaryAttribute.replicate;
810
- if (table && !recreateForEngineChange) {
1087
+ if (table && !recreateTable) {
1088
+ if (primaryAttribute.audit === true && table.audit !== true)
1089
+ table.enableAuditing();
811
1090
  indices = table.indices;
812
1091
  existingAttributes = table.attributes;
813
1092
  table.schemaVersion++;
@@ -837,13 +1116,13 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
837
1116
  // per-table override of the storage.randomAccessFields default (see OpenDBIObject)
838
1117
  if (typeof primaryAttribute.randomAccessFields === 'boolean')
839
1118
  dbiInit.randomAccessStructure = primaryAttribute.randomAccessFields;
840
- if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
841
- primaryStore = (0, RecordEncoder_ts_1.handleLocalTimeForGets)(openRocksDatabase(rootStore.path, { ...dbiInit, name: primaryAttribute.key, cache: true }), rootStore);
842
- }
843
- else {
844
- primaryStore = (0, RecordEncoder_ts_1.handleLocalTimeForGets)(rootStore.openDB(primaryAttribute.key, dbiInit), rootStore);
845
- }
846
- rootStore.databaseName = databaseName;
1119
+ // recorded before the wrapper below, which is the only thing between the native open and
1120
+ // the only list a failed open can release it from
1121
+ const opened = rootStore instanceof rocksdb_js_1.RocksDatabase
1122
+ ? openRocksDatabase(rootStore.path, { ...dbiInit, name: primaryAttribute.key, cache: true })
1123
+ : rootStore.openDB(primaryAttribute.key, dbiInit);
1124
+ openedStores?.push(opened);
1125
+ primaryStore = (0, RecordEncoder_ts_1.handleLocalTimeForGets)(opened, rootStore);
847
1126
  primaryStore.tableId = tableId;
848
1127
  }
849
1128
  let attributesUpdated;
@@ -854,6 +1133,7 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
854
1133
  if (!attribute.isPrimaryKey && (attribute.indexed || (attribute.attribute && !attribute.name))) {
855
1134
  if (!indices[attribute.name]) {
856
1135
  const dbi = openIndex(attribute.key, rootStore, attribute);
1136
+ openedStores?.push(dbi);
857
1137
  indices[attribute.name] = dbi;
858
1138
  indices[attribute.name].indexNulls = attribute.indexNulls;
859
1139
  }
@@ -918,14 +1198,18 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
918
1198
  existingAttributes.splice(existingAttributes.indexOf(existingAttribute), 1);
919
1199
  attributesUpdated = true;
920
1200
  }
921
- if (table && !recreateForEngineChange) {
1201
+ if (table && !recreateTable) {
922
1202
  if (attributesUpdated) {
923
1203
  table.schemaVersion++;
924
1204
  table.updatedAttributes();
925
1205
  }
926
1206
  }
927
1207
  else {
1208
+ if (recreateForTableIdChange)
1209
+ table.cleanup();
928
1210
  table = setTable(tables, tableName, (0, Table_ts_1.makeTable)({
1211
+ // A branch builds into a caller-owned destination; its tables must refuse DDL.
1212
+ isBranch: Boolean(destination),
929
1213
  primaryStore,
930
1214
  auditStore,
931
1215
  audit,
@@ -947,11 +1231,391 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
947
1231
  dbisDB: attributesDbi,
948
1232
  }));
949
1233
  table.schemaVersion = 1;
950
- exports.databaseEventsEmitter.emit('updateTable', table);
1234
+ if (!destination)
1235
+ exports.databaseEventsEmitter.emit('updateTable', table);
1236
+ }
1237
+ void table.derivedIndexRuntime?.close();
1238
+ table.derivedIndexRuntime = (0, hnswDerivedIndex_ts_1.attachDerivedIndexes)(table);
1239
+ if (Array.isArray(primaryAttribute.relationships)) {
1240
+ relationshipsToHydrate.push({ table, databaseName, tableName, definitions: primaryAttribute.relationships });
1241
+ }
1242
+ else if (primaryAttribute.relationships !== undefined) {
1243
+ reportRelationshipError(`${databaseName}.${tableName}:list`, `Ignoring invalid persisted relationship list for ${databaseName}.${tableName}`);
1244
+ relationshipsToHydrate.push({ table, databaseName, tableName, definitions: [] });
951
1245
  }
952
1246
  }
953
1247
  return rootStore;
954
1248
  }
1249
+ /**
1250
+ * Branch directories live beside the base database's own storage root, never under the HDB root: a
1251
+ * database can be placed on its own volume, and `createCheckpoint` only hardlinks when source and
1252
+ * target share a filesystem — off-volume it degrades to a full byte copy, which is the property the
1253
+ * whole feature rests on.
1254
+ *
1255
+ * The backticks are what make the name reserved rather than merely conventional: `schemaRegex`
1256
+ * (validation/common_validators.ts) excludes 0x60, so no database can ever be created under this
1257
+ * name and shadow the branch root -- the same protection RESTORE_META_DIR uses.
1258
+ */
1259
+ exports.BRANCH_ROOT_DIR = '`branches`';
1260
+ /**
1261
+ * Where the branch of `baseName` belonging to `appName` lives. Derived only from those two names, so
1262
+ * every node in a cluster resolves the same application's branch to the same place — the identity an
1263
+ * application's data needs if it is to be addressed, and eventually replicated, cluster-wide.
1264
+ *
1265
+ * App and database are separate path segments: joining them (`<app>__<db>`) is not injective —
1266
+ * `(a__b, c)` and `(a, b__c)` collide — so two declarations could otherwise open one directory.
1267
+ */
1268
+ function resolveBranchPath(baseName, appName) {
1269
+ for (const [label, segment] of [
1270
+ ['application', appName],
1271
+ ['database', baseName],
1272
+ ]) {
1273
+ if (!segment || segment.includes('/') || segment.includes('\\') || segment === '.' || segment === '..') {
1274
+ throw new Error(`Invalid ${label} name for a branch path: ${JSON.stringify(segment)}`);
1275
+ }
1276
+ }
1277
+ return (0, node_path_1.join)(resolveDatabaseStorageRoot(baseName), exports.BRANCH_ROOT_DIR, appName, baseName);
1278
+ }
1279
+ /** `undefined` marks a path reserved by an open still in flight, which owns it just as firmly. */
1280
+ const openBranches = new Map();
1281
+ /** Store identities in use, so two branches cannot resolve one set of blob roots. */
1282
+ const openBranchIdentities = new Set();
1283
+ /**
1284
+ * Materialization renames its clone in from `<blobRoot>.staging`, so a branch owns two database
1285
+ * names rather than one: a database legally called `<storeName>.staging` resolves its own blob root
1286
+ * to exactly the path the clone removes and renames over. Every check, reservation and release
1287
+ * covers the pair, so the name cannot be claimed at any point where a branch operation may still
1288
+ * delete what it resolves to.
1289
+ */
1290
+ const BRANCH_STAGING_SUFFIX = '.staging';
1291
+ /**
1292
+ * Suffix of the sibling a branch is renamed to while being removed. A backtick, not a dot:
1293
+ * `schemaRegex` excludes 0x60, so no database can be named such that `<db>` + this suffix is another
1294
+ * branch's directory (with `.removing`, an application branching both `data` and `data.removing`, both
1295
+ * legal names, would destroy one by opening the other).
1296
+ */
1297
+ exports.BRANCH_REMOVING_SUFFIX = '`removing`';
1298
+ function branchIdentityPair(storeName) {
1299
+ return [storeName, storeName + BRANCH_STAGING_SUFFIX];
1300
+ }
1301
+ /**
1302
+ * Identities whose blob roots outlived the branch that owned them, because a removal or an abandoned
1303
+ * materialization could not delete them. A database created under such a name would resolve its own
1304
+ * fresh file ids onto files it never wrote, so the name stays refused -- but only against DATABASES.
1305
+ * The branch itself may take it back: materializing it replaces those roots wholesale, which is the
1306
+ * only route that clears the condition without an operator.
1307
+ */
1308
+ const quarantinedBranchIdentities = new Set();
1309
+ function quarantineBranchIdentity(storeName) {
1310
+ for (const name of branchIdentityPair(storeName)) {
1311
+ quarantinedBranchIdentities.add(name);
1312
+ openBranchIdentities.delete(name);
1313
+ }
1314
+ }
1315
+ /**
1316
+ * True when `dbPath` is a directory an open branch owns. The database scan opens any directory that
1317
+ * holds CURRENT + MANIFEST-*, and harper#643 places a branch inside the directory it walks, so
1318
+ * without this a rescan would rebuild the branch's tables into the global map, overwrite the store
1319
+ * identity its blob roots resolve from, and hand its store to `closeLoadedDatabases`.
1320
+ */
1321
+ function isOpenBranchPath(dbPath) {
1322
+ if (openBranches.size === 0)
1323
+ return false;
1324
+ // the literal path first: `rocksdbDatabaseEnvs` is keyed by it too, so a directory unlinked under
1325
+ // a live branch handle (realpathSync then throws) must not read as unowned
1326
+ if (openBranches.has(dbPath))
1327
+ return true;
1328
+ try {
1329
+ return openBranches.has((0, node_fs_1.realpathSync)(dbPath));
1330
+ }
1331
+ catch {
1332
+ return false;
1333
+ }
1334
+ }
1335
+ /**
1336
+ * A branch identity resolves its blob roots through `join(…, 'blobs', storeName)`, so it must be a
1337
+ * single path segment: `schemaRegex`, which every other database name is validated against, plus the
1338
+ * dot segments and backslash that regex permits but a path component must not be.
1339
+ */
1340
+ function assertLegalBranchName(name, description) {
1341
+ if (!name ||
1342
+ name.length > common_validators_ts_1.commonValidators.schema_length.maximum ||
1343
+ !common_validators_ts_1.schemaRegex.test(name) ||
1344
+ name.includes('\\') ||
1345
+ name === '.' ||
1346
+ name === '..') {
1347
+ throw new Error(`Cannot use '${name}' as a branch ${description}: it is not a legal database name`);
1348
+ }
1349
+ }
1350
+ /**
1351
+ * Refuse a branch store identity that something else already answers to.
1352
+ *
1353
+ * `storeName` picks the branch's blob roots, and blob file ids restart from each store's own counter,
1354
+ * so two holders of one identity write the same file paths and truncate each other. It must be
1355
+ * checked BEFORE anything destructive runs: materialization removes and replaces the blob root that
1356
+ * this name resolves to, and a real database may legally be called `5_myapp__data` -- `schemaRegex`
1357
+ * permits digits, `_` and `.`. The `.staging` sibling materialization writes is covered too, since a
1358
+ * database may legally carry that name as well.
1359
+ */
1360
+ function assertBranchIdentityAvailable(storeName) {
1361
+ // The on-disk scan, not just the in-memory maps: a database that exists on disk but has not been
1362
+ // loaded is absent from both, and it owns the blob root this identity would destroy.
1363
+ getDatabases();
1364
+ for (const name of branchIdentityPair(storeName)) {
1365
+ // The directory as well as the maps. `getDatabases` skips a database blocked by restore, so an
1366
+ // in-memory check alone reports its name as free while its blob root is very much real -- and
1367
+ // materialization would then remove and replace it.
1368
+ if (exports.databases[name] ||
1369
+ definedDatabases?.has(name) ||
1370
+ openBranchIdentities.has(name) ||
1371
+ (0, node_fs_1.existsSync)(resolveDatabasePath(name)) ||
1372
+ anotherBranchOwns(name, storeName)) {
1373
+ throw new Error(`Cannot use '${storeName}' as a branch store identity: '${name}' is already in use`);
1374
+ }
1375
+ }
1376
+ }
1377
+ /**
1378
+ * Does a branch OTHER than the one being opened already answer to this name on disk? `.staging` is
1379
+ * what makes the question two-sided: `<identity>.staging` is both the path a clone renames over and
1380
+ * a legal identity for a branch of a database literally named `<base>.staging`, so each of the pair
1381
+ * can belong to somebody else. Only the primary name read as itself is excluded -- that directory is
1382
+ * the very branch this call is opening.
1383
+ */
1384
+ function anotherBranchOwns(name, storeName) {
1385
+ if (name !== storeName)
1386
+ return branchDirectoryExistsFor(name);
1387
+ return name.endsWith(BRANCH_STAGING_SUFFIX)
1388
+ ? branchDirectoryExistsFor(name.slice(0, -BRANCH_STAGING_SUFFIX.length))
1389
+ : false;
1390
+ }
1391
+ /**
1392
+ * Claim the identity as well as checking it, so the window between the check and the branch actually
1393
+ * opening cannot be filled by a concurrent create or a second branch. `releaseBranchIdentity` hands
1394
+ * it back if materialization never gets as far as opening.
1395
+ */
1396
+ function reserveBranchIdentity(storeName) {
1397
+ assertBranchIdentityAvailable(storeName);
1398
+ retakeBranchIdentity(storeName);
1399
+ }
1400
+ /**
1401
+ * Take the pair back for an operation that owned it a statement ago -- cleanup, which has to keep
1402
+ * holding the names through the deletions its `close()` just released them for. Deliberately without
1403
+ * the availability check: nothing can have taken a name the caller held until now, and the check runs
1404
+ * the database scan, which at that exact moment would find the branch directory unowned.
1405
+ */
1406
+ function retakeBranchIdentity(storeName) {
1407
+ for (const name of branchIdentityPair(storeName)) {
1408
+ openBranchIdentities.add(name);
1409
+ quarantinedBranchIdentities.delete(name);
1410
+ }
1411
+ }
1412
+ function releaseBranchIdentity(storeName) {
1413
+ for (const name of branchIdentityPair(storeName))
1414
+ openBranchIdentities.delete(name);
1415
+ }
1416
+ /** Is this name spoken for by a branch? Database creation has to refuse it -- they share a blob root. */
1417
+ function isBranchIdentity(name) {
1418
+ if (openBranchIdentities.has(name) || quarantinedBranchIdentities.has(name))
1419
+ return true;
1420
+ // The in-memory set covers only branches open in THIS process, so after a restart -- or for an
1421
+ // application that is simply not loaded -- a database could take the name of an on-disk branch and
1422
+ // share its blob root. The staging sibling goes through the same route, because it names the path
1423
+ // materialization renames over -- but BOTH readings of a name ending in `.staging` have to be
1424
+ // tried: `schemaRegex` permits `.`, so `4_myapp__data.staging` is either the sibling of a branch of
1425
+ // `data` or a branch of a database actually called `data.staging`.
1426
+ if (branchDirectoryExistsFor(name))
1427
+ return true;
1428
+ return name.endsWith(BRANCH_STAGING_SUFFIX)
1429
+ ? branchDirectoryExistsFor(name.slice(0, -BRANCH_STAGING_SUFFIX.length))
1430
+ : false;
1431
+ }
1432
+ /**
1433
+ * Is there a branch directory answering to this store identity? The identity carries the application
1434
+ * name's length precisely so it can be taken apart again without guessing where the name ends.
1435
+ */
1436
+ function branchDirectoryExistsFor(storeName) {
1437
+ const prefix = /^(\d+)_/.exec(storeName);
1438
+ if (!prefix)
1439
+ return false;
1440
+ const appLength = Number(prefix[1]);
1441
+ const appName = storeName.slice(prefix[0].length, prefix[0].length + appLength);
1442
+ if (appName.length !== appLength ||
1443
+ storeName.slice(prefix[0].length + appLength, prefix[0].length + appLength + 2) !== '__')
1444
+ return false;
1445
+ const baseName = storeName.slice(prefix[0].length + appLength + 2);
1446
+ if (!baseName)
1447
+ return false;
1448
+ try {
1449
+ const branchPath = resolveBranchPath(baseName, appName);
1450
+ return (0, node_fs_1.existsSync)(branchPath) || (0, node_fs_1.existsSync)(branchPath + exports.BRANCH_REMOVING_SUFFIX);
1451
+ }
1452
+ catch {
1453
+ // Not a name a branch path could hold, so no branch owns it.
1454
+ return false;
1455
+ }
1456
+ }
1457
+ /**
1458
+ * Open a RocksDB directory as a **scope-private** database: its Table classes are built into an
1459
+ * object the caller owns and nothing is registered in the global `databases` map, so no enumerator
1460
+ * of that map — analytics, `describe_all`, worker teardown, replication — can observe it.
1461
+ *
1462
+ * `databaseName` is the *logical* name the application knows (`data`), so its schema and code need
1463
+ * no changes. `storeName` is the branch's own identity and is what `getRootBlobPathsForDB` resolves
1464
+ * blob directories from, which is how a branch gets its own blob roots rather than writing into the
1465
+ * base's.
1466
+ *
1467
+ * The caller owns the returned handle; the only thing that closes it on the caller's behalf is
1468
+ * `closeBranchDatabases`, run by an exiting job worker (via `closeLoadedDatabases`) and by an HTTP
1469
+ * worker's shutdown path, so a branch left open on an exiting worker does not linger in the
1470
+ * process-global RocksDB registry.
1471
+ *
1472
+ * Schema changes reach a branch only through its own bound factory (`scopedTableFactory`): a
1473
+ * declaration re-asserted against the branch's store. A branch's Table classes carry the base's
1474
+ * logical name, so the Table statics (`dropTable()`, `addAttributes()`) — which resolve the global
1475
+ * schema by that name and would act on the live base table — stay refused (`assertSchemaMutable`).
1476
+ *
1477
+ * A branch's blob roots are a hard-link clone of the base's, taken with the checkpoint, so a row
1478
+ * whose blob predates the branch reads back normally and the branch allocates new file ids in its own
1479
+ * directory (harper#644).
1480
+ *
1481
+ * A branch is the checkpoint's SST content plus its own transaction-log tail. This function opens
1482
+ * only the stores; replaying the tail is `openOrCreate`'s job (branchDatabase.ts), where the
1483
+ * cross-thread claim elects exactly one replayer and awaits it before any thread may open the
1484
+ * branch — the same recovery contract a base database gets at boot, without which a process that
1485
+ * died unflushed silently rewinds the branch to its last memtable flush (harper#643).
1486
+ *
1487
+ * Pass `blobRoots` to pin the handle to the roots the branch was published with; without it the
1488
+ * store resolves them from current configuration, which is only right for a branch being created.
1489
+ */
1490
+ function openBranchDatabase(path, databaseName, storeName, blobRoots) {
1491
+ assertLegalBranchName(databaseName, 'logical database name');
1492
+ assertLegalBranchName(storeName, 'store identity');
1493
+ if (!(0, node_fs_1.existsSync)(path))
1494
+ throw new Error(`Cannot open branch database: no directory at ${path}`);
1495
+ // the guards compare against env-map keys, so two spellings of one directory must not read as two
1496
+ path = (0, node_fs_1.realpathSync)(path);
1497
+ // FIRST: the guards below read the registry, and loading is itself what populates
1498
+ // `rocksdbDatabaseEnvs`. Claiming the path ahead of this scan would make the scan skip it, which
1499
+ // also means a directory that IS a real database no longer reads as one — so the pre-open window
1500
+ // where the scan can adopt a branch directory stays open, by choice (harper#643).
1501
+ getDatabases();
1502
+ // a rival graph over one shared root store; the two callers would disagree about who may close it
1503
+ if (openBranches.has(path))
1504
+ throw new Error(`Branch database at ${path} is already open`);
1505
+ // a loaded database's store is closed by `closeLoadedDatabases`, so adopting it would mean this
1506
+ // handle's `close()` tears down a live database
1507
+ if (rocksdbDatabaseEnvs.has(path))
1508
+ throw new Error(`Cannot branch ${path}: it is already open as a database`);
1509
+ assertBranchIdentityAvailable(storeName);
1510
+ const tables = Object.create(null);
1511
+ // initStores opens a table's column families well before `setTable` publishes it into `tables`,
1512
+ // so the graph is not a complete record of what a failed open must release
1513
+ const openedStores = [];
1514
+ // The boot-time hydration pass has already run by the time a branch opens, so anything this open
1515
+ // queues would never be drained. It is handed to the caller instead, which is the only place that
1516
+ // knows the application's other branches and can therefore resolve targets without leaking to base.
1517
+ const queuedRelationshipsAt = relationshipsToHydrate.length;
1518
+ let rootStore;
1519
+ // claim the path before the open, not after: readRocksMetaDb registers the store in
1520
+ // `rocksdbDatabaseEnvs` partway through, so anything re-entering `database()` during initStores
1521
+ // would otherwise find the branch's store on an unowned path
1522
+ openBranches.set(path, undefined);
1523
+ retakeBranchIdentity(storeName);
1524
+ try {
1525
+ // before the open: table load schedules TTL, eviction and audit cleanup, which ask who owns this store
1526
+ manageThreads.markBranchStorePath(path);
1527
+ rootStore = readRocksMetaDb(path, null, databaseName, { destination: tables, storeName, openedStores });
1528
+ // Pin the handle to the roots the caller proved this branch was published with, before it is
1529
+ // handed out. A row's `storageIndex` is a position in that list, so resolving through current
1530
+ // configuration instead would let an appended volume take writes at an index the branch's own
1531
+ // completion marker never recorded -- and a later change at that index would then silently
1532
+ // re-address them. `closeBranchHandles` clears the entry with the rest of the handle.
1533
+ if (blobRoots)
1534
+ blob_ts_1.databasePaths.set(rootStore, blobRoots);
1535
+ }
1536
+ catch (error) {
1537
+ openBranches.delete(path);
1538
+ manageThreads.markBranchStorePath(path, false);
1539
+ releaseBranchIdentity(storeName);
1540
+ const stranded = rocksdbDatabaseEnvs.get(path);
1541
+ rocksdbDatabaseEnvs.delete(path);
1542
+ closeBranchHandles(path, stranded, openedStores, tables);
1543
+ throw error;
1544
+ }
1545
+ let closed = false;
1546
+ const branch = {
1547
+ tables,
1548
+ rootStore,
1549
+ path,
1550
+ databaseName,
1551
+ storeName,
1552
+ openedStores,
1553
+ pendingRelationships: relationshipsToHydrate.splice(queuedRelationshipsAt),
1554
+ close() {
1555
+ // guard on the handle, not on the registrations: those are keyed by path, and a closed
1556
+ // branch frees its path, so a stale handle would otherwise tear down its successor
1557
+ if (closed)
1558
+ return;
1559
+ closed = true;
1560
+ openBranches.delete(path);
1561
+ releaseBranchIdentity(storeName);
1562
+ rocksdbDatabaseEnvs.delete(path);
1563
+ manageThreads.markBranchStorePath(path, false);
1564
+ closeBranchHandles(path, rootStore, openedStores, tables);
1565
+ },
1566
+ };
1567
+ openBranches.set(path, branch);
1568
+ return branch;
1569
+ }
1570
+ /**
1571
+ * Release everything a branch open created. Each table's primary store and each index is its own
1572
+ * column family, on top of the internal-dbis and audit families, so closing the root alone leaves
1573
+ * all of them behind — which is why `closeDatabase` walks them individually for a real database.
1574
+ * Two process-global registrations outlive the stores as well, neither with a lifetime of its own:
1575
+ * a storage-reclamation handler per store path, whose closure pins the now-closed store, and the
1576
+ * memoized blob roots in `databasePaths`. A real database is opened once per thread; harper#643
1577
+ * makes branch open/close routine, so both would grow with branch churn.
1578
+ */
1579
+ function closeBranchHandles(path, rootStore, openedStores = [], tables = {}) {
1580
+ const reclamationPaths = new Set([path]);
1581
+ rootStore?.auditStore?.stopAuditCleanup?.();
1582
+ const closeStore = (store, description) => {
1583
+ if (!store || store.status === 'closed')
1584
+ return;
1585
+ if (store.path)
1586
+ reclamationPaths.add(store.path);
1587
+ try {
1588
+ store.close?.();
1589
+ }
1590
+ catch (error) {
1591
+ logger.warn(`Error closing ${description} for branch database at ${path}`, error);
1592
+ }
1593
+ };
1594
+ // the class, before its stores: an expiration timer or a reclamation handler on a closed store
1595
+ // would otherwise keep firing against it for the life of the process
1596
+ for (const tableName in tables) {
1597
+ try {
1598
+ tables[tableName]?.cleanup?.();
1599
+ }
1600
+ catch (error) {
1601
+ logger.warn(`Error releasing table ${tableName} of branch database at ${path}`, error);
1602
+ }
1603
+ }
1604
+ for (const store of openedStores)
1605
+ closeStore(store, 'column family');
1606
+ closeStore(rootStore?.dbisDb, 'attributes store');
1607
+ closeStore(rootStore?.auditStore, 'audit store');
1608
+ closeStore(rootStore, 'root store');
1609
+ if (rootStore)
1610
+ blob_ts_1.databasePaths.delete(rootStore);
1611
+ for (const reclamationPath of reclamationPaths)
1612
+ (0, storageReclamation_ts_1.removeStorageReclamation)(reclamationPath);
1613
+ }
1614
+ /** Branches are process-local, so this is shutdown, not a data operation. */
1615
+ function closeBranchDatabases() {
1616
+ for (const branch of [...openBranches.values()])
1617
+ branch?.close();
1618
+ }
955
1619
  function resetDatabases() {
956
1620
  loadedDatabases = false;
957
1621
  for (const store of Object.values(lmdbDatabaseEnvs)) {
@@ -1022,10 +1686,10 @@ function resolveDatabaseStorageRoot(databaseName, tableName) {
1022
1686
  databaseConfig[databaseName]?.path ||
1023
1687
  process.env.STORAGE_PATH ||
1024
1688
  (0, configUtils_ts_1.getConfigPath)(hdbTerms_ts_1.CONFIG_PARAMS.STORAGE_PATH) ||
1025
- (hdbBasePath && (0, node_fs_1.existsSync)((0, path_1.join)(hdbBasePath, hdbTerms_ts_1.DATABASES_DIR_NAME))
1026
- ? (0, path_1.join)(hdbBasePath, hdbTerms_ts_1.DATABASES_DIR_NAME)
1689
+ (hdbBasePath && (0, node_fs_1.existsSync)((0, node_path_1.join)(hdbBasePath, hdbTerms_ts_1.DATABASES_DIR_NAME))
1690
+ ? (0, node_path_1.join)(hdbBasePath, hdbTerms_ts_1.DATABASES_DIR_NAME)
1027
1691
  : hdbBasePath
1028
- ? (0, path_1.join)(hdbBasePath, hdbTerms_ts_1.LEGACY_DATABASES_DIR_NAME)
1692
+ ? (0, node_path_1.join)(hdbBasePath, hdbTerms_ts_1.LEGACY_DATABASES_DIR_NAME)
1029
1693
  : undefined);
1030
1694
  if (!databasePath) {
1031
1695
  throw new Error(`Unable to determine database storage path. Ensure STORAGE_PATH, HDB_ROOT, or a valid config path is set.`);
@@ -1036,7 +1700,7 @@ function resolveDatabaseStorageRoot(databaseName, tableName) {
1036
1700
  * Resolve the directory path of a RocksDB database (whether or not it exists or is loaded).
1037
1701
  */
1038
1702
  function resolveDatabasePath(databaseName) {
1039
- return (0, path_1.join)(resolveDatabaseStorageRoot(databaseName), databaseName);
1703
+ return (0, node_path_1.join)(resolveDatabaseStorageRoot(databaseName), databaseName);
1040
1704
  }
1041
1705
  /**
1042
1706
  * Get root store for a database
@@ -1061,7 +1725,15 @@ function database({ database: databaseName, table: tableName }) {
1061
1725
  let rootStore;
1062
1726
  const useRocksdb = (process.env.HARPER_STORAGE_ENGINE || (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.STORAGE_ENGINE)) !== 'lmdb';
1063
1727
  if (useRocksdb) {
1064
- const path = (0, path_1.join)(databasePath, tablePath ? tableName : databaseName);
1728
+ const path = (0, node_path_1.join)(databasePath, tablePath ? tableName : databaseName);
1729
+ // the scan is not the only way to reach a branch's directory: a branch leaves its store in
1730
+ // `rocksdbDatabaseEnvs`, so without this an on-demand open would staple it onto
1731
+ // `definedDatabases` and the next `closeDatabase` would close it under the live handle
1732
+ if (isOpenBranchPath(path)) {
1733
+ const error = new Error(`Database '${databaseName}' is open as a scope-private branch`);
1734
+ error.statusCode = 409;
1735
+ throw error;
1736
+ }
1065
1737
  rootStore = rocksdbDatabaseEnvs.get(path);
1066
1738
  if (!rootStore || rootStore.status === 'closed') {
1067
1739
  // this on-demand open (create_table/create_database and friends) must not resurrect a
@@ -1076,7 +1748,7 @@ function database({ database: databaseName, table: tableName }) {
1076
1748
  }
1077
1749
  }
1078
1750
  else {
1079
- const path = (0, path_1.join)(databasePath, `${tablePath ? tableName : databaseName}.mdb`);
1751
+ const path = (0, node_path_1.join)(databasePath, `${tablePath ? tableName : databaseName}.mdb`);
1080
1752
  rootStore = lmdbDatabaseEnvs.get(path);
1081
1753
  if (!rootStore || rootStore.status === 'closed') {
1082
1754
  // TODO: validate database name
@@ -1289,8 +1961,12 @@ function closeDatabase(databaseName) {
1289
1961
  * database is closed). The `system` database is intentionally left open: it is non-enumerable here
1290
1962
  * (skipped by the loop), is never restored online, and the exiting worker may still touch the job
1291
1963
  * table during teardown. Best-effort: closing failures are swallowed inside `closeDatabase`.
1964
+ *
1965
+ * Branches are invisible to the loop below but hold handles from the same registry, so this — the
1966
+ * thread's one teardown entry point — closes them too.
1292
1967
  */
1293
1968
  function closeLoadedDatabases() {
1969
+ closeBranchDatabases();
1294
1970
  // snapshot the names first: closeDatabase() deletes from `databases` as it goes
1295
1971
  for (const databaseName of Object.keys(exports.databases)) {
1296
1972
  const dbTables = exports.databases[databaseName];
@@ -1393,24 +2069,41 @@ function openIndex(dbiKey, rootStore, attribute) {
1393
2069
  cache: isCustomObjectIndex,
1394
2070
  });
1395
2071
  dbi.rootStore = rootStore;
1396
- // Custom-index object stores (e.g. HNSW) write graph nodes via plain put() with no staged
1397
- // transaction timestamp, so their values carry no version and the PrimaryRocksDatabase
1398
- // Verification-Table cache can't track them. A versioned index initialises its encoder as a
1399
- // versioned RocksDB store (isRocksDB → metadata-prefix encode/decode) and marks it
1400
- // self-versioning, so each node gets a monotonic version the VT can extract — enabling cached,
1401
- // decode-free graph traversal. The format is resolved from the persisted attribute descriptor
1402
- // (decided once at create — see resolveIndexFormat) so every worker and reload agree on it.
1403
- if (isCustomObjectIndex && resolveIndexFormat(dbiKey, rootStore, dbi, attribute) === 'versioned') {
1404
- armVersionedIndexEncoder(dbi, rootStore);
2072
+ try {
2073
+ // Custom-index object stores (e.g. HNSW) write graph nodes via plain put() with no staged
2074
+ // transaction timestamp, so their values carry no version and the PrimaryRocksDatabase
2075
+ // Verification-Table cache can't track them. A versioned index initialises its encoder as a
2076
+ // versioned RocksDB store (isRocksDB → metadata-prefix encode/decode) and marks it
2077
+ // self-versioning, so each node gets a monotonic version the VT can extract — enabling cached,
2078
+ // decode-free graph traversal. The format is resolved from the persisted attribute descriptor
2079
+ // (decided once at create — see resolveIndexFormat) so every worker and reload agree on it.
2080
+ if (isCustomObjectIndex && resolveIndexFormat(dbiKey, rootStore, dbi, attribute) === 'versioned') {
2081
+ armVersionedIndexEncoder(dbi, rootStore);
2082
+ }
2083
+ installCustomIndex(dbi);
2084
+ }
2085
+ catch (error) {
2086
+ // the handle is not yet owned by any table, so nobody else can close it
2087
+ try {
2088
+ dbi.close();
2089
+ }
2090
+ catch { }
2091
+ throw error;
1405
2092
  }
1406
2093
  }
1407
2094
  else {
1408
2095
  dbi = rootStore.openDB(dbiKey, dbiInit);
2096
+ installCustomIndex(dbi);
1409
2097
  }
1410
- if (attribute.indexed.type) {
2098
+ function installCustomIndex(indexStore) {
2099
+ if (!attribute.indexed.type)
2100
+ return;
1411
2101
  const CustomIndex = customIndexes_ts_1.CUSTOM_INDEXES[attribute.indexed.type];
1412
2102
  if (CustomIndex) {
1413
- dbi.customIndex = new CustomIndex(dbi, attribute.indexed);
2103
+ indexStore.customIndex = new CustomIndex(indexStore, attribute.indexed);
2104
+ // derived state whose maintaining option is now off must not linger to be adopted
2105
+ // stale on a later re-enable
2106
+ indexStore.customIndex.cleanupDisabledPlane?.();
1414
2107
  }
1415
2108
  else {
1416
2109
  logger.error(`The indexing type '${attribute.indexed.type}' is unknown`);
@@ -1433,7 +2126,88 @@ function openIndex(dbiKey, rootStore, attribute) {
1433
2126
  * @param replicate
1434
2127
  */
1435
2128
  function table(tableDefinition) {
1436
- let { table: tableName, database: databaseName, expiration, eviction, scanInterval, attributes, audit, sealed, splitSegments, replicate, randomAccessFields, trackDeletes, schemaDefined, origin, description, properties, hidden, cacheControl, } = tableDefinition;
2129
+ return declareTable(GLOBAL_TARGET, tableDefinition);
2130
+ }
2131
+ const GLOBAL_TARGET = {
2132
+ rootStore: (databaseName, tableName) => database({ database: databaseName, table: tableName }),
2133
+ tables: (databaseName) => exports.databases[databaseName],
2134
+ reload: () => resetDatabases(),
2135
+ // a real database's stores live until the process (or `closeDatabase`, which walks the graph) ends
2136
+ adopt: () => { },
2137
+ };
2138
+ /**
2139
+ * The factory a branched application declares tables through: each declaration goes to the branch
2140
+ * of the database it names, or to `table()` itself for a database the application did not branch.
2141
+ * An unbranched, shared application gets `table` by identity. An isolated application still gets a
2142
+ * wrapper so its own declarations can claim their single-threaded maintenance work.
2143
+ */
2144
+ function scopedTableFactory(branches, isolatedApplicationOwner = false) {
2145
+ if (!branches?.size && !isolatedApplicationOwner)
2146
+ return table;
2147
+ return function scopedTable(tableDefinition) {
2148
+ if (isolatedApplicationOwner)
2149
+ tableDefinition = { ...tableDefinition, isolatedApplicationOwner: true };
2150
+ // `||`, not `??`: `table()` resolves every falsy name to the default database
2151
+ const branch = branches?.get(tableDefinition.database || DEFAULT_DATABASE_NAME);
2152
+ return branch ? declareTable(branchTarget(branch), tableDefinition) : table(tableDefinition);
2153
+ };
2154
+ }
2155
+ function branchTarget(branch) {
2156
+ return {
2157
+ rootStore: () => branch.rootStore,
2158
+ tables: () => branch.tables,
2159
+ reload: () => reloadBranch(branch),
2160
+ adopt: (store) => branch.openedStores.push(store),
2161
+ branch,
2162
+ };
2163
+ }
2164
+ /**
2165
+ * Re-read a branch's catalog into its `tables`: tables and indexes another thread declared since the
2166
+ * open (or since the last reload) are opened here, the same way a schema-change rescan does for a
2167
+ * real database. Existing classes are kept and their attribute lists refreshed.
2168
+ */
2169
+ function reloadBranch(branch) {
2170
+ const { rootStore, tables, databaseName, storeName, openedStores } = branch;
2171
+ const queuedRelationshipsAt = relationshipsToHydrate.length;
2172
+ try {
2173
+ initStores(rootStore.path, rootStore, databaseName, { destination: tables, storeName, openedStores });
2174
+ }
2175
+ finally {
2176
+ for (const hydration of relationshipsToHydrate.splice(queuedRelationshipsAt))
2177
+ queueBranchHydration(branch, hydration);
2178
+ }
2179
+ // Until `prepareBranches` has the whole set, a cross-database target cannot be resolved without
2180
+ // falling through to the base; it hydrates the complete set once. After that, every sibling is
2181
+ // re-hydrated: the table this reload brought in may be the target a sibling's relationship waited for.
2182
+ if (!branch.relatedBranches)
2183
+ return;
2184
+ for (const sibling of branch.relatedBranches.values())
2185
+ hydrateBranchRelationships(sibling, branch.relatedBranches);
2186
+ }
2187
+ /** One pending hydration per table: a re-declaration replaces the entry the earlier declaration queued. */
2188
+ function queueBranchHydration(branch, hydration) {
2189
+ const existing = branch.pendingRelationships.findIndex((pending) => pending.databaseName === hydration.databaseName && pending.tableName === hydration.tableName);
2190
+ if (existing >= 0)
2191
+ branch.pendingRelationships[existing] = hydration;
2192
+ else
2193
+ branch.pendingRelationships.push(hydration);
2194
+ }
2195
+ /**
2196
+ * The receiving side of a branch's schema-change signal: a thread that holds this branch open reloads
2197
+ * it, any other thread has nothing to do. Returns the branch's tables so the caller can address the
2198
+ * table the signal named.
2199
+ */
2200
+ function reloadBranchAt(path) {
2201
+ const branch = openBranches.get(path);
2202
+ if (!branch)
2203
+ return undefined;
2204
+ reloadBranch(branch);
2205
+ return branch.tables;
2206
+ }
2207
+ function declareTable(target, tableDefinition) {
2208
+ let { table: tableName, database: databaseName, expiration, eviction, scanInterval, attributes, audit, sealed, splitSegments, replicate, randomAccessFields, trackDeletes, schemaDefined, schemaRelationshipsDefined, origin, description, properties, hidden, cacheControl, isolatedApplicationOwner, } = tableDefinition;
2209
+ const auditExplicitlyEnabled = audit === true;
2210
+ const auditExplicitlyDisabled = audit === false;
1437
2211
  if (!databaseName)
1438
2212
  databaseName = DEFAULT_DATABASE_NAME;
1439
2213
  // Reject reserved names here too, not only at the operations API: a database
@@ -1446,8 +2220,14 @@ function table(tableDefinition) {
1446
2220
  if (hdbTerms_ts_1.RESERVED_DATABASE_NAMES.includes(databaseName)) {
1447
2221
  throw new hdbError_ts_1.ClientError(`'${databaseName}' is a reserved name and cannot be used as a database name`);
1448
2222
  }
1449
- const rootStore = database({ database: databaseName, table: tableName });
1450
- const tables = exports.databases[databaseName];
2223
+ // A branch resolves its blob root from its store identity, so a database created under that same
2224
+ // name would share the root: two allocators minting the same file paths and truncating each other,
2225
+ // and the branch's teardown removing the database's blobs.
2226
+ if (isBranchIdentity(databaseName)) {
2227
+ throw new hdbError_ts_1.ClientError(`'${databaseName}' is in use as a branch store identity and cannot be a database name`);
2228
+ }
2229
+ const rootStore = target.rootStore(databaseName, tableName);
2230
+ const tables = target.tables(databaseName);
1451
2231
  logger.trace(`Defining ${tableName} in ${databaseName}`);
1452
2232
  let Table = tables?.[tableName];
1453
2233
  if (rootStore.status === 'closed') {
@@ -1462,137 +2242,296 @@ function table(tableDefinition) {
1462
2242
  const schemaDefinedExplicit = tableDefinition.schemaDefined !== undefined;
1463
2243
  if (schemaDefined == undefined)
1464
2244
  schemaDefined = true;
2245
+ const relationshipDefinitions = schemaRelationshipsDefined ? normalizeRelationships(attributes) : undefined;
1465
2246
  const internalDbiInit = createOpenDBIObject(false);
1466
- for (const attribute of attributes) {
1467
- if (attribute.attribute && !attribute.name) {
1468
- // there is some legacy code that calls the attribute's name the attribute's attribute
1469
- attribute.name = attribute.attribute;
1470
- attribute.indexed = true;
2247
+ let releaseExclusiveLock;
2248
+ const hasHnswAtEntry = attributes.some((attribute) => attribute.indexed?.type === 'HNSW');
2249
+ const persistedPrimaryDescriptor = (catalog) => {
2250
+ const declaredPrimaryKey = attributes.find((attribute) => attribute.isPrimaryKey)?.name ?? Table?.primaryKey;
2251
+ if (declaredPrimaryKey) {
2252
+ const key = `${tableName}/${declaredPrimaryKey}`;
2253
+ const descriptor = catalog?.getSync(key);
2254
+ if (descriptor?.isPrimaryKey)
2255
+ return { key, descriptor };
2256
+ }
2257
+ const key = `${tableName}/`;
2258
+ return { key, descriptor: catalog?.getSync(key) };
2259
+ };
2260
+ const hasLegacyHnswStateAtEntry = Table &&
2261
+ origin !== 'cluster' &&
2262
+ attributes.some((attribute) => {
2263
+ if (attribute.indexed?.type !== 'HNSW')
2264
+ return false;
2265
+ const persisted = Table.dbisDB?.getSync(`${tableName}/${attribute.name || attribute.attribute || ''}`)?.indexed;
2266
+ if (persisted?.type !== 'HNSW')
2267
+ return false;
2268
+ if (Object.hasOwn(persisted, 'nativePlane') && typeof persisted.nativePlane !== 'boolean')
2269
+ return true;
2270
+ for (const name of customIndexes_ts_1.CUSTOM_INDEXES.HNSW.numericOptions)
2271
+ if (Object.hasOwn(persisted, name) && typeof persisted[name] !== 'number')
2272
+ return true;
2273
+ return false;
2274
+ });
2275
+ try {
2276
+ if (Table &&
2277
+ hasHnswAtEntry &&
2278
+ origin !== 'cluster' &&
2279
+ (rootStore instanceof rocksdb_js_1.RocksDatabase || hasLegacyHnswStateAtEntry))
2280
+ exclusiveLock();
2281
+ const persistedAuditAtEntry = hasHnswAtEntry || (Table && Table.audit !== true)
2282
+ ? persistedPrimaryDescriptor(Table?.dbisDB).descriptor?.audit
2283
+ : undefined;
2284
+ if (!auditExplicitlyDisabled && persistedAuditAtEntry === true && Table?.audit !== true)
2285
+ Table.enableAuditing();
2286
+ for (const attribute of attributes) {
2287
+ if (attribute.attribute && !attribute.name) {
2288
+ // there is some legacy code that calls the attribute's name the attribute's attribute
2289
+ attribute.name = attribute.attribute;
2290
+ attribute.indexed = true;
2291
+ }
2292
+ else
2293
+ attribute.attribute = attribute.name;
2294
+ if (attribute.expiresAt)
2295
+ attribute.indexed = true;
2296
+ if (attribute.indexed?.type === 'HNSW' && origin !== 'cluster') {
2297
+ const existingAttribute = Table?.attributes.find((existing) => existing.name === attribute.name);
2298
+ const persistedIndexed = Table?.dbisDB?.getSync(`${tableName}/${attribute.name || ''}`)?.indexed ?? existingAttribute?.indexed;
2299
+ customIndexes_ts_1.CUSTOM_INDEXES.HNSW.normalizeDeclarationOptions(attribute.indexed, persistedIndexed);
2300
+ if (attribute.indexed.nativePlane != null) {
2301
+ const persistedNativePlane = persistedIndexed?.nativePlane;
2302
+ const matchesPersistedLegacySpelling = persistedIndexed?.type === 'HNSW' &&
2303
+ Object.hasOwn(persistedIndexed, 'nativePlane') &&
2304
+ typeof persistedNativePlane !== 'boolean' &&
2305
+ (Object.is(persistedNativePlane, attribute.indexed.nativePlane) ||
2306
+ (typeof persistedNativePlane === 'string' &&
2307
+ typeof attribute.indexed.nativePlane === 'number' &&
2308
+ persistedNativePlane.trim() !== '' &&
2309
+ Number(persistedNativePlane) === attribute.indexed.nativePlane));
2310
+ if (matchesPersistedLegacySpelling) {
2311
+ attribute.indexed.nativePlane = persistedNativePlane;
2312
+ }
2313
+ else {
2314
+ attribute.indexed.nativePlane = customIndexes_ts_1.CUSTOM_INDEXES.HNSW.normalizeNativePlaneDeclaration(attribute.indexed.nativePlane);
2315
+ }
2316
+ }
2317
+ }
1471
2318
  }
1472
- else
1473
- attribute.attribute = attribute.name;
1474
- if (attribute.expiresAt)
1475
- attribute.indexed = true;
2319
+ const auditEnabledAtEntry = auditExplicitlyEnabled ||
2320
+ (!auditExplicitlyDisabled &&
2321
+ (persistedAuditAtEntry === true || (persistedAuditAtEntry == null && Table?.audit === true)));
2322
+ if (origin !== 'cluster' &&
2323
+ attributes.some((attribute) => {
2324
+ if (attribute.indexed?.type !== 'HNSW')
2325
+ return false;
2326
+ if (attribute.indexed.nativePlane != null)
2327
+ return Boolean(attribute.indexed.nativePlane);
2328
+ const existingAttribute = Table?.attributes.find((existing) => existing.name === attribute.name && existing.indexed?.type === 'HNSW');
2329
+ return Boolean(existingAttribute?.indexed.nativePlane);
2330
+ }) &&
2331
+ !auditEnabledAtEntry) {
2332
+ throw new hdbError_ts_1.ClientError(`Table '${databaseName}.${tableName}' must enable audit logging before using nativePlane because its transaction log is the derived-index recovery source; set nativePlane: false to use the JS index`);
2333
+ }
2334
+ }
2335
+ catch (error) {
2336
+ releaseLock();
2337
+ throw error;
1476
2338
  }
2339
+ const validateHnswOptions = (catalog, auditQualifiesDefault) => {
2340
+ for (const attribute of attributes) {
2341
+ const indexed = attribute.indexed;
2342
+ if (indexed?.type !== 'HNSW')
2343
+ continue;
2344
+ const persistedIndexed = catalog?.getSync(`${tableName}/${attribute.name || ''}`)?.indexed;
2345
+ if (indexed.nativePlane) {
2346
+ customIndexes_ts_1.CUSTOM_INDEXES.HNSW.validateNativePlaneOptions(rootStore, indexed);
2347
+ continue;
2348
+ }
2349
+ if (indexed.nativePlane != null)
2350
+ continue;
2351
+ if (persistedIndexed?.type === 'HNSW' && Object.hasOwn(persistedIndexed, 'nativePlane')) {
2352
+ if (persistedIndexed.nativePlane)
2353
+ customIndexes_ts_1.CUSTOM_INDEXES.HNSW.validateNativePlaneOptions(rootStore, {
2354
+ ...indexed,
2355
+ nativePlane: persistedIndexed.nativePlane,
2356
+ });
2357
+ continue;
2358
+ }
2359
+ if (!auditQualifiesDefault)
2360
+ continue;
2361
+ if (persistedIndexed?.type !== 'HNSW')
2362
+ customIndexes_ts_1.CUSTOM_INDEXES.HNSW.canDefaultToNativePlane(rootStore, indexed);
2363
+ }
2364
+ };
2365
+ if (!Table && origin !== 'cluster')
2366
+ validateHnswOptions(undefined, auditExplicitlyEnabled);
1477
2367
  let hasChanges;
1478
- let releaseExclusiveLock;
1479
- if (Table) {
1480
- primaryKey = Table.primaryKey;
1481
- if (Table.primaryStore.rootStore.status === 'closed') {
1482
- throw new Error(`Can not use a closed data store from ${tableName} class`);
1483
- }
1484
- // Reject moving the primary key to a different attribute on a table that already has records.
1485
- // The storage key (Table.primaryKey) is never re-pointed here, so honoring the change would
1486
- // leave describe reporting the new attribute while every record — old and newly inserted — stays
1487
- // keyed by the original one; search_by_id/update/delete by the declared key then all miss. Only
1488
- // schema-authored callers (@table / defineTable / create_table) reassert the declaration, so
1489
- // gate on schemaDefinedExplicit to leave cluster schema-replication / data-loader callers alone.
1490
- // See HarperFast/studio#1199.
1491
- const declaredPrimaryKey = attributes.find((attribute) => attribute.isPrimaryKey)?.name;
1492
- if (schemaDefinedExplicit && declaredPrimaryKey && declaredPrimaryKey !== Table.primaryKey) {
1493
- let hasRecords = false;
1494
- for (const _entry of Table.primaryStore.getRange({ start: true })) {
1495
- hasRecords = true;
1496
- break;
2368
+ let refreshRelationshipAttributes = false;
2369
+ let refreshedLiveAttributes = false;
2370
+ let deferredPrimaryRow;
2371
+ let unpublishedPrimaryStore;
2372
+ let published = false;
2373
+ const attributesToIndex = [];
2374
+ const indicesToRemove = [];
2375
+ try {
2376
+ if (Table) {
2377
+ refreshedLiveAttributes = true;
2378
+ primaryKey = Table.primaryKey;
2379
+ if (Table.primaryStore.rootStore.status === 'closed') {
2380
+ throw new Error(`Can not use a closed data store from ${tableName} class`);
1497
2381
  }
1498
- if (hasRecords) {
1499
- throw new hdbError_ts_1.ClientError(`Cannot change the primary key of table '${databaseName}.${tableName}' from '${Table.primaryKey}' to ` +
1500
- `'${declaredPrimaryKey}' because it already contains records. Recreate the table with the new primary ` +
1501
- `key, or migrate the existing records.`, 400);
1502
- }
1503
- }
1504
- // it table already exists, get the split segments setting
1505
- if (splitSegments == undefined)
1506
- splitSegments = Table.splitSegments;
1507
- Table.attributes.splice(0, Table.attributes.length, ...attributes);
1508
- // Re-assert from the live declaration so a stale value on disk (replicated event,
1509
- // v4-era backfill) is corrected on every reload. Gated on `schemaDefinedExplicit` so
1510
- // callers that omit the flag (cluster schema-replication, data loader) don't flip a
1511
- // dynamic table to true via the default at the top of table().
1512
- if (schemaDefinedExplicit)
1513
- Table.schemaDefined = schemaDefined;
1514
- // Refresh class-level schema metadata to track docstring/directive changes across reloads.
1515
- Table.description = description;
1516
- Table.properties = properties;
1517
- Table.hidden = hidden;
1518
- // undefined means a non-schema caller (add_attribute, cluster schema events) — don't clobber
1519
- if (cacheControl !== undefined)
1520
- Table.cacheControl = cacheControl;
1521
- }
1522
- else {
1523
- const auditStore = rootStore.auditStore;
1524
- primaryKeyAttribute = attributes.find((attribute) => attribute.isPrimaryKey) || {};
1525
- primaryKey = primaryKeyAttribute.name;
1526
- primaryKeyAttribute.isPrimaryKey = true;
1527
- primaryKeyAttribute.is_hash_attribute = true; // backward-compat: harperdb@4.x reads this field to open the DBI with correct flags
1528
- primaryKeyAttribute.schemaDefined = schemaDefined;
1529
- // can't change compression after the fact (except threshold), so save only when we create the table
1530
- primaryKeyAttribute.compression = getDefaultCompression();
1531
- if (trackDeletes)
1532
- primaryKeyAttribute.trackDeletes = true;
1533
- audit = primaryKeyAttribute.audit = typeof audit === 'boolean' ? audit : (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.LOGGING_AUDITLOG);
1534
- if (expiration)
1535
- primaryKeyAttribute.expiration = expiration;
1536
- if (eviction)
1537
- primaryKeyAttribute.eviction = eviction;
1538
- // persist cacheControl so all threads (and future boots) see it; undefined callers inherit
1539
- // a descriptor value carried by cluster schema events; null (schema has no directive)
1540
- // clears a stale value the carried descriptor may hold
1541
- if (cacheControl === undefined)
1542
- cacheControl = primaryKeyAttribute.cacheControl;
1543
- else if (cacheControl === null)
1544
- delete primaryKeyAttribute.cacheControl;
1545
- else
1546
- primaryKeyAttribute.cacheControl = cacheControl;
1547
- splitSegments ??= false;
1548
- primaryKeyAttribute.splitSegments = splitSegments; // always default to not splitting segments going forward
1549
- if (typeof sealed === 'boolean')
1550
- primaryKeyAttribute.sealed = sealed;
1551
- if (typeof replicate === 'boolean')
1552
- primaryKeyAttribute.replicate = replicate;
1553
- // An explicit directive PINS this table's encoding: we persist the boolean, so later changes
1554
- // to the global storage.randomAccessFields default never affect this table. Tables WITHOUT the
1555
- // directive are intentionally not persisted here — they follow the current global default on
1556
- // each open (a runtime lever to flip encoding fleet-wide). Switching either way is safe: the
1557
- // struct READ hook always stays on and struct (0x20-0x3f) vs classic-record (0x40-0x7f) bytes
1558
- // are disjoint, so already-written records still decode; only the encoding of NEW writes changes.
1559
- if (typeof randomAccessFields === 'boolean')
1560
- primaryKeyAttribute.randomAccessFields = randomAccessFields;
1561
- if (origin) {
1562
- if (!primaryKeyAttribute.origins)
1563
- primaryKeyAttribute.origins = [origin];
1564
- else if (!primaryKeyAttribute.origins.includes(origin))
1565
- primaryKeyAttribute.origins.push(origin);
1566
- }
1567
- logger.trace(`${tableName} table loading, opening primary store`);
1568
- const dbiInit = createOpenDBIObject(false, true);
1569
- dbiInit.compression = primaryKeyAttribute.compression;
1570
- // per-table override of the storage.randomAccessFields default (see OpenDBIObject)
1571
- if (typeof primaryKeyAttribute.randomAccessFields === 'boolean')
1572
- dbiInit.randomAccessStructure = primaryKeyAttribute.randomAccessFields;
1573
- const dbiName = tableName + '/';
1574
- if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
1575
- attributesDbi = rootStore.dbisDb = openRocksDatabase(rootStore.path, {
1576
- ...internalDbiInit,
1577
- disableWAL: false,
1578
- name: terms_ts_1.INTERNAL_DBIS_NAME,
1579
- });
2382
+ // Reject moving the primary key to a different attribute on a table that already has records.
2383
+ // The storage key (Table.primaryKey) is never re-pointed here, so honoring the change would
2384
+ // leave describe reporting the new attribute while every record — old and newly inserted — stays
2385
+ // keyed by the original one; search_by_id/update/delete by the declared key then all miss. Only
2386
+ // schema-authored callers (@table / defineTable / create_table) reassert the declaration, so
2387
+ // gate on schemaDefinedExplicit to leave cluster schema-replication / data-loader callers alone.
2388
+ // See HarperFast/studio#1199.
2389
+ const declaredPrimaryKey = attributes.find((attribute) => attribute.isPrimaryKey)?.name;
2390
+ if (schemaDefinedExplicit && declaredPrimaryKey && declaredPrimaryKey !== Table.primaryKey) {
2391
+ let hasRecords = false;
2392
+ for (const _entry of Table.primaryStore.getRange({ start: true })) {
2393
+ hasRecords = true;
2394
+ break;
2395
+ }
2396
+ if (hasRecords) {
2397
+ throw new hdbError_ts_1.ClientError(`Cannot change the primary key of table '${databaseName}.${tableName}' from '${Table.primaryKey}' to ` +
2398
+ `'${declaredPrimaryKey}' because it already contains records. Recreate the table with the new primary ` +
2399
+ `key, or migrate the existing records.`, 400);
2400
+ }
2401
+ }
2402
+ // Acquire before the first mutation of the live Table below, so a lost race leaves no
2403
+ // attributes this worker describes but never persisted. Only the RocksDB acquire is bounded;
2404
+ // ordinary LMDB declarations stay lazy, while legacy HNSW normalization locks at entry.
2405
+ if (rootStore instanceof rocksdb_js_1.RocksDatabase)
2406
+ exclusiveLock();
2407
+ if (origin !== 'cluster') {
2408
+ const lockedAttributesDbi = Table.dbisDB;
2409
+ const persistedAuditUnderLock = persistedPrimaryDescriptor(lockedAttributesDbi).descriptor?.audit;
2410
+ validateHnswOptions(lockedAttributesDbi, auditExplicitlyEnabled || (!auditExplicitlyDisabled && persistedAuditUnderLock === true));
2411
+ }
2412
+ // it table already exists, get the split segments setting
2413
+ if (splitSegments == undefined)
2414
+ splitSegments = Table.splitSegments;
2415
+ if (origin === 'cluster') {
2416
+ const merged = Table.attributes.slice();
2417
+ for (const attribute of attributes) {
2418
+ const existing = merged.find((existingAttribute) => existingAttribute.name === attribute.name);
2419
+ if (!existing) {
2420
+ merged.push(attribute);
2421
+ continue;
2422
+ }
2423
+ // Nodes that apply the same peer definitions in a different order keep different index sets, and
2424
+ // this warn is the only signal of it. An absent field and an explicit falsy one declare the same
2425
+ // thing, so neither direction of that pair is a difference.
2426
+ const discarded = PEER_DECLARABLE_FIELDS.filter((field) => (attribute[field] || existing[field]) &&
2427
+ JSON.stringify(attribute[field]) !== JSON.stringify(existing[field]));
2428
+ if (discarded.length > 0)
2429
+ logger.warn(`Ignoring peer redefinition of ${databaseName}.${tableName}.${attribute.name} (${discarded
2430
+ .map((field) => `${field}: local ${JSON.stringify(existing[field])}, peer ${JSON.stringify(attribute[field])}`)
2431
+ .join('; ')}); the local schema is authoritative`);
2432
+ }
2433
+ attributes = merged;
2434
+ }
2435
+ else if (!attributes.some((attribute) => attribute.isPrimaryKey)) {
2436
+ const existingPrimary = Table.attributes.find((attribute) => attribute.isPrimaryKey);
2437
+ if (existingPrimary && attributes.some((attribute) => attribute.name === existingPrimary.name))
2438
+ throw new hdbError_ts_1.ClientError(`Cannot remove the primary key designation from '${databaseName}.${tableName}.${existingPrimary.name}'`);
2439
+ if (existingPrimary)
2440
+ attributes = [existingPrimary, ...attributes];
2441
+ }
2442
+ Table.attributes.splice(0, Table.attributes.length, ...attributes);
2443
+ // Re-assert from the live declaration so a stale value on disk (replicated event,
2444
+ // v4-era backfill) is corrected on every reload. Gated on `schemaDefinedExplicit` so
2445
+ // callers that omit the flag (cluster schema-replication, data loader) don't flip a
2446
+ // dynamic table to true via the default at the top of table(), and on origin so a
2447
+ // peer-derived definition never overrides the local declaration.
2448
+ if (schemaDefinedExplicit && origin !== 'cluster')
2449
+ Table.schemaDefined = schemaDefined;
2450
+ // Refresh class-level schema metadata to track docstring/directive changes across reloads.
2451
+ Table.description = description;
2452
+ Table.properties = properties;
2453
+ Table.hidden = hidden;
2454
+ // undefined means a non-schema caller (add_attribute, cluster schema events) — don't clobber
2455
+ if (cacheControl !== undefined)
2456
+ Table.cacheControl = cacheControl;
1580
2457
  }
1581
2458
  else {
1582
- attributesDbi = rootStore.dbisDb = rootStore.openDB(terms_ts_1.INTERNAL_DBIS_NAME, internalDbiInit);
1583
- }
1584
- markInternalDbiNonVersioned(attributesDbi);
1585
- exclusiveLock(); // get an exclusive lock on the database so we can verify that we are the only thread creating the table (and assigning the table id)
1586
- const existingTableMeta = attributesDbi.getSync(dbiName);
1587
- if (existingTableMeta && !existingTableMeta.dropping) {
1588
- // table was created while we were setting up
1589
- if (releaseExclusiveLock)
1590
- releaseExclusiveLock();
1591
- resetDatabases();
1592
- return table(tableDefinition);
1593
- }
1594
- let primaryStore;
1595
- try {
2459
+ const auditStore = rootStore.auditStore;
2460
+ primaryKeyAttribute = attributes.find((attribute) => attribute.isPrimaryKey) || {};
2461
+ primaryKey = primaryKeyAttribute.name;
2462
+ primaryKeyAttribute.isPrimaryKey = true;
2463
+ primaryKeyAttribute.is_hash_attribute = true; // backward-compat: harperdb@4.x reads this field to open the DBI with correct flags
2464
+ primaryKeyAttribute.schemaDefined = schemaDefined;
2465
+ // Old readers treat every attribute row as live schema, so relationships stay on the ignored primary descriptor.
2466
+ if (relationshipDefinitions)
2467
+ primaryKeyAttribute.relationships = relationshipDefinitions;
2468
+ // can't change compression after the fact (except threshold), so save only when we create the table
2469
+ primaryKeyAttribute.compression = getDefaultCompression();
2470
+ if (trackDeletes)
2471
+ primaryKeyAttribute.trackDeletes = true;
2472
+ audit = primaryKeyAttribute.audit = typeof audit === 'boolean' ? audit : (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.LOGGING_AUDITLOG);
2473
+ if (expiration)
2474
+ primaryKeyAttribute.expiration = expiration;
2475
+ if (eviction)
2476
+ primaryKeyAttribute.eviction = eviction;
2477
+ // persist cacheControl so all threads (and future boots) see it; undefined callers inherit
2478
+ // a descriptor value carried by cluster schema events; null (schema has no directive)
2479
+ // clears a stale value the carried descriptor may hold
2480
+ if (cacheControl === undefined)
2481
+ cacheControl = primaryKeyAttribute.cacheControl;
2482
+ else if (cacheControl === null)
2483
+ delete primaryKeyAttribute.cacheControl;
2484
+ else
2485
+ primaryKeyAttribute.cacheControl = cacheControl;
2486
+ splitSegments ??= false;
2487
+ primaryKeyAttribute.splitSegments = splitSegments; // always default to not splitting segments going forward
2488
+ if (typeof sealed === 'boolean')
2489
+ primaryKeyAttribute.sealed = sealed;
2490
+ if (typeof replicate === 'boolean')
2491
+ primaryKeyAttribute.replicate = replicate;
2492
+ // An explicit directive PINS this table's encoding: we persist the boolean, so later changes
2493
+ // to the global storage.randomAccessFields default never affect this table. Tables WITHOUT the
2494
+ // directive are intentionally not persisted here — they follow the current global default on
2495
+ // each open (a runtime lever to flip encoding fleet-wide). Switching either way is safe: the
2496
+ // struct READ hook always stays on and struct (0x20-0x3f) vs classic-record (0x40-0x7f) bytes
2497
+ // are disjoint, so already-written records still decode; only the encoding of NEW writes changes.
2498
+ if (typeof randomAccessFields === 'boolean')
2499
+ primaryKeyAttribute.randomAccessFields = randomAccessFields;
2500
+ if (origin) {
2501
+ if (!primaryKeyAttribute.origins)
2502
+ primaryKeyAttribute.origins = [origin];
2503
+ else if (!primaryKeyAttribute.origins.includes(origin))
2504
+ primaryKeyAttribute.origins.push(origin);
2505
+ }
2506
+ logger.trace(`${tableName} table loading, opening primary store`);
2507
+ const dbiInit = createOpenDBIObject(false, true);
2508
+ dbiInit.compression = primaryKeyAttribute.compression;
2509
+ // per-table override of the storage.randomAccessFields default (see OpenDBIObject)
2510
+ if (typeof primaryKeyAttribute.randomAccessFields === 'boolean')
2511
+ dbiInit.randomAccessStructure = primaryKeyAttribute.randomAccessFields;
2512
+ const dbiName = tableName + '/';
2513
+ if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
2514
+ attributesDbi = rootStore.dbisDb = openRocksDatabase(rootStore.path, {
2515
+ ...internalDbiInit,
2516
+ disableWAL: false,
2517
+ name: terms_ts_1.INTERNAL_DBIS_NAME,
2518
+ });
2519
+ }
2520
+ else {
2521
+ attributesDbi = rootStore.dbisDb = rootStore.openDB(terms_ts_1.INTERNAL_DBIS_NAME, internalDbiInit);
2522
+ }
2523
+ target.adopt(attributesDbi);
2524
+ markInternalDbiNonVersioned(attributesDbi);
2525
+ exclusiveLock(); // get an exclusive lock on the database so we can verify that we are the only thread creating the table (and assigning the table id)
2526
+ const existingTableMeta = attributesDbi.getSync(dbiName);
2527
+ if (existingTableMeta && !existingTableMeta.dropping) {
2528
+ // table was created while we were setting up; the lock is not reentrant, so release
2529
+ // before the recursive reload
2530
+ releaseLock();
2531
+ target.reload(databaseName);
2532
+ return declareTable(target, tableDefinition);
2533
+ }
2534
+ let primaryStore;
1596
2535
  if (existingTableMeta?.dropping) {
1597
2536
  // A previous drop of this table was interrupted after its tombstone
1598
2537
  // was written. Complete it now (under the exclusive lock) so the
@@ -1620,15 +2559,20 @@ function table(tableDefinition) {
1620
2559
  else {
1621
2560
  primaryStore = rootStore.openDB(dbiName, dbiInit);
1622
2561
  }
2562
+ target.adopt(primaryStore);
2563
+ unpublishedPrimaryStore = primaryStore;
1623
2564
  primaryStore = (0, RecordEncoder_ts_1.handleLocalTimeForGets)(primaryStore, rootStore);
1624
- rootStore.databaseName = databaseName;
2565
+ // only a store no table has loaded yet is unnamed; a branch's store carries its own store
2566
+ // identity here, which its blob roots resolve from, and must not take the logical name
2567
+ rootStore.databaseName ??= databaseName;
1625
2568
  primaryStore.tableId = attributesDbi.getSync(NEXT_TABLE_ID);
1626
2569
  logger.trace(`Assigning new table id ${primaryStore.tableId} for ${tableName}`);
1627
2570
  if (!primaryStore.tableId)
1628
2571
  primaryStore.tableId = 1;
1629
2572
  attributesDbi.put(NEXT_TABLE_ID, primaryStore.tableId + 1);
1630
2573
  primaryKeyAttribute.tableId = primaryStore.tableId;
1631
- Table = setTable(tables, tableName, (0, Table_ts_1.makeTable)({
2574
+ Table = (0, Table_ts_1.makeTable)({
2575
+ isBranch: Boolean(target.branch),
1632
2576
  primaryStore,
1633
2577
  auditStore,
1634
2578
  audit,
@@ -1651,96 +2595,165 @@ function table(tableDefinition) {
1651
2595
  properties,
1652
2596
  hidden,
1653
2597
  cacheControl,
1654
- }));
2598
+ });
1655
2599
  Table.schemaVersion = 1;
1656
2600
  hasChanges = true;
1657
- attributesDbi.put(dbiName, primaryKeyAttribute);
1658
- }
1659
- catch (error) {
1660
- // A failure while opening/creating the column family or writing the
1661
- // table id / catalog entry (e.g. into an env poisoned by a prior
1662
- // dangling column family) must NOT leak the exclusive
1663
- // 'update-attributes' spin lock. If it leaks, every subsequent
1664
- // create_table / attribute update on this database spins forever
1665
- // (a hard wedge that pins a worker at 100% CPU). Release before rethrow.
1666
- if (releaseExclusiveLock)
1667
- releaseExclusiveLock();
1668
- throw error;
1669
- }
1670
- }
1671
- const indices = Table.indices;
1672
- if (!attributesDbi) {
1673
- if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
1674
- rootStore.dbisDb = openRocksDatabase(rootStore.path, {
1675
- ...internalDbiInit,
1676
- disableWAL: false,
1677
- name: terms_ts_1.INTERNAL_DBIS_NAME,
1678
- });
2601
+ deferredPrimaryRow = primaryKeyAttribute;
1679
2602
  }
1680
- else {
1681
- rootStore.dbisDb = rootStore.openDB(terms_ts_1.INTERNAL_DBIS_NAME, internalDbiInit);
2603
+ const indices = Table.indices;
2604
+ if (!attributesDbi) {
2605
+ if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
2606
+ rootStore.dbisDb = openRocksDatabase(rootStore.path, {
2607
+ ...internalDbiInit,
2608
+ disableWAL: false,
2609
+ name: terms_ts_1.INTERNAL_DBIS_NAME,
2610
+ });
2611
+ }
2612
+ else {
2613
+ rootStore.dbisDb = rootStore.openDB(terms_ts_1.INTERNAL_DBIS_NAME, internalDbiInit);
2614
+ }
2615
+ target.adopt(rootStore.dbisDb);
2616
+ attributesDbi = markInternalDbiNonVersioned(rootStore.dbisDb);
2617
+ }
2618
+ Table.dbisDB = attributesDbi;
2619
+ // A cluster-origin list can miss a descriptor another thread committed moments ago, so removal
2620
+ // reconciliation is reserved for local schema authoring; on a create the rows can only be aborted state.
2621
+ const reconcileRemovals = origin !== 'cluster' || Boolean(deferredPrimaryRow);
2622
+ for (const { key, value } of reconcileRemovals
2623
+ ? attributesDbi.getRange({ start: tableName + '/', end: tableName + '0' })
2624
+ : []) {
2625
+ if (value == null)
2626
+ continue;
2627
+ let [attributeTableName, attribute_name] = key.toString().split('/');
2628
+ if (attribute_name === '')
2629
+ attribute_name = value.name; // primary key
2630
+ if (attribute_name) {
2631
+ if (attributeTableName !== tableName)
2632
+ continue;
2633
+ }
2634
+ else {
2635
+ // table attribute for a table with no primary key, we don't want to remove this, so continue on
2636
+ continue;
2637
+ }
2638
+ const attribute = attributes.find((attribute) => attribute.name === attribute_name);
2639
+ const removeIndex = !attribute?.indexed && value.indexed && !value.isPrimaryKey;
2640
+ // rows already present under a create are aborted state
2641
+ const staleRow = (!attribute && !value.isPrimaryKey) || Boolean(deferredPrimaryRow);
2642
+ if (staleRow || removeIndex) {
2643
+ exclusiveLock();
2644
+ hasChanges = true;
2645
+ if (staleRow)
2646
+ attributesDbi.remove(key);
2647
+ if (removeIndex) {
2648
+ const indexDbi = Table.indices[attributeTableName];
2649
+ if (indexDbi)
2650
+ indicesToRemove.push(indexDbi);
2651
+ }
2652
+ }
1682
2653
  }
1683
- attributesDbi = markInternalDbiNonVersioned(rootStore.dbisDb);
1684
- }
1685
- Table.dbisDB = attributesDbi;
1686
- const indicesToRemove = [];
1687
- for (const { key, value } of attributesDbi.getRange({ start: true })) {
1688
- if (value == null)
1689
- continue;
1690
- let [attributeTableName, attribute_name] = key.toString().split('/');
1691
- if (attribute_name === '')
1692
- attribute_name = value.name; // primary key
1693
- if (attribute_name) {
1694
- if (attributeTableName !== tableName)
2654
+ const hasHnswDeclaration = attributes.some((attribute) => attribute.indexed?.type === 'HNSW');
2655
+ const persistedAudit = hasHnswDeclaration ? persistedPrimaryDescriptor(attributesDbi).descriptor?.audit : undefined;
2656
+ // A cluster declaration can apply audit on a create, but deliberately cannot rewrite an existing
2657
+ // table's primary row. Do not let an incoming audit value qualify a replicated native descriptor
2658
+ // that this node would then persist beside its durable audit:false row.
2659
+ const explicitAuditCanBeApplied = origin !== 'cluster' || Boolean(deferredPrimaryRow);
2660
+ const auditEnabledForNativeDefault = (auditExplicitlyEnabled && explicitAuditCanBeApplied) || (!auditExplicitlyDisabled && persistedAudit === true);
2661
+ const auditEnabledForNativePlane = !auditExplicitlyDisabled && (auditEnabledForNativeDefault || (persistedAudit == null && Table.audit === true));
2662
+ for (const attribute of attributes) {
2663
+ const indexed = attribute.indexed;
2664
+ if (!indexed || typeof indexed !== 'object' || indexed.type !== 'HNSW')
1695
2665
  continue;
2666
+ const descriptor = attributesDbi.getSync(tableName + '/' + (attribute.name || ''));
2667
+ const existingHnsw = descriptor?.indexed?.type === 'HNSW';
2668
+ if (indexed.nativePlane == null) {
2669
+ if (existingHnsw) {
2670
+ if (Object.hasOwn(descriptor.indexed, 'nativePlane')) {
2671
+ indexed.nativePlane = descriptor.indexed.nativePlane;
2672
+ }
2673
+ }
2674
+ else if (origin !== 'cluster' &&
2675
+ auditEnabledForNativeDefault &&
2676
+ customIndexes_ts_1.CUSTOM_INDEXES.HNSW.canDefaultToNativePlane(rootStore, indexed)) {
2677
+ indexed.nativePlane = true;
2678
+ }
2679
+ }
2680
+ else if (origin === 'cluster' && !existingHnsw && indexed.nativePlane) {
2681
+ let canRunNative = false;
2682
+ try {
2683
+ canRunNative = auditEnabledForNativeDefault && customIndexes_ts_1.CUSTOM_INDEXES.HNSW.canRunNativePlane(rootStore, indexed);
2684
+ }
2685
+ catch { }
2686
+ if (!canRunNative) {
2687
+ logger.warn(`Using the JS HNSW index for replicated attribute ${databaseName}.${tableName}.${attribute.name} because this node does not satisfy the nativePlane requirements`);
2688
+ indexed.nativePlane = false;
2689
+ }
2690
+ }
1696
2691
  }
1697
- else {
1698
- // table attribute for a table with no primary key, we don't want to remove this, so continue on
1699
- continue;
2692
+ const nativePlaneEnabled = origin !== 'cluster' &&
2693
+ attributes.some((attribute) => attribute.indexed?.type === 'HNSW' && attribute.indexed.nativePlane);
2694
+ if (nativePlaneEnabled && !auditEnabledForNativePlane) {
2695
+ throw new hdbError_ts_1.ClientError(`Table '${databaseName}.${tableName}' must enable audit logging before using nativePlane because its transaction log is the derived-index recovery source; set nativePlane: false to use the JS index`);
1700
2696
  }
1701
- const attribute = attributes.find((attribute) => attribute.name === attribute_name);
1702
- const removeIndex = !attribute?.indexed && value.indexed && !value.isPrimaryKey;
1703
- if (!attribute || removeIndex) {
2697
+ if (nativePlaneEnabled && persistedAudit !== true)
2698
+ audit = true;
2699
+ if (nativePlaneEnabled && persistedAudit !== true && !attributes.some((attribute) => attribute.isPrimaryKey)) {
1704
2700
  exclusiveLock();
1705
- hasChanges = true;
1706
- if (!attribute)
1707
- attributesDbi.remove(key);
1708
- if (removeIndex) {
1709
- const indexDbi = Table.indices[attributeTableName];
1710
- if (indexDbi)
1711
- indicesToRemove.push(indexDbi);
2701
+ const primaryKey = primaryDescriptorKey();
2702
+ const primaryDescriptor = attributesDbi.getSync(primaryKey);
2703
+ if (primaryDescriptor && !tableIsDropping(primaryDescriptor, primaryKey)) {
2704
+ Table.enableAuditing();
2705
+ attributesDbi.put(primaryKey, { ...primaryDescriptor, audit: true });
2706
+ hasChanges = true;
1712
2707
  }
1713
2708
  }
1714
- }
1715
- const attributesToIndex = [];
1716
- try {
1717
2709
  // TODO: If we have attributes and the schemaDefined flag is not set, turn it on
1718
2710
  // iterate through the attributes to ensure that we have all the dbis created and indexed
1719
- for (const attribute of attributes || []) {
1720
- if (attribute.relationship || attribute.computed) {
1721
- hasChanges = true; // need to update the table so the computed properties are translated to property resolvers
1722
- if (attribute.relationship)
1723
- continue;
2711
+ const attributesInPersistenceOrder = nativePlaneEnabled
2712
+ ? [
2713
+ ...attributes.filter((attribute) => attribute.isPrimaryKey),
2714
+ ...attributes.filter((attribute) => !attribute.isPrimaryKey),
2715
+ ]
2716
+ : auditExplicitlyDisabled
2717
+ ? [
2718
+ ...attributes.filter((attribute) => !attribute.isPrimaryKey),
2719
+ ...attributes.filter((attribute) => attribute.isPrimaryKey),
2720
+ ]
2721
+ : attributes;
2722
+ for (const attribute of attributesInPersistenceOrder) {
2723
+ if (attribute.relationship) {
2724
+ refreshRelationshipAttributes = true;
2725
+ continue;
1724
2726
  }
2727
+ if (attribute.computed)
2728
+ hasChanges = true;
1725
2729
  let dbiKey = tableName + '/' + (attribute.name || '');
1726
2730
  Object.defineProperty(attribute, 'key', { value: dbiKey, configurable: true });
1727
2731
  let attributeDescriptor = attributesDbi.getSync(dbiKey);
1728
2732
  if (attribute.isPrimaryKey) {
2733
+ if (deferredPrimaryRow)
2734
+ continue;
1729
2735
  attributeDescriptor = attributeDescriptor || attributesDbi.getSync((dbiKey = tableName + '/')) || {};
1730
2736
  // Persist schemaDefined when the explicit live value disagrees with disk. Without this,
1731
2737
  // a stale `false` (from a v4-era write or replicated event) survives every reload: the
1732
2738
  // in-memory re-assert in the existing-Table branch only fixes the worker that ran @table,
1733
- // but other workers' next disk-load re-reads the stale value.
2739
+ // but other workers' next disk-load re-reads the stale value. The whole settings update is
2740
+ // gated off for cluster-origin callers: their values come from this worker's (possibly
2741
+ // stale) snapshot, so a rewrite could revert a newer local declaration already on disk.
1734
2742
  const schemaDefinedMismatch = schemaDefinedExplicit && attributeDescriptor.schemaDefined !== schemaDefined;
1735
2743
  // primary key can't change indexing, but settings can change
1736
- if (schemaDefinedMismatch ||
1737
- (audit !== undefined && audit !== Table.audit) ||
1738
- (sealed !== undefined && sealed !== Table.sealed) ||
1739
- (replicate !== undefined && replicate !== Table.replicate) ||
1740
- (+expiration || undefined) !== (+attributeDescriptor.expiration || undefined) ||
1741
- (+eviction || undefined) !== (+attributeDescriptor.eviction || undefined) ||
1742
- attribute.type !== attributeDescriptor.type) {
1743
- const updatedPrimaryAttribute = { ...attributeDescriptor };
2744
+ if (origin !== 'cluster' &&
2745
+ (schemaDefinedMismatch ||
2746
+ (typeof audit === 'boolean' && audit !== attributeDescriptor.audit) ||
2747
+ (sealed !== undefined && sealed !== Table.sealed) ||
2748
+ (replicate !== undefined && replicate !== Table.replicate) ||
2749
+ (+expiration || undefined) !== (+attributeDescriptor.expiration || undefined) ||
2750
+ (+eviction || undefined) !== (+attributeDescriptor.eviction || undefined) ||
2751
+ attribute.type !== attributeDescriptor.type)) {
2752
+ exclusiveLock();
2753
+ const currentPrimaryAttribute = attributesDbi.getSync(dbiKey);
2754
+ if (!currentPrimaryAttribute || tableIsDropping(currentPrimaryAttribute, dbiKey))
2755
+ continue;
2756
+ const updatedPrimaryAttribute = { ...currentPrimaryAttribute };
1744
2757
  if (typeof audit === 'boolean') {
1745
2758
  if (audit)
1746
2759
  Table.enableAuditing();
@@ -1759,14 +2772,51 @@ function table(tableDefinition) {
1759
2772
  if (schemaDefinedMismatch)
1760
2773
  updatedPrimaryAttribute.schemaDefined = schemaDefined;
1761
2774
  hasChanges = true; // send out notification of the change
1762
- exclusiveLock();
1763
2775
  attributesDbi.put(dbiKey, updatedPrimaryAttribute);
1764
2776
  }
1765
2777
  continue;
1766
2778
  }
1767
- // note that non-indexed attributes do not need a dbi
1768
2779
  if (attributeDescriptor?.attribute && !attributeDescriptor.name)
1769
2780
  attributeDescriptor.indexed = true; // legacy descriptor
2781
+ if (origin === 'cluster' && attributeDescriptor) {
2782
+ // An existing descriptor is a local declaration this caller may not have seen yet, so it wins
2783
+ // over the incoming definition and is never written back from it.
2784
+ applyDurableDeclaration(attribute, attributeDescriptor);
2785
+ const abandonedIndexBuild = attribute.indexed &&
2786
+ (attributeDescriptor.indexingFailed ||
2787
+ isAbandonedIndexBuild(attributeDescriptor, worker_threads_1.workerData?.restartNumber ?? manageThreads.restartNumber));
2788
+ if (abandonedIndexBuild) {
2789
+ // Recovery is the exception to skipping the handling below, because without it `isIndexing`
2790
+ // stays pinned on with nothing left to clear it and every query on the attribute fails with
2791
+ // IndexRebuildingError for the life of the worker. It persists the attribute (here and again
2792
+ // from runIndexing), so restate the declaration from a descriptor read under the lock.
2793
+ exclusiveLock();
2794
+ applyDurableDeclaration(attribute, attributesDbi.getSync(dbiKey) ?? attributeDescriptor);
2795
+ }
2796
+ else {
2797
+ if (attribute.indexed) {
2798
+ const dbi = openIndex(dbiKey, rootStore, attribute);
2799
+ target.adopt(dbi);
2800
+ // Persisting the indexFormat openIndex just resolved adds a field the descriptor lacks
2801
+ // rather than rewriting one it has. Without it an empty index resolves 'versioned', writes
2802
+ // versioned nodes, then re-derives 'legacy' on the next load — see indexFormatNeedsPersist.
2803
+ if (attribute.indexFormat != null && attributeDescriptor.indexFormat == null) {
2804
+ exclusiveLock();
2805
+ const durableDescriptor = attributesDbi.getSync(dbiKey);
2806
+ if (durableDescriptor && durableDescriptor.indexFormat == null) {
2807
+ hasChanges = true;
2808
+ attributesDbi.put(dbiKey, { ...durableDescriptor, indexFormat: attribute.indexFormat });
2809
+ }
2810
+ }
2811
+ if (attributeDescriptor.indexingPID)
2812
+ dbi.isIndexing = true;
2813
+ dbi.indexNulls = attribute.indexNulls;
2814
+ indices[attribute.name] = dbi;
2815
+ }
2816
+ continue;
2817
+ }
2818
+ }
2819
+ // note that non-indexed attributes do not need a dbi
1770
2820
  // Some index options affect only search, not the stored structure (e.g. HNSW's
1771
2821
  // efConstructionSearch). Changing those should persist the new metadata but NOT trigger a
1772
2822
  // reindex. A custom index declares such keys via a static `searchOnlyOptions`.
@@ -1809,6 +2859,9 @@ function table(tableDefinition) {
1809
2859
  // on the main thread, where workerData is undefined (and it is initialized to 1).
1810
2860
  const currentRestartGeneration = worker_threads_1.workerData?.restartNumber ?? manageThreads.restartNumber;
1811
2861
  const dbi = openIndex(dbiKey, rootStore, attribute);
2862
+ target.adopt(dbi);
2863
+ if (deferredPrimaryRow)
2864
+ indices[attribute.name] = dbi; // private until published; lets the rollback close it
1812
2865
  // openIndex resolves and stamps attribute.indexFormat for a versioned-capable (RocksDB
1813
2866
  // custom-object) index. An index created before this field existed has no indexFormat on
1814
2867
  // disk; persist the resolved value now — even when nothing else changed — so the format is
@@ -1951,45 +3004,134 @@ function table(tableDefinition) {
1951
3004
  attributesDbi.put(dbiKey, attribute);
1952
3005
  }
1953
3006
  }
3007
+ // The primary row is what makes a table loadable, so it lands last: a scan on another thread that
3008
+ // runs mid-create skips the table instead of building (and announcing) a partial one. It already
3009
+ // carries this table's relationships (set on primaryKeyAttribute above), so the persistence block
3010
+ // below is a no-op for a create — a table is never published with an incomplete relationship list.
3011
+ if (deferredPrimaryRow) {
3012
+ attributesDbi.put(tableName + '/', deferredPrimaryRow);
3013
+ // That write, not the registration below, is the publish point: it is durable from here
3014
+ // (on LMDB releaseLock()'s finally commits this create's write transaction even while an
3015
+ // error unwinds), so any later throw must leave the catalog alone. Rolling back past it
3016
+ // would delete the attribute rows out from under a live primary row and leave every
3017
+ // thread loading the primary-only schema this change exists to prevent.
3018
+ published = true;
3019
+ setTable(tables, tableName, Table);
3020
+ }
3021
+ // a table with no declared primary key has no attribute row to carry relationships, and the
3022
+ // loop above never visits its descriptor
3023
+ if (relationshipDefinitions) {
3024
+ const relationshipsKey = primaryDescriptorKey();
3025
+ if (!relationshipListsEqual(attributesDbi.getSync(relationshipsKey)?.relationships, relationshipDefinitions)) {
3026
+ exclusiveLock();
3027
+ const currentPrimaryAttribute = attributesDbi.getSync(relationshipsKey);
3028
+ // a missing row means a concurrent drop completed; writing one back would resurrect the table
3029
+ if (currentPrimaryAttribute &&
3030
+ !tableIsDropping(currentPrimaryAttribute, relationshipsKey) &&
3031
+ !relationshipListsEqual(currentPrimaryAttribute.relationships, relationshipDefinitions)) {
3032
+ attributesDbi.put(relationshipsKey, { ...currentPrimaryAttribute, relationships: relationshipDefinitions });
3033
+ hasChanges = true;
3034
+ }
3035
+ }
3036
+ }
3037
+ }
3038
+ catch (error) {
3039
+ if (unpublishedPrimaryStore && !published)
3040
+ discardUnpublishedTable();
3041
+ else if (published && tables[tableName] !== Table)
3042
+ discardUnregisteredClass();
3043
+ throw error;
1954
3044
  }
1955
3045
  finally {
1956
- if (releaseExclusiveLock)
1957
- releaseExclusiveLock();
3046
+ releaseLock();
1958
3047
  }
1959
- if (hasChanges) {
3048
+ if (hasChanges || refreshRelationshipAttributes)
1960
3049
  Table.schemaVersion++;
3050
+ if (hasChanges || refreshRelationshipAttributes || refreshedLiveAttributes)
1961
3051
  Table.updatedAttributes();
1962
- }
1963
3052
  logger.trace(`${tableName} table loading, running index`);
3053
+ const branchPath = target.branch?.path;
1964
3054
  if (attributesToIndex.length > 0 || indicesToRemove.length > 0) {
1965
3055
  // captured before the backfill can rewrite the attributes
1966
3056
  const buildIds = new Map(attributesToIndex.map((attribute) => [attribute, attribute.indexingBuildId]));
1967
3057
  const markSettled = () => markAbandonedIndexBuild(Table, rootStore, buildIds);
1968
- Table.indexingOperation = runIndexing(Table, attributesToIndex, indicesToRemove).then(markSettled, markSettled);
3058
+ Table.indexingOperation = runIndexing(Table, attributesToIndex, indicesToRemove, branchPath).then(markSettled, markSettled);
1969
3059
  }
1970
3060
  else if (hasChanges)
1971
- signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName));
3061
+ signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName, undefined, branchPath));
3062
+ void Table.derivedIndexRuntime?.close();
3063
+ Table.derivedIndexRuntime = (0, hnswDerivedIndex_ts_1.attachDerivedIndexes)(Table);
1972
3064
  Table.origin = origin;
1973
- if (hasChanges) {
3065
+ // scope-private: replication and other global subscribers must not learn of a branch class
3066
+ if ((hasChanges || refreshRelationshipAttributes) && !target.branch) {
1974
3067
  exports.databaseEventsEmitter.emit('updateTable', Table, origin !== 'cluster');
1975
3068
  }
1976
- if (expiration || eviction || scanInterval)
3069
+ if (expiration || eviction || scanInterval || attributes.some((attribute) => attribute.expiresAt))
1977
3070
  Table.setTTLExpiration({
1978
3071
  expiration,
1979
3072
  eviction,
1980
3073
  scanInterval,
3074
+ fromSchema: true,
3075
+ isolatedApplicationOwner,
1981
3076
  });
1982
3077
  logger.trace(`${tableName} table loaded`);
1983
3078
  return Table;
3079
+ // A migrated catalog can retain a named primary descriptor beside a bare table tombstone, so a
3080
+ // drop in flight has to be checked on both representations.
3081
+ function tableIsDropping(descriptor, descriptorKey) {
3082
+ if (descriptor?.dropping)
3083
+ return true;
3084
+ return descriptorKey !== tableName + '/' && attributesDbi.getSync(tableName + '/')?.dropping;
3085
+ }
3086
+ // The catalog row initStores() reads a table's settings from: the primary key's own row when it
3087
+ // has one, and the bare table row otherwise.
3088
+ function primaryDescriptorKey() {
3089
+ return persistedPrimaryDescriptor(attributesDbi).key;
3090
+ }
3091
+ // The catalog of a published table stays, but a class the registration never accepted is
3092
+ // unreachable, so release what makeTable() registered process-wide instead of leaving its timers
3093
+ // and reclamation handler live for the process. The stores stay open: the table is durable, and
3094
+ // whichever scan reloads it opens its own handles.
3095
+ function discardUnregisteredClass() {
3096
+ try {
3097
+ Table.cleanup();
3098
+ }
3099
+ catch (discardError) {
3100
+ logger.warn(`Error releasing the unregistered class of ${databaseName}.${tableName}`, discardError);
3101
+ }
3102
+ }
3103
+ function discardUnpublishedTable() {
3104
+ const discard = (description, action) => {
3105
+ try {
3106
+ action();
3107
+ }
3108
+ catch (discardError) {
3109
+ logger.warn(`Error discarding ${description} of the failed create of ${databaseName}.${tableName}`, discardError);
3110
+ }
3111
+ };
3112
+ discard('catalog rows', () => {
3113
+ for (const attribute of attributes) {
3114
+ if (!attribute.isPrimaryKey && !attribute.relationship)
3115
+ attributesDbi.remove(tableName + '/' + attribute.name);
3116
+ }
3117
+ });
3118
+ if (Table)
3119
+ discard('callbacks', () => Table.cleanup());
3120
+ // an LMDB store is a per-environment handle slot shared with every thread and still inside this
3121
+ // create's write transaction; only RocksDB column-family handles hold native state to release
3122
+ if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
3123
+ for (const indexName in Table?.indices ?? {})
3124
+ discard(`index ${indexName}`, () => Table.indices[indexName].close());
3125
+ discard('primary store', () => unpublishedPrimaryStore.close());
3126
+ }
3127
+ }
1984
3128
  // Acquire an exclusive lock for attribute updates
1985
3129
  function exclusiveLock() {
1986
3130
  if (releaseExclusiveLock)
1987
3131
  return;
1988
3132
  if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
1989
- while (!rootStore.tryLock('update-attributes')) { } // use a spin lock, we really need an synchronous exclusive lock here
1990
- releaseExclusiveLock = () => {
1991
- rootStore.unlock('update-attributes');
1992
- };
3133
+ (0, Table_ts_1.acquireUpdateAttributesLock)(rootStore, `table '${databaseName}.${tableName}'`);
3134
+ releaseExclusiveLock = () => (0, Table_ts_1.releaseUpdateAttributesLock)(rootStore);
1993
3135
  }
1994
3136
  else {
1995
3137
  // we only need an exclusive transaction lock in lmdb
@@ -2002,37 +3144,41 @@ function table(tableDefinition) {
2002
3144
  });
2003
3145
  }
2004
3146
  }
3147
+ // idempotent: the early release before the recursive reload and the finally both run, and a
3148
+ // second unlock could release another thread's lock
3149
+ function releaseLock() {
3150
+ const release = releaseExclusiveLock;
3151
+ releaseExclusiveLock = undefined;
3152
+ if (release)
3153
+ release();
3154
+ }
2005
3155
  }
2006
3156
  /**
2007
- * Canonical form used ONLY for the structural (reindex-triggering) comparison of index options.
2008
- * `@indexed(...)` records options in source-argument order and as strings, while the operations API
2009
- * and config objects can supply them reordered or as numbers; without canonicalizing, such a
2010
- * representation-only difference flips the structural comparison and forces a needless full rebuild
2011
- * (clearing + rebuilding the index, 503-ing the attribute throughout) for a semantically identical
2012
- * index. Sorts object keys and coerces numeric-looking (non-zero) string scalars to numbers.
2013
- * Conservative by design: boolean-vs-object, absent-vs-present, and string-"0"-vs-number-0
2014
- * differences are all preserved, so a genuine change (`true` vs `{ type: 'HNSW' }`, an added/removed
2015
- * option, a changed value) still triggers a rebuild. Persistence keys off the raw form, so the stored
2016
- * descriptor self-heals toward this shape over time. harper#1357
3157
+ * Stable structural form for deciding whether an index must be rebuilt. `coerceZero` extends numeric
3158
+ * coercion to zero; a truthiness-sensitive numeric option must normalize its value before using it.
2017
3159
  */
2018
- function canonicalizeIndexOptions(value) {
3160
+ function canonicalizeIndexOptions(value, coerceZero = false) {
2019
3161
  if (Array.isArray(value))
2020
- return value.map(canonicalizeIndexOptions);
3162
+ return value.map((item) => canonicalizeIndexOptions(item, coerceZero));
2021
3163
  if (value && typeof value === 'object') {
2022
3164
  const canonical = {};
2023
- for (const key of Object.keys(value).sort())
2024
- canonical[key] = canonicalizeIndexOptions(value[key]);
3165
+ const customIndex = value.type && customIndexes_ts_1.CUSTOM_INDEXES[value.type];
3166
+ for (const key of Object.keys(value).sort()) {
3167
+ if (customIndex?.truthyStructuralOptions?.has(key)) {
3168
+ if (value[key])
3169
+ canonical[key] = true;
3170
+ continue;
3171
+ }
3172
+ const optionValue = customIndex?.normalizeOptionValue
3173
+ ? customIndex.normalizeOptionValue(key, value[key])
3174
+ : value[key];
3175
+ canonical[key] = canonicalizeIndexOptions(optionValue, coerceZero || Boolean(customIndex?.numericOptions?.has(key)));
3176
+ }
2025
3177
  return canonical;
2026
3178
  }
2027
- // Coerce numeric-looking strings ("16" -> 16) so string-vs-number representations of the same
2028
- // option compare equal — EXCEPT zero: the string "0" is truthy while the number 0 is falsy, and
2029
- // index code may branch on truthiness (e.g. HNSW `if (this.optimizeRouting)` doubles maxConnections),
2030
- // so "0" and 0 build structurally different indexes and must still trigger a rebuild. Zero is the
2031
- // only finite number whose string and numeric forms diverge in truthiness, so excluding it fully
2032
- // closes that gap. Leave non-numeric strings, booleans, null, etc. intact.
2033
3179
  if (typeof value === 'string' && value.trim() !== '') {
2034
3180
  const numeric = Number(value);
2035
- if (numeric !== 0 && Number.isFinite(numeric))
3181
+ if ((numeric !== 0 || coerceZero) && Number.isFinite(numeric))
2036
3182
  return numeric;
2037
3183
  }
2038
3184
  return value;
@@ -2092,33 +3238,6 @@ function resumeStartKey(attributes) {
2092
3238
  }
2093
3239
  return start;
2094
3240
  }
2095
- // Bounded, unlike the exclusiveLock() spin above: that one runs on the declaring path, where the
2096
- // caller is waiting on the result and there is nothing useful to do without the lock. This one runs
2097
- // after a backfill has already settled, so a holder that never releases would wedge the worker's
2098
- // event loop for nothing. Giving up costs only the marker, and the next load of the table
2099
- // re-triggers the build regardless.
2100
- exports.ABANDONED_MARK_LOCK_TIMEOUT = 10000;
2101
- const abandonedMarkLockWait = new Int32Array(new SharedArrayBuffer(4));
2102
- // `timeout` is the test seam; production callers take the default.
2103
- function tryAcquireUpdateAttributesLock(rootStore, timeout = exports.ABANDONED_MARK_LOCK_TIMEOUT) {
2104
- if (rootStore.tryLock('update-attributes'))
2105
- return true;
2106
- const startTime = performance.now();
2107
- let waitTime = 1;
2108
- while (!rootStore.tryLock('update-attributes')) {
2109
- const elapsed = performance.now() - startTime;
2110
- if (elapsed >= timeout)
2111
- return false;
2112
- // Atomics.wait rather than a busy spin: the section this guards is synchronous, so the wait
2113
- // blocks this thread either way, but sleeping does not burn a core while it does.
2114
- if (elapsed >= 2) {
2115
- Atomics.wait(abandonedMarkLockWait, 0, 0, Math.min(waitTime, timeout - elapsed));
2116
- if (waitTime < 16)
2117
- waitTime *= 2;
2118
- }
2119
- }
2120
- return true;
2121
- }
2122
3241
  /**
2123
3242
  * Persists the failure marker for a build that ended without running one of runIndexing's own exit
2124
3243
  * paths, so something re-triggers it. Fenced on `indexingBuildId` inside the storage engine's catalog
@@ -2141,17 +3260,12 @@ async function markAbandonedIndexBuild(Table, rootStore, buildIds) {
2141
3260
  }
2142
3261
  };
2143
3262
  if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
2144
- if (!tryAcquireUpdateAttributesLock(rootStore)) {
2145
- logger.warn(`Could not mark the abandoned index build of ${Table.databaseName}.${Table.tableName}.${attribute.name}: ` +
2146
- `timed out after ${exports.ABANDONED_MARK_LOCK_TIMEOUT}ms waiting for the exclusive 'update-attributes' lock. ` +
2147
- `The index stays incomplete and the next load of the table re-triggers the backfill.`);
2148
- continue;
2149
- }
3263
+ (0, Table_ts_1.acquireUpdateAttributesLock)(rootStore, `abandoned index build '${Table.tableName}.${attribute.name}'`);
2150
3264
  try {
2151
3265
  markIfOwned();
2152
3266
  }
2153
3267
  finally {
2154
- rootStore.unlock('update-attributes');
3268
+ (0, Table_ts_1.releaseUpdateAttributesLock)(rootStore);
2155
3269
  }
2156
3270
  }
2157
3271
  else {
@@ -2171,7 +3285,7 @@ async function markAbandonedIndexBuild(Table, rootStore, buildIds) {
2171
3285
  }
2172
3286
  }
2173
3287
  }
2174
- async function runIndexing(Table, attributes, indicesToRemove) {
3288
+ async function runIndexing(Table, attributes, indicesToRemove, branchPath) {
2175
3289
  let checkpointing;
2176
3290
  let hadIndexingErrors = false;
2177
3291
  const attributeErrorReported = {};
@@ -2185,7 +3299,7 @@ async function runIndexing(Table, attributes, indicesToRemove) {
2185
3299
  const putRejectionHandlers = attributes.map((attribute) => (error) => onIndexPutRejected(attribute.name, error));
2186
3300
  try {
2187
3301
  logger.info(`Indexing ${Table.tableName} attributes`, attributes);
2188
- await signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName));
3302
+ await signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName, undefined, branchPath));
2189
3303
  let lastResolution;
2190
3304
  // The checkpoint and completion barriers have to cover every mutation still in flight: any of them
2191
3305
  // may reject after those barriers read hadIndexingErrors.
@@ -2221,6 +3335,7 @@ async function runIndexing(Table, attributes, indicesToRemove) {
2221
3335
  };
2222
3336
  });
2223
3337
  for (const index of indicesToRemove) {
3338
+ index.customIndex?.resetDerivedStorage?.();
2224
3339
  track(index.drop(), (error) => onIndexPutRejected(index.name, error));
2225
3340
  }
2226
3341
  let interrupted;
@@ -2231,6 +3346,8 @@ async function runIndexing(Table, attributes, indicesToRemove) {
2231
3346
  const start = resumeStartKey(attributes);
2232
3347
  if (start === undefined) {
2233
3348
  for (const attribute of attributes) {
3349
+ // if we are starting from the beginning, clear out any previous index entries since we are rewriting
3350
+ attribute.dbi.customIndex?.resetDerivedStorage?.();
2234
3351
  if (attribute.dbi.clearAsync) {
2235
3352
  // LMDB enqueues this ahead of the index writes, so the scan need not wait for it — but the
2236
3353
  // barriers must, or a rejected clear certifies a checkpoint over stale entries.
@@ -2408,7 +3525,7 @@ async function runIndexing(Table, attributes, indicesToRemove) {
2408
3525
  }
2409
3526
  await lastResolution;
2410
3527
  // now notify all the threads that we are done and the index is ready to use
2411
- await signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, 'indexing-finished', Table.databaseName, Table.tableName));
3528
+ await signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, 'indexing-finished', Table.databaseName, Table.tableName, undefined, branchPath));
2412
3529
  logger.info(`Finished indexing ${Table.tableName} attributes`, attributes);
2413
3530
  }
2414
3531
  }
@@ -2477,6 +3594,26 @@ function completeInterruptedDrop(rootStore, attributesDbi, databaseName, tableNa
2477
3594
  finally {
2478
3595
  columnStore.close();
2479
3596
  }
3597
+ // derived HNSW plane files live next to the store; the normal drop path removes
3598
+ // them through the custom index, but this recovery path drops raw column stores,
3599
+ // and a same-name recreate must never open a stale plane over a fresh CF
3600
+ try {
3601
+ (0, node_fs_1.unlinkSync)((0, hnswPlaneBinding_ts_1.planeFilePathFor)(rootStore.path, columnName));
3602
+ }
3603
+ catch (error) {
3604
+ // a stale plane left behind (e.g. Windows EBUSY while still mapped) would be
3605
+ // opened over a fresh same-name CF, resolving another graph's node ids
3606
+ // against it — tombstone it so no attach ever adopts it
3607
+ if (error?.code !== 'ENOENT') {
3608
+ logger.warn(`could not delete the HNSW plane file for ${columnName}; tombstoning it as stale`, error);
3609
+ try {
3610
+ (0, node_fs_1.closeSync)((0, node_fs_1.openSync)((0, hnswPlaneBinding_ts_1.planeStalePathFor)((0, hnswPlaneBinding_ts_1.planeFilePathFor)(rootStore.path, columnName)), 'w'));
3611
+ }
3612
+ catch (tombstoneError) {
3613
+ logger.warn(`could not tombstone the stale HNSW plane file for ${columnName}`, tombstoneError);
3614
+ }
3615
+ }
3616
+ }
2480
3617
  }
2481
3618
  }
2482
3619
  }