Skip to content

Repository files navigation

SQL Manager

sql-manager is a terminal-first SQL Server, PostgreSQL, and MySQL/MariaDB management tool built on .NET 10. It can run as a command-line utility or as a full-screen Terminal.Gui application.

Highlights

  • Manage multiple SQL Server, PostgreSQL, and MySQL/MariaDB connections from one sql-config.json
  • Add, edit, and remove connections; removed entries move to the trash bin instead of disappearing
  • Create and remove databases, create users, update passwords, show users and databases, and manage roles
  • Inspect a server without leaving the tool: database sizes, table listings with row counts, active sessions, and an ad-hoc query console
  • Use the CLI for scripting, the TUI with sql-manager -term, or the browser UI with sql-manager -ui; a bare sql-manager asks which you want
  • Emit machine-readable output with --json, or trim text output to failures and data with --quiet
  • Configure provider-aware connection settings per server: port, admin database, SQL Server trust mode, PostgreSQL SSL mode, MySQL SSL mode, pooling, MySQL public key retrieval, and per-server timeouts
  • Protect the config at rest with Argon2id-derived encryption and AES-256-GCM, with migration support for older encrypted config formats
  • Write the config atomically and keep the previous revision as a sql-config.json.bak sibling
  • Track selected connection, theme preference, version history, and deleted config entries in a built-in trash bin
  • Build and publish self-contained single-file binaries for Windows, macOS, Linux, and musl targets

Security

Treat sql-config.json as sensitive data.

When config encryption is disabled, stored admin and user passwords may be written in plaintext. When config encryption is enabled, the full config payload is encrypted at rest and stored connection strings remain masked.

Every save writes to a temporary file and then replaces the config, keeping the previous revision as sql-config.json.bak. That backup holds the same secrets as the config itself, so treat it with the same care and include it in any cleanup.

The web UI binds to loopback only and never listens on an external interface. Each run mints a single-use session token that is printed with the URL and required on every API call, so the printed URL is itself a credential: anyone who has it can administer your servers for as long as the process runs. Do not paste it into a chat, an issue, or a shared terminal recording.

Repository Layout

  • src/SqlManager: current .NET 10 application
  • src/SqlManager/Web: web UI host, endpoints, and the embedded front end
  • tests/SqlManager.Tests: xUnit v3 test project
  • global.json: opts dotnet test into the Microsoft.Testing.Platform runner used by xUnit v3
  • build.ps1: local build and publish helper
  • test.ps1: restore and test helper
  • sql-manager.ps1: older PowerShell implementation retained in the repo; the primary app is the .NET project under src/SqlManager

Requirements

  • .NET 10 SDK to build or run from source
  • PowerShell 7 to use build.ps1 and test.ps1
  • Network access to the target SQL Server, PostgreSQL, MySQL, or MariaDB host
  • No .NET installation on the target machine when using self-contained publish output

Quick Start

Choose a front end (terminal or browser) at startup:

dotnet run --project .\src\SqlManager\SqlManager.csproj --

Or go straight to one:

dotnet run --project .\src\SqlManager\SqlManager.csproj -- -term
dotnet run --project .\src\SqlManager\SqlManager.csproj -- -ui

Show help:

dotnet run --project .\src\SqlManager\SqlManager.csproj -- help

Show version information:

dotnet run --project .\src\SqlManager\SqlManager.csproj -- version

After publishing, the Windows binary is sql-manager.exe. On Linux and macOS self-contained publishes, the binary is sql-manager.

Config File

The default config path is sql-config.json next to the executable.

The app stores more metadata than older versions did. A shortened config example looks like this, and a fuller three-provider sample is available at docs/sample-sql-config.json:

