@harperfast/harper 5.2.10 → 5.3.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/mcpTools.ts +1 -1
- package/agent/session.ts +25 -14
- package/bin/cliOperations.ts +46 -9
- package/bin/copyDb.ts +282 -59
- package/bin/deploySetup.ts +16 -5
- package/bin/harper.ts +1 -1
- package/bin/help.ts +4 -1
- package/bin/lite.ts +4 -1
- package/bin/restart.ts +59 -3
- package/bin/run.ts +6 -11
- package/bin/upgrade.js +7 -3
- package/bin/workloadIdentity.ts +119 -0
- package/components/Application.ts +2414 -242
- package/components/ApplicationScope.ts +8 -0
- package/components/EntryHandler.ts +59 -39
- package/components/OptionsWatcher.ts +150 -74
- package/components/RuntimeModuleTracker.ts +38 -7
- package/components/Scope.ts +37 -15
- package/components/awaitRestart.ts +84 -0
- package/components/componentLoader.ts +340 -30
- package/components/componentPreparationLock.ts +16 -5
- package/components/mcp/adapters/harperHttp.ts +4 -0
- package/components/mcp/listChanged.ts +4 -0
- package/components/mcp/toolRegistry.ts +2 -0
- package/components/mcp/tools/operations.ts +9 -0
- package/components/mcp/tools/schemas/operationDescriptions.ts +2 -2
- package/components/operations.js +270 -78
- package/components/operationsValidation.js +49 -1
- package/components/status/ComponentStatusRegistry.ts +59 -0
- package/config/RootConfigWatcher.ts +80 -34
- package/config/configUtils.ts +291 -34
- package/config/harperConfigEnvVars.ts +170 -27
- package/config-root.schema.json +29 -0
- package/dataLayer/blobBackup.ts +160 -50
- package/dataLayer/delete.ts +6 -1
- package/dataLayer/harperBridge/ResourceBridge.ts +52 -8
- package/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js +3 -1
- package/dataLayer/hdbInfoController.ts +34 -1
- package/dataLayer/insert.ts +44 -1
- package/dataLayer/rocksdbBackup.ts +53 -10
- package/dataLayer/schema.ts +11 -1
- package/dataLayer/schemaDescribe.ts +8 -1
- package/dist/agent/mcpTools.js +1 -1
- package/dist/agent/mcpTools.js.map +1 -1
- package/dist/agent/session.d.ts +22 -0
- package/dist/agent/session.js +26 -15
- package/dist/agent/session.js.map +1 -1
- package/dist/bin/cliOperations.js +48 -9
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/copyDb.d.ts +12 -1
- package/dist/bin/copyDb.js +248 -60
- package/dist/bin/copyDb.js.map +1 -1
- package/dist/bin/deploySetup.d.ts +2 -0
- package/dist/bin/deploySetup.js +11 -3
- package/dist/bin/deploySetup.js.map +1 -1
- package/dist/bin/harper.js +1 -1
- package/dist/bin/harper.js.map +1 -1
- package/dist/bin/help.js +4 -1
- package/dist/bin/help.js.map +1 -1
- package/dist/bin/lite.js +4 -1
- package/dist/bin/lite.js.map +1 -1
- package/dist/bin/restart.js +54 -5
- package/dist/bin/restart.js.map +1 -1
- package/dist/bin/run.js +4 -10
- package/dist/bin/run.js.map +1 -1
- package/dist/bin/upgrade.js +4 -3
- package/dist/bin/upgrade.js.map +1 -1
- package/dist/bin/workloadIdentity.d.ts +18 -0
- package/dist/bin/workloadIdentity.js +100 -0
- package/dist/bin/workloadIdentity.js.map +1 -0
- package/dist/components/Application.d.ts +139 -16
- package/dist/components/Application.js +2215 -267
- package/dist/components/Application.js.map +1 -1
- package/dist/components/ApplicationScope.d.ts +8 -0
- package/dist/components/ApplicationScope.js +7 -0
- package/dist/components/ApplicationScope.js.map +1 -1
- package/dist/components/EntryHandler.js +26 -10
- package/dist/components/EntryHandler.js.map +1 -1
- package/dist/components/OptionsWatcher.d.ts +1 -0
- package/dist/components/OptionsWatcher.js +141 -74
- package/dist/components/OptionsWatcher.js.map +1 -1
- package/dist/components/RuntimeModuleTracker.js +40 -6
- package/dist/components/RuntimeModuleTracker.js.map +1 -1
- package/dist/components/Scope.js +38 -13
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/awaitRestart.d.ts +33 -0
- package/dist/components/awaitRestart.js +61 -0
- package/dist/components/awaitRestart.js.map +1 -0
- package/dist/components/componentLoader.d.ts +38 -1
- package/dist/components/componentLoader.js +279 -22
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/componentPreparationLock.d.ts +5 -0
- package/dist/components/componentPreparationLock.js +14 -6
- package/dist/components/componentPreparationLock.js.map +1 -1
- package/dist/components/mcp/adapters/harperHttp.js +4 -0
- package/dist/components/mcp/adapters/harperHttp.js.map +1 -1
- package/dist/components/mcp/listChanged.js +5 -0
- package/dist/components/mcp/listChanged.js.map +1 -1
- package/dist/components/mcp/toolRegistry.d.ts +1 -0
- package/dist/components/mcp/toolRegistry.js.map +1 -1
- package/dist/components/mcp/tools/operations.d.ts +5 -0
- package/dist/components/mcp/tools/operations.js +9 -0
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/mcp/tools/schemas/operationDescriptions.js +2 -2
- package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
- package/dist/components/operations.js +231 -77
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +49 -1
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/status/ComponentStatusRegistry.d.ts +0 -4
- package/dist/components/status/ComponentStatusRegistry.js +63 -0
- package/dist/components/status/ComponentStatusRegistry.js.map +1 -1
- package/dist/config/RootConfigWatcher.d.ts +7 -1
- package/dist/config/RootConfigWatcher.js +64 -27
- package/dist/config/RootConfigWatcher.js.map +1 -1
- package/dist/config/configUtils.d.ts +9 -1
- package/dist/config/configUtils.js +254 -33
- package/dist/config/configUtils.js.map +1 -1
- package/dist/config/harperConfigEnvVars.d.ts +16 -0
- package/dist/config/harperConfigEnvVars.js +162 -25
- package/dist/config/harperConfigEnvVars.js.map +1 -1
- package/dist/dataLayer/blobBackup.d.ts +49 -20
- package/dist/dataLayer/blobBackup.js +139 -50
- package/dist/dataLayer/blobBackup.js.map +1 -1
- package/dist/dataLayer/delete.js +1 -1
- package/dist/dataLayer/delete.js.map +1 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +14 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.js +51 -10
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.d.ts +3 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js +3 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js.map +1 -1
- package/dist/dataLayer/hdbInfoController.d.ts +10 -0
- package/dist/dataLayer/hdbInfoController.js +30 -1
- package/dist/dataLayer/hdbInfoController.js.map +1 -1
- package/dist/dataLayer/insert.d.ts +9 -1
- package/dist/dataLayer/insert.js +30 -0
- package/dist/dataLayer/insert.js.map +1 -1
- package/dist/dataLayer/rocksdbBackup.d.ts +2 -2
- package/dist/dataLayer/rocksdbBackup.js +45 -8
- package/dist/dataLayer/rocksdbBackup.js.map +1 -1
- package/dist/dataLayer/schema.js +8 -0
- package/dist/dataLayer/schema.js.map +1 -1
- package/dist/dataLayer/schemaDescribe.js +8 -1
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +52 -0
- package/dist/resources/DatabaseTransaction.d.ts +55 -3
- package/dist/resources/DatabaseTransaction.js +460 -134
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/LMDBTransaction.js +22 -4
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.d.ts +1 -0
- package/dist/resources/PrimaryRocksDatabase.js +30 -2
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
- package/dist/resources/RecordEncoder.d.ts +20 -0
- package/dist/resources/RecordEncoder.js +94 -9
- package/dist/resources/RecordEncoder.js.map +1 -1
- package/dist/resources/RequestTarget.d.ts +2 -0
- package/dist/resources/RequestTarget.js.map +1 -1
- package/dist/resources/Resource.js +20 -11
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +20 -1
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/RocksIndexStore.d.ts +6 -1
- package/dist/resources/RocksIndexStore.js +24 -10
- package/dist/resources/RocksIndexStore.js.map +1 -1
- package/dist/resources/RocksTransactionLogStore.d.ts +14 -1
- package/dist/resources/RocksTransactionLogStore.js +57 -17
- package/dist/resources/RocksTransactionLogStore.js.map +1 -1
- package/dist/resources/Table.d.ts +112 -8
- package/dist/resources/Table.js +1071 -197
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/auditStore.d.ts +11 -2
- package/dist/resources/auditStore.js +183 -18
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/blob.d.ts +129 -9
- package/dist/resources/blob.js +992 -114
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/branchDatabase.d.ts +48 -0
- package/dist/resources/branchDatabase.js +892 -0
- package/dist/resources/branchDatabase.js.map +1 -0
- package/dist/resources/crdt.js +50 -12
- package/dist/resources/crdt.js.map +1 -1
- package/dist/resources/databases.d.ts +150 -9
- package/dist/resources/databases.js +1247 -525
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/defineTable.d.ts +10 -2
- package/dist/resources/defineTable.js +9 -1
- package/dist/resources/defineTable.js.map +1 -1
- package/dist/resources/graphql.d.ts +1 -1
- package/dist/resources/graphql.js +50 -15
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +1 -0
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +6 -2
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/longLivedTransactions.d.ts +71 -0
- package/dist/resources/longLivedTransactions.js +358 -0
- package/dist/resources/longLivedTransactions.js.map +1 -0
- package/dist/resources/models/backendRegistry.d.ts +26 -0
- package/dist/resources/models/backendRegistry.js +60 -2
- package/dist/resources/models/backendRegistry.js.map +1 -1
- package/dist/resources/models/bootstrap.d.ts +33 -1
- package/dist/resources/models/bootstrap.js +416 -31
- package/dist/resources/models/bootstrap.js.map +1 -1
- package/dist/resources/recordLock.d.ts +80 -0
- package/dist/resources/recordLock.js +184 -0
- package/dist/resources/recordLock.js.map +1 -0
- package/dist/resources/replayLogs.d.ts +14 -1
- package/dist/resources/replayLogs.js +152 -24
- package/dist/resources/replayLogs.js.map +1 -1
- package/dist/resources/replayLogsGuards.d.ts +94 -7
- package/dist/resources/replayLogsGuards.js +111 -7
- package/dist/resources/replayLogsGuards.js.map +1 -1
- package/dist/resources/search.js +158 -32
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/transactionBroadcast.d.ts +1 -1
- package/dist/resources/transactionBroadcast.js +2 -2
- package/dist/resources/transactionBroadcast.js.map +1 -1
- package/dist/security/auth.js +61 -30
- package/dist/security/auth.js.map +1 -1
- package/dist/security/authn/oidc/claims.d.ts +22 -0
- package/dist/security/authn/oidc/claims.js +71 -0
- package/dist/security/authn/oidc/claims.js.map +1 -0
- package/dist/security/authn/oidc/identityToken.d.ts +27 -0
- package/dist/security/authn/oidc/identityToken.js +111 -0
- package/dist/security/authn/oidc/identityToken.js.map +1 -0
- package/dist/security/authn/oidc/jwks.d.ts +25 -0
- package/dist/security/authn/oidc/jwks.js +261 -0
- package/dist/security/authn/oidc/jwks.js.map +1 -0
- package/dist/security/authn/oidc/providers/generic.d.ts +13 -0
- package/dist/security/authn/oidc/providers/generic.js +34 -0
- package/dist/security/authn/oidc/providers/generic.js.map +1 -0
- package/dist/security/authn/oidc/providers/githubActions.d.ts +11 -0
- package/dist/security/authn/oidc/providers/githubActions.js +129 -0
- package/dist/security/authn/oidc/providers/githubActions.js.map +1 -0
- package/dist/security/authn/oidc/providers/index.d.ts +37 -0
- package/dist/security/authn/oidc/providers/index.js +24 -0
- package/dist/security/authn/oidc/providers/index.js.map +1 -0
- package/dist/security/authn/oidc/tokenExchange.d.ts +12 -0
- package/dist/security/authn/oidc/tokenExchange.js +306 -0
- package/dist/security/authn/oidc/tokenExchange.js.map +1 -0
- package/dist/security/authn/oidc/trustPolicyOperations.d.ts +49 -0
- package/dist/security/authn/oidc/trustPolicyOperations.js +358 -0
- package/dist/security/authn/oidc/trustPolicyOperations.js.map +1 -0
- package/dist/security/authn/oidc/types.d.ts +38 -0
- package/dist/security/authn/oidc/types.js +6 -0
- package/dist/security/authn/oidc/types.js.map +1 -0
- package/dist/security/certificateVerification/index.js +40 -11
- package/dist/security/certificateVerification/index.js.map +1 -1
- package/dist/security/certificateVerification/trustedIssuers.d.ts +24 -0
- package/dist/security/certificateVerification/trustedIssuers.js +79 -0
- package/dist/security/certificateVerification/trustedIssuers.js.map +1 -0
- package/dist/security/certificateVerification/types.d.ts +1 -0
- package/dist/security/credentialProvenance.d.ts +35 -0
- package/dist/security/credentialProvenance.js +51 -0
- package/dist/security/credentialProvenance.js.map +1 -0
- package/dist/security/credentialRejection.d.ts +4 -0
- package/dist/security/credentialRejection.js +24 -0
- package/dist/security/credentialRejection.js.map +1 -0
- package/dist/security/deferredAuthentication.d.ts +36 -0
- package/dist/security/deferredAuthentication.js +70 -0
- package/dist/security/deferredAuthentication.js.map +1 -0
- package/dist/security/impersonation.d.ts +21 -0
- package/dist/security/impersonation.js +108 -9
- package/dist/security/impersonation.js.map +1 -1
- package/dist/security/jsLoader.d.ts +6 -0
- package/dist/security/jsLoader.js +75 -15
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/security/keys.js +301 -71
- package/dist/security/keys.js.map +1 -1
- package/dist/security/operationScope.d.ts +21 -0
- package/dist/security/operationScope.js +36 -0
- package/dist/security/operationScope.js.map +1 -0
- package/dist/security/permissionsTranslator.js +21 -0
- package/dist/security/permissionsTranslator.js.map +1 -1
- package/dist/security/tokenAuthentication.d.ts +19 -1
- package/dist/security/tokenAuthentication.js +191 -10
- package/dist/security/tokenAuthentication.js.map +1 -1
- package/dist/security/user.js +4 -3
- package/dist/security/user.js.map +1 -1
- package/dist/server/DurableSubscriptionsSession.d.ts +2 -2
- package/dist/server/DurableSubscriptionsSession.js +67 -10
- package/dist/server/DurableSubscriptionsSession.js.map +1 -1
- package/dist/server/REST.js +73 -0
- package/dist/server/REST.js.map +1 -1
- package/dist/server/graphqlQuerying.js +4 -0
- package/dist/server/graphqlQuerying.js.map +1 -1
- package/dist/server/http.d.ts +11 -0
- package/dist/server/http.js +88 -15
- package/dist/server/http.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +8 -1
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/server/jobs/jobProcess.js +6 -2
- package/dist/server/jobs/jobProcess.js.map +1 -1
- package/dist/server/jobs/jobs.js +4 -1
- package/dist/server/jobs/jobs.js.map +1 -1
- package/dist/server/liveSubscriptionAuth.d.ts +26 -4
- package/dist/server/liveSubscriptionAuth.js +105 -39
- package/dist/server/liveSubscriptionAuth.js.map +1 -1
- package/dist/server/loadRootComponents.js +49 -10
- package/dist/server/loadRootComponents.js.map +1 -1
- package/dist/server/mqtt.d.ts +2 -0
- package/dist/server/mqtt.js +165 -30
- package/dist/server/mqtt.js.map +1 -1
- package/dist/server/nodeName.d.ts +2 -0
- package/dist/server/nodeName.js +107 -23
- package/dist/server/nodeName.js.map +1 -1
- package/dist/server/serverHelpers/Headers.d.ts +25 -0
- package/dist/server/serverHelpers/Headers.js +139 -1
- package/dist/server/serverHelpers/Headers.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.d.ts +9 -0
- package/dist/server/serverHelpers/contentTypes.js +32 -25
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/deployValidationState.d.ts +3 -0
- package/dist/server/serverHelpers/deployValidationState.js +9 -19
- package/dist/server/serverHelpers/deployValidationState.js.map +1 -1
- package/dist/server/serverHelpers/operationAuthorizationState.d.ts +12 -0
- package/dist/server/serverHelpers/operationAuthorizationState.js +24 -2
- package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -1
- package/dist/server/serverHelpers/registeredOperations.d.ts +5 -4
- package/dist/server/serverHelpers/registeredOperations.js +74 -21
- package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
- package/dist/server/serverHelpers/requestSanitization.d.ts +11 -0
- package/dist/server/serverHelpers/requestSanitization.js +20 -0
- package/dist/server/serverHelpers/requestSanitization.js.map +1 -0
- package/dist/server/serverHelpers/serverHandlers.js +6 -3
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.d.ts +18 -0
- package/dist/server/serverHelpers/serverUtilities.js +90 -20
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/serverHelpers/sharedMessageEncoding.d.ts +67 -0
- package/dist/server/serverHelpers/sharedMessageEncoding.js +280 -0
- package/dist/server/serverHelpers/sharedMessageEncoding.js.map +1 -0
- package/dist/server/serverHelpers/uwsServer.js +19 -1
- package/dist/server/serverHelpers/uwsServer.js.map +1 -1
- package/dist/server/static.js +24 -28
- package/dist/server/static.js.map +1 -1
- package/dist/server/storageReclamation.d.ts +5 -0
- package/dist/server/storageReclamation.js +16 -0
- package/dist/server/storageReclamation.js.map +1 -1
- package/dist/server/threads/itc.d.ts +7 -2
- package/dist/server/threads/itc.js +5 -1
- package/dist/server/threads/itc.js.map +1 -1
- package/dist/server/threads/manageThreads.d.ts +26 -4
- package/dist/server/threads/manageThreads.js +517 -72
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/socketRouter.js +89 -26
- package/dist/server/threads/socketRouter.js.map +1 -1
- package/dist/server/threads/threadHeapMemory.d.ts +2 -0
- package/dist/server/threads/threadHeapMemory.js +31 -0
- package/dist/server/threads/threadHeapMemory.js.map +1 -0
- package/dist/server/threads/threadServer.js +46 -15
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/sqlEngine/config.d.ts +1 -3
- package/dist/sqlEngine/config.js +19 -16
- package/dist/sqlEngine/config.js.map +1 -1
- package/dist/sqlTranslator/index.d.ts +1 -1
- package/dist/sqlTranslator/index.js +30 -7
- package/dist/sqlTranslator/index.js.map +1 -1
- package/dist/upgrade/directives/5-3-0.d.ts +7 -0
- package/dist/upgrade/directives/5-3-0.js +148 -0
- package/dist/upgrade/directives/5-3-0.js.map +1 -0
- package/dist/upgrade/directives/directivesController.js +2 -1
- package/dist/upgrade/directives/directivesController.js.map +1 -1
- package/dist/utility/OperationFunctionCaller.js +2 -1
- package/dist/utility/OperationFunctionCaller.js.map +1 -1
- package/dist/utility/common_utils.d.ts +16 -0
- package/dist/utility/common_utils.js +32 -6
- package/dist/utility/common_utils.js.map +1 -1
- package/dist/utility/componentNames.d.ts +8 -0
- package/dist/utility/componentNames.js +12 -1
- package/dist/utility/componentNames.js.map +1 -1
- package/dist/utility/environment/environmentManager.js +3 -6
- package/dist/utility/environment/environmentManager.js.map +1 -1
- package/dist/utility/environment/systemInformation.d.ts +1 -0
- package/dist/utility/environment/systemInformation.js +1 -0
- package/dist/utility/environment/systemInformation.js.map +1 -1
- package/dist/utility/errors/commonErrors.d.ts +2 -0
- package/dist/utility/errors/commonErrors.js +2 -0
- package/dist/utility/errors/commonErrors.js.map +1 -1
- package/dist/utility/errors/hdbError.d.ts +17 -0
- package/dist/utility/errors/hdbError.js +30 -1
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/globalSchema.d.ts +18 -0
- package/dist/utility/hdbTerms.d.ts +16 -0
- package/dist/utility/hdbTerms.js +18 -2
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/logging/harper_logger.d.ts +2 -0
- package/dist/utility/logging/harper_logger.js +91 -14
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/logging/logRotator.js +76 -49
- package/dist/utility/logging/logRotator.js.map +1 -1
- package/dist/utility/nodeIdentity.d.ts +9 -0
- package/dist/utility/nodeIdentity.js +58 -0
- package/dist/utility/nodeIdentity.js.map +1 -0
- package/dist/utility/npmUtilities.js +9 -7
- package/dist/utility/npmUtilities.js.map +1 -1
- package/dist/utility/operationPermissions.d.ts +3 -1
- package/dist/utility/operationPermissions.js +16 -1
- package/dist/utility/operationPermissions.js.map +1 -1
- package/dist/utility/operation_authorization.d.ts +10 -7
- package/dist/utility/operation_authorization.js +212 -41
- package/dist/utility/operation_authorization.js.map +1 -1
- package/dist/utility/watchPath.d.ts +29 -0
- package/dist/utility/watchPath.js +68 -0
- package/dist/utility/watchPath.js.map +1 -0
- package/dist/utility/watcherFallback.d.ts +86 -0
- package/dist/utility/watcherFallback.js +278 -1
- package/dist/utility/watcherFallback.js.map +1 -1
- package/dist/validation/configValidator.d.ts +12 -0
- package/dist/validation/configValidator.js +199 -72
- package/dist/validation/configValidator.js.map +1 -1
- package/dist/validation/installValidator.js +12 -0
- package/dist/validation/installValidator.js.map +1 -1
- package/dist/validation/validationWrapper.d.ts +11 -0
- package/dist/validation/validationWrapper.js +16 -3
- package/dist/validation/validationWrapper.js.map +1 -1
- package/index.ts +2 -0
- package/json/systemSchema.json +52 -0
- package/npm-shrinkwrap.json +235 -233
- package/package.json +8 -7
- package/resources/DESIGN.md +114 -52
- package/resources/DatabaseTransaction.ts +522 -130
- package/resources/LMDBTransaction.ts +26 -4
- package/resources/PrimaryRocksDatabase.ts +29 -3
- package/resources/RecordEncoder.ts +103 -8
- package/resources/RequestTarget.ts +2 -0
- package/resources/Resource.ts +17 -9
- package/resources/ResourceInterface.ts +23 -0
- package/resources/RocksIndexStore.ts +30 -10
- package/resources/RocksTransactionLogStore.ts +78 -21
- package/resources/Table.ts +1107 -164
- package/resources/auditStore.ts +187 -21
- package/resources/blob.ts +1029 -110
- package/resources/branchDatabase.ts +941 -0
- package/resources/crdt.ts +76 -12
- package/resources/databases.ts +1354 -522
- package/resources/defineTable.ts +18 -2
- package/resources/graphql.ts +70 -16
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +6 -2
- package/resources/longLivedTransactions.ts +360 -0
- package/resources/models/backendRegistry.ts +84 -2
- package/resources/models/bootstrap.ts +473 -28
- package/resources/recordLock.ts +253 -0
- package/resources/replayLogs.ts +147 -26
- package/resources/replayLogsGuards.ts +171 -8
- package/resources/search.ts +154 -32
- package/resources/transactionBroadcast.ts +3 -3
- package/security/auth.ts +68 -29
- package/security/authn/oidc/claims.ts +72 -0
- package/security/authn/oidc/identityToken.ts +129 -0
- package/security/authn/oidc/jwks.ts +260 -0
- package/security/authn/oidc/providers/generic.ts +40 -0
- package/security/authn/oidc/providers/githubActions.ts +137 -0
- package/security/authn/oidc/providers/index.ts +52 -0
- package/security/authn/oidc/tokenExchange.ts +300 -0
- package/security/authn/oidc/trustPolicyOperations.ts +343 -0
- package/security/authn/oidc/types.ts +41 -0
- package/security/certificateVerification/index.ts +54 -13
- package/security/certificateVerification/trustedIssuers.ts +76 -0
- package/security/certificateVerification/types.ts +1 -0
- package/security/credentialProvenance.ts +47 -0
- package/security/credentialRejection.ts +22 -0
- package/security/deferredAuthentication.ts +71 -0
- package/security/impersonation.ts +117 -12
- package/security/jsLoader.ts +81 -18
- package/security/keys.ts +298 -72
- package/security/operationScope.ts +33 -0
- package/security/permissionsTranslator.js +23 -0
- package/security/tokenAuthentication.ts +233 -12
- package/security/user.ts +4 -3
- package/server/DESIGN.md +183 -16
- package/server/DurableSubscriptionsSession.ts +71 -11
- package/server/REST.ts +79 -1
- package/server/graphqlQuerying.ts +4 -0
- package/server/http.ts +99 -18
- package/server/itc/serverHandlers.js +8 -1
- package/server/jobs/jobProcess.ts +8 -2
- package/server/jobs/jobs.ts +4 -1
- package/server/liveSubscriptionAuth.ts +129 -46
- package/server/loadRootComponents.js +50 -8
- package/server/mqtt.ts +179 -38
- package/server/nodeName.ts +103 -21
- package/server/serverHelpers/Headers.ts +131 -0
- package/server/serverHelpers/contentTypes.ts +29 -21
- package/server/serverHelpers/deployValidationState.ts +24 -13
- package/server/serverHelpers/operationAuthorizationState.ts +34 -3
- package/server/serverHelpers/registeredOperations.ts +79 -22
- package/server/serverHelpers/requestSanitization.ts +15 -0
- package/server/serverHelpers/serverHandlers.js +6 -3
- package/server/serverHelpers/serverUtilities.ts +104 -31
- package/server/serverHelpers/sharedMessageEncoding.ts +307 -0
- package/server/serverHelpers/uwsServer.ts +17 -2
- package/server/static.ts +23 -29
- package/server/storageReclamation.ts +13 -0
- package/server/threads/itc.js +11 -1
- package/server/threads/manageThreads.js +526 -63
- package/server/threads/socketRouter.ts +81 -26
- package/server/threads/threadHeapMemory.ts +26 -0
- package/server/threads/threadServer.js +43 -15
- package/sqlTranslator/index.ts +31 -8
- package/studio/web/assets/{Chat-Br06zdMA.js → Chat-BnCBegQz.js} +1 -1
- package/studio/web/assets/{FloatingChat-BWImX5fA.js → FloatingChat-CoDW1ySS.js} +3 -3
- package/studio/web/assets/{apiToken-DN0nmDsq.js → apiToken-Bwk5BLXW.js} +1 -1
- package/studio/web/assets/{applications-kSxVoyeU.js → applications-DHxGi7JH.js} +1 -1
- package/studio/web/assets/{cssMode-C1JeufH5.js → cssMode-s0cWI-_M.js} +1 -1
- package/studio/web/assets/{editor-qoo9CrGO.js → editor-DNcRHK54.js} +1 -1
- package/studio/web/assets/{html-Dt4IIy04.js → html-Bdssedlg.js} +1 -1
- package/studio/web/assets/{htmlMode-DXgKKr4C.js → htmlMode-CoDlJ3fw.js} +1 -1
- package/studio/web/assets/{index-6onkYFOG.js → index-D6sxmFLR.js} +5 -5
- package/studio/web/assets/{index.lazy-BrCFnpNJ.js → index.lazy-tmU5BS8s.js} +1 -1
- package/studio/web/assets/{javascript-DNCQGUBc.js → javascript-B8meVSTH.js} +1 -1
- package/studio/web/assets/{jsonMode-CR6HWruP.js → jsonMode-DpIPd35T.js} +1 -1
- package/studio/web/assets/{languageServices-BM4fI4rS.js → languageServices-C_5FMJzQ.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-DSa1ttcD.js → lspLanguageFeatures-BIzNBzPK.js} +1 -1
- package/studio/web/assets/{notifications-BHXLnh6x.js → notifications-CQf18QKb.js} +1 -1
- package/studio/web/assets/{notifications-CUmtIA6z.js → notifications-CvZivSbh.js} +1 -1
- package/studio/web/assets/{profile-8BeFSF3j.js → profile-DdOwtntb.js} +1 -1
- package/studio/web/assets/{regions-C8qR0HhD.js → regions-n69fwagr.js} +1 -1
- package/studio/web/assets/{register-B4n5i0SD.js → register-PfWTCXWB.js} +2 -2
- package/studio/web/assets/{setComponentFile-g0_B0lgX.js → setComponentFile-Bg6O7X0S.js} +1 -1
- package/studio/web/assets/{setup-B0CTj_Q5.js → setup-CUx_aUDl.js} +2 -2
- package/studio/web/assets/{status-C6Yib7-K.js → status-D7BVKqX9.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-Dgta7bVi.js → toggleHighContrast-DBSyXzMr.js} +1 -1
- package/studio/web/assets/{tsMode-CH_jHvU-.js → tsMode-BByKCjBS.js} +1 -1
- package/studio/web/assets/{typescript-Co9LCXd5.js → typescript-DDLnLpw9.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-wKC8NsC_.js → useEntityRestURL-31CHGaHk.js} +1 -1
- package/studio/web/assets/{workers-CWeLxCXA.js → workers-pR3jRY9D.js} +1 -1
- package/studio/web/assets/{xml-BadC-0Rk.js → xml-2iRnMhQO.js} +1 -1
- package/studio/web/assets/{yaml-BiUfxPbC.js → yaml-Bf92gJpd.js} +1 -1
- package/studio/web/index.html +1 -1
- package/upgrade/directives/5-3-0.ts +132 -0
- package/upgrade/directives/directivesController.ts +2 -1
- package/utility/OperationFunctionCaller.ts +2 -1
- package/utility/common_utils.ts +30 -5
- package/utility/componentNames.ts +12 -0
- package/utility/environment/environmentManager.ts +3 -7
- package/utility/environment/systemInformation.ts +7 -0
- package/utility/errors/commonErrors.ts +4 -0
- package/utility/errors/hdbError.ts +29 -0
- package/utility/hdbTerms.ts +17 -0
- package/utility/logging/harper_logger.ts +87 -14
- package/utility/logging/logRotator.ts +72 -45
- package/utility/nodeIdentity.ts +45 -0
- package/utility/npmUtilities.ts +10 -8
- package/utility/operationPermissions.ts +18 -1
- package/utility/operation_authorization.ts +231 -42
- package/utility/watchPath.ts +63 -0
- package/utility/watcherFallback.ts +270 -0
- package/validation/configValidator.ts +209 -72
- package/validation/installValidator.ts +15 -0
- package/validation/validationWrapper.ts +18 -4
|
@@ -36,14 +36,24 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.
|
|
39
|
+
exports.BRANCH_REMOVING_SUFFIX = exports.BRANCH_ROOT_DIR = exports.databases = exports.tables = exports.databaseEventsEmitter = exports.NON_REPLICATING_SYSTEM_TABLES = void 0;
|
|
40
40
|
exports.isReadOnlyMode = isReadOnlyMode;
|
|
41
41
|
exports.getRocksCompression = getRocksCompression;
|
|
42
42
|
exports.resetRocksCompression = resetRocksCompression;
|
|
43
43
|
exports.toRocksCompression = toRocksCompression;
|
|
44
44
|
exports.getTables = getTables;
|
|
45
45
|
exports.getDatabases = getDatabases;
|
|
46
|
+
exports.hydrateBranchRelationships = hydrateBranchRelationships;
|
|
46
47
|
exports.readMetaDb = readMetaDb;
|
|
48
|
+
exports.resolveBranchPath = resolveBranchPath;
|
|
49
|
+
exports.quarantineBranchIdentity = quarantineBranchIdentity;
|
|
50
|
+
exports.assertBranchIdentityAvailable = assertBranchIdentityAvailable;
|
|
51
|
+
exports.reserveBranchIdentity = reserveBranchIdentity;
|
|
52
|
+
exports.retakeBranchIdentity = retakeBranchIdentity;
|
|
53
|
+
exports.releaseBranchIdentity = releaseBranchIdentity;
|
|
54
|
+
exports.isBranchIdentity = isBranchIdentity;
|
|
55
|
+
exports.openBranchDatabase = openBranchDatabase;
|
|
56
|
+
exports.closeBranchDatabases = closeBranchDatabases;
|
|
47
57
|
exports.resetDatabases = resetDatabases;
|
|
48
58
|
exports.resolveDatabaseStorageRoot = resolveDatabaseStorageRoot;
|
|
49
59
|
exports.resolveDatabasePath = resolveDatabasePath;
|
|
@@ -52,10 +62,9 @@ exports.dropDatabase = dropDatabase;
|
|
|
52
62
|
exports.closeDatabase = closeDatabase;
|
|
53
63
|
exports.closeLoadedDatabases = closeLoadedDatabases;
|
|
54
64
|
exports.table = table;
|
|
65
|
+
exports.scopedTableFactory = scopedTableFactory;
|
|
66
|
+
exports.reloadBranchAt = reloadBranchAt;
|
|
55
67
|
exports.canonicalizeIndexOptions = canonicalizeIndexOptions;
|
|
56
|
-
exports.setIndexingCheckpointPeriod = setIndexingCheckpointPeriod;
|
|
57
|
-
exports.resumeStartKey = resumeStartKey;
|
|
58
|
-
exports.tryAcquireUpdateAttributesLock = tryAcquireUpdateAttributesLock;
|
|
59
68
|
exports.dropTableMeta = dropTableMeta;
|
|
60
69
|
exports.onUpdatedTable = onUpdatedTable;
|
|
61
70
|
exports.onRemovedTable = onRemovedTable;
|
|
@@ -63,7 +72,6 @@ exports.onRemovedDB = onRemovedDB;
|
|
|
63
72
|
exports.getDefaultCompression = getDefaultCompression;
|
|
64
73
|
exports.flushDatabases = flushDatabases;
|
|
65
74
|
const node_events_1 = require("node:events");
|
|
66
|
-
const node_crypto_1 = require("node:crypto");
|
|
67
75
|
const environmentManager_ts_1 = require("../utility/environment/environmentManager.js");
|
|
68
76
|
const terms_ts_1 = require("../utility/lmdb/terms.js");
|
|
69
77
|
const lmdb_1 = require("lmdb");
|
|
@@ -85,9 +93,10 @@ const harper_logger_ts_1 = __importDefault(require("../utility/logging/harper_lo
|
|
|
85
93
|
const { forComponent } = harper_logger_ts_1.default;
|
|
86
94
|
const manageThreads = __importStar(require("../server/threads/manageThreads.js"));
|
|
87
95
|
const auditStore_ts_1 = require("./auditStore.js");
|
|
88
|
-
const storageReclamation_ts_1 = require("../server/storageReclamation.js");
|
|
89
96
|
const RecordEncoder_ts_1 = require("./RecordEncoder.js");
|
|
90
97
|
const blob_ts_1 = require("./blob.js");
|
|
98
|
+
const storageReclamation_ts_1 = require("../server/storageReclamation.js");
|
|
99
|
+
const common_validators_ts_1 = require("../validation/common_validators.js");
|
|
91
100
|
const customIndexes_ts_1 = require("./indexes/customIndexes.js");
|
|
92
101
|
const OpenDBIObject_ts_1 = require("../utility/lmdb/OpenDBIObject.js");
|
|
93
102
|
const rocksdb_js_1 = require("@harperfast/rocksdb-js");
|
|
@@ -144,8 +153,60 @@ function markInternalDbiNonVersioned(dbisDb) {
|
|
|
144
153
|
const logger = forComponent('storage');
|
|
145
154
|
const DEFAULT_DATABASE_NAME = 'data';
|
|
146
155
|
const DEFINED_TABLES = Symbol('defined-tables');
|
|
156
|
+
const CATALOG_RELATIONSHIP = Symbol('catalog-relationship');
|
|
147
157
|
const DEFAULT_COMPRESSION_THRESHOLD = ((0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.STORAGE_PAGESIZE) || 4096) - 60; // larger than this requires multiple pages
|
|
148
158
|
(0, environmentManager_ts_1.initSync)();
|
|
159
|
+
let relationshipsToHydrate = [];
|
|
160
|
+
const reportedRelationshipErrors = new Set();
|
|
161
|
+
// an interrupted create is reported once per table and thread, not on every rescan
|
|
162
|
+
const reportedIncompleteCatalogs = new Set();
|
|
163
|
+
function normalizeRelationships(attributes) {
|
|
164
|
+
const relationships = [];
|
|
165
|
+
for (const attribute of attributes) {
|
|
166
|
+
const target = attribute.relationshipReference;
|
|
167
|
+
if (!attribute.relationship || !target)
|
|
168
|
+
continue;
|
|
169
|
+
const relationship = {};
|
|
170
|
+
if (typeof attribute.relationship.from === 'string')
|
|
171
|
+
relationship.from = attribute.relationship.from;
|
|
172
|
+
if (typeof attribute.relationship.to === 'string')
|
|
173
|
+
relationship.to = attribute.relationship.to;
|
|
174
|
+
// the GraphQL parser hands every directive argument over as a string, and the resolver reads
|
|
175
|
+
// filterMissing for truthiness, so persist what the resolver would see rather than the literal
|
|
176
|
+
if (attribute.relationship.filterMissing !== undefined)
|
|
177
|
+
relationship.filterMissing = Boolean(attribute.relationship.filterMissing);
|
|
178
|
+
if (!relationship.from && !relationship.to)
|
|
179
|
+
continue;
|
|
180
|
+
const definition = {
|
|
181
|
+
name: attribute.name,
|
|
182
|
+
type: attribute.type,
|
|
183
|
+
relationship,
|
|
184
|
+
target: { database: target.database, table: target.table },
|
|
185
|
+
};
|
|
186
|
+
if (attribute.type === 'array')
|
|
187
|
+
definition.elements = { type: attribute.elements?.type };
|
|
188
|
+
relationships.push(definition);
|
|
189
|
+
}
|
|
190
|
+
return relationships;
|
|
191
|
+
}
|
|
192
|
+
function relationshipEquals(left, right) {
|
|
193
|
+
return (left?.name === right?.name &&
|
|
194
|
+
left?.type === right?.type &&
|
|
195
|
+
left?.elements?.type === right?.elements?.type &&
|
|
196
|
+
left?.relationship?.from === right?.relationship?.from &&
|
|
197
|
+
left?.relationship?.to === right?.relationship?.to &&
|
|
198
|
+
left?.relationship?.filterMissing === right?.relationship?.filterMissing &&
|
|
199
|
+
left?.target?.database === right?.target?.database &&
|
|
200
|
+
left?.target?.table === right?.target?.table);
|
|
201
|
+
}
|
|
202
|
+
function relationshipListsEqual(left, right) {
|
|
203
|
+
if (!Array.isArray(left) || left.length !== right.length)
|
|
204
|
+
return false;
|
|
205
|
+
for (let index = 0; index < right.length; index++)
|
|
206
|
+
if (!relationshipEquals(left[index], right[index]))
|
|
207
|
+
return false;
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
149
210
|
/**
|
|
150
211
|
* The RocksDB block/blob codec for every column family this process opens (`storage.rocks.compression`),
|
|
151
212
|
* or `undefined` to leave rocksdb-js on its own default (lz4 wherever the native build has it).
|
|
@@ -312,9 +373,12 @@ function openRocksDatabase(path, options) {
|
|
|
312
373
|
else {
|
|
313
374
|
db = new PrimaryRocksDatabase_ts_1.PrimaryRocksDatabase(path, options).open();
|
|
314
375
|
// the RocksDB put and remove return promises, which masks thrown errors in non-awaiting calls to put/remove,
|
|
315
|
-
// making them unsafe to replace LMDB methods, which will synchronously throw errors if there is a problem
|
|
376
|
+
// making them unsafe to replace LMDB methods, which will synchronously throw errors if there is a problem.
|
|
377
|
+
// The versioned remove is necessarily async and its callers must await or otherwise track its promise.
|
|
316
378
|
db.put = db.putSync;
|
|
317
|
-
db.remove =
|
|
379
|
+
db.remove = ((id, removeOptions) => typeof removeOptions === 'number'
|
|
380
|
+
? db.removeIfVersion(id, removeOptions)
|
|
381
|
+
: db.removeSync(id, removeOptions));
|
|
318
382
|
db.encoder.name = options.name;
|
|
319
383
|
}
|
|
320
384
|
db.env = {};
|
|
@@ -326,22 +390,32 @@ const rocksdbDatabaseEnvs = new Map();
|
|
|
326
390
|
(0, globals_js_1._assignPackageExport)('databases', exports.databases);
|
|
327
391
|
(0, globals_js_1._assignPackageExport)('tables', exports.tables);
|
|
328
392
|
const NEXT_TABLE_ID = Symbol.for('next-table-id');
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
393
|
+
// Restore every field used by `commonChanged`, plus `indexed` and `indexNulls`,
|
|
394
|
+
// from the durable descriptor. In particular, preserve `indexNulls: false` so
|
|
395
|
+
// an index that excludes nulls is not reopened as though it contains them.
|
|
396
|
+
const PEER_REDEFINABLE_FIELDS = [
|
|
397
|
+
'type',
|
|
398
|
+
'indexed',
|
|
399
|
+
'indexNulls',
|
|
400
|
+
'nullable',
|
|
401
|
+
'enumerable',
|
|
402
|
+
'version',
|
|
403
|
+
'elements',
|
|
404
|
+
'properties',
|
|
405
|
+
'embed',
|
|
406
|
+
];
|
|
407
|
+
// `indexNulls` is derived from the durable descriptor, never sent by a peer, so naming it in the
|
|
408
|
+
// discard warn would blame the peer for a field it did not write.
|
|
409
|
+
const PEER_DECLARABLE_FIELDS = PEER_REDEFINABLE_FIELDS.filter((field) => field !== 'indexNulls');
|
|
410
|
+
// A cluster-origin caller's list can predate a declaration another thread has already committed, so on
|
|
411
|
+
// that path the descriptor — not the caller — decides what the attribute is, in both directions.
|
|
412
|
+
function applyDurableDeclaration(attribute, descriptor) {
|
|
413
|
+
for (const field of PEER_REDEFINABLE_FIELDS) {
|
|
414
|
+
if (field in descriptor)
|
|
415
|
+
attribute[field] = descriptor[field];
|
|
416
|
+
else
|
|
417
|
+
delete attribute[field];
|
|
418
|
+
}
|
|
345
419
|
}
|
|
346
420
|
// How many times the schema load will try to finish a tombstoned drop before
|
|
347
421
|
// giving up for the rest of this process's lifetime. A drop that fails once
|
|
@@ -416,6 +490,7 @@ function getDatabases() {
|
|
|
416
490
|
}
|
|
417
491
|
loadedDatabases = true;
|
|
418
492
|
definedDatabases = new Map();
|
|
493
|
+
relationshipsToHydrate = [];
|
|
419
494
|
const hdbBasePath = (0, environmentManager_ts_1.getHdbBasePath)();
|
|
420
495
|
let databasePath = hdbBasePath && (0, path_1.join)(hdbBasePath, hdbTerms_ts_1.DATABASES_DIR_NAME);
|
|
421
496
|
const schemaConfigs = (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.DATABASES) || {};
|
|
@@ -440,10 +515,15 @@ function getDatabases() {
|
|
|
440
515
|
// create it (schemaRegex forbids the backtick), but the scan opens any CURRENT+MANIFEST dir
|
|
441
516
|
if (databaseEntry.name === restoreMarker_ts_1.RESTORE_META_DIR)
|
|
442
517
|
continue;
|
|
518
|
+
// branch directories are process-local derivatives, never databases in their own right
|
|
519
|
+
if (databaseEntry.name === exports.BRANCH_ROOT_DIR)
|
|
520
|
+
continue;
|
|
443
521
|
const dbName = (0, path_1.basename)(databaseEntry.name, '.mdb');
|
|
444
522
|
const dbPath = (0, path_1.join)(databasePath, databaseEntry.name);
|
|
445
523
|
if (blockedByRestore.has(dbName))
|
|
446
524
|
continue;
|
|
525
|
+
if (isOpenBranchPath(dbPath))
|
|
526
|
+
continue;
|
|
447
527
|
if (databaseEntry.isFile() &&
|
|
448
528
|
(0, path_1.extname)(databaseEntry.name).toLowerCase() === '.mdb' &&
|
|
449
529
|
!schemaConfigs[dbName]?.path) {
|
|
@@ -495,8 +575,12 @@ function getDatabases() {
|
|
|
495
575
|
continue; // migration staging dir
|
|
496
576
|
if (databaseEntry.name === restoreMarker_ts_1.RESTORE_META_DIR)
|
|
497
577
|
continue; // reserved restore-metadata dir
|
|
578
|
+
if (databaseEntry.name === exports.BRANCH_ROOT_DIR)
|
|
579
|
+
continue; // reserved branch root
|
|
498
580
|
if (blockedByRestore.has((0, path_1.basename)(databaseEntry.name, '.mdb')))
|
|
499
581
|
continue;
|
|
582
|
+
if (isOpenBranchPath((0, path_1.join)(databasePath, databaseEntry.name)))
|
|
583
|
+
continue;
|
|
500
584
|
if (databaseEntry.isFile() && (0, path_1.extname)(databaseEntry.name).toLowerCase() === '.mdb') {
|
|
501
585
|
readMetaDb((0, path_1.join)(databasePath, databaseEntry.name), (0, path_1.basename)(databaseEntry.name, '.mdb'), dbName);
|
|
502
586
|
}
|
|
@@ -541,11 +625,15 @@ function getDatabases() {
|
|
|
541
625
|
for (const tableName in tables) {
|
|
542
626
|
if (!definedTables.has(tableName)) {
|
|
543
627
|
logger.trace(`delete table class ${tableName}`);
|
|
628
|
+
tables[tableName]?.cleanup?.();
|
|
544
629
|
delete tables[tableName];
|
|
545
630
|
}
|
|
546
631
|
}
|
|
547
632
|
}
|
|
548
633
|
else {
|
|
634
|
+
const removedTables = exports.databases[dbName];
|
|
635
|
+
for (const tableName in removedTables)
|
|
636
|
+
removedTables[tableName]?.cleanup?.();
|
|
549
637
|
delete exports.databases[dbName];
|
|
550
638
|
if (dbName === 'data') {
|
|
551
639
|
for (const tableName in exports.tables) {
|
|
@@ -555,6 +643,7 @@ function getDatabases() {
|
|
|
555
643
|
}
|
|
556
644
|
}
|
|
557
645
|
}
|
|
646
|
+
hydrateCatalogRelationships();
|
|
558
647
|
if ((0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.ANALYTICS_REPLICATE) === false) {
|
|
559
648
|
if (!exports.NON_REPLICATING_SYSTEM_TABLES.includes('hdb_analytics'))
|
|
560
649
|
exports.NON_REPLICATING_SYSTEM_TABLES.push('hdb_analytics');
|
|
@@ -573,6 +662,148 @@ function getDatabases() {
|
|
|
573
662
|
}
|
|
574
663
|
return exports.databases;
|
|
575
664
|
}
|
|
665
|
+
/**
|
|
666
|
+
* Hydrate one branch's relationships, resolving each target against the application's own branches
|
|
667
|
+
* first and only then against the real databases: a target the application also branched must be its
|
|
668
|
+
* branch's table, and a target it did not branch is legitimately the shared one.
|
|
669
|
+
*/
|
|
670
|
+
function hydrateBranchRelationships(branch, branches) {
|
|
671
|
+
const resolveTarget = (target) => {
|
|
672
|
+
const targetBranch = branches.get(target.database);
|
|
673
|
+
// A branched target resolves ONLY within that branch. A durable branch is a checkpoint frozen
|
|
674
|
+
// at creation while the base keeps evolving, so falling through to the base for a table the
|
|
675
|
+
// branch's own copy lacks would point a branched application's relationship reads at live base
|
|
676
|
+
// data -- the fallback belongs to a database the application did not branch, never to one it did.
|
|
677
|
+
return targetBranch ? targetBranch.tables?.[target.table] : exports.databases[target.database]?.[target.table];
|
|
678
|
+
};
|
|
679
|
+
// Kept, not drained, like the global list: a target declared later (on this or another thread) is
|
|
680
|
+
// picked up by the next pass, and `hydrateTableRelationships` is a no-op once everything resolves.
|
|
681
|
+
for (const hydration of branch.pendingRelationships) {
|
|
682
|
+
try {
|
|
683
|
+
hydrateTableRelationships(hydration, resolveTarget, false);
|
|
684
|
+
}
|
|
685
|
+
catch (error) {
|
|
686
|
+
logger.error(`Unable to hydrate persisted relationships for branch table ${hydration.databaseName}.${hydration.tableName}`, error);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
function hydrateCatalogRelationships() {
|
|
691
|
+
for (const hydration of relationshipsToHydrate) {
|
|
692
|
+
try {
|
|
693
|
+
hydrateTableRelationships(hydration);
|
|
694
|
+
}
|
|
695
|
+
catch (error) {
|
|
696
|
+
const key = `${hydration.databaseName}.${hydration.tableName}:hydrate`;
|
|
697
|
+
if (!reportedRelationshipErrors.has(key)) {
|
|
698
|
+
reportedRelationshipErrors.add(key);
|
|
699
|
+
logger.error(`Unable to hydrate persisted relationships for ${hydration.databaseName}.${hydration.tableName}`, error);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
const resolveTargetGlobally = (target) => exports.databases[target.database]?.[target.table];
|
|
705
|
+
function hydrateTableRelationships({ table, databaseName, tableName, definitions }, resolveTarget = resolveTargetGlobally, announce = true) {
|
|
706
|
+
const hydratable = [];
|
|
707
|
+
for (let index = 0; index < definitions.length; index++) {
|
|
708
|
+
const definition = definitions[index];
|
|
709
|
+
// Keyed by name rather than list position, so a reordered list cannot inherit the previous
|
|
710
|
+
// occupant's reported state and swallow a different relationship's failure — and by reason, so
|
|
711
|
+
// hydrating one entry does not clear the report of a same-named invalid duplicate.
|
|
712
|
+
const errorKey = `${databaseName}.${tableName}:${definition?.name || `#${index}`}`;
|
|
713
|
+
if (!validRelationshipDefinition(definition, definitions, index)) {
|
|
714
|
+
reportRelationshipError(`${errorKey}:invalid`, `Ignoring invalid persisted relationship ${databaseName}.${tableName}[${index}]`);
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
// a live schema attribute of the same name owns the name; the catalog copy is only a stand-in
|
|
718
|
+
// for threads that never loaded the schema
|
|
719
|
+
if (table.attributes.some((attribute) => attribute.name === definition.name && !attribute[CATALOG_RELATIONSHIP]))
|
|
720
|
+
continue;
|
|
721
|
+
const targetTable = resolveTarget(definition.target);
|
|
722
|
+
if (!targetTable || !relationshipFieldsExist(table, targetTable, definition)) {
|
|
723
|
+
reportRelationshipError(`${errorKey}:unavailable`, `Unable to hydrate persisted relationship ${databaseName}.${tableName}.${definition.name}: target or foreign key is unavailable`);
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
reportedRelationshipErrors.delete(`${errorKey}:unavailable`);
|
|
727
|
+
hydratable.push({ definition, targetTable });
|
|
728
|
+
}
|
|
729
|
+
const installed = table.attributes.filter((attribute) => attribute[CATALOG_RELATIONSHIP]);
|
|
730
|
+
if (installed.length === hydratable.length &&
|
|
731
|
+
hydratable.every(({ definition, targetTable }, index) => relationshipEquals(installed[index], definition) &&
|
|
732
|
+
(installed[index].definition || installed[index].elements?.definition)?.tableClass === targetTable))
|
|
733
|
+
return;
|
|
734
|
+
const attributes = table.attributes.filter((attribute) => !attribute[CATALOG_RELATIONSHIP]);
|
|
735
|
+
for (const { definition, targetTable } of hydratable)
|
|
736
|
+
attributes.push(createCatalogRelationship(definition, targetTable));
|
|
737
|
+
table.attributes.splice(0, table.attributes.length, ...attributes);
|
|
738
|
+
table.schemaVersion++;
|
|
739
|
+
table.updatedAttributes();
|
|
740
|
+
if (announce)
|
|
741
|
+
exports.databaseEventsEmitter.emit('updateTable', table);
|
|
742
|
+
}
|
|
743
|
+
function validRelationshipDefinition(definition, definitions, index) {
|
|
744
|
+
if (!definition || typeof definition !== 'object')
|
|
745
|
+
return false;
|
|
746
|
+
const validName = (value) => typeof value === 'string' && value.length > 0 && !/[`/]/.test(value);
|
|
747
|
+
if (!validName(definition.name) || !validName(definition.type))
|
|
748
|
+
return false;
|
|
749
|
+
if (!validName(definition.target?.database) || !validName(definition.target?.table))
|
|
750
|
+
return false;
|
|
751
|
+
if (!definition.relationship || typeof definition.relationship !== 'object')
|
|
752
|
+
return false;
|
|
753
|
+
const { from, to, filterMissing } = definition.relationship;
|
|
754
|
+
if (from !== undefined && !validName(from))
|
|
755
|
+
return false;
|
|
756
|
+
if (to !== undefined && !validName(to))
|
|
757
|
+
return false;
|
|
758
|
+
if (!from && !to)
|
|
759
|
+
return false;
|
|
760
|
+
if (filterMissing !== undefined && typeof filterMissing !== 'boolean')
|
|
761
|
+
return false;
|
|
762
|
+
if (definition.type === 'array' ? !validName(definition.elements?.type) : definition.elements !== undefined)
|
|
763
|
+
return false;
|
|
764
|
+
for (let earlier = 0; earlier < index; earlier++)
|
|
765
|
+
if (definitions[earlier]?.name === definition.name)
|
|
766
|
+
return false;
|
|
767
|
+
return true;
|
|
768
|
+
}
|
|
769
|
+
function relationshipFieldsExist(sourceTable, targetTable, definition) {
|
|
770
|
+
if (definition.relationship.from &&
|
|
771
|
+
!sourceTable.attributes.some((attribute) => attribute.name === definition.relationship.from))
|
|
772
|
+
return false;
|
|
773
|
+
if (definition.relationship.to &&
|
|
774
|
+
!targetTable.attributes.some((attribute) => attribute.name === definition.relationship.to))
|
|
775
|
+
return false;
|
|
776
|
+
return true;
|
|
777
|
+
}
|
|
778
|
+
function createCatalogRelationship(definition, targetTable) {
|
|
779
|
+
const attribute = {
|
|
780
|
+
name: definition.name,
|
|
781
|
+
attribute: definition.name,
|
|
782
|
+
type: definition.type,
|
|
783
|
+
relationship: { ...definition.relationship },
|
|
784
|
+
target: { ...definition.target },
|
|
785
|
+
};
|
|
786
|
+
const targetDefinition = {
|
|
787
|
+
tableClass: targetTable,
|
|
788
|
+
type: targetTable.tableName,
|
|
789
|
+
attributes: targetTable.attributes,
|
|
790
|
+
};
|
|
791
|
+
if (definition.elements) {
|
|
792
|
+
attribute.elements = { type: definition.elements.type };
|
|
793
|
+
Object.defineProperty(attribute.elements, 'definition', { value: targetDefinition, configurable: true });
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
Object.defineProperty(attribute, 'definition', { value: targetDefinition, configurable: true });
|
|
797
|
+
}
|
|
798
|
+
Object.defineProperty(attribute, CATALOG_RELATIONSHIP, { value: true });
|
|
799
|
+
return attribute;
|
|
800
|
+
}
|
|
801
|
+
function reportRelationshipError(key, message) {
|
|
802
|
+
if (reportedRelationshipErrors.has(key))
|
|
803
|
+
return;
|
|
804
|
+
reportedRelationshipErrors.add(key);
|
|
805
|
+
logger.error(message);
|
|
806
|
+
}
|
|
576
807
|
/**
|
|
577
808
|
* Scan a databases directory's entries for restore lock/marker files and return the names of
|
|
578
809
|
* databases that must not be loaded: a held restore lock means a restore is in progress in some
|
|
@@ -612,15 +843,14 @@ function readMetaDb(path, defaultTable, databaseName = DEFAULT_DATABASE_NAME, au
|
|
|
612
843
|
rootStore = (0, lmdb_1.open)(envInit);
|
|
613
844
|
lmdbDatabaseEnvs.set(path, rootStore);
|
|
614
845
|
}
|
|
615
|
-
rootStore
|
|
616
|
-
return initStores(path, rootStore, databaseName, defaultTable, auditPath, isLegacy);
|
|
846
|
+
return initStores(path, rootStore, databaseName, { defaultTable, auditPath, isLegacy });
|
|
617
847
|
}
|
|
618
848
|
catch (error) {
|
|
619
849
|
error.message += ` opening database ${path}`;
|
|
620
850
|
throw error;
|
|
621
851
|
}
|
|
622
852
|
}
|
|
623
|
-
function readRocksMetaDb(path, defaultTable, databaseName = DEFAULT_DATABASE_NAME) {
|
|
853
|
+
function readRocksMetaDb(path, defaultTable, databaseName = DEFAULT_DATABASE_NAME, { destination, storeName, openedStores } = {}) {
|
|
624
854
|
try {
|
|
625
855
|
logger.trace(`loading rocksdb database: ${path}`);
|
|
626
856
|
if (process.env.HARPER_PARENT_PROCESS_PID) {
|
|
@@ -631,14 +861,18 @@ function readRocksMetaDb(path, defaultTable, databaseName = DEFAULT_DATABASE_NAM
|
|
|
631
861
|
}
|
|
632
862
|
let rootStore = rocksdbDatabaseEnvs.get(path);
|
|
633
863
|
if (rootStore) {
|
|
634
|
-
initStores(path, rootStore, databaseName, defaultTable);
|
|
864
|
+
initStores(path, rootStore, databaseName, { defaultTable, destination, storeName, openedStores });
|
|
635
865
|
}
|
|
636
866
|
else {
|
|
637
867
|
rootStore = openRocksDatabase(path, { disableWAL: false, enableStats: true });
|
|
638
868
|
rocksdbDatabaseEnvs.set(path, rootStore);
|
|
639
|
-
initStores(path, rootStore, databaseName, defaultTable);
|
|
640
|
-
//
|
|
641
|
-
|
|
869
|
+
initStores(path, rootStore, databaseName, { defaultTable, destination, storeName, openedStores });
|
|
870
|
+
// A branch (`destination`) recovers its transaction-log tail in `openOrCreate`
|
|
871
|
+
// (branchDatabase.ts), not here: the branch claim elects exactly one replaying thread —
|
|
872
|
+
// applications load on workers, where this call would be a no-op — and awaits the replay
|
|
873
|
+
// before the branch is published to any reader. See the contract note on
|
|
874
|
+
// `openBranchDatabase` (harper#643).
|
|
875
|
+
if (!isReadOnlyMode() && !destination) {
|
|
642
876
|
(0, replayLogs_ts_1.replayLogs)(rootStore, exports.databases[databaseName]);
|
|
643
877
|
}
|
|
644
878
|
}
|
|
@@ -649,7 +883,9 @@ function readRocksMetaDb(path, defaultTable, databaseName = DEFAULT_DATABASE_NAM
|
|
|
649
883
|
throw error;
|
|
650
884
|
}
|
|
651
885
|
}
|
|
652
|
-
function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLegacy) {
|
|
886
|
+
function initStores(path, rootStore, databaseName, { defaultTable, auditPath, isLegacy, destination, storeName, openedStores } = {}) {
|
|
887
|
+
// a store with no tables never reaches the per-table loop below, and blob roots resolve from this
|
|
888
|
+
rootStore.databaseName = storeName ?? databaseName;
|
|
653
889
|
const envInit = new OpenEnvironmentObject_ts_1.default(path, isReadOnlyMode());
|
|
654
890
|
const internalDbiInit = createOpenDBIObject(false);
|
|
655
891
|
let attributesDbi = rootStore.dbisDb;
|
|
@@ -664,6 +900,7 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
|
|
|
664
900
|
else {
|
|
665
901
|
attributesDbi = rootStore.openDB(terms_ts_1.INTERNAL_DBIS_NAME, internalDbiInit);
|
|
666
902
|
}
|
|
903
|
+
openedStores?.push(attributesDbi);
|
|
667
904
|
rootStore.dbisDb = markInternalDbiNonVersioned(attributesDbi);
|
|
668
905
|
}
|
|
669
906
|
let auditStore = rootStore.auditStore;
|
|
@@ -690,7 +927,9 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
|
|
|
690
927
|
auditStore = (0, auditStore_ts_1.openAuditStore)(rootStore);
|
|
691
928
|
}
|
|
692
929
|
}
|
|
693
|
-
const tables = ensureDB(databaseName);
|
|
930
|
+
const tables = destination ?? ensureDB(databaseName);
|
|
931
|
+
if (destination && !destination[DEFINED_TABLES])
|
|
932
|
+
destination[DEFINED_TABLES] = new Set();
|
|
694
933
|
const definedTables = tables[DEFINED_TABLES];
|
|
695
934
|
definedTables.rootStore = rootStore;
|
|
696
935
|
const tablesToLoad = new Map();
|
|
@@ -789,10 +1028,20 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
|
|
|
789
1028
|
}
|
|
790
1029
|
}
|
|
791
1030
|
if (!primaryAttribute) {
|
|
792
|
-
|
|
1031
|
+
const tableKey = `${databaseName}/${tableName}`;
|
|
1032
|
+
if (reportedIncompleteCatalogs.has(tableKey))
|
|
1033
|
+
logger.debug(`Skipping table ${databaseName}.${tableName}: still no primary key row`);
|
|
1034
|
+
else {
|
|
1035
|
+
reportedIncompleteCatalogs.add(tableKey);
|
|
1036
|
+
logger.warn(`Skipping table ${databaseName}.${tableName}: its catalog has attribute rows (${attributes.map((attribute) => attribute.name).join(', ')}) but no primary key row - a create in progress on another thread, or an interrupted one that re-running create_table repairs`);
|
|
1037
|
+
}
|
|
1038
|
+
// not defined until it loads, so the cleanup pass evicts a class left from a dropped same-name table
|
|
1039
|
+
definedTables?.delete(tableName);
|
|
793
1040
|
continue;
|
|
794
1041
|
}
|
|
795
1042
|
}
|
|
1043
|
+
if (reportedIncompleteCatalogs.size)
|
|
1044
|
+
reportedIncompleteCatalogs.delete(`${databaseName}/${tableName}`);
|
|
796
1045
|
// if the table has already been defined, use that class, don't create a new one
|
|
797
1046
|
let table = tables[tableName];
|
|
798
1047
|
// unless its store was migrated to a different engine (e.g. LMDB to RocksDB on startup)
|
|
@@ -838,13 +1087,13 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
|
|
|
838
1087
|
// per-table override of the storage.randomAccessFields default (see OpenDBIObject)
|
|
839
1088
|
if (typeof primaryAttribute.randomAccessFields === 'boolean')
|
|
840
1089
|
dbiInit.randomAccessStructure = primaryAttribute.randomAccessFields;
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1090
|
+
// recorded before the wrapper below, which is the only thing between the native open and
|
|
1091
|
+
// the only list a failed open can release it from
|
|
1092
|
+
const opened = rootStore instanceof rocksdb_js_1.RocksDatabase
|
|
1093
|
+
? openRocksDatabase(rootStore.path, { ...dbiInit, name: primaryAttribute.key, cache: true })
|
|
1094
|
+
: rootStore.openDB(primaryAttribute.key, dbiInit);
|
|
1095
|
+
openedStores?.push(opened);
|
|
1096
|
+
primaryStore = (0, RecordEncoder_ts_1.handleLocalTimeForGets)(opened, rootStore);
|
|
848
1097
|
primaryStore.tableId = tableId;
|
|
849
1098
|
}
|
|
850
1099
|
let attributesUpdated;
|
|
@@ -855,11 +1104,10 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
|
|
|
855
1104
|
if (!attribute.isPrimaryKey && (attribute.indexed || (attribute.attribute && !attribute.name))) {
|
|
856
1105
|
if (!indices[attribute.name]) {
|
|
857
1106
|
const dbi = openIndex(attribute.key, rootStore, attribute);
|
|
1107
|
+
openedStores?.push(dbi);
|
|
858
1108
|
indices[attribute.name] = dbi;
|
|
859
1109
|
indices[attribute.name].indexNulls = attribute.indexNulls;
|
|
860
1110
|
}
|
|
861
|
-
// the only way a thread that never declares the schema reaches Table.indices
|
|
862
|
-
indices[attribute.name].isIndexing = !!attribute.indexingPID;
|
|
863
1111
|
const existingAttribute = existingAttributes.find((existingAttribute) => existingAttribute.name === attribute.name);
|
|
864
1112
|
if (existingAttribute)
|
|
865
1113
|
existingAttributes.splice(existingAttributes.indexOf(existingAttribute), 1, attribute);
|
|
@@ -927,6 +1175,8 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
|
|
|
927
1175
|
}
|
|
928
1176
|
else {
|
|
929
1177
|
table = setTable(tables, tableName, (0, Table_ts_1.makeTable)({
|
|
1178
|
+
// A branch builds into a caller-owned destination; its tables must refuse DDL.
|
|
1179
|
+
isBranch: Boolean(destination),
|
|
930
1180
|
primaryStore,
|
|
931
1181
|
auditStore,
|
|
932
1182
|
audit,
|
|
@@ -948,11 +1198,385 @@ function initStores(path, rootStore, databaseName, defaultTable, auditPath, isLe
|
|
|
948
1198
|
dbisDB: attributesDbi,
|
|
949
1199
|
}));
|
|
950
1200
|
table.schemaVersion = 1;
|
|
951
|
-
|
|
1201
|
+
if (!destination)
|
|
1202
|
+
exports.databaseEventsEmitter.emit('updateTable', table);
|
|
1203
|
+
}
|
|
1204
|
+
if (Array.isArray(primaryAttribute.relationships)) {
|
|
1205
|
+
relationshipsToHydrate.push({ table, databaseName, tableName, definitions: primaryAttribute.relationships });
|
|
1206
|
+
}
|
|
1207
|
+
else if (primaryAttribute.relationships !== undefined) {
|
|
1208
|
+
reportRelationshipError(`${databaseName}.${tableName}:list`, `Ignoring invalid persisted relationship list for ${databaseName}.${tableName}`);
|
|
1209
|
+
relationshipsToHydrate.push({ table, databaseName, tableName, definitions: [] });
|
|
952
1210
|
}
|
|
953
1211
|
}
|
|
954
1212
|
return rootStore;
|
|
955
1213
|
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Branch directories live beside the base database's own storage root, never under the HDB root: a
|
|
1216
|
+
* database can be placed on its own volume, and `createCheckpoint` only hardlinks when source and
|
|
1217
|
+
* target share a filesystem — off-volume it degrades to a full byte copy, which is the property the
|
|
1218
|
+
* whole feature rests on.
|
|
1219
|
+
*
|
|
1220
|
+
* The backticks are what make the name reserved rather than merely conventional: `schemaRegex`
|
|
1221
|
+
* (validation/common_validators.ts) excludes 0x60, so no database can ever be created under this
|
|
1222
|
+
* name and shadow the branch root -- the same protection RESTORE_META_DIR uses.
|
|
1223
|
+
*/
|
|
1224
|
+
exports.BRANCH_ROOT_DIR = '`branches`';
|
|
1225
|
+
/**
|
|
1226
|
+
* Where the branch of `baseName` belonging to `appName` lives. Derived only from those two names, so
|
|
1227
|
+
* every node in a cluster resolves the same application's branch to the same place — the identity an
|
|
1228
|
+
* application's data needs if it is to be addressed, and eventually replicated, cluster-wide.
|
|
1229
|
+
*
|
|
1230
|
+
* App and database are separate path segments: joining them (`<app>__<db>`) is not injective —
|
|
1231
|
+
* `(a__b, c)` and `(a, b__c)` collide — so two declarations could otherwise open one directory.
|
|
1232
|
+
*/
|
|
1233
|
+
function resolveBranchPath(baseName, appName) {
|
|
1234
|
+
for (const [label, segment] of [
|
|
1235
|
+
['application', appName],
|
|
1236
|
+
['database', baseName],
|
|
1237
|
+
]) {
|
|
1238
|
+
if (!segment || segment.includes('/') || segment.includes('\\') || segment === '.' || segment === '..') {
|
|
1239
|
+
throw new Error(`Invalid ${label} name for a branch path: ${JSON.stringify(segment)}`);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return (0, path_1.join)(resolveDatabaseStorageRoot(baseName), exports.BRANCH_ROOT_DIR, appName, baseName);
|
|
1243
|
+
}
|
|
1244
|
+
/** `undefined` marks a path reserved by an open still in flight, which owns it just as firmly. */
|
|
1245
|
+
const openBranches = new Map();
|
|
1246
|
+
/** Store identities in use, so two branches cannot resolve one set of blob roots. */
|
|
1247
|
+
const openBranchIdentities = new Set();
|
|
1248
|
+
/**
|
|
1249
|
+
* Materialization renames its clone in from `<blobRoot>.staging`, so a branch owns two database
|
|
1250
|
+
* names rather than one: a database legally called `<storeName>.staging` resolves its own blob root
|
|
1251
|
+
* to exactly the path the clone removes and renames over. Every check, reservation and release
|
|
1252
|
+
* covers the pair, so the name cannot be claimed at any point where a branch operation may still
|
|
1253
|
+
* delete what it resolves to.
|
|
1254
|
+
*/
|
|
1255
|
+
const BRANCH_STAGING_SUFFIX = '.staging';
|
|
1256
|
+
/**
|
|
1257
|
+
* Suffix of the sibling a branch is renamed to while being removed. A backtick, not a dot:
|
|
1258
|
+
* `schemaRegex` excludes 0x60, so no database can be named such that `<db>` + this suffix is another
|
|
1259
|
+
* branch's directory (with `.removing`, an application branching both `data` and `data.removing`, both
|
|
1260
|
+
* legal names, would destroy one by opening the other).
|
|
1261
|
+
*/
|
|
1262
|
+
exports.BRANCH_REMOVING_SUFFIX = '`removing`';
|
|
1263
|
+
function branchIdentityPair(storeName) {
|
|
1264
|
+
return [storeName, storeName + BRANCH_STAGING_SUFFIX];
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Identities whose blob roots outlived the branch that owned them, because a removal or an abandoned
|
|
1268
|
+
* materialization could not delete them. A database created under such a name would resolve its own
|
|
1269
|
+
* fresh file ids onto files it never wrote, so the name stays refused -- but only against DATABASES.
|
|
1270
|
+
* The branch itself may take it back: materializing it replaces those roots wholesale, which is the
|
|
1271
|
+
* only route that clears the condition without an operator.
|
|
1272
|
+
*/
|
|
1273
|
+
const quarantinedBranchIdentities = new Set();
|
|
1274
|
+
function quarantineBranchIdentity(storeName) {
|
|
1275
|
+
for (const name of branchIdentityPair(storeName)) {
|
|
1276
|
+
quarantinedBranchIdentities.add(name);
|
|
1277
|
+
openBranchIdentities.delete(name);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* True when `dbPath` is a directory an open branch owns. The database scan opens any directory that
|
|
1282
|
+
* holds CURRENT + MANIFEST-*, and harper#643 places a branch inside the directory it walks, so
|
|
1283
|
+
* without this a rescan would rebuild the branch's tables into the global map, overwrite the store
|
|
1284
|
+
* identity its blob roots resolve from, and hand its store to `closeLoadedDatabases`.
|
|
1285
|
+
*/
|
|
1286
|
+
function isOpenBranchPath(dbPath) {
|
|
1287
|
+
if (openBranches.size === 0)
|
|
1288
|
+
return false;
|
|
1289
|
+
// the literal path first: `rocksdbDatabaseEnvs` is keyed by it too, so a directory unlinked under
|
|
1290
|
+
// a live branch handle (realpathSync then throws) must not read as unowned
|
|
1291
|
+
if (openBranches.has(dbPath))
|
|
1292
|
+
return true;
|
|
1293
|
+
try {
|
|
1294
|
+
return openBranches.has((0, node_fs_1.realpathSync)(dbPath));
|
|
1295
|
+
}
|
|
1296
|
+
catch {
|
|
1297
|
+
return false;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* A branch identity resolves its blob roots through `join(…, 'blobs', storeName)`, so it must be a
|
|
1302
|
+
* single path segment: `schemaRegex`, which every other database name is validated against, plus the
|
|
1303
|
+
* dot segments and backslash that regex permits but a path component must not be.
|
|
1304
|
+
*/
|
|
1305
|
+
function assertLegalBranchName(name, description) {
|
|
1306
|
+
if (!name ||
|
|
1307
|
+
name.length > common_validators_ts_1.commonValidators.schema_length.maximum ||
|
|
1308
|
+
!common_validators_ts_1.schemaRegex.test(name) ||
|
|
1309
|
+
name.includes('\\') ||
|
|
1310
|
+
name === '.' ||
|
|
1311
|
+
name === '..') {
|
|
1312
|
+
throw new Error(`Cannot use '${name}' as a branch ${description}: it is not a legal database name`);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Refuse a branch store identity that something else already answers to.
|
|
1317
|
+
*
|
|
1318
|
+
* `storeName` picks the branch's blob roots, and blob file ids restart from each store's own counter,
|
|
1319
|
+
* so two holders of one identity write the same file paths and truncate each other. It must be
|
|
1320
|
+
* checked BEFORE anything destructive runs: materialization removes and replaces the blob root that
|
|
1321
|
+
* this name resolves to, and a real database may legally be called `5_myapp__data` -- `schemaRegex`
|
|
1322
|
+
* permits digits, `_` and `.`. The `.staging` sibling materialization writes is covered too, since a
|
|
1323
|
+
* database may legally carry that name as well.
|
|
1324
|
+
*/
|
|
1325
|
+
function assertBranchIdentityAvailable(storeName) {
|
|
1326
|
+
// The on-disk scan, not just the in-memory maps: a database that exists on disk but has not been
|
|
1327
|
+
// loaded is absent from both, and it owns the blob root this identity would destroy.
|
|
1328
|
+
getDatabases();
|
|
1329
|
+
for (const name of branchIdentityPair(storeName)) {
|
|
1330
|
+
// The directory as well as the maps. `getDatabases` skips a database blocked by restore, so an
|
|
1331
|
+
// in-memory check alone reports its name as free while its blob root is very much real -- and
|
|
1332
|
+
// materialization would then remove and replace it.
|
|
1333
|
+
if (exports.databases[name] ||
|
|
1334
|
+
definedDatabases?.has(name) ||
|
|
1335
|
+
openBranchIdentities.has(name) ||
|
|
1336
|
+
(0, node_fs_1.existsSync)(resolveDatabasePath(name)) ||
|
|
1337
|
+
anotherBranchOwns(name, storeName)) {
|
|
1338
|
+
throw new Error(`Cannot use '${storeName}' as a branch store identity: '${name}' is already in use`);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Does a branch OTHER than the one being opened already answer to this name on disk? `.staging` is
|
|
1344
|
+
* what makes the question two-sided: `<identity>.staging` is both the path a clone renames over and
|
|
1345
|
+
* a legal identity for a branch of a database literally named `<base>.staging`, so each of the pair
|
|
1346
|
+
* can belong to somebody else. Only the primary name read as itself is excluded -- that directory is
|
|
1347
|
+
* the very branch this call is opening.
|
|
1348
|
+
*/
|
|
1349
|
+
function anotherBranchOwns(name, storeName) {
|
|
1350
|
+
if (name !== storeName)
|
|
1351
|
+
return branchDirectoryExistsFor(name);
|
|
1352
|
+
return name.endsWith(BRANCH_STAGING_SUFFIX)
|
|
1353
|
+
? branchDirectoryExistsFor(name.slice(0, -BRANCH_STAGING_SUFFIX.length))
|
|
1354
|
+
: false;
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* Claim the identity as well as checking it, so the window between the check and the branch actually
|
|
1358
|
+
* opening cannot be filled by a concurrent create or a second branch. `releaseBranchIdentity` hands
|
|
1359
|
+
* it back if materialization never gets as far as opening.
|
|
1360
|
+
*/
|
|
1361
|
+
function reserveBranchIdentity(storeName) {
|
|
1362
|
+
assertBranchIdentityAvailable(storeName);
|
|
1363
|
+
retakeBranchIdentity(storeName);
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Take the pair back for an operation that owned it a statement ago -- cleanup, which has to keep
|
|
1367
|
+
* holding the names through the deletions its `close()` just released them for. Deliberately without
|
|
1368
|
+
* the availability check: nothing can have taken a name the caller held until now, and the check runs
|
|
1369
|
+
* the database scan, which at that exact moment would find the branch directory unowned.
|
|
1370
|
+
*/
|
|
1371
|
+
function retakeBranchIdentity(storeName) {
|
|
1372
|
+
for (const name of branchIdentityPair(storeName)) {
|
|
1373
|
+
openBranchIdentities.add(name);
|
|
1374
|
+
quarantinedBranchIdentities.delete(name);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
function releaseBranchIdentity(storeName) {
|
|
1378
|
+
for (const name of branchIdentityPair(storeName))
|
|
1379
|
+
openBranchIdentities.delete(name);
|
|
1380
|
+
}
|
|
1381
|
+
/** Is this name spoken for by a branch? Database creation has to refuse it -- they share a blob root. */
|
|
1382
|
+
function isBranchIdentity(name) {
|
|
1383
|
+
if (openBranchIdentities.has(name) || quarantinedBranchIdentities.has(name))
|
|
1384
|
+
return true;
|
|
1385
|
+
// The in-memory set covers only branches open in THIS process, so after a restart -- or for an
|
|
1386
|
+
// application that is simply not loaded -- a database could take the name of an on-disk branch and
|
|
1387
|
+
// share its blob root. The staging sibling goes through the same route, because it names the path
|
|
1388
|
+
// materialization renames over -- but BOTH readings of a name ending in `.staging` have to be
|
|
1389
|
+
// tried: `schemaRegex` permits `.`, so `4_myapp__data.staging` is either the sibling of a branch of
|
|
1390
|
+
// `data` or a branch of a database actually called `data.staging`.
|
|
1391
|
+
if (branchDirectoryExistsFor(name))
|
|
1392
|
+
return true;
|
|
1393
|
+
return name.endsWith(BRANCH_STAGING_SUFFIX)
|
|
1394
|
+
? branchDirectoryExistsFor(name.slice(0, -BRANCH_STAGING_SUFFIX.length))
|
|
1395
|
+
: false;
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Is there a branch directory answering to this store identity? The identity carries the application
|
|
1399
|
+
* name's length precisely so it can be taken apart again without guessing where the name ends.
|
|
1400
|
+
*/
|
|
1401
|
+
function branchDirectoryExistsFor(storeName) {
|
|
1402
|
+
const prefix = /^(\d+)_/.exec(storeName);
|
|
1403
|
+
if (!prefix)
|
|
1404
|
+
return false;
|
|
1405
|
+
const appLength = Number(prefix[1]);
|
|
1406
|
+
const appName = storeName.slice(prefix[0].length, prefix[0].length + appLength);
|
|
1407
|
+
if (appName.length !== appLength ||
|
|
1408
|
+
storeName.slice(prefix[0].length + appLength, prefix[0].length + appLength + 2) !== '__')
|
|
1409
|
+
return false;
|
|
1410
|
+
const baseName = storeName.slice(prefix[0].length + appLength + 2);
|
|
1411
|
+
if (!baseName)
|
|
1412
|
+
return false;
|
|
1413
|
+
try {
|
|
1414
|
+
const branchPath = resolveBranchPath(baseName, appName);
|
|
1415
|
+
return (0, node_fs_1.existsSync)(branchPath) || (0, node_fs_1.existsSync)(branchPath + exports.BRANCH_REMOVING_SUFFIX);
|
|
1416
|
+
}
|
|
1417
|
+
catch {
|
|
1418
|
+
// Not a name a branch path could hold, so no branch owns it.
|
|
1419
|
+
return false;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Open a RocksDB directory as a **scope-private** database: its Table classes are built into an
|
|
1424
|
+
* object the caller owns and nothing is registered in the global `databases` map, so no enumerator
|
|
1425
|
+
* of that map — analytics, `describe_all`, worker teardown, replication — can observe it.
|
|
1426
|
+
*
|
|
1427
|
+
* `databaseName` is the *logical* name the application knows (`data`), so its schema and code need
|
|
1428
|
+
* no changes. `storeName` is the branch's own identity and is what `getRootBlobPathsForDB` resolves
|
|
1429
|
+
* blob directories from, which is how a branch gets its own blob roots rather than writing into the
|
|
1430
|
+
* base's.
|
|
1431
|
+
*
|
|
1432
|
+
* The caller owns the returned handle; the only thing that closes it on the caller's behalf is
|
|
1433
|
+
* `closeBranchDatabases`, run by an exiting job worker (via `closeLoadedDatabases`) and by an HTTP
|
|
1434
|
+
* worker's shutdown path, so a branch left open on an exiting worker does not linger in the
|
|
1435
|
+
* process-global RocksDB registry.
|
|
1436
|
+
*
|
|
1437
|
+
* Schema changes reach a branch only through its own bound factory (`scopedTableFactory`): a
|
|
1438
|
+
* declaration re-asserted against the branch's store. A branch's Table classes carry the base's
|
|
1439
|
+
* logical name, so the Table statics (`dropTable()`, `addAttributes()`) — which resolve the global
|
|
1440
|
+
* schema by that name and would act on the live base table — stay refused (`assertSchemaMutable`).
|
|
1441
|
+
*
|
|
1442
|
+
* A branch's blob roots are a hard-link clone of the base's, taken with the checkpoint, so a row
|
|
1443
|
+
* whose blob predates the branch reads back normally and the branch allocates new file ids in its own
|
|
1444
|
+
* directory (harper#644).
|
|
1445
|
+
*
|
|
1446
|
+
* A branch is the checkpoint's SST content plus its own transaction-log tail. This function opens
|
|
1447
|
+
* only the stores; replaying the tail is `openOrCreate`'s job (branchDatabase.ts), where the
|
|
1448
|
+
* cross-thread claim elects exactly one replayer and awaits it before any thread may open the
|
|
1449
|
+
* branch — the same recovery contract a base database gets at boot, without which a process that
|
|
1450
|
+
* died unflushed silently rewinds the branch to its last memtable flush (harper#643).
|
|
1451
|
+
*
|
|
1452
|
+
* Pass `blobRoots` to pin the handle to the roots the branch was published with; without it the
|
|
1453
|
+
* store resolves them from current configuration, which is only right for a branch being created.
|
|
1454
|
+
*/
|
|
1455
|
+
function openBranchDatabase(path, databaseName, storeName, blobRoots) {
|
|
1456
|
+
assertLegalBranchName(databaseName, 'logical database name');
|
|
1457
|
+
assertLegalBranchName(storeName, 'store identity');
|
|
1458
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
1459
|
+
throw new Error(`Cannot open branch database: no directory at ${path}`);
|
|
1460
|
+
// the guards compare against env-map keys, so two spellings of one directory must not read as two
|
|
1461
|
+
path = (0, node_fs_1.realpathSync)(path);
|
|
1462
|
+
// FIRST: the guards below read the registry, and loading is itself what populates
|
|
1463
|
+
// `rocksdbDatabaseEnvs`. Claiming the path ahead of this scan would make the scan skip it, which
|
|
1464
|
+
// also means a directory that IS a real database no longer reads as one — so the pre-open window
|
|
1465
|
+
// where the scan can adopt a branch directory stays open, by choice (harper#643).
|
|
1466
|
+
getDatabases();
|
|
1467
|
+
// a rival graph over one shared root store; the two callers would disagree about who may close it
|
|
1468
|
+
if (openBranches.has(path))
|
|
1469
|
+
throw new Error(`Branch database at ${path} is already open`);
|
|
1470
|
+
// a loaded database's store is closed by `closeLoadedDatabases`, so adopting it would mean this
|
|
1471
|
+
// handle's `close()` tears down a live database
|
|
1472
|
+
if (rocksdbDatabaseEnvs.has(path))
|
|
1473
|
+
throw new Error(`Cannot branch ${path}: it is already open as a database`);
|
|
1474
|
+
assertBranchIdentityAvailable(storeName);
|
|
1475
|
+
const tables = Object.create(null);
|
|
1476
|
+
// initStores opens a table's column families well before `setTable` publishes it into `tables`,
|
|
1477
|
+
// so the graph is not a complete record of what a failed open must release
|
|
1478
|
+
const openedStores = [];
|
|
1479
|
+
// The boot-time hydration pass has already run by the time a branch opens, so anything this open
|
|
1480
|
+
// queues would never be drained. It is handed to the caller instead, which is the only place that
|
|
1481
|
+
// knows the application's other branches and can therefore resolve targets without leaking to base.
|
|
1482
|
+
const queuedRelationshipsAt = relationshipsToHydrate.length;
|
|
1483
|
+
let rootStore;
|
|
1484
|
+
// claim the path before the open, not after: readRocksMetaDb registers the store in
|
|
1485
|
+
// `rocksdbDatabaseEnvs` partway through, so anything re-entering `database()` during initStores
|
|
1486
|
+
// would otherwise find the branch's store on an unowned path
|
|
1487
|
+
openBranches.set(path, undefined);
|
|
1488
|
+
retakeBranchIdentity(storeName);
|
|
1489
|
+
try {
|
|
1490
|
+
rootStore = readRocksMetaDb(path, null, databaseName, { destination: tables, storeName, openedStores });
|
|
1491
|
+
// Pin the handle to the roots the caller proved this branch was published with, before it is
|
|
1492
|
+
// handed out. A row's `storageIndex` is a position in that list, so resolving through current
|
|
1493
|
+
// configuration instead would let an appended volume take writes at an index the branch's own
|
|
1494
|
+
// completion marker never recorded -- and a later change at that index would then silently
|
|
1495
|
+
// re-address them. `closeBranchHandles` clears the entry with the rest of the handle.
|
|
1496
|
+
if (blobRoots)
|
|
1497
|
+
blob_ts_1.databasePaths.set(rootStore, blobRoots);
|
|
1498
|
+
}
|
|
1499
|
+
catch (error) {
|
|
1500
|
+
openBranches.delete(path);
|
|
1501
|
+
releaseBranchIdentity(storeName);
|
|
1502
|
+
const stranded = rocksdbDatabaseEnvs.get(path);
|
|
1503
|
+
rocksdbDatabaseEnvs.delete(path);
|
|
1504
|
+
closeBranchHandles(path, stranded, openedStores, tables);
|
|
1505
|
+
throw error;
|
|
1506
|
+
}
|
|
1507
|
+
let closed = false;
|
|
1508
|
+
const branch = {
|
|
1509
|
+
tables,
|
|
1510
|
+
rootStore,
|
|
1511
|
+
path,
|
|
1512
|
+
databaseName,
|
|
1513
|
+
storeName,
|
|
1514
|
+
openedStores,
|
|
1515
|
+
pendingRelationships: relationshipsToHydrate.splice(queuedRelationshipsAt),
|
|
1516
|
+
close() {
|
|
1517
|
+
// guard on the handle, not on the registrations: those are keyed by path, and a closed
|
|
1518
|
+
// branch frees its path, so a stale handle would otherwise tear down its successor
|
|
1519
|
+
if (closed)
|
|
1520
|
+
return;
|
|
1521
|
+
closed = true;
|
|
1522
|
+
openBranches.delete(path);
|
|
1523
|
+
releaseBranchIdentity(storeName);
|
|
1524
|
+
rocksdbDatabaseEnvs.delete(path);
|
|
1525
|
+
closeBranchHandles(path, rootStore, openedStores, tables);
|
|
1526
|
+
},
|
|
1527
|
+
};
|
|
1528
|
+
openBranches.set(path, branch);
|
|
1529
|
+
return branch;
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Release everything a branch open created. Each table's primary store and each index is its own
|
|
1533
|
+
* column family, on top of the internal-dbis and audit families, so closing the root alone leaves
|
|
1534
|
+
* all of them behind — which is why `closeDatabase` walks them individually for a real database.
|
|
1535
|
+
* Two process-global registrations outlive the stores as well, neither with a lifetime of its own:
|
|
1536
|
+
* a storage-reclamation handler per store path, whose closure pins the now-closed store, and the
|
|
1537
|
+
* memoized blob roots in `databasePaths`. A real database is opened once per thread; harper#643
|
|
1538
|
+
* makes branch open/close routine, so both would grow with branch churn.
|
|
1539
|
+
*/
|
|
1540
|
+
function closeBranchHandles(path, rootStore, openedStores = [], tables = {}) {
|
|
1541
|
+
const reclamationPaths = new Set([path]);
|
|
1542
|
+
rootStore?.auditStore?.stopAuditCleanup?.();
|
|
1543
|
+
const closeStore = (store, description) => {
|
|
1544
|
+
if (!store || store.status === 'closed')
|
|
1545
|
+
return;
|
|
1546
|
+
if (store.path)
|
|
1547
|
+
reclamationPaths.add(store.path);
|
|
1548
|
+
try {
|
|
1549
|
+
store.close?.();
|
|
1550
|
+
}
|
|
1551
|
+
catch (error) {
|
|
1552
|
+
logger.warn(`Error closing ${description} for branch database at ${path}`, error);
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1555
|
+
// the class, before its stores: an expiration timer or a reclamation handler on a closed store
|
|
1556
|
+
// would otherwise keep firing against it for the life of the process
|
|
1557
|
+
for (const tableName in tables) {
|
|
1558
|
+
try {
|
|
1559
|
+
tables[tableName]?.cleanup?.();
|
|
1560
|
+
}
|
|
1561
|
+
catch (error) {
|
|
1562
|
+
logger.warn(`Error releasing table ${tableName} of branch database at ${path}`, error);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
for (const store of openedStores)
|
|
1566
|
+
closeStore(store, 'column family');
|
|
1567
|
+
closeStore(rootStore?.dbisDb, 'attributes store');
|
|
1568
|
+
closeStore(rootStore?.auditStore, 'audit store');
|
|
1569
|
+
closeStore(rootStore, 'root store');
|
|
1570
|
+
if (rootStore)
|
|
1571
|
+
blob_ts_1.databasePaths.delete(rootStore);
|
|
1572
|
+
for (const reclamationPath of reclamationPaths)
|
|
1573
|
+
(0, storageReclamation_ts_1.removeStorageReclamation)(reclamationPath);
|
|
1574
|
+
}
|
|
1575
|
+
/** Branches are process-local, so this is shutdown, not a data operation. */
|
|
1576
|
+
function closeBranchDatabases() {
|
|
1577
|
+
for (const branch of [...openBranches.values()])
|
|
1578
|
+
branch?.close();
|
|
1579
|
+
}
|
|
956
1580
|
function resetDatabases() {
|
|
957
1581
|
loadedDatabases = false;
|
|
958
1582
|
for (const store of Object.values(lmdbDatabaseEnvs)) {
|
|
@@ -1063,6 +1687,14 @@ function database({ database: databaseName, table: tableName }) {
|
|
|
1063
1687
|
const useRocksdb = (process.env.HARPER_STORAGE_ENGINE || (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.STORAGE_ENGINE)) !== 'lmdb';
|
|
1064
1688
|
if (useRocksdb) {
|
|
1065
1689
|
const path = (0, path_1.join)(databasePath, tablePath ? tableName : databaseName);
|
|
1690
|
+
// the scan is not the only way to reach a branch's directory: a branch leaves its store in
|
|
1691
|
+
// `rocksdbDatabaseEnvs`, so without this an on-demand open would staple it onto
|
|
1692
|
+
// `definedDatabases` and the next `closeDatabase` would close it under the live handle
|
|
1693
|
+
if (isOpenBranchPath(path)) {
|
|
1694
|
+
const error = new Error(`Database '${databaseName}' is open as a scope-private branch`);
|
|
1695
|
+
error.statusCode = 409;
|
|
1696
|
+
throw error;
|
|
1697
|
+
}
|
|
1066
1698
|
rootStore = rocksdbDatabaseEnvs.get(path);
|
|
1067
1699
|
if (!rootStore || rootStore.status === 'closed') {
|
|
1068
1700
|
// this on-demand open (create_table/create_database and friends) must not resurrect a
|
|
@@ -1290,8 +1922,12 @@ function closeDatabase(databaseName) {
|
|
|
1290
1922
|
* database is closed). The `system` database is intentionally left open: it is non-enumerable here
|
|
1291
1923
|
* (skipped by the loop), is never restored online, and the exiting worker may still touch the job
|
|
1292
1924
|
* table during teardown. Best-effort: closing failures are swallowed inside `closeDatabase`.
|
|
1925
|
+
*
|
|
1926
|
+
* Branches are invisible to the loop below but hold handles from the same registry, so this — the
|
|
1927
|
+
* thread's one teardown entry point — closes them too.
|
|
1293
1928
|
*/
|
|
1294
1929
|
function closeLoadedDatabases() {
|
|
1930
|
+
closeBranchDatabases();
|
|
1295
1931
|
// snapshot the names first: closeDatabase() deletes from `databases` as it goes
|
|
1296
1932
|
for (const databaseName of Object.keys(exports.databases)) {
|
|
1297
1933
|
const dbTables = exports.databases[databaseName];
|
|
@@ -1394,24 +2030,38 @@ function openIndex(dbiKey, rootStore, attribute) {
|
|
|
1394
2030
|
cache: isCustomObjectIndex,
|
|
1395
2031
|
});
|
|
1396
2032
|
dbi.rootStore = rootStore;
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
2033
|
+
try {
|
|
2034
|
+
// Custom-index object stores (e.g. HNSW) write graph nodes via plain put() with no staged
|
|
2035
|
+
// transaction timestamp, so their values carry no version and the PrimaryRocksDatabase
|
|
2036
|
+
// Verification-Table cache can't track them. A versioned index initialises its encoder as a
|
|
2037
|
+
// versioned RocksDB store (isRocksDB → metadata-prefix encode/decode) and marks it
|
|
2038
|
+
// self-versioning, so each node gets a monotonic version the VT can extract — enabling cached,
|
|
2039
|
+
// decode-free graph traversal. The format is resolved from the persisted attribute descriptor
|
|
2040
|
+
// (decided once at create — see resolveIndexFormat) so every worker and reload agree on it.
|
|
2041
|
+
if (isCustomObjectIndex && resolveIndexFormat(dbiKey, rootStore, dbi, attribute) === 'versioned') {
|
|
2042
|
+
armVersionedIndexEncoder(dbi, rootStore);
|
|
2043
|
+
}
|
|
2044
|
+
installCustomIndex(dbi);
|
|
2045
|
+
}
|
|
2046
|
+
catch (error) {
|
|
2047
|
+
// the handle is not yet owned by any table, so nobody else can close it
|
|
2048
|
+
try {
|
|
2049
|
+
dbi.close();
|
|
2050
|
+
}
|
|
2051
|
+
catch { }
|
|
2052
|
+
throw error;
|
|
1406
2053
|
}
|
|
1407
2054
|
}
|
|
1408
2055
|
else {
|
|
1409
2056
|
dbi = rootStore.openDB(dbiKey, dbiInit);
|
|
2057
|
+
installCustomIndex(dbi);
|
|
1410
2058
|
}
|
|
1411
|
-
|
|
2059
|
+
function installCustomIndex(indexStore) {
|
|
2060
|
+
if (!attribute.indexed.type)
|
|
2061
|
+
return;
|
|
1412
2062
|
const CustomIndex = customIndexes_ts_1.CUSTOM_INDEXES[attribute.indexed.type];
|
|
1413
2063
|
if (CustomIndex) {
|
|
1414
|
-
|
|
2064
|
+
indexStore.customIndex = new CustomIndex(indexStore, attribute.indexed);
|
|
1415
2065
|
}
|
|
1416
2066
|
else {
|
|
1417
2067
|
logger.error(`The indexing type '${attribute.indexed.type}' is unknown`);
|
|
@@ -1434,7 +2084,83 @@ function openIndex(dbiKey, rootStore, attribute) {
|
|
|
1434
2084
|
* @param replicate
|
|
1435
2085
|
*/
|
|
1436
2086
|
function table(tableDefinition) {
|
|
1437
|
-
|
|
2087
|
+
return declareTable(GLOBAL_TARGET, tableDefinition);
|
|
2088
|
+
}
|
|
2089
|
+
const GLOBAL_TARGET = {
|
|
2090
|
+
rootStore: (databaseName, tableName) => database({ database: databaseName, table: tableName }),
|
|
2091
|
+
tables: (databaseName) => exports.databases[databaseName],
|
|
2092
|
+
reload: () => resetDatabases(),
|
|
2093
|
+
// a real database's stores live until the process (or `closeDatabase`, which walks the graph) ends
|
|
2094
|
+
adopt: () => { },
|
|
2095
|
+
};
|
|
2096
|
+
/**
|
|
2097
|
+
* The factory a branched application declares tables through: each declaration goes to the branch
|
|
2098
|
+
* of the database it names, or to `table()` itself for a database the application did not branch.
|
|
2099
|
+
* An unbranched application gets `table` by identity -- no wrapper, no per-call routing.
|
|
2100
|
+
*/
|
|
2101
|
+
function scopedTableFactory(branches) {
|
|
2102
|
+
if (!branches?.size)
|
|
2103
|
+
return table;
|
|
2104
|
+
return function scopedTable(tableDefinition) {
|
|
2105
|
+
// `||`, not `??`: `table()` resolves every falsy name to the default database
|
|
2106
|
+
const branch = branches.get(tableDefinition.database || DEFAULT_DATABASE_NAME);
|
|
2107
|
+
return branch ? declareTable(branchTarget(branch), tableDefinition) : table(tableDefinition);
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
function branchTarget(branch) {
|
|
2111
|
+
return {
|
|
2112
|
+
rootStore: () => branch.rootStore,
|
|
2113
|
+
tables: () => branch.tables,
|
|
2114
|
+
reload: () => reloadBranch(branch),
|
|
2115
|
+
adopt: (store) => branch.openedStores.push(store),
|
|
2116
|
+
branch,
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
/**
|
|
2120
|
+
* Re-read a branch's catalog into its `tables`: tables and indexes another thread declared since the
|
|
2121
|
+
* open (or since the last reload) are opened here, the same way a schema-change rescan does for a
|
|
2122
|
+
* real database. Existing classes are kept and their attribute lists refreshed.
|
|
2123
|
+
*/
|
|
2124
|
+
function reloadBranch(branch) {
|
|
2125
|
+
const { rootStore, tables, databaseName, storeName, openedStores } = branch;
|
|
2126
|
+
const queuedRelationshipsAt = relationshipsToHydrate.length;
|
|
2127
|
+
try {
|
|
2128
|
+
initStores(rootStore.path, rootStore, databaseName, { destination: tables, storeName, openedStores });
|
|
2129
|
+
}
|
|
2130
|
+
finally {
|
|
2131
|
+
for (const hydration of relationshipsToHydrate.splice(queuedRelationshipsAt))
|
|
2132
|
+
queueBranchHydration(branch, hydration);
|
|
2133
|
+
}
|
|
2134
|
+
// Until `prepareBranches` has the whole set, a cross-database target cannot be resolved without
|
|
2135
|
+
// falling through to the base; it hydrates the complete set once. After that, every sibling is
|
|
2136
|
+
// re-hydrated: the table this reload brought in may be the target a sibling's relationship waited for.
|
|
2137
|
+
if (!branch.relatedBranches)
|
|
2138
|
+
return;
|
|
2139
|
+
for (const sibling of branch.relatedBranches.values())
|
|
2140
|
+
hydrateBranchRelationships(sibling, branch.relatedBranches);
|
|
2141
|
+
}
|
|
2142
|
+
/** One pending hydration per table: a re-declaration replaces the entry the earlier declaration queued. */
|
|
2143
|
+
function queueBranchHydration(branch, hydration) {
|
|
2144
|
+
const existing = branch.pendingRelationships.findIndex((pending) => pending.databaseName === hydration.databaseName && pending.tableName === hydration.tableName);
|
|
2145
|
+
if (existing >= 0)
|
|
2146
|
+
branch.pendingRelationships[existing] = hydration;
|
|
2147
|
+
else
|
|
2148
|
+
branch.pendingRelationships.push(hydration);
|
|
2149
|
+
}
|
|
2150
|
+
/**
|
|
2151
|
+
* The receiving side of a branch's schema-change signal: a thread that holds this branch open reloads
|
|
2152
|
+
* it, any other thread has nothing to do. Returns the branch's tables so the caller can address the
|
|
2153
|
+
* table the signal named.
|
|
2154
|
+
*/
|
|
2155
|
+
function reloadBranchAt(path) {
|
|
2156
|
+
const branch = openBranches.get(path);
|
|
2157
|
+
if (!branch)
|
|
2158
|
+
return undefined;
|
|
2159
|
+
reloadBranch(branch);
|
|
2160
|
+
return branch.tables;
|
|
2161
|
+
}
|
|
2162
|
+
function declareTable(target, tableDefinition) {
|
|
2163
|
+
let { table: tableName, database: databaseName, expiration, eviction, scanInterval, attributes, audit, sealed, splitSegments, replicate, randomAccessFields, trackDeletes, schemaDefined, schemaRelationshipsDefined, origin, description, properties, hidden, cacheControl, } = tableDefinition;
|
|
1438
2164
|
if (!databaseName)
|
|
1439
2165
|
databaseName = DEFAULT_DATABASE_NAME;
|
|
1440
2166
|
// Reject reserved names here too, not only at the operations API: a database
|
|
@@ -1447,8 +2173,14 @@ function table(tableDefinition) {
|
|
|
1447
2173
|
if (hdbTerms_ts_1.RESERVED_DATABASE_NAMES.includes(databaseName)) {
|
|
1448
2174
|
throw new hdbError_ts_1.ClientError(`'${databaseName}' is a reserved name and cannot be used as a database name`);
|
|
1449
2175
|
}
|
|
1450
|
-
|
|
1451
|
-
|
|
2176
|
+
// A branch resolves its blob root from its store identity, so a database created under that same
|
|
2177
|
+
// name would share the root: two allocators minting the same file paths and truncating each other,
|
|
2178
|
+
// and the branch's teardown removing the database's blobs.
|
|
2179
|
+
if (isBranchIdentity(databaseName)) {
|
|
2180
|
+
throw new hdbError_ts_1.ClientError(`'${databaseName}' is in use as a branch store identity and cannot be a database name`);
|
|
2181
|
+
}
|
|
2182
|
+
const rootStore = target.rootStore(databaseName, tableName);
|
|
2183
|
+
const tables = target.tables(databaseName);
|
|
1452
2184
|
logger.trace(`Defining ${tableName} in ${databaseName}`);
|
|
1453
2185
|
let Table = tables?.[tableName];
|
|
1454
2186
|
if (rootStore.status === 'closed') {
|
|
@@ -1463,6 +2195,7 @@ function table(tableDefinition) {
|
|
|
1463
2195
|
const schemaDefinedExplicit = tableDefinition.schemaDefined !== undefined;
|
|
1464
2196
|
if (schemaDefined == undefined)
|
|
1465
2197
|
schemaDefined = true;
|
|
2198
|
+
const relationshipDefinitions = schemaRelationshipsDefined ? normalizeRelationships(attributes) : undefined;
|
|
1466
2199
|
const internalDbiInit = createOpenDBIObject(false);
|
|
1467
2200
|
for (const attribute of attributes) {
|
|
1468
2201
|
if (attribute.attribute && !attribute.name) {
|
|
@@ -1476,124 +2209,161 @@ function table(tableDefinition) {
|
|
|
1476
2209
|
attribute.indexed = true;
|
|
1477
2210
|
}
|
|
1478
2211
|
let hasChanges;
|
|
2212
|
+
let refreshRelationshipAttributes = false;
|
|
2213
|
+
let deferredPrimaryRow;
|
|
2214
|
+
let unpublishedPrimaryStore;
|
|
2215
|
+
let published = false;
|
|
1479
2216
|
let releaseExclusiveLock;
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
// leave describe reporting the new attribute while every record — old and newly inserted — stays
|
|
1488
|
-
// keyed by the original one; search_by_id/update/delete by the declared key then all miss. Only
|
|
1489
|
-
// schema-authored callers (@table / defineTable / create_table) reassert the declaration, so
|
|
1490
|
-
// gate on schemaDefinedExplicit to leave cluster schema-replication / data-loader callers alone.
|
|
1491
|
-
// See HarperFast/studio#1199.
|
|
1492
|
-
const declaredPrimaryKey = attributes.find((attribute) => attribute.isPrimaryKey)?.name;
|
|
1493
|
-
if (schemaDefinedExplicit && declaredPrimaryKey && declaredPrimaryKey !== Table.primaryKey) {
|
|
1494
|
-
let hasRecords = false;
|
|
1495
|
-
for (const _entry of Table.primaryStore.getRange({ start: true })) {
|
|
1496
|
-
hasRecords = true;
|
|
1497
|
-
break;
|
|
2217
|
+
const attributesToIndex = [];
|
|
2218
|
+
const indicesToRemove = [];
|
|
2219
|
+
try {
|
|
2220
|
+
if (Table) {
|
|
2221
|
+
primaryKey = Table.primaryKey;
|
|
2222
|
+
if (Table.primaryStore.rootStore.status === 'closed') {
|
|
2223
|
+
throw new Error(`Can not use a closed data store from ${tableName} class`);
|
|
1498
2224
|
}
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
if (!primaryKeyAttribute.origins)
|
|
1564
|
-
primaryKeyAttribute.origins = [origin];
|
|
1565
|
-
else if (!primaryKeyAttribute.origins.includes(origin))
|
|
1566
|
-
primaryKeyAttribute.origins.push(origin);
|
|
1567
|
-
}
|
|
1568
|
-
logger.trace(`${tableName} table loading, opening primary store`);
|
|
1569
|
-
const dbiInit = createOpenDBIObject(false, true);
|
|
1570
|
-
dbiInit.compression = primaryKeyAttribute.compression;
|
|
1571
|
-
// per-table override of the storage.randomAccessFields default (see OpenDBIObject)
|
|
1572
|
-
if (typeof primaryKeyAttribute.randomAccessFields === 'boolean')
|
|
1573
|
-
dbiInit.randomAccessStructure = primaryKeyAttribute.randomAccessFields;
|
|
1574
|
-
const dbiName = tableName + '/';
|
|
1575
|
-
if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
|
|
1576
|
-
attributesDbi = rootStore.dbisDb = openRocksDatabase(rootStore.path, {
|
|
1577
|
-
...internalDbiInit,
|
|
1578
|
-
disableWAL: false,
|
|
1579
|
-
name: terms_ts_1.INTERNAL_DBIS_NAME,
|
|
1580
|
-
});
|
|
2225
|
+
// Reject moving the primary key to a different attribute on a table that already has records.
|
|
2226
|
+
// The storage key (Table.primaryKey) is never re-pointed here, so honoring the change would
|
|
2227
|
+
// leave describe reporting the new attribute while every record — old and newly inserted — stays
|
|
2228
|
+
// keyed by the original one; search_by_id/update/delete by the declared key then all miss. Only
|
|
2229
|
+
// schema-authored callers (@table / defineTable / create_table) reassert the declaration, so
|
|
2230
|
+
// gate on schemaDefinedExplicit to leave cluster schema-replication / data-loader callers alone.
|
|
2231
|
+
// See HarperFast/studio#1199.
|
|
2232
|
+
const declaredPrimaryKey = attributes.find((attribute) => attribute.isPrimaryKey)?.name;
|
|
2233
|
+
if (schemaDefinedExplicit && declaredPrimaryKey && declaredPrimaryKey !== Table.primaryKey) {
|
|
2234
|
+
let hasRecords = false;
|
|
2235
|
+
for (const _entry of Table.primaryStore.getRange({ start: true })) {
|
|
2236
|
+
hasRecords = true;
|
|
2237
|
+
break;
|
|
2238
|
+
}
|
|
2239
|
+
if (hasRecords) {
|
|
2240
|
+
throw new hdbError_ts_1.ClientError(`Cannot change the primary key of table '${databaseName}.${tableName}' from '${Table.primaryKey}' to ` +
|
|
2241
|
+
`'${declaredPrimaryKey}' because it already contains records. Recreate the table with the new primary ` +
|
|
2242
|
+
`key, or migrate the existing records.`, 400);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
// Acquire before the first mutation of the live Table below, so a lost race leaves no
|
|
2246
|
+
// attributes this worker describes but never persisted. Only the RocksDB acquire is bounded
|
|
2247
|
+
// and can throw, and only it is cheap when uncontended: LMDB's exclusiveLock() opens an
|
|
2248
|
+
// environment-wide write transaction that cannot time out, so it stays lazy.
|
|
2249
|
+
if (rootStore instanceof rocksdb_js_1.RocksDatabase)
|
|
2250
|
+
exclusiveLock();
|
|
2251
|
+
// it table already exists, get the split segments setting
|
|
2252
|
+
if (splitSegments == undefined)
|
|
2253
|
+
splitSegments = Table.splitSegments;
|
|
2254
|
+
if (origin === 'cluster') {
|
|
2255
|
+
const merged = Table.attributes.slice();
|
|
2256
|
+
for (const attribute of attributes) {
|
|
2257
|
+
const existing = merged.find((existingAttribute) => existingAttribute.name === attribute.name);
|
|
2258
|
+
if (!existing) {
|
|
2259
|
+
merged.push(attribute);
|
|
2260
|
+
continue;
|
|
2261
|
+
}
|
|
2262
|
+
// Nodes that apply the same peer definitions in a different order keep different index sets, and
|
|
2263
|
+
// this warn is the only signal of it. An absent field and an explicit falsy one declare the same
|
|
2264
|
+
// thing, so neither direction of that pair is a difference.
|
|
2265
|
+
const discarded = PEER_DECLARABLE_FIELDS.filter((field) => (attribute[field] || existing[field]) &&
|
|
2266
|
+
JSON.stringify(attribute[field]) !== JSON.stringify(existing[field]));
|
|
2267
|
+
if (discarded.length > 0)
|
|
2268
|
+
logger.warn(`Ignoring peer redefinition of ${databaseName}.${tableName}.${attribute.name} (${discarded
|
|
2269
|
+
.map((field) => `${field}: local ${JSON.stringify(existing[field])}, peer ${JSON.stringify(attribute[field])}`)
|
|
2270
|
+
.join('; ')}); the local schema is authoritative`);
|
|
2271
|
+
}
|
|
2272
|
+
attributes = merged;
|
|
2273
|
+
}
|
|
2274
|
+
Table.attributes.splice(0, Table.attributes.length, ...attributes);
|
|
2275
|
+
// Re-assert from the live declaration so a stale value on disk (replicated event,
|
|
2276
|
+
// v4-era backfill) is corrected on every reload. Gated on `schemaDefinedExplicit` so
|
|
2277
|
+
// callers that omit the flag (cluster schema-replication, data loader) don't flip a
|
|
2278
|
+
// dynamic table to true via the default at the top of table(), and on origin so a
|
|
2279
|
+
// peer-derived definition never overrides the local declaration.
|
|
2280
|
+
if (schemaDefinedExplicit && origin !== 'cluster')
|
|
2281
|
+
Table.schemaDefined = schemaDefined;
|
|
2282
|
+
// Refresh class-level schema metadata to track docstring/directive changes across reloads.
|
|
2283
|
+
Table.description = description;
|
|
2284
|
+
Table.properties = properties;
|
|
2285
|
+
Table.hidden = hidden;
|
|
2286
|
+
// undefined means a non-schema caller (add_attribute, cluster schema events) — don't clobber
|
|
2287
|
+
if (cacheControl !== undefined)
|
|
2288
|
+
Table.cacheControl = cacheControl;
|
|
1581
2289
|
}
|
|
1582
2290
|
else {
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
//
|
|
1590
|
-
if (
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
2291
|
+
const auditStore = rootStore.auditStore;
|
|
2292
|
+
primaryKeyAttribute = attributes.find((attribute) => attribute.isPrimaryKey) || {};
|
|
2293
|
+
primaryKey = primaryKeyAttribute.name;
|
|
2294
|
+
primaryKeyAttribute.isPrimaryKey = true;
|
|
2295
|
+
primaryKeyAttribute.is_hash_attribute = true; // backward-compat: harperdb@4.x reads this field to open the DBI with correct flags
|
|
2296
|
+
primaryKeyAttribute.schemaDefined = schemaDefined;
|
|
2297
|
+
// Old readers treat every attribute row as live schema, so relationships stay on the ignored primary descriptor.
|
|
2298
|
+
if (relationshipDefinitions)
|
|
2299
|
+
primaryKeyAttribute.relationships = relationshipDefinitions;
|
|
2300
|
+
// can't change compression after the fact (except threshold), so save only when we create the table
|
|
2301
|
+
primaryKeyAttribute.compression = getDefaultCompression();
|
|
2302
|
+
if (trackDeletes)
|
|
2303
|
+
primaryKeyAttribute.trackDeletes = true;
|
|
2304
|
+
audit = primaryKeyAttribute.audit = typeof audit === 'boolean' ? audit : (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.LOGGING_AUDITLOG);
|
|
2305
|
+
if (expiration)
|
|
2306
|
+
primaryKeyAttribute.expiration = expiration;
|
|
2307
|
+
if (eviction)
|
|
2308
|
+
primaryKeyAttribute.eviction = eviction;
|
|
2309
|
+
// persist cacheControl so all threads (and future boots) see it; undefined callers inherit
|
|
2310
|
+
// a descriptor value carried by cluster schema events; null (schema has no directive)
|
|
2311
|
+
// clears a stale value the carried descriptor may hold
|
|
2312
|
+
if (cacheControl === undefined)
|
|
2313
|
+
cacheControl = primaryKeyAttribute.cacheControl;
|
|
2314
|
+
else if (cacheControl === null)
|
|
2315
|
+
delete primaryKeyAttribute.cacheControl;
|
|
2316
|
+
else
|
|
2317
|
+
primaryKeyAttribute.cacheControl = cacheControl;
|
|
2318
|
+
splitSegments ??= false;
|
|
2319
|
+
primaryKeyAttribute.splitSegments = splitSegments; // always default to not splitting segments going forward
|
|
2320
|
+
if (typeof sealed === 'boolean')
|
|
2321
|
+
primaryKeyAttribute.sealed = sealed;
|
|
2322
|
+
if (typeof replicate === 'boolean')
|
|
2323
|
+
primaryKeyAttribute.replicate = replicate;
|
|
2324
|
+
// An explicit directive PINS this table's encoding: we persist the boolean, so later changes
|
|
2325
|
+
// to the global storage.randomAccessFields default never affect this table. Tables WITHOUT the
|
|
2326
|
+
// directive are intentionally not persisted here — they follow the current global default on
|
|
2327
|
+
// each open (a runtime lever to flip encoding fleet-wide). Switching either way is safe: the
|
|
2328
|
+
// struct READ hook always stays on and struct (0x20-0x3f) vs classic-record (0x40-0x7f) bytes
|
|
2329
|
+
// are disjoint, so already-written records still decode; only the encoding of NEW writes changes.
|
|
2330
|
+
if (typeof randomAccessFields === 'boolean')
|
|
2331
|
+
primaryKeyAttribute.randomAccessFields = randomAccessFields;
|
|
2332
|
+
if (origin) {
|
|
2333
|
+
if (!primaryKeyAttribute.origins)
|
|
2334
|
+
primaryKeyAttribute.origins = [origin];
|
|
2335
|
+
else if (!primaryKeyAttribute.origins.includes(origin))
|
|
2336
|
+
primaryKeyAttribute.origins.push(origin);
|
|
2337
|
+
}
|
|
2338
|
+
logger.trace(`${tableName} table loading, opening primary store`);
|
|
2339
|
+
const dbiInit = createOpenDBIObject(false, true);
|
|
2340
|
+
dbiInit.compression = primaryKeyAttribute.compression;
|
|
2341
|
+
// per-table override of the storage.randomAccessFields default (see OpenDBIObject)
|
|
2342
|
+
if (typeof primaryKeyAttribute.randomAccessFields === 'boolean')
|
|
2343
|
+
dbiInit.randomAccessStructure = primaryKeyAttribute.randomAccessFields;
|
|
2344
|
+
const dbiName = tableName + '/';
|
|
2345
|
+
if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
|
|
2346
|
+
attributesDbi = rootStore.dbisDb = openRocksDatabase(rootStore.path, {
|
|
2347
|
+
...internalDbiInit,
|
|
2348
|
+
disableWAL: false,
|
|
2349
|
+
name: terms_ts_1.INTERNAL_DBIS_NAME,
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
else {
|
|
2353
|
+
attributesDbi = rootStore.dbisDb = rootStore.openDB(terms_ts_1.INTERNAL_DBIS_NAME, internalDbiInit);
|
|
2354
|
+
}
|
|
2355
|
+
target.adopt(attributesDbi);
|
|
2356
|
+
markInternalDbiNonVersioned(attributesDbi);
|
|
2357
|
+
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)
|
|
2358
|
+
const existingTableMeta = attributesDbi.getSync(dbiName);
|
|
2359
|
+
if (existingTableMeta && !existingTableMeta.dropping) {
|
|
2360
|
+
// table was created while we were setting up; the lock is not reentrant, so release
|
|
2361
|
+
// before the recursive reload
|
|
2362
|
+
releaseLock();
|
|
2363
|
+
target.reload(databaseName);
|
|
2364
|
+
return declareTable(target, tableDefinition);
|
|
2365
|
+
}
|
|
2366
|
+
let primaryStore;
|
|
1597
2367
|
if (existingTableMeta?.dropping) {
|
|
1598
2368
|
// A previous drop of this table was interrupted after its tombstone
|
|
1599
2369
|
// was written. Complete it now (under the exclusive lock) so the
|
|
@@ -1621,15 +2391,20 @@ function table(tableDefinition) {
|
|
|
1621
2391
|
else {
|
|
1622
2392
|
primaryStore = rootStore.openDB(dbiName, dbiInit);
|
|
1623
2393
|
}
|
|
2394
|
+
target.adopt(primaryStore);
|
|
2395
|
+
unpublishedPrimaryStore = primaryStore;
|
|
1624
2396
|
primaryStore = (0, RecordEncoder_ts_1.handleLocalTimeForGets)(primaryStore, rootStore);
|
|
1625
|
-
|
|
2397
|
+
// only a store no table has loaded yet is unnamed; a branch's store carries its own store
|
|
2398
|
+
// identity here, which its blob roots resolve from, and must not take the logical name
|
|
2399
|
+
rootStore.databaseName ??= databaseName;
|
|
1626
2400
|
primaryStore.tableId = attributesDbi.getSync(NEXT_TABLE_ID);
|
|
1627
2401
|
logger.trace(`Assigning new table id ${primaryStore.tableId} for ${tableName}`);
|
|
1628
2402
|
if (!primaryStore.tableId)
|
|
1629
2403
|
primaryStore.tableId = 1;
|
|
1630
2404
|
attributesDbi.put(NEXT_TABLE_ID, primaryStore.tableId + 1);
|
|
1631
2405
|
primaryKeyAttribute.tableId = primaryStore.tableId;
|
|
1632
|
-
Table =
|
|
2406
|
+
Table = (0, Table_ts_1.makeTable)({
|
|
2407
|
+
isBranch: Boolean(target.branch),
|
|
1633
2408
|
primaryStore,
|
|
1634
2409
|
auditStore,
|
|
1635
2410
|
audit,
|
|
@@ -1652,96 +2427,99 @@ function table(tableDefinition) {
|
|
|
1652
2427
|
properties,
|
|
1653
2428
|
hidden,
|
|
1654
2429
|
cacheControl,
|
|
1655
|
-
})
|
|
2430
|
+
});
|
|
1656
2431
|
Table.schemaVersion = 1;
|
|
1657
2432
|
hasChanges = true;
|
|
1658
|
-
|
|
1659
|
-
}
|
|
1660
|
-
catch (error) {
|
|
1661
|
-
// A failure while opening/creating the column family or writing the
|
|
1662
|
-
// table id / catalog entry (e.g. into an env poisoned by a prior
|
|
1663
|
-
// dangling column family) must NOT leak the exclusive
|
|
1664
|
-
// 'update-attributes' spin lock. If it leaks, every subsequent
|
|
1665
|
-
// create_table / attribute update on this database spins forever
|
|
1666
|
-
// (a hard wedge that pins a worker at 100% CPU). Release before rethrow.
|
|
1667
|
-
if (releaseExclusiveLock)
|
|
1668
|
-
releaseExclusiveLock();
|
|
1669
|
-
throw error;
|
|
2433
|
+
deferredPrimaryRow = primaryKeyAttribute;
|
|
1670
2434
|
}
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
2435
|
+
const indices = Table.indices;
|
|
2436
|
+
if (!attributesDbi) {
|
|
2437
|
+
if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
|
|
2438
|
+
rootStore.dbisDb = openRocksDatabase(rootStore.path, {
|
|
2439
|
+
...internalDbiInit,
|
|
2440
|
+
disableWAL: false,
|
|
2441
|
+
name: terms_ts_1.INTERNAL_DBIS_NAME,
|
|
2442
|
+
});
|
|
2443
|
+
}
|
|
2444
|
+
else {
|
|
2445
|
+
rootStore.dbisDb = rootStore.openDB(terms_ts_1.INTERNAL_DBIS_NAME, internalDbiInit);
|
|
2446
|
+
}
|
|
2447
|
+
target.adopt(rootStore.dbisDb);
|
|
2448
|
+
attributesDbi = markInternalDbiNonVersioned(rootStore.dbisDb);
|
|
2449
|
+
}
|
|
2450
|
+
Table.dbisDB = attributesDbi;
|
|
2451
|
+
// A cluster-origin list can miss a descriptor another thread committed moments ago, so removal
|
|
2452
|
+
// reconciliation is reserved for local schema authoring; on a create the rows can only be aborted state.
|
|
2453
|
+
const reconcileRemovals = origin !== 'cluster' || Boolean(deferredPrimaryRow);
|
|
2454
|
+
for (const { key, value } of reconcileRemovals
|
|
2455
|
+
? attributesDbi.getRange({ start: tableName + '/', end: tableName + '0' })
|
|
2456
|
+
: []) {
|
|
2457
|
+
if (value == null)
|
|
2458
|
+
continue;
|
|
2459
|
+
let [attributeTableName, attribute_name] = key.toString().split('/');
|
|
2460
|
+
if (attribute_name === '')
|
|
2461
|
+
attribute_name = value.name; // primary key
|
|
2462
|
+
if (attribute_name) {
|
|
2463
|
+
if (attributeTableName !== tableName)
|
|
2464
|
+
continue;
|
|
2465
|
+
}
|
|
2466
|
+
else {
|
|
2467
|
+
// table attribute for a table with no primary key, we don't want to remove this, so continue on
|
|
1696
2468
|
continue;
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
indicesToRemove.push(indexDbi);
|
|
2469
|
+
}
|
|
2470
|
+
const attribute = attributes.find((attribute) => attribute.name === attribute_name);
|
|
2471
|
+
const removeIndex = !attribute?.indexed && value.indexed && !value.isPrimaryKey;
|
|
2472
|
+
// rows already present under a create are aborted state
|
|
2473
|
+
const staleRow = !attribute || Boolean(deferredPrimaryRow);
|
|
2474
|
+
if (staleRow || removeIndex) {
|
|
2475
|
+
exclusiveLock();
|
|
2476
|
+
hasChanges = true;
|
|
2477
|
+
if (staleRow)
|
|
2478
|
+
attributesDbi.remove(key);
|
|
2479
|
+
if (removeIndex) {
|
|
2480
|
+
const indexDbi = Table.indices[attributeTableName];
|
|
2481
|
+
if (indexDbi)
|
|
2482
|
+
indicesToRemove.push(indexDbi);
|
|
2483
|
+
}
|
|
1713
2484
|
}
|
|
1714
2485
|
}
|
|
1715
|
-
}
|
|
1716
|
-
const attributesToIndex = [];
|
|
1717
|
-
try {
|
|
1718
2486
|
// TODO: If we have attributes and the schemaDefined flag is not set, turn it on
|
|
1719
2487
|
// iterate through the attributes to ensure that we have all the dbis created and indexed
|
|
1720
2488
|
for (const attribute of attributes || []) {
|
|
1721
|
-
if (attribute.relationship
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
continue;
|
|
2489
|
+
if (attribute.relationship) {
|
|
2490
|
+
refreshRelationshipAttributes = true;
|
|
2491
|
+
continue;
|
|
1725
2492
|
}
|
|
2493
|
+
if (attribute.computed)
|
|
2494
|
+
hasChanges = true;
|
|
1726
2495
|
let dbiKey = tableName + '/' + (attribute.name || '');
|
|
1727
2496
|
Object.defineProperty(attribute, 'key', { value: dbiKey, configurable: true });
|
|
1728
2497
|
let attributeDescriptor = attributesDbi.getSync(dbiKey);
|
|
1729
2498
|
if (attribute.isPrimaryKey) {
|
|
2499
|
+
if (deferredPrimaryRow)
|
|
2500
|
+
continue;
|
|
1730
2501
|
attributeDescriptor = attributeDescriptor || attributesDbi.getSync((dbiKey = tableName + '/')) || {};
|
|
1731
2502
|
// Persist schemaDefined when the explicit live value disagrees with disk. Without this,
|
|
1732
2503
|
// a stale `false` (from a v4-era write or replicated event) survives every reload: the
|
|
1733
2504
|
// in-memory re-assert in the existing-Table branch only fixes the worker that ran @table,
|
|
1734
|
-
// but other workers' next disk-load re-reads the stale value.
|
|
2505
|
+
// but other workers' next disk-load re-reads the stale value. The whole settings update is
|
|
2506
|
+
// gated off for cluster-origin callers: their values come from this worker's (possibly
|
|
2507
|
+
// stale) snapshot, so a rewrite could revert a newer local declaration already on disk.
|
|
1735
2508
|
const schemaDefinedMismatch = schemaDefinedExplicit && attributeDescriptor.schemaDefined !== schemaDefined;
|
|
1736
2509
|
// primary key can't change indexing, but settings can change
|
|
1737
|
-
if (
|
|
1738
|
-
(
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
2510
|
+
if (origin !== 'cluster' &&
|
|
2511
|
+
(schemaDefinedMismatch ||
|
|
2512
|
+
(audit !== undefined && audit !== Table.audit) ||
|
|
2513
|
+
(sealed !== undefined && sealed !== Table.sealed) ||
|
|
2514
|
+
(replicate !== undefined && replicate !== Table.replicate) ||
|
|
2515
|
+
(+expiration || undefined) !== (+attributeDescriptor.expiration || undefined) ||
|
|
2516
|
+
(+eviction || undefined) !== (+attributeDescriptor.eviction || undefined) ||
|
|
2517
|
+
attribute.type !== attributeDescriptor.type)) {
|
|
2518
|
+
exclusiveLock();
|
|
2519
|
+
const currentPrimaryAttribute = attributesDbi.getSync(dbiKey);
|
|
2520
|
+
if (!currentPrimaryAttribute || tableIsDropping(currentPrimaryAttribute, dbiKey))
|
|
2521
|
+
continue;
|
|
2522
|
+
const updatedPrimaryAttribute = { ...currentPrimaryAttribute };
|
|
1745
2523
|
if (typeof audit === 'boolean') {
|
|
1746
2524
|
if (audit)
|
|
1747
2525
|
Table.enableAuditing();
|
|
@@ -1760,14 +2538,52 @@ function table(tableDefinition) {
|
|
|
1760
2538
|
if (schemaDefinedMismatch)
|
|
1761
2539
|
updatedPrimaryAttribute.schemaDefined = schemaDefined;
|
|
1762
2540
|
hasChanges = true; // send out notification of the change
|
|
1763
|
-
exclusiveLock();
|
|
1764
2541
|
attributesDbi.put(dbiKey, updatedPrimaryAttribute);
|
|
1765
2542
|
}
|
|
1766
2543
|
continue;
|
|
1767
2544
|
}
|
|
1768
|
-
// note that non-indexed attributes do not need a dbi
|
|
1769
2545
|
if (attributeDescriptor?.attribute && !attributeDescriptor.name)
|
|
1770
2546
|
attributeDescriptor.indexed = true; // legacy descriptor
|
|
2547
|
+
if (origin === 'cluster' && attributeDescriptor) {
|
|
2548
|
+
// An existing descriptor is a local declaration this caller may not have seen yet, so it wins
|
|
2549
|
+
// over the incoming definition and is never written back from it.
|
|
2550
|
+
applyDurableDeclaration(attribute, attributeDescriptor);
|
|
2551
|
+
const abandonedIndexBuild = attribute.indexed &&
|
|
2552
|
+
(attributeDescriptor.indexingFailed ||
|
|
2553
|
+
(attributeDescriptor.indexingPID && attributeDescriptor.indexingPID !== process.pid) ||
|
|
2554
|
+
attributeDescriptor.restartNumber < (worker_threads_1.workerData?.restartNumber ?? manageThreads.restartNumber));
|
|
2555
|
+
if (abandonedIndexBuild) {
|
|
2556
|
+
// Recovery is the exception to skipping the handling below, because without it `isIndexing`
|
|
2557
|
+
// stays pinned on with nothing left to clear it and every query on the attribute fails with
|
|
2558
|
+
// IndexRebuildingError for the life of the worker. It persists the attribute (here and again
|
|
2559
|
+
// from runIndexing), so restate the declaration from a descriptor read under the lock.
|
|
2560
|
+
exclusiveLock();
|
|
2561
|
+
applyDurableDeclaration(attribute, attributesDbi.getSync(dbiKey) ?? attributeDescriptor);
|
|
2562
|
+
}
|
|
2563
|
+
else {
|
|
2564
|
+
if (attribute.indexed) {
|
|
2565
|
+
const dbi = openIndex(dbiKey, rootStore, attribute);
|
|
2566
|
+
target.adopt(dbi);
|
|
2567
|
+
// Persisting the indexFormat openIndex just resolved adds a field the descriptor lacks
|
|
2568
|
+
// rather than rewriting one it has. Without it an empty index resolves 'versioned', writes
|
|
2569
|
+
// versioned nodes, then re-derives 'legacy' on the next load — see indexFormatNeedsPersist.
|
|
2570
|
+
if (attribute.indexFormat != null && attributeDescriptor.indexFormat == null) {
|
|
2571
|
+
exclusiveLock();
|
|
2572
|
+
const durableDescriptor = attributesDbi.getSync(dbiKey);
|
|
2573
|
+
if (durableDescriptor && durableDescriptor.indexFormat == null) {
|
|
2574
|
+
hasChanges = true;
|
|
2575
|
+
attributesDbi.put(dbiKey, { ...durableDescriptor, indexFormat: attribute.indexFormat });
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
if (attributeDescriptor.indexingPID)
|
|
2579
|
+
dbi.isIndexing = true;
|
|
2580
|
+
dbi.indexNulls = attribute.indexNulls;
|
|
2581
|
+
indices[attribute.name] = dbi;
|
|
2582
|
+
}
|
|
2583
|
+
continue;
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
// note that non-indexed attributes do not need a dbi
|
|
1771
2587
|
// Some index options affect only search, not the stored structure (e.g. HNSW's
|
|
1772
2588
|
// efConstructionSearch). Changing those should persist the new metadata but NOT trigger a
|
|
1773
2589
|
// reindex. A custom index declares such keys via a static `searchOnlyOptions`.
|
|
@@ -1810,6 +2626,9 @@ function table(tableDefinition) {
|
|
|
1810
2626
|
// on the main thread, where workerData is undefined (and it is initialized to 1).
|
|
1811
2627
|
const currentRestartGeneration = worker_threads_1.workerData?.restartNumber ?? manageThreads.restartNumber;
|
|
1812
2628
|
const dbi = openIndex(dbiKey, rootStore, attribute);
|
|
2629
|
+
target.adopt(dbi);
|
|
2630
|
+
if (deferredPrimaryRow)
|
|
2631
|
+
indices[attribute.name] = dbi; // private until published; lets the rollback close it
|
|
1813
2632
|
// openIndex resolves and stamps attribute.indexFormat for a versioned-capable (RocksDB
|
|
1814
2633
|
// custom-object) index. An index created before this field existed has no indexFormat on
|
|
1815
2634
|
// disk; persist the resolved value now — even when nothing else changed — so the format is
|
|
@@ -1821,13 +2640,15 @@ function table(tableDefinition) {
|
|
|
1821
2640
|
if (changed ||
|
|
1822
2641
|
indexFormatNeedsPersist ||
|
|
1823
2642
|
attributeDescriptor?.indexingFailed ||
|
|
1824
|
-
|
|
2643
|
+
(attributeDescriptor?.indexingPID && attributeDescriptor?.indexingPID !== process.pid) ||
|
|
2644
|
+
attributeDescriptor?.restartNumber < currentRestartGeneration) {
|
|
1825
2645
|
hasChanges = true;
|
|
1826
2646
|
exclusiveLock();
|
|
1827
2647
|
attributeDescriptor = attributesDbi.getSync(dbiKey);
|
|
1828
2648
|
if (structurallyChanged ||
|
|
1829
2649
|
attributeDescriptor?.indexingFailed ||
|
|
1830
|
-
|
|
2650
|
+
(attributeDescriptor?.indexingPID && attributeDescriptor?.indexingPID !== process.pid) ||
|
|
2651
|
+
attributeDescriptor?.restartNumber < currentRestartGeneration) {
|
|
1831
2652
|
hasChanges = true;
|
|
1832
2653
|
if (attribute.indexNulls === undefined)
|
|
1833
2654
|
attribute.indexNulls = true;
|
|
@@ -1846,17 +2667,9 @@ function table(tableDefinition) {
|
|
|
1846
2667
|
// representation-only option difference — preserve the checkpoint so the backfill
|
|
1847
2668
|
// resumes rather than restarts. Canonicalized to match structurallyChanged above.
|
|
1848
2669
|
const indexOptionsChanged = canonicalIndexKey(attributeDescriptor?.indexed) !== canonicalIndexKey(attribute.indexed);
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
(attributeDescriptor.checkpointCertified === undefined ||
|
|
1853
|
-
(0, lmdb_1.compareKeys)(attributeDescriptor.checkpointCertified, attributeDescriptor.lastIndexedKey) !== 0);
|
|
1854
|
-
attribute.lastIndexedKey =
|
|
1855
|
-
indexOptionsChanged || uncertifiedCheckpoint
|
|
1856
|
-
? undefined
|
|
1857
|
-
: (attributeDescriptor?.lastIndexedKey ?? undefined);
|
|
1858
|
-
if (attribute.lastIndexedKey !== undefined)
|
|
1859
|
-
attribute.checkpointCertified = attribute.lastIndexedKey;
|
|
2670
|
+
attribute.lastIndexedKey = indexOptionsChanged
|
|
2671
|
+
? undefined
|
|
2672
|
+
: (attributeDescriptor?.lastIndexedKey ?? undefined);
|
|
1860
2673
|
// Explicit reindex is the upgrade path from a legacy (un-versioned) custom-index
|
|
1861
2674
|
// object store to the versioned, VT-cacheable format. A full rebuild from scratch
|
|
1862
2675
|
// (lastIndexedKey === undefined) clears the store and rewrites every node, so the
|
|
@@ -1878,9 +2691,6 @@ function table(tableDefinition) {
|
|
|
1878
2691
|
// the new process reuses the old PID. Cleared on clean completion; left in place
|
|
1879
2692
|
// on failure/crash so the next, higher-numbered restart re-triggers the backfill.
|
|
1880
2693
|
attribute.restartNumber = currentRestartGeneration;
|
|
1881
|
-
if (manageThreads.processIncarnation != null)
|
|
1882
|
-
attribute.indexingIncarnation = manageThreads.processIncarnation;
|
|
1883
|
-
attribute.indexingBuildId = (0, node_crypto_1.randomBytes)(8).toString('hex');
|
|
1884
2694
|
delete attribute.indexingFailed; // clear failure flag for the new run
|
|
1885
2695
|
dbi.isIndexing = true;
|
|
1886
2696
|
Object.defineProperty(attribute, 'dbi', { value: dbi, configurable: true, enumerable: false });
|
|
@@ -1896,12 +2706,6 @@ function table(tableDefinition) {
|
|
|
1896
2706
|
reindexReasons.push(`crash-recovery(pid=${attributeDescriptor.indexingPID})`);
|
|
1897
2707
|
if (attributeDescriptor?.restartNumber < currentRestartGeneration)
|
|
1898
2708
|
reindexReasons.push('restart-number');
|
|
1899
|
-
if (uncertifiedCheckpoint)
|
|
1900
|
-
reindexReasons.push('uncertified-checkpoint');
|
|
1901
|
-
if (attributeDescriptor?.indexingPID === process.pid &&
|
|
1902
|
-
manageThreads.processIncarnation != null &&
|
|
1903
|
-
attributeDescriptor.indexingIncarnation !== manageThreads.processIncarnation)
|
|
1904
|
-
reindexReasons.push('abandoned-build(previous process incarnation)');
|
|
1905
2709
|
logger.info(`reindex ${databaseName}.${tableName}.${attribute.name}: reason=${reindexReasons.join(',') || 'unknown'}`);
|
|
1906
2710
|
// we only set indexing nulls to true if new or reindexing, we can't have partial indexing of null
|
|
1907
2711
|
attributesToIndex.push(attribute);
|
|
@@ -1914,13 +2718,9 @@ function table(tableDefinition) {
|
|
|
1914
2718
|
// workers / a reload would treat the still-partial index as ready and return incomplete results.
|
|
1915
2719
|
attribute.indexingPID = attributeDescriptor.indexingPID;
|
|
1916
2720
|
attribute.lastIndexedKey = attributeDescriptor.lastIndexedKey;
|
|
1917
|
-
if (attributeDescriptor.checkpointCertified !== undefined)
|
|
1918
|
-
attribute.checkpointCertified = attributeDescriptor.checkpointCertified;
|
|
1919
2721
|
// Carry the in-progress restart generation too, so persisting this metadata-only
|
|
1920
2722
|
// change doesn't drop it and break the crash-recovery trigger for the running backfill.
|
|
1921
2723
|
attribute.restartNumber = attributeDescriptor.restartNumber;
|
|
1922
|
-
attribute.indexingIncarnation = attributeDescriptor.indexingIncarnation;
|
|
1923
|
-
attribute.indexingBuildId = attributeDescriptor.indexingBuildId;
|
|
1924
2724
|
if (attributeDescriptor.indexingFailed)
|
|
1925
2725
|
attribute.indexingFailed = attributeDescriptor.indexingFailed;
|
|
1926
2726
|
}
|
|
@@ -1943,26 +2743,61 @@ function table(tableDefinition) {
|
|
|
1943
2743
|
attributesDbi.put(dbiKey, attribute);
|
|
1944
2744
|
}
|
|
1945
2745
|
}
|
|
2746
|
+
// The primary row is what makes a table loadable, so it lands last: a scan on another thread that
|
|
2747
|
+
// runs mid-create skips the table instead of building (and announcing) a partial one. It already
|
|
2748
|
+
// carries this table's relationships (set on primaryKeyAttribute above), so the persistence block
|
|
2749
|
+
// below is a no-op for a create — a table is never published with an incomplete relationship list.
|
|
2750
|
+
if (deferredPrimaryRow) {
|
|
2751
|
+
attributesDbi.put(tableName + '/', deferredPrimaryRow);
|
|
2752
|
+
// That write, not the registration below, is the publish point: it is durable from here
|
|
2753
|
+
// (on LMDB releaseLock()'s finally commits this create's write transaction even while an
|
|
2754
|
+
// error unwinds), so any later throw must leave the catalog alone. Rolling back past it
|
|
2755
|
+
// would delete the attribute rows out from under a live primary row and leave every
|
|
2756
|
+
// thread loading the primary-only schema this change exists to prevent.
|
|
2757
|
+
published = true;
|
|
2758
|
+
setTable(tables, tableName, Table);
|
|
2759
|
+
}
|
|
2760
|
+
// a table with no declared primary key has no attribute row to carry relationships, and the
|
|
2761
|
+
// loop above never visits its descriptor
|
|
2762
|
+
if (relationshipDefinitions) {
|
|
2763
|
+
const relationshipsKey = primaryDescriptorKey();
|
|
2764
|
+
if (!relationshipListsEqual(attributesDbi.getSync(relationshipsKey)?.relationships, relationshipDefinitions)) {
|
|
2765
|
+
exclusiveLock();
|
|
2766
|
+
const currentPrimaryAttribute = attributesDbi.getSync(relationshipsKey);
|
|
2767
|
+
// a missing row means a concurrent drop completed; writing one back would resurrect the table
|
|
2768
|
+
if (currentPrimaryAttribute &&
|
|
2769
|
+
!tableIsDropping(currentPrimaryAttribute, relationshipsKey) &&
|
|
2770
|
+
!relationshipListsEqual(currentPrimaryAttribute.relationships, relationshipDefinitions)) {
|
|
2771
|
+
attributesDbi.put(relationshipsKey, { ...currentPrimaryAttribute, relationships: relationshipDefinitions });
|
|
2772
|
+
hasChanges = true;
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
catch (error) {
|
|
2778
|
+
if (unpublishedPrimaryStore && !published)
|
|
2779
|
+
discardUnpublishedTable();
|
|
2780
|
+
else if (published && tables[tableName] !== Table)
|
|
2781
|
+
discardUnregisteredClass();
|
|
2782
|
+
throw error;
|
|
1946
2783
|
}
|
|
1947
2784
|
finally {
|
|
1948
|
-
|
|
1949
|
-
releaseExclusiveLock();
|
|
2785
|
+
releaseLock();
|
|
1950
2786
|
}
|
|
1951
|
-
if (hasChanges) {
|
|
2787
|
+
if (hasChanges || refreshRelationshipAttributes) {
|
|
1952
2788
|
Table.schemaVersion++;
|
|
1953
2789
|
Table.updatedAttributes();
|
|
1954
2790
|
}
|
|
1955
2791
|
logger.trace(`${tableName} table loading, running index`);
|
|
2792
|
+
const branchPath = target.branch?.path;
|
|
1956
2793
|
if (attributesToIndex.length > 0 || indicesToRemove.length > 0) {
|
|
1957
|
-
|
|
1958
|
-
const buildIds = new Map(attributesToIndex.map((attribute) => [attribute, attribute.indexingBuildId]));
|
|
1959
|
-
const markSettled = () => markAbandonedIndexBuild(Table, rootStore, buildIds);
|
|
1960
|
-
Table.indexingOperation = runIndexing(Table, attributesToIndex, indicesToRemove).then(markSettled, markSettled);
|
|
2794
|
+
Table.indexingOperation = runIndexing(Table, attributesToIndex, indicesToRemove, branchPath);
|
|
1961
2795
|
}
|
|
1962
2796
|
else if (hasChanges)
|
|
1963
|
-
signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName));
|
|
2797
|
+
signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName, undefined, branchPath));
|
|
1964
2798
|
Table.origin = origin;
|
|
1965
|
-
|
|
2799
|
+
// scope-private: replication and other global subscribers must not learn of a branch class
|
|
2800
|
+
if ((hasChanges || refreshRelationshipAttributes) && !target.branch) {
|
|
1966
2801
|
exports.databaseEventsEmitter.emit('updateTable', Table, origin !== 'cluster');
|
|
1967
2802
|
}
|
|
1968
2803
|
if (expiration || eviction || scanInterval)
|
|
@@ -1973,15 +2808,68 @@ function table(tableDefinition) {
|
|
|
1973
2808
|
});
|
|
1974
2809
|
logger.trace(`${tableName} table loaded`);
|
|
1975
2810
|
return Table;
|
|
2811
|
+
// dropTable() tombstones the bare table row, which is not the row a legacy catalog keeps the
|
|
2812
|
+
// table's settings in, so a drop in flight has to be checked on both.
|
|
2813
|
+
function tableIsDropping(descriptor, descriptorKey) {
|
|
2814
|
+
if (descriptor?.dropping)
|
|
2815
|
+
return true;
|
|
2816
|
+
return descriptorKey !== tableName + '/' && attributesDbi.getSync(tableName + '/')?.dropping;
|
|
2817
|
+
}
|
|
2818
|
+
// The catalog row initStores() reads a table's settings from: the primary key's own row when it
|
|
2819
|
+
// has one, and the bare table row otherwise.
|
|
2820
|
+
function primaryDescriptorKey() {
|
|
2821
|
+
const declaredPrimaryKey = attributes?.find((attribute) => attribute.isPrimaryKey)?.name;
|
|
2822
|
+
if (declaredPrimaryKey) {
|
|
2823
|
+
const attributeKey = tableName + '/' + declaredPrimaryKey;
|
|
2824
|
+
if (attributesDbi.getSync(attributeKey))
|
|
2825
|
+
return attributeKey;
|
|
2826
|
+
}
|
|
2827
|
+
return tableName + '/';
|
|
2828
|
+
}
|
|
2829
|
+
// The catalog of a published table stays, but a class the registration never accepted is
|
|
2830
|
+
// unreachable, so release what makeTable() registered process-wide instead of leaving its timers
|
|
2831
|
+
// and reclamation handler live for the process. The stores stay open: the table is durable, and
|
|
2832
|
+
// whichever scan reloads it opens its own handles.
|
|
2833
|
+
function discardUnregisteredClass() {
|
|
2834
|
+
try {
|
|
2835
|
+
Table.cleanup();
|
|
2836
|
+
}
|
|
2837
|
+
catch (discardError) {
|
|
2838
|
+
logger.warn(`Error releasing the unregistered class of ${databaseName}.${tableName}`, discardError);
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
function discardUnpublishedTable() {
|
|
2842
|
+
const discard = (description, action) => {
|
|
2843
|
+
try {
|
|
2844
|
+
action();
|
|
2845
|
+
}
|
|
2846
|
+
catch (discardError) {
|
|
2847
|
+
logger.warn(`Error discarding ${description} of the failed create of ${databaseName}.${tableName}`, discardError);
|
|
2848
|
+
}
|
|
2849
|
+
};
|
|
2850
|
+
discard('catalog rows', () => {
|
|
2851
|
+
for (const attribute of attributes) {
|
|
2852
|
+
if (!attribute.isPrimaryKey && !attribute.relationship)
|
|
2853
|
+
attributesDbi.remove(tableName + '/' + attribute.name);
|
|
2854
|
+
}
|
|
2855
|
+
});
|
|
2856
|
+
if (Table)
|
|
2857
|
+
discard('callbacks', () => Table.cleanup());
|
|
2858
|
+
// an LMDB store is a per-environment handle slot shared with every thread and still inside this
|
|
2859
|
+
// create's write transaction; only RocksDB column-family handles hold native state to release
|
|
2860
|
+
if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
|
|
2861
|
+
for (const indexName in Table?.indices ?? {})
|
|
2862
|
+
discard(`index ${indexName}`, () => Table.indices[indexName].close());
|
|
2863
|
+
discard('primary store', () => unpublishedPrimaryStore.close());
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
1976
2866
|
// Acquire an exclusive lock for attribute updates
|
|
1977
2867
|
function exclusiveLock() {
|
|
1978
2868
|
if (releaseExclusiveLock)
|
|
1979
2869
|
return;
|
|
1980
2870
|
if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
|
|
1981
|
-
|
|
1982
|
-
releaseExclusiveLock = () =>
|
|
1983
|
-
rootStore.unlock('update-attributes');
|
|
1984
|
-
};
|
|
2871
|
+
(0, Table_ts_1.acquireUpdateAttributesLock)(rootStore, `table '${databaseName}.${tableName}'`);
|
|
2872
|
+
releaseExclusiveLock = () => (0, Table_ts_1.releaseUpdateAttributesLock)(rootStore);
|
|
1985
2873
|
}
|
|
1986
2874
|
else {
|
|
1987
2875
|
// we only need an exclusive transaction lock in lmdb
|
|
@@ -1994,6 +2882,14 @@ function table(tableDefinition) {
|
|
|
1994
2882
|
});
|
|
1995
2883
|
}
|
|
1996
2884
|
}
|
|
2885
|
+
// idempotent: the early release before the recursive reload and the finally both run, and a
|
|
2886
|
+
// second unlock could release another thread's lock
|
|
2887
|
+
function releaseLock() {
|
|
2888
|
+
const release = releaseExclusiveLock;
|
|
2889
|
+
releaseExclusiveLock = undefined;
|
|
2890
|
+
if (release)
|
|
2891
|
+
release();
|
|
2892
|
+
}
|
|
1997
2893
|
}
|
|
1998
2894
|
/**
|
|
1999
2895
|
* Canonical form used ONLY for the structural (reindex-triggering) comparison of index options.
|
|
@@ -2031,163 +2927,28 @@ function canonicalizeIndexOptions(value) {
|
|
|
2031
2927
|
}
|
|
2032
2928
|
const MAX_OUTSTANDING_INDEXING = 1000;
|
|
2033
2929
|
const MIN_OUTSTANDING_INDEXING = 10;
|
|
2034
|
-
|
|
2035
|
-
// A resumable checkpoint is written only after a flush (see flushIndexStores), at most once per period
|
|
2036
|
-
// and never before this many more records: the flush seals every column family in the database, so a
|
|
2037
|
-
// slow backfill must not impose the period's flush rate on unrelated tables.
|
|
2038
|
-
let indexingCheckpointPeriodMs = 5000;
|
|
2039
|
-
let indexingCheckpointMinRecords = 10000;
|
|
2040
|
-
function setIndexingCheckpointPeriod(ms, minRecords = indexingCheckpointMinRecords) {
|
|
2041
|
-
const previous = { ms: indexingCheckpointPeriodMs, minRecords: indexingCheckpointMinRecords };
|
|
2042
|
-
indexingCheckpointPeriodMs = ms;
|
|
2043
|
-
indexingCheckpointMinRecords = minRecords;
|
|
2044
|
-
return previous;
|
|
2045
|
-
}
|
|
2046
|
-
const yieldEventTurn = () => new Promise((resolve) => setImmediate(resolve));
|
|
2047
|
-
// RocksDB index stores have no WAL (openRocksDatabase defaults disableWAL), so a flush is what makes the
|
|
2048
|
-
// entries a checkpoint certifies durable. A flush only covers writes issued before it started, so a caller
|
|
2049
|
-
// never joins one in flight: it joins the next one, which every backfill on that database asking meanwhile
|
|
2050
|
-
// shares — at most one in flight and one queued.
|
|
2051
|
-
const indexingFlushes = new WeakMap();
|
|
2052
|
-
function flushIndexStores(rootStore) {
|
|
2053
|
-
if (!(rootStore instanceof rocksdb_js_1.RocksDatabase))
|
|
2054
|
-
return;
|
|
2055
|
-
let flushes = indexingFlushes.get(rootStore);
|
|
2056
|
-
if (!flushes)
|
|
2057
|
-
indexingFlushes.set(rootStore, (flushes = {}));
|
|
2058
|
-
if (flushes.queued)
|
|
2059
|
-
return flushes.queued;
|
|
2060
|
-
const start = () => {
|
|
2061
|
-
flushes.queued = undefined;
|
|
2062
|
-
const flush = rootStore.flush().finally(() => {
|
|
2063
|
-
if (flushes.inFlight === flush)
|
|
2064
|
-
flushes.inFlight = undefined;
|
|
2065
|
-
});
|
|
2066
|
-
flushes.inFlight = flush;
|
|
2067
|
-
return flush;
|
|
2068
|
-
};
|
|
2069
|
-
if (!flushes.inFlight)
|
|
2070
|
-
return start();
|
|
2071
|
-
return (flushes.queued = flushes.inFlight.then(start, start));
|
|
2072
|
-
}
|
|
2073
|
-
function resumeStartKey(attributes) {
|
|
2074
|
-
let start;
|
|
2075
|
-
for (const attribute of attributes) {
|
|
2076
|
-
if (attribute.lastIndexedKey == undefined)
|
|
2077
|
-
return undefined;
|
|
2078
|
-
if (start === undefined || (0, lmdb_1.compareKeys)(attribute.lastIndexedKey, start) < 0)
|
|
2079
|
-
start = attribute.lastIndexedKey;
|
|
2080
|
-
}
|
|
2081
|
-
return start;
|
|
2082
|
-
}
|
|
2083
|
-
// Bounded, unlike the exclusiveLock() spin above: that one runs on the declaring path, where the
|
|
2084
|
-
// caller is waiting on the result and there is nothing useful to do without the lock. This one runs
|
|
2085
|
-
// after a backfill has already settled, so a holder that never releases would wedge the worker's
|
|
2086
|
-
// event loop for nothing. Giving up costs only the marker, and the next load of the table
|
|
2087
|
-
// re-triggers the build regardless.
|
|
2088
|
-
exports.ABANDONED_MARK_LOCK_TIMEOUT = 10000;
|
|
2089
|
-
const abandonedMarkLockWait = new Int32Array(new SharedArrayBuffer(4));
|
|
2090
|
-
// `timeout` is the test seam; production callers take the default.
|
|
2091
|
-
function tryAcquireUpdateAttributesLock(rootStore, timeout = exports.ABANDONED_MARK_LOCK_TIMEOUT) {
|
|
2092
|
-
if (rootStore.tryLock('update-attributes'))
|
|
2093
|
-
return true;
|
|
2094
|
-
const startTime = performance.now();
|
|
2095
|
-
let waitTime = 1;
|
|
2096
|
-
while (!rootStore.tryLock('update-attributes')) {
|
|
2097
|
-
const elapsed = performance.now() - startTime;
|
|
2098
|
-
if (elapsed >= timeout)
|
|
2099
|
-
return false;
|
|
2100
|
-
// Atomics.wait rather than a busy spin: the section this guards is synchronous, so the wait
|
|
2101
|
-
// blocks this thread either way, but sleeping does not burn a core while it does.
|
|
2102
|
-
if (elapsed >= 2) {
|
|
2103
|
-
Atomics.wait(abandonedMarkLockWait, 0, 0, Math.min(waitTime, timeout - elapsed));
|
|
2104
|
-
if (waitTime < 16)
|
|
2105
|
-
waitTime *= 2;
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
2108
|
-
return true;
|
|
2109
|
-
}
|
|
2110
|
-
/**
|
|
2111
|
-
* Persists the failure marker for a build that ended without running one of runIndexing's own exit
|
|
2112
|
-
* paths, so something re-triggers it. Fenced on `indexingBuildId` inside the storage engine's catalog
|
|
2113
|
-
* serialization boundary, because a replacement generation (or another thread declaring different index
|
|
2114
|
-
* options) can claim the attribute before an outgoing build's promise settles, and marking that would fail
|
|
2115
|
-
* a live build. The fence read and write stay synchronous, and nothing here may throw because
|
|
2116
|
-
* `Table.indexingOperation` reaches operations-API callers.
|
|
2117
|
-
*/
|
|
2118
|
-
async function markAbandonedIndexBuild(Table, rootStore, buildIds) {
|
|
2119
|
-
for (const [attribute, buildId] of buildIds) {
|
|
2120
|
-
try {
|
|
2121
|
-
let marked;
|
|
2122
|
-
if (buildId == null || Table.dbisDB.getSync(attribute.key)?.indexingBuildId !== buildId)
|
|
2123
|
-
continue;
|
|
2124
|
-
const markIfOwned = () => {
|
|
2125
|
-
const descriptor = Table.dbisDB.getSync(attribute.key);
|
|
2126
|
-
if (descriptor?.indexingBuildId === buildId && !descriptor.indexingFailed) {
|
|
2127
|
-
Table.dbisDB.putSync(attribute.key, { ...descriptor, indexingFailed: true });
|
|
2128
|
-
marked = true;
|
|
2129
|
-
}
|
|
2130
|
-
};
|
|
2131
|
-
if (rootStore instanceof rocksdb_js_1.RocksDatabase) {
|
|
2132
|
-
if (!tryAcquireUpdateAttributesLock(rootStore)) {
|
|
2133
|
-
logger.warn(`Could not mark the abandoned index build of ${Table.databaseName}.${Table.tableName}.${attribute.name}: ` +
|
|
2134
|
-
`timed out after ${exports.ABANDONED_MARK_LOCK_TIMEOUT}ms waiting for the exclusive 'update-attributes' lock. ` +
|
|
2135
|
-
`The index stays incomplete and the next load of the table re-triggers the backfill.`);
|
|
2136
|
-
continue;
|
|
2137
|
-
}
|
|
2138
|
-
try {
|
|
2139
|
-
markIfOwned();
|
|
2140
|
-
}
|
|
2141
|
-
finally {
|
|
2142
|
-
rootStore.unlock('update-attributes');
|
|
2143
|
-
}
|
|
2144
|
-
}
|
|
2145
|
-
else {
|
|
2146
|
-
rootStore.transactionSync(markIfOwned);
|
|
2147
|
-
}
|
|
2148
|
-
if (marked)
|
|
2149
|
-
logger.warn(`Indexing of ${Table.databaseName}.${Table.tableName}.${attribute.name} ended without completing. ` +
|
|
2150
|
-
`The index stays incomplete and every query on the attribute reports it as not indexed yet; ` +
|
|
2151
|
-
`the next load of the table retries the backfill from the last checkpoint (indexingFailed=true).`);
|
|
2152
|
-
}
|
|
2153
|
-
catch (error) {
|
|
2154
|
-
// A store closed by shutdown is the common case, and it cannot be written to at all.
|
|
2155
|
-
try {
|
|
2156
|
-
logger.debug(`Could not mark the abandoned index build of ${Table.tableName}.${attribute.name}`, error);
|
|
2157
|
-
}
|
|
2158
|
-
catch { }
|
|
2159
|
-
}
|
|
2160
|
-
}
|
|
2161
|
-
}
|
|
2162
|
-
async function runIndexing(Table, attributes, indicesToRemove) {
|
|
2163
|
-
let checkpointing;
|
|
2164
|
-
let hadIndexingErrors = false;
|
|
2165
|
-
const attributeErrorReported = {};
|
|
2166
|
-
const onIndexPutRejected = (property, error) => {
|
|
2167
|
-
hadIndexingErrors = true;
|
|
2168
|
-
if (attributeErrorReported[property])
|
|
2169
|
-
return;
|
|
2170
|
-
attributeErrorReported[property] = true;
|
|
2171
|
-
logger.error(`Error indexing attribute ${property}`, error);
|
|
2172
|
-
};
|
|
2173
|
-
const putRejectionHandlers = attributes.map((attribute) => (error) => onIndexPutRejected(attribute.name, error));
|
|
2930
|
+
async function runIndexing(Table, attributes, indicesToRemove, branchPath) {
|
|
2174
2931
|
try {
|
|
2175
2932
|
logger.info(`Indexing ${Table.tableName} attributes`, attributes);
|
|
2176
|
-
await signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName));
|
|
2933
|
+
await signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName, undefined, branchPath));
|
|
2177
2934
|
let lastResolution;
|
|
2178
2935
|
for (const index of indicesToRemove) {
|
|
2179
2936
|
lastResolution = index.drop();
|
|
2180
|
-
if (lastResolution?.then)
|
|
2181
|
-
lastResolution.then(undefined, (error) => onIndexPutRejected(index.name, error));
|
|
2182
2937
|
}
|
|
2183
2938
|
let interrupted;
|
|
2939
|
+
let hadIndexingErrors = false;
|
|
2940
|
+
const attributeErrorReported = {};
|
|
2184
2941
|
let indexed = 0;
|
|
2185
2942
|
const attributesLength = attributes.length;
|
|
2186
2943
|
await new Promise((resolve) => setImmediate(resolve)); // yield event turn, indexing should consistently take at least one event turn
|
|
2187
2944
|
if (attributesLength > 0) {
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2945
|
+
let start;
|
|
2946
|
+
for (const attribute of attributes) {
|
|
2947
|
+
// if we are resuming, we need to start from the last key we indexed by all attributes
|
|
2948
|
+
if ((0, lmdb_1.compareKeys)(attribute.lastIndexedKey, start) < 0)
|
|
2949
|
+
start = attribute.lastIndexedKey;
|
|
2950
|
+
if (attribute.lastIndexedKey == undefined) {
|
|
2951
|
+
// if we are starting from the beginning, clear out any previous index entries since we are rewriting
|
|
2191
2952
|
if (attribute.dbi.clearAsync) {
|
|
2192
2953
|
// LMDB, note that we don't need to wait for this to complete, just gets enqueued in front of the other writes
|
|
2193
2954
|
attribute.dbi.clearAsync();
|
|
@@ -2198,28 +2959,6 @@ async function runIndexing(Table, attributes, indicesToRemove) {
|
|
|
2198
2959
|
}
|
|
2199
2960
|
}
|
|
2200
2961
|
let outstanding = 0;
|
|
2201
|
-
// A resumed scan starts at the checkpoint, so it must only name a key whose every predecessor is
|
|
2202
|
-
// durably indexed: persisted once the writes it covers have settled and flushed, frozen after any
|
|
2203
|
-
// record fails so the retry re-covers it, and stamped with its own key (see the trigger in table()).
|
|
2204
|
-
const persistCheckpoint = async (key) => {
|
|
2205
|
-
if (hadIndexingErrors)
|
|
2206
|
-
return;
|
|
2207
|
-
try {
|
|
2208
|
-
await flushIndexStores(Table.primaryStore.rootStore);
|
|
2209
|
-
const puts = [];
|
|
2210
|
-
for (const attribute of attributes) {
|
|
2211
|
-
attribute.lastIndexedKey = key;
|
|
2212
|
-
attribute.checkpointCertified = key;
|
|
2213
|
-
puts.push(Table.dbisDB.put(attribute.key, attribute));
|
|
2214
|
-
}
|
|
2215
|
-
await Promise.all(puts);
|
|
2216
|
-
}
|
|
2217
|
-
catch (error) {
|
|
2218
|
-
logger.warn(`Could not persist the indexing checkpoint for ${Table.tableName}`, error);
|
|
2219
|
-
}
|
|
2220
|
-
};
|
|
2221
|
-
let nextCheckpointAt = performance.now() + indexingCheckpointPeriodMs;
|
|
2222
|
-
let nextCheckpointRecord = indexingCheckpointMinRecords;
|
|
2223
2962
|
// this means that a new attribute has been introduced that needs to be indexed
|
|
2224
2963
|
for (const { key, value: record } of Table.primaryStore.getRange({
|
|
2225
2964
|
start,
|
|
@@ -2227,7 +2966,8 @@ async function runIndexing(Table, attributes, indicesToRemove) {
|
|
|
2227
2966
|
versions: true,
|
|
2228
2967
|
snapshot: false, // don't hold a read transaction this whole time
|
|
2229
2968
|
})) {
|
|
2230
|
-
|
|
2969
|
+
if (!record)
|
|
2970
|
+
continue; // deletion entry
|
|
2231
2971
|
// TODO: Do we ever need to interrupt due to a schema change that was not a restart?
|
|
2232
2972
|
//if (Table.schemaVersion !== schemaVersion) return; // break out if there are any schema changes and let someone else pick it up
|
|
2233
2973
|
outstanding++;
|
|
@@ -2239,75 +2979,72 @@ async function runIndexing(Table, attributes, indicesToRemove) {
|
|
|
2239
2979
|
// we index, that's fine because indexing is idempotent, we can just put the same values again. If it changes
|
|
2240
2980
|
// during the indexing, the indexing here will fail. This is also fine because it means the other thread will have
|
|
2241
2981
|
// performed indexing and we don't need to do anything further
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
const
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
didSynchronousIndexing = true;
|
|
2254
|
-
continue;
|
|
2255
|
-
}
|
|
2256
|
-
const values = (0, commonUtility_ts_1.getIndexedValues)(value, index.indexNulls);
|
|
2257
|
-
if (values) {
|
|
2258
|
-
for (let i = 0, l = values.length; i < l; i++) {
|
|
2259
|
-
lastResolution = index.put(values[i], key);
|
|
2260
|
-
if (lastResolution?.then)
|
|
2261
|
-
lastResolution.then(undefined, onPutRejected);
|
|
2262
|
-
}
|
|
2263
|
-
}
|
|
2982
|
+
for (let i = 0; i < attributesLength; i++) {
|
|
2983
|
+
const attribute = attributes[i];
|
|
2984
|
+
const property = attribute.name;
|
|
2985
|
+
const index = attribute.dbi;
|
|
2986
|
+
try {
|
|
2987
|
+
const resolver = attribute.resolve;
|
|
2988
|
+
const value = record && (resolver ? resolver(record) : record[property]);
|
|
2989
|
+
if (index.customIndex) {
|
|
2990
|
+
index.customIndex.index(key, value);
|
|
2991
|
+
didSynchronousIndexing = true;
|
|
2992
|
+
continue;
|
|
2264
2993
|
}
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
// A store closed by worker shutdown surfaces here as "Database not open"; that is
|
|
2270
|
-
// a benign interruption (the next generation re-runs the backfill), so don't log
|
|
2271
|
-
// it as an error — the outer catch returns quietly once the iterator also throws.
|
|
2272
|
-
attributeErrorReported[property] = true;
|
|
2273
|
-
if (Table.primaryStore?.rootStore?.status === 'closed')
|
|
2274
|
-
logger.debug(`Indexing attribute ${property} interrupted by store shutdown`, error);
|
|
2275
|
-
else
|
|
2276
|
-
logger.error(`Error indexing attribute ${property}`, error);
|
|
2994
|
+
const values = (0, commonUtility_ts_1.getIndexedValues)(value, index.indexNulls);
|
|
2995
|
+
if (values) {
|
|
2996
|
+
for (let i = 0, l = values.length; i < l; i++) {
|
|
2997
|
+
lastResolution = index.put(values[i], key);
|
|
2277
2998
|
}
|
|
2278
2999
|
}
|
|
2279
3000
|
}
|
|
3001
|
+
catch (error) {
|
|
3002
|
+
hadIndexingErrors = true;
|
|
3003
|
+
if (!attributeErrorReported[property]) {
|
|
3004
|
+
// just report an indexing error once per attribute so we don't spam the logs.
|
|
3005
|
+
// A store closed by worker shutdown surfaces here as "Database not open"; that is
|
|
3006
|
+
// a benign interruption (the next generation re-runs the backfill), so don't log
|
|
3007
|
+
// it as an error — the outer catch returns quietly once the iterator also throws.
|
|
3008
|
+
attributeErrorReported[property] = true;
|
|
3009
|
+
if (Table.primaryStore?.rootStore?.status === 'closed')
|
|
3010
|
+
logger.debug(`Indexing attribute ${property} interrupted by store shutdown`, error);
|
|
3011
|
+
else
|
|
3012
|
+
logger.error(`Error indexing attribute ${property}`, error);
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
2280
3015
|
}
|
|
2281
|
-
(0, when_ts_1.when)(lastResolution, () => outstanding--, () =>
|
|
3016
|
+
(0, when_ts_1.when)(lastResolution, () => outstanding--, (error) => {
|
|
3017
|
+
outstanding--;
|
|
3018
|
+
hadIndexingErrors = true;
|
|
3019
|
+
logger.error(error);
|
|
3020
|
+
});
|
|
2282
3021
|
if (worker_threads_1.workerData && worker_threads_1.workerData.restartNumber !== manageThreads.restartNumber) {
|
|
2283
3022
|
interrupted = true;
|
|
2284
3023
|
}
|
|
2285
|
-
if (interrupted) {
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
// already counted and logged by the rejection handler above
|
|
3024
|
+
if (++indexed % 100 === 0 || interrupted) {
|
|
3025
|
+
// occasionally update our progress so if we crash, we can resume
|
|
3026
|
+
for (const attribute of attributes) {
|
|
3027
|
+
attribute.lastIndexedKey = key;
|
|
3028
|
+
Table.dbisDB.put(attribute.key, attribute);
|
|
2291
3029
|
}
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
return;
|
|
2295
|
-
}
|
|
2296
|
-
if (atInterval && indexed >= nextCheckpointRecord && performance.now() >= nextCheckpointAt) {
|
|
2297
|
-
nextCheckpointAt = performance.now() + indexingCheckpointPeriodMs;
|
|
2298
|
-
nextCheckpointRecord = indexed + indexingCheckpointMinRecords;
|
|
2299
|
-
await checkpointing;
|
|
2300
|
-
checkpointing = (0, when_ts_1.when)(lastResolution, () => persistCheckpoint(key), () => { });
|
|
3030
|
+
if (interrupted)
|
|
3031
|
+
return;
|
|
2301
3032
|
}
|
|
2302
3033
|
if (outstanding > MAX_OUTSTANDING_INDEXING)
|
|
2303
3034
|
await lastResolution;
|
|
2304
|
-
if (
|
|
2305
|
-
await
|
|
3035
|
+
else if (outstanding > MIN_OUTSTANDING_INDEXING)
|
|
3036
|
+
await new Promise((resolve) => setImmediate(resolve)); // yield event turn, don't want to use all computation
|
|
3037
|
+
else if (didSynchronousIndexing)
|
|
3038
|
+
await new Promise((resolve) => setImmediate(resolve)); // custom indexes (e.g. HNSW) index synchronously and never raise `outstanding`; without this yield a large backfill runs in a single event-loop turn, starving keepalive/replication and queries and never letting the isIndexing flag be observed
|
|
2306
3039
|
}
|
|
2307
3040
|
}
|
|
2308
|
-
|
|
2309
|
-
//
|
|
2310
|
-
//
|
|
3041
|
+
// Await the last pending put. If it rejects, that is also an indexing error.
|
|
3042
|
+
// Note: the when() calls above already attach rejection handlers to each record's
|
|
3043
|
+
// last-put promise; this try-catch specifically handles the case where lastResolution
|
|
3044
|
+
// itself rejects (i.e. the very last put in the loop failed) which would otherwise
|
|
3045
|
+
// throw past the hadIndexingErrors check to the outer catch. The broader issue of
|
|
3046
|
+
// unhandled rejections from non-last puts in multi-value attributes is pre-existing
|
|
3047
|
+
// and out of scope for this fix.
|
|
2311
3048
|
try {
|
|
2312
3049
|
await lastResolution;
|
|
2313
3050
|
}
|
|
@@ -2319,17 +3056,6 @@ async function runIndexing(Table, attributes, indicesToRemove) {
|
|
|
2319
3056
|
// microtasks when their tracked promise settles) have a chance to set hadIndexingErrors
|
|
2320
3057
|
// before we decide whether to mark indexing as complete.
|
|
2321
3058
|
await new Promise((resolve) => setImmediate(resolve));
|
|
2322
|
-
// the tail since the last checkpoint is not durable until flushed; announcing the index complete
|
|
2323
|
-
// before that would outlive a crash that loses it
|
|
2324
|
-
if (!hadIndexingErrors) {
|
|
2325
|
-
try {
|
|
2326
|
-
await flushIndexStores(Table.primaryStore.rootStore);
|
|
2327
|
-
}
|
|
2328
|
-
catch (error) {
|
|
2329
|
-
hadIndexingErrors = true;
|
|
2330
|
-
logger.error(`Could not flush the indexes of ${Table.tableName} before marking them complete`, error);
|
|
2331
|
-
}
|
|
2332
|
-
}
|
|
2333
3059
|
if (hadIndexingErrors) {
|
|
2334
3060
|
// Some records failed to index. Persist the failure marker in the descriptor so
|
|
2335
3061
|
// the next call to table() (including after a restart with a fresh PID) re-triggers
|
|
@@ -2358,12 +3084,9 @@ async function runIndexing(Table, attributes, indicesToRemove) {
|
|
|
2358
3084
|
// update the attributes to indicate that we are finished
|
|
2359
3085
|
for (const attribute of attributes) {
|
|
2360
3086
|
delete attribute.lastIndexedKey;
|
|
2361
|
-
delete attribute.checkpointCertified;
|
|
2362
3087
|
delete attribute.indexingPID;
|
|
2363
3088
|
delete attribute.indexingFailed;
|
|
2364
3089
|
delete attribute.restartNumber;
|
|
2365
|
-
delete attribute.indexingIncarnation;
|
|
2366
|
-
delete attribute.indexingBuildId;
|
|
2367
3090
|
attribute.dbi.isIndexing = false;
|
|
2368
3091
|
// Also clear isIndexing on the currently-active dbi in Table.indices, which may
|
|
2369
3092
|
// differ from attribute.dbi if a resetDatabases() call during this migration
|
|
@@ -2375,12 +3098,11 @@ async function runIndexing(Table, attributes, indicesToRemove) {
|
|
|
2375
3098
|
}
|
|
2376
3099
|
await lastResolution;
|
|
2377
3100
|
// now notify all the threads that we are done and the index is ready to use
|
|
2378
|
-
await signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, 'indexing-finished', Table.databaseName, Table.tableName));
|
|
3101
|
+
await signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, 'indexing-finished', Table.databaseName, Table.tableName, undefined, branchPath));
|
|
2379
3102
|
logger.info(`Finished indexing ${Table.tableName} attributes`, attributes);
|
|
2380
3103
|
}
|
|
2381
3104
|
}
|
|
2382
3105
|
catch (error) {
|
|
2383
|
-
await checkpointing;
|
|
2384
3106
|
// A worker shutting down closes its stores mid-backfill, so the range iterator or a
|
|
2385
3107
|
// put throws (e.g. "Database not open" / "Iterator not initialized"). This is an
|
|
2386
3108
|
// interruption, not a data error: the next worker generation re-runs the backfill via
|