@harperfast/harper 5.3.0-beta.3 → 5.3.0-beta.4

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 (311) hide show
  1. package/bin/cliOperations.ts +25 -3
  2. package/bin/run.ts +18 -0
  3. package/components/Application.ts +52 -78
  4. package/components/DESIGN.md +76 -3
  5. package/components/Scope.ts +10 -4
  6. package/components/anthropic/index.ts +10 -1
  7. package/components/bedrock/index.ts +10 -1
  8. package/components/componentLoader.ts +23 -14
  9. package/components/mcp/tools/operations.ts +3 -1
  10. package/components/ollama/index.ts +10 -1
  11. package/components/openai/index.ts +265 -18
  12. package/components/operations.js +40 -1
  13. package/config/configUtils.ts +3 -19
  14. package/dataLayer/DESIGN.md +29 -5
  15. package/dataLayer/harperBridge/ResourceBridge.ts +39 -3
  16. package/dataLayer/restoreMarker.ts +250 -14
  17. package/dataLayer/schema.ts +2 -7
  18. package/dataLayer/schemaDescribe.ts +10 -0
  19. package/dist/bin/cliOperations.js +28 -3
  20. package/dist/bin/cliOperations.js.map +1 -1
  21. package/dist/bin/run.js +19 -0
  22. package/dist/bin/run.js.map +1 -1
  23. package/dist/components/Application.d.ts +8 -2
  24. package/dist/components/Application.js +43 -69
  25. package/dist/components/Application.js.map +1 -1
  26. package/dist/components/Scope.d.ts +1 -1
  27. package/dist/components/Scope.js +9 -4
  28. package/dist/components/Scope.js.map +1 -1
  29. package/dist/components/anthropic/index.d.ts +0 -10
  30. package/dist/components/anthropic/index.js +9 -1
  31. package/dist/components/anthropic/index.js.map +1 -1
  32. package/dist/components/bedrock/index.d.ts +0 -12
  33. package/dist/components/bedrock/index.js +9 -1
  34. package/dist/components/bedrock/index.js.map +1 -1
  35. package/dist/components/componentLoader.js +23 -14
  36. package/dist/components/componentLoader.js.map +1 -1
  37. package/dist/components/mcp/tools/operations.js +1 -1
  38. package/dist/components/mcp/tools/operations.js.map +1 -1
  39. package/dist/components/ollama/index.d.ts +0 -13
  40. package/dist/components/ollama/index.js +9 -1
  41. package/dist/components/ollama/index.js.map +1 -1
  42. package/dist/components/openai/index.d.ts +11 -15
  43. package/dist/components/openai/index.js +224 -12
  44. package/dist/components/openai/index.js.map +1 -1
  45. package/dist/components/operations.d.ts +9 -0
  46. package/dist/components/operations.js +32 -2
  47. package/dist/components/operations.js.map +1 -1
  48. package/dist/config/configUtils.d.ts +1 -2
  49. package/dist/config/configUtils.js +7 -22
  50. package/dist/config/configUtils.js.map +1 -1
  51. package/dist/dataLayer/harperBridge/ResourceBridge.js +29 -2
  52. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  53. package/dist/dataLayer/restoreMarker.d.ts +25 -3
  54. package/dist/dataLayer/restoreMarker.js +213 -13
  55. package/dist/dataLayer/restoreMarker.js.map +1 -1
  56. package/dist/dataLayer/schema.js +1 -4
  57. package/dist/dataLayer/schema.js.map +1 -1
  58. package/dist/dataLayer/schemaDescribe.js +10 -0
  59. package/dist/dataLayer/schemaDescribe.js.map +1 -1
  60. package/dist/index.d.ts +1 -1
  61. package/dist/index.js.map +1 -1
  62. package/dist/json/systemSchema.json +28 -0
  63. package/dist/resources/DatabaseTransaction.d.ts +21 -1
  64. package/dist/resources/DatabaseTransaction.js +147 -8
  65. package/dist/resources/DatabaseTransaction.js.map +1 -1
  66. package/dist/resources/PrimaryRocksDatabase.js +1 -1
  67. package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
  68. package/dist/resources/Table.d.ts +31 -3
  69. package/dist/resources/Table.js +397 -79
  70. package/dist/resources/Table.js.map +1 -1
  71. package/dist/resources/auditStore.js +16 -10
  72. package/dist/resources/auditStore.js.map +1 -1
  73. package/dist/resources/blob.d.ts +1 -0
  74. package/dist/resources/blob.js +14 -5
  75. package/dist/resources/blob.js.map +1 -1
  76. package/dist/resources/branchDatabase.js +3 -3
  77. package/dist/resources/branchDatabase.js.map +1 -1
  78. package/dist/resources/databaseDropPreparation.d.ts +20 -0
  79. package/dist/resources/databaseDropPreparation.js +106 -0
  80. package/dist/resources/databaseDropPreparation.js.map +1 -0
  81. package/dist/resources/databases.d.ts +26 -21
  82. package/dist/resources/databases.js +1040 -204
  83. package/dist/resources/databases.js.map +1 -1
  84. package/dist/resources/derivedIndexRegistry.d.ts +23 -0
  85. package/dist/resources/derivedIndexRegistry.js +92 -0
  86. package/dist/resources/derivedIndexRegistry.js.map +1 -1
  87. package/dist/resources/derivedIndexRuntime.d.ts +10 -0
  88. package/dist/resources/derivedIndexRuntime.js +136 -25
  89. package/dist/resources/derivedIndexRuntime.js.map +1 -1
  90. package/dist/resources/derivedIndexes.d.ts +39 -0
  91. package/dist/resources/derivedIndexes.js +733 -0
  92. package/dist/resources/derivedIndexes.js.map +1 -0
  93. package/dist/resources/fullTextSchema.d.ts +8 -0
  94. package/dist/resources/fullTextSchema.js +42 -0
  95. package/dist/resources/fullTextSchema.js.map +1 -1
  96. package/dist/resources/fullTextSchemaLifecycle.d.ts +0 -5
  97. package/dist/resources/fullTextSchemaLifecycle.js +0 -9
  98. package/dist/resources/fullTextSchemaLifecycle.js.map +1 -1
  99. package/dist/resources/graphql.js +101 -9
  100. package/dist/resources/graphql.js.map +1 -1
  101. package/dist/resources/indexes/hnswDerivedIndex.d.ts +0 -10
  102. package/dist/resources/indexes/hnswDerivedIndex.js +0 -230
  103. package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -1
  104. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.d.ts +3 -0
  105. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js +57 -8
  106. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js.map +1 -1
  107. package/dist/resources/models/Models.d.ts +26 -3
  108. package/dist/resources/models/Models.js +256 -13
  109. package/dist/resources/models/Models.js.map +1 -1
  110. package/dist/resources/models/TestBackend.d.ts +3 -1
  111. package/dist/resources/models/TestBackend.js +35 -1
  112. package/dist/resources/models/TestBackend.js.map +1 -1
  113. package/dist/resources/models/analyticsTable.d.ts +2 -2
  114. package/dist/resources/models/analyticsTable.js +4 -2
  115. package/dist/resources/models/analyticsTable.js.map +1 -1
  116. package/dist/resources/models/backendHelpers.d.ts +24 -0
  117. package/dist/resources/models/backendHelpers.js +18 -1
  118. package/dist/resources/models/backendHelpers.js.map +1 -1
  119. package/dist/resources/models/backendRegistry.d.ts +10 -21
  120. package/dist/resources/models/backendRegistry.js +74 -18
  121. package/dist/resources/models/backendRegistry.js.map +1 -1
  122. package/dist/resources/models/bootstrap.d.ts +7 -0
  123. package/dist/resources/models/bootstrap.js +36 -4
  124. package/dist/resources/models/bootstrap.js.map +1 -1
  125. package/dist/resources/models/decideHook.d.ts +33 -0
  126. package/dist/resources/models/decideHook.js +105 -0
  127. package/dist/resources/models/decideHook.js.map +1 -0
  128. package/dist/resources/models/decision.d.ts +64 -0
  129. package/dist/resources/models/decision.js +507 -0
  130. package/dist/resources/models/decision.js.map +1 -0
  131. package/dist/resources/models/decisionStore.d.ts +134 -0
  132. package/dist/resources/models/decisionStore.js +350 -0
  133. package/dist/resources/models/decisionStore.js.map +1 -0
  134. package/dist/resources/models/embedHook.d.ts +66 -8
  135. package/dist/resources/models/embedHook.js +228 -61
  136. package/dist/resources/models/embedHook.js.map +1 -1
  137. package/dist/resources/models/generativeDecision.d.ts +58 -0
  138. package/dist/resources/models/generativeDecision.js +315 -0
  139. package/dist/resources/models/generativeDecision.js.map +1 -0
  140. package/dist/resources/models/types.d.ts +232 -4
  141. package/dist/security/authn/oidc/tokenExchange.js +3 -33
  142. package/dist/security/authn/oidc/tokenExchange.js.map +1 -1
  143. package/dist/security/authn/oidc/tokenUseTable.d.ts +9 -0
  144. package/dist/security/authn/oidc/tokenUseTable.js +28 -0
  145. package/dist/security/authn/oidc/tokenUseTable.js.map +1 -0
  146. package/dist/security/certificateVerification/certificateVerificationSource.d.ts +0 -1
  147. package/dist/security/certificateVerification/certificateVerificationSource.js +3 -3
  148. package/dist/security/certificateVerification/certificateVerificationSource.js.map +1 -1
  149. package/dist/security/certificateVerification/crlVerification.js +77 -165
  150. package/dist/security/certificateVerification/crlVerification.js.map +1 -1
  151. package/dist/security/certificateVerification/types.d.ts +0 -3
  152. package/dist/security/certificateVerification/verificationTables.d.ts +19 -0
  153. package/dist/security/certificateVerification/verificationTables.js +125 -0
  154. package/dist/security/certificateVerification/verificationTables.js.map +1 -0
  155. package/dist/security/certificateVerification/verificationUtils.d.ts +1 -1
  156. package/dist/security/certificateVerification/verificationUtils.js +6 -31
  157. package/dist/security/certificateVerification/verificationUtils.js.map +1 -1
  158. package/dist/security/jsLoader.d.ts +5 -0
  159. package/dist/security/jsLoader.js +8 -0
  160. package/dist/security/jsLoader.js.map +1 -1
  161. package/dist/server/Server.d.ts +1 -0
  162. package/dist/server/Server.js.map +1 -1
  163. package/dist/server/http.js +2 -2
  164. package/dist/server/http.js.map +1 -1
  165. package/dist/server/itc/serverHandlers.js +21 -2
  166. package/dist/server/itc/serverHandlers.js.map +1 -1
  167. package/dist/server/jobs/jobOwnership.d.ts +25 -0
  168. package/dist/server/jobs/jobOwnership.js +127 -0
  169. package/dist/server/jobs/jobOwnership.js.map +1 -0
  170. package/dist/server/jobs/jobProcess.js +9 -2
  171. package/dist/server/jobs/jobProcess.js.map +1 -1
  172. package/dist/server/jobs/jobs.js +18 -5
  173. package/dist/server/jobs/jobs.js.map +1 -1
  174. package/dist/server/loadRootComponents.js +10 -0
  175. package/dist/server/loadRootComponents.js.map +1 -1
  176. package/dist/server/serverHelpers/contentTypes.js +11 -1
  177. package/dist/server/serverHelpers/contentTypes.js.map +1 -1
  178. package/dist/server/serverHelpers/serverHandlers.js +12 -2
  179. package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
  180. package/dist/server/threads/itc.d.ts +4 -1
  181. package/dist/server/threads/itc.js +29 -6
  182. package/dist/server/threads/itc.js.map +1 -1
  183. package/dist/server/threads/manageThreads.d.ts +12 -4
  184. package/dist/server/threads/manageThreads.js +241 -119
  185. package/dist/server/threads/manageThreads.js.map +1 -1
  186. package/dist/server/threads/threadServer.d.ts +1 -0
  187. package/dist/server/threads/threadServer.js +15 -1
  188. package/dist/server/threads/threadServer.js.map +1 -1
  189. package/dist/server/threads/windowsProcessTree.d.ts +91 -0
  190. package/dist/server/threads/windowsProcessTree.js +361 -0
  191. package/dist/server/threads/windowsProcessTree.js.map +1 -0
  192. package/dist/server/throttle.d.ts +1 -1
  193. package/dist/server/throttle.js +8 -1
  194. package/dist/server/throttle.js.map +1 -1
  195. package/dist/upgrade/directives/5-3-0.js +53 -46
  196. package/dist/upgrade/directives/5-3-0.js.map +1 -1
  197. package/dist/utility/errors/hdbError.d.ts +10 -1
  198. package/dist/utility/errors/hdbError.js +55 -11
  199. package/dist/utility/errors/hdbError.js.map +1 -1
  200. package/dist/utility/fsync.d.ts +2 -0
  201. package/dist/utility/fsync.js +20 -0
  202. package/dist/utility/fsync.js.map +1 -0
  203. package/dist/utility/globalSchema.d.ts +18 -0
  204. package/dist/utility/hdbTerms.d.ts +3 -0
  205. package/dist/utility/hdbTerms.js +3 -0
  206. package/dist/utility/hdbTerms.js.map +1 -1
  207. package/dist/utility/signalling.d.ts +7 -1
  208. package/dist/utility/signalling.js +27 -3
  209. package/dist/utility/signalling.js.map +1 -1
  210. package/dist/utility/when.d.ts +1 -0
  211. package/dist/utility/when.js +21 -0
  212. package/dist/utility/when.js.map +1 -1
  213. package/dist/validation/configValidator.js +30 -3
  214. package/dist/validation/configValidator.js.map +1 -1
  215. package/index.ts +13 -0
  216. package/json/systemSchema.json +28 -0
  217. package/npm-shrinkwrap.json +151 -55
  218. package/package.json +14 -13
  219. package/resources/DESIGN.md +80 -100
  220. package/resources/DatabaseTransaction.ts +157 -10
  221. package/resources/PrimaryRocksDatabase.ts +2 -2
  222. package/resources/Table.ts +494 -101
  223. package/resources/auditStore.ts +18 -10
  224. package/resources/blob.ts +13 -6
  225. package/resources/branchDatabase.ts +3 -3
  226. package/resources/databaseDropPreparation.ts +120 -0
  227. package/resources/databases.ts +1155 -205
  228. package/resources/derivedIndexRegistry.ts +122 -0
  229. package/resources/derivedIndexRuntime.ts +157 -30
  230. package/resources/derivedIndexes.ts +856 -0
  231. package/resources/fullTextSchema.ts +55 -0
  232. package/resources/fullTextSchemaLifecycle.ts +0 -15
  233. package/resources/graphql.ts +107 -12
  234. package/resources/indexes/DESIGN.md +7 -0
  235. package/resources/indexes/hnswDerivedIndex.ts +0 -260
  236. package/resources/indexes/nativeFullTextDerivedIndexLifecycle.ts +72 -9
  237. package/resources/models/DESIGN.md +39 -0
  238. package/resources/models/Models.ts +361 -18
  239. package/resources/models/TestBackend.ts +56 -1
  240. package/resources/models/analyticsTable.ts +6 -4
  241. package/resources/models/backendHelpers.ts +17 -0
  242. package/resources/models/backendRegistry.ts +76 -21
  243. package/resources/models/bootstrap.ts +57 -7
  244. package/resources/models/decideHook.ts +135 -0
  245. package/resources/models/decision.ts +505 -0
  246. package/resources/models/decisionStore.ts +419 -0
  247. package/resources/models/embedHook.ts +272 -55
  248. package/resources/models/generativeDecision.ts +459 -0
  249. package/resources/models/types.ts +231 -4
  250. package/security/DESIGN.md +22 -2
  251. package/security/authn/oidc/tokenExchange.ts +6 -34
  252. package/security/authn/oidc/tokenUseTable.ts +27 -0
  253. package/security/certificateVerification/certificateVerificationSource.ts +2 -3
  254. package/security/certificateVerification/crlVerification.ts +96 -174
  255. package/security/certificateVerification/types.ts +0 -3
  256. package/security/certificateVerification/verificationTables.ts +124 -0
  257. package/security/certificateVerification/verificationUtils.ts +8 -32
  258. package/security/jsLoader.ts +9 -1
  259. package/server/DESIGN.md +27 -1
  260. package/server/Server.ts +2 -1
  261. package/server/http.ts +4 -2
  262. package/server/itc/serverHandlers.js +29 -1
  263. package/server/jobs/jobOwnership.ts +89 -0
  264. package/server/jobs/jobProcess.ts +8 -2
  265. package/server/jobs/jobs.ts +15 -4
  266. package/server/loadRootComponents.js +11 -0
  267. package/server/serverHelpers/contentTypes.ts +13 -1
  268. package/server/serverHelpers/serverHandlers.js +11 -2
  269. package/server/threads/itc.js +32 -6
  270. package/server/threads/manageThreads.js +268 -122
  271. package/server/threads/threadServer.js +13 -1
  272. package/server/threads/windowsProcessTree.ts +409 -0
  273. package/server/throttle.ts +11 -1
  274. package/studio/web/assets/{Chat-CveGbxe6.js → Chat-Bq60AO-g.js} +1 -1
  275. package/studio/web/assets/{FloatingChat-IpCFgYWL.js → FloatingChat-D4560T0p.js} +3 -3
  276. package/studio/web/assets/{apiToken-CS8fFTgL.js → apiToken-BEvbFPMc.js} +1 -1
  277. package/studio/web/assets/{applications-D7F4Jopl.js → applications-CSUVTH4E.js} +1 -1
  278. package/studio/web/assets/{cssMode-Daw_oLPq.js → cssMode-DJyc9mHj.js} +1 -1
  279. package/studio/web/assets/{editor-B3Bs1zWX.js → editor-DDIsQfj6.js} +1 -1
  280. package/studio/web/assets/{html-CDaQwWiE.js → html-g1BJHxLy.js} +1 -1
  281. package/studio/web/assets/{htmlMode-Cb6emO4U.js → htmlMode-Vh5IglCQ.js} +1 -1
  282. package/studio/web/assets/{index-B4g2Fu_2.js → index-DGRAzvO4.js} +5 -5
  283. package/studio/web/assets/{index.lazy-NwhvF1q1.js → index.lazy-vgSic7et.js} +1 -1
  284. package/studio/web/assets/{javascript-2_86gB9Q.js → javascript-5VIolqf7.js} +1 -1
  285. package/studio/web/assets/{jsonMode-DX-i86uQ.js → jsonMode-DG4zQraI.js} +1 -1
  286. package/studio/web/assets/{languageServices-B85WN-Ej.js → languageServices-t5FBpX-i.js} +1 -1
  287. package/studio/web/assets/{lspLanguageFeatures-JCWHIsKg.js → lspLanguageFeatures-DgVnNPh-.js} +1 -1
  288. package/studio/web/assets/{notifications-D9ejzt52.js → notifications-BeWcX04m.js} +1 -1
  289. package/studio/web/assets/{notifications-B7hjKhai.js → notifications-DUGWrWoO.js} +1 -1
  290. package/studio/web/assets/{profile-D8B6No3F.js → profile-BvUEDBzW.js} +1 -1
  291. package/studio/web/assets/{regions-DFazIfTK.js → regions-CB-_2m7M.js} +1 -1
  292. package/studio/web/assets/{register-CwFLviLF.js → register-1w1Zr3b2.js} +2 -2
  293. package/studio/web/assets/{setComponentFile-VnJ80SSJ.js → setComponentFile-z4-qJCbA.js} +1 -1
  294. package/studio/web/assets/{setup-BYQxFy-C.js → setup-CS50uJ4x.js} +2 -2
  295. package/studio/web/assets/{status-Cn5XjmsL.js → status-CI0wOdOE.js} +1 -1
  296. package/studio/web/assets/{toggleHighContrast-g9xIh6TX.js → toggleHighContrast-BsAE4jPe.js} +1 -1
  297. package/studio/web/assets/{tsMode-EC-n0Wqc.js → tsMode-D83Gg3cq.js} +1 -1
  298. package/studio/web/assets/{typescript-DHmM9JCO.js → typescript-Dfp-GrNq.js} +1 -1
  299. package/studio/web/assets/{useEntityRestURL-BUdnVSVv.js → useEntityRestURL-BKetlINf.js} +1 -1
  300. package/studio/web/assets/{workers-BVl9bBB-.js → workers-CCvmDLH3.js} +1 -1
  301. package/studio/web/assets/{xml-C4_sqOkX.js → xml-06pZp00U.js} +1 -1
  302. package/studio/web/assets/{yaml-CVkbDqtW.js → yaml-iSnWMVlV.js} +1 -1
  303. package/studio/web/index.html +1 -1
  304. package/upgrade/directives/5-3-0.ts +61 -54
  305. package/utility/DESIGN.md +5 -1
  306. package/utility/errors/hdbError.ts +56 -8
  307. package/utility/fsync.ts +16 -0
  308. package/utility/hdbTerms.ts +3 -0
  309. package/utility/signalling.ts +33 -4
  310. package/utility/when.ts +22 -0
  311. package/validation/configValidator.ts +36 -7
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
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
+ exports.__setReadOnlyModeForTest = __setReadOnlyModeForTest;
40
41
  exports.isReadOnlyMode = isReadOnlyMode;
