Repository navigation
provision-roles.sql stops at its role-collision guard, and the collector toggle verbs exit 2 for a store problem (#4746, #4744) - #4809
Merged
Conversation
erikdarlingdata
marked this pull request as ready for review
September 29, 2026 12:09
…ON_ERROR_STOP instead of forcing it off (#4746) The guard switched ON_ERROR_STOP on and then to off, which overrode a caller who ran psql with -v ON_ERROR_STOP=1 or set it in psqlrc: the script went on through every error and exited 0. It now saves the caller's value before the guard and sets it back after, and the pins check the save, the restore and that nothing forces it off.
…ction string that cannot be used exits 1 instead of crashing (#4744) An unparsable postgres.connectionString (for example sslmode=NotARealSslMode), or a managed credential file that cannot be read or unprotected, threw out of the connection build in the two collector verbs and in --check-settings, and the process died with an unhandled exception. The verbs now print 'postgres.connectionString could not be used: ...' and exit with the usage-or-config code (1); --check-settings exits with its config code (1).
… psql (#4746) Until now only line-order pins guarded the role-collision guard, and the Npgsql script test drops the script's backslash lines and stops at the RAISE without them. Two gated tests run the shipped script with the runtime's psql.exe (-X -f): one against a cluster with an unmarked viewer role, which must exit 3 with no admin role created and viewer's password hash unchanged; one against the maintenance database, where the grants fail, which must exit 0 on a plain run and 3 when the caller passed -v ON_ERROR_STOP=1.
… wrong password exits 2, and run provision-roles.sql with psql -X (#4746, #4744) The role-collision check tests only the darling-managed marker, so the README now says a role is refused when it lacks the marker rather than that Darling did not create it. The collector verbs exit 2 for a refused login as well as an unreachable store or a refused change, so the README no longer claims the codes separate a script's own mistake from a store that is down. The psql command in the README and in the script header now passes -X: an AUTOCOMMIT off in the operator's psqlrc would leave every role, grant and setting uncommitted while psql exits 0. Pins for all three.
…nd application name on the constructor call, as the connection-string censuses require (#4744)
… connection string, not crash (#4744) Fifteen of the twenty-one cases fail on the current sources: --validate-config, the four endpoint toggles, --collapse-legacy-slices, --recompress-plan-dim, --add-server and --backfill-rollups throw on a connection string Npgsql cannot parse or a managed credential file that cannot be read.
…onnection string instead of crashing (#4744) --validate-config, the four endpoint toggles, --collapse-legacy-slices, --recompress-plan-dim, --add-server and --backfill-rollups threw an unhandled exception when postgres.connectionString could not be parsed or a managed credential file could not be read. One shared method now builds the store connection string and catches that, the way --enable-collector, --disable-collector and --check-settings did on their own; each verb prints its message and exits 1 (--check-settings keeps its config code, which is also 1).
…that, not as a bad connection string (#4744) A managed store has no postgres.connectionString to blame. When its stored credential file cannot be read or unprotected, the shared store-connection method now says the stored store credential could not be read, and a DPAPI failure gets DarlingSecrets.DescribeDecryptFailure's explanation instead of CryptographicException's raw text. The --configure-firewall note that reports the same failure uses the same detail.
Only psql 10 and later read an unset ON_ERROR_STOP as off. In psql 9.6 :ON_ERROR_STOP stays literal and reads as on, so the run stops at its first error and an owner without superuser gets no grants. The script header, the comment at the collision guard, the README and a pin now say psql 10 or later. The README also says which of --check-settings and the collector toggles exits with which code (a managed credential that was never stored is 2 from --check-settings and 1 from the toggles; an empty connection string is 1 from both) and that every other verb that opens the store exits 1 on an unusable connection string.
… with the server-password remedies (fails until the fix in the next commit) (#4744) The DPAPI case of the unusable-store-connection tests now requires the store's own credential file name in the message and refuses the monitored-server remedies (re-add the server, --add-server, --encrypt-password) and the words SQL Server. It fails on all 12 verbs until the next commit gives the store credential its own text.
… not the server-password remedies (#4744) A managed store's pg-credential.dpapi that DPAPI cannot unprotect was explained with the text written for a monitored server's saved password: it said the failure was not SQL Server rejecting a login, blamed a Viewer on a different PC, and told the operator to re-add the server, run --add-server or --encrypt-password, or use an env:/file: reference. None of that applies to the store owner's generated password, which cannot be regenerated. DarlingSecrets.DescribeStoreCredentialDecryptFailure now says it is a Windows Data Protection failure on this machine, names the credential file, and says to run the command on the machine that created the store. Both users of StoreCredentialFailureDetail carry the new text; DescribeDecryptFailure is unchanged.
This was referenced Sep 30, 2026
Closed
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4746
Fixes #4744
Why
#4746.
Darling/tools/provision-roles.sqlhad noON_ERROR_STOP, and psql carries on after an error by default. The role-collision guard'sRAISEfailed only its ownDOblock. So when a role namedadmin,viewerormcpalready existed without Darling'sdarling-managedmarker, theALTER ROLE ... PASSWORDlines right after the guard reset that role's password, and every grant after them ran too. A comment above thetemp_file_limitlines also said a failure there stops the script. It does not: psql carries on.Around that stop:
\set ON_ERROR_STOP off) would undo a caller's-v ON_ERROR_STOP=1or a psqlrc setting: the script would go on through every later error and exit 0, where a caller who asked for a stop expects exit 3. So the script saves the caller's value and puts it back.\setlines and would stop at theRAISEwithout them, so nothing ran the script through psql.-X, so the operator's psqlrc applied. With\set AUTOCOMMIT offin it, psql opens a transaction before the first statement and never commits it: psql exits 0 and every role, grant and setting rolls back.#4744.
--enable-collectorand--disable-collectorreturned 1 for every failure (12return 1lines inToggleCollectorAsync), so a script could not tell bad arguments from a store that is down.--check-settingsalready separates its failure kinds withCheckSettingsExitCode. Also, apostgres.connectionStringthat Npgsql cannot parse (for examplesslmode=NotARealSslMode), or a managed credential file that cannot be read or unprotected, threw out of the connection build in both verbs and in--check-settings, and the process ended with an unhandled exception instead of an exit code.What changes
Darling/tools/provision-roles.sql\set darling_saved_stop :ON_ERROR_STOPsaves the caller's value just before the guard,\set ON_ERROR_STOP onsits right before step 1's guard, and\set ON_ERROR_STOP :darling_saved_stopputs the caller's value back right after it, before the firstALTER ROLE ... PASSWORD. Only the guard stops the run when the caller asked for nothing (psql exits with status 3); a caller who passed-v ON_ERROR_STOP=1keeps it for the whole script. From psql 10, an unsetON_ERROR_STOPreads as off, so the saved value is alwaysonoroff. In psql 9.6 the unset variable stays the literal:ON_ERROR_STOPand reads as on, so the run would stop at its first error and an owner without superuser would get no grants (which contradicts the README). The script header, the comment at the guard and the README now say psql 10 or later. Step 0'sSETbefore the guard and thetemp_file_limitand slow-statement lines after it keep the caller's setting (psql's default is to carry on), so an owner without superuser still gets every grant it can make. A global-v ON_ERROR_STOP=1in the README's command would have stopped that owner at thetemp_file_limitline, before any grant, so the README's command does not get one.DOblock with the three checks in it (not three blocks), so the save and restore bracket that one block.temp_file_limitcomment now says a failure there does not stop the script, and what to do about it.-X, with one line saying why.Darling/PerformanceMonitor.Darling.Service/DarlingCliCommands.csCollectorToggleExitCode:Success = 0,UsageOrConfig = 1,StoreUnavailable = 2.return 1lines inToggleCollectorAsyncnow returns the constant for its kind. Nine areUsageOrConfig: bad arguments, an unknown collector, a config that will not load, nopostgressection, a managed store on a machine that is not Windows, a managed store whose credential is not stored, an empty connection string, a--serverthat names no server, and a--serverthat names more than one. Three areStoreUnavailable: the servers registry cannot be read, the write fails, and the read-back after the write fails. The success path returnsSuccess. The doc comment names the codes.TryBuildStoreConnectionString, builds the store connection string for every verb that opens the store, and holds the onlycatch (Exception ex) when (ex is not OperationCanceledException)for it. A managed store's credential read (TryBuildConnectionStringFromStoredCredential) and, for a bring-your-own string,EnsureStoreSearchPath(the collector toggles ask for it) run inside it, and a non-blank string is proved by building it the way the verb is about to (time zone pin and application name), so the verb's own build a few lines later cannot throw on it. It returns the message to print; the sentencepostgres.connectionString could not be used: <reason>is written in that one method and nowhere else.--validate-config,--check-settings, the four endpoint verbs (--enable-mcp,--disable-mcp,--enable-web,--disable-web),--collapse-legacy-slices,--recompress-plan-dim,--add-server,--backfill-rollups,--enable-collectorand--disable-collector. Each prints the message and exits 1;--check-settingsreturnsCheckSettingsExitCode.ConfigError, which is also 1. The catchesToggleCollectorAsyncandCheckSettingsAsynccarried are gone. The other verbs let the exception escape when the connection string could not be parsed or the managed credential file could not be read.--validate-configstill falls back to darling.json's own server list for a store that is merely unreachable; only a store setting that cannot be used at all exits 1. Each verb still builds its own connection with the pin and name on the constructor call, as the census tests require, and keeps its own wording for an empty string or a credential that was never stored.The stored store credential could not be read: <detail>, not a message that blamespostgres.connectionString. A DPAPI failure (CryptographicException) gets its own explanation,DarlingSecrets.DescribeStoreCredentialDecryptFailure, instead of the raw "Key not valid for use in specified state". It names the store's credential file (pg-credential.dpapi), says only the machine that wrote it can read it, and does not offer the fixes meant for a monitored server's saved password (--add-server,--encrypt-password) thatDescribeDecryptFailuregives.DescribeDecryptFailureitself is unchanged; anything else (a file that is not base64, a permission error) keeps its own message. The--configure-firewallnote that already saidthe stored store credential could not be read (...)takes the same detail, so the two cannot drift. That note printedex.Messagebefore; it did not callDescribeDecryptFailure.Darling/Darling.TestsComposeStoreRolesLiveTests.cs: the script is still fed to Npgsql throughWithoutPsqlMetaCommands(now three backslash lines), and two gated tests run the runtime's ownpsql.exe -X -fon the shipped script. One plants an unmarkedviewerlogin with a known password and asserts psql exits 3, noadminormcprole exists, and viewer'srolpasswordand marker are unchanged. The other runs it against the cluster's maintenance database, where the grants fail, and asserts exit 0 on a plain run and exit 3 with-v ON_ERROR_STOP=1.ProvisionRolesCollisionGuardTests.cs: the stop pin follows the restore line; new pins for the save before the guard, the restore right after it and no line that forces the variable off; both shown psql commands pass-X; the README says the check tests the marker alone.DarlingCollectorToggleExitCodeTests.cs: a connection string that does not parse exits 1 for both verbs and for--check-settings; a managed credential file that cannot be unprotected does the same (Windows only) and now asserts the managed wording; the README pin also holds that a refused login exits 2 and which case differs between--check-settingsand the toggles; two new tests show that an empty connection string is 1 from both and a managed credential that was never stored is 2 from--check-settingsand 1 from the toggles.DarlingCliUnusableStoreConnectionTests.cs: one case per verb that opens the store, for a bring-your-own string withsslmode=NotARealSslMode(8 verbs; the four endpoint verbs stop earlier, at the managed-mode guard), for a managed credential file that is not base64 (all 12 verbs, Windows only), and for a well-formed blob that DPAPI cannot unprotect (all 12, Windows only, asserts the DPAPI wording and no raw "Key not valid"). Each expects exit 1 and no exception. A wiring pin holds that the sentence appears once, that 8 call sites use the shared method, and that the stored credential is read bare in only two places (the shared method, and the firewall verb's best-effort note).ProvisionRolesCollisionGuardTests.cs: the comment that repeated the psql claim now says psql 10; a new pin holds that the script and the README say psql 10 or later and that the old sentence is gone.Darling/README.mddarling-managedmarker) and that any other error is printed while the run carries on. The command passes-X, with a sentence on why: anAUTOCOMMIT offorON_ERROR_STOPin the operator's psqlrc would change the run.--enable-collector/--disable-collectorcodes are listed next to the--check-settingscodes, and the verbs' own section says which failure is which, including that a refused login exits 2 (a wrong password is the store's answer, 28P01, pinned by a test) and that a connection string that cannot be used exits 1.--check-settingsexits 1 for an unusable store setting and for an empty connection string (config validation rejects it first) and 2 for a managed credential that was never stored; the toggles exit 1 for that last case. A pin holds the sentence and two tests hold the codes. A line lists the other verbs that open the store and says they print the problem and exit 1.Test plan
Each issue's first commit holds only its tests. Run on those sources (dev's code):
ProvisionRolesCollisionGuardTests: 2 of 3 fail (the order of the stop around the guard, and the false comment). The third, which pins that the README's psql line has no global stop, passes before and after.DarlingCollectorToggleExitCodeTests: 6 of 18 fail withExpected: 2, Actual: 1(an unreachable store, an unreachable store while resolving--server, and a store that answers the login with an error, each for both verbs). The other 12 cases already returned 1 and are pins.provision-roles.sqlreplaced byorigin/dev's, not committed):TheBringYourOwnScript_RunByPsql_StopsAtAnUnmarkedRole_AndChangesNothing_Gated [FAIL],psql exited 0, not 3: the script did not stop at the unmarked viewer role.RED on a script that ends the guard with\set ON_ERROR_STOP off:TheBringYourOwnScript_RunByPsql_KeepsTheCallersOnErrorStop_Gated [FAIL],psql exited 0, not 3, with -v ON_ERROR_STOP=1: the script switched the caller's stop off.Both pass on the fixed script.DARLING_TEST_PGRUNTIMEset to an extracted runtime:ComposeStoreRolesLiveTestsandProvisionRolesCollisionGuardTeststogether, Total 16, Failed 0, Skipped 0 (so the two new psql tests ran, and the existing bring-your-own live test ran on the script with three backslash lines dropped).DarlingCollectorToggleExitCodeTestspasses (27 tests) andProvisionRolesCollisionGuardTestspasses (8).Darling.Testsbuilds with 0 warnings.EnsureStoreSearchPath,CheckSettingsorprovision-roles, every class in a test file that mentions the README, andDocCommentHygieneTests(60 classes, Total 1218, Failed 0, Skipped 24 live tests without a database).StoreSessionTimeZonePinCensusTests,StoreApplicationNameCensusTests): they want the time zone pin and application name on the connection-constructing call itself, which is why each verb still builds its own connection and the shared method only proves the string first. The first version of the shared method had the text of a constructor call inside a doc comment, which the census reads as a call site, and both census tests failed with "unterminated call"; the comment is reworded.Darling.Testsrun once on the final tree (withorigin/devmerged), no database and no runtime configured: Total 17831, Failed 0, Skipped 1162 (live tests), Not Run 1 (the oneExplicittest). Build: 0 warnings.DarlingCliUnusableStoreConnectionTestsTotal 21, Failed 15. NineSystem.FormatException : The input is not a valid Base-64 string(the managed credential file:--validate-config, the four endpoint verbs,--collapse-legacy-slices,--recompress-plan-dim,--add-server,--backfill-rollups); fourSystem.ArgumentException : Couldn't set sslmode (Parameter 'sslmode')(--validate-config,--collapse-legacy-slices,--recompress-plan-dim,--backfill-rollups);--add-serverwith a bad stringAssert.Contains() Failure: Sub-string not found(it printed "Could not reach the store"); and the wiring pinAssert.Single() Failure: The collection contained 3 items(the sentence was written three times).--check-settings,--enable-collectorand--disable-collectorpassed before and after (6 cases). After the fix: Total 46 withDarlingCollectorToggleExitCodeTests, Failed 0.DarlingCliUnusableStoreConnectionTestsandDarlingCollectorToggleExitCodeTestsTotal 58, Failed 27 (24 new cases, all 12 verbs for each of the two credential kinds, plus the 3 existing tests that asserted the old text). After: Failed 0. The test seam for an unreadable managed credential already existed (a file atCredentialPathFor(dataDirectory)), so both credential kinds are covered.DarlingCliUnusableStoreConnectionTestsTotal 33, Failed 12, eachNot found: "pg-credential.dpapi". After:DarlingCliUnusableStoreConnectionTests,DarlingCollectorToggleExitCodeTests,DarlingSecretsDecryptFailureTestsandDocCommentHygieneTestsTotal 143, Failed 0. FullDarling.Testsonce more: Total 17832, Failed 0, Skipped 1162.TheScriptAndTheReadme_SayPsql10OrLater, the README pin for the differing exit codes, and the two exit-code tests for--check-settings(they assert text or codes that the old sources already had or lacked by construction). I did not run a psql 9.6 to confirm the version claim (it is the one in provision-roles.sql keeps going after an error, including past its role-collision guard #4746).--check-settingstests against the old code (revertingDarlingCliCommands.cs); the crash they pin comes fromEnsureStoreSearchPathand the credential read, which had no catch.catchas the two covered store cases (a closed port, and a listener that answers the login with a28P01error).CHANGELOG
SECTION: None
ENTRY:
REF:
[provision-roles.sql stops at its role-collision guard, and the collector toggle verbs exit 2 for a store problem (#4746, #4744) #4809]: provision-roles.sql stops at its role-collision guard, and the collector toggle verbs exit 2 for a store problem (#4746, #4744) #4809