{
  "selectedServerName": "1",
  "themeName": "iTerm2 Tango Dark",
  "encryptPasswords": true,
  "encryptionKey": "<unlock-password-verifier>",
  "timeouts": {
    "connectionTimeoutSeconds": 15,
    "commandTimeoutSeconds": 30
  },
  "servers": [
    {
      "serverIdentifier": "1",
      "displayName": "Primary SQL Server",
      "serverName": "sql01.contoso.local",
      "provider": "sqlserver",
      "port": 1433,
      "adminDatabase": "master",
      "adminUsername": "sa",
      "adminPassword": "<encrypted-or-plain>",
      "sqlServerTrustMode": "false",
      "connectionTimeoutSeconds": 15,
      "commandTimeoutSeconds": 30,
      "encrypted": true,
      "databases": [
        {
          "databaseName": "LabDB",
          "users": [
            {
              "username": "LabDBUser",
              "password": "<encrypted-or-plain>",
              "encrypted": true,
              "roles": [
                "db_owner",
                "db_datareader"
              ],
              "connectionString": "Server=tcp:sql01.contoso.local,1433;Initial Catalog=LabDB;User ID=LabDBUser;Password=********;Encrypt=False;TrustServerCertificate=False;"
            }
          ]
        }
      ]
    },
    {
      "serverIdentifier": "2",
      "displayName": "Primary PostgreSQL",
      "serverName": "pg01.contoso.local",
      "provider": "postgresql",
      "port": 5432,
      "adminDatabase": "postgres",
      "adminUsername": "postgres",
      "adminPassword": "<encrypted-or-plain>",
      "postgreSqlSslMode": "require",
      "postgreSqlPooling": true,
      "connectionTimeoutSeconds": 15,
      "commandTimeoutSeconds": 30,
      "encrypted": true,
      "databases": [
        {
          "databaseName": "appdb",
          "users": [
            {
              "username": "app_owner",
              "password": "<encrypted-or-plain>",
              "encrypted": true,
              "roles": [
                "db_owner"
              ],
              "connectionString": "Host=pg01.contoso.local;Database=appdb;Username=app_owner;Password=********;Ssl Mode=Require;Port=5432;Timeout=15;Command Timeout=30;Pooling=true;"
            }
          ]
        }
      ]
    }
  ],
  "trash": []
}

Config notes:

  • selectedServerName stores the selected connection identifier, not just the host name
  • serverIdentifier is a generated connection id used by selection and TUI workflows
  • displayName is the human-readable label shown in the UI
  • Top-level timeouts provide defaults; per-server timeout values override them
  • New SQL Server connections default to port 1433, admin database master, and trust mode false
  • New PostgreSQL connections default to port 5432, admin database postgres, SSL mode prefer, and pooling true
  • New MySQL/MariaDB connections default to port 3306, admin database mysql, SSL mode required, pooling true, and allowPublicKeyRetrieval false
  • versionHistory metadata is generated for servers, databases, users, and trash entries even though it is omitted from the example above for brevity
  • The TUI trash bin stores deleted server, database, and user config entries so they can be restored later
  • Generic roles in config are provider-aware at execution time: SQL Server supports db_owner, db_datareader, and db_datawriter; PostgreSQL and MySQL/MariaDB support db_owner only

CLI Commands

Current top-level commands:

  • version
  • -term (terminal UI; also accepted as term, terminal, or tui)
  • -ui (browser UI; also accepted as ui, webui, or web)
  • view-config
  • init-config
  • add-server
  • edit-server
  • remove-server
  • select-server
  • sync-server
  • test-connection
  • set-theme
  • show-trash
  • restore-trash
  • show-databases
  • show-database-sizes
  • show-tables
  • show-connections
  • run-query
  • create-database
  • remove-database
  • create-user
  • set-user-access
  • add-role
  • remove-role
  • show-users
  • test-user-login
  • remove-user
  • update-password
  • enable-config-encryption
  • disable-config-encryption
  • migrate-config-encryption-format
  • help

Compatibility notes:

  • sql-manager --version is supported in addition to sql-manager version
  • PowerShell-style compatibility is still supported, for example --action CreateUser
  • Many server-scoped commands can reuse the selected connection and stored admin credentials from config when those options are omitted
  • --json prints one result document per command and implies --non-interactive; --quiet keeps text output but prints only failures and data tables
  • Slash-prefixed switches such as /json are accepted on Windows only, so absolute POSIX paths work as option values on Linux and macOS

