@harperfast/harper 5.2.13 → 5.3.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/mcpTools.ts +1 -1
- package/agent/session.ts +25 -14
- package/bin/cliOperations.ts +58 -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 +120 -4
- package/bin/run.ts +6 -11
- package/bin/upgrade.js +7 -3
- package/bin/workloadIdentity.ts +119 -0
- package/components/Application.ts +3436 -242
- package/components/ApplicationScope.ts +8 -0
- package/components/EntryHandler.ts +59 -39
- package/components/OptionsWatcher.ts +440 -98
- package/components/RuntimeModuleTracker.ts +38 -7
- package/components/Scope.ts +56 -15
- package/components/awaitRestart.ts +84 -0
- package/components/componentLoader.ts +381 -32
- package/components/componentPreparationLock.ts +16 -5
- package/components/deploymentOperations.ts +4 -1
- package/components/deploymentRecorder.ts +9 -2
- 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 +537 -113
- package/components/operationsValidation.js +98 -3
- package/components/packageComponent.ts +25 -1
- package/components/requestRestart.ts +11 -0
- package/components/status/ComponentStatusRegistry.ts +59 -0
- package/config/RootConfigWatcher.ts +240 -40
- package/config/configReadRetry.ts +62 -0
- package/config/configUtils.ts +357 -48
- package/config/harperConfigEnvVars.ts +170 -27
- package/config/parseConfigFile.ts +34 -0
- package/config/readConfigFileSync.ts +44 -0
- package/config/watcherArming.ts +59 -0
- package/config-root.schema.json +33 -0
- package/dataLayer/blobBackup.ts +160 -50
- package/dataLayer/delete.ts +6 -1
- package/dataLayer/harperBridge/ResourceBridge.ts +80 -10
- 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 +61 -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 +91 -6
- 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 +235 -17
- package/dist/components/Application.js +3038 -238
- 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 +4 -0
- package/dist/components/OptionsWatcher.js +440 -99
- 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 +52 -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 +313 -24
- 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/deploymentOperations.js +4 -1
- package/dist/components/deploymentOperations.js.map +1 -1
- package/dist/components/deploymentRecorder.d.ts +4 -2
- package/dist/components/deploymentRecorder.js +1 -0
- package/dist/components/deploymentRecorder.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.d.ts +28 -0
- package/dist/components/operations.js +476 -113
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +97 -3
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/packageComponent.js +24 -0
- package/dist/components/packageComponent.js.map +1 -1
- package/dist/components/requestRestart.d.ts +1 -0
- package/dist/components/requestRestart.js +7 -0
- package/dist/components/requestRestart.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 +9 -1
- package/dist/config/RootConfigWatcher.js +228 -37
- package/dist/config/RootConfigWatcher.js.map +1 -1
- package/dist/config/configReadRetry.d.ts +8 -0
- package/dist/config/configReadRetry.js +62 -0
- package/dist/config/configReadRetry.js.map +1 -0
- package/dist/config/configUtils.d.ts +11 -2
- package/dist/config/configUtils.js +304 -47
- 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/config/parseConfigFile.d.ts +4 -0
- package/dist/config/parseConfigFile.js +35 -0
- package/dist/config/parseConfigFile.js.map +1 -0
- package/dist/config/readConfigFileSync.d.ts +1 -0
- package/dist/config/readConfigFileSync.js +47 -0
- package/dist/config/readConfigFileSync.js.map +1 -0
- package/dist/config/watcherArming.d.ts +15 -0
- package/dist/config/watcherArming.js +59 -0
- package/dist/config/watcherArming.js.map +1 -0
- 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 +72 -12
- 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 +2 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +55 -0
- package/dist/resources/DatabaseTransaction.d.ts +77 -0
- package/dist/resources/DatabaseTransaction.js +586 -54
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/LMDBTransaction.d.ts +2 -1
- package/dist/resources/LMDBTransaction.js +43 -6
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.d.ts +1 -0
- package/dist/resources/PrimaryRocksDatabase.js +49 -5
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
- package/dist/resources/RecordEncoder.d.ts +20 -0
- package/dist/resources/RecordEncoder.js +110 -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 +117 -24
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +28 -1
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/RocksIndexStore.d.ts +6 -1
- package/dist/resources/RocksIndexStore.js +24 -9
- package/dist/resources/RocksIndexStore.js.map +1 -1
- package/dist/resources/RocksTransactionLogStore.d.ts +24 -1
- package/dist/resources/RocksTransactionLogStore.js +160 -49
- package/dist/resources/RocksTransactionLogStore.js.map +1 -1
- package/dist/resources/Table.d.ts +165 -10
- package/dist/resources/Table.js +1805 -303
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +10 -3
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/auditStore.d.ts +181 -2
- package/dist/resources/auditStore.js +640 -29
- 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/dataLoader.js +3 -4
- package/dist/resources/dataLoader.js.map +1 -1
- package/dist/resources/databases.d.ts +157 -14
- package/dist/resources/databases.js +1462 -325
- 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/derivedIndexRegistry.d.ts +5 -0
- package/dist/resources/derivedIndexRegistry.js +68 -0
- package/dist/resources/derivedIndexRegistry.js.map +1 -0
- package/dist/resources/derivedIndexRuntime.d.ts +215 -0
- package/dist/resources/derivedIndexRuntime.js +2027 -0
- package/dist/resources/derivedIndexRuntime.js.map +1 -0
- package/dist/resources/graphql.d.ts +1 -1
- package/dist/resources/graphql.js +52 -16
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +103 -9
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +854 -41
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/indexes/hnswDerivedIndex.d.ts +67 -0
- package/dist/resources/indexes/hnswDerivedIndex.js +464 -0
- package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -0
- package/dist/resources/indexes/hnswPlaneBinding.d.ts +65 -0
- package/dist/resources/indexes/hnswPlaneBinding.js +91 -0
- package/dist/resources/indexes/hnswPlaneBinding.js.map +1 -0
- 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/nodeIdMapping.d.ts +5 -0
- package/dist/resources/nodeIdMapping.js +49 -0
- package/dist/resources/nodeIdMapping.js.map +1 -1
- package/dist/resources/recordLock.d.ts +123 -0
- package/dist/resources/recordLock.js +315 -0
- package/dist/resources/recordLock.js.map +1 -0
- package/dist/resources/recordLockCoordinator.d.ts +557 -0
- package/dist/resources/recordLockCoordinator.js +2565 -0
- package/dist/resources/recordLockCoordinator.js.map +1 -0
- package/dist/resources/replayLogs.d.ts +14 -1
- package/dist/resources/replayLogs.js +157 -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/replicatedApplyFailure.d.ts +16 -0
- package/dist/resources/replicatedApplyFailure.js +63 -0
- package/dist/resources/replicatedApplyFailure.js.map +1 -0
- package/dist/resources/scheduler/scheduler.js +3 -3
- package/dist/resources/scheduler/scheduler.js.map +1 -1
- package/dist/resources/search.d.ts +10 -4
- package/dist/resources/search.js +283 -37
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/tracked.d.ts +5 -1
- package/dist/resources/tracked.js +74 -23
- package/dist/resources/tracked.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 +77 -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 +106 -2
- 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 +16 -1
- package/dist/server/http.js +122 -17
- 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 +27 -0
- package/dist/server/serverHelpers/Headers.js +145 -1
- package/dist/server/serverHelpers/Headers.js.map +1 -1
- package/dist/server/serverHelpers/NodeAdapterResponse.d.ts +48 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js +220 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -0
- package/dist/server/serverHelpers/Request.d.ts +5 -10
- package/dist/server/serverHelpers/Request.js +38 -136
- package/dist/server/serverHelpers/Request.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.d.ts +11 -0
- package/dist/server/serverHelpers/contentTypes.js +221 -40
- 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 +178 -29
- 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 +17 -1
- package/dist/server/storageReclamation.js.map +1 -1
- package/dist/server/threads/isolatedApplications.d.ts +47 -0
- package/dist/server/threads/isolatedApplications.js +171 -0
- package/dist/server/threads/isolatedApplications.js.map +1 -0
- 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/logRotationTransport.d.ts +1 -0
- package/dist/server/threads/logRotationTransport.js +33 -0
- package/dist/server/threads/logRotationTransport.js.map +1 -0
- package/dist/server/threads/manageThreads.d.ts +83 -3
- package/dist/server/threads/manageThreads.js +769 -75
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/socketRouter.d.ts +1 -0
- package/dist/server/threads/socketRouter.js +277 -31
- 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 +76 -22
- 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 +33 -0
- package/dist/utility/errors/hdbError.js +58 -1
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/globalSchema.d.ts +18 -0
- package/dist/utility/hdbTerms.d.ts +18 -0
- package/dist/utility/hdbTerms.js +20 -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 +286 -30
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/logging/logGenerationCoordinator.d.ts +35 -0
- package/dist/utility/logging/logGenerationCoordinator.js +184 -0
- package/dist/utility/logging/logGenerationCoordinator.js.map +1 -0
- package/dist/utility/logging/logRotation.d.ts +46 -0
- package/dist/utility/logging/logRotation.js +365 -0
- package/dist/utility/logging/logRotation.js.map +1 -0
- package/dist/utility/logging/logRotator.d.ts +1 -1
- package/dist/utility/logging/logRotator.js +192 -85
- 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 +11 -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 +41 -0
- package/dist/utility/watcherFallback.js +153 -0
- package/dist/utility/watcherFallback.js.map +1 -1
- package/dist/validation/configValidator.d.ts +12 -0
- package/dist/validation/configValidator.js +205 -75
- 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 +8 -0
- package/json/systemSchema.json +55 -0
- package/npm-shrinkwrap.json +286 -194
- package/package.json +15 -7
- package/resources/DESIGN.md +219 -52
- package/resources/DatabaseTransaction.ts +657 -52
- package/resources/LMDBTransaction.ts +46 -6
- package/resources/PrimaryRocksDatabase.ts +46 -7
- package/resources/RecordEncoder.ts +124 -9
- package/resources/RequestTarget.ts +2 -0
- package/resources/Resource.ts +114 -22
- package/resources/ResourceInterface.ts +31 -0
- package/resources/RocksIndexStore.ts +30 -9
- package/resources/RocksTransactionLogStore.ts +188 -51
- package/resources/Table.ts +1960 -301
- package/resources/analytics/write.ts +10 -3
- package/resources/auditStore.ts +647 -32
- package/resources/blob.ts +1029 -110
- package/resources/branchDatabase.ts +941 -0
- package/resources/crdt.ts +76 -12
- package/resources/dataLoader.ts +3 -4
- package/resources/databases.ts +1584 -320
- package/resources/defineTable.ts +18 -2
- package/resources/derivedIndexRegistry.ts +56 -0
- package/resources/derivedIndexRuntime.ts +2292 -0
- package/resources/graphql.ts +72 -17
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +911 -48
- package/resources/indexes/hnswDerivedIndex.ts +531 -0
- package/resources/indexes/hnswPlaneBinding.ts +174 -0
- package/resources/longLivedTransactions.ts +360 -0
- package/resources/models/backendRegistry.ts +84 -2
- package/resources/models/bootstrap.ts +473 -28
- package/resources/nodeIdMapping.ts +50 -0
- package/resources/recordLock.ts +419 -0
- package/resources/recordLockCoordinator.ts +3043 -0
- package/resources/replayLogs.ts +152 -26
- package/resources/replayLogsGuards.ts +171 -8
- package/resources/replicatedApplyFailure.ts +77 -0
- package/resources/scheduler/scheduler.ts +4 -4
- package/resources/search.ts +288 -46
- package/resources/tracked.ts +73 -22
- 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 +83 -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 +194 -16
- package/server/DurableSubscriptionsSession.ts +71 -11
- package/server/REST.ts +115 -4
- package/server/graphqlQuerying.ts +4 -0
- package/server/http.ts +133 -20
- 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 +135 -0
- package/server/serverHelpers/NodeAdapterResponse.ts +221 -0
- package/server/serverHelpers/Request.ts +33 -131
- package/server/serverHelpers/contentTypes.ts +217 -36
- 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 +232 -40
- package/server/serverHelpers/sharedMessageEncoding.ts +307 -0
- package/server/serverHelpers/uwsServer.ts +17 -2
- package/server/static.ts +23 -29
- package/server/storageReclamation.ts +15 -2
- package/server/threads/isolatedApplications.ts +157 -0
- package/server/threads/itc.js +11 -1
- package/server/threads/logRotationTransport.ts +40 -0
- package/server/threads/manageThreads.js +771 -66
- package/server/threads/socketRouter.ts +291 -30
- package/server/threads/threadHeapMemory.ts +26 -0
- package/server/threads/threadServer.js +73 -22
- package/sqlTranslator/index.ts +31 -8
- package/studio/web/assets/{Chat-4RrB5134.js → Chat-D3j-1yY1.js} +1 -1
- package/studio/web/assets/{FloatingChat-omlNMDcJ.js → FloatingChat-BxJGYcfB.js} +3 -3
- package/studio/web/assets/{apiToken-Bke3wvfZ.js → apiToken-CT55oWOe.js} +1 -1
- package/studio/web/assets/{applications-DvFDYJgK.js → applications-D9Ct9_vm.js} +1 -1
- package/studio/web/assets/{cssMode-C1vRa7zh.js → cssMode-DV8H7VwA.js} +1 -1
- package/studio/web/assets/{editor-3XRWEDWX.js → editor-uatc0unt.js} +1 -1
- package/studio/web/assets/{html-CTY5tdMr.js → html-Bm6D6paN.js} +1 -1
- package/studio/web/assets/{htmlMode-CG1vSD9t.js → htmlMode-CEn7tpLG.js} +1 -1
- package/studio/web/assets/{index-Dfpeofdu.js → index-BIXW6Pu4.js} +5 -5
- package/studio/web/assets/{index.lazy-B1VOIv-t.js → index.lazy-UI7L-Vrk.js} +1 -1
- package/studio/web/assets/{javascript-CJeJzGnI.js → javascript-CJ0G3AFZ.js} +1 -1
- package/studio/web/assets/{jsonMode-CYPBwM82.js → jsonMode-DQADAYEa.js} +1 -1
- package/studio/web/assets/{languageServices-IKH4GUHl.js → languageServices-CAQJXWcI.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-BC8_gxKG.js → lspLanguageFeatures-CCQ8P5sY.js} +1 -1
- package/studio/web/assets/{notifications-BfKBpYcq.js → notifications-BbxTU6Aw.js} +1 -1
- package/studio/web/assets/{notifications-DFbArTfC.js → notifications-Cvb3P1lB.js} +1 -1
- package/studio/web/assets/{profile-Q4-T6c-S.js → profile-Yyb7gsvL.js} +1 -1
- package/studio/web/assets/{regions-CtkV0xje.js → regions-OgjGHlU5.js} +1 -1
- package/studio/web/assets/{register-1ZZuMsiA.js → register-6qwNEOY3.js} +2 -2
- package/studio/web/assets/{setComponentFile-ZWLcWv5X.js → setComponentFile-BilDMtgB.js} +1 -1
- package/studio/web/assets/{setup-DWprJyJy.js → setup-J6qJ7OIU.js} +2 -2
- package/studio/web/assets/{status--aNm8isn.js → status-0RWGcfyD.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-Cq_lt3XD.js → toggleHighContrast-BIn-vErT.js} +1 -1
- package/studio/web/assets/{tsMode-pjgytARx.js → tsMode-DgUXku4d.js} +1 -1
- package/studio/web/assets/{typescript-Cdg0mqUh.js → typescript-C9orXcsM.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-RZhaY8Rn.js → useEntityRestURL-BEoXXbUB.js} +1 -1
- package/studio/web/assets/{workers-BgoXIqQe.js → workers-JVzSDmgx.js} +1 -1
- package/studio/web/assets/{xml-BwSeDMiP.js → xml-Cq-S8S4X.js} +1 -1
- package/studio/web/assets/{yaml-DotCUG5l.js → yaml-sfoRdh1M.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 +57 -0
- package/utility/hdbTerms.ts +19 -0
- package/utility/logging/harper_logger.ts +278 -26
- package/utility/logging/logGenerationCoordinator.ts +196 -0
- package/utility/logging/logRotation.ts +367 -0
- package/utility/logging/logRotator.ts +213 -81
- package/utility/nodeIdentity.ts +45 -0
- package/utility/npmUtilities.ts +12 -8
- package/utility/operationPermissions.ts +18 -1
- package/utility/operation_authorization.ts +231 -42
- package/utility/watchPath.ts +63 -0
- package/utility/watcherFallback.ts +148 -0
- package/validation/configValidator.ts +215 -75
- package/validation/installValidator.ts +15 -0
- package/validation/validationWrapper.ts +18 -4
package/resources/databases.ts
CHANGED
|
@@ -3,14 +3,23 @@ import { randomBytes } from 'node:crypto';
|
|
|
3
3
|
import { initSync, getHdbBasePath, get as envGet } from '../utility/environment/environmentManager.ts';
|
|
4
4
|
import { INTERNAL_DBIS_NAME } from '../utility/lmdb/terms.ts';
|
|
5
5
|
import { open, compareKeys, type Database, type RootDatabase } from 'lmdb';
|
|
6
|
-
import { join, extname, basename } from 'path';
|
|
7
|
-
import {
|
|
6
|
+
import { join, extname, basename } from 'node:path';
|
|
7
|
+
import {
|
|
8
|
+
closeSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
openSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
readdirSync,
|
|
14
|
+
realpathSync,
|
|
15
|
+
unlinkSync,
|
|
16
|
+
} from 'node:fs';
|
|
8
17
|
import { unlink } from 'node:fs/promises';
|
|
9
18
|
import {
|
|
10
19
|
getBaseSchemaPath,
|
|
11
20
|
getTransactionAuditStoreBasePath,
|
|
12
21
|
} from '../dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js';
|
|
13
|
-
import { makeTable, ignoreAlreadyDropped } from './Table.ts';
|
|
22
|
+
import { makeTable, ignoreAlreadyDropped, acquireUpdateAttributesLock, releaseUpdateAttributesLock } from './Table.ts';
|
|
14
23
|
import OpenEnvironmentObject from '../utility/lmdb/OpenEnvironmentObject.ts';
|
|
15
24
|
import {
|
|
16
25
|
CONFIG_PARAMS,
|
|
@@ -29,15 +38,24 @@ import { workerData } from 'worker_threads';
|
|
|
29
38
|
import harperLogger from '../utility/logging/harper_logger.ts';
|
|
30
39
|
const { forComponent } = harperLogger;
|
|
31
40
|
import * as manageThreads from '../server/threads/manageThreads.js';
|
|
32
|
-
import {
|
|
33
|
-
|
|
41
|
+
import {
|
|
42
|
+
establishAuditFloor,
|
|
43
|
+
openAuditStore,
|
|
44
|
+
readAuditEntry,
|
|
45
|
+
createAuditEntry,
|
|
46
|
+
type AuditRecord,
|
|
47
|
+
} from './auditStore.ts';
|
|
34
48
|
import { handleLocalTimeForGets } from './RecordEncoder.ts';
|
|
35
|
-
import { deleteRootBlobPathsForDB } from './blob.ts';
|
|
49
|
+
import { databasePaths, deleteRootBlobPathsForDB } from './blob.ts';
|
|
50
|
+
import { removeStorageReclamation } from '../server/storageReclamation.ts';
|
|
51
|
+
import { commonValidators, schemaRegex } from '../validation/common_validators.ts';
|
|
36
52
|
import { CUSTOM_INDEXES } from './indexes/customIndexes.ts';
|
|
53
|
+
import { planeFilePathFor, planeStalePathFor } from './indexes/hnswPlaneBinding.ts';
|
|
37
54
|
import { OpenDBIObject } from '../utility/lmdb/OpenDBIObject.ts';
|
|
38
55
|
import { RocksDatabase, supportedCompression, type RocksDatabaseOptions } from '@harperfast/rocksdb-js';
|
|
39
56
|
import { PrimaryRocksDatabase } from './PrimaryRocksDatabase.ts';
|
|
40
57
|
import { replayLogs } from './replayLogs.ts';
|
|
58
|
+
import { attachDerivedIndexes } from './indexes/hnswDerivedIndex.ts';
|
|
41
59
|
import { totalmem } from 'node:os';
|
|
42
60
|
import { RocksIndexStore } from './RocksIndexStore.ts';
|
|
43
61
|
import { resolveRocksMemoryConfig } from '../utility/rocksMemoryConfig.ts';
|
|
@@ -97,8 +115,74 @@ const logger = forComponent('storage');
|
|
|
97
115
|
|
|
98
116
|
const DEFAULT_DATABASE_NAME = 'data';
|
|
99
117
|
const DEFINED_TABLES = Symbol('defined-tables');
|
|
118
|
+
const CATALOG_RELATIONSHIP = Symbol('catalog-relationship');
|
|
100
119
|
const DEFAULT_COMPRESSION_THRESHOLD = (envGet(CONFIG_PARAMS.STORAGE_PAGESIZE) || 4096) - 60; // larger than this requires multiple pages
|
|
101
120
|
initSync();
|
|
121
|
+
|
|
122
|
+
type RelationshipTarget = { database: string; table: string };
|
|
123
|
+
type PersistedRelationship = {
|
|
124
|
+
name: string;
|
|
125
|
+
type: string;
|
|
126
|
+
elements?: { type: string };
|
|
127
|
+
relationship: { from?: string; to?: string; filterMissing?: boolean };
|
|
128
|
+
target: RelationshipTarget;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
type RelationshipHydration = {
|
|
132
|
+
table: any;
|
|
133
|
+
databaseName: string;
|
|
134
|
+
tableName: string;
|
|
135
|
+
definitions: unknown[];
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
let relationshipsToHydrate: RelationshipHydration[] = [];
|
|
139
|
+
const reportedRelationshipErrors = new Set<string>();
|
|
140
|
+
// an interrupted create is reported once per table and thread, not on every rescan
|
|
141
|
+
const reportedIncompleteCatalogs = new Set<string>();
|
|
142
|
+
|
|
143
|
+
function normalizeRelationships(attributes: any[]): PersistedRelationship[] {
|
|
144
|
+
const relationships: PersistedRelationship[] = [];
|
|
145
|
+
for (const attribute of attributes) {
|
|
146
|
+
const target = attribute.relationshipReference;
|
|
147
|
+
if (!attribute.relationship || !target) continue;
|
|
148
|
+
const relationship: PersistedRelationship['relationship'] = {};
|
|
149
|
+
if (typeof attribute.relationship.from === 'string') relationship.from = attribute.relationship.from;
|
|
150
|
+
if (typeof attribute.relationship.to === 'string') relationship.to = attribute.relationship.to;
|
|
151
|
+
// the GraphQL parser hands every directive argument over as a string, and the resolver reads
|
|
152
|
+
// filterMissing for truthiness, so persist what the resolver would see rather than the literal
|
|
153
|
+
if (attribute.relationship.filterMissing !== undefined)
|
|
154
|
+
relationship.filterMissing = Boolean(attribute.relationship.filterMissing);
|
|
155
|
+
if (!relationship.from && !relationship.to) continue;
|
|
156
|
+
const definition: PersistedRelationship = {
|
|
157
|
+
name: attribute.name,
|
|
158
|
+
type: attribute.type,
|
|
159
|
+
relationship,
|
|
160
|
+
target: { database: target.database, table: target.table },
|
|
161
|
+
};
|
|
162
|
+
if (attribute.type === 'array') definition.elements = { type: attribute.elements?.type };
|
|
163
|
+
relationships.push(definition);
|
|
164
|
+
}
|
|
165
|
+
return relationships;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function relationshipEquals(left: any, right: any): boolean {
|
|
169
|
+
return (
|
|
170
|
+
left?.name === right?.name &&
|
|
171
|
+
left?.type === right?.type &&
|
|
172
|
+
left?.elements?.type === right?.elements?.type &&
|
|
173
|
+
left?.relationship?.from === right?.relationship?.from &&
|
|
174
|
+
left?.relationship?.to === right?.relationship?.to &&
|
|
175
|
+
left?.relationship?.filterMissing === right?.relationship?.filterMissing &&
|
|
176
|
+
left?.target?.database === right?.target?.database &&
|
|
177
|
+
left?.target?.table === right?.target?.table
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function relationshipListsEqual(left: any, right: PersistedRelationship[]): boolean {
|
|
182
|
+
if (!Array.isArray(left) || left.length !== right.length) return false;
|
|
183
|
+
for (let index = 0; index < right.length; index++) if (!relationshipEquals(left[index], right[index])) return false;
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
102
186
|
/**
|
|
103
187
|
* The RocksDB block/blob codec for every column family this process opens (`storage.rocks.compression`),
|
|
104
188
|
* or `undefined` to leave rocksdb-js on its own default (lz4 wherever the native build has it).
|
|
@@ -332,9 +416,13 @@ function openRocksDatabase(path: string, options: RocksDatabaseOptions & { dupSo
|
|
|
332
416
|
} else {
|
|
333
417
|
db = new PrimaryRocksDatabase(path, options).open() as unknown as RocksRootDatabase;
|
|
334
418
|
// the RocksDB put and remove return promises, which masks thrown errors in non-awaiting calls to put/remove,
|
|
335
|
-
// making them unsafe to replace LMDB methods, which will synchronously throw errors if there is a problem
|
|
419
|
+
// making them unsafe to replace LMDB methods, which will synchronously throw errors if there is a problem.
|
|
420
|
+
// The versioned remove is necessarily async and its callers must await or otherwise track its promise.
|
|
336
421
|
db.put = db.putSync as any;
|
|
337
|
-
db.remove =
|
|
422
|
+
db.remove = ((id: any, removeOptions?: any) =>
|
|
423
|
+
typeof removeOptions === 'number'
|
|
424
|
+
? (db as unknown as PrimaryRocksDatabase).removeIfVersion(id, removeOptions)
|
|
425
|
+
: db.removeSync(id, removeOptions)) as any;
|
|
338
426
|
(db.encoder as any).name = options.name;
|
|
339
427
|
}
|
|
340
428
|
db.env = {};
|
|
@@ -349,6 +437,33 @@ _assignPackageExport('databases', databases);
|
|
|
349
437
|
_assignPackageExport('tables', tables);
|
|
350
438
|
|
|
351
439
|
const NEXT_TABLE_ID = Symbol.for('next-table-id');
|
|
440
|
+
// Restore every field used by `commonChanged`, plus `indexed` and `indexNulls`,
|
|
441
|
+
// from the durable descriptor. In particular, preserve `indexNulls: false` so
|
|
442
|
+
// an index that excludes nulls is not reopened as though it contains them.
|
|
443
|
+
const PEER_REDEFINABLE_FIELDS = [
|
|
444
|
+
'type',
|
|
445
|
+
'indexed',
|
|
446
|
+
'indexNulls',
|
|
447
|
+
'nullable',
|
|
448
|
+
'enumerable',
|
|
449
|
+
'version',
|
|
450
|
+
'elements',
|
|
451
|
+
'properties',
|
|
452
|
+
'embed',
|
|
453
|
+
];
|
|
454
|
+
// `indexNulls` is derived from the durable descriptor, never sent by a peer, so naming it in the
|
|
455
|
+
// discard warn would blame the peer for a field it did not write.
|
|
456
|
+
const PEER_DECLARABLE_FIELDS = PEER_REDEFINABLE_FIELDS.filter((field) => field !== 'indexNulls');
|
|
457
|
+
|
|
458
|
+
// A cluster-origin caller's list can predate a declaration another thread has already committed, so on
|
|
459
|
+
// that path the descriptor — not the caller — decides what the attribute is, in both directions.
|
|
460
|
+
function applyDurableDeclaration(attribute: any, descriptor: any) {
|
|
461
|
+
for (const field of PEER_REDEFINABLE_FIELDS) {
|
|
462
|
+
if (field in descriptor) attribute[field] = descriptor[field];
|
|
463
|
+
else delete attribute[field];
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
352
467
|
/**
|
|
353
468
|
* True when a descriptor claims an index build no live operation in this process can own. The PID and
|
|
354
469
|
* worker generation cannot answer that alone: a container reuses PID 1 and starts the in-memory
|
|
@@ -444,6 +559,7 @@ export function getDatabases(): Databases {
|
|
|
444
559
|
loadedDatabases = true;
|
|
445
560
|
|
|
446
561
|
definedDatabases = new Map();
|
|
562
|
+
relationshipsToHydrate = [];
|
|
447
563
|
const hdbBasePath = getHdbBasePath();
|
|
448
564
|
let databasePath = hdbBasePath && join(hdbBasePath, DATABASES_DIR_NAME);
|
|
449
565
|
const schemaConfigs = envGet(CONFIG_PARAMS.DATABASES) || {};
|
|
@@ -466,9 +582,12 @@ export function getDatabases(): Databases {
|
|
|
466
582
|
// (out-of-band) RocksDB directory happens to occupy that reserved name — the API can't
|
|
467
583
|
// create it (schemaRegex forbids the backtick), but the scan opens any CURRENT+MANIFEST dir
|
|
468
584
|
if (databaseEntry.name === RESTORE_META_DIR) continue;
|
|
585
|
+
// branch directories are process-local derivatives, never databases in their own right
|
|
586
|
+
if (databaseEntry.name === BRANCH_ROOT_DIR) continue;
|
|
469
587
|
const dbName = basename(databaseEntry.name, '.mdb');
|
|
470
588
|
const dbPath = join(databasePath, databaseEntry.name);
|
|
471
589
|
if (blockedByRestore.has(dbName)) continue;
|
|
590
|
+
if (isOpenBranchPath(dbPath)) continue;
|
|
472
591
|
|
|
473
592
|
if (
|
|
474
593
|
databaseEntry.isFile() &&
|
|
@@ -530,7 +649,9 @@ export function getDatabases(): Databases {
|
|
|
530
649
|
for (const databaseEntry of entries) {
|
|
531
650
|
if (databaseEntry.name.endsWith(MIGRATING_DIR_SUFFIX)) continue; // migration staging dir
|
|
532
651
|
if (databaseEntry.name === RESTORE_META_DIR) continue; // reserved restore-metadata dir
|
|
652
|
+
if (databaseEntry.name === BRANCH_ROOT_DIR) continue; // reserved branch root
|
|
533
653
|
if (blockedByRestore.has(basename(databaseEntry.name, '.mdb'))) continue;
|
|
654
|
+
if (isOpenBranchPath(join(databasePath, databaseEntry.name))) continue;
|
|
534
655
|
if (databaseEntry.isFile() && extname(databaseEntry.name).toLowerCase() === '.mdb') {
|
|
535
656
|
readMetaDb(join(databasePath, databaseEntry.name), basename(databaseEntry.name, '.mdb'), dbName);
|
|
536
657
|
} else {
|
|
@@ -575,10 +696,13 @@ export function getDatabases(): Databases {
|
|
|
575
696
|
for (const tableName in tables) {
|
|
576
697
|
if (!definedTables.has(tableName)) {
|
|
577
698
|
logger.trace(`delete table class ${tableName}`);
|
|
699
|
+
tables[tableName]?.cleanup?.();
|
|
578
700
|
delete tables[tableName];
|
|
579
701
|
}
|
|
580
702
|
}
|
|
581
703
|
} else {
|
|
704
|
+
const removedTables = databases[dbName];
|
|
705
|
+
for (const tableName in removedTables) removedTables[tableName]?.cleanup?.();
|
|
582
706
|
delete databases[dbName];
|
|
583
707
|
if (dbName === 'data') {
|
|
584
708
|
for (const tableName in tables) {
|
|
@@ -588,6 +712,7 @@ export function getDatabases(): Databases {
|
|
|
588
712
|
}
|
|
589
713
|
}
|
|
590
714
|
}
|
|
715
|
+
hydrateCatalogRelationships();
|
|
591
716
|
if (envGet(CONFIG_PARAMS.ANALYTICS_REPLICATE) === false) {
|
|
592
717
|
if (!NON_REPLICATING_SYSTEM_TABLES.includes('hdb_analytics')) NON_REPLICATING_SYSTEM_TABLES.push('hdb_analytics');
|
|
593
718
|
} else {
|
|
@@ -605,6 +730,171 @@ export function getDatabases(): Databases {
|
|
|
605
730
|
return databases;
|
|
606
731
|
}
|
|
607
732
|
|
|
733
|
+
/**
|
|
734
|
+
* Hydrate one branch's relationships, resolving each target against the application's own branches
|
|
735
|
+
* first and only then against the real databases: a target the application also branched must be its
|
|
736
|
+
* branch's table, and a target it did not branch is legitimately the shared one.
|
|
737
|
+
*/
|
|
738
|
+
export function hydrateBranchRelationships(branch: BranchDatabase, branches: Map<string, BranchDatabase>): void {
|
|
739
|
+
const resolveTarget: ResolveRelationshipTarget = (target) => {
|
|
740
|
+
const targetBranch = branches.get(target.database);
|
|
741
|
+
// A branched target resolves ONLY within that branch. A durable branch is a checkpoint frozen
|
|
742
|
+
// at creation while the base keeps evolving, so falling through to the base for a table the
|
|
743
|
+
// branch's own copy lacks would point a branched application's relationship reads at live base
|
|
744
|
+
// data -- the fallback belongs to a database the application did not branch, never to one it did.
|
|
745
|
+
return targetBranch ? targetBranch.tables?.[target.table] : databases[target.database]?.[target.table];
|
|
746
|
+
};
|
|
747
|
+
// Kept, not drained, like the global list: a target declared later (on this or another thread) is
|
|
748
|
+
// picked up by the next pass, and `hydrateTableRelationships` is a no-op once everything resolves.
|
|
749
|
+
for (const hydration of branch.pendingRelationships) {
|
|
750
|
+
try {
|
|
751
|
+
hydrateTableRelationships(hydration, resolveTarget, false);
|
|
752
|
+
} catch (error) {
|
|
753
|
+
logger.error(
|
|
754
|
+
`Unable to hydrate persisted relationships for branch table ${hydration.databaseName}.${hydration.tableName}`,
|
|
755
|
+
error
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function hydrateCatalogRelationships(): void {
|
|
762
|
+
for (const hydration of relationshipsToHydrate) {
|
|
763
|
+
try {
|
|
764
|
+
hydrateTableRelationships(hydration);
|
|
765
|
+
} catch (error) {
|
|
766
|
+
const key = `${hydration.databaseName}.${hydration.tableName}:hydrate`;
|
|
767
|
+
if (!reportedRelationshipErrors.has(key)) {
|
|
768
|
+
reportedRelationshipErrors.add(key);
|
|
769
|
+
logger.error(
|
|
770
|
+
`Unable to hydrate persisted relationships for ${hydration.databaseName}.${hydration.tableName}`,
|
|
771
|
+
error
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
type ResolveRelationshipTarget = (target: RelationshipTarget) => any;
|
|
779
|
+
|
|
780
|
+
const resolveTargetGlobally: ResolveRelationshipTarget = (target) => databases[target.database]?.[target.table];
|
|
781
|
+
|
|
782
|
+
function hydrateTableRelationships(
|
|
783
|
+
{ table, databaseName, tableName, definitions }: RelationshipHydration,
|
|
784
|
+
resolveTarget: ResolveRelationshipTarget = resolveTargetGlobally,
|
|
785
|
+
announce = true
|
|
786
|
+
): void {
|
|
787
|
+
const hydratable: { definition: PersistedRelationship; targetTable: any }[] = [];
|
|
788
|
+
for (let index = 0; index < definitions.length; index++) {
|
|
789
|
+
const definition = definitions[index] as PersistedRelationship;
|
|
790
|
+
// Keyed by name rather than list position, so a reordered list cannot inherit the previous
|
|
791
|
+
// occupant's reported state and swallow a different relationship's failure — and by reason, so
|
|
792
|
+
// hydrating one entry does not clear the report of a same-named invalid duplicate.
|
|
793
|
+
const errorKey = `${databaseName}.${tableName}:${(definition as any)?.name || `#${index}`}`;
|
|
794
|
+
if (!validRelationshipDefinition(definition, definitions, index)) {
|
|
795
|
+
reportRelationshipError(
|
|
796
|
+
`${errorKey}:invalid`,
|
|
797
|
+
`Ignoring invalid persisted relationship ${databaseName}.${tableName}[${index}]`
|
|
798
|
+
);
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
// a live schema attribute of the same name owns the name; the catalog copy is only a stand-in
|
|
802
|
+
// for threads that never loaded the schema
|
|
803
|
+
if (table.attributes.some((attribute) => attribute.name === definition.name && !attribute[CATALOG_RELATIONSHIP]))
|
|
804
|
+
continue;
|
|
805
|
+
const targetTable = resolveTarget(definition.target);
|
|
806
|
+
if (!targetTable || !relationshipFieldsExist(table, targetTable, definition)) {
|
|
807
|
+
reportRelationshipError(
|
|
808
|
+
`${errorKey}:unavailable`,
|
|
809
|
+
`Unable to hydrate persisted relationship ${databaseName}.${tableName}.${definition.name}: target or foreign key is unavailable`
|
|
810
|
+
);
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
reportedRelationshipErrors.delete(`${errorKey}:unavailable`);
|
|
814
|
+
hydratable.push({ definition, targetTable });
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const installed = table.attributes.filter((attribute) => attribute[CATALOG_RELATIONSHIP]);
|
|
818
|
+
if (
|
|
819
|
+
installed.length === hydratable.length &&
|
|
820
|
+
hydratable.every(
|
|
821
|
+
({ definition, targetTable }, index) =>
|
|
822
|
+
relationshipEquals(installed[index], definition) &&
|
|
823
|
+
(installed[index].definition || installed[index].elements?.definition)?.tableClass === targetTable
|
|
824
|
+
)
|
|
825
|
+
)
|
|
826
|
+
return;
|
|
827
|
+
|
|
828
|
+
const attributes = table.attributes.filter((attribute) => !attribute[CATALOG_RELATIONSHIP]);
|
|
829
|
+
for (const { definition, targetTable } of hydratable)
|
|
830
|
+
attributes.push(createCatalogRelationship(definition, targetTable));
|
|
831
|
+
table.attributes.splice(0, table.attributes.length, ...attributes);
|
|
832
|
+
table.schemaVersion++;
|
|
833
|
+
table.updatedAttributes();
|
|
834
|
+
if (announce) databaseEventsEmitter.emit('updateTable', table);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function validRelationshipDefinition(definition: any, definitions: unknown[], index: number): boolean {
|
|
838
|
+
if (!definition || typeof definition !== 'object') return false;
|
|
839
|
+
const validName = (value: any) => typeof value === 'string' && value.length > 0 && !/[`/]/.test(value);
|
|
840
|
+
if (!validName(definition.name) || !validName(definition.type)) return false;
|
|
841
|
+
if (!validName(definition.target?.database) || !validName(definition.target?.table)) return false;
|
|
842
|
+
if (!definition.relationship || typeof definition.relationship !== 'object') return false;
|
|
843
|
+
const { from, to, filterMissing } = definition.relationship;
|
|
844
|
+
if (from !== undefined && !validName(from)) return false;
|
|
845
|
+
if (to !== undefined && !validName(to)) return false;
|
|
846
|
+
if (!from && !to) return false;
|
|
847
|
+
if (filterMissing !== undefined && typeof filterMissing !== 'boolean') return false;
|
|
848
|
+
if (definition.type === 'array' ? !validName(definition.elements?.type) : definition.elements !== undefined)
|
|
849
|
+
return false;
|
|
850
|
+
for (let earlier = 0; earlier < index; earlier++)
|
|
851
|
+
if ((definitions[earlier] as any)?.name === definition.name) return false;
|
|
852
|
+
return true;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function relationshipFieldsExist(sourceTable: any, targetTable: any, definition: PersistedRelationship): boolean {
|
|
856
|
+
if (
|
|
857
|
+
definition.relationship.from &&
|
|
858
|
+
!sourceTable.attributes.some((attribute) => attribute.name === definition.relationship.from)
|
|
859
|
+
)
|
|
860
|
+
return false;
|
|
861
|
+
if (
|
|
862
|
+
definition.relationship.to &&
|
|
863
|
+
!targetTable.attributes.some((attribute) => attribute.name === definition.relationship.to)
|
|
864
|
+
)
|
|
865
|
+
return false;
|
|
866
|
+
return true;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function createCatalogRelationship(definition: PersistedRelationship, targetTable: any): any {
|
|
870
|
+
const attribute: any = {
|
|
871
|
+
name: definition.name,
|
|
872
|
+
attribute: definition.name,
|
|
873
|
+
type: definition.type,
|
|
874
|
+
relationship: { ...definition.relationship },
|
|
875
|
+
target: { ...definition.target },
|
|
876
|
+
};
|
|
877
|
+
const targetDefinition = {
|
|
878
|
+
tableClass: targetTable,
|
|
879
|
+
type: targetTable.tableName,
|
|
880
|
+
attributes: targetTable.attributes,
|
|
881
|
+
};
|
|
882
|
+
if (definition.elements) {
|
|
883
|
+
attribute.elements = { type: definition.elements.type };
|
|
884
|
+
Object.defineProperty(attribute.elements, 'definition', { value: targetDefinition, configurable: true });
|
|
885
|
+
} else {
|
|
886
|
+
Object.defineProperty(attribute, 'definition', { value: targetDefinition, configurable: true });
|
|
887
|
+
}
|
|
888
|
+
Object.defineProperty(attribute, CATALOG_RELATIONSHIP, { value: true });
|
|
889
|
+
return attribute;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function reportRelationshipError(key: string, message: string): void {
|
|
893
|
+
if (reportedRelationshipErrors.has(key)) return;
|
|
894
|
+
reportedRelationshipErrors.add(key);
|
|
895
|
+
logger.error(message);
|
|
896
|
+
}
|
|
897
|
+
|
|
608
898
|
/**
|
|
609
899
|
* Scan a databases directory's entries for restore lock/marker files and return the names of
|
|
610
900
|
* databases that must not be loaded: a held restore lock means a restore is in progress in some
|
|
@@ -653,14 +943,19 @@ export function readMetaDb(
|
|
|
653
943
|
}
|
|
654
944
|
|
|
655
945
|
rootStore.dbisDb?.resetReadTxn();
|
|
656
|
-
return initStores(path, rootStore, databaseName, defaultTable, auditPath, isLegacy);
|
|
946
|
+
return initStores(path, rootStore, databaseName, { defaultTable, auditPath, isLegacy });
|
|
657
947
|
} catch (error) {
|
|
658
948
|
error.message += ` opening database ${path}`;
|
|
659
949
|
throw error;
|
|
660
950
|
}
|
|
661
951
|
}
|
|
662
952
|
|
|
663
|
-
function readRocksMetaDb(
|
|
953
|
+
function readRocksMetaDb(
|
|
954
|
+
path: string,
|
|
955
|
+
defaultTable?: string,
|
|
956
|
+
databaseName: string = DEFAULT_DATABASE_NAME,
|
|
957
|
+
{ destination, storeName, openedStores }: Pick<InitStoresOptions, 'destination' | 'storeName' | 'openedStores'> = {}
|
|
958
|
+
) {
|
|
664
959
|
try {
|
|
665
960
|
logger.trace(`loading rocksdb database: ${path}`);
|
|
666
961
|
|
|
@@ -673,13 +968,17 @@ function readRocksMetaDb(path: string, defaultTable?: string, databaseName: stri
|
|
|
673
968
|
|
|
674
969
|
let rootStore: RocksRootDatabase | undefined = rocksdbDatabaseEnvs.get(path);
|
|
675
970
|
if (rootStore) {
|
|
676
|
-
initStores(path, rootStore, databaseName, defaultTable);
|
|
971
|
+
initStores(path, rootStore, databaseName, { defaultTable, destination, storeName, openedStores });
|
|
677
972
|
} else {
|
|
678
973
|
rootStore = openRocksDatabase(path, { disableWAL: false, enableStats: true }) as any;
|
|
679
974
|
rocksdbDatabaseEnvs.set(path, rootStore);
|
|
680
|
-
initStores(path, rootStore, databaseName, defaultTable);
|
|
681
|
-
//
|
|
682
|
-
|
|
975
|
+
initStores(path, rootStore, databaseName, { defaultTable, destination, storeName, openedStores });
|
|
976
|
+
// A branch (`destination`) recovers its transaction-log tail in `openOrCreate`
|
|
977
|
+
// (branchDatabase.ts), not here: the branch claim elects exactly one replaying thread —
|
|
978
|
+
// applications load on workers, where this call would be a no-op — and awaits the replay
|
|
979
|
+
// before the branch is published to any reader. See the contract note on
|
|
980
|
+
// `openBranchDatabase` (harper#643).
|
|
981
|
+
if (!isReadOnlyMode() && !destination) {
|
|
683
982
|
replayLogs(rootStore, databases[databaseName]);
|
|
684
983
|
}
|
|
685
984
|
}
|
|
@@ -690,14 +989,32 @@ function readRocksMetaDb(path: string, defaultTable?: string, databaseName: stri
|
|
|
690
989
|
}
|
|
691
990
|
}
|
|
692
991
|
|
|
992
|
+
interface InitStoresOptions {
|
|
993
|
+
defaultTable?: string;
|
|
994
|
+
auditPath?: string;
|
|
995
|
+
isLegacy?: boolean;
|
|
996
|
+
/** Build the Table classes here instead of the global `databases` map, and emit no global event. */
|
|
997
|
+
destination?: Tables;
|
|
998
|
+
/**
|
|
999
|
+
* Identity stamped on the root store, when it must differ from the logical `databaseName` the
|
|
1000
|
+
* Table classes carry. `getRootBlobPathsForDB` resolves blob directories from it.
|
|
1001
|
+
*/
|
|
1002
|
+
storeName?: string;
|
|
1003
|
+
/**
|
|
1004
|
+
* Every column family opened here is appended, so a caller can release the ones a failure left
|
|
1005
|
+
* unreachable — a table's stores are opened well before `setTable` publishes it into the graph.
|
|
1006
|
+
*/
|
|
1007
|
+
openedStores?: any[];
|
|
1008
|
+
}
|
|
1009
|
+
|
|
693
1010
|
function initStores(
|
|
694
1011
|
path: string,
|
|
695
1012
|
rootStore: RootDatabaseKind,
|
|
696
1013
|
databaseName: string,
|
|
697
|
-
defaultTable
|
|
698
|
-
auditPath?: string,
|
|
699
|
-
isLegacy?: boolean
|
|
1014
|
+
{ defaultTable, auditPath, isLegacy, destination, storeName, openedStores }: InitStoresOptions = {}
|
|
700
1015
|
) {
|
|
1016
|
+
// a store with no tables never reaches the per-table loop below, and blob roots resolve from this
|
|
1017
|
+
rootStore.databaseName = storeName ?? databaseName;
|
|
701
1018
|
const envInit = new OpenEnvironmentObject(path, isReadOnlyMode());
|
|
702
1019
|
const internalDbiInit = createOpenDBIObject(false);
|
|
703
1020
|
let attributesDbi = rootStore.dbisDb;
|
|
@@ -711,6 +1028,7 @@ function initStores(
|
|
|
711
1028
|
} else {
|
|
712
1029
|
attributesDbi = rootStore.openDB(INTERNAL_DBIS_NAME, internalDbiInit as any);
|
|
713
1030
|
}
|
|
1031
|
+
openedStores?.push(attributesDbi);
|
|
714
1032
|
rootStore.dbisDb = markInternalDbiNonVersioned(attributesDbi);
|
|
715
1033
|
}
|
|
716
1034
|
|
|
@@ -731,13 +1049,17 @@ function initStores(
|
|
|
731
1049
|
}) as any;
|
|
732
1050
|
}
|
|
733
1051
|
auditStore.isLegacy = true;
|
|
1052
|
+
// A legacy standalone audit root skips openAuditStore, so give it a floor here or it
|
|
1053
|
+
// reports its retention horizon as permanently unknown.
|
|
1054
|
+
establishAuditFloor(auditStore);
|
|
734
1055
|
}
|
|
735
1056
|
} else {
|
|
736
1057
|
auditStore = openAuditStore(rootStore);
|
|
737
1058
|
}
|
|
738
1059
|
}
|
|
739
1060
|
|
|
740
|
-
const tables = ensureDB(databaseName);
|
|
1061
|
+
const tables = destination ?? ensureDB(databaseName);
|
|
1062
|
+
if (destination && !destination[DEFINED_TABLES]) destination[DEFINED_TABLES] = new Set<string>();
|
|
741
1063
|
const definedTables = tables[DEFINED_TABLES];
|
|
742
1064
|
(definedTables as any).rootStore = rootStore;
|
|
743
1065
|
const tablesToLoad = new Map<string, any>();
|
|
@@ -835,17 +1157,29 @@ function initStores(
|
|
|
835
1157
|
}
|
|
836
1158
|
}
|
|
837
1159
|
if (!primaryAttribute) {
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1160
|
+
const tableKey = `${databaseName}/${tableName}`;
|
|
1161
|
+
if (reportedIncompleteCatalogs.has(tableKey))
|
|
1162
|
+
logger.debug(`Skipping table ${databaseName}.${tableName}: still no primary key row`);
|
|
1163
|
+
else {
|
|
1164
|
+
reportedIncompleteCatalogs.add(tableKey);
|
|
1165
|
+
logger.warn(
|
|
1166
|
+
`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`
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
// not defined until it loads, so the cleanup pass evicts a class left from a dropped same-name table
|
|
1170
|
+
definedTables?.delete(tableName);
|
|
841
1171
|
continue;
|
|
842
1172
|
}
|
|
843
1173
|
}
|
|
1174
|
+
if (reportedIncompleteCatalogs.size) reportedIncompleteCatalogs.delete(`${databaseName}/${tableName}`);
|
|
844
1175
|
// if the table has already been defined, use that class, don't create a new one
|
|
845
1176
|
let table = tables[tableName];
|
|
846
1177
|
// unless its store was migrated to a different engine (e.g. LMDB to RocksDB on startup)
|
|
847
1178
|
const recreateForEngineChange =
|
|
848
1179
|
!!table && (table as any).primaryStore?.rootStore instanceof RocksDatabase !== rootStore instanceof RocksDatabase;
|
|
1180
|
+
const recreateForTableIdChange =
|
|
1181
|
+
!!table && primaryAttribute.tableId != null && table.tableId !== primaryAttribute.tableId;
|
|
1182
|
+
const recreateTable = recreateForEngineChange || recreateForTableIdChange;
|
|
849
1183
|
let indices = {},
|
|
850
1184
|
existingAttributes = [];
|
|
851
1185
|
let tableId;
|
|
@@ -859,7 +1193,8 @@ function initStores(
|
|
|
859
1193
|
const cacheControl = primaryAttribute.cacheControl;
|
|
860
1194
|
const splitSegments = primaryAttribute.splitSegments;
|
|
861
1195
|
const replicate = primaryAttribute.replicate;
|
|
862
|
-
if (table && !
|
|
1196
|
+
if (table && !recreateTable) {
|
|
1197
|
+
if (primaryAttribute.audit === true && table.audit !== true) table.enableAuditing();
|
|
863
1198
|
indices = table.indices;
|
|
864
1199
|
existingAttributes = table.attributes;
|
|
865
1200
|
table.schemaVersion++;
|
|
@@ -887,18 +1222,14 @@ function initStores(
|
|
|
887
1222
|
// per-table override of the storage.randomAccessFields default (see OpenDBIObject)
|
|
888
1223
|
if (typeof primaryAttribute.randomAccessFields === 'boolean')
|
|
889
1224
|
dbiInit.randomAccessStructure = primaryAttribute.randomAccessFields;
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
rootStore
|
|
899
|
-
);
|
|
900
|
-
}
|
|
901
|
-
rootStore.databaseName = databaseName;
|
|
1225
|
+
// recorded before the wrapper below, which is the only thing between the native open and
|
|
1226
|
+
// the only list a failed open can release it from
|
|
1227
|
+
const opened =
|
|
1228
|
+
rootStore instanceof RocksDatabase
|
|
1229
|
+
? openRocksDatabase(rootStore.path, { ...dbiInit, name: primaryAttribute.key, cache: true } as any)
|
|
1230
|
+
: (rootStore as any).openDB(primaryAttribute.key, dbiInit as any);
|
|
1231
|
+
openedStores?.push(opened);
|
|
1232
|
+
primaryStore = handleLocalTimeForGets(opened, rootStore);
|
|
902
1233
|
primaryStore.tableId = tableId;
|
|
903
1234
|
}
|
|
904
1235
|
let attributesUpdated: boolean;
|
|
@@ -909,6 +1240,7 @@ function initStores(
|
|
|
909
1240
|
if (!attribute.isPrimaryKey && (attribute.indexed || (attribute.attribute && !attribute.name))) {
|
|
910
1241
|
if (!indices[attribute.name]) {
|
|
911
1242
|
const dbi = openIndex(attribute.key, rootStore, attribute);
|
|
1243
|
+
openedStores?.push(dbi);
|
|
912
1244
|
indices[attribute.name] = dbi;
|
|
913
1245
|
indices[attribute.name].indexNulls = attribute.indexNulls;
|
|
914
1246
|
}
|
|
@@ -979,16 +1311,19 @@ function initStores(
|
|
|
979
1311
|
existingAttributes.splice(existingAttributes.indexOf(existingAttribute), 1);
|
|
980
1312
|
attributesUpdated = true;
|
|
981
1313
|
}
|
|
982
|
-
if (table && !
|
|
1314
|
+
if (table && !recreateTable) {
|
|
983
1315
|
if (attributesUpdated) {
|
|
984
1316
|
table.schemaVersion++;
|
|
985
1317
|
table.updatedAttributes();
|
|
986
1318
|
}
|
|
987
1319
|
} else {
|
|
1320
|
+
if (recreateForTableIdChange) table.cleanup();
|
|
988
1321
|
table = setTable(
|
|
989
1322
|
tables,
|
|
990
1323
|
tableName,
|
|
991
1324
|
makeTable({
|
|
1325
|
+
// A branch builds into a caller-owned destination; its tables must refuse DDL.
|
|
1326
|
+
isBranch: Boolean(destination),
|
|
992
1327
|
primaryStore,
|
|
993
1328
|
auditStore,
|
|
994
1329
|
audit,
|
|
@@ -1011,12 +1346,436 @@ function initStores(
|
|
|
1011
1346
|
})
|
|
1012
1347
|
);
|
|
1013
1348
|
table.schemaVersion = 1;
|
|
1014
|
-
databaseEventsEmitter.emit('updateTable', table);
|
|
1349
|
+
if (!destination) databaseEventsEmitter.emit('updateTable', table);
|
|
1350
|
+
}
|
|
1351
|
+
void table.derivedIndexRuntime?.close();
|
|
1352
|
+
table.derivedIndexRuntime = attachDerivedIndexes(table);
|
|
1353
|
+
if (Array.isArray(primaryAttribute.relationships)) {
|
|
1354
|
+
relationshipsToHydrate.push({ table, databaseName, tableName, definitions: primaryAttribute.relationships });
|
|
1355
|
+
} else if (primaryAttribute.relationships !== undefined) {
|
|
1356
|
+
reportRelationshipError(
|
|
1357
|
+
`${databaseName}.${tableName}:list`,
|
|
1358
|
+
`Ignoring invalid persisted relationship list for ${databaseName}.${tableName}`
|
|
1359
|
+
);
|
|
1360
|
+
relationshipsToHydrate.push({ table, databaseName, tableName, definitions: [] });
|
|
1015
1361
|
}
|
|
1016
1362
|
}
|
|
1017
1363
|
return rootStore;
|
|
1018
1364
|
}
|
|
1019
1365
|
|
|
1366
|
+
/**
|
|
1367
|
+
* Branch directories live beside the base database's own storage root, never under the HDB root: a
|
|
1368
|
+
* database can be placed on its own volume, and `createCheckpoint` only hardlinks when source and
|
|
1369
|
+
* target share a filesystem — off-volume it degrades to a full byte copy, which is the property the
|
|
1370
|
+
* whole feature rests on.
|
|
1371
|
+
*
|
|
1372
|
+
* The backticks are what make the name reserved rather than merely conventional: `schemaRegex`
|
|
1373
|
+
* (validation/common_validators.ts) excludes 0x60, so no database can ever be created under this
|
|
1374
|
+
* name and shadow the branch root -- the same protection RESTORE_META_DIR uses.
|
|
1375
|
+
*/
|
|
1376
|
+
export const BRANCH_ROOT_DIR = '`branches`';
|
|
1377
|
+
|
|
1378
|
+
/**
|
|
1379
|
+
* Where the branch of `baseName` belonging to `appName` lives. Derived only from those two names, so
|
|
1380
|
+
* every node in a cluster resolves the same application's branch to the same place — the identity an
|
|
1381
|
+
* application's data needs if it is to be addressed, and eventually replicated, cluster-wide.
|
|
1382
|
+
*
|
|
1383
|
+
* App and database are separate path segments: joining them (`<app>__<db>`) is not injective —
|
|
1384
|
+
* `(a__b, c)` and `(a, b__c)` collide — so two declarations could otherwise open one directory.
|
|
1385
|
+
*/
|
|
1386
|
+
export function resolveBranchPath(baseName: string, appName: string): string {
|
|
1387
|
+
for (const [label, segment] of [
|
|
1388
|
+
['application', appName],
|
|
1389
|
+
['database', baseName],
|
|
1390
|
+
]) {
|
|
1391
|
+
if (!segment || segment.includes('/') || segment.includes('\\') || segment === '.' || segment === '..') {
|
|
1392
|
+
throw new Error(`Invalid ${label} name for a branch path: ${JSON.stringify(segment)}`);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
return join(resolveDatabaseStorageRoot(baseName), BRANCH_ROOT_DIR, appName, baseName);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
/** A branch's private table graph plus the handle needed to tear it down. */
|
|
1399
|
+
export interface BranchDatabase {
|
|
1400
|
+
tables: Tables;
|
|
1401
|
+
rootStore: RootDatabaseKind;
|
|
1402
|
+
/** The realpath of the branch directory; what a schema-change signal names to address this branch. */
|
|
1403
|
+
path: string;
|
|
1404
|
+
/** The logical name the application uses (`data`); every Table class in `tables` carries it. */
|
|
1405
|
+
databaseName: string;
|
|
1406
|
+
/** The branch's own store identity, which its blob roots resolve from. */
|
|
1407
|
+
storeName: string;
|
|
1408
|
+
/**
|
|
1409
|
+
* Every column-family wrapper opened on this store -- by the open, by a reload, or by a table
|
|
1410
|
+
* declaration -- so `close()` can release them all. Recorded at acquisition rather than
|
|
1411
|
+
* reconstructed from `tables` at close: a re-declaration displaces the index and catalog wrappers it
|
|
1412
|
+
* replaces, and a failed declaration can leave one that no class ever held.
|
|
1413
|
+
*/
|
|
1414
|
+
openedStores: any[];
|
|
1415
|
+
/**
|
|
1416
|
+
* Relationships this branch's tables declared, still un-hydrated. They cannot be resolved at open
|
|
1417
|
+
* time: a branch's definitions name the BASE database (its tables carry the base's logical names),
|
|
1418
|
+
* so resolving them through the global map would point the application's relationship reads at the
|
|
1419
|
+
* base. `hydrateBranchRelationships` finishes the job once the whole branch set is known.
|
|
1420
|
+
*/
|
|
1421
|
+
pendingRelationships: RelationshipHydration[];
|
|
1422
|
+
/**
|
|
1423
|
+
* The application's whole branch set, once `prepareBranches` has opened it, so a reload can hydrate
|
|
1424
|
+
* a relationship whose target the application also branched against that branch.
|
|
1425
|
+
*/
|
|
1426
|
+
relatedBranches?: Map<string, BranchDatabase>;
|
|
1427
|
+
close(): void;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
/** `undefined` marks a path reserved by an open still in flight, which owns it just as firmly. */
|
|
1431
|
+
const openBranches = new Map<string, BranchDatabase | undefined>();
|
|
1432
|
+
/** Store identities in use, so two branches cannot resolve one set of blob roots. */
|
|
1433
|
+
const openBranchIdentities = new Set<string>();
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* Materialization renames its clone in from `<blobRoot>.staging`, so a branch owns two database
|
|
1437
|
+
* names rather than one: a database legally called `<storeName>.staging` resolves its own blob root
|
|
1438
|
+
* to exactly the path the clone removes and renames over. Every check, reservation and release
|
|
1439
|
+
* covers the pair, so the name cannot be claimed at any point where a branch operation may still
|
|
1440
|
+
* delete what it resolves to.
|
|
1441
|
+
*/
|
|
1442
|
+
const BRANCH_STAGING_SUFFIX = '.staging';
|
|
1443
|
+
/**
|
|
1444
|
+
* Suffix of the sibling a branch is renamed to while being removed. A backtick, not a dot:
|
|
1445
|
+
* `schemaRegex` excludes 0x60, so no database can be named such that `<db>` + this suffix is another
|
|
1446
|
+
* branch's directory (with `.removing`, an application branching both `data` and `data.removing`, both
|
|
1447
|
+
* legal names, would destroy one by opening the other).
|
|
1448
|
+
*/
|
|
1449
|
+
export const BRANCH_REMOVING_SUFFIX = '`removing`';
|
|
1450
|
+
function branchIdentityPair(storeName: string): string[] {
|
|
1451
|
+
return [storeName, storeName + BRANCH_STAGING_SUFFIX];
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
/**
|
|
1455
|
+
* Identities whose blob roots outlived the branch that owned them, because a removal or an abandoned
|
|
1456
|
+
* materialization could not delete them. A database created under such a name would resolve its own
|
|
1457
|
+
* fresh file ids onto files it never wrote, so the name stays refused -- but only against DATABASES.
|
|
1458
|
+
* The branch itself may take it back: materializing it replaces those roots wholesale, which is the
|
|
1459
|
+
* only route that clears the condition without an operator.
|
|
1460
|
+
*/
|
|
1461
|
+
const quarantinedBranchIdentities = new Set<string>();
|
|
1462
|
+
|
|
1463
|
+
export function quarantineBranchIdentity(storeName: string): void {
|
|
1464
|
+
for (const name of branchIdentityPair(storeName)) {
|
|
1465
|
+
quarantinedBranchIdentities.add(name);
|
|
1466
|
+
openBranchIdentities.delete(name);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* True when `dbPath` is a directory an open branch owns. The database scan opens any directory that
|
|
1472
|
+
* holds CURRENT + MANIFEST-*, and harper#643 places a branch inside the directory it walks, so
|
|
1473
|
+
* without this a rescan would rebuild the branch's tables into the global map, overwrite the store
|
|
1474
|
+
* identity its blob roots resolve from, and hand its store to `closeLoadedDatabases`.
|
|
1475
|
+
*/
|
|
1476
|
+
function isOpenBranchPath(dbPath: string): boolean {
|
|
1477
|
+
if (openBranches.size === 0) return false;
|
|
1478
|
+
// the literal path first: `rocksdbDatabaseEnvs` is keyed by it too, so a directory unlinked under
|
|
1479
|
+
// a live branch handle (realpathSync then throws) must not read as unowned
|
|
1480
|
+
if (openBranches.has(dbPath)) return true;
|
|
1481
|
+
try {
|
|
1482
|
+
return openBranches.has(realpathSync(dbPath));
|
|
1483
|
+
} catch {
|
|
1484
|
+
return false;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/**
|
|
1489
|
+
* A branch identity resolves its blob roots through `join(…, 'blobs', storeName)`, so it must be a
|
|
1490
|
+
* single path segment: `schemaRegex`, which every other database name is validated against, plus the
|
|
1491
|
+
* dot segments and backslash that regex permits but a path component must not be.
|
|
1492
|
+
*/
|
|
1493
|
+
function assertLegalBranchName(name: string, description: string): void {
|
|
1494
|
+
if (
|
|
1495
|
+
!name ||
|
|
1496
|
+
name.length > commonValidators.schema_length.maximum ||
|
|
1497
|
+
!schemaRegex.test(name) ||
|
|
1498
|
+
name.includes('\\') ||
|
|
1499
|
+
name === '.' ||
|
|
1500
|
+
name === '..'
|
|
1501
|
+
) {
|
|
1502
|
+
throw new Error(`Cannot use '${name}' as a branch ${description}: it is not a legal database name`);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
/**
|
|
1507
|
+
* Refuse a branch store identity that something else already answers to.
|
|
1508
|
+
*
|
|
1509
|
+
* `storeName` picks the branch's blob roots, and blob file ids restart from each store's own counter,
|
|
1510
|
+
* so two holders of one identity write the same file paths and truncate each other. It must be
|
|
1511
|
+
* checked BEFORE anything destructive runs: materialization removes and replaces the blob root that
|
|
1512
|
+
* this name resolves to, and a real database may legally be called `5_myapp__data` -- `schemaRegex`
|
|
1513
|
+
* permits digits, `_` and `.`. The `.staging` sibling materialization writes is covered too, since a
|
|
1514
|
+
* database may legally carry that name as well.
|
|
1515
|
+
*/
|
|
1516
|
+
export function assertBranchIdentityAvailable(storeName: string): void {
|
|
1517
|
+
// The on-disk scan, not just the in-memory maps: a database that exists on disk but has not been
|
|
1518
|
+
// loaded is absent from both, and it owns the blob root this identity would destroy.
|
|
1519
|
+
getDatabases();
|
|
1520
|
+
for (const name of branchIdentityPair(storeName)) {
|
|
1521
|
+
// The directory as well as the maps. `getDatabases` skips a database blocked by restore, so an
|
|
1522
|
+
// in-memory check alone reports its name as free while its blob root is very much real -- and
|
|
1523
|
+
// materialization would then remove and replace it.
|
|
1524
|
+
if (
|
|
1525
|
+
databases[name] ||
|
|
1526
|
+
definedDatabases?.has(name) ||
|
|
1527
|
+
openBranchIdentities.has(name) ||
|
|
1528
|
+
existsSync(resolveDatabasePath(name)) ||
|
|
1529
|
+
anotherBranchOwns(name, storeName)
|
|
1530
|
+
) {
|
|
1531
|
+
throw new Error(`Cannot use '${storeName}' as a branch store identity: '${name}' is already in use`);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* Does a branch OTHER than the one being opened already answer to this name on disk? `.staging` is
|
|
1538
|
+
* what makes the question two-sided: `<identity>.staging` is both the path a clone renames over and
|
|
1539
|
+
* a legal identity for a branch of a database literally named `<base>.staging`, so each of the pair
|
|
1540
|
+
* can belong to somebody else. Only the primary name read as itself is excluded -- that directory is
|
|
1541
|
+
* the very branch this call is opening.
|
|
1542
|
+
*/
|
|
1543
|
+
function anotherBranchOwns(name: string, storeName: string): boolean {
|
|
1544
|
+
if (name !== storeName) return branchDirectoryExistsFor(name);
|
|
1545
|
+
return name.endsWith(BRANCH_STAGING_SUFFIX)
|
|
1546
|
+
? branchDirectoryExistsFor(name.slice(0, -BRANCH_STAGING_SUFFIX.length))
|
|
1547
|
+
: false;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/**
|
|
1551
|
+
* Claim the identity as well as checking it, so the window between the check and the branch actually
|
|
1552
|
+
* opening cannot be filled by a concurrent create or a second branch. `releaseBranchIdentity` hands
|
|
1553
|
+
* it back if materialization never gets as far as opening.
|
|
1554
|
+
*/
|
|
1555
|
+
export function reserveBranchIdentity(storeName: string): void {
|
|
1556
|
+
assertBranchIdentityAvailable(storeName);
|
|
1557
|
+
retakeBranchIdentity(storeName);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
/**
|
|
1561
|
+
* Take the pair back for an operation that owned it a statement ago -- cleanup, which has to keep
|
|
1562
|
+
* holding the names through the deletions its `close()` just released them for. Deliberately without
|
|
1563
|
+
* the availability check: nothing can have taken a name the caller held until now, and the check runs
|
|
1564
|
+
* the database scan, which at that exact moment would find the branch directory unowned.
|
|
1565
|
+
*/
|
|
1566
|
+
export function retakeBranchIdentity(storeName: string): void {
|
|
1567
|
+
for (const name of branchIdentityPair(storeName)) {
|
|
1568
|
+
openBranchIdentities.add(name);
|
|
1569
|
+
quarantinedBranchIdentities.delete(name);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
export function releaseBranchIdentity(storeName: string): void {
|
|
1574
|
+
for (const name of branchIdentityPair(storeName)) openBranchIdentities.delete(name);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/** Is this name spoken for by a branch? Database creation has to refuse it -- they share a blob root. */
|
|
1578
|
+
export function isBranchIdentity(name: string): boolean {
|
|
1579
|
+
if (openBranchIdentities.has(name) || quarantinedBranchIdentities.has(name)) return true;
|
|
1580
|
+
// The in-memory set covers only branches open in THIS process, so after a restart -- or for an
|
|
1581
|
+
// application that is simply not loaded -- a database could take the name of an on-disk branch and
|
|
1582
|
+
// share its blob root. The staging sibling goes through the same route, because it names the path
|
|
1583
|
+
// materialization renames over -- but BOTH readings of a name ending in `.staging` have to be
|
|
1584
|
+
// tried: `schemaRegex` permits `.`, so `4_myapp__data.staging` is either the sibling of a branch of
|
|
1585
|
+
// `data` or a branch of a database actually called `data.staging`.
|
|
1586
|
+
if (branchDirectoryExistsFor(name)) return true;
|
|
1587
|
+
return name.endsWith(BRANCH_STAGING_SUFFIX)
|
|
1588
|
+
? branchDirectoryExistsFor(name.slice(0, -BRANCH_STAGING_SUFFIX.length))
|
|
1589
|
+
: false;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/**
|
|
1593
|
+
* Is there a branch directory answering to this store identity? The identity carries the application
|
|
1594
|
+
* name's length precisely so it can be taken apart again without guessing where the name ends.
|
|
1595
|
+
*/
|
|
1596
|
+
function branchDirectoryExistsFor(storeName: string): boolean {
|
|
1597
|
+
const prefix = /^(\d+)_/.exec(storeName);
|
|
1598
|
+
if (!prefix) return false;
|
|
1599
|
+
const appLength = Number(prefix[1]);
|
|
1600
|
+
const appName = storeName.slice(prefix[0].length, prefix[0].length + appLength);
|
|
1601
|
+
if (
|
|
1602
|
+
appName.length !== appLength ||
|
|
1603
|
+
storeName.slice(prefix[0].length + appLength, prefix[0].length + appLength + 2) !== '__'
|
|
1604
|
+
)
|
|
1605
|
+
return false;
|
|
1606
|
+
const baseName = storeName.slice(prefix[0].length + appLength + 2);
|
|
1607
|
+
if (!baseName) return false;
|
|
1608
|
+
try {
|
|
1609
|
+
const branchPath = resolveBranchPath(baseName, appName);
|
|
1610
|
+
return existsSync(branchPath) || existsSync(branchPath + BRANCH_REMOVING_SUFFIX);
|
|
1611
|
+
} catch {
|
|
1612
|
+
// Not a name a branch path could hold, so no branch owns it.
|
|
1613
|
+
return false;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
/**
|
|
1618
|
+
* Open a RocksDB directory as a **scope-private** database: its Table classes are built into an
|
|
1619
|
+
* object the caller owns and nothing is registered in the global `databases` map, so no enumerator
|
|
1620
|
+
* of that map — analytics, `describe_all`, worker teardown, replication — can observe it.
|
|
1621
|
+
*
|
|
1622
|
+
* `databaseName` is the *logical* name the application knows (`data`), so its schema and code need
|
|
1623
|
+
* no changes. `storeName` is the branch's own identity and is what `getRootBlobPathsForDB` resolves
|
|
1624
|
+
* blob directories from, which is how a branch gets its own blob roots rather than writing into the
|
|
1625
|
+
* base's.
|
|
1626
|
+
*
|
|
1627
|
+
* The caller owns the returned handle; the only thing that closes it on the caller's behalf is
|
|
1628
|
+
* `closeBranchDatabases`, run by an exiting job worker (via `closeLoadedDatabases`) and by an HTTP
|
|
1629
|
+
* worker's shutdown path, so a branch left open on an exiting worker does not linger in the
|
|
1630
|
+
* process-global RocksDB registry.
|
|
1631
|
+
*
|
|
1632
|
+
* Schema changes reach a branch only through its own bound factory (`scopedTableFactory`): a
|
|
1633
|
+
* declaration re-asserted against the branch's store. A branch's Table classes carry the base's
|
|
1634
|
+
* logical name, so the Table statics (`dropTable()`, `addAttributes()`) — which resolve the global
|
|
1635
|
+
* schema by that name and would act on the live base table — stay refused (`assertSchemaMutable`).
|
|
1636
|
+
*
|
|
1637
|
+
* A branch's blob roots are a hard-link clone of the base's, taken with the checkpoint, so a row
|
|
1638
|
+
* whose blob predates the branch reads back normally and the branch allocates new file ids in its own
|
|
1639
|
+
* directory (harper#644).
|
|
1640
|
+
*
|
|
1641
|
+
* A branch is the checkpoint's SST content plus its own transaction-log tail. This function opens
|
|
1642
|
+
* only the stores; replaying the tail is `openOrCreate`'s job (branchDatabase.ts), where the
|
|
1643
|
+
* cross-thread claim elects exactly one replayer and awaits it before any thread may open the
|
|
1644
|
+
* branch — the same recovery contract a base database gets at boot, without which a process that
|
|
1645
|
+
* died unflushed silently rewinds the branch to its last memtable flush (harper#643).
|
|
1646
|
+
*
|
|
1647
|
+
* Pass `blobRoots` to pin the handle to the roots the branch was published with; without it the
|
|
1648
|
+
* store resolves them from current configuration, which is only right for a branch being created.
|
|
1649
|
+
*/
|
|
1650
|
+
export function openBranchDatabase(
|
|
1651
|
+
path: string,
|
|
1652
|
+
databaseName: string,
|
|
1653
|
+
storeName: string,
|
|
1654
|
+
blobRoots?: string[]
|
|
1655
|
+
): BranchDatabase {
|
|
1656
|
+
assertLegalBranchName(databaseName, 'logical database name');
|
|
1657
|
+
assertLegalBranchName(storeName, 'store identity');
|
|
1658
|
+
if (!existsSync(path)) throw new Error(`Cannot open branch database: no directory at ${path}`);
|
|
1659
|
+
// the guards compare against env-map keys, so two spellings of one directory must not read as two
|
|
1660
|
+
path = realpathSync(path);
|
|
1661
|
+
// FIRST: the guards below read the registry, and loading is itself what populates
|
|
1662
|
+
// `rocksdbDatabaseEnvs`. Claiming the path ahead of this scan would make the scan skip it, which
|
|
1663
|
+
// also means a directory that IS a real database no longer reads as one — so the pre-open window
|
|
1664
|
+
// where the scan can adopt a branch directory stays open, by choice (harper#643).
|
|
1665
|
+
getDatabases();
|
|
1666
|
+
// a rival graph over one shared root store; the two callers would disagree about who may close it
|
|
1667
|
+
if (openBranches.has(path)) throw new Error(`Branch database at ${path} is already open`);
|
|
1668
|
+
// a loaded database's store is closed by `closeLoadedDatabases`, so adopting it would mean this
|
|
1669
|
+
// handle's `close()` tears down a live database
|
|
1670
|
+
if (rocksdbDatabaseEnvs.has(path)) throw new Error(`Cannot branch ${path}: it is already open as a database`);
|
|
1671
|
+
assertBranchIdentityAvailable(storeName);
|
|
1672
|
+
|
|
1673
|
+
const tables: Tables = Object.create(null);
|
|
1674
|
+
// initStores opens a table's column families well before `setTable` publishes it into `tables`,
|
|
1675
|
+
// so the graph is not a complete record of what a failed open must release
|
|
1676
|
+
const openedStores: any[] = [];
|
|
1677
|
+
// The boot-time hydration pass has already run by the time a branch opens, so anything this open
|
|
1678
|
+
// queues would never be drained. It is handed to the caller instead, which is the only place that
|
|
1679
|
+
// knows the application's other branches and can therefore resolve targets without leaking to base.
|
|
1680
|
+
const queuedRelationshipsAt = relationshipsToHydrate.length;
|
|
1681
|
+
let rootStore: RootDatabaseKind;
|
|
1682
|
+
// claim the path before the open, not after: readRocksMetaDb registers the store in
|
|
1683
|
+
// `rocksdbDatabaseEnvs` partway through, so anything re-entering `database()` during initStores
|
|
1684
|
+
// would otherwise find the branch's store on an unowned path
|
|
1685
|
+
openBranches.set(path, undefined);
|
|
1686
|
+
retakeBranchIdentity(storeName);
|
|
1687
|
+
try {
|
|
1688
|
+
// before the open: table load schedules TTL, eviction and audit cleanup, which ask who owns this store
|
|
1689
|
+
manageThreads.markBranchStorePath(path);
|
|
1690
|
+
rootStore = readRocksMetaDb(path, null, databaseName, { destination: tables, storeName, openedStores });
|
|
1691
|
+
// Pin the handle to the roots the caller proved this branch was published with, before it is
|
|
1692
|
+
// handed out. A row's `storageIndex` is a position in that list, so resolving through current
|
|
1693
|
+
// configuration instead would let an appended volume take writes at an index the branch's own
|
|
1694
|
+
// completion marker never recorded -- and a later change at that index would then silently
|
|
1695
|
+
// re-address them. `closeBranchHandles` clears the entry with the rest of the handle.
|
|
1696
|
+
if (blobRoots) databasePaths.set(rootStore as unknown as RootDatabase, blobRoots);
|
|
1697
|
+
} catch (error) {
|
|
1698
|
+
openBranches.delete(path);
|
|
1699
|
+
manageThreads.markBranchStorePath(path, false);
|
|
1700
|
+
releaseBranchIdentity(storeName);
|
|
1701
|
+
const stranded = rocksdbDatabaseEnvs.get(path);
|
|
1702
|
+
rocksdbDatabaseEnvs.delete(path);
|
|
1703
|
+
closeBranchHandles(path, stranded, openedStores, tables);
|
|
1704
|
+
throw error;
|
|
1705
|
+
}
|
|
1706
|
+
let closed = false;
|
|
1707
|
+
const branch: BranchDatabase = {
|
|
1708
|
+
tables,
|
|
1709
|
+
rootStore,
|
|
1710
|
+
path,
|
|
1711
|
+
databaseName,
|
|
1712
|
+
storeName,
|
|
1713
|
+
openedStores,
|
|
1714
|
+
pendingRelationships: relationshipsToHydrate.splice(queuedRelationshipsAt),
|
|
1715
|
+
close() {
|
|
1716
|
+
// guard on the handle, not on the registrations: those are keyed by path, and a closed
|
|
1717
|
+
// branch frees its path, so a stale handle would otherwise tear down its successor
|
|
1718
|
+
if (closed) return;
|
|
1719
|
+
closed = true;
|
|
1720
|
+
openBranches.delete(path);
|
|
1721
|
+
releaseBranchIdentity(storeName);
|
|
1722
|
+
rocksdbDatabaseEnvs.delete(path);
|
|
1723
|
+
manageThreads.markBranchStorePath(path, false);
|
|
1724
|
+
closeBranchHandles(path, rootStore, openedStores, tables);
|
|
1725
|
+
},
|
|
1726
|
+
};
|
|
1727
|
+
openBranches.set(path, branch);
|
|
1728
|
+
return branch;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
/**
|
|
1732
|
+
* Release everything a branch open created. Each table's primary store and each index is its own
|
|
1733
|
+
* column family, on top of the internal-dbis and audit families, so closing the root alone leaves
|
|
1734
|
+
* all of them behind — which is why `closeDatabase` walks them individually for a real database.
|
|
1735
|
+
* Two process-global registrations outlive the stores as well, neither with a lifetime of its own:
|
|
1736
|
+
* a storage-reclamation handler per store path, whose closure pins the now-closed store, and the
|
|
1737
|
+
* memoized blob roots in `databasePaths`. A real database is opened once per thread; harper#643
|
|
1738
|
+
* makes branch open/close routine, so both would grow with branch churn.
|
|
1739
|
+
*/
|
|
1740
|
+
function closeBranchHandles(
|
|
1741
|
+
path: string,
|
|
1742
|
+
rootStore?: RootDatabaseKind,
|
|
1743
|
+
openedStores: any[] = [],
|
|
1744
|
+
tables: Tables = {}
|
|
1745
|
+
): void {
|
|
1746
|
+
const reclamationPaths = new Set<string>([path]);
|
|
1747
|
+
(rootStore as any)?.auditStore?.stopAuditCleanup?.();
|
|
1748
|
+
const closeStore = (store: any, description: string) => {
|
|
1749
|
+
if (!store || store.status === 'closed') return;
|
|
1750
|
+
if (store.path) reclamationPaths.add(store.path);
|
|
1751
|
+
try {
|
|
1752
|
+
store.close?.();
|
|
1753
|
+
} catch (error) {
|
|
1754
|
+
logger.warn(`Error closing ${description} for branch database at ${path}`, error);
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
// the class, before its stores: an expiration timer or a reclamation handler on a closed store
|
|
1758
|
+
// would otherwise keep firing against it for the life of the process
|
|
1759
|
+
for (const tableName in tables) {
|
|
1760
|
+
try {
|
|
1761
|
+
tables[tableName]?.cleanup?.();
|
|
1762
|
+
} catch (error) {
|
|
1763
|
+
logger.warn(`Error releasing table ${tableName} of branch database at ${path}`, error);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
for (const store of openedStores) closeStore(store, 'column family');
|
|
1767
|
+
closeStore((rootStore as any)?.dbisDb, 'attributes store');
|
|
1768
|
+
closeStore((rootStore as any)?.auditStore, 'audit store');
|
|
1769
|
+
closeStore(rootStore, 'root store');
|
|
1770
|
+
if (rootStore) databasePaths.delete(rootStore as RootDatabase);
|
|
1771
|
+
for (const reclamationPath of reclamationPaths) removeStorageReclamation(reclamationPath);
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
/** Branches are process-local, so this is shutdown, not a data operation. */
|
|
1775
|
+
export function closeBranchDatabases(): void {
|
|
1776
|
+
for (const branch of [...openBranches.values()]) branch?.close();
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1020
1779
|
export function resetDatabases() {
|
|
1021
1780
|
loadedDatabases = false;
|
|
1022
1781
|
for (const store of Object.values(lmdbDatabaseEnvs)) {
|
|
@@ -1047,6 +1806,7 @@ interface TableDefinition {
|
|
|
1047
1806
|
trackDeletes?: boolean;
|
|
1048
1807
|
attributes: any[];
|
|
1049
1808
|
schemaDefined?: boolean;
|
|
1809
|
+
schemaRelationshipsDefined?: boolean;
|
|
1050
1810
|
origin?: string;
|
|
1051
1811
|
description?: string;
|
|
1052
1812
|
properties?: Record<string, any>;
|
|
@@ -1054,6 +1814,8 @@ interface TableDefinition {
|
|
|
1054
1814
|
// default Cache-Control for anonymous REST reads; null = schema explicitly has none (clears a
|
|
1055
1815
|
// prior value on reload), undefined = caller is not schema-defining (leave the current value)
|
|
1056
1816
|
cacheControl?: string | null;
|
|
1817
|
+
/** Internal: this declaration came from the application owned by the current dedicated worker. */
|
|
1818
|
+
isolatedApplicationOwner?: boolean;
|
|
1057
1819
|
}
|
|
1058
1820
|
/**
|
|
1059
1821
|
* Ensure that we have this database object (that holds a set of tables) set up
|
|
@@ -1159,6 +1921,14 @@ export function database({ database: databaseName, table: tableName }) {
|
|
|
1159
1921
|
const useRocksdb = (process.env.HARPER_STORAGE_ENGINE || envGet(CONFIG_PARAMS.STORAGE_ENGINE)) !== 'lmdb';
|
|
1160
1922
|
if (useRocksdb) {
|
|
1161
1923
|
const path = join(databasePath, tablePath ? tableName : databaseName);
|
|
1924
|
+
// the scan is not the only way to reach a branch's directory: a branch leaves its store in
|
|
1925
|
+
// `rocksdbDatabaseEnvs`, so without this an on-demand open would staple it onto
|
|
1926
|
+
// `definedDatabases` and the next `closeDatabase` would close it under the live handle
|
|
1927
|
+
if (isOpenBranchPath(path)) {
|
|
1928
|
+
const error: any = new Error(`Database '${databaseName}' is open as a scope-private branch`);
|
|
1929
|
+
error.statusCode = 409;
|
|
1930
|
+
throw error;
|
|
1931
|
+
}
|
|
1162
1932
|
rootStore = rocksdbDatabaseEnvs.get(path);
|
|
1163
1933
|
if (!rootStore || rootStore.status === 'closed') {
|
|
1164
1934
|
// this on-demand open (create_table/create_database and friends) must not resurrect a
|
|
@@ -1378,8 +2148,12 @@ export function closeDatabase(databaseName: string): boolean {
|
|
|
1378
2148
|
* database is closed). The `system` database is intentionally left open: it is non-enumerable here
|
|
1379
2149
|
* (skipped by the loop), is never restored online, and the exiting worker may still touch the job
|
|
1380
2150
|
* table during teardown. Best-effort: closing failures are swallowed inside `closeDatabase`.
|
|
2151
|
+
*
|
|
2152
|
+
* Branches are invisible to the loop below but hold handles from the same registry, so this — the
|
|
2153
|
+
* thread's one teardown entry point — closes them too.
|
|
1381
2154
|
*/
|
|
1382
2155
|
export function closeLoadedDatabases(): void {
|
|
2156
|
+
closeBranchDatabases();
|
|
1383
2157
|
// snapshot the names first: closeDatabase() deletes from `databases` as it goes
|
|
1384
2158
|
for (const databaseName of Object.keys(databases)) {
|
|
1385
2159
|
const dbTables = databases[databaseName];
|
|
@@ -1493,23 +2267,37 @@ function openIndex(dbiKey: string, rootStore: RootDatabaseKind, attribute: any)
|
|
|
1493
2267
|
cache: isCustomObjectIndex,
|
|
1494
2268
|
} as any) as any;
|
|
1495
2269
|
(dbi as any).rootStore = rootStore;
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
2270
|
+
try {
|
|
2271
|
+
// Custom-index object stores (e.g. HNSW) write graph nodes via plain put() with no staged
|
|
2272
|
+
// transaction timestamp, so their values carry no version and the PrimaryRocksDatabase
|
|
2273
|
+
// Verification-Table cache can't track them. A versioned index initialises its encoder as a
|
|
2274
|
+
// versioned RocksDB store (isRocksDB → metadata-prefix encode/decode) and marks it
|
|
2275
|
+
// self-versioning, so each node gets a monotonic version the VT can extract — enabling cached,
|
|
2276
|
+
// decode-free graph traversal. The format is resolved from the persisted attribute descriptor
|
|
2277
|
+
// (decided once at create — see resolveIndexFormat) so every worker and reload agree on it.
|
|
2278
|
+
if (isCustomObjectIndex && resolveIndexFormat(dbiKey, rootStore, dbi, attribute) === 'versioned') {
|
|
2279
|
+
armVersionedIndexEncoder(dbi, rootStore);
|
|
2280
|
+
}
|
|
2281
|
+
installCustomIndex(dbi);
|
|
2282
|
+
} catch (error) {
|
|
2283
|
+
// the handle is not yet owned by any table, so nobody else can close it
|
|
2284
|
+
try {
|
|
2285
|
+
dbi.close();
|
|
2286
|
+
} catch {}
|
|
2287
|
+
throw error;
|
|
1505
2288
|
}
|
|
1506
2289
|
} else {
|
|
1507
2290
|
dbi = (rootStore as any).openDB(dbiKey, dbiInit as any);
|
|
2291
|
+
installCustomIndex(dbi);
|
|
1508
2292
|
}
|
|
1509
|
-
|
|
2293
|
+
function installCustomIndex(indexStore: any) {
|
|
2294
|
+
if (!attribute.indexed.type) return;
|
|
1510
2295
|
const CustomIndex = CUSTOM_INDEXES[attribute.indexed.type];
|
|
1511
2296
|
if (CustomIndex) {
|
|
1512
|
-
|
|
2297
|
+
indexStore.customIndex = new CustomIndex(indexStore, attribute.indexed);
|
|
2298
|
+
// derived state whose maintaining option is now off must not linger to be adopted
|
|
2299
|
+
// stale on a later re-enable
|
|
2300
|
+
indexStore.customIndex.cleanupDisabledPlane?.();
|
|
1513
2301
|
} else {
|
|
1514
2302
|
logger.error(`The indexing type '${attribute.indexed.type}' is unknown`);
|
|
1515
2303
|
}
|
|
@@ -1532,6 +2320,107 @@ function openIndex(dbiKey: string, rootStore: RootDatabaseKind, attribute: any)
|
|
|
1532
2320
|
* @param replicate
|
|
1533
2321
|
*/
|
|
1534
2322
|
export function table<TableResourceType>(tableDefinition: TableDefinition): TableResourceType {
|
|
2323
|
+
return declareTable(GLOBAL_TARGET, tableDefinition);
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
/**
|
|
2327
|
+
* Where a declaration lands. `table()` is bound to the global catalog; a branched application's
|
|
2328
|
+
* declarations are bound to its branch (`scopedTableFactory`). Everything in `declareTable` that is
|
|
2329
|
+
* global by construction -- the root store, the `tables` graph a class is published into, the reload
|
|
2330
|
+
* after a lost create race, who owns the handles it opens -- goes through this, and nothing else does,
|
|
2331
|
+
* so the unbranched path is the same code with the same objects behind it.
|
|
2332
|
+
*/
|
|
2333
|
+
interface TableTarget {
|
|
2334
|
+
rootStore(databaseName: string, tableName: string): RootDatabaseKind;
|
|
2335
|
+
tables(databaseName: string): Tables;
|
|
2336
|
+
/** Another thread created the table this declaration was about to; make `tables` reflect it. */
|
|
2337
|
+
reload(databaseName: string): void;
|
|
2338
|
+
/** Records a column-family wrapper the declaration opened, for whoever closes the store. */
|
|
2339
|
+
adopt(store: any): void;
|
|
2340
|
+
/** Set for a branch: its Table classes refuse DDL, and its schema signals address it by path. */
|
|
2341
|
+
branch?: BranchDatabase;
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
const GLOBAL_TARGET: TableTarget = {
|
|
2345
|
+
rootStore: (databaseName, tableName) => database({ database: databaseName, table: tableName }),
|
|
2346
|
+
tables: (databaseName) => databases[databaseName],
|
|
2347
|
+
reload: () => resetDatabases(),
|
|
2348
|
+
// a real database's stores live until the process (or `closeDatabase`, which walks the graph) ends
|
|
2349
|
+
adopt: () => {},
|
|
2350
|
+
};
|
|
2351
|
+
|
|
2352
|
+
/**
|
|
2353
|
+
* The factory a branched application declares tables through: each declaration goes to the branch
|
|
2354
|
+
* of the database it names, or to `table()` itself for a database the application did not branch.
|
|
2355
|
+
* An unbranched, shared application gets `table` by identity. An isolated application still gets a
|
|
2356
|
+
* wrapper so its own declarations can claim their single-threaded maintenance work.
|
|
2357
|
+
*/
|
|
2358
|
+
export function scopedTableFactory(
|
|
2359
|
+
branches?: Map<string, BranchDatabase>,
|
|
2360
|
+
isolatedApplicationOwner = false
|
|
2361
|
+
): typeof table {
|
|
2362
|
+
if (!branches?.size && !isolatedApplicationOwner) return table;
|
|
2363
|
+
return function scopedTable<TableResourceType>(tableDefinition: TableDefinition): TableResourceType {
|
|
2364
|
+
if (isolatedApplicationOwner) tableDefinition = { ...tableDefinition, isolatedApplicationOwner: true };
|
|
2365
|
+
// `||`, not `??`: `table()` resolves every falsy name to the default database
|
|
2366
|
+
const branch = branches?.get(tableDefinition.database || DEFAULT_DATABASE_NAME);
|
|
2367
|
+
return branch ? declareTable(branchTarget(branch), tableDefinition) : table(tableDefinition);
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
function branchTarget(branch: BranchDatabase): TableTarget {
|
|
2372
|
+
return {
|
|
2373
|
+
rootStore: () => branch.rootStore,
|
|
2374
|
+
tables: () => branch.tables,
|
|
2375
|
+
reload: () => reloadBranch(branch),
|
|
2376
|
+
adopt: (store) => branch.openedStores.push(store),
|
|
2377
|
+
branch,
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
/**
|
|
2382
|
+
* Re-read a branch's catalog into its `tables`: tables and indexes another thread declared since the
|
|
2383
|
+
* open (or since the last reload) are opened here, the same way a schema-change rescan does for a
|
|
2384
|
+
* real database. Existing classes are kept and their attribute lists refreshed.
|
|
2385
|
+
*/
|
|
2386
|
+
function reloadBranch(branch: BranchDatabase): void {
|
|
2387
|
+
const { rootStore, tables, databaseName, storeName, openedStores } = branch;
|
|
2388
|
+
const queuedRelationshipsAt = relationshipsToHydrate.length;
|
|
2389
|
+
try {
|
|
2390
|
+
initStores(rootStore.path, rootStore, databaseName, { destination: tables, storeName, openedStores });
|
|
2391
|
+
} finally {
|
|
2392
|
+
for (const hydration of relationshipsToHydrate.splice(queuedRelationshipsAt))
|
|
2393
|
+
queueBranchHydration(branch, hydration);
|
|
2394
|
+
}
|
|
2395
|
+
// Until `prepareBranches` has the whole set, a cross-database target cannot be resolved without
|
|
2396
|
+
// falling through to the base; it hydrates the complete set once. After that, every sibling is
|
|
2397
|
+
// re-hydrated: the table this reload brought in may be the target a sibling's relationship waited for.
|
|
2398
|
+
if (!branch.relatedBranches) return;
|
|
2399
|
+
for (const sibling of branch.relatedBranches.values()) hydrateBranchRelationships(sibling, branch.relatedBranches);
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
/** One pending hydration per table: a re-declaration replaces the entry the earlier declaration queued. */
|
|
2403
|
+
function queueBranchHydration(branch: BranchDatabase, hydration: RelationshipHydration): void {
|
|
2404
|
+
const existing = branch.pendingRelationships.findIndex(
|
|
2405
|
+
(pending) => pending.databaseName === hydration.databaseName && pending.tableName === hydration.tableName
|
|
2406
|
+
);
|
|
2407
|
+
if (existing >= 0) branch.pendingRelationships[existing] = hydration;
|
|
2408
|
+
else branch.pendingRelationships.push(hydration);
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
/**
|
|
2412
|
+
* The receiving side of a branch's schema-change signal: a thread that holds this branch open reloads
|
|
2413
|
+
* it, any other thread has nothing to do. Returns the branch's tables so the caller can address the
|
|
2414
|
+
* table the signal named.
|
|
2415
|
+
*/
|
|
2416
|
+
export function reloadBranchAt(path: string): Tables | undefined {
|
|
2417
|
+
const branch = openBranches.get(path);
|
|
2418
|
+
if (!branch) return undefined;
|
|
2419
|
+
reloadBranch(branch);
|
|
2420
|
+
return branch.tables;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
function declareTable<TableResourceType>(target: TableTarget, tableDefinition: TableDefinition): TableResourceType {
|
|
1535
2424
|
let {
|
|
1536
2425
|
table: tableName,
|
|
1537
2426
|
database: databaseName,
|
|
@@ -1546,12 +2435,16 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
|
|
|
1546
2435
|
randomAccessFields,
|
|
1547
2436
|
trackDeletes,
|
|
1548
2437
|
schemaDefined,
|
|
2438
|
+
schemaRelationshipsDefined,
|
|
1549
2439
|
origin,
|
|
1550
2440
|
description,
|
|
1551
2441
|
properties,
|
|
1552
2442
|
hidden,
|
|
1553
2443
|
cacheControl,
|
|
2444
|
+
isolatedApplicationOwner,
|
|
1554
2445
|
} = tableDefinition;
|
|
2446
|
+
const auditExplicitlyEnabled = audit === true;
|
|
2447
|
+
const auditExplicitlyDisabled = audit === false;
|
|
1555
2448
|
if (!databaseName) databaseName = DEFAULT_DATABASE_NAME;
|
|
1556
2449
|
// Reject reserved names here too, not only at the operations API: a database
|
|
1557
2450
|
// is also created by schema authoring — a `schema.graphql` `@table(database:)`
|
|
@@ -1563,8 +2456,14 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
|
|
|
1563
2456
|
if ((RESERVED_DATABASE_NAMES as readonly string[]).includes(databaseName)) {
|
|
1564
2457
|
throw new ClientError(`'${databaseName}' is a reserved name and cannot be used as a database name`);
|
|
1565
2458
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
2459
|
+
// A branch resolves its blob root from its store identity, so a database created under that same
|
|
2460
|
+
// name would share the root: two allocators minting the same file paths and truncating each other,
|
|
2461
|
+
// and the branch's teardown removing the database's blobs.
|
|
2462
|
+
if (isBranchIdentity(databaseName)) {
|
|
2463
|
+
throw new ClientError(`'${databaseName}' is in use as a branch store identity and cannot be a database name`);
|
|
2464
|
+
}
|
|
2465
|
+
const rootStore = target.rootStore(databaseName, tableName);
|
|
2466
|
+
const tables = target.tables(databaseName);
|
|
1568
2467
|
logger.trace(`Defining ${tableName} in ${databaseName}`);
|
|
1569
2468
|
let Table = tables?.[tableName];
|
|
1570
2469
|
if (rootStore.status === 'closed') {
|
|
@@ -1578,124 +2477,296 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
|
|
|
1578
2477
|
// flag must be left as-is. Only an explicit value can re-assert on the existing-Table branch.
|
|
1579
2478
|
const schemaDefinedExplicit = tableDefinition.schemaDefined !== undefined;
|
|
1580
2479
|
if (schemaDefined == undefined) schemaDefined = true;
|
|
2480
|
+
const relationshipDefinitions = schemaRelationshipsDefined ? normalizeRelationships(attributes) : undefined;
|
|
1581
2481
|
const internalDbiInit = createOpenDBIObject(false);
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
2482
|
+
let releaseExclusiveLock: (() => void) | undefined;
|
|
2483
|
+
|
|
2484
|
+
const hasHnswAtEntry = attributes.some((attribute) => attribute.indexed?.type === 'HNSW');
|
|
2485
|
+
const persistedPrimaryDescriptor = (catalog: any) => {
|
|
2486
|
+
const declaredPrimaryKey = attributes.find((attribute) => attribute.isPrimaryKey)?.name ?? Table?.primaryKey;
|
|
2487
|
+
if (declaredPrimaryKey) {
|
|
2488
|
+
const key = `${tableName}/${declaredPrimaryKey}`;
|
|
2489
|
+
const descriptor = catalog?.getSync(key);
|
|
2490
|
+
if (descriptor?.isPrimaryKey) return { key, descriptor };
|
|
2491
|
+
}
|
|
2492
|
+
const key = `${tableName}/`;
|
|
2493
|
+
return { key, descriptor: catalog?.getSync(key) };
|
|
2494
|
+
};
|
|
2495
|
+
const hasLegacyHnswStateAtEntry =
|
|
2496
|
+
Table &&
|
|
2497
|
+
origin !== 'cluster' &&
|
|
2498
|
+
attributes.some((attribute) => {
|
|
2499
|
+
if (attribute.indexed?.type !== 'HNSW') return false;
|
|
2500
|
+
const persisted = Table.dbisDB?.getSync(`${tableName}/${attribute.name || attribute.attribute || ''}`)?.indexed;
|
|
2501
|
+
if (persisted?.type !== 'HNSW') return false;
|
|
2502
|
+
if (Object.hasOwn(persisted, 'nativePlane') && typeof persisted.nativePlane !== 'boolean') return true;
|
|
2503
|
+
for (const name of CUSTOM_INDEXES.HNSW.numericOptions)
|
|
2504
|
+
if (Object.hasOwn(persisted, name) && typeof persisted[name] !== 'number') return true;
|
|
2505
|
+
return false;
|
|
2506
|
+
});
|
|
2507
|
+
try {
|
|
2508
|
+
if (
|
|
2509
|
+
Table &&
|
|
2510
|
+
hasHnswAtEntry &&
|
|
2511
|
+
origin !== 'cluster' &&
|
|
2512
|
+
(rootStore instanceof RocksDatabase || hasLegacyHnswStateAtEntry)
|
|
2513
|
+
)
|
|
2514
|
+
exclusiveLock();
|
|
2515
|
+
const persistedAuditAtEntry =
|
|
2516
|
+
hasHnswAtEntry || (Table && Table.audit !== true)
|
|
2517
|
+
? persistedPrimaryDescriptor(Table?.dbisDB).descriptor?.audit
|
|
2518
|
+
: undefined;
|
|
2519
|
+
if (!auditExplicitlyDisabled && persistedAuditAtEntry === true && Table?.audit !== true) Table.enableAuditing();
|
|
2520
|
+
for (const attribute of attributes) {
|
|
2521
|
+
if (attribute.attribute && !attribute.name) {
|
|
2522
|
+
// there is some legacy code that calls the attribute's name the attribute's attribute
|
|
2523
|
+
attribute.name = attribute.attribute;
|
|
2524
|
+
attribute.indexed = true;
|
|
2525
|
+
} else attribute.attribute = attribute.name;
|
|
2526
|
+
if (attribute.expiresAt) attribute.indexed = true;
|
|
2527
|
+
if (attribute.indexed?.type === 'HNSW' && origin !== 'cluster') {
|
|
2528
|
+
const existingAttribute = Table?.attributes.find((existing: any) => existing.name === attribute.name);
|
|
2529
|
+
const persistedIndexed =
|
|
2530
|
+
Table?.dbisDB?.getSync(`${tableName}/${attribute.name || ''}`)?.indexed ?? existingAttribute?.indexed;
|
|
2531
|
+
CUSTOM_INDEXES.HNSW.normalizeDeclarationOptions(attribute.indexed, persistedIndexed);
|
|
2532
|
+
if (attribute.indexed.nativePlane != null) {
|
|
2533
|
+
const persistedNativePlane = persistedIndexed?.nativePlane;
|
|
2534
|
+
const matchesPersistedLegacySpelling =
|
|
2535
|
+
persistedIndexed?.type === 'HNSW' &&
|
|
2536
|
+
Object.hasOwn(persistedIndexed, 'nativePlane') &&
|
|
2537
|
+
typeof persistedNativePlane !== 'boolean' &&
|
|
2538
|
+
(Object.is(persistedNativePlane, attribute.indexed.nativePlane) ||
|
|
2539
|
+
(typeof persistedNativePlane === 'string' &&
|
|
2540
|
+
typeof attribute.indexed.nativePlane === 'number' &&
|
|
2541
|
+
persistedNativePlane.trim() !== '' &&
|
|
2542
|
+
Number(persistedNativePlane) === attribute.indexed.nativePlane));
|
|
2543
|
+
if (matchesPersistedLegacySpelling) {
|
|
2544
|
+
attribute.indexed.nativePlane = persistedNativePlane;
|
|
2545
|
+
} else {
|
|
2546
|
+
attribute.indexed.nativePlane = CUSTOM_INDEXES.HNSW.normalizeNativePlaneDeclaration(
|
|
2547
|
+
attribute.indexed.nativePlane
|
|
2548
|
+
);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
const auditEnabledAtEntry =
|
|
2554
|
+
auditExplicitlyEnabled ||
|
|
2555
|
+
(!auditExplicitlyDisabled &&
|
|
2556
|
+
(persistedAuditAtEntry === true || (persistedAuditAtEntry == null && Table?.audit === true)));
|
|
2557
|
+
if (
|
|
2558
|
+
origin !== 'cluster' &&
|
|
2559
|
+
attributes.some((attribute) => {
|
|
2560
|
+
if (attribute.indexed?.type !== 'HNSW') return false;
|
|
2561
|
+
if (attribute.indexed.nativePlane != null) return Boolean(attribute.indexed.nativePlane);
|
|
2562
|
+
const existingAttribute = Table?.attributes.find(
|
|
2563
|
+
(existing: any) => existing.name === attribute.name && existing.indexed?.type === 'HNSW'
|
|
2564
|
+
);
|
|
2565
|
+
return Boolean(existingAttribute?.indexed.nativePlane);
|
|
2566
|
+
}) &&
|
|
2567
|
+
!auditEnabledAtEntry
|
|
2568
|
+
) {
|
|
2569
|
+
throw new ClientError(
|
|
2570
|
+
`Table '${databaseName}.${tableName}' must enable audit logging before using nativePlane because its transaction log is the derived-index recovery source; set nativePlane: false to use the JS index`
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
2573
|
+
} catch (error) {
|
|
2574
|
+
releaseLock();
|
|
2575
|
+
throw error;
|
|
1590
2576
|
}
|
|
2577
|
+
const validateHnswOptions = (catalog: any, auditQualifiesDefault: boolean) => {
|
|
2578
|
+
for (const attribute of attributes) {
|
|
2579
|
+
const indexed = attribute.indexed;
|
|
2580
|
+
if (indexed?.type !== 'HNSW') continue;
|
|
2581
|
+
const persistedIndexed = catalog?.getSync(`${tableName}/${attribute.name || ''}`)?.indexed;
|
|
2582
|
+
if (indexed.nativePlane) {
|
|
2583
|
+
CUSTOM_INDEXES.HNSW.validateNativePlaneOptions(rootStore, indexed);
|
|
2584
|
+
continue;
|
|
2585
|
+
}
|
|
2586
|
+
if (indexed.nativePlane != null) continue;
|
|
2587
|
+
if (persistedIndexed?.type === 'HNSW' && Object.hasOwn(persistedIndexed, 'nativePlane')) {
|
|
2588
|
+
if (persistedIndexed.nativePlane)
|
|
2589
|
+
CUSTOM_INDEXES.HNSW.validateNativePlaneOptions(rootStore, {
|
|
2590
|
+
...indexed,
|
|
2591
|
+
nativePlane: persistedIndexed.nativePlane,
|
|
2592
|
+
});
|
|
2593
|
+
continue;
|
|
2594
|
+
}
|
|
2595
|
+
if (!auditQualifiesDefault) continue;
|
|
2596
|
+
if (persistedIndexed?.type !== 'HNSW') CUSTOM_INDEXES.HNSW.canDefaultToNativePlane(rootStore, indexed);
|
|
2597
|
+
}
|
|
2598
|
+
};
|
|
2599
|
+
if (!Table && origin !== 'cluster') validateHnswOptions(undefined, auditExplicitlyEnabled);
|
|
1591
2600
|
let hasChanges;
|
|
1592
|
-
let
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
2601
|
+
let refreshRelationshipAttributes = false;
|
|
2602
|
+
let refreshedLiveAttributes = false;
|
|
2603
|
+
let deferredPrimaryRow: any;
|
|
2604
|
+
let unpublishedPrimaryStore: any;
|
|
2605
|
+
let published = false;
|
|
2606
|
+
const attributesToIndex = [];
|
|
2607
|
+
const indicesToRemove = [];
|
|
2608
|
+
try {
|
|
2609
|
+
if (Table) {
|
|
2610
|
+
refreshedLiveAttributes = true;
|
|
2611
|
+
primaryKey = Table.primaryKey;
|
|
2612
|
+
if (Table.primaryStore.rootStore.status === 'closed') {
|
|
2613
|
+
throw new Error(`Can not use a closed data store from ${tableName} class`);
|
|
2614
|
+
}
|
|
2615
|
+
// Reject moving the primary key to a different attribute on a table that already has records.
|
|
2616
|
+
// The storage key (Table.primaryKey) is never re-pointed here, so honoring the change would
|
|
2617
|
+
// leave describe reporting the new attribute while every record — old and newly inserted — stays
|
|
2618
|
+
// keyed by the original one; search_by_id/update/delete by the declared key then all miss. Only
|
|
2619
|
+
// schema-authored callers (@table / defineTable / create_table) reassert the declaration, so
|
|
2620
|
+
// gate on schemaDefinedExplicit to leave cluster schema-replication / data-loader callers alone.
|
|
2621
|
+
// See HarperFast/studio#1199.
|
|
2622
|
+
const declaredPrimaryKey = attributes.find((attribute) => attribute.isPrimaryKey)?.name;
|
|
2623
|
+
if (schemaDefinedExplicit && declaredPrimaryKey && declaredPrimaryKey !== Table.primaryKey) {
|
|
2624
|
+
let hasRecords = false;
|
|
2625
|
+
for (const _entry of Table.primaryStore.getRange({ start: true })) {
|
|
2626
|
+
hasRecords = true;
|
|
2627
|
+
break;
|
|
2628
|
+
}
|
|
2629
|
+
if (hasRecords) {
|
|
2630
|
+
throw new ClientError(
|
|
2631
|
+
`Cannot change the primary key of table '${databaseName}.${tableName}' from '${Table.primaryKey}' to ` +
|
|
2632
|
+
`'${declaredPrimaryKey}' because it already contains records. Recreate the table with the new primary ` +
|
|
2633
|
+
`key, or migrate the existing records.`,
|
|
2634
|
+
400
|
|
2635
|
+
);
|
|
2636
|
+
}
|
|
1611
2637
|
}
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
2638
|
+
// Acquire before the first mutation of the live Table below, so a lost race leaves no
|
|
2639
|
+
// attributes this worker describes but never persisted. Only the RocksDB acquire is bounded;
|
|
2640
|
+
// ordinary LMDB declarations stay lazy, while legacy HNSW normalization locks at entry.
|
|
2641
|
+
if (rootStore instanceof RocksDatabase) exclusiveLock();
|
|
2642
|
+
if (origin !== 'cluster') {
|
|
2643
|
+
const lockedAttributesDbi = Table.dbisDB;
|
|
2644
|
+
const persistedAuditUnderLock = persistedPrimaryDescriptor(lockedAttributesDbi).descriptor?.audit;
|
|
2645
|
+
validateHnswOptions(
|
|
2646
|
+
lockedAttributesDbi,
|
|
2647
|
+
auditExplicitlyEnabled || (!auditExplicitlyDisabled && persistedAuditUnderLock === true)
|
|
1618
2648
|
);
|
|
1619
2649
|
}
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
const dbiInit = createOpenDBIObject(false, true);
|
|
1671
|
-
dbiInit.compression = primaryKeyAttribute.compression;
|
|
1672
|
-
// per-table override of the storage.randomAccessFields default (see OpenDBIObject)
|
|
1673
|
-
if (typeof primaryKeyAttribute.randomAccessFields === 'boolean')
|
|
1674
|
-
dbiInit.randomAccessStructure = primaryKeyAttribute.randomAccessFields;
|
|
1675
|
-
const dbiName = tableName + '/';
|
|
1676
|
-
|
|
1677
|
-
if (rootStore instanceof RocksDatabase) {
|
|
1678
|
-
attributesDbi = (rootStore as any).dbisDb = openRocksDatabase(rootStore.path, {
|
|
1679
|
-
...internalDbiInit,
|
|
1680
|
-
disableWAL: false,
|
|
1681
|
-
name: INTERNAL_DBIS_NAME,
|
|
1682
|
-
} as any);
|
|
2650
|
+
// it table already exists, get the split segments setting
|
|
2651
|
+
if (splitSegments == undefined) splitSegments = Table.splitSegments;
|
|
2652
|
+
if (origin === 'cluster') {
|
|
2653
|
+
const merged = Table.attributes.slice();
|
|
2654
|
+
for (const attribute of attributes) {
|
|
2655
|
+
const existing = merged.find((existingAttribute) => existingAttribute.name === attribute.name);
|
|
2656
|
+
if (!existing) {
|
|
2657
|
+
merged.push(attribute);
|
|
2658
|
+
continue;
|
|
2659
|
+
}
|
|
2660
|
+
// Nodes that apply the same peer definitions in a different order keep different index sets, and
|
|
2661
|
+
// this warn is the only signal of it. An absent field and an explicit falsy one declare the same
|
|
2662
|
+
// thing, so neither direction of that pair is a difference.
|
|
2663
|
+
const discarded = PEER_DECLARABLE_FIELDS.filter(
|
|
2664
|
+
(field) =>
|
|
2665
|
+
(attribute[field] || existing[field]) &&
|
|
2666
|
+
JSON.stringify(attribute[field]) !== JSON.stringify(existing[field])
|
|
2667
|
+
);
|
|
2668
|
+
if (discarded.length > 0)
|
|
2669
|
+
logger.warn(
|
|
2670
|
+
`Ignoring peer redefinition of ${databaseName}.${tableName}.${attribute.name} (${discarded
|
|
2671
|
+
.map(
|
|
2672
|
+
(field) =>
|
|
2673
|
+
`${field}: local ${JSON.stringify(existing[field])}, peer ${JSON.stringify(attribute[field])}`
|
|
2674
|
+
)
|
|
2675
|
+
.join('; ')}); the local schema is authoritative`
|
|
2676
|
+
);
|
|
2677
|
+
}
|
|
2678
|
+
attributes = merged;
|
|
2679
|
+
} else if (!attributes.some((attribute) => attribute.isPrimaryKey)) {
|
|
2680
|
+
const existingPrimary = Table.attributes.find((attribute: any) => attribute.isPrimaryKey);
|
|
2681
|
+
if (existingPrimary && attributes.some((attribute) => attribute.name === existingPrimary.name))
|
|
2682
|
+
throw new ClientError(
|
|
2683
|
+
`Cannot remove the primary key designation from '${databaseName}.${tableName}.${existingPrimary.name}'`
|
|
2684
|
+
);
|
|
2685
|
+
if (existingPrimary) attributes = [existingPrimary, ...attributes];
|
|
2686
|
+
}
|
|
2687
|
+
Table.attributes.splice(0, Table.attributes.length, ...attributes);
|
|
2688
|
+
// Re-assert from the live declaration so a stale value on disk (replicated event,
|
|
2689
|
+
// v4-era backfill) is corrected on every reload. Gated on `schemaDefinedExplicit` so
|
|
2690
|
+
// callers that omit the flag (cluster schema-replication, data loader) don't flip a
|
|
2691
|
+
// dynamic table to true via the default at the top of table(), and on origin so a
|
|
2692
|
+
// peer-derived definition never overrides the local declaration.
|
|
2693
|
+
if (schemaDefinedExplicit && origin !== 'cluster') Table.schemaDefined = schemaDefined;
|
|
2694
|
+
// Refresh class-level schema metadata to track docstring/directive changes across reloads.
|
|
2695
|
+
Table.description = description;
|
|
2696
|
+
Table.properties = properties;
|
|
2697
|
+
Table.hidden = hidden;
|
|
2698
|
+
// undefined means a non-schema caller (add_attribute, cluster schema events) — don't clobber
|
|
2699
|
+
if (cacheControl !== undefined) Table.cacheControl = cacheControl;
|
|
1683
2700
|
} else {
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
2701
|
+
const auditStore = rootStore.auditStore;
|
|
2702
|
+
primaryKeyAttribute = attributes.find((attribute) => attribute.isPrimaryKey) || {};
|
|
2703
|
+
primaryKey = primaryKeyAttribute.name;
|
|
2704
|
+
primaryKeyAttribute.isPrimaryKey = true;
|
|
2705
|
+
primaryKeyAttribute.is_hash_attribute = true; // backward-compat: harperdb@4.x reads this field to open the DBI with correct flags
|
|
2706
|
+
primaryKeyAttribute.schemaDefined = schemaDefined;
|
|
2707
|
+
// Old readers treat every attribute row as live schema, so relationships stay on the ignored primary descriptor.
|
|
2708
|
+
if (relationshipDefinitions) primaryKeyAttribute.relationships = relationshipDefinitions;
|
|
2709
|
+
// can't change compression after the fact (except threshold), so save only when we create the table
|
|
2710
|
+
primaryKeyAttribute.compression = getDefaultCompression();
|
|
2711
|
+
if (trackDeletes) primaryKeyAttribute.trackDeletes = true;
|
|
2712
|
+
audit = primaryKeyAttribute.audit = typeof audit === 'boolean' ? audit : envGet(CONFIG_PARAMS.LOGGING_AUDITLOG);
|
|
2713
|
+
if (expiration) primaryKeyAttribute.expiration = expiration;
|
|
2714
|
+
if (eviction) primaryKeyAttribute.eviction = eviction;
|
|
2715
|
+
// persist cacheControl so all threads (and future boots) see it; undefined callers inherit
|
|
2716
|
+
// a descriptor value carried by cluster schema events; null (schema has no directive)
|
|
2717
|
+
// clears a stale value the carried descriptor may hold
|
|
2718
|
+
if (cacheControl === undefined) cacheControl = primaryKeyAttribute.cacheControl;
|
|
2719
|
+
else if (cacheControl === null) delete primaryKeyAttribute.cacheControl;
|
|
2720
|
+
else primaryKeyAttribute.cacheControl = cacheControl;
|
|
2721
|
+
splitSegments ??= false;
|
|
2722
|
+
primaryKeyAttribute.splitSegments = splitSegments; // always default to not splitting segments going forward
|
|
2723
|
+
if (typeof sealed === 'boolean') primaryKeyAttribute.sealed = sealed;
|
|
2724
|
+
if (typeof replicate === 'boolean') primaryKeyAttribute.replicate = replicate;
|
|
2725
|
+
// An explicit directive PINS this table's encoding: we persist the boolean, so later changes
|
|
2726
|
+
// to the global storage.randomAccessFields default never affect this table. Tables WITHOUT the
|
|
2727
|
+
// directive are intentionally not persisted here — they follow the current global default on
|
|
2728
|
+
// each open (a runtime lever to flip encoding fleet-wide). Switching either way is safe: the
|
|
2729
|
+
// struct READ hook always stays on and struct (0x20-0x3f) vs classic-record (0x40-0x7f) bytes
|
|
2730
|
+
// are disjoint, so already-written records still decode; only the encoding of NEW writes changes.
|
|
2731
|
+
if (typeof randomAccessFields === 'boolean') primaryKeyAttribute.randomAccessFields = randomAccessFields;
|
|
2732
|
+
if (origin) {
|
|
2733
|
+
if (!primaryKeyAttribute.origins) primaryKeyAttribute.origins = [origin];
|
|
2734
|
+
else if (!primaryKeyAttribute.origins.includes(origin)) primaryKeyAttribute.origins.push(origin);
|
|
2735
|
+
}
|
|
2736
|
+
logger.trace(`${tableName} table loading, opening primary store`);
|
|
2737
|
+
const dbiInit = createOpenDBIObject(false, true);
|
|
2738
|
+
dbiInit.compression = primaryKeyAttribute.compression;
|
|
2739
|
+
// per-table override of the storage.randomAccessFields default (see OpenDBIObject)
|
|
2740
|
+
if (typeof primaryKeyAttribute.randomAccessFields === 'boolean')
|
|
2741
|
+
dbiInit.randomAccessStructure = primaryKeyAttribute.randomAccessFields;
|
|
2742
|
+
const dbiName = tableName + '/';
|
|
1687
2743
|
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
2744
|
+
if (rootStore instanceof RocksDatabase) {
|
|
2745
|
+
attributesDbi = (rootStore as any).dbisDb = openRocksDatabase(rootStore.path, {
|
|
2746
|
+
...internalDbiInit,
|
|
2747
|
+
disableWAL: false,
|
|
2748
|
+
name: INTERNAL_DBIS_NAME,
|
|
2749
|
+
} as any);
|
|
2750
|
+
} else {
|
|
2751
|
+
attributesDbi = (rootStore as any).dbisDb = (rootStore as any).openDB(
|
|
2752
|
+
INTERNAL_DBIS_NAME,
|
|
2753
|
+
internalDbiInit as any
|
|
2754
|
+
);
|
|
2755
|
+
}
|
|
2756
|
+
target.adopt(attributesDbi);
|
|
2757
|
+
markInternalDbiNonVersioned(attributesDbi);
|
|
2758
|
+
|
|
2759
|
+
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)
|
|
2760
|
+
const existingTableMeta = (attributesDbi as any).getSync(dbiName);
|
|
2761
|
+
if (existingTableMeta && !existingTableMeta.dropping) {
|
|
2762
|
+
// table was created while we were setting up; the lock is not reentrant, so release
|
|
2763
|
+
// before the recursive reload
|
|
2764
|
+
releaseLock();
|
|
2765
|
+
target.reload(databaseName);
|
|
2766
|
+
return declareTable(target, tableDefinition);
|
|
2767
|
+
}
|
|
1696
2768
|
|
|
1697
|
-
|
|
1698
|
-
try {
|
|
2769
|
+
let primaryStore;
|
|
1699
2770
|
if (existingTableMeta?.dropping) {
|
|
1700
2771
|
// A previous drop of this table was interrupted after its tombstone
|
|
1701
2772
|
// was written. Complete it now (under the exclusive lock) so the
|
|
@@ -1722,124 +2793,197 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
|
|
|
1722
2793
|
} else {
|
|
1723
2794
|
primaryStore = (rootStore as any).openDB(dbiName, dbiInit as any);
|
|
1724
2795
|
}
|
|
2796
|
+
target.adopt(primaryStore);
|
|
2797
|
+
unpublishedPrimaryStore = primaryStore;
|
|
1725
2798
|
primaryStore = handleLocalTimeForGets(primaryStore, rootStore);
|
|
1726
|
-
|
|
2799
|
+
// only a store no table has loaded yet is unnamed; a branch's store carries its own store
|
|
2800
|
+
// identity here, which its blob roots resolve from, and must not take the logical name
|
|
2801
|
+
rootStore.databaseName ??= databaseName;
|
|
1727
2802
|
primaryStore.tableId = attributesDbi.getSync(NEXT_TABLE_ID);
|
|
1728
2803
|
logger.trace(`Assigning new table id ${primaryStore.tableId} for ${tableName}`);
|
|
1729
2804
|
if (!primaryStore.tableId) primaryStore.tableId = 1;
|
|
1730
2805
|
attributesDbi.put(NEXT_TABLE_ID, primaryStore.tableId + 1);
|
|
1731
2806
|
|
|
1732
2807
|
primaryKeyAttribute.tableId = primaryStore.tableId;
|
|
1733
|
-
Table =
|
|
1734
|
-
|
|
2808
|
+
Table = makeTable({
|
|
2809
|
+
isBranch: Boolean(target.branch),
|
|
2810
|
+
primaryStore,
|
|
2811
|
+
auditStore,
|
|
2812
|
+
audit,
|
|
2813
|
+
sealed,
|
|
2814
|
+
splitSegments,
|
|
2815
|
+
replicate,
|
|
2816
|
+
trackDeletes,
|
|
2817
|
+
expirationMS: expiration && expiration * 1000,
|
|
2818
|
+
evictionMS: eviction && eviction * 1000,
|
|
2819
|
+
primaryKey,
|
|
1735
2820
|
tableName,
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
tableId: primaryStore.tableId,
|
|
1749
|
-
databasePath: databaseName,
|
|
1750
|
-
databaseName,
|
|
1751
|
-
indices: {},
|
|
1752
|
-
attributes,
|
|
1753
|
-
schemaDefined,
|
|
1754
|
-
dbisDB: attributesDbi,
|
|
1755
|
-
description,
|
|
1756
|
-
properties,
|
|
1757
|
-
hidden,
|
|
1758
|
-
cacheControl,
|
|
1759
|
-
})
|
|
1760
|
-
);
|
|
2821
|
+
tableId: primaryStore.tableId,
|
|
2822
|
+
databasePath: databaseName,
|
|
2823
|
+
databaseName,
|
|
2824
|
+
indices: {},
|
|
2825
|
+
attributes,
|
|
2826
|
+
schemaDefined,
|
|
2827
|
+
dbisDB: attributesDbi,
|
|
2828
|
+
description,
|
|
2829
|
+
properties,
|
|
2830
|
+
hidden,
|
|
2831
|
+
cacheControl,
|
|
2832
|
+
});
|
|
1761
2833
|
Table.schemaVersion = 1;
|
|
1762
2834
|
hasChanges = true;
|
|
1763
|
-
|
|
1764
|
-
attributesDbi.put(dbiName, primaryKeyAttribute);
|
|
1765
|
-
} catch (error) {
|
|
1766
|
-
// A failure while opening/creating the column family or writing the
|
|
1767
|
-
// table id / catalog entry (e.g. into an env poisoned by a prior
|
|
1768
|
-
// dangling column family) must NOT leak the exclusive
|
|
1769
|
-
// 'update-attributes' spin lock. If it leaks, every subsequent
|
|
1770
|
-
// create_table / attribute update on this database spins forever
|
|
1771
|
-
// (a hard wedge that pins a worker at 100% CPU). Release before rethrow.
|
|
1772
|
-
if (releaseExclusiveLock) releaseExclusiveLock();
|
|
1773
|
-
throw error;
|
|
2835
|
+
deferredPrimaryRow = primaryKeyAttribute;
|
|
1774
2836
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
|
|
2837
|
+
const indices = Table.indices;
|
|
2838
|
+
if (!attributesDbi) {
|
|
2839
|
+
if (rootStore instanceof RocksDatabase) {
|
|
2840
|
+
(rootStore as any).dbisDb = openRocksDatabase(rootStore.path, {
|
|
2841
|
+
...internalDbiInit,
|
|
2842
|
+
disableWAL: false,
|
|
2843
|
+
name: INTERNAL_DBIS_NAME,
|
|
2844
|
+
} as any);
|
|
2845
|
+
} else {
|
|
2846
|
+
(rootStore as any).dbisDb = (rootStore as any).openDB(INTERNAL_DBIS_NAME, internalDbiInit as any);
|
|
2847
|
+
}
|
|
2848
|
+
target.adopt((rootStore as any).dbisDb);
|
|
2849
|
+
attributesDbi = markInternalDbiNonVersioned((rootStore as any).dbisDb);
|
|
1786
2850
|
}
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
if (
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
2851
|
+
Table.dbisDB = attributesDbi;
|
|
2852
|
+
// A cluster-origin list can miss a descriptor another thread committed moments ago, so removal
|
|
2853
|
+
// reconciliation is reserved for local schema authoring; on a create the rows can only be aborted state.
|
|
2854
|
+
const reconcileRemovals = origin !== 'cluster' || Boolean(deferredPrimaryRow);
|
|
2855
|
+
for (const { key, value } of reconcileRemovals
|
|
2856
|
+
? attributesDbi.getRange({ start: tableName + '/', end: tableName + '0' })
|
|
2857
|
+
: []) {
|
|
2858
|
+
if (value == null) continue;
|
|
2859
|
+
let [attributeTableName, attribute_name] = key.toString().split('/');
|
|
2860
|
+
if (attribute_name === '') attribute_name = value.name; // primary key
|
|
2861
|
+
if (attribute_name) {
|
|
2862
|
+
if (attributeTableName !== tableName) continue;
|
|
2863
|
+
} else {
|
|
2864
|
+
// table attribute for a table with no primary key, we don't want to remove this, so continue on
|
|
2865
|
+
continue;
|
|
2866
|
+
}
|
|
2867
|
+
const attribute = attributes.find((attribute) => attribute.name === attribute_name);
|
|
2868
|
+
const removeIndex = !attribute?.indexed && value.indexed && !value.isPrimaryKey;
|
|
2869
|
+
// rows already present under a create are aborted state
|
|
2870
|
+
const staleRow = (!attribute && !value.isPrimaryKey) || Boolean(deferredPrimaryRow);
|
|
2871
|
+
if (staleRow || removeIndex) {
|
|
2872
|
+
exclusiveLock();
|
|
2873
|
+
hasChanges = true;
|
|
2874
|
+
if (staleRow) attributesDbi.remove(key);
|
|
2875
|
+
if (removeIndex) {
|
|
2876
|
+
const indexDbi = Table.indices[attributeTableName];
|
|
2877
|
+
if (indexDbi) indicesToRemove.push(indexDbi);
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
1800
2880
|
}
|
|
1801
|
-
const
|
|
1802
|
-
const
|
|
1803
|
-
|
|
2881
|
+
const hasHnswDeclaration = attributes.some((attribute) => attribute.indexed?.type === 'HNSW');
|
|
2882
|
+
const persistedAudit = hasHnswDeclaration ? persistedPrimaryDescriptor(attributesDbi).descriptor?.audit : undefined;
|
|
2883
|
+
// A cluster declaration can apply audit on a create, but deliberately cannot rewrite an existing
|
|
2884
|
+
// table's primary row. Do not let an incoming audit value qualify a replicated native descriptor
|
|
2885
|
+
// that this node would then persist beside its durable audit:false row.
|
|
2886
|
+
const explicitAuditCanBeApplied = origin !== 'cluster' || Boolean(deferredPrimaryRow);
|
|
2887
|
+
const auditEnabledForNativeDefault =
|
|
2888
|
+
(auditExplicitlyEnabled && explicitAuditCanBeApplied) || (!auditExplicitlyDisabled && persistedAudit === true);
|
|
2889
|
+
const auditEnabledForNativePlane =
|
|
2890
|
+
!auditExplicitlyDisabled && (auditEnabledForNativeDefault || (persistedAudit == null && Table.audit === true));
|
|
2891
|
+
for (const attribute of attributes) {
|
|
2892
|
+
const indexed = attribute.indexed;
|
|
2893
|
+
if (!indexed || typeof indexed !== 'object' || indexed.type !== 'HNSW') continue;
|
|
2894
|
+
const descriptor = attributesDbi.getSync(tableName + '/' + (attribute.name || ''));
|
|
2895
|
+
const existingHnsw = descriptor?.indexed?.type === 'HNSW';
|
|
2896
|
+
if (indexed.nativePlane == null) {
|
|
2897
|
+
if (existingHnsw) {
|
|
2898
|
+
if (Object.hasOwn(descriptor.indexed, 'nativePlane')) {
|
|
2899
|
+
indexed.nativePlane = descriptor.indexed.nativePlane;
|
|
2900
|
+
}
|
|
2901
|
+
} else if (
|
|
2902
|
+
origin !== 'cluster' &&
|
|
2903
|
+
auditEnabledForNativeDefault &&
|
|
2904
|
+
CUSTOM_INDEXES.HNSW.canDefaultToNativePlane(rootStore, indexed)
|
|
2905
|
+
) {
|
|
2906
|
+
indexed.nativePlane = true;
|
|
2907
|
+
}
|
|
2908
|
+
} else if (origin === 'cluster' && !existingHnsw && indexed.nativePlane) {
|
|
2909
|
+
let canRunNative = false;
|
|
2910
|
+
try {
|
|
2911
|
+
canRunNative = auditEnabledForNativeDefault && CUSTOM_INDEXES.HNSW.canRunNativePlane(rootStore, indexed);
|
|
2912
|
+
} catch {}
|
|
2913
|
+
if (!canRunNative) {
|
|
2914
|
+
logger.warn(
|
|
2915
|
+
`Using the JS HNSW index for replicated attribute ${databaseName}.${tableName}.${attribute.name} because this node does not satisfy the nativePlane requirements`
|
|
2916
|
+
);
|
|
2917
|
+
indexed.nativePlane = false;
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
const nativePlaneEnabled =
|
|
2922
|
+
origin !== 'cluster' &&
|
|
2923
|
+
attributes.some((attribute) => attribute.indexed?.type === 'HNSW' && attribute.indexed.nativePlane);
|
|
2924
|
+
if (nativePlaneEnabled && !auditEnabledForNativePlane) {
|
|
2925
|
+
throw new ClientError(
|
|
2926
|
+
`Table '${databaseName}.${tableName}' must enable audit logging before using nativePlane because its transaction log is the derived-index recovery source; set nativePlane: false to use the JS index`
|
|
2927
|
+
);
|
|
2928
|
+
}
|
|
2929
|
+
if (nativePlaneEnabled && persistedAudit !== true) audit = true;
|
|
2930
|
+
if (nativePlaneEnabled && persistedAudit !== true && !attributes.some((attribute) => attribute.isPrimaryKey)) {
|
|
1804
2931
|
exclusiveLock();
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
if (
|
|
1808
|
-
|
|
1809
|
-
|
|
2932
|
+
const primaryKey = primaryDescriptorKey();
|
|
2933
|
+
const primaryDescriptor = attributesDbi.getSync(primaryKey);
|
|
2934
|
+
if (primaryDescriptor && !tableIsDropping(primaryDescriptor, primaryKey)) {
|
|
2935
|
+
Table.enableAuditing();
|
|
2936
|
+
attributesDbi.put(primaryKey, { ...primaryDescriptor, audit: true });
|
|
2937
|
+
hasChanges = true;
|
|
1810
2938
|
}
|
|
1811
2939
|
}
|
|
1812
|
-
}
|
|
1813
|
-
const attributesToIndex = [];
|
|
1814
|
-
try {
|
|
1815
2940
|
// TODO: If we have attributes and the schemaDefined flag is not set, turn it on
|
|
1816
2941
|
// iterate through the attributes to ensure that we have all the dbis created and indexed
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
2942
|
+
const attributesInPersistenceOrder = nativePlaneEnabled
|
|
2943
|
+
? [
|
|
2944
|
+
...attributes.filter((attribute) => attribute.isPrimaryKey),
|
|
2945
|
+
...attributes.filter((attribute) => !attribute.isPrimaryKey),
|
|
2946
|
+
]
|
|
2947
|
+
: auditExplicitlyDisabled
|
|
2948
|
+
? [
|
|
2949
|
+
...attributes.filter((attribute) => !attribute.isPrimaryKey),
|
|
2950
|
+
...attributes.filter((attribute) => attribute.isPrimaryKey),
|
|
2951
|
+
]
|
|
2952
|
+
: attributes;
|
|
2953
|
+
for (const attribute of attributesInPersistenceOrder) {
|
|
2954
|
+
if (attribute.relationship) {
|
|
2955
|
+
refreshRelationshipAttributes = true;
|
|
2956
|
+
continue;
|
|
1821
2957
|
}
|
|
2958
|
+
if (attribute.computed) hasChanges = true;
|
|
1822
2959
|
let dbiKey = tableName + '/' + (attribute.name || '');
|
|
1823
2960
|
Object.defineProperty(attribute, 'key', { value: dbiKey, configurable: true });
|
|
1824
2961
|
let attributeDescriptor = attributesDbi.getSync(dbiKey);
|
|
1825
2962
|
if (attribute.isPrimaryKey) {
|
|
2963
|
+
if (deferredPrimaryRow) continue;
|
|
1826
2964
|
attributeDescriptor = attributeDescriptor || attributesDbi.getSync((dbiKey = tableName + '/')) || {};
|
|
1827
2965
|
// Persist schemaDefined when the explicit live value disagrees with disk. Without this,
|
|
1828
2966
|
// a stale `false` (from a v4-era write or replicated event) survives every reload: the
|
|
1829
2967
|
// in-memory re-assert in the existing-Table branch only fixes the worker that ran @table,
|
|
1830
|
-
// but other workers' next disk-load re-reads the stale value.
|
|
2968
|
+
// but other workers' next disk-load re-reads the stale value. The whole settings update is
|
|
2969
|
+
// gated off for cluster-origin callers: their values come from this worker's (possibly
|
|
2970
|
+
// stale) snapshot, so a rewrite could revert a newer local declaration already on disk.
|
|
1831
2971
|
const schemaDefinedMismatch = schemaDefinedExplicit && attributeDescriptor.schemaDefined !== schemaDefined;
|
|
1832
2972
|
// primary key can't change indexing, but settings can change
|
|
1833
2973
|
if (
|
|
1834
|
-
|
|
1835
|
-
(
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
2974
|
+
origin !== 'cluster' &&
|
|
2975
|
+
(schemaDefinedMismatch ||
|
|
2976
|
+
(typeof audit === 'boolean' && audit !== attributeDescriptor.audit) ||
|
|
2977
|
+
(sealed !== undefined && sealed !== Table.sealed) ||
|
|
2978
|
+
(replicate !== undefined && replicate !== Table.replicate) ||
|
|
2979
|
+
(+expiration || undefined) !== (+attributeDescriptor.expiration || undefined) ||
|
|
2980
|
+
(+eviction || undefined) !== (+attributeDescriptor.eviction || undefined) ||
|
|
2981
|
+
attribute.type !== attributeDescriptor.type)
|
|
1841
2982
|
) {
|
|
1842
|
-
|
|
2983
|
+
exclusiveLock();
|
|
2984
|
+
const currentPrimaryAttribute = attributesDbi.getSync(dbiKey);
|
|
2985
|
+
if (!currentPrimaryAttribute || tableIsDropping(currentPrimaryAttribute, dbiKey)) continue;
|
|
2986
|
+
const updatedPrimaryAttribute = { ...currentPrimaryAttribute };
|
|
1843
2987
|
if (typeof audit === 'boolean') {
|
|
1844
2988
|
if (audit) Table.enableAuditing();
|
|
1845
2989
|
updatedPrimaryAttribute.audit = audit;
|
|
@@ -1851,15 +2995,53 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
|
|
|
1851
2995
|
if (attribute.type) updatedPrimaryAttribute.type = attribute.type;
|
|
1852
2996
|
if (schemaDefinedMismatch) updatedPrimaryAttribute.schemaDefined = schemaDefined;
|
|
1853
2997
|
hasChanges = true; // send out notification of the change
|
|
1854
|
-
exclusiveLock();
|
|
1855
2998
|
attributesDbi.put(dbiKey, updatedPrimaryAttribute);
|
|
1856
2999
|
}
|
|
1857
3000
|
|
|
1858
3001
|
continue;
|
|
1859
3002
|
}
|
|
1860
3003
|
|
|
1861
|
-
// note that non-indexed attributes do not need a dbi
|
|
1862
3004
|
if (attributeDescriptor?.attribute && !attributeDescriptor.name) attributeDescriptor.indexed = true; // legacy descriptor
|
|
3005
|
+
|
|
3006
|
+
if (origin === 'cluster' && attributeDescriptor) {
|
|
3007
|
+
// An existing descriptor is a local declaration this caller may not have seen yet, so it wins
|
|
3008
|
+
// over the incoming definition and is never written back from it.
|
|
3009
|
+
applyDurableDeclaration(attribute, attributeDescriptor);
|
|
3010
|
+
const abandonedIndexBuild =
|
|
3011
|
+
attribute.indexed &&
|
|
3012
|
+
(attributeDescriptor.indexingFailed ||
|
|
3013
|
+
isAbandonedIndexBuild(attributeDescriptor, workerData?.restartNumber ?? manageThreads.restartNumber));
|
|
3014
|
+
if (abandonedIndexBuild) {
|
|
3015
|
+
// Recovery is the exception to skipping the handling below, because without it `isIndexing`
|
|
3016
|
+
// stays pinned on with nothing left to clear it and every query on the attribute fails with
|
|
3017
|
+
// IndexRebuildingError for the life of the worker. It persists the attribute (here and again
|
|
3018
|
+
// from runIndexing), so restate the declaration from a descriptor read under the lock.
|
|
3019
|
+
exclusiveLock();
|
|
3020
|
+
applyDurableDeclaration(attribute, attributesDbi.getSync(dbiKey) ?? attributeDescriptor);
|
|
3021
|
+
} else {
|
|
3022
|
+
if (attribute.indexed) {
|
|
3023
|
+
const dbi = openIndex(dbiKey, rootStore, attribute);
|
|
3024
|
+
target.adopt(dbi);
|
|
3025
|
+
// Persisting the indexFormat openIndex just resolved adds a field the descriptor lacks
|
|
3026
|
+
// rather than rewriting one it has. Without it an empty index resolves 'versioned', writes
|
|
3027
|
+
// versioned nodes, then re-derives 'legacy' on the next load — see indexFormatNeedsPersist.
|
|
3028
|
+
if (attribute.indexFormat != null && attributeDescriptor.indexFormat == null) {
|
|
3029
|
+
exclusiveLock();
|
|
3030
|
+
const durableDescriptor = attributesDbi.getSync(dbiKey);
|
|
3031
|
+
if (durableDescriptor && durableDescriptor.indexFormat == null) {
|
|
3032
|
+
hasChanges = true;
|
|
3033
|
+
attributesDbi.put(dbiKey, { ...durableDescriptor, indexFormat: attribute.indexFormat });
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
if (attributeDescriptor.indexingPID) dbi.isIndexing = true;
|
|
3037
|
+
dbi.indexNulls = attribute.indexNulls;
|
|
3038
|
+
indices[attribute.name] = dbi;
|
|
3039
|
+
}
|
|
3040
|
+
continue;
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
// note that non-indexed attributes do not need a dbi
|
|
1863
3045
|
// Some index options affect only search, not the stored structure (e.g. HNSW's
|
|
1864
3046
|
// efConstructionSearch). Changing those should persist the new metadata but NOT trigger a
|
|
1865
3047
|
// reindex. A custom index declares such keys via a static `searchOnlyOptions`.
|
|
@@ -1903,6 +3085,8 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
|
|
|
1903
3085
|
// on the main thread, where workerData is undefined (and it is initialized to 1).
|
|
1904
3086
|
const currentRestartGeneration = workerData?.restartNumber ?? manageThreads.restartNumber;
|
|
1905
3087
|
const dbi = openIndex(dbiKey, rootStore, attribute);
|
|
3088
|
+
target.adopt(dbi);
|
|
3089
|
+
if (deferredPrimaryRow) indices[attribute.name] = dbi; // private until published; lets the rollback close it
|
|
1906
3090
|
// openIndex resolves and stamps attribute.indexFormat for a versioned-capable (RocksDB
|
|
1907
3091
|
// custom-object) index. An index created before this field existed has no indexFormat on
|
|
1908
3092
|
// disk; persist the resolved value now — even when nothing else changed — so the format is
|
|
@@ -2049,45 +3233,133 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
|
|
|
2049
3233
|
attributesDbi.put(dbiKey, attribute);
|
|
2050
3234
|
}
|
|
2051
3235
|
}
|
|
3236
|
+
// The primary row is what makes a table loadable, so it lands last: a scan on another thread that
|
|
3237
|
+
// runs mid-create skips the table instead of building (and announcing) a partial one. It already
|
|
3238
|
+
// carries this table's relationships (set on primaryKeyAttribute above), so the persistence block
|
|
3239
|
+
// below is a no-op for a create — a table is never published with an incomplete relationship list.
|
|
3240
|
+
if (deferredPrimaryRow) {
|
|
3241
|
+
attributesDbi.put(tableName + '/', deferredPrimaryRow);
|
|
3242
|
+
// That write, not the registration below, is the publish point: it is durable from here
|
|
3243
|
+
// (on LMDB releaseLock()'s finally commits this create's write transaction even while an
|
|
3244
|
+
// error unwinds), so any later throw must leave the catalog alone. Rolling back past it
|
|
3245
|
+
// would delete the attribute rows out from under a live primary row and leave every
|
|
3246
|
+
// thread loading the primary-only schema this change exists to prevent.
|
|
3247
|
+
published = true;
|
|
3248
|
+
setTable(tables, tableName, Table);
|
|
3249
|
+
}
|
|
3250
|
+
// a table with no declared primary key has no attribute row to carry relationships, and the
|
|
3251
|
+
// loop above never visits its descriptor
|
|
3252
|
+
if (relationshipDefinitions) {
|
|
3253
|
+
const relationshipsKey = primaryDescriptorKey();
|
|
3254
|
+
if (!relationshipListsEqual(attributesDbi.getSync(relationshipsKey)?.relationships, relationshipDefinitions)) {
|
|
3255
|
+
exclusiveLock();
|
|
3256
|
+
const currentPrimaryAttribute = attributesDbi.getSync(relationshipsKey);
|
|
3257
|
+
// a missing row means a concurrent drop completed; writing one back would resurrect the table
|
|
3258
|
+
if (
|
|
3259
|
+
currentPrimaryAttribute &&
|
|
3260
|
+
!tableIsDropping(currentPrimaryAttribute, relationshipsKey) &&
|
|
3261
|
+
!relationshipListsEqual(currentPrimaryAttribute.relationships, relationshipDefinitions)
|
|
3262
|
+
) {
|
|
3263
|
+
attributesDbi.put(relationshipsKey, { ...currentPrimaryAttribute, relationships: relationshipDefinitions });
|
|
3264
|
+
hasChanges = true;
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
} catch (error) {
|
|
3269
|
+
if (unpublishedPrimaryStore && !published) discardUnpublishedTable();
|
|
3270
|
+
else if (published && tables[tableName] !== Table) discardUnregisteredClass();
|
|
3271
|
+
throw error;
|
|
2052
3272
|
} finally {
|
|
2053
|
-
|
|
2054
|
-
}
|
|
2055
|
-
if (hasChanges) {
|
|
2056
|
-
Table.schemaVersion++;
|
|
2057
|
-
Table.updatedAttributes();
|
|
3273
|
+
releaseLock();
|
|
2058
3274
|
}
|
|
3275
|
+
if (hasChanges || refreshRelationshipAttributes) Table.schemaVersion++;
|
|
3276
|
+
if (hasChanges || refreshRelationshipAttributes || refreshedLiveAttributes) Table.updatedAttributes();
|
|
2059
3277
|
logger.trace(`${tableName} table loading, running index`);
|
|
3278
|
+
const branchPath = target.branch?.path;
|
|
2060
3279
|
if (attributesToIndex.length > 0 || indicesToRemove.length > 0) {
|
|
2061
3280
|
// captured before the backfill can rewrite the attributes
|
|
2062
3281
|
const buildIds = new Map(attributesToIndex.map((attribute) => [attribute, attribute.indexingBuildId]));
|
|
2063
3282
|
const markSettled = () => markAbandonedIndexBuild(Table, rootStore, buildIds);
|
|
2064
|
-
Table.indexingOperation = runIndexing(Table, attributesToIndex, indicesToRemove).then(
|
|
3283
|
+
Table.indexingOperation = runIndexing(Table, attributesToIndex, indicesToRemove, branchPath).then(
|
|
3284
|
+
markSettled,
|
|
3285
|
+
markSettled
|
|
3286
|
+
);
|
|
2065
3287
|
} else if (hasChanges)
|
|
2066
3288
|
signalling.signalSchemaChange(
|
|
2067
|
-
new SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName)
|
|
3289
|
+
new SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName, undefined, branchPath)
|
|
2068
3290
|
);
|
|
3291
|
+
void Table.derivedIndexRuntime?.close();
|
|
3292
|
+
Table.derivedIndexRuntime = attachDerivedIndexes(Table);
|
|
2069
3293
|
|
|
2070
3294
|
Table.origin = origin;
|
|
2071
|
-
|
|
3295
|
+
// scope-private: replication and other global subscribers must not learn of a branch class
|
|
3296
|
+
if ((hasChanges || refreshRelationshipAttributes) && !target.branch) {
|
|
2072
3297
|
databaseEventsEmitter.emit('updateTable', Table, origin !== 'cluster');
|
|
2073
3298
|
}
|
|
2074
|
-
if (expiration || eviction || scanInterval)
|
|
3299
|
+
if (expiration || eviction || scanInterval || attributes.some((attribute) => attribute.expiresAt))
|
|
2075
3300
|
Table.setTTLExpiration({
|
|
2076
3301
|
expiration,
|
|
2077
3302
|
eviction,
|
|
2078
3303
|
scanInterval,
|
|
3304
|
+
fromSchema: true,
|
|
3305
|
+
isolatedApplicationOwner,
|
|
2079
3306
|
});
|
|
2080
3307
|
logger.trace(`${tableName} table loaded`);
|
|
2081
3308
|
|
|
2082
3309
|
return Table as TableResourceType;
|
|
3310
|
+
// A migrated catalog can retain a named primary descriptor beside a bare table tombstone, so a
|
|
3311
|
+
// drop in flight has to be checked on both representations.
|
|
3312
|
+
function tableIsDropping(descriptor: any, descriptorKey: string) {
|
|
3313
|
+
if (descriptor?.dropping) return true;
|
|
3314
|
+
return descriptorKey !== tableName + '/' && attributesDbi.getSync(tableName + '/')?.dropping;
|
|
3315
|
+
}
|
|
3316
|
+
// The catalog row initStores() reads a table's settings from: the primary key's own row when it
|
|
3317
|
+
// has one, and the bare table row otherwise.
|
|
3318
|
+
function primaryDescriptorKey() {
|
|
3319
|
+
return persistedPrimaryDescriptor(attributesDbi).key;
|
|
3320
|
+
}
|
|
3321
|
+
// The catalog of a published table stays, but a class the registration never accepted is
|
|
3322
|
+
// unreachable, so release what makeTable() registered process-wide instead of leaving its timers
|
|
3323
|
+
// and reclamation handler live for the process. The stores stay open: the table is durable, and
|
|
3324
|
+
// whichever scan reloads it opens its own handles.
|
|
3325
|
+
function discardUnregisteredClass() {
|
|
3326
|
+
try {
|
|
3327
|
+
Table.cleanup();
|
|
3328
|
+
} catch (discardError) {
|
|
3329
|
+
logger.warn(`Error releasing the unregistered class of ${databaseName}.${tableName}`, discardError);
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
function discardUnpublishedTable() {
|
|
3333
|
+
const discard = (description: string, action: () => unknown) => {
|
|
3334
|
+
try {
|
|
3335
|
+
action();
|
|
3336
|
+
} catch (discardError) {
|
|
3337
|
+
logger.warn(
|
|
3338
|
+
`Error discarding ${description} of the failed create of ${databaseName}.${tableName}`,
|
|
3339
|
+
discardError
|
|
3340
|
+
);
|
|
3341
|
+
}
|
|
3342
|
+
};
|
|
3343
|
+
discard('catalog rows', () => {
|
|
3344
|
+
for (const attribute of attributes) {
|
|
3345
|
+
if (!attribute.isPrimaryKey && !attribute.relationship) attributesDbi.remove(tableName + '/' + attribute.name);
|
|
3346
|
+
}
|
|
3347
|
+
});
|
|
3348
|
+
if (Table) discard('callbacks', () => Table.cleanup());
|
|
3349
|
+
// an LMDB store is a per-environment handle slot shared with every thread and still inside this
|
|
3350
|
+
// create's write transaction; only RocksDB column-family handles hold native state to release
|
|
3351
|
+
if (rootStore instanceof RocksDatabase) {
|
|
3352
|
+
for (const indexName in Table?.indices ?? {})
|
|
3353
|
+
discard(`index ${indexName}`, () => Table.indices[indexName].close());
|
|
3354
|
+
discard('primary store', () => unpublishedPrimaryStore.close());
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
2083
3357
|
// Acquire an exclusive lock for attribute updates
|
|
2084
3358
|
function exclusiveLock() {
|
|
2085
3359
|
if (releaseExclusiveLock) return;
|
|
2086
3360
|
if (rootStore instanceof RocksDatabase) {
|
|
2087
|
-
|
|
2088
|
-
releaseExclusiveLock = () =>
|
|
2089
|
-
rootStore.unlock('update-attributes');
|
|
2090
|
-
};
|
|
3361
|
+
acquireUpdateAttributesLock(rootStore, `table '${databaseName}.${tableName}'`);
|
|
3362
|
+
releaseExclusiveLock = () => releaseUpdateAttributesLock(rootStore);
|
|
2091
3363
|
} else {
|
|
2092
3364
|
// we only need an exclusive transaction lock in lmdb
|
|
2093
3365
|
rootStore.transactionSync(() => {
|
|
@@ -2099,35 +3371,41 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
|
|
|
2099
3371
|
});
|
|
2100
3372
|
}
|
|
2101
3373
|
}
|
|
3374
|
+
// idempotent: the early release before the recursive reload and the finally both run, and a
|
|
3375
|
+
// second unlock could release another thread's lock
|
|
3376
|
+
function releaseLock() {
|
|
3377
|
+
const release = releaseExclusiveLock;
|
|
3378
|
+
releaseExclusiveLock = undefined;
|
|
3379
|
+
if (release) release();
|
|
3380
|
+
}
|
|
2102
3381
|
}
|
|
2103
3382
|
/**
|
|
2104
|
-
*
|
|
2105
|
-
*
|
|
2106
|
-
* and config objects can supply them reordered or as numbers; without canonicalizing, such a
|
|
2107
|
-
* representation-only difference flips the structural comparison and forces a needless full rebuild
|
|
2108
|
-
* (clearing + rebuilding the index, 503-ing the attribute throughout) for a semantically identical
|
|
2109
|
-
* index. Sorts object keys and coerces numeric-looking (non-zero) string scalars to numbers.
|
|
2110
|
-
* Conservative by design: boolean-vs-object, absent-vs-present, and string-"0"-vs-number-0
|
|
2111
|
-
* differences are all preserved, so a genuine change (`true` vs `{ type: 'HNSW' }`, an added/removed
|
|
2112
|
-
* option, a changed value) still triggers a rebuild. Persistence keys off the raw form, so the stored
|
|
2113
|
-
* descriptor self-heals toward this shape over time. harper#1357
|
|
3383
|
+
* Stable structural form for deciding whether an index must be rebuilt. `coerceZero` extends numeric
|
|
3384
|
+
* coercion to zero; a truthiness-sensitive numeric option must normalize its value before using it.
|
|
2114
3385
|
*/
|
|
2115
|
-
export function canonicalizeIndexOptions(value: any): any {
|
|
2116
|
-
if (Array.isArray(value)) return value.map(canonicalizeIndexOptions);
|
|
3386
|
+
export function canonicalizeIndexOptions(value: any, coerceZero = false): any {
|
|
3387
|
+
if (Array.isArray(value)) return value.map((item) => canonicalizeIndexOptions(item, coerceZero));
|
|
2117
3388
|
if (value && typeof value === 'object') {
|
|
2118
3389
|
const canonical: Record<string, any> = {};
|
|
2119
|
-
|
|
3390
|
+
const customIndex = value.type && (CUSTOM_INDEXES as Record<string, any>)[value.type];
|
|
3391
|
+
for (const key of Object.keys(value).sort()) {
|
|
3392
|
+
if (customIndex?.truthyStructuralOptions?.has(key)) {
|
|
3393
|
+
if (value[key]) canonical[key] = true;
|
|
3394
|
+
continue;
|
|
3395
|
+
}
|
|
3396
|
+
const optionValue = customIndex?.normalizeOptionValue
|
|
3397
|
+
? customIndex.normalizeOptionValue(key, value[key])
|
|
3398
|
+
: value[key];
|
|
3399
|
+
canonical[key] = canonicalizeIndexOptions(
|
|
3400
|
+
optionValue,
|
|
3401
|
+
coerceZero || Boolean(customIndex?.numericOptions?.has(key))
|
|
3402
|
+
);
|
|
3403
|
+
}
|
|
2120
3404
|
return canonical;
|
|
2121
3405
|
}
|
|
2122
|
-
// Coerce numeric-looking strings ("16" -> 16) so string-vs-number representations of the same
|
|
2123
|
-
// option compare equal — EXCEPT zero: the string "0" is truthy while the number 0 is falsy, and
|
|
2124
|
-
// index code may branch on truthiness (e.g. HNSW `if (this.optimizeRouting)` doubles maxConnections),
|
|
2125
|
-
// so "0" and 0 build structurally different indexes and must still trigger a rebuild. Zero is the
|
|
2126
|
-
// only finite number whose string and numeric forms diverge in truthiness, so excluding it fully
|
|
2127
|
-
// closes that gap. Leave non-numeric strings, booleans, null, etc. intact.
|
|
2128
3406
|
if (typeof value === 'string' && value.trim() !== '') {
|
|
2129
3407
|
const numeric = Number(value);
|
|
2130
|
-
if (numeric !== 0 && Number.isFinite(numeric)) return numeric;
|
|
3408
|
+
if ((numeric !== 0 || coerceZero) && Number.isFinite(numeric)) return numeric;
|
|
2131
3409
|
}
|
|
2132
3410
|
return value;
|
|
2133
3411
|
}
|
|
@@ -2180,34 +3458,6 @@ export function resumeStartKey(attributes: { lastIndexedKey?: any }[]): any {
|
|
|
2180
3458
|
return start;
|
|
2181
3459
|
}
|
|
2182
3460
|
|
|
2183
|
-
// Bounded, unlike the exclusiveLock() spin above: that one runs on the declaring path, where the
|
|
2184
|
-
// caller is waiting on the result and there is nothing useful to do without the lock. This one runs
|
|
2185
|
-
// after a backfill has already settled, so a holder that never releases would wedge the worker's
|
|
2186
|
-
// event loop for nothing. Giving up costs only the marker, and the next load of the table
|
|
2187
|
-
// re-triggers the build regardless.
|
|
2188
|
-
export const ABANDONED_MARK_LOCK_TIMEOUT = 10000;
|
|
2189
|
-
const abandonedMarkLockWait = new Int32Array(new SharedArrayBuffer(4));
|
|
2190
|
-
// `timeout` is the test seam; production callers take the default.
|
|
2191
|
-
export function tryAcquireUpdateAttributesLock(
|
|
2192
|
-
rootStore: RocksDatabase,
|
|
2193
|
-
timeout = ABANDONED_MARK_LOCK_TIMEOUT
|
|
2194
|
-
): boolean {
|
|
2195
|
-
if (rootStore.tryLock('update-attributes')) return true;
|
|
2196
|
-
const startTime = performance.now();
|
|
2197
|
-
let waitTime = 1;
|
|
2198
|
-
while (!rootStore.tryLock('update-attributes')) {
|
|
2199
|
-
const elapsed = performance.now() - startTime;
|
|
2200
|
-
if (elapsed >= timeout) return false;
|
|
2201
|
-
// Atomics.wait rather than a busy spin: the section this guards is synchronous, so the wait
|
|
2202
|
-
// blocks this thread either way, but sleeping does not burn a core while it does.
|
|
2203
|
-
if (elapsed >= 2) {
|
|
2204
|
-
Atomics.wait(abandonedMarkLockWait, 0, 0, Math.min(waitTime, timeout - elapsed));
|
|
2205
|
-
if (waitTime < 16) waitTime *= 2;
|
|
2206
|
-
}
|
|
2207
|
-
}
|
|
2208
|
-
return true;
|
|
2209
|
-
}
|
|
2210
|
-
|
|
2211
3461
|
/**
|
|
2212
3462
|
* Persists the failure marker for a build that ended without running one of runIndexing's own exit
|
|
2213
3463
|
* paths, so something re-triggers it. Fenced on `indexingBuildId` inside the storage engine's catalog
|
|
@@ -2229,18 +3479,11 @@ async function markAbandonedIndexBuild(Table, rootStore, buildIds: Map<any, stri
|
|
|
2229
3479
|
}
|
|
2230
3480
|
};
|
|
2231
3481
|
if (rootStore instanceof RocksDatabase) {
|
|
2232
|
-
|
|
2233
|
-
logger.warn(
|
|
2234
|
-
`Could not mark the abandoned index build of ${Table.databaseName}.${Table.tableName}.${attribute.name}: ` +
|
|
2235
|
-
`timed out after ${ABANDONED_MARK_LOCK_TIMEOUT}ms waiting for the exclusive 'update-attributes' lock. ` +
|
|
2236
|
-
`The index stays incomplete and the next load of the table re-triggers the backfill.`
|
|
2237
|
-
);
|
|
2238
|
-
continue;
|
|
2239
|
-
}
|
|
3482
|
+
acquireUpdateAttributesLock(rootStore, `abandoned index build '${Table.tableName}.${attribute.name}'`);
|
|
2240
3483
|
try {
|
|
2241
3484
|
markIfOwned();
|
|
2242
3485
|
} finally {
|
|
2243
|
-
rootStore
|
|
3486
|
+
releaseUpdateAttributesLock(rootStore);
|
|
2244
3487
|
}
|
|
2245
3488
|
} else {
|
|
2246
3489
|
rootStore.transactionSync(markIfOwned);
|
|
@@ -2259,7 +3502,7 @@ async function markAbandonedIndexBuild(Table, rootStore, buildIds: Map<any, stri
|
|
|
2259
3502
|
}
|
|
2260
3503
|
}
|
|
2261
3504
|
}
|
|
2262
|
-
async function runIndexing(Table, attributes, indicesToRemove) {
|
|
3505
|
+
async function runIndexing(Table, attributes, indicesToRemove, branchPath?: string) {
|
|
2263
3506
|
let checkpointing;
|
|
2264
3507
|
let hadIndexingErrors = false;
|
|
2265
3508
|
const attributeErrorReported = {};
|
|
@@ -2273,7 +3516,7 @@ async function runIndexing(Table, attributes, indicesToRemove) {
|
|
|
2273
3516
|
try {
|
|
2274
3517
|
logger.info(`Indexing ${Table.tableName} attributes`, attributes);
|
|
2275
3518
|
await signalling.signalSchemaChange(
|
|
2276
|
-
new SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName)
|
|
3519
|
+
new SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName, undefined, branchPath)
|
|
2277
3520
|
);
|
|
2278
3521
|
let lastResolution;
|
|
2279
3522
|
// The checkpoint and completion barriers have to cover every mutation still in flight: any of them
|
|
@@ -2312,6 +3555,7 @@ async function runIndexing(Table, attributes, indicesToRemove) {
|
|
|
2312
3555
|
};
|
|
2313
3556
|
});
|
|
2314
3557
|
for (const index of indicesToRemove) {
|
|
3558
|
+
index.customIndex?.resetDerivedStorage?.();
|
|
2315
3559
|
track(index.drop(), (error) => onIndexPutRejected(index.name, error));
|
|
2316
3560
|
}
|
|
2317
3561
|
let interrupted;
|
|
@@ -2322,6 +3566,8 @@ async function runIndexing(Table, attributes, indicesToRemove) {
|
|
|
2322
3566
|
const start = resumeStartKey(attributes);
|
|
2323
3567
|
if (start === undefined) {
|
|
2324
3568
|
for (const attribute of attributes) {
|
|
3569
|
+
// if we are starting from the beginning, clear out any previous index entries since we are rewriting
|
|
3570
|
+
attribute.dbi.customIndex?.resetDerivedStorage?.();
|
|
2325
3571
|
if (attribute.dbi.clearAsync) {
|
|
2326
3572
|
// LMDB enqueues this ahead of the index writes, so the scan need not wait for it — but the
|
|
2327
3573
|
// barriers must, or a rejected clear certifies a checkpoint over stale entries.
|
|
@@ -2491,7 +3737,7 @@ async function runIndexing(Table, attributes, indicesToRemove) {
|
|
|
2491
3737
|
await lastResolution;
|
|
2492
3738
|
// now notify all the threads that we are done and the index is ready to use
|
|
2493
3739
|
await signalling.signalSchemaChange(
|
|
2494
|
-
new SchemaEventMsg(process.pid, 'indexing-finished', Table.databaseName, Table.tableName)
|
|
3740
|
+
new SchemaEventMsg(process.pid, 'indexing-finished', Table.databaseName, Table.tableName, undefined, branchPath)
|
|
2495
3741
|
);
|
|
2496
3742
|
logger.info(`Finished indexing ${Table.tableName} attributes`, attributes);
|
|
2497
3743
|
}
|
|
@@ -2560,6 +3806,24 @@ function completeInterruptedDrop(rootStore, attributesDbi, databaseName: string,
|
|
|
2560
3806
|
} finally {
|
|
2561
3807
|
columnStore.close();
|
|
2562
3808
|
}
|
|
3809
|
+
// derived HNSW plane files live next to the store; the normal drop path removes
|
|
3810
|
+
// them through the custom index, but this recovery path drops raw column stores,
|
|
3811
|
+
// and a same-name recreate must never open a stale plane over a fresh CF
|
|
3812
|
+
try {
|
|
3813
|
+
unlinkSync(planeFilePathFor(rootStore.path, columnName));
|
|
3814
|
+
} catch (error: any) {
|
|
3815
|
+
// a stale plane left behind (e.g. Windows EBUSY while still mapped) would be
|
|
3816
|
+
// opened over a fresh same-name CF, resolving another graph's node ids
|
|
3817
|
+
// against it — tombstone it so no attach ever adopts it
|
|
3818
|
+
if (error?.code !== 'ENOENT') {
|
|
3819
|
+
logger.warn(`could not delete the HNSW plane file for ${columnName}; tombstoning it as stale`, error);
|
|
3820
|
+
try {
|
|
3821
|
+
closeSync(openSync(planeStalePathFor(planeFilePathFor(rootStore.path, columnName)), 'w'));
|
|
3822
|
+
} catch (tombstoneError) {
|
|
3823
|
+
logger.warn(`could not tombstone the stale HNSW plane file for ${columnName}`, tombstoneError);
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
2563
3827
|
}
|
|
2564
3828
|
}
|
|
2565
3829
|
} else {
|