41
42
  exports.getRocksCompression = getRocksCompression;
42
43
  exports.resetRocksCompression = resetRocksCompression;
@@ -58,12 +59,17 @@ exports.retakeBranchIdentity = retakeBranchIdentity;
58
59
  exports.releaseBranchIdentity = releaseBranchIdentity;
59
60
  exports.isBranchIdentity = isBranchIdentity;
60
61
  exports.openBranchDatabase = openBranchDatabase;
62
+ exports.branchDatabasesHaveWork = branchDatabasesHaveWork;
61
63
  exports.closeBranchDatabases = closeBranchDatabases;
62
64
  exports.resetDatabases = resetDatabases;
63
65
  exports.resolveDatabaseStorageRoot = resolveDatabaseStorageRoot;
64
66
  exports.resolveDatabasePath = resolveDatabasePath;
65
67
  exports.database = database;
66
68
  exports.dropDatabase = dropDatabase;
69
+ exports.databaseDropRecoveryPending = databaseDropRecoveryPending;
70
+ exports.databaseDropPreparationTargets = databaseDropPreparationTargets;
71
+ exports.prepareDatabaseDrop = prepareDatabaseDrop;
72
+ exports.completeDatabaseDropPreparation = completeDatabaseDropPreparation;
67
73
  exports.closeDatabase = closeDatabase;
68
74
  exports.closeLoadedDatabases = closeLoadedDatabases;
69
75
  exports.table = table;
@@ -113,8 +119,10 @@ const hnswPlaneBinding_ts_1 = require("./indexes/hnswPlaneBinding.js");
113
119
  const OpenDBIObject_ts_1 = require("../utility/lmdb/OpenDBIObject.js");
114
120
  const rocksdb_js_1 = require("@harperfast/rocksdb-js");
115
121
  const PrimaryRocksDatabase_ts_1 = require("./PrimaryRocksDatabase.js");
122
+ const DatabaseTransaction_ts_1 = require("./DatabaseTransaction.js");
116
123
  const replayLogs_ts_1 = require("./replayLogs.js");
117
- const hnswDerivedIndex_ts_1 = require("./indexes/hnswDerivedIndex.js");
124
+ const derivedIndexes_ts_1 = require("./derivedIndexes.js");
125
+ const derivedIndexRegistry_ts_1 = require("./derivedIndexRegistry.js");
118
126
  const node_os_1 = require("node:os");
119
127
  const RocksIndexStore_ts_1 = require("./RocksIndexStore.js");
120
128
  const rocksMemoryConfig_ts_1 = require("../utility/rocksMemoryConfig.js");