Examples

Add a SQL Server connection:

.\sql-manager.exe add-server --display-name "Primary SQL" --server-name sql01.contoso.local --provider sqlserver --port 1433 --admin-database master --admin-username sa --admin-password "Secret123!" --trust-mode false

Add a PostgreSQL connection:

.\sql-manager.exe add-server --display-name "Primary Postgres" --server-name pg01.contoso.local --provider postgresql --port 5432 --admin-database postgres --admin-username postgres --admin-password "Secret123!" --ssl-mode require --pooling true

Add an Azure MySQL Flexible Server connection:

.\sql-manager.exe add-server --display-name "Azure MySQL" --server-name mysql-flex-prod.mysql.database.azure.com --provider mysql --port 3306 --admin-database mysql --admin-username sqlmanager --admin-password "Secret123!" --mysql-ssl-mode required --pooling true --allow-public-key-retrieval false

Add a local MariaDB connection:

.\sql-manager.exe add-server --display-name "Local MariaDB" --server-name localhost --provider mariadb --port 3306 --admin-database mysql --admin-username root --admin-password "Secret123!" --mysql-ssl-mode preferred --pooling true --allow-public-key-retrieval true

Select the active connection by identifier:

.\sql-manager.exe select-server --server-identifier 2

Sync the selected server into config:

.\sql-manager.exe sync-server --admin-password "Secret123!"

Sync merges rather than rebuilds. Databases and users the server reports are updated in place, keeping their entry ids, version history, and the passwords the config is holding for them. Newly seen ones are added. Anything the server no longer reports is moved to the trash bin rather than dropped, because the config is the only place a user's password is written down; the result says how many entries moved.

Create a database:

.\sql-manager.exe create-database --server-name sql01.contoso.local --admin-username sa --admin-password "Secret123!" --database-name LabDB

Create a SQL Server user with generated password and two roles:

.\sql-manager.exe create-user --server-name sql01.contoso.local --admin-username sa --admin-password "Secret123!" --database-name LabDB --user-name LabDBUser --roles db_owner,db_datareader

Remove a role from a user:

.\sql-manager.exe remove-role --server-name sql01.contoso.local --admin-username sa --admin-password "Secret123!" --database-name LabDB --user-name LabDBUser --roles db_datareader

Remove a connection from the config (the entry moves to the trash bin; nothing is dropped on the server):

.\sql-manager.exe remove-server --server-identifier 2

List the trash bin and recover an entry:

.\sql-manager.exe show-trash
.\sql-manager.exe restore-trash --trash-id 85cd9764199845b784b3980479172

Check that a stored connection still works before running anything against it:

.\sql-manager.exe test-connection --server-identifier 1 --admin-password "Secret123!"

Review database sizes, table row counts, and active sessions:

.\sql-manager.exe show-database-sizes --admin-password "Secret123!"
.\sql-manager.exe show-tables --database-name LabDB --admin-password "Secret123!"
.\sql-manager.exe show-connections --admin-password "Secret123!"

Run an ad-hoc statement, inline or from a file:

.\sql-manager.exe run-query --database-name LabDB --query "SELECT TOP 10 * FROM dbo.Orders;" --admin-password "Secret123!"
.\sql-manager.exe run-query --database-name LabDB --query-file .\reports\daily.sql --max-rows 2000 --admin-password "Secret123!"

Set a user's access across several databases in one call, revoking one of them:

.\sql-manager.exe set-user-access --user-name LabDBUser --access "LabDB=db_owner,db_datareader" --access "ScratchDB=" --admin-password "Secret123!"

Script against the CLI with JSON output:

$databases = .\sql-manager.exe show-databases --admin-password "Secret123!" --json | ConvertFrom-Json
$databases.data.databases
sql-manager show-tables --database-name LabDB --admin-password "Secret123!" --json | jq -r '.data.rows[][0]'

Test a stored or supplied user login:

.\sql-manager.exe test-user-login --server-name pg01.contoso.local --database-name appdb --user-name app_owner --user-password "Secret123!"

