SQL Workbench is a desktop application (swgui), a command line (swcli) and the SDK
underneath both. One download holds all of it.
This is what the product does today, not what is planned. Where a feature exists for some engines and not others, the entry says which — the four engines differ a great deal, and a capability an engine's provider does not declare is one the product greys out with a reason rather than one it attempts.
The four engines
SQL Server, PostgreSQL, MySQL and SQLite all ship in the default build. What each can do is decided by its provider package, so the command line and the desktop application agree about it.
| SQL Server | PostgreSQL | MySQL | SQLite | |
|---|---|---|---|---|
| Browse a database | yes | yes | yes | yes |
| Run scripts, several result sets, transactions | yes | yes | yes | yes |
| List a server's databases | yes | yes | yes | yes |
| Read a folder of SQL files, or a dump file | yes | yes | yes | yes |
Read a .dacpac or a .sqlproj folder |
yes | — | — | — |
| Compare two sources | yes | yes | yes | yes |
| Normalise layout before comparing | yes | — | — | — |
| Deploy a schema to a database | yes | yes | yes | — |
| Transactional deployment | yes | yes | no | — |
Write a schema out to a folder (export) |
yes | yes | yes | yes |
Sync into a folder (sync) |
yes | yes | yes | — |
| Compare and sync a table's rows | yes | yes | yes | — |
| Ask the engine to check a statement | — | yes | yes | yes |
| Completion from the connected database's vocabulary | — | yes | yes | yes |
| Syntax errors from a parser, Format Document | yes | — | — | — |
| Incremental refresh from a schema stamp | yes | — | — | — |
| Dependencies — what uses this object | yes | — | — | — |
| Foreign-key relations for join authoring | yes | — | — | — |
| Deploy one script object to a database | yes | — | — | — |
| Shipped language reference | yes | yes | — | yes |
| The server's own manual, read live | — | — | yes | — |
| Engine primer | — | yes | yes | — |
- Cross-engine comparison is refused. A comparison between two engines is a usage error, not a best effort: the differ will not compare a PostgreSQL view against a T-SQL one.
- Build flavours. Which engine packages a build carries is decided when it is built. A build without the SQL Server deployment engine still browses, queries, compares and exports, and says "… is not available in this build", with the providers it does carry, wherever it cannot go further.
Connections and environments
- Named connections. A server is registered once under a short name and referred to as
@uatafterwards, in both front ends and in every file the product writes. - A connection is server-level.
@uatopens its default database and@uat/OtherDbthe same server's other database, so one entry serves every database on a server. - Wildcards over databases.
@uat/App_*expands to one source per matching database, and every verb that takes a source takes a set. Each engine's own databases (master,model,msdb,tempdb;postgres,template0,template1;information_schema,mysql,performance_schema,sys;temp) are never picked up by a wildcard, and there is no flag that puts them back. Naming one outright still works. - Connection strings, paths and URLs are all sources.
@alias,@alias/db, a connection string, a folder of.sqlfiles, a.sqlprojfolder, a.dacpac, a dump file and ascheme://URL are parsed by one grammar, and whatever the product can do to one kind it offers for the others. - URL addresses.
postgres://,postgresql://,mysql://,mariadb://,sqlserver://,mssql://,sqlite://and thejdbc:spellings are read into the engine's own connection keywords before any driver sees them, so nothing downstream ever handles a URL. - The engine is part of the reference. It comes from a URL scheme, a
driver=orengine=key, the alias's own engine, or defaults to SQL Server. For a verb with two sides the side that names an engine decides for a plain folder path, which is what makes comparing a PostgreSQL database against a checkout work without saying so twice. - A SQLite alias is the file. The database is the connection, so the path stays in the
connection string and
@lite/otheris refused in one sentence rather than silently opening a file calledotherin the working directory. - Authentication per engine. Integrated (Kerberos on Linux, the signed-in account on Windows), SQL login and Microsoft Entra for SQL Server; integrated and password for PostgreSQL; password for MySQL; none for SQLite. The connection editor offers only the kinds the chosen engine has, writes only the keywords that engine understands, and shows Encrypt and Trust server certificate only where they exist.
- Test a connection. Test on the Connections page and
swcli alias testopen one connection and report the data source, the engine, the server version, the database they landed in and the sign-on kind. - Reachability probe. A two-second TCP connect in front of every automatic path, so a
dropped VPN becomes an immediate "offline" rather than a long driver timeout. A shape with
no TCP endpoint — LocalDB, a named pipe, shared memory, a PostgreSQL socket directory —
reports reachable rather than falsely unreachable, because only the real connect can tell.
For SQLite the probe is
File.Exists, and an in-memory database reads as reachable. - A hint beside a failed integrated sign-on. When the driver's message names GSSAPI,
Kerberos or SSPI, one extra line says to get a ticket with
kiniton Linux or to check the account and the server's SPN on Windows. The driver's own message is always shown unchanged. - Environment rules. A rule marks a connection as production, UAT or anything else by name
or by pattern (substring, or
regex:), gives it a label and a colour, and sets its write policy. The label shows on explorer rows, query tabs and compare confirmations. - The write gate.
openruns,confirmasks once per execution or per sync, andblockrefuses until the environment's name is typed. It applies in the query editor, in the Compare and Data compare tabs, and inswcli sync,data syncanddeployalike. - Break-glass, journalled. A blocked query tab can be unlocked for writes; every gate decision — allowed, confirmed, refused, bypassed — is one line in a local journal with the redacted target, the action, the environment, the policy and what was let through.
- What counts as a write. Every engine answers, and every one is allowed to over-block and
never to under-block. SQL Server decides from its parse tree: the read-only reporting
procedures (
sp_help,sp_who,sp_spaceused, …) and anEXECover a literal that itself only reads are allowed, whileOPENQUERY,OPENROWSET,WAITFOR RECEIVE, dynamic SQL it cannot read and a script that does not parse are all writes. The other three judge the leading word of each statement, and SQLite additionally reads aPRAGMAas a write only when it assigns. Reads are never gated. - Compare mappings. A connection can name its counterparts — the folder holding its code, the next environment's database — and a new Compare tab fills its other side from the first of them. Renaming a connection rewrites every rule that names it.
- Connections page. Add, edit, duplicate, test and remove servers in one page, with the structured fields and the raw connection string as two views of the same thing: editing either fills the other, a pasted password moves to the secret field, a pasted database becomes the default, and keywords no field covers are kept as written.
- A database list cache. Which databases a server has survives a restart, so the explorer opens on what it knew.
The object explorer
- Servers, databases, objects. Each registered connection is a server row, its databases below it, and an opened database's schemas and object folders below that. Any number of servers and folder sources stand side by side, each with its own freshness, cache, counterparts and environment badge.
- Four groupings. Schema then type (the default), type then schema, type then schema-qualified name, or one flat name-ordered list with the type appended.
- Object kinds per engine. SQL Server has about thirty-five, PostgreSQL sixteen (including materialized views, domains and extensions), MySQL seven (no constraint, key or index kinds at all — the engine prints those inside the table), and SQLite four. Each engine's own folder names, order and icons come from the engine, not from a shared list.
- Table children fold into their table. Constraints, indexes, triggers and extended properties nest under the table and show their short names; a child whose table is not visible stays at the top level rather than disappearing.
- Quick filter. The funnel button or Ctrl+F while the tree has focus: a case-insensitive substring match over the object name or its schema-qualified name, opening every matching source and every folder on the way. Escape clears it.
- Counts on folders. Each grouping node carries the number of objects beneath it, and a table the number of children folded into it.
- Read once, then cached. Opening a database reads its catalogue directly rather than building a model of it, so a large server opens quickly; the inventory is then kept as a zip and a later session starts from it.
- Lazy at three levels. A server lists its databases only when opened, a source builds its tree only when opened, and a folder fills its rows only when expanded — so a database with thousands of objects costs its folders and not its objects. A filter opens everything.
- The tree fills while the read runs. For SQL Server and PostgreSQL objects arrive one at a
time and the partial tree is shown while there is nothing better on screen; refreshing a
loaded source keeps its tree until the new one is complete. MySQL and SQLite read in full
first — MySQL because
SHOW CREATEhas no bulk form and a read is one round trip per object. - Freshness per row. A database row reads as fresh, stale (
offline 5m,error) or not watched, by icon colour and a short caption, with the whole story in its tooltip: object count, content as of, last checked, last change seen, the watch interval, and why it is not watched when it is not. - Only what is in use is crawled. A database is refreshed automatically when it is open in the tree, explicitly registered, or was used within the last seven days; a connection marked not to auto-connect is never touched by a timer. Anything the user asks for connects regardless, and doing so is what brings a source back into scope.
- Incremental refresh. For SQL Server the schedule reads a one-row schema stamp, takes a per-object snapshot only when the stamp moved, and re-reads only the objects that changed. PostgreSQL and MySQL record no per-object modification time and SQLite no catalogue timestamp at all, so a change on those three is a full re-read rather than a badly guessed delta.
- What only a full read sees. Schemas, users, roles and alias types are not in
sys.objects, and an extended-property edit or aCHECK/NOCHECKdoes not move a modification date, so a bounded full-refresh timer catches up on what the delta cannot see. - Backoff per server. A failed probe or database listing backs off from 30 seconds, doubling to a 30-minute cap, cleared by the next success. While it lasts the sweep does not even probe.
- Pause and resume. The status bar reads
Watching 3 of 40 databases · next check 14:22:07and toggles automatic refresh off and on. - Environment and counterpart badges. A row wears its environment badge and, once clicked, one badge per mapped counterpart: not checked, then 3 differ · code (main) or matches · @prod/AppDb, with the breakdown in the tooltip. No counterpart is contacted until it is asked for.
- Objects that differ are marked. After a counterpart check, differing objects carry a dot, a filter chip above the tree narrows it to them, and Diff with … opens the comparison on that one object.
- Git facts on a folder source. A folder inside a checkout shows a git glyph, its branch
(or the short commit when the head is detached) and
+Nuncommitted once a count has been asked for; switching branch in a terminal is picked up within one sweep. - Context menus per node kind. Server: new query, refresh databases, remove. Source: new
query, add to favorites, compare, compare data, check counterparts, refresh, remove. Object:
open its
CREATE, object info, show dependencies, new query, script asALTER/CREATE OR ALTER/ drop-and-create /EXECUTE,SELECT TOP 1000, anINSERTtemplate, and Diff with each counterpart. Which entries appear is read off the engine's own object-type table, and a shape an engine has no verb for is simply not offered. - Favorites. A database kept as a favorite gets a one-click chip on the favorites bar that opens a query tab on it, dimmed while the server is offline.
- Read errors shown in place. An object the source could not supply is an error row in the tree rather than a silent absence.
- An honest cost budget. An open seven-thousand-object database stays under a thousand realised rows; an optional hover highlight is dropped automatically on remote-desktop sessions to save repaint bandwidth.
The query editor
- Syntax highlighting. One SQL grammar colours the text for every engine. What is
per-engine is the lexer behind the analysis — dollar-quoted bodies, nested block comments
and
E''escapes for PostgreSQL; backticks,#comments and double-quoted strings for MySQL; the three quote forms SQLite accepts; T-SQL's own — and the extra colour for a name the connected database actually has. - Identifier folding per engine. PostgreSQL folds an unquoted name to lower case, SQL Server folds nothing, MySQL folds nothing because case sensitivity is the server's own setting, and SQLite keeps a name as written. The editor and the comparison both follow the engine.
- Completion. Ctrl+Space, or typing
.,@or a letter: the schemas, tables, views, procedures and functions of the connected database, the columns of the tables the statement reads, declared variables, the engine's keywords and functions, and snippets. - Columns behind an alias. After
from Invoice i,i.offers that table's columns, and a column is inserted the way it will be written —o.Totalunder its table's alias. - Columns with the star deleted. With the select list empty, completion offers the columns
of the tables in the
FROMclause, each once and with its table and type beside it. The statement does not have to parse for the editor to know what it reads. - Completion from the connected database's own vocabulary. On PostgreSQL, MySQL and SQLite
the lists are read from the server itself once per connection and merged with the shipped
ones, so an extension's functions and this version's keywords are offered too: PostgreSQL
reads
pg_get_keywords()andpg_procwith real signatures and comments, MySQL readsinformation_schema.KEYWORDS, and SQLite readspragma function_list. The shipped wording wins, the engine's signature wins, names only the engine knows are added, nothing is written to disk, and a login with no rights on the catalogue simply falls back to the shipped list. SQL Server has no catalogue view for either, so its shipped pack is the whole answer. - Signature help. While a call is being written the status bar shows its signature with the argument at the caret marked, and the completion list leads a function's tip with the same line.
- Hover cards you can use. Resting the pointer on a keyword, clause or function shows its
summary and syntax; on a column, its description, unit and code list from the knowledge
packs; on a literal compared with a coded column,
3 = Posted. The card stays while the pointer travels to it, and More… and … N more are buttons that can be pressed. - Syntax errors as you type. For SQL Server the parser marks the error where it stands. For PostgreSQL, MySQL and SQLite the lexer marks an unterminated literal or comment and a bracket that does not match. The analysis runs on a debounce and is versioned, so a stale answer is never applied to newer text.
- Unknown objects marked. A table or view the database does not have is squiggled, with a "did you mean" suggestion where a near name exists, and a name that is in the database is coloured as one. Nothing is marked while the inventory is empty, and an alias used anywhere in the script is never mistaken for a missing table.
- The engine checks the statement, without running it. On PostgreSQL, MySQL and SQLite each
statement is prepared — which parses it and resolves every name and column and executes
nothing — so
select * form customersis marked underformand a missing column is caught in the engine's own words, at the engine's own offset. SQLite answers in microseconds because it is linked into the process. It never runs on the connection the tab is using, so an open transaction is undisturbed; typing cancels a check in flight; and a statement the engine will not prepare is reported as unchecked rather than called wrong. SQL Server does not need it: its parser is already in the process. - Format Document and Format selection. SQL Server only, through its parser: keyword
casing, indentation, aligned clause bodies, which clauses start a new line, one select item or
WHEREpredicate per line,ASon its own line and semicolons, all settable, comments preserved, one undo step. Text that does not parse is left exactly as it was with the parser's message on the status line. The other three engines leave the script alone and say so in one sentence. - Expand the star. Expand * writes out the columns of the tables the statement reads under the aliases it uses, bracketing a name only where it needs it.
- A column checklist. Expand and Add columns open a filterable list of the columns with their types and what the knowledge packs call them: type to filter, Space to tick, Ctrl+A for all or none, a layout choice of one line, one per line or a wrap width, and a live preview.
- Add a join. One entry per relation to a table not yet in the query, writing the whole join
with a free alias and every pair of a composite key. It asks inner, left, right or full outer
— left already chosen where the referencing column may be null — and picks between several
relations to the same table, each named with its foreign key and marked where a pack describes
it. Where a join could take more than one
ON, it asks which. - Write the predicate between two tables the query already reads. After
join Customer c, after a bareON, and in aWHERE. - Aliases that read like the table. Add join strips the prefix a system puts on every
table and takes the initials of the words in the name, so
app_customeraccountlinkjoins ascal. The words of an all-lower-case name are found from the database's own table and column names, with nothing to configure; a knowledge pack or the settings can add prefixes and words; an alias the script already gives the table is reused; and a reserved word is never proposed. - List layout is a setting. Lists a code action writes wrap at a chosen width, or go on one
line, or one item per line, with the comma — or a join's
AND— leading or trailing, and the editor's own tabs or spaces. - Code actions ask rather than list. Alt+Space (or Ctrl+. where the desktop keeps Alt+Space for itself) and the right-click menu show one line per kind of thing, so a sixty-column table with eight relations is two lines. Each action is one undo step that leaves the caret after what was written, a right-click acts where it was clicked, Escape changes nothing, and none of it reads the database while you type.
- Selection-aware actions. With text selected the list leads with Format selection and Execute selection.
- Templates from the explorer and the caret.
SELECTandINSERTfor a table,EXECUTEwith a typed placeholder per parameter for a procedure, and drop-and-create orCREATE OR REPLACEfor a module — each in the verb the engine actually has. Rewriting a stored script into a plainALTERneeds a grammar, so it is offered for SQL Server only; MySQL offersCREATE OR REPLACEfor a view and refuses it for a routine; SQLite has onlyCREATE. - Coded columns offer their codes.
where Status =opens the column's code list as soon as the space is typed, and Ctrl+Space there does the same, in a fresh tab and in a statement that does not parse yet. - Line and text commands. Comment, uncomment, toggle comments, a title banner, a header block from a template, trim trailing spaces, duplicate lines, move lines up and down, upper-case and lower-case — each one undo step with the selection restored.
- Go to definition (F12) and Go to line. And Object info on the caret, which opens the object's inspector page, or a column's own page, or the word in the Help tab.
- Paste a spreadsheet as INSERTs. A tabular clipboard becomes
INSERTstatements. - Execute the statement under the caret. Statement boundaries follow the engine's own
rules —
GOandGO nfor T-SQL, semicolons elsewhere,DELIMITER $$bodies for MySQL, andCREATE TRIGGER … BEGIN … END;as one statement for SQLite while a bareBEGINstays a transaction. - The rest of the editor. Find and replace, line numbers, indent and outdent, and a settable editor font size. There is no code folding, no bookmarks and no multiple cursors.
- The connection box. Each tab names where it points, with completion over aliases and their databases; the change takes effect on Enter or on leaving the box. Another database on the same server is switched to on the same session — open transactions and temp tables survive — and anything else reconnects. With a transaction open, a change of server waits for a commit, a rollback or Connect. Shift+Tab at the very start of the script goes up to the box.
- A status light beside the box. Grey when not connected, green when connected, blue while connecting or while the box names somewhere the session is not yet, red when the last connect failed — with the reason in its tip.
- Execute with nothing to run against. F5 or Ctrl+E in an unconnected tab puts the cursor in the connection box and says what is missing; Enter connects and returns to the editor, or stays and says why not; Escape goes back without connecting.
- Tabs reconnect when you come back to them. A tab that was connected when the application closed connects again the first time it is looked at after a restart; tabs that are not opened stay closed.
- A second session on the same database. One click opens another connection to where the tab points, for the times two sessions are the point.
- A baseline per tab. Each tab remembers the text it started from and shows what has changed since as a line diff, live while you type, with the caret scrolling the nearest change into view.
- Compare with something else. Show changes can measure the tab's text against the code folder or the next environment instead of its own baseline, one at a time.
- Deploy one script object from the editor. A tab holding one
CREATEfor a view, procedure, function or trigger has Deploy to… listing its own source and every mapped counterpart. A folder target works on every engine; a database target is SQL Server only, because verifying the write means reading the stored definition back. - Unsaved work is not lost. A tab asks before closing something unsaved or inside a transaction, and a closed tab goes to a searchable trash it can be restored from.
- Query history. Every execution is recorded per connection — the text, the connection (redacted), the time, the elapsed and the row count or the error — searchable across connections and grouped by day, with Enter opening one in a new tab.
Running queries and results
- Execute, execute selection, execute the statement at the caret, and parse only. Parse-only checks a script without running any of it.
- Cancel reaches the engine. An attention for SQL Server, an interrupt for SQLite: a cancelled read stops on the server, not only in the client, and the connection stays usable.
- One execution at a time per tab. A second execution or a reconnect is refused with a reason on the status line, and a failed run never wedges the tab.
- A session that behaves like a session. Transactions, temporary tables and
USEpersist between executions on the same tab, because one tab is one server session. - Messages as they arrive. A
PRINTbefore a long statement, orRAISERROR(…, 0, 1) WITH NOWAITfrom a script reporting its progress, reaches the Messages pane the moment the server sends it rather than when the run ends. SQL Server, PostgreSQL and MySQL all deliver mid-run messages; SQLite sends none. - Errors point at the line. An engine error is a message with its line mapped back to the script — offset by where an executed selection starts — and selecting it moves the caret. MySQL carries the engine's own error number and SQLite its result code.
- Every result set, as it finishes. Each set is put on screen as it completes, with rows published while the run is still going and a live row count for a set still filling.
- Per-set header lines.
Result 1 · 1,234 rows · 0.42s, and12 of 1,250 rowswhile a filter is on. - Two result layouts. One tab per result set, or all of them stacked in one column.
- Row counts and timing. Per-statement "(n rows affected)" as SSMS prints it, the elapsed time per set, and the total count of a set even when the display was capped.
- A row cap that does not lie. Rows past the cap (100,000 by default, 0 for all) are
counted, not kept, and the reader keeps reading so the count is right and the connection stays
clean; the summary says
n kept, cap m per set. - Transactions are visible.
Tran: nin the status bar, in the warning colour above zero, and closing a tab with an open transaction asks whether to commit or roll back. FOR XMLandFOR JSONreassembled. A streamed document chunked across rows is concatenated back into one cell, as SSMS does.- DDL you run is attributed to you. A script that changes the schema refreshes exactly what it touched and records the change in the feed as this session's.
- Execute report. A script with a
-- ##QR:line is a report: it runs with every row kept whatever the tab's cap and writes one Excel workbook with a sheet per result set. Directives name the file, date it, freeze the header or a number of columns, set a column's Excel number format and name a sheet; a first column called##PAGENAMEnames its sheet from the data instead and is not written. A directive it cannot read is reported among the tab's messages and never stops the report.
The result grid
- A drawn, virtualised grid. No control per cell, and only the cells that changed are repainted, so a hundred thousand rows scroll at full speed and a selection dragged over a remote desktop does not redraw the whole pane. Sixty columns behave the same way.
- Selection. Click a cell, drag a block with auto-scroll past the edge, Shift+arrows to extend, Ctrl+A for everything, a header click for a column, the row gutter for a row, the corner for all of it. Several disjoint blocks are written out as one aligned rectangle with the gaps treated as nulls.
- Columns. Widths measured from a sample of the values, draggable, double-click or Fit column width to auto-fit, and draggable to reorder. Density is Comfortable or Compact and the result font size is its own setting.
- Cell kinds. Text, numbers right-aligned, booleans, binary as hex, and truncated values marked as such so you know to open the cell.
- NULL is never a string. A real null is drawn as
NULLin a muted colour, distinct from the text "NULL"; copy and export write the full value, and each format spells null its own way — an empty delimited field, JSONnull, an empty Excel cell,xsi:nil="true"— while a string that readsNULLis quoted so the two cannot collapse into one. - A row filter per result. Ctrl+Shift+L or the funnel button: a column picker and a text box
matching what the grid shows, so typing
NULLfinds real nulls. Copy and export then cover the filtered rows. - The cell viewer. Enter or a double-click opens one cell in its own read-only tab, as text, JSON or XML, with line numbers.
- Copy. Ctrl+C for TSV and Ctrl+Shift+C for TSV with headers.
- Nine formats, one picker. TSV, CSV, JSON, XML, SQL, a Markdown table, a Confluence/Jira wiki table, fixed-width text and Excel, with headers where they apply and meanings where the result has coded columns. Copy as… offers everything but Excel; Save as… adds it and a file picker.
- CSV written for a spreadsheet. A cell Excel or LibreOffice would evaluate gets a leading apostrophe; a negative number keeps its sign; a lone carriage return is quoted so it cannot end a row mid-cell. Separator and quoting are settings. TSV, the clipboard shape, is left verbatim, because a paste is expected to be exactly what was copied.
- Copy as SQL. The selected rows as a script that runs elsewhere:
CREATE TABLEwith the real column types, then the rows batched a thousand to a statement, written in the connected engine's own quoting, literals and type names. - Script as DELETE. Where the product knows which table a result came from and what keys it,
the row menu writes one
DELETEper selected row into a new query tab. Nothing is ever executed from the grid. - A menu that follows the pointer. Right-click a cell, a column header or the row gutter and the menu is the one that region has verbs for: the cell viewer, Copy, Copy with headers, Copy as…, Save as… and Select all in the body; Select column and Fit column width on a header; Select row and Script as DELETE on the gutter. The menu key opens it without a pointer.
- Show meanings. For columns a knowledge pack describes, the grid draws what each code means
beside it (
3 · Posted) while the stored code stays the cell's text; the header tooltip carries the column's description, and a cell tooltip explains one value whether the toggle is on or not. It is SQL Server only, because it needs to know which table each result column came from, and that needs the parser. The same pack knowledge in the editor works on all four engines. - Read-only, and it does not sort. The rows cannot be typed into and a header click sorts nothing. In-place editing was removed deliberately and will come back as a separate row editor.
- Whether a result could be written back is still answered. For SQL Server the product says
yes only for a single
SELECTover one table with a primary key whose key columns are in the result, and names which of those a result failed. PostgreSQL, MySQL and SQLite each refuse in one sentence: knowing which table a column came from needs a grammar they do not ship.
Export and import
- Three export shapes. A clean folder of
.sqlfiles, an SDK-style SQL project with a.sqlproj, or a.dacpac. The clean folder works for all four engines; the project file and the package are SQL Server's. - One file per object.
{Schema}/{ObjectType}/{Name}.sql, with each engine's own type folder names, and a table's constraints, indexes, triggers and extended properties inside the table's own file after the engine's statement separator, in a deterministic order so two sources that enumerate children differently never diff as changed. - What stands in the schema segment. SQL Server and PostgreSQL write the object's real
schema; MySQL writes the database it was read from, because a database is what a schema is
there — so an export of
App_UATcompares equal to a database of that name and not toApp_PROD; SQLite writesmain. - An export reads back equal. The folder reader is the same code path the comparison uses, and the round trip — read a database, export it, read the folder back, compare — is asserted for every engine that has a server.
- Reading a folder without a parser. PostgreSQL, MySQL and SQLite read a clean folder or a dump file off the dialect alone: the dialect says where a statement ends, the engine's own type table says what is a table's child, each object keeps the file's own text including its comments and terminators, and the path supplies the schema a script leaves out. A file whose statements name nothing is reported rather than silently skipped, because next time it would read as a deletion.
- Names a file system refuses. Separators, colons and forbidden characters become underscores, a segment of only dots becomes underscores, every path is checked against the output root so a crafted schema name cannot write outside it, and two objects whose names sanitise to one path fail the export rather than overwriting each other.
- Warnings are not failures. An export reports separately whether the artifact was written and whether the source could supply every object, so a folder written without one object is not called a failed export.
- A buildable project. The
.sqlprojwritten isMicrosoft.Build.Sql, which globs**/*.sql, so the project file plus a clean export builds as soon as it is written. Data files are excluded from the build. - An export tab with a log. Source, destination, format (with the unavailable formats disabled and explained), progress with cancel, a problems list and a line-by-line log.
- Open a spreadsheet as a grid. A CSV, TSV or Excel file opens in a data viewer with a
worksheet picker, tickable column headers and row selection, a live SQL preview and a
CREATE TABLEtoggle; Open in query builds the whole script off the UI thread. - Reading a CSV the way it costs data. A
"opens a quoted field only at the start of a field, so an inch mark (5") does not swallow every delimiter up to the next quote and merge the rows; rows end on LF, CRLF or a lone CR alike. - CSV to a table, carefully. A parsed block becomes
CREATE TABLEplusINSERTs with bracketed names and a numeric suffix on a repeated header. A column is typed as a timestamp only when its values really carry a day, a month and a year — a column of month names is not rewritten into this year's dates — and numeric cells are re-emitted from the parsed value, because1,250insideVALUESis two columns. - File dialogs start where you keep things. Separate folders for SQL scripts, exports and reports, globally or per workspace.
Schema compare
- Any source against any source, within one engine. A database, a folder, a SQL project, a
.dacpacor a dump file on either side. - Four levels of detail. A summary, the list of differing objects (the default), the changed hunks, or word-level spans inside changed lines.
- A diff you can walk. Each side has a gutter with the line number and a
+,−or~glyph in the diff colours, with row backgrounds and changed spans as reinforcement so colour is never the only cue. F7 and Shift+F7 step between changes; the header saysChange 2 of 5; Ctrl+C copies the visible rows as a valid unified diff. - Folds. The diff holds the whole object and folds the unchanged stretches into one row saying how many lines it hides; open it a step with the chevrons, Enter or Space, or all of it with Ctrl+Enter. Whole object opens every fold, and fold state survives a recompute.
- A change map. A narrow strip beside the scrollbar with one mark per change at its position; click a mark to go there. One shared horizontal scrollbar shifts both halves together.
- Layout normalisation. For SQL Server both scripts are re-emitted in one layout before the line keys are built, and the comments are put back where they were, so two scripts that mean the same thing compare equal; where the generator rewrites something beyond tolerance, or a script does not parse, the object is kept as written and the result says so by name. The other three engines have no parser, so this is a stated no-op for them — said once per run, not left to be discovered. The lexical rules still apply, but two differently laid out scripts will compare as changed. It matters less than it sounds between two databases, because those engines print their own parse trees back; it matters for a hand-edited checkout.
- Ignore rules. Whitespace, comments, keyword casing, identifier casing, identifier quoting,
semicolons between statements, trailing blank lines, fill factor, index options, filegroup
placement, identity seed, extended properties, permissions, a synonym's target database, column
order, default schema,
ANSI_NULLS, quoted identifiers andWITH NOCHECK— each set or cleared by name, with the ones only the deployment engine honours listed in the result's notes rather than silently dropped. --exact. Layout normalisation and every lexical rule off, so whitespace, casing and semicolons count.- Presets. A named set of ignore rules and options, applied by name, saved from the Compare tab's options panel or kept in the configuration.
- Filters. By name (substring, or an anchored wildcard with
*and?), by schema include or exclude, and by object type include or exclude — so users, roles and permissions can be left out of every comparison. An index is judged by its table's schema, and a filter keyed by identity tells the schemaappfrom the userapp. - A stated scope. The default compares every object on both sides. A subset scope makes the folder's objects the working set and reports objects outside it as out of scope rather than as added or removed, while the summary still counts them. A folder's project file can declare itself partial, which selects that scope by itself.
- A scope banner. Every comparison says in one sentence what it compared and what it left out — "N objects exist only in the database; a sync would drop them" — and a compare, a dry run and a sync over the same pair all say the same thing.
- Two engines, cross-checked. For SQL Server,
--verifyruns the deployment engine's own comparison beside the text one and footnotes every object the two disagree about, with the reason where it is cheap: comments and layout, a synonym target, identifier casing, out of scope, a deployment option. PostgreSQL and MySQL plan over the same comparison the text engine ran, so there is nothing for them to disagree about; SQLite has no deployment engine and refuses the flag. - Grouped, counted and ticked. The Compare tab groups objects by type with a tri-state tick per group, marks an object only on the left "To add" and one only on the right "To remove" — always from the target's point of view — and says when a ticked object's dependencies are not ticked, with an Include button. Equal objects are listed but never tickable.
- Summary chips that filter. All, Modified, To add, To remove, Equal, Warnings, plus a name filter box.
- A comparison that fills as it runs. Objects are computed in parallel and arrive in identity order, so the list fills and the percentage moves while the comparison runs.
- Stale rather than cleared. Changing a source marks the result stale instead of throwing it away, and the summary names the pair the rows belong to. A swap button and Ctrl+Shift+S exchange the sides; F5 compares.
- Why Compare is disabled, when it is. "App_UAT is not on @prod (databases: App_PROD).", "@prod is unreachable." — named before the run rather than discovered during it.
- It says when a side is old. A warning when an inventory is older than the poll interval, and another when the destination changed since the comparison read it, naming who changed it.
- Removals start unticked after a source error. While the source reported load errors every drop starts unticked and withheld, with an explicit per-review checkbox to allow them.
- Merge lines between two scripts. For a view, procedure, function or trigger the two scripts are shown line by line as they are really written, and each change carries take ◀ and take ▶ (Space and Shift+Space). Write to … writes the chosen changes into that side alone: a folder file gets its own layout, comments and line endings back; a database gets one statement in one transaction, read back afterwards to prove it took.
- Compare one object. From the explorer's Diff with … or a counterpart badge, landing on that object rather than on a list to scroll.
- Duplicates are an error, not a winner. An object defined twice in one source is an error row naming both origins, and neither definition enters the comparison.
- Snapshot files. What a source contained at one moment, in one portable JSON file: the source's redacted name, the engine, when it was taken and how long it took, the comparison flags the hashes were taken under, and one entry per object with its hash — and its script when asked for. A file from a newer format version is refused rather than half-read.
- Drift checks. A source against a snapshot file or against another source, reporting each
object as unchanged, added, removed or modified from the baseline's side, with a watch mode that
re-checks on an interval.
--fail-on data-lossattaches the deployment engine's data-loss warnings, so it needs an engine that has one. - Drift reports. JUnit XML, SARIF 2.1.0 or Markdown, for a CI job to publish, naming the branch and commit a folder was checked at.
- Reformatting is not drift. An object's hash is taken over exactly the lines the comparison would have compared, so two scripts hash alike precisely when the comparison calls them equal.
- Inventory cache. A read inventory is kept as a zip so a session starts from it instantly, and once the schema stamp has moved only the objects the delta names are re-read. The cache key never reaches disk — the file is named by a hash and carries a redacted label — and two equivalent connection strings share one entry.
- A comparison settings file. SQL Server only:
plan --scmpplans from an.scmpthe engine's own tooling wrote, and--save-scmpand the Compare tab's Export .scmp write one.
Schema sync and deployment
- Plan, then publish. A plan is built and reviewed, and publishing applies that plan — nothing is compared or read again in between, so a source that changed while the plan was on screen cannot change what runs.
- A plan you can read. The change list, the engine's warnings, what stays unchanged, what was withheld and why, the implied changes with their reasons, and the exact script that will run.
- Dry run.
swcli plan, or Preview in the Compare tab: everything the sync would do, with the deployment validated, nothing applied, and the script opened in a query tab. - Targets. A database, a folder of SQL files, a SQL project folder or a
.dacpac. - Which engines deploy. SQL Server (through DacFx), PostgreSQL and MySQL, to a database or a
folder. SQLite has no deployment engine, so
syncis refused for it whatever the target — the way to write a SQLite schema out isexport— and the Compare tab's Sync selected, Preview and script buttons are disabled with a reason. - Transactional by default. For SQL Server and PostgreSQL the whole deployment — the pre- and post-deployment scripts included — runs in one transaction, so a failure anywhere leaves the target as it was.
- An engine that cannot roll back says so first. Every MySQL DDL statement commits as it runs.
The sentence rides on every plan, dry run, confirmation and generated script header, with the
statement count beside it because that is the size of the risk;
--transactionalis refused as a usage error rather than accepted and ignored; and a run that fails part-way reports how many statements were applied and committed, the last one that succeeded, the one that failed, and the rest as a script to run once the failure is fixed. - Data-loss guards. A dropped column, a dropped table or a narrowed type is a data-loss warning
on the plan before the publish. Without
--allow-data-lossthe deployment asks whether the affected table actually holds rows and refuses only when it does, with nothing applied. PostgreSQL puts its guards in front of every change, so even a run without a transaction changes nothing before it refuses. - Target-only objects are kept. With drops off — the default — objects that exist only in the target are withheld from the plan and listed, together with the flag that would drop them.
- Deletes withheld on a source error. While the source reports load errors every drop is withheld and listed separately, because an object that failed to load looks deleted.
- Dependencies. The objects a partial selection needs but did not include are listed with the reason — "because of [dbo].[V]", or "…and 2 other change(s)" — and the policy is ask, always or never. Never drops every change that needs one, repeatedly, until the remaining set stands on its own, and says which it dropped. An object the engine keeps in anyway is a warning on the plan, so a filtered sync never leaves a folder that does not build.
- Per-object selection. Ticked rows in the Compare tab,
--filter, or--interactiveasking yes or no per object. A selection is keyed by identity including type, so a schema and a user of one name are decided apart, and a table's decision carries its constraints, indexes and triggers. - A confirmation you can read. Docked inside the tab, not over the window: the engine's warnings, a consent checkbox for dependent objects, the change list as the only part that scrolls, the count of what is not selected, the git working-tree refusal for a folder destination, and a type-the-environment-name box for a blocking one — plus a Script button for the exact text.
- A
.dacpactarget is whole. It is rebuilt entirely, so any filter at all is refused as a usage error rather than quietly producing an unfiltered package. - Backup first. SQL Server can back the target up before the first change. PostgreSQL has no statement that does it, so the option is refused with a blocking warning rather than ignored.
- Pre- and post-deployment scripts. Read when the plan is built, so a missing file stops the plan and not the publish, and written into the script as batches around the deployment, inside the same transaction as the changes. What the script file holds is exactly what runs.
- Migration files.
--as-migration <folder>writes the plan's script as one versioned migration for Flyway (V{n}__{description}.sql), DbUp ({yyyyMMddHHmm}_…) or grate (up/V{n}__…), with a header naming the tool and version, the runner, the two sides, the moment in UTC and the SHA-256 of the body — taken with LF endings, so the same plan yields the same digest on any platform. A file of that name is refused rather than overwritten, the header says whether the script opens its own transaction or leaves it to the runner, and a plan with no changes writes nothing and leaves no folder behind. - Nothing is created before the source resolves. A missing folder target is asked about only after the source has been read, and a preview removes the folder it created on the way out.
- A folder sync keeps the file's shape. A changed object's statement is replaced inside the file that holds it, so a hand-written header comment and the batches around it stay as they were; a dropped object's batch is removed from a file that defines several, and the file is deleted only when nothing is left; a standalone child is rewritten in place; a new object gets one new file at the layout path, so a synced folder and an exported one agree.
- A folder is a mirror. What the source no longer holds leaves the folder whether drops were asked for or not, because nothing else would ever make the two agree.
- Unparseable target files are reported, not skipped. A file the engine cannot read would make its objects look absent and be written a second time, so the sync refuses until it is fixed or the failures are explicitly accepted.
- PostgreSQL's DDL, phase by phase. Triggers, indexes, constraints, views, routines, tables, sequences and schemas leave; then schemas, sequences, tables, columns, constraints, indexes, routines, views and triggers arrive — and within a phase in dependency order. A cycle is refused by naming the objects in it rather than broken by dropping something the plan never showed.
- What PostgreSQL rebuilds, and says it is rebuilding. A view whose column list moved, a materialized view, a routine whose return type changed, a stored generated column whose expression moved, and every view over an altered table — each one a warning, because being rebuilt is not nothing.
- What PostgreSQL will not pretend to do. Column order, because it can only append a column; and a partitioned table, because the catalogue prints no partition clause. Both are left alone with a blocking warning rather than deployed wrongly.
- MySQL writes the engine's own words back. A view, routine, trigger or event is deployed as the
statement
SHOW CREATEprinted for it; a table's columns and keys are added and rewritten exactly as the source declares them, down to theAFTERthat puts a column where the source keeps it — which is what makes a second run find nothing. - MySQL orders by the foreign-key graph, the only dependency its catalogue records: what goes leaves first, tables arrive after the tables they point at, and cross-table keys go in once every table is there. A ring of foreign keys is refused by naming the tables in it.
- MySQL never drops the database it is deploying inside, and the plan says so rather than offering to.
- Data, not schema, left alone. A MySQL table's
AUTO_INCREMENTcounter and an event's server-setSTARTStimestamp are reported as left alone, with the reason, rather than deployed into a statement that would never settle. - A body with statements of its own is fenced. MySQL routines, triggers and events are wrapped
in
DELIMITER $$ … $$in the generated script and handed to the executor the same way, so one statement arrives as one batch. - Deploy one script object. A view, procedure, function or trigger written on its own, with the
given script as the unit of work and no deployment engine in the path. To a database (SQL Server):
the verb is rewritten to what the server takes —
CREATE OR ALTER, plainALTERon a server older than SQL Server 2016 SP1,CREATEwhen the object is not there — the statement runs in one transaction, and the definition is read back and hashed to prove it took. To a folder (any engine): the object's own batch is replaced inside its file, ignored copies are left alone, the working-tree gate is per file rather than per tree, and the folder is read again afterwards so the verification is what a reader will see. A script that creates nothing, creates something else, or is declaredWITH ENCRYPTIONis refused. - Readable failures. Every distinct message down the exception chain, never a stack trace, and every refusal is a field on a result rather than an exception.
Data compare and sync
- Rows, for the tables that matter. Schema compare says nothing about rows; a data set names the lookup and reference tables whose contents should match, and nothing is compared unless it is named.
- Which engines have it. SQL Server, PostgreSQL and MySQL. SQLite has neither half of the
capability, so
swcli dataand the Data compare tab refuse a SQLite source rather than reading half a comparison. A.dacpacand a dump file are refused on any engine: they hold no rows. - Keys, not "all columns". Two rows identical in every column cannot be told apart, so each table
is matched by a key: the explicit key the run names — which outranks the primary key, because a
natural key sometimes means more across environments than an identity value — else the primary key,
else a unique index that is enabled, unfiltered and all-
NOT NULL, with ties broken so two runs never pick different indexes. A table with none is refused by name with the way out, and the other tables still compare. - The key comes from the source and must exist on the target, column for column. It is not re-inferred on the other side.
- A refusal is per table. Anything a read cannot complete — the cap, a missing table, a key that turns out not to be unique — refuses that one table; nothing on a result throws.
- Shared columns only. Computed columns, row versions and — unless asked for — the large types are
left out, and a column only one side has is noted rather than making every row look changed. Which
types count as large is the engine's own: PostgreSQL's
textis an ordinary string and is compared, and an array or composite column is left out entirely rather than compared wrongly. - One equality contract. Two values are equal exactly when their normalised text is: every integer
width to one decimal text,
decimaltoG29so1.500and1.5agree, floats toG15because the last bit is not a change a user made, timestamps fixed-width, offsets converted to UTC, binary as hex, and NULL as a marker no value can produce — so NULL,''and the stringNULLare three different things. - The column's collation, not the client's. Case and accent sensitivity come from the column's own
collation on all three engines, the source's deciding when the two sides disagree and noted once per
column, and a side that cannot tell takes the other's. Trailing-blank padding of
charand Unicode normalisation forms are stated gaps. - Hash first, then cells. Rows are hashed as they stream in; a matched pair is called changed when the hashes differ, and only then are the values walked to name the columns. Column order never affects a hash, so two sides that enumerate their columns differently still agree.
- Hash-only mode. Keys and hashes only, at ten times the row cap, answering "these keys differ" with no cells and no script.
- A row cap that fires during the read. The reader stops at the cap and names the table, the cap and the two ways out. It is 50,000 by default and can be set per table.
- A diff from the target's point of view. Rows the target lacks, rows only the target has, and rows whose cells differ with the differing cells named, in key order so the same diff always produces the same text.
- Three properties every engine's script keeps. Rows in key order, so a file in git does not churn; deletes taken from the diff rather than "everything this batch does not mention", which batching would turn into a mass delete; and idempotence — applied twice, nothing moves the second time.
- SQL Server's statements. One
MERGE … WITH (HOLDLOCK)per batch of upserts and a separate batchedDELETE, with anEXCEPTprobe that treats NULL as equal to NULL so an unchanged row is not rewritten,IDENTITY_INSERTonly where the table has an identity column and there are inserts, and typedCASTs so an all-NULL column is not typed asint. - PostgreSQL's statements.
INSERT … ON CONFLICT (key) DO UPDATEwith anIS DISTINCT FROMguard naming the updated columns,DO NOTHINGfor a key-only table,OVERRIDING SYSTEM VALUEwherever an identity column is written, and asetvalon its sequence afterwards so the next default insert does not collide — which is what this engine has in place ofIDENTITY_INSERT, and what lets a folder rebuild a database rather than only fill it. An explicit key that no unique index backs compares but does not sync, becauseON CONFLICTneeds the index, and the server says so. - MySQL's statements. An insert-only
INSERT … ON DUPLICATE KEY UPDATEfollowed by anUPDATE … JOIN (VALUES ROW(…))carrying a null-safe<=>guard in itsWHERE, where it is read once before anything is written — so a row whose compared columns already match is not touched, no trigger fires for it and noON UPDATE CURRENT_TIMESTAMPcolumn moves. Nothing is cast, because MySQL'sCASTdoes not reachvarchar; the types go in the data file's header instead. - MySQL's "schema" is a filing label. A MySQL connection already stands in one database, so the schema a data set names only decides which folder the rows are filed under — a run that names none at all still compares and still round-trips through a folder.
- Table order from the foreign keys. Upserts parents first, deletes children first. A cycle is refused with its tables named rather than "solved" by disabling constraints, and an engine whose catalogue records no foreign keys leaves the tables in name order.
- One transaction per run, so a sync that half-applies cannot happen. MySQL has no
XACT_ABORT, so there the runner stops at the first error and the connection closes without a commit. - A delete policy that defaults by direction. Deletes are on for a database source and off for a folder source, because a folder-to-database sync that deletes rows the folder lacks is also how someone loses rows they inserted on purpose.
- The script is a document. It is written to be read before it runs; the Data compare tab opens it in a query tab and the sync applies the script that was reviewed.
- Rows in the folder, beside the schema.
{Schema}/Data/{Table}.sql, one file per table, in the schema folder next to the object folders. A script of typed literals rather than CSV or JSON, so types and NULL survive, it reviews in a pull request, and it runs by hand or as a post-deploy step. The set of tables is the set of files; there is no manifest. The folder nameDatais reserved so no object type can claim it, and no schema reader ever sees a data file. - Read back strictly. A data file that is not in the exact shape the writer emits — another statement, another table, a value that is not a literal — is refused by name with the sentence that it must be regenerated. It is never parsed heuristically and never silently ignored.
- Data export. Any source's data set written into a folder, so a git checkout can rebuild the lookup tables as well as the schema.
- The Data compare tab. The source's tables with the configured ones ticked; one table compared at a time so the list fills as the run goes; per-table state, the key and where it came from, and inserted/updated/deleted counts; an inline retry that takes an explicit key for a refused table; chips and a name filter; and the selected table's rows as a grid with the key columns first, the changed cells tinted and a was: … tooltip on each.
- Save as data set. The ticked tables written into the folder's project file when a side is a folder
and into the user's own configuration otherwise, saying which file it wrote — so the next tab and the
next
swcli datarun read the same set. Writing the same set twice writes the same file, and a table the caller never offered is left alone. - Progress and cancellation. Reading (with a row count as it goes), comparing, scripting, writing and applying are each reported, and cancelling reaches the engine.
Folders, projects and git
- A folder is a first-class source. It compares, exports, syncs and is read for the editor's completion exactly as a database is.
- Clean or project. A clean folder is
.sqlfiles and nothing else; a project folder adds the engine's own project file. A clean folder never gains one, even as a sync target, and a project folder's.sqlprojis read but never modified. - A committed project file.
svalefelt-sql.jsonin the root of a schema folder holds what a team shares: the engine, the SQL version, the partial marker, the preset, the ignore rules, the filters, the comparison options, the compare mappings, the environments, the knowledge packs and the data set. Never a connection string and never a secret. - Project settings union or replace, and never weaken. The project replaces the preset, ignore rules, filters, options and SQL version; it unions mappings and environments; and where both sides define an environment of one name the stricter write policy wins. A committed file can add protection but never remove it, and an environment it introduces can never turn change attribution on.
- Discovery. An explicit file, else the file in the target folder, else in the source folder, else
walking up from the current directory to the first directory holding
.git. Two conflicting files produce one warning and the target's wins. - A dirty working tree refuses a folder sync.
git statusbeneath the target folder decides — untracked files count, and a checkout dirty somewhere else is not that sync's business —--allow-dirtyis the override, and the Compare tab puts it in the confirmation. A dry run never asks git, and a sync with no changes never does either. - Ignored files are not part of the managed set. An ignored
.sqlfile is never read, never compared, never reported as target-only and never deleted. A tracked file is never ignored, whatever.gitignoresays. - The sync can commit.
--commit "<message>"stages everything beneath the folder and commits it after a successful publish, with the commit id on the result. A commit message on a folder that is not a checkout is refused before anything is written, and a commit that fails afterwards is reported without failing the sync. - Branch and head, read without running git. The branch, the head commit and whether it is detached
are read from
.gitdirectly — packed refs and linked worktrees included — so a poll can ask. The uncommitted count costs a git process and is read only when a user asks for it. - A folder follows its checkout. Its freshness stamp is the head commit plus the index file's modification time, two file stats, so a checkout, a pull, a commit or a staging is picked up within one sweep.
- Nothing in the product checks out, pulls or stages. Git facts are shown; the only write is the optional commit after a sync.
- Reports name the branch. A drift or snapshot report says which branch and commit it was checked against.
The knowledge packs
- No pack ships with the product. Knowledge packs are a format, a loader and the tooling to author, validate, install and edit one. The packs describing real systems live outside this repository, so what the product gives you is the ability to read and write them, not a library of them.
- What a system's tables and columns mean. A pack is a folder of hand-written JSON that gives tables and columns descriptions, units and code lists, so a database whose names are opaque explains itself. A real one runs to a hundred and forty files.
- Where they surface. Explorer tooltips, editor hover cards, the completion list, the caret's status
field, the object and column inspector, described relations in the join dialog,
swcli knowledge explain— all on every engine — and Show meanings in the result grid, which is SQL Server only. - Codes. A column's codes can be a static list or a read-only query against the system's own
description tables; completion after
=orIN (offers them, and a literal compared with a coded column is explained as3 = Posted. - Dictionary queries. A pack can read the installation's own field dictionary — a table of fields with labels and the lookup each column uses — so the descriptions and code lists are that installation's rather than a snapshot. Every query in a pack passes the write detector before it runs, and a pack is never written to a database.
- Parameters. A pack declares parameters such as a language or a company; the configuration, a connection or the project file sets them, each layer overriding the one under it, over the pack's own default. A value goes into a query as a quoted literal, never as raw text, so it is a setting and not a way to write SQL.
- Nothing is resolved at startup. A pack that recognises its system with a query waits until the database is opened, and a filter that opens many rows at once reaches no server.
- Relations a pack describes. A database that declares no foreign keys can have its relations described in the pack, and the editor offers them beside the declared ones, marked … (described by ledger) — a suggestion a reader cannot trace is worse than none. A declared key wins where both describe the same fact.
- Conditional relations. A described relation may hold for some rows only — the type column beside a
key that points at a different table per type — and the condition follows into the join, the
ONclause, the inspector andswcli relations:on r.SubjectId = i.Id and r.SubjectKind = 1. - Only what the database has. A described relation is offered only where both its tables are in the inventory and every column it names is on its table, so a pack covering several versions of a system never suggests a join this database cannot make.
- One file or a folder. A pack folder, a
.knpackarchive or a plain zip are read the same way, andknowledge export -o ledger.knpackwrites one. - Where packs are looked for. The application's own
knowledge/folder, the configuration root's, and any folders the configuration adds. Which packs apply is set globally, per connection or in the project file, and a detection query answers when no layer names any. - Install from the application. Settings lists every pack it can see — what it holds, where it came
from, and why one was refused — and installs from a
.knpack, a zip or a folder. A pack that cannot be read is refused before anything is copied, one of the same name asks before it is replaced, Remove takes away one that was installed, a pack that came with a build or is named in the configuration is listed but left alone, and changes apply without a restart. - Edit a pack in place. Edit opens the tree of files the pack is written in and an editor for the one picked, and saves only the file that changed, so the author's names and groupings survive. The whole pack is loaded as it would be afterwards before the disk sees anything, and a change that would not load leaves its reasons on screen and the file untouched.
- Check a pack against a database. Check marks every table and column the pack names that the
database does not have; Check codes reads what a column actually holds — one read-only
SELECT DISTINCTat a time — marking codes the column never carries and reporting values the pack does not name. - From the command line.
knowledge checkvalidates a pack folder or archive,knowledge listsays what each query contributed to a database and which parameters are in effect,knowledge exportwrites a pack, andknowledge explain @uat/AppDb dbo.Invoice.Status 3answers the question. - Exporting from a database's own metadata — extended properties and description tables — is SQL Server only. Exporting a pack's live answers as static files works on any engine.
- A table whose name a file system cannot spell — a dot inside it, a forbidden character, letters outside ASCII — is described by a file that names its schema and table itself, so a pack reads the same on another machine.
Language help and the primers
- A shipped reference per engine, offline. T-SQL's is about a thousand entries condensed from Microsoft's documentation; PostgreSQL's is 185 statements in 87 KiB from its own manual; SQLite's is 227 entries in 18 KiB — 122 functions, 74 pragmas and 23 statements. MySQL ships none, because its manual is not ours to redistribute.
- What each pack does not cover. PostgreSQL documents its functions as tables rather than pages, so those show only their names; SQLite documents grammar as railroad diagrams, so its statements carry no syntax line.
- Licences are checked, not assumed. The importer refuses to write a pack unless the documentation checkout's licence permits it, and records that licence and the checkout's commit in the pack's header. Every attribution is on the About page.
- Help answers as the engine in front of you. F1 on a PostgreSQL tab opens PostgreSQL's own reference, not T-SQL's, and an engine this build carries nothing for says so plainly rather than explaining another engine's language.
- A Help tab. A search box over the whole reference, category buttons for browsing, and an entry page with the heading, summary, syntax, example, arguments, return value, applies-to line and aliases, plus More… opening the engine's own manual in a browser. A word with no entry gets near matches.
- On the command line.
swcli help MERGE,swcli help STRING_AGG,swcli help datetime2print the summary, the syntax, the example and the link, offline;--searchsearches the whole reference and--enginepicks which engine's to read. It opens no connection, so it answers from the shipped packs and the primers and never from a server's own manual. - MySQL explains itself out of your own server. A stock MySQL server carries the tables behind its own
HELPstatement — around seven hundred topics. Where the connection in front of you has them they are read once, held in memory, written to no file, and answered beside the primer, with the Help tab saying which answer came from where. A server whose help tables were never filled is not an error; you keep what the build carries. - Engine primers. F1 on a PostgreSQL or MySQL tab answers the question a developer actually asks — how a CTE is spelled here, what this engine's upsert is, how temporary tables and indexes are written, what happens to a name you did not quote — in about forty topics per engine, each with a worked example. They are written for someone who knows SQL and not this engine, they say what the engine does not have as plainly as what it does, and every example is run against a real server by the build, so one that stopped working cannot ship. On PostgreSQL they sit in front of the statement pages, so the grammar stays one search away.
- Reserved words come from the engine. PostgreSQL's 164, MySQL's 262 and SQLite's 147 reserved words
are read from the engine itself and checked against it by the build, so a table called
authorizationor a column calleddualis quoted rather than scripted bare and refused. SQL Server hands the word to its own parser instead, so it cannot fall behind the parser. - Snippets. Statement skeletons in each engine's own spelling, offered in the completion list. SQL Server's twelve carry tab stops you step through; the other engines' are plain text.
- Code actions work on every engine. Expanding a star, qualifying a column, adding columns and writing
a join predicate need only a table's columns, which every engine can supply from its own
CREATE TABLE. What differs is where a join comes from: a declared foreign key on SQL Server, and a knowledge pack's described relation anywhere. Nothing is ever guessed from column names. - The object inspector. Alt+F1 opens a page for an object: its identity, what the packs say it is, a synonym chain where there is one, its columns with their types, descriptions and code counts, and the relations it takes part in in both directions with Enter opening the table at the other end. The script waits behind Show script and the dependency lists behind Show dependencies, so opening the page reads nothing it was not asked for.
- A column has a page of its own. Its type and flags, the pack's sentence and unit, its code list in full, its relations, and a way back to the table.
- Dependencies — SQL Server only. What an object depends on and what depends on it, optionally transitively, from the engine's own recorded dependencies plus the foreign keys, with a table child folded onto its table. It is what the engine can prove, not everything there is: dynamic SQL and a name built from a variable are invisible, and cross-database and linked-server references are named but not resolved. PostgreSQL, MySQL and SQLite record nothing a graph could be built from, and the inspector says so rather than showing an empty list as an answer.
- Relations — SQL Server only, plus any pack. The columns that join two tables, in the order the
constraint declares them, read from
sys.foreign_keysfor a live SQL Server database. It comes from the driver-level provider, so a.dacpacor a folder has no declared relations; and no other engine declares a relation reader, so a source of theirs offers only what a knowledge pack describes.swcli relations @uat/AppDb [dbo.Invoice]lists them, each line saying which is which. - A full-text search tab. Search across the scripts of every loaded source, or names only, with the source, type, object and matching line, and a double-click opening the hit.
The command line
- One engine, two front ends.
swcliandswguishare the SDK, so a comparison, a plan and a sync mean the same thing from either. - The verb ladder.
compare→plan→sync, withexport,snapshot,drift,script,deps,relations,help,deploy,data,knowledge,alias,configandupdatebeside them. compare. Two sources, neither modified:--level summary|objects|hunks|inline,--scope,--context <n>,--ignore-comments,--exact,--verify,--script-source auto|model|raw, the five filter flags, the project flags,--jsonand--exit-code.plan. What a sync would do, with nothing applied:-o <file>,--show-script,--verify,--scmp/--save-scmp,--create,--ignore-errors, the deployment flags, the migration flags and the filter flags.sync. Source applied to target:--interactivefor yes/no per object,--yes,--create,--allow-dirty,--commit <message>,-o <file>,--verify,--ignore-errors,--allow-writes-to <name>, the deployment flags and the filter flags.- Deployment flags.
--transactional/--no-transactional,--allow-data-loss,--backup-first,--drop-missing,--pre-deploy <file>,--post-deploy <file>,--dependencies ask|always|neverand--no-dependencies. - Filter flags.
--filter <text>,--exclude-schema,--exclude-type,--include-schema,--include-type. - Project flags.
--project <file>,--no-project,--preset <name>,--ignore <flag>,--respect <flag>, on every comparison-shaped verb. export.--format clean|project|dacpac,--name,--force. An existing destination is asked about on a terminal and needs--forcein a pipe.snapshot.-o <file>, which is required, and--with-scripts.drift.--since <file>or--against <source>(exactly one),--details,--report junit|sarif|mdwith-o,--fail-on any|data-loss,--watch <seconds>.deps.--transitive,--used-by,--depends-on. SQL Server only; the other engines are refused by name with the reason.data compare|export|sync.--table <schema.table>(repeatable),--only,--key <schema.table=col,col>,--row-cap <n>,--include-large,--hash-only(compare),--delete-rows/--no-delete-rows(sync),--batch-rows <n>,--filter,--level tables|rows,-o <file>,--interactive,--create, and--dryrunondata sync— spelled without the hyphen thatsync --dry-runanddeploy --dry-runcarry.help, also spelledreference. The one verb with an alias.knowledge check|list|export|explain. Validate a pack folder or.knpack, say what a pack contributed to a database, write one out (to a folder or a.knpack), or answer what a coded value means.alias add|remove|list|show|test.addtakes--database,--engine,--environment,--auth integrated|sqland one of--password-stdin,--password-env,--password-refor--no-password. An--enginethe build does not know is a usage error naming the engines it does.config path|show|migrate. Where the files are, the effective configuration after every layer is merged, and a migration from an earlier version's files.deploy <file> <target>. One script object from a.sqlfile:--object <schema.name>,--dry-run,--allow-dirty,--yes,--allow-writes-to.update.--checkreports what is available; without it the whole install folder is replaced and the one it replaced is kept beside it as.previous. On Windows it says to unpack the archive by hand, because a running program there holds its own files open.--jsonon every data-producing verb, in one envelope shape, so a script reads a comparison, a plan, a drift check or a relation list the same way.- Five exit codes. 0 success, 1 differences (only with
--exit-code), 2 usage, 3 blocked, 4 failed. One table for every command, printed by--helpon the root. - stdout is the payload. Headers, progress, warnings, scope notes and diagnostics all go to stderr, so
a pipe gets only the answer.
--quietdrops everything but errors;--verboseadds the log. --color auto|always|never. Tables are drawn with a rounded border on a terminal and minimally in a pipe.- Sets and pairs. A wildcard source, or
--pairs <file>listing source/target pairs one per line, run with--parallel <n>and, forsync,--continue-on-error. - Prompts, and what happens without a terminal. Creating a missing target, confirming a write to a gated environment, pulling in dependent objects, overwriting an export and supplying a missing password are each asked on a terminal and are a usage or blocked exit in a pipe, naming the flag that answers them.
--config <file>. Another configuration file without moving the config, data and cache roots.- Deprecated spellings still work.
sync --dry-runisplan,scriptisplan -o <file>, andcompare --detailsis--level hunks— each accepted with a hint on stderr rather than removed under someone's script.
Settings, appearance and keyboard
- A command palette on Ctrl+K. One ranked list of actions, sources and objects, with
>,@and#to narrow it to one kind. Each action carries its real chord beside it, so the palette doubles as the shortcut reference, and recent choices persist so an empty query opens on what was last run. An action whose context is missing is simply not offered. - Ctrl+K is also a chord prefix. A second key within a configurable pause runs that command; a pause opens the palette instead.
- Light and dark themes that switch live, over one set of design tokens and vector icons.
- UI zoom and window scale. Ctrl+=, Ctrl+-, Ctrl+0 and Ctrl+Wheel; the window reflows rather than magnifying. A window-scale setting covers sessions that report the wrong one, and an environment variable overrides it for one launch.
- A rendering switch. Auto, Accelerated or Software — Auto picks the CPU path for remote-desktop sessions, where the GPU one is slower.
- Native Wayland on Linux. A setting, or an environment variable for one start, runs the window without XWayland where a compositor answers, and on X11 as before where none does. The page and the system report name the backend the running window actually got, and the scale setting says when the compositor has the last word.
- The window reopens where it was left, unless told not to. A desktop that places windows itself has the last word, and the setting says so.
- A collapsible explorer that remembers its width, with Ctrl+B collapsing it to a strip and an optional "widen while focused" that grows it to the widest visible row while the keyboard is inside it.
- Workspaces. Open tabs kept in workspaces the way a desktop keeps windows: Alt+PgDn and Alt+PgUp move between them and Alt+Shift+PgUp/PgDn take the current tab along. Each is a chip in the top bar, with always one empty one after the last in use, and an unnamed empty one goes away once it is left. Right-click to name it, colour it, keep it when empty, move it, close its tabs, delete it or open its settings. The explorer is shared; the tabs are per workspace and come back at the next start.
- An empty workspace says where to start — New query, Open .sql file, Restore last closed tab, Query history, Search, Connections, Settings — as buttons beside their own chords.
- Ctrl+Tab is a most-recently-used switcher, with Shift reversing and Escape dismissing.
- Folders for the dialogs. Where the file dialogs start for SQL scripts, exports and reports, globally or per workspace. A script opened from a file is saved next to it.
- Settings in sections. General, Workspaces and folders, Knowledge packs, Editor, Query and results, Explorer, Storage, Notifications and Keyboard shortcuts, one at a time, each labelled with the file it writes. A scope switch chooses every workspace or this one, and what cannot differ per workspace greys out.
- Editor settings. Font size, the join-alias style, the list layout code actions write, every Format Document option, the header-block template, and the Ctrl+K chord pause.
- Query and results settings. Command timeout, rows kept per result set, session autosave and restore, result font size, density, result-set layout, CSV separator and CSV quoting.
- Explorer settings. Grouping mode, collapsed state, widen-while-focused and the hover highlight.
- Cache and schedule settings. The change-detection poll interval, the full-refresh interval, whether a full refresh runs at startup, how many days of use keep a database watched, Refresh everything now, Clear cache and the cache's current state.
- Trash settings. How long a closed tab is kept, how many, whether empty ones and unchanged scripts are kept, and the folder they live in.
- Notification settings. Whether external changes raise a toast, and whether the change journal is written.
- A keyboard reference, not a rebinding surface. One page lists every chord the application has, from one catalogue that the palette reads too. Shortcuts cannot currently be remapped.
- In-window dialogs, never native ones. A confirmation, a format picker, a column checklist, a join dialog and a rename are all panels scoped to the tab or the window, answerable from the keyboard: Enter is the default, Escape the way out, Left and Right walk the buttons.
- Feedback in one place. Toasts that fade with an optional action, a per-tab problems list with severity, object name and line, a background-work line in the status bar, and progress with a cancel button — inline in a query tab's toolbar so it never moves the editor.
- An About page. The build, the commit, the configuration paths, the release notes, the provider packages compiled in, every shipped pack's attribution, a system report, and a Performance section recording what a key press, a button, a tab switch and a frame cost this session — copyable and savable as JSON, redacted of names by default.
- One configuration root.
config.json, the workspace, the per-front-end UI and session files and the secret stores under one per-user directory, with the data and cache roots beside it, each movable by an environment variable, and every path shown in Settings. - Files are written safely. Owner-only and atomically, with the previous version kept as
.bak; unknown keys survive a round trip so an older build never erases a newer one's settings; a file that does not parse is left in place, defaults are used and the next write moves it aside as.corrupt; and a file written by a newer version is refused outright rather than half-read. - One log a day, pruned after a week, with a second instance writing beside the first rather than failing on a held file, and a line the disk refuses lost rather than thrown.
- A migration from the previous version. The old alias, UI and session files are located and parsed in
full before anything is written — so one unreadable file stops the run rather than losing half the
settings — then mapped across, with each legacy file kept as
.v0.bak, so a downgrade is a rename. - The change feed. What changed in the databases being watched, when, and whether this application caused it: a tab with time, source, origin, actor, change and object, an unseen-change badge on the toolbar, toasts for changes this session did not make, and a journal that can be read, bounded or deleted outright.
- Attribution is opt-in, per environment, and SQL Server only. Off by default.
sessionrecognises this product's own windows and sessions and records nothing about anyone else;tracereads SQL Server's default trace so a change can carry the login behind it. A committed project file can never raise it, login names are shown verbatim and never mapped to a person, nothing leaves the machine, the journal is bounded to ninety days by default and can be erased, and the product creates no server object — no event session, no audit, no DDL trigger, no trace of its own — to get any of it.
Security and secrets
- A connection never carries a password. The entry holds a reference to where the password lives, and the store named by the reference's scheme fetches it on demand.
- Four stores. The OS keyring (Credential Manager on Windows, the freedesktop Secret Service on Linux), DPAPI on Windows, an AES-256-GCM protected file for headless Linux and macOS boxes, and a read-only environment variable for CI.
- An honest threat model for the file store. The key sits beside the data, so it protects against other
accounts and casual reads — a
grep, a config backup, a screenshot — and not against anything running as the same user. It is a fallback for headless boxes, not a keyring. - The keyring is spoken directly. On Linux over the session D-Bus with the specification's own
transport encryption, with no native
libsecretand nosecret-toolshell-out. Items are tagged sosecret-toolfinds them too. - Headless rules. No session bus means unavailable; a bus with no running service is activated only when a display is present, so an SSH session never gets a locked promptless daemon; and a locked collection is unlocked through the service's own prompt only when the caller opted into prompting.
- Where a new password goes. The keyring when its probe answers, else the protected file — with the probe's detail carried along, so the user is told what the fallback protects.
- Passwords from an earlier version are moved. A password still embedded in a connection string is reported on every load; the command line offers once per alias to move it, and the Connections page lists such connections with a Move to secret store action.
${NAME}in a connection string is expanded from the process environment before the password reference is resolved.- A missing secret is data, not a crash. The resolution result says resolved, missing — with enough to prompt and complete the string — or failed; the command line prompts on a terminal and exits 2 otherwise, and the desktop application asks in a dialog. A store that is locked or absent surfaces at Test with the probe's own detail rather than on the first query.
- Nothing the product writes contains a connection string. Every display name, cache key, log line,
change-log entry, session entry, workspace entry, query-history row and error message goes through one
redactor that reduces a descriptor to
server/databaseand never a password or a user name. - URLs are redacted too.
scheme://user:password@hosthas its user information taken out wherever it appears in a string, for any scheme, whatever punctuation the password holds — and while it is still half typed, so a password being typed into the source box never reaches a label, a log or a saved file. - Two strip modes, for two purposes. One removes the password and nothing else, for a string that must still connect after the password has moved to the store; the other removes every secret, for everything that is persisted or echoed.
- One driver-free boundary. The redactor uses the BCL's own connection-string builder rather than a driver's, and a build check keeps the SDK core, the application layer, the command line and the desktop host off every database driver.
- Reads are never gated, so safe mode does not get in the way of looking.
Platforms and packaging
- Linux and Windows, x64. One framework-dependent folder per operating system holding both executables and one copy of everything they share. No macOS build.
- The .NET 10 runtime must be installed. That is the price of the shared folder: a download of about 30 MiB instead of two of 70 MiB each. A machine without it gets the runtime's own "framework not found" message; there is no bootstrapper.
- One archive per platform. A
.tar.gzfor Linux and a.zipfor Windows, holding the folder's contents with no wrapping directory and written reproducibly — sorted names, no owner, a fixed timestamp — so the same input gives the same bytes. Unpack it anywhere and run either executable. - A Linux desktop entry and icon theme ship in the tarball, with a launcher and a script that installs them for the current user.
- Checksums and a manifest.
SHA256SUMSfor a human and a package recipe, and arelease.jsonthe updater reads — it hard-codes no asset name and matches an archive by operating system, architecture and provider flavour. swcli update. Reads the manifest, checks the archive's SHA-256 against it before extracting anything, and replaces the whole install folder, keeping the previous one beside it. Windows refuses and says to unpack by hand.- A bundle check in the build. Both executables must be present, the folder must stay under its size budget, and the command line must start and print its version.
- Nothing is signed, and there is no installer. Integrity between the manifest and an asset is the SHA-256; integrity of the manifest itself is TLS and the registry. There is no MSIX, no Flatpak, no AppImage and no delta update.
The SDK
- Every rule lives in the library. Comparison, sync, export, data compare, the language services and the configuration are all in the SDK; the command line and the desktop application are front ends over it, which is why they never disagree.
- Results are data. Every operation returns a record — a plan, a result, a diff, a refusal — and nothing on a result throws. A refusal is a field, not an exception.
- Progress and cancellation everywhere. Each operation reports a phase, a current/total pair where the work can be counted, and the message a status line shows; each takes a cancellation token that reaches the engine.
- One provider contract per engine. An engine package declares its capabilities, brings its dialect, and buys a set of shared contract tests that hold every engine to the same round trips — including "export a database to a folder, read it back, and find it equal".
- A tracked public API. The public surface of each package is written down in a checked-in file, and adding or removing a member without recording it fails the build.
- Tracing that costs nothing unless asked. One activity source, one activity per extract, compare, diff, script, publish and export, recorded only when a listener subscribes.
- Not published yet. The packages are versioned per package but are not on a public feed; the SDK is consumed from source today.