@@ -122,7 +130,9 @@ const processManagement_js_1 = require("../utility/processManagement/processMana
122
130
  const fullTextSchema_ts_1 = require("./fullTextSchema.js");
123
131
  const jsonSchemaTypes_ts_1 = require("./jsonSchemaTypes.js");
124
132
  const fullTextSchemaLifecycle_ts_1 = require("./fullTextSchemaLifecycle.js");
133
+ const embedHook_ts_1 = require("./models/embedHook.js");
125
134
  const restoreMarker_ts_1 = require("../dataLayer/restoreMarker.js");
135
+ const databaseDropPreparation_ts_1 = require("./databaseDropPreparation.js");
126
136
  /**
127
137
  * Check if Harper is running in read-only mode.
128
138
  * Read-only mode can be enabled via:
@@ -131,6 +141,9 @@ const restoreMarker_ts_1 = require("../dataLayer/restoreMarker.js");
131
141
  * - storage.readOnly config setting
132
142
  */
133
143
  let _isReadOnlyMode;
144
+ function __setReadOnlyModeForTest(value) {
145
+ _isReadOnlyMode = value;
146
+ }
134
147
  function isReadOnlyMode() {
135
148
  if (_isReadOnlyMode !== undefined)
136
149
  return _isReadOnlyMode;
@@ -401,6 +414,11 @@ function openRocksDatabase(path, options) {
401
414
  }
402
415
  const lmdbDatabaseEnvs = new Map();
403
416
  const rocksdbDatabaseEnvs = new Map();
417
+ const incompleteDatabaseCloses = new Map();
418
+ function databaseRootUnavailable(rootPath) {
419
+ return ((0, databaseDropPreparation_ts_1.databaseDropPrepared)(rootPath) ||
420
+ (incompleteDatabaseCloses.size > 0 && incompleteDatabaseCloses.has((0, node_path_1.resolve)(rootPath))));
421
+ }
404
422
  // set the following in both global and exports
405
423
  (0, globals_js_1._assignPackageExport)('databases', exports.databases);
406
424
  (0, globals_js_1._assignPackageExport)('tables', exports.tables);
@@ -419,6 +437,7 @@ const PEER_REDEFINABLE_FIELDS = [
419
437
  'elements',
420
438
  'properties',
421
439
  'embed',
440
+ 'decide',
422
441
  ];
423
442
  // `indexNulls` is derived from the durable descriptor, never sent by a peer, so naming it in the
424
443
  // discard warn would blame the peer for a field it did not write.
@@ -433,6 +452,19 @@ function applyDurableDeclaration(attribute, descriptor) {
433
452
  delete attribute[field];
434
453
  }
435
454
  }
455
+ function persistedFullTextIndexGenerations(value, definitions) {
456
+ const persisted = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
457
+ const generations = Object.create(null);
458
+ for (const { name } of definitions) {
459
+ const generation = persisted[name];
460
+ if (typeof generation === 'string' && generation.length > 0)
461
+ generations[name] = generation;
462
+ }
463
+ return generations;
464
+ }
465
+ function createFullTextIndexGeneration() {
466
+ return (0, node_crypto_1.randomBytes)(16).toString('hex');
467
+ }
436
468
  /**
437
469
  * True when a descriptor claims an index build no live operation in this process can own. The PID and
438
470
  * worker generation cannot answer that alone: a container reuses PID 1 and starts the in-memory
@@ -593,6 +625,8 @@ function getDatabases() {
593
625
  // TODO: Load any databases defined with explicit storage paths from the config
594
626
  const entries = (0, node_fs_1.readdirSync)(databasePath, { withFileTypes: true });
595
627
  const blockedByRestore = databasesBlockedByRestore(databasePath);
628
+ const blockedByDrop = databaseRootsBlockedByDrop(databasePath);
629
+ const configuredDropCoversDirectory = Object.entries(schemaConfigs).some(([name, config]) => config?.path && (0, node_path_1.resolve)(config.path) === (0, node_path_1.resolve)(databasePath) && blockedByDrop.databaseNames.has(name));
596
630
  for (const databaseEntry of entries) {
597
631
  // in-progress migration staging dirs are not databases until atomically renamed into place
598
632
  if (databaseEntry.name.endsWith(hdbTerms_ts_1.MIGRATING_DIR_SUFFIX))
@@ -607,6 +641,12 @@ function getDatabases() {
607
641
  continue;
608
642
  const dbName = (0, node_path_1.basename)(databaseEntry.name, '.mdb');
609
643
  const dbPath = (0, node_path_1.join)(databasePath, databaseEntry.name);
644
+ if (databaseRootUnavailable(dbPath))
645
+ continue;
646
+ if (configuredDropCoversDirectory ||
647
+ blockedByDrop.rootPaths.has(dbPath) ||
648
+ blockedByDrop.databaseNames.has(dbName))
649
+ continue;
610
650
  if (blockedByRestore.has(dbName))
611
651
  continue;
612
652
  if (isOpenBranchPath(dbPath))
@@ -641,10 +681,16 @@ function getDatabases() {
641
681
  if (!schemaEntry.isFile()) {
642
682
  const schemaPath = (0, node_path_1.join)(baseSchemaPath, schemaEntry.name);
643
683
  const schemaAuditPath = (0, node_path_1.join)((0, initializePaths_js_1.getTransactionAuditStoreBasePath)(), schemaEntry.name);
684
+ const blockedByDrop = databaseRootsBlockedByDrop(schemaPath);
644
685
  for (const tableEntry of (0, node_fs_1.readdirSync)(schemaPath, { withFileTypes: true })) {
645
686
  if (tableEntry.isFile() && (0, node_path_1.extname)(tableEntry.name).toLowerCase() === '.mdb') {
687
+ const tablePath = (0, node_path_1.join)(schemaPath, tableEntry.name);
688
+ if (databaseRootUnavailable(tablePath))
689
+ continue;
690
+ if (blockedByDrop.rootPaths.has(tablePath) || blockedByDrop.databaseNames.has(schemaEntry.name))
691
+ continue;
646
692
  const auditPath = (0, node_path_1.join)(schemaAuditPath, tableEntry.name);
647
- readMetaDb((0, node_path_1.join)(schemaPath, tableEntry.name), (0, node_path_1.basename)(tableEntry.name, '.mdb'), schemaEntry.name, auditPath, true);
693
+ readMetaDb(tablePath, (0, node_path_1.basename)(tableEntry.name, '.mdb'), schemaEntry.name, auditPath, true);
648
694
  }
649
695
  }
650
696
  }
@@ -653,10 +699,13 @@ function getDatabases() {
653
699
  if (schemaConfigs) {
654
700
  for (const dbName in schemaConfigs) {
655
701
  const schemaConfig = schemaConfigs[dbName];
702
+ if (databaseDropRecoveryPending(dbName))
703
+ continue;
656
704
  const databasePath = schemaConfig.path;
657
705
  if ((0, node_fs_1.existsSync)(databasePath)) {
658
706
  const entries = (0, node_fs_1.readdirSync)(databasePath, { withFileTypes: true });
659
707
  const blockedByRestore = databasesBlockedByRestore(databasePath);
708
+ const blockedByDrop = databaseRootsBlockedByDrop(databasePath);
660
709
  for (const databaseEntry of entries) {
661
710
  if (databaseEntry.name.endsWith(hdbTerms_ts_1.MIGRATING_DIR_SUFFIX))
662
711
  continue; // migration staging dir
@@ -666,14 +715,18 @@ function getDatabases() {
666
715
  continue; // reserved branch root
667
716
  if (blockedByRestore.has((0, node_path_1.basename)(databaseEntry.name, '.mdb')))
668
717
  continue;
669
- if (isOpenBranchPath((0, node_path_1.join)(databasePath, databaseEntry.name)))
718
+ const dbPath = (0, node_path_1.join)(databasePath, databaseEntry.name);
719
+ if (databaseRootUnavailable(dbPath))
720
+ continue;
721
+ if (blockedByDrop.rootPaths.has(dbPath) || blockedByDrop.databaseNames.has(dbName))
722
+ continue;
723
+ if (isOpenBranchPath(dbPath))
670
724
  continue;
671
725
  if (databaseEntry.isFile() && (0, node_path_1.extname)(databaseEntry.name).toLowerCase() === '.mdb') {
672
- readMetaDb((0, node_path_1.join)(databasePath, databaseEntry.name), (0, node_path_1.basename)(databaseEntry.name, '.mdb'), dbName);
726
+ readMetaDb(dbPath, (0, node_path_1.basename)(databaseEntry.name, '.mdb'), dbName);
673
727
  }
674
728
  else {
675
729
  try {
676
- const dbPath = (0, node_path_1.join)(databasePath, databaseEntry.name);
677
730
  const files = (0, node_fs_1.readdirSync)(dbPath, { withFileTypes: true });
678
731
  if (files.find((file) => file.name === 'CURRENT')?.isFile() &&
679
732
  files.some((file) => file.name.startsWith('MANIFEST-'))) {
@@ -694,7 +747,7 @@ function getDatabases() {
694
747
  for (const tableName in tableConfigs) {
695
748
  const tableConfig = tableConfigs[tableName];
696
749
  const tablePath = (0, node_path_1.join)(tableConfig.path, (0, node_path_1.basename)(tableName + '.mdb'));
697
- if ((0, node_fs_1.existsSync)(tablePath)) {
750
+ if (!databaseRootUnavailable(tablePath) && !(0, restoreMarker_ts_1.databaseDropMarkerPresent)(tablePath) && (0, node_fs_1.existsSync)(tablePath)) {
698
751
  readMetaDb(tablePath, tableName, dbName, null, true);
699
752
  }
700
753
  }
@@ -704,6 +757,8 @@ function getDatabases() {
704
757
  }
705
758
  // now remove any databases or tables that have been removed
706
759
  for (const dbName in exports.databases) {
760
+ if (databaseUsesPreparedRoot(dbName))
761
+ continue;
707
762
  const definedTables = definedDatabases.get(dbName);
708
763
  if (definedTables) {
709
764
  const tables = exports.databases[dbName];
@@ -912,6 +967,38 @@ function databasesBlockedByRestore(databasePath) {
912
967
  }
913
968
  return blocked;
914
969
  }
970
+ const loggedDropMarkersByDirectory = new Map();
971
+ function databaseRootsBlockedByDrop(databasePath) {
972
+ const rootPaths = new Set();
973
+ const databaseNames = new Set();
974
+ const directoryKey = (0, node_path_1.resolve)(databasePath);
975
+ const previouslyLogged = loggedDropMarkersByDirectory.get(directoryKey) ?? new Set();
976
+ const currentMarkers = new Set();
977
+ for (const drop of (0, restoreMarker_ts_1.scanBlockedDatabaseDrops)(databasePath)) {
978
+ currentMarkers.add(drop.markerPath);
979
+ const message = drop.databaseName
980
+ ? `Incomplete drop of database '${drop.databaseName}' detected; not loading ${drop.rootPath} — retry drop_database to recover (${drop.markerPath})`
981
+ : `Incomplete database drop with invalid marker metadata detected; not loading ${drop.rootPath} — inspect the root and marker before manual recovery (${drop.markerPath})`;
982
+ if (previouslyLogged.has(drop.markerPath))
983
+ logger.trace(message);
984
+ else
985
+ logger.error(message);
986
+ rootPaths.add(drop.rootPath);
987
+ if (drop.databaseName)
988
+ databaseNames.add(drop.databaseName);
989
+ }
990
+ if (currentMarkers.size > 0)
991
+ loggedDropMarkersByDirectory.set(directoryKey, currentMarkers);
992
+ else
993
+ loggedDropMarkersByDirectory.delete(directoryKey);
994
+ return { rootPaths, databaseNames };
995
+ }
996
+ function configuredDropPendingWithin(databasePath, schemaConfigs) {
997
+ const normalizedPath = (0, node_path_1.resolve)(databasePath);
998
+ return Object.entries(schemaConfigs).some(([name, config]) => config?.path &&
999
+ (0, node_path_1.resolve)(config.path) === normalizedPath &&
1000
+ (0, restoreMarker_ts_1.scanBlockedDatabaseDrops)(config.path).some((drop) => drop.databaseName === name));
1001
+ }
915
1002
  /**
916
1003
  * This is responsible for reading the internal dbi of a single database file to get a list of all the tables and
917
1004
  * their indexed or registered attributes
@@ -1094,14 +1181,15 @@ function initStores(path, rootStore, databaseName, { defaultTable, auditPath, is
1094
1181
  }
1095
1182
  if (failedAttempts < MAX_INTERRUPTED_DROP_ATTEMPTS) {
1096
1183
  try {
1097
- completeInterruptedDrop(rootStore, attributesDbi, databaseName, tableName);
1184
+ const completed = completeInterruptedDrop(rootStore, attributesDbi, databaseName, tableName);
1098
1185
  // Sweep every generation this worker has ever tracked for this table, not
1099
1186
  // just the one just resolved: if a prior generation was exhausted here,
1100
1187
  // then resolved+recreated+re-dropped by another worker as this generation
1101
1188
  // without this worker ever observing a live row in between (the only other
1102
1189
  // place that sweeps), the prior generation's entry would otherwise never
1103
1190
  // be cleared.
1104
- clearInterruptedDropEntries(path, tableName);
1191
+ if (completed)
1192
+ clearInterruptedDropEntries(path, tableName);
1105
1193
  definedTables?.delete(tableName);
1106
1194
  }
1107
1195
  catch (error) {
@@ -1188,10 +1276,14 @@ function initStores(path, rootStore, databaseName, { defaultTable, auditPath, is
1188
1276
  else if (warnings.length === 0)
1189
1277
  warnedFullTextStates.delete(warningKey);
1190
1278
  }
1279
+ const fullTextIndexGenerations = persistedFullTextIndexGenerations(primaryAttribute.fullTextIndexGenerations, fullTextIndexes);
1280
+ const fullTextIndexRetirements = (0, fullTextSchema_ts_1.persistedFullTextIndexNames)(primaryAttribute.fullTextIndexRetirements);
1191
1281
  if (table && !recreateTable) {
1192
1282
  if (primaryAttribute.audit === true && table.audit !== true)
1193
1283
  table.enableAuditing();
1194
1284
  table.fullTextIndexes = fullTextIndexes;
1285
+ table.fullTextIndexGenerations = fullTextIndexGenerations;
1286
+ table.fullTextIndexRetirements = fullTextIndexRetirements;
1195
1287
  indices = table.indices;
1196
1288
  existingAttributes = table.attributes;
1197
1289
  table.schemaVersion++;
@@ -1338,6 +1430,8 @@ function initStores(path, rootStore, databaseName, { defaultTable, auditPath, is
1338
1430
  indices,
1339
1431
  attributes,
1340
1432
  fullTextIndexes,
1433
+ fullTextIndexGenerations,
1434
+ fullTextIndexRetirements,
1341
1435
  schemaDefined: primaryAttribute.schemaDefined,
1342
1436
  dbisDB: attributesDbi,
1343
1437
  }));
@@ -1345,8 +1439,7 @@ function initStores(path, rootStore, databaseName, { defaultTable, auditPath, is
1345
1439
  if (!destination)
1346
1440
  exports.databaseEventsEmitter.emit('updateTable', table);
1347
1441
  }
1348
- void table.derivedIndexRuntime?.close();
1349
- table.derivedIndexRuntime = (0, hnswDerivedIndex_ts_1.attachDerivedIndexes)(table);
1442
+ (0, derivedIndexes_ts_1.refreshDerivedIndexes)(table);
1350
1443
  if (Array.isArray(primaryAttribute.relationships)) {
1351
1444
  relationshipsToHydrate.push({ table, databaseName, tableName, definitions: primaryAttribute.relationships });
1352
1445
  }
@@ -1650,10 +1743,27 @@ function openBranchDatabase(path, databaseName, storeName, blobRoots) {
1650
1743
  releaseBranchIdentity(storeName);
1651
1744
  const stranded = rocksdbDatabaseEnvs.get(path);
1652
1745
  rocksdbDatabaseEnvs.delete(path);
1746
+ const auditCleanup = stranded?.auditStore?.stopAuditCleanup?.();
1747
+ auditCleanup?.catch((cleanupError) => logger.warn(`Error retiring audit cleanup for branch database at ${path}`, cleanupError));
1653
1748
  closeBranchHandles(path, stranded, openedStores, tables);
1654
1749
  throw error;
1655
1750
  }
1656
- let closed = false;
1751
+ let closing;
1752
+ let handleTeardownStarted = false;
1753
+ const unregisterBranch = () => {
1754
+ if (openBranches.get(path) === branch)
1755
+ openBranches.delete(path);
1756
+ releaseBranchIdentity(storeName);
1757
+ rocksdbDatabaseEnvs.delete(path);
1758
+ manageThreads.markBranchStorePath(path, false);
1759
+ };
1760
+ const closeRemainingHandles = () => {
1761
+ const closeFailures = closeBranchHandles(path, rootStore, openedStores, tables);
1762
+ if (closeFailures.length) {
1763
+ throw new AggregateError(closeFailures, `Could not close branch database '${databaseName}'`);
1764
+ }
1765
+ unregisterBranch();
1766
+ };
1657
1767
  const branch = {
1658
1768
  tables,
1659
1769
  rootStore,
@@ -1665,14 +1775,59 @@ function openBranchDatabase(path, databaseName, storeName, blobRoots) {
1665
1775
  close() {
1666
1776
  // guard on the handle, not on the registrations: those are keyed by path, and a closed
1667
1777
  // branch frees its path, so a stale handle would otherwise tear down its successor
1668
- if (closed)
1669
- return;
1670
- closed = true;
1671
- openBranches.delete(path);
1672
- releaseBranchIdentity(storeName);
1673
- rocksdbDatabaseEnvs.delete(path);
1674
- manageThreads.markBranchStorePath(path, false);
1675
- closeBranchHandles(path, rootStore, openedStores, tables);
1778
+ if (closing)
1779
+ return closing;
1780
+ if (handleTeardownStarted) {
1781
+ const operation = Promise.resolve().then(closeRemainingHandles);
1782
+ const retryable = operation.catch((error) => {
1783
+ if (closing === retryable)
1784
+ closing = undefined;
1785
+ throw error;
1786
+ });
1787
+ closing = retryable;
1788
+ closing.catch(() => { });
1789
+ return closing;
1790
+ }
1791
+ const releaseActivation = (0, derivedIndexes_ts_1.suspendDerivedIndexActivation)(rootStore);
1792
+ const commitSuspension = (0, DatabaseTransaction_ts_1.suspendDatabaseCommits)([rootStore]);
1793
+ const deadline = Date.now() + (0, DatabaseTransaction_ts_1.getDatabaseCommitDrainTimeoutMilliseconds)();
1794
+ let closed = false;
1795
+ const operation = commitSuspension
1796
+ .waitForDrain({ deadline, databaseName })
1797
+ .then(() => settleTableMaintenance(tables, deadline))
1798
+ .then(() => settleBranchDerivedIndexes(tables))
1799
+ .then(() => rootStore.auditStore?.stopAuditCleanup?.())
1800
+ .then(() => {
1801
+ handleTeardownStarted = true;
1802
+ closeRemainingHandles();
1803
+ commitSuspension.release();
1804
+ releaseActivation();
1805
+ closed = true;
1806
+ })
1807
+ .finally(() => {
1808
+ if (!closed && !handleTeardownStarted) {
1809
+ commitSuspension.release();
1810
+ releaseActivation();
1811
+ resumeTableMaintenance(tables);
1812
+ for (const table of Object.values(tables))
1813
+ (0, derivedIndexes_ts_1.refreshDerivedIndexes)(table);
1814
+ }
1815
+ else if (!closed) {
1816
+ // Native handle teardown started, so this wrapper graph cannot safely return to service.
1817
+ (0, DatabaseTransaction_ts_1.permanentlySuspendDatabaseCommits)([rootStore]);
1818
+ (0, derivedIndexes_ts_1.permanentlySuspendDerivedIndexActivation)(rootStore);
1819
+ commitSuspension.release();
1820
+ releaseActivation();
1821
+ }
1822
+ });
1823
+ const retryable = operation.catch((error) => {
1824
+ if (closing === retryable)
1825
+ closing = undefined;
1826
+ throw error;
1827
+ });
1828
+ closing = retryable;
1829
+ closing.catch(() => { });
1830
+ return closing;
1676
1831
  },
1677
1832
  };
1678
1833
  openBranches.set(path, branch);
@@ -1689,7 +1844,7 @@ function openBranchDatabase(path, databaseName, storeName, blobRoots) {
1689
1844
  */
1690
1845
  function closeBranchHandles(path, rootStore, openedStores = [], tables = {}) {
1691
1846
  const reclamationPaths = new Set([path]);
1692
- rootStore?.auditStore?.stopAuditCleanup?.();
1847
+ const closeFailures = [];
1693
1848
  const closeStore = (store, description) => {
1694
1849
  if (!store || store.status === 'closed')
1695
1850
  return;
@@ -1700,6 +1855,7 @@ function closeBranchHandles(path, rootStore, openedStores = [], tables = {}) {
1700
1855
  }
1701
1856
  catch (error) {
1702
1857
  logger.warn(`Error closing ${description} for branch database at ${path}`, error);
1858
+ closeFailures.push(error);
1703
1859
  }
1704
1860
  };
1705
1861
  // the class, before its stores: an expiration timer or a reclamation handler on a closed store
@@ -1721,11 +1877,32 @@ function closeBranchHandles(path, rootStore, openedStores = [], tables = {}) {
1721
1877
  blob_ts_1.databasePaths.delete(rootStore);
1722
1878
  for (const reclamationPath of reclamationPaths)
1723
1879
  (0, storageReclamation_ts_1.removeStorageReclamation)(reclamationPath);
1880
+ return closeFailures;
1881
+ }
1882
+ async function settleBranchDerivedIndexes(tables) {
1883
+ const attachments = new Set();
1884
+ for (const tableName in tables) {
1885
+ const attachment = tables[tableName]?.derivedIndexRuntime;
1886
+ if (attachment)
1887
+ attachments.add(attachment);
1888
+ }
1889
+ await Promise.all([...attachments].map((attachment) => attachment.close()));
1724
1890
  }
1725
1891
  /** Branches are process-local, so this is shutdown, not a data operation. */
1726
- function closeBranchDatabases() {
1727
- for (const branch of [...openBranches.values()])
1728
- branch?.close();
1892
+ function branchDatabasesHaveWork() {
1893
+ return openBranches.size > 0;
1894
+ }
1895
+ async function closeBranchDatabases(requireClosed = false) {
1896
+ const results = await Promise.allSettled([...openBranches.values()].map((branch) => branch?.close()));
1897
+ const failures = [];
1898
+ for (const result of results) {
1899
+ if (result.status === 'rejected') {
1900
+ logger.warn('Error closing branch database during worker teardown', result.reason);
1901
+ failures.push(result.reason);
1902
+ }
1903
+ }
1904
+ if (requireClosed && failures.length > 0)
1905
+ throw new AggregateError(failures, 'Could not close every branch database during worker teardown');
1729
1906
  }
1730
1907
  function resetDatabases() {
1731
1908
  loadedDatabases = false;
@@ -1819,13 +1996,22 @@ function resolveDatabasePath(databaseName) {
1819
1996
  * @returns
1820
1997
  */
1821
1998
  function database({ database: databaseName, table: tableName }) {
1999
+ return openDatabaseRoot({ database: databaseName, table: tableName });
2000
+ }
2001
+ /**
2002
+ * Open a database root. Destructive DDL uses the internal bypass only after it owns the database's
2003
+ * drop-preparation fence; public callers must continue to fail while that fence is held.
2004
+ */
2005
+ function openDatabaseRoot({ database: databaseName, table: tableName }, { allowPreparedDrop = false } = {}) {
1822
2006
  if (!databaseName)
1823
2007
  databaseName = DEFAULT_DATABASE_NAME;
1824
2008
  getDatabases();
1825
- ensureDB(databaseName);
1826
- const definedDatabase = definedDatabases.get(databaseName);
2009
+ let definedDatabase = definedDatabases.get(databaseName);
1827
2010
  if (definedDatabase?.rootStore) {
1828
- return definedDatabase.rootStore;
2011
+ const rootStore = definedDatabase.rootStore;
2012
+ if (!allowPreparedDrop && databaseRootUnavailable(rootStore.path))
2013
+ throw new hdbError_ts_1.DatabaseClosingError(databaseName);
2014
+ return rootStore;
1829
2015
  }
1830
2016
  const databaseConfig = (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.DATABASES) || {};
1831
2017
  if (process.env.SCHEMAS_DATA_PATH) {
@@ -1833,10 +2019,36 @@ function database({ database: databaseName, table: tableName }) {
1833
2019
  }
1834
2020
  const tablePath = tableName && databaseConfig[databaseName]?.tables?.[tableName]?.path;
1835
2021
  const databasePath = resolveDatabaseStorageRoot(databaseName, tableName);
2022
+ if (!allowPreparedDrop && configuredDropPendingWithin(databasePath, databaseConfig))
2023
+ throw new hdbError_ts_1.DatabaseClosingError(databaseName);
2024
+ // A configured path is a scan root, not an exact database path, so an unresolved alias can
2025
+ // target any child store. Keep first-open fenced until the destructive operation completes.
2026
+ if (!allowPreparedDrop && databaseConfig[databaseName]?.path) {
2027
+ const blockedRestores = (0, restoreMarker_ts_1.scanBlockedRestores)(databasePath);
2028
+ if (blockedRestores.length > 0) {
2029
+ const error = new Error(`Database '${databaseName}' shares a configured path with an ${blockedRestores[0][1]} restore of '${blockedRestores[0][0]}'; complete that restore before reopening it`);
2030
+ error.statusCode = 409;
2031
+ throw error;
2032
+ }
2033
+ if ((0, databaseDropPreparation_ts_1.databaseDropPreparedWithin)(databasePath))
2034
+ throw new hdbError_ts_1.DatabaseClosingError(databaseName);
2035
+ if ((0, restoreMarker_ts_1.scanBlockedDatabaseDrops)(databasePath).length > 0) {
2036
+ const error = new Error(`Database '${databaseName}' shares a configured path with an incomplete drop; retry drop_database to recover it`);
2037
+ error.statusCode = 409;
2038
+ throw error;
2039
+ }
2040
+ }
1836
2041
  let rootStore;
1837
2042
  const useRocksdb = (process.env.HARPER_STORAGE_ENGINE || (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.STORAGE_ENGINE)) !== 'lmdb';
2043
+ const path = useRocksdb
2044
+ ? (0, node_path_1.join)(databasePath, tablePath ? tableName : databaseName)
2045
+ : (0, node_path_1.join)(databasePath, `${tablePath ? tableName : databaseName}.mdb`);
2046
+ if (!allowPreparedDrop && databaseRootUnavailable(path))
2047
+ throw new hdbError_ts_1.DatabaseClosingError(databaseName);
2048
+ throwIfBlockedByRestore(path, databaseName);
2049
+ ensureDB(databaseName);
2050
+ definedDatabase = definedDatabases.get(databaseName);
1838
2051
  if (useRocksdb) {
1839
- const path = (0, node_path_1.join)(databasePath, tablePath ? tableName : databaseName);
1840
2052
  // the scan is not the only way to reach a branch's directory: a branch leaves its store in
1841
2053
  // `rocksdbDatabaseEnvs`, so without this an on-demand open would staple it onto
1842
2054
  // `definedDatabases` and the next `closeDatabase` would close it under the live handle
@@ -1850,7 +2062,6 @@ function database({ database: databaseName, table: tableName }) {
1850
2062
  // this on-demand open (create_table/create_database and friends) must not resurrect a
1851
2063
  // database that a restore is rewriting (or left half-purged) — the scan-time restore
1852
2064
  // checks don't cover this path
1853
- throwIfBlockedByRestore(path, databaseName);
1854
2065
  rootStore = openRocksDatabase(path, {
1855
2066
  disableWAL: false,
1856
2067
  enableStats: true,
@@ -1859,7 +2070,6 @@ function database({ database: databaseName, table: tableName }) {
1859
2070
  }
1860
2071
  }
1861
2072
  else {
1862
- const path = (0, node_path_1.join)(databasePath, `${tablePath ? tableName : databaseName}.mdb`);
1863
2073
  rootStore = lmdbDatabaseEnvs.get(path);
1864
2074
  if (!rootStore || rootStore.status === 'closed') {
1865
2075
  // TODO: validate database name
@@ -1876,6 +2086,11 @@ function database({ database: databaseName, table: tableName }) {
1876
2086
  return rootStore;
1877
2087
  }
1878
2088
  function throwIfBlockedByRestore(dbPath, databaseName) {
2089
+ if ((0, restoreMarker_ts_1.databaseDropMarkerPresent)(dbPath)) {
2090
+ const error = new Error(`Database '${databaseName}' has an incomplete drop; retry drop_database to recover it`);
2091
+ error.statusCode = 409;
2092
+ throw error;
2093
+ }
1879
2094
  const restoreState = (0, restoreMarker_ts_1.checkRestoreState)(dbPath);
1880
2095
  if (restoreState !== 'clear') {
1881
2096
  const error = new Error(restoreState === 'in-progress'
@@ -1885,83 +2100,234 @@ function throwIfBlockedByRestore(dbPath, databaseName) {
1885
2100
  throw error;
1886
2101
  }
1887
2102
  }
1888
- /**
1889
- * Take the per-database restore lock for a drop, refusing (409) if a restore holds it (in-progress)
1890
- * or a crashed restore left a marker (incomplete). Pushes the acquired lock onto `held` so the
1891
- * caller releases it after the drop. On refusal, releases anything already held and throws.
1892
- *
1893
- * The lock is not reentrant within a process, so a path already in `held` must be skipped — every
1894
- * table in a RocksDB database shares one root store (and one lock path), and re-acquiring it in the
1895
- * same drop would spuriously 409 on the second table.
1896
- */
1897
- function lockDatabaseForDrop(dbPath, databaseName, held) {
1898
- if (held.some((h) => h.dbPath === dbPath))
2103
+ function lockDatabaseForDrop(dbPath, databaseName, held, blobDatabaseName = databaseName, blobPaths = (0, blob_ts_1.getBlobPathsForDatabaseName)(blobDatabaseName)) {
2104
+ if (held.some((lock) => lock.dbPath === dbPath))
1899
2105
  return;
1900
- let lock;
1901
2106
  try {
1902
- lock = (0, restoreMarker_ts_1.acquireRestoreLock)(dbPath);
2107
+ held.push((0, restoreMarker_ts_1.beginDatabaseDrop)(dbPath, databaseName, blobDatabaseName, blobPaths));
1903
2108
  }
1904
2109
  catch (error) {
1905
- for (const h of held)
1906
- (0, restoreMarker_ts_1.releaseRestoreLock)(h);
1907
- throw error; // 409: a restore is in progress and holds the lock
1908
- }
1909
- // We now hold the lock, so no restore is active. A surviving marker is therefore debris from a
1910
- // crashed restore (incomplete) — refuse rather than delete a directory that still needs recovery.
1911
- if ((0, restoreMarker_ts_1.restoreMarkerPresent)(dbPath)) {
1912
- (0, restoreMarker_ts_1.releaseRestoreLock)(lock);
1913
- for (const h of held)
1914
- (0, restoreMarker_ts_1.releaseRestoreLock)(h);
1915
- const error = new Error(`Database '${databaseName}' has an incomplete restore; rerun restore_backup to recover it`);
1916
- error.statusCode = 409;
2110
+ const cleanupFailures = [];
2111
+ for (const lock of held.splice(0)) {
2112
+ try {
2113
+ (0, restoreMarker_ts_1.cancelDatabaseDrop)(lock);
2114
+ }
2115
+ catch (cleanupError) {
2116
+ cleanupFailures.push(cleanupError);
2117
+ }
2118
+ }
2119
+ if (cleanupFailures.length > 0)
2120
+ throw new AggregateError([error, ...cleanupFailures], `Could not cancel database drop '${databaseName}'`);
1917
2121
  throw error;
1918
2122
  }
1919
- held.push(lock);
2123
+ }
2124
+ function releaseDatabaseDropLocks(locks, retainMarkers) {
2125
+ const failures = [];
2126
+ while (locks.length > 0) {
2127
+ try {
2128
+ const lock = locks.shift();
2129
+ if (retainMarkers)
2130
+ (0, restoreMarker_ts_1.abandonDatabaseDrop)(lock);
2131
+ else
2132
+ (0, restoreMarker_ts_1.cancelDatabaseDrop)(lock);
2133
+ }
2134
+ catch (error) {
2135
+ failures.push(error);
2136
+ }
2137
+ }
2138
+ if (failures.length > 0)
2139
+ throw new AggregateError(failures, 'Could not release database drop locks');
2140
+ }
2141
+ function inferDropBlobDatabaseName(databaseName, rootPath) {
2142
+ const schemaConfigs = (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.DATABASES) || {};
2143
+ const databaseConfig = schemaConfigs[databaseName];
2144
+ const normalizedRootPath = (0, node_path_1.resolve)(rootPath);
2145
+ for (const [tableName, tableConfig] of Object.entries(databaseConfig?.tables || {})) {
2146
+ if (!tableConfig?.path)
2147
+ continue;
2148
+ if ([(0, node_path_1.resolve)(tableConfig.path, tableName), (0, node_path_1.resolve)(tableConfig.path, `${tableName}.mdb`)].includes(normalizedRootPath))
2149
+ return databaseName;
2150
+ }
2151
+ if ((0, node_path_1.resolve)((0, node_path_1.dirname)(rootPath)) === (0, node_path_1.resolve)((0, initializePaths_js_1.getBaseSchemaPath)(), databaseName))
2152
+ return databaseName;
2153
+ const physicalName = (0, node_path_1.basename)(rootPath).replace(/\.mdb$/, '');
2154
+ const rootDirectory = (0, node_path_1.resolve)((0, node_path_1.dirname)(rootPath));
2155
+ if (databaseConfig?.path && (0, node_path_1.resolve)(databaseConfig.path) === rootDirectory) {
2156
+ if (!schemaConfigs[physicalName]?.path)
2157
+ return physicalName;
2158
+ for (const [configuredName, configured] of Object.entries(schemaConfigs)) {
2159
+ if (configured?.path && (0, node_path_1.resolve)(configured.path) === rootDirectory)
2160
+ return configuredName;
2161
+ }
2162
+ }
2163
+ try {
2164
+ const storageRoot = resolveDatabaseStorageRoot(databaseName);
2165
+ if ([(0, node_path_1.resolve)(storageRoot, databaseName), (0, node_path_1.resolve)(storageRoot, `${databaseName}.mdb`)].includes(normalizedRootPath))
2166
+ return databaseName;
2167
+ }
2168
+ catch { }
2169
+ return physicalName;
2170
+ }
2171
+ const incompleteDatabaseDropStores = new Map();
2172
+ function rememberIncompleteDatabaseClose(databaseNames, rootPaths, retry) {
2173
+ const names = new Set(databaseNames);
2174
+ const normalizedPaths = [...new Set([...rootPaths].map((rootPath) => (0, node_path_1.resolve)(rootPath)))];
2175
+ const incompleteClose = { databaseNames: names, rootPaths: normalizedPaths, retry };
2176
+ for (const rootPath of normalizedPaths)
2177
+ incompleteDatabaseCloses.set(rootPath, incompleteClose);
2178
+ logger.warn(`Database '${[...names].join("', '")}' handles remain partially closed on this worker; the affected roots stay unavailable until drop_database is retried or the worker restarts`, normalizedPaths);
2179
+ }
2180
+ function incompleteDatabaseClosePaths(databaseName) {
2181
+ const rootPaths = new Set();
2182
+ for (const incompleteClose of new Set(incompleteDatabaseCloses.values())) {
2183
+ if (!incompleteClose.databaseNames.has(databaseName))
2184
+ continue;
2185
+ for (const rootPath of incompleteClose.rootPaths)
2186
+ rootPaths.add(rootPath);
2187
+ }
2188
+ return [...rootPaths];
2189
+ }
2190
+ async function retryIncompleteDatabaseClose(rootPath) {
2191
+ const incompleteClose = incompleteDatabaseCloses.get((0, node_path_1.resolve)(rootPath));
2192
+ if (!incompleteClose)
2193
+ return;
2194
+ if (!incompleteClose.task) {
2195
+ const operation = incompleteClose.retry().then(() => {
2196
+ for (const path of incompleteClose.rootPaths) {
2197
+ if (incompleteDatabaseCloses.get(path) === incompleteClose)
2198
+ incompleteDatabaseCloses.delete(path);
2199
+ }
2200
+ });
2201
+ let retryable;
2202
+ retryable = operation.catch((error) => {
2203
+ if (incompleteClose.task === retryable)
2204
+ incompleteClose.task = undefined;
2205
+ throw error;
2206
+ });
2207
+ incompleteClose.task = retryable;
2208
+ }
2209
+ await incompleteClose.task;
2210
+ }
2211
+ async function destroyIncompleteDatabaseRoot(rootPath) {
2212
+ const rootStore = incompleteDatabaseDropStores.get(rootPath);
2213
+ if (!rootStore || !(0, node_fs_1.existsSync)(rootPath)) {
2214
+ incompleteDatabaseDropStores.delete(rootPath);
2215
+ await (0, promises_1.rm)(rootPath, { recursive: true, force: true });
2216
+ return;
2217
+ }
2218
+ if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
2219
+ if (rootStore.status === 'open') {
2220
+ await rootStore.dbisDb?.close?.();
2221
+ await rootStore.close();
2222
+ }
2223
+ await rootStore.destroy();
2224
+ }
2225
+ else {
2226
+ if (rootStore.status === 'open')
2227
+ await rootStore.close();
2228
+ await (0, promises_1.rm)(rootPath, { force: true });
2229
+ }
2230
+ incompleteDatabaseDropStores.delete(rootPath);
2231
+ }
2232
+ async function resumeIncompleteDatabaseDrop(databaseName, rootPaths) {
2233
+ const dropLocks = [];
2234
+ let destructiveWorkStarted = false;
2235
+ try {
2236
+ const paths = new Set(rootPaths);
2237
+ for (const rootPath of paths)
2238
+ await retryIncompleteDatabaseClose(rootPath);
2239
+ if ((0, rocksdb_js_1.registryStatus)().some((entry) => paths.has(entry.path) && entry.refCount > 0 && !incompleteDatabaseDropStores.has(entry.path)))
2240
+ throw new hdbError_ts_1.DatabaseClosingError(databaseName);
2241
+ for (const rootPath of paths)
2242
+ lockDatabaseForDrop(rootPath, databaseName, dropLocks, inferDropBlobDatabaseName(databaseName, rootPath));
2243
+ destructiveWorkStarted = true;
2244
+ for (const lock of dropLocks)
2245
+ await destroyIncompleteDatabaseRoot(lock.dbPath);
2246
+ await (0, blob_ts_1.deleteBlobPaths)(dropLocks.flatMap((lock) => lock.blobPaths));
2247
+ while (dropLocks.length > 0)
2248
+ (0, restoreMarker_ts_1.completeDatabaseDrop)(dropLocks.shift());
2249
+ }
2250
+ finally {
2251
+ releaseDatabaseDropLocks(dropLocks, destructiveWorkStarted);
2252
+ }
1920
2253
  }
1921
2254
  /**
1922
2255
  * Delete the database
1923
2256
  * @param databaseName
1924
2257
  */
1925
- async function dropDatabase(databaseName) {
1926
- if (!exports.databases[databaseName])
1927
- throw new Error('Database does not exist');
1928
- const dbTables = exports.databases[databaseName];
1929
- let rootStore;
1930
- // Hold the per-database restore lock across the entire drop so its file deletion can never
1931
- // interleave with a restore's purge-and-copy on the same directory — a destroy landing after a
1932
- // restore's copy would gut a "successful" restore, and vice versa. Restore takes the same lock
1933
- // (before writing its marker), so both operations serialize on this one primitive rather than on
1934
- // a check-then-act marker probe. Released in the finally below.
1935
- const restoreLocks = [];
2258
+ async function dropDatabase(databaseName, requestedRootPaths = []) {
2259
+ if (!exports.databases[databaseName]) {
2260
+ const rootPaths = new Set(requestedRootPaths);
2261
+ if (rootPaths.size === 0)
2262
+ throw new Error('Database does not exist');
2263
+ return resumeIncompleteDatabaseDrop(databaseName, rootPaths);
2264
+ }
2265
+ const { databaseNames, dbTables, rootStores } = collectDatabaseGraph(databaseName);
2266
+ const dropLocks = [];
2267
+ let releaseDerivedIndexActivation;
2268
+ let destructiveWorkStarted = false;
2269
+ let dropCompleted = false;
1936
2270
  try {
1937
- for (const tableName in dbTables) {
1938
- const table = dbTables[tableName];
1939
- rootStore = table.primaryStore.rootStore;
1940
- if (rootStore instanceof rocksdb_js_1.RocksDatabase)
1941
- lockDatabaseForDrop(rootStore.path, databaseName, restoreLocks);
2271
+ if (rootStores.size === 0) {
2272
+ rootStores.add(openDatabaseRoot({ database: databaseName }, { allowPreparedDrop: true }));
2273
+ }
2274
+ const blobStorageByRootPath = new Map([...rootStores].map((rootStore) => [
2275
+ rootStore.path,
2276
+ {
2277
+ databaseName: rootStore.databaseName ?? databaseName,
2278
+ paths: (0, blob_ts_1.getRootBlobPathsForDB)(rootStore),
2279
+ },
2280
+ ]));
2281
+ for (const rootPath of new Set([...requestedRootPaths, ...blobStorageByRootPath.keys()])) {
2282
+ const blobStorage = blobStorageByRootPath.get(rootPath);
2283
+ const blobDatabaseName = blobStorage?.databaseName ?? inferDropBlobDatabaseName(databaseName, rootPath);
2284
+ lockDatabaseForDrop(rootPath, databaseName, dropLocks, blobDatabaseName, blobStorage?.paths ?? (0, blob_ts_1.getBlobPathsForDatabaseName)(blobDatabaseName));
2285
+ }
2286
+ const openedRootPaths = new Set([...rootStores].map((rootStore) => rootStore.path));
2287
+ const detachedRootPaths = dropLocks.map((lock) => lock.dbPath).filter((rootPath) => !openedRootPaths.has(rootPath));
2288
+ const detachedRootPathSet = new Set(detachedRootPaths);
2289
+ const detachedRootHasForeignHandle = () => (0, rocksdb_js_1.registryStatus)().some((entry) => detachedRootPathSet.has(entry.path) && entry.refCount > 0 && !incompleteDatabaseDropStores.has(entry.path));
2290
+ if (detachedRootHasForeignHandle())
2291
+ throw new hdbError_ts_1.DatabaseClosingError(databaseName);
2292
+ releaseDerivedIndexActivation = await settleDatabaseDerivedIndexes(dbTables, true, rootStores);
2293
+ destructiveWorkStarted = true;
2294
+ for (const rootStore of rootStores) {
1942
2295
  lmdbDatabaseEnvs.delete(rootStore.path);
1943
2296
  rocksdbDatabaseEnvs.delete(rootStore.path);
1944
2297
  }
1945
- for (const tableName in dbTables) {
1946
- exports.databaseEventsEmitter.emit('dropTable', tableName, databaseName);
2298
+ for (const [tableName, table] of Object.entries(dbTables)) {
2299
+ exports.databaseEventsEmitter.emit('dropTable', table.tableName ?? tableName, table.databaseName);
1947
2300
  }
1948
- if (databaseName === 'data') {
1949
- for (const tableName in exports.tables) {
1950
- delete exports.tables[tableName];
2301
+ for (const name of databaseNames) {
2302
+ if (name === 'data') {
2303
+ for (const tableName in exports.tables) {
2304
+ delete exports.tables[tableName];
2305
+ }
2306
+ delete exports.tables[DEFINED_TABLES];
1951
2307
  }
1952
- delete exports.tables[DEFINED_TABLES];
2308
+ delete exports.databases[name];
2309
+ definedDatabases?.delete(name);
2310
+ exports.databaseEventsEmitter.emit('dropDatabase', name);
1953
2311
  }
1954
- delete exports.databases[databaseName];
1955
- exports.databaseEventsEmitter.emit('dropDatabase', databaseName);
1956
- if (rootStore) {
1957
- // awaited: retirement stops the loop admitting work, the barrier is what says the pass that was
1958
- // already running has released the stores this is about to close and unlink
2312
+ const closedStores = new Set();
2313
+ for (const table of Object.values(dbTables)) {
2314
+ if (!(table?.primaryStore?.rootStore instanceof rocksdb_js_1.RocksDatabase))
2315
+ continue;
2316
+ for (const store of [...Object.values(table.indices || {}), table.primaryStore]) {
2317
+ if (!store || closedStores.has(store))
2318
+ continue;
2319
+ closedStores.add(store);
2320
+ await store.close?.();
2321
+ }
2322
+ }
2323
+ for (const rootStore of rootStores) {
1959
2324
  await rootStore.auditStore?.stopAuditCleanup?.();
1960
2325
  (0, storageReclamation_ts_1.removeStorageReclamation)(rootStore.path);
1961
2326
  if (rootStore.status === 'open') {
1962
2327
  if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
1963
- rootStore.close();
1964
- rootStore.destroy();
2328
+ await rootStore.dbisDb?.close?.();
2329
+ await rootStore.close();
2330
+ await rootStore.destroy();
1965
2331
  }
1966
2332
  else {
1967
2333
  await rootStore.close();
@@ -1969,116 +2335,367 @@ async function dropDatabase(databaseName) {
1969
2335
  }
1970
2336
  }
1971
2337
  }
1972
- else {
1973
- rootStore = database({ database: databaseName, table: null });
1974
- // a tableless database resolves its root store here rather than in the loop above, so take
1975
- // the drop lock now (still before any destructive step)
1976
- if (rootStore instanceof rocksdb_js_1.RocksDatabase)
1977
- lockDatabaseForDrop(rootStore.path, databaseName, restoreLocks);
1978
- await rootStore.auditStore?.stopAuditCleanup?.();
1979
- (0, storageReclamation_ts_1.removeStorageReclamation)(rootStore.path);
1980
- if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
1981
- rootStore.close();
1982
- rootStore.destroy();
2338
+ if (detachedRootHasForeignHandle())
2339
+ throw new hdbError_ts_1.DatabaseClosingError(databaseName);
2340
+ for (const rootPath of detachedRootPaths)
2341
+ await (0, promises_1.rm)(rootPath, { recursive: true, force: true });
2342
+ await (0, blob_ts_1.deleteBlobPaths)(dropLocks.flatMap((lock) => lock.blobPaths));
2343
+ while (dropLocks.length > 0)
2344
+ (0, restoreMarker_ts_1.completeDatabaseDrop)(dropLocks.shift());
2345
+ dropCompleted = true;
2346
+ }
2347
+ finally {
2348
+ if (destructiveWorkStarted) {
2349
+ for (const rootStore of rootStores)
2350
+ blob_ts_1.databasePaths.delete(rootStore);
2351
+ for (const rootStore of rootStores) {
2352
+ if ((0, restoreMarker_ts_1.databaseDropMarkerPresent)(rootStore.path))
2353
+ incompleteDatabaseDropStores.set(rootStore.path, rootStore);
1983
2354
  }
1984
- else if (rootStore.status === 'open') {
1985
- await rootStore.close();
1986
- await (0, promises_1.unlink)(rootStore.path);
2355
+ if (!dropCompleted) {
2356
+ (0, DatabaseTransaction_ts_1.permanentlySuspendDatabaseCommits)(rootStores);
2357
+ for (const rootStore of rootStores)
2358
+ (0, derivedIndexes_ts_1.permanentlySuspendDerivedIndexActivation)(rootStore);
1987
2359
  }
1988
2360
  }
1989
- await (0, blob_ts_1.deleteRootBlobPathsForDB)(rootStore);
1990
- }
1991
- finally {
1992
- for (const lock of restoreLocks)
1993
- (0, restoreMarker_ts_1.releaseRestoreLock)(lock);
2361
+ releaseDerivedIndexActivation?.();
2362
+ releaseDatabaseDropLocks(dropLocks, destructiveWorkStarted);
1994
2363
  }
1995
2364
  }
1996
- /**
1997
- * Close a database's store handles on the current thread and unregister it, without
1998
- * touching its files. Used by the restore_backup flow: every thread must release its handles so
1999
- * `backups.restore()` can purge and rewrite the (fully closed) database directory. A subsequent
2000
- * `resetDatabases()`/`getDatabases()` rescan reloads it (or skips it while a restore is in
2001
- * progress, per the restore marker checks in the scan).
2002
- *
2003
- * An LMDB database is closed by closing its environment, which releases every dbi in it, so every
2004
- * other database alias sharing that environment is closed with it.
2005
- */
2006
- function closeDatabase(databaseName) {
2007
- const dbTables = exports.databases[databaseName];
2008
- if (!dbTables)
2009
- return false;
2010
- const rootStores = new Set();
2011
- const closeStore = (store, description) => {
2365
+ const databaseDropPreparationTasks = new Map();
2366
+ function addPhysicalDatabaseRoots(directory, rootPaths) {
2367
+ if (!(0, node_fs_1.existsSync)(directory))
2368
+ return;
2369
+ for (const entry of (0, node_fs_1.readdirSync)(directory, { withFileTypes: true })) {
2370
+ if (entry.name === restoreMarker_ts_1.RESTORE_META_DIR || entry.name === exports.BRANCH_ROOT_DIR || entry.name.endsWith(hdbTerms_ts_1.MIGRATING_DIR_SUFFIX))
2371
+ continue;
2372
+ const rootPath = (0, node_path_1.join)(directory, entry.name);
2373
+ if (entry.isFile()) {
2374
+ if ((0, node_path_1.extname)(entry.name).toLowerCase() === '.mdb')
2375
+ rootPaths.add(rootPath);
2376
+ continue;
2377
+ }
2378
+ if (!entry.isDirectory())
2379
+ continue;
2012
2380
  try {
2013
- store?.close?.();
2381
+ const files = (0, node_fs_1.readdirSync)(rootPath, { withFileTypes: true });
2382
+ if (files.find((file) => file.name === 'CURRENT')?.isFile() &&
2383
+ files.some((file) => file.name.startsWith('MANIFEST-')))
2384
+ rootPaths.add(rootPath);
2014
2385
  }
2015
2386
  catch (error) {
2016
- logger.warn(`Error closing ${description} while closing database ${databaseName}:`, error);
2387
+ if (!('code' in error && error.code === 'ENOENT'))
2388
+ throw error;
2389
+ }
2390
+ }
2391
+ }
2392
+ function databaseDropRecoveryGraphPaths(databaseName, incompleteRootPaths) {
2393
+ const rootPaths = new Set(incompleteRootPaths);
2394
+ if (incompleteRootPaths.length === 0)
2395
+ return [];
2396
+ const databaseConfig = ((0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.DATABASES) || {})[databaseName];
2397
+ if (databaseConfig?.path)
2398
+ addPhysicalDatabaseRoots(databaseConfig.path, rootPaths);
2399
+ addPhysicalDatabaseRoots((0, node_path_1.join)((0, initializePaths_js_1.getBaseSchemaPath)(), databaseName), rootPaths);
2400
+ for (const [tableName, tableConfig] of Object.entries(databaseConfig?.tables || {})) {
2401
+ if (!tableConfig?.path)
2402
+ continue;
2403
+ for (const rootPath of [(0, node_path_1.join)(tableConfig.path, tableName), (0, node_path_1.join)(tableConfig.path, `${tableName}.mdb`)]) {
2404
+ if ((0, node_fs_1.existsSync)(rootPath))
2405
+ rootPaths.add(rootPath);
2406
+ }
2407
+ }
2408
+ try {
2409
+ const storageRoot = resolveDatabaseStorageRoot(databaseName);
2410
+ for (const rootPath of [(0, node_path_1.join)(storageRoot, databaseName), (0, node_path_1.join)(storageRoot, `${databaseName}.mdb`)]) {
2411
+ if ((0, node_fs_1.existsSync)(rootPath))
2412
+ rootPaths.add(rootPath);
2413
+ }
2414
+ }
2415
+ catch { }
2416
+ return [...rootPaths];
2417
+ }
2418
+ function incompleteDatabaseDropPaths(databaseName) {
2419
+ const directories = new Set();
2420
+ try {
2421
+ directories.add(resolveDatabaseStorageRoot(databaseName));
2422
+ }
2423
+ catch { }
2424
+ directories.add((0, node_path_1.join)((0, initializePaths_js_1.getBaseSchemaPath)(), databaseName));
2425
+ const databaseConfig = ((0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.DATABASES) || {})[databaseName];
2426
+ if (databaseConfig?.path)
2427
+ directories.add(databaseConfig.path);
2428
+ for (const tableConfig of Object.values(databaseConfig?.tables || {})) {
2429
+ if (tableConfig?.path)
2430
+ directories.add(tableConfig.path);
2431
+ }
2432
+ const rootPaths = new Set();
2433
+ for (const directory of directories) {
2434
+ for (const drop of (0, restoreMarker_ts_1.scanBlockedDatabaseDrops)(directory)) {
2435
+ if (drop.databaseName === databaseName)
2436
+ rootPaths.add(drop.rootPath);
2017
2437
  }
2438
+ }
2439
+ return [...rootPaths];
2440
+ }
2441
+ function databaseDropRecoveryPending(databaseName) {
2442
+ return incompleteDatabaseDropPaths(databaseName).length > 0 || incompleteDatabaseClosePaths(databaseName).length > 0;
2443
+ }
2444
+ function databaseDropPreparationTargets(databaseName) {
2445
+ getDatabases();
2446
+ const incompleteRootPaths = [
2447
+ ...new Set([...incompleteDatabaseDropPaths(databaseName), ...incompleteDatabaseClosePaths(databaseName)]),
2448
+ ];
2449
+ const recoveryRootPaths = databaseDropRecoveryGraphPaths(databaseName, incompleteRootPaths);
2450
+ if (!exports.databases[databaseName]) {
2451
+ if (recoveryRootPaths.length > 0)
2452
+ return { rootPaths: recoveryRootPaths };
2453
+ throw new Error(`Database '${databaseName}' does not exist`);
2454
+ }
2455
+ const graph = collectDatabaseGraph(databaseName);
2456
+ if (graph.rootStores.size === 0)
2457
+ graph.rootStores.add(openDatabaseRoot({ database: databaseName }));
2458
+ return {
2459
+ rootPaths: [...new Set([...recoveryRootPaths, ...[...graph.rootStores].map((rootStore) => rootStore.path)])],
2018
2460
  };
2019
- for (const tableName in dbTables) {
2020
- const table = dbTables[tableName];
2021
- if (!table?.primaryStore)
2461
+ }
2462
+ function prepareDatabaseDrop(databaseName, preparationId, ownerThreadId, requestedRootPaths = []) {
2463
+ const existing = databaseDropPreparationTasks.get(databaseName);
2464
+ if (existing?.id === preparationId)
2465
+ return existing.task;
2466
+ const rootPaths = new Set(requestedRootPaths);
2467
+ if (exports.databases[databaseName]) {
2468
+ const graph = collectDatabaseGraph(databaseName);
2469
+ for (const rootStore of graph.rootStores)
2470
+ rootPaths.add(rootStore.path);
2471
+ }
2472
+ const names = [];
2473
+ for (const name of Object.keys(exports.databases)) {
2474
+ const definedRoot = definedDatabases?.get(name)?.rootStore;
2475
+ if (definedRoot && rootPaths.has(definedRoot.path)) {
2476
+ names.push(name);
2022
2477
  continue;
2023
- if (table.primaryStore.rootStore)
2024
- rootStores.add(table.primaryStore.rootStore);
2478
+ }
2479
+ const tables = exports.databases[name];
2480
+ if (Object.values(tables).some((table) => {
2481
+ const rootStore = table?.primaryStore?.rootStore;
2482
+ return rootStore && rootPaths.has(rootStore.path);
2483
+ }))
2484
+ names.push(name);
2025
2485
  }
2026
- // a database with no tables (an empty schema, or one whose tables were all dropped) still holds
2027
- // an open root store, tracked only on the defined-database entry rather than any table — include
2028
- // it so its handles are released too (the Set dedupes it against the per-table root stores above)
2029
- const definedRoot = definedDatabases?.get(databaseName)?.rootStore;
2030
- if (definedRoot)
2031
- rootStores.add(definedRoot);
2032
- // before any table store closes, so no further pass is admitted. This is synchronous, so it cannot
2033
- // await the drain barrier stopAuditCleanup() returns; what covers it is the in-pass status checks,
2034
- // plus the fact that its production callers reach it only for RocksDB databases, whose pass is one
2035
- // synchronous purgeLogs() call with nothing suspended mid-removal.
2036
- for (const rootStore of rootStores)
2037
- rootStore.auditStore?.stopAuditCleanup?.();
2486
+ const paths = [...rootPaths];
2487
+ (0, databaseDropPreparation_ts_1.claimDatabaseDropPreparations)(paths, preparationId, ownerThreadId, databaseName);
2488
+ const task = (async () => {
2489
+ const failures = [];
2490
+ for (const rootPath of paths) {
2491
+ try {
2492
+ await retryIncompleteDatabaseClose(rootPath);
2493
+ }
2494
+ catch (error) {
2495
+ failures.push(error);
2496
+ }
2497
+ }
2498
+ for (const name of names) {
2499
+ try {
2500
+ await closeDatabase(name, { requireClosed: true });
2501
+ }
2502
+ catch (error) {
2503
+ failures.push(error);
2504
+ }
2505
+ }
2506
+ for (const rootPath of paths) {
2507
+ if (!incompleteDatabaseDropStores.has(rootPath))
2508
+ continue;
2509
+ try {
2510
+ await destroyIncompleteDatabaseRoot(rootPath);
2511
+ }
2512
+ catch (error) {
2513
+ failures.push(error);
2514
+ }
2515
+ }
2516
+ if (failures.length > 0)
2517
+ throw new AggregateError(failures, `Could not prepare database '${databaseName}' for drop`);
2518
+ })();
2519
+ databaseDropPreparationTasks.set(databaseName, { id: preparationId, task, rootPaths: paths });
2520
+ for (const rootPath of paths)
2521
+ (0, databaseDropPreparation_ts_1.trackDatabaseDropPreparationTask)(rootPath, preparationId, task);
2522
+ return task;
2523
+ }
2524
+ async function completeDatabaseDropPreparation(databaseName, preparationId, requestedRootPaths = []) {
2525
+ const existing = databaseDropPreparationTasks.get(databaseName);
2526
+ if (existing?.id === preparationId) {
2527
+ try {
2528
+ await existing.task;
2529
+ }
2530
+ catch {
2531
+ // Preparation already reported this failure to the strict broadcaster.
2532
+ }
2533
+ databaseDropPreparationTasks.delete(databaseName);
2534
+ }
2535
+ (0, databaseDropPreparation_ts_1.releaseDatabaseDropPreparations)(existing?.rootPaths ?? requestedRootPaths, preparationId);
2536
+ }
2537
+ async function closeDatabaseStores(databaseName, dbTables, rootStores, requireClosed) {
2538
+ const rootStorePaths = [];
2539
+ const closeFailures = [];
2540
+ const closeStore = async (store, description) => {
2541
+ if (!store || store.status === 'closed')
2542
+ return true;
2543
+ try {
2544
+ await store.close?.();
2545
+ return true;
2546
+ }
2547
+ catch (error) {
2548
+ logger.warn(`Error closing ${description} while closing database ${databaseName}:`, error);
2549
+ closeFailures.push(error);
2550
+ return false;
2551
+ }
2552
+ };
2038
2553
  const lmdbRootStores = new Set([...rootStores].filter((rootStore) => !(rootStore instanceof rocksdb_js_1.RocksDatabase)));
2039
2554
  for (const tableName in dbTables) {
2040
2555
  const table = dbTables[tableName];
2041
- // a dbi closed natively after its environment closed (through another alias sharing it) runs
2042
- // mdb_dbi_close against the freed environment
2043
2556
  if (!table?.primaryStore || lmdbRootStores.has(table.primaryStore.rootStore))
2044
2557
  continue;
2558
+ const tableIdentity = `${table.databaseName ?? databaseName}.${table.tableName ?? tableName}`;
2045
2559
  for (const indexName in table.indices || {}) {
2046
- closeStore(table.indices[indexName], `index ${tableName}.${indexName}`);
2560
+ await closeStore(table.indices[indexName], `index ${tableIdentity}.${indexName}`);
2047
2561
  }
2048
- closeStore(table.primaryStore, `table ${tableName}`);
2562
+ await closeStore(table.primaryStore, `table ${tableIdentity}`);
2049
2563
  }
2050
2564
  for (const rootStore of rootStores) {
2051
2565
  (0, storageReclamation_ts_1.removeStorageReclamation)(rootStore.path);
2566
+ blob_ts_1.databasePaths.delete(rootStore);
2567
+ let rootClosed = rootStore.status !== 'open';
2052
2568
  if (!lmdbRootStores.has(rootStore)) {
2053
- closeStore(rootStore.dbisDb, 'attributes store');
2054
- closeStore(rootStore, 'root store');
2055
- }
2056
- else if (rootStore.status === 'open')
2057
- closeStore(rootStore, 'root store');
2058
- lmdbDatabaseEnvs.delete(rootStore.path);
2059
- rocksdbDatabaseEnvs.delete(rootStore.path);
2060
- }
2061
- unregisterDatabase(databaseName);
2062
- for (const aliasName of Object.keys(exports.databases)) {
2063
- for (const rootStore of lmdbRootStores) {
2064
- if (databaseUsesRootStore(aliasName, rootStore)) {
2065
- closeDatabase(aliasName);
2569
+ await closeStore(rootStore.dbisDb, 'attributes store');
2570
+ rootClosed = await closeStore(rootStore, 'root store');
2571
+ }
2572
+ else if (!rootClosed)
2573
+ rootClosed = await closeStore(rootStore, 'root store');
2574
+ if (rootClosed || !requireClosed)
2575
+ rootStorePaths.push(rootStore.path);
2576
+ }
2577
+ for (const path of rootStorePaths) {
2578
+ lmdbDatabaseEnvs.delete(path);
2579
+ rocksdbDatabaseEnvs.delete(path);
2580
+ }
2581
+ return closeFailures;
2582
+ }
2583
+ /**
2584
+ * Close a database's store handles on this thread and unregister every alias sharing those roots,
2585
+ * without touching files. Restore uses this before rewriting the closed directory. With
2586
+ * `requireClosed`, any handle failure rejects and leaves the affected wrappers permanently fenced
2587
+ * for an explicit retry. Closing an LMDB environment closes every DBI and alias that shares it.
2588
+ */
2589
+ async function closeDatabase(databaseName, { requireClosed = false } = {}) {
2590
+ if (!exports.databases[databaseName])
2591
+ return false;
2592
+ const { databaseNames, dbTables, rootStores } = collectDatabaseGraph(databaseName);
2593
+ const releaseDerivedIndexActivation = await settleDatabaseDerivedIndexes(dbTables, false, rootStores);
2594
+ let databaseClosed = false;
2595
+ let handleCloseStarted = false;
2596
+ try {
2597
+ await Promise.all([...rootStores].map((rootStore) => rootStore.auditStore?.stopAuditCleanup?.()));
2598
+ handleCloseStarted = true;
2599
+ const closeFailures = await closeDatabaseStores(databaseName, dbTables, rootStores, requireClosed);
2600
+ if (requireClosed && closeFailures.length > 0) {
2601
+ for (const rootStore of rootStores) {
2602
+ lmdbDatabaseEnvs.delete(rootStore.path);
2603
+ rocksdbDatabaseEnvs.delete(rootStore.path);
2604
+ }
2605
+ for (const name of databaseNames)
2606
+ unregisterDatabase(name);
2607
+ rememberIncompleteDatabaseClose(databaseNames, [...rootStores].map((rootStore) => rootStore.path), async () => {
2608
+ const retryFailures = await closeDatabaseStores(databaseName, dbTables, rootStores, true);
2609
+ if (retryFailures.length > 0)
2610
+ throw new AggregateError(retryFailures, `Could not finish closing database graph '${databaseName}'`);
2611
+ });
2612
+ throw new AggregateError(closeFailures, `Could not close database graph '${[...databaseNames].join("', '")}' for destructive DDL`);
2613
+ }
2614
+ for (const name of databaseNames)
2615
+ unregisterDatabase(name);
2616
+ databaseClosed = true;
2617
+ return true;
2618
+ }
2619
+ finally {
2620
+ if (handleCloseStarted && !databaseClosed) {
2621
+ (0, DatabaseTransaction_ts_1.permanentlySuspendDatabaseCommits)(rootStores);
2622
+ for (const rootStore of rootStores)
2623
+ (0, derivedIndexes_ts_1.permanentlySuspendDerivedIndexActivation)(rootStore);
2624
+ }
2625
+ else if (!databaseClosed && !handleCloseStarted) {
2626
+ resumeTableMaintenance(dbTables);
2627
+ for (const table of Object.values(dbTables))
2628
+ (0, derivedIndexes_ts_1.refreshDerivedIndexes)(table);
2629
+ }
2630
+ releaseDerivedIndexActivation();
2631
+ }
2632
+ }
2633
+ function collectDatabaseGraph(databaseName) {
2634
+ const databaseNames = new Set([databaseName]);
2635
+ const processedNames = new Set();
2636
+ const rootStores = new Set();
2637
+ const tableSet = new Set();
2638
+ for (;;) {
2639
+ for (const name of databaseNames) {
2640
+ if (processedNames.has(name))
2641
+ continue;
2642
+ processedNames.add(name);
2643
+ const tables = exports.databases[name];
2644
+ for (const tableName in tables) {
2645
+ const table = tables[tableName];
2646
+ tableSet.add(table);
2647
+ if (table?.primaryStore?.rootStore)
2648
+ rootStores.add(table.primaryStore.rootStore);
2649
+ }
2650
+ const definedRoot = definedDatabases?.get(name)?.rootStore;
2651
+ if (definedRoot)
2652
+ rootStores.add(definedRoot);
2653
+ }
2654
+ let foundAlias = false;
2655
+ for (const aliasName of Object.keys(exports.databases)) {
2656
+ if (databaseNames.has(aliasName))
2657
+ continue;
2658
+ for (const rootStore of rootStores) {
2659
+ if (!databaseUsesRootStore(aliasName, rootStore))
2660
+ continue;
2661
+ databaseNames.add(aliasName);
2662
+ foundAlias = true;
2066
2663
  break;
2067
2664
  }
2068
2665
  }
2666
+ if (!foundAlias)
2667
+ break;
2069
2668
  }
2070
- return true;
2669
+ return {
2670
+ databaseNames,
2671
+ dbTables: Object.fromEntries([...tableSet].map((table, index) => [index, table])),
2672
+ rootStores,
2673
+ };
2071
2674
  }
2072
2675
  function databaseUsesRootStore(databaseName, rootStore) {
2073
2676
  if (definedDatabases?.get(databaseName)?.rootStore === rootStore)
2074
2677
  return true;
2075
2678
  const dbTables = exports.databases[databaseName];
2679
+ if (!dbTables)
2680
+ return false;
2076
2681
  for (const tableName in dbTables) {
2077
2682
  if (dbTables[tableName]?.primaryStore?.rootStore === rootStore)
2078
2683
  return true;
2079
2684
  }
2080
2685
  return false;
2081
2686
  }
2687
+ function databaseUsesPreparedRoot(databaseName) {
2688
+ const definedRoot = definedDatabases?.get(databaseName)?.rootStore;
2689
+ if (definedRoot && (0, databaseDropPreparation_ts_1.databaseDropPrepared)(definedRoot.path))
2690
+ return true;
2691
+ const dbTables = exports.databases[databaseName];
2692
+ for (const tableName in dbTables) {
2693
+ const rootStore = dbTables[tableName]?.primaryStore?.rootStore;
2694
+ if (rootStore && (0, databaseDropPreparation_ts_1.databaseDropPrepared)(rootStore.path))
2695
+ return true;
2696
+ }
2697
+ return false;
2698
+ }
2082
2699
  function unregisterDatabase(databaseName) {
2083
2700
  const definedDatabase = definedDatabases?.get(databaseName);
2084
2701
  if (definedDatabase)
@@ -2092,43 +2709,115 @@ function unregisterDatabase(databaseName) {
2092
2709
  delete exports.databases[databaseName];
2093
2710
  }
2094
2711
  /**
2095
- * Close every RocksDB (user) database this thread has open, releasing its native handles.
2712
+ * Close every user database this thread has open, releasing its native handles.
2096
2713
  *
2097
- * rocksdb-js's registry is process-global across worker threads, and a thread that exits WITHOUT
2098
- * closing leaks its handles (the process-global refCount never drops), while the only alternative,
2099
- * `shutdown()`, tears down rocksdb for the entire process. So a worker thread that opens databases
2100
- * and then exits — notably a job worker (jobProcess), which opens the whole database graph via
2101
- * `getDatabases()` and exits when the job finishes — must close its handles explicitly, or those
2102
- * handles linger process-wide (and, e.g., block an online `restore_backup` from confirming the
2103
- * database is closed). The `system` database is intentionally left open: it is non-enumerable here
2714
+ * A job worker opens the database graph and must prove every handle is closed before its exit can
2715
+ * satisfy destructive-DDL preparation. RocksDB handles otherwise leak in the process-global
2716
+ * registry; an open LMDB handle can likewise prevent deletion on Windows. The `system` database is
2717
+ * intentionally left open: it is non-enumerable here
2104
2718
  * (skipped by the loop), is never restored online, and the exiting worker may still touch the job
2105
2719
  * table during teardown. Best-effort: closing failures are swallowed inside `closeDatabase`.
2106
2720
  *
2107
2721
  * Branches are invisible to the loop below but hold handles from the same registry, so this — the
2108
2722
  * thread's one teardown entry point — closes them too.
2109
2723
  */
2110
- function closeLoadedDatabases() {
2111
- closeBranchDatabases();
2724
+ async function closeLoadedDatabases({ requireClosed = false } = {}) {
2725
+ const closeFailures = [];
2726
+ try {
2727
+ await closeBranchDatabases(requireClosed);
2728
+ }
2729
+ catch (error) {
2730
+ closeFailures.push(error);
2731
+ }
2112
2732
  // snapshot the names first: closeDatabase() deletes from `databases` as it goes
2113
2733
  for (const databaseName of Object.keys(exports.databases)) {
2114
2734
  const dbTables = exports.databases[databaseName];
2115
2735
  if (!dbTables)
2116
2736
  continue;
2117
- let isRocks = false;
2118
- for (const tableName in dbTables) {
2119
- if (dbTables[tableName]?.primaryStore?.rootStore instanceof rocksdb_js_1.RocksDatabase) {
2120
- isRocks = true;
2121
- break;
2122
- }
2737
+ try {
2738
+ await closeDatabase(databaseName, { requireClosed });
2123
2739
  }
2124
- // a tableless database exposes no table root store, so also check the defined-database
2125
- // entry — otherwise its open root store would leak on worker exit
2126
- if (!isRocks && definedDatabases?.get(databaseName)?.rootStore instanceof rocksdb_js_1.RocksDatabase) {
2127
- isRocks = true;
2740
+ catch (error) {
2741
+ logger.warn(`Error closing database ${databaseName} during worker teardown`, error);
2742
+ closeFailures.push(error);
2128
2743
  }
2129
- if (isRocks)
2130
- closeDatabase(databaseName);
2131
2744
  }
2745
+ if (requireClosed && closeFailures.length > 0)
2746
+ throw new AggregateError(closeFailures, 'Could not close every database during worker teardown');
2747
+ }
2748
+ async function settleTableMaintenance(dbTables, deadline) {
2749
+ await Promise.all(Object.values(dbTables).map((table) => table?.closeMaintenance?.(deadline)));
2750
+ }
2751
+ function resumeTableMaintenance(dbTables) {
2752
+ for (const table of Object.values(dbTables))
2753
+ table?.resumeMaintenance?.();
2754
+ }
2755
+ async function settleDatabaseDerivedIndexes(dbTables, dropping, additionalRootStores = []) {
2756
+ const rootStores = new Set(additionalRootStores);
2757
+ for (const tableName in dbTables) {
2758
+ const rootStore = dbTables[tableName]?.primaryStore?.rootStore;
2759
+ if (rootStore)
2760
+ rootStores.add(rootStore);
2761
+ }
2762
+ const activationReleases = [...rootStores].map(derivedIndexes_ts_1.suspendDerivedIndexActivation);
2763
+ const commitSuspension = (0, DatabaseTransaction_ts_1.suspendDatabaseCommits)(rootStores);
2764
+ const deadline = Date.now() + (0, DatabaseTransaction_ts_1.getDatabaseCommitDrainTimeoutMilliseconds)();
2765
+ const databaseName = [...rootStores][0]?.databaseName ?? 'unknown';
2766
+ let lifecycleReleased = false;
2767
+ const releaseLifecycle = () => {
2768
+ if (lifecycleReleased)
2769
+ return;
2770
+ lifecycleReleased = true;
2771
+ commitSuspension.release();
2772
+ for (const release of activationReleases)
2773
+ release();
2774
+ };
2775
+ const attachments = new Map();
2776
+ for (const tableName in dbTables) {
2777
+ const table = dbTables[tableName];
2778
+ const attachment = table?.derivedIndexRuntime;
2779
+ if (!attachment)
2780
+ continue;
2781
+ let tables = attachments.get(attachment);
2782
+ if (!tables)
2783
+ attachments.set(attachment, (tables = []));
2784
+ tables.push(table);
2785
+ }
2786
+ const entries = [...attachments];
2787
+ try {
2788
+ await commitSuspension.waitForDrain({ deadline, databaseName });
2789
+ await settleInterruptedDropRetirements(rootStores);
2790
+ await settleTableMaintenance(dbTables, deadline);
2791
+ if (attachments.size === 0)
2792
+ return releaseLifecycle;
2793
+ const results = await Promise.allSettled(entries.map(async ([attachment]) => attachment.close(dropping)));
2794
+ const failures = results
2795
+ .filter((result) => result.status === 'rejected')
2796
+ .map((result) => result.reason);
2797
+ if (failures.length === 1)
2798
+ throw failures[0];
2799
+ if (failures.length)
2800
+ throw new AggregateError(failures, 'Could not settle derived indexes before closing the database');
2801
+ }
2802
+ catch (error) {
2803
+ resumeTableMaintenance(dbTables);
2804
+ releaseLifecycle();
2805
+ for (const [attachment, tables] of entries) {
2806
+ if (dropping)
2807
+ attachment.completeDrop?.(false);
2808
+ for (const table of tables)
2809
+ (0, derivedIndexes_ts_1.refreshDerivedIndexes)(table);
2810
+ }
2811
+ throw error;
2812
+ }
2813
+ for (const [attachment, tables] of entries) {
2814
+ if (dropping)
2815
+ attachment.completeDrop?.();
2816
+ for (const table of tables)
2817
+ if (table.derivedIndexRuntime === attachment)
2818
+ table.derivedIndexRuntime = undefined;
2819
+ }
2820
+ return releaseLifecycle;
2132
2821
  }
2133
2822
  // HNSW_NO_AUTOVERSION kill-switch: when set, a NEW index initializes as legacy rather than
2134
2823
  // versioned. process.env values are strings, so a bare truthiness check would treat "0"/"false"
@@ -2557,6 +3246,8 @@ function declareTable(target, tableDefinition) {
2557
3246
  let fullTextValuesForPersistence;
2558
3247
  let fullTextPersistencePending = false;
2559
3248
  let activeFullTextIndexes;
3249
+ let fullTextIndexGenerationMap = Object.create(null);
3250
+ let fullTextIndexRetirementNames;
2560
3251
  let restoreFullTextLiveState;
2561
3252
  let armFullTextLiveStateRestore;
2562
3253
  const attributesToIndex = [];
@@ -2600,7 +3291,9 @@ function declareTable(target, tableDefinition) {
2600
3291
  let persistedPrimary = persistedPrimaryDescriptor(Table.dbisDB);
2601
3292
  let persistedFullTextValues = persistedPrimary.descriptor?.fullTextIndexes;
2602
3293
  const incomingFullTextValues = fullTextIndexesExplicit ? fullTextIndexes : [];
3294
+ let fullTextValidationAttributes;
2603
3295
  if ((persistedFullTextValues !== undefined && (rootStore instanceof rocksdb_js_1.RocksDatabase || fullTextIndexesExplicit)) ||
3296
+ persistedPrimary.descriptor?.fullTextIndexRetirements !== undefined ||
2604
3297
  Table.fullTextIndexes?.length > 0 ||
2605
3298
  (fullTextIndexesExplicit && (!Array.isArray(incomingFullTextValues) || incomingFullTextValues.length > 0))) {
2606
3299
  if (!(rootStore instanceof rocksdb_js_1.RocksDatabase)) {
@@ -2609,6 +3302,7 @@ function declareTable(target, tableDefinition) {
2609
3302
  persistedFullTextValues = persistedPrimary.descriptor?.fullTextIndexes;
2610
3303
  }
2611
3304
  const originalAttributes = Table.attributes.slice();
3305
+ fullTextIndexRetirementNames = (0, fullTextSchema_ts_1.persistedFullTextIndexNames)(persistedPrimary.descriptor?.fullTextIndexRetirements);
2612
3306
  const relationshipAttributes = originalAttributes.filter((attribute) => attribute.relationship);
2613
3307
  const originalMetadata = {
2614
3308
  description: Table.description,
@@ -2629,14 +3323,15 @@ function declareTable(target, tableDefinition) {
2629
3323
  Object.assign(Table, originalMetadata);
2630
3324
  Table.properties = (0, jsonSchemaTypes_ts_1.projectAttributesToProperties)(restored);
2631
3325
  const restoredPrimary = persistedPrimaryDescriptor(Table.dbisDB).descriptor;
2632
- Table.fullTextIndexes =
2633
- rootStore instanceof rocksdb_js_1.RocksDatabase && restoredPrimary?.audit === true
2634
- ? (0, fullTextSchemaLifecycle_ts_1.readPersistedFullTextDefinitions)(restoredPrimary?.fullTextIndexes, restored, fullTextWarning)
2635
- : [];
3326
+ const restoredFullTextIndexes = rootStore instanceof rocksdb_js_1.RocksDatabase && restoredPrimary?.audit === true
3327
+ ? (0, fullTextSchemaLifecycle_ts_1.readPersistedFullTextDefinitions)(restoredPrimary?.fullTextIndexes, restored, fullTextWarning)
3328
+ : [];
3329
+ Table.fullTextIndexes = restoredFullTextIndexes;
3330
+ Table.fullTextIndexGenerations = persistedFullTextIndexGenerations(restoredPrimary?.fullTextIndexGenerations, restoredFullTextIndexes);
3331
+ Table.fullTextIndexRetirements = (0, fullTextSchema_ts_1.persistedFullTextIndexNames)(restoredPrimary?.fullTextIndexRetirements);
2636
3332
  Table.schemaVersion++;
2637
3333
  Table.updatedAttributes();
2638
- void Table.derivedIndexRuntime?.close();
2639
- Table.derivedIndexRuntime = (0, hnswDerivedIndex_ts_1.attachDerivedIndexes)(Table);
3334
+ (0, derivedIndexes_ts_1.refreshDerivedIndexes)(Table);
2640
3335
  };
2641
3336
  };
2642
3337
  const durableAttributes = catalogAttributes(Table.dbisDB);
@@ -2656,6 +3351,10 @@ function declareTable(target, tableDefinition) {
2656
3351
  validationAttributes.unshift(durablePrimary);
2657
3352
  }
2658
3353
  }
3354
+ fullTextValidationAttributes = validationAttributes;
3355
+ // Before the full-text branches can write an interim descriptor; the check on the merged list
3356
+ // below stays authoritative.
3357
+ (0, embedHook_ts_1.assertDerivedFieldOwnership)(validationAttributes);
2659
3358
  const persistedAudit = persistedPrimary.descriptor?.audit;
2660
3359
  const durableAudit = persistedAudit === true;
2661
3360
  const finalAudit = origin === 'cluster'
@@ -2683,25 +3382,59 @@ function declareTable(target, tableDefinition) {
2683
3382
  throw new hdbError_ts_1.ClientError(`Table '${databaseName}.${tableName}' cannot use @fullText with the LMDB storage engine`, 400);
2684
3383
  if (compiled.length > 0 && !finalAudit)
2685
3384
  throw new hdbError_ts_1.ClientError(`Table '${databaseName}.${tableName}' must enable audit logging before using @fullText because its transaction log is the derived-index recovery source`, 400);
2686
- const interim = (0, fullTextSchemaLifecycle_ts_1.safeInterimFullTextDefinitions)(persistedFullTextValues, validationAttributes, compiled);
2687
3385
  const pinAudit = compiled.length > 0 && persistedPrimary.descriptor?.audit !== true;
2688
- if (pinAudit || !(0, fullTextSchemaLifecycle_ts_1.definitionsEqual)(interim, persistedFullTextValues)) {
3386
+ const requestedNames = new Set(compiled.map(({ name }) => name));
3387
+ const durableDefinitions = (0, fullTextSchemaLifecycle_ts_1.readPersistedFullTextDefinitions)(persistedFullTextValues, durableAttributes, fullTextWarning);
3388
+ const removedNames = durableDefinitions.map(({ name }) => name).filter((name) => !requestedNames.has(name));
3389
+ const invalidatedNames = durableDefinitions
3390
+ .filter(({ name }) => requestedNames.has(name))
3391
+ .filter((definition) => {
3392
+ try {
3393
+ (0, fullTextSchema_ts_1.compileFullTextDefinitions)([definition], validationAttributes);
3394
+ return false;
3395
+ }
3396
+ catch (error) {
3397
+ if (error instanceof hdbError_ts_1.ClientError)
3398
+ return true;
3399
+ throw error;
3400
+ }
3401
+ })
3402
+ .map(({ name }) => name);
3403
+ const transitionRetirements = new Set([...removedNames, ...invalidatedNames]);
3404
+ if (pinAudit || transitionRetirements.size > 0) {
2689
3405
  const interimPrimary = { ...persistedPrimary.descriptor };
2690
3406
  if (pinAudit)
2691
3407
  interimPrimary.audit = true;
2692
- if (interim.length > 0)
2693
- interimPrimary.fullTextIndexes = interim;
2694
- else
2695
- delete interimPrimary.fullTextIndexes;
3408
+ if (transitionRetirements.size > 0) {
3409
+ const interimDefinitions = durableDefinitions.filter(({ name }) => requestedNames.has(name));
3410
+ if (interimDefinitions.length > 0) {
3411
+ interimPrimary.fullTextIndexes = interimDefinitions;
3412
+ interimPrimary.fullTextIndexGenerations = persistedFullTextIndexGenerations(persistedPrimary.descriptor?.fullTextIndexGenerations, interimDefinitions);
3413
+ }
3414
+ else {
3415
+ delete interimPrimary.fullTextIndexes;
3416
+ delete interimPrimary.fullTextIndexGenerations;
3417
+ }
3418
+ const retirements = new Set((0, fullTextSchema_ts_1.persistedFullTextIndexNames)(persistedPrimary.descriptor?.fullTextIndexRetirements));
3419
+ for (const name of transitionRetirements)
3420
+ retirements.add(name);
3421
+ for (const name of requestedNames)
3422
+ if (!transitionRetirements.has(name))
3423
+ retirements.delete(name);
3424
+ fullTextIndexRetirementNames = [...retirements].sort();
3425
+ if (fullTextIndexRetirementNames.length > 0)
3426
+ interimPrimary.fullTextIndexRetirements = fullTextIndexRetirementNames.map((name) => ({ name }));
3427
+ else
3428
+ delete interimPrimary.fullTextIndexRetirements;
3429
+ }
2696
3430
  armFullTextLiveStateRestore?.();
2697
3431
  Table.dbisDB.put(persistedPrimary.key, interimPrimary);
2698
3432
  if (pinAudit && Table.audit !== true)
2699
3433
  Table.enableAuditing();
2700
- Table.fullTextIndexes = rootStore instanceof rocksdb_js_1.RocksDatabase && finalAudit ? interim : [];
2701
3434
  hasChanges = true;
2702
3435
  }
2703
3436
  fullTextValuesForPersistence = compiled;
2704
- fullTextPersistencePending = !(0, fullTextSchemaLifecycle_ts_1.definitionsEqual)(compiled, interim);
3437
+ fullTextPersistencePending = !(0, fullTextSchemaLifecycle_ts_1.definitionsEqual)(compiled, persistedFullTextValues);
2705
3438
  activeFullTextIndexes = compiled;
2706
3439
  }
2707
3440
  else {
@@ -2712,6 +3445,16 @@ function declareTable(target, tableDefinition) {
2712
3445
  activeFullTextIndexes = rootStore instanceof rocksdb_js_1.RocksDatabase && finalAudit ? retained : [];
2713
3446
  }
2714
3447
  }
3448
+ if (activeFullTextIndexes !== undefined) {
3449
+ const durableGenerationDefinitions = (0, fullTextSchemaLifecycle_ts_1.readPersistedFullTextDefinitions)(persistedFullTextValues, fullTextValidationAttributes ?? Table.attributes, fullTextWarning);
3450
+ const finalGenerationDefinitions = (0, fullTextSchemaLifecycle_ts_1.readPersistedFullTextDefinitions)(fullTextValuesForPersistence ?? persistedFullTextValues, fullTextValidationAttributes ?? Table.attributes, fullTextWarning);
3451
+ fullTextIndexGenerationMap = (0, fullTextSchema_ts_1.reconcileFullTextIndexGenerations)(durableGenerationDefinitions, persistedPrimary.descriptor?.fullTextIndexGenerations, finalGenerationDefinitions, createFullTextIndexGeneration);
3452
+ (0, derivedIndexes_ts_1.assertFullTextActivationSupported)(rootStore, databaseName, tableName, activeFullTextIndexes);
3453
+ if (activeFullTextIndexes.length > 0 &&
3454
+ rootStore instanceof rocksdb_js_1.RocksDatabase &&
3455
+ (0, derivedIndexRegistry_ts_1.fullTextClearInProgress)(rootStore, Table.tableId))
3456
+ throw new hdbError_ts_1.ClientError(`Cannot activate @fullText on '${databaseName}.${tableName}' while Table.clear() is in progress`, 409);
3457
+ }
2715
3458
  // it table already exists, get the split segments setting
2716
3459
  if (splitSegments == undefined)
2717
3460
  splitSegments = Table.splitSegments;
@@ -2742,6 +3485,9 @@ function declareTable(target, tableDefinition) {
2742
3485
  if (existingPrimary)
2743
3486
  attributes = [existingPrimary, ...attributes];
2744
3487
  }
3488
+ // On the complete list (a peer's fields merged, an omitted primary key inherited) and before
3489
+ // it replaces the live one or reaches the catalog, so a refused declaration changes nothing.
3490
+ (0, embedHook_ts_1.assertDerivedFieldOwnership)(attributes);
2745
3491
  armFullTextLiveStateRestore?.();
2746
3492
  Table.attributes.splice(0, Table.attributes.length, ...attributes);
2747
3493
  // Re-assert from the live declaration so a stale value on disk (replicated event,
@@ -2760,6 +3506,8 @@ function declareTable(target, tableDefinition) {
2760
3506
  Table.cacheControl = cacheControl;
2761
3507
  }
2762
3508
  else {
3509
+ if (Array.isArray(attributes))
3510
+ (0, embedHook_ts_1.assertDerivedFieldOwnership)(attributes);
2763
3511
  if (fullTextIndexesExplicit) {
2764
3512
  if (origin === 'cluster') {
2765
3513
  const merged = (0, fullTextSchemaLifecycle_ts_1.mergePeerFullTextDefinitions)([], fullTextIndexes, attributes, fullTextWarning);
@@ -2789,8 +3537,12 @@ function declareTable(target, tableDefinition) {
2789
3537
  // Old readers treat every attribute row as live schema, so relationships stay on the ignored primary descriptor.
2790
3538
  if (relationshipDefinitions)
2791
3539
  primaryKeyAttribute.relationships = relationshipDefinitions;
2792
- if (Array.isArray(fullTextValuesForPersistence) && fullTextValuesForPersistence.length > 0)
3540
+ if (Array.isArray(fullTextValuesForPersistence) && fullTextValuesForPersistence.length > 0) {
3541
+ const durableFullTextIndexes = (0, fullTextSchemaLifecycle_ts_1.readPersistedFullTextDefinitions)(fullTextValuesForPersistence, attributes, fullTextWarning);
3542
+ fullTextIndexGenerationMap = (0, fullTextSchema_ts_1.reconcileFullTextIndexGenerations)([], undefined, durableFullTextIndexes, createFullTextIndexGeneration);
2793
3543
  primaryKeyAttribute.fullTextIndexes = fullTextValuesForPersistence;
3544
+ primaryKeyAttribute.fullTextIndexGenerations = fullTextIndexGenerationMap;
3545
+ }
2794
3546
  // can't change compression after the fact (except threshold), so save only when we create the table
2795
3547
  primaryKeyAttribute.compression = getDefaultCompression();
2796
3548
  if (trackDeletes)
@@ -2849,6 +3601,8 @@ function declareTable(target, tableDefinition) {
2849
3601
  target.adopt(attributesDbi);
2850
3602
  markInternalDbiNonVersioned(attributesDbi);
2851
3603
  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)
3604
+ if (rootStore instanceof rocksdb_js_1.RocksDatabase && (0, derivedIndexRegistry_ts_1.fullTextRetirementInProgress)(rootStore, tableName))
3605
+ throw new hdbError_ts_1.ClientError(`Cannot create '${databaseName}.${tableName}' while its previous full-text storage is being retired`, 409);
2852
3606
  const existingTableMeta = attributesDbi.getSync(dbiName);
2853
3607
  if (existingTableMeta && !existingTableMeta.dropping) {
2854
3608
  // table was created while we were setting up; the lock is not reentrant, so release
@@ -2864,7 +3618,8 @@ function declareTable(target, tableDefinition) {
2864
3618
  // create below starts from a clean slate; treating the tombstoned
2865
3619
  // entry as an existing table would recurse forever on the stale
2866
3620
  // catalog row.
2867
- completeInterruptedDrop(rootStore, attributesDbi, databaseName, tableName);
3621
+ if (!completeInterruptedDrop(rootStore, attributesDbi, databaseName, tableName))
3622
+ throw new hdbError_ts_1.ClientError(`Cannot create '${databaseName}.${tableName}' while its interrupted full-text drop is being retired`, 409);
2868
3623
  // This resolves the drop without ever going through the schema-load
2869
3624
  // reconcile below, which is the only other place that returns a spent
2870
3625
  // budget. Without clearing it here too, a table that gets dropped again
@@ -2921,6 +3676,8 @@ function declareTable(target, tableDefinition) {
2921
3676
  indices: {},
2922
3677
  attributes,
2923
3678
  fullTextIndexes: activeFullTextIndexes ?? [],
3679
+ fullTextIndexGenerations: fullTextIndexGenerationMap,
3680
+ fullTextIndexRetirements: fullTextIndexRetirementNames ?? [],
2924
3681
  schemaDefined,
2925
3682
  dbisDB: attributesDbi,
2926
3683
  description,
@@ -3176,8 +3933,12 @@ function declareTable(target, tableDefinition) {
3176
3933
  JSON.stringify(attributeDescriptor.elements) !== JSON.stringify(attribute.elements) ||
3177
3934
  // Include `embed` so a source/model change refreshes the embed registry.
3178
3935
  JSON.stringify(attributeDescriptor.embed) !== JSON.stringify(attribute.embed);
3179
- // any metadata difference (drives persistence)
3180
- const changed = commonChanged || JSON.stringify(attributeDescriptor?.indexed) !== JSON.stringify(attribute.indexed);
3936
+ // any metadata difference (drives persistence). `decide` is compared here and not in
3937
+ // `commonChanged`: a changed directive refreshes the decide registry, and a stored decision
3938
+ // does not depend on the model, so an indexed decision attribute must not rebuild its index.
3939
+ const changed = commonChanged ||
3940
+ JSON.stringify(attributeDescriptor?.indexed) !== JSON.stringify(attribute.indexed) ||
3941
+ JSON.stringify(attributeDescriptor?.decide) !== JSON.stringify(attribute.decide);
3181
3942
  // structure-affecting difference (drives reindex) — ignores search-only option changes and
3182
3943
  // representation-only differences (key order, string-vs-number) via canonicalIndexKey
3183
3944
  const indexOptionsStructurallyChanged = canonicalIndexKey(attributeDescriptor?.indexed) !== canonicalIndexKey(attribute.indexed);
@@ -3337,15 +4098,39 @@ function declareTable(target, tableDefinition) {
3337
4098
  attributesDbi.put(dbiKey, attribute);
3338
4099
  }
3339
4100
  }
3340
- if (fullTextPersistencePending && !deferredPrimaryRow) {
4101
+ if (!deferredPrimaryRow &&
4102
+ (fullTextPersistencePending ||
4103
+ (rootStore instanceof rocksdb_js_1.RocksDatabase &&
4104
+ activeFullTextIndexes !== undefined &&
4105
+ Array.isArray(fullTextValuesForPersistence)))) {
3341
4106
  const { key, descriptor } = persistedPrimaryDescriptor(attributesDbi);
3342
4107
  if (descriptor && !tableIsDropping(descriptor, key)) {
3343
4108
  const updatedPrimary = { ...descriptor };
3344
- if (Array.isArray(fullTextValuesForPersistence) && fullTextValuesForPersistence.length > 0)
4109
+ if (Array.isArray(fullTextValuesForPersistence) && fullTextValuesForPersistence.length > 0) {
3345
4110
  updatedPrimary.fullTextIndexes = fullTextValuesForPersistence;
3346
- else
4111
+ updatedPrimary.fullTextIndexGenerations = fullTextIndexGenerationMap;
4112
+ }
4113
+ else {
3347
4114
  delete updatedPrimary.fullTextIndexes;
3348
- if (!(0, fullTextSchemaLifecycle_ts_1.definitionsEqual)(descriptor.fullTextIndexes, updatedPrimary.fullTextIndexes)) {
4115
+ delete updatedPrimary.fullTextIndexGenerations;
4116
+ }
4117
+ const activeNames = new Set((0, fullTextSchema_ts_1.persistedFullTextIndexNames)(updatedPrimary.fullTextIndexes));
4118
+ const retirements = new Set((0, fullTextSchema_ts_1.persistedFullTextIndexNames)(descriptor.fullTextIndexRetirements));
4119
+ for (const name of (0, fullTextSchema_ts_1.persistedFullTextIndexNames)(descriptor.fullTextIndexes))
4120
+ if (!activeNames.has(name))
4121
+ retirements.add(name);
4122
+ for (const name of activeNames)
4123
+ retirements.delete(name);
4124
+ fullTextIndexRetirementNames = [...retirements].sort();
4125
+ if (fullTextIndexRetirementNames.length > 0)
4126
+ updatedPrimary.fullTextIndexRetirements = fullTextIndexRetirementNames.map((name) => ({ name }));
4127
+ else
4128
+ delete updatedPrimary.fullTextIndexRetirements;
4129
+ if (!(0, fullTextSchemaLifecycle_ts_1.definitionsEqual)(descriptor.fullTextIndexes, updatedPrimary.fullTextIndexes) ||
4130
+ JSON.stringify(descriptor.fullTextIndexGenerations ?? {}) !==
4131
+ JSON.stringify(updatedPrimary.fullTextIndexGenerations ?? {}) ||
4132
+ JSON.stringify(descriptor.fullTextIndexRetirements ?? []) !==
4133
+ JSON.stringify(updatedPrimary.fullTextIndexRetirements ?? [])) {
3349
4134
  exclusiveLock();
3350
4135
  attributesDbi.put(key, updatedPrimary);
3351
4136
  hasChanges = true;
@@ -3414,8 +4199,11 @@ function declareTable(target, tableDefinition) {
3414
4199
  finally {
3415
4200
  releaseLock();
3416
4201
  }
3417
- if (activeFullTextIndexes !== undefined)
4202
+ if (activeFullTextIndexes !== undefined) {
3418
4203
  Table.fullTextIndexes = activeFullTextIndexes;
4204
+ Table.fullTextIndexGenerations = fullTextIndexGenerationMap;
4205
+ Table.fullTextIndexRetirements = fullTextIndexRetirementNames ?? [];
4206
+ }
3419
4207
  if (hasChanges || refreshRelationshipAttributes)
3420
4208
  Table.schemaVersion++;
3421
4209
  if (hasChanges || refreshRelationshipAttributes || refreshedLiveAttributes)
@@ -3430,8 +4218,7 @@ function declareTable(target, tableDefinition) {
3430
4218
  }
3431
4219
  else if (hasChanges)
3432
4220
  signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName, undefined, branchPath));
3433
- void Table.derivedIndexRuntime?.close();
3434
- Table.derivedIndexRuntime = (0, hnswDerivedIndex_ts_1.attachDerivedIndexes)(Table);
4221
+ (0, derivedIndexes_ts_1.refreshDerivedIndexes)(Table);
3435
4222
  Table.origin = origin;
3436
4223
  // scope-private: replication and other global subscribers must not learn of a branch class
3437
4224
  if ((hasChanges || refreshRelationshipAttributes) && !target.branch) {
@@ -4273,13 +5060,61 @@ function scheduleGenerationReclaim(rootStore, attributesDbi, databaseName) {
4273
5060
  state.timer.unref?.();
4274
5061
  state.delay = Math.min(state.delay * 2, 60_000);
4275
5062
  }
5063
+ const interruptedDropRetirements = new WeakMap();
5064
+ function trackInterruptedDropRetirement(rootStore, task) {
5065
+ let tasks = interruptedDropRetirements.get(rootStore);
5066
+ if (!tasks)
5067
+ interruptedDropRetirements.set(rootStore, (tasks = new Set()));
5068
+ tasks.add(task);
5069
+ const remove = () => {
5070
+ tasks.delete(task);
5071
+ };
5072
+ task.then(remove, remove);
5073
+ }
5074
+ async function settleInterruptedDropRetirements(rootStores) {
5075
+ for (;;) {
5076
+ const tasks = [...rootStores].flatMap((rootStore) => [...(interruptedDropRetirements.get(rootStore) ?? [])]);
5077
+ if (tasks.length === 0)
5078
+ return;
5079
+ await Promise.allSettled(tasks);
5080
+ }
5081
+ }
4276
5082
  /** Finishes the durable logical state of a drop that stopped after writing its tombstone. */
4277
- function completeInterruptedDrop(rootStore, attributesDbi, databaseName, tableName) {
5083
+ function completeInterruptedDrop(rootStore, attributesDbi, databaseName, tableName, fullTextRetired = false) {
4278
5084
  logger.debug(`Completing interrupted drop of table ${databaseName}.${tableName}`);
4279
5085
  const catalogRows = [...attributesDbi.getRange({ start: tableName + '/', end: tableName + '0' })];
4280
5086
  const bareEntry = catalogRows.find(({ key }) => key === tableName + '/');
4281
5087
  const primaryEntry = (bareEntry?.value?.isPrimaryKey ? bareEntry : undefined) ?? catalogRows.find(({ value }) => value?.isPrimaryKey);
4282
5088
  const tombstoneEntry = bareEntry ?? primaryEntry;
5089
+ if (rootStore instanceof rocksdb_js_1.RocksDatabase && !fullTextRetired) {
5090
+ const names = [
5091
+ ...new Set([
5092
+ ...(0, fullTextSchema_ts_1.persistedFullTextIndexNames)(primaryEntry?.value?.fullTextIndexes),
5093
+ ...(0, fullTextSchema_ts_1.persistedFullTextIndexNames)(primaryEntry?.value?.fullTextIndexRetirements),
5094
+ ]),
5095
+ ];
5096
+ if (names.length > 0) {
5097
+ const releaseRetirement = (0, derivedIndexRegistry_ts_1.acquireFullTextRetirementFence)(rootStore, tableName);
5098
+ if (releaseRetirement) {
5099
+ const retirement = (async () => {
5100
+ try {
5101
+ const retired = await (0, derivedIndexes_ts_1.retireFullTextIndexes)({ databaseName, tableName, primaryStore: { rootStore } }, names.map((name) => ({ name })), () => rootStore.status === 'open' && !(0, DatabaseTransaction_ts_1.databaseCommitsSuspended)(rootStore));
5102
+ if (!retired || rootStore.status === 'closed' || (0, DatabaseTransaction_ts_1.databaseCommitsSuspended)(rootStore))
5103
+ return;
5104
+ await (0, Table_ts_1.withUpdateAttributesLockNonBlocking)(rootStore, `complete interrupted drop of '${databaseName}.${tableName}'`, () => completeInterruptedDrop(rootStore, attributesDbi, databaseName, tableName, true));
5105
+ }
5106
+ catch (error) {
5107
+ logger.warn(`Could not retire full-text storage for interrupted drop ${databaseName}.${tableName}`, error);
5108
+ }
5109
+ finally {
5110
+ releaseRetirement();
5111
+ }
5112
+ })();
5113
+ trackInterruptedDropRetirement(rootStore, retirement);
5114
+ }
5115
+ return false;
5116
+ }
5117
+ }
4283
5118
  if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
4284
5119
  const tombstone = tombstoneEntry?.value;
4285
5120
  const generation = tombstone?.generation ?? primaryEntry?.value?.generation;
@@ -4327,6 +5162,7 @@ function completeInterruptedDrop(rootStore, attributesDbi, databaseName, tableNa
4327
5162
  }
4328
5163
  if (tombstoneEntry)
4329
5164
  attributesDbi.removeSync(tombstoneEntry.key);
5165
+ return true;
4330
5166
  }
4331
5167
  function dropTableMeta({ table: tableName, database: databaseName }) {
4332
5168
  const rootStore = database({ database: databaseName, table: tableName });