Enable config encryption:

.\sql-manager.exe enable-config-encryption --config-path .\sql-config.json --encryption-password "ComplexPass!123"

Migrate an older encrypted config to the current full-file encrypted format:

.\sql-manager.exe migrate-config-encryption-format --config-path .\sql-config.json --encryption-password "ComplexPass!123"

Disable config encryption:

.\sql-manager.exe disable-config-encryption --config-path .\sql-config.json --encryption-password "ComplexPass!123"

MySQL / MariaDB Notes

  • Azure Database for MySQL Flexible Server: use the server FQDN, port 3306, the admin login shown in Azure, and keep TLS on with --mysql-ssl-mode required or verifyfull. Leave --allow-public-key-retrieval false.
  • Local MySQL or MariaDB: use the local host or VM host name, keep --mysql-ssl-mode preferred when the server negotiates TLS, or switch to disabled only if the server is intentionally running without TLS. --allow-public-key-retrieval true can be needed for local MySQL 8 password auth without TLS.
  • SQL Manager manages MySQL/MariaDB accounts as 'username'@'%' so the generated login can connect from Azure clients and local tools unless you later tighten the host part manually.
  • The MySQL/MariaDB db_owner mapping is GRANT ALL PRIVILEGES ON db.* TO 'username'@'%';. That is the closest equivalent to the SQL Server-style full-database owner role this tool manages.
  • To create users and grant that access, the admin account you configure for MySQL/MariaDB needs privileges to run CREATE USER, ALTER USER, DROP USER, and GRANT on the target databases. Azure Flexible Server admin accounts have the needed rights for this workflow.

TUI Overview

Running sql-manager with no arguments asks which front end you want:

How would you like to run sql-manager?

> Run in Terminal
  Run in Web UI (opens a browser)

Pick one with the arrow keys and Enter. To skip the question, start a front end directly with sql-manager -term or sql-manager -ui. When input is redirected there is nobody to answer, so the terminal UI starts as it always has.

Current TUI areas include:

  • Server management: select active server, add, edit, or remove a connection, sync configuration, and show or manage databases
  • User management: create users, manage roles, show users, test user logins, remove users, and update passwords
  • Server insights: test the connection, review database sizes, list tables with row counts, inspect active sessions, and run ad-hoc SQL in the query console (Ctrl+R runs the statement)
  • Configuration menu: save, view config, initialize config, change theme, toggle password encryption, inspect the trash bin, and refresh from disk
  • Help and About views: version details, command reference, repository URL, and general navigation help

Web UI

sql-manager -ui serves the same operations as the TUI in a browser and opens it for you.

.\sql-manager.exe -ui
sql-manager -ui --ui-port 9000 --no-browser

The command prints the URL to use:

Web UI listening on http://127.0.0.1:8973/
Open: http://127.0.0.1:8973/?token=<session token>

Options:

  • --ui-port <port> pins the port. Without it the server starts at 8973 and scans upward for a free one, so a second instance still runs. This is deliberately separate from --port, which is the database port
  • --no-browser prints the URL without launching a browser, for headless or remote-forwarded use
  • --encryption-password <password> unlocks an encrypted config at startup instead of unlocking from the Session panel
  • --config-path <path> picks the config, exactly as it does for every other command

Panels mirror the TUI: Connections (select, add, edit, remove, sync, test, history), Databases (list, sizes, tables, active sessions, create, remove), Users (show, create, set access, roles, test login, remove, update password), Query (Ctrl+Enter runs the statement), Activity (what this page has run), and Config (unlock/lock, save, theme, encryption, trash bin).

A context bar sits under the tabs and holds the choice every panel works against:

  • Connection lists the configured connections. Picking one selects it in the config, exactly as the Select button on the Connections tab does
  • Database offers the databases of that connection and accepts a name you type. The list comes from the server, falling back to the databases already recorded in the config; Reload databases re-reads it from the server
  • The summary beside them, and a line on the Databases, Users, and Query panels, names the connection, provider, host, admin user, and database in play, so a result is never read against the wrong server

Users, List tables, Query, and every database field in an action form inherit that choice, so a database is named once rather than retyped on each panel. A query with no database chosen runs against the connection's admin database, which the Query panel says outright. The chosen database is per connection and lives in the browser tab, not the config.

Switching connections tests it once, quietly, and reports the outcome in the header pill next to the config path. Click that pill to test again. A connection that does not answer is not asked for its database list, so a bad admin password shows up on arrival rather than three clicks later.

Adding and editing a connection

The connection form is shaped by the provider you pick, and re-shapes itself when you change it:

  • SQL Server offers a TLS mode dropdown — Encrypt=False, Trust=False, Encrypt=True, Trust=True, or Encrypt=Strict, Trust=False
  • PostgreSQL offers an SSL mode dropdown — Prefer, Require, Disable, VerifyCA, VerifyFull — plus a pooling toggle
  • MySQL / MariaDB offers its own SSL mode list plus pooling and Allow public key retrieval

The choices come from the same normalizers the save validates against, so the form cannot offer a value the save would reject. Every field carries a note saying what it means and what blank does: the default port and admin database for that provider, host examples including a named SQL Server instance, what each TLS level actually enforces, and the timeout the config falls back to. Switching provider keeps whatever you have already typed.

Editing shows what is stored — provider, port, admin database, TLS or SSL mode, pooling, and timeouts all come back as saved. The admin password box stays blank and says so: leaving it blank keeps the stored password, and typing one replaces it. Clearing one on purpose means sending an empty password, which update-server --admin-password "" does.

Backing up the config

Config → Backup writes a copy of the config beside it, stamped with the UTC time it was taken:

sql-config.20260830T125233Z.json

The copy is byte for byte, so an encrypted config stays encrypted, and a locked config can still be backed up. Nothing is overwritten: each click writes a new file.

Restoring is manual. Stop sql-manager, copy the backup over sql-config.json under that exact name, and start it again. This is separate from the one-deep sql-config.json.bak that every save leaves behind, which the next save overwrites — a stamped backup is the one that survives. Backups hold the same secrets as the config, so keep them where you would be willing to keep the config itself.

Removing a user

The Scope on the remove-user form decides how far the removal reaches:

  • Database removes the user only from the databases listed on the form
  • Server drops the login itself, which reaches every database on that connection whatever the list says. The API also accepts Both, which does the same thing as Server

The form opens on Database, and a Remove button on a user row opens it scoped to that row's database. Choosing Server asks for confirmation first, because the config entries go to the trash bin but the changes on the server cannot be undone. Dropping a database asks in the same way.

Working with results

Every result table carries the same tools: a filter box, click-to-sort headers (numeric where the column is numeric), Copy CSV, Download CSV, and a line saying how many rows are shown, how long the call took, and whether the row limit truncated it.

Results are also where the work happens:

  • List databases gives each row Use (adopt it as the chosen database), Tables, Users, History, and Drop
  • Show users gives each row Roles, Password, Test login, History, and Remove, each opening the matching form already filled in for that user and database

History reads the version history the config records for a connection, a database, a user, or a trash entry — the same history the TUI shows, and the only other place it is readable. Version details name what changed; passwords appear as a state word, never a value.

Query console

  • Ctrl+Enter runs. With text selected, only the selection runs, and the result says so
  • Ctrl+Up and Ctrl+Down walk the history, which also sits in a dropdown. The last 25 statements are kept in the browser tab; a statement that was refused is not among them
  • Confirm writes and drops (on by default) asks before anything that writes, and says outright when an UPDATE or DELETE has no WHERE clause
  • Read-only refuses anything but a SELECT-shaped statement, rather than trusting the confirmation
  • Cancel stops a running statement. The server answers a cancel while the query still holds the request lock, so it does not wait in line behind it

Seeing what is happening

  • A spinner in the header names the call in flight, and the button that started it stays out of action until it finishes
  • The Activity panel keeps the last 100 calls with their outcome, message, and timing. It lives in the browser tab and is lost when the tab closes
  • The header says whether an admin password is currently held in the page

Notes:

  • The theme paints both front ends. The Config panel previews a theme as you move through the list and writes it with Save theme. The page derives its panels, borders, and muted text from the palette the terminal UI uses, lifting any colour that would otherwise be unreadable against that background
  • Enabling encryption from the Config panel keeps the session unlocked with the password you just typed, so the next write is not refused. Disabling it clears the held password
  • While an encrypted config is locked, reads still work but writes are refused; the Config panel shows a banner saying so
  • The admin password is typed once in the header and held only in the page for that tab. It is never written to disk or browser storage
  • Both 127.0.0.1 and localhost work; any other host name is refused, which closes the DNS-rebinding path
  • Requests are handled one at a time because they all read and write the same config file. They are accepted concurrently so that Cancel can be heard, but only one runs at a time
  • The active tab, the chosen database, and the query history live in sessionStorage, which is per browser tab and cleared when it closes. So does the session token the launch URL carried; the admin password never is
  • Stop the server with Ctrl+C or the Shut down button

Build and Test

Run validation locally:

.\test.ps1 -Configuration Release

Testing notes:

  • The test project targets xUnit v3, which runs on Microsoft.Testing.Platform rather than VSTest
  • global.json carries the test.runner opt-in that .NET 10 requires for that runner
  • Under this runner dotnet test takes the project via --project <path>, and it rejects --nologo

Build debug output:

.\build.ps1 -Target Debug

Build release output:

.\build.ps1 -Target Release

Publish self-contained production output for selected runtimes:

.\build.ps1 -Target Prod -Runtime win-x64,linux-x64

Publish the full runtime matrix:

.\build.ps1 -Target Prod -Runtime All

Current publish targets supported by build.ps1:

  • win-x64
  • win-arm64
  • osx-x64
  • osx-arm64
  • linux-x64
  • linux-arm64
  • linux-musl-x64
  • linux-musl-arm64

Build outputs are written under:

artifacts\debug
artifacts\release
artifacts\prod\<runtime>

Versioning notes:

  • build.ps1 increments build-number.txt when -VersionOverride is not supplied
  • -BuildNumber and -VersionOverride are mutually exclusive
  • -InformationalVersionOverride lets you keep a stable numeric version while appending metadata such as a commit hash

GitHub Releases

The repository includes .github/workflows/release.yml to publish release assets from Git tags.

The workflow:

  • runs on every pushed tag
  • requires the tag to be a numeric version like 1.0.0 or 1.0.0.1
  • uses actions/checkout@v5 and actions/setup-dotnet@v5 so the release pipeline is aligned with the GitHub Actions Node.js 24 transition
  • runs .\test.ps1 -Configuration Release
  • publishes self-contained binaries for the full runtime matrix
  • zips each runtime folder into a release asset
  • creates or updates the GitHub release and uploads the generated archives

Release builds use the tag as the numeric version and append the short commit SHA to the informational version. For example, tag 1.0.0 built from commit abcdef1... becomes informational version 1.0.0+abcdef1.

GitHub Actions note:

  • GitHub will force JavaScript actions to run on Node.js 24 by default on June 2, 2026, and remove Node.js 20 from hosted runners on September 16, 2026
  • this repository already uses the Node.js 24-ready major versions of the GitHub-maintained actions in release.yml
  • on self-hosted runners, actions/setup-dotnet@v5 requires a runner version that supports Node.js 24, so keep the runner updated rather than relying on the temporary FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 or ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION overrides

Notes

  • Started without arguments the app asks whether to open the terminal UI or the web UI, and switches to CLI mode when arguments are supplied
  • SQL Server role aliases accepted by the CLI include dbowner, db_owner, dbreader, db_reader, db_datareader, dbwriter, db_writer, and db_datawriter
  • PostgreSQL and MySQL/MariaDB currently support db_owner only; reader and writer role aliases are rejected for those targets
  • Existing encrypted configs from older versions remain readable and can be migrated to the full-file encrypted format

About

A command line TUI for managing sql